main
py 760 lines 26.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 from collections.abc import Iterable
5
6 import torch
7 import torch.nn as nn
8 from transformers import ProcessorMixin
9
10 from vllm.config import VllmConfig
11 from vllm.model_executor.layers.layernorm import RMSNorm
12 from vllm.model_executor.layers.linear import (
13 ColumnParallelLinear,
14 RowParallelLinear,
15 )
16 from vllm.model_executor.layers.logits_processor import LogitsProcessor
17 from vllm.model_executor.layers.rotary_embedding import get_rope
18 from vllm.model_executor.layers.vocab_parallel_embedding import ParallelLMHead
19 from vllm.model_executor.models.module_mapping import MultiModelKeys
20 from vllm.multimodal import MULTIMODAL_REGISTRY
21 from vllm.multimodal.inputs import MultiModalFeatureSpec
22 from vllm.sequence import IntermediateTensors
23 from vllm.transformers_utils.configs.nemotron_h import NemotronHConfig
24 from vllm.utils.torch_utils import async_tensor_h2d
25
26 from .interfaces import (
27 MultiModalEmbeddings,
28 SupportsMRoPE,
29 SupportsMultiModal,
30 SupportsPP,
31 )
32 from .lfm2_siglip2 import Siglip2VisionTransformer
33 from .nemotron_h import (
34 NemotronHAttention,
35 NemotronHMLPDecoderLayer,
36 NemotronHModel,
37 )
38 from .qwen2_5_vl import (
39 Qwen2_5_VLImageEmbeddingInputs,
40 Qwen2_5_VLImageInputs,
41 Qwen2_5_VLImagePixelInputs,
42 Qwen2_5_VLVideoEmbeddingInputs,
43 Qwen2_5_VLVideoInputs,
44 Qwen2_5_VLVideoPixelInputs,
45 )
46 from .qwen3_vl import (
47 Qwen3VLDummyInputsBuilder,
48 Qwen3VLForConditionalGeneration,
49 Qwen3VLMultiModalProcessor,
50 Qwen3VLProcessingInfo,
51 )
52 from .utils import (
53 AutoWeightsLoader,
54 WeightsMapper,
55 maybe_prefix,
56 )
57 from .vision import (
58 is_vit_use_data_parallel,
59 run_dp_sharded_mrope_vision_model,
60 )
61
62
63 class Cosmos3EdgeVisionEncoder(Siglip2VisionTransformer):
64 """Adapts Cosmos (T, H, W) metadata to vLLM packed SigLIP2."""
65
66 @property
67 def dtype(self) -> torch.dtype:
68 return self.embeddings.patch_embedding.weight.dtype
69
70 def encode(
71 self,
72 pixel_values: torch.Tensor,
73 grid_thw: torch.Tensor,
74 ) -> torch.Tensor:
75 # SigLIP2 attends to each frame independently, so expand every THW
76 # entry into T separate HW attention sequences.
77 grid_thw_cpu = grid_thw.to(device="cpu")
78 spatial_shapes = torch.repeat_interleave(
79 grid_thw_cpu[:, 1:],
80 grid_thw_cpu[:, 0],
81 dim=0,
82 )
83 lengths_cpu = spatial_shapes.prod(dim=-1).to(torch.int32)
84 lengths = async_tensor_h2d(lengths_cpu, pixel_values.device)
85
86 cu_seqlens = torch.zeros(
87 lengths.numel() + 1,
88 dtype=torch.int32,
89 device=pixel_values.device,
90 )
91 cu_seqlens[1:] = lengths.cumsum(dim=0)
92
93 max_seqlen = lengths_cpu.max().reshape(1)
94
95 return super().forward(
96 pixel_values_packed=pixel_values,
97 spatial_shapes=spatial_shapes,
98 cu_seqlens=cu_seqlens,
99 max_seqlen=max_seqlen,
100 )[0]
101
102
103 def patch_merging_by_param(
104 image_embeds: torch.Tensor,
105 grid_thw: torch.Tensor,
106 merge_size: int = 2,
107 ) -> torch.Tensor:
108 """Merge each spatial ``merge_size`` block into the channel dimension."""
109 merged_embeds: list[torch.Tensor] = []
110 current_idx = 0
111 hidden_size = image_embeds.shape[-1]
112
113 for grid in grid_thw:
114 t, h, w = grid.tolist()
115 if h % merge_size != 0 or w % merge_size != 0:
116 raise ValueError(
117 "Grid height and width must be divisible by merge_size: "
118 f"got grid={(t, h, w)} and merge_size={merge_size}."
119 )
120
121 num_patches = t * h * w
122 media_embeds = image_embeds[current_idx : current_idx + num_patches]
123 if media_embeds.shape[0] != num_patches:
124 raise ValueError(
125 "image_embeds contains fewer patches than grid_thw describes."
126 )
127 current_idx += num_patches
128
129 media_embeds = media_embeds.view(t, h, w, hidden_size)
130 media_embeds = media_embeds.view(
131 t,
132 h // merge_size,
133 merge_size,
134 w // merge_size,
135 merge_size,
136 hidden_size,
137 )
138 media_embeds = media_embeds.permute(0, 1, 3, 2, 4, 5).contiguous()
139 merged_embeds.append(
140 media_embeds.view(-1, merge_size * merge_size * hidden_size)
141 )
142
143 if current_idx != image_embeds.shape[0]:
144 raise ValueError(
145 "image_embeds contains more patches than grid_thw describes: "
146 f"got {image_embeds.shape[0]}, expected {current_idx}."
147 )
148 if not merged_embeds:
149 return image_embeds.new_empty((0, merge_size * merge_size * hidden_size))
150 return torch.cat(merged_embeds, dim=0)
151
152
153 class Cosmos3EdgePatchMerger(nn.Module):
154 """Projector: LayerNorm -> Linear -> GELU -> Linear.
155
156 Reads config from projector_config (not vision_config).
157 input_hidden_size * spatial_merge_size² -> merger_intermediate_size
158 -> out_hidden_size
159 """
160
161 def __init__(
162 self,
163 input_hidden_size: int,
164 out_hidden_size: int,
165 merger_intermediate_size: int,
166 spatial_merge_size: int = 2,
167 use_postshuffle_norm: bool = False,
168 quant_config=None,
169 prefix: str = "",
170 ) -> None:
171 super().__init__()
172 use_data_parallel = is_vit_use_data_parallel()
173 self.spatial_merge_size = spatial_merge_size
174 self.hidden_size = input_hidden_size * (spatial_merge_size**2)
175 self.input_hidden_size = input_hidden_size
176 self.out_hidden_size = out_hidden_size
177 self.use_postshuffle_norm = use_postshuffle_norm
178
179 norm_dim = self.hidden_size if use_postshuffle_norm else input_hidden_size
180 self.norm = nn.LayerNorm(norm_dim, eps=1e-6)
181
182 self.linear_fc1 = ColumnParallelLinear(
183 self.hidden_size,
184 merger_intermediate_size,
185 bias=True,
186 quant_config=quant_config,
187 prefix=f"{prefix}.linear_fc1",
188 disable_tp=use_data_parallel,
189 )
190 self.act_fn = nn.GELU()
191 self.linear_fc2 = RowParallelLinear(
192 merger_intermediate_size,
193 out_hidden_size,
194 bias=True,
195 quant_config=quant_config,
196 prefix=f"{prefix}.linear_fc2",
197 disable_tp=use_data_parallel,
198 )
199
200 def forward(self, x: torch.Tensor) -> torch.Tensor:
201 if self.use_postshuffle_norm:
202 x = self.norm(x.view(-1, self.hidden_size))
203 else:
204 x = self.norm(x).view(-1, self.hidden_size)
205
206 x, _ = self.linear_fc1(x)
207 x = self.act_fn(x)
208 x, _ = self.linear_fc2(x)
209 return x
210
211
212 class Cosmos3EdgeVisionModel(nn.Module):
213 """Complete Cosmos vision tower returning language-model embeddings."""
214
215 def __init__(
216 self,
217 vision_config,
218 projector_config,
219 quant_config=None,
220 prefix: str = "",
221 ) -> None:
222 super().__init__()
223 self.spatial_merge_size = projector_config.spatial_merge_size
224 self.out_hidden_size = projector_config.out_hidden_size
225
226 self.encoder = Cosmos3EdgeVisionEncoder(
227 vision_config,
228 quant_config=quant_config,
229 prefix=maybe_prefix(prefix, "encoder"),
230 )
231 self.projector = Cosmos3EdgePatchMerger(
232 input_hidden_size=projector_config.input_hidden_size,
233 out_hidden_size=projector_config.out_hidden_size,
234 merger_intermediate_size=projector_config.merger_intermediate_size,
235 spatial_merge_size=self.spatial_merge_size,
236 use_postshuffle_norm=getattr(
237 projector_config, "use_postshuffle_norm", False
238 ),
239 quant_config=quant_config,
240 prefix=maybe_prefix(prefix, "projector"),
241 )
242
243 @property
244 def dtype(self) -> torch.dtype:
245 return self.encoder.dtype
246
247 def forward(
248 self,
249 pixel_values: torch.Tensor,
250 grid_thw: torch.Tensor | list[list[int]],
251 ) -> torch.Tensor:
252 grid_thw = torch.as_tensor(grid_thw, dtype=torch.int64, device="cpu")
253 image_embeds = self.encoder.encode(pixel_values.type(self.dtype), grid_thw)
254 image_embeds = patch_merging_by_param(
255 image_embeds,
256 grid_thw,
257 merge_size=self.spatial_merge_size,
258 )
259 image_embeds = image_embeds.view(
260 -1,
261 self.spatial_merge_size**2,
262 self.projector.input_hidden_size,
263 )
264 return self.projector(image_embeds)
265
266
267 class Cosmos3EdgeProcessingInfo(Qwen3VLProcessingInfo):
268 def get_hf_config(self):
269 return self.ctx.get_hf_config()
270
271 def get_hf_processor(self, **kwargs: object) -> ProcessorMixin:
272 return self.ctx.get_hf_processor(**kwargs)
273
274
275 class Cosmos3EdgeMultiModalProcessor(Qwen3VLMultiModalProcessor):
276 @staticmethod
277 def _expands_only_video_token(_hf_processor: ProcessorMixin) -> bool:
278 # Cosmos renders each video as a full
279 # <|vision_start|><|video_pad|><|vision_end|> placeholder, and its
280 # reference processor replaces that entire triplet with timestamped,
281 # per-frame vision sequences.
282 return False
283
284
285 class Cosmos3EdgeDummyInputsBuilder(Qwen3VLDummyInputsBuilder):
286 pass
287
288
289 class Cosmos3EdgeAttention(NemotronHAttention):
290 """Nemotron-H attention with interleaved multimodal RoPE."""
291
292 def __init__(
293 self,
294 config: NemotronHConfig,
295 layer_idx: int,
296 model_config=None,
297 cache_config=None,
298 quant_config=None,
299 prefix: str = "",
300 ) -> None:
301 super().__init__(
302 config,
303 layer_idx,
304 model_config,
305 cache_config,
306 quant_config,
307 prefix,
308 )
309 self.rotary_emb = get_rope(
310 self.head_dim,
311 max_position=config.max_position_embeddings,
312 rope_parameters=config.rope_parameters,
313 )
314
315 def forward_with_positions(
316 self,
317 positions: torch.Tensor,
318 hidden_states: torch.Tensor,
319 ) -> torch.Tensor:
320 qkv, _ = self.qkv_proj(hidden_states)
321 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
322 q, k = self.rotary_emb(positions, q, k)
323 attn_output = self.attn(q, k, v)
324 output, _ = self.o_proj(attn_output)
325 return output
326
327
328 class Cosmos3EdgeAttentionDecoderLayer(nn.Module):
329 """Pre-norm attention layer for the Cosmos3 Edge dense text model."""
330
331 def __init__(
332 self,
333 config: NemotronHConfig,
334 layer_idx: int,
335 model_config=None,
336 cache_config=None,
337 quant_config=None,
338 parallel_config=None,
339 prefix: str = "",
340 ) -> None:
341 super().__init__()
342 get_layer_config = getattr(config, "get_nemotron_h_config_for_layer", None)
343 layer_config = get_layer_config(layer_idx) if get_layer_config else config
344 self.mixer = Cosmos3EdgeAttention(
345 layer_config,
346 layer_idx,
347 model_config,
348 cache_config,
349 quant_config,
350 prefix=f"{prefix}.mixer",
351 )
352 self.norm = RMSNorm(config.hidden_size, eps=config.layer_norm_epsilon)
353
354 def forward(
355 self,
356 positions: torch.Tensor,
357 hidden_states: torch.Tensor,
358 residual: torch.Tensor | None,
359 **kwargs,
360 ) -> tuple[torch.Tensor, torch.Tensor]:
361 if residual is None:
362 residual = hidden_states
363 hidden_states = self.norm(hidden_states)
364 else:
365 hidden_states, residual = self.norm(hidden_states, residual)
366 hidden_states = self.mixer.forward_with_positions(positions, hidden_states)
367 return hidden_states, residual
368
369
370 class Cosmos3EdgeTextModel(NemotronHModel):
371 """Nemotron-H backbone with Cosmos-owned checkpoint name mapping."""
372
373 # The enclosing Cosmos model maps and packs checkpoint weights. Disabling
374 # Nemotron-H's checkpoint-specific hook prevents those names from being
375 # remapped a second time when AutoWeightsLoader descends into this module.
376 load_weights = None # type: ignore[assignment]
377
378
379 class Cosmos3EdgeForCausalLM(nn.Module):
380 """Minimal CausalLM wrapper for the Cosmos3 Edge dense text model."""
381
382 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
383 super().__init__()
384 hf_config = vllm_config.model_config.hf_config
385 text_config = getattr(hf_config, "text_config", hf_config)
386 vllm_config = vllm_config.with_hf_config(text_config)
387
388 self.vllm_config = vllm_config
389 self.model_config = vllm_config.model_config
390 self.quant_config = vllm_config.quant_config
391 self.config = text_config
392 pattern = text_config.hybrid_override_pattern
393 unsupported = set(pattern) - {"*", "-"}
394 if unsupported:
395 raise ValueError(
396 "Cosmos3 Edge only supports attention (`*`) and MLP (`-`) "
397 f"layers, but found: {sorted(unsupported)}"
398 )
399 if len(pattern) != text_config.num_hidden_layers:
400 raise ValueError(
401 "hybrid_override_pattern must contain one entry per layer: "
402 f"expected {text_config.num_hidden_layers}, "
403 f"got {len(pattern)}"
404 )
405
406 self.model = Cosmos3EdgeTextModel(
407 vllm_config=vllm_config,
408 prefix=maybe_prefix(prefix, "model"),
409 decoder_layer_types={
410 "*": Cosmos3EdgeAttentionDecoderLayer,
411 "-": NemotronHMLPDecoderLayer,
412 },
413 )
414 self.lm_head = ParallelLMHead(
415 text_config.vocab_size,
416 text_config.hidden_size,
417 quant_config=self.quant_config,
418 prefix=maybe_prefix(prefix, "lm_head"),
419 )
420 self.logits_processor = LogitsProcessor(text_config.vocab_size)
421 self.make_empty_intermediate_tensors = (
422 self.model.make_empty_intermediate_tensors
423 )
424
425 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
426 return self.model.embed_input_ids(input_ids)
427
428 def forward(
429 self,
430 input_ids: torch.Tensor | None,
431 positions: torch.Tensor,
432 intermediate_tensors: IntermediateTensors | None = None,
433 inputs_embeds: torch.Tensor | None = None,
434 **kwargs,
435 ):
436 return self.model(
437 input_ids=input_ids,
438 positions=positions,
439 intermediate_tensors=intermediate_tensors,
440 inputs_embeds=inputs_embeds,
441 )
442
443 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor | None:
444 return self.logits_processor(self.lm_head, hidden_states)
445
446
447 def _cosmos3_edge_diffusers_prefix_map() -> dict[str, str]:
448 """Map Diffusers blocks to interleaved Nemotron-H layers.
449
450 The checkpoint stores attention and MLP in one ``layers.N`` block, while
451 the Nemotron-H model represents them as separate decoder layers. Therefore,
452 checkpoint block ``N`` maps its attention to layer ``2N`` and its MLP to
453 layer ``2N + 1``.
454 """
455 mappings = {
456 "embed_tokens.": "language_model.model.embed_tokens.",
457 "norm.": "language_model.model.norm_f.",
458 }
459 for physical_idx in range(28):
460 attention_idx = 2 * physical_idx
461 mlp_idx = attention_idx + 1
462 source_prefix = f"layers.{physical_idx}"
463 attention_prefix = f"language_model.model.layers.{attention_idx}"
464 mlp_prefix = f"language_model.model.layers.{mlp_idx}"
465 mappings.update(
466 {
467 f"{source_prefix}.input_layernorm.": f"{attention_prefix}.norm.",
468 f"{source_prefix}.self_attn.qkv_proj.": (
469 f"{attention_prefix}.mixer.qkv_proj."
470 ),
471 f"{source_prefix}.self_attn.o_proj.": (
472 f"{attention_prefix}.mixer.o_proj."
473 ),
474 f"{source_prefix}.post_attention_layernorm.": (f"{mlp_prefix}.norm."),
475 f"{source_prefix}.mlp.": f"{mlp_prefix}.mixer.",
476 }
477 )
478 return mappings
479
480
481 @MULTIMODAL_REGISTRY.register_processor(
482 Cosmos3EdgeMultiModalProcessor,
483 info=Cosmos3EdgeProcessingInfo,
484 dummy_inputs=Cosmos3EdgeDummyInputsBuilder,
485 )
486 class Cosmos3EdgeForConditionalGeneration(
487 nn.Module,
488 SupportsMultiModal,
489 SupportsPP,
490 SupportsMRoPE,
491 ):
492 """Cosmos3 Edge model with a SigLIP2 vision encoder.
493
494 Architecture:
495 - self.visual: SigLIP2 encoder + patch merger + projector
496 - self.language_model: Cosmos3EdgeForCausalLM (pure attention + RoPE)
497 """
498
499 hf_to_vllm_mapper = WeightsMapper(
500 orig_to_new_stacked={
501 ".self_attn.q_proj": (".self_attn.qkv_proj", "q"),
502 ".self_attn.k_proj": (".self_attn.qkv_proj", "k"),
503 ".self_attn.v_proj": (".self_attn.qkv_proj", "v"),
504 },
505 orig_to_new_prefix={
506 **_cosmos3_edge_diffusers_prefix_map(),
507 "proj_in.": None,
508 "proj_out.": None,
509 "time_embedder.": None,
510 "action_proj_in.": None,
511 "action_proj_out.": None,
512 "audio_modality_embed": None,
513 "action_modality_embed": None,
514 "model.visual.": "visual.encoder.",
515 "model.projector.": "visual.projector.",
516 "lm_head.": "language_model.lm_head.",
517 "model.language_model.": "language_model.model.",
518 },
519 orig_to_new_substr={
520 "_moe_gen": None,
521 "k_norm_und_for_gen": None,
522 ".add_q_proj.": None,
523 ".add_k_proj.": None,
524 ".add_v_proj.": None,
525 ".to_add_out.": None,
526 ".norm_added_q.": None,
527 ".norm_added_k.": None,
528 ".self_attn.to_q.": ".self_attn.q_proj.",
529 ".self_attn.to_k.": ".self_attn.k_proj.",
530 ".self_attn.to_v.": ".self_attn.v_proj.",
531 ".self_attn.to_out.": ".self_attn.o_proj.",
532 "language_model.embeddings": "language_model.embed_tokens",
533 },
534 )
535
536 packed_modules_mapping = {
537 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
538 }
539
540 # Cosmos3 Edge unified Diffusers checkpoints store reasoner weights across
541 # transformer/ and vision_encoder/. Match both while excluding VAE weights.
542 allow_patterns_overrides = ["[tv]*er/*.safetensors"]
543
544 supports_encoder_tp_data = True
545
546 @classmethod
547 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
548 if modality.startswith("image"):
549 return "<|vision_start|><|image_pad|><|vision_end|>"
550 if modality.startswith("video"):
551 return "<|vision_start|><|video_pad|><|vision_end|>"
552 raise ValueError(f"Unsupported modality: {modality}")
553
554 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model"):
555 super().__init__()
556
557 config = vllm_config.model_config.hf_config
558 quant_config = vllm_config.quant_config
559 multimodal_config = vllm_config.model_config.multimodal_config
560 assert multimodal_config is not None
561
562 self.config = config
563 self.multimodal_config = multimodal_config
564 self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
565
566 with self._mark_tower_model(vllm_config, {"image", "video"}):
567 self.visual = Cosmos3EdgeVisionModel(
568 vision_config=config.vision_config,
569 projector_config=config.projector_config,
570 quant_config=quant_config,
571 prefix=maybe_prefix(prefix, "visual"),
572 )
573
574 with self._mark_language_model(vllm_config):
575 self.language_model = Cosmos3EdgeForCausalLM(
576 vllm_config=vllm_config.with_hf_config(
577 config.text_config, architectures=["NemotronHForCausalLM"]
578 ),
579 prefix=maybe_prefix(prefix, "language_model"),
580 )
581
582 self.make_empty_intermediate_tensors = (
583 self.language_model.make_empty_intermediate_tensors
584 )
585
586 def _get_image_features(
587 self,
588 pixel_values: torch.Tensor,
589 grid_thw: torch.Tensor,
590 ) -> tuple[torch.Tensor, ...]:
591 """Run the complete vision tower and split by media item."""
592 image_embeds = self.visual(pixel_values, grid_thw=grid_thw)
593 sizes = (grid_thw.prod(-1) // self.visual.spatial_merge_size**2).tolist()
594 return image_embeds.split(sizes)
595
596 def _parse_and_validate_image_input(
597 self, **kwargs: object
598 ) -> Qwen2_5_VLImageInputs | None:
599 pixel_values = kwargs.pop("pixel_values", None)
600 image_embeds = kwargs.pop("image_embeds", None)
601 image_grid_thw = kwargs.pop("image_grid_thw", None)
602
603 if pixel_values is None and image_embeds is None:
604 return None
605
606 if pixel_values is not None:
607 return Qwen2_5_VLImagePixelInputs(
608 type="pixel_values",
609 pixel_values=pixel_values,
610 image_grid_thw=image_grid_thw,
611 )
612
613 if image_embeds is not None:
614 return Qwen2_5_VLImageEmbeddingInputs(
615 type="image_embeds",
616 image_embeds=image_embeds,
617 image_grid_thw=image_grid_thw,
618 )
619
620 raise AssertionError
621
622 def _parse_and_validate_video_input(
623 self, **kwargs: object
624 ) -> Qwen2_5_VLVideoInputs | None:
625 pixel_values_videos = kwargs.pop("pixel_values_videos", None)
626 video_embeds = kwargs.pop("video_embeds", None)
627 video_grid_thw = kwargs.pop("video_grid_thw", None)
628 second_per_grid_ts = kwargs.pop("second_per_grid_ts", None)
629
630 if pixel_values_videos is None and video_embeds is None:
631 return None
632
633 if pixel_values_videos is not None:
634 return Qwen2_5_VLVideoPixelInputs(
635 type="pixel_values_videos",
636 pixel_values_videos=pixel_values_videos,
637 video_grid_thw=video_grid_thw,
638 second_per_grid_ts=second_per_grid_ts,
639 )
640
641 if video_embeds is not None:
642 return Qwen2_5_VLVideoEmbeddingInputs(
643 type="video_embeds",
644 video_embeds=video_embeds,
645 video_grid_thw=video_grid_thw,
646 )
647
648 raise AssertionError
649
650 def _process_image_input(
651 self, image_input: Qwen2_5_VLImageInputs
652 ) -> tuple[torch.Tensor, ...]:
653 grid_thw = image_input["image_grid_thw"]
654 assert grid_thw.ndim == 2
655
656 if image_input["type"] == "image_embeds":
657 image_embeds = image_input["image_embeds"].type(self.visual.dtype)
658 merge_size = self.visual.spatial_merge_size
659 sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
660 return image_embeds.split(sizes)
661
662 pixel_values = image_input["pixel_values"].type(self.visual.dtype)
663 if self.use_data_parallel:
664 return run_dp_sharded_mrope_vision_model(
665 self.visual, pixel_values, grid_thw.tolist(), rope_type="rope_3d"
666 )
667 return self._get_image_features(pixel_values, grid_thw)
668
669 def _process_video_input(
670 self, video_input: Qwen2_5_VLVideoInputs
671 ) -> tuple[torch.Tensor, ...]:
672 grid_thw = video_input["video_grid_thw"]
673 assert grid_thw.ndim == 2
674
675 if video_input["type"] == "video_embeds":
676 video_embeds = video_input["video_embeds"].type(self.visual.dtype)
677 merge_size = self.visual.spatial_merge_size
678 sizes = (grid_thw.prod(-1) // merge_size // merge_size).tolist()
679 return video_embeds.split(sizes)
680
681 pixel_values_videos = video_input["pixel_values_videos"].type(self.visual.dtype)
682 if self.use_data_parallel:
683 return run_dp_sharded_mrope_vision_model(
684 self.visual, pixel_values_videos, grid_thw.tolist(), rope_type="rope_3d"
685 )
686 return self._get_image_features(pixel_values_videos, grid_thw)
687
688 def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
689 modalities: dict = {}
690 image_input = self._parse_and_validate_image_input(**kwargs)
691 if image_input is not None:
692 modalities["image"] = image_input
693 video_input = self._parse_and_validate_video_input(**kwargs)
694 if video_input is not None:
695 modalities["video"] = video_input
696 return modalities
697
698 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
699 mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
700 if not mm_input_by_modality:
701 return None
702
703 multimodal_embeddings: tuple[torch.Tensor, ...] = ()
704
705 for modality in mm_input_by_modality:
706 multimodal_input = mm_input_by_modality[modality]
707 if modality == "image":
708 image_embeddings = self._process_image_input(multimodal_input)
709 multimodal_embeddings += tuple(image_embeddings)
710 if modality == "video":
711 video_embeddings = self._process_video_input(multimodal_input)
712 multimodal_embeddings += tuple(video_embeddings)
713
714 return multimodal_embeddings
715
716 def forward(
717 self,
718 input_ids: torch.Tensor | None,
719 positions: torch.Tensor,
720 intermediate_tensors: IntermediateTensors | None = None,
721 inputs_embeds: torch.Tensor | None = None,
722 **kwargs: object,
723 ) -> torch.Tensor | IntermediateTensors:
724 if intermediate_tensors is not None:
725 inputs_embeds = None
726
727 return self.language_model.model(
728 input_ids=input_ids,
729 positions=positions,
730 intermediate_tensors=intermediate_tensors,
731 inputs_embeds=inputs_embeds,
732 )
733
734 def compute_logits(
735 self,
736 hidden_states: torch.Tensor,
737 ) -> torch.Tensor | None:
738 return self.language_model.compute_logits(hidden_states)
739
740 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
741 loader = AutoWeightsLoader(self)
742 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
743
744 def get_mm_mapping(self) -> MultiModelKeys:
745 return MultiModelKeys.from_string_field(
746 language_model="language_model",
747 connector="visual.projector",
748 tower_model="visual.",
749 )
750
751 def get_mrope_input_positions(
752 self,
753 input_tokens: list[int],
754 mm_features: list[MultiModalFeatureSpec],
755 ) -> tuple[torch.Tensor, int]:
756 return Qwen3VLForConditionalGeneration._get_mrope_input_positions(
757 input_tokens=input_tokens,
758 mm_features=mm_features,
759 config=self.config,
760 )