feat(mimo-v2): add EPD disaggregation support (#24931)
This commit is contained in:
@@ -193,7 +193,29 @@ _MODALITY_GRID_ATTRS = {
|
||||
Modality.VIDEO: ("video_grid_thw", False),
|
||||
Modality.AUDIO: ("audio_feature_lens", True),
|
||||
}
|
||||
_VIDEO_META_ATTRS = ("video_timestamps", "second_per_grid_ts")
|
||||
# Per-part video metadata for EPD. Tensor attrs cat on dim=0 across parts;
|
||||
# others chain as lists. video_meta_attrs_for(model_type) resolves the active
|
||||
# set per instance so non-MiMo runs skip the MiMo audio fields entirely.
|
||||
_GENERAL_VIDEO_META_ATTRS = (
|
||||
"video_timestamps",
|
||||
"second_per_grid_ts",
|
||||
)
|
||||
# MiMo-VL audio-in-video fields; appended only when model_type is MiMo.
|
||||
_MIMO_VIDEO_AUDIO_META_ATTRS = (
|
||||
"video_audio_feature_lens",
|
||||
"video_audio_segment_lens_flat",
|
||||
"video_audio_per_video_num_units",
|
||||
"video_audio_embedding",
|
||||
)
|
||||
_VIDEO_META_TENSOR_ATTRS = ("video_audio_feature_lens", "video_audio_embedding")
|
||||
|
||||
|
||||
def video_meta_attrs_for(model_type: Optional[str]) -> tuple:
|
||||
"""Video-meta attrs for model_type. MiMo appends its audio-in-video fields."""
|
||||
attrs = _GENERAL_VIDEO_META_ATTRS
|
||||
if model_type and "mimo" in model_type.lower():
|
||||
attrs = attrs + _MIMO_VIDEO_AUDIO_META_ATTRS
|
||||
return attrs
|
||||
|
||||
|
||||
def _cat_grid(dims, flatten_items=False):
|
||||
@@ -231,6 +253,7 @@ class MultiModalEmbeddingData(EmbeddingData):
|
||||
modality,
|
||||
embedding,
|
||||
embedding_shape,
|
||||
model_type: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(
|
||||
@@ -243,6 +266,7 @@ class MultiModalEmbeddingData(EmbeddingData):
|
||||
embedding_shape,
|
||||
**kwargs,
|
||||
)
|
||||
self.video_meta_attrs = video_meta_attrs_for(model_type)
|
||||
self.img_grid_thw = [None] * num_parts
|
||||
self.video_grid_thw = [None] * num_parts
|
||||
self.audio_feature_lens = [None] * num_parts
|
||||
@@ -256,8 +280,8 @@ class MultiModalEmbeddingData(EmbeddingData):
|
||||
self.embedding_shape_list = [
|
||||
embedding_shape if i == part_idx else None for i in range(num_parts)
|
||||
]
|
||||
self.video_timestamps = [None] * num_parts
|
||||
self.second_per_grid_ts = [None] * num_parts
|
||||
for attr in self.video_meta_attrs:
|
||||
setattr(self, attr, [None] * num_parts)
|
||||
|
||||
self._set_part_grid(part_idx, modality, self.get_grid())
|
||||
if modality == Modality.VIDEO:
|
||||
@@ -274,7 +298,7 @@ class MultiModalEmbeddingData(EmbeddingData):
|
||||
|
||||
def _set_video_meta_for_part(self, part_idx, source):
|
||||
"""Copy video_timestamps and second_per_grid_ts from source (dict or object)."""
|
||||
for attr_name in _VIDEO_META_ATTRS:
|
||||
for attr_name in self.video_meta_attrs:
|
||||
val = (
|
||||
source.get(attr_name)
|
||||
if isinstance(source, dict)
|
||||
@@ -284,11 +308,15 @@ class MultiModalEmbeddingData(EmbeddingData):
|
||||
getattr(self, attr_name)[part_idx] = val
|
||||
|
||||
@classmethod
|
||||
def from_embedding_data(cls, embedding_data: EmbeddingData):
|
||||
def from_embedding_data(
|
||||
cls,
|
||||
embedding_data: EmbeddingData,
|
||||
model_type: Optional[str] = None,
|
||||
):
|
||||
"""Create MultiModalEmbeddingData from an EmbeddingData instance."""
|
||||
# Only forward known optional attrs (e.g. video metadata) so they land on the instance
|
||||
extra = {}
|
||||
for attr in _VIDEO_META_ATTRS:
|
||||
for attr in video_meta_attrs_for(model_type):
|
||||
val = getattr(embedding_data, attr, None)
|
||||
if val is not None:
|
||||
extra[attr] = val
|
||||
@@ -300,6 +328,7 @@ class MultiModalEmbeddingData(EmbeddingData):
|
||||
modality=embedding_data.modality,
|
||||
embedding=embedding_data.embedding,
|
||||
embedding_shape=embedding_data.shape,
|
||||
model_type=model_type,
|
||||
**extra,
|
||||
)
|
||||
mm_data.send_time = embedding_data.send_time
|
||||
@@ -313,11 +342,8 @@ class MultiModalEmbeddingData(EmbeddingData):
|
||||
groups = defaultdict(list)
|
||||
for i, e in enumerate(self.embedding_list):
|
||||
if e is not None:
|
||||
groups[self.modality_list[i]].append(e.cuda())
|
||||
return {
|
||||
mod: torch.concat(tensors).to("cpu", non_blocking=True)
|
||||
for mod, tensors in groups.items()
|
||||
}
|
||||
groups[self.modality_list[i]].append(e)
|
||||
return {mod: torch.cat(tensors, dim=0) for mod, tensors in groups.items()}
|
||||
return self.embedding_list
|
||||
|
||||
@property
|
||||
@@ -333,12 +359,16 @@ class MultiModalEmbeddingData(EmbeddingData):
|
||||
self.audio_feature_lens, flatten_items=True
|
||||
),
|
||||
}
|
||||
for attr in _VIDEO_META_ATTRS:
|
||||
for attr in self.video_meta_attrs:
|
||||
lst = getattr(self, attr, None)
|
||||
if not lst:
|
||||
continue
|
||||
valid = [a for a in lst if a is not None]
|
||||
if valid:
|
||||
if not valid:
|
||||
continue
|
||||
if attr in _VIDEO_META_TENSOR_ATTRS:
|
||||
kwargs[attr] = torch.cat(valid, dim=0)
|
||||
else:
|
||||
kwargs[attr] = list(itertools.chain(*valid))
|
||||
return kwargs
|
||||
|
||||
@@ -546,7 +576,7 @@ class WaitingImageRequest:
|
||||
|
||||
if self.recv_embedding_data is None:
|
||||
self.recv_embedding_data = MultiModalEmbeddingData.from_embedding_data(
|
||||
recv_obj
|
||||
recv_obj, model_type=self.model_type
|
||||
)
|
||||
else:
|
||||
self.recv_embedding_data.add(recv_obj)
|
||||
@@ -634,7 +664,13 @@ class MMReceiverBase(ABC):
|
||||
self.context = zmq.asyncio.Context(20)
|
||||
self.encoder_transfer_backend = server_args.encoder_transfer_backend
|
||||
self.encode_urls = server_args.encoder_urls
|
||||
self.recv_timeout = envs.SGLANG_ENCODER_RECV_TIMEOUT.get()
|
||||
self.host = get_local_ip_auto(server_args.host)
|
||||
self.model_type = (
|
||||
getattr(hf_config, "model_type", "").lower()
|
||||
if hf_config is not None
|
||||
else None
|
||||
)
|
||||
if self.encoder_transfer_backend == "mooncake":
|
||||
self.dtype = dtype
|
||||
self.embeddings_engine = get_mooncake_transfer_engine()
|
||||
@@ -724,15 +760,25 @@ class MMReceiverBase(ABC):
|
||||
self.context, zmq.PULL, host=self.host
|
||||
)
|
||||
mm_data = self._extract_url_data(request_obj)
|
||||
modalities = [m.get("modality") for m in mm_data]
|
||||
logger.info(
|
||||
f"[{req_id}] Sending encode request to E, "
|
||||
f"modalities={modalities}, num_items={len(mm_data)}"
|
||||
)
|
||||
send_time = time.monotonic()
|
||||
asyncio.create_task(
|
||||
self.encode(req_id, mm_data, embedding_port, "encode", "send")
|
||||
)
|
||||
return await asyncio.wait_for(
|
||||
result = await asyncio.wait_for(
|
||||
self._recv_mm_data(req_id, recv_socket, mm_processor, prompt),
|
||||
timeout=20,
|
||||
timeout=self.recv_timeout,
|
||||
)
|
||||
elapsed = time.monotonic() - send_time
|
||||
logger.info(f"[{req_id}] Received embedding from E in {elapsed:.3f}s")
|
||||
return result
|
||||
except asyncio.TimeoutError:
|
||||
logger.warning(f"Embedding recv timeout for request {req_id}")
|
||||
elapsed = time.monotonic() - send_time
|
||||
logger.warning(f"[{req_id}] Embedding recv timeout after {elapsed:.3f}s")
|
||||
if req_id is not None:
|
||||
self._cleanup_mooncake_buffer(req_id)
|
||||
return None
|
||||
@@ -797,7 +843,7 @@ class MMReceiverBase(ABC):
|
||||
)
|
||||
if recv_embedding_data is None:
|
||||
recv_embedding_data = MultiModalEmbeddingData.from_embedding_data(
|
||||
recv_obj
|
||||
recv_obj, model_type=self.model_type
|
||||
)
|
||||
else:
|
||||
recv_embedding_data.add(recv_obj)
|
||||
|
||||
@@ -24,7 +24,10 @@ from sglang.srt.configs.device_config import DeviceConfig
|
||||
from sglang.srt.configs.load_config import LoadConfig
|
||||
from sglang.srt.configs.model_config import ModelConfig
|
||||
from sglang.srt.constants import HEALTH_CHECK_RID_PREFIX
|
||||
from sglang.srt.disaggregation.encode_receiver import EmbeddingData
|
||||
from sglang.srt.disaggregation.encode_receiver import (
|
||||
EmbeddingData,
|
||||
video_meta_attrs_for,
|
||||
)
|
||||
from sglang.srt.distributed.parallel_state import (
|
||||
get_default_distributed_backend,
|
||||
get_mooncake_transfer_engine,
|
||||
@@ -45,6 +48,7 @@ from sglang.srt.server_args import (
|
||||
set_global_server_args_for_scheduler,
|
||||
)
|
||||
from sglang.srt.utils import (
|
||||
configure_logger,
|
||||
load_audio,
|
||||
load_image,
|
||||
load_video,
|
||||
@@ -170,15 +174,9 @@ def _get_mm_feature(mm_inputs, modality):
|
||||
)
|
||||
|
||||
|
||||
def _build_mm_aux_data(mm_inputs):
|
||||
"""
|
||||
Build auxiliary data for video modality.
|
||||
"""
|
||||
aux_data = {
|
||||
"video_timestamps": mm_inputs.get("video_timestamps", None),
|
||||
"second_per_grid_ts": mm_inputs.get("second_per_grid_ts", None),
|
||||
}
|
||||
return aux_data
|
||||
def _build_mm_aux_data(mm_inputs, model_type=None):
|
||||
# Video aux metadata, scoped to model_type's video-meta attrs.
|
||||
return {attr: mm_inputs.get(attr) for attr in video_meta_attrs_for(model_type)}
|
||||
|
||||
|
||||
class MMEncoder:
|
||||
@@ -832,7 +830,7 @@ class MMEncoder:
|
||||
self.background_tasks.add(task)
|
||||
task.add_done_callback(self.background_tasks.discard)
|
||||
|
||||
aux_data = _build_mm_aux_data(mm_inputs)
|
||||
aux_data = _build_mm_aux_data(mm_inputs, self.model_type)
|
||||
self.embedding_to_send[req_id] = EmbeddingData(
|
||||
req_id,
|
||||
num_parts,
|
||||
@@ -950,104 +948,117 @@ class MMEncoder:
|
||||
return normalized
|
||||
|
||||
async def _process_mm_items(self, mm_items, modality):
|
||||
if modality == Modality.IMAGE and self.image_processor:
|
||||
images = await self._flatten_and_load_images(mm_items)
|
||||
image_config = self.vision_config.get("image", {})
|
||||
if self.model_type in ["kimi_k25", "kimi_vl"]:
|
||||
images = self._normalize_kimi_encoder_images(images)
|
||||
processor_input = self.image_processor(images=images, **image_config)
|
||||
if hasattr(self.model, "thinker"): # for omni models
|
||||
get_feature_method = self.model.thinker.get_image_feature
|
||||
else:
|
||||
get_feature_method = self.model.get_image_feature
|
||||
elif modality == Modality.VIDEO and self.video_processor:
|
||||
videos, video_processor_kwargs = await self._flatten_and_load_videos(
|
||||
mm_items
|
||||
)
|
||||
processor_input = self.video_processor(
|
||||
videos=videos, **video_processor_kwargs
|
||||
)
|
||||
# Get additional video metadata
|
||||
if (
|
||||
self.model_type
|
||||
in [
|
||||
"qwen3_vl",
|
||||
"qwen3_vl_moe",
|
||||
"qwen3_5",
|
||||
"qwen3_5_moe",
|
||||
"intern_s2_preview",
|
||||
]
|
||||
and video_processor_kwargs.get("video_metadata", None) is not None
|
||||
):
|
||||
# For qwen3-vl/qwen3.5 models, we need to store the video timestamps
|
||||
video_metadata = video_processor_kwargs["video_metadata"]
|
||||
try:
|
||||
merge_size = (
|
||||
self.model_config.hf_config.vision_config.spatial_merge_size
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
merge_size = 2 # Default merge_size
|
||||
model_preprocessor = getattr(self.model, "preprocess_mm_for_encoder", None)
|
||||
|
||||
video_timestamps = []
|
||||
for metadata in video_metadata:
|
||||
video_fps = metadata.get("fps", None) or 24 # original video fps
|
||||
frames_indices = metadata.get("frames_indices", None)
|
||||
timestamps = self._calculate_timestamps(
|
||||
frames_indices, video_fps, merge_size
|
||||
)
|
||||
video_timestamps.append(timestamps)
|
||||
processor_input["video_timestamps"] = video_timestamps
|
||||
elif (
|
||||
self.model_type in ["qwen2_5_vl", "qwen2_5_omni", "qwen3_omni_moe"]
|
||||
and processor_input.get("video_grid_thw", None) is not None
|
||||
):
|
||||
# For omni/qwen2_5_vl models, calculate second_per_grid_ts for rotary embedding
|
||||
video_grid_thw = processor_input["video_grid_thw"]
|
||||
try:
|
||||
temporal_patch_size = self.video_processor.temporal_patch_size
|
||||
except AttributeError:
|
||||
temporal_patch_size = 2 # Default temporal_patch_size
|
||||
# get sampled fps, default: 2
|
||||
fps_list = [
|
||||
self.vision_config.get("video", {}).get("fps", None) or 2
|
||||
] * len(video_grid_thw)
|
||||
second_per_grid_ts = [(temporal_patch_size / fps) for fps in fps_list]
|
||||
second_per_grid_ts_tensor = torch.tensor(
|
||||
second_per_grid_ts, dtype=torch.float32
|
||||
)
|
||||
processor_input["second_per_grid_ts"] = second_per_grid_ts_tensor
|
||||
|
||||
if hasattr(self.model, "thinker"): # for omni models
|
||||
get_feature_method = self.model.thinker.get_video_feature
|
||||
else:
|
||||
get_feature_method = self.model.get_video_feature
|
||||
elif modality == Modality.AUDIO and self.audio_processor:
|
||||
audios = await self._flatten_and_load_audios(mm_items)
|
||||
audio_config = self.vision_config.get("audio", {})
|
||||
processor_input = self.audio_processor.feature_extractor(
|
||||
audios, **audio_config
|
||||
if modality == Modality.IMAGE:
|
||||
processor_input = await self._process_image_items(
|
||||
mm_items, model_preprocessor
|
||||
)
|
||||
processor_input["feature_attention_mask"] = processor_input.pop(
|
||||
"attention_mask"
|
||||
elif modality == Modality.VIDEO:
|
||||
processor_input = await self._process_video_items(
|
||||
mm_items, model_preprocessor
|
||||
)
|
||||
# convert to same format as image/video
|
||||
input_lengths = torch.tensor(
|
||||
processor_input["feature_attention_mask"].sum(-1), dtype=torch.long
|
||||
elif modality == Modality.AUDIO:
|
||||
processor_input = await self._process_audio_items(
|
||||
mm_items, model_preprocessor
|
||||
)
|
||||
processor_input["audio_feature_lens_raw"] = input_lengths
|
||||
output_lengths = self._get_feat_extract_output_lengths(input_lengths)
|
||||
processor_input["audio_feature_lens"] = output_lengths
|
||||
if hasattr(self.model, "thinker"): # for omni models
|
||||
get_feature_method = self.model.thinker.get_audio_feature
|
||||
else:
|
||||
get_feature_method = self.model.get_audio_feature
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Currently only support image, video and audio modalities, {modality} modality has no processor available."
|
||||
)
|
||||
raise ValueError(f"Unsupported modality: {modality}")
|
||||
|
||||
target = self.model.thinker if hasattr(self.model, "thinker") else self.model
|
||||
get_feature_method = getattr(target, f"get_{modality.name.lower()}_feature")
|
||||
return processor_input, get_feature_method
|
||||
|
||||
async def _process_image_items(self, mm_items, model_preprocessor):
|
||||
if not (self.image_processor or model_preprocessor):
|
||||
raise ValueError("No image processor available")
|
||||
images = await self._flatten_and_load_images(mm_items)
|
||||
if model_preprocessor:
|
||||
return model_preprocessor(images, Modality.IMAGE, self.vision_config)
|
||||
image_config = self.vision_config.get("image", {})
|
||||
if self.model_type in ["kimi_k25", "kimi_vl"]:
|
||||
images = self._normalize_kimi_encoder_images(images)
|
||||
return self.image_processor(images=images, **image_config)
|
||||
|
||||
async def _process_video_items(self, mm_items, model_preprocessor):
|
||||
if model_preprocessor:
|
||||
return model_preprocessor(mm_items, Modality.VIDEO, self.vision_config)
|
||||
if not self.video_processor:
|
||||
raise ValueError("No video processor available")
|
||||
|
||||
videos, video_processor_kwargs = await self._flatten_and_load_videos(mm_items)
|
||||
processor_input = self.video_processor(videos=videos, **video_processor_kwargs)
|
||||
|
||||
# Get additional video metadata
|
||||
if (
|
||||
self.model_type
|
||||
in [
|
||||
"qwen3_vl",
|
||||
"qwen3_vl_moe",
|
||||
"qwen3_5",
|
||||
"qwen3_5_moe",
|
||||
"intern_s2_preview",
|
||||
]
|
||||
and video_processor_kwargs.get("video_metadata", None) is not None
|
||||
):
|
||||
video_metadata = video_processor_kwargs["video_metadata"]
|
||||
try:
|
||||
merge_size = (
|
||||
self.model_config.hf_config.vision_config.spatial_merge_size
|
||||
)
|
||||
except (AttributeError, KeyError):
|
||||
merge_size = 2 # Default merge_size
|
||||
|
||||
video_timestamps = []
|
||||
for metadata in video_metadata:
|
||||
video_fps = metadata.get("fps", None) or 24 # original video fps
|
||||
frames_indices = metadata.get("frames_indices", None)
|
||||
timestamps = self._calculate_timestamps(
|
||||
frames_indices, video_fps, merge_size
|
||||
)
|
||||
video_timestamps.append(timestamps)
|
||||
processor_input["video_timestamps"] = video_timestamps
|
||||
elif (
|
||||
self.model_type in ["qwen2_5_vl", "qwen2_5_omni", "qwen3_omni_moe"]
|
||||
and processor_input.get("video_grid_thw", None) is not None
|
||||
):
|
||||
video_grid_thw = processor_input["video_grid_thw"]
|
||||
try:
|
||||
temporal_patch_size = self.video_processor.temporal_patch_size
|
||||
except AttributeError:
|
||||
temporal_patch_size = 2 # Default temporal_patch_size
|
||||
fps_list = [
|
||||
self.vision_config.get("video", {}).get("fps", None) or 2
|
||||
] * len(video_grid_thw)
|
||||
second_per_grid_ts = [(temporal_patch_size / fps) for fps in fps_list]
|
||||
second_per_grid_ts_tensor = torch.tensor(
|
||||
second_per_grid_ts, dtype=torch.float32
|
||||
)
|
||||
processor_input["second_per_grid_ts"] = second_per_grid_ts_tensor
|
||||
|
||||
return processor_input
|
||||
|
||||
async def _process_audio_items(self, mm_items, model_preprocessor):
|
||||
if model_preprocessor:
|
||||
return model_preprocessor(mm_items, Modality.AUDIO, self.vision_config)
|
||||
if not self.audio_processor:
|
||||
raise ValueError("No audio processor available")
|
||||
|
||||
audios = await self._flatten_and_load_audios(mm_items)
|
||||
audio_config = self.vision_config.get("audio", {})
|
||||
processor_input = self.audio_processor.feature_extractor(audios, **audio_config)
|
||||
processor_input["feature_attention_mask"] = processor_input.pop(
|
||||
"attention_mask"
|
||||
)
|
||||
# convert to same format as image/video
|
||||
input_lengths = torch.tensor(
|
||||
processor_input["feature_attention_mask"].sum(-1), dtype=torch.long
|
||||
)
|
||||
processor_input["audio_feature_lens_raw"] = input_lengths
|
||||
output_lengths = self._get_feat_extract_output_lengths(input_lengths)
|
||||
processor_input["audio_feature_lens"] = output_lengths
|
||||
return processor_input
|
||||
|
||||
async def _encode(self, mm_items, modality: Modality) -> torch.Tensor:
|
||||
try:
|
||||
mm_inputs, get_feature_fn = await self._process_mm_items(mm_items, modality)
|
||||
@@ -1092,7 +1103,23 @@ class MMEncoder:
|
||||
if self.profiler is not None:
|
||||
self.profiler.step()
|
||||
|
||||
aux_data = _build_mm_aux_data(mm_inputs)
|
||||
aux_data = _build_mm_aux_data(mm_inputs, self.model_type)
|
||||
|
||||
if modality == Modality.VIDEO and mm_inputs.get("video_audio_features"):
|
||||
target = (
|
||||
self.model.thinker if hasattr(self.model, "thinker") else self.model
|
||||
)
|
||||
encode_video_audio_fn = getattr(target, "encode_video_audio", None)
|
||||
if encode_video_audio_fn is not None:
|
||||
audio_embedding = encode_video_audio_fn(mm_inputs)
|
||||
if audio_embedding is not None:
|
||||
aux_data["video_audio_embedding"] = audio_embedding
|
||||
else:
|
||||
logger.warning(
|
||||
"Videos carry audio tracks but model has no "
|
||||
"encode_video_audio; dropping audio for EPD encoding."
|
||||
)
|
||||
|
||||
return (
|
||||
_get_mm_grid_dim(mm_inputs, modality, self.model_type),
|
||||
mm_embedding,
|
||||
@@ -1417,6 +1444,7 @@ def launch_encoder(server_args, schedule_path, dist_init_method, rank):
|
||||
|
||||
|
||||
def launch_server(server_args: ServerArgs):
|
||||
configure_logger(server_args, prefix=" encode_server")
|
||||
global encoder
|
||||
ctx = mp.get_context("spawn")
|
||||
zmq_ctx = zmq.Context(10)
|
||||
@@ -1453,6 +1481,7 @@ async def get_condition(rid):
|
||||
@app.post("/encode")
|
||||
async def handle_encode_request(request: dict):
|
||||
req_id = request["req_id"]
|
||||
start_time = time.monotonic()
|
||||
try:
|
||||
|
||||
def start_background_send(req_id):
|
||||
@@ -1538,6 +1567,11 @@ async def handle_encode_request(request: dict):
|
||||
embedding_port=request["embedding_port"],
|
||||
)
|
||||
encoder.embedding_to_send.pop(request["req_id"], None)
|
||||
elapsed = time.monotonic() - start_time
|
||||
logger.info(
|
||||
f"[{req_id}] /encode completed in {elapsed:.3f}s, "
|
||||
f"modality={request['modality']}, tokens={embedding_len}"
|
||||
)
|
||||
return ORJSONResponse(content=None)
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
|
||||
@@ -1598,6 +1598,19 @@ def _get_is_default_transport():
|
||||
return _is_default_tensor_transport
|
||||
|
||||
|
||||
def _wrap_tensor_or_list(value):
|
||||
"""Wrap a CPU tensor (or list of CPU tensors) in ShmPointerMMData."""
|
||||
if isinstance(value, torch.Tensor) and value.is_cpu:
|
||||
return ShmPointerMMData(value)
|
||||
elif isinstance(value, (list, tuple)):
|
||||
wrapped = [
|
||||
(ShmPointerMMData(t) if isinstance(t, torch.Tensor) and t.is_cpu else t)
|
||||
for t in value
|
||||
]
|
||||
return type(value)(wrapped) if isinstance(value, tuple) else wrapped
|
||||
return value
|
||||
|
||||
|
||||
def wrap_shm_features(obj):
|
||||
"""
|
||||
Scan the object for multimodal tensors and wrap them in SHM pointers.
|
||||
@@ -1607,22 +1620,14 @@ def wrap_shm_features(obj):
|
||||
|
||||
if hasattr(obj, "mm_inputs") and obj.mm_inputs:
|
||||
for item in obj.mm_inputs.mm_items:
|
||||
if not hasattr(item, "feature"):
|
||||
continue
|
||||
feat = item.feature
|
||||
if isinstance(feat, torch.Tensor) and feat.is_cpu:
|
||||
item.feature = ShmPointerMMData(feat)
|
||||
elif isinstance(feat, (list, tuple)):
|
||||
wrapped = [
|
||||
(
|
||||
ShmPointerMMData(t)
|
||||
if isinstance(t, torch.Tensor) and t.is_cpu
|
||||
else t
|
||||
)
|
||||
for t in feat
|
||||
]
|
||||
item.feature = (
|
||||
type(feat)(wrapped) if isinstance(feat, tuple) else wrapped
|
||||
if hasattr(item, "feature") and item.feature is not None:
|
||||
item.feature = _wrap_tensor_or_list(item.feature)
|
||||
if (
|
||||
hasattr(item, "precomputed_embeddings")
|
||||
and item.precomputed_embeddings is not None
|
||||
):
|
||||
item.precomputed_embeddings = _wrap_tensor_or_list(
|
||||
item.precomputed_embeddings
|
||||
)
|
||||
return obj
|
||||
|
||||
@@ -1646,9 +1651,23 @@ def has_shm_features(recv_reqs):
|
||||
for item in req.mm_inputs.mm_items:
|
||||
if _feature_has_shm(item.feature):
|
||||
return True
|
||||
if _feature_has_shm(getattr(item, "precomputed_embeddings", None)):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _unwrap_tensor_or_list(value):
|
||||
"""Restore ShmPointerMMData wrappers back into standard torch.Tensors."""
|
||||
if isinstance(value, ShmPointerMMData):
|
||||
return value.materialize()
|
||||
elif isinstance(value, (list, tuple)):
|
||||
unwrapped = [
|
||||
t.materialize() if isinstance(t, ShmPointerMMData) else t for t in value
|
||||
]
|
||||
return type(value)(unwrapped) if isinstance(value, tuple) else unwrapped
|
||||
return value
|
||||
|
||||
|
||||
def unwrap_shm_features(obj):
|
||||
"""
|
||||
Restore ShmPointerMMData wrappers back into standard torch.Tensors.
|
||||
@@ -1663,17 +1682,14 @@ def unwrap_shm_features(obj):
|
||||
return obj
|
||||
# Handle single requests
|
||||
if hasattr(obj, "mm_inputs") and obj.mm_inputs:
|
||||
mm_items = obj.mm_inputs.mm_items
|
||||
for item in mm_items:
|
||||
feat = item.feature
|
||||
if isinstance(feat, ShmPointerMMData):
|
||||
item.feature = feat.materialize()
|
||||
elif isinstance(feat, (list, tuple)):
|
||||
unwrapped = [
|
||||
t.materialize() if isinstance(t, ShmPointerMMData) else t
|
||||
for t in feat
|
||||
]
|
||||
item.feature = (
|
||||
type(feat)(unwrapped) if isinstance(feat, tuple) else unwrapped
|
||||
for item in obj.mm_inputs.mm_items:
|
||||
if hasattr(item, "feature") and item.feature is not None:
|
||||
item.feature = _unwrap_tensor_or_list(item.feature)
|
||||
if (
|
||||
hasattr(item, "precomputed_embeddings")
|
||||
and item.precomputed_embeddings is not None
|
||||
):
|
||||
item.precomputed_embeddings = _unwrap_tensor_or_list(
|
||||
item.precomputed_embeddings
|
||||
)
|
||||
return obj
|
||||
|
||||
@@ -779,6 +779,11 @@ class TokenizerManager(TokenizerControlMixin, TokenizerManagerScoreMixin):
|
||||
need_wait_for_mm_inputs=obj.need_wait_for_mm_inputs,
|
||||
)
|
||||
if mm_inputs is None:
|
||||
if self.server_args.language_only:
|
||||
logger.warning(
|
||||
"Encoder embedding not available, "
|
||||
"falling back to local mm processing"
|
||||
)
|
||||
mm_inputs = await self.mm_processor.process_mm_data_async(
|
||||
image_data=obj.image_data,
|
||||
audio_data=obj.audio_data,
|
||||
|
||||
@@ -68,7 +68,11 @@ from sglang.srt.managers.mm_utils import (
|
||||
MultiModalityDataPaddingPatternMultimodalTokens,
|
||||
general_mm_embed_routine,
|
||||
)
|
||||
from sglang.srt.managers.schedule_batch import MultimodalDataItem, MultimodalInputs
|
||||
from sglang.srt.managers.schedule_batch import (
|
||||
Modality,
|
||||
MultimodalDataItem,
|
||||
MultimodalInputs,
|
||||
)
|
||||
from sglang.srt.model_executor.forward_batch_info import ForwardBatch, PPProxyTensors
|
||||
from sglang.srt.model_loader.weight_utils import (
|
||||
default_weight_loader,
|
||||
@@ -1007,6 +1011,11 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
"up_proj": ("gate_up_proj", 1),
|
||||
}
|
||||
|
||||
# Prefixes for weight routing in encoder_only/language_only modes
|
||||
_LANGUAGE_WEIGHT_PREFIXES = ("model.", "lm_head.")
|
||||
_VISION_AUDIO_WEIGHT_PREFIXES = ("visual.", "vision_model.", "audio_")
|
||||
_VISION_AUDIO_WEIGHT_SUBSTRING = "speech_embeddings"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: MiMoV2Config,
|
||||
@@ -1017,27 +1026,36 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
self.pp_group = get_pp_group()
|
||||
self.config = config
|
||||
self.quant_config = quant_config
|
||||
self.model = MiMoV2Model(
|
||||
config, quant_config=quant_config, prefix=add_prefix("model", prefix)
|
||||
)
|
||||
self._encoder_processor = None # lazy-created in preprocess_mm_for_encoder
|
||||
|
||||
if self.pp_group.is_last_rank:
|
||||
self.lm_head = ParallelLMHead(
|
||||
config.vocab_size,
|
||||
config.hidden_size,
|
||||
quant_config=quant_config,
|
||||
prefix=add_prefix("lm_head", prefix),
|
||||
use_attn_tp_group=get_global_server_args().enable_dp_lm_head,
|
||||
if not self.config.encoder_only:
|
||||
self.model = MiMoV2Model(
|
||||
config, quant_config=quant_config, prefix=add_prefix("model", prefix)
|
||||
)
|
||||
else:
|
||||
# ranks other than the last rank will have a placeholder layer
|
||||
self.lm_head = PPMissingLayer()
|
||||
|
||||
self.logits_processor = LogitsProcessor(config)
|
||||
if self.pp_group.is_last_rank:
|
||||
self.lm_head = ParallelLMHead(
|
||||
config.vocab_size,
|
||||
config.hidden_size,
|
||||
quant_config=quant_config,
|
||||
prefix=add_prefix("lm_head", prefix),
|
||||
use_attn_tp_group=get_global_server_args().enable_dp_lm_head,
|
||||
)
|
||||
else:
|
||||
self.lm_head = PPMissingLayer()
|
||||
else:
|
||||
self.model = None
|
||||
self.lm_head = None
|
||||
|
||||
self.logits_processor = (
|
||||
LogitsProcessor(config) if not self.config.encoder_only else None
|
||||
)
|
||||
|
||||
vision_config = getattr(config, "vision_config", None)
|
||||
audio_config = getattr(config, "audio_config", None)
|
||||
self._is_multimodal = vision_config is not None and audio_config is not None
|
||||
# Always build vision/audio encoders so P can fall back to local
|
||||
# encoding when the EPD encoder is unreachable.
|
||||
if self._is_multimodal:
|
||||
if hasattr(vision_config, "to_dict"):
|
||||
vision_config = vision_config.to_dict()
|
||||
@@ -1054,11 +1072,15 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
self.audio_encoder = MiMoAudioEncoder(self.audio_config)
|
||||
|
||||
self._routed_experts_weights_of_layer = LazyValue(
|
||||
lambda: {
|
||||
layer_id: layer.mlp.get_moe_weights()
|
||||
for layer_id, layer in enumerate(self.model.layers)
|
||||
if isinstance(layer.mlp, MiMoV2MoE)
|
||||
}
|
||||
lambda: (
|
||||
{
|
||||
layer_id: layer.mlp.get_moe_weights()
|
||||
for layer_id, layer in enumerate(self.model.layers)
|
||||
if isinstance(layer.mlp, MiMoV2MoE)
|
||||
}
|
||||
if self.model is not None
|
||||
else {}
|
||||
)
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -1066,12 +1088,24 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
return self._routed_experts_weights_of_layer.value
|
||||
|
||||
def get_input_embedding(self, input_ids: torch.Tensor) -> torch.Tensor:
|
||||
assert (
|
||||
self.model is not None
|
||||
), "get_input_embedding() is not available in encoder_only mode"
|
||||
return self.model.get_input_embedding(input_ids)
|
||||
|
||||
def pad_input_ids(self, input_ids: List[int], mm_inputs: MultimodalInputs):
|
||||
pattern = MultiModalityDataPaddingPatternMultimodalTokens()
|
||||
return pattern.pad_input_tokens(input_ids, mm_inputs)
|
||||
|
||||
def preprocess_mm_for_encoder(self, mm_data, modality, config):
|
||||
if self._encoder_processor is None:
|
||||
from sglang.srt.multimodal.processors.mimo_v2 import MiMoProcessor
|
||||
|
||||
self._encoder_processor = MiMoProcessor.from_hf_config(
|
||||
self.config, mm_config=config
|
||||
)
|
||||
return self._encoder_processor.preprocess_for_encoder(mm_data, modality)
|
||||
|
||||
def get_image_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
|
||||
pixel_values = torch.cat([item.feature for item in items], dim=0).type(
|
||||
self.visual.dtype
|
||||
@@ -1093,8 +1127,76 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
def get_audio_feature(self, items: List[MultimodalDataItem]) -> torch.Tensor:
|
||||
return self.audio_encoder.get_audio_feature(items)
|
||||
|
||||
def get_input_embeddings(self) -> nn.Embedding:
|
||||
return self.model.embed_tokens
|
||||
@torch.inference_mode()
|
||||
def encode_video_audio(self, mm_inputs: Dict) -> Optional[torch.Tensor]:
|
||||
# EPD-side hook: encode audio tracks pulled from videos and trim to the
|
||||
# interleaved per-video segments produced by MiMoProcessor (segment
|
||||
# starts / lens / per_video_num_units). Returns None if there is no
|
||||
# audio to encode. The server passes the result through to the receiver
|
||||
# under aux_data["video_audio_embedding"].
|
||||
import numpy as np
|
||||
|
||||
audio_features = mm_inputs.get("video_audio_features")
|
||||
if not audio_features:
|
||||
return None
|
||||
|
||||
def _as_tensor(data):
|
||||
if isinstance(data, torch.Tensor):
|
||||
return data
|
||||
if isinstance(data, np.ndarray):
|
||||
return torch.tensor(data)
|
||||
if isinstance(data, list) and data and isinstance(data[0], np.ndarray):
|
||||
return torch.tensor(np.array(data))
|
||||
if isinstance(data, list) and data and isinstance(data[0], (int, float)):
|
||||
return torch.tensor(data)
|
||||
return data
|
||||
|
||||
audio_feature_lens = mm_inputs["video_audio_feature_lens"]
|
||||
audio_item = MultimodalDataItem.from_dict(
|
||||
{
|
||||
"modality": Modality.AUDIO,
|
||||
"feature": _as_tensor(audio_features),
|
||||
}
|
||||
)
|
||||
audio_item.set("audio_feature_lens", _as_tensor(audio_feature_lens))
|
||||
|
||||
audio_embedding = self.get_audio_feature([audio_item]).cpu()
|
||||
if audio_embedding.ndim != 2:
|
||||
audio_embedding = audio_embedding.reshape(-1, audio_embedding.shape[-1])
|
||||
|
||||
segment_lens_flat = mm_inputs["video_audio_segment_lens_flat"]
|
||||
segment_starts_flat = mm_inputs["video_audio_segment_starts_flat"]
|
||||
per_video_num_units = mm_inputs["video_audio_per_video_num_units"]
|
||||
per_video_audio_token_lens = (
|
||||
audio_feature_lens.tolist()
|
||||
if hasattr(audio_feature_lens, "tolist")
|
||||
else list(audio_feature_lens)
|
||||
)
|
||||
|
||||
trimmed_chunks = []
|
||||
emb_offset = 0
|
||||
unit_idx = 0
|
||||
audio_video_idx = 0
|
||||
for num_units in per_video_num_units:
|
||||
if num_units <= 0:
|
||||
continue
|
||||
vid_audio_len = per_video_audio_token_lens[audio_video_idx]
|
||||
for _ in range(num_units):
|
||||
start = segment_starts_flat[unit_idx]
|
||||
seg_len = segment_lens_flat[unit_idx]
|
||||
trimmed_chunks.append(
|
||||
audio_embedding[emb_offset + start : emb_offset + start + seg_len]
|
||||
)
|
||||
unit_idx += 1
|
||||
emb_offset += vid_audio_len
|
||||
audio_video_idx += 1
|
||||
|
||||
return (
|
||||
torch.cat(trimmed_chunks, dim=0) if trimmed_chunks else audio_embedding[:0]
|
||||
)
|
||||
|
||||
def get_input_embeddings(self) -> Optional[nn.Embedding]:
|
||||
return self.model.embed_tokens if self.model is not None else None
|
||||
|
||||
@torch.no_grad()
|
||||
def forward(
|
||||
@@ -1105,6 +1207,10 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
input_embeds: torch.Tensor = None,
|
||||
pp_proxy_tensors: Optional[PPProxyTensors] = None,
|
||||
) -> torch.Tensor:
|
||||
assert (
|
||||
not self.config.encoder_only
|
||||
), "forward() should not be called in encoder_only mode"
|
||||
|
||||
if self._is_multimodal:
|
||||
hidden_states, hidden_states_before_norm = general_mm_embed_routine(
|
||||
input_ids=input_ids,
|
||||
@@ -1136,11 +1242,11 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
|
||||
@property
|
||||
def start_layer(self):
|
||||
return self.model.start_layer
|
||||
return self.model.start_layer if self.model is not None else 0
|
||||
|
||||
@property
|
||||
def end_layer(self):
|
||||
return self.model.end_layer
|
||||
return self.model.end_layer if self.model is not None else 0
|
||||
|
||||
def load_weights(self, weights: Iterable[Tuple[str, torch.Tensor]]):
|
||||
stacked_params_mapping = [
|
||||
@@ -1168,11 +1274,18 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
params_dict = dict(self.named_parameters())
|
||||
skipped_mtp_weights = False
|
||||
|
||||
def _is_vision_audio_weight(name):
|
||||
return (
|
||||
name.startswith(self._VISION_AUDIO_WEIGHT_PREFIXES)
|
||||
or self._VISION_AUDIO_WEIGHT_SUBSTRING in name
|
||||
)
|
||||
|
||||
for name, loaded_weight in weights:
|
||||
if not self._is_multimodal and (
|
||||
name.startswith(("visual.", "vision_model.", "audio_encoder."))
|
||||
or name.startswith("audio_")
|
||||
or "speech_embeddings" in name
|
||||
if not self._is_multimodal and _is_vision_audio_weight(name):
|
||||
continue
|
||||
|
||||
if self.config.encoder_only and name.startswith(
|
||||
self._LANGUAGE_WEIGHT_PREFIXES
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -1374,9 +1487,15 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
logger.warning(f"Parameter {name} not found in params_dict")
|
||||
|
||||
def get_embed_and_head(self):
|
||||
assert (
|
||||
self.model is not None and self.lm_head is not None
|
||||
), "get_embed_and_head() is not available in encoder_only mode"
|
||||
return self.model.embed_tokens.weight, self.lm_head.weight
|
||||
|
||||
def set_embed_and_head(self, embed, head):
|
||||
assert (
|
||||
self.model is not None and self.lm_head is not None
|
||||
), "set_embed_and_head() is not available in encoder_only mode"
|
||||
del self.model.embed_tokens.weight
|
||||
del self.lm_head.weight
|
||||
self.model.embed_tokens.weight = embed
|
||||
@@ -1385,7 +1504,8 @@ class MiMoV2ForCausalLM(nn.Module):
|
||||
torch.cuda.synchronize()
|
||||
|
||||
def load_kv_cache_scales(self, quantization_param_path: str) -> None:
|
||||
self.model.load_kv_cache_scales(quantization_param_path)
|
||||
if self.model is not None:
|
||||
self.model.load_kv_cache_scales(quantization_param_path)
|
||||
|
||||
@classmethod
|
||||
def get_model_config_for_expert_location(cls, config):
|
||||
|
||||
@@ -268,6 +268,49 @@ _QWEN2VL_PIXEL_STD = torch.Tensor([58.395, 57.12, 57.375]).view(-1, 1, 1)
|
||||
_mean_std_cache = {}
|
||||
|
||||
|
||||
def _decode_frames_and_timestamps(vdw, ele):
|
||||
# Shared E/D frame-sampling recipe: smart_nframes + linspace + permute.
|
||||
total_frames, video_fps = len(vdw), vdw.avg_fps
|
||||
nframes = smart_nframes(ele, total_frames=total_frames, video_fps=video_fps)
|
||||
idx = list(np.unique(np.linspace(0, total_frames - 1, num=nframes, dtype=np.int64)))
|
||||
video_tensor = vdw.get_frames_as_tensor(idx).permute(0, 3, 1, 2).float()
|
||||
timestamps = torch.as_tensor(idx, dtype=torch.float32) / video_fps
|
||||
return video_tensor, timestamps
|
||||
|
||||
|
||||
def _ffprobe_has_audio(src, stdin=None, label=None) -> bool:
|
||||
# Header-only audio-stream probe for HTTP URLs; avoids full download.
|
||||
try:
|
||||
r = subprocess.run(
|
||||
[
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_streams",
|
||||
"-select_streams",
|
||||
"a",
|
||||
src,
|
||||
],
|
||||
input=stdin,
|
||||
capture_output=True,
|
||||
timeout=30,
|
||||
)
|
||||
if r.returncode != 0:
|
||||
stderr = r.stderr.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"ffprobe failed for {label}: {stderr}")
|
||||
return bool(json.loads(r.stdout).get("streams"))
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("ffprobe timed out for %s", label)
|
||||
raise
|
||||
except FileNotFoundError as e:
|
||||
raise RuntimeError("ffprobe not found; install ffmpeg") from e
|
||||
except json.JSONDecodeError:
|
||||
logger.error("ffprobe returned invalid JSON for %s", label)
|
||||
raise
|
||||
|
||||
|
||||
class MiMoProcessor:
|
||||
def __init__(
|
||||
self,
|
||||
@@ -445,6 +488,207 @@ class MiMoProcessor:
|
||||
for k in kwargs:
|
||||
logger.info(f"[Warning] Ignored unknown parameter {k} for MiMoProcessor")
|
||||
|
||||
@classmethod
|
||||
def from_hf_config(cls, hf_config, mm_config=None, **overrides):
|
||||
# Params must come from hf_config.processor_config so E and D agree;
|
||||
# any drift shifts input_ids on the D side.
|
||||
def _as_dict(obj):
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
return obj.to_dict() if obj and hasattr(obj, "to_dict") else {}
|
||||
|
||||
pc = _as_dict(getattr(hf_config, "processor_config", None))
|
||||
ac = _as_dict(getattr(hf_config, "audio_config", None))
|
||||
vc = hf_config.vision_config
|
||||
vget = vc.get if isinstance(vc, dict) else (lambda k, d=None: getattr(vc, k, d))
|
||||
patch_size = vget("patch_size", 14)
|
||||
merge_size = vget("spatial_merge_size", 2)
|
||||
f = patch_size * merge_size
|
||||
|
||||
kwargs = {
|
||||
"tokenizer": None,
|
||||
"patch_size": patch_size,
|
||||
"merge_size": merge_size,
|
||||
"temporal_patch_size": vget("temporal_patch_size", 2),
|
||||
"image_min_pixels": pc.get("image_min_pixels") or 4 * f * f,
|
||||
"image_max_pixels": pc.get("image_max_pixels") or 4096 * f * f,
|
||||
"video_min_pixels": pc.get("video_min_pixels") or 4 * f * f,
|
||||
"video_max_pixels": pc.get("video_max_pixels") or 4096 * f * f,
|
||||
"video_total_max_pixels": pc.get("video_total_max_pixels") or 16384 * f * f,
|
||||
"fps": pc.get("fps") or 2,
|
||||
"num_frames": pc.get("num_frames"),
|
||||
"max_frames": pc.get("max_frames") or 256,
|
||||
"min_frames": pc.get("min_frames") or 8,
|
||||
"video_audio_interleave_length": pc.get("video_audio_interleave_length", 0),
|
||||
"use_per_grid_t_timestamps": pc.get("use_per_grid_t_timestamps", False),
|
||||
"use_video_timestamps": pc.get("use_video_timestamps", False),
|
||||
}
|
||||
# audio_sampling_rate: processor_config > audio_config > mm_config.audio.
|
||||
asr = (
|
||||
pc.get("audio_sampling_rate")
|
||||
or ac.get("sampling_rate")
|
||||
or ac.get("sample_rate")
|
||||
)
|
||||
if asr is not None:
|
||||
kwargs["audio_sampling_rate"] = asr
|
||||
|
||||
audio_cfg = (mm_config or {}).get("audio", {})
|
||||
for k in (
|
||||
"audio_sampling_rate",
|
||||
"audio_hop_length",
|
||||
"audio_n_mels",
|
||||
"audio_kernel_size",
|
||||
"audio_stride_size",
|
||||
"audio_avg_pooler",
|
||||
):
|
||||
if k in audio_cfg:
|
||||
kwargs[k] = audio_cfg[k]
|
||||
if "sampling_rate" in audio_cfg and "audio_sampling_rate" not in kwargs:
|
||||
kwargs["audio_sampling_rate"] = audio_cfg["sampling_rate"]
|
||||
|
||||
kwargs.update(overrides)
|
||||
return cls(**kwargs)
|
||||
|
||||
@staticmethod
|
||||
def has_audio_track(path_or_data) -> bool:
|
||||
# In-process probe via torchcodec for bytes/path; ffprobe range
|
||||
# request for HTTP URLs so we do not pre-download the blob here.
|
||||
if isinstance(path_or_data, str) and path_or_data.startswith(
|
||||
("http://", "https://")
|
||||
):
|
||||
return _ffprobe_has_audio(path_or_data, stdin=None, label=path_or_data)
|
||||
|
||||
if isinstance(path_or_data, bytes):
|
||||
source = io.BytesIO(path_or_data)
|
||||
elif (
|
||||
isinstance(path_or_data, str)
|
||||
and path_or_data.startswith("data:")
|
||||
and ";base64," in path_or_data
|
||||
):
|
||||
source = io.BytesIO(base64.b64decode(path_or_data.split(";base64,")[1]))
|
||||
else:
|
||||
source = path_or_data # local path or file://
|
||||
try:
|
||||
AudioDecoder(source)
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def _load_video_for_encoder(self, video_data):
|
||||
# Normalise once to bytes-or-path; reused by frame decode, audio
|
||||
# detection, and audio preprocessing without re-downloading.
|
||||
from sglang.srt.utils.common import VideoData, _normalize_video_input
|
||||
from sglang.srt.utils.video_decoder import VideoDecoderWrapper
|
||||
|
||||
if isinstance(video_data, VideoData):
|
||||
video_data = video_data.url
|
||||
if isinstance(video_data, bytes):
|
||||
video_blob = video_data
|
||||
else:
|
||||
video_blob = _normalize_video_input(video_data)
|
||||
if video_blob is None:
|
||||
raise ValueError(
|
||||
f"Unsupported video input type for EPD encoder: {type(video_data)}"
|
||||
)
|
||||
|
||||
vdw = VideoDecoderWrapper(video_blob, device="cpu")
|
||||
try:
|
||||
video_tuple = _decode_frames_and_timestamps(
|
||||
vdw, self.default_video_processor_kwargs
|
||||
)
|
||||
finally:
|
||||
if hasattr(vdw, "close"):
|
||||
vdw.close()
|
||||
return video_blob, video_tuple
|
||||
|
||||
def preprocess_for_encoder(self, mm_data, modality):
|
||||
# EPD encoder-side features. video_audio_* fields appear when any
|
||||
# video has audio; the D side uses them to rebuild input_ids.
|
||||
from sglang.srt.managers.schedule_batch import Modality
|
||||
|
||||
if not isinstance(mm_data, (list, tuple)):
|
||||
mm_data = [mm_data]
|
||||
|
||||
if modality == Modality.IMAGE:
|
||||
factor = self.patch_size * self.merge_size
|
||||
min_pixels = self.default_image_processor_kwargs["min_pixels"]
|
||||
max_pixels = self.default_image_processor_kwargs["max_pixels"]
|
||||
all_patches, all_grids = [], []
|
||||
for img in mm_data:
|
||||
img_tensor, _, _ = self.get_visual_transform(
|
||||
img, factor=factor, min_pixels=min_pixels, max_pixels=max_pixels
|
||||
)
|
||||
patches, grid = self._flatten_visual_inputs(img_tensor, "image")
|
||||
all_patches.append(patches)
|
||||
all_grids.append(grid)
|
||||
return {
|
||||
"pixel_values": torch.cat(all_patches, dim=0),
|
||||
"image_grid_thw": torch.stack(all_grids),
|
||||
}
|
||||
|
||||
if modality == Modality.VIDEO:
|
||||
all_patches, all_grids, all_timestamps = [], [], []
|
||||
audio_features, audio_feature_lens = [], []
|
||||
seg_lens_flat, seg_starts_flat, per_video_num_units = [], [], []
|
||||
|
||||
for video_data in mm_data:
|
||||
video_blob, video_tuple = self._load_video_for_encoder(video_data)
|
||||
patches, grid, aligned_ts, video_meta = self.process_video(
|
||||
VideoInput(video=video_tuple)
|
||||
)
|
||||
all_patches.append(patches)
|
||||
all_grids.append(grid)
|
||||
step = self.temporal_patch_size * self.temporal_compression_ratio
|
||||
all_timestamps.extend(aligned_ts[::step].tolist())
|
||||
|
||||
if self.has_audio_track(video_blob):
|
||||
audio_spec, audio_token_len = self.preprocess_audio(video_blob)
|
||||
units = self._build_video_audio_units(
|
||||
grid,
|
||||
aligned_ts,
|
||||
video_meta,
|
||||
processed_audio=audio_spec,
|
||||
is_tokenized=False,
|
||||
audio_token_len=audio_token_len,
|
||||
)
|
||||
audio_features.append(audio_spec)
|
||||
audio_feature_lens.append(audio_token_len)
|
||||
seg_lens_flat.extend(u["segment_audio_token_len"] for u in units)
|
||||
seg_starts_flat.extend(u["audio_start_token_idx"] for u in units)
|
||||
per_video_num_units.append(len(units))
|
||||
else:
|
||||
per_video_num_units.append(0)
|
||||
|
||||
result = {
|
||||
"pixel_values_videos": torch.cat(all_patches, dim=0),
|
||||
"video_grid_thw": torch.stack(all_grids),
|
||||
"video_timestamps": all_timestamps,
|
||||
}
|
||||
if audio_features:
|
||||
result["video_audio_features"] = audio_features
|
||||
result["video_audio_feature_lens"] = torch.tensor(
|
||||
audio_feature_lens, dtype=torch.long
|
||||
)
|
||||
result["video_audio_segment_lens_flat"] = seg_lens_flat
|
||||
result["video_audio_segment_starts_flat"] = seg_starts_flat
|
||||
result["video_audio_per_video_num_units"] = per_video_num_units
|
||||
return result
|
||||
|
||||
if modality == Modality.AUDIO:
|
||||
all_specs, all_lens = [], []
|
||||
for audio in mm_data:
|
||||
if isinstance(audio, np.ndarray):
|
||||
audio = (torch.from_numpy(audio).float(), self.audio_sampling_rate)
|
||||
spec, token_len = self.preprocess_audio(audio)
|
||||
all_specs.append(spec)
|
||||
all_lens.append(token_len)
|
||||
return {
|
||||
"input_features": all_specs,
|
||||
"audio_feature_lens_raw": torch.tensor(all_lens, dtype=torch.long),
|
||||
}
|
||||
|
||||
raise ValueError(f"Unsupported modality for EPD preprocessing: {modality}")
|
||||
|
||||
@property
|
||||
def mel_spectrogram(self):
|
||||
self._ensure_audio_dependencies()
|
||||
@@ -928,39 +1172,31 @@ class MiMoProcessor:
|
||||
"verbose": verbose_str,
|
||||
}
|
||||
|
||||
def _process_video_audio_content(
|
||||
self, content_idx, content, video_results, verbose
|
||||
def _build_video_audio_units(
|
||||
self,
|
||||
thw_grid,
|
||||
timestamps,
|
||||
video_meta,
|
||||
processed_audio,
|
||||
is_tokenized,
|
||||
audio_token_len,
|
||||
):
|
||||
visual_patches, thw_grid, timestamps, video_meta = video_results[content_idx]
|
||||
# Compute per-grid_t audio-segment boundaries. Tokenizer-free so it
|
||||
# runs identically on the single-node path and the EPD encoder side.
|
||||
grid_t, grid_h, grid_w = thw_grid
|
||||
|
||||
processed_audio = self.process_audio(content.content)
|
||||
audio_token_per_second = self.audio_input_id_per_second / self.audio_group_size
|
||||
|
||||
assert (
|
||||
len(timestamps) == grid_t * self.temporal_patch_size
|
||||
), f"Expected {grid_t} * {self.temporal_patch_size} timestamps, got {len(timestamps)}"
|
||||
if not self.use_video_timestamps:
|
||||
raise NotImplementedError
|
||||
|
||||
if isinstance(processed_audio, tuple):
|
||||
assert (
|
||||
content.content.start_time is None and content.content.end_time is None
|
||||
), "Audio start_time and end_time must be None when audio is not tokenized"
|
||||
is_tokenized = False
|
||||
audio_spec, audio_token_len = processed_audio
|
||||
audio_input = audio_spec
|
||||
else:
|
||||
is_tokenized = True
|
||||
audio_token_len = processed_audio.shape[0]
|
||||
audio_input = None
|
||||
|
||||
# Build video-audio units
|
||||
num_media_tokens_per_grid = grid_h * grid_w // (self.merge_size**2)
|
||||
grid_t_timestamps = timestamps[
|
||||
:: self.temporal_patch_size * self.temporal_compression_ratio
|
||||
]
|
||||
text_timestamps = [self.format_timestamp(ts) for ts in grid_t_timestamps]
|
||||
text_timestamp_ids = [self.tokenizer.encode(ts) for ts in text_timestamps]
|
||||
audio_token_per_second = self.audio_input_id_per_second / self.audio_group_size
|
||||
|
||||
video_audio_units = []
|
||||
units = []
|
||||
for i in range(len(grid_t_timestamps)):
|
||||
audio_start_token_idx = int(grid_t_timestamps[i] * audio_token_per_second)
|
||||
audio_end_token_idx = (
|
||||
@@ -980,80 +1216,102 @@ class MiMoProcessor:
|
||||
if is_tokenized
|
||||
else None
|
||||
)
|
||||
video_audio_units.append(
|
||||
(
|
||||
grid_t_timestamps[i],
|
||||
text_timestamps[i],
|
||||
text_timestamp_ids[i],
|
||||
num_media_tokens_per_grid,
|
||||
segment_audio_token_len,
|
||||
segment_audio,
|
||||
)
|
||||
units.append(
|
||||
{
|
||||
"timestamp": grid_t_timestamps[i],
|
||||
"num_video_tokens": num_media_tokens_per_grid,
|
||||
"segment_audio_token_len": segment_audio_token_len,
|
||||
"segment_audio": segment_audio,
|
||||
# Used by encode_server to trim audio_encoder output.
|
||||
"audio_start_token_idx": audio_start_token_idx,
|
||||
}
|
||||
)
|
||||
return units
|
||||
|
||||
def _build_video_audio_input_ids(
|
||||
self,
|
||||
units,
|
||||
thw_grid,
|
||||
video_meta,
|
||||
is_tokenized,
|
||||
audio_token_len,
|
||||
verbose=False,
|
||||
timestamps=None,
|
||||
):
|
||||
# Assemble video+audio input_ids from the unit list produced above.
|
||||
# Tokenizer-dependent; the language node replays this on EPD.
|
||||
text_timestamps = [self.format_timestamp(u["timestamp"]) for u in units]
|
||||
text_timestamp_ids = [self.tokenizer.encode(ts) for ts in text_timestamps]
|
||||
|
||||
# Group units by interleave length
|
||||
if self.video_audio_interleave_length == -1:
|
||||
groups = [list(enumerate(video_audio_units))]
|
||||
groups = [list(enumerate(units))]
|
||||
elif self.video_audio_interleave_length == 0:
|
||||
groups = [[(i, u)] for i, u in enumerate(video_audio_units)]
|
||||
groups = [[(i, u)] for i, u in enumerate(units)]
|
||||
else:
|
||||
assert self.video_audio_interleave_length > 0
|
||||
groups = []
|
||||
unit_idx = 0
|
||||
current_group = []
|
||||
time_ptr = 0
|
||||
while unit_idx < len(video_audio_units):
|
||||
while unit_idx < len(units):
|
||||
while (
|
||||
unit_idx < len(video_audio_units)
|
||||
and video_audio_units[unit_idx][0] >= time_ptr
|
||||
and video_audio_units[unit_idx][0]
|
||||
unit_idx < len(units)
|
||||
and units[unit_idx]["timestamp"] >= time_ptr
|
||||
and units[unit_idx]["timestamp"]
|
||||
< time_ptr + self.video_audio_interleave_length
|
||||
):
|
||||
current_group.append((unit_idx, video_audio_units[unit_idx]))
|
||||
current_group.append((unit_idx, units[unit_idx]))
|
||||
unit_idx += 1
|
||||
if current_group:
|
||||
groups.append(current_group)
|
||||
current_group = []
|
||||
time_ptr += self.video_audio_interleave_length
|
||||
|
||||
# Build input_ids and collect audio segments
|
||||
_input_ids = [self.video_start_token_id]
|
||||
audio_segments = []
|
||||
verbose_str = ""
|
||||
if verbose:
|
||||
verbose_str = f"VideoAudio (video_thw_grid={thw_grid}, video_meta={video_meta}, is_audio_tokenized={is_tokenized}, audio_token_len={audio_token_len}): [<video_start> "
|
||||
verbose_str = (
|
||||
f"VideoAudio (video_thw_grid={thw_grid}, video_meta={video_meta}, "
|
||||
f"is_audio_tokenized={is_tokenized}, audio_token_len={audio_token_len}): "
|
||||
f"[<video_start> "
|
||||
)
|
||||
|
||||
for group in groups:
|
||||
head_idx = group[0][0]
|
||||
if not self.use_per_grid_t_timestamps:
|
||||
_input_ids += group[0][1][2]
|
||||
_input_ids += text_timestamp_ids[head_idx]
|
||||
if verbose:
|
||||
verbose_str += f"{group[0][1][1]} "
|
||||
verbose_str += f"{text_timestamps[head_idx]} "
|
||||
_video_tokens, _audio_tokens = [], []
|
||||
video_verbose_str, audio_verbose_str = "", ""
|
||||
for unit_idx, unit in group:
|
||||
(
|
||||
timestamp,
|
||||
timestamp_text,
|
||||
timestamp_ids,
|
||||
video_token_len,
|
||||
segment_audio_token_len,
|
||||
segment_audio,
|
||||
) = unit
|
||||
if self.use_per_grid_t_timestamps:
|
||||
_video_tokens += timestamp_ids
|
||||
_audio_tokens += timestamp_ids
|
||||
video_verbose_str += timestamp_text + " "
|
||||
audio_verbose_str += timestamp_text + " "
|
||||
_video_tokens += text_timestamp_ids[unit_idx]
|
||||
_audio_tokens += text_timestamp_ids[unit_idx]
|
||||
video_verbose_str += text_timestamps[unit_idx] + " "
|
||||
audio_verbose_str += text_timestamps[unit_idx] + " "
|
||||
_video_tokens += (
|
||||
[self.vision_start_token_id]
|
||||
+ [self.video_token_id] * video_token_len
|
||||
+ [self.video_token_id] * unit["num_video_tokens"]
|
||||
+ [self.vision_end_token_id]
|
||||
)
|
||||
video_verbose_str += f"[{','.join([f'{ts:.2f}' for ts in timestamps.tolist()[unit_idx*self.temporal_patch_size*self.temporal_compression_ratio : (unit_idx+1)*self.temporal_patch_size*self.temporal_compression_ratio]])}] <vision_start> {video_token_len}*<video> <vision_end> "
|
||||
_audio_tokens += [self.audio_token_id] * segment_audio_token_len
|
||||
audio_verbose_str += f"{segment_audio_token_len}*<audio> "
|
||||
if segment_audio is not None:
|
||||
audio_segments.append(segment_audio)
|
||||
if verbose and timestamps is not None:
|
||||
ts_slice = timestamps.tolist()[
|
||||
unit_idx
|
||||
* self.temporal_patch_size
|
||||
* self.temporal_compression_ratio : (unit_idx + 1)
|
||||
* self.temporal_patch_size
|
||||
* self.temporal_compression_ratio
|
||||
]
|
||||
video_verbose_str += (
|
||||
f"[{','.join(f'{ts:.2f}' for ts in ts_slice)}] "
|
||||
f"<vision_start> {unit['num_video_tokens']}*<video> <vision_end> "
|
||||
)
|
||||
_audio_tokens += [self.audio_token_id] * unit["segment_audio_token_len"]
|
||||
audio_verbose_str += f"{unit['segment_audio_token_len']}*<audio> "
|
||||
if unit["segment_audio"] is not None:
|
||||
audio_segments.append(unit["segment_audio"])
|
||||
|
||||
_input_ids += (
|
||||
_video_tokens
|
||||
@@ -1072,13 +1330,55 @@ class MiMoProcessor:
|
||||
|
||||
return {
|
||||
"input_ids": _input_ids,
|
||||
"audio_segments": audio_segments,
|
||||
"verbose": verbose_str,
|
||||
}
|
||||
|
||||
def _process_video_audio_content(
|
||||
self, content_idx, content, video_results, verbose
|
||||
):
|
||||
visual_patches, thw_grid, timestamps, video_meta = video_results[content_idx]
|
||||
processed_audio = self.process_audio(content.content)
|
||||
|
||||
if isinstance(processed_audio, tuple):
|
||||
assert (
|
||||
content.content.start_time is None and content.content.end_time is None
|
||||
), "Audio start_time and end_time must be None when audio is not tokenized"
|
||||
is_tokenized = False
|
||||
audio_spec, audio_token_len = processed_audio
|
||||
audio_input = audio_spec
|
||||
else:
|
||||
is_tokenized = True
|
||||
audio_token_len = processed_audio.shape[0]
|
||||
audio_input = None
|
||||
|
||||
units = self._build_video_audio_units(
|
||||
thw_grid,
|
||||
timestamps,
|
||||
video_meta,
|
||||
processed_audio,
|
||||
is_tokenized,
|
||||
audio_token_len,
|
||||
)
|
||||
built = self._build_video_audio_input_ids(
|
||||
units,
|
||||
thw_grid,
|
||||
video_meta,
|
||||
is_tokenized,
|
||||
audio_token_len,
|
||||
verbose=verbose,
|
||||
timestamps=timestamps,
|
||||
)
|
||||
|
||||
return {
|
||||
"input_ids": built["input_ids"],
|
||||
"pixel_values": visual_patches,
|
||||
"thw_grid": thw_grid,
|
||||
"second_per_grid_t": self.temporal_patch_size / video_meta["fps_sampled"],
|
||||
"audio_input": audio_input,
|
||||
"audio_segments": audio_segments,
|
||||
"audio_segments": built["audio_segments"],
|
||||
"is_tokenized": is_tokenized,
|
||||
"verbose": verbose_str,
|
||||
"verbose": built["verbose"],
|
||||
}
|
||||
|
||||
def process(self, contents: list[Content], verbose: bool = False):
|
||||
@@ -1561,22 +1861,20 @@ class MiMoV2Processor(BaseMultimodalProcessor):
|
||||
return self.vision_config.spatial_merge_size
|
||||
|
||||
def _preprocess_video_sync(self, vdw, preprocess_kwargs=None):
|
||||
ele = preprocess_kwargs or {}
|
||||
total_frames, video_fps = len(vdw), vdw.avg_fps
|
||||
nframes = smart_nframes(ele, total_frames=total_frames, video_fps=video_fps)
|
||||
idx = list(
|
||||
np.unique(np.linspace(0, total_frames - 1, num=nframes, dtype=np.int64))
|
||||
)
|
||||
# Seed with processor_config defaults so E/D agree on fps/min/max.
|
||||
default_kwargs = {
|
||||
k: v
|
||||
for k, v in self.mimo_processor.default_video_processor_kwargs.items()
|
||||
if v is not None and k in ("fps", "min_frames", "max_frames", "num_frames")
|
||||
}
|
||||
ele = {**default_kwargs, **(preprocess_kwargs or {})}
|
||||
try:
|
||||
video_tensor = vdw.get_frames_as_tensor(idx)
|
||||
return _decode_frames_and_timestamps(vdw, ele)
|
||||
except Exception as e:
|
||||
logger.error(f"Video decode failed in _preprocess_video_sync: {e}")
|
||||
raise HTTPException(
|
||||
status_code=432, detail="Video file is corrupted or cannot be decoded"
|
||||
)
|
||||
video_tensor = video_tensor.permute(0, 3, 1, 2).float()
|
||||
timestamps = torch.as_tensor(idx, dtype=torch.float32) / video_fps
|
||||
return (video_tensor, timestamps)
|
||||
|
||||
def process_mm_data(
|
||||
self, input_text, images=None, videos=None, audios=None, **kwargs
|
||||
@@ -1613,7 +1911,7 @@ class MiMoV2Processor(BaseMultimodalProcessor):
|
||||
if "use_audio" in preprocess_kwargs:
|
||||
use_audio = preprocess_kwargs["use_audio"]
|
||||
elif isinstance(raw_video_source, str):
|
||||
use_audio = self.has_audio_track(raw_video_source)
|
||||
use_audio = self.mimo_processor.has_audio_track(raw_video_source)
|
||||
else:
|
||||
use_audio = False
|
||||
|
||||
@@ -1853,15 +2151,17 @@ class MiMoV2Processor(BaseMultimodalProcessor):
|
||||
preprocess_kwargs = (
|
||||
getattr(raw_video_item, "preprocess_kwargs", {}) or {}
|
||||
)
|
||||
use_audio = self.has_audio_track(raw_video_item.url)
|
||||
use_audio = self.mimo_processor.has_audio_track(
|
||||
raw_video_item.url
|
||||
)
|
||||
raw_video_item_audio = raw_video_item.url
|
||||
elif isinstance(raw_video_item, dict):
|
||||
use_audio = self.has_audio_track(
|
||||
use_audio = self.mimo_processor.has_audio_track(
|
||||
raw_video_item.get("url", raw_video_item)
|
||||
)
|
||||
raw_video_item_audio = raw_video_item
|
||||
elif isinstance(raw_video_item, str):
|
||||
use_audio = self.has_audio_track(raw_video_item)
|
||||
use_audio = self.mimo_processor.has_audio_track(raw_video_item)
|
||||
raw_video_item_audio = raw_video_item
|
||||
|
||||
video_tuple = self._preprocess_video_sync(
|
||||
@@ -1976,39 +2276,188 @@ class MiMoV2Processor(BaseMultimodalProcessor):
|
||||
mrope_position_delta=input_sample.rope_deltas,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def has_audio_track(path_or_data: str) -> bool:
|
||||
try:
|
||||
is_base64 = path_or_data.startswith("data:") and ";base64," in path_or_data
|
||||
cmd = [
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"quiet",
|
||||
"-print_format",
|
||||
"json",
|
||||
"-show_streams",
|
||||
"-select_streams",
|
||||
"a",
|
||||
"pipe:0" if is_base64 else path_or_data,
|
||||
]
|
||||
inp = (
|
||||
base64.b64decode(path_or_data.split(";base64,")[1])
|
||||
if is_base64
|
||||
else None
|
||||
def get_mm_data(self, prompt, embeddings, **kwargs):
|
||||
# EPD: rebuild input_ids from E-side embeddings + segment metadata;
|
||||
# video+audio reuses _build_video_audio_input_ids for layout parity.
|
||||
img_grid_thw = kwargs.get("img_grid_thw")
|
||||
video_grid_thw = kwargs.get("video_grid_thw")
|
||||
audio_feature_lens = kwargs.get("audio_feature_lens")
|
||||
video_timestamps = kwargs.get("video_timestamps")
|
||||
video_audio_feature_lens = kwargs.get("video_audio_feature_lens")
|
||||
video_audio_segment_lens_flat = kwargs.get("video_audio_segment_lens_flat")
|
||||
video_audio_per_video_num_units = kwargs.get("video_audio_per_video_num_units")
|
||||
video_audio_embedding = kwargs.get("video_audio_embedding")
|
||||
|
||||
if not isinstance(prompt, str):
|
||||
prompt = self._tokenizer.decode(prompt)
|
||||
|
||||
mp = self.mimo_processor
|
||||
text_parts = re.split(self.mm_tokens.get_combined_regex(), prompt)
|
||||
|
||||
per_video_timestamps = None
|
||||
if video_timestamps and video_grid_thw is not None:
|
||||
per_video_timestamps = []
|
||||
ts_offset = 0
|
||||
for grid in video_grid_thw:
|
||||
n_frames = int(grid[0].item()) // mp.temporal_compression_ratio
|
||||
per_video_timestamps.append(
|
||||
video_timestamps[ts_offset : ts_offset + n_frames]
|
||||
)
|
||||
ts_offset += n_frames
|
||||
|
||||
# Un-flatten per-video audio segmentation; None = video has no audio.
|
||||
num_videos = len(video_grid_thw) if video_grid_thw is not None else 0
|
||||
per_video_audio_info = [None] * num_videos
|
||||
if video_audio_per_video_num_units and video_audio_segment_lens_flat:
|
||||
off, av_idx = 0, 0
|
||||
for i, nu in enumerate(video_audio_per_video_num_units):
|
||||
if nu <= 0:
|
||||
continue
|
||||
seg_lens = list(video_audio_segment_lens_flat[off : off + nu])
|
||||
off += nu
|
||||
per_video_audio_info[i] = {
|
||||
"segment_lens": seg_lens,
|
||||
"audio_token_len": (
|
||||
int(video_audio_feature_lens[av_idx].item())
|
||||
if video_audio_feature_lens is not None
|
||||
else sum(seg_lens)
|
||||
),
|
||||
}
|
||||
av_idx += 1
|
||||
|
||||
# Merge video-borne audio into AUDIO bucket for uniform slicing.
|
||||
if video_audio_embedding is not None:
|
||||
if Modality.AUDIO in embeddings:
|
||||
raise NotImplementedError(
|
||||
"Request mixes standalone audio and video-with-audio; "
|
||||
"EPD merge path for this combination is not yet implemented."
|
||||
)
|
||||
embeddings = dict(embeddings)
|
||||
embeddings[Modality.AUDIO] = video_audio_embedding
|
||||
|
||||
merge_size = self.spatial_merge_size
|
||||
input_ids = []
|
||||
img_idx = video_idx = audio_idx = 0
|
||||
for part in text_parts:
|
||||
mod = self.mm_tokens.get_modality_of_token(part)
|
||||
if mod == Modality.IMAGE:
|
||||
grid = img_grid_thw[img_idx]
|
||||
n = int(grid.prod().item()) // (merge_size**2)
|
||||
input_ids += (
|
||||
[mp.vision_start_token_id]
|
||||
+ [mp.image_token_id] * n
|
||||
+ [mp.vision_end_token_id]
|
||||
)
|
||||
img_idx += 1
|
||||
elif mod == Modality.VIDEO:
|
||||
grid = video_grid_thw[video_idx]
|
||||
ts = per_video_timestamps[video_idx]
|
||||
n_per_frame = int(grid[1]) * int(grid[2]) // (merge_size**2)
|
||||
audio_info = per_video_audio_info[video_idx]
|
||||
if audio_info is not None:
|
||||
units = [
|
||||
{
|
||||
"timestamp": ts[i] if i < len(ts) else 0.0,
|
||||
"num_video_tokens": n_per_frame,
|
||||
"segment_audio_token_len": int(seg_len),
|
||||
"segment_audio": None,
|
||||
}
|
||||
for i, seg_len in enumerate(audio_info["segment_lens"])
|
||||
]
|
||||
built = mp._build_video_audio_input_ids(
|
||||
units,
|
||||
thw_grid=grid,
|
||||
video_meta=None,
|
||||
is_tokenized=False,
|
||||
audio_token_len=audio_info["audio_token_len"],
|
||||
)
|
||||
input_ids += built["input_ids"]
|
||||
else:
|
||||
ts_ids_per_frame = [
|
||||
mp.tokenizer.encode(mp.format_timestamp(t)) for t in ts
|
||||
]
|
||||
input_ids += (
|
||||
[mp.video_start_token_id]
|
||||
+ sum(
|
||||
[
|
||||
ts_ids
|
||||
+ [mp.vision_start_token_id]
|
||||
+ [mp.video_token_id] * n_per_frame
|
||||
+ [mp.vision_end_token_id]
|
||||
for ts_ids in ts_ids_per_frame
|
||||
],
|
||||
[],
|
||||
)
|
||||
+ [mp.video_end_token_id]
|
||||
)
|
||||
video_idx += 1
|
||||
elif mod == Modality.AUDIO:
|
||||
n = int(audio_feature_lens[audio_idx].item())
|
||||
input_ids += (
|
||||
[mp.audio_start_token_id]
|
||||
+ [mp.audio_token_id] * n
|
||||
+ [mp.audio_end_token_id]
|
||||
)
|
||||
audio_idx += 1
|
||||
elif part:
|
||||
input_ids += mp.tokenizer.encode(part)
|
||||
|
||||
input_ids_tensor = torch.tensor(input_ids)
|
||||
|
||||
# Slice precomputed embeddings into per-placeholder items
|
||||
mm_items = []
|
||||
consumed = {}
|
||||
for mod, token_id in [
|
||||
(Modality.IMAGE, mp.image_token_id),
|
||||
(Modality.VIDEO, mp.video_token_id),
|
||||
(Modality.AUDIO, mp.audio_token_id),
|
||||
]:
|
||||
if mod not in embeddings:
|
||||
continue
|
||||
for offset in self.get_mm_items_offset(input_ids_tensor, token_id):
|
||||
n = offset[1] - offset[0] + 1
|
||||
s = consumed.get(mod, 0)
|
||||
mm_items.append(
|
||||
MultimodalDataItem(
|
||||
modality=mod,
|
||||
offsets=[offset],
|
||||
precomputed_embeddings=embeddings[mod][s : s + n],
|
||||
)
|
||||
)
|
||||
consumed[mod] = s + n
|
||||
|
||||
# Position ids
|
||||
if mp.rope_type == "mrope":
|
||||
from sglang.srt.layers.rotary_embedding import MRotaryEmbedding
|
||||
|
||||
mrope_positions, mrope_position_delta = MRotaryEmbedding.get_rope_index(
|
||||
spatial_merge_size=self.spatial_merge_size,
|
||||
image_token_id=mp.image_token_id,
|
||||
video_token_id=mp.video_token_id,
|
||||
vision_start_token_id=mp.vision_start_token_id,
|
||||
model_type="qwen2_5_vl",
|
||||
input_ids=input_ids_tensor.unsqueeze(0),
|
||||
image_grid_thw=img_grid_thw,
|
||||
video_grid_thw=video_grid_thw,
|
||||
)
|
||||
r = subprocess.run(cmd, input=inp, capture_output=True, timeout=30)
|
||||
if r.returncode != 0:
|
||||
stderr = r.stderr.decode("utf-8", errors="replace")
|
||||
raise RuntimeError(f"ffprobe failed for {path_or_data}: {stderr}")
|
||||
return bool(json.loads(r.stdout).get("streams"))
|
||||
except subprocess.TimeoutExpired:
|
||||
logger.error("ffprobe timed out for %s", path_or_data)
|
||||
raise
|
||||
except FileNotFoundError as e:
|
||||
raise RuntimeError("ffprobe not found; install ffmpeg") from e
|
||||
except json.JSONDecodeError:
|
||||
logger.error("ffprobe returned invalid JSON for %s", path_or_data)
|
||||
raise
|
||||
mrope_positions = mrope_positions.squeeze(1)
|
||||
else:
|
||||
mrope_positions = torch.arange(len(input_ids)).expand(3, -1)
|
||||
mrope_position_delta = torch.zeros((1, 1), dtype=torch.int32)
|
||||
|
||||
return MultimodalProcessorOutput(
|
||||
mm_items=mm_items,
|
||||
input_ids=input_ids,
|
||||
im_start_id=self.IM_START_TOKEN_ID,
|
||||
im_end_id=self.IM_END_TOKEN_ID,
|
||||
im_token_id=mp.image_token_id,
|
||||
video_token_id=mp.video_token_id,
|
||||
audio_token_id=mp.audio_token_id,
|
||||
audio_start_id=self.AUDIO_START_TOKEN_ID,
|
||||
audio_end_id=self.AUDIO_END_TOKEN_ID,
|
||||
mrope_positions=mrope_positions,
|
||||
mrope_position_delta=mrope_position_delta,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _make_video_content(
|
||||
|
||||
@@ -2095,7 +2095,7 @@ class ServerArgs:
|
||||
), "Triton kernel MoE is only supported when ep_size == 1"
|
||||
|
||||
elif model_arch in MIMO_V2_MODEL_ARCHS:
|
||||
if model_arch == "MiMoV2ForCausalLM":
|
||||
if model_arch == "MiMoV2ForCausalLM" and not self.encoder_only:
|
||||
expected_attn_tp_size = get_mimo_v2_fused_qkv_expected_tp_size(
|
||||
hf_config
|
||||
)
|
||||
@@ -3721,7 +3721,7 @@ class ServerArgs:
|
||||
self.disaggregation_ib_device
|
||||
)
|
||||
|
||||
# Validate model type: only support Qwen models for now
|
||||
# Validate model type for encoder disaggregation
|
||||
hf_config = self.get_model_config().hf_config
|
||||
model_arch = hf_config.architectures[0]
|
||||
if (self.encoder_only or self.language_only) and model_arch not in [
|
||||
@@ -3737,9 +3737,11 @@ class ServerArgs:
|
||||
"Qwen2_5OmniForConditionalGeneration",
|
||||
"KimiVLForConditionalGeneration",
|
||||
"KimiK25ForConditionalGeneration",
|
||||
"MiMoV2ForCausalLM",
|
||||
]:
|
||||
raise ValueError(
|
||||
f"Model type {model_arch} is not supported for encoder disaggregation, only Qwen models are supported for now."
|
||||
f"Model type {model_arch} is not supported for encoder disaggregation. "
|
||||
f"Supported architectures: Qwen2VL, Qwen3VL, Qwen3.5, InternS2, Qwen2Audio, Qwen2.5Omni, Kimi, MiMoV2."
|
||||
)
|
||||
|
||||
def _validate_ib_devices(self, device_str: str) -> Optional[str]:
|
||||
|
||||
Reference in New Issue
Block a user