Skip to main content

qualia_core_db/inference/
inference_kernel_parity.rs

1//! W3 — in-project GPU↔CPU kernel-parity oracle (no external LLM libs).
2//!
3//! The "no external libraries" rule (Prime Directive #4) means the only trustworthy reference for a
4//! GPU kernel is an in-project CPU implementation of the identical math. This module supplies the
5//! comparison metrics (max / mean absolute error + ULP distance) and helpers to synthesize valid
6//! quantized weights, so a GPU shader can be checked against its CPU twin on random, controlled
7//! inputs — fast, deterministic, and library-free. The first consumer is the GEMM parity test
8//! (`QTensorEngine::gemm_parity_probe`: GPU `dispatch_gemm_raw_into` vs CPU `stack_gemm_quant`).
9//!
10//! Pure CPU + zero-heap on the metric paths (slices in, scalars out); safe on every target.
11
12/// Maximum absolute error between two equal-length slices. Returns `+inf` on length mismatch so a
13/// caller cannot silently pass a comparison of differently-shaped outputs.
14pub fn max_abs_err(a: &[f32], b: &[f32]) -> f32 {
15    if a.len() != b.len() {
16        return f32::INFINITY;
17    }
18    a.iter()
19        .zip(b)
20        .map(|(x, y)| (x - y).abs())
21        .fold(0.0f32, f32::max)
22}
23
24/// Mean absolute error (f64 accumulation to avoid catastrophic cancellation over long vectors).
25pub fn mean_abs_err(a: &[f32], b: &[f32]) -> f64 {
26    if a.is_empty() || a.len() != b.len() {
27        return f64::INFINITY;
28    }
29    let s: f64 = a
30        .iter()
31        .zip(b)
32        .map(|(x, y)| (*x as f64 - *y as f64).abs())
33        .sum();
34    s / a.len() as f64
35}
36
37/// Monotone ordering key: integer distance between two keys == ULP distance for finite f32.
38#[inline]
39fn ulp_key(x: f32) -> i64 {
40    let b = x.to_bits();
41    if b & 0x8000_0000 == 0 {
42        b as i64
43    } else {
44        -((b & 0x7fff_ffff) as i64)
45    }
46}
47
48/// Maximum ULP (unit-in-the-last-place) distance over finite pairs; non-finite pairs are skipped.
49/// Returns `u64::MAX` on length mismatch.
50pub fn max_ulp_diff(a: &[f32], b: &[f32]) -> u64 {
51    if a.len() != b.len() {
52        return u64::MAX;
53    }
54    let mut m = 0u64;
55    for (&x, &y) in a.iter().zip(b) {
56        if x.is_finite() && y.is_finite() {
57            let d = (ulp_key(x) - ulp_key(y)).unsigned_abs();
58            if d > m {
59                m = d;
60            }
61        }
62    }
63    m
64}
65
66const Q8_0_BLOCK_ELEMS: usize = 32;
67const Q8_0_BLOCK_BYTES: usize = 34; // f16 scale (2) + 32 × i8
68
69/// Bytes needed to hold `n_elems` weights in Q8_0 (ceil to whole 32-element blocks).
70pub fn q8_0_bytes(n_elems: usize) -> usize {
71    n_elems.div_ceil(Q8_0_BLOCK_ELEMS) * Q8_0_BLOCK_BYTES
72}
73
74/// Quantize `weights` (f32) into Q8_0 blocks in `out` (must be >= `q8_0_bytes(weights.len())`).
75/// Standard ggml Q8_0: per 32-block, `scale = absmax / 127`, `q = round(w / scale)` clamped to i8,
76/// stored as little-endian f16 scale followed by 32 signed bytes. Returns false if `out` is too small.
77pub fn quantize_q8_0_from_f32(weights: &[f32], out: &mut [u8]) -> bool {
78    if out.len() < q8_0_bytes(weights.len()) {
79        return false;
80    }
81    let n_blocks = weights.len().div_ceil(Q8_0_BLOCK_ELEMS);
82    for b in 0..n_blocks {
83        let start = b * Q8_0_BLOCK_ELEMS;
84        let end = (start + Q8_0_BLOCK_ELEMS).min(weights.len());
85        let absmax = weights[start..end]
86            .iter()
87            .fold(0.0f32, |m, &w| m.max(w.abs()));
88        let scale = if absmax > 0.0 { absmax / 127.0 } else { 1.0 };
89        let inv = if scale > 0.0 { 1.0 / scale } else { 0.0 };
90        let bs = b * Q8_0_BLOCK_BYTES;
91        let s16 = half::f16::from_f32(scale).to_le_bytes();
92        out[bs] = s16[0];
93        out[bs + 1] = s16[1];
94        for j in 0..Q8_0_BLOCK_ELEMS {
95            let q = if start + j < end {
96                (weights[start + j] * inv).round().clamp(-127.0, 127.0) as i8
97            } else {
98                0
99            };
100            out[bs + 2 + j] = q as u8;
101        }
102    }
103    true
104}
105
106/// Bytes needed to hold `n_elems` weights as little-endian IEEE F16 (2 bytes each).
107pub fn f16_bytes(n_elems: usize) -> usize {
108    n_elems * 2
109}
110
111/// Encode `weights` (f32) as little-endian IEEE F16 into `out` (>= `f16_bytes(weights.len())`).
112/// The exact byte layout `dequant_f16` / the GPU `unpack2x16float` path consume.
113pub fn quantize_f16_from_f32(weights: &[f32], out: &mut [u8]) -> bool {
114    if out.len() < f16_bytes(weights.len()) {
115        return false;
116    }
117    for (i, &w) in weights.iter().enumerate() {
118        let h = half::f16::from_f32(w).to_le_bytes();
119        out[i * 2] = h[0];
120        out[i * 2 + 1] = h[1];
121    }
122    true
123}
124
125/// Bytes for `n_elems` weights as ggml Q4_0 (18-byte blocks of 32: f16 scale + 16 nibble bytes).
126pub fn q4_0_bytes(n_elems: usize) -> usize {
127    n_elems.div_ceil(32) * 18
128}
129
130/// Quantize `weights` (f32) to ggml **Q4_0** into `out` (>= `q4_0_bytes`). Matches
131/// `ggml_quants::dequant_q4_0` exactly: per 32-block `d = max_abs_signed / -8`, nibble
132/// `q = clamp(round(x/d)+8, 0..15)`, dequant `x = (q-8)*d`; **interleaved** layout — block index
133/// `k < 16` is the low nibble of byte `k`, `k >= 16` the high nibble of byte `k-16`.
134pub fn quantize_q4_0_from_f32(weights: &[f32], out: &mut [u8]) -> bool {
135    if out.len() < q4_0_bytes(weights.len()) {
136        return false;
137    }
138    let n_blocks = weights.len().div_ceil(32);
139    for b in 0..n_blocks {
140        let start = b * 32;
141        let end = (start + 32).min(weights.len());
142        let mut amax = 0.0f32;
143        let mut max_signed = 0.0f32;
144        for &x in &weights[start..end] {
145            if x.abs() > amax {
146                amax = x.abs();
147                max_signed = x;
148            }
149        }
150        let d = max_signed / -8.0;
151        let id = if d != 0.0 { 1.0 / d } else { 0.0 };
152        let bs = b * 18;
153        out[bs..bs + 2].copy_from_slice(&half::f16::from_f32(d).to_le_bytes());
154        let q = |k: usize| -> u8 {
155            let gk = start + k;
156            if gk >= end {
157                return 8; // (8-8)*d = 0 padding
158            }
159            (weights[gk] * id + 8.5).floor().clamp(0.0, 15.0) as u8
160        };
161        for j in 0..16 {
162            out[bs + 2 + j] = (q(j) & 0x0F) | ((q(j + 16) & 0x0F) << 4);
163        }
164    }
165    true
166}
167
168/// Bytes for `n_elems` weights as ggml Q4_K (144-byte super-blocks of 256).
169pub fn q4_k_bytes(n_elems: usize) -> usize {
170    n_elems.div_ceil(256) * 144
171}
172
173/// Quantize `weights` (f32) to ggml **Q4_K** into `out` (>= `q4_k_bytes`). Matches
174/// `ggml_quants::dequant_q4_k`: super-block of 256 = 8 sub-blocks of 32, each with an asymmetric
175/// scale+min — 6-bit sub-scales (`d*sc`) and mins (`dmin*m`) packed via `get_scale_min_k4`, 4-bit
176/// quants (even sub-block = low nibble, odd = high nibble of the same `qs` byte). Dequant:
177/// `x = d*sc[s]*q - dmin*m[s]`. Simplified vs ggml's iterative search (per-sub-block min clamped ≤ 0,
178/// which holds for zero-centred weights); round-trip tested. Q4_K's 6-bit sub-scales make it markedly
179/// more accurate per bit than Q4_0 — AWQ's intended 4-bit partner.
180pub fn quantize_q4_k_from_f32(weights: &[f32], out: &mut [u8]) -> bool {
181    if out.len() < q4_k_bytes(weights.len()) {
182        return false;
183    }
184    let n_super = weights.len().div_ceil(256);
185    for sb in 0..n_super {
186        let base = sb * 256;
187        let bb = sb * 144;
188        let mut scale_s = [0f32; 8];
189        let mut mt_s = [0f32; 8];
190        for s in 0..8 {
191            let s0 = base + s * 32;
192            if s0 >= weights.len() {
193                continue;
194            }
195            let s1 = (s0 + 32).min(weights.len());
196            let mut mn = f32::INFINITY;
197            let mut mx = f32::NEG_INFINITY;
198            for &x in &weights[s0..s1] {
199                mn = mn.min(x);
200                mx = mx.max(x);
201            }
202            let eff_min = mn.min(0.0); // ≤ 0 so the shared dmin stays non-negative
203            scale_s[s] = ((mx - eff_min) / 15.0).max(0.0);
204            mt_s[s] = -eff_min; // ≥ 0
205        }
206        let d = scale_s.iter().cloned().fold(0.0f32, f32::max) / 63.0;
207        let dmin = mt_s.iter().cloned().fold(0.0f32, f32::max) / 63.0;
208        let idd = if d > 0.0 { 1.0 / d } else { 0.0 };
209        let idm = if dmin > 0.0 { 1.0 / dmin } else { 0.0 };
210        let mut sc = [0u8; 8];
211        let mut m = [0u8; 8];
212        for s in 0..8 {
213            sc[s] = (scale_s[s] * idd).round().clamp(0.0, 63.0) as u8;
214            m[s] = (mt_s[s] * idm).round().clamp(0.0, 63.0) as u8;
215        }
216        out[bb..bb + 2].copy_from_slice(&half::f16::from_f32(d).to_le_bytes());
217        out[bb + 2..bb + 4].copy_from_slice(&half::f16::from_f32(dmin).to_le_bytes());
218        // Pack sc/m into scales[12] — inverse of get_scale_min_k4.
219        let mut scales = [0u8; 12];
220        for j in 0..4 {
221            scales[j] = sc[j] & 63;
222            scales[j + 4] = m[j] & 63;
223        }
224        for j in 4..8 {
225            scales[j + 4] = (sc[j] & 0xF) | ((m[j] & 0xF) << 4);
226            scales[j - 4] |= (sc[j] >> 4) << 6;
227            scales[j] |= (m[j] >> 4) << 6;
228        }
229        out[bb + 4..bb + 16].copy_from_slice(&scales);
230        // Quantize + pack nibbles using the reconstructed grid (x = dq*q - mq).
231        for s in 0..8 {
232            let dq = d * sc[s] as f32;
233            let mq = dmin * m[s] as f32;
234            let idq = if dq > 0.0 { 1.0 / dq } else { 0.0 };
235            for l in 0..32 {
236                let gi = base + s * 32 + l;
237                let q = if gi < weights.len() {
238                    ((weights[gi] + mq) * idq).round().clamp(0.0, 15.0) as u8
239                } else {
240                    0
241                };
242                let byte = bb + 16 + (s / 2) * 32 + l;
243                if s % 2 == 0 {
244                    out[byte] = (out[byte] & 0xF0) | (q & 0x0F);
245                } else {
246                    out[byte] = (out[byte] & 0x0F) | ((q & 0x0F) << 4);
247                }
248            }
249        }
250    }
251    true
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn metrics_zero_for_identical() {
260        let a = [1.0f32, -2.0, 3.5, 0.0, -0.0];
261        assert_eq!(max_abs_err(&a, &a), 0.0);
262        assert_eq!(mean_abs_err(&a, &a), 0.0);
263        assert_eq!(max_ulp_diff(&a, &a), 0);
264    }
265
266    #[test]
267    fn ulp_one_step_is_one() {
268        let x = 1.0f32;
269        let y = f32::from_bits(x.to_bits() + 1);
270        assert_eq!(max_ulp_diff(&[x], &[y]), 1);
271        assert!(max_abs_err(&[x], &[y]) > 0.0);
272    }
273
274    #[test]
275    fn q8_0_roundtrip_within_one_step() {
276        let w: Vec<f32> = (0..32).map(|i| (i as f32 - 16.0) / 16.0).collect();
277        let mut bytes = vec![0u8; q8_0_bytes(w.len())];
278        assert!(quantize_q8_0_from_f32(&w, &mut bytes));
279        let info = crate::gguf_sharder::GgufTensorInfo {
280            dims: [32, 1, 1, 1],
281            n_dims: 1,
282            ggml_type: crate::ggml_quants::GGML_TYPE_Q8_0,
283            byte_offset: 0,
284        };
285        let mut back = vec![0f32; 32];
286        let n = crate::ggml_quants::dequant_matrix_row_into(&bytes, &info, 0, &mut back).unwrap();
287        assert_eq!(n, 32);
288        // absmax = 15/16 → scale ≈ (15/16)/127; round-trip error ≤ one quant step.
289        let step = (15.0f32 / 16.0) / 127.0;
290        assert!(max_abs_err(&w, &back) <= step + 1e-5);
291    }
292
293    #[test]
294    fn q4_0_roundtrip_within_one_step() {
295        let w: Vec<f32> = (0..32).map(|i| (i as f32 - 16.0) / 8.0).collect(); // ~[-2.0, 1.875]
296        let mut bytes = vec![0u8; q4_0_bytes(w.len())];
297        assert!(quantize_q4_0_from_f32(&w, &mut bytes));
298        let info = crate::gguf_sharder::GgufTensorInfo {
299            dims: [32, 1, 1, 1],
300            n_dims: 1,
301            ggml_type: crate::ggml_quants::GGML_TYPE_Q4_0,
302            byte_offset: 0,
303        };
304        let mut back = vec![0f32; 32];
305        let n = crate::ggml_quants::dequant_matrix_row_into(&bytes, &info, 0, &mut back).unwrap();
306        assert_eq!(n, 32);
307        let absmax = w.iter().cloned().fold(0f32, |m, x| m.max(x.abs())); // 2.0
308        let step = absmax / 8.0; // ~0.25 (one Q4_0 level)
309        assert!(
310            max_abs_err(&w, &back) <= step * 1.1,
311            "q4_0 roundtrip err {} > step {step}",
312            max_abs_err(&w, &back)
313        );
314    }
315
316    #[test]
317    fn q4_k_roundtrip_and_beats_q4_0() {
318        // 256-element super-block, zero-centred spread.
319        let w: Vec<f32> = (0..256).map(|i| (i as f32 - 128.0) / 64.0).collect(); // ~[-2.0, 1.98]
320        let info_k = crate::gguf_sharder::GgufTensorInfo {
321            dims: [256, 1, 1, 1],
322            n_dims: 1,
323            ggml_type: crate::ggml_quants::GGML_TYPE_Q4_K,
324            byte_offset: 0,
325        };
326        let mut kbytes = vec![0u8; q4_k_bytes(w.len())];
327        assert!(quantize_q4_k_from_f32(&w, &mut kbytes));
328        let mut back_k = vec![0f32; 256];
329        let nk =
330            crate::ggml_quants::dequant_matrix_row_into(&kbytes, &info_k, 0, &mut back_k).unwrap();
331        assert_eq!(nk, 256);
332        let err_k = max_abs_err(&w, &back_k);
333
334        // Same data through Q4_0 — Q4_K's 6-bit sub-scales should be at least as accurate.
335        let info_0 = crate::gguf_sharder::GgufTensorInfo {
336            dims: [256, 1, 1, 1],
337            n_dims: 1,
338            ggml_type: crate::ggml_quants::GGML_TYPE_Q4_0,
339            byte_offset: 0,
340        };
341        let mut zbytes = vec![0u8; q4_0_bytes(w.len())];
342        assert!(quantize_q4_0_from_f32(&w, &mut zbytes));
343        let mut back_0 = vec![0f32; 256];
344        crate::ggml_quants::dequant_matrix_row_into(&zbytes, &info_0, 0, &mut back_0).unwrap();
345        let err_0 = max_abs_err(&w, &back_0);
346
347        let range = 2.0 + 1.98; // ~3.98
348        eprintln!("q4_k err {err_k:.4} vs q4_0 err {err_0:.4} (range {range:.2})");
349        assert!(
350            err_k.is_finite() && err_k <= range / 15.0 * 1.3,
351            "q4_k roundtrip err {err_k} too high"
352        );
353        assert!(
354            err_k <= err_0 + 1e-4,
355            "q4_k ({err_k}) should not be worse than q4_0 ({err_0})"
356        );
357    }
358}