metrale_core/
mxfp4_e8m0.rs1use anyhow::{Result, ensure};
20
21pub const E2M1: [f32; 16] = [
23 0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0, -0.0, -0.5, -1.0, -1.5, -2.0, -3.0, -4.0, -6.0,
24];
25
26pub const GROUP_SIZE: usize = 32;
29
30const FP8_E8M0_LUT: [f32; 256] = {
32 let mut table = [0.0f32; 256];
33 let mut i: u32 = 0;
34 while i < 256 {
35 let exp = i as u8;
36 table[i as usize] = if exp == 0 || exp == 255 {
37 0.0f32
38 } else {
39 f32::from_bits((exp as u32) << 23)
40 };
41 i += 1;
42 }
43 table
44};
45
46#[inline(always)]
48pub fn fp8_e8m0_to_f32(bits: u8) -> f32 {
49 FP8_E8M0_LUT[bits as usize]
50}
51
52pub fn dequant_nvfp4_e8m0_to_f32(
59 packed: &[u8],
60 scales: &[u8],
61 n: usize,
62 k: usize,
63) -> Result<Vec<f32>> {
64 let total = n.checked_mul(k).expect("mxfp4 n*k");
65 ensure!(
66 total.is_multiple_of(2),
67 "MXFP4 E8M0: n*k={total} is odd (need even nibble count)"
68 );
69 let packed_bytes = total / 2;
70 ensure!(
71 packed.len() == packed_bytes,
72 "MXFP4 E8M0: packed {} B, expected {packed_bytes} for [{n},{k}]",
73 packed.len()
74 );
75 let num_groups = scales.len();
76 ensure!(
77 num_groups > 0 && total.is_multiple_of(num_groups),
78 "MXFP4 E8M0: weight elems {total} not divisible by E8M0 scale groups {num_groups}"
79 );
80 let block = total / num_groups;
81 let mut out = vec![0.0f32; total];
82 for (group, &sb) in scales.iter().enumerate() {
83 let block_scale = fp8_e8m0_to_f32(sb);
84 for elem in 0..block {
85 let flat_idx = group * block + elem;
86 let byte_idx = flat_idx / 2;
87 let nibble = if flat_idx.is_multiple_of(2) {
88 packed[byte_idx] & 0x0F
89 } else {
90 (packed[byte_idx] >> 4) & 0x0F
91 };
92 out[flat_idx] = E2M1[nibble as usize] * block_scale;
93 }
94 }
95 Ok(out)
96}
97
98pub fn dequant_nvfp4_e8m0_to_bf16(
100 packed: &[u8],
101 scales: &[u8],
102 n: usize,
103 k: usize,
104) -> Result<Vec<u16>> {
105 let f = dequant_nvfp4_e8m0_to_f32(packed, scales, n, k)?;
106 Ok(f.into_iter().map(crate::numeric::f32_to_bf16).collect())
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn e8m0_pow2_and_sentinels() {
115 assert_eq!(fp8_e8m0_to_f32(0), 0.0);
116 assert_eq!(fp8_e8m0_to_f32(255), 0.0);
117 assert_eq!(fp8_e8m0_to_f32(127), 1.0);
118 assert_eq!(fp8_e8m0_to_f32(128), 2.0);
119 assert_eq!(fp8_e8m0_to_f32(126), 0.5);
120 }
121
122 #[test]
123 fn matches_dsv4_nibble_order_and_lut() {
124 let packed = [0x12u8];
126 let scales = [127u8];
127 let got = dequant_nvfp4_e8m0_to_f32(&packed, &scales, 1, 2).unwrap();
128 assert_eq!(got, vec![1.0, 0.5]);
129 }
130}