[Bugfix][Qwen3-VL] Fix multi-video crash with list-valued fps/num_frames (#46305)
Signed-off-by: Ting Sun <suntcrick@gmail.com>
Ting SUN committed
Jun 22, 2026 at 04:31 UTC
12fe2a9aac8e0284ff1dfbd53857f7e6f7f50da1
2 files changed
+59
-2
tests/models/multimodal/processing/test_qwen3_vl.py
+46
@@ -138,3 +138,49 @@ def test_processor_multi_video(
138
assert video_phs[i].offset >= prev_end, (
139
f"Placeholder {i} overlaps with placeholder {i - 1}"
140
)
141
+
142
+
143
+@pytest.mark.parametrize("model_id", [MODEL_ID])
144
+@pytest.mark.parametrize(
145
+ "hf_mm_kwargs",
146
+ [{"num_frames": [8, 16]}, {"fps": [2.0, 4.0]}],
147
+)
148
+def test_processor_multi_video_list_kwargs(
149
+ model_id: str,
150
+ hf_mm_kwargs: dict[str, Any],
151
+) -> None:
152
+ """Regression test: a multi-video request with list-valued per-video
153
+ ``mm_processor_kwargs`` (one ``fps``/``num_frames`` per video) must not
154
+ crash.
155
+
156
+ Before the fix, ``_call_hf_processor`` copied the whole kwargs to every
157
+ video without slicing, so ``_get_video_second_idx`` received the list
158
+ where a scalar was expected and raised ``TypeError``.
159
+ """
160
+ ctx = build_model_context(
161
+ model_id,
162
+ limit_mm_per_prompt={"image": 0, "video": 2},
163
+ )
164
+ processor = MULTIMODAL_REGISTRY.create_processor(ctx.model_config)
165
+
166
+ prompt = (
167
+ "<|vision_start|><|video_pad|><|vision_end|>"
168
+ "<|vision_start|><|video_pad|><|vision_end|>"
169
+ )
170
+ mm_data = {
171
+ "video": [
172
+ _build_video_mm_data(num_frames=16)["video"][0],
173
+ _build_video_mm_data(num_frames=32)["video"][0],
174
+ ]
175
+ }
176
+
177
+ processed = processor(
178
+ prompt,
179
+ mm_items=processor.info.parse_mm_data(mm_data),
180
+ hf_processor_mm_kwargs=hf_mm_kwargs,
181
+ )
182
+
183
+ video_phs = processed["mm_placeholders"].get("video", [])
184
+ assert len(video_phs) == 2, (
185
+ f"Expected exactly 2 video placeholders, got {len(video_phs)}"
186
+ )
vllm/model_executor/models/qwen3_vl.py
+13
-2
@@ -1271,7 +1271,7 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
1271
vision_end_token_id = hf_config.vision_end_token_id
1272
video_token_id = hf_config.video_token_id
1273
1274
- for item in videos:
1274
+ for item_idx, item in enumerate(videos):
1275
video_array, metadata = item
1276
1277
# NOTE: @JJJYmmm new attr metadata.frames_indices indicates
@@ -1282,6 +1282,12 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
1282
# NOTE: a copy of is created to update do_sample_frames,
1283
# otherwise mm_hash for the object will be incorrect.
1284
video_mm_kwargs = dict(**mm_kwargs)
1285
+ sampled_fps = video_mm_kwargs.get("fps")
1286
+ if is_list_of(sampled_fps, float):
1287
+ video_mm_kwargs["fps"] = sampled_fps[item_idx]
1288
+ sampled_num_frames = video_mm_kwargs.get("num_frames")
1289
+ if is_list_of(sampled_num_frames, int):
1290
+ video_mm_kwargs["num_frames"] = sampled_num_frames[item_idx]
1291
if "do_sample_frames" not in video_mm_kwargs:
1292
# qwen_vl_utils already has "do_sample_frames" in
1293
# mm_kwargs, don't overwrite it.
@@ -1363,10 +1369,15 @@ class Qwen3VLMultiModalProcessor(BaseMultiModalProcessor[Qwen3VLProcessingInfo])
1369
else:
1370
video_outputs = dict()
1371
1372
+ # fps/num_frames are video-only kwargs already consumed by the loop;
1373
+ # exclude them so the text/image processor call below never gets a list.
1374
+ non_video_mm_kwargs = {
1375
+ k: v for k, v in mm_kwargs.items() if k not in ("fps", "num_frames")
1376
+ }
1377
processed_outputs = super()._call_hf_processor(
1378
prompt=prompt,
1379
mm_data=mm_data,
1369
- mm_kwargs=mm_kwargs,
1380
+ mm_kwargs=non_video_mm_kwargs,
1381
tok_kwargs=tok_kwargs,
1382
)
1383