Skip to main content

burn_cubecl/tensor/
base.rs

1use crate::CubeRuntime;
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::server::Handle;
8use cubecl::std::tensor::TensorHandle;
9use cubecl::{client::ComputeClient, std::tensor::layout::linear::LinearViewLaunch};
10use cubecl::{frontend::Numeric, std::tensor::layout::linear::LinearViewLayoutLaunch};
11use cubecl::{
12    prelude::{TensorBinding, *},
13    std::tensor::layout::linear::LinearViewLayout,
14};
15use std::marker::PhantomData;
16
17use super::QParams;
18
19/// The basic tensor primitive struct.
20pub struct CubeTensor<R: CubeRuntime> {
21    /// Compute client for the [runtime](CubeRuntime).
22    pub client: ComputeClient<R>,
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: R::Device,
29    /// The datatype of the tensor.
30    pub dtype: DType,
31    /// Runtime quantization parameters, if applicable
32    pub qparams: Option<QParams>,
33}
34
35impl<R: CubeRuntime> From<CubeTensor<R>> for TensorHandle<R> {
36    fn from(val: CubeTensor<R>) -> Self {
37        TensorHandle::new(
38            val.handle.clone(),
39            val.meta.shape().clone(),
40            val.meta.strides().clone(),
41            dtype_to_storage_type(val.dtype),
42        )
43    }
44}
45
46impl<R: CubeRuntime> cubecl::tune::AutotuneOutput for CubeTensor<R> {
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::<R>(self.clone());
53        let actual = into_data_sync::<R>(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<R: CubeRuntime> Drop for CubeTensor<R> {
64//     fn drop(&mut self) {
65//         todo!()
66//     }
67// }
68
69impl<R> core::fmt::Debug for CubeTensor<R>
70where
71    R: CubeRuntime,
72{
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        f.write_fmt(format_args!(
75            "CubeTensor {{ shape: {:?}, device: {:?}, strides: {:?}, elem: {}, runtime: {}}}",
76            self.meta.shape(),
77            self.device,
78            self.meta.strides(),
79            self.dtype.name(),
80            R::name(&self.client),
81        ))
82    }
83}
84
85impl<R> Clone for CubeTensor<R>
86where
87    R: CubeRuntime,
88{
89    fn clone(&self) -> Self {
90        Self {
91            client: self.client.clone(),
92            handle: self.handle.clone(),
93            meta: self.meta.clone(),
94            device: self.device.clone(),
95            dtype: self.dtype,
96            qparams: self.qparams.clone(),
97        }
98    }
99}
100
101impl<R: CubeRuntime> TensorMetadata for CubeTensor<R> {
102    type Device = R::CubeDevice;
103    fn dtype(&self) -> DType {
104        self.dtype
105    }
106
107    fn shape(&self) -> Shape {
108        self.meta.shape().clone()
109    }
110
111    fn rank(&self) -> usize {
112        self.meta.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<R> CubeTensor<R>
125where
126    R: CubeRuntime,
127{
128    /// Create a new standard tensor
129    pub fn new(
130        client: ComputeClient<R>,
131        handle: Handle,
132        metadata: Metadata,
133        device: R::Device,
134        dtype: DType,
135    ) -> Self {
136        CubeTensor {
137            client,
138            handle,
139            meta: Box::new(metadata),
140            device,
141            dtype,
142            qparams: None,
143        }
144    }
145
146    /// Create a new tensor with a contiguous memory layout.
147    pub fn new_contiguous(
148        client: ComputeClient<R>,
149        device: R::Device,
150        shape: Shape,
151        handle: Handle,
152        dtype: DType,
153    ) -> Self {
154        let ndims = shape.num_dims();
155        let mut strides = strides![0; ndims];
156        let mut current = 1;
157
158        shape.iter().enumerate().rev().for_each(|(index, val)| {
159            strides[index] = current;
160            current *= val;
161        });
162
163        Self {
164            client,
165            handle,
166            meta: Box::new(Metadata::new(shape, strides)),
167            device,
168            dtype,
169            qparams: None,
170        }
171    }
172
173    /// Change the context of the current tensor and return the newly transferred tensor.
174    pub fn to_client(&mut self, client: ComputeClient<R>, device: R::Device) -> Self {
175        let desc = self.handle.clone().copy_descriptor(
176            self.meta.shape().clone(),
177            self.meta.strides().clone(),
178            self.elem_size(),
179        );
180        let handle = self
181            .client
182            .to_client_tensor(desc, &client, dtype_to_elem_type(self.dtype));
183
184        Self {
185            client,
186            handle,
187            meta: Box::new(Metadata::new(self.shape(), self.meta.strides().clone())),
188            device,
189            dtype: self.dtype,
190            qparams: self.qparams.clone(),
191        }
192    }
193
194    /// Return the reference to a tensor handle.
195    pub fn binding(self) -> TensorBinding<R> {
196        TensorBinding {
197            handle: self.handle.binding(),
198            strides: self.meta.strides,
199            shape: self.meta.shape,
200            runtime: PhantomData,
201        }
202    }
203
204    /// Returns the element size of this tensor
205    pub fn elem_size(&self) -> usize {
206        self.dtype.size()
207    }
208
209    /// Return the reference to a tensor argument.
210    pub fn into_tensor_arg(self) -> TensorArg<R> {
211        self.binding().into_tensor_arg()
212    }
213
214    /// Return the reference to a buffer argument.
215    pub fn into_buffer_arg(self) -> BufferArg<R> {
216        self.into_tensor_arg().into_buffer_arg()
217    }
218
219    /// Returns a reference to the aliased tensor argument.
220    pub fn as_tensor_alias(&self, input_pos: usize) -> TensorArg<R> {
221        TensorArg::Alias {
222            input_pos,
223            strides: self.meta.strides().clone(),
224            shape: self.meta.shape().clone(),
225        }
226    }
227
228    /// Return a linear view of this tensor.
229    pub fn into_linear_view(self) -> LinearViewLaunch<R> {
230        let layout = LinearViewLayoutLaunch::new();
231        let buffer = self.into_tensor_arg();
232        LinearViewLaunch::new_tensor::<LinearViewLayout>(buffer, layout)
233    }
234
235    /// Return an aliased linear view of this tensor
236    pub fn as_linear_view_alias(&self, input_pos: usize) -> LinearViewLaunch<R> {
237        let layout = LinearViewLayoutLaunch::new();
238        let buffer = self.as_tensor_alias(input_pos);
239        LinearViewLaunch::new_tensor::<LinearViewLayout>(buffer, layout)
240    }
241
242    /// Return a linear view broadcast to the reference tensor's shape
243    pub fn into_linear_view_like(self, reference: &Self) -> LinearViewLaunch<R> {
244        let layout = LinearViewLayoutLaunch::from_reference_shape(reference.shape());
245        let buffer = self.into_tensor_arg();
246        LinearViewLaunch::new_tensor::<LinearViewLayout>(buffer, layout)
247    }
248
249    /// Returns the address type required to index this tensor
250    pub fn required_address_type(&self) -> AddressType {
251        match self.try_scheme() {
252            Some(scheme) => {
253                let len = self.handle.size() as usize * 8 / scheme.size_bits_value();
254                AddressType::from_len(len)
255            }
256            None => AddressType::from_len(self.handle.size() as usize / self.dtype.size()),
257        }
258    }
259
260    /// Return the `QuantScheme` if present
261    pub fn try_scheme(&self) -> Option<&QuantScheme> {
262        match &self.dtype {
263            DType::QFloat(scheme) => Some(scheme),
264            _ => None,
265        }
266    }
267
268    pub(crate) fn can_mut_broadcast(&self, rhs: &Self) -> bool {
269        if !self.handle.can_mut() || !self.is_nonoverlapping() {
270            return false;
271        }
272        let ndims = self.meta.num_dims();
273
274        for i in 0..ndims {
275            let shape_lhs = self.meta.shape()[i];
276            let shape_rhs = rhs.meta.shape()[i];
277
278            // Output tensor will be different from the mutable tensor.
279            if shape_lhs < shape_rhs {
280                return false;
281            }
282        }
283
284        true
285    }
286
287    /// Copy the current tensor.
288    pub fn copy(&self) -> Self {
289        struct Copy;
290
291        #[cube]
292        impl<T: Numeric, N: Size> NumericUnaryOp<T, N> for Copy {
293            type Options = ();
294
295            fn execute(input: Vector<T, N>, _options: &Self::Options) -> Vector<T, N> {
296                input
297            }
298        }
299
300        impl NumericUnaryOpFamily for Copy {
301            type Options = ();
302            type Unary<T: Numeric, N: Size> = Self;
303        }
304
305        let tensor = self.clone();
306        launch_unary_numeric::<R, Copy, _>(tensor, |_| ())
307    }
308
309    /// Check if the tensor is safe to mutate.
310    pub fn can_mut(&self) -> bool {
311        self.handle.can_mut()
312    }
313
314    /// Assert that both tensors are on the same device.
315    pub fn assert_is_on_same_device(&self, other: &Self) {
316        if self.device != other.device {
317            panic!(
318                "Both tensors should be on the same device {:?} != {:?}",
319                self.device, other.device
320            );
321        }
322    }
323
324    /// Check if the current tensor is contiguous.
325    ///
326    /// A tensor is contiguous if the elements are stored in memory
327    /// if the strides in non-increasing order and the
328    /// strides at position k is equal to the product of the shapes
329    /// at all positions greater than k. However, all axes with a shape of 1 are ignored.
330    pub fn is_contiguous(&self) -> bool {
331        is_contiguous(self.meta.shape(), self.meta.strides())
332    }
333
334    /// Check if the current tensor has a contiguous backing buffer (no overlap and no empty memory
335    /// regions within the shape).
336    pub fn is_contiguous_buffer(&self) -> bool {
337        self.meta.shape().num_elements() * self.dtype.size() == self.handle.size() as usize
338    }
339
340    /// Checks if the tensor is non-overlapping (can be safely written to).
341    pub fn is_nonoverlapping(&self) -> bool {
342        let shape = self.meta.shape();
343        let strides = self.meta.strides();
344
345        if strides.contains(&0) {
346            return false;
347        }
348        let rank = self.rank();
349        if rank > 1 {
350            let mut dims = shape.iter().zip(strides.iter()).collect::<Vec<_>>();
351            dims.sort_by_key(|(_, stride)| **stride);
352
353            let mut max_offset = 0;
354            for (shape, stride) in dims.into_iter() {
355                if *stride <= max_offset && *shape != 1 {
356                    return false;
357                }
358
359                max_offset += (*shape - 1) * *stride;
360            }
361        }
362        true
363    }
364}
365
366#[cfg(test)]
367mod tests {
368    use super::*;
369
370    #[test]
371    fn is_contiguous_non_increasing() {
372        assert!(is_contiguous(&[3, 1], &[1, 1]));
373    }
374
375    #[test]
376    fn is_contiguous_basic() {
377        assert!(is_contiguous(&[32, 32], &[32, 1]));
378    }
379
380    #[test]
381    fn is_contiguous_permuted() {
382        assert!(!is_contiguous(&[32, 32], &[1, 32]));
383    }
384
385    #[test]
386    fn is_contiguous_slice() {
387        assert!(!is_contiguous(&[32, 1, 64], &[32, 64, 1]));
388    }
389
390    #[test]
391    fn is_contiguous_4d_positive() {
392        assert!(is_contiguous(&[8, 256, 32, 32], &[262144, 1024, 32, 1]));
393    }
394
395    #[test]
396    fn is_contiguous_4d_negative() {
397        assert!(!is_contiguous(&[256, 8, 32, 32], &[1024, 262144, 32, 1]));
398    }
399
400    /// Based on a bug encountered in interpolate_1d
401    #[test]
402    fn is_contiguous_4d_unit_shape() {
403        assert!(!is_contiguous(&[1, 1, 1, 9], &[72, 1, 72, 8]));
404    }
405}