1use alloc::vec;
2use alloc::vec::Vec;
3use core::{default::Default, ops::Deref};
4use serde::{Deserialize, Serialize};
5
6use crate::{e2m1, ue8m0};
7
8#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
34pub struct QuantScheme {
35 pub value: QuantValue,
39 pub store: QuantStore,
41 pub mode: QuantMode,
43 tensor: Option<ScaleDtype>,
46 block: Option<BlockScale>,
48}
49
50impl Default for QuantScheme {
51 fn default() -> Self {
52 Self {
53 value: QuantValue::Q8F,
54 store: QuantStore::PackedU32(0),
55 mode: QuantMode::Symmetric,
56 tensor: None,
57 block: None,
58 }
59 }
60}
61
62impl QuantScheme {
63 pub fn with_mode(mut self, mode: QuantMode) -> Self {
65 self.mode = mode;
66 self
67 }
68
69 pub fn with_value(mut self, value: QuantValue) -> Self {
71 self.value = value;
72 self
73 }
74
75 pub fn with_store(mut self, store: QuantStore) -> Self {
77 self.store = store;
78 self
79 }
80
81 pub fn per_tensor(mut self, dtype: ScaleDtype) -> Self {
83 self.tensor = Some(dtype);
84 self
85 }
86
87 pub fn per_block(mut self, block: impl AsRef<[u8]>, dtype: ScaleDtype) -> Self {
89 self.block = Some(BlockScale {
90 size: BlockSize::new(block),
91 dtype,
92 });
93 self
94 }
95
96 pub fn tensor_scale(&self) -> Option<ScaleDtype> {
102 if self.tensor.is_none() && self.block.is_none() {
103 return Some(ScaleDtype::F32);
104 }
105 self.tensor
106 }
107
108 pub fn block_scale(&self) -> Option<BlockScale> {
110 self.block
111 }
112
113 pub fn num_levels(&self) -> usize {
115 self.block_scale().is_some() as usize + self.tensor_scale().is_some() as usize
116 }
117
118 pub fn scale_dtype(&self) -> ScaleDtype {
120 self.block
121 .map(|block| block.dtype)
122 .or(self.tensor)
123 .unwrap_or(ScaleDtype::F32)
124 }
125
126 pub fn block_size(&self) -> Option<BlockSize> {
128 self.block.map(|block| block.size)
129 }
130
131 pub fn swap_block_dims(&mut self, rank: usize, dim0: usize, dim1: usize) {
136 let mut axes: Vec<usize> = (0..rank).collect();
137 axes.swap(dim0, dim1);
138 self.permute_block_dims(rank, &axes);
139 }
140
141 pub fn permute_block_dims(&mut self, rank: usize, axes: &[usize]) {
144 if let Some(block) = &mut self.block {
145 let dims = block.size.to_dim_vec(rank);
146 let permuted: Vec<u8> = axes.iter().map(|&axis| dims[axis]).collect();
147 block.size = BlockSize::new(permuted);
148 }
149 }
150
151 pub fn size_bits_stored(&self) -> usize {
153 self.store.size_bits(&self.value)
154 }
155
156 pub fn size_bits_value(&self) -> usize {
158 self.value.size_bits()
159 }
160
161 pub fn num_quants(&self) -> usize {
163 self.size_bits_stored() / self.value.size_bits()
164 }
165
166 pub fn native_packing(&self) -> usize {
169 self.value.native_packing()
170 }
171
172 pub fn packing_dim(&self) -> Option<usize> {
174 self.store.packing_dim()
175 }
176
177 fn swap_packing_dim(&mut self, rank: usize, dim0: usize, dim1: usize) {
181 let mut axes: Vec<usize> = (0..rank).collect();
182 axes.swap(dim0, dim1);
183 self.permute_packing_dim(&axes);
184 }
185
186 fn permute_packing_dim(&mut self, axes: &[usize]) {
188 if let QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim) =
190 &mut self.store
191 && !axes.is_empty()
192 {
193 let rank = axes.len();
194 let packed_axis = rank
195 .checked_sub(*packed_dim + 1)
196 .expect("the packing dim to be one of the permuted axes");
197 let new_axis = axes
198 .iter()
199 .position(|axis| *axis == packed_axis)
200 .expect("the permutation to contain the packed axis");
201 *packed_dim = rank - new_axis - 1;
202 }
203 }
204
205 pub fn swap_dims(&mut self, rank: usize, dim0: usize, dim1: usize) {
208 self.swap_block_dims(rank, dim0, dim1);
209 self.swap_packing_dim(rank, dim0, dim1);
210 }
211
212 pub fn permute_dims(&mut self, axes: &[usize]) {
214 self.permute_block_dims(axes.len(), axes);
215 self.permute_packing_dim(axes);
216 }
217
218 pub fn reshape_packing_dim(&mut self, rank: usize) {
222 if let QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim) =
223 &mut self.store
224 && *packed_dim >= rank.max(1)
225 {
226 *packed_dim = 0;
227 }
228 }
229}
230
231#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
233pub struct BlockScale {
234 pub size: BlockSize,
236 pub dtype: ScaleDtype,
238}
239
240impl ScaleDtype {
241 pub fn max_representable(&self) -> f32 {
250 match self {
251 ScaleDtype::F32 => f32::MAX,
252 ScaleDtype::F16 => half::f16::MAX.to_f32(),
253 ScaleDtype::BF16 => half::bf16::MAX.to_f32(),
254 ScaleDtype::UE8M0 => ue8m0::MAX.to_f32(),
255 ScaleDtype::UE4M3 => 448.0,
258 }
259 }
260
261 pub fn round_up(&self, scale: f32) -> f32 {
276 if scale.is_nan() {
277 return scale;
278 }
279 debug_assert!(scale >= 0.0, "a quantization scale is never negative");
280
281 match self {
282 ScaleDtype::F32 => {
283 return scale;
284 }
285 ScaleDtype::UE8M0 => {
286 return ue8m0::from_f32(scale).to_f32();
287 }
288 _ => {}
289 }
290
291 let max = self.max_representable();
294 if scale >= max {
295 return max;
296 }
297
298 let grid = self.f32_grid();
299
300 if let Some(subnormals) = grid.subnormals
301 && scale < subnormals.min_normal
302 {
303 return num_traits::Float::ceil(scale / subnormals.spacing) * subnormals.spacing;
306 }
307
308 f32::from_bits((scale.to_bits() + grid.round_up_bias()) & grid.truncate_mask())
309 }
310
311 pub fn f32_grid(&self) -> F32Grid {
322 const fn bit_step(mantissa_digits: u32) -> u32 {
324 1 << (f32::MANTISSA_DIGITS - mantissa_digits)
325 }
326
327 match self {
328 ScaleDtype::F16 => F32Grid {
329 bit_step: bit_step(half::f16::MANTISSA_DIGITS),
330 subnormals: Some(SubnormalRange {
331 min_normal: half::f16::MIN_POSITIVE.to_f32(),
332 spacing: half::f16::MIN_POSITIVE_SUBNORMAL.to_f32(),
333 }),
334 },
335 ScaleDtype::BF16 => F32Grid {
336 bit_step: bit_step(half::bf16::MANTISSA_DIGITS),
337 subnormals: None,
338 },
339 ScaleDtype::UE4M3 => F32Grid {
342 bit_step: bit_step(4),
343 subnormals: Some(SubnormalRange {
344 min_normal: 0.015625, spacing: 0.001953125, }),
347 },
348 ScaleDtype::F32 => {
349 unimplemented!("F32 is the grid, it has no narrower one to round onto")
350 }
351 ScaleDtype::UE8M0 => F32Grid {
355 bit_step: bit_step(1),
356 subnormals: None,
357 },
358 }
359 }
360
361 pub const UE8M0_MIN: f32 = ue8m0::MIN.to_f32();
363 pub const UE8M0_MAX: f32 = ue8m0::MAX.to_f32();
365}
366
367#[derive(Clone, Copy, Debug, PartialEq)]
375pub struct F32Grid {
376 pub bit_step: u32,
378 pub subnormals: Option<SubnormalRange>,
380}
381
382#[derive(Clone, Copy, Debug, PartialEq)]
384pub struct SubnormalRange {
385 pub min_normal: f32,
387 pub spacing: f32,
389}
390
391impl F32Grid {
392 pub fn truncate_mask(&self) -> u32 {
394 !(self.bit_step - 1)
395 }
396
397 pub fn round_up_bias(&self) -> u32 {
401 self.bit_step - 1
402 }
403}
404
405#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
407pub enum QuantValue {
408 Q8F,
410 E5M2,
412 E4M3,
414 Q4F,
416 E2M1,
418 Q2F,
420 Q8S,
422 Q4S,
424 Q2S,
426}
427
428impl QuantValue {
429 pub fn size_bits(&self) -> usize {
431 match self {
432 QuantValue::Q8F | QuantValue::Q8S | QuantValue::E4M3 | QuantValue::E5M2 => 8,
433 QuantValue::Q4F | QuantValue::Q4S | QuantValue::E2M1 => 4,
434 QuantValue::Q2F | QuantValue::Q2S => 2,
435 }
436 }
437
438 pub fn native_packing(&self) -> usize {
441 match self {
442 QuantValue::E2M1 => 2,
443 _ => 1,
444 }
445 }
446
447 pub fn range(&self) -> (f32, f32) {
449 match self {
450 QuantValue::Q8F => (i8::MIN as f32, i8::MAX as f32),
451 QuantValue::Q4F => (-8.0, 7.0),
452 QuantValue::Q2F => (-2.0, 1.0),
453 QuantValue::Q8S => (-i8::MAX as f32, i8::MAX as f32),
454 QuantValue::Q4S => (-7.0, 7.0),
455 QuantValue::Q2S => (-1.0, 1.0),
456 QuantValue::E4M3 => (-448.0, 448.0),
457 QuantValue::E5M2 => (-57344.0, 57344.0),
458 QuantValue::E2M1 => (e2m1::MIN.to_f32(), e2m1::MAX.to_f32()),
459 }
460 }
461
462 pub fn is_float(&self) -> bool {
464 match self {
465 Self::E4M3 | Self::E5M2 | Self::E2M1 => true,
466 Self::Q8F | Self::Q8S | Self::Q4F | Self::Q4S | Self::Q2F | Self::Q2S => false,
467 }
468 }
469
470 pub fn is_symmetric(&self) -> bool {
472 match self {
473 Self::Q8F | Self::Q4F | Self::Q2F | Self::E4M3 | Self::E5M2 | Self::E2M1 => false,
474 Self::Q8S | Self::Q4S | Self::Q2S => true,
475 }
476 }
477}
478
479impl QuantStore {
480 pub fn size_bits(&self, value: &QuantValue) -> usize {
482 match self {
483 QuantStore::Native => value.size_bits(),
484 QuantStore::PackedNative(_) => value.size_bits() * value.native_packing(),
485 QuantStore::PackedU32(_) => 32,
486 }
487 }
488
489 fn packing_dim(&self) -> Option<usize> {
490 match self {
491 QuantStore::Native => None,
492 QuantStore::PackedNative(packing_dim) | QuantStore::PackedU32(packing_dim) => {
493 Some(*packing_dim)
494 }
495 }
496 }
497}
498
499#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
501pub enum QuantStore {
502 Native,
504 PackedNative(usize),
507 PackedU32(usize),
510 }
513
514#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
516pub enum QuantMode {
517 Symmetric,
519 Lookup,
525}
526
527#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
529pub enum ScaleDtype {
530 F32,
532 F16,
534 BF16,
536 UE8M0,
538 UE4M3,
540}
541
542const MAX_DIMS: usize = 5;
543
544#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
546pub struct BlockSize {
547 storage: [u8; MAX_DIMS],
548 len: u8,
549}
550
551impl PartialOrd for BlockSize {
554 fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
555 Some(self.cmp(other))
556 }
557}
558
559impl Ord for BlockSize {
560 fn cmp(&self, other: &Self) -> core::cmp::Ordering {
561 (self.len, self.as_slice()).cmp(&(other.len, other.as_slice()))
562 }
563}
564
565impl core::fmt::Debug for BlockSize {
566 fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
567 write!(f, "BlockSize({:?})", self.as_slice())
568 }
569}
570
571impl BlockSize {
572 pub const MAX_DIMS: usize = MAX_DIMS;
574
575 pub fn new(values: impl AsRef<[u8]>) -> Self {
582 Self::canonicalize(values.as_ref())
583 }
584
585 fn canonicalize(values: &[u8]) -> Self {
586 let skip = values
587 .iter()
588 .position(|&value| value != 1)
589 .unwrap_or(values.len());
590 let values = &values[skip..];
591 debug_assert!(
592 values.len() <= MAX_DIMS,
593 "Tried creating a block size larger than the cap"
594 );
595 let len = values.len().min(MAX_DIMS);
596 let mut storage = [1; MAX_DIMS];
597 storage[..len].copy_from_slice(&values[..len]);
598 Self {
599 storage,
600 len: len as u8,
601 }
602 }
603
604 pub fn as_slice(&self) -> &[u8] {
606 &self.storage[..self.len as usize]
607 }
608
609 pub fn to_vec(&self) -> Vec<u8> {
611 self.storage[..self.len as usize].to_vec()
612 }
613
614 pub fn as_dim<const N: usize>(&self) -> [u8; N] {
616 let data_len = N.min(self.len as usize);
617 let data_start = N - data_len;
618 let mut out = [1; N];
619 out[data_start..].copy_from_slice(&self.storage[..data_len]);
620 out
621 }
622
623 pub fn to_dim_vec(&self, len: usize) -> Vec<u8> {
626 let data_len = len.min(self.len as usize);
627 let data_start = len - data_len;
628 let mut out = vec![1; len];
629 out[data_start..].copy_from_slice(&self.storage[..data_len]);
630 out
631 }
632
633 pub fn num_blocks(&self, shape: &[usize]) -> Vec<usize> {
636 self.to_dim_vec(shape.len())
637 .into_iter()
638 .zip(shape)
639 .map(|(block, &dim)| dim.div_ceil(block as usize))
640 .collect()
641 }
642
643 pub fn iter(&self) -> impl Iterator<Item = &u8> {
645 self.as_slice().iter()
646 }
647
648 pub fn num_elements(&self) -> usize {
650 self.iter().map(|it| *it as usize).product()
651 }
652}
653
654impl Deref for BlockSize {
655 type Target = [u8];
656
657 fn deref(&self) -> &Self::Target {
658 self.as_slice()
659 }
660}
661
662impl<T: AsRef<[u8]>> From<T> for BlockSize {
663 fn from(value: T) -> Self {
664 BlockSize::new(value)
665 }
666}
667
668macro_rules! codes {
671 ($ty:ty { $($variant:ident => $code:literal),+ $(,)? }) => {
672 impl $ty {
673 fn to_code(self) -> u64 {
674 match self {
675 $(Self::$variant => $code,)+
676 }
677 }
678
679 fn from_code(code: u64) -> Option<Self> {
680 match code {
681 $($code => Some(Self::$variant),)+
682 _ => None,
683 }
684 }
685 }
686 };
687}
688
689codes!(QuantValue {
690 Q8F => 0,
691 E5M2 => 1,
692 E4M3 => 2,
693 Q4F => 3,
694 E2M1 => 4,
695 Q2F => 5,
696 Q8S => 6,
697 Q4S => 7,
698 Q2S => 8,
699});
700
701codes!(QuantMode {
702 Symmetric => 0,
703 Lookup => 1,
704});
705
706codes!(ScaleDtype {
708 F32 => 1,
709 F16 => 2,
710 BF16 => 3,
711 UE8M0 => 4,
712 UE4M3 => 5,
713});
714
715impl QuantScheme {
723 pub fn to_code(&self) -> Option<u64> {
727 let (store, axis) = match self.store {
728 QuantStore::Native => (0, 0),
729 QuantStore::PackedNative(axis) => (1, axis),
730 QuantStore::PackedU32(axis) => (2, axis),
731 };
732 if axis >= 64 {
733 return None;
734 }
735 let level = |dtype: Option<ScaleDtype>| dtype.map_or(0, ScaleDtype::to_code);
736 let sizes = self
737 .block
738 .as_ref()
739 .map_or(&[][..], |block| block.size.as_slice());
740 if sizes.contains(&0) {
741 return None;
742 }
743 let packed_sizes = sizes
744 .iter()
745 .enumerate()
746 .fold(0u64, |packed, (index, size)| {
747 packed | (u64::from(*size) << (8 * index))
748 });
749 Some(
750 self.value.to_code()
751 | (store << 4)
752 | ((axis as u64) << 6)
753 | (self.mode.to_code() << 12)
754 | (level(self.tensor) << 14)
755 | (level(self.block.map(|block| block.dtype)) << 17)
756 | ((sizes.len() as u64) << 20)
757 | (packed_sizes << 23),
758 )
759 }
760
761 pub fn from_code(code: u64) -> Option<Self> {
764 let field = |offset: u32, bits: u32| (code >> offset) & ((1 << bits) - 1);
765 let value = QuantValue::from_code(field(0, 4))?;
766 let axis = field(6, 6) as usize;
767 let store = match field(4, 2) {
768 0 => QuantStore::Native,
769 1 => QuantStore::PackedNative(axis),
770 2 => QuantStore::PackedU32(axis),
771 _ => return None,
772 };
773 let mode = QuantMode::from_code(field(12, 2))?;
774 let level = |bits: u64| match bits {
775 0 => Some(None),
776 bits => ScaleDtype::from_code(bits).map(Some),
777 };
778 let tensor = level(field(14, 3))?;
779 let rank = field(20, 3) as usize;
780 if rank > MAX_DIMS {
781 return None;
782 }
783 let mut sizes = [0; MAX_DIMS];
784 for (index, size) in sizes[..rank].iter_mut().enumerate() {
785 *size = field(23 + 8 * index as u32, 8) as u8;
786 }
787 let block = level(field(17, 3))?.map(|dtype| BlockScale {
788 size: BlockSize::new(&sizes[..rank]),
789 dtype,
790 });
791 Some(Self {
795 value,
796 store,
797 mode,
798 tensor,
799 block,
800 })
801 .filter(|scheme| scheme.to_code() == Some(code))
802 }
803}
804
805#[cfg(test)]
806mod tests {
807 use super::*;
808
809 #[test]
810 fn blocks_remain_rank_relative() {
811 assert_ne!(BlockSize::new([32]), BlockSize::new([32, 32]));
812 assert_eq!(BlockSize::new([32]).to_dim_vec(2), vec![1, 32]);
813 assert_eq!(BlockSize::new([32, 32]).to_dim_vec(2), vec![32, 32]);
814 }
815
816 #[test]
817 fn leading_unit_dimensions_canonicalize_away() {
818 assert_eq!(BlockSize::new([1, 32]), BlockSize::new([32]));
819 }
820
821 #[test]
822 fn leading_unit_dimensions_beyond_the_cap_still_canonicalize() {
823 assert_eq!(
824 BlockSize::new([1, 1, 8, 4, 2, 3]),
825 BlockSize::new([8, 4, 2, 3])
826 );
827 }
828
829 #[test]
830 fn there_is_one_block_per_scale() {
831 assert_eq!(BlockSize::new([32]).num_blocks(&[8, 64]), vec![8, 2]);
832 assert_eq!(BlockSize::new([4, 32]).num_blocks(&[8, 64]), vec![2, 2]);
833 assert_eq!(BlockSize::new([32]).num_blocks(&[4, 8, 64]), vec![4, 8, 2]);
834 }
835
836 #[test]
837 fn a_partial_block_still_takes_a_scale() {
838 assert_eq!(BlockSize::new([32]).num_blocks(&[8, 70]), vec![8, 3]);
839 }
840
841 #[test]
842 fn the_default_scheme_resolves_to_per_tensor_f32() {
843 let scheme = QuantScheme::default();
844 assert_eq!(scheme.tensor_scale(), Some(ScaleDtype::F32));
845 assert_eq!(scheme.block_scale(), None);
846 assert_eq!(scheme.scale_dtype(), ScaleDtype::F32);
847 assert_eq!(scheme.block_size(), None);
848 assert_eq!(scheme.num_levels(), 1);
849 }
850
851 #[test]
852 fn a_block_level_stands_alone() {
853 let scheme = QuantScheme::default().per_block([32], ScaleDtype::F16);
854 assert_eq!(scheme.tensor_scale(), None);
855 assert_eq!(scheme.scale_dtype(), ScaleDtype::F16);
856 assert_eq!(scheme.block_size(), Some(BlockSize::new([32])));
857 assert_eq!(scheme.num_levels(), 1);
858 }
859
860 #[test]
861 fn both_levels_nest_the_block_inside_the_tensor() {
862 let scheme = QuantScheme::default()
863 .per_block([16], ScaleDtype::UE4M3)
864 .per_tensor(ScaleDtype::F32);
865 assert_eq!(scheme.scale_dtype(), ScaleDtype::UE4M3);
866 assert_eq!(scheme.tensor_scale(), Some(ScaleDtype::F32));
867 assert_eq!(scheme.num_levels(), 2);
868 }
869
870 #[test]
871 fn levels_set_in_any_order_are_the_same_scheme() {
872 assert_eq!(
873 QuantScheme::default()
874 .per_block([16], ScaleDtype::UE4M3)
875 .per_tensor(ScaleDtype::F32),
876 QuantScheme::default()
877 .per_tensor(ScaleDtype::F32)
878 .per_block([16], ScaleDtype::UE4M3),
879 );
880 }
881
882 #[test]
883 fn swapping_dims_rewrites_the_block_and_leaves_the_tensor_level_alone() {
884 let mut scheme = QuantScheme::default()
885 .per_block([4, 32], ScaleDtype::F16)
886 .per_tensor(ScaleDtype::F32);
887 scheme.swap_block_dims(2, 0, 1);
888 assert_eq!(
889 scheme,
890 QuantScheme::default()
891 .per_block([32, 4], ScaleDtype::F16)
892 .per_tensor(ScaleDtype::F32)
893 );
894
895 let mut per_tensor = QuantScheme::default();
896 per_tensor.swap_block_dims(2, 0, 1);
897 assert_eq!(per_tensor, QuantScheme::default());
898 }
899
900 #[test]
901 fn swapping_dims_canonicalizes_the_block() {
902 let mut scheme = QuantScheme::default().per_block([32, 1], ScaleDtype::F32);
903 scheme.swap_block_dims(2, 0, 1);
904 assert_eq!(scheme.block_size(), Some(BlockSize::new([32])));
905 }
906
907 #[test]
908 fn permuting_dims_moves_the_packing_dim_and_the_block_with_their_axes() {
909 let mut scheme = QuantScheme::default()
910 .with_store(QuantStore::PackedU32(0))
911 .per_block([1, 2, 4], ScaleDtype::F32);
912 scheme.permute_dims(&[2, 0, 1]);
913 assert_eq!(scheme.store, QuantStore::PackedU32(2));
914 assert_eq!(scheme.block_size(), Some(BlockSize::new([4, 1, 2])));
915 }
916
917 #[test]
918 fn swapping_dims_moves_the_packing_dim_only_when_its_dim_is_swapped() {
919 let mut scheme = QuantScheme::default().with_store(QuantStore::PackedU32(0));
920 scheme.swap_dims(3, 0, 1);
921 assert_eq!(scheme.store, QuantStore::PackedU32(0));
922 scheme.swap_dims(3, 1, 2);
923 assert_eq!(scheme.store, QuantStore::PackedU32(1));
924 }
925
926 #[test]
927 fn a_scalar_keeps_its_packing_dim() {
928 let mut scheme = QuantScheme::default().with_store(QuantStore::PackedU32(0));
929 scheme.reshape_packing_dim(0);
930 scheme.permute_dims(&[]);
931 assert_eq!(scheme.store, QuantStore::PackedU32(0));
932 }
933
934 #[test]
935 fn a_reshape_that_drops_the_packing_dim_packs_innermost() {
936 let mut scheme = QuantScheme::default().with_store(QuantStore::PackedNative(1));
937 scheme.reshape_packing_dim(2);
938 assert_eq!(scheme.store, QuantStore::PackedNative(1));
939 scheme.reshape_packing_dim(1);
940 assert_eq!(scheme.store, QuantStore::PackedNative(0));
941 }
942
943 #[test]
944 fn permuting_dims_rewrites_the_block() {
945 let mut scheme = QuantScheme::default().per_block([1, 4, 32], ScaleDtype::F16);
946 scheme.permute_block_dims(3, &[2, 0, 1]);
947 assert_eq!(scheme.block_size(), Some(BlockSize::new([32, 1, 4])));
948 }
949
950 #[test]
951 fn round_up_never_lands_below_the_scale() {
952 for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
953 for exp in -12..8 {
954 for step in 1..17 {
955 let scale = (step as f32 / 16.0) * 2f32.powi(exp);
956 let up = dtype.round_up(scale);
957 assert!(
958 up >= scale,
959 "{dtype:?}: {up} is below {scale}, which clips the block maximum"
960 );
961 }
962 }
963 }
964 }
965
966 #[test]
967 fn round_up_saturates_rather_than_stepping_off_the_top() {
968 for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
969 let max = dtype.max_representable();
970 assert_eq!(dtype.round_up(max), max);
971 assert!(dtype.round_up(max * 2.0).is_finite());
972 }
973 }
974
975 #[test]
978 fn round_up_answers_for_every_param() {
979 for dtype in [
980 ScaleDtype::F32,
981 ScaleDtype::F16,
982 ScaleDtype::BF16,
983 ScaleDtype::UE8M0,
984 ScaleDtype::UE4M3,
985 ] {
986 let up = dtype.round_up(0.3);
987 assert!(up.is_finite() && up > 0.0, "{dtype:?} answered {up:e}");
988 }
989 }
990
991 fn power_of_two(exp: i32) -> f32 {
998 f32::from_bits(((exp + 127) as u32) << 23)
999 }
1000
1001 #[test]
1003 fn ue8m0_rounds_up_to_a_power_of_two() {
1004 for exp in -120..120 {
1005 let power = power_of_two(exp);
1006 assert_eq!(ScaleDtype::UE8M0.round_up(power), power, "2^{exp}");
1008 for scale in [power * 1.0001, power * 1.5, power * 1.9999] {
1010 assert_eq!(
1011 ScaleDtype::UE8M0.round_up(scale),
1012 power * 2.0,
1013 "{scale} (2^{exp} scaled)"
1014 );
1015 }
1016 }
1017 }
1018
1019 #[test]
1023 fn ue8m0_saturates_at_both_ends() {
1024 let min = ScaleDtype::UE8M0_MIN;
1025 let max = ScaleDtype::UE8M0_MAX;
1026
1027 for scale in [0.0, f32::MIN_POSITIVE * 0.5, min * 0.5, min] {
1028 assert_eq!(ScaleDtype::UE8M0.round_up(scale), min, "{scale:e}");
1029 }
1030 for scale in [max, max * 2.0, f32::MAX, f32::INFINITY] {
1031 assert_eq!(ScaleDtype::UE8M0.round_up(scale), max, "{scale:e}");
1032 }
1033 }
1034
1035 #[test]
1037 fn ue8m0_round_up_is_idempotent() {
1038 for exp in -130..130 {
1039 for factor in [1.0, 1.3, 1.7] {
1040 let scale = factor * 2f32.powi(exp);
1041 let up = ScaleDtype::UE8M0.round_up(scale);
1042 assert_eq!(
1043 ScaleDtype::UE8M0.round_up(up),
1044 up,
1045 "not idempotent at {scale:e}"
1046 );
1047 assert!(up >= scale.min(ScaleDtype::UE8M0_MAX), "{up:e} < {scale:e}");
1048 }
1049 }
1050 }
1051
1052 #[test]
1053 fn round_up_is_the_identity_for_f32() {
1054 for scale in [1.0e-30, 0.1, 1.0, 12345.678, f32::MAX] {
1055 assert_eq!(ScaleDtype::F32.round_up(scale), scale);
1056 }
1057 }
1058
1059 #[cfg(feature = "fp8")]
1061 mod storage_types {
1062 use super::*;
1063
1064 #[test]
1065 fn round_up_is_the_nearest_representable_value_not_below() {
1066 for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
1068 for exp in -8..6 {
1069 let scale = 1.7 * 2f32.powi(exp);
1070 let up = dtype.round_up(scale);
1071 assert_eq!(
1072 up,
1073 dtype.round_up(up),
1074 "{dtype:?}: not idempotent at {scale}"
1075 );
1076 assert!(
1077 step(dtype, up, -1) < scale,
1078 "{dtype:?}: {up} overshoots {scale} by at least a step"
1079 );
1080 }
1081 }
1082 }
1083
1084 #[test]
1089 fn f32_grid_matches_the_storage_types() {
1090 for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
1091 let grid = dtype.f32_grid();
1092
1093 if let Some(subnormals) = grid.subnormals {
1095 assert_eq!(
1096 subnormals.min_normal,
1097 min_normal(dtype),
1098 "{dtype:?}: minimum normal"
1099 );
1100 assert_eq!(
1101 subnormals.spacing,
1102 step(dtype, 0.0, 1),
1103 "{dtype:?}: subnormal spacing"
1104 );
1105 }
1106
1107 let mut value = min_normal(dtype);
1110 let max = dtype.max_representable();
1111 while value < max {
1112 let stepped = f32::from_bits(value.to_bits() + grid.bit_step);
1113 assert_eq!(
1114 stepped,
1115 step(dtype, value, 1),
1116 "{dtype:?}: step above {value}"
1117 );
1118 value = stepped;
1119 }
1120 assert_eq!(
1121 value, max,
1122 "{dtype:?}: the grid has to land exactly on the maximum"
1123 );
1124 }
1125 }
1126
1127 #[test]
1128 fn max_representable_matches_the_e4m3_type() {
1129 assert_eq!(
1130 ScaleDtype::UE4M3.max_representable(),
1131 crate::e4m3::MAX.to_f32()
1132 );
1133 }
1134
1135 fn step(dtype: ScaleDtype, value: f32, offset: i32) -> f32 {
1139 match dtype {
1140 ScaleDtype::F16 => half::f16::from_bits(
1141 (half::f16::from_f32(value).to_bits() as i32 + offset) as u16,
1142 )
1143 .to_f32(),
1144 ScaleDtype::BF16 => half::bf16::from_bits(
1145 (half::bf16::from_f32(value).to_bits() as i32 + offset) as u16,
1146 )
1147 .to_f32(),
1148 ScaleDtype::UE4M3 => crate::e4m3::from_bits(
1149 (crate::e4m3::from_f32(value).to_bits() as i32 + offset) as u8,
1150 )
1151 .to_f32(),
1152 ScaleDtype::F32 | ScaleDtype::UE8M0 => unreachable!(),
1153 }
1154 }
1155
1156 fn min_normal(dtype: ScaleDtype) -> f32 {
1157 match dtype {
1158 ScaleDtype::F16 => half::f16::MIN_POSITIVE.to_f32(),
1159 ScaleDtype::BF16 => half::bf16::MIN_POSITIVE.to_f32(),
1160 ScaleDtype::UE4M3 => crate::e4m3::MIN_POSITIVE.to_f32(),
1161 ScaleDtype::F32 | ScaleDtype::UE8M0 => unreachable!(),
1162 }
1163 }
1164 }
1165
1166 #[test]
1171 fn every_scheme_round_trips_through_its_code() {
1172 let values: Vec<_> = (0..1 << 4).filter_map(QuantValue::from_code).collect();
1173 let modes: Vec<_> = (0..1 << 2).filter_map(QuantMode::from_code).collect();
1174 let dtypes: Vec<_> = (0..1 << 3).filter_map(ScaleDtype::from_code).collect();
1175 let stores = [
1176 QuantStore::Native,
1177 QuantStore::PackedNative(0),
1178 QuantStore::PackedNative(63),
1179 QuantStore::PackedU32(0),
1180 QuantStore::PackedU32(63),
1181 ];
1182 let sizes: [&[u8]; 3] = [&[32], &[255, 1], &[2, 4, 8, 16, 255]];
1183 let mut codes = alloc::collections::BTreeSet::new();
1184 let mut count = 0;
1185 for value in values.iter().copied() {
1186 for store in stores {
1187 for mode in modes.iter().copied() {
1188 let base = QuantScheme::default()
1189 .with_value(value)
1190 .with_store(store)
1191 .with_mode(mode);
1192 let tensors = core::iter::once(base)
1193 .chain(dtypes.iter().map(|dtype| base.per_tensor(*dtype)));
1194 for scheme in tensors {
1195 let blocks =
1196 core::iter::once(scheme).chain(dtypes.iter().flat_map(|dtype| {
1197 sizes
1198 .iter()
1199 .map(move |size| scheme.per_block(*size, *dtype))
1200 }));
1201 for scheme in blocks {
1202 let code = scheme.to_code().unwrap();
1203 assert_eq!(QuantScheme::from_code(code), Some(scheme), "{scheme:?}");
1204 codes.insert(code);
1205 count += 1;
1206 }
1207 }
1208 }
1209 }
1210 }
1211 assert_eq!(codes.len(), count, "two schemes share a code");
1212 }
1213
1214 #[test]
1217 fn a_code_no_scheme_packs_to_is_refused() {
1218 let canonical = QuantScheme::default()
1219 .with_store(QuantStore::PackedU32(1))
1220 .per_block([32], ScaleDtype::F16)
1221 .to_code()
1222 .unwrap();
1223 let native = QuantScheme::default()
1224 .with_store(QuantStore::Native)
1225 .to_code()
1226 .unwrap();
1227 let refused = [
1228 ("value 9", (canonical & !0xF) | 9),
1229 ("value 15", (canonical & !0xF) | 15),
1230 ("store 3", canonical | (3 << 4)),
1231 ("mode 2", canonical | (2 << 12)),
1232 ("per-tensor level 6", canonical | (6 << 14)),
1233 ("block level 7", canonical | (7 << 17)),
1234 ("rank 6", (canonical & !(7 << 20)) | (6 << 20)),
1235 ("an axis on a native store", native | (5 << 6)),
1236 ("a rank with no block level", native | (1 << 20)),
1237 ("a size past the rank", canonical | (8 << 31)),
1238 ("a block size of zero", canonical & !(0xFF << 23)),
1239 ("bit 63", canonical | (1 << 63)),
1240 ];
1241 for (case, code) in refused {
1242 assert_eq!(QuantScheme::from_code(code), None, "{case}");
1243 }
1244 }
1245
1246 #[test]
1250 fn a_scheme_the_layout_cannot_hold_has_no_code() {
1251 let unheld = [
1252 (
1253 "a packing axis of 64",
1254 QuantScheme::default().with_store(QuantStore::PackedU32(64)),
1255 ),
1256 (
1257 "a block size of zero",
1258 QuantScheme::default().per_block([0, 16], ScaleDtype::F16),
1259 ),
1260 ];
1261 for (case, scheme) in unheld {
1262 assert_eq!(scheme.to_code(), None, "{case}");
1263 }
1264 }
1265}