[MRV1] Scope breakable cudagraphs to the piecewise path only (#56312)

Signed-off-by: Nick Hill <nickhill123@gmail.com>
Co-authored-by: Andreas Karatzas <akaratza@amd.com>
This commit is contained in:
Nick Hill
2026-09-11 19:55:34 +00:00
committed by GitHub
co-authored by Andreas Karatzas
parent dffb863c97
commit cc5dd0a857
4 changed files with 165 additions and 17 deletions
+131 -1
View File
@@ -8,7 +8,7 @@ from __future__ import annotations
import threading
from contextlib import nullcontext
from unittest.mock import patch
from unittest.mock import MagicMock, patch
import pytest
import torch
@@ -536,3 +536,133 @@ def test_nested_decorated_op_runs_inline(cuda_capture_stream):
# 0 -> +2 -> +1 (inner) -> +10 (outer) -> +100 = 113
assert torch.equal(x, torch.full((4,), 113.0, device="cuda"))
assert inner_calls == 2 # replay invokes the outer's lambda again
# ---------------------------------------------------------------------------
# BreakableCUDAGraphWrapper: runtime_mode dispatch gating
# ---------------------------------------------------------------------------
def _mock_vllm_config():
config = MagicMock()
config.parallel_config.data_parallel_size = 1
config.parallel_config.use_sequence_parallel_moe = False
config.parallel_config.is_moe_model = False
return config
def test_wrapper_falls_through_on_runtime_mode_mismatch():
"""A PIECEWISE-scoped wrapper runs other modes eagerly, capturing
nothing, so an outer CUDAGraphWrapper can own them."""
from vllm.compilation.breakable_cudagraph import BreakableCUDAGraphWrapper
from vllm.config import CUDAGraphMode
from vllm.forward_context import BatchDescriptor, set_forward_context
calls = []
def model(x):
calls.append(x)
return x + 1
config = _mock_vllm_config()
wrapper = BreakableCUDAGraphWrapper(
model, config, runtime_mode=CUDAGraphMode.PIECEWISE
)
desc = BatchDescriptor(num_tokens=4)
with set_forward_context(
None,
config,
cudagraph_runtime_mode=CUDAGraphMode.FULL,
batch_descriptor=desc,
):
assert wrapper(1) == 2
assert calls == [1]
assert not wrapper.entries
def test_wrapper_captures_on_runtime_mode_match():
from vllm.compilation.breakable_cudagraph import BreakableCUDAGraphWrapper
from vllm.config import CUDAGraphMode
from vllm.forward_context import BatchDescriptor, set_forward_context
calls = []
def model(x):
calls.append(x)
return x + 1
config = _mock_vllm_config()
wrapper = BreakableCUDAGraphWrapper(
model, config, runtime_mode=CUDAGraphMode.PIECEWISE
)
desc = BatchDescriptor(num_tokens=4)
sentinel = object()
with (
patch.object(
BreakableCUDAGraphWrapper, "_capture", return_value=sentinel
) as mock_capture,
set_forward_context(
None,
config,
cudagraph_runtime_mode=CUDAGraphMode.PIECEWISE,
batch_descriptor=desc,
),
):
assert wrapper(1) is sentinel
mock_capture.assert_called_once()
assert calls == []
assert desc in wrapper.entries
def test_wrapper_default_matches_any_non_none_mode():
"""runtime_mode=None matches any non-NONE mode (MRV2 relies on this)."""
from vllm.compilation.breakable_cudagraph import BreakableCUDAGraphWrapper
from vllm.config import CUDAGraphMode
from vllm.forward_context import BatchDescriptor, set_forward_context
def model(x):
return x + 1
config = _mock_vllm_config()
wrapper = BreakableCUDAGraphWrapper(model, config)
desc = BatchDescriptor(num_tokens=4)
sentinel = object()
with (
patch.object(
BreakableCUDAGraphWrapper, "_capture", return_value=sentinel
) as mock_capture,
set_forward_context(
None,
config,
cudagraph_runtime_mode=CUDAGraphMode.FULL,
batch_descriptor=desc,
),
):
assert wrapper(1) is sentinel
mock_capture.assert_called_once()
assert desc in wrapper.entries
def test_unwrap_recurses_through_nested_wrappers():
"""unwrap() reaches the raw model through nested wrappers."""
from vllm.compilation.breakable_cudagraph import BreakableCUDAGraphWrapper
from vllm.compilation.cuda_graph import CUDAGraphWrapper
from vllm.config import CUDAGraphMode
def model(x):
return x + 1
config = _mock_vllm_config()
inner = BreakableCUDAGraphWrapper(
model, config, runtime_mode=CUDAGraphMode.PIECEWISE
)
outer = CUDAGraphWrapper(inner, config, runtime_mode=CUDAGraphMode.FULL)
assert inner.unwrap() is model
assert outer.unwrap() is model
+19 -12
View File
@@ -256,6 +256,10 @@ class BreakableCUDAGraphWrapper:
* If runtime mode mismatch / NONE, run eagerly.
* Otherwise, lazily capture per ``batch_descriptor`` and replay
on subsequent invocations with the same descriptor.
When ``runtime_mode`` is given, only that mode is captured/replayed;
other non-NONE modes fall through, so the wrapper can be nested under
a ``CUDAGraphWrapper`` for another mode.
"""
_all_instances: ClassVar[weakref.WeakSet[BreakableCUDAGraphWrapper]] = (
@@ -271,15 +275,14 @@ class BreakableCUDAGraphWrapper:
self,
runnable: Callable[..., Any],
vllm_config: VllmConfig,
runtime_mode: CUDAGraphMode | None = None,
) -> None:
# Unlike the original CUDAGraphWrapper which strictly matches a
# single runtime_mode, this wrapper captures whatever the
# dispatcher emits (any non-NONE runtime_mode) -- breakable's
# capture is identical for prefill and decode, so there's nothing
# to dispatch on at the runtime_mode level. Entries are keyed by
# BatchDescriptor which already encodes batch shape / uniformity.
# runtime_mode=None matches any non-NONE dispatch mode; entries
# are keyed by BatchDescriptor, which already encodes batch
# shape / uniformity.
self.runnable = runnable
self.vllm_config = vllm_config
self.runtime_mode = runtime_mode
self.compilation_config = vllm_config.compilation_config
self.graph_pool = current_platform.get_global_graph_pool()
self.is_debugging_mode = envs.VLLM_LOGGING_LEVEL == "DEBUG"
@@ -298,7 +301,9 @@ class BreakableCUDAGraphWrapper:
raise AttributeError(key)
def unwrap(self) -> Callable[..., Any]:
return self.runnable
# Recurse through nested wrappers to reach the original runnable.
runnable = self.runnable
return runnable.unwrap() if hasattr(runnable, "unwrap") else runnable
@property
def cudagraph_wrapper(self) -> BreakableCUDAGraphWrapper:
@@ -317,11 +322,13 @@ class BreakableCUDAGraphWrapper:
batch_descriptor = forward_context.batch_descriptor
cudagraph_runtime_mode = forward_context.cudagraph_runtime_mode
# Capture whenever the dispatcher says "some cudagraph mode" --
# breakable produces the same artifact regardless of PIECEWISE
# vs FULL, so we match either. Entries are keyed by batch
# descriptor, which already encodes prefill/decode distinctions.
if cudagraph_runtime_mode == CUDAGraphMode.NONE:
# By default (runtime_mode=None) match any non-NONE mode; a set
# runtime_mode restricts capture so other modes can be handled by
# an outer wrapper (e.g. CUDAGraphWrapper for FULL).
if cudagraph_runtime_mode == CUDAGraphMode.NONE or (
self.runtime_mode is not None
and cudagraph_runtime_mode != self.runtime_mode
):
return self.runnable(*args, **kwargs)
assert batch_descriptor is not None
+3 -1
View File
@@ -221,7 +221,9 @@ class CUDAGraphWrapper:
def unwrap(self) -> Callable[..., Any]:
# in case we need to access the original runnable.
return self.runnable
runnable = self.runnable
# Recurse through nested wrappers.
return runnable.unwrap() if hasattr(runnable, "unwrap") else runnable
@property
def cudagraph_wrapper(self) -> "CUDAGraphWrapper":
+12 -3
View File
@@ -5461,13 +5461,22 @@ class GPUModelRunner(
and cudagraph_mode != CUDAGraphMode.NONE
and not self.parallel_config.use_ubatching
):
self.model = BreakableCUDAGraphWrapper(self.model, self.vllm_config)
# Scoped to PIECEWISE dispatch; FULL cudagraphs (below) are
# unaffected. PIECEWISE dispatch can also arise after wrapping
# (drafters under a FULL target mode, or a later FULL ->
# FULL_AND_PIECEWISE upgrade in _check_and_update_cudagraph_mode).
self.model = BreakableCUDAGraphWrapper(
self.model, self.vllm_config, runtime_mode=CUDAGraphMode.PIECEWISE
)
drafter = getattr(self, "drafter", None)
if drafter is not None and hasattr(drafter, "model"):
drafter.model = BreakableCUDAGraphWrapper(
drafter.model, self.vllm_config
drafter.model,
self.vllm_config,
runtime_mode=CUDAGraphMode.PIECEWISE,
)
elif (
if (
cudagraph_mode.has_full_cudagraphs()
and not self.parallel_config.use_ubatching
):