Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:38:16 +08:00

349 lines
13 KiB
Python

# Copyright 2026 Liquid AI. All rights reserved.
# 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.
# ==============================================================================
"""Inference-only LFM2-VL model compatible with HuggingFace weights.
LFM2-VL is a vision-language model that combines:
- SigLip2 vision encoder with NaFlex variable-resolution support
- LFM2 language model (hybrid attention + short convolution)
- Multimodal projector with pixel unshuffle downsampling
"""
import logging
from typing import Iterable, List, Optional, Tuple
import numpy as np
import torch
from torch import nn
from transformers.activations import ACT2FN
from sglang.srt.layers.linear import ColumnParallelLinear, RowParallelLinear
from sglang.srt.layers.logits_processor import LogitsProcessor
from sglang.srt.layers.quantization.base_config import QuantizationConfig
from sglang.srt.managers.mm_utils import (
MultiModalityDataPaddingPatternMultimodalTokens,
general_mm_embed_routine,
)
from sglang.srt.managers.schedule_batch import (
MultimodalDataItem,
MultimodalInputs,
)
from sglang.srt.model_executor.forward_batch_info import ForwardBatch
from sglang.srt.model_loader.weight_utils import default_weight_loader
from sglang.srt.models.lfm2 import Lfm2ForCausalLM
from sglang.srt.models.siglip2 import Siglip2Model
from sglang.srt.utils import add_prefix
logger = logging.getLogger(__name__)
class Lfm2VlMultiModalProjector(nn.Module):
"""Multimodal projector with pixel unshuffle downsampling and TP/DP support."""
def __init__(
self,
config,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
):
super().__init__()
in_channels = config.vision_config.hidden_size * (config.downsample_factor**2)
self.factor = config.downsample_factor
self.use_layer_norm = config.projector_use_layernorm
self.layer_norm = (
nn.LayerNorm(in_channels) if config.projector_use_layernorm else None
)
self.linear_1 = ColumnParallelLinear(
in_channels,
config.projector_hidden_size,
bias=config.projector_bias,
quant_config=quant_config,
)
self.act = ACT2FN[config.projector_hidden_act]
self.linear_2 = RowParallelLinear(
config.projector_hidden_size,
config.text_config.hidden_size,
bias=config.projector_bias,
quant_config=quant_config,
)
def forward(
self,
vision_features_packed: torch.Tensor,
spatial_shapes: torch.Tensor,
) -> torch.Tensor:
"""Project packed vision features with pixel unshuffle.
Args:
vision_features_packed: (total_tokens, hidden_size) packed in tile order.
spatial_shapes: (num_tiles, 2) on CPU (height, width) per tile.
Returns:
projected_packed: (total_projected_tokens, text_hidden_size)
"""
factor = self.factor
hidden_size = vision_features_packed.shape[-1]
# Compute tile lengths from spatial shapes
lengths = (spatial_shapes[:, 0] * spatial_shapes[:, 1]).tolist()
# Split packed tensor into per-tile tensors
tile_features = torch.split(vision_features_packed, lengths, dim=0)
# Apply pixel unshuffle to each tile using reshape/permute (GPU operations)
unshuffled_parts = []
for tile, (h, w) in zip(tile_features, spatial_shapes.tolist()):
if h == 0 or w == 0:
continue
# Reshape: (H*W, C) -> (H, W, C) -> (H/f, f, W/f, f, C)
tile_2d = tile.view(h, w, hidden_size)
tile_blocks = tile_2d.view(
h // factor, factor, w // factor, factor, hidden_size
)
# Permute: (H/f, f, W/f, f, C) -> (H/f, W/f, f, f, C)
tile_permuted = tile_blocks.permute(0, 2, 1, 3, 4)
# Reshape: (H/f, W/f, f*f*C)
tile_unshuffled = tile_permuted.reshape(
(h // factor) * (w // factor), factor * factor * hidden_size
)
unshuffled_parts.append(tile_unshuffled)
if unshuffled_parts:
unshuffled = torch.cat(unshuffled_parts, dim=0)
else:
unshuffled = vision_features_packed.new_empty(
(0, factor * factor * hidden_size)
)
if self.use_layer_norm:
unshuffled = self.layer_norm(unshuffled)
hidden_states, _ = self.linear_1(unshuffled)
hidden_states = self.act(hidden_states)
projected_packed, _ = self.linear_2(hidden_states)
return projected_packed
class Lfm2VlForConditionalGeneration(nn.Module):
"""LFM2-VL Vision-Language Model."""
def __init__(
self,
config,
quant_config: Optional[QuantizationConfig] = None,
prefix: str = "",
) -> None:
super().__init__()
self.config = config
self.quant_config = quant_config
# Vision tower: Native Siglip2 implementation
self.vision_tower = Siglip2Model(
config=config.vision_config,
quant_config=quant_config,
prefix=add_prefix("vision_tower", prefix),
)
# Multimodal projector
self.multi_modal_projector = Lfm2VlMultiModalProjector(
config,
quant_config=quant_config,
prefix=add_prefix("multi_modal_projector", prefix),
)
# Language model: reuse SGLang's LFM2 implementation
self.language_model = Lfm2ForCausalLM(
config.text_config,
quant_config=quant_config,
prefix=add_prefix("language_model", prefix),
)
self.logits_processor = LogitsProcessor(config.text_config)
def pad_input_ids(
self, input_ids: List[int], mm_inputs: MultimodalInputs
) -> List[int]:
pattern = MultiModalityDataPaddingPatternMultimodalTokens()
result = pattern.pad_input_tokens(input_ids, mm_inputs)
return result
def get_input_embeddings(self) -> nn.Embedding:
return self.language_model.model.embed_tokens
def get_image_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
"""Process images through vision tower and projector.
Handles SigLip2's NaFlex variable-resolution output.
Pixel values arrive padded from the base processor; we pack them
using the attention mask before feeding into the vision tower.
"""
# Collect data from all items
all_pixel_values = []
all_attention_masks = []
all_spatial_shapes = []
for item in items:
pv = item.feature
am = item.pixel_attention_mask
ss = item.spatial_shapes
if isinstance(pv, np.ndarray):
pv = torch.from_numpy(pv)
if isinstance(am, np.ndarray):
am = torch.from_numpy(am)
if isinstance(ss, np.ndarray):
ss = torch.from_numpy(ss)
all_pixel_values.append(pv)
all_attention_masks.append(am)
all_spatial_shapes.append(ss)
pixel_values = torch.cat(all_pixel_values, dim=0)
attention_mask = torch.cat(all_attention_masks, dim=0)
spatial_shapes = torch.cat(all_spatial_shapes, dim=0)
pixel_values = pixel_values.to(
device=self.vision_tower.device,
dtype=self.vision_tower.dtype,
)
spatial_shapes_cpu = spatial_shapes.cpu()
# Pack padded pixel values using attention mask
packed_list = []
for i in range(pixel_values.shape[0]):
mask = attention_mask[i].bool()
packed_list.append(pixel_values[i][mask])
if not packed_list:
return torch.tensor(
[], device=self.vision_tower.device, dtype=self.vision_tower.dtype
)
pixel_values_packed = torch.cat(packed_list, dim=0)
# Compute cu_seqlens and max_seqlen for packed attention
spatial_shapes_list = spatial_shapes_cpu.tolist()
lengths_list = [int(h * w) for h, w in spatial_shapes_list]
total_tokens = sum(lengths_list)
if total_tokens == 0:
return torch.tensor(
[], device=self.vision_tower.device, dtype=self.vision_tower.dtype
)
lengths = torch.tensor(
lengths_list, dtype=torch.int32, device=pixel_values_packed.device
)
cu_seqlens = torch.zeros(
len(lengths_list) + 1,
dtype=torch.int32,
device=pixel_values_packed.device,
)
cu_seqlens[1:] = torch.cumsum(lengths, dim=0)
max_seqlen = lengths.max()
# Forward through vision tower
vision_outputs = self.vision_tower(
pixel_values_packed=pixel_values_packed,
spatial_shapes=spatial_shapes_cpu,
cu_seqlens=cu_seqlens,
max_seqlen=max_seqlen,
)
# Get the packed features (remove batch dim if present)
if vision_outputs.dim() == 3:
vision_features_packed = vision_outputs[0]
else:
vision_features_packed = vision_outputs
# Project through multimodal projector
projected_packed = self.multi_modal_projector(
vision_features_packed=vision_features_packed,
spatial_shapes=spatial_shapes_cpu,
)
return projected_packed
@torch.no_grad()
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
forward_batch: ForwardBatch,
) -> torch.Tensor:
return general_mm_embed_routine(
input_ids=input_ids,
forward_batch=forward_batch,
language_model=self.language_model,
multimodal_model=self,
positions=positions,
)
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
"""Load weights from HuggingFace format."""
# Collect weights by destination
vision_weights = []
projector_weights = []
lm_weights = []
for name, loaded_weight in weights:
if name.startswith("model.vision_tower."):
# model.vision_tower.* → * (strip model.vision_tower. prefix)
# siglip2.py expects names like "vision_model.embeddings.patch_embedding.weight"
new_name = name.replace("model.vision_tower.", "", 1)
vision_weights.append((new_name, loaded_weight))
elif name.startswith("model.multi_modal_projector."):
# model.multi_modal_projector.* → multi_modal_projector.*
new_name = name.replace(
"model.multi_modal_projector.", "multi_modal_projector.", 1
)
projector_weights.append((new_name, loaded_weight))
elif name.startswith("model.language_model."):
# model.language_model.* → language_model.model.*
new_name = name.replace(
"model.language_model.", "language_model.model.", 1
)
lm_weights.append((new_name, loaded_weight))
elif name.startswith("lm_head."):
# lm_head.* → language_model.lm_head.*
new_name = name.replace("lm_head.", "language_model.lm_head.", 1)
lm_weights.append((new_name, loaded_weight))
else:
# Try direct mapping
lm_weights.append((name, loaded_weight))
# Load vision tower weights using its own load_weights method
self.vision_tower.load_weights(vision_weights)
# Load projector weights
params_dict = dict(self.named_parameters())
for name, loaded_weight in projector_weights:
if name not in params_dict:
continue
param = params_dict[name]
weight_loader = getattr(param, "weight_loader", default_weight_loader)
weight_loader(param, loaded_weight)
# Load language model weights via Lfm2ForCausalLM.load_weights
# Strip the "language_model." prefix since Lfm2ForCausalLM expects
# names like "model.layers.0..." and "lm_head.weight"
lm_weights_stripped = []
for name, loaded_weight in lm_weights:
if name.startswith("language_model."):
name = name[len("language_model.") :]
lm_weights_stripped.append((name, loaded_weight))
self.language_model.load_weights(lm_weights_stripped)
EntryClass = Lfm2VlForConditionalGeneration