Skip to content

[Bugfix][V2] Warm up kernels before capturing CUDA graphs - #55341

Merged
njhill merged 4 commits into
vllm-project:mainfrom
aoshen02:fix/v2-warmup-before-capture
Sep 4, 2026
Merged

njhill merged 4 commits into
vllm-project:mainfrom
aoshen02:fix/v2-warmup-before-capture

Conversation

@aoshen02

@aoshen02 aoshen02 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator

Purpose

Fixes #55336.

warmup_kernels runs a scheduler-realistic prefill plus a decode step at
max_num_seqs. On the V2 path it is called after capture_model(), so those
shapes reach the shared workspace arena only once CUDA graphs already hold
pointers into it. WorkspaceManager._ensure_workspace_size grows the arena by
replacing the tensor, which frees the buffer the captured graphs baked in, and
the next replay writes into freed memory.

This hoists the V2 warmup next to kernel_warmup(), which already runs before
capture for the same class of reason. The V1 sampler warmup is untouched — its
comment documents why it has to follow capture.

Warming up before capture is also what TensorRT-LLM and SGLang do:
tensorrt_llm/_torch/pyexecutor/cuda_graph_runner.py warms up ahead of each
capture with the comment "This also lets us initialize states in the
attn_metadata and resize the shared attention workspace before any graph is
captured"
, and SGLang runs two warmup iterations per shape before capturing.

Test Plan

pre-commit run --all-files   # ruff, ruff-format, mypy

Serving DeepSeek-V4-Flash on GB200, which aborts during startup on main with

RuntimeError: Triton Error [CUDA]: an illegal memory access was encountered

under VLLM_BATCH_INVARIANT=1, --max-num-seqs 256, cudagraph_mode=FULL_AND_PIECEWISE.
Two setups: 8x GB200 (DP8/EP8/TP1, 43 layers) and a single GB200 with a 4-layer
proxy of the same model. Measured four things: whether this patch alone fixes
the crash, cold-start time, KV cache sizing, and throughput on a healthy
configuration that never hit the bug.

Test Result

pre-commit: passed (ruff, ruff-format, mypy).

Fix, isolated. This patch is the only change in the third row; a separate
one-line model-side fix I am upstreaming as #55299 is not sufficient on its own,
which rules it out as the cause of the repair:

patch applied max-num-seqs 256, FULL_AND_PIECEWISE
none illegal memory access during startup
#55299 only illegal memory access during startup
this PR only starts, 14 FULL + 53 PIECEWISE graphs captured
both starts

8x GB200, 43 layers: 37 FULL + 53 PIECEWISE graphs captured, zero illegal
memory accesses, batch-invariance probes intact, server serving.

Cold start (first log line to health, 8x GB200, 43 layers): 8m19s with this
patch vs 8m10s without, i.e. +1.8%, within run-to-run noise. The workload is
unchanged, only its position moves.

Memory accounting is byte-identical. The workspace this bug is about is
allocated either way — the patch only moves when — and the KV cache is sized
before compile_or_warm_up_model runs, so nothing shifts. Every startup figure
on the 8x GB200 run matches:

main this PR
model loading 41.61 GiB 41.61 GiB
actual usage 43.17 GiB 43.17 GiB
non-torch 6.57 GiB 6.57 GiB
peak activation 3.09 GiB 3.09 GiB
graph capturing 3.09 GiB 3.09 GiB
available KV cache 106.92 GiB 106.92 GiB
GPU KV cache size 279,010 tokens 279,010 tokens

No throughput regression on a configuration that was already healthy —
DeepSeek-V4-Flash, VLLM_BATCH_INVARIANT=0, FULL_AND_PIECEWISE,
--max-num-batched-tokens 8192, 256 concurrency, 150/3000 in/out:

output throughput TPOT TTFT
main 16107 tok/s 15.62 ms 800 ms
this PR 16109 tok/s 15.67 ms 616 ms

