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