first commit
Some checks failed
Self-hosted runner (nightly-past-ci-caller) / Get number (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.11 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.10 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.9 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.8 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.7 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.6 (push) Has been cancelled
Self-hosted runner (nightly-past-ci-caller) / TensorFlow 2.5 (push) Has been cancelled
Self-hosted runner (benchmark) / Benchmark (aws-g5-4xlarge-cache) (push) Has been cancelled
Build documentation / build (push) Has been cancelled
Build documentation / build_other_lang (push) Has been cancelled
CodeQL Security Analysis / CodeQL Analysis (push) Has been cancelled
New model PR merged notification / Notify new model (push) Has been cancelled
PR CI / pr-ci (push) Has been cancelled
Slow tests on important models (on Push - A10) / Get all modified files (push) Has been cancelled
Secret Leaks / trufflehog (push) Has been cancelled
Update Transformers metadata / build_and_package (push) Has been cancelled
Slow tests on important models (on Push - A10) / Model CI (push) Has been cancelled
Check Tiny Models / Check tiny models (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / Model CI (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / Pipeline CI (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / Example CI (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / DeepSpeed CI (push) Has been cancelled
Self-hosted runner (Intel Gaudi3 scheduled CI caller) / Trainer/FSDP CI (push) Has been cancelled
Nvidia CI - Flash Attn / Setup (push) Has been cancelled
Nvidia CI - Flash Attn / Model CI (push) Has been cancelled
Nvidia CI / Setup (push) Has been cancelled
Nvidia CI / Model CI (push) Has been cancelled
Nvidia CI / Torch pipeline CI (push) Has been cancelled
Nvidia CI / Example CI (push) Has been cancelled
Nvidia CI / Trainer/FSDP CI (push) Has been cancelled
Nvidia CI / DeepSpeed CI (push) Has been cancelled
Nvidia CI / Quantization CI (push) Has been cancelled
Nvidia CI / Kernels CI (push) Has been cancelled
Doctests / Setup (push) Has been cancelled
Doctests / Call doctest jobs (push) Has been cancelled
Doctests / Send results to webhook (push) Has been cancelled
Extras Smoke Test / Get supported Python versions (push) Has been cancelled
Extras Smoke Test / Test extras on Python ${{ matrix.python-version }} (push) Has been cancelled
Extras Smoke Test / Check Slack token availability (push) Has been cancelled
Extras Smoke Test / Notify failures to Slack (push) Has been cancelled
Self-hosted runner (AMD scheduled CI caller) / Trigger Scheduled AMD CI (push) Has been cancelled
Stale Bot / Close Stale Issues (push) Has been cancelled

This commit is contained in:
陈赣
2026-06-05 16:53:03 +08:00
commit 06f1fd69a6
6047 changed files with 1895387 additions and 0 deletions

View File

View File

@@ -0,0 +1,493 @@
# Copyright 2025 The Qwen Team and The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Testing suite for the PyTorch Qwen3-VL model."""
import copy
import unittest
import pytest
from transformers import (
Qwen3VLConfig,
Qwen3VLForConditionalGeneration,
Qwen3VLModel,
is_torch_available,
)
from transformers.models.qwen3_vl.configuration_qwen3_vl import Qwen3VLTextConfig, Qwen3VLVisionConfig
from transformers.testing_utils import (
require_torch,
torch_device,
)
from ...test_modeling_common import floats_tensor, ids_tensor
from ...vlm_tester import VLMModelTest, VLMModelTester
if is_torch_available():
from transformers.models.qwen3_vl.configuration_qwen3_vl import Qwen3VLTextConfig
from transformers.models.qwen3_vl.modeling_qwen3_vl import Qwen3VLTextModel
if is_torch_available():
import torch
class Qwen3VLVisionText2TextModelTester(VLMModelTester):
base_model_class = Qwen3VLModel
config_class = Qwen3VLConfig
text_config_class = Qwen3VLTextConfig
vision_config_class = Qwen3VLVisionConfig
conditional_generation_class = Qwen3VLForConditionalGeneration
def __init__(self, parent, **kwargs):
kwargs.setdefault("image_token_id", 3)
kwargs.setdefault("video_token_id", 4)
kwargs.setdefault("vision_start_token_id", 5)
kwargs.setdefault("vision_end_token_id", 6)
kwargs.setdefault("image_size", 16)
kwargs.setdefault("patch_size", 16)
kwargs.setdefault("num_image_tokens", 32)
kwargs.setdefault("hidden_act", "silu")
kwargs.setdefault("num_attention_heads", 4)
kwargs.setdefault("num_key_value_heads", 2)
kwargs.setdefault("head_dim", 8)
kwargs.setdefault("depth", 2)
kwargs.setdefault("vision_hidden_act", "gelu_pytorch_tanh")
kwargs.setdefault("num_heads", 4)
kwargs.setdefault("spatial_merge_size", 1)
kwargs.setdefault("temporal_patch_size", 2)
kwargs.setdefault("num_position_embeddings", 16)
kwargs.setdefault("deepstack_visual_indexes", [0, 1])
kwargs.setdefault(
"rope_parameters",
{
"rope_type": "default",
"mrope_section": [16, 8, 8],
"mrope_interleaved": True,
"rope_theta": 10000,
},
)
super().__init__(parent, **kwargs)
# These can be inferred from existing properties and don't get separate kwargs
self.out_hidden_size = self.hidden_size
self.vision_hidden_size = self.hidden_size
self.vision_intermediate_size = self.hidden_size
def create_pixel_values(self):
# Qwen3VL expects flattened patches: (total_patches, channels * patch_size^2 * temporal_patch_size)
return floats_tensor(
[
self.batch_size * (self.image_size**2) // (self.patch_size**2),
self.num_channels * (self.patch_size**2) * self.temporal_patch_size,
]
)
def place_image_tokens(self, input_ids, config):
# Place image tokens with vision_start_token_id prefix
input_ids = input_ids.clone()
# Clear any accidental special tokens first
input_ids[:, -1] = self.pad_token_id
input_ids[input_ids == self.video_token_id] = self.pad_token_id
input_ids[input_ids == self.image_token_id] = self.pad_token_id
input_ids[input_ids == self.vision_start_token_id] = self.pad_token_id
# Place image tokens with vision_start_token_id prefix
input_ids[:, 1] = self.image_token_id
input_ids[:, 0] = self.vision_start_token_id
return input_ids
def get_additional_inputs(self, config, input_ids, modality_inputs):
mm_token_type_ids = torch.zeros_like(input_ids)
mm_token_type_ids[input_ids == self.image_token_id] = 1
return {
"image_grid_thw": torch.tensor([[1, 1, 1]] * self.batch_size, device=torch_device),
"mm_token_type_ids": mm_token_type_ids,
}
def get_config(self):
# Qwen3VLConfig expects text_config and vision_config as dicts, not config objects
return self.config_class(
text_config=self.get_text_config().to_dict(),
vision_config=self.get_vision_config().to_dict(),
image_token_id=self.image_token_id,
video_token_id=self.video_token_id,
vision_start_token_id=self.vision_start_token_id,
vision_end_token_id=self.vision_end_token_id,
tie_word_embeddings=self.tie_word_embeddings,
pad_token_id=self.pad_token_id,
)
@require_torch
class Qwen3VLModelTest(VLMModelTest, unittest.TestCase):
model_tester_class = Qwen3VLVisionText2TextModelTester
@pytest.mark.xfail(reason="This architecture seems to not compute gradients for some layer.")
def test_training_gradient_checkpointing(self):
super().test_training_gradient_checkpointing()
@pytest.mark.xfail(reason="This architecture seems to not compute gradients for some layer.")
def test_training_gradient_checkpointing_use_reentrant_false(self):
super().test_training_gradient_checkpointing_use_reentrant_false()
@pytest.mark.xfail(reason="This architecture seems to not compute gradients for some layer.")
def test_training_gradient_checkpointing_use_reentrant_true(self):
super().test_training_gradient_checkpointing_use_reentrant_true()
def test_vision_position_ids(self):
"""
Tests that vision position ids are built correctly for images and for videos.
See https://github.com/huggingface/transformers/pull/45400
"""
config, input_dict = self.model_tester.prepare_config_and_inputs_for_common()
model = Qwen3VLModel(config).to(torch_device)
batch_size = input_dict["input_ids"].shape[0]
# Test most simple case when num_image_tokens == 1. Position ids will be sunsequent and text-like
position_ids = model.get_rope_index(
input_dict["input_ids"], input_dict["mm_token_type_ids"], input_dict["image_grid_thw"]
)[0]
expected_positions = torch.arange(39)[None, None, :].repeat(3, batch_size, 1)
self.assertListEqual(list(position_ids.shape), [3, batch_size, 39])
self.assertListEqual(position_ids.tolist(), expected_positions.tolist())
# Each image encodes to more than 1 token (i.e. 4 height and 3 width patches = 12 tokens)
image_token_id = config.image_token_id
pad_token_id = config.text_config.pad_token_id
input_ids = torch.tensor([[pad_token_id] + [image_token_id] * 12 + [pad_token_id]], device=torch_device)
mm_token_type_ids = torch.tensor([[0] + [1] * 12 + [0]], device=torch_device)
image_grid_thw = torch.tensor([[1, 4, 3]], device=torch_device)
position_ids = model.get_rope_index(input_ids, mm_token_type_ids, image_grid_thw)[0]
expected_positions = torch.tensor(
[
[[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5]],
[[0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5]],
[[0, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5]],
]
)
self.assertListEqual(list(position_ids.shape), [3, 1, 14])
self.assertListEqual(position_ids.tolist(), expected_positions.tolist())
# Check video position ids with 2 frames, and 4 height, 3 width patches (= 12 * 2 tokens)
video_token_id = config.video_token_id
input_ids = torch.tensor(
[[pad_token_id] + [video_token_id] * 12 + [pad_token_id] + [video_token_id] * 12 + [pad_token_id]],
device=torch_device,
)
mm_token_type_ids = torch.tensor([[0] + [2] * 12 + [0] + [2] * 12 + [0]], device=torch_device)
video_grid_thw = torch.tensor([[2, 4, 3]], device=torch_device)
position_ids = model.get_rope_index(input_ids, mm_token_type_ids, video_grid_thw=video_grid_thw)[0]
expected_positions = torch.tensor(
[
[[0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 10]],
[[0, 1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 5, 6, 6, 6, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10]],
[[0, 1, 2, 3, 1, 2, 3, 1, 2, 3, 1, 2, 3, 5, 6, 7, 8, 6, 7, 8, 6, 7, 8, 6, 7, 8, 10]],
]
)
self.assertListEqual(list(position_ids.shape), [3, 1, 27])
self.assertListEqual(position_ids.tolist(), expected_positions.tolist())
def test_mismatching_num_image_tokens(self):
# Override the base test because we need to slice image_grid_thw too
config, input_dict = self.model_tester.prepare_config_and_inputs_for_common()
for model_class in self.all_model_classes:
model = model_class(config).to(torch_device)
model.eval()
_ = model(**input_dict) # successful forward with no modifications
curr_input_dict = copy.deepcopy(input_dict)
# remove one image but leave the image token in text
patch_size = config.vision_config.patch_size
one_img_length = (self.model_tester.image_size**2) // (patch_size**2)
curr_input_dict["pixel_values"] = curr_input_dict["pixel_values"][-one_img_length:, ...]
curr_input_dict["image_grid_thw"] = curr_input_dict["image_grid_thw"][-1:, ...]
with self.assertRaises(ValueError):
_ = model(**curr_input_dict)
model.base_model.rope_deltas = None
# simulate multi-image case by concatenating inputs where each has exactly one image/image-token
input_ids = curr_input_dict["input_ids"][:1]
pixel_values = curr_input_dict["pixel_values"][:one_img_length]
image_grid_thw = curr_input_dict["image_grid_thw"][:1]
mm_token_type_ids = curr_input_dict["mm_token_type_ids"][:1]
input_ids = torch.cat([input_ids, input_ids], dim=0)
# one image and two image tokens raise an error
with self.assertRaises(ValueError):
_ = model(
input_ids=input_ids,
pixel_values=pixel_values,
image_grid_thw=image_grid_thw,
mm_token_type_ids=torch.cat([mm_token_type_ids, mm_token_type_ids], dim=0),
)
model.base_model.rope_deltas = None
# two images and two image tokens don't raise an error
pixel_values = torch.cat([pixel_values, pixel_values], dim=0)
image_grid_thw = torch.cat([image_grid_thw, image_grid_thw], dim=0)
mm_token_type_ids = torch.cat(
[curr_input_dict["mm_token_type_ids"][:1], curr_input_dict["mm_token_type_ids"][:1]], dim=0
)
_ = model(
input_ids=input_ids,
pixel_values=pixel_values,
image_grid_thw=image_grid_thw,
mm_token_type_ids=mm_token_type_ids,
)
def test_image_forward(self):
config, _ = self.model_tester.prepare_config_and_inputs_for_common()
B = self.model_tester.batch_size
C = config.vision_config.in_channels
T = config.vision_config.temporal_patch_size
P = config.vision_config.patch_size
num_images = 2
input_ids = ids_tensor([B, self.model_tester.seq_length], self.model_tester.vocab_size)
input_ids[:, -1] = self.model_tester.pad_token_id
input_ids[input_ids == self.model_tester.video_token_id] = self.model_tester.pad_token_id
input_ids[input_ids == self.model_tester.image_token_id] = self.model_tester.pad_token_id
input_ids[input_ids == self.model_tester.vision_start_token_id] = self.model_tester.pad_token_id
input_ids[input_ids == self.model_tester.vision_end_token_id] = self.model_tester.pad_token_id
# For this tiny config, each image corresponds to one patch token.
patches_per_image = 1
pixel_values = floats_tensor(
[
B * num_images * patches_per_image,
C * T * (P**2),
]
)
image_grid_thw = torch.tensor([[1, 1, 1]] * (B * num_images), device=torch_device)
self.assertEqual(pixel_values.shape[0], image_grid_thw.prod(dim=1).sum().item())
insertion_point = 0
tokens_per_image = 3 # vision_start + image_token + vision_end
required_seq_length = insertion_point + num_images * tokens_per_image
self.assertLessEqual(required_seq_length, input_ids.shape[1])
for b in range(B):
for image_idx in range(num_images):
image_start = insertion_point + image_idx * tokens_per_image
input_ids[b, image_start] = self.model_tester.vision_start_token_id
input_ids[b, image_start + 1] = self.model_tester.image_token_id
input_ids[b, image_start + 2] = self.model_tester.vision_end_token_id
mm_token_type_ids = torch.zeros_like(input_ids)
mm_token_type_ids[input_ids == self.model_tester.image_token_id] = 1
for model_class in self.all_model_classes:
model = model_class(config).to(torch_device)
outputs = model(
input_ids=input_ids,
pixel_values=pixel_values,
image_grid_thw=image_grid_thw,
mm_token_type_ids=mm_token_type_ids,
)
self.assertIsNotNone(outputs)
def test_video_forward(self):
config, _ = self.model_tester.prepare_config_and_inputs_for_common()
B = self.model_tester.batch_size
C = config.vision_config.in_channels
T = config.vision_config.temporal_patch_size
P = config.vision_config.patch_size
input_ids = ids_tensor([B, self.model_tester.seq_length], self.model_tester.vocab_size)
F = 4
num_video = 2
frame_timestamp_tokens = 5
patch_H = self.model_tester.image_size // P
patch_W = self.model_tester.image_size // P
patch_T = F // T
patches_per_video = patch_T * patch_H * patch_W
pathed_per_frame = patch_H * patch_W
pixel_values_videos = floats_tensor(
[
# first dim: batch_size * num_patches
B * num_video * patches_per_video,
# second dim: in_channels * temporal_patch_size * patch_size^2
C * T * (P**2),
]
)
video_grid_thw = torch.tensor([[patch_T, patch_H, patch_W]] * (B * num_video), device=torch_device)
# sanity check
self.assertEqual(pixel_values_videos.shape[0], video_grid_thw.prod(dim=1).sum().item())
# Insert video token sequence
input_ids[:, -1] = self.model_tester.pad_token_id
input_ids[input_ids == self.model_tester.video_token_id] = self.model_tester.pad_token_id
input_ids[input_ids == self.model_tester.image_token_id] = self.model_tester.pad_token_id
input_ids[input_ids == self.model_tester.vision_start_token_id] = self.model_tester.pad_token_id
input_ids[input_ids == self.model_tester.vision_end_token_id] = self.model_tester.pad_token_id
insertion_point = 0
tokens_per_frame = frame_timestamp_tokens + 1 + pathed_per_frame + 1
tokens_per_video = patch_T * tokens_per_frame
required_seq_length = insertion_point + num_video * tokens_per_video
if required_seq_length > input_ids.shape[1]:
pad_extension = torch.full(
(B, required_seq_length - input_ids.shape[1]),
self.model_tester.pad_token_id,
dtype=input_ids.dtype,
device=input_ids.device,
)
input_ids = torch.cat([input_ids, pad_extension], dim=1)
timestamp_start_token_id = self.model_tester.vision_end_token_id + 1
self.assertLessEqual(timestamp_start_token_id + frame_timestamp_tokens, self.model_tester.vocab_size)
timestamp_token_ids = torch.arange(
timestamp_start_token_id,
timestamp_start_token_id + frame_timestamp_tokens,
device=input_ids.device,
dtype=input_ids.dtype,
)
self.assertLessEqual(required_seq_length, input_ids.shape[1])
for b in range(B):
for video_idx in range(num_video):
video_start = insertion_point + video_idx * tokens_per_video
for frame_idx in range(patch_T):
frame_start = video_start + frame_idx * tokens_per_frame
input_ids[b, frame_start : frame_start + frame_timestamp_tokens] = timestamp_token_ids
vision_start_pos = frame_start + frame_timestamp_tokens
input_ids[b, vision_start_pos] = self.model_tester.vision_start_token_id
frame_token_start = vision_start_pos + 1
frame_token_end = frame_token_start + pathed_per_frame
input_ids[b, frame_token_start:frame_token_end] = self.model_tester.video_token_id
input_ids[b, frame_token_end] = self.model_tester.vision_end_token_id
# build mm_token_type_ids
mm_token_type_ids = torch.zeros_like(input_ids)
mm_token_type_ids[input_ids == self.model_tester.video_token_id] = 2
for model_class in self.all_model_classes:
# TODO:we should remove this because we use timestamps for video
model = model_class(config).to(torch_device)
outputs = model(
input_ids=input_ids,
pixel_values_videos=pixel_values_videos,
video_grid_thw=video_grid_thw,
mm_token_type_ids=mm_token_type_ids,
)
self.assertIsNotNone(outputs)
@require_torch
class Qwen3VLTextModelPositionIdsTest(unittest.TestCase):
"""Regression tests for text_position_ids extraction (PR #44158)."""
def get_text_config(self):
return Qwen3VLTextConfig(
vocab_size=99,
hidden_size=32,
intermediate_size=37,
num_hidden_layers=2,
num_attention_heads=4,
num_key_value_heads=2,
head_dim=8,
hidden_act="silu",
max_position_embeddings=512,
rope_parameters={"rope_type": "default", "mrope_section": [16, 8, 8], "mrope_interleaved": True},
)
def _make_vision_position_ids(self, batch_size, seq_len):
"""Create 3D vision position_ids (temporal=0, height=arange, width=arange)."""
pos = torch.zeros(3, batch_size, seq_len, dtype=torch.long, device=torch_device)
pos[1] = torch.arange(seq_len, device=torch_device).unsqueeze(0).expand(batch_size, -1)
pos[2] = torch.arange(seq_len, device=torch_device).unsqueeze(0).expand(batch_size, -1)
return pos
def test_3d_vision_position_ids_no_cache(self):
config = self.get_text_config()
model = Qwen3VLTextModel(config).to(torch_device).eval()
batch_size, seq_len = 2, 10
input_ids = ids_tensor([batch_size, seq_len], config.vocab_size).to(torch_device)
vision_position_ids = self._make_vision_position_ids(batch_size, seq_len)
with torch.no_grad():
output = model(input_ids=input_ids, position_ids=vision_position_ids, use_cache=False)
self.assertEqual(output.last_hidden_state.shape, (batch_size, seq_len, config.hidden_size))
def test_3d_vision_position_ids_produce_finite_output(self):
config = self.get_text_config()
model = Qwen3VLTextModel(config).to(torch_device).eval()
batch_size, seq_len = 2, 8
input_ids = ids_tensor([batch_size, seq_len], config.vocab_size).to(torch_device)
vision_position_ids = self._make_vision_position_ids(batch_size, seq_len)
with torch.no_grad():
output_3d = model(input_ids=input_ids, position_ids=vision_position_ids, use_cache=False)
output_none = model(input_ids=input_ids, position_ids=None, use_cache=False)
self.assertTrue(torch.isfinite(output_3d.last_hidden_state).all())
self.assertTrue(torch.isfinite(output_none.last_hidden_state).all())
def test_4d_position_ids_forward(self):
config = self.get_text_config()
model = Qwen3VLTextModel(config).to(torch_device).eval()
batch_size, seq_len = 2, 8
input_ids = ids_tensor([batch_size, seq_len], config.vocab_size).to(torch_device)
text_pos = torch.arange(seq_len, device=torch_device).unsqueeze(0).expand(batch_size, -1)
spatial_pos = torch.arange(seq_len, device=torch_device).unsqueeze(0).expand(batch_size, -1)
zero_pos = torch.zeros(batch_size, seq_len, dtype=torch.long, device=torch_device)
position_ids_4d = torch.stack([text_pos, zero_pos, spatial_pos, spatial_pos], dim=0)
with torch.no_grad():
output = model(input_ids=input_ids, position_ids=position_ids_4d, use_cache=False)
self.assertEqual(output.last_hidden_state.shape, (batch_size, seq_len, config.hidden_size))
self.assertTrue(torch.isfinite(output.last_hidden_state).all())
def test_use_cache_true_vs_false_with_vision_position_ids(self):
"""use_cache should not affect output when 3D vision position_ids are provided."""
config = self.get_text_config()
model = Qwen3VLTextModel(config).to(torch_device).eval()
batch_size, seq_len = 1, 12
input_ids = ids_tensor([batch_size, seq_len], config.vocab_size).to(torch_device)
vision_position_ids = self._make_vision_position_ids(batch_size, seq_len)
with torch.no_grad():
output_cached = model(input_ids=input_ids, position_ids=vision_position_ids.clone(), use_cache=True)
output_no_cache = model(input_ids=input_ids, position_ids=vision_position_ids.clone(), use_cache=False)
torch.testing.assert_close(
output_cached.last_hidden_state, output_no_cache.last_hidden_state, atol=1e-5, rtol=1e-5
)
def test_2d_position_ids_forward(self):
config = self.get_text_config()
model = Qwen3VLTextModel(config).to(torch_device).eval()
batch_size, seq_len = 2, 8
input_ids = ids_tensor([batch_size, seq_len], config.vocab_size).to(torch_device)
position_ids_2d = torch.arange(seq_len, device=torch_device).unsqueeze(0).expand(batch_size, -1)
with torch.no_grad():
output = model(input_ids=input_ids, position_ids=position_ids_2d, use_cache=False)
self.assertEqual(output.last_hidden_state.shape, (batch_size, seq_len, config.hidden_size))
self.assertTrue(torch.isfinite(output.last_hidden_state).all())

View File

@@ -0,0 +1,284 @@
# Copyright 2025 The Qwen Team and The HuggingFace Inc. team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import numpy as np
from transformers.testing_utils import require_av, require_torch, require_torchvision, require_vision
from transformers.utils import is_torch_available, is_vision_available
from ...test_processing_common import ProcessorTesterMixin, url_to_local_path
if is_vision_available():
from transformers import Qwen3VLProcessor
if is_torch_available():
import torch
@require_vision
@require_torch
@require_torchvision
class Qwen3VLProcessorTest(ProcessorTesterMixin, unittest.TestCase):
processor_class = Qwen3VLProcessor
model_id = "Qwen/Qwen3-VL-235B-A22B-Instruct"
video_unstructured_max_length = 870
video_text_kwargs_max_length = 870
video_text_kwargs_override_max_length = 870
@classmethod
def _setup_from_pretrained(cls, model_id, **kwargs):
return super()._setup_from_pretrained(model_id, patch_size=4, max_pixels=56 * 56, min_pixels=28 * 28, **kwargs)
@classmethod
def _setup_test_attributes(cls, processor):
cls.image_token = processor.image_token
cls.video_token = processor.video_token
def test_get_num_vision_tokens(self):
"Tests general functionality of the helper used internally in vLLM"
processor = self.get_processor()
output = processor._get_num_multimodal_tokens(image_sizes=[(100, 100), (300, 100), (500, 30)])
self.assertTrue("num_image_tokens" in output)
self.assertEqual(len(output["num_image_tokens"]), 3)
self.assertTrue("num_image_patches" in output)
self.assertEqual(len(output["num_image_patches"]), 3)
def test_model_input_names(self):
processor = self.get_processor()
text = self.prepare_text_inputs(modalities=["image", "video"])
image_input = self.prepare_image_inputs()
video_inputs = self.prepare_video_inputs()
inputs_dict = {"text": text, "images": image_input, "videos": video_inputs}
inputs = processor(**inputs_dict, return_tensors="pt", do_sample_frames=False)
self.assertSetEqual(set(inputs.keys()), set(processor.model_input_names))
@require_torch
@require_av
def _test_apply_chat_template(
self,
modality: str,
batch_size: int,
return_tensors: str,
input_name: str,
processor_name: str,
input_data: list[str],
):
processor = self.get_processor()
if processor.chat_template is None:
self.skipTest("Processor has no chat template")
if processor_name not in self.processor_class.get_attributes():
self.skipTest(f"{processor_name} attribute not present in {self.processor_class}")
batch_messages = [
[
{
"role": "user",
"content": [{"type": "text", "text": "Describe this."}],
},
]
] * batch_size
# Test that jinja can be applied
formatted_prompt = processor.apply_chat_template(batch_messages, add_generation_prompt=True, tokenize=False)
self.assertEqual(len(formatted_prompt), batch_size)
# Test that tokenizing with template and directly with `self.tokenizer` gives same output
formatted_prompt_tokenized = processor.apply_chat_template(
batch_messages, add_generation_prompt=True, tokenize=True, return_tensors=return_tensors
)
add_special_tokens = True
if processor.tokenizer.bos_token is not None and formatted_prompt[0].startswith(processor.tokenizer.bos_token):
add_special_tokens = False
tok_output = processor.tokenizer(
formatted_prompt, return_tensors=return_tensors, add_special_tokens=add_special_tokens
)
expected_output = tok_output.input_ids
self.assertListEqual(expected_output.tolist(), formatted_prompt_tokenized.tolist())
# Test that kwargs passed to processor's `__call__` are actually used
tokenized_prompt_100 = processor.apply_chat_template(
batch_messages,
add_generation_prompt=True,
tokenize=True,
padding="max_length",
truncation=True,
return_tensors=return_tensors,
max_length=100,
)
self.assertEqual(len(tokenized_prompt_100[0]), 100)
# Test that `return_dict=True` returns text related inputs in the dict
out_dict_text = processor.apply_chat_template(
batch_messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors=return_tensors,
)
self.assertTrue(all(key in out_dict_text for key in ["input_ids", "attention_mask"]))
self.assertEqual(len(out_dict_text["input_ids"]), batch_size)
self.assertEqual(len(out_dict_text["attention_mask"]), batch_size)
# Test that with modality URLs and `return_dict=True`, we get modality inputs in the dict
for idx, url in enumerate(input_data[:batch_size]):
batch_messages[idx][0]["content"] = [batch_messages[idx][0]["content"][0], {"type": modality, "url": url}]
out_dict = processor.apply_chat_template(
batch_messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
return_tensors=return_tensors,
max_frames=2, # by default no more than 2 frames, otherwise too slow
)
input_name = getattr(self, input_name)
self.assertTrue(input_name in out_dict)
self.assertEqual(len(out_dict["input_ids"]), batch_size)
self.assertEqual(len(out_dict["attention_mask"]), batch_size)
if modality == "video":
# qwen pixels don't scale with bs same way as other models, calculate expected video token count based on video_grid_thw
expected_video_token_count = 0
for thw in out_dict["video_grid_thw"]:
expected_video_token_count += thw[0] * thw[1] * thw[2]
mm_len = expected_video_token_count
else:
mm_len = batch_size * 192
self.assertEqual(len(out_dict[input_name]), mm_len)
return_tensor_to_type = {"pt": torch.Tensor, "np": np.ndarray, None: list}
for k in out_dict:
self.assertIsInstance(out_dict[k], return_tensor_to_type[return_tensors])
@require_av
def test_apply_chat_template_video_frame_sampling(self):
processor = self.get_processor()
if processor.chat_template is None:
self.skipTest("Processor has no chat template")
messages = [
[
{
"role": "user",
"content": [
{
"type": "video",
"url": url_to_local_path(
"https://huggingface.co/datasets/raushan-testing-hf/videos-test/resolve/main/tiny_video.mp4"
),
},
{"type": "text", "text": "What is shown in this video?"},
],
},
]
]
formatted_prompt = processor.apply_chat_template(messages, add_generation_prompt=True, tokenize=False)
self.assertEqual(len(formatted_prompt), 1)
# for fast test, set the longest edge to 8192
processor.video_processor.size.longest_edge = 8192
# Add video URL for return dict and load with `num_frames` arg
num_frames = 3
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
num_frames=num_frames,
fps=None, # if pass num_frames, fps should be None
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 256)
# Load with `fps` arg
fps = 1
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
fps=fps,
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 224)
# Load with `fps` and `num_frames` args, should raise an error
with self.assertRaises(ValueError):
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
fps=fps,
num_frames=num_frames,
)
# Load without any arg should load the whole video
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 224)
# Load video as a list of frames (i.e. images). NOTE: each frame should have same size
# because we assume they come from one video
messages[0][0]["content"][0] = {
"type": "video",
"url": [
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/australia.jpg",
"https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/transformers/tasks/australia.jpg",
],
}
out_dict_with_video = processor.apply_chat_template(
messages,
add_generation_prompt=True,
tokenize=True,
return_dict=True,
do_sample_frames=False,
)
self.assertTrue(self.videos_input_name in out_dict_with_video)
self.assertEqual(len(out_dict_with_video[self.videos_input_name]), 216)
def test_kwargs_overrides_custom_image_processor_kwargs(self):
processor = self.get_processor()
self.skip_processor_without_typed_kwargs(processor)
input_str = self.prepare_text_inputs()
image_input = self.prepare_image_inputs()
inputs = processor(text=input_str, images=image_input, max_pixels=56 * 56 * 4, return_tensors="pt")
self.assertEqual(inputs[self.images_input_name].shape[0], 612)
inputs = processor(text=input_str, images=image_input, return_tensors="pt")
self.assertEqual(inputs[self.images_input_name].shape[0], 100)
@unittest.skip("qwen3_vl can't sample frames from image frames directly, user can use `qwen-vl-utils`")
def test_apply_chat_template_video_1(self):
pass
@unittest.skip("qwen3_vl can't sample frames from image frames directly, user can use `qwen-vl-utils`")
def test_apply_chat_template_video_2(self):
pass

View File

@@ -0,0 +1,348 @@
# Copyright 2025 HuggingFace Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import unittest
import numpy as np
from transformers.image_utils import IMAGENET_STANDARD_MEAN, IMAGENET_STANDARD_STD
from transformers.testing_utils import require_torch, require_vision
from transformers.utils import is_torch_available, is_torchvision_available, is_vision_available
from ...test_video_processing_common import VideoProcessingTestMixin, prepare_video_inputs
if is_torch_available():
from PIL import Image
if is_vision_available() and is_torchvision_available():
from transformers import Qwen3VLVideoProcessor
from transformers.models.qwen3_vl.video_processing_qwen3_vl import smart_resize
class Qwen3VLVideoProcessingTester:
def __init__(
self,
parent,
batch_size=5,
num_frames=8,
num_channels=3,
min_resolution=32,
max_resolution=80,
temporal_patch_size=2,
patch_size=16,
merge_size=2,
do_resize=True,
size=None,
do_normalize=True,
image_mean=IMAGENET_STANDARD_MEAN,
image_std=IMAGENET_STANDARD_STD,
do_convert_rgb=True,
):
size = size if size is not None else {"longest_edge": 20, "shortest_edge": 10}
self.parent = parent
self.batch_size = batch_size
self.num_frames = num_frames
self.num_channels = num_channels
self.min_resolution = min_resolution
self.max_resolution = max_resolution
self.do_resize = do_resize
self.size = size
self.do_normalize = do_normalize
self.image_mean = image_mean
self.image_std = image_std
self.do_convert_rgb = do_convert_rgb
self.temporal_patch_size = temporal_patch_size
self.patch_size = patch_size
self.merge_size = merge_size
def prepare_video_processor_dict(self):
return {
"do_resize": self.do_resize,
"size": self.size,
"do_normalize": self.do_normalize,
"image_mean": self.image_mean,
"image_std": self.image_std,
"do_convert_rgb": self.do_convert_rgb,
"do_sample_frames": True,
}
def prepare_video_metadata(self, videos):
video_metadata = []
for video in videos:
if isinstance(video, list):
num_frames = len(video)
elif hasattr(video, "shape"):
if len(video.shape) == 4: # (T, H, W, C)
num_frames = video.shape[0]
else:
num_frames = 1
else:
num_frames = self.num_frames
metadata = {
"fps": 2,
"duration": num_frames / 2,
"total_num_frames": num_frames,
}
video_metadata.append(metadata)
return video_metadata
def expected_output_video_shape(self, videos):
grid_t = self.num_frames // self.temporal_patch_size
hidden_dim = self.num_channels * self.temporal_patch_size * self.patch_size * self.patch_size
seq_len = 0
for video in videos:
if isinstance(video, list) and isinstance(video[0], Image.Image):
video = np.stack([np.array(frame) for frame in video])
elif hasattr(video, "shape"):
pass
else:
video = np.array(video)
if hasattr(video, "shape") and len(video.shape) >= 3:
if len(video.shape) == 4:
t, height, width = video.shape[:3]
elif len(video.shape) == 3:
height, width = video.shape[:2]
t = 1
else:
t, height, width = self.num_frames, self.min_resolution, self.min_resolution
else:
t, height, width = self.num_frames, self.min_resolution, self.min_resolution
resized_height, resized_width = smart_resize(
t,
height,
width,
factor=self.patch_size * self.merge_size,
min_pixels=self.size["shortest_edge"],
max_pixels=self.size["longest_edge"],
)
grid_h, grid_w = resized_height // self.patch_size, resized_width // self.patch_size
seq_len += grid_t * grid_h * grid_w
return [seq_len, hidden_dim]
def prepare_video_inputs(self, equal_resolution=False, return_tensors="pil"):
videos = prepare_video_inputs(
batch_size=self.batch_size,
num_frames=self.num_frames,
num_channels=self.num_channels,
min_resolution=self.min_resolution,
max_resolution=self.max_resolution,
equal_resolution=equal_resolution,
return_tensors=return_tensors,
)
return videos
@require_torch
@require_vision
class Qwen3VLVideoProcessingTest(VideoProcessingTestMixin, unittest.TestCase):
fast_video_processing_class = Qwen3VLVideoProcessor if is_torchvision_available() else None
input_name = "pixel_values_videos"
def setUp(self):
super().setUp()
self.video_processor_tester = Qwen3VLVideoProcessingTester(self)
@property
def video_processor_dict(self):
return self.video_processor_tester.prepare_video_processor_dict()
def test_video_processor_from_dict_with_kwargs(self):
video_processor = self.fast_video_processing_class.from_dict(self.video_processor_dict)
self.assertEqual(video_processor.size, {"longest_edge": 20, "shortest_edge": 10})
video_processor = self.fast_video_processing_class.from_dict(
self.video_processor_dict, size={"longest_edge": 42, "shortest_edge": 42}
)
self.assertEqual(video_processor.size, {"longest_edge": 42, "shortest_edge": 42})
def test_call_pil(self):
for video_processing_class in self.video_processor_list:
video_processing = video_processing_class(**self.video_processor_dict)
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False, return_tensors="pil"
)
for video in video_inputs:
self.assertIsInstance(video[0], Image.Image)
video_metadata = self.video_processor_tester.prepare_video_metadata(video_inputs)
encoded_videos = video_processing(
video_inputs[0], video_metadata=[video_metadata[0]], return_tensors="pt"
)[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
encoded_videos = video_processing(video_inputs, video_metadata=video_metadata, return_tensors="pt")[
self.input_name
]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
def test_call_numpy(self):
for video_processing_class in self.video_processor_list:
video_processing = video_processing_class(**self.video_processor_dict)
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False, return_tensors="np"
)
video_metadata = self.video_processor_tester.prepare_video_metadata(video_inputs)
encoded_videos = video_processing(
video_inputs[0], video_metadata=[video_metadata[0]], return_tensors="pt"
)[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
encoded_videos = video_processing(video_inputs, video_metadata=video_metadata, return_tensors="pt")[
self.input_name
]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
def test_call_pytorch(self):
for video_processing_class in self.video_processor_list:
video_processing = video_processing_class(**self.video_processor_dict)
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False, return_tensors="pt"
)
video_metadata = self.video_processor_tester.prepare_video_metadata(video_inputs)
encoded_videos = video_processing(
video_inputs[0], video_metadata=[video_metadata[0]], return_tensors="pt"
)[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
encoded_videos = video_processing(video_inputs, video_metadata=video_metadata, return_tensors="pt")[
self.input_name
]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
@unittest.skip("Skip for now, the test needs adjustment for Qwen3VL")
def test_call_numpy_4_channels(self):
for video_processing_class in self.video_processor_list:
# Test that can process videos which have an arbitrary number of channels
# Initialize video_processing
video_processor = video_processing_class(**self.video_processor_dict)
# create random numpy tensors
self.video_processor_tester.num_channels = 4
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False, return_tensors="np"
)
# Test not batched input
encoded_videos = video_processor(
video_inputs[0],
return_tensors="pt",
input_data_format="channels_last",
image_mean=(0.0, 0.0, 0.0, 0.0),
image_std=(1.0, 1.0, 1.0, 1.0),
)[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
# Test batched
encoded_videos = video_processor(
video_inputs,
return_tensors="pt",
input_data_format="channels_last",
image_mean=(0.0, 0.0, 0.0, 0.0),
image_std=(1.0, 1.0, 1.0, 1.0),
)[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
def test_nested_input(self):
"""Tests that the processor can work with nested list where each video is a list of arrays"""
for video_processing_class in self.video_processor_list:
video_processing = video_processing_class(**self.video_processor_dict)
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False, return_tensors="np"
)
video_inputs_nested = [list(video) for video in video_inputs]
video_metadata = self.video_processor_tester.prepare_video_metadata(video_inputs)
# Test not batched input
encoded_videos = video_processing(
video_inputs_nested[0], video_metadata=[video_metadata[0]], return_tensors="pt"
)[self.input_name]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape([video_inputs[0]])
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
# Test batched
encoded_videos = video_processing(video_inputs_nested, video_metadata=video_metadata, return_tensors="pt")[
self.input_name
]
expected_output_video_shape = self.video_processor_tester.expected_output_video_shape(video_inputs)
self.assertEqual(list(encoded_videos.shape), expected_output_video_shape)
def test_call_sample_frames(self):
for video_processing_class in self.video_processor_list:
video_processor_dict = self.video_processor_dict.copy()
video_processing = video_processing_class(**video_processor_dict)
prev_num_frames = self.video_processor_tester.num_frames
self.video_processor_tester.num_frames = 8
prev_min_resolution = getattr(self.video_processor_tester, "min_resolution", None)
prev_max_resolution = getattr(self.video_processor_tester, "max_resolution", None)
self.video_processor_tester.min_resolution = 56
self.video_processor_tester.max_resolution = 112
video_inputs = self.video_processor_tester.prepare_video_inputs(
equal_resolution=False,
return_tensors="torch",
)
metadata = [[{"total_num_frames": 8, "fps": 4}]]
batched_metadata = metadata * len(video_inputs)
encoded_videos = video_processing(video_inputs[0], return_tensors="pt", video_metadata=metadata)[
self.input_name
]
encoded_videos_batched = video_processing(
video_inputs, return_tensors="pt", video_metadata=batched_metadata
)[self.input_name]
self.assertIsNotNone(encoded_videos)
self.assertIsNotNone(encoded_videos_batched)
self.assertEqual(len(encoded_videos.shape), 2)
self.assertEqual(len(encoded_videos_batched.shape), 2)
self.video_processor_tester.num_frames = prev_num_frames
if prev_min_resolution is not None:
self.video_processor_tester.min_resolution = prev_min_resolution
if prev_max_resolution is not None:
self.video_processor_tester.max_resolution = prev_max_resolution
def test_num_frames_equal_temporal_patch_size_plus_two(self):
for video_processing_class in self.video_processor_list:
video_processor_dict = self.video_processor_dict.copy()
video_processor_dict["size"] = {"longest_edge": 5 * 32 * 32, "shortest_edge": 32 * 32}
video_processor_dict["do_sample_frames"] = False
temporal_patch_size = 3
video_processor_dict["temporal_patch_size"] = temporal_patch_size
video_processing = video_processing_class(**video_processor_dict)
n, w, h = 5, 32, 32
video_inputs = [(np.random.randint(0, 256, (h, w, 3), dtype=np.uint8)) for _ in range(n)]
video_processed = video_processing(video_inputs, return_tensors="pt")
encoded_videos = video_processed[self.input_name]
self.assertEqual(list(encoded_videos.shape), [8, temporal_patch_size * 3 * 16 * 16])
video_grid_thw = video_processed["video_grid_thw"]
self.assertEqual(video_grid_thw.tolist(), [[2, 2, 2]])