main
py 428 lines 15.7 KB
Raw
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 from vllm.v1.worker.utils import get_uniform_decode_token_count
20
21 pytestmark = pytest.mark.cpu_test
22
23
24 def _create_vllm_config_for_dsd(
25 max_num_seqs: int,
26 max_spec_tokens: int,
27 *,
28 cudagraph_mode: CUDAGraphMode = CUDAGraphMode.FULL_AND_PIECEWISE,
29 use_dynamic_sd: bool = True,
30 num_spec_per_batch_size: list[tuple[int, int, int]] | None = None,
31 ) -> MagicMock:
32 """Create a minimal config that exercises DSD cudagraph dispatch.
33
34 The test uses an exact capture-size grid so that every valid uniform decode
35 shape has a directly matching FULL graph candidate.
36
37 ``num_spec_per_batch_size`` lets a test supply an explicit DSD schedule of
38 ``(range_start, range_end, num_speculative_tokens)`` tuples. When omitted,
39 a schedule covering every query length in ``[1, max_decode_query_len]`` is
40 generated.
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 max_num_seqs = 512
94 max_spec_tokens = 7
95 max_decode_query_len = max_spec_tokens + 1
96
97 # CudaGraphManager consults PP rank helpers during initialization even
98 # though this test only exercises CPU-side candidate generation.
99 monkeypatch.setattr(
100 gpu_cudagraph_utils,
101 "get_pp_group",
102 lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
103 )
104
105 vllm_config = _create_vllm_config_for_dsd(
106 max_num_seqs=max_num_seqs,
107 max_spec_tokens=max_spec_tokens,
108 )
109 manager = gpu_cudagraph_utils.CudaGraphManager(
110 vllm_config=vllm_config,
111 device=torch.device("cpu"),
112 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
113 decode_query_len=max_decode_query_len,
114 )
115
116 # dispatch() only uses the precomputed candidate table after graphs are
117 # considered captured. The actual graph objects are irrelevant here.
118 manager._graphs_captured = True
119
120 for num_reqs in range(1, max_num_seqs + 1):
121 for max_query_len in range(1, max_decode_query_len + 1):
122 # Uniform decode means every request contributes the same number of
123 # tokens, so the total token count is exactly num_reqs * query_len.
124 num_tokens = num_reqs * max_query_len
125 uniform_tok_count = get_uniform_decode_token_count(
126 num_reqs,
127 num_tokens,
128 max_query_len,
129 decode_graph_eligible=True,
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_cudagraphs_use_clamped_query_length(monkeypatch):
153 """Clamp configured K=5 to K=3, yielding query length 4 rather than 6."""
154 monkeypatch.setattr(
155 gpu_cudagraph_utils,
156 "get_pp_group",
157 lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
158 )
159 monkeypatch.setattr(
160 gpu_cudagraph_utils.current_platform,
161 "get_global_graph_pool",
162 lambda: None,
163 )
164
165 vllm_config = _create_vllm_config_for_dsd(
166 max_num_seqs=16,
167 max_spec_tokens=3,
168 num_spec_per_batch_size=[(1, 16, 5)],
169 )
170 manager = gpu_cudagraph_utils.CudaGraphManager(
171 vllm_config=vllm_config,
172 device=torch.device("cpu"),
173 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
174 decode_query_len=4,
175 )
176
177 full_query_lens = {
178 desc.uniform_token_count for desc in manager._capture_descs[CUDAGraphMode.FULL]
179 }
180 assert full_query_lens == {4}
181
182
183 def test_dynamic_sd_non_uniform_batch_falls_back_to_piecewise(monkeypatch):
184 """DSD should use PIECEWISE when the batch is not a uniform decode batch.
185
186 FULL DSD graphs are captured separately for each decode query length k.
187 When runtime tokens are not uniform, uniform_token_count is None and those
188 FULL candidates should be skipped in favor of the mixed-batch PIECEWISE
189 graph under FULL_AND_PIECEWISE mode.
190 """
191 max_spec_tokens = 4
192
193 monkeypatch.setattr(
194 gpu_cudagraph_utils,
195 "get_pp_group",
196 lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
197 )
198
199 vllm_config = _create_vllm_config_for_dsd(
200 max_num_seqs=512,
201 max_spec_tokens=max_spec_tokens,
202 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
203 use_dynamic_sd=True,
204 )
205 manager = gpu_cudagraph_utils.CudaGraphManager(
206 vllm_config=vllm_config,
207 device=torch.device("cpu"),
208 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
209 decode_query_len=max_spec_tokens + 1,
210 )
211 manager._graphs_captured = True
212
213 # This shape is intentionally non-uniform: 3 tokens across 2 requests
214 # cannot correspond to a single per-request query length.
215 desc = manager.dispatch(
216 num_reqs=2,
217 num_tokens=3,
218 uniform_token_count=None,
219 num_active_loras=0,
220 )
221
222 assert desc.cg_mode == CUDAGraphMode.PIECEWISE
223 assert desc.uniform_token_count is None
224 assert desc.num_reqs is None
225 assert desc.num_tokens == 3
226 assert desc.num_active_loras == 0
227
228
229 def test_prompt_chunks_shaped_like_spec_decode_miss_the_full_graph(monkeypatch):
230 """A batch holding K+1-token prompt chunks must not replay a decode graph.
231
232 Prompt chunks of exactly K + 1 tokens give the batch a spec-decode shape
233 by coincidence; replaying the FULL graph over them corrupts every request
234 in the batch. See https://github.com/vllm-project/vllm/issues/49918.
235 """
236 max_spec_tokens = 7
237 decode_query_len = max_spec_tokens + 1
238
239 monkeypatch.setattr(
240 gpu_cudagraph_utils,
241 "get_pp_group",
242 lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
243 )
244
245 vllm_config = _create_vllm_config_for_dsd(
246 max_num_seqs=64,
247 max_spec_tokens=max_spec_tokens,
248 use_dynamic_sd=False,
249 )
250 manager = gpu_cudagraph_utils.CudaGraphManager(
251 vllm_config=vllm_config,
252 device=torch.device("cpu"),
253 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
254 decode_query_len=decode_query_len,
255 )
256 manager._graphs_captured = True
257
258 num_decodes, num_chunks = 6, 2
259 num_reqs = num_decodes + num_chunks
260 num_tokens = num_reqs * decode_query_len
261
262 # The batch shape is indistinguishable from a full batch of spec decodes.
263 assert (
264 get_uniform_decode_token_count(
265 num_reqs, num_tokens, decode_query_len, decode_graph_eligible=True
266 )
267 == decode_query_len
268 )
269
270 # Two of the requests are decode_query_len tokens into a longer prompt.
271 uniform_tok_count = get_uniform_decode_token_count(
272 num_reqs, num_tokens, decode_query_len, decode_graph_eligible=False
273 )
274 assert uniform_tok_count is None
275 desc = manager.dispatch(
276 num_reqs=num_reqs,
277 num_tokens=num_tokens,
278 uniform_token_count=uniform_tok_count,
279 num_active_loras=0,
280 )
281 assert desc.cg_mode == CUDAGraphMode.PIECEWISE
282 assert desc.uniform_token_count is None
283
284 # The same shape with every request decoding still gets its FULL graph.
285 uniform_tok_count = get_uniform_decode_token_count(
286 num_reqs, num_tokens, decode_query_len, decode_graph_eligible=True
287 )
288 assert uniform_tok_count == decode_query_len
289 desc = manager.dispatch(
290 num_reqs=num_reqs,
291 num_tokens=num_tokens,
292 uniform_token_count=uniform_tok_count,
293 num_active_loras=0,
294 )
295 assert desc.cg_mode == CUDAGraphMode.FULL
296 assert desc.uniform_token_count == decode_query_len
297
298
299 def test_basic_sd_does_not_capture_shorter_full_decode_shapes(monkeypatch):
300 """Without DSD, only the max decode query length should get FULL graphs.
301
302 Basic SD captures FULL decode graphs only for decode_query_len = K + 1.
303 Uniform batches with smaller query lengths should therefore miss the FULL
304 path entirely when using FULL_AND_PIECEWISE.
305 """
306 max_num_seqs = 512
307 max_spec_tokens = 7
308 max_decode_query_len = max_spec_tokens + 1
309
310 monkeypatch.setattr(
311 gpu_cudagraph_utils,
312 "get_pp_group",
313 lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
314 )
315
316 vllm_config = _create_vllm_config_for_dsd(
317 max_num_seqs=max_num_seqs,
318 max_spec_tokens=max_spec_tokens,
319 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
320 use_dynamic_sd=False,
321 )
322 manager = gpu_cudagraph_utils.CudaGraphManager(
323 vllm_config=vllm_config,
324 device=torch.device("cpu"),
325 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
326 decode_query_len=max_decode_query_len,
327 )
328 manager._graphs_captured = True
329
330 for num_reqs in range(1, max_num_seqs + 1):
331 for max_query_len in range(1, max_decode_query_len):
332 # These are still uniform decode batches, but basic SD should only
333 # have FULL graphs for query_len == max_decode_query_len.
334 num_tokens = num_reqs * max_query_len
335 uniform_tok_count = get_uniform_decode_token_count(
336 num_reqs,
337 num_tokens,
338 max_query_len,
339 decode_graph_eligible=True,
340 )
341 assert uniform_tok_count == max_query_len
342
343 desc = manager.dispatch(
344 num_reqs=num_reqs,
345 num_tokens=num_tokens,
346 uniform_token_count=uniform_tok_count,
347 num_active_loras=0,
348 )
349
350 assert desc.cg_mode == CUDAGraphMode.PIECEWISE
351 assert desc.uniform_token_count is None
352 assert desc.num_tokens == num_tokens
353 assert desc.num_reqs is None
354 assert desc.num_active_loras == 0
355
356
357 def test_dynamic_sd_only_captures_scheduled_query_lengths(monkeypatch):
358 """DSD should only capture FULL graphs for query lengths in the schedule.
359
360 With a partial schedule of ``(1, 31, 4)`` and ``(32, 128, 3)``, only the
361 scheduled speculative-token counts (K = 4 and K = 3) become decode query
362 lengths (K + 1 = 5 and 4). Uniform batches at those query lengths should get
363 FULL graphs, while every other query length (e.g. the lower values 1, 2, 3)
364 must fall back to the mixed-batch PIECEWISE graph.
365 """
366 max_num_seqs = 128
367 max_spec_tokens = 7
368 max_decode_query_len = max_spec_tokens + 1
369
370 # (range_start, range_end, num_speculative_tokens): K = 4 and K = 3 are
371 # scheduled in non-overlapping tiers, so FULL decode graphs should exist
372 # for query lengths K + 1, i.e. exactly {5, 4}.
373 num_spec_per_batch_size = [(1, 31, 4), (32, 128, 3)]
374 scheduled_query_lens = {entry[2] + 1 for entry in num_spec_per_batch_size}
375
376 monkeypatch.setattr(
377 gpu_cudagraph_utils,
378 "get_pp_group",
379 lambda: SimpleNamespace(is_first_rank=True, is_last_rank=True),
380 )
381
382 vllm_config = _create_vllm_config_for_dsd(
383 max_num_seqs=max_num_seqs,
384 max_spec_tokens=max_spec_tokens,
385 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
386 use_dynamic_sd=True,
387 num_spec_per_batch_size=num_spec_per_batch_size,
388 )
389 manager = gpu_cudagraph_utils.CudaGraphManager(
390 vllm_config=vllm_config,
391 device=torch.device("cpu"),
392 cudagraph_mode=CUDAGraphMode.FULL_AND_PIECEWISE,
393 decode_query_len=max_decode_query_len,
394 )
395 manager._graphs_captured = True
396
397 for num_reqs in range(1, max_num_seqs + 1):
398 for max_query_len in range(1, max_decode_query_len + 1):
399 num_tokens = num_reqs * max_query_len
400 uniform_tok_count = get_uniform_decode_token_count(
401 num_reqs,
402 num_tokens,
403 max_query_len,
404 decode_graph_eligible=True,
405 )
406 assert uniform_tok_count == max_query_len
407
408 desc = manager.dispatch(
409 num_reqs=num_reqs,
410 num_tokens=num_tokens,
411 uniform_token_count=uniform_tok_count,
412 num_active_loras=0,
413 )
414
415 if max_query_len in scheduled_query_lens:
416 # Scheduled query lengths get a dedicated FULL decode graph.
417 assert desc.cg_mode == CUDAGraphMode.FULL
418 assert desc.uniform_token_count == max_query_len
419 assert desc.num_tokens == num_tokens
420 assert desc.num_reqs == num_reqs
421 else:
422 # Unscheduled query lengths (including the lower values 1 and 2)
423 # have no FULL candidate and must fall back to PIECEWISE.
424 assert desc.cg_mode == CUDAGraphMode.PIECEWISE
425 assert desc.uniform_token_count is None
426 assert desc.num_tokens == num_tokens
427 assert desc.num_reqs is None
428 assert desc.num_active_loras == 0