Skip to content

Fix DSPARK SM120 decode dispatch for non-instantiated topk widths - #33407

Open
hassellof wants to merge 4 commits into
sgl-project:mainfrom
hassellof:fix/dspark-sm120-decode-dispatch
Open

hassellof wants to merge 4 commits into
sgl-project:mainfrom
hassellof:fix/dspark-sm120-decode-dispatch

Conversation

@hassellof

@hassellof hassellof commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Independent reproductions (added 2026-08-06)

Three environments beyond the original report now hit this crash, and one open PR carries this patch as a prerequisite:

So on consumer/workstation Blackwell, DeepSeek's own recommended DSPARK recipe currently cannot boot without either this change or a throughput-costing env override.

Relationship to flashinfer#4309

flashinfer#4309 adds native topk=192 instantiations and is the preferred fast path for that width once it is released and pinned. This change is complementary rather than superseded:

  • it fail-softs any non-instantiated width on already released flashinfer (≤ 0.6.15.post1, sglang's current pin), so users are not blocked on a flashinfer release + pin bump;
  • draft indexer widths are model-controlled, so 192 will not be the last odd width — flashinfer#4336 already reports a missing (32, 256) decode instance;
  • the test suite here includes a bucket-table canary (assertNotIn(192, ...) over the installed decode dispatch table) that goes red exactly when a pinned flashinfer gains 192, flagging the moment the pad can be retired for that width.

Motivation

DeepSeek-V4-Flash-0731 — the official release of DeepSeek-V4-Flash — ships with the DSpark speculative-decoding module bundled, and its official SGLang launch recipe enables it via --speculative-algorithm DSPARK, with the draft weights taken from the same checkpoint.

Running that officially recommended configuration on SM120 (Blackwell workstation/desktop) GPUs crashes the server at boot. DSPARK's draft indexer emits topk=192, which is in no instantiated CUTLASS sparse-MLA bucket (decode: (num_heads, topk) table with topk ∈ {128, 512, 1024}; prefill orchestrator: {128, 512, 1024, 2048}), and the prefill kernel additionally asserts num_tokens > 64. Two boot-time crashes result:

  • the warmup draft pass (num_tokens=5) falls through to the prefill kernel:
    Check failed: num_tokens > 64 (5 vs. 64): Decode (num_tokens <= 64) must go through sparse_mla_sm120_decode_dsv3_2 or sparse_mla_sm120_decode_dsv4
  • the draft CUDA-graph capture (the draft head runs unsharded, num_heads=64, and capture batches exceed 64 tokens):
    Unsupported sparse-MLA prefill configuration: model=DSV4 num_heads=64 topk=192 page_block_size=64

Same failure family as #33134 (DGX Spark, sm_121, also topk=192); this change may resolve that report as well, though we could not verify on sm_121 hardware.

Modifications

In python/sglang/kernels/ops/attention/flash_mla_sm120.py (_flash_mla_flashinfer):

  • Right-pad the sparse-MLA index tensor with -1 (the kernels' documented "skip" sentinel, see flashinfer csrc/sparse_mla_sm120.cu) up to the next instantiated topk bucket, and cap the scan via topk_length so the padding is never read — padded rows add no scan work.
  • If a decode-sized batch is still not dispatchable to the CUTLASS decode kernel after padding (e.g. draft head_dim != 512), fall back to the existing Triton sparse-decode kernel for that call instead of routing into the prefill path, which would assert.

Dispatch behavior for already-instantiated shapes is unchanged.

Accuracy Tests

The change only widens the index tensor with skip sentinels (never read; scan capped via topk_length) and reroutes shapes that previously crashed — it does not alter attention math for configurations that already worked.

Reference hardware: 4× NVIDIA RTX PRO 6000 Blackwell Max-Q Workstation Edition (GB202, sm_120, 96 GB GDDR7 each), TP=4, deepseek-ai/DeepSeek-V4-Flash-0731, official DSPARK launch recipe (--speculative-algorithm DSPARK, --moe-runner-backend flashinfer_mxfp4, --disable-flashinfer-autotune, --kv-cache-dtype fp8_e4m3), sampling at DeepSeek's official calibration (temperature 1.0).

The full environment this was verified in — image build, patch list, serving flags and measured numbers — is published at https://github.com/ombori/deepseek-v4-flash-0731-sglang-4x-rtx-pro-6000 (prebuilt image on ghcr.io) for anyone wanting to reproduce on SM120.

  • Both boot-time crashes above no longer reproduce; the server boots (including full decode CUDA-graph capture for target-verify and draft) and serves continuously under DSPARK.
  • Chat completions, multi-tool-call requests (valid JSON arguments), and streaming tool calls verified correct through an OpenAI-compatible proxy chain.

Speed Tests and Profiling

python -m sglang.bench_serving --dataset-name random --random-input-len 1024 --random-output-len 512 --random-range-ratio 1.0 on the reference hardware above:

max concurrency output tok/s mean ITL accept length
16 697 19.1 ms 3.45
32 995 26.8 ms 5.27
64 1282 43.2 ms 5.19

For reference, the same hardware and workload previously measured 649 output tok/s @ concurrency 32 with a conservative EAGLE configuration (steps 1 / draft 2), which this recipe replaces.

Checklist

  • Format your code according to the Format code with pre-commit.
  • Add unit tests according to the Run and add unit tests. Note: the affected dispatch path requires SM120-class hardware plus the DSV4 checkpoint; validated end-to-end on the reference hardware above. Happy to add a hardware-gated test if maintainers can point at the preferred harness for SM12x-only paths.
  • Update documentation according to Write documentations.
  • Provide accuracy and speed benchmark results according to Test the accuracy and Benchmark the speed.
  • Follow the SGLang code style guidance.

CI States

Latest PR Test (Base): ❌ Run #31072115451
Latest PR Test (Extra): ❌ Run #31072115243

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

efschu added a commit to efschu/htsglang that referenced this pull request Aug 3, 2026
…utation, sgl-project#471/sgl-project#33407/sgl-project#33312/sgl-project#33098/sgl-project#33271 ports, sgl-project#481 PP fixes

Eight commits, largest bundle this session:

- sgl-project#479 (two commits): the GGUF-MoE offload coverage test and its refusal
  message named MXFP4/type 39 as THE example of a ggml type with no MoE
  kernel -- sgl-project#398 made that false (type 39 has a full native kernel set on
  a wheel that carries it). Traced the ACTIVE DSV4-Flash MXFP4 path end to
  end and confirmed it is native, not an untraced fallback; the refusal
  message now names the layer's own declared types instead of a stale
  hardcoded example, so it cannot go stale again the same way.
  docs/dev/NOTE_479_mxfp4_active_driver_path.md has the trace.
- sgl-project#471: ports upstream sglang#32320 -- mask the SM120 SWA page-split to
  touched pages only, instead of the full page range.
  docs/dev/TICKET_471_masked_page_split.md.
- sgl-project#33407 port: SM120 sparse-MLA buckets the topk width instead of using
  it unbucketed, falling back to Triton outside the bucketed range.
- sgl-project#33312 port: DSpark draft head resolves and loads fused shared experts.
- sgl-project#33098 port: DSpark draft fills the EP token-accounting fields.
- sgl-project#481: three pipeline-parallel defects from the sgl-project#445 window fixed in
  server_args.py/resident_fraction.py/expert_stats.py.
- sgl-project#33271 port: auto-round GPTQ MoE completes the MoeWNA16 delegation.

FEATURE_CATALOG.md §8 conflict-risk as flagged, but no actual conflict --
this branch's base (a5eff26) is now several merges behind (sgl-project#520, sgl-project#523,
design-453 landed since), yet none of those touched §8, so git's 3-way
merge combined everything automatically. All 17 sections and all 22
previously-preserved additive paragraphs verified present.

