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::{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
95impl GgmlType for BlockIq4Nl {
96    const DTYPE: GgmlDType = GgmlDType::Iq4Nl;
97    const BLCK_SIZE: usize = QK4_NL;
98
99    // ggml-quants.c `dequantize_row_iq4_nl`.
100    fn to_float(xs: &[Self], ys: &mut [f32]) {
101        for (b, y) in xs.iter().zip(ys.as_chunks_mut::<QK4_NL>().0.iter_mut()) {
102            let d = b.d.to_f32();
103            for j in 0..QK4_NL / 2 {
104                y[j] = d * f32::from(KVALUES_IQ4NL[(b.qs[j] & 0xf) as usize]);
105                y[j + QK4_NL / 2] = d * f32::from(KVALUES_IQ4NL[(b.qs[j] >> 4) as usize]);
106            }
107        }
108    }
109
110    fn from_float(xs: &[f32], ys: &mut [Self]) {
111        for (x, y) in xs.as_chunks::<QK4_NL>().0.iter().zip(ys.iter_mut()) {
112            let d = block_scale(x);
113            y.d = f16::from_f32(d);
114            // Quantize against the scale as stored (f16-rounded), so the
115            // codes are chosen for what `to_float` will multiply by.
116            pack32(x, y.d.to_f32(), &mut y.qs);
117        }
118    }
119}
120
121impl GgmlType for BlockIq4Xs {
122    const DTYPE: GgmlDType = GgmlDType::Iq4Xs;
123    const BLCK_SIZE: usize = QK_K;
124
125    // ggml-quants.c `dequantize_row_iq4_xs`.
126    fn to_float(xs: &[Self], ys: &mut [f32]) {
127        for (b, y) in xs.iter().zip(ys.as_chunks_mut::<QK_K>().0.iter_mut()) {
128            let d = b.d.to_f32();
129            for ib in 0..QK_K / 32 {
130                let ls = ((b.scales_l[ib / 2] >> (4 * (ib % 2))) & 0xf) as i32
131                    | ((((b.scales_h >> (2 * ib)) & 3) as i32) << 4);
132                let dl = d * (ls - 32) as f32;
133                let qs = &b.qs[ib * 16..ib * 16 + 16];
134                let out = &mut y[ib * 32..ib * 32 + 32];
135                for j in 0..16 {
136                    out[j] = dl * f32::from(KVALUES_IQ4NL[(qs[j] & 0xf) as usize]);
137                    out[j + 16] = dl * f32::from(KVALUES_IQ4NL[(qs[j] >> 4) as usize]);
138                }
139            }
140        }
141    }
142
143    fn from_float(xs: &[f32], ys: &mut [Self]) {
144        for (x, y) in xs.as_chunks::<QK_K>().0.iter().zip(ys.iter_mut()) {
145            // Per-32 sub-block scales, then one f16 super-scale `d` such that
146            // each sub-scale is `d * (ls - 32)` with `ls` in 0..=63.
147            let mut sub = [0f32; QK_K / 32];
148            let mut dmax = 0f32;
149            for (ib, s) in sub.iter_mut().enumerate() {
150                *s = block_scale(&x[ib * 32..ib * 32 + 32]);
151                dmax = dmax.max(s.abs());
152            }
153            let d = f16::from_f32(dmax / 31.0);
154            let df = d.to_f32();
155            y.d = d;
156            y.scales_h = 0;
157            y.scales_l = [0; QK_K / 64];
158            for (ib, &s) in sub.iter().enumerate() {
159                let ls = if df != 0.0 {
160                    ((s / df).round() as i32).clamp(-32, 31) + 32
161                } else {
162                    32
163                } as u16;
164                y.scales_l[ib / 2] |= ((ls & 0xf) as u8) << (4 * (ib % 2));
165                y.scales_h |= (ls >> 4) << (2 * ib);
166                let dl = df * (i32::from(ls) - 32) as f32;
167                pack32(
168                    &x[ib * 32..ib * 32 + 32],
169                    dl,
170                    &mut y.qs[ib * 16..ib * 16 + 16],
171                );
172            }
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    /// The codebook as `ggml-common.h` prints it (`kvalues_iq4nl`), spelled out
182    /// a second time so an edit to [`KVALUES_IQ4NL`] cannot silently pass.
183    #[test]
184    fn codebook_matches_ggml_common_h() {
185        let printed: [i8; 16] = [
186            -127, -104, -83, -65, -49, -35, -22, -10, 1, 13, 25, 38, 53, 69, 89, 113,
187        ];
188        assert_eq!(KVALUES_IQ4NL, printed);
189    }
190
191    /// Hand-evaluated `dequantize_row_iq4_nl`: d = 0.5, byte j = (j) | (15-j)<<4,
192    /// so y[j] = 0.5*kv[j] and y[j+16] = 0.5*kv[15-j]. Spot values are
193    /// literal arithmetic (kv[0] = -127 -> -63.5; kv[15] = 113 -> 56.5), not
194    /// read back through the table under test.
195    #[test]
196    fn iq4_nl_dequantizes_as_the_ggml_formula() {
197        let mut qs = [0u8; 16];
198        for (j, q) in qs.iter_mut().enumerate() {
199            *q = j as u8 | ((15 - j as u8) << 4);
200        }
201        let blk = BlockIq4Nl {
202            d: f16::from_f32(0.5),
203            qs,
204        };
205        let mut y = [0f32; 32];
206        BlockIq4Nl::to_float(&[blk], &mut y);
207        assert_eq!(y[0], -63.5);
208        assert_eq!(y[15], 56.5);
209        assert_eq!(y[16], 56.5); // high nibble of byte 0 is 15
210        assert_eq!(y[31], -63.5); // high nibble of byte 15 is 0
211        assert_eq!(y[8], 0.5); // kv[8] = 1
212    }
213
214    /// Hand-evaluated `dequantize_row_iq4_xs`: d = 2.0, sub-block 0 has
215    /// ls = 33 (scales_l[0] low nibble 1, scales_h bits 0..2 = 0b10) so
216    /// dl = 2*(33-32) = 2; sub-block 1 has ls = 30 (scales_l[0] high nibble
217    /// 14, scales_h bits 2..4 = 0b01 -> 14 | 16 = 30) so dl = 2*(30-32) = -4.
218    #[test]
219    fn iq4_xs_dequantizes_as_the_ggml_formula() {
220        let mut blk = BlockIq4Xs::zeros();
221        blk.d = f16::from_f32(2.0);
222        blk.scales_l[0] = 0xE1;
223        blk.scales_h = 0b0000_0110; // sub-block0: 0b10, sub-block1: 0b01
224        blk.qs[0] = 0x80; // sub-block 0, j=0: lo code 0 (-127), hi code 8 (1)
225        blk.qs[16] = 0x0F; // sub-block 1, j=0: lo code 15 (113), hi code 0 (-127)
226        let mut y = vec![0f32; QK_K];
227        BlockIq4Xs::to_float(&[blk], &mut y);
228        assert_eq!(y[0], 2.0 * -127.0);
229        assert_eq!(y[16], 2.0 * 1.0);
230        assert_eq!(y[32], -4.0 * 113.0);
231        assert_eq!(y[48], -4.0 * -127.0);
232    }
233
234    fn wave(n: usize) -> Vec<f32> {
235        (0..n)
236            .map(|i| ((i as f32) * 0.37).sin() * 3.0 + ((i as f32) * 0.11).cos())
237            .collect()
238    }
239
240    /// The quantizer's blocks must round-trip through `to_float` within the
241    /// codebook's resolution (largest gap 24 of 127 -> half-gap ~ 0.10 of the
242    /// block max), for both formats and for a block whose max is negative.
243    #[test]
244    fn quantize_then_dequantize_stays_within_codebook_resolution() {
245        for sign in [1.0f32, -1.0] {
246            let x: Vec<f32> = wave(QK_K).into_iter().map(|v| v * sign).collect();
247            let amax = x.iter().fold(0f32, |m, v| m.max(v.abs()));
248
249            let mut nl = vec![BlockIq4Nl::zeros(); QK_K / QK4_NL];
250            BlockIq4Nl::from_float(&x, &mut nl);
251            let mut y = vec![0f32; QK_K];
252            BlockIq4Nl::to_float(&nl, &mut y);
253            let nl_err = x
254                .iter()
255                .zip(&y)
256                .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
257
258            let mut xs = vec![BlockIq4Xs::zeros()];
259            BlockIq4Xs::from_float(&x, &mut xs);
260            BlockIq4Xs::to_float(&xs, &mut y);
261            let xs_err = x
262                .iter()
263                .zip(&y)
264                .fold(0f32, |m, (a, b)| m.max((a - b).abs()));
265
266            assert!(nl_err <= 0.14 * amax, "iq4_nl err {nl_err} vs amax {amax}");
267            assert!(xs_err <= 0.20 * amax, "iq4_xs err {xs_err} vs amax {amax}");
268            // Control: the bound is not vacuous -- an all-zero block is far off.
269            assert!(amax > 1.0);
270        }
271    }
272
273    /// An all-zero input must not divide by zero or produce NaN.
274    #[test]
275    fn all_zero_input_quantizes_to_zero() {
276        let x = vec![0f32; QK_K];
277        let mut xs = vec![BlockIq4Xs::zeros()];
278        BlockIq4Xs::from_float(&x, &mut xs);
279        let mut y = vec![1f32; QK_K];
280        BlockIq4Xs::to_float(&xs, &mut y);
281        assert!(y.iter().all(|v| *v == 0.0));
282    }
283
284    /// The quantized matmul against IQ4_NL weights equals the dot of the dequantized row with the f32
285    /// activation row.
286    #[test]
287    fn matmul_matches_the_dequantized_dot() {
288        let x = wave(QK_K);
289        let a: Vec<f32> = wave(QK_K).iter().map(|v| v * 0.5 + 0.1).collect();
290        let mut w = vec![BlockIq4Nl::zeros(); QK_K / QK4_NL];
291        BlockIq4Nl::from_float(&x, &mut w);
292        let mut wf = vec![0f32; QK_K];
293        BlockIq4Nl::to_float(&w, &mut wf);
294        let want: f32 = wf.iter().zip(&a).map(|(p, r)| p * r).sum();
295        let mut got = [0f32; 1];
296        crate::k_quants::matmul((1, QK_K, 1), &a, &w, &mut got).expect("matmul");
297        assert!(
298            (got[0] - want).abs() <= 1e-3 * want.abs().max(1.0),
299            "{} vs {want}",
300            got[0]
301        );
302    }
303}