Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Chapter 6 — The kquant family on the reference lane

status: polished · path: Muse Glimmer, pinned Muser tree

Prerequisites: Chapter 5. You have hand-packed an 8-element 4-bit block, dequantized it with every multiply shown, and measured the error. This chapter runs the same procedure on the real bytes Muser ships — no more schematic numbers. Two Metal kernels get quoted; the rest is byte layout and arithmetic.

Chapter 5 ended on a promise: the concrete formats. Here is the first one. The kquant family — llama.cpp’s “K-quant” block formats — is what fills the 16,756,681,056-byte reference artifact that Muser’s kquant lane decodes from, the lane measured at 35.440 tok/s [claims #11] and used as the program’s reference lock.

Everything that follows answers one question in different disguises: when a weight has been squeezed down to a handful of bits inside a packed block, what exactly must the machine read, and in what order, to get a usable number back out? So we read the byte layouts straight from Muser’s own dequantizers, dequantize real-format elements by hand, map which tensor class carries which member of the family, and then watch the dispatch code choose a kernel for each batch shape. The receipts are all here; they simply arrive after the idea they support rather than in the middle of it.


6.1 One model, several dtypes — decided by the GGUF

Before any bytes, a question of authority: who decides that this tensor is Q4_K and that one is Q6_K? Not the engine, and not an operator flag at startup. The file decides, and it decides tensor by tensor. The stake is higher than it sounds — guess wrong here and you do not get a slightly worse model, you get an engine that refuses to start, because every admission check below is fail-closed.

A GGUF file carries a dtype per tensor, not per model. Muser’s parser enumerates the types it is willing to meet:

#![allow(unused)]
fn main() {
// crates/muser-engine/src/gguf/types.rs:9
pub enum GgmlType {
    F32 = 0,
    F16 = 1,
    Q4_0 = 2,
    Q4_1 = 3,
    Q5_0 = 6,
    Q5_1 = 7,
    Q8_0 = 8,
    // …
    Q4_K = 12,
    Q5_K = 13,
    Q6_K = 14,
    // …
    /// Muser-native NVFP4 E2M1 payload, two logical values per byte.
    /// Per-16 E4M3FN scales and the per-tensor f32 scale2 live in bound
    /// companion tensors; this keeps the serving representation at exactly
    /// 4.5 bits/weight without relying on llama.cpp's experimental format.
    NVFP4_E2M1 = 1000,
    /// Raw E4M3FN bytes used only by NVFP4 companion scale tensors.
    F8_E4M3FN = 1001,
}
}

Every type knows its geometry — bytes per block and elements per block — and those two numbers define the format’s bitrate [crates/muser-engine/src/gguf/types.rs:75-127]:

 format   block bytes   elements   bits/element   min+offset?   codebook
 ───────  ───────────   ────────   ────────────   ───────────   ────────
 Q4_0        18            32         4.50         no (sym)      4-bit int
 Q8_0        34            32         8.50         no (sym)      8-bit int
 Q4_K       144           256         4.50         yes           4-bit int
 Q5_K       176           256         5.50         yes           5-bit int
 Q6_K       210           256         6.5625       no (signed)   6-bit int
 F16          2             1        16.0          exact         float

Figure 6.1: The kquant family as registered in GgmlType::block_size / block_elements. Bits/element = block_bytes × 8 ÷ elements — derive each one yourself: Q4_K is 144×8/256 = 4.5; Q6_K is 210×8/256 = 6.5625.

Two paths read these types, and they are not equally permissive. The CPU reference path will dequant anything on the list — quant/dispatch.rs simply fans out per dtype. The live Metal decode path is deliberately narrower. A projection tensor must be one of Q4_K | Q5_K | Q6_K | NVFP4_E2M1 | F16, and the embedding table is narrower still: Q4_K or F16, nothing else. Anything outside those sets is not quietly converted on the way in; it is refused. The two fences are drawn at [crates/muser-engine/src/decode.rs:136-139] for projections and [crates/muser-engine/src/decode.rs:1209] for the embedding.

The lane itself is chosen the same fail-closed way at load. The GGUF must declare muser.weight_precision, and a kquant artifact becomes the default only when the file contains no native NVFP4 tensors at all [crates/muser-engine/src/loader.rs:72-91]. The artifact picks the lane — not a flag, not a heuristic, not a fallback that guesses. Chapter 7 covers the nvfp4 pairing; this chapter stays on q4_k_xl.

6.2 Q4_K: the 144-byte super-block

Q4_K is the format most of this artifact is written in, which makes it the one worth knowing to the byte. It answers the question the previous chapter left open: how do you give every small group of weights its own local scale without the scales themselves eating the savings you went to all this trouble for? Watch where each header byte goes, and the answer is arithmetic rather than magic.

The unit of Q4_K storage is a super-block: 256 weights in 144 bytes, which is the 4.5 bits/weight of Figure 6.1. As in Chapter 5, we read the layout off the dequantizer — this is Muser’s complete, real function:

#![allow(unused)]
fn main() {
// crates/muser-engine/src/quant/k_block.rs:12
pub fn dequant_q4_k(block: &[u8], out: &mut [f32]) {
    debug_assert!(block.len() >= 144);
    debug_assert!(out.len() >= 256);

    let d = f16_to_f32(u16::from_le_bytes([block[0], block[1]]));
    let dmin = f16_to_f32(u16::from_le_bytes([block[2], block[3]]));
    let scales = &block[4..16];
    let qs = &block[16..144];

    // get_scale_min_k4: extract 6-bit sc and m for sub-block j (0..7)
    let get_scale_min = |j: usize| -> (f32, f32) {
        let (sc, m) = if j < 4 {
            (scales[j] & 0x3F, scales[j + 4] & 0x3F)
        } else {
            let sc = (scales[j + 4] & 0x0F) | ((scales[j - 4] >> 6) << 4);
            let m = (scales[j + 4] >> 4) | ((scales[j] >> 6) << 4);
            (sc, m)
        };
        (d * sc as f32, dmin * m as f32)
    };

    // 4 outer groups of 64 elements, 2 sub-blocks per group, 32 qs bytes per group.
    let mut q_off = 0usize;
    let mut is = 0usize;
    let mut base = 0usize;
    while base < 256 {
        let (d1, m1) = get_scale_min(is);
        let (d2, m2) = get_scale_min(is + 1);
        for l in 0..32 {
            let q = qs[q_off + l];
            out[base + l] = d1 * (q & 0x0F) as f32 - m1;
            out[base + l + 32] = d2 * (q >> 4) as f32 - m2;
        }
        q_off += 32;
        is += 2;
        base += 64;
    }
}
}

The byte map (Figure 6.2), in the ASCII style Chapter 5 promised:

 Q4_K super-block — 144 bytes for 256 elements

 offset   size   field      meaning
 ─────────────────────────────────────────────────────────────────
  0x00     2     d          f16  super-scale        (shared by 8 sub-scales)
  0x02     2     dmin       f16  super-min-scale    (shared by 8 sub-mins)
  0x04    12     scales     8× 6-bit sc + 8× 6-bit m, packed (Fig 6.3)
  0x10   128     qs         256 nibbles (2 per byte) — the weights
  0x90     —    (end)      2+2+12+128 = 144 bytes
 ─────────────────────────────────────────────────────────────────
  0x10 = 16,  0x90 = 144

Figure 6.2: The Q4_K super-block. Four regions: two f16 headers, a 12-byte packed scale/min strip, 128 bytes of nibbles. Compare the 8-byte toy block of Chapter 5 — same idea, one more level of hierarchy.

The structure is Chapter 5’s min+offset scheme with the two-level header that keeps it at 4.5 bits: the 256 elements split into eight sub-blocks of 32, each with its own 6-bit scale sc (0–63) and 6-bit min m (0–63), all multiplied by the shared f16 d and dmin. The value of element i in sub-block j is:

y = d × sc_j × nibble  −  dmin × m_j

Exactly scale × index + min from Chapter 5 — as d·sc_j (effective scale) and −dmin·m_j (effective min) with a sign flip folded in. The header budget: 4 bytes of f16s + 12 bytes of packed 6-bit fields = 16 bytes per 256 weights = 0.5 bits/weight of overhead. Chapter 5’s dial table charged 0.125 bits for a naive one-f16-pair header per 256; Q4_K spends 0.5 — and buys eight independent local scales/mins instead of one.

The 96 bits of scale/min strip are packed with zero padding. Sub-blocks 0–3 live wholly in bytes 0–7; sub-blocks 4–7 split their values between the low/high halves of bytes 8–11 and the top two bits of bytes 0–7:

 byte          b7 b6 │ b5 b4 b3 b2 b1 b0      contents
 ──────────────────────────────────────────────────────────────────────
 scales[0..4]   sc4+ │ sc0 (6 bits)            top2 of scales[j] = sc_{j+4} hi
 scales[4..8]   m4+  │ m0  (6 bits)            top2 of scales[4+j] = m_{j+4} hi
 scales[8..12]  m_lo │ sc_lo                   lo4|hi4 reassembles sc/m of 4..7

Figure 6.3: The 6-bit packing, compressed. The get_scale_min closure above is the authoritative spec: sub-blocks ≥4 reassemble sc from the low nibble of scales[j+4] plus the top 2 bits of scales[j-4], and m from the high nibble plus the top 2 bits of scales[j].

Note also the interleaving in the dequant loop: within each 64-element group, the low nibble of byte qs[q_off+l] feeds element base+l (even sub-block) and the high nibble feeds element base+l+32 (odd sub-block). Said the other way round, because this is the part that trips people up: a byte of qs does not belong to any single sub-block. Its low half is a weight in one sub-block and its high half is a weight in the neighbouring one, and the two halves are reconstructed with different effective scales and different mins. One byte, two sub-blocks, two different scales — the property Chapter 5’s toy deliberately did not have.

6.3 A worked dequant of real-format bytes

Reading a layout table is not the same as believing it. The only way to be sure you have the packing right is to build a block out of nothing, run the extraction the shipping code runs, and watch the values come back. That is the exercise here, and it is worth doing with a pencil rather than skimming.

Hand-build one super-block (values schematic in magnitude — real weights dequant near ±0.05; the mechanics are identical):

Headers. Pick d = 0.03125 = 2⁻⁵ and dmin = 0.0078125 = 2⁻⁷, both exact in f16. f16 bits: exponent = power+15, mantissa 0 → d = 0x2800, dmin = 0x2000; little-endian bytes 00 28 and 00 20.

Sub-block scales. Choose (all 0–63):

 sc = [20, 12, 44,  8, 33, 25, 17, 9]      m  = [ 4, 16,  8, 24, 12, 20, 28, 36]

Pack per Figure 6.3 — each line is the formula with the arithmetic shown:

 scales[0]  = sc0 | ((sc4 >> 4) << 6) = 20 | 128 = 148 = 0x94
 scales[1]  = sc1 | ((sc5 >> 4) << 6) = 12 |  64 =  76 = 0x4C
 scales[2]  = sc2 | ((sc6 >> 4) << 6) = 44 |  64 = 108 = 0x6C
 scales[3]  = sc3 | ((sc7 >> 4) << 6) =  8 |   0 =   8 = 0x08
 scales[4]  = m0  | ((m4  >> 4) << 6) =  4 |   0 =   4 = 0x04
 scales[5]  = m1  | ((m5  >> 4) << 6) = 16 |  64 =  80 = 0x50
 scales[6]  = m2  | ((m6  >> 4) << 6) =  8 |  64 =  72 = 0x48
 scales[7]  = m3  | ((m7  >> 4) << 6) = 24 | 128 = 152 = 0x98
 scales[8]  = (sc4 & 0x0F) | ((m4 & 0x0F) << 4) = 1 | 192 = 193 = 0xC1
 scales[9]  = (sc5 & 0x0F) | ((m5 & 0x0F) << 4) = 9 |  64 =  73 = 0x49
 scales[10] = (sc6 & 0x0F) | ((m6 & 0x0F) << 4) = 1 | 192 = 193 = 0xC1
 scales[11] = (sc7 & 0x0F) | ((m7 & 0x0F) << 4) = 9 |  64 =  73 = 0x49

Round-trip check for sub-block 5, substituting into get_scale_min exactly as the code does:

 sc5 = (scales[9] & 0x0F) | ((scales[1] >> 6) << 4) = 9 | (1 << 4) = 25  ✓
 m5  = (scales[9] >> 4)    | ((scales[5] >> 6) << 4) = 4 | (1 << 4) = 20  ✓

Nibbles. Set qs[0] = 0x5E (offset 16) and qs[32] = 0x3C (offset 48); all other qs bytes zero.

Dequantize three elements. Precompute the effective scale/min per sub-block (d·sc, dmin·m):

sub-blockscmeff. scale = d·sceff. min = dmin·m
02040.03125×20 = 0.6250.0078125×4 = 0.03125
112160.03125×12 = 0.3750.0078125×16 = 0.125
24480.03125×44 = 1.3750.0078125×8 = 0.0625
Element 0   (sub-block 0, low nibble of qs[0]):
 q = 0x5E → nibble = q & 0x0F = 14
 y0  = 0.625 × 14 − 0.03125 = 8.75 − 0.03125 = 8.71875

Element 32  (sub-block 1, high nibble of the SAME byte):
 q = 0x5E → nibble = q >> 4 = 5
 y32 = 0.375 × 5 − 0.125     = 1.875 − 0.125  = 1.75

Element 64  (sub-block 2, low nibble of qs[32]):
 q = 0x3C → nibble = q & 0x0F = 12
 y64 = 1.375 × 12 − 0.0625   = 16.5 − 0.0625  = 16.4375

One byte, two sub-blocks, two different effective scales — Chapter 5’s “zoom into the local band,” now twice per byte. You have dequantized Q4_K by hand against the shipping code.

6.4 Q5_K and Q6_K: the siblings

Q4_K has two siblings on this artifact, and each one changes exactly one thing. Both are answering the same question — where do you put the bits that did not fit in a nibble? — and they answer it differently: Q5_K adds a plane, Q6_K changes the codebook. Keep that distinction in hand; it is what makes their layouts feel different when they are really not.

Q5_K is Q4_K plus one extra bit per element, stored in a separate plane. Its 176 bytes (Figure 6.4): the same d/dmin/12-byte scale strip, then 32 bytes qh holding one high bit per element (256 bits), then the same 128 bytes of low nibbles [crates/muser-engine/src/quant/k_block.rs:51-60]:

 Q5_K — 176 bytes / 256 elements      y = d·sc·(nibble | qh_bit<<4) − dmin·m
 ────────────────────────────────────────────────────────────────────────
  0x00   2   d          0x02   2   dmin       0x04  12   scales
  0x10  32   qh         0x30 128   qs         0xB0   —  (end = 176)

Figure 6.4: Q5_K layout (offsets hex: 0x30 = 48, 0xB0 = 176). The 5-bit codebook lifts the index range from 0–15 to 0–31; 176×8/256 = 5.5 bits/element ([crates/muser-engine/src/quant/k_block.rs:61-105] for the dequant).

Q6_K changes shape more: a signed 6-bit codebook split across two planes, with the per-sub-block scales as plain int8 and the single f16 super-scale at the end of the block (Figure 6.5):

 Q6_K — 210 bytes / 256 elements      y = d · sc_j · q ,  q = 6-bit signed
 ────────────────────────────────────────────────────────────────────────
  0x00 128   ql   low 4 bits of each code, packed 2/byte
  0x80  64   qh   high 2 bits of four codes per byte
  0xC0  16   sc   16 × int8 sub-block scales (sub-blocks of 16 elements)
  0xD0   2   d    f16 super-scale (LAST field — bytes 208..210)

Figure 6.5: Q6_K layout (0x80 = 128, 0xC0 = 192, 0xD0 = 208). The Muser kernel reads these offsets verbatim: ql = &block[0..128]; qh = &block[128..192]; sc = &block[192..208]; d = f16(block[208..210]) [crates/muser-engine/src/quant/k_block/q6.rs:21-26].

Each of the 16 sub-blocks (16 elements each) reconstructs its codes as ((ql_nibble) | (qh_2bits << 4)) − 32 — an unsigned 0–63 payload shifted to signed −32…+31 — then scales once: 210×8/256 = 6.5625 bits/element. No min term: a signed symmetric grid, but with 64 levels the zero-waste argument of Chapter 5 matters less. The code-extraction quadruple (q1..q4 pairing ql low/high nibbles with two-bit qh fields) is at [crates/muser-engine/src/quant/k_block/q6.rs:45-52], in the llama-pinned deferred-scaling order (§6.7).

6.5 Which tensor carries which format

So the family has members. Which tensor gets which, and who is keeping score? This is where a quantization recipe stops being theory: a few tensors get promoted to more bits, most do not, and every promotion is paid for out of the per-token byte budget this book opened with.

The release artifact is a mix, like llama.cpp’s “Q4_K_M” recipes. The authoritative in-repo map is the shape table of the M=16 microbenchmark harness, which enumerates the exact verify/draft projection shapes with their dtypes and how often each fires per speculative cycle ([crates/muser-bench/src/m16.rs:137-226]):

 label                    dtype   shape (n_in→n_out)   per-cycle mult
 ───────────────────────  ─────   ──────────────────   ──────────────
 attn_q/gate              Q4_K    6656→4096            104  (= 2 × 52 layers)
 attn_k/v                 Q4_K    6656→256              78
 attn_v                   Q6_K    6656→256              26
 attn_output              Q4_K    4096→6656             52
 ffn_gate/up              Q4_K    6656→19968           104
 ffn_down                 Q4_K    19968→6656            26
 ffn_down                 Q6_K    19968→6656            26
 lm_head                  Q5_K    6656→202048            1
 draft.k                  Q4_K    6656→1024              5  (draft layers)
 draft.v                  Q6_K    6656→1024              5
 draft.fc                 Q4_K    33280→6656             1

Figure 6.6: Dtypes and shapes on the release path, from the M16 bench SHAPES table. The multiplicities are the harness’s own counts of dispatches per verify cycle / draft block.

Read the mix off the counts. Attention k/v projections: 78 + 26 = 104 tensors = two per layer, so 26 of the 104 k/v tensors are Q6_K and the rest Q4_K. FFN down-projections: 26 + 26 = 52, so ffn_down alternates Q4_K/Q6_K by layer. Everything bandwidth-dominant — q, gate, output, ffn_gate/up — is Q4_K; the FFN gate/up tensors are pinned Q4_K by the fused kernel’s own guard ([crates/muser-engine/src/decode.rs:5819-5821]). The lm_head is Q5_K — one tensor, but a 6656×202,048 one, worth 924,571,648 B by the arithmetic: 26 blocks/row × 176 B × 202,048 rows. The embedding table rides Q4_K on this artifact through the dedicated muser_embedding_q4k kernel (§6.7). This is Chapter 5’s bitrate analysis made flesh: bulk at 4.5 bits, promoted tensors at 5.5/6.5625, averaging the whole-artifact 4.81 bits/weight computed in Chapter 5.

The draft rows (draft.*) preview Ch 8: the DFlash assistant is itself kquant — its loader requires Q4_K/Q5_K/Q6_K ([crates/muser-engine/src/dflash/weights.rs:148-158]).

6.6 The bytes-per-token tie-back to Chapter 1

Chapter 1’s whole thesis was “one token ≈ stream the model.” Now you can compute that stream precisely for one projection. A matvec — a matrix-by-vector multiply, one dot product of the weight row against the input vector per output element; the shape every projection takes when exactly one token is in flight (Ch 13 derives it from zero) — reads n_in / 256 super-blocks per row; each row of the q projection (n_in = 6656) is:

26 blocks × 144 B = 3,744 B per row   × 4,096 rows = 15,335,424 B per q matrix

Do the same for every tensor class in Figure 6.6 and sum: you converge on the artifact’s own 16,756,681,056 B (≈ 16.76 GB decimal, ≈ 15.6 GiB — this book follows docs/memory-footprint.md in using decimal GB). That file is Chapter 1’s per-token weight read, and its size is not an accident: it is 27.9 B parameters at the mixed 4.5/5.5/6.5625-bit rates of Figure 6.6. The block layout is row-major — each output row is a contiguous run of super-blocks — which is exactly why a matvec kernel can stream it (§6.7) and why prefill of T tokens costs roughly the same DRAM traffic as one token ([crates/muser-engine/src/weights.rs:4-7]).

6.7 The kernels that eat these bytes

A format is only worth what the code reading it is worth, so the next question is: whose kernel actually touches these bytes? The answer is not “ours,” and that turns out to be a deliberate choice with a numerical argument behind it rather than a shortcut.

Muser deliberately runs kquant matmuls through three sources (recall Chapter 4). For a single-token decode projection (encode_quantized_matmul, tokens = 1), the first choice is the pinned llama.cpp metallib:

#![allow(unused)]
fn main() {
// crates/muser-engine/src/metal/encode/qkv.rs:429
if tokens == 1 {
    if let Some(pipeline) = self.ggml_matvec(dtype) {
        let (block_bytes, rows_per_group) = match dtype {
            GgmlType::Q4_K => (144, 2),
            GgmlType::Q5_K => (176, 1),
            GgmlType::Q6_K => (210, 2),
            _ => unreachable!("ggml_matvec returned only for K-quant projections"),
        };
        // …
}

ggml_matvec resolves to kernel_mul_mv_q4_K_f32, kernel_mul_mv_q5_K_f32, or kernel_mul_mv_q6_K_f32 from the metallib loaded via MUSER_GGML_METALLIB ([crates/muser-engine/src/metal/encode.rs:278-280]). The launch geometry is named in prose as the contract requires: grid = n_out ÷ (rows_per_group × 2) threadgroups, threadgroup size (32, 2) — 64 threads, two SIMD groups (Chapter 2’s 32-lane hardware execution unit), Q4_K/Q6_K computing two rows per group and Q5_K one ([crates/muser-engine/src/metal/encode/qkv.rs:444-448]).

Now look at what the unreachable! arm quietly admits. Without the metallib there is no Muser-authored single-token Q6_K matvec. Only Q4_K and Q5_K have hand-written siblings — muser_matvec_q4k_4r2s and muser_matvec_q5k_4sg — so when the pinned metallib is absent the fallback switch does not degrade gracefully on Q6_K, it panics [crates/muser-engine/src/metal/encode/qkv.rs:451-459]. That is the fail-closed reflex again, and it is the right reflex: a wrong logit is worse than no logit.

When the ferrite-lineage fallback does run, here is the Q4_K kernel’s core — read it against §6.3:

// crates/muser-engine/src/shaders/muse_reference.metal:735
kernel void muser_matvec_q4k_4r2s(
    device const uchar *weights [[buffer(0)]],
    device const float *input [[buffer(1)]],
    device float *output [[buffer(2)]],
    constant uint &rows [[buffer(3)]],
    constant uint &cols [[buffer(4)]],
    uint group [[threadgroup_position_in_grid]],
    uint lane [[thread_index_in_simdgroup]],
    uint simd [[simdgroup_index_in_threadgroup]]) {
    uint block_count = cols / 256;
    uint row_bytes = block_count * 144;
    uint base_row = group * 8 + simd * 4;
    // …
    for (uint block_index = 0; block_index < block_count; ++block_index) {
        for (uint row_index = 0; row_index < active_rows; ++row_index) {
            device const uchar *block = row[row_index] + block_index * 144;
            uint delta = *reinterpret_cast<device const uint *>(block);
            float d = float(as_type<half>(ushort(delta & 0xffff)));
            float dmin = float(as_type<half>(ushort(delta >> 16)));
            uint sd0 = *reinterpret_cast<device const uint *>(block + 4);
            uint sd1 = *reinterpret_cast<device const uint *>(block + 8);
            uint sd2 = *reinterpret_cast<device const uint *>(block + 12);
            float d_scale[8];
            float neg_min[8];
            muser_decode_all_q4k_scales(d, dmin, sd0, sd1, sd2, d_scale, neg_min);
            uint input_base = block_index * 256;
            for (uint quant_group = 0; quant_group < 4; ++quant_group) {
                uint packed = uint(block[16 + quant_group * 32 + lane]);
                float low = input[input_base + quant_group * 64 + lane];
                float high = input[input_base + quant_group * 64 + 32 + lane];
                accumulator[row_index] +=
                    fma(d_scale[quant_group * 2], float(packed & 0x0f), neg_min[quant_group * 2]) * low;
                accumulator[row_index] +=
                    fma(d_scale[quant_group * 2 + 1], float(packed >> 4), neg_min[quant_group * 2 + 1]) * high;
            }
        }
    }

(Lines elided: the row-pointer setup and the final simd_sum reduction — see the file.) Every offset you computed by hand is there: bytes 0–3 read as one uint and split into the two f16s, bytes 4–15 as three uints feeding the 8-way scale decode (muser_decode_all_q4k_scales, muse_reference.metal:41), nibbles from byte 16 onward with the low/high sub-block pairing of §6.3. The lane geometry (one lane per byte, 32 lanes covering one 64-element group per iteration) is Part IV material — Ch 13 walks it properly.

What a matvec must read per block, then: 4 bytes of headers, 12 bytes of packed scales, and 128 bytes of nibbles, per 144-byte super-block, per row — plus the activation vector it is dotted against. Nothing else exists to read; the block is entirely self-describing. That is the access pattern in one sentence, and it is why the bytes-per-token accounting of §6.6 is exact.

Batch shapes get their own kernels, all selected in encode_quantized_matmul — the dispatch ladder of Figure 6.7 ([crates/muser-engine/src/metal/encode/qkv.rs:414-641]):

 token count   route (kquant)                                    source
 ───────────   ──────────────────────────────────────────────   ──────────
 1             kernel_mul_mv_q{4,5,6}_K_f32                      llama metallib
 2–3           same kernel, one launch per token                 llama metallib
 4–8           mul_mv_ext family (rows-per-TG 2/3/4/5)           llama metallib
 16            m16_q{4,5,6}k_n32 weight-stationary tile          muser (ferrite)
 Q4_K, ≥16,    matmul_q4k_batch_sgm_aligned                      muser (ferrite)
 aligned
 any (else)    kernel_mul_mm_q{4,5,6}_K_f32 aligned/bounds       llama metallib

Figure 6.7: The kquant dispatch ladder. MUSER_CROSS_VENDOR_QK=1 swaps any rung for the strict-f32 muser_cross_vendor_q* kernels (Chapter 4’s second source), and MUSER_MULTI_COL_VERIFY gates an exact multi-column verify route ([crates/muser-engine/src/metal/encode/multicol.rs:12-14]).

Why keep llama’s batch boundaries at all? Look at the ladder and an obvious simplification suggests itself: the middle rungs could just call the single-token kernel repeatedly, once per activation row, and a whole family of kernels would disappear from the engine. It is the kind of cleanup that looks free. The comment in the dispatch exists to close that door, and it is the chapter’s most important citation:

#![allow(unused)]
fn main() {
// crates/muser-engine/src/metal/encode/qkv.rs:476
// Match the source-pinned llama.cpp Metal dispatch boundary exactly:
// K-quant projections with four through eight activation rows use
// `mul_mv_ext`, with a token-count-specific number of rows per
// threadgroup. This changes the floating-point reduction order, so
// substituting repeated decode GEMVs here breaks embedding/logprob
// numerical parity even when every other layer is identical.
}

That is worth restating slowly, because it is the second genuinely hard idea in this chapter. Floating-point addition is not associative: add the same products in a different order and you get a different sum, usually differing only in the last bits. A different kernel means a different reduction order, a different order means different last bits, and different last bits mean a logprob that no longer matches the comparator’s. So these batch boundaries are not a performance-tuning quirk inherited by accident. They are part of the numerical contract. Match the comparator’s kernels and you inherit its arithmetic; improvise, and parity is gone before anyone has asked a speed question.

The embedding kernel completes the single-token picture. muser_embedding_q4k ([crates/muser-engine/src/shaders/muse_reference.metal:961]) is one thread per output element: it resolves row_bytes = (hidden_dim/256) × 144 — the same 3,744-byte arithmetic as §6.6 — and calls the shared muser_q4k_value scalar dequant (muse_reference.metal:946), which is §6.3’s formula indexed by element rather than streamed by block.

6.8 Tradeoffs

The n32 tile: measured occupancy over intuition. Start with the fork. The 16-row batch — the DFlash verify shape of Chapter 8 — had to run on something, and the retained kernels already covered it. Keeping them was the cheap answer, and on paper it looked like the right one: same shapes, same arithmetic, only the batch width had changed. The bandwidth counter disagreed. Those projections came back at ~50–180 GB/s on an ~800 GB/s-class M3 Ultra, and the reason was structural rather than arithmetic: the n_out/64 K-serial threadgroup shape carried 6–20 KiB of threadgroup memory, and a threadgroup that fat could not fill a core [ledger §Stage B L0]. The kernel was not doing too much work. It was leaving most of the machine standing idle.

That reframed the problem from arithmetic to occupancy — and occupancy is a shape question, so the microbenchmark-first L-series went shape-hunting before anything touched the engine. The t128/t64/cross-sg designs were iterated and none of them won. What won was a weight-stationary tile, m16_q*k_n32: 32 output rows per threadgroup, 64-K stages, 6 KiB of threadgroup memory. The verify-cycle matmul estimate fell 148.3 → 82.8–85.0 ms against the retained SGM tile, with the pinned mul_mm measured at ~177 for scale [ledger §Stage B L0]; in the integrated engine the verify forward followed it down, 202.3 → 128.4 ms [ledger §Stage B L1].

Speed alone would not have shipped it. Exactness was gated, not assumed: the tile landed “within the accepted half-staged error envelope (max-abs identical to the pinned kernels on the same data, zero argmax flips on every shape)” [ledger §Stage B L0]. That sequence is the house style — hypothesis (occupancy), apparatus (muser-m16-bench at the exact Figure 6.6 shapes), then a gate — and the lesson outlives this particular tile: when a kernel runs far under the machine’s rated bandwidth, suspect its shape before you suspect its math.

Q5_K at 5.5 bits on the lm_head — and the transcode temptation. The promoted lm_head is the one place this artifact pays a premium you can see from orbit, so it is fair to ask what that premium buys and whether a cheaper trick could have dodged it. The 5-bit lm_head costs 924,571,648 B (§6.5 arithmetic) — 1.22× a Q4_K equivalent (756,467,712 B).

There is a tempting move available here, and the tree already contains a worked example of it. The Q5_0 sibling has an instructive in-tree trade: transcode_q5_to_q8 expands 5-bit blocks to Q8_0 at GPU upload time, “trading 30 % more memory bandwidth for 3× fewer GPU decode instructions” [crates/muser-engine/src/quant/blocks.rs:100-103]. The reason that digression matters for the lm_head is that it is the same class of trade the n32 tile makes, run in the opposite direction: one spends bandwidth to buy decode instructions, the other spends decode work to buy occupancy. On the live path the lm_head takes neither deal. It stays Q5_K and rides m16_q5k_n32 at 16 rows: 5.87 → 4.11 ms per dispatch against mul_mm [ledger §Stage B L0].

Why not Q6_K everywhere? By Figure 6.1, Q6_K is 6.5625/4.5 = 1.458× the bytes of Q4_K. Every weight byte is read per token (§6.6), so Q6_K-everywhere would scale the per-token stream by ~1.46× and, in the bandwidth-bound regime Chapter 1 proved, cut throughput by the same factor. The artifact spends 6.5625 bits only where the recipe says it pays (ffn_down on half the layers, 26 k/v tensors — Figure 6.6); whether that specific allocation is the quality optimum is the recipe designer’s claim, not a Muser measurement [unverified].

6.9 Where the gap lives

Every chapter in this book has to answer the same question about its own subject: is this where the missing time hides? Here the answer is no — and the way that answer was reached matters more than the answer itself, because it was reconciled rather than assumed.

The quant format is not the decode gap. When the engine’s one-token graphs were reconciled closure-by-closure (the +196 dispatch-gap diagnosis), the families were 104 norm-boundary groups, 39 SWA staging groups, 52 KV-publication splits, one copy — not matvec arithmetic [docs/decode-dispatch-gap-20260815.md]. The kquant matvecs themselves run on llama.cpp’s own pinned kernels for numerical parity ([crates/muser-engine/src/metal/encode.rs:278-280]), so on this lane the weight-format question was settled before it could become a gap: same bytes, same kernels, same arithmetic order as the comparator. The lane’s measured decode ratios — 1.0274–1.0504× across the six-depth plain matrix, five exact-token reps per depth [docs/benchmarks.md §1] — carry the paradox this book keeps meeting: parity engines can still differ in dispatch, and the gap lives there, not here.

6.10 What comes next

The kquant family is integer codebooks: uniform grids, local scales, a min where the format wants one. Chapter 7 meets the other family — NVFP4, where the 4-bit payload is itself a tiny float, the block scale is itself a tiny float, and the whole lane was built for the same bytes-per-token argument with a different arithmetic inside. You will pack one of its groups by hand (using the repo’s own fixture bytes), watch the fail-closed loader bind its scale tensors, and read the Metal kernel that keeps the whole contraction in integers — and you will see why this lane’s 35.491 tok/s must never be called “faster” than kquant’s 35.440.


References

  • [crates/muser-engine/src/gguf/types.rs:9-127]GgmlType, block sizes/elements (Figure 6.1’s source), the NVFP4_E2M1/F8_E4M3FN doc.
  • [crates/muser-engine/src/quant/k_block.rs:12-49]dequant_q4_k, this chapter’s primary source (§6.2, §6.3).
  • [crates/muser-engine/src/quant/k_block.rs:51-105]dequant_q5_k and the 176-byte layout doc.
  • [crates/muser-engine/src/quant/k_block/q6.rs:15-75]dot_q6_k_f32_llama: the 210-byte layout (ql/qh/sc/d offsets), signed-code extraction, deferred-scaling order.
  • [crates/muser-engine/src/loader.rs:72-91]weight_precision: fail-closed lane selection (q4_k_xl default).
  • [crates/muser-engine/src/decode.rs:136-139], :1209 — Metal-path dtype admissions for projections and the embedding.
  • [crates/muser-bench/src/m16.rs:137-226] — the SHAPES table: dtypes, real dimensions, per-cycle multiplicities (Figure 6.6).
  • [crates/muser-engine/src/metal/encode/qkv.rs:414-641]encode_quantized_matmul: the full dispatch ladder (Figure 6.7), including the source-pinned boundary comment at :476.
  • [crates/muser-engine/src/metal/encode.rs:278-280] — the pinned kernel_mul_mv_q{4,5,6}_K_f32 metallib PSOs.
  • [crates/muser-engine/src/shaders/muse_reference.metal:735-788]muser_matvec_q4k_4r2s; :41-62 muser_decode_all_q4k_scales; :961-977 muser_embedding_q4k.
  • [crates/muser-engine/src/quant/blocks.rs:100-103] — the Q5→Q8 transcode trade comment.
  • [crates/muser-engine/src/weights.rs:4-7] — row-contiguity / prefill DRAM-amortization note.
  • [docs/decode-dispatch-gap-20260815.md] — the +196-closure reconciliation (§6.9).
  • [ledger §Stage B L0], [ledger §Stage B L1]docs/goal-parity-ledger-2026-08.md, the M16 microbenchmark and integration entries (§6.8’s ms figures).
  • [docs/benchmarks.md §1] — the six-depth plain matrix ratios.
  • [claims #11] — kquant 35.440 / NVFP4 35.491 tok/s scope (§6.1, §6.9).
  • Ch 5 — the format-agnostic template this chapter instantiated.
  • [ferrite-book Ch 5] — the ancestor Q4_K chapter; Muser’s dequant_q4_k is the same extraction lineage (NOTICE, docs/extraction-manifest.md).