Repository navigation
[Bugfix][CUDA Graph] Weak-ref nested outputs of captured graphs - #63427
Conversation
weak_ref_tensors only weak-ref'd one level, so the aux hidden states in (hidden_states, [aux_hidden_states, ...]) stayed alive for every captured size. Recurse into nested lists/tuples and pass ints through. Co-authored-by: Bo Pang <bopang@inferact.ai> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a06d7fe559
ℹ️ 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".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
|
|
||
| @pytest.mark.skipif(not torch.cuda.is_available(), reason="needs CUDA") | ||
| def test_weak_ref_tensors_does_not_keep_nested_tensors_alive() -> None: |
There was a problem hiding this comment.
Add an LLM-level regression for nested graph outputs
The new regression invokes only weak_ref_tensors, so it never traverses either CUDA-graph wrapper, captures multiple sizes, or consumes nested auxiliary outputs through serving—the user-visible path whose per-entry retention this fix targets. This leaves the wrapper integration unguarded; add an LLM/vllm_runner regression with real weights that exercises nested aux outputs and verifies the capture-memory behavior.
AGENTS.md reference: tests/AGENTS.md:L8-L10
Useful? React with 👍 / 👎.
|
/ci run |
|
✅ Triggered Buildkite CI #97427 for commit |
Inductor graph partition outputs can carry hoisted LayerName inputs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Pt8XT23FfZneogRxtKm8uS Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
|
/ci run --allow-stale |
|
✅ Triggered Buildkite CI #97453 for commit
|
Port upstream vllm-project/vllm#63427. Recursively weak-ref list and tuple outputs so auxiliary hidden states cannot retain a pool slot for each captured batch descriptor. Preserve hoisted ints and LayerNames and report the offending type for unsupported leaves. Keep the upstream output contract rather than adding dict or None support. Full graph callers return hidden-state tensors, auxiliary tensor lists, or IntermediateTensors; piecewise callers return compiled FX outputs. No supported graph-output caller was found returning nested dicts or None. Internal decoder None residuals are not model graph outputs, and IntermediateTensors already has its tensor-map handling. Leave weak_ref_tensor permissive: breakable capture uses it for mixed tensor and non-tensor arguments. Recursive weak_ref_tensors now validates every list/tuple leaf before invoking that helper, eliminating the silent nesting bypass without changing capture-argument behavior. Regression tests cover both outer container types, three nesting levels, CUDA allocation release, hoisted-value identity, and unsupported nested leaves including dict and None. All eight new cases fail before the fix. The full utility suite passes (42 tests), and CUDA-hidden validation reports 6 passed and 2 skipped. Pre-commit and manual mypy-3.12 checks pass. Signed-off-by: alec <alec.thomas@repositpower.com>
…-project#63427) Signed-off-by: zjy0516 <riverclouds.zhu@qq.com> Co-authored-by: Bo Pang <bopang@inferact.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> Signed-off-by: Christopher Mackenzie <christopher.mackenzie.software@gmail.com>
Overview
weak_ref_tensorsonly weak-refs one level. For a model output like(hidden_states, [aux_hidden_states, ...]), the inner list is returned as is, so every captured CUDA graph entry keeps its aux hidden states alive and the graph pool cannot reuse that memory for the next capture size. This PR makesweak_ref_tensorsrecurse into nested lists/tuples.Related: #63406 lists this as "aux hidden states kept by every PIECEWISE size" in its memory breakdown. This PR is the standalone fix and also applies to main's capture sizes.
Claims
extract_hidden_states. Before, each captured size leakednum_aux × num_tokens × hidden_size × dtype_sizebytes (3 × 8192 × 4096 × 2 B = 192 MiB for one 8192-token size of DeepSeek-V4-Flash).weak_ref_tensorsnow raises on unknown types at any nesting level, not only at the top level, and passes ints andLayerNames through (piecewise subgraph outputs can contain ints, and Inductor graph partition outputs can containLayerNames, see below).Validation
Unit test (
tests/utils_/test_torch_utils.py, run by the "Async Engine, Inputs, Utils, Worker" CI step), on GB200:What actually reaches
weak_ref_tensors. I wrapped the function invllm/compilation/cuda_graph.pyandvllm/compilation/breakable_cudagraph.py, then started each engine and recorded the structure of every input during capture (T= tensor;[n:...]= list of length n;(n:...)= tuple of length n):[1:T][2:T][4:T][5:T][6:T](2:T,[3:T]), drafter(2:T)[1:T][2:T][5:T][6:T](2:T,[3:T]), drafter(2:T)[1:T][5:T][6:T,int]TThe nested case is the breakable target output with aux hidden states. The int case is a piecewise subgraph output of the FP8 MoE model, which is why ints are passed through. No
None, float or 0-size tensor showed up.The table does not cover
use_inductor_graph_partition=True. There, on torch >= 2.11,CUDAGraphWrapperwraps each Inductor partition, and a partition's outputs can includeLayerNameobjects that torch.compile hoists as graph inputs. This PR's first CI run caught it (ValueError: Invalid type for tensors: <class 'vllm.utils.torch_utils.LayerName'>intest_full_cudagraph.py[*-partition=True]andtest_full_graph.py::test_custom_compile_config), soLayerNames pass through as well. After the fix, on GB200:End-to-end A/B: DeepSeek-V4-Flash-DSpark, TP4+EP, DSpark K=7, 4×GB200. Breakable CUDA graph is on by default for this model, and capture sizes are main's defaults. Main is this PR's parent (2c58be2). Memory numbers are from TP rank 0.
The leak on main is about 0.65 GiB here, and it grows with the number and size of PIECEWISE capture sizes (#63406 raises both).
The 5-question GSM8K gap is from a single run per arm; serving is not batch invariant, so some run-to-run variance is expected. It does not indicate corrupted aux hidden states: they only feed the drafter, so corruption would lower acceptance, and acceptance is unchanged. The target model alone decides the output tokens under greedy rejection sampling.
Serve command
vllm serve deepseek-ai/DeepSeek-V4-Flash-DSpark \ --tensor-parallel-size 4 --enable-expert-parallel \ --trust-remote-code --kv-cache-dtype fp8 --block-size 256 \ --tokenizer-mode deepseek_v4 --reasoning-parser deepseek_v4 \ --speculative-config '{"method":"dspark","num_speculative_tokens":7,"draft_sample_method":"greedy"}'GSM8K: 5-shot, temperature 0, chat completions, thinking off. Acceptance is read from
vllm:spec_decode_*metrics after the GSM8K run. Memory numbers come from theCUDA graph pool memory,Available KV cache memoryandGPU KV cache sizelog lines.Details
Affected paths.
weak_ref_tensorsis called on capture outputs by:BreakableCUDAGraphWrapper._capture: captures the whole model forward, so EAGLE3/DSpark targets return(hidden_states, [aux, ...]). Breakable CUDA graph is on by default for DeepSeek-V4/V4.1, MiniMax-M3, GLM-5-Next, Kimi-K3 and others, under both model runners.CUDAGraphWrapperin FULL mode on MRV1, which also wraps the whole model.Not affected: torch.compile piecewise subgraph outputs are flat, and MRV2 FULL graphs copy outputs into persistent buffers instead of weak-refing them.
Safety of the weak refs. Once the aux tensors are weak, later captures may reuse their memory, the same as for
hidden_statestoday. So the aux tensors must be consumed before the next graph replay. Every consumer copies them first: MRV1torch.cat/torch.stackingpu_model_runner.py, MRV2 DFlash_store_hidden_states(concatenates into its staging buffer), MRV2extract_hidden_states(torch.stack+copy_).Type contract. Previously, non-tensor values inside a list/tuple passed through
weak_ref_tensorsilently, which is how the nested list slipped through. Now containers recurse, ints andLayerNames pass, and any other type raises with its type name. The tradeoff: a model whose capture output contains a leaf type not in the table above (e.g.None) fails at capture time instead of passing silently. The raise already caught one such case before (IntermediateTensors, #24275), and caughtLayerNamein this PR's first CI run.Not a duplicate: no open PR touches
weak_ref_tensorsor this leak. #63406 changes capture sizes and only references the leak in its memory accounting.The fix and analysis originate from Bo Pang (co-author). AI assistance (Claude Code) was used to write the change, the tests and the validation scripts.
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.
🤖 Generated with Claude Code