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

836 lines
32 KiB
Python

import math
import os
import re
import time
from typing import List, Optional, Union
import numpy as np
import torch
import torchvision
from PIL import Image
from torchvision.transforms import InterpolationMode
from sglang.srt.environ import envs
from sglang.srt.layers.rotary_embedding import MRotaryEmbedding
from sglang.srt.managers.schedule_batch import (
Modality,
MultimodalDataItem,
MultimodalProcessorOutput,
)
from sglang.srt.models.interns2preview import InternS2PreviewForConditionalGeneration
from sglang.srt.models.qwen2_5_vl import Qwen2_5_VLForConditionalGeneration
from sglang.srt.models.qwen2_vl import Qwen2VLForConditionalGeneration
from sglang.srt.models.qwen3_5 import (
Qwen3_5ForConditionalGeneration,
Qwen3_5MoeForConditionalGeneration,
)
from sglang.srt.models.qwen3_5_mtp import Qwen3_5ForCausalLMMTP
from sglang.srt.models.qwen3_omni_moe import Qwen3OmniMoeForConditionalGeneration
from sglang.srt.models.qwen3_vl import Qwen3VLForConditionalGeneration
from sglang.srt.models.qwen3_vl_moe import Qwen3VLMoeForConditionalGeneration
from sglang.srt.multimodal.processors.base_processor import (
BaseMultimodalProcessor as SGLangBaseProcessor,
)
from sglang.srt.multimodal.processors.base_processor import (
MultimodalSpecialTokens,
)
from sglang.srt.utils import cpu_has_amx_support, is_cpu
from sglang.srt.utils.video_decoder import VideoDecoderWrapper
from sglang.utils import logger
IMAGE_FACTOR = 28
MIN_PIXELS = 4 * 28 * 28
MAX_PIXELS = envs.SGLANG_IMAGE_MAX_PIXELS.get()
MAX_RATIO = 200
RESIZE_RESAMPLE = getattr(Image, envs.SGLANG_RESIZE_RESAMPLE.get(), None)
if envs.SGLANG_RESIZE_RESAMPLE.is_set() and RESIZE_RESAMPLE is None:
logger.warning(
f"Invalid RESIZE_RESAMPLE value: '{envs.SGLANG_RESIZE_RESAMPLE.get()}'. "
f"Ignoring and using default."
)
VIDEO_TOTAL_PIXELS = int(
float(os.environ.get("VIDEO_MAX_PIXELS", 128000 * 28 * 28 * 0.9))
)
VIDEO_MIN_PIXELS = 128 * 28 * 28
VIDEO_MAX_PIXELS = 768 * 28 * 28
FRAME_FACTOR = 2
FPS = 2.0
FPS_MIN_FRAMES = 4
FPS_MAX_FRAMES = 768
_is_cpu_amx_available = cpu_has_amx_support()
_is_cpu = is_cpu()
if _is_cpu and _is_cpu_amx_available:
try:
import transformers
from sglang.srt.layers.amx_utils import fast_preprocess_cpu
transformers.models.qwen2_vl.image_processing_qwen2_vl_fast.Qwen2VLImageProcessorFast._preprocess = (
fast_preprocess_cpu
)
except Exception as e:
logger.warning(
f"Failed to hack Qwen2VLImageProcessorFast with AMX optimization: {e}"
)
def smart_resize(
height: int,
width: int,
factor: int = IMAGE_FACTOR,
min_pixels: int = MIN_PIXELS,
max_pixels: int = MAX_PIXELS,
) -> tuple[int, int]:
"""
Rescales the image so that the following conditions are met:
1. Both dimensions (height and width) are divisible by 'factor'.
2. The total number of pixels is within the range ['min_pixels', 'max_pixels'].
3. The aspect ratio of the image is maintained as closely as possible.
"""
if max(height, width) / min(height, width) > MAX_RATIO:
raise ValueError(
f"absolute aspect ratio must be smaller than {MAX_RATIO}, got {max(height, width) / min(height, width)}"
)
h_bar = max(factor, round_by_factor(height, factor))
w_bar = max(factor, round_by_factor(width, factor))
if h_bar * w_bar > max_pixels:
beta = math.sqrt((height * width) / max_pixels)
h_bar = floor_by_factor(height / beta, factor)
w_bar = floor_by_factor(width / beta, factor)
elif h_bar * w_bar < min_pixels:
beta = math.sqrt(min_pixels / (height * width))
h_bar = ceil_by_factor(height * beta, factor)
w_bar = ceil_by_factor(width * beta, factor)
return h_bar, w_bar
def round_by_factor(number: int, factor: int) -> int:
"""Returns the closest integer to 'number' that is divisible by 'factor'."""
return round(number / factor) * factor
def ceil_by_factor(number: int, factor: int) -> int:
"""Returns the smallest integer greater than or equal to 'number' that is divisible by 'factor'."""
return math.ceil(number / factor) * factor
def floor_by_factor(number: int, factor: int) -> int:
"""Returns the largest integer less than or equal to 'number' that is divisible by 'factor'."""
return math.floor(number / factor) * factor
def smart_nframes(
ele: dict,
total_frames: int,
video_fps: int | float,
) -> int:
"""calculate the number of frames for video used for model inputs.
Args:
ele (dict): a dict contains the configuration of video.
support either `fps` or `nframes`:
- nframes: the number of frames to extract for model inputs.
- fps: the fps to extract frames for model inputs.
- min_frames: the minimum number of frames of the video, only used when fps is provided.
- max_frames: the maximum number of frames of the video, only used when fps is provided.
total_frames (int): the original total number of frames of the video.
video_fps (int | float): the original fps of the video.
Raises:
ValueError: nframes should in interval [FRAME_FACTOR, total_frames].
Returns:
int: the number of frames for video used for model inputs.
"""
assert not (
"fps" in ele and "nframes" in ele
), "Only accept either `fps` or `nframes`"
if "nframes" in ele:
nframes = round_by_factor(ele["nframes"], FRAME_FACTOR)
else:
fps = ele.get("fps", FPS)
min_frames = ceil_by_factor(ele.get("min_frames", FPS_MIN_FRAMES), FRAME_FACTOR)
max_frames = floor_by_factor(
ele.get("max_frames", min(FPS_MAX_FRAMES, total_frames)), FRAME_FACTOR
)
nframes = total_frames / video_fps * fps
if nframes > total_frames:
logger.warning(
f"smart_nframes: nframes[{nframes}] > total_frames[{total_frames}]"
)
nframes = min(min(max(nframes, min_frames), max_frames), total_frames)
nframes = floor_by_factor(nframes, FRAME_FACTOR)
if not (FRAME_FACTOR <= nframes and nframes <= total_frames):
raise ValueError(
f"nframes should in interval [{FRAME_FACTOR}, {total_frames}], but got {nframes}."
)
return nframes
# process video, qwen-specific
async def preprocess_video(
vr,
image_factor: int = IMAGE_FACTOR,
video_config: dict = {},
) -> torch.Tensor:
# preprocessed video
is_video_obj = isinstance(vr, VideoDecoderWrapper)
if not is_video_obj:
return vr, None
entry_time = time.perf_counter()
total_frames, video_fps = len(vr), vr.avg_fps
nframes = smart_nframes(
video_config, total_frames=total_frames, video_fps=video_fps
)
idx = np.linspace(0, total_frames - 1, num=nframes, dtype=np.int64)
idx = np.unique(idx)
video = vr.get_frames_as_tensor(idx.tolist())
video = video.permute(0, 3, 1, 2) # NHWC -> TCHW
nframes, _, height, width = video.shape
min_pixels = video_config.get("min_pixels", VIDEO_MIN_PIXELS)
total_pixels = video_config.get("total_pixels", VIDEO_TOTAL_PIXELS)
max_pixels = max(
min(
video_config.get("max_pixels", VIDEO_MAX_PIXELS),
total_pixels / nframes * FRAME_FACTOR,
),
int(min_pixels * 1.05),
)
get_batch_time = time.perf_counter()
max_pixels_supposed = video_config.get("max_pixels", max_pixels)
if max_pixels_supposed > max_pixels:
logger.warning(
f"The given max_pixels[{max_pixels_supposed}] exceeds limit[{max_pixels}]."
)
max_pixels = min(max_pixels_supposed, max_pixels)
if "resized_height" in video_config and "resized_width" in video_config:
resized_height, resized_width = smart_resize(
video_config["resized_height"],
video_config["resized_width"],
factor=image_factor,
)
else:
resized_height, resized_width = smart_resize(
height,
width,
factor=image_factor,
min_pixels=min_pixels,
max_pixels=max_pixels,
)
smart_resize_time = time.perf_counter()
video = torchvision.transforms.functional.resize(
video,
[resized_height, resized_width],
interpolation=InterpolationMode.BILINEAR,
)
video = video.pin_memory()
video_metadata = {
"fps": video_fps,
"duration": total_frames / video_fps,
"total_num_frames": total_frames,
"frames_indices": idx,
"video_backend": "torchvision",
}
torchvision_resize_time = time.perf_counter()
logger.debug(
f"[preprocess_video Perf], "
f"get_batch_time: {(get_batch_time - entry_time) * 1000:.2f} ms, "
f"smart_resize_time: {(smart_resize_time - get_batch_time) * 1000:.2f} ms, "
f"torchvision_resize_time: {(torchvision_resize_time - smart_resize_time) * 1000:.2f} ms, "
f"total_time: {(torchvision_resize_time - entry_time) * 1000:.2f} ms"
)
return video, video_metadata
# Compatible with Qwen-VL & Qwen-Omni Series
class QwenVLImageProcessor(SGLangBaseProcessor):
supports_transformers_backend = True
models = [
Qwen2VLForConditionalGeneration,
Qwen2_5_VLForConditionalGeneration,
Qwen3VLForConditionalGeneration,
Qwen3VLMoeForConditionalGeneration,
Qwen3_5ForConditionalGeneration,
Qwen3_5MoeForConditionalGeneration,
Qwen3_5ForCausalLMMTP,
InternS2PreviewForConditionalGeneration,
Qwen3OmniMoeForConditionalGeneration,
]
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
self.model_type = hf_config.model_type
if hf_config.model_type == "qwen3_omni_moe":
hf_config = hf_config.thinker_config
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
self.IM_START_TOKEN_ID = hf_config.vision_start_token_id
self.IM_END_TOKEN_ID = hf_config.vision_end_token_id
self.IM_TOKEN_ID = hf_config.image_token_id
self.VIDEO_TOKEN_ID = hf_config.video_token_id
self.vision_start_token_id = hf_config.vision_start_token_id
self.vision_end_token_id = getattr(hf_config, "vision_end_token_id", None)
self.audio_start_token_id = getattr(hf_config, "audio_start_token_id", None)
self.audio_token_id = getattr(hf_config, "audio_token_id", None)
self._spatial_merge_size = self.hf_config.vision_config.spatial_merge_size
self._tokens_per_second = getattr(
self.hf_config.vision_config, "tokens_per_second", None
)
self.mm_tokens = MultimodalSpecialTokens(
image_token="<|vision_start|><|image_pad|><|vision_end|>",
image_token_id=hf_config.image_token_id,
# The regex that matches expanded image tokens.
image_token_regex=re.compile(
r"<\|vision_start\|>(?:<\|image_pad\|>)+<\|vision_end\|>"
),
video_token_id=self.VIDEO_TOKEN_ID,
audio_token_id=self.audio_token_id,
).build(_processor)
@property
def spatial_merge_size(self):
return self._spatial_merge_size
def build_input_ids_with_timestamps(
self, prompt, embeddings, img_grid_thw, video_grid_thw, video_timestamps
):
"""
Build input_ids with timestamps for qwen3_vl models.
"""
if not isinstance(prompt, list):
prompt = self._processor.tokenizer.encode(prompt)
img_token_id = getattr(self, "IM_TOKEN_ID", None)
video_token_id = getattr(self, "VIDEO_TOKEN_ID", None)
spatial_merge_size = self.spatial_merge_size
vision_start_token_id = getattr(self, "vision_start_token_id", None)
vision_end_token_id = getattr(self, "vision_end_token_id", None)
input_ids = []
offsets = []
modality_list = []
cur_idx = 0
vision_start_indices = []
for i in range(len(prompt) - 1):
if img_token_id is not None and prompt[i + 1] == img_token_id:
vision_start_indices.append((i, Modality.IMAGE))
elif video_token_id is not None and prompt[i + 1] == video_token_id:
vision_start_indices.append((i, Modality.VIDEO))
img_idx = 0
video_idx = 0
for mm_start_idx, modality in vision_start_indices:
modality_list.append(modality)
video_tokens = None
if modality == Modality.IMAGE:
mm_token_num = img_grid_thw[img_idx].prod() // (spatial_merge_size**2)
mm_token_id = img_token_id
img_idx += 1
elif modality == Modality.VIDEO:
curr_timestamps = video_timestamps[video_idx]
num_frames = video_grid_thw[video_idx][0]
frame_seqlen = video_grid_thw[video_idx][1:].prod().item() // (
spatial_merge_size**2
)
video_tokens = []
_current_offset = len(input_ids) + mm_start_idx + 1 - cur_idx
# take single frame as one mm_item
for frame_idx in range(num_frames):
if frame_idx > 0:
modality_list.append(Modality.VIDEO)
curr_time = curr_timestamps[frame_idx]
timestamp_text = f"<{curr_time:.1f} seconds>"
timestamp_tokens = self._processor.tokenizer.encode(
timestamp_text, add_special_tokens=False
)
video_tokens.extend(timestamp_tokens)
_current_offset += len(timestamp_tokens)
if vision_start_token_id is not None:
video_tokens.append(vision_start_token_id)
_current_offset += 1
video_tokens.extend([video_token_id] * frame_seqlen)
if vision_end_token_id is not None:
video_tokens.append(vision_end_token_id)
offsets.append(
(_current_offset, _current_offset + frame_seqlen - 1)
)
_current_offset += (
frame_seqlen + 1
if vision_end_token_id is not None
else frame_seqlen
) # for vision_end_token_id
mm_token_num = len(video_tokens)
mm_token_id = None
video_idx += 1
else:
logger.warning(
f"{modality} modality is not supported for qwen3_vl models with timestamps."
)
continue
assert cur_idx <= mm_start_idx
input_ids.extend(prompt[cur_idx : mm_start_idx + 1])
if modality == Modality.VIDEO:
input_ids.extend(video_tokens)
else:
mm_offset_start = len(input_ids)
input_ids.extend([mm_token_id] * mm_token_num)
offsets.append((mm_offset_start, len(input_ids) - 1))
cur_idx = mm_start_idx + 2 # jump to vision_end_id
else:
input_ids.extend(prompt[cur_idx:])
return input_ids, offsets, modality_list
def compute_mrope_positions(self, input_ids, mm_items):
image_grid_thw = self._concat_mm_item_grid(
mm_items, "image_grid_thw", Modality.IMAGE
)
video_grid_thw = self._concat_mm_item_grid(
mm_items, "video_grid_thw", Modality.VIDEO
)
input_ids_tensor = torch.tensor(input_ids, dtype=torch.long).unsqueeze(0)
mrope_positions, mrope_position_delta = MRotaryEmbedding.get_rope_index(
spatial_merge_size=self._spatial_merge_size,
image_token_id=self.mm_tokens.image_token_id,
video_token_id=self.mm_tokens.video_token_id,
vision_start_token_id=self.vision_start_token_id,
model_type=self.model_type,
tokens_per_second=self._tokens_per_second,
input_ids=input_ids_tensor,
image_grid_thw=image_grid_thw,
video_grid_thw=video_grid_thw,
)
return mrope_positions.squeeze(1), mrope_position_delta
@staticmethod
def _get_processor_output_value(ret, key):
if ret is None:
return None
return ret.get(key) if hasattr(ret, "get") else getattr(ret, key, None)
def _get_precomputed_mrope_from_output(self, ret):
mrope_positions = self._get_processor_output_value(ret, "mrope_positions")
mrope_position_delta = self._get_processor_output_value(
ret, "mrope_position_delta"
)
if mrope_positions is None or mrope_position_delta is None:
return None
mrope_positions = torch.as_tensor(mrope_positions)
if mrope_positions.ndim == 3:
if mrope_positions.shape[1] != 1:
return None
mrope_positions = mrope_positions.squeeze(1)
if mrope_positions.ndim != 2 or mrope_positions.shape[0] != 3:
return None
mrope_position_delta = torch.as_tensor(mrope_position_delta)
if mrope_position_delta.ndim <= 1:
mrope_position_delta = mrope_position_delta.reshape(-1, 1)
return mrope_positions, mrope_position_delta
@staticmethod
def _as_grid_batch(value):
if value is None:
return None
if isinstance(value, torch.Tensor):
return value.unsqueeze(0) if value.ndim == 1 else value
tensor = torch.as_tensor(value, dtype=torch.long)
return tensor.unsqueeze(0) if tensor.ndim == 1 else tensor
def _compute_image_only_mrope_positions_from_offsets(
self,
input_len: int,
mm_items: List[MultimodalDataItem],
dtype: torch.dtype,
device: torch.device,
) -> Optional[tuple[torch.Tensor, torch.Tensor]]:
"""instead of calling get_rope_index, build mrope position from mm_items.offsets and image_grid_thw of each image
basically a simplified version of get_rope_index for image-only reqs
"""
if self.model_type not in (
"qwen3_vl",
"qwen3_vl_moe",
"qwen3_5",
"qwen3_5_moe",
"intern_s2_preview",
):
return None
image_items = [item for item in mm_items if item.is_image()]
if not image_items or len(image_items) != len(mm_items):
return None
spatial_merge_size = self._spatial_merge_size
sorted_items = sorted(image_items, key=lambda item: item.offsets[0][0])
position_segments = []
st = 0
next_pos = 0
for item in sorted_items:
if item.offsets is None or len(item.offsets) != 1:
return None
start, end = item.offsets[0]
if start < st or end >= input_len:
return None
text_len = start - st
if text_len > 0:
position_segments.append(
torch.arange(text_len, dtype=dtype, device=device)
.view(1, -1)
.expand(3, -1)
+ next_pos
)
next_pos += text_len
grid = self._as_grid_batch(item.model_specific_data.get("image_grid_thw"))
if grid is None or grid.shape[0] != 1:
return None
t, h, w = [int(x) for x in grid[0].tolist()]
llm_grid_t = t
llm_grid_h = h // spatial_merge_size
llm_grid_w = w // spatial_merge_size
num_image_tokens = llm_grid_t * llm_grid_h * llm_grid_w
if num_image_tokens != end - start + 1:
return None
t_index = (
torch.arange(llm_grid_t, dtype=dtype, device=device)
.view(-1, 1)
.expand(llm_grid_t, llm_grid_h * llm_grid_w)
.reshape(-1)
)
h_index = (
torch.arange(llm_grid_h, dtype=dtype, device=device)
.view(1, -1, 1)
.expand(llm_grid_t, llm_grid_h, llm_grid_w)
.reshape(-1)
)
w_index = (
torch.arange(llm_grid_w, dtype=dtype, device=device)
.view(1, 1, -1)
.expand(llm_grid_t, llm_grid_h, llm_grid_w)
.reshape(-1)
)
position_segments.append(
torch.stack([t_index, h_index, w_index]) + next_pos
)
next_pos += max(llm_grid_t, llm_grid_h, llm_grid_w)
st = end + 1
if st < input_len:
text_len = input_len - st
position_segments.append(
torch.arange(text_len, dtype=dtype, device=device)
.view(1, -1)
.expand(3, -1)
+ next_pos
)
mrope_positions = torch.cat(position_segments, dim=1).unsqueeze(1)
mrope_position_delta = (mrope_positions.max() + 1 - input_len).reshape(1, 1)
return mrope_positions, mrope_position_delta
@classmethod
def _concat_mm_item_grid(cls, mm_items: list[MultimodalDataItem], key, modality):
grids = []
for item in mm_items:
if not item.is_modality(modality):
continue
grid = cls._as_grid_batch(item.model_specific_data.get(key))
if grid is not None:
grids.append(grid)
if not grids:
return None
if len(grids) == 1:
return grids[0]
return torch.cat(grids, dim=0)
@classmethod
def _get_grid_from_output_or_items(
cls, ret, mm_items, key, modality, input_data=None
):
grid = cls._get_processor_output_value(ret, key)
if grid is None:
grid = cls._concat_mm_item_grid(mm_items, key, modality)
if grid is None and input_data and isinstance(input_data[0], dict):
grid = input_data[0].get(key)
return grid
def get_mm_data(self, prompt, embeddings, **kwargs):
img_grid_thw = kwargs.get("img_grid_thw", None)
video_grid_thw = kwargs.get("video_grid_thw", None)
audio_feature_lens = kwargs.get("audio_feature_lens", None)
video_timestamps = kwargs.get("video_timestamps", None)
second_per_grid_ts = kwargs.get("second_per_grid_ts", None)
audio_seq_lens = None
if audio_feature_lens is not None:
if self.model_type == "qwen3_omni_moe":
# apply _get_feat_extract_lengths to get seq_lens
input_lengths_leave = audio_feature_lens % 100
feat_lengths = (input_lengths_leave - 1) // 2 + 1
audio_seq_lens = (
((feat_lengths - 1) // 2 + 1 - 1) // 2
+ 1
+ (audio_feature_lens // 100) * 13
)
elif self.model_type == "qwen2_5_omni":
audio_seq_lens = (audio_feature_lens - 1) // 2 + 1
audio_seq_lens = (audio_seq_lens - 2) // 2 + 1
if (
self.model_type
in [
"qwen3_vl",
"qwen3_vl_moe",
"qwen3_5",
"qwen3_5_moe",
"intern_s2_preview",
]
and video_timestamps is not None
):
input_ids, offsets, modality_list = self.build_input_ids_with_timestamps(
prompt, embeddings, img_grid_thw, video_grid_thw, video_timestamps
)
else:
input_ids, offsets, modality_list = self.build_input_ids(
prompt, img_grid_thw, video_grid_thw, audio_seq_lens=audio_seq_lens
)
assert all(isinstance(modality, Modality) for modality in modality_list)
mrope_positions, mrope_position_delta = MRotaryEmbedding.get_rope_index(
spatial_merge_size=self._spatial_merge_size,
image_token_id=self.mm_tokens.image_token_id,
video_token_id=self.mm_tokens.video_token_id,
vision_start_token_id=self.vision_start_token_id,
model_type=self.model_type,
input_ids=torch.tensor(input_ids, dtype=torch.long).unsqueeze(0),
image_grid_thw=img_grid_thw,
video_grid_thw=video_grid_thw,
second_per_grid_ts=second_per_grid_ts,
use_audio_in_video=False,
audio_seqlens=(
audio_feature_lens if self.model_type == "qwen3_omni_moe" else None
),
audio_token_id=getattr(self.hf_config, "audio_token_id", None),
audio_start_token_id=self.audio_start_token_id,
position_id_per_seconds=getattr(
self.hf_config, "position_id_per_seconds", None
),
tokens_per_second=self._tokens_per_second,
)
mrope_positions = mrope_positions.squeeze(1)
mm_items = []
consumed_per_modality = {}
for modality, offset in zip(modality_list, offsets):
num_tokens = offset[1] - offset[0] + 1
embedding_start = consumed_per_modality.get(modality, 0)
embedding_slice = embeddings[modality][
embedding_start : embedding_start + num_tokens
]
consumed_per_modality[modality] = embedding_start + num_tokens
mm_items.append(
MultimodalDataItem(
modality=modality,
offsets=[offset],
precomputed_embeddings=embedding_slice,
)
)
return MultimodalProcessorOutput(
input_ids=input_ids,
mm_items=mm_items,
im_start_id=self.IM_START_TOKEN_ID,
im_end_id=self.IM_END_TOKEN_ID,
im_token_id=self.mm_tokens.image_token_id,
video_token_id=self.mm_tokens.video_token_id,
audio_token_id=self.mm_tokens.audio_token_id,
mrope_positions=mrope_positions,
mrope_position_delta=mrope_position_delta,
)
async def process_mm_data_async(
self,
image_data: List[Union[str, bytes]],
input_text,
request_obj,
*args,
**kwargs,
):
entry_time = time.perf_counter()
base_output = await self.load_mm_data(
prompt=input_text,
image_data=image_data,
video_data=request_obj.video_data,
audio_data=request_obj.audio_data,
multimodal_tokens=self.mm_tokens,
)
load_time = time.perf_counter()
rid = getattr(request_obj, "rid", "anonymous_rid")
video_metadata = None
if base_output.videos and not isinstance(base_output.videos[0], dict):
videos_processed = [
await preprocess_video(video, video_config=self.video_config)
for video in base_output.videos
]
base_output.videos, video_metadata = map(list, zip(*videos_processed))
preprocess_time = time.perf_counter()
# NOTE: for qwen3-vl, video_meta need to be passed in, since do_sample_frames is already done in preprocess_video
if self.hf_config.model_type in (
"qwen3_vl",
"qwen3_vl_moe",
"qwen3_5",
"qwen3_5_moe",
"intern_s2_preview",
):
mm_items, input_ids, ret = self.process_and_combine_mm_data(
base_output,
self.mm_tokens,
video_metadata=video_metadata,
do_sample_frames=False,
)
else:
mm_items, input_ids, ret = self.process_and_combine_mm_data(
base_output, self.mm_tokens
)
audio_feature_lengths = None
if self.model_type == "qwen3_omni_moe":
audio_item = next((mm for mm in mm_items if mm.is_audio()), None)
if audio_item:
audio_feature_lengths = torch.sum(
audio_item.feature_attention_mask, dim=1
)
second_per_grid_ts = self._get_processor_output_value(ret, "second_per_grid_ts")
if second_per_grid_ts is None:
second_per_grid_ts = self._get_processor_output_value(
ret, "video_second_per_grid"
)
process_time = time.perf_counter()
input_ids = input_ids.flatten()
base_input_ids = getattr(base_output, "input_ids", None)
if (
isinstance(base_input_ids, list)
and len(base_input_ids) == input_ids.numel()
):
# reuse preprocess input if it already carries list of input_ids
input_ids_list = base_input_ids
else:
input_ids_list = input_ids.tolist()
# look for if padded_input_ids already exists before computing
padded_input_ids = self._get_processor_output_value(ret, "padded_input_ids")
if padded_input_ids is None:
padded_input_ids = MultimodalProcessorOutput.build_padded_input_ids(
input_ids_list, mm_items
)
elif isinstance(padded_input_ids, torch.Tensor):
# reuse existing padded_input_ids
padded_input_ids = padded_input_ids.flatten().tolist()
else:
padded_input_ids = list(padded_input_ids)
image_grid_thw = self._get_grid_from_output_or_items(
ret, mm_items, "image_grid_thw", Modality.IMAGE, image_data
)
video_grid_thw = self._get_grid_from_output_or_items(
ret,
mm_items,
"video_grid_thw",
Modality.VIDEO,
request_obj.video_data,
)
mrope_result = self._get_precomputed_mrope_from_output(ret)
if mrope_result is None:
if (
video_grid_thw is None
and second_per_grid_ts is None
and audio_feature_lengths is None
):
mrope_result = self._compute_image_only_mrope_positions_from_offsets(
input_len=input_ids.numel(),
mm_items=mm_items,
dtype=input_ids.dtype,
device=input_ids.device,
)
if mrope_result is None:
mrope_result = MRotaryEmbedding.get_rope_index(
spatial_merge_size=self._spatial_merge_size,
image_token_id=self.mm_tokens.image_token_id,
video_token_id=self.mm_tokens.video_token_id,
vision_start_token_id=self.vision_start_token_id,
model_type=self.model_type,
tokens_per_second=self._tokens_per_second,
# use the expanded token ids
input_ids=input_ids.unsqueeze(0),
image_grid_thw=image_grid_thw,
video_grid_thw=video_grid_thw,
second_per_grid_ts=second_per_grid_ts,
use_audio_in_video=False,
audio_seqlens=audio_feature_lengths,
audio_token_id=getattr(self.hf_config, "audio_token_id", None),
audio_start_token_id=self.audio_start_token_id,
position_id_per_seconds=getattr(
self.hf_config, "position_id_per_seconds", None
),
)
mrope_positions, mrope_position_delta = mrope_result
if mrope_positions.ndim == 3:
mrope_positions = mrope_positions.squeeze(1)
get_rope_index_time = time.perf_counter()
logger.debug(
f"[QwenVLProcessor Perf] {rid=}, "
f"load_time: {(load_time - entry_time) * 1000:.2f} ms, "
f"preprocess_time: {(preprocess_time - load_time) * 1000:.2f} ms, "
f"process_time: {(process_time - preprocess_time) * 1000:.2f} ms, "
f"get_rope_index_time: {(get_rope_index_time - process_time) * 1000:.2f} ms, "
f"total_time: {(get_rope_index_time - entry_time) * 1000:.2f} ms"
)
return MultimodalProcessorOutput(
input_ids=input_ids_list,
padded_input_ids=padded_input_ids,
mm_items=mm_items,
im_start_id=self.vision_start_token_id,
im_end_id=self.vision_end_token_id,
im_token_id=self.mm_tokens.image_token_id,
video_token_id=self.mm_tokens.video_token_id,
audio_token_id=self.mm_tokens.audio_token_id,
mrope_positions=mrope_positions,
mrope_position_delta=mrope_position_delta,
)