main
py 522 lines 17.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import math
5 from collections.abc import Iterable, Mapping, Sequence
6 from typing import Annotated, Literal
7
8 import torch
9 import torch.nn as nn
10 from transformers import BatchFeature, LlavaNextVideoConfig, LlavaNextVideoProcessor
11
12 from vllm.config import VllmConfig
13 from vllm.config.multimodal import MultiModalDummyOptions
14 from vllm.inputs import MultiModalDataDict
15 from vllm.model_executor.layers.activation import get_act_fn
16 from vllm.model_executor.models.clip import CLIPVisionModel
17 from vllm.multimodal import MULTIMODAL_REGISTRY
18 from vllm.multimodal.inputs import (
19 MultiModalFieldConfig,
20 MultiModalKwargsItem,
21 MultiModalKwargsItems,
22 )
23 from vllm.multimodal.parse import (
24 ImageSize,
25 MultiModalDataItems,
26 VideoEmbeddingItems,
27 VideoProcessorItems,
28 )
29 from vllm.multimodal.processing import (
30 BaseDummyInputsBuilder,
31 BaseMultiModalProcessor,
32 BaseProcessingInfo,
33 PromptReplacement,
34 PromptUpdate,
35 )
36 from vllm.sequence import IntermediateTensors
37 from vllm.utils.collection_utils import is_list_of
38 from vllm.utils.tensor_schema import TensorSchema, TensorShape
39
40 from .interfaces import (
41 MultiModalEmbeddings,
42 SupportsLoRA,
43 SupportsMultiModal,
44 SupportsPP,
45 )
46 from .llava import init_vision_tower_for_llava
47 from .module_mapping import MultiModelKeys
48 from .siglip import SiglipVisionModel
49 from .utils import (
50 AutoWeightsLoader,
51 WeightsMapper,
52 init_vllm_registered_model,
53 maybe_prefix,
54 )
55 from .vision import get_vision_encoder_info
56
57
58 class LlavaNextVideoPixelInputs(TensorSchema):
59 """Dimensions:
60 - bn: Batch size * number of videos
61 - f: Number of frames
62 - c: Number of channels (3)
63 - h: Height of each frame
64 - w: Width of each frame
65
66 Note that `f` may be different for each batch, in which case
67 the data is passed as a list instead of a batched tensor.
68
69 Note that it only supports one video input for one batch.
70 """
71
72 type: Literal["pixel_values_videos"] = "pixel_values_videos"
73
74 pixel_values_videos: Annotated[
75 torch.Tensor | list[torch.Tensor],
76 TensorShape("bn", "f", 3, "h", "w", dynamic_dims={"f"}),
77 ]
78
79
80 class LlavaNextVideoProcessingInfo(BaseProcessingInfo):
81 def get_hf_config(self):
82 return self.ctx.get_hf_config(LlavaNextVideoConfig)
83
84 def get_vision_encoder_info(self):
85 return get_vision_encoder_info(self.get_hf_config())
86
87 def get_hf_processor(self, **kwargs: object):
88 return self.ctx.get_hf_processor(LlavaNextVideoProcessor, **kwargs)
89
90 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
91 return {"video": 1}
92
93 def get_image_size_with_most_features(self) -> ImageSize:
94 vision_encoder_info = self.get_vision_encoder_info()
95 width = height = vision_encoder_info.get_image_size()
96 return ImageSize(width=width, height=height)
97
98 def _get_num_frame_tokens(
99 self,
100 *,
101 image_width: int,
102 image_height: int,
103 ) -> int:
104 hf_config = self.get_hf_config()
105 spatial_pool_stride = hf_config.spatial_pool_stride
106
107 vision_encoder_info = self.get_vision_encoder_info()
108 patch_grid_length = vision_encoder_info.get_patch_grid_length()
109 pooled_grid_length = math.ceil(patch_grid_length / spatial_pool_stride)
110
111 return pooled_grid_length * pooled_grid_length
112
113 def get_num_video_tokens(
114 self,
115 *,
116 image_width: int,
117 image_height: int,
118 num_frames: int,
119 ) -> int:
120 num_frame_tokens = self._get_num_frame_tokens(
121 image_width=image_width,
122 image_height=image_height,
123 )
124
125 return num_frame_tokens * num_frames
126
127 def _get_max_video_frames(self, max_tokens: int) -> int:
128 target_width, target_height = self.get_image_size_with_most_features()
129
130 num_frames = 0
131
132 while True:
133 next_num_frames = num_frames + 1
134 next_max_tokens = self.get_num_video_tokens(
135 image_width=target_width,
136 image_height=target_height,
137 num_frames=next_num_frames,
138 )
139
140 if next_max_tokens > max_tokens:
141 break
142
143 num_frames = next_num_frames
144
145 return num_frames
146
147 def get_num_frames_with_most_features(
148 self,
149 seq_len: int,
150 mm_counts: Mapping[str, int],
151 ) -> int:
152 max_videos = mm_counts.get("video", 0)
153
154 max_total_frames = self._get_max_video_frames(seq_len)
155
156 return max(max_total_frames // max(max_videos, 1), 1)
157
158
159 class LlavaNextVideoDummyInputsBuilder(
160 BaseDummyInputsBuilder[LlavaNextVideoProcessingInfo]
161 ):
162 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
163 num_videos = mm_counts.get("video", 0)
164
165 processor = self.info.get_hf_processor()
166 video_token = processor.video_token
167
168 return video_token * num_videos
169
170 def get_dummy_mm_data(
171 self,
172 seq_len: int,
173 mm_counts: Mapping[str, int],
174 mm_options: MultiModalDummyOptions,
175 ) -> MultiModalDataDict:
176 target_width, target_height = self.info.get_image_size_with_most_features()
177 target_num_frames = self.info.get_num_frames_with_most_features(
178 seq_len, mm_counts
179 )
180
181 return {
182 "video": self._get_dummy_videos(
183 width=target_width,
184 height=target_height,
185 num_frames=target_num_frames,
186 num_videos=mm_counts.get("video", 0),
187 overrides=mm_options.get("video"),
188 )
189 }
190
191
192 class LlavaNextVideoMultiModalProcessor(
193 BaseMultiModalProcessor[LlavaNextVideoProcessingInfo]
194 ):
195 def _get_mm_fields_config(
196 self,
197 hf_inputs: BatchFeature,
198 hf_processor_mm_kwargs: Mapping[str, object],
199 ) -> Mapping[str, MultiModalFieldConfig]:
200 return dict(pixel_values_videos=MultiModalFieldConfig.batched("video"))
201
202 def _get_prompt_updates(
203 self,
204 mm_items: MultiModalDataItems,
205 hf_processor_mm_kwargs: Mapping[str, object],
206 out_mm_kwargs: MultiModalKwargsItems,
207 ) -> Sequence[PromptUpdate]:
208 hf_config = self.info.get_hf_config()
209 video_token_id = hf_config.video_token_index
210
211 def get_replacement(item_idx: int):
212 videos = mm_items.get_items(
213 "video", (VideoEmbeddingItems, VideoProcessorItems)
214 )
215
216 if isinstance(videos, VideoEmbeddingItems):
217 num_video_tokens = videos.get_feature_size(item_idx)
218 else:
219 assert isinstance(videos, VideoProcessorItems)
220 image_size = videos.get_frame_size(item_idx)
221 num_video_tokens = self.info.get_num_video_tokens(
222 image_width=image_size.width,
223 image_height=image_size.height,
224 num_frames=videos.get_num_frames(item_idx),
225 )
226
227 return [video_token_id] * num_video_tokens
228
229 return [
230 PromptReplacement(
231 modality="video",
232 target=[video_token_id],
233 replacement=get_replacement,
234 ),
235 ]
236
237
238 # adopted from transformers modeling_llava_next_video.py
239 class LlavaNextVideoPooler(nn.Module):
240 def __init__(self, config: LlavaNextVideoConfig):
241 super().__init__()
242
243 mode = config.spatial_pool_mode
244 stride = config.spatial_pool_stride
245 image_size = config.vision_config.image_size
246 patch_size = config.vision_config.patch_size
247 self.image_size = image_size // patch_size**2
248
249 if mode == "average":
250 self.pool = nn.AvgPool2d(kernel_size=stride, stride=stride)
251 elif mode == "max":
252 self.pool = nn.MaxPool2d(kernel_size=stride, stride=stride)
253 else:
254 # TODO: Support Conv2d pooling layer, need to load weights
255 raise ValueError(
256 f"Unknown pooling mode: {mode}. Expected [`average`, `max`]"
257 )
258
259 def forward(self, image_features: torch.Tensor):
260 ori_width = int(
261 math.sqrt(image_features.shape[1] * self.image_size // self.image_size)
262 )
263 ori_height = int(ori_width * self.image_size // self.image_size)
264
265 batch_size, _, dim = image_features.shape
266 image_features_spatial = image_features.view(
267 batch_size, ori_height, ori_height, dim
268 ).permute(0, 3, 1, 2)
269 image_features_spatial = self.pool(image_features_spatial)
270
271 return image_features_spatial.flatten(2).transpose(1, 2).contiguous()
272
273
274 class LlavaNextMultiModalProjector(nn.Module):
275 def __init__(
276 self,
277 vision_hidden_size: int,
278 text_hidden_size: int,
279 projector_hidden_act: str,
280 multimodal_projector_bias: bool,
281 ):
282 super().__init__()
283
284 self.linear_1 = nn.Linear(
285 vision_hidden_size, text_hidden_size, bias=multimodal_projector_bias
286 )
287 self.act = get_act_fn(projector_hidden_act)
288 self.linear_2 = nn.Linear(
289 text_hidden_size, text_hidden_size, bias=multimodal_projector_bias
290 )
291
292 def forward(self, image_features: torch.Tensor) -> torch.Tensor:
293 hidden_states = self.linear_1(image_features)
294 hidden_states = self.act(hidden_states)
295 hidden_states = self.linear_2(hidden_states)
296 return hidden_states
297
298
299 @MULTIMODAL_REGISTRY.register_processor(
300 LlavaNextVideoMultiModalProcessor,
301 info=LlavaNextVideoProcessingInfo,
302 dummy_inputs=LlavaNextVideoDummyInputsBuilder,
303 )
304 class LlavaNextVideoForConditionalGeneration(
305 nn.Module, SupportsLoRA, SupportsMultiModal, SupportsPP
306 ):
307 packed_modules_mapping = {
308 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
309 "gate_up_proj": ["gate_proj", "up_proj"],
310 }
311
312 hf_to_vllm_mapper = WeightsMapper(
313 orig_to_new_prefix={
314 # mapping for new names in checkpoint saved after transformers v4.52
315 "model.language_model.": "language_model.model.",
316 "model.vision_tower.": "vision_tower.",
317 "model.multi_modal_projector.": "multi_modal_projector.",
318 "model.image_newline": "image_newline",
319 "lm_head.": "language_model.lm_head.",
320 }
321 )
322
323 supports_tower_connector_lora = True
324
325 @classmethod
326 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
327 if modality.startswith("video"):
328 return "<video>"
329
330 raise ValueError("Only video modality is supported")
331
332 def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
333 super().__init__()
334
335 config = vllm_config.model_config.hf_config
336 quant_config = vllm_config.quant_config
337 multimodal_config = vllm_config.model_config.multimodal_config
338
339 self.config = config
340 self.multimodal_config = multimodal_config
341
342 vision_encoder_info = get_vision_encoder_info(config)
343 self.patch_grid_length = vision_encoder_info.get_patch_grid_length()
344 self.pooled_grid_length = math.ceil(
345 self.patch_grid_length / config.spatial_pool_stride
346 )
347
348 with self._mark_tower_model(vllm_config, "video"):
349 # Initialize the vision tower only up to the required feature layer
350 self.vision_tower = init_vision_tower_for_llava(
351 config,
352 quant_config=quant_config,
353 require_post_norm=False,
354 prefix=maybe_prefix(prefix, "vision_tower"),
355 )
356 self.vision_resampler = LlavaNextVideoPooler(config)
357 self.multi_modal_projector = LlavaNextMultiModalProjector(
358 vision_hidden_size=config.vision_config.hidden_size,
359 text_hidden_size=config.text_config.hidden_size,
360 projector_hidden_act=config.projector_hidden_act,
361 multimodal_projector_bias=config.multimodal_projector_bias,
362 )
363
364 with self._mark_language_model(vllm_config):
365 self.language_model = init_vllm_registered_model(
366 vllm_config=vllm_config,
367 hf_config=config.text_config,
368 prefix=maybe_prefix(prefix, "language_model"),
369 )
370
371 self.make_empty_intermediate_tensors = (
372 self.language_model.model.make_empty_intermediate_tensors
373 )
374
375 def _parse_and_validate_video_input(
376 self, **kwargs: object
377 ) -> LlavaNextVideoPixelInputs | None:
378 """A legal video input should have the following dimensions:
379 {
380 "pixel_values_videos" :
381 list[b, Tensor(nb_frames, nb_channels, height, width)]
382 }
383 """
384 pixel_values_videos = kwargs.pop("pixel_values_videos", None)
385
386 if pixel_values_videos is None:
387 return None
388
389 expected_h = expected_w = self.config.vision_config.image_size
390 return LlavaNextVideoPixelInputs(
391 type="pixel_values_videos",
392 pixel_values_videos=pixel_values_videos,
393 resolve_bindings={
394 "h": expected_h,
395 "w": expected_w,
396 },
397 )
398
399 def _video_pixels_to_features(
400 self,
401 vision_tower: CLIPVisionModel | SiglipVisionModel,
402 pixel_values: torch.Tensor,
403 ) -> torch.Tensor:
404 # NOTE: we skip the step to select the vision feature layer since
405 # this is already done inside the vision tower
406 image_features = vision_tower(
407 pixel_values,
408 feature_select_strategy=self.config.vision_feature_select_strategy,
409 )
410 image_features = self.vision_resampler(image_features)
411 image_features = self.multi_modal_projector(image_features)
412 return image_features
413
414 def _process_video_pixels(self, inputs: LlavaNextVideoPixelInputs):
415 video_pixels = inputs["pixel_values_videos"]
416
417 if isinstance(video_pixels, torch.Tensor):
418 bn, f, c, h, w = video_pixels.shape
419 stacked_pixels = video_pixels.view(bn * f, c, h, w)
420 stacked_embeddings = self._video_pixels_to_features(
421 self.vision_tower, stacked_pixels
422 )
423 embeds = stacked_embeddings.view(bn, f, *stacked_embeddings.shape[1:])
424
425 elif is_list_of(video_pixels, torch.Tensor):
426 frames_per_videos = [v.shape[0] for v in video_pixels]
427 stacked_pixels = torch.cat(video_pixels, dim=0)
428 stacked_embeddings = self._video_pixels_to_features(
429 self.vision_tower, stacked_pixels
430 )
431 embeds = torch.split(stacked_embeddings, frames_per_videos, dim=0)
432 else:
433 raise ValueError(f"Unsupported type of video input {type(video_pixels)}")
434
435 return [e.flatten(0, 1) for e in embeds]
436
437 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
438 video_input = self._parse_and_validate_video_input(**kwargs)
439 if video_input is None:
440 return []
441 vision_embeddings = self._process_video_pixels(video_input)
442 return vision_embeddings
443
444 def forward(
445 self,
446 input_ids: torch.Tensor | None,
447 positions: torch.Tensor,
448 intermediate_tensors: IntermediateTensors | None = None,
449 inputs_embeds: torch.Tensor | None = None,
450 **kwargs: object,
451 ) -> torch.Tensor | IntermediateTensors:
452 """Run forward pass for LlaVA-NeXT-Video.
453
454 Args:
455 input_ids: Flattened (concatenated) input_ids corresponding to a
456 batch.
457 positions: Flattened (concatenated) position ids corresponding to a
458 batch.
459 intermediate_tensors: Intermediate tensors from prior forward pass.
460 inputs_embeds: Optional tensor of input embeddings.
461 **kwargs: Multimodal inputs for this batch, forwarded to the
462 multimodal embedding path.
463
464 """
465 if intermediate_tensors is not None:
466 inputs_embeds = None
467
468 hidden_states = self.language_model.model(
469 input_ids, positions, intermediate_tensors, inputs_embeds=inputs_embeds
470 )
471
472 return hidden_states
473
474 def compute_logits(
475 self,
476 hidden_states: torch.Tensor,
477 ) -> torch.Tensor | None:
478 return self.language_model.compute_logits(hidden_states)
479
480 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
481 loader = AutoWeightsLoader(
482 self,
483 # This model doesn't support images for now
484 ignore_unexpected_prefixes=["image_newline"],
485 )
486 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
487
488 def get_mm_mapping(self) -> MultiModelKeys:
489 """Get the module prefix in multimodal models."""
490 return MultiModelKeys.from_string_field(
491 language_model="language_model",
492 connector="multi_modal_projector",
493 tower_model="vision_tower",
494 )
495
496 def get_mm_lora_token_counts(
497 self,
498 *,
499 modality: str,
500 mm_kwargs: MultiModalKwargsItem | None,
501 num_mm_embeds: int,
502 ) -> tuple[int, int | None]:
503 del modality, mm_kwargs
504
505 # Invert the spatial pooling done by `vision_resampler`: each frame
506 # contributes `pooled_grid_length ** 2` tokens after the language
507 # model's placeholder count, but `patch_grid_length ** 2` tokens
508 # when it leaves the vision encoder.
509 pooled_tokens_per_frame = self.pooled_grid_length**2
510 patch_tokens_per_frame = self.patch_grid_length**2
511 if (
512 num_mm_embeds <= 0
513 or pooled_tokens_per_frame <= 0
514 or patch_tokens_per_frame <= 0
515 ):
516 return 0, 0
517
518 num_frames = num_mm_embeds // pooled_tokens_per_frame
519 return (
520 num_frames * patch_tokens_per_frame,
521 num_frames * pooled_tokens_per_frame,
522 )