Notes

AI assistance was used for this change: the fault was located with
instrumentation written by Claude Code, and the patch was drafted with it. The
diff moves one call and I have reviewed it end to end.

🤖 Generated with Claude Code

@aoshen02
aoshen02 requested a review from njhill as a code owner September 4, 2026 13:43

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

@mergify mergify Bot added nvidia bug Something isn't working labels Sep 4, 2026
warmup_kernels runs a scheduler-realistic prefill and a max_num_seqs decode
step. Running it after capture_model() means those shapes reach the shared
workspace arena only once graphs already hold pointers into it, and a resize
swaps in a fresh buffer and frees the one they captured, so the next replay
writes into freed memory.

Hoist it next to kernel_warmup(), which already runs before capture for the
same reason. The V1 sampler warmup stays where it is; its comment documents
why it has to follow capture.

Fixes vllm-project#55336

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Signed-off-by: aoshen02 <aoshen02@users.noreply.github.com>
@aoshen02
aoshen02 force-pushed the fix/v2-warmup-before-capture branch from 9f85583 to c587b02 Compare September 4, 2026 13:44
@coderabbitai

coderabbitai Bot commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Team

Run ID: 0d6d9883-84c3-4a31-abd0-b34f9c48041a

📥 Commits

Reviewing files that changed from the base of the PR and between c587b02 and d27be25.

📒 Files selected for processing (6)
  • tests/v1/worker/test_gpu_model_runner_v2.py
  • tests/v1/worker/test_gpu_model_runner_v2_cudagraph_profiling.py
  • tests/v1/worker/test_workspace.py
  • vllm/v1/worker/gpu/cudagraph_utils.py
  • vllm/v1/worker/gpu/model_runner.py
  • vllm/v1/worker/mm_encoder_model_runner.py

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.


📝 Summary

Summary by CodeRabbit

  • Performance

    • Improved model startup by warming up GPU kernels earlier for V2 model runners.
    • Ensured kernel compilation occurs before model capture to improve runtime stability and execution readiness.
  • Reliability

    • Prevented captured execution resources from being resized unexpectedly during runtime.
    • Preserved workspace growth during profiling and restored normal allocation behavior afterward.

Walkthrough

The V2 runner now warms up kernels before CUDA graph capture and locks workspace storage after non-profile captures. Profiling captures remain unlocked, and related capture stubs and workspace tests support the new behavior.

Changes

V2 workspace safety and warmup

Layer / File(s) Summary
Lock workspace after execution capture
vllm/v1/worker/gpu/model_runner.py, vllm/v1/worker/mm_encoder_model_runner.py, vllm/v1/worker/gpu/cudagraph_utils.py, tests/v1/worker/test_gpu_model_runner_v2.py, tests/v1/worker/test_workspace.py
capture_model accepts profile_only. Non-profile captures lock the workspace after capture. Profile-only captures skip the lock. Tests cover capture conditions, profile-only behavior, and workspace growth after unlock.
Warm up V2 before CUDA graph capture
vllm/v1/worker/gpu_worker.py, tests/v1/worker/test_gpu_model_runner_v2_cudagraph_profiling.py
V2 runners call warmup_kernels before capture_model. The later sampler warmup path now applies only to V1 runners on the last pipeline-parallel rank. Capture stubs accept and forward profile_only.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Merge Risk: ⚪ Minimal · up to d27be

V2 kernel warmup now occurs before CUDA graph capture, and execution captures lock the workspace afterward to prevent later reallocation from invalidating captured buffers. Profiling remains able to grow the workspace, with matching test coverage; the change is ready to merge.

Sequence Diagram(s)

