Skip to main content

cubecl_common/quant/
scheme.rs

1use alloc::vec;
2use alloc::vec::Vec;
3use core::{default::Default, ops::Deref};
4use serde::{Deserialize, Serialize};
5
6use crate::{e2m1, ue8m0};
7
8/// Describes a quantization scheme/configuration.
9///
10/// Scales come at up to two levels, each an optional field set through
11/// [`per_tensor`](Self::per_tensor) and [`per_block`](Self::per_block) in any order:
12///
13/// ```
14/// # use cubecl_common::quant::scheme::{QuantScheme, ScaleDtype};
15/// // One scale for the whole tensor, stored as f32. Also what a scheme with no level resolves to.
16/// QuantScheme::default().per_tensor(ScaleDtype::F32);
17///
18/// // One scale per block of 32 values.
19/// QuantScheme::default().per_block([32], ScaleDtype::F32);
20///
21/// // Two levels: ue4m3 block scales, normalized by a single per-tensor f32 scale.
22/// QuantScheme::default()
23///     .per_block([16], ScaleDtype::UE4M3)
24///     .per_tensor(ScaleDtype::F32);
25/// ```
26///
27/// A two-level scheme exists so block scales can live in a narrow type: the global per-tensor scale
28/// absorbs the tensor's dynamic range, and the block dtype only covers the spread between blocks.
29/// That spread is still bounded: a block whose scale falls further below the largest one than the
30/// block dtype can express is stored at that dtype's smallest value, far too coarse for it, and
31/// every value in the block quantizes to zero. [`ScaleDtype::UE4M3`] spans about 2^18 this way, so
32/// a tensor holding a genuine outlier can lose its ordinary values.
33#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
34pub struct QuantScheme {
35    /// The logical data type of quantized input values (e.g., [`QuantValue::Q8F`]).
36    ///
37    /// This defines how values are interpreted during computation, independent of how they're stored.
38    pub value: QuantValue,
39    /// Data type used for storing quantized values.
40    pub store: QuantStore,
41    /// Quantization mode (e.g., symmetric).
42    pub mode: QuantMode,
43    /// The per-tensor scale level. Private with [`tensor_scale`](Self::tensor_scale) as the
44    /// reader, which resolves a scheme storing no level at all to a per-tensor f32 scale.
45    tensor: Option<ScaleDtype>,
46    /// The per-block scale level, the innermost when both levels are present.
47    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    /// Set the quantization mode.
64    pub fn with_mode(mut self, mode: QuantMode) -> Self {
65        self.mode = mode;
66        self
67    }
68
69    /// Set the data type used for quantized values.
70    pub fn with_value(mut self, value: QuantValue) -> Self {
71        self.value = value;
72        self
73    }
74
75    /// Set the data type used to store quantized values.
76    pub fn with_store(mut self, store: QuantStore) -> Self {
77        self.store = store;
78        self
79    }
80
81    /// Set the per-tensor scale level, stored as `dtype`.
82    pub fn per_tensor(mut self, dtype: ScaleDtype) -> Self {
83        self.tensor = Some(dtype);
84        self
85    }
86
87    /// Set the per-block scale level: one scale per block of `block` values, stored as `dtype`.
88    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    /// The per-tensor scale level, the global level when a block level is present.
97    ///
98    /// A scheme storing no level at all resolves here to a per-tensor f32 scale; the resolution
99    /// is not stored, so such a scheme compares equal to [`Default`], not to an explicit
100    /// `per_tensor(F32)`.
101    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    /// The per-block scale level, the innermost when both levels are present.
109    pub fn block_scale(&self) -> Option<BlockScale> {
110        self.block
111    }
112
113    /// The number of scale levels: as many scale tensors ride along with the values.
114    pub fn num_levels(&self) -> usize {
115        self.block_scale().is_some() as usize + self.tensor_scale().is_some() as usize
116    }
117
118    /// The innermost level's scale dtype, the type the per-position scales are stored in.
119    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    /// The block level's size, or [`None`] for per-tensor quantization.
127    pub fn block_size(&self) -> Option<BlockSize> {
128        self.block.map(|block| block.size)
129    }
130
131    /// Swap two tensor dimensions in the block level, mirroring `shape.swap(dim0, dim1)`. The
132    /// per-tensor level is unaffected.
133    ///
134    /// `dim0`/`dim1` are bare indices on purpose, mirroring `[T]::swap`'s own signature.
135    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    /// Permute the block level, mirroring a permutation of the tensor's axes. The per-tensor
142    /// level is unaffected.
143    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    /// Returns the size of the quantization storage type in bits.
152    pub fn size_bits_stored(&self) -> usize {
153        self.store.size_bits(&self.value)
154    }
155
156    /// Returns the size of the quantization storage type in bits.
157    pub fn size_bits_value(&self) -> usize {
158        self.value.size_bits()
159    }
160
161    /// Returns the number of quantized values stored in a single element.
162    pub fn num_quants(&self) -> usize {
163        self.size_bits_stored() / self.value.size_bits()
164    }
165
166    /// Returns the native packing factor for the values. When native packing > 1, the packed
167    /// representation stores `num_quants` elements grouped into packs of `native_packing` size.
168    pub fn native_packing(&self) -> usize {
169        self.value.native_packing()
170    }
171
172    /// Returns the packing dim for the store.
173    pub fn packing_dim(&self) -> Option<usize> {
174        self.store.packing_dim()
175    }
176
177    /// Swap two tensor dimensions in the packing dim, mirroring `shape.swap(dim0, dim1)` on a
178    /// tensor of `rank` dimensions: the packing dim counts from the innermost dimension, so it
179    /// takes the rank to know which dimension it is.
180    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    /// Permute the packing dim, mirroring a permutation of the tensor's axes.
187    fn permute_packing_dim(&mut self, axes: &[usize]) {
188        // A scalar keeps its packing dim, as `reshape_packing_dim` leaves it.
189        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    /// Swap two tensor dimensions in the block level and the packing dim, mirroring
206    /// `shape.swap(dim0, dim1)`.
207    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    /// Permute the block level and the packing dim, mirroring a permutation of the tensor's axes.
213    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    /// Update the packing dim for a reshape to `rank` dimensions: a packing dim the reshape drops
219    /// becomes the innermost one. Only the index changes, so a backend holding codes packed in
220    /// words repacks them itself.
221    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/// The per-block scale level of a [`QuantScheme`]: one scale per block of values.
232#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
233pub struct BlockScale {
234    /// The block of values sharing one scale.
235    pub size: BlockSize,
236    /// The dtype the level's scales are stored in.
237    pub dtype: ScaleDtype,
238}
239
240impl ScaleDtype {
241    /// The largest finite value representable by the dtype.
242    ///
243    /// A two-level scheme picks its per-tensor scale so that the largest block scale lands here,
244    /// which is what keeps the block scales inside the range their type can express. That recipe
245    /// only holds for a block dtype narrower than the scale it divides: dividing by
246    /// [`ScaleDtype::F32`]'s or [`ScaleDtype::UE8M0`]'s maximum drives the per-tensor scale
247    /// subnormal and the renormalized block scales to infinity. A two-level scheme has nothing to
248    /// gain from those params anyway, since their block scales already reach the full range.
249    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            // Spelled out because `e4m3` sits behind the `fp8` feature and this function is not
256            // gated. The tests check it against that type when it is on.
257            ScaleDtype::UE4M3 => 448.0,
258        }
259    }
260
261    /// The smallest value representable by the dtype that is not below `scale`.
262    ///
263    /// Storing a quantization scale wants this rather than the nearest value. Rounding down puts
264    /// the scale below what calibration asked for, so every value at the block maximum clips to
265    /// the quantization range; rounding up costs one step of coarseness instead. Backends have to
266    /// agree on this, or a tensor quantized on one reconstructs differently on another.
267    ///
268    /// This is not a cast: conversion to these types rounds to nearest, which is what a cast
269    /// should do, and this is the storage policy for a scale. [`ScaleDtype::UE8M0`] is the
270    /// exception, rounded by [`ue8m0`]'s own conversion, which rounds up because that format
271    /// exists to store scales.
272    ///
273    /// `scale` must not be negative. Symmetric quantization only produces non-negative scales,
274    /// and the stepping below walks away from zero for a negative input.
275    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        // Nothing representable sits above the maximum, and converting past it yields an infinity
292        // for the params that have one, which would make every reconstructed value NaN.
293        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            // Below the minimum normal the spacing stops halving, so the answer is a count of steps.
304            // Qualified call: the inherent `f32::ceil` lives in std, and this crate builds no_std.
305            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    /// The dtype's grid, expressed on the f32 bit pattern. See [`F32Grid`].
312    ///
313    /// bf16 reports no subnormal range because it does not need the separate treatment: its pattern
314    /// is f32's top half all the way down, so the bit step stays right where the others stop. Its
315    /// own subnormals start at 2^-133, which is subnormal in f32 too and flushed to zero by most
316    /// backends.
317    ///
318    /// # Panics
319    ///
320    /// For [`ScaleDtype::F32`], which is the grid itself.
321    pub fn f32_grid(&self) -> F32Grid {
322        /// One f32 ulp per dtype ulp: the mantissa bits f32 carries and the dtype does not.
323        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            // Spelled out rather than read off `e4m3`, which sits behind the `fp8` feature while
340            // this is not gated. The tests check them against that type when it is on.
341            ScaleDtype::UE4M3 => F32Grid {
342                bit_step: bit_step(4),
343                subnormals: Some(SubnormalRange {
344                    min_normal: 0.015625, // 2^-6
345                    spacing: 0.001953125, // 2^-9
346                }),
347            },
348            ScaleDtype::F32 => {
349                unimplemented!("F32 is the grid, it has no narrower one to round onto")
350            }
351            // No mantissa at all: the grid is the powers of two, so the step clears every f32
352            // mantissa bit. `subnormals` stays `None` because ue8m0 has no subnormal *ladder* —
353            // its bottom is the single value 2^-127, which the callers clamp to.
354            ScaleDtype::UE8M0 => F32Grid {
355                bit_step: bit_step(1),
356                subnormals: None,
357            },
358        }
359    }
360
361    /// The smallest and largest values [`ScaleDtype::UE8M0`] represents: 2^-127 and 2^127.
362    pub const UE8M0_MIN: f32 = ue8m0::MIN.to_f32();
363    /// See [`ScaleDtype::UE8M0_MIN`].
364    pub const UE8M0_MAX: f32 = ue8m0::MAX.to_f32();
365}
366
367/// A narrower float format's grid, laid over the f32 bit pattern.
368///
369/// f32 carries every dtype this exists for exactly, so the grid can be walked there rather than
370/// through the storage type. A value representable in the dtype leaves the low f32 mantissa bits
371/// zero, so one dtype ulp is an increment at that position and the carry into the exponent falls
372/// out on its own. Working in f32 also keeps the grid available to backends with no narrow integer,
373/// and to builds without the `fp8` feature.
374#[derive(Clone, Copy, Debug, PartialEq)]
375pub struct F32Grid {
376    /// One step up in the normal range, as an increment on the f32 bit pattern.
377    pub bit_step: u32,
378    /// The dtype's subnormals, for the formats whose subnormals land in f32's normal range.
379    pub subnormals: Option<SubnormalRange>,
380}
381
382/// Where a format's subnormals begin and how far apart they are, in f32.
383#[derive(Clone, Copy, Debug, PartialEq)]
384pub struct SubnormalRange {
385    /// The smallest normal value, below which the spacing stops halving.
386    pub min_normal: f32,
387    /// The constant distance between neighbouring subnormals.
388    pub spacing: f32,
389}
390
391impl F32Grid {
392    /// Clears the mantissa bits the dtype does not carry, truncating a bit pattern onto the grid.
393    pub fn truncate_mask(&self) -> u32 {
394        !(self.bit_step - 1)
395    }
396
397    /// Added to a bit pattern before [`truncate_mask`](Self::truncate_mask) to turn that truncation
398    /// into a round up. The carry it can produce is only safe below the dtype's maximum, which is
399    /// why callers saturate there first.
400    pub fn round_up_bias(&self) -> u32 {
401        self.bit_step - 1
402    }
403}
404
405/// Data type used to represent quantized values.
406#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
407pub enum QuantValue {
408    /// 8-bit quantization with full range.
409    Q8F,
410    /// 8-bit floating point, e5m2 format.
411    E5M2,
412    /// 8-bit floating point, e4m3 format.
413    E4M3,
414    /// 4-bit quantization with full range.
415    Q4F,
416    /// 4-bit floating point, e2m1 format.
417    E2M1,
418    /// 2-bit quantization with full range.
419    Q2F,
420    /// 8-bit quantization with symmetric range.
421    Q8S,
422    /// 4-bit quantization with symmetric range.
423    Q4S,
424    /// 2-bit quantization with symmetric range.
425    Q2S,
426}
427
428impl QuantValue {
429    /// Returns the size of the quantization input type in bits.
430    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    /// Packing factor for the native representation used for intermediate values. If > 1, values
439    /// should always be processed in `native_packing` sized chunks.
440    pub fn native_packing(&self) -> usize {
441        match self {
442            QuantValue::E2M1 => 2,
443            _ => 1,
444        }
445    }
446
447    /// The possible range of values allowed by the quant value.
448    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    /// If the values are minifloat codes, stored as sign and magnitude, rather than integers.
463    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    /// If the range of values is symmetric around zero.
471    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    /// Returns the size of the quantization input type in bits.
481    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/// Data type used to stored quantized values.
500#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
501pub enum QuantStore {
502    /// Native quantization doesn't require packing and unpacking.
503    Native,
504    /// Store packed quantized values in a natively supported packing format (i.e. e2m1x2).
505    /// Argument is the dimension the tensor is packed on, starting from the innermost dimension.
506    PackedNative(usize),
507    /// Store packed quantized values in a 4-byte unsigned integer.
508    /// Argument is the dimension the tensor is packed on, starting from the innermost dimension.
509    PackedU32(usize),
510    // /// Store packed quantized values in a 8-bit unsigned integer.
511    // U8,
512}
513
514/// Strategy used to quantize values.
515#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
516pub enum QuantMode {
517    /// Symmetric or scale quantization.
518    Symmetric,
519    /// The stored field is an index into a lookup table of `2^bits` floats, not a number: a read
520    /// reconstructs `table[field] * scale`. (Known as a codebook in the quantization literature —
521    /// NF4, K-quants, and vector quantizers all decode this way.) The table travels as its own
522    /// binding beside the values and scales; only the field's bit width is read from
523    /// [`QuantScheme::value`], since an index has no sign or float semantics of its own.
524    Lookup,
525}
526
527/// The data type a scale level stores its scales in.
528#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
529pub enum ScaleDtype {
530    /// Full precision.
531    F32,
532    /// Half precision.
533    F16,
534    /// bfloat16 precision.
535    BF16,
536    /// unsigned floating point, e8m0 format.
537    UE8M0,
538    /// unsigned floating point, e4m3 format.
539    UE4M3,
540}
541
542const MAX_DIMS: usize = 5;
543
544/// Copyable block size, specialized version of `SmallVec`.
545#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
546pub struct BlockSize {
547    storage: [u8; MAX_DIMS],
548    len: u8,
549}
550
551/// Hand-written: `storage` precedes `len`, so a derived `Ord` would compare filler bytes before
552/// length.
553impl 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    /// Max number of dimensions for block size
573    pub const MAX_DIMS: usize = MAX_DIMS;
574
575    /// Create a new blocksize from a set of values. The number of values must be `<= MAX_DIMS`.
576    ///
577    /// The result is canonical, so equal rank-relative blocks compare and hash equal however they
578    /// are spelled: leading unit dimensions are dropped, since the missing-dimension fill restates
579    /// them. In particular, `[1, 32]` canonicalizes to `[32]`. Whole-tensor granularity is a
580    /// scheme's per-tensor level, not a block size.
581    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    /// Return a slice of only the initialized values
605    pub fn as_slice(&self) -> &[u8] {
606        &self.storage[..self.len as usize]
607    }
608
609    /// Return a vec of only the initialized values
610    pub fn to_vec(&self) -> Vec<u8> {
611        self.storage[..self.len as usize].to_vec()
612    }
613
614    /// Returns `N` dimensions, unsqueezing if necessary. Missing leading dimensions fill with `1`.
615    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    /// Returns a vector of `len` dimensions, unsqueezing if necessary. Missing leading dimensions
624    /// fill with `1`.
625    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    /// How many blocks cover each dimension of `shape`, which is the shape of the scale grid:
634    /// one scale per block.
635    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    /// Create an iterator over all stored dimensions
644    pub fn iter(&self) -> impl Iterator<Item = &u8> {
645        self.as_slice().iter()
646    }
647
648    /// Returns the total number of elements in each block.
649    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    /// Every variant is dispatched somewhere, so none of them may panic here, and every answer is
839    /// a scale something can be divided by.
840    #[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    /// 2^`exp`, from the exponent field rather than through `powi`.
855    ///
856    /// `powi` is only ever approximate — Miri returns a value a few ulp off, deliberately, so
857    /// nothing comes to depend on one host's precision — and a power of two that is a hair off is
858    /// a different power of two once its mantissa is cleared. Only for exponents f32 has a normal
859    /// for: -126..=127.
860    fn power_of_two(exp: i32) -> f32 {
861        f32::from_bits(((exp + 127) as u32) << 23)
862    }
863
864    /// `ue8m0` stores a bare exponent, so rounding up to it is rounding up to a power of two.
865    #[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            // Already a power of two: nothing to round.
870            assert_eq!(ScaleDtype::UE8M0.round_up(power), power, "2^{exp}");
871            // Anything above it goes to the next one up, however little above.
872            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    /// Both ends saturate. The bottom is the reason `ue8m0` needs its own path at all: 2^-127 is
883    /// subnormal in f32, and zero — which a fully-zero block calibrates to — is not a `ue8m0`
884    /// value in the first place.
885    #[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    /// The answer is always representable, so rounding it again changes nothing.
899    #[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    /// The checks that need the real storage types to compare against.
923    #[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            // Rounding up must not overshoot: stepping down from the answer has to land below.
930            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        /// `round_up` reads the grid instead of converting through the storage type, so a wrong
948        /// constant there is only visible against the type itself. Nothing else in this file would
949        /// catch one: a grid finer than the real thing still lands above the scale, still steps
950        /// down below it, and still looks idempotent.
951        #[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                // bf16 deliberately reports no subnormal range, since its bit step covers them too.
957                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                // Walk the whole normal range: one step on the f32 pattern has to be one step in
971                // the type, at every exponent.
972                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        /// `offset` representable steps from `value` in `dtype`, for positive values. Counted on
999        /// the storage type's own bit pattern, so this is an oracle independent of the grid under
1000        /// test.
1001        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}