| 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 | ) |