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}