| 1 | # SPDX-License-Identifier: Apache-2.0 |
| 2 | # SPDX-FileCopyrightText: Copyright contributors to the vLLM project |
| 3 | |
| 4 | from typing import Annotated, Literal |
| 5 | |
| 6 | import pytest |
| 7 | import torch |
| 8 | |
| 9 | from vllm.model_executor.models.glm4_1v import Glm4vImageEmbeddingInputs |
| 10 | from vllm.model_executor.models.granite_speech import GraniteSpeechAudioInputs |
| 11 | from vllm.model_executor.models.phi3v import Phi3VImagePixelInputs |
| 12 | from vllm.utils.tensor_schema import TensorSchema, TensorShape |
| 13 | |
| 14 | |
| 15 | class _DoubleNestedVideoPixelInputs(TensorSchema): |
| 16 | """Minimal schema exercising doubly-nested list[list[torch.Tensor]] inputs. |
| 17 | |
| 18 | Dimensions: |
| 19 | - n: Number of videos |
| 20 | - f: Number of frames |
| 21 | - g: Number of grids |
| 22 | - c: Number of channels (3) |
| 23 | - h: Height |
| 24 | - w: Width |
| 25 | """ |
| 26 | |
| 27 | type: Literal["pixel_values_videos"] = "pixel_values_videos" |
| 28 | pixel_values_videos: Annotated[ |
| 29 | list[list[torch.Tensor]], |
| 30 | TensorShape("n", "f", "g", 3, "h", "w", dynamic_dims={"f", "g"}), |
| 31 | ] |
| 32 | |
| 33 | |
| 34 | def test_tensor_schema_valid_tensor(): |
| 35 | Phi3VImagePixelInputs( |
| 36 | pixel_values=torch.randn(16, 64, 3, 32, 32), |
| 37 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 38 | ) |
| 39 | |
| 40 | |
| 41 | def test_tensor_schema_optional_fields(): |
| 42 | Phi3VImagePixelInputs( |
| 43 | pixel_values=torch.randn(16, 64, 3, 32, 32), |
| 44 | image_sizes=None, |
| 45 | ) |
| 46 | |
| 47 | Phi3VImagePixelInputs(pixel_values=torch.randn(16, 64, 3, 32, 32)) |
| 48 | |
| 49 | |
| 50 | def test_tensor_schema_constant_dim_failure(): |
| 51 | with pytest.raises(ValueError, match="dim\\[2\\] expected 3, got 4"): |
| 52 | Phi3VImagePixelInputs( |
| 53 | pixel_values=torch.randn(16, 64, 4, 32, 32), # dim[2] = 4 |
| 54 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 55 | ) |
| 56 | |
| 57 | |
| 58 | def test_tensor_schema_invalid_types_in_list(): |
| 59 | with pytest.raises(TypeError, match="is not one of the expected types"): |
| 60 | Phi3VImagePixelInputs( |
| 61 | pixel_values=[ |
| 62 | torch.randn(64, 3, 32, 32), |
| 63 | "not_a_tensor", |
| 64 | torch.randn(64, 3, 32, 32), |
| 65 | ], |
| 66 | image_sizes=torch.randint(0, 256, (3, 2)), |
| 67 | ) |
| 68 | |
| 69 | |
| 70 | def test_tensor_schema_rank_mismatch(): |
| 71 | with pytest.raises(ValueError, match="has rank 3 but expected 5"): |
| 72 | Phi3VImagePixelInputs( |
| 73 | pixel_values=torch.randn(16, 64, 3), |
| 74 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 75 | ) |
| 76 | |
| 77 | |
| 78 | def test_tensor_schema_missing_required_field(): |
| 79 | with pytest.raises(ValueError, match="Required field 'pixel_values' is missing"): |
| 80 | Phi3VImagePixelInputs( |
| 81 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 82 | ) |
| 83 | |
| 84 | |
| 85 | def test_tensor_schema_symbolic_dim_mismatch(): |
| 86 | with pytest.raises(ValueError, match="expected 'bn'=12, got 16"): |
| 87 | Phi3VImagePixelInputs( |
| 88 | pixel_values=torch.randn(12, 64, 3, 32, 32), |
| 89 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 90 | ) |
| 91 | |
| 92 | |
| 93 | def test_tensor_schema_list_tensor_valid(): |
| 94 | Phi3VImagePixelInputs( |
| 95 | pixel_values=[torch.randn(64, 3, 32, 32) for _ in range(16)], |
| 96 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 97 | ) |
| 98 | |
| 99 | |
| 100 | def test_tensor_schema_variable_patch_counts_valid(): |
| 101 | # Each image has a different number of patches (p) |
| 102 | # Each tensor has shape (p, 3, 32, 32) |
| 103 | Phi3VImagePixelInputs( |
| 104 | pixel_values=[ |
| 105 | torch.randn(16, 3, 32, 32), # p = 16 |
| 106 | torch.randn(32, 3, 32, 32), # p = 32 |
| 107 | torch.randn(64, 3, 32, 32), # p = 64 |
| 108 | ], |
| 109 | image_sizes=torch.randint(0, 256, (3, 2)), # bn = 3 |
| 110 | ) |
| 111 | |
| 112 | |
| 113 | def test_tensor_schema_tuple_tensor_valid(): |
| 114 | Phi3VImagePixelInputs( |
| 115 | pixel_values=tuple(torch.randn(64, 3, 32, 32) for _ in range(16)), |
| 116 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 117 | ) |
| 118 | |
| 119 | |
| 120 | def test_tensor_schema_double_nested_tensors(): |
| 121 | x = torch.rand(4, 3, 32, 32) |
| 122 | y = torch.rand(2, 3, 32, 32) |
| 123 | |
| 124 | _DoubleNestedVideoPixelInputs(pixel_values_videos=([x, y, x], [y], [x, y])) |
| 125 | |
| 126 | |
| 127 | def test_tensor_schema_inconsistent_shapes_in_list(): |
| 128 | with pytest.raises(ValueError, match="contains inconsistent shapes"): |
| 129 | Phi3VImagePixelInputs( |
| 130 | pixel_values=[ |
| 131 | torch.randn(64, 3, 32, 32), |
| 132 | torch.randn(64, 3, 16, 16), |
| 133 | *(torch.randn(64, 3, 32, 32) for _ in range(14)), |
| 134 | ], |
| 135 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 136 | ) |
| 137 | |
| 138 | |
| 139 | def test_tensor_schema_empty_list(): |
| 140 | with pytest.raises(ValueError, match="is an empty sequence"): |
| 141 | Phi3VImagePixelInputs( |
| 142 | pixel_values=[], |
| 143 | image_sizes=torch.randint(0, 256, (0, 2)), |
| 144 | ) |
| 145 | |
| 146 | |
| 147 | def test_tensor_schema_validation_disabled_skips_shape_check(): |
| 148 | # This should NOT raise, because validation is turned off |
| 149 | # This would normally fail (dim[2] should be 3, not 4) |
| 150 | Phi3VImagePixelInputs( |
| 151 | pixel_values=torch.randn(16, 64, 4, 32, 32), |
| 152 | image_sizes=torch.randint(0, 256, (16, 2)), |
| 153 | validate=False, |
| 154 | ) |
| 155 | |
| 156 | |
| 157 | def test_tensor_schema_with_valid_resolve_binding_dims(): |
| 158 | pixel_values = torch.randn(16, 64, 3, 336, 336) # h=336, w=336 |
| 159 | image_sizes = torch.randint(0, 256, (16, 2)) |
| 160 | |
| 161 | Phi3VImagePixelInputs( |
| 162 | pixel_values=pixel_values, |
| 163 | image_sizes=image_sizes, |
| 164 | resolve_bindings={"h": 336, "w": 336}, |
| 165 | ) |
| 166 | |
| 167 | |
| 168 | def test_tensor_schema_with_invalid_resolve_binding_dims(): |
| 169 | pixel_values = torch.randn(16, 64, 3, 36, 36) # h=36, w=36 |
| 170 | image_sizes = torch.randint(0, 256, (16, 2)) |
| 171 | |
| 172 | # Should raise because 'h' and 'w' don't match resolve bindings |
| 173 | with pytest.raises(ValueError, match="dim\\[3\\] expected 336, got 36"): |
| 174 | Phi3VImagePixelInputs( |
| 175 | pixel_values=pixel_values, |
| 176 | image_sizes=image_sizes, |
| 177 | resolve_bindings={"h": 336, "w": 336}, |
| 178 | ) |
| 179 | |
| 180 | |
| 181 | def test_tensor_schema_with_list_of_symbolic_dim(): |
| 182 | input_features = torch.randn(3, 10, 160) # (b=3, fi=10, 160) |
| 183 | input_features_mask = torch.randn(3, 8) # (b=3, fo=8) |
| 184 | audio_embed_sizes = [8, 8, 8] # len = b = 3 |
| 185 | |
| 186 | GraniteSpeechAudioInputs( |
| 187 | input_features=input_features, |
| 188 | input_features_mask=input_features_mask, |
| 189 | audio_embed_sizes=audio_embed_sizes, |
| 190 | ) |
| 191 | |
| 192 | |
| 193 | def test_tensor_schema_with_list_of_symbolic_dim_mismatch_in_length(): |
| 194 | input_features = torch.randn(4, 10, 160) # (b=4, fi=10, 160) |
| 195 | input_features_mask = torch.randn(4, 8) # (b=4, fo=8) |
| 196 | audio_embed_sizes = [8, 8, 8] # len = 3 ≠ b |
| 197 | |
| 198 | with pytest.raises(ValueError, match="expected 'b'=4, got 3"): |
| 199 | GraniteSpeechAudioInputs( |
| 200 | input_features=input_features, |
| 201 | input_features_mask=input_features_mask, |
| 202 | audio_embed_sizes=audio_embed_sizes, |
| 203 | ) |
| 204 | |
| 205 | |
| 206 | def test_valid_tensor_schema_with_static_last_dim(): |
| 207 | image_embeds = torch.randn(256, 1024) |
| 208 | image_grid_thw = torch.randint(0, 4, (2, 3)) |
| 209 | |
| 210 | Glm4vImageEmbeddingInputs( |
| 211 | image_embeds=image_embeds, |
| 212 | image_grid_thw=image_grid_thw, |
| 213 | ) |
| 214 | |
| 215 | |
| 216 | def test_invalid_tensor_schema_with_static_last_dim(): |
| 217 | image_embeds = torch.randn(256, 1024) |
| 218 | image_grid_thw = torch.randint(0, 4, (2, 4)) # Wrong last dim |
| 219 | |
| 220 | with pytest.raises(ValueError, match="dim\\[1\\] expected 3, got 4"): |
| 221 | Glm4vImageEmbeddingInputs( |
| 222 | image_embeds=image_embeds, |
| 223 | image_grid_thw=image_grid_thw, |
| 224 | ) |