|
| 1 | +# K-cache mean-centering |
| 2 | + |
| 3 | +`--kv-mean-center` is an optional, opt-in feature that subtracts a fixed, precomputed |
| 4 | +per-(kv-head, channel) bias from the K vector at the moment it is written into the KV cache, |
| 5 | +in order to improve quantization fidelity for `GGML_TYPE_Q4_0` K caches. |
| 6 | + |
| 7 | +This is currently scoped to `GGML_TYPE_Q4_0` only. |
| 8 | + |
| 9 | +## The idea |
| 10 | + |
| 11 | +`GGML_TYPE_Q4_0` is a symmetric (zero-point-free) block quantizer: each block is represented as |
| 12 | +`value ~= scale * q`, where `q` is a signed low-bit integer and there is no bias/zero-point term. |
| 13 | +If a given (kv-head, channel) position's real K activations have a nonzero mean across tokens, |
| 14 | +symmetric quantization wastes some of its dynamic range encoding that constant bias, which |
| 15 | +increases quantization error for that channel. |
| 16 | + |
| 17 | +The fix: measure a per-(kv-head, channel) bias `k_bar` ahead of time (see |
| 18 | +[Calibration](#calibration) below), and subtract it from the K vector for every token, right |
| 19 | +before `Q4_0` quantization happens as part of writing it into the cache. Nothing else in |
| 20 | +attention needs to change. |
| 21 | + |
| 22 | +### Why this is safe (softmax-invariance) |
| 23 | + |
| 24 | +For a fixed query row `q` attending over cached keys `k_0 ... k_n` (all in one layer/head), if |
| 25 | +every cached key is centered by the same `k_bar` before being quantized and stored, the true dot |
| 26 | +product decomposes as: |
| 27 | + |
| 28 | +``` |
| 29 | +q . k_i = q . (k_i - k_bar) + q . k_bar = q . k_i_stored + q . k_bar |
| 30 | +``` |
| 31 | + |
| 32 | +The `q . k_bar` term does not depend on `i` (the key's position) -- it is added identically to |
| 33 | +every logit in that query's row. Softmax is invariant to a constant additive shift applied to |
| 34 | +every logit in the same row (`softmax(x + c) == softmax(x)`), so the attention weights, and |
| 35 | +therefore the rest of the model's output, are unaffected. This means the technique is exactly |
| 36 | +correctness-preserving in infinite precision, and in practice the only observable difference is |
| 37 | +ordinary floating point rounding (see `tests/test-kv-mean-center.cpp`, which checks this directly |
| 38 | +against an unquantized F32 K cache). This is also why it is a zero decode-time-cost win: one |
| 39 | +subtract at the point of cache write, nothing else changes. |
| 40 | + |
| 41 | +The actual benefit is purely on quantization fidelity: centering the residual around zero before |
| 42 | +`Q4_0`'s symmetric quantizer reduces per-channel quantization error for channels that have a real, |
| 43 | +consistent activation bias. Quantifying that improvement on a production-scale model (e.g. via a |
| 44 | +logit-KLD comparison against an uncentered `Q4_0` baseline) is a natural follow-up; this repo does |
| 45 | +not ship a measured number for a specific trained model. |
| 46 | + |
| 47 | +## Usage |
| 48 | + |
| 49 | +1. Generate a bias file with `tools/kv-mean-center` (see its |
| 50 | + [README](../tools/kv-mean-center/README.md) for details): |
| 51 | + |
| 52 | + ``` |
| 53 | + ./llama-kv-mean-center -m model.gguf -f calibration-data.txt -o kv-mean-center.gguf |
| 54 | + ``` |
| 55 | + |
| 56 | +2. Load it at inference time, together with a `Q4_0` K cache: |
| 57 | + |
| 58 | + ``` |
| 59 | + ./llama-cli -m model.gguf -ctk q4_0 --kv-mean-center kv-mean-center.gguf -p "..." |
| 60 | + ``` |
| 61 | + |
| 62 | +`--kv-mean-center` requires `--cache-type-k q4_0`. If the K cache type is anything else, context |
| 63 | +creation fails with a clear error rather than silently doing nothing, matching this codebase's |
| 64 | +existing convention for other cache-type-gated options (e.g. quantized V cache requiring flash |
| 65 | +attention). |
| 66 | + |
| 67 | +## Bias file format |
| 68 | + |
| 69 | +The bias file is a small GGUF file with one F32 1-D tensor per layer that has a bias, named |
| 70 | +`kv_bar.blk.<il>.k`, holding `n_embd_head_k(il) * n_head_kv(il)` values laid out as |
| 71 | +`[n_embd_head_k, n_head_kv]` (channel-fastest). This matches the in-memory layout of the K tensor |
| 72 | +at the point it is written into the cache, so the file can be loaded directly as a small |
| 73 | +broadcastable bias tensor per layer. |
| 74 | + |
| 75 | +## Calibration |
| 76 | + |
| 77 | +`tools/kv-mean-center` computes the bias by running a plain text calibration corpus through the |
| 78 | +model and averaging the K tensor right before it would be written into the cache (via the |
| 79 | +`k_cache_in` tag added to `llm_graph_context::build_attn()`, read through the same backend |
| 80 | +scheduler eval-callback mechanism `llama-imatrix` uses to capture activations). See |
| 81 | +[tools/kv-mean-center/README.md](../tools/kv-mean-center/README.md) for usage. |
| 82 | + |
| 83 | +## Scope and limitations |
| 84 | + |
| 85 | +- Only `GGML_TYPE_Q4_0` is supported; other K cache types are rejected. Generalizing the mechanism |
| 86 | + to other quantization types is future work. |
| 87 | +- Only the plain (non-recurrent, non-hybrid, non-MLA/DSA) KV cache is supported. |
| 88 | +- The calibration hook (`k_cache_in`) is currently only wired into the standard |
| 89 | + dense/GQA attention path (`llm_graph_context::build_attn(llm_graph_input_attn_kv *, ...)`), |
| 90 | + which covers the large majority of architectures. MLA and other specialized attention variants |
| 91 | + are not covered yet. |
| 92 | +- If this fork's optional Hadamard K/Q rotation feature is also active (automatic for `Q4_0` |
| 93 | + caches whose head dimension is a multiple of 64, unless `LLAMA_ATTN_ROT_DISABLE=1`), the bias is |
| 94 | + calibrated in the pre-rotation basis while it is applied in whatever basis `cpy_k()` sees |
| 95 | + (post-rotation, if active). This remains exactly safe (the invariance argument above is |
| 96 | + basis-independent), but the calibrated bias is a less accurate estimate of that channel's true |
| 97 | + post-rotation mean in that configuration. Calibrating directly against the post-rotation |
| 98 | + representation is a natural follow-up. |
0 commit comments