[Bugfix][Model Runner V2] Restore multimodal draft capability detection (#50417)

Signed-off-by: Raphael Rialland <raphael.rialland@mistral.ai>

Raphaël Rialland committed Aug 3, 2026 at 23:18 UTC 4a3447d200e5aa428d68d1a00aa00f1a19a1a729
12 files changed +317 -58
tests/v1/worker/test_gpu_autoregressive_speculator.py
+199 -1
@@ -4,18 +4,32 @@
4 from contextlib import nullcontext
5 from types import SimpleNamespace
6
7 +import pytest
8 import torch
9
10 from vllm.config.compilation import CUDAGraphMode
11 +from vllm.model_executor.models import supports_multimodal_embeddings
12 +from vllm.model_executor.models.exaone4_5_mtp import Exaone4_5_MTP
13 +from vllm.model_executor.models.llama4_eagle import EagleLlama4ForCausalLM
14 +from vllm.model_executor.models.llama_eagle3 import Eagle3LlamaForCausalLM
15 +from vllm.model_executor.models.mistral_eagle import EagleMistralForCausalLM
16 +from vllm.model_executor.models.mistral_large_3_eagle import (
17 + EagleMistralLarge3ForCausalLM,
18 +)
19 +from vllm.v1.worker.gpu.spec_decode import speculator as base_spec_module
20 from vllm.v1.worker.gpu.spec_decode.autoregressive import speculator as spec_module
21 from vllm.v1.worker.gpu.spec_decode.autoregressive.speculator import (
22 AutoRegressiveSpeculator,
23 )
24 +from vllm.v1.worker.gpu.spec_decode.multi_module_mtp.speculator import (
25 + MultiModuleMTPSpeculator,
26 +)
27 +from vllm.v1.worker.gpu.spec_decode.speculator import DraftModelSpeculator
28
29
30 class _TestSpeculator(AutoRegressiveSpeculator):
31 def load_draft_model(self, target_model, target_attn_layer_names):
18 - raise NotImplementedError
32 + return self.test_draft_model
33
34
35 class _DraftModel(torch.nn.Module):
@@ -27,6 +41,43 @@ class _DraftModel(torch.nn.Module):
41 return self.output
42
43
44 +class _MultimodalDraftModel(torch.nn.Module):
45 + supports_multimodal_embeddings = True
46 +
47 + def embed_input_ids(
48 + self,
49 + input_ids,
50 + multimodal_embeddings=None,
51 + *,
52 + is_multimodal=None,
53 + ):
54 + raise AssertionError("embed_input_ids should not be called during loading")
55 +
56 +
57 +class _TextOnlyDraftModel(torch.nn.Module):
58 + def embed_input_ids(
59 + self,
60 + input_ids,
61 + multimodal_embeddings=None,
62 + *,
63 + is_multimodal=None,
64 + ):
65 + raise AssertionError("embed_input_ids should not be called during loading")
66 +
67 +
68 +def _mock_base_model_load(monkeypatch):
69 + monkeypatch.setattr(
70 + base_spec_module,
71 + "get_layers_from_vllm_config",
72 + lambda *args, **kwargs: {},
73 + )
74 + monkeypatch.setattr(
75 + DraftModelSpeculator,
76 + "_validate_local_argmax_reduction",
77 + lambda self: None,
78 + )
79 +
80 +
81 def _make_speculator(
82 monkeypatch,
83 output: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
@@ -49,6 +100,153 @@ def _make_speculator(
100 return speculator
101
102
103 +def test_mm_support_configured_after_model_load(monkeypatch):
104 + target_model_config = object()
105 + draft_model_config = object()
106 + vllm_config = SimpleNamespace(model_config=target_model_config)
107 + draft_model = _MultimodalDraftModel()
108 +
109 + def init_base(speculator, vllm_config, device):
110 + speculator.vllm_config = vllm_config
111 + speculator.device = device
112 + speculator.max_num_tokens = 4
113 + speculator.max_num_reqs = 2
114 + speculator.hidden_size = 3
115 + speculator.dtype = torch.float32
116 + speculator.draft_model_config = draft_model_config
117 + speculator.supports_mm_inputs = False
118 +
119 + checked_configs = []
120 +
121 + def supports_multimodal_inputs(model_config):
122 + checked_configs.append(model_config)
123 + return True
124 +
125 + monkeypatch.setattr(DraftModelSpeculator, "__init__", init_base)
126 + _mock_base_model_load(monkeypatch)
127 + monkeypatch.setattr(
128 + base_spec_module.MULTIMODAL_REGISTRY,
129 + "supports_multimodal_inputs",
130 + supports_multimodal_inputs,
131 + )
132 +
133 + speculator = _TestSpeculator(vllm_config, torch.device("cpu"))
134 +
135 + assert checked_configs == []
136 + assert not speculator.supports_mm_inputs
137 + assert speculator.inputs_embeds is None
138 +
139 + speculator.test_draft_model = draft_model
140 + speculator.load_model(torch.nn.Module())
141 +
142 + assert checked_configs == [target_model_config]
143 + assert speculator.supports_mm_inputs
144 + assert speculator.inputs_embeds is not None
145 + assert speculator.inputs_embeds.shape == (4, 3)
146 +
147 +
148 +def test_load_model_keeps_mm_support_for_capable_drafter(monkeypatch):
149 + speculator = object.__new__(_TestSpeculator)
150 + speculator.supports_mm_inputs = False
151 + speculator.inputs_embeds = None
152 + speculator.vllm_config = SimpleNamespace(model_config=object())
153 + speculator.max_num_tokens = 4
154 + speculator.hidden_size = 3
155 + speculator.dtype = torch.float32
156 + speculator.device = torch.device("cpu")
157 + draft_model = _MultimodalDraftModel()
158 + speculator.test_draft_model = draft_model
159 + _mock_base_model_load(monkeypatch)
160 + monkeypatch.setattr(
161 + base_spec_module.MULTIMODAL_REGISTRY,
162 + "supports_multimodal_inputs",
163 + lambda model_config: True,
164 + )
165 +
166 + speculator.load_model(torch.nn.Module())
167 +
168 + assert speculator.supports_mm_inputs
169 + assert speculator.inputs_embeds is not None
170 +
171 +
172 +def test_load_model_disables_mm_support_for_text_only_drafter(monkeypatch):
173 + speculator = object.__new__(_TestSpeculator)
174 + speculator.supports_mm_inputs = False
175 + speculator.inputs_embeds = None
176 + speculator.vllm_config = SimpleNamespace(model_config=object())
177 + draft_model = _TextOnlyDraftModel()
178 + speculator.test_draft_model = draft_model
179 + warning_messages = []
180 + _mock_base_model_load(monkeypatch)
181 + monkeypatch.setattr(
182 + base_spec_module.MULTIMODAL_REGISTRY,
183 + "supports_multimodal_inputs",
184 + lambda model_config: True,
185 + )
186 + monkeypatch.setattr(
187 + base_spec_module.logger,
188 + "warning_once",
189 + lambda message, *args: warning_messages.append(message % args),
190 + )
191 +
192 + speculator.load_model(torch.nn.Module())
193 +
194 + assert not speculator.supports_mm_inputs
195 + assert warning_messages == [
196 + "Draft model _TextOnlyDraftModel does not support external multimodal "
197 + "embeddings. Embeddings from the target model will not be passed to the "
198 + "drafter; using text-only draft inputs instead."
199 + ]
200 +
201 +
202 +def test_multi_module_mm_support_configured_after_model_load(monkeypatch):
203 + speculator = object.__new__(MultiModuleMTPSpeculator)
204 + speculator.supports_mm_inputs = False
205 + speculator.inputs_embeds = None
206 + speculator.cached_draft_input_embeds = None
207 + speculator.vllm_config = SimpleNamespace(model_config=object())
208 + speculator.max_num_tokens = 4
209 + speculator.max_num_reqs = 2
210 + speculator.num_speculative_steps = 3
211 + speculator.hidden_size = 3
212 + speculator.dtype = torch.float32
213 + speculator.device = torch.device("cpu")
214 + draft_model = _MultimodalDraftModel()
215 + _mock_base_model_load(monkeypatch)
216 + monkeypatch.setattr(
217 + MultiModuleMTPSpeculator,
218 + "load_draft_model",
219 + lambda self, target_model, target_attn_layer_names: draft_model,
220 + )
221 + monkeypatch.setattr(
222 + base_spec_module.MULTIMODAL_REGISTRY,
223 + "supports_multimodal_inputs",
224 + lambda model_config: True,
225 + )
226 +
227 + speculator.load_model(torch.nn.Module())
228 +
229 + assert speculator.supports_mm_inputs
230 + assert speculator.inputs_embeds is not None
231 + assert speculator.inputs_embeds.shape == (4, 3)
232 + assert speculator.cached_draft_input_embeds is not None
233 + assert speculator.cached_draft_input_embeds.shape == (2, 2, 3)
234 +
235 +
236 +@pytest.mark.parametrize(
237 + ("model_cls", "expected"),
238 + [
239 + (EagleLlama4ForCausalLM, True),
240 + (EagleMistralForCausalLM, True),
241 + (EagleMistralLarge3ForCausalLM, True),
242 + (Exaone4_5_MTP, True),
243 + (Eagle3LlamaForCausalLM, False),
244 + ],
245 +)
246 +def test_draft_model_multimodal_embedding_capability(model_cls, expected):
247 + assert supports_multimodal_embeddings(model_cls) is expected
248 +
249 +
250 def test_run_model_unpacks_tuple_return_for_mtp(monkeypatch):
251 logits_hidden = torch.full((4, 3), 1.0)
252 feedback_hidden = torch.full((4, 3), 2.0)
vllm/model_executor/models/__init__.py
+4
@@ -6,12 +6,14 @@ from .interfaces import (
6 SupportsLoRA,
7 SupportsMRoPE,
8 SupportsMultiModal,
9 + SupportsMultiModalEmbeddings,
10 SupportsPP,
11 SupportsTranscription,
12 has_inner_state,
13 supports_lora,
14 supports_mrope,
15 supports_multimodal,
16 + supports_multimodal_embeddings,
17 supports_pp,
18 supports_transcription,
19 )
@@ -35,6 +37,8 @@ __all__ = [
37 "supports_lora",
38 "SupportsMultiModal",
39 "supports_multimodal",
40 + "SupportsMultiModalEmbeddings",
41 + "supports_multimodal_embeddings",
42 "SupportsMRoPE",
43 "supports_mrope",
44 "SupportsPP",
vllm/model_executor/models/interfaces.py
+34 -1
@@ -110,7 +110,22 @@ _language_model_by_module = dict[nn.Module, "VllmModel"]()
110
111
112 @runtime_checkable
113 -class SupportsMultiModal(Protocol):
113 +class SupportsMultiModalEmbeddings(Protocol):
114 + """The interface for models that can merge external multimodal embeddings."""
115 +
116 + supports_multimodal_embeddings: ClassVar[Literal[True]] = True
117 +
118 + def embed_input_ids(
119 + self,
120 + input_ids: Tensor,
121 + multimodal_embeddings: MultiModalEmbeddings | None = None,
122 + *,
123 + is_multimodal: Tensor | None = None,
124 + ) -> Tensor: ...
125 +
126 +
127 +@runtime_checkable
128 +class SupportsMultiModal(SupportsMultiModalEmbeddings, Protocol):
129 """The interface required for all multi-modal models."""
130
131 supports_multimodal: ClassVar[Literal[True]] = True
@@ -488,6 +503,24 @@ def supports_multimodal(
503 return getattr(model, "supports_multimodal", False)
504
505
506 +@overload
507 +def supports_multimodal_embeddings(
508 + model: type[object],
509 +) -> TypeIs[type[SupportsMultiModalEmbeddings]]: ...
510 +
511 +
512 +@overload
513 +def supports_multimodal_embeddings(
514 + model: object,
515 +) -> TypeIs[SupportsMultiModalEmbeddings]: ...
516 +
517 +
518 +def supports_multimodal_embeddings(
519 + model: type[object] | object,
520 +) -> TypeIs[type[SupportsMultiModalEmbeddings]] | TypeIs[SupportsMultiModalEmbeddings]:
521 + return getattr(model, "supports_multimodal_embeddings", False)
522 +
523 +
524 def supports_multimodal_raw_input_only(model: type[object] | object) -> bool:
525 return getattr(model, "supports_multimodal_raw_input_only", False)
526
vllm/model_executor/models/llama4_eagle.py
+2 -2
@@ -35,7 +35,7 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
35 from vllm.model_executor.models.llama4 import Llama4DecoderLayer, Llama4ForCausalLM
36 from vllm.model_executor.models.utils import extract_layer_index
37
38 -from .interfaces import SupportsMultiModal
38 +from .interfaces import SupportsMultiModal, SupportsMultiModalEmbeddings
39 from .utils import (
40 AutoWeightsLoader,
41 WeightsMapper,
@@ -152,7 +152,7 @@ class LlamaModel(nn.Module):
152 }
153
154
155 -class EagleLlama4ForCausalLM(Llama4ForCausalLM):
155 +class EagleLlama4ForCausalLM(Llama4ForCausalLM, SupportsMultiModalEmbeddings):
156 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
157 nn.Module.__init__(self)
158 self.config = vllm_config.speculative_config.draft_model_config.hf_config
vllm/model_executor/models/mistral_eagle.py
+5 -2
@@ -14,7 +14,10 @@ from vllm.model_executor.layers.linear import RowParallelLinear
14 from vllm.model_executor.layers.logits_processor import LogitsProcessor
15 from vllm.model_executor.layers.quantization.base_config import QuantizationConfig
16 from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
17 -from vllm.model_executor.models.interfaces import MultiModalEmbeddings
17 +from vllm.model_executor.models.interfaces import (
18 + MultiModalEmbeddings,
19 + SupportsMultiModalEmbeddings,
20 +)
21 from vllm.model_executor.models.llama import LlamaConfig
22 from vllm.model_executor.models.mistral import (
23 MistralDecoderLayer,
@@ -109,7 +112,7 @@ class EagleMistralModel(MistralModel):
112 return hidden_states, hidden_states
113
114
112 -class EagleMistralForCausalLM(MistralForCausalLM):
115 +class EagleMistralForCausalLM(MistralForCausalLM, SupportsMultiModalEmbeddings):
116 mistral_mapping = MistralForCausalLM.mistral_mapping | {
117 "eagle_linear": "model.fc",
118 }
vllm/model_executor/models/mistral_large_3_eagle.py
+4 -2
@@ -22,7 +22,7 @@ from vllm.model_executor.models.deepseek_v2 import (
22 )
23 from vllm.model_executor.models.mistral_large_3 import MistralLarge3ForCausalLM
24
25 -from .interfaces import SupportsMultiModal
25 +from .interfaces import SupportsMultiModal, SupportsMultiModalEmbeddings
26 from .utils import WeightsMapper, make_empty_intermediate_tensors_factory, maybe_prefix
27
28 logger = init_logger(__name__)
@@ -107,7 +107,9 @@ class EagleMistralLarge3Model(DeepseekV2Model):
107 return output
108
109
110 -class EagleMistralLarge3ForCausalLM(MistralLarge3ForCausalLM):
110 +class EagleMistralLarge3ForCausalLM(
111 + MistralLarge3ForCausalLM, SupportsMultiModalEmbeddings
112 +):
113 hf_to_vllm_mapper = MistralLarge3ForCausalLM.hf_to_vllm_mapper | WeightsMapper(
114 orig_to_new_regex={
115 regex.compile(r"\Aeagle_linear\.weight\Z"): r"model.fc.weight",
vllm/models/inkling/amd/mtp.py
+2 -1
@@ -29,6 +29,7 @@ from vllm.model_executor.model_loader.mtp_validation import (
29 is_mtp_completeness_check_enabled,
30 )
31 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
32 +from vllm.model_executor.models.interfaces import SupportsMultiModalEmbeddings
33 from vllm.model_executor.models.utils import maybe_prefix
34 from vllm.sequence import IntermediateTensors
35
@@ -213,7 +214,7 @@ class InklingMultiTokenPredictor(nn.Module):
214 return hidden
215
216
216 -class InklingMTP(nn.Module):
217 +class InklingMTP(nn.Module, SupportsMultiModalEmbeddings):
218 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
219 super().__init__()
220 assert vllm_config.speculative_config is not None
vllm/models/inkling/nvidia/mtp.py
+2 -1
@@ -33,6 +33,7 @@ from vllm.model_executor.model_loader.mtp_validation import (
33 is_mtp_completeness_check_enabled,
34 )
35 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
36 +from vllm.model_executor.models.interfaces import SupportsMultiModalEmbeddings
37 from vllm.model_executor.models.utils import maybe_prefix
38 from vllm.sequence import IntermediateTensors
39
@@ -242,7 +243,7 @@ class InklingMultiTokenPredictor(nn.Module):
243 return hidden
244
245
245 -class InklingMTP(nn.Module):
246 +class InklingMTP(nn.Module, SupportsMultiModalEmbeddings):
247 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
248 super().__init__()
249 assert vllm_config.speculative_config is not None
vllm/v1/spec_decode/llm_base_proposer.py
+14 -13
@@ -25,7 +25,10 @@ from vllm.forward_context import set_forward_context
25 from vllm.logger import init_logger
26 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
27 from vllm.model_executor.model_loader import get_model
28 -from vllm.model_executor.models import supports_multimodal
28 +from vllm.model_executor.models import (
29 + supports_multimodal,
30 + supports_multimodal_embeddings,
31 +)
32 from vllm.model_executor.models.deepseek_eagle3 import Eagle3DeepseekV2ForCausalLM
33 from vllm.model_executor.models.interfaces import SupportsMultiModal
34 from vllm.model_executor.models.laguna_dflash import DFlashLagunaForCausalLM
@@ -1349,18 +1352,16 @@ class SpecDecodeBaseProposer:
1352 if all_attn_layers[name].get_kv_cache_spec(self.vllm_config) is not None
1353 }
1354
1352 - if self.supports_mm_inputs:
1353 - # Even if the target model is multimodal, we can also use
1354 - # text-only draft models
1355 - try:
1356 - dummy_input_ids = torch.tensor([[1]], device=self.input_ids.device)
1357 - self.model.embed_input_ids(dummy_input_ids, multimodal_embeddings=None)
1358 - except (NotImplementedError, AttributeError, TypeError):
1359 - logger.warning(
1360 - "Draft model does not support multimodal inputs, "
1361 - "falling back to text-only mode"
1362 - )
1363 - self.supports_mm_inputs = False
1355 + # Even if the target model is multimodal, we can also use
1356 + # text-only draft models
1357 + if self.supports_mm_inputs and not supports_multimodal_embeddings(self.model):
1358 + logger.warning_once(
1359 + "Draft model %s does not support external multimodal embeddings. "
1360 + "Embeddings from the target model will not be passed to the "
1361 + "drafter; using text-only draft inputs instead.",
1362 + type(self.model).__name__,
1363 + )
1364 + self.supports_mm_inputs = False
1365
1366 if supports_multimodal(target_model):
1367 # handle multimodality
vllm/v1/worker/gpu/spec_decode/autoregressive/speculator.py
+15 -8
@@ -3,12 +3,12 @@
3 from typing import Any
4
5 import torch
6 +import torch.nn as nn
7
8 from vllm.config import VllmConfig
9 from vllm.config.compilation import CUDAGraphMode
10 from vllm.forward_context import BatchDescriptor, set_forward_context
11 from vllm.logger import init_logger
11 -from vllm.multimodal import MULTIMODAL_REGISTRY
12 from vllm.triton_utils import tl, triton
13 from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer
14 from vllm.v1.worker.gpu.cudagraph_utils import (
@@ -37,17 +37,23 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
37 self.max_num_reqs, dtype=torch.int64, device=device
38 )
39
40 - self.supports_mm_inputs = MULTIMODAL_REGISTRY.supports_multimodal_inputs(
41 - self.draft_model_config
42 - )
43 - if self.supports_mm_inputs:
44 - self.inputs_embeds = torch.zeros(
45 - self.max_num_tokens, self.hidden_size, dtype=self.dtype, device=device
46 - )
40 + self.inputs_embeds: torch.Tensor | None = None
41
42 self.prefill_cudagraph_manager: SpeculatorCudaGraphManager | None = None
43 self.decode_cudagraph_manager: SpeculatorCudaGraphManager | None = None
44
45 + def load_model(self, target_model: nn.Module) -> None:
46 + super().load_model(target_model)
47 + if not self.supports_mm_inputs:
48 + return
49 +
50 + self.inputs_embeds = torch.zeros(
51 + self.max_num_tokens,
52 + self.hidden_size,
53 + dtype=self.dtype,
54 + device=self.device,
55 + )
56 +
57 @property
58 def advance_draft_positions(self) -> bool:
59 """
@@ -295,6 +301,7 @@ class AutoRegressiveSpeculator(DraftModelSpeculator):
301 ):
302 inputs_embeds = None
303 if self.supports_mm_inputs:
304 + assert self.inputs_embeds is not None
305 # Merge multimodal embeddings with input ids.
306 mm_embeds, is_mm_embed = mm_inputs or (None, None)
307 num_input_tokens = (
vllm/v1/worker/gpu/spec_decode/multi_module_mtp/speculator.py
+19 -27
@@ -9,7 +9,6 @@ from vllm.config import VllmConfig
9 from vllm.config.compilation import CUDAGraphMode
10 from vllm.forward_context import BatchDescriptor, set_forward_context
11 from vllm.logger import init_logger
12 -from vllm.multimodal import MULTIMODAL_REGISTRY
12 from vllm.triton_utils import tl, triton
13 from vllm.v1.attention.backends.utils import PAD_SLOT_ID
14 from vllm.v1.worker.gpu.attn_utils import build_slot_mappings_by_layer
@@ -39,25 +38,7 @@ class MultiModuleMTPSpeculator(DraftModelSpeculator):
38 self.max_num_reqs, dtype=torch.int64, device=device
39 )
40
42 - self.supports_mm_inputs = MULTIMODAL_REGISTRY.supports_multimodal_inputs(
43 - self.draft_model_config
44 - )
45 - # HACK: the Inkling MTP draft has no MM processor of its own (its draft
46 - # config is flattened text-only), but it consumes the target's merged
47 - # embeddings at draft prefill — treat it as MM-capable whenever the
48 - # target is.
49 - if (
50 - not self.supports_mm_inputs
51 - and self.draft_model_config.hf_config.model_type == "inkling_mtp"
52 - ):
53 - self.supports_mm_inputs = MULTIMODAL_REGISTRY.supports_multimodal_inputs(
54 - vllm_config.model_config
55 - )
41 self.inputs_embeds: torch.Tensor | None = None
57 - if self.supports_mm_inputs:
58 - self.inputs_embeds = torch.zeros(
59 - self.max_num_tokens, self.hidden_size, dtype=self.dtype, device=device
60 - )
42
43 # Input id overrides for the last num_speculative_steps - 1 draft steps.
44 # Used by chunked-prefilling requests to swap in the future prefill token
@@ -80,14 +61,6 @@ class MultiModuleMTPSpeculator(DraftModelSpeculator):
61 device=self.device,
62 )
63 self.cached_draft_input_embeds: torch.Tensor | None = None
83 - if self.supports_mm_inputs:
84 - self.cached_draft_input_embeds = torch.zeros(
85 - self.max_num_reqs,
86 - self.num_speculative_steps - 1,
87 - self.hidden_size,
88 - dtype=self.dtype,
89 - device=self.device,
90 - )
64 self.cached_target_hidden_states = torch.zeros(
65 self.max_num_reqs,
66 self.num_speculative_steps - 1,
@@ -105,6 +78,25 @@ class MultiModuleMTPSpeculator(DraftModelSpeculator):
78 ) -> nn.Module:
79 return load_eagle_model(target_model, self.vllm_config)
80
81 + def load_model(self, target_model: nn.Module) -> None:
82 + super().load_model(target_model)
83 + if not self.supports_mm_inputs:
84 + return
85 +
86 + self.inputs_embeds = torch.zeros(
87 + self.max_num_tokens,
88 + self.hidden_size,
89 + dtype=self.dtype,
90 + device=self.device,
91 + )
92 + self.cached_draft_input_embeds = torch.zeros(
93 + self.max_num_reqs,
94 + self.num_speculative_steps - 1,
95 + self.hidden_size,
96 + dtype=self.dtype,
97 + device=self.device,
98 + )
99 +
100 def init_cudagraph_manager(self, cudagraph_mode: CUDAGraphMode) -> None:
101 # TODO(TheEpicDolphin): Support piecewise cudagraph for multi-module MTP.
102 if cudagraph_mode.has_piecewise_cudagraphs():
vllm/v1/worker/gpu/spec_decode/speculator.py
+17
@@ -13,6 +13,8 @@ from vllm.config.compilation import CUDAGraphMode
13 from vllm.distributed.eplb.eplb_state import EplbState
14 from vllm.logger import init_logger
15 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
16 +from vllm.model_executor.models import supports_multimodal_embeddings
17 +from vllm.multimodal import MULTIMODAL_REGISTRY
18 from vllm.v1.kv_cache_interface import KVCacheConfig
19 from vllm.v1.worker.gpu.attn_utils import (
20 build_attn_metadata,
@@ -137,6 +139,8 @@ class DraftModelSpeculator(BaseSpeculator):
139 device=device,
140 )
141
142 + self.supports_mm_inputs = False
143 +
144 @abstractmethod
145 def load_draft_model(
146 self,
@@ -164,6 +168,19 @@ class DraftModelSpeculator(BaseSpeculator):
168 )
169 self.draft_attn_layer_names = all_attn_layers - target_attn_layer_names
170
171 + target_supports_mm = MULTIMODAL_REGISTRY.supports_multimodal_inputs(
172 + self.vllm_config.model_config
173 + )
174 + draft_supports_mm = supports_multimodal_embeddings(self.model)
175 + self.supports_mm_inputs = target_supports_mm and draft_supports_mm
176 + if target_supports_mm and not draft_supports_mm:
177 + logger.warning_once(
178 + "Draft model %s does not support external multimodal embeddings. "
179 + "Embeddings from the target model will not be passed to the "
180 + "drafter; using text-only draft inputs instead.",
181 + type(self.model).__name__,
182 + )
183 +
184 def set_eplb_state(self, eplb_state: EplbState) -> None:
185 """Inject EPLB state after construction."""
186 self.eplb_state = eplb_state