1pub use cubecl_common::quant::scheme::{
3 BlockScale, BlockSize, QuantMode, QuantScheme, QuantStore, QuantValue, ScaleDtype,
4};
5
6pub const QPARAM_ALIGN: usize = core::mem::align_of::<f32>();
12
13use alloc::vec::Vec;
14use core::any::TypeId;
15use cubecl_common::{e4m3, ue8m0};
16use serde::{Deserialize, Serialize};
17
18use super::packing::{PackedOrder, ValueLayout};
19use crate::{DType, Metadata, Shape, bytes::Bytes};
20
21#[derive(new, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
27pub struct QuantConfig {
28 pub scheme: QuantScheme,
30 pub propagation: QuantPropagation,
32 }
36
37#[derive(
38 Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default,
39)]
40pub enum QuantAcc {
42 #[default]
44 F32,
45 F16,
47 BF16,
49}
50
51pub enum Calibration {
53 MinMax,
55 AbsMean,
60}
61
62#[derive(
65 Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default,
66)]
67pub enum QuantPropagation {
68 Propagate,
70 #[default]
72 Inhibit,
73}
74
75#[derive(Clone, Debug)]
77pub struct QParams<S> {
78 pub scales: S,
80 pub global: Option<S>,
82}
83
84#[derive(Clone, Debug, PartialEq)]
86pub struct DecodedScales {
87 pub block: Vec<f32>,
89 pub global: Option<f32>,
91}
92
93#[derive(Debug, Clone, PartialEq, Eq)]
95pub struct QParamTensor {
96 pub offset_start: usize,
98 pub offset_end: usize,
100 pub metadata: Metadata,
102 pub dtype: DType,
104}
105
106pub fn quantizable(scheme: &QuantScheme) -> bool {
112 match (scheme.block_scale(), global_scale_dtype(scheme)) {
113 (Some(block), Some(global)) => {
114 block.dtype.max_representable() <= crate::f16::MAX.to_f32() && global == ScaleDtype::F32
118 }
119 _ => true,
120 }
121}
122
123pub fn global_scale_dtype(scheme: &QuantScheme) -> Option<ScaleDtype> {
128 scheme.block_scale().and(scheme.tensor_scale())
129}
130
131pub fn params_shape(data_shape: &Shape, scheme: &QuantScheme) -> Shape {
135 match scheme.block_size() {
136 None => Shape::new([1]),
137 Some(block_size) => Shape::from(block_size.num_blocks(data_shape.as_slice())),
138 }
139}
140
141#[derive(Debug, Clone)]
145pub struct BlockLayout {
146 shape: Shape,
147 block: Vec<u8>,
148 blocks: Shape,
149}
150
151impl BlockLayout {
152 pub fn new(shape: &Shape, block: &BlockSize) -> Self {
154 Self {
155 shape: shape.clone(),
156 block: block.to_dim_vec(shape.num_dims()),
157 blocks: Shape::from(block.num_blocks(shape.as_slice())),
158 }
159 }
160
161 pub fn num_blocks(&self) -> usize {
163 self.blocks.num_elements()
164 }
165
166 pub fn divides(&self) -> bool {
168 self.shape
169 .iter()
170 .zip(&self.block)
171 .all(|(&dim, &extent)| dim.is_multiple_of(extent as usize))
172 }
173
174 pub fn block_of(&self, mut index: usize) -> usize {
176 let mut block = 0;
177 let mut stride = 1;
178 for dim in (0..self.shape.num_dims()).rev() {
179 let coordinate = index % self.shape[dim];
180 index /= self.shape[dim];
181 block += coordinate / self.block[dim] as usize * stride;
182 stride *= self.blocks[dim];
183 }
184 block
185 }
186}
187
188pub struct QuantizedBytes {
198 pub bytes: Bytes,
200 pub scheme: QuantScheme,
202 pub shape: Shape,
205}
206
207impl QuantizedBytes {
208 pub fn new<E: bytemuck::CheckedBitPattern + bytemuck::NoUninit>(
213 value: Vec<E>,
214 shape: impl Into<Shape>,
215 scheme: QuantScheme,
216 scales: &[f32],
217 global: Option<f32>,
218 ) -> Self {
219 let shape = shape.into();
220 assert_eq!(
221 value.len(),
222 shape.num_elements(),
223 "{} quantized values do not fill a tensor of shape {shape:?}",
224 value.len()
225 );
226 if TypeId::of::<E>() != TypeId::of::<i8>() {
227 panic!("Invalid quantized type");
228 }
229
230 let i8s: Vec<i8> = bytemuck::allocation::cast_vec(value);
231 let layout = ValueLayout::new(&scheme);
232 let mut bytes = match layout {
233 ValueLayout::Bytes => Bytes::from_elems(i8s),
234 ValueLayout::Packed { packed_dim, word } => {
235 Bytes::from_elems(PackedOrder::new(shape.as_slice(), packed_dim).pack(&i8s, &word))
236 }
237 };
238
239 let scales = match (
240 scheme.block_size(),
241 layout.block_scale_order(&shape, &scheme),
242 ) {
243 (None, _) => scales[..1].to_vec(),
244 (Some(_), Some(order)) => order.to_stored(scales),
245 (Some(_), None) => scales.to_vec(),
246 };
247 let scale_bytes = encode_scales(&scales, scheme.scale_dtype());
248 bytes.extend_from_byte_slice_aligned(scale_bytes.as_slice(), QPARAM_ALIGN);
249
250 match (global_scale_dtype(&scheme), global) {
252 (Some(dtype), Some(global)) => {
253 assert_eq!(
256 dtype,
257 ScaleDtype::F32,
258 "a two-level scheme stores its per-tensor scale as f32, got {scheme:?}"
259 );
260 let global_bytes = encode_scales(&[global], dtype);
261 bytes.extend_from_byte_slice_aligned(global_bytes.as_slice(), QPARAM_ALIGN);
262 }
263 (Some(_), None) => panic!("{scheme:?} requires a per-tensor scale"),
264 (None, Some(_)) => panic!("{scheme:?} does not take a per-tensor scale"),
265 (None, None) => {}
266 }
267
268 Self {
269 bytes,
270 scheme,
271 shape,
272 }
273 }
274
275 pub fn num_elements(&self) -> usize {
277 self.shape.num_elements()
278 }
279
280 pub fn into_vec_i8(self) -> (Vec<i8>, DecodedScales) {
283 let scheme = self.scheme;
284 let scale_order = ValueLayout::new(&scheme).block_scale_order(&self.shape, &scheme);
285 let (values, (qparams, num_params)) = self.split_values_off();
286
287 let global_bytes = global_scale_size(&scheme);
289 let block_end = qparams
290 .len()
291 .checked_sub(global_bytes)
292 .expect("quantized parameter buffer is shorter than the scheme's global scale");
293 let block_start = block_end
294 .checked_sub(scale_size(scheme.scale_dtype()) * num_params)
295 .expect("quantized parameter buffer is shorter than the scheme's block scales");
296
297 let block = decode_scales(&qparams[block_start..block_end], scheme.scale_dtype());
298 let block = match scale_order {
299 Some(order) => order.to_row_major(&block),
300 None => block,
301 };
302 let global =
303 global_scale_dtype(&scheme).map(|dtype| decode_scales(&qparams[block_end..], dtype)[0]);
304
305 (values, DecodedScales { block, global })
306 }
307
308 fn split_values_off(self) -> (Vec<i8>, (Vec<u8>, usize)) {
313 let num_params = params_shape(&self.shape, &self.scheme).num_elements();
314 let scale_bytes =
315 scale_size(self.scheme.scale_dtype()) * num_params + global_scale_size(&self.scheme);
316 let (shape, scheme) = (self.shape, self.scheme);
317
318 let mut stored = read_bytes_to_i8(self.bytes);
319 let split_at = stored
320 .len()
321 .checked_sub(scale_bytes)
322 .expect("quantized tensor data is shorter than its scheme's parameters");
323 let qparams = bytemuck::cast_vec(stored.split_off(split_at));
324
325 let layout = ValueLayout::new(&scheme);
326 let expected = value_bytes(&scheme, &shape);
327 assert_eq!(
328 stored.len(),
329 expected,
330 "the values of a {shape:?} tensor under {scheme:?} take {expected} bytes, not {}",
331 stored.len()
332 );
333 let values = match layout {
334 ValueLayout::Bytes => stored,
335 ValueLayout::Packed { packed_dim, word } => {
336 PackedOrder::new(shape.as_slice(), packed_dim)
337 .unpack(bytemuck::cast_slice(&stored), &word)
338 }
339 };
340
341 (values, (qparams, num_params))
342 }
343}
344
345pub fn scale_to_dtype(scale: f32, dtype: ScaleDtype) -> f32 {
355 dtype.round_up(scale)
356}
357
358pub fn global_scale_size(scheme: &QuantScheme) -> usize {
360 global_scale_dtype(scheme).map_or(0, scale_size)
361}
362
363fn storage_elements(scheme: &QuantScheme, shape: &Shape) -> Option<usize> {
371 let num_quants = scheme.num_quants();
372 if num_quants == 0 {
373 return None;
374 }
375
376 match scheme.store {
377 QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim) => {
378 if shape.is_empty() {
380 return (packed_dim == 0).then_some(1);
381 }
382 let packed_dim = shape.num_dims().checked_sub(packed_dim.checked_add(1)?)?;
383 let mut storage = shape.clone();
384 if num_quants > 1 {
385 storage[packed_dim] = storage[packed_dim].div_ceil(num_quants);
386 }
387 storage
388 .iter()
389 .try_fold(1usize, |elements, &dim| elements.checked_mul(dim))
390 }
391 QuantStore::Native => shape
392 .iter()
393 .try_fold(1usize, |elements, &dim| elements.checked_mul(dim)),
394 }
395}
396
397pub fn quantized_data_len(scheme: &QuantScheme, shape: &Shape) -> usize {
400 try_quantized_data_len(scheme, shape)
401 .expect("quantized tensor layout is invalid or overflows usize")
402}
403
404pub(crate) fn try_quantized_data_len(scheme: &QuantScheme, shape: &Shape) -> Option<usize> {
409 let value_bytes_per_element = match scheme.store {
410 QuantStore::Native => core::mem::size_of::<i8>(),
411 _ => scheme.size_bits_stored().div_ceil(8),
412 };
413 let value_bytes = storage_elements(scheme, shape)?.checked_mul(value_bytes_per_element)?;
414
415 let num_params = if let Some(block) = scheme.block_size() {
416 if block.as_slice().len() > shape.num_dims() {
417 return None;
418 }
419 let block_shape = block.to_dim_vec(shape.num_dims());
420 let mut num_params = 1usize;
421 for (&dim, block) in shape.iter().zip(block_shape) {
422 let block = block as usize;
423 if block == 0 {
424 return None;
425 }
426 num_params = num_params.checked_mul(dim.div_ceil(block))?;
427 }
428 num_params
429 } else {
430 1
431 };
432 let scale_bytes = num_params.checked_mul(scale_size(scheme.scale_dtype()))?;
433
434 value_bytes
435 .checked_add(scale_bytes)?
436 .checked_add(global_scale_size(scheme))
437}
438
439fn value_bytes(scheme: &QuantScheme, shape: &Shape) -> usize {
441 storage_elements(scheme, shape).expect("quantized tensor layout is invalid or overflows usize")
442 * scheme.size_bits_stored().div_ceil(8)
443}
444
445pub fn scale_size(dtype: ScaleDtype) -> usize {
447 match dtype {
448 ScaleDtype::F32 => 4,
449 ScaleDtype::F16 | ScaleDtype::BF16 => 2,
450 ScaleDtype::UE8M0 | ScaleDtype::UE4M3 => 1,
451 }
452}
453
454fn decode_scales(bytes: &[u8], dtype: ScaleDtype) -> Vec<f32> {
456 match dtype {
457 ScaleDtype::F32 => bytes
458 .as_chunks::<4>()
459 .0
460 .iter()
461 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
462 .collect(),
463 ScaleDtype::F16 => bytes
464 .as_chunks::<2>()
465 .0
466 .iter()
467 .map(|c| crate::f16::from_ne_bytes([c[0], c[1]]).to_f32())
468 .collect(),
469 ScaleDtype::BF16 => bytes
470 .as_chunks::<2>()
471 .0
472 .iter()
473 .map(|c| crate::bf16::from_ne_bytes([c[0], c[1]]).to_f32())
474 .collect(),
475 ScaleDtype::UE4M3 => bytes.iter().map(|b| e4m3::from_bits(*b).to_f32()).collect(),
476 ScaleDtype::UE8M0 => bytes
477 .iter()
478 .map(|b| ue8m0::from_bits(*b).to_f32())
479 .collect(),
480 }
481}
482
483fn encode_scales(scales: &[f32], dtype: ScaleDtype) -> Vec<u8> {
485 match dtype {
486 ScaleDtype::F32 => scales.iter().flat_map(|s| s.to_ne_bytes()).collect(),
487 ScaleDtype::F16 => scales
488 .iter()
489 .flat_map(|s| crate::f16::from_f32(*s).to_ne_bytes())
490 .collect(),
491 ScaleDtype::BF16 => scales
492 .iter()
493 .flat_map(|s| crate::bf16::from_f32(*s).to_ne_bytes())
494 .collect(),
495 ScaleDtype::UE4M3 => scales
496 .iter()
497 .map(|s| e4m3::from_f32(*s).to_bits())
498 .collect(),
499 ScaleDtype::UE8M0 => scales
502 .iter()
503 .map(|s| ue8m0::from_f32(*s).to_bits())
504 .collect(),
505 }
506}
507
508fn read_bytes_to_i8(bytes: Bytes) -> Vec<i8> {
509 match bytes.try_into_vec::<i8>() {
510 Ok(val) => val,
511 Err(bytes) => unsafe { core::mem::transmute::<Vec<u8>, Vec<i8>>(bytes.to_vec()) },
515 }
516}
517
518#[cfg(test)]
519mod tests {
520 use super::*;
521 use alloc::vec;
522
523 #[test]
524 fn should_pack_unpack_quantization_parameters_per_tensor_symmetric() {
525 let scale = 0.03937008;
527 let values = vec![0i8, 25, 51, 76, 102, 127];
528
529 let q_bytes = QuantizedBytes::new(
530 values.clone(),
531 [2, 3],
532 QuantScheme::default()
533 .with_value(QuantValue::Q8S)
534 .with_store(QuantStore::Native),
535 &[scale],
536 None,
537 );
538
539 let (q_values, qparams) = q_bytes.into_vec_i8();
540
541 assert_eq!(qparams.block, vec![scale]);
542
543 assert_eq!(q_values, values);
544 }
545
546 #[test]
550 fn scale_to_dtype_survives_the_codec() {
551 let scales = [0.5f32, 0.3, 1.0 / 3.0, 500.0, 1e-3, 7.7e-4];
554
555 for dtype in [
556 ScaleDtype::F32,
557 ScaleDtype::F16,
558 ScaleDtype::BF16,
559 ScaleDtype::UE4M3,
560 ScaleDtype::UE8M0,
561 ] {
562 let rounded: Vec<f32> = scales.iter().map(|s| scale_to_dtype(*s, dtype)).collect();
563 let via_codec = decode_scales(&encode_scales(&rounded, dtype), dtype);
564
565 assert_eq!(
566 rounded, via_codec,
567 "the codec moves a scale {dtype:?} can already represent"
568 );
569 for (scale, rounded) in scales.iter().zip(&rounded).filter(|(s, _)| **s < 500.0) {
571 assert!(
572 rounded >= scale,
573 "{scale} rounded down to {rounded} for {dtype:?}"
574 );
575 }
576 }
577 }
578
579 #[test]
582 fn should_pack_unpack_two_level_scales() {
583 let block_scales = [0.5f32, 0.125];
585 let global = 3.0f32;
586 let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
587
588 let scheme = QuantScheme::default()
589 .with_value(QuantValue::Q8S)
590 .with_store(QuantStore::Native)
591 .per_block([4], ScaleDtype::UE4M3)
592 .per_tensor(ScaleDtype::F32);
593
594 let q_bytes = QuantizedBytes::new(values.clone(), [8], scheme, &block_scales, Some(global));
595
596 assert_eq!(q_bytes.bytes.len(), 8 + 2 + 4);
598
599 let (q_values, scales) = q_bytes.into_vec_i8();
600
601 assert_eq!(q_values, values);
602 assert_eq!(scales.block, block_scales);
603 assert_eq!(scales.global, Some(global));
604 }
605
606 #[test]
612 fn should_pack_unpack_ue8m0_block_scales() {
613 let block_scales = [0.25f32, 8.0];
614 let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
615
616 let scheme = QuantScheme::default()
617 .with_value(QuantValue::Q8S)
618 .with_store(QuantStore::Native)
619 .per_block([4], ScaleDtype::UE8M0);
620
621 let q_bytes = QuantizedBytes::new(values.clone(), [8], scheme, &block_scales, None);
622
623 assert_eq!(q_bytes.bytes.len(), 8 + 2);
625
626 let (q_values, scales) = q_bytes.into_vec_i8();
627
628 assert_eq!(q_values, values);
629 assert_eq!(scales.block, block_scales);
630 assert_eq!(scales.global, None);
631 }
632
633 #[test]
634 #[should_panic(expected = "requires a per-tensor scale")]
635 fn two_level_scheme_without_a_global_scale_is_rejected() {
636 let scheme = QuantScheme::default()
637 .with_value(QuantValue::Q8S)
638 .with_store(QuantStore::Native)
639 .per_block([4], ScaleDtype::F32)
640 .per_tensor(ScaleDtype::F32);
641
642 QuantizedBytes::new(vec![0i8; 8], [8], scheme, &[0.5, 0.125], None);
643 }
644
645 #[test]
646 #[should_panic(expected = "stores its per-tensor scale as f32")]
647 fn a_narrower_per_tensor_scale_is_rejected() {
648 let scheme = QuantScheme::default()
649 .with_value(QuantValue::Q8S)
650 .with_store(QuantStore::Native)
651 .per_block([4], ScaleDtype::UE4M3)
652 .per_tensor(ScaleDtype::F16);
653
654 QuantizedBytes::new(vec![0i8; 8], [8], scheme, &[0.5, 0.125], Some(3.0));
655 }
656
657 #[test]
660 fn quantizable_declines_what_no_backend_can_store() {
661 assert!(quantizable(&QuantScheme::default()));
662 assert!(quantizable(
663 &QuantScheme::default().per_block([4], ScaleDtype::F16)
664 ));
665 assert!(quantizable(
666 &QuantScheme::default()
667 .per_block([4], ScaleDtype::UE4M3)
668 .per_tensor(ScaleDtype::F32)
669 ));
670
671 assert!(quantizable(
673 &QuantScheme::default().per_block([4], ScaleDtype::UE8M0)
674 ));
675 assert!(quantizable(
676 &QuantScheme::default().per_tensor(ScaleDtype::UE8M0)
677 ));
678 assert!(quantizable(
680 &QuantScheme::default()
681 .with_value(QuantValue::E2M1)
682 .per_block([32], ScaleDtype::UE8M0)
683 ));
684
685 assert!(!quantizable(
688 &QuantScheme::default()
689 .per_block([4], ScaleDtype::F32)
690 .per_tensor(ScaleDtype::F32)
691 ));
692 assert!(!quantizable(
693 &QuantScheme::default()
694 .per_block([4], ScaleDtype::UE4M3)
695 .per_tensor(ScaleDtype::BF16)
696 ));
697 }
698
699 #[test]
702 fn encoded_scale_width_matches_scale_size() {
703 let scales = [0.5f32, 0.25, 0.125];
705
706 for dtype in [
707 ScaleDtype::F32,
708 ScaleDtype::F16,
709 ScaleDtype::BF16,
710 ScaleDtype::UE4M3,
711 ScaleDtype::UE8M0,
712 ] {
713 assert_eq!(
714 encode_scales(&scales, dtype).len(),
715 scale_size(dtype) * scales.len(),
716 "encoded width disagrees with scale_size for {dtype:?}"
717 );
718 }
719 }
720
721 #[test]
722 fn should_pack_unpack_ue4m3_block_scales() {
723 let scales = [0.5f32, 0.125];
726 let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
727
728 let q_bytes = QuantizedBytes::new(
729 values.clone(),
730 [8],
731 QuantScheme::default()
732 .with_value(QuantValue::Q8S)
733 .with_store(QuantStore::Native)
734 .per_block([4], ScaleDtype::UE4M3),
735 &scales,
736 None,
737 );
738
739 let (q_values, qparams) = q_bytes.into_vec_i8();
740
741 assert_eq!(qparams.block, scales);
742 assert_eq!(q_values, values);
743 }
744}