Repository navigation
[Perf][DSv4.1] Capture the decoder replay graphs on the model graphs' stream - #63155
Conversation
… stream Every CudaGraphManager.capture() enters graph_capture(), which creates a new stream, and the caching allocator reuses a free block only on the stream that freed it. The decoder replay graphs, captured after the model graphs into the same global graph pool, therefore cannot reuse the model graphs' free pool blocks and reserve a second pool's worth of their own. graph_capture() now takes an optional stream, CudaGraphManager.capture() records the stream it captured on, and the runner hands the model graphs' stream to capture_inner_cudagraphs(), which DeepSeek-V4.1 passes on to the replay graphs. Other captures keep their own stream. A replay graph runs in a model graph's eager break after gathering its inputs into static buffers, and the model graph then reads only the break's own output buffers, so no model graph tensor in the pool is live while it runs. Aux hidden states captured before the replay layers would be; with those, the replay graphs keep a stream of their own. Signed-off-by: Qing <xqoasis@gmail.com>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
|
/ci run |
|
✅ @xqoasis, CI is now available for this PR.
|
|
✅ Triggered Buildkite CI #97422 for commit ℹ️ This PR is 208 commits behind upstream |
Return a stream-bearing context from the graph_capture mocks and provide capture_stream in the V2 runner fixture. Check the complete inner-capture argument forwarding while preserving the ROCm graph-pool setting. Validation: 53 tests pass on B200 with the real root cleanup callback restored because the existing runtime lacks tblib. Standard root conftest loading remains unverified. Ruff check and format checks pass. Co-authored-by: Codex Signed-off-by: qing.xiao <qing.xiao@bytedance.com>
|
Hi @xqoasis, the pre-commit checks have failed. Please run: uv pip install pre-commit>=4.5.1
pre-commit install
pre-commit run --all-filesThen, commit the changes and push to your branch. For future commits, |
Assert that the runner's CUDA graph manager is initialized before reading capture_stream in the argument-forwarding assertion. This preserves the optional production type while satisfying mypy's union-attr check. Validation: the original file reproduces the CI mypy error, and the fixed file passes mypy 1.17 with Python 3.11 target and the project configuration. Cache writes were disabled to avoid a mypy cache serialization crash. Five capture_model cases pass with GPUs hidden and the real root cleanup callback restored; full root conftest loading still requires tblib. Ruff check and format checks pass. Co-authored-by: Codex Signed-off-by: qing.xiao <qing.xiao@bytedance.com>
|
/ci run |
|
✅ Triggered Buildkite CI #97667 for commit ℹ️ This PR is 81 commits behind upstream |
Overview
Capture the DeepSeek-V4.1 decoder replay graphs on the model graphs' capture stream, so that they reuse the model graphs' free blocks in the shared graph pool instead of reserving a second pool. Fixes #62965.
Claims
Validation
GB200, DeepSeek-V4.1-Flash, 1P1D, prefill DP4,
max_model_len16k, breakable PIECEWISE graphs with capture sizes up to 16384 tokens, DSpark on. Two fresh service starts per arm. Measured on a v0.32.0-based build with this change; see Details for what differs onmain.main)torch.cuda.memory._snapshot()segments at the end ofcapture_model(), grouped by(segment_pool_id, stream), show the replay graphs' own pool: 5.18 GiB reserved, none of it in use. After the change, the replay capture adds no graph pool segment.No test is added: CI has no runner for DeepSeek-V4.1-Flash with replay graphs, and the existing replay capture test (
tests/models/test_deepseek_v41_decoder_replay_layers.py) callscapture_replay_graphs()without a stream, so it keeps today's behavior.Details
Root cause. Since #59532,
DecoderReplayCudaGraphManagercaptures the replay graphs after the model graphs, in aCudaGraphManager.capture()call of its own. Everycapture()entersgraph_capture(), which creates a new stream. Both managers capture into the global graph pool, but the caching allocator reuses a free block only on the stream that freed it, so the replay graphs cannot use the model graphs' free pool blocks and reserve their own.Change.
graph_capture(device, stream=None)reusesstreamwhen one is given.CudaGraphManager.capture(..., stream=None)captures on it and records the stream it used incapture_stream.GPUModelRunner.capture_model()passes the model graphs'capture_streamtoModelState.capture_inner_cudagraphs(), and DeepSeek-V4.1 forwards it tocapture_replay_graphs().Why sharing is safe. A replay graph runs inside an eager break of a model graph.
DecoderReplayCudaGraphManager.run()first gathers the layer inputs into its staticinput_buffers, then replays. After the break, the model graph reads only the break's own output buffers (DecoderReplayLayers._break_outputs), which live outside the graph pool. KV caches and the replay batch metadata are persistent. So no model graph tensor in the pool is still needed while a replay graph runs.The one exception is aux hidden states captured before the replay layers: the forward holds them across the break.
DecoderReplayLayers.has_early_aux()reports whether any aux layer id is below the replay start, and in that case the replay graphs keep their own stream. For DSpark on DeepSeek-V4.1-Flash, the aux layers (37-39) are produced inside the replay layers (from layer 20 on), so the fallback does not apply there.Differences from the measured build. The port to
mainalso forwards the stream through the specialized decode model state'scapture_inner_cudagraphs()override (#61868), and thegraph_capture()change sits next to #57481's edits. The early-aux fallback is new in this PR. It returnsFalsein the measured configuration, so the measured path is unchanged.Alternatives.
graph_capture()call. It would also let the replay graphs reuse the model graphs' pool, but it puts the speculator capture on that stream as well. The speculator runs after the target forward and reads its outputs, so it needs its own lifetime argument. Passing the stream only to the graphs replayed inside the model graphs limits the change to the case argued above.decoder_replay_cudagraph_capture_sizesshrink the replay pool proportionally, but larger replay batches then run eagerly.Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.