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
392 lines
16 KiB
Python
392 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
# 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.
|
|
# ==============================================================================
|
|
# Adapted from https://github.com/vllm-project/vllm/blob/main/vllm/model_executor/models/nano_nemotron_vl.py
|
|
|
|
import logging
|
|
from typing import Iterable
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
from sglang.srt.configs.nano_nemotron_vl import NemotronH_Nano_VL_V2_Config
|
|
from sglang.srt.layers.activation import ReLU2
|
|
from sglang.srt.layers.layernorm import RMSNorm
|
|
from sglang.srt.layers.quantization.base_config import QuantizationConfig
|
|
from sglang.srt.managers.mm_utils import (
|
|
MultiModalityDataPaddingPatternTokenPairs,
|
|
general_mm_embed_routine,
|
|
)
|
|
from sglang.srt.managers.schedule_batch import (
|
|
Modality,
|
|
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.nemotron_h import NemotronHForCausalLM
|
|
from sglang.srt.models.parakeet import ProjectedParakeet
|
|
from sglang.srt.models.radio import RadioModel
|
|
from sglang.srt.models.utils import WeightsMapper
|
|
from sglang.srt.multimodal.evs import EVS, EVSConfig
|
|
from sglang.srt.multimodal.evs.evs_module import VideoEVSDataItem
|
|
from sglang.srt.utils import add_prefix
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class NemotronH_Nano_VL_V2(EVS):
|
|
# The loader reads `hf_to_sglang_mapper` off the outer model class when
|
|
# applying name rewrites to the quant config's `quantized_layers` keys;
|
|
# the inner NemotronHForCausalLM mapper is not consulted there.
|
|
hf_to_sglang_mapper = WeightsMapper(
|
|
orig_to_new_prefix={
|
|
"language_model.backbone.": "language_model.model.",
|
|
},
|
|
)
|
|
|
|
@staticmethod
|
|
def create_evs_config(config: NemotronH_Nano_VL_V2_Config):
|
|
return EVSConfig(video_pruning_rate=config.video_pruning_rate)
|
|
|
|
def __init__(
|
|
self,
|
|
config: NemotronH_Nano_VL_V2_Config,
|
|
quant_config: QuantizationConfig | None = None,
|
|
prefix: str = "",
|
|
) -> None:
|
|
super().__init__(config)
|
|
|
|
self.downsample_ratio = config.downsample_ratio
|
|
self.language_model = NemotronHForCausalLM(
|
|
config=config.llm_config,
|
|
quant_config=quant_config,
|
|
prefix=add_prefix("language_model", prefix),
|
|
)
|
|
self.vision_model = RadioModel(config=config.create_radio_config()).to(
|
|
self.language_model.config.dtype
|
|
)
|
|
|
|
vit_hidden_size = config.vit_hidden_size
|
|
self.rmsnorm_hidden_size = (
|
|
vit_hidden_size * int(round(1 / self.downsample_ratio)) ** 2
|
|
)
|
|
vision_projection_hidden_size = config.projector_hidden_size
|
|
llm_hidden_size = config.llm_config.hidden_size
|
|
self.llm_hidden_size = llm_hidden_size
|
|
self.model_dtype = self.language_model.config.torch_dtype
|
|
|
|
self.mlp1 = nn.Sequential(
|
|
RMSNorm(
|
|
hidden_size=self.rmsnorm_hidden_size,
|
|
eps=1e-5,
|
|
),
|
|
nn.Linear(
|
|
self.rmsnorm_hidden_size,
|
|
vision_projection_hidden_size,
|
|
bias=False,
|
|
),
|
|
ReLU2(),
|
|
nn.Linear(vision_projection_hidden_size, llm_hidden_size, bias=False),
|
|
).to(self.model_dtype)
|
|
|
|
self.sound_encoder: ProjectedParakeet | None = None
|
|
if getattr(config, "sound_config", None) is not None:
|
|
logger.info(
|
|
"Found sound config, initializing sound encoder for Nemotron AVLM"
|
|
)
|
|
self.sound_encoder = ProjectedParakeet(
|
|
config.sound_config,
|
|
dtype=self.language_model.config.torch_dtype,
|
|
llm_hidden_size=llm_hidden_size,
|
|
max_model_len=getattr(config, "max_model_len", 8192),
|
|
)
|
|
|
|
self.config = config
|
|
|
|
def pad_input_ids(self, input_ids: list[int], mm_inputs: MultimodalInputs):
|
|
im_start_id: int = mm_inputs.im_start_id
|
|
im_end_id: int = mm_inputs.im_end_id
|
|
|
|
visual_items = [item for item in mm_inputs.mm_items if not item.is_audio()]
|
|
audio_items = [item for item in mm_inputs.mm_items if item.is_audio()]
|
|
|
|
all_data_offsets = []
|
|
|
|
if visual_items:
|
|
mm_inputs.mm_items = visual_items
|
|
helper = MultiModalityDataPaddingPatternTokenPairs(
|
|
[(im_start_id, im_end_id)]
|
|
)
|
|
input_ids = helper.pad_input_tokens(input_ids, mm_inputs)
|
|
all_data_offsets.extend(mm_inputs.data_offsets)
|
|
|
|
audio_start_id = getattr(mm_inputs, "audio_start_id", None)
|
|
audio_end_id = getattr(mm_inputs, "audio_end_id", None)
|
|
if audio_items and audio_start_id is not None and audio_end_id is not None:
|
|
mm_inputs.mm_items = audio_items
|
|
helper = MultiModalityDataPaddingPatternTokenPairs(
|
|
[(audio_start_id, audio_end_id)]
|
|
)
|
|
input_ids = helper.pad_input_tokens(input_ids, mm_inputs)
|
|
all_data_offsets.extend(mm_inputs.data_offsets)
|
|
|
|
mm_inputs.mm_items = visual_items + audio_items
|
|
mm_inputs.data_offsets = all_data_offsets
|
|
|
|
if audio_items:
|
|
for item in visual_items:
|
|
if isinstance(item, VideoEVSDataItem):
|
|
item.pre_chunked_input_ids = input_ids
|
|
|
|
return input_ids
|
|
|
|
def pixel_shuffle(self, x: torch.Tensor, scale_factor: float = 0.5) -> torch.Tensor:
|
|
n, w, h, c = x.size()
|
|
# N, W, H, C --> N, W, H * scale, C // scale
|
|
x = x.view(
|
|
n,
|
|
w,
|
|
int(h * scale_factor),
|
|
int(c / scale_factor),
|
|
)
|
|
# N, W, H * scale, C // scale --> N, H * scale, W, C // scale
|
|
x = x.permute(0, 2, 1, 3).contiguous()
|
|
# N, H * scale, W, C // scale -->
|
|
# N, H * scale, W * scale, C // (scale ** 2)
|
|
x = x.view(
|
|
n,
|
|
int(h * scale_factor),
|
|
int(w * scale_factor),
|
|
int(c / (scale_factor * scale_factor)),
|
|
)
|
|
if self.config.ps_version != "v1":
|
|
x = x.permute(0, 2, 1, 3).contiguous()
|
|
return x
|
|
|
|
def extract_feature_dynamic(self, pixel_values_list: list[torch.Tensor]):
|
|
"""Extract features from variable-size images (dynamic resolution).
|
|
|
|
Each image has different spatial dimensions. They are passed as a list
|
|
to RADIO which handles ragged packing with cu_seqlens internally.
|
|
"""
|
|
features, num_patches_list = self.vision_model(pixel_values_list)
|
|
patch_size = self.config.patch_size
|
|
results = []
|
|
offset = 0
|
|
for i, num_patches in enumerate(num_patches_list):
|
|
img_feats = features[0, offset : offset + num_patches]
|
|
h_patches = pixel_values_list[i].shape[-2] // patch_size
|
|
w_patches = pixel_values_list[i].shape[-1] // patch_size
|
|
img_feats = img_feats.reshape(1, h_patches, w_patches, -1)
|
|
img_feats = self.pixel_shuffle(img_feats, self.downsample_ratio)
|
|
img_feats = img_feats.view(-1, self.rmsnorm_hidden_size)
|
|
img_feats = self.mlp1(img_feats)
|
|
results.append(img_feats)
|
|
offset += num_patches
|
|
return torch.cat(results, dim=0)
|
|
|
|
def extract_video_feature_temporal(self, pixel_values, num_frames):
|
|
"""Extract video features with temporal compression (tubelet grouping)."""
|
|
vit_embeds = self.vision_model(pixel_values, num_frames=num_frames)
|
|
num_tubelets = vit_embeds.shape[0]
|
|
patch_size = self.config.patch_size
|
|
h_patches = pixel_values.shape[-2] // patch_size
|
|
w_patches = pixel_values.shape[-1] // patch_size
|
|
vit_embeds = vit_embeds.reshape(num_tubelets, h_patches, w_patches, -1)
|
|
vit_embeds = self.pixel_shuffle(vit_embeds, self.downsample_ratio)
|
|
vit_embeds = vit_embeds.view(-1, self.rmsnorm_hidden_size)
|
|
vit_embeds = self.mlp1(vit_embeds)
|
|
vit_embeds = vit_embeds.view(num_tubelets, -1, self.llm_hidden_size)
|
|
return vit_embeds
|
|
|
|
def get_input_embeddings(self):
|
|
return self.language_model.get_input_embeddings()
|
|
|
|
def extract_feature(self, pixel_values):
|
|
micro_batch_size = 128
|
|
n = pixel_values.shape[0]
|
|
patch_size = self.config.patch_size
|
|
h_patches = pixel_values.shape[-2] // patch_size
|
|
w_patches = pixel_values.shape[-1] // patch_size
|
|
vit_embeds_list = []
|
|
for i in range(0, n, micro_batch_size):
|
|
chunk = pixel_values[i : i + micro_batch_size]
|
|
batch_size = chunk.shape[0]
|
|
vit_embeds = self.vision_model(chunk)
|
|
vit_embeds = vit_embeds.to(dtype=self.model_dtype)
|
|
vit_embeds = vit_embeds.reshape(batch_size, h_patches, w_patches, -1)
|
|
vit_embeds = self.pixel_shuffle(
|
|
vit_embeds, scale_factor=self.downsample_ratio
|
|
)
|
|
vit_embeds = vit_embeds.view(-1, self.rmsnorm_hidden_size)
|
|
vit_embeds = self.mlp1(vit_embeds)
|
|
vit_embeds = vit_embeds.view(batch_size, -1, self.llm_hidden_size)
|
|
vit_embeds_list.append(vit_embeds)
|
|
vit_embeds = torch.cat(vit_embeds_list, dim=0)
|
|
return vit_embeds
|
|
|
|
def get_image_feature(self, items: list[MultimodalDataItem]):
|
|
"""
|
|
Projects the last hidden state from the vision model into language model space.
|
|
|
|
Returns:
|
|
image_features (`torch.Tensor`): Image feature tensor of shape `(num_images, image_length, embed_dim)`).
|
|
"""
|
|
is_dynamic = any(getattr(item, "is_dynamic", False) for item in items)
|
|
if is_dynamic:
|
|
pixel_values_list = [item.feature for item in items]
|
|
return self.extract_feature_dynamic(pixel_values_list)
|
|
|
|
pixel_values = torch.cat([item.feature for item in items])
|
|
image_features = self.extract_feature(pixel_values)
|
|
return image_features
|
|
|
|
def get_video_feature(self, items: list[MultimodalDataItem]):
|
|
"""
|
|
Projects the last hidden state from the video model into language model space.
|
|
|
|
Returns:
|
|
video_features (`torch.Tensor`): Video feature tensor of shape `(num_videos, video_length, embed_dim)`).
|
|
"""
|
|
pixel_values = torch.cat([item.feature for item in items])
|
|
if getattr(self.config, "video_temporal_patch_size", 1) > 1:
|
|
num_frames = pixel_values.shape[0]
|
|
return self.extract_video_feature_temporal(pixel_values, num_frames)
|
|
video_features = self.extract_feature(pixel_values)
|
|
return video_features
|
|
|
|
def get_audio_feature(self, items: list[MultimodalDataItem]):
|
|
"""
|
|
Encode audio features through the Parakeet sound encoder.
|
|
|
|
Each item carries mel spectrogram features, an attention mask, and a
|
|
clip count. Multiple clips per audio item are grouped and concatenated
|
|
(trimmed to valid output lengths) to form a single embedding per item.
|
|
"""
|
|
assert self.sound_encoder is not None
|
|
|
|
all_features = []
|
|
all_masks = []
|
|
all_num_clips = []
|
|
for item in items:
|
|
all_features.append(item.feature)
|
|
all_masks.append(item.feature_attention_mask)
|
|
all_num_clips.append(item.audio_num_clips)
|
|
|
|
input_audio_features = torch.cat(all_features, dim=0)
|
|
feature_attention_mask = torch.cat(all_masks, dim=0)
|
|
|
|
target_device = next(self.sound_encoder.parameters()).device
|
|
input_audio_features = input_audio_features.to(
|
|
dtype=self.language_model.config.torch_dtype, device=target_device
|
|
)
|
|
feature_attention_mask = feature_attention_mask.to(device=target_device)
|
|
|
|
sound_embeds = self.sound_encoder(input_audio_features, feature_attention_mask)
|
|
|
|
valid_input_lens = feature_attention_mask.sum(dim=1)
|
|
valid_output_lens = (
|
|
self.sound_encoder.encoder._get_subsampling_output_length(valid_input_lens)
|
|
.long()
|
|
.tolist()
|
|
)
|
|
|
|
grouped_embeds = []
|
|
clip_offset = 0
|
|
for num_clips in all_num_clips:
|
|
embeds = []
|
|
for clip_idx in range(clip_offset, clip_offset + num_clips):
|
|
valid_len = valid_output_lens[clip_idx]
|
|
embeds.append(sound_embeds[clip_idx, :valid_len])
|
|
grouped_embeds.append(torch.cat(embeds, dim=0))
|
|
clip_offset += num_clips
|
|
|
|
return torch.cat(grouped_embeds, dim=0)
|
|
|
|
@torch.no_grad()
|
|
def forward(
|
|
self,
|
|
input_ids: torch.Tensor,
|
|
positions: torch.Tensor,
|
|
forward_batch: ForwardBatch,
|
|
get_embedding: bool = False,
|
|
):
|
|
data_embedding_funcs = {
|
|
Modality.IMAGE: self.get_image_feature,
|
|
Modality.VIDEO: self.get_video_feature,
|
|
}
|
|
if self.sound_encoder is not None:
|
|
data_embedding_funcs[Modality.AUDIO] = self.get_audio_feature
|
|
|
|
hidden_states = general_mm_embed_routine(
|
|
input_ids=input_ids,
|
|
forward_batch=forward_batch,
|
|
language_model=self.language_model,
|
|
multimodal_model=self,
|
|
data_embedding_funcs=data_embedding_funcs,
|
|
positions=positions,
|
|
)
|
|
return hidden_states
|
|
|
|
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]):
|
|
adapter_dict = dict(self.mlp1.named_parameters())
|
|
|
|
def is_llm(name: str) -> bool:
|
|
return name.startswith("language_model")
|
|
|
|
def is_adapter_weights(weight: tuple[str, torch.Tensor]):
|
|
return weight[0].startswith("mlp1")
|
|
|
|
def is_vision_weights(name: str) -> bool:
|
|
return name.startswith("vision_model.radio_model.")
|
|
|
|
def is_sound_weights(name: str) -> bool:
|
|
return name.startswith("sound")
|
|
|
|
# Separate weights by component
|
|
llm_weights = []
|
|
vision_weights = []
|
|
sound_weights = []
|
|
|
|
for name, w in weights:
|
|
if is_llm(name):
|
|
# Strip 'language_model.' prefix for LLM weights
|
|
llm_weights.append((".".join(name.split(".")[1:]), w))
|
|
elif is_adapter_weights((name, w)):
|
|
# Load vision-language adapter weights directly
|
|
trimmed_name = ".".join(name.split(".")[1:])
|
|
param = adapter_dict[trimmed_name]
|
|
with torch.no_grad():
|
|
default_weight_loader(param, w)
|
|
elif is_vision_weights(name):
|
|
# Convert: vision_model.radio_model.* → radio_model.*
|
|
hf_key = name[len("vision_model.") :]
|
|
vision_weights.append((hf_key, w))
|
|
elif is_sound_weights(name):
|
|
sound_weights.append((name, w))
|
|
|
|
self.language_model.load_weights(llm_weights)
|
|
self.vision_model.load_weights(vision_weights)
|
|
if self.sound_encoder is not None and len(sound_weights) > 0:
|
|
self.sound_encoder.load_weights(sound_weights)
|
|
|
|
|
|
class NemotronH_Nano_Omni_Reasoning_V3(NemotronH_Nano_VL_V2):
|
|
pass
|
|
|
|
|
|
EntryClass = [NemotronH_Nano_VL_V2, NemotronH_Nano_Omni_Reasoning_V3]
|