Skip to content

Commit dea3841

Browse files
committed
kv-cache: per-channel K-cache mean-centering for Q4_0 (fork #51)
Re-port of the fork's --kv-mean-center feature onto upstream master. Adds common/kv-mean-center, the arg + LLAMA_EXAMPLE_KV_MEAN_CENTER, the llama.h API, and the kv-cache/context/graph hooks. Conflicts resolved against upstream's rewritten kv-cache/context (kept only the path_kv_mean_center guard).
1 parent b17e4c1 commit dea3841

18 files changed

Lines changed: 1099 additions & 2 deletions

common/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,8 @@ add_library(${TARGET}
8181
imatrix-loader.cpp
8282
imatrix-loader.h
8383
json-schema-to-grammar.cpp
84+
kv-mean-center.cpp
85+
kv-mean-center.h
8486
llguidance.cpp
8587
log.cpp
8688
log.h

common/arg.cpp

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1682,7 +1682,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
16821682
[](common_params & params, int value) {
16831683
params.n_chunks = value;
16841684
}
1685-
).set_examples({LLAMA_EXAMPLE_IMATRIX, LLAMA_EXAMPLE_PERPLEXITY, LLAMA_EXAMPLE_RETRIEVAL}));
1685+
).set_examples({LLAMA_EXAMPLE_IMATRIX, LLAMA_EXAMPLE_PERPLEXITY, LLAMA_EXAMPLE_RETRIEVAL, LLAMA_EXAMPLE_KV_MEAN_CENTER}));
16861686
add_opt(common_arg({ "-fa", "--flash-attn" }, "[on|off|auto]",
16871687
string_format("set Flash Attention use ('on', 'off', or 'auto', default: '%s')",
16881688
llama_flash_attn_type_name(params.flash_attn_type)),
@@ -2391,6 +2391,15 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
23912391
params.cache_type_v = kv_cache_type_from_str(value);
23922392
}
23932393
).set_env("LLAMA_ARG_CACHE_TYPE_V"));
2394+
add_opt(common_arg(
2395+
{"--kv-mean-center"}, "FNAME",
2396+
"path to a K-cache mean-centering bias file (GGUF), generated with tools/kv-mean-center\n"
2397+
"subtracts a fixed per-(head,channel) bias from K before it is quantized into the cache;\n"
2398+
"requires --cache-type-k q4_0 (see docs/kv-mean-center.md)",
2399+
[](common_params & params, const std::string & value) {
2400+
params.kv_mean_center_path = value;
2401+
}
2402+
).set_env("LLAMA_ARG_KV_MEAN_CENTER"));
23942403
add_opt(common_arg(
23952404
{"--hellaswag"},
23962405
"compute HellaSwag score over random tasks from datafile supplied with -f",
@@ -3044,7 +3053,7 @@ common_params_context common_params_parser_init(common_params & params, llama_ex
30443053
params.out_file = value;
30453054
}
30463055
).set_examples({LLAMA_EXAMPLE_IMATRIX, LLAMA_EXAMPLE_CVECTOR_GENERATOR, LLAMA_EXAMPLE_EXPORT_LORA, LLAMA_EXAMPLE_TTS, LLAMA_EXAMPLE_FINETUNE,
3047-
LLAMA_EXAMPLE_RESULTS, LLAMA_EXAMPLE_EXPORT_GRAPH_OPS, LLAMA_EXAMPLE_CLI}));
3056+
LLAMA_EXAMPLE_RESULTS, LLAMA_EXAMPLE_EXPORT_GRAPH_OPS, LLAMA_EXAMPLE_CLI, LLAMA_EXAMPLE_KV_MEAN_CENTER}));
30483057
add_opt(common_arg(
30493058
{"-ofreq", "--output-frequency"}, "N",
30503059
string_format("output the imatrix every N iterations (default: %d)", params.n_out_freq),

common/common.cpp

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1667,6 +1667,10 @@ struct llama_context_params common_context_params_to_llama(const common_params &
16671667
cparams.type_k = params.cache_type_k;
16681668
cparams.type_v = params.cache_type_v;
16691669

1670+
// note: params (and therefore params.kv_mean_center_path) is kept alive by the caller for
1671+
// at least as long as it takes to call llama_init_from_model() with the returned cparams
1672+
cparams.path_kv_mean_center = params.kv_mean_center_path.empty() ? nullptr : params.kv_mean_center_path.c_str();
1673+
16701674
return cparams;
16711675
}
16721676

common/common.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ enum llama_example {
107107
LLAMA_EXAMPLE_EXPORT_GRAPH_OPS,
108108
LLAMA_EXAMPLE_DOWNLOAD,
109109
LLAMA_EXAMPLE_TOKENIZE,
110+
LLAMA_EXAMPLE_KV_MEAN_CENTER,
110111

111112
LLAMA_EXAMPLE_COUNT,
112113
};
@@ -576,6 +577,10 @@ struct common_params {
576577
ggml_type cache_type_k = GGML_TYPE_F16; // KV cache data type for the K
577578
ggml_type cache_type_v = GGML_TYPE_F16; // KV cache data type for the V
578579

580+
// path to a K-cache mean-centering bias file (GGUF), or empty to disable.
581+
// only takes effect when cache_type_k == GGML_TYPE_Q4_0; see docs/kv-mean-center.md
582+
std::string kv_mean_center_path = "";
583+
579584
common_conversation_mode conversation_mode = COMMON_CONVERSATION_MODE_AUTO;
580585

581586
// multimodal models (see tools/mtmd)

common/kv-mean-center.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#include "kv-mean-center.h"
2+
3+
#include "log.h"
4+
5+
#include "ggml.h"
6+
#include "gguf.h"
7+
8+
#include <cstring>
9+
10+
bool common_kv_mean_center_write(
11+
const std::string & fname,
12+
const std::vector<common_kv_mean_center_layer> & layers) {
13+
size_t n_with_bias = 0;
14+
for (const auto & layer : layers) {
15+
if (!layer.bias.empty()) {
16+
n_with_bias++;
17+
}
18+
}
19+
20+
if (n_with_bias == 0) {
21+
LOG_ERR("%s: no layers with bias data to write\n", __func__);
22+
return false;
23+
}
24+
25+
size_t data_size = 0;
26+
for (const auto & layer : layers) {
27+
if (!layer.bias.empty()) {
28+
data_size += GGML_PAD(ggml_tensor_overhead() + sizeof(float)*layer.bias.size(), GGML_MEM_ALIGN);
29+
}
30+
}
31+
32+
struct ggml_init_params params = {
33+
/*.mem_size =*/ data_size,
34+
/*.mem_buffer =*/ NULL,
35+
/*.no_alloc =*/ false,
36+
};
37+
38+
struct ggml_context * ctx = ggml_init(params);
39+
struct gguf_context * ctx_gguf = gguf_init_empty();
40+
41+
if (!ctx || !ctx_gguf) {
42+
LOG_ERR("%s: failed to allocate ggml/gguf context\n", __func__);
43+
if (ctx) ggml_free(ctx);
44+
if (ctx_gguf) gguf_free(ctx_gguf);
45+
return false;
46+
}
47+
48+
gguf_set_val_str(ctx_gguf, "general.type", "kv-mean-center");
49+
50+
for (const auto & layer : layers) {
51+
if (layer.bias.empty()) {
52+
continue;
53+
}
54+
55+
const std::string name = "kv_bar.blk." + std::to_string(layer.il) + ".k";
56+
57+
ggml_tensor * t = ggml_new_tensor_1d(ctx, GGML_TYPE_F32, (int64_t) layer.bias.size());
58+
ggml_set_name(t, name.c_str());
59+
60+
memcpy(t->data, layer.bias.data(), layer.bias.size()*sizeof(float));
61+
62+
gguf_add_tensor(ctx_gguf, t);
63+
}
64+
65+
const bool ok = gguf_write_to_file(ctx_gguf, fname.c_str(), false);
66+
if (!ok) {
67+
LOG_ERR("%s: failed to write %s\n", __func__, fname.c_str());
68+
}
69+
70+
gguf_free(ctx_gguf);
71+
ggml_free(ctx);
72+
73+
return ok;
74+
}

common/kv-mean-center.h

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#pragma once
2+
3+
#include <cstdint>
4+
#include <string>
5+
#include <vector>
6+
7+
// Shared GGUF file format for the K-cache mean-centering bias vectors produced by
8+
// tools/kv-mean-center and consumed by llama_kv_cache::load_kv_mean_center() (see
9+
// docs/kv-mean-center.md).
10+
//
11+
// The file stores one F32 tensor per model layer that has a bias, named
12+
// "kv_bar.blk.<il>.k", holding n_embd_head_k(il) * n_head_kv(il) values laid out as
13+
// [n_embd_head_k, n_head_kv] (channel-fastest), matching the memory layout of the K
14+
// tensor at the point it is written into the cache.
15+
16+
// per-layer entry to write; `bias` empty means "no bias for this layer" (skipped on write)
17+
struct common_kv_mean_center_layer {
18+
int32_t il = -1;
19+
std::vector<float> bias; // length n_embd_head_k(il) * n_head_kv(il)
20+
};
21+
22+
// write a K-cache mean-centering bias file in GGUF format.
23+
// returns false (and logs an error) on failure.
24+
bool common_kv_mean_center_write(
25+
const std::string & fname,
26+
const std::vector<common_kv_mean_center_layer> & layers);

docs/kv-mean-center.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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.

include/llama.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,14 @@ extern "C" {
380380
enum ggml_type type_k; // data type for K cache [EXPERIMENTAL]
381381
enum ggml_type type_v; // data type for V cache [EXPERIMENTAL]
382382

383+
// optional path to a per-layer K-cache mean-centering bias file (GGUF), or NULL to disable.
384+
// the bias is subtracted from the K vector for each (kv-head, channel) right before it is
385+
// written into the K cache, which improves quantization fidelity for GGML_TYPE_Q4_0 without
386+
// changing attention results (the same constant is added to every logit in a query's row,
387+
// which softmax is invariant to). currently only supported when type_k == GGML_TYPE_Q4_0.
388+
// see tools/kv-mean-center to generate this file and docs/kv-mean-center.md for details.
389+
const char * path_kv_mean_center;
390+
383391
// Abort callback
384392
// if it returns true, execution of llama_decode() will be aborted
385393
// currently works only with CPU execution

src/llama-context.cpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "llama-impl.h"
77
#include "llama-batch.h"
88
#include "llama-io.h"
9+
#include "llama-kv-cache.h"
910
#include "llama-memory.h"
1011
#include "llama-mmap.h"
1112
#include "llama-model.h"
@@ -389,6 +390,17 @@ llama_context::llama_context(
389390
};
390391

391392
memory.reset(model.create_memory(params_mem, cparams));
393+
394+
if (params.path_kv_mean_center != nullptr) {
395+
auto * kv = dynamic_cast<llama_kv_cache *>(memory.get());
396+
if (!kv) {
397+
throw std::runtime_error("path_kv_mean_center is only supported for the standard KV cache "
398+
"(not recurrent, hybrid or MLA/DSA memory types)");
399+
}
400+
if (!kv->load_kv_mean_center(params.path_kv_mean_center)) {
401+
throw std::runtime_error("failed to load K-cache mean-centering bias file");
402+
}
403+
}
392404
}
393405

394406
// init backends
@@ -3507,6 +3519,7 @@ llama_context_params llama_context_default_params() {
35073519
/*.cb_eval_user_data =*/ nullptr,
35083520
/*.type_k =*/ GGML_TYPE_F16,
35093521
/*.type_v =*/ GGML_TYPE_F16,
3522+
/*.path_kv_mean_center =*/ nullptr,
35103523
/*.abort_callback =*/ nullptr,
35113524
/*.abort_callback_data =*/ nullptr,
35123525
/*.embeddings =*/ false,
@@ -3595,6 +3608,13 @@ llama_context * llama_init_from_model(
35953608
}
35963609
}
35973610

3611+
if (params.path_kv_mean_center != nullptr && params.type_k != GGML_TYPE_Q4_0) {
3612+
LLAMA_LOG_ERROR("%s: path_kv_mean_center requires the K cache type to be Q4_0 (got %s)\n",
3613+
__func__, ggml_type_name(params.type_k));
3614+
return nullptr;
3615+
}
3616+
3617+
35983618
if (params.pooling_type != LLAMA_POOLING_TYPE_UNSPECIFIED &&
35993619
params.pooling_type != model->hparams.pooling_type) {
36003620
//user-specified pooling-type is different from the model default

src/llama-graph.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2779,6 +2779,11 @@ ggml_tensor * llm_graph_context::build_attn(
27792779
const auto & k_idxs = inp->get_k_idxs();
27802780
const auto & v_idxs = inp->get_v_idxs();
27812781

2782+
// hook point for KV cache calibration tooling (e.g. tools/kv-mean-center): this is
2783+
// exactly the K tensor that cpy_k() writes into the cache, after any RoPE/rotation
2784+
// the architecture applies upstream
2785+
cb(k_cur, "k_cache_in", il);
2786+
27822787
ggml_build_forward_expand(gf, mctx_cur->cpy_k(ctx0, k_cur, k_idxs, il));
27832788
ggml_build_forward_expand(gf, mctx_cur->cpy_v(ctx0, v_cur, v_idxs, il));
27842789
}

0 commit comments

Comments
 (0)