sequenceDiagram
  participant GPUWorker
  participant GPUModelRunner
  participant WorkspaceManager
  GPUWorker->>GPUModelRunner: warmup_kernels()
  GPUWorker->>GPUModelRunner: capture_model()
  GPUModelRunner->>WorkspaceManager: lock_workspace()
  WorkspaceManager-->>GPUModelRunner: reject post-capture growth
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 39.29% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 28 functions across 7 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly describes the primary change: moving V2 kernel warmup before CUDA graph capture.
Description check ✅ Passed The description explains the workspace invalidation bug, the warmup ordering fix, workspace locking, and validation results. It is directly related to the changeset.
Linked Issues check ✅ Passed The pull request satisfies issue #55336 by moving V2 warmup before capture, locking the workspace after capture, preserving profile-only behavior, and adding tests for the locking semantics.
Out of Scope Changes check ✅ Passed The production changes and tests support the linked issue objectives. The signature updates and profiling test adjustments are required for the workspace-locking implementation and introduce no unrela…
  • Fix all pre-merge checks with AI

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@aoshen02 aoshen02 added the ready ONLY add when PR is ready to merge/full CI is needed label Sep 4, 2026
@aoshen02

aoshen02 commented Sep 4, 2026

Copy link
Copy Markdown
Collaborator Author

/ci run

@he-yufeng he-yufeng left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Read this against the V1 path since #55336 quoted the missing lock. The hoist looks right as the primary fix: sizing the arena to scheduler-realistic shapes before capture means the graphs bake in a buffer that never needs to grow for warmup-bounded demand, which is what corrupts today.

One gap worth considering. This covers demand bounded by the warmup shapes. If anything later exceeds them (a request shape outside what warmup_kernels exercised, or a path the warmup does not hit), _ensure_workspace_size still replaces the arena after capture and the corruption is back, silently. V1 handles that class by locking after capture (lock_workspace() at gpu_model_runner.py:6998), so residual growth fails with the manager's precise error instead of writing into freed memory. Would it make sense to also lock on the V2 path once capture_model() returns? It is a one-liner that turns every remaining unknown growth path from silent corruption into a loud failure, and it mirrors the invariant V1 already enforces.

Side note: the format/pre-commit lanes are red on the current head, looks like trivial formatting.

@sylvesterkaczmarek sylvesterkaczmarek 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.

The ordering invariant is correct: any warmup that can resize or replace shared workspace has to finish before CUDA graph capture freezes addresses. Hoisting the existing V2 warmup rather than adding a second allocator path keeps the fix narrow, and leaving the V1 sampler warmup in its documented post-capture position avoids conflating two different lifecycle requirements.

@mergify mergify Bot added the mrv2 Model Runner V2 specific label Sep 4, 2026
@njhill

njhill commented Sep 4, 2026

Copy link
Copy Markdown
Member

Thanks @aoshen02, good catch!

I added some additional changes to lock the workspace as additional protection, along with some tests.

@github-project-automation github-project-automation Bot moved this to Ready in NVIDIA Sep 4, 2026
@njhill

njhill commented Sep 4, 2026

Copy link
Copy Markdown
Member

/ci run

@njhill
njhill enabled auto-merge (squash) September 4, 2026 21:11
@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #87320 for commit d27be2522c5a.

@njhill
njhill merged commit 685074c into vllm-project:main Sep 4, 2026
103 of 104 checks passed
@github-project-automation github-project-automation Bot moved this from Ready to Done in NVIDIA Sep 4, 2026
@aoshen02
aoshen02 deleted the fix/v2-warmup-before-capture branch September 5, 2026 00:39
ItsRoy69 pushed a commit to ItsRoy69/vllm that referenced this pull request Sep 10, 2026
…ct#55341)

Signed-off-by: aoshen02 <aoshen02@users.noreply.github.com>
Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: aoshen02 <aoshen02@users.noreply.github.com>
Co-authored-by: Nick Hill <nickhill123@gmail.com>
Signed-off-by: Jyotirmoy Roy <jyotirmoyroy649@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working 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.

[Bug]: Model Runner V2 never locks the workspace, so post-capture growth silently invalidates captured CUDA graphs

4 participants