main
py 1,001 lines 32.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 from collections.abc import Callable, Iterable, Mapping, Sequence
4 from functools import cached_property
5 from typing import Annotated, Literal
6
7 import torch
8 import torch.nn as nn
9 from transformers import (
10 BatchFeature,
11 CLIPConfig,
12 CLIPProcessor,
13 CLIPTextConfig,
14 CLIPVisionConfig,
15 )
16
17 from vllm.config import VllmConfig
18 from vllm.config.multimodal import MultiModalDummyOptions
19 from vllm.distributed import divide, get_tensor_model_parallel_world_size
20 from vllm.inputs import MultiModalDataDict, MultiModalInput
21 from vllm.model_executor.layers.activation import get_act_fn
22 from vllm.model_executor.layers.attention import Attention, MMEncoderAttention
23 from vllm.model_executor.layers.conv import Conv2dLayer
24 from vllm.model_executor.layers.linear import (
25 ColumnParallelLinear,
26 QKVParallelLinear,
27 RowParallelLinear,
28 )
29 from vllm.model_executor.layers.pooler import DispatchPooler
30 from vllm.model_executor.layers.quantization import QuantizationConfig
31 from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
32 from vllm.model_executor.models.interfaces import SupportsQuant
33 from vllm.multimodal import MULTIMODAL_REGISTRY
34 from vllm.multimodal.inputs import (
35 MultiModalFieldConfig,
36 MultiModalKwargsItems,
37 )
38 from vllm.multimodal.parse import (
39 ImageProcessorItems,
40 ImageSize,
41 MultiModalDataItems,
42 )
43 from vllm.multimodal.processing import (
44 BaseDummyInputsBuilder,
45 BaseMultiModalProcessor,
46 BaseProcessingInfo,
47 ProcessorInputs,
48 PromptIndexTargets,
49 PromptReplacement,
50 PromptUpdate,
51 TimingContext,
52 )
53 from vllm.sequence import IntermediateTensors
54 from vllm.utils.tensor_schema import TensorSchema, TensorShape
55
56 from .interfaces import MultiModalEmbeddings, SupportsMultiModal
57 from .interfaces_base import default_pooling_type
58 from .utils import AutoWeightsLoader, WeightsMapper, maybe_prefix
59 from .vision import (
60 VisionEncoderInfo,
61 VisionFeatureSelectStrategy,
62 VisionFeatureSelectStrategyStr,
63 get_num_selected_vision_tokens,
64 is_vit_use_data_parallel,
65 resolve_visual_encoder_outputs,
66 )
67
68
69 class CLIPImagePixelInputs(TensorSchema):
70 """Dimensions:
71 - bn: Batch size * number of images
72 - c: Number of channels (3)
73 - h: Height of each image
74 - w: Width of each image
75 """
76
77 type: Literal["pixel_values"]
78 data: Annotated[torch.Tensor, TensorShape("bn", 3, "h", "w")]
79
80
81 class CLIPEncoderInfo(VisionEncoderInfo[CLIPVisionConfig]):
82 def get_num_image_tokens(
83 self,
84 *,
85 image_width: int,
86 image_height: int,
87 ) -> int:
88 return self.get_patch_grid_length() ** 2 + 1
89
90 def get_image_size(self) -> int:
91 return self.vision_config.image_size
92
93 def get_patch_size(self) -> int:
94 return self.vision_config.patch_size
95
96 def get_patch_grid_length(self) -> int:
97 image_size, patch_size = self.get_image_size(), self.get_patch_size()
98 assert image_size % patch_size == 0
99 return image_size // patch_size
100
101
102 _POOLING_TYPE_TO_STRATEGY: dict[str, VisionFeatureSelectStrategyStr] = {
103 "MEAN": "full",
104 "ALL": "full",
105 "CLS": "class",
106 # This lets us use the same pooling type for both text and image
107 "LAST": "class",
108 }
109
110
111 def _get_vision_feature_select_strategy(pooling_type: str):
112 try:
113 return _POOLING_TYPE_TO_STRATEGY[pooling_type]
114 except KeyError:
115 raise ValueError(
116 f"No feature selection strategy is defined for "
117 f"pooling_type: {pooling_type!r}"
118 ) from None
119
120
121 def dual_encoder_has_text_tokens(
122 has_mm_embeddings: bool,
123 is_multimodal: torch.Tensor | None,
124 ) -> bool:
125 """Whether a dual-encoder pooling batch still contains text tokens.
126
127 ``embed_input_ids`` used to treat "the batch has any image embeddings" as
128 "the whole batch is vision-only". Mixed image+text batches then skipped
129 the text encoder, so completion-style text embeddings collapsed (issue
130 #53091). Honor the per-token ``is_multimodal`` mask instead.
131 """
132 if not has_mm_embeddings or is_multimodal is None:
133 return not has_mm_embeddings
134 return bool((~is_multimodal).any().item())
135
136
137 def merge_dual_encoder_text_and_vision(
138 text_features: torch.Tensor,
139 vision_embeds: torch.Tensor,
140 is_multimodal: torch.Tensor | None,
141 ) -> torch.Tensor:
142 """Restore vision embeddings on multimodal tokens after the text encoder.
143
144 The model runner builds ``is_multimodal`` on CPU; text and vision features
145 are on the compute device.
146 """
147 if is_multimodal is None or not bool(is_multimodal.any().item()):
148 return text_features
149 mm_mask = is_multimodal.to(device=vision_embeds.device, non_blocking=True)
150 return torch.where(mm_mask.unsqueeze(-1), vision_embeds, text_features)
151
152
153 class CLIPProcessingInfo(BaseProcessingInfo):
154 def get_hf_config(self):
155 return self.ctx.get_hf_config(CLIPConfig)
156
157 def get_vision_encoder_info(self):
158 return CLIPEncoderInfo(self.get_hf_config())
159
160 def get_hf_processor(self, **kwargs: object):
161 return self.ctx.get_hf_processor(CLIPProcessor, **kwargs)
162
163 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
164 return {"image": 1}
165
166 def get_num_image_tokens(
167 self,
168 *,
169 image_width: int,
170 image_height: int,
171 ) -> int:
172 vision_encoder_info = self.get_vision_encoder_info()
173
174 pooler_config = self.ctx.model_config.pooler_config
175 assert pooler_config is not None
176 assert pooler_config.seq_pooling_type is not None
177
178 return get_num_selected_vision_tokens(
179 vision_encoder_info.get_num_image_tokens(
180 image_width=image_width,
181 image_height=image_height,
182 ),
183 _get_vision_feature_select_strategy(pooler_config.seq_pooling_type),
184 )
185
186 def get_image_size_with_most_features(self) -> ImageSize:
187 vision_encoder_info = self.get_vision_encoder_info()
188 width = height = vision_encoder_info.get_image_size()
189 return ImageSize(width=width, height=height)
190
191 def get_max_image_tokens(self) -> int:
192 target_width, target_height = self.get_image_size_with_most_features()
193
194 return self.get_num_image_tokens(
195 image_width=target_width,
196 image_height=target_height,
197 )
198
199
200 class CLIPDummyInputsBuilder(BaseDummyInputsBuilder[CLIPProcessingInfo]):
201 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
202 return ""
203
204 def get_dummy_mm_data(
205 self,
206 seq_len: int,
207 mm_counts: Mapping[str, int],
208 mm_options: MultiModalDummyOptions,
209 ) -> MultiModalDataDict:
210 target_width, target_height = self.info.get_image_size_with_most_features()
211
212 return {
213 "image": self._get_dummy_images(
214 width=target_width,
215 height=target_height,
216 num_images=mm_counts.get("image", 0),
217 overrides=mm_options.get("image"),
218 )
219 }
220
221
222 class CLIPMultiModalProcessor(BaseMultiModalProcessor[CLIPProcessingInfo]):
223 @cached_property
224 def image_token_id(self) -> int:
225 tokenizer = self.info.get_tokenizer()
226 dummy_token_id = 0
227
228 assert dummy_token_id not in tokenizer.all_special_ids
229
230 return dummy_token_id
231
232 def apply(
233 self,
234 inputs: ProcessorInputs,
235 timing_ctx: TimingContext,
236 ) -> MultiModalInput:
237 if inputs.mm_data_items:
238 special_tokens = self.info.get_tokenizer().all_special_ids
239 if all(tok in special_tokens for tok in inputs.prompt):
240 inputs.prompt = []
241 else:
242 raise ValueError(
243 "CLIP accepts text-only or image-only inputs, not both! "
244 "You must pass an image with an empty token prompt."
245 )
246
247 return super().apply(inputs, timing_ctx)
248
249 def _get_mm_fields_config(
250 self,
251 hf_inputs: BatchFeature,
252 hf_processor_mm_kwargs: Mapping[str, object],
253 ) -> Mapping[str, MultiModalFieldConfig]:
254 return dict(pixel_values=MultiModalFieldConfig.batched("image"))
255
256 def _get_prompt_updates(
257 self,
258 mm_items: MultiModalDataItems,
259 hf_processor_mm_kwargs: Mapping[str, object],
260 out_mm_kwargs: MultiModalKwargsItems,
261 ) -> Sequence[PromptUpdate]:
262 image_token_id = self.image_token_id
263
264 def get_replacement(item_idx: int):
265 images = mm_items.get_items("image", ImageProcessorItems)
266 image_size = images.get_image_size(item_idx)
267
268 num_image_tokens = self.info.get_num_image_tokens(
269 image_width=image_size.width,
270 image_height=image_size.height,
271 )
272 return [image_token_id] * num_image_tokens
273
274 return [
275 PromptReplacement(
276 modality="image",
277 target=PromptIndexTargets.start(),
278 replacement=get_replacement,
279 ),
280 ]
281
282
283 # Adapted from: https://github.com/huggingface/transformers/blob/v4.56.2/src/transformers/models/clip/modeling_clip.py
284 class CLIPTextEmbeddings(nn.Module):
285 def __init__(self, config: CLIPTextConfig):
286 super().__init__()
287
288 embed_dim = config.hidden_size
289
290 self.token_embedding = VocabParallelEmbedding(config.vocab_size, embed_dim)
291 self.position_embedding = VocabParallelEmbedding(
292 config.max_position_embeddings, embed_dim
293 )
294
295 def forward(
296 self,
297 input_ids: torch.Tensor | None,
298 position_ids: torch.Tensor,
299 inputs_embeds: torch.Tensor | None = None,
300 ) -> torch.Tensor:
301 if inputs_embeds is None:
302 if input_ids is None:
303 raise ValueError(
304 "Either `input_ids` or `input_embeds` must be provided"
305 )
306
307 inputs_embeds = self.token_embedding(input_ids)
308
309 position_embeddings = self.position_embedding(position_ids)
310 embeddings = inputs_embeds + position_embeddings
311
312 return embeddings
313
314
315 class CLIPVisionEmbeddings(nn.Module):
316 def __init__(self, config: CLIPVisionConfig):
317 super().__init__()
318 self.config = config
319 self.embed_dim = config.hidden_size
320 self.image_size = config.image_size
321 self.patch_size = config.patch_size
322 assert self.image_size % self.patch_size == 0
323
324 self.class_embedding = nn.Parameter(torch.randn(self.embed_dim))
325
326 self.patch_embedding = Conv2dLayer(
327 in_channels=config.num_channels,
328 out_channels=self.embed_dim,
329 kernel_size=self.patch_size,
330 stride=self.patch_size,
331 bias=False,
332 )
333
334 self.num_patches = (self.image_size // self.patch_size) ** 2
335 self.num_positions = self.num_patches + 1
336 self.position_embedding = nn.Embedding(self.num_positions, self.embed_dim)
337 self.register_buffer(
338 "position_ids",
339 torch.arange(self.num_positions).expand((1, -1)),
340 persistent=False,
341 )
342
343 def forward(self, pixel_values: torch.Tensor) -> torch.Tensor:
344 batch_size = pixel_values.shape[0]
345 target_dtype = self.patch_embedding.weight.dtype
346 patch_embeds = self.patch_embedding(
347 pixel_values.to(dtype=target_dtype)
348 ) # shape = [*, width, grid, grid]
349 patch_embeds = patch_embeds.flatten(2).transpose(1, 2)
350
351 class_embeds = self.class_embedding.expand(batch_size, 1, -1)
352 embeddings = torch.cat([class_embeds, patch_embeds], dim=1)
353 embeddings = embeddings + self.position_embedding(self.position_ids)
354
355 return embeddings
356
357
358 class CLIPAttention(nn.Module):
359 def __init__(
360 self,
361 config: CLIPTextConfig | CLIPVisionConfig,
362 quant_config: QuantizationConfig | None = None,
363 *,
364 prefix: str = "",
365 attn_cls: type[Attention] | type[MMEncoderAttention],
366 ) -> None:
367 super().__init__()
368
369 self.config = config
370 self.embed_dim = config.hidden_size
371 self.num_heads = config.num_attention_heads
372 self.head_dim = self.embed_dim // self.num_heads
373 if self.head_dim * self.num_heads != self.embed_dim:
374 raise ValueError(
375 f"embed_dim must be divisible by num_heads "
376 f"(got `embed_dim`: {self.embed_dim} and "
377 f"`num_heads`: {self.num_heads})."
378 )
379 self.scale = self.head_dim**-0.5
380
381 use_data_parallel = is_vit_use_data_parallel()
382 self.qkv_proj = QKVParallelLinear(
383 hidden_size=self.embed_dim,
384 head_size=self.head_dim,
385 total_num_heads=self.num_heads,
386 quant_config=quant_config,
387 prefix=f"{prefix}.qkv_proj",
388 disable_tp=use_data_parallel,
389 )
390
391 self.out_proj = RowParallelLinear(
392 input_size=self.embed_dim,
393 output_size=self.embed_dim,
394 quant_config=quant_config,
395 prefix=f"{prefix}.out_proj",
396 disable_tp=use_data_parallel,
397 )
398
399 self.tp_size = (
400 1 if use_data_parallel else get_tensor_model_parallel_world_size()
401 )
402 self.num_heads_per_partition = divide(self.num_heads, self.tp_size)
403
404 self.attn = attn_cls(
405 self.num_heads_per_partition,
406 self.head_dim,
407 self.scale,
408 prefix=f"{prefix}.attn",
409 )
410
411 def forward(
412 self,
413 hidden_states: torch.Tensor,
414 ):
415 """Input shape: Batch x Time x Channel."""
416 qkv_states, _ = self.qkv_proj(hidden_states)
417 query_states, key_states, value_states = qkv_states.chunk(3, dim=-1)
418 out = self.attn(query_states, key_states, value_states)
419 attn_output, _ = self.out_proj(out)
420
421 return attn_output, None
422
423
424 class CLIPMLP(nn.Module):
425 def __init__(
426 self,
427 config: CLIPTextConfig | CLIPVisionConfig,
428 quant_config: QuantizationConfig | None = None,
429 prefix: str = "",
430 ) -> None:
431 super().__init__()
432
433 self.config = config
434 use_data_parallel = is_vit_use_data_parallel()
435 self.activation_fn = get_act_fn(config.hidden_act)
436
437 self.fc1 = ColumnParallelLinear(
438 config.hidden_size,
439 config.intermediate_size,
440 bias=True,
441 quant_config=quant_config,
442 prefix=f"{prefix}.fc1",
443 disable_tp=use_data_parallel,
444 )
445 self.fc2 = RowParallelLinear(
446 config.intermediate_size,
447 config.hidden_size,
448 bias=True,
449 quant_config=quant_config,
450 prefix=f"{prefix}.fc2",
451 disable_tp=use_data_parallel,
452 )
453
454 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
455 hidden_states, _ = self.fc1(hidden_states)
456 hidden_states = self.activation_fn(hidden_states)
457 hidden_states, _ = self.fc2(hidden_states)
458
459 return hidden_states
460
461
462 class CLIPEncoderLayer(nn.Module):
463 def __init__(
464 self,
465 config: CLIPTextConfig | CLIPVisionConfig,
466 quant_config: QuantizationConfig | None = None,
467 *,
468 prefix: str = "",
469 attn_cls: type[Attention] | type[MMEncoderAttention],
470 ) -> None:
471 super().__init__()
472
473 self.self_attn = CLIPAttention(
474 config,
475 quant_config=quant_config,
476 prefix=f"{prefix}.self_attn",
477 attn_cls=attn_cls,
478 )
479 self.layer_norm1 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
480 self.mlp = CLIPMLP(
481 config,
482 quant_config=quant_config,
483 prefix=f"{prefix}.mlp",
484 )
485 self.layer_norm2 = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
486
487 def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
488 residual = hidden_states
489
490 hidden_states = self.layer_norm1(hidden_states)
491 hidden_states, _ = self.self_attn(hidden_states=hidden_states)
492 hidden_states = residual + hidden_states
493
494 residual = hidden_states
495 hidden_states = self.layer_norm2(hidden_states)
496 hidden_states = self.mlp(hidden_states)
497 hidden_states = residual + hidden_states
498
499 return hidden_states
500
501
502 class CLIPEncoder(nn.Module):
503 """Transformer encoder consisting of `config.num_hidden_layers` self
504 attention layers. Each layer is a [`CLIPEncoderLayer`].
505
506 Args:
507 config: CLIPConfig
508
509 """
510
511 def __init__(
512 self,
513 config: CLIPTextConfig | CLIPVisionConfig,
514 quant_config: QuantizationConfig | None = None,
515 num_hidden_layers_override: int | None = None,
516 *,
517 prefix: str = "",
518 attn_cls: type[Attention] | type[MMEncoderAttention],
519 ) -> None:
520 super().__init__()
521
522 self.config = config
523
524 if num_hidden_layers_override is None:
525 num_hidden_layers = config.num_hidden_layers
526 else:
527 num_hidden_layers = num_hidden_layers_override
528
529 self.layers = nn.ModuleList(
530 [
531 CLIPEncoderLayer(
532 config=config,
533 quant_config=quant_config,
534 prefix=f"{prefix}.layers.{layer_idx}",
535 attn_cls=attn_cls,
536 )
537 for layer_idx in range(num_hidden_layers)
538 ]
539 )
540
541 def forward(
542 self,
543 inputs_embeds: torch.Tensor,
544 return_all_hidden_states: bool,
545 ) -> torch.Tensor | list[torch.Tensor]:
546 hidden_states_pool = [inputs_embeds]
547 hidden_states = inputs_embeds
548
549 for encoder_layer in self.layers:
550 hidden_states = encoder_layer(hidden_states)
551 if return_all_hidden_states:
552 hidden_states_pool.append(hidden_states)
553 # If we have multiple feature sample layers, we return all hidden
554 # states in order and grab the ones we need by index.
555 if return_all_hidden_states:
556 return hidden_states_pool
557 return hidden_states
558
559
560 class CLIPTextTransformer(nn.Module):
561 hf_to_vllm_mapper = WeightsMapper(
562 orig_to_new_stacked={
563 ".q_proj": (".qkv_proj", "q"),
564 ".k_proj": (".qkv_proj", "k"),
565 ".v_proj": (".qkv_proj", "v"),
566 }
567 )
568
569 def __init__(
570 self,
571 config: CLIPTextConfig,
572 quant_config: QuantizationConfig | None = None,
573 *,
574 prefix: str = "",
575 ) -> None:
576 super().__init__()
577
578 self.config = config
579 embed_dim = config.hidden_size
580
581 self.embeddings = CLIPTextEmbeddings(config)
582
583 self.encoder = CLIPEncoder(
584 config=config,
585 quant_config=quant_config,
586 prefix=f"{prefix}.encoder",
587 attn_cls=Attention,
588 )
589
590 self.final_layer_norm = nn.LayerNorm(
591 embed_dim,
592 eps=config.layer_norm_eps,
593 )
594
595 def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
596 return self.embeddings.token_embedding(input_ids)
597
598 def forward(
599 self,
600 input_ids: torch.Tensor | None,
601 position_ids: torch.Tensor,
602 inputs_embeds: torch.Tensor | None = None,
603 ) -> torch.Tensor:
604 hidden_states = self.embeddings(
605 input_ids=input_ids,
606 position_ids=position_ids,
607 inputs_embeds=inputs_embeds,
608 )
609
610 last_hidden_state = self.encoder(
611 inputs_embeds=hidden_states,
612 return_all_hidden_states=False,
613 )
614 last_hidden_state = self.final_layer_norm(last_hidden_state)
615
616 return last_hidden_state
617
618 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
619 loader = AutoWeightsLoader(self)
620 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
621
622
623 class CLIPVisionTransformer(nn.Module):
624 hf_to_vllm_mapper = WeightsMapper(
625 orig_to_new_stacked={
626 ".q_proj": (".qkv_proj", "q"),
627 ".k_proj": (".qkv_proj", "k"),
628 ".v_proj": (".qkv_proj", "v"),
629 }
630 )
631
632 def __init__(
633 self,
634 config: CLIPVisionConfig,
635 quant_config: QuantizationConfig | None = None,
636 *,
637 num_hidden_layers_override: int | None = None,
638 require_post_norm: bool | None = None,
639 prefix: str = "",
640 ) -> None:
641 super().__init__()
642
643 self.config = config
644 embed_dim = config.hidden_size
645
646 self.embeddings = CLIPVisionEmbeddings(config)
647
648 # NOTE: This typo of "layrnorm" is not fixed on purpose to match
649 # the original transformers code and name of the model weights.
650 self.pre_layrnorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
651
652 self.encoder = CLIPEncoder(
653 config=config,
654 quant_config=quant_config,
655 num_hidden_layers_override=num_hidden_layers_override,
656 prefix=f"{prefix}.encoder",
657 attn_cls=MMEncoderAttention,
658 )
659
660 num_hidden_layers = config.num_hidden_layers
661 if len(self.encoder.layers) > config.num_hidden_layers:
662 raise ValueError(
663 f"The original encoder only has {num_hidden_layers} "
664 f"layers, but you requested {len(self.encoder.layers)} layers."
665 )
666
667 # If possible, skip post_layernorm to conserve memory
668 if require_post_norm is None:
669 require_post_norm = len(self.encoder.layers) == num_hidden_layers
670
671 if require_post_norm:
672 self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
673 else:
674 self.post_layernorm = None
675 self.hf_to_vllm_mapper = self.hf_to_vllm_mapper | WeightsMapper(
676 orig_to_new_prefix={"post_layernorm.": None}
677 )
678
679 @property
680 def dtype(self):
681 return next(self.parameters()).dtype
682
683 @property
684 def device(self):
685 return next(self.parameters()).device
686
687 def forward(
688 self,
689 pixel_values: torch.Tensor,
690 *,
691 select_layers: list[int] | None = None,
692 feature_select_strategy: VisionFeatureSelectStrategy | None = None,
693 ) -> torch.Tensor:
694 hidden_states = self.embeddings(pixel_values)
695 hidden_states = self.pre_layrnorm(hidden_states)
696
697 # Produces either the last layer output or all of the hidden states,
698 # depending on if we have select_layers or not
699 encoder_outputs = self.encoder(
700 inputs_embeds=hidden_states,
701 return_all_hidden_states=select_layers is not None,
702 )
703
704 # Handle post-norm (if applicable) and stacks feature layers if needed
705 encoder_outputs = resolve_visual_encoder_outputs(
706 encoder_outputs,
707 self.post_layernorm,
708 select_layers=select_layers,
709 max_possible_layers=self.config.num_hidden_layers,
710 feature_select_strategy=feature_select_strategy,
711 )
712
713 return encoder_outputs
714
715 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
716 loader = AutoWeightsLoader(self)
717
718 # Drop layers beyond num_hidden_layers_override.
719 def _filter(ws):
720 for name, w in ws:
721 if name.startswith("encoder.layers.") and int(
722 name.split(".")[2]
723 ) >= len(self.encoder.layers):
724 continue
725 yield name, w
726
727 return loader.load_weights(_filter(weights), mapper=self.hf_to_vllm_mapper)
728
729
730 class CLIPVisionModel(nn.Module):
731 def __init__(
732 self,
733 config: CLIPVisionConfig,
734 quant_config: QuantizationConfig | None = None,
735 *,
736 num_hidden_layers_override: int | None = None,
737 require_post_norm: bool | None = None,
738 prefix: str = "",
739 ) -> None:
740 super().__init__()
741
742 self.vision_model = CLIPVisionTransformer(
743 config=config,
744 quant_config=quant_config,
745 num_hidden_layers_override=num_hidden_layers_override,
746 require_post_norm=require_post_norm,
747 prefix=maybe_prefix(prefix, "vision_model"),
748 )
749
750 def forward(
751 self,
752 pixel_values: torch.Tensor,
753 select_layers: list[int] | None = None,
754 feature_select_strategy: VisionFeatureSelectStrategy | None = None,
755 ) -> torch.Tensor:
756 return self.vision_model(
757 pixel_values,
758 select_layers=select_layers,
759 feature_select_strategy=feature_select_strategy,
760 )
761
762 @property
763 def dtype(self):
764 return self.vision_model.dtype
765
766 @property
767 def device(self):
768 return self.vision_model.device
769
770
771 # Assume EOS token corresponds to LAST token in text model
772 @default_pooling_type(seq_pooling_type="LAST")
773 @MULTIMODAL_REGISTRY.register_processor(
774 CLIPMultiModalProcessor,
775 info=CLIPProcessingInfo,
776 dummy_inputs=CLIPDummyInputsBuilder,
777 )
778 class CLIPEmbeddingModel(nn.Module, SupportsMultiModal, SupportsQuant):
779 is_pooling_model = True
780
781 hf_to_vllm_mapper = WeightsMapper(orig_to_new_substr={".position_ids": None})
782
783 packed_modules_mapping = {"qkv_proj": ["q_proj", "k_proj", "v_proj"]}
784
785 @classmethod
786 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
787 if modality.startswith("image"):
788 return None
789
790 raise ValueError("Only image modality is supported")
791
792 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
793 super().__init__()
794
795 config: CLIPConfig = vllm_config.model_config.hf_config
796 quant_config = vllm_config.quant_config
797 multimodal_config = vllm_config.model_config.multimodal_config
798 self.config = config
799 self.multimodal_config = multimodal_config
800
801 text_config = config.text_config
802 vision_config = config.vision_config
803
804 self.projection_dim = config.projection_dim
805 self.text_embed_dim = text_config.hidden_size
806 self.vision_embed_dim = vision_config.hidden_size
807
808 with self._mark_language_model(vllm_config):
809 self.text_model = CLIPTextTransformer(
810 text_config,
811 quant_config=quant_config,
812 prefix=maybe_prefix(prefix, "text_model"),
813 )
814 self.text_projection = nn.Linear(
815 self.text_embed_dim,
816 self.projection_dim,
817 bias=False,
818 )
819
820 with self._mark_tower_model(vllm_config, "image"):
821 self.vision_model = CLIPVisionTransformer(
822 vision_config,
823 quant_config=quant_config,
824 prefix=maybe_prefix(prefix, "vision_model"),
825 )
826 self.visual_projection = nn.Linear(
827 self.vision_embed_dim,
828 self.projection_dim,
829 bias=False,
830 )
831
832 pooler_config = vllm_config.model_config.pooler_config
833 assert pooler_config is not None
834 self.pooler_config = pooler_config
835
836 self.pooler = DispatchPooler.for_embedding(pooler_config)
837
838 # Set in embed_input_ids; consumed by forward.
839 self._has_text_tokens = True
840 self._mm_token_mask: torch.Tensor | None = None
841
842 def get_text_features(
843 self,
844 input_ids: torch.Tensor | None,
845 position_ids: torch.Tensor,
846 inputs_embeds: torch.Tensor | None = None,
847 ) -> torch.Tensor:
848 pooled_output = self.text_model(
849 input_ids=input_ids,
850 position_ids=position_ids,
851 inputs_embeds=inputs_embeds,
852 )
853
854 text_features = self.text_projection(pooled_output)
855
856 return text_features
857
858 def get_image_features(
859 self,
860 pixel_values: torch.Tensor,
861 feature_select_strategy: VisionFeatureSelectStrategy | None = None,
862 ) -> torch.Tensor:
863 if feature_select_strategy is None:
864 assert self.pooler_config.seq_pooling_type is not None
865 feature_select_strategy = _get_vision_feature_select_strategy(
866 self.pooler_config.seq_pooling_type
867 )
868
869 pooled_output = self.vision_model(
870 pixel_values=pixel_values,
871 select_layers=None,
872 feature_select_strategy=feature_select_strategy,
873 )
874
875 image_features = self.visual_projection(pooled_output)
876
877 return image_features
878
879 def _parse_and_validate_image_input(
880 self, **kwargs: object
881 ) -> CLIPImagePixelInputs | None:
882 pixel_values = kwargs.pop("pixel_values", None)
883 if pixel_values is None:
884 return None
885
886 expected_h = expected_w = self.config.vision_config.image_size
887 return CLIPImagePixelInputs(
888 type="pixel_values",
889 data=pixel_values,
890 resolve_bindings={"h": expected_h, "w": expected_w},
891 )
892
893 def _process_image_inputs(self, inputs: CLIPImagePixelInputs) -> torch.Tensor:
894 pixel_values = inputs["data"]
895
896 return self.get_image_features(pixel_values)
897
898 def _embed_text_input_ids(
899 self,
900 input_ids: torch.Tensor,
901 embed_input_ids: Callable[[torch.Tensor], torch.Tensor],
902 *,
903 is_multimodal: torch.Tensor | None,
904 ) -> torch.Tensor:
905 inputs_embeds = super()._embed_text_input_ids(
906 input_ids,
907 embed_input_ids,
908 is_multimodal=is_multimodal,
909 )
910
911 # NOTE: inputs_embeds in model runner has size text_config.projection_dim
912 # (instead of text_config.hidden_size) to accommodate image embeddings
913 inputs_embeds_size = self.projection_dim
914 if inputs_embeds.shape[1] < inputs_embeds_size:
915 inputs_embeds = torch.cat(
916 [
917 inputs_embeds,
918 inputs_embeds.new_empty(
919 inputs_embeds.shape[0],
920 inputs_embeds_size - inputs_embeds.shape[1],
921 ),
922 ],
923 dim=1,
924 )
925 elif inputs_embeds.shape[1] > inputs_embeds_size:
926 # No need to handle this case for now
927 raise NotImplementedError
928
929 return inputs_embeds
930
931 def embed_input_ids(
932 self,
933 input_ids: torch.Tensor,
934 multimodal_embeddings: MultiModalEmbeddings | None = None,
935 *,
936 is_multimodal: torch.Tensor | None = None,
937 ) -> torch.Tensor:
938 has_mm_embeddings = (
939 multimodal_embeddings is not None and len(multimodal_embeddings) > 0
940 )
941 self._mm_token_mask = is_multimodal
942 self._has_text_tokens = dual_encoder_has_text_tokens(
943 has_mm_embeddings, is_multimodal
944 )
945
946 # This is to satisfy the type checker for each overload
947 if multimodal_embeddings is None or is_multimodal is None:
948 return super().embed_input_ids(input_ids)
949
950 return super().embed_input_ids(
951 input_ids,
952 multimodal_embeddings=multimodal_embeddings,
953 is_multimodal=is_multimodal,
954 )
955
956 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
957 image_input = self._parse_and_validate_image_input(**kwargs)
958 if image_input is None:
959 return []
960
961 vision_embeddings = self._process_image_inputs(image_input)
962 return vision_embeddings
963
964 def forward(
965 self,
966 input_ids: torch.Tensor | None,
967 positions: torch.Tensor,
968 intermediate_tensors: IntermediateTensors | None = None,
969 inputs_embeds: torch.Tensor | None = None,
970 **kwargs: object,
971 ) -> torch.Tensor:
972 if intermediate_tensors is not None:
973 raise RuntimeError("PP is not supported for this model")
974
975 # Multimodal inputs
976 assert inputs_embeds is not None
977 if not self._has_text_tokens:
978 return inputs_embeds
979
980 vision_embeds = inputs_embeds
981 # NOTE: inputs_embeds in model runner has size text_config.projection_dim
982 # (instead of text_config.hidden_size) to accommodate image embeddings
983 hidden_size = self.text_embed_dim
984 if inputs_embeds.shape[1] > hidden_size:
985 inputs_embeds = inputs_embeds[:, :hidden_size]
986 elif inputs_embeds.shape[1] < hidden_size:
987 # No need to handle this case for now
988 raise NotImplementedError
989
990 text_features = self.get_text_features(input_ids, positions, inputs_embeds)
991 return merge_dual_encoder_text_and_vision(
992 text_features, vision_embeds, self._mm_token_mask
993 )
994
995 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
996 loader = AutoWeightsLoader(
997 self,
998 ignore_unexpected_prefixes=["logit_scale."],
999 )
1000
1001 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)