Skip to content

CPU: KV cache quantization — KV8 (fp8 e4m3) + KV_TQ (rotated-int4 / PolarQuant)#553

Open
NeuralNotwerk wants to merge 2 commits into
JustVugg:devfrom
NeuralNotwerk:cpu-kv-quant
Open

CPU: KV cache quantization — KV8 (fp8 e4m3) + KV_TQ (rotated-int4 / PolarQuant)#553
NeuralNotwerk wants to merge 2 commits into
JustVugg:devfrom
NeuralNotwerk:cpu-kv-quant

Conversation

@NeuralNotwerk

Copy link
Copy Markdown
Contributor

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=1 use the PolarQuant codec.
  • .coli_kv across mode changes — the file magic encodes the tier (COLIKV/COLIKV2/COLIKV3) and h[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 by tests/test_kv_disk (upgrade, both reject directions, TQ-params reject, append-side self-heal).
  • Memory budgetingkv_pool_bytes accounts the quantized row widths, so cap_for_ram/PIN recover the KV savings for the expert tiers.
  • Guards on the existing GPU fast paths — the CUDA/Metal attention branches read f32 rows that aren't allocated under quantized KV, so they gain !g_kv8&&!g_tq and 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.
  • Second commit (small, optional to keep): PROF=1 disk-load throughput lines (pin-load GB/s + experts/s, live streaming rate). Byte-identical output with PROF unset.

Validation (reproducible on your machine)

pip install "transformers>=5.11" && python tools/make_glm_oracle.py   # byte-identical to committed ref_glm.json
SNAP=./glm_tiny TF=1 [KV8=1|KV_TQ=6|KV_TQ=4] ./colibri 64 16 16
KV mode TF (prefill) greedy gen (decode)
f32 32/32 20/20
KV8 30/32 14/20
TQ6 30/32 17/20
TQ4 23/32 10/20
  • f32 stays token-exact — the quantized paths are strictly additive.
  • The quantized flips are identical positions + identical tokens on CPU-x86 (AVX2) and CPU-ARM (NEON) — and on the CUDA/Metal implementations waiting in the follow-ups — i.e. deterministic quantization loss, not implementation divergence.
  • The tiny random-weight model is a worst case (near-zero logit margins); on the real 744B, TQ4 decodes coherently end-to-end. KV8/TQ6 lose 2/32 even here; TQ4 is the aggressive tier, TQ6 the quality-parity choice.
  • make test-c green (incl. the four new kv suites); builds clean with CUDA=1 and METAL=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

@NeuralNotwerk

Copy link
Copy Markdown
Contributor Author

CPU performance validation for this branch, measured on its exact tree (744B, fully resident — PIN=auto PIN_GB=all RAM_GB=490, 32-thread AVX2, greedy templated prompt, NGEN=80, 100% expert hit so KV precision is the only variable):

decode tok/s MTP off DRAFT=1 DRAFT=2
f32 3.02 2.71 2.25
KV8 2.96 2.72 2.18
TQ4 2.90 2.73 2.11

Prefill ~5.5–5.6 tok/s across all cells. Two takeaways:

  • The quantized consumers hold decode parity with f32 in every MTP mode (≤4% spread, within run noise; TQ4 actually edges f32 under DRAFT=1). Resident CPU decode is expert-matmul-bound, so the KV consumers' cost is hidden — the tiers buy their memory savings for free.
  • The f32 column is byte-identical behavior to dev (this branch doesn't touch the f32 path — the token-exact oracle CI confirms), and its numbers match our pre-rebase baseline within 1%, so [Performance]: hot-path float reductions are scalar and don't auto-vectorize (router matmul + MLA-absorb attention) #442's SIMD absorb dots carry over intact.

Environment: EPYC-class 32C host, ZFS storage, dockerized gcc 13 build, colibri 64 4 4.

@JustVugg

Copy link
Copy Markdown
Owner

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 dev — several PRs landed today in the KV/attention area. Please rebase onto current dev.

Two things to confirm on the rebase, given the 'no silent errors' bar here: (1) the producer-loop parallelization in attention_rows (#pragma omp parallel for … if(S>8)) is on ALL paths including default f32 — the oracle passing says it's bit-identical, but please keep that schedule(static) deterministic through the rebase; (2) it's lossy quant with round-trip tests but no automated accuracy gate — that's inherent and fine as opt-in, just noting it's why it stays behind a flag. Rebase and I'll merge.

NeuralNotwerk and others added 2 commits July 23, 2026 15:25
…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>
@NeuralNotwerk

Copy link
Copy Markdown
Contributor Author

Rebased onto current dev (5724dae). One trivial conflict (TEST_BINS union with the new test_router_nan); every code hunk applied clean.

On your two points:

  1. Producer-loop determinism — preserved verbatim: #pragma omp parallel for schedule(static) if(S>8) on the KV producer, all paths including default f32. Each s writes only its own row (no shared state; the CUDA shadow-shorten is hoisted out of the loop), and schedule(static) keeps the row→thread mapping fixed, so f32 stays bit-identical — the token-exact oracle in CI re-verifies this on the rebased tree.
  2. Lossy-by-design, opt-in — agreed; that's the contract. The quality bounds we ship are the tiny-oracle tables above (KV8/TQ6 30/32, TQ4 23/32, deterministic and backend-identical) plus the round-trip/distortion unit tests. A model-level perplexity gate would need a real-weights fixture in CI — happy to discuss as a follow-up if you want one.

Also verified through the rebase: your new COLI_DSA_GATHER path composes correctly with the quant guards — it stages f32 rows, and under KV8/KV_TQ the enclosing branch is skipped (byte caches, no f32 rows), so quantized runs keep falling to the CPU consumer.

Post-rebase validation: CPU + METAL=1 builds clean, make test-c green (incl. the four KV suites), teacher-forcing/generation tables unchanged (f32 32/32 & 20/20; KV8 30/32; TQ6 30/32; TQ4 23/32 — identical positions on both builds). The CPU perf table posted above was measured on the pre-rebase tree; the rebase changes none of the measured CPU code paths (the dev-side deltas are the CUDA gather + allocator hardening), so the parity numbers stand.

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.

2 participants