Skip to main content

ferrox_quant/encode/
q4_k.rs

1//! The Q4_K weight encoder: a transcription of llama.cpp's
2//! `quantize_row_q4_K_ref` (`ggml/src/ggml-quants.c`), not a
3//! reimplementation of it.
4//!
5//! A K-quant is NOT min/max over a block. Q4_K's 256-element
6//! super-block is fitted in three stages, and every one of them has to
7//! be reproduced exactly or the file differs:
8//!
9//! 1. Each of the 8 sub-blocks of 32 gets an **iterative** affine fit
10//!    (`make_qkx2_quants`): 21 candidate inverse scales are tried, each
11//!    one re-solves a weighted least-squares for (scale, min) from the
12//!    integer codes it produced, and the lowest weighted squared error
13//!    wins. The weights are `sqrt(mean(x^2)) + |x|`, so a sub-block's
14//!    large values pull the fit toward themselves.
15//! 2. The 8 scales and 8 mins are themselves quantized to 6 bits
16//!    against the super-block's `d`/`dmin` and packed into 12 bytes.
17//! 3. The 4-bit codes are then recomputed **against the 6-bit-rounded**
18//!    scale and min, not against the fit from stage 1 -- so stage 3
19//!    sees a slightly different affine map than stage 1 did.
20//!
21//! A naive min/max encoder skips all three and produces a file that
22//! loads and generates measurably worse text. That is the failure this
23//! module exists to not ship, so the arithmetic below is deliberately
24//! the same shape as the C, down to the operation order in the
25//! least-squares accumulation.
26//!
27//! Deviations from upstream, all of them shown not to change a byte by
28//! `q4_k_matches_llama_cpp_quantize_row_q4_k_ref` in `tests`:
29//!
30//! * `nearest_int`'s `assert(fabsf(fval) <= 4194303.f)` is not
31//!   reproduced. It is compiled out of the release `libggml` that
32//!   `llama-quantize` actually links, so asserting here would make
33//!   ferrox stop where llama.cpp proceeds -- a refusal that fires on
34//!   input llama.cpp handles is not coverage, it is a different tool.
35//! * The 6-bit scale/min are unpacked for stage 3 by the same
36//!   [`crate::q4_k_scale_min`] the *reader* uses, rather than by a
37//!   second copy of `get_scale_min_k4`. Two copies of that bit-packing
38//!   is precisely the shape of bug this repo keeps finding; one
39//!   function means the encoder and the decoder cannot disagree about
40//!   what was packed.
41
42use half::f16;
43
44use crate::{q4_k_scale_min, Q4_K_BLOCK_BYTES, Q4_K_BLOCK_ELEMS, Q4_K_SCALE_BYTES};
45
46/// Sub-blocks per Q4_K super-block, and elements in each.
47const SUB: usize = 8;
48const SUB_ELEMS: usize = Q4_K_BLOCK_ELEMS / SUB; // 32
49
50/// ggml's `nearest_int`: add 1.5 * 2^23 so the mantissa's low bits hold
51/// the rounded integer, then read them back out.
52///
53/// This is **round-half-to-even**, because it is the FPU's own rounding
54/// mode that does the work. `f32::round` is round-half-away-from-zero
55/// and disagrees on every exact tie -- and ties are not rare here: the
56/// candidate inverse scales in [`make_qkx2_quants`] walk a 0.1-wide
57/// grid, so `iscale * (x - min)` lands on `.5` constantly.
58#[inline]
59fn nearest_int(fval: f32) -> i32 {
60    let val = fval + 12_582_912.0f32;
61    let i = val.to_bits() as i32;
62    (i & 0x007f_ffff) - 0x0040_0000
63}
64
65/// llama.cpp's `make_qkx2_quants`: fit `x[i] ~= scale * L[i] - the_min`
66/// with `L[i]` in `0..=nmax`, minimising the `weights`-weighted error.
67///
68/// Returns `(scale, the_min)` and fills `l`. `laux` is scratch, passed
69/// in rather than allocated because the C does the same and this runs
70/// once per 32 weights of the checkpoint.
71///
72/// The signature is upstream's, `use_mad` and all: Q2_K passes `true`
73/// with `n = 16`, Q5_K passes `nmax = 31`. Keeping the parameters means
74/// the next K-quant is a call, not a copy of this function with two
75/// constants changed -- which is how this repo has lost a model feature
76/// eight times.
77#[allow(clippy::too_many_arguments)]
78fn make_qkx2_quants(
79    x: &[f32],
80    weights: &[f32],
81    l: &mut [u8],
82    laux: &mut [u8],
83    nmax: i32,
84    rmin: f32,
85    rdelta: f32,
86    nstep: i32,
87    use_mad: bool,
88) -> (f32, f32) {
89    let n = x.len();
90    debug_assert_eq!(weights.len(), n);
91    debug_assert_eq!(l.len(), n);
92    debug_assert!(laux.len() >= n);
93
94    // Deliberately not `min.min(x[i])` / `max.max(x[i])`. They differ
95    // from the C comparisons only when `x[0]` is NaN -- Rust's
96    // `f32::min` returns the non-NaN operand, `x[i] < NaN` is false and
97    // keeps the NaN -- so no fixture can tell them apart on a real
98    // checkpoint. The C's shape is kept anyway, because a checkpoint
99    // with a NaN weight should produce llama.cpp's bytes rather than
100    // politely different ones. Same choice, same reason, as the `amax`
101    // fold in the Q8_0 encoder next door.
102    let mut min = x[0];
103    let mut max = x[0];
104    let mut sum_w = weights[0];
105    let mut sum_x = sum_w * x[0];
106    for i in 1..n {
107        if x[i] < min {
108            min = x[i];
109        }
110        if x[i] > max {
111            max = x[i];
112        }
113        let w = weights[i];
114        sum_w += w;
115        sum_x += w * x[i];
116    }
117    if min > 0.0 {
118        min = 0.0;
119    }
120    if max == min {
121        l[..n].fill(0);
122        return (0.0, -min);
123    }
124
125    let mut iscale = nmax as f32 / (max - min);
126    let mut scale = 1.0 / iscale;
127    let mut best_error = 0.0f32;
128    for i in 0..n {
129        let li = nearest_int(iscale * (x[i] - min)).clamp(0, nmax);
130        l[i] = li as u8;
131        let diff = scale * l[i] as f32 + min - x[i];
132        let diff = if use_mad { diff.abs() } else { diff * diff };
133        best_error += weights[i] * diff;
134    }
135    if nstep < 1 {
136        return (scale, -min);
137    }
138
139    for is in 0..=nstep {
140        iscale = (rmin + rdelta * is as f32 + nmax as f32) / (max - min);
141        let (mut sum_l, mut sum_l2, mut sum_xl) = (0.0f32, 0.0f32, 0.0f32);
142        for i in 0..n {
143            let li = nearest_int(iscale * (x[i] - min)).clamp(0, nmax);
144            laux[i] = li as u8;
145            let w = weights[i];
146            sum_l += w * li as f32;
147            sum_l2 += w * li as f32 * li as f32;
148            sum_xl += w * li as f32 * x[i];
149        }
150        let det = sum_w * sum_l2 - sum_l * sum_l;
151        if det > 0.0 {
152            let mut this_scale = (sum_w * sum_xl - sum_x * sum_l) / det;
153            let mut this_min = (sum_l2 * sum_x - sum_l * sum_xl) / det;
154            if this_min > 0.0 {
155                this_min = 0.0;
156                this_scale = sum_xl / sum_l2;
157            }
158            let mut cur_error = 0.0f32;
159            for i in 0..n {
160                let diff = this_scale * laux[i] as f32 + this_min - x[i];
161                let diff = if use_mad { diff.abs() } else { diff * diff };
162                cur_error += weights[i] * diff;
163            }
164            if cur_error < best_error {
165                l[..n].copy_from_slice(&laux[..n]);
166                best_error = cur_error;
167                scale = this_scale;
168                min = this_min;
169            }
170        }
171    }
172    (scale, -min)
173}
174
175/// Runs one 32-element sub-block through exactly the path
176/// [`encode_block_q4_k`] uses and returns its `(scale, min)`.
177///
178/// Tooling, not a code path: it exists so a single sub-block can be
179/// compared against llama.cpp's own `make_qkx2_quants` on the same
180/// input. Chasing a floating-point difference that affects 0.55% of
181/// super-blocks by quantizing whole checkpoints is far too coarse a
182/// loop, and `examples/q4k_probe.rs` is the other half of it.
183#[doc(hidden)]
184pub fn probe_sub_block(xs: &[f32]) -> (f32, f32) {
185    assert_eq!(xs.len(), SUB_ELEMS);
186    let mut l = [0u8; SUB_ELEMS];
187    let mut laux = [0u8; SUB_ELEMS];
188    let mut weights = [0f32; SUB_ELEMS];
189    let mut sum_x2 = 0f32;
190    for &v in xs {
191        sum_x2 += v * v;
192    }
193    let av_x = (sum_x2 / SUB_ELEMS as f32).sqrt();
194    for (w, &v) in weights.iter_mut().zip(xs) {
195        *w = av_x + v.abs();
196    }
197    make_qkx2_quants(xs, &weights, &mut l, &mut laux, 15, -1.0, 0.1, 20, false)
198}
199
200/// Encodes one Q4_K super-block (exactly [`Q4_K_BLOCK_ELEMS`] values)
201/// and appends its [`Q4_K_BLOCK_BYTES`] bytes to `out`.
202pub fn encode_block_q4_k(block: &[f32; Q4_K_BLOCK_ELEMS], out: &mut Vec<u8>) {
203    // `l` is deliberately carried from stage 1 into stage 3. Stage 3
204    // skips any sub-block whose reconstructed `d` rounded to zero (`if
205    // (!d) continue;` upstream), and the codes then written are the
206    // ones stage 1 left behind -- NOT zeros. Clearing `l` per sub-block
207    // reads as tidier and writes a different file.
208    let mut l = [0u8; Q4_K_BLOCK_ELEMS];
209    let mut laux = [0u8; SUB_ELEMS];
210    let mut weights = [0f32; SUB_ELEMS];
211    let mut mins = [0f32; SUB];
212    let mut scales = [0f32; SUB];
213
214    let mut max_scale = 0f32; // deducting the min keeps scales positive
215    let mut max_min = 0f32;
216    for j in 0..SUB {
217        let lo = SUB_ELEMS * j;
218        let xs = &block[lo..lo + SUB_ELEMS];
219        let mut sum_x2 = 0f32;
220        for &v in xs {
221            sum_x2 += v * v;
222        }
223        let av_x = (sum_x2 / SUB_ELEMS as f32).sqrt();
224        for (w, &v) in weights.iter_mut().zip(xs) {
225            *w = av_x + v.abs();
226        }
227        let (scale, min) = make_qkx2_quants(
228            xs,
229            &weights,
230            &mut l[lo..lo + SUB_ELEMS],
231            &mut laux,
232            15,
233            -1.0,
234            0.1,
235            20,
236            false,
237        );
238        scales[j] = scale;
239        mins[j] = min;
240        if scale > max_scale {
241            max_scale = scale;
242        }
243        if min > max_min {
244            max_min = min;
245        }
246    }
247
248    let inv_scale = if max_scale > 0.0 {
249        63.0 / max_scale
250    } else {
251        0.0
252    };
253    let inv_min = if max_min > 0.0 { 63.0 / max_min } else { 0.0 };
254    let mut packed = [0u8; Q4_K_SCALE_BYTES];
255    for j in 0..SUB {
256        // Upstream's `MIN(63, ls)`. It cannot fire on THIS path:
257        // `inv_scale` is `63/max_scale` and `max_scale` is the largest
258        // of `scales`, so the product is at most 63 plus an ulp and
259        // rounds to 63. It is kept because it is what the C says and
260        // because the imatrix variant of this encoder
261        // (`quantize_row_q4_K_impl`) reaches the same packing from
262        // `make_qp_quants`, where the bound is not automatic -- but no
263        // fixture here can turn its removal red, and saying so is
264        // better than implying the golden covers it.
265        // The cast comes BEFORE the clamp, because upstream's does:
266        //
267        //     uint8_t ls = nearest_int(inv_scale*scales[j]);
268        //     ls = MIN(63, ls);
269        //
270        // `nearest_int` returns `int`, and storing it in a `uint8_t`
271        // truncates to eight bits FIRST. Clamping to 63 and casting
272        // afterwards is the same for every value in `0..=255` and
273        // different for a negative one: C wraps -1 to 255 and then
274        // clamps to 63, this order clamps -1 to -1 and casts to 255.
275        //
276        // A negative reaches here when a sub-block's least-squares fit
277        // returns a negative scale while some other sub-block's is
278        // positive, so `inv_scale` is positive and the product is not.
279        // Upstream's comment says scales are always positive "as we are
280        // deducting the min", which is the assumption this arithmetic
281        // quietly does not rely on. Rare, and it was 0.55% of the
282        // super-blocks in a real Qwen3-0.6B tensor.
283        let ls = (nearest_int(inv_scale * scales[j]) as u8).min(63);
284        let lm = (nearest_int(inv_min * mins[j]) as u8).min(63);
285        if j < 4 {
286            packed[j] = ls;
287            packed[j + 4] = lm;
288        } else {
289            packed[j + 4] = (ls & 0xF) | ((lm & 0xF) << 4);
290            packed[j - 4] |= (ls >> 4) << 6;
291            packed[j] |= (lm >> 4) << 6;
292        }
293    }
294    let d = f16::from_f32(max_scale / 63.0);
295    let dmin = f16::from_f32(max_min / 63.0);
296
297    for j in 0..SUB {
298        let (sc, m) = q4_k_scale_min(j, &packed);
299        let dj = d.to_f32() * sc as f32;
300        if dj == 0.0 {
301            continue;
302        }
303        let dm = dmin.to_f32() * m as f32;
304        for ii in 0..SUB_ELEMS {
305            let idx = SUB_ELEMS * j + ii;
306            l[idx] = nearest_int((block[idx] + dm) / dj).clamp(0, 15) as u8;
307        }
308    }
309
310    out.reserve(Q4_K_BLOCK_BYTES);
311    out.extend_from_slice(&d.to_le_bytes());
312    out.extend_from_slice(&dmin.to_le_bytes());
313    out.extend_from_slice(&packed);
314    for j in (0..Q4_K_BLOCK_ELEMS).step_by(64) {
315        for i in 0..32 {
316            out.push(l[j + i] | (l[j + i + 32] << 4));
317        }
318    }
319}
320
321/// Encodes a whole row (or any slice whose length is a multiple of
322/// [`Q4_K_BLOCK_ELEMS`]) into Q4_K super-blocks, appending to `out`.
323///
324/// Returns `None` when `src.len()` is not a multiple of the super-block
325/// size. llama.cpp handles that case by silently *changing type* --
326/// `tensor_type_fallback` rewrites a Q4_K tensor with an awkward row
327/// length to Q5_0, and to F16 if that does not fit either -- and ferrox
328/// has neither encoder, so this refuses instead of padding. Padding
329/// would write more elements than the tensor's shape declares and every
330/// following row would decode shifted.
331pub fn encode_row_q4_k(src: &[f32], out: &mut Vec<u8>) -> Option<()> {
332    let (blocks, rest) = src.as_chunks::<Q4_K_BLOCK_ELEMS>();
333    if !rest.is_empty() {
334        return None;
335    }
336    out.reserve(blocks.len() * Q4_K_BLOCK_BYTES);
337    for block in blocks {
338        encode_block_q4_k(block, out);
339    }
340    Some(())
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346    use crate::dequant_q4_k;
347
348    /// Four super-blocks of deterministic, **f16-shaped** input, built
349    /// so that every branch of the reference a plausible rewrite would
350    /// get wrong is exercised at least once.
351    ///
352    /// f16-shaped is not decoration. Step 1 of this work learned it the
353    /// expensive way: its Q8_0 golden was documented as catching
354    /// `v * (1/d)` versus `v / d` and did not, because over uniform f32
355    /// noise the two spellings agree for 8192 consecutive values. f16's
356    /// 11-bit mantissa lands on rounding boundaries constantly, and
357    /// real weights are f16, so the fixture is f16.
358    ///
359    /// The sub-block roster, by index (32 sub-blocks of 32 values):
360    ///
361    /// * 8 -- all zero: `max == min`, the early return that fills the
362    ///   codes with 0 and reports a scale of 0.
363    /// * 9 -- constant non-zero: `max == min` again, but with a min
364    ///   that is clamped to 0 because it is positive.
365    /// * 10 -- all positive: exercises `if (min > 0) min = 0`.
366    /// * 11 -- all negative: `max` is negative and `min` is not clamped.
367    /// * 17 -- four orders of magnitude smaller than its super-block's
368    ///   neighbours, so its 6-bit scale rounds to **zero** and stage 3
369    ///   skips it. The codes written for it are the ones stage 1 left
370    ///   in `l`; an encoder that clears `l` per sub-block writes 32
371    ///   different bytes here and nowhere else.
372    /// * everything else -- weight-like noise at one of four gains, so
373    ///   sub-blocks within a super-block disagree about scale and the
374    ///   6-bit scale quantization actually has to do something.
375    ///
376    /// **The seed is not decorative either.** Two of the reference's
377    /// decisions -- `nearest_int`'s round-half-to-even and the
378    /// `this_min > 0` clamp inside the least-squares step -- only show
379    /// up on some data, and the first seed tried exercised neither: the
380    /// whole golden stayed green with `f32::round` substituted for
381    /// `nearest_int`. This one was picked by encoding 3999 candidate
382    /// fixtures twice, once with each spelling of every decision in the
383    /// reference, and keeping a seed where all of them differ. 255 of
384    /// the 3999 qualify, so this is a fixture chosen to be able to
385    /// fail, not a seed fitted to one assertion.
386    fn sample_input() -> Vec<f32> {
387        const GAINS: [f32; 4] = [0.02, 0.05, 0.1, 0.25];
388        let mut state: u32 = 0xb54c_da26;
389        let mut next = move || {
390            state ^= state << 13;
391            state ^= state >> 17;
392            state ^= state << 5;
393            // [-1, 1)
394            ((state >> 8) as f32 / 8_388_608.0) - 1.0
395        };
396        let mut out = Vec::with_capacity(4 * Q4_K_BLOCK_ELEMS);
397        for sub in 0..4 * SUB {
398            for _ in 0..SUB_ELEMS {
399                let v = next();
400                let shaped = match sub {
401                    8 => 0.0,
402                    9 => 0.125,
403                    10 => v.abs() * 0.05 + 0.01,
404                    11 => -(v.abs() * 0.05 + 0.01),
405                    17 => v * 1e-4,
406                    _ => v * GAINS[sub % GAINS.len()],
407                };
408                out.push(f16::from_f32(shaped).to_f32());
409            }
410        }
411        out
412    }
413
414    /// Regenerates the golden below. Ignored, because it needs a
415    /// llama.cpp checkout: it writes [`sample_input`] as raw
416    /// little-endian f32 to `$FERROX_Q4_K_FIXTURE_OUT`, which the C
417    /// harness described in the PR body then feeds to llama.cpp's own
418    /// encoder.
419    ///
420    /// The input lives here and only here. A C harness that re-derived
421    /// the same values from a copy of the generator would be two
422    /// structures that must agree with nothing enforcing it -- this
423    /// repo's dominant bug shape -- and it would silently compare two
424    /// different inputs the day one copy drifted.
425    #[test]
426    #[ignore = "developer tool: regenerates LLAMA_CPP_Q4_K_GOLDEN"]
427    fn dump_the_fixture_the_c_harness_reads() {
428        let path = std::env::var("FERROX_Q4_K_FIXTURE_OUT")
429            .expect("set FERROX_Q4_K_FIXTURE_OUT to the path to write");
430        let mut bytes = Vec::new();
431        for v in sample_input() {
432            bytes.extend_from_slice(&v.to_le_bytes());
433        }
434        std::fs::write(path, bytes).unwrap();
435    }
436
437    /// llama.cpp's own bytes for [`sample_input`].
438    ///
439    /// Produced by linking `.scratch/llama.cpp/build/bin/libggml-base`
440    /// and calling the exported `quantize_row_q4_K_ref` on the f32s
441    /// [`dump_the_fixture_the_c_harness_reads`] writes. The same
442    /// harness also calls `ggml_quantize_chunk(GGML_TYPE_Q4_K, ...)` --
443    /// the entry point `llama-quantize` itself goes through -- and
444    /// asserts the two agree, so this is what the real tool writes and
445    /// not merely what a reference function does.
446    const LLAMA_CPP_Q4_K_GOLDEN: [u8; 4 * Q4_K_BLOCK_BYTES] = [
447        0x32, 0x10, 0x14, 0x1c, 0x05, 0x0c, 0x59, 0xff, 0x04, 0x0b, 0x58, 0xff, 0x55, 0xcc, 0x8a,
448        0xb3, 0xed, 0xc8, 0xba, 0x4e, 0xeb, 0x91, 0x85, 0xa6, 0x9c, 0x87, 0xd8, 0xab, 0x42, 0xe9,
449        0x87, 0x0b, 0xb3, 0x82, 0x59, 0xb2, 0xc0, 0x80, 0x87, 0xa7, 0x98, 0x62, 0x75, 0x94, 0x31,
450        0x0a, 0x89, 0xda, 0xc5, 0x32, 0xd4, 0xfa, 0xf6, 0xd6, 0xc1, 0xbd, 0xf1, 0xc8, 0x6c, 0xbf,
451        0xc4, 0xa0, 0xeb, 0x46, 0x7d, 0xb0, 0xf4, 0xb7, 0x95, 0xbc, 0xd1, 0xe6, 0x84, 0x8d, 0x77,
452        0x1d, 0x01, 0xd7, 0x1f, 0xda, 0x1b, 0xf6, 0x4f, 0x62, 0x3f, 0xce, 0x28, 0x47, 0x5b, 0xba,
453        0xeb, 0xfc, 0x04, 0xb3, 0xba, 0x44, 0x94, 0xe0, 0xd6, 0xbf, 0x7e, 0x02, 0xf0, 0xac, 0x4c,
454        0xda, 0xbf, 0x21, 0x4d, 0xc7, 0xd1, 0xb0, 0x6b, 0xf0, 0xb2, 0x0a, 0x8d, 0x25, 0xbc, 0x2c,
455        0xda, 0xd7, 0xa9, 0x51, 0x32, 0xa2, 0xc0, 0x5e, 0x1c, 0x86, 0x95, 0x53, 0x40, 0x7d, 0xf1,
456        0xf5, 0x34, 0xf8, 0x9c, 0xf0, 0x9f, 0xa8, 0x4c, 0x48, 0x2d, 0x10, 0xeb, 0x1b, 0x00, 0x10,
457        0x48, 0xc6, 0x00, 0x00, 0x40, 0xcf, 0x45, 0xcc, 0xaa, 0xff, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
458        0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
459        0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xe6, 0xba, 0x13,
460        0x8b, 0x45, 0x3b, 0x24, 0x4e, 0x69, 0xbb, 0x96, 0xd7, 0xc9, 0xe9, 0x13, 0xad, 0x75, 0xeb,
461        0xeb, 0x8a, 0x0e, 0x9e, 0x76, 0xfb, 0x0d, 0x17, 0xfe, 0x5a, 0x07, 0x7e, 0x6d, 0x95, 0xa5,
462        0x30, 0x33, 0xe7, 0xf1, 0x3d, 0x29, 0xfa, 0x07, 0x84, 0x99, 0xea, 0xca, 0x06, 0xe3, 0x27,
463        0xa5, 0x40, 0x07, 0x12, 0xf3, 0x55, 0x72, 0xe5, 0xa1, 0x12, 0x35, 0xee, 0x06, 0xf2, 0x51,
464        0x0d, 0x11, 0x70, 0x92, 0xc1, 0xd3, 0x7c, 0x99, 0x33, 0x74, 0x49, 0x6e, 0x6d, 0x2a, 0xdc,
465        0xa2, 0x57, 0x35, 0xbe, 0xd3, 0x65, 0xbb, 0xf1, 0x14, 0x05, 0x09, 0xd4, 0x87, 0x3e, 0xbf,
466        0x4c, 0xc8, 0xd1, 0x30, 0x10, 0xdc, 0x1b, 0x05, 0x00, 0x58, 0xff, 0x05, 0x00, 0x58, 0xff,
467        0x55, 0xdd, 0x89, 0xce, 0x44, 0xa8, 0xc2, 0x9c, 0xec, 0x84, 0x2c, 0x8f, 0x6f, 0x30, 0x74,
468        0xae, 0x66, 0x2b, 0x16, 0x5b, 0xe6, 0xe0, 0x96, 0x69, 0x66, 0x8f, 0xe4, 0x5c, 0x57, 0x24,
469        0x06, 0x52, 0x67, 0xa1, 0xa0, 0x43, 0xaa, 0x5d, 0x43, 0x4d, 0x7c, 0xbf, 0x78, 0x16, 0x59,
470        0xf9, 0x30, 0x58, 0x03, 0x12, 0x73, 0xed, 0x8d, 0x00, 0xdd, 0x49, 0xe2, 0xf9, 0xa1, 0x88,
471        0x2c, 0x80, 0x90, 0x0f, 0xb3, 0x2b, 0xf5, 0xc9, 0x72, 0x61, 0x6a, 0x85, 0x99, 0xc3, 0x02,
472        0xd4, 0xd8, 0x2a, 0xee, 0x20, 0xa9, 0xcd, 0x9a, 0xa8, 0xed, 0x6b, 0x95, 0x98, 0x8c, 0x96,
473        0x6f, 0x1f, 0xda, 0x13, 0xf9, 0xc7, 0x75, 0xdd, 0x55, 0x17, 0x71, 0xd4, 0xbd, 0xc5, 0x79,
474        0xa0, 0x2d, 0xcb, 0x7b, 0x40, 0x76, 0x1b, 0xf4, 0x04, 0x56, 0xd2, 0x1b, 0x24, 0x44, 0x25,
475        0x68, 0x01, 0x33, 0xa1, 0x92, 0xf5, 0x1f, 0x69, 0xed, 0xd1, 0xa8, 0x28, 0x3c, 0x10, 0x2d,
476        0x1c, 0x05, 0x0c, 0x58, 0xff, 0x05, 0x0c, 0x53, 0xff, 0x55, 0xcc, 0x88, 0x9f, 0xba, 0xf2,
477        0xc5, 0x51, 0xfe, 0x43, 0xec, 0x47, 0x96, 0x64, 0x14, 0x78, 0xf3, 0x6b, 0x46, 0x52, 0x79,
478        0x15, 0x26, 0x05, 0x50, 0x9f, 0xdd, 0xec, 0x0b, 0x0d, 0x5a, 0x8f, 0xe1, 0x15, 0x76, 0x87,
479        0x1c, 0x6a, 0xf7, 0xe1, 0xe2, 0x46, 0xc4, 0xcc, 0x90, 0x95, 0x40, 0x67, 0xdb, 0x70, 0x53,
480        0xd4, 0x70, 0xb4, 0xcd, 0x80, 0x52, 0xb4, 0x0b, 0xc1, 0xd5, 0xda, 0x17, 0x15, 0x1e, 0x99,
481        0x57, 0x22, 0x9c, 0x58, 0xc3, 0xc4, 0x5e, 0xd2, 0x78, 0x37, 0x69, 0xe2, 0x21, 0xf7, 0x83,
482        0x5c, 0xa1, 0x6a, 0xbd, 0xbe, 0x72, 0xa4, 0x3d, 0x61, 0x76, 0xcb, 0x55, 0x2a, 0x01, 0x8d,
483        0x14, 0xcb, 0xdc, 0x4f, 0x6f, 0x15, 0x46, 0x6e, 0xe8, 0x5d, 0x6d, 0xf0, 0xea, 0x0d, 0xaa,
484        0x8f, 0xdd, 0xd7, 0x3e, 0x52, 0x20, 0x24, 0x1b, 0x15, 0x62, 0x98, 0x0a, 0xf4, 0x42, 0x9b,
485        0xdc, 0x8a, 0xb7, 0xab, 0xae, 0x57,
486    ];
487
488    /// The property that makes `ferrox quantize --type q4_k_s --pure`'s
489    /// output a file llama.cpp would have written, rather than one that
490    /// merely decodes to similar numbers.
491    ///
492    /// An encoder that is within Q4_K's error bound passes any
493    /// tolerance test and still writes a different file. Only this
494    /// catches that.
495    #[test]
496    fn q4_k_matches_llama_cpp_quantize_row_q4_k_ref() {
497        let x = sample_input();
498        let mut got = Vec::new();
499        encode_row_q4_k(&x, &mut got).unwrap();
500        assert_eq!(got.len(), LLAMA_CPP_Q4_K_GOLDEN.len());
501        for (b, (g, w)) in got
502            .as_chunks::<Q4_K_BLOCK_BYTES>()
503            .0
504            .iter()
505            .zip(LLAMA_CPP_Q4_K_GOLDEN.as_chunks::<Q4_K_BLOCK_BYTES>().0)
506            .enumerate()
507        {
508            assert_eq!(g, w, "super-block {b} disagrees with llama.cpp");
509        }
510    }
511
512    /// A row that is not a whole number of super-blocks is refused, not
513    /// padded. llama.cpp answers this case by changing the tensor's
514    /// TYPE (Q4_K -> Q5_0 -> F16); ferrox has neither encoder, and
515    /// padding would shift every following row on decode.
516    #[test]
517    fn a_row_that_is_not_a_whole_number_of_super_blocks_is_refused() {
518        let mut out = Vec::new();
519        assert!(encode_row_q4_k(&[0.5; Q4_K_BLOCK_ELEMS + 1], &mut out).is_none());
520        // 32 is a Q8_0 block and a Q4_K sub-block, and still not a
521        // Q4_K row: the block size that matters here is 256.
522        assert!(encode_row_q4_k(&[0.5; 32], &mut out).is_none());
523        assert!(encode_row_q4_k(&[], &mut out).is_some());
524    }
525
526    /// Round trip through this crate's own reader, against an exact
527    /// property rather than a tolerance: for every element, **no
528    /// representable level is strictly closer** than the one the
529    /// encoder chose.
530    ///
531    /// A tolerance would have to be invented, and an invented tolerance
532    /// is what this whole issue exists to avoid. This is a fact instead:
533    /// stage 3 rounds to the nearest of the 16 levels `d*sc*k -
534    /// dmin*m`, so a nibble packed into the wrong half-byte, a scale
535    /// unpacked from the wrong bits, or an off-by-one in the sub-block
536    /// stride all move some element off its nearest level and turn this
537    /// red. It says nothing about whether the *fit* is good -- that is
538    /// what the golden above is for, and this is the weak half.
539    ///
540    /// (A sub-block whose 6-bit scale rounded to zero has all 16 levels
541    /// equal, so it passes trivially. Sub-block 17 is that case, on
542    /// purpose.)
543    #[test]
544    fn every_element_lands_on_its_nearest_representable_level() {
545        let x = sample_input();
546        let mut bytes = Vec::new();
547        encode_row_q4_k(&x, &mut bytes).unwrap();
548        let back = dequant_q4_k(&bytes).unwrap();
549        assert_eq!(back.len(), x.len());
550
551        for (b, block) in bytes.as_chunks::<Q4_K_BLOCK_BYTES>().0.iter().enumerate() {
552            let d = f16::from_le_bytes([block[0], block[1]]).to_f32();
553            let dmin = f16::from_le_bytes([block[2], block[3]]).to_f32();
554            let packed: [u8; Q4_K_SCALE_BYTES] = block[4..16].try_into().unwrap();
555            for j in 0..SUB {
556                let (sc, m) = q4_k_scale_min(j, &packed);
557                let (dj, dm) = (d * sc as f32, dmin * m as f32);
558                for ii in 0..SUB_ELEMS {
559                    let idx = b * Q4_K_BLOCK_ELEMS + SUB_ELEMS * j + ii;
560                    let chosen = (x[idx] - back[idx]).abs();
561                    for k in 0..=15u8 {
562                        let level = dj * k as f32 - dm;
563                        assert!(
564                            (x[idx] - level).abs() >= chosen,
565                            "block {b} sub-block {j} element {ii}: {} is closer to {} than to the \
566                             chosen {}",
567                            x[idx],
568                            level,
569                            back[idx]
570                        );
571                    }
572                }
573            }
574        }
575    }
576}