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}