[SM120] Add opt-in NVFP4 sparse-MLA KV cache format for DeepSeek-V4 - #38646
Open
AliceChenyy wants to merge 2 commits into
Open
AliceChenyy wants to merge 2 commits into
AliceChenyy wants to merge 2 commits into
Conversation
FlashInfer PR sgl-project#4955 added native NVFP4 sparse-MLA kernels for SM120/SM121 alongside the existing FP8 ones. This wires them into the DSv4 attention path behind SGLANG_SM120_KV_CACHE_FORMAT, which defaults to "fp8" and leaves every existing code path untouched. The NVFP4 cache ABI is 384 B/token (packed E2M1 nope + BF16 rope + E4M3 group scales) against FP8's 584 B, and it has no per-page padding, so the pool groups it into the page sizes the kernels accept (64 primary, 2 or 64 extra) and the flat slot space stays identity-mapped. That also removes the 256->64 page-split copy the FP8 path performs on every attention call. Three fused kernels only emit the FP8 ABI, so under NVFP4: - the SWA store takes the existing bf16-intermediate path (SGLANG_DSV4_USE_BF16_KV_QUANT_SOURCE) and quantizes on store; - the compressed c4/c128 store lands in a BF16 staging buffer via the existing bf16_store path and is quantized afterwards; - the pre-quantized FP8 write path asserts instead of corrupting silently. Gains are shape-dependent and only materialize when attention runs all 64 heads per rank (attn_tp=1, i.e. DP-attention, PP, or CP). Under attention TP the NVFP4 prefill kernel is slower than FP8, so this stays opt-in. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every case corresponds to a defect that reached a running server while building this path, so these are regression guards rather than coverage: staging-buffer pointer stability under CUDA graph capture, bucketing so that cache does not grow without bound, per-call zeroing, int64 out_loc, uint8 coercion in nvfp4_cache_view, and the platform gate. All of that runs on CPU. The slot-addressing round-trip needs the kernels and is skipped off SM120. sglang CI has no SM120 runner, so the file is not registered in run_suite.py. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
AliceChenyy
marked this pull request as ready for review
September 11, 2026 10:13
AliceChenyy
requested review from
BBuf,
DarkSharpness,
Fridge003,
HaiShaw,
HydraQYH,
Qiaolin-Yu,
Ying1123,
alphabetc1,
celve,
hanming-lu,
hebiao064,
hnyls2002,
huangtingwei9988,
hzh0425,
ispobock,
merrymercy,
xiezhq-hermann,
yizhang2077 and
yuan-luo
as code owners
September 11, 2026 10:13
5 tasks
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.
Motivation
FlashInfer #4955 added native NVFP4 sparse-MLA kernels for SM120/SM121 next to the existing FP8 ones. This wires them into the DeepSeek-V4 attention path on SM120.
Opt-in through
SGLANG_SM120_KV_CACHE_FORMAT=nvfp4. The default staysfp8and no existing code path changes behavior.Modifications
KV pool (
deepseek_v4_memory_pool.py) — 384 B/token ABI (packed E2M1 nope + BF16 rope + E4M3 group scales) against FP8's 584 B. The NVFP4 ABI packs each page exactly, so the pool regroups the flat slot space into the page sizes the kernels accept (64 primary, 2 or 64 extra) with no per-page padding. Slot ids are unchanged by that regrouping, so writers and readers agree without remapping — and unlike the FP8 path, there is no 256→64 page-split copy on every attention call. Writes go throughnvfp4_quantize_append_sparse_mla_cache.Attention (
flash_mla_sm120.py) — an NVFP4 branch that selects phase/CPB viaplan_nvfp4_sparse_mla_sm120(that planner skips calibration while a stream is capturing, so it is CUDA-graph safe) and memoizes the decision per shape.Stores — three fused kernels only emit the FP8 ABI, so under NVFP4:
SGLANG_DSV4_USE_BF16_KV_QUANT_SOURCE) and quantizes on store;bf16_storepath and is quantized afterwards;Platform gate —
is_nvfp4_kv_cache()raises at startup if the flag is set on anything other than SM120/SM121.Performance
DeepSeek-V4-Flash, 4× RTX PRO 6000 Blackwell Server Edition (SM120, 96 GB),
--moe-runner-backend marlin,--kv-cache-dtype fp8_e4m3.max_total_num_tokenspinned identically across each FP8/NVFP4 pair — NVFP4 stores 66% of FP8's bytes per token, so an unpinned NVFP4 arm gets a ~1.5× larger pool and its throughput is not comparable.Gains depend on heads-per-rank, not on batch or sequence length
DSv4-Flash has 64 attention heads. Operator benchmark (FlashInfer's own, primary topk 128 =
sliding_window, extra topk 512 =index_topk), NVFP4 vs FP8 prefill:Under attention TP the NVFP4 prefill kernel is slower than FP8. This is why the feature is opt-in and why the flag should only be enabled with
attn_tp=1.End-to-end, ISL 8192 (DP4:
--tp 4 --enable-dp-attention --dp 4)--disable-radix-cache, pinned to 3,990,784 tokens. Both arms measured againstthe code in this branch, in the same container:
End-to-end, ISL 32768, three
attn_tp=1topologiesOSL=8,
--num-prompts BS --max-concurrency BS --warmup-requests 0,--disable-radix-cache,--chunked-prefill-size 32768, every arm pinned to 3,864,064 tokens. Median of 5 samples per cell, measured against this branch:--tp 4 --attn-cp-size 4 --enable-prefill-cp --cp-strategy interleave)--tp 1 --pp-size 4)At BS=1 the gain tracks CP degree inversely, which is consistent with CP adding cross-rank communication that NVFP4 does not accelerate. That ordering reproduced across two independent passes.
An earlier pass on the pre-fix build showed the same two modes in the opposite proportion (four fast, one slow), which is why a first 2-sample pair on this cell produced opposite-signed conclusions. FP8 is flat on the same shape, so this is specific to the NVFP4 path — plausibly the same tactic-selection issue as the 64-query-token operator regression noted under Limitations. Every other cell reproduced within 0.6 pp across the two passes.
For reference, FlashInfer #4955 reports +9.02% token throughput for a vLLM PP=4 / ISL8192 / OSL1 run.
Accuracy
GSM8K, 5-shot, DP4:
No measurable loss (0.3σ). An earlier n=200 pass showed a 0.5–1.0 pp dip; at n=1000 that does not hold up, so it was noise.
Limitations
flashinfer-jit-cacheis installed from a wheel predating that PR, its prebuilt FP8 module has the old 12-argument signature and the FP8 path breaks too; uninstall it so the kernels JIT from source.Checklist
black,isortclean;ruffintroduces no new findingsfp8; FP8 behavior unchangedtest/srt/mem_cache/test_dsv4_nvfp4_cache.py(14 cases). Each one pins a defect that reached a running server during development: staging-buffer pointer stability, bucketing, per-call zeroing, int64out_loc, uint8 coercion innvfp4_cache_view, and the platform gate. All of that runs on CPU; only the slot-addressing round-trip needs the kernels and is skipped off SM120 (13 passed, 1 skippedwith no GPU visible,14 passedon an RTX PRO 6000). Verified by mutation: restoring the grow-in-place staging buffer turns two of them red withstaging storage moved; a replayed CUDA graph would write through a freed pointer. Not registered inrun_suite.py— sglang CI has B200 runners but no SM120, so there is nowhere for the GPU case to run; happy to wire it in if a suitable suite exists.Two bugs found in self-review and fixed in this branch, noted for reviewers since they are subtle. They cost roughly 0.9 pp of throughput (an earlier build measured +3.9/+4.0% where this one measures +3.0/+3.1%); the FP8 baseline reproduced to within 0.3% across the container rebuild that separates those runs, so the difference is more likely real than environmental, though each NVFP4 configuration is a single sample.
🤖 Generated with Claude Code