[MRV2][SD] Make Dynamic SD comatible with Full Cuda Graphs (#45953)
Signed-off-by: Ekagra Ranjan <3116519+ekagra-ranjan@users.noreply.github.com> Co-authored-by: Benjamin Chislett <chislett.ben@gmail.com> Co-authored-by: Nick Hill <nickhill123@gmail.com>
Ekagra Ranjan committed
Jul 3, 2026 at 23:58 UTC
07516fda67d2133e26c0fd7386c0b0c8641e2a6e
8 files changed
+436
-36
docs/features/speculative_decoding/dynamic_speculative_decoding.md
+2
-5
@@ -71,8 +71,5 @@ VLLM_USE_V2_MODEL_RUNNER=0 vllm serve meta-llama/Llama-3.1-8B-Instruct \
71
72
## Limitations
73
74
-* only tested with Eagle and Eagle-3. Other SD methods may or may not work out of the box
75
-* only usable with Model Runner V1
76
-* not compatible with full cuda graph so we force piece-wise cuda graph with this feature
77
-
78
-We are working on enabling it on MRv2 with full cuda graph support.
74
+* Tested with Eagle, Eagle-3, and DFlash. Other SD methods may or may not work out of the box
75
+* Full Cudagraph only works with Model Runner V2. MRv1 only supports piece-wise cuda graph with this feature
tests/test_config.py
+31
@@ -34,6 +34,7 @@ from vllm.config.vllm import (
34
OptimizationLevel,
35
)
36
from vllm.platforms import current_platform
37
+from vllm.v1.attention.backend import AttentionCGSupport
38
39
DEVICE_TYPE = current_platform.device_type
40
@@ -67,6 +68,36 @@ def test_v2_model_runner_env_tri_state(monkeypatch, env_value, expected):
68
assert envs.VLLM_USE_V2_MODEL_RUNNER is expected
69
70
71
+@pytest.mark.parametrize(
72
+ ("use_v2_model_runner", "expected_capture_sizes"),
73
+ [
74
+ (False, [4, 8, 12, 16]),
75
+ (True, list(range(1, 17))),
76
+ ],
77
+)
78
+def test_resolve_cudagraph_mode_adjusts_spec_decode_sizes_only_for_v1(
79
+ use_v2_model_runner,
80
+ expected_capture_sizes,
81
+):
82
+ compilation_config = CompilationConfig(
83
+ cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
84
+ cudagraph_capture_sizes=list(range(1, 17)),
85
+ )
86
+ compilation_config.max_cudagraph_capture_size = 16
87
+ compilation_config.post_init_cudagraph_sizes()
88
+
89
+ cudagraph_mode = compilation_config.resolve_cudagraph_mode_and_sizes(
90
+ AttentionCGSupport.ALWAYS,
91
+ "FakeAttentionBackend",
92
+ uniform_decode_query_len=4,
93
+ use_v2_model_runner=use_v2_model_runner,
94
+ tensor_parallel_size=1,
95
+ )
96
+
97
+ assert cudagraph_mode == CUDAGraphMode.FULL_AND_PIECEWISE
98
+ assert compilation_config.cudagraph_capture_sizes == expected_capture_sizes
99
+
100
+
101
@pytest.mark.parametrize(
102
("model_config", "expected"),
103
[
tests/v1/spec_decode/test_dynamic_sd_cug.py
new
+328
@@ -0,0 +1,328 @@
1
+#!/usr/bin/env python3
2
+# SPDX-License-Identifier: Apache-2.0
3
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4
+
5
+from types import SimpleNamespace
6
+from unittest.mock import MagicMock
7
+
8
+import pytest
9
+import torch
10
+
11
+from vllm.config import (
12
+ CompilationConfig,
13
+ CUDAGraphMode,
14
+ ParallelConfig,
15
+ SchedulerConfig,
16
+ VllmConfig,
17
+)
18
+from vllm.v1.worker.gpu import cudagraph_utils as gpu_cudagraph_utils
19
+
20
+pytestmark = pytest.mark.cpu_test
21
+
22
+
23
+def _create_vllm_config_for_dsd(
24
+ max_num_seqs: int,
25
+ max_spec_tokens: int,
26
+ *,
27
+ cudagraph_mode: str = "FULL_AND_PIECEWISE",
28
+ use_dynamic_sd: bool = True,
29
+ num_spec_per_batch_size: list[tuple[int, int, int]] | None = None,
30
+) -> MagicMock:
31
+ """Create a minimal config that exercises DSD cudagraph dispatch.
32
+
33
+ The test uses an exact capture-size grid so that every valid uniform decode
34
+ shape has a directly matching FULL graph candidate.
35
+
36
+ ``num_spec_per_batch_size`` lets a test supply an explicit DSD schedule of
37
+ ``(range_start, range_end, num_speculative_tokens)`` tuples. When omitted,
38
+ a schedule covering every query length in ``[1, max_decode_query_len]`` is
39
+ generated.
40
+ """
41
+
42
+ max_decode_query_len = max_spec_tokens + 1
43
+ max_capture_tokens = max_num_seqs * max_decode_query_len
44
+
45
+ compilation_config = CompilationConfig(
46
+ cudagraph_mode=cudagraph_mode,
47
+ cudagraph_capture_sizes=list(range(1, max_capture_tokens + 1)),
48
+ )
49
+ compilation_config.max_cudagraph_capture_size = max_capture_tokens
50
+ compilation_config.post_init_cudagraph_sizes()
51
+
52
+ vllm_config = MagicMock(spec=VllmConfig)
53
+ vllm_config.compilation_config = compilation_config
54
+ vllm_config.scheduler_config = SchedulerConfig.default_factory(
55
+ max_num_seqs=max_num_seqs,
56
+ )
57
+ vllm_config.parallel_config = ParallelConfig()
58
+ # num_speculative_tokens is the max K (num_speculative_steps). The manager
59
+ # recovers num_new_sampled_tokens_per_step as
60
+ # decode_query_len - num_speculative_tokens; with decode_query_len =
61
+ # max_spec_tokens + 1 this yields the normal per-step bonus of 1.
62
+ vllm_config.num_speculative_tokens = max_spec_tokens
63
+
64
+ speculative_config = MagicMock()
65
+ speculative_config.uses_dynamic_speculative_decoding.return_value = use_dynamic_sd
66
+ if use_dynamic_sd:
67
+ # DSD reads the per-batch-size schedule; a schedule entry with K
68
+ # speculative tokens maps to decode query length K + 1. By default
69
+ # provide every query length in [1, max_decode_query_len] (i.e. K in
70
+ # [0, max_spec_tokens]) so the manager captures a FULL decode graph for
71
+ # each uniform shape.
72
+ if num_spec_per_batch_size is None:
73
+ num_spec_per_batch_size = [
74
+ (qlen, qlen, qlen - 1) for qlen in range(1, max_decode_query_len + 1)
75
+ ]
76
+ speculative_config.num_speculative_tokens_per_batch_size = (
77
+ num_spec_per_batch_size
78
+ )
79
+ else:
80
+ speculative_config.num_speculative_tokens_per_batch_size = None
81
+ vllm_config.speculative_config = speculative_config
82
+
83
+ return vllm_config
84
+
85
+
86
+def test_dynamic_sd_full_cudagraph_covers_all_uniform_decode_shapes(monkeypatch):
87
+ """Dynamic SD should create FULL decode candidates for every k in [1, K+1].
88
+
89
+ This validates the MRv2 CudaGraphManager path directly: once candidate
90
+ shapes have been built, dispatch() should pick a FULL graph for every
91
+ uniform decode batch shape produced by DSD up to max_num_seqs.
92
+ """
93
+
94
+ max_num_seqs = 512
95
+ max_spec_tokens = 7
96
+ max_decode_query_len = max_spec_tokens + 1
97
+
98
+ # CudaGraphManager consults PP rank helpers during initialization even
99
+ # though this test only exercises CPU-side candidate generation.
100
+ monkeypatch.setattr(
101
+ gpu_cudagraph_utils,
102
+ "get_pp_group",
103
+ lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
104
+ )
105
+
106
+ vllm_config = _create_vllm_config_for_dsd(
107
+ max_num_seqs=max_num_seqs,
108
+ max_spec_tokens=max_spec_tokens,
109
+ )
110
+ manager = gpu_cudagraph_utils.CudaGraphManager(
111
+ vllm_config=vllm_config,
112
+ device=torch.device("cpu"),
113
+ cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
114
+ decode_query_len=max_decode_query_len,
115
+ )
116
+
117
+ # dispatch() only uses the precomputed candidate table after graphs are
118
+ # considered captured. The actual graph objects are irrelevant here.
119
+ manager._graphs_captured = True
120
+
121
+ for num_reqs in range(1, max_num_seqs + 1):
122
+ for max_query_len in range(1, max_decode_query_len + 1):
123
+ # Uniform decode means every request contributes the same number of
124
+ # tokens, so the total token count is exactly num_reqs * query_len.
125
+ num_tokens = num_reqs * max_query_len
126
+ uniform_tok_count = gpu_cudagraph_utils.get_uniform_token_count(
127
+ num_reqs,
128
+ num_tokens,
129
+ max_query_len,
130
+ )
131
+
132
+ # The scheduler should mark every one of these shapes as a uniform
133
+ # decode batch, which is what enables FULL decode graph selection.
134
+ assert uniform_tok_count == max_query_len
135
+
136
+ desc = manager.dispatch(
137
+ num_reqs=num_reqs,
138
+ num_tokens=num_tokens,
139
+ uniform_token_count=uniform_tok_count,
140
+ num_active_loras=0,
141
+ )
142
+
143
+ # With DSD enabled, MRv2 should have captured a FULL candidate for
144
+ # every k in [1, K+1], so dispatch should stay on the FULL path.
145
+ assert desc.cg_mode == CUDAGraphMode.FULL
146
+ assert desc.uniform_token_count == max_query_len
147
+ assert desc.num_tokens == num_tokens
148
+ assert desc.num_reqs == num_reqs
149
+ assert desc.num_active_loras == 0
150
+
151
+
152
+def test_dynamic_sd_non_uniform_batch_falls_back_to_piecewise(monkeypatch):
153
+ """DSD should use PIECEWISE when the batch is not a uniform decode batch.
154
+
155
+ FULL DSD graphs are captured separately for each decode query length k.
156
+ When runtime tokens are not uniform, uniform_token_count is None and those
157
+ FULL candidates should be skipped in favor of the mixed-batch PIECEWISE
158
+ graph under FULL_AND_PIECEWISE mode.
159
+ """
160
+
161
+ max_spec_tokens = 4
162
+
163
+ monkeypatch.setattr(
164
+ gpu_cudagraph_utils,
165
+ "get_pp_group",
166
+ lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
167
+ )
168
+
169
+ vllm_config = _create_vllm_config_for_dsd(
170
+ max_num_seqs=512,
171
+ max_spec_tokens=max_spec_tokens,
172
+ cudagraph_mode="FULL_AND_PIECEWISE",
173
+ use_dynamic_sd=True,
174
+ )
175
+ manager = gpu_cudagraph_utils.CudaGraphManager(
176
+ vllm_config=vllm_config,
177
+ device=torch.device("cpu"),
178
+ cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
179
+ decode_query_len=max_spec_tokens + 1,
180
+ )
181
+ manager._graphs_captured = True
182
+
183
+ # This shape is intentionally non-uniform: 3 tokens across 2 requests
184
+ # cannot correspond to a single per-request query length.
185
+ desc = manager.dispatch(
186
+ num_reqs=2,
187
+ num_tokens=3,
188
+ uniform_token_count=None,
189
+ num_active_loras=0,
190
+ )
191
+
192
+ assert desc.cg_mode == CUDAGraphMode.PIECEWISE
193
+ assert desc.uniform_token_count is None
194
+ assert desc.num_reqs is None
195
+ assert desc.num_tokens == 3
196
+ assert desc.num_active_loras == 0
197
+
198
+
199
+def test_basic_sd_does_not_capture_shorter_full_decode_shapes(monkeypatch):
200
+ """Without DSD, only the max decode query length should get FULL graphs.
201
+
202
+ Basic SD captures FULL decode graphs only for decode_query_len = K + 1.
203
+ Uniform batches with smaller query lengths should therefore miss the FULL
204
+ path entirely when using FULL_AND_PIECEWISE.
205
+ """
206
+
207
+ max_num_seqs = 512
208
+ max_spec_tokens = 7
209
+ max_decode_query_len = max_spec_tokens + 1
210
+
211
+ monkeypatch.setattr(
212
+ gpu_cudagraph_utils,
213
+ "get_pp_group",
214
+ lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
215
+ )
216
+
217
+ vllm_config = _create_vllm_config_for_dsd(
218
+ max_num_seqs=max_num_seqs,
219
+ max_spec_tokens=max_spec_tokens,
220
+ cudagraph_mode="FULL_AND_PIECEWISE",
221
+ use_dynamic_sd=False,
222
+ )
223
+ manager = gpu_cudagraph_utils.CudaGraphManager(
224
+ vllm_config=vllm_config,
225
+ device=torch.device("cpu"),
226
+ cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
227
+ decode_query_len=max_decode_query_len,
228
+ )
229
+ manager._graphs_captured = True
230
+
231
+ for num_reqs in range(1, max_num_seqs + 1):
232
+ for max_query_len in range(1, max_decode_query_len):
233
+ # These are still uniform decode batches, but basic SD should only
234
+ # have FULL graphs for query_len == max_decode_query_len.
235
+ num_tokens = num_reqs * max_query_len
236
+ uniform_tok_count = gpu_cudagraph_utils.get_uniform_token_count(
237
+ num_reqs,
238
+ num_tokens,
239
+ max_query_len,
240
+ )
241
+ assert uniform_tok_count == max_query_len
242
+
243
+ desc = manager.dispatch(
244
+ num_reqs=num_reqs,
245
+ num_tokens=num_tokens,
246
+ uniform_token_count=uniform_tok_count,
247
+ num_active_loras=0,
248
+ )
249
+
250
+ assert desc.cg_mode == CUDAGraphMode.PIECEWISE
251
+ assert desc.uniform_token_count is None
252
+ assert desc.num_tokens == num_tokens
253
+ assert desc.num_reqs is None
254
+ assert desc.num_active_loras == 0
255
+
256
+
257
+def test_dynamic_sd_only_captures_scheduled_query_lengths(monkeypatch):
258
+ """DSD should only capture FULL graphs for query lengths in the schedule.
259
+
260
+ With a partial schedule of ``(1, 32, 4)`` and ``(32, 128, 3)``, only the
261
+ scheduled speculative-token counts (K = 4 and K = 3) become decode query
262
+ lengths (K + 1 = 5 and 4). Uniform batches at those query lengths should get
263
+ FULL graphs, while every other query length (e.g. the lower values 1, 2, 3)
264
+ must fall back to the mixed-batch PIECEWISE graph.
265
+ """
266
+
267
+ max_num_seqs = 128
268
+ max_spec_tokens = 7
269
+ max_decode_query_len = max_spec_tokens + 1
270
+
271
+ # (range_start, range_end, num_speculative_tokens): K = 4 and K = 3 are
272
+ # scheduled, so FULL decode graphs should exist for query lengths K + 1,
273
+ # i.e. exactly {5, 4}.
274
+ num_spec_per_batch_size = [(1, 32, 4), (32, 128, 3)]
275
+ scheduled_query_lens = {entry[2] + 1 for entry in num_spec_per_batch_size}
276
+
277
+ monkeypatch.setattr(
278
+ gpu_cudagraph_utils,
279
+ "get_pp_group",
280
+ lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
281
+ )
282
+
283
+ vllm_config = _create_vllm_config_for_dsd(
284
+ max_num_seqs=max_num_seqs,
285
+ max_spec_tokens=max_spec_tokens,
286
+ cudagraph_mode="FULL_AND_PIECEWISE",
287
+ use_dynamic_sd=True,
288
+ num_spec_per_batch_size=num_spec_per_batch_size,
289
+ )
290
+ manager = gpu_cudagraph_utils.CudaGraphManager(
291
+ vllm_config=vllm_config,
292
+ device=torch.device("cpu"),
293
+ cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
294
+ decode_query_len=max_decode_query_len,
295
+ )
296
+ manager._graphs_captured = True
297
+
298
+ for num_reqs in range(1, max_num_seqs + 1):
299
+ for max_query_len in range(1, max_decode_query_len + 1):
300
+ num_tokens = num_reqs * max_query_len
301
+ uniform_tok_count = gpu_cudagraph_utils.get_uniform_token_count(
302
+ num_reqs,
303
+ num_tokens,
304
+ max_query_len,
305
+ )
306
+ assert uniform_tok_count == max_query_len
307
+
308
+ desc = manager.dispatch(
309
+ num_reqs=num_reqs,
310
+ num_tokens=num_tokens,
311
+ uniform_token_count=uniform_tok_count,
312
+ num_active_loras=0,
313
+ )
314
+
315
+ if max_query_len in scheduled_query_lens:
316
+ # Scheduled query lengths get a dedicated FULL decode graph.
317
+ assert desc.cg_mode == CUDAGraphMode.FULL
318
+ assert desc.uniform_token_count == max_query_len
319
+ assert desc.num_tokens == num_tokens
320
+ assert desc.num_reqs == num_reqs
321
+ else:
322
+ # Unscheduled query lengths (including the lower values 1 and 2)
323
+ # have no FULL candidate and must fall back to PIECEWISE.
324
+ assert desc.cg_mode == CUDAGraphMode.PIECEWISE
325
+ assert desc.uniform_token_count is None
326
+ assert desc.num_tokens == num_tokens
327
+ assert desc.num_reqs is None
328
+ assert desc.num_active_loras == 0
vllm/config/compilation.py
+6
-2
@@ -1321,6 +1321,7 @@ class CompilationConfig:
1321
min_cg_support: "AttentionCGSupport",
1322
min_cg_attn_backend: str | None,
1323
uniform_decode_query_len: int = 1,
1324
+ use_v2_model_runner: bool = False,
1325
tensor_parallel_size: int = 1,
1326
kv_cache_config: "KVCacheConfig | None" = None,
1327
max_num_reqs: int | None = None,
@@ -1421,13 +1422,16 @@ class CompilationConfig:
1422
"and make sure compilation mode is VLLM_COMPILE"
1423
)
1424
1424
- # Adjust cudagraph sizes to be a multiple of uniform_decode_query_len
1425
+ # MRV1 adjusts cudagraph sizes to be a multiple of uniform_decode_query_len
1426
# to avoid: https://github.com/vllm-project/vllm/issues/28207 and temp-fix:
1427
# https://github.com/vllm-project/vllm/issues/28207#issuecomment-3504004536
1428
# Will be removed in the near future when we have separate cudagraph capture
1429
# sizes for decode and mixed prefill-decode.
1430
+ # MRV2 handles cudagraph capture sizing in cudagraph_utils.py
1431
+ # and doesn't need below: https://github.com/vllm-project/vllm/pull/45953
1432
if (
1430
- cudagraph_mode.decode_mode() == CUDAGraphMode.FULL
1433
+ not use_v2_model_runner
1434
+ and cudagraph_mode.decode_mode() == CUDAGraphMode.FULL
1435
and uniform_decode_query_len > 1
1436
):
1437
self.adjust_cudagraph_sizes_for_spec_decode(
vllm/config/vllm.py
+3
-4
@@ -774,13 +774,15 @@ class VllmConfig:
774
speculative_config is None
775
or not speculative_config.uses_dynamic_speculative_decoding()
776
or not self.compilation_config.cudagraph_mode.has_full_cudagraphs()
777
+ or self.use_v2_model_runner
778
):
779
return
780
781
logger.warning_once(
782
"Dynamic speculative decoding changes the target verification "
783
"length at runtime. Overriding cudagraph_mode from %s to "
783
- "PIECEWISE for reliability.",
784
+ "PIECEWISE for reliability. Use VLLM_USE_V2_MODEL_RUNNER=1 "
785
+ "if you want to use full CUDA graphs.",
786
self.compilation_config.cudagraph_mode.name,
787
)
788
self.compilation_config.cudagraph_mode = CUDAGraphMode.PIECEWISE
@@ -2062,9 +2064,6 @@ class VllmConfig:
2064
):
2065
unsupported.append(f"speculative method '{speculative_config.method}'")
2066
2065
- if speculative_config.uses_dynamic_speculative_decoding():
2066
- unsupported.append("dynamic speculative decoding")
2067
-
2067
# V2 EagleSpeculator does not support parallel_drafting (for P-Eagle).
2068
# DFlash and DSpark use parallel drafting natively in V2 via their
2069
# own speculators.
vllm/v1/worker/gpu/cudagraph_utils.py
+58
-19
@@ -27,6 +27,7 @@ from vllm.logger import init_logger
27
from vllm.model_executor.offloader.base import get_offloader
28
from vllm.platforms import current_platform
29
from vllm.sequence import IntermediateTensors
30
+from vllm.utils.math_utils import round_up
31
from vllm.v1.kv_cache_interface import KVCacheConfig
32
from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer
33
from vllm.v1.worker.gpu.block_table import BlockTables
@@ -141,10 +142,7 @@ class CudaGraphManager:
142
143
self._candidates: dict[tuple[int, int], list[BatchExecutionDescriptor]] = {}
144
self._capture_descs: dict[CUDAGraphMode, list[BatchExecutionDescriptor]] = {}
144
- # adjust the cudagraph sizes to be a multiple of the uniform decode query length
145
- self.compilation_config.adjust_cudagraph_sizes_for_spec_decode(
146
- self.decode_query_len, self.tp_size
147
- )
145
+
146
self._init_candidates()
147
148
# Breakable CUDA graph (PW CUDA graph without torch.compile)
@@ -191,31 +189,72 @@ class CudaGraphManager:
189
decode_mode = self.cudagraph_mode.decode_mode()
190
mixed_mode = self.cudagraph_mode.mixed_mode()
191
separate_decode_routine = self.cudagraph_mode.separate_routine()
192
+ max_cg_capture_size = self.compilation_config.max_cudagraph_capture_size
193
194
descs_by_token_lora: dict[tuple[int, int], list[BatchExecutionDescriptor]] = (
195
defaultdict(list)
196
)
198
- descs_by_mode = defaultdict(list)
197
+ descs_by_mode: defaultdict[CUDAGraphMode, list[BatchExecutionDescriptor]] = (
198
+ defaultdict(list)
199
+ )
200
+
201
+ # When using Dynamic SD, num_speculative_tokens is the max number of
202
+ # draft tokens. The scheduler might use a smaller number so we need
203
+ # to capture graphs for all possible values during decode.
204
+ speculative_config = self.vllm_config.speculative_config
205
+ if (
206
+ speculative_config
207
+ and speculative_config.uses_dynamic_speculative_decoding()
208
+ ):
209
+ num_spec_per_batch_size = (
210
+ speculative_config.num_speculative_tokens_per_batch_size
211
+ )
212
+ # uses_dynamic_speculative_decoding() guarantees this is set.
213
+ assert num_spec_per_batch_size is not None
214
+ # decode_query_len = num_speculative_steps + num_new_sampled_tokens
215
+ # _per_step. Recover num_new_sampled_tokens_per_step
216
+ # from the values the manager already has.
217
+ num_new_sampled_tokens_per_step = (
218
+ self.decode_query_len - self.vllm_config.num_speculative_tokens
219
+ )
220
+ # Each entry is (range_start, range_end, num_speculative_tokens).
221
+ decode_query_lens = [
222
+ x[2] + num_new_sampled_tokens_per_step for x in num_spec_per_batch_size
223
+ ]
224
+ else:
225
+ decode_query_lens = [self.decode_query_len]
226
227
for num_tokens, num_active_loras in product(
228
capture_sizes, self.lora_capture_cases
229
):
230
# Capture uniform decode specfifc graphs if required
231
# (i.e. separate decode routine)
205
- if (
206
- separate_decode_routine
207
- and decode_mode
208
- and self.decode_query_len <= num_tokens <= max_decode_tokens
209
- ):
210
- desc = BatchExecutionDescriptor(
211
- cg_mode=decode_mode,
212
- num_tokens=num_tokens,
213
- num_reqs=num_tokens // self.decode_query_len,
214
- uniform_token_count=self.decode_query_len,
215
- num_active_loras=num_active_loras,
216
- )
217
- descs_by_mode[decode_mode].append(desc)
218
- descs_by_token_lora[(num_tokens, num_active_loras)].append(desc)
232
+ if separate_decode_routine and decode_mode:
233
+ for decode_query_len in decode_query_lens:
234
+ rounded_num_tokens = round_up(num_tokens, decode_query_len)
235
+ rounded_num_reqs = rounded_num_tokens // decode_query_len
236
+
237
+ if (
238
+ rounded_num_tokens > max_decode_tokens
239
+ or rounded_num_tokens > max_cg_capture_size
240
+ or rounded_num_reqs > self.max_num_reqs
241
+ ):
242
+ continue
243
+
244
+ desc = BatchExecutionDescriptor(
245
+ cg_mode=decode_mode,
246
+ num_tokens=rounded_num_tokens,
247
+ num_reqs=rounded_num_reqs,
248
+ uniform_token_count=decode_query_len,
249
+ num_active_loras=num_active_loras,
250
+ )
251
+
252
+ # avoid duplicate graphs
253
+ if desc not in descs_by_mode[decode_mode]:
254
+ descs_by_mode[decode_mode].append(desc)
255
+ descs_by_token_lora[
256
+ (rounded_num_tokens, num_active_loras)
257
+ ].append(desc)
258
259
if mixed_mode:
260
# for PIECEWISE graphs there is no limit on requests when replaying
vllm/v1/worker/gpu/model_runner.py
+4
-3
@@ -454,9 +454,10 @@ class GPUModelRunner(LoRAModelRunnerMixin):
454
attn_cg_support.min_cg_support,
455
attn_cg_support.min_cg_attn_backend,
456
self.decode_query_len,
457
- self.parallel_config.tensor_parallel_size,
458
- self.kv_cache_config,
459
- self.max_num_reqs,
457
+ use_v2_model_runner=True,
458
+ tensor_parallel_size=self.parallel_config.tensor_parallel_size,
459
+ kv_cache_config=self.kv_cache_config,
460
+ max_num_reqs=self.max_num_reqs,
461
)
462
self.cudagraph_manager = ModelCudaGraphManager(
463
self.vllm_config,
vllm/v1/worker/gpu_model_runner.py
+4
-3
@@ -6930,9 +6930,10 @@ class GPUModelRunner(
6930
min_cg_support,
6931
min_cg_attn_backend,
6932
self.uniform_decode_query_len,
6933
- self.parallel_config.tensor_parallel_size,
6934
- self.kv_cache_config,
6935
- self.max_num_reqs,
6933
+ use_v2_model_runner=False,
6934
+ tensor_parallel_size=self.parallel_config.tensor_parallel_size,
6935
+ kv_cache_config=self.kv_cache_config,
6936
+ max_num_reqs=self.max_num_reqs,
6937
is_profiling=is_profiling,
6938
)
6939
# Trigger cudagraph dispatching keys initialization after