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