use half::f16;
pub(crate) const E4M3_MAX: f32 = 448.0;
pub(crate) const E2M1_MAX: f32 = 6.0;
pub(crate) fn e4m3_from_f32(x: f32) -> u8 {
const FP8_MAX: u32 = 543 << 21;
const DENORM_MASK: u32 = 141 << 23;
let mut bits = x.to_bits();
let sign = bits & 0x8000_0000;
bits ^= sign;
let code = if bits >= FP8_MAX {
0x7E
} else if bits < (121 << 23) {
let sum = f32::from_bits(bits) + f32::from_bits(DENORM_MASK);
sum.to_bits().wrapping_sub(DENORM_MASK) as u8
} else {
let odd = (bits >> 20) & 1;
bits = bits.wrapping_add((7u32.wrapping_sub(127) << 23).wrapping_add(0x7FFFF));
bits = bits.wrapping_add(odd);
(bits >> 20) as u8
};
code | (sign >> 24) as u8
}
pub(crate) fn e4m3_to_f32(code: u8) -> f32 {
let v = u16::from(code & 127);
let sign = u16::from(code >> 7) << 15;
let u = (v << 7) | (((v + 1) >> 7) << 14) | sign;
f16::from_f32(f16::from_bits(u).to_f32() * 256.0).to_f32()
}
pub(crate) fn e2m1_from_f32(x: f32) -> u8 {
if x.is_nan() {
return 0x7;
}
let sign = if x.is_sign_negative() { 0x8 } else { 0x0 };
let a = x.abs();
let code = if a > 5.0 {
0x7
} else if a >= 3.5 {
0x6
} else if a > 2.5 {
0x5
} else if a >= 1.75 {
0x4
} else if a > 1.25 {
0x3
} else if a >= 0.75 {
0x2
} else if a > 0.25 {
0x1
} else {
0x0
};
code | sign
}
pub(crate) fn e2m1_to_f32(code: u8) -> f32 {
const VALUES: [f32; 8] = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0];
let magnitude = VALUES[usize::from(code & 7)];
if code & 8 != 0 { -magnitude } else { magnitude }
}
pub(crate) fn e8m0_from_f32(x: f32) -> u8 {
if !x.is_finite() {
return 0xFF;
}
if x < 0.0 {
return 0x00;
}
let n = (x.log2().round() as i32).clamp(-127, 127);
(n + 127) as u8
}
pub(crate) fn e8m0_to_f32(code: u8) -> f32 {
if code == 0 {
f32::from_bits(0x40_0000)
} else {
f32::from_bits(u32::from(code) << 23)
}
}
pub(crate) fn ftz(x: f32) -> f32 {
if x.is_subnormal() {
0.0f32.copysign(x)
} else {
x
}
}
pub(crate) fn canonical(x: f32) -> f32 {
if x.is_nan() { f32::NAN } else { x }
}
pub(crate) fn mx_scale_round_up(x: f32) -> f32 {
let mut code = e8m0_from_f32(x);
if code < 0xFE && ftz(e8m0_to_f32(code)) < x {
code += 1;
}
e8m0_to_f32(code)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg_attr(miri, ignore = "Miri cannot execute half's hardware f16 conversion")]
fn e4m3_matches_mlx_probes() {
let values = [
0.1,
1.0,
448.0,
500.0,
-3.3,
1e-4,
0.0,
-0.0,
f32::INFINITY,
f32::NEG_INFINITY,
f32::NAN,
2f32.powi(-9),
2f32.powi(-10),
0.002_929_687_5,
464.0,
465.0,
1.0625,
1.1875,
];
let expected = [
29, 56, 126, 126, 197, 0, 0, 128, 126, 254, 126, 1, 0, 2, 126, 126, 56, 58,
];
let got: Vec<u8> = values.iter().map(|&v| e4m3_from_f32(v)).collect();
assert_eq!(got, expected);
}
#[test]
#[cfg_attr(miri, ignore = "Miri cannot execute half's hardware f16 conversion")]
fn e4m3_codes_round_trip() {
for (code, value) in [
(0, 0.0),
(1, 0.001_953_125),
(7, 0.013_671_875),
(8, 0.015_625),
(56, 1.0),
(126, 448.0),
(128, -0.0),
(254, -448.0),
] {
let v = e4m3_to_f32(code);
assert!(v == value && v.is_sign_negative() == value.is_sign_negative());
}
assert!(e4m3_to_f32(0x7F).is_nan() && e4m3_to_f32(0xFF).is_nan());
for code in (0..=255u8).filter(|c| c & 0x7F != 0x7F) {
assert_eq!(e4m3_from_f32(e4m3_to_f32(code)), code, "code {code:#x}");
}
}
#[test]
fn e2m1_round_trips_and_ties() {
for code in 0..16u8 {
assert_eq!(e2m1_from_f32(e2m1_to_f32(code)), code, "code {code:#x}");
}
let values = [0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0];
for (code, v) in values.iter().enumerate() {
assert_eq!(e2m1_to_f32(code as u8), *v);
assert_eq!(e2m1_to_f32(code as u8 | 8), -*v);
}
let ties = [
(0.25, 0x0),
(0.75, 0x2),
(1.25, 0x2),
(1.75, 0x4),
(2.5, 0x4),
(3.5, 0x6),
(5.0, 0x6),
(7.0, 0x7),
];
for (x, code) in ties {
assert_eq!(e2m1_from_f32(x), code, "{x}");
}
assert_eq!(e2m1_from_f32(-0.0), 0x8);
assert_eq!(e2m1_from_f32(f32::NAN), 0x7);
}
#[test]
fn e8m0_rounds_and_clamps() {
assert_eq!(e8m0_from_f32(1.0), 127);
assert_eq!(e8m0_from_f32(0.25), 125);
assert_eq!(e8m0_from_f32(0.0), 0);
assert_eq!(e8m0_from_f32(-1.0), 0);
assert_eq!(e8m0_from_f32(f32::INFINITY), 0xFF);
assert_eq!(e8m0_from_f32(f32::NAN), 0xFF);
let tiny = f32::from_bits(0x40_0000);
assert_eq!(e8m0_to_f32(0), tiny);
assert_eq!(e8m0_to_f32(0xFF), f32::INFINITY);
assert_eq!(mx_scale_round_up(1.0 / 6.0), 0.25);
assert_eq!(mx_scale_round_up(0.0), tiny);
assert_eq!(mx_scale_round_up(0.5), 0.5);
}
}