Files
vLLM/vllm/model_executor/models/embedding_gemma2.py
T

1142 lines
43 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""EmbeddingGemma2: bidirectional Gemma4-derived multimodal embedding model."""
import dataclasses
from collections.abc import Iterable, Mapping, Sequence
from typing import Any, cast
import torch
from torch import nn
from transformers import AutoModel, BatchFeature, PreTrainedConfig
from transformers.models.embedding_gemma2.configuration_embedding_gemma2 import (
EmbeddingGemma2Config,
EmbeddingGemma2TextConfig,
)
from transformers.models.embedding_gemma2.processing_embedding_gemma2 import (
EmbeddingGemma2Processor,
)
from transformers.video_utils import VideoMetadata
from vllm.compilation.decorators import support_torch_compile
from vllm.config import VllmConfig
from vllm.distributed import get_tensor_model_parallel_world_size
from vllm.logger import init_logger
from vllm.model_executor.layers.activation import get_act_fn
from vllm.model_executor.layers.attention import Attention
from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.layers.linear import (
ColumnParallelLinear,
QKVParallelLinear,
ReplicatedLinear,
RowParallelLinear,
)
from vllm.model_executor.layers.pooler import DispatchPooler
from vllm.model_executor.layers.quantization import QuantizationConfig
from vllm.model_executor.layers.rotary_embedding import get_rope
from vllm.model_executor.layers.vocab_parallel_embedding import (
VocabParallelEmbedding,
)
from vllm.model_executor.models.gemma4 import Gemma4MLP
from vllm.model_executor.models.gemma4_mm import (
_SUPPORTED_SOFT_TOKENS,
Gemma4DummyInputsBuilder,
Gemma4ForConditionalGeneration,
Gemma4MultimodalEmbedder,
Gemma4MultiModalProcessor,
Gemma4ProcessingInfo,
_get_max_soft_tokens,
)
from vllm.model_executor.models.interfaces import SupportsMultiModal
from vllm.model_executor.models.interfaces_base import (
VllmModelForPooling,
default_pooling_type,
)
from vllm.model_executor.models.transformers.utils import (
recursive_replace_linear,
)
from vllm.model_executor.models.utils import (
AutoWeightsLoader,
WeightsMapper,
maybe_prefix,
)
from vllm.multimodal import MULTIMODAL_REGISTRY
from vllm.multimodal.inputs import (
MultiModalKwargsItems,
VideoItem,
)
from vllm.multimodal.parse import (
AudioProcessorItems,
ImageProcessorItems,
MultiModalDataItems,
)
from vllm.multimodal.processing import (
BaseProcessingInfo,
PromptReplacement,
PromptUpdate,
PromptUpdateDetails,
)
from vllm.sequence import IntermediateTensors
from vllm.v1.attention.backend import AttentionType
logger = init_logger(__name__)
_WINDOW_OFFSET = 1 # HF masks |q-k| <= W; vLLM ENCODER_ONLY masks <= W-1.
_VIDEO_FRAME_WIDTH = 672
_VIDEO_FRAME_HEIGHT = 480
_VIDEO_MAX_FRAMES = 32
_VIDEO_MAX_SOFT_TOKENS = 140
class EmbeddingGemma2Attention(nn.Module):
"""Bidirectional attention with sliding window offset for EmbeddingGemma2."""
def __init__(
self,
config: EmbeddingGemma2TextConfig,
layer_cfg,
layer_idx: int,
cache_config,
quant_config: QuantizationConfig | None,
max_position: int,
prefix: str,
):
super().__init__()
self.config = config
self.layer_idx = layer_idx
tp = get_tensor_model_parallel_world_size()
self.num_heads = config.num_attention_heads // tp
self.num_kv_heads = max(1, layer_cfg.num_key_value_heads // tp)
self.head_dim = layer_cfg.head_dim
self.q_size = self.num_heads * self.head_dim
self.kv_size = self.num_kv_heads * self.head_dim
self.qkv_proj = QKVParallelLinear(
config.hidden_size,
self.head_dim,
config.num_attention_heads,
layer_cfg.num_key_value_heads,
bias=config.attention_bias,
quant_config=quant_config,
prefix=f"{prefix}.qkv_proj",
)
self.o_proj = RowParallelLinear(
config.num_attention_heads * self.head_dim,
config.hidden_size,
bias=config.attention_bias,
quant_config=quant_config,
prefix=f"{prefix}.o_proj",
)
self.q_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.k_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps)
self.v_norm = RMSNorm(self.head_dim, eps=config.rms_norm_eps, has_weight=False)
assert config.layer_types is not None
layer_type = config.layer_types[layer_idx]
self.is_sliding = layer_type == "sliding_attention"
assert config.rope_parameters is not None
rope_parameters = dict(config.rope_parameters[layer_type])
self.rotary_emb = get_rope(
self.head_dim,
max_position=max_position,
rope_parameters=rope_parameters,
is_neox_style=True,
)
window = config.sliding_window if self.is_sliding else None
self.attn = Attention(
self.num_heads,
self.head_dim,
scale=1.0,
num_kv_heads=self.num_kv_heads,
cache_config=cache_config,
quant_config=quant_config,
per_layer_sliding_window=(window + _WINDOW_OFFSET)
if window is not None
else None,
attn_type=AttentionType.ENCODER_ONLY,
prefix=f"{prefix}.attn",
)
def forward(
self, positions: torch.Tensor, hidden_states: torch.Tensor
) -> torch.Tensor:
qkv, _ = self.qkv_proj(hidden_states)
q, k, v = qkv.split([self.q_size, self.kv_size, self.kv_size], dim=-1)
q = q.unflatten(-1, (self.num_heads, self.head_dim))
q = self.q_norm(q)
q = q.flatten(-2, -1)
k = k.unflatten(-1, (self.num_kv_heads, self.head_dim))
k = self.k_norm(k)
k = k.flatten(-2, -1)
q, k = self.rotary_emb(positions, q, k)
v = v.unflatten(-1, (self.num_kv_heads, self.head_dim))
v = self.v_norm(v)
v = v.flatten(-2, -1)
attn_output = self.attn(q, k, v)
out, _ = self.o_proj(attn_output)
return out
class EmbeddingGemma2PLEBlock(nn.Module):
"""Per-layer-embedding residual block (mirrors HF ple_block)."""
def __init__(
self,
config: EmbeddingGemma2TextConfig,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.hidden_size = config.hidden_size
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
self.per_layer_input_gate = ReplicatedLinear(
self.hidden_size,
self.hidden_size_per_layer_input,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.per_layer_input_gate",
return_bias=False,
)
self.per_layer_projection = ReplicatedLinear(
self.hidden_size_per_layer_input,
self.hidden_size,
bias=False,
quant_config=quant_config,
prefix=f"{prefix}.per_layer_projection",
return_bias=False,
)
self.post_per_layer_input_norm = RMSNorm(
self.hidden_size, eps=config.rms_norm_eps
)
self.act_fn = get_act_fn(config.hidden_activation)
def forward(
self, hidden_states: torch.Tensor, per_layer_input: torch.Tensor
) -> torch.Tensor:
residual = hidden_states
gate = self.per_layer_input_gate(hidden_states)
gate = self.act_fn(gate)
gated = gate * per_layer_input
proj = self.per_layer_projection(gated)
proj = self.post_per_layer_input_norm(proj)
return residual + proj
class EmbeddingGemma2DecoderLayer(nn.Module):
"""Transformer decoder layer for EmbeddingGemma2 text model with PLE gating."""
def __init__(
self,
config: EmbeddingGemma2TextConfig,
idx: int,
cache_config,
quant_config: QuantizationConfig | None,
max_position: int,
prefix: str,
):
super().__init__()
lc = config.per_layer_config[idx]
self.self_attn = EmbeddingGemma2Attention(
config,
lc,
idx,
cache_config,
quant_config,
max_position,
f"{prefix}.self_attn",
)
self.mlp = Gemma4MLP(
hidden_size=config.hidden_size,
intermediate_size=config.intermediate_size,
hidden_activation=config.hidden_activation,
quant_config=quant_config,
prefix=f"{prefix}.mlp",
)
self.ple_block = EmbeddingGemma2PLEBlock(
config,
quant_config=quant_config,
prefix=f"{prefix}.ple_block",
)
self.input_layernorm = RMSNorm(config.hidden_size, eps=config.rms_norm_eps)
self.post_attention_layernorm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.pre_feedforward_layernorm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.post_feedforward_layernorm = RMSNorm(
config.hidden_size, eps=config.rms_norm_eps
)
self.layer_scalar: torch.Tensor
self.register_buffer("layer_scalar", torch.ones(1), persistent=True)
def forward(
self,
positions: torch.Tensor,
h: torch.Tensor,
per_layer_input: torch.Tensor,
) -> torch.Tensor:
r = h
h = (
self.post_attention_layernorm(
self.self_attn(positions, self.input_layernorm(h))
)
+ r
)
r = h
h = (
self.post_feedforward_layernorm(self.mlp(self.pre_feedforward_layernorm(h)))
+ r
)
h = self.ple_block(h, per_layer_input)
return h * cast(torch.Tensor, self.layer_scalar)
class EmbeddingGemma2TextPLE(nn.Module):
"""Per-layer embedding (PLE) projection and normalization module."""
def __init__(
self,
config: EmbeddingGemma2TextConfig,
quant_config: QuantizationConfig | None = None,
prefix: str = "",
):
super().__init__()
self.num_hidden_layers = config.num_hidden_layers
self.hidden_size_per_layer_input = config.hidden_size_per_layer_input
self.per_layer_model_projection = ColumnParallelLinear(
config.hidden_size,
config.num_hidden_layers * config.hidden_size_per_layer_input,
bias=False,
gather_output=True,
quant_config=quant_config,
prefix=f"{prefix}.per_layer_model_projection",
)
self.per_layer_model_projection_scale = config.hidden_size**-0.5
self.per_layer_projection_norm = RMSNorm(
config.hidden_size_per_layer_input, eps=config.rms_norm_eps
)
def forward(self, inputs_embeds: torch.Tensor) -> torch.Tensor:
proj, _ = self.per_layer_model_projection(inputs_embeds)
proj = proj * self.per_layer_model_projection_scale
proj = proj.reshape(
*inputs_embeds.shape[:-1],
self.num_hidden_layers,
self.hidden_size_per_layer_input,
)
return self.per_layer_projection_norm(proj)
@support_torch_compile
class EmbeddingGemma2TextModel(nn.Module):
"""Text embedding backbone for EmbeddingGemma2 with bidirectional attention."""
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
cfg = vllm_config.model_config.hf_config.text_config
self.config = cfg
max_pos = min(
cfg.max_position_embeddings, vllm_config.model_config.max_model_len
)
self.embed_tokens = VocabParallelEmbedding(
cfg.vocab_size,
cfg.hidden_size,
quant_config=vllm_config.quant_config,
prefix=f"{prefix}.embed_tokens",
)
self.layers = nn.ModuleList(
[
EmbeddingGemma2DecoderLayer(
cfg,
i,
vllm_config.cache_config,
vllm_config.quant_config,
max_pos,
f"{prefix}.layers.{i}",
)
for i in range(cfg.num_hidden_layers)
]
)
self.norm = RMSNorm(cfg.hidden_size, eps=cfg.rms_norm_eps)
self.ple = EmbeddingGemma2TextPLE(
cfg,
quant_config=vllm_config.quant_config,
prefix=f"{prefix}.ple",
)
self.embedding_projection = ReplicatedLinear(
cfg.hidden_size,
cfg.embedding_dim,
bias=False,
quant_config=vllm_config.quant_config,
prefix=f"{prefix}.embedding_projection",
return_bias=False,
)
self.embed_scale: torch.Tensor
self.register_buffer(
"embed_scale",
torch.tensor(cfg.hidden_size**0.5),
persistent=False,
)
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
embed_scale = cast(torch.Tensor, self.embed_scale)
dtype = cast(torch.dtype, self.embed_tokens.weight.dtype)
return self.embed_tokens(input_ids) * embed_scale.to(dtype)
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor:
if inputs_embeds is None:
inputs_embeds = self.embed_input_ids(input_ids)
per_layer = self.ple(inputs_embeds)
h = inputs_embeds
for i, layer in enumerate(self.layers):
h = layer(positions, h, per_layer[..., i, :])
h = self.norm(h)
h = self.embedding_projection(h)
return h
class EmbeddingGemma2ProcessingInfo(Gemma4ProcessingInfo):
"""Processing information and token budgeting for EmbeddingGemma2."""
def get_hf_config(self) -> EmbeddingGemma2Config:
return self.ctx.get_hf_config(EmbeddingGemma2Config)
def get_hf_processor( # type: ignore[override]
self, **kwargs: object
) -> EmbeddingGemma2Processor:
return self.ctx.get_hf_processor(EmbeddingGemma2Processor, **kwargs)
def get_default_tok_params(self):
# Base-class behavior: add_special_tokens=True (BOS + EOS)
return BaseProcessingInfo.get_default_tok_params(self)
def get_mm_max_tokens_per_item(
self, seq_len: int, mm_counts: Mapping[str, int]
) -> Mapping[str, int] | None:
config = self.get_hf_config()
merged_kwargs = self.ctx.get_merged_mm_kwargs({})
val, _ = _get_max_soft_tokens(merged_kwargs)
if isinstance(val, int) and val in _SUPPORTED_SOFT_TOKENS:
tokens_per_image = val + 2
else:
tokens_per_image = (
getattr(config.vision_config, "default_output_length", 256) + 2
)
tokens: dict[str, int] = {"image": tokens_per_image}
if config.audio_config is not None:
processor = self.get_hf_processor()
tokens["audio"] = processor.audio_seq_length + 2
processor = self.get_hf_processor()
video_proc = getattr(processor, "video_processor", None)
max_frames = getattr(video_proc, "max_frames", 32)
mm_config = self.ctx.model_config.get_multimodal_config()
# The loader's max_frames cap is 128 when media_io_kwargs can raise it
video_media_io = (
getattr(mm_config, "media_io_kwargs", {}).get("video", {})
if mm_config
else {}
)
media_io_max_frames = video_media_io.get("max_frames")
if media_io_max_frames is not None:
max_frames = min(int(media_io_max_frames), 128)
elif "max_frames" in merged_kwargs:
max_frames = min(int(merged_kwargs["max_frames"]), 128)
video_opts = mm_config.limit_per_prompt.get("video") if mm_config else None
if video_opts is not None:
num_frames_opt = getattr(video_opts, "num_frames", None)
if isinstance(num_frames_opt, int):
max_frames = min(max_frames, num_frames_opt)
video_max_soft = merged_kwargs.get(
"max_soft_tokens",
getattr(video_proc, "max_soft_tokens", _VIDEO_MAX_SOFT_TOKENS),
)
if video_max_soft is not None and video_max_soft not in _SUPPORTED_SOFT_TOKENS:
raise ValueError(
f"Unsupported max_soft_tokens={video_max_soft} for video. "
f"Valid values are {_SUPPORTED_SOFT_TOKENS}."
)
if not isinstance(video_max_soft, int):
video_max_soft = _VIDEO_MAX_SOFT_TOKENS
# Token allocation per video frame verified against HF EmbeddingGemma2Processor:
# - Without timestamps: boi (1) + video_token * num_soft_tokens + eoi (1)
# Total = num_soft_tokens + 2
# - With timestamps: timestamp prefix string (e.g. ' 01:23 ' = up to 7 tokens)
# Total = num_soft_tokens + 2 + 7
add_ts = getattr(video_proc, "add_timestamps", False) or merged_kwargs.get(
"add_timestamps", False
)
ts_slack = 7 if add_ts else 0
tokens["video"] = max_frames * (video_max_soft + 2 + ts_slack)
return tokens
def get_image_repl( # type: ignore[override]
self,
*,
image_width: int,
image_height: int,
processor: EmbeddingGemma2Processor | None,
max_soft_tokens: int | None = None,
) -> PromptUpdateDetails:
if processor is None:
processor = self.get_hf_processor()
num_soft = self._compute_num_soft_tokens(
image_width,
image_height,
max_soft_tokens=max_soft_tokens,
)
config = self.get_hf_config()
assert config.boi_token_id is not None
assert processor.image_token_id is not None
assert config.eoi_token_id is not None
token_ids = (
[config.boi_token_id]
+ [processor.image_token_id] * num_soft
+ [config.eoi_token_id]
)
return PromptUpdateDetails.select_token_id(token_ids, processor.image_token_id)
def get_audio_repl( # type: ignore[override]
self,
*,
audio_len: int,
processor: EmbeddingGemma2Processor | None,
) -> PromptUpdateDetails:
if processor is None:
processor = self.get_hf_processor()
sampling_rate = processor.feature_extractor.sampling_rate
num_tokens = self._compute_audio_num_tokens(
audio_len, sampling_rate, processor.audio_seq_length
)
config = self.get_hf_config()
assert config.boa_token_id is not None
assert processor.audio_token_id is not None
assert config.eoa_token_index is not None
token_ids = (
[config.boa_token_id]
+ [processor.audio_token_id] * num_tokens
+ [config.eoa_token_index]
)
return PromptUpdateDetails.select_token_id(token_ids, processor.audio_token_id)
def get_video_repl( # type: ignore[override]
self,
*,
timestamps: list[float],
num_soft_tokens_per_frame: list[int],
processor: EmbeddingGemma2Processor,
add_timestamps: bool = False,
) -> PromptUpdateDetails:
tokenizer = self.ctx.get_tokenizer()
config = self.get_hf_config()
boi_token_id = config.boi_token_id
eoi_token_id = config.eoi_token_id
video_token_id = processor.video_token_id
assert boi_token_id is not None
assert eoi_token_id is not None
assert video_token_id is not None
all_token_ids: list[int] = []
if not add_timestamps:
for n_tokens in num_soft_tokens_per_frame:
all_token_ids.append(boi_token_id)
all_token_ids.extend([video_token_id] * n_tokens)
all_token_ids.append(eoi_token_id)
else:
for i, (ts, n_tokens) in enumerate(
zip(timestamps, num_soft_tokens_per_frame)
):
minutes = int(ts // 60)
seconds = int(ts % 60)
ts_str = f"{minutes:02d}:{seconds:02d}"
prefix = f" {ts_str} " if i > 0 else f"{ts_str} "
ts_token_ids = tokenizer.encode(prefix, add_special_tokens=False)
all_token_ids.extend(ts_token_ids)
all_token_ids.append(boi_token_id)
all_token_ids.extend([video_token_id] * n_tokens)
all_token_ids.append(eoi_token_id)
return PromptUpdateDetails.select_token_id(all_token_ids, video_token_id)
class EmbeddingGemma2DummyInputsBuilder(Gemma4DummyInputsBuilder):
"""Dummy input builder for EmbeddingGemma2 profiling and testing."""
def get_dummy_text(self, mm_counts: Mapping[str, int]) -> str:
num_images = mm_counts.get("image", 0)
num_audios = mm_counts.get("audio", 0)
num_videos = mm_counts.get("video", 0)
processor = self.info.get_hf_processor()
text = (
(processor.image_token * num_images)
+ (processor.audio_token * num_audios if processor.audio_token else "")
+ (processor.video_token * num_videos)
)
return text
def _get_dummy_videos( # type: ignore[override]
self,
*,
width: int,
height: int,
num_frames: int,
num_videos: int,
overrides=None,
) -> list[VideoItem]:
num_frames = _VIDEO_MAX_FRAMES
width = _VIDEO_FRAME_WIDTH
height = _VIDEO_FRAME_HEIGHT
videos = super(Gemma4DummyInputsBuilder, self)._get_dummy_videos(
width=width,
height=height,
num_frames=num_frames,
num_videos=num_videos,
overrides=overrides,
)
video_items: list[VideoItem] = []
for video in videos:
video_num_frames = video.shape[0]
video_metadata = {
"fps": 1.0,
"duration": float(video_num_frames),
"total_num_frames": video_num_frames,
"frames_indices": list(range(video_num_frames)),
"video_backend": "opencv",
"do_sample_frames": False,
}
video_items.append((video, video_metadata))
return video_items
class EmbeddingGemma2MultiModalProcessor(Gemma4MultiModalProcessor):
"""Multimodal input processor for EmbeddingGemma2."""
info: EmbeddingGemma2ProcessingInfo
def _apply_hf_processor_main(
self,
mm_items: MultiModalDataItems,
hf_kwargs: Mapping[str, object],
) -> BatchFeature:
hf_data, hf_kwargs, passthrough_data = self._get_hf_mm_inputs(
mm_items, hf_kwargs
)
if not hf_data:
return self._finalize_hf_mm_data(
hf_data, hf_kwargs, passthrough_data, BatchFeature()
)
prompt_text = hf_data.pop("text")
assert isinstance(prompt_text, str)
processor = self.info.get_hf_processor()
video_outputs: dict[str, Any] = {}
if videos := hf_data.pop("videos", []):
assert isinstance(videos, list)
all_video_pixel_values: list[torch.Tensor] = []
all_video_position_ids: list[torch.Tensor] = []
video_num_soft_tokens_per_video: list[list[int]] = []
video_timestamps_per_video: list[list[float]] = []
video_frame_counts: list[int] = []
for item in videos:
video_array, metadata = item
if hf_kwargs.get("num_frames") not in (-1, None):
raise ValueError(
"EmbeddingGemma2 does not accept num_frames; "
"use max_frames/fps."
)
if isinstance(metadata, dict):
if not metadata.get("do_sample_frames", True) and any(
k in hf_kwargs
for k in ("fps", "max_frames", "overflow_strategy")
):
logger.warning_once(
"For file/URL-loaded videos, "
"fps/max_frames/overflow_strategy in mm_processor_kwargs "
"have no effect because frames are decoded and sampled by "
"media_io. Please specify these sampling options via "
"media_io_kwargs instead."
)
valid_meta_fields = {
f.name for f in dataclasses.fields(VideoMetadata)
}
meta_dict = {
k: v for k, v in metadata.items() if k in valid_meta_fields
}
metadata_obj = VideoMetadata(**meta_dict)
else:
if getattr(metadata, "do_sample_frames", True) is False and any(
k in hf_kwargs
for k in ("fps", "max_frames", "overflow_strategy")
):
logger.warning_once(
"For file/URL-loaded videos, "
"fps/max_frames/overflow_strategy in mm_processor_kwargs "
"have no effect because frames are decoded and sampled by "
"media_io. Please specify these sampling options via "
"media_io_kwargs instead."
)
metadata_obj = metadata
video_mm_kwargs = dict(hf_kwargs)
video_mm_kwargs.pop("num_frames", None)
video_mm_kwargs["return_metadata"] = True
if isinstance(metadata, dict) and "do_sample_frames" in metadata:
video_mm_kwargs["do_sample_frames"] = metadata["do_sample_frames"]
elif hasattr(metadata, "do_sample_frames"):
video_mm_kwargs["do_sample_frames"] = metadata.do_sample_frames
single_video_out = self.info.ctx.call_hf_processor(
self.info.get_hf_processor(**video_mm_kwargs),
dict(
text=processor.video_token,
videos=[video_array],
video_metadata=[metadata_obj],
),
video_mm_kwargs,
)
all_video_pixel_values.append(single_video_out["pixel_values_videos"])
all_video_position_ids.append(single_video_out["video_position_ids"])
num_frames = int(single_video_out["num_frames_per_video"][0])
video_frame_counts.append(num_frames)
num_soft = single_video_out.get("num_soft_tokens_per_video")
if num_soft is not None:
soft_val = num_soft[0]
if isinstance(soft_val, torch.Tensor):
soft_val = soft_val.item()
soft_tokens_list = [int(soft_val)] * num_frames
elif "video_position_ids" in single_video_out and num_frames > 0:
pos = single_video_out["video_position_ids"]
pooling_k2 = (
getattr(processor.video_processor, "pooling_kernel_size", 3)
** 2
)
valid_patches = (pos[0] != -1).all(dim=-1).sum().item()
soft_tokens_list = [valid_patches // pooling_k2] * num_frames
elif "input_ids" in single_video_out and num_frames > 0:
vtok = (
(
single_video_out["input_ids"][0]
== processor.tokenizer.video_token_id
)
.sum()
.item()
)
soft_tokens_list = [vtok // num_frames] * num_frames
else:
raise ValueError(
f"Unable to determine soft tokens for video frame: "
f"keys={list(single_video_out.keys())}"
)
video_num_soft_tokens_per_video.append(soft_tokens_list)
# Use returned video_metadata if available, else input metadata_obj
ret_meta = single_video_out.get("video_metadata")
meta_for_ts = (
ret_meta[0] if (ret_meta and len(ret_meta) > 0) else metadata_obj
)
add_ts = hf_kwargs.get(
"add_timestamps",
getattr(processor.video_processor, "add_timestamps", False),
)
if add_ts:
try:
ts = meta_for_ts.timestamps
except ValueError as e:
raise ValueError(
"Asked to build a prompt with frame timestamps, but no "
"valid fps/frames_indices was available in video metadata."
) from e
video_timestamps_per_video.append([float(t) for t in ts])
else:
try:
ts = meta_for_ts.timestamps
video_timestamps_per_video.append([float(t) for t in ts])
except ValueError:
video_timestamps_per_video.append([0.0] * num_frames)
video_outputs = {
"pixel_values_videos": torch.cat(all_video_pixel_values, dim=0),
"pixel_position_ids_videos": torch.cat(all_video_position_ids, dim=0),
"video_frame_counts": torch.tensor(
video_frame_counts, dtype=torch.long
),
"video_num_soft_tokens": video_num_soft_tokens_per_video,
"video_timestamps": video_timestamps_per_video,
}
if hf_data:
remaining_counts: dict[str, int] = {}
if "images" in hf_data:
images_list = hf_data["images"]
remaining_counts["image"] = (
len(images_list) if isinstance(images_list, list) else 1
)
if "audio" in hf_data:
audio_list = hf_data["audio"]
remaining_counts["audio"] = (
len(audio_list) if isinstance(audio_list, (list, tuple)) else 1
)
remaining_prompt_text = self.dummy_inputs.get_dummy_text(remaining_counts)
call_kwargs = dict(hf_kwargs)
call_kwargs.pop("num_frames", None)
processed_data = self.info.ctx.call_hf_processor(
self.info.get_hf_processor(**call_kwargs),
dict(text=remaining_prompt_text, **hf_data),
call_kwargs,
)
else:
processed_data = BatchFeature()
if "image_position_ids" in processed_data:
processed_data["pixel_position_ids"] = processed_data.pop(
"image_position_ids"
)
if "input_features" in processed_data:
input_features = processed_data.pop("input_features")
input_features_mask = processed_data.pop("input_features_mask")
if not isinstance(input_features, list):
unpadded_features = []
unpadded_masks = []
for i in range(input_features.shape[0]):
mask = input_features_mask[i]
valid_len = int(mask.sum().item())
unpadded_features.append(input_features[i, :valid_len])
unpadded_masks.append(mask[:valid_len])
processed_data["input_features_padded"] = unpadded_features
processed_data["input_features_mask"] = unpadded_masks
else:
processed_data["input_features_padded"] = input_features
processed_data["input_features_mask"] = input_features_mask
processed_data.update(video_outputs)
processed_data.pop("num_frames_per_video", None)
processed_data.pop("video_position_ids", None)
return self._finalize_hf_mm_data(
hf_data, hf_kwargs, passthrough_data, processed_data
)
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 = []
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)
merged_kwargs = self.info.ctx.get_merged_mm_kwargs(
hf_processor_mm_kwargs,
)
val, _ = _get_max_soft_tokens(merged_kwargs)
max_soft_tokens = (
val
if isinstance(val, int) and val in _SUPPORTED_SOFT_TOKENS
else None
)
return self.info.get_image_repl(
image_width=image_size.width,
image_height=image_size.height,
processor=hf_processor,
max_soft_tokens=max_soft_tokens,
)
prompt_updates.append(
PromptReplacement(
modality="image",
target=[image_token_id],
replacement=get_replacement_image,
)
)
if "video" in mm_items:
video_token_id = hf_processor.video_token_id
assert video_token_id is not None
def get_replacement_video(item_idx: int):
out_item = out_mm_kwargs["video"][item_idx]
raw_ts = out_item["video_timestamps"].data
if isinstance(raw_ts, torch.Tensor):
ts_list: list[Any] = raw_ts.tolist()
elif isinstance(raw_ts, (list, tuple)):
ts_list = list(raw_ts)
else:
ts_list = [raw_ts]
timestamps = [float(t) for t in ts_list]
raw_soft = out_item["video_num_soft_tokens"].data
if isinstance(raw_soft, torch.Tensor):
soft_list: list[Any] = raw_soft.tolist()
elif isinstance(raw_soft, (list, tuple)):
soft_list = list(raw_soft)
else:
soft_list = [raw_soft]
num_soft = [int(n) for n in soft_list]
add_ts = hf_processor_mm_kwargs.get(
"add_timestamps",
getattr(hf_processor.video_processor, "add_timestamps", False),
)
return self.info.get_video_repl(
timestamps=timestamps,
num_soft_tokens_per_frame=num_soft,
processor=hf_processor,
add_timestamps=add_ts,
)
prompt_updates.append(
PromptReplacement(
modality="video",
target=[video_token_id],
replacement=get_replacement_video,
)
)
if "audio" in mm_items:
audio_token_id = hf_processor.audio_token_id
assert audio_token_id is not None
def get_replacement_audio(item_idx: int):
audios = mm_items.get_items("audio", AudioProcessorItems)
audio_len = audios.get_audio_length(item_idx)
return self.info.get_audio_repl(
audio_len=audio_len,
processor=hf_processor,
)
prompt_updates.append(
PromptReplacement(
modality="audio",
target=[audio_token_id],
replacement=get_replacement_audio,
)
)
return prompt_updates
def _embedding_gemma2_weights_mapper(
config_or_layers: PreTrainedConfig | int | None = None,
) -> WeightsMapper:
return WeightsMapper(
orig_to_new_stacked={
".self_attn.q_proj.weight": (".self_attn.qkv_proj.weight", "q"),
".self_attn.k_proj.weight": (".self_attn.qkv_proj.weight", "k"),
".self_attn.v_proj.weight": (".self_attn.qkv_proj.weight", "v"),
".self_attn.q_proj.bias": (".self_attn.qkv_proj.bias", "q"),
".self_attn.k_proj.bias": (".self_attn.qkv_proj.bias", "k"),
".self_attn.v_proj.bias": (".self_attn.qkv_proj.bias", "v"),
".mlp.gate_proj.weight": (".mlp.gate_up_proj.weight", 0),
".mlp.up_proj.weight": (".mlp.gate_up_proj.weight", 1),
".mlp.gate_proj.bias": (".mlp.gate_up_proj.bias", 0),
".mlp.up_proj.bias": (".mlp.gate_up_proj.bias", 1),
}
)
@MULTIMODAL_REGISTRY.register_processor(
EmbeddingGemma2MultiModalProcessor,
info=EmbeddingGemma2ProcessingInfo,
dummy_inputs=EmbeddingGemma2DummyInputsBuilder,
)
@default_pooling_type(seq_pooling_type="MEAN", tok_pooling_type="ALL")
class EmbeddingGemma2Model(nn.Module, SupportsMultiModal, VllmModelForPooling):
"""Multimodal pooling embedding model supporting text, vision, and audio."""
is_pooling_model = True
hf_to_vllm_mapper = _embedding_gemma2_weights_mapper()
packed_modules_mapping = {
"qkv_proj": [
"q_proj",
"k_proj",
"v_proj",
],
"gate_up_proj": [
"gate_proj",
"up_proj",
],
}
# TODO(lucianomartins): Extract Gemma4EncoderMixin into a shared module
# once generative model tests are migrated.
# TODO(lucianomartins): Investigate @support_torch_compile integration for encoder.
_parse_and_validate_image_input = (
Gemma4ForConditionalGeneration._parse_and_validate_image_input
)
_parse_and_validate_video_input = (
Gemma4ForConditionalGeneration._parse_and_validate_video_input
)
_parse_and_validate_audio_input = (
Gemma4ForConditionalGeneration._parse_and_validate_audio_input
)
_parse_and_validate_multimodal_inputs = (
Gemma4ForConditionalGeneration._parse_and_validate_multimodal_inputs
)
_process_image_input = Gemma4ForConditionalGeneration._process_image_input
_process_video_input = Gemma4ForConditionalGeneration._process_video_input
_process_audio_input = Gemma4ForConditionalGeneration._process_audio_input
embed_multimodal = Gemma4ForConditionalGeneration.embed_multimodal
_encoder_chunk = staticmethod(Gemma4ForConditionalGeneration._encoder_chunk)
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
nn.Module.__init__(self)
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.model_dtype = vllm_config.model_config.dtype
self.vllm_config = vllm_config
self.hf_to_vllm_mapper = _embedding_gemma2_weights_mapper(config)
lora_config = vllm_config.lora_config
self._enable_mm_lora = bool(
lora_config is not None and lora_config.enable_tower_connector_lora
)
tower_quant: QuantizationConfig | None
if quant_config and quant_config.get_name() in [
"bitsandbytes",
"torchao",
"compressed-tensors",
]:
tower_quant = quant_config
else:
vision_cfg = getattr(config, "vision_config", None)
quantizable = (
vision_cfg is not None
and vision_cfg.hidden_size % 64 == 0
and vision_cfg.intermediate_size % 64 == 0
)
tower_quant = quant_config if quantizable else None
# ---- Vision tower (shared by image and video) ----
self.embed_vision: Gemma4MultimodalEmbedder | None
if getattr(config, "vision_config", None) is not None:
with self._mark_tower_model(vllm_config, {"image", "video"}):
self.vision_tower = AutoModel.from_config(config=config.vision_config)
self.embed_vision = Gemma4MultimodalEmbedder(
config.vision_config,
config.text_config,
quant_config=tower_quant,
prefix=maybe_prefix(prefix, "embed_vision"),
)
recursive_replace_linear(
self.vision_tower,
tower_quant,
prefix=maybe_prefix(prefix, "vision_tower"),
)
else:
self.vision_tower = None
self.embed_vision = None
# ---- Audio tower ----
self.embed_audio: Gemma4MultimodalEmbedder | None
if getattr(config, "audio_config", None) is not None:
with self._mark_tower_model(vllm_config, "audio"):
self.audio_tower = AutoModel.from_config(config=config.audio_config)
self.audio_tower.post_init()
self.embed_audio = Gemma4MultimodalEmbedder(
config.audio_config,
config.text_config,
quant_config=tower_quant,
prefix=maybe_prefix(prefix, "embed_audio"),
)
recursive_replace_linear(
self.audio_tower,
tower_quant,
prefix=maybe_prefix(prefix, "audio_tower"),
)
else:
self.audio_tower = None
self.embed_audio = None
# ---- Language model ----
with self._mark_language_model(vllm_config):
self.language_model = EmbeddingGemma2TextModel(
vllm_config=vllm_config,
prefix=maybe_prefix(prefix, "language_model"),
)
assert vllm_config.model_config.pooler_config is not None
self.pooler = DispatchPooler.for_embedding(
vllm_config.model_config.pooler_config
)
def get_language_model(self) -> nn.Module: # type: ignore[override]
return self.language_model
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
**kwargs: object,
) -> torch.Tensor:
return self.language_model(input_ids, positions, inputs_embeds=inputs_embeds)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
ignore_prefixes = [
"embed_vision.embedding.",
"embed_audio.embedding.",
]
if self.audio_tower is None:
ignore_prefixes.extend(["audio_tower.", "embed_audio."])
if self.vision_tower is None:
ignore_prefixes.extend(["vision_tower.", "embed_vision."])
return AutoWeightsLoader(
self,
ignore_unexpected_prefixes=ignore_prefixes,
).load_weights(weights, mapper=self.hf_to_vllm_mapper)
@classmethod
def get_placeholder_str(cls, modality: str, i: int = 0) -> str | None:
if modality == "image":
return "<|image|>"
if modality == "audio":
return "<|audio|>"
if modality == "video":
return "<|video|>"
raise ValueError(f"Unsupported modality: {modality}")