Repository navigation
feat(cuda-device): add safe DisjointSlice::empty - #1722
Open
praveenperera wants to merge 1 commit into
Open
praveenperera wants to merge 1 commit into
praveenperera wants to merge 1 commit into
Conversation
This was referenced Nov 3, 2026
praveenperera
force-pushed
the
feat/disjoint-slice-empty-1693
branch
from
November 3, 2026 16:13
2b23689 to
d5e521f
Compare
A device helper with an optional output takes a DisjointSlice and skips writes when it is empty, but every constructor is unsafe, so callers that do not want the output needed an unsafe block for a value that cannot be misused. Add a const DisjointSlice::empty() and a Default impl for every index space. A zero-length slice covers no memory, and every safe accessor in every space checks the slice length before resolving an element, so each one returns None. The pointer is NonNull::dangling(), and spaces with a runtime row width carry a width of zero, which their accessors also reject. SpaceLayout gains a sealed EMPTY constant for that layout value and documents the length-check rule new spaces must keep. rustc folds empty() into a constant allocation, which mir-importer had no decoder for. Decode a slice constant field by field at its layout offsets into the same construct_disjoint_slice op a slice literal produces. A top-level slice constant, one nested in a constant aggregate, and an array of slices all go through the existing allocation decoder first, which rejects pointer provenance and zeroes padding. A shared reference to such a constant would be materialized in the current function and its address taken, so a helper returning it as &'static would hand back a dead local. Import refuses a reference to a constant holding a slice instead; error_disjoint_slice_const_ref pins that diagnostic. A raw pointer constant without provenance, such as a null one, names no storage and still compiles. The disjoint_from_raw_parts example now passes empty() and default() to helpers, by value, by reference and as named constants, and host tests check that every accessor returns None on an empty slice where it resolves the same witness on a non-empty one. Closes NVIDIA#1693 Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera
force-pushed
the
feat/disjoint-slice-empty-1693
branch
from
November 3, 2026 17:01
d5e521f to
8e4b2e8
Compare
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 3, 2026
Bring DisjointSlice::empty to the reviewed version of upstream NVIDIA#1722. Every index space gets a Default impl, and SpaceLayout gains a sealed EMPTY constant and documents the length-check rule new spaces keep. mir-importer decodes a slice constant field by field into the same construct op a slice literal produces, including slices nested in constant aggregates and arrays. A reference to a constant that holds a slice is now refused, since it would point at a dead local; error_disjoint_slice_const_ref pins that diagnostic. The disjoint_from_raw_parts example passes empty() and default() by value, by reference and as named constants. Signed-off-by: Praveen Perera <me@praveenperera.com>
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>
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 4, 2026
Static shared allocations were named `__shared_mem_N` from a counter in MIR walk order, and ordinary device globals `__device_global_N` the same way. Walk order follows rustc's mono-item order, so adding a kernel renumbered the other kernels' globals and changed their PTX although their code had not changed. Release builds also keyed shared statics by the constant's Debug spelling, which carries walk-order dependent interner ids, while full-debug builds already used the mangled symbol. Key every shared static by its mangled Rust symbol in all build profiles, and name keyed shared allocations and device globals `__shared_mem_<crate>_<hash>` and `__device_global_<crate>_<hash>`, where the hash is a 64-bit FNV-1a of that key. The crate component stays the StableCrateId hash, since a static from a shared dependency hashes the same in every crate (NVIDIA#1277). References to one static still share one global, and a hash collision between two keys fails lowering rather than aliasing them. Keyless shared allocations keep the module counter. SharedArray is zero-sized, so a constant pointer to such a field of a tuple, struct or array static can have the same byte offset as its siblings, as union fields and enum variants do whatever their size. Find the field from the static's layout and the pointee type and add its path to the key, so each field gets its own stable global, and `*const` and `*mut` pointers to one field share it, as a static's single address requires. Two fields of one type at one offset cannot be told apart and would alias, so reject them with a compile error asking for one static per array. Add the shared_static_names example and a compile-only check that builds it with extra kernels spliced in, in release and full-debug builds, and requires the original kernels' PTX, field pointers included, to stay the same. CI runs it in examples-compile. Add the error_shared_static_same_fields and error_shared_static_union_field fixtures for the rejected cases. Fixes NVIDIA#1477 Stacked on NVIDIA#1722: the smoketest ERROR_EXAMPLES list keeps NVIDIA#1722's error_disjoint_slice_const_ref and adds this change's error_shared_static_same_fields and error_shared_static_union_field. Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 4, 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 #[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 Stacked on NVIDIA#1722 and NVIDIA#1728. A shared 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, and the operand and statics imports keep what NVIDIA#1722 and NVIDIA#1728 use. The smoketest ERROR_EXAMPLES list and the STATUS.md fixture table carry all three changes' fixtures, and NVVM_VERIFY_EXAMPLES gains promoted_ref_return. Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 5, 2026
Static shared allocations were named `__shared_mem_N` from a counter in MIR walk order, and ordinary device globals `__device_global_N` the same way. Walk order follows rustc's mono-item order, so adding a kernel renumbered the other kernels' globals and changed their PTX although their code had not changed. Release builds also keyed shared statics by the constant's Debug spelling, which carries walk-order dependent interner ids, while full-debug builds already used the mangled symbol. Key every shared static by its mangled Rust symbol in all build profiles, and name keyed shared allocations and device globals `__shared_mem_<crate>_<hash>` and `__device_global_<crate>_<hash>`, where the hash is a 64-bit FNV-1a of that key. The crate component stays the StableCrateId hash, since a static from a shared dependency hashes the same in every crate (NVIDIA#1277). References to one static still share one global, and a hash collision between two keys fails lowering rather than aliasing them. Keyless shared allocations keep the module counter. SharedArray is zero-sized, so a constant pointer to such a field of a tuple, struct or array static can have the same byte offset as its siblings, as union fields and enum variants do whatever their size. Find the field from the static's layout and the pointee type and add its path to the key, so each field gets its own stable global, and `*const` and `*mut` pointers to one field share it, as a static's single address requires. Two fields of one type at one offset cannot be told apart and would alias, so reject them with a compile error asking for one static per array. Add the shared_static_names example and a compile-only check that builds it with extra kernels spliced in, in release and full-debug builds, and requires the original kernels' PTX, field pointers included, to stay the same. CI runs it in examples-compile. Add the error_shared_static_same_fields and error_shared_static_union_field fixtures for the rejected cases. Fixes NVIDIA#1477 Stacked on NVIDIA#1722: the smoketest ERROR_EXAMPLES list keeps NVIDIA#1722's error_disjoint_slice_const_ref and adds this change's error_shared_static_same_fields and error_shared_static_union_field. Signed-off-by: Praveen Perera <me@praveenperera.com>
praveenperera
added a commit
to praveenperera/cuda-rust
that referenced
this pull request
Nov 5, 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 #[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 Stacked on NVIDIA#1722 and NVIDIA#1728. A shared 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, and the operand and statics imports keep what NVIDIA#1722 and NVIDIA#1728 use. The smoketest ERROR_EXAMPLES list and the STATUS.md fixture table carry all three changes' fixtures, and NVVM_VERIFY_EXAMPLES gains promoted_ref_return. 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.
Problem
A device helper with an optional output takes a
DisjointSlice<T>and skips writes when it is empty. EveryDisjointSliceconstructor isunsafe, so a caller that does not want the output wrote anunsafeblock, with a SAFETY comment, for a value that cannot be misused.Change
DisjointSlice::empty(), aconst fnreturning a slice withNonNull::dangling()and length 0, andDefaultfor every index space. A zero-length slice covers no memory, and every safe accessor in every space checks the length before it resolves an element, so each returnsNone. The doc comment walks through each accessor.Runtime2DIndex,RuntimeRowMajorTiles) are covered too: their empty slice carries a row width of 0, whichget_mut,tile_2d32_rtandthread::index_2d_runtimealready reject.SpaceLayoutgains a sealedEMPTYconstant for that layout value and documents the length-check rule a new space must keep.empty()into a constant allocation, which import could not decode. ADisjointSliceconstant is now rebuilt field by field, at rustc's layout offsets, into the sameconstruct_disjoint_sliceop a slice literal produces. It goes through the existing allocation decoder first, which rejects pointer provenance and zeroes padding. This covers a top-level constant, a slice nested in a constant aggregate, and an array of slices.const E: &DisjointSlice<f32> = &DisjointSlice::empty()) is refused with a diagnostic. Import materializes a promoted reference's pointee in the current function, so a helper returning it as&'staticwould hand back a dead local. Every accessor takes&mut self, so such a reference has little use, and&mut DisjointSlice::empty()and by-value passing both work. Raw pointer constants without provenance, such as a null*mut DisjointSlice, still compile.Tests
cuda-devicecheck that every accessor (get_mut,element_thread32,tile_thread32,thread_run32,grid_stride_runs32,tile_2d32,tile_2d32_rt) returnsNoneon an empty slice where the same witness resolves on a non-empty one, and that the pointer is non-null and aligned.disjoint_from_raw_partsgainsskip_partial, which passesempty()anddefault()by value and by&mut. It also gainsskip_partial_consts, which passes named constants: a flat slice, a runtime-width slice whose image ends in padding, a tuple holding a slice beside a scalar, and an array of two slices. That kernel also reads null and dangling raw pointers to a slice back from#[inline(never)]helpers. Without the importer change the example fails to compile (Unsupported constant type).error_disjoint_slice_const_refpins the reference diagnostic, and it compiles (and so fails the smoketest) without the refusal.Validation
cargo test -p cuda-device -p mir-importer, clippy with-D warningsfor both crates and both examples,cargo oxide fmt --checkcheck-spdx-headers.sh,sync-example-locks.sh --check,check-book-catalog-stamp.sh,check-error-example-status.sh,check-example-smoketest-contract.sh,just booksmoketest.sh --compile-onlyfor both examplescargo oxide run disjoint_from_raw_partson an RTX 5090: all four kernels match exactlyCloses #1693