Add CUDA graph support for HybridEngine generation - #8271
Merged
Conversation
Token generation in the HybridEngine is bound by CPU work, not by the GPU. A single decode step issues on the order of 1300 kernel launches, and the kernels finish well before the host can queue the next step. Measured on opt-1.3b with an H100, per-decode-step latency is essentially flat from batch 1 to batch 32 (5.00ms -> 5.37ms) while the GPU work grows 32x, and the model forward accounts for 4.52ms of the 5.22ms step. Capture the decode forward into CUDA graphs, behind a new opt-in `hybrid_engine.enable_cuda_graph` config flag. Two properties of the inference kernels shape the design. The kernels read the current sequence length from a host-side counter (`InferenceContext::current_tokens()`) and pass it to kernels as a launch parameter, which graph capture freezes, so one graph is captured per decode position rather than one overall. That counter is also advanced from host code and is not reachable from Python, so a replay leaves it behind and an eager decode step after a replay would use a stale sequence length. Eager and replayed steps therefore must not be mixed within a sequence, and the choice is made once per sequence in `begin_sequence()` from the pinned generation length. Graphs are refused, with a warning, where captured pointers would not stay valid: ZeRO stage 3 gathers parameters into fresh buffers per generate, `release_inference_cache` frees the buffers graphs write into, and inference tensor parallelism is untested. Capture failures fall back to eager execution rather than failing the job. Measured on opt-1.3b, ZeRO-2, batch 8, 256-token prompt, 128 new tokens, single H100, over an RLHF-style loop: generate 676.7ms -> 360.8ms (1.88x) iteration 812.8ms -> 503.5ms (1.61x) Generated tokens are unchanged: 1024/1024 token agreement with the eager path across all 8 sequences. Peak memory grows by 132 MiB for 127 captured positions. The first generation captures the graphs and is a one-time cost of about 12s. Signed-off-by: Zhipeng Wang <zhipeng.rainbowserie@gmail.com>
|
Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits. |
sfc-gh-truwase
approved these changes
Aug 19, 2026
github-merge-queue
Bot
removed this pull request from the merge queue due to failed status checks
Aug 19, 2026
Merged
via the queue into
deepspeedai:master
with commit Aug 19, 2026
80c8e5b
15 of 19 checks passed
Collaborator
|
Is it possible to change flag name from |
Collaborator
Author
Let me submit another PR to change it. Thanks for flagging it out:) |
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.
Motivation
Token generation dominates a HybridEngine RLHF iteration, and it is bound by CPU work rather than by the GPU.
Measured on
facebook/opt-1.3b, ZeRO-2, single H100, 256-token prompt, 128 new tokens:32× the batch costs 1.07× the latency. The GPU is idle waiting on the host. A single decode step issues roughly 1300 kernel launches, and of the 5.22 ms step, 4.52 ms is the model forward while summed kernel time is only ~2.75 ms.
Replaying a captured CUDA graph replaces those launches with one call.
Design
Adds an opt-in
hybrid_engine.enable_cuda_graphflag. Two properties of the inference kernels shape the design.One graph per decode position. The kernels read the current sequence length from a host-side counter (
InferenceContext::current_tokens()incsrc/transformer/inference/csrc/pt_binding.cpp) and pass it to kernels as a launch parameter. Capture freezes launch parameters, so a single graph would keep reading and writing one position forever. Host code does run during capture, so capturing one graph per position records the correct sequence offsets into each.All-or-nothing per sequence. That counter is advanced from host code (
advance_tokens()) and is not exposed to Python. Replay runs no host code, so it leaves the counter behind, and an eager decode step after a replay would use a stale sequence length and corrupt the KV cache. Eager and replayed steps therefore must never be mixed within a sequence.begin_sequence()makes the decision once, up front, from the pinned generation length, before any decode step runs.Capture is followed immediately by a replay, since capture records work without executing it; the replay is what actually fills the KV cache for that position.
Safety
Graphs are refused, with a warning, wherever captured pointers would not stay valid:
attn_qkvw is None). A graph would replay whichever buffers existed at capture time, which is silently wrong rather than merely slow.release_inference_cache— frees the workspace buffers the graphs write into.inference_tp_size > 1— untested here.min_new_tokens == max_new_tokens.Capture failures fall back to eager execution and disable graphs, rather than failing the training job.
Weight updates were verified explicitly:
reset_params()writes into the same inference buffers in place, so the captured pointers stay valid across optimizer steps. After a step that moved the logits by 11.75, replay matched a fresh eager forward to within 0.09 and differed from the pre-step result by the full 11.75 — i.e. graphs track updated weights rather than replaying stale ones.Results
facebook/opt-1.3b, ZeRO-2, batch 8, 256-token prompt, 128 new tokens, single H100, averaged over 4 measured iterations of an RLHF-style loop (eval→generate→train→ forward/backward/step):Tests
tests/unit/hybrid_engine/test_he_cuda_graph.py:release_inference_cache/inference_tp_sizerejections, and the dispatch state machine (unknown length, over-long length, prompt forwards, and invalidation when the generation length changes).seq_inferencemarker,opt-125m) asserting that a graphed generation is token-identical to the same generation run eagerly.Docs: a Hybrid Engine section added to
docs/_pages/config-json.md, covering the config block and the new flag's requirements and restrictions.Scope
Default is off, so nothing changes unless the flag is set; the eager path measured identically before and after this change (676.7 ms generate both ways). Only ZeRO-2 with
inference_tp_size=1was benchmarked, which is what the guards allow.