[Bugfix][Model] Fix Qwen3.5 MTP for text-only checkpoints (#50734)

Signed-off-by: efschu <51944948+efschu@users.noreply.github.com>

efschu committed Aug 10, 2026 at 13:04 UTC 900d09f91a1fe164c0df175c334060cad68ce245
3 files changed +55 -7
tests/models/test_qwen3_5_mtp_config.py new
+33
@@ -0,0 +1,33 @@
1 +# SPDX-License-Identifier: Apache-2.0
2 +# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 +"""CPU-only tests for Qwen3.5 text-only MTP speculative decoding."""
4 +
5 +import pytest
6 +from transformers import PretrainedConfig
7 +
8 +from vllm.config.speculative import SpeculativeConfig
9 +
10 +
11 +def _mtp_config(model_type: str) -> PretrainedConfig:
12 + return PretrainedConfig(
13 + model_type=model_type,
14 + architectures=["SomeArch"],
15 + mtp_num_hidden_layers=1,
16 + )
17 +
18 +
19 +@pytest.mark.parametrize(
20 + "model_type,expected_arch",
21 + [
22 + ("qwen3_5", "Qwen3_5MTP"),
23 + ("qwen3_5_moe", "Qwen3_5MoeMTP"),
24 + # Text-only config variants must map to the same MTP architectures.
25 + ("qwen3_5_text", "Qwen3_5MTP"),
26 + ("qwen3_5_moe_text", "Qwen3_5MoeMTP"),
27 + ],
28 +)
29 +def test_mtp_override_recognizes_text_only_types(model_type, expected_arch):
30 + cfg = SpeculativeConfig.hf_config_override(_mtp_config(model_type))
31 + assert cfg.model_type == "qwen3_5_mtp"
32 + assert cfg.architectures == [expected_arch]
33 + assert cfg.n_predict == 1
vllm/config/speculative.py
+10 -2
@@ -528,8 +528,16 @@ class SpeculativeConfig:
528 hf_config.update(
529 {"n_predict": n_predict, "architectures": ["Exaone4_5_MTP"]}
530 )
531 - if hf_config.model_type in ("qwen3_5", "qwen3_5_moe"):
532 - is_moe = hf_config.model_type == "qwen3_5_moe"
531 + if hf_config.model_type in (
532 + "qwen3_5",
533 + "qwen3_5_moe",
534 + "qwen3_5_text",
535 + "qwen3_5_moe_text",
536 + ):
537 + # Checkpoints that ship only the text config resolve to the
538 + # `qwen3_5_text` / `qwen3_5_moe_text` model types and carry the
539 + # same `mtp_num_hidden_layers` field as the multimodal ones.
540 + is_moe = hf_config.model_type in ("qwen3_5_moe", "qwen3_5_moe_text")
541 hf_config.model_type = "qwen3_5_mtp"
542 n_predict = getattr(hf_config, "mtp_num_hidden_layers", None)
543 hf_config.update(
vllm/multimodal/registry.py
+12 -5
@@ -114,11 +114,18 @@ class MultiModalRegistry:
114 try:
115 info = self._create_processing_info(model_config, tokenizer=None)
116 except ValueError:
117 - logger.warning_once(
118 - "Model %s is treated as multimodal but has no registered "
119 - "multimodal processor; running in text-only mode.",
120 - model_config.model,
121 - )
117 + # Speculative drafters for multimodal targets (e.g. Qwen3_5MTP,
118 + # Exaone4_5_MTP, MiMoV2OmniMTP) declare `SupportsMultiModal` so
119 + # that they can consume the embeddings merged by the target model,
120 + # but they never run a multi-modal processor of their own. Running
121 + # in text-only mode is the expected outcome for them, not a
122 + # misconfiguration worth warning about.
123 + if model_config.runner_type != "draft":
124 + logger.warning_once(
125 + "Model %s is treated as multimodal but has no registered "
126 + "multimodal processor; running in text-only mode.",
127 + model_config.model,
128 + )
129 return False
130
131 # Check if all supported modalities have limit == 0