use crate::NodeId;
use std::collections::HashMap;
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum QuantScheme {
Int8Block { block_size: u32 },
Int8BlockAsym { block_size: u32 },
Int4Block { block_size: u32 },
Fp8E4m3,
Fp8E5m2,
GgufQ4K,
GgufQ5K,
GgufQ6K,
GgufQ8K,
GgufQ2K,
GgufQ3K,
GgufQ4_0,
GgufQ4_1,
GgufQ5_0,
GgufQ5_1,
GgufQ8_0,
Nvfp4Block,
GgufIQ4NL,
GgufIQ4XS,
GgufIQ2XXS,
GgufIQ2XS,
GgufIQ2S,
GgufIQ3XXS,
GgufIQ3S,
GgufIQ1S,
GgufIQ1M,
GgufTQ1_0,
GgufTQ2_0,
GgufMXFP4,
GgufNVFP4,
GgufQ1_0,
GgufQ2_0,
}
macro_rules! define_gguf_gpu_dequant_ids {
($(($variant:ident, $id:literal)),+ $(,)?) => {
pub const fn gpu_dequant_scheme_id(self) -> Option<u32> {
match self {
$(Self::$variant => Some($id),)+
_ => None,
}
}
pub const fn from_gpu_dequant_scheme_id(id: u32) -> Option<Self> {
match id {
$($id => Some(Self::$variant),)+
_ => None,
}
}
#[cfg(test)]
pub(crate) const GPU_DEQUANT_SCHEME_ID_PAIRS: &'static [(Self, u32)] = &[
$((Self::$variant, $id),)+
];
};
}
impl QuantScheme {
pub const fn bits_per_element_x10(self) -> u32 {
match self {
Self::Int8Block { .. } | Self::Int8BlockAsym { .. } => 80,
Self::Int4Block { .. } => 40,
Self::Fp8E4m3 | Self::Fp8E5m2 => 80,
Self::GgufQ4K => 45, Self::GgufQ5K => 55, Self::GgufQ6K => 66, Self::GgufQ8K => 91, Self::GgufQ2K => 26, Self::GgufQ3K => 34, Self::GgufQ4_0 => 45, Self::GgufQ4_1 => 50, Self::GgufQ5_0 => 55, Self::GgufQ5_1 => 60, Self::GgufQ8_0 => 85, Self::Nvfp4Block => 40,
Self::GgufIQ4NL => 45,
Self::GgufIQ4XS => 42, Self::GgufIQ2XXS => 20,
Self::GgufIQ2XS => 23,
Self::GgufIQ2S => 25,
Self::GgufIQ3XXS => 30,
Self::GgufIQ3S => 34,
Self::GgufIQ1S => 15,
Self::GgufIQ1M => 17,
Self::GgufTQ1_0 => 16, Self::GgufTQ2_0 => 20,
Self::GgufMXFP4 => 42,
Self::GgufNVFP4 => 45,
Self::GgufQ1_0 => 11, Self::GgufQ2_0 => 21, }
}
pub const fn bits_per_element(self) -> u32 {
self.bits_per_element_x10() / 10
}
define_gguf_gpu_dequant_ids! {
(GgufQ4K, 0),
(GgufQ5K, 1),
(GgufQ6K, 2),
(GgufQ8K, 3),
(GgufQ2K, 4),
(GgufQ3K, 5),
(GgufIQ4NL, 6),
(GgufIQ4XS, 7),
(GgufTQ1_0, 8),
(GgufTQ2_0, 9),
(GgufMXFP4, 10),
(GgufNVFP4, 11),
(GgufIQ2XXS, 12),
(GgufIQ2XS, 13),
(GgufIQ2S, 14),
(GgufIQ3XXS, 15),
(GgufIQ3S, 16),
(GgufIQ1S, 17),
(GgufIQ1M, 18),
(GgufQ4_0, 19),
(GgufQ8_0, 20),
(GgufQ4_1, 21),
(GgufQ5_0, 22),
(GgufQ5_1, 23),
(GgufQ1_0, 24),
(GgufQ2_0, 25),
}
pub const fn has_scale(self) -> bool {
matches!(
self,
Self::Int8Block { .. }
| Self::Int8BlockAsym { .. }
| Self::Int4Block { .. }
| Self::Nvfp4Block
)
}
pub const fn scale_is_fp8(self) -> bool {
matches!(self, Self::Nvfp4Block)
}
pub const fn nvfp4_group_size(self) -> u32 {
match self {
Self::Nvfp4Block => crate::nvfp4::NVFP4_GROUP_SIZE as u32,
_ => 0,
}
}
pub const fn has_zero_point(self) -> bool {
matches!(self, Self::Int8BlockAsym { .. })
}
pub const fn gguf_block_size(self) -> u32 {
match self {
Self::GgufQ4K
| Self::GgufQ5K
| Self::GgufQ6K
| Self::GgufQ8K
| Self::GgufQ2K
| Self::GgufQ3K
| Self::GgufIQ4XS
| Self::GgufIQ2XXS
| Self::GgufIQ2XS
| Self::GgufIQ2S
| Self::GgufIQ3XXS
| Self::GgufIQ3S
| Self::GgufIQ1S
| Self::GgufIQ1M
| Self::GgufTQ1_0
| Self::GgufTQ2_0 => 256,
Self::GgufQ4_0
| Self::GgufQ4_1
| Self::GgufQ5_0
| Self::GgufQ5_1
| Self::GgufQ8_0
| Self::GgufIQ4NL
| Self::GgufMXFP4 => 32,
Self::GgufNVFP4 => 16,
Self::GgufQ1_0 => 128,
Self::GgufQ2_0 => 128,
_ => 0,
}
}
pub const fn gguf_block_bytes(self) -> u32 {
match self {
Self::GgufQ4K => 144, Self::GgufQ5K => 176, Self::GgufQ6K => 210, Self::GgufQ8K => 292, Self::GgufQ2K => 84, Self::GgufQ3K => 110, Self::GgufQ4_0 => 18, Self::GgufQ4_1 => 20, Self::GgufQ5_0 => 22, Self::GgufQ5_1 => 24, Self::GgufQ8_0 => 34, Self::GgufIQ4NL => 18,
Self::GgufIQ4XS => 136,
Self::GgufIQ2XXS => 66,
Self::GgufIQ2XS => 74,
Self::GgufIQ2S => 82,
Self::GgufIQ3XXS => 98,
Self::GgufIQ3S => 110,
Self::GgufIQ1S => 50,
Self::GgufIQ1M => 56,
Self::GgufTQ1_0 => 54,
Self::GgufTQ2_0 => 66,
Self::GgufMXFP4 => 17,
Self::GgufNVFP4 => 9,
Self::GgufQ1_0 => 18, Self::GgufQ2_0 => 34, _ => 0,
}
}
pub const fn is_gguf(self) -> bool {
matches!(
self,
Self::GgufQ4K
| Self::GgufQ5K
| Self::GgufQ6K
| Self::GgufQ8K
| Self::GgufQ2K
| Self::GgufQ3K
| Self::GgufQ4_0
| Self::GgufQ4_1
| Self::GgufQ5_0
| Self::GgufQ5_1
| Self::GgufQ8_0
| Self::GgufIQ4NL
| Self::GgufIQ4XS
| Self::GgufIQ2XXS
| Self::GgufIQ2XS
| Self::GgufIQ2S
| Self::GgufIQ3XXS
| Self::GgufIQ3S
| Self::GgufIQ1S
| Self::GgufIQ1M
| Self::GgufTQ1_0
| Self::GgufTQ2_0
| Self::GgufMXFP4
| Self::GgufNVFP4
| Self::GgufQ1_0
| Self::GgufQ2_0
)
}
}
impl std::fmt::Display for QuantScheme {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Int8Block { block_size } => write!(f, "int8/{block_size}"),
Self::Int8BlockAsym { block_size } => write!(f, "int8a/{block_size}"),
Self::Int4Block { block_size } => write!(f, "int4/{block_size}"),
Self::Fp8E4m3 => write!(f, "fp8e4m3"),
Self::Fp8E5m2 => write!(f, "fp8e5m2"),
Self::GgufQ4K => write!(f, "gguf_q4k"),
Self::GgufQ5K => write!(f, "gguf_q5k"),
Self::GgufQ6K => write!(f, "gguf_q6k"),
Self::GgufQ8K => write!(f, "gguf_q8k"),
Self::GgufQ2K => write!(f, "gguf_q2k"),
Self::GgufQ3K => write!(f, "gguf_q3k"),
Self::GgufQ4_0 => write!(f, "gguf_q4_0"),
Self::GgufQ4_1 => write!(f, "gguf_q4_1"),
Self::GgufQ5_0 => write!(f, "gguf_q5_0"),
Self::GgufQ5_1 => write!(f, "gguf_q5_1"),
Self::GgufQ8_0 => write!(f, "gguf_q8_0"),
Self::Nvfp4Block => write!(f, "nvfp4/16"),
Self::GgufIQ4NL => write!(f, "gguf_iq4_nl"),
Self::GgufIQ4XS => write!(f, "gguf_iq4_xs"),
Self::GgufIQ2XXS => write!(f, "gguf_iq2_xxs"),
Self::GgufIQ2XS => write!(f, "gguf_iq2_xs"),
Self::GgufIQ2S => write!(f, "gguf_iq2_s"),
Self::GgufIQ3XXS => write!(f, "gguf_iq3_xxs"),
Self::GgufIQ3S => write!(f, "gguf_iq3_s"),
Self::GgufIQ1S => write!(f, "gguf_iq1_s"),
Self::GgufIQ1M => write!(f, "gguf_iq1_m"),
Self::GgufTQ1_0 => write!(f, "gguf_tq1_0"),
Self::GgufTQ2_0 => write!(f, "gguf_tq2_0"),
Self::GgufMXFP4 => write!(f, "gguf_mxfp4"),
Self::GgufNVFP4 => write!(f, "gguf_nvfp4"),
Self::GgufQ1_0 => write!(f, "gguf_q1_0"),
Self::GgufQ2_0 => write!(f, "gguf_q2_0"),
}
}
}
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScaledFormat {
F8E4M3,
F8E5M2,
F8E4M3Fnuz,
F8E5M2Fnuz,
F6E2M3,
F6E3M2,
F4E2M1,
Custom {
exp_bits: u8,
mant_bits: u8,
bias: i8,
},
}
impl ScaledFormat {
pub const fn bit_width(self) -> u32 {
match self {
Self::F8E4M3 | Self::F8E5M2 | Self::F8E4M3Fnuz | Self::F8E5M2Fnuz => 8,
Self::F6E2M3 | Self::F6E3M2 => 6,
Self::F4E2M1 => 4,
Self::Custom {
exp_bits,
mant_bits,
..
} => 1 + exp_bits as u32 + mant_bits as u32,
}
}
pub const fn fields(self) -> (u32, u32, i32) {
match self {
Self::F8E4M3 => (4, 3, 7),
Self::F8E5M2 => (5, 2, 15),
Self::F8E4M3Fnuz => (4, 3, 8),
Self::F8E5M2Fnuz => (5, 2, 16),
Self::F6E2M3 => (2, 3, 1),
Self::F6E3M2 => (3, 2, 3),
Self::F4E2M1 => (2, 1, 1),
Self::Custom {
exp_bits,
mant_bits,
bias,
} => (exp_bits as u32, mant_bits as u32, bias as i32),
}
}
pub const fn custom(exp_bits: u8, mant_bits: u8) -> Self {
assert!(exp_bits >= 1, "minifloat needs at least one exponent bit");
assert!(
1 + exp_bits + mant_bits <= 8,
"minifloat code must fit in a byte (1 + exp + mant <= 8)"
);
let bias = (1i32 << (exp_bits - 1)) - 1;
Self::Custom {
exp_bits,
mant_bits,
bias: bias as i8,
}
}
pub const fn custom_with_bias(exp_bits: u8, mant_bits: u8, bias: i8) -> Self {
assert!(exp_bits >= 1, "minifloat needs at least one exponent bit");
assert!(
1 + exp_bits + mant_bits <= 8,
"minifloat code must fit in a byte (1 + exp + mant <= 8)"
);
Self::Custom {
exp_bits,
mant_bits,
bias,
}
}
pub const NAMED: [ScaledFormat; 7] = [
Self::F8E4M3,
Self::F8E5M2,
Self::F8E4M3Fnuz,
Self::F8E5M2Fnuz,
Self::F6E2M3,
Self::F6E3M2,
Self::F4E2M1,
];
pub const fn exp_bits(self) -> u32 {
self.fields().0
}
pub const fn mant_bits(self) -> u32 {
self.fields().1
}
pub const fn bias(self) -> i32 {
self.fields().2
}
pub const fn is_custom(self) -> bool {
matches!(self, Self::Custom { .. })
}
pub const fn is_named(self) -> bool {
!self.is_custom()
}
pub const fn is_fnuz(self) -> bool {
matches!(self, Self::F8E4M3Fnuz | Self::F8E5M2Fnuz)
}
pub const fn has_inf(self) -> bool {
matches!(self, Self::F8E5M2)
}
pub fn max_finite(self) -> f32 {
crate::lowp_codec::max_finite(self)
}
pub fn decode(self, code: u8) -> f32 {
crate::lowp_codec::decode(self, code)
}
pub fn encode(self, x: f32) -> u8 {
crate::lowp_codec::encode(self, x)
}
pub fn quantize(self, x: f32) -> f32 {
self.decode(self.encode(x))
}
pub fn representable_values(self) -> Vec<f32> {
let n = 1u16 << self.bit_width();
let mut v: Vec<f32> = (0..n)
.map(|c| self.decode(c as u8))
.filter(|x| x.is_finite())
.collect();
v.sort_by(|a, b| a.partial_cmp(b).unwrap());
v.dedup();
v
}
pub const fn kernel_id(self) -> u32 {
match self {
Self::F8E4M3 => 0,
Self::F8E5M2 => 1,
Self::F8E4M3Fnuz => 2,
Self::F8E5M2Fnuz => 3,
Self::F6E2M3 => 4,
Self::F6E3M2 => 5,
Self::F4E2M1 => 6,
Self::Custom {
exp_bits,
mant_bits,
bias,
} => {
0x8000_0000
| (exp_bits as u32 & 0xF)
| ((mant_bits as u32 & 0xF) << 4)
| (((bias as u8) as u32) << 8)
}
}
}
pub const fn is_native_fp8(self) -> bool {
matches!(self, Self::F8E4M3 | Self::F8E5M2)
}
}
impl std::fmt::Display for ScaledFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::F8E4M3 => "f8e4m3",
Self::F8E5M2 => "f8e5m2",
Self::F8E4M3Fnuz => "f8e4m3fnuz",
Self::F8E5M2Fnuz => "f8e5m2fnuz",
Self::F6E2M3 => "f6e2m3",
Self::F6E3M2 => "f6e3m2",
Self::F4E2M1 => "f4e2m1",
Self::Custom {
exp_bits,
mant_bits,
..
} => {
return write!(
f,
"f{}e{}m{}",
1 + exp_bits + mant_bits,
exp_bits,
mant_bits
);
}
};
f.write_str(s)
}
}
impl std::str::FromStr for ScaledFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"f8e4m3" => return Ok(Self::F8E4M3),
"f8e5m2" => return Ok(Self::F8E5M2),
"f8e4m3fnuz" => return Ok(Self::F8E4M3Fnuz),
"f8e5m2fnuz" => return Ok(Self::F8E5M2Fnuz),
"f6e2m3" => return Ok(Self::F6E2M3),
"f6e3m2" => return Ok(Self::F6E3M2),
"f4e2m1" => return Ok(Self::F4E2M1),
_ => {}
}
let err = || format!("invalid minifloat format {s:?} (expected e.g. \"f4e3m0\")");
let rest = s.strip_prefix('f').ok_or_else(err)?;
let (total, rest) = take_leading_u32(rest).ok_or_else(err)?;
let rest = rest.strip_prefix('e').ok_or_else(err)?;
let (exp, rest) = take_leading_u32(rest).ok_or_else(err)?;
let rest = rest.strip_prefix('m').ok_or_else(err)?;
let (mant, rest) = take_leading_u32(rest).ok_or_else(err)?;
if !rest.is_empty() || exp == 0 || total != 1 + exp + mant || 1 + exp + mant > 8 {
return Err(err());
}
Ok(Self::custom(exp as u8, mant as u8))
}
}
fn take_leading_u32(s: &str) -> Option<(u32, &str)> {
let end = s.find(|c: char| !c.is_ascii_digit()).unwrap_or(s.len());
if end == 0 {
return None;
}
Some((s[..end].parse().ok()?, &s[end..]))
}
#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ScaleLayout {
PerTensor,
BlockMxE8M0 { block: u32 },
Nvfp4 { group: u32 },
}
impl ScaleLayout {
pub const fn mx() -> Self {
Self::BlockMxE8M0 { block: 32 }
}
pub fn nvfp4() -> Self {
Self::Nvfp4 {
group: crate::nvfp4::NVFP4_GROUP_SIZE as u32,
}
}
pub const fn scale_dtype(self) -> crate::DType {
match self {
Self::PerTensor => crate::DType::F32,
Self::BlockMxE8M0 { .. } | Self::Nvfp4 { .. } => crate::DType::U8,
}
}
pub const fn block(self) -> u32 {
match self {
Self::PerTensor => 1,
Self::BlockMxE8M0 { block } => block,
Self::Nvfp4 { group } => group,
}
}
pub const fn mode_block(self) -> (u32, u32) {
match self {
Self::PerTensor => (0, 1),
Self::BlockMxE8M0 { block } => (1, block),
Self::Nvfp4 { group } => (2, group),
}
}
}
impl std::fmt::Display for ScaleLayout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::PerTensor => write!(f, "per_tensor"),
Self::BlockMxE8M0 { block } => write!(f, "mx_e8m0/{block}"),
Self::Nvfp4 { group } => write!(f, "nvfp4/{group}"),
}
}
}
impl std::str::FromStr for ScaleLayout {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"per_tensor" | "pertensor" => return Ok(Self::PerTensor),
"mx" | "mxfp8" | "block" => return Ok(Self::mx()),
"nvfp4" => return Ok(Self::nvfp4()),
_ => {}
}
if let Some(rest) = s.strip_prefix("mx/").or_else(|| s.strip_prefix("mx_e8m0/")) {
if let Ok(block) = rest.parse::<u32>() {
return Ok(Self::BlockMxE8M0 { block });
}
}
if let Some(rest) = s.strip_prefix("nvfp4/") {
if let Ok(group) = rest.parse::<u32>() {
return Ok(Self::Nvfp4 { group });
}
}
Err(format!(
"unknown scale layout {s:?} (expected \"per_tensor\", \"mx\", \"nvfp4\", or \"mx/<block>\")"
))
}
}
#[derive(Debug, Clone, Default)]
pub struct QuantMap {
map: HashMap<NodeId, QuantScheme>,
}
impl QuantMap {
pub fn new() -> Self {
Self::default()
}
pub fn get(&self, id: NodeId) -> Option<QuantScheme> {
self.map.get(&id).copied()
}
pub fn insert(&mut self, id: NodeId, scheme: QuantScheme) -> Option<QuantScheme> {
self.map.insert(id, scheme)
}
pub fn is_empty(&self) -> bool {
self.map.is_empty()
}
pub fn len(&self) -> usize {
self.map.len()
}
pub fn iter(&self) -> impl Iterator<Item = (&NodeId, &QuantScheme)> {
self.map.iter()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scheme_traits() {
assert_eq!(
QuantScheme::Int4Block { block_size: 32 }.bits_per_element(),
4
);
assert!(QuantScheme::Int8BlockAsym { block_size: 64 }.has_zero_point());
assert!(!QuantScheme::Fp8E4m3.has_scale());
}
#[test]
fn gpu_dequant_scheme_id_is_stable() {
use QuantScheme::*;
assert_eq!(QuantScheme::GPU_DEQUANT_SCHEME_ID_PAIRS.len(), 26);
for &(scheme, id) in QuantScheme::GPU_DEQUANT_SCHEME_ID_PAIRS {
assert_eq!(scheme.gpu_dequant_scheme_id(), Some(id), "{scheme:?}");
assert_eq!(
QuantScheme::from_gpu_dequant_scheme_id(id),
Some(scheme),
"id {id}"
);
}
for scheme in [
Int8Block { block_size: 32 },
Int8BlockAsym { block_size: 32 },
Int4Block { block_size: 32 },
Fp8E4m3,
Fp8E5m2,
Nvfp4Block,
] {
assert_eq!(scheme.gpu_dequant_scheme_id(), None, "{scheme:?}");
}
assert_eq!(QuantScheme::from_gpu_dequant_scheme_id(999), None);
}
#[test]
fn quant_map_lookup() {
let mut q = QuantMap::new();
let id = NodeId(7);
q.insert(id, QuantScheme::Int8Block { block_size: 32 });
assert_eq!(q.get(id), Some(QuantScheme::Int8Block { block_size: 32 }));
assert_eq!(q.get(NodeId(99)), None);
}
#[test]
fn custom_format_ieee_bias_and_width() {
assert_eq!(ScaledFormat::custom(3, 0).fields(), (3, 0, 3)); assert_eq!(ScaledFormat::custom(4, 3).fields(), (4, 3, 7)); assert_eq!(ScaledFormat::custom(5, 2).fields(), (5, 2, 15)); assert_eq!(ScaledFormat::custom(2, 1).fields(), (2, 1, 1)); assert_eq!(ScaledFormat::custom(3, 0).bit_width(), 4);
assert_eq!(ScaledFormat::custom(3, 4).bit_width(), 8);
}
#[test]
fn custom_format_parse_display_round_trip() {
let f: ScaledFormat = "f4e3m0".parse().unwrap();
assert_eq!(f, ScaledFormat::custom(3, 0));
assert_eq!(f.to_string(), "f4e3m0");
assert_eq!(
"f5e2m2".parse::<ScaledFormat>().unwrap().to_string(),
"f5e2m2"
);
assert_eq!(
"f8e3m4".parse::<ScaledFormat>().unwrap().to_string(),
"f8e3m4"
);
assert_eq!(
"f8e4m3".parse::<ScaledFormat>().unwrap(),
ScaledFormat::F8E4M3
);
assert_eq!(
"f4e2m1".parse::<ScaledFormat>().unwrap(),
ScaledFormat::F4E2M1
);
assert_eq!(
"f8e5m2fnuz".parse::<ScaledFormat>().unwrap(),
ScaledFormat::F8E5M2Fnuz
);
}
#[test]
fn custom_format_parse_rejects_invalid() {
assert!("f4e3m1".parse::<ScaledFormat>().is_err()); assert!("f9e4m4".parse::<ScaledFormat>().is_err()); assert!("f1e0m0".parse::<ScaledFormat>().is_err()); assert!("e3m0".parse::<ScaledFormat>().is_err()); assert!("f4e3m0x".parse::<ScaledFormat>().is_err()); assert!("garbage".parse::<ScaledFormat>().is_err());
}
#[test]
fn custom_gpu_word_is_packed_descriptor() {
assert_eq!(ScaledFormat::F8E4M3.kernel_id(), 0);
assert_eq!(ScaledFormat::F4E2M1.kernel_id(), 6);
assert!(ScaledFormat::F4E2M1.kernel_id() & 0x8000_0000 == 0);
let w = ScaledFormat::custom(3, 0).kernel_id(); assert_eq!(w & 0x8000_0000, 0x8000_0000);
assert_eq!(w & 0xF, 3); assert_eq!((w >> 4) & 0xF, 0); assert_eq!((w >> 8) & 0xFF, 3); let wn = ScaledFormat::custom_with_bias(3, 0, -2).kernel_id();
assert_eq!(((wn >> 8) & 0xFF) as u8 as i8, -2);
}
#[test]
fn format_is_const_and_introspectable() {
const F: ScaledFormat = ScaledFormat::custom(3, 0);
assert!(F.is_custom() && !F.is_named());
assert_eq!((F.exp_bits(), F.mant_bits(), F.bias()), (3, 0, 3));
assert!(ScaledFormat::F8E4M3.is_named() && !ScaledFormat::F8E4M3.is_custom());
assert_eq!(ScaledFormat::NAMED.len(), 7);
assert!(ScaledFormat::NAMED.iter().all(|f| f.is_named()));
}
#[test]
fn format_codec_methods_and_quantize() {
for f in ScaledFormat::NAMED {
for c in 0..(1u16 << f.bit_width()) {
assert_eq!(
f.decode(c as u8).to_bits(),
crate::lowp_codec::decode(f, c as u8).to_bits()
);
}
}
let cf = ScaledFormat::custom(3, 0); assert_eq!(cf.quantize(1.4), 1.0);
assert_eq!(cf.quantize(1.6), 2.0);
assert_eq!(cf.quantize(-3.0), -2.0); assert_eq!(cf.decode(cf.encode(100.0)), 16.0); assert_eq!(cf.decode(cf.encode(f32::INFINITY)), 16.0);
}
#[test]
fn representable_values_grid() {
assert_eq!(
ScaledFormat::custom(3, 0).representable_values(),
vec![
-16.0, -8.0, -4.0, -2.0, -1.0, -0.5, -0.25, 0.0, 0.25, 0.5, 1.0, 2.0, 4.0, 8.0,
16.0
]
);
for f in ScaledFormat::NAMED {
let g = f.representable_values();
assert!(!g.is_empty() && g.windows(2).all(|w| w[0] < w[1]));
}
}
#[test]
fn enumerate_all_fnexmy() {
let mut all = vec![];
for exp in 1u8..=7 {
for mant in 0u8..=(7 - exp) {
all.push(ScaledFormat::custom(exp, mant));
}
}
assert_eq!(all.len(), 28, "there are exactly 28 fNeXmY formats");
eprintln!(
"{:<8} {:>4} {:>3} {:>3} {:>4} {:>12} {:>12} {:>5}",
"name", "bits", "e", "m", "bias", "max_finite", "min_pos", "vals"
);
for f in &all {
let vals = f.representable_values();
let min_pos = vals.iter().copied().find(|&x| x > 0.0).unwrap();
assert!(f.bit_width() <= 8 && f.exp_bits() >= 1 && !vals.is_empty());
eprintln!(
"{:<8} {:>4} {:>3} {:>3} {:>4} {:>12.3e} {:>12.3e} {:>5}",
f.to_string(),
f.bit_width(),
f.exp_bits(),
f.mant_bits(),
f.bias(),
f.max_finite(),
min_pos,
vals.len()
);
}
}
#[test]
fn scale_layout_parse_round_trip() {
assert_eq!(
"per_tensor".parse::<ScaleLayout>().unwrap(),
ScaleLayout::PerTensor
);
assert_eq!("mx".parse::<ScaleLayout>().unwrap(), ScaleLayout::mx());
assert_eq!(
"nvfp4".parse::<ScaleLayout>().unwrap(),
ScaleLayout::nvfp4()
);
assert_eq!(
"mx/64".parse::<ScaleLayout>().unwrap(),
ScaleLayout::BlockMxE8M0 { block: 64 }
);
assert!("bogus".parse::<ScaleLayout>().is_err());
let mx = ScaleLayout::mx();
assert_eq!(mx.to_string().parse::<ScaleLayout>().unwrap(), mx);
}
}