Skip to content

[SC26 SCC] NTU Team Supernova's GPU optimizations to MFC - #2093

Draft
aliencaocao wants to merge 2 commits into
MFlowCode:masterfrom
aliencaocao:sc26-pr
Draft

aliencaocao wants to merge 2 commits into
MFlowCode:masterfrom
aliencaocao:sc26-pr

Conversation

@aliencaocao

@aliencaocao aliencaocao commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

[SC26 SCC] NTU Team Supernova's GPU optimizations to MFC

Note: I am still running the full CI on my own hardware. This notice will be edited away once they all pass. If any fixes are needed arising from my own CI run, I will push to this PR. This PR is not ready for review yet but will be ready once my own CI runs pass. I will not be marking it ready for review myself to not flood the runner.

For the full version of the PR description, please refer the full version below, as they are too long for Github to accept.
PR.md

This PR adds GPU performance optimizations for the simulation solver and communication optimizations, mostly for NVIDIA OpenACC path. It also fixes some existing numerical bugs. Most of the optimizations are guarded via opt-in compiler argument, except for some that will cause too much duplicate code when guarded. For those, they are always-on and guarded by the relevant compiler/GPU where necessary.

If I should be adding the options as case.py arguments, please let me know too, I know that is the current pattern, but I think case.py should focous on defining case-specific options and not general performance opt-ins. This is my own decision, and I am happy to change the approach if the maintainers prefer otherwise.

This is a large PR and if the maintainers prefer to split them into smaller PRs, I can do that too, but the goal was to not flood the CI runners so I merged them into 1 single PR.

If the maintainers are satisfied with some of the lower-risk optimizations and would like to make them always-on as default, I can also remove their respective opt-in toggles by replacing the existing implementation. This reduces code duplication. A significant part of the changed LOCs are from preserving existing code paths.

This PR assumes existing bug-fix PRs by me will be merged, and I will handle any conflicts arising from other team's contributions where necessary.

In general, the direction that I am working towards to is based on the following findings:

  1. With case optimization, MFC is largely NOT memory-capacity constrained
  2. On modern GPUs like H200, MFC is heavily bottlenecked by memory bandwidth, but that is due to it doing too many round-trips
  3. Communication pattern, which is a bottleneck in all HPC applications

Thus, you will find many optimizations that trades some extra memory usage for data residency/reduced H2D/D2H trips, and maximizing on-device cache/register/shared memory reuse.

Unfortunately I do not have access to any larger cluster like Frontier to test scalability of my changes, so I want to focus more on improving compute-bound parts, and I do not want to claim anything in scalability for communication changes beyond what I have tested on.

Benchmark environment

All performance figures in this description were measured on 1-8 NVIDIA H200 NVL (141 GB) with 4-way NVLink bridge at 600W TDP · NVHPC 26.9 · CUDA 13.4 · NVSHMEM 3.8.0 · OpenACC · FP64 unless otherwise specified.

For fair comparison, all runs enabled GPU-direct MPI exchange
(rdma_mpi) to achieve optimal performance on NVLinked NVIDIA GPUs. This is by default off in the current code and this PR does not change that default.

Primary Results

Measured on https://github.com/MFlowCode/MFC/blob/master/examples/3D_performance_test/case.py 200³ performance test, 300 steps, one rank per GPU.
The tables give the median grind time of the runs at each point as reported by MFC. As reported in #2088, current master code has a significiant regression in this test case (and many others). For our hardware we expected pre-regression timing to be around 0.34ns. The regression has been identified and fixed in this PR, but the numbers below is unaffected by it because one of the optimizations prevented the regression by construction.

This PR includes an improved version of #1628. To ensure fairness and isolate geniue contributions, the number and scaling study below EXCLUDES this optimization. It also varies greatly with each case so it is unfair to base it all on the 3D performance test case. In Section 12, I separately report how the optimization further improves the performance.

GPUs unmodified original this PR (NVSHMEM) this PR (GPU-direct MPI) speedup vs original, NVSHMEM speedup vs original, GPU-direct MPI
1 0.447 0.0558 0.0559 8.0× 8.0×
2 0.236 0.0313 0.0310 7.6× 7.6×
4 0.130 0.0183 0.0179 7.1× 7.2×
8 0.0764 0.0127 0.0127 6.0× 6.0×

