main
py 944 lines 34.7 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import inspect
4 from collections.abc import Iterable, Mapping, Sequence, Sized
5 from typing import Annotated, Any, Literal
6
7 import torch
8 from torch import nn
9 from transformers import AutoModel, BatchFeature
10 from transformers.models.gemma3n import (
11 Gemma3nAudioConfig,
12 Gemma3nAudioFeatureExtractor,
13 Gemma3nConfig,
14 Gemma3nProcessor,
15 Gemma3nTextConfig,
16 Gemma3nVisionConfig,
17 )
18 from transformers.models.siglip import SiglipImageProcessor
19
20 from vllm.config import ModelConfig, SpeechToTextConfig, VllmConfig
21 from vllm.config.multimodal import MultiModalDummyOptions
22 from vllm.config.speech_to_text import SpeechToTextParams
23 from vllm.inputs import MultiModalDataDict, PromptType, TextPrompt
24 from vllm.logger import init_logger
25 from vllm.model_executor.layers.layernorm import RMSNorm
26 from vllm.model_executor.layers.linear import RowParallelLinear
27 from vllm.model_executor.layers.vocab_parallel_embedding import VocabParallelEmbedding
28 from vllm.model_executor.models.gemma3n import Gemma3nForCausalLM
29 from vllm.model_executor.models.gemma3n_audio_utils import (
30 adjust_audio_features_to_expected_length,
31 )
32 from vllm.model_executor.models.module_mapping import MultiModelKeys
33 from vllm.model_executor.models.whisper import ISO639_1_SUPPORTED_LANGS
34 from vllm.multimodal import MULTIMODAL_REGISTRY
35 from vllm.multimodal.inputs import (
36 MultiModalFieldConfig,
37 MultiModalKwargsItems,
38 )
39 from vllm.multimodal.parse import (
40 ImageProcessorItems,
41 MultiModalDataItems,
42 MultiModalDataParser,
43 )
44 from vllm.multimodal.processing import BaseDummyInputsBuilder
45 from vllm.multimodal.processing.processor import (
46 BaseMultiModalProcessor,
47 BaseProcessingInfo,
48 MultiModalPromptUpdates,
49 MultiModalPromptUpdatesApplyResult,
50 PlaceholderFeaturesInfo,
51 PromptReplacement,
52 PromptUpdate,
53 PromptUpdateDetails,
54 cached_encode,
55 replace_token_matches,
56 )
57 from vllm.sequence import IntermediateTensors
58 from vllm.utils.tensor_schema import TensorSchema, TensorShape
59 from vllm.utils.torch_utils import async_tensor_h2d
60
61 from .interfaces import MultiModalEmbeddings, SupportsMultiModal, SupportsTranscription
62 from .utils import (
63 AutoWeightsLoader,
64 WeightsMapper,
65 init_vllm_registered_model,
66 maybe_prefix,
67 )
68
69 logger = init_logger(__name__)
70
71 # This should be based on model config but we hardcode them for now.
72 TOKENS_PER_IMAGE = 256
73 TOKENS_PER_AUDIO = 188
74
75
76 class Gemma3nImagePixelInputs(TensorSchema):
77 """Dimensions:
78 - bn: Batch size * number of images
79 - c: Number of channels (3)
80 - h: Height of each patch
81 - w: Width of each patch
82 """
83
84 type: Literal["pixel_values"] = "pixel_values"
85 pixel_values: Annotated[torch.Tensor, TensorShape("bn", 3, "h", "w")]
86
87
88 def batch_audio_features(
89 input_features: torch.Tensor | list[torch.Tensor],
90 input_features_mask: torch.Tensor | list[torch.Tensor],
91 ) -> tuple[torch.Tensor, torch.Tensor]:
92 """Return mel features and their validity mask as batched tensors.
93
94 Audio features are unpadded per item so that a multimodal cache entry does
95 not depend on the batch it was first processed in.
96 [`MultiModalFieldConfig.batched`][vllm.multimodal.inputs.MultiModalFieldConfig.batched]
97 stacks items only when their shapes agree, so a batch of clips with
98 differing durations reaches the model as a list and must be re-padded here.
99
100 Padded frames are zero-filled and marked invalid. Audio towers consume the
101 mask, and callers keep only masked-in positions, so padding never reaches
102 the language model.
103
104 Gemma4 shares this helper: its audio path has the same unpad/re-pad
105 contract and the same fields.
106
107 Args:
108 input_features: `(bn, s, f)` tensor, or a list of `(s_i, f)` tensors
109 when clip durations differ.
110 input_features_mask: Matching `(bn, s)` tensor, or list of `(s_i,)`
111 tensors. `True` marks a valid frame.
112
113 Returns:
114 The `(bn, s_max, f)` features and their `(bn, s_max)` mask.
115
116 """
117 if isinstance(input_features, torch.Tensor):
118 assert isinstance(input_features_mask, torch.Tensor)
119 return input_features.squeeze(1), input_features_mask.squeeze(1)
120
121 assert isinstance(input_features_mask, list)
122 max_len = max(features.shape[0] for features in input_features)
123 batched_features = input_features[0].new_zeros(
124 (len(input_features), max_len, input_features[0].shape[-1])
125 )
126 batched_mask = input_features_mask[0].new_zeros(
127 (len(input_features_mask), max_len), dtype=torch.bool
128 )
129 for i, (features, mask) in enumerate(
130 zip(input_features, input_features_mask, strict=True)
131 ):
132 batched_features[i, : features.shape[0]] = features
133 batched_mask[i, : mask.shape[0]] = mask
134 return batched_features, batched_mask
135
136
137 class Gemma3nAudioInputs(TensorSchema):
138 """Dimensions:
139 - bn: Batch size * number of audios
140 - s: seq_length
141 - f: num_features
142 """
143
144 type: Literal["audio"] = "audio"
145 input_features_padded: Annotated[
146 torch.Tensor, TensorShape("bn", "s", "f", dynamic_dims={"s"})
147 ]
148 input_features_mask: Annotated[
149 torch.Tensor, TensorShape("bn", "s", dynamic_dims={"s"})
150 ]
151
152
153 Gemma3nImageInputs = Gemma3nImagePixelInputs
154
155
156 class Gemma3nProcessingInfo(BaseProcessingInfo):
157 def get_hf_config(self):
158 return self.ctx.get_hf_config(Gemma3nConfig)
159
160 def get_hf_processor(self, **kwargs: object):
161 return self.ctx.get_hf_processor(Gemma3nProcessor, **kwargs)
162
163 def get_feature_extractor(self, **kwargs: object) -> Gemma3nAudioFeatureExtractor:
164 return self.get_hf_processor(**kwargs).feature_extractor
165
166 def get_data_parser(self):
167 feature_extractor = self.get_feature_extractor()
168
169 return MultiModalDataParser(
170 target_sr=feature_extractor.sampling_rate,
171 expected_hidden_size=self._get_expected_hidden_size(),
172 )
173
174 def get_supported_mm_limits(self) -> Mapping[str, int | None]:
175 return {"image": None, "audio": None}
176
177 def get_max_tokens_per_item(
178 self, seq_len: int, mm_counts: Mapping[str, int]
179 ) -> Mapping[str, int] | None:
180 return {"image": TOKENS_PER_IMAGE, "audio": TOKENS_PER_AUDIO}
181
182 def get_image_repl(
183 self,
184 *,
185 image_width: int,
186 image_height: int,
187 processor: Gemma3nProcessor,
188 ) -> PromptUpdateDetails:
189 """Get the replacement metadata for image tokens.
190
191 For Gemma3n, this should return the full_image_sequence which includes
192 BOI token, repeated image tokens, and EOI token.
193 """
194 full_token_ids = cached_encode(
195 processor.tokenizer, processor.full_image_sequence, add_special_tokens=False
196 )
197 return PromptUpdateDetails.select_token_id(
198 full_token_ids, processor.image_token_id
199 )
200
201 def get_audio_repl(
202 self,
203 *,
204 processor: Gemma3nProcessor,
205 ) -> PromptUpdateDetails:
206 """Get the replacement metadata for audio tokens.
207
208 For Gemma3n, this should return the full_audio_sequence which includes
209 BOA token, repeated audio tokens, and EOA token.
210 """
211 # Return the full audio sequence as defined by the processor
212 full_token_ids = cached_encode(
213 processor.tokenizer, processor.full_audio_sequence, add_special_tokens=False
214 )
215 return PromptUpdateDetails.select_token_id(
216 full_token_ids, processor.audio_token_id
217 )
218
219
220 class Gemma3nDummyInputsBuilder(BaseDummyInputsBuilder[Gemma3nProcessingInfo]):
221 def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
222 num_images = mm_counts.get("image", 0)
223 num_audios = mm_counts.get("audio", 0)
224
225 processor = self.info.get_hf_processor()
226 image_token = processor.image_token
227 audio_token = processor.audio_token
228
229 return image_token * num_images + audio_token * num_audios
230
231 def get_dummy_mm_data(
232 self,
233 seq_len: int,
234 mm_counts: Mapping[str, int],
235 mm_options: MultiModalDummyOptions,
236 ) -> MultiModalDataDict:
237 processor = self.info.get_hf_processor()
238 audio_feature_extractor: Gemma3nAudioFeatureExtractor = (
239 processor.feature_extractor
240 )
241 audio_len = audio_feature_extractor.fft_length
242 image_processor: SiglipImageProcessor = processor.image_processor
243 img_width = image_processor.size.get("width", 224)
244 img_height = image_processor.size.get("height", 224)
245
246 return {
247 "image": self._get_dummy_images(
248 width=img_width,
249 height=img_height,
250 num_images=mm_counts.get("image", 0),
251 overrides=mm_options.get("image"),
252 ),
253 "audio": self._get_dummy_audios(
254 length=audio_len,
255 num_audios=mm_counts.get("audio", 0),
256 overrides=mm_options.get("audio"),
257 ),
258 }
259
260
261 class Gemma3nMultiModalProcessor(BaseMultiModalProcessor[Gemma3nProcessingInfo]):
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 "input_features" in processed_data:
272 # The feature extractor pads every clip to the longest one in
273 # the batch, so each clip's frame count depends on the other
274 # clips it was processed with. Trim each clip to the number of
275 # frames it would have if processed alone so that each item's
276 # output (and thus its cache entry) is independent of the other
277 # items in the batch. `batch_audio_features` re-pads the
278 # per-item features so audio_tower can still run batched.
279 audios = mm_data["audio"]
280 if not isinstance(audios, Sequence):
281 raise TypeError("audio data must be a sequence")
282 num_frames = []
283 for audio in audios:
284 if not isinstance(audio, Sized):
285 raise TypeError("each audio item must have a length")
286 num_frames.append(self._get_num_audio_frames_alone(len(audio)))
287 processed_data["input_features_padded"] = [
288 features[:n]
289 for features, n in zip(processed_data.pop("input_features"), num_frames)
290 ]
291 processed_data["input_features_mask"] = [
292 mask[:n]
293 for mask, n in zip(processed_data["input_features_mask"], num_frames)
294 ]
295
296 return processed_data
297
298 def _get_num_audio_frames_alone(self, num_samples: int) -> int:
299 """Get the number of mel frames that the feature extractor produces
300 for a single audio clip of the given length, independent of batch
301 padding.
302 """
303 feature_extractor = self.info.get_feature_extractor()
304 call_params = inspect.signature(type(feature_extractor).__call__).parameters
305 max_length = call_params["max_length"].default
306 pad_to_multiple_of = call_params["pad_to_multiple_of"].default
307
308 num_samples = min(num_samples, max_length)
309 remainder = num_samples % pad_to_multiple_of
310 if remainder:
311 num_samples += pad_to_multiple_of - remainder
312
313 frame_size = feature_extractor.frame_length + 1
314 num_frames = (num_samples - frame_size) // feature_extractor.hop_length + 1
315
316 return max(num_frames, 0)
317
318 def _get_mm_fields_config(
319 self,
320 hf_inputs: BatchFeature,
321 hf_processor_mm_kwargs: Mapping[str, object],
322 ) -> Mapping[str, MultiModalFieldConfig]:
323 return dict(
324 pixel_values=MultiModalFieldConfig.batched("image"),
325 input_features_padded=MultiModalFieldConfig.batched("audio"),
326 input_features_mask=MultiModalFieldConfig.batched("audio"),
327 )
328
329 def _get_prompt_updates(
330 self,
331 mm_items: MultiModalDataItems,
332 hf_processor_mm_kwargs: Mapping[str, Any],
333 out_mm_kwargs: MultiModalKwargsItems,
334 ) -> Sequence[PromptUpdate]:
335 hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)
336
337 prompt_updates = []
338
339 # Handle image tokens
340 if "image" in mm_items:
341 image_token_id = hf_processor.image_token_id
342
343 def get_replacement_image(item_idx: int):
344 images = mm_items.get_items("image", ImageProcessorItems)
345 image_size = images.get_image_size(item_idx)
346 return self.info.get_image_repl(
347 image_width=image_size.width,
348 image_height=image_size.height,
349 processor=hf_processor,
350 )
351
352 prompt_updates.append(
353 PromptReplacement(
354 modality="image",
355 target=[image_token_id],
356 replacement=get_replacement_image,
357 )
358 )
359
360 # Handle audio tokens
361 if "audio" in mm_items:
362 audio_token_id = hf_processor.audio_token_id
363
364 def get_replacement_audio(item_idx: int):
365 return self.info.get_audio_repl(
366 processor=hf_processor,
367 )
368
369 prompt_updates.append(
370 PromptReplacement(
371 modality="audio",
372 target=[audio_token_id],
373 replacement=get_replacement_audio,
374 )
375 )
376
377 return prompt_updates
378
379 def _apply_token_matches(
380 self,
381 prompt: list[int],
382 mm_prompt_updates: MultiModalPromptUpdates,
383 ) -> tuple[list[int], MultiModalPromptUpdatesApplyResult]:
384 token_ids, res = super()._apply_token_matches(prompt, mm_prompt_updates)
385
386 # "\n\n\n" and "\n\n\n\n" are single tokens
387 # Since our replacement can insert "\n\n" next to "\n"
388 # tokens, we have to combine them to be consistent with
389 # the output of the tokenizer
390 tokenizer = self.info.get_tokenizer()
391 vocab = tokenizer.get_vocab()
392 newline_1 = vocab["\n"]
393 newline_2 = vocab["\n\n"]
394 newline_3 = vocab["\n\n\n"]
395 newline_4 = vocab["\n\n\n\n"]
396
397 token_ids = replace_token_matches(
398 token_ids,
399 [newline_1, newline_2],
400 [newline_3],
401 )
402 token_ids = replace_token_matches(
403 token_ids,
404 [newline_2, newline_1],
405 [newline_3],
406 )
407 token_ids = replace_token_matches(
408 token_ids,
409 [newline_2, newline_2],
410 [newline_4],
411 )
412
413 return token_ids, res
414
415 def _apply_token_matches_with_placeholders(
416 self,
417 token_ids: list[int],
418 mm_prompt_updates: MultiModalPromptUpdates,
419 ) -> tuple[
420 list[int],
421 MultiModalPromptUpdatesApplyResult,
422 Mapping[str, list[PlaceholderFeaturesInfo]],
423 ]:
424 new_token_ids, match_result = self._apply_token_matches(
425 token_ids,
426 mm_prompt_updates,
427 )
428
429 placeholders: dict[str, list[PlaceholderFeaturesInfo]] = {
430 modality: [] for modality in mm_prompt_updates
431 }
432
433 if all(
434 all(update_idx is not None for update_idx in update_idxs)
435 for update_idxs in match_result.values()
436 ):
437 placeholders = dict(
438 self._find_mm_placeholders(
439 new_token_ids,
440 self._matched_updates_from_result(
441 mm_prompt_updates,
442 match_result,
443 ),
444 )
445 )
446
447 return new_token_ids, match_result, placeholders
448
449 def _find_mm_placeholders(
450 self,
451 new_token_ids: list[int],
452 mm_prompt_updates: MultiModalPromptUpdates,
453 ) -> Mapping[str, list[PlaceholderFeaturesInfo]]:
454 # We need to detect "\n\n" inside "\n\n\n" and "\n\n\n\n"
455 tokenizer = self.info.get_tokenizer()
456 vocab = tokenizer.get_vocab()
457 newline_1 = vocab["\n"]
458 newline_2 = vocab["\n\n"]
459 newline_3 = vocab["\n\n\n"]
460 newline_4 = vocab["\n\n\n\n"]
461
462 def get_repl_toks(tok: int) -> list[int]:
463 if tok == newline_3:
464 return [newline_1, newline_2]
465 if tok == newline_4:
466 return [newline_2, newline_2]
467
468 return [tok]
469
470 repl_token_ids = list[int]()
471 repl_orig_idxs = list[int]()
472 for orig_idx, orig_tok in enumerate(new_token_ids):
473 repl_toks = get_repl_toks(orig_tok)
474 repl_token_ids.extend(repl_toks)
475 repl_orig_idxs.extend(orig_idx for _ in range(len(repl_toks)))
476
477 repls = super()._find_mm_placeholders(repl_token_ids, mm_prompt_updates)
478
479 return {
480 modality: [
481 PlaceholderFeaturesInfo(
482 modality=p.modality,
483 item_idx=p.item_idx,
484 start_idx=repl_orig_idxs[p.start_idx],
485 tokens=p.tokens,
486 is_embed=p.is_embed,
487 )
488 for p in placeholders
489 ]
490 for modality, placeholders in repls.items()
491 }
492
493
494 class Gemma3nMultimodalEmbedder(nn.Module):
495 """Embeds token ids or soft tokens for multimodal content into language
496 model space."""
497
498 def __init__(
499 self,
500 multimodal_config: Gemma3nAudioConfig | Gemma3nVisionConfig,
501 text_config: Gemma3nTextConfig,
502 ):
503 super().__init__()
504
505 self.multimodal_hidden_size = multimodal_config.hidden_size
506 self.eps = multimodal_config.rms_norm_eps
507 self.vocab_offset = multimodal_config.vocab_offset
508 self.vocab_size = multimodal_config.vocab_size
509 self.text_hidden_size = text_config.hidden_size
510
511 self.embedding = VocabParallelEmbedding(
512 self.vocab_size,
513 self.multimodal_hidden_size,
514 )
515
516 self.hard_embedding_norm = RMSNorm(
517 self.multimodal_hidden_size,
518 eps=self.eps,
519 )
520
521 self.soft_embedding_norm = RMSNorm(
522 self.multimodal_hidden_size,
523 eps=self.eps,
524 )
525
526 self.embedding_projection = RowParallelLinear(
527 self.multimodal_hidden_size,
528 self.text_hidden_size,
529 bias=False,
530 input_is_parallel=False, # scatter the full-width input internally
531 )
532
533 self.embedding_post_projection_norm = RMSNorm(
534 self.text_hidden_size,
535 eps=self.eps,
536 has_weight=False,
537 )
538
539 def forward(
540 self,
541 input_ids: torch.LongTensor | None = None,
542 inputs_embeds: torch.Tensor | None = None,
543 ) -> torch.Tensor:
544 """Embeds token ids or soft tokens for multimodal content into language model space.
545
546 Args:
547 input_ids: A torch.LongTensor containing the token ids to embed. Values should be in the range
548 `[vocab_offset, vocab_offset + vocab_size)`.
549 inputs_embeds: A torch.Tensor containing the soft tokens to embed.
550
551 Returns:
552 A torch.Tensor of embeddings with shape `[batch_size, seq_len, self.config.text_config.hidden_size]`.
553
554 """ # noqa: E501
555 if (input_ids is None) ^ (inputs_embeds is not None):
556 raise ValueError(
557 "You must specify exactly one of input_ids or inputs_embeds"
558 )
559
560 if inputs_embeds is not None:
561 emb_norm = self.soft_embedding_norm(inputs_embeds)
562 else:
563 hard_emb = self.embedding(input_ids - self.vocab_offset)
564 emb_norm = self.hard_embedding_norm(hard_emb)
565
566 emb_norm_proj, _ = self.embedding_projection(emb_norm)
567 return self.embedding_post_projection_norm(emb_norm_proj)
568
569
570 @MULTIMODAL_REGISTRY.register_processor(
571 Gemma3nMultiModalProcessor,
572 info=Gemma3nProcessingInfo,
573 dummy_inputs=Gemma3nDummyInputsBuilder,
574 )
575 class Gemma3nForConditionalGeneration(
576 nn.Module, SupportsMultiModal, SupportsTranscription
577 ):
578 supported_languages = ISO639_1_SUPPORTED_LANGS
579
580 packed_modules_mapping = {
581 "qkv_proj": [
582 "q_proj",
583 "k_proj",
584 "v_proj",
585 ],
586 "gate_up_proj": [
587 "gate_proj",
588 "up_proj",
589 ],
590 }
591
592 hf_to_vllm_mapper = WeightsMapper(
593 orig_to_new_prefix={
594 # mapping for new names in checkpoint saved after transformers v4.52
595 "model.embed_audio.": "embed_audio.",
596 "model.embed_vision.": "embed_vision.",
597 "model.language_model.": "language_model.model.",
598 "model.vision_tower.": "vision_tower.",
599 "model.audio_tower.": "audio_tower.",
600 "model.multi_modal_projector.": "multi_modal_projector.",
601 "lm_head.": "language_model.lm_head.",
602 "model": "language_model.model",
603 }
604 )
605
606 def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
607 super().__init__()
608 config = vllm_config.model_config.hf_config
609 quant_config = vllm_config.quant_config
610 multimodal_config = vllm_config.model_config.multimodal_config
611 self.config = config
612 self.quant_config = quant_config
613 self.multimodal_config = multimodal_config
614 self.vocab_size = config.text_config.vocab_size
615
616 with self._mark_tower_model(vllm_config, "image"):
617 self.vision_tower = AutoModel.from_config(config=config.vision_config)
618 self.embed_vision = Gemma3nMultimodalEmbedder(
619 config.vision_config, config.text_config
620 )
621
622 with self._mark_tower_model(vllm_config, "audio"):
623 self.audio_tower = AutoModel.from_config(config=config.audio_config)
624 self.embed_audio = Gemma3nMultimodalEmbedder(
625 config.audio_config, config.text_config
626 )
627
628 with self._mark_language_model(vllm_config):
629 self.language_model: Gemma3nForCausalLM = init_vllm_registered_model(
630 vllm_config=vllm_config,
631 hf_config=config.text_config,
632 prefix=maybe_prefix(prefix, "language_model"),
633 architectures=["Gemma3nForCausalLM"],
634 )
635
636 # NOTE (NickLucche) In order to be compatible with cudagraph, the
637 # buffer needs to be consistent, so we pre-allocate here.
638 self.per_layer_embeddings = torch.zeros(
639 vllm_config.scheduler_config.max_num_batched_tokens,
640 self.config.text_config.num_hidden_layers,
641 self.config.text_config.hidden_size_per_layer_input,
642 device=self.language_model.model.embed_tokens.weight.device,
643 dtype=self.language_model.model.embed_tokens.weight.dtype,
644 )
645
646 def _parse_and_validate_image_input(
647 self, **kwargs: object
648 ) -> Gemma3nImageInputs | None:
649 pixel_values = kwargs.pop("pixel_values", None)
650 image_embeds = kwargs.pop("image_embeds", None)
651 # TODO is this the case?
652 assert image_embeds is None, "Gemma3n does not support image_embeds."
653 if pixel_values is None:
654 return None
655
656 return Gemma3nImagePixelInputs(pixel_values=pixel_values)
657
658 def _parse_and_validate_audio_input(
659 self, **kwargs: object
660 ) -> Gemma3nAudioInputs | None:
661 input_features_padded = kwargs.pop("input_features_padded", None)
662 if input_features_padded is None:
663 return None
664
665 input_features_mask = kwargs.pop("input_features_mask", None)
666 if input_features_mask is None:
667 return None
668
669 return Gemma3nAudioInputs(
670 input_features_padded=input_features_padded,
671 input_features_mask=input_features_mask,
672 )
673
674 def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
675 mm_input_by_modality: dict[
676 str, Gemma3nImageInputs | Gemma3nAudioInputs | None
677 ] = {}
678
679 # Preserve the order of modalities if there are multiple of them
680 # from the order of kwargs.
681 for input_key in kwargs:
682 if (
683 input_key in ("pixel_values", "image_embeds")
684 and "image" not in mm_input_by_modality
685 ):
686 mm_input_by_modality["image"] = self._parse_and_validate_image_input(
687 **kwargs
688 )
689 if (
690 input_key == "input_features_padded"
691 and "audio" not in mm_input_by_modality
692 ):
693 mm_input_by_modality["audio"] = self._parse_and_validate_audio_input(
694 **kwargs
695 )
696 return mm_input_by_modality
697
698 def _process_image_input(
699 self,
700 image_input: Gemma3nImageInputs,
701 ) -> list[torch.Tensor]:
702 pixel_values = image_input["pixel_values"]
703 vision_outputs = self.vision_tower(
704 pixel_values=pixel_values, do_pooling=False, return_dict=True
705 ).last_hidden_state
706 # TODO try to avoid copy here
707 # (batch, channels, height, width) to (batch, height * width, channels)
708 vision_outputs = (
709 vision_outputs.reshape(
710 vision_outputs.shape[0],
711 self.config.vision_config.hidden_size,
712 self.config.vision_soft_tokens_per_image,
713 )
714 .permute(0, 2, 1)
715 .contiguous()
716 )
717 # Normalize and embed the soft tokens into language model space.
718 vision_outputs *= self.config.vision_config.hidden_size**0.5
719 # Return a list of embeddings instead of a batched tensor
720 return self.embed_vision(inputs_embeds=vision_outputs).unbind(0)
721
722 def _process_audio_input(
723 self,
724 audio_input: Gemma3nAudioInputs,
725 ) -> list[torch.Tensor]:
726 # Run on padded features to enable batching
727 input_features, input_features_mask = batch_audio_features(
728 audio_input["input_features_padded"],
729 audio_input["input_features_mask"],
730 )
731 audio_outputs = self.audio_tower(input_features, ~input_features_mask)
732 audio_encodings = audio_outputs.last_hidden_state
733 audio_mask = audio_outputs.audio_mel_mask
734 audio_features = self.embed_audio(inputs_embeds=audio_encodings)
735
736 # The Gemma3nProcessor expects all audio will be 30s in length and
737 # inserts 188 audio soft tokens into the text to account for this.
738 # However, the audio preprocessing and encoder do not guarantee they
739 # will produce exactly 188 soft tokens; they may produce fewer tokens
740 # (for shorter audio) or more tokens (for longer audio or due to
741 # BOA/EOA special tokens in the placeholder sequence).
742 # We handle both cases:
743 # - If fewer tokens: pad with the embedding of the last vocab token
744 # - If more tokens: truncate to the expected count
745 # Cache the single-scalar padding-token tensor per-device to avoid a
746 # synchronous H2D tensor construction on every forward.
747 cache = getattr(self, "_audio_padding_toks_cache", None)
748 if cache is None:
749 cache = {}
750 self._audio_padding_toks_cache = cache
751 audio_padding_toks = cache.get(audio_features.device)
752 if audio_padding_toks is None:
753 audio_padding_toks = async_tensor_h2d(
754 [[self.vocab_size - 1]], dtype=torch.long, device=audio_features.device
755 )
756 cache[audio_features.device] = audio_padding_toks
757 audio_padding_embs = self.embed_audio(input_ids=audio_padding_toks)
758 audio_features = torch.where(
759 audio_mask.unsqueeze(-1), audio_padding_embs, audio_features
760 )
761
762 expected_tokens = self.config.audio_soft_tokens_per_image
763 audio_features, tokens_truncated = adjust_audio_features_to_expected_length(
764 audio_features, expected_tokens, audio_padding_embs
765 )
766 if tokens_truncated > 0:
767 logger.warning(
768 "Gemma3n audio encoder produced %d extra tokens. "
769 "Truncating to match placeholder count of %d.",
770 tokens_truncated,
771 expected_tokens,
772 )
773
774 # Return a list of embeddings instead of a batched tensor
775 return audio_features.unbind(0)
776
777 def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
778 mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
779 if mm_input_by_modality is None:
780 return []
781
782 multimodal_embeddings: list[torch.Tensor] = []
783
784 # NOTE: It is important to iterate over the keys in this dictionary
785 # to preserve the order of the modalities.
786 for modality in mm_input_by_modality:
787 multimodal_input = mm_input_by_modality[modality]
788 if modality == "image":
789 vision_embeddings = self._process_image_input(multimodal_input)
790 multimodal_embeddings.extend(vision_embeddings)
791 if modality == "audio":
792 audio_embeddings = self._process_audio_input(multimodal_input)
793 multimodal_embeddings.extend(audio_embeddings)
794 return multimodal_embeddings
795
796 def embed_input_ids(
797 self,
798 input_ids: torch.Tensor,
799 multimodal_embeddings: MultiModalEmbeddings | None = None,
800 *,
801 is_multimodal: torch.Tensor | None = None,
802 ) -> torch.Tensor:
803 # NOTE (NickLucche) Each pass needs tokens to compute PLE so we cache
804 # them here, as the model forward has only access to the input_embeds.
805 if input_ids is not None:
806 per_layer_inputs = self.language_model.model.get_per_layer_input_embeddings(
807 input_ids
808 )
809 per_layer_inputs = per_layer_inputs.reshape(
810 -1,
811 self.config.text_config.num_hidden_layers,
812 self.config.text_config.hidden_size_per_layer_input,
813 )
814 self.per_layer_embeddings[: per_layer_inputs.shape[0]].copy_(
815 per_layer_inputs
816 )
817
818 # This is to satisfy the type checker for each overload
819 if multimodal_embeddings is None or is_multimodal is None:
820 return super().embed_input_ids(input_ids)
821
822 return super().embed_input_ids(
823 input_ids,
824 multimodal_embeddings=multimodal_embeddings,
825 is_multimodal=is_multimodal,
826 )
827
828 def forward(
829 self,
830 input_ids: torch.Tensor | None,
831 positions: torch.Tensor,
832 intermediate_tensors: IntermediateTensors | None = None,
833 inputs_embeds: torch.Tensor | None = None,
834 **kwargs: object,
835 ) -> IntermediateTensors:
836 if intermediate_tensors is not None:
837 inputs_embeds = None
838
839 # NOTE (NickLucche) During profiling, `embed_input_ids` is not
840 # called, hence we don't have input_ids to compute PLEs. We simply
841 # select a chunk of pre-allocated PLEs. During normal execution,
842 # `embed_input_ids` is called before forward, hence this slice
843 # will contain PLEs computed from the actual input_ids.
844 if inputs_embeds is not None:
845 num_tokens = inputs_embeds.shape[0]
846 elif intermediate_tensors is not None:
847 num_tokens = intermediate_tensors["hidden_states"].shape[0]
848 else:
849 raise ValueError("inputs_embeds is required on the first PP rank")
850 per_layer_inputs = self.per_layer_embeddings[:num_tokens]
851
852 hidden_states = self.language_model.model(
853 input_ids,
854 positions,
855 per_layer_inputs=per_layer_inputs,
856 intermediate_tensors=intermediate_tensors,
857 inputs_embeds=inputs_embeds,
858 **kwargs,
859 )
860
861 return hidden_states
862
863 def compute_logits(
864 self,
865 hidden_states: torch.Tensor,
866 ) -> torch.Tensor | None:
867 return self.language_model.compute_logits(hidden_states)
868
869 def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
870 loader = AutoWeightsLoader(self)
871 return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
872
873 def get_mm_mapping(self) -> MultiModelKeys:
874 """Get the module prefix in multimodal models"""
875 return MultiModelKeys.from_string_field(
876 language_model="language_model",
877 connector="multi_modal_projector",
878 tower_model="vision_tower",
879 )
880
881 @classmethod
882 def get_placeholder_str(cls, modality: str, i: int) -> str | None:
883 if modality == "image":
884 return "<image_soft_token>"
885 elif modality == "audio":
886 return "<audio_soft_token>"
887 else:
888 raise ValueError(f"Unsupported modality: {modality}")
889
890 @classmethod
891 def get_generation_prompt(cls, stt_params: SpeechToTextParams) -> PromptType:
892 """Gemma3n supports "free-form" transcription.
893 We fix its prompt here to standardize transcriptions/translations
894 requests.
895 """
896 audio = stt_params.audio
897 stt_config = stt_params.stt_config
898 language = stt_params.language
899 task_type = stt_params.task_type
900 to_language = stt_params.to_language
901 # Transcribe this audio [into <>] | for transcription
902 # Translate this audio [from <> into <>] | for translation
903 prompt = "<start_of_turn>user\n"
904 prompt += "Transcribe" if task_type == "transcribe" else "Translate"
905 prompt += " this audio"
906
907 # We assume the language is a valid ISO 639-1 code.
908 full_lang_name = (
909 cls.supported_languages.get(language, "") if language is not None else ""
910 )
911 # Translation only for now
912 full_lang_name_to = (
913 cls.supported_languages.get(to_language, "")
914 if to_language is not None
915 else ""
916 )
917
918 if task_type == "transcribe" and full_lang_name:
919 prompt += f" into {full_lang_name}"
920 elif task_type == "translate":
921 if full_lang_name:
922 prompt += f" from {full_lang_name}"
923 if full_lang_name_to:
924 prompt += f" into {full_lang_name_to}"
925
926 prompt += ": <audio_soft_token><end_of_turn>\n<start_of_turn>model\n"
927
928 return TextPrompt(
929 prompt=prompt,
930 multi_modal_data={"audio": (audio, stt_config.sample_rate)},
931 )
932
933 @classmethod
934 def get_speech_to_text_config(
935 cls, model_config: ModelConfig, task_type: str
936 ) -> SpeechToTextConfig:
937 return SpeechToTextConfig(
938 # Let's set this to 30 as suggested in the docs for now, although
939 # the model is only limited by its context length.
940 max_audio_clip_s=30,
941 sample_rate=16000,
942 # TODO enable chunking after more thorough testing.
943 min_energy_split_window_size=None,
944 )