From 356c11d5d90b7c62c073ef9cae1b35fea1a4e4ed Mon Sep 17 00:00:00 2001 From: longxin9715 <59550463+longxin9715@users.noreply.github.com> Date: Tue, 28 Jul 2026 09:24:40 +0800 Subject: [PATCH] [Fix] --mm-process-config crash when video config contains (#30260) --- .../npu/modules/qwen_vl_processor.py | 2 +- .../multimodal/processors/base_processor.py | 10 +++- .../srt/multimodal/processors/qwen_vl.py | 43 ++++++++++--- .../unit/managers/test_mm_process_config.py | 60 +++++++++++++++++++ 4 files changed, 105 insertions(+), 10 deletions(-) diff --git a/python/sglang/srt/hardware_backend/npu/modules/qwen_vl_processor.py b/python/sglang/srt/hardware_backend/npu/modules/qwen_vl_processor.py index 5dadafac6..dfbbbf927 100644 --- a/python/sglang/srt/hardware_backend/npu/modules/qwen_vl_processor.py +++ b/python/sglang/srt/hardware_backend/npu/modules/qwen_vl_processor.py @@ -29,7 +29,7 @@ def transform_patches_to_flatten( patch_size: int, merge_size: int, ) -> torch.Tensor: - patches = patches.view( + patches = patches.reshape( batch_size * grid_t, temporal_patch_size * channel, grid_h // merge_size, diff --git a/python/sglang/srt/multimodal/processors/base_processor.py b/python/sglang/srt/multimodal/processors/base_processor.py index 65b0f4151..dafa4889c 100644 --- a/python/sglang/srt/multimodal/processors/base_processor.py +++ b/python/sglang/srt/multimodal/processors/base_processor.py @@ -489,6 +489,7 @@ class BaseMultimodalProcessor(ABC): videos=None, audios=None, processor=None, + processor_video_config: Optional[Dict[str, Any]] = None, **kwargs, ) -> dict: """ @@ -502,8 +503,13 @@ class BaseMultimodalProcessor(ABC): kwargs.setdefault("images_kwargs", {}).update(self.image_config) if videos: kwargs["videos"] = videos - if self.video_config: - kwargs.setdefault("videos_kwargs", {}).update(self.video_config) + 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 processor.__class__.__name__ in { "Gemma3nProcessor", diff --git a/python/sglang/srt/multimodal/processors/qwen_vl.py b/python/sglang/srt/multimodal/processors/qwen_vl.py index c68f31529..a53e93730 100644 --- a/python/sglang/srt/multimodal/processors/qwen_vl.py +++ b/python/sglang/srt/multimodal/processors/qwen_vl.py @@ -59,6 +59,30 @@ FPS = 2.0 FPS_MIN_FRAMES = 4 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 = is_cpu() @@ -719,6 +743,13 @@ class QwenVLImageProcessor(SGLangBaseProcessor): 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 if self.hf_config.model_type in ( "qwen3_vl", @@ -727,16 +758,14 @@ class QwenVLImageProcessor(SGLangBaseProcessor): "qwen3_5_moe", "intern_s2_preview", ): - mm_items, input_ids, ret = await self.process_and_combine_mm_data_async( - base_output, - self.mm_tokens, + processor_kwargs.update( video_metadata=video_metadata, do_sample_frames=False, ) - else: - mm_items, input_ids, ret = await self.process_and_combine_mm_data_async( - base_output, self.mm_tokens - ) + + mm_items, input_ids, ret = await self.process_and_combine_mm_data_async( + base_output, self.mm_tokens, **processor_kwargs + ) audio_feature_lengths = None diff --git a/test/registered/unit/managers/test_mm_process_config.py b/test/registered/unit/managers/test_mm_process_config.py index 3d4495df8..63970ee7d 100644 --- a/test/registered/unit/managers/test_mm_process_config.py +++ b/test/registered/unit/managers/test_mm_process_config.py @@ -395,6 +395,37 @@ class TestProcessMmDataKwargs(unittest.TestCase): 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): """Core test: image and video both have max_pixels but stay separate.""" config = { @@ -527,6 +558,35 @@ class TestOverrideProcessorsConfigInjection(unittest.TestCase): 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): """Regression test for the multimodal double-BOS bug.