Repository navigation
Conversation
DeviceBuffer::cast_elem, DeviceBuffer::cast_chunks, and Tensor::reinterpret reinterpreted device bytes as any element type of the right size and alignment, which let a u8 buffer become a DeviceBuffer<bool> or Tensor<bool> holding values other than 0 and 1 (NVIDIA#1506). The casts now require bytemuck::AnyBitPattern of their target, so bool is rejected at compile time, and bool implements DeviceCopy: a typed buffer holds valid values by construction, so bool buffers can be allocated, filled, launched, and read back like any other element type. cuda-core re-exports bytemuck, implements Pod for the DType newtypes, and enables half's bytemuck feature for f16 and bf16. The atomic wrappers in cuda-device are not Copy, so they cannot be AnyBitPattern. DeviceBuffer gains wrap_elem and peel_elem, bounded on bytemuck::TransparentWrapper, to view a scalar buffer as its atomic wrapper and back. cuda-device implements TransparentWrapper for its atomic types and Pod for its vector types, and the examples that cast into atomics use the new methods. Every example lock gains the bytemuck dependency of cuda-device. Signed-off-by: Melih Elibol <1884921+elibol@users.noreply.github.com>
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
|
So, as a first step I think the But we need to address two safety issues that popped up from trying to break the above setup via adversarial examples. 1. The first one is the atomic The intended operation is converting an owned integer buffer into an atomic buffer. But implementing use bytemuck::TransparentWrapper;
// Device code:
fn overwrite(value: &u32) {
let atomic = DeviceAtomicU32::wrap_ref(value);
atomic.store(7, AtomicOrdering::Relaxed);
}The conversion aliases the same memory: The original Proposed fix: remove the atomic pub unsafe trait DeviceRepr<Inner>: Sized {}Its documented contract would require matching layout, valid initialized representations in both directions, and no additional ownership or destruction obligations. It would provide no reference-conversion methods. The buffer’s let atomics = integers.wrap_elem::<DeviceAtomicU32>();
// Run the kernel and complete its accesses.
let integers = atomics.peel_elem::<u32>();
|
|
2. Suppose an allocation still contains old byte // writer and reader are independent GPU streams.
let flags = DeviceBuffer::<bool>::zeroed(&writer, 1)?;
let values = flags.to_host_vec(&reader)?;Both calls are safe Rust. However,
Proposed fix: the simplest repair is to finish initialization before safe // After queuing the zero-fill:
stream.synchronize()?;
Ok(buf)To preserve asynchronous initialization, retain its completion dependency and make every safe consumer honor it, including readback and kernel launches. Asking callers to synchronize in documentation does not repair the safe API. Our GPU probe observed byte I think the initialization-ordering code predates this PR and it only surfaced after admitting |
Closes #1506. First change under #1460; the
booldecision recorded there and in #1461 and #1503 is implemented here.The hole
DeviceBuffer::cast_elem,DeviceBuffer::cast_chunks, andTensor::reinterpretare safe and checked only size and alignment, so au8buffer holding2could become aDeviceBuffer<bool>or aTensor<bool>. Reading that value as aboolis undefined behavior, on the host throughto_host_vecand on the device inside a kernel.The fix
The three casts now require their target to implement
bytemuck::AnyBitPattern: every bit pattern is a valid value, so no byte written asTcan become an invalidA.boolis notAnyBitPattern, and no downstream crate can make it one, so casting intoboolis a compile error. The trybuild cases undercuda-core/tests/device_buffer_casts/andcutile/tests/ui/reinterpret_bool.rspin the diagnostics.boolimplementsDeviceCopy. The trait documentation now states the rule:DeviceCopypromises a byte-copyable type with a valid zero pattern, not that every bit pattern is valid. A typed buffer holds valid values by construction (from_host,zeroed, kernels writing through&mut T,unsaferaw constructors), and the casts are the one place arbitrary bytes could become typed values, which the bound closes.boolbuffers can now be allocated, filled, launched, and read back like any other element type.cuda-core takes
bytemuckas a dependency and re-exports it (cuda_core::bytemuck) so downstream crates implement the marker traits against the same crate. TheDTypenewtypes (tf32, thef8*types,f4e2m1fn,f4e2m1fnx2,i4) implementPod, andhalf'sbytemuckfeature coversf16andbf16, so every numericTensor::reinterprettarget still compiles.Atomics:
wrap_elemandpeel_elembytemuck::AnyBitPatternrequiresCopy, and the cuda-device atomic wrappers hold anUnsafeCell, so they can never satisfy the newcast_elembound. The documented atomic-slice mapping from #151 moves to two new methods:DeviceBuffer::wrap_elem<W: TransparentWrapper<T>>views a scalar buffer as its wrapper, andpeel_elemgoes back so the result can be read on the host.TransparentWrapperpromises the same layout and the same valid values as the inner type without requiringCopy, which is exactly the relation the atomics have to their integers.cuda-device change, for Nihal's review: cuda-device takes
bytemuck = { version = "1", default-features = false }and the two atomic macros gainunsafe impl bytemuck::TransparentWrapper<$ty> for $Name {}. The orphan rule leaves no other place for that impl: cuda-core cannot name these types, and cuda-device cannot depend on cuda-core. The vector types (F32x4and friends) implementPod, sofloats.cast_chunks::<F32x4>()works as the module docs describe. The examples that cast into atomics (hashmap,hashmap_v2,hashmap_v3,legacy_atomic_fadd,legacy_atomic_rmw_cas) callwrap_elemandpeel_elem;cast_chunks'sQuadimplementsPod. Every example lock and the device-only fixture lock gain cuda-device's new dependency at the samebytemuck 1.25.0the cuda-oxide and cutile-rs locks already resolve to; no other pin moved.The earlier draft of #1506 also wanted
T: DeviceCopyonKernelSliceArg. That is dropped, and the issue says why: the atomic wrappers are legitimate kernel element types but notDeviceCopy, and the constructor invariant above already covers the launch boundary.Compatibility
Adding a bound to
cast_elem,cast_chunks,can_cast_chunks, andTensor::reinterpretis a breaking change for callers casting into types that are notAnyBitPattern. In this tree those were only the atomic casts, nowwrap_elem/peel_elem. Tuples are no longer valid cast targets either, since their layout is unspecified; tuple element types still work everywhere else. The cutile-rs changelog records thereinterpretchange under Unreleased.Checks
cargo fmt --check,cargo clippy -p cuda-core --all-targets -D warnings,cargo test -p cuda-core(all suites, GPU included),cargo docwith warnings denied.cargo fmt --check,cargo clippy -p cutile --all-targets -D warnings,cargo test -p cutile --test ui,cargo test -p cutile --test tensor_reinterpret(GPU).cargo clippy -p cuda-device -p cuda-host --all-targets -D warnings,cargo test -p cuda-device,cargo oxide fmt --check, andcargo oxide runfor the six touched examples on an RTX 5090, which also shows bytemuck compiles through the device backend.sync-example-locks.sh --check,check-dependency-licenses.sh(newbytemuckrow),check-device-only-build.sh,check-spdx-headers.sh,check-source-references.sh, andcargo deny checkfor the root, cuda-oxide, backend, device-only, and cutile-rs workspaces.cargo semver-checks -p cuda-coreagainst the crates.io 0.4.0 release reports no semver update required.One pre-existing thing I did not touch:
cutile/tests/ui/graph_scope_rejects_allocating_input.stderrhas not matched since the monorepo move (it records a$WORKSPACE-relative path into cuda-async, which trybuild can no longer normalize fromcutile-rs/), andrun_cpu_tests.shdoes not run theuitarget, so CI does not notice. Every other ui case, including the new one, passes locally. Worth a follow-up.