main
py 460 lines 16.2 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 # Adapted from vllm/model_executor/models/aya_vision.py
4 """Command-A-Vision (Cohere2Vision) multimodal model implementation for vLLM."""
5
6 from collections.abc import Iterable, Mapping, Sequence
7 from typing import Annotated, Literal
8
9 import torch
10 from torch import nn
11 from transformers import BatchFeature, PreTrainedConfig
12 from transformers.models.cohere2_vision import Cohere2VisionConfig
13 from transformers.models.cohere2_vision.image_processing_cohere2_vision import ( # noqa: E501
14 Cohere2VisionImageProcessor,
15 )
16 from transformers.models.cohere2_vision.processing_cohere2_vision import (
17 Cohere2VisionProcessor,
18 )
19
20 from vllm.config import VllmConfig
21 from vllm.config.multimodal import MultiModalDummyOptions
22 from vllm.inputs import MultiModalDataDict
23 from vllm.model_executor.layers.activation import MulAndSilu
24 from vllm.model_executor.layers.linear import (
25 MergedColumnParallelLinear,
26 RowParallelLinear,
27 )
28 from vllm.model_executor.layers.quantization import QuantizationConfig
29 from vllm.model_executor.layers.quantization.auto_awq import AutoAWQConfig
30 from vllm.multimodal import MULTIMODAL_REGISTRY
31 from vllm.multimodal.inputs import (
32 MultiModalFieldConfig,
33 MultiModalKwargsItems,
34 )
35 from vllm.multimodal.parse import ImageProcessorItems, ImageSize, MultiModalDataItems
36 from vllm.multimodal.processing import (
37 BaseDummyInputsBuilder,
38 BaseMultiModalProcessor,
39 BaseProcessingInfo,
40 PromptReplacement,
41 PromptUpdate,
42 PromptUpdateDetails,
43 cached_encode,
44 )
45 from vllm.sequence import IntermediateTensors
46 from vllm.utils.tensor_schema import TensorSchema, TensorShape
47
48 from .interfaces import (
49 MultiModalEmbeddings,
50 SupportsMultiModal,
51 SupportsPP,
52 SupportsQuant,
53 )
54 from .siglip import SiglipVisionModel
55 from .utils import (
56 AutoWeightsLoader,
57 WeightsMapper,
58 init_vllm_registered_model,
59 maybe_prefix,
60 )
61
62
63 class Cohere2VisionImagePixelInputs(TensorSchema):
64 """Dimensions:
65 - np: The total number of patches over each image over each prompt in
66 the batch
67 - c: Number of channels
68 - h: Height of each image patch
69 - w: Width of each image patch
70 - bn: Batch size * number of images
71 """
72
73 type: Literal["pixel_values"]
74
75 pixel_values: Annotated[
76 torch.Tensor,
77 TensorShape("np", 3, "h", "w"),
78 ]
79
80 num_patches: Annotated[
81 torch.Tensor,
82 TensorShape("bn"),
83 ]
84
85
86 class Cohere2VisionMultiModalProjector(nn.Module):
87 """Multimodal projector that maps vision features to text embedding space.
88
89 Uses pixel shuffle downsampling followed by SwiGLU activation.
90 """
91
92 def __init__(self, config: Cohere2VisionConfig, prefix: str = ""):
93 super().__init__()
94 self.downsample_factor = config.downsample_factor
95
96 # Input dimension after pixel shuffle downsampling
97 input_dim = config.vision_config.hidden_size * (config.downsample_factor**2)
98 # MergedColumnParallelLinear expects the intermediate size to be a list
99 # of sizes, so that it will load the weights as two separate linear
100 # layers before applying any parallelism.
101 # We need to divide the alignment intermediate size by 2 because
102 # the weights are merged weights of two linear layers for SwiGLU.
103 self.intermediate_size = config.alignment_intermediate_size // 2
104
105 self.linear_1 = MergedColumnParallelLinear(
106 input_dim,
107 [self.intermediate_size] * 2,
108 bias=True,
109 return_bias=False,
110 prefix=f"{prefix}.linear_1",
111 )
112 self.act = MulAndSilu()
113 self.linear_2 = RowParallelLinear(
114 self.intermediate_size,
115 config.text_config.hidden_size,
116 bias=True,
117 return_bias=False,
118 prefix=f"{prefix}.linear_2",
119 )
120
121 def forward(self, image_features):
122 image_features = self.pixel_shuffle(image_features)
123 hidden_states = self.linear_1(image_features)
124 hidden_states = self.act(hidden_states)
125 hidden_states = self.linear_2(hidden_states)
126 return hidden_states
127
128 def pixel_shuffle(self, image_features: torch.Tensor) -> torch.Tensor:
129 """Apply pixel shuffle downsampling to reduce spatial dimensions.
130
131 Args:
132 image_features: Input tensor of shape [B, S, D] where S = H*W
133
134 Returns:
135 Downsampled tensor with increased channel dimension
136
137 """
138 height = width = int(image_features.shape[1] ** 0.5)
139 x = image_features.reshape(image_features.shape[0], width, height, -1)
140 n, h, w, c = x.size()
141 scale_factor = 1.0 / self.downsample_factor
142 nh = int(h * scale_factor)
143 nw = int(w * scale_factor)
144 x = x.reshape(n, nh, self.downsample_factor, nw, self.downsample_factor, c)
145 x = x.permute(0, 1, 3, 2, 4, 5).contiguous()
146 x = x.reshape(n, nh, nw, -1)
147 return x
148
149
150 class Cohere2VisionProcessingInfo(BaseProcessingInfo):
151 def get_hf_config(self) -> Cohere2VisionConfig:
152 return self.ctx.get_hf_config(Cohere2VisionConfig)
153
154 def get_hf_processor(self, **kwargs: object) -> Cohere2VisionProcessor:
155 return self.ctx.get_hf_processor(Cohere2VisionProcessor, **kwargs)
156
157 def get_image_processor(self, **kwargs: object):
158 return self.get_hf_processor(**kwargs).image_processor
159
160 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
161 return {"image": None}
162
163 def get_image_size_with_most_features(self) -> ImageSize:
164 image_processor = self.get_image_processor()
165 height = image_processor.size["height"]
166 width = image_processor.size["width"]
167 max_patches = image_processor.max_patches
168 return ImageSize(height=height * max_patches, width=width)
169
170 def get_num_patches(
171 self,
172 *,
173 image_width: int,
174 image_height: int,
175 processor: Cohere2VisionProcessor,
176 mm_kwargs: Mapping[str, object],
177 ) -> int:
178 """Calculate the number of image patches for a given image.
179 Uses the HF processor to determine the actual number of patches.
180 """
181 image_processor: Cohere2VisionImageProcessor = processor.image_processor
182
183 return image_processor.get_number_of_image_patches(
184 image_height,
185 image_width,
186 self.ctx.get_merged_mm_kwargs(mm_kwargs),
187 )
188
189
190 class Cohere2VisionDummyInputsBuilder(
191 BaseDummyInputsBuilder[Cohere2VisionProcessingInfo]
192 ):
193 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
194 num_images = mm_counts.get("image", 0)
195
196 processor = self.info.get_hf_processor()
197 image_token = processor.image_token
198
199 return image_token * num_images
200
201 def get_dummy_mm_data(
202 self,
203 seq_len: int,
204 mm_counts: Mapping[str, int],
205 mm_options: MultiModalDummyOptions,
206 ) -> MultiModalDataDict:
207 image_size = self.info.get_image_size_with_most_features()
208
209 return {
210 "image": self._get_dummy_images(
211 width=image_size.width,
212 height=image_size.height,
213 num_images=mm_counts.get("image", 0),
214 overrides=mm_options.get("image"),
215 )
216 }
217
218
219 class Cohere2VisionMultiModalProcessor(
220 BaseMultiModalProcessor[Cohere2VisionProcessingInfo]
221 ):
222 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
223 return self.dummy_inputs.get_dummy_text(mm_counts)
224
225 def _postprocess_hf_mm_data(
226 self,
227 mm_data: Mapping[str, object],
228 hf_processor_mm_kwargs: Mapping[str, object],
229 processed_data: BatchFeature,
230 ) -> BatchFeature:
231 if not mm_data:
232 return processed_data
233
234 if (
235 "num_patches" not in processed_data
236 and (images := mm_data.get("images")) is not None
237 ):
238 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
239
240 # Fallback calculation if HF processor didn't provide num_patches
241 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
242 parsed_images = mm_items.get_items("image", ImageProcessorItems)
243
244 num_patches = [
245 self.info.get_num_patches(
246 image_width=parsed_images.get_image_size(i).width,
247 image_height=parsed_images.get_image_size(i).height,
248 processor=hf_processor,
249 mm_kwargs=hf_processor_mm_kwargs,
250 )
251 for i in range(len(parsed_images))
252 ]
253 processed_data["num_patches"] = torch.tensor(num_patches)
254
255 return processed_data
256
257 def _get_mm_fields_config(
258 self,
259 hf_inputs: BatchFeature,
260 hf_processor_mm_kwargs: Mapping[str, object],
261 ) -> Mapping[str, MultiModalFieldConfig]:
262 num_patches = hf_inputs.get("num_patches", torch.empty(0))
263 return dict(
264 pixel_values=MultiModalFieldConfig.flat_from_sizes("image", num_patches),
265 num_patches=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
266 image_embeds=MultiModalFieldConfig.batched("image"),
267 )
268
269 def _get_prompt_updates(
270 self,
271 mm_items: MultiModalDataItems,
272 hf_processor_mm_kwargs: Mapping[str, object],
273 out_mm_kwargs: MultiModalKwargsItems,
274 ) -> Sequence[PromptUpdate]:
275 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
276 image_token = hf_processor.image_token
277 image_token_id = hf_processor.image_token_id
278 img_tokens_per_tile = int(hf_processor.patch_size**2)
279 img_line_break_token = hf_processor.img_line_break_token
280 boi_token = hf_processor.boi_token
281 eoi_token = hf_processor.eoi_token
282 tokenizer = self.info.get_tokenizer()
283
284 def get_replacement(item_idx: int):
285 images = mm_items.get_items("image", ImageProcessorItems)
286 image_size: ImageSize = images.get_image_size(item_idx)
287
288 num_patches = self.info.get_num_patches(
289 image_width=image_size.width,
290 image_height=image_size.height,
291 processor=hf_processor,
292 mm_kwargs=hf_processor_mm_kwargs,
293 )
294 patch_tokens = image_token * img_tokens_per_tile + img_line_break_token
295 repl = f"{boi_token}{patch_tokens * num_patches}{eoi_token}"
296
297 repl_ids = cached_encode(tokenizer, repl, add_special_tokens=False)
298 return PromptUpdateDetails.select_token_id(repl_ids, image_token_id)
299
300 return [
301 PromptReplacement(
302 modality="image",
303 target=[image_token_id],
304 replacement=get_replacement,
305 )
306 ]
307
308
309 @MULTIMODAL_REGISTRY.register_processor(
310 Cohere2VisionMultiModalProcessor,
311 info=Cohere2VisionProcessingInfo,
312 dummy_inputs=Cohere2VisionDummyInputsBuilder,
313 )
314 class Cohere2VisionForConditionalGeneration(
315 nn.Module, SupportsMultiModal, SupportsPP, SupportsQuant
316 ):
317 hf_to_vllm_mapper = WeightsMapper(
318 orig_to_new_prefix={
319 "model.vision_tower.": "vision_tower.",
320 "model.multi_modal_projector.": "multi_modal_projector.",
321 "model.language_model.": "language_model.model.",
322 }
323 )
324
325 packed_modules_mapping = {
326 "qkv_proj": ["q_proj", "k_proj", "v_proj"],
327 "gate_up_proj": ["gate_proj", "up_proj"],
328 }
329
330 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
331 super().__init__()
332 config: Cohere2VisionConfig = vllm_config.model_config.hf_config
333 quant_config = vllm_config.quant_config
334 multimodal_config = vllm_config.model_config.multimodal_config
335 self.config = config
336 self.quant_config = quant_config
337 self.multimodal_config = multimodal_config
338 self._patch_quant_config(config, quant_config)
339
340 with self._mark_tower_model(vllm_config, "image"):
341 self.vision_tower = SiglipVisionModel(
342 config.vision_config,
343 quant_config,
344 prefix=maybe_prefix(prefix, "vision_tower"),
345 )
346 self.multi_modal_projector = Cohere2VisionMultiModalProjector(
347 config, prefix=maybe_prefix(prefix, "multi_modal_projector")
348 )
349
350 with self._mark_language_model(vllm_config):
351 self.language_model = init_vllm_registered_model(
352 vllm_config=vllm_config,
353 hf_config=config.text_config,
354 prefix=maybe_prefix(prefix, "language_model"),
355 architectures=config.text_config.architectures,
356 )
357
358 self.make_empty_intermediate_tensors = (
359 self.language_model.make_empty_intermediate_tensors
360 )
361
362 @property
363 def dtype(self):
364 return next(self.parameters()).dtype
365
366 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
367 loader = AutoWeightsLoader(self)
368 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
369
370 def _process_image_input(
371 self, image_input: Cohere2VisionImagePixelInputs, **kwargs: object
372 ) -> list[torch.Tensor]:
373 """Process image pixels through vision tower and projector.
374
375 Args:
376 image_input: Validated image input containing pixel values and
377 patch counts
378 **kwargs: Unused; accepted for interface compatibility.
379
380 Returns:
381 List of flattened image embeddings, one per image
382
383 """
384 pixel_values = image_input["pixel_values"]
385 num_patches = image_input["num_patches"]
386
387 # Extract visual features
388 image_features = self.vision_tower(pixel_values)
389
390 # Project to text embedding space
391 image_embeds = self.multi_modal_projector(image_features)
392
393 # Split and flatten embeddings per image
394 return [e.flatten(0, 2) for e in image_embeds.split(num_patches.tolist())]
395
396 def _parse_and_validate_image_input(
397 self, **kwargs: object
398 ) -> Cohere2VisionImagePixelInputs | None:
399 pixel_values = kwargs.pop("pixel_values", None)
400 num_patches = kwargs.pop("num_patches", None)
401 image_embeds = kwargs.pop("image_embeds", None)
402 assert image_embeds is None, "Cohere2Vision does not support image_embeds."
403
404 if pixel_values is None:
405 return None
406
407 return Cohere2VisionImagePixelInputs(
408 type="pixel_values",
409 pixel_values=pixel_values,
410 num_patches=num_patches,
411 resolve_bindings={
412 "h": self.config.vision_config.image_size,
413 "w": self.config.vision_config.image_size,
414 },
415 )
416
417 def _patch_quant_config(
418 self, config: PreTrainedConfig, quant_config: QuantizationConfig | None
419 ):
420 # the awq models from OpenGVLab missing `modules_to_not_convert`
421 # patch the quant_config to add `modules_to_not_convert` back
422 if isinstance(quant_config, AutoAWQConfig):
423 text_config = config.text_config
424 llm_quant_config = getattr(text_config, "quantization_config", None)
425 if (not quant_config.modules_to_not_convert) and (
426 llm_quant_config is not None
427 ):
428 quant_config.modules_to_not_convert.append("vision_tower")
429
430 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
431 image_input = self._parse_and_validate_image_input(**kwargs)
432 if image_input is None:
433 return []
434
435 return self._process_image_input(image_input, **kwargs)
436
437 def forward(
438 self,
439 input_ids: torch.Tensor | None,
440 positions: torch.Tensor,
441 intermediate_tensors: IntermediateTensors | None = None,
442 inputs_embeds: torch.Tensor | None = None,
443 **kwargs: object,
444 ) -> torch.Tensor | IntermediateTensors:
445 if intermediate_tensors is not None:
446 inputs_embeds = None
447
448 hidden_states = self.language_model.model(
449 input_ids=input_ids,
450 positions=positions,
451 intermediate_tensors=intermediate_tensors,
452 inputs_embeds=inputs_embeds,
453 )
454 return hidden_states
455
456 def compute_logits(
457 self,
458 hidden_states: torch.Tensor,
459 ) -> torch.Tensor | None:
460 return self.language_model.compute_logits(hidden_states)