Skip to content

SCC26: Inline EOS and Riemann-state helpers in GPU kernels - #2095

Closed
AppleChen17 wants to merge 3 commits into
MFlowCode:masterfrom
AppleChen17:feature/inline-eos-riemann-state
Closed

AppleChen17 wants to merge 3 commits into
MFlowCode:masterfrom
AppleChen17:feature/inline-eos-riemann-state

Conversation

@AppleChen17

@AppleChen17 AppleChen17 commented Oct 20, 2026 •

Copy link
Copy Markdown

Closes #2094. Related: #2088, #2093.

Summary

NVHPC does not inline EOS and Riemann-state helpers into OpenACC kernels when they are defined in another source file, so each call is a real device call (see #2094).

This PR moves 16 EOS and 6 Riemann-state procedures into Fypp macros and expands a private copy in each module that call them from GPU kernels, so the compiler can inline them. Kernels are not restructured and no build option is added. Existing call sites and numerical formulas are unchanged, apart from one case-optimization specialization described below.

On one NVIDIA H200 (NVHPC 26.3, default build flags, --case-optimization):

  • 3D_performance_test (WENO5, HLLC): grind time −11.7%, bit-identical output.
  • 3D_IGR_TaylorGreenVortex (IGR, Lax-Friedrichs): grind time −10.5%.

This PR is from Team NTHU SCC, participating in the SC26 Student Cluster Competition.

Implementation

  • src/common/include/inline_eos_procs.fpp and src/simulation/include/riemann_state_inline_procs.fpp (new) hold the existing procedure bodies in the macros EOS_INLINE_PROCS and RS_INLINE_PROCS.
  • src/common/include/inline_eos.fpp and src/simulation/include/riemann_state_inline.fpp (new) provide EOS_INLINE_USE / RS_INLINE_USE, which rename the public procedures on use, and EOS_INLINE_PRIVATE / RS_INLINE_PRIVATE, which keep the local copies private.
  • m_eos and m_riemann_state expand the same macros for their public definitions, so each procedure still has a single source and other callers are unaffected.
  • Each of the calling modules renames the public procedures on use, marks the copies private and expands them after contains. Existing call sites resolve to the local copies.
  • s_compute_energy moves from m_variables_conversion to m_eos, and m_variables_conversion no longer re-exports the EOS procedures. m_eos makes s_eos_coefficients, f_has_isentropic_reference, s_reference_curve and s_rk4 public, because the private copies call them.

One intentional specialization: in the private copy of f_is_state_dependent, a case-optimized build of a case without a state-dependent EOS (#:if MFC_CASE_OPTIMIZATION and not any_state_dependent_eos) returns .false. at compile time, so the state-dependent branches drop out of the kernels. The public version is unchanged, and s_initialize_eos_module already aborts when a case does not match the build. All other procedure bodies are identical to master apart from whitespace.

Environment

Component Setting
MFC master at 3ef5e1e (baseline) and this PR
GPU / CPU NVIDIA H200 SXM / Intel Xeon Platinum 8480+
Compiler / MPI nvfortran 26.3-0 and 25.9-0, CUDA 13.0, Open MPI 4.1.9 (HPC-X 2.25.1 / 2.24)

Device calls and resource usage

Case-optimized 3D_performance_test binary, default build flags:

master 3ef5e1e this PR
Device CALL sites to MFC routines, whole binary 634 365
Device CALL sites in HLLC functions 140 6
HLLC kernel registers 127–159 146–148
HLLC kernel stack frame 528–576 B 48 B

Across all 486 kernels in this binary, no stack frame grows and 48 shrink. 11 kernels use more registers, and each of them has a smaller stack frame.

The remaining calls are outside the scope of this change.

Performance

NVIDIA H200 (700 W), NVHPC 26.3, default build flags, --case-optimization, unmodified examples. Mean of five runs interleaved in one allocation. Grind time in ns/gp/eq/rhs.

Case master 3ef5e1e this PR Change
3D_performance_test (1 GPU, 30 steps) 0.41568 0.36704 −11.7%
3D_IGR_TaylorGreenVortex (1 GPU, 6366 steps) 0.46764 0.41868 −10.5%

HLLC kernel in 3D_performance_test, Nsight Compute:

master 3ef5e1e this PR
Kernel duration 2.45 ms 1.42 ms
Local load sectors 326.4M 57.1M
Local store sectors 175.5M 12.2M

Numerical validation

  • 3D_performance_test: every output file of the last step is bit-identical to master, and two runs of the same binary are identical.
  • 3D_IGR_TaylorGreenVortex: master itself is not reproducible run to run. Against master, the last step (step 6366) differs by at most 2.1e-15 relative to the maximum absolute field value.

Testing

Test Result
Full suite, NVHPC 26.3 + OpenACC, H200 725 passed, 0 failed, 39 skipped
Case-optimized subset, 72 tests (./mfc.sh test --case-optimization --only <tests>) 71 passed, 0 failed; the test harness excludes Gauss-Seidel tests from GPU runs
gfortran 13.2 CPU build builds (tests not run on CPU)
Full suite, NVHPC 25.9 + OpenACC, H200: an earlier revision of this change (plain include files instead of Fypp macros, same 20 modules) on an earlier master 8551f17 726 passed, 0 failed, 39 skipped (master 8551f17: the same)

The case-optimized subset contains all 10 state-dependent EOS tests (Mie-Grüneisen, JWL, Vinet, reactive burn) and three tests each for HLLC 1D/2D/3D, HLL, HLLD, LF, IGR, 6-equation, hypoelasticity, Euler-Euler and Lagrangian bubbles, QBMM, IBM, CBC, MHD, viscous, surface tension, chemistry, low-Mach, and other affected paths.

Trade-offs

  • The main trade-off is a Fypp macro that contains complete procedure definitions. This is a new pattern in MFC, but it lets the public routines and the private inline copies share one source. We also tried a plain include file containing the procedures. It generates the same code, but MFC's include files otherwise only define macros.
  • A future module that calls these routines from a GPU kernel needs the same three-line setup (use rename, private, expansion) to benefit. Without it, the module still computes correctly through the public procedures, but keeps the device calls.
  • The private copies are also compiled for the host, where they are not called. In the case-optimized 3D_performance_test build, host code (.text) in simulation grows from 17.1 MB to 17.9 MB (+4.6%), and the whole executable, which is mostly device code, from 564.4 MB to 565.9 MB (+0.26%).
  • cmake/MFCTargets.cmake builds m_start_up, m_cbc and m_pressure_relaxation with -Mnoinline, because of NVHPC 25.x ICEs and the six-equation relaxation miscompile in Stop NVHPC IPO silently miscompiling the six-equation relaxation kernel #1878. That flag stops the cross-file IPO, but not the inlining of same-file procedures, so with this PR the EOS routines named in #1878 (s_phase_coefficients, s_phase_internal_energy, f_pressure) are inlined into the relaxation kernel, and m_cbc gets private copies too. With NVHPC 26.3, and with the earlier revision on 25.9, the six-equation and phase-change tests pass, and so do all CBC tests. #1878 reproduced the miscompile on 25.5 and 25.11, which we cannot run locally, so the CI results on those versions are the relevant check. If they fail, the private copies can be dropped from m_pressure_relaxation and m_cbc.

We are happy to adjust the implementation if reviewers prefer a different approach.

Relationship to #2093

Case-optimized 3D_performance_test, NVHPC 26.3, H200. #2093 is built with --hllc-register-tuning; "#2093 + this PR" is a local merge in which the HLLC module carries both changes, built the same way.

#2093 #2093 + this PR
Device CALL sites in the HLLC kernel 14 0
HLLC kernel registers / stack frame 114–154 / 352–400 B 126–128 / 48 B
Grind time (ns/gp/eq/rhs, mean of 5 interleaved runs) 0.31607 0.29044 (−8.1%)

Other Riemann solvers, each change built on its own (same case and compiler):

Device CALL sites #2093 this PR
HLL kernels 24 6
LF kernels 19 1
HLLD kernels 18 0

Contribution Policy

All contributions are expected to demonstrate:

  • A clear understanding of the codebase
  • Alignment with product direction
  • Thoughtful reasoning behind changes
  • Evidence of real-world usage or hands-on experience with the problem

If these expectations are not met, we would prefer to implement the changes ourselves rather than spend time reviewing low-effort submissions.

nvfortran does not inline GPU routines from another source file into
OpenACC kernels, so the EOS and Riemann-state helpers called from
modules remain device calls.

Move these routines into the Fypp macros EOS_INLINE_PROCS and
RS_INLINE_PROCS, and expand a private copy in each calling module.
Call sites and formulas are unchanged. In case-optimized builds
without a state-dependent EOS, the private f_is_state_dependent
returns .false.
@github-actions

Copy link
Copy Markdown

Claude Code Review

Head SHA: a0ee5d5

Files changed:

  • 26
  • src/common/include/eos_inline.fpp
  • src/common/include/eos_inline_procs.fpp
  • src/common/m_eos.fpp
  • src/common/m_variables_conversion.fpp
  • src/simulation/include/riemann_state_inline.fpp
  • src/simulation/include/riemann_state_inline_procs.fpp
  • src/simulation/m_acoustic_src.fpp
  • src/simulation/m_bubbles_EE.fpp
  • src/simulation/m_bubbles_EL.fpp
  • src/simulation/m_hypoelastic.fpp

Findings:

  • medium src/common/include/eos_inline_procs.fpp (f_is_state_dependent, #:if MFC_CASE_OPTIMIZATION and not any_state_dependent_eos): any_state_dependent_eos is a Fypp variable set only by the generated case.fpp (#:set any_state_dependent_eos = ..., toolchain/mfc/case.py:418). The macro is now expanded in every kernel module, and five of them do not #:include 'case.fpp': m_acoustic_src.fpp, m_bubbles_EE.fpp, m_bubbles_EL.fpp, m_hypoelastic.fpp and m_ibm.fpp. In a --case-optimization build, MFC_CASE_OPTIMIZATION is true, so Fypp evaluates the second operand and should raise a NameError for those files. Normal builds short-circuit and are unaffected. I did not run a build, so this is untested; the cause is clear from the hunks. Fix: add #:include 'case.fpp' to those five modules, or have eos_inline.fpp include it, or guard the macro with defined('any_state_dependent_eos').
  • medium src/simulation/m_hypoelastic.fpp, m_reactive_burn.fpp, m_pressure_relaxation.fpp, m_cbc.fpp, m_data_output.fpp (EOS_INLINE_USE / EOS_INLINE_PROCS): the copied routines reference sgm_eps, verysmall, avg_state_roe and model_eqns_6eq/model_eqns_5eq from m_constants. m_eos imports these explicitly (use m_constants, only: ... sgm_eps, verysmall). The macro adds only ode_isentrope and ode_reference_temperature. m_cbc and m_data_output import m_constants with a narrow only: list that lacks sgm_eps, verysmall and avg_state_roe, and m_global_parameters_common also uses m_constants with only:. m_hypoelastic, m_pressure_relaxation and m_reactive_burn now depend on an only: import of m_eos. The m_hypoelastic hunk previously imported just s_phase_bulk_modulus, so m_eos was never its source for these symbols. Confidence is moderate, because some of them may reach these modules transitively through other use statements, and I could not compile. If any are missing, this is a compile error. Fix: have EOS_INLINE_USE import everything the copies need from m_constants (sgm_eps, verysmall, avg_state_roe, model_eqns_5eq, model_eqns_6eq).

Checked in step 4: eos_inline.fpp, eos_inline_procs.fpp, m_eos.fpp, m_variables_conversion.fpp, riemann_state_inline.fpp, riemann_state_inline_procs.fpp, and the use/include hunks of the simulation modules.

test_eos_selector requires every eos_* identifier under src/ to be an
EOS family constant or a known non-family name, and it matched the
include file names eos_inline and eos_inline_procs. Rename the files
to inline_eos.fpp and inline_eos_procs.fpp. The generated Fortran is
unchanged apart from the file names in comments.
@github-actions

Copy link
Copy Markdown

Lines of Code

File Lines Diff
src/common/include/inline_eos_procs.fpp 293 +293
src/common/m_eos.fpp 253 -265
src/simulation/include/riemann_state_inline_procs.fpp 120 +120
src/simulation/m_riemann_state.fpp 1069 -116
src/common/include/inline_eos.fpp 24 +24
src/simulation/include/riemann_state_inline.fpp 15 +15
src/common/m_variables_conversion.fpp 947 -13
src/simulation/m_riemann_solver_hllc.fpp 1314 +6
src/simulation/m_cbc.fpp 1118 +4
src/simulation/m_data_output.fpp 1582 +4
src/simulation/m_riemann_solver_hlld.fpp 196 +4
src/simulation/m_riemann_solver_lf.fpp 513 +4
src/simulation/m_time_steppers.fpp 914 +4
src/simulation/m_acoustic_src.fpp 519 +3
src/simulation/m_bubbles_EE.fpp 307 +3
src/simulation/m_bubbles_EL.fpp 1642 +3
src/simulation/m_hypoelastic.fpp 637 +3
src/simulation/m_ibm.fpp 1821 +3
src/simulation/m_igr.fpp 2292 +3
src/simulation/m_pressure_relaxation.fpp 219 +3
src/simulation/m_qbmm.fpp 882 +3
src/simulation/m_rhs.fpp 1971 +3
src/simulation/m_riemann_solver_hll.fpp 610 +3
src/simulation/m_riemann_solver_hypo_hlld.fpp 786 +3
src/simulation/m_viscous.fpp 1024 +3
src/simulation/m_reactive_burn.fpp 115 +2
Directory Lines Diff
common 10565 +39
simulation 28614 +83
total 47712 +122

ifx reports error #6580 for the names in "use m_constants, only:" from
EOS_INLINE_USE when the same module also has a plain "use m_constants"
after it. Move the plain use above the macro in m_acoustic_src and
m_ibm. The accessible names, and the generated code, are unchanged.
@wilfonba

Copy link
Copy Markdown
Collaborator

@AppleChen17 This solution is too cumbersome and will not be accepted.

@wilfonba wilfonba closed this Oct 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

GPU device calls across modules cause local-memory overhead

2 participants