#[inline]
pub fn quantize(x: f32, exp_bits: u32, man_bits: u32, max_normal: f32) -> f32 {
if x == 0.0 || x.is_nan() {
return x;
}
let neg = x.is_sign_negative();
let ax = x.abs();
if ax >= max_normal {
return if neg { -max_normal } else { max_normal };
}
let bias = (1i32 << (exp_bits - 1)) - 1;
let emin = 1 - bias; let e = ax.log2().floor() as i32;
let e_eff = e.max(emin);
let ulp = exp2i(e_eff - man_bits as i32);
let q = round_half_even(ax / ulp) * ulp;
let q = q.min(max_normal);
if neg { -q } else { q }
}
#[inline]
fn exp2i(n: i32) -> f32 {
2f32.powi(n.clamp(-149, 127))
}
#[inline]
fn round_half_even(v: f32) -> f32 {
let f = v.floor();
let diff = v - f;
if diff < 0.5 {
f
} else if diff > 0.5 {
f + 1.0
} else if (f as i64) & 1 == 0 {
f } else {
f + 1.0
}
}
#[inline]
pub fn quantize_slice(xs: &mut [f32], exp_bits: u32, man_bits: u32, max_normal: f32) {
for x in xs {
*x = quantize(*x, exp_bits, man_bits, max_normal);
}
}
#[inline]
pub fn quantize_slice_scaled(xs: &mut [f32], exp_bits: u32, man_bits: u32, max_normal: f32) {
let absmax = xs.iter().fold(0f32, |a, &x| a.max(x.abs()));
if absmax == 0.0 {
return;
}
let scale = absmax / max_normal;
for x in xs {
*x = quantize(*x / scale, exp_bits, man_bits, max_normal) * scale;
}
}
pub trait FloatFormat: Copy {
const EXP_BITS: u32;
const MAN_BITS: u32;
const MAX_NORMAL: f32;
const NAME: &'static str;
const BITS: u32 = 1 + Self::EXP_BITS + Self::MAN_BITS;
#[inline]
fn quantize(x: f32) -> f32 {
quantize(x, Self::EXP_BITS, Self::MAN_BITS, Self::MAX_NORMAL)
}
}
#[macro_export]
macro_rules! float_format {
($($(#[$m:meta])* $name:ident = ($exp:expr, $man:expr, $max:expr, $spec:literal);)*) => {
$(
$(#[$m])*
#[derive(Clone, Copy, Debug, Default)]
pub struct $name;
impl $crate::lowp::FloatFormat for $name {
const EXP_BITS: u32 = $exp;
const MAN_BITS: u32 = $man;
const MAX_NORMAL: f32 = $max;
const NAME: &'static str = $spec;
}
)*
pub const BUILTIN: &[(&str, u32, u32, f32)] = &[ $(($spec, $exp, $man, $max)),* ];
};
}
float_format! {
F16 = (5, 10, 65504.0, "f16");
Bf16 = (8, 7, 3.3895314e38, "bf16");
F8E4M3 = (4, 3, 448.0, "f8e4m3");
F8E5M2 = (5, 2, 57344.0, "bf8");
Nvf4 = (2, 1, 6.0, "nvf4");
F6E3M2 = (3, 2, 28.0, "f6e3m2");
}
pub fn parse_format(spec: &str) -> Option<(u32, u32, f32)> {
let s = spec.trim().to_ascii_lowercase();
for (name, e, m, max) in BUILTIN {
if s == *name {
return Some((*e, *m, *max));
}
}
match s.as_str() {
"fp8" | "f8" | "e4m3" => return Some((4, 3, 448.0)),
"e5m2" => return Some((5, 2, 57344.0)),
"fp4" | "f4" | "e2m1" => return Some((2, 1, 6.0)),
"half" => return Some((5, 10, 65504.0)),
_ => {}
}
let rest = s.strip_prefix('f')?;
let (x_str, rest) = rest.split_once('m')?;
let (y_str, z_str) = rest.split_once('e')?;
let total: u32 = x_str.parse().ok()?;
let man: u32 = y_str.parse().ok()?;
let exp: u32 = z_str.parse().ok()?;
if exp == 0 || 1 + man + exp != total {
return None; }
let bias = (1i32 << (exp - 1)) - 1;
let max = (2.0 - exp2i(-(man as i32))) * exp2i(bias);
Some((exp, man, max))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exact_values_pass_through() {
for &(e, m, max) in &[(5u32, 10u32, 65504.0f32), (4, 3, 448.0), (2, 1, 6.0)] {
for &v in &[0.0, 1.0, -1.0, 0.5, 2.0, -4.0] {
assert_eq!(quantize(v, e, m, max), v, "fmt e{e}m{m} v{v}");
}
}
}
#[test]
fn saturates_no_inf() {
assert_eq!(quantize(1e30, 4, 3, 448.0), 448.0);
assert_eq!(quantize(-1e30, 4, 3, 448.0), -448.0);
assert_eq!(quantize(f32::INFINITY, 5, 10, 65504.0), 65504.0);
assert_eq!(quantize(100.0, 2, 1, 6.0), 6.0); }
#[test]
fn nvf4_grid_is_exact() {
for &v in &[0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0] {
assert_eq!(quantize(v, 2, 1, 6.0), v, "nvf4 {v}");
assert_eq!(quantize(-v, 2, 1, 6.0), -v);
}
assert_eq!(quantize(5.0, 2, 1, 6.0), 4.0);
assert_eq!(quantize(2.5, 2, 1, 6.0), 2.0);
}
#[test]
fn rounds_toward_nearest() {
assert_eq!(quantize(1.0 + 2f32.powi(-11), 5, 10, 65504.0), 1.0);
assert_eq!(
quantize(1.0 + 2f32.powi(-9), 5, 10, 65504.0),
1.0 + 2f32.powi(-9)
);
assert_eq!(quantize(1.1, 4, 3, 448.0), 1.125);
}
#[test]
fn parse_named_and_generic() {
assert_eq!(parse_format("nvf4"), Some((2, 1, 6.0)));
assert_eq!(parse_format("bf8"), Some((5, 2, 57344.0)));
assert_eq!(parse_format("f8e4m3"), Some((4, 3, 448.0)));
assert_eq!(parse_format("f8m3e4").map(|(e, m, _)| (e, m)), Some((4, 3)));
assert_eq!(parse_format("f4m1e2").map(|(e, m, _)| (e, m)), Some((2, 1)));
assert_eq!(
parse_format("f16m10e5").map(|(e, m, _)| (e, m)),
Some((5, 10))
);
assert_eq!(parse_format("f8m4e4"), None); assert_eq!(parse_format("nonsense"), None);
}
#[test]
fn scaled_uses_full_range() {
let mut unscaled = vec![0.02f32, -0.05, 0.1, -0.01];
quantize_slice(&mut unscaled, 2, 1, 6.0);
assert!(unscaled.iter().all(|&x| x == 0.0), "{unscaled:?}");
let mut scaled = vec![0.02f32, -0.05, 0.1, -0.01];
quantize_slice_scaled(&mut scaled, 2, 1, 6.0);
assert!(scaled.iter().any(|&x| x != 0.0), "{scaled:?}");
assert!(
(scaled[2] - 0.1).abs() < 1e-6,
"absmax preserved: {}",
scaled[2]
);
}
#[test]
fn format_trait_consts() {
assert_eq!(Nvf4::BITS, 4);
assert_eq!(F8E4M3::BITS, 8);
assert_eq!(F16::BITS, 16);
assert_eq!(F8E4M3::quantize(1.1), 1.125);
}
}