**Explicit verification requested and confirmed**: the two "ancient
pre-existing" failures in tests/moe_offload/test_gguf_moe_offload.py
(test_materialize_declines_uncovered_ggml_type,
test_guard_still_refuses_an_unstaged_gguf_layer) that this session
bisected back to before this session even began -- sgl-project#479's fix to
expert_offload.py and the type-39 assumption in gguf.py directly
addresses their root cause. Full file run: 20 passed, 0 failed, both
confirmed GREEN now.

One test-isolation quirk found and run to ground, not a code bug: the
three flash_mla test files this branch adds/touches
(test_flash_mla_backends.py, test_flash_mla_page_split_mask_471.py,
test_flash_mla_sm120_topk_buckets.py) interfere with each other when run
in the same pytest process -- a shared Triton-decorated function object
resolves to JITFunction instead of the expected InterpretedFunction once
another file in the same process has already imported flash_mla_sm120.py
under real (non-interpret) conditions. Verified this is NOT a merge
regression: reproduced identically on the unmerged branch tip in
isolation (fresh archive, no other code involved), and every one of the
30 tests across the three files passes cleanly when its file is run
alone. Branch-inherent test-ordering fragility, not in scope for this
merge to fix.

Tests (CUDA_VISIBLE_DEVICES=99, PYTHONPATH pinned to this worktree):
- tests/moe_offload/test_gguf_moe_offload.py alone: 20 passed (was 18
  passed/2 failed before this merge, per this session's own bisection).
- The eight new/touched test files, run individually where the isolation
  quirk above requires it: all pass in full (dspark 442: 18; auto_round:
  17; dspark shared-expert 33312: 21; gguf mxfp4 479: 18; pp defects 481:
  26; flash_mla backends/topk-buckets/page-split-mask: 10/25/10 each
  alone).
- test/registered/unit/quantization/ + unit/models/ + spec/dspark/: 28
  failed both before and after (diffed by test ID, not count -- the
  pre-merge archive showed 35 because 7 of its failures were an artifact
  of an incomplete git-archive extraction missing sgl-kernel/, confirmed
  by re-checking those 7 IDs against the working tree directly). The
  remaining 28 are dspark_kernel_parity's CUDA-only subtests plus
  test_gguf_dequant_scratch_budget.py, both pre-named as pre-existing.
- test/registered/unit/layers/ + tests/moe_offload/: 3 failed (the
  pre-named test_deterministic_fp8_gemm.py x2 +
  test_flashinfer_workspace_zeroing.py x1), 1168 passed -- confirms
  test_gguf_moe_offload.py's contribution to this count is now 0 failures.
- test/registered/unit/distributed/ (full, server_args.py touched): 24
  failed, byte-identical established baseline.
- ruff check --select=F401,F821,UP037: 15 findings, all in
  expert_offload.py, all confirmed pre-existing (same set already
  verified at cc5a92e in the earlier sgl-project#494 merge) and outside this
  branch's two diff hunks (2112-2138, 2616-2648) -- auto_round.py and
  every other touched file clean.
- codespell: clean on all 21 touched/added files.

LingZ315 commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Independent reproduction on 8x RTX 5090 (SM120), TP=8.

Environment:

  • Model: deepseek-ai/DeepSeek-V4-Flash-0731 (local HF snapshot)
  • SGLang: 0.5.16
  • FlashInfer: 0.6.14
  • PyTorch: 2.11.0
  • Triton: 3.6.0
  • Python: 3.12.3
  • NVIDIA driver: 595.58.03
  • GPUs: 8x NVIDIA GeForce RTX 5090

Relevant launch configuration:

SGLANG_JIT_DEEPGEMM_PRECOMPILE=0 \
SGLANG_ENABLE_SPEC_V2=1 \
SGLANG_ENABLE_THINKING=1 \
HF_HUB_OFFLINE=1 TRANSFORMERS_OFFLINE=1 \
python3 -m sglang.launch_server \
  --trust-remote-code \
  --model-path /path/to/DeepSeek-V4-Flash-0731 \
  --tp 8 \
  --moe-runner-backend flashinfer_mxfp4 \
  --mem-fraction-static 0.92 \
  --speculative-algorithm DSPARK \
  --chunked-prefill-size 4096 \
  --cuda-graph-max-bs-decode 8 \
  --disable-flashinfer-autotune \
  --tool-call-parser deepseekv4 \
  --reasoning-parser deepseek-v4 \
  --enable-metrics \
  --enable-cache-report \
  --host 0.0.0.0 --port 8000

The target and bundled draft weights load successfully. SGLang reports:

Initialized DSpark draft runner. attention_backend=dsv4, model=DeepseekV4ForCausalLMDSpark, gamma=5, verify_num_draft_tokens=6

The warmup then crashes deterministically in _flash_mla_flashinfer -> flashinfer/mla/_sparse_mla_sm120.py:

TVM_FFI_ICHECK_GT(num_tokens, 64)
tvm.error.InternalError: Check failed: num_tokens > 64 (5 vs. 64):
Decode (num_tokens <= 64) must go through sparse_mla_sm120_decode_dsv3_2
or sparse_mla_sm120_decode_dsv4; got num_tokens=5

Forcing SGLANG_SM120_FLASHMLA_BACKEND=triton avoids this dispatch failure on the same host and allows the model to start and serve inference (with memory-related concurrency limits configured separately).

This confirms the PR's first boot-time failure on consumer SM120 at TP=8 as well as the reported TP=4 setup.

@hassellof

Copy link
Copy Markdown
Contributor Author

Thanks for the independent reproduction — that's a third SM120 topology now
(4x RTX PRO 6000 TP=4, 2x RTX PRO 6000 TP=2 via #33134, and your 8x RTX 5090
TP=8), and your trace is the same dispatch failure: the DSpark draft indexer
emits topk=192, which is in no instantiated CUTLASS decode bucket, so the
call falls through to the prefill orchestrator and trips its
num_tokens > 64 check with the draft's tiny token count.

Two notes that may save you time:

SGLANG_SM120_FLASHMLA_BACKEND=triton works but costs throughput. It
routes every sparse-MLA call to the Triton fallback, including normal decode
(topk=2048), which is the hot path. This PR keeps CUTLASS for every shape
that has an instantiation and only pads the non-instantiated widths
(192 → next bucket, -1 skip sentinels, scan capped via topk_length),
falling back to Triton for shapes that remain non-dispatchable. So you should
be able to drop the env var once this lands.

Unrelated to this dispatch bug, but it will bite you next on that config:
we found that DSpark draft depth gamma=5 — the value your log shows, which
is the checkpoint's own dspark_block_size default and what you get when the
flag is omitted — corrupts generations on SM120 under real load, while
gamma 3, 4, 6 and 7 are all clean. It's not a depth-monotonic effect;
gamma=5 specifically is the broken value, explicitly set or inferred. If you
see repetition loops or token salad after the dispatch fix, set
--speculative-dspark-block-size 4 explicitly. I'm writing that up as a
separate issue with the full sweep and repro; I'll link it here when it's
filed.

Our serving recipe for this hardware class, including the patch set and the
measured numbers, is at
https://github.com/ombori/deepseek-v4-flash-0731-sglang-4x-rtx-pro-6000 if
it's useful for your 5090 setup.

hassellof and others added 3 commits August 6, 2026 05:07
Adds hermetic (CPU-only, CUDA_VISIBLE_DEVICES-independent) coverage for the
topk-bucket padding + Triton-fallback dispatch this PR introduces:
bucket arithmetic (192 -> 512, instantiated widths self-bucket, >2048 has no
bucket, widths match the installed flashinfer decode table) and dispatch
behaviour via recorders around the CUTLASS kernel and the Triton fallback
(padded-to-512 call, -1 skip sentinel, topk_length capped to the true width,
split-K scratch sized to the padded width, and the three fall-back-to-Triton
geometries).

To make the pad target testable, the inline "next instantiated width" search
is extracted into a small pure helper, _next_topk_bucket().

Test authored by @efschu (github.com/efschu/htsglang); import paths adapted to
this tree's sglang.kernels.ops.attention layout.

Co-authored-by: efschu <efschu@users.noreply.github.com>
…t test

isort grouped flashinfer/torch third-party imports and the new
test/registered/ file needs a CI registry call (check-registered-tests
hook). Register it on base-b/1-gpu-small, matching the sibling SM120
kernel test.
@hassellof
hassellof force-pushed the fix/dspark-sm120-decode-dispatch branch from 86c83ea to 0f3e99a Compare August 6, 2026 03:25
@hassellof

Copy link
Copy Markdown
Contributor Author

Hi @ch-wan — could you add the run-ci label here? The only failing checks are the label gate itself (Missing required label 'run-ci'); no tests have actually run yet.

State of the PR:

  • Rebased onto current main — conflict-free; neither touched file (kernels/ops/attention/flash_mla_sm120.py, flash_mla_sm120_triton.py) changed upstream since the merge base.
  • Lint/isort clean; the bucket-dispatch test passes 15/15 locally. It is hermetic (CPU only, CUDA_VISIBLE_DEVICES=99, kernels replaced by recorders), so it needs no SM120 card to run in CI.

Since this was filed, three independent environments have hit the same boot crash — 8× RTX 5090 TP=8 (repro above), DGX Spark sm_121 (#33134), and the original 4× RTX PRO 6000 TP=4 — and #33614 carries this patch as a prerequisite ("Dspark + DeepSeek V4 Flash cannot start on DGX Sparks without it").

So on consumer/workstation Blackwell, DeepSeek's own recommended DSPARK recipe currently cannot boot without either this change or SGLANG_SM120_FLASHMLA_BACKEND=triton, which routes every sparse-MLA call — including normal decode at topk=2048 — through Triton and costs throughput on the hot path.

The port from the htsglang fork kept that fork's recorder seam and
geometry, and never ran green on this tree:

- The recorder patched fi.sparse_mla_sm120_decode_dsv4, but this tree's
  wrapper calls _sparse_mla_sm120_paged_attention, which JIT-builds the
  SM120 module before any Python-level dispatch -- the old seam is only
  reachable after a successful build on an SM12x card. Intercepting the
  paged-attention entry (imported per-call by _flash_mla_flashinfer)
  keeps the suite genuinely hermetic.
- _HEAD_DIM_V was 448; flashinfer hard-requires d_v == 512 on this path
  (_require_d_v_512), so every dispatch test died in the entry gate.
- The fork's decode-only wrapper sent batch > 64 to Triton; this tree
  hands prefill-sized batches to the CUTLASS prefill orchestrator via
  the same entry. The expectation now pins that (no decode scratch, no
  Triton detour).

All 15 cases pass on a card-less host (CUDA_VISIBLE_DEVICES=99, stock
flashinfer 0.6.15.post1, no JIT).
@guqiong96

guqiong96 commented Aug 22, 2026

Copy link
Copy Markdown

@hassellof Thanks for the PR that fixes this issue:
Exception: Capture cuda graph failed: Check failed: num_tokens > 64 (20 vs. 64) : Decode (num_tokens <= 64) must go through sparse_mla_sm120_decode_dsv3_2 or sparse_mla_sm120_decode_dsv4; got num_tokens=20
The fix has been applied in https://github.com/guqiong96/Lsglang/releases#release-lsglang-v.1.4.7 and later versions. The latest sglang 0.5.18 still hasn't fixed this issue, but with this PR it runs well.😅

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants