main
py 2,674 lines 90.3 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import math
4 from collections.abc import Iterable, Mapping, Sequence
5 from dataclasses import dataclass, fields
6 from functools import partial
7 from itertools import islice
8 from typing import Annotated, Any
9
10 import numpy as np
11 import torch
12 import torch.nn as nn
13 import torch.nn.functional as F
14 from PIL.Image import Image
15 from transformers import (
16 BaseImageProcessor,
17 BaseVideoProcessor,
18 BatchFeature,
19 PreTrainedConfig,
20 ProcessorMixin,
21 )
22 from transformers.image_utils import ImageInput
23 from transformers.video_utils import VideoMetadata
24 from typing_extensions import TypedDict
25
26 from vllm.compilation.decorators import support_torch_compile
27 from vllm.config import CacheConfig, VllmConfig
28 from vllm.config.multimodal import MultiModalDummyOptions, VideoDummyOptions
29 from vllm.distributed import (
30 get_pp_group,
31 get_tensor_model_parallel_rank,
32 get_tensor_model_parallel_world_size,
33 split_tensor_along_last_dim,
34 tensor_model_parallel_all_gather,
35 )
36 from vllm.inputs import MultiModalDataDict
37 from vllm.logger import init_logger
38 from vllm.model_executor.layers.activation import MulAndSilu, SiluAndMul, get_act_fn
39 from vllm.model_executor.layers.attention import Attention, MMEncoderAttention
40 from vllm.model_executor.layers.layernorm import RMSNorm
41 from vllm.model_executor.layers.linear import (
42 ColumnParallelLinear,
43 MergedColumnParallelLinear,
44 QKVParallelLinear,
45 RowParallelLinear,
46 )
47 from vllm.model_executor.layers.logits_processor import LogitsProcessor
48 from vllm.model_executor.layers.quantization import QuantizationConfig
49 from vllm.model_executor.layers.rotary_embedding import get_rope
50 from vllm.model_executor.layers.vocab_parallel_embedding import (
51 ParallelLMHead,
52 VocabParallelEmbedding,
53 )
54 from vllm.model_executor.models.module_mapping import MultiModelKeys
55 from vllm.multimodal import MULTIMODAL_REGISTRY
56 from vllm.multimodal.image import normalize_image
57 from vllm.multimodal.inputs import (
58 MultiModalFieldConfig,
59 MultiModalKwargsItems,
60 VideoItem,
61 )
62 from vllm.multimodal.parse import (
63 ImageProcessorItems,
64 ImageSize,
65 MultiModalDataItems,
66 MultiModalDataParser,
67 )
68 from vllm.multimodal.processing import (
69 BaseMultiModalProcessor,
70 BaseProcessingInfo,
71 PromptReplacement,
72 PromptUpdate,
73 PromptUpdateDetails,
74 )
75 from vllm.multimodal.processing.dummy_inputs import BaseDummyInputsBuilder
76 from vllm.sequence import IntermediateTensors
77 from vllm.utils.math_utils import round_down
78 from vllm.utils.tensor_schema import TensorSchema, TensorShape
79
80 from .interfaces import (
81 MultiModalEmbeddings,
82 SupportsLoRA,
83 SupportsMultiModal,
84 SupportsPP,
85 SupportsQuant,
86 )
87 from .utils import (
88 AutoWeightsLoader,
89 WeightsMapper,
90 _merge_multimodal_embeddings,
91 extract_layer_index,
92 make_empty_intermediate_tensors_factory,
93 make_layers,
94 maybe_prefix,
95 )
96
97 logger = init_logger(__name__)
98
99
100 # Special tokens. These should be present in any tokenizer we use
101 # because the preprocessor relies on them.
102 IMAGE_PROMPT = "<|image|>"
103 VIDEO_PROMPT = "<|video|>"
104 _MAX_VIDEO_FPS = 8
105
106
107 class Molmo2ImageInputs(TensorSchema):
108 """Dimensions:
109 - nc: The total number of crops (dynamic)
110 - np: The total number of patches per crop
111 - cps: Number of channels * patch_size * patch_size
112 - npp: Number of pooled patches (dynamic)
113 - pp: pooling_size * pooling_size
114 - ni: Number of images
115 - nt: Number of image tokens (dynamic)
116 """
117
118 pixel_values: Annotated[torch.Tensor, TensorShape("nc", "np", "cps")]
119
120 token_pooling: Annotated[torch.Tensor, TensorShape("npp", "pp")]
121 """
122 An index tensor that maps image features to their corresponding
123 patch tokens before pooling.
124 """
125
126 num_pooled_patches: Annotated[torch.Tensor, TensorShape("ni")]
127
128 image_tokens: Annotated[torch.BoolTensor, TensorShape("nt")]
129
130 num_image_tokens: Annotated[torch.Tensor, TensorShape("ni")]
131
132
133 class Molmo2VideoInputs(TensorSchema):
134 """Dimensions:
135 - nc: The total number of frames (dynamic)
136 - np: The total number of patches per frame
137 - cps: Number of channels * patch_size * patch_size
138 - npp: Number of pooled patches (dynamic)
139 - pp: pooling_size * pooling_size
140 - nv: Number of videos
141 - nt: Number of video tokens (dynamic)
142 """
143
144 pixel_values_videos: Annotated[torch.Tensor, TensorShape("nc", "np", "cps")]
145
146 token_pooling: Annotated[torch.Tensor, TensorShape("npp", "pp")]
147 """
148 An index tensor that maps image features to their corresponding
149 patch tokens before pooling.
150 """
151
152 num_pooled_patches: Annotated[torch.Tensor, TensorShape("nv")]
153
154 video_tokens: Annotated[torch.BoolTensor, TensorShape("nt")]
155
156 num_video_tokens: Annotated[torch.Tensor, TensorShape("nv")]
157
158
159 class Molmo2MultiModalInputs(TypedDict, total=False):
160 images: Molmo2ImageInputs | None
161 videos: Molmo2VideoInputs | None
162
163
164 @dataclass
165 class VitConfig:
166 """Config for a vision transformer."""
167
168 hidden_size: int = 1152
169 intermediate_size: int = 4304
170 num_hidden_layers: int = 27
171 num_attention_heads: int = 16
172 num_key_value_heads: int = 16
173 head_dim: int = 72
174 hidden_act: str = "gelu_pytorch_tanh"
175 layer_norm_eps: float = 1e-6
176 image_default_input_size: tuple[int, int] = (378, 378)
177 image_patch_size: int = 14
178 image_num_pos: int = 577
179
180 def __post_init__(self):
181 self.image_default_input_size = tuple(self.image_default_input_size) # type: ignore[assignment]
182
183 @property
184 def image_num_patch(self):
185 h, w = self.image_default_input_size
186 return h // self.image_patch_size, w // self.image_patch_size
187
188
189 @dataclass
190 class AdapterConfig:
191 """Config for a vit-llm adapter."""
192
193 vit_layers: tuple[int, int] = (-3, -9)
194 pooling_attention_mask: bool = False
195 hidden_size: int = 1152
196 num_attention_heads: int = 16
197 num_key_value_heads: int = 16
198 head_dim: int = 72
199 hidden_act: str = "silu"
200 intermediate_size: int = 18944
201 text_hidden_size: int = 3584
202
203
204 @dataclass
205 class TextConfig:
206 """Configuration for a text model transformer."""
207
208 hidden_size: int = 3584
209 """
210 The hidden size of the model.
211 """
212
213 num_attention_heads: int = 28
214 """
215 The number of self-attention heads.
216 """
217
218 num_key_value_heads: int = 4
219 """
220 The number of heads to use for keys and values.
221 """
222
223 head_dim: int = 128
224 """
225 The head dimensionality for the attention mechanism.
226 """
227
228 vocab_size: int = 152064
229 """Vocabulary size of the model."""
230
231 additional_vocab_size: int = 128
232 """Number of additional tokens to have the input embeddings for"""
233
234 qkv_bias: bool = True
235 """
236 Do QKV projection a bias
237 """
238
239 num_hidden_layers: int = 48
240 """
241 The number of layers/blocks.
242 """
243
244 intermediate_size: int = 18944
245 """
246 The hidden size for the MLP.
247 """
248
249 hidden_act: str = "silu"
250 """
251 The activation function to use within the MLP layers.
252 """
253
254 max_position_embeddings: int = 4096
255 """
256 Max positional embeddings to use in RoPE cache
257 """
258
259 rope_theta: float = 1000000.0
260 """
261 RoPE theta parameter.
262 """
263
264 use_qk_norm: bool = False
265 """
266 Apply layer norm to the keys and queries within the attention mechanism.
267 This can help stabilize training.
268 """
269
270 qk_norm_type: str = "olmo"
271 """
272 The type of layer norm to use for the keys and queries.
273 Can be "olmo" or "qwen3".
274 """
275
276 layer_norm_eps: float = 1e-6
277 """
278 epsilon for layer norms
279 """
280
281 norm_after: bool = False
282 """Apply layer norm before and after the attention and MLP blocks."""
283
284 rope_scaling_layers: tuple[int, ...] | None = None
285 """
286 RoPE scaling layers.
287 """
288
289
290 class ViTMLP(nn.Module):
291 """MLP used in Vision Transformer."""
292
293 def __init__(
294 self,
295 dim: int,
296 hidden_dim: int,
297 hidden_act: str,
298 quant_config: QuantizationConfig | None = None,
299 prefix: str = "",
300 ) -> None:
301 super().__init__()
302 self.w1 = ColumnParallelLinear(
303 dim,
304 hidden_dim,
305 bias=True,
306 quant_config=quant_config,
307 prefix=f"{prefix}.w1",
308 )
309 # Activation function.
310 self.act = get_act_fn(hidden_act)
311 self.w2 = RowParallelLinear(
312 hidden_dim,
313 dim,
314 bias=True,
315 quant_config=quant_config,
316 prefix=f"{prefix}.w2",
317 )
318
319 def forward(self, x: torch.Tensor) -> torch.Tensor:
320 x, _ = self.w1(x)
321 x = self.act(x)
322 x, _ = self.w2(x)
323 return x
324
325
326 class ViTMultiHeadDotProductAttention(nn.Module):
327 """Multi-head attention used in Vision Transformer."""
328
329 def __init__(
330 self,
331 hidden_size: int,
332 num_heads: int,
333 num_key_value_heads: int,
334 head_dim: int,
335 use_bias: bool = True,
336 quant_config: QuantizationConfig | None = None,
337 prefix: str = "",
338 ) -> None:
339 super().__init__()
340
341 self.hidden_size = hidden_size
342 self.total_num_heads = num_heads
343 tp_size = get_tensor_model_parallel_world_size()
344
345 assert self.hidden_size % self.total_num_heads == 0
346 assert self.total_num_heads % tp_size == 0
347
348 self.num_heads = self.total_num_heads // tp_size
349 self.head_dim = head_dim
350
351 assert self.head_dim == self.hidden_size // self.total_num_heads
352
353 self.total_num_kv_heads = num_key_value_heads
354 if self.total_num_kv_heads >= tp_size:
355 assert self.total_num_kv_heads % tp_size == 0
356 else:
357 assert tp_size % self.total_num_kv_heads == 0
358
359 self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
360
361 self.q_size = self.num_heads * self.head_dim
362 self.kv_size = self.num_kv_heads * self.head_dim
363
364 self.merged_qkv = QKVParallelLinear(
365 self.hidden_size,
366 self.head_dim,
367 self.total_num_heads,
368 self.total_num_kv_heads,
369 bias=use_bias,
370 quant_config=quant_config,
371 prefix=f"{prefix}.merged_qkv",
372 )
373 self.wo = RowParallelLinear(
374 self.total_num_heads * self.head_dim,
375 self.hidden_size,
376 bias=use_bias,
377 quant_config=quant_config,
378 prefix=f"{prefix}.wo",
379 )
380 self.scale = self.head_dim**-0.5
381 self.attn = MMEncoderAttention(
382 self.num_heads,
383 self.head_dim,
384 self.scale,
385 num_kv_heads=self.num_kv_heads,
386 prefix=f"{prefix}.attn",
387 )
388
389 def forward(self, inputs: torch.Tensor) -> torch.Tensor:
390 qkv, _ = self.merged_qkv(inputs)
391 xq, xk, xv = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
392
393 output = self.attn(xq, xk, xv)
394
395 output, _ = self.wo(output)
396
397 return output
398
399
400 class Molmo2VisionBlock(nn.Module):
401 """Residual attention block used in Vision Transformer."""
402
403 def __init__(
404 self,
405 config: VitConfig,
406 quant_config: QuantizationConfig | None = None,
407 prefix: str = "",
408 ) -> None:
409 super().__init__()
410 self.attention = ViTMultiHeadDotProductAttention(
411 hidden_size=config.hidden_size,
412 num_heads=config.num_attention_heads,
413 num_key_value_heads=config.num_key_value_heads,
414 head_dim=config.head_dim,
415 quant_config=quant_config,
416 prefix=f"{prefix}.attention",
417 )
418 self.feed_forward = ViTMLP(
419 dim=config.hidden_size,
420 hidden_dim=config.intermediate_size,
421 hidden_act=config.hidden_act,
422 quant_config=quant_config,
423 prefix=f"{prefix}.feed_forward",
424 )
425 self.attention_norm = nn.LayerNorm(
426 config.hidden_size,
427 eps=config.layer_norm_eps,
428 )
429 self.ffn_norm = nn.LayerNorm(
430 config.hidden_size,
431 eps=config.layer_norm_eps,
432 )
433
434 def forward(self, x: torch.Tensor) -> torch.Tensor:
435 x = x + self.attention(self.attention_norm(x))
436 x = x + self.feed_forward(self.ffn_norm(x))
437 return x
438
439
440 class Molmo2VisionBlockCollection(nn.Module):
441 """Collection of residual attention blocks used in Vision Transformer."""
442
443 def __init__(
444 self,
445 config: VitConfig,
446 quant_config: QuantizationConfig | None = None,
447 prefix: str = "",
448 ) -> None:
449 super().__init__()
450 self.resblocks = nn.ModuleList(
451 [
452 Molmo2VisionBlock(
453 config,
454 quant_config,
455 prefix=f"{prefix}.resblocks.{layer_idx}",
456 )
457 for layer_idx in range(config.num_hidden_layers)
458 ]
459 )
460
461 def forward(self, x: torch.Tensor) -> list[torch.Tensor]:
462 hidden_states = []
463 for r in self.resblocks:
464 x = r(x)
465 hidden_states.append(x)
466 return hidden_states
467
468
469 class Molmo2VisionTransformer(nn.Module):
470 """Vision Transformer used in Vision Backbone."""
471
472 def __init__(
473 self,
474 config: VitConfig,
475 quant_config: QuantizationConfig | None = None,
476 prefix: str = "",
477 ) -> None:
478 super().__init__()
479 scale = config.hidden_size**-0.5
480 self.num_prefix_tokens: int = 0 # no class embeddings
481 self.patch_num: tuple[int, int] = config.image_num_patch
482 self.positional_embedding = nn.Parameter(
483 torch.randn(config.image_num_pos, config.hidden_size) * scale,
484 )
485 image_patch_size = config.image_patch_size
486 self.patch_embedding = nn.Linear(
487 image_patch_size * image_patch_size * 3,
488 config.hidden_size,
489 bias=True,
490 )
491 self.transformer = Molmo2VisionBlockCollection(
492 config,
493 quant_config,
494 prefix=maybe_prefix(prefix, "transformer"),
495 )
496
497 def add_pos_emb(self, x: torch.Tensor, patch_num: tuple[int, int]) -> torch.Tensor:
498 pos_emb = self.positional_embedding
499
500 pos_emb = pos_emb.reshape(
501 (
502 int(math.sqrt(pos_emb.shape[0])),
503 int(math.sqrt(pos_emb.shape[0])),
504 pos_emb.shape[1],
505 )
506 )
507
508 (patch_num_0, patch_num_1) = patch_num
509
510 if pos_emb.shape[0] != patch_num_0 or pos_emb.shape[1] != patch_num_1:
511 # from https://github.com/facebookresearch/mae/blob/main/util/pos_embed.py
512 pos_emb = pos_emb.unsqueeze(0).permute(0, 3, 1, 2)
513 pos_emb = F.interpolate(
514 pos_emb,
515 size=(patch_num_0, patch_num_1),
516 mode="bicubic",
517 align_corners=False,
518 antialias=True,
519 )
520 pos_emb = pos_emb.permute(0, 2, 3, 1).squeeze(0)
521
522 pos_emb = pos_emb.reshape(-1, pos_emb.shape[-1])
523 x = x + pos_emb[None, :, :].to(x.dtype)
524 return x
525
526 def forward(
527 self,
528 x: torch.Tensor,
529 patch_num: tuple[int, int] | None = None,
530 ) -> list[torch.Tensor]:
531 """: param x: (batch_size, num_patch, n_pixels)."""
532 if patch_num is None:
533 patch_num = self.patch_num
534
535 x = self.patch_embedding(x)
536
537 x = self.add_pos_emb(x, patch_num)
538
539 hidden_states = self.transformer(x)
540 return hidden_states
541
542
543 class ImagePoolingAttention(nn.Module):
544 """Multi-head attention used for image pooling."""
545
546 def __init__(
547 self,
548 input_dim: int,
549 hidden_size: int,
550 num_heads: int,
551 num_key_value_heads: int,
552 head_dim: int,
553 use_bias: bool = True,
554 use_pytorch_sdpa: bool = False,
555 quant_config: QuantizationConfig | None = None,
556 prefix: str = "",
557 ) -> None:
558 super().__init__()
559
560 self.input_dim = input_dim
561 self.hidden_size = hidden_size
562 self.total_num_heads = num_heads
563 tp_size = get_tensor_model_parallel_world_size()
564
565 assert self.hidden_size % self.total_num_heads == 0
566 assert self.total_num_heads % tp_size == 0
567
568 self.num_heads = self.total_num_heads // tp_size
569 self.head_dim = head_dim
570
571 assert self.head_dim == self.hidden_size // self.total_num_heads
572
573 self.total_num_kv_heads = num_key_value_heads
574 if self.total_num_kv_heads >= tp_size:
575 assert self.total_num_kv_heads % tp_size == 0
576 else:
577 assert tp_size % self.total_num_kv_heads == 0
578
579 self.num_kv_heads = max(1, self.total_num_kv_heads // tp_size)
580
581 self.kv_size = self.num_kv_heads * self.head_dim
582
583 self.q_proj = ColumnParallelLinear(
584 self.input_dim,
585 self.total_num_heads * self.head_dim,
586 bias=use_bias,
587 quant_config=quant_config,
588 prefix=f"{prefix}.q_proj",
589 )
590 self.merged_kv = MergedColumnParallelLinear(
591 self.input_dim,
592 [self.total_num_kv_heads * self.head_dim] * 2,
593 bias=use_bias,
594 quant_config=quant_config,
595 prefix=f"{prefix}.merged_kv",
596 )
597 self.o_proj = RowParallelLinear(
598 self.total_num_heads * self.head_dim,
599 self.hidden_size,
600 bias=use_bias,
601 quant_config=quant_config,
602 prefix=f"{prefix}.o_proj",
603 )
604 self.scale = self.head_dim**-0.5
605 self.use_pytorch_sdpa = use_pytorch_sdpa
606 if use_pytorch_sdpa:
607 self.attn = None
608 else:
609 self.attn = MMEncoderAttention(
610 self.num_heads,
611 self.head_dim,
612 self.scale,
613 num_kv_heads=self.num_kv_heads,
614 prefix=f"{prefix}.attn",
615 )
616
617 def forward_sdpa(
618 self,
619 query: torch.Tensor,
620 key: torch.Tensor,
621 value: torch.Tensor,
622 attn_mask: torch.Tensor | None = None,
623 ) -> torch.Tensor:
624 bsz, q_len, _ = query.size()
625 kv_len = key.size(1)
626
627 query = query.view(bsz, q_len, self.num_heads, self.head_dim)
628 key = key.view(bsz, kv_len, self.num_kv_heads, self.head_dim)
629 value = value.view(bsz, kv_len, self.num_kv_heads, self.head_dim)
630
631 query, key, value = (x.transpose(1, 2) for x in (query, key, value))
632
633 out = F.scaled_dot_product_attention(
634 query,
635 key,
636 value,
637 attn_mask=attn_mask,
638 is_causal=False,
639 enable_gqa=self.num_heads > self.num_kv_heads,
640 ).transpose(1, 2)
641
642 return out.reshape(bsz, q_len, -1)
643
644 def forward(
645 self,
646 inputs_q: torch.Tensor,
647 inputs_kv: torch.Tensor,
648 attn_mask: torch.Tensor | None = None,
649 ) -> torch.Tensor:
650 xq, _ = self.q_proj(inputs_q)
651 kv, _ = self.merged_kv(inputs_kv)
652 xk, xv = kv.split([self.kv_size, self.kv_size], dim=-1)
653
654 if self.use_pytorch_sdpa:
655 output = self.forward_sdpa(xq, xk, xv, attn_mask)
656 else:
657 assert self.attn is not None
658 output = self.attn(xq, xk, xv)
659
660 output, _ = self.o_proj(output)
661
662 return output
663
664
665 class ImageProjectorMLP(nn.Module):
666 """MLP used for the image projector."""
667
668 def __init__(
669 self,
670 input_dim: int,
671 hidden_dim: int,
672 output_dim: int,
673 hidden_act: str,
674 quant_config: QuantizationConfig | None = None,
675 prefix: str = "",
676 ) -> None:
677 super().__init__()
678
679 self.merged_linear = MergedColumnParallelLinear(
680 input_dim,
681 [hidden_dim] * 2,
682 bias=False,
683 quant_config=quant_config,
684 prefix=f"{prefix}.merged_linear",
685 )
686 # Activation function.
687 assert hidden_act == "silu"
688 self.act_fn = SiluAndMul()
689
690 # Feed-forward output projection.
691 self.down_proj = RowParallelLinear(
692 hidden_dim,
693 output_dim,
694 bias=False,
695 quant_config=quant_config,
696 prefix=f"{prefix}.down_proj",
697 )
698
699 def forward(self, x: torch.Tensor) -> torch.Tensor:
700 x, _ = self.merged_linear(x)
701 x = self.act_fn(x)
702 x, _ = self.down_proj(x)
703 return x
704
705
706 class Molmo2VisionBackbone(nn.Module, SupportsQuant):
707 packed_modules_mapping = {
708 "merged_qkv": ["wq", "wk", "wv"], # vision backbone
709 "merged_kv": ["k_proj", "v_proj"], # image_pooling_2d
710 "merged_linear": ["gate_proj", "up_proj"],
711 }
712
713 # Runs after the top-level mapper, so image_pooling_2d/image_projector
714 # source names are already renamed to q/k/v_proj and gate/up_proj.
715 hf_to_vllm_mapper = WeightsMapper(
716 orig_to_new_stacked={
717 "wq": ("merged_qkv", "q"),
718 "wk": ("merged_qkv", "k"),
719 "wv": ("merged_qkv", "v"),
720 "k_proj": ("merged_kv", 0),
721 "v_proj": ("merged_kv", 1),
722 "gate_proj": ("merged_linear", 0),
723 "up_proj": ("merged_linear", 1),
724 },
725 )
726
727 def __init__(
728 self,
729 vit_config: VitConfig,
730 adapter_config: AdapterConfig,
731 quant_config: QuantizationConfig | None = None,
732 prefix: str = "",
733 ) -> None:
734 super().__init__()
735 self.vit_config = vit_config
736 self.adapter_config = adapter_config
737
738 self.vit_layers = []
739 for layer in adapter_config.vit_layers:
740 if layer >= 0:
741 self.vit_layers.append(layer)
742 else:
743 self.vit_layers.append(layer + vit_config.num_hidden_layers)
744
745 last_layer_needed = max(self.vit_layers) + 1
746 if last_layer_needed < vit_config.num_hidden_layers:
747 vit_config.num_hidden_layers = last_layer_needed
748 self.image_vit = Molmo2VisionTransformer(
749 vit_config,
750 quant_config,
751 prefix=f"{prefix}.image_vit",
752 )
753
754 self.num_prefix_tokens: int = self.image_vit.num_prefix_tokens
755
756 pool_dim = vit_config.hidden_size * len(adapter_config.vit_layers)
757 self.image_pooling_2d = ImagePoolingAttention(
758 input_dim=pool_dim,
759 hidden_size=adapter_config.hidden_size,
760 num_heads=adapter_config.num_attention_heads,
761 num_key_value_heads=adapter_config.num_key_value_heads,
762 head_dim=adapter_config.head_dim,
763 use_pytorch_sdpa=adapter_config.pooling_attention_mask,
764 quant_config=quant_config,
765 prefix=f"{prefix}.image_pooling_2d",
766 )
767 self.image_projector = ImageProjectorMLP(
768 input_dim=adapter_config.hidden_size,
769 hidden_dim=adapter_config.intermediate_size,
770 output_dim=adapter_config.text_hidden_size,
771 hidden_act=adapter_config.hidden_act,
772 quant_config=quant_config,
773 prefix=f"{prefix}.image_projector",
774 )
775
776 @property
777 def dtype(self) -> torch.dtype:
778 return self.image_vit.patch_embedding.weight.dtype
779
780 @property
781 def device(self) -> torch.device:
782 return self.image_vit.patch_embedding.weight.device
783
784 def encode_image(self, images: torch.Tensor) -> torch.Tensor:
785 """: param images: (batch_size, num_crops, num_patch, n_pixels)."""
786 B, T, N, D = images.shape
787 images = images.view(B * T, N, D)
788 image_features = self.image_vit(images)
789
790 features = []
791 for layer in self.vit_layers:
792 features.append(image_features[layer])
793 image_features = torch.cat(features, dim=-1)
794
795 if self.num_prefix_tokens > 0:
796 image_features = image_features[:, 1:]
797 image_features = image_features.view(B, T, N, -1)
798 return image_features
799
800 def forward(
801 self,
802 images: torch.Tensor,
803 token_pooling: torch.Tensor,
804 ) -> torch.Tensor:
805 # image_features shape:
806 # (batch_size, num_crops(=num_image), num_patch, nximage_emb_dim)
807 batch_size, num_image = images.shape[:2]
808 images = images.to(device=self.device, dtype=self.dtype)
809 image_features = self.encode_image(images)
810
811 dim = image_features.shape[-1]
812 valid = token_pooling >= 0
813 valid_token = torch.any(valid, -1)
814
815 # Use `token_pooling` to arange the features for image pooling
816 batch_idx = torch.arange(
817 token_pooling.shape[0],
818 dtype=torch.long,
819 device=token_pooling.device,
820 )
821 batch_idx = torch.tile(
822 batch_idx.view(batch_size, 1, 1),
823 [1, token_pooling.shape[1], token_pooling.shape[2]],
824 )
825
826 # Now [batch, num_features, num_pooled_patches, dim]
827 to_pool = image_features.reshape(batch_size, -1, dim)[
828 batch_idx, torch.clip(token_pooling, 0)
829 ]
830 to_pool = to_pool * valid.to(self.dtype)[:, :, :, None]
831 to_pool = to_pool.reshape([-1, token_pooling.shape[-1], dim])
832 if self.adapter_config.pooling_attention_mask:
833 attn_mask = valid.reshape([-1, 1, 1, valid.shape[-1]])
834 denom = valid.view(-1, to_pool.shape[-2]).float().sum(-1)
835 denom = torch.where(denom == 0, 1, denom)
836 query = to_pool.sum(-2, keepdim=True) / denom[:, None, None].to(
837 to_pool.dtype
838 )
839 else:
840 attn_mask = None
841 query = to_pool.mean(-2, keepdim=True)
842
843 pooled_features = self.image_pooling_2d(query, to_pool, attn_mask=attn_mask)
844 pooled_features = pooled_features.reshape(
845 [batch_size, -1, pooled_features.shape[-1]]
846 )
847
848 # MLP layer to map the feature.
849 pooled_features = self.image_projector(pooled_features)
850 return pooled_features.view(-1, pooled_features.shape[-1])[
851 valid_token.flatten()
852 ]
853
854 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
855 loader = AutoWeightsLoader(self)
856 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
857
858
859 class Molmo2Attention(nn.Module):
860 """Molmo2's LLM Attention."""
861
862 def __init__(
863 self,
864 config: TextConfig,
865 rope_parameters: dict[str, Any],
866 cache_config: CacheConfig | None = None,
867 quant_config: QuantizationConfig | None = None,
868 prefix: str = "",
869 ) -> None:
870 super().__init__()
871 self.hidden_size = config.hidden_size
872 self.tp_size = get_tensor_model_parallel_world_size()
873 self.total_num_heads = config.num_attention_heads
874
875 assert self.hidden_size % self.total_num_heads == 0
876 assert self.total_num_heads % self.tp_size == 0
877
878 self.num_heads = self.total_num_heads // self.tp_size
879 self.total_num_kv_heads = config.num_key_value_heads
880 if self.total_num_kv_heads >= self.tp_size:
881 assert self.total_num_kv_heads % self.tp_size == 0
882 else:
883 assert self.tp_size % self.total_num_kv_heads == 0
884 self.num_kv_heads = max(1, self.total_num_kv_heads // self.tp_size)
885 self.head_dim = config.head_dim
886
887 self.q_size = self.num_heads * self.head_dim
888 self.kv_size = self.num_kv_heads * self.head_dim
889 self.max_position_embeddings = config.max_position_embeddings
890 self.rope_theta = config.rope_theta
891
892 # Attention input projection. Projects x -> (q, k, v)
893 self.qkv_proj = QKVParallelLinear(
894 self.hidden_size,
895 self.head_dim,
896 self.total_num_heads,
897 self.total_num_kv_heads,
898 bias=config.qkv_bias,
899 quant_config=quant_config,
900 )
901
902 self.tp_rank: int | None = None
903 self.k_norm: nn.Module | None = None
904 self.q_norm: nn.Module | None = None
905 self.qk_norm_type: str | None = None
906 if config.use_qk_norm:
907 k_norm_size = (
908 self.head_dim
909 if config.qk_norm_type == "qwen3"
910 else self.total_num_kv_heads * self.head_dim
911 )
912 self.tp_rank = get_tensor_model_parallel_rank()
913 self.k_norm = RMSNorm(k_norm_size, eps=config.layer_norm_eps)
914 q_norm_size = (
915 self.head_dim
916 if config.qk_norm_type == "qwen3"
917 else self.total_num_heads * self.head_dim
918 )
919 self.q_norm = RMSNorm(q_norm_size, eps=config.layer_norm_eps)
920 self.qk_norm_type = config.qk_norm_type
921 # Rotary embeddings. Rope scaling is only applied on full attention layers.
922 layer_idx = extract_layer_index(prefix)
923 if (
924 config.rope_scaling_layers is not None
925 and layer_idx not in config.rope_scaling_layers
926 ):
927 rope_theta = rope_parameters["rope_theta"]
928 rope_parameters = {"rope_type": "default", "rope_theta": rope_theta}
929 self.rotary_emb = get_rope(
930 self.head_dim,
931 max_position=self.max_position_embeddings,
932 rope_parameters=rope_parameters,
933 )
934 self.scaling = self.head_dim**-0.5
935 self.attn = Attention(
936 self.num_heads,
937 self.head_dim,
938 self.scaling,
939 num_kv_heads=self.num_kv_heads,
940 cache_config=cache_config,
941 quant_config=quant_config,
942 prefix=f"{prefix}.attn",
943 )
944
945 # Attention output projection.
946 self.o_proj = RowParallelLinear(
947 self.total_num_heads * self.head_dim,
948 self.hidden_size,
949 bias=False,
950 quant_config=quant_config,
951 )
952
953 def _apply_qk_norm(
954 self,
955 q: torch.Tensor,
956 k: torch.Tensor,
957 ) -> tuple[torch.Tensor, torch.Tensor]:
958 assert self.q_norm is not None
959 assert self.k_norm is not None
960 if self.tp_size > 1:
961 q = tensor_model_parallel_all_gather(q.contiguous())
962 k = tensor_model_parallel_all_gather(k.contiguous())
963 q = self.q_norm(q)
964 k = self.k_norm(k)
965 if self.tp_size > 1:
966 assert self.tp_rank is not None
967 splitter = partial(split_tensor_along_last_dim, num_partitions=self.tp_size)
968 q = splitter(q)[self.tp_rank]
969 k = splitter(k)[self.tp_rank]
970 return q, k
971
972 def forward(
973 self,
974 positions: torch.Tensor,
975 hidden_states: torch.Tensor,
976 **kwargs: object,
977 ) -> torch.Tensor:
978 qkv, _ = self.qkv_proj(hidden_states)
979 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
980 if (
981 self.q_norm is not None
982 and self.k_norm is not None
983 and self.qk_norm_type == "olmo"
984 ):
985 q, k = self._apply_qk_norm(q, k)
986 elif self.q_norm is not None and self.k_norm is not None:
987 q_by_head = q.view(
988 *q.shape[:-1],
989 q.shape[-1] // self.head_dim,
990 self.head_dim,
991 )
992 q_by_head = self.q_norm(q_by_head)
993 q = q_by_head.view(q.shape)
994 k_by_head = k.view(
995 *k.shape[:-1],
996 k.shape[-1] // self.head_dim,
997 self.head_dim,
998 )
999 k_by_head = self.k_norm(k_by_head)
1000 k = k_by_head.view(k.shape)
1001 q, k = self.rotary_emb(positions, q, k)
1002 attn_output = self.attn(q, k, v)
1003
1004 output, _ = self.o_proj(attn_output)
1005 return output
1006
1007
1008 class LanguageModelMLP(nn.Module):
1009 """Molmo2's LLM mlp."""
1010
1011 def __init__(
1012 self,
1013 input_dim: int,
1014 intermediate_size: int,
1015 hidden_act: str,
1016 quant_config: QuantizationConfig | None = None,
1017 ) -> None:
1018 super().__init__()
1019
1020 self.up_gate_proj = MergedColumnParallelLinear(
1021 input_dim,
1022 [intermediate_size] * 2,
1023 bias=False,
1024 quant_config=quant_config,
1025 )
1026 # Activation function.
1027 assert hidden_act == "silu"
1028 self.act_fn = MulAndSilu()
1029 # Feed-forward output projection.
1030 self.down_proj = RowParallelLinear(
1031 intermediate_size,
1032 input_dim,
1033 bias=False,
1034 quant_config=quant_config,
1035 )
1036
1037 def forward(
1038 self,
1039 x: torch.Tensor,
1040 ) -> torch.Tensor:
1041 up_gate, _ = self.up_gate_proj(x)
1042 x = self.act_fn(up_gate)
1043 x, _ = self.down_proj(x)
1044 return x
1045
1046
1047 class Molmo2DecoderLayer(nn.Module):
1048 def __init__(
1049 self,
1050 config: TextConfig,
1051 rope_parameters: dict[str, Any],
1052 cache_config: CacheConfig | None = None,
1053 quant_config: QuantizationConfig | None = None,
1054 prefix: str = "",
1055 ) -> None:
1056 super().__init__()
1057 # Attention block.
1058 self.self_attn = Molmo2Attention(
1059 config,
1060 rope_parameters,
1061 cache_config,
1062 quant_config,
1063 prefix=f"{prefix}.self_attn",
1064 )
1065
1066 # MLP block.
1067 self.mlp = LanguageModelMLP(
1068 config.hidden_size,
1069 config.intermediate_size,
1070 config.hidden_act,
1071 quant_config,
1072 )
1073
1074 # LayerNorm
1075 self.input_layernorm = RMSNorm(config.hidden_size, eps=config.layer_norm_eps)
1076 self.post_attention_layernorm = RMSNorm(
1077 config.hidden_size,
1078 eps=config.layer_norm_eps,
1079 )
1080
1081 def forward(
1082 self,
1083 positions: torch.Tensor,
1084 hidden_states: torch.Tensor,
1085 residual: torch.Tensor | None,
1086 **kwargs: object,
1087 ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor] | None]:
1088 # Self Attention
1089 if residual is None:
1090 residual = hidden_states
1091 hidden_states = self.input_layernorm(hidden_states)
1092 else:
1093 hidden_states, residual = self.input_layernorm(hidden_states, residual)
1094 hidden_states = self.self_attn(
1095 positions=positions,
1096 hidden_states=hidden_states,
1097 **kwargs,
1098 )
1099
1100 hidden_states, residual = self.post_attention_layernorm(hidden_states, residual)
1101 hidden_states = self.mlp(hidden_states)
1102 return hidden_states, residual
1103
1104
1105 class Molmo2DecoderNormAfterLayer(Molmo2DecoderLayer):
1106 def forward(
1107 self,
1108 positions: torch.Tensor,
1109 hidden_states: torch.Tensor,
1110 residual: torch.Tensor | None,
1111 **kwargs: object,
1112 ) -> tuple[torch.Tensor, tuple[torch.Tensor, torch.Tensor] | None]:
1113 # Self Attention
1114 residual = hidden_states
1115 hidden_states = self.self_attn(
1116 positions=positions,
1117 hidden_states=hidden_states,
1118 **kwargs,
1119 )
1120
1121 hidden_states = self.input_layernorm(hidden_states)
1122 hidden_states = hidden_states + residual
1123 residual = hidden_states
1124
1125 hidden_states = self.mlp(hidden_states)
1126 hidden_states = self.post_attention_layernorm(hidden_states)
1127 hidden_states = hidden_states + residual
1128 residual = None
1129 return hidden_states, residual
1130
1131
1132 @support_torch_compile
1133 class Molmo2TextModel(nn.Module, SupportsQuant):
1134 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
1135 super().__init__()
1136
1137 config = vllm_config.model_config.hf_config
1138 cache_config = vllm_config.cache_config
1139 quant_config = vllm_config.quant_config
1140
1141 self.config = config
1142
1143 if hasattr(config, "text_config"):
1144 hf_text_config = config.text_config
1145 else:
1146 hf_text_config = config.llm_config
1147
1148 kwargs = {}
1149 for field in fields(TextConfig):
1150 kwargs[field.name] = getattr(hf_text_config, field.name)
1151 text_config = TextConfig(**kwargs)
1152
1153 self.embedding_size = text_config.vocab_size
1154 self.embedding_size += text_config.additional_vocab_size or 0
1155 self.embed_tokens = VocabParallelEmbedding(
1156 self.embedding_size,
1157 text_config.hidden_size,
1158 quant_config=quant_config,
1159 )
1160
1161 decoder_layer = (
1162 Molmo2DecoderNormAfterLayer
1163 if text_config.norm_after
1164 else Molmo2DecoderLayer
1165 )
1166 self.start_layer, self.end_layer, self.layers = make_layers(
1167 text_config.num_hidden_layers,
1168 lambda prefix: decoder_layer(
1169 text_config,
1170 hf_text_config.rope_parameters,
1171 cache_config=cache_config,
1172 quant_config=quant_config,
1173 prefix=prefix,
1174 ),
1175 prefix=f"{prefix}.layers",
1176 )
1177
1178 self.norm = RMSNorm(text_config.hidden_size, eps=text_config.layer_norm_eps)
1179
1180 self.make_empty_intermediate_tensors = make_empty_intermediate_tensors_factory(
1181 ["hidden_states", "residual"],
1182 text_config.hidden_size,
1183 )
1184
1185 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
1186 return self.embed_tokens(input_ids)
1187
1188 def forward(
1189 self,
1190 input_ids: torch.Tensor | None,
1191 positions: torch.Tensor,
1192 intermediate_tensors: IntermediateTensors | None = None,
1193 inputs_embeds: torch.Tensor | None = None,
1194 **kwargs: object,
1195 ) -> torch.Tensor:
1196 if get_pp_group().is_first_rank:
1197 if inputs_embeds is not None:
1198 hidden_states = inputs_embeds
1199 else:
1200 hidden_states = self.embed_tokens(input_ids)
1201 residual = None
1202 else:
1203 assert intermediate_tensors is not None
1204 hidden_states = intermediate_tensors["hidden_states"]
1205 residual = intermediate_tensors["residual"]
1206
1207 # Apply blocks one-by-one.
1208 for layer in islice(self.layers, self.start_layer, self.end_layer):
1209 hidden_states, residual = layer(
1210 positions,
1211 hidden_states,
1212 residual,
1213 **kwargs,
1214 )
1215 if not get_pp_group().is_last_rank:
1216 return IntermediateTensors(
1217 {"hidden_states": hidden_states, "residual": residual}
1218 )
1219 if residual is not None:
1220 hidden_states, _ = self.norm(hidden_states, residual)
1221 else:
1222 hidden_states = self.norm(hidden_states)
1223 return hidden_states
1224
1225 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1226 return AutoWeightsLoader(self).load_weights(weights)
1227
1228
1229 def get_patches_grid_size(
1230 *,
1231 image_h: int,
1232 image_w: int,
1233 patch_size: int,
1234 pool_h: int,
1235 pool_w: int,
1236 ) -> tuple[int, int]:
1237 patch_h = image_h // patch_size
1238 patch_w = image_w // patch_size
1239 h_pad = round_down(patch_h + pool_h - 1, pool_h) - patch_h
1240 w_pad = round_down(patch_w + pool_w - 1, pool_w) - patch_w
1241 nrows = (patch_h + h_pad) // pool_h
1242 ncols = (patch_w + w_pad) // pool_w
1243
1244 return nrows, ncols
1245
1246
1247 def get_candidate_tilings(max_num: int) -> list[tuple[int, int]]:
1248 tilings = [
1249 (i, j)
1250 for i in range(1, max_num + 1)
1251 for j in range(1, max_num + 1)
1252 if i * j <= max_num
1253 ]
1254 return sorted(tilings, key=lambda x: (x[0] * x[1], x[0]))
1255
1256
1257 def select_tiling(
1258 *,
1259 height: int,
1260 width: int,
1261 patch_size: int,
1262 max_num_patches: int,
1263 ):
1264 tilings = get_candidate_tilings(max_num_patches)
1265 candidate_tilings = np.array(tilings, dtype=np.int32)
1266 candidate_resolutions = candidate_tilings * patch_size
1267
1268 original_size = np.array([height, width], dtype=np.float32)
1269 required_scale_d = candidate_resolutions.astype(np.float32) / original_size
1270 required_scale = required_scale_d.min(axis=-1, keepdims=True)
1271
1272 if (required_scale < 1).all():
1273 ix = required_scale.argmax()
1274 else:
1275 ix = np.where(required_scale < 1.0, 10e9, required_scale).argmin()
1276
1277 return candidate_tilings[ix]
1278
1279
1280 def get_image_size(image: ImageInput) -> ImageSize:
1281 if isinstance(image, Image):
1282 return ImageSize(*image.size)
1283 elif isinstance(image, (np.ndarray, torch.Tensor)):
1284 assert image.ndim == 3
1285 h, w, c = image.shape
1286 assert c in [1, 3]
1287 return ImageSize(w, h)
1288 else:
1289 raise ValueError(f"Unknown image type: {type(image)}")
1290
1291
1292 def exif_transpose(
1293 images: ImageInput | None,
1294 ) -> ImageInput | None:
1295 if images is None:
1296 return None
1297 if images is not None and isinstance(images, (list, tuple)):
1298 images = [
1299 exif_transpose(img) if isinstance(img, Image) else img for img in images
1300 ]
1301 elif images is not None and isinstance(images, Image):
1302 images = normalize_image(images)
1303 return images
1304
1305
1306 def build_flat_image_bool_length(
1307 image_grids: torch.LongTensor,
1308 hf_config: PreTrainedConfig,
1309 image_use_col_tokens: bool = True,
1310 use_single_crop_col_tokens: bool | None = None,
1311 use_single_crop_start_token: bool = True,
1312 ) -> tuple[torch.LongTensor, torch.LongTensor]:
1313 image_patch_id = hf_config.image_patch_id
1314 low_res_image_start_id = hf_config.low_res_image_start_token_id
1315 image_start_id = hf_config.image_start_token_id
1316 image_col_id = hf_config.image_col_id
1317 image_end_id = hf_config.image_end_token_id
1318
1319 device = image_grids.device
1320 B = image_grids.shape[0]
1321
1322 resized_h = image_grids[:, 0]
1323 resized_w = image_grids[:, 1]
1324 h = image_grids[:, 2]
1325 w = image_grids[:, 3]
1326
1327 low_res_use_col_tokens = (
1328 image_use_col_tokens
1329 if use_single_crop_col_tokens is None
1330 else use_single_crop_col_tokens
1331 )
1332 low_res_extra = int(low_res_use_col_tokens)
1333 high_res_extra = int(image_use_col_tokens)
1334
1335 lengths = (
1336 resized_h * (resized_w + low_res_extra) + h * (w + high_res_extra) + 4
1337 ) # [B]
1338 total_len = int(lengths.sum().item())
1339
1340 flat = torch.empty(total_len, dtype=torch.long, device=device)
1341
1342 offset = 0
1343 for i in range(B):
1344 resized_h_i, resized_w_i, h_i, w_i = image_grids[i].tolist()
1345 L_i = int(lengths[i].item())
1346
1347 idx = offset
1348
1349 flat[idx] = (
1350 low_res_image_start_id if use_single_crop_start_token else image_start_id
1351 )
1352 idx += 1
1353
1354 low_res_block_len = resized_w_i + low_res_extra
1355 if low_res_block_len > 0 and resized_h_i > 0:
1356 line = torch.empty(low_res_block_len, dtype=torch.long, device=device)
1357 if resized_w_i > 0:
1358 line[:resized_w_i] = image_patch_id
1359 if low_res_use_col_tokens:
1360 line[resized_w_i] = image_col_id
1361
1362 block = line.repeat(resized_h_i)
1363 flat[idx : idx + resized_h_i * low_res_block_len] = block
1364 idx += resized_h_i * low_res_block_len
1365
1366 flat[idx] = image_end_id
1367 idx += 1
1368
1369 flat[idx] = image_start_id
1370 idx += 1
1371
1372 block_len = w_i + high_res_extra
1373 if block_len > 0 and h_i > 0:
1374 line = torch.empty(block_len, dtype=torch.long, device=device)
1375 if w_i > 0:
1376 line[:w_i] = image_patch_id
1377 if image_use_col_tokens:
1378 line[w_i] = image_col_id
1379
1380 block = line.repeat(h_i)
1381 flat[idx : idx + h_i * block_len] = block
1382 idx += h_i * block_len
1383
1384 flat[idx] = image_end_id
1385 idx += 1
1386
1387 assert idx - offset == L_i
1388
1389 offset += L_i
1390
1391 return flat, lengths
1392
1393
1394 def build_flat_video_bool_length(
1395 video_grids: torch.LongTensor,
1396 hf_config: PreTrainedConfig,
1397 ) -> tuple[torch.LongTensor, torch.LongTensor]:
1398 image_patch_id = hf_config.image_patch_id
1399 frame_start_id = hf_config.frame_start_token_id
1400 frame_end_id = hf_config.frame_end_token_id
1401
1402 device = video_grids.device
1403 B = video_grids.shape[0]
1404
1405 t = video_grids[:, 0]
1406 resized_h = video_grids[:, 1]
1407 resized_w = video_grids[:, 2]
1408
1409 P = resized_h * resized_w
1410 block_len = P + 2
1411 lengths = t * block_len
1412
1413 total_len = int(lengths.sum().item())
1414 flat = torch.empty(total_len, dtype=torch.long, device=device)
1415
1416 offset = 0
1417 for i in range(B):
1418 ti = int(t[i].item())
1419 Pi = int(P[i].item())
1420 Li = int(lengths[i].item())
1421
1422 block = torch.empty(Pi + 2, dtype=torch.long, device=device)
1423 block[0] = frame_start_id
1424 if Pi > 0:
1425 block[1 : 1 + Pi] = image_patch_id
1426 block[-1] = frame_end_id
1427
1428 seq = block.repeat(ti)
1429
1430 flat[offset : offset + Li] = seq
1431 offset += Li
1432
1433 return flat, lengths
1434
1435
1436 def get_candidate_target_fps(
1437 video_fps: int | float,
1438 sampling_fps: int | float,
1439 max_fps: int | float = _MAX_VIDEO_FPS,
1440 ) -> list[float]:
1441 """Return the subset of `video_fps` factors that remain multiples
1442 of `sampling_fps`.
1443
1444 Examples:
1445 >>> get_candidate_target_fps(video_fps=6, sampling_fps=2)
1446 [2, 6]
1447 >>> get_candidate_target_fps(video_fps=5, sampling_fps=1)
1448 [1, 5]
1449 >>> get_candidate_target_fps(video_fps=2, sampling_fps=2)
1450 [2]
1451 >>> get_candidate_target_fps(video_fps=5, sampling_fps=2)
1452 Traceback (most recent call last):
1453 ...
1454 ValueError: sampling_fps=2 must divide video_fps=5 to produce
1455 consistent frame steps.
1456
1457 """
1458 video_fps = int(video_fps)
1459 sampling_fps = int(sampling_fps)
1460 max_fps = int(max_fps)
1461
1462 if sampling_fps is None:
1463 raise ValueError("sampling_fps must be provided")
1464 if video_fps <= 0 or sampling_fps <= 0:
1465 raise ValueError(
1466 "video_fps and sampling_fps must be positive "
1467 f"(got {video_fps}, {sampling_fps})"
1468 )
1469 if video_fps % sampling_fps != 0:
1470 raise ValueError(
1471 f"sampling_fps={sampling_fps} must divide video_fps={video_fps}."
1472 )
1473
1474 candidates = []
1475 for candidate in range(sampling_fps, video_fps + 1, sampling_fps):
1476 if candidate > max_fps:
1477 break
1478 if video_fps % candidate == 0:
1479 candidates.append(float(candidate))
1480
1481 return candidates
1482
1483
1484 def get_target_fps(
1485 video_fps: float,
1486 max_frames: int,
1487 total_frames: int,
1488 frame_sample_mode: str,
1489 candidate_target_fps: list[float],
1490 ) -> float | None:
1491 """Get the target fps that best spans the video and has the most frames sampled."""
1492 num_frames_sampled = 0
1493 selected_target_fps = None
1494 for target_fps in candidate_target_fps:
1495 step_size = max(int(video_fps / target_fps), 1)
1496 num_frames_sampled_at_fps = int(total_frames / step_size)
1497 if num_frames_sampled == 0:
1498 if (
1499 "uniform" in frame_sample_mode
1500 and num_frames_sampled_at_fps > max_frames
1501 ):
1502 break
1503 selected_target_fps = target_fps
1504 num_frames_sampled = num_frames_sampled_at_fps
1505
1506 else:
1507 # the candidate sampling fps increases so frame count can't decrease
1508 assert num_frames_sampled <= num_frames_sampled_at_fps
1509 if num_frames_sampled_at_fps > max_frames:
1510 # choose the sampling fps that spans the video
1511 continue
1512
1513 elif num_frames_sampled_at_fps > num_frames_sampled:
1514 # both are less than max_frames; choose the one with higher
1515 # density of frames sampled
1516 selected_target_fps = target_fps
1517 num_frames_sampled = num_frames_sampled_at_fps
1518 return selected_target_fps
1519
1520
1521 def get_frame_times_and_chosen_fps(
1522 selected_target_fps, total_frames, max_frames, video_fps
1523 ):
1524 if selected_target_fps is None:
1525 frame_indices = np.linspace(
1526 0, total_frames, max_frames, endpoint=False, dtype=int
1527 )
1528 else:
1529 step_size = max(int(video_fps / selected_target_fps), 1)
1530 frame_indices = np.arange(0, total_frames, step_size)
1531 if len(frame_indices) > max_frames:
1532 frame_indices = frame_indices[:max_frames]
1533 return selected_target_fps, frame_indices
1534
1535
1536 class Molmo2ProcessingInfo(BaseProcessingInfo):
1537 def get_data_parser(self):
1538 return MultiModalDataParser(
1539 video_needs_metadata=True,
1540 expected_hidden_size=self._get_expected_hidden_size(),
1541 )
1542
1543 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
1544 return {"image": None, "video": 1}
1545
1546 def select_tiling(
1547 self,
1548 *,
1549 image_width: int,
1550 image_height: int,
1551 image_processor: BaseImageProcessor,
1552 ) -> tuple[int, int]:
1553 max_crops = image_processor.max_crops
1554 left_margin, right_margin = image_processor.overlap_margins
1555 base_image_input_d = image_processor.patch_size
1556
1557 total_margin_pixels = base_image_input_d * (right_margin + left_margin)
1558 crop_patches = image_processor.size["height"] // base_image_input_d
1559 crop_window_patches = crop_patches - (right_margin + left_margin)
1560 crop_window_size = crop_window_patches * base_image_input_d
1561 tiling_h, tiling_w = select_tiling(
1562 height=image_height - total_margin_pixels,
1563 width=image_width - total_margin_pixels,
1564 patch_size=crop_window_size,
1565 max_num_patches=max_crops,
1566 )
1567
1568 return tiling_w, tiling_h
1569
1570 def get_base_grid_size(
1571 self,
1572 image_processor: BaseImageProcessor | BaseVideoProcessor,
1573 ) -> tuple[int, int]:
1574 nrows, ncols = get_patches_grid_size(
1575 image_h=image_processor.size["height"],
1576 image_w=image_processor.size["width"],
1577 patch_size=image_processor.patch_size,
1578 pool_h=image_processor.pooling_size[0],
1579 pool_w=image_processor.pooling_size[1],
1580 )
1581
1582 return ncols, nrows
1583
1584 def get_patches_grid_size(
1585 self,
1586 *,
1587 image_width: int,
1588 image_height: int,
1589 image_processor: BaseImageProcessor,
1590 ) -> tuple[int, int]:
1591 left_margin, right_margin = image_processor.overlap_margins
1592 base_image_input_d = image_processor.patch_size
1593
1594 total_margin_pixels = base_image_input_d * (right_margin + left_margin)
1595 crop_patches = image_processor.size["height"] // base_image_input_d
1596 crop_window_patches = crop_patches - (right_margin + left_margin)
1597 crop_window_size = crop_window_patches * base_image_input_d
1598
1599 tiling_w, tiling_h = self.select_tiling(
1600 image_height=image_height,
1601 image_width=image_width,
1602 image_processor=image_processor,
1603 )
1604
1605 nrows, ncols = get_patches_grid_size(
1606 image_h=tiling_h * crop_window_size + total_margin_pixels,
1607 image_w=tiling_w * crop_window_size + total_margin_pixels,
1608 patch_size=base_image_input_d,
1609 pool_h=image_processor.pooling_size[0],
1610 pool_w=image_processor.pooling_size[1],
1611 )
1612
1613 return ncols, nrows
1614
1615 def get_num_image_tokens(
1616 self,
1617 *,
1618 image_height: int,
1619 image_width: int,
1620 processor: ProcessorMixin,
1621 ) -> int:
1622 image_processor = processor.image_processor
1623
1624 resize_ncols, resize_nrows = self.get_base_grid_size(image_processor)
1625 # start/end tokens + image patch token + col tokens
1626 if processor.use_single_crop_col_tokens is not None:
1627 use_col_tokens = processor.use_single_crop_col_tokens
1628 else:
1629 use_col_tokens = processor.image_use_col_tokens
1630 extra = 2 + resize_nrows * (resize_ncols + int(use_col_tokens))
1631 overlap_ncols, overlap_nrows = self.get_patches_grid_size(
1632 image_height=image_height,
1633 image_width=image_width,
1634 image_processor=image_processor,
1635 )
1636 joint = 2 + overlap_nrows * (
1637 overlap_ncols + int(processor.image_use_col_tokens)
1638 )
1639
1640 return extra + joint
1641
1642 def get_num_video_tokens(
1643 self,
1644 *,
1645 num_frames: int,
1646 processor: ProcessorMixin,
1647 ) -> int:
1648 video_processor = processor.video_processor
1649
1650 resize_ncols, resize_nrows = self.get_base_grid_size(video_processor)
1651 # start/end tokens
1652 extra = 2 + resize_nrows * (resize_ncols + int(processor.video_use_col_tokens))
1653 return num_frames * extra
1654
1655 def get_image_size_with_most_features(self) -> ImageSize:
1656 processor = self.get_hf_processor()
1657 image_processor = processor.image_processor
1658
1659 left_margin, right_margin = image_processor.overlap_margins
1660 base_image_input_d = image_processor.patch_size
1661
1662 total_margin_pixels = base_image_input_d * (right_margin + left_margin)
1663 crop_patches = image_processor.size["height"] // base_image_input_d
1664 crop_window_patches = crop_patches - (right_margin + left_margin)
1665 crop_window_size = crop_window_patches * base_image_input_d
1666
1667 tilings = get_candidate_tilings(image_processor.max_crops)
1668 largest_feature_size, largest_feature_pinpoint = 0, None
1669
1670 for hr, wr in tilings:
1671 height = hr * crop_window_size + total_margin_pixels
1672 width = wr * crop_window_size + total_margin_pixels
1673
1674 feat_size = self.get_num_image_tokens(
1675 image_height=height,
1676 image_width=width,
1677 processor=processor,
1678 )
1679 if feat_size > largest_feature_size:
1680 largest_feature_size = feat_size
1681 largest_feature_pinpoint = ImageSize(width=width, height=height)
1682
1683 if largest_feature_size == 0 or largest_feature_pinpoint is None:
1684 raise ValueError("Cannot have a largest feature size of 0!")
1685
1686 return largest_feature_pinpoint
1687
1688 def _get_max_video_frames(
1689 self,
1690 max_tokens: int,
1691 processor: ProcessorMixin,
1692 ) -> int:
1693 num_tokens_per_frame = self.get_num_video_tokens(
1694 num_frames=1,
1695 processor=processor,
1696 )
1697 max_frames = max_tokens // num_tokens_per_frame
1698 return max(max_frames, 1)
1699
1700 def get_num_frames_with_most_features(
1701 self,
1702 seq_len: int,
1703 mm_counts: Mapping[str, int],
1704 ) -> int:
1705 processor = self.get_hf_processor()
1706 video_processor = processor.video_processor
1707
1708 num_frames = video_processor.num_frames
1709 max_videos = mm_counts.get("video", 0)
1710 max_total_frames = self._get_max_video_frames(seq_len, processor)
1711 max_frames_per_video = min(
1712 max_total_frames // max(max_videos, 1),
1713 num_frames,
1714 )
1715 return max(max_frames_per_video, 1)
1716
1717 def _sample_frames(
1718 self,
1719 total_num_frames: int,
1720 video_fps: float,
1721 duration: float,
1722 frame_sample_mode: str,
1723 num_frames: int,
1724 max_fps: int,
1725 sampling_fps: int,
1726 ) -> np.ndarray:
1727 if frame_sample_mode == "uniform_last_frame" and max_fps is not None:
1728 if total_num_frames <= 2:
1729 indices = np.arange(total_num_frames).astype(int)
1730 elif duration > (num_frames - 1) / max_fps: # -1 to include the last frame
1731 # uniform fallback
1732 indices = np.linspace(
1733 0,
1734 total_num_frames - 1,
1735 num=min(num_frames, total_num_frames),
1736 endpoint=True,
1737 ).astype(int)
1738 else:
1739 float_indices = np.arange(
1740 0.0,
1741 stop=total_num_frames - 1,
1742 step=float(video_fps / max_fps),
1743 )
1744 if np.round(float_indices[-1]) != total_num_frames - 1:
1745 float_indices = np.concatenate(
1746 [float_indices, [total_num_frames - 1]], axis=0
1747 )
1748 indices = np.round(float_indices).astype(int)
1749 assert indices[-1] < total_num_frames
1750 assert len(float_indices) <= num_frames
1751 elif frame_sample_mode == "uniform_last_frame":
1752 indices = np.linspace(
1753 0,
1754 total_num_frames - 1,
1755 num=min(num_frames, total_num_frames),
1756 endpoint=True,
1757 ).astype(int)
1758 elif frame_sample_mode == "fps":
1759 candidate_target_fps = get_candidate_target_fps(video_fps, sampling_fps)
1760 selected_target_fps = get_target_fps(
1761 video_fps,
1762 num_frames,
1763 total_num_frames,
1764 frame_sample_mode,
1765 candidate_target_fps,
1766 )
1767 _, indices = get_frame_times_and_chosen_fps(
1768 selected_target_fps,
1769 total_num_frames,
1770 num_frames,
1771 video_fps,
1772 )
1773 else:
1774 raise NotImplementedError(frame_sample_mode)
1775
1776 return indices
1777
1778 def _get_video_second_idx(
1779 self,
1780 metadata: dict[str, Any],
1781 do_sample_frames: bool | None = None,
1782 ) -> list[float]:
1783 processor = self.get_hf_processor()
1784 video_processor = processor.video_processor
1785
1786 # metadata["fps"] refers to the true fps of the input video.
1787 video_fps = metadata["fps"]
1788 frames_indices = metadata.get("frames_indices")
1789 if do_sample_frames is None:
1790 do_sample_frames = metadata.get("do_sample_frames", False)
1791
1792 if do_sample_frames:
1793 # Frame-based sampling is applied in HF video processor
1794 total_num_frames = metadata["total_num_frames"]
1795 duration = total_num_frames / video_fps
1796 frame_sample_mode = video_processor.frame_sample_mode
1797 num_frames = video_processor.num_frames
1798 max_fps = video_processor.max_fps
1799 sampling_fps = video_processor.sampling_fps
1800 frames_indices = self._sample_frames(
1801 total_num_frames,
1802 video_fps,
1803 duration,
1804 frame_sample_mode,
1805 num_frames,
1806 max_fps,
1807 sampling_fps,
1808 )
1809 else:
1810 # Time-based sampling is done in vllm molmo2 video loader or molmo_utils
1811 assert frames_indices is not None
1812 timestamps = [frame_idx / video_fps for frame_idx in frames_indices]
1813 return timestamps
1814
1815
1816 class Molmo2DummyInputsBuilder(BaseDummyInputsBuilder[Molmo2ProcessingInfo]):
1817 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
1818 num_images = mm_counts.get("image", 0)
1819 num_videos = mm_counts.get("video", 0)
1820
1821 image_placeholder_token = IMAGE_PROMPT
1822 video_placeholder_token = VIDEO_PROMPT
1823
1824 if num_images == 1:
1825 image_string = image_placeholder_token
1826 else:
1827 image_string = "".join(
1828 [f"Image {i + 1}" + image_placeholder_token for i in range(num_images)]
1829 )
1830
1831 return image_string + video_placeholder_token * num_videos
1832
1833 def get_dummy_mm_data(
1834 self,
1835 seq_len: int,
1836 mm_counts: Mapping[str, int],
1837 mm_options: MultiModalDummyOptions,
1838 ) -> MultiModalDataDict:
1839 num_images = mm_counts.get("image", 0)
1840 num_videos = mm_counts.get("video", 0)
1841
1842 dummy_images = []
1843 dummy_videos = []
1844
1845 if num_images > 0:
1846 target_width, target_height = self.info.get_image_size_with_most_features()
1847
1848 dummy_images = self._get_dummy_images(
1849 width=target_width,
1850 height=target_height,
1851 num_images=num_images,
1852 overrides=mm_options.get("image"),
1853 )
1854
1855 if num_videos > 0:
1856 processor = self.info.get_hf_processor()
1857 video_size = processor.video_processor.size
1858 target_num_frames = self.info.get_num_frames_with_most_features(
1859 seq_len, mm_counts
1860 )
1861
1862 video_overrides = mm_options.get("video")
1863
1864 if video_overrides:
1865 num_frames_override = video_overrides.num_frames
1866 if num_frames_override:
1867 if num_frames_override > target_num_frames:
1868 logger.warning(
1869 "video.num_frames override (%d) exceeds model's "
1870 "maximum number of frames (%d), will be ignored",
1871 num_frames_override,
1872 target_num_frames,
1873 )
1874 if num_frames_override < 2:
1875 logger.warning(
1876 "video.num_frames override (%d) cannot be less "
1877 "than 2, will be ignored",
1878 num_frames_override,
1879 )
1880 target_num_frames = min(target_num_frames, num_frames_override)
1881 target_num_frames = max(target_num_frames, 2)
1882
1883 dummy_videos = self._get_dummy_videos(
1884 width=video_size["width"],
1885 height=video_size["height"],
1886 num_frames=target_num_frames,
1887 num_videos=num_videos,
1888 )
1889
1890 return {
1891 "image": dummy_images,
1892 "video": dummy_videos,
1893 }
1894
1895 def _get_dummy_videos(
1896 self,
1897 *,
1898 width: int,
1899 height: int,
1900 num_frames: int,
1901 num_videos: int,
1902 overrides: VideoDummyOptions | None = None,
1903 ) -> list[VideoItem]:
1904 videos = super()._get_dummy_videos(
1905 width=width,
1906 height=height,
1907 num_frames=num_frames,
1908 num_videos=num_videos,
1909 overrides=overrides,
1910 )
1911 videos = [v.copy() for v in videos]
1912
1913 video_items: list[VideoItem] = []
1914 for video in videos:
1915 video_num_frames = video.shape[0]
1916 video_metadata: dict[str, int | float | str | bool | list[int]] = {
1917 "fps": 2.0,
1918 "duration": video_num_frames / 2.0,
1919 "total_num_frames": video_num_frames,
1920 "frames_indices": list(range(video_num_frames)),
1921 "video_backend": "decord",
1922 "do_sample_frames": False,
1923 "height": height,
1924 "width": width,
1925 }
1926 video_items.append((video, video_metadata))
1927
1928 return video_items
1929
1930
1931 class Molmo2MultiModalProcessor(BaseMultiModalProcessor[Molmo2ProcessingInfo]):
1932 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
1933 return self.dummy_inputs.get_dummy_text(mm_counts)
1934
1935 def _postprocess_prompt(self, prompt: list[int]) -> list[int]:
1936 processor = self.info.get_hf_processor()
1937 tokenizer = processor.tokenizer
1938 bos_token_id = tokenizer.bos_token_id or tokenizer.eos_token_id
1939
1940 if len(prompt) == 0 or prompt[0] != bos_token_id:
1941 # Prepend the bos token to the prompt tokens
1942 prompt = [bos_token_id] + prompt
1943
1944 return prompt
1945
1946 def _apply_hf_processor_main(
1947 self,
1948 mm_items: MultiModalDataItems,
1949 hf_kwargs: Mapping[str, object],
1950 ) -> BatchFeature:
1951 hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
1952 mm_items, hf_kwargs
1953 )
1954
1955 if not hf_data:
1956 return self._finalize_hf_mm_data(hf_data, hf_kwargs, passthrough_data)
1957
1958 prompt_text = hf_data.pop("text")
1959 assert isinstance(prompt_text, str)
1960
1961 hf_config = self.info.get_hf_config()
1962 hf_processor = self.info.get_hf_processor(**hf_kwargs)
1963
1964 def patched_call(text=None, images=None, videos=None, **kwargs) -> BatchFeature:
1965 res = hf_processor(text=text, images=images, videos=videos, **kwargs)
1966
1967 # Molmo2Processor.insert_bos results in float outputs
1968 # if the input text is empty
1969 if not text:
1970 res["input_ids"] = res["input_ids"].long()
1971
1972 return res
1973
1974 tokenizer = hf_processor.tokenizer
1975 image_processor = hf_processor.image_processor
1976
1977 if videos := hf_data.pop("videos", []):
1978 assert isinstance(videos, Sequence)
1979 bos_token_id = tokenizer.bos_token_id or tokenizer.eos_token_id
1980
1981 pixel_values_videos_lst = []
1982 video_token_pooling_lst = []
1983 video_num_crops_lst = []
1984 video_num_pooled_patches_lst = []
1985 video_num_patches_lst = []
1986 video_tokens_lst = []
1987 num_video_tokens_lst = []
1988
1989 for item in videos:
1990 assert isinstance(item, tuple) and len(item) == 2
1991 video_array, metadata = item
1992 assert isinstance(metadata, Mapping)
1993
1994 # NOTE: metadata.frames_indices indicates
1995 # the sampled frames indices of pre-sampled videos, which is
1996 # used to calculate the timestamps. Make sure that
1997 # do_sample_frames in hf_kwargs is false for
1998 # presampled videos.
1999
2000 # NOTE: a copy of hf_kwargs is created to update
2001 # do_sample_frames, otherwise mm_hash for the object will be
2002 # incorrect.
2003 video_mm_kwargs = dict(**hf_kwargs)
2004 if "do_sample_frames" not in video_mm_kwargs:
2005 # molmo_utils already has "do_sample_frames" in
2006 # hf_kwargs, don't overwrite it.
2007 video_mm_kwargs["do_sample_frames"] = metadata.get(
2008 "do_sample_frames", False
2009 )
2010
2011 metadata = VideoMetadata(
2012 **{k: metadata[k] for k in metadata if k != "do_sample_frames"}
2013 )
2014
2015 video_mm_data = dict()
2016 video_mm_data["videos"] = [[video_array]]
2017 video_mm_data["video_metadata"] = [[metadata]]
2018
2019 video_outputs = self.info.ctx.call_hf_processor(
2020 patched_call,
2021 dict(text=VIDEO_PROMPT, **video_mm_data),
2022 video_mm_kwargs,
2023 )
2024
2025 input_ids = video_outputs.pop("input_ids")
2026 if input_ids[0, 0] == bos_token_id:
2027 input_ids = input_ids[:, 1:]
2028
2029 video_string = tokenizer.batch_decode(input_ids)[0]
2030 prompt_text = prompt_text.replace(VIDEO_PROMPT, video_string, 1)
2031
2032 video_grids = video_outputs.pop("video_grids")
2033 assert video_grids[:, 0].sum() == len(
2034 video_outputs["pixel_values_videos"]
2035 )
2036
2037 video_outputs["video_num_crops"] = video_grids[:, 0]
2038 video_outputs["video_num_pooled_patches"] = video_grids.prod(dim=1)
2039 n_patches = video_outputs["pixel_values_videos"].shape[1]
2040 video_outputs["video_num_patches"] = (
2041 video_outputs["video_num_crops"] * n_patches
2042 )
2043 (video_outputs["video_tokens"], video_outputs["num_video_tokens"]) = (
2044 build_flat_video_bool_length(video_grids, hf_config)
2045 )
2046
2047 pixel_values_videos_lst.append(video_outputs["pixel_values_videos"])
2048 video_token_pooling_lst.append(video_outputs["video_token_pooling"])
2049 video_num_crops_lst.append(video_outputs["video_num_crops"])
2050 video_num_pooled_patches_lst.append(
2051 video_outputs["video_num_pooled_patches"]
2052 )
2053 video_num_patches_lst.append(video_outputs["video_num_patches"])
2054 video_tokens_lst.append(video_outputs["video_tokens"])
2055 num_video_tokens_lst.append(video_outputs["num_video_tokens"])
2056
2057 all_video_outputs = dict(
2058 pixel_values_videos=torch.cat(pixel_values_videos_lst),
2059 video_token_pooling=torch.cat(video_token_pooling_lst),
2060 video_num_crops=torch.cat(video_num_crops_lst),
2061 video_num_pooled_patches=torch.cat(video_num_pooled_patches_lst),
2062 video_num_patches=torch.cat(video_num_patches_lst),
2063 video_tokens=torch.cat(video_tokens_lst),
2064 num_video_tokens=torch.cat(num_video_tokens_lst),
2065 )
2066 else:
2067 all_video_outputs = dict()
2068
2069 processed_data = self.info.ctx.call_hf_processor(
2070 patched_call,
2071 dict(text=prompt_text, **hf_data),
2072 hf_kwargs,
2073 )
2074
2075 if (images := hf_data.get("images")) is not None:
2076 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
2077 parsed_images = mm_items.get_items("image", ImageProcessorItems)
2078 image_sizes = [
2079 parsed_images.get_image_size(i) for i in range(len(parsed_images))
2080 ]
2081
2082 # For each image: tiling_h * tiling_w + global view
2083 tilings = [
2084 self.info.select_tiling(
2085 image_width=image_size.width,
2086 image_height=image_size.height,
2087 image_processor=image_processor,
2088 )
2089 for image_size in image_sizes
2090 ]
2091 num_crops = torch.tensor(tilings).prod(-1) + 1
2092 assert sum(num_crops) == len(processed_data["pixel_values"])
2093 assert sum(num_crops) == processed_data["image_num_crops"].sum().item()
2094
2095 image_grids = processed_data.pop("image_grids")
2096 image_num_pooled_patches = image_grids[:, :2].prod(dim=1) + image_grids[
2097 :, 2:
2098 ].prod(dim=1)
2099
2100 processed_data["image_num_pooled_patches"] = image_num_pooled_patches
2101 n_patches = processed_data["pixel_values"].shape[1]
2102 processed_data["image_num_patches"] = (
2103 processed_data["image_num_crops"] * n_patches
2104 )
2105 (
2106 processed_data["image_tokens"],
2107 processed_data["num_image_tokens"],
2108 ) = build_flat_image_bool_length(
2109 image_grids,
2110 hf_config,
2111 image_use_col_tokens=hf_processor.image_use_col_tokens,
2112 use_single_crop_col_tokens=hf_processor.use_single_crop_col_tokens,
2113 use_single_crop_start_token=hf_processor.use_single_crop_start_token,
2114 )
2115
2116 processed_data.update(all_video_outputs)
2117 processed_data.pop("input_ids")
2118
2119 return self._finalize_hf_mm_data(
2120 hf_data, hf_kwargs, passthrough_data, processed_data
2121 )
2122
2123 def _get_mm_fields_config(
2124 self,
2125 hf_inputs: BatchFeature,
2126 hf_processor_mm_kwargs: Mapping[str, object],
2127 ) -> Mapping[str, MultiModalFieldConfig]:
2128 image_num_crops = hf_inputs.get("image_num_crops", torch.empty(0))
2129 image_num_pooled_patches = hf_inputs.get(
2130 "image_num_pooled_patches", torch.empty(0)
2131 )
2132 video_num_crops = hf_inputs.get("video_num_crops", torch.empty(0))
2133 video_num_pooled_patches = hf_inputs.get(
2134 "video_num_pooled_patches", torch.empty(0)
2135 )
2136 num_image_tokens = hf_inputs.get("num_image_tokens", torch.empty(0))
2137 num_video_tokens = hf_inputs.get("num_video_tokens", torch.empty(0))
2138
2139 return dict(
2140 pixel_values=MultiModalFieldConfig.flat_from_sizes(
2141 "image", image_num_crops
2142 ),
2143 image_token_pooling=MultiModalFieldConfig.flat_from_sizes(
2144 "image", image_num_pooled_patches
2145 ),
2146 image_num_crops=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
2147 image_num_pooled_patches=MultiModalFieldConfig.batched(
2148 "image", keep_on_cpu=True
2149 ),
2150 image_num_patches=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
2151 image_tokens=MultiModalFieldConfig.flat_from_sizes(
2152 "image", num_image_tokens
2153 ),
2154 num_image_tokens=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
2155 pixel_values_videos=MultiModalFieldConfig.flat_from_sizes(
2156 "video", video_num_crops
2157 ),
2158 video_token_pooling=MultiModalFieldConfig.flat_from_sizes(
2159 "video", video_num_pooled_patches
2160 ),
2161 video_num_crops=MultiModalFieldConfig.batched("video", keep_on_cpu=True),
2162 video_num_pooled_patches=MultiModalFieldConfig.batched(
2163 "video", keep_on_cpu=True
2164 ),
2165 video_num_patches=MultiModalFieldConfig.batched("video", keep_on_cpu=True),
2166 video_tokens=MultiModalFieldConfig.flat_from_sizes(
2167 "video", num_video_tokens
2168 ),
2169 num_video_tokens=MultiModalFieldConfig.batched("video", keep_on_cpu=True),
2170 )
2171
2172 def _get_prompt_updates(
2173 self,
2174 mm_items: MultiModalDataItems,
2175 hf_processor_mm_kwargs: Mapping[str, object],
2176 out_mm_kwargs: MultiModalKwargsItems,
2177 ) -> Sequence[PromptUpdate]:
2178 hf_config = self.info.get_hf_config()
2179 img_patch_id = hf_config.image_patch_id
2180 img_col_id = hf_config.image_col_id
2181 img_start_id = hf_config.image_start_token_id
2182 img_end_id = hf_config.image_end_token_id
2183 low_res_im_start_id = hf_config.low_res_image_start_token_id
2184 frame_start_id = hf_config.frame_start_token_id
2185 frame_end_id = hf_config.frame_end_token_id
2186 im_low_res_id = hf_config.image_low_res_id
2187
2188 emb_tok_ids = [
2189 img_patch_id,
2190 img_col_id,
2191 img_start_id,
2192 low_res_im_start_id,
2193 frame_start_id,
2194 img_end_id,
2195 frame_end_id,
2196 im_low_res_id,
2197 ]
2198
2199 processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
2200 image_use_col_tokens = processor.image_use_col_tokens
2201 use_single_crop_col_tokens = processor.use_single_crop_col_tokens
2202 use_single_crop_start_token = processor.use_single_crop_start_token
2203 video_use_col_tokens = processor.video_use_col_tokens
2204 use_frame_special_tokens = processor.use_frame_special_tokens
2205
2206 tokenizer = processor.tokenizer
2207 vocab = tokenizer.get_vocab()
2208
2209 image_processor = processor.image_processor
2210 video_processor = processor.video_processor
2211
2212 def get_image_replacement_molmo2(item_idx: int):
2213 images = mm_items.get_items("image", ImageProcessorItems)
2214 image = images.get(item_idx)
2215 image = exif_transpose(image)
2216
2217 resize_ncols, resize_nrows = self.info.get_base_grid_size(image_processor)
2218 if use_single_crop_col_tokens is not None:
2219 use_col_tokens = use_single_crop_col_tokens
2220 else:
2221 use_col_tokens = image_use_col_tokens
2222 if use_single_crop_start_token:
2223 start_id = low_res_im_start_id
2224 else:
2225 start_id = img_start_id
2226 extra_row = [img_patch_id] * resize_ncols + [img_col_id] * int(
2227 use_col_tokens
2228 )
2229 extra_joint = [start_id] + extra_row * resize_nrows + [img_end_id]
2230
2231 image_size = get_image_size(image)
2232
2233 ncols, nrows = self.info.get_patches_grid_size(
2234 image_height=image_size.height,
2235 image_width=image_size.width,
2236 image_processor=image_processor,
2237 )
2238
2239 joint_row = [img_patch_id] * ncols + [img_col_id] * int(
2240 image_use_col_tokens
2241 )
2242 joint = [img_start_id] + joint_row * nrows + [img_end_id]
2243 img_token_ids = extra_joint + joint
2244
2245 return PromptUpdateDetails.select_token_ids(img_token_ids, emb_tok_ids)
2246
2247 def get_video_replacement_molmo2(item_idx: int):
2248 video, metadata = mm_items["video"][item_idx]
2249 do_sample_frames = hf_processor_mm_kwargs.get("do_sample_frames")
2250 assert do_sample_frames is None or isinstance(do_sample_frames, bool)
2251
2252 timestamps = self.info._get_video_second_idx(metadata, do_sample_frames)
2253 ncols, nrows = self.info.get_base_grid_size(video_processor)
2254
2255 if use_frame_special_tokens:
2256 start_id = frame_start_id
2257 end_id = frame_end_id
2258 else:
2259 start_id = img_start_id
2260 end_id = img_end_id
2261
2262 img_token_ids = []
2263
2264 for frame_idx, frame_time in enumerate(timestamps):
2265 prev_space = " " if frame_idx > 0 else ""
2266 frame_prefix = (
2267 prev_space + f"{frame_time:.1f} "
2268 ) # explicit whitespace before/after image tokens
2269
2270 img_token_ids += tokenizer.encode(
2271 frame_prefix,
2272 add_special_tokens=False,
2273 )
2274
2275 joint_row = [img_patch_id] * ncols + [img_col_id] * int(
2276 video_use_col_tokens
2277 )
2278 joint = [start_id] + nrows * joint_row + [end_id]
2279 img_token_ids += joint
2280
2281 return PromptUpdateDetails.select_token_ids(img_token_ids, emb_tok_ids)
2282
2283 return [
2284 PromptReplacement(
2285 modality=modality,
2286 target=[target],
2287 replacement=replacement_fn,
2288 )
2289 for modality, target, replacement_fn in zip(
2290 ["image", "video"],
2291 [vocab[IMAGE_PROMPT], vocab[VIDEO_PROMPT]],
2292 [get_image_replacement_molmo2, get_video_replacement_molmo2],
2293 )
2294 ]
2295
2296
2297 @MULTIMODAL_REGISTRY.register_processor(
2298 Molmo2MultiModalProcessor,
2299 info=Molmo2ProcessingInfo,
2300 dummy_inputs=Molmo2DummyInputsBuilder,
2301 )
2302 class Molmo2ForConditionalGeneration(
2303 nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA, SupportsQuant
2304 ):
2305 hf_to_vllm_mapper = WeightsMapper(
2306 orig_to_new_substr={
2307 # vision backbone mapping
2308 "image_pooling_2d.wq": "image_pooling_2d.q_proj",
2309 "image_pooling_2d.wk": "image_pooling_2d.k_proj",
2310 "image_pooling_2d.wv": "image_pooling_2d.v_proj",
2311 "image_pooling_2d.wo": "image_pooling_2d.o_proj",
2312 "image_projector.w1": "image_projector.gate_proj",
2313 "image_projector.w3": "image_projector.up_proj",
2314 "image_projector.w2": "image_projector.down_proj",
2315 # language backbone mapping
2316 "att_proj": "qkv_proj",
2317 "attn_out": "o_proj",
2318 "q_norm": "q_norm",
2319 "k_norm": "k_norm",
2320 "ff_proj": "up_gate_proj",
2321 "ff_out": "down_proj",
2322 "attn_norm": "input_layernorm",
2323 "ff_norm": "post_attention_layernorm",
2324 },
2325 orig_to_new_prefix={
2326 # vision backbone mapping
2327 "model.vision_backbone.": "vision_backbone.",
2328 # language backbone mapping
2329 "model.transformer.blocks.": "model.layers.",
2330 "model.transformer.ln_f.": "model.norm.",
2331 },
2332 )
2333
2334 packed_modules_mapping = {
2335 "qkv_proj": ["qkv_proj"],
2336 "up_gate_proj": ["up_gate_proj"], # language model
2337 "merged_qkv": ["wq", "wk", "wv"], # vision backbone
2338 "merged_kv": ["k_proj", "v_proj"], # image_pooling_2d
2339 "merged_linear": ["gate_proj", "up_proj"], # image_projector
2340 }
2341
2342 @classmethod
2343 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
2344 if modality.startswith("image"):
2345 return IMAGE_PROMPT
2346 if modality.startswith("video"):
2347 return VIDEO_PROMPT
2348
2349 raise ValueError("Only image or video modality is supported")
2350
2351 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
2352 super().__init__()
2353 config = vllm_config.model_config.hf_config
2354 quant_config = vllm_config.quant_config
2355 multimodal_config = vllm_config.model_config.multimodal_config
2356 self.config = config
2357 self.multimodal_config = multimodal_config
2358
2359 kwargs = {}
2360 for field in fields(VitConfig):
2361 kwargs[field.name] = getattr(config.vit_config, field.name)
2362 vit_config = VitConfig(**kwargs)
2363
2364 kwargs = {}
2365 for field in fields(AdapterConfig):
2366 kwargs[field.name] = getattr(config.adapter_config, field.name)
2367 adapter_config = AdapterConfig(**kwargs)
2368
2369 with self._mark_tower_model(vllm_config, {"image", "video"}):
2370 self.vision_backbone = Molmo2VisionBackbone(
2371 vit_config,
2372 adapter_config,
2373 quant_config,
2374 prefix=maybe_prefix(prefix, "vision_backbone"),
2375 )
2376
2377 with self._mark_language_model(vllm_config):
2378 self.model = Molmo2TextModel(
2379 vllm_config=vllm_config,
2380 prefix=maybe_prefix(prefix, "model"),
2381 )
2382
2383 self.img_patch_id = config.image_patch_id
2384
2385 if hasattr(config, "text_config"):
2386 hf_text_config = config.text_config
2387 else:
2388 hf_text_config = config.llm_config
2389
2390 self.lm_head = ParallelLMHead(
2391 hf_text_config.vocab_size,
2392 hf_text_config.hidden_size,
2393 quant_config=quant_config,
2394 )
2395 self.logits_processor = LogitsProcessor(hf_text_config.vocab_size)
2396
2397 self.make_empty_intermediate_tensors = (
2398 self.model.make_empty_intermediate_tensors
2399 )
2400
2401 @property
2402 def dtype(self):
2403 return next(self.parameters()).dtype
2404
2405 def _parse_and_validate_image_input(
2406 self,
2407 **kwargs: object,
2408 ) -> Molmo2ImageInputs | None:
2409 pixel_values = kwargs.pop("pixel_values", None)
2410 if pixel_values is None:
2411 return None
2412
2413 token_pooling = kwargs.pop("image_token_pooling", None)
2414 num_pooled_patches = kwargs.pop("image_num_pooled_patches", None)
2415 num_patches = kwargs.pop("image_num_patches", None)
2416 image_tokens = kwargs.pop("image_tokens", None)
2417 num_image_tokens = kwargs.pop("num_image_tokens", None)
2418
2419 assert isinstance(token_pooling, torch.Tensor)
2420 assert isinstance(num_pooled_patches, torch.Tensor)
2421 assert isinstance(num_patches, torch.Tensor)
2422 assert isinstance(image_tokens, torch.Tensor)
2423 assert isinstance(num_image_tokens, torch.Tensor)
2424
2425 accum_patches = [0] + num_patches.cumsum(dim=0)[:-1].tolist()
2426 patch_offset = 0
2427 new_token_pooling = token_pooling.clone()
2428 for i, n_tensor in enumerate(num_pooled_patches):
2429 n = int(n_tensor)
2430 cur_slice = token_pooling[patch_offset : patch_offset + n]
2431 index_offset = int(accum_patches[i])
2432 new_token_pooling[patch_offset : patch_offset + n] = torch.where(
2433 cur_slice >= 0,
2434 cur_slice + index_offset,
2435 cur_slice,
2436 )
2437 patch_offset += n
2438
2439 return Molmo2ImageInputs(
2440 pixel_values=pixel_values,
2441 token_pooling=new_token_pooling,
2442 num_pooled_patches=num_pooled_patches,
2443 image_tokens=image_tokens,
2444 num_image_tokens=num_image_tokens,
2445 )
2446
2447 def _parse_and_validate_video_input(
2448 self,
2449 **kwargs: object,
2450 ) -> Molmo2VideoInputs | None:
2451 pixel_values_videos = kwargs.pop("pixel_values_videos", None)
2452 if pixel_values_videos is None:
2453 return None
2454
2455 token_pooling = kwargs.pop("video_token_pooling", None)
2456 num_pooled_patches = kwargs.pop("video_num_pooled_patches", None)
2457 num_patches = kwargs.pop("video_num_patches", None)
2458 video_tokens = kwargs.pop("video_tokens", None)
2459 num_video_tokens = kwargs.pop("num_video_tokens", None)
2460
2461 assert isinstance(token_pooling, torch.Tensor)
2462 assert isinstance(num_pooled_patches, torch.Tensor)
2463 assert isinstance(num_patches, torch.Tensor)
2464 assert isinstance(video_tokens, torch.Tensor)
2465 assert isinstance(num_video_tokens, torch.Tensor)
2466
2467 accum_patches = [0] + num_patches.cumsum(dim=0)[:-1].tolist()
2468 patch_offset = 0
2469 new_token_pooling = token_pooling.clone()
2470 for i, n_tensor in enumerate(num_pooled_patches):
2471 n = int(n_tensor)
2472 cur_slice = token_pooling[patch_offset : patch_offset + n]
2473 index_offset = int(accum_patches[i])
2474 new_token_pooling[patch_offset : patch_offset + n] = torch.where(
2475 cur_slice >= 0,
2476 cur_slice + index_offset,
2477 cur_slice,
2478 )
2479 patch_offset += n
2480
2481 return Molmo2VideoInputs(
2482 pixel_values_videos=pixel_values_videos,
2483 token_pooling=new_token_pooling,
2484 num_pooled_patches=num_pooled_patches,
2485 video_tokens=video_tokens,
2486 num_video_tokens=num_video_tokens,
2487 )
2488
2489 def _parse_and_validate_multimodal_inputs(
2490 self, **kwargs: object
2491 ) -> Molmo2MultiModalInputs:
2492 modalities: Molmo2MultiModalInputs = {}
2493
2494 for input_key in kwargs:
2495 if input_key in ("pixel_values",) and "images" not in modalities:
2496 modalities["images"] = self._parse_and_validate_image_input(**kwargs)
2497 if input_key in ("pixel_values_videos",) and "videos" not in modalities:
2498 modalities["videos"] = self._parse_and_validate_video_input(**kwargs)
2499 return modalities
2500
2501 def _process_image_input(
2502 self,
2503 image_input: Molmo2ImageInputs,
2504 ) -> tuple[torch.Tensor, ...]:
2505 pixel_values = image_input["pixel_values"]
2506 token_pooling = image_input["token_pooling"]
2507 num_pooled_patches = image_input["num_pooled_patches"]
2508 image_tokens = image_input["image_tokens"]
2509 num_image_tokens = image_input["num_image_tokens"]
2510
2511 image_features_flat = self.vision_backbone(
2512 images=pixel_values.unsqueeze(0),
2513 token_pooling=token_pooling.unsqueeze(0),
2514 )
2515
2516 assert len(image_features_flat) == num_pooled_patches.sum()
2517 image_features_list = image_features_flat.split(
2518 num_pooled_patches.tolist(), dim=0
2519 )
2520 image_tokens_list = image_tokens.split(num_image_tokens.tolist(), dim=0)
2521 out = []
2522 for image_features_i, image_tokens_i in zip(
2523 image_features_list, image_tokens_list
2524 ):
2525 out_features = self.get_language_model().embed_input_ids(image_tokens_i)
2526 is_image_patch = image_tokens_i == self.img_patch_id
2527 out_features[is_image_patch] = image_features_i
2528 out.append(out_features)
2529 return tuple(out)
2530
2531 def _process_video_input(
2532 self,
2533 video_input: Molmo2VideoInputs,
2534 ) -> tuple[torch.Tensor, ...]:
2535 pixel_values_videos = video_input["pixel_values_videos"]
2536 token_pooling = video_input["token_pooling"]
2537 num_pooled_patches = video_input["num_pooled_patches"]
2538 video_tokens = video_input["video_tokens"]
2539 num_video_tokens = video_input["num_video_tokens"]
2540
2541 image_features_flat = self.vision_backbone(
2542 images=pixel_values_videos.unsqueeze(0),
2543 token_pooling=token_pooling.unsqueeze(0),
2544 )
2545
2546 assert len(image_features_flat) == num_pooled_patches.sum()
2547 image_features_list = image_features_flat.split(
2548 num_pooled_patches.tolist(), dim=0
2549 )
2550 video_tokens_list = video_tokens.split(num_video_tokens.tolist(), dim=0)
2551 out = []
2552 for image_features_i, video_tokens_i in zip(
2553 image_features_list, video_tokens_list
2554 ):
2555 out_features = self.get_language_model().embed_input_ids(video_tokens_i)
2556 is_image_patch = video_tokens_i == self.img_patch_id
2557 out_features[is_image_patch] = image_features_i
2558 out.append(out_features)
2559 return tuple(out)
2560
2561 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings | None:
2562 modalities = self._parse_and_validate_multimodal_inputs(**kwargs)
2563 if not modalities:
2564 return []
2565
2566 multimodal_embeddings: tuple[torch.Tensor, ...] = ()
2567
2568 for modality in modalities:
2569 if modality == "images":
2570 image_input = modalities["images"]
2571 assert image_input is not None
2572 image_embeddings = self._process_image_input(image_input)
2573 multimodal_embeddings += image_embeddings
2574 if modality == "videos":
2575 video_input = modalities["videos"]
2576 assert video_input is not None
2577 video_embeddings = self._process_video_input(video_input)
2578 multimodal_embeddings += video_embeddings
2579
2580 return multimodal_embeddings
2581
2582 def embed_input_ids(
2583 self,
2584 input_ids: torch.Tensor,
2585 multimodal_embeddings: MultiModalEmbeddings | None = None,
2586 *,
2587 is_multimodal: torch.Tensor | None = None,
2588 ) -> torch.Tensor:
2589 inputs_embeds = self._embed_text_input_ids(
2590 input_ids,
2591 self.get_language_model().embed_input_ids,
2592 is_multimodal=is_multimodal,
2593 )
2594
2595 if multimodal_embeddings is None or len(multimodal_embeddings) == 0:
2596 return inputs_embeds
2597
2598 if is_multimodal is None:
2599 raise ValueError(
2600 "`embed_input_ids` now requires `is_multimodal` arg, "
2601 "please update your model runner according to "
2602 "https://github.com/vllm-project/vllm/pull/16229."
2603 )
2604
2605 inputs_embeds = _merge_multimodal_embeddings(
2606 inputs_embeds=inputs_embeds,
2607 multimodal_embeddings=multimodal_embeddings,
2608 is_multimodal=is_multimodal,
2609 )
2610 return inputs_embeds
2611
2612 def forward(
2613 self,
2614 input_ids: torch.LongTensor,
2615 positions: torch.LongTensor,
2616 intermediate_tensors: IntermediateTensors | None = None,
2617 inputs_embeds: torch.Tensor | None = None,
2618 **kwargs: object,
2619 ) -> torch.Tensor:
2620 if intermediate_tensors is not None:
2621 inputs_embeds = None
2622
2623 hidden_states = self.model(
2624 input_ids,
2625 positions,
2626 intermediate_tensors,
2627 inputs_embeds=inputs_embeds,
2628 **kwargs,
2629 )
2630
2631 return hidden_states
2632
2633 def compute_logits(self, hidden_states: torch.Tensor) -> torch.Tensor:
2634 logits = self.logits_processor(self.lm_head, hidden_states)
2635 return logits
2636
2637 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
2638 loader = AutoWeightsLoader(self)
2639 weights = _get_weights_with_merged_embedding(weights)
2640 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
2641
2642 def get_mm_mapping(self) -> MultiModelKeys:
2643 """Get the module prefix in multimodal models."""
2644 return MultiModelKeys.from_string_field(
2645 language_model="model",
2646 connector="vision_backbone.image_projector",
2647 tower_model="vision_backbone",
2648 )
2649
2650
2651 def _get_weights_with_merged_embedding(
2652 weights: Iterable[tuple[str, torch.Tensor]],
2653 ) -> Iterable[tuple[str, torch.Tensor]]:
2654 embedding_weights = {}
2655 for name, weight in weights:
2656 if "wte.embedding" in name:
2657 embedding_weights["embedding"] = weight
2658 elif "wte.new_embedding" in name:
2659 embedding_weights["new_embedding"] = weight
2660 else:
2661 yield (name, weight)
2662 # this is compatible with most of quantization,
2663 # because they won't quantize embed_tokens
2664 if "embedding" not in embedding_weights or "new_embedding" not in embedding_weights:
2665 raise ValueError(
2666 "Checkpoint is missing 'wte.embedding' or "
2667 "'wte.new_embedding' weights required for Molmo2."
2668 )
2669
2670 embedding_weights = torch.cat(
2671 [embedding_weights["embedding"], embedding_weights["new_embedding"]],
2672 dim=0,
2673 )
2674 yield ("model.embed_tokens.weight", embedding_weights)