main
py 1,039 lines 38.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Inference-only Qwen4Exp model."""
4
5 from collections.abc import Iterable
6 from itertools import islice
7
8 import torch
9 from torch import nn
10 from transformers import Qwen4ExpConfig, Qwen4ExpTextConfig
11
12 from vllm.compilation.decorators import support_torch_compile
13 from vllm.config import VllmConfig
14 from vllm.distributed import get_pp_group
15 from vllm.model_executor.layers.fused_moe.utils import (
16 is_model_fused_shared_expert_compatible,
17 )
18 from vllm.model_executor.layers.fusion.mm_input_norm import build_mm_input_norm
19 from vllm.model_executor.layers.logits_processor import LogitsProcessor
20 from vllm.model_executor.layers.mamba.gdn.qwen_gdn_linear_attn import (
21 QwenGatedDeltaNetAttention,
22 )
23 from vllm.model_executor.layers.mamba.mamba_utils import (
24 MambaStateCopyFunc,
25 MambaStateCopyFuncCalculator,
26 MambaStateCopyFuncsByType,
27 MambaStateDtypeCalculator,
28 MambaStateShapeCalculator,
29 )
30 from vllm.model_executor.layers.quantization import QuantizationConfig
31 from vllm.model_executor.layers.vocab_parallel_embedding import (
32 ParallelLMHead,
33 VocabParallelEmbedding,
34 )
35 from vllm.model_executor.models.interfaces import (
36 HasInnerState,
37 IsHybrid,
38 MixtureOfExperts,
39 MultiModalEmbeddings,
40 SupportsLoRA,
41 SupportsMRoPE,
42 SupportsPP,
43 _require_is_multimodal,
44 )
45 from vllm.model_executor.models.qwen3_5 import (
46 Qwen3_5ForConditionalGeneration,
47 Qwen3_5Model,
48 )
49 from vllm.model_executor.models.qwen3_next import (
50 Qwen3NextAttention,
51 Qwen3NextSparseMoeBlock,
52 )
53 from vllm.model_executor.models.qwen3_vl import (
54 Qwen3_VisionTransformer,
55 Qwen3VLDummyInputsBuilder,
56 Qwen3VLMultiModalProcessor,
57 Qwen3VLProcessingInfo,
58 )
59 from vllm.model_executor.models.utils import (
60 AutoWeightsLoader,
61 StageMissingLayer,
62 WeightsMapper,
63 _merge_multimodal_embeddings,
64 extract_layer_index,
65 make_empty_intermediate_tensors_factory,
66 make_layers,
67 maybe_fuse_shared_experts,
68 maybe_prefix,
69 )
70 from vllm.multimodal import MULTIMODAL_REGISTRY
71 from vllm.multimodal.inputs import MultiModalFeatureSpec
72 from vllm.sequence import IntermediateTensors
73 from vllm.tokenizers.registry import cached_tokenizer_from_config
74 from vllm.v1.attention.backends.registry import MambaAttentionBackendEnum
75 from vllm.v1.kv_cache_interface import MambaSpec
76
77 from .hyperconnection import GatedResidual, HyperConnectionConfig
78 from .low_latency_gemm import enable_qwen4_exp_low_latency_gemm
79 from .ple_layer import Qwen4ExpPLELayer
80 from .qsa import Qwen4ExpQSAAttention
81
82
83 def without_modelopt_fp4(
84 quant_config: QuantizationConfig | None,
85 ) -> QuantizationConfig | None:
86 """Return ``None`` for weights excluded from Qwen4Exp ModelOpt-FP4."""
87 if quant_config is not None and quant_config.get_name() == "modelopt_fp4":
88 return None
89 return quant_config
90
91
92 def _remap_qsa_cache_scale_name(
93 name: str,
94 qsa_layer_ids: frozenset[int],
95 ) -> str:
96 """Map serialized main-cache scales onto the merged QSA owner.
97
98 Regular attention keeps cache scales below its ``attn`` child. QSA owns
99 that cache directly, so only QSA layers need the final path component
100 moved to the owner's persistent ``_k_scale``/``_v_scale`` buffers.
101 """
102 scale_suffixes = {
103 "k_proj.k_scale": "_k_scale",
104 "k_proj.output_scale": "_k_scale",
105 "attn.k_scale": "_k_scale",
106 "attn._k_scale": "_k_scale",
107 "k_scale": "_k_scale",
108 "_k_scale": "_k_scale",
109 "v_proj.v_scale": "_v_scale",
110 "v_proj.output_scale": "_v_scale",
111 "attn.v_scale": "_v_scale",
112 "attn._v_scale": "_v_scale",
113 "v_scale": "_v_scale",
114 "_v_scale": "_v_scale",
115 }
116 for layer_id in qsa_layer_ids:
117 marker = f"layers.{layer_id}.self_attn."
118 marker_start = name.find(marker)
119 if marker_start < 0 or (marker_start > 0 and name[marker_start - 1] != "."):
120 continue
121 suffix = name[marker_start + len(marker) :]
122 mapped_suffix = scale_suffixes.get(suffix)
123 if mapped_suffix is not None:
124 return f"{name[: marker_start + len(marker)]}{mapped_suffix}"
125 return name
126
127
128 _QWEN4_EXP_IGNORED_MISSING_SUFFIXES = [
129 ".bias",
130 "_bias",
131 ".k_scale",
132 "_k_scale",
133 ".v_scale",
134 "_v_scale",
135 "_weight_scale",
136 "_input_scale",
137 ]
138
139 # The checkpoint keeps down and injection projections separate; runtime packs
140 # them into adjacent logical shards of one MergedColumnParallelLinear.
141 _HC_WEIGHTS_MAPPER = WeightsMapper(
142 orig_to_new_stacked={
143 "hyper_connection.input_mix_weight_down.weight": (
144 "hyper_connection.input_mix_weight_down_block_inject.weight",
145 0,
146 ),
147 "hyper_connection.block_inject_weight.weight": (
148 "hyper_connection.input_mix_weight_down_block_inject.weight",
149 1,
150 ),
151 }
152 )
153
154
155 class Qwen4ExpSparseMoeBlock(Qwen3NextSparseMoeBlock):
156 """Qwen3Next MoE with Qwen4Exp HC validation."""
157
158 def __init__(self, vllm_config: VllmConfig, prefix: str = "") -> None:
159 parallel_config = vllm_config.parallel_config
160 if parallel_config.use_sequence_parallel_moe:
161 raise NotImplementedError(
162 "Qwen4Exp HC does not support sequence-parallel MoE"
163 )
164 super().__init__(vllm_config=vllm_config, prefix=prefix)
165 config = vllm_config.model_config.hf_text_config
166 self.n_shared_experts = int(config.shared_expert_intermediate_size > 0)
167
168
169 class Qwen4ExpDecoderLayer(nn.Module):
170 def __init__(
171 self,
172 vllm_config: VllmConfig,
173 layer_type: str,
174 prefix: str = "",
175 ) -> None:
176 super().__init__()
177 config: Qwen4ExpTextConfig = vllm_config.model_config.hf_text_config
178 quant_config = vllm_config.quant_config
179
180 self.config = config
181 self.layer_type = layer_type
182 self.layer_idx = extract_layer_index(prefix)
183 if vllm_config.parallel_config.use_sequence_parallel_moe:
184 raise NotImplementedError(
185 "Qwen4Exp HC does not support sequence-parallel MoE"
186 )
187 self.ple: Qwen4ExpPLELayer | None = None
188 ple_layer_ids = config.ple_layer_ids
189 if (self.layer_idx + 1) in ple_layer_ids:
190 ple_layer_ids_sorted = sorted(set(ple_layer_ids))
191 ple_dense_layer_id_map = {
192 abs_id: idx for idx, abs_id in enumerate(ple_layer_ids_sorted)
193 }
194 ple_dense_layer_id = ple_dense_layer_id_map[self.layer_idx + 1]
195 self.ple = Qwen4ExpPLELayer(
196 config,
197 vllm_config=vllm_config,
198 layer_idx=self.layer_idx,
199 ple_dense_layer_id=ple_dense_layer_id,
200 prefix=f"{prefix}.ple",
201 )
202
203 if layer_type == "linear_attention":
204 self.linear_attn = QwenGatedDeltaNetAttention(
205 config,
206 vllm_config=vllm_config,
207 prefix=f"{prefix}.linear_attn",
208 gqa_interleaved_layout=False,
209 )
210 elif layer_type == "qwen_sparse_attention":
211 use_qsa = getattr(config, "indexer_n_heads", None) is not None
212 if not use_qsa:
213 self.self_attn = Qwen3NextAttention(
214 config,
215 model_config=vllm_config.model_config,
216 cache_config=vllm_config.cache_config,
217 quant_config=quant_config,
218 prefix=f"{prefix}.self_attn",
219 )
220 else:
221 self.self_attn = Qwen4ExpQSAAttention(
222 vllm_config=vllm_config,
223 config=config,
224 layer_id=self.layer_idx,
225 quant_config=quant_config,
226 prefix=f"{prefix}.self_attn",
227 )
228 else:
229 raise ValueError(f"Invalid layer_type {layer_type}")
230
231 self.mlp = Qwen4ExpSparseMoeBlock(
232 vllm_config=vllm_config, prefix=f"{prefix}.mlp"
233 )
234
235 hc_config = HyperConnectionConfig(
236 hc_count=config.hc_count,
237 hidden_size=config.hidden_size,
238 params_dtype=torch.bfloat16,
239 hc_lowrank=config.hc_lowrank,
240 rms_norm_eps=config.rms_norm_eps,
241 hc_per_branch_norm=True,
242 )
243 self.attn_hyper_connection = GatedResidual(
244 hc_config,
245 prefix=maybe_prefix(prefix, "attn_hyper_connection"),
246 )
247 self.mlp_hyper_connection = GatedResidual(
248 hc_config,
249 prefix=maybe_prefix(prefix, "mlp_hyper_connection"),
250 )
251
252 def forward(
253 self,
254 hidden_states: torch.Tensor,
255 prev_block_output: torch.Tensor | None,
256 prev_injection: torch.Tensor | None,
257 positions: torch.Tensor,
258 *,
259 input_ids: torch.Tensor | None,
260 query_start_loc: torch.Tensor | None,
261 ngram_context: torch.Tensor | None,
262 ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
263 attn_hc = self.attn_hyper_connection
264 if self.ple is not None:
265 # PLE adds directly to the multi-stream state, so pending HC state
266 # must be materialized before the addition.
267 if prev_block_output is not None and prev_injection is not None:
268 hidden_states = attn_hc.combine(
269 hidden_states, prev_block_output, prev_injection
270 )
271 prev_block_output = prev_injection = None
272
273 if input_ids is None or query_start_loc is None or ngram_context is None:
274 raise RuntimeError("PLE inputs were not prepared")
275 hidden_states = hidden_states + self.ple(
276 hidden_states,
277 input_ids,
278 query_start_loc,
279 ngram_context,
280 )
281
282 # Fuse a pending combine with this HC module's mix when possible.
283 if prev_block_output is not None and prev_injection is not None:
284 hidden_states, block_input, injection = attn_hc.combine_and_mix(
285 hidden_states, prev_block_output, prev_injection
286 )
287 else:
288 hidden_states, block_input, injection = attn_hc.mix(hidden_states)
289
290 if self.layer_type == "linear_attention":
291 attn_out = self.linear_attn(hidden_states=block_input)
292 elif self.layer_type == "qwen_sparse_attention":
293 attn_out = self.self_attn(
294 hidden_states=block_input,
295 positions=positions,
296 )
297 else:
298 raise ValueError("Invalid layer_type")
299
300 mlp_hc = self.mlp_hyper_connection
301 hidden_states, block_input, injection = mlp_hc.combine_and_mix(
302 hidden_states, attn_out, injection
303 )
304 mlp_out = self.mlp(block_input)
305 return hidden_states, mlp_out, injection
306
307
308 class Qwen4ExpMixtureOfExperts(MixtureOfExperts):
309 """Expose Qwen4Exp routed experts through vLLM's EPLB protocol."""
310
311 def set_moe_parameters(self, layers: Iterable[nn.Module]) -> None:
312 self.moe_layers = []
313 self.moe_mlp_layers = []
314 example_moe = None
315 for layer in layers:
316 if isinstance(layer, Qwen4ExpDecoderLayer) and isinstance(
317 layer.mlp, Qwen4ExpSparseMoeBlock
318 ):
319 example_moe = layer.mlp
320 self.moe_mlp_layers.append(layer.mlp)
321 self.moe_layers.append(layer.mlp.experts)
322
323 self.num_moe_layers = len(self.moe_layers)
324 if example_moe is None:
325 self.num_expert_groups = 0
326 self.num_shared_experts = 0
327 self.num_logical_experts = 0
328 self.num_physical_experts = 0
329 self.num_local_physical_experts = 0
330 self.num_routed_experts = 0
331 self.num_redundant_experts = 0
332 return
333
334 self.num_expert_groups = 1
335 self.num_shared_experts = example_moe.n_shared_experts
336 self.num_logical_experts = example_moe.n_logical_experts
337 self.num_physical_experts = example_moe.n_physical_experts
338 self.num_local_physical_experts = example_moe.n_local_physical_experts
339 self.num_routed_experts = example_moe.n_routed_experts
340 self.num_redundant_experts = example_moe.n_redundant_experts
341
342 def update_physical_experts_metadata(
343 self,
344 num_physical_experts: int,
345 num_local_physical_experts: int,
346 ) -> None:
347 assert self.num_local_physical_experts == num_local_physical_experts
348 self.num_physical_experts = num_physical_experts
349 self.num_local_physical_experts = num_local_physical_experts
350 self.num_redundant_experts = num_physical_experts - self.num_logical_experts
351 for moe in self.moe_mlp_layers:
352 moe.n_physical_experts = num_physical_experts
353 moe.n_local_physical_experts = num_local_physical_experts
354 moe.n_redundant_experts = self.num_redundant_experts
355 moe.experts.update_expert_map()
356
357
358 @support_torch_compile(
359 dynamic_arg_dims={
360 "input_ids": 0,
361 "positions": -1,
362 "intermediate_tensors": 0,
363 "inputs_embeds": 0,
364 "query_start_loc": 0,
365 "ngram_context": 0,
366 "deepstack_input_embeds": 0,
367 }
368 )
369 class Qwen4ExpModel(nn.Module):
370 hf_to_vllm_mapper = Qwen3_5Model.hf_to_vllm_mapper | _HC_WEIGHTS_MAPPER
371
372 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
373 super().__init__()
374 config: Qwen4ExpTextConfig = vllm_config.model_config.hf_text_config
375 self.config = config
376 self.num_redundant_experts = (
377 vllm_config.parallel_config.eplb_config.num_redundant_experts
378 )
379 self.vocab_size = config.vocab_size
380 self._qsa_layer_ids = frozenset(
381 layer_idx
382 for layer_idx, layer_type in enumerate(config.layer_types)
383 if layer_type == "qwen_sparse_attention"
384 and getattr(config, "indexer_n_heads", None) is not None
385 )
386 self.embed_tokens = VocabParallelEmbedding(self.vocab_size, config.hidden_size)
387
388 def get_layer(prefix: str) -> Qwen4ExpDecoderLayer:
389 layer_idx = extract_layer_index(prefix)
390 return Qwen4ExpDecoderLayer(
391 vllm_config,
392 layer_type=config.layer_types[layer_idx],
393 prefix=prefix,
394 )
395
396 self.start_layer, self.end_layer, self.layers = make_layers(
397 config.num_hidden_layers, get_layer, prefix=f"{prefix}.layers"
398 )
399 self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
400 self.layers,
401 Qwen4ExpSparseMoeBlock,
402 "mlp",
403 )
404 intermediate_size = config.hidden_size * config.hc_count
405 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
406 ["hidden_states"], intermediate_size
407 )
408
409 self.hyper_connection_mixer: GatedResidual | None
410 if get_pp_group().is_last_rank:
411 hc_config = HyperConnectionConfig(
412 hc_count=config.hc_count,
413 hidden_size=config.hidden_size,
414 params_dtype=torch.bfloat16,
415 hc_lowrank=config.hc_lowrank,
416 rms_norm_eps=config.rms_norm_eps,
417 hc_per_branch_norm=True,
418 )
419 self.hyper_connection_mixer = GatedResidual(
420 hc_config,
421 use_combine=False,
422 prefix=maybe_prefix(prefix, "hyper_connection_mixer"),
423 )
424 else:
425 self.hyper_connection_mixer = None
426
427 spec_config = vllm_config.speculative_config
428 # MTP HC multi-stream outputs: when speculative method=="mtp" and the
429 # model uses HC with hc_count>1, retain the pre-final-mixer multi-stream
430 # hidden state [T, hc_count*H] so the MTP drafter can feed a real
431 # multi-stream backbone hidden on its first step (scheme A). Derived
432 # purely from config (NOT node identity) so P/D nodes stay consistent.
433 needs_mtp_hidden = (
434 spec_config is not None
435 and getattr(spec_config, "method", None) == "mtp"
436 and get_pp_group().is_last_rank
437 )
438 if needs_mtp_hidden:
439 self._mtp_hidden_buffer = torch.empty(
440 vllm_config.scheduler_config.max_num_batched_tokens,
441 config.hc_count * config.hidden_size,
442 dtype=vllm_config.model_config.dtype,
443 )
444 else:
445 self._mtp_hidden_buffer = None
446
447 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
448 return self.embed_tokens(input_ids)
449
450 def forward(
451 self,
452 input_ids: torch.Tensor | None,
453 positions: torch.Tensor,
454 intermediate_tensors: IntermediateTensors | None = None,
455 inputs_embeds: torch.Tensor | None = None,
456 query_start_loc: torch.Tensor | None = None,
457 ngram_context: torch.Tensor | None = None,
458 deepstack_input_embeds: IntermediateTensors | None = None,
459 ) -> torch.Tensor | IntermediateTensors:
460 if get_pp_group().is_first_rank:
461 if inputs_embeds is not None:
462 hidden_states = inputs_embeds
463 else:
464 if input_ids is None:
465 raise ValueError("input_ids or inputs_embeds is required")
466 hidden_states = self.embed_input_ids(input_ids)
467 hidden_states = hidden_states.repeat(1, self.config.hc_count)
468 else:
469 if intermediate_tensors is None:
470 raise ValueError("pipeline stage requires intermediate tensors")
471 hidden_states = intermediate_tensors["hidden_states"]
472
473 block_output = None
474 injection = None
475 last_layer = None
476 for layer_idx, layer in islice(
477 enumerate(self.layers), self.start_layer, self.end_layer
478 ):
479 last_layer = layer
480 hidden_states, block_output, injection = layer(
481 hidden_states=hidden_states,
482 prev_block_output=block_output,
483 prev_injection=injection,
484 positions=positions,
485 input_ids=input_ids,
486 query_start_loc=query_start_loc,
487 ngram_context=ngram_context,
488 )
489 if deepstack_input_embeds is not None and layer_idx < len(
490 deepstack_input_embeds
491 ):
492 deepstack_embed = deepstack_input_embeds[
493 f"deepstack_input_embeds_{layer_idx}"
494 ]
495 deepstack_embed = (
496 deepstack_embed.unsqueeze(-2)
497 .expand(
498 *deepstack_embed.shape[:-1],
499 self.config.hc_count,
500 self.config.hidden_size,
501 )
502 .flatten(-2)
503 )
504 # Deepstack is an external addition to the materialized
505 # multi-stream state and therefore terminates delayed combine.
506 hidden_states = layer.mlp_hyper_connection.combine(
507 hidden_states, block_output, injection
508 )
509 block_output = None
510 injection = None
511 hidden_states = hidden_states + deepstack_embed
512
513 if not get_pp_group().is_last_rank:
514 # PP transports one tensor, not the delayed HC tuple. Materialize
515 # with the HC module that produced the pending injection.
516 if last_layer is not None and block_output is not None:
517 hidden_states = last_layer.mlp_hyper_connection.combine(
518 hidden_states, block_output, injection
519 )
520 return IntermediateTensors({"hidden_states": hidden_states})
521
522 # The final mixer consumes the last pending combine and returns both
523 # the sampled single stream and the materialized multi-stream state.
524 final_mixer = self.hyper_connection_mixer
525 assert final_mixer is not None
526 multi_hidden, sample_hidden_states, _ = final_mixer.combine_and_mix(
527 hidden_states, block_output, injection
528 )
529 if self._mtp_hidden_buffer is not None:
530 # Capture the pre-final-mixer multi-stream hidden state
531 # [T, hc_count*H] for the MTP drafter (zero extra compute:
532 # this tensor is needed by the final mixer regardless).
533 num_tokens = multi_hidden.shape[0]
534 self._mtp_hidden_buffer[:num_tokens].copy_(multi_hidden)
535 return sample_hidden_states
536
537 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
538 weights = (
539 (
540 _remap_qsa_cache_scale_name(name, self._qsa_layer_ids),
541 weight,
542 )
543 for name, weight in weights
544 )
545 weights = maybe_fuse_shared_experts(
546 weights,
547 enabled=self.is_fused_shared_expert_enabled,
548 n_routed_experts=getattr(self.config, "num_experts", 0) or 0,
549 n_shared_experts=1,
550 ckpt_prefix="mlp.shared_expert",
551 )
552 # Non-persistent PLE state rebuilt in __init__; skip any ckpt
553 # column for them.
554 skip_substrs = (
555 "hashstats_",
556 "token_lookup",
557 "hyper_connection_mixer.block_inject_weight",
558 )
559 mapper = self.hf_to_vllm_mapper | WeightsMapper(
560 orig_to_new_substr={substr: None for substr in skip_substrs}
561 )
562 # The final HC mixer only exists on the last PP rank; earlier ranks
563 # must drop its checkpoint weights instead of failing to place them.
564 ignore_prefixes = (
565 None
566 if self.hyper_connection_mixer is not None
567 else ["hyper_connection_mixer."]
568 )
569 loader = AutoWeightsLoader(
570 self,
571 ignore_unexpected_prefixes=ignore_prefixes,
572 ignore_unexpected_suffixes=_QWEN4_EXP_IGNORED_MISSING_SUFFIXES.copy(),
573 )
574 loaded = loader.load_weights(
575 weights,
576 mapper=mapper,
577 )
578 return loaded
579
580
581 class Qwen4ExpForCausalLM(
582 nn.Module,
583 HasInnerState,
584 SupportsLoRA,
585 SupportsMRoPE,
586 SupportsPP,
587 Qwen4ExpMixtureOfExperts,
588 IsHybrid,
589 ):
590 packed_modules_mapping = {
591 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
592 "gate_up_proj": ["gate_proj", "up_proj"],
593 "in_proj_qkvz": ["in_proj_qkv", "in_proj_z"],
594 "in_proj_ba": ["in_proj_b", "in_proj_a"],
595 "input_mix_weight_down_block_inject": [
596 "input_mix_weight_down",
597 "block_inject_weight",
598 "_input_mix_padding",
599 ],
600 }
601 hf_to_vllm_mapper = WeightsMapper(
602 orig_to_new_prefix={"model.language_model.": "model."}
603 )
604 requires_raw_input_tokens = True
605
606 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
607 super().__init__()
608 config: Qwen4ExpTextConfig = vllm_config.model_config.hf_text_config
609 self.vllm_config = vllm_config
610 self.model_config = vllm_config.model_config
611 self.quant_config = vllm_config.quant_config
612 self.config = config
613 self.scheduler_config = vllm_config.scheduler_config
614 self.model = Qwen4ExpModel(
615 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
616 )
617 self.lm_head = ParallelLMHead(
618 config.vocab_size,
619 config.hidden_size,
620 prefix=maybe_prefix(prefix, "lm_head"),
621 )
622 self.logits_processor = LogitsProcessor(config.vocab_size)
623 self.make_empty_intermediate_tensors = (
624 self.model.make_empty_intermediate_tensors
625 )
626 self.set_moe_parameters(self.model.layers)
627 enable_qwen4_exp_low_latency_gemm(self, self.model_config.dtype)
628
629 @staticmethod
630 def get_model_state_cls():
631 from .model_state import Qwen4ExpModelState
632
633 return Qwen4ExpModelState
634
635 def forward(
636 self,
637 input_ids: torch.Tensor | None,
638 positions: torch.Tensor,
639 intermediate_tensors: IntermediateTensors | None = None,
640 inputs_embeds: torch.Tensor | None = None,
641 **kwargs: object,
642 ) -> torch.Tensor | IntermediateTensors:
643 # Forward kwargs unchanged so the runner's _maybe_add_ngram_kwargs
644 # path (query_start_loc / ngram_context) reaches Qwen4ExpModel.
645 return self.model(
646 input_ids,
647 positions,
648 intermediate_tensors,
649 inputs_embeds,
650 **kwargs,
651 )
652
653 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
654 return self.model.embed_input_ids(input_ids)
655
656 @classmethod
657 def get_ple_mamba_state_dtype_from_config(
658 cls,
659 vllm_config: VllmConfig,
660 ) -> tuple[torch.dtype, ...]:
661 return MambaStateDtypeCalculator.short_conv_state_dtype(
662 vllm_config.model_config.dtype,
663 vllm_config.cache_config.mamba_cache_dtype,
664 )
665
666 @classmethod
667 def get_ple_mamba_state_shape_from_config(
668 cls, vllm_config: VllmConfig
669 ) -> tuple[tuple[int, int]]:
670 hf_config = vllm_config.model_config.hf_text_config
671 conv_kernel_size = hf_config.ple_conv_kernel_size
672 short_conv_dilation = hf_config.ngram_size
673 conv_state_len = (conv_kernel_size - 1) * short_conv_dilation
674 num_spec = (
675 vllm_config.speculative_config.num_speculative_tokens
676 if vllm_config.speculative_config
677 else 0
678 )
679 hc_count = hf_config.hc_count
680 hc_hidden_size = hf_config.hidden_size * hc_count
681 return MambaStateShapeCalculator.short_conv_state_shape(
682 tp_world_size=1,
683 intermediate_size=hc_hidden_size,
684 conv_kernel=conv_state_len + 1,
685 num_spec=num_spec,
686 )
687
688 @classmethod
689 def get_gdn_mamba_state_dtype_from_config(
690 cls, vllm_config: VllmConfig
691 ) -> tuple[torch.dtype, torch.dtype]:
692 return MambaStateDtypeCalculator.gated_delta_net_state_dtype(
693 vllm_config.model_config.dtype,
694 vllm_config.cache_config.mamba_cache_dtype,
695 vllm_config.cache_config.mamba_ssm_cache_dtype,
696 )
697
698 @classmethod
699 def get_gdn_mamba_state_shape_from_config(
700 cls, vllm_config: VllmConfig
701 ) -> tuple[tuple[int, int], tuple[int, int]]:
702 parallel_config = vllm_config.parallel_config
703 hf_config = vllm_config.model_config.hf_text_config
704 tp_size = parallel_config.tensor_parallel_size
705 num_spec = (
706 vllm_config.speculative_config.num_speculative_tokens
707 if vllm_config.speculative_config
708 else 0
709 )
710 return MambaStateShapeCalculator.gated_delta_net_state_shape(
711 tp_size,
712 hf_config.linear_num_key_heads,
713 hf_config.linear_num_value_heads,
714 hf_config.linear_key_head_dim,
715 hf_config.linear_value_head_dim,
716 hf_config.linear_conv_kernel_dim,
717 num_spec,
718 )
719
720 @classmethod
721 def get_mamba_state_dtype_from_config(
722 cls,
723 vllm_config: VllmConfig,
724 ) -> tuple[torch.dtype, torch.dtype]:
725 return cls.get_gdn_mamba_state_dtype_from_config(vllm_config)
726
727 @classmethod
728 def get_mamba_state_shape_from_config(
729 cls, vllm_config: VllmConfig
730 ) -> tuple[tuple[int, int], tuple[int, int]]:
731 return cls.get_gdn_mamba_state_shape_from_config(vllm_config)
732
733 @classmethod
734 def get_mamba_state_copy_func(
735 cls,
736 ) -> tuple[MambaStateCopyFunc, MambaStateCopyFunc]:
737 return MambaStateCopyFuncCalculator.gated_delta_net_state_copy_func()
738
739 @classmethod
740 def get_mamba_state_copy_funcs(
741 cls,
742 mamba_types: set[MambaAttentionBackendEnum],
743 ) -> MambaStateCopyFuncsByType:
744 copy_funcs_by_type = {
745 MambaAttentionBackendEnum.GDN_ATTN: cls.get_mamba_state_copy_func(),
746 MambaAttentionBackendEnum.SHORT_CONV: (
747 MambaStateCopyFuncCalculator.short_conv_state_copy_func()
748 ),
749 }
750 missing_types = mamba_types - copy_funcs_by_type.keys()
751 assert not missing_types, f"missing state copy funcs for {missing_types}"
752 return {
753 mamba_type: copy_funcs_by_type[mamba_type] for mamba_type in mamba_types
754 }
755
756 @classmethod
757 def get_mamba_specs_from_config(
758 cls, vllm_config: VllmConfig
759 ) -> tuple[MambaSpec, ...]:
760 """Return all MambaSpecs for this model (GDN layers + PLE layer).
761
762 The PLE layer uses a separate short_conv MambaSpec whose page_size_bytes
763 may exceed the GDN spec; callers should take the maximum.
764 """
765 return (
766 MambaSpec(
767 shapes=cls.get_gdn_mamba_state_shape_from_config(vllm_config),
768 dtypes=cls.get_gdn_mamba_state_dtype_from_config(vllm_config),
769 block_size=-1,
770 ),
771 MambaSpec(
772 shapes=cls.get_ple_mamba_state_shape_from_config(vllm_config),
773 dtypes=cls.get_ple_mamba_state_dtype_from_config(vllm_config),
774 block_size=-1,
775 tp_replicated=True,
776 ),
777 )
778
779 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
780 return self.logits_processor(self.lm_head, hidden_states)
781
782 def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
783 return self.model._mtp_hidden_buffer
784
785 def get_mrope_input_positions(
786 self,
787 input_tokens: list[int],
788 mm_features: list[MultiModalFeatureSpec],
789 ) -> tuple[torch.Tensor, int]:
790 positions = torch.arange(len(input_tokens), dtype=torch.long)
791 return positions.unsqueeze(0).expand(3, -1), 0
792
793 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
794 mapper = self.hf_to_vllm_mapper | WeightsMapper(
795 orig_to_new_substr={"mtp.": None}
796 )
797 loader = AutoWeightsLoader(
798 self,
799 ignore_unexpected_suffixes=_QWEN4_EXP_IGNORED_MISSING_SUFFIXES.copy(),
800 )
801 return loader.load_weights(weights, mapper=mapper)
802
803
804 class Qwen4ExpProcessingInfo(Qwen3VLProcessingInfo):
805 def get_hf_config(self) -> Qwen4ExpConfig:
806 return self.ctx.get_hf_config(Qwen4ExpConfig)
807
808
809 @MULTIMODAL_REGISTRY.register_processor(
810 Qwen3VLMultiModalProcessor,
811 info=Qwen4ExpProcessingInfo,
812 dummy_inputs=Qwen3VLDummyInputsBuilder,
813 )
814 class Qwen4ExpForConditionalGeneration(
815 Qwen3_5ForConditionalGeneration,
816 HasInnerState,
817 Qwen4ExpMixtureOfExperts,
818 ):
819 """Qwen3-VL vision tower backed by the Qwen4Exp language model."""
820
821 requires_raw_input_tokens = True
822
823 packed_modules_mapping = Qwen3_5ForConditionalGeneration.packed_modules_mapping | {
824 "input_mix_weight_down_block_inject": [
825 "input_mix_weight_down",
826 "block_inject_weight",
827 "_input_mix_padding",
828 ]
829 }
830
831 @staticmethod
832 def get_model_state_cls():
833 from .model_state import Qwen4ExpModelState
834
835 return Qwen4ExpModelState
836
837 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model") -> None:
838 nn.Module.__init__(self)
839 config: Qwen4ExpConfig = vllm_config.model_config.hf_config
840 quant_config = vllm_config.quant_config
841 multimodal_config = vllm_config.model_config.multimodal_config
842 if multimodal_config is None:
843 raise ValueError(
844 "Qwen4ExpForConditionalGeneration requires multimodal_config"
845 )
846
847 self.config = config
848 self.model_config = vllm_config.model_config
849 self.multimodal_config = multimodal_config
850 self.language_model_only = multimodal_config.language_model_only
851 if self.language_model_only:
852 self.use_data_parallel = False
853 self.is_multimodal_pruning_enabled = False
854 self.video_pruning_method = None
855 self.video_pruning_rate = 0.0
856 self._tokenizer = None
857 self.visual = StageMissingLayer("vision_tower")
858 self._tower_model_names = []
859 else:
860 self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
861 self._init_video_pruning(multimodal_config)
862 self._tokenizer = cached_tokenizer_from_config(vllm_config.model_config)
863
864 with self._mark_tower_model(vllm_config, {"image", "video"}):
865 self.visual = Qwen3_VisionTransformer(
866 config.vision_config,
867 norm_eps=config.text_config.rms_norm_eps,
868 quant_config=quant_config,
869 input_norm=build_mm_input_norm(self.model_config),
870 prefix=maybe_prefix(prefix, "visual"),
871 )
872
873 self.use_deepstack = (
874 not self.language_model_only
875 and bool(config.vision_config.deepstack_visual_indexes)
876 and not isinstance(self.visual, StageMissingLayer)
877 )
878 self.deepstack_num_level = (
879 len(config.vision_config.deepstack_visual_indexes)
880 if self.use_deepstack
881 else 0
882 )
883 self.visual_dim = config.vision_config.out_hidden_size
884 self.multiscale_dim = self.visual_dim * self.deepstack_num_level
885
886 if self.use_deepstack:
887 self.deepstack_input_embeds = [
888 torch.zeros(
889 vllm_config.scheduler_config.max_num_batched_tokens,
890 config.text_config.hidden_size,
891 )
892 for _ in range(self.deepstack_num_level)
893 ]
894 self.deepstack_input_embeds_num_tokens = 0
895
896 with self._mark_language_model(vllm_config):
897 self.language_model = Qwen4ExpForCausalLM(
898 vllm_config=vllm_config,
899 prefix=maybe_prefix(prefix, "language_model"),
900 )
901
902 self.make_empty_intermediate_tensors = (
903 self.language_model.make_empty_intermediate_tensors
904 )
905 if not get_pp_group().is_first_rank and self.use_deepstack:
906 assert self.language_model.model.start_layer >= len(
907 config.vision_config.deepstack_visual_indexes
908 ), (
909 "start_layer should be greater than or equal to "
910 "len(deepstack_visual_indexes)"
911 )
912 self.set_moe_parameters(self.language_model.model.layers)
913
914 def embed_input_ids(
915 self,
916 input_ids: torch.Tensor,
917 multimodal_embeddings: MultiModalEmbeddings | None = None,
918 *,
919 is_multimodal: torch.Tensor | None = None,
920 ) -> torch.Tensor:
921 inputs_embeds = self._embed_text_input_ids(
922 input_ids,
923 self.language_model.embed_input_ids,
924 is_multimodal=is_multimodal,
925 )
926 if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
927 return inputs_embeds
928 if self.language_model_only:
929 raise ValueError(
930 "Qwen4Exp language_model_only does not accept multimodal embeddings"
931 )
932
933 is_multimodal = _require_is_multimodal(is_multimodal)
934 if self.use_deepstack:
935 deepstack_input_embeds, multimodal_embeddings = (
936 self._compute_deepstack_embeds(
937 inputs_embeds=inputs_embeds,
938 multimodal_embeddings=multimodal_embeddings,
939 is_multimodal=is_multimodal,
940 )
941 )
942 else:
943 deepstack_input_embeds = None
944
945 inputs_embeds = _merge_multimodal_embeddings(
946 inputs_embeds=inputs_embeds,
947 multimodal_embeddings=multimodal_embeddings,
948 is_multimodal=is_multimodal,
949 )
950 if deepstack_input_embeds is not None:
951 self._set_deepstack_input_embeds(deepstack_input_embeds)
952 return inputs_embeds
953
954 def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
955 return self.language_model.get_mtp_target_hidden_states()
956
957 def forward(
958 self,
959 input_ids: torch.Tensor | None,
960 positions: torch.Tensor,
961 intermediate_tensors: IntermediateTensors | None = None,
962 inputs_embeds: torch.Tensor | None = None,
963 **kwargs: object,
964 ) -> torch.Tensor | IntermediateTensors:
965 if intermediate_tensors is not None:
966 inputs_embeds = None
967 if inputs_embeds is not None and get_pp_group().is_first_rank:
968 deepstack_input_embeds = self._get_deepstack_input_embeds(
969 inputs_embeds.size(0)
970 )
971 else:
972 deepstack_input_embeds = None
973
974 hidden_states = self.language_model.model(
975 input_ids=input_ids,
976 positions=positions,
977 intermediate_tensors=intermediate_tensors,
978 inputs_embeds=inputs_embeds,
979 query_start_loc=kwargs.get("query_start_loc"),
980 ngram_context=kwargs.get("ngram_context"),
981 deepstack_input_embeds=deepstack_input_embeds,
982 )
983 if inputs_embeds is not None and get_pp_group().is_first_rank:
984 self._clear_deepstack_input_embeds(inputs_embeds.size(0))
985 return hidden_states
986
987 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
988 mapper = self.hf_to_vllm_mapper | WeightsMapper(
989 orig_to_new_substr={"mtp.": None},
990 orig_to_new_prefix={"visual.": None} if self.language_model_only else {},
991 )
992 loader = AutoWeightsLoader(
993 self,
994 ignore_unexpected_suffixes=_QWEN4_EXP_IGNORED_MISSING_SUFFIXES.copy(),
995 )
996 return loader.load_weights(weights, mapper=mapper)
997
998 @classmethod
999 def get_mamba_state_dtype_from_config(
1000 cls,
1001 vllm_config: VllmConfig,
1002 ) -> tuple[torch.dtype, torch.dtype]:
1003 return Qwen4ExpForCausalLM.get_mamba_state_dtype_from_config(vllm_config)
1004
1005 @classmethod
1006 def get_mamba_state_shape_from_config(
1007 cls,
1008 vllm_config: VllmConfig,
1009 ) -> tuple[tuple[int, int], tuple[int, int]]:
1010 return Qwen4ExpForCausalLM.get_mamba_state_shape_from_config(vllm_config)
1011
1012 @classmethod
1013 def get_mamba_state_copy_func(
1014 cls,
1015 ) -> tuple[MambaStateCopyFunc, MambaStateCopyFunc]:
1016 return Qwen4ExpForCausalLM.get_mamba_state_copy_func()
1017
1018 @classmethod
1019 def get_mamba_state_copy_funcs(
1020 cls,
1021 mamba_types: set[MambaAttentionBackendEnum],
1022 ) -> MambaStateCopyFuncsByType:
1023 return Qwen4ExpForCausalLM.get_mamba_state_copy_funcs(mamba_types)
1024
1025 @classmethod
1026 def get_mamba_specs_from_config(
1027 cls, vllm_config: VllmConfig
1028 ) -> tuple[MambaSpec, ...]:
1029 return Qwen4ExpForCausalLM.get_mamba_specs_from_config(vllm_config)
1030
1031
1032 __all__ = [
1033 "Qwen4ExpDecoderLayer",
1034 "Qwen4ExpForCausalLM",
1035 "Qwen4ExpForConditionalGeneration",
1036 "Qwen4ExpMixtureOfExperts",
1037 "Qwen4ExpModel",
1038 "Qwen4ExpSparseMoeBlock",
1039 ]