Skip to content

vllm.model_executor.models.gemma3n_mm

Classes:

Functions:

Gemma3nAudioInputs

Bases: TensorSchema

Dimensions
  • bn: Batch size * number of audios
  • s: seq_length
  • f: num_features
Source code in vllm/model_executor/models/gemma3n_mm.py
class Gemma3nAudioInputs(TensorSchema):
    """
    Dimensions:
        - bn: Batch size * number of audios
        - s: seq_length
        - f: num_features
    """

    type: Literal["audio"] = "audio"
    input_features_padded: Annotated[
        torch.Tensor, TensorShape("bn", "s", "f", dynamic_dims={"s"})
    ]
    input_features_mask: Annotated[
        torch.Tensor, TensorShape("bn", "s", dynamic_dims={"s"})
    ]

Gemma3nForConditionalGeneration

Bases: Module, SupportsMultiModal, SupportsTranscription

Methods:

Source code in vllm/model_executor/models/gemma3n_mm.py
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
@MULTIMODAL_REGISTRY.register_processor(
    Gemma3nMultiModalProcessor,
    info=Gemma3nProcessingInfo,
    dummy_inputs=Gemma3nDummyInputsBuilder,
)
class Gemma3nForConditionalGeneration(
    nn.Module, SupportsMultiModal, SupportsTranscription
):
    supported_languages = ISO639_1_SUPPORTED_LANGS

    packed_modules_mapping = {
        "qkv_proj": [
            "q_proj",
            "k_proj",
            "v_proj",
        ],
        "gate_up_proj": [
            "gate_proj",
            "up_proj",
        ],
    }

    hf_to_vllm_mapper = WeightsMapper(
        orig_to_new_prefix={
            # mapping for new names in checkpoint saved after transformers v4.52
            "model.embed_audio.": "embed_audio.",
            "model.embed_vision.": "embed_vision.",
            "model.language_model.": "language_model.model.",
            "model.vision_tower.": "vision_tower.",
            "model.audio_tower.": "audio_tower.",
            "model.multi_modal_projector.": "multi_modal_projector.",
            "lm_head.": "language_model.lm_head.",
            "model": "language_model.model",
        }
    )

    def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
        super().__init__()
        config = vllm_config.model_config.hf_config
        quant_config = vllm_config.quant_config
        multimodal_config = vllm_config.model_config.multimodal_config
        self.config = config
        self.quant_config = quant_config
        self.multimodal_config = multimodal_config
        self.vocab_size = config.text_config.vocab_size

        with self._mark_tower_model(vllm_config, "image"):
            self.vision_tower = AutoModel.from_config(config=config.vision_config)
            self.embed_vision = Gemma3nMultimodalEmbedder(
                config.vision_config, config.text_config
            )

        with self._mark_tower_model(vllm_config, "audio"):
            self.audio_tower = AutoModel.from_config(config=config.audio_config)
            self.embed_audio = Gemma3nMultimodalEmbedder(
                config.audio_config, config.text_config
            )

        with self._mark_language_model(vllm_config):
            self.language_model: Gemma3nForCausalLM = init_vllm_registered_model(
                vllm_config=vllm_config,
                hf_config=config.text_config,
                prefix=maybe_prefix(prefix, "language_model"),
                architectures=["Gemma3nForCausalLM"],
            )

            # NOTE (NickLucche) In order to be compatible with cudagraph, the
            # buffer needs to be consistent, so we pre-allocate here.
            self.per_layer_embeddings = torch.zeros(
                vllm_config.scheduler_config.max_num_batched_tokens,
                self.config.text_config.num_hidden_layers,
                self.config.text_config.hidden_size_per_layer_input,
                device=self.language_model.model.embed_tokens.weight.device,
                dtype=self.language_model.model.embed_tokens.weight.dtype,
            )

    def _parse_and_validate_image_input(
        self, **kwargs: object
    ) -> Gemma3nImageInputs | None:
        pixel_values = kwargs.pop("pixel_values", None)
        image_embeds = kwargs.pop("image_embeds", None)
        # TODO is this the case?
        assert image_embeds is None, "Gemma3n does not support image_embeds."
        if pixel_values is None:
            return None

        return Gemma3nImagePixelInputs(pixel_values=pixel_values)

    def _parse_and_validate_audio_input(
        self, **kwargs: object
    ) -> Gemma3nAudioInputs | None:
        input_features_padded = kwargs.pop("input_features_padded", None)
        if input_features_padded is None:
            return None

        input_features_mask = kwargs.pop("input_features_mask", None)
        if input_features_mask is None:
            return None

        return Gemma3nAudioInputs(
            input_features_padded=input_features_padded,
            input_features_mask=input_features_mask,
        )

    def _parse_and_validate_multimodal_inputs(self, **kwargs: object) -> dict:
        mm_input_by_modality: dict[
            str, Gemma3nImageInputs | Gemma3nAudioInputs | None
        ] = {}

        # Preserve the order of modalities if there are multiple of them
        # from the order of kwargs.
        for input_key in kwargs:
            if (
                input_key in ("pixel_values", "image_embeds")
                and "image" not in mm_input_by_modality
            ):
                mm_input_by_modality["image"] = self._parse_and_validate_image_input(
                    **kwargs
                )
            if (
                input_key == "input_features_padded"
                and "audio" not in mm_input_by_modality
            ):
                mm_input_by_modality["audio"] = self._parse_and_validate_audio_input(
                    **kwargs
                )
        return mm_input_by_modality

    def _process_image_input(
        self,
        image_input: Gemma3nImageInputs,
    ) -> list[torch.Tensor]:
        pixel_values = image_input["pixel_values"]
        vision_outputs = self.vision_tower(
            pixel_values=pixel_values, do_pooling=False, return_dict=True
        ).last_hidden_state
        # TODO try to avoid copy here
        # (batch, channels, height, width) to (batch, height * width, channels)
        vision_outputs = (
            vision_outputs.reshape(
                vision_outputs.shape[0],
                self.config.vision_config.hidden_size,
                self.config.vision_soft_tokens_per_image,
            )
            .permute(0, 2, 1)
            .contiguous()
        )
        # Normalize and embed the soft tokens into language model space.
        vision_outputs *= self.config.vision_config.hidden_size**0.5
        # Return a list of embeddings instead of a batched tensor
        return self.embed_vision(inputs_embeds=vision_outputs).unbind(0)

    def _process_audio_input(
        self,
        audio_input: Gemma3nAudioInputs,
    ) -> list[torch.Tensor]:
        # Run on padded features to enable batching
        input_features, input_features_mask = batch_audio_features(
            audio_input["input_features_padded"],
            audio_input["input_features_mask"],
        )
        audio_outputs = self.audio_tower(input_features, ~input_features_mask)
        audio_encodings = audio_outputs.last_hidden_state
        audio_mask = audio_outputs.audio_mel_mask
        audio_features = self.embed_audio(inputs_embeds=audio_encodings)

        # The Gemma3nProcessor expects all audio will be 30s in length and
        # inserts 188 audio soft tokens into the text to account for this.
        # However, the audio preprocessing and encoder do not guarantee they
        # will produce exactly 188 soft tokens; they may produce fewer tokens
        # (for shorter audio) or more tokens (for longer audio or due to
        # BOA/EOA special tokens in the placeholder sequence).
        # We handle both cases:
        # - If fewer tokens: pad with the embedding of the last vocab token
        # - If more tokens: truncate to the expected count
        # Cache the single-scalar padding-token tensor per-device to avoid a
        # synchronous H2D tensor construction on every forward.
        cache = getattr(self, "_audio_padding_toks_cache", None)
        if cache is None:
            cache = {}
            self._audio_padding_toks_cache = cache
        audio_padding_toks = cache.get(audio_features.device)
        if audio_padding_toks is None:
            audio_padding_toks = async_tensor_h2d(
                [[self.vocab_size - 1]], dtype=torch.long, device=audio_features.device
            )
            cache[audio_features.device] = audio_padding_toks
        audio_padding_embs = self.embed_audio(input_ids=audio_padding_toks)
        audio_features = torch.where(
            audio_mask.unsqueeze(-1), audio_padding_embs, audio_features
        )

        expected_tokens = self.config.audio_soft_tokens_per_image
        audio_features, tokens_truncated = adjust_audio_features_to_expected_length(
            audio_features, expected_tokens, audio_padding_embs
        )
        if tokens_truncated > 0:
            logger.warning(
                "Gemma3n audio encoder produced %d extra tokens. "
                "Truncating to match placeholder count of %d.",
                tokens_truncated,
                expected_tokens,
            )

        # Return a list of embeddings instead of a batched tensor
        return audio_features.unbind(0)

    def embed_multimodal(self, **kwargs: object) -> MultiModalEmbeddings:
        mm_input_by_modality = self._parse_and_validate_multimodal_inputs(**kwargs)
        if mm_input_by_modality is None:
            return []

        multimodal_embeddings: list[torch.Tensor] = []

        # NOTE: It is important to iterate over the keys in this dictionary
        # to preserve the order of the modalities.
        for modality in mm_input_by_modality:
            multimodal_input = mm_input_by_modality[modality]
            if modality == "image":
                vision_embeddings = self._process_image_input(multimodal_input)
                multimodal_embeddings.extend(vision_embeddings)
            if modality == "audio":
                audio_embeddings = self._process_audio_input(multimodal_input)
                multimodal_embeddings.extend(audio_embeddings)
        return multimodal_embeddings

    def embed_input_ids(
        self,
        input_ids: torch.Tensor,
        multimodal_embeddings: MultiModalEmbeddings | None = None,
        *,
        is_multimodal: torch.Tensor | None = None,
    ) -> torch.Tensor:
        # NOTE (NickLucche) Each pass needs tokens to compute PLE so we cache
        # them here, as the model  forward has only access to the input_embeds.
        if input_ids is not None:
            per_layer_inputs = self.language_model.model.get_per_layer_input_embeddings(
                input_ids
            )
            per_layer_inputs = per_layer_inputs.reshape(
                -1,
                self.config.text_config.num_hidden_layers,
                self.config.text_config.hidden_size_per_layer_input,
            )
            self.per_layer_embeddings[: per_layer_inputs.shape[0]].copy_(
                per_layer_inputs
            )

        # This is to satisfy the type checker for each overload
        if multimodal_embeddings is None or is_multimodal is None:
            return super().embed_input_ids(input_ids)

        return super().embed_input_ids(
            input_ids,
            multimodal_embeddings=multimodal_embeddings,
            is_multimodal=is_multimodal,
        )

    def forward(
        self,
        input_ids: torch.Tensor | None,
        positions: torch.Tensor,
        intermediate_tensors: IntermediateTensors | None = None,
        inputs_embeds: torch.Tensor | None = None,
        **kwargs: object,
    ) -> IntermediateTensors:
        if intermediate_tensors is not None:
            inputs_embeds = None

        # NOTE (NickLucche) During profiling, `embed_input_ids` is not
        # called, hence we don't have input_ids to compute PLEs. We simply
        # select a chunk of pre-allocated PLEs. During normal execution,
        # `embed_input_ids` is called before forward, hence this slice
        # will contain PLEs computed from the actual input_ids.
        if inputs_embeds is not None:
            num_tokens = inputs_embeds.shape[0]
        elif intermediate_tensors is not None:
            num_tokens = intermediate_tensors["hidden_states"].shape[0]
        else:
            raise ValueError("inputs_embeds is required on the first PP rank")
        per_layer_inputs = self.per_layer_embeddings[:num_tokens]

        hidden_states = self.language_model.model(
            input_ids,
            positions,
            per_layer_inputs=per_layer_inputs,
            intermediate_tensors=intermediate_tensors,
            inputs_embeds=inputs_embeds,
            **kwargs,
        )

        return hidden_states

    def compute_logits(
        self,
        hidden_states: torch.Tensor,
    ) -> torch.Tensor | None:
        return self.language_model.compute_logits(hidden_states)

    def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
        loader = AutoWeightsLoader(self)
        return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)

    def get_mm_mapping(self) -> MultiModelKeys:
        """
        Get the module prefix in multimodal models
        """
        return MultiModelKeys.from_string_field(
            language_model="language_model",
            connector="multi_modal_projector",
            tower_model="vision_tower",
        )

    @classmethod
    def get_placeholder_str(cls, modality: str, i: int) -> str | None:
        if modality == "image":
            return "<image_soft_token>"
        elif modality == "audio":
            return "<audio_soft_token>"
        else:
            raise ValueError(f"Unsupported modality: {modality}")

    @classmethod
    def get_generation_prompt(cls, stt_params: SpeechToTextParams) -> PromptType:
        """
        Gemma3n supports "free-form" transcription.
        We fix its prompt here to standardize transcriptions/translations
        requests.
        """
        audio = stt_params.audio
        stt_config = stt_params.stt_config
        language = stt_params.language
        task_type = stt_params.task_type
        to_language = stt_params.to_language
        # Transcribe this audio [into <>] | for transcription
        # Translate this audio [from <> into <>] | for translation
        prompt = "<start_of_turn>user\n"
        prompt += "Transcribe" if task_type == "transcribe" else "Translate"
        prompt += " this audio"

        # We assume the language is a valid ISO 639-1 code.
        full_lang_name = (
            cls.supported_languages.get(language, "") if language is not None else ""
        )
        # Translation only for now
        full_lang_name_to = (
            cls.supported_languages.get(to_language, "")
            if to_language is not None
            else ""
        )

        if task_type == "transcribe" and full_lang_name:
            prompt += f" into {full_lang_name}"
        elif task_type == "translate":
            if full_lang_name:
                prompt += f" from {full_lang_name}"
            if full_lang_name_to:
                prompt += f" into {full_lang_name_to}"

        prompt += ": <audio_soft_token><end_of_turn>\n<start_of_turn>model\n"

        return TextPrompt(
            prompt=prompt,
            multi_modal_data={"audio": (audio, stt_config.sample_rate)},
        )

    @classmethod
    def get_speech_to_text_config(
        cls, model_config: ModelConfig, task_type: str
    ) -> SpeechToTextConfig:
        return SpeechToTextConfig(
            # Let's set this to 30 as suggested in the docs for now, although
            # the model is only limited by its context length.
            max_audio_clip_s=30,
            sample_rate=16000,
            # TODO enable chunking after more thorough testing.
            min_energy_split_window_size=None,
        )

