use alloc::vec;
use alloc::vec::Vec;
use core::{default::Default, ops::Deref};
use serde::{Deserialize, Serialize};
use crate::{e2m1, ue8m0};
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct QuantScheme {
pub value: QuantValue,
pub store: QuantStore,
pub mode: QuantMode,
tensor: Option<ScaleDtype>,
block: Option<BlockScale>,
}
impl Default for QuantScheme {
fn default() -> Self {
Self {
value: QuantValue::Q8F,
store: QuantStore::PackedU32(0),
mode: QuantMode::Symmetric,
tensor: None,
block: None,
}
}
}
impl QuantScheme {
pub fn with_mode(mut self, mode: QuantMode) -> Self {
self.mode = mode;
self
}
pub fn with_value(mut self, value: QuantValue) -> Self {
self.value = value;
self
}
pub fn with_store(mut self, store: QuantStore) -> Self {
self.store = store;
self
}
pub fn per_tensor(mut self, dtype: ScaleDtype) -> Self {
self.tensor = Some(dtype);
self
}
pub fn per_block(mut self, block: impl AsRef<[u8]>, dtype: ScaleDtype) -> Self {
self.block = Some(BlockScale {
size: BlockSize::new(block),
dtype,
});
self
}
pub fn tensor_scale(&self) -> Option<ScaleDtype> {
if self.tensor.is_none() && self.block.is_none() {
return Some(ScaleDtype::F32);
}
self.tensor
}
pub fn block_scale(&self) -> Option<BlockScale> {
self.block
}
pub fn num_levels(&self) -> usize {
self.block_scale().is_some() as usize + self.tensor_scale().is_some() as usize
}
pub fn scale_dtype(&self) -> ScaleDtype {
self.block
.map(|block| block.dtype)
.or(self.tensor)
.unwrap_or(ScaleDtype::F32)
}
pub fn block_size(&self) -> Option<BlockSize> {
self.block.map(|block| block.size)
}
pub fn swap_block_dims(&mut self, rank: usize, dim0: usize, dim1: usize) {
let mut axes: Vec<usize> = (0..rank).collect();
axes.swap(dim0, dim1);
self.permute_block_dims(rank, &axes);
}
pub fn permute_block_dims(&mut self, rank: usize, axes: &[usize]) {
if let Some(block) = &mut self.block {
let dims = block.size.to_dim_vec(rank);
let permuted: Vec<u8> = axes.iter().map(|&axis| dims[axis]).collect();
block.size = BlockSize::new(permuted);
}
}
pub fn size_bits_stored(&self) -> usize {
self.store.size_bits(&self.value)
}
pub fn size_bits_value(&self) -> usize {
self.value.size_bits()
}
pub fn num_quants(&self) -> usize {
self.size_bits_stored() / self.value.size_bits()
}
pub fn native_packing(&self) -> usize {
self.value.native_packing()
}
pub fn packing_dim(&self) -> Option<usize> {
self.store.packing_dim()
}
fn swap_packing_dim(&mut self, rank: usize, dim0: usize, dim1: usize) {
let mut axes: Vec<usize> = (0..rank).collect();
axes.swap(dim0, dim1);
self.permute_packing_dim(&axes);
}
fn permute_packing_dim(&mut self, axes: &[usize]) {
if let QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim) =
&mut self.store
&& !axes.is_empty()
{
let rank = axes.len();
let packed_axis = rank
.checked_sub(*packed_dim + 1)
.expect("the packing dim to be one of the permuted axes");
let new_axis = axes
.iter()
.position(|axis| *axis == packed_axis)
.expect("the permutation to contain the packed axis");
*packed_dim = rank - new_axis - 1;
}
}
pub fn swap_dims(&mut self, rank: usize, dim0: usize, dim1: usize) {
self.swap_block_dims(rank, dim0, dim1);
self.swap_packing_dim(rank, dim0, dim1);
}
pub fn permute_dims(&mut self, axes: &[usize]) {
self.permute_block_dims(axes.len(), axes);
self.permute_packing_dim(axes);
}
pub fn reshape_packing_dim(&mut self, rank: usize) {
if let QuantStore::PackedU32(packed_dim) | QuantStore::PackedNative(packed_dim) =
&mut self.store
&& *packed_dim >= rank.max(1)
{
*packed_dim = 0;
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub struct BlockScale {
pub size: BlockSize,
pub dtype: ScaleDtype,
}
impl ScaleDtype {
pub fn max_representable(&self) -> f32 {
match self {
ScaleDtype::F32 => f32::MAX,
ScaleDtype::F16 => half::f16::MAX.to_f32(),
ScaleDtype::BF16 => half::bf16::MAX.to_f32(),
ScaleDtype::UE8M0 => ue8m0::MAX.to_f32(),
ScaleDtype::UE4M3 => 448.0,
}
}
pub fn round_up(&self, scale: f32) -> f32 {
if scale.is_nan() {
return scale;
}
debug_assert!(scale >= 0.0, "a quantization scale is never negative");
match self {
ScaleDtype::F32 => {
return scale;
}
ScaleDtype::UE8M0 => {
return ue8m0::from_f32(scale).to_f32();
}
_ => {}
}
let max = self.max_representable();
if scale >= max {
return max;
}
let grid = self.f32_grid();
if let Some(subnormals) = grid.subnormals
&& scale < subnormals.min_normal
{
return num_traits::Float::ceil(scale / subnormals.spacing) * subnormals.spacing;
}
f32::from_bits((scale.to_bits() + grid.round_up_bias()) & grid.truncate_mask())
}
pub fn f32_grid(&self) -> F32Grid {
const fn bit_step(mantissa_digits: u32) -> u32 {
1 << (f32::MANTISSA_DIGITS - mantissa_digits)
}
match self {
ScaleDtype::F16 => F32Grid {
bit_step: bit_step(half::f16::MANTISSA_DIGITS),
subnormals: Some(SubnormalRange {
min_normal: half::f16::MIN_POSITIVE.to_f32(),
spacing: half::f16::MIN_POSITIVE_SUBNORMAL.to_f32(),
}),
},
ScaleDtype::BF16 => F32Grid {
bit_step: bit_step(half::bf16::MANTISSA_DIGITS),
subnormals: None,
},
ScaleDtype::UE4M3 => F32Grid {
bit_step: bit_step(4),
subnormals: Some(SubnormalRange {
min_normal: 0.015625, spacing: 0.001953125, }),
},
ScaleDtype::F32 => {
unimplemented!("F32 is the grid, it has no narrower one to round onto")
}
ScaleDtype::UE8M0 => F32Grid {
bit_step: bit_step(1),
subnormals: None,
},
}
}
pub const UE8M0_MIN: f32 = ue8m0::MIN.to_f32();
pub const UE8M0_MAX: f32 = ue8m0::MAX.to_f32();
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct F32Grid {
pub bit_step: u32,
pub subnormals: Option<SubnormalRange>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SubnormalRange {
pub min_normal: f32,
pub spacing: f32,
}
impl F32Grid {
pub fn truncate_mask(&self) -> u32 {
!(self.bit_step - 1)
}
pub fn round_up_bias(&self) -> u32 {
self.bit_step - 1
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum QuantValue {
Q8F,
E5M2,
E4M3,
Q4F,
E2M1,
Q2F,
Q8S,
Q4S,
Q2S,
}
impl QuantValue {
pub fn size_bits(&self) -> usize {
match self {
QuantValue::Q8F | QuantValue::Q8S | QuantValue::E4M3 | QuantValue::E5M2 => 8,
QuantValue::Q4F | QuantValue::Q4S | QuantValue::E2M1 => 4,
QuantValue::Q2F | QuantValue::Q2S => 2,
}
}
pub fn native_packing(&self) -> usize {
match self {
QuantValue::E2M1 => 2,
_ => 1,
}
}
pub fn range(&self) -> (f32, f32) {
match self {
QuantValue::Q8F => (i8::MIN as f32, i8::MAX as f32),
QuantValue::Q4F => (-8.0, 7.0),
QuantValue::Q2F => (-2.0, 1.0),
QuantValue::Q8S => (-i8::MAX as f32, i8::MAX as f32),
QuantValue::Q4S => (-7.0, 7.0),
QuantValue::Q2S => (-1.0, 1.0),
QuantValue::E4M3 => (-448.0, 448.0),
QuantValue::E5M2 => (-57344.0, 57344.0),
QuantValue::E2M1 => (e2m1::MIN.to_f32(), e2m1::MAX.to_f32()),
}
}
pub fn is_float(&self) -> bool {
match self {
Self::E4M3 | Self::E5M2 | Self::E2M1 => true,
Self::Q8F | Self::Q8S | Self::Q4F | Self::Q4S | Self::Q2F | Self::Q2S => false,
}
}
pub fn is_symmetric(&self) -> bool {
match self {
Self::Q8F | Self::Q4F | Self::Q2F | Self::E4M3 | Self::E5M2 | Self::E2M1 => false,
Self::Q8S | Self::Q4S | Self::Q2S => true,
}
}
}
impl QuantStore {
pub fn size_bits(&self, value: &QuantValue) -> usize {
match self {
QuantStore::Native => value.size_bits(),
QuantStore::PackedNative(_) => value.size_bits() * value.native_packing(),
QuantStore::PackedU32(_) => 32,
}
}
fn packing_dim(&self) -> Option<usize> {
match self {
QuantStore::Native => None,
QuantStore::PackedNative(packing_dim) | QuantStore::PackedU32(packing_dim) => {
Some(*packing_dim)
}
}
}
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum QuantStore {
Native,
PackedNative(usize),
PackedU32(usize),
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum QuantMode {
Symmetric,
Lookup,
}
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
pub enum ScaleDtype {
F32,
F16,
BF16,
UE8M0,
UE4M3,
}
const MAX_DIMS: usize = 5;
#[derive(Clone, Copy, Hash, PartialEq, Eq, Serialize, Deserialize)]
pub struct BlockSize {
storage: [u8; MAX_DIMS],
len: u8,
}
impl PartialOrd for BlockSize {
fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for BlockSize {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
(self.len, self.as_slice()).cmp(&(other.len, other.as_slice()))
}
}
impl core::fmt::Debug for BlockSize {
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
write!(f, "BlockSize({:?})", self.as_slice())
}
}
impl BlockSize {
pub const MAX_DIMS: usize = MAX_DIMS;
pub fn new(values: impl AsRef<[u8]>) -> Self {
Self::canonicalize(values.as_ref())
}
fn canonicalize(values: &[u8]) -> Self {
let skip = values
.iter()
.position(|&value| value != 1)
.unwrap_or(values.len());
let values = &values[skip..];
debug_assert!(
values.len() <= MAX_DIMS,
"Tried creating a block size larger than the cap"
);
let len = values.len().min(MAX_DIMS);
let mut storage = [1; MAX_DIMS];
storage[..len].copy_from_slice(&values[..len]);
Self {
storage,
len: len as u8,
}
}
pub fn as_slice(&self) -> &[u8] {
&self.storage[..self.len as usize]
}
pub fn to_vec(&self) -> Vec<u8> {
self.storage[..self.len as usize].to_vec()
}
pub fn as_dim<const N: usize>(&self) -> [u8; N] {
let data_len = N.min(self.len as usize);
let data_start = N - data_len;
let mut out = [1; N];
out[data_start..].copy_from_slice(&self.storage[..data_len]);
out
}
pub fn to_dim_vec(&self, len: usize) -> Vec<u8> {
let data_len = len.min(self.len as usize);
let data_start = len - data_len;
let mut out = vec![1; len];
out[data_start..].copy_from_slice(&self.storage[..data_len]);
out
}
pub fn num_blocks(&self, shape: &[usize]) -> Vec<usize> {
self.to_dim_vec(shape.len())
.into_iter()
.zip(shape)
.map(|(block, &dim)| dim.div_ceil(block as usize))
.collect()
}
pub fn iter(&self) -> impl Iterator<Item = &u8> {
self.as_slice().iter()
}
pub fn num_elements(&self) -> usize {
self.iter().map(|it| *it as usize).product()
}
}
impl Deref for BlockSize {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.as_slice()
}
}
impl<T: AsRef<[u8]>> From<T> for BlockSize {
fn from(value: T) -> Self {
BlockSize::new(value)
}
}
macro_rules! codes {
($ty:ty { $($variant:ident => $code:literal),+ $(,)? }) => {
impl $ty {
fn to_code(self) -> u64 {
match self {
$(Self::$variant => $code,)+
}
}
fn from_code(code: u64) -> Option<Self> {
match code {
$($code => Some(Self::$variant),)+
_ => None,
}
}
}
};
}
codes!(QuantValue {
Q8F => 0,
E5M2 => 1,
E4M3 => 2,
Q4F => 3,
E2M1 => 4,
Q2F => 5,
Q8S => 6,
Q4S => 7,
Q2S => 8,
});
codes!(QuantMode {
Symmetric => 0,
Lookup => 1,
});
codes!(ScaleDtype {
F32 => 1,
F16 => 2,
BF16 => 3,
UE8M0 => 4,
UE4M3 => 5,
});
impl QuantScheme {
pub fn to_code(&self) -> Option<u64> {
let (store, axis) = match self.store {
QuantStore::Native => (0, 0),
QuantStore::PackedNative(axis) => (1, axis),
QuantStore::PackedU32(axis) => (2, axis),
};
if axis >= 64 {
return None;
}
let level = |dtype: Option<ScaleDtype>| dtype.map_or(0, ScaleDtype::to_code);
let sizes = self
.block
.as_ref()
.map_or(&[][..], |block| block.size.as_slice());
if sizes.contains(&0) {
return None;
}
let packed_sizes = sizes
.iter()
.enumerate()
.fold(0u64, |packed, (index, size)| {
packed | (u64::from(*size) << (8 * index))
});
Some(
self.value.to_code()
| (store << 4)
| ((axis as u64) << 6)
| (self.mode.to_code() << 12)
| (level(self.tensor) << 14)
| (level(self.block.map(|block| block.dtype)) << 17)
| ((sizes.len() as u64) << 20)
| (packed_sizes << 23),
)
}
pub fn from_code(code: u64) -> Option<Self> {
let field = |offset: u32, bits: u32| (code >> offset) & ((1 << bits) - 1);
let value = QuantValue::from_code(field(0, 4))?;
let axis = field(6, 6) as usize;
let store = match field(4, 2) {
0 => QuantStore::Native,
1 => QuantStore::PackedNative(axis),
2 => QuantStore::PackedU32(axis),
_ => return None,
};
let mode = QuantMode::from_code(field(12, 2))?;
let level = |bits: u64| match bits {
0 => Some(None),
bits => ScaleDtype::from_code(bits).map(Some),
};
let tensor = level(field(14, 3))?;
let rank = field(20, 3) as usize;
if rank > MAX_DIMS {
return None;
}
let mut sizes = [0; MAX_DIMS];
for (index, size) in sizes[..rank].iter_mut().enumerate() {
*size = field(23 + 8 * index as u32, 8) as u8;
}
let block = level(field(17, 3))?.map(|dtype| BlockScale {
size: BlockSize::new(&sizes[..rank]),
dtype,
});
Some(Self {
value,
store,
mode,
tensor,
block,
})
.filter(|scheme| scheme.to_code() == Some(code))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn blocks_remain_rank_relative() {
assert_ne!(BlockSize::new([32]), BlockSize::new([32, 32]));
assert_eq!(BlockSize::new([32]).to_dim_vec(2), vec![1, 32]);
assert_eq!(BlockSize::new([32, 32]).to_dim_vec(2), vec![32, 32]);
}
#[test]
fn leading_unit_dimensions_canonicalize_away() {
assert_eq!(BlockSize::new([1, 32]), BlockSize::new([32]));
}
#[test]
fn leading_unit_dimensions_beyond_the_cap_still_canonicalize() {
assert_eq!(
BlockSize::new([1, 1, 8, 4, 2, 3]),
BlockSize::new([8, 4, 2, 3])
);
}
#[test]
fn there_is_one_block_per_scale() {
assert_eq!(BlockSize::new([32]).num_blocks(&[8, 64]), vec![8, 2]);
assert_eq!(BlockSize::new([4, 32]).num_blocks(&[8, 64]), vec![2, 2]);
assert_eq!(BlockSize::new([32]).num_blocks(&[4, 8, 64]), vec![4, 8, 2]);
}
#[test]
fn a_partial_block_still_takes_a_scale() {
assert_eq!(BlockSize::new([32]).num_blocks(&[8, 70]), vec![8, 3]);
}
#[test]
fn the_default_scheme_resolves_to_per_tensor_f32() {
let scheme = QuantScheme::default();
assert_eq!(scheme.tensor_scale(), Some(ScaleDtype::F32));
assert_eq!(scheme.block_scale(), None);
assert_eq!(scheme.scale_dtype(), ScaleDtype::F32);
assert_eq!(scheme.block_size(), None);
assert_eq!(scheme.num_levels(), 1);
}
#[test]
fn a_block_level_stands_alone() {
let scheme = QuantScheme::default().per_block([32], ScaleDtype::F16);
assert_eq!(scheme.tensor_scale(), None);
assert_eq!(scheme.scale_dtype(), ScaleDtype::F16);
assert_eq!(scheme.block_size(), Some(BlockSize::new([32])));
assert_eq!(scheme.num_levels(), 1);
}
#[test]
fn both_levels_nest_the_block_inside_the_tensor() {
let scheme = QuantScheme::default()
.per_block([16], ScaleDtype::UE4M3)
.per_tensor(ScaleDtype::F32);
assert_eq!(scheme.scale_dtype(), ScaleDtype::UE4M3);
assert_eq!(scheme.tensor_scale(), Some(ScaleDtype::F32));
assert_eq!(scheme.num_levels(), 2);
}
#[test]
fn levels_set_in_any_order_are_the_same_scheme() {
assert_eq!(
QuantScheme::default()
.per_block([16], ScaleDtype::UE4M3)
.per_tensor(ScaleDtype::F32),
QuantScheme::default()
.per_tensor(ScaleDtype::F32)
.per_block([16], ScaleDtype::UE4M3),
);
}
#[test]
fn swapping_dims_rewrites_the_block_and_leaves_the_tensor_level_alone() {
let mut scheme = QuantScheme::default()
.per_block([4, 32], ScaleDtype::F16)
.per_tensor(ScaleDtype::F32);
scheme.swap_block_dims(2, 0, 1);
assert_eq!(
scheme,
QuantScheme::default()
.per_block([32, 4], ScaleDtype::F16)
.per_tensor(ScaleDtype::F32)
);
let mut per_tensor = QuantScheme::default();
per_tensor.swap_block_dims(2, 0, 1);
assert_eq!(per_tensor, QuantScheme::default());
}
#[test]
fn swapping_dims_canonicalizes_the_block() {
let mut scheme = QuantScheme::default().per_block([32, 1], ScaleDtype::F32);
scheme.swap_block_dims(2, 0, 1);
assert_eq!(scheme.block_size(), Some(BlockSize::new([32])));
}
#[test]
fn permuting_dims_moves_the_packing_dim_and_the_block_with_their_axes() {
let mut scheme = QuantScheme::default()
.with_store(QuantStore::PackedU32(0))
.per_block([1, 2, 4], ScaleDtype::F32);
scheme.permute_dims(&[2, 0, 1]);
assert_eq!(scheme.store, QuantStore::PackedU32(2));
assert_eq!(scheme.block_size(), Some(BlockSize::new([4, 1, 2])));
}
#[test]
fn swapping_dims_moves_the_packing_dim_only_when_its_dim_is_swapped() {
let mut scheme = QuantScheme::default().with_store(QuantStore::PackedU32(0));
scheme.swap_dims(3, 0, 1);
assert_eq!(scheme.store, QuantStore::PackedU32(0));
scheme.swap_dims(3, 1, 2);
assert_eq!(scheme.store, QuantStore::PackedU32(1));
}
#[test]
fn a_scalar_keeps_its_packing_dim() {
let mut scheme = QuantScheme::default().with_store(QuantStore::PackedU32(0));
scheme.reshape_packing_dim(0);
scheme.permute_dims(&[]);
assert_eq!(scheme.store, QuantStore::PackedU32(0));
}
#[test]
fn a_reshape_that_drops_the_packing_dim_packs_innermost() {
let mut scheme = QuantScheme::default().with_store(QuantStore::PackedNative(1));
scheme.reshape_packing_dim(2);
assert_eq!(scheme.store, QuantStore::PackedNative(1));
scheme.reshape_packing_dim(1);
assert_eq!(scheme.store, QuantStore::PackedNative(0));
}
#[test]
fn permuting_dims_rewrites_the_block() {
let mut scheme = QuantScheme::default().per_block([1, 4, 32], ScaleDtype::F16);
scheme.permute_block_dims(3, &[2, 0, 1]);
assert_eq!(scheme.block_size(), Some(BlockSize::new([32, 1, 4])));
}
#[test]
fn round_up_never_lands_below_the_scale() {
for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
for exp in -12..8 {
for step in 1..17 {
let scale = (step as f32 / 16.0) * 2f32.powi(exp);
let up = dtype.round_up(scale);
assert!(
up >= scale,
"{dtype:?}: {up} is below {scale}, which clips the block maximum"
);
}
}
}
}
#[test]
fn round_up_saturates_rather_than_stepping_off_the_top() {
for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
let max = dtype.max_representable();
assert_eq!(dtype.round_up(max), max);
assert!(dtype.round_up(max * 2.0).is_finite());
}
}
#[test]
fn round_up_answers_for_every_param() {
for dtype in [
ScaleDtype::F32,
ScaleDtype::F16,
ScaleDtype::BF16,
ScaleDtype::UE8M0,
ScaleDtype::UE4M3,
] {
let up = dtype.round_up(0.3);
assert!(up.is_finite() && up > 0.0, "{dtype:?} answered {up:e}");
}
}
fn power_of_two(exp: i32) -> f32 {
f32::from_bits(((exp + 127) as u32) << 23)
}
#[test]
fn ue8m0_rounds_up_to_a_power_of_two() {
for exp in -120..120 {
let power = power_of_two(exp);
assert_eq!(ScaleDtype::UE8M0.round_up(power), power, "2^{exp}");
for scale in [power * 1.0001, power * 1.5, power * 1.9999] {
assert_eq!(
ScaleDtype::UE8M0.round_up(scale),
power * 2.0,
"{scale} (2^{exp} scaled)"
);
}
}
}
#[test]
fn ue8m0_saturates_at_both_ends() {
let min = ScaleDtype::UE8M0_MIN;
let max = ScaleDtype::UE8M0_MAX;
for scale in [0.0, f32::MIN_POSITIVE * 0.5, min * 0.5, min] {
assert_eq!(ScaleDtype::UE8M0.round_up(scale), min, "{scale:e}");
}
for scale in [max, max * 2.0, f32::MAX, f32::INFINITY] {
assert_eq!(ScaleDtype::UE8M0.round_up(scale), max, "{scale:e}");
}
}
#[test]
fn ue8m0_round_up_is_idempotent() {
for exp in -130..130 {
for factor in [1.0, 1.3, 1.7] {
let scale = factor * 2f32.powi(exp);
let up = ScaleDtype::UE8M0.round_up(scale);
assert_eq!(
ScaleDtype::UE8M0.round_up(up),
up,
"not idempotent at {scale:e}"
);
assert!(up >= scale.min(ScaleDtype::UE8M0_MAX), "{up:e} < {scale:e}");
}
}
}
#[test]
fn round_up_is_the_identity_for_f32() {
for scale in [1.0e-30, 0.1, 1.0, 12345.678, f32::MAX] {
assert_eq!(ScaleDtype::F32.round_up(scale), scale);
}
}
#[cfg(feature = "fp8")]
mod storage_types {
use super::*;
#[test]
fn round_up_is_the_nearest_representable_value_not_below() {
for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
for exp in -8..6 {
let scale = 1.7 * 2f32.powi(exp);
let up = dtype.round_up(scale);
assert_eq!(
up,
dtype.round_up(up),
"{dtype:?}: not idempotent at {scale}"
);
assert!(
step(dtype, up, -1) < scale,
"{dtype:?}: {up} overshoots {scale} by at least a step"
);
}
}
}
#[test]
fn f32_grid_matches_the_storage_types() {
for dtype in [ScaleDtype::F16, ScaleDtype::BF16, ScaleDtype::UE4M3] {
let grid = dtype.f32_grid();
if let Some(subnormals) = grid.subnormals {
assert_eq!(
subnormals.min_normal,
min_normal(dtype),
"{dtype:?}: minimum normal"
);
assert_eq!(
subnormals.spacing,
step(dtype, 0.0, 1),
"{dtype:?}: subnormal spacing"
);
}
let mut value = min_normal(dtype);
let max = dtype.max_representable();
while value < max {
let stepped = f32::from_bits(value.to_bits() + grid.bit_step);
assert_eq!(
stepped,
step(dtype, value, 1),
"{dtype:?}: step above {value}"
);
value = stepped;
}
assert_eq!(
value, max,
"{dtype:?}: the grid has to land exactly on the maximum"
);
}
}
#[test]
fn max_representable_matches_the_e4m3_type() {
assert_eq!(
ScaleDtype::UE4M3.max_representable(),
crate::e4m3::MAX.to_f32()
);
}
fn step(dtype: ScaleDtype, value: f32, offset: i32) -> f32 {
match dtype {
ScaleDtype::F16 => half::f16::from_bits(
(half::f16::from_f32(value).to_bits() as i32 + offset) as u16,
)
.to_f32(),
ScaleDtype::BF16 => half::bf16::from_bits(
(half::bf16::from_f32(value).to_bits() as i32 + offset) as u16,
)
.to_f32(),
ScaleDtype::UE4M3 => crate::e4m3::from_bits(
(crate::e4m3::from_f32(value).to_bits() as i32 + offset) as u8,
)
.to_f32(),
ScaleDtype::F32 | ScaleDtype::UE8M0 => unreachable!(),
}
}
fn min_normal(dtype: ScaleDtype) -> f32 {
match dtype {
ScaleDtype::F16 => half::f16::MIN_POSITIVE.to_f32(),
ScaleDtype::BF16 => half::bf16::MIN_POSITIVE.to_f32(),
ScaleDtype::UE4M3 => crate::e4m3::MIN_POSITIVE.to_f32(),
ScaleDtype::F32 | ScaleDtype::UE8M0 => unreachable!(),
}
}
}
#[test]
fn every_scheme_round_trips_through_its_code() {
let values: Vec<_> = (0..1 << 4).filter_map(QuantValue::from_code).collect();
let modes: Vec<_> = (0..1 << 2).filter_map(QuantMode::from_code).collect();
let dtypes: Vec<_> = (0..1 << 3).filter_map(ScaleDtype::from_code).collect();
let stores = [
QuantStore::Native,
QuantStore::PackedNative(0),
QuantStore::PackedNative(63),
QuantStore::PackedU32(0),
QuantStore::PackedU32(63),
];
let sizes: [&[u8]; 3] = [&[32], &[255, 1], &[2, 4, 8, 16, 255]];
let mut codes = alloc::collections::BTreeSet::new();
let mut count = 0;
for value in values.iter().copied() {
for store in stores {
for mode in modes.iter().copied() {
let base = QuantScheme::default()
.with_value(value)
.with_store(store)
.with_mode(mode);
let tensors = core::iter::once(base)
.chain(dtypes.iter().map(|dtype| base.per_tensor(*dtype)));
for scheme in tensors {
let blocks =
core::iter::once(scheme).chain(dtypes.iter().flat_map(|dtype| {
sizes
.iter()
.map(move |size| scheme.per_block(*size, *dtype))
}));
for scheme in blocks {
let code = scheme.to_code().unwrap();
assert_eq!(QuantScheme::from_code(code), Some(scheme), "{scheme:?}");
codes.insert(code);
count += 1;
}
}
}
}
}
assert_eq!(codes.len(), count, "two schemes share a code");
}
#[test]
fn a_code_no_scheme_packs_to_is_refused() {
let canonical = QuantScheme::default()
.with_store(QuantStore::PackedU32(1))
.per_block([32], ScaleDtype::F16)
.to_code()
.unwrap();
let native = QuantScheme::default()
.with_store(QuantStore::Native)
.to_code()
.unwrap();
let refused = [
("value 9", (canonical & !0xF) | 9),
("value 15", (canonical & !0xF) | 15),
("store 3", canonical | (3 << 4)),
("mode 2", canonical | (2 << 12)),
("per-tensor level 6", canonical | (6 << 14)),
("block level 7", canonical | (7 << 17)),
("rank 6", (canonical & !(7 << 20)) | (6 << 20)),
("an axis on a native store", native | (5 << 6)),
("a rank with no block level", native | (1 << 20)),
("a size past the rank", canonical | (8 << 31)),
("a block size of zero", canonical & !(0xFF << 23)),
("bit 63", canonical | (1 << 63)),
];
for (case, code) in refused {
assert_eq!(QuantScheme::from_code(code), None, "{case}");
}
}
#[test]
fn a_scheme_the_layout_cannot_hold_has_no_code() {
let unheld = [
(
"a packing axis of 64",
QuantScheme::default().with_store(QuantStore::PackedU32(64)),
),
(
"a block size of zero",
QuantScheme::default().per_block([0, 16], ScaleDtype::F16),
),
];
for (case, scheme) in unheld {
assert_eq!(scheme.to_code(), None, "{case}");
}
}
}