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
19pub struct CubeTensor {
21 pub client: Client,
23 pub handle: Handle,
25 pub meta: Box<Metadata>,
27 pub device: CubeDevice,
29 pub dtype: DType,
31 pub qparams: Option<QParams>,
33}
34
35impl From<CubeTensor> for TensorHandle {
36 fn from(val: CubeTensor) -> Self {
37 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
58impl 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 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 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 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 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 Self {
189 client,
190 handle,
191 meta: self.meta.clone(),
192 device,
193 dtype: self.dtype,
194 qparams,
195 }
196 }
197
198 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 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 pub fn elem_size(&self) -> usize {
240 self.dtype.size()
241 }
242
243 pub fn into_tensor_arg(self) -> TensorArg {
251 self.assert_rows("into_tensor_arg");
252 self.binding().into_tensor_arg()
253 }
254
255 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 pub fn into_buffer_arg(self) -> BufferArg {
267 self.into_tensor_arg().into_buffer_arg()
268 }
269
270 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 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 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 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 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 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 if shape_lhs < shape_rhs {
332 return false;
333 }
334 }
335
336 true
337 }
338
339 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 pub fn can_mut(&self) -> bool {
363 self.handle.can_mut()
364 }
365
366 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 pub fn is_contiguous(&self) -> bool {
383 is_contiguous(self.meta.shape(), self.meta.strides())
384 }
385
386 pub fn is_contiguous_buffer(&self) -> bool {
389 self.meta.shape().num_elements() * self.dtype.size() == self.handle.size() as usize
390 }
391
392 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 #[test]
454 fn is_contiguous_4d_unit_shape() {
455 assert!(!is_contiguous(&[1, 1, 1, 9], &[72, 1, 72, 8]));
456 }
457
458 #[test]
461 fn is_contiguous_unit_axis_keeps_a_stale_stride() {
462 assert!(is_contiguous(&[2, 3, 1], &[3, 1, 3]));
464 assert!(is_contiguous(&[32, 1, 1, 1], &[1, 32, 32, 32]));
466 }
467}