model: support Moss-VL (#23454)

This commit is contained in:
Shaojun Zhou
2026-04-24 11:14:29 +08:00
committed by GitHub
parent bf98eb3ab7
commit 59724e90a9
10 changed files with 2401 additions and 6 deletions
@@ -1360,6 +1360,7 @@ multimodal_model_archs = [
"Mistral3ForConditionalGeneration",
"MultiModalityCausalLM",
"MllamaForConditionalGeneration",
"MossVLForConditionalGeneration",
"NemotronH_Nano_VL_V2",
"PixtralForConditionalGeneration",
"Qwen2AudioForConditionalGeneration",
@@ -1427,6 +1428,7 @@ def is_encoder_decoder_model(model_architectures: List[str]):
models = [
"WhisperForConditionalGeneration",
"MllamaForConditionalGeneration",
"MossVLForConditionalGeneration",
]
return any(model in model_architectures for model in models)
@@ -1442,6 +1444,7 @@ def is_multimodal_chunked_prefill_supported(model_architectures: List[str]):
"Grok1AForCausalLM",
"LlavaLlamaForCausalLM",
"MllamaForConditionalGeneration",
"MossVLForConditionalGeneration",
"CLIPModel",
]
if any(multi_model_arch in unsupported for multi_model_arch in model_architectures):
@@ -511,6 +511,7 @@ class FlashInferAttnBackend(AttentionBackend):
spec_info=None,
fixed_split_size=self.prefill_split_tile_size,
multi_item_params=multi_item_params,
cross_attention_custom_mask=forward_batch.cross_attention_custom_mask,
)
self.forward_metadata = PrefillMetadata(
self.prefill_wrappers_paged,
@@ -1242,6 +1243,8 @@ class FlashInferIndicesUpdaterPrefill:
encoder_lens: Optional[torch.Tensor],
spec_info: Optional[SpecInput],
fixed_split_size: Optional[int] = None,
multi_item_params: Optional[MultiItemScoringParams] = None,
cross_attention_custom_mask: Optional[torch.Tensor] = None,
):
# Keep the signature for type checking. It will be assigned during runtime.
raise NotImplementedError()
@@ -1259,6 +1262,7 @@ class FlashInferIndicesUpdaterPrefill:
spec_info: Optional[SpecInput],
fixed_split_size: Optional[int] = None,
multi_item_params: Optional[MultiItemScoringParams] = None,
cross_attention_custom_mask: Optional[torch.Tensor] = None,
):
if use_ragged:
# TODO: remove this device sync, we can use forward_batch.extend_prefix_lens_cpu
@@ -1299,6 +1303,7 @@ class FlashInferIndicesUpdaterPrefill:
spec_info: Optional[SpecInput],
fixed_split_size: Optional[int] = None,
multi_item_params: Optional[MultiItemScoringParams] = None,
cross_attention_custom_mask: Optional[torch.Tensor] = None,
):
for wrapper_id in range(2):
if wrapper_id == 0:
@@ -1348,6 +1353,7 @@ class FlashInferIndicesUpdaterPrefill:
spec_info: Optional[SpecInput],
fixed_split_size: Optional[int] = None,
multi_item_params: Optional[MultiItemScoringParams] = None,
cross_attention_custom_mask: Optional[torch.Tensor] = None,
):
for wrapper_id in range(2):
if wrapper_id == 0:
@@ -1375,6 +1381,9 @@ class FlashInferIndicesUpdaterPrefill:
use_ragged,
spec_info,
multi_item_params=multi_item_params,
cross_attention_custom_mask=(
cross_attention_custom_mask if wrapper_id == 1 else None
),
)
def call_begin_forward(
@@ -1394,6 +1403,7 @@ class FlashInferIndicesUpdaterPrefill:
use_sliding_window_kv_pool: bool = False,
fixed_split_size: Optional[int] = None,
multi_item_params: Optional[MultiItemScoringParams] = None,
cross_attention_custom_mask: Optional[torch.Tensor] = None,
):
bs = len(seq_lens)
if spec_info is None:
@@ -1418,7 +1428,7 @@ class FlashInferIndicesUpdaterPrefill:
qo_indptr[1 : bs + 1] = torch.cumsum(seq_lens - prefix_lens, dim=0)
qo_indptr = qo_indptr[: bs + 1]
custom_mask = None
custom_mask = cross_attention_custom_mask
else:
assert isinstance(spec_info, SpecInput)
kv_indices, kv_indptr, qo_indptr, custom_mask = (
@@ -385,6 +385,11 @@ class MultimodalProcessorOutput:
mrope_positions: Optional[torch.Tensor] = None
mrope_position_delta: Optional[torch.Tensor] = None
# Moss-VL related
vision_position_ids: Optional[torch.Tensor] = None
media_nums_per_sample: Optional[List[int]] = None
visible_frame_counts: Optional[torch.Tensor] = None
# for transformers-compatibility
token_type_ids: Optional[torch.Tensor] = None
@@ -404,6 +409,9 @@ class MultimodalProcessorOutput:
audio_end_id=d.get("audio_end_id"),
mrope_positions=d.get("mrope_positions"),
mrope_position_delta=d.get("mrope_position_delta"),
vision_position_ids=d.get("vision_position_ids"),
media_nums_per_sample=d.get("media_nums_per_sample"),
visible_frame_counts=d.get("visible_frame_counts"),
)
@@ -436,6 +444,11 @@ class MultimodalInputs:
mrope_position_delta: Optional[torch.Tensor] = None
mrope_position_delta_repeated_cache: Optional[torch.Tensor] = None
# Moss-VL related
vision_position_ids: Optional[torch.Tensor] = None
media_nums_per_sample: Optional[List[int]] = None
visible_frame_counts: Optional[torch.Tensor] = None
def release_features(self):
"""Release feature tensors to free GPU memory."""
for item in self.mm_items:
@@ -494,6 +507,9 @@ class MultimodalInputs:
"audio_start_id",
"audio_end_id",
"audio_token_id",
"vision_position_ids",
"media_nums_per_sample",
"visible_frame_counts",
]
for arg in optional_args:
val = getattr(obj, arg, None)
@@ -1595,6 +1611,49 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
len(self.out_cache_loc) == self.extend_num_tokens
), f"Expected {len(self.out_cache_loc)}, got {self.extend_num_tokens}"
if self.extend_input_logprob_token_ids is not None:
new_token_ids_parts = []
offset = 0
for i, req in enumerate(self.reqs):
encoder_len = self.encoder_lens_cpu[i]
old_start_len = self.extend_logprob_start_lens[i]
old_contribution = req.extend_input_len - old_start_len
if len(req.prefix_indices) < encoder_len:
tokens_to_strip = max(0, encoder_len - old_start_len)
new_token_ids_parts.append(
self.extend_input_logprob_token_ids[
offset + tokens_to_strip : offset + old_contribution
]
)
self.extend_logprob_start_lens[i] = max(
0, old_start_len - encoder_len
)
else:
new_token_ids_parts.append(
self.extend_input_logprob_token_ids[
offset : offset + old_contribution
]
)
offset += old_contribution
if new_token_ids_parts:
self.extend_input_logprob_token_ids = torch.cat(new_token_ids_parts)
else:
self.extend_input_logprob_token_ids = None
for i, req in enumerate(self.reqs):
encoder_len = self.encoder_lens_cpu[i]
if encoder_len == 0:
continue
if len(req.prefix_indices) < encoder_len:
req.extend_input_len -= encoder_len
req.extend_logprob_start_len = max(
0, req.extend_logprob_start_len - encoder_len
)
req.logprob_start_len = max(req.logprob_start_len, encoder_len)
def prepare_for_extend(self):
self.forward_mode = ForwardMode.EXTEND
@@ -1831,6 +1890,17 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
)
self.replace_embeds = replace_embeds_tensor
self.replace_positions = replace_positions_tensor
for mm_input in multimodal_inputs:
if mm_input is None:
continue
if isinstance(mm_input.vision_position_ids, torch.Tensor):
mm_input.vision_position_ids = mm_input.vision_position_ids.to(
self.device, non_blocking=True
)
if isinstance(mm_input.visible_frame_counts, torch.Tensor):
mm_input.visible_frame_counts = mm_input.visible_frame_counts.to(
self.device, non_blocking=True
)
self.multimodal_inputs = multimodal_inputs
self.token_type_ids = token_type_ids_tensor
self.seq_lens_sum = sum(seq_lens)
@@ -730,14 +730,24 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
input_text, is_cross_encoder_request
)
if self.mm_processor and obj.contains_mm_input():
contains_mm_input = obj.contains_mm_input()
is_mossvl = (
"MossVLForConditionalGeneration"
in self.model_config.hf_config.architectures
)
should_run_mm_processor = self.mm_processor is not None and (
contains_mm_input or is_mossvl
)
if should_run_mm_processor:
if obj.image_data is not None and not isinstance(obj.image_data, list):
obj.image_data = [obj.image_data]
if obj.video_data is not None and not isinstance(obj.video_data, list):
obj.video_data = [obj.video_data]
if obj.audio_data is not None and not isinstance(obj.audio_data, list):
obj.audio_data = [obj.audio_data]
self._validate_mm_limits(obj)
if contains_mm_input:
self._validate_mm_limits(obj)
mm_inputs = None
@@ -351,6 +351,7 @@ class ForwardBatch(ForwardBatchDeepSeekMHAMixin):
encoder_lens: Optional[torch.Tensor] = None
encoder_lens_cpu: Optional[List[int]] = None
encoder_out_cache_loc: Optional[torch.Tensor] = None
cross_attention_custom_mask: Optional[torch.Tensor] = None
# For LoRA
lora_ids: Optional[List[str]] = None
@@ -2786,6 +2786,10 @@ class ModelRunner(ModelRunnerKVCacheMixin):
pp_proxy_tensors=None,
) -> Union[LogitsProcessorOutput, PPProxyTensors]:
if not skip_attn_backend_init:
if hasattr(self.model, "prepare_forward_batch"):
# Prepare model-specific attention metadata before planning,
# e.g. Moss-VL's prefill cross-attention custom mask.
self.model.prepare_forward_batch(forward_batch)
if self.server_args.enable_pdmux:
self.decode_attn_backend.init_forward_metadata(forward_batch)
forward_batch.attn_backend = self.decode_attn_backend
@@ -2841,6 +2845,10 @@ class ModelRunner(ModelRunnerKVCacheMixin):
)
if not skip_attn_backend_init:
if hasattr(self.model, "prepare_forward_batch"):
# Prepare model-specific attention metadata before planning,
# e.g. Moss-VL's prefill cross-attention custom mask.
self.model.prepare_forward_batch(forward_batch)
self.attn_backend.init_forward_metadata(forward_batch)
return (
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,612 @@
import asyncio
import os
import re
import tempfile
from typing import Dict, List, Optional, Tuple, Union
from urllib.parse import unquote, urlparse
import pybase64
import requests
import torch
from sglang.srt.managers.schedule_batch import (
Modality,
MultimodalDataItem,
MultimodalProcessorOutput,
)
from sglang.srt.models.moss_vl import MossVLForConditionalGeneration
from sglang.srt.multimodal.processors.base_processor import (
SGL_USE_CUDA_IPC,
)
from sglang.srt.multimodal.processors.base_processor import (
BaseMultimodalProcessor as SGLangBaseProcessor,
)
from sglang.srt.multimodal.processors.base_processor import (
MultimodalSpecialTokens,
)
from sglang.srt.utils.cuda_ipc_transport_utils import CudaIpcTensorTransportProxy
class MossVLImageProcessor(SGLangBaseProcessor):
models = [MossVLForConditionalGeneration]
def __init__(self, hf_config, server_args, _processor, *args, **kwargs):
super().__init__(hf_config, server_args, _processor, *args, **kwargs)
self.image_only_mm_tokens = MultimodalSpecialTokens(
image_token="<|image|>",
image_token_regex=re.compile(re.escape("<|image|>")),
).build(_processor)
self.image_token_id = getattr(hf_config, "image_token_id", None)
self.vision_seq_pad_multiple = 1
def _build_mm_items(
self, processor_output: Dict, input_ids: torch.Tensor
) -> List[MultimodalDataItem]:
pixel_values = processor_output.get("pixel_values")
if pixel_values is None:
return []
item = MultimodalDataItem(
modality=Modality.IMAGE,
feature=pixel_values,
model_specific_data={},
)
grid_thw = processor_output.get("grid_thw")
if grid_thw is not None:
item.set("grid_thw", grid_thw)
return [item]
def _build_vision_token_info(
self,
grid_thw: Optional[torch.Tensor],
media_nums_per_sample: Optional[List[int]],
) -> List[dict]:
if grid_thw is None:
return []
grid_thw = torch.as_tensor(grid_thw, dtype=torch.long)
if grid_thw.ndim == 1:
grid_thw = grid_thw.unsqueeze(0)
if grid_thw.numel() == 0:
return []
tokens_per_media = (grid_thw[:, 0] * grid_thw[:, 1] * grid_thw[:, 2]) // (
self.spatial_merge_size**2
)
if media_nums_per_sample is None:
media_nums_per_sample = [grid_thw.shape[0]]
batch_size = len(media_nums_per_sample)
if batch_size == 1:
total_len = 0
for i in range(grid_thw.shape[0]):
num_tokens = tokens_per_media[i].item()
num_frames = grid_thw[i, 0].item()
total_len += num_tokens + num_frames
if total_len % self.vision_seq_pad_multiple != 0:
max_seq_len = (
(total_len + self.vision_seq_pad_multiple - 1)
// self.vision_seq_pad_multiple
* self.vision_seq_pad_multiple
)
else:
max_seq_len = total_len
sample_info = {
"medias": [],
"total_length": total_len,
"pad_start": total_len,
"pad_end": max_seq_len,
}
current_seq_len = 0
for media_idx in range(grid_thw.shape[0]):
num_tokens = tokens_per_media[media_idx].item()
t, h, w = grid_thw[media_idx].tolist()
num_frames = t
tokens_per_frame = num_tokens // num_frames
chunk_len = num_frames * (tokens_per_frame + 1)
sample_info["medias"].append(
{
"start": current_seq_len,
"end": current_seq_len + chunk_len,
"length": chunk_len,
"num_frames": num_frames,
"grid_h": h,
"grid_w": w,
"vision_tokens_per_frame": tokens_per_frame,
"has_separator": True,
}
)
current_seq_len += chunk_len
return [sample_info]
tokens_per_sample = []
media_idx = 0
for num_medias_in_sample in media_nums_per_sample:
sample_tokens = 0
for i in range(num_medias_in_sample):
num_tokens = tokens_per_media[media_idx + i].item()
num_frames = grid_thw[media_idx + i, 0].item()
sample_tokens += num_tokens + num_frames
tokens_per_sample.append(sample_tokens)
media_idx += num_medias_in_sample
max_seq_len = max(tokens_per_sample)
if max_seq_len % self.vision_seq_pad_multiple != 0:
max_seq_len = (
(max_seq_len + self.vision_seq_pad_multiple - 1)
// self.vision_seq_pad_multiple
* self.vision_seq_pad_multiple
)
vision_token_info = []
media_idx = 0
for sample_idx, num_medias_in_sample in enumerate(media_nums_per_sample):
sample_info = {
"medias": [],
"total_length": tokens_per_sample[sample_idx],
"pad_start": tokens_per_sample[sample_idx],
"pad_end": max_seq_len,
}
seq_offset = 0
for _ in range(num_medias_in_sample):
num_tokens = tokens_per_media[media_idx].item()
t, h, w = grid_thw[media_idx].tolist()
num_frames = t
tokens_per_frame = num_tokens // num_frames
media_length = num_tokens + num_frames
sample_info["medias"].append(
{
"start": seq_offset,
"end": seq_offset + media_length,
"length": media_length,
"num_frames": num_frames,
"grid_h": h,
"grid_w": w,
"vision_tokens_per_frame": tokens_per_frame,
"has_separator": True,
}
)
seq_offset += media_length
media_idx += 1
vision_token_info.append(sample_info)
return vision_token_info
def _compute_position_ids(
self,
input_ids: torch.Tensor,
attention_mask: Optional[torch.Tensor] = None,
) -> torch.Tensor:
is_image_token = input_ids == self.image_token_id
if attention_mask is not None:
is_padding = attention_mask == 0
else:
is_padding = torch.zeros_like(input_ids, dtype=torch.bool)
is_regular_token = ~(is_image_token | is_padding)
cumulative_regular = is_regular_token.long().cumsum(dim=1)
base_position_ids = cumulative_regular - is_regular_token.long()
base_position_ids = base_position_ids.masked_fill(is_padding, 0)
return base_position_ids.unsqueeze(0).expand(3, -1, -1).clone()
def _compute_vision_position_ids(
self,
input_ids: torch.Tensor,
position_ids: torch.Tensor,
vision_token_info: List[dict],
max_vision_seq_len: int,
attention_mask: Optional[torch.Tensor],
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
batch_size = input_ids.shape[0]
device = input_ids.device
image_token_indices = (input_ids == self.image_token_id).nonzero()
flat_eff_h = []
flat_eff_w = []
flat_vis_starts = []
for info in vision_token_info:
medias = info.get("medias", [])
for media in medias:
num_frames = media["num_frames"]
h, w = media["grid_h"], media["grid_w"]
eh, ew = h // self.spatial_merge_size, w // self.spatial_merge_size
start = media["start"]
tok_per_frame = media["vision_tokens_per_frame"]
stride = tok_per_frame + 1
for f in range(num_frames):
flat_eff_h.append(eh)
flat_eff_w.append(ew)
flat_vis_starts.append(start + f * stride)
vision_pos_ids = torch.zeros(
(3, batch_size, max_vision_seq_len),
dtype=torch.long,
device=device,
)
if len(flat_eff_h) == 0 or len(image_token_indices) == 0:
rope_deltas = (
position_ids.max(dim=0).values.max(dim=-1).values
+ 1
- input_ids.shape[1]
)
return vision_pos_ids, position_ids, rope_deltas
num_matches = min(len(flat_eff_h), len(image_token_indices))
flat_eff_h = torch.tensor(
flat_eff_h[:num_matches], device=device, dtype=torch.long
)
flat_eff_w = torch.tensor(
flat_eff_w[:num_matches], device=device, dtype=torch.long
)
flat_vis_starts = torch.tensor(
flat_vis_starts[:num_matches], device=device, dtype=torch.long
)
target_indices = image_token_indices[:num_matches]
batch_rows = target_indices[:, 0]
text_cols = target_indices[:, 1]
max_hw = torch.maximum(flat_eff_h, flat_eff_w)
shifts = max_hw + 1
shift_map = torch.zeros(
(batch_size, input_ids.shape[1]), dtype=torch.long, device=device
)
shift_map[batch_rows, text_cols] = shifts
cum_shifts = shift_map.cumsum(dim=1)
orig_pos = position_ids[0, batch_rows, text_cols]
shifts_before = cum_shifts[batch_rows, text_cols] - shifts
t_vals = orig_pos + shifts_before
new_pos_ids = position_ids + cum_shifts.unsqueeze(0)
img_token_mask = torch.zeros_like(input_ids, dtype=torch.bool)
img_token_mask[batch_rows, text_cols] = True
new_pos_ids[:, img_token_mask] -= 1
if attention_mask is not None:
padding_mask = (attention_mask == 0).unsqueeze(0)
new_pos_ids.masked_fill_(padding_mask, 0)
position_ids = new_pos_ids
unique_shapes = torch.unique(
torch.stack([flat_eff_h, flat_eff_w], dim=1), dim=0
)
for shape in unique_shapes:
eh, ew = shape[0].item(), shape[1].item()
mask = (flat_eff_h == eh) & (flat_eff_w == ew)
sub_t_vals = t_vals[mask]
sub_batch_rows = batch_rows[mask]
sub_vis_starts = flat_vis_starts[mask]
num_frames_sub = sub_t_vals.shape[0]
if num_frames_sub == 0:
continue
y_grid = (
torch.arange(eh, device=device)
.view(1, eh, 1)
.expand(num_frames_sub, -1, ew)
)
x_grid = (
torch.arange(ew, device=device)
.view(1, 1, ew)
.expand(num_frames_sub, eh, -1)
)
t_grid = sub_t_vals.view(-1, 1, 1).expand(-1, eh, ew)
h_grid = t_grid + y_grid
w_grid = t_grid + x_grid
flat_t = t_grid.reshape(-1)
flat_h = h_grid.reshape(-1)
flat_w = w_grid.reshape(-1)
tokens_per_frame = eh * ew
seq_offsets = torch.arange(tokens_per_frame, device=device).unsqueeze(0)
abs_seq_offsets = seq_offsets + sub_vis_starts.unsqueeze(1)
flat_seq_inds = abs_seq_offsets.reshape(-1)
flat_batch_inds = (
sub_batch_rows.unsqueeze(1).expand(-1, tokens_per_frame).reshape(-1)
)
valid_mask = flat_seq_inds < max_vision_seq_len
if valid_mask.any():
final_b = flat_batch_inds[valid_mask]
final_s = flat_seq_inds[valid_mask]
vision_pos_ids[0, final_b, final_s] = flat_t[valid_mask]
vision_pos_ids[1, final_b, final_s] = flat_h[valid_mask]
vision_pos_ids[2, final_b, final_s] = flat_w[valid_mask]
sep_vals = t_vals + max_hw
sep_indices = flat_vis_starts + (flat_eff_h * flat_eff_w)
valid_sep_mask = sep_indices < max_vision_seq_len
if valid_sep_mask.any():
final_b = batch_rows[valid_sep_mask]
final_s = sep_indices[valid_sep_mask]
vals = sep_vals[valid_sep_mask]
vision_pos_ids[0, final_b, final_s] = vals
vision_pos_ids[1, final_b, final_s] = vals
vision_pos_ids[2, final_b, final_s] = vals
max_pos = position_ids.max(dim=0).values.max(dim=-1).values
rope_deltas = max_pos + 1 - input_ids.shape[1]
return vision_pos_ids, position_ids, rope_deltas
def _compute_position_metadata(
self,
input_ids: torch.Tensor,
attention_mask: Optional[torch.Tensor],
grid_thw: Optional[torch.Tensor],
media_nums_per_sample: Optional[List[int]],
) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor], List[dict]]:
position_ids = self._compute_position_ids(input_ids, attention_mask)
if grid_thw is None:
max_pos = position_ids.max(dim=0).values.max(dim=-1).values
rope_deltas = (max_pos + 1 - input_ids.shape[1]).unsqueeze(1)
return position_ids, rope_deltas, None, []
vision_token_info = self._build_vision_token_info(
grid_thw, media_nums_per_sample
)
max_vision_seq_len = 0
if vision_token_info:
max_vision_seq_len = max(
info.get("pad_end", 0) for info in vision_token_info
)
if max_vision_seq_len == 0:
max_pos = position_ids.max(dim=0).values.max(dim=-1).values
rope_deltas = (max_pos + 1 - input_ids.shape[1]).unsqueeze(1)
return position_ids, rope_deltas, None, vision_token_info
vision_position_ids, position_ids, rope_deltas = (
self._compute_vision_position_ids(
input_ids=input_ids,
position_ids=position_ids,
vision_token_info=vision_token_info,
max_vision_seq_len=max_vision_seq_len,
attention_mask=attention_mask,
)
)
return (
position_ids,
rope_deltas.unsqueeze(1),
vision_position_ids,
vision_token_info,
)
def _compute_visible_frame_counts(
self, cross_attention_mask: Optional[Union[torch.Tensor, List]]
) -> Optional[torch.Tensor]:
if cross_attention_mask is None:
return None
# HF Moss-VL processor outputs a bool mask with shape
# (batch_size, 1, text_len, num_frames), where True means masked.
cross_attention_mask = torch.as_tensor(cross_attention_mask, dtype=torch.bool)
visible_frame_counts = (~cross_attention_mask).sum(dim=-1, dtype=torch.int32)
return visible_frame_counts.reshape(-1)
def _resolve_file_url(self, value: str) -> str:
parsed = urlparse(value)
path = unquote(parsed.path or "")
if parsed.netloc and not path.startswith("/"):
path = f"/{path}"
return path
def _write_video_bytes_to_tempfile(
self, video_bytes: bytes, suffix: str = ".mp4"
) -> str:
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as f:
f.write(video_bytes)
return f.name
def _normalize_video_string(self, value: str) -> Tuple[str, Optional[str]]:
if value.startswith("file://"):
return self._resolve_file_url(value), None
if os.path.isfile(value):
return value, None
if value.startswith(("http://", "https://")):
timeout = int(os.getenv("REQUEST_TIMEOUT", "10"))
response = requests.get(value, stream=True, timeout=timeout)
response.raise_for_status()
suffix = os.path.splitext(urlparse(value).path)[1] or ".mp4"
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix) as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
return f.name, f.name
if value.startswith("data:"):
header, encoded = value.split(",", 1)
mime = header.split(";", 1)[0]
suffix = ".mp4"
if "/" in mime:
ext = mime.rsplit("/", 1)[-1]
if ext:
suffix = f".{ext}"
temp_path = self._write_video_bytes_to_tempfile(
pybase64.b64decode(encoded, validate=True),
suffix=suffix,
)
return temp_path, temp_path
temp_path = self._write_video_bytes_to_tempfile(
pybase64.b64decode(value, validate=True)
)
return temp_path, temp_path
def _normalize_single_video_input(
self, video_input: Union[str, Dict]
) -> Tuple[Union[str, Dict], List[str]]:
temp_paths: List[str] = []
if isinstance(video_input, dict):
normalized = dict(video_input)
video_path, temp_path = self._normalize_video_string(
normalized["video_path"]
)
normalized["video_path"] = video_path
if temp_path is not None:
temp_paths.append(temp_path)
return normalized, temp_paths
normalized_path, temp_path = self._normalize_video_string(video_input)
if temp_path is not None:
temp_paths.append(temp_path)
return normalized_path, temp_paths
async def _normalize_video_inputs_async(
self, video_data: Optional[List[Union[str, Dict]]]
) -> Tuple[Optional[List[Union[str, Dict]]], List[str]]:
if not video_data:
return video_data, []
loop = asyncio.get_running_loop()
futures = [
loop.run_in_executor(
self.io_executor, self._normalize_single_video_input, v
)
for v in video_data
]
results = await asyncio.gather(*futures)
normalized_inputs: List[Union[str, Dict]] = []
temp_paths: List[str] = []
for normalized_input, created_paths in results:
normalized_inputs.append(normalized_input)
temp_paths.extend(created_paths)
return normalized_inputs, temp_paths
async def process_mm_data_async(
self,
image_data: List[Union[str, bytes, Dict]],
input_text,
request_obj,
*args,
**kwargs,
):
normalized_video_data, temp_video_paths = (
await self._normalize_video_inputs_async(request_obj.video_data)
)
try:
base_output = self.load_mm_data(
prompt=input_text,
image_data=image_data,
multimodal_tokens=self.image_only_mm_tokens,
)
processor_output = self.process_mm_data(
input_text=base_output.input_text,
images=base_output.images,
videos=normalized_video_data,
)
input_ids = torch.as_tensor(processor_output["input_ids"], dtype=torch.long)
attention_mask = processor_output.get("attention_mask")
if attention_mask is not None:
attention_mask = torch.as_tensor(attention_mask, dtype=torch.long)
grid_thw = processor_output.get("grid_thw")
if grid_thw is not None:
grid_thw = torch.as_tensor(grid_thw, dtype=torch.long)
media_nums_per_sample = processor_output.get("media_nums_per_sample")
visible_frame_counts = self._compute_visible_frame_counts(
processor_output.get("cross_attention_mask")
)
(
mrope_positions,
mrope_position_delta,
vision_position_ids,
vision_token_info,
) = self._compute_position_metadata(
input_ids=input_ids,
attention_mask=attention_mask,
grid_thw=grid_thw,
media_nums_per_sample=media_nums_per_sample,
)
input_ids = input_ids.flatten()
mm_items = self._build_mm_items(processor_output, input_ids)
if mm_items and vision_token_info:
mm_items[0].set("vision_token_info", vision_token_info[0])
if SGL_USE_CUDA_IPC:
for item in mm_items:
if isinstance(item.feature, torch.Tensor) and item.feature.is_cuda:
sync_flag, available_slice = (
self.cudaipc_mmfeature_pool.return_a_slice_tensor_with_flag(
item.feature
)
)
if isinstance(available_slice, torch.Tensor):
available_slice.copy_(
item.feature.reshape(-1).view(torch.int8),
non_blocking=True,
)
item.feature = CudaIpcTensorTransportProxy(
data=available_slice,
info_data=item.feature,
sync_buffer_meta=sync_flag,
)
elif (
isinstance(item.precomputed_embeddings, torch.Tensor)
and item.precomputed_embeddings.is_cuda
):
sync_flag, available_slice = (
self.cudaipc_mmfeature_pool.return_a_slice_tensor_with_flag(
item.precomputed_embeddings
)
)
if isinstance(available_slice, torch.Tensor):
flattened = item.precomputed_embeddings.reshape(-1)
available_slice.copy_(
flattened.view(torch.int8),
non_blocking=True,
)
item.precomputed_embeddings = CudaIpcTensorTransportProxy(
data=available_slice,
info_data=item.precomputed_embeddings,
sync_buffer_meta=sync_flag,
)
return MultimodalProcessorOutput(
input_ids=input_ids.tolist(),
mm_items=mm_items,
im_token_id=self.image_token_id,
mrope_positions=mrope_positions.squeeze(1),
mrope_position_delta=mrope_position_delta,
media_nums_per_sample=media_nums_per_sample,
vision_position_ids=(
vision_position_ids.squeeze(1)
if vision_position_ids is not None
else None
),
visible_frame_counts=visible_frame_counts,
)
finally:
for temp_path in temp_video_paths:
try:
os.unlink(temp_path)
except FileNotFoundError:
pass
+29 -2
View File
@@ -133,7 +133,11 @@ class Conversation:
ret += role + ": " # must be end with a space
return ret
elif self.sep_style == SeparatorStyle.ADD_NEW_LINE_SINGLE:
ret = "" if system_prompt == "" else system_prompt + self.sep
ret = (
""
if (not self.system_message or system_prompt == "")
else system_prompt + self.sep
)
for role, message in self.messages:
if message:
ret += role + "\n" + message + self.sep
@@ -634,7 +638,7 @@ def generate_chat_conv(
conv.modalities.append(content.modalities)
image_token = (
conv.image_token + "\n"
if conv.name != "qwen2-vl"
if conv.name not in ("qwen2-vl", "moss-vl")
else conv.image_token
)
add_token_as_needed: bool = (
@@ -1013,6 +1017,20 @@ register_conv_template(
)
)
register_conv_template(
Conversation(
name="moss-vl",
system_message="",
system_template="<|im_start|>system\n{system_message}",
roles=("<|im_start|>user", "<|im_start|>assistant"),
sep="<|im_end|>\n",
sep_style=SeparatorStyle.ADD_NEW_LINE_SINGLE,
stop_str=["<|im_end|>"],
image_token="<|image|>",
video_token="<|video|>",
)
)
register_conv_template(
Conversation(
name="points-v15-chat",
@@ -1051,6 +1069,7 @@ MODEL_TYPE_TO_TEMPLATE = {
"phi4mm": "phi-4-mm",
"minicpmv": "minicpmv",
"minicpmo": "minicpmo",
"moss_vl": "moss-vl",
"deepseek-ocr": "deepseek-ocr",
"paddleocr_vl": "paddle-ocr",
"whisper": "whisper",
@@ -1064,6 +1083,14 @@ def match_points_v15_chat(model_path: str):
return "points-v15-chat"
@register_conv_template_matching_function
def match_moss_vl(model_path: str):
if re.search(r"moss.*vl|moss-vl", model_path, re.IGNORECASE):
return "moss-vl"
model_type = get_model_type(model_path)
return MODEL_TYPE_TO_TEMPLATE.get(model_type)
def get_model_type(model_path: str) -> Optional[str]:
config_path = os.path.join(model_path, "config.json")
if not os.path.exists(config_path):
+12 -1
View File
@@ -2033,6 +2033,17 @@ class ServerArgs:
if self.is_attention_backend_not_set():
self.attention_backend = "triton"
logger.info("Use triton as default attention backend for Gemma4")
elif model_arch == "MossVLForConditionalGeneration":
if self.is_attention_backend_not_set():
self.prefill_attention_backend = "flashinfer"
logger.info(
"Use flashinfer as default prefill attention backend for Moss-VL"
)
prefill_backend, _ = self.get_attention_backends()
assert prefill_backend == "flashinfer", (
"MossVLForConditionalGeneration requires flashinfer prefill "
"attention backend for cross-attention custom mask support."
)
elif model_arch in ["Exaone4ForCausalLM", "ExaoneMoEForCausalLM"]:
if hf_config.sliding_window_pattern is not None:
logger.warning(
@@ -2407,7 +2418,7 @@ class ServerArgs:
if current_platform.is_out_of_tree():
return current_platform.get_default_attention_backend()
# Whisper requires flashinfer for cross-attention CUDA graph support
# Whisper requires flashinfer for cross-attention CUDA graph support.
if "WhisperForConditionalGeneration" in (
model_config.hf_config.architectures or []
):