Repository navigation
Description
What problem does this solve?
One or two sentences on the gap this fills.
Proposed API / behavior
Show the Rust you would like to write (no need to get # Feature request: borrowed DeviceBuffer subviews for typed kernel launches
Summary
Could cuda-core provide non-owning immutable and mutable views over contiguous
subranges of a DeviceBuffer<T>, with direct support in the generated typed
launcher API?
Possible names are:
DeviceSlice<'a, T>
DeviceSliceMut<'a, T>This request is limited to host-side borrowing and kernel argument lowering. It
does not require strided views, device-side slice projection, or ownership of an
interior allocation pointer.
Motivation
A larger device allocation often contains several logical vectors, matrix rows,
or temporary regions. A kernel should be able to operate directly on one
contiguous region without this sequence:
- allocate another
DeviceBuffer; - copy the region into it;
- launch the kernel;
- copy the result back.
Constructing an owning DeviceBuffer from an interior pointer is not a valid
workaround. The resulting value could free an interior address or cause the
original allocation to be freed twice.
The desired usage is approximately:
let mut buffer = DeviceBuffer::<f32>::zeroed(&stream, rows * cols)?;
let mut row = buffer.slice_mut(start..start + cols)?;
module.scale(
&stream,
&prepared,
&mut row,
2.0,
)?;The subview must borrow the original allocation, carry its adjusted device
pointer and element count, and never free memory.
Current downstream solution
We currently implement this outside CUDA-Oxide with two borrowing wrappers and
two ABI descriptors.
The host-side wrappers retain the borrow with PhantomData:
pub struct DeviceSpan<'a, T> {
ptr: *const T,
len: usize,
_borrow: PhantomData<&'a [T]>,
}
pub struct DeviceSpanMut<'a, T> {
descriptor: DeviceSliceMutDescriptor<T>,
_borrow: PhantomData<&'a mut [T]>,
}Kernel arguments use explicit #[repr(C)] descriptors:
#[repr(C)]
pub struct DeviceSliceDescriptor<T> {
pub ptr: *const T,
pub len: usize,
}
impl<T> Copy for DeviceSliceDescriptor<T> {}
impl<T> Clone for DeviceSliceDescriptor<T> {
fn clone(&self) -> Self { *self }
}
#[repr(C)]
pub struct DeviceSliceMutDescriptor<T> {
pub ptr: *mut T,
pub len: usize,
}
impl<T> Copy for DeviceSliceMutDescriptor<T> {}
impl<T> Clone for DeviceSliceMutDescriptor<T> {
fn clone(&self) -> Self { *self }
}Subview construction validates the element range and uses checked byte-offset
arithmetic before adjusting DeviceBuffer::cu_deviceptr():
impl<'a, T> DeviceSpan<'a, T> {
pub fn from_buffer(
buffer: &'a DeviceBuffer<T>,
offset: usize,
len: usize,
) -> Self {
assert!(offset <= buffer.len());
let end = offset.checked_add(len).expect("device span overflow");
assert!(end <= buffer.len());
let byte_offset = offset
.checked_mul(core::mem::size_of::<T>())
.expect("device span byte offset overflow");
let ptr = buffer
.cu_deviceptr()
.checked_add(byte_offset as u64)
.expect("device span pointer overflow");
Self {
ptr: ptr as usize as *const T,
len,
_borrow: PhantomData,
}
}
}The descriptor can be passed by value through the generated typed launcher:
#[kernel]
pub fn slice_for_each<F>(span: DeviceSliceMutDescriptor<f32>, f: F)
where
F: Fn(f32) -> f32 + Copy,
{
let index = thread::index_1d().get();
if index < span.len {
unsafe {
let element = span.ptr.add(index);
element.write(f(element.read()));
}
}
}This workaround is functional. It currently supports:
- immutable and exclusive mutable contiguous borrows;
- checked
(offset, len)construction; - consuming mutable-to-immutable downgrade;
- consuming split/chunk operations that produce disjoint mutable spans;
- direct kernel operations on the span;
- device-to-device copying into a newly owned buffer;
- concatenating several immutable spans into a new buffer.
It also confirms that subviews do not require a new CUDA allocation primitive:
an adjusted pointer, a length, and a borrow of the original buffer are enough.
Limitations of the workaround
Although the custom descriptor works with typed launches, every downstream
crate must reimplement the same unsafe boundary:
- define and maintain a private pointer/length ABI;
- repeat range and byte-offset validation;
- use raw-pointer reads and writes in every kernel;
- replace native
&[T]andDisjointSlice<T>parameters with custom structs; - manually preserve immutable and exclusive mutable borrowing rules;
- maintain separate wrapper APIs for buffers and subviews.
Most importantly, a kernel written with the standard CUDA-Oxide parameter types
cannot accept the downstream span directly:
input: &[T]
output: DisjointSlice<T>Changing those parameters to custom descriptors gives up the standard slice
ergonomics and the launcher's native slice-specific integration. The workaround
therefore solves the immediate memory problem, but it does not provide
interoperability with existing kernels or libraries.
Proposed API
One possible host API is:
impl<T> DeviceBuffer<T> {
pub fn slice<R>(&self, range: R) -> Result<DeviceSlice<'_, T>, SliceError>
where
R: RangeBounds<usize>;
pub fn slice_mut<R>(
&mut self,
range: R,
) -> Result<DeviceSliceMut<'_, T>, SliceError>
where
R: RangeBounds<usize>;
}The view types could expose only the small set of operations needed to compose
safe subranges:
DeviceSlice::len
DeviceSlice::is_empty
DeviceSlice::split_at
DeviceSlice::chunks
DeviceSliceMut::len
DeviceSliceMut::is_empty
DeviceSliceMut::split_at_mut
DeviceSliceMut::chunks_mut
DeviceSliceMut::into_sharedAn immutable view may be Copy/Clone if that matches the existing host API.
A mutable view must not be Copy or Clone. Operations returning multiple
mutable views must prove that the resulting ranges are disjoint.
An iterator return type for chunks/chunks_mut would also be sufficient; a
Vec is not required.
Typed launcher integration
Ideally, a generated launch method would accept a DeviceSlice anywhere the
kernel declares &[T], and a DeviceSliceMut anywhere it declares a mutable
slice-like output such as DisjointSlice<T>.
Conceptually, both view types lower to the existing slice ABI:
adjusted device pointer + view element count
The launch contract and bounds metadata must use the view length rather than the
length of the parent allocation.
If accepting multiple host argument types directly is undesirable, a public
sealed argument trait or an explicit conversion into the launcher's existing
slice argument representation would also solve the interoperability problem.
The important property is that converting a borrowed subview must not allocate,
copy, or transfer ownership.
Safety requirements
- Range bounds, element offsets, byte offsets, and pointer addition must be
checked. - A view must never free the underlying allocation.
- A view must not outlive its parent
DeviceBuffer. - Immutable views may alias only according to ordinary shared-borrow rules.
- Mutable views must retain an exclusive borrow of the parent allocation.
- APIs producing multiple mutable views must guarantee disjoint ranges.
- The adjusted pointer must remain associated with the original CUDA context.
- Empty ranges must be representable without dereferencing their pointer.
- Asynchronous launch and drop behavior should provide the same guarantees as
launching with the parentDeviceBufferdirectly.
Scope
The requested views are intentionally contiguous. A matrix column, strided
tensor region, or arbitrary gather is outside this proposal and should be
transposed or physically rearranged before taking a view.
This request is also not asking for:
- a new owning buffer constructor from an interior pointer;
- changes to device-side
DisjointSliceindexing; - device-side Rust subslice lowering;
- a general tensor layout or stride abstraction;
- upstream versions of downstream operations such as reductions or
concatenation.
The minimal useful upstream feature is a safe host-side borrow plus typed
launcher compatibility.
Acceptance criteria
A complete solution would allow all of the following without allocation or
copying:
- create an immutable contiguous subview of
DeviceBuffer<T>; - create an exclusive mutable contiguous subview;
- split a mutable view into proven-disjoint regions;
- pass immutable views to kernels declared with standard immutable slice
parameters; - pass mutable views to kernels declared with standard mutable slice-like
parameters; - have generated launch validation use the subview length.
Activity
nihalpasham commentedon Sep 3, 2026
I believe uurl's #1081 has the design we want: a view is a device pointer plus a length that borrows its parent buffer; the mutable view is not
Copyand splits by value, so overlapping mutable views cannot exist.But a couple of things:
DeviceBuffernow lives there, soDeviceSlice/DeviceSliceMutandslice()/slice_mut()belong there too.&impl KernelSliceArg; the sync ones will follow.Stop-gap in the meantime: with the
asyncfeatureKernelSliceArgis public, so yourDeviceSpancan implement it and feed the async launchers today.Plan: propose the view types on cutile-rs first (I can carry it in #1102's gap list), then #1081's launcher half once #1102 lands.
nihalpasham commentedon Sep 15, 2026
Reviewed #1081: partial; the owner prerequisite remains.
DeviceSlice/DeviceSliceMutmust land in cutile-rs first; current cuda-core 0.3.1 has no such API.Keeping this issue open.
feat: accept any kernel slice arg in generated launchers
elibol commentedon Oct 8, 2026
Tracks pull request NVlabs/cutile-rs#308 (NVlabs/cutile-rs#308) by @uurl, opened 2026-09-23, which implements this request: feat(cuda-core): add borrowed DeviceBuffer subviews. The branch was rebased onto cutile-rs main on 2026-10-07 and merges cleanly. @uurl: that repository is archived, so the pull request is closed there, not rejected; if you want to continue the work, please recreate it against NVIDIA/cuda-rust.
AGENTS.mdin NVIDIA/cuda-rust has a "Transplanting a cutile-rs pull request" section that lets an agent do the replay.