[OpenVINO] Support Youtu-VL-4B-Instruct with task image-text-to-text - #4210
[OpenVINO] Support Youtu-VL-4B-Instruct with task image-text-to-text#4210popovaan wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Pull request overview
Adds support for the tencent/Youtu-VL-4B-Instruct VLM (task: image-text-to-text) in OpenVINO GenAI by introducing a dedicated Youtu-VL vision encoder (SigLIP2 “naflex” patchification to pixel_values) and a Youtu-VL inputs embedder (Qwen2.5-VL-like merger with plain 1D position_ids). The PR also updates Who-What-Benchmark (WWB) and test scaffolding to enable/validate the new model path and documents the supported model list.
Changes:
- Introduces Youtu-VL C++ implementation and registers it in VLM factories/config.
- Extends processor configuration with
max_num_patchesfor SigLIP2-naflex preprocessing. - Updates WWB to support local CSV datasets (including image path resolution) and improves prompt alignment behavior in visual-text evaluation; updates docs/tests for the new model entry.
Reviewed changes
Copilot reviewed 18 out of 18 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| tools/who_what_benchmark/whowhatbench/wwb.py | Adds local CSV dataset support and local image resolution. |
| tools/who_what_benchmark/whowhatbench/visualtext_evaluator.py | Adjusts GT/prediction alignment and comparison-frame construction for visual-text tasks. |
| tools/who_what_benchmark/whowhatbench/model_loaders.py | Removes deprecated use_flash_attention_2 forwarding to avoid remote-code model init failures. |
| tools/who_what_benchmark/whowhatbench/inputs_preprocessors/youtu_vl.py | Adds Youtu-VL-specific input preprocessing for WWB. |
| tools/who_what_benchmark/whowhatbench/inputs_preprocessors/init.py | Registers the Youtu-VL inputs preprocessor. |
| tests/python_tests/utils/hugging_face.py | Adds Youtu-VL tiny-random fixture to trusted-remote-code list. |
| tests/python_tests/test_vlm_pipeline.py | Adds Youtu-VL model ID wiring and a skip until fixture publication. |
| src/cpp/src/visual_language/youtu_vl/classes.hpp | Declares Youtu-VL vision encoder + inputs embedder and helper sizing util. |
| src/cpp/src/visual_language/youtu_vl/classes.cpp | Implements SigLIP2-naflex patch preprocessing, vision inference, and 1D position id generation. |
| src/cpp/src/visual_language/vlm_config.hpp | Adds VLMModelType::YOUTU_VL. |
| src/cpp/src/visual_language/vlm_config.cpp | Adds string-to-enum mapping for youtu_vl. |
| src/cpp/src/visual_language/vision_encoder.cpp | Registers VisionEncoderYoutuVL in factory. |
| src/cpp/src/visual_language/processor_config.hpp | Adds ProcessorConfig.max_num_patches. |
| src/cpp/src/visual_language/processor_config.cpp | Parses max_num_patches from JSON. |
| src/cpp/src/visual_language/inputs_embedder.hpp | Adds friend class InputsEmbedderYoutuVL. |
| src/cpp/src/visual_language/inputs_embedder.cpp | Registers InputsEmbedderYoutuVL in factory. |
| site/docs/supported-models/_components/vlm-models-table/models.ts | Adds Youtu-VL to supported VLM models table. |
| .model_analysis/youtu_vl_analysis.md | Adds model analysis write-up for Youtu-VL enablement. |
| if value.strip() == "": | ||
| return None | ||
| img_path = value if os.path.isabs(value) else os.path.join(base_dir, value) | ||
| return Image.open(img_path).convert("RGB") |
| if "prompts" not in res and args.dataset_field in res: | ||
| res["prompts"] = res[args.dataset_field] | ||
| return res |
| # Align gt_data with predictions (handles skipped prompts). Keep the | ||
| # ground-truth rows in the same order as predictions so per-prompt | ||
| # source/optimized answers stay row-aligned when only a subset matches. | ||
| self.gt_data = self.gt_data[self.gt_data["prompts"].isin(predictions["prompts"].values)] | ||
|
|
||
| # Restrict predictions to the intersecting prompts as well so both | ||
| # frames describe the same prompt set before similarity is computed. | ||
| predictions = predictions[predictions["prompts"].isin(self.gt_data["prompts"].values)] | ||
|
|
| # Guard against any residual length mismatch between the metric arrays | ||
| # and the prompt/answer arrays so DataFrame construction never raises an | ||
| # opaque "All arrays must be of the same length" error. | ||
| compared_rows = min(len(self.gt_data), len(predictions)) | ||
| self.last_cmp = all_metrics_per_prompt |
| from .vlm_inputs_preprocessor import VLMInputsPreprocessor | ||
| from typing import TYPE_CHECKING, Optional, Union, Any | ||
|
|
| # The YoutuVL processor __call__ signature accepts (text, images, ...) | ||
| # and does not take a `videos` keyword. Only forward `videos` when the | ||
| # processor actually supports it to keep the image-text path working. | ||
| processor_kwargs = { | ||
| "images": self.images, | ||
| "text": text_prompt, | ||
| "return_tensors": "pt", | ||
| } | ||
| if self.videos is not None: | ||
| processor_kwargs["videos"] = self.videos | ||
|
|
| #include <algorithm> | ||
| #include <cmath> | ||
|
|
| ImageSize get_image_size_for_patches(size_t image_height, size_t image_width, size_t patch_size, size_t max_num_patches) { | ||
| // Mirrors image_processing_siglip2_fast.get_image_size_for_patches: | ||
| // round each dimension up to a multiple of patch_size*2, shrinking scale | ||
| // until the number of patches fits within max_num_patches. | ||
| auto scaled = [patch_size](double scale, size_t size) -> size_t { | ||
| size_t step = patch_size * 2; | ||
| double scaled_size = static_cast<double>(size) * scale; | ||
| size_t rounded = static_cast<size_t>(std::ceil(scaled_size / static_cast<double>(step))) * step; | ||
| return std::max(step, rounded); | ||
| }; | ||
|
|
||
| double scale = 1.0; | ||
| size_t target_height = 0; | ||
| size_t target_width = 0; | ||
| while (true) { | ||
| target_height = scaled(scale, image_height); | ||
| target_width = scaled(scale, image_width); | ||
| double num_patches = (static_cast<double>(target_height) / patch_size) * | ||
| (static_cast<double>(target_width) / patch_size); | ||
| if (num_patches > static_cast<double>(max_num_patches)) { | ||
| scale -= 0.02; | ||
| } else { | ||
| break; | ||
| } | ||
| } |
| // rope_delta is unused for plain 1D position ids; keep it at (max_pos + 1) so the | ||
| // generation phase continues sequentially from history. | ||
| int64_t rope_delta = 0; | ||
| return {position_ids, rope_delta}; |
| - SigLIP2 naflex patch preprocessing (resize to multiple of 32, patchify to [N,768], spatial_shapes) — NEW; not covered by Qwen2VL raw-pixel path. Must be implemented in VisionEncoderYoutuVL. | ||
| - Plain 1D position_ids for the LM (simpler than Qwen mRoPE) — override get_position_ids to return arange. | ||
| - EncodedImage must carry grid_thw = {1, h, w} (from spatial_shapes) so merger utils (window_index/rotary_pos_emb) work unchanged. | ||
| </content> |
3781e15 to
ff23f77
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Comments suppressed due to low confidence (4)
tools/who_what_benchmark/whowhatbench/inputs_preprocessors/youtu_vl.py:87
- When a video is provided, this preprocessor updates
self.videosand inserts a{type: "video"}placeholder into the chat template, but the actual video tensor list is never passed to the Hugging Face processor. This means video inputs are ignored (or the processor will error if the template expects video features).
inputs = processor(
images=self.images,
text=text_prompt,
return_tensors="pt",
)
tools/who_what_benchmark/whowhatbench/wwb.py:564
Image.open(path)leaves the underlying file handle open until the image object is closed/GC’d. When loading larger CSV datasets this can exhaust file descriptors. Use a context manager and append the converted copy.
for cell in df["images"].tolist():
path = _resolve_media_path(cell, base_dir)
images.append(Image.open(path).convert("RGB") if path is not None else None)
result["images"] = images
src/cpp/src/visual_language/youtu_vl/classes.hpp:69
- The comment claims the vision merger returns embeddings with shape
[tokens, 16], but the merger output width is the model hidden size (and can’t be assumed to be 16). This is misleading for future maintenance/debugging.
// Runs the vision merger for a single image and returns embeddings [tokens, 16].
src/cpp/src/visual_language/youtu_vl/classes.cpp:56
- The PR description says
ProcessorConfig.max_num_patcheswas added, but this implementation hardcodesmax_num_patches = 36864and there is nomax_num_patchesfield inProcessorConfigin this PR. Either the PR description needs updating, or this should be wired to an actual configurable processor setting.
// The Youtu-VL processor (__call__) overrides the SigLIP2 image processor's
// max_num_patches (256) with max_image_patches=36864, effectively resizing
// to the nearest multiple of (patch_size * merge_size) without shrinking to
// 256 patches. Replicate that authoritative value here.
const size_t max_num_patches = 36864;
| const ov::Tensor& hidden_states = image.resized_source; | ||
| const size_t seq_len = hidden_states.get_shape().at(0); |
| // Single image: full sequence is one attention block. | ||
| std::vector<int32_t> cu_seqlens = {0, static_cast<int32_t>(seq_len)}; | ||
| ov::Tensor attention_mask = make_block_diagonal_mask(seq_len, cu_seqlens); | ||
| ov::Tensor window_attention_mask = make_block_diagonal_mask(seq_len, cu_window_seqlens); |
ff23f77 to
c458199
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.
Suppressed comments (3)
tools/who_what_benchmark/whowhatbench/wwb.py:553
- Images are opened without closing the underlying file handle. In a longer WWB run this can leak file descriptors and prevent deleting/overwriting the images on Windows. Use a context manager when opening and converting the image.
if "images" in df.columns:
for cell in df["images"].tolist():
resolved = _resolve(cell)
images.append(Image.open(resolved).convert("RGB") if resolved else None)
src/cpp/src/visual_language/youtu_vl/classes.cpp:9
- This file uses std::numeric_limits, std::fill, and std::copy but only includes . Relying on indirect includes is non-portable and can break builds depending on compiler/STL implementation. Include the standard headers for the symbols used.
#include "visual_language/youtu_vl/classes.hpp"
#include <cmath>
#include "visual_language/clip.hpp"
#include "utils.hpp"
src/cpp/src/visual_language/youtu_vl/classes.cpp:313
- make_block_diag_mask() always fills the whole [seq_len x seq_len] mask with -inf and then overwrites blocks with 0. For attention_mask in the single-image case (cu_seqlens == {0, seq_len}) this does an unnecessary O(seq_len^2) write twice. Fast-path the single-block case by filling with zeros directly.
// Block-diagonal float attention mask: 0 inside a block, -inf outside.
ov::Tensor make_block_diag_mask(size_t seq_len, const std::vector<int32_t>& cu_seqlens) {
ov::Tensor mask(ov::element::f32, ov::Shape{1, seq_len, seq_len});
float* data = mask.data<float>();
const float neg_inf = -std::numeric_limits<float>::infinity();
std::fill(data, data + seq_len * seq_len, neg_inf);
for (size_t b = 1; b < cu_seqlens.size(); ++b) {
|
@popovaan Please summarize the validation performed in this PR. Do not modify any files. |
|
👀 @Mohamed-Ashraf273, review request received. |
Validation summary for PR #4210 (Youtu-VL-4B-Instruct, image-text-to-text)Hardware: Intel Core i9-14900 CPU, Intel Arc a780 GPU, 125.5 GiB RAM. Accuracy (WWB, similarity vs. HF reference)
GPU numbers (int8/int4) are still tbd and not yet reported. Performance (LLM Bench)
INT4 quality-ceiling exceptionThe author documents that int4_cpu lands at 0.92044 GenAI fidelity / 0.90804 accepted similarity, below the nominal 0.95 threshold. Three repair strategies (data-aware AWQ + scale estimation, mixed-precision ratio 0.8 with layer sensitivity, group_size 64) all scored at/below the data-free baseline; GPTQ and LoRA-correction were killed on the time budget (not counted). The author attributes the gap to sentence-embedding phrasing drift on verbose-but-correct answers (dominated by the surfboard sample) rather than factual errors, and notes INT8 reaching 0.97636 with identical support code as evidence the exporter/patcher/runtime integration is correct. Automated tests added in the PR
Observations / gaps for reviewers
Net: INT8 CPU accuracy and both CPU perf runs are solid and reported; INT4 CPU is a documented ceiling exception; GPU runs and the executable tiny-random pipeline test remain outstanding. |
Reproduce generation
Validation
Machine Info:
WWB Accuracy:
LLM Bench Performance:
Validated Quantization Ceiling Exception:
Related model-support PRs
Checklist