main
py 1,292 lines 45.8 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import itertools
5 import math
6 import typing
7 from collections.abc import Hashable, Iterable, Mapping, Sequence
8 from typing import Annotated, Any, Literal
9
10 import torch
11 import torch.nn as nn
12 from transformers import BatchFeature
13 from transformers.activations import ACT2FN
14 from transformers.models.lfm2_vl import Lfm2VlProcessor
15 from transformers.models.lfm2_vl.configuration_lfm2_vl import Lfm2VlConfig
16 from transformers.models.lfm2_vl.image_processing_lfm2_vl import (
17 Lfm2VlImageProcessor,
18 find_closest_aspect_ratio,
19 round_by_factor,
20 )
21 from typing_extensions import Buffer
22
23 from vllm.config import VllmConfig
24 from vllm.config.multimodal import MultiModalDummyOptions
25 from vllm.forward_context import set_forward_context
26 from vllm.inputs import MultiModalDataDict
27 from vllm.model_executor.layers.linear import ReplicatedLinear
28 from vllm.model_executor.layers.mamba.mamba_utils import (
29 MambaStateCopyFunc,
30 MambaStateCopyFuncCalculator,
31 MambaStateDtypeCalculator,
32 MambaStateShapeCalculator,
33 )
34 from vllm.model_executor.models.module_mapping import MultiModelKeys
35 from vllm.multimodal import MULTIMODAL_REGISTRY
36 from vllm.multimodal.inputs import (
37 MultiModalFieldConfig,
38 MultiModalKwargsItem,
39 MultiModalKwargsItems,
40 )
41 from vllm.multimodal.parse import ImageProcessorItems, ImageSize, MultiModalDataItems
42 from vllm.multimodal.processing import (
43 BaseDummyInputsBuilder,
44 BaseMultiModalProcessor,
45 BaseProcessingInfo,
46 PromptReplacement,
47 PromptUpdateDetails,
48 cached_encode,
49 )
50 from vllm.renderers import TokenizeParams
51 from vllm.sequence import IntermediateTensors
52 from vllm.utils.tensor_schema import TensorSchema, TensorShape
53
54 from .interfaces import (
55 IsHybrid,
56 MultiModalEmbeddings,
57 SupportsEncoderCudaGraph,
58 SupportsLoRA,
59 SupportsMultiModal,
60 SupportsPP,
61 )
62 from .lfm2_siglip2 import Siglip2Model
63 from .utils import (
64 AutoWeightsLoader,
65 WeightsMapper,
66 init_vllm_registered_model,
67 maybe_prefix,
68 )
69 from .vision import is_vit_use_data_parallel
70
71
72 def _pad_cumulative_seqlens_buffer(
73 dst: torch.Tensor,
74 src: torch.Tensor,
75 ) -> None:
76 n = src.shape[0]
77 dst.zero_()
78 dst[:n].copy_(src)
79 if n < dst.shape[0]:
80 dst[n:] = src[-1]
81
82
83 class Lfm2VLImagePixelInputs(TensorSchema):
84 """Dimensions:
85 - b: Number of images in the prompt
86 - bn: Batch size * number of images
87 - d: Number of dimensions
88 - fd: Number of features per dimension
89 """
90
91 type: Literal["pixel_values"] = "pixel_values"
92 pixel_values: Annotated[torch.Tensor, TensorShape("bn", "d", "fd")]
93 spatial_shapes: Annotated[torch.Tensor, TensorShape("bn", 2)]
94 num_patches: Annotated[torch.Tensor, TensorShape("b")]
95
96
97 LFM2VLImageInputs = Lfm2VLImagePixelInputs
98
99
100 class Lfm2VLProcessingInfo(BaseProcessingInfo):
101 def get_hf_config(self):
102 return self.ctx.get_hf_config(Lfm2VlConfig)
103
104 def get_hf_processor(self, **kwargs):
105 return self.ctx.get_hf_processor(Lfm2VlProcessor, **kwargs)
106
107 def get_image_processor(self, **kwargs: object) -> Lfm2VlImageProcessor:
108 return self.get_hf_processor(**kwargs).image_processor
109
110 def get_default_tok_params(self) -> TokenizeParams:
111 return super().get_default_tok_params().with_kwargs(add_special_tokens=False)
112
113 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
114 return {"image": None}
115
116 def get_image_size_with_most_features(self) -> ImageSize:
117 processor = self.get_image_processor()
118 max_image_tokens = processor.max_image_tokens
119 encoder_patch_size = processor.encoder_patch_size
120 downsample_factor = processor.downsample_factor
121 max_pixels = max_image_tokens * (encoder_patch_size**2) * (downsample_factor**2)
122 side = int(math.sqrt(max_pixels))
123 return ImageSize(width=side, height=side)
124
125 def _is_image_too_large(
126 self,
127 height: int,
128 width: int,
129 max_image_tokens: int,
130 encoder_patch_size: int,
131 downsample_factor: int,
132 max_pixels_tolerance: float,
133 ) -> bool:
134 """Check if the image is too large to be processed as one tile."""
135 total_factor = encoder_patch_size * downsample_factor
136
137 h_bar = max(encoder_patch_size, round_by_factor(height, total_factor))
138 w_bar = max(encoder_patch_size, round_by_factor(width, total_factor))
139 return (
140 h_bar * w_bar
141 > max_image_tokens
142 * encoder_patch_size**2
143 * downsample_factor**2
144 * max_pixels_tolerance
145 )
146
147 def smart_resize(
148 self,
149 height: int,
150 width: int,
151 downsample_factor: int,
152 min_image_tokens: int,
153 max_image_tokens: int,
154 encoder_patch_size: int,
155 ) -> tuple[int, int]:
156 total_factor = encoder_patch_size * downsample_factor
157 smart_resize_min_pixels = (
158 min_image_tokens * encoder_patch_size**2 * downsample_factor**2
159 )
160 smart_resize_max_pixels = (
161 max_image_tokens * encoder_patch_size**2 * downsample_factor**2
162 )
163
164 h_bar = max(total_factor, round_by_factor(height, total_factor))
165 w_bar = max(total_factor, round_by_factor(width, total_factor))
166
167 if h_bar * w_bar > smart_resize_max_pixels:
168 beta = math.sqrt((height * width) / smart_resize_max_pixels)
169 h_bar = max(
170 total_factor, math.floor(height / beta / total_factor) * total_factor
171 )
172 w_bar = max(
173 total_factor, math.floor(width / beta / total_factor) * total_factor
174 )
175 elif h_bar * w_bar < smart_resize_min_pixels:
176 beta = math.sqrt(smart_resize_min_pixels / (height * width))
177 h_bar = math.ceil(height * beta / total_factor) * total_factor
178 w_bar = math.ceil(width * beta / total_factor) * total_factor
179
180 return w_bar, h_bar
181
182 def _target_ratios(self, min_tiles: int, max_tiles: int) -> list[tuple[int, int]]:
183 ratios = [
184 (w, h)
185 for n in range(min_tiles, max_tiles + 1)
186 for w in range(1, n + 1)
187 for h in range(1, n + 1)
188 if min_tiles <= w * h <= max_tiles
189 ]
190 return sorted(set(ratios), key=lambda x: x[0] * x[1])
191
192 def _get_grid_layout(
193 self,
194 height: int,
195 width: int,
196 min_tiles: int,
197 max_tiles: int,
198 tile_size: int,
199 ) -> tuple[int, int, int]:
200 aspect_ratio = width / height
201 target_ratios = self._target_ratios(min_tiles, max_tiles)
202 # find best matching grid configuration
203 grid_width, grid_height = find_closest_aspect_ratio(
204 aspect_ratio, target_ratios, width, height, tile_size
205 )
206 total_patches = grid_width * grid_height
207 return grid_width, grid_height, total_patches
208
209 def _get_image_feature_grid_size(
210 self,
211 image_width: int,
212 image_height: int,
213 processor: Lfm2VlProcessor,
214 mm_kwargs: Mapping[str, object],
215 ) -> tuple[int, int, int]:
216 image_processor: Lfm2VlImageProcessor = processor.image_processor
217
218 mm_kwargs = self.ctx.get_merged_mm_kwargs(mm_kwargs)
219 downsample_factor = mm_kwargs.get(
220 "downsample_factor", image_processor.downsample_factor
221 )
222 encoder_patch_size = mm_kwargs.get(
223 "encoder_patch_size", image_processor.encoder_patch_size
224 )
225 max_pixels_tolerance = mm_kwargs.get(
226 "max_pixels_tolerance", image_processor.max_pixels_tolerance
227 )
228 min_tiles = mm_kwargs.get("min_tiles", image_processor.min_tiles)
229 max_tiles = mm_kwargs.get("max_tiles", image_processor.max_tiles)
230 max_image_tokens = mm_kwargs.get(
231 "max_image_tokens", image_processor.max_image_tokens
232 )
233 tile_size = mm_kwargs.get("tile_size", image_processor.tile_size)
234 assert isinstance(downsample_factor, int)
235 assert isinstance(encoder_patch_size, int)
236 assert isinstance(max_pixels_tolerance, int | float)
237 assert isinstance(min_tiles, int)
238 assert isinstance(max_tiles, int)
239 assert isinstance(max_image_tokens, int)
240 assert isinstance(tile_size, int)
241
242 do_image_splitting = not min_tiles == max_tiles == 1
243 is_image_large = self._is_image_too_large(
244 height=image_height,
245 width=image_width,
246 max_image_tokens=max_image_tokens,
247 encoder_patch_size=encoder_patch_size,
248 downsample_factor=downsample_factor,
249 max_pixels_tolerance=max_pixels_tolerance,
250 )
251
252 # Big image will be cropped into patches and small images are just resized
253 if is_image_large and do_image_splitting:
254 grid_width, grid_height, total_patches = self._get_grid_layout(
255 image_height,
256 image_width,
257 min_tiles=min_tiles,
258 max_tiles=max_tiles,
259 tile_size=tile_size,
260 )
261 else:
262 grid_width = grid_height = total_patches = 1
263
264 if grid_width * grid_height != 1: # Thumbnail
265 total_patches += 1
266
267 return grid_width, grid_height, total_patches
268
269 def get_num_patches(
270 self,
271 *,
272 image_width: int,
273 image_height: int,
274 processor: Lfm2VlProcessor,
275 mm_kwargs: Mapping[str, object],
276 ) -> int:
277 _, _, total_patches = self._get_image_feature_grid_size(
278 image_width=image_width,
279 image_height=image_height,
280 processor=processor,
281 mm_kwargs=mm_kwargs,
282 )
283 return total_patches
284
285 def get_image_repl(
286 self,
287 image_width: int,
288 image_height: int,
289 spatial_shapes: torch.Tensor,
290 processor: Lfm2VlProcessor,
291 mm_kwargs: Mapping[str, object],
292 ) -> str:
293 grid_placeholder = "<|img_row_{n_h}_col_{n_w}|>"
294 image_token = processor.image_token
295 image_start_token = processor.image_start_token
296 image_end_token = processor.image_end_token
297 image_thumbnail_token = processor.image_thumbnail_token
298
299 num_thumbnail_tokens, num_tokens_per_tile = self.get_num_image_tokens(
300 spatial_shapes=spatial_shapes,
301 processor=processor,
302 mm_kwargs=mm_kwargs,
303 )
304 tile_img_placeholder = grid_placeholder + (image_token * num_tokens_per_tile)
305
306 grid_w, grid_h, _ = self._get_image_feature_grid_size(
307 image_width=image_width,
308 image_height=image_height,
309 processor=processor,
310 mm_kwargs=mm_kwargs,
311 )
312
313 if grid_w > 1 or grid_h > 1:
314 tiles_placeholder: list[str] = [
315 tile_img_placeholder.format(n_h=i + 1, n_w=j + 1)
316 for i in range(grid_h)
317 for j in range(grid_w)
318 ]
319
320 if num_thumbnail_tokens > 0:
321 tiles_placeholder.append(
322 image_thumbnail_token + (image_token * num_thumbnail_tokens)
323 )
324 else:
325 tiles_placeholder = [image_token * num_thumbnail_tokens]
326
327 placeholder = "".join(
328 itertools.chain([image_start_token], tiles_placeholder, [image_end_token])
329 )
330 return placeholder
331
332 def get_num_image_tokens(
333 self,
334 *,
335 spatial_shapes: torch.Tensor,
336 processor: Lfm2VlProcessor,
337 mm_kwargs: Mapping[str, object],
338 ) -> tuple[int, int]:
339 image_processor: Lfm2VlImageProcessor = processor.image_processor
340
341 mm_kwargs = self.ctx.get_merged_mm_kwargs(mm_kwargs)
342 downsample_factor = mm_kwargs.get(
343 "downsample_factor", image_processor.downsample_factor
344 )
345 encoder_patch_size = mm_kwargs.get(
346 "encoder_patch_size", image_processor.encoder_patch_size
347 )
348 tile_size = mm_kwargs.get("tile_size", image_processor.tile_size)
349 assert isinstance(downsample_factor, int)
350 assert isinstance(encoder_patch_size, int)
351 assert isinstance(tile_size, int)
352
353 thumbnail_height_patches = int(spatial_shapes[-1][0].item())
354 thumbnail_width_patches = int(spatial_shapes[-1][1].item())
355 # HF computes thumbnail tokens as
356 # ceil(h_patches / downsample_factor) * ceil(w_patches / downsample_factor).
357 # We assert divisibility here so any processor/model drift is surfaced
358 # immediately instead of being hidden by floor division.
359 assert thumbnail_height_patches % downsample_factor == 0, (
360 "LFM2-VL thumbnail height patch grid must be divisible by "
361 f"downsample_factor, got height_patches={thumbnail_height_patches}, "
362 f"downsample_factor={downsample_factor}"
363 )
364 assert thumbnail_width_patches % downsample_factor == 0, (
365 "LFM2-VL thumbnail width patch grid must be divisible by "
366 f"downsample_factor, got width_patches={thumbnail_width_patches}, "
367 f"downsample_factor={downsample_factor}"
368 )
369 num_thumbnail_tokens = math.ceil(
370 thumbnail_height_patches / downsample_factor
371 ) * math.ceil(thumbnail_width_patches / downsample_factor)
372 num_patches_tile = tile_size // encoder_patch_size
373 dwn_num_patches_tile = math.ceil(num_patches_tile / downsample_factor)
374 num_tiles_tokens = dwn_num_patches_tile * dwn_num_patches_tile
375
376 return num_thumbnail_tokens, num_tiles_tokens
377
378
379 class Lfm2VLDummyInputsBuilder(BaseDummyInputsBuilder[Lfm2VLProcessingInfo]):
380 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
381 num_images = mm_counts.get("image", 0)
382 processor = self.info.get_hf_processor()
383 image_token = processor.image_token
384 return image_token * num_images
385
386 def get_dummy_mm_data(
387 self,
388 seq_len: int,
389 mm_counts: Mapping[str, int],
390 mm_options: MultiModalDummyOptions,
391 ) -> MultiModalDataDict:
392 target_width, target_height = self.info.get_image_size_with_most_features()
393
394 return {
395 "image": self._get_dummy_images(
396 width=target_width,
397 height=target_height,
398 num_images=mm_counts.get("image", 0),
399 overrides=mm_options.get("image"),
400 ),
401 }
402
403
404 class Lfm2VLMultiModalProcessor(BaseMultiModalProcessor[Lfm2VLProcessingInfo]):
405 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
406 return self.dummy_inputs.get_dummy_text(mm_counts)
407
408 def _postprocess_hf_mm_data(
409 self,
410 mm_data: Mapping[str, object],
411 hf_processor_mm_kwargs: Mapping[str, object],
412 processed_data: BatchFeature,
413 ) -> BatchFeature:
414 if not mm_data:
415 return processed_data
416
417 images = mm_data.get("images", [])
418 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
419 parsed_images = mm_items.get_items("image", ImageProcessorItems)
420 image_sizes = [
421 parsed_images.get_image_size(i) for i in range(len(parsed_images))
422 ]
423 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
424
425 num_patches = [
426 self.info.get_num_patches(
427 image_width=size.width,
428 image_height=size.height,
429 processor=hf_processor,
430 mm_kwargs=hf_processor_mm_kwargs,
431 )
432 for size in image_sizes
433 ]
434 processed_data["num_patches"] = torch.tensor(num_patches)
435
436 return processed_data
437
438 def _get_mm_fields_config(
439 self,
440 hf_inputs: BatchFeature,
441 hf_processor_mm_kwargs: Mapping[str, object],
442 ) -> Mapping[str, MultiModalFieldConfig]:
443 num_patches = hf_inputs.get("num_patches", torch.empty(0))
444
445 return dict[str, MultiModalFieldConfig](
446 pixel_values=MultiModalFieldConfig.flat_from_sizes("image", num_patches),
447 spatial_shapes=MultiModalFieldConfig.flat_from_sizes(
448 "image", num_patches, keep_on_cpu=True
449 ),
450 num_patches=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
451 )
452
453 def _get_prompt_updates(
454 self,
455 mm_items: MultiModalDataItems,
456 hf_processor_mm_kwargs: Mapping[str, object],
457 out_mm_kwargs: MultiModalKwargsItems,
458 ) -> Sequence[PromptReplacement]:
459 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
460 image_token_id = hf_processor.image_token_id
461 tokenizer = self.info.get_tokenizer()
462
463 def get_image_replacement_lfm2vl(item_idx: int):
464 images = mm_items.get_items("image", ImageProcessorItems)
465 image_size = images.get_image_size(item_idx)
466 out_item = out_mm_kwargs["image"][item_idx]
467 spatial_shapes = out_item["spatial_shapes"].data
468 assert isinstance(spatial_shapes, torch.Tensor)
469 image_repl = self.info.get_image_repl(
470 image_width=image_size.width,
471 image_height=image_size.height,
472 spatial_shapes=spatial_shapes,
473 processor=hf_processor,
474 mm_kwargs=hf_processor_mm_kwargs,
475 )
476 image_repl_ids = cached_encode(
477 tokenizer, image_repl, add_special_tokens=False
478 )
479 return PromptUpdateDetails.select_token_id(
480 image_repl_ids,
481 image_token_id,
482 )
483
484 return [
485 PromptReplacement(
486 modality="image",
487 target=[image_token_id],
488 replacement=get_image_replacement_lfm2vl,
489 )
490 ]
491
492
493 class Lfm2VLMultiModalProjector(nn.Module):
494 def __init__(
495 self,
496 config: Lfm2VlConfig,
497 prefix: str = "",
498 ):
499 super().__init__()
500 self.use_data_parallel = is_vit_use_data_parallel()
501
502 in_channels = config.vision_config.hidden_size * (config.downsample_factor**2)
503 self.factor = config.downsample_factor
504 self.projector_use_layernorm = config.projector_use_layernorm
505 if self.projector_use_layernorm:
506 self.layer_norm = nn.LayerNorm(in_channels)
507 self.linear_1 = ReplicatedLinear(
508 in_channels,
509 config.projector_hidden_size,
510 bias=config.projector_bias,
511 prefix=maybe_prefix(prefix, "linear_1"),
512 return_bias=False,
513 )
514 self.act = ACT2FN[config.projector_hidden_act]
515 self.linear_2 = ReplicatedLinear(
516 config.projector_hidden_size,
517 config.text_config.hidden_size,
518 bias=config.projector_bias,
519 prefix=maybe_prefix(prefix, "linear_2"),
520 return_bias=False,
521 )
522
523 def forward(
524 self,
525 vision_features_packed: torch.Tensor,
526 spatial_shapes: torch.Tensor,
527 ) -> torch.Tensor:
528 """Project packed vision features without materializing padded tensors.
529
530 Args:
531 vision_features_packed: (total_tokens, hidden_size) packed in tile order.
532 spatial_shapes: (num_tiles, 2) on CPU (height, width) per tile.
533
534 Returns:
535 projected_packed: (total_projected_tokens, text_hidden_size)
536
537 """
538 assert spatial_shapes.device.type == "cpu", (
539 "Expected `spatial_shapes` on CPU to avoid device-to-host sync in "
540 "variable-length packing."
541 )
542 factor = self.factor
543 device = vision_features_packed.device
544 hidden_size = vision_features_packed.shape[-1]
545
546 spatial_shapes_list: list[list[int]] = spatial_shapes.tolist()
547 lengths_list = [h * w for h, w in spatial_shapes_list]
548
549 gather_idx_parts: list[torch.Tensor] = []
550 offset = 0
551
552 dh = torch.arange(factor, dtype=torch.int64)
553 dw = torch.arange(factor, dtype=torch.int64)
554 dh_grid, dw_grid = torch.meshgrid(dh, dw, indexing="ij")
555 dh_flat = dh_grid.reshape(-1)
556 dw_flat = dw_grid.reshape(-1)
557
558 for (height, width), length in zip(spatial_shapes_list, lengths_list):
559 if length <= 0:
560 continue
561 if height % factor != 0 or width % factor != 0:
562 raise ValueError(
563 "spatial_shapes must be divisible by downsample_factor: "
564 f"got ({height}, {width}) with factor={factor}."
565 )
566 height_out = height // factor
567 width_out = width // factor
568
569 rows_out = torch.arange(height_out, dtype=torch.int64)
570 cols_out = torch.arange(width_out, dtype=torch.int64)
571 rr, cc = torch.meshgrid(rows_out, cols_out, indexing="ij")
572 rr = rr.reshape(-1)
573 cc = cc.reshape(-1)
574
575 token_idx = (rr[:, None] * factor + dh_flat[None, :]) * width + (
576 cc[:, None] * factor + dw_flat[None, :]
577 )
578 gather_idx_parts.append(token_idx.reshape(-1) + offset)
579 offset += length
580
581 if gather_idx_parts:
582 gather_idx = torch.cat(gather_idx_parts).to(device=device)
583 return self.forward_with_gather_idx(vision_features_packed, gather_idx)
584 else:
585 unshuffled = vision_features_packed.new_empty(
586 (0, factor * factor * hidden_size)
587 )
588
589 return self.forward_from_unshuffled(unshuffled)
590
591 def forward_with_gather_idx(
592 self,
593 vision_features_packed: torch.Tensor,
594 gather_idx: torch.Tensor,
595 ) -> torch.Tensor:
596 hidden_size = vision_features_packed.shape[-1]
597 factor = self.factor
598 gathered = vision_features_packed.index_select(0, gather_idx)
599 unshuffled = gathered.reshape(-1, factor * factor * hidden_size)
600 return self.forward_from_unshuffled(unshuffled)
601
602 def forward_from_unshuffled(self, unshuffled: torch.Tensor) -> torch.Tensor:
603 if self.projector_use_layernorm:
604 unshuffled = self.layer_norm(unshuffled)
605 hidden_states = self.linear_1(unshuffled)
606 hidden_states = self.act(hidden_states)
607 projected_packed = self.linear_2(hidden_states)
608 return projected_packed
609
610
611 @MULTIMODAL_REGISTRY.register_processor(
612 Lfm2VLMultiModalProcessor,
613 info=Lfm2VLProcessingInfo,
614 dummy_inputs=Lfm2VLDummyInputsBuilder,
615 )
616 class Lfm2VLForConditionalGeneration(
617 nn.Module,
618 SupportsMultiModal,
619 SupportsEncoderCudaGraph,
620 SupportsLoRA,
621 SupportsPP,
622 IsHybrid,
623 ):
624 supports_tower_connector_lora = True
625 merge_by_field_config = True
626
627 hf_to_vllm_mapper = WeightsMapper(
628 orig_to_new_prefix={
629 "lm_head.": "language_model.lm_head.",
630 "model.language_model.": "language_model.model.",
631 "model.vision_tower.": "vision_tower.",
632 "model.multi_modal_projector.": "multi_modal_projector.",
633 }
634 )
635
636 @classmethod
637 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
638 if modality.startswith("image"):
639 return "<image>"
640
641 raise ValueError("Only image modality is supported")
642
643 @classmethod
644 def get_mamba_state_dtype_from_config(
645 cls,
646 vllm_config: "VllmConfig",
647 ) -> tuple[torch.dtype, ...]:
648 return MambaStateDtypeCalculator.short_conv_state_dtype(
649 vllm_config.model_config.dtype,
650 vllm_config.cache_config.mamba_cache_dtype,
651 )
652
653 @classmethod
654 def get_mamba_state_shape_from_config(
655 cls,
656 vllm_config: "VllmConfig",
657 ) -> tuple[tuple[int, int]]:
658 """Calculate shapes for LFM2's convolutional cache.
659
660 Args:
661 vllm_config: vLLM config
662
663 Returns:
664 Tuple containing:
665 - conv_state_shape: Shape for convolutional state cache
666
667 """
668 parallel_config = vllm_config.parallel_config
669 hf_language_config = vllm_config.model_config.hf_config.text_config
670
671 return MambaStateShapeCalculator.short_conv_state_shape(
672 tp_world_size=parallel_config.tensor_parallel_size,
673 intermediate_size=hf_language_config.hidden_size,
674 conv_kernel=hf_language_config.conv_L_cache,
675 )
676
677 @classmethod
678 def get_mamba_state_copy_func(cls) -> tuple[MambaStateCopyFunc]:
679 return MambaStateCopyFuncCalculator.short_conv_state_copy_func()
680
681 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "model"):
682 super().__init__()
683 config: Lfm2VlConfig = vllm_config.model_config.hf_config
684 multimodal_config = vllm_config.model_config.multimodal_config
685 assert multimodal_config is not None
686 vision_config = config.vision_config
687 quant_config = vllm_config.quant_config
688
689 self.config = config
690 self.vllm_config = vllm_config
691 self.model_config = vllm_config.model_config
692 self.multimodal_config = multimodal_config
693 self.use_data_parallel = multimodal_config.mm_encoder_tp_mode == "data"
694
695 with self._mark_tower_model(vllm_config, "image"):
696 if vision_config.model_type == "siglip2_vision_model":
697 self.vision_tower = Siglip2Model(
698 config=vision_config,
699 quant_config=quant_config,
700 prefix=maybe_prefix(prefix, "vision_tower"),
701 )
702 else:
703 raise ValueError(
704 f"Unsupported visual tokenizer type: {vision_config.model_type}"
705 )
706
707 self.multi_modal_projector = Lfm2VLMultiModalProjector(
708 config=config,
709 prefix=maybe_prefix(prefix, "multi_modal_projector"),
710 )
711
712 with self._mark_language_model(vllm_config):
713 self.language_model = init_vllm_registered_model(
714 vllm_config=vllm_config,
715 hf_config=config.text_config,
716 prefix=maybe_prefix(prefix, "language"),
717 architectures=config.text_config.architectures,
718 )
719
720 self.make_empty_intermediate_tensors = (
721 self.language_model.make_empty_intermediate_tensors
722 )
723
724 def _parse_and_validate_image_input(
725 self, **kwargs: object
726 ) -> LFM2VLImageInputs | None:
727 pixel_values = kwargs.pop("pixel_values", None)
728 spatial_shapes = kwargs.pop("spatial_shapes", None)
729 num_patches = kwargs.pop("num_patches", None)
730 if pixel_values is None:
731 return None
732
733 return LFM2VLImageInputs(
734 type="pixel_values",
735 pixel_values=pixel_values,
736 spatial_shapes=spatial_shapes,
737 num_patches=num_patches,
738 )
739
740 def image_pixels_to_features(
741 self,
742 pixel_values: torch.FloatTensor,
743 spatial_shapes: torch.Tensor,
744 ) -> list[torch.Tensor]:
745 assert spatial_shapes.device.type == "cpu", (
746 "Expected `spatial_shapes` on CPU to avoid device-to-host sync in "
747 "variable-length packing."
748 )
749
750 pixel_values = pixel_values.to(
751 dtype=self.vision_tower.vision_model.embeddings.patch_embedding.weight.dtype
752 ) # fp16 compatibility
753
754 # LFM2-VL's HF processor pads patch sequences with trailing zeros.
755 # Pack patch tokens upfront so the vision tower runs entirely unpadded.
756 spatial_shapes_list: list[list[int]] = spatial_shapes.tolist()
757 lengths_list = [h * w for h, w in spatial_shapes_list]
758 total_tokens = int(sum(lengths_list))
759 lengths_cpu = (spatial_shapes[:, 0] * spatial_shapes[:, 1]).to(
760 dtype=torch.int32
761 )
762 max_seqlen = (
763 lengths_cpu.max().reshape(1)
764 if lengths_cpu.numel()
765 else torch.tensor([0], dtype=torch.int32)
766 )
767
768 if total_tokens == 0:
769 return []
770
771 packed_pixel_values = pixel_values.new_empty(
772 (total_tokens, pixel_values.shape[-1])
773 )
774 offset = 0
775 for i, length in enumerate(lengths_list):
776 if length <= 0:
777 continue
778 packed_pixel_values[offset : offset + length].copy_(
779 pixel_values[i, :length]
780 )
781 offset += length
782 packed_pixel_values = packed_pixel_values.unsqueeze(0)
783
784 lengths = torch.tensor(
785 lengths_list, dtype=torch.int32, device=pixel_values.device
786 )
787 cu_seqlens = torch.zeros(
788 lengths.shape[0] + 1,
789 dtype=torch.int32,
790 device=pixel_values.device,
791 )
792 cu_seqlens[1:] = torch.cumsum(lengths, dim=0)
793
794 with set_forward_context(None, self.vllm_config):
795 vision_outputs = self.vision_tower(
796 pixel_values_packed=packed_pixel_values,
797 spatial_shapes=spatial_shapes,
798 cu_seqlens=cu_seqlens,
799 max_seqlen=max_seqlen,
800 )
801 image_outputs_packed = getattr(
802 vision_outputs, "last_hidden_state", vision_outputs
803 )
804 vision_features_packed = image_outputs_packed[0]
805
806 projected_packed = self.multi_modal_projector(
807 vision_features_packed=vision_features_packed,
808 spatial_shapes=spatial_shapes,
809 )
810 projected_lengths_list = self._get_lfm2vl_tile_output_lengths(
811 spatial_shapes_list
812 )
813
814 image_features: list[torch.Tensor] = []
815 offset = 0
816 for out_len in projected_lengths_list:
817 image_features.append(projected_packed[offset : offset + out_len])
818 offset += out_len
819
820 return image_features
821
822 def _process_image_input(
823 self,
824 image_input: LFM2VLImageInputs,
825 ) -> torch.Tensor | list[torch.Tensor]:
826 pixel_values = image_input["pixel_values"]
827 spatial_shapes = image_input["spatial_shapes"]
828 num_patches = image_input["num_patches"]
829
830 image_features = self.image_pixels_to_features(
831 pixel_values,
832 spatial_shapes=spatial_shapes,
833 )
834
835 # Group patches by image - num_patches is on CPU (keep_on_cpu=True)
836 # so .tolist() is instant with no DtoH sync
837 num_patches_list = num_patches.tolist()
838 batched_features: list[torch.Tensor] = []
839 patch_idx = 0
840 for count in num_patches_list:
841 # Slice the list of patch tensors for this image
842 image_patches = image_features[patch_idx : patch_idx + count]
843 # Concatenate patches for this image
844 batched_features.append(torch.cat(image_patches, dim=0))
845 patch_idx += count
846
847 return batched_features
848
849 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
850 image_input = self._parse_and_validate_image_input(**kwargs)
851 if image_input is None:
852 return []
853
854 return self._process_image_input(image_input)
855
856 def get_encoder_cudagraph_config(self):
857 from vllm.v1.worker.encoder_cudagraph_defs import (
858 EncoderCudaGraphConfig,
859 )
860
861 return EncoderCudaGraphConfig(
862 modalities=["image"],
863 buffer_keys=[
864 "pixel_values_packed",
865 "pos_embeds",
866 "cu_seqlens",
867 "max_seqlen",
868 "gather_idx",
869 ],
870 out_hidden_size=self.config.text_config.hidden_size,
871 padding_logics={
872 "cu_seqlens": _pad_cumulative_seqlens_buffer,
873 },
874 )
875
876 def get_max_frames_per_video(self) -> int:
877 return 0
878
879 def get_encoder_cudagraph_budget_range(
880 self,
881 vllm_config: VllmConfig,
882 ) -> tuple[int, int]:
883 min_budget = self._get_lfm2vl_min_image_tokens()
884 max_budget = min(
885 vllm_config.scheduler_config.max_num_batched_tokens,
886 self.model_config.max_model_len,
887 )
888 return min_budget, max_budget
889
890 def _get_spatial_shapes_list(
891 self,
892 spatial_shapes: torch.Tensor,
893 ) -> list[list[int]]:
894 assert spatial_shapes.device.type == "cpu", (
895 "Expected `spatial_shapes` on CPU to avoid device-to-host sync in "
896 "variable-length packing."
897 )
898 return spatial_shapes.tolist()
899
900 @staticmethod
901 def _get_lfm2vl_tile_input_lengths(
902 spatial_shapes_list: list[list[int]],
903 ) -> list[int]:
904 return [height * width for height, width in spatial_shapes_list]
905
906 def _get_lfm2vl_tile_output_lengths(
907 self,
908 spatial_shapes_list: list[list[int]],
909 ) -> list[int]:
910 factor = self.multi_modal_projector.factor
911 output_lengths: list[int] = []
912 for height, width in spatial_shapes_list:
913 if height % factor != 0 or width % factor != 0:
914 raise ValueError(
915 "spatial_shapes must be divisible by downsample_factor: "
916 f"got ({height}, {width}) with factor={factor}."
917 )
918 output_lengths.append((height // factor) * (width // factor))
919 return output_lengths
920
921 def _get_lfm2vl_mm_processor_kwargs(self) -> Mapping[str, object]:
922 return self.multimodal_config.mm_processor_kwargs or {}
923
924 def _get_lfm2vl_min_image_tokens(self) -> int:
925 value = self._get_lfm2vl_mm_processor_kwargs().get(
926 "min_image_tokens",
927 getattr(self.config, "min_image_tokens", None) or 64,
928 )
929 if not isinstance(
930 value,
931 (str, Buffer, typing.SupportsInt, typing.SupportsIndex),
932 ):
933 raise TypeError(
934 "int() argument must be a string, a bytes-like object "
935 f"or a real number, not '{type(value).__name__}'"
936 )
937 return max(1, int(value))
938
939 def _get_lfm2vl_item_tile_slices(
940 self,
941 num_patches: torch.Tensor,
942 ) -> list[tuple[int, int]]:
943 num_patches_list = [int(x) for x in num_patches.tolist()]
944 starts = [0]
945 for count in num_patches_list:
946 starts.append(starts[-1] + count)
947 return list(zip(starts[:-1], starts[1:]))
948
949 def get_encoder_cudagraph_item_specs(
950 self,
951 mm_kwargs: dict[str, Any],
952 ):
953 from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
954
955 spatial_shapes = mm_kwargs["spatial_shapes"]
956 num_patches = mm_kwargs["num_patches"]
957 spatial_shapes_list = self._get_spatial_shapes_list(spatial_shapes)
958 input_lengths = self._get_lfm2vl_tile_input_lengths(spatial_shapes_list)
959 output_lengths = self._get_lfm2vl_tile_output_lengths(spatial_shapes_list)
960
961 return [
962 EncoderItemSpec(
963 input_size=sum(input_lengths[start:end]),
964 output_tokens=sum(output_lengths[start:end]),
965 )
966 for start, end in self._get_lfm2vl_item_tile_slices(num_patches)
967 ]
968
969 def select_encoder_cudagraph_items(
970 self,
971 mm_kwargs: dict[str, Any],
972 indices: list[int],
973 ) -> dict[str, Any]:
974 pixel_values = mm_kwargs["pixel_values"]
975 spatial_shapes = mm_kwargs["spatial_shapes"]
976 num_patches = mm_kwargs["num_patches"]
977
978 tile_slices = self._get_lfm2vl_item_tile_slices(num_patches)
979
980 if len(indices) == 0:
981 return {
982 "pixel_values": pixel_values[:0],
983 "spatial_shapes": spatial_shapes[:0],
984 "num_patches": num_patches[:0],
985 }
986
987 tile_indices: list[int] = []
988 for image_idx in indices:
989 start, end = tile_slices[image_idx]
990 tile_indices.extend(range(start, end))
991
992 return {
993 "pixel_values": pixel_values[tile_indices],
994 "spatial_shapes": spatial_shapes[tile_indices],
995 "num_patches": num_patches[indices],
996 }
997
998 def _pack_lfm2vl_pixel_values(
999 self,
1000 pixel_values: torch.Tensor,
1001 spatial_shapes_list: list[list[int]],
1002 ) -> torch.Tensor:
1003 input_lengths = self._get_lfm2vl_tile_input_lengths(spatial_shapes_list)
1004 total_tokens = sum(input_lengths)
1005 packed = pixel_values.new_empty((total_tokens, pixel_values.shape[-1]))
1006
1007 offset = 0
1008 for i, length in enumerate(input_lengths):
1009 if length <= 0:
1010 continue
1011 packed[offset : offset + length].copy_(pixel_values[i, :length])
1012 offset += length
1013 return packed
1014
1015 def _get_lfm2vl_pos_embeds(
1016 self,
1017 spatial_shapes: torch.Tensor,
1018 spatial_shapes_list: list[list[int]],
1019 ) -> torch.Tensor:
1020 embeddings = self.vision_tower.vision_model.embeddings
1021 positional_embeddings = embeddings.position_embedding.weight.reshape(
1022 embeddings.position_embedding_size,
1023 embeddings.position_embedding_size,
1024 -1,
1025 )
1026 lengths_list = self._get_lfm2vl_tile_input_lengths(spatial_shapes_list)
1027 return embeddings.resize_positional_embeddings_packed(
1028 positional_embeddings,
1029 spatial_shapes,
1030 lengths_list=lengths_list,
1031 )
1032
1033 def _get_lfm2vl_cu_seqlens(
1034 self,
1035 spatial_shapes_list: list[list[int]],
1036 device: torch.device,
1037 ) -> torch.Tensor:
1038 lengths = torch.tensor(
1039 self._get_lfm2vl_tile_input_lengths(spatial_shapes_list),
1040 dtype=torch.int32,
1041 device=device,
1042 )
1043 cu_seqlens = torch.zeros(
1044 lengths.shape[0] + 1,
1045 dtype=torch.int32,
1046 device=device,
1047 )
1048 if lengths.numel() > 0:
1049 cu_seqlens[1:] = torch.cumsum(lengths, dim=0)
1050 return cu_seqlens
1051
1052 def _get_lfm2vl_max_seqlen(
1053 self,
1054 spatial_shapes_list: list[list[int]],
1055 ) -> torch.Tensor:
1056 input_lengths = self._get_lfm2vl_tile_input_lengths(spatial_shapes_list)
1057 max_seqlen = max(input_lengths) if input_lengths else 0
1058 return torch.tensor(max_seqlen, dtype=torch.int32)
1059
1060 def _get_lfm2vl_projector_gather_idx(
1061 self,
1062 spatial_shapes_list: list[list[int]],
1063 device: torch.device,
1064 ) -> torch.Tensor:
1065 factor = self.multi_modal_projector.factor
1066 dh = torch.arange(factor, dtype=torch.int64)
1067 dw = torch.arange(factor, dtype=torch.int64)
1068 dh_grid, dw_grid = torch.meshgrid(dh, dw, indexing="ij")
1069 dh_flat = dh_grid.reshape(-1)
1070 dw_flat = dw_grid.reshape(-1)
1071
1072 gather_idx_parts: list[torch.Tensor] = []
1073 offset = 0
1074 for height, width in spatial_shapes_list:
1075 length = height * width
1076 if length <= 0:
1077 continue
1078 if height % factor != 0 or width % factor != 0:
1079 raise ValueError(
1080 "spatial_shapes must be divisible by downsample_factor: "
1081 f"got ({height}, {width}) with factor={factor}."
1082 )
1083
1084 rows_out = torch.arange(height // factor, dtype=torch.int64)
1085 cols_out = torch.arange(width // factor, dtype=torch.int64)
1086 rr, cc = torch.meshgrid(rows_out, cols_out, indexing="ij")
1087 rr = rr.reshape(-1)
1088 cc = cc.reshape(-1)
1089 token_idx = (rr[:, None] * factor + dh_flat[None, :]) * width + (
1090 cc[:, None] * factor + dw_flat[None, :]
1091 )
1092 gather_idx_parts.append(token_idx.reshape(-1) + offset)
1093 offset += length
1094
1095 if not gather_idx_parts:
1096 return torch.empty(0, dtype=torch.int64, device=device)
1097 return torch.cat(gather_idx_parts).to(device=device)
1098
1099 def _prepare_lfm2vl_cudagraph_values(
1100 self,
1101 pixel_values: torch.Tensor,
1102 spatial_shapes: torch.Tensor,
1103 ) -> dict[str, torch.Tensor]:
1104 spatial_shapes_list = self._get_spatial_shapes_list(spatial_shapes)
1105 pixel_values_packed = self._pack_lfm2vl_pixel_values(
1106 pixel_values,
1107 spatial_shapes_list,
1108 )
1109 pos_embeds = self._get_lfm2vl_pos_embeds(spatial_shapes, spatial_shapes_list)
1110 device = pixel_values.device
1111
1112 return {
1113 "pixel_values_packed": pixel_values_packed,
1114 "pos_embeds": pos_embeds,
1115 "cu_seqlens": self._get_lfm2vl_cu_seqlens(spatial_shapes_list, device),
1116 "max_seqlen": self._get_lfm2vl_max_seqlen(spatial_shapes_list),
1117 "gather_idx": self._get_lfm2vl_projector_gather_idx(
1118 spatial_shapes_list,
1119 device,
1120 ),
1121 }
1122
1123 def _get_lfm2vl_capture_spatial_shapes(
1124 self,
1125 token_budget: int,
1126 ) -> torch.Tensor:
1127 factor = self.multi_modal_projector.factor
1128 min_image_tokens = self._get_lfm2vl_min_image_tokens()
1129 remaining = token_budget
1130 shapes: list[list[int]] = []
1131
1132 while remaining > 0:
1133 out_tokens = min(remaining, min_image_tokens)
1134 shapes.append([factor, out_tokens * factor])
1135 remaining -= out_tokens
1136
1137 return torch.tensor(shapes, dtype=torch.int64)
1138
1139 def prepare_encoder_cudagraph_capture_inputs(
1140 self,
1141 token_budget: int,
1142 max_batch_size: int,
1143 max_frames_per_batch: int,
1144 device: torch.device,
1145 dtype: torch.dtype,
1146 path: str = "default",
1147 axis_keys: tuple[Hashable, ...] | None = None,
1148 ):
1149 from vllm.v1.worker.encoder_cudagraph_defs import (
1150 EncoderCudaGraphCaptureInputs,
1151 )
1152
1153 spatial_shapes = self._get_lfm2vl_capture_spatial_shapes(token_budget)
1154 spatial_shapes_list = self._get_spatial_shapes_list(spatial_shapes)
1155 input_lengths = self._get_lfm2vl_tile_input_lengths(spatial_shapes_list)
1156 total_input_tokens = sum(input_lengths)
1157
1158 patch_dim = (
1159 self.vision_tower.vision_model.embeddings.patch_embedding.weight.shape[1]
1160 )
1161 dummy_pixel_values = torch.randn(
1162 total_input_tokens,
1163 patch_dim,
1164 device=device,
1165 dtype=dtype,
1166 )
1167 pos_embeds = self._get_lfm2vl_pos_embeds(
1168 spatial_shapes,
1169 spatial_shapes_list,
1170 ).to(device=device, dtype=dtype)
1171
1172 # max_seqlen.item() is baked into the captured ViT attention graph, so
1173 # capture with a budget-level upper bound that covers any replay item.
1174 max_tile_input_tokens = token_budget * self.multi_modal_projector.factor**2
1175 values = {
1176 "pixel_values_packed": dummy_pixel_values,
1177 "pos_embeds": pos_embeds,
1178 "cu_seqlens": self._get_lfm2vl_cu_seqlens(spatial_shapes_list, device),
1179 "max_seqlen": torch.tensor(max_tile_input_tokens, dtype=torch.int32),
1180 "gather_idx": self._get_lfm2vl_projector_gather_idx(
1181 spatial_shapes_list,
1182 device,
1183 ),
1184 }
1185
1186 return EncoderCudaGraphCaptureInputs(values=values)
1187
1188 def prepare_encoder_cudagraph_replay_buffers(
1189 self,
1190 mm_kwargs: dict[str, Any],
1191 max_batch_size: int,
1192 max_frames_per_batch: int,
1193 path: str = "default",
1194 ):
1195 from vllm.v1.worker.encoder_cudagraph_defs import (
1196 EncoderCudaGraphReplayBuffers,
1197 )
1198
1199 values = self._prepare_lfm2vl_cudagraph_values(
1200 mm_kwargs["pixel_values"],
1201 mm_kwargs["spatial_shapes"],
1202 )
1203 return EncoderCudaGraphReplayBuffers(values=values)
1204
1205 def encoder_cudagraph_forward(
1206 self,
1207 values: dict[str, torch.Tensor],
1208 path: str = "default",
1209 ) -> torch.Tensor:
1210 embeddings = self.vision_tower.vision_model.embeddings
1211 pixel_values = values["pixel_values_packed"].to(
1212 dtype=embeddings.patch_embedding.weight.dtype
1213 )
1214 patch_embeds = embeddings.patch_embedding(pixel_values)
1215 hidden_states = (patch_embeds + values["pos_embeds"]).unsqueeze(0)
1216
1217 with set_forward_context(None, self.vllm_config):
1218 encoder_outputs = self.vision_tower.vision_model.encoder(
1219 inputs_embeds=hidden_states,
1220 cu_seqlens=values["cu_seqlens"],
1221 max_seqlen=values["max_seqlen"],
1222 )
1223
1224 post_layernorm = self.vision_tower.vision_model.post_layernorm
1225 if post_layernorm is not None:
1226 encoder_outputs = post_layernorm(encoder_outputs)
1227
1228 return self.multi_modal_projector.forward_with_gather_idx(
1229 vision_features_packed=encoder_outputs[0],
1230 gather_idx=values["gather_idx"],
1231 )
1232
1233 def encoder_eager_forward(
1234 self,
1235 mm_kwargs: dict[str, Any],
1236 path: str = "default",
1237 ) -> torch.Tensor:
1238 image_input = LFM2VLImageInputs(
1239 type="pixel_values",
1240 pixel_values=mm_kwargs["pixel_values"],
1241 spatial_shapes=mm_kwargs["spatial_shapes"],
1242 num_patches=mm_kwargs["num_patches"],
1243 )
1244 return torch.cat(self._process_image_input(image_input), dim=0)
1245
1246 def forward(
1247 self,
1248 input_ids: torch.Tensor | None,
1249 positions: torch.Tensor,
1250 intermediate_tensors: IntermediateTensors | None = None,
1251 inputs_embeds: torch.Tensor | None = None,
1252 **kwargs: object,
1253 ) -> torch.Tensor | IntermediateTensors:
1254 if intermediate_tensors is not None:
1255 inputs_embeds = None
1256
1257 hidden_states = self.language_model(
1258 input_ids=input_ids,
1259 positions=positions,
1260 intermediate_tensors=intermediate_tensors,
1261 inputs_embeds=inputs_embeds,
1262 )
1263 return hidden_states
1264
1265 def compute_logits(
1266 self,
1267 hidden_states: torch.Tensor,
1268 ) -> torch.Tensor | None:
1269 return self.language_model.compute_logits(hidden_states)
1270
1271 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
1272 loader = AutoWeightsLoader(self)
1273 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
1274
1275 def get_mm_mapping(self) -> MultiModelKeys:
1276 """Get the module prefix in multimodal models."""
1277 return MultiModelKeys.from_string_field(
1278 language_model="language_model",
1279 connector="multi_modal_projector",
1280 tower_model="vision_tower",
1281 )
1282
1283 def get_mm_lora_token_counts(
1284 self,
1285 *,
1286 modality: str,
1287 mm_kwargs: MultiModalKwargsItem | None,
1288 num_mm_embeds: int,
1289 ) -> tuple[int, int | None]:
1290 del modality, mm_kwargs
1291 downsample_factor = self.config.downsample_factor
1292 return num_mm_embeds * downsample_factor**2, num_mm_embeds