8-GPU runs were 2 groups of 4xH200 NVL within a single node running dual AMD EPYC 9755 CPU.

Step time in ms/step

GPUs unmodified original this PR (NVSHMEM) this PR (GPU-direct MPI)
1 86.5 8.62 8.62
2 45.8 5.68 6.14
4 25.1 3.31 3.54
8 14.8 2.53 3.15

The two transports agree on the grind within 2% at 2, 4 and 8 GPUs, however due to removal of MPI host-stall outside of the grind time region, NVSHMEM runs 8%, 7% and 25% faster at 2, 4 and 8 GPUs (§6), judging from step time. This is meaningful to total runtime which the current grind time fail to account for. More info on NVSHMEM below.

Strong scaling

image

The original code scales 1.89×, 3.44× and 5.85× at 2, 4 and 8 GPUs. This PR
scales 1.79×, 3.06× and 4.40× on NVSHMEM, and 1.80×, 3.11× and 4.38× on
GPU-direct MPI. The speedup over the original code falls from 8.0× at one GPU
to 6.0× at 8 GPUs.

image

In host step time the original scales 1.89×, 3.45× and 5.84×. This PR scales 1.52×, 2.60× and 3.41× on NVSHMEM, and 1.40×, 2.44× and 2.74× on GPU-direct MPI. The step time contains the host side of the exchange, which the device-timed grind excludes, so it scales less than the grind.

Weak scaling

The work per GPU stays at 100³ cells. The grids are 100³, 200×100×100, 200×200×100 and
200³ on 1, 2, 4 and 8 GPUs.

image

The original code holds perfect weak efficiency at our scale. This PR runs every point 6.0× to 6.9× faster in grind. Its weak efficiency is 1.12, 1.10 and 0.98 at 2, 4 and 8 GPUs on NVSHMEM. On GPU-direct MPI it is 1.13, 1.13 and 0.97.

The 8-GPU runs place the ranks in a 1×2×4 grid which is most optimized for our specific topology. The 2-GPU and
4-GPU runs and the original code use the built-in most-cubic Cartesian cuts.

The performance-test case applies characteristic boundary conditions (CBC) on all six faces. At 1 GPU the single rank need to do the CBC staging on all 6 physical faces. On multi rank, the domain is split and the cut faces become rank interfaces, which exchange halos but run no CBC staging. The slowest rank's physical-face count falls 6 → 5 (2 GPUs) → 4 (4 GPUs) → 3–4 (8, where x is unsplit). With less boundary work per rank, the weak-scaling efficiency become higher than 100% at 2 and 4.

masterstays flat at 1.00 because its per-rank time is ~6× larger, so the same boundary saving is ~1–2% and invisible.

Weak scaling at 8 GPU drops sharply partly due to the additional cross-NUMA traffic instead of NVLink, since we are not using a full-mesh node, so communication become much more bottlenecked. On H200 SXM I do not expect such a sharp drop.

Step time in ms/step

GPUs unmodified original this PR (NVSHMEM) this PR (GPU-direct MPI)
1 14.7 1.89 1.89
2 14.7 2.00 2.16
4 14.7 2.02 2.16
8 14.8 2.53 3.15
image

In host step time the original stays flat at 14.7 ms. This PR goes from 1.89ms at one GPU to 2.53 ms at eight on NVSHMEM, and to 3.15 ms on GPU-direct MPI. The grind-based weak efficiency at 8 GPUs is 0.98 on NVSHMEM, so the rise in step time is host-side work outside the device-timed region.

New Build options

Most of the optimizations in this PR are default-off and can be enabled by these new build options. Each option is a ./mfc.sh build flag and a CMake option of the same name. For example,
--fused-rhs sets MFC_FUSED_RHS=ON, and --no-fused-rhs sets it to OFF. Every option is off
by default. The toolchain (toolchain/mfc/args.py) and CMake check the requirements below and
stop with a message when any incompatible options are used, since some depends on others. Some options automatically enable the other dependent ones, unless the user explicitly passed --no-*, in which case they conflict and it will error with a custom error message.

