main
py 467 lines 17.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Inference-only Qwen4Exp MTP (Multi-Token Predictor) model.
4
5 The MTP draft model reuses the Qwen4Exp backbone (PLE/HC/MoE) but:
6 - drops all multi-modal handling (text-only),
7 - forces PLE off while keeping the main model's HC stream count,
8 - fuses the backbone hidden and the new-token embedding via
9 ``residual_linear_shared`` (fc_embedding + shared fc_hidden) instead of
10 the ``Linear(2H, H)`` + repeat used by other MTP variants,
11 - emits TWO hidden streams per step (scheme A): a single stream [T, H]
12 (final-mixer collapsed, fed to the LM head) and a pre-final-mixer
13 multi stream [T, hc_count*H] (fed to the next draft step).
14 """
15
16 from collections.abc import Iterable
17
18 import regex as re
19 import torch
20 from torch import nn
21 from transformers import Qwen4ExpTextConfig
22
23 from vllm.compilation.decorators import support_torch_compile
24 from vllm.config import VllmConfig, replace, set_current_vllm_config
25 from vllm.distributed import get_pp_group
26 from vllm.model_executor.layers.fused_moe.utils import (
27 is_model_fused_shared_expert_compatible,
28 )
29 from vllm.model_executor.layers.layernorm import GemmaRMSNorm
30 from vllm.model_executor.layers.linear import ColumnParallelLinear
31 from vllm.model_executor.layers.logits_processor import LogitsProcessor
32 from vllm.model_executor.layers.vocab_parallel_embedding import (
33 ParallelLMHead,
34 VocabParallelEmbedding,
35 )
36 from vllm.model_executor.model_loader.utils import configure_quant_config
37 from vllm.model_executor.models.interfaces import SupportsPP
38 from vllm.model_executor.models.qwen3_5 import Qwen3_5Model
39 from vllm.model_executor.models.utils import (
40 AutoWeightsLoader,
41 PPMissingLayer,
42 WeightsMapper,
43 get_draft_quant_config,
44 make_empty_intermediate_tensors_factory,
45 maybe_fuse_shared_experts,
46 maybe_prefix,
47 )
48 from vllm.sequence import IntermediateTensors
49
50 from .hyperconnection import GatedResidual, HyperConnectionConfig
51 from .low_latency_gemm import enable_qwen4_exp_low_latency_gemm
52 from .model import (
53 _HC_WEIGHTS_MAPPER,
54 _QWEN4_EXP_IGNORED_MISSING_SUFFIXES,
55 Qwen4ExpDecoderLayer,
56 Qwen4ExpMixtureOfExperts,
57 Qwen4ExpSparseMoeBlock,
58 )
59
60
61 def _remap_ignored_layers(
62 ignored_layers: list[str],
63 mtp_start_layer_idx: int,
64 ) -> list[str]:
65 remapped: list[str] = []
66 for name in ignored_layers:
67 if name.startswith("mtp."):
68 new_name = re.sub(
69 r"(?<=\.layers\.)\d+",
70 lambda m: str(mtp_start_layer_idx + int(m.group(0))),
71 name,
72 )
73 remapped.append(new_name)
74 else:
75 remapped.append(name)
76 return remapped
77
78
79 def _remap_quantized_layers(
80 quantized_layers: dict[str, dict],
81 mtp_start_layer_idx: int,
82 ) -> dict[str, dict]:
83 """Map checkpoint MTP layer indices to standalone draft indices."""
84 return {
85 _remap_ignored_layers([name], mtp_start_layer_idx)[0]: layer_info
86 for name, layer_info in quantized_layers.items()
87 }
88
89
90 def _remap_mtp_weight_name(name: str) -> str | None:
91 """Map Qwen4Exp checkpoint paths into the standalone draft model."""
92 for checkpoint_prefix in (
93 "model.language_model.",
94 "language_model.",
95 ):
96 if name.startswith(checkpoint_prefix):
97 name = name.removeprefix(checkpoint_prefix)
98 break
99
100 if name.startswith("embed_tokens."):
101 name = f"model.{name}"
102 if name.startswith("model.mtp."):
103 name = name.removeprefix("model.")
104 if name.startswith("mtp.shared_head.head."):
105 return name.replace("mtp.shared_head.head.", "lm_head.", 1)
106 if name.startswith("model.shared_head.head."):
107 return name.replace("model.shared_head.head.", "lm_head.", 1)
108 if name.startswith("shared_head.head."):
109 return name.replace("shared_head.head.", "lm_head.", 1)
110 if name.startswith("model.lm_head."):
111 return name.removeprefix("model.")
112 if name.startswith("mtp."):
113 return name.replace("mtp.", "model.", 1)
114 if name.startswith("model.embed_tokens.") or name.startswith("lm_head."):
115 return name
116 return None
117
118
119 def _make_draft_vllm_config(
120 vllm_config: VllmConfig,
121 mtp_start_layer_idx: int,
122 ) -> VllmConfig:
123 """Ensure that the draft model config is set in the vLLM config."""
124 speculative_config = vllm_config.speculative_config
125 if speculative_config is None or speculative_config.draft_model_config is None:
126 raise ValueError("speculative_config.draft_model_config must be set")
127
128 draft_quant_config = get_draft_quant_config(vllm_config)
129
130 # inject packed and ignored modules to the quantization config of draft model
131 if draft_quant_config is not None:
132 configure_quant_config(draft_quant_config, Qwen4ExpMTP)
133 ignored_layers = getattr(draft_quant_config, "ignored_layers", None)
134 if ignored_layers:
135 setattr( # noqa: B010
136 draft_quant_config,
137 "ignored_layers",
138 _remap_ignored_layers(ignored_layers, mtp_start_layer_idx),
139 )
140 exclude_modules = getattr(draft_quant_config, "exclude_modules", None)
141 if exclude_modules:
142 setattr( # noqa: B010
143 draft_quant_config,
144 "exclude_modules",
145 _remap_ignored_layers(exclude_modules, mtp_start_layer_idx),
146 )
147 quantized_layers = getattr(draft_quant_config, "quantized_layers", None)
148 if quantized_layers:
149 setattr( # noqa: B010
150 draft_quant_config,
151 "quantized_layers",
152 _remap_quantized_layers(quantized_layers, mtp_start_layer_idx),
153 )
154
155 draft_vllm_config = replace(
156 vllm_config,
157 model_config=speculative_config.draft_model_config,
158 )
159 # VllmConfig post-init derives the target quant config, so restore the
160 # independently resolved draft quant config after replacement.
161 draft_vllm_config.quant_config = draft_quant_config
162 return draft_vllm_config
163
164
165 @support_torch_compile(
166 dynamic_arg_dims={
167 "input_ids": 0,
168 "positions": -1,
169 "intermediate_tensors": 0,
170 "inputs_embeds": 0,
171 "hidden_states": 0,
172 }
173 )
174 class Qwen4ExpMultiTokenPredictor(nn.Module):
175 hf_to_vllm_mapper = Qwen3_5Model.hf_to_vllm_mapper | _HC_WEIGHTS_MAPPER
176
177 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
178 super().__init__()
179
180 model_config = vllm_config.model_config
181 config: Qwen4ExpTextConfig = model_config.hf_text_config
182
183 self.config = config
184 self.vocab_size = config.vocab_size
185
186 self.mtp_start_layer_idx = config.num_hidden_layers
187 self.num_mtp_layers = getattr(config, "mtp_num_hidden_layers", 1)
188
189 self.hidden_size = config.hidden_size
190 self.hc_count = config.hc_count
191
192 self.embed_tokens = VocabParallelEmbedding(self.vocab_size, self.hidden_size)
193 draft_vllm_config = _make_draft_vllm_config(
194 vllm_config,
195 self.mtp_start_layer_idx,
196 )
197 with set_current_vllm_config(draft_vllm_config, prefix=prefix):
198 # residual_linear_shared fusion: fc_embedding projects the token
199 # embedding, fc_hidden (shared across HC branches) projects the
200 # backbone hidden; the embedding is added as a residual to every
201 # branch (see mtp_residual_linear_shared.md).
202 self.fc_embedding = ColumnParallelLinear(
203 self.hidden_size,
204 self.hidden_size,
205 gather_output=True,
206 bias=False,
207 return_bias=False,
208 quant_config=draft_vllm_config.quant_config,
209 prefix=f"{prefix}.fc_embedding",
210 )
211 self.fc_hidden = ColumnParallelLinear(
212 self.hidden_size,
213 self.hidden_size,
214 gather_output=True,
215 bias=False,
216 return_bias=False,
217 quant_config=draft_vllm_config.quant_config,
218 prefix=f"{prefix}.fc_hidden",
219 )
220 self.layers = nn.ModuleList(
221 Qwen4ExpDecoderLayer(
222 draft_vllm_config,
223 layer_type="qwen_sparse_attention",
224 prefix=f"{prefix}.layers.{self.mtp_start_layer_idx + idx}",
225 )
226 for idx in range(self.num_mtp_layers)
227 )
228 self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
229 self.layers,
230 Qwen4ExpSparseMoeBlock,
231 "mlp",
232 )
233
234 self.pre_fc_norm_embedding = GemmaRMSNorm(
235 self.hidden_size, eps=config.rms_norm_eps
236 )
237 self.pre_fc_norm_hidden = GemmaRMSNorm(
238 self.hidden_size * self.hc_count, eps=config.rms_norm_eps
239 )
240 # HC final mixer collapses the multi stream into [T, H] for the LM head.
241 hc_config = HyperConnectionConfig(
242 hc_count=config.hc_count,
243 hidden_size=config.hidden_size,
244 params_dtype=torch.bfloat16,
245 hc_lowrank=config.hc_lowrank,
246 rms_norm_eps=config.rms_norm_eps,
247 hc_per_branch_norm=True,
248 )
249 self.hyper_connection_mixer = GatedResidual(
250 hc_config,
251 use_combine=False,
252 prefix=maybe_prefix(prefix, "hyper_connection_mixer"),
253 )
254 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
255 ["hidden_states"], self.hidden_size * self.hc_count
256 )
257
258 def _iter_qsa_attentions(self):
259 """Yield MTP attention modules that own a QSA indexer."""
260 for layer in self.layers:
261 attention = getattr(layer, "self_attn", None)
262 if (
263 attention is not None
264 and getattr(attention, "indexer", None) is not None
265 ):
266 yield attention
267
268 def set_skip_topk(self, skip: bool) -> None:
269 """Select on MTP step 0 and reuse its QSA indices on later steps."""
270 for attention in self._iter_qsa_attentions():
271 attention.indexer.skip_topk = skip
272
273 def compact_topk_indices(self, row_indices: torch.Tensor) -> None:
274 """Keep each request's target-aligned step-0 sparse-index row."""
275 num_rows = row_indices.numel()
276 for attention in self._iter_qsa_attentions():
277 buffer = attention.topk_indices_buffer
278 selected = buffer.index_select(0, row_indices)
279 buffer[:num_rows].copy_(selected)
280
281 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
282 return self.embed_tokens(input_ids)
283
284 def forward(
285 self,
286 input_ids: torch.Tensor | None,
287 positions: torch.Tensor,
288 hidden_states: torch.Tensor | None = None,
289 intermediate_tensors: IntermediateTensors | None = None,
290 inputs_embeds: torch.Tensor | None = None,
291 spec_step_idx: int = 0,
292 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor] | IntermediateTensors:
293 hc_count = self.hc_count
294 hidden_size = self.hidden_size
295
296 if get_pp_group().is_first_rank:
297 assert hidden_states is not None
298 if inputs_embeds is None:
299 assert input_ids is not None
300 inputs_embeds = self.embed_input_ids(input_ids)
301 # Embedding branch: pre-norm -> fc_embedding -> [T, H].
302 inputs_embeds = self.pre_fc_norm_embedding(inputs_embeds)
303 inputs_embeds = self.fc_embedding(inputs_embeds)
304
305 # Backbone hidden is multi-stream [T, hc_count*H] (scheme A:
306 # the main model truly emits the pre-final-mixer multi stream
307 # on the first step; subsequent steps reuse the prior draft
308 # step's multi stream).
309 num_tokens = hidden_states.shape[0]
310 hidden_states = hidden_states.view(num_tokens, hc_count, hidden_size)
311 hidden_states = self.pre_fc_norm_hidden(hidden_states.flatten(-2)).view(
312 num_tokens, hc_count, hidden_size
313 )
314 hidden_states = self.fc_hidden(hidden_states)
315 # Add the embedding residual to every branch, then fold back
316 # to [T, hc_count*H] (HC outer, HS inner) for the HC decoder.
317 hidden_states = inputs_embeds.unsqueeze(-2) + hidden_states
318 hidden_states = hidden_states.flatten(-2)
319 else:
320 assert intermediate_tensors is not None
321 hidden_states = intermediate_tensors["hidden_states"]
322
323 current_step_idx = spec_step_idx % self.num_mtp_layers
324 layer = self.layers[current_step_idx]
325 hidden_states, block_output, injection = layer(
326 hidden_states=hidden_states,
327 prev_block_output=None,
328 prev_injection=None,
329 positions=positions,
330 input_ids=None,
331 query_start_loc=None,
332 ngram_context=None,
333 )
334 if not get_pp_group().is_last_rank:
335 # As in the target model, PP carries a materialized tensor rather
336 # than the delayed hidden/output/injection tuple.
337 hidden_states = layer.mlp_hyper_connection.combine(
338 hidden_states, block_output, injection
339 )
340 return IntermediateTensors({"hidden_states": hidden_states})
341
342 # Last PP rank finalize. Keep both:
343 # (A) sample_hidden_states [T, H] -> single stream for the LM head
344 # (B) multi_hidden [T, hc_count*H] -> pre-final-mixer multi stream
345 # for the next draft step (zero extra compute, just kept).
346 multi_hidden, sample_hidden_states, _ = (
347 self.hyper_connection_mixer.combine_and_mix(
348 hidden_states, block_output, injection
349 )
350 )
351 return sample_hidden_states, multi_hidden
352
353 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
354 weights = maybe_fuse_shared_experts(
355 weights,
356 enabled=self.is_fused_shared_expert_enabled,
357 n_routed_experts=getattr(self.config, "num_experts", 0) or 0,
358 n_shared_experts=1,
359 ckpt_prefix="mlp.shared_expert",
360 )
361 mapper = self.hf_to_vllm_mapper | WeightsMapper(
362 orig_to_new_substr={"hyper_connection_mixer.block_inject_weight": None}
363 )
364 loader = AutoWeightsLoader(
365 self,
366 ignore_unexpected_suffixes=_QWEN4_EXP_IGNORED_MISSING_SUFFIXES.copy(),
367 )
368 return loader.load_weights(weights, mapper=mapper)
369
370
371 @support_torch_compile(
372 dynamic_arg_dims={
373 "input_ids": 0,
374 "positions": -1,
375 "intermediate_tensors": 0,
376 "inputs_embeds": 0,
377 "hidden_states": 0,
378 }
379 )
380 class Qwen4ExpMTP(nn.Module, SupportsPP, Qwen4ExpMixtureOfExperts):
381 packed_modules_mapping = {
382 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
383 "gate_up_proj": ["gate_proj", "up_proj"],
384 "in_proj_qkvz": ["in_proj_qkv", "in_proj_z"],
385 "in_proj_ba": ["in_proj_b", "in_proj_a"],
386 "input_mix_weight_down_block_inject": [
387 "input_mix_weight_down",
388 "block_inject_weight",
389 "_input_mix_padding",
390 ],
391 }
392
393 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
394 config: Qwen4ExpTextConfig = vllm_config.model_config.hf_text_config
395 self.vllm_config = vllm_config
396 self.quant_config = vllm_config.quant_config
397
398 super().__init__()
399 self.config = config
400 self.model = Qwen4ExpMultiTokenPredictor(
401 vllm_config=vllm_config,
402 prefix=maybe_prefix(prefix, "mtp"),
403 )
404
405 if get_pp_group().is_last_rank:
406 if config.tie_word_embeddings:
407 self.lm_head = self.model.embed_tokens
408 else:
409 self.lm_head = ParallelLMHead(
410 config.vocab_size,
411 config.hidden_size,
412 prefix=maybe_prefix(prefix, "lm_head"),
413 )
414 else:
415 self.lm_head = PPMissingLayer()
416
417 self.logits_processor = LogitsProcessor(config.vocab_size)
418 self.make_empty_intermediate_tensors = (
419 self.model.make_empty_intermediate_tensors
420 )
421 self.set_moe_parameters(self.model.layers)
422 enable_qwen4_exp_low_latency_gemm(self, vllm_config.model_config.dtype)
423
424 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
425 return self.model.embed_input_ids(input_ids)
426
427 def forward(
428 self,
429 input_ids: torch.Tensor | None,
430 positions: torch.Tensor,
431 hidden_states: torch.Tensor | None = None,
432 intermediate_tensors: IntermediateTensors | None = None,
433 inputs_embeds: torch.Tensor | None = None,
434 spec_step_idx: int = 0,
435 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor] | IntermediateTensors:
436 return self.model(
437 input_ids,
438 positions,
439 hidden_states,
440 intermediate_tensors,
441 inputs_embeds,
442 spec_step_idx=spec_step_idx,
443 )
444
445 def compute_logits(
446 self, hidden_states: torch.Tensor, spec_step_idx: int = 0
447 ) -> torch.Tensor | None:
448 return self.logits_processor(self.lm_head, hidden_states)
449
450 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
451 def remap_weight_names():
452 for name, weight in weights:
453 remapped_name = _remap_mtp_weight_name(name)
454 if remapped_name is not None:
455 yield remapped_name, weight
456
457 mapper = WeightsMapper(
458 orig_to_new_substr={"hyper_connection_mixer.block_inject_weight": None}
459 )
460 loader = AutoWeightsLoader(
461 self,
462 ignore_unexpected_suffixes=_QWEN4_EXP_IGNORED_MISSING_SUFFIXES.copy(),
463 )
464 return loader.load_weights(remap_weight_names(), mapper=mapper)
465
466
467 __all__ = ["Qwen4ExpMTP", "Qwen4ExpMultiTokenPredictor"]