fix(tq): default --tq-mode to auto — KVarN where supported, Lloyd-Max fallback with quality warning (#432) - #436
Conversation
… fallback with quality warning (#432) Lloyd-Max 3-bit collapses on QK-norm models: Qwen3-0.6B wikitext-2 PPL 945.6 vs 15.47 fp32, degenerate decode. Root cause is intrinsic codec distortion (~7% relative K-score error) meeting Qwen3's large attention logits — not a read-path bug (FastScan tracks the per-block reference to ~1%, and the sign-flip-after-WHT ordering is a distributional no-op on real K/V). 4-bit Lloyd-Max measures PPL 51.8 — better, still unusable. KVarN measures 15.67, so it becomes the default wherever supported. - CLI (run + perplexity): --tq-mode defaults to auto → KVarN on CPU (any pow-2 head dim) and full-CUDA-offload dense (head dim ≤ 256); falls back to Lloyd-Max with a loud #432 quality warning on Vulkan, partial offload, MoE-on-GPU, SnapKV, or window < 128. Explicit lloydmax keeps the old behavior silently; explicit kvarn still errors on unsupported combos. - Server: new TqMode option (default auto) resolved per-path in InferenceEngineLoader; SHARPI_TQ / SHARPI_TQ_MODE env vars in Host. - Docs: flag descriptions, CLI README, quality warnings on TqQuantizer.LloydMax / ForwardPass.EnableTurboQuant XML docs. Validated on the issue's gate: default --tq now runs tq-kvarn-k4v2 at PPL 15.6741 (c=3072, window=256); SnapKV fallback prints the warning; CUDA -g -1 resolves to KVarN K4V2. The GPU Lloyd-Max V-basis defect found during the audit is tracked separately as #435. Closes #432 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EuBdnPNJ2XdaFp9PjYoJyz
There was a problem hiding this comment.
Code Review
This pull request introduces an "auto" mode for the TurboQuant KV cache compression quantizer, which automatically selects the higher-quality KVarN quantizer where supported and falls back to Lloyd-Max with a warning on unsupported configurations. It also adds environment variable support for these options in the server host and updates the CLI commands, documentation, and tests. The review feedback highlights two important issues in the server's InferenceEngineLoader: first, an early head-dimension check unconditionally blocks KVarN for head dimensions other than 128 or 256; second, the CUDA path lacks a check to fall back to Lloyd-Max when the head dimension exceeds 256.
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.
| if (turboQuant && hp.HeadDim is not 128 and not 256) | ||
| throw new InvalidOperationException( | ||
| $"TurboQuant requires head dimension 128 or 256; this model has head dim {hp.HeadDim}."); | ||
| bool tqModeIsAuto = false; | ||
| TqQuantizer tqQuantizer; | ||
| switch ((opts.TqMode ?? "auto").Trim().ToLowerInvariant()) | ||
| { | ||
| case "" or "auto": | ||
| tqModeIsAuto = true; | ||
| tqQuantizer = TqQuantizer.LloydMax; // resolved per-path in BuildForwardPass | ||
| break; | ||
| case "lloydmax" or "lloyd-max": | ||
| tqQuantizer = TqQuantizer.LloydMax; | ||
| break; | ||
| case "kvarn": | ||
| tqQuantizer = TqQuantizer.KVarN; | ||
| break; | ||
| default: | ||
| throw new InvalidOperationException( | ||
| $"Unknown TqMode '{opts.TqMode}'. Expected one of: auto, lloydmax, kvarn."); | ||
| } |
There was a problem hiding this comment.
The current head dimension check is executed before tqQuantizer and tqModeIsAuto are resolved, and it unconditionally rejects any head dimension other than 128 or 256 when turboQuant is enabled. This prevents loading models with other power-of-2 head dimensions (such as 64, 512, or 1024) when using KVarN on the server, even though KVarN supports them. Moving the check after the switch statement and validating based on the resolved quantizer fixes this issue.
bool tqModeIsAuto = false;
TqQuantizer tqQuantizer;
switch ((opts.TqMode ?? "auto").Trim().ToLowerInvariant())
{
case "" or "auto":
tqModeIsAuto = true;
tqQuantizer = TqQuantizer.LloydMax; // resolved per-path in BuildForwardPass
break;
case "lloydmax" or "lloyd-max":
tqQuantizer = TqQuantizer.LloydMax;
break;
case "kvarn":
tqQuantizer = TqQuantizer.KVarN;
break;
default:
throw new InvalidOperationException(
$"Unknown TqMode '{opts.TqMode}'. Expected one of: auto, lloydmax, kvarn.");
}
if (turboQuant)
{
bool isKVarN = tqModeIsAuto || tqQuantizer == TqQuantizer.KVarN;
if (isKVarN)
{
bool headDimOk = hp.HeadDim >= 8 && hp.HeadDim <= 1024 && (hp.HeadDim & (hp.HeadDim - 1)) == 0;
if (!headDimOk)
throw new InvalidOperationException(
$"TurboQuant KVarN requires a power-of-2 head dimension in [8, 1024]; this model has head dim {hp.HeadDim}.");
}
else if (hp.HeadDim is not 128 and not 256)
{
throw new InvalidOperationException(
$"TurboQuant Lloyd-Max requires head dimension 128 or 256; this model has head dim {hp.HeadDim}.");
}
}| // per-sequence-eviction decode). An explicit SHARPI_SNAPKV_BUDGET>0 still wins and | ||
| // composes with batching via #196 Option 1. | ||
| var cfwd = new CudaForwardPass(model, cuda, hp, ctxSize, enableTurboQuant: turboQuant, | ||
| tqQuantizer: ResolveTq(hp.IsMoE ? "KVarN on CUDA supports dense models only" : null), |
There was a problem hiding this comment.
When resolving the quantizer for CudaForwardPass, we should check if the head dimension exceeds 256 on CUDA. If it does, we should block KVarN and fall back to Lloyd-Max (with a warning in auto mode) instead of potentially failing to load the model later.
tqQuantizer: ResolveTq(hp.IsMoE ? "KVarN on CUDA supports dense models only" : hp.HeadDim > 256 ? $"KVarN on CUDA requires head dim ≤ 256; this model has {hp.HeadDim}" : null),…/cleanup from review Code review of the #432 fix found a new crash path: a dense model with a pow-2 head dim outside {128,256} (e.g. 64) run with --tq -g -1 on a GPU too small for full offload resolves auto→KVarN (passes the pow-2 gate), skips the Lloyd-Max 128/256 gate, then the partial-offload branch downgraded to Lloyd-Max without re-checking — constructing the forward pass with a head dim Lloyd-Max has no codebook for, throwing an uncaught NotSupportedException instead of the clean CLI error explicit --tq-mode lloydmax produces. - RunCommand: before the partial-offload KVarN→Lloyd-Max downgrade, reject head dims outside {128,256} with an actionable error pointing at -g 0 (CPU KVarN supports them). - RunCommand: drop the dead KVarN branch in the Vulkan CPU-fallback message (KVarN can never reach the Vulkan backend). - CLAUDE.md: TurboQuant now describes both codecs and the KVarN default. Tests.Cli 93/93; full solution builds clean.
Code review (high effort, 8 finder angles + verify)Ran an 8-angle multi-agent review (line-by-line, removed-behavior, cross-file, reuse, simplification, efficiency, altitude, conventions). One real regression this PR introduced, fixed in 80c1445; the rest are pre-existing or tech-debt, filed as follow-ups. Fixed (80c1445)
Follow-ups (not blocking — pre-existing or tech-debt)
I'll open a tracking issue for the matrix centralization + server head-dim extension. |
…arN defaults (#439) - Document the Grammar/ subsystem in Core (ITokenConstraint, JsonSchemaOutputConstraint, per-family tool-arg constraints, #423/#425) - Add the perplexity CLI command and -j/--json-schema structured-output flags - Note --tq-mode auto default (KVarN where supported, Lloyd-Max fallback, #436) and KVarN CPU/CUDA decode paths (#180) - Add SnapKvEval benchmark project; grammar coverage in Tests.Core Claude-Session: https://claude.ai/code/session_01QNixcRevA5NPyFsKUeCySr Co-authored-by: Claude <noreply@anthropic.com>
…N support matrix (#437) (#438) * fix(tq): un-rotate the compressed-region V aggregate on GPU Lloyd-Max attention (#435) Both GPU Lloyd-Max TurboQuant attention kernels (CUDA `llm_tq_attention` and the Vulkan `TqAttention` shader) aggregated the compressed-region V contribution in the rotated/sign-flipped basis and never applied the inverse transform, mixing it with the unrotated FP32-window contribution. This scrambled attention output for ANY model once context exceeded the 256-token FP32 window, on CUDA and Vulkan alike. Rewrite Phase 3 to mirror the CPU `TurboQuantKvCache.ComputeVAggregation` and the KVarN kernel: accumulate the compressed V aggregate in the rotated domain, apply the deferred per-head sign-flip + inverse WHT once (reusing shared scratch), then add the FP32-window contribution in the original domain. Thread the per-head sign patterns into both backends' `TqAttention` host methods (CUDA arg array 16->17; Vulkan binding 9, count 9->10) and all four engine call sites. Rewrite `TqAttention_NeedleInHaystack` to compare against the ORIGINAL basis via a new `ReconstructOriginal` helper; the test previously enshrined the wrong basis. Validated on CUDA (RTX 4070): needle test passes in the fully-compressed region; end-to-end Qwen3-0.6B Lloyd-Max stays coherent past the window and matches the CPU path. Vulkan is a faithful port of the same pattern but its shader source changed — run `scripts/gen-spirv.ps1` on a Vulkan-SDK machine to regenerate `Shaders.Precompiled.g.cs` before the Vulkan drift tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor(tq): centralize the KVarN support matrix into TqSupport + extend to the server (#437) The KVarN-vs-Lloyd-Max TurboQuant support matrix — which combinations of (SnapKV, head dim, backend, full-vs-partial offload, MoE, FP32 window) support KVarN — was hand-copied across three frontends (RunCommand, PerplexityCommand.ResolveAutoQuantizer, InferenceEngineLoader.ResolveTq), with the #432 quality warning copied 4x. A missed edit silently resolved auto -> Lloyd-Max on one surface only, reintroducing the #432 collapse (the exact shape of the partial-offload crash fixed in #436). Add `SharpInference.Engine.TqSupport` as the single source of truth: `KVarNBlockedReason` (the auto matrix), head-dim envelope predicates (IsKVarNHeadDim pow2[8,1024], IsKVarNCudaHeadDim pow2[8,256], IsLloydMaxHeadDim {128,256}), reason-string constants, and `QualityWarningReason`. All three frontends now delegate. Extend to the server: - Relax the InferenceEngineLoader up-front head-dim gate from a hard {128,256} reject to the codec-aware envelope, so KVarN's broader head dims (32/64/512/1024) are reachable on the server CPU path, as they already are on the CLI. - ResolveTq now throws a clean error instead of silently downgrading auto -> Lloyd-Max on a head dim Lloyd-Max has no codebook for (would have crashed in the fwd-pass ctor). - Reject explicit TqMode=kvarn when TurboQuant=false (the CLI already did). Unit-test the matrix once in TqSupportTests (28 cases incl. the Vulkan / no-CUDA GPU sub-cases the Perplexity wrapper can't express). Existing Perplexity + server tests unchanged and green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(tq): use exact 1/sqrt(D) for the Vulkan un-rotate WHT normalization (review) Code-review nit on #435: the Vulkan TqAttention un-rotate used inversesqrt() for the inverse-WHT normalization while the CPU WalshHadamard.Transform (which this inverts) and the CUDA kernel use exact 1/sqrt(D). inversesqrt is a lower-precision approximation on some GPUs and no Vulkan-vs-CPU parity test would catch the drift. Match the exact convention (same as the TqRotateQuery shader). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * build(vulkan): regenerate precompiled SPIR-V for the #435 TqAttention un-rotate The TqAttention shader source changed in this PR (deferred per-head sign flip + inverse WHT of the compressed-region V aggregate), leaving its precompiled SPIR-V blob and FNV hash key stale. Regenerated via scripts/gen-spirv.ps1 (Vulkan SDK 1.4.341.1); only the TqAttention entry changed (9668 -> 10972 bytes). VulkanPrecompiledShaderTests (drift guard) and the on-device VulkanShaderTests.TqAttention paths now pass, alongside the CUDA TQ/KVarN suites. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JNwRmR1tuioHTiiabU34Rp --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Problem
--tq(Lloyd-Max 3-bit, the default TurboQuant codec) collapses on Qwen3: wikitext-2 PPL 945.6 vs 15.47 fp32 at c=3072, and 19/20 math-eval generations degenerate into token loops (#432).Root cause
Three probes, all on real Qwen3-0.6B K/V vectors captured from the live forward pass (full analysis in #432 comment):
TurboQuantOpsapplies the sign flip after the WHT, which is mathematically a bare un-randomized WHT — but an A/B on real K/V shows identical reconstruction error for both orderings (relErr ≈ 0.18).DequantDot/Decompressreference to ~1% of the quantizer's own error.Fix
Make the validated KVarN codec the default wherever it is supported, per the issue's own recommendation:
run+perplexity):--tq-modedefaults toauto→ KVarN on CPU (any pow-2 head dim in [8, 1024]) and full-CUDA-offload dense (head dim ≤ 256); falls back to Lloyd-Max with a loud quality warning citing TurboQuant Lloyd-Max 3-bit collapses on Qwen3 (PPL 61x, degenerate decode) — pre-existing on master #432 on Vulkan, partial CUDA offload, MoE-on-GPU, SnapKV, or window < 128. Explicit--tq-mode lloydmaxkeeps the old behavior and silences the warning; explicitkvarnstill errors on unsupported combos.TqModeoption (defaultauto) with the same per-path resolution inInferenceEngineLoader(explicit kvarn on an unsupported path fails model load);SHARPI_TQ/SHARPI_TQ_MODEenv vars in the Host.TqQuantizer.LloydMax/ForwardPass.EnableTurboQuantXML docs.Validation
--tqon the issue's exact gate now runstq-kvarn-k4v2at PPL 15.6741 (was 945.56); explicit--tq-mode lloydmaxreproduces the old numbers.SHARPI_SNAPKV_BUDGET+--tqprints the Lloyd-Max fallback warning and runs.run --tq -g -1on CUDA resolves toKVarN K4V2.Follow-up
The GPU Lloyd-Max
TqAttentionV-basis defect found during this audit (compressed-region V aggregate never un-rotated on CUDA/Vulkan) is tracked separately as #435.Closes #432
🤖 Generated with Claude Code
https://claude.ai/code/session_01EuBdnPNJ2XdaFp9PjYoJyz