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/// A fieldless enum's `to_code` and `from_code` from one table, so the two cannot disagree and a
669/// variant added to the enum fails the build until it has a code.
670macro_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
706// `0` is no code: a level's bits read `0` when the scheme has no such level.
707codes!(ScaleDtype {
708    F32 => 1,
709    F16 => 2,
710    BF16 => 3,
711    UE8M0 => 4,
712    UE4M3 => 5,
713});
714
715/// A [`QuantScheme`] packed into one `u64`, for an IR that carries a scheme as a scalar: a remote
716/// backend's custom operation crosses the wire as tensors and scalars only.
717///
718/// The layout, from the low bits: the value (4), the store's kind (2) and packing axis (6), the
719/// mode (2), the per-tensor level (3), the block level's dtype (3), its rank (3) and its sizes
720/// (8 each, up to [`BlockSize::MAX_DIMS`]). A level's 3 bits are `0` for none, else its dtype's
721/// code.
722impl QuantScheme {
723    /// This scheme as one `u64`; [`from_code`](Self::from_code) reads it back. `None` for a scheme
724    /// no code holds: a packing axis past 63, or a block dimension of zero, which covers no
725    /// element.
726    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    /// The scheme [`to_code`](Self::to_code) packed into `code`; `None` for a code no scheme packs
762    /// to, so decoding is exactly the inverse of packing.
763    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        // Read fields alone accept codes no scheme packs to: bits a field of this scheme does
792        // not use, a size past the rank, a block size `BlockSize::new` trims, a zero size.
793        // Packing the result again keeps the one code each scheme has.
794        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    /// Every variant is dispatched somewhere, so none of them may panic here, and every answer is
976    /// a scale something can be divided by.
977    #[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    /// 2^`exp`, from the exponent field rather than through `powi`.
992    ///
993    /// `powi` is only ever approximate — Miri returns a value a few ulp off, deliberately, so
994    /// nothing comes to depend on one host's precision — and a power of two that is a hair off is
995    /// a different power of two once its mantissa is cleared. Only for exponents f32 has a normal
996    /// for: -126..=127.
997    fn power_of_two(exp: i32) -> f32 {
998        f32::from_bits(((exp + 127) as u32) << 23)
999    }
1000
1001    /// `ue8m0` stores a bare exponent, so rounding up to it is rounding up to a power of two.
1002    #[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            // Already a power of two: nothing to round.
1007            assert_eq!(ScaleDtype::UE8M0.round_up(power), power, "2^{exp}");
1008            // Anything above it goes to the next one up, however little above.
1009            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    /// Both ends saturate. The bottom is the reason `ue8m0` needs its own path at all: 2^-127 is
1020    /// subnormal in f32, and zero — which a fully-zero block calibrates to — is not a `ue8m0`
1021    /// value in the first place.
1022    #[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    /// The answer is always representable, so rounding it again changes nothing.
1036    #[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    /// The checks that need the real storage types to compare against.
1060    #[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            // Rounding up must not overshoot: stepping down from the answer has to land below.
1067            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        /// `round_up` reads the grid instead of converting through the storage type, so a wrong
1085        /// constant there is only visible against the type itself. Nothing else in this file would
1086        /// catch one: a grid finer than the real thing still lands above the scale, still steps
1087        /// down below it, and still looks idempotent.
1088        #[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                // bf16 deliberately reports no subnormal range, since its bit step covers them too.
1094                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                // Walk the whole normal range: one step on the f32 pattern has to be one step in
1108                // the type, at every exponent.
1109                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        /// `offset` representable steps from `value` in `dtype`, for positive values. Counted on
1136        /// the storage type's own bit pattern, so this is an oracle independent of the grid under
1137        /// test.
1138        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    /// Every scheme packs into a code that reads back as itself, its private per-tensor level
1167    /// included: every value, store (at both ends of the axis), mode, per-tensor level, and block
1168    /// level at ranks one to five with sizes at both ends. The variants come from the code
1169    /// tables, which the build holds to every variant.
1170    #[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    /// A code no scheme packs to reads back as no scheme: a reserved value in any field, and a
1215    /// bit the scheme it would decode to does not use.
1216    #[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    /// A scheme the layout cannot hold has no code rather than a code that reads back as another
1247    /// scheme: the axis would spill into the mode, and a zero size would decode to a block that
1248    /// covers nothing.
1249    #[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}