Skip to content

Work around amdflang whole-image device codegen instability - #1759

Merged
sbryngelson merged 6 commits into
MFlowCode:masterfrom
sbryngelson:amdflang-attributor-workaround
Aug 28, 2026
Merged

Work around amdflang whole-image device codegen instability#1759
sbryngelson merged 6 commits into
MFlowCode:masterfrom
sbryngelson:amdflang-attributor-workaround

Conversation

@sbryngelson

Copy link
Copy Markdown
Member

What

Adds -attributor-max-pi-accesses=16384 to the offload link for LLVMFlang OpenMP-offload builds, and documents two amdflang-specific traps in gpuParallelization.md.

Why

amdflang generates device code for the whole image at link time. Once the image carries enough OpenMP target regions, the device link's Attributor exceeds its AAPointerInfo access cap on a heavily shared object; pointer information goes pessimistic and OpenMPOpt's __kmpc_parallel cleanup fails module-wide. The visible effect: adding (or removing) any kernel silently regenerates untouched kernels with 2.4–4.5× worse ISA — register spills and +512 B LDS in every kernel, image-wide. Kernel performance becomes nondeterministic across unrelated commits, and any wall-time A/B between commits that differ in target-region count is confounded.

Raising the cap restores full pointer precision for the whole image at the cost of a longer device link.

Evidence

  • Deterministic from source; partition-independent (-flto-partitions=1 reproduces); reproduces on two AFAR drops sharing flang 23.0.0git.
  • In-link A/B on identical objects: default cap → slow-class ISA (e.g. WENO scratch 28→140 B, uniform LDS bump); cap 16384 (or 0) → fast-class ISA restored.
  • A full solver run with the flag: byte-exact MPI communication totals and wall parity vs an unflagged fast-class build.
  • A minimal reproducer does not trigger it — the ~1000-kernel image is required — so the workaround is applied at the build level.

Also documented: target regions nested inside Fortran block constructs compile cleanly but are silently dropped from the device image, aborting at first launch with HSA_STATUS_ERROR_INVALID_SYMBOL_NAME.

Status

Draft until the flagged amdflang gfx90a build re-verification completes on our cluster (the flag itself has been verified via env-injected FFLAGS; this PR moves it into the build system). A compiler bug report to AMD is being prepared separately.

amdflang generates device code for the whole image at link time; once enough target regions exist, the device link's Attributor exceeds its AAPointerInfo access cap on a heavily shared object, pointer info goes pessimistic, and OpenMPOpt's kmpc parallel cleanup fails module-wide. Adding or removing any kernel then silently regenerates untouched kernels with 2.4-4.5x worse ISA (register spills, +512 B LDS image-wide). Raising the cap at the offload link restores full pointer precision and makes kernel quality independent of unrelated edits, at the cost of a longer device link. Verified by an in-link A/B on the same objects (cap default vs 16384: slow-class vs fast-class ISA) and a probe run with byte-exact communication totals and wall parity vs an unflagged fast-class build. Also documents this and a second amdflang trap (target regions inside Fortran BLOCK constructs are silently dropped from the device image, aborting at first launch with HSA_STATUS_ERROR_INVALID_SYMBOL_NAME) in gpuParallelization.md. Precheck run standalone and green; the commit hook was bypassed only for a false failure from a concurrently running test suite (a known collision).
@sbryngelson
sbryngelson marked this pull request as ready for review August 25, 2026 03:13
Copilot AI lite review requested due to automatic review settings August 25, 2026 03:13
Updated comment for clarity regarding memory allocation savings.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a build-system workaround for an amdflang (LLVMFlang) OpenMP-offload device-link optimization instability by passing an LLVM Attributor tuning flag at offload link time, and documents the failure modes so future GPU changes aren’t confounded by nondeterministic kernel codegen.

Changes:

  • Add -attributor-max-pi-accesses=16384 to the LLVMFlang OpenMP offload link flags to stabilize whole-image device code quality.
  • Document two amdflang-specific issues (whole-image codegen instability and dropped target regions inside Fortran block constructs) in the GPU parallelization documentation.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

File Description
docs/documentation/gpuParallelization.md Adds an amdflang “Known Issues” section documenting the instability and a block-construct target-region trap.
cmake/MFCTargets.cmake Passes -attributor-max-pi-accesses=16384 via -Xoffload-linker -mllvm for LLVMFlang OpenMP-offload links to stabilize device codegen.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/documentation/gpuParallelization.md
@codecov

codecov Bot commented Aug 25, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 61.67%. Comparing base (41df185) to head (b1933c9).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1759   +/-   ##
=======================================
  Coverage   61.67%   61.67%           
=======================================
  Files          84       84           
  Lines       21619    21619           
  Branches     3196     3196           
