spark_runtime/kv_dequant/
luts.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! The dequantization codebooks, and the group size they are indexed in.
4//!
5//! Split from the dequant routines that read them purely for the file-size
6//! cap: the tables are data, the routines are the loops over that data, and
7//! the seam falls between them cleanly. Every table matches its CUDA-side
8//! counterpart byte-for-byte — see the parent module's table for which
9//! kernel each mirrors.
10
11/// Group size for per-group FP8 scales. Matches `NVFP4_GROUP_SIZE` in the
12/// per-quant attention kernels.
13pub const NVFP4_GROUP_SIZE: usize = 16;
14
15/// E2M1 4-bit codebook (NVFP4). Matches
16/// `kernels/gb10/common/paged_decode_attn_nvfp4.cu:118`.
17pub const NVFP4_E2M1_LUT: [f32; 16] = [
18    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,
19];
20
21/// Turbo4 16-level Lloyd-Max codebook. Matches
22/// `kernels/gb10/common/paged_decode_attn_turbo4.cu:121`.
23pub const TURBO4_LUT: [f32; 16] = [
24    -2.7326, -2.0690, -1.6180, -1.2562, -0.9423, -0.6568, -0.3880, -0.1284, 0.1284, 0.3880, 0.6568,
25    0.9423, 1.2562, 1.6180, 2.0690, 2.7326,
26];
27
28/// Turbo3 8-level Lloyd-Max codebook. Matches
29/// `kernels/gb10/common/paged_decode_attn_turbo3.cu:137`.
30pub const TURBO3_LUT: [f32; 8] = [
31    -2.1520, -1.3440, -0.7560, -0.2451, 0.2451, 0.7560, 1.3440, 2.1520,
32];
33
34/// E4M3 → f32 LUT (256 entries).
35///
36/// A `const`, not a lazily-filled static: the table is pure arithmetic over a
37/// fixed domain, so it is computed at compile time and there is no runtime
38/// state to initialise, synchronise or invalidate.
39const E4M3_LUT: [f32; 256] = {
40    let mut lut = [0.0f32; 256];
41    let mut byte = 0u32;
42    while byte < 256 {
43        let sign_bit = (byte >> 7) & 1;
44        let exp = ((byte >> 3) & 0xF) as i32;
45        let mant = byte & 0x7;
46        let s: f32 = if sign_bit == 0 { 1.0 } else { -1.0 };
47        lut[byte as usize] = if exp == 0 {
48            if mant == 0 {
49                s * 0.0
50            } else {
51                s * (mant as f32) * exp2(-9)
52            }
53        } else if exp == 0xF && mant == 0x7 {
54            f32::NAN
55        } else {
56            s * exp2(exp - 7) * (1.0 + (mant as f32) / 8.0)
57        };
58        byte += 1;
59    }
60    lut
61};
62
63/// `2^e`, for the small integer exponents this table needs (`-9..=8`).
64///
65/// `f32::powi` is not a const fn. Repeated multiply/divide is EXACT here rather
66/// than merely close: powers of two are representable exactly in binary
67/// floating point, so every step is lossless and the result is bit-identical to
68/// `powi`. A test pins that.
69const fn exp2(e: i32) -> f32 {
70    let mut v = 1.0f32;
71    let mut i = 0i32;
72    if e >= 0 {
73        while i < e {
74            v *= 2.0;
75            i += 1;
76        }
77    } else {
78        while i < -e {
79            v /= 2.0;
80            i += 1;
81        }
82    }
83    v
84}
85
86/// Borrow the compile-time table.
87pub fn e4m3_lut() -> &'static [f32; 256] {
88    &E4M3_LUT
89}
90
91#[cfg(test)]
92mod lut_tests {
93    use super::*;
94
95    /// The table moved from a lazily-filled `OnceLock` to a `const`, which
96    /// meant replacing `f32::powi` (not const) with `exp2`. This proves the
97    /// substitution changed no value: every entry must be bit-identical to the
98    /// `powi` formula it replaced.
99    #[test]
100    fn the_const_table_is_bit_identical_to_the_powi_formula() {
101        for byte in 0..256u32 {
102            let sign_bit = (byte >> 7) & 1;
103            let exp = ((byte >> 3) & 0xF) as i32;
104            let mant = byte & 0x7;
105            let s: f32 = if sign_bit == 0 { 1.0 } else { -1.0 };
106            let expected: f32 = if exp == 0 {
107                if mant == 0 {
108                    s * 0.0
109                } else {
110                    s * (mant as f32) * 2.0f32.powi(-9)
111                }
112            } else if exp == 0xF && mant == 0x7 {
113                f32::NAN
114            } else {
115                s * 2.0f32.powi(exp - 7) * (1.0 + (mant as f32) / 8.0)
116            };
117            let got = E4M3_LUT[byte as usize];
118            if expected.is_nan() {
119                assert!(got.is_nan(), "byte {byte}: expected NaN, got {got}");
120            } else {
121                assert_eq!(
122                    got.to_bits(),
123                    expected.to_bits(),
124                    "byte {byte}: {got} != {expected}"
125                );
126            }
127        }
128    }
129
130    #[test]
131    fn exp2_matches_powi_across_the_domain() {
132        for e in -9..=8i32 {
133            assert_eq!(exp2(e).to_bits(), 2.0f32.powi(e).to_bits(), "2^{e}");
134        }
135    }
136}