get_generation_prompt(stt_params) classmethod

Gemma3n supports "free-form" transcription. We fix its prompt here to standardize transcriptions/translations requests.

Source code in vllm/model_executor/models/gemma3n_mm.py
@classmethod
def get_generation_prompt(cls, stt_params: SpeechToTextParams) -> PromptType:
    """
    Gemma3n supports "free-form" transcription.
    We fix its prompt here to standardize transcriptions/translations
    requests.
    """
    audio = stt_params.audio
    stt_config = stt_params.stt_config
    language = stt_params.language
    task_type = stt_params.task_type
    to_language = stt_params.to_language
    # Transcribe this audio [into <>] | for transcription
    # Translate this audio [from <> into <>] | for translation
    prompt = "<start_of_turn>user\n"
    prompt += "Transcribe" if task_type == "transcribe" else "Translate"
    prompt += " this audio"

    # We assume the language is a valid ISO 639-1 code.
    full_lang_name = (
        cls.supported_languages.get(language, "") if language is not None else ""
    )
    # Translation only for now
    full_lang_name_to = (
        cls.supported_languages.get(to_language, "")
        if to_language is not None
        else ""
    )

    if task_type == "transcribe" and full_lang_name:
        prompt += f" into {full_lang_name}"
    elif task_type == "translate":
        if full_lang_name:
            prompt += f" from {full_lang_name}"
        if full_lang_name_to:
            prompt += f" into {full_lang_name_to}"

    prompt += ": <audio_soft_token><end_of_turn>\n<start_of_turn>model\n"

    return TextPrompt(
        prompt=prompt,
        multi_modal_data={"audio": (audio, stt_config.sample_rate)},
    )

