Repository navigation
fix(mir-importer): emit promoted refs as globals - #1726
Open
praveenperera wants to merge 1 commit into
Open
praveenperera wants to merge 1 commit into
praveenperera wants to merge 1 commit into
Conversation
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 3, 2026
A reference constant such as `const PAIR: &(u32, u32) = &(3, 4)`, or a borrow rustc promotes to 'static such as `&42`, was materialized as an alloca, a store, and the slot's address in whichever function named it. The reference is 'static, so safe code can return it: an stack frame, and LLVM deleted the store. Raw pointers formed from such references (`&42 as *const u32 as *mut u32`) had the same problem. Each anonymous rustc allocation a reference constant points at now becomes one module-level read-only device global holding rustc's byte image, keyed by its AllocId so every reference into it, including interior pointers such as `&PAIR.1`, shares one global. Pointers stored inside the allocation become relocations to the inner allocation's own global or to the device static. The same globals back thin pointer fields of aggregate and enum constants and pointer words in device static initializers, which previously rejected anonymous memory. A `*mut T` into that read-only storage is established as `*const T` and then cast raw-to-raw, as the source's `as` cast does; the storage stays read-only. A pointee whose device layout the importer cannot prove byte-identical to rustc's, such as `&Option<u32>`, declares the same global typed as the pointee. Lowering now accepts several types for one immutable initialized global, validates each against the shared bytes, and keeps one symbol, so `&Option<u32>` and `&u32` into its payload still compare equal. An interior view is rejected when its pointee's layout cannot be proven. A pointer into shared memory cannot be stored in a constant or static. Every stored pointer targets an initialized global in the global or constant address space, while a pointer to SharedArray or Barrier is an addrspace(3) pointer. A constant, constant table, or static initializer that stores a relocation and whose type holds such a pointer, through fields, closure captures, or pointees, is refused, as is a type-erased stored pointer to a SharedArray or Barrier static, which has no initialized global. The promoted_ref_return example returns each form from an device, and verifies that no helper keeps a stack slot or a .local depot. error_shared_pointer_constant and error_shared_static_relocation pin the two shared-memory refusals. Closes NVIDIA#1724 From upstream NVIDIA#1726, which is based on a main without NVIDIA#1722. Merged here, a reference to a constant that holds a DisjointSlice is still refused before the new global path, so error_disjoint_slice_const_ref keeps its diagnostic. Signed-off-by: Praveen Perera <me@praveenperera.com>
A reference constant such as `const PAIR: &(u32, u32) = &(3, 4)`, or a borrow rustc promotes to 'static such as `&42`, was materialized as an alloca, a store, and the slot's address in whichever function named it. The reference is 'static, so safe code can return it: an #[inline(never)] device helper handed its caller a pointer into a dead stack frame, and LLVM deleted the store. Raw pointers formed from such references (`&42 as *const u32 as *mut u32`) had the same problem. Each anonymous rustc allocation a reference constant points at now becomes one module-level read-only device global holding rustc's byte image. Every reference into it, including interior pointers such as `&PAIR.1`, shares that global. The global is keyed by a SHA-256 digest of its contents: alignment, bytes, and relocations resolved to their targets' own keys. rustc's AllocId numbering follows interning order, so keying by it renamed a global whenever unrelated constants were added. Identical read-only allocations share one global, which Rust permits. Pointers stored inside the allocation become relocations to the inner allocation's own global or to the device static. The same globals back thin pointer fields of aggregate and enum constants and pointer words in device static initializers, which previously rejected anonymous memory. A `*mut T` into that read-only storage is established as `*const T` and then cast raw-to-raw, as the source's `as` cast does; the storage stays read-only. A pointee whose device layout the importer cannot prove byte-identical to rustc's, such as `&Option<u32>`, declares the same global typed as the pointee. Lowering now accepts several types for one immutable initialized global, validates each against the shared bytes, and keeps one symbol, so `&Option<u32>` and `&u32` into its payload still compare equal. An interior view is rejected when its pointee's layout cannot be proven. A pointer into shared memory cannot be stored in a constant or static. Every stored pointer targets an initialized global in the global or constant address space, while a pointer to SharedArray or Barrier is an addrspace(3) pointer. A constant, constant table, or static initializer that stores a relocation and whose type holds such a pointer, through fields, closure captures, or pointees, is refused, as is a type-erased stored pointer to a SharedArray or Barrier static, which has no initialized global. The promoted_ref_return example returns each form from an #[inline(never)] helper, checks the values and pointer identity on the device, and verifies that no helper keeps a stack slot or a .local depot. error_shared_pointer_constant and error_shared_static_relocation pin the two shared-memory refusals. The example's check also splices in kernels with their own constants and requires every helper's global keys to stay the same. The compile-only smoketest now builds and runs the shape check of an example in the libNVVM lane before emit-ltoir, so CI runs it too. Closes NVIDIA#1724 Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera
force-pushed
the
fix/promoted-const-ref-static
branch
from
November 3, 2026 20:59
f4c42d1 to
b4346bc
Compare
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 3, 2026
Bring in the review update to upstream pull request NVIDIA#1726 (head b4346bc). A promoted reference-constant global was keyed by rustc's AllocId, which follows mono-item order, so adding unrelated constants or kernels renamed it and changed the PTX of code that had not changed. An immutable allocation is now keyed by a SHA-256 digest of its alignment, bytes and relocation targets, so its name stays put and identical read-only constants share one global. A mutable allocation keeps its AllocId key. The promoted_ref_return shape check now splices in extra kernels and requires every helper's globals to be unchanged, and the compile-only smoketest runs that check. Keep this branch's module doc diagram, which shows the hashed __device_global_<crate>_<hash> name lowering emits here. Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 4, 2026
The promoted allocation module doc drew the LLVM global as @__device_global_0, the counter name lowering used before NVIDIA#1728. Lowering now names an ordinary device global from a hash of its key, prefixed by the crate disambiguator when one is set, so the diagram shows @__device_global_<crate>_<hash>. Carried over from the fork's 08ac925, which kept this diagram when it adopted upstream NVIDIA#1726. Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 5, 2026
The promoted allocation module doc drew the LLVM global as @__device_global_0, the counter name lowering used before NVIDIA#1728. Lowering now names an ordinary device global from a hash of its key, prefixed by the crate disambiguator when one is set, so the diagram shows @__device_global_<crate>_<hash>. Carried over from the fork's 08ac925, which kept this diagram when it adopted upstream NVIDIA#1726. Signed-off-by: Praveen Perera <me@praveenperera.com>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1724
Problem
A reference constant such as
const PAIR: &(u32, u32) = &(3, 4), or a borrow rustc promotes to'staticsuch as&42, was built as an alloca, a store, and the slot's address inside whichever function named it. The reference is'static, so safe code can return it. An#[inline(never)]device helper that did so handed its caller a pointer into a dead stack frame, and the caller read zeros.Raw pointers formed from such references had the same problem. On
main,const RAW: *mut u32 = &59_u32 as *const u32 as *mut u32compiles but returns the same dangling stack pointer.Fix
One global per allocation. Each anonymous rustc allocation behind a reference constant becomes one module-level, read-only device global holding rustc's byte image. Every reference into one allocation shares its global, including interior pointers such as
&PAIR.1.Stable keys. Each global is keyed by a SHA-256 digest of its contents: alignment, bytes, and relocations resolved to their targets' own keys. Its identity is therefore the same in every build: adding unrelated constants or kernels doesn't rename it, unlike a key based on rustc's interning-ordered
AllocId. Two read-only allocations with identical contents share one global, which Rust permits because it does not promise constants distinct addresses. Mutable anonymous memory, such as&mut [..]in astatic mutinitializer, is never merged.Nested pointers. Pointers stored inside an allocation become initializer relocations to the inner allocation's own global, or to the device static.
Aggregate fields and static initializers. The same globals back thin pointer fields of tuple, struct, array, and enum constants, and pointer words in device static initializers (
static S: &[u32] = &[1, 2]). Both previously rejected anonymous memory.Typed views. When the importer cannot prove a pointee's device layout byte-identical to rustc's, for example
&Option<u32>, the reference declares the same global typed as the pointee. Lowering accepts several types for one immutable initialized global, validates each type against the shared bytes, and keeps one symbol. As a result&Option<u32>and&u32into its payload still compare equal. An interior view is rejected when its pointee's layout cannot be proven.*mut Tinto read-only storage. Such a pointer is established as*const Tand then cast raw-to-raw, matching the source'sascast. The storage stays read-only.Shared memory. A pointer into a
SharedArrayorBarriercannot be stored in a constant or a static. Those pointers areaddrspace(3), but every stored pointer targets an initialized global. Two cases are refused at compile time:SharedArrayorBarrierstatic.The type check is conservative. A value whose type holds a shared-memory pointer is refused whenever it stores any relocated pointer, even if the shared-memory pointer itself is null.
Not changed: a direct
&[T; N]constantA direct
&[T; N]reference constant still uses the existing content-keyed promoted-table path. Forconst A: &[u32; 2] = &[3, 4]andconst E: &u32 = &A[0],AandEcan therefore get separate globals with the same bytes, andcore::ptr::eq(&A[0], E)may be false.This is allowed. The Rust Reference says constants are inlined wherever they are used, and that references to the same constant are not necessarily guaranteed to refer to the same memory address. Address identity is guaranteed only within a single rustc allocation, and every reference into one allocation shares its global.
Tests
promoted_ref_return. Each form above is returned from an#[inline(never)]helper, and the kernel reads back 20 values, including a pointer-identity check. The forms are:Option<&T>;*mutconstants.Its
verify-code-shape.shchecks that no helper keeps an alloca or a.localdepot, and that the constants live in read-onlyaddrspace(1)globals, with one global per shared allocation. It also builds a copy of the example with extra kernels that carry their own constants, and checks that every helper's promoted global keeps the same key.CI coverage. The compile-only lane for libNVVM-verified examples now builds each example normally and runs its
verify-code-shape.shbeforeemit-ltoir, so these checks, andkernel_reference_validity's, run in CI rather than only in GPU smoketests.Negative examples.
error_shared_pointer_constantanderror_shared_static_relocationpin the two shared-memory diagnostics.Lowering.
mir-lowertests cover sharing one global across typed views in both declaration orders, and rejecting a view that does not match its bytes.Follow-up
The
DisjointSlicereference-constant refusal added in #1722 (fixtureerror_disjoint_slice_const_ref) can be lifted on top of this change.