atlas_core/
numeric.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! Host-side numeric conversions shared by every weight loader: the FP8
4//! E4M3 decode table and the f32 -> BF16 cast.
5//!
6//! This is the single copy. It used to exist twice — once in
7//! `spark-model/src/weight_map/fp8_lut.rs` (live) and once in
8//! `atlas-quant/src/fp8.rs` (unreachable, zero dependents) — with the
9//! byte-exactness tests attached to the copy that never ran. Both crates
10//! already depended on `atlas-core`, so the fix was to move the arithmetic
11//! down here and bring the tests with it.
12//!
13//! Pure arithmetic: no CUDA, no allocation, compiles under every feature
14//! combination. The CUDA-side mirrors are `E4M3_LUT_GMOE` and
15//! `__float2bfloat16_rn` in `kernels/gb10/common/moe_fp8_grouped_gemm.cu`;
16//! they must agree with this file element for element.
17
18/// FP8 E4M3 -> f32 lookup table (256 entries, one per byte value).
19///
20/// OCP FP8 E4M3FN: sign(1) | exponent(4) | mantissa(3), bias = 7. There
21/// are no infinities; `0x7F` / `0xFF` are NaN and max finite is +/-448.0
22/// (exp = 15, mant = 6).
23///
24/// NaN entries decode to `0.0`. A NaN weight should not exist in a
25/// checkpoint, and zero stops one bad byte from poisoning an entire
26/// dequanted tensor — which is what propagating NaN through the loader
27/// would do.
28///
29/// Built at compile time so the hot dequant loop is a single indexed load
30/// with no branches.
31#[allow(clippy::if_same_then_else)]
32pub static FP8_E4M3_LUT: [f32; 256] = {
33    let mut table = [0.0f32; 256];
34    let mut i: u32 = 0;
35    while i < 256 {
36        let bits = i as u8;
37        let sign = (bits >> 7) & 1;
38        let exp = (bits >> 3) & 0x0F;
39        let mantissa = bits & 0x07;
40
41        let val = if exp == 0 && mantissa == 0 {
42            0.0f32
43        } else if exp == 0x0F && mantissa == 0x07 {
44            0.0f32 // NaN -> 0.0
45        } else if exp == 0 {
46            // Subnormal: 2^(-6) * (mantissa / 8).
47            (mantissa as f32) * (0.015625f32 / 8.0)
48        } else {
49            // Normal: 2^(exp-7) * (1 + mantissa/8), assembled directly in
50            // f32 bits — f32 exponent = fp8_exp - 7 + 127 = fp8_exp + 120,
51            // f32 mantissa = fp8_mant << 20 (3 bits left-aligned into 23).
52            let f32_exp = (exp as u32 + 120) << 23;
53            let f32_mant = (mantissa as u32) << 20;
54            f32::from_bits(f32_exp | f32_mant)
55        };
56
57        table[i as usize] = if sign == 1 { -val } else { val };
58        i += 1;
59    }
60    table
61};
62
63/// Decode one FP8 E4M3 byte to f32 (branchless, single array lookup).
64#[inline(always)]
65pub fn fp8_e4m3_to_f32(bits: u8) -> f32 {
66    FP8_E4M3_LUT[bits as usize]
67}
68
69/// Convert f32 to BF16 with IEEE-754 round-to-nearest-even.
70///
71/// Must stay byte-identical to PyTorch's `torch.float32 -> torch.bfloat16`
72/// cast: reference activations and the dequanted-weight snapshots Atlas is
73/// scored against are produced that way, so any drift here shows up as an
74/// accuracy regression with no other symptom.
75///
76/// Phase 2b (FP8 dequant audit, 2026-05-24) replaced truncation
77/// (`bits >> 16`) with ties-to-even. Truncation is biased toward zero and
78/// the bias accumulated across the 31745 dequanted tensors of
79/// Qwen3.6-35B-FP8 to a mean per-layer cosine of 0.969.
80///
81/// NaN maps to the canonical quiet-NaN pattern with the sign preserved,
82/// which is also what PyTorch does.
83///
84/// `ATLAS_DISABLE_RNE` is a bisect escape hatch that reverts to
85/// truncation. It is a PRESENCE check, not a value check — `=0` disables
86/// RNE just as `=1` does.
87///
88/// ★ THE ESCAPE HATCH IS READ ONCE PER PROCESS. This is a scalar primitive —
89/// five arithmetic operations, `#[inline(always)]` — called ONCE PER ELEMENT
90/// over whole weight tensors (`weight_map::quant_helpers` iterates every byte
91/// of an FP8 tensor; `fp8_lut` walks every NVFP4 group). A `std::env::var`
92/// here allocates a `String` and takes the process-wide environment lock, and
93/// dominated the arithmetic by roughly three orders of magnitude. The
94/// dequantisation path this serves has a MEASURED cost of ~80 s
95/// (`weight_map/fp8_dequant.rs`), which is ~1.4e8 elements at the per-read
96/// rate — so the getenv plausibly accounted for most of it.
97///
98/// Caching is safe here specifically because the test that varies this
99/// re-execs the test binary as a CHILD PROCESS with the variable set
100/// (`disable_rne_presence_uses_truncation` below), so each process resolves it
101/// once at its own start. Do NOT convert that test to `set_var` in-process.
102#[inline(always)]
103pub fn f32_to_bf16(val: f32) -> u16 {
104    static DISABLE_RNE: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
105    if *DISABLE_RNE.get_or_init(|| std::env::var("ATLAS_DISABLE_RNE").is_ok()) {
106        return (val.to_bits() >> 16) as u16;
107    }
108    let bits = val.to_bits();
109    if val.is_nan() {
110        let sign = ((bits >> 16) & 0x8000) as u16;
111        return sign | 0x7FC0;
112    }
113    let lsb = (bits >> 16) & 1;
114    let rounding_bias = 0x7FFFu32 + lsb;
115    (bits.wrapping_add(rounding_bias) >> 16) as u16
116}
117
118/// Widen little-endian BF16 bytes to f32. Exact — BF16 is the top 16 bits
119/// of an f32, so this is a shift, never a rounding.
120#[inline(always)]
121pub fn bf16_bytes_to_f32(bytes: [u8; 2]) -> f32 {
122    let bits = u16::from_le_bytes(bytes);
123    f32::from_bits((bits as u32) << 16)
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129
130    #[test]
131    fn fp8_lut_reference_values() {
132        assert_eq!(fp8_e4m3_to_f32(0x00).to_bits(), 0x0000_0000); // +0
133        assert_eq!(fp8_e4m3_to_f32(0x80).to_bits(), 0x8000_0000); // -0
134        assert_eq!(fp8_e4m3_to_f32(0x38), 1.0); // exp=7, mant=0
135        assert_eq!(fp8_e4m3_to_f32(0xB8), -1.0);
136        assert_eq!(fp8_e4m3_to_f32(0x3C), 1.5); // exp=7, mant=4
137        assert_eq!(fp8_e4m3_to_f32(0x7E), 448.0); // max finite
138        assert_eq!(fp8_e4m3_to_f32(0xFE), -448.0); // min finite
139        assert_eq!(fp8_e4m3_to_f32(0x7F).to_bits(), 0x0000_0000); // NaN -> +0
140        assert_eq!(fp8_e4m3_to_f32(0xFF).to_bits(), 0x8000_0000); // -NaN -> -0
141
142        // Subnormals: 2^(-6) * mant/8.
143        let eps = 1e-10;
144        assert!((fp8_e4m3_to_f32(0x01) - 0.001953125).abs() < eps);
145        assert!((fp8_e4m3_to_f32(0x07) - 0.013671875).abs() < eps);
146    }
147
148    #[test]
149    #[allow(clippy::if_same_then_else)]
150    fn fp8_lut_matches_ocp_values_and_atlas_nan_policy_for_all_bytes() {
151        // Re-derived from the OCP finite-value definition with float math,
152        // independently of the table's bit assembly. Atlas deliberately maps
153        // the two OCP NaN encodings to signed zero, matching its CUDA decoder.
154        for i in 0u16..256 {
155            let bits = i as u8;
156            let sign = (bits >> 7) & 1;
157            let exp = (bits >> 3) & 0x0F;
158            let mant = bits & 0x07;
159
160            let magnitude = if exp == 0x0F && mant == 0x07 {
161                0.0f32
162            } else if exp == 0 && mant == 0 {
163                0.0f32
164            } else if exp == 0 {
165                (mant as f32 / 8.0) * 2.0f32.powi(-6)
166            } else {
167                (1.0 + mant as f32 / 8.0) * 2.0f32.powi(exp as i32 - 7)
168            };
169            let expected = if sign == 1 { -magnitude } else { magnitude };
170            let actual = fp8_e4m3_to_f32(bits);
171            assert_eq!(
172                actual.to_bits(),
173                expected.to_bits(),
174                "LUT mismatch at {i:#04x}: expected {expected:?}, got {actual:?}"
175            );
176        }
177    }
178
179    /// The assertions that separate round-to-nearest-even from
180    /// truncation-toward-zero. Truncation FAILS every "round up" case here.
181    #[test]
182    fn f32_to_bf16_is_rne_byte_exact() {
183        fn convert(bits: u32) -> u16 {
184            f32_to_bf16(f32::from_bits(bits))
185        }
186
187        // Below half-ULP: round DOWN. Truncation agrees.
188        assert_eq!(convert(0x3F80_0800), 0x3F80, "1.0 + below-half-ULP -> 1.0");
189        // Exactly half-ULP, LSB=0: tie -> round to EVEN (down). Does not
190        // distinguish RNE from truncation; kept for the tie coverage.
191        assert_eq!(
192            convert(0x3F80_8000),
193            0x3F80,
194            "1.0 + exact-half-ULP, LSB=0 -> 1.0 (even)"
195        );
196        // Above half-ULP: round UP. Truncation would give 0x3F80.
197        assert_eq!(
198            convert(0x3F80_8001),
199            0x3F81,
200            "1.0 + above-half-ULP -> next bf16 (truncation would give 0x3F80)"
201        );
202        // Exactly half-ULP, LSB=1: tie -> round to EVEN (up). Truncation
203        // would give 0x3F81.
204        assert_eq!(
205            convert(0x3F81_8000),
206            0x3F82,
207            "1.0078125 + exact-half-ULP, LSB=1 -> 1.015625"
208        );
209        // Negative parity: magnitude grows the same way.
210        assert_eq!(convert(0xBF80_8001), 0xBF81, "negative round up");
211        // Zero: exact, no rounding, sign preserved.
212        assert_eq!(convert(0x0000_0000), 0x0000, "+0.0");
213        assert_eq!(convert(0x8000_0000), 0x8000, "-0.0");
214        // Smallest f32 subnormal (2^-149) -> nearest bf16 is 0 (LSB=0 tie).
215        assert_eq!(convert(0x0000_0001), 0x0000, "tiny subnormal -> 0");
216        // Infinities pass through.
217        assert_eq!(convert(0x7F80_0000), 0x7F80, "+inf");
218        assert_eq!(convert(0xFF80_0000), 0xFF80, "-inf");
219        // Max-finite f32 rounds UP to +inf in bf16 — the closest
220        // representable value. PyTorch does the same.
221        assert_eq!(
222            convert(0x7F7F_FFFF),
223            0x7F80,
224            "max-finite f32 rounds to +inf bf16"
225        );
226        // NaN -> canonical quiet NaN, sign preserved; signalling NaN is
227        // quieted rather than passed through.
228        assert_eq!(convert(0x7FC0_0000), 0x7FC0, "qnan +");
229        assert_eq!(convert(0xFFC0_0000), 0xFFC0, "qnan -");
230        assert_eq!(convert(0x7F80_0001), 0x7FC0, "snan + -> qnan +");
231    }
232
233    /// Byte-exact match against values captured from PyTorch 2.9 via
234    /// `torch.tensor([x], dtype=torch.float32).bfloat16()`. If this fails
235    /// after a math change, the converter has drifted from PyTorch's RNE
236    /// and every dequanted weight in the engine is off by a bit.
237    #[test]
238    fn f32_to_bf16_matches_pytorch() {
239        let cases: &[(u32, u16, &str)] = &[
240            (0x3F80_0000, 0x3F80, "1.0"),
241            (0x4000_0000, 0x4000, "2.0"),
242            (0xC000_0000, 0xC000, "-2.0"),
243            (0x3FC0_0000, 0x3FC0, "1.5"),
244            (
245                0x3DCC_CCCD,
246                0x3DCD,
247                "0.1 -> RNE rounds UP to 0x3DCD (trunc=0x3DCC)",
248            ),
249            (0x3F4C_CCCD, 0x3F4D, "0.8 -> RNE rounds UP to 0x3F4D"),
250            (0x40C9_0FDB, 0x40C9, "pi -> truncates (next bit < half)"),
251            (0x402D_F854, 0x402E, "e -> RNE rounds UP (next bit > half)"),
252            (0x4490_0000, 0x4490, "1152.0"),
253            (0x3727_C5AC, 0x3728, "1e-5 -> RNE rounds UP"),
254        ];
255        for (f32_bits, want, desc) in cases {
256            let got = f32_to_bf16(f32::from_bits(*f32_bits));
257            assert_eq!(
258                got, *want,
259                "f32={f32_bits:#010x} ({desc}): want bf16={want:#06x}, got {got:#06x}"
260            );
261        }
262    }
263
264    #[test]
265    fn disable_rne_presence_uses_truncation() {
266        const THIS_TEST: &str = "numeric::tests::disable_rne_presence_uses_truncation";
267        const CHILD_MARKER: &str = "ATLAS_NUMERIC_RNE_CHILD";
268
269        if std::env::var_os(CHILD_MARKER).is_some() {
270            assert_eq!(
271                f32_to_bf16(f32::from_bits(0x3F80_8001)),
272                0x3F80,
273                "the escape hatch must truncate an above-half-ULP value"
274            );
275            return;
276        }
277
278        for value in ["0", "1"] {
279            let output = std::process::Command::new(std::env::current_exe().unwrap())
280                .args(["--exact", THIS_TEST])
281                .env(CHILD_MARKER, "1")
282                .env("ATLAS_DISABLE_RNE", value)
283                .output()
284                .unwrap();
285            assert!(
286                output.status.success(),
287                "ATLAS_DISABLE_RNE={value} child failed:\n{}",
288                String::from_utf8_lossy(&output.stdout)
289            );
290        }
291    }
292
293    #[test]
294    fn bf16_widening_is_byte_exact_for_every_pattern() {
295        for bits in 0u32..=0xFFFF {
296            let bf16 = bits as u16;
297            let widened = bf16_bytes_to_f32(bf16.to_le_bytes());
298            assert_eq!(
299                widened.to_bits(),
300                bits << 16,
301                "widening moved bits for bf16 {bf16:#06x}"
302            );
303        }
304    }
305
306    #[test]
307    fn bf16_narrowing_preserves_every_non_nan_bf16_value() {
308        for bits in 0u32..=0xFFFF {
309            let bf16 = bits as u16;
310            let widened = f32::from_bits(bits << 16);
311            if widened.is_nan() {
312                continue;
313            }
314            assert_eq!(
315                f32_to_bf16(widened),
316                bf16,
317                "round trip failed for bf16 {bf16:#06x}"
318            );
319        }
320    }
321}