Skip to main content

ferrox_quant/encode/
q4_k.rs

1//! The Q4_K weight encoder: a transcription of llama.cpp b7650's
2//! `quantize_row_q4_K_ref` (`ggml/src/ggml-quants.c:1280`), 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, all of which live in
7//! [`super::fit`] because Q5_K's fit is the same three stages with four
8//! numbers changed:
9//!
10//! 1. Each of the 8 sub-blocks of 32 gets an **iterative** affine fit
11//!    (`make_qkx2_quants`): 21 candidate inverse scales are tried, each
12//!    one re-solves a weighted least-squares for (scale, min) from the
13//!    integer codes it produced, and the lowest weighted squared error
14//!    wins. The weights are `sqrt(mean(x^2)) + |x|`, so a sub-block's
15//!    large values pull the fit toward themselves.
16//! 2. The 8 scales and 8 mins are themselves quantized to 6 bits
17//!    against the super-block's `d`/`dmin` and packed into 12 bytes.
18//! 3. The 4-bit codes are then recomputed **against the 6-bit-rounded**
19//!    scale and min, not against the fit from stage 1 -- so stage 3
20//!    sees a slightly different affine map than stage 1 did.
21//!
22//! A naive min/max encoder skips all three and produces a file that
23//! loads and generates measurably worse text. That is the failure this
24//! module exists to not ship, so the arithmetic in [`super::fit`] is
25//! deliberately the same shape as the C, down to the operation order in
26//! the least-squares accumulation.
27//!
28//! With an importance matrix the same three stages run with the
29//! weights, grid and stage 2 of `quantize_row_q4_K_impl`
30//! (`ggml-quants.c:1376`); that switch lives in [`super::fit`] too, and
31//! this module only passes the slice through.
32//!
33//! What is left HERE is only what is Q4_K's own: the candidate grid it
34//! passes to the shared fit, and the nibble packing.
35
36use super::fit::{fit_qk_super_block, make_qkx2_quants, QkFit, QK_SUB_ELEMS};
37use crate::{Q4_K_BLOCK_BYTES, Q4_K_BLOCK_ELEMS};
38
39/// Q4_K's half of the shared super-block fit: 4-bit codes, the
40/// `(-1.0, 0.1, 20)` candidate grid from `ggml-quants.c:1301`, and NO
41/// clamp on the imatrix scale codes (`:1411-1424` packs `Ls[j]` as is).
42const Q4_K_FIT: QkFit = QkFit {
43    nmax: 15,
44    rmin: -1.0,
45    rdelta: 0.1,
46    nstep: 20,
47    imatrix_clamps_scale_codes: false,
48};
49
50/// Runs one 32-element sub-block through exactly the path
51/// [`encode_block_q4_k`] uses and returns its `(scale, min)`.
52///
53/// Tooling, not a code path: it exists so a single sub-block can be
54/// compared against llama.cpp's own `make_qkx2_quants` on the same
55/// input. Chasing a floating-point difference that affects 0.55% of
56/// super-blocks by quantizing whole checkpoints is far too coarse a
57/// loop, and `examples/q4k_probe.rs` is the other half of it.
58#[doc(hidden)]
59pub fn probe_sub_block(xs: &[f32]) -> (f32, f32) {
60    assert_eq!(xs.len(), QK_SUB_ELEMS);
61    let mut l = [0u8; QK_SUB_ELEMS];
62    let mut laux = [0u8; QK_SUB_ELEMS];
63    let mut weights = [0f32; QK_SUB_ELEMS];
64    super::fit::qk_sub_block_weights(xs, &mut weights);
65    make_qkx2_quants(
66        xs,
67        &weights,
68        &mut l,
69        &mut laux,
70        Q4_K_FIT.nmax,
71        Q4_K_FIT.rmin,
72        Q4_K_FIT.rdelta,
73        Q4_K_FIT.nstep,
74        false,
75    )
76}
77
78/// Encodes one Q4_K super-block (exactly [`Q4_K_BLOCK_ELEMS`] values)
79/// and appends its [`Q4_K_BLOCK_BYTES`] bytes to `out`.
80///
81/// `qw` is this super-block's slice of the importance matrix
82/// (llama.cpp's `quant_weights + QK_K*i`), or `None` for the plain fit.
83pub fn encode_block_q4_k(
84    block: &[f32; Q4_K_BLOCK_ELEMS],
85    qw: Option<&[f32; Q4_K_BLOCK_ELEMS]>,
86    out: &mut Vec<u8>,
87) {
88    let fitted = fit_qk_super_block(block, Q4_K_FIT, qw);
89
90    out.reserve(Q4_K_BLOCK_BYTES);
91    out.extend_from_slice(&fitted.d.to_le_bytes());
92    out.extend_from_slice(&fitted.dmin.to_le_bytes());
93    out.extend_from_slice(&fitted.packed);
94    // Two 32-element halves of every 64 elements share a byte: the low
95    // nibble is the first half, the high nibble the second. The reader
96    // in `dequant_q4_k` walks the same pairing.
97    for j in (0..Q4_K_BLOCK_ELEMS).step_by(64) {
98        for i in 0..32 {
99            out.push(fitted.l[j + i] | (fitted.l[j + i + 32] << 4));
100        }
101    }
102}
103
104/// Encodes a whole row (or any slice whose length is a multiple of
105/// [`Q4_K_BLOCK_ELEMS`]) into Q4_K super-blocks, appending to `out`.
106///
107/// `qw` is the row's importance-matrix weights, one per column and the
108/// same for every row of the tensor, or `None` for the plain fit. Its
109/// length must equal `src.len()`; a mismatch is refused like a ragged
110/// row, because a shifted slice would weight every element by its
111/// neighbour's importance and still produce a plausible file.
112///
113/// Returns `None` when `src.len()` is not a multiple of the super-block
114/// size. llama.cpp handles that case by silently *changing type* --
115/// `convert_incompatible_tensor` rewrites a Q4_K tensor with an awkward
116/// row length to Q5_0, and to F16 if that does not fit either -- and
117/// ferrox has neither encoder, so this refuses instead of padding.
118/// Padding would write more elements than the tensor's shape declares
119/// and every following row would decode shifted.
120pub fn encode_row_q4_k(src: &[f32], qw: Option<&[f32]>, out: &mut Vec<u8>) -> Option<()> {
121    let (blocks, rest) = src.as_chunks::<Q4_K_BLOCK_ELEMS>();
122    if !rest.is_empty() {
123        return None;
124    }
125    let qw_blocks = super::imatrix_blocks::<Q4_K_BLOCK_ELEMS>(qw, blocks.len())?;
126    out.reserve(blocks.len() * Q4_K_BLOCK_BYTES);
127    for (i, block) in blocks.iter().enumerate() {
128        encode_block_q4_k(block, qw_blocks.map(|q| &q[i]), out);
129    }
130    Some(())
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use crate::encode::fit::QK_SUBS;
137    use crate::encode::testdata::k_quant_fixture;
138    use crate::{dequant_q4_k, q4_k_scale_min, Q4_K_SCALE_BYTES};
139    use half::f16;
140
141    /// llama.cpp's own bytes for [`k_quant_fixture`].
142    ///
143    /// Produced by the C harness described in the PR body: it links
144    /// llama.cpp b7650's `libggml-base` and calls the exported
145    /// `quantize_row_q4_K_ref` on the f32s
146    /// `encode::testdata::dump_the_fixture_the_c_harness_reads` writes.
147    /// The same harness also calls `ggml_quantize_chunk(GGML_TYPE_Q4_K,
148    /// ...)` --
149    /// the entry point `llama-quantize` itself goes through -- and
150    /// asserts the two agree, so this is what the real tool writes and
151    /// not merely what a reference function does.
152    const LLAMA_CPP_Q4_K_GOLDEN: [u8; 12 * Q4_K_BLOCK_BYTES] = [
153        0x32, 0x10, 0x14, 0x1c, 0x05, 0x0c, 0x59, 0xff, 0x04, 0x0b, 0x58, 0xff, 0x55, 0xcc, 0x8a,
154        0xb3, 0xed, 0xc8, 0xba, 0x4e, 0xeb, 0x91, 0x85, 0xa6, 0x9c, 0x87, 0xd8, 0xab, 0x42, 0xe9,
155        0x87, 0x0b, 0xb3, 0x82, 0x59, 0xb2, 0xc0, 0x80, 0x87, 0xa7, 0x98, 0x62, 0x75, 0x94, 0x31,
156        0x0a, 0x89, 0xda, 0xc5, 0x32, 0xd4, 0xfa, 0xf6, 0xd6, 0xc1, 0xbd, 0xf1, 0xc8, 0x6c, 0xbf,
157        0xc4, 0xa0, 0xeb, 0x46, 0x7d, 0xb0, 0xf4, 0xb7, 0x95, 0xbc, 0xd1, 0xe6, 0x84, 0x8d, 0x77,
158        0x1d, 0x01, 0xd7, 0x1f, 0xda, 0x1b, 0xf6, 0x4f, 0x62, 0x3f, 0xce, 0x28, 0x47, 0x5b, 0xba,
159        0xeb, 0xfc, 0x04, 0xb3, 0xba, 0x44, 0x94, 0xe0, 0xd6, 0xbf, 0x7e, 0x02, 0xf0, 0xac, 0x4c,
160        0xda, 0xbf, 0x21, 0x4d, 0xc7, 0xd1, 0xb0, 0x6b, 0xf0, 0xb2, 0x0a, 0x8d, 0x25, 0xbc, 0x2c,
161        0xda, 0xd7, 0xa9, 0x51, 0x32, 0xa2, 0xc0, 0x5e, 0x1c, 0x86, 0x95, 0x53, 0x40, 0x7d, 0xf1,
162        0xf5, 0x34, 0xf8, 0x9c, 0xf0, 0x9f, 0xa8, 0x4c, 0x48, 0x2d, 0x10, 0xeb, 0x1b, 0x00, 0x10,
163        0x48, 0xc6, 0x00, 0x00, 0x40, 0xcf, 0x45, 0xcc, 0xaa, 0xff, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
164        0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0,
165        0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xf0, 0xe6, 0xba, 0x13,
166        0x8b, 0x45, 0x3b, 0x24, 0x4e, 0x69, 0xbb, 0x96, 0xd7, 0xc9, 0xe9, 0x13, 0xad, 0x75, 0xeb,
167        0xeb, 0x8a, 0x0e, 0x9e, 0x76, 0xfb, 0x0d, 0x17, 0xfe, 0x5a, 0x07, 0x7e, 0x6d, 0x95, 0xa5,
168        0x30, 0x33, 0xe7, 0xf1, 0x3d, 0x29, 0xfa, 0x07, 0x84, 0x99, 0xea, 0xca, 0x06, 0xe3, 0x27,
169        0xa5, 0x40, 0x07, 0x12, 0xf3, 0x55, 0x72, 0xe5, 0xa1, 0x12, 0x35, 0xee, 0x06, 0xf2, 0x51,
170        0x0d, 0x11, 0x70, 0x92, 0xc1, 0xd3, 0x7c, 0x99, 0x33, 0x74, 0x49, 0x6e, 0x6d, 0x2a, 0xdc,
171        0xa2, 0x57, 0x35, 0xbe, 0xd3, 0x65, 0xbb, 0xf1, 0x14, 0x05, 0x09, 0xd4, 0x87, 0x3e, 0xbf,
172        0x4c, 0xc8, 0xd1, 0x30, 0x10, 0xdc, 0x1b, 0x05, 0x00, 0x58, 0xff, 0x05, 0x00, 0x58, 0xff,
173        0x55, 0xdd, 0x89, 0xce, 0x44, 0xa8, 0xc2, 0x9c, 0xec, 0x84, 0x2c, 0x8f, 0x6f, 0x30, 0x74,
174        0xae, 0x66, 0x2b, 0x16, 0x5b, 0xe6, 0xe0, 0x96, 0x69, 0x66, 0x8f, 0xe4, 0x5c, 0x57, 0x24,
175        0x06, 0x52, 0x67, 0xa1, 0xa0, 0x43, 0xaa, 0x5d, 0x43, 0x4d, 0x7c, 0xbf, 0x78, 0x16, 0x59,
176        0xf9, 0x30, 0x58, 0x03, 0x12, 0x73, 0xed, 0x8d, 0x00, 0xdd, 0x49, 0xe2, 0xf9, 0xa1, 0x88,
177        0x2c, 0x80, 0x90, 0x0f, 0xb3, 0x2b, 0xf5, 0xc9, 0x72, 0x61, 0x6a, 0x85, 0x99, 0xc3, 0x02,
178        0xd4, 0xd8, 0x2a, 0xee, 0x20, 0xa9, 0xcd, 0x9a, 0xa8, 0xed, 0x6b, 0x95, 0x98, 0x8c, 0x96,
179        0x6f, 0x1f, 0xda, 0x13, 0xf9, 0xc7, 0x75, 0xdd, 0x55, 0x17, 0x71, 0xd4, 0xbd, 0xc5, 0x79,
180        0xa0, 0x2d, 0xcb, 0x7b, 0x40, 0x76, 0x1b, 0xf4, 0x04, 0x56, 0xd2, 0x1b, 0x24, 0x44, 0x25,
181        0x68, 0x01, 0x33, 0xa1, 0x92, 0xf5, 0x1f, 0x69, 0xed, 0xd1, 0xa8, 0x28, 0x3c, 0x10, 0x2d,
182        0x1c, 0x05, 0x0c, 0x58, 0xff, 0x05, 0x0c, 0x53, 0xff, 0x55, 0xcc, 0x88, 0x9f, 0xba, 0xf2,
183        0xc5, 0x51, 0xfe, 0x43, 0xec, 0x47, 0x96, 0x64, 0x14, 0x78, 0xf3, 0x6b, 0x46, 0x52, 0x79,
184        0x15, 0x26, 0x05, 0x50, 0x9f, 0xdd, 0xec, 0x0b, 0x0d, 0x5a, 0x8f, 0xe1, 0x15, 0x76, 0x87,
185        0x1c, 0x6a, 0xf7, 0xe1, 0xe2, 0x46, 0xc4, 0xcc, 0x90, 0x95, 0x40, 0x67, 0xdb, 0x70, 0x53,
186        0xd4, 0x70, 0xb4, 0xcd, 0x80, 0x52, 0xb4, 0x0b, 0xc1, 0xd5, 0xda, 0x17, 0x15, 0x1e, 0x99,
187        0x57, 0x22, 0x9c, 0x58, 0xc3, 0xc4, 0x5e, 0xd2, 0x78, 0x37, 0x69, 0xe2, 0x21, 0xf7, 0x83,
188        0x5c, 0xa1, 0x6a, 0xbd, 0xbe, 0x72, 0xa4, 0x3d, 0x61, 0x76, 0xcb, 0x55, 0x2a, 0x01, 0x8d,
189        0x14, 0xcb, 0xdc, 0x4f, 0x6f, 0x15, 0x46, 0x6e, 0xe8, 0x5d, 0x6d, 0xf0, 0xea, 0x0d, 0xaa,
190        0x8f, 0xdd, 0xd7, 0x3e, 0x52, 0x20, 0x24, 0x1b, 0x15, 0x62, 0x98, 0x0a, 0xf4, 0x42, 0x9b,
191        0xdc, 0x8a, 0xb7, 0xab, 0xae, 0x57, 0xb2, 0x04, 0x0f, 0x11, 0xe9, 0xe8, 0xf8, 0xf4, 0xac,
192        0xa9, 0xb1, 0xd7, 0xc6, 0xc5, 0x5c, 0xff, 0x7c, 0xa8, 0xf7, 0x8b, 0x09, 0x68, 0x8b, 0xa0,
193        0x69, 0x6f, 0x9d, 0xb0, 0xbb, 0xe7, 0xc9, 0x87, 0x6e, 0xfe, 0x58, 0xca, 0x56, 0xad, 0xb9,
194        0xb4, 0xd4, 0x7b, 0x96, 0x0b, 0x19, 0x71, 0xea, 0xd2, 0x87, 0x37, 0x18, 0x9e, 0x47, 0x09,
195        0x0b, 0x37, 0x05, 0x22, 0x43, 0x2a, 0x6a, 0x7a, 0x07, 0x3d, 0x53, 0x0f, 0x28, 0x37, 0x57,
196        0x3b, 0x7b, 0x68, 0x7c, 0x55, 0x99, 0x0a, 0x6c, 0x26, 0x20, 0xf7, 0x81, 0x7c, 0x53, 0x43,
197        0xf5, 0xd4, 0x77, 0x37, 0x66, 0x89, 0xc7, 0x17, 0xbf, 0xad, 0x73, 0x48, 0xc2, 0x86, 0x39,
198        0x7e, 0x86, 0x63, 0x5d, 0x5c, 0xb0, 0x73, 0x04, 0x77, 0x98, 0x72, 0xd3, 0xcd, 0x97, 0x44,
199        0x35, 0xb6, 0xb5, 0x06, 0x75, 0xcc, 0xb2, 0x81, 0xa5, 0xe7, 0x67, 0x76, 0xf1, 0xbc, 0x4a,
200        0xa6, 0x77, 0x9b, 0x78, 0x95, 0xb5, 0x96, 0x53, 0x95, 0x9e, 0xd2, 0x61, 0x86, 0xa9, 0x90,
201        0xfe, 0x09, 0x83, 0x16, 0xb6, 0xb2, 0xa9, 0xff, 0xad, 0xa8, 0xaa, 0xbf, 0x3b, 0x7a, 0xcf,
202        0x04, 0xc8, 0xba, 0x14, 0x42, 0x87, 0x7a, 0xb5, 0x8a, 0x65, 0x84, 0x84, 0x04, 0x56, 0x96,
203        0xab, 0x24, 0xb2, 0x61, 0xc8, 0x85, 0x53, 0x84, 0x65, 0x80, 0x6e, 0x57, 0x73, 0x7a, 0x35,
204        0x5a, 0x0b, 0xf7, 0xbc, 0x88, 0xa8, 0xca, 0x82, 0x85, 0x30, 0xba, 0xaa, 0x98, 0x97, 0x88,
205        0x78, 0xfc, 0x7a, 0x96, 0xd8, 0x07, 0x9d, 0x98, 0x6c, 0x88, 0x78, 0x7a, 0xa2, 0x4b, 0x8b,
206        0xa8, 0xcf, 0xdd, 0x85, 0xb4, 0xc7, 0x93, 0xa8, 0x6e, 0x47, 0x26, 0x74, 0x96, 0x53, 0xba,
207        0x53, 0xba, 0x67, 0x79, 0xc5, 0x06, 0xf6, 0x9a, 0xe0, 0x4e, 0x74, 0x03, 0x74, 0x83, 0x82,
208        0xa8, 0x9a, 0x8a, 0xb6, 0xad, 0xbb, 0x74, 0x7f, 0x37, 0xc0, 0x49, 0x9b, 0x16, 0x68, 0x84,
209        0x89, 0x39, 0x98, 0x49, 0x7c, 0x25, 0x5a, 0x23, 0x99, 0x37, 0x45, 0x05, 0x03, 0x27, 0x35,
210        0x49, 0x32, 0xed, 0x65, 0x1b, 0x34, 0xcd, 0xa6, 0x89, 0x58, 0x09, 0x9c, 0x14, 0xb0, 0xe2,
211        0xbf, 0xee, 0xe8, 0xe5, 0xbf, 0xfa, 0xaf, 0x12, 0xff, 0xf2, 0x76, 0x65, 0x95, 0x54, 0x70,
212        0x99, 0xa5, 0x14, 0x00, 0xfc, 0x93, 0xa3, 0x24, 0xc7, 0x78, 0x27, 0x58, 0x73, 0x54, 0x53,
213        0x5c, 0xf6, 0x3a, 0xa4, 0x56, 0xaa, 0x64, 0x36, 0x83, 0x6d, 0x72, 0x7f, 0xe6, 0x57, 0x57,
214        0x67, 0xc2, 0x98, 0x54, 0x06, 0x86, 0x58, 0xbf, 0x7a, 0x99, 0xf4, 0xb7, 0xa8, 0xf6, 0x7b,
215        0xc9, 0xb8, 0x58, 0x97, 0x96, 0xc5, 0x30, 0xb8, 0xa6, 0x77, 0x89, 0x99, 0xd4, 0xa7, 0x46,
216        0x79, 0x8a, 0x89, 0x9c, 0x78, 0x09, 0x83, 0x99, 0x2c, 0x74, 0x75, 0x78, 0xb9, 0x89, 0x40,
217        0x88, 0xfa, 0x84, 0x6e, 0x7c, 0x93, 0xab, 0x26, 0x87, 0x4e, 0xa8, 0x5c, 0x6a, 0x97, 0xaa,
218        0x57, 0x92, 0x84, 0x96, 0xd0, 0x93, 0x6c, 0x07, 0x86, 0x87, 0xa5, 0x8b, 0xc9, 0xd9, 0xa9,
219        0xa4, 0x98, 0x81, 0xc6, 0x94, 0xeb, 0xd4, 0x9a, 0x74, 0xaa, 0x4a, 0x85, 0x5f, 0x33, 0x7c,
220        0x86, 0x5a, 0xd6, 0xb7, 0x08, 0x9e, 0x15, 0xf5, 0xfa, 0xf6, 0xff, 0xa1, 0x9c, 0xa1, 0xbf,
221        0xc8, 0x69, 0x65, 0xcf, 0x13, 0x08, 0x63, 0x85, 0x10, 0x75, 0x37, 0x21, 0x5b, 0xb1, 0x27,
222        0x07, 0xe7, 0x18, 0x17, 0x12, 0x13, 0x8b, 0x51, 0x64, 0x2f, 0x35, 0x67, 0x73, 0x37, 0x62,
223        0x25, 0x96, 0xbe, 0x4b, 0x93, 0x44, 0xb5, 0x81, 0x98, 0x95, 0x96, 0xa5, 0x43, 0xba, 0x6a,
224        0x06, 0x9d, 0xb9, 0x9a, 0xc8, 0xc4, 0xf3, 0x82, 0xa4, 0xdc, 0xc0, 0xb6, 0xd7, 0xb4, 0xa5,
225        0x90, 0xaf, 0xbb, 0x7a, 0xad, 0x75, 0xc2, 0x97, 0x69, 0x23, 0x72, 0xaa, 0x73, 0x25, 0x9e,
226        0x4c, 0xa8, 0xe8, 0x74, 0x65, 0x6d, 0xf2, 0x84, 0xa9, 0xa0, 0x4c, 0xc8, 0x87, 0xb5, 0x4a,
227        0xa4, 0x5c, 0x8d, 0x85, 0xc3, 0xd2, 0x33, 0xa3, 0x87, 0x0b, 0x0b, 0x75, 0x4f, 0x97, 0x77,
228        0x84, 0xa8, 0xb9, 0xc8, 0x8a, 0x97, 0x66, 0x65, 0x9b, 0x68, 0xf4, 0x6c, 0x86, 0x49, 0x80,
229        0x66, 0x35, 0x75, 0x75, 0xb2, 0x74, 0x26, 0x88, 0x5b, 0xa3, 0x65, 0xa9, 0x80, 0x09, 0x7e,
230        0x16, 0xfb, 0xf5, 0xf9, 0xb5, 0xa6, 0xb2, 0xf4, 0x6c, 0x60, 0x2f, 0xfb, 0xed, 0x56, 0x2a,
231        0xf7, 0x93, 0xc5, 0xb5, 0x85, 0xc5, 0xb5, 0x83, 0xa4, 0x79, 0xb2, 0x74, 0x96, 0x98, 0x79,
232        0x96, 0x38, 0xb2, 0xc8, 0xb7, 0x66, 0x85, 0x46, 0x8f, 0x44, 0x95, 0x6d, 0xb8, 0xa7, 0x00,
233        0x7b, 0x40, 0x7b, 0x58, 0x8f, 0xa9, 0xc6, 0x9b, 0x6d, 0x6e, 0x55, 0x8c, 0x49, 0xd9, 0xe8,
234        0x61, 0x29, 0x8b, 0x88, 0x76, 0x95, 0x1a, 0xb9, 0x45, 0x6f, 0x0f, 0x89, 0xf6, 0x86, 0x5d,
235        0x7a, 0xaa, 0x8d, 0x48, 0xa1, 0xa7, 0x29, 0x47, 0x97, 0x74, 0x57, 0x69, 0x3b, 0x98, 0x51,
236        0x78, 0x79, 0x40, 0x68, 0x42, 0x5d, 0x09, 0x8c, 0x5f, 0x78, 0x94, 0xf6, 0x89, 0x98, 0x62,
237        0x46, 0x5a, 0x68, 0x34, 0x98, 0x0b, 0x4a, 0x4b, 0x9c, 0x28, 0x7e, 0x00, 0x66, 0xad, 0xca,
238        0x69, 0x4d, 0x7c, 0x8a, 0xc6, 0x7e, 0x8e, 0x27, 0x3d, 0x2b, 0x6f, 0x0a, 0x7c, 0x79, 0xa6,
239        0xaa, 0xf7, 0x6e, 0x06, 0xbb, 0x49, 0xaf, 0x08, 0x4f, 0x14, 0xfe, 0xb6, 0xef, 0xfb, 0xff,
240        0xee, 0xed, 0xed, 0xef, 0x9e, 0xb9, 0xee, 0xb8, 0x17, 0x4b, 0xbf, 0x84, 0x35, 0x98, 0x98,
241        0x59, 0x18, 0xe0, 0x64, 0x45, 0xc6, 0x04, 0x58, 0xa6, 0xac, 0x44, 0x67, 0x9a, 0x59, 0x98,
242        0xc5, 0x12, 0x44, 0xf8, 0x20, 0x63, 0x66, 0x18, 0x59, 0xe2, 0x23, 0x32, 0xb9, 0x49, 0x28,
243        0x6d, 0x86, 0x4a, 0xdb, 0xbb, 0xa7, 0x75, 0x32, 0x05, 0x7a, 0x18, 0x3c, 0x4f, 0x16, 0x05,
244        0xd0, 0x85, 0x84, 0x58, 0x21, 0x96, 0xa7, 0x62, 0x8b, 0xa9, 0x73, 0x1a, 0xf8, 0xd7, 0x83,
245        0x57, 0x57, 0x53, 0x6f, 0x80, 0x56, 0xcb, 0xaa, 0x96, 0x73, 0x79, 0xf6, 0xa5, 0xb8, 0x47,
246        0x78, 0x14, 0xad, 0xa7, 0xb3, 0x08, 0xd5, 0xb3, 0xd6, 0xf4, 0x48, 0xf3, 0x7a, 0x49, 0xa5,
247        0x94, 0x35, 0xeb, 0x98, 0x5d, 0x0f, 0x82, 0x66, 0x88, 0x77, 0x46, 0xac, 0x2c, 0x67, 0xc7,
248        0x72, 0x98, 0xf7, 0x78, 0x50, 0x75, 0x7d, 0x32, 0x92, 0xa5, 0xcd, 0x9a, 0x59, 0x60, 0xf9,
249        0xd9, 0x0a, 0x01, 0x18, 0xb0, 0xf4, 0xfd, 0xf5, 0x6b, 0x68, 0xb3, 0xdf, 0x9d, 0x7c, 0x7f,
250        0xf9, 0x94, 0xd8, 0xa7, 0xf0, 0x7e, 0x9f, 0x9a, 0x99, 0x7e, 0x35, 0x85, 0x6c, 0x69, 0x5a,
251        0x74, 0x0d, 0x98, 0x76, 0x64, 0x99, 0x38, 0x8c, 0x5d, 0x74, 0x6a, 0xb4, 0x8e, 0x77, 0x8c,
252        0x63, 0x7a, 0x8d, 0x07, 0xb7, 0x77, 0x88, 0x4c, 0x6a, 0xf8, 0x35, 0x9b, 0x53, 0x83, 0x23,
253        0x28, 0xc4, 0x48, 0x5a, 0x84, 0x45, 0x4c, 0x17, 0xc2, 0x38, 0xa7, 0xc5, 0x90, 0x4f, 0x7c,
254        0x47, 0x86, 0x89, 0x79, 0x58, 0x3a, 0x41, 0x92, 0x7b, 0x41, 0x30, 0x25, 0x69, 0x89, 0x22,
255        0x37, 0x05, 0x25, 0x64, 0xf5, 0x38, 0x42, 0x32, 0x22, 0x54, 0x5a, 0x0f, 0x52, 0x52, 0x94,
256        0xa4, 0x21, 0x20, 0x79, 0x2c, 0x17, 0x46, 0xa2, 0x96, 0xa5, 0xc5, 0xb6, 0x97, 0xa6, 0xc5,
257        0xb5, 0xa0, 0x87, 0x93, 0xb3, 0xb2, 0xb9, 0x87, 0xd6, 0xa4, 0x0b, 0xb7, 0x6a, 0xc6, 0x3b,
258        0xf7, 0xc2, 0xc2, 0x85, 0xc5, 0xb7, 0xa6, 0x7b, 0xef, 0x45, 0x0b, 0xc5, 0x16, 0xf5, 0xf0,
259        0xaf, 0xef, 0xef, 0xfd, 0xa4, 0xf2, 0x87, 0x43, 0x8b, 0xff, 0xc7, 0x1b, 0x83, 0xc9, 0x19,
260        0x36, 0x72, 0x75, 0xcf, 0x90, 0xcc, 0x88, 0xf7, 0x54, 0x52, 0x2a, 0x0e, 0x85, 0x8a, 0x17,
261        0xaa, 0xe1, 0xa6, 0xd6, 0xe4, 0xe4, 0x73, 0xd3, 0xf7, 0xfd, 0xa6, 0x7d, 0xb7, 0x34, 0x87,
262        0x68, 0xd7, 0x52, 0xc2, 0xb3, 0xb3, 0x39, 0xa4, 0xc5, 0xcc, 0x80, 0x21, 0xd5, 0x5c, 0x64,
263        0x83, 0x99, 0x66, 0x76, 0x85, 0x6a, 0xea, 0x8e, 0xb3, 0xb2, 0x9f, 0x29, 0x52, 0x07, 0x47,
264        0x46, 0x69, 0x77, 0x89, 0x8a, 0x48, 0xa4, 0x8c, 0x82, 0xa7, 0x6a, 0x78, 0x66, 0xaf, 0x99,
265        0x61, 0x77, 0xb4, 0xb8, 0x26, 0x65, 0xba, 0x7a, 0xed, 0x59, 0x09, 0xab, 0x80, 0xf6, 0x16,
266        0x2b, 0xa6, 0x62, 0x9b, 0x53, 0x17, 0x5c, 0xb4, 0x8a, 0xd7, 0x87, 0xd0, 0x4d, 0x69, 0xac,
267        0x8b, 0x39, 0x0d, 0x97, 0x54, 0x84, 0x8b, 0xb9, 0xa7, 0xff, 0x75, 0x60, 0x8a, 0x7d, 0xb7,
268        0xa8, 0x98, 0x57,
269    ];
270
271    /// The property that makes `ferrox quantize --type q4_k_s --pure`'s
272    /// output a file llama.cpp would have written, rather than one that
273    /// merely decodes to similar numbers.
274    ///
275    /// An encoder that is within Q4_K's error bound passes any
276    /// tolerance test and still writes a different file. Only this
277    /// catches that.
278    #[test]
279    fn q4_k_matches_llama_cpp_quantize_row_q4_k_ref() {
280        let x = k_quant_fixture();
281        let mut got = Vec::new();
282        encode_row_q4_k(&x, None, &mut got).unwrap();
283        assert_eq!(got.len(), LLAMA_CPP_Q4_K_GOLDEN.len());
284        for (b, (g, w)) in got
285            .as_chunks::<Q4_K_BLOCK_BYTES>()
286            .0
287            .iter()
288            .zip(LLAMA_CPP_Q4_K_GOLDEN.as_chunks::<Q4_K_BLOCK_BYTES>().0)
289            .enumerate()
290        {
291            assert_eq!(g, w, "super-block {b} disagrees with llama.cpp");
292        }
293    }
294
295    /// A row that is not a whole number of super-blocks is refused, not
296    /// padded. llama.cpp answers this case by changing the tensor's
297    /// TYPE (Q4_K -> Q5_0 -> F16); ferrox has neither encoder, and
298    /// padding would shift every following row on decode.
299    #[test]
300    fn a_row_that_is_not_a_whole_number_of_super_blocks_is_refused() {
301        let mut out = Vec::new();
302        assert!(encode_row_q4_k(&[0.5; Q4_K_BLOCK_ELEMS + 1], None, &mut out).is_none());
303        // 32 is a Q8_0 block and a Q4_K sub-block, and still not a
304        // Q4_K row: the block size that matters here is 256.
305        assert!(encode_row_q4_k(&[0.5; 32], None, &mut out).is_none());
306        assert!(encode_row_q4_k(&[], None, &mut out).is_some());
307        // An importance slice that does not cover the row is refused
308        // the same way: a shifted weight is a wrong file, not an error
309        // the reader can see.
310        assert!(encode_row_q4_k(&[0.5; Q4_K_BLOCK_ELEMS], Some(&[1.0; 32]), &mut out).is_none());
311    }
312
313    /// Round trip through this crate's own reader, against an exact
314    /// property rather than a tolerance: for every element, **no
315    /// representable level is strictly closer** than the one the
316    /// encoder chose.
317    ///
318    /// A tolerance would have to be invented, and an invented tolerance
319    /// is what this whole issue exists to avoid. This is a fact instead:
320    /// stage 3 rounds to the nearest of the 16 levels `d*sc*k -
321    /// dmin*m`, so a nibble packed into the wrong half-byte, a scale
322    /// unpacked from the wrong bits, or an off-by-one in the sub-block
323    /// stride all move some element off its nearest level and turn this
324    /// red. It says nothing about whether the *fit* is good -- that is
325    /// what the golden above is for, and this is the weak half.
326    ///
327    /// (A sub-block whose 6-bit scale rounded to zero has all 16 levels
328    /// equal, so it passes trivially. Sub-block 17 is that case, on
329    /// purpose.)
330    #[test]
331    fn every_element_lands_on_its_nearest_representable_level() {
332        let x = k_quant_fixture();
333        let mut bytes = Vec::new();
334        encode_row_q4_k(&x, None, &mut bytes).unwrap();
335        let back = dequant_q4_k(&bytes).unwrap();
336        assert_eq!(back.len(), x.len());
337
338        for (b, block) in bytes.as_chunks::<Q4_K_BLOCK_BYTES>().0.iter().enumerate() {
339            let d = f16::from_le_bytes([block[0], block[1]]).to_f32();
340            let dmin = f16::from_le_bytes([block[2], block[3]]).to_f32();
341            let packed: [u8; Q4_K_SCALE_BYTES] = block[4..16].try_into().unwrap();
342            for j in 0..QK_SUBS {
343                let (sc, m) = q4_k_scale_min(j, &packed);
344                let (dj, dm) = (d * sc as f32, dmin * m as f32);
345                for ii in 0..QK_SUB_ELEMS {
346                    let idx = b * Q4_K_BLOCK_ELEMS + QK_SUB_ELEMS * j + ii;
347                    let chosen = (x[idx] - back[idx]).abs();
348                    for k in 0..=15u8 {
349                        let level = dj * k as f32 - dm;
350                        assert!(
351                            (x[idx] - level).abs() >= chosen,
352                            "block {b} sub-block {j} element {ii}: {} is closer to {} than to the \
353                             chosen {}",
354                            x[idx],
355                            level,
356                            back[idx]
357                        );
358                    }
359                }
360            }
361        }
362    }
363}