[Feature] Add optional FP8 (float8_e4m3fn) KV cache pool - #132
Open
javierlimt6 wants to merge 7 commits into
Open
[Feature] Add optional FP8 (float8_e4m3fn) KV cache pool#132javierlimt6 wants to merge 7 commits into
javierlimt6 wants to merge 7 commits into
Conversation
Author
|
@DarkSharpness any thoughts? :) |
| use_pynccl: bool = True | ||
| max_seq_len_override: int | None = None | ||
| num_page_override: int | None = None # if not None, will override the number of pages | ||
| kv_dtype: torch.dtype | None = None # if torch.float8_e4m3fn, use FP8 KV cache; else compute dtype |
Collaborator
There was a problem hiding this comment.
Can we make this a torch.dtype (set to default dtype when constructing the config)? Do not make it an optional
Author
There was a problem hiding this comment.
@DarkSharpness i set it to assign to dtype by default if None via __post_init__, does this look good to you?
| } | ||
| kwargs["dtype"] = DTYPE_MAP[dtype_str] if isinstance(dtype_str, str) else dtype_str | ||
|
|
||
| KV_DTYPE_MAP = { |
Collaborator
There was a problem hiding this comment.
Please reuse the DTYPE_MAP. It should just be mapping from a string to dtype.
Author
There was a problem hiding this comment.
got it, thanks for flagging!
javierlimt6
force-pushed
the
feat/kv-cache-quantisation
branch
2 times, most recently
from
May 17, 2026 13:33
c024693 to
22b2681
Compare
Adds a "FP8 KV Cache" section to features.md covering the flag, the sm_90 + FlashAttention restriction, and the uncalibrated-scales caveat.
javierlimt6
force-pushed
the
feat/kv-cache-quantisation
branch
from
May 17, 2026 13:41
22b2681 to
c19cf8d
Compare
Author
|
@DarkSharpness made the changes you requested |
Author
|
@DarkSharpness bringing your attention to this again if you are free and this is good to go |
javierlimt6
force-pushed
the
feat/kv-cache-quantisation
branch
from
May 22, 2026 16:05
bf420c1 to
0dc496e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an opt-in FP8 KV cache pool (
QuantizedMHAKVCache) that halves the bytes-per-token of the cache, doubling effective context/concurrency capacity at the same VRAM budget. Plumbing-only v1: no calibrated per-tensor scales, justclamp(-448, 448).to(fp8)at the write boundary. Backend kernels handle dequantisation on read via their existing fp8 paths.--kv-dtype float8orEngineConfig(kv_dtype=torch.float8_e4m3fn); default unchangedMotivation
The KV cache dominates VRAM for long-context or high-concurrency serving. Upstream SGLang and vLLM both support fp8 KV; mini-sglang did not. The change is small because mini-sglang's pool/manager/backend separation localises the change to the pool layer plus a small touch on the FlashInfer metadata.
Design
Two dtype properties, not one
Existing pools conflated "compute precision" and "storage precision" into a single
dtypeproperty. For an fp16 pool those are identical. For a quantised pool they differ: the buffer holds fp8 bytes, attention computes Q in fp16.This PR splits them on
BaseKVCachePool:The default returns
self.dtype, soMHAKVCacheand any future non-quantised pool inherits the right behaviour with zero changes. Follows the codebase's existing "abstract core, concrete default" pattern (seeBaseOP.state_dictfor precedent).The pool subclass
QuantizedMHAKVCacheoverrides__init__to allocate the buffer asfloat8_e4m3fn, overridesstore_kvto clamp-and-cast before delegating to the parent, and overrides the two dtype properties. The parent'sstore_cachekernel call is reused unchanged; it does a raw byte copy viawarp::copy<kElementSize>and is dtype-agnostic.Full class is 57 lines.
Factory + hardware gate
create_kvcache_poolgains one branch:kv_dtype == torch.float8_e4m3fn:logger.warning_rank0documenting v1 limitations.QuantizedMHAKVCache.Two contract surprises worth flagging
Trap 1:
torch.to(float8_e4m3fn)does not saturateValidated on torch 2.9.1 + CUDA 12.8. The default cast produces e4m3fn NaN (raw bytes
0x7F/0xFF) for out-of-range inputs, including±500,±inf, and (correctly) NaN:.to(fp8)then back to fp16clamp(-448, 448).to(fp8)Without the explicit clamp, any K/V outlier silently fills the cache with NaN and downstream attention output is garbage. The clamp is mandatory, not stylistic. Pinned by a regression test (
test_default_cast_to_fp8_produces_nan) so torch version drift fails loudly.Trap 2: FlashAttention's fp8 KV path requires sm_90+
flash_attn_with_kvcache's Python wrapper accepts fp8 KV tensors on any GPU, but the underlying SASS path is missing on Ada (sm_89). Result on Ada is silent corruption, no error.The factory raises at engine init if
--kv-dtype float8is combined with--attn faon sm < 90, with a clear error pointing the user at--attn fi. FlashInfer and TRT-LLM work on sm_89+; only FA needs the gate.(Tested on RTX 4060 Laptop / sm_89. FA + fp8 on Hopper is gated open but locally unverified; needs an H100 hour from anyone reviewing.)
Files changed
kvcache/base.pystore_dtypeproperty with concrete defaultkvcache/quantized_mha_pool.pyQuantizedMHAKVCachesubclasskvcache/__init__.pyengine/config.pykv_dtype: torch.dtype | None = Noneengine/engine.pykv_dtypethrough; page count uses storage dtypeattention/fi.pyFIMetadata.kv_dtypefield; passed askv_data_type=toplan()server/args.py--kv-dtype {auto,float8}CLI flagtests/kernel/test_kvcache_quantized.pyfa.pyandtrtllm.pyare unchanged; both backends dispatch on cache tensor dtype directly.naive_cache.py,radix_cache.py,csrc/jit/store.cu,kernel/store.pyuntouched.Testing
New tests:
test_quantized_kvcache: write fp16 K/V viastore_kv, read back viak_cache()andv_cache(), verify byte-exact round-trip on in-range values, exact ±448 saturation on out-of-range, NaN propagation on NaN inputs.test_default_cast_to_fp8_produces_nan: regression pin for the Trap 1 finding. Fails loudly if a future torch version changes the cast semantics.Benchmarks
Methodology: launch axon with
--kv-dtype float8 --attn fi, run[bench_serving.py](https://raw.githubusercontent.com/sgl-project/sglang/main/python/sglang/bench_serving.py)(SGLang's, patched for axon's OpenAI endpoint), 2 to 3 trials per configuration, aggregate mean and stdev. fp16 baseline re-run on the same axon build for apples-to-apples (CV < 1 per cent across all runs).Capacity (always 2x exact)
Latency / throughput (Qwen3-0.6B, RTX 4060)
ITL wins scale monotonically with memory-bandwidth pressure (longer context, higher concurrency, longer output), as expected. Peak at conc=20 hits 21 per cent throughput / 27 per cent median ITL reduction; within striking distance of the theoretical 2x bandwidth ceiling.
TTFT regression (honest)
TTFT regresses 4 to 12 per cent across every configuration. Cause:
clamp().to(fp8)is two unfused kernel launches per K and V per layer per token. Prefill is compute-bound, so fp8 storage does not help; the cast is pure additive cost. Where TTFT matters (interactive short-context) the effect is 4 to 5 per cent; where it does not (long-form output, batch serving) it is invisible against the ITL wins.The fix is a fused Triton kernel for clamp + cast + scatter. Roughly 50 lines, entirely additive, deferred to a follow-up perf PR. vLLM and SGLang both shipped their first fp8 KV with the same unfused regression and closed it in a later version.
Limitations and deferred work
Documented honestly in the
logger.warning_rank0that fires on opt-in:k_scale/v_scale. v1 uses scale=1.0 (saturating cast). Checkpoints that ship calibrated fp8 scales (most W8A8 quantised models) will have those scales silently ignored. v2 will add plumbing: FI exposesrun(k_scale=..., v_scale=...), FA exposesk_descale=/v_descale=, TRT-LLM extends the existingbmm1_scale/bmm2_scale. No new kernels needed.Open questions for maintainers
logger.warning_rank0acceptable as a first PR, or would you prefer I land v2 calibration in the same PR? My read is that v1 demonstrates the abstraction cleanly and v2 builds on it without changing it, but happy to combine if reviewer time is the constraint.--kv-dtype float8to match the existing--dtype float16/bfloat16/float32convention.--kv-cache-dtype(vLLM's convention) is also reasonable. Preference?Reproducing benchmarks
Full bench scripts and aggregator are in
tools/benchmarks/fp8_kv/(added separately) or available on request. Single-config repro:Checklist
pytest tests/ --no-cov)Happy to split this into smaller PRs if preferred (e.g. base abstraction + tests as PR 1, quantised pool as PR 2). Also happy to drop scope items that feel out-of-place for a reference implementation.