Summary
In a large OpenMP offload image, the AAPointerInfo access cap from PR1807 (attributor-max-pi-accesses, default 512; AttributorAttributes.cpp:945 at 35849413f758) triggers on the DeviceRTL kernel-state globals. In our image each kernel contributes about one access to each of TeamState, ThreadStates, IsSPMDMode, KernelEnvironmentPtr, KernelLaunchEnvironmentPtr and SharedMemVariableSharingSpace, so these access lists track kernel count and cross the cap together at ~512 kernels.
Once SharedMemVariableSharingSpace goes pessimistic, store-to-load forwarding through __kmpc_begin/get_sharing_variables fails and OpenMPOpt cannot clean up the parallel-region call protocol anywhere in the module. Two consequences:
- Argument-marshalling allocas (
structArg, .reloaded) reach codegen in 515 of 523 kernels (92 of 519 in the good image). Measured in MFC on gfx90a: a previously spill-free WENO kernel gains a 112 B stack frame and 88 spill instructions (scratch 28→140 B); the LF Riemann kernel goes from ~130 to ~176 VGPRs (3→2 waves). Kernels run 2.4-4.5x slower per dispatch (rocprofv3), application wall +36%.
- The sharing space is dead by the end of the pipeline but still referenced when
AMDGPULowerModuleLDS forms per-kernel LDS structs, so every kernel carries 512 B of dead LDS (group segment 2048→2560).
Because the cap is a property of the whole module, adding a few unrelated target regions changes the codegen of untouched kernels and the flip is all-or-nothing. Note the trigger is the accumulated access count on one object, not kernel count as such: synthetic images with more kernels do not reproduce (see below).
Reproducer
Application-level: https://github.com/sbryngelson/MFC branch up/mega, commit e53db278 (519 kernels, good) vs a7970743 (523 kernels, bad). The diff only touches src/simulation/m_amr.fpp; device IR of all other files is bit-identical between the two builds.
Standalone, no app build and no GPU needed: the two merged device-LTO modules (post-internalize, captured from the real link with -Xoffload-linker --save-temps) are in https://github.com/sbryngelson/compiler-bugs/tree/main/amd/attributor-pi-access-cap along with the patch below and its lit test.
gunzip -k fast.internalize.bc.gz slow.internalize.bc.gz
opt -passes='lto<O3>' fast.internalize.bc -o out.bc # 0 structArg allocas remain
opt -passes='lto<O3>' slow.internalize.bc -o out.bc # 22470 structArg occurrences; [64 x ptr] in 515 kernel .lds structs
A synthetic Fortran reproducer does not work and the generator plus its negative result are in the same directory: images of up to 602 generated target regions, in three filler designs, never trip the cap (0 cap events vs 3800 on the real module) even though they contain more __kmpc_parallel_60/sharing-space references than MFC does. Kernel count alone is not the trigger; what matters is how many accesses accumulate on one object through the real call structure, which we could not synthesize.
Measurements
| quantity |
value |
| cap default |
512 |
accesses on SharedMemVariableSharingSpace, good/bad image |
~501-511 / 516+ |
| minimal kernel prefix of the bad module that still triggers |
519-520 |
| minimal cap value that heals the bad module |
510-636 |
Access counts come from a build instrumented at the cap site. It also shows the good image is already pessimistic on the other five state objects; SharedMemVariableSharingSpace is the one that flips between the two builds.
Workaround in production: -Xoffload-linker -mllvm -Xoffload-linker -attributor-max-pi-accesses=16384 restores good codegen at ~4x device-link time.
Proposed fix
Count the cap per accessing function instead of per object. Interference reasoning is execution-scoped, so many functions with a few accesses each are cheap and need precision; a single function with a huge access list, the case the cap targets, is still cut off. Keep an absolute ceiling as a memory backstop:
--- a/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
+++ b/llvm/lib/Transforms/IPO/AttributorAttributes.cpp
@@ private:
/// State to track fixpoint and validity.
BooleanState BS;
+
+ /// Number of tracked accesses per accessing function, for the per-scope cap.
+ DenseMap<const Function *, unsigned> PerScopeAccesses;
};
ChangeStatus AA::PointerInfo::State::addAccess(...) {
+ const Function *AccScope = I.getFunction();
if (MaxAccessesPerAAPointerInfo > 0 &&
- AccessList.size() >= MaxAccessesPerAAPointerInfo)
+ (PerScopeAccesses.lookup(AccScope) >= MaxAccessesPerAAPointerInfo ||
+ AccessList.size() >= 64 * MaxAccessesPerAAPointerInfo))
return indicatePessimisticFixpoint();
@@ append path (!AccExists)
AddToBins(AccessList[AccIndex].getRanges());
+ ++PerScopeAccesses[AccScope];
return ChangeStatus::CHANGED;
Posted as #4094, with a lit test. Validated by building opt with and without the change, both at 35849413f758 and at the amd-staging tip a830b4135bfc:
| build |
module below the cap |
module over the cap |
35849413f758, unpatched |
0 marshalling allocas |
22470 allocas, 515 dead LDS slots |
35849413f758, patched |
0 |
0, 0 |
a830b4135bfc, unpatched |
0 |
8670 allocas, 515 dead LDS slots |
a830b4135bfc, patched |
0 |
0, 0 |
So this is live at the tip, not only in the AFAR drops. After the change the cap still fires on the genuinely dense cases (llvm-libc slab_cache, Fortran runtime ShallowCopy specializations), and pipeline time is about 2x default against roughly 4x if the cap is merely raised.
Separately: a remark when the cap fires would have made this findable; today it is silent.
Environment
AFAR 23.2.0 and 23.2.1 (both AMD flang 23.0.0git, ROCm/llvm-project 35849413f758), gfx90a MI250X (AMD HPC Fund), Linux EL9. Compile: -fopenmp --offload-arch=gfx90a -O3 -fopenmp-assume-threads-oversubscription -fopenmp-assume-teams-oversubscription -fopenmp-assume-no-nested-parallelism. Link: -flto-partitions=16 (also reproduces with =1).
Summary
In a large OpenMP offload image, the AAPointerInfo access cap from PR1807 (
attributor-max-pi-accesses, default 512;AttributorAttributes.cpp:945at35849413f758) triggers on the DeviceRTL kernel-state globals. In our image each kernel contributes about one access to each ofTeamState,ThreadStates,IsSPMDMode,KernelEnvironmentPtr,KernelLaunchEnvironmentPtrandSharedMemVariableSharingSpace, so these access lists track kernel count and cross the cap together at ~512 kernels.Once
SharedMemVariableSharingSpacegoes pessimistic, store-to-load forwarding through__kmpc_begin/get_sharing_variablesfails and OpenMPOpt cannot clean up the parallel-region call protocol anywhere in the module. Two consequences:structArg,.reloaded) reach codegen in 515 of 523 kernels (92 of 519 in the good image). Measured in MFC on gfx90a: a previously spill-free WENO kernel gains a 112 B stack frame and 88 spill instructions (scratch 28→140 B); the LF Riemann kernel goes from ~130 to ~176 VGPRs (3→2 waves). Kernels run 2.4-4.5x slower per dispatch (rocprofv3), application wall +36%.AMDGPULowerModuleLDSforms per-kernel LDS structs, so every kernel carries 512 B of dead LDS (group segment 2048→2560).Because the cap is a property of the whole module, adding a few unrelated
targetregions changes the codegen of untouched kernels and the flip is all-or-nothing. Note the trigger is the accumulated access count on one object, not kernel count as such: synthetic images with more kernels do not reproduce (see below).Reproducer
Application-level: https://github.com/sbryngelson/MFC branch
up/mega, commite53db278(519 kernels, good) vsa7970743(523 kernels, bad). The diff only touchessrc/simulation/m_amr.fpp; device IR of all other files is bit-identical between the two builds.Standalone, no app build and no GPU needed: the two merged device-LTO modules (post-internalize, captured from the real link with
-Xoffload-linker --save-temps) are in https://github.com/sbryngelson/compiler-bugs/tree/main/amd/attributor-pi-access-cap along with the patch below and its lit test.A synthetic Fortran reproducer does not work and the generator plus its negative result are in the same directory: images of up to 602 generated target regions, in three filler designs, never trip the cap (0 cap events vs 3800 on the real module) even though they contain more
__kmpc_parallel_60/sharing-space references than MFC does. Kernel count alone is not the trigger; what matters is how many accesses accumulate on one object through the real call structure, which we could not synthesize.Measurements
SharedMemVariableSharingSpace, good/bad imageAccess counts come from a build instrumented at the cap site. It also shows the good image is already pessimistic on the other five state objects;
SharedMemVariableSharingSpaceis the one that flips between the two builds.Workaround in production:
-Xoffload-linker -mllvm -Xoffload-linker -attributor-max-pi-accesses=16384restores good codegen at ~4x device-link time.Proposed fix
Count the cap per accessing function instead of per object. Interference reasoning is execution-scoped, so many functions with a few accesses each are cheap and need precision; a single function with a huge access list, the case the cap targets, is still cut off. Keep an absolute ceiling as a memory backstop:
Posted as #4094, with a lit test. Validated by building
optwith and without the change, both at35849413f758and at theamd-stagingtipa830b4135bfc:35849413f758, unpatched35849413f758, patcheda830b4135bfc, unpatcheda830b4135bfc, patchedSo this is live at the tip, not only in the AFAR drops. After the change the cap still fires on the genuinely dense cases (llvm-libc
slab_cache, Fortran runtimeShallowCopyspecializations), and pipeline time is about 2x default against roughly 4x if the cap is merely raised.Separately: a remark when the cap fires would have made this findable; today it is silent.
Environment
AFAR 23.2.0 and 23.2.1 (both
AMD flang 23.0.0git, ROCm/llvm-project35849413f758), gfx90a MI250X (AMD HPC Fund), Linux EL9. Compile:-fopenmp --offload-arch=gfx90a -O3 -fopenmp-assume-threads-oversubscription -fopenmp-assume-teams-oversubscription -fopenmp-assume-no-nested-parallelism. Link:-flto-partitions=16(also reproduces with=1).