Skip to content

[Perf][DSv4.1] Capture the decoder replay graphs on the model graphs' stream - #63155

Merged
njhill merged 6 commits into
vllm-project:mainfrom
xqoasis:qing/replay-graphs-shared-stream
Oct 27, 2026
Merged

njhill merged 6 commits into
vllm-project:mainfrom
xqoasis:qing/replay-graphs-shared-stream

Conversation

@xqoasis

@xqoasis xqoasis commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

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

  • DeepSeek-V4.1-Flash prefill on GB200 (DP4, breakable PIECEWISE graphs up to 16384 tokens): graph capture memory drops from 15.52 GiB to 9.88 GiB per rank, and the replay capture no longer adds a graph pool segment.
  • No accuracy or capture-time change beyond run-to-run noise (table below).
  • Only the replay graphs move to the shared stream. The speculator and encoder captures, and Model Runner V1, keep a new stream per capture.

Validation

GB200, DeepSeek-V4.1-Flash, 1P1D, prefill DP4, max_model_len 16k, 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 on main.

separate streams (main) shared stream (this PR)
prefill graph capture memory per rank 15.52 / 15.52 GiB 9.88 / 9.88 GiB
capture time 36 / 36 s 36 / 37 s
GSM8K (1319, greedy) 97.50 / 97.42 97.57 / 97.12
  • Before the change, torch.cuda.memory._snapshot() segments at the end of capture_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.
  • First-token top-20 logprobs on 64 prompts of 1k-7k tokens (prefix cache off, concurrency 1 and 16) differ across arms by as much as across two starts of the same arm. At concurrency 1: first token equal in 59-62/64 across arms vs. 60-61/64 within an arm; median |Δlogprob| of the first token 0.05-0.06 in both cases.

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) calls capture_replay_graphs() without a stream, so it keeps today's behavior.

Details

Root cause. Since #59532, DecoderReplayCudaGraphManager captures the replay graphs after the model graphs, in a CudaGraphManager.capture() call of its own. Every capture() enters graph_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) reuses stream when one is given.
  • CudaGraphManager.capture(..., stream=None) captures on it and records the stream it used in capture_stream.
  • GPUModelRunner.capture_model() passes the model graphs' capture_stream to ModelState.capture_inner_cudagraphs(), and DeepSeek-V4.1 forwards it to capture_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 static input_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 main also forwards the stream through the specialized decode model state's capture_inner_cudagraphs() override (#61868), and the graph_capture() change sits next to #57481's edits. The early-aux fallback is new in this PR. It returns False in the measured configuration, so the measured path is unchanged.

Alternatives.

  • [Core] Reuse one CUDA graph capture stream per device #59537 caches one capture stream per device for every 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.
  • Capturing the replay graphs nested inside the model graph capture, as [Feat][Model] DeepSeek-V4.1 decoder-side SWA bounded replay #56752 did, also shares the pool, but then a replay graph is captured inside one model graph size and replayed inside others.
  • Smaller decoder_replay_cudagraph_capture_sizes shrink the replay pool proportionally, but larger replay batches then run eagerly.

Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (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.

… 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>
@github-actions

Copy link
Copy Markdown

👋 Hi! Thank you for contributing to the vLLM project.

💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in #pr-reviews, coordinate on features in #feat- channels, or join special interest groups in #sig- channels.

PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment /ci run for upstream CI or /amd-ci run for AMD CI only whenever CI signals are needed.

Once the PR is approved or has the ready label, the PR author can also use the corresponding /ci run, /ci retry, and /ci cancel commands, or their /amd-ci variants. New commits do not start upstream CI automatically.

If you have any questions, please reach out to us on Slack at https://slack.vllm.ai.

Agent Guidelines

IMPORTANT: 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.

🚀

@mergify mergify Bot added deepseek Related to DeepSeek models DSv4.1 Related to DeepSeek-V4.1 models nvidia mrv2 Model Runner V2 specific labels Oct 22, 2026
@xqoasis
xqoasis marked this pull request as ready for review October 23, 2026 06:11

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@njhill njhill left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @xqoasis!

@github-project-automation github-project-automation Bot moved this to Ready in NVIDIA Oct 26, 2026
@njhill njhill added the ready ONLY add when PR is ready to merge/full CI is needed label Oct 26, 2026
@njhill

njhill commented Oct 26, 2026

Copy link
Copy Markdown
Member

/ci run

@ZJY0516 ZJY0516 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@github-actions

Copy link
Copy Markdown

✅ @xqoasis, CI is now available for this PR.

  • /ci run starts upstream CI; /amd-ci run starts AMD CI only.
  • Run commands test your branch with every commit currently on its upstream target branch. If it is behind, the bot merges the target branch into it first (pull before pushing again). Append --allow-stale to a run command to test an outdated branch as is, at your own risk.
  • /ci retry retries failed jobs in the CI build for the current PR head. If the current head has no CI build, it starts a new CI build for the current head containing only jobs that failed in the latest earlier CI build for this PR.
  • /amd-ci retry retries failed jobs in AMD CI for the current PR head. Use /amd-ci run when the current head has no AMD CI build.
  • /ci cancel cancels scheduled or running CI builds for this PR branch; /amd-ci cancel does the same for AMD CI only.

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #97422 for commit db3696e4fc3e.

ℹ️ This PR is 208 commits behind upstream main. Merged upstream main into this branch as db3696e4fc3e before starting CI. Pull the branch before pushing more changes.

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>
@mergify

mergify Bot commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

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-files

Then, commit the changes and push to your branch.

For future commits, pre-commit will run automatically on changed files before each commit.

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>
@njhill

njhill commented Oct 27, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #97667 for commit c91b1d28cf72.

ℹ️ This PR is 81 commits behind upstream main. Merged upstream main into this branch as c91b1d28cf72 before starting CI. Pull the branch before pushing more changes.

@njhill
njhill merged commit 274a5e1 into vllm-project:main Oct 27, 2026
261 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in NVIDIA Oct 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deepseek Related to DeepSeek models DSv4.1 Related to DeepSeek-V4.1 models mrv2 Model Runner V2 specific nvidia ready ONLY add when PR is ready to merge/full CI is needed

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

[Feature]: Capture the DeepSeek-V4.1 decoder replay graphs on the model graphs' stream so they share its graph pool

4 participants