KVarN variance-normalized KV-cache quantization: CPU + CUDA (#180) - #434
Conversation
…c RTN K4V2 (#180) Clean-room implementation of the KVarN per-tile pipeline (arXiv:2606.03458) as a sibling quantizer inside SharpInference.TurboQuant: - Hadamard rotation (reuses WalshHadamard) on K and V channel dims; V stays in the rotated domain through aggregation with one deferred inverse WHT per head (UnrotateOutput). - Log-space dual-axis Sinkhorn variance normalization (columns first, rows last; RMS-about-zero so the asymmetric zero-point absorbs the mean). - Asymmetric RTN: 4-bit keys per-channel (Sinkhorn channel factor folded into the affine), 2-bit values per-token per-128-channel group. - Channel-major K codes for sequential score walks (query-side fold of chanStep + chanMin bias once per tile); token-major V codes. - 2-bit pack/unpack added to BitPacking (masked RMW, dirty-buffer safe). Measured on Gaussian tiles: K4 round-trip relFrob ~0.099, V2 ~0.50 (at the 2-bit min/max RTN floor); score-error vs TurboQuant 4-bit Lloyd-Max ratio 0.980 (Gaussian) / 1.047 (planted outliers). Fused-kernel parity vs decompress-reference <= 3.3e-5. 25 new tests. Review round: RotateQuery span-length guards, split ArgumentOutOfRange paramName for tokens/dim, corrected V-aggregate error comment (model-level gate decides 2-bit acceptability, not unit tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EWo1niax1g8A5E79RSALJ
…-mode flag (#180) Wires KVarNCompressor into the existing TurboQuant machinery as a selectable quantizer (research doc §6.3 — one cache type, two codecs): - TurboQuantKvCache: TqQuantizer enum (LloydMax | KVarN). KVarN mode uses the FP32 window itself as staging (window >= 128 enforced): promotion compresses the oldest 128 window rows into one whole tile when the window overflows, so TQ length grows in 128-steps and staging count is always 0. Lloyd-Max path behaviorally unchanged. - TqAttention: mode-dispatching RotateQuery; KVarN K-scores fold rowScale and attention scale into real q·k units so one softmax spans compressed + FP32-window segments; V aggregates in the rotated domain with one UnrotateOutput per head, window contribution added un-rotated. - SnapKV eviction deferred: rejected at EnableTurboQuant and the CLI, Compact throws in KVarN mode as defense-in-depth (#180 follow-up). - CLI: --tq-mode <lloydmax|kvarn> (kvarn requires --tq and -g 0; head-dim gate relaxed to pow2 in [8,1024] for kvarn). README row added. - 13 new tests: promotion boundaries (win 128/192, multi-tile), GQA, wiring parity vs decompressed reference ~2e-6, end-to-end vs FP32 truth 0.39-0.57 rel-L2 (the 2-bit V codec floor — model-level gate decides acceptability), Reset reuse, TruncateTo, config rejections, Qwen3-8B prefill/decode smoke (model-gated). Review round: documented the reduced KVarN rewind depth (window − 127) on EnableTurboQuant/TruncateTo for future speculation composition; corrected the stale "ring buffer" comment (window is a linear shift-down buffer). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EWo1niax1g8A5E79RSALJ
…180) Adds the P0 go/no-go measurement harness and records the gate result: - New `perplexity` subcommand: teacher-forced token-by-token NLL on the CPU forward pass (never Prefill, so every step reads through the TQ decode path), full-vocab log-softmax with double accumulation, position-bucket breakdown keyed off --tq-window so compressed-region degradation is visible. Same --tq/--tq-mode validation as run. - scripts/kvarn-gate/: wikitext-2-raw fetcher + 20-problem greedy math micro-eval (640-token prepend forces reasoning over a compressed cache; invalid CLI invocations abort instead of scoring as failures). - 9 model-free CLI tests (flag validation, NLL math properties). Gate results (window 256, wiki.test.raw): - Qwen3-0.6B c=3072: PPL fp32 15.47 / kvarn 15.67 (+1.26%) / lloydmax 945.6 (collapse; verified pre-existing on master, not a regression). - Qwen3-0.6B c=8192 (review probe): kvarn +1.90% aggregate; per-token penalty FLATTENS at ~+2.2% ppl-equiv in deep buckets, no acceleration. kvarn decoded faster than fp32 at depth (19.1 vs 17.4 tok/s, scalar). - Qwen3-8B c=1024: +0.47%. - Math eval: fp32 18/20, kvarn 18/20 (coherent, no loops), lloydmax 0/20. - FP32-window parity: [1,256) bucket bit-identical across all configs. P0 verdict per docs/kvarn-feasibility-research.md §6: 2-bit V accuracy holds on models we serve -> proceed to P1 (AVX2) and P2 (CUDA) kernels. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EWo1niax1g8A5E79RSALJ
…epth (#180) P1 from the research doc: vectorize the two hot KVarN decode kernels, dispatching on Avx2+Fma with the Task-1 scalar bodies as the untouched fallback (internal ForceScalar hook for parity tests and benchmarks): - KeyScoresAvx2: channel-major walk kept; per channel, one 16-byte load = 32 nibbles, psrlw+mask nibble split, punpcklbw/hbw restore token order, vpmovzxbd+cvtdq2ps widen, 4 FMAs vs the broadcast folded query weight into 16 YMM score accumulators. All Vector128 shuffles — no cross-lane unpack hazard by construction. - AggregateValuesAvx2: token-major walk kept; per 16 code bytes = 64 channels, 4 crumb planes (shift 0/2/4/6 + 0x03 mask), two-level byte->word interleave restores channel order, FMA vs broadcast w·tokStep. d%64!=0 dims fall back to scalar (all shipping head dims vectorize); skip semantics (qw==0/w==0/ws==0) identical to scalar. Measured (Core Ultra 9 185H): KeyScores 8312->861 ns/tile (9.7x), AggregateValues 8732->1039 ns/tile (8.4x), zero managed allocs. Parity vs scalar worst 2.6e-6 rel (12 new tests + D=512 case). End-to-end Qwen3-0.6B wikitext c=3072: PPL 15.6741 (+0.036% vs scalar — FP reassociation + FMA contraction only), 52.6 tok/s vs FP32 33.5 (1.57x); c=8192: PPL 21.1409 (+0.004% vs scalar), 38.3 tok/s vs FP32 17.4 (2.20x). The paper's "throughput >= FP16" claim now holds on CPU. Review round: xunit.runner.json added to Tests.TurboQuant (ForceScalar is a bare static; disable collection parallelism like sibling test projects), FMA-contraction wording, D=512 parity coverage. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EWo1niax1g8A5E79RSALJ
…P2 Task 5a) Clones the CUDA Lloyd-Max TurboQuant seam for KVarN, correctness-first: - Three NVRTC kernels in CudaTextKernels: llm_kvarn_rotate_query (WHT, no sign flip), llm_kvarn_compress_tile (gather 128 oldest window rows -> per-row WHT of K and V -> dual-axis Sinkhorn (log-space, columns first) -> asymmetric RTN K4/V2 pack; working tile in global scratch since a 128x128 f32 tile exceeds the 48 KB shared ceiling; amortized once per 128 decode steps), llm_kvarn_attention (llm_tq_attention's 3-phase hybrid: folded-query K-tile scores + fp32-window scores under one softmax, V accumulated in the rotated domain, one deferred inverse WHT per head, >4096-score global spill). - Packed codes are BYTE-IDENTICAL to the CPU KVarNCompressor (__fadd_rn/__fmul_rn block FMA contraction feeding codes, __float2int_rn ties-to-even, sequential reduction order matches CPU); stored scales within 1e-5 rel (device logf/expf ulp). Tiles download- verifiable with the CPU decompressor — asserted in tests. - CudaForwardPass: tqQuantizer ctor param; linear shift-down window mirroring the CPU cache position-for-position (promotion at ring-full, scratch-bounced D2D shifts, same-stream ordering); TruncateTo resync; EstimateMaxContextKvarn wired into the auto -c solve; all TQ exclusivity guards inherited (SnapKV, narrowed KV, batching, MoE, graphs bail). Head dim >256 stays CPU-only (shared WHT cap). - CLI: --tq-mode kvarn now allowed with -g -1 full CUDA offload of dense models; perplexity command gains -g -1. CPU-fallback path now forwards the quantizer (latent P0 bug: fallback silently ran Lloyd-Max). - 12 new CUDA parity tests (TryCreate/skip): byte-exact compress, rotate 1e-4, attention vs CPU oracle per-dim max(1e-3,1e-2|x|) cosine>0.999 incl. GQA + spill path, promotion cadence, guard rejections. Measured (RTX 4070 Laptop 8 GB): - Qwen3-0.6B c=3072: CUDA kvarn PPL 15.6266 (CPU 15.6684), fp32 15.4711; 59.9 tok/s vs fp32 90.2. - Qwen3-8B c=4096: kvarn PPL 8.9904 vs fp32 8.9032 (+0.98%), 22.8 vs 27.6 tok/s (-17.5%); [1,256) bucket identical to fp32 by design. - Capacity: auto-context on Qwen3-8B at -g -1 grows 2376 -> 18304 (7.7x vs fp32 KV; ~8.5x/token in the compressed region) at ~6.1 GiB peak — beats the research doc's ~4x promise. Review round: no blockers; corrected the bytes-per-token doc comment to compare K+V against K+V. 5b targets: CUDA-graph re-entry (gap is launch- overhead-dominated: 34% on 0.6B -> 17.5% on 8B), phase-1a coalescing, packed prefill. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EWo1niax1g8A5E79RSALJ
…beats fp32 (#180 P2 Task 5b) Closes the CUDA decode gap where it matters. Two mechanisms: 1. Graph re-entry. Whole-tile promotion is hoisted out of RunDeviceRegion into Forward (direct launches once per 128 tokens, byte-exact reordering — promotion touches only its own layer's window/tiles, all one stream). The region then has two static topologies keyed on tqLen==0: pre-promotion (append + plain fp32 attention) and steady-state (append + rotate + kvarn attention), captured once each into per-id multi-graphs (ids -101/-102). Three tracked params (KvarnAppendSlot/KvarnFp32SeqLen/KvarnTqLen) published per replay; capture-scoped GraphKvarnTracking redirects KvAppend/Attention arg registration; shared-vs-scratch stays an in-kernel branch on tracked scalars. ResetCache/TruncateTo need no invalidation. Instance latch prevents replaying a disposed sibling's baked pointers. 2. Kernel rewrites (the actual bottleneck at depth): phase-1a K-scores warp-per-channel (a channel's 64-byte code run = 2 coalesced 32B sectors per warp) and phase-3a V-aggregate flat-index walks (256 consecutive bytes per block pass, per-tile w-folds in shared). Compress kernel untouched — byte-exactness preserved. Measured (RTX 4070 Laptop 8 GB, greedy CLI, warm): - Qwen3-8B: kvarn >= fp32 everywhere — shallow 38.0 vs 37.2, deep-4k 31.9 vs 28.2 (+13%). The paper's "throughput >= FP16" claim now holds on CUDA for the model class the 7.7x-capacity win targets. - Qwen3-0.6B: shallow parity (190-196 t/s), deep 17.4 -> 35.4 (2.0x) but fp32's split-KV flash still wins at depth (92.9) — the kvarn attention grid (numHeads=16 blocks) underfills 36 SMs; split-tile+LSE redesign documented as follow-up. - KVarN prefill 2x via graph replay (0.6B 30.6->66, 8B 25.8->35 t/s); chunked batched prefill remains the big prefill lever. - PPL gate: 0.6B 15.6004, 8B 8.9887 (within reassociation noise of 5a; pre-promotion bucket bit-identical to fp32). New graph-vs-direct parity test: 400 tokens across two promotions, max |dlogit| = 0.0. Review round: no blockers; parity test now hard-fails when graphs fail to engage on a capture-capable device (was a silent soft-skip); GraphMaxKernelArgs 16 -> 20 with the widest-op note corrected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EWo1niax1g8A5E79RSALJ
…98 s (#180 Task 6) Replaces the per-token KVarN prefill loop with 128-token batched chunks through the existing GEMM trunk, plus the depth measurements that settle the deferred split-tile decision. - New llm_kvarn_prefill_attention: one streaming (online) softmax per query spanning BOTH regions — tile K-scores (rotated folded-q over channel-major nibbles) then causal fp32-window dots (unrotated q), shared running max/sum; two V partials (tile in rotated domain, window in original), inverse WHT applied once to the tile partial at the end (sound by WHT linearity). 16 queries/block, no score storage -> no 4096-position cap. Sub-warp __shfl_*_sync UB on odd-tail chunks found and fixed during hardening (all reductions now full-mask uniform). - PrefillKvarnChunked driver: promote-on-window-full before appending position W+128k (tile k covers [128k,128k+128) for ANY window size — same invariant as decode's 5b hoist, so tiles are schedule-independent: re-chunking the same prompt yields 0 differing tile bytes over 10.3 MB, with a promotion inside a split PrefillWithCache call). Pre-promotion chunks ride the existing flash path. Decode graphs untouched. - Fallbacks: SHARPI_KVARN_BATCHED_PREFILL=0 or any unsupported shape (head_dim 256, MoE, SWA, attn-bias, ...) -> per-token loop, never the non-KVarN trunk. 6 new tests incl. CPU-oracle parity (cosine 1.000000, 33-tile deep walk), logit parity (argmax-equal), greedy continuation. Measured (RTX 4070 Laptop 8 GB): - Prefill: 8B@4k 35.3->460 t/s (13.1x), 8B@15.5k 26.4->203 (7.7x), 0.6B@4k 145->1490, 0.6B@15.5k 55->583. Decode unchanged. - Headline: 8B, 15.5k-token prompt + 300-token decode = 98 s wall-clock (was ~610 s). PPL gates unchanged (0.6B 15.6004, 8B 8.9887). - Depth curve (settles the split-tile deferral): 8B kvarn decode 30.2 -> 25.3 -> 20.0 t/s at 4k/8k/15.5k — ~0.9 t/s per 1k depth from the serial tile walk; interactive at max context where fp32 cannot run at all. Split-tile decode + int8-TC K-score prefill GEMM (203->~700 t/s class) documented as follow-ups. Review round: no blockers, no should-fixes; kernel-header plane-count comment corrected (four MQ x 128 planes). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018EWo1niax1g8A5E79RSALJ
There was a problem hiding this comment.
Code Review
This pull request implements the KVarN KV-cache quantizer (4-bit keys / 2-bit values) as an alternative to Lloyd-Max codebooks, adding CPU and CUDA support (including CUDA graph capture/replay and chunked prefill), a CLI perplexity evaluation command, and comprehensive tests. Feedback is provided regarding a critical bug in the CUDA prefill attention kernel (llm_kvarn_prefill_attention) where uninitialized shared memory s_r can contain garbage values (such as NaN or Infinity) and poison the accumulator s_racc during the first tile loop iteration; initializing s_r to 1.0f is recommended to prevent this.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| int i16 = tid >> 4; | ||
| int lane16 = tid & 15; | ||
|
|
||
| if (tid < KPF_MQ) { s_m[tid] = sharpi_neg_inf(); s_l[tid] = 0.0f; } |
There was a problem hiding this comment.
In the first iteration of the tile loop (tile = 0), s_racc[idx] *= s_r[idx / head_dim] is executed. At this point, s_racc is 0.0f, but s_r is uninitialized and contains garbage values from shared memory. If s_r contains NaN or Infinity, the multiplication 0.0f * NaN or 0.0f * Infinity will result in NaN, which will poison s_racc and propagate to the output. To prevent this, initialize s_r to 1.0f at the start of the kernel.
if (tid < KPF_MQ) { s_m[tid] = sharpi_neg_inf(); s_l[tid] = 0.0f; s_r[tid] = 1.0f; }
Implements KVarN (arXiv:2606.03458) end-to-end per the phased plan in
docs/kvarn-feasibility-research.mdand #180: calibration-free 4-bit-key / 2-bit-value KV-cache quantization as a selectable quantizer inside the existing TurboQuant machinery, on the CPU (scalar + AVX2) and CUDA paths. Clean-room from the paper — no code from the reference vLLM fork.Commits (each implemented + adversarially reviewed before landing)
82efc4fCore quantizer — Hadamard rotation (reusesWalshHadamard, K and V), log-space dual-axis Sinkhorn variance normalization, asymmetric RTN K4/V2 in 128-token tiles (channel-major K / token-major V packing), 2-bitBitPacking.6040076Cache + attention integration —TqQuantizerenum inTurboQuantKvCache(KVarN mode: the FP32 window is the staging; whole-tile promotion),TqAttentiondispatch, CLI--tq-mode <lloydmax|kvarn>.f95fb3eAccuracy gate — newperplexityCLI command (teacher-forced NLL, position buckets) +scripts/kvarn-gate/(wikitext fetcher, 20-problem math eval). P0 gate: strong pass.44a5e1aP1 AVX2 — fused KeyScores/AggregateValues (9.7×/8.4× per-kernel), scalar fallback +ForceScalarparity hook.0d7cd28P2 CUDA correctness —llm_kvarn_rotate_query/llm_kvarn_compress_tile/llm_kvarn_attention; packed codes byte-identical to the CPU compressor;EstimateMaxContextKvarn;--tq-mode kvarn -g -1;perplexity -g -1.e43f3f0P2 CUDA decode perf — CUDA-graph re-entry (two topologies, tracked params, promotion hoisted), warp-per-channel / flat-index attention walks.99c3773Chunked batched prefill —llm_kvarn_prefill_attention(single streaming softmax over compressed tiles + FP32 window, deferred inverse WHT), schedule-independent tiles (re-chunking is byte-exact).Measured (RTX 4070 Laptop 8 GB · Qwen3-0.6B-Q8_0 / Qwen3-8B-Q4_K_M · wikitext-2)
Gates that must keep holding:
perplexity(0.6B c=3072 kvarn ≈ 15.60, 8B c=4096 ≈ 8.99), compress byte-exactness tests, graph-vs-direct decode parity (bit-identical over promotion boundaries).Scope notes
--tqalone). Related: the gate found Lloyd-Max 3-bit collapses on Qwen3 — TurboQuant Lloyd-Max 3-bit collapses on Qwen3 (PPL 61x, degenerate decode) — pre-existing on master #432.Closes #180 P0–P2 (P3 Vulkan optional, tracked in #433).
Tests: 32 KVarN (ForwardPass) + 7 graph + 78 TurboQuant + 80 CLI green; zero-warning Release build (TreatWarningsAsErrors, AOT/trim analyzers on).
🤖 Generated with Claude Code
https://claude.ai/code/session_018EWo1niax1g8A5E79RSALJ