get_mm_mapping()

Get the module prefix in multimodal models

Source code in vllm/model_executor/models/gemma3n_mm.py
def get_mm_mapping(self) -> MultiModelKeys:
    """
    Get the module prefix in multimodal models
    """
    return MultiModelKeys.from_string_field(
        language_model="language_model",
        connector="multi_modal_projector",
        tower_model="vision_tower",
    )

Gemma3nImagePixelInputs

Bases: TensorSchema

Dimensions
  • bn: Batch size * number of images
  • c: Number of channels (3)
  • h: Height of each patch
  • w: Width of each patch
Source code in vllm/model_executor/models/gemma3n_mm.py
class Gemma3nImagePixelInputs(TensorSchema):
    """
    Dimensions:
        - bn: Batch size * number of images
        - c: Number of channels (3)
        - h: Height of each patch
        - w: Width of each patch
    """

    type: Literal["pixel_values"] = "pixel_values"
    pixel_values: Annotated[torch.Tensor, TensorShape("bn", 3, "h", "w")]

Gemma3nMultiModalProcessor

Bases: BaseMultiModalProcessor[Gemma3nProcessingInfo]

Source code in vllm/model_executor/models/gemma3n_mm.py
class Gemma3nMultiModalProcessor(BaseMultiModalProcessor[Gemma3nProcessingInfo]):
    def _get_hf_processor_text(self, mm_counts: Mapping[str, int]) -> str:
        return self.dummy_inputs.get_dummy_text(mm_counts)

    def _preprocess_hf_mm_data(
        self,
        mm_data: Mapping[str, object],
        hf_processor_mm_kwargs: Mapping[str, object],
    ) -> tuple[Mapping[str, object], Mapping[str, object]]:
        mm_data = dict(mm_data)
        if "audios" in mm_data:
            mm_data["audio"] = mm_data.pop("audios")

        return mm_data, hf_processor_mm_kwargs

    def _postprocess_hf_mm_data(
        self,
        mm_data: Mapping[str, object],
        hf_processor_mm_kwargs: Mapping[str, object],
        processed_data: BatchFeature,
    ) -> BatchFeature:
        if "input_features" in processed_data:
            # The feature extractor pads every clip to the longest one in
            # the batch, so each clip's frame count depends on the other
            # clips it was processed with. Trim each clip to the number of
            # frames it would have if processed alone so that each item's
            # output (and thus its cache entry) is independent of the other
            # items in the batch. `batch_audio_features` re-pads the
            # per-item features so audio_tower can still run batched.
            audios = mm_data["audio"]
            if not isinstance(audios, Sequence):
                raise TypeError("audio data must be a sequence")
            num_frames = []
            for audio in audios:
                if not isinstance(audio, Sized):
                    raise TypeError("each audio item must have a length")
                num_frames.append(self._get_num_audio_frames_alone(len(audio)))
            processed_data["input_features_padded"] = [
                features[:n]
                for features, n in zip(processed_data.pop("input_features"), num_frames)
            ]
            processed_data["input_features_mask"] = [
                mask[:n]
                for mask, n in zip(processed_data["input_features_mask"], num_frames)
            ]

        return processed_data

    def _get_num_audio_frames_alone(self, num_samples: int) -> int:
        """
        Get the number of mel frames that the feature extractor produces
        for a single audio clip of the given length, independent of batch
        padding.
        """
        feature_extractor = self.info.get_feature_extractor()
        call_params = inspect.signature(type(feature_extractor).__call__).parameters
        max_length = call_params["max_length"].default
        pad_to_multiple_of = call_params["pad_to_multiple_of"].default

        num_samples = min(num_samples, max_length)
        remainder = num_samples % pad_to_multiple_of
        if remainder:
            num_samples += pad_to_multiple_of - remainder

        frame_size = feature_extractor.frame_length + 1
        num_frames = (num_samples - frame_size) // feature_extractor.hop_length + 1

        return max(num_frames, 0)

    def _get_mm_fields_config(
        self,
        hf_inputs: BatchFeature,
        hf_processor_mm_kwargs: Mapping[str, object],
    ) -> Mapping[str, MultiModalFieldConfig]:
        return dict(
            pixel_values=MultiModalFieldConfig.batched("image"),
            input_features_padded=MultiModalFieldConfig.batched("audio"),
            input_features_mask=MultiModalFieldConfig.batched("audio"),
        )

    def _get_prompt_updates(
        self,
        mm_items: MultiModalDataItems,
        hf_processor_mm_kwargs: Mapping[str, Any],
        out_mm_kwargs: MultiModalKwargsItems,
    ) -> Sequence[PromptUpdate]:
        hf_processor = self.info.get_hf_processor(**hf_processor_mm_kwargs)

        prompt_updates = []

        # Handle image tokens
        if "image" in mm_items:
            image_token_id = hf_processor.image_token_id

            def get_replacement_image(item_idx: int):
                images = mm_items.get_items("image", ImageProcessorItems)
                image_size = images.get_image_size(item_idx)
                return self.info.get_image_repl(
                    image_width=image_size.width,
                    image_height=image_size.height,
                    processor=hf_processor,
                )

            prompt_updates.append(
                PromptReplacement(
                    modality="image",
                    target=[image_token_id],
                    replacement=get_replacement_image,
                )
            )

        # Handle audio tokens
        if "audio" in mm_items:
            audio_token_id = hf_processor.audio_token_id

            def get_replacement_audio(item_idx: int):
                return self.info.get_audio_repl(
                    processor=hf_processor,
                )

            prompt_updates.append(
                PromptReplacement(
                    modality="audio",
                    target=[audio_token_id],
                    replacement=get_replacement_audio,
                )
            )

        return prompt_updates

    def _apply_token_matches(
        self,
        prompt: list[int],
        mm_prompt_updates: MultiModalPromptUpdates,
    ) -> tuple[list[int], MultiModalPromptUpdatesApplyResult]:
        token_ids, res = super()._apply_token_matches(prompt, mm_prompt_updates)

        # "\n\n\n" and "\n\n\n\n" are single tokens
        # Since our replacement can insert "\n\n" next to "\n"
        # tokens, we have to combine them to be consistent with
        # the output of the tokenizer
        tokenizer = self.info.get_tokenizer()
        vocab = tokenizer.get_vocab()
        newline_1 = vocab["\n"]
        newline_2 = vocab["\n\n"]
        newline_3 = vocab["\n\n\n"]
        newline_4 = vocab["\n\n\n\n"]

        token_ids = replace_token_matches(
            token_ids,
            [newline_1, newline_2],
            [newline_3],
        )
        token_ids = replace_token_matches(
            token_ids,
            [newline_2, newline_1],
            [newline_3],
        )
        token_ids = replace_token_matches(
            token_ids,
            [newline_2, newline_2],
            [newline_4],
        )

        return token_ids, res

    def _apply_token_matches_with_placeholders(
        self,
        token_ids: list[int],
        mm_prompt_updates: MultiModalPromptUpdates,
    ) -> tuple[
        list[int],
        MultiModalPromptUpdatesApplyResult,
        Mapping[str, list[PlaceholderFeaturesInfo]],
    ]:
        new_token_ids, match_result = self._apply_token_matches(
            token_ids,
            mm_prompt_updates,
        )

        placeholders: dict[str, list[PlaceholderFeaturesInfo]] = {
            modality: [] for modality in mm_prompt_updates
        }

        if all(
            all(update_idx is not None for update_idx in update_idxs)
            for update_idxs in match_result.values()
        ):
            placeholders = dict(
                self._find_mm_placeholders(
                    new_token_ids,
                    self._matched_updates_from_result(
                        mm_prompt_updates,
                        match_result,
                    ),
                )
            )

        return new_token_ids, match_result, placeholders

    def _find_mm_placeholders(
        self,
        new_token_ids: list[int],
        mm_prompt_updates: MultiModalPromptUpdates,
    ) -> Mapping[str, list[PlaceholderFeaturesInfo]]:
        # We need to detect "\n\n" inside "\n\n\n" and "\n\n\n\n"
        tokenizer = self.info.get_tokenizer()
        vocab = tokenizer.get_vocab()
        newline_1 = vocab["\n"]
        newline_2 = vocab["\n\n"]
        newline_3 = vocab["\n\n\n"]
        newline_4 = vocab["\n\n\n\n"]

        def get_repl_toks(tok: int) -> list[int]:
            if tok == newline_3:
                return [newline_1, newline_2]
            if tok == newline_4:
                return [newline_2, newline_2]

            return [tok]

        repl_token_ids = list[int]()
        repl_orig_idxs = list[int]()
        for orig_idx, orig_tok in enumerate(new_token_ids):
            repl_toks = get_repl_toks(orig_tok)
            repl_token_ids.extend(repl_toks)
            repl_orig_idxs.extend(orig_idx for _ in range(len(repl_toks)))

        repls = super()._find_mm_placeholders(repl_token_ids, mm_prompt_updates)

        return {
            modality: [
                PlaceholderFeaturesInfo(
                    modality=p.modality,
                    item_idx=p.item_idx,
                    start_idx=repl_orig_idxs[p.start_idx],
                    tokens=p.tokens,
                    is_embed=p.is_embed,
                )
                for p in placeholders
            ]
            for modality, placeholders in repls.items()
        }

