139 lines
4.9 KiB
Python
139 lines
4.9 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
|
"""Unit tests for MiniMax-M3 VL ``max_long_side_pixel`` resize support.
|
|
|
|
These exercise the vendored processor directly (no checkpoint / GPU needed), so
|
|
they validate the long-side resize spec and the resulting prompt-token counts
|
|
deterministically.
|
|
"""
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from vllm.transformers_utils.processors.minimax_m3 import (
|
|
IMAGE_MAX_TOTAL_PIXELS,
|
|
MIN_SHORT_SIDE_PIXEL,
|
|
VIDEO_MAX_TOTAL_PIXELS,
|
|
MiniMaxM3VLImageProcessor,
|
|
MiniMaxM3VLVideoProcessor,
|
|
smart_resize,
|
|
)
|
|
|
|
# Long sides are multiples of patch_size*merge_size (28) so the rounding is
|
|
# exact and the expected token counts are unambiguous.
|
|
LONG_SIDES = [252, 504, 1008]
|
|
MERGE2 = 2**2 # merge_size ** 2
|
|
|
|
|
|
def _image_tokens(grid_thw) -> int:
|
|
g = list(grid_thw)
|
|
return int(g[0] * g[1] * g[2]) // MERGE2
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# smart_resize: the long-side spec (a) shrink / (b) enlarge / (c) hard cap
|
|
# --------------------------------------------------------------------------- #
|
|
def test_smart_resize_long_side_shrink():
|
|
# (a) long side exceeds the cap -> shrink so the long side equals the cap.
|
|
h, w = smart_resize(
|
|
2048, 1024, factor=28, max_long_side_pixel=1008, max_total_pixels=10**9
|
|
)
|
|
assert max(h, w) == 1008
|
|
assert (h, w) == (1008, 504) # aspect ratio preserved
|
|
|
|
|
|
def test_smart_resize_short_side_enlarge():
|
|
# (b) long side within the cap but short side below the floor -> enlarge so
|
|
# the short side reaches min_short_side_pixel.
|
|
h, w = smart_resize(
|
|
200, 40, factor=28, max_long_side_pixel=1008, max_total_pixels=10**9
|
|
)
|
|
assert min(h, w) == MIN_SHORT_SIDE_PIXEL # 112
|
|
|
|
|
|
def test_smart_resize_total_pixels_raises():
|
|
# (c) still over the area cap after resizing -> raise instead of inferring.
|
|
with pytest.raises(ValueError, match="max_total_pixels"):
|
|
smart_resize(
|
|
5000,
|
|
5000,
|
|
factor=28,
|
|
max_long_side_pixel=4000,
|
|
max_total_pixels=IMAGE_MAX_TOTAL_PIXELS,
|
|
)
|
|
|
|
|
|
def test_smart_resize_backward_compatible_area_bound():
|
|
# Without max_long_side_pixel the original Qwen-style area bound is used.
|
|
assert smart_resize(2048, 2048, factor=28, max_pixels=451584) == (672, 672)
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Image processor: monotonic prompt-token counts for 252 < 504 < 1008
|
|
# --------------------------------------------------------------------------- #
|
|
def test_image_tokens_increase_with_max_long_side_pixel():
|
|
proc = MiniMaxM3VLImageProcessor()
|
|
counts = []
|
|
for long_side in LONG_SIDES:
|
|
patches = proc.get_number_of_image_patches(
|
|
2048, 2048, images_kwargs={"max_long_side_pixel": long_side}
|
|
)
|
|
counts.append(patches // MERGE2)
|
|
|
|
assert counts == [81, 324, 1296]
|
|
assert counts[0] < counts[1] < counts[2]
|
|
|
|
|
|
def test_image_processor_defaults_match_spec():
|
|
proc = MiniMaxM3VLImageProcessor()
|
|
assert proc.max_long_side_pixel is None # opt-in
|
|
assert proc.min_short_side_pixel == MIN_SHORT_SIDE_PIXEL
|
|
assert proc.max_total_pixels == IMAGE_MAX_TOTAL_PIXELS
|
|
|
|
|
|
def test_image_preprocess_pipeline_monotonic():
|
|
proc = MiniMaxM3VLImageProcessor()
|
|
image = torch.randint(0, 255, (3, 2048, 2048), dtype=torch.uint8)
|
|
counts = []
|
|
for long_side in LONG_SIDES:
|
|
out = proc.preprocess(
|
|
[image],
|
|
do_resize=True,
|
|
max_long_side_pixel=long_side,
|
|
return_tensors="pt",
|
|
)
|
|
counts.append(_image_tokens(out["image_grid_thw"][0]))
|
|
assert counts == [81, 324, 1296]
|
|
|
|
|
|
# --------------------------------------------------------------------------- #
|
|
# Video processor: same monotonic behavior + volumetric (w*h*frames) cap
|
|
# --------------------------------------------------------------------------- #
|
|
def test_video_tokens_increase_with_max_long_side_pixel():
|
|
proc = MiniMaxM3VLVideoProcessor()
|
|
assert proc.max_total_pixels == VIDEO_MAX_TOTAL_PIXELS
|
|
video = torch.randint(0, 255, (4, 3, 2048, 2048), dtype=torch.uint8)
|
|
counts = []
|
|
for long_side in LONG_SIDES:
|
|
out = proc.preprocess(
|
|
videos=[video],
|
|
do_resize=True,
|
|
max_long_side_pixel=long_side,
|
|
return_tensors="pt",
|
|
)
|
|
counts.append(_image_tokens(out["video_grid_thw"][0]))
|
|
assert counts[0] < counts[1] < counts[2]
|
|
|
|
|
|
def test_video_volumetric_cap_raises():
|
|
proc = MiniMaxM3VLVideoProcessor()
|
|
# 400 frames at a 1008-long-side square: 1008*1008*400 >> 301,056,000.
|
|
video = torch.randint(0, 255, (400, 3, 2048, 2048), dtype=torch.uint8)
|
|
with pytest.raises(ValueError, match="max_total_pixels"):
|
|
proc.preprocess(
|
|
videos=[video],
|
|
do_resize=True,
|
|
max_long_side_pixel=1008,
|
|
return_tensors="pt",
|
|
)
|