Skip to content
98 changes: 42 additions & 56 deletions tensorrt_llm/_torch/models/modeling_gemma4mm.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import copy
import dataclasses
import math
from collections.abc import Sequence
from itertools import groupby
from typing import TYPE_CHECKING, Any, Dict, List, Literal, Optional, Tuple

Expand All @@ -47,7 +46,7 @@
TextPrompt,
register_input_processor,
)
from ...inputs.multimodal import MultimodalParams
from ...inputs.multimodal import MultimodalItemMetadata, MultimodalItemPartition, MultimodalParams
from ...logger import logger
from ...sampling_params import SamplingParams
from ..modules.embedding import Embedding
Expand Down Expand Up @@ -200,6 +199,26 @@ def _normalize_audio_inputs(audios, target_sr: int = 16000):
# ---------------------------------------------------------------------------


def _gemma4_input_partitions(
multimodal_data: dict[str, Any],
metadata: MultimodalItemMetadata,
) -> dict[str, dict[str, MultimodalItemPartition]]:
"""Declare item-major fields; video frame grouping stays model-owned."""
partitions = {}
for modality, fields in (
("image", ("pixel_values", "image_position_ids", "image_seq_lens")),
("audio", ("audio_features", "audio_features_mask")),
):
data = multimodal_data.get(modality)
if not isinstance(data, dict):
continue
item_count = sum(m == modality for m, _ in metadata.item_refs)
value = data.get(fields[0])
if isinstance(value, torch.Tensor) and value.ndim > 0 and value.shape[0] == item_count:
partitions[modality] = {name: MultimodalItemPartition() for name in fields}
return partitions


class Gemma4InputProcessor(BaseMultimodalInputProcessor, BaseMultimodalDummyInputsBuilder):
"""Preprocesses image inputs for Gemma4.

Expand All @@ -208,6 +227,17 @@ class Gemma4InputProcessor(BaseMultimodalInputProcessor, BaseMultimodalDummyInpu
processing using the image processor saved in the model directory.
"""

def get_mm_item_metadata(
self,
prompt_token_ids: List[int],
multimodal_data: Dict[str, Any],
mm_item_order: Optional[List[Dict[str, Any]]] = None,
) -> Optional[MultimodalItemMetadata]:
metadata = super().get_mm_item_metadata(prompt_token_ids, multimodal_data, mm_item_order)
if metadata is not None:
metadata.input_partitions.update(_gemma4_input_partitions(multimodal_data, metadata))
return metadata

# Default class-level fallback. Real value computed per-instance below
# from text_config.use_bidirectional_attention. Only 26B/31B set
# use_bidirectional_attention="vision" — their image blocks need intact
Expand Down Expand Up @@ -702,62 +732,18 @@ def partition_encoder_cache(
)
return partition

def build_multimodal_encoder_input(
def get_multimodal_item_partitions(
self,
param: MultimodalParams,
item_indices: Sequence[int],
) -> MultimodalParams:
"""Build a Gemma4 image or audio input containing selected items.

The generic implementation recognizes item-major images through a parallel `image_sizes`
field and item-major audio through the `input_features` key. Gemma4 instead provides
fixed-size `pixel_values` without `image_sizes` and uses `audio_features`.

Partial encoder-cache hits therefore need this override to slice those tensors, together
with their per-item position, length, and mask fields.
"""
modality = self._encoder_cache_modality(param)
# Partial video partitions are converted to full misses above, so they cannot call this
# hook. Delegate unexpected direct calls to the generic validation so they fail instead of
# returning an incorrectly unsliced input.
if modality not in ("image", "audio"):
return super().build_multimodal_encoder_input(param, item_indices)
input_key = {"image": "pixel_values", "audio": "audio_features"}[modality]

modality_data = param.multimodal_data[modality]
if not isinstance(modality_data, dict):
raise TypeError(
f"multimodal_data[{modality!r}] must be a dict, got {type(modality_data).__name__}"
)

item_count = len(param.multimodal_data.get("multimodal_embedding_lengths") or ())
input_tensor = modality_data.get(input_key)
# Only slice dim 0 after confirming it is the per-item axis declared by the cache metadata.
# Let the generic implementation handle any other recognized layout, or reject an
# inconsistent Gemma4 payload.
if (
not isinstance(input_tensor, torch.Tensor)
or input_tensor.dim() == 0
or input_tensor.shape[0] != item_count
):
return super().build_multimodal_encoder_input(param, item_indices)

indices = list(item_indices)
sliced = {input_key: input_tensor[indices]}
sliced = {
**modality_data,
**sliced,
**self._slice_per_item_sibling_fields(
modality_data, item_count, indices, sliced.keys()
),
}
residual_input = (
copy.copy(param.multimodal_input) if param.multimodal_input is not None else None
)
return MultimodalParams(
multimodal_data={**param.multimodal_data, modality: sliced},
multimodal_input=residual_input,
)
modality: str,
) -> dict[str, MultimodalItemPartition] | None:
partitions = super().get_multimodal_item_partitions(param, modality)
if partitions is not None:
return partitions
metadata = param.item_metadata
if metadata is None:
return None
return _gemma4_input_partitions(param.multimodal_data, metadata).get(modality)

def encode_multimodal_inputs(self, multimodal_params: List[MultimodalParams]) -> torch.Tensor:
"""Encode uncached Gemma4 image, video, and audio payloads."""
Expand Down
6 changes: 3 additions & 3 deletions tensorrt_llm/_torch/models/modeling_mistral.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,9 +49,9 @@
MultimodalPlaceholderMetadata,
MultimodalPlaceholderPlacement, TextPrompt,
register_input_processor)
from tensorrt_llm.inputs.multimodal import MultimodalParams
from tensorrt_llm.inputs.registry import (MULTIMODAL_PLACEHOLDER_REGISTRY,
MultimodalEncoderItemMetadata)
from tensorrt_llm.inputs.multimodal import (MultimodalEncoderItemMetadata,
MultimodalParams)
from tensorrt_llm.inputs.registry import MULTIMODAL_PLACEHOLDER_REGISTRY
from tensorrt_llm.inputs.utils import encode_base64_image
from tensorrt_llm.llmapi import SamplingParams
from tensorrt_llm.logger import logger
Expand Down
Loading
Loading