_get_num_audio_frames_alone(num_samples)

Get the number of mel frames that the feature extractor produces for a single audio clip of the given length, independent of batch padding.

Source code in vllm/model_executor/models/gemma3n_mm.py
def _get_num_audio_frames_alone(self, num_samples: int) -> int:
    """
    Get the number of mel frames that the feature extractor produces
    for a single audio clip of the given length, independent of batch
    padding.
    """
    feature_extractor = self.info.get_feature_extractor()
    call_params = inspect.signature(type(feature_extractor).__call__).parameters
    max_length = call_params["max_length"].default
    pad_to_multiple_of = call_params["pad_to_multiple_of"].default

    num_samples = min(num_samples, max_length)
    remainder = num_samples % pad_to_multiple_of
    if remainder:
        num_samples += pad_to_multiple_of - remainder

    frame_size = feature_extractor.frame_length + 1
    num_frames = (num_samples - frame_size) // feature_extractor.hop_length + 1

    return max(num_frames, 0)

Gemma3nMultimodalEmbedder

Bases: Module

Embeds token ids or soft tokens for multimodal content into language model space.

Methods:

  • forward

    Embeds token ids or soft tokens for multimodal content into language model space.

Source code in vllm/model_executor/models/gemma3n_mm.py
class Gemma3nMultimodalEmbedder(nn.Module):
    """Embeds token ids or soft tokens for multimodal content into language
    model space."""

    def __init__(
        self,
        multimodal_config: Gemma3nAudioConfig | Gemma3nVisionConfig,
        text_config: Gemma3nTextConfig,
    ):
        super().__init__()

        self.multimodal_hidden_size = multimodal_config.hidden_size
        self.eps = multimodal_config.rms_norm_eps
        self.vocab_offset = multimodal_config.vocab_offset
        self.vocab_size = multimodal_config.vocab_size
        self.text_hidden_size = text_config.hidden_size

        self.embedding = VocabParallelEmbedding(
            self.vocab_size,
            self.multimodal_hidden_size,
        )

        self.hard_embedding_norm = RMSNorm(
            self.multimodal_hidden_size,
            eps=self.eps,
        )

        self.soft_embedding_norm = RMSNorm(
            self.multimodal_hidden_size,
            eps=self.eps,
        )

        self.embedding_projection = RowParallelLinear(
            self.multimodal_hidden_size,
            self.text_hidden_size,
            bias=False,
            input_is_parallel=False,  # scatter the full-width input internally
        )

        self.embedding_post_projection_norm = RMSNorm(
            self.text_hidden_size,
            eps=self.eps,
            has_weight=False,
        )

    def forward(
        self,
        input_ids: torch.LongTensor | None = None,
        inputs_embeds: torch.Tensor | None = None,
    ) -> torch.Tensor:
        """Embeds token ids or soft tokens for multimodal content into language model space.

        Args:
            input_ids: A torch.LongTensor containing the token ids to embed. Values should be in the range
                `[vocab_offset, vocab_offset + vocab_size)`.
            inputs_embeds: A torch.Tensor containing the soft tokens to embed.

        Returns:
            A torch.Tensor of embeddings with  shape `[batch_size, seq_len, self.config.text_config.hidden_size]`.
        """  # noqa: E501
        if (input_ids is None) ^ (inputs_embeds is not None):
            raise ValueError(
                "You must specify exactly one of input_ids or inputs_embeds"
            )

        if inputs_embeds is not None:
            emb_norm = self.soft_embedding_norm(inputs_embeds)
        else:
            hard_emb = self.embedding(input_ids - self.vocab_offset)
            emb_norm = self.hard_embedding_norm(hard_emb)

        emb_norm_proj, _ = self.embedding_projection(emb_norm)
        return self.embedding_post_projection_norm(emb_norm_proj)

