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
6/// Describes a quantization scheme/configuration.
7///
8/// Scales come at up to two levels, each an optional field set through
9/// [`per_tensor`](Self::per_tensor) and [`per_block`](Self::per_block) in any order:
10///
11/// ```
12/// # use cubecl_common::quant::scheme::{QuantScheme, ScaleDtype};
13/// // One scale for the whole tensor, stored as f32. Also what a scheme with no level resolves to.
14/// QuantScheme::default().per_tensor(ScaleDtype::F32);
15///
16/// // One scale per block of 32 values.
17/// QuantScheme::default().per_block([32], ScaleDtype::F32);
18///
19/// // Two levels: ue4m3 block scales, normalized by a single per-tensor f32 scale.
20/// QuantScheme::default()
21///     .per_block([16], ScaleDtype::UE4M3)
22///     .per_tensor(ScaleDtype::F32);
23/// ```
24///
25/// A two-level scheme exists so block scales can live in a narrow type: the global per-tensor scale
26/// absorbs the tensor's dynamic range, and the block dtype only covers the spread between blocks.
27/// That spread is still bounded: a block whose scale falls further below the largest one than the
28/// block dtype can express is stored at that dtype's smallest value, far too coarse for it, and
29/// every value in the block quantizes to zero. [`ScaleDtype::UE4M3`] spans about 2^18 this way, so
30/// a tensor holding a genuine outlier can lose its ordinary values.
31#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
32pub struct QuantScheme {
33    /// The logical data type of quantized input values (e.g., [`QuantValue::Q8F`]).
34    ///
35    /// This defines how values are interpreted during computation, independent of how they're stored.
36    pub value: QuantValue,
37    /// Data type used for storing quantized values.
38    pub store: QuantStore,
39    /// Quantization mode (e.g., symmetric).
40    pub mode: QuantMode,
41    /// The per-tensor scale level. Private with [`tensor_scale`](Self::tensor_scale) as the
42    /// reader, which resolves a scheme storing no level at all to a per-tensor f32 scale.
43    tensor: Option<ScaleDtype>,
44    /// The per-block scale level, the innermost when both levels are present.
45    block: Option<BlockScale>,
46}
47
48impl Default for QuantScheme {
49    fn default() -> Self {
50        Self {
51            value: QuantValue::Q8F,
52            store: QuantStore::PackedU32(0),
53            mode: QuantMode::Symmetric,
54            tensor: None,
55            block: None,
56        }
57    }
58}
59
60impl QuantScheme {
61    /// Set the quantization mode.
62    pub fn with_mode(mut self, mode: QuantMode) -> Self {
63        self.mode = mode;
64        self
65    }
66
67    /// Set the data type used for quantized values.
68    pub fn with_value(mut self, value: QuantValue) -> Self {
69        self.value = value;
70        self
71    }
72
73    /// Set the data type used to store quantized values.
74    pub fn with_store(mut self, store: QuantStore) -> Self {
75        self.store = store;
76        self
77    }
78
79    /// Set the per-tensor scale level, stored as `dtype`.
80    pub fn per_tensor(mut self, dtype: ScaleDtype) -> Self {
81        self.tensor = Some(dtype);
82        self
83    }
84
85    /// Set the per-block scale level: one scale per block of `block` values, stored as `dtype`.
86    pub fn per_block(mut self, block: impl AsRef<[u8]>, dtype: ScaleDtype) -> Self {
87        self.block = Some(BlockScale {
88            size: BlockSize::new(block),
89            dtype,
90        });
91        self
92    }
93
94    /// The per-tensor scale level, the global level when a block level is present.
95    ///
96    /// A scheme storing no level at all resolves here to a per-tensor f32 scale; the resolution
97    /// is not stored, so such a scheme compares equal to [`Default`], not to an explicit
98    /// `per_tensor(F32)`.
99    pub fn tensor_scale(&self) -> Option<ScaleDtype> {
100        if self.tensor.is_none() && self.block.is_none() {
101            return Some(ScaleDtype::F32);
102        }
103        self.tensor
104    }
105
106    /// The per-block scale level, the innermost when both levels are present.
107    pub fn block_scale(&self) -> Option<BlockScale> {
108        self.block
109    }
110
111    /// The number of scale levels: as many scale tensors ride along with the values.
112    pub fn num_levels(&self) -> usize {
113        self.block_scale().is_some() as usize + self.tensor_scale().is_some() as usize
114    }
115
116    /// The innermost level's scale dtype, the type the per-position scales are stored in.
117    pub fn scale_dtype(&self) -> ScaleDtype {
118        self.block
119            .map(|block| block.dtype)
120            .or(self.tensor)
121            .unwrap_or(ScaleDtype::F32)
122    }
123
124    /// The block level's size, or [`None`] for per-tensor quantization.
125    pub fn block_size(&self) -> Option<BlockSize> {
126        self.block.map(|block| block.size)
127    }
128
129    /// Swap two tensor dimensions in the block level, mirroring `shape.swap(dim0, dim1)`. The
130    /// per-tensor level is unaffected.
131    ///
132    /// `dim0`/`dim1` are bare indices on purpose, mirroring `[T]::swap`'s own signature.
133    pub fn swap_block_dims(&mut self, rank: usize, dim0: usize, dim1: usize) {
134        let mut axes: Vec<usize> = (0..rank).collect();
135        axes.swap(dim0, dim1);
136        self.permute_block_dims(rank, &axes);
137    }
138
139    /// Permute the block level, mirroring a permutation of the tensor's axes. The per-tensor
140    /// level is unaffected.
141    pub fn permute_block_dims(&mut self, rank: usize, axes: &[usize]) {
142        if let Some(block) = &mut self.block {
143            let dims = block.size.to_dim_vec(rank);
144            let permuted: Vec<u8> = axes.iter().map(|&axis| dims[axis]).collect();
145            block.size = BlockSize::new(permuted);
146        }
147    }
148
149    /// Returns the size of the quantization storage type in bits.
150    pub fn size_bits_stored(&self) -> usize {
151        self.store.size_bits(&self.value)
152    }
153
154    /// Returns the size of the quantization storage type in bits.
155    pub fn size_bits_value(&self) -> usize {
156        self.value.size_bits()
157    }
158
159    /// Returns the number of quantized values stored in a single element.
160    pub fn num_quants(&self) -> usize {
161        self.size_bits_stored() / self.value.size_bits()
162    }
163
164    /// Returns the native packing factor for the values. When native packing > 1, the packed
165    /// representation stores `num_quants` elements grouped into packs of `native_packing` size.
166    pub fn native_packing(&self) -> usize {
167        self.value.native_packing()
168    }
169
170    /// Returns the packing dim for the store.
171    pub fn packing_dim(&self) -> Option<usize> {
172        self.store.packing_dim()
173    }
174
175    /// Swaps the packing dim if it's either of `dim0` or `dim1`.
176    /// Executes the corresponding update to `shape.swap(dim0, dim1)`.
177    pub fn swap_packing_dim(&mut self, dim0: usize, dim1: usize) {
178        if let QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim) =
179            &mut self.store
180        {
181            if *packed_dim == dim0 {
182                *packed_dim = dim1;
183            } else if *packed_dim == dim1 {
184                *packed_dim = dim0;
185            }
186        }
187    }
188}
189
190/// The per-block scale level of a [`QuantScheme`]: one scale per block of values.
191#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
192pub struct BlockScale {
193    /// The block of values sharing one scale.
194    pub size: BlockSize,
195    /// The dtype the level's scales are stored in.
196    pub dtype: ScaleDtype,
197}
198
199impl ScaleDtype {
200    /// The largest finite value representable by the dtype.
201    ///
202    /// A two-level scheme picks its per-tensor scale so that the largest block scale lands here,
203    /// which is what keeps the block scales inside the range their type can express. That recipe
204    /// only holds for a block dtype narrower than the scale it divides: dividing by
205    /// [`ScaleDtype::F32`]'s or [`ScaleDtype::UE8M0`]'s maximum drives the per-tensor scale
206    /// subnormal and the renormalized block scales to infinity. A two-level scheme has nothing to
207    /// gain from those params anyway, since their block scales already reach the full range.
208    pub fn max_representable(&self) -> f32 {
209        match self {
210            ScaleDtype::F32 => f32::MAX,
211            ScaleDtype::F16 => half::f16::MAX.to_f32(),
212            ScaleDtype::BF16 => half::bf16::MAX.to_f32(),
213            // Spelled out because `ue8m0` and `e4m3` sit behind the `fp8` feature and this
214            // function is not gated. The tests check both against those types when it is on.
215            ScaleDtype::UE8M0 => f32::from_bits(0x7F00_0000), // 2^127
216            ScaleDtype::UE4M3 => 448.0,
217        }
218    }
219
220    /// The smallest value representable by the dtype that is not below `scale`.
221    ///
222    /// Storing a quantization scale wants this rather than the nearest value. Rounding down puts
223    /// the scale below what calibration asked for, so every value at the block maximum clips to
224    /// the quantization range; rounding up costs one step of coarseness instead. Backends have to
225    /// agree on this, or a tensor quantized on one reconstructs differently on another.
226    ///
227    /// This is not a cast. Conversion to these types rounds to nearest, which is what a cast
228    /// should do; this is the storage policy for a scale specifically.
229    ///
230    /// `scale` must not be negative. Symmetric quantization only produces non-negative scales,
231    /// and the stepping below walks away from zero for a negative input.
232    ///
233    /// [`ScaleDtype::UE8M0`] takes its own path rather than the shared grid below: its range runs
234    /// to 2^-127, which is subnormal in f32, so the two ends need clamping before the bit stepping
235    /// is meaningful. Between them the rule is the same one — a ue8m0 value is a bare exponent, so
236    /// rounding up to it is rounding up to a power of two.
237    pub fn round_up(&self, scale: f32) -> f32 {
238        match self {
239            ScaleDtype::F32 => {
240                return scale;
241            }
242            ScaleDtype::UE8M0 => {
243                return round_up_to_power_of_two(scale);
244            }
245            _ => {}
246        }
247        if scale.is_nan() {
248            return scale;
249        }
250        debug_assert!(scale >= 0.0, "a quantization scale is never negative");
251
252        // Nothing representable sits above the maximum, and converting past it yields an infinity
253        // for the params that have one, which would make every reconstructed value NaN.
254        let max = self.max_representable();
255        if scale >= max {
256            return max;
257        }
258
259        let grid = self.f32_grid();
260
261        if let Some(subnormals) = grid.subnormals
262            && scale < subnormals.min_normal
263        {
264            // Below the minimum normal the spacing stops halving, so the answer is a count of steps.
265            // Qualified call: the inherent `f32::ceil` lives in std, and this crate builds no_std.
266            return num_traits::Float::ceil(scale / subnormals.spacing) * subnormals.spacing;
267        }
268
269        f32::from_bits((scale.to_bits() + grid.round_up_bias()) & grid.truncate_mask())
270    }
271
272    /// The dtype's grid, expressed on the f32 bit pattern. See [`F32Grid`].
273    ///
274    /// bf16 reports no subnormal range because it does not need the separate treatment: its pattern
275    /// is f32's top half all the way down, so the bit step stays right where the others stop. Its
276    /// own subnormals start at 2^-133, which is subnormal in f32 too and flushed to zero by most
277    /// backends.
278    ///
279    /// # Panics
280    ///
281    /// For [`ScaleDtype::F32`], which is the grid itself.
282    pub fn f32_grid(&self) -> F32Grid {
283        /// One f32 ulp per dtype ulp: the mantissa bits f32 carries and the dtype does not.
284        const fn bit_step(mantissa_digits: u32) -> u32 {
285            1 << (f32::MANTISSA_DIGITS - mantissa_digits)
286        }
287
288        match self {
289            ScaleDtype::F16 => F32Grid {
290                bit_step: bit_step(half::f16::MANTISSA_DIGITS),
291                subnormals: Some(SubnormalRange {
292                    min_normal: half::f16::MIN_POSITIVE.to_f32(),
293                    spacing: half::f16::MIN_POSITIVE_SUBNORMAL.to_f32(),
294                }),
295            },
296            ScaleDtype::BF16 => F32Grid {
297                bit_step: bit_step(half::bf16::MANTISSA_DIGITS),
298                subnormals: None,
299            },
300            // Spelled out rather than read off `e4m3`, which sits behind the `fp8` feature while
301            // this is not gated. The tests check them against that type when it is on.
302            ScaleDtype::UE4M3 => F32Grid {
303                bit_step: bit_step(4),
304                subnormals: Some(SubnormalRange {
305                    min_normal: 0.015625, // 2^-6
306                    spacing: 0.001953125, // 2^-9
307                }),
308            },
309            ScaleDtype::F32 => {
310                unimplemented!("F32 is the grid, it has no narrower one to round onto")
311            }
312            // No mantissa at all: the grid is the powers of two, so the step clears every f32
313            // mantissa bit. `subnormals` stays `None` because ue8m0 has no subnormal *ladder* —
314            // its bottom is the single value 2^-127, which the callers clamp to.
315            ScaleDtype::UE8M0 => F32Grid {
316                bit_step: bit_step(1),
317                subnormals: None,
318            },
319        }
320    }
321
322    /// The smallest and largest values [`ScaleDtype::UE8M0`] represents: 2^-127 and 2^127.
323    ///
324    /// The minimum is subnormal in f32 and the maximum is the largest power of two it holds, so
325    /// both are spelled as bit patterns rather than computed.
326    pub const UE8M0_MIN: f32 = f32::from_bits(0x0040_0000);
327    /// See [`ScaleDtype::UE8M0_MIN`].
328    pub const UE8M0_MAX: f32 = f32::from_bits(0x7F00_0000);
329}
330
331/// A `ue8m0` scale as its stored byte: the code is the exponent, biased by 127.
332///
333/// Rounds up, which is both the storage rule for a scale and what the host `ue8m0` codec and
334/// CUDA's `__nv_cvt_bfloat16raw_to_e8m0` (at `cudaRoundPosInf`) already do — a scale rounded down
335/// puts the block's largest value outside the quantization range.
336///
337/// Lives here rather than on the `ue8m0` type so it is available without the `float4` feature:
338/// serialization needs it, and `ue8m0` is a bare exponent, so the byte is the whole of it.
339/// (`ue8m0` itself is behind `fp8`, but its *conversions* come from `float4`, which is the gate
340/// that would otherwise reach serialization.) It has to keep answering what those conversions
341/// answer, which `the_ue8m0_codec_matches_the_storage_type` checks wherever they are compiled in.
342pub fn f32_to_ue8m0(scale: f32) -> u8 {
343    let rounded = round_up_to_power_of_two(scale);
344    if rounded.is_nan() {
345        return 0xFF;
346    }
347    // Codes 1..=254 are f32's own exponent field; the clamping above keeps the shift in range,
348    // and 2^-127 is subnormal in f32, so it lands on the exponent field 0 that code 0 names.
349    (rounded.to_bits() >> 23) as u8
350}
351
352/// The value a `ue8m0` byte stands for. Inverse of [`f32_to_ue8m0`] on every code it produces.
353pub fn ue8m0_to_f32(code: u8) -> f32 {
354    match code {
355        // f32 has no exponent field 0 to spare: its own is the subnormals.
356        0 => ScaleDtype::UE8M0_MIN,
357        0xFF => f32::NAN,
358        code => f32::from_bits((code as u32) << 23),
359    }
360}
361
362/// The smallest power of two not below `scale`, saturated into ue8m0's range.
363///
364/// A ue8m0 code *is* an exponent, so this is the whole storage rule for that dtype. Clamping both
365/// ends first is what lets the middle be the same mantissa-clearing step the other dtypes use:
366/// below 2^-127 there is nothing to round onto, and above 2^127 the step would carry into f32's
367/// infinity and take every value scaled by it with it. Zero clamps up to the minimum — ue8m0 has
368/// no zero, and a zero scale reconstructs an all-zero block correctly at any scale.
369fn round_up_to_power_of_two(scale: f32) -> f32 {
370    if scale.is_nan() {
371        return scale;
372    }
373    debug_assert!(scale >= 0.0, "a quantization scale is never negative");
374
375    if scale <= ScaleDtype::UE8M0_MIN {
376        return ScaleDtype::UE8M0_MIN;
377    }
378    if scale >= ScaleDtype::UE8M0_MAX {
379        return ScaleDtype::UE8M0_MAX;
380    }
381
382    let grid = ScaleDtype::UE8M0.f32_grid();
383    f32::from_bits((scale.to_bits() + grid.round_up_bias()) & grid.truncate_mask())
384}
385
386/// A narrower float format's grid, laid over the f32 bit pattern.
387///
388/// f32 carries every dtype this exists for exactly, so the grid can be walked there rather than
389/// through the storage type. A value representable in the dtype leaves the low f32 mantissa bits
390/// zero, so one dtype ulp is an increment at that position and the carry into the exponent falls
391/// out on its own. Working in f32 also keeps the grid available to backends with no narrow integer,
392/// and to builds without the `fp8` feature.
393#[derive(Clone, Copy, Debug, PartialEq)]
394pub struct F32Grid {
395    /// One step up in the normal range, as an increment on the f32 bit pattern.
396    pub bit_step: u32,
397    /// The dtype's subnormals, for the formats whose subnormals land in f32's normal range.
398    pub subnormals: Option<SubnormalRange>,
399}
400
401/// Where a format's subnormals begin and how far apart they are, in f32.
402#[derive(Clone, Copy, Debug, PartialEq)]
403pub struct SubnormalRange {
404    /// The smallest normal value, below which the spacing stops halving.
405    pub min_normal: f32,
406    /// The constant distance between neighbouring subnormals.
407    pub spacing: f32,
408}
409
410impl F32Grid {
411    /// Clears the mantissa bits the dtype does not carry, truncating a bit pattern onto the grid.
412    pub fn truncate_mask(&self) -> u32 {
413        !(self.bit_step - 1)
414    }
415
416    /// Added to a bit pattern before [`truncate_mask`](Self::truncate_mask) to turn that truncation
417    /// into a round up. The carry it can produce is only safe below the dtype's maximum, which is
418    /// why callers saturate there first.
419    pub fn round_up_bias(&self) -> u32 {
420        self.bit_step - 1
421    }
422}
423
424/// Data type used to represent quantized values.
425#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
426pub enum QuantValue {
427    /// 8-bit quantization with full range.
428    Q8F,
429    /// 8-bit floating point, e5m2 format.
430    E5M2,
431    /// 8-bit floating point, e4m3 format.
432    E4M3,
433    /// 4-bit quantization with full range.
434    Q4F,
435    /// 4-bit floating point, e2m1 format.
436    E2M1,
437    /// 2-bit quantization with full range.
438    Q2F,
439    /// 8-bit quantization with symmetric range.
440    Q8S,
441    /// 4-bit quantization with symmetric range.
442    Q4S,
443    /// 2-bit quantization with symmetric range.
444    Q2S,
445}
446
447impl QuantValue {
448    /// Returns the size of the quantization input type in bits.
449    pub fn size_bits(&self) -> usize {
450        match self {
451            QuantValue::Q8F | QuantValue::Q8S | QuantValue::E4M3 | QuantValue::E5M2 => 8,
452            QuantValue::Q4F | QuantValue::Q4S | QuantValue::E2M1 => 4,
453            QuantValue::Q2F | QuantValue::Q2S => 2,
454        }
455    }
456
457    /// Packing factor for the native representation used for intermediate values. If > 1, values
458    /// should always be processed in `native_packing` sized chunks.
459    pub fn native_packing(&self) -> usize {
460        match self {
461            QuantValue::E2M1 => 2,
462            _ => 1,
463        }
464    }
465
466    /// The possible range of values allowed by the quant value.
467    pub fn range(&self) -> (f32, f32) {
468        match self {
469            QuantValue::Q8F => (i8::MIN as f32, i8::MAX as f32),
470            QuantValue::Q4F => (-8.0, 7.0),
471            QuantValue::Q2F => (-2.0, 1.0),
472            QuantValue::Q8S => (-i8::MAX as f32, i8::MAX as f32),
473            QuantValue::Q4S => (-7.0, 7.0),
474            QuantValue::Q2S => (-1.0, 1.0),
475            QuantValue::E4M3 => (-448.0, 448.0),
476            QuantValue::E5M2 => (-57344.0, 57344.0),
477            QuantValue::E2M1 => (-6.0, 6.0), // Hardcoded because of no-std
478        }
479    }
480
481    /// If the range of values is symmetric around zero.
482    pub fn is_symmetric(&self) -> bool {
483        match self {
484            Self::Q8F | Self::Q4F | Self::Q2F | Self::E4M3 | Self::E5M2 | Self::E2M1 => false,
485            Self::Q8S | Self::Q4S | Self::Q2S => true,
486        }
487    }
488}
489
490impl QuantStore {
491    /// Returns the size of the quantization input type in bits.
492    pub fn size_bits(&self, value: &QuantValue) -> usize {
493        match self {
494            QuantStore::Native => value.size_bits(),
495            QuantStore::PackedNative(_) => value.size_bits() * value.native_packing(),
496            QuantStore::PackedU32(_) => 32,
497        }
498    }
499
500    fn packing_dim(&self) -> Option<usize> {
501        match self {
502            QuantStore::Native => None,
503            QuantStore::PackedNative(packing_dim) | QuantStore::PackedU32(packing_dim) => {
504                Some(*packing_dim)
505            }
506        }
507    }
508}
509
510/// Data type used to stored quantized values.
511#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
512pub enum QuantStore {
513    /// Native quantization doesn't require packing and unpacking.
514    Native,
515    /// Store packed quantized values in a natively supported packing format (i.e. e2m1x2).
516    /// Argument is the dimension the tensor is packed on, starting from the innermost dimension.
517    PackedNative(usize),
518    /// Store packed quantized values in a 4-byte unsigned integer.
519    /// Argument is the dimension the tensor is packed on, starting from the innermost dimension.
520    PackedU32(usize),
521    // /// Store packed quantized values in a 8-bit unsigned integer.
522    // U8,
523}
524
525/// Strategy used to quantize values.
526#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
527pub enum QuantMode {
528    /// Symmetric or scale quantization.
529    Symmetric,
530    /// The stored field is an index into a lookup table of `2^bits` floats, not a number: a read
531    /// reconstructs `table[field] * scale`. (Known as a codebook in the quantization literature —
532    /// NF4, K-quants, and vector quantizers all decode this way.) The table travels as its own
533    /// binding beside the values and scales; only the field's bit width is read from
534    /// [`QuantScheme::value`], since an index has no sign or float semantics of its own.
535    Lookup,
536}
537
538/// The data type a scale level stores its scales in.
539#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
540pub enum ScaleDtype {
541    /// Full precision.
542    F32,
543    /// Half precision.
544    F16,
545    /// bfloat16 precision.
546    BF16,
547    /// unsigned floating point, e8m0 format.
548    UE8M0,
549    /// unsigned floating point, e4m3 format.
550    UE4M3,
551}
552
553const MAX_DIMS: usize = 5;
554
555/// Copyable block size, specialized version of `SmallVec`.
556#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
557pub struct BlockSize {
558    storage: [u8; MAX_DIMS],
559    len: u8,
560}
561
562/// Hand-written: `storage` precedes `len`, so a derived `Ord` would compare filler bytes before
563/// length.
564impl PartialOrd for BlockSize {
565    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
566        Some(self.cmp(other))
567    }
568}
569
570impl Ord for BlockSize {
571    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
572        (self.len, self.as_slice()).cmp(&(other.len, other.as_slice()))
573    }
574}
575
576impl core::fmt::Debug for BlockSize {
577    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
578        write!(f, "BlockSize({:?})", self.as_slice())
579    }
580}
581
582impl BlockSize {
583    /// Max number of dimensions for block size
584    pub const MAX_DIMS: usize = MAX_DIMS;
585
586    /// Create a new blocksize from a set of values. The number of values must be `<= MAX_DIMS`.
587    ///
588    /// The result is canonical, so equal rank-relative blocks compare and hash equal however they
589    /// are spelled: leading unit dimensions are dropped, since the missing-dimension fill restates
590    /// them. In particular, `[1, 32]` canonicalizes to `[32]`. Whole-tensor granularity is a
591    /// scheme's per-tensor level, not a block size.
592    pub fn new(values: impl AsRef<[u8]>) -> Self {
593        Self::canonicalize(values.as_ref())
594    }
595
596    fn canonicalize(values: &[u8]) -> Self {
597        let skip = values
598            .iter()
599            .position(|&value| value != 1)
600            .unwrap_or(values.len());
601        let values = &values[skip..];
602        debug_assert!(
603            values.len() <= MAX_DIMS,
604            "Tried creating a block size larger than the cap"
605        );
606        let len = values.len().min(MAX_DIMS);
607        let mut storage = [1; MAX_DIMS];
608        storage[..len].copy_from_slice(&values[..len]);
609        Self {
610            storage,
611            len: len as u8,
612        }
613    }
614
615    /// Return a slice of only the initialized values
616    pub fn as_slice(&self) -> &[u8] {
617        &self.storage[..self.len as usize]
618    }
619
620    /// Return a vec of only the initialized values
621    pub fn to_vec(&self) -> Vec<u8> {
622        self.storage[..self.len as usize].to_vec()
623    }
624
625    /// Returns `N` dimensions, unsqueezing if necessary. Missing leading dimensions fill with `1`.
626    pub fn as_dim<const N: usize>(&self) -> [u8; N] {
627        let data_len = N.min(self.len as usize);
628        let data_start = N - data_len;
629        let mut out = [1; N];
630        out[data_start..].copy_from_slice(&self.storage[..data_len]);
631        out
632    }
633
634    /// Returns a vector of `len` dimensions, unsqueezing if necessary. Missing leading dimensions
635    /// fill with `1`.
636    pub fn to_dim_vec(&self, len: usize) -> Vec<u8> {
637        let data_len = len.min(self.len as usize);
638        let data_start = len - data_len;
639        let mut out = vec![1; len];
640        out[data_start..].copy_from_slice(&self.storage[..data_len]);
641        out
642    }
643
644    /// How many blocks cover each dimension of `shape`, which is the shape of the scale grid:
645    /// one scale per block.
646    pub fn num_blocks(&self, shape: &[usize]) -> Vec<usize> {
647        self.to_dim_vec(shape.len())
648            .into_iter()
649            .zip(shape)
650            .map(|(block, &dim)| dim.div_ceil(block as usize))
651            .collect()
652    }
653
654    /// Create an iterator over all stored dimensions
655    pub fn iter(&self) -> impl Iterator<Item = &u8> {
656        self.as_slice().iter()
657    }
658
659    /// Returns the total number of elements in each block.
660    pub fn num_elements(&self) -> usize {
661        self.iter().map(|it| *it as usize).product()
662    }
663}
664
665impl Deref for BlockSize {
666    type Target = [u8];
667
668    fn deref(&self) -> &Self::Target {
669        self.as_slice()
670    }
671}
672
673impl<T: AsRef<[u8]>> From<T> for BlockSize {
674    fn from(value: T) -> Self {
675        BlockSize::new(value)
676    }
677}
678
679#[cfg(test)]
680mod tests {
681    use super::*;
682
683    #[test]
684    fn blocks_remain_rank_relative() {
685        assert_ne!(BlockSize::new([32]), BlockSize::new([32, 32]));
686        assert_eq!(BlockSize::new([32]).to_dim_vec(2), vec![1, 32]);
687        assert_eq!(BlockSize::new([32, 32]).to_dim_vec(2), vec![32, 32]);
688    }
689
690    #[test]
691    fn leading_unit_dimensions_canonicalize_away() {
692        assert_eq!(BlockSize::new([1, 32]), BlockSize::new([32]));
693    }
694
695    #[test]
696    fn leading_unit_dimensions_beyond_the_cap_still_canonicalize() {
697        assert_eq!(
698            BlockSize::new([1, 1, 8, 4, 2, 3]),
699            BlockSize::new([8, 4, 2, 3])
700        );
701    }
702
703    #[test]
704    fn there_is_one_block_per_scale() {
705        assert_eq!(BlockSize::new([32]).num_blocks(&[8, 64]), vec![8, 2]);
706        assert_eq!(BlockSize::new([4, 32]).num_blocks(&[8, 64]), vec![2, 2]);
707        assert_eq!(BlockSize::new([32]).num_blocks(&[4, 8, 64]), vec![4, 8, 2]);
708    }
709
710    #[test]
711    fn a_partial_block_still_takes_a_scale() {
712        assert_eq!(BlockSize::new([32]).num_blocks(&[8, 70]), vec![8, 3]);
713    }
714
715    #[test]
716    fn the_default_scheme_resolves_to_per_tensor_f32() {
717        let scheme = QuantScheme::default();
718        assert_eq!(scheme.tensor_scale(), Some(ScaleDtype::F32));
719        assert_eq!(scheme.block_scale(), None);
720        assert_eq!(scheme.scale_dtype(), ScaleDtype::F32);
721        assert_eq!(scheme.block_size(), None);
722        assert_eq!(scheme.num_levels(), 1);
723    }
724
725    #[test]
726    fn a_block_level_stands_alone() {
727        let scheme = QuantScheme::default().per_block([32], ScaleDtype::F16);
728        assert_eq!(scheme.tensor_scale(), None);
729        assert_eq!(scheme.scale_dtype(), ScaleDtype::F16);
730        assert_eq!(scheme.block_size(), Some(BlockSize::new([32])));
731        assert_eq!(scheme.num_levels(), 1);
732    }
733
734    #[test]
735    fn both_levels_nest_the_block_inside_the_tensor() {
736        let scheme = QuantScheme::default()
737            .per_block([16], ScaleDtype::UE4M3)
738            .per_tensor(ScaleDtype::F32);
739        assert_eq!(scheme.scale_dtype(), ScaleDtype::UE4M3);
740        assert_eq!(scheme.tensor_scale(), Some(ScaleDtype::F32));
741        assert_eq!(scheme.num_levels(), 2);
742    }
743
744    #[test]
745    fn levels_set_in_any_order_are_the_same_scheme() {
746        assert_eq!(
747            QuantScheme::default()
748                .per_block([16], ScaleDtype::UE4M3)
749                .per_tensor(ScaleDtype::F32),
750            QuantScheme::default()
751                .per_tensor(ScaleDtype::F32)
752                .per_block([16], ScaleDtype::UE4M3),
753        );
754    }
755
756    #[test]
757    fn swapping_dims_rewrites_the_block_and_leaves_the_tensor_level_alone() {
758        let mut scheme = QuantScheme::default()
759            .per_block([4, 32], ScaleDtype::F16)
760            .per_tensor(ScaleDtype::F32);
761        scheme.swap_block_dims(2, 0, 1);
762        assert_eq!(
763            scheme,
764            QuantScheme::default()
765                .per_block([32, 4], ScaleDtype::F16)
766                .per_tensor(ScaleDtype::F32)
767        );
768
769        let mut per_tensor = QuantScheme::default();
770        per_tensor.swap_block_dims(2, 0, 1);
771        assert_eq!(per_tensor, QuantScheme::default());
772    }
773
774    #[test]
775    fn swapping_dims_canonicalizes_the_block() {
776        let mut scheme = QuantScheme::default().per_block([32, 1], ScaleDtype::F32);
777        scheme.swap_block_dims(2, 0, 1);
778        assert_eq!(scheme.block_size(), Some(BlockSize::new([32])));
779    }
780
781    #[test]
782    fn permuting_dims_rewrites_the_block() {
783        let mut scheme = QuantScheme::default().per_block([1, 4, 32], ScaleDtype::F16);
784        scheme.permute_block_dims(3, &[2, 0, 1]);
785        assert_eq!(scheme.block_size(), Some(BlockSize::new([32, 1, 4])));
786    }
787
788    #[test]
789    fn round_up_never_lands_below_the_scale() {
790        for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
791            for exp in -12..8 {
792                for step in 1..17 {
793                    let scale = (step as f32 / 16.0) * 2f32.powi(exp);
794                    let up = dtype.round_up(scale);
795                    assert!(
796                        up >= scale,
797                        "{dtype:?}: {up} is below {scale}, which clips the block maximum"
798                    );
799                }
800            }
801        }
802    }
803
804    #[test]
805    fn round_up_saturates_rather_than_stepping_off_the_top() {
806        for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
807            let max = dtype.max_representable();
808            assert_eq!(dtype.round_up(max), max);
809            assert!(dtype.round_up(max * 2.0).is_finite());
810        }
811    }
812
813    /// Every variant is dispatched somewhere, so none of them may panic here, and every answer is
814    /// a scale something can be divided by.
815    #[test]
816    fn round_up_answers_for_every_param() {
817        for dtype in [
818            ScaleDtype::F32,
819            ScaleDtype::F16,
820            ScaleDtype::BF16,
821            ScaleDtype::UE8M0,
822            ScaleDtype::UE4M3,
823        ] {
824            let up = dtype.round_up(0.3);
825            assert!(up.is_finite() && up > 0.0, "{dtype:?} answered {up:e}");
826        }
827    }
828
829    /// 2^`exp`, from the exponent field rather than through `powi`.
830    ///
831    /// `powi` is only ever approximate — Miri returns a value a few ulp off, deliberately, so
832    /// nothing comes to depend on one host's precision — and a power of two that is a hair off is
833    /// a different power of two once its mantissa is cleared. Only for exponents f32 has a normal
834    /// for: -126..=127.
835    fn power_of_two(exp: i32) -> f32 {
836        f32::from_bits(((exp + 127) as u32) << 23)
837    }
838
839    /// `ue8m0` stores a bare exponent, so rounding up to it is rounding up to a power of two.
840    #[test]
841    fn ue8m0_rounds_up_to_a_power_of_two() {
842        for exp in -120..120 {
843            let power = power_of_two(exp);
844            // Already a power of two: nothing to round.
845            assert_eq!(ScaleDtype::UE8M0.round_up(power), power, "2^{exp}");
846            // Anything above it goes to the next one up, however little above.
847            for scale in [power * 1.0001, power * 1.5, power * 1.9999] {
848                assert_eq!(
849                    ScaleDtype::UE8M0.round_up(scale),
850                    power * 2.0,
851                    "{scale} (2^{exp} scaled)"
852                );
853            }
854        }
855    }
856
857    /// Both ends saturate. The bottom is the reason `ue8m0` needs its own path at all: 2^-127 is
858    /// subnormal in f32, and zero — which a fully-zero block calibrates to — is not a `ue8m0`
859    /// value in the first place.
860    #[test]
861    fn ue8m0_saturates_at_both_ends() {
862        let min = ScaleDtype::UE8M0_MIN;
863        let max = ScaleDtype::UE8M0_MAX;
864
865        for scale in [0.0, f32::MIN_POSITIVE * 0.5, min * 0.5, min] {
866            assert_eq!(ScaleDtype::UE8M0.round_up(scale), min, "{scale:e}");
867        }
868        for scale in [max, max * 2.0, f32::MAX, f32::INFINITY] {
869            assert_eq!(ScaleDtype::UE8M0.round_up(scale), max, "{scale:e}");
870        }
871    }
872
873    /// Every byte stands for a value that encodes back to it — the codec is a bijection on the
874    /// codes, which is what serializing a scale and reading it back depends on.
875    #[test]
876    fn every_ue8m0_code_round_trips() {
877        for code in 0..=0xFEu8 {
878            let value = ue8m0_to_f32(code);
879            assert_eq!(
880                f32_to_ue8m0(value),
881                code,
882                "code {code} decoded to {value:e}"
883            );
884        }
885        assert!(ue8m0_to_f32(0xFF).is_nan());
886    }
887
888    /// The codec agrees with `round_up`, so a scale stored through either lands on the same value.
889    #[test]
890    fn the_ue8m0_codec_agrees_with_the_round_up_rule() {
891        for exp in -130..130 {
892            for factor in [1.0, 1.3, 1.9] {
893                let scale = factor * 2f32.powi(exp);
894                assert_eq!(
895                    ue8m0_to_f32(f32_to_ue8m0(scale)),
896                    ScaleDtype::UE8M0.round_up(scale),
897                    "{scale:e}"
898                );
899            }
900        }
901    }
902
903    /// The answer is always representable, so rounding it again changes nothing.
904    #[test]
905    fn ue8m0_round_up_is_idempotent() {
906        for exp in -130..130 {
907            for factor in [1.0, 1.3, 1.7] {
908                let scale = factor * 2f32.powi(exp);
909                let up = ScaleDtype::UE8M0.round_up(scale);
910                assert_eq!(
911                    ScaleDtype::UE8M0.round_up(up),
912                    up,
913                    "not idempotent at {scale:e}"
914                );
915                assert!(up >= scale.min(ScaleDtype::UE8M0_MAX), "{up:e} < {scale:e}");
916            }
917        }
918    }
919
920    #[test]
921    fn round_up_is_the_identity_for_f32() {
922        for scale in [1.0e-30, 0.1, 1.0, 12345.678, f32::MAX] {
923            assert_eq!(ScaleDtype::F32.round_up(scale), scale);
924        }
925    }
926
927    /// The checks that need the real storage types to compare against.
928    #[cfg(feature = "fp8")]
929    mod storage_types {
930        use super::*;
931
932        #[test]
933        fn round_up_is_the_nearest_representable_value_not_below() {
934            // Rounding up must not overshoot: stepping down from the answer has to land below.
935            for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
936                for exp in -8..6 {
937                    let scale = 1.7 * 2f32.powi(exp);
938                    let up = dtype.round_up(scale);
939                    assert_eq!(
940                        up,
941                        dtype.round_up(up),
942                        "{dtype:?}: not idempotent at {scale}"
943                    );
944                    assert!(
945                        step(dtype, up, -1) < scale,
946                        "{dtype:?}: {up} overshoots {scale} by at least a step"
947                    );
948                }
949            }
950        }
951
952        /// `round_up` reads the grid instead of converting through the storage type, so a wrong
953        /// constant there is only visible against the type itself. Nothing else in this file would
954        /// catch one: a grid finer than the real thing still lands above the scale, still steps
955        /// down below it, and still looks idempotent.
956        #[test]
957        fn f32_grid_matches_the_storage_types() {
958            for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
959                let grid = dtype.f32_grid();
960
961                // bf16 deliberately reports no subnormal range, since its bit step covers them too.
962                if let Some(subnormals) = grid.subnormals {
963                    assert_eq!(
964                        subnormals.min_normal,
965                        min_normal(dtype),
966                        "{dtype:?}: minimum normal"
967                    );
968                    assert_eq!(
969                        subnormals.spacing,
970                        step(dtype, 0.0, 1),
971                        "{dtype:?}: subnormal spacing"
972                    );
973                }
974
975                // Walk the whole normal range: one step on the f32 pattern has to be one step in
976                // the type, at every exponent.
977                let mut value = min_normal(dtype);
978                let max = dtype.max_representable();
979                while value < max {
980                    let stepped = f32::from_bits(value.to_bits() + grid.bit_step);
981                    assert_eq!(
982                        stepped,
983                        step(dtype, value, 1),
984                        "{dtype:?}: step above {value}"
985                    );
986                    value = stepped;
987                }
988                assert_eq!(
989                    value, max,
990                    "{dtype:?}: the grid has to land exactly on the maximum"
991                );
992            }
993        }
994
995        #[test]
996        fn max_representable_matches_the_e4m3_type() {
997            assert_eq!(
998                ScaleDtype::UE4M3.max_representable(),
999                crate::e4m3::MAX.to_f32()
1000            );
1001        }
1002
1003        /// The other limit spelled out as a literal. `ue8m0` is exponent only, so its maximum is
1004        /// the power of two the hex literal encodes.
1005        #[test]
1006        fn max_representable_matches_the_e8m0_type() {
1007            assert_eq!(
1008                ScaleDtype::UE8M0.max_representable(),
1009                crate::ue8m0::MAX.to_f32()
1010            );
1011        }
1012
1013        /// [`f32_to_ue8m0`] and [`ue8m0_to_f32`] restate what the `ue8m0` type already does, so
1014        /// that this module stays usable without the `fp8` feature. Restating it is only safe
1015        /// while the two agree: a scale written through one and read through the other has to be
1016        /// the same scale, and the two ends of that trip are on opposite sides of the gate.
1017        ///
1018        /// Includes the rounding, which is the part that could plausibly drift — `ue8m0` rounds
1019        /// up, where a conversion would normally round to nearest.
1020        ///
1021        /// Gated on `float4` rather than on this module's `fp8`, because that is where `ue8m0`'s
1022        /// conversions live — and it is exactly that split which is the reason the pair above
1023        /// exists at all.
1024        #[test]
1025        #[cfg(feature = "fp4")]
1026        fn the_ue8m0_codec_matches_the_storage_type() {
1027            for code in 0..=0xFFu8 {
1028                let ours = ue8m0_to_f32(code);
1029                let theirs = crate::ue8m0::from_bits(code).to_f32();
1030                if theirs.is_nan() {
1031                    assert!(ours.is_nan(), "code {code}: {ours:e} is not a NaN");
1032                } else {
1033                    assert_eq!(ours.to_bits(), theirs.to_bits(), "code {code}");
1034                }
1035            }
1036
1037            for exp in -130..130 {
1038                for factor in [1.0, 1.25, 1.5, 1.9] {
1039                    let scale = factor * 2f32.powi(exp);
1040                    assert_eq!(
1041                        f32_to_ue8m0(scale),
1042                        crate::ue8m0::from_f32(scale).to_bits(),
1043                        "{scale:e}"
1044                    );
1045                }
1046            }
1047        }
1048
1049        /// `offset` representable steps from `value` in `dtype`, for positive values. Counted on
1050        /// the storage type's own bit pattern, so this is an oracle independent of the grid under
1051        /// test.
1052        fn step(dtype: ScaleDtype, value: f32, offset: i32) -> f32 {
1053            match dtype {
1054                ScaleDtype::F16 => half::f16::from_bits(
1055                    (half::f16::from_f32(value).to_bits() as i32 + offset) as u16,
1056                )
1057                .to_f32(),
1058                ScaleDtype::BF16 => half::bf16::from_bits(
1059                    (half::bf16::from_f32(value).to_bits() as i32 + offset) as u16,
1060                )
1061                .to_f32(),
1062                ScaleDtype::UE4M3 => crate::e4m3::from_bits(
1063                    (crate::e4m3::from_f32(value).to_bits() as i32 + offset) as u8,
1064                )
1065                .to_f32(),
1066                ScaleDtype::F32 | ScaleDtype::UE8M0 => unreachable!(),
1067            }
1068        }
1069
1070        fn min_normal(dtype: ScaleDtype) -> f32 {
1071            match dtype {
1072                ScaleDtype::F16 => half::f16::MIN_POSITIVE.to_f32(),
1073                ScaleDtype::BF16 => half::bf16::MIN_POSITIVE.to_f32(),
1074                ScaleDtype::UE4M3 => crate::e4m3::MIN_POSITIVE.to_f32(),
1075                ScaleDtype::F32 | ScaleDtype::UE8M0 => unreachable!(),
1076            }
1077        }
1078    }
1079}