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