cubecl_utils_rs/tensor.rs
1//! GPU-resident tensor.
2
3use cubecl::prelude::*;
4use cubecl::server::Handle;
5use cubecl::zspace::striding::row_major_contiguous_strides;
6use cubecl::zspace::{Shape, Strides};
7use std::marker::PhantomData;
8
9use crate::errors::CubeclUtilsErrors;
10use crate::limits::{fits_binding, GpuLimits};
11
12///////////////
13// GpuTensor //
14///////////////
15
16/// GPU-resident tensor for use with CubeCL kernels.
17pub struct GpuTensor<R: Runtime, F: CubeElement + Numeric> {
18 /// Handle to the GPU buffer containing tensor data
19 data: Handle,
20 /// Dimensions of the tensor (e.g. `[n_rows, n_cols]`)
21 shape: Vec<usize>,
22 /// Memory strides for each dimension in row-major order
23 strides: Vec<usize>,
24 /// Phantom marker for the runtime type
25 _r: PhantomData<R>,
26 /// Phantom marker for the element type
27 _f: PhantomData<F>,
28}
29
30impl<R: Runtime, F: CubeElement + Numeric> Clone for GpuTensor<R, F> {
31 fn clone(&self) -> Self {
32 Self {
33 data: self.data.clone(),
34 shape: self.shape.clone(),
35 strides: self.strides.clone(),
36 _r: PhantomData,
37 _f: PhantomData,
38 }
39 }
40}
41
42impl<R: Runtime, F: Numeric + CubeElement> GpuTensor<R, F> {
43 /// Byte size of a tensor with the given shape.
44 ///
45 /// ### Params
46 ///
47 /// * `shape` - Dimensions of the tensor
48 ///
49 /// ### Returns
50 ///
51 /// Element count multiplied by the element size.
52 fn byte_size(shape: &[usize]) -> u64 {
53 (shape.iter().product::<usize>() * core::mem::size_of::<F>()) as u64
54 }
55
56 /// Create a tensor from CPU data.
57 ///
58 /// ### Params
59 ///
60 /// * `data` - Slice of values to upload
61 /// * `shape` - Dimensions of the tensor
62 /// * `client` - GPU compute client for memory allocation
63 ///
64 /// ### Returns
65 ///
66 /// A new tensor with the data copied to GPU memory, or `BindingTooLarge`
67 /// when the allocation exceeds what this device binds in one go.
68 pub fn from_slice(
69 data: &[F],
70 shape: Vec<usize>,
71 client: &ComputeClient<R>,
72 ) -> Result<Self, CubeclUtilsErrors> {
73 fits_binding(Self::byte_size(&shape), &GpuLimits::from_client(client))?;
74
75 let handle = client.create_from_slice(F::as_bytes(data));
76 let strides = row_major_contiguous_strides(&shape).to_vec();
77 Ok(Self {
78 data: handle,
79 shape,
80 strides,
81 _r: PhantomData,
82 _f: PhantomData,
83 })
84 }
85
86 /// Create an uninitialised tensor.
87 ///
88 /// ### Params
89 ///
90 /// * `shape` - Dimensions of the tensor
91 /// * `client` - GPU compute client for memory allocation
92 ///
93 /// ### Returns
94 ///
95 /// A new tensor with allocated but uninitialised GPU memory, or
96 /// `BindingTooLarge` when the allocation exceeds what this device binds in
97 /// one go.
98 ///
99 /// ### Note
100 ///
101 /// The allocation returns quickly but its pages are not backed until
102 /// something writes them. On a large buffer the first kernel write pays
103 /// that fault and can cost more than the kernel itself, so prefer reusing
104 /// one scratch tensor over allocating per call. See [`Self::reshaped_view`].
105 pub fn empty(shape: Vec<usize>, client: &ComputeClient<R>) -> Result<Self, CubeclUtilsErrors> {
106 let size = Self::byte_size(&shape);
107 fits_binding(size, &GpuLimits::from_client(client))?;
108
109 let handle = client.empty(size as usize);
110 let strides = row_major_contiguous_strides(&shape).to_vec();
111 Ok(Self {
112 data: handle,
113 shape,
114 strides,
115 _r: PhantomData,
116 _f: PhantomData,
117 })
118 }
119
120 /// Convert to a `TensorArg` for kernel launches.
121 ///
122 /// ### Returns
123 ///
124 /// A `TensorArg` suitable for passing to CubeCL kernels. Vectorisation
125 /// width is not set per tensor; it is passed once at launch as the argument
126 /// for the kernel's `N: Size` generic.
127 pub fn into_tensor_arg(&self) -> TensorArg<R> {
128 unsafe {
129 TensorArg::from_raw_parts(
130 self.data.clone(),
131 Strides::new(&self.strides),
132 Shape::from(self.shape.clone()),
133 )
134 }
135 }
136
137 /// Read tensor data back to CPU.
138 ///
139 /// Consumes the tensor and transfers data from GPU to CPU memory.
140 ///
141 /// ### Params
142 ///
143 /// * `client` - GPU compute client for memory transfer
144 ///
145 /// ### Returns
146 ///
147 /// Vector of exactly [`Self::len`] elements.
148 ///
149 /// ### Note
150 ///
151 /// The read is truncated to the shape. That only matters for a tensor
152 /// produced by [`Self::reshaped_view`], where the underlying allocation is
153 /// larger than the view: the runtime hands back the whole binding, and
154 /// returning that would silently include another view's data.
155 pub fn read(self, client: &ComputeClient<R>) -> Result<Vec<F>, CubeclUtilsErrors> {
156 let len = self.len();
157 let bytes = client.read_one(self.data)?;
158 let mut values = F::from_bytes(&bytes).to_vec();
159 values.truncate(len);
160 Ok(values)
161 }
162
163 /// Dimensions of the tensor.
164 ///
165 /// ### Returns
166 ///
167 /// Slice of the per-dimension extents, outermost first.
168 pub fn shape(&self) -> &[usize] {
169 &self.shape
170 }
171
172 /// Number of elements the underlying allocation holds.
173 ///
174 /// ### Returns
175 ///
176 /// Product of the shape dimensions.
177 pub fn len(&self) -> usize {
178 self.shape.iter().product()
179 }
180
181 /// Whether the tensor holds no elements.
182 ///
183 /// ### Returns
184 ///
185 /// True if the element count is zero.
186 pub fn is_empty(&self) -> bool {
187 self.len() == 0
188 }
189
190 /// Reinterpret an existing allocation under a smaller shape.
191 ///
192 /// Shares the underlying buffer rather than allocating, so callers can keep
193 /// one scratch tensor alive across several differently shaped uses. The
194 /// first kernel write to a fresh allocation faults its pages in, which for
195 /// a large buffer costs more than the kernel itself, so reuse is worth the
196 /// sharp edge.
197 ///
198 /// ### Params
199 ///
200 /// * `shape` - New shape; its element count must not exceed the current one
201 ///
202 /// ### Returns
203 ///
204 /// A tensor sharing this one's buffer, with row-major strides for `shape`.
205 ///
206 /// ### Note
207 ///
208 /// The returned tensor aliases `self`. Writing through both concurrently is
209 /// a data race the type system does not prevent here.
210 pub fn reshaped_view(&self, shape: Vec<usize>) -> Self {
211 debug_assert!(
212 shape.iter().product::<usize>() <= self.len(),
213 "reshaped_view would exceed the allocation"
214 );
215 let strides = row_major_contiguous_strides(&shape).to_vec();
216 Self {
217 data: self.data.clone(),
218 shape,
219 strides,
220 _r: PhantomData,
221 _f: PhantomData,
222 }
223 }
224
225 /// Size of the tensor on the GPU, in bytes.
226 ///
227 /// ### Returns
228 ///
229 /// Element count multiplied by the element size.
230 pub fn vram_bytes(&self) -> usize {
231 self.shape.iter().product::<usize>() * std::mem::size_of::<F>()
232 }
233
234 /// Return the handle of the tensor.
235 ///
236 /// Escape hatch for crates that need to hand the raw buffer to their own
237 /// kernels or to a library matmul without going through
238 /// [`GpuTensor::into_tensor_arg`].
239 ///
240 /// ### Returns
241 ///
242 /// A reference to the underlying `Handle`.
243 pub fn handle(&self) -> &Handle {
244 &self.data
245 }
246}
247
248///////////
249// Tests //
250///////////
251
252#[cfg(test)]
253mod tests {
254 use super::*;
255 use cubecl::cpu::{CpuDevice, CpuRuntime};
256
257 #[test]
258 fn test_tensor_from_slice_and_read() {
259 let client = CpuRuntime::client(&CpuDevice);
260
261 let data: Vec<f32> = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
262 let tensor = GpuTensor::<CpuRuntime, f32>::from_slice(&data, vec![2, 3], &client).unwrap();
263
264 assert_eq!(tensor.read(&client).unwrap(), data);
265 }
266
267 #[test]
268 fn test_tensor_empty() {
269 let client = CpuRuntime::client(&CpuDevice);
270
271 let tensor = GpuTensor::<CpuRuntime, f32>::empty(vec![3, 4], &client).unwrap();
272
273 assert_eq!(tensor.shape(), &[3, 4]);
274 assert_eq!(tensor.len(), 12);
275 assert!(!tensor.is_empty());
276 assert_eq!(tensor.vram_bytes(), 48);
277 }
278
279 #[test]
280 fn test_tensor_reshaped_view_shares_the_buffer() {
281 let client = CpuRuntime::client(&CpuDevice);
282
283 let data: Vec<f32> = (0..12).map(|i| i as f32).collect();
284 let tensor = GpuTensor::<CpuRuntime, f32>::from_slice(&data, vec![3, 4], &client).unwrap();
285 let view = tensor.reshaped_view(vec![2, 3]);
286
287 assert_eq!(view.shape(), &[2, 3]);
288 // The runtime hands back the whole 12-element binding; `read` truncates
289 // to the view's own shape.
290 assert_eq!(view.read(&client).unwrap(), &data[..6]);
291 }
292
293 #[test]
294 fn test_tensor_rejects_an_oversized_binding() {
295 let client = CpuRuntime::client(&CpuDevice);
296 let limit = GpuLimits::from_client(&client).max_binding_bytes;
297
298 // One element past what a single binding takes.
299 let too_many = (limit / 4) as usize + 1;
300 assert!(matches!(
301 GpuTensor::<CpuRuntime, f32>::empty(vec![too_many], &client),
302 Err(CubeclUtilsErrors::BindingTooLarge { .. })
303 ));
304 }
305}