Repository navigation
feat: video parts for Qwen3.5/Qwen3-VL renderers - #216
Draft
hubert-marek wants to merge 1 commit into
Draft
hubert-marek wants to merge 1 commit into
hubert-marek wants to merge 1 commit into
Conversation
Qwen35Renderer (Qwen3.5/3.6/3.8) and Qwen3VLRenderer raised NotImplementedError on any video content part. They now render videos next to the image path, laid out as vLLM 0.32's Qwen3-VL processor lays them out. With process_multimodal=False the template's <|vision_start|><|video_pad|><|vision_end|> goes out as is, and vLLM expands it from the raw video_url content part. Otherwise the renderer decodes and samples the video as vLLM's Qwen3-VL loader does (OpenCV, 2 fps, 4 to 768 frames), runs the HF video processor, and expands the <|video_pad|> into a <t seconds> timestamp, <|vision_start|>, the pads and <|vision_end|> per frame pair. The video's placeholder spans that expansion; a new optional PlaceholderRange.is_embed marks its pads, so build_training_sample flags only the pads as video tokens. vLLM's generate features carry no is_embed mask, so the client refuses processed videos there and points to process_multimodal=False. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Wiz Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What changed
Qwen35Renderer(Qwen3.5 / 3.6 / 3.8, so Qwen3.8-Flash-Next too) andQwen3VLRendererraisedNotImplementedError("Video parts are not yet supported by ...")on any video content part. They now render videos with a sibling of the image path in every place images go: user and tool content,renderandbridge_to_next_turn, withprocess_multimodalon and off, and withadd_vision_id(Video N:).process_multimodal=False(the path verifiers' train client uses): the template's<|vision_start|><|video_pad|><|vision_end|>goes out as is, exactly like the single<|image_pad|>for images, and vLLM expands it from the rawvideo_urlcontent part.client._content_partsalready forwardedvideo_url. The bridge counts prior videos from the logical prompt's<|video_pad|>tokens, as it counts images.process_multimodal=True: the renderer decodes, samples and processes the video, then expands the<|video_pad|>as vLLM 0.32's Qwen3-VL processor does.mm_items["video"]carriespixel_values_videosandvideo_grid_thw, andmm_hashesandmm_placeholdersare filled as they are for images.PlaceholderRangegets an optionalis_embed: list[bool] | None = None, mirroring vLLM's field of the same name. A video's range also holds its frame timestamps and per-frame vision markers, sois_embedmarks which positions are pads._build_mm_token_type_idshonors it, sobuild_training_sampleflags only the pads as video (2), which matches HF'smm_token_type_ids. Images keepis_embed=Noneand behave exactly as before.MULTIMODAL_MODELSlists"video"for the 13 Qwen3-VL / 3.5 / 3.6 / 3.8 checkpoints. All of them ship avideo_preprocessor_config.json.How the layout matches vLLM 0.32.0
renderers/qwen3_vl.py_load_video/_decode_video): these mirrorQwen3VLVideoBackend.load_byteswith its default OpenCV codec. That covers the stream-bufferedcv2.VideoCapture, the frame-count edit-list check, the 24 fps fallback,num = int(total / fps * 2)clamped to [4, 768],linspace(...).round()indices, the same grab/retrieve loop, and the same metadata (frames_indices,do_sample_frames = len(valid) == total). A(frames, metadata)pair, the shape vLLM's loaders return, is also accepted without decoding._run_video_processor): the HF video processor runs withVideoMetadataanddo_sample_framesfrom that metadata, asQwen3VLMultiModalProcessor._apply_hf_processor_mainand prime-rl#4390's trainer do.temporal_patch_sizeis 2,merge_size2,patch_size16._video_timestamps): this isQwen3VLProcessingInfo._get_video_second_idx, the mean seconds of each frame pair. When the HF processor samples the frames itself, its indices are recomputed first._video_placeholder_ids;emit_videoinqwen3_vl.pyandqwen35.py): this isget_video_repl. Per frame pair it emits<t seconds>, tokenized on its own, then<|vision_start|>,H*W/4×<|video_pad|>and<|vision_end|>. The template's own<|vision_start|>/<|vision_end|>stay around the expansion, because vLLM targets only the bare<|video_pad|>with transformers ≥ 5.10 processors. The placeholder spans the frame groups, as vLLM's does.Tests
tests/test_qwen_video.py:test_video_prompt_matches_vllm: a clip with the measured 4 s, 512×288, 10 fps sampling renders to vLLM 0.32.0'sprompt_token_idsfor the real clip, all 617 tokens, hard-coded. It checks placeholder(4, 608), grid[4, 18, 32], and 576 pads.test_video_expansion_follows_vllm_get_video_repl: checks the frame-group layout,is_embedandis_contenton Qwen3.8-Flash-Next and Qwen3-VL-4B.test_video_mm_token_type_ids_mark_only_pads: coversbuild_training_sample.test_deferred_video_leaves_one_pad_for_vllm: coversprocess_multimodal=False, including bridge numbering ofVideo 2:.test_video_timestamps_follow_vllm: offline; covers both sampling branches.test_video_url_decodes_like_vllm: a synthetic mp4 written with OpenCV. It is skipped withoutcv2.tests/test_multimodal.py: the(model, modality)matrix now runsvideofor the Qwen-VL family. That includes token parity withprocessor.apply_chat_template+processor(videos=..., video_metadata=...)(HF), placeholder pads viais_embed, bridge carry-forward, tool-response videos for the Qwen3.5 family, andadd_vision_idparity and bridge refusal.tests/test_client.py:video_urlcontent parts, and the refusal of processed video features.tests/test_build_helpers.py:is_embedin_build_mm_token_type_ids.Results:
uv run --frozen pytest tests/ -m "not model": 218 passed, 2 skipped (main: 214 passed, 1 skipped)uv run --frozen pytest tests/test_multimodal.py tests/test_qwen_video.py tests/test_qwen38.py: 250 passed, 15 skipped (skips: the private Nemotron checkpoint, Qwen3-VL tool-response media, audio, and the OpenCV test). The Qwen subsets oftest_build_helpers.py,test_bridge.pyandtest_is_content.pyalso pass: 152 passed, 3 skippeduv run --frozen --with opencv-python-headless pytest tests/test_qwen_video.py: 9 passedruff check,ruff format --check(0.15.12): clean.ty check renderersexits 0; the only new diagnostics are twounresolved-importwarnings for the optionalcv2, like the existingvllmones._decode_video's frames and metadata are identical toMediaConnector().fetch_video(..., video_processor="Qwen3VLVideoProcessor"). The rendered prompt equals vLLM's 617 ids with placeholder(4, 608).Qwen3VLMultiModalProcessor.applygives the same ids, placeholder andis_embedas the renderer for three synthetic videos with Qwen3-VL-4B, including one that vLLM keeps whole so the HF processor samples it. vLLM'spixel_values_videosare uint8 with normalization deferred to the model; normalized, they match the renderer's to 6e-8.Not equivalent to images
/inference/v1/generate. Its features payload has nois_embed; vLLM's ownPlaceholderRangeInfohas a TODO for it. A video's placeholder has timestamps and markers between the pads, so_build_qwen_vl_featuresraisesNotImplementedErrorand points toprocess_multimodal=False. That is the path RL uses.opencv-python-headlessto themultimodalextra, to keep this PR from touchinguv.lock. Say if you want it there.image_cache_max; one processed video is tens of MB, so a cache would need its own bound.<|video_pad|>expansion). This matches vLLM with the transformers this repo locks (5.14.1) and prime-rl uses (5.19). Transformers 5.22 plans to makecap_pixels_per_framethe default, which changes grids for large videos in both vLLM and here.Why
This unblocks Qwen3.8 video training. PrimeIntellect-ai/verifiers#3067 adds
video_urlcontent parts, and its train client sends media through this renderer withprocess_multimodal=False, which failed on any video. PrimeIntellect-ai/prime-rl#4390 is the trainer side that pairs thosevideo_urls with vLLM's expanded placeholders.🤖 Generated with Claude Code
Note
Add video part rendering to Qwen3-VL and Qwen3.5 renderers
VideoPartsupport toQwen3VLRendererandQwen35Rendererwith the vLLM-style expansion: timestamp text, vision markers, and video-pad tokens per frame group, plus processed features (pixel_values_videos,video_grid_thw) in multimodal databridge_to_next_turnpreserves video numbering across processed and deferred turnsPlaceholderRangegains anis_embedmask, and_build_mm_token_type_idsin renderers/base.py assigns modality type IDs only at embedded positions within masked ranges;_build_qwen_vl_featuresin renderers/client.py now raisesNotImplementedErrorfor video data — useprocess_multimodal=Falsefor videos through the vLLM generate endpointMacroscope summarized 65a7146.