1pub use cubecl_common::quant::scheme::{
5 BlockScale, BlockSize, QuantMode, QuantScheme, QuantStore, QuantValue, ScaleDtype,
6};
7
8pub const QPARAM_ALIGN: usize = core::mem::align_of::<f32>();
14
15use alloc::vec::Vec;
16use core::any::TypeId;
17use cubecl_common::e4m3;
18use cubecl_common::quant::scheme::{f32_to_ue8m0, ue8m0_to_f32};
19use num_traits::PrimInt;
20use serde::{Deserialize, Serialize};
21
22use crate::{DType, Metadata, Shape, bytes::Bytes};
23
24#[derive(new, Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
30pub struct QuantConfig {
31 pub scheme: QuantScheme,
33 pub propagation: QuantPropagation,
35 }
39
40#[derive(
41 Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default,
42)]
43pub enum QuantAcc {
45 #[default]
47 F32,
48 F16,
50 BF16,
52}
53
54pub enum Calibration {
56 MinMax,
58 AbsMean,
63}
64
65#[derive(
68 Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize, Default,
69)]
70pub enum QuantPropagation {
71 Propagate,
73 #[default]
75 Inhibit,
76}
77
78#[derive(Clone, Debug)]
80pub struct QParams<S> {
81 pub scales: S,
83 pub global: Option<S>,
85}
86
87#[derive(Clone, Debug, PartialEq)]
89pub struct DecodedScales {
90 pub block: Vec<f32>,
92 pub global: Option<f32>,
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct QParamTensor {
99 pub offset_start: usize,
101 pub offset_end: usize,
103 pub metadata: Metadata,
105 pub dtype: DType,
107}
108
109pub fn quantizable(scheme: &QuantScheme) -> bool {
115 match (scheme.block_scale(), global_scale_dtype(scheme)) {
116 (Some(block), Some(global)) => {
117 block.dtype.max_representable() <= crate::f16::MAX.to_f32() && global == ScaleDtype::F32
121 }
122 _ => true,
123 }
124}
125
126pub fn global_scale_dtype(scheme: &QuantScheme) -> Option<ScaleDtype> {
131 scheme.block_scale().and(scheme.tensor_scale())
132}
133
134pub fn params_shape(data_shape: &Shape, scheme: &QuantScheme) -> Shape {
138 match scheme.block_size() {
139 None => Shape::new([1]),
140 Some(block_size) => Shape::from(block_size.num_blocks(data_shape.as_slice())),
141 }
142}
143
144#[derive(Debug, Clone)]
148pub struct BlockLayout {
149 shape: Shape,
150 block: Vec<u8>,
151 blocks: Shape,
152}
153
154impl BlockLayout {
155 pub fn new(shape: &Shape, block: &BlockSize) -> Self {
157 Self {
158 shape: shape.clone(),
159 block: block.to_dim_vec(shape.num_dims()),
160 blocks: Shape::from(block.num_blocks(shape.as_slice())),
161 }
162 }
163
164 pub fn num_blocks(&self) -> usize {
166 self.blocks.num_elements()
167 }
168
169 pub fn divides(&self) -> bool {
171 self.shape
172 .iter()
173 .zip(&self.block)
174 .all(|(&dim, &extent)| dim.is_multiple_of(extent as usize))
175 }
176
177 pub fn block_of(&self, mut index: usize) -> usize {
179 let mut block = 0;
180 let mut stride = 1;
181 for dim in (0..self.shape.num_dims()).rev() {
182 let coordinate = index % self.shape[dim];
183 index /= self.shape[dim];
184 block += coordinate / self.block[dim] as usize * stride;
185 stride *= self.blocks[dim];
186 }
187 block
188 }
189}
190
191pub struct QuantizedBytes {
201 pub bytes: Bytes,
203 pub scheme: QuantScheme,
205 pub shape: Shape,
208}
209
210impl QuantizedBytes {
211 pub fn new<E: bytemuck::CheckedBitPattern + bytemuck::NoUninit>(
216 value: Vec<E>,
217 shape: impl Into<Shape>,
218 scheme: QuantScheme,
219 scales: &[f32],
220 global: Option<f32>,
221 ) -> Self {
222 let shape = shape.into();
223 assert_eq!(
224 value.len(),
225 shape.num_elements(),
226 "{} quantized values do not fill a tensor of shape {shape:?}",
227 value.len()
228 );
229 if TypeId::of::<E>() != TypeId::of::<i8>() {
231 panic!("Invalid quantized type");
232 }
233
234 let i8s: Vec<i8> = bytemuck::allocation::cast_vec(value);
236 let mut bytes = Bytes::from_elems(i8s);
237
238 let scales = match scheme.block_size() {
239 None => &scales[..1],
240 Some(_) => scales,
241 };
242 let scale_bytes = encode_scales(scales, scheme.scale_dtype());
243 bytes.extend_from_byte_slice_aligned(scale_bytes.as_slice(), QPARAM_ALIGN);
244
245 match (global_scale_dtype(&scheme), global) {
247 (Some(dtype), Some(global)) => {
248 assert_eq!(
251 dtype,
252 ScaleDtype::F32,
253 "a two-level scheme stores its per-tensor scale as f32, got {scheme:?}"
254 );
255 let global_bytes = encode_scales(&[global], dtype);
256 bytes.extend_from_byte_slice_aligned(global_bytes.as_slice(), QPARAM_ALIGN);
257 }
258 (Some(_), None) => panic!("{scheme:?} requires a per-tensor scale"),
259 (None, Some(_)) => panic!("{scheme:?} does not take a per-tensor scale"),
260 (None, None) => {}
261 }
262
263 Self {
264 bytes,
265 scheme,
266 shape,
267 }
268 }
269
270 pub fn num_elements(&self) -> usize {
272 self.shape.num_elements()
273 }
274
275 pub fn into_vec_i8(self) -> (Vec<i8>, DecodedScales) {
277 let scheme = self.scheme;
278 let (values, (qparams, num_params)) = self.split_values_off();
279
280 let global_bytes = global_scale_size(&scheme);
282 let block_end = qparams
283 .len()
284 .checked_sub(global_bytes)
285 .expect("quantized parameter buffer is shorter than the scheme's global scale");
286 let block_start = block_end
287 .checked_sub(scale_size(scheme.scale_dtype()) * num_params)
288 .expect("quantized parameter buffer is shorter than the scheme's block scales");
289
290 let block = decode_scales(&qparams[block_start..block_end], scheme.scale_dtype());
291 let global =
292 global_scale_dtype(&scheme).map(|dtype| decode_scales(&qparams[block_end..], dtype)[0]);
293
294 (values, DecodedScales { block, global })
295 }
296
297 fn split_i8_values(self, scale_bytes: usize) -> (Vec<i8>, Vec<u8>) {
298 let mut values = read_bytes_to_i8(self.bytes);
299
300 let values_end = values
301 .len()
302 .checked_sub(scale_bytes)
303 .expect("quantized tensor data is shorter than its scheme's parameters");
304 let qparams = values.split_off(values_end);
305
306 (values, bytemuck::cast_vec(qparams))
307 }
308
309 fn split_values_off(self) -> (Vec<i8>, (Vec<u8>, usize)) {
314 let num_params = params_shape(&self.shape, &self.scheme).num_elements();
315 let scale_bytes =
316 scale_size(self.scheme.scale_dtype()) * num_params + global_scale_size(&self.scheme);
317
318 if let QuantStore::PackedU32(packed_dim) = self.scheme.store {
319 assert_eq!(
320 packed_dim, 0,
321 "Packing must be on innermost dimension for splitting off values"
322 );
323 }
324
325 let (values, qparams) = match self.scheme.store {
326 QuantStore::Native => self.split_i8_values(scale_bytes),
327 QuantStore::PackedU32(_) => match self.scheme.value {
328 QuantValue::Q8F | QuantValue::Q8S => self.split_i8_values(scale_bytes),
329 QuantValue::Q4F | QuantValue::Q4S | QuantValue::Q2F | QuantValue::Q2S => {
330 let split_at =
331 self.bytes.len().checked_sub(scale_bytes).expect(
332 "quantized tensor data is shorter than its scheme's parameters",
333 );
334 let qparams = self.bytes[split_at..].to_vec();
335 let values = bytemuck::cast_slice::<_, u32>(&self.bytes[..split_at]);
336 let values = unpack_q_to_i8s(values, self.num_elements(), &self.scheme.value);
338 (values, qparams)
339 }
340 QuantValue::E4M3 | QuantValue::E5M2 | QuantValue::E2M1 => {
341 unimplemented!("Not yet supported")
342 }
343 },
344 QuantStore::PackedNative(_) => unimplemented!("Not yet supported"),
345 };
346
347 (values, (qparams, num_params))
348 }
349}
350
351pub fn scale_to_dtype(scale: f32, dtype: ScaleDtype) -> f32 {
361 dtype.round_up(scale)
362}
363
364pub fn global_scale_size(scheme: &QuantScheme) -> usize {
366 global_scale_dtype(scheme).map_or(0, scale_size)
367}
368
369fn storage_elements(scheme: &QuantScheme, shape: &Shape) -> usize {
377 let num_quants = scheme.num_quants();
378
379 match scheme.store {
380 QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim)
381 if num_quants > 1 && !shape.is_empty() =>
382 {
383 let packed_dim = shape.num_dims() - packed_dim - 1;
384 let mut storage = shape.clone();
385 storage[packed_dim] = storage[packed_dim].div_ceil(num_quants);
386 storage.num_elements()
387 }
388 _ => shape.num_elements().div_ceil(num_quants),
389 }
390}
391
392pub fn quantized_data_len(scheme: &QuantScheme, shape: &Shape) -> usize {
395 let value_bytes = storage_elements(scheme, shape) * scheme.size_bits_stored().div_ceil(8);
396
397 let num_params = params_shape(shape, scheme).num_elements();
398 let scale_bytes = num_params * scale_size(scheme.scale_dtype());
399
400 value_bytes + scale_bytes + global_scale_size(scheme)
401}
402
403pub fn scale_size(dtype: ScaleDtype) -> usize {
405 match dtype {
406 ScaleDtype::F32 => 4,
407 ScaleDtype::F16 | ScaleDtype::BF16 => 2,
408 ScaleDtype::UE8M0 | ScaleDtype::UE4M3 => 1,
409 }
410}
411
412fn decode_scales(bytes: &[u8], dtype: ScaleDtype) -> Vec<f32> {
414 match dtype {
415 ScaleDtype::F32 => bytes
416 .as_chunks::<4>()
417 .0
418 .iter()
419 .map(|c| f32::from_ne_bytes([c[0], c[1], c[2], c[3]]))
420 .collect(),
421 ScaleDtype::F16 => bytes
422 .as_chunks::<2>()
423 .0
424 .iter()
425 .map(|c| crate::f16::from_ne_bytes([c[0], c[1]]).to_f32())
426 .collect(),
427 ScaleDtype::BF16 => bytes
428 .as_chunks::<2>()
429 .0
430 .iter()
431 .map(|c| crate::bf16::from_ne_bytes([c[0], c[1]]).to_f32())
432 .collect(),
433 ScaleDtype::UE4M3 => bytes.iter().map(|b| e4m3::from_bits(*b).to_f32()).collect(),
434 ScaleDtype::UE8M0 => bytes.iter().map(|b| ue8m0_to_f32(*b)).collect(),
435 }
436}
437
438fn encode_scales(scales: &[f32], dtype: ScaleDtype) -> Vec<u8> {
440 match dtype {
441 ScaleDtype::F32 => scales.iter().flat_map(|s| s.to_ne_bytes()).collect(),
442 ScaleDtype::F16 => scales
443 .iter()
444 .flat_map(|s| crate::f16::from_f32(*s).to_ne_bytes())
445 .collect(),
446 ScaleDtype::BF16 => scales
447 .iter()
448 .flat_map(|s| crate::bf16::from_f32(*s).to_ne_bytes())
449 .collect(),
450 ScaleDtype::UE4M3 => scales
451 .iter()
452 .map(|s| e4m3::from_f32(*s).to_bits())
453 .collect(),
454 ScaleDtype::UE8M0 => scales.iter().map(|s| f32_to_ue8m0(*s)).collect(),
457 }
458}
459
460fn read_bytes_to_i8(bytes: Bytes) -> Vec<i8> {
461 match bytes.try_into_vec::<i8>() {
462 Ok(val) => val,
463 Err(bytes) => unsafe { core::mem::transmute::<Vec<u8>, Vec<i8>>(bytes.to_vec()) },
467 }
468}
469
470pub fn pack_i8s_to_u32s(values: Vec<i8>) -> Vec<u32> {
472 #[cfg(target_endian = "big")]
476 {
477 values
478 .chunks(4)
479 .map(|x| {
480 x.iter()
481 .enumerate()
482 .fold(0u32, |acc, (i, x)| acc | (*x as u32 & 0xFF) << (i * 8))
483 })
484 .collect()
485 }
486
487 #[cfg(target_endian = "little")]
490 {
491 let mut values = values;
492 let remainder = values.len() % 4;
493 if remainder != 0 {
494 values.extend(core::iter::repeat_n(0, 4 - remainder));
496 }
497
498 let len = values.len() / 4;
499 let capacity = values.capacity() / 4;
500
501 let mut values = core::mem::ManuallyDrop::new(values);
503 let ptr = values.as_mut_ptr() as *mut u32;
504
505 unsafe { Vec::from_raw_parts(ptr, len, capacity) }
506 }
507}
508
509pub(crate) fn unpack_q_to_i8s<Q: PrimInt>(
511 values: &[Q],
512 numel: usize,
513 value: &QuantValue,
514) -> Vec<i8> {
515 let size_store = size_of::<Q>() * 8;
516 let size_quant = value.size_bits();
517 let num_quants = size_store / size_quant;
518 let mask = Q::from((1 << size_quant) - 1).unwrap();
519 let sign_shift = 8 - size_quant; values
521 .iter()
522 .enumerate()
523 .flat_map(|(i, &packed)| {
524 let n = core::cmp::min(num_quants, numel - i * num_quants);
526 (0..n).map(move |i| {
533 let raw = (packed >> (i * size_quant) & mask).to_u8().unwrap();
534 ((raw << sign_shift) as i8) >> sign_shift
535 })
536 })
537 .collect()
538}
539
540#[cfg(test)]
541mod tests {
542
543 use super::*;
544 use alloc::vec;
545
546 #[test]
547 fn should_pack_i8s_to_u32() {
548 let packed = pack_i8s_to_u32s(vec![-128, 2, -3, 127]);
549
550 assert_eq!(packed, vec![2147287680]);
551 }
552
553 #[test]
554 fn should_pack_i8s_to_u32_padded() {
555 let packed = pack_i8s_to_u32s(vec![-128, 2, -3, 127, 55]);
556 let packed_padded = pack_i8s_to_u32s(vec![-128, 2, -3, 127, 55, 0, 0, 0]);
557
558 assert_eq!(packed, vec![2147287680, 55]);
559 assert_eq!(packed, packed_padded);
560 }
561
562 #[test]
563 fn should_unpack_u32s_to_i8s() {
564 let unpacked = unpack_q_to_i8s(&[2147287680u32], 4, &QuantValue::Q8S);
565
566 assert_eq!(unpacked, vec![-128, 2, -3, 127]);
567 }
568
569 #[test]
570 fn should_unpack_u32s_to_i8s_padded() {
571 let unpacked = unpack_q_to_i8s(&[55u32], 1, &QuantValue::Q8S);
572
573 assert_eq!(unpacked, vec![55]);
574 }
575
576 #[test]
577 fn should_unpack_u32s_to_i8s_arange() {
578 let unpacked = unpack_q_to_i8s(
579 &[
580 0u32, 286331136, 286331153, 572657937, 572662306, 857874978, 858993459, 858993459,
581 1145324612, 1145324612, 1431655748, 1431655765, 1717982549, 1717986918, 2003199590,
582 2004318071,
583 ],
584 128,
585 &QuantValue::Q4S,
586 );
587
588 assert_eq!(
589 unpacked,
590 vec![
591 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
592 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
593 3, 3, 3, 3, 3, 3, 3, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 5, 5,
594 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,
595 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7
596 ]
597 );
598 }
599
600 #[test]
601 fn should_pack_unpack_quantization_parameters_per_tensor_symmetric() {
602 let scale = 0.03937008;
604 let values = vec![0i8, 25, 51, 76, 102, 127];
605
606 let q_bytes = QuantizedBytes::new(
607 values.clone(),
608 [2, 3],
609 QuantScheme::default()
610 .with_value(QuantValue::Q8S)
611 .with_store(QuantStore::Native),
612 &[scale],
613 None,
614 );
615
616 let (q_values, qparams) = q_bytes.into_vec_i8();
617
618 assert_eq!(qparams.block, vec![scale]);
619
620 assert_eq!(q_values, values);
621 }
622
623 #[test]
627 fn scale_to_dtype_survives_the_codec() {
628 let scales = [0.5f32, 0.3, 1.0 / 3.0, 500.0, 1e-3, 7.7e-4];
631
632 for dtype in [
633 ScaleDtype::F32,
634 ScaleDtype::F16,
635 ScaleDtype::BF16,
636 ScaleDtype::UE4M3,
637 ScaleDtype::UE8M0,
638 ] {
639 let rounded: Vec<f32> = scales.iter().map(|s| scale_to_dtype(*s, dtype)).collect();
640 let via_codec = decode_scales(&encode_scales(&rounded, dtype), dtype);
641
642 assert_eq!(
643 rounded, via_codec,
644 "the codec moves a scale {dtype:?} can already represent"
645 );
646 for (scale, rounded) in scales.iter().zip(&rounded).filter(|(s, _)| **s < 500.0) {
648 assert!(
649 rounded >= scale,
650 "{scale} rounded down to {rounded} for {dtype:?}"
651 );
652 }
653 }
654 }
655
656 #[test]
659 fn should_pack_unpack_two_level_scales() {
660 let block_scales = [0.5f32, 0.125];
662 let global = 3.0f32;
663 let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
664
665 let scheme = QuantScheme::default()
666 .with_value(QuantValue::Q8S)
667 .with_store(QuantStore::Native)
668 .per_block([4], ScaleDtype::UE4M3)
669 .per_tensor(ScaleDtype::F32);
670
671 let q_bytes = QuantizedBytes::new(values.clone(), [8], scheme, &block_scales, Some(global));
672
673 assert_eq!(q_bytes.bytes.len(), 8 + 2 + 4);
675
676 let (q_values, scales) = q_bytes.into_vec_i8();
677
678 assert_eq!(q_values, values);
679 assert_eq!(scales.block, block_scales);
680 assert_eq!(scales.global, Some(global));
681 }
682
683 #[test]
689 fn should_pack_unpack_ue8m0_block_scales() {
690 let block_scales = [0.25f32, 8.0];
691 let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
692
693 let scheme = QuantScheme::default()
694 .with_value(QuantValue::Q8S)
695 .with_store(QuantStore::Native)
696 .per_block([4], ScaleDtype::UE8M0);
697
698 let q_bytes = QuantizedBytes::new(values.clone(), [8], scheme, &block_scales, None);
699
700 assert_eq!(q_bytes.bytes.len(), 8 + 2);
702
703 let (q_values, scales) = q_bytes.into_vec_i8();
704
705 assert_eq!(q_values, values);
706 assert_eq!(scales.block, block_scales);
707 assert_eq!(scales.global, None);
708 }
709
710 #[test]
711 #[should_panic(expected = "requires a per-tensor scale")]
712 fn two_level_scheme_without_a_global_scale_is_rejected() {
713 let scheme = QuantScheme::default()
714 .with_value(QuantValue::Q8S)
715 .with_store(QuantStore::Native)
716 .per_block([4], ScaleDtype::F32)
717 .per_tensor(ScaleDtype::F32);
718
719 QuantizedBytes::new(vec![0i8; 8], [8], scheme, &[0.5, 0.125], None);
720 }
721
722 #[test]
723 #[should_panic(expected = "stores its per-tensor scale as f32")]
724 fn a_narrower_per_tensor_scale_is_rejected() {
725 let scheme = QuantScheme::default()
726 .with_value(QuantValue::Q8S)
727 .with_store(QuantStore::Native)
728 .per_block([4], ScaleDtype::UE4M3)
729 .per_tensor(ScaleDtype::F16);
730
731 QuantizedBytes::new(vec![0i8; 8], [8], scheme, &[0.5, 0.125], Some(3.0));
732 }
733
734 #[test]
737 fn quantizable_declines_what_no_backend_can_store() {
738 assert!(quantizable(&QuantScheme::default()));
739 assert!(quantizable(
740 &QuantScheme::default().per_block([4], ScaleDtype::F16)
741 ));
742 assert!(quantizable(
743 &QuantScheme::default()
744 .per_block([4], ScaleDtype::UE4M3)
745 .per_tensor(ScaleDtype::F32)
746 ));
747
748 assert!(quantizable(
750 &QuantScheme::default().per_block([4], ScaleDtype::UE8M0)
751 ));
752 assert!(quantizable(
753 &QuantScheme::default().per_tensor(ScaleDtype::UE8M0)
754 ));
755 assert!(quantizable(
757 &QuantScheme::default()
758 .with_value(QuantValue::E2M1)
759 .per_block([32], ScaleDtype::UE8M0)
760 ));
761
762 assert!(!quantizable(
765 &QuantScheme::default()
766 .per_block([4], ScaleDtype::F32)
767 .per_tensor(ScaleDtype::F32)
768 ));
769 assert!(!quantizable(
770 &QuantScheme::default()
771 .per_block([4], ScaleDtype::UE4M3)
772 .per_tensor(ScaleDtype::BF16)
773 ));
774 }
775
776 #[test]
779 fn encoded_scale_width_matches_scale_size() {
780 let scales = [0.5f32, 0.25, 0.125];
782
783 for dtype in [
784 ScaleDtype::F32,
785 ScaleDtype::F16,
786 ScaleDtype::BF16,
787 ScaleDtype::UE4M3,
788 ScaleDtype::UE8M0,
789 ] {
790 assert_eq!(
791 encode_scales(&scales, dtype).len(),
792 scale_size(dtype) * scales.len(),
793 "encoded width disagrees with scale_size for {dtype:?}"
794 );
795 }
796 }
797
798 #[test]
799 fn should_pack_unpack_ue4m3_block_scales() {
800 let scales = [0.5f32, 0.125];
803 let values = vec![0i8, 25, 51, 76, 102, 127, -128, -1];
804
805 let q_bytes = QuantizedBytes::new(
806 values.clone(),
807 [8],
808 QuantScheme::default()
809 .with_value(QuantValue::Q8S)
810 .with_store(QuantStore::Native)
811 .per_block([4], ScaleDtype::UE4M3),
812 &scales,
813 None,
814 );
815
816 let (q_values, qparams) = q_bytes.into_vec_i8();
817
818 assert_eq!(qparams.block, scales);
819 assert_eq!(q_values, values);
820 }
821}