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