main
py 1,317 lines 51.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Inference-only MiniMax M3 (text backbone) model.
4
5 The MiniMax-M3-preview config selects a single set of branches:
6 * qk_norm_type == "per_head"
7 * hidden_act == "swigluoai"
8 * use_gemma_norm == True -> Gemma-style RMSNorm everywhere
9 * attention_output_gate == False
10 * scoring_func == "sigmoid" with a routing-bias correction term
11 * sparse_attention_config present -> a subset of layers run the extra
12 "index" attention branch.
13 """
14
15 from collections.abc import Iterable
16
17 import torch
18 from torch import nn
19 from transformers import PreTrainedConfig
20
21 from vllm import _custom_ops as ops
22 from vllm.compilation.breakable_cudagraph import eager_break_during_capture
23 from vllm.config import CacheConfig, VllmConfig, get_current_vllm_config
24 from vllm.distributed import get_pp_group, get_tensor_model_parallel_world_size
25 from vllm.forward_context import get_forward_context
26 from vllm.model_executor.layers.activation import SiluAndMulWithClamp
27 from vllm.model_executor.layers.attention import Attention
28 from vllm.model_executor.layers.attention.attention import set_default_quant_scales
29 from vllm.model_executor.layers.attention_layer_base import AttentionLayerBase
30 from vllm.model_executor.layers.fused_allreduce_gemma_rms_norm import (
31 fused_allreduce_gemma_rms_norm,
32 )
33 from vllm.model_executor.layers.fused_moe import (
34 FusedMoEFactory,
35 GateLinear,
36 fused_moe_make_expert_params_mapping,
37 )
38 from vllm.model_executor.layers.linear import (
39 MergedColumnParallelLinear,
40 MinimaxM3QKVParallelLinearWithIndexer,
41 QKVParallelLinear,
42 RowParallelLinear,
43 )
44 from vllm.model_executor.layers.logits_processor import LogitsProcessor
45 from vllm.model_executor.layers.quantization import QuantizationConfig
46 from vllm.model_executor.layers.rotary_embedding import get_rope
47 from vllm.model_executor.layers.vocab_parallel_embedding import (
48 ParallelLMHead,
49 VocabParallelEmbedding,
50 )
51 from vllm.model_executor.model_loader.weight_utils import (
52 default_weight_loader,
53 maybe_remap_kv_scale_name,
54 )
55 from vllm.model_executor.models.interfaces import (
56 EagleModelMixin,
57 MultiModalEmbeddings,
58 SupportsEagle3,
59 SupportsMultiModal,
60 SupportsPP,
61 )
62 from vllm.model_executor.models.utils import (
63 AutoWeightsLoader,
64 PPMissingLayer,
65 WeightsMapper,
66 init_vllm_registered_model,
67 is_pp_missing_parameter,
68 make_empty_intermediate_tensors_factory,
69 make_layers,
70 maybe_prefix,
71 spec_decode_needs_target_embed,
72 )
73 from vllm.model_executor.models.vision import run_dp_sharded_mrope_vision_model
74 from vllm.models.minimax_m3.common.encoder_cudagraph import (
75 MiniMaxM3EncoderCudaGraphMixin,
76 )
77 from vllm.models.minimax_m3.common.indexer import MiniMaxM3Indexer
78 from vllm.models.minimax_m3.common.mm_preprocess import (
79 MiniMaxM3VLDummyInputsBuilder,
80 MiniMaxM3VLMultiModalProcessor,
81 MiniMaxM3VLProcessingInfo,
82 )
83 from vllm.models.minimax_m3.common.sparse_attention import (
84 MiniMaxM3SparseBackend,
85 MiniMaxM3SparseImpl,
86 select_main_backend_and_impl_cls,
87 )
88 from vllm.models.minimax_m3.common.vision_tower import MiniMaxVLVisionModel
89 from vllm.multimodal import MULTIMODAL_REGISTRY
90 from vllm.sequence import IntermediateTensors
91 from vllm.utils.torch_utils import kv_cache_dtype_str_to_dtype
92 from vllm.v1.kv_cache_interface import (
93 FullAttentionSpec,
94 KVCacheSpec,
95 get_kv_quant_mode,
96 )
97
98
99 def _sparse_attention_layer_ids(config: PreTrainedConfig) -> set[int]:
100 """Layer ids whose attention runs the extra sparse "index" branch."""
101 cfg = getattr(config, "sparse_attention_config", None)
102 if not cfg:
103 return set()
104 freq = cfg.get("sparse_attention_freq")
105 if freq is None:
106 return set()
107 return {i for i, f in enumerate(freq) if f != 0}
108
109
110 def _is_moe_layer(config: PreTrainedConfig, layer_id: int) -> bool:
111 """Whether this layer's MLP is a sparse MoE block (vs a dense MLP)."""
112 moe_layer_freq = getattr(config, "moe_layer_freq", None)
113 if moe_layer_freq is None:
114 return True
115 return moe_layer_freq[layer_id] != 0
116
117
118 class MiniMAXGemmaRMSNorm(nn.Module):
119 """Gemma-style RMS normalization backed by FlashInfer kernels.
120
121 When ``residual`` is given, the fused add + norm runs in place and the
122 updated ``(x, residual)`` pair is returned.
123 """
124
125 def __init__(
126 self,
127 hidden_size: int,
128 eps: float = 1e-6,
129 ) -> None:
130 super().__init__()
131 self.weight = nn.Parameter(torch.zeros(hidden_size))
132 self.variance_epsilon = eps
133
134 def forward(
135 self,
136 x: torch.Tensor,
137 residual: torch.Tensor | None = None,
138 ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
139 from flashinfer.norm import gemma_fused_add_rmsnorm, gemma_rmsnorm
140
141 if residual is None:
142 return gemma_rmsnorm(x, self.weight, self.variance_epsilon)
143
144 # gemma_fused_add_rmsnorm mutates x and residual in place.
145 gemma_fused_add_rmsnorm(x, residual, self.weight, self.variance_epsilon)
146 return x, residual
147
148
149 class MiniMaxM3MLP(nn.Module):
150 """Dense SwiGLU-OAI MLP (used by the leading dense layers)."""
151
152 def __init__(
153 self,
154 config: PreTrainedConfig,
155 intermediate_size: int,
156 quant_config: QuantizationConfig | None = None,
157 reduce_results: bool = True,
158 prefix: str = "",
159 ) -> None:
160 super().__init__()
161 self.gate_up_proj = MergedColumnParallelLinear(
162 config.hidden_size,
163 [intermediate_size] * 2,
164 bias=False,
165 quant_config=quant_config,
166 prefix=f"{prefix}.gate_up_proj",
167 )
168 self.down_proj = RowParallelLinear(
169 intermediate_size,
170 config.hidden_size,
171 bias=False,
172 quant_config=quant_config,
173 reduce_results=reduce_results,
174 prefix=f"{prefix}.down_proj",
175 )
176 if config.hidden_act != "swigluoai":
177 raise ValueError(
178 f"Unsupported activation: {config.hidden_act}. "
179 "Only swigluoai is supported."
180 )
181 # gate * sigmoid(alpha * gate) * (up + beta), with both halves clamped.
182 self.act_fn = SiluAndMulWithClamp(
183 swiglu_limit=config.swiglu_limit,
184 alpha=config.swiglu_alpha,
185 beta=config.swiglu_beta,
186 )
187
188 def forward(self, x: torch.Tensor) -> torch.Tensor:
189 gate_up, _ = self.gate_up_proj(x)
190 x = self.act_fn(gate_up)
191 x, _ = self.down_proj(x)
192 return x
193
194
195 class MiniMaxM3MoE(nn.Module):
196 """Sigmoid-routed MoE block with a routing-bias correction and a shared
197 expert."""
198
199 def __init__(
200 self,
201 config: PreTrainedConfig,
202 layer_id: int,
203 quant_config: QuantizationConfig | None = None,
204 reduce_results: bool = True,
205 prefix: str = "",
206 ) -> None:
207 super().__init__()
208 self.tp_size = get_tensor_model_parallel_world_size()
209 if self.tp_size > config.num_local_experts:
210 raise ValueError(
211 f"Tensor parallel size {self.tp_size} is greater than "
212 f"the number of experts {config.num_local_experts}."
213 )
214
215 self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
216 self.n_shared_experts = getattr(config, "n_shared_experts", None)
217
218 # Sigmoid routing uses a per-expert score-correction bias for selection.
219 self.use_routing_bias = getattr(config, "use_routing_bias", False)
220 if self.use_routing_bias:
221 self.e_score_correction_bias = nn.Parameter(
222 torch.empty(config.num_local_experts, dtype=torch.float32)
223 )
224 self.e_score_correction_bias.weight_loader = (
225 MiniMaxM3MoE.ebias_weight_loader
226 )
227 else:
228 self.e_score_correction_bias = None
229
230 # Router weights are stored in fp32; GateLinear upcasts the bf16
231 # activations and computes the gate in fp32 (fp32 router logits).
232 self.gate = GateLinear(
233 config.hidden_size,
234 config.num_local_experts,
235 bias=False,
236 params_dtype=torch.float32,
237 out_dtype=torch.float32,
238 prefix=f"{prefix}.gate",
239 )
240
241 self.shared_experts: MiniMaxM3MLP | None = None
242 if self.n_shared_experts:
243 self.shared_experts = MiniMaxM3MLP(
244 config=config,
245 intermediate_size=config.intermediate_size * self.n_shared_experts,
246 quant_config=quant_config,
247 reduce_results=False,
248 prefix=f"{prefix}.shared_experts",
249 )
250
251 self.experts = FusedMoEFactory(
252 num_experts=config.num_local_experts,
253 top_k=config.num_experts_per_tok,
254 hidden_size=config.hidden_size,
255 intermediate_size=config.intermediate_size,
256 scoring_func=config.scoring_func,
257 e_score_correction_bias=self.e_score_correction_bias,
258 renormalize=True,
259 # w13 (gate_up_proj) is loaded packed via MergedColumnParallelLinear
260 # ([all gates; all ups]), so use the uninterleaved SwiGLU-OAI variant
261 # rather than the interleaved gpt-oss layout.
262 activation="swigluoai_uninterleave",
263 swiglu_limit=config.swiglu_limit,
264 swiglu_alpha=config.swiglu_alpha,
265 swiglu_beta=config.swiglu_beta,
266 routed_scaling_factor=self.routed_scaling_factor,
267 router_logits_dtype=self.gate.out_dtype,
268 shared_experts=self.shared_experts,
269 quant_config=quant_config,
270 reduce_results=reduce_results,
271 prefix=f"{prefix}.experts",
272 )
273
274 @staticmethod
275 def ebias_weight_loader(param: nn.Parameter, loaded_weight: torch.Tensor) -> None:
276 assert param.size() == loaded_weight.size()
277 param.data.copy_(loaded_weight.to(torch.float32))
278
279 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
280 num_tokens, hidden_dim = hidden_states.shape
281 hidden_states = hidden_states.view(-1, hidden_dim)
282
283 # router_logits: (num_tokens, n_experts); GateLinear casts to fp32.
284 router_logits, _ = self.gate(hidden_states)
285 final_hidden_states = self.experts(
286 hidden_states=hidden_states, router_logits=router_logits
287 )
288
289 return final_hidden_states.view(num_tokens, hidden_dim)
290
291
292 class MiniMaxM3Attention(nn.Module):
293 """Dense attention with per-head QK norm and partial RoPE."""
294
295 def __init__(
296 self,
297 config: PreTrainedConfig,
298 layer_id: int,
299 quant_config: QuantizationConfig | None = None,
300 prefix: str = "",
301 cache_config: CacheConfig | None = None,
302 ) -> None:
303 super().__init__()
304 self.hidden_size = config.hidden_size
305 tp_size = get_tensor_model_parallel_world_size()
306
307 self.total_num_heads = config.num_attention_heads
308 assert self.total_num_heads % tp_size == 0
309 self.num_heads = self.total_num_heads // tp_size
310 self.total_num_kv_heads = config.num_key_value_heads
311 if self.total_num_kv_heads >= tp_size:
312 assert self.total_num_kv_heads % tp_size == 0
313 else:
314 assert tp_size % self.total_num_kv_heads == 0
315 self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
316 self.head_dim = config.head_dim
317 self.q_size = self.num_heads * self.head_dim
318 self.kv_size = self.num_kv_heads * self.head_dim
319 self.scaling = self.head_dim**-0.5
320
321 self.qkv_proj = QKVParallelLinear(
322 self.hidden_size,
323 self.head_dim,
324 self.total_num_heads,
325 self.total_num_kv_heads,
326 bias=False,
327 quant_config=quant_config,
328 prefix=f"{prefix}.qkv_proj",
329 )
330 # reduce_results=False: the attention all-reduce is fused with the
331 # following post_attention_layernorm (GemmaRMSNorm) in the decoder layer
332 # via fused_allreduce_gemma_rms_norm.
333 self.o_proj = RowParallelLinear(
334 self.total_num_heads * self.head_dim,
335 self.hidden_size,
336 bias=False,
337 reduce_results=False,
338 quant_config=quant_config,
339 prefix=f"{prefix}.o_proj",
340 )
341
342 # Per-head QK norm (qk_norm_type == "per_head", use_gemma_norm == True).
343 self.q_norm = MiniMAXGemmaRMSNorm(self.head_dim, eps=config.rms_norm_eps)
344 self.k_norm = MiniMAXGemmaRMSNorm(self.head_dim, eps=config.rms_norm_eps)
345
346 # Partial RoPE: rotary_dim == head_dim * partial_rotary_factor.
347 self.rotary_emb = get_rope(
348 self.head_dim,
349 max_position=config.max_position_embeddings,
350 rope_parameters={
351 "rope_theta": config.rope_theta,
352 "partial_rotary_factor": config.partial_rotary_factor,
353 },
354 )
355
356 self.attn = Attention(
357 self.num_heads,
358 self.head_dim,
359 self.scaling,
360 num_kv_heads=self.num_kv_heads,
361 cache_config=cache_config,
362 quant_config=quant_config,
363 prefix=f"{prefix}.attn",
364 )
365
366 def forward(
367 self,
368 positions: torch.Tensor,
369 hidden_states: torch.Tensor,
370 ) -> torch.Tensor:
371 qkv, _ = self.qkv_proj(hidden_states)
372 # Fused per-head Gemma QK-norm + partial NeoX RoPE on q/k, in place.
373
374 ops.fused_minimax_m3_qknorm_rope_kv_insert(
375 qkv,
376 self.q_norm.weight,
377 self.k_norm.weight,
378 self.rotary_emb.cos_sin_cache,
379 positions,
380 self.num_heads,
381 self.num_kv_heads,
382 self.rotary_emb.rotary_dim,
383 self.q_norm.variance_epsilon,
384 kv_cache_dtype="auto",
385 )
386 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
387 attn_output = self.attn(q, k, v)
388 output, _ = self.o_proj(attn_output)
389 return output
390
391
392 class MiniMaxM3SparseAttention(nn.Module, AttentionLayerBase):
393 """Block-sparse attention layer with the lightning-indexer branch.
394
395 This is a merged attention layer: it owns the projections (qkv + index
396 q/k), per-head QK norms and RoPE, *and* the attention-backend wiring that a
397 generic ``Attention`` layer would normally provide — it binds the
398 ``MiniMaxM3SparseBackend`` + main impl, registers the main paged K/V cache,
399 and owns the lightning indexer (``MiniMaxM3Indexer``), which holds the
400 index-key side cache.
401
402 The index branch (index_{q,k}_proj + index_{q,k}_norm) feeds the sparse
403 top-k block selection. M3 always disables the index value/output
404 projections (``sparse_disable_index_value`` set for every sparse layer), so
405 ``index_{v,o}_proj`` are never created.
406 """
407
408 def __init__(
409 self,
410 config: PreTrainedConfig,
411 layer_id: int,
412 quant_config: QuantizationConfig | None = None,
413 prefix: str = "",
414 cache_config: CacheConfig | None = None,
415 topk_indices_buffer: torch.Tensor | None = None,
416 ) -> None:
417 super().__init__()
418 self.hidden_size = config.hidden_size
419 tp_size = get_tensor_model_parallel_world_size()
420
421 self.total_num_heads = config.num_attention_heads
422 assert self.total_num_heads % tp_size == 0
423 self.num_heads = self.total_num_heads // tp_size
424 self.total_num_kv_heads = config.num_key_value_heads
425 if self.total_num_kv_heads >= tp_size:
426 assert self.total_num_kv_heads % tp_size == 0
427 else:
428 assert tp_size % self.total_num_kv_heads == 0
429 self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
430 self.head_dim = config.head_dim
431 self.q_size = self.num_heads * self.head_dim
432 self.kv_size = self.num_kv_heads * self.head_dim
433 self.scaling = self.head_dim**-0.5
434
435 # Sparse "index" branch dims. index_q has the same head count as the KV
436 # heads (sparse_num_index_heads == num_key_value_heads), so it shards
437 # identically -- including replication when tp_size > num_key_value_heads.
438 sparse_cfg = config.sparse_attention_config
439 self.total_idx_heads = sparse_cfg["sparse_num_index_heads"]
440 self.num_idx_heads = self.num_kv_heads
441 self.idx_head_dim = sparse_cfg["sparse_index_dim"]
442 self.index_q_size = self.num_idx_heads * self.idx_head_dim
443
444 # Single fused projection: q, k, v, index_q, index_k in one GEMM.
445 self.qkv_proj = MinimaxM3QKVParallelLinearWithIndexer(
446 self.hidden_size,
447 self.head_dim,
448 self.total_num_heads,
449 self.total_num_kv_heads,
450 self.total_idx_heads,
451 self.idx_head_dim,
452 bias=False,
453 quant_config=quant_config,
454 prefix=f"{prefix}.qkv_proj",
455 )
456 # reduce_results=False: the attention all-reduce is fused with the
457 # following post_attention_layernorm (GemmaRMSNorm) in the decoder layer
458 # via fused_allreduce_gemma_rms_norm.
459 self.o_proj = RowParallelLinear(
460 self.total_num_heads * self.head_dim,
461 self.hidden_size,
462 bias=False,
463 reduce_results=False,
464 quant_config=quant_config,
465 prefix=f"{prefix}.o_proj",
466 )
467
468 # Per-head QK norm (qk_norm_type == "per_head", use_gemma_norm == True).
469 self.q_norm = MiniMAXGemmaRMSNorm(self.head_dim, eps=config.rms_norm_eps)
470 self.k_norm = MiniMAXGemmaRMSNorm(self.head_dim, eps=config.rms_norm_eps)
471
472 # Partial RoPE: rotary_dim == head_dim * partial_rotary_factor.
473 self.rotary_emb = get_rope(
474 self.head_dim,
475 max_position=config.max_position_embeddings,
476 rope_parameters={
477 "rope_theta": config.rope_theta,
478 "partial_rotary_factor": config.partial_rotary_factor,
479 },
480 )
481
482 self.index_q_norm = MiniMAXGemmaRMSNorm(
483 self.idx_head_dim, eps=config.rms_norm_eps
484 )
485 self.index_k_norm = MiniMAXGemmaRMSNorm(
486 self.idx_head_dim, eps=config.rms_norm_eps
487 )
488 self.index_rotary_emb = self.rotary_emb
489
490 # Attention-backend wiring.
491 vllm_config = get_current_vllm_config()
492 self.layer_name = f"{prefix}.attn"
493 self.kv_cache_dtype = (
494 cache_config.cache_dtype if cache_config is not None else "auto"
495 )
496 self.kv_cache_torch_dtype = kv_cache_dtype_str_to_dtype(
497 self.kv_cache_dtype, vllm_config.model_config
498 )
499 # MiniMax-M3 sparse attention owns its KV-cache insert/read path instead
500 # of wrapping the generic Attention module. Keep the same runtime scale
501 # attributes so FP8 KV reads can honor vLLM's per-layer descale contract.
502 set_default_quant_scales(self, register_buffer=True)
503 # The fused insert quantizes k/v into NVFP4 pages with these scales.
504 self.use_nvfp4_kv = get_kv_quant_mode(self.kv_cache_dtype).is_nvfp4
505 # Indexer side-cache dtype, mirroring --kv-cache-dtype for the main
506 # cache (--attention-config '{"indexer_kv_dtype": ...}').
507 self.indexer_kv_dtype = vllm_config.attention_config.resolve_indexer_kv_dtype(
508 "bf16"
509 )
510
511 # Shared top-k buffer: the indexer writes the selected blocks into it and
512 # the attend impl reads them back (so nothing crosses the eager break as a
513 # Python value, which would freeze at capture).
514 self.topk_indices_buffer = topk_indices_buffer
515 # Indexer (top-k selection) and main attention are separate impls, each
516 # picking Triton vs MSA off its cache dtype. impl is AttentionImplBase
517 # (broader than the AttentionImpl that AttentionLayerBase annotates).
518 self.attn_backend, impl_cls = select_main_backend_and_impl_cls(
519 topk_blocks=sparse_cfg["sparse_topk_blocks"],
520 kv_cache_dtype=self.kv_cache_dtype,
521 num_kv_heads=self.num_kv_heads,
522 )
523 self.impl: MiniMaxM3SparseImpl = impl_cls( # type: ignore[assignment]
524 self.num_heads,
525 self.head_dim,
526 self.scaling,
527 self.num_kv_heads,
528 kv_cache_dtype=self.kv_cache_dtype,
529 topk_blocks=sparse_cfg["sparse_topk_blocks"],
530 sparse_block_size=sparse_cfg["sparse_block_size"],
531 msa_decode_backend=(
532 vllm_config.attention_config.minimax_m3_msa_decode_backend
533 ),
534 )
535 # Self-contained nn.Module: owns its side cache, selects its impl in init.
536 self.indexer = MiniMaxM3Indexer(
537 num_kv_heads=self.num_kv_heads,
538 scale=self.scaling,
539 topk_blocks=sparse_cfg["sparse_topk_blocks"],
540 sparse_block_size=sparse_cfg["sparse_block_size"],
541 num_index_heads=self.num_idx_heads,
542 index_head_dim=self.idx_head_dim,
543 prefix=self.layer_name,
544 init_blocks=sparse_cfg.get("sparse_init_block", 0),
545 local_blocks=sparse_cfg.get("sparse_local_block", 0),
546 score_type=sparse_cfg.get("sparse_score_type", "max"),
547 cache_config=cache_config,
548 indexer_kv_dtype=self.indexer_kv_dtype,
549 topk_indices_buffer=topk_indices_buffer,
550 )
551
552 # Register the main K/V cache so the KV-cache manager allocates it.
553 compilation_config = vllm_config.compilation_config
554 if self.layer_name in compilation_config.static_forward_context:
555 raise ValueError(f"Duplicate layer name: {self.layer_name}")
556 compilation_config.static_forward_context[self.layer_name] = self
557 self.kv_cache = torch.tensor([]) # replaced by bind_kv_cache
558
559 def get_attn_backend(self) -> type[MiniMaxM3SparseBackend]:
560 return self.attn_backend
561
562 def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec | None:
563 # Main GQA K/V cache. Block size may change after load, refresh it.
564 return FullAttentionSpec(
565 block_size=vllm_config.cache_config.block_size,
566 num_kv_heads=self.num_kv_heads,
567 head_size=self.head_dim,
568 head_size_v=self.head_dim,
569 dtype=self.kv_cache_torch_dtype,
570 kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
571 )
572
573 def _allocate_query_fp8(self, qkv: torch.Tensor) -> torch.Tensor | None:
574 if not getattr(self.impl, "use_cutlass_decode", False):
575 return None
576 return torch.empty(
577 (qkv.shape[0], self.q_size),
578 dtype=torch.float8_e4m3fn,
579 device=qkv.device,
580 )
581
582 def forward(
583 self,
584 positions: torch.Tensor,
585 hidden_states: torch.Tensor,
586 ) -> torch.Tensor:
587 # Single fused projection emitting [q | k | v | index_q | index_k].
588 qkv, _ = self.qkv_proj(hidden_states)
589
590 # Horizontally-fused per-head Gemma QK-norm + partial NeoX RoPE on the
591 # main (q/k) and index (index_q/index_k) branches, all read straight out
592 # of the single fused ``qkv`` tensor (the "5 results"). Once the paged
593 # caches are bound the kernel also inserts k/v and the index key into
594 # them; the initial memory-profiling run (caches unbound, no slot_mapping)
595 # short-circuits to zeros below. k/v and index_k are rewritten in place
596 # inside qkv (and scatter-inserted into the caches); q and index_q are
597 # de-interleaved
598 # straight into the dedicated contiguous ``q``/``index_q`` buffers below.
599
600 cos_sin_cache = self.rotary_emb.cos_sin_cache
601 rotary_dim = self.rotary_emb.rotary_dim
602 eps = self.q_norm.variance_epsilon
603 num_tokens = qkv.shape[0]
604
605 fwd_slot_mapping = get_forward_context().slot_mapping
606 if (
607 not isinstance(fwd_slot_mapping, dict)
608 or self.layer_name not in fwd_slot_mapping
609 ):
610 # Memory-profiling run: caches not yet bound, slot_mapping is empty.
611 return qkv.new_zeros((num_tokens, self.hidden_size))
612
613 main_slot_mapping = fwd_slot_mapping[self.layer_name]
614 index_slot_mapping = fwd_slot_mapping[self.indexer.index_cache.prefix]
615 q = qkv.new_empty((num_tokens, self.q_size))
616 # With query_fp8, q is emitted only in fp8; the attend dequantizes it
617 # into ``q`` for the tokens that run on bf16-query kernels.
618 query_fp8 = self._allocate_query_fp8(qkv)
619 # index_q matches the index-K cache dtype (e4m3 for the fp8 score path);
620 # the fused kernel emits fp8 directly when this buffer is e4m3.
621 index_q = qkv.new_empty(
622 (num_tokens, self.index_q_size),
623 dtype=self.indexer.index_cache.dtype,
624 )
625 ops.fused_minimax_m3_qknorm_rope_kv_insert(
626 qkv,
627 self.q_norm.weight,
628 self.k_norm.weight,
629 cos_sin_cache,
630 positions,
631 self.num_heads,
632 self.num_kv_heads,
633 rotary_dim,
634 eps,
635 self.index_q_norm.weight,
636 self.index_k_norm.weight,
637 self.num_idx_heads,
638 main_slot_mapping,
639 index_slot_mapping,
640 self.kv_cache,
641 self.indexer.index_cache.kv_cache,
642 self.kv_cache.size(2), # paged-cache block size
643 q if query_fp8 is None else None,
644 index_q,
645 self.kv_cache_dtype,
646 q_fp8_out=query_fp8,
647 q_fp8_scale=self._q_scale_float,
648 kv_k_scale=self._k_scale if self.use_nvfp4_kv else None,
649 kv_v_scale=self._v_scale if self.use_nvfp4_kv else None,
650 )
651
652 output = torch.empty_like(q)
653 attn_output = self._run_attention(q, query_fp8, index_q, output)
654 output, _ = self.o_proj(attn_output)
655 return output
656
657 @eager_break_during_capture
658 def _run_attention(
659 self,
660 query: torch.Tensor,
661 query_fp8: torch.Tensor | None,
662 index_query: torch.Tensor,
663 output: torch.Tensor,
664 ) -> torch.Tensor:
665 # Single eager break around both: their split-K kernels read per-request
666 # metadata and can't be captured into a cudagraph. The indexer writes its
667 # top-k into the shared ``topk_indices_buffer``; the attend reads it back.
668 self.indexer(index_query)
669 return self.impl.forward(
670 self,
671 query,
672 self.kv_cache,
673 output,
674 query_fp8=query_fp8,
675 )
676
677
678 class MiniMaxM3DecoderLayer(nn.Module):
679 def __init__(
680 self,
681 *,
682 vllm_config: VllmConfig,
683 prefix: str,
684 force_sparse_attn: bool = False,
685 force_moe: bool = False,
686 is_mtp_block: bool = False,
687 topk_indices_buffer: torch.Tensor | None = None,
688 ) -> None:
689 super().__init__()
690 if is_mtp_block:
691 assert vllm_config.speculative_config is not None
692 config = vllm_config.speculative_config.draft_model_config.hf_config
693 else:
694 config = vllm_config.model_config.hf_text_config
695 cache_config = vllm_config.cache_config
696 quant_config = vllm_config.quant_config
697 self.hidden_size = config.hidden_size
698 # DecoderLayers are created with `make_layers` which passes the prefix
699 # with the layer's index.
700 layer_id = int(prefix.split(sep=".")[-1])
701 self.layer_id = layer_id
702
703 # When set, complete the preceding layer's deferred FFN all-reduce
704 # fused into this layer's input_layernorm.
705 # Configured by MiniMaxM3Model.__init__
706 self.fuse_input_allreduce = False
707
708 is_sparse_attention_layer = (
709 force_sparse_attn or layer_id in _sparse_attention_layer_ids(config)
710 )
711
712 if is_sparse_attention_layer:
713 self.self_attn = MiniMaxM3SparseAttention(
714 config=config,
715 layer_id=layer_id,
716 quant_config=quant_config,
717 prefix=f"{prefix}.self_attn",
718 cache_config=cache_config,
719 topk_indices_buffer=topk_indices_buffer,
720 )
721 else:
722 self.self_attn = MiniMaxM3Attention(
723 config=config,
724 layer_id=layer_id,
725 quant_config=quant_config,
726 prefix=f"{prefix}.self_attn",
727 cache_config=cache_config,
728 )
729
730 # Dense layers store the FFN under `mlp`; MoE layers under
731 # `block_sparse_moe` -- matching the checkpoint's naming.
732 # Leave the FFN output un-reduced so its all-reduce fuses into the
733 # next RMSNorm. MTP blocks add the residual directly and PP sends
734 # hidden states across stages, so both must reduce.
735 reduce_results = (
736 is_mtp_block or vllm_config.parallel_config.pipeline_parallel_size > 1
737 )
738 self.is_moe_layer = force_moe or _is_moe_layer(config, layer_id)
739 if self.is_moe_layer:
740 self.block_sparse_moe = MiniMaxM3MoE(
741 config=config,
742 layer_id=layer_id,
743 quant_config=quant_config,
744 reduce_results=reduce_results,
745 prefix=f"{prefix}.block_sparse_moe",
746 )
747 else:
748 self.mlp = MiniMaxM3MLP(
749 config=config,
750 intermediate_size=config.dense_intermediate_size,
751 quant_config=quant_config,
752 prefix=f"{prefix}.mlp",
753 reduce_results=reduce_results,
754 )
755
756 # config.use_gemma_norm is True for M3 -> Gemma-style RMSNorm.
757 self.input_layernorm = MiniMAXGemmaRMSNorm(
758 config.hidden_size, eps=config.rms_norm_eps
759 )
760 self.post_attention_layernorm = MiniMAXGemmaRMSNorm(
761 config.hidden_size, eps=config.rms_norm_eps
762 )
763
764 def forward(
765 self,
766 positions: torch.Tensor,
767 hidden_states: torch.Tensor,
768 residual: torch.Tensor | None,
769 ) -> tuple[torch.Tensor, torch.Tensor]:
770 if self.fuse_input_allreduce and residual is not None:
771 hidden_states, residual = fused_allreduce_gemma_rms_norm(
772 hidden_states, residual, self.input_layernorm
773 )
774 else:
775 if residual is None:
776 residual = hidden_states
777 hidden_states = self.input_layernorm(hidden_states)
778 else:
779 hidden_states, residual = self.input_layernorm(hidden_states, residual)
780 hidden_states = self.self_attn(
781 positions=positions,
782 hidden_states=hidden_states,
783 )
784
785 hidden_states, residual = fused_allreduce_gemma_rms_norm(
786 hidden_states, residual, self.post_attention_layernorm
787 )
788 ffn = self.block_sparse_moe if self.is_moe_layer else self.mlp
789 hidden_states = ffn(hidden_states)
790 return hidden_states, residual
791
792 @property
793 def ffn_all_reduce_deferred(self) -> bool:
794 """This layer's FFN output is left un-reduced; the caller fuses the
795 all-reduce into the next RMSNorm."""
796 if self.is_moe_layer:
797 return self.block_sparse_moe.experts.moe_config.skip_final_all_reduce
798 return not self.mlp.down_proj.reduce_results
799
800
801 class MiniMaxM3Model(nn.Module, EagleModelMixin):
802 fall_back_to_pt_during_load = False
803 supports_aux_hidden_states_over_pp = True
804
805 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
806 super().__init__()
807
808 config = vllm_config.model_config.hf_text_config
809 quant_config = vllm_config.quant_config
810 self.config = config
811
812 self.vocab_size = config.vocab_size
813
814 if get_pp_group().is_first_rank or spec_decode_needs_target_embed(
815 vllm_config, include_mtp=vllm_config.use_v2_model_runner
816 ):
817 self.embed_tokens = VocabParallelEmbedding(
818 config.vocab_size,
819 config.hidden_size,
820 quant_config=quant_config,
821 prefix=f"{prefix}.embed_tokens",
822 )
823 else:
824 self.embed_tokens = PPMissingLayer()
825
826 # Reserved top-k indices buffer shared by all sparse-attention indexer
827 # layers (mirrors DeepseekV4); kept at a stable address so the indexer's
828 # top-k output survives cudagraph capture/replay. Token-major
829 # [total_q, num_index_heads, topk] so the indexer writes its native
830 # [token, head, topk] top-k; the attend transposes to [H, tokens, topk].
831 sparse_cfg = getattr(config, "sparse_attention_config", None)
832 if sparse_cfg is not None:
833 tp_size = get_tensor_model_parallel_world_size()
834 num_index_heads = max(1, sparse_cfg["sparse_num_index_heads"] // tp_size)
835 # Pad tokens to a multiple of 4 so the buffer head stride stays
836 # int4-aligned for build_k2q_csr's vectorised int4 loads.
837 max_num_batched_tokens = vllm_config.scheduler_config.max_num_batched_tokens
838 padded_num_tokens = (max_num_batched_tokens + 3) // 4 * 4
839 self.topk_indices_buffer = torch.empty(
840 padded_num_tokens,
841 num_index_heads,
842 sparse_cfg["sparse_topk_blocks"],
843 dtype=torch.int32,
844 )
845 else:
846 self.topk_indices_buffer = None
847
848 self.start_layer, self.end_layer, self.layers = make_layers(
849 config.num_hidden_layers,
850 lambda prefix: MiniMaxM3DecoderLayer(
851 vllm_config=vllm_config,
852 prefix=prefix,
853 topk_indices_buffer=self.topk_indices_buffer,
854 ),
855 prefix=f"{prefix}.layers",
856 )
857
858 if get_pp_group().is_last_rank:
859 self.norm = MiniMAXGemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps)
860 else:
861 self.norm = PPMissingLayer()
862 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
863 ["hidden_states", "residual"], config.hidden_size
864 )
865
866 # Configure cross-layer all-reduce/RMSNorm fusion: a layer whose FFN output
867 # is left un-reduced has that all-reduce fused into the next layer's
868 # input_layernorm (or the final norm).
869 prev_defers = False
870 for idx, layer in enumerate(self.layers[self.start_layer : self.end_layer]):
871 layer.fuse_input_allreduce = idx > 0 and prev_defers
872 prev_defers = layer.ffn_all_reduce_deferred
873 self.fuse_final_norm_allreduce = prev_defers
874
875 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
876 return self.embed_tokens(input_ids)
877
878 def forward(
879 self,
880 input_ids: torch.Tensor | None,
881 positions: torch.Tensor,
882 intermediate_tensors: IntermediateTensors | None,
883 inputs_embeds: torch.Tensor | None = None,
884 ) -> torch.Tensor | IntermediateTensors | tuple[torch.Tensor, list[torch.Tensor]]:
885 if get_pp_group().is_first_rank:
886 if inputs_embeds is not None:
887 hidden_states = inputs_embeds
888 else:
889 hidden_states = self.embed_input_ids(input_ids)
890 residual = None
891 else:
892 assert intermediate_tensors is not None
893 hidden_states = intermediate_tensors["hidden_states"]
894 residual = intermediate_tensors["residual"]
895
896 remote_aux = self.collect_remote_aux_hidden_states(intermediate_tensors)
897 aux_hidden_states: list[torch.Tensor] = []
898 if get_pp_group().is_first_rank:
899 self._maybe_add_hidden_state(
900 aux_hidden_states, self.start_layer, hidden_states, residual
901 )
902 # Preserve the entry tap before in-place residual updates.
903 if aux_hidden_states:
904 aux_hidden_states[0] = aux_hidden_states[0].clone()
905 for idx, layer in enumerate(
906 self.layers[self.start_layer : self.end_layer], start=self.start_layer
907 ):
908 hidden_states, residual = layer(positions, hidden_states, residual)
909 self._maybe_add_hidden_state(
910 aux_hidden_states, idx + 1, hidden_states, residual
911 )
912
913 if not get_pp_group().is_last_rank:
914 return IntermediateTensors(
915 {
916 "hidden_states": hidden_states,
917 "residual": residual,
918 **self.pack_local_aux_hidden_states(aux_hidden_states),
919 }
920 )
921
922 if self.fuse_final_norm_allreduce:
923 hidden_states, _ = fused_allreduce_gemma_rms_norm(
924 hidden_states, residual, self.norm
925 )
926 else:
927 hidden_states, _ = self.norm(hidden_states, residual)
928
929 aux_hidden_states = remote_aux + aux_hidden_states
930 if len(aux_hidden_states) > 0:
931 return hidden_states, aux_hidden_states
932 return hidden_states
933
934 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
935 # Checkpoint experts use w1=gate, w2=down, w3=up.
936 return fused_moe_make_expert_params_mapping(
937 self,
938 ckpt_gate_proj_name="w1",
939 ckpt_down_proj_name="w2",
940 ckpt_up_proj_name="w3",
941 num_experts=self.config.num_local_experts,
942 )
943
944 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
945 # q/k/v_proj -> fused qkv_proj; gate_proj/up_proj -> fused gate_up_proj
946 # (dense MLP and shared expert). On sparse layers the indexer
947 # index_q/index_k_proj fold into the same fused qkv_proj
948 # (MinimaxM3QKVParallelLinearWithIndexer); these entries simply never match on
949 # dense layers, whose checkpoints have no index_*_proj weights. Leading
950 # dots keep `q_proj`/`k_proj` from matching `index_q_proj`/`index_k_proj`
951 # (preceded by `_`, not `.`).
952 stacked_params_mapping: list[tuple[str, str, int | str]] = [
953 # (param_name, shard_name, shard_id)
954 (".qkv_proj", ".q_proj", "q"),
955 (".qkv_proj", ".k_proj", "k"),
956 (".qkv_proj", ".v_proj", "v"),
957 (".qkv_proj", ".index_q_proj", "index_q"),
958 (".qkv_proj", ".index_k_proj", "index_k"),
959 (".gate_up_proj", ".gate_proj", 0),
960 (".gate_up_proj", ".up_proj", 1),
961 ]
962
963 # (param_name, weight_name, expert_id, shard_id)
964 expert_params_mapping = self.get_expert_mapping()
965
966 params_dict = dict(self.named_parameters())
967 loaded_params: set[str] = set()
968 for name, loaded_weight in weights:
969 # The MTP module is not modeled yet.
970 if "mtp." in name:
971 continue
972
973 # The checkpoint stores block scales as ``weight_scale_inv``; the
974 # ModelOpt MXFP8 layers expose them as ``weight_scale``.
975 if "weight_scale_inv" in name:
976 name = name.replace("weight_scale_inv", "weight_scale")
977
978 for param_name, weight_name, shard_id in stacked_params_mapping:
979 if weight_name not in name:
980 continue
981 # Routed experts (w1/w2/w3) are handled below; don't let the
982 # stacked mapping rewrite them.
983 if ("block_sparse_moe.experts." in name) and name not in params_dict:
984 continue
985 name = name.replace(weight_name, param_name)
986 if name.endswith(".bias") and name not in params_dict:
987 continue
988 if is_pp_missing_parameter(name, self):
989 continue
990 if name not in params_dict:
991 continue
992 param = params_dict[name]
993 weight_loader = param.weight_loader
994 weight_loader(param, loaded_weight, shard_id)
995 break
996 else:
997 for (
998 param_name,
999 weight_name,
1000 expert_id,
1001 expert_shard_id,
1002 ) in expert_params_mapping:
1003 if weight_name not in name:
1004 continue
1005 name = name.replace(weight_name, param_name)
1006 if is_pp_missing_parameter(name, self):
1007 continue
1008 if name not in params_dict:
1009 continue
1010 param = params_dict[name]
1011 weight_loader = param.weight_loader
1012 weight_loader(
1013 param,
1014 loaded_weight,
1015 name,
1016 shard_id=expert_shard_id,
1017 expert_id=expert_id,
1018 )
1019 break
1020 else:
1021 if name.endswith(".bias") and name not in params_dict:
1022 continue
1023 remapped = maybe_remap_kv_scale_name(name, params_dict)
1024 if remapped is None:
1025 continue
1026 name = remapped
1027 if is_pp_missing_parameter(name, self):
1028 continue
1029 # Modules not modeled yet (e.g. attention) are skipped until
1030 # they are ported.
1031 if name not in params_dict:
1032 continue
1033 param = params_dict[name]
1034 weight_loader = getattr(
1035 param, "weight_loader", default_weight_loader
1036 )
1037 weight_loader(param, loaded_weight)
1038 loaded_params.add(name)
1039 return loaded_params
1040
1041
1042 class MiniMaxM3SparseForCausalLM(nn.Module, SupportsPP, SupportsEagle3):
1043 """MiniMax M3 (sparse/dense backbone) for causal language modeling."""
1044
1045 packed_modules_mapping = {
1046 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
1047 "gate_up_proj": ["gate_proj", "up_proj"],
1048 }
1049
1050 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1051 super().__init__()
1052 config = vllm_config.model_config.hf_text_config
1053 quant_config = vllm_config.quant_config
1054 self.config = config
1055 self.quant_config = quant_config
1056 self.model = MiniMaxM3Model(
1057 vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
1058 )
1059 if get_pp_group().is_last_rank:
1060 self.lm_head = ParallelLMHead(
1061 config.vocab_size,
1062 config.hidden_size,
1063 quant_config=quant_config,
1064 prefix=maybe_prefix(prefix, "lm_head"),
1065 )
1066 else:
1067 self.lm_head = PPMissingLayer()
1068 self.logits_processor = LogitsProcessor(config.vocab_size)
1069 self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
1070 self.model.make_empty_intermediate_tensors
1071 )
1072
1073 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
1074 return self.model.embed_input_ids(input_ids)
1075
1076 def forward(
1077 self,
1078 input_ids: torch.Tensor | None,
1079 positions: torch.Tensor,
1080 intermediate_tensors: IntermediateTensors | None = None,
1081 inputs_embeds: torch.Tensor | None = None,
1082 **kwargs,
1083 ) -> torch.Tensor | IntermediateTensors:
1084 return self.model(input_ids, positions, intermediate_tensors, inputs_embeds)
1085
1086 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
1087 return self.logits_processor(self.lm_head, hidden_states)
1088
1089 def compute_logits_local(self, hidden_states: torch.Tensor) -> torch.Tensor:
1090 return self.logits_processor(self.lm_head, hidden_states, skip_gather=True)
1091
1092 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
1093 return self.model.get_expert_mapping()
1094
1095 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1096 loader = AutoWeightsLoader(self)
1097 return loader.load_weights(weights)
1098
1099
1100 @MULTIMODAL_REGISTRY.register_processor(
1101 MiniMaxM3VLMultiModalProcessor,
1102 info=MiniMaxM3VLProcessingInfo,
1103 dummy_inputs=MiniMaxM3VLDummyInputsBuilder,
1104 )
1105 class MiniMaxM3SparseForConditionalGeneration(
1106 nn.Module,
1107 SupportsMultiModal,
1108 MiniMaxM3EncoderCudaGraphMixin,
1109 SupportsPP,
1110 SupportsEagle3,
1111 ):
1112 """Top-level (VL) entry point for MiniMax M3.
1113
1114 The vision tower is not modeled yet; this wrapper routes the text
1115 backbone by constructing ``MiniMaxM3SparseForCausalLM`` from the nested
1116 ``text_config`` and delegating generation to it.
1117 """
1118
1119 # The vision tower runs replicated per rank under ``--mm-encoder-tp-mode
1120 # data``; ``run_dp_sharded_mrope_vision_model`` shards the work across
1121 # ranks (see ``_process_image_input`` / ``_process_video_input``).
1122 supports_encoder_tp_data = True
1123
1124 packed_modules_mapping = {
1125 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
1126 "gate_up_proj": ["gate_proj", "up_proj"],
1127 }
1128
1129 hf_to_vllm_mapper = WeightsMapper(
1130 orig_to_new_prefix={
1131 "multi_modal_projector.": "vision_tower.multi_modal_projector.",
1132 "patch_merge_mlp.": "vision_tower.patch_merge_mlp.",
1133 },
1134 orig_to_new_substr={
1135 ".mlp.fc1.": ".fc1.",
1136 ".mlp.fc2.": ".fc2.",
1137 },
1138 )
1139
1140 @classmethod
1141 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
1142 if modality == "image":
1143 return MiniMaxM3VLProcessingInfo.IMAGE_TOKEN
1144 if modality == "video":
1145 return MiniMaxM3VLProcessingInfo.VIDEO_TOKEN
1146 raise ValueError(f"Unsupported modality: {modality!r}")
1147
1148 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1149 super().__init__()
1150 config = vllm_config.model_config.hf_config
1151 self.config = config
1152 self.quant_config = vllm_config.quant_config
1153 self.multimodal_config = vllm_config.model_config.multimodal_config
1154 assert self.multimodal_config is not None
1155 self.use_data_parallel = self.multimodal_config.mm_encoder_tp_mode == "data"
1156
1157 text_hidden_size = getattr(config.text_config, "hidden_size", None)
1158 assert text_hidden_size is not None, "text_config.hidden_size is required"
1159 projector_hidden_size = getattr(config, "projector_hidden_size", None)
1160
1161 with self._mark_tower_model(vllm_config, {"image", "video"}):
1162 vision_config = config.vision_config
1163 self.vision_tower = MiniMaxVLVisionModel(
1164 config=PreTrainedConfig.from_dict(vision_config),
1165 text_hidden_size=text_hidden_size,
1166 projector_hidden_size=projector_hidden_size,
1167 quant_config=self.quant_config,
1168 prefix=maybe_prefix(prefix, "vision_tower"),
1169 )
1170
1171 self.language_model = init_vllm_registered_model(
1172 vllm_config=vllm_config,
1173 hf_config=config.text_config,
1174 prefix=maybe_prefix(prefix, "language_model"),
1175 architectures=["MiniMaxM3SparseForCausalLM"],
1176 )
1177 self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
1178 self.language_model.make_empty_intermediate_tensors
1179 )
1180
1181 # Expose language model / lm_head for EAGLE3 spec decode.
1182 @property
1183 def model(self) -> nn.Module:
1184 return self.language_model.model
1185
1186 @property
1187 def lm_head(self) -> nn.Module:
1188 return self.language_model.lm_head
1189
1190 def _parse_and_validate_image_input(self, **kwargs: object) -> dict | None:
1191 pixel_values = kwargs.pop("pixel_values", None)
1192 image_grid_thw = kwargs.pop("image_grid_thw", None)
1193 if pixel_values is None:
1194 return None
1195 return {"pixel_values": pixel_values, "image_grid_thw": image_grid_thw}
1196
1197 def _parse_and_validate_video_input(self, **kwargs: object) -> dict | None:
1198 pixel_values_videos = kwargs.pop("pixel_values_videos", None)
1199 video_grid_thw = kwargs.pop("video_grid_thw", None)
1200 if pixel_values_videos is None:
1201 return None
1202 return {
1203 "pixel_values_videos": pixel_values_videos,
1204 "video_grid_thw": video_grid_thw,
1205 }
1206
1207 def _process_image_input(self, image_input: dict) -> tuple[torch.Tensor, ...]:
1208 pixel_values: torch.Tensor = image_input["pixel_values"].type(
1209 self.vision_tower.dtype
1210 )
1211 grid_thw: torch.Tensor = image_input["image_grid_thw"]
1212 assert grid_thw.ndim == 2
1213
1214 if self.use_data_parallel:
1215 # Already returns a per-item tuple of embeddings.
1216 return run_dp_sharded_mrope_vision_model(
1217 self.vision_tower,
1218 pixel_values,
1219 grid_thw.tolist(),
1220 rope_type="rope_3d",
1221 )
1222
1223 image_embeds = self.vision_tower(
1224 pixel_values=pixel_values,
1225 grid_thw=grid_thw.tolist(),
1226 )
1227
1228 # Split the concatenated output into one tensor per image item.
1229 merge_size = self.vision_tower.spatial_merge_size
1230 sizes = (grid_thw.prod(-1) // (merge_size * merge_size)).tolist()
1231 return image_embeds.split(sizes)
1232
1233 def _process_video_input(self, video_input: dict) -> tuple[torch.Tensor, ...]:
1234 pixel_values: torch.Tensor = video_input["pixel_values_videos"].type(
1235 self.vision_tower.dtype
1236 )
1237 grid_thw: torch.Tensor = video_input["video_grid_thw"]
1238 assert grid_thw.ndim == 2
1239
1240 if self.use_data_parallel:
1241 # Already returns a per-item tuple of embeddings.
1242 return run_dp_sharded_mrope_vision_model(
1243 self.vision_tower,
1244 pixel_values,
1245 grid_thw.tolist(),
1246 rope_type="rope_3d",
1247 )
1248
1249 video_embeds = self.vision_tower(
1250 pixel_values=pixel_values,
1251 grid_thw=grid_thw.tolist(),
1252 )
1253
1254 # Split the concatenated output into one tensor per video item.
1255 merge_size = self.vision_tower.spatial_merge_size
1256 sizes = (grid_thw.prod(-1) // (merge_size * merge_size)).tolist()
1257 return video_embeds.split(sizes)
1258
1259 def _parse_and_validate_multimodal_inputs(
1260 self, **kwargs: object
1261 ) -> dict[str, dict]:
1262 mm_input_by_modality: dict[str, dict] = {}
1263 for input_key in kwargs:
1264 if input_key == "pixel_values" and "image" not in mm_input_by_modality:
1265 image_input = self._parse_and_validate_image_input(**kwargs)
1266 if image_input is not None:
1267 mm_input_by_modality["image"] = image_input
1268 if (
1269 input_key == "pixel_values_videos"
1270 and "video" not in mm_input_by_modality
1271 ):
1272 video_input = self._parse_and_validate_video_input(**kwargs)
1273 if video_input is not None:
1274 mm_input_by_modality["video"] = video_input
1275 return mm_input_by_modality
1276
1277 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
1278 mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
1279 if not mm_input_by_modality:
1280 return []
1281
1282 multimodal_embeddings: list[torch.Tensor] = []
1283 for modality in mm_input_by_modality:
1284 multimodal_input = mm_input_by_modality[modality]
1285 if modality == "image":
1286 image_embeddings = self._process_image_input(multimodal_input)
1287 multimodal_embeddings.extend(image_embeddings)
1288 if modality == "video":
1289 video_embeddings = self._process_video_input(multimodal_input)
1290 multimodal_embeddings.extend(video_embeddings)
1291
1292 return tuple(multimodal_embeddings)
1293
1294 def forward(
1295 self,
1296 input_ids: torch.Tensor | None,
1297 positions: torch.Tensor,
1298 intermediate_tensors: IntermediateTensors | None = None,
1299 inputs_embeds: torch.Tensor | None = None,
1300 **kwargs,
1301 ) -> torch.Tensor:
1302 return self.language_model(
1303 input_ids, positions, intermediate_tensors, inputs_embeds
1304 )
1305
1306 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
1307 return self.language_model.compute_logits(hidden_states)
1308
1309 def compute_logits_local(self, hidden_states: torch.Tensor) -> torch.Tensor:
1310 return self.language_model.compute_logits_local(hidden_states)
1311
1312 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
1313 return self.language_model.get_expert_mapping()
1314
1315 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1316 loader = AutoWeightsLoader(self)
1317 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)