@tduncan / Nvidia-Optimization-Stack-VLLM Mirror · read-only / commits / d7e34b4210
Fork 0

A high-throughput and memory-efficient inference and serving engine for LLMs

Mirrored from https://github.com/tduncan-lum/Nvidia-Optimization-Stack---VLLM.git

Synced Nov 14, 05:30 UTC

[Model] Move `vision_feature_select_strategy` into `resolve_visual_encoder_outputs` (#25938)

Signed-off-by: DarkLight1337 <tlleungac@connect.ust.hk>

Cyrus Leung committed Sep 30, 2025 at 19:24 UTC d7e34b4210fd7ce7d8f49441c50f2b99c93913ae
12 files changed +155 -179
tests/models/test_vision.py
+6 -5
@@ -18,7 +18,7 @@ from vllm.utils import get_open_port, update_environment_variables
18
19
20 @pytest.mark.parametrize(
21 - ("feature_sample_layers", "num_layers_loaded", "max_possible_layers",
21 + ("select_layers", "num_layers_loaded", "max_possible_layers",
22 "expected_features"),
23 [
24 # All layers loaded
@@ -28,8 +28,8 @@ from vllm.utils import get_open_port, update_environment_variables
28 ([1, 10], 10, 20, [1, 10]),
29 ([-20, -11], 10, 20, [1, 10]),
30 ])
31 -def test_resolve_visual_encoder_outputs(feature_sample_layers,
32 - num_layers_loaded, max_possible_layers,
31 +def test_resolve_visual_encoder_outputs(select_layers, num_layers_loaded,
32 + max_possible_layers,
33 expected_features):
34 """
35 Test that offsets are correctly handled for vision feature layers.
@@ -39,9 +39,10 @@ def test_resolve_visual_encoder_outputs(feature_sample_layers,
39 ]
40 output_tensor = resolve_visual_encoder_outputs(
41 encoder_outputs=encoder_outputs,
42 - feature_sample_layers=feature_sample_layers,
42 post_layer_norm=None,
44 - max_possible_layers=max_possible_layers)
43 + select_layers=select_layers,
44 + max_possible_layers=max_possible_layers,
45 + )
46 assert torch.equal(torch.tensor(expected_features), output_tensor)
47
48
vllm/model_executor/models/aya_vision.py
+4 -23
@@ -27,7 +27,6 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
27 PromptUpdateDetails)
28 from vllm.multimodal.profiling import BaseDummyInputsBuilder
29 from vllm.sequence import IntermediateTensors
30 -from vllm.utils.jsontree import json_map_leaves
30 from vllm.utils.tensor_schema import TensorSchema, TensorShape
31
32 from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
@@ -350,29 +349,11 @@ class AyaVisionForConditionalGeneration(nn.Module, SupportsMultiModal,
349 self,
350 vision_tower: SiglipVisionModel,
351 pixel_values: torch.Tensor,
353 - **kwargs,
352 ) -> Union[torch.Tensor, tuple[torch.Tensor, ...]]:
355 - target_dtype: torch.dtype = \
356 - vision_tower.get_input_embeddings().weight.dtype
357 - image_features: Union[torch.Tensor, tuple[torch.Tensor, ...]] = \
358 - vision_tower(pixel_values.to(dtype=target_dtype), **kwargs)
359 -
360 - def select_features(leaf: torch.Tensor):
361 - return self._select_image_features(
362 - leaf,
363 - strategy=self.config.vision_feature_select_strategy,
364 - )
365 -
366 - return json_map_leaves(select_features, image_features)
367 -
368 - def _select_image_features(self, image_features: torch.Tensor, *,
369 - strategy: str) -> torch.Tensor:
370 - if strategy == "default":
371 - return image_features[:, 1:]
372 - elif strategy == "full":
373 - return image_features
374 -
375 - raise ValueError(f"Unexpected select feature strategy: {strategy}")
353 + return vision_tower(
354 + pixel_values.to(dtype=vision_tower.dtype),
355 + feature_select_strategy=self.config.vision_feature_select_strategy,
356 + )
357
358 def _process_image_input(self, image_input: AyaVisionImagePixelInputs,
359 **kwargs) -> list[torch.Tensor]:
vllm/model_executor/models/clip.py
+21 -10
@@ -19,7 +19,8 @@ from vllm.model_executor.layers.quantization import QuantizationConfig
19 from vllm.model_executor.model_loader.weight_utils import default_weight_loader
20 from vllm.model_executor.models.interfaces import SupportsQuant
21
22 -from .vision import VisionEncoderInfo, resolve_visual_encoder_outputs
22 +from .vision import (VisionEncoderInfo, VisionFeatureSelectStrategy,
23 + resolve_visual_encoder_outputs)
24
25
26 class CLIPEncoderInfo(VisionEncoderInfo[CLIPVisionConfig]):
@@ -308,24 +309,29 @@ class CLIPVisionTransformer(nn.Module):
309 def forward(
310 self,
311 pixel_values: torch.Tensor,
311 - feature_sample_layers: Optional[list[int]] = None,
312 + *,
313 + select_layers: Optional[list[int]] = None,
314 + feature_select_strategy: Optional[VisionFeatureSelectStrategy] = None,
315 ) -> torch.Tensor:
316
317 hidden_states = self.embeddings(pixel_values)
318 hidden_states = self.pre_layrnorm(hidden_states)
319
317 - return_all_hidden_states = feature_sample_layers is not None
318 -
320 # Produces either the last layer output or all of the hidden states,
320 - # depending on if we have feature_sample_layers or not
321 + # depending on if we have select_layers or not
322 encoder_outputs = self.encoder(
323 inputs_embeds=hidden_states,
323 - return_all_hidden_states=return_all_hidden_states)
324 + return_all_hidden_states=select_layers is not None,
325 + )
326
327 # Handle post-norm (if applicable) and stacks feature layers if needed
328 encoder_outputs = resolve_visual_encoder_outputs(
327 - encoder_outputs, feature_sample_layers, self.post_layernorm,
328 - self.config.num_hidden_layers)
329 + encoder_outputs,
330 + self.post_layernorm,
331 + select_layers=select_layers,
332 + max_possible_layers=self.config.num_hidden_layers,
333 + feature_select_strategy=feature_select_strategy,
334 + )
335
336 return encoder_outputs
337
@@ -355,9 +361,14 @@ class CLIPVisionModel(nn.Module, SupportsQuant):
361 def forward(
362 self,
363 pixel_values: torch.Tensor,
358 - feature_sample_layers: Optional[list[int]] = None,
364 + select_layers: Optional[list[int]] = None,
365 + feature_select_strategy: Optional[VisionFeatureSelectStrategy] = None,
366 ) -> torch.Tensor:
360 - return self.vision_model(pixel_values, feature_sample_layers)
367 + return self.vision_model(
368 + pixel_values,
369 + select_layers=select_layers,
370 + feature_select_strategy=feature_select_strategy,
371 + )
372
373 @property
374 def device(self):
vllm/model_executor/models/llava.py
+4 -21
@@ -33,7 +33,6 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
33 PromptUpdateDetails)
34 from vllm.multimodal.profiling import BaseDummyInputsBuilder
35 from vllm.sequence import IntermediateTensors
36 -from vllm.utils.jsontree import json_map_leaves
36 from vllm.utils.tensor_schema import TensorSchema, TensorShape
37
38 from .clip import CLIPVisionModel
@@ -604,16 +603,6 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
603
604 raise AssertionError("This line should be unreachable.")
605
607 - def _select_image_features(self, image_features: torch.Tensor, *,
608 - strategy: str) -> torch.Tensor:
609 - # Copied from https://github.com/huggingface/transformers/blob/39c3c0a72af6fbda5614dde02ff236069bb79827/src/transformers/models/llava/modeling_llava.py#L421 # noqa
610 - if strategy == "default":
611 - return image_features[:, 1:]
612 - elif strategy == "full":
613 - return image_features
614 -
615 - raise ValueError(f"Unexpected select feature strategy: {strategy}")
616 -
606 def _image_pixels_to_features(
607 self,
608 vision_tower: Union[CLIPVisionModel, SiglipVisionModel,
@@ -622,16 +611,10 @@ class LlavaForConditionalGeneration(nn.Module, SupportsMultiModal, SupportsPP):
611 ) -> Union[torch.Tensor, tuple[torch.Tensor, ...]]:
612 # NOTE: we skip the step to select the vision feature layer since
613 # this is already done inside the vision tower
625 - image_features: Union[torch.Tensor, tuple[torch.Tensor, ...]] = \
626 - vision_tower(pixel_values)
627 -
628 - def select_features(leaf: torch.Tensor):
629 - return self._select_image_features(
630 - leaf,
631 - strategy=self.config.vision_feature_select_strategy,
632 - )
633 -
634 - return json_map_leaves(select_features, image_features)
614 + return vision_tower(
615 + pixel_values,
616 + feature_select_strategy=self.config.vision_feature_select_strategy,
617 + )
618
619 def _process_image_pixels(
620 self,
vllm/model_executor/models/llava_next.py
+6 -19
@@ -235,12 +235,12 @@ class LlavaNextForConditionalGeneration(nn.Module, SupportsMultiModal,
235 # Determine the layer up to which we will initialize the vision tower
236 if isinstance(vision_feature_layer, int):
237 vision_hidden_size = config.vision_config.hidden_size
238 - self.feature_sample_layers = None
238 + self.select_layers = None
239 # Used for multimodal granite models to control encoder outputs
240 elif isinstance(vision_feature_layer, (list, tuple)):
241 vision_hidden_size = config.vision_config.hidden_size * len(
242 vision_feature_layer)
243 - self.feature_sample_layers = vision_feature_layer
243 + self.select_layers = vision_feature_layer
244 else:
245 raise TypeError(
246 f"vision_layer_feature type: {type(vision_feature_layer)}"
@@ -312,30 +312,17 @@ class LlavaNextForConditionalGeneration(nn.Module, SupportsMultiModal,
312
313 raise AssertionError("This line should be unreachable.")
314
315 - def _select_image_features(self, image_features: torch.Tensor, *,
316 - strategy: str) -> torch.Tensor:
317 - # Copied from https://github.com/huggingface/transformers/blob/39c3c0a72af6fbda5614dde02ff236069bb79827/src/transformers/models/llava/modeling_llava.py#L421 # noqa
318 - if strategy == "default":
319 - return image_features[:, 1:]
320 - elif strategy == "full":
321 - return image_features
322 -
323 - raise ValueError(f"Unexpected select feature strategy: {strategy}")
324 -
315 def _image_pixels_to_features(
316 self,
317 vision_tower: Union[CLIPVisionModel, SiglipVisionModel],
318 pixel_values: torch.Tensor,
319 ) -> torch.Tensor:
330 -
320 # NOTE: we skip the step to select the vision feature layer since
321 # this is already done inside the vision tower
333 - image_features = vision_tower(
334 - pixel_values, feature_sample_layers=self.feature_sample_layers)
335 -
336 - return self._select_image_features(
337 - image_features,
338 - strategy=self.config.vision_feature_select_strategy,
322 + return vision_tower(
323 + pixel_values,
324 + select_layers=self.select_layers,
325 + feature_select_strategy=self.config.vision_feature_select_strategy,
326 )
327
328 # Based on: https://github.com/haotian-liu/LLaVA/blob/main/llava/model/llava_arch.py
vllm/model_executor/models/llava_next_video.py
+3 -14
@@ -349,27 +349,16 @@ class LlavaNextVideoForConditionalGeneration(nn.Module, SupportsMultiModal,
349 "w": expected_w,
350 })
351
352 - def _select_image_features(self, image_features: torch.Tensor, *,
353 - strategy: str) -> torch.Tensor:
354 - if strategy == "default":
355 - return image_features[:, 1:]
356 - elif strategy == "full":
357 - return image_features
358 -
359 - raise ValueError(f"Unexpected select feature strategy: {strategy}")
360 -
352 def _video_pixels_to_features(
353 self,
354 vision_tower: Union[CLIPVisionModel, SiglipVisionModel],
355 pixel_values: torch.Tensor,
356 ) -> torch.Tensor:
366 -
357 # NOTE: we skip the step to select the vision feature layer since
358 # this is already done inside the vision tower
369 - image_features = vision_tower(pixel_values)
370 - image_features = self._select_image_features(
371 - image_features,
372 - strategy=self.config.vision_feature_select_strategy,
359 + image_features = vision_tower(
360 + pixel_values,
361 + feature_select_strategy=self.config.vision_feature_select_strategy,
362 )
363 image_features = self.vision_resampler(image_features)
364 image_features = self.multi_modal_projector(image_features)
vllm/model_executor/models/llava_onevision.py
+6 -19
@@ -577,27 +577,16 @@ class LlavaOnevisionForConditionalGeneration(nn.Module, SupportsMultiModal,
577
578 return mm_input_by_modality
579
580 - def _select_image_features(self, image_features: torch.Tensor, *,
581 - strategy: str) -> torch.Tensor:
582 - if strategy == "default":
583 - return image_features[:, 1:]
584 - elif strategy == "full":
585 - return image_features
586 -
587 - raise ValueError(f"Unexpected select feature strategy: {strategy}")
588 -
580 def _image_pixels_to_features(
581 self,
582 vision_tower: Union[CLIPVisionModel, SiglipVisionModel],
583 pixel_values: torch.Tensor,
584 ) -> torch.Tensor:
594 -
585 # NOTE: we skip the step to select the vision feature layer since
586 # this is already done inside the vision tower
597 - image_features = vision_tower(pixel_values)
598 - return self._select_image_features(
599 - image_features,
600 - strategy=self.config.vision_feature_select_strategy,
587 + return vision_tower(
588 + pixel_values,
589 + feature_select_strategy=self.config.vision_feature_select_strategy,
590 )
591
592 # Based on: https://github.com/haotian-liu/LLaVA/blob/main/llava/model/llava_arch.py
@@ -750,13 +739,11 @@ class LlavaOnevisionForConditionalGeneration(nn.Module, SupportsMultiModal,
739 vision_tower: Union[CLIPVisionModel, SiglipVisionModel],
740 pixel_values: torch.Tensor,
741 ) -> torch.Tensor:
753 -
742 # NOTE: we skip the step to select the vision feature layer since
743 # this is already done inside the vision tower
756 - video_features = vision_tower(pixel_values)
757 - video_features = self._select_image_features(
758 - video_features,
759 - strategy=self.config.vision_feature_select_strategy,
744 + video_features = vision_tower(
745 + pixel_values,
746 + feature_select_strategy=self.config.vision_feature_select_strategy,
747 )
748 video_features = self.multi_modal_projector(video_features)
749 video_features = self.apply_pooling(video_features)
vllm/model_executor/models/minimax_vl_01.py
+4 -20
@@ -17,7 +17,6 @@ from vllm.model_executor.layers.quantization import QuantizationConfig
17 from vllm.multimodal import MULTIMODAL_REGISTRY
18 from vllm.multimodal.inputs import MultiModalFieldConfig
19 from vllm.sequence import IntermediateTensors
20 -from vllm.utils.jsontree import json_map_leaves
20 from vllm.utils.tensor_schema import TensorSchema, TensorShape
21
22 from .clip import CLIPVisionModel
@@ -221,15 +220,6 @@ class MiniMaxVL01ForConditionalGeneration(nn.Module, SupportsMultiModal,
220 def get_language_model(self) -> torch.nn.Module:
221 return self.language_model
222
224 - def _select_image_features(self, image_features: torch.Tensor, *,
225 - strategy: str) -> torch.Tensor:
226 - if strategy == "default":
227 - return image_features[:, 1:]
228 - elif strategy == "full":
229 - return image_features
230 -
231 - raise ValueError(f"Unexpected select feature strategy: {strategy}")
232 -
223 def _image_pixels_to_features(
224 self,
225 vision_tower: Union[CLIPVisionModel, SiglipVisionModel,
@@ -238,16 +228,10 @@ class MiniMaxVL01ForConditionalGeneration(nn.Module, SupportsMultiModal,
228 ) -> Union[torch.Tensor, tuple[torch.Tensor, ...]]:
229 # NOTE: we skip the step to select the vision feature layer since
230 # this is already done inside the vision tower
241 - image_features: tuple[torch.Tensor, ...] = \
242 - tuple(vision_tower(p) for p in pixel_values)
243 -
244 - def select_features(leaf: torch.Tensor):
245 - return self._select_image_features(
246 - leaf,
247 - strategy=self.config.vision_feature_select_strategy,
248 - )
249 -
250 - return json_map_leaves(select_features, image_features)
231 + feature_select_strategy = self.config.vision_feature_select_strategy
232 + return tuple(
233 + vision_tower(p, feature_select_strategy=feature_select_strategy)
234 + for p in pixel_values)
235
236 # adapted from https://huggingface.co/MiniMaxAI/MiniMax-VL-01/blob/main/modeling_minimax_vl_01.py#L616-L631
237 def pack_image_features(self, image_features: list[torch.Tensor],
vllm/model_executor/models/pixtral.py
+15 -7
@@ -51,7 +51,8 @@ from vllm.utils.tensor_schema import TensorSchema, TensorShape
51
52 from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsPP
53 from .utils import flatten_bn, init_vllm_registered_model, maybe_prefix
54 -from .vision import VisionEncoderInfo, resolve_visual_encoder_outputs
54 +from .vision import (VisionEncoderInfo, VisionFeatureSelectStrategy,
55 + resolve_visual_encoder_outputs)
56
57 try:
58 from xformers import ops as xops
@@ -1218,7 +1219,9 @@ class PixtralHFVisionModel(nn.Module):
1219 def forward(
1220 self,
1221 pixel_values: list[torch.Tensor],
1221 - feature_sample_layers: Optional[list[int]] = None,
1222 + *,
1223 + select_layers: Optional[list[int]] = None,
1224 + feature_select_strategy: Optional[VisionFeatureSelectStrategy] = None,
1225 ) -> tuple[torch.Tensor, ...]:
1226 """
1227 Args:
@@ -1226,7 +1229,7 @@ class PixtralHFVisionModel(nn.Module):
1229 in pixel_values. This means it will be a list of tensors
1230 because multiple requests batched can have multiple images,
1231 each with their own shape potentially
1229 - feature_sample_layers: Layer indices whose features should be
1232 + select_layers: Layer indices whose features should be
1233 concatenated and used as the visual encoder output. If none
1234 are provided, the last layer is used.
1235
@@ -1267,15 +1270,20 @@ class PixtralHFVisionModel(nn.Module):
1270 [p.shape[-2] * p.shape[-1] for p in patch_embeds_list],
1271 patch_embeds)
1272
1270 - return_all_hidden_states = feature_sample_layers is not None
1273 out = self.transformer(
1274 patch_embeds,
1275 attention_mask,
1276 position_embedding,
1275 - return_all_hidden_states=return_all_hidden_states)
1277 + return_all_hidden_states=select_layers is not None,
1278 + )
1279
1277 - out = resolve_visual_encoder_outputs(out, feature_sample_layers, None,
1278 - self.config.num_hidden_layers)
1280 + out = resolve_visual_encoder_outputs(
1281 + out,
1282 + None,
1283 + select_layers=select_layers,
1284 + max_possible_layers=self.config.num_hidden_layers,
1285 + feature_select_strategy=feature_select_strategy,
1286 + )
1287
1288 # squeeze dim 0 and split into separate tensors for each image
1289 return torch.split(out.squeeze(0), embed_sizes)
vllm/model_executor/models/siglip.py
+22 -12
@@ -23,7 +23,8 @@ from vllm.model_executor.layers.vocab_parallel_embedding import (
23 from vllm.model_executor.model_loader.weight_utils import (
24 default_weight_loader, maybe_remap_kv_scale_name)
25
26 -from .vision import VisionEncoderInfo, resolve_visual_encoder_outputs
26 +from .vision import (VisionEncoderInfo, VisionFeatureSelectStrategy,
27 + resolve_visual_encoder_outputs)
28
29
30 class SiglipEncoderInfo(VisionEncoderInfo[SiglipVisionConfig]):
@@ -415,28 +416,31 @@ class SiglipVisionTransformer(nn.Module):
416 def forward(
417 self,
418 pixel_values: torch.Tensor,
418 - interpolate_pos_encoding: bool = True,
419 - feature_sample_layers: Optional[list[int]] = None,
419 + *,
420 + interpolate_pos_encoding: bool = False,
421 + select_layers: Optional[list[int]] = None,
422 + feature_select_strategy: Optional[VisionFeatureSelectStrategy] = None,
423 ) -> torch.Tensor:
421 -
424 hidden_states = self.embeddings(
425 pixel_values,
426 interpolate_pos_encoding=interpolate_pos_encoding,
427 )
428
427 - return_all_hidden_states = feature_sample_layers is not None
428 -
429 # Produces either the last layer output or all of the hidden states,
430 - # depending on if we have feature_sample_layers or not
430 + # depending on if we have select_layers or not
431 encoder_outputs = self.encoder(
432 inputs_embeds=hidden_states,
433 - return_all_hidden_states=return_all_hidden_states,
433 + return_all_hidden_states=select_layers is not None,
434 )
435
436 # Handle post-norm (if applicable) and stacks feature layers if needed
437 encoder_outputs = resolve_visual_encoder_outputs(
438 - encoder_outputs, feature_sample_layers, self.post_layernorm,
439 - self.config.num_hidden_layers)
438 + encoder_outputs,
439 + self.post_layernorm,
440 + select_layers=select_layers,
441 + max_possible_layers=self.config.num_hidden_layers,
442 + feature_select_strategy=feature_select_strategy,
443 + )
444
445 # TODO: add this back when pooled_output is used in inference.
446 # if self.use_head:
@@ -471,16 +475,22 @@ class SiglipVisionModel(nn.Module):
475 def get_input_embeddings(self) -> nn.Module:
476 return self.vision_model.embeddings.patch_embedding
477
478 + @property
479 + def dtype(self):
480 + return self.get_input_embeddings().weight.dtype
481 +
482 def forward(
483 self,
484 pixel_values: torch.Tensor,
485 interpolate_pos_encoding: bool = False,
478 - feature_sample_layers: Optional[list[int]] = None,
486 + select_layers: Optional[list[int]] = None,
487 + feature_select_strategy: Optional[VisionFeatureSelectStrategy] = None,
488 ) -> torch.Tensor:
489 return self.vision_model(
490 pixel_values=pixel_values,
491 interpolate_pos_encoding=interpolate_pos_encoding,
483 - feature_sample_layers=feature_sample_layers,
492 + select_layers=select_layers,
493 + feature_select_strategy=feature_select_strategy,
494 )
495
496 def load_weights(self, weights: Iterable[tuple[str,
vllm/model_executor/models/tarsier.py
+4 -19
@@ -33,7 +33,6 @@ from vllm.multimodal.processing import (BaseMultiModalProcessor,
33 PromptReplacement, PromptUpdate)
34 from vllm.multimodal.profiling import BaseDummyInputsBuilder
35 from vllm.sequence import IntermediateTensors
36 -from vllm.utils.jsontree import json_map_leaves
36 from vllm.utils.tensor_schema import TensorSchema, TensorShape
37
38 from .clip import CLIPVisionModel
@@ -476,30 +475,16 @@ class TarsierForConditionalGeneration(nn.Module, SupportsMultiModal,
475
476 raise AssertionError("This line should be unreachable.")
477
479 - def _select_image_features(self, image_features: torch.Tensor, *,
480 - strategy: str) -> torch.Tensor:
481 - if strategy == "default":
482 - return image_features[:, 1:]
483 - elif strategy == "full":
484 - return image_features
485 - raise ValueError(f"Unexpected select feature strategy: {strategy}")
486 -
478 def _image_pixels_to_features(
479 self,
480 vision_tower: Union[CLIPVisionModel, SiglipVisionModel],
481 pixel_values: Union[torch.Tensor, list[torch.Tensor]],
482 ) -> Union[torch.Tensor, tuple[torch.Tensor, ...]]:
483 # From vLLM LLaVA, vision tower output handling
493 - image_hidden_states: Union[torch.Tensor, tuple[torch.Tensor, ...]] = \
494 - vision_tower(pixel_values)
495 -
496 - def select_features_fn(leaf: torch.Tensor):
497 - return self._select_image_features(
498 - leaf,
499 - strategy=self.config.vision_feature_select_strategy,
500 - )
501 -
502 - return json_map_leaves(select_features_fn, image_hidden_states)
484 + return vision_tower(
485 + pixel_values,
486 + feature_select_strategy=self.config.vision_feature_select_strategy,
487 + )
488
489 def _add_tarsier_split_tokens(
490 self, projected_image_features: torch.Tensor) -> torch.Tensor:
vllm/model_executor/models/vision.py
+60 -10
@@ -4,10 +4,12 @@
4 import itertools
5 import math
6 from abc import ABC, abstractmethod
7 -from typing import Final, Generic, Literal, Optional, Protocol, TypeVar, Union
7 +from typing import (Callable, Final, Generic, Literal, Optional, Protocol,
8 + TypeVar, Union)
9
10 import torch
11 from transformers import PretrainedConfig
12 +from typing_extensions import assert_never
13
14 from vllm.distributed import (get_tensor_model_parallel_rank,
15 get_tensor_model_parallel_world_size,
@@ -86,11 +88,39 @@ def get_vit_attn_backend(head_size: int, dtype: torch.dtype) -> _Backend:
88 return current_platform.get_vit_attn_backend(head_size, dtype)
89
90
91 +VisionFeatureSelectStrategy = Union[
92 + Literal["class", "default", "full"],
93 + Callable[[torch.Tensor], torch.Tensor],
94 +]
95 +
96 +
97 +def _get_vision_feature_selector(
98 + strategy: VisionFeatureSelectStrategy,
99 +) -> Callable[[torch.Tensor], torch.Tensor]:
100 + if callable(strategy):
101 + return strategy
102 +
103 + # https://github.com/huggingface/transformers/blob/cd74917ffc3e8f84e4a886052c5ab32b7ac623cc/src/transformers/models/clip/modeling_clip.py#L762
104 + if strategy == "class":
105 + return lambda feats: feats[:, 0, :]
106 +
107 + # https://github.com/huggingface/transformers/blob/4a02bc7004285bdb12cc033e87ad2578ce2fa900/src/transformers/models/llava/modeling_llava.py#L196
108 + if strategy == "default":
109 + return lambda feats: feats[:, 1:, :]
110 +
111 + if strategy == "full":
112 + return lambda feats: feats
113 +
114 + assert_never(strategy)
115 +
116 +
117 def resolve_visual_encoder_outputs(
118 encoder_outputs: Union[torch.Tensor, list[torch.Tensor]],
91 - feature_sample_layers: Optional[list[int]],
119 post_layer_norm: Optional[torch.nn.LayerNorm],
93 - max_possible_layers: int,
120 + *,
121 + select_layers: Optional[list[int]] = None,
122 + max_possible_layers: Optional[int] = None,
123 + feature_select_strategy: Optional[VisionFeatureSelectStrategy] = None,
124 ) -> torch.Tensor:
125 """Given the outputs a visual encoder module that may correspond to the
126 output of the last layer, or a list of hidden states to be stacked,
@@ -98,17 +128,32 @@ def resolve_visual_encoder_outputs(
128
129 Args:
130 encoder_outputs: Output of encoder's last layer or all hidden states.
101 - feature_sample_layers: Optional layer indices to grab from the encoder
102 - outputs; if provided, encoder outputs must be a list.
131 post_layer_norm: Post norm to apply to the output of the encoder.
132 + select_layers: Optional layer indices to grab from the encoder
133 + outputs; if provided, encoder outputs must be a list.
134 max_possible_layers: Total layers in the fully loaded visual encoder.
105 -
135 + feature_select_strategy: Defines how to select the hidden states
136 + from each layer.
137 """
107 - if feature_sample_layers is None:
138 + if select_layers is None:
139 + if not isinstance(encoder_outputs, torch.Tensor):
140 + raise ValueError("Expected only a single encoder output when "
141 + "`select_layers` is not provided")
142 +
143 + if feature_select_strategy is not None:
144 + select_features = _get_vision_feature_selector(
145 + feature_select_strategy)
146 + encoder_outputs = select_features(encoder_outputs)
147 +
148 if post_layer_norm is not None:
149 return post_layer_norm(encoder_outputs)
150 +
151 return encoder_outputs
152
153 + if max_possible_layers is None:
154 + raise ValueError("`max_possible_layers` must be provided "
155 + "alongside `select_layers`")
156 +
157 # Get the hidden states corresponding to the layer indices.
158 # Negative values are relative to the full visual encoder,
159 # so offset them depending on how many layers were loaded.
@@ -120,13 +165,18 @@ def resolve_visual_encoder_outputs(
165 hs_pool = [
166 encoder_outputs[layer_idx]
167 if layer_idx >= 0 else encoder_outputs[layer_idx + offset]
123 - for layer_idx in feature_sample_layers
168 + for layer_idx in select_layers
169 ]
170
171 + if feature_select_strategy is not None:
172 + select_features = _get_vision_feature_selector(feature_select_strategy)
173 + hs_pool = [select_features(hs) for hs in hs_pool]
174 +
175 # Apply post-norm on the final hidden state if we are using it
127 - uses_last_layer = feature_sample_layers[-1] in (len(hs_pool) - 1, -1)
176 + uses_last_layer = select_layers[-1] in (max_possible_layers - 1, -1)
177 if post_layer_norm is not None and uses_last_layer:
129 - hs_pool[-1] = post_layer_norm(encoder_outputs)
178 + hs_pool[-1] = post_layer_norm(hs_pool[-1])
179 +
180 return torch.cat(hs_pool, dim=-1)
181
182