1
+# SPDX-License-Identifier: Apache-2.0
2
+#
3
+# Copyright 2025 the LLAMA4, Meta Inc., vLLM, and HuggingFace Inc. team.
4
+# All rights reserved.
5
+#
6
+#
7
+# Licensed under the Apache License, Version 2.0 (the "License");
8
+# you may not use this file except in compliance with the License.
9
+# You may obtain a copy of the License at
10
+#
11
+# http://www.apache.org/licenses/LICENSE-2.0
12
+#
13
+# Unless required by applicable law or agreed to in writing, software
14
+# distributed under the License is distributed on an "AS IS" BASIS,
15
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+# See the License for the specific language governing permissions and
17
+# limitations under the License.
18
+import math
19
+from collections.abc import Iterable, Mapping
20
+from functools import cached_property
21
+from itertools import tee
22
+from typing import List, Literal, Optional, Set, Tuple, TypedDict, Union
23
+
24
+import torch
25
+from torch import nn
26
+from transformers import BatchFeature, Llama4Config, Llama4VisionConfig
27
+from transformers.image_utils import SizeDict
28
+from transformers.models.llama4 import Llama4Processor
29
+from transformers.models.llama4.image_processing_llama4_fast import (
30
+ find_supported_resolutions, get_best_fit)
31
+
32
+from vllm.attention.layer import MultiHeadAttention
33
+from vllm.config import VllmConfig
34
+from vllm.distributed import get_tensor_model_parallel_world_size
35
+from vllm.inputs import InputProcessingContext
36
+from vllm.logger import init_logger
37
+from vllm.model_executor.layers.linear import (ColumnParallelLinear,
38
+ QKVParallelLinear,
39
+ RowParallelLinear)
40
+from vllm.model_executor.layers.quantization import QuantizationConfig
41
+from vllm.model_executor.layers.rotary_embedding import get_rope
42
+from vllm.model_executor.layers.sampler import SamplerOutput, get_sampler
43
+from vllm.model_executor.model_loader.loader import _initialize_model
44
+from vllm.model_executor.model_loader.weight_utils import default_weight_loader
45
+from vllm.model_executor.sampling_metadata import SamplingMetadata
46
+from vllm.multimodal import MULTIMODAL_REGISTRY
47
+from vllm.multimodal.inputs import (MultiModalFieldConfig, MultiModalKwargs,
48
+ NestedTensors)
49
+from vllm.multimodal.parse import (ImageProcessorItems, ImageSize,
50
+ MultiModalDataItems)
51
+from vllm.multimodal.processing import (BaseMultiModalProcessor,
52
+ BaseProcessingInfo, PromptReplacement,
53
+ PromptUpdate)
54
+from vllm.multimodal.profiling import BaseDummyInputsBuilder, ProcessorInputs
55
+from vllm.sequence import IntermediateTensors
56
+
57
+from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
58
+from .llama4 import Llama4ForCausalLM
59
+from .utils import (AutoWeightsLoader, flatten_bn, maybe_prefix,
60
+ merge_multimodal_embeddings)
61
+from .vision import scatter_patch_features, select_patch_features
62
+
63
+logger = init_logger(__name__)
64
+
65
+
66
+class Llama4ImagePatchInputs(TypedDict):
67
+ type: Literal["pixel_values"]
68
+ flat_data: torch.Tensor
69
+ """
70
+ Shape:
71
+ `(batch_size * num_chunks, num_channels, image size, image size)`
72
+ """
73
+ patches_per_image: torch.Tensor
74
+ """
75
+ The number of total patches for each image in the batch.
76
+
77
+ This is used to split the embeddings which has the first two dimensions
78
+ flattened just like `flat_data`.
79
+ """
80
+ embed_is_patch: Union[torch.Tensor, list[torch.Tensor]]
81
+ """
82
+ A boolean mask indicating which image embeddings correspond
83
+ to patch tokens.
84
+ """
85
+ aspect_ratios: Union[torch.Tensor, list[torch.Tensor]]
86
+ """
87
+ A list of aspect ratios corresponding to the number of tiles
88
+ in each dimension that each image in the batch corresponds to.
89
+
90
+ Shape:
91
+ `(batch_size, ratio)` where ratio is a pair `(ratio_h, ratio_w)`
92
+ """
93
+
94
+
95
+class Llama4VisionMLP(nn.Module):
96
+
97
+ def __init__(self,
98
+ input_size: int,
99
+ intermediate_size: int,
100
+ output_size: int,
101
+ bias: bool,
102
+ output_activation: bool,
103
+ quant_config: Optional[QuantizationConfig] = None,
104
+ prefix: str = ""):
105
+ super().__init__()
106
+ self.fc1 = ColumnParallelLinear(
107
+ input_size=input_size,
108
+ output_size=intermediate_size,
109
+ bias=bias,
110
+ quant_config=quant_config,
111
+ prefix=f"{prefix}.fc1",
112
+ )
113
+ self.fc2 = RowParallelLinear(
114
+ input_size=intermediate_size,
115
+ output_size=output_size,
116
+ bias=bias,
117
+ quant_config=quant_config,
118
+ prefix=f"{prefix}.fc2",
119
+ )
120
+ self.activation_fn = nn.GELU()
121
+ self.output_activation = output_activation
122
+
123
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
124
+ hidden_states, _ = self.fc1(hidden_states)
125
+ hidden_states = self.activation_fn(hidden_states)
126
+ hidden_states, _ = self.fc2(hidden_states)
127
+ if self.output_activation:
128
+ return self.activation_fn(hidden_states)
129
+ return hidden_states
130
+
131
+
132
+class Llama4MultiModalProjector(nn.Module):
133
+
134
+ def __init__(
135
+ self,
136
+ config,
137
+ quant_config: Optional[QuantizationConfig] = None,
138
+ prefix: str = "",
139
+ ):
140
+ super().__init__()
141
+ self.linear_1 = ColumnParallelLinear(
142
+ input_size=config.vision_config.vision_output_dim,
143
+ output_size=config.text_config.hidden_size,
144
+ bias=False,
145
+ quant_config=quant_config,
146
+ gather_output=True,
147
+ prefix=f"{prefix}.linear_1",
148
+ )
149
+
150
+ def forward(self, image_features):
151
+ hidden_states, _ = self.linear_1(image_features)
152
+ return hidden_states
153
+
154
+
155
+def pixel_shuffle(input_tensor, shuffle_ratio):
156
+ # input_tensor: [batch_size, num_patches, channels]
157
+ batch_size, num_patches, channels = input_tensor.shape
158
+ patch_size = int(math.sqrt(num_patches))
159
+
160
+ input_tensor = input_tensor.view(batch_size, patch_size, patch_size, -1)
161
+ batch_size, height, width, channels = input_tensor.size()
162
+
163
+ reshaped_tensor = input_tensor.view(batch_size, height,
164
+ int(width * shuffle_ratio),
165
+ int(channels / shuffle_ratio))
166
+ reshaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
167
+
168
+ reshaped_tensor = reshaped_tensor.view(batch_size,
169
+ int(height * shuffle_ratio),
170
+ int(width * shuffle_ratio),
171
+ int(channels / (shuffle_ratio**2)))
172
+ reshaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
173
+
174
+ output_tensor = reshaped_tensor.view(batch_size, -1,
175
+ reshaped_tensor.shape[-1])
176
+ return output_tensor
177
+
178
+
179
+class Llama4VisionPixelShuffleMLP(nn.Module):
180
+
181
+ def __init__(
182
+ self,
183
+ config,
184
+ quant_config: Optional[QuantizationConfig] = None,
185
+ prefix: str = "",
186
+ ):
187
+ super().__init__()
188
+ self.pixel_shuffle_ratio = config.pixel_shuffle_ratio
189
+ self.inner_dim = int(config.projector_input_dim //
190
+ (self.pixel_shuffle_ratio**2))
191
+ self.output_dim = config.projector_output_dim
192
+ self.mlp = Llama4VisionMLP(
193
+ input_size=config.intermediate_size,
194
+ intermediate_size=config.projector_input_dim,
195
+ output_size=config.projector_output_dim,
196
+ bias=config.multi_modal_projector_bias,
197
+ output_activation=True,
198
+ quant_config=quant_config,
199
+ prefix=f"{prefix}.mlp")
200
+
201
+ def forward(self, encoded_patches: torch.Tensor) -> torch.Tensor:
202
+ encoded_patches = pixel_shuffle(encoded_patches,
203
+ self.pixel_shuffle_ratio)
204
+ return self.mlp(encoded_patches)
205
+
206
+
207
+class Llama4VisionAttention(nn.Module):
208
+
209
+ def __init__(
210
+ self,
211
+ config: Llama4VisionConfig,
212
+ quant_config: Optional[QuantizationConfig],
213
+ prefix: str = "",
214
+ ):
215
+ super().__init__()
216
+ self.config = config
217
+ self.tp_size = get_tensor_model_parallel_world_size()
218
+ self.embed_dim = config.hidden_size
219
+ self.num_heads = config.num_attention_heads
220
+ self.head_dim = config.hidden_size // self.num_heads
221
+ assert self.num_heads % self.tp_size == 0
222
+ self.num_local_heads = self.num_heads // self.tp_size
223
+ self.q_size = self.num_local_heads * self.head_dim
224
+ self.kv_size = self.num_local_heads * self.head_dim
225
+ self.attention_dropout = config.attention_dropout
226
+ self.scaling = self.head_dim**-0.5
227
+
228
+ self.attn = MultiHeadAttention(self.num_local_heads, self.head_dim,
229
+ self.scaling)
230
+ self.qkv_proj = QKVParallelLinear(
231
+ self.embed_dim,
232
+ self.head_dim,
233
+ self.num_heads,
234
+ bias=True,
235
+ quant_config=quant_config,
236
+ prefix=f"{prefix}.qkv_proj",
237
+ )
238
+ self.o_proj = RowParallelLinear(
239
+ self.num_heads * self.head_dim,
240
+ self.embed_dim,
241
+ bias=True,
242
+ input_is_parallel=True,
243
+ quant_config=quant_config,
244
+ prefix=f"{prefix}.o_proj",
245
+ )
246
+
247
+ self.rotary_emb = get_rope(
248
+ head_size=self.head_dim,
249
+ rotary_dim=config.hidden_size // config.num_attention_heads // 2,
250
+ # number of image patches
251
+ max_position=(config.image_size // config.patch_size)**2,
252
+ base=config.rope_theta,
253
+ rope_scaling={"rope_type": "mllama4"},
254
+ is_neox_style=False,
255
+ dtype=torch.complex64, # important
256
+ )
257
+
258
+ def forward(
259
+ self,
260
+ hidden_states: torch.Tensor,
261
+ ) -> torch.Tensor:
262
+ input_shape = hidden_states.shape[:-1]
263
+
264
+ qkv, _ = self.qkv_proj(hidden_states)
265
+ q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
266
+
267
+ q = q.view(q.shape[0], q.shape[1], self.num_local_heads, self.head_dim)
268
+ k = k.view(k.shape[0], k.shape[1], self.num_local_heads, self.head_dim)
269
+ q, k = self.rotary_emb(q, k)
270
+
271
+ q = q.view(q.shape[0], q.shape[1], -1)
272
+ k = k.view(k.shape[0], k.shape[1], -1)
273
+
274
+ attn_output = self.attn(q, k, v)
275
+ attn_output = attn_output.reshape(*input_shape, -1).contiguous()
276
+ attn_output, _ = self.o_proj(attn_output)
277
+
278
+ return attn_output
279
+
280
+
281
+class Llama4VisionEncoderLayer(nn.Module):
282
+
283
+ def __init__(
284
+ self,
285
+ config: Llama4VisionConfig,
286
+ quant_config: Optional[QuantizationConfig],
287
+ prefix: str = "",
288
+ ):
289
+ super().__init__()
290
+ self.hidden_size = config.hidden_size
291
+ self.num_attention_heads = config.num_attention_heads
292
+ self.intermediate_size = config.intermediate_size
293
+
294
+ self.self_attn = Llama4VisionAttention(config,
295
+ quant_config=quant_config,
296
+ prefix=f"{prefix}.self_attn")
297
+ self.mlp = Llama4VisionMLP(input_size=config.hidden_size,
298
+ intermediate_size=config.intermediate_size,
299
+ output_size=config.hidden_size,
300
+ bias=True,
301
+ output_activation=False,
302
+ quant_config=quant_config,
303
+ prefix=f"{prefix}.mlp")
304
+
305
+ self.input_layernorm = nn.LayerNorm(config.hidden_size)
306
+ self.post_attention_layernorm = nn.LayerNorm(config.hidden_size)
307
+
308
+ def forward(
309
+ self,
310
+ hidden_state: torch.Tensor,
311
+ ):
312
+ # Self Attention
313
+ residual = hidden_state
314
+ hidden_state = self.input_layernorm(hidden_state)
315
+ hidden_state = self.self_attn(hidden_state)
316
+ hidden_state = residual + hidden_state
317
+
318
+ # Feed forward
319
+ residual = hidden_state
320
+ hidden_state = self.post_attention_layernorm(hidden_state)
321
+ hidden_state = self.mlp(hidden_state)
322
+ hidden_state = residual + hidden_state
323
+
324
+ outputs = (hidden_state, )
325
+ return outputs
326
+
327
+
328
+class Llama4VisionEncoder(nn.Module):
329
+
330
+ def __init__(
331
+ self,
332
+ config: Llama4VisionConfig,
333
+ quant_config: Optional[QuantizationConfig],
334
+ prefix: str = "",
335
+ ):
336
+ super().__init__()
337
+ self.config = config
338
+ self.layers = nn.ModuleList([
339
+ Llama4VisionEncoderLayer(
340
+ config,
341
+ quant_config=quant_config,
342
+ prefix=f"{prefix}.layers.{layer_idx}",
343
+ ) for layer_idx in range(config.num_hidden_layers)
344
+ ])
345
+
346
+ def forward(
347
+ self,
348
+ hidden_states: torch.Tensor,
349
+ ) -> torch.Tensor:
350
+ r"""
351
+ Args:
352
+ inputs_embeds (`torch.FloatTensor` of shape
353
+ `(batch_size, sequence_length, hidden_size)`):
354
+ Optionally, instead of passing `input_ids` you can choose to
355
+ directly pass an embedded representation. This is useful if you
356
+ want more control over how to convert `input_ids` indices into
357
+ associated vectors than the model's internal embedding
358
+ lookup matrix.
359
+ """
360
+
361
+ for encoder_layer in self.layers:
362
+ layer_outputs = encoder_layer(hidden_states)
363
+ hidden_states = layer_outputs[0]
364
+
365
+ return hidden_states
366
+
367
+
368
+class Llama4UnfoldConvolution(nn.Module):
369
+
370
+ def __init__(self,
371
+ config: Llama4VisionConfig,
372
+ quant_config: Optional[QuantizationConfig] = None,
373
+ prefix: str = ""):
374
+ super().__init__()
375
+ kernel_size = config.patch_size
376
+ if isinstance(kernel_size, int):
377
+ kernel_size = (kernel_size, kernel_size)
378
+ self.unfold = torch.nn.Unfold(kernel_size=kernel_size,
379
+ stride=config.patch_size)
380
+ self.linear = ColumnParallelLinear(config.num_channels *
381
+ kernel_size[0] * kernel_size[1],
382
+ config.hidden_size,
383
+ bias=False,
384
+ quant_config=quant_config,
385
+ gather_output=True,
386
+ prefix=f"{prefix}.linear")
387
+
388
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
389
+ hidden_states = self.unfold(hidden_states)
390
+ hidden_states = hidden_states.permute(0, 2, 1)
391
+ hidden_states, _ = self.linear(hidden_states)
392
+ return hidden_states
393
+
394
+
395
+class Llama4VisionModel(nn.Module):
396
+
397
+ def __init__(
398
+ self,
399
+ config: Llama4VisionConfig,
400
+ quant_config: Optional[QuantizationConfig] = None,
401
+ prefix: str = "",
402
+ ):
403
+ super().__init__()
404
+ self.config = config
405
+ self.image_size = config.image_size
406
+ self.patch_size = config.patch_size
407
+ self.hidden_size = config.hidden_size
408
+ self.num_channels = config.num_channels
409
+
410
+ self.num_patches = (self.image_size // self.patch_size)**2 + 1
411
+ self.scale = config.hidden_size**-0.5
412
+
413
+ self.patch_embedding = Llama4UnfoldConvolution(
414
+ config,
415
+ quant_config=quant_config,
416
+ prefix=f"{prefix}.patch_embedding")
417
+
418
+ self.class_embedding = nn.Parameter(self.scale *
419
+ torch.randn(self.hidden_size))
420
+ self.positional_embedding_vlm = nn.Parameter(
421
+ self.scale * torch.randn(self.num_patches, self.hidden_size))
422
+
423
+ # layer norms
424
+ self.layernorm_pre = nn.LayerNorm(self.hidden_size, eps=1e-5)
425
+ self.layernorm_post = nn.LayerNorm(self.hidden_size, eps=1e-5)
426
+
427
+ # encoders
428
+ self.model = Llama4VisionEncoder(config,
429
+ quant_config=quant_config,
430
+ prefix=f"{prefix}.model")
431
+ self.vision_adapter = Llama4VisionPixelShuffleMLP(
432
+ config, quant_config, prefix=f"{prefix}.vision_adapter")
433
+
434
+ def forward(
435
+ self,
436
+ images_flattened: torch.Tensor,
437
+ ) -> torch.Tensor:
438
+ # Patch embedding
439
+ hidden_state = self.patch_embedding(images_flattened)
440
+ num_tiles, num_patches, hidden_dim = hidden_state.shape
441
+
442
+ # Add cls token
443
+ class_embedding = self.class_embedding.expand(hidden_state.shape[0], 1,
444
+ hidden_state.shape[-1])
445
+ hidden_state = torch.cat([hidden_state, class_embedding], dim=1)
446
+ num_patches += 1
447
+
448
+ # Position embeddings
449
+ hidden_state = hidden_state.reshape(
450
+ num_tiles,
451
+ 1,
452
+ num_patches,
453
+ hidden_dim,
454
+ )
455
+ positional_embedding = self.positional_embedding_vlm.to(
456
+ dtype=hidden_state.dtype, device=hidden_state.device)
457
+ hidden_state = hidden_state + positional_embedding
458
+ hidden_state = self.layernorm_pre(hidden_state)
459
+ hidden_state = hidden_state.view(num_tiles, -1, hidden_dim)
460
+
461
+ # Apply encoder
462
+ hidden_state = self.model(hidden_state)
463
+ hidden_state = self.layernorm_post(hidden_state)
464
+
465
+ # Remove CLS token output
466
+ hidden_state = hidden_state[:, :-1, :]
467
+
468
+ # now, we use Llama4VisionPixelShuffle + mlp to project embeddings
469
+ hidden_state = self.vision_adapter(hidden_state)
470
+
471
+ return hidden_state
472
+
473
+
474
+class Mllama4ProcessingInfo(BaseProcessingInfo):
475
+
476
+ def __init__(self, ctx: InputProcessingContext) -> None:
477
+ super().__init__(ctx)
478
+
479
+ def get_hf_config(self) -> Llama4Config:
480
+ return self.ctx.get_hf_config(Llama4Config)
481
+
482
+ def get_hf_processor(self, **kwargs: object) -> Llama4Processor:
483
+ return self.ctx.get_hf_processor(Llama4Processor,
484
+ use_fast=True,
485
+ **kwargs)
486
+
487
+ def get_supported_mm_limits(self) -> Mapping[str, Optional[int]]:
488
+ return {"image": 10}
489
+
490
+ @staticmethod
491
+ def get_patch_per_chunk(vision_config: Llama4VisionConfig) -> int:
492
+ image_size = vision_config.image_size
493
+ patch_size = vision_config.patch_size
494
+
495
+ assert (
496
+ image_size %
497
+ patch_size == 0), f"chunk size {image_size} should be multiple of "
498
+ f"patch_size {patch_size}"
499
+
500
+ ds_ratio = int(round(1.0 / (vision_config.pixel_shuffle_ratio**2)))
501
+ return (image_size // patch_size)**2 // ds_ratio
502
+
503
+ def get_max_num_tiles(self) -> int:
504
+ image_processor = self.get_hf_processor().image_processor
505
+ return image_processor.max_patches
506
+
507
+ def get_mm_max_tokens_per_item(
508
+ self,
509
+ seq_len: int,
510
+ mm_counts: Mapping[str, int],
511
+ ) -> Mapping[str, int]:
512
+ vision_config = self.get_hf_config().vision_config
513
+ # image_start + local tiles * (patches + 1 x separator) +
514
+ # 1 global tile * (image x 1 + patches) + image_end
515
+ token_per_chunk = self.get_patch_per_chunk(vision_config) + 1
516
+ mm_max_tokens = (self.get_max_num_tiles() + 1) * token_per_chunk + 2
517
+ return {"image": mm_max_tokens}
518
+
519
+ def get_image_size_with_most_features(self) -> ImageSize:
520
+ vision_config = self.get_hf_config().vision_config
521
+ image_size = vision_config.image_size
522
+ # Result in the max possible feature size (h:w = 16:1)
523
+ return ImageSize(height=self.get_max_num_tiles() * image_size,
524
+ width=image_size)
525
+
526
+
527
+class Mllama4MultiModalProcessor(BaseMultiModalProcessor[Mllama4ProcessingInfo]
528
+ ):
529
+
530
+ def _call_hf_processor(
531
+ self,
532
+ prompt: str,
533
+ mm_data: Mapping[str, object],
534
+ mm_kwargs: Mapping[str, object],
535
+ ) -> BatchFeature:
536
+ tokenizer = self.info.get_tokenizer()
537
+
538
+ if mm_data is None:
539
+ return tokenizer(prompt, add_special_tokens=False) # exclude bos
540
+ processed_outputs = super()._call_hf_processor(
541
+ prompt=prompt,
542
+ mm_data=mm_data,
543
+ mm_kwargs=mm_kwargs,
544
+ )
545
+
546
+ processor = self.info.get_hf_processor(**mm_kwargs)
547
+ image_processor = processor.image_processor
548
+ vision_config = self.info.get_hf_config().vision_config
549
+
550
+ if processed_outputs.get("pixel_values") is not None:
551
+ assert "images" in mm_data, \
552
+ "images expected to be in mm_data when pixel_values is present"
553
+
554
+ images = mm_data["images"]
555
+ parsed_images = (self._get_data_parser().parse_mm_data({
556
+ "image":
557
+ images
558
+ }).get_items("image", ImageProcessorItems))
559
+
560
+ tile_size = vision_config.image_size
561
+ possible_resolutions = find_supported_resolutions(
562
+ max_num_chunks=self.info.get_max_num_tiles(),
563
+ patch_size=SizeDict(height=tile_size, width=tile_size),
564
+ )
565
+ best_fit_sizes = [
566
+ get_best_fit(
567
+ (image.size[1], image.size[0]),
568
+ torch.tensor(possible_resolutions),
569
+ resize_to_max_canvas=image_processor.resize_to_max_canvas)
570
+ for image in parsed_images
571
+ ]
572
+ # TODO tile height/width do not necessarily need to match
573
+ aspect_ratios = [(image_size[0] // tile_size,
574
+ image_size[1] // tile_size)
575
+ for image_size in best_fit_sizes]
576
+ patches_per_image = [
577
+ 1 if r_h * r_w == 1 else 1 + r_h * r_w
578
+ for (r_h, r_w) in aspect_ratios
579
+ ]
580
+
581
+ # embed_is_patch should have one feature per image-related token:
582
+ # <|image_start|>, <|tile_*_separator|>, <|image|>, <|image_end|>
583
+ # -> False
584
+ # <|patch|> -> True
585
+ # embed_is_patch has no entries corresponding to non-image-related
586
+ # tokens.
587
+ patch_id = tokenizer.get_vocab()[processor.img_patch_token]
588
+ num_patches_per_chunk = self.info.get_patch_per_chunk(
589
+ vision_config)
590
+ expanded_image_tokens_list = [
591
+ processor._prompt_split_image(aspect_ratio,
592
+ num_patches_per_chunk)
593
+ for aspect_ratio in aspect_ratios
594
+ ]
595
+ expanded_image_token_ids = [
596
+ tokenizer.encode(image_tokens, add_special_tokens=False)
597
+ for image_tokens in expanded_image_tokens_list
598
+ ]
599
+ embed_is_patch = [
600
+ torch.tensor(tokens) == patch_id
601
+ for tokens in expanded_image_token_ids
602
+ ]
603
+
604
+ processed_outputs["aspect_ratios"] = aspect_ratios
605
+ processed_outputs["patches_per_image"] = torch.tensor(
606
+ patches_per_image)
607
+ processed_outputs["embed_is_patch"] = embed_is_patch
608
+
609
+ return processed_outputs
610
+
611
+ def _get_mm_fields_config(
612
+ self,
613
+ hf_inputs: BatchFeature,
614
+ hf_processor_mm_kwargs: Mapping[str, object],
615
+ ) -> Mapping[str, MultiModalFieldConfig]:
616
+ patches_per_image = hf_inputs.get("patches_per_image", torch.empty(0))
617
+ return dict(
618
+ pixel_values=MultiModalFieldConfig.flat_from_sizes(
619
+ "image", patches_per_image),
620
+ patches_per_image=MultiModalFieldConfig.batched("image"),
621
+ aspect_ratios=MultiModalFieldConfig.batched("image"),
622
+ embed_is_patch=MultiModalFieldConfig.batched("image"),
623
+ )
624
+
625
+ def _get_prompt_updates(
626
+ self,
627
+ mm_items: MultiModalDataItems,
628
+ hf_processor_mm_kwargs: Mapping[str, object],
629
+ out_mm_kwargs: MultiModalKwargs,
630
+ ) -> List[PromptUpdate]:
631
+ assert (
632
+ mm_items.get_count("image", strict=False) == 0
633
+ or "aspect_ratios" in out_mm_kwargs
634
+ ), "Transformers expect to include aspect_ratios in out_mm_kwargs"
635
+
636
+ config = self.info.get_hf_config()
637
+ vision_config = config.vision_config
638
+
639
+ num_patches_per_chunk = self.info.get_patch_per_chunk(vision_config)
640
+ hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
641
+ image_token = hf_processor.image_token
642
+
643
+ def get_replacement(item_idx: int):
644
+ aspect_ratio = out_mm_kwargs["aspect_ratios"][item_idx]
645
+ return hf_processor._prompt_split_image(
646
+ aspect_ratio=aspect_ratio,
647
+ num_patches_per_chunk=num_patches_per_chunk)
648
+
649
+ return [
650
+ PromptReplacement(
651
+ modality="image",
652
+ target=image_token,
653
+ replacement=get_replacement,
654
+ )
655
+ ]
656
+
657
+
658
+class Mllama4DummyInputsBuilder(BaseDummyInputsBuilder[Mllama4ProcessingInfo]):
659
+
660
+ def get_dummy_processor_inputs(
661
+ self,
662
+ seq_len: int,
663
+ mm_counts: Mapping[str, int],
664
+ ) -> ProcessorInputs:
665
+ num_images = mm_counts.get("image", 0)
666
+
667
+ (target_width,
668
+ target_height) = self.info.get_image_size_with_most_features()
669
+
670
+ mm_data = {
671
+ "image":
672
+ self._get_dummy_images(width=target_width,
673
+ height=target_height,
674
+ num_images=num_images)
675
+ }
676
+
677
+ image_token = self.info.get_hf_processor().fake_image_token
678
+ return ProcessorInputs(
679
+ prompt_text=image_token * num_images,
680
+ mm_data=mm_data,
681
+ )
682
+
683
+
684
+@MULTIMODAL_REGISTRY.register_processor(
685
+ Mllama4MultiModalProcessor,
686
+ info=Mllama4ProcessingInfo,
687
+ dummy_inputs=Mllama4DummyInputsBuilder,
688
+)
689
+class Llama4ForConditionalGeneration(nn.Module, SupportsMultiModal,
690
+ SupportsPP):
691
+ packed_modules_mapping = {
692
+ "qkv_proj": ["q_proj", "k_proj", "v_proj"],
693
+ }
694
+
695
+ def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
696
+ super().__init__()
697
+ config = vllm_config.model_config.hf_config
698
+ quant_config = vllm_config.quant_config
699
+ multimodal_config = vllm_config.model_config.multimodal_config
700
+ self.config = config
701
+ self.quant_config = quant_config
702
+ self.multimodal_config = multimodal_config
703
+ self.vision_model = Llama4VisionModel(config.vision_config,
704
+ None,
705
+ prefix=maybe_prefix(
706
+ prefix, "vision_model"))
707
+ self.multi_modal_projector = Llama4MultiModalProjector(
708
+ self.config,
709
+ None,
710
+ prefix=maybe_prefix(prefix, "multi_modal_projector"))
711
+
712
+ self.language_model = _initialize_model(
713
+ vllm_config=vllm_config.with_hf_config(config.text_config),
714
+ prefix=maybe_prefix(prefix, "language_model"),
715
+ model_class=Llama4ForCausalLM,
716
+ )
717
+
718
+ self.make_empty_intermediate_tensors = (
719
+ self.language_model.make_empty_intermediate_tensors)
720
+
721
+ @cached_property
722
+ def sampler(self):
723
+ if hasattr(self.language_model, "sampler"):
724
+ return self.language_model.sampler
725
+
726
+ return get_sampler()
727
+
728
+ def _parse_and_validate_image_input(
729
+ self, **kwargs: object) -> Optional[Llama4ImagePatchInputs]:
730
+ # num_images, 1, num_chunks, channel, image_size, image_size
731
+ pixel_values = kwargs.pop("pixel_values", None)
732
+ if pixel_values is None:
733
+ return None
734
+
735
+ # num_images x num_chunks, channel, image_size, image_size
736
+ # TODO: confirm handling for variable lengths
737
+ flat_pixel_values = flatten_bn(pixel_values, concat=True)
738
+ patches_per_image = flatten_bn(kwargs.pop("patches_per_image"))
739
+
740
+ embed_is_patch = kwargs.pop("embed_is_patch", None)
741
+ if not isinstance(embed_is_patch, (torch.Tensor, list)):
742
+ raise ValueError("Incorrect type of embed_is_patch. "
743
+ f"Got type: {type(embed_is_patch)}")
744
+
745
+ aspect_ratios = kwargs.pop("aspect_ratios", None)
746
+ if not isinstance(aspect_ratios, (torch.Tensor, list)):
747
+ raise ValueError("Incorrect type of aspect_ratios. "
748
+ f"Got type: {type(aspect_ratios)}")
749
+
750
+ return Llama4ImagePatchInputs(
751
+ type="pixel_values",
752
+ flat_data=flat_pixel_values,
753
+ patches_per_image=patches_per_image,
754
+ embed_is_patch=embed_is_patch,
755
+ aspect_ratios=aspect_ratios,
756
+ )
757
+
758
+ def _process_image_input(
759
+ self, image_input: Llama4ImagePatchInputs) -> MultiModalEmbeddings:
760
+ flat_data = image_input["flat_data"]
761
+ patches_per_image = image_input["patches_per_image"].tolist()
762
+ vision_embeddings_flat = self.vision_model(flat_data)
763
+ return vision_embeddings_flat.split(patches_per_image, dim=0)
764
+
765
+ def get_multimodal_embeddings(self,
766
+ **kwargs) -> Optional[MultiModalEmbeddings]:
767
+ image_input = self._parse_and_validate_image_input(**kwargs)
768
+ if image_input is None:
769
+ return None
770
+
771
+ # num_images x [num_chunks, num_patches, hidden_dim]
772
+ image_features = self._process_image_input(image_input)
773
+ # num_images x [num_chunks x num_patches, hidden_dim]
774
+ image_features_flat = [img.flatten(0, 1) for img in image_features]
775
+ # num_images x [1, input_len] -> num_images x [input_len]
776
+ embed_is_patch_flat = [
777
+ is_patch.flatten(0, 1)
778
+ for is_patch in image_input["embed_is_patch"]
779
+ ]
780
+
781
+ return scatter_patch_features(
782
+ image_features_flat,
783
+ embed_is_patch_flat,
784
+ )
785
+
786
+ def get_input_embeddings(
787
+ self,
788
+ input_ids: torch.Tensor,
789
+ multimodal_embeddings: Optional[NestedTensors] = None,
790
+ ) -> torch.Tensor:
791
+ inputs_embeds = self.language_model.get_input_embeddings(input_ids)
792
+
793
+ if multimodal_embeddings is not None:
794
+ multimodal_embeddings = torch.cat(multimodal_embeddings)
795
+ mm_embeddings = self.multi_modal_projector(multimodal_embeddings)
796
+ inputs_embeds = merge_multimodal_embeddings(
797
+ input_ids, inputs_embeds, select_patch_features(mm_embeddings),
798
+ self.config.image_token_index)
799
+
800
+ return inputs_embeds
801
+
802
+ def forward(
803
+ self,
804
+ input_ids: torch.Tensor,
805
+ positions: torch.Tensor,
806
+ intermediate_tensors: Optional[IntermediateTensors] = None,
807
+ inputs_embeds: Optional[torch.Tensor] = None,
808
+ **kwargs: object,
809
+ ) -> Union[torch.Tensor, IntermediateTensors]:
810
+ if intermediate_tensors is not None:
811
+ inputs_embeds = None
812
+
813
+ # NOTE: In v1, inputs_embeds is always generated at model runner,
814
+ # this condition is for v0 compatibility.
815
+ elif inputs_embeds is None:
816
+ vision_embeddings = self.get_multimodal_embeddings(**kwargs)
817
+ inputs_embeds = self.get_input_embeddings(input_ids,
818
+ vision_embeddings)
819
+ input_ids = None
820
+
821
+ return self.language_model(input_ids, positions, intermediate_tensors,
822
+ inputs_embeds)
823
+
824
+ def compute_logits(
825
+ self,
826
+ hidden_states: torch.Tensor,
827
+ sampling_metadata: SamplingMetadata,
828
+ ) -> Optional[torch.Tensor]:
829
+ return self.language_model.compute_logits(hidden_states,
830
+ sampling_metadata)
831
+
832
+ def sample(self, logits: torch.Tensor,
833
+ sampling_metadata: SamplingMetadata) -> Optional[SamplerOutput]:
834
+ return self.language_model.sample(logits, sampling_metadata)
835
+
836
+ def separate_weights(
837
+ self,
838
+ weights: Iterable[Tuple[str, torch.Tensor]],
839
+ prefix: str,
840
+ ) -> Tuple[Iterable[Tuple[str, torch.Tensor]], Iterable[Tuple[
841
+ str, torch.Tensor]]]:
842
+ weights1, weights2 = tee(weights, 2)
843
+
844
+ def get_prefix_weights() -> Iterable[Tuple[str, torch.Tensor]]:
845
+ for name, data in weights1:
846
+ if name.startswith(prefix):
847
+ yield (name, data)
848
+
849
+ def get_other_weights() -> Iterable[Tuple[str, torch.Tensor]]:
850
+ for name, data in weights2:
851
+ if not name.startswith(prefix):
852
+ yield (name, data)
853
+
854
+ return get_prefix_weights(), get_other_weights()
855
+
856
+ def load_weights(self, weights: Iterable[Tuple[str,
857
+ torch.Tensor]]) -> Set[str]:
858
+
859
+ stacked_params_mapping = [
860
+ # (param_name, shard_name, shard_id)
861
+ (".self_attn.qkv_proj", ".self_attn.q_proj", "q"),
862
+ (".self_attn.qkv_proj", ".self_attn.k_proj", "k"),
863
+ (".self_attn.qkv_proj", ".self_attn.v_proj", "v"),
864
+ ]
865
+ params_dict = dict(self.named_parameters())
866
+ updated_params: Set[str] = set()
867
+
868
+ # language_model is an Llama4ForCausalLM instance. We load it's
869
+ # using llama4's load_weights routine.
870
+ language_model_weights, other_weights = self.separate_weights(
871
+ weights, prefix="language_model.model.")
872
+ loader = AutoWeightsLoader(self)
873
+ loaded_language_model_params = loader.load_weights(
874
+ language_model_weights)
875
+ assert loaded_language_model_params is not None
876
+ updated_params.update(loaded_language_model_params)
877
+
878
+ for name, loaded_weight in other_weights:
879
+ for param_name, weight_name, shard_id in stacked_params_mapping:
880
+ if weight_name not in name:
881
+ continue
882
+ name = name.replace(weight_name, param_name)
883
+ param = params_dict[name]
884
+ updated_params.add(name)
885
+ weight_loader = param.weight_loader
886
+ weight_loader(param, loaded_weight, shard_id)
887
+ break
888
+ else:
889
+ param = params_dict[name]
890
+ weight_loader = getattr(param, "weight_loader",
891
+ default_weight_loader)
892
+
893
+ weight_loader(param, loaded_weight)
894
+ updated_params.add(name)
895
+ return updated_params