From ee406adf0cbcd1e7b073a6b0ca6a3afeec2b9171 Mon Sep 17 00:00:00 2001 From: Vadim Gimpelson <156319763+vadiklyutiy@users.noreply.github.com> Date: Thu, 8 Oct 2026 10:28:44 +0400 Subject: [PATCH] [Bugfix] Fix ZeroDivisionError in Qwen3-VL video sampling when the source fps is unknown (#60501) Signed-off-by: Vadim Gimpelson Co-authored-by: Claude --- tests/multimodal/test_video.py | 33 ++++++++++++++++++++++++++++++++- vllm/multimodal/video.py | 10 ++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/tests/multimodal/test_video.py b/tests/multimodal/test_video.py index d593edba29..407bffed20 100644 --- a/tests/multimodal/test_video.py +++ b/tests/multimodal/test_video.py @@ -13,7 +13,7 @@ import numpy as np import numpy.typing as npt import pytest import torch -from transformers import AutoVideoProcessor +from transformers import AutoVideoProcessor, Qwen3VLVideoProcessor from transformers.video_utils import VideoMetadata from vllm.assets.base import get_vllm_public_assets @@ -35,6 +35,7 @@ from vllm.multimodal.video import ( get_video_loader_backend_for_processor, ) from vllm.multimodal.video_decoders import decode_video, resolve_video_backend_kwargs +from vllm.multimodal.video_decoders.opencv import OpenCVVideoBackendMixin from vllm.multimodal.video_decoders.pynvvideocodec import ( PYNVVIDEOCODEC_DECODER_CACHE_SIZE, PyNvVideoCodecDecoderSlot, @@ -730,6 +731,36 @@ def test_cosmos3_edge_uses_qwen3_vl_video_backend(): assert isinstance(VIDEO_LOADER_REGISTRY.load(backend), Qwen3VLVideoBackend) +def test_qwen3_vl_unknown_source_fps_matches_hf( + dummy_video_path, monkeypatch: pytest.MonkeyPatch +): + """An unknown source fps (reported as 0) samples like HF's fps=None, and + the metadata carries the fps HF assumes, which the frame timestamps use.""" + get_video_metadata = OpenCVVideoBackendMixin.get_video_metadata + + def unknown_fps(cap) -> VideoSourceMetadata: + return get_video_metadata(cap)._replace(original_fps=0.0, duration=0.0) + + monkeypatch.setattr( + OpenCVVideoBackendMixin, "get_video_metadata", staticmethod(unknown_fps) + ) + _, metadata = Qwen3VLVideoBackend.load_bytes( + dummy_video_path.read_bytes(), fps=2, backend="opencv" + ) + + total_frames = metadata["total_num_frames"] + hf_metadata = VideoMetadata(total_num_frames=total_frames, fps=None) + hf_indices = Qwen3VLVideoProcessor().sample_frames(hf_metadata, fps=2).tolist() + assert metadata["frames_indices"] == hf_indices + assert metadata["fps"] == hf_metadata.fps + + source = VideoSourceMetadata(total_frames, original_fps=0.0, duration=0.0) + target = VideoTargetMetadata(num_frames=-1, fps=2, max_duration=300) + assert ( + Qwen3VLVideoBackend.compute_frames_index_to_sample(source, target) == hf_indices + ) + + @pytest.mark.parametrize( "model_repo, expected_loader_cls, hf_sample_kwargs", [ diff --git a/vllm/multimodal/video.py b/vllm/multimodal/video.py index 8f22bffe95..fc0cdc28d4 100644 --- a/vllm/multimodal/video.py +++ b/vllm/multimodal/video.py @@ -361,6 +361,15 @@ class Qwen3VLVideoBackend(VideoBackend): _MAX_FRAMES: ClassVar[int] = 768 _MAX_FPS: ClassVar[int] = 30 + @classmethod + def _prepare_source(cls, source: VideoSourceMetadata) -> VideoSourceMetadata: + # vLLM reports an unknown source fps as 0. HF's Qwen3VLVideoProcessor + # assumes a 24 fps source then, and keeps it in the metadata for the + # frame timestamps. + if source.original_fps > 0: + return source + return VideoSourceMetadata(source.total_frames_num, 24.0, source.duration) + @classmethod def compute_frames_index_to_sample( cls, @@ -368,6 +377,7 @@ class Qwen3VLVideoBackend(VideoBackend): target: VideoTargetMetadata, **kwargs, ) -> list[int]: + source = cls._prepare_source(source) total_frames_num = source.total_frames_num original_fps = source.original_fps fps = min(target.fps, cls._MAX_FPS)