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
23fn 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
30fn 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
40fn 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
51pub 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
60pub 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
70fn 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
84fn 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 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 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, ¶ms[..scales_bytes]),
199 (global_desc, ¶ms[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, ¶ms[..scales_bytes]),
209 ];
210 if let Some(global_desc) = global_desc {
211 entries.push((global_desc, ¶ms[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 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 fn quantize(
299 tensor: FloatTensor<Self>,
300 scheme: &QuantScheme,
301 qparams: QuantizationParametersPrimitive<Self>,
302 ) -> QuantizedTensor<Self> {
303 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 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 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 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}