Skip to main content

dspk_ops/core/
buffer.rs

1//! Typed device buffers: sizes come from the element type, never from a literal byte count.
2
3use cubecl::prelude::*;
4use cubecl::bytes::Bytes;
5use cubecl::server::Handle;
6use dspk_core::compute::MAX_DEVICE_ELEMENTS;
7
8/// Bytes of `len` elements of `E`, at least one element (runtimes reject empty allocations).
9///
10/// # Panics
11/// If `len` exceeds [`MAX_DEVICE_ELEMENTS`] (kernels index with 32-bit integers): sizes chosen by
12/// users are checked earlier with [`dspk_core::compute::device_elements`], so this is a backstop
13/// against a silent wrap.
14pub fn bytes<E: CubeElement>(len: usize) -> usize {
15    assert!(len <= MAX_DEVICE_ELEMENTS, "device buffer of {len} elements exceeds the 32-bit index limit ({MAX_DEVICE_ELEMENTS})");
16    len.max(1) * size_of::<E>()
17}
18
19/// Uninitialized device buffer of `len` elements of `E`.
20pub fn empty<E: CubeElement>(client: &Client, len: usize) -> Handle {
21    client.empty(bytes::<E>(len))
22}
23
24/// Device copy of `data`.
25///
26/// # Panics
27/// If `data` holds more than [`MAX_DEVICE_ELEMENTS`] elements (see [`bytes`]).
28pub fn upload<E: CubeElement>(client: &Client, data: &[E]) -> Handle {
29    bytes::<E>(data.len());
30    // `create` with owned bytes: `create_from_slice` copies through a slower general path
31    // (≈2–3× longer for a 92 MB window on wgpu and CUDA; `benchmarks/kernels/tests/bench_upload.rs`)
32    client.create(Bytes::from_elems(data.to_vec()))
33}
34
35/// Writes `data` to the start of the existing device buffer `handle` (which holds at least
36/// `data.len()` elements of `E`): no allocation, enqueued after the work already queued on the
37/// client's stream, so a buffer reused every window is not overwritten before the previous window's
38/// kernels have read it.
39pub fn write<E: CubeElement>(client: &Client, handle: &Handle, data: &[E]) {
40    bytes::<E>(data.len());
41    write_bytes(client, handle, Bytes::from_elems(data.to_vec()));
42}
43
44/// [`fn@write`] taking ownership of `data`: the bytes move into the transfer without a copy (useful
45/// when the buffer was filled on another thread, e.g. a read-ahead loader).
46pub fn write_owned<E: CubeElement>(client: &Client, handle: &Handle, data: Vec<E>) {
47    bytes::<E>(data.len());
48    write_bytes(client, handle, Bytes::from_elems(data));
49}
50
51/// Writes `data` to the start of `handle`, narrowed to `data`'s length: a reused buffer is often
52/// larger than the chunk written into it, and the CPU runtime copies the whole bound range (a write
53/// shorter than its handle panics there; wgpu and CUDA accept it).
54///
55/// # Panics
56/// If `data` is longer than the bytes `handle` addresses.
57pub fn write_bytes(client: &Client, handle: &Handle, data: Bytes) {
58    let (len, used) = (data.len() as u64, handle.size_in_used());
59    assert!(len <= used, "writing {len} bytes into a device buffer of {used}");
60    client.write(&handle.clone().offset_end(used - len), data);
61}
62
63/// Device buffer of `len` zeros, filled on the device (cubecl-std's zero kernel): nothing is
64/// uploaded.
65pub fn zeros<E: CubeElement + Scalar>(client: &Client, len: usize) -> Handle {
66    bytes::<E>(len);
67    cubecl::std::tensor::TensorHandle::zeros(client, [len.max(1)], E::elem_type_native()).handle
68}
69
70/// Device buffer of `len` copies of `value`, filled on the device: nothing is uploaded.
71pub fn filled<F: crate::core::DspFloat>(client: &Client, len: usize, value: F) -> Handle {
72    let out = empty::<F>(client, len);
73    if len > 0 {
74        let geom = dspk_core::compute::LaunchGeometry::elementwise(client, len);
75        unsafe {
76            super::kernels::fill_kernel::launch::<F>(
77                client,
78                geom.cube_count,
79                geom.cube_dim,
80                BufferArg::from_raw_parts(out.clone(), len),
81                value,
82                len as u32,
83            );
84        }
85    }
86    out
87}
88
89/// Sets the first `len` values of `handle` (which holds at least `len` values of `F`) to `value`,
90/// on the device.
91pub fn fill<F: crate::core::DspFloat>(client: &Client, handle: &Handle, len: usize, value: F) {
92    if len == 0 {
93        return;
94    }
95    let geom = dspk_core::compute::LaunchGeometry::elementwise(client, len);
96    // SAFETY: `handle` holds at least `len` values of `F`
97    unsafe {
98        super::kernels::fill_kernel::launch::<F>(
99            client,
100            geom.cube_count,
101            geom.cube_dim,
102            BufferArg::from_raw_parts(handle.clone(), len),
103            value,
104            len as u32,
105        );
106    }
107}
108
109/// Copies the first `len` values of `F` of `src` into `dst`, on the device.
110pub fn copy<F: crate::core::DspFloat>(client: &Client, src: &Handle, dst: &Handle, len: usize) {
111    if len == 0 {
112        return;
113    }
114    // SAFETY: both views cover `len` contiguous values of buffers that hold them
115    let (src, dst) = unsafe {
116        (
117            TensorBinding::from_raw_parts(src.clone(), cubecl::zspace::Strides::new(&[1]), cubecl::zspace::Shape::new([len])),
118            TensorBinding::from_raw_parts(dst.clone(), cubecl::zspace::Strides::new(&[1]), cubecl::zspace::Shape::new([len])),
119        )
120    };
121    cubecl::std::tensor::copy_into(client, src, dst, F::elem_type_native());
122}
123
124/// Host copy of a device buffer of `E` (waits for the queued work that writes it).
125pub fn download<E: CubeElement>(client: &Client, handle: Handle) -> Vec<E> {
126    E::from_bytes(&client.read_one_unchecked(handle)).to_vec()
127}
128
129/// Host copy of the first `len` elements of `E` of a device buffer: only those bytes are read, so a
130/// buffer sized for the largest batch costs only the current batch's transfer.
131pub fn download_prefix<E: CubeElement>(client: &Client, handle: Handle, len: usize) -> Vec<E> {
132    let used = handle.size_in_used();
133    let want = (len * size_of::<E>()) as u64;
134    assert!(want <= used, "{len} elements exceed the buffer's {used} bytes");
135    let mut out = download::<E>(client, handle.offset_end(used - want));
136    out.truncate(len);
137    out
138}
139
140/// Host copy of elements `start .. start + len` of `E` of a device buffer: only those bytes are
141/// read.
142pub fn download_range<E: CubeElement>(client: &Client, handle: Handle, start: usize, len: usize) -> Vec<E> {
143    let used = handle.size_in_used();
144    let (from, want) = ((start * size_of::<E>()) as u64, (len * size_of::<E>()) as u64);
145    assert!(from + want <= used, "elements {start}..{} exceed the buffer's {used} bytes", start + len);
146    let mut out = download::<E>(client, handle.offset_start(from).offset_end(used - from - want));
147    out.truncate(len);
148    out
149}
150
151/// `handle` without its last `unused` elements of `E` (a view of a buffer larger than the data).
152pub fn truncate<E: CubeElement>(handle: Handle, unused: usize) -> Handle {
153    handle.offset_end((unused * size_of::<E>()) as u64)
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    fn fills_every_element(client: &Client) {
161        // Not a multiple of any cube size, so the last cube is partial
162        let len = 1_000_003;
163        let v: Vec<f32> = download(client, filled::<f32>(client, len, f32::MAX));
164        assert_eq!(v.len(), len);
165        assert!(v.iter().all(|&x| x == f32::MAX), "{}", client.name());
166    }
167    runtime_test!(test_filled, fills_every_element);
168
169    /// A chunk shorter than the reused buffer it is written into (a tail chunk): only its own
170    /// elements change, on every runtime (the CPU runtime once panicked on the length mismatch).
171    fn short_write_into_larger_buffer(client: &Client) {
172        let handle = upload::<f32>(client, &[7.0; 100]);
173        write(client, &handle, &[1.0f32; 10]);
174        write_owned(client, &handle, vec![2.0f32; 3]);
175        let v: Vec<f32> = download(client, handle);
176        assert_eq!(&v[..3], &[2.0; 3], "{}", client.name());
177        assert_eq!(&v[3..10], &[1.0; 7], "{}", client.name());
178        assert!(v[10..].iter().all(|&x| x == 7.0), "{}", client.name());
179    }
180    runtime_test!(test_short_write, short_write_into_larger_buffer);
181}