Skip to content

Reuse the static rollout decode graph across calls - #9077

Open
jinyouzhi wants to merge 3 commits into
deepspeedai:masterfrom
jinyouzhi:static-graph-reuse
Open

jinyouzhi wants to merge 3 commits into
deepspeedai:masterfrom
jinyouzhi:static-graph-reuse

Conversation

@jinyouzhi

@jinyouzhi jinyouzhi commented Nov 13, 2026 •

Copy link
Copy Markdown
Contributor

Summary

The default graph path (use_graph_capture=True without continuous_batch_size) currently rebuilds its DeepSpeedStaticCache, runs three warmup forwards and captures a new graph on every generate() call. In an RL loop that is a fixed setup cost per rollout round, paid even though the decode graph depends only on the decode shape, not on the prompt contents or the weight values.

This PR keeps the static path's graph and cache across calls, the same way #8732 / #9009 already do for the continuous path.

Before After
Per generate() call Allocate DeepSpeedStaticCache, 3 warmups, capture Eager prefill only; replay the retained graph
Recapture trigger Every call Graph signature changes: batch size, prompt width + max_new_tokens, device, dtype, or a parameter's address / shape / strides / dtype / device
Prompt width n/a (always recaptured) One graph serves every width with the same total length, since prefill runs eagerly and the decode mask is rebuilt per call
In-place weight updates n/a Replayed without recapture (same reasoning as #9009)
shutdown() Releases the continuous state Releases both the continuous and static states

Implementation

  • _continuous_graph_signature → _graph_signature, shared by both paths (no behaviour change).
  • Rebased onto master after Size HybridEngine rollout KV caches from local prefill shapes under AutoTP #9008 and Reuse continuous rollout graphs and release captured resources on shutdown #9009. The retained static cache is built as DeepSpeedStaticCache(module.config, max_cache_len=...) and, as on the continuous path, capture first runs one forward on the current stream so each layer is sized from the model's local (AutoTP-sharded) KV tensors before the side-stream warmup. _graph_signature keeps Reuse continuous rollout graphs and release captured resources on shutdown #9009's signature (parameters and buffers, normalized device index).
  • New _StaticGraphState / _get_static_graph_state / _capture_static_graph. The warmup + capture code is moved unchanged out of _generate_graph. Capture now runs on a zeroed cache before the prefill KV is loaded; the replayed values never depend on capture-time contents.
  • Each call resets the retained cache, zeros the 2-D mask, copies the prefill KV and rebuilds the 4-D mask before the first replay. The decode loop is unchanged.
  • Memory: the static cache and graph pool stay allocated between rollouts (as on the continuous path) until the shape changes or shutdown() is called. Documented in inference-engine.rst.

Tests

New accelerator test test_static_graph_is_reused_across_rollouts_with_the_same_decode_shape (Llama, sdpa). Every call is compared against model.generate():

  1. width 8 + 16 tokens → capture
  2. in-place parameter update, same shape → reuse; tokens must change
  3. width 12 + 12 tokens → reuse (same total length; catches a stale mask or KV from the previous call)
  4. width 8 + 8 tokens → recapture

Then it asserts exactly 2 captures, and that shutdown() frees at least the cache bytes.

Mutation check (each must fail the new test): not zeroing the mask on reuse → fails; recapturing every call → fails (capture count); skipping the prefill KV copy → fails (all three static tests).

Validation

1× NVIDIA H20 (driver 580.178.04), torch 2.13.0+cu130, Transformers 5.15.1:

CUDA_VISIBLE_DEVICES=7 DS_ACCELERATOR=cuda OMP_NUM_THREADS=1 HF_HUB_OFFLINE=1 \
  python -m pytest -q -p no:cacheprovider -o addopts= \
  tests/unit/v1/test_hybrid_engine_rollout_v1.py \
  tests/unit/runtime/rollout/test_hybrid_engine_rollout.py tests/unit/utils/test_static_cache.py
# 105 passed, 2 skipped (optional pretrained model, native inference extension)

After the rebase, 2× NVIDIA L20 (driver 580.173.02), torch 2.14.1+cu130, Transformers 5.17.0, same files: 147 passed, 4 skipped, 1 failed. The failure is TestZero3SynchronizedEarlyStop, which fails the same way on master on this machine (JIT build of transformer_inference fails).

CPU (DS_ACCELERATOR=cpu, same files): 78 passed, 29 skipped (accelerator-only cases).

Pre-commit passes on the three changed files.

Not covered

@tohtana tohtana added the needs-gpu-test GPU testing is recommended to validate the changes in this PR. label Nov 13, 2026
@tohtana tohtana removed the needs-gpu-test GPU testing is recommended to validate the changes in this PR. label Nov 14, 2026
@jinyouzhi
jinyouzhi marked this pull request as ready for review November 15, 2026 07:00
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 15, 2026 •

Copy link
Copy Markdown
Codex Review Summary: ✅ Completed

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-15T07:08:37.483953Z 9680221 Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 968022173a

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".


signature = self._graph_signature(module, batch_size, max_len, device, model_dtype)
state = self._static_graph_state
if state is None or state.signature != signature:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Coordinate recapture across tensor-parallel ranks

When inference tensor parallelism is active and only one rank's tensor address or layout changes (for example, for a rank-local shard), this rank-local condition makes that rank execute the initialization, warmup, and capture forwards while its peers immediately replay their retained graph. Those forwards contain TP collectives, so the ranks execute different collective counts and generation can hang; all-reduce a recapture flag over the inference TP group before entering this branch, and apply the same fix to the continuous sibling.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d5c3d71. Both graph paths now all-reduce (MAX) the recapture decision over the AutoTP group, so if any rank needs to capture, the whole group captures together. The new TestAutoTPGraphRecapture moves one parameter on rank 1 only, on the static and continuous paths. With the fix, both ranks capture twice and match generate(). Without the all-reduce, the test hangs.

state = self._static_graph_state
if state is None or state.signature != signature:
# Free the stale cache and graph pool before allocating replacements.
self._static_graph_state = None

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Drop the stale state before allocating its replacement

When a same-device call changes the batch size or total decode length, the local state variable still references the old _StaticGraphState, so clearing only self._static_graph_state does not release its cache or graph. The replacement cache, static buffers, and graph are therefore captured while both the old state and the new prefill cache remain live, which can transiently double this large allocation and OOM even when either state fits alone; set the local to None before allocating, as _get_continuous_graph_state already does.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d5c3d71: the local state is cleared before the replacement is allocated, as in _get_continuous_graph_state.

"""
from deepspeed.utils.static_cache import DeepSpeedStaticCache

signature = self._graph_signature(module, batch_size, max_len, device, model_dtype)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recapture when the module's training mode changes

When the same rollout is invoked once in training mode and later in eval mode (or vice versa) with a model whose forward branches on module.training, such as one with nonzero dropout, this reuse call sees the same tensor signature and replays kernels captured in the previous mode while only the eager prefill reflects the current mode. Before this change every static call recaptured, so decode can now silently retain or omit training-only operations; include the execution mode in the shared graph signature so the static and continuous siblings both recapture.

AGENTS.md reference: AGENTS.md:L19-L19

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in d5c3d71: module.training is part of the shared graph signature, so both paths capture again when the mode changes. The reuse test now switches to training mode and checks for a third capture.

Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>
Capture runs forwards that hold TP collectives, so a rank whose own graph
signature changed must not capture while its peers replay. Both graph
paths now all-reduce the recapture decision over the AutoTP group.

The graph signature also records module.training, so a graph captured in
one mode is not replayed in the other, and the static path drops its
stale state before allocating the replacement.

Signed-off-by: Jin, Youzhi <youzhi.jin@intel.com>

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants