Skip to main content

ferrox_quant/encode/
fit.rs

1//! The per-sub-block fitting helpers every K-quant encoder is built
2//! from, transcribed from llama.cpp b7650's `ggml/src/ggml-quants.c`:
3//!
4//! * [`nearest_int`] (`ggml-quants.c:444`), the rounding every encoder
5//!   shares;
6//! * [`make_qkx2_quants`] (`ggml-quants.c:622`), the affine
7//!   `scale * L - min` fit Q4_K and Q5_K use per 32 weights;
8//! * [`make_qx_quants`] (`ggml-quants.c:451`), the symmetric `scale * L`
9//!   fit Q6_K uses per 16 weights;
10//! * [`fit_qk_super_block`], the three-stage Q4_K/Q5_K super-block flow
11//!   (`quantize_row_q4_K_ref` at `ggml-quants.c:1280`,
12//!   `quantize_row_q5_K_ref` at `ggml-quants.c:1467`) that differs
13//!   between the two formats by exactly four numbers -- and, with an
14//!   importance matrix, the flow of `quantize_row_q4_K_impl` (`:1376`)
15//!   and `quantize_row_q5_K_impl` (`:1581`), which is the SAME three
16//!   stages with a different weight rule, a different candidate grid
17//!   and a different stage 2 (`make_qp_quants`, in
18//!   [`super::qp_quants`]).
19//!
20//! One module, because the alternative is what this repo keeps paying
21//! for: a copy of `make_qkx2_quants` in each of `q4_k.rs` and `q5_k.rs`
22//! that agree today and drift the first time one of them is corrected.
23//! Q5_K is the same super-block fit as Q4_K with `nmax = 31` and a
24//! different candidate grid, so it is a CALL into the same code, not a
25//! second transcription with the constants changed.
26//!
27//! The imatrix path is the same rule applied once more. Upstream's
28//! `make_qkx3_quants` (`ggml-quants.c:816`) is `make_qkx2_quants` with
29//! a `weights ? weights[i] : x[i]*x[i]` fallback that no caller in the
30//! file exercises (every call passes a weight array) and `max <= min`
31//! for `max == min`, which cannot differ once `min` has been clamped to
32//! at most zero. So there is ONE [`make_qkx2_quants`] here, and
33//! [`fit_qk_super_block`] takes the per-super-block importance slice as
34//! an `Option` and switches the three things that actually differ.
35//!
36//! Deviation from upstream, shown not to change a byte by the goldens
37//! in `q4_k`, `q5_k` and `q6_k`: `nearest_int`'s
38//! `assert(fabsf(fval) <= 4194303.f)` is not reproduced. It is compiled
39//! out of the release `libggml` that `llama-quantize` actually links,
40//! so asserting here would make ferrox stop where llama.cpp proceeds --
41//! a refusal that fires on input llama.cpp handles is not coverage, it
42//! is a different tool.
43//!
44//! # Every `mul_add` here is load-bearing. Do not "simplify" one.
45//!
46//! `sumlx += w*x[i]*l` in the C is **one fused multiply-add**, not a
47//! multiply followed by an add: the compiler that builds `libggml`
48//! contracts it, so the intermediate product is never rounded to f32.
49//! Rust does not contract, so every such site is spelled `mul_add`
50//! explicitly. Writing `sumlx += w * x[i] * l as f32` instead is one
51//! rounding more, and that rounding is not cosmetic: these fits choose
52//! between candidate scales with `sumlx*sumlx > best*suml2`, a
53//! comparison that is a near-tie often enough that ONE ulp flips which
54//! candidate wins and rewrites the whole super-block.
55//!
56//! Measured on an F16 Llama-3.2-1B, against the installed
57//! `llama-quantize` b7650: with these `mul_add`s, ferrox writes
58//! byte-identical files -- 0 of 3244032 Q4_K super-blocks differ, 0 of
59//! 3244032 Q5_K, 0 of 4827136 Q6_K. Remove them and it is 1.15%, 0.15%
60//! and 1.39% respectively. That is the entire difference between "a
61//! file llama.cpp would have written" and "a file that decodes to
62//! similar numbers".
63//!
64//! Nine of the thirteen sites have a real-weight super-block in
65//! `testdata::REAL_WEIGHT_BLOCKS` that turns a golden red when that one
66//! `mul_add` is removed; the fixture doc names the four that do not and
67//! why. Synthetic noise pins NONE of them, which is how they were
68//! nearly shipped wrong.
69
70use half::f16;
71
72use super::qp_quants::make_qp_quants;
73use crate::Q4_K_SCALE_BYTES;
74
75/// Elements per Q4_K/Q5_K sub-block, and sub-blocks per super-block.
76pub(crate) const QK_SUB_ELEMS: usize = 32;
77pub(crate) const QK_SUBS: usize = 8;
78
79/// `GROUP_MAX_EPS` (`ggml-quants.c:16`): below this, a group is "all
80/// zero" to [`make_qx_quants`].
81pub(crate) const GROUP_MAX_EPS: f32 = 1e-15;
82
83/// ggml's `nearest_int`: add 1.5 * 2^23 so the mantissa's low bits hold
84/// the rounded integer, then read them back out.
85///
86/// This is **round-half-to-even**, because it is the FPU's own rounding
87/// mode that does the work. `f32::round` is round-half-away-from-zero
88/// and disagrees on every exact tie -- and ties are not rare here: the
89/// candidate inverse scales in [`make_qkx2_quants`] walk a 0.1-wide
90/// grid, so `iscale * (x - min)` lands on `.5` constantly.
91#[inline]
92pub(crate) fn nearest_int(fval: f32) -> i32 {
93    let val = fval + 12_582_912.0f32;
94    let i = val.to_bits() as i32;
95    (i & 0x007f_ffff) - 0x0040_0000
96}
97
98/// llama.cpp's `make_qkx2_quants`: fit `x[i] ~= scale * L[i] - the_min`
99/// with `L[i]` in `0..=nmax`, minimising the `weights`-weighted error.
100///
101/// Returns `(scale, the_min)` and fills `l`. `laux` is scratch, passed
102/// in rather than allocated because the C does the same and this runs
103/// once per 32 weights of the checkpoint.
104///
105/// The signature is upstream's, `use_mad` and all: Q2_K passes `true`
106/// with `n = 16`, Q4_K passes `nmax = 15`, Q5_K passes `nmax = 31`.
107/// Keeping the parameters means the next K-quant is a call, not a copy
108/// of this function with two constants changed -- which is how this
109/// repo has lost a model feature eight times.
110#[allow(clippy::too_many_arguments)]
111pub(crate) fn make_qkx2_quants(
112    x: &[f32],
113    weights: &[f32],
114    l: &mut [u8],
115    laux: &mut [u8],
116    nmax: i32,
117    rmin: f32,
118    rdelta: f32,
119    nstep: i32,
120    use_mad: bool,
121) -> (f32, f32) {
122    let n = x.len();
123    debug_assert_eq!(weights.len(), n);
124    debug_assert_eq!(l.len(), n);
125    debug_assert!(laux.len() >= n);
126
127    // Deliberately not `min.min(x[i])` / `max.max(x[i])`. They differ
128    // from the C comparisons only when `x[0]` is NaN -- Rust's
129    // `f32::min` returns the non-NaN operand, `x[i] < NaN` is false and
130    // keeps the NaN -- so no fixture can tell them apart on a real
131    // checkpoint. The C's shape is kept anyway, because a checkpoint
132    // with a NaN weight should produce llama.cpp's bytes rather than
133    // politely different ones. Same choice, same reason, as the `amax`
134    // fold in the Q8_0 encoder next door.
135    let mut min = x[0];
136    let mut max = x[0];
137    let mut sum_w = weights[0];
138    let mut sum_x = sum_w * x[0];
139    for i in 1..n {
140        if x[i] < min {
141            min = x[i];
142        }
143        if x[i] > max {
144            max = x[i];
145        }
146        let w = weights[i];
147        sum_w += w;
148        sum_x = w.mul_add(x[i], sum_x);
149    }
150    if min > 0.0 {
151        min = 0.0;
152    }
153    if max == min {
154        l[..n].fill(0);
155        return (0.0, -min);
156    }
157
158    let mut iscale = nmax as f32 / (max - min);
159    let mut scale = 1.0 / iscale;
160    let mut best_error = 0.0f32;
161    for i in 0..n {
162        let li = nearest_int(iscale * (x[i] - min)).clamp(0, nmax);
163        l[i] = li as u8;
164        let diff = scale.mul_add(l[i] as f32, min) - x[i];
165        let diff = if use_mad { diff.abs() } else { diff * diff };
166        best_error = weights[i].mul_add(diff, best_error);
167    }
168    if nstep < 1 {
169        return (scale, -min);
170    }
171
172    for is in 0..=nstep {
173        iscale = (rmin + rdelta * is as f32 + nmax as f32) / (max - min);
174        let (mut sum_l, mut sum_l2, mut sum_xl) = (0.0f32, 0.0f32, 0.0f32);
175        for i in 0..n {
176            let li = nearest_int(iscale * (x[i] - min)).clamp(0, nmax);
177            laux[i] = li as u8;
178            let w = weights[i];
179            sum_l = w.mul_add(li as f32, sum_l);
180            sum_l2 = (w * li as f32).mul_add(li as f32, sum_l2);
181            sum_xl = (w * li as f32).mul_add(x[i], sum_xl);
182        }
183        let det = sum_w.mul_add(sum_l2, -(sum_l * sum_l));
184        if det > 0.0 {
185            let mut this_scale = sum_w.mul_add(sum_xl, -(sum_x * sum_l)) / det;
186            let mut this_min = sum_l2.mul_add(sum_x, -(sum_l * sum_xl)) / det;
187            if this_min > 0.0 {
188                this_min = 0.0;
189                this_scale = sum_xl / sum_l2;
190            }
191            let mut cur_error = 0.0f32;
192            for i in 0..n {
193                let diff = this_scale.mul_add(laux[i] as f32, this_min) - x[i];
194                let diff = if use_mad { diff.abs() } else { diff * diff };
195                cur_error = weights[i].mul_add(diff, cur_error);
196            }
197            if cur_error < best_error {
198                l[..n].copy_from_slice(&laux[..n]);
199                best_error = cur_error;
200                scale = this_scale;
201                min = this_min;
202            }
203        }
204    }
205    (scale, -min)
206}
207
208/// The importance weight [`make_qx_quants`] gives element `i`.
209///
210/// Spelled ONCE and called from all three loops that need it, because
211/// upstream spells the same ternary chain out three times and two of
212/// the three are inside the candidate search. Three copies of one
213/// weight rule is the shape this repo names first, and here it is
214/// upstream's own.
215#[inline]
216fn qx_weight(x: &[f32], qw: Option<&[f32]>, rmse_type: i32, i: usize) -> f32 {
217    match qw {
218        Some(qw) => qw[i],
219        None => match rmse_type {
220            1 => x[i] * x[i],
221            2 => 1.0,
222            3 => x[i].abs(),
223            _ => x[i].abs().sqrt(),
224        },
225    }
226}
227
228/// llama.cpp's `make_qx_quants` (`ggml-quants.c:451`): fit
229/// `x[i] ~= scale * (L[i] - nmax)` with `L[i]` in `0..2*nmax`, that is a
230/// SYMMETRIC fit with no min, which is what Q6_K's 16-element
231/// sub-blocks use (`nmax = 32`, so the codes are `-32..=31` plus 32).
232///
233/// The signature is upstream's. `rmse_type` selects the error weight
234/// (`1` is `x^2`, which Q6_K uses; `0` skips the search entirely, which
235/// Q3_K uses; a negative value returns early with a blended scale) and
236/// `qw` is the importance-matrix weight, `None` for the plain encoders.
237/// Q6_K only ever calls this one way, and the other arms are kept
238/// because Q3_K and the imatrix variants reach the same function with
239/// different arguments -- a second copy with the arms removed is how
240/// two encoders come to disagree about one fit. Only the `rmse_type=1,
241/// qw=None` path is covered by a golden, and saying so is better than
242/// implying otherwise.
243pub(crate) fn make_qx_quants(
244    x: &[f32],
245    l: &mut [i8],
246    nmax: i32,
247    rmse_type: i32,
248    qw: Option<&[f32]>,
249) -> f32 {
250    let n = x.len();
251    debug_assert_eq!(l.len(), n);
252    let mut max = 0f32;
253    let mut amax = 0f32;
254    for &v in x {
255        let ax = v.abs();
256        if ax > amax {
257            amax = ax;
258            max = v;
259        }
260    }
261    if amax < GROUP_MAX_EPS {
262        l[..n].fill(0);
263        return 0.0;
264    }
265    let mut iscale = -(nmax as f32) / max;
266    if rmse_type == 0 {
267        for i in 0..n {
268            let li = nearest_int(iscale * x[i]);
269            l[i] = (nmax + li.clamp(-nmax, nmax - 1)) as i8;
270        }
271        return 1.0 / iscale;
272    }
273    // Upstream flips the sign of `rmse_type` in place and then keeps
274    // using it to pick the weight, so the weight for a negative
275    // `rmse_type` is the POSITIVE one's. Shadowing reproduces that
276    // without a second variable that could be read in the wrong order.
277    let (rmse_type, return_early) = if rmse_type < 0 {
278        (-rmse_type, true)
279    } else {
280        (rmse_type, false)
281    };
282    let mut sumlx = 0f32;
283    let mut suml2 = 0f32;
284    for i in 0..n {
285        let li = nearest_int(iscale * x[i]).clamp(-nmax, nmax - 1);
286        l[i] = (li + nmax) as i8;
287        let w = qx_weight(x, qw, rmse_type, i);
288        sumlx = (w * x[i]).mul_add(li as f32, sumlx);
289        suml2 = (w * li as f32).mul_add(li as f32, suml2);
290    }
291    let mut scale = if suml2 != 0.0 { sumlx / suml2 } else { 0.0 };
292    if return_early {
293        return if suml2 > 0.0 {
294            0.5 * (scale + 1.0 / iscale)
295        } else {
296            1.0 / iscale
297        };
298    }
299    let mut best = scale * sumlx;
300    for is in -9..=9i32 {
301        if is == 0 {
302            continue;
303        }
304        iscale = -(nmax as f32 + 0.1 * is as f32) / max;
305        sumlx = 0.0;
306        suml2 = 0.0;
307        for i in 0..n {
308            let li = nearest_int(iscale * x[i]).clamp(-nmax, nmax - 1);
309            let w = qx_weight(x, qw, rmse_type, i);
310            sumlx = (w * x[i]).mul_add(li as f32, sumlx);
311            suml2 = (w * li as f32).mul_add(li as f32, suml2);
312        }
313        if suml2 > 0.0 && sumlx * sumlx > best * suml2 {
314            for i in 0..n {
315                let li = nearest_int(iscale * x[i]);
316                l[i] = (nmax + li.clamp(-nmax, nmax - 1)) as i8;
317            }
318            scale = sumlx / suml2;
319            best = scale * sumlx;
320        }
321    }
322    scale
323}
324
325/// The `sqrt(mean(x^2)) + |x|` weights Q4_K and Q5_K hand to
326/// [`make_qkx2_quants`] for one 32-element sub-block.
327pub(crate) fn qk_sub_block_weights(xs: &[f32], weights: &mut [f32; QK_SUB_ELEMS]) {
328    let mut sum_x2 = 0f32;
329    for &v in xs {
330        sum_x2 += v * v;
331    }
332    let av_x = (sum_x2 / QK_SUB_ELEMS as f32).sqrt();
333    for (w, &v) in weights.iter_mut().zip(xs) {
334        *w = av_x + v.abs();
335    }
336}
337
338/// The fitted super-block a Q4_K or Q5_K encoder packs: the two f16
339/// super-scales, the 12 bytes of 6-bit sub-block scales and mins, and
340/// the per-element codes.
341pub(crate) struct QkSuperBlock {
342    pub d: f16,
343    pub dmin: f16,
344    pub packed: [u8; Q4_K_SCALE_BYTES],
345    pub l: [u8; QK_SUBS * QK_SUB_ELEMS],
346}
347
348/// The candidate grid and code range that distinguish one
349/// `make_qkx2_quants`-based super-block format from another. Q4_K and
350/// Q5_K differ by these numbers and NOTHING else, which is why they
351/// share [`fit_qk_super_block`] instead of having a transcription
352/// each.
353#[derive(Clone, Copy)]
354pub(crate) struct QkFit {
355    /// Largest code: 15 for Q4_K, 31 for Q5_K.
356    pub nmax: i32,
357    /// `rmin`, `rdelta`, `nstep` for `make_qkx2_quants` on the PLAIN
358    /// path: `(-1.0, 0.1, 20)` for Q4_K, `(-0.5, 0.1, 15)` for Q5_K.
359    /// With an importance matrix both formats use [`IMATRIX_GRID`].
360    pub rmin: f32,
361    pub rdelta: f32,
362    pub nstep: i32,
363    /// Whether the imatrix path clamps the `make_qp_quants` codes to 63
364    /// before packing them. `quantize_row_q5_K_impl`
365    /// (`ggml-quants.c:1619-1622`) does, `quantize_row_q4_K_impl`
366    /// (`:1411-1424`) does NOT, and the difference is a byte in the
367    /// file whenever a sub-block's fitted scale is negative: the code
368    /// wraps to `>= 200` in `make_qp_quants`, and Q4_K packs the wrapped
369    /// byte while Q5_K packs 63.
370    ///
371    /// UNMEASURED, and saying so is better than implying otherwise: the
372    /// plain path sees a negative sub-block scale in ~0.55% of a real
373    /// Qwen3-0.6B tensor's super-blocks, but with a real imatrix the
374    /// same checkpoint produced ZERO wrapped codes across 2,098,688
375    /// Q4_K and 2,098,688 Q5_K super-blocks (counted with a probe on
376    /// this branch), and forcing the clamp on for Q4_K left every byte
377    /// identical. The field is kept because it is what the two C
378    /// functions say, and a shared "obviously harmless" clamp would be
379    /// a silent divergence from `quantize_row_q4_K_impl` the day a
380    /// checkpoint does reach it.
381    pub imatrix_clamps_scale_codes: bool,
382}
383
384/// The candidate grid `quantize_row_q4_K_impl` and
385/// `quantize_row_q5_K_impl` both hand to the sub-block fit
386/// (`ggml-quants.c:1406` and `:1612`): `(-0.9, 0.05, 36)`, the same for
387/// both formats, unlike the plain grids in [`QkFit`].
388const IMATRIX_GRID: (f32, f32, i32) = (-0.9, 0.05, 36);
389
390/// The three-stage super-block fit `quantize_row_q4_K_ref`
391/// (`ggml-quants.c:1280`) and `quantize_row_q5_K_ref`
392/// (`ggml-quants.c:1467`) share, parameterised by [`QkFit`], and the
393/// three-stage fit their `_impl` twins (`:1376`, `:1581`) share when
394/// `qw` carries the importance-matrix slice for this super-block.
395///
396/// 1. Each of the 8 sub-blocks of 32 gets an **iterative** affine fit:
397///    the candidate inverse scales are tried, each one re-solves a
398///    weighted least-squares for (scale, min) from the integer codes it
399///    produced, and the lowest weighted squared error wins. Plain, the
400///    element weights are `sqrt(mean(x^2)) + |x|` over the sub-block;
401///    with an imatrix they are `qw * sqrt(sigma2 + x^2)` where `sigma2`
402///    is `2 * mean(x^2)` over the whole SUPER-block.
403/// 2. The 8 scales and 8 mins are themselves quantized to 6 bits
404///    against the super-block's `d`/`dmin` and packed into 12 bytes.
405///    Plain, that is `63/max`; with an imatrix it is `make_qp_quants`,
406///    weighted by each sub-block's summed element weights.
407/// 3. The codes are then recomputed **against the 6-bit-rounded** scale
408///    and min, not against the fit from stage 1. This stage is the same
409///    on both paths.
410///
411/// `l` is deliberately carried from stage 1 into stage 3. Stage 3 skips
412/// any sub-block whose reconstructed `d` rounded to zero (`if (!d)
413/// continue;` upstream), and the codes then written are the ones stage
414/// 1 left behind -- NOT zeros. Clearing `l` per sub-block reads as
415/// tidier and writes a different file.
416pub(crate) fn fit_qk_super_block(
417    block: &[f32; QK_SUBS * QK_SUB_ELEMS],
418    fit: QkFit,
419    qw: Option<&[f32; QK_SUBS * QK_SUB_ELEMS]>,
420) -> QkSuperBlock {
421    let mut l = [0u8; QK_SUBS * QK_SUB_ELEMS];
422    let mut laux = [0u8; QK_SUB_ELEMS];
423    let mut weights = [0f32; QK_SUB_ELEMS];
424    let mut mins = [0f32; QK_SUBS];
425    let mut scales = [0f32; QK_SUBS];
426    // Per-sub-block summed weights, `sw[j]` upstream: only the imatrix
427    // stage 2 reads them.
428    let mut sw = [0f32; QK_SUBS];
429
430    // `sigma2` for the imatrix weight rule: `2*sum_x2/QK_K` over the
431    // whole super-block, with the accumulation contracted as the C's
432    // `sum_x2 += x[l] * x[l]` is.
433    let sigma2 = qw.map(|_| {
434        let mut sum_x2 = 0f32;
435        for &v in block.iter() {
436            sum_x2 = v.mul_add(v, sum_x2);
437        }
438        2.0 * sum_x2 / (QK_SUBS * QK_SUB_ELEMS) as f32
439    });
440    let (rmin, rdelta, nstep) = match qw {
441        Some(_) => IMATRIX_GRID,
442        None => (fit.rmin, fit.rdelta, fit.nstep),
443    };
444
445    let mut max_scale = 0f32; // deducting the min keeps scales positive
446    let mut max_min = 0f32;
447    for j in 0..QK_SUBS {
448        let lo = QK_SUB_ELEMS * j;
449        let xs = &block[lo..lo + QK_SUB_ELEMS];
450        match (qw, sigma2) {
451            (Some(qw), Some(sigma2)) => {
452                // `weights[l] = qw[l] * sqrtf(sigma2 + x*x)`, the
453                // `sigma2 + x*x` contracted.
454                for (w, (&v, &q)) in weights.iter_mut().zip(xs.iter().zip(&qw[lo..])) {
455                    *w = q * v.mul_add(v, sigma2).sqrt();
456                }
457                let mut sumw = 0f32;
458                for &w in &weights {
459                    sumw += w;
460                }
461                sw[j] = sumw;
462            }
463            _ => qk_sub_block_weights(xs, &mut weights),
464        }
465        let (scale, min) = make_qkx2_quants(
466            xs,
467            &weights,
468            &mut l[lo..lo + QK_SUB_ELEMS],
469            &mut laux,
470            fit.nmax,
471            rmin,
472            rdelta,
473            nstep,
474            false,
475        );
476        scales[j] = scale;
477        mins[j] = min;
478        if scale > max_scale {
479            max_scale = scale;
480        }
481        if min > max_min {
482            max_min = min;
483        }
484    }
485
486    let mut packed = [0u8; Q4_K_SCALE_BYTES];
487    let (d, dmin) = match qw {
488        None => {
489            let inv_scale = if max_scale > 0.0 {
490                63.0 / max_scale
491            } else {
492                0.0
493            };
494            let inv_min = if max_min > 0.0 { 63.0 / max_min } else { 0.0 };
495            for j in 0..QK_SUBS {
496                // Upstream's `MIN(63, ls)`. It cannot fire on THIS path:
497                // `inv_scale` is `63/max_scale` and `max_scale` is the
498                // largest of `scales`, so the product is at most 63 plus
499                // an ulp and rounds to 63. It is kept because it is what
500                // the C says -- but no fixture here can turn its removal
501                // red, and saying so is better than implying the goldens
502                // cover it. (The imatrix arm below is where the same
503                // clamp is NOT automatic, and there it is a `QkFit`
504                // field because the two formats disagree about it.)
505                // The cast comes BEFORE the clamp, because upstream's
506                // does:
507                //
508                //     uint8_t ls = nearest_int(inv_scale*scales[j]);
509                //     ls = MIN(63, ls);
510                //
511                // `nearest_int` returns `int`, and storing it in a
512                // `uint8_t` truncates to eight bits FIRST. Clamping to 63
513                // and casting afterwards is the same for every value in
514                // `0..=255` and different for a negative one: C wraps -1
515                // to 255 and then clamps to 63, this order clamps -1 to
516                // -1 and casts to 255.
517                //
518                // A negative reaches here when a sub-block's
519                // least-squares fit returns a negative scale while some
520                // other sub-block's is positive, so `inv_scale` is
521                // positive and the product is not. Upstream's comment
522                // says scales are always positive "as we are deducting
523                // the min", which is the assumption this arithmetic
524                // quietly does not rely on. Rare, and it was 0.55% of
525                // the super-blocks in a real Qwen3-0.6B tensor.
526                let ls = (nearest_int(inv_scale * scales[j]) as u8).min(63);
527                let lm = (nearest_int(inv_min * mins[j]) as u8).min(63);
528                pack_scale_min(&mut packed, j, ls, lm);
529            }
530            (
531                f16::from_f32(max_scale / 63.0),
532                f16::from_f32(max_min / 63.0),
533            )
534        }
535        Some(_) => {
536            let mut ls = [0u8; QK_SUBS];
537            let mut lm = [0u8; QK_SUBS];
538            let d_block = make_qp_quants(&scales, &mut ls, 63, &sw);
539            let m_block = make_qp_quants(&mins, &mut lm, 63, &sw);
540            for j in 0..QK_SUBS {
541                let (mut s, mut m) = (ls[j], lm[j]);
542                if fit.imatrix_clamps_scale_codes {
543                    s = s.min(63);
544                    m = m.min(63);
545                }
546                pack_scale_min(&mut packed, j, s, m);
547            }
548            (f16::from_f32(d_block), f16::from_f32(m_block))
549        }
550    };
551
552    // Stage 3 unpacks the 6-bit scale/min with the same
553    // `q4_k_scale_min` the READER uses, rather than a second copy of
554    // `get_scale_min_k4`. One function means the encoder and the
555    // decoder cannot disagree about what was packed. (Q5_K shares
556    // Q4_K's packing, which is why one unpacker serves both.)
557    for j in 0..QK_SUBS {
558        let (sc, m) = crate::q4_k_scale_min(j, &packed);
559        let dj = d.to_f32() * sc as f32;
560        if dj == 0.0 {
561            continue;
562        }
563        let dm = dmin.to_f32() * m as f32;
564        for ii in 0..QK_SUB_ELEMS {
565            let idx = QK_SUB_ELEMS * j + ii;
566            l[idx] = nearest_int((block[idx] + dm) / dj).clamp(0, fit.nmax) as u8;
567        }
568    }
569
570    QkSuperBlock { d, dmin, packed, l }
571}
572
573/// Packs sub-block `j`'s 6-bit scale and min codes into the 12-byte
574/// layout `get_scale_min_k4` reads: the first four of each in their own
575/// bytes, the last four split across the low nibbles of bytes 8..12
576/// and the top two bits of bytes 0..8.
577///
578/// One function for both stage-2 arms of [`fit_qk_super_block`],
579/// because the packing is the ONE thing the plain and imatrix paths
580/// share verbatim at that stage, and a second copy of a shift-and-mask
581/// is exactly the kind of thing that is corrected in one place.
582#[inline]
583fn pack_scale_min(packed: &mut [u8; Q4_K_SCALE_BYTES], j: usize, ls: u8, lm: u8) {
584    if j < 4 {
585        packed[j] = ls;
586        packed[j + 4] = lm;
587    } else {
588        packed[j + 4] = (ls & 0xF) | ((lm & 0xF) << 4);
589        packed[j - 4] |= (ls >> 4) << 6;
590        packed[j] |= (lm >> 4) << 6;
591    }
592}
593
594#[cfg(test)]
595mod tests {
596    use super::*;
597
598    /// `nearest_int` is round-half-to-even, not `f32::round`. The two
599    /// agree everywhere except exact ties, and ties are where the
600    /// candidate-grid search lands constantly.
601    #[test]
602    fn nearest_int_rounds_ties_to_even_like_the_fpu() {
603        assert_eq!(nearest_int(0.5), 0);
604        assert_eq!(nearest_int(1.5), 2);
605        assert_eq!(nearest_int(2.5), 2);
606        assert_eq!(nearest_int(-0.5), 0);
607        assert_eq!(nearest_int(-1.5), -2);
608        assert_eq!(nearest_int(3.7), 4);
609        assert_eq!(nearest_int(-3.7), -4);
610        // And where they agree, they agree.
611        assert_eq!(nearest_int(3.2), 3.2f32.round() as i32);
612    }
613
614    /// A group under `GROUP_MAX_EPS` is all-zero to `make_qx_quants`:
615    /// codes 0 (NOT `nmax`, which is the code a zero value gets
616    /// everywhere else) and a scale of exactly 0. Q6_K's super-block
617    /// reads that scale to decide whether to write an all-zero block,
618    /// so the two conventions meeting here is load-bearing.
619    #[test]
620    fn make_qx_quants_reports_an_all_zero_group_with_zero_codes_and_zero_scale() {
621        let mut l = [7i8; 16];
622        let scale = make_qx_quants(&[0.0; 16], &mut l, 32, 1, None);
623        assert_eq!(scale, 0.0);
624        assert_eq!(l, [0i8; 16]);
625    }
626
627    /// The symmetric fit puts the largest-magnitude element at the
628    /// NEGATIVE end of the code range: `iscale = -nmax/max`, so the
629    /// element equal to `max` maps to `-nmax`, and a positive `max`
630    /// yields a negative scale. Getting the sign convention backwards
631    /// dequantizes to the negated tensor, which no error bound catches
632    /// on a symmetric distribution.
633    #[test]
634    fn make_qx_quants_maps_the_extreme_element_to_the_negative_end() {
635        let x: [f32; 16] = [
636            1.0, -0.5, 0.25, 0.0, 0.125, -0.75, 0.5, -0.25, 0.0625, -0.0625, 0.3, -0.3, 0.9, -0.9,
637            0.7, -0.1,
638        ];
639        let mut l = [0i8; 16];
640        let scale = make_qx_quants(&x, &mut l, 32, 1, None);
641        assert!(scale < 0.0, "scale {scale}");
642        // x[0] = 1.0 is the extreme: code -32, stored as -32 + 32 = 0.
643        assert_eq!(l[0], 0);
644        // And its mirror image lands on the far side of 32.
645        assert!(l[13] > 32, "l[13] = {}", l[13]);
646    }
647
648    /// The `x^2` weight (`rmse_type = 1`, the one Q6_K uses) is not the
649    /// uniform weight. A group with one large element and fifteen tiny
650    /// ones fits the large element under `rmse_type = 1` and splits the
651    /// difference under `rmse_type = 2`, so an encoder that passed the
652    /// wrong `rmse_type` would still produce plausible output.
653    #[test]
654    fn the_rmse_type_actually_selects_a_different_weight() {
655        let mut x = [0.001f32; 16];
656        x[0] = 1.0;
657        x[7] = -0.4;
658        let mut l1 = [0i8; 16];
659        let mut l2 = [0i8; 16];
660        let s1 = make_qx_quants(&x, &mut l1, 32, 1, None);
661        let s2 = make_qx_quants(&x, &mut l2, 32, 2, None);
662        assert_ne!(
663            s1, s2,
664            "rmse_type 1 and 2 produced the same scale; this input no \
665             longer distinguishes the weights"
666        );
667    }
668
669    /// The two Q4_K/Q5_K grids are not interchangeable. If they were,
670    /// `QkFit` would be decoration and one encoder could quietly use
671    /// the other's constants.
672    #[test]
673    fn the_q4_k_and_q5_k_candidate_grids_fit_the_same_data_differently() {
674        let mut block = [0f32; QK_SUBS * QK_SUB_ELEMS];
675        let mut state: u32 = 0x2f6b_1c05;
676        for v in block.iter_mut() {
677            state ^= state << 13;
678            state ^= state >> 17;
679            state ^= state << 5;
680            *v = f16::from_f32(((state >> 8) as f32 / 8_388_608.0 - 1.0) * 0.07).to_f32();
681        }
682        let q4 = fit_qk_super_block(
683            &block,
684            QkFit {
685                nmax: 15,
686                rmin: -1.0,
687                rdelta: 0.1,
688                nstep: 20,
689                imatrix_clamps_scale_codes: false,
690            },
691            None,
692        );
693        let q5 = fit_qk_super_block(
694            &block,
695            QkFit {
696                nmax: 31,
697                rmin: -0.5,
698                rdelta: 0.1,
699                nstep: 15,
700                imatrix_clamps_scale_codes: true,
701            },
702            None,
703        );
704        assert_ne!(q4.d, q5.d);
705        assert_ne!(q4.packed, q5.packed);
706        // Q5_K's codes use the top half of the range, Q4_K's cannot.
707        assert!(q5.l.iter().any(|&c| c > 15));
708        assert!(q4.l.iter().all(|&c| c <= 15));
709    }
710}