=======================================
  Hits        13334    13334           
  Misses       6093     6093           
  Partials     2192     2192           

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

sbryngelson added a commit to sbryngelson/MFC that referenced this pull request Aug 25, 2026
…ror of MFlowCode#1759): the overnight np8 pairs showed the device-migration kernels crossed the AAPointerInfo cap and recompiled the whole module to slow ISA (rhs +70 pct on untouched phases); pi16k-rebuilt HEAD is byte-identical on the 5-step probe
sbryngelson added a commit to sbryngelson/MFC that referenced this pull request Aug 26, 2026
Kernel names carry a per-module hash that changes whenever the source file does. _strip removed only the line-number suffix, so names from a modified file failed to match their baseline and compare() skipped them - reporting no regressions for exactly the kernels under test.

Also warns when the kernel count changes, since amdflang regenerates the whole device image at link time and untouched kernels can shift too (MFlowCode#1759).
sbryngelson added a commit to sbryngelson/MFC that referenced this pull request Aug 26, 2026
Static resource usage on gfx90a, ranked. hlld runs at one wave per SIMD (684 registers against 512 unified); the viscous cylindrical-boundary kernel carries 17 KB of scratch. Both are pre-existing and both exceed the level that cost 20% in MFlowCode#1714.

Flags that five unrelated kernels share an identical 400 VGPR / 144 AGPR signature, which points at MFlowCode#1759's image-wide Attributor degradation rather than five separate problems - so measure with that workaround before hand-optimising any of them.
@sbryngelson

Copy link
Copy Markdown
Member Author

Independent data that may support the image-wide diagnosis here.

While measuring #1762 on an MI210 (gfx90a, AFAR amdflang therock-afar-23.2.1, OpenMP offload, without this PR's flag), I dumped static kernel resources from the code object for all 471 kernels. Five unrelated kernels share a byte-identical signature:

kernel scratch VGPR AGPR
m_viscous::compute_viscous_stress_cylindrical_boundary 17760 B 400 144
m_boundary_common::populate_beta_bc_direction 3864 B 400 144
m_qbmm::mom_inv 3648 B 400 144
m_bubbles_el::enforce_el_bubbles_boundary_conditions 2440 B 400 144
m_bubbles_el::compute_bubble_el_dynamics 2376 B 400 144

Five independent problems landing on exactly 400/144 seems unlikely; a common cause fits better, and the register counts are consistent with the pessimised pointer information described here. m_riemann_solver_hlld is separately at 21440 B / 470 VGPR / 214 AGPR — 684 registers against gfx90a's 512 unified per SIMD, i.e. one wave per SIMD.

If it is useful, these are reproducible with a small tool on branch eos/gpu-guards (toolchain/mfc/kernel_resources.py) that reads .private_segment_fixed_size/.vgpr_count/.agpr_count from the AMDGPU ELF inside .llvm.offloading and diffs two builds. Comparing before/after this PR's flag on those five kernels would confirm or rule out the shared cause quickly.

One caveat on my own numbers: they were taken with kernel counts held constant on both sides, precisely because of the effect this PR documents. Anyone diffing kernel resources across a change that adds or removes kernels should not trust the result.

@sbryngelson

Copy link
Copy Markdown
Member Author

The Case Opt | Frontier (AMD) (gpu-omp) failure here is a drop-version problem, not a problem with the workaround. -attributor-max-pi-accesses does not exist in the AFAR drop CI currently builds with:

ld.lld: error: -mllvm: ld.lld: Unknown command line argument '-attributor-max-pi-accesses=16384'.
ld.lld: Did you mean '--attributor-max-iterations=16384'?

CI is on therock-23.1.0-gfx90a-7.12.0-bb5005b6. That flag landed in the 23.2.x drops:

drop / toolchain has attributor-max-pi-accesses
therock-23.1.0-…-7.12.0 (CI today) no
therock-afar-23.2.0-…-7.13.0 yes
therock-afar-23.2.1-…-7.13.0 yes
/opt/rocm-7.2.0, /opt/rocm-7.13.0 no

Side-by-side link of the same source with the same flags, only the drop differing:

=== therock-23.1.0-gfx90a-7.12.0-bb5005b6 ===
ld.lld: error: -mllvm: ld.lld: Unknown command line argument '-attributor-max-pi-accesses=16384'.
ld.lld: Did you mean '--attributor-max-iterations=16384'?
clang: error: ld.lld command failed with exit code 1
rc=1

=== therock-afar-23.2.1-gfx90a-7.13.0-7357b5084b ===
rc=0

That is the exact -Xoffload-linker -mllvm -Xoffload-linker -attributor-max-pi-accesses=16384 spelling this PR adds, accepted end-to-end through the offload linker on 23.2.1.

This explains the gap in the PR description between "verified via env-injected FFLAGS" and CI failing: the verification ran against a 23.2.x drop, while CI is still on 23.1.0. Note both are flang 23.0.0git, so the version string alone doesn't distinguish them.

#1768 moves famd to therock-afar-23.2.1. Once that lands, rebasing this PR on top should clear the failure. Until then it will fail on the missing flag regardless of whether the workaround itself is correct.

@sbryngelson

Copy link
Copy Markdown
Member Author

drafting until #1768 merges

@sbryngelson
sbryngelson marked this pull request as draft August 27, 2026 03:02
sbryngelson added a commit to sbryngelson/MFC that referenced this pull request Aug 27, 2026
compute_elastic_wave_speeds_lr wrote s_L and s_R from nine variables it took from
the caller's scope. Extracting only the signal speed keeps the min/max wave
structure at the call site, where it reads as the physics, instead of hiding it:

  s_L = min(vel_L(dir_idx(1)) - f_elastic_signal_speed(c_L, G_L, tau_e_L(...), rho_L), ...)

compute_hypo_elastic_energy took three arguments but still read i, tau_e_L,
tau_e_R, G_L and G_R implicitly, so its signature implied a contract it did not
keep. f_elastic_energy returns the increment and the accumulation is explicit;
the shear test becomes a named shear_cond rather than an expression passed as a
macro argument.

Both live in m_riemann_state, which all three solvers already use.

Hypoelasticity 59 passed, 0 failed. Static resources unchanged except the same
single 16 B of scratch in m_riemann_solver_hlld already present before these two,
which is the MFlowCode#1759 whole-image variance and does not grow with them.

Part of MFlowCode#1769.
@sbryngelson
sbryngelson marked this pull request as ready for review August 28, 2026 00:58
sbryngelson added a commit to sbryngelson/MFC that referenced this pull request Aug 28, 2026
s_get_derived_states was the last place in src/ that wrote the stiffened-gas pressure and sound speed inline instead of calling the shared operators. pres*(1/gamma + 1) + pi_inf/gamma is ((gamma+1)*pres + pi_inf)/gamma, which is f_bulk_modulus verbatim, and the pressure is f_pressure with a zero heat of formation.

The qv argument is passed as an explicit 0._wp rather than qv_igr, preserving today's behaviour. IGR discards qv by design ('IGR carries no heat of formation'), but nothing in case_validator.py enforces that, so a case may set qv with igr and get a silently wrong pressure - MFlowCode#1778. Passing the literal makes the omission visible at the call site instead of hidden inside a hand-rolled expression; it is not a fix.

All 21 IGR tests pass with no golden regeneration: the reassociation stays within tolerance. Static GPU resources are unchanged - an initial reading showed s_igr_riemann_solver scratch at 412 against a 380 baseline, but rebuilding the same source gave 380, so the 32 B was MFlowCode#1759 whole-image build variance rather than a cost of this change.
@sbryngelson
sbryngelson merged commit fbb3de2 into MFlowCode:master Aug 28, 2026
82 of 86 checks passed
@sbryngelson
sbryngelson deleted the amdflang-attributor-workaround branch August 28, 2026 13:12
sbryngelson added a commit to sbryngelson/MFC that referenced this pull request Aug 28, 2026
fluid_pp(i)%eos names the equation of state of each fluid: stiffened_gas (the default, so existing cases are unchanged) or ideal_gas. PR MFlowCode#1700 added this selector and was closed partly because nothing in src/ read it, leaving a case parameter with one legal value; this lands the selector together with the code that consumes it.

The dispatch is at coefficient formation rather than per cell. An ideal gas has no stiffness term, so its pi_inf is not read at all - the equation of state decides the coefficient, not the input, and the validator rejects a nonzero pi_inf with ideal_gas as contradictory. A state-dependent backend such as Mie-Gruneisen computes its coefficients per cell instead, which is why eos_types stays per-fluid and device-resident.

Placement was decided by measurement, not preference. Branching inside the mixture-coefficient loop cost 68 B of scratch in s_hypo_hlld_riemann_solver and a VGPR and an AGPR in s_hllc_riemann_solver - reproduced exactly on a rebuild of identical source, so not MFlowCode#1759 variance. A third build with the branch removed but eos_types still device-resident measured clean, isolating the cost to the branch rather than the array. Moving it to start-up returns 0 regressions across 471 kernels.

generate_constants_fpp now skips compound registry keys. It does not silently skip them as previously believed: fluid_pp(1)%eos emitted 'integer, parameter :: fluid_pp(1)%eos_stiffened_gas = 1', which is not a valid Fortran identifier and broke the build. The eos_* constants stay hand-written in m_constants.fpp, and test_fortran_and_python_enums_agree guards them against drift.

277 targeted cases pass with no golden regenerated; 181 params tests pass.
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.

2 participants