./mfc.sh build -t simulation --gpu acc --case-optimization -i case.py --fused-rhs
flag effect requirements section
--fused-rhs Builds the fused WENO, Riemann and RHS kernel. --case-optimization, OpenACC GPU build with NVHPC §1
--hllc-register-tuning Selects the restructured HLLC paths of the classic pipeline, which developers can tune. --case-optimization, OpenACC GPU build with NVHPC §1
--cbc-no-host-sync Passes the CBC control values as kernel arguments. OpenACC GPU build with NVHPC §2
--cbc-parallel-scatter Copies the 3D CBC output with a parallel kernel. --cbc-no-host-sync §2
--async-queue Puts the step on one non-default OpenACC queue. OpenACC GPU build with NVHPC, --no-unified, --accurate-grind-time §3
--accurate-grind-time Measures the grind with device events. OpenACC GPU build with NVHPC or CCE §4
--cuda-graphs Captures the step into a CUDA graph and replays it. OpenACC GPU build with NVHPC, --no-unified, --async-queue and therefore --accurate-grind-time §5
--halo-single-round Exchanges all 26 neighbor halos in one round. --mpi §6
--halo-index-cache Builds the halo point coordinates once. --halo-single-round §6
--nvshmem Sends the halo with NVSHMEM puts. --mpi, OpenACC GPU build with NVHPC, --no-unified, the NVSHMEM library §6
--weno-direct-loads Reads the WENO and THINC input in place. --case-optimization, OpenACC GPU build with NVHPC, --fused-rhs or --hllc-register-tuning §7
--muscl-direct-loads Reads the MUSCL input in place. none §8
--direct-riemann-flux Uses the Riemann flux directly for the flux difference. --case-optimization, OpenACC GPU build with NVHPC §9
--persistent-bc Keeps the ghost-cell fill tables on the device. The tables run in builds with --halo-single-round. OpenACC GPU build with NVHPC §10
--fused-bubble-source Computes the Euler-Euler bubble source in one kernel. OpenACC GPU build with NVHPC §11
--qbmm-device-inversion Runs the QBMM moment inversion as a device kernel. OpenACC GPU build with NVHPC, --case-optimization §11
--igr-tuned-sweeps Selects the restructured IGR sweeps. Three of its four parts need --case-optimization, otherwise it will be partially enabled. §11
--igr-shallow-halo Exchanges one IGR ghost layer per non-final Jacobi iteration. none §11
--lagrangian-device-cell-list Builds the Lagrangian bubble cell list on the device. none §11
--active-box Builds the active compute window. Env var MFC_ACTIVE_BOX turns it on at run time. --fused-rhs, checked when MFC_ACTIVE_BOX is set §12

New Environment variables

Run time

The binaries read these variables at start-up. An unset variable gives the behavior of the
original code.

variable values effect default require
MFC_ACTIVE_BOX <steps>[,<growth>[,<boxes>]] Turns on the active compute window with this cadence, growth and box count (§12). off --active-box --fused-rhs
MFC_ACTIVE_BOX_FLOOR real in (0, 1) Sets the deviation floor fraction of the window. source default --active-box
MFC_ACTIVE_BOX_INNER integer k ≥ 1 Builds the inner cover every k steps. It needs the single-box window. off --active-box
MFC_HALO_OVERLAP 1 Overlaps the halo exchange with the interior computation of the fused path. off --fused-rhs --halo-single-round --async-queue
MFC_HALO_EPOCH_DEVICE 1 Completes the 26-neighbor exchange on the device, so a CUDA graph can capture it. The mode is for ranks on one node with peer-to-peer access. off --nvshmem --halo-single-round
MFC_HALO_26_TRIM 1 Shrinks the 26-neighbor exchange to the active-box window. off --nvshmem --halo-single-round --active-box
MFC_IGR_PINGPONG 1 Alternates the two IGR Jacobi buffers. off none
MFC_SFC_WEIGHTS file path Cuts the domain to balance the cost weights per block in the file. All three binaries read it. off --mpi
MFC_DECOMP_PROCESS_GRID <px>,<py>,<pz> Sets the Cartesian process grid of the simulation. The product of the three counts equals the rank count. Needs parallel_io = T. built-in search --mpi
MFC_DECOMP_AXIS_ORDER permutation of 1,2,3 Sets the order of the cut axes of the simulation. Needs parallel_io = T. 1,2,3 --mpi
MFC_DECOMP_X_OFFSETS, MFC_DECOMP_Y_OFFSETS, MFC_DECOMP_Z_OFFSETS 0,<cut>,...,<size> Set the cut positions per direction of the simulation. Each list runs from 0 to the direction's global cell count and holds one entry more than its rank count. equal cuts --mpi parallel_io = T
CUDA_VISIBLE_DEVICES, ROCR_VISIBLE_DEVICES, HIP_VISIBLE_DEVICES comma-separated device list Group the ranks per GPU in the grind report. set by the system --accurate-grind-time
NVSHMEM_REMOTE_TRANSPORT, NVSHMEM_IB_ENABLE_IBGDA transport name, or 1 Split each put and its signal into two operations when either variable selects ibgda. NVSHMEM defaults --nvshmem

