Skip to content

Offer a half-precision KV cache behind a flag (default off) - #109

Merged
bsbodden merged 2 commits into
mainfrom
fix/f16-kv-cache
Aug 7, 2026
Merged

Offer a half-precision KV cache behind a flag (default off)#109
bsbodden merged 2 commits into
mainfrom
fix/f16-kv-cache

Conversation

@bsbodden

@bsbodden bsbodden commented Aug 7, 2026

Copy link
Copy Markdown
Member

llama.cpp holds cache_k/cache_v as F16, so attention there reads back rounded keys and values. This runtime keeps F32 — more precise than the reference rather than equivalent to it.

How it was localized

Replayed layer 0 of Qwen3-Embedding-0.6B Q6_K op by op against llama-eval-callback. Every matmul already matched — Q6_K dequant is bit-exact, q8_K activation quantization matches -127.f/max with upper-only clamp, and the Q6_K×Q8_K dot matches to 3e-8 at both cols=1024 and cols=2048.

The first divergence was node_27, the attention output projection. Its only differing input was V:

F32 V F16 V llama.cpp
node_27 0.635219 0.619453 0.619455
l_out-0 −7.648265 −7.676750 −7.676746

Why it is off by default

Two llama.cpp equivalence references disagree about this change.

Rounding reproduces Qwen3 layer output exactly and raises embedding agreement everywhere measured:

model quant default flag on
Qwen3-Embedding-0.6B Q8_0 0.9995014 0.9996100
Qwen3-Embedding-0.6B Q4_K_M 0.9969991 0.9977295
Qwen3-Embedding-0.6B Q6_K 0.9969038 0.9970416

But it flips one greedy token on the pinned SQLCoder Q5_K_M fixture, which asserts token equality against llama.cpp b9960:

actual   [13, 917, 29901, 4576]
expected [13, 917, 29901,  274]

Since one llama.cpp reference says the rounding is right and another says it is wrong, it cannot be the default until that is resolved. Default behaviour is unchanged, so no recorded evidence moves and the full slow-model suite is unaffected. Select it with -Dmodels.purejava.halfPrecisionKvCache=true.

Scope

This does not on its own lift the qwen3 K-quant variants over the 0.999 floor — 0.99773 is closer but still short. A second source remains, tracked in the follow-up task.

Separately, this work found that :models-bench:run does not forward arbitrary -D system properties (only models.native.kernels.library), so any tuning-flag experiment run through Gradle run silently had no effect. The toggle sweep was re-done with direct java -cp invocation; the conclusions held, but the earlier method was invalid.

🤖 Generated with Claude Code

llama.cpp holds cache_k and cache_v as F16, so attention there reads back
rounded keys and values. This runtime kept them F32, making it more
precise than the reference rather than equivalent to it.

Localized by replaying layer 0 of Qwen3-Embedding-0.6B Q6_K op by op
against llama-eval-callback. Every matmul already matched; the first
divergence was the attention output projection, whose only differing
input was V. Rounding V to F16 reproduces llama.cpp:

                       F32 V      F16 V      llama.cpp
  node_27 (attn_out)   0.635219   0.619453   0.619455
  l_out-0             -7.648265  -7.676750  -7.676746

Agreement against llama.cpp 6ea215d17, minimum cosine over the probe set:

  Qwen3-Embedding-0.6B  Q8_0    0.9995014 -> 0.9996100
  E5-Mistral-7B         Q4_K_M  0.9995117 -> 0.9994905
  Qwen3-Embedding-0.6B  Q4_K_M  0.9969991 -> 0.9977295
  Qwen3-Embedding-0.6B  Q5_K_M  0.9969529 -> 0.9972010
  Qwen3-Embedding-0.6B  Q6_K    0.9969038 -> 0.9970416

Both published embedding artifacts still reproduce, and the qwen3 Q8_0
one improves. Greedy generation is byte-identical on the published
qwen3_0_6b_q4_0 across 48 tokens, so no recorded token reference moves.

This does not by itself lift the qwen3 K-quant variants over the 0.999
floor; a second source of divergence remains, tracked separately.
llama.cpp holds cache_k and cache_v as F16, so attention there reads back
rounded keys and values. This runtime keeps F32, making it more precise
than the reference rather than equivalent to it.

Localized by replaying layer 0 of Qwen3-Embedding-0.6B Q6_K op by op
against llama-eval-callback. Every matmul already matched; the first
divergence was the attention output projection, whose only differing
input was V. Rounding V to F16 reproduces llama.cpp:

                       F32 V      F16 V      llama.cpp
  node_27 (attn_out)   0.635219   0.619453   0.619455
  l_out-0             -7.648265  -7.676750  -7.676746

Enabling it raises agreement on every embedding artifact measured:

  Qwen3-Embedding-0.6B  Q8_0    0.9995014 -> 0.9996100
  Qwen3-Embedding-0.6B  Q4_K_M  0.9969991 -> 0.9977295
  Qwen3-Embedding-0.6B  Q6_K    0.9969038 -> 0.9970416

It is off by default because two llama.cpp equivalence references
disagree about it: rounding reproduces Qwen3 layer output exactly, but
flips one greedy token on the pinned SQLCoder Q5_K_M fixture, which
asserts token equality against llama.cpp b9960.

    actual   [13, 917, 29901, 4576]
    expected [13, 917, 29901,  274]

Default behaviour is therefore unchanged and no recorded evidence moves.
Select it with -Dmodels.purejava.halfPrecisionKvCache=true.

This does not on its own lift the qwen3 K-quant variants over the 0.999
floor; a second source of divergence remains.
@bsbodden bsbodden changed the title Store the KV cache at half precision, as llama.cpp does Offer a half-precision KV cache behind a flag (default off) Aug 7, 2026
@bsbodden
bsbodden merged commit 948c9a7 into main Aug 7, 2026
24 checks passed
@bsbodden
bsbodden deleted the fix/f16-kv-cache branch August 7, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant