You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
perf(sglang-atom): CUDA graphs for the Qwen3.8-Flash-Next MTP draft (decode + extend) - #2645
#2637 has merged; this branch is rebased onto main.
Summary
Qwen3.8-Flash-Next MTP on SGLang-ATOM runs every draft step eagerly. #2385 skips draft graph capture for Qwen4ExpForCausalLMNextN, citing the [3, N] mRoPE positions that native ATOM's DraftGraph cannot stage. SGLang's EAGLEDraftCudaGraphRunner / EAGLEDraftExtendCudaGraphRunner already carry mrope_positions buffers, so that reason does not hold on the SGLang path. As #2385 itself measured, the eager draft step is host-launch bound: one eager draft decode step costs about 8 ms, while the same step graphed costs about 0.7 ms. With MTP that overhead more than cancels the accepted tokens, so after #2418 MTP was slower than no MTP (TP2/EP1).
Both draft graphs are now captured whenever the Flash drafter runs (no flags; --disable-cuda-graph turns them off with every other graph):
Graph
Effect
Draft decode
Capture the Flash draft decode graph. The draft runner's out-of-graph hook no longer fills the target's _DECODE_GRAPH QSA buffers (TARGET_VERIFY replays from them); inside the captured draft forward the QSA metadata is rebuilt into the private _DRAFT_QSA buffers, so each draft step's positions / out_cache_loc are recomputed on replay.
Draft extend
Also graph the draft extend after every verify. SGLang's HIP gate only admits aiter multi-step / DSV4 / DSA backends, so the Flash drafter takes the plugin-built EAGLEDraftExtendCudaGraphRunner path DSV4 / GLM-5.2 already use (without their long pad-row seq_len fill).
Also fixes the plugin's draft-extend replay wrapper, which took only (self, forward_batch) while SGLang 0.5.20 calls execute(forward_batch, select_index); any draft-extend graph on this SGLang hit TypeError before.
Plugin-only: atom/plugin/register.py, atom/plugin/sglang/qwen4_exp_bridge.py. No SGLang change, no new environment variables.
Results
2x MI308X, Qwen3.8-Flash-Next-PTPC-FP8, sglang.bench_serving random (the #2418 harness: range 1.0, OSL 2048, num-prompts = 2 x concurrency, 1 warmup).
Not a recovered regression. Current main + #2637 + this PR on rocm/atom-dev:sglang-lateste021a3935c1c. Baseline = #2418 recipe (TP2/EP1, no MTP); MTP = TP2/EP2, EAGLE 4 steps, both graphs, ctx 132096, max-running 64, decode graph bs 64. The baseline matches #2418's published numbers (output throughput geomean -0.2%, every case within 3.4%), and MTP is measured on top of it:
MTP vs baseline: output throughput geomean +22.5%, TPOT -23.5%, ahead in 9/11 cases.
Draft steps, release-note launch (TP2/EP2, ctx 8192, max-running 8), ISL 1024 / OSL 2048, concurrency 1, on sglang-v0.5.20-nightly_20261012 (TPOT; no MTP TP2/EP1 on the same image: 6.4 ms):
steps
eager draft
decode graph
decode + extend graph
GSM8K 400, both graphs
2
12.37 ms
9.16 ms
5.90 ms
97.00%
3
13.57 ms
8.19 ms
6.01 ms
97.00% (accept 3.44, same as eager extend)
4
-
8.69 ms
5.17 ms
97.25%
5
-
-
5.02 ms
97.75%
Full #2418 sweep (29 cases, ISL 1024-129024, concurrency 1-32, 668/668 requests) on the nightly image, both graphs, output throughput geomean vs no MTP TP2/EP1: +14.5% (2 steps), +15.4% (4 steps), ahead in 25/29. Behind: 63488 x 16/32 and 129024 x 8/16 (-0.3% to -9.2%), where long-context, high-concurrency decode dominates and EP2 costs the target about 2.3 ms per step.
Rebased onto main (f30f8b0cc, which includes #2637); results below are on this tree.
mRoPE in the draft graphs. Confirmed with a probe on the draft runners: the decode graph's mrope_positions buffer was never written, so every draft step ran at RoPE position 1, and the draft-extend graph read the buffer it shares by name and shape with the target-verify runner. The same staleness existed in eager drafting: EAGLE draft_forward advances only the 1-D positions, so draft steps 1+ reused step 0's mRoPE.
Before each draft decode / draft-extend replay, stage SGLang's live mrope_positions (multimodal offsets kept; the 1-D positions on all three rows when SGLang computed none; rows past the batch zeroed): stage_draft_graph_mrope.
After every Flash draft decode forward, advance the caller's mRoPE together with the 1-D positions: advance_draft_decode_mrope, called from a draft_forward wrapper, because the eager and graph runners hand the model a copy of the batch. During capture the add is recorded in the graph. This also applies to eager drafting.
Verified in serving: after a decode replay the buffer holds the staged row + 3 (4 steps), and an in-graph recorder shows the draft RoPE at staged+1, +2, +3 on an image request (offset of -992 kept).
Flag parsing → no flags.ATOM_SGLANG_QWEN4_DRAFT_CG / ATOM_SGLANG_QWEN4_DRAFT_EXTEND_CG are removed (94cef79ce): with the mRoPE fix the graphs draft as well as eager drafting, and eager drafting is host-launch bound (slower than no MTP), so both graphs are always captured for the Flash drafter. The out-of-graph QSA hook now keys on the draft batch alone, so there is no second gate to disagree with the first.
Tests.tests/plugin/test_sglang_qwen4_exp_mtp.py covers staging (text broadcast, multimodal offsets, padding), the per-step advance, and that a draft decode batch never fills the target's graph metadata.
On the agent request, MTP with both graphs cuts TPOT by 27% against the best no-MTP launch (TP2/EP1); TTFT goes up by about 0.55 s (5.09 s to 5.63 s). Image requests accept the same with and without the draft graphs.
Accept length on few-prompt runs is noisy: two identical greedy runs diverge within the first tokens, and outputs that fall into repetition accept far more, so 4-8 prompt runs swing by about ±0.5. Removing the flags was checked with both builds served side by side (GPUs 2-3 and 4-5, same host load), 16 prompts, OSL 512, concurrency 1, release-note launch, two rounds:
build
text 1024 accept / TPOT
image 720p + 256 accept / TPOT
no flags (94cef79ce)
3.13, 3.10 / 4.74, 4.68 ms
3.07, 3.04 / 6.55, 6.91 ms
both flags set (399206b7e)
3.34, 3.10 / 4.74, 4.61 ms
2.97, 2.97 / 6.80, 7.00 ms
Reproduction
These are the commands behind the "Not a recovered regression" table (image rocm/atom-dev:sglang-lateste021a3935c1c, SGLang 0.5.20 94602c9c2b). The step-sweep and 29-case numbers used the same launches on rocm/atom-dev:sglang-v0.5.20-nightly_20261012, with this branch's two commits on ATOM a0527c9db (no #2637 needed there).
Every launch below puts the branch ahead of the image's ATOM with PYTHONPATH=/app/sglang/python:/atom. Confirm with python -c "import atom; print(atom.__file__)" -> /atom/atom/__init__.py. Run one server at a time; first start takes about 3.5 minutes.
Expected log: Capture target verify CUDA graph end, Capture draft decode CUDA graph end, and no Failed to enable draft-extend cuda graph in ATOM plugin. The plugin-built draft-extend runner is not counted in SGLang's startup timing line, so draft_extend=0.00 there is expected. KV pool: 1,698,944 tokens at 4 steps (2,229,568 at 2 steps).
Eager drafting (the behaviour before this PR) has no switch any more; --disable-cuda-graph disables every graph, the target's included.
Draft-step sweep: same command with --context-length 8192 --max-running-requests 8 --mem-fraction-static 0.70 --cuda-graph-max-bs-decode 8 (the release-note launch) and --speculative-num-steps {2..6}.
MTP requires --ep-size 2: with EP1 the BF16 drafter MoE (intermediate 320 per rank) has no CK kernel and the first request fails.
Runs automatically on every eligible PR before approval:
✅ Pre Checkin: Black, Ruff, catalog schema validation, non-GPU unit tests
Heavy model tests:
✅ Run after the PR is approved and Pre Checkin passes
✅ Run immediately when an approval review is submitted
✅ Can be requested before approval with labels
Label
Tests
ci:full
Run all heavy PR model tests: native ATOM, vLLM, and SGLang
ci:atom
Run native ATOM model accuracy tests
ci:vllm
Run ATOM vLLM OOT model accuracy tests
ci:sglang
Run ATOM SGLang model accuracy tests
Heavy jobs are skipped when the PR is not approved and no matching ci:* label is present.
Add labels via the sidebar or gh pr edit 2645 --add-label <label>
…prototype)
On top of the draft decode graph (ATOM_SGLANG_QWEN4_DRAFT_CG=1),
ATOM_SGLANG_QWEN4_DRAFT_EXTEND_CG=1 also graphs the draft extend that runs
after every verify.
- SGLang only captures draft extend on HIP for aiter multi-step / DSV4 / DSA
backends, so the Flash drafter takes the plugin-built
EAGLEDraftExtendCudaGraphRunner path that DSV4 / GLM-5.2 use. The QSA
metadata is rebuilt into _DRAFT_QSA inside the graph, as for draft decode.
The DSV4 / GLM-5.2 long seq_len fill for pad rows is not applied to Flash.
- The plugin's EAGLEDraftExtendCudaGraphRunner replay wrapper took only
(self, forward_batch); SGLang 0.5.20 calls execute(forward_batch,
select_index). Forward extra arguments in every branch.
Measured on rocm/atom-dev:sglang-v0.5.20-nightly_20261012 (ATOM a0527c9),
2x MI308X, TP2/EP2.
Release-note launch (ctx 8192, max-running 8), ISL 1024 / OSL 2048, conc 1:
| steps | decode graph only | decode + extend graph | GSM8K 400 |
|---|---|---|---|
| 2 | 9.16 ms | 5.90 ms | 97.00% |
| 3 | 8.19 ms | 6.01 ms | 97.00% (accept 3.44, same as eager extend) |
| 4 | 8.69 ms | 5.17 ms | 97.25% |
| 5 | - | 5.02 ms | 97.75% |
| 6 | - | 5.88 ms | 96.75% |
No MTP, TP2/EP1 on the same image: 6.4 ms.
Long-context launch (ctx 132096, max-running 64, decode graph bs 64,
mem 0.88), ROCm#2418 29-case sweep (ISL 1024-129024, conc 1-32, OSL 2048),
668/668 requests each. Output throughput geomean:
| | vs MTP off, TP2/EP1 | vs eager MTP, steps 2 | cases ahead of MTP off |
|---|---|---|---|
| steps 2 | +14.5% | +43.9% | 25/29 |
| steps 4 | +15.4% | +45.0% | 25/29 |
Cases behind MTP off: 63488 x 16/32 and 129024 x 8/16, -0.3% to -9.2%.
Multimodal (1080p image + ~3.8K-token text, conc 1-16): all answers
correct on both launches.
…e draft graph flags
SGLang's EAGLE draft decode / draft-extend graph runners capture
`mrope_positions` but refresh only the 1-D `positions` on replay, and the
buffer is shared by name and shape with other runners. The Flash drafter
reads the 3-row positions, so the decode graph ran every draft step at RoPE
position 1 and the extend graph read whatever target verify left behind.
Stage SGLang's live rows (multimodal offsets included; 1-D positions on all
three rows when SGLang computed none) before both replays.
EAGLE `draft_forward` also advances only the 1-D positions between draft
decode steps, and the runners hand the model a copy of the batch, so the
3-row positions stayed at step 0 (eager and graph). Advance the caller's
mRoPE after every Flash draft decode forward; in a graph capture the add is
recorded on the graph's buffer.
ATOM_SGLANG_QWEN4_DRAFT_CG / _DRAFT_EXTEND_CG are now registered in
atom.utils.envs and read there by both the capture gate and the QSA metadata
hook, which parsed them differently (`yes` / `on` enabled capture but not the
hook's target-buffer isolation).
mRoPE wrapper is lost when argmax patch replaces draft_forward
atom/plugin/register.py:725
[verified] On the legacy SGLang runner API without execute, register_ops_to_sglang() installs this wrapper and then calls _patch_sglang_eagle_v2_draft_argmax(), which replaces EagleDraftWorker.draft_forward outright. The mRoPE advancement is lost, while _atom_qwen4_draft_mrope_patched prevents reinstalling it. Flash draft steps therefore keep using step-0 mRoPE in eager execution and graph capture. Install the argmax replacement before capturing original_draft_forward so this wrapper composes with it.
…LANG_QWEN4_DRAFT_CG / _DRAFT_EXTEND_CG
With the draft mRoPE staged and advanced, the draft decode and draft-extend
graphs match eager drafting in accuracy and acceptance, and eager drafting
is host-launch bound (slower than no MTP). Capture both graphs whenever the
Flash drafter runs; --disable-cuda-graph still turns them off.
…for the graph bucket; order the legacy argmax patch
- prepare_qwen4_exp_decode_graph_metadata validates the Qwen4 config before
the draft-decode shortcut, so other EAGLE drafters on the ATOM backend keep
their normal attention tables.
- _ensure_draft_qsa covers the decode graph bucket when
--max-running-requests is omitted (32-row default vs. 64-row capture).
- On legacy SGLang, install the argmax draft_forward replacement before the
mRoPE wrapper so the wrapper composes with it.
zovonoir
changed the title
perf(sglang): CUDA graphs for the Qwen3.8-Flash-Next MTP draft (decode + extend)
perf(sglang-atom): CUDA graphs for the Qwen3.8-Flash-Next MTP draft (decode + extend)
Oct 21, 2026
@valarLip — friendly ping for a review. This has been open since Oct 20; the last automated review round reported no open issues and only flagged the shared-buffer GPU replay for human validation.
State
Both draft CUDA graphs (decode + extend) are now captured by default for the Flash drafter. No flags and no SGLang change: atom/plugin/register.py and atom/plugin/sglang/qwen4_exp_bridge.py only.
Fixes the plugin's draft-extend replay wrapper, which took (self, forward_batch) while SGLang 0.5.20 calls execute(forward_batch, select_index).
Draft step cost drops from ~8 ms eager to ~0.7 ms graphed, which is what makes MTP a net win.
GSM8K 400 97.0-97.75% across configurations; multimodal (1080p + ~3.8K text) correct at concurrency 1-16.
Deliberately left out of scope (documented under "Not covered" in the PR body)
ROCm + forced SGLANG_ENABLE_WAR_BARRIER=1 + the Flash draft-extend graph. The WAR barrier is off by default on ROCm (is_cuda() or SGLANG_ENABLE_WAR_BARRIER), so this combination needs the event published after the draft-extend replay; the fix is a follow-up.
MTP still requires --ep-size 2 (the BF16 drafter MoE at TP2/EP1 has no CK kernel for intermediate 320).
The reason will be displayed to describe this comment to others. Learn more.
LGTM, approving. Keeping this plugin-only makes sense, since SGLang's draft runners already carry mrope_positions. The stale-mRoPE issue is now covered in both modes: the replay wrappers stage the live rows (multimodal offsets included), and the draft_forward wrapper advances them per step for eager drafting. _DRAFT_QSA keeps the target-verify graph metadata untouched, and the *args, **kwargs passthrough also fixes the execute(forward_batch, select_index)TypeError for the other draft-extend users. Tracking the WAR-barrier caveat as a follow-up is fine, since ROCm doesn't enable that barrier by default.
One optional thought, not blocking. ATOM_SGLANG_V4_DISABLE_DRAFT_EXTEND_CG still turns off the Flash draft-extend graph, but the Flash draft decode graph can now only be turned off with --disable-cuda-graph, which drops the target graphs too. If you ever need to bisect a draft-graph issue in production, letting ATOM_SGLANG_V4_DISABLE_DRAFT_CG also cover the Flash drafter would give you a cheap escape hatch.
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
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
Qwen3.8-Flash-Next MTP on SGLang-ATOM runs every draft step eagerly. #2385 skips draft graph capture for
Qwen4ExpForCausalLMNextN, citing the[3, N]mRoPE positions that native ATOM'sDraftGraphcannot stage. SGLang'sEAGLEDraftCudaGraphRunner/EAGLEDraftExtendCudaGraphRunneralready carrymrope_positionsbuffers, so that reason does not hold on the SGLang path. As #2385 itself measured, the eager draft step is host-launch bound: one eager draft decode step costs about 8 ms, while the same step graphed costs about 0.7 ms. With MTP that overhead more than cancels the accepted tokens, so after #2418 MTP was slower than no MTP (TP2/EP1).Both draft graphs are now captured whenever the Flash drafter runs (no flags;
--disable-cuda-graphturns them off with every other graph):_DECODE_GRAPHQSA buffers (TARGET_VERIFY replays from them); inside the captured draft forward the QSA metadata is rebuilt into the private_DRAFT_QSAbuffers, so each draft step's positions /out_cache_locare recomputed on replay.EAGLEDraftExtendCudaGraphRunnerpath DSV4 / GLM-5.2 already use (without their long pad-rowseq_lenfill).Also fixes the plugin's draft-extend replay wrapper, which took only
(self, forward_batch)while SGLang 0.5.20 callsexecute(forward_batch, select_index); any draft-extend graph on this SGLang hitTypeErrorbefore.Plugin-only:
atom/plugin/register.py,atom/plugin/sglang/qwen4_exp_bridge.py. No SGLang change, no new environment variables.Results
2x MI308X,
Qwen3.8-Flash-Next-PTPC-FP8,sglang.bench_servingrandom (the #2418 harness: range 1.0, OSL 2048, num-prompts = 2 x concurrency, 1 warmup).Not a recovered regression. Current
main+ #2637 + this PR onrocm/atom-dev:sglang-lateste021a3935c1c. Baseline = #2418 recipe (TP2/EP1, no MTP); MTP = TP2/EP2, EAGLE 4 steps, both graphs, ctx 132096, max-running 64, decode graph bs 64. The baseline matches #2418's published numbers (output throughput geomean -0.2%, every case within 3.4%), and MTP is measured on top of it:MTP vs baseline: output throughput geomean +22.5%, TPOT -23.5%, ahead in 9/11 cases.
Draft steps, release-note launch (TP2/EP2, ctx 8192, max-running 8), ISL 1024 / OSL 2048, concurrency 1, on
sglang-v0.5.20-nightly_20261012(TPOT; no MTP TP2/EP1 on the same image: 6.4 ms):Full #2418 sweep (29 cases, ISL 1024-129024, concurrency 1-32, 668/668 requests) on the nightly image, both graphs, output throughput geomean vs no MTP TP2/EP1: +14.5% (2 steps), +15.4% (4 steps), ahead in 25/29. Behind: 63488 x 16/32 and 129024 x 8/16 (-0.3% to -9.2%), where long-context, high-concurrency decode dominates and EP2 costs the target about 2.3 ms per step.
Accuracy / multimodal. GSM8K 400 (
run_eval, chat, thinking off) 97.0-97.75% across configurations, eager 97.75%. 1080p image + ~3.8K-token text, concurrency 1-16: every answer correct.Review follow-up (
b5809b6df,94cef79ce)Rebased onto
main(f30f8b0cc, which includes #2637); results below are on this tree.mRoPE in the draft graphs. Confirmed with a probe on the draft runners: the decode graph's
mrope_positionsbuffer was never written, so every draft step ran at RoPE position 1, and the draft-extend graph read the buffer it shares by name and shape with the target-verify runner. The same staleness existed in eager drafting: EAGLEdraft_forwardadvances only the 1-Dpositions, so draft steps 1+ reused step 0's mRoPE.mrope_positions(multimodal offsets kept; the 1-D positions on all three rows when SGLang computed none; rows past the batch zeroed):stage_draft_graph_mrope.advance_draft_decode_mrope, called from adraft_forwardwrapper, because the eager and graph runners hand the model a copy of the batch. During capture the add is recorded in the graph. This also applies to eager drafting.Flag parsing → no flags.
ATOM_SGLANG_QWEN4_DRAFT_CG/ATOM_SGLANG_QWEN4_DRAFT_EXTEND_CGare removed (94cef79ce): with the mRoPE fix the graphs draft as well as eager drafting, and eager drafting is host-launch bound (slower than no MTP), so both graphs are always captured for the Flash drafter. The out-of-graph QSA hook now keys on the draft batch alone, so there is no second gate to disagree with the first.Tests.
tests/plugin/test_sglang_qwen4_exp_mtp.pycovers staging (text broadcast, multimodal offsets, padding), the per-step advance, and that a draft decode batch never fills the target's graph metadata.Results. 2x MI308X,
rocm/atom-dev:sglang-lateste021a3935c1c, TP2/EP2, EAGLE 4 steps, concurrency 1.Release-note launch (ctx 8192); text is
bench_servingrandom 1024/2048:Long-context launch (ctx 132096, the launch below), after the fix:
bench_servingimage dataset (720p image + 256 tokens, OSL 512), two runsOn the agent request, MTP with both graphs cuts TPOT by 27% against the best no-MTP launch (TP2/EP1); TTFT goes up by about 0.55 s (5.09 s to 5.63 s). Image requests accept the same with and without the draft graphs.
Accept length on few-prompt runs is noisy: two identical greedy runs diverge within the first tokens, and outputs that fall into repetition accept far more, so 4-8 prompt runs swing by about ±0.5. Removing the flags was checked with both builds served side by side (GPUs 2-3 and 4-5, same host load), 16 prompts, OSL 512, concurrency 1, release-note launch, two rounds:
94cef79ce)399206b7e)Reproduction
These are the commands behind the "Not a recovered regression" table (image
rocm/atom-dev:sglang-lateste021a3935c1c, SGLang 0.5.2094602c9c2b). The step-sweep and 29-case numbers used the same launches onrocm/atom-dev:sglang-v0.5.20-nightly_20261012, with this branch's two commits on ATOMa0527c9db(no #2637 needed there).Tree and container
Every launch below puts the branch ahead of the image's ATOM with
PYTHONPATH=/app/sglang/python:/atom. Confirm withpython -c "import atom; print(atom.__file__)"->/atom/atom/__init__.py. Run one server at a time; first start takes about 3.5 minutes.Baseline: no MTP, TP2/EP1 (#2418 recipe)
Expected log:
KV Cache is allocated. dtype: torch.bfloat16, #tokens: 3578944.MTP with draft CUDA graphs: TP2/EP2, EAGLE 4 steps, long context
Expected log:
Capture target verify CUDA graph end,Capture draft decode CUDA graph end, and noFailed to enable draft-extend cuda graph in ATOM plugin. The plugin-built draft-extend runner is not counted in SGLang's startup timing line, sodraft_extend=0.00there is expected. KV pool: 1,698,944 tokens at 4 steps (2,229,568 at 2 steps).--disable-cuda-graphdisables every graph, the target's included.--context-length 8192 --max-running-requests 8 --mem-fraction-static 0.70 --cuda-graph-max-bs-decode 8(the release-note launch) and--speculative-num-steps {2..6}.--ep-size 2: with EP1 the BF16 drafter MoE (intermediate 320 per rank) has no CK kernel and the first request fails.Benchmark (the #2418 harness)
MTP acceptance is the mean of the server log's
accept len:values printed during each case.Accuracy
Do not use
sglang.test.few_shot_gsm8kfor this model: it sends prompts without the chat template, and the model answers<|im_end|>immediately.Not covered
Test plan
main+ fix(sglang-atom): fill the per-step GDN prefill indices for Qwen3.8-Flash-Next #2637, latest image