Build time

The toolchain reads these variables when it generates and builds a case.

variable values effect build
MFC_TUNING_FILE <path> Names a JSON profile that overrides the compile-time constants of the fused kernels. The profile maps constant names to their values under a values key. --fused-rhs or --fused-bubble-source
MFC_FUSION_* positive integer, or a list of them Overrides one compile-time constant of the fused kernel. The variable name is the constant name. --fused-rhs
MFC_BUBBLE_VECTOR_LENGTH positive integer Overrides the vector length of the bubble kernels. --fused-bubble-source
MFC_STATE_ALIGNMENT_BYTES positive integer Overrides the stride alignment of the state arrays. --fused-rhs
MFC_REGISTER_CAPS <module>=<cap>[,<module>=<cap>...] Sets the register cap of one or more solver modules. A module names one source file of src/simulation. The cap is an integer from 1 to 255. --gpu acc with NVHPC
NVSHMEM_ROOT <path> Names the NVSHMEM installation. --nvshmem

The source gives the default of every constant. A value that is not set keeps the default.

Tests

All tests pass on NVHPC + OpenACC, AMD + OpenMP on all golden cases (assuming that the numerical bug PRs are merged.) This PR adds 1 golden AE748A63 to catch #2048.

Build verification on the default configuration (every optional optimization off):

  • CPU (GCC 16.2): pre_process, simulation, and post_process compile; the golden cases B44AF4A1 (axisymmetric LF), AFBCBDFA (IGR), 8EAC3DA7 (viscous), and 63CD6ADF (grcbc) pass.
  • NVHPC 26.9 + OpenMP: the simulation target compiles. Golden cases: not ran.

Design notes

  • Compatible interfaces. Existing inputs, output formats, build commands, restart
    files, and post-processing pipelines stay compatible. The optimization flags
    compose through compile-time configuration.
  • Code structure. The new device code follows the existing OpenACC macro
    layer and module conventions of the source. The fused kernel uses fypp templates over
    direction and physics, like the existing preprocessor usage.

Fixes #2088 #2048
Addresses the hang observed in #1927
Related:
#2090 where I believe my implementation is better for the cases where it covers, and #2090 is better for other cases.
#2092 came up with similar idea independently but chose to go the more extreme route of optimizing away the loads using fused kernels. also optimize away "mp_weno and int_comp > 0" mentioned in the PR.
#1628 where I improved upon by supporting multi-rank natively.

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.

Acknowledgement

This PR is prepared by Billy Cao from Nanyang Technological University SCC team: Supernova for SC26 Student Cluster Competition. It unintentionally overlapped with ideas that existing PRs from other teams have raised, and I have fully acknowledged them in these cases. It also took inspiration from existing PRs by the MFC team, which I have also fully credited where due. For each of such cases, I have explained the differences/improvements on top of existing PRs.

@github-actions

Copy link
Copy Markdown

Claude Code Review

Head SHA: 0221014

Files changed:

  • 84
  • .typos.toml
  • CMakeLists.txt
  • cmake/Fypp.cmake
  • cmake/MFCTargets.cmake
  • cmake/NVSHMEMCodegen.cmake
  • docs/documentation/equations.md
  • src/common/include/acc_macros.fpp
  • src/common/m_mpi_common.fpp
  • src/simulation/m_start_up.fpp
  • toolchain/mfc/case.py

