metrale_core/
numeric.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Host-side numeric conversions: the FP8 E4M3 decode table, the f32 -> BF16 cast and BF16 widening.
4//!
5//! Owner: metrale-core.
6//! Invariants:
7//! - No CUDA and no `cfg`: the module compiles under every feature combination.
8//! - `FP8_E4M3_LUT` decodes the two NaN bytes (`0x7F`, `0xFF`) to signed zero.
9//! - `E4M3_LUT_GMOE` in `kernels/gb10/common/moe_fp8_grouped_gemm.cu` must agree with
10//!   `FP8_E4M3_LUT` element for element; it does, including the signed zeros.
11
12/// 2026-09-25: FP8 E4M3 -> f32 lookup table, one entry per byte value.
13///
14/// OCP FP8 E4M3FN: sign(1) | exponent(4) | mantissa(3), bias 7. There are no
15/// infinities; `0x7F` / `0xFF` are NaN and max finite is +/-448.0 (exp 15,
16/// mant 6).
17///
18/// NaN entries decode to signed zero: one bad byte then cannot turn a whole
19/// dequantised tensor into NaN.
20///
21/// Built at compile time, so a decode is one indexed load with no branches.
22#[allow(clippy::if_same_then_else)]
23pub static FP8_E4M3_LUT: [f32; 256] = {
24    let mut table = [0.0f32; 256];
25    let mut i: u32 = 0;
26    while i < 256 {
27        let bits = i as u8;
28        let sign = (bits >> 7) & 1;
29        let exp = (bits >> 3) & 0x0F;
30        let mantissa = bits & 0x07;
31
32        let val = if exp == 0 && mantissa == 0 {
33            0.0f32
34        } else if exp == 0x0F && mantissa == 0x07 {
35            0.0f32
36        } else if exp == 0 {
37            // 2026-09-25: Subnormal: 2^(-6) * (mantissa / 8).
38            (mantissa as f32) * (0.015625f32 / 8.0)
39        } else {
40            // 2026-09-25: Normal: 2^(exp-7) * (1 + mantissa/8), assembled directly in
41            // f32 bits — f32 exponent = fp8_exp - 7 + 127 = fp8_exp + 120,
42            // f32 mantissa = fp8_mant << 20 (3 bits left-aligned into 23).
43            let f32_exp = (exp as u32 + 120) << 23;
44            let f32_mant = (mantissa as u32) << 20;
45            f32::from_bits(f32_exp | f32_mant)
46        };
47
48        table[i as usize] = if sign == 1 { -val } else { val };
49        i += 1;
50    }
51    table
52};
53
54/// 2026-09-25: Decode one FP8 E4M3 byte to f32 with a single table load.
55#[inline(always)]
56pub fn fp8_e4m3_to_f32(bits: u8) -> f32 {
57    FP8_E4M3_LUT[bits as usize]
58}
59
60/// 2026-09-25: Convert f32 to BF16 with IEEE-754 round-to-nearest-even.
61///
62/// Truncation (`bits >> 16`) is biased toward zero; the tests below pin the
63/// cases where the two differ.
64///
65/// NaN maps to the canonical quiet-NaN pattern with the sign preserved.
66///
67/// `METRALE_DISABLE_RNE` is a bisect escape hatch that reverts to
68/// truncation. It is a presence check, not a value check: `=0` disables RNE
69/// just as `=1` does.
70///
71/// The escape hatch is read once per process. Weight loaders call this once
72/// per element (`weight_map::quant_helpers`, `weight_map::fp8_lut`), and a
73/// `std::env::var` per call would allocate a `String` and take the
74/// process-wide environment lock each time.
75///
76/// Caching is safe because the test that varies the variable re-execs the
77/// test binary as a child process with it set
78/// (`disable_rne_presence_uses_truncation` below), so each process resolves
79/// it once at its own start. Do not convert that test to an in-process
80/// `set_var`.
81#[inline(always)]
82pub fn f32_to_bf16(val: f32) -> u16 {
83    static DISABLE_RNE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
84    if *DISABLE_RNE.get_or_init(|| std::env::var("METRALE_DISABLE_RNE").is_ok()) {
85        return (val.to_bits() >> 16) as u16;
86    }
87    let bits = val.to_bits();
88    if val.is_nan() {
89        let sign = ((bits >> 16) & 0x8000) as u16;
90        return sign | 0x7FC0;
91    }
92    let lsb = (bits >> 16) & 1;
93    let rounding_bias = 0x7FFFu32 + lsb;
94    (bits.wrapping_add(rounding_bias) >> 16) as u16
95}
96
97/// 2026-09-25: Widen little-endian BF16 bytes to f32. Exact: BF16 is the
98/// top 16 bits of an f32, so this is a shift, never a rounding.
99#[inline(always)]
100pub fn bf16_bytes_to_f32(bytes: [u8; 2]) -> f32 {
101    let bits = u16::from_le_bytes(bytes);
102    f32::from_bits((bits as u32) << 16)
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn fp8_lut_reference_values() {
111        assert_eq!(fp8_e4m3_to_f32(0x00).to_bits(), 0x0000_0000);
112        assert_eq!(fp8_e4m3_to_f32(0x80).to_bits(), 0x8000_0000);
113        assert_eq!(fp8_e4m3_to_f32(0x38), 1.0);
114        assert_eq!(fp8_e4m3_to_f32(0xB8), -1.0);
115        assert_eq!(fp8_e4m3_to_f32(0x3C), 1.5);
116        assert_eq!(fp8_e4m3_to_f32(0x7E), 448.0);
117        assert_eq!(fp8_e4m3_to_f32(0xFE), -448.0);
118        assert_eq!(fp8_e4m3_to_f32(0x7F).to_bits(), 0x0000_0000);
119        assert_eq!(fp8_e4m3_to_f32(0xFF).to_bits(), 0x8000_0000);
120
121        // 2026-09-25: Subnormals: 2^(-6) * mant/8.
122        let eps = 1e-10;
123        assert!((fp8_e4m3_to_f32(0x01) - 0.001953125).abs() < eps);
124        assert!((fp8_e4m3_to_f32(0x07) - 0.013671875).abs() < eps);
125    }
126
127    #[test]
128    #[allow(clippy::if_same_then_else)]
129    fn fp8_lut_matches_ocp_values_and_metrale_nan_policy_for_all_bytes() {
130        // 2026-09-25: Re-derived from the OCP finite-value definition with float
131        // math, independently of the table's bit assembly. The two OCP NaN
132        // encodings map to signed zero, as `E4M3_LUT_GMOE` does on the device.
133        for i in 0u16..256 {
134            let bits = i as u8;
135            let sign = (bits >> 7) & 1;
136            let exp = (bits >> 3) & 0x0F;
137            let mant = bits & 0x07;
138
139            let magnitude = if exp == 0x0F && mant == 0x07 {
140                0.0f32
141            } else if exp == 0 && mant == 0 {
142                0.0f32
143            } else if exp == 0 {
144                (mant as f32 / 8.0) * 2.0f32.powi(-6)
145            } else {
146                (1.0 + mant as f32 / 8.0) * 2.0f32.powi(exp as i32 - 7)
147            };
148            let expected = if sign == 1 { -magnitude } else { magnitude };
149            let actual = fp8_e4m3_to_f32(bits);
150            assert_eq!(
151                actual.to_bits(),
152                expected.to_bits(),
153                "LUT mismatch at {i:#04x}: expected {expected:?}, got {actual:?}"
154            );
155        }
156    }
157
158    /// 2026-09-25: The assertions that separate round-to-nearest-even from
159    /// truncation toward zero. Truncation fails every "round up" case here.
160    #[test]
161    fn f32_to_bf16_is_rne_byte_exact() {
162        fn convert(bits: u32) -> u16 {
163            f32_to_bf16(f32::from_bits(bits))
164        }
165
166        // 2026-09-25: Below half-ULP: round down. Truncation agrees.
167        assert_eq!(convert(0x3F80_0800), 0x3F80, "1.0 + below-half-ULP -> 1.0");
168        // 2026-09-25: Exactly half-ULP, LSB=0: tie -> round to even (down). Does
169        // not distinguish RNE from truncation; kept for the tie coverage.
170        assert_eq!(
171            convert(0x3F80_8000),
172            0x3F80,
173            "1.0 + exact-half-ULP, LSB=0 -> 1.0 (even)"
174        );
175        // 2026-09-25: Above half-ULP: round up. Truncation would give 0x3F80.
176        assert_eq!(
177            convert(0x3F80_8001),
178            0x3F81,
179            "1.0 + above-half-ULP -> next bf16 (truncation would give 0x3F80)"
180        );
181        // 2026-09-25: Exactly half-ULP, LSB=1: tie -> round to even (up).
182        // Truncation would give 0x3F81.
183        assert_eq!(
184            convert(0x3F81_8000),
185            0x3F82,
186            "1.0078125 + exact-half-ULP, LSB=1 -> 1.015625"
187        );
188        // 2026-09-25: Negative values round the magnitude the same way.
189        assert_eq!(convert(0xBF80_8001), 0xBF81, "negative round up");
190        assert_eq!(convert(0x0000_0000), 0x0000, "+0.0");
191        assert_eq!(convert(0x8000_0000), 0x8000, "-0.0");
192        // 2026-09-25: Smallest f32 subnormal (2^-149) is below half-ULP and
193        // rounds down to 0.
194        assert_eq!(convert(0x0000_0001), 0x0000, "tiny subnormal -> 0");
195        assert_eq!(convert(0x7F80_0000), 0x7F80, "+inf");
196        assert_eq!(convert(0xFF80_0000), 0xFF80, "-inf");
197        // 2026-09-25: Max-finite f32 rounds up to +inf in bf16.
198        assert_eq!(
199            convert(0x7F7F_FFFF),
200            0x7F80,
201            "max-finite f32 rounds to +inf bf16"
202        );
203        // 2026-09-25: NaN -> canonical quiet NaN, sign preserved; a signalling
204        // NaN is quieted rather than passed through.
205        assert_eq!(convert(0x7FC0_0000), 0x7FC0, "qnan +");
206        assert_eq!(convert(0xFFC0_0000), 0xFFC0, "qnan -");
207        assert_eq!(convert(0x7F80_0001), 0x7FC0, "snan + -> qnan +");
208    }
209
210    /// 2026-09-25: Byte-exact round-to-nearest-even results for common values,
211    /// including cases where truncation gives a different result.
212    #[test]
213    fn f32_to_bf16_matches_pytorch() {
214        let cases: &[(u32, u16, &str)] = &[
215            (0x3F80_0000, 0x3F80, "1.0"),
216            (0x4000_0000, 0x4000, "2.0"),
217            (0xC000_0000, 0xC000, "-2.0"),
218            (0x3FC0_0000, 0x3FC0, "1.5"),
219            (
220                0x3DCC_CCCD,
221                0x3DCD,
222                "0.1 -> RNE rounds UP to 0x3DCD (trunc=0x3DCC)",
223            ),
224            (0x3F4C_CCCD, 0x3F4D, "0.8 -> RNE rounds UP to 0x3F4D"),
225            (0x40C9_0FDB, 0x40C9, "pi -> truncates (next bit < half)"),
226            (0x402D_F854, 0x402E, "e -> RNE rounds UP (next bit > half)"),
227            (0x4490_0000, 0x4490, "1152.0"),
228            (0x3727_C5AC, 0x3728, "1e-5 -> RNE rounds UP"),
229        ];
230        for (f32_bits, want, desc) in cases {
231            let got = f32_to_bf16(f32::from_bits(*f32_bits));
232            assert_eq!(
233                got, *want,
234                "f32={f32_bits:#010x} ({desc}): want bf16={want:#06x}, got {got:#06x}"
235            );
236        }
237    }
238
239    #[test]
240    fn disable_rne_presence_uses_truncation() {
241        let this = format!(
242            "{}::disable_rne_presence_uses_truncation",
243            module_path!()
244                .split_once("::")
245                .map_or(module_path!(), |(_, p)| p)
246        );
247        const CHILD_MARKER: &str = "METRALE_NUMERIC_RNE_CHILD";
248
249        if std::env::var_os(CHILD_MARKER).is_some() {
250            assert_eq!(
251                f32_to_bf16(f32::from_bits(0x3F80_8001)),
252                0x3F80,
253                "the escape hatch must truncate an above-half-ULP value"
254            );
255            return;
256        }
257
258        for value in ["0", "1"] {
259            let output = std::process::Command::new(std::env::current_exe().unwrap())
260                .args(["--exact", this.as_str()])
261                .env(CHILD_MARKER, "1")
262                .env("METRALE_DISABLE_RNE", value)
263                .output()
264                .unwrap();
265            assert!(
266                output.status.success(),
267                "METRALE_DISABLE_RNE={value} child failed:\n{}",
268                String::from_utf8_lossy(&output.stdout)
269            );
270            assert!(
271                String::from_utf8_lossy(&output.stdout).contains("1 passed"),
272                "the child must run exactly this test: {}",
273                String::from_utf8_lossy(&output.stdout)
274            );
275        }
276    }
277
278    #[test]
279    fn bf16_widening_is_byte_exact_for_every_pattern() {
280        for bits in 0u32..=0xFFFF {
281            let bf16 = bits as u16;
282            let widened = bf16_bytes_to_f32(bf16.to_le_bytes());
283            assert_eq!(
284                widened.to_bits(),
285                bits << 16,
286                "widening moved bits for bf16 {bf16:#06x}"
287            );
288        }
289    }
290
291    #[test]
292    fn bf16_narrowing_preserves_every_non_nan_bf16_value() {
293        for bits in 0u32..=0xFFFF {
294            let bf16 = bits as u16;
295            let widened = f32::from_bits(bits << 16);
296            if widened.is_nan() {
297                continue;
298            }
299            assert_eq!(
300                f32_to_bf16(widened),
301                bf16,
302                "round trip failed for bf16 {bf16:#06x}"
303            );
304        }
305    }
306}