main
py 828 lines 27 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import math
4 from collections.abc import Hashable, Iterable, Mapping, Sequence
5 from typing import Annotated, Any, Literal
6
7 import torch
8 from torch import nn
9 from transformers import BatchFeature, Gemma3Config, Gemma3Processor
10 from transformers.models.gemma3.image_processing_gemma3 import Gemma3ImageProcessor
11 from transformers.models.gemma3.processing_gemma3 import Gemma3ProcessorKwargs
12
13 from vllm.config import VllmConfig
14 from vllm.config.multimodal import MultiModalDummyOptions
15 from vllm.inputs import MultiModalDataDict
16 from vllm.logger import init_logger
17 from vllm.model_executor.layers.layernorm import GemmaRMSNorm
18 from vllm.model_executor.models.module_mapping import MultiModelKeys
19 from vllm.multimodal import MULTIMODAL_REGISTRY
20 from vllm.multimodal.inputs import (
21 MultiModalFieldConfig,
22 MultiModalKwargsItem,
23 MultiModalKwargsItems,
24 )
25 from vllm.multimodal.parse import ImageProcessorItems, ImageSize, MultiModalDataItems
26 from vllm.multimodal.processing import BaseDummyInputsBuilder
27 from vllm.multimodal.processing.processor import (
28 BaseMultiModalProcessor,
29 BaseProcessingInfo,
30 MultiModalPromptUpdates,
31 MultiModalPromptUpdatesApplyResult,
32 PlaceholderFeaturesInfo,
33 PromptReplacement,
34 PromptUpdate,
35 PromptUpdateDetails,
36 cached_encode,
37 replace_token_matches,
38 )
39 from vllm.sequence import IntermediateTensors
40 from vllm.utils.tensor_schema import TensorSchema, TensorShape
41
42 from .interfaces import (
43 MultiModalEmbeddings,
44 SupportsEncoderCudaGraph,
45 SupportsLoRA,
46 SupportsMultiModal,
47 SupportsPP,
48 )
49 from .siglip import SiglipVisionModel
50 from .utils import (
51 AutoWeightsLoader,
52 WeightsMapper,
53 init_vllm_registered_model,
54 maybe_prefix,
55 )
56
57 logger = init_logger(__name__)
58
59
60 class Gemma3ImagePixelInputs(TensorSchema):
61 """Dimensions:
62 - p: Number of patches total (over each image over each prompt in the
63 batch)
64 - c: Number of channels (3)
65 - h: Height of each patch
66 - w: Width of each patch
67 - bn: Batch size * number of images
68 """
69
70 type: Literal["pixel_values"] = "pixel_values"
71
72 pixel_values: Annotated[torch.Tensor, TensorShape("p", 3, "h", "w")]
73
74 num_patches: Annotated[torch.Tensor, TensorShape("bn")]
75
76
77 Gemma3ImageInputs = Gemma3ImagePixelInputs
78
79
80 class Gemma3ProcessingInfo(BaseProcessingInfo):
81 def get_hf_config(self):
82 return self.ctx.get_hf_config(Gemma3Config)
83
84 def get_hf_processor(self, **kwargs: object):
85 return self.ctx.get_hf_processor(Gemma3Processor, **kwargs)
86
87 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
88 return {"image": None}
89
90 def get_num_crops(
91 self,
92 *,
93 image_width: int,
94 image_height: int,
95 processor: Gemma3Processor,
96 mm_kwargs: Mapping[str, object],
97 ) -> int:
98 image_processor: Gemma3ImageProcessor = processor.image_processor
99
100 images_kwargs = processor._merge_kwargs(
101 Gemma3ProcessorKwargs,
102 tokenizer_init_kwargs=processor.tokenizer.init_kwargs,
103 **self.ctx.get_merged_mm_kwargs(mm_kwargs),
104 )["images_kwargs"]
105
106 do_pan_and_scan = images_kwargs.get(
107 "do_pan_and_scan", image_processor.do_pan_and_scan
108 )
109 pan_and_scan_min_crop_size = images_kwargs.get(
110 "pan_and_scan_min_crop_size", image_processor.pan_and_scan_min_crop_size
111 )
112 pan_and_scan_max_num_crops = images_kwargs.get(
113 "pan_and_scan_max_num_crops", image_processor.pan_and_scan_max_num_crops
114 )
115 pan_and_scan_min_ratio_to_activate = images_kwargs.get(
116 "pan_and_scan_min_ratio_to_activate",
117 image_processor.pan_and_scan_min_ratio_to_activate,
118 )
119
120 if not do_pan_and_scan:
121 return 0
122
123 logger.warning_once(
124 "`do_pan_and_scan=True` has suboptimal results on V1 "
125 "because of the simplified attention pattern being used."
126 )
127
128 # Based on Gemma3ImageProcessor.pan_and_scan
129 if image_width >= image_height:
130 if image_width / image_height < pan_and_scan_min_ratio_to_activate:
131 return 0
132
133 num_crops_w = min(
134 int(math.floor(image_width / pan_and_scan_min_crop_size)),
135 int(math.floor(image_width / image_height + 0.5)),
136 )
137
138 num_crops_w = max(2, num_crops_w)
139 num_crops_w = min(pan_and_scan_max_num_crops, num_crops_w)
140 num_crops_h = 1
141 else:
142 if image_height / image_width < pan_and_scan_min_ratio_to_activate:
143 return 0
144
145 num_crops_h = min(
146 int(math.floor(image_height / pan_and_scan_min_crop_size)),
147 int(math.floor(image_height / image_width + 0.5)),
148 )
149
150 num_crops_h = max(2, num_crops_h)
151 num_crops_h = min(pan_and_scan_max_num_crops, num_crops_h)
152 num_crops_w = 1
153
154 crop_size_w = int(math.ceil(image_width / num_crops_w))
155 crop_size_h = int(math.ceil(image_height / num_crops_h))
156
157 if min(crop_size_w, crop_size_h) < pan_and_scan_min_crop_size:
158 return 0
159
160 return num_crops_w * num_crops_h
161
162 def get_image_repl(
163 self,
164 *,
165 image_width: int,
166 image_height: int,
167 processor: Gemma3Processor,
168 mm_kwargs: Mapping[str, object],
169 ) -> PromptUpdateDetails:
170 boi_token = processor.boi_token
171
172 num_crops = self.get_num_crops(
173 image_width=image_width,
174 image_height=image_height,
175 processor=processor,
176 mm_kwargs=mm_kwargs,
177 )
178
179 if num_crops == 0:
180 image_text = boi_token
181 else:
182 crops_image_tokens = " ".join(boi_token for _ in range(num_crops))
183 image_text = (
184 f"Here is the original image {boi_token} and here are some "
185 f"crops to help you see better {crops_image_tokens}"
186 )
187
188 repl_full = image_text.replace(boi_token, processor.full_image_sequence)
189
190 tokenizer = processor.tokenizer
191 vocab = tokenizer.get_vocab()
192 image_token_id = vocab[tokenizer.image_token]
193 repl_full_ids = cached_encode(tokenizer, repl_full, add_special_tokens=False)
194
195 return PromptUpdateDetails.select_token_id(repl_full_ids, image_token_id)
196
197 def get_num_image_tokens(
198 self,
199 *,
200 image_width: int,
201 image_height: int,
202 processor: Gemma3Processor,
203 mm_kwargs: Mapping[str, object],
204 ) -> int:
205 num_crops = self.get_num_crops(
206 image_width=image_width,
207 image_height=image_height,
208 processor=processor,
209 mm_kwargs=mm_kwargs,
210 )
211 image_seq_len = processor.image_seq_length
212
213 return (num_crops + 1) * image_seq_len
214
215 def get_image_size_with_most_features(self) -> ImageSize:
216 processor = self.get_hf_processor()
217 image_processor: Gemma3ImageProcessor = processor.image_processor
218
219 images_kwargs = processor._merge_kwargs(
220 Gemma3ProcessorKwargs,
221 tokenizer_init_kwargs=processor.tokenizer.init_kwargs,
222 **self.ctx.get_merged_mm_kwargs({}),
223 )["images_kwargs"]
224
225 max_num_crops = images_kwargs.get(
226 "pan_and_scan_max_num_crops", image_processor.pan_and_scan_max_num_crops
227 )
228
229 vision_config = self.get_hf_config().vision_config
230 native_size = vision_config.image_size
231 return ImageSize(height=native_size * max_num_crops, width=native_size)
232
233
234 class Gemma3DummyInputsBuilder(BaseDummyInputsBuilder[Gemma3ProcessingInfo]):
235 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
236 num_images = mm_counts.get("image", 0)
237
238 processor = self.info.get_hf_processor()
239 image_token = processor.boi_token
240
241 return image_token * num_images
242
243 def get_dummy_mm_data(
244 self,
245 seq_len: int,
246 mm_counts: Mapping[str, int],
247 mm_options: MultiModalDummyOptions,
248 ) -> MultiModalDataDict:
249 target_width, target_height = self.info.get_image_size_with_most_features()
250
251 return {
252 "image": self._get_dummy_images(
253 width=target_width,
254 height=target_height,
255 num_images=mm_counts.get("image", 0),
256 overrides=mm_options.get("image"),
257 )
258 }
259
260
261 class Gemma3MultiModalProcessor(BaseMultiModalProcessor[Gemma3ProcessingInfo]):
262 def _get_hf_mm_text(self, mm_counts: Mapping[str, int]) -> str:
263 return self.dummy_inputs.get_dummy_text(mm_counts)
264
265 def _postprocess_hf_mm_data(
266 self,
267 mm_data: Mapping[str, object],
268 hf_processor_mm_kwargs: Mapping[str, object],
269 processed_data: BatchFeature,
270 ) -> BatchFeature:
271 if not mm_data:
272 return processed_data
273
274 if (images := mm_data.get("images")) is not None:
275 mm_items = self.info.parse_mm_data({"image": images}, validate=False)
276 parsed_images = mm_items.get_items("image", ImageProcessorItems)
277 image_sizes = [
278 parsed_images.get_image_size(i) for i in range(len(parsed_images))
279 ]
280 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
281
282 num_crops = [
283 self.info.get_num_crops(
284 image_width=size.width,
285 image_height=size.height,
286 processor=hf_processor,
287 mm_kwargs=hf_processor_mm_kwargs,
288 )
289 for size in image_sizes
290 ]
291 processed_data["num_patches"] = torch.tensor(num_crops) + 1
292
293 return processed_data
294
295 def _get_mm_fields_config(
296 self,
297 hf_inputs: BatchFeature,
298 hf_processor_mm_kwargs: Mapping[str, object],
299 ) -> Mapping[str, MultiModalFieldConfig]:
300 num_patches = hf_inputs.get("num_patches", torch.empty(0))
301
302 return dict(
303 pixel_values=MultiModalFieldConfig.flat_from_sizes("image", num_patches),
304 num_patches=MultiModalFieldConfig.batched("image", keep_on_cpu=True),
305 )
306
307 def _get_prompt_updates(
308 self,
309 mm_items: MultiModalDataItems,
310 hf_processor_mm_kwargs: Mapping[str, Any],
311 out_mm_kwargs: MultiModalKwargsItems,
312 ) -> Sequence[PromptUpdate]:
313 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
314 tokenizer = self.info.get_tokenizer()
315 boi_token_id = tokenizer.get_vocab()[hf_processor.boi_token]
316
317 def get_replacement_gemma3(item_idx: int):
318 images = mm_items.get_items("image", ImageProcessorItems)
319
320 image_size = images.get_image_size(item_idx)
321 return self.info.get_image_repl(
322 image_width=image_size.width,
323 image_height=image_size.height,
324 processor=hf_processor,
325 mm_kwargs=hf_processor_mm_kwargs,
326 )
327
328 return [
329 PromptReplacement(
330 modality="image",
331 target=[boi_token_id],
332 replacement=get_replacement_gemma3,
333 )
334 ]
335
336 def _apply_token_matches(
337 self,
338 prompt: list[int],
339 mm_prompt_updates: MultiModalPromptUpdates,
340 ) -> tuple[list[int], MultiModalPromptUpdatesApplyResult]:
341 token_ids, res = super()._apply_token_matches(prompt, mm_prompt_updates)
342
343 # "\n\n\n" and "\n\n\n\n" are single tokens
344 # Since our replacement can insert "\n\n" next to "\n"
345 # tokens, we have to combine them to be consistent with
346 # the output of the tokenizer
347 tokenizer = self.info.get_tokenizer()
348 vocab = tokenizer.get_vocab()
349 newline_1 = vocab["\n"]
350 newline_2 = vocab["\n\n"]
351 newline_3 = vocab["\n\n\n"]
352 newline_4 = vocab["\n\n\n\n"]
353
354 token_ids = replace_token_matches(
355 token_ids,
356 [newline_1, newline_2],
357 [newline_3],
358 )
359 token_ids = replace_token_matches(
360 token_ids,
361 [newline_2, newline_1],
362 [newline_3],
363 )
364 token_ids = replace_token_matches(
365 token_ids,
366 [newline_2, newline_2],
367 [newline_4],
368 )
369
370 return token_ids, res
371
372 def _apply_token_matches_with_placeholders(
373 self,
374 token_ids: list[int],
375 mm_prompt_updates: MultiModalPromptUpdates,
376 ) -> tuple[
377 list[int],
378 MultiModalPromptUpdatesApplyResult,
379 Mapping[str, list[PlaceholderFeaturesInfo]],
380 ]:
381 new_token_ids, match_result = self._apply_token_matches(
382 token_ids,
383 mm_prompt_updates,
384 )
385
386 placeholders: dict[str, list[PlaceholderFeaturesInfo]] = {
387 modality: [] for modality in mm_prompt_updates
388 }
389
390 if all(
391 all(update_idx is not None for update_idx in update_idxs)
392 for update_idxs in match_result.values()
393 ):
394 placeholders = dict(
395 self._find_mm_placeholders(
396 new_token_ids,
397 self._matched_updates_from_result(
398 mm_prompt_updates,
399 match_result,
400 ),
401 )
402 )
403
404 return new_token_ids, match_result, placeholders
405
406 def _find_mm_placeholders(
407 self,
408 new_token_ids: list[int],
409 mm_prompt_updates: MultiModalPromptUpdates,
410 ) -> Mapping[str, list[PlaceholderFeaturesInfo]]:
411 # We need to detect "\n\n" inside "\n\n\n" and "\n\n\n\n"
412 tokenizer = self.info.get_tokenizer()
413 vocab = tokenizer.get_vocab()
414 newline_1 = vocab["\n"]
415 newline_2 = vocab["\n\n"]
416 newline_3 = vocab["\n\n\n"]
417 newline_4 = vocab["\n\n\n\n"]
418
419 def get_repl_toks(tok: int) -> list[int]:
420 if tok == newline_3:
421 return [newline_1, newline_2]
422 if tok == newline_4:
423 return [newline_2, newline_2]
424
425 return [tok]
426
427 repl_token_ids = list[int]()
428 repl_orig_idxs = list[int]()
429 for orig_idx, orig_tok in enumerate(new_token_ids):
430 repl_toks = get_repl_toks(orig_tok)
431 repl_token_ids.extend(repl_toks)
432 repl_orig_idxs.extend(orig_idx for _ in range(len(repl_toks)))
433
434 repls = super()._find_mm_placeholders(repl_token_ids, mm_prompt_updates)
435
436 return {
437 modality: [
438 PlaceholderFeaturesInfo(
439 modality=p.modality,
440 item_idx=p.item_idx,
441 start_idx=repl_orig_idxs[p.start_idx],
442 tokens=p.tokens,
443 is_embed=p.is_embed,
444 )
445 for p in placeholders
446 ]
447 for modality, placeholders in repls.items()
448 }
449
450
451 class Gemma3MultiModalProjector(nn.Module):
452 def __init__(self, config: Gemma3Config):
453 super().__init__()
454
455 self.mm_input_projection_weight = nn.Parameter(
456 torch.zeros(
457 config.vision_config.hidden_size, config.text_config.hidden_size
458 )
459 )
460
461 self.mm_soft_emb_norm = GemmaRMSNorm(
462 config.vision_config.hidden_size, eps=config.vision_config.layer_norm_eps
463 )
464
465 self.patches_per_image = int(
466 config.vision_config.image_size // config.vision_config.patch_size
467 )
468 self.tokens_per_side = int(config.mm_tokens_per_image**0.5)
469 self.kernel_size = self.patches_per_image // self.tokens_per_side
470 self.avg_pool = nn.AvgPool2d(
471 kernel_size=self.kernel_size, stride=self.kernel_size
472 )
473
474 def forward(self, vision_outputs: torch.Tensor):
475 batch_size, _, seq_length = vision_outputs.shape
476
477 reshaped_vision_outputs = vision_outputs.transpose(1, 2)
478 reshaped_vision_outputs = reshaped_vision_outputs.reshape(
479 batch_size, seq_length, self.patches_per_image, self.patches_per_image
480 )
481 reshaped_vision_outputs = reshaped_vision_outputs.contiguous()
482
483 pooled_vision_outputs = self.avg_pool(reshaped_vision_outputs)
484 pooled_vision_outputs = pooled_vision_outputs.flatten(2)
485 pooled_vision_outputs = pooled_vision_outputs.transpose(1, 2)
486
487 normed_vision_outputs = self.mm_soft_emb_norm(pooled_vision_outputs)
488
489 projected_vision_outputs = torch.matmul(
490 normed_vision_outputs, self.mm_input_projection_weight
491 )
492 return projected_vision_outputs.type_as(vision_outputs)
493
494
495 @MULTIMODAL_REGISTRY.register_processor(
496 Gemma3MultiModalProcessor,
497 info=Gemma3ProcessingInfo,
498 dummy_inputs=Gemma3DummyInputsBuilder,
499 )
500 class Gemma3ForConditionalGeneration(
501 nn.Module, SupportsMultiModal, SupportsPP, SupportsLoRA, SupportsEncoderCudaGraph
502 ):
503 packed_modules_mapping = {
504 "qkv_proj": [
505 "q_proj",
506 "k_proj",
507 "v_proj",
508 ],
509 "gate_up_proj": [
510 "gate_proj",
511 "up_proj",
512 ],
513 }
514
515 hf_to_vllm_mapper = WeightsMapper(
516 orig_to_new_prefix={
517 # mapping for new names in checkpoint saved after transformers v4.52
518 "model.language_model.": "language_model.model.",
519 "model.vision_tower.": "vision_tower.",
520 "model.multi_modal_projector.": "multi_modal_projector.",
521 "lm_head.": "language_model.lm_head.",
522 }
523 )
524
525 supports_tower_connector_lora = True
526
527 @classmethod
528 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
529 if modality.startswith("image"):
530 return "<start_of_image>"
531
532 raise ValueError("Only image modality is supported")
533
534 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
535 super().__init__()
536 config = vllm_config.model_config.hf_config
537 quant_config = vllm_config.quant_config
538 multimodal_config = vllm_config.model_config.multimodal_config
539 self.config = config
540 self.model_config = vllm_config.model_config
541 self.quant_config = quant_config
542 self.multimodal_config = multimodal_config
543 self.vit_positions_per_patch = (
544 self.config.vision_config.image_size // self.config.vision_config.patch_size
545 ) ** 2
546
547 self.configure_mm_token_handling(
548 vocab_size=config.text_config.vocab_size,
549 mm_token_ids=[config.image_token_index],
550 )
551
552 with self._mark_tower_model(vllm_config, "image"):
553 self.vision_tower = SiglipVisionModel(
554 config.vision_config,
555 quant_config,
556 prefix=maybe_prefix(prefix, "vision_tower"),
557 )
558 self.multi_modal_projector = Gemma3MultiModalProjector(config)
559
560 with self._mark_language_model(vllm_config):
561 self.language_model = init_vllm_registered_model(
562 vllm_config=vllm_config,
563 hf_config=config.text_config,
564 prefix=maybe_prefix(prefix, "language_model"),
565 architectures=["Gemma3ForCausalLM"],
566 )
567
568 logit_scale = getattr(config, "logit_scale", 1.0)
569 self.language_model.logits_processor.scale *= logit_scale
570
571 self.make_empty_intermediate_tensors = (
572 self.language_model.make_empty_intermediate_tensors
573 )
574
575 @property
576 def dtype(self):
577 return next(self.parameters()).dtype
578
579 def _parse_and_validate_image_input(
580 self, **kwargs: object
581 ) -> Gemma3ImageInputs | None:
582 pixel_values = kwargs.pop("pixel_values", None)
583 num_patches = kwargs.pop("num_patches", None)
584 image_embeds = kwargs.pop("image_embeds", None)
585 assert image_embeds is None, "Gemma3 does not support image_embeds."
586 if pixel_values is None:
587 return None
588
589 image_size = self.config.vision_config.image_size
590
591 return Gemma3ImagePixelInputs(
592 pixel_values=pixel_values,
593 num_patches=num_patches,
594 resolve_bindings={"h": image_size, "w": image_size},
595 )
596
597 def _image_pixels_to_features(
598 self,
599 vision_tower: SiglipVisionModel,
600 pixel_values: torch.Tensor,
601 ) -> torch.Tensor:
602 return vision_tower(pixel_values)
603
604 def _process_image_input(
605 self,
606 image_input: Gemma3ImageInputs,
607 ) -> list[torch.Tensor]:
608 pixel_values = image_input["pixel_values"]
609 num_patches = image_input["num_patches"]
610
611 image_features = self._image_pixels_to_features(
612 self.vision_tower,
613 pixel_values,
614 )
615 image_embeds = self.multi_modal_projector(image_features)
616
617 return [e.flatten(0, 1) for e in image_embeds.split(num_patches.tolist())]
618
619 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
620 image_input = self._parse_and_validate_image_input(**kwargs)
621 if image_input is None:
622 return []
623
624 return self._process_image_input(image_input)
625
626 def embed_input_ids(
627 self,
628 input_ids: torch.Tensor,
629 multimodal_embeddings: MultiModalEmbeddings | None = None,
630 *,
631 is_multimodal: torch.Tensor | None = None,
632 ) -> torch.Tensor:
633 # Early return for text-only inference (no multimodal data)
634 if multimodal_embeddings is None or is_multimodal is None:
635 return super().embed_input_ids(input_ids)
636
637 # Use interface default with OOV handling enabled
638 return super().embed_input_ids(
639 input_ids,
640 multimodal_embeddings=multimodal_embeddings,
641 is_multimodal=is_multimodal,
642 )
643
644 def forward(
645 self,
646 input_ids: torch.Tensor | None,
647 positions: torch.Tensor,
648 intermediate_tensors: IntermediateTensors | None = None,
649 inputs_embeds: torch.Tensor | None = None,
650 **kwargs: object,
651 ) -> IntermediateTensors:
652 if intermediate_tensors is not None:
653 inputs_embeds = None
654
655 hidden_states = self.language_model.model(
656 input_ids,
657 positions,
658 intermediate_tensors,
659 inputs_embeds=inputs_embeds,
660 **kwargs,
661 )
662
663 return hidden_states
664
665 def compute_logits(
666 self,
667 hidden_states: torch.Tensor,
668 ) -> torch.Tensor | None:
669 return self.language_model.compute_logits(hidden_states)
670
671 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
672 loader = AutoWeightsLoader(self)
673 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
674
675 def get_mm_mapping(self) -> MultiModelKeys:
676 """Get the module prefix in multimodal models"""
677 return MultiModelKeys.from_string_field(
678 language_model="language_model",
679 connector="multi_modal_projector",
680 tower_model="vision_tower",
681 )
682
683 def get_mm_lora_token_counts(
684 self,
685 *,
686 modality: str,
687 mm_kwargs: MultiModalKwargsItem | None,
688 num_mm_embeds: int,
689 ) -> tuple[int, int | None]:
690 del modality, mm_kwargs
691 tower_tokens = num_mm_embeds * 16
692 return tower_tokens, tower_tokens
693
694 def get_encoder_cudagraph_config(self):
695 from vllm.v1.worker.encoder_cudagraph_defs import (
696 EncoderCudaGraphConfig,
697 )
698
699 return EncoderCudaGraphConfig(
700 modalities=["image"],
701 buffer_keys=["pixel_values"],
702 out_hidden_size=self.config.text_config.hidden_size,
703 )
704
705 def get_encoder_cudagraph_budget_range(
706 self,
707 vllm_config: VllmConfig,
708 ) -> tuple[int, int]:
709 min_budget = self.config.mm_tokens_per_image
710 max_budget = min(
711 vllm_config.scheduler_config.max_num_batched_tokens,
712 self.model_config.max_model_len,
713 )
714 return (min_budget, max_budget)
715
716 def get_encoder_cudagraph_item_specs(
717 self,
718 mm_kwargs: dict[str, Any],
719 ):
720 from vllm.v1.worker.encoder_cudagraph_defs import EncoderItemSpec
721
722 num_patches = mm_kwargs["num_patches"]
723 mm_tokens_per_image = self.config.mm_tokens_per_image
724
725 return [
726 EncoderItemSpec(
727 input_size=int(np) * self.vit_positions_per_patch,
728 output_tokens=int(np) * mm_tokens_per_image,
729 )
730 for np in num_patches
731 ]
732
733 def select_encoder_cudagraph_items(
734 self,
735 mm_kwargs: dict[str, Any],
736 indices: list[int],
737 ) -> dict[str, Any]:
738 pixel_values = mm_kwargs["pixel_values"]
739 num_patches = mm_kwargs["num_patches"]
740
741 if len(indices) == 0:
742 return {
743 "pixel_values": pixel_values[:0],
744 "num_patches": num_patches[:0],
745 }
746 cum_patches = [0]
747 for p in num_patches:
748 cum_patches.append(cum_patches[-1] + int(p))
749
750 selected_pv = torch.cat(
751 [pixel_values[cum_patches[i] : cum_patches[i + 1]] for i in indices]
752 )
753 selected_np = num_patches[indices]
754
755 return {
756 "pixel_values": selected_pv,
757 "num_patches": selected_np,
758 }
759
760 def prepare_encoder_cudagraph_capture_inputs(
761 self,
762 token_budget: int,
763 max_batch_size: int,
764 max_frames_per_batch: int,
765 device: torch.device,
766 dtype: torch.dtype,
767 path: str = "default",
768 axis_keys: tuple[Hashable, ...] | None = None,
769 ):
770 from vllm.v1.worker.encoder_cudagraph_defs import (
771 EncoderCudaGraphCaptureInputs,
772 )
773
774 mm_tokens_per_image = self.config.mm_tokens_per_image
775 num_images = min(
776 token_budget // mm_tokens_per_image,
777 max_batch_size,
778 )
779
780 image_size = self.config.vision_config.image_size
781 dummy_pixel_values = torch.randn(
782 num_images,
783 3,
784 image_size,
785 image_size,
786 device=device,
787 dtype=dtype,
788 )
789 values = {"pixel_values": dummy_pixel_values}
790
791 return EncoderCudaGraphCaptureInputs(
792 values,
793 )
794
795 def prepare_encoder_cudagraph_replay_buffers(
796 self,
797 mm_kwargs: dict[str, Any],
798 max_batch_size: int,
799 max_frames_per_batch: int,
800 path: str = "default",
801 ):
802 from vllm.v1.worker.encoder_cudagraph_defs import (
803 EncoderCudaGraphReplayBuffers,
804 )
805
806 return EncoderCudaGraphReplayBuffers(
807 values={"pixel_values": mm_kwargs["pixel_values"]},
808 )
809
810 def encoder_cudagraph_forward(
811 self,
812 values: dict[str, torch.Tensor],
813 path: str = "default",
814 ) -> torch.Tensor:
815 pixel_values = values["pixel_values"]
816 image_features = self.vision_tower(pixel_values)
817 image_features = self.multi_modal_projector(image_features)
818 return image_features.flatten(end_dim=1)
819
820 def encoder_eager_forward(
821 self,
822 mm_kwargs: dict[str, Any],
823 path: str = "default",
824 ) -> torch.Tensor:
825 image_input = self._parse_and_validate_image_input(**mm_kwargs)
826 assert image_input is not None
827 results = self._process_image_input(image_input)
828 return torch.cat(results, dim=0)