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 4 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. |
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 |
5059f5d to
e6252ca
Compare
…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.
…e_row: refuse negative scale A power-of-two width >512 (e.g. qk_rope=1024) passed the pow2 gate but overflows the 512-wide qtl/qtr and Lf/Rf stack staging buffers in the attention consumers -- same refuse-don't-corrupt rule, checked once at startup. And kv8_sanitize_row accepted a corrupt *negative* row scale, which sign-flips the whole row on load; the encoder only emits scales > 0, so anything else is now an inert row, matching the TQ twin. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
e6252ca to
62b1004
Compare
|
Rebase request — and an apology for how many of these you have had.
Before asking, three things changed on our side so that this is the last one of these you should need for a while. 1. The main source of these conflicts is closed. Gates are now derived from the build rules (#733). Adding a test means adding your 2. We resolved what we could ourselves instead of asking. Eight PRs were unblocked by a maintainer pushing the merge to the contributor's branch rather than requesting a rebase — including three where both sides carried a real change and had to be merged rather than picked. No commits were rewritten. Yours is here because its conflict is in engine code ( 3. Merge order is oldest-clean-first from now on. A PR that is green and unconflicted merges ahead of anything opened after it. The reason some of you rebased many times is that newer, smaller PRs kept jumping the queue and resetting you — a starvation loop we built, not bad luck on your side. What we need: one rebase onto current If you would rather not, say so and we will close it with thanks and the branch stays yours to reopen. No pressure either way, and no hard feelings — several of these have been open a while through no fault of the author. |
|
All good, I can have an agent rebase and try again if this kind of feature hasn't made it in yet. I just moved on to slightly different projects with smaller models and this hasn't been in front of me much. |
|
Reviving this: KV quantization just moved near the top of our priority list, and this PR is months ahead of where we thought we were. Context: #768 measured our fp32 KV at 3.2x llama.cpp's cost on the same checkpoint (0.316 vs 0.098 MB/token on GLM-5.2), forcing a choice between long context and expert residency — 5.46 tok/s at ctx 32k vs 0.86 at 262k. Your KV8 addresses exactly that, and the KV_TQ rotated-int4 approach is the same outlier-spreading idea llama.cpp recently added for its quantized KV (a Walsh–Hadamard rotation before storage) — you were there first on our side. Would you rebase onto current |
|
Offering to carry the rebase, unless either of you would rather keep it. Context for why now: in #1140 the maintainer ranked this PR as the single highest-value unblock for the attention-bandwidth wall on resident hosts (fp32 KV is bandwidth the S=1 MLA kernel has to move, every token), and described it as stalled on time rather than on any technical doubt — and @NeuralNotwerk, you mentioned having moved on to other projects, so this looks like exactly the kind of hand-off that keeps good work from rotting. What I'd do, following the carve-up already suggested in this thread:
One reason I'm a reasonable person to do this specifically: the maintainer flagged that three of the eleven conflicts sit inside Say go and I'll start with KV8; say no and I'll stay out of the way. |
|
@ZacharyZcR for me ok is important to merge this in colibri |
|
Rebase delivered: #1147. All three of your commits with authorship preserved, @NeuralNotwerk; the PROF disk-metrics commit is dropped (dev's PROF accounting was rebuilt since — it can return separately). The four semantic resolutions are itemized in the PR body for your veto, including the one new overlap that appeared since the maintainer's count: dev's #1142 tiled kvb reconstruction now shares attention_rows with your dequant staging — resolved by keeping your one-shot design under quantized KV and gating the tiled arm to f32 KV, so neither side's semantics moved. Default path verified byte-identical (tiny TF oracle 32/32 with the flags unset); KV8/KV_TQ opt-in behavior and all four kv suites pass as in your original. |
CPU KV cache quantization: KV8 (fp8 e4m3) + KV_TQ — #553 rebased onto dev
The JustVugg#1151 rebase gated the tiled (flash) and DSA-gather reconstruction arms off under quantized KV, keeping JustVugg#553's one-shot + staging design untouched — correct for the merge, but it meant long context and KV quantization could not compose: the 30 GB kvb transient is independent of KV width, so a KV8 run at ctx 262144 still paid the one-shot buffer. Both arms now dequantize the latent rows they need through the same per-row codecs the one-shot path uses: the flash arm stages one tile's worth of Lc rows (tile-sized f32 buffer, ~8 MB at the default tile) before its kv_b matmul, the gather arm dequantizes exactly the union rows it rebuilds, and the roped-key reads inline the one-shot path's three-way branch so each representation keeps its exact accumulation order. The f32 paths are byte-identical to before — the branches sit on g_kv8/g_tq only. Verified on glm_tiny TF oracle: f32 default 32/32 (unchanged); KV8 and KV_TQ under KVB_FLASH=1 score 30/32 and 23/32 — identical to their one-shot scores — in both single-tile and (row floor lowered locally) 8-tile runs; make test-c green, zero-warning build.
|
Closing this because the work is in @ZacharyZcR carried the rebase in #1151 with all three of your commits intact and authorship preserved through it, listing every conflict resolution for your veto rather than quietly deciding them. Two follow-ups have since built on that base: #1155 taught the flash and gather kvb arms about KV8/KV_TQ, and #1207 added grouped latent scales ( That is also why this PR now shows as conflicting: it collides with its own contents. Thank you for it. KV quantization on the CPU path was the single highest-value unblock on the list — fp32 KV is bandwidth the S=1 MLA kernel has to move for every token, and it is now the base other people are extending rather than a branch waiting its turn. Five weeks between opening and landing is longer than it should have been, and that is on our sequencing, not on the work. |
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