Skip to main content

burn_cubecl/tensor/
base.rs

1use crate::CubeDevice;
2use crate::kernel::{NumericUnaryOp, NumericUnaryOpFamily, launch_unary_numeric};
3use burn_backend::cubecl::{dtype_to_elem_type, dtype_to_storage_type};
4use burn_backend::quantization::QuantScheme;
5use burn_backend::{DType, Shape, TensorMetadata};
6use burn_std::{Metadata, strides, tensor::is_contiguous};
7use cubecl::ir::{ElemType, UIntKind};
8use cubecl::server::Handle;
9use cubecl::std::tensor::TensorHandle;
10use cubecl::{client::Client, std::tensor::layout::linear::LinearViewLaunch};
11use cubecl::{frontend::Numeric, std::tensor::layout::linear::LinearViewLayoutLaunch};
12use cubecl::{
13    prelude::{TensorBinding, *},
14    std::tensor::layout::linear::LinearViewLayout,
15};
16
17use super::QParams;
18
19/// The basic tensor primitive struct.
20pub struct CubeTensor {
21    /// Compute client for the runtime this tensor's device names.
22    pub client: Client,
23    /// The buffer where the data are stored.
24    pub handle: Handle,
25    /// The metadata of the tensor.
26    pub meta: Box<Metadata>,
27    /// The device of the tensor.
28    pub device: CubeDevice,
29    /// The datatype of the tensor.
30    pub dtype: DType,
31    /// Runtime quantization parameters, if applicable
32    pub qparams: Option<QParams>,
33}
34
35impl From<CubeTensor> for TensorHandle {
36    fn from(val: CubeTensor) -> Self {
37        // The metadata whole: rebuilt from shape and strides it would lose the storage tiling.
38        TensorHandle::from_metadata(
39            val.handle.clone(),
40            *val.meta.clone(),
41            dtype_to_storage_type(val.dtype),
42        )
43    }
44}
45
46impl cubecl::tune::AutotuneOutput for CubeTensor {
47    #[cfg(feature = "autotune-checks")]
48    fn check_equivalence(&self, other: Self) {
49        use crate::ops::into_data_sync;
50        use burn_backend::Tolerance;
51
52        let expected = into_data_sync(self.clone());
53        let actual = into_data_sync(other);
54        expected.assert_approx_eq::<f32>(&actual, Tolerance::permissive());
55    }
56}
57
58// TODO: Needed to cleanup leaves tensor.
59//
60// Maybe not needed when fusion is activated, since we have a detector there.
61// We could rely on basic GC strategy when not using fusion.
62//
63// impl Drop for CubeTensor {
64//     fn drop(&mut self) {
65//         todo!()
66//     }
67// }
68
69impl core::fmt::Debug for CubeTensor {
70    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
71        f.write_fmt(format_args!(
72            "CubeTensor {{ shape: {:?}, device: {:?}, strides: {:?}, elem: {}, runtime: {}}}",
73            self.meta.shape(),
74            self.device,
75            self.meta.strides(),
76            self.dtype.name(),
77            self.client.name(),
78        ))
79    }
80}
81
82impl Clone for CubeTensor {
83    fn clone(&self) -> Self {
84        Self {
85            client: self.client.clone(),
86            handle: self.handle.clone(),
87            meta: self.meta.clone(),
88            device: self.device.clone(),
89            dtype: self.dtype,
90            qparams: self.qparams.clone(),
91        }
92    }
93}
94
95impl TensorMetadata for CubeTensor {
96    type Device = CubeDevice;
97    fn dtype(&self) -> DType {
98        self.dtype
99    }
100
101    /// The logical shape: a storage-tiled tensor's physical dims split each matrix dim in two,
102    /// and its own metadata folds them back. Identity on a plain tensor.
103    fn shape(&self) -> Shape {
104        self.meta
105            .logical_shape()
106            .expect("a tensor's tiling describes its own rank")
107    }
108
109    fn rank(&self) -> usize {
110        self.meta
111            .logical_rank()
112            .expect("a tensor's tiling describes its own rank")
113    }
114
115    fn device(&self) -> Self::Device {
116        self.device.clone()
117    }
118
119    fn can_mut(&self) -> bool {
120        self.handle.can_mut()
121    }
122}
123
124impl CubeTensor {
125    /// Create a new standard tensor
126    pub fn new(
127        client: Client,
128        handle: Handle,
129        metadata: Metadata,
130        device: CubeDevice,
131        dtype: DType,
132    ) -> Self {
133        CubeTensor {
134            client,
135            handle,
136            meta: Box::new(metadata),
137            device,
138            dtype,
139            qparams: None,
140        }
141    }
142
143    /// Create a new tensor with a contiguous memory layout.
144    pub fn new_contiguous(
145        client: Client,
146        device: CubeDevice,
147        shape: Shape,
148        handle: Handle,
149        dtype: DType,
150    ) -> Self {
151        let ndims = shape.num_dims();
152        let mut strides = strides![0; ndims];
153        let mut current = 1;
154
155        shape.iter().enumerate().rev().for_each(|(index, val)| {
156            strides[index] = current;
157            current *= val;
158        });
159
160        Self {
161            client,
162            handle,
163            meta: Box::new(Metadata::new(shape, strides)),
164            device,
165            dtype,
166            qparams: None,
167        }
168    }
169
170    /// Change the context of the current tensor and return the newly transferred tensor.
171    pub fn to_client(&mut self, client: Client, device: CubeDevice) -> Self {
172        let (handle, qparams) = match self.qparams.clone() {
173            Some(qparams) => {
174                let (handle, qparams) = self.whole_allocation_to_client(&client, qparams);
175                (handle, Some(qparams))
176            }
177            None => {
178                let handle = self.client.to_client(
179                    self.handle.clone(),
180                    &client,
181                    dtype_to_elem_type(self.dtype),
182                );
183                (handle, None)
184            }
185        };
186
187        // The copy keeps the physical layout, so the metadata travels whole, tiling included.
188        Self {
189            client,
190            handle,
191            meta: self.meta.clone(),
192            device,
193            dtype: self.dtype,
194            qparams,
195        }
196    }
197
198    /// Copy the whole allocation behind the handle, not the region its offsets bound.
199    ///
200    /// A quantized tensor's scales live in the same allocation as its values, past the region
201    /// `handle` bounds, and `qparams` names them by offset into that allocation. Moving all of it
202    /// as bytes keeps every start offset valid on the destination. End offsets count back from the
203    /// end of the allocation, which the destination may round up to its own alignment, so each one
204    /// grows by what the allocation did.
205    fn whole_allocation_to_client(
206        &mut self,
207        client: &Client,
208        mut qparams: QParams,
209    ) -> (Handle, QParams) {
210        let mut whole = self.handle.clone();
211        whole.offset_start = None;
212        whole.offset_end = None;
213
214        let mut moved = self
215            .client
216            .to_client(whole, client, ElemType::UInt(UIntKind::U8));
217        let grown = moved.size() - self.handle.size();
218
219        moved.offset_start = self.handle.offset_start;
220        moved.offset_end = Some(self.handle.offset_end.unwrap_or(0) + grown);
221        qparams.scales.offset_end += grown as usize;
222        if let Some(global) = &mut qparams.global {
223            global.offset_end += grown as usize;
224        }
225        (moved, qparams)
226    }
227
228    /// Return the reference to a tensor handle.
229    pub fn binding(self) -> TensorBinding {
230        TensorBinding {
231            handle: self.handle.binding(),
232            strides: self.meta.strides,
233            shape: self.meta.shape,
234            tiling: self.meta.tiling,
235        }
236    }
237
238    /// Returns the element size of this tensor
239    pub fn elem_size(&self) -> usize {
240        self.dtype.size()
241    }
242
243    /// Return the reference to a tensor argument.
244    ///
245    /// # Panics
246    ///
247    /// On a storage-tiled tensor: a kernel argument is read as rows, and only cubek's matmul
248    /// reads storage tiles, through [`binding`](Self::binding). Un-tile it first
249    /// ([`untile`](crate::kernel::untile)).
250    pub fn into_tensor_arg(self) -> TensorArg {
251        self.assert_rows("into_tensor_arg");
252        self.binding().into_tensor_arg()
253    }
254
255    /// A storage-tiled tensor is read as rows by nothing but cubek's matmul; every other kernel
256    /// refuses it here rather than read its tiles as rows.
257    fn assert_rows(&self, op: &str) {
258        assert!(
259            !self.meta.is_tiled(),
260            "CubeTensor::{op}: a storage-tiled tensor is read only by the matmul it was tiled \
261             for; un-tile it (kernel::untile) for anything else"
262        );
263    }
264
265    /// Return the reference to a buffer argument.
266    pub fn into_buffer_arg(self) -> BufferArg {
267        self.into_tensor_arg().into_buffer_arg()
268    }
269
270    /// Returns a reference to the aliased tensor argument.
271    pub fn as_tensor_alias(&self, input_pos: usize) -> TensorArg {
272        self.assert_rows("as_tensor_alias");
273        TensorArg::Alias {
274            input_pos,
275            strides: self.meta.strides().clone(),
276            shape: self.meta.shape().clone(),
277        }
278    }
279
280    /// Return a linear view of this tensor.
281    pub fn into_linear_view(self) -> LinearViewLaunch {
282        let layout = LinearViewLayoutLaunch::new();
283        let buffer = self.into_tensor_arg();
284        LinearViewLaunch::new_tensor::<LinearViewLayout>(buffer, layout)
285    }
286
287    /// Return an aliased linear view of this tensor
288    pub fn as_linear_view_alias(&self, input_pos: usize) -> LinearViewLaunch {
289        let layout = LinearViewLayoutLaunch::new();
290        let buffer = self.as_tensor_alias(input_pos);
291        LinearViewLaunch::new_tensor::<LinearViewLayout>(buffer, layout)
292    }
293
294    /// Return a linear view broadcast to the reference tensor's shape
295    pub fn into_linear_view_like(self, reference: &Self) -> LinearViewLaunch {
296        let layout = LinearViewLayoutLaunch::from_reference_shape(reference.shape());
297        let buffer = self.into_tensor_arg();
298        LinearViewLaunch::new_tensor::<LinearViewLayout>(buffer, layout)
299    }
300
301    /// Returns the address type required to index this tensor
302    pub fn required_address_type(&self) -> AddressType {
303        match self.try_scheme() {
304            Some(scheme) => {
305                let len = self.handle.size() as usize * 8 / scheme.size_bits_value();
306                AddressType::from_len(len)
307            }
308            None => AddressType::from_len(self.handle.size() as usize / self.dtype.size()),
309        }
310    }
311
312    /// Return the `QuantScheme` if present
313    pub fn try_scheme(&self) -> Option<&QuantScheme> {
314        match &self.dtype {
315            DType::QFloat(scheme) => Some(scheme),
316            _ => None,
317        }
318    }
319
320    pub(crate) fn can_mut_broadcast(&self, rhs: &Self) -> bool {
321        if !self.handle.can_mut() || !self.is_nonoverlapping() {
322            return false;
323        }
324        let ndims = self.meta.num_dims();
325
326        for i in 0..ndims {
327            let shape_lhs = self.meta.shape()[i];
328            let shape_rhs = rhs.meta.shape()[i];
329
330            // Output tensor will be different from the mutable tensor.
331            if shape_lhs < shape_rhs {
332                return false;
333            }
334        }
335
336        true
337    }
338
339    /// Copy the current tensor.
340    pub fn copy(&self) -> Self {
341        struct Copy;
342
343        #[cube]
344        impl<T: Numeric, N: Size> NumericUnaryOp<T, N> for Copy {
345            type Options = ();
346
347            fn execute(input: Vector<T, N>, _options: &Self::Options) -> Vector<T, N> {
348                input
349            }
350        }
351
352        impl NumericUnaryOpFamily for Copy {
353            type Options = ();
354            type Unary<T: Numeric, N: Size> = Self;
355        }
356
357        let tensor = self.clone();
358        launch_unary_numeric::<Copy, _>(tensor, |_| ())
359    }
360
361    /// Check if the tensor is safe to mutate.
362    pub fn can_mut(&self) -> bool {
363        self.handle.can_mut()
364    }
365
366    /// Assert that both tensors are on the same device.
367    pub fn assert_is_on_same_device(&self, other: &Self) {
368        if self.device != other.device {
369            panic!(
370                "Both tensors should be on the same device {:?} != {:?}",
371                self.device, other.device
372            );
373        }
374    }
375
376    /// Check if the current tensor is contiguous.
377    ///
378    /// A tensor is contiguous if the elements are stored in memory
379    /// if the strides in non-increasing order and the
380    /// strides at position k is equal to the product of the shapes
381    /// at all positions greater than k. However, all axes with a shape of 1 are ignored.
382    pub fn is_contiguous(&self) -> bool {
383        is_contiguous(self.meta.shape(), self.meta.strides())
384    }
385
386    /// Check if the current tensor has a contiguous backing buffer (no overlap and no empty memory
387    /// regions within the shape).
388    pub fn is_contiguous_buffer(&self) -> bool {
389        self.meta.shape().num_elements() * self.dtype.size() == self.handle.size() as usize
390    }
391
392    /// Checks if the tensor is non-overlapping (can be safely written to).
393    pub fn is_nonoverlapping(&self) -> bool {
394        let shape = self.meta.shape();
395        let strides = self.meta.strides();
396
397        if strides.contains(&0) {
398            return false;
399        }
400        let rank = self.rank();
401        if rank > 1 {
402            let mut dims = shape.iter().zip(strides.iter()).collect::<Vec<_>>();
403            dims.sort_by_key(|(_, stride)| **stride);
404
405            let mut max_offset = 0;
406            for (shape, stride) in dims.into_iter() {
407                if *stride <= max_offset && *shape != 1 {
408                    return false;
409                }
410
411                max_offset += (*shape - 1) * *stride;
412            }
413        }
414        true
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421
422    #[test]
423    fn is_contiguous_non_increasing() {
424        assert!(is_contiguous(&[3, 1], &[1, 1]));
425    }
426
427    #[test]
428    fn is_contiguous_basic() {
429        assert!(is_contiguous(&[32, 32], &[32, 1]));
430    }
431
432    #[test]
433    fn is_contiguous_permuted() {
434        assert!(!is_contiguous(&[32, 32], &[1, 32]));
435    }
436
437    #[test]
438    fn is_contiguous_slice() {
439        assert!(!is_contiguous(&[32, 1, 64], &[32, 64, 1]));
440    }
441
442    #[test]
443    fn is_contiguous_4d_positive() {
444        assert!(is_contiguous(&[8, 256, 32, 32], &[262144, 1024, 32, 1]));
445    }
446
447    #[test]
448    fn is_contiguous_4d_negative() {
449        assert!(!is_contiguous(&[256, 8, 32, 32], &[1024, 262144, 32, 1]));
450    }
451
452    /// Based on a bug encountered in interpolate_1d
453    #[test]
454    fn is_contiguous_4d_unit_shape() {
455        assert!(!is_contiguous(&[1, 1, 1, 9], &[72, 1, 72, 8]));
456    }
457
458    /// A permute leaves a unit axis holding the stride it had before, which says nothing
459    /// about the layout: both of these are contiguous.
460    #[test]
461    fn is_contiguous_unit_axis_keeps_a_stale_stride() {
462        // [2, 1, 3] strides [3, 3, 1] permuted by [0, 2, 1]
463        assert!(is_contiguous(&[2, 3, 1], &[3, 1, 3]));
464        // only the first axis is ever indexed, and it steps by one
465        assert!(is_contiguous(&[32, 1, 1, 1], &[1, 32, 32, 32]));
466    }
467}