Skip to content

HipGlobalVariables: expand lowered globals nested in constant aggregates - #1651

Merged
pvelesko merged 2 commits into
mainfrom
2026-09-16-github-1640-aggregate-init
Sep 19, 2026
Merged

pvelesko merged 2 commits into
mainfrom
2026-09-16-github-1640-aggregate-init

Conversation

@pvelesko

Copy link
Copy Markdown
Collaborator

expandConstant in HipGlobalVariables returned constant aggregates unchanged, so a lowered device global's address nested in a struct or array initializer (or in an aggregate operand of an instruction) became poison once the original global was erased. It now expands the aggregate's operands like a constant expression and, when any of them needs a runtime load, rebuilds the aggregate with insertvalue/insertelement. The reproducer runs the post-link pipeline with opt, so it covers both program-scope-globals settings without a device.

A non-lowered global initialized with a device global's address (a function-local static, or the constant behind a local aggregate) is a separate path, tracked in #1650.

Fixes #1640

This PR was generated using AI

…ce global's address

A device global whose struct initializer holds the address of another
lowered device global reads back a wrong pointer on the device, because
HipGlobalVariables leaves the field poison in the init kernel:

  store %struct.Agg { ptr addrspace(4) poison, [4 x i32] [...] }, ...

The fixture runs the real post-link pipeline with opt, so it needs no
device and runs on every lane and under both
CHIP_ENABLE_DEVICE_PROGRAM_SCOPE_GLOBALS settings. @s's initializer is
the one hipcc emits for

  __device__ int Scalar = 42;
  struct Agg { int *P; int A[4]; };
  __device__ Agg S = {&Scalar, {1, 2, 3, 4}};

and the kernel adds the other way the same early return is reached: a
constant aggregate operand of an instruction, here a <2 x i64> of
ptrtoint addresses, which the unfixed pass stores as poison too. The
test fails when any store in the output has a poison operand; on the
unfixed tree both stores do.

A device-level test of the struct case was dropped in review in favour
of this one. On an Arc B570 it printed PtrOK=0 A3=4 before the fix and
PtrOK=1 A3=4 after it on Level Zero and OpenCL, but it could not run on
the OFF lane: there the fix leaves Scalar a program-scope global (its
address is loaded from S's init kernel, which isConvertibleGlobal
rejects), and rusticl rejects initialized program-scope globals (#1279),
so it needed a CMake gate and never exercised the vector path.
expandConstant rewrites a reference to a lowered device global into a
load of its __chip_var_<name> address holder, recursing through
constant expressions, but returned a ConstantAggregate (struct, array
or vector constant) unchanged. An initializer such as

  __device__ int Scalar = 42;
  __device__ Agg S = {&Scalar, {1, 2, 3, 4}};

takes path B of emitGlobalVarInitBody (hasNoRuntimeConstants already
recurses into aggregates and sees the reference), so the init kernel
stored the original constant, and when eraseMappedGlobalVariables
replaced @scalar with poison the field became poison:

  store %struct.Agg { ptr addrspace(4) poison, [4 x i32] [...] }, ...

The device then read a wrong S.P. The same early return left an
aggregate operand of an ordinary kernel instruction referring to the
original global too; eraseMappedGlobalVariables marks that
llvm_unreachable, and a Release build poisons the operand instead (a
kernel storing <2 x i64> <ptrtoint @A, ptrtoint @b> stored poison).

Expand the operands like a constant expression's. If none changed the
aggregate is returned as before; otherwise it is rebuilt at the
insertion point with one insertvalue per operand, or insertelement for
a vector (a <2 x i64> of ptrtoint addresses reaches this path as an
instruction operand). IRBuilder folds the inserts of constant leading
operands, so a large aggregate costs instructions only from its first
runtime operand on. The init kernel for S now stores

  %5 = insertvalue %struct.Agg poison, ptr addrspace(4) %4, 0
  %6 = insertvalue %struct.Agg %5, [4 x i32] [i32 1, i32 2, i32 3, i32 4], 1

Rebuilding from poison was chosen over writing the non-runtime part with
path A's memcpy and patching the pointer fields through GEPs, which
would need a second copy of the initializer and per-field offsets.

Under CHIP_ENABLE_DEVICE_PROGRAM_SCOPE_GLOBALS=OFF the referenced
global is now loaded from another global's init kernel, so
lowerGlobalsToKernelArgs keeps it program-scope, exactly as it already
does for a plain `__device__ int *P = &Scalar;` initializer.

Fixes #1640
@pvelesko

Copy link
Copy Markdown
Collaborator Author

/run-aurora-ci

@pvelesko
pvelesko marked this pull request as ready for review September 19, 2026 05:18
@pvelesko
pvelesko merged commit 151adb0 into main Sep 19, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Aggregate initializer holding another device global's address stores poison for that field

1 participant