main
py 138 lines 4.9 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 """Unit tests for MiniMax-M3 VL ``max_long_side_pixel`` resize support.
4
5 These exercise the vendored processor directly (no checkpoint / GPU needed), so
6 they validate the long-side resize spec and the resulting prompt-token counts
7 deterministically.
8 """
9
10 import pytest
11 import torch
12
13 from vllm.transformers_utils.processors.minimax_m3 import (
14 IMAGE_MAX_TOTAL_PIXELS,
15 MIN_SHORT_SIDE_PIXEL,
16 VIDEO_MAX_TOTAL_PIXELS,
17 MiniMaxM3VLImageProcessor,
18 MiniMaxM3VLVideoProcessor,
19 smart_resize,
20 )
21
22 # Long sides are multiples of patch_size*merge_size (28) so the rounding is
23 # exact and the expected token counts are unambiguous.
24 LONG_SIDES = [252, 504, 1008]
25 MERGE2 = 2**2 # merge_size ** 2
26
27
28 def _image_tokens(grid_thw) -> int:
29 g = list(grid_thw)
30 return int(g[0] * g[1] * g[2]) // MERGE2
31
32
33 # --------------------------------------------------------------------------- #
34 # smart_resize: the long-side spec (a) shrink / (b) enlarge / (c) hard cap
35 # --------------------------------------------------------------------------- #
36 def test_smart_resize_long_side_shrink():
37 # (a) long side exceeds the cap -> shrink so the long side equals the cap.
38 h, w = smart_resize(
39 2048, 1024, factor=28, max_long_side_pixel=1008, max_total_pixels=10**9
40 )
41 assert max(h, w) == 1008
42 assert (h, w) == (1008, 504) # aspect ratio preserved
43
44
45 def test_smart_resize_short_side_enlarge():
46 # (b) long side within the cap but short side below the floor -> enlarge so
47 # the short side reaches min_short_side_pixel.
48 h, w = smart_resize(
49 200, 40, factor=28, max_long_side_pixel=1008, max_total_pixels=10**9
50 )
51 assert min(h, w) == MIN_SHORT_SIDE_PIXEL # 112
52
53
54 def test_smart_resize_total_pixels_raises():
55 # (c) still over the area cap after resizing -> raise instead of inferring.
56 with pytest.raises(ValueError, match="max_total_pixels"):
57 smart_resize(
58 5000,
59 5000,
60 factor=28,
61 max_long_side_pixel=4000,
62 max_total_pixels=IMAGE_MAX_TOTAL_PIXELS,
63 )
64
65
66 def test_smart_resize_backward_compatible_area_bound():
67 # Without max_long_side_pixel the original Qwen-style area bound is used.
68 assert smart_resize(2048, 2048, factor=28, max_pixels=451584) == (672, 672)
69
70
71 # --------------------------------------------------------------------------- #
72 # Image processor: monotonic prompt-token counts for 252 < 504 < 1008
73 # --------------------------------------------------------------------------- #
74 def test_image_tokens_increase_with_max_long_side_pixel():
75 proc = MiniMaxM3VLImageProcessor()
76 counts = []
77 for long_side in LONG_SIDES:
78 patches = proc.get_number_of_image_patches(
79 2048, 2048, images_kwargs={"max_long_side_pixel": long_side}
80 )
81 counts.append(patches // MERGE2)
82
83 assert counts == [81, 324, 1296]
84 assert counts[0] < counts[1] < counts[2]
85
86
87 def test_image_processor_defaults_match_spec():
88 proc = MiniMaxM3VLImageProcessor()
89 assert proc.max_long_side_pixel is None # opt-in
90 assert proc.min_short_side_pixel == MIN_SHORT_SIDE_PIXEL
91 assert proc.max_total_pixels == IMAGE_MAX_TOTAL_PIXELS
92
93
94 def test_image_preprocess_pipeline_monotonic():
95 proc = MiniMaxM3VLImageProcessor()
96 image = torch.randint(0, 255, (3, 2048, 2048), dtype=torch.uint8)
97 counts = []
98 for long_side in LONG_SIDES:
99 out = proc.preprocess(
100 [image],
101 do_resize=True,
102 max_long_side_pixel=long_side,
103 return_tensors="pt",
104 )
105 counts.append(_image_tokens(out["image_grid_thw"][0]))
106 assert counts == [81, 324, 1296]
107
108
109 # --------------------------------------------------------------------------- #
110 # Video processor: same monotonic behavior + volumetric (w*h*frames) cap
111 # --------------------------------------------------------------------------- #
112 def test_video_tokens_increase_with_max_long_side_pixel():
113 proc = MiniMaxM3VLVideoProcessor()
114 assert proc.max_total_pixels == VIDEO_MAX_TOTAL_PIXELS
115 video = torch.randint(0, 255, (4, 3, 2048, 2048), dtype=torch.uint8)
116 counts = []
117 for long_side in LONG_SIDES:
118 out = proc.preprocess(
119 videos=[video],
120 do_resize=True,
121 max_long_side_pixel=long_side,
122 return_tensors="pt",
123 )
124 counts.append(_image_tokens(out["video_grid_thw"][0]))
125 assert counts[0] < counts[1] < counts[2]
126
127
128 def test_video_volumetric_cap_raises():
129 proc = MiniMaxM3VLVideoProcessor()
130 # 400 frames at a 1008-long-side square: 1008*1008*400 >> 301,056,000.
131 video = torch.randint(0, 255, (400, 3, 2048, 2048), dtype=torch.uint8)
132 with pytest.raises(ValueError, match="max_total_pixels"):
133 proc.preprocess(
134 videos=[video],
135 do_resize=True,
136 max_long_side_pixel=1008,
137 return_tensors="pt",
138 )