Skip to content

[Attributor][OpenMP] attributor-max-pi-accesses cap degrades codegen image-wide once an offload image has ~512 kernels #4070

Description

@sbryngelson

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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions