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
668#[cfg(test)]
669mod tests {
670 use super::*;
671
672 #[test]
673 fn blocks_remain_rank_relative() {
674 assert_ne!(BlockSize::new([32]), BlockSize::new([32, 32]));
675 assert_eq!(BlockSize::new([32]).to_dim_vec(2), vec![1, 32]);
676 assert_eq!(BlockSize::new([32, 32]).to_dim_vec(2), vec![32, 32]);
677 }
678
679 #[test]
680 fn leading_unit_dimensions_canonicalize_away() {
681 assert_eq!(BlockSize::new([1, 32]), BlockSize::new([32]));
682 }
683
684 #[test]
685 fn leading_unit_dimensions_beyond_the_cap_still_canonicalize() {
686 assert_eq!(
687 BlockSize::new([1, 1, 8, 4, 2, 3]),
688 BlockSize::new([8, 4, 2, 3])
689 );
690 }
691
692 #[test]
693 fn there_is_one_block_per_scale() {
694 assert_eq!(BlockSize::new([32]).num_blocks(&[8, 64]), vec![8, 2]);
695 assert_eq!(BlockSize::new([4, 32]).num_blocks(&[8, 64]), vec![2, 2]);
696 assert_eq!(BlockSize::new([32]).num_blocks(&[4, 8, 64]), vec![4, 8, 2]);
697 }
698
699 #[test]
700 fn a_partial_block_still_takes_a_scale() {
701 assert_eq!(BlockSize::new([32]).num_blocks(&[8, 70]), vec![8, 3]);
702 }
703
704 #[test]
705 fn the_default_scheme_resolves_to_per_tensor_f32() {
706 let scheme = QuantScheme::default();
707 assert_eq!(scheme.tensor_scale(), Some(ScaleDtype::F32));
708 assert_eq!(scheme.block_scale(), None);
709 assert_eq!(scheme.scale_dtype(), ScaleDtype::F32);
710 assert_eq!(scheme.block_size(), None);
711 assert_eq!(scheme.num_levels(), 1);
712 }
713
714 #[test]
715 fn a_block_level_stands_alone() {
716 let scheme = QuantScheme::default().per_block([32], ScaleDtype::F16);
717 assert_eq!(scheme.tensor_scale(), None);
718 assert_eq!(scheme.scale_dtype(), ScaleDtype::F16);
719 assert_eq!(scheme.block_size(), Some(BlockSize::new([32])));
720 assert_eq!(scheme.num_levels(), 1);
721 }
722
723 #[test]
724 fn both_levels_nest_the_block_inside_the_tensor() {
725 let scheme = QuantScheme::default()
726 .per_block([16], ScaleDtype::UE4M3)
727 .per_tensor(ScaleDtype::F32);
728 assert_eq!(scheme.scale_dtype(), ScaleDtype::UE4M3);
729 assert_eq!(scheme.tensor_scale(), Some(ScaleDtype::F32));
730 assert_eq!(scheme.num_levels(), 2);
731 }
732
733 #[test]
734 fn levels_set_in_any_order_are_the_same_scheme() {
735 assert_eq!(
736 QuantScheme::default()
737 .per_block([16], ScaleDtype::UE4M3)
738 .per_tensor(ScaleDtype::F32),
739 QuantScheme::default()
740 .per_tensor(ScaleDtype::F32)
741 .per_block([16], ScaleDtype::UE4M3),
742 );
743 }
744
745 #[test]
746 fn swapping_dims_rewrites_the_block_and_leaves_the_tensor_level_alone() {
747 let mut scheme = QuantScheme::default()
748 .per_block([4, 32], ScaleDtype::F16)
749 .per_tensor(ScaleDtype::F32);
750 scheme.swap_block_dims(2, 0, 1);
751 assert_eq!(
752 scheme,
753 QuantScheme::default()
754 .per_block([32, 4], ScaleDtype::F16)
755 .per_tensor(ScaleDtype::F32)
756 );
757
758 let mut per_tensor = QuantScheme::default();
759 per_tensor.swap_block_dims(2, 0, 1);
760 assert_eq!(per_tensor, QuantScheme::default());
761 }
762
763 #[test]
764 fn swapping_dims_canonicalizes_the_block() {
765 let mut scheme = QuantScheme::default().per_block([32, 1], ScaleDtype::F32);
766 scheme.swap_block_dims(2, 0, 1);
767 assert_eq!(scheme.block_size(), Some(BlockSize::new([32])));
768 }
769
770 #[test]
771 fn permuting_dims_moves_the_packing_dim_and_the_block_with_their_axes() {
772 let mut scheme = QuantScheme::default()
773 .with_store(QuantStore::PackedU32(0))
774 .per_block([1, 2, 4], ScaleDtype::F32);
775 scheme.permute_dims(&[2, 0, 1]);
776 assert_eq!(scheme.store, QuantStore::PackedU32(2));
777 assert_eq!(scheme.block_size(), Some(BlockSize::new([4, 1, 2])));
778 }
779
780 #[test]
781 fn swapping_dims_moves_the_packing_dim_only_when_its_dim_is_swapped() {
782 let mut scheme = QuantScheme::default().with_store(QuantStore::PackedU32(0));
783 scheme.swap_dims(3, 0, 1);
784 assert_eq!(scheme.store, QuantStore::PackedU32(0));
785 scheme.swap_dims(3, 1, 2);
786 assert_eq!(scheme.store, QuantStore::PackedU32(1));
787 }
788
789 #[test]
790 fn a_scalar_keeps_its_packing_dim() {
791 let mut scheme = QuantScheme::default().with_store(QuantStore::PackedU32(0));
792 scheme.reshape_packing_dim(0);
793 scheme.permute_dims(&[]);
794 assert_eq!(scheme.store, QuantStore::PackedU32(0));
795 }
796
797 #[test]
798 fn a_reshape_that_drops_the_packing_dim_packs_innermost() {
799 let mut scheme = QuantScheme::default().with_store(QuantStore::PackedNative(1));
800 scheme.reshape_packing_dim(2);
801 assert_eq!(scheme.store, QuantStore::PackedNative(1));
802 scheme.reshape_packing_dim(1);
803 assert_eq!(scheme.store, QuantStore::PackedNative(0));
804 }
805
806 #[test]
807 fn permuting_dims_rewrites_the_block() {
808 let mut scheme = QuantScheme::default().per_block([1, 4, 32], ScaleDtype::F16);
809 scheme.permute_block_dims(3, &[2, 0, 1]);
810 assert_eq!(scheme.block_size(), Some(BlockSize::new([32, 1, 4])));
811 }
812
813 #[test]
814 fn round_up_never_lands_below_the_scale() {
815 for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
816 for exp in -12..8 {
817 for step in 1..17 {
818 let scale = (step as f32 / 16.0) * 2f32.powi(exp);
819 let up = dtype.round_up(scale);
820 assert!(
821 up >= scale,
822 "{dtype:?}: {up} is below {scale}, which clips the block maximum"
823 );
824 }
825 }
826 }
827 }
828
829 #[test]
830 fn round_up_saturates_rather_than_stepping_off_the_top() {
831 for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
832 let max = dtype.max_representable();
833 assert_eq!(dtype.round_up(max), max);
834 assert!(dtype.round_up(max * 2.0).is_finite());
835 }
836 }
837
838 #[test]
841 fn round_up_answers_for_every_param() {
842 for dtype in [
843 ScaleDtype::F32,
844 ScaleDtype::F16,
845 ScaleDtype::BF16,
846 ScaleDtype::UE8M0,
847 ScaleDtype::UE4M3,
848 ] {
849 let up = dtype.round_up(0.3);
850 assert!(up.is_finite() && up > 0.0, "{dtype:?} answered {up:e}");
851 }
852 }
853
854 fn power_of_two(exp: i32) -> f32 {
861 f32::from_bits(((exp + 127) as u32) << 23)
862 }
863
864 #[test]
866 fn ue8m0_rounds_up_to_a_power_of_two() {
867 for exp in -120..120 {
868 let power = power_of_two(exp);
869 assert_eq!(ScaleDtype::UE8M0.round_up(power), power, "2^{exp}");
871 for scale in [power * 1.0001, power * 1.5, power * 1.9999] {
873 assert_eq!(
874 ScaleDtype::UE8M0.round_up(scale),
875 power * 2.0,
876 "{scale} (2^{exp} scaled)"
877 );
878 }
879 }
880 }
881
882 #[test]
886 fn ue8m0_saturates_at_both_ends() {
887 let min = ScaleDtype::UE8M0_MIN;
888 let max = ScaleDtype::UE8M0_MAX;
889
890 for scale in [0.0, f32::MIN_POSITIVE * 0.5, min * 0.5, min] {
891 assert_eq!(ScaleDtype::UE8M0.round_up(scale), min, "{scale:e}");
892 }
893 for scale in [max, max * 2.0, f32::MAX, f32::INFINITY] {
894 assert_eq!(ScaleDtype::UE8M0.round_up(scale), max, "{scale:e}");
895 }
896 }
897
898 #[test]
900 fn ue8m0_round_up_is_idempotent() {
901 for exp in -130..130 {
902 for factor in [1.0, 1.3, 1.7] {
903 let scale = factor * 2f32.powi(exp);
904 let up = ScaleDtype::UE8M0.round_up(scale);
905 assert_eq!(
906 ScaleDtype::UE8M0.round_up(up),
907 up,
908 "not idempotent at {scale:e}"
909 );
910 assert!(up >= scale.min(ScaleDtype::UE8M0_MAX), "{up:e} < {scale:e}");
911 }
912 }
913 }
914
915 #[test]
916 fn round_up_is_the_identity_for_f32() {
917 for scale in [1.0e-30, 0.1, 1.0, 12345.678, f32::MAX] {
918 assert_eq!(ScaleDtype::F32.round_up(scale), scale);
919 }
920 }
921
922 #[cfg(feature = "fp8")]
924 mod storage_types {
925 use super::*;
926
927 #[test]
928 fn round_up_is_the_nearest_representable_value_not_below() {
929 for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
931 for exp in -8..6 {
932 let scale = 1.7 * 2f32.powi(exp);
933 let up = dtype.round_up(scale);
934 assert_eq!(
935 up,
936 dtype.round_up(up),
937 "{dtype:?}: not idempotent at {scale}"
938 );
939 assert!(
940 step(dtype, up, -1) < scale,
941 "{dtype:?}: {up} overshoots {scale} by at least a step"
942 );
943 }
944 }
945 }
946
947 #[test]
952 fn f32_grid_matches_the_storage_types() {
953 for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
954 let grid = dtype.f32_grid();
955
956 if let Some(subnormals) = grid.subnormals {
958 assert_eq!(
959 subnormals.min_normal,
960 min_normal(dtype),
961 "{dtype:?}: minimum normal"
962 );
963 assert_eq!(
964 subnormals.spacing,
965 step(dtype, 0.0, 1),
966 "{dtype:?}: subnormal spacing"
967 );
968 }
969
970 let mut value = min_normal(dtype);
973 let max = dtype.max_representable();
974 while value < max {
975 let stepped = f32::from_bits(value.to_bits() + grid.bit_step);
976 assert_eq!(
977 stepped,
978 step(dtype, value, 1),
979 "{dtype:?}: step above {value}"
980 );
981 value = stepped;
982 }
983 assert_eq!(
984 value, max,
985 "{dtype:?}: the grid has to land exactly on the maximum"
986 );
987 }
988 }
989
990 #[test]
991 fn max_representable_matches_the_e4m3_type() {
992 assert_eq!(
993 ScaleDtype::UE4M3.max_representable(),
994 crate::e4m3::MAX.to_f32()
995 );
996 }
997
998 fn step(dtype: ScaleDtype, value: f32, offset: i32) -> f32 {
1002 match dtype {
1003 ScaleDtype::F16 => half::f16::from_bits(
1004 (half::f16::from_f32(value).to_bits() as i32 + offset) as u16,
1005 )
1006 .to_f32(),
1007 ScaleDtype::BF16 => half::bf16::from_bits(
1008 (half::bf16::from_f32(value).to_bits() as i32 + offset) as u16,
1009 )
1010 .to_f32(),
1011 ScaleDtype::UE4M3 => crate::e4m3::from_bits(
1012 (crate::e4m3::from_f32(value).to_bits() as i32 + offset) as u8,
1013 )
1014 .to_f32(),
1015 ScaleDtype::F32 | ScaleDtype::UE8M0 => unreachable!(),
1016 }
1017 }
1018
1019 fn min_normal(dtype: ScaleDtype) -> f32 {
1020 match dtype {
1021 ScaleDtype::F16 => half::f16::MIN_POSITIVE.to_f32(),
1022 ScaleDtype::BF16 => half::bf16::MIN_POSITIVE.to_f32(),
1023 ScaleDtype::UE4M3 => crate::e4m3::MIN_POSITIVE.to_f32(),
1024 ScaleDtype::F32 | ScaleDtype::UE8M0 => unreachable!(),
1025 }
1026 }
1027 }
1028}