Skip to main content

fuel_quantized/
iq4.rs

1// Derived in part from llama.cpp / ggml (MIT License, Copyright (c) 2023-2026 The ggml authors).
2// The full licence text is in THIRD-PARTY-NOTICES.md at the repository root. No licence identifier line
3// is stamped here: the file derives from somebody else's copyright.
4//! `IQ4_NL` and `IQ4_XS` block formats (GAP-211 B1).
5//!
6//! # Provenance
7//!
8//! These two formats and their dequantization are llama.cpp's / ggml's
9//! (MIT License, Copyright (c) 2023-2026 The ggml authors). What is taken from
10//! there is the on-disk layout, the 16-entry non-linear codebook and the
11//! dequantization arithmetic, each checked against the source at commit
12//! `9d57ce456c` (the pin recorded on [`GgmlDType`]):
13//!
14//! - `ggml/src/ggml-common.h`: `QK4_NL = 32`, `block_iq4_nl` (`ggml_half d;
15//!   uint8_t qs[QK4_NL/2]`), `block_iq4_xs` (`ggml_half d; uint16_t scales_h;
16//!   uint8_t scales_l[QK_K/64]; uint8_t qs[QK_K/2]`), and `kvalues_iq4nl`.
17//! - `ggml/src/ggml-quants.c`: `dequantize_row_iq4_nl`, `dequantize_row_iq4_xs`.
18//! - `ggml/include/ggml.h`: `GGML_TYPE_IQ4_NL = 20`, `GGML_TYPE_IQ4_XS = 23`.
19//!
20//! The quantizer ([`GgmlType::from_float`]) is NOT llama.cpp's: llama.cpp
21//! searches for the scale that minimises weighted error. This one picks the
22//! scale that maps the block's largest-magnitude value onto the end of the
23//! codebook and rounds to the nearest codebook entry. It writes valid blocks
24//! llama.cpp reads, but not bit-identical ones, and is less accurate; it
25//! exists so the formats satisfy the whole `GgmlType` contract without a
26//! panic path.
27
28use crate::k_quants::{BlockQ8_0, BlockQ8K, GgmlType, QK_K};
29use fuel_ir::quantized::GgmlDType;
30use half::f16;
31
32pub const QK4_NL: usize = 32;
33
34/// `kvalues_iq4nl` from `ggml-common.h`: the codebook both formats index with a
35/// 4-bit code.
36pub const KVALUES_IQ4NL: [i8; 16] = [
37    -127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113,
38];
39
40#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
41#[repr(C)]
42pub struct BlockIq4Nl {
43    pub(crate) d: f16,
44    pub(crate) qs: [u8; QK4_NL / 2],
45}
46const _: () = assert!(std::mem::size_of::<BlockIq4Nl>() == 2 + QK4_NL / 2);
47
48#[derive(Debug, Clone, Copy, PartialEq, bytemuck::Pod, bytemuck::Zeroable)]
49#[repr(C)]
50pub struct BlockIq4Xs {
51    pub(crate) d: f16,
52    pub(crate) scales_h: u16,
53    pub(crate) scales_l: [u8; QK_K / 64],
54    pub(crate) qs: [u8; QK_K / 2],
55}
56const _: () = assert!(std::mem::size_of::<BlockIq4Xs>() == 2 + 2 + QK_K / 64 + QK_K / 2);
57
58/// Index of the codebook entry nearest `v`.
59fn nearest_code(v: f32) -> u8 {
60    let mut best = 0u8;
61    let mut best_err = f32::INFINITY;
62    for (i, &k) in KVALUES_IQ4NL.iter().enumerate() {
63        let err = (v - f32::from(k)).abs();
64        if err < best_err {
65            best_err = err;
66            best = i as u8;
67        }
68    }
69    best
70}
71
72/// The scale that maps the value of largest magnitude onto codebook entry 0
73/// (-127): `max / -127`, sign included so the extreme lands on the long end.
74fn block_scale(xs: &[f32]) -> f32 {
75    let mut amax = 0f32;
76    let mut max = 0f32;
77    for &x in xs {
78        if amax < x.abs() {
79            amax = x.abs();
80            max = x;
81        }
82    }
83    max / f32::from(KVALUES_IQ4NL[0])
84}
85
86/// Quantize 32 values with scale `d` into 16 packed bytes (low nibble =
87/// element `j`, high nibble = element `j + 16`, as `dequantize_row_iq4_nl`).
88fn pack32(xs: &[f32], d: f32, qs: &mut [u8]) {
89    let id = if d != 0.0 { 1.0 / d } else { 0.0 };
90    for j in 0..16 {
91        qs[j] = nearest_code(xs[j] * id) | (nearest_code(xs[j + 16] * id) << 4);
92    }
93}
94
95/// `sum_i dequant(xs)[i] * dequant(ys)[i]` for any pair of block types: the
96/// reference dot product (no SIMD), correct by construction from `to_float`.
97fn dot_via_to_float<X: GgmlType, Y: GgmlType>(n: usize, xs: &[X], ys: &[Y]) -> f32 {
98    let mut a = vec![0f32; n];
99    let mut b = vec![0f32; n];
100    X::to_float(&xs[..n / X::BLCK_SIZE], &mut a);
101    Y::to_float(&ys[..n / Y::BLCK_SIZE], &mut b);
102    a.iter().zip(&b).map(|(x, y)| x * y).sum()
103}
104
105impl GgmlType for BlockIq4Nl {
106    const DTYPE: GgmlDType = GgmlDType::Iq4Nl;
107    const BLCK_SIZE: usize = QK4_NL;
108    type VecDotType = BlockQ8_0;
109
110    // ggml-quants.c `dequantize_row_iq4_nl`.
111    fn to_float(xs: &[Self], ys: &mut [f32]) {
112        for (b, y) in xs.iter().zip(ys.as_chunks_mut::<QK4_NL>().0.iter_mut()) {
113            let d = b.d.to_f32();
114            for j in 0..QK4_NL / 2 {
115                y[j] = d * f32::from(KVALUES_IQ4NL[(b.qs[j] & 0xf) as usize]);
116                y[j + QK4_NL / 2] = d * f32::from(KVALUES_IQ4NL[(b.qs[j] >> 4) as usize]);
117            }
118        }
119    }
120
121    fn from_float(xs: &[f32], ys: &mut [Self]) {
122        for (x, y) in xs.as_chunks::<QK4_NL>().0.iter().zip(ys.iter_mut()) {
123            let d = block_scale(x);
124            y.d = f16::from_f32(d);
125            // Quantize against the scale as stored (f16-rounded), so the
126            // codes are chosen for what `to_float` will multiply by.
127            pack32(x, y.d.to_f32(), &mut y.qs);
128        }
129    }
130
131    fn vec_dot(n: usize, xs: &[Self], ys: &[Self::VecDotType]) -> f32 {
132        Self::vec_dot_unopt(n, xs, ys)
133    }
134
135    fn vec_dot_unopt(n: usize, xs: &[Self], ys: &[Self::VecDotType]) -> f32 {
136        dot_via_to_float(n, xs, ys)
137    }
138}
139
140impl GgmlType for BlockIq4Xs {
141    const DTYPE: GgmlDType = GgmlDType::Iq4Xs;
142    const BLCK_SIZE: usize = QK_K;
143    type VecDotType = BlockQ8K;
144
145    // ggml-quants.c `dequantize_row_iq4_xs`.
146    fn to_float(xs: &[Self], ys: &mut [f32]) {
147        for (b, y) in xs.iter().zip(ys.as_chunks_mut::<QK_K>().0.iter_mut()) {
148            let d = b.d.to_f32();
149            for ib in 0..QK_K / 32 {
150                let ls = ((b.scales_l[ib / 2] >> (4 * (ib % 2))) & 0xf) as i32
151                    | ((((b.scales_h >> (2 * ib)) & 3) as i32) << 4);
152                let dl = d * (ls - 32) as f32;
153                let qs = &b.qs[ib * 16..ib * 16 + 16];
154                let out = &mut y[ib * 32..ib * 32 + 32];
155                for j in 0..16 {
156                    out[j] = dl * f32::from(KVALUES_IQ4NL[(qs[j] & 0xf) as usize]);
157                    out[j + 16] = dl * f32::from(KVALUES_IQ4NL[(qs[j] >> 4) as usize]);
158                }
159            }
160        }
161    }
162
163    fn from_float(xs: &[f32], ys: &mut [Self]) {
164        for (x, y) in xs.as_chunks::<QK_K>().0.iter().zip(ys.iter_mut()) {
165            // Per-32 sub-block scales, then one f16 super-scale `d` such that
166            // each sub-scale is `d * (ls - 32)` with `ls` in 0..=63.
167            let mut sub = [0f32; QK_K / 32];
168            let mut dmax = 0f32;
169            for (ib, s) in sub.iter_mut().enumerate() {
170                *s = block_scale(&x[ib * 32..ib * 32 + 32]);
171                dmax = dmax.max(s.abs());
172            }
173            let d = f16::from_f32(dmax / 31.0);
174            let df = d.to_f32();
175            y.d = d;
176            y.scales_h = 0;
177            y.scales_l = [0; QK_K / 64];
178            for (ib, &s) in sub.iter().enumerate() {
179                let ls = if df != 0.0 {
180                    ((s / df).round() as i32).clamp(-32, 31) + 32
181                } else {
182                    32
183                } as u16;
184                y.scales_l[ib / 2] |= ((ls & 0xf) as u8) << (4 * (ib % 2));
185                y.scales_h |= (ls >> 4) << (2 * ib);
186                let dl = df * (i32::from(ls) - 32) as f32;
187                pack32(
188                    &x[ib * 32..ib * 32 + 32],
189                    dl,
190                    &mut y.qs[ib * 16..ib * 16 + 16],
191                );
192            }
193        }
194    }
195
196    fn vec_dot(n: usize, xs: &[Self], ys: &[Self::VecDotType]) -> f32 {
197        Self::vec_dot_unopt(n, xs, ys)
198    }
199
200    fn vec_dot_unopt(n: usize, xs: &[Self], ys: &[Self::VecDotType]) -> f32 {
201        dot_via_to_float(n, xs, ys)
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    /// The codebook as `ggml-common.h` prints it (`kvalues_iq4nl`), spelled out
210    /// a second time so an edit to [`KVALUES_IQ4NL`] cannot silently pass.
211    #[test]
212    fn codebook_matches_ggml_common_h() {
213        let printed: [i8; 16] = [
214            -127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113,
215        ];
216        assert_eq!(KVALUES_IQ4NL, printed);
217    }
218
219    /// Hand-evaluated `dequantize_row_iq4_nl`: d = 0.5, byte j = (j) | (15-j)<<4,
220    /// so y[j] = 0.5*kv[j] and y[j+16] = 0.5*kv[15-j]. Spot values are
221    /// literal arithmetic (kv[0] = -127 -> -63.5; kv[15] = 113 -> 56.5), not
222    /// read back through the table under test.
223    #[test]
224    fn iq4_nl_dequantizes_as_the_ggml_formula() {
225        let mut qs = [0u8; 16];
226        for (j, q) in qs.iter_mut().enumerate() {
227            *q = j as u8 | ((15 - j as u8) << 4);
228        }
229        let blk = BlockIq4Nl {
230            d: f16::from_f32(0.5),
231            qs,
232        };
233        let mut y = [0f32; 32];
234        BlockIq4Nl::to_float(&[blk], &mut y);
235        assert_eq!(y[0], -63.5);
236        assert_eq!(y[15], 56.5);
237        assert_eq!(y[16], 56.5); // high nibble of byte 0 is 15
238        assert_eq!(y[31], -63.5); // high nibble of byte 15 is 0
239        assert_eq!(y[8], 0.5); // kv[8] = 1
240    }
241
242    /// Hand-evaluated `dequantize_row_iq4_xs`: d = 2.0, sub-block 0 has
243    /// ls = 33 (scales_l[0] low nibble 1, scales_h bits 0..2 = 0b10) so
244    /// dl = 2*(33-32) = 2; sub-block 1 has ls = 30 (scales_l[0] high nibble
245    /// 14, scales_h bits 2..4 = 0b01 -> 14 | 16 = 30) so dl = 2*(30-32) = -4.
246    #[test]
247    fn iq4_xs_dequantizes_as_the_ggml_formula() {
248        let mut blk = BlockIq4Xs::zeros();
249        blk.d = f16::from_f32(2.0);
250        blk.scales_l[0] = 0xE1;
251        blk.scales_h = 0b0000_0110; // sub-block0: 0b10, sub-block1: 0b01
252        blk.qs[0] = 0x80; // sub-block 0, j=0: lo code 0 (-127), hi code 8 (1)
253        blk.qs[16] = 0x0F; // sub-block 1, j=0: lo code 15 (113), hi code 0 (-127)
254        let mut y = vec![0f32; QK_K];
255        BlockIq4Xs::to_float(&[blk], &mut y);
256        assert_eq!(y[0], 2.0 * -127.0);
257        assert_eq!(y[16], 2.0 * 1.0);
258        assert_eq!(y[32], -4.0 * 113.0);
259        assert_eq!(y[48], -4.0 * -127.0);
260    }
261
262    fn wave(n: usize) -> Vec<f32> {
263        (0..n)
264            .map(|i| ((i as f32) * 0.37).sin() * 3.0 + ((i as f32) * 0.11).cos())
265            .collect()
266    }
267
268    /// The quantizer's blocks must round-trip through `to_float` within the
269    /// codebook's resolution (largest gap 24 of 127 -> half-gap ~ 0.10 of the
270    /// block max), for both formats and for a block whose max is negative.
271    #[test]
272    fn quantize_then_dequantize_stays_within_codebook_resolution() {
273        for sign in [1.0f32, -1.0] {
274            let x: Vec<f32> = wave(QK_K).into_iter().map(|v| v * sign).collect();
275            let amax = x.iter().fold(0f32, |m, v| m.max(v.abs()));
276
277            let mut nl = vec![BlockIq4Nl::zeros(); QK_K / QK4_NL];
278            BlockIq4Nl::from_float(&x, &mut nl);
279            let mut y = vec![0f32; QK_K];
280            BlockIq4Nl::to_float(&nl, &mut y);
281            let nl_err = x
282                .iter()
283                .zip(&y)
284                .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
285
286            let mut xs = vec![BlockIq4Xs::zeros()];
287            BlockIq4Xs::from_float(&x, &mut xs);
288            BlockIq4Xs::to_float(&xs, &mut y);
289            let xs_err = x
290                .iter()
291                .zip(&y)
292                .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
293
294            assert!(nl_err <= 0.14 * amax, "iq4_nl err {nl_err} vs amax {amax}");
295            assert!(xs_err <= 0.20 * amax, "iq4_xs err {xs_err} vs amax {amax}");
296            // Control: the bound is not vacuous -- an all-zero block is far off.
297            assert!(amax > 1.0);
298        }
299    }
300
301    /// An all-zero input must not divide by zero or produce NaN.
302    #[test]
303    fn all_zero_input_quantizes_to_zero() {
304        let x = vec![0f32; QK_K];
305        let mut xs = vec![BlockIq4Xs::zeros()];
306        BlockIq4Xs::from_float(&x, &mut xs);
307        let mut y = vec![1f32; QK_K];
308        BlockIq4Xs::to_float(&xs, &mut y);
309        assert!(y.iter().all(|v| *v == 0.0));
310    }
311
312    /// `vec_dot` against the Q8 activation type equals the dot of the two
313    /// dequantized rows.
314    #[test]
315    fn vec_dot_matches_dequantized_dot() {
316        let x = wave(QK_K);
317        let a: Vec<f32> = wave(QK_K).iter().map(|v| v * 0.5 + 0.1).collect();
318        let mut w = vec![BlockIq4Nl::zeros(); QK_K / QK4_NL];
319        BlockIq4Nl::from_float(&x, &mut w);
320        let mut q = vec![BlockQ8_0::zeros(); QK_K / 32];
321        BlockQ8_0::from_float(&a, &mut q);
322        let (mut wf, mut qf) = (vec![0f32; QK_K], vec![0f32; QK_K]);
323        BlockIq4Nl::to_float(&w, &mut wf);
324        BlockQ8_0::to_float(&q, &mut qf);
325        let want: f32 = wf.iter().zip(&qf).map(|(p, r)| p * r).sum();
326        let got = BlockIq4Nl::vec_dot(QK_K, &w, &q);
327        assert!(
328            (got - want).abs() <= 1e-3 * want.abs().max(1.0),
329            "{got} vs {want}"
330        );
331    }
332}