94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
600 lines
21 KiB
Python
600 lines
21 KiB
Python
# Copyright 2025 SGLang Team
|
||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||
# you may not use this file except in compliance with the License.
|
||
# You may obtain a copy of the License at
|
||
#
|
||
# http://www.apache.org/licenses/LICENSE-2.0
|
||
#
|
||
# Unless required by applicable law or agreed to in writing, software
|
||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||
# See the License for the specific language governing permissions and
|
||
# limitations under the License.
|
||
# ==============================================================================
|
||
from __future__ import annotations
|
||
|
||
from typing import Optional, Tuple
|
||
|
||
import torch
|
||
import torch.nn as nn
|
||
import torch.nn.functional as F
|
||
from einops import rearrange
|
||
from transformers import Gemma4VisionConfig
|
||
|
||
from sglang.srt.layers.attention.vision import QKV_BACKEND_IMPL
|
||
from sglang.srt.layers.clippable_linear import (
|
||
ClippableGateUpParallelLinear,
|
||
ClippableQKVParallelLinear,
|
||
ClippableRowParallelLinear,
|
||
)
|
||
from sglang.srt.layers.layernorm import Gemma4RMSNorm
|
||
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
||
from sglang.srt.runtime_context import get_parallel
|
||
from sglang.srt.utils import add_prefix, get_device_capability, is_cuda, is_hip
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# 2-D Multidimensional RoPE (matches HF Gemma4RotaryEmbedding for vision)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _rotate_half(x: torch.Tensor) -> torch.Tensor:
|
||
x1 = x[..., : x.shape[-1] // 2]
|
||
x2 = x[..., x.shape[-1] // 2 :]
|
||
return torch.cat((-x2, x1), dim=-1)
|
||
|
||
|
||
def _apply_rotary(
|
||
x: torch.Tensor, cos: torch.Tensor, sin: torch.Tensor
|
||
) -> torch.Tensor:
|
||
return (x * cos) + (_rotate_half(x) * sin)
|
||
|
||
|
||
class Gemma4VisionRotaryEmbedding(nn.Module):
|
||
"""Compute 2-D multidimensional RoPE cos/sin for patch positions."""
|
||
|
||
def __init__(self, config: Gemma4VisionConfig):
|
||
super().__init__()
|
||
self.head_dim = config.head_dim
|
||
self.rope_theta: float = config.rope_parameters["rope_theta"]
|
||
|
||
@torch.no_grad()
|
||
def forward(
|
||
self, x: torch.Tensor, patch_positions: torch.Tensor
|
||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||
"""
|
||
Args:
|
||
x: [batch, seq, hidden] – only used for device/dtype.
|
||
patch_positions: [batch, num_patches, 2] – (x, y) coordinates.
|
||
Returns:
|
||
(cos, sin) each of shape [batch, num_patches, head_dim].
|
||
"""
|
||
ndim = patch_positions.shape[-1] # 2
|
||
head_dim_per_dim = self.head_dim // ndim
|
||
|
||
all_embs = []
|
||
for d in range(ndim):
|
||
dim_inv_freq = 1.0 / (
|
||
self.rope_theta
|
||
** (
|
||
torch.arange(
|
||
0, head_dim_per_dim, 2, device=x.device, dtype=torch.float
|
||
)
|
||
/ head_dim_per_dim
|
||
)
|
||
)
|
||
dim_inv_freq_expanded = dim_inv_freq[None, :, None].expand(
|
||
patch_positions.shape[0], -1, 1
|
||
)
|
||
dim_positions = patch_positions[:, :, d].float()
|
||
dim_positions_expanded = dim_positions[:, None, :]
|
||
|
||
dim_freqs = (dim_inv_freq_expanded @ dim_positions_expanded).transpose(1, 2)
|
||
dim_emb = torch.cat((dim_freqs, dim_freqs), dim=-1)
|
||
all_embs.append(dim_emb)
|
||
|
||
emb = torch.cat(all_embs, dim=-1)
|
||
cos = emb.cos().to(dtype=x.dtype)
|
||
sin = emb.sin().to(dtype=x.dtype)
|
||
return cos, sin
|
||
|
||
|
||
def _apply_multidimensional_rope(
|
||
x: torch.Tensor,
|
||
cos: torch.Tensor,
|
||
sin: torch.Tensor,
|
||
) -> torch.Tensor:
|
||
"""Apply 2-D RoPE to x of shape [batch*seq, heads, head_dim].
|
||
|
||
cos/sin have shape [batch, seq, head_dim]. We split along head_dim into
|
||
ndim=2 parts and apply standard rotary to each independently.
|
||
"""
|
||
ndim = 2
|
||
chunk_size = x.shape[-1] // ndim
|
||
x_parts = x.split(chunk_size, dim=-1)
|
||
cos_parts = cos.split(chunk_size, dim=-1)
|
||
sin_parts = sin.split(chunk_size, dim=-1)
|
||
y_parts = [
|
||
_apply_rotary(x_parts[k], cos_parts[k], sin_parts[k]) for k in range(ndim)
|
||
]
|
||
return torch.cat(y_parts, dim=-1)
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Vision Attention (TP-sharded, fused QKV)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class Gemma4VisionAttention(nn.Module):
|
||
"""Multi-head attention for the Gemma 4 vision encoder.
|
||
|
||
QKV uses a fused ``ClippableQKVParallelLinear`` for efficient matmul with
|
||
per-projection clip bounds. Output projection uses ``ClippableLinear``.
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
config: Gemma4VisionConfig,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
self.head_dim = config.head_dim
|
||
|
||
tp_size = get_parallel().attn_tp_size
|
||
self.num_heads_per_partition = config.num_attention_heads // tp_size
|
||
self.num_kv_heads_per_partition = config.num_key_value_heads // tp_size
|
||
|
||
self.qkv = ClippableQKVParallelLinear(
|
||
hidden_size=config.hidden_size,
|
||
head_size=config.head_dim,
|
||
total_num_heads=config.num_attention_heads,
|
||
total_num_kv_heads=config.num_key_value_heads,
|
||
bias=config.attention_bias,
|
||
quant_config=quant_config,
|
||
prefix=prefix,
|
||
)
|
||
self.o_proj = ClippableRowParallelLinear(
|
||
input_size=config.num_attention_heads * config.head_dim,
|
||
output_size=config.hidden_size,
|
||
bias=config.attention_bias,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("o_proj", prefix),
|
||
)
|
||
|
||
self.q_norm = Gemma4RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||
self.k_norm = Gemma4RMSNorm(self.head_dim, eps=config.rms_norm_eps)
|
||
self.v_norm = Gemma4RMSNorm(
|
||
self.head_dim, eps=config.rms_norm_eps, scale_shift=0.0, with_scale=False
|
||
)
|
||
|
||
backend = self._select_backend()
|
||
self.qkv_backend = QKV_BACKEND_IMPL[backend](
|
||
head_dim=config.head_dim,
|
||
num_heads=self.num_heads_per_partition,
|
||
num_kv_heads=self.num_kv_heads_per_partition,
|
||
dropout=0.0,
|
||
flatten_batch=True,
|
||
softmax_in_single_precision=False,
|
||
softmax_scale=1.0,
|
||
)
|
||
|
||
@staticmethod
|
||
def _select_backend() -> str:
|
||
"""Mirror VisionAttention._determine_attention_backend for consistency."""
|
||
from sglang.srt.runtime_context import get_server_args
|
||
|
||
override = get_server_args().mm_attention_backend
|
||
if override is not None:
|
||
return override
|
||
if is_cuda():
|
||
major, _ = get_device_capability()
|
||
if major == 9:
|
||
from sglang.srt.utils import is_blackwell_supported
|
||
|
||
if is_blackwell_supported():
|
||
return "triton_attn"
|
||
return "fa3"
|
||
return "triton_attn"
|
||
if is_hip():
|
||
# ROCm: use triton_attn to avoid SDPA flatten_batch issues
|
||
# with multi-image/video inputs
|
||
return "triton_attn"
|
||
return "sdpa"
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
cos: torch.Tensor,
|
||
sin: torch.Tensor,
|
||
attention_mask: Optional[torch.Tensor] = None,
|
||
) -> torch.Tensor:
|
||
bsz, seq_len, _ = hidden_states.shape
|
||
|
||
q, k, v = self.qkv(hidden_states)
|
||
|
||
q = q.reshape(bsz * seq_len, self.num_heads_per_partition, self.head_dim)
|
||
k = k.reshape(bsz * seq_len, self.num_kv_heads_per_partition, self.head_dim)
|
||
v = v.reshape(bsz * seq_len, self.num_kv_heads_per_partition, self.head_dim)
|
||
|
||
q = self.q_norm(q.reshape(-1, self.head_dim)).reshape(q.shape)
|
||
k = self.k_norm(k.reshape(-1, self.head_dim)).reshape(k.shape)
|
||
v = self.v_norm(v.reshape(-1, self.head_dim)).reshape(v.shape)
|
||
|
||
cos_flat = cos.reshape(bsz * seq_len, 1, self.head_dim)
|
||
sin_flat = sin.reshape(bsz * seq_len, 1, self.head_dim)
|
||
q = _apply_multidimensional_rope(q, cos_flat, sin_flat)
|
||
k = _apply_multidimensional_rope(k, cos_flat, sin_flat)
|
||
|
||
if attention_mask is not None:
|
||
attn_mask_4d = (
|
||
attention_mask.unsqueeze(-1) * attention_mask.unsqueeze(1)
|
||
).unsqueeze(1)
|
||
else:
|
||
attn_mask_4d = None
|
||
|
||
output = self.qkv_backend.forward(
|
||
q=q,
|
||
k=k,
|
||
v=v,
|
||
cu_seqlens=None,
|
||
bsz=bsz,
|
||
seq_len=seq_len,
|
||
attention_mask=attn_mask_4d,
|
||
softmax_scale=1.0,
|
||
)
|
||
|
||
output = rearrange(output, "(b s) h d -> b s (h d)", b=bsz)
|
||
output = self.o_proj(output)
|
||
return output
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Vision MLP (GatedGELU, TP-sharded)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class Gemma4VisionMLP(nn.Module):
|
||
def __init__(
|
||
self,
|
||
config: Gemma4VisionConfig,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
if config.hidden_activation != "gelu_pytorch_tanh":
|
||
raise ValueError(
|
||
f"Gemma4VisionMLP expects hidden_activation='gelu_pytorch_tanh', "
|
||
f"got {config.hidden_activation!r}"
|
||
)
|
||
self.gate_up = ClippableGateUpParallelLinear(
|
||
input_size=config.hidden_size,
|
||
intermediate_size=config.intermediate_size,
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
prefix=prefix,
|
||
)
|
||
self.down_proj = ClippableRowParallelLinear(
|
||
input_size=config.intermediate_size,
|
||
output_size=config.hidden_size,
|
||
bias=False,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("down_proj", prefix),
|
||
)
|
||
|
||
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
||
gate, up = self.gate_up(x)
|
||
x = F.gelu(gate, approximate="tanh") * up
|
||
x = self.down_proj(x)
|
||
return x
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Encoder Layer
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class Gemma4VisionEncoderLayer(nn.Module):
|
||
def __init__(
|
||
self,
|
||
config: Gemma4VisionConfig,
|
||
layer_idx: int,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
self.self_attn = Gemma4VisionAttention(
|
||
config,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("self_attn", prefix),
|
||
)
|
||
self.mlp = Gemma4VisionMLP(
|
||
config,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("mlp", prefix),
|
||
)
|
||
eps = config.rms_norm_eps
|
||
hs = config.hidden_size
|
||
self.input_layernorm = Gemma4RMSNorm(hs, eps=eps)
|
||
self.post_attention_layernorm = Gemma4RMSNorm(hs, eps=eps)
|
||
self.pre_feedforward_layernorm = Gemma4RMSNorm(hs, eps=eps)
|
||
self.post_feedforward_layernorm = Gemma4RMSNorm(hs, eps=eps)
|
||
|
||
self.register_buffer("layer_scalar", torch.ones(()))
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
cos: torch.Tensor,
|
||
sin: torch.Tensor,
|
||
attention_mask: Optional[torch.Tensor] = None,
|
||
) -> torch.Tensor:
|
||
residual = hidden_states
|
||
hidden_states = self.input_layernorm(hidden_states)
|
||
hidden_states = self.self_attn(hidden_states, cos, sin, attention_mask)
|
||
hidden_states = self.post_attention_layernorm(hidden_states)
|
||
hidden_states = residual + hidden_states
|
||
|
||
residual = hidden_states
|
||
hidden_states = self.pre_feedforward_layernorm(hidden_states)
|
||
hidden_states = self.mlp(hidden_states)
|
||
hidden_states = self.post_feedforward_layernorm(hidden_states)
|
||
hidden_states = residual + hidden_states
|
||
|
||
hidden_states = hidden_states * self.layer_scalar
|
||
return hidden_states
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Vision Transformer (stack of encoder layers + RoPE)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class Gemma4VisionTransformer(nn.Module):
|
||
def __init__(
|
||
self,
|
||
config: Gemma4VisionConfig,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
self.config = config
|
||
self.rotary_emb = Gemma4VisionRotaryEmbedding(config)
|
||
self.layers = nn.ModuleList(
|
||
[
|
||
Gemma4VisionEncoderLayer(
|
||
config,
|
||
layer_idx=i,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix(f"layers.{i}", prefix),
|
||
)
|
||
for i in range(config.num_hidden_layers)
|
||
]
|
||
)
|
||
|
||
def forward(
|
||
self,
|
||
inputs_embeds: torch.Tensor,
|
||
attention_mask: torch.Tensor,
|
||
patch_positions: torch.Tensor,
|
||
) -> torch.Tensor:
|
||
"""
|
||
Args:
|
||
inputs_embeds: [batch, seq, hidden_size]
|
||
attention_mask: [batch, seq] — True = valid token
|
||
patch_positions: [batch, seq, 2]
|
||
Returns:
|
||
last_hidden_state: [batch, seq, hidden_size]
|
||
"""
|
||
cos, sin = self.rotary_emb(inputs_embeds, patch_positions)
|
||
hidden_states = inputs_embeds
|
||
for layer in self.layers:
|
||
hidden_states = layer(hidden_states, cos, sin, attention_mask)
|
||
return hidden_states
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Patch Embedder
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class Gemma4VisionPatchEmbedder(nn.Module):
|
||
def __init__(self, config: Gemma4VisionConfig):
|
||
super().__init__()
|
||
self.patch_size = config.patch_size
|
||
self.hidden_size = config.hidden_size
|
||
self.position_embedding_size = config.position_embedding_size
|
||
|
||
self.input_proj = nn.Linear(
|
||
3 * self.patch_size**2, self.hidden_size, bias=False
|
||
)
|
||
self.position_embedding_table = nn.Parameter(
|
||
torch.ones(2, self.position_embedding_size, self.hidden_size)
|
||
)
|
||
|
||
def _position_embeddings(
|
||
self, patch_positions: torch.Tensor, padding_positions: torch.Tensor
|
||
) -> torch.Tensor:
|
||
clamped_positions = patch_positions.clamp(min=0)
|
||
one_hot = F.one_hot(clamped_positions, num_classes=self.position_embedding_size)
|
||
one_hot = one_hot.permute(0, 2, 1, 3).to(self.position_embedding_table)
|
||
position_embeddings = one_hot @ self.position_embedding_table
|
||
position_embeddings = position_embeddings.sum(dim=1)
|
||
position_embeddings = torch.where(
|
||
padding_positions.unsqueeze(-1), 0.0, position_embeddings
|
||
)
|
||
return position_embeddings
|
||
|
||
def _patch_projection(self, pixel_values: torch.Tensor) -> torch.Tensor:
|
||
"""Project pre-patchified pixels into model space.
|
||
|
||
Args:
|
||
pixel_values: [batch, num_patches, patch_pixels] — already patchified
|
||
by the image processor, values in [0, 1].
|
||
"""
|
||
patches = 2 * (pixel_values - 0.5)
|
||
return self.input_proj(patches.to(self.input_proj.weight.dtype))
|
||
|
||
def forward(
|
||
self,
|
||
pixel_values: torch.Tensor,
|
||
pixel_position_ids: torch.Tensor,
|
||
padding_positions: torch.Tensor,
|
||
) -> torch.Tensor:
|
||
"""Compute patch embeddings with positional information.
|
||
|
||
Args:
|
||
pixel_values: [batch, num_patches, patch_pixels] — pre-patchified.
|
||
pixel_position_ids: [batch, num_patches, 2] — (x, y) positions,
|
||
-1 for padding patches.
|
||
padding_positions: [batch, num_patches] — True for padding patches.
|
||
"""
|
||
hidden_states = self._patch_projection(pixel_values)
|
||
position_embeddings = self._position_embeddings(
|
||
pixel_position_ids, padding_positions
|
||
)
|
||
return hidden_states + position_embeddings
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Pooler
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class Gemma4VisionPooler(nn.Module):
|
||
def __init__(self, config: Gemma4VisionConfig):
|
||
super().__init__()
|
||
self.hidden_size = config.hidden_size
|
||
self.root_hidden_size = self.hidden_size**0.5
|
||
|
||
def _avg_pool_by_positions(
|
||
self, x: torch.Tensor, patch_positions: torch.Tensor, length: int
|
||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||
input_seq_len = x.shape[1]
|
||
k = int((input_seq_len // length) ** 0.5)
|
||
k_squared = k**2
|
||
if k_squared * length != input_seq_len:
|
||
raise ValueError(
|
||
f"Cannot pool {x.shape} to {length}: {k=}^2 times {length=} must be {input_seq_len}."
|
||
)
|
||
clamped_positions = patch_positions.clamp(min=0)
|
||
max_x = clamped_positions[..., 0].max(dim=-1, keepdim=True)[0] + 1
|
||
kernel_idxs = torch.div(clamped_positions, k, rounding_mode="floor")
|
||
kernel_idxs = kernel_idxs[..., 0] + (max_x // k) * kernel_idxs[..., 1]
|
||
|
||
weights = F.one_hot(kernel_idxs.long(), length).float() / k_squared
|
||
output = weights.transpose(1, 2).to(x.dtype) @ x
|
||
mask = torch.logical_not((weights == 0).all(dim=1))
|
||
return output, mask
|
||
|
||
def forward(
|
||
self,
|
||
hidden_states: torch.Tensor,
|
||
patch_positions: torch.Tensor,
|
||
padding_positions: torch.Tensor,
|
||
output_length: Optional[int] = None,
|
||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||
"""
|
||
Returns:
|
||
(pooled_hidden_states, mask) where mask is True for valid tokens.
|
||
"""
|
||
if output_length is None:
|
||
raise ValueError("output_length is required for Gemma4VisionPooler")
|
||
if output_length > hidden_states.shape[1]:
|
||
raise ValueError(
|
||
f"Cannot output more soft tokens (requested {output_length}) than there are patches"
|
||
f" ({hidden_states.shape[1]}). Change the value of `num_soft_tokens` when processing."
|
||
)
|
||
length = output_length
|
||
if isinstance(length, (list, tuple)):
|
||
length = length[0]
|
||
if hidden_states.shape[1] == length:
|
||
mask = padding_positions
|
||
else:
|
||
hidden_states, mask = self._avg_pool_by_positions(
|
||
hidden_states, patch_positions, length
|
||
)
|
||
hidden_states = hidden_states * self.root_hidden_size
|
||
return hidden_states, mask
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Top-level Vision Encoder (patch_embedder → transformer → pooler)
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class Gemma4VisionEncoder(nn.Module):
|
||
"""Drop-in replacement for HF ``Gemma4VisionEncoder`` with TP support."""
|
||
|
||
def __init__(
|
||
self,
|
||
config: Gemma4VisionConfig,
|
||
quant_config: Optional[QuantizationConfig] = None,
|
||
prefix: str = "",
|
||
):
|
||
super().__init__()
|
||
self.config = config
|
||
self.patch_size = config.patch_size
|
||
self.pooling_kernel_size = config.pooling_kernel_size
|
||
|
||
self.patch_embedder = Gemma4VisionPatchEmbedder(config)
|
||
self.encoder = Gemma4VisionTransformer(
|
||
config,
|
||
quant_config=quant_config,
|
||
prefix=add_prefix("encoder", prefix),
|
||
)
|
||
self.pooler = Gemma4VisionPooler(config)
|
||
|
||
# Post-pooling standardization (normalizes vision tokens before projection)
|
||
self.standardize = getattr(config, "standardize", False)
|
||
if self.standardize:
|
||
self.register_buffer("std_bias", torch.zeros(config.hidden_size))
|
||
self.register_buffer("std_scale", torch.ones(config.hidden_size))
|
||
|
||
@property
|
||
def device(self) -> torch.device:
|
||
return self.patch_embedder.input_proj.weight.device
|
||
|
||
def forward(
|
||
self,
|
||
pixel_values: torch.Tensor,
|
||
pixel_position_ids: torch.Tensor,
|
||
) -> Tuple[torch.Tensor, torch.Tensor]:
|
||
"""Encode pre-patchified pixel_values into soft tokens.
|
||
|
||
Args:
|
||
pixel_values: [batch, num_patches, patch_pixels] — pre-patchified
|
||
by the image processor.
|
||
pixel_position_ids: [batch, num_patches, 2] — (x, y) positions,
|
||
-1 for padding patches.
|
||
|
||
Returns:
|
||
(hidden_states, pooler_mask) — hidden_states [batch, output_len, hidden],
|
||
pooler_mask [batch, output_len] True = valid.
|
||
"""
|
||
k2 = self.pooling_kernel_size * self.pooling_kernel_size
|
||
output_length = pixel_values.shape[-2] // k2
|
||
|
||
padding_positions = (pixel_position_ids == -1).all(dim=-1)
|
||
|
||
inputs_embeds = self.patch_embedder(
|
||
pixel_values, pixel_position_ids, padding_positions
|
||
)
|
||
|
||
last_hidden = self.encoder(
|
||
inputs_embeds=inputs_embeds,
|
||
attention_mask=~padding_positions,
|
||
patch_positions=pixel_position_ids,
|
||
)
|
||
|
||
pooled, pooler_mask = self.pooler(
|
||
last_hidden,
|
||
pixel_position_ids,
|
||
padding_positions,
|
||
output_length=output_length,
|
||
)
|
||
|
||
if self.standardize:
|
||
pooled = (pooled - self.std_bias) * self.std_scale
|
||
|
||
return pooled, pooler_mask
|