forward(input_ids=None, inputs_embeds=None)

Embeds token ids or soft tokens for multimodal content into language model space.

Parameters:

  • input_ids

    (LongTensor | None, default: None ) –

    A torch.LongTensor containing the token ids to embed. Values should be in the range [vocab_offset, vocab_offset + vocab_size).

  • inputs_embeds

    (Tensor | None, default: None ) –

    A torch.Tensor containing the soft tokens to embed.

Returns:

  • Tensor

    A torch.Tensor of embeddings with shape [batch_size, seq_len, self.config.text_config.hidden_size].

Source code in vllm/model_executor/models/gemma3n_mm.py
def forward(
    self,
    input_ids: torch.LongTensor | None = None,
    inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor:
    """Embeds token ids or soft tokens for multimodal content into language model space.

    Args:
        input_ids: A torch.LongTensor containing the token ids to embed. Values should be in the range
            `[vocab_offset, vocab_offset + vocab_size)`.
        inputs_embeds: A torch.Tensor containing the soft tokens to embed.

    Returns:
        A torch.Tensor of embeddings with  shape `[batch_size, seq_len, self.config.text_config.hidden_size]`.
    """  # noqa: E501
    if (input_ids is None) ^ (inputs_embeds is not None):
        raise ValueError(
            "You must specify exactly one of input_ids or inputs_embeds"
        )

    if inputs_embeds is not None:
        emb_norm = self.soft_embedding_norm(inputs_embeds)
    else:
        hard_emb = self.embedding(input_ids - self.vocab_offset)
        emb_norm = self.hard_embedding_norm(hard_emb)

    emb_norm_proj, _ = self.embedding_projection(emb_norm)
    return self.embedding_post_projection_norm(emb_norm_proj)

Gemma3nProcessingInfo

Bases: BaseProcessingInfo

Methods:

Source code in vllm/model_executor/models/gemma3n_mm.py
class Gemma3nProcessingInfo(BaseProcessingInfo):
    def get_hf_config(self):
        return self.ctx.get_hf_config(Gemma3nConfig)

    def get_hf_processor(self, **kwargs: object):
        return self.ctx.get_hf_processor(Gemma3nProcessor, **kwargs)

    def get_feature_extractor(self, **kwargs: object) -> Gemma3nAudioFeatureExtractor:
        return self.get_hf_processor(**kwargs).feature_extractor

    def get_data_parser(self):
        feature_extractor = self.get_feature_extractor()

        return MultiModalDataParser(
            target_sr=feature_extractor.sampling_rate,
            expected_hidden_size=self._get_expected_hidden_size(),
        )

    def get_supported_mm_limits(self) -> Mapping[str, int | None]:
        return {"image": None, "audio": None}

    def get_max_tokens_per_item(
        self, seq_len: int, mm_counts: Mapping[str, int]
    ) -> Mapping[str, int] | None:
        return {"image": TOKENS_PER_IMAGE, "audio": TOKENS_PER_AUDIO}

    def get_image_repl(
        self,
        *,
        image_width: int,
        image_height: int,
        processor: Gemma3nProcessor,
    ) -> PromptUpdateDetails:
        """
        Get the replacement metadata for image tokens.

        For Gemma3n, this should return the full_image_sequence which includes
        BOI token, repeated image tokens, and EOI token.
        """
        full_token_ids = cached_encode(
            processor.tokenizer, processor.full_image_sequence, add_special_tokens=False
        )
        return PromptUpdateDetails.select_token_id(
            full_token_ids, processor.image_token_id
        )

    def get_audio_repl(
        self,
        *,
        processor: Gemma3nProcessor,
    ) -> PromptUpdateDetails:
        """
        Get the replacement metadata for audio tokens.

        For Gemma3n, this should return the full_audio_sequence which includes
        BOA token, repeated audio tokens, and EOA token.
        """
        # Return the full audio sequence as defined by the processor
        full_token_ids = cached_encode(
            processor.tokenizer, processor.full_audio_sequence, add_special_tokens=False
        )
        return PromptUpdateDetails.select_token_id(
            full_token_ids, processor.audio_token_id
        )

get_audio_repl(*, processor)

Get the replacement metadata for audio tokens.

For Gemma3n, this should return the full_audio_sequence which includes BOA token, repeated audio tokens, and EOA token.

Source code in vllm/model_executor/models/gemma3n_mm.py
def get_audio_repl(
    self,
    *,
    processor: Gemma3nProcessor,
) -> PromptUpdateDetails:
    """
    Get the replacement metadata for audio tokens.

    For Gemma3n, this should return the full_audio_sequence which includes
    BOA token, repeated audio tokens, and EOA token.
    """
    # Return the full audio sequence as defined by the processor
    full_token_ids = cached_encode(
        processor.tokenizer, processor.full_audio_sequence, add_special_tokens=False
    )
    return PromptUpdateDetails.select_token_id(
        full_token_ids, processor.audio_token_id
    )

get_image_repl(*, image_width, image_height, processor)

Get the replacement metadata for image tokens.

For Gemma3n, this should return the full_image_sequence which includes BOI token, repeated image tokens, and EOI token.

Source code in vllm/model_executor/models/gemma3n_mm.py
def get_image_repl(
    self,
    *,
    image_width: int,
    image_height: int,
    processor: Gemma3nProcessor,
) -> PromptUpdateDetails:
    """
    Get the replacement metadata for image tokens.

    For Gemma3n, this should return the full_image_sequence which includes
    BOI token, repeated image tokens, and EOI token.
    """
    full_token_ids = cached_encode(
        processor.tokenizer, processor.full_image_sequence, add_special_tokens=False
    )
    return PromptUpdateDetails.select_token_id(
        full_token_ids, processor.image_token_id
    )

batch_audio_features(input_features, input_features_mask)

Return mel features and their validity mask as batched tensors.

Audio features are unpadded per item so that a multimodal cache entry does not depend on the batch it was first processed in. MultiModalFieldConfig.batched stacks items only when their shapes agree, so a batch of clips with differing durations reaches the model as a list and must be re-padded here.

Padded frames are zero-filled and marked invalid. Audio towers consume the mask, and callers keep only masked-in positions, so padding never reaches the language model.

Gemma4 shares this helper: its audio path has the same unpad/re-pad contract and the same fields.

Parameters:

  • input_features

    (Tensor | list[Tensor]) –

    (bn, s, f) tensor, or a list of (s_i, f) tensors when clip durations differ.

  • input_features_mask

    (Tensor | list[Tensor]) –

    Matching (bn, s) tensor, or list of (s_i,) tensors. True marks a valid frame.

Returns:

Source code in vllm/model_executor/models/gemma3n_mm.py
def batch_audio_features(
    input_features: torch.Tensor | list[torch.Tensor],
    input_features_mask: torch.Tensor | list[torch.Tensor],
) -> tuple[torch.Tensor, torch.Tensor]:
    """Return mel features and their validity mask as batched tensors.

    Audio features are unpadded per item so that a multimodal cache entry does
    not depend on the batch it was first processed in.
    [`MultiModalFieldConfig.batched`][vllm.multimodal.inputs.MultiModalFieldConfig.batched]
    stacks items only when their shapes agree, so a batch of clips with
    differing durations reaches the model as a list and must be re-padded here.

    Padded frames are zero-filled and marked invalid. Audio towers consume the
    mask, and callers keep only masked-in positions, so padding never reaches
    the language model.

    Gemma4 shares this helper: its audio path has the same unpad/re-pad
    contract and the same fields.

    Args:
        input_features: `(bn, s, f)` tensor, or a list of `(s_i, f)` tensors
            when clip durations differ.
        input_features_mask: Matching `(bn, s)` tensor, or list of `(s_i,)`
            tensors. `True` marks a valid frame.

    Returns:
        The `(bn, s_max, f)` features and their `(bn, s_max)` mask.
    """
    if isinstance(input_features, torch.Tensor):
        assert isinstance(input_features_mask, torch.Tensor)
        return input_features.squeeze(1), input_features_mask.squeeze(1)

    assert isinstance(input_features_mask, list)
    max_len = max(features.shape[0] for features in input_features)
    batched_features = input_features[0].new_zeros(
        (len(input_features), max_len, input_features[0].shape[-1])
    )
    batched_mask = input_features_mask[0].new_zeros(
        (len(input_features_mask), max_len), dtype=torch.bool
    )
    for i, (features, mask) in enumerate(
        zip(input_features, input_features_mask, strict=True)
    ):
        batched_features[i, : features.shape[0]] = features
        batched_mask[i, : mask.shape[0]] = mask
    return batched_features, batched_mask