feat(kvcache): 8-bit KV cache (q8_0 / fp8_e4m3) behind --kv-cache-dtype - #103
feat(kvcache): 8-bit KV cache (q8_0 / fp8_e4m3) behind --kv-cache-dtype#103lucaspirola wants to merge 1 commit into
Conversation
Stores the KV cache in 8 bits plus an fp16 scale per 32 elements along head_dim
(1.0625 bytes/element vs 2), freeing VRAM for the MoE expert cache. Two schemes
share the scale tensor, store kernel and dequant path -- q8_0 (int8, s = max/127)
and fp8_e4m3 (s = max/448) -- so comparing them is a flag change, not a port.
- kvcache/quant.py: KVQuantSpec (storage dtype, block 32, torch reference
quantize/dequantize, effective bytes/element)
- kernel/triton/kv_quant.py: store kernel computing per-block max-abs and writing
the quantized buffer + scales
- kernel/triton/attention.py: dequant inside the four attention kernels behind a
QUANT constexpr (0 compiles the existing bf16 path unchanged); the scale varies
along head_dim, the reduction dim, so K/V dequantize to bf16 before the dot
- kvcache pools: parallel scale buffers, k_scale()/v_scale(), rebuild() realloc,
unit_bytes()/kv_cost() accounting on effective bytes
- server/args.py, engine: --kv-cache-dtype {auto,q8_0,fp8_e4m3} with gating
(triton backend only, head_dim % 32 == 0, supported pool families)
Tests: 53 new (round-trip vs torch reference, quantized attention vs the bf16
reference, pool sizing and hot rebuild, flag gating); the existing 33 triton
attention tests still pass.
Step 9 of tasks/todo.md is NOT done: no end-to-end validation on this host --
needle-in-246k, perplexity vs bf16, and the real expert-slot / tok-s gain are
unmeasured, so q8_0 vs fp8_e4m3 as the default is still an open question.
Co-Authored-By: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Je2rjENB9qJiiRGmNcnAct
|
Merged this into a deployment branch and ran it on 2x RTX 6000 Ada (sm_89), which is a different arch from the RTX 5080 you tested on. One bug, and the end-to-end numbers your "Not done" section asks for. A bug on sm_89: the fp8 store does not round to nearestTwo of your own tests fail here, and only the fp8 ones:
if IS_INT:
# Round half away from zero (what GGUF's Q8_0 does), then clamp -- the
# float->int cast truncates.
q = tl.where(q >= 0, tl.floor(q + 0.5), tl.ceil(q - 0.5))
q = tl.minimum(tl.maximum(q, -MAX_MAG), MAX_MAG)
tl.store(dst_ptr + ..., q.to(dst_ptr.dtype.element_ty))The int branch rounds because the cast truncates. The fp8 cast does not round to nearest either: triton lowers Fix is one line, mirroring what the int branch already does: else:
q = round_e4m3(tl.minimum(tl.maximum(q, -MAX_MAG), MAX_MAG))with End-to-end numbersQwen3.6-35B-A3B-FP8 (head_dim 256, GQA, 40 layers, 256 experts), TP=1,
The pool grows +88%, which is exactly the 2 / 1.0625 the format predicts, and aggregate throughput rises +27% because more requests fit without preemption. Single stream costs 5.8% for The expert-slot gain did not appear, for a reason specific to this model. 40 layers x 256 experts is 10,240 slots and
|
FlashML-org#113 and FlashML-org#103 both add --kv-cache-dtype and neither covers the other's pools, so the merge needs two resolutions git cannot make: - args.py auto-merged into TWO --kv-cache-dtype definitions. argparse rejects a duplicate option string at runtime, and git saw no textual conflict because the two landed in different parts of the file. Kept FlashML-org#103's, whose choices already cover both value sets (auto / q8_0 / fp8_e4m3); dropped mine. - _validate_kv_cache_dtype existed twice. Unified by routing on the pool family: a DSV4 checkpoint takes the DSV4 checks (fp8 only, head_dim % 32, native fp8), everything else falls through to FlashML-org#103's (triton backend, non-MLA, head_dim % BLOCK). q8_0 on DSV4 is now an explicit rejection rather than a wrong-dtype tensor reaching a kernel that only stores fp8. Deployment branch only.
…e flag string The unified gate short-circuited on kv_cache_dtype == 'auto' before reaching the non-DSV4 branch. FlashML-org#103's callers carry only the resolved kv_quant spec, so every one of its cases read as 'auto' and nothing was rejected -- its own gating tests caught it (9 failures, all DID NOT RAISE). Each branch now owns its early-out: DSV4 keys on the flag string (it also has to stamp dsv4_args for the cost model), everything else keys on the resolved spec. Deployment branch only.
What
8-bit KV cache storage behind
--kv-cache-dtype {auto,q8_0,fp8_e4m3}, trading KVVRAM for the MoE expert cache.
Two schemes share one scale tensor, store kernel and dequant path —
q8_0(int8,
s = max/127) andfp8_e4m3(s = max/448) — both 1.0625 bytes/elementvs 2 for bf16, an fp16 scale per 32 elements along
head_dim.kvcache/quant.py—KVQuantSpec(storage dtype, block 32, torch referencequantize/dequantize, effective bytes/element)
kernel/triton/kv_quant.py— store kernel (per-block max-abs → quantizedbuffer + scales)
kernel/triton/attention.py— dequant inside the four attention kernels behinda
QUANTconstexpr (0compiles the existing bf16 path unchanged); the scalevaries along
head_dim(the reduction dim), so K/V dequantize to bf16 before dotk_scale()/v_scale(),rebuild(),unit_bytes()/kv_cost()on effective bytes--kv-cache-dtypeflag with gating (triton backend only,head_dim % 32 == 0,supported pool families)
Tested on
NVIDIA RTX 5080 (16 GB), WSL2, driver 610.62, CUDA 13.0. Exercised in the same
run as the Laguna GGUF model (fp8 KV at 262144 tokens = 8.79 GiB vs 17.6 bf16,
which would not fit this card).
Results
tests/kernels/test_kv_quant.py,tests/kvcache/test_kv_quant_pool.py,tests/engine/test_kv_cache_dtype_gating.py), plus the 33 pre-existing tritonattention tests unchanged.
pool sizing/rebuild; flag gating.
Not done
The step-9 e2e gate (
tasks/todo.md) is open: needle-in-246k, perplexity vs bf16,and the measured expert-slot / tok-s gain are not yet captured, so
q8_0vsfp8_e4m3as default is still undecided.