[Fix] --mm-process-config crash when video config contains (#30260)

This commit is contained in:
longxin9715
2026-07-28 09:24:40 +08:00
committed by GitHub
parent edc0e5489f
commit 356c11d5d9
4 changed files with 105 additions and 10 deletions
@@ -29,7 +29,7 @@ def transform_patches_to_flatten(
patch_size: int, patch_size: int,
merge_size: int, merge_size: int,
) -> torch.Tensor: ) -> torch.Tensor:
patches = patches.view( patches = patches.reshape(
batch_size * grid_t, batch_size * grid_t,
temporal_patch_size * channel, temporal_patch_size * channel,
grid_h // merge_size, grid_h // merge_size,
@@ -489,6 +489,7 @@ class BaseMultimodalProcessor(ABC):
videos=None, videos=None,
audios=None, audios=None,
processor=None, processor=None,
processor_video_config: Optional[Dict[str, Any]] = None,
**kwargs, **kwargs,
) -> dict: ) -> dict:
""" """
@@ -502,8 +503,13 @@ class BaseMultimodalProcessor(ABC):
kwargs.setdefault("images_kwargs", {}).update(self.image_config) kwargs.setdefault("images_kwargs", {}).update(self.image_config)
if videos: if videos:
kwargs["videos"] = videos kwargs["videos"] = videos
if self.video_config: video_config = (
kwargs.setdefault("videos_kwargs", {}).update(self.video_config) self.video_config
if processor_video_config is None
else processor_video_config
)
if video_config:
kwargs.setdefault("videos_kwargs", {}).update(video_config)
if audios: if audios:
if processor.__class__.__name__ in { if processor.__class__.__name__ in {
"Gemma3nProcessor", "Gemma3nProcessor",
@@ -59,6 +59,30 @@ FPS = 2.0
FPS_MIN_FRAMES = 4 FPS_MIN_FRAMES = 4
FPS_MAX_FRAMES = 768 FPS_MAX_FRAMES = 768
QWEN_VIDEO_PREPROCESS_CONFIG_KEYS = frozenset(
{
"fps",
"nframes",
"min_frames",
"max_frames",
"min_pixels",
"max_pixels",
"total_pixels",
"resized_height",
"resized_width",
}
)
def _get_processor_video_config(video_config, video_metadata):
if video_metadata and all(metadata is not None for metadata in video_metadata):
return {
key: value
for key, value in video_config.items()
if key not in QWEN_VIDEO_PREPROCESS_CONFIG_KEYS
}
return None
_is_cpu_amx_available = cpu_has_amx_support() _is_cpu_amx_available = cpu_has_amx_support()
_is_cpu = is_cpu() _is_cpu = is_cpu()
@@ -719,6 +743,13 @@ class QwenVLImageProcessor(SGLangBaseProcessor):
preprocess_time = time.perf_counter() preprocess_time = time.perf_counter()
processor_kwargs = {}
processor_video_config = _get_processor_video_config(
self.video_config, video_metadata
)
if processor_video_config is not None:
processor_kwargs["processor_video_config"] = processor_video_config
# NOTE: for qwen3-vl, video_meta need to be passed in, since do_sample_frames is already done in preprocess_video # NOTE: for qwen3-vl, video_meta need to be passed in, since do_sample_frames is already done in preprocess_video
if self.hf_config.model_type in ( if self.hf_config.model_type in (
"qwen3_vl", "qwen3_vl",
@@ -727,15 +758,13 @@ class QwenVLImageProcessor(SGLangBaseProcessor):
"qwen3_5_moe", "qwen3_5_moe",
"intern_s2_preview", "intern_s2_preview",
): ):
mm_items, input_ids, ret = await self.process_and_combine_mm_data_async( processor_kwargs.update(
base_output,
self.mm_tokens,
video_metadata=video_metadata, video_metadata=video_metadata,
do_sample_frames=False, do_sample_frames=False,
) )
else:
mm_items, input_ids, ret = await self.process_and_combine_mm_data_async( mm_items, input_ids, ret = await self.process_and_combine_mm_data_async(
base_output, self.mm_tokens base_output, self.mm_tokens, **processor_kwargs
) )
audio_feature_lengths = None audio_feature_lengths = None
@@ -395,6 +395,37 @@ class TestProcessMmDataKwargs(unittest.TestCase):
call_kwargs.kwargs.get("videos_kwargs"), {"fps": 3, "max_frames": 60} call_kwargs.kwargs.get("videos_kwargs"), {"fps": 3, "max_frames": 60}
) )
def test_preprocessed_video_config_is_filtered_before_single_call(self):
config = {
"video": {
"fps": 3,
"max_frames": 60,
"do_normalize": False,
}
}
proc, mock_proc, _ = self._make_base_processor(config)
proc.process_mm_data(
"test",
videos=["vid1"],
processor_video_config={"do_normalize": False},
)
self.assertEqual(mock_proc.__call__.call_count, 1)
self.assertEqual(
mock_proc.__call__.call_args.kwargs.get("videos_kwargs"),
{"do_normalize": False},
)
def test_processor_error_is_not_retried(self):
proc, mock_proc, _ = self._make_base_processor({"video": {"max_frames": 60}})
mock_proc.__call__.side_effect = ValueError("processor failure")
with self.assertRaisesRegex(ValueError, "processor failure"):
proc.process_mm_data("test", videos=["vid1"])
self.assertEqual(mock_proc.__call__.call_count, 1)
def test_no_collision_with_overlapping_keys(self): def test_no_collision_with_overlapping_keys(self):
"""Core test: image and video both have max_pixels but stay separate.""" """Core test: image and video both have max_pixels but stay separate."""
config = { config = {
@@ -527,6 +558,35 @@ class TestOverrideProcessorsConfigInjection(unittest.TestCase):
self.assertTrue(audio_kw.get("truncation")) self.assertTrue(audio_kw.get("truncation"))
class TestQwenVideoConfigRouting(unittest.TestCase):
def test_preprocessed_video_drops_sglang_owned_config(self):
from sglang.srt.multimodal.processors.qwen_vl import (
_get_processor_video_config,
)
video_config = {
"fps": 3,
"nframes": 12,
"max_frames": 60,
"max_pixels": 500000,
"do_normalize": False,
}
processor_config = _get_processor_video_config(video_config, [{"fps": 30.0}])
self.assertEqual(processor_config, {"do_normalize": False})
def test_unprocessed_video_uses_original_config(self):
from sglang.srt.multimodal.processors.qwen_vl import (
_get_processor_video_config,
)
video_config = {"fps": 3, "max_frames": 60}
self.assertIsNone(_get_processor_video_config(video_config, None))
self.assertIsNone(_get_processor_video_config(video_config, [None]))
class TestDoubleBosGuard(unittest.TestCase): class TestDoubleBosGuard(unittest.TestCase):
"""Regression test for the multimodal double-BOS bug. """Regression test for the multimodal double-BOS bug.