Affected versions
Bisected release-by-release; the regression was introduced in 24.7. The full application (MFC, \3D_performance_test\ benchmark, grind time in ns/gp/eq/rhs, lower is better) and this MWE (kernel ms/call) flip at the same boundary. MWE numbers below are from a single sweep over all seven SDKs on an otherwise-idle A100-80GB (2026-07-15); the kernel’s register count from ptxas (`build_.log`) steps at the same release:
| nvfortran | status | app grind time | MWE ms/call | vs 24.3 | registers |
|---|---|---|---|---|---|
| 24.3-0 | good | 0.653 | 4.677 | 1.00x | 180 |
| 24.5-0 | good | 0.652 | 4.660 | 1.00x | 184 |
| 24.7-0 | bad | 0.830 | 7.901 | 1.69x | 196 |
| 24.9-0 | bad | 0.838 | 7.900 | 1.69x | 196 |
| 25.1-0 | bad | 0.847 | 7.922 | 1.69x | 196 |
| 25.11-0 | bad | 0.837 | 7.911 | 1.69x | 196 |
| 26.3-0 | bad | 0.838 | 7.906 | 1.69x | 196 |
Performance is flat within each regime (24.3–24.5 and 24.7–26.3): this is a single step change at 24.7, not a gradual drift, and nothing recovers through 26.3. The checksum is 2.047598784747795E+08 for every version, identical to all printed digits.
Hardware / software environment
- GPU: NVIDIA A100 80GB PCIe (sm_80)
- Driver: 590.48.01 (CUDA 13.1), host: x86-64 Linux (Icelake server)
- Target flags:
-fast -O3 -acc -gpu=cc80,keep,ptxinfo -Minfo=accel - The regression also reproduces with
-gpu=cuda12.9on 26.3 (7.94 ms/call, identical checksum — seereference/cuda12_9_build_26_3.logandreference/cuda12_9_run_26_3.log), so it is in the Fortran front/middle end, not in the bundled ptxas.
Description
MFC (GitHub - MFlowCode/MFC: Exascale multiphase flow solver — 2025 Gordon Bell Prize Finalist | 200T grid points on 43K+ GPUs · GitHub), a compressible-flow CFD solver written in Fortran + OpenACC, shows a 28% end-to-end slowdown going from NVHPC 24.3 to 25.11/26.3 on A100. Profiling isolated the loss to its WENO5 reconstruction kernel (m_weno_s_weno_1071_gpu and its y/z variants), which goes from 5.0 ms/call to 8.3 ms/call (+66%). The attached tarball contains a single-file extraction of that kernel that reproduces the regression standalone: the same loop nest (gang vector collapse(3) over the grid with a seq inner loop over 8 field variables), small private arrays, the mapped-WENO (WENO-M) weight formula, and compile-time-constant scheme parameters (MFC’s “case optimization” mode, which amplifies the regression from ~1.1x to ~1.7x). Comparing SASS, the newer compilers stop CSE-ing/hoisting loop-invariant subexpressions: 26.3 emits ~40% more instructions for the identical source, with nearly double the DFMA count and 3x the MOVs (same 60 LDG global loads, zero spills in both), and adds per-iteration recomputation plus extra branch convergence (BSSY/BSYNC) that the 24.3 code did not need. -Minfo=accel output is identical between versions (vector(128), same implicit reductions, same “Local memory used” line), so the schedule is unchanged — only the quality of the generated device code differs.
Reproduction
./run_comparison.sh
With no arguments this builds weno_mwe.f90 with 24.3 and 26.3 (each environment sourced in its own subshell), runs both binaries on the same GPU, and prints a table. If those SDKs are not present in ./hpc_sdk_24_3 / ./hpc_sdk_26_3 next to the script, they are downloaded and installed there automatically via nvhpcInstall.sh (looked for in this directory, then one level up).
The full sweep in the tables above was produced by passing all seven environments explicitly — any list of setupEnv.sh paths works and is built/run in the order given:
./run_comparison.sh <24.3-prefix>/setupEnv.sh <24.5-prefix>/setupEnv.sh \
<24.7-prefix>/setupEnv.sh <24.9-prefix>/setupEnv.sh \
<25.1-prefix>/setupEnv.sh <25.11-prefix>/setupEnv.sh \
<26.3-prefix>/setupEnv.sh
Additional SDKs for the sweep can be installed the same way the bootstrap does it, e.g. ./nvhpcInstall.sh -v 24.7 -p ./hpc_sdk_24_7, which writes the corresponding `setupEnv.sh` into the install prefix.
Compilation is exactly:
nvfortran -fast -O3 -acc -gpu=cc80,keep,ptxinfo -Minfo=accel weno_mwe.f90 -o weno_mwe\_<ver>
The binary warms up 10 iterations, then times 100 launches of the kernel (system_clock around an !$acc wait-bounded region) and prints ms/call plus a checksum (sum of |values| of both output arrays) so nothing is dead-code eliminated and cross-compiler agreement can be verified.
(Version labels in the output and in artifact names come from nvfortran --version of whichever compiler each environment provides, not from the paths passed on the command line.)
Expected vs actual (measured on the machine above, A100-80GB, idle GPU)
| nvfortran | ms/call | checksum | ratio vs 24.3 |
|---|---|---|---|
| 24.3-0 | 4.677 | 2.047598784747795E+08 | 1.00x |
| 24.5-0 | 4.660 | 2.047598784747795E+08 | 1.00x |
| 24.7-0 | 7.901 | 2.047598784747795E+08 | 1.69x |
| 24.9-0 | 7.900 | 2.047598784747795E+08 | 1.69x |
| 25.1-0 | 7.922 | 2.047598784747795E+08 | 1.69x |
| 25.11-0 | 7.911 | 2.047598784747795E+08 | 1.69x |
| 26.3-0 | 7.906 | 2.047598784747795E+08 | 1.69x |
Expected: ms/call roughly equal across versions. Actual: every release from 24.7 onward is ~1.7x slower, in a single step at 24.7. Checksums agree to all printed digits, so all compilers generate numerically equivalent code — only performance regresses. Timing measurements taken with the GPU otherwise idle (kernel-only timing brackets an !$acc wait, so contention from other jobs inflates absolute numbers and compresses the ratio).
SASS / ptxas evidence
MWE kernel m_weno_mwe_s_weno_173_gpu (dumps in reference/sass_mwe_*.txt):
| metric | 24.3 | 26.3 |
|---|---|---|
| SASS instructions | 752 | 1056 |
| DFMA | 119 | 222 |
| MOV | 42 | 126 |
| LDG | 60 | 60 |
| BSSY/BSYNC | 16/16 | 27/27 |
| registers | 180 | 196 |
| spills | 0 | 0 |
Full-application kernel m_weno_s_weno_1071_gpu from MFC, for comparison (dumps and ptxas/-Minfo excerpts in reference/):
| metric | 24.3 | 26.3 |
|---|---|---|
| SASS instructions | 760 | 1064 |
| DFMA | 119 | 222 |
| MOV | 43 | 128 |
| LDG | 60 | 60 |
| registers | 186 | 198 |
| spills | 0 | 0 |
The MWE reproduces the full application’s instruction-count and opcode-mix divergence almost exactly, so it exercises the same code-generation defect.
Files
All files below are in the attached tarball:
weno_mwe.f90— self-contained reproducer (no dependencies beyond OpenACC)build.sh <setupEnv.sh>— builds with one compiler, logs-Minfo/ptxas tobuild_<ver>.logrun_comparison.sh— good-vs-bad comparison; bootstraps its own SDKs if needednvhpcInstall.sh— downloads/installs an NVHPC SDK and generates its `setupEnv.sh` (used by the bootstrap)build_<ver>.log— compile logs for all seven tested versions (24.3, 24.5, 24.7, 24.9, 25.1, 25.11, 26.3)reference/sass_mwe_24_3.txt,sass_mwe_26_3.txt— cuobjdump SASS of the MWE kernelsass_weno_243.txt,sass_weno_263.txt— cuobjdump SASS of the original MFC kernelmfc_minfo_s_weno_243.txt,mfc_minfo_s_weno_263.txt—-Minfo=accelfor the original subroutinemfc_ptxas_m_weno_243.txt,mfc_ptxas_m_weno_263.txt— ptxas info for the original kernels
(The weno_mwe_<ver> binaries and keep\_<ver>/ intermediates are not shipped; a run regenerates them.)
weno_mwe_nvfortran_24_7_regression.tar.gz (84.7 KB)
Source provenance
Extracted from MFC src/simulation/m_weno.fpp, subroutine s_weno, weno_order == 5 / weno_dir == 1 branch, with Fypp macros expanded to the exact OpenACC directives MFC generates and the case-optimization constants (weno_polyn=2, weno_num_stencils=2, mapped_weno=.true., etc.) baked in as Fortran parameters, as in MFC’s optimized production builds. Grid: 200^3 interior cells + 4 ghost layers, 8 reconstructed variables, double precision, synthetic smooth input data.
Happy to run additional experiments (Nsight Compute profiles, other flags or versions) on request. A TPR number for tracking would be appreciated.
AI/LLM Disclosure: The code and descriptions in this post were generated with the assistance of Claude Code