main
py 531 lines 17.6 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from contextlib import nullcontext
5 from types import SimpleNamespace
6 from unittest.mock import Mock
7
8 import pytest
9 import torch
10
11 from vllm.config.compilation import CUDAGraphMode
12 from vllm.model_executor.models import supports_multimodal_embeddings
13 from vllm.model_executor.models.exaone4_5_mtp import Exaone4_5_MTP
14 from vllm.model_executor.models.llama4_eagle import EagleLlama4ForCausalLM
15 from vllm.model_executor.models.llama_eagle3 import Eagle3LlamaForCausalLM
16 from vllm.model_executor.models.mistral_eagle import EagleMistralForCausalLM
17 from vllm.model_executor.models.mistral_large_3_eagle import (
18 EagleMistralLarge3ForCausalLM,
19 )
20 from vllm.v1.attention.backends import flash_attn as flash_attn_module
21 from vllm.v1.attention.backends.flash_attn import FlashAttentionMetadata
22 from vllm.v1.attention.backends.utils import split_decodes_and_prefills
23 from vllm.v1.worker.gpu.cudagraph_utils import BatchExecutionDescriptor
24 from vllm.v1.worker.gpu.spec_decode import speculator as base_spec_module
25 from vllm.v1.worker.gpu.spec_decode.autoregressive import speculator as spec_module
26 from vllm.v1.worker.gpu.spec_decode.autoregressive.speculator import (
27 AutoRegressiveSpeculator,
28 )
29 from vllm.v1.worker.gpu.spec_decode.multi_module_mtp.speculator import (
30 MultiModuleMTPSpeculator,
31 )
32 from vllm.v1.worker.gpu.spec_decode.speculator import DraftModelSpeculator
33
34
35 class _TestSpeculator(AutoRegressiveSpeculator):
36 def load_draft_model(self, target_model, target_attn_layer_names):
37 return self.test_draft_model
38
39
40 class _DraftModel(torch.nn.Module):
41 def __init__(self, output: torch.Tensor | tuple[torch.Tensor, torch.Tensor]):
42 super().__init__()
43 self.output = output
44
45 def forward(self, **kwargs):
46 return self.output
47
48
49 class _MultimodalDraftModel(torch.nn.Module):
50 supports_multimodal_embeddings = True
51
52 def embed_input_ids(
53 self,
54 input_ids,
55 multimodal_embeddings=None,
56 *,
57 is_multimodal=None,
58 ):
59 raise AssertionError("embed_input_ids should not be called during loading")
60
61
62 class _TextOnlyDraftModel(torch.nn.Module):
63 def embed_input_ids(
64 self,
65 input_ids,
66 multimodal_embeddings=None,
67 *,
68 is_multimodal=None,
69 ):
70 raise AssertionError("embed_input_ids should not be called during loading")
71
72
73 @pytest.mark.parametrize("cg_mode", [CUDAGraphMode.NONE, CUDAGraphMode.FULL])
74 def test_pcp_draft_metadata_keeps_graph_padding_in_decode(cg_mode):
75 def build(common_prefix_len, common_attn_metadata):
76 return split_decodes_and_prefills(
77 common_attn_metadata,
78 decode_threshold=1,
79 require_uniform=True,
80 treat_short_extends_as_decodes=False,
81 )
82
83 speculator = object.__new__(_TestSpeculator)
84 speculator.arange_np = torch.arange(5, dtype=torch.int32).numpy()
85 speculator.max_model_len = speculator.draft_max_seq_len = 32
86 speculator.draft_is_prefilling = torch.zeros(4, dtype=torch.bool)
87 speculator.input_buffers = SimpleNamespace(
88 query_start_loc=torch.tensor([0, 1, 2, 2, 2], dtype=torch.int32),
89 seq_lens=torch.tensor([11, 21, 0, 0], dtype=torch.int32),
90 )
91 speculator.dcp_size = 1
92 speculator.block_tables = SimpleNamespace(
93 cp_size=1,
94 input_block_tables=[torch.zeros(4, 1, dtype=torch.int32)],
95 slot_mappings=torch.tensor([[10, 20, -1, -1]]),
96 )
97 speculator.kv_cache_config = SimpleNamespace(kv_cache_groups=[object()])
98 speculator.attn_groups = [
99 [
100 SimpleNamespace(
101 get_metadata_builder=lambda _: SimpleNamespace(
102 build=build, supports_update_block_table=False
103 ),
104 layer_names=["draft"],
105 )
106 ]
107 ]
108 num_reqs_padded = 4 if cg_mode == CUDAGraphMode.FULL else 2
109 metadata = speculator._build_uniform_attn_metadata(
110 batch_desc=BatchExecutionDescriptor(cg_mode, 4, num_reqs_padded),
111 num_reqs=2,
112 num_query_per_req=1,
113 seq_lens_cpu_upper_bound=torch.tensor([10, 20], dtype=torch.int32),
114 step=1,
115 )
116 assert metadata["draft"] == (num_reqs_padded, 0, num_reqs_padded, 0)
117
118
119 def _mock_base_model_load(monkeypatch):
120 monkeypatch.setattr(
121 base_spec_module,
122 "get_layers_from_vllm_config",
123 lambda *args, **kwargs: {},
124 )
125 monkeypatch.setattr(
126 DraftModelSpeculator,
127 "_validate_local_argmax_reduction",
128 lambda self: None,
129 )
130
131
132 def _make_speculator(
133 monkeypatch,
134 output: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
135 ) -> _TestSpeculator:
136 monkeypatch.setattr(
137 spec_module,
138 "set_forward_context",
139 lambda *args, **kwargs: nullcontext(),
140 )
141
142 speculator = object.__new__(_TestSpeculator)
143 speculator.supports_mm_inputs = False
144 speculator.pcp_manager = None
145 speculator.vllm_config = None
146 speculator.input_buffers = SimpleNamespace(
147 input_ids=torch.arange(4),
148 positions=torch.arange(4),
149 )
150 speculator.hidden_states = torch.zeros(4, 3)
151 speculator.model = _DraftModel(output)
152 return speculator
153
154
155 @pytest.mark.parametrize(("hc_mult", "expected"), [(None, 64), (4, 256)])
156 def test_speculator_uses_draft_model_hidden_size(monkeypatch, hc_mult, expected):
157 # Qwen4Exp targets expose multi-stream HC residuals to the drafter.
158 monkeypatch.setattr(base_spec_module, "_target_feeds_hc_residual", lambda _: True)
159 hf_config = SimpleNamespace()
160 if hc_mult is not None:
161 hf_config.hc_mult = hc_mult
162 draft_model_config = SimpleNamespace(
163 hf_config=hf_config,
164 get_hidden_size=lambda: 64,
165 get_vocab_size=lambda: 32,
166 )
167 speculative_config = SimpleNamespace(
168 method="mtp",
169 num_speculative_tokens=3,
170 draft_model_config=draft_model_config,
171 use_local_argmax_reduction=False,
172 draft_sample_method="greedy",
173 enable_adaptive_verification=False,
174 )
175 vllm_config = SimpleNamespace(
176 speculative_config=speculative_config,
177 scheduler_config=SimpleNamespace(
178 max_num_seqs=2,
179 max_num_batched_tokens=8,
180 ),
181 model_config=SimpleNamespace(
182 max_model_len=32,
183 dtype=torch.float32,
184 use_fp64_gumbel=False,
185 ),
186 parallel_config=SimpleNamespace(
187 data_parallel_size=1,
188 data_parallel_rank=0,
189 decode_context_parallel_size=1,
190 ),
191 )
192
193 speculator = _TestSpeculator(vllm_config, torch.device("cpu"))
194
195 assert speculator.hidden_size == expected
196
197
198 def test_mm_support_configured_after_model_load(monkeypatch):
199 checked_configs = []
200
201 class _ModelConfig:
202 @property
203 def supports_multimodal_inputs(self):
204 checked_configs.append(self)
205 return True
206
207 target_model_config = _ModelConfig()
208 draft_model_config = _ModelConfig()
209 vllm_config = SimpleNamespace(model_config=target_model_config)
210 draft_model = _MultimodalDraftModel()
211
212 def init_base(speculator, vllm_config, device):
213 speculator.vllm_config = vllm_config
214 speculator.device = device
215 speculator.max_num_tokens = 4
216 speculator.max_num_reqs = 2
217 speculator.hidden_size = 3
218 speculator.dtype = torch.float32
219 speculator.draft_model_config = draft_model_config
220 speculator.supports_mm_inputs = False
221 speculator.use_acceptance_estimator = False
222
223 monkeypatch.setattr(DraftModelSpeculator, "__init__", init_base)
224 _mock_base_model_load(monkeypatch)
225
226 speculator = _TestSpeculator(vllm_config, torch.device("cpu"))
227
228 assert checked_configs == []
229 assert not speculator.supports_mm_inputs
230 assert speculator.inputs_embeds is None
231
232 speculator.test_draft_model = draft_model
233 speculator.load_model(torch.nn.Module())
234
235 assert checked_configs == [target_model_config]
236 assert speculator.supports_mm_inputs
237 assert speculator.inputs_embeds is not None
238 assert speculator.inputs_embeds.shape == (4, 3)
239
240
241 def test_load_model_keeps_mm_support_for_capable_drafter(monkeypatch):
242 speculator = object.__new__(_TestSpeculator)
243 speculator.supports_mm_inputs = False
244 speculator.inputs_embeds = None
245 speculator.use_acceptance_estimator = False
246 speculator.vllm_config = SimpleNamespace(
247 model_config=SimpleNamespace(supports_multimodal_inputs=True)
248 )
249 speculator.max_num_tokens = 4
250 speculator.hidden_size = 3
251 speculator.dtype = torch.float32
252 speculator.device = torch.device("cpu")
253 draft_model = _MultimodalDraftModel()
254 speculator.test_draft_model = draft_model
255 _mock_base_model_load(monkeypatch)
256
257 speculator.load_model(torch.nn.Module())
258
259 assert speculator.supports_mm_inputs
260 assert speculator.inputs_embeds is not None
261
262
263 def test_load_model_disables_mm_support_for_text_only_drafter(monkeypatch):
264 speculator = object.__new__(_TestSpeculator)
265 speculator.supports_mm_inputs = False
266 speculator.inputs_embeds = None
267 speculator.use_acceptance_estimator = False
268 speculator.vllm_config = SimpleNamespace(
269 model_config=SimpleNamespace(supports_multimodal_inputs=True)
270 )
271 draft_model = _TextOnlyDraftModel()
272 speculator.test_draft_model = draft_model
273 warning_messages = []
274 _mock_base_model_load(monkeypatch)
275 monkeypatch.setattr(
276 base_spec_module.logger,
277 "warning_once",
278 lambda message, *args: warning_messages.append(message % args),
279 )
280
281 speculator.load_model(torch.nn.Module())
282
283 assert not speculator.supports_mm_inputs
284 assert warning_messages == [
285 (
286 "Draft model _TextOnlyDraftModel does not support external multimodal "
287 "embeddings. Embeddings from the target model will not be passed to the "
288 "drafter; using text-only draft inputs instead."
289 )
290 ]
291
292
293 def test_multi_module_mm_support_configured_after_model_load(monkeypatch):
294 speculator = object.__new__(MultiModuleMTPSpeculator)
295 speculator.supports_mm_inputs = False
296 speculator.inputs_embeds = None
297 speculator.use_acceptance_estimator = False
298 speculator.cached_draft_input_embeds = None
299 speculator.vllm_config = SimpleNamespace(
300 model_config=SimpleNamespace(supports_multimodal_inputs=True)
301 )
302 speculator.max_num_tokens = 4
303 speculator.max_num_reqs = 2
304 speculator.num_speculative_steps = 3
305 speculator.hidden_size = 3
306 speculator.dtype = torch.float32
307 speculator.device = torch.device("cpu")
308 draft_model = _MultimodalDraftModel()
309 _mock_base_model_load(monkeypatch)
310 monkeypatch.setattr(
311 MultiModuleMTPSpeculator,
312 "load_draft_model",
313 lambda self, target_model, target_attn_layer_names: draft_model,
314 )
315
316 speculator.load_model(torch.nn.Module())
317
318 assert speculator.supports_mm_inputs
319 assert speculator.inputs_embeds is not None
320 assert speculator.inputs_embeds.shape == (4, 3)
321 assert speculator.cached_draft_input_embeds is not None
322 assert speculator.cached_draft_input_embeds.shape == (2, 2, 3)
323
324
325 @pytest.mark.parametrize(
326 ("model_cls", "expected"),
327 [
328 (EagleLlama4ForCausalLM, True),
329 (EagleMistralForCausalLM, True),
330 (EagleMistralLarge3ForCausalLM, True),
331 (Exaone4_5_MTP, True),
332 (Eagle3LlamaForCausalLM, False),
333 ],
334 )
335 def test_draft_model_multimodal_embedding_capability(model_cls, expected):
336 assert supports_multimodal_embeddings(model_cls) is expected
337
338
339 def test_run_model_unpacks_tuple_return_for_mtp(monkeypatch):
340 logits_hidden = torch.full((4, 3), 1.0)
341 feedback_hidden = torch.full((4, 3), 2.0)
342 speculator = _make_speculator(monkeypatch, (logits_hidden, feedback_hidden))
343
344 actual_logits_hidden, actual_feedback_hidden = speculator._run_model(
345 4,
346 attn_metadata=None,
347 slot_mappings=None,
348 num_tokens_across_dp=None,
349 cudagraph_runtime_mode=CUDAGraphMode.NONE,
350 )
351
352 assert actual_logits_hidden is logits_hidden
353 assert actual_feedback_hidden is feedback_hidden
354
355
356 def test_run_model_reuses_tensor_return_for_mtp(monkeypatch):
357 hidden = torch.full((4, 3), 1.0)
358 speculator = _make_speculator(monkeypatch, hidden)
359
360 actual_logits_hidden, actual_feedback_hidden = speculator._run_model(
361 4,
362 attn_metadata=None,
363 slot_mappings=None,
364 num_tokens_across_dp=None,
365 cudagraph_runtime_mode=CUDAGraphMode.NONE,
366 )
367
368 assert actual_logits_hidden is hidden
369 assert actual_feedback_hidden is hidden
370
371
372 @pytest.mark.parametrize(
373 (
374 "method_name",
375 "cg_mode",
376 "expected_eager_calls",
377 "expected_graph_replays",
378 ),
379 [
380 ("_multi_step_decode", CUDAGraphMode.NONE, 3, 0),
381 ("_multi_step_decode", CUDAGraphMode.FULL, 0, 3),
382 ("_fused_multi_step_decode", CUDAGraphMode.NONE, 3, 0),
383 ("_fused_multi_step_decode", CUDAGraphMode.FULL, 0, 1),
384 ],
385 )
386 def test_multi_step_decode_replays_captured_graph_as_expected(
387 method_name,
388 cg_mode,
389 expected_eager_calls,
390 expected_graph_replays,
391 ):
392 speculator = object.__new__(_TestSpeculator)
393 speculator.num_speculative_steps = 4
394 speculator.current_draft_step = torch.tensor(0)
395 speculator.slot_mapping_observer = None
396 speculator.host_mirror_forward_observer = None
397 speculator.input_buffers = SimpleNamespace(
398 positions=torch.arange(2),
399 query_start_loc=torch.arange(3),
400 )
401 speculator.idx_mapping = torch.arange(2)
402 generate_draft = Mock()
403 speculator._generate_draft = generate_draft
404 run_fullgraph = Mock()
405 speculator.decode_cudagraph_manager = SimpleNamespace(run_fullgraph=run_fullgraph)
406 batch_desc = BatchExecutionDescriptor(
407 cg_mode=cg_mode,
408 num_tokens=2,
409 num_reqs=2,
410 )
411
412 getattr(speculator, method_name)(
413 num_reqs=2,
414 skip_attn=True,
415 batch_desc=batch_desc,
416 seq_lens_cpu_upper_bound=None,
417 num_tokens_across_dp=None,
418 )
419
420 assert generate_draft.call_count == expected_eager_calls
421 assert run_fullgraph.call_count == expected_graph_replays
422
423
424 def test_update_draft_decode_metadata_updates_fa3_scheduler_metadata(
425 monkeypatch,
426 ):
427 builder = object.__new__(flash_attn_module.FlashAttentionMetadataBuilder)
428 builder.aot_schedule = True
429 builder.use_full_cuda_graph = True
430 builder.scheduler_metadata = torch.zeros(8, dtype=torch.int32)
431 builder.cache_config = SimpleNamespace(cache_dtype="bfloat16")
432 builder.kv_cache_dtype = torch.bfloat16
433 builder.num_heads_q = 2
434 builder.num_heads_kv = 1
435 builder.headdim = 128
436 builder.block_size = 16
437 builder.dcp_world_size = 1
438 builder.dcp_rank = 0
439 builder.cp_kv_cache_interleave_size = 1
440 builder.aot_sliding_window = None
441
442 expected = torch.tensor([7, 8, 9], dtype=torch.int32)
443
444 def fake_get_scheduler_metadata(**kwargs):
445 return expected
446
447 monkeypatch.setattr(builder, "_get_scheduler_metadata", fake_get_scheduler_metadata)
448
449 metadata = FlashAttentionMetadata(
450 num_actual_tokens=3,
451 max_query_len=2,
452 query_start_loc=torch.tensor([0, 1, 3], dtype=torch.int32),
453 max_seq_len=8,
454 seq_lens=torch.tensor([5, 6], dtype=torch.int32),
455 block_table=torch.zeros((2, 1), dtype=torch.int32),
456 slot_mapping=torch.zeros(3, dtype=torch.int32),
457 use_cascade=False,
458 common_prefix_len=0,
459 cu_prefix_query_lens=None,
460 prefix_kv_lens=None,
461 suffix_kv_lens=None,
462 max_dcp_context_kv_len=None,
463 dcp_context_kv_lens=None,
464 num_decode_reqs=2,
465 num_prefill_reqs=0,
466 num_decode_tokens=3,
467 num_prefill_tokens=0,
468 scheduler_metadata=torch.tensor([-1, -1, -1], dtype=torch.int32),
469 prefix_scheduler_metadata=None,
470 max_num_splits=4,
471 causal=True,
472 mm_prefix_query_range_tensor=None,
473 rswa_prefix_lens=None,
474 rswa_window=None,
475 rswa_window_tensor=None,
476 )
477
478 builder.update_draft_decode_metadata(metadata)
479
480 assert torch.equal(metadata.scheduler_metadata, expected)
481 assert torch.equal(builder.scheduler_metadata[:3], expected)
482
483
484 def test_update_draft_decode_metadata_skips_without_scheduler_metadata(monkeypatch):
485 builder = object.__new__(flash_attn_module.FlashAttentionMetadataBuilder)
486 builder.aot_schedule = True
487 builder.use_full_cuda_graph = True
488 builder.scheduler_metadata = torch.zeros(4, dtype=torch.int32)
489
490 called = False
491
492 def fake_get_scheduler_metadata(**kwargs):
493 nonlocal called
494 called = True
495 return torch.tensor([1], dtype=torch.int32)
496
497 monkeypatch.setattr(builder, "_get_scheduler_metadata", fake_get_scheduler_metadata)
498
499 metadata = FlashAttentionMetadata(
500 num_actual_tokens=1,
501 max_query_len=1,
502 query_start_loc=torch.tensor([0, 1], dtype=torch.int32),
503 max_seq_len=1,
504 seq_lens=torch.tensor([1], dtype=torch.int32),
505 block_table=torch.zeros((1, 1), dtype=torch.int32),
506 slot_mapping=torch.zeros(1, dtype=torch.int32),
507 use_cascade=False,
508 common_prefix_len=0,
509 cu_prefix_query_lens=None,
510 prefix_kv_lens=None,
511 suffix_kv_lens=None,
512 max_dcp_context_kv_len=None,
513 dcp_context_kv_lens=None,
514 num_decode_reqs=1,
515 num_prefill_reqs=0,
516 num_decode_tokens=1,
517 num_prefill_tokens=0,
518 scheduler_metadata=None,
519 prefix_scheduler_metadata=None,
520 max_num_splits=1,
521 causal=True,
522 mm_prefix_query_range_tensor=None,
523 rswa_prefix_lens=None,
524 rswa_window=None,
525 rswa_window_tensor=None,
526 )
527
528 builder.update_draft_decode_metadata(metadata)
529
530 assert not called
531 assert metadata.scheduler_metadata is None