Background
turbovec (MIT-licensed Rust ANN index) and SharpInference.TurboQuant both implement the same underlying algorithm (Zandieh et al., arXiv:2504.19874): WHT rotation + Lloyd-Max scalar quantization. turbovec uses a FAISS-FastScan-derived SIMD inner loop (nibble-split byte-shuffle LUTs + u16 accumulators) that we can borrow to speed up the existing TurboQuant KV-cache hot paths.
This is option (2) from the integration analysis — borrow the kernel, not the library. Pure C# port, no FFI, NativeAOT-friendly.
Why this is worth doing
The current K-scoring loop at src/SharpInference.Engine/ForwardPass.cs:995-1002 calls KvCacheCompressor.DequantDot once per cached position. The AVX2 inner loop (TurboQuantOps.DequantDot4Avx2, src/SharpInference.TurboQuant/TurboQuantOps.cs:232) vectorizes along dim using Avx2.GatherVector256, which is the slow leg on every recent uarch (~2-3× slower than equivalent scalar throughput).
FastScan inverts the loop: vectorize along vectors (32 KV positions per tile) and replace gathers with vpshufb (1/clock). The V-aggregation at ForwardPass.cs:1016-1025 (currently full Decompress + scalar weighted sum) benefits from the same kernel transposed.
Kernel design
TurboQuant is scalar quantization (one Lloyd-Max codebook per layer, not PQ), so the FAISS layout doesn't port 1:1, but the byte-shuffle trick does. For 4-bit, dim=128:
Per-query setup (once per head):
scale = max over (d,v) of |centroids[v] * rotatedQuery[d]| / 127
For each d in 0..128:
LUT[d][v] = quantize_i8( centroids[v] * rotatedQuery[d] / scale )
Per FastScan tile (32 KV positions):
acc[0..32] : two YMM regs holding 32 × i16 = 0
For d in 0..128:
codes_lo = load 16 nibbles for dim d (positions 0..15)
codes_hi = load 16 nibbles for dim d (positions 16..31)
lut = load LUT[d]
acc.lo += Ssse3.Shuffle(lut, codes_lo) // saturating i16 add
acc.hi += Ssse3.Shuffle(lut, codes_hi)
For t in 0..32:
score[t] = (acc[t] * scale) * norm[t]
- Range: 128 dims × i8 ≈ ±16256, fits i16.
- AVX-512 path:
VPDPBUSD fuses two-dim int8 multiply-add into i32 accumulators.
- V-path is symmetric — replace per-dim
q[d] with per-position attention weight w[t], accumulate per-d instead of per-t.
Files & estimated LOC
| File |
Change |
LOC |
src/SharpInference.TurboQuant/FastScan.cs (new) |
Block-of-32 packer, i8 LUT builder, AVX2 + AVX-512 K-score kernels, symmetric V-aggregation kernel |
~500 |
src/SharpInference.TurboQuant/TurboQuantOps.cs |
Add DequantDot32 / WeightedDecompress32 entry points; keep existing per-block fns for the partial-tile tail |
+80 |
src/SharpInference.Engine/TurboQuantKvCache.cs:65-150 |
Storage: write FastScan-tiled tiles (32-position groups) directly; keep small per-position buffer only for in-flight partial tile |
~150 modified |
src/SharpInference.Engine/ForwardPass.cs:994-1031 |
Replace per-position for (t = 0; t < tqLen; t++) loops with tiled for (tile = 0; tile + 32 ≤ tqLen; tile += 32) + scalar tail |
~50 modified |
src/SharpInference.Engine/HybridForwardPass.cs:979 |
Mirror change |
~50 modified |
tests/SharpInference.Tests.TurboQuant/FastScanTests.cs (new) |
Verify tile-scoring matches sum of per-block DequantDot within ~1e-3 (LUT i8 quant is the only drift source) |
~150 |
benchmarks/SharpInference.Bench/TurboQuantBench.cs |
New benches: q·KV for ctxLen ∈ {1K, 4K, 8K, 16K} |
+60 |
Total: ~1000 LOC, one new public file, surgical engine edits.
Phased plan
Phase 1 — Kernel only, no engine changes
- Add
FastScan.cs with K-score and V-aggregation kernels (AVX2 first; AVX-512 as a follow-up).
- Add tests verifying numerical equivalence with existing
DequantDot within tolerance.
- Add standalone benchmark.
- Decision gate: if standalone speedup is <1.5× over existing AVX2 path, stop here.
Phase 2 — Engine integration (K-path)
- Switch
TurboQuantKvCache to write tiled layout directly. Per-position layout only retained as staging for the in-flight partial tile (<32 positions).
- Rewrite
ForwardPass.cs:995-1002 to call tiled DequantDot32 for full tiles + scalar tail.
- Mirror in
HybridForwardPass.cs.
- Perplexity check on a held-out set to confirm no quality regression from i8 LUT quantization.
Phase 3 — V-aggregation path
- Replace the
Decompress + weighted-sum loop at ForwardPass.cs:1016-1025 with the symmetric FastScan V-kernel.
Phase 4 — AVX-512 VNNI variant
VPDPBUSD-based kernel for Sapphire Rapids / Zen 4+.
Gotchas
- i8 LUT quantization is the only quality-risk surface. TurboQuant centroids are bounded (~±3 after WHT) and rotated queries are unit-ish, so the per-query scale factor should be well-behaved. Fallback if perplexity regresses: i16 LUTs with
vpmaddwd accumulation — still much faster than the gather path.
- CPU/Vulkan-CPU-fallback only. The CUDA path (
CudaForwardPass, CudaHybridForwardPass) already has GPU TQ kernels and is out of scope.
- Partial-tile tail. For
tqLen not a multiple of 32, the existing per-position DequantDot loop handles the last <32 positions. The per-position layout doesn't disappear entirely — it's the staging area for the in-flight tile.
- Expected end-to-end gain. K-scoring + V-aggregation are ~30-50% of TQ-enabled decode at long context. A 2-3× phase-level speedup translates to ~15-30% end-to-end at ctxLen=8K. Bigger context → bigger win.
- No public API breakage.
KvCacheCompressor surface can stay identical; new tiled entry points are additive.
References
- turbovec: https://github.com/RyanCodrai/turbovec (MIT)
- TurboQuant paper: arXiv:2504.19874
- FAISS FastScan: André et al., "Accelerated Nearest Neighbor Search with Quick ADC" (ICMR 2017) and "Quicker ADC" (TPAMI 2021)
- Existing impl:
src/SharpInference.TurboQuant/TurboQuantOps.cs, src/SharpInference.Engine/TurboQuantKvCache.cs
Background
turbovec (MIT-licensed Rust ANN index) and
SharpInference.TurboQuantboth implement the same underlying algorithm (Zandieh et al., arXiv:2504.19874): WHT rotation + Lloyd-Max scalar quantization. turbovec uses a FAISS-FastScan-derived SIMD inner loop (nibble-split byte-shuffle LUTs + u16 accumulators) that we can borrow to speed up the existing TurboQuant KV-cache hot paths.This is option (2) from the integration analysis — borrow the kernel, not the library. Pure C# port, no FFI, NativeAOT-friendly.
Why this is worth doing
The current K-scoring loop at
src/SharpInference.Engine/ForwardPass.cs:995-1002callsKvCacheCompressor.DequantDotonce per cached position. The AVX2 inner loop (TurboQuantOps.DequantDot4Avx2,src/SharpInference.TurboQuant/TurboQuantOps.cs:232) vectorizes alongdimusingAvx2.GatherVector256, which is the slow leg on every recent uarch (~2-3× slower than equivalent scalar throughput).FastScan inverts the loop: vectorize along vectors (32 KV positions per tile) and replace gathers with
vpshufb(1/clock). The V-aggregation atForwardPass.cs:1016-1025(currently fullDecompress+ scalar weighted sum) benefits from the same kernel transposed.Kernel design
TurboQuant is scalar quantization (one Lloyd-Max codebook per layer, not PQ), so the FAISS layout doesn't port 1:1, but the byte-shuffle trick does. For 4-bit, dim=128:
VPDPBUSDfuses two-dim int8 multiply-add into i32 accumulators.q[d]with per-position attention weightw[t], accumulate per-d instead of per-t.Files & estimated LOC
src/SharpInference.TurboQuant/FastScan.cs(new)src/SharpInference.TurboQuant/TurboQuantOps.csDequantDot32/WeightedDecompress32entry points; keep existing per-block fns for the partial-tile tailsrc/SharpInference.Engine/TurboQuantKvCache.cs:65-150src/SharpInference.Engine/ForwardPass.cs:994-1031for (t = 0; t < tqLen; t++)loops with tiledfor (tile = 0; tile + 32 ≤ tqLen; tile += 32)+ scalar tailsrc/SharpInference.Engine/HybridForwardPass.cs:979tests/SharpInference.Tests.TurboQuant/FastScanTests.cs(new)DequantDotwithin ~1e-3 (LUT i8 quant is the only drift source)benchmarks/SharpInference.Bench/TurboQuantBench.csTotal: ~1000 LOC, one new public file, surgical engine edits.
Phased plan
Phase 1 — Kernel only, no engine changes
FastScan.cswith K-score and V-aggregation kernels (AVX2 first; AVX-512 as a follow-up).DequantDotwithin tolerance.Phase 2 — Engine integration (K-path)
TurboQuantKvCacheto write tiled layout directly. Per-position layout only retained as staging for the in-flight partial tile (<32 positions).ForwardPass.cs:995-1002to call tiledDequantDot32for full tiles + scalar tail.HybridForwardPass.cs.Phase 3 — V-aggregation path
Decompress+ weighted-sum loop atForwardPass.cs:1016-1025with the symmetric FastScan V-kernel.Phase 4 — AVX-512 VNNI variant
VPDPBUSD-based kernel for Sapphire Rapids / Zen 4+.Gotchas
vpmaddwdaccumulation — still much faster than the gather path.CudaForwardPass,CudaHybridForwardPass) already has GPU TQ kernels and is out of scope.tqLennot a multiple of 32, the existing per-positionDequantDotloop handles the last <32 positions. The per-position layout doesn't disappear entirely — it's the staging area for the in-flight tile.KvCacheCompressorsurface can stay identical; new tiled entry points are additive.References
src/SharpInference.TurboQuant/TurboQuantOps.cs,src/SharpInference.Engine/TurboQuantKvCache.cs