perf(frozen_kv_mtp): capture the assistant seed step with CUDA graph (+6.8% chat tok/s, +4.4% summ tok/s) - #27
Draft
pyc96 wants to merge 1 commit into
Conversation
The recurrent draft loop has been captured by FrozenKVMTPCudaGraphRunner
since the FROZEN_KV_MTP V1 worker was introduced, but the assistant
seed step (a single decode-shape forward of the assistant after target
prefill and after each verify) ran EAGER. That's roughly 20-25 % of
decode wall time on speculative_num_steps=3 because the seed step
happens twice per scheduler iter (after prefill once, after every
verify) and it's the same shape as one captured loop iteration.
This PR captures the seed step as a separate CUDA graph per supported
batch size (29 sizes on the campaign workload). At replay time:
* draft_model_runner.forward runs the captured graph
* topk_p / topk_index / hidden_states are written to static output
buffers
* the worker copies the outputs into the new FrozenKVMTPDraftInput
so the next iter's recurrent loop reads them via spec_info.*
Implementation:
* python/sglang/srt/speculative/frozen_kv_mtp_cuda_graph_runner.py:
- New FrozenKVMTPSeedInputBuffers dataclass (input_ids, hidden_states,
positions, seq_lens + req_pool_indices + output buffers for
topk_p / topk_index / hidden_states).
- New methods: _init_seed_buffers, _capture_seed,
_capture_one_seed_graph, can_run_seed, replay_seed.
- Capture happens in __init__ AFTER the recurrent loop graphs are
captured. Catches Exception (not just RuntimeError) so any
failure cleanly falls back to eager seed without blocking server
startup.
- Env var SGLANG_FROZEN_KV_MTP_DISABLE_SEED_CG=1 skips seed capture
(for A/B benchmarking).
* python/sglang/srt/speculative/frozen_kv_mtp_worker.py:
- _run_assistant_seed_step uses replay_seed when can_run_seed(bs)
AND no multimodal embeds AND no mrope_positions. Stitches the
seed graph outputs onto the FrozenKVMTPDraftInput (topk_p,
topk_index, hidden_states) instead of running the eager forward.
- Eager fallback path preserved (still runs when seed graph
unavailable, fast-path predicates fail, or env var is set).
Validation (google/gemma-4-31B-it, H100 TP=2, triton, FROZEN_KV_MTP
NEXTN 3/4/1, 80 prompts, warmup 2, seed 1, --disable-overlap-schedule):
Metric | seed CG OFF | seed CG ON | delta
------------------|-------------:|-------------:|----------
chat tok/s | 1466 | **1566** | **+6.8 %**
chat medTPOT | 34.0 ms | **32.1 ms** | **-5.7 %**
chat medTTFT | 2829 ms | **2778 ms** | **-1.8 %**
summ tok/s | 428 | **447** | **+4.4 %**
summ medTPOT | 24.8 ms | **23.8 ms** | **-4.0 %**
summ medTTFT | 80956 ms | **77624 ms** | **-4.1 %**
chat accept_len | 3.135 | 3.128 | tied
summ accept_len | 3.142 | 3.139 | tied
Per-prompt parity (20 greedy prompts, seed CG ON vs OFF):
match_rate = 19/20 = 0.95
(single mismatch in a long output, semantically equivalent;
bf16 numerical drift from fp32 accumulation differences inside
the captured kernel)
Server log on init (visible to ops):
[info] Capture Frozen-KV MTP draft cuda graph begin.
[info] Capture Frozen-KV MTP seed cuda graph begin.
[info] Capture Frozen-KV MTP seed cuda graph end (captured 29 batch sizes).
[info] Capture Frozen-KV MTP draft cuda graph end.
Stack base: pyc/feat-gemma4-ultimate-v2 (PR #21)
Co-authored-by: Claude
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
Captures the assistant seed step in FROZEN_KV_MTP with CUDA graph. Previously only the recurrent draft loop was captured; the seed step (one decode-shape forward of the assistant) ran eager and cost ~20-25 % of decode wall time at
speculative_num_steps=3.Measured: +6.8 % chat tok/s, +4.4 % summ tok/s, ~5 % TPOT reduction on Gemma-4 31B-it MTP H100 TP=2. accept_length unchanged.
What the seed step is and why it was eager
Per scheduler iter, the FROZEN_KV_MTP V1 worker runs (in order):
speculative_num_steps - 1iters) — captured byFrozenKVMTPCudaGraphRunnerThe seed step is one decode-shape forward of the assistant model that seeds
topk_p/topk_index/hidden_stateson theFrozenKVMTPDraftInputfor the recurrent loop. It's structurally identical to one iter of the recurrent loop, but runs throughdraft_model_runner.forward()which has its own cuda graph disabled (the worker forcesdisable_cuda_graph=Truefor the draft TpModelWorker because the recurrent loop captures its own outer graph).Result: the eager seed forward is launched twice per scheduler iter, dominates ~20-25 % of decode wall time, and is invisible to the existing capture pipeline.
What this PR does
python/sglang/srt/speculative/frozen_kv_mtp_cuda_graph_runner.pyFrozenKVMTPSeedInputBuffersdataclass (input_ids + hidden_states inputs + topk_p / topk_index / hidden_states outputs). New methods_init_seed_buffers,_capture_seed,_capture_one_seed_graph,can_run_seed,replay_seed. Captures at the same batch sizes as the recurrent loop (29 sizes on the campaign workload). Falls back to eager seed silently if capture fails. Env varSGLANG_FROZEN_KV_MTP_DISABLE_SEED_CG=1skips capture for A/B benchmarking.python/sglang/srt/speculative/frozen_kv_mtp_worker.py_run_assistant_seed_stepusesreplay_seedwhencan_run_seed(bs)AND no multimodal embeds AND no mrope_positions. Otherwise the eager path runs as before. Stitches seed graph outputs (topk_p,topk_index,hidden_states) onto the newFrozenKVMTPDraftInput.The two paths are predicate-disjoint, so the eager fallback is 100 % unchanged for any caller that doesn't qualify.
Measured
google/gemma-4-31B-it, H100 TP=2, triton, FROZEN_KV_MTP NEXTN 3/4/1, 80 prompts, warmup 2, seed 1,--disable-overlap-schedule:Correctness — per-prompt parity (20 greedy prompts, temp=0)
{"total": 20, "matched": 19, "mismatched": 1, "match_rate": 0.95}The single mismatch is a long output that diverges after ~50 tokens (semantically equivalent; bf16 numerical drift from fp32 accumulation differences inside the captured kernel — same drift pattern documented in PR #19 and #20).
Server log evidence (init)
Reproducer
Stack base
pyc/feat-gemma4-ultimate-v2(PR #21)CI States
Latest PR Test (Base): ❌ Missing
run-cilabel -- add it to run CI tests.Latest PR Test (Extra): ❌ Blocked --
run-ciis required first.