main
py 489 lines 15.4 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3 import math
4
5 import pytest
6 import torch
7 import torch.multiprocessing as mp
8
9 from tests.utils import ensure_current_vllm_config, multi_gpu_test
10 from vllm.distributed import get_tensor_model_parallel_world_size
11 from vllm.distributed.parallel_state import (
12 init_distributed_environment,
13 initialize_model_parallel,
14 )
15 from vllm.model_executor.models.vision import (
16 get_load_balance_assignment,
17 resolve_visual_encoder_outputs,
18 run_dp_sharded_mrope_vision_model,
19 run_dp_sharded_vision_model,
20 )
21 from vllm.platforms import current_platform
22 from vllm.utils.network_utils import get_open_port
23 from vllm.utils.system_utils import update_environment_variables
24 from vllm.utils.torch_utils import set_random_seed
25
26 pytestmark = pytest.mark.cpu_test
27
28
29 @pytest.mark.parametrize(
30 ("select_layers", "num_layers_loaded", "max_possible_layers", "expected_features"),
31 [
32 # All layers loaded
33 ([1, 10], 10, 10, [1, 10]),
34 ([-10, -1], 10, 10, [1, 10]),
35 # Some layers not loaded
36 ([1, 10], 10, 20, [1, 10]),
37 ([-20, -11], 10, 20, [1, 10]),
38 ],
39 )
40 def test_resolve_visual_encoder_outputs(
41 select_layers, num_layers_loaded, max_possible_layers, expected_features
42 ):
43 """Test that offsets are correctly handled for vision feature layers."""
44 encoder_outputs = [torch.tensor([idx]) for idx in range(num_layers_loaded + 1)]
45 output_tensor = resolve_visual_encoder_outputs(
46 encoder_outputs=encoder_outputs,
47 post_layer_norm=None,
48 select_layers=select_layers,
49 max_possible_layers=max_possible_layers,
50 )
51 assert torch.equal(torch.tensor(expected_features), output_tensor)
52
53
54 class SimpleLinearModel(torch.nn.Module):
55 """A simple linear vision model for testing."""
56
57 def __init__(self, input_dim: int = 3 * 224 * 224, output_dim: int = 32):
58 super().__init__()
59 self.flatten = torch.nn.Flatten()
60 self.linear = torch.nn.Linear(input_dim, output_dim)
61
62 def forward(self, x: torch.Tensor):
63 # Flatten the input and apply linear transformation
64 x = self.flatten(x)
65 return self.linear(x)
66
67
68 @multi_gpu_test(num_gpus=2)
69 @pytest.mark.parametrize(
70 "batch_size",
71 [
72 1, # Single image
73 4, # Small batch
74 5, # Odd batch size (for testing padding)
75 ],
76 )
77 def test_run_dp_sharded_vision_model(batch_size: int):
78 world_size = 2
79 # Launch processes
80 mp.spawn(
81 run_dp_sharded_vision_model_vs_direct,
82 args=(
83 world_size,
84 batch_size,
85 get_open_port(),
86 ),
87 nprocs=world_size,
88 )
89
90
91 def run_dp_sharded_vision_model_vs_direct(
92 local_rank: int, world_size: int, batch_size: int, master_port: int
93 ):
94 """Test that run_dp_sharded_vision_model produces the same results as
95 calling the model directly.
96 """
97 # Set random seed for reproducibility
98 set_random_seed(0)
99
100 device = f"{current_platform.device_type}:{local_rank}"
101 torch.accelerator.set_device_index(device)
102 torch.set_default_device(device)
103
104 update_environment_variables(
105 {
106 "RANK": str(local_rank),
107 "LOCAL_RANK": str(local_rank),
108 "WORLD_SIZE": str(world_size),
109 "MASTER_ADDR": "localhost",
110 "MASTER_PORT": str(master_port),
111 }
112 )
113
114 # initialize distributed
115 init_distributed_environment()
116 with ensure_current_vllm_config():
117 initialize_model_parallel(tensor_model_parallel_size=world_size)
118
119 # Create a test input tensor
120 image_input = torch.randn(batch_size, 3, 224, 224)
121
122 # Create a simple linear model
123 vision_model = SimpleLinearModel()
124
125 # Run the model directly on the full input
126 with torch.inference_mode():
127 direct_output = vision_model(image_input)
128
129 # Run the model through the sharded function
130 with torch.inference_mode():
131 sharded_output = run_dp_sharded_vision_model(image_input, vision_model)
132
133 # Check that the world size is set up correctly
134 assert get_tensor_model_parallel_world_size() == world_size
135
136 # Check that the outputs have the same shape
137 assert direct_output.shape == sharded_output.shape
138
139 # Check that the outputs are close (they should be identical)
140 assert torch.allclose(direct_output, sharded_output, rtol=1e-5, atol=1e-5)
141
142
143 @pytest.mark.parametrize(
144 "sizes,num_gpus,expected_shuffle_indices,expected_gpu_sample_counts,"
145 "expected_grouped_sizes_per_gpu,test_description",
146 [
147 # Empty input
148 ([], 2, [], [0, 0], [0, 0], "empty input"),
149 # Fewer samples than GPUs
150 (
151 [100, 200],
152 4,
153 [1, 0],
154 [1, 1, 0, 0],
155 [200, 100, 0, 0],
156 "fewer samples than GPUs",
157 ),
158 # Single GPU
159 ([100, 200, 300], 1, [2, 1, 0], [3], [600], "single GPU"),
160 # Balanced assignment
161 (
162 [100, 100, 100, 100],
163 2,
164 [0, 2, 1, 3],
165 [2, 2],
166 [200, 200],
167 "balanced assignment",
168 ),
169 # Unbalanced sizes - this one is trickier since the algorithm is greedy
170 (
171 [1000, 100, 200, 50],
172 2,
173 [0, 2, 1, 3],
174 [1, 3],
175 [1000, 350],
176 "unbalanced sizes",
177 ),
178 ],
179 )
180 def test_get_load_balance_assignment_cases(
181 sizes,
182 num_gpus,
183 expected_shuffle_indices,
184 expected_gpu_sample_counts,
185 expected_grouped_sizes_per_gpu,
186 test_description,
187 ):
188 """Test get_load_balance_assignment with various input cases."""
189 result = get_load_balance_assignment(sizes, num_gpus=num_gpus)
190 (shuffle_indices, gpu_sample_counts, grouped_sizes_per_gpu) = result
191
192 # Common assertions for all cases
193 assert len(shuffle_indices) == len(sizes)
194 assert len(gpu_sample_counts) == num_gpus
195 assert len(grouped_sizes_per_gpu) == num_gpus
196 assert sum(gpu_sample_counts) == len(sizes)
197
198 assert shuffle_indices == expected_shuffle_indices
199
200 assert gpu_sample_counts == expected_gpu_sample_counts
201 assert grouped_sizes_per_gpu == expected_grouped_sizes_per_gpu
202
203
204 class SimpleMRopeVisionModel(torch.nn.Module):
205 """A simple vision model for testing mrope functionality."""
206
207 def __init__(self, spatial_merge_size: int = 2, out_hidden_size: int = 64):
208 super().__init__()
209 self.spatial_merge_size = spatial_merge_size
210 self.out_hidden_size = out_hidden_size
211 self.linear = torch.nn.Linear(768, out_hidden_size)
212
213 def forward(self, pixel_values: torch.Tensor, grid_thw_list: list[list[int]]):
214 """Simple forward pass that simulates spatial merging."""
215 # Apply linear transformation
216 embeddings = self.linear(pixel_values)
217
218 # Simulate spatial merging by reducing the number of patches
219 merge_factor = self.spatial_merge_size * self.spatial_merge_size
220
221 # Group patches and merge spatially
222 merged_embeddings = []
223 start_idx = 0
224
225 for grid_thw in grid_thw_list:
226 num_patches = math.prod(grid_thw)
227 end_idx = start_idx + num_patches
228
229 # Get patches for this image
230 image_patches = embeddings[start_idx:end_idx]
231
232 # Simulate spatial merging by averaging groups of patches
233 merged_patches = num_patches // merge_factor
234 if merged_patches > 0:
235 # Reshape and average to simulate merging
236 reshaped = image_patches[: merged_patches * merge_factor].view(
237 merged_patches, merge_factor, -1
238 )
239 merged = reshaped.mean(dim=1)
240 merged_embeddings.append(merged)
241
242 start_idx = end_idx
243
244 if merged_embeddings:
245 return torch.cat(merged_embeddings, dim=0)
246 else:
247 return torch.empty(
248 (0, self.out_hidden_size),
249 device=pixel_values.device,
250 dtype=pixel_values.dtype,
251 )
252
253
254 @multi_gpu_test(num_gpus=2)
255 @pytest.mark.parametrize(
256 "batch_size",
257 [
258 1, # Single image
259 3, # Small batch
260 5, # Odd batch size (for testing padding)
261 ],
262 )
263 def test_run_dp_sharded_mrope_vision_model(batch_size: int):
264 world_size = 2
265 # Launch processes
266 mp.spawn(
267 run_dp_sharded_mrope_vision_model_vs_direct,
268 args=(
269 world_size,
270 batch_size,
271 get_open_port(),
272 ),
273 nprocs=world_size,
274 )
275
276
277 def run_dp_sharded_mrope_vision_model_vs_direct(
278 local_rank: int, world_size: int, batch_size: int, master_port: int
279 ):
280 """Test that run_dp_sharded_mrope_vision_model produces the same results as
281 calling the model directly.
282 """
283 # Set random seed for reproducibility
284 set_random_seed(0)
285 device = f"{current_platform.device_type}:{local_rank}"
286 torch.accelerator.set_device_index(device)
287 torch.set_default_device(device)
288
289 update_environment_variables(
290 {
291 "RANK": str(local_rank),
292 "LOCAL_RANK": str(local_rank),
293 "WORLD_SIZE": str(world_size),
294 "MASTER_ADDR": "localhost",
295 "MASTER_PORT": str(master_port),
296 }
297 )
298
299 # initialize distributed
300 init_distributed_environment()
301 with ensure_current_vllm_config():
302 initialize_model_parallel(tensor_model_parallel_size=world_size)
303
304 # Create test data
305 grid_thw_list = []
306 pixel_values_list = []
307
308 for i in range(batch_size):
309 # Varying image sizes for better testing
310 t, h, w = 1, 4 + i, 4 + i
311 grid_thw_list.append([t, h, w])
312
313 num_patches = t * h * w
314 # Create random pixel values for this image
315 image_pixels = torch.randn(num_patches, 768)
316 pixel_values_list.append(image_pixels)
317
318 # Concatenate all pixel values
319 pixel_values = torch.cat(pixel_values_list, dim=0)
320
321 # Create a simple mrope vision model
322 vision_model = SimpleMRopeVisionModel()
323
324 # Run the model directly on the full input (only on rank 0)
325 if local_rank == 0:
326 with torch.inference_mode():
327 direct_output = vision_model(pixel_values, grid_thw_list)
328
329 # Run the model through the sharded function
330 with torch.inference_mode():
331 sharded_output = run_dp_sharded_mrope_vision_model(
332 vision_model, pixel_values, grid_thw_list, rope_type="rope_3d"
333 )
334 sharded_output = torch.cat(sharded_output, dim=0)
335
336 # Check that the world size is set up correctly
337 assert get_tensor_model_parallel_world_size() == world_size
338
339 # Compare outputs (only on rank 0)
340 if local_rank == 0:
341 # Check that the outputs have the same shape
342 assert direct_output.shape == sharded_output.shape
343 # Check that the outputs are close (they should be identical)
344 assert torch.allclose(direct_output, sharded_output, rtol=1e-5, atol=1e-5)
345
346
347 @multi_gpu_test(num_gpus=2)
348 def test_run_dp_sharded_mrope_vision_model_empty_input():
349 world_size = 2
350 mp.spawn(
351 run_dp_sharded_mrope_vision_model_empty_input_worker,
352 args=(world_size, get_open_port()),
353 nprocs=world_size,
354 )
355
356
357 def run_dp_sharded_mrope_vision_model_empty_input_worker(
358 local_rank: int, world_size: int, master_port: int
359 ):
360 """Test run_dp_sharded_mrope_vision_model with empty input."""
361 # Set up distributed environment
362 device = f"{current_platform.device_type}:{local_rank}"
363 torch.accelerator.set_device_index(device)
364 torch.set_default_device(device)
365
366 update_environment_variables(
367 {
368 "RANK": str(local_rank),
369 "LOCAL_RANK": str(local_rank),
370 "WORLD_SIZE": str(world_size),
371 "MASTER_ADDR": "localhost",
372 "MASTER_PORT": str(master_port),
373 }
374 )
375
376 init_distributed_environment()
377 with ensure_current_vllm_config():
378 initialize_model_parallel(tensor_model_parallel_size=world_size)
379
380 # Create empty inputs
381 pixel_values = torch.empty((0, 768))
382 grid_thw_list: list[list[int]] = []
383
384 vision_model = SimpleMRopeVisionModel()
385
386 # Should handle empty input gracefully
387 with torch.inference_mode():
388 output = run_dp_sharded_mrope_vision_model(
389 vision_model, pixel_values, grid_thw_list, rope_type="rope_3d"
390 )
391
392 assert len(output) == 0
393
394
395 @multi_gpu_test(num_gpus=4)
396 def test_run_dp_sharded_mrope_vision_model_uneven_load():
397 world_size = 4
398 mp.spawn(
399 run_dp_sharded_mrope_vision_model_uneven_load_worker,
400 args=(world_size, get_open_port()),
401 nprocs=world_size,
402 )
403
404
405 def run_dp_sharded_mrope_vision_model_uneven_load_worker(
406 local_rank: int, world_size: int, master_port: int
407 ):
408 """Test run_dp_sharded_mrope_vision_model with uneven load distribution."""
409 # Set up distributed environment
410 set_random_seed(123)
411 device = f"{current_platform.device_type}:{local_rank}"
412 torch.accelerator.set_device_index(device)
413 torch.set_default_device(device)
414
415 update_environment_variables(
416 {
417 "RANK": str(local_rank),
418 "LOCAL_RANK": str(local_rank),
419 "WORLD_SIZE": str(world_size),
420 "MASTER_ADDR": "localhost",
421 "MASTER_PORT": str(master_port),
422 }
423 )
424
425 init_distributed_environment()
426 with ensure_current_vllm_config():
427 initialize_model_parallel(tensor_model_parallel_size=world_size)
428
429 # Create images with very different sizes
430 grid_thw_list = [
431 [1, 2, 2], # Small: 4 patches
432 [1, 8, 8], # Large: 64 patches
433 [1, 3, 3], # Medium: 9 patches
434 ]
435
436 pixel_values_list = []
437 for grid_thw in grid_thw_list:
438 num_patches = math.prod(grid_thw)
439 image_pixels = torch.randn(num_patches, 768)
440 pixel_values_list.append(image_pixels)
441
442 pixel_values = torch.cat(pixel_values_list, dim=0)
443 vision_model = SimpleMRopeVisionModel()
444
445 # Should handle uneven distribution without errors
446 with torch.inference_mode():
447 output_tuple = run_dp_sharded_mrope_vision_model(
448 vision_model, pixel_values, grid_thw_list, rope_type="rope_3d"
449 )
450
451 # Verify output shape is reasonable
452 merge_factor = vision_model.spatial_merge_size**2
453 expected_output_patches = list(
454 math.prod(grid_thw) // merge_factor for grid_thw in grid_thw_list
455 )
456
457 for i, output in enumerate(output_tuple):
458 assert output.shape[0] == expected_output_patches[i]
459 assert output.shape[1] == vision_model.out_hidden_size
460
461
462 @pytest.mark.parametrize("spatial_merge_size", [2, 4])
463 def test_simple_mrope_vision_model_spatial_merge(spatial_merge_size: int):
464 """Test SimpleMRopeVisionModel with different spatial merge sizes."""
465 device = current_platform.device_type
466
467 grid_thw_list = [[1, 4, 4], [1, 6, 6]] # Two images
468 pixel_values_list = []
469
470 for grid_thw in grid_thw_list:
471 num_patches = math.prod(grid_thw)
472 image_pixels = torch.randn(num_patches, 768, device=device)
473 pixel_values_list.append(image_pixels)
474
475 pixel_values = torch.cat(pixel_values_list, dim=0)
476 vision_model = SimpleMRopeVisionModel(spatial_merge_size=spatial_merge_size).to(
477 device
478 )
479
480 with torch.inference_mode():
481 output = vision_model(pixel_values, grid_thw_list)
482
483 # Verify output dimensions based on spatial merging
484 total_patches = sum(math.prod(grid_thw) for grid_thw in grid_thw_list)
485 merge_factor = spatial_merge_size**2
486 expected_output_patches = total_patches // merge_factor
487
488 assert output.shape[0] == expected_output_patches
489 assert output.shape[1] == vision_model.out_hidden_size