Skip to main content

burn_cubecl/ops/
qtensor.rs

1use burn_backend::{
2    Bytes, DType, ExecutionError, Shape, SplitPolicy, TensorData, TensorMetadata, TensorPrimitive,
3    get_or_init_device_settings,
4    ops::QTensorOps,
5    quantization::{
6        QParamTensor, QuantMode, QuantPropagation, QuantScheme, QuantValue,
7        QuantizationParametersPrimitive, ScaleDtype, global_scale_dtype, params_shape,
8    },
9    tensor::{Device, FloatTensor, QuantizedTensor},
10};
11use burn_std::{FloatDType, Metadata, quantization::global_scale_size};
12use cubecl::server::{MemoryLayout, MemoryLayoutDescriptor, MemoryLayoutStrategy};
13use cubecl::{e2m1x2, quant::scheme::QuantStore};
14
15use crate::{
16    CubeBackend, CubeDevice,
17    kernel::{self, matmul::MatmulStrategy},
18    tensor::{CubeTensor, QParams},
19};
20
21use super::{into_data, permute, swap_dims};
22
23/// Length of the block-scales region within a combined scales+global byte buffer.
24fn scales_region_len(total: usize, scheme: &QuantScheme) -> usize {
25    total
26        .checked_sub(global_scale_size(scheme))
27        .expect("quantized tensor data is shorter than the scheme's global scale")
28}
29
30/// Create a quantized tensor with packed values (u32).
31fn new_qtensor_optimized(
32    data: Bytes,
33    shape: impl Into<Shape>,
34    scheme: QuantScheme,
35    device: &CubeDevice,
36) -> CubeTensor {
37    new_qtensor(data, shape, scheme, device, MemoryLayoutStrategy::Optimized)
38}
39
40/// Create a quantized tensor with packed values (u32).
41fn new_qtensor(
42    data: Bytes,
43    shape: impl Into<Shape>,
44    scheme: QuantScheme,
45    device: &CubeDevice,
46    kind: MemoryLayoutStrategy,
47) -> CubeTensor {
48    new_quantized(shape, scheme, device, Some(data), kind)
49}
50
51/// Create an empty quantized tensor.
52pub fn empty_qtensor_optimized(
53    shape: impl Into<Shape>,
54    scheme: QuantScheme,
55    device: &CubeDevice,
56) -> CubeTensor {
57    empty_qtensor(shape, scheme, device, MemoryLayoutStrategy::Optimized)
58}
59
60/// Create an empty quantized tensor.
61pub fn empty_qtensor(
62    shape: impl Into<Shape>,
63    scheme: QuantScheme,
64    device: &CubeDevice,
65    kind: MemoryLayoutStrategy,
66) -> CubeTensor {
67    new_quantized(shape, scheme, device, None, kind)
68}
69
70/// The axis a packed `scheme` packs along on a tensor of `rank`, when it is not the innermost.
71///
72/// Such a tensor is stored with that axis swapped innermost, which is where packing puts its
73/// words, and presented with the two swapped back: its bytes are the stored tensor's, row-major,
74/// so a packed word never straddles two of the axes it does not pack.
75fn outer_packed_axis(scheme: &QuantScheme, rank: usize) -> Option<usize> {
76    match scheme.store {
77        QuantStore::PackedU32(packed) | QuantStore::PackedNative(packed) if packed != 0 => {
78            Some(rank - packed - 1)
79        }
80        _ => None,
81    }
82}
83
84/// `scheme` as it reads on the tensor with `axis` swapped innermost: packed along the
85/// innermost axis, its blocks swapped with it.
86fn packed_innermost(mut scheme: QuantScheme, rank: usize, axis: usize) -> QuantScheme {
87    scheme.store = match scheme.store {
88        QuantStore::PackedU32(_) => QuantStore::PackedU32(0),
89        QuantStore::PackedNative(_) => QuantStore::PackedNative(0),
90        QuantStore::Native => QuantStore::Native,
91    };
92    if scheme.block_size().is_some() {
93        scheme.swap_block_dims(rank, axis, rank - 1);
94    }
95    scheme
96}
97
98fn new_quantized(
99    shape: impl Into<Shape>,
100    scheme: QuantScheme,
101    device: &CubeDevice,
102    data: Option<Bytes>,
103    alloc_kind: MemoryLayoutStrategy,
104) -> CubeTensor {
105    let shape: Shape = shape.into();
106    if let Some(axis) = outer_packed_axis(&scheme, shape.rank()) {
107        let (rank, innermost) = (shape.rank(), shape.rank() - 1);
108        let stored_shape = shape
109            .swapped(axis, innermost)
110            .expect("the packed axis is one of the tensor's");
111        let stored = new_quantized(
112            stored_shape,
113            packed_innermost(scheme, rank, axis),
114            device,
115            data,
116            alloc_kind,
117        );
118        return swap_dims(stored, axis, innermost);
119    }
120
121    let client = device.client();
122    let mut shape_value: Shape = shape.clone();
123
124    let rank = shape.rank();
125    let shape_last = shape[rank - 1];
126    let num_quants = scheme.num_quants();
127
128    let data_size = match scheme.store {
129        QuantStore::PackedU32(_) => {
130            if !shape_last.is_multiple_of(num_quants) {
131                panic!("Can't store in u32")
132            }
133            shape_value[rank - 1] = shape_last.div_ceil(num_quants);
134            size_of::<u32>()
135        }
136        QuantStore::Native => match scheme.value {
137            QuantValue::Q8F | QuantValue::Q8S | QuantValue::E4M3 | QuantValue::E5M2 => {
138                size_of::<i8>()
139            }
140            QuantValue::Q4F
141            | QuantValue::Q4S
142            | QuantValue::Q2F
143            | QuantValue::Q2S
144            | QuantValue::E2M1 => {
145                panic!("Can't store native sub-byte values")
146            }
147        },
148        QuantStore::PackedNative(_) => match scheme.value {
149            QuantValue::E2M1 => size_of::<e2m1x2>(),
150            other => panic!("{other:?} doesn't support native packing"),
151        },
152    };
153
154    let scales_dtype = match scheme.scale_dtype() {
155        ScaleDtype::F32 => DType::F32,
156        ScaleDtype::F16 => DType::F16,
157        ScaleDtype::BF16 => DType::BF16,
158        // Represented by U8 and reinterpreted in the kernel
159        ScaleDtype::UE8M0 | ScaleDtype::UE4M3 => DType::U8,
160    };
161
162    let scales_shape = params_shape(&shape, &scheme);
163    let data_desc = MemoryLayoutDescriptor::new(alloc_kind, shape_value.clone(), data_size);
164    let scales_desc =
165        MemoryLayoutDescriptor::new(alloc_kind, scales_shape.clone(), scales_dtype.size());
166
167    let global_shape = Shape::new([1]);
168    let global_dtype = global_scale_dtype(&scheme).map(|dtype| {
169        // The region is f32-sized and the kernels bind it as f32.
170        assert_eq!(
171            dtype,
172            ScaleDtype::F32,
173            "a two-level scheme binds its per-tensor scale as f32, got {scheme:?}"
174        );
175        DType::F32
176    });
177    let global_desc = global_dtype
178        .map(|dtype| MemoryLayoutDescriptor::new(alloc_kind, global_shape.clone(), dtype.size()));
179
180    let mut tensors = match data {
181        Some(data) => {
182            let num_bytes = shape_value.num_elements() * data_size;
183            let split = data.split(num_bytes, SplitPolicy::Shared);
184
185            match (split, global_desc.clone()) {
186                (Ok((bytes_data, bytes_params)), None) => client
187                    .create_tensors(vec![(data_desc, bytes_data), (scales_desc, bytes_params)]),
188                (Ok((bytes_data, bytes_params)), Some(global_desc)) => {
189                    let scales_bytes = scales_region_len(bytes_params.len(), &scheme);
190                    match bytes_params.split(scales_bytes, SplitPolicy::Shared) {
191                        Ok((block, global)) => client.create_tensors(vec![
192                            (data_desc, bytes_data),
193                            (scales_desc, block),
194                            (global_desc, global),
195                        ]),
196                        Err((params, _)) => client.create_tensors_from_slices(vec![
197                            (data_desc, &bytes_data[..]),
198                            (scales_desc, &params[..scales_bytes]),
199                            (global_desc, &params[scales_bytes..]),
200                        ]),
201                    }
202                }
203                (Err((data, _)), global_desc) => {
204                    let params = &data[num_bytes..];
205                    let scales_bytes = scales_region_len(params.len(), &scheme);
206                    let mut entries = vec![
207                        (data_desc, &data[..num_bytes]),
208                        (scales_desc, &params[..scales_bytes]),
209                    ];
210                    if let Some(global_desc) = global_desc {
211                        entries.push((global_desc, &params[scales_bytes..]));
212                    }
213                    client.create_tensors_from_slices(entries)
214                }
215            }
216        }
217        None => {
218            let mut descs = vec![data_desc, scales_desc];
219            descs.extend(global_desc);
220            client.empty_tensors(descs)
221        }
222    };
223
224    let global = global_dtype.map(|dtype| {
225        let MemoryLayout {
226            memory: handle,
227            strides,
228        } = tensors.remove(2);
229        QParamTensor {
230            offset_start: handle.offset_start.unwrap_or(0) as usize,
231            offset_end: handle.offset_end.unwrap_or(0) as usize,
232            metadata: Metadata::new(global_shape, strides),
233            dtype,
234        }
235    });
236    let MemoryLayout {
237        memory: scales_handle,
238        strides: scales_strides,
239    } = tensors.remove(1);
240    let MemoryLayout { memory, strides } = tensors.remove(0);
241
242    let scales = QParamTensor {
243        offset_start: scales_handle.offset_start.unwrap_or(0) as usize,
244        offset_end: scales_handle.offset_end.unwrap_or(0) as usize,
245        metadata: Metadata::new(scales_shape, scales_strides),
246        dtype: scales_dtype,
247    };
248    let qparams = QParams { scales, global };
249
250    CubeTensor::new_quantized(
251        client,
252        memory,
253        shape,
254        device.clone(),
255        strides,
256        DType::QFloat(scheme),
257        qparams,
258    )
259}
260
261impl QTensorOps<Self> for CubeBackend {
262    fn q_from_data(data: TensorData, device: &Device<Self>) -> QuantizedTensor<Self> {
263        match data.dtype() {
264            DType::QFloat(scheme) => match scheme {
265                QuantScheme {
266                    mode: QuantMode::Symmetric,
267                    value:
268                        QuantValue::Q8F
269                        | QuantValue::Q8S
270                        | QuantValue::Q4F
271                        | QuantValue::Q4S
272                        | QuantValue::Q2F
273                        | QuantValue::Q2S
274                        | QuantValue::E4M3
275                        | QuantValue::E5M2
276                        | QuantValue::E2M1,
277                    ..
278                } => {
279                    // TensorData quantized representation is the same, with multiple quantized values
280                    // packed into u32 and quantization parameters appended to the bytes
281                    let (bytes, shape, _) = data.into_parts();
282                    new_qtensor_optimized(bytes, shape, scheme, device)
283                }
284                QuantScheme {
285                    mode: QuantMode::Lookup,
286                    ..
287                } => unimplemented!("lookup quantization does not travel as a QFloat tensor"),
288            },
289            _ => panic!(
290                "Invalid dtype (expected DType::QFloat, got {:?})",
291                data.dtype()
292            ),
293        }
294    }
295
296    // TODO: quantize_dynamic (we can compute min-max on the fly and scale, especially when not per-tensor)
297
298    fn quantize(
299        tensor: FloatTensor<Self>,
300        scheme: &QuantScheme,
301        qparams: QuantizationParametersPrimitive<Self>,
302    ) -> QuantizedTensor<Self> {
303        // The kernel reads this at the scheme's scale dtype, not the tensor's actual dtype.
304        if let Some(global) = &qparams.global {
305            assert_eq!(
306                global.dtype,
307                DType::F32,
308                "a two-level scheme's per-tensor scale must be an f32 tensor, got {:?}",
309                global.dtype
310            );
311        }
312        kernel::quantization::quantize(tensor, scheme, qparams.scales, qparams.global)
313    }
314
315    fn dequantize(tensor: QuantizedTensor<Self>, dtype: FloatDType) -> FloatTensor<Self> {
316        kernel::quantization::dequantize(tensor, dtype.into())
317    }
318
319    fn q_to_device(tensor: QuantizedTensor<Self>, device: &Device<Self>) -> QuantizedTensor<Self> {
320        super::to_device(tensor, device)
321    }
322
323    fn q_reshape(tensor: QuantizedTensor<Self>, shape: Shape) -> QuantizedTensor<Self> {
324        super::q_reshape(tensor, shape)
325    }
326
327    async fn q_into_data(tensor: QuantizedTensor<Self>) -> Result<TensorData, ExecutionError> {
328        if tensor.qparams.is_none() {
329            return into_data(tensor).await;
330        }
331        // Storage tiles are a layout for one machine's kernels, laid out at load; what is saved
332        // is the rows every machine reads.
333        assert!(
334            !tensor.meta.is_tiled(),
335            "q_into_data: a storage-tiled quantized tensor is not saved; save the weight it was \
336             tiled from"
337        );
338
339        let (shape, dtype) = (tensor.shape(), tensor.dtype);
340        // Written as stored, packed axis innermost — the bytes `q_from_data` reads back.
341        let tensor = match outer_packed_axis(&tensor.scheme(), shape.rank()) {
342            Some(axis) => swap_dims(tensor, axis, shape.rank() - 1),
343            None => tensor,
344        };
345        let global = tensor.global();
346        let (values, params) = tensor.quantized_handles().unwrap();
347
348        let mut bytes = into_data(values).await?.into_bytes();
349        let data_params = into_data(params).await?;
350
351        bytes.extend_from_byte_slice(data_params.as_bytes());
352
353        if let Some(global) = global {
354            let data_global = into_data(global).await?;
355            bytes.extend_from_byte_slice(data_global.as_bytes());
356        }
357
358        Ok(TensorData::from_bytes(bytes, shape, dtype))
359    }
360
361    fn q_swap_dims(
362        tensor: QuantizedTensor<Self>,
363        dim1: usize,
364        dim2: usize,
365    ) -> QuantizedTensor<Self> {
366        swap_dims(tensor, dim1, dim2)
367    }
368
369    fn q_permute(tensor: QuantizedTensor<Self>, axes: &[usize]) -> QuantizedTensor<Self> {
370        permute(tensor, axes)
371    }
372
373    fn q_flip(_tensor: QuantizedTensor<Self>, _axes: &[usize]) -> QuantizedTensor<Self> {
374        unimplemented!()
375    }
376
377    fn q_matmul(lhs: TensorPrimitive<Self>, rhs: TensorPrimitive<Self>) -> TensorPrimitive<Self> {
378        let (settings, scheme) = match (&lhs, &rhs) {
379            (TensorPrimitive::QFloat(lhs), _) => (
380                get_or_init_device_settings::<Self>(&lhs.device),
381                lhs.scheme(),
382            ),
383            (_, TensorPrimitive::QFloat(rhs)) => (
384                get_or_init_device_settings::<Self>(&rhs.device),
385                rhs.scheme(),
386            ),
387            _ => unreachable!(),
388        };
389
390        // Inherit precision for mixed inputs, default to `FloatElem` for fully quantized.
391        let out_dtype = match (&lhs, &rhs) {
392            (TensorPrimitive::Float(lhs), _) => lhs.dtype,
393            (_, TensorPrimitive::Float(rhs)) => rhs.dtype,
394            _ => settings.float_dtype.into(),
395        };
396
397        let (_lhs_dtype, lhs) = match lhs {
398            TensorPrimitive::Float(lhs) => (lhs.dtype, lhs),
399            TensorPrimitive::QFloat(lhs) => (out_dtype, lhs),
400        };
401        let (_rhs_dtype, rhs) = match rhs {
402            TensorPrimitive::Float(rhs) => (rhs.dtype, rhs),
403            TensorPrimitive::QFloat(rhs) => (out_dtype, rhs),
404        };
405
406        let out =
407            kernel::matmul::matmul(lhs, rhs, None, MatmulStrategy::default(), out_dtype).unwrap();
408
409        match settings.quantization.propagation {
410            QuantPropagation::Propagate => {
411                TensorPrimitive::QFloat(Self::quantize_dynamic(out, &scheme))
412            }
413            QuantPropagation::Inhibit => TensorPrimitive::Float(out),
414        }
415    }
416}