Findings:

  • medium toolchain/mfc/test/cases.py: The diff adds new test cases (2D LF, three 3D Azimuthal cases with HLL/HLLC/LF, and a 3D cylindrical jet case). No tests/<UUID>/golden.txt files appear in changed_files.txt for them. Each new case needs a golden directory, and its UUID comes from the case trace. If the goldens are not part of this PR, ./mfc.sh test fails on the missing files. Confidence is medium, because golden files may have been left out of the prepared diff. Confirm they are committed, and that the Azimuthal and jet results were checked against an independent reference. This PR changes CFD results, so CLAUDE.md requires that verification.
  • medium src/common/m_mpi_common.fpp: This is not a defect I found. It is a coverage limit. The ~4,000-line rewrite of the halo exchange adds s_mpi_sendrecv_variables_buffers_26, the NVSHMEM puts, persistent RDMA requests and the IGR batched exchange. I checked the setup hunks (halo_26_beg/halo_26_extent, the neighbor tag handshake, and the IGR face-offset exchange with 6100+fi tags and ieor(fi,1) pairing) and they are consistent. I did not trace the pack/unpack hunks against the original per-direction code. The pitfalls "all sweep directions" and "buffer sizing" apply. The new halo_size = max(halo_size, v_size*halo_26_total_points - 1) must cover the QBMM and temperature v_size variants. Please confirm with ppn=2 and ppn=4 tests, with MPI decompositions in more than one direction, run under MFC_HALO_SINGLE_ROUND.

Checked and found consistent:

  • Toolchain: the args.py flag-dependency checks match the CMake FATAL_ERROR checks. The OPT_IN_FEATURES slug and from_dict default handling are sound.
  • Case optimization: model_eqns, bubbles_euler, qbmm and hypoelasticity are moved into the #:if not MFC_CASE_OPTIMIZATION initialization block. I found no other assignments in src. The GPU_UPDATE of bubbles_euler and qbmm is correctly guarded.
  • Async queue macros: the GEN_QUEUE_STR/GEN_QUEUE_WAIT_STR logic keeps reduction, copy and copyout regions synchronous.
  • Other hunks: the device NaN reduction in s_save_data reads device-resident q_cons_ts(stor). s_check_active_box_request is defined in the non-active-box branch of m_active_box.fpp. I found no double-precision intrinsics and no hard-coded GPU architectures in the added lines.

Coverage note: about 35k diff lines were not reviewed hunk-by-hunk. Only src/common and toolchain hunks and the CMake changes were read in depth. The context directory held only 10 of the changed files. The fused RHS, Riemann solvers, WENO/MUSCL, CBC, CUDA graph, NVSHMEM codegen, bubbles and QBMM hunks were not individually reviewed.

GPU performance optimizations for the simulation solver, mostly for the
NVIDIA OpenACC path, together with fixes to existing numerical bugs.

Most optimizations are opt-in via compiler arguments; the remainder are
always-on (guarded by the relevant compiler/GPU where necessary) because
guarding them would duplicate too much code. The work is directed by three
findings: with case optimization MFC is largely not memory-capacity
constrained; on modern GPUs the solver is memory-bandwidth bound through
excessive device round-trips; and communication is a bottleneck. The
changes therefore trade extra memory for on-device data residency and
fewer H2D/D2H trips, maximize on-device cache/register/shared-memory
reuse, and improve communication on both GPU-direct MPI and a new NVSHMEM
transport.
@aliencaocao

aliencaocao commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

The Intel compiler failures is due to a regression in ifx 2026.2. I tested locally with 2026.1 and it compiles fine. Seems to be happening on unmodified master branch code too.

ifx segfaults on m_variables_conversion.mod. Flag-independent (segfault at -O0, no -ipo/-march).

I will fix all the lint/format tests

…-CMake renders

