[Fix] SM120 DSV4 decode: read FlashInfer's dispatch envelope (0.7.0) as well as the 0.6.18 pair table; opt-in padded heads - #39492
Open
avifenesh wants to merge 1 commit into
Conversation
…as well as the 0.6.18 pair table; opt-in padded heads `_flashinfer_dsv4_decode_capabilities` (sgl-project#36655) iterates `flashinfer.mla._sparse_mla_sm120._DECODE_DSV4_DISPATCH` as `(num_heads, topk)` pairs. That is the 0.6.18 shape. FlashInfer main since flashinfer-ai/flashinfer#4802 (453aa7c7296e, in every 0.7.0 build) publishes it as a `_DecodeDispatchEnvelope(min_topk)` predicate with `__contains__` only, so a DeepSeek-V4 launch with attention TP on SM120 dies at EagerRunner warm-up: File ".../sglang/srt/models/deepseek_v4.py", line 957, in _kernel_num_heads if flashinfer_dsv4_decode_supports_num_heads( File ".../sglang/kernels/ops/attention/flash_mla_sm120.py", line 314, in _flashinfer_dsv4_decode_capabilities return int(_DECODE_MAX_TOKENS), frozenset( TypeError: '_DecodeDispatchEnvelope' object is not iterable Seen on 4x RTX PRO 6000, TP4, DeepSeek-V4.1-Flash, flashinfer_python 0.7.0 (7629d218). main hits the same the day its flashinfer pin moves past 0.6.18. The reader now handles both shapes and fails closed. `_flashinfer_dsv4_decode_dispatch()` (lru_cache, one read) returns `(decode_max_tokens, head widths, shape)`: `pairs` when the dispatch is iterable (first elements of the tuples), `envelope` when it is a container only (heads 1..`_DECODE_MAX_HEADS` probed with the envelope's own `min_topk`), `none` for anything else (no flashinfer, no attribute, an envelope without `_DECODE_MAX_HEADS`, a predicate that raises, a table that does not unpack): no heads, so every caller stays on the padded 64-head path. `_flashinfer_dsv4_decode_capabilities` and `flashinfer_dsv4_decode_supports_num_heads` keep their signatures. While measuring the fix: with the envelope readable, sgl-project#36655's routing put TP4 decode (16 local heads) on FlashInfer's native 16-head kernel for the first time on this box. Single-request decode TPOT went from 8.115 to 9.134 ms (+12.6%) against the padded 64-head path; batch 4 / 8 (-2.0% / -0.9%) and every prefill and cache row were within the bar. So the exact-width routing stays the default (it is the measured win for batched decode) and `SGLANG_SM120_DSV4_DECODE_PADDED=1` keeps the padded path for a deployment tuned for c=1 latency. `_kernel_num_heads` asks the new `flashinfer_dsv4_decode_native_heads(n_local_heads, num_tokens)`, which logs once per head count which path it took and which dispatch shape it read: SM120 DSV4 decode heads: path=native local_heads=16 flashinfer_supports=yes flashinfer_dispatch=envelope(max_tokens=64, heads=1-128) SGLANG_SM120_DSV4_DECODE_PADDED=0 Tests: test/registered/unit/kernels/ops/attention/test_flash_mla_sm120_decode_dispatch.py (11, CPU, no torch: the shim is loaded from its path with torch / triton / envs stubbed and a stub flashinfer module in each shape). Pairs -> {8,16,32,64,128}; envelope -> 1..128 probed, a 513-min_topk envelope still yields its heads; an envelope without `_DECODE_MAX_HEADS`, `None`, `object()`, a 1-tuple table and a missing package all read `none`; the read is cached; default routes native under both shapes with one log line; the knob keeps padded; an uncovered width is padded with `flashinfer_supports=no`; > decode_max_tokens is never native.
avifenesh
requested review from
BBuf,
DarkSharpness,
HaiShaw,
HydraQYH,
celve and
yuan-luo
as code owners
September 14, 2026 23:58
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_dsv4_decode_capabilities(#36655) iteratesflashinfer.mla._sparse_mla_sm120._DECODE_DSV4_DISPATCHas(num_heads, topk)pairs. That is the flashinfer_python 0.6.18 shape. FlashInfer main since flashinfer-ai/flashinfer#4802 (453aa7c7296e, 2026-09-03, in every 0.7.0 build) publishes it as a_DecodeDispatchEnvelope(min_topk)predicate with__contains__only (topk became a runtime kernel argument, so the dispatch is no longer an enumerable pair set). With that FlashInfer installed, a DeepSeek-V4 launch with attention TP on SM120 dies at EagerRunner warm-up:Seen on 4x RTX PRO 6000, TP4, DeepSeek-V4.1-Flash, flashinfer_python 0.7.0 (7629d218). main hits the same the day its flashinfer pin moves past 0.6.18; the capability check was written to be fail-closed for a locally installed FlashInfer and this is the case it did not cover.
Modifications
_flashinfer_dsv4_decode_dispatch()(lru_cache, one read) returns(decode_max_tokens, head widths, shape):pairswhen the dispatch is iterable (0.6.18: the tuples' first elements),envelopewhen it is a container only (heads1..._DECODE_MAX_HEADSprobed with the envelope's ownmin_topk),nonefor anything else (no flashinfer, no attribute, an envelope without_DECODE_MAX_HEADS, a predicate that raises, a table that does not unpack), which keeps every caller on the padded 64-head path._flashinfer_dsv4_decode_capabilitiesandflashinfer_dsv4_decode_supports_num_headskeep their signatures (test_flash_mla_backends.pycalls the latter).SGLANG_SM120_DSV4_DECODE_PADDED(environ.py,EnvBool(False)): opt-in. While measuring the fix, [SM120] Use exact query-head widths for DeepSeek-V4 sparse MLA decode #36655's routing put TP4 decode (16 local heads) on FlashInfer's native 16-head kernel for the first time on this box, and single-request decode TPOT went from 8.115 to 9.134 ms (+12.6%) against the padded 64-head path; batch 4 / 8 (-2.0% / -0.9%) and every prefill and prefix-cache row were within the bar. The exact-width routing stays the default (it is the measured win for batched decode);=1keeps the padded path for a deployment tuned for c=1 latency. A width the installed FlashInfer does not cover takes the padded path either way._kernel_num_headsasks the newflashinfer_dsv4_decode_native_heads(n_local_heads, num_tokens), which logs once per head count which path it took and which dispatch shape it read, so the routing is visible in the server log:SM120 DSV4 decode heads: path=native local_heads=16 flashinfer_supports=yes flashinfer_dispatch=envelope(max_tokens=64, heads=1-128) SGLANG_SM120_DSV4_DECODE_PADDED=0Not a duplicate of the open SM120 PRs that touch the same files: #38646 (NVFP4 KV cache format), #35104 (drops the 64-head pad on the prefill path; this PR is about the decode dispatch read and leaves prefill alone), #38969 (per-call Triton fallback for prefill shapes). None of them changes
_flashinfer_dsv4_decode_capabilities.Accuracy Tests
No numerics change: the fix only decides which already-existing decode path runs. On the 4x RTX PRO 6000 box the fixed build passed the same DeepSeek-V4.1 gate set as the pre-#36655 reference (greedy decode, tool calls, long-prefix cache hits, speculative acceptance 2.644 vs 2.636 tokens/verify).
Benchmarking and Profiling
4x RTX PRO 6000 (SM120), DeepSeek-V4.1-Flash, TP4 (16 local heads), flashinfer_python 0.7.0, 5% bar against the padded 64-head reference:
The c=1 row is the reason the padded path stays reachable through the knob.
Checklist
test/registered/unit/kernels/ops/attention/test_flash_mla_sm120_decode_dispatch.py(11, CPU, no torch: the module is loaded from its path with torch / triton / envs stubbed and a stub flashinfer module in each of the two shapes; the fail-closed cases; the cached read; default native, knob padded, uncovered width padded, > decode_max_tokens never native).environ.pynext toSGLANG_SM120_FLASHMLA_BACKEND.run-cilabel if you want the CI to run.CI States
Latest PR Test (Base): ❌ Run #34911171997
Latest PR Test (Extra): ❌ Run #34911171792
Latest PR Test (AMD ROCm 10): ❌ Run #34911171966