CPU: KV cache quantization — KV8 (fp8 e4m3) + KV_TQ (rotated-int4 / PolarQuant)#553
CPU: KV cache quantization — KV8 (fp8 e4m3) + KV_TQ (rotated-int4 / PolarQuant)#553NeuralNotwerk wants to merge 3 commits into
Conversation
|
CPU performance validation for this branch, measured on its exact tree (744B, fully resident —
Prefill ~5.5–5.6 tok/s across all cells. Two takeaways:
Environment: EPYC-class 32C host, ZFS storage, dockerized gcc 13 build, |
|
This is on-priority — a smaller KV cache is exactly what buys longer context on a low-RAM box — and the review is positive: opt-in (KV8/KV_TQ default off), format-versioned (.coli_kv stays byte-compatible for default users, resume-safe), heavily unit-tested, and the token-exact oracle is green. I want it in. But it now conflicts with Two things to confirm on the rebase, given the 'no silent errors' bar here: (1) the producer-loop parallelization in |
|
Rebased onto current On your two points:
Also verified through the rebase: your new Post-rebase validation: CPU + |
c4b27ed to
b98cf47
Compare
|
Rebased onto current Flagging why this needed a second pass: the rebase I posted on the 23rd targeted On your point (1) — producer-loop determinism: preserved verbatim. On your point (2) — no automated accuracy gate: agreed and unchanged; both tiers stay opt-in and default off. One correction I made while rebasing, worth calling out because it changes what this PR claims. The That was a leftover from the split, and shipping it would have meant the docs promising GPU acceleration that the merged code doesn't have. The rebased commit now documents the CPU-only reality:
Local re-verification on the rebased tree (macOS 26.5.1, Apple M4, arm64/NEON — the other half of the "identical flips across ISAs" claim): Nothing else changed — no code differences from what you reviewed, same two commits. |
|
Reviewed this end to end and reproduced your numbers independently — thank you for the clean split from #399, and for gating GPU behind the CPU consumer so a KV8/KV_TQ build is safe today. What I verified locally (all good):
The one thing blocking a merge — real-model quality. Could you post a real-model quality datapoint — the #108 protocol (hellaswag n=200, or MMLU), f32 vs KV8 vs KV_TQ on the real GLM-5.2 gs64 container? That's the number that decides it:
Either way the merge is close — the mechanics are solid and it's off by default. I just won't call a decode-quality feature safe on the strength of a random tiny model. Ping me with the n=200 row and we land it. |
…olarQuant) Latent-KV cache quantization for the MLA attention, CPU path only — the CUDA and Metal kernels are follow-up PRs, each gated by a hardware owner. - KV8=1: fp8 e4m3 byte cache + per-row f32 scale (~3.9x less KV RAM). The CPU consumer LUT-decodes inline in the score/context dot products; the per-row scale factors out of the sums. - KV_TQ=4: rotated-int4 codec (randomized-Hadamard rotation + Lloyd codebook, radius rides the per-row scale). The consumer uses the rotation's orthogonality (q.x_hat == rotate(q).c): rotate the query once per head, dot the packed nibbles through the 16-level codebook, unrotate the accumulated context once — the cache is never reconstructed. KV_TQ=2|3|5|6 and KV_TQ_POLAR=1 use the PolarQuant codec (recursive-polar, variable bits). - .coli_kv persistence: v2 (KV8) / v3 (KV_TQ) formats; the file magic encodes the tier and h[7] the TQ codec+bits. Any mode mismatch is refused and the cache restarted with an explicit message — never misread. v1 files upgrade in place (old file untouched until the first save rewrites it). - Existing CUDA/Metal attention fast paths gain !g_kv8&&!g_tq guards: they read f32 rows that are not allocated under quantized KV, so quantized runs fall to the CPU consumer (with a one-time stderr notice under CUDA). - Memory budgeting (kv_pool_bytes) accounts the quantized row widths, so cap_for_ram/PIN recover the KV savings for the expert tiers. - Tests: test_kv_fp8 (exhaustive e4m3 roundtrip + RNE), test_kv_tq (rotation orthogonality, roundtrip, distortion, inert rows), test_kv_disk (v1->v2/v3 upgrade + reject + self-heal), test_kv_alloc (f32<->KV8 transitions). Tiny-oracle validation (tools/make_glm_oracle.py, seed 1234): teacher-forcing f32 32/32 (exact), KV8 30/32, TQ6 30/32, TQ4 23/32 — flips are identical across CPU-x86/CPU-ARM (and the CUDA/Metal implementations of the follow-ups), i.e. deterministic quantization loss, not backend divergence. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Opt-in (PROF=1) instrumentation for expert-weight disk I/O rates, previously reported only as seconds: - pin_load: '[PROF] pin load: X GB read in Ys = Z GB/s aggregate | N experts @ M experts/s' — the initial hot-expert load rate off disk at startup. - prof_report: '[PROF] disk stream: E experts/s | A GB/s aggregate over the phase (Cx avg read concurrency, P GB/s per loader thread)' — the live streaming rate during prefill/decode. Additive only; with PROF unset every mode's output stays byte-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Two no-silent-error gaps in the KV_TQ env handling, both found in review. 1. Power-of-two guard. Both codecs rotate through a radix-2 FWHT, and coli_kvq_quant_row returns an inert radius 0 for any other width. On a model whose kv_lora/qk_rope are not powers of two, that meant EVERY latent row quantized to zero and the engine generated confident garbage with no diagnostic -- the same silent-misread class the .coli_kv tier magic exists to prevent, just reached through model shape instead of file format. Now checked once after model_init and refused with the shapes named and KV8 (no width constraint) suggested. GLM-5.2 is 512/64, so nothing that works today changes; this only fires where the codec cannot represent the model. test_kv_tq pins the underlying behavior for all four entry points (both codecs, both dispatch paths) so the guard cannot become quietly harmless. 2. KV_TQ=1 clamped UP to 2, handing "just turn it on" the most aggressive, lowest-quality tier. It now lands on the recommended 4-bit tier and says so. >6 still clamps down to the grid. Unchanged: default (no KV env) is 32/32 token-exact, KV8 30/32, KV_TQ=4 23/32 on the tiny oracle -- identical to the pre-change numbers.
b98cf47 to
3678834
Compare
|
Thanks for reproducing it end to end — and for naming a decision rule that works either way. The real-model number is being worked; one question about the container at the bottom. Meanwhile, rebased onto current 1. GLM-5.2 is 512/64, so nothing that works today changes. 2. Post-rebase verificationRe-ran everything after the rebase rather than assuming it carried:
Same numbers you reproduced, unchanged by the fixes, and still bit-identical across the two ISAs. CI 10/10 green on On the real-model quality datapointAgreed on the reasoning — One thing before I burn the GPU-hours, so I measure the right thing: which container do you mean by gs64? The one I have locally reports If you point me at the exact container (or confirm "convert with current |
The CPU half of #399, split out as requested — the part you can verify end-to-end against the oracle. 1,282 lines / 12 files (vs #399's ~3,400 / 20): no backend files, no new kernels. Once the format and KV plumbing land here, the CUDA and Metal kernels become small focused follow-ups, each gated by a hardware owner's token-identity check.
What's in it
KV8=1— latent KV in fp8 e4m3 + per-row f32 scale (~3.9× less KV RAM). The consumer LUT-decodes inline in the score/context dots; the per-row scale factors out of the sums.KV_TQ=4— rotated-int4 codec (randomized-Hadamard rotation + Lloyd codebook; radius rides the per-row scale). The consumer uses the rotation's orthogonality (q·x̂ == rotate(q)·c): rotate the query once per head, dot the packed nibbles through the 16-level codebook, unrotate the accumulated context once — the cache is never reconstructed.KV_TQ=2|3|5|6/KV_TQ_POLAR=1use the PolarQuant codec..coli_kvacross mode changes — the file magic encodes the tier (COLIKV/COLIKV2/COLIKV3) andh[7]the TQ codec+bits; any mismatch is refused and restarted with an explicit message, never misread (e.g.[KV] .coli_kv is fp8 (saved under KV8=1): starting over). v1 files upgrade in place, old file untouched until the first save rewrites it. Covered bytests/test_kv_disk(upgrade, both reject directions, TQ-params reject, append-side self-heal).kv_pool_bytesaccounts the quantized row widths, socap_for_ram/PIN recover the KV savings for the expert tiers.!g_kv8&&!g_tqand quantized runs fall to the CPU consumer (one-time stderr notice under CUDA). A CUDA or Metal build with KV8/KV_TQ set is therefore safe today — just CPU-bound in attention until the kernel PRs.PROF=1disk-load throughput lines (pin-load GB/s + experts/s, live streaming rate). Byte-identical output withPROFunset.Validation (reproducible on your machine)
make test-cgreen (incl. the four new kv suites); builds clean withCUDA=1andMETAL=1(the guards compile against the existing backends — no new entry points referenced).Follow-ups queued behind this: CUDA kernels (fp8 shadow + absorb8/TQ native, hardware-validated on 4×5090+4090, sm_89+sm_120, resident-744B decode at f32 parity) and Metal kernels (fused-decode fp8/TQ + the two-library fast-math split), each with its perf + token-identity data attached.
🤖 Generated with Claude Code