MFC_FUSION_STAGING_DEFAULT is injected into the generated case.fpp by the
toolchain only (toolchain/mfc/run/input.py). Documentation and other
plain-CMake renders resolve case.fpp from src/common/include/defaults,
which does not define it, so fypp aborted with a NameError and the
documentation build failed. Keep the toolchain define first (same value)
and fall back to the literal, mirroring the sibling constants below and
toolchain/mfc/tuning.py.

Co-Authored-By: Claude Code <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

Lines of Code

File Lines Diff
src/simulation/m_fused_rhs_openacc.fpp 2267 +2267
src/simulation/m_weno.fpp 2854 +1533
src/common/m_mpi_common.fpp 2723 +1229
src/simulation/m_rhs.fpp 2951 +983
src/simulation/m_active_box.fpp 968 +968
src/simulation/m_cbc.fpp 2077 +963
src/simulation/m_qbmm_openacc.fpp 812 +812
src/common/m_boundary_common.fpp 1125 +677
src/common/m_boundary_primitives.fpp 873 -599
src/simulation/include/fused_weno.fpp 341 +341
src/simulation/m_igr.fpp 2568 +279
src/simulation/m_time_steppers.fpp 1163 +253
src/simulation/m_start_up.fpp 1511 +240
src/simulation/m_bubbles_EE.fpp 526 +222
src/simulation/m_ibm.fpp 2025 +207
src/simulation/m_riemann_solver_hllc.fpp 1514 +206
src/common/m_nvshmem_interfaces.fpp 199 +199
src/simulation/include/mhd_face.fpp 189 +189
src/common/m_chemistry.fpp 510 +115
src/simulation/include/chemistry_face.fpp 113 +113
src/simulation/m_bubbles_EL_kernels.fpp 625 +109
src/simulation/m_riemann_state.fpp 1291 +106
src/simulation/m_bubbles_EE_openacc.fpp 80 +80
src/simulation/m_bubbles_EL.fpp 1711 +72
src/simulation/m_riemann_solver_hll.fpp 676 +69
src/simulation/m_data_output.fpp 1632 +54
src/simulation/m_qbmm.fpp 826 -53
src/simulation/include/graph_macros.fpp 50 +50
src/simulation/m_cuda_graph.fpp 49 +49
src/common/include/acc_macros.fpp 330 +48
src/simulation/include/thinc_integrals.fpp 48 +48
src/common/include/shared_parallel_macros.fpp 203 +42
src/simulation/m_riemann_solver_hlld.fpp 233 +41
src/simulation/include/qbmm_quadrature.fpp 35 +35
src/simulation/m_surface_tension.fpp 292 +29
src/common/m_variables_conversion.fpp 986 +26
src/simulation/include/hypoelastic_energy.fpp 25 +25
src/simulation/m_riemann_solver_lf.fpp 533 +24
src/simulation/m_hypoelastic.fpp 654 +20
src/simulation/m_fftw.fpp 272 +19
src/simulation/m_riemann_solvers.fpp 130 +18
src/simulation/m_thinc.fpp 294 -18
src/simulation/m_muscl.fpp 204 +17
src/simulation/include/hllc_burn.fpp 15 +15
src/simulation/m_viscous.fpp 1036 +15
src/common/m_phase_change.fpp 296 +11
src/simulation/m_mpi_proxy.fpp 544 +10
src/simulation/include/hll_velocity.fpp 9 +9
src/simulation/m_acoustic_src.fpp 524 +8
src/simulation/m_conduction.fpp 104 +8
src/simulation/m_global_parameters.fpp 802 +7
src/common/include/parallel_macros.fpp 257 +6
src/common/m_global_parameters_common.fpp 256 +6
src/post_process/m_global_parameters.fpp 408 -6
src/simulation/m_compute_cbc.fpp 280 +6
src/simulation/m_riemann_solver_hypo_hlld.fpp 777 -6
src/simulation/m_ib_patches.fpp 644 +4
src/simulation/p_main.fpp 77 +4
src/simulation/m_collisions.fpp 316 +3
src/pre_process/m_global_parameters.fpp 485 -2
Directory Lines Diff
common 12286 +1760
pre_process 5032 -2
simulation 38984 +10453
post_process 3493 -6
total 59795 +12205

This branch has not been deployed

No deployments
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.

Perf regression: 3D_performance_test regresses 31% from v4.9.3

1 participant