main
py 1,269 lines 44.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 #
4 # Copyright 2025 the LLAMA4, Meta Inc., vLLM, and HuggingFace Inc. team.
5 # All rights reserved.
6 #
7 #
8 # Licensed under the Apache License, Version 2.0 (the "License");
9 # you may not use this file except in compliance with the License.
10 # You may obtain a copy of the License at
11 #
12 # http://www.apache.org/licenses/LICENSE-2.0
13 #
14 # Unless required by applicable law or agreed to in writing, software
15 # distributed under the License is distributed on an "AS IS" BASIS,
16 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 # See the License for the specific language governing permissions and
18 # limitations under the License.
19 import math
20 from collections.abc import Hashable, Iterable, Mapping
21 from typing import Annotated, Any, Literal
22
23 import torch
24 from torch import nn
25 from transformers import BatchFeature, Llama4Config, Llama4VisionConfig
26 from transformers.image_utils import SizeDict
27 from transformers.models.llama4 import Llama4Processor
28 from transformers.models.llama4.image_processing_llama4 import (
29 find_supported_resolutions,
30 get_best_fit,
31 )
32
33 from vllm.compilation.decorators import (
34 should_torch_compile_mm_encoder,
35 support_torch_compile,
36 )
37 from vllm.config import VllmConfig, set_current_vllm_config
38 from vllm.config.multimodal import MultiModalDummyOptions
39 from vllm.distributed import get_tensor_model_parallel_world_size
40 from vllm.inputs import MultiModalDataDict
41 from vllm.model_executor.layers.attention import MMEncoderAttention
42 from vllm.model_executor.layers.fused_moe import (
43 fused_moe_make_expert_params_mapping,
44 )
45 from vllm.model_executor.layers.linear import (
46 ColumnParallelLinear,
47 QKVParallelLinear,
48 ReplicatedLinear,
49 RowParallelLinear,
50 )
51 from vllm.model_executor.layers.quantization import QuantizationConfig
52 from vllm.model_executor.layers.rotary_embedding import get_rope
53 from vllm.model_executor.model_loader.utils import initialize_model
54 from vllm.model_executor.model_loader.weight_utils import (
55 default_weight_loader,
56 maybe_remap_moe_expert_param_name,
57 )
58 from vllm.model_executor.models.module_mapping import MultiModelKeys
59 from vllm.multimodal import MULTIMODAL_REGISTRY
60 from vllm.multimodal.inputs import (
61 MultiModalFieldConfig,
62 MultiModalKwargsItem,
63 MultiModalKwargsItems,
64 )
65 from vllm.multimodal.parse import ImageProcessorItems, ImageSize, MultiModalDataItems
66 from vllm.multimodal.processing import (
67 BaseDummyInputsBuilder,
68 BaseMultiModalProcessor,
69 BaseProcessingInfo,
70 PromptReplacement,
71 PromptUpdate,
72 PromptUpdateDetails,
73 cached_encode,
74 )
75 from vllm.sequence import IntermediateTensors
76 from vllm.utils.tensor_schema import TensorSchema, TensorShape
77
78 from .interfaces import (
79 MixtureOfExperts,
80 MultiModalEmbeddings,
81 SupportsEagle3,
82 SupportsEncoderCudaGraph,
83 SupportsLoRA,
84 SupportsMultiModal,
85 SupportsPP,
86 )
87 from .llama4 import Llama4ForCausalLM
88 from .utils import AutoWeightsLoader, StageMissingLayer, maybe_prefix
89 from .vision import is_vit_use_data_parallel, run_dp_sharded_vision_model
90
91
92 class Llama4ImagePatchInputs(TensorSchema):
93 """Dimensions:
94 - batch_size: Batch size
95 - total_num_chunks: Batch size * number of chunks
96 - num_channels: Number of channels
97 - image_size: Size of each image
98 """
99
100 type: Literal["pixel_values"] = "pixel_values"
101
102 pixel_values: Annotated[
103 torch.Tensor,
104 TensorShape("total_num_chunks", "num_channels", "image_size", "image_size"),
105 ]
106
107 patches_per_image: Annotated[torch.Tensor, TensorShape("batch_size")]
108 """
109 The number of chunked image tiles for each image in the batch.
110
111 This is used to split the embeddings which has the first two dimensions
112 flattened just like `pixel_values`.
113 """
114
115 aspect_ratios: Annotated[torch.Tensor, TensorShape("batch_size", 2)]
116 """
117 A list of aspect ratios corresponding to the number of tiles
118 in each dimension that each image in the batch corresponds to.
119 Each aspect ratio is a pair (ratio_h, ratio_w).
120 """
121
122
123 class Llama4VisionMLP(nn.Module):
124 def __init__(
125 self,
126 input_size: int,
127 intermediate_size: int,
128 output_size: int,
129 bias: bool,
130 output_activation: bool,
131 quant_config: QuantizationConfig | None = None,
132 prefix: str = "",
133 ):
134 super().__init__()
135 use_data_parallel = is_vit_use_data_parallel()
136 self.fc1 = ColumnParallelLinear(
137 input_size=input_size,
138 output_size=intermediate_size,
139 bias=bias,
140 quant_config=quant_config,
141 prefix=f"{prefix}.fc1",
142 disable_tp=use_data_parallel,
143 )
144 self.fc2 = RowParallelLinear(
145 input_size=intermediate_size,
146 output_size=output_size,
147 bias=bias,
148 quant_config=quant_config,
149 prefix=f"{prefix}.fc2",
150 disable_tp=use_data_parallel,
151 )
152 self.activation_fn = nn.GELU()
153 self.output_activation = output_activation
154
155 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
156 hidden_states, _ = self.fc1(hidden_states)
157 hidden_states = self.activation_fn(hidden_states)
158 hidden_states, _ = self.fc2(hidden_states)
159 if self.output_activation:
160 return self.activation_fn(hidden_states)
161 return hidden_states
162
163
164 class Llama4MultiModalProjector(nn.Module):
165 def __init__(
166 self,
167 config,
168 quant_config: QuantizationConfig | None = None,
169 prefix: str = "",
170 ):
171 super().__init__()
172 self.linear_1 = ColumnParallelLinear(
173 input_size=config.vision_config.vision_output_dim,
174 output_size=config.text_config.hidden_size,
175 bias=False,
176 quant_config=quant_config,
177 gather_output=True,
178 prefix=f"{prefix}.linear_1",
179 )
180
181 def forward(self, image_features):
182 hidden_states, _ = self.linear_1(image_features)
183 return hidden_states
184
185
186 def pixel_shuffle(input_tensor, shuffle_ratio):
187 # input_tensor: [batch_size, num_patches, channels]
188 batch_size, num_patches, channels = input_tensor.shape
189 patch_size = int(math.sqrt(num_patches))
190
191 input_tensor = input_tensor.view(batch_size, patch_size, patch_size, -1)
192 batch_size, height, width, channels = input_tensor.size()
193
194 reshaped_tensor = input_tensor.view(
195 batch_size, height, int(width * shuffle_ratio), int(channels / shuffle_ratio)
196 )
197 reshaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
198
199 reshaped_tensor = reshaped_tensor.view(
200 batch_size,
201 int(height * shuffle_ratio),
202 int(width * shuffle_ratio),
203 int(channels / (shuffle_ratio**2)),
204 )
205 reshaped_tensor = reshaped_tensor.permute(0, 2, 1, 3).contiguous()
206
207 output_tensor = reshaped_tensor.view(batch_size, -1, reshaped_tensor.shape[-1])
208 return output_tensor
209
210
211 class Llama4VisionPixelShuffleMLP(nn.Module):
212 def __init__(
213 self,
214 config,
215 quant_config: QuantizationConfig | None = None,
216 prefix: str = "",
217 ):
218 super().__init__()
219 self.pixel_shuffle_ratio = config.pixel_shuffle_ratio
220 self.inner_dim = int(
221 config.projector_input_dim // (self.pixel_shuffle_ratio**2)
222 )
223 self.output_dim = config.projector_output_dim
224 self.mlp = Llama4VisionMLP(
225 input_size=config.intermediate_size,
226 intermediate_size=config.projector_input_dim,
227 output_size=config.projector_output_dim,
228 bias=config.multi_modal_projector_bias,
229 output_activation=True,
230 quant_config=quant_config,
231 prefix=f"{prefix}.mlp",
232 )
233
234 def forward(self, encoded_patches: torch.Tensor) -> torch.Tensor:
235 encoded_patches = pixel_shuffle(encoded_patches, self.pixel_shuffle_ratio)
236 return self.mlp(encoded_patches)
237
238
239 class Llama4VisionAttention(nn.Module):
240 def __init__(
241 self,
242 config: Llama4VisionConfig,
243 quant_config: QuantizationConfig | None,
244 prefix: str = "",
245 ):
246 super().__init__()
247 self.config = config
248 use_data_parallel = is_vit_use_data_parallel()
249 self.tp_size = (
250 1 if use_data_parallel else get_tensor_model_parallel_world_size()
251 )
252 self.embed_dim = config.hidden_size
253 self.num_heads = config.num_attention_heads
254 self.head_dim = config.hidden_size // self.num_heads
255 assert self.num_heads % self.tp_size == 0
256 self.num_local_heads = self.num_heads // self.tp_size
257 self.q_size = self.num_local_heads * self.head_dim
258 self.kv_size = self.num_local_heads * self.head_dim
259 self.attention_dropout = config.attention_dropout
260 self.scaling = self.head_dim**-0.5
261
262 self.attn = MMEncoderAttention(
263 self.num_local_heads,
264 self.head_dim,
265 self.scaling,
266 prefix=f"{prefix}.attn",
267 )
268
269 if use_data_parallel:
270 self.qkv_proj = ReplicatedLinear(
271 self.embed_dim,
272 self.q_size + 2 * self.kv_size,
273 bias=True,
274 quant_config=quant_config,
275 prefix=f"{prefix}.qkv_proj",
276 )
277 self.o_proj = ReplicatedLinear(
278 self.num_heads * self.head_dim,
279 self.embed_dim,
280 bias=True,
281 quant_config=quant_config,
282 prefix=f"{prefix}.o_proj",
283 )
284 else:
285 self.qkv_proj = QKVParallelLinear(
286 self.embed_dim,
287 self.head_dim,
288 self.num_heads,
289 bias=True,
290 quant_config=quant_config,
291 prefix=f"{prefix}.qkv_proj",
292 )
293 self.o_proj = RowParallelLinear(
294 self.num_heads * self.head_dim,
295 self.embed_dim,
296 bias=True,
297 input_is_parallel=True,
298 quant_config=quant_config,
299 prefix=f"{prefix}.o_proj",
300 )
301
302 rope_parameters = {
303 "rope_type": "mllama4",
304 "rope_theta": config.rope_parameters["rope_theta"],
305 "partial_rotary_factor": 0.5,
306 }
307
308 self.rotary_emb = get_rope(
309 head_size=self.head_dim,
310 # number of image patches
311 max_position=(config.image_size // config.patch_size) ** 2,
312 rope_parameters=rope_parameters,
313 is_neox_style=False,
314 dtype=torch.complex64, # important
315 )
316
317 def forward(
318 self,
319 hidden_states: torch.Tensor,
320 ) -> torch.Tensor:
321 input_shape = hidden_states.shape[:-1]
322
323 qkv, _ = self.qkv_proj(hidden_states)
324 q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
325
326 q = q.view(q.shape[0], q.shape[1], self.num_local_heads, self.head_dim)
327 k = k.view(k.shape[0], k.shape[1], self.num_local_heads, self.head_dim)
328 q, k = self.rotary_emb(q, k)
329
330 q = q.view(q.shape[0], q.shape[1], -1)
331 k = k.view(k.shape[0], k.shape[1], -1)
332
333 attn_output = self.attn(q, k, v)
334 attn_output = attn_output.reshape(*input_shape, -1).contiguous()
335 attn_output, _ = self.o_proj(attn_output)
336
337 return attn_output
338
339
340 class Llama4VisionEncoderLayer(nn.Module):
341 def __init__(
342 self,
343 config: Llama4VisionConfig,
344 quant_config: QuantizationConfig | None,
345 prefix: str = "",
346 ):
347 super().__init__()
348 self.hidden_size = config.hidden_size
349 self.num_attention_heads = config.num_attention_heads
350 self.intermediate_size = config.intermediate_size
351
352 self.self_attn = Llama4VisionAttention(
353 config,
354 quant_config=quant_config,
355 prefix=f"{prefix}.self_attn",
356 )
357 self.mlp = Llama4VisionMLP(
358 input_size=config.hidden_size,
359 intermediate_size=config.intermediate_size,
360 output_size=config.hidden_size,
361 bias=True,
362 output_activation=False,
363 quant_config=quant_config,
364 prefix=f"{prefix}.mlp",
365 )
366
367 self.input_layernorm = nn.LayerNorm(config.hidden_size)
368 self.post_attention_layernorm = nn.LayerNorm(config.hidden_size)
369
370 def forward(
371 self,
372 hidden_state: torch.Tensor,
373 ):
374 # Self Attention
375 residual = hidden_state
376 hidden_state = self.input_layernorm(hidden_state)
377 hidden_state = self.self_attn(hidden_state)
378 hidden_state = residual + hidden_state
379
380 # Feed forward
381 residual = hidden_state
382 hidden_state = self.post_attention_layernorm(hidden_state)
383 hidden_state = self.mlp(hidden_state)
384 hidden_state = residual + hidden_state
385
386 outputs = (hidden_state,)
387 return outputs
388
389
390 class Llama4VisionEncoder(nn.Module):
391 def __init__(
392 self,
393 config: Llama4VisionConfig,
394 quant_config: QuantizationConfig | None,
395 prefix: str = "",
396 ):
397 super().__init__()
398 self.config = config
399 self.layers = nn.ModuleList(
400 [
401 Llama4VisionEncoderLayer(
402 config,
403 quant_config=quant_config,
404 prefix=f"{prefix}.layers.{layer_idx}",
405 )
406 for layer_idx in range(config.num_hidden_layers)
407 ]
408 )
409
410 def forward(
411 self,
412 hidden_states: torch.Tensor,
413 ) -> torch.Tensor:
414 r"""Args:
415 hidden_states: Input tensor of shape
416 (batch_size, sequence_length, hidden_size).
417 Hidden states from the model embeddings, representing
418 the input tokens.
419 associated vectors than the model's internal embedding
420 lookup matrix.
421
422 """
423 for encoder_layer in self.layers:
424 layer_outputs = encoder_layer(hidden_states)
425 hidden_states = layer_outputs[0]
426
427 return hidden_states
428
429
430 class Llama4UnfoldConvolution(nn.Module):
431 def __init__(
432 self,
433 config: Llama4VisionConfig,
434 quant_config: QuantizationConfig | None = None,
435 prefix: str = "",
436 ):
437 super().__init__()
438 kernel_size = config.patch_size
439 if isinstance(kernel_size, int):
440 kernel_size = (kernel_size, kernel_size)
441 self.unfold = torch.nn.Unfold(kernel_size=kernel_size, stride=config.patch_size)
442 use_data_parallel = is_vit_use_data_parallel()
443 self.linear = ColumnParallelLinear(
444 input_size=config.num_channels * kernel_size[0] * kernel_size[1],
445 output_size=config.hidden_size,
446 bias=False,
447 gather_output=True,
448 quant_config=quant_config,
449 prefix=f"{prefix}.linear",
450 disable_tp=use_data_parallel,
451 )
452
453 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
454 hidden_states = self.unfold(hidden_states)
455 hidden_states = hidden_states.permute(0, 2, 1)
456 hidden_states, _ = self.linear(hidden_states)
457 return hidden_states
458
459
460 @support_torch_compile(
461 dynamic_arg_dims={"images_flattened": 0},
462 enable_if=should_torch_compile_mm_encoder,
463 is_encoder=True,
464 )
465 class Llama4VisionModel(nn.Module):
466 def __init__(
467 self,
468 config: Llama4VisionConfig,
469 quant_config: QuantizationConfig | None = None,
470 prefix: str = "",
471 ):
472 super().__init__()
473 self.config = config
474 self.image_size = config.image_size
475 self.patch_size = config.patch_size
476 self.hidden_size = config.hidden_size
477 self.num_channels = config.num_channels
478
479 self.num_patches = (self.image_size // self.patch_size) ** 2 + 1
480 self.scale = config.hidden_size**-0.5
481
482 self.patch_embedding = Llama4UnfoldConvolution(
483 config,
484 quant_config=quant_config,
485 prefix=f"{prefix}.patch_embedding",
486 )
487
488 self.class_embedding = nn.Parameter(self.scale * torch.randn(self.hidden_size))
489 self.positional_embedding_vlm = nn.Parameter(
490 self.scale * torch.randn(self.num_patches, self.hidden_size)
491 )
492
493 # layer norms
494 self.layernorm_pre = nn.LayerNorm(self.hidden_size, eps=1e-5)
495 self.layernorm_post = nn.LayerNorm(self.hidden_size, eps=1e-5)
496
497 # encoders
498 self.model = Llama4VisionEncoder(
499 config,
500 quant_config=quant_config,
501 prefix=f"{prefix}.model",
502 )
503
504 self.vision_adapter = Llama4VisionPixelShuffleMLP(
505 config,
506 quant_config,
507 prefix=f"{prefix}.vision_adapter",
508 )
509
510 def forward(
511 self,
512 images_flattened: torch.Tensor,
513 ) -> torch.Tensor:
514 # Patch embedding
515 hidden_state = self.patch_embedding(images_flattened)
516 num_tiles, num_patches, hidden_dim = hidden_state.shape
517
518 # Add cls token
519 class_embedding = self.class_embedding.expand(
520 hidden_state.shape[0], 1, hidden_state.shape[-1]
521 )
522 hidden_state = torch.cat([hidden_state, class_embedding], dim=1)
523 num_patches += 1
524
525 # Position embeddings
526 hidden_state = hidden_state.reshape(
527 num_tiles,
528 1,
529 num_patches,
530 hidden_dim,
531 )
532 positional_embedding = self.positional_embedding_vlm.to(
533 dtype=hidden_state.dtype, device=hidden_state.device
534 )
535 hidden_state = hidden_state + positional_embedding
536 hidden_state = self.layernorm_pre(hidden_state)
537 hidden_state = hidden_state.view(num_tiles, -1, hidden_dim)
538
539 # Apply encoder
540 hidden_state = self.model(hidden_state)
541 hidden_state = self.layernorm_post(hidden_state)
542
543 # Remove CLS token output
544 hidden_state = hidden_state[:, :-1, :]
545
546 # now, we use Llama4VisionPixelShuffle + mlp to project embeddings
547 hidden_state = self.vision_adapter(hidden_state)
548
549 return hidden_state
550
551
552 class Mllama4ProcessingInfo(BaseProcessingInfo):
553 def get_hf_config(self) -> Llama4Config:
554 return self.ctx.get_hf_config(Llama4Config)
555
556 def get_hf_processor(self, **kwargs: object) -> Llama4Processor:
557 return self.ctx.get_hf_processor(Llama4Processor, **kwargs)
558
559 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
560 # Although vLLM can support more images from an infra capability
561 # perspective, we do not recommend using >10 images in practice.
562 return {"image": None}
563
564 @staticmethod
565 def get_patch_per_chunk(vision_config: Llama4VisionConfig) -> int:
566 image_size = vision_config.image_size
567 patch_size = vision_config.patch_size
568
569 assert image_size % patch_size == 0, (
570 f"chunk size {image_size} should be multiple of "
571 )
572 f"patch_size {patch_size}"
573
574 ds_ratio = int(round(1.0 / (vision_config.pixel_shuffle_ratio**2)))
575 return (image_size // patch_size) ** 2 // ds_ratio
576
577 def get_max_num_tiles(self) -> int:
578 image_processor = self.get_hf_processor().image_processor
579 return image_processor.max_patches
580
581 def get_image_size_with_most_features(self) -> ImageSize:
582 vision_config = self.get_hf_config().vision_config
583 image_size = vision_config.image_size
584 # Result in the max possible feature size (h:w = 16:1)
585 return ImageSize(height=self.get_max_num_tiles() * image_size, width=image_size)
586
587
588 class Mllama4MultiModalProcessor(BaseMultiModalProcessor[Mllama4ProcessingInfo]):
589 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
590 return self.dummy_inputs.get_dummy_text(mm_counts)
591
592 def _postprocess_hf_mm_data(
593 self,
594 mm_data: Mapping[str, object],
595 hf_processor_mm_kwargs: Mapping[str, object],
596 processed_data: BatchFeature,
597 ) -> BatchFeature:
598 if not mm_data:
599 return processed_data
600
601 processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
602 image_processor = processor.image_processor
603 vision_config = self.info.get_hf_config().vision_config
604
605 if processed_data.get("pixel_values") is not None:
606 assert "images" in mm_data, (
607 "images expected to be in mm_data when pixel_values is present"
608 )
609
610 images = mm_data["images"]
611 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
612 parsed_images = mm_items.get_items("image", ImageProcessorItems)
613
614 tile_size = vision_config.image_size
615 possible_resolutions = find_supported_resolutions(
616 max_num_chunks=self.info.get_max_num_tiles(),
617 patch_size=SizeDict(height=tile_size, width=tile_size),
618 )
619 best_fit_sizes = []
620 for image in parsed_images:
621 assert image is not None
622 best_fit_sizes.append(
623 get_best_fit(
624 (image.size[1], image.size[0]),
625 torch.tensor(possible_resolutions),
626 resize_to_max_canvas=image_processor.resize_to_max_canvas,
627 )
628 )
629 # TODO tile height/width do not necessarily need to match
630 aspect_ratios = [
631 (image_size[0] // tile_size, image_size[1] // tile_size)
632 for image_size in best_fit_sizes
633 ]
634 patches_per_image = [
635 1 if r_h * r_w == 1 else 1 + r_h * r_w for (r_h, r_w) in aspect_ratios
636 ]
637
638 processed_data["aspect_ratios"] = torch.tensor(aspect_ratios)
639 processed_data["patches_per_image"] = torch.tensor(patches_per_image)
640
641 return processed_data
642
643 def _get_mm_fields_config(
644 self,
645 hf_inputs: BatchFeature,
646 hf_processor_mm_kwargs: Mapping[str, object],
647 ) -> Mapping[str, MultiModalFieldConfig]:
648 patches_per_image = hf_inputs.get("patches_per_image", torch.empty(0))
649 return dict(
650 pixel_values=MultiModalFieldConfig.flat_from_sizes(
651 "image", patches_per_image
652 ),
653 patches_per_image=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
654 aspect_ratios=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
655 )
656
657 def _get_prompt_updates(
658 self,
659 mm_items: MultiModalDataItems,
660 hf_processor_mm_kwargs: Mapping[str, object],
661 out_mm_kwargs: MultiModalKwargsItems,
662 ) -> list[PromptUpdate]:
663 config = self.info.get_hf_config()
664 vision_config = config.vision_config
665
666 num_patches_per_chunk = self.info.get_patch_per_chunk(vision_config)
667 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
668 img_patch_token = hf_processor.img_patch_token
669
670 tokenizer = self.info.get_tokenizer()
671 img_patch_token_ids = cached_encode(
672 tokenizer, img_patch_token, add_special_tokens=False
673 )
674
675 def get_replacement(item_idx: int):
676 out_item = out_mm_kwargs["image"][item_idx]
677 aspect_ratio = out_item["aspect_ratios"].data
678
679 repl = hf_processor._prompt_split_image(
680 aspect_ratio=aspect_ratio,
681 num_patches_per_chunk=num_patches_per_chunk,
682 )
683
684 repl_ids = cached_encode(tokenizer, repl, add_special_tokens=False)
685 return PromptUpdateDetails.select_token_ids(repl_ids, img_patch_token_ids)
686
687 return [
688 PromptReplacement(
689 modality="image",
690 target=[hf_processor.image_token_id],
691 replacement=get_replacement,
692 )
693 ]
694
695
696 class Mllama4DummyInputsBuilder(BaseDummyInputsBuilder[Mllama4ProcessingInfo]):
697 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
698 num_images = mm_counts.get("image", 0)
699
700 processor = self.info.get_hf_processor()
701 image_token = processor.fake_image_token
702
703 return image_token * num_images
704
705 def get_dummy_mm_data(
706 self,
707 seq_len: int,
708 mm_counts: Mapping[str, int],
709 mm_options: MultiModalDummyOptions,
710 ) -> MultiModalDataDict:
711 (target_width, target_height) = self.info.get_image_size_with_most_features()
712
713 return {
714 "image": self._get_dummy_images(
715 width=target_width,
716 height=target_height,
717 num_images=mm_counts.get("image", 0),
718 overrides=mm_options.get("image"),
719 )
720 }
721
722
723 @MULTIMODAL_REGISTRY.register_processor(
724 Mllama4MultiModalProcessor,
725 info=Mllama4ProcessingInfo,
726 dummy_inputs=Mllama4DummyInputsBuilder,
727 )
728 class Llama4ForConditionalGeneration(
729 nn.Module,
730 SupportsMultiModal,
731 SupportsPP,
732 MixtureOfExperts,
733 SupportsEncoderCudaGraph,
734 SupportsEagle3,
735 SupportsLoRA,
736 ):
737 packed_modules_mapping = {
738 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
739 "gate_up_proj": ["gate_proj", "up_proj"],
740 }
741
742 supports_encoder_tp_data = True
743 supports_tower_connector_lora = True
744
745 @classmethod
746 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
747 if modality.startswith("image"):
748 return "<|image|>"
749
750 raise ValueError("Only image modality is supported")
751
752 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
753 super().__init__()
754 config = vllm_config.model_config.hf_config
755 quant_config = vllm_config.quant_config
756 multimodal_config = vllm_config.model_config.multimodal_config
757 assert multimodal_config is not None
758 self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
759
760 self.vllm_config = vllm_config
761 self.config = config
762 self.quant_config = quant_config
763 self.multimodal_config = multimodal_config
764
765 with self._mark_tower_model(vllm_config, "image"):
766 with set_current_vllm_config(vllm_config):
767 self.vision_model = Llama4VisionModel(
768 config=config.vision_config,
769 quant_config=None,
770 prefix=maybe_prefix(prefix, "vision_model"),
771 )
772
773 self.multi_modal_projector = Llama4MultiModalProjector(
774 config=self.config,
775 quant_config=None,
776 prefix=maybe_prefix(prefix, "multi_modal_projector"),
777 )
778
779 with self._mark_language_model(vllm_config):
780 self.language_model = initialize_model(
781 vllm_config=vllm_config.with_hf_config(
782 config.text_config, ["LlamaForCausalLM"]
783 ),
784 prefix=maybe_prefix(prefix, "language_model"),
785 model_class=Llama4ForCausalLM,
786 )
787
788 self.make_empty_intermediate_tensors = (
789 self.language_model.make_empty_intermediate_tensors
790 )
791
792 # Set MoE hyperparameters
793 self.num_expert_groups = 1
794 self.num_logical_experts = self.language_model.num_logical_experts
795 self.num_physical_experts = self.language_model.num_physical_experts
796 self.num_local_physical_experts = self.language_model.num_local_physical_experts
797 self.num_routed_experts = self.language_model.num_routed_experts
798 self.num_shared_experts = self.language_model.num_shared_experts
799 self.num_redundant_experts = self.language_model.num_redundant_experts
800 self.moe_layers = self.language_model.moe_layers
801 self.num_moe_layers = len(self.moe_layers)
802
803 def set_aux_hidden_state_layers(self, layers: tuple[int, ...]) -> None:
804 # Delegate to underlying language model (Llama4ForCausalLM)
805 assert hasattr(self.language_model, "set_aux_hidden_state_layers")
806 self.language_model.set_aux_hidden_state_layers(layers)
807
808 def get_eagle3_default_aux_hidden_state_layers(self) -> tuple[int, ...]:
809 # Delegate to underlying language model (Llama4ForCausalLM)
810 assert hasattr(
811 self.language_model, "get_eagle3_default_aux_hidden_state_layers"
812 )
813 return self.language_model.get_eagle3_default_aux_hidden_state_layers()
814
815 def update_physical_experts_metadata(
816 self, num_physical_experts: int, num_local_physical_experts: int
817 ):
818 self.language_model.update_physical_experts_metadata(
819 num_physical_experts, num_local_physical_experts
820 )
821
822 def get_image_patches_per_chunk(self) -> int:
823 return Mllama4ProcessingInfo.get_patch_per_chunk(self.config.vision_config)
824
825 def encode_image_chunks(
826 self,
827 pixel_values: torch.Tensor,
828 *,
829 use_data_parallel: bool,
830 ) -> torch.Tensor:
831 if use_data_parallel:
832 vision_embeddings = run_dp_sharded_vision_model(
833 pixel_values, self.vision_model
834 )
835 else:
836 vision_embeddings = self.vision_model(pixel_values)
837
838 return self.multi_modal_projector(vision_embeddings)
839
840 def get_encoder_cudagraph_config(self):
841 from vllm.v1.worker.encoder_cudagraph_defs import (
842 EncoderCudaGraphConfig,
843 )
844
845 return EncoderCudaGraphConfig(
846 modalities=["image"],
847 buffer_keys=["pixel_values"],
848 out_hidden_size=self.config.text_config.hidden_size,
849 )
850
851 def get_input_modality(
852 self,
853 mm_kwargs: dict[str, Any],
854 ) -> str:
855 return "image"
856
857 def get_encoder_cudagraph_budget_range(
858 self,
859 vllm_config: VllmConfig,
860 ) -> tuple[int, int]:
861 min_budget = self.get_image_patches_per_chunk()
862 max_budget = min(
863 vllm_config.scheduler_config.max_num_batched_tokens,
864 self.vllm_config.model_config.max_model_len,
865 )
866 return (min_budget, max_budget)
867
868 def get_encoder_cudagraph_item_specs(
869 self,
870 mm_kwargs: dict[str, Any],
871 ):
872 from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
873
874 patches_per_chunk = self.get_image_patches_per_chunk()
875 return [
876 EncoderItemSpec(
877 input_size=num_chunks,
878 output_tokens=num_chunks * patches_per_chunk,
879 )
880 for num_chunks in mm_kwargs["patches_per_image"].tolist()
881 ]
882
883 def select_encoder_cudagraph_items(
884 self,
885 mm_kwargs: dict[str, Any],
886 indices: list[int],
887 ) -> dict[str, Any]:
888 pixel_values = mm_kwargs["pixel_values"]
889 patches_per_image = mm_kwargs["patches_per_image"]
890
891 if len(indices) == 0:
892 return {
893 "pixel_values": pixel_values[:0],
894 "patches_per_image": patches_per_image[:0],
895 }
896
897 cum_chunks = [0]
898 for num_chunks in patches_per_image.tolist():
899 cum_chunks.append(cum_chunks[-1] + num_chunks)
900
901 selected_pixel_values = torch.cat(
902 [pixel_values[cum_chunks[i] : cum_chunks[i + 1]] for i in indices],
903 dim=0,
904 )
905
906 return {
907 "pixel_values": selected_pixel_values,
908 "patches_per_image": patches_per_image[indices],
909 }
910
911 def prepare_encoder_cudagraph_capture_inputs(
912 self,
913 token_budget: int,
914 max_batch_size: int,
915 max_frames_per_batch: int,
916 device: torch.device,
917 dtype: torch.dtype,
918 path: str = "default",
919 axis_keys: tuple[Hashable, ...] | None = None,
920 ):
921 from vllm.v1.worker.encoder_cudagraph_defs import (
922 EncoderCudaGraphCaptureInputs,
923 )
924
925 vision_config = self.config.vision_config
926 patches_per_chunk = self.get_image_patches_per_chunk()
927 chunks_per_capture = max(
928 1, (token_budget + patches_per_chunk - 1) // patches_per_chunk
929 )
930 dummy_pixel_values = torch.randn(
931 chunks_per_capture,
932 vision_config.num_channels,
933 vision_config.image_size,
934 vision_config.image_size,
935 device=device,
936 dtype=dtype,
937 )
938
939 return EncoderCudaGraphCaptureInputs(
940 values={"pixel_values": dummy_pixel_values},
941 )
942
943 def prepare_encoder_cudagraph_replay_buffers(
944 self,
945 mm_kwargs: dict[str, Any],
946 max_batch_size: int,
947 max_frames_per_batch: int,
948 path: str = "default",
949 ):
950 from vllm.v1.worker.encoder_cudagraph_defs import (
951 EncoderCudaGraphReplayBuffers,
952 )
953
954 return EncoderCudaGraphReplayBuffers(
955 values={"pixel_values": mm_kwargs["pixel_values"]},
956 )
957
958 def encoder_cudagraph_forward(
959 self,
960 inputs: dict[str, torch.Tensor],
961 path: str = "default",
962 ) -> torch.Tensor:
963 return self.encode_image_chunks(
964 inputs["pixel_values"],
965 use_data_parallel=False,
966 ).flatten(0, 1)
967
968 def encoder_eager_forward(
969 self,
970 mm_kwargs: dict[str, Any],
971 path: str = "default",
972 ) -> torch.Tensor:
973 return self.encode_image_chunks(
974 mm_kwargs["pixel_values"],
975 use_data_parallel=False,
976 ).flatten(0, 1)
977
978 def _parse_and_validate_image_input(
979 self, **kwargs: object
980 ) -> Llama4ImagePatchInputs | None:
981 # total_num_chunks, channel, image_size, image_size
982 pixel_values = kwargs.pop("pixel_values", None)
983 if pixel_values is None:
984 return None
985
986 patches_per_image = kwargs.pop("patches_per_image")
987 aspect_ratios = kwargs.pop("aspect_ratios")
988
989 return Llama4ImagePatchInputs(
990 type="pixel_values",
991 pixel_values=pixel_values,
992 patches_per_image=patches_per_image,
993 aspect_ratios=aspect_ratios,
994 )
995
996 def _process_image_input(
997 self, image_input: Llama4ImagePatchInputs
998 ) -> MultiModalEmbeddings:
999 assert self.vision_model and self.multi_modal_projector
1000 pixel_values = image_input["pixel_values"]
1001 patches_per_image = image_input["patches_per_image"].tolist()
1002
1003 vision_embeddings_flat = self.encode_image_chunks(
1004 pixel_values,
1005 use_data_parallel=self.use_data_parallel,
1006 )
1007
1008 return [
1009 img.flatten(0, 1)
1010 for img in vision_embeddings_flat.split(patches_per_image, dim=0)
1011 ]
1012
1013 def embed_multimodal(self, **kwargs) -> MultiModalEmbeddings:
1014 image_input = self._parse_and_validate_image_input(**kwargs)
1015 if image_input is None:
1016 return []
1017
1018 return self._process_image_input(image_input)
1019
1020 def forward(
1021 self,
1022 input_ids: torch.Tensor | None,
1023 positions: torch.Tensor,
1024 intermediate_tensors: IntermediateTensors | None = None,
1025 inputs_embeds: torch.Tensor | None = None,
1026 **kwargs: object,
1027 ) -> torch.Tensor | IntermediateTensors:
1028 if intermediate_tensors is not None:
1029 inputs_embeds = None
1030
1031 return self.language_model(
1032 input_ids, positions, intermediate_tensors, inputs_embeds
1033 )
1034
1035 def compute_logits(
1036 self,
1037 hidden_states: torch.Tensor,
1038 ) -> torch.Tensor | None:
1039 return self.language_model.compute_logits(hidden_states)
1040
1041 def _consolidate_qkv_weights(
1042 self, weights: Iterable[tuple[str, torch.Tensor]]
1043 ) -> Iterable[tuple[str, torch.Tensor]]:
1044 qkv_idx_mappings = {
1045 ".self_attn.q_proj": 0,
1046 ".self_attn.k_proj": 1,
1047 ".self_attn.v_proj": 2,
1048 }
1049 qkv_weights = {}
1050 for name, loaded_weight in weights:
1051 for weight_name, idx in qkv_idx_mappings.items():
1052 if weight_name not in name:
1053 continue
1054 new_name = name.replace(weight_name, ".self_attn.qkv_proj")
1055 if new_name not in qkv_weights:
1056 qkv_weights[new_name] = [None] * 3
1057 qkv_weights[new_name][idx] = loaded_weight
1058 break
1059 else:
1060 yield name, loaded_weight
1061 for key, weight in qkv_weights.items():
1062 qkv_weight = torch.cat(weight, dim=0)
1063 yield key, qkv_weight
1064
1065 def _rename_weight_for_modelopt_checkpoint(self, name: str) -> str:
1066 """Rename weights from ModelOpt llama4 fp8 checkpoints to vLLM
1067 format."""
1068 if name.startswith("model.") or name.startswith("language_model.model."):
1069 renamed = (
1070 name.replace("model.", "language_model.model.", 1)
1071 if name.startswith("model.")
1072 else name
1073 )
1074 # Handle expert scale parameters with flat naming
1075 if "feed_forward.experts." in name and (
1076 "_input_scale" in name or "_weight_scale" in name
1077 ):
1078 # Map checkpoint naming to vLLM's expected naming
1079 if "down_proj_input_scale" in renamed:
1080 return renamed.replace("down_proj_input_scale", "w2_input_scale")
1081 elif "down_proj_weight_scale" in renamed:
1082 return renamed.replace("down_proj_weight_scale", "w2_weight_scale")
1083 elif "gate_up_proj_input_scale" in renamed:
1084 return renamed.replace(
1085 "gate_up_proj_input_scale", "w13_input_scale"
1086 )
1087 elif "gate_up_proj_weight_scale" in renamed:
1088 return renamed.replace(
1089 "gate_up_proj_weight_scale", "w13_weight_scale"
1090 )
1091 return renamed
1092
1093 # Handle attention scale parameters
1094 elif "self_attn." in name and (".k_scale" in name or ".v_scale" in name):
1095 if ".k_proj.k_scale" in renamed:
1096 return renamed.replace(".k_proj.k_scale", ".attn.k_scale")
1097 elif ".v_proj.v_scale" in renamed:
1098 return renamed.replace(".v_proj.v_scale", ".attn.v_scale")
1099 return renamed
1100
1101 # Standard model.* to language_model.model.* renaming
1102 return renamed
1103
1104 elif name.startswith("lm_head.weight"):
1105 return name.replace("lm_head.weight", "language_model.lm_head.weight")
1106
1107 return name
1108
1109 def _load_other_weights(
1110 self,
1111 other_weights: Iterable[tuple[str, torch.Tensor]],
1112 params_dict: dict,
1113 stacked_params_mapping: list,
1114 ) -> set[str]:
1115 """Load non-language-model weights with stacking support."""
1116 updated_params = set()
1117
1118 if self.use_data_parallel:
1119 other_weights = self._consolidate_qkv_weights(other_weights)
1120
1121 for name, loaded_weight in other_weights:
1122 # Try stacked parameter mapping first
1123 for param_name, weight_name, shard_id in stacked_params_mapping:
1124 if weight_name not in name or self.use_data_parallel:
1125 continue
1126 name = name.replace(weight_name, param_name)
1127 param = params_dict[name]
1128 updated_params.add(name)
1129 weight_loader = param.weight_loader
1130 weight_loader(param, loaded_weight, shard_id)
1131 break
1132 else:
1133 # Use regular weight loading
1134 param = params_dict[name]
1135 weight_loader = getattr(param, "weight_loader", default_weight_loader)
1136 weight_loader(param, loaded_weight)
1137 updated_params.add(name)
1138
1139 return updated_params
1140
1141 def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
1142 # Params for weights, fp8 weight scales, fp8 activation scales
1143 # (param_name, weight_name, expert_id, shard_id)
1144 return fused_moe_make_expert_params_mapping(
1145 self,
1146 ckpt_gate_proj_name="gate_proj",
1147 ckpt_down_proj_name="down_proj",
1148 ckpt_up_proj_name="up_proj",
1149 num_experts=self.config.text_config.num_local_experts,
1150 num_redundant_experts=self.num_redundant_experts,
1151 )
1152
1153 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1154 stacked_params_mapping = [
1155 # (param_name, shard_name, shard_id)
1156 (".self_attn.qkv_proj", ".self_attn.q_proj", "q"),
1157 (".self_attn.qkv_proj", ".self_attn.k_proj", "k"),
1158 (".self_attn.qkv_proj", ".self_attn.v_proj", "v"),
1159 # Shared expert gate_up_proj stacking
1160 (".shared_expert.gate_up_proj", ".shared_expert.gate_proj", 0),
1161 (".shared_expert.gate_up_proj", ".shared_expert.up_proj", 1),
1162 # Feed forward gate_up_proj stacking (for non-MoE layers if any)
1163 (".feed_forward.gate_up_proj", ".feed_forward.gate_proj", 0),
1164 (".feed_forward.gate_up_proj", ".feed_forward.up_proj", 1),
1165 ]
1166 params_dict = dict(self.named_parameters())
1167 updated_params: set[str] = set()
1168
1169 # Stream thelanguage-model weights straight into
1170 # AutoWeightsLoader so each tensor is loaded and released as we iterate,
1171 # instead of materializing the whole checkpoint in host memory first.
1172 # Only the small vision/projector and scalar expert-scale groups are
1173 # buffered.
1174 other_weights: list[tuple[str, torch.Tensor]] = []
1175 expert_scale_weights: list[tuple[str, torch.Tensor]] = []
1176
1177 def regular_language_model_weights() -> Iterable[tuple[str, torch.Tensor]]:
1178 """Rename weights and separate them into language_model and other
1179 weights.
1180
1181 Yields the (large) language_model weights for streaming; the small
1182 groups (vision/projector and scalar expert scales) are buffered into
1183 the lists above.
1184 """
1185 for name, weight in weights:
1186 renamed = self._rename_weight_for_modelopt_checkpoint(name)
1187
1188 attr = renamed.split(".", 1)[0]
1189 if isinstance(getattr(self, attr), StageMissingLayer):
1190 continue
1191
1192 if not renamed.startswith("language_model."):
1193 other_weights.append((renamed, weight))
1194 continue
1195
1196 # Handle expert scale parameters that need broadcasting.
1197 # ModelOpt checkpoints use a single value tensor scalar for BMM
1198 # style experts, vLLM expects the scale to be broadcasted across
1199 # all experts.
1200 if (
1201 "feed_forward.experts." in renamed
1202 and "scale" in renamed
1203 and ".shared_expert" not in renamed
1204 ):
1205 renamed = maybe_remap_moe_expert_param_name(renamed, params_dict)
1206 if renamed in params_dict:
1207 param = params_dict[renamed]
1208 if (
1209 hasattr(param, "data")
1210 and param.data.numel() > 1
1211 and weight.numel() == 1
1212 ):
1213 # Broadcast single value to all experts
1214 param.data.fill_(weight.item())
1215 updated_params.add(renamed)
1216 continue
1217
1218 expert_scale_weights.append((renamed, weight))
1219 continue
1220
1221 yield renamed, weight
1222
1223 loader = AutoWeightsLoader(self)
1224 # AutoWeightsLoader consumes its input lazily and runs to exhaustion,
1225 # so other_weights / expert_scale_weights are fully populated as a side
1226 # effect by the time this returns.
1227 loaded_language_model_params = loader.load_weights(
1228 regular_language_model_weights()
1229 )
1230 assert loaded_language_model_params is not None
1231 updated_params.update(loaded_language_model_params)
1232
1233 if expert_scale_weights:
1234 loaded_expert_scale_params = loader.load_weights(expert_scale_weights)
1235 if loaded_expert_scale_params:
1236 updated_params.update(loaded_expert_scale_params)
1237
1238 updated_params.update(
1239 self._load_other_weights(other_weights, params_dict, stacked_params_mapping)
1240 )
1241
1242 return updated_params
1243
1244 def get_mm_mapping(self) -> MultiModelKeys:
1245 """Get the module prefix in multimodal models."""
1246 return MultiModelKeys.from_string_field(
1247 language_model="language_model",
1248 connector=[
1249 "multi_modal_projector.",
1250 "vision_model.vision_adapter.",
1251 ],
1252 tower_model="vision_model.",
1253 )
1254
1255 def get_mm_lora_token_counts(
1256 self,
1257 *,
1258 modality: str,
1259 mm_kwargs: MultiModalKwargsItem | None,
1260 num_mm_embeds: int,
1261 ) -> tuple[int, int | None]:
1262 del modality, mm_kwargs
1263 vision_config = self.config.vision_config
1264 patches_per_chunk = Mllama4ProcessingInfo.get_patch_per_chunk(vision_config)
1265 if num_mm_embeds <= 0 or patches_per_chunk <= 0:
1266 return 0, 0
1267 raw_patches = (vision_config.image_size // vision_config.patch_size) ** 2
1268 num_chunks = num_mm_embeds // patches_per_chunk
1269 return num_chunks * (raw_patches + 1), num_chunks * patches_per_chunk