main
py 513 lines 17 KB
Raw
1 # SPDX-License-Identifier: Apache-2.0
2 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4 import os
5
6 import multiprocess as mp
7 import numpy as np
8 import pytest
9 import torch
10 import torch.distributed
11
12 import vllm.envs as envs
13 from tests.utils import ensure_current_vllm_config
14 from vllm.distributed.communication_op import tensor_model_parallel_all_reduce # noqa
15 from vllm.distributed.device_communicators.pynccl import PyNcclCommunicator
16 from vllm.distributed.device_communicators.pynccl_wrapper import NCCLLibrary
17 from vllm.distributed.parallel_state import (
18 ensure_model_parallel_initialized,
19 get_tp_group,
20 get_world_group,
21 graph_capture,
22 init_distributed_environment,
23 )
24 from vllm.utils.system_utils import update_environment_variables
25
26 mp.set_start_method("spawn", force=True)
27
28
29 def distributed_run(fn, world_size):
30 number_of_processes = world_size
31 processes: list[mp.Process] = []
32 for i in range(number_of_processes):
33 env: dict[str, str] = {}
34 env["RANK"] = str(i)
35 env["LOCAL_RANK"] = str(i)
36 env["WORLD_SIZE"] = str(number_of_processes)
37 env["LOCAL_WORLD_SIZE"] = str(number_of_processes)
38 env["MASTER_ADDR"] = "localhost"
39 env["MASTER_PORT"] = "12345"
40 p = mp.Process(target=fn, args=(env,))
41 processes.append(p)
42 p.start()
43
44 for p in processes:
45 p.join()
46
47 for p in processes:
48 assert p.exitcode == 0
49
50
51 def worker_fn_wrapper(fn):
52 # `multiprocessing.Process` cannot accept environment variables directly
53 # so we need to pass the environment variables as arguments
54 # and update the environment variables in the function
55 def wrapped_fn(env):
56 update_environment_variables(env)
57 local_rank = os.environ["LOCAL_RANK"]
58 device = torch.device(f"cuda:{local_rank}")
59 torch.accelerator.set_device_index(device)
60 init_distributed_environment()
61 fn()
62
63 return wrapped_fn
64
65
66 @worker_fn_wrapper
67 def worker_fn():
68 pynccl_comm = PyNcclCommunicator(
69 get_world_group().cpu_group, device=get_world_group().device
70 )
71 tensor = torch.ones(16, 1024, 1024, dtype=torch.float32).cuda(pynccl_comm.rank)
72 tensor = pynccl_comm.all_reduce(tensor)
73 torch.accelerator.synchronize()
74 assert torch.all(tensor == pynccl_comm.world_size).cpu().item()
75
76
77 @pytest.mark.skipif(
78 torch.accelerator.device_count() < 2, reason="Need at least 2 GPUs to run the test."
79 )
80 def test_pynccl():
81 distributed_run(worker_fn, 2)
82
83
84 @worker_fn_wrapper
85 def multiple_allreduce_worker_fn():
86 device = torch.device(f"cuda:{torch.distributed.get_rank()}")
87 if envs.VLLM_DISTRIBUTED_USE_SPLIT_GROUP:
88 # Eager-init path: parent PG has bound_device_id + a CPU backend,
89 # so split_group is supported.
90 group = torch.distributed.split_group(
91 split_ranks=[[0, 1], [2, 3]], backend="cpu:gloo,cuda:nccl"
92 )
93 else:
94 groups = [
95 torch.distributed.new_group(ranks=[0, 1], backend="gloo"),
96 torch.distributed.new_group(ranks=[2, 3], backend="gloo"),
97 ]
98 group = groups[0] if torch.distributed.get_rank() in [0, 1] else groups[1]
99 pynccl_comm = PyNcclCommunicator(group=group, device=device)
100 tensor = torch.ones(16, 1024, 1024, dtype=torch.float32, device=device)
101 # two groups can communicate independently
102 if torch.distributed.get_rank() in [0, 1]:
103 tensor = pynccl_comm.all_reduce(tensor)
104 tensor = pynccl_comm.all_reduce(tensor)
105 torch.accelerator.synchronize()
106 assert torch.all(tensor == 4).cpu().item()
107 else:
108 tensor = pynccl_comm.all_reduce(tensor)
109 torch.accelerator.synchronize()
110 assert torch.all(tensor == 2).cpu().item()
111
112
113 @pytest.mark.skipif(
114 torch.accelerator.device_count() < 4, reason="Need at least 4 GPUs to run the test."
115 )
116 def test_pynccl_multiple_allreduce():
117 # this tests pynccl for multiple tp groups, in a standalone way
118 # i.e. call `pynccl_comm.all_reduce` directly
119 distributed_run(multiple_allreduce_worker_fn, 4)
120
121
122 @worker_fn_wrapper
123 def multiple_allreduce_with_vllm_worker_fn():
124 device = torch.device(f"cuda:{torch.distributed.get_rank()}")
125 with ensure_current_vllm_config():
126 ensure_model_parallel_initialized(2, 2)
127 tensor = torch.ones(16, 1024, 1024, dtype=torch.float32, device=device)
128 with graph_capture(device=device):
129 # two tp groups can communicate independently
130 if torch.distributed.get_rank() in [0, 1]:
131 tensor = tensor_model_parallel_all_reduce(tensor)
132 tensor = tensor_model_parallel_all_reduce(tensor)
133 torch.accelerator.synchronize()
134 assert torch.all(tensor == 4).cpu().item()
135 else:
136 tensor = tensor_model_parallel_all_reduce(tensor)
137 torch.accelerator.synchronize()
138 assert torch.all(tensor == 2).cpu().item()
139
140
141 @pytest.mark.skipif(
142 torch.accelerator.device_count() < 4, reason="Need at least 4 GPUs to run the test."
143 )
144 def test_pynccl_multiple_allreduce_with_vllm():
145 # this tests pynccl for multiple tp groups, together with vllm
146 # i.e. call `tensor_model_parallel_all_reduce`
147 distributed_run(multiple_allreduce_with_vllm_worker_fn, 4)
148
149
150 @worker_fn_wrapper
151 def worker_fn_with_cudagraph():
152 with torch.no_grad():
153 graph = torch.cuda.CUDAGraph()
154 pynccl_comm = PyNcclCommunicator(
155 get_world_group().cpu_group, device=get_world_group().device
156 )
157 # run something in the default stream to initialize torch engine
158 a = torch.ones((4, 4), device=f"cuda:{pynccl_comm.rank}")
159 torch.accelerator.synchronize()
160 with torch.cuda.graph(graph):
161 a_out = pynccl_comm.all_reduce(a)
162 torch.accelerator.synchronize()
163 graph.replay()
164 torch.accelerator.synchronize()
165 assert torch.all(a_out == pynccl_comm.world_size).cpu().item()
166
167
168 @worker_fn_wrapper
169 def all_gather_worker_fn():
170 pynccl_comm = PyNcclCommunicator(
171 get_world_group().cpu_group, device=get_world_group().device
172 )
173
174 rank = pynccl_comm.rank
175 world_size = pynccl_comm.world_size
176 device = f"cuda:{pynccl_comm.rank}"
177
178 num_elems = 1000
179 tensor = (
180 torch.arange(num_elems, dtype=torch.float32, device=device) + rank * num_elems
181 )
182 result = torch.zeros(num_elems * world_size, dtype=torch.float32, device=device)
183
184 expected = torch.cat(
185 [
186 torch.arange(num_elems, dtype=torch.float32) + r * num_elems
187 for r in range(world_size)
188 ]
189 ).to(device)
190
191 pynccl_comm.all_gather(result, tensor)
192 torch.accelerator.synchronize()
193 torch.testing.assert_close(result, expected, rtol=1e-5, atol=1e-8)
194
195
196 @pytest.mark.skipif(
197 torch.accelerator.device_count() < 2, reason="Need at least 2 GPUs to run the test."
198 )
199 def test_pynccl_all_gather():
200 distributed_run(all_gather_worker_fn, 2)
201
202
203 @worker_fn_wrapper
204 def cuda_communicator_all_gather_dim_worker_fn():
205 with ensure_current_vllm_config():
206 ensure_model_parallel_initialized(2, 1)
207
208 tp_group = get_tp_group()
209 comm = tp_group.device_communicator
210 assert comm is not None
211
212 rank = tp_group.rank_in_group
213 world_size = tp_group.world_size
214 device = tp_group.device
215
216 shape = (2, 3, 4)
217 num_elems = 1
218 for size in shape:
219 num_elems *= size
220
221 for dim in (1, -1):
222 tensor = (
223 torch.arange(num_elems, dtype=torch.float32, device=device).reshape(shape)
224 + rank * num_elems
225 )
226 expected = torch.cat(
227 [
228 torch.arange(num_elems, dtype=torch.float32, device=device).reshape(
229 shape
230 )
231 + r * num_elems
232 for r in range(world_size)
233 ],
234 dim=dim,
235 )
236
237 result = comm.all_gather(tensor, dim=dim)
238 torch.accelerator.synchronize()
239 torch.testing.assert_close(result, expected, rtol=1e-5, atol=1e-8)
240
241
242 @pytest.mark.skipif(
243 torch.accelerator.device_count() < 2, reason="Need at least 2 GPUs to run the test."
244 )
245 def test_cuda_communicator_all_gather_dim_not_zero():
246 distributed_run(cuda_communicator_all_gather_dim_worker_fn, 2)
247
248
249 @worker_fn_wrapper
250 def all_gatherv_worker_fn():
251 pynccl_comm = PyNcclCommunicator(
252 get_world_group().cpu_group, device=get_world_group().device
253 )
254
255 rank = pynccl_comm.rank
256 world_size = pynccl_comm.world_size
257 device = f"cuda:{pynccl_comm.rank}"
258
259 assert world_size <= 8
260 sizes = [81, 20, 57, 52, 81, 5, 49, 49][:world_size]
261 num_elems = sizes[rank]
262 tensor = torch.arange(num_elems, dtype=torch.float32, device=device) + rank * 100
263 result = torch.zeros(sum(sizes), dtype=torch.float32, device=device)
264
265 expected = torch.cat(
266 [
267 torch.arange(sizes[r], dtype=torch.float32) + r * 100
268 for r in range(world_size)
269 ]
270 ).to(device)
271
272 pynccl_comm.all_gatherv(result, tensor, sizes=sizes)
273 torch.accelerator.synchronize()
274 torch.testing.assert_close(result, expected, rtol=1e-5, atol=1e-8)
275
276
277 @pytest.mark.skipif(
278 torch.accelerator.device_count() < 2, reason="Need at least 2 GPUs to run the test."
279 )
280 def test_pynccl_all_gatherv():
281 distributed_run(all_gatherv_worker_fn, 2)
282
283
284 @worker_fn_wrapper
285 def reduce_scatter_worker_fn():
286 pynccl_comm = PyNcclCommunicator(
287 get_world_group().cpu_group, device=get_world_group().device
288 )
289
290 rank = pynccl_comm.rank
291 world_size = pynccl_comm.world_size
292 device = f"cuda:{pynccl_comm.rank}"
293
294 num_elems = 1000
295 tensor = (
296 torch.arange(num_elems, dtype=torch.float32, device=device) + rank * num_elems
297 )
298 assert num_elems % world_size == 0
299 result = torch.zeros(num_elems // world_size, dtype=torch.float32, device=device)
300
301 # Calculate expected result for this rank's chunk
302 scattered_size = num_elems // world_size
303 all_tensors = [
304 torch.arange(num_elems, dtype=torch.float32) + r * num_elems
305 for r in range(world_size)
306 ]
307 expected = sum(
308 tensor[rank * scattered_size : (rank + 1) * scattered_size]
309 for tensor in all_tensors
310 ).to(device)
311
312 pynccl_comm.reduce_scatter(result, tensor)
313 torch.accelerator.synchronize()
314 torch.testing.assert_close(result, expected, rtol=1e-5, atol=1e-8)
315
316
317 @pytest.mark.skipif(
318 torch.accelerator.device_count() < 2, reason="Need at least 2 GPUs to run the test."
319 )
320 def test_pynccl_reduce_scatter():
321 distributed_run(reduce_scatter_worker_fn, 2)
322
323
324 @worker_fn_wrapper
325 def reduce_scatterv_worker_fn():
326 pynccl_comm = PyNcclCommunicator(
327 get_world_group().cpu_group, device=get_world_group().device
328 )
329
330 rank = pynccl_comm.rank
331 world_size = pynccl_comm.world_size
332 device = f"cuda:{pynccl_comm.rank}"
333
334 assert world_size <= 8
335 sizes = [81, 20, 57, 52, 81, 5, 49, 49][:world_size]
336 num_elems = sum(sizes)
337 tensor = torch.arange(num_elems, dtype=torch.float32, device=device) + rank * 100
338 result = torch.zeros(sizes[rank], dtype=torch.float32, device=device)
339
340 # Calculate expected result for this rank's chunk
341 all_tensors = [
342 torch.arange(num_elems, dtype=torch.float32) + r * 100
343 for r in range(world_size)
344 ]
345 sizes_cumsum = np.cumsum(sizes)
346 start = 0 if rank == 0 else sizes_cumsum[rank - 1]
347 end = sizes_cumsum[rank]
348 expected = sum(tensor[start:end] for tensor in all_tensors).to(device)
349
350 pynccl_comm.reduce_scatterv(result, tensor, sizes=sizes)
351 torch.accelerator.synchronize()
352 torch.testing.assert_close(result, expected, rtol=1e-5, atol=1e-8)
353
354
355 @pytest.mark.skipif(
356 torch.accelerator.device_count() < 2, reason="Need at least 2 GPUs to run the test."
357 )
358 def test_pynccl_reduce_scatterv():
359 distributed_run(reduce_scatterv_worker_fn, 2)
360
361
362 @pytest.mark.skipif(
363 torch.accelerator.device_count() < 2, reason="Need at least 2 GPUs to run the test."
364 )
365 def test_pynccl_with_cudagraph():
366 distributed_run(worker_fn_with_cudagraph, 2)
367
368
369 @worker_fn_wrapper
370 def send_recv_worker_fn():
371 pynccl_comm = PyNcclCommunicator(
372 get_world_group().cpu_group, device=get_world_group().device
373 )
374 if pynccl_comm.rank == 0:
375 tensor = torch.ones(16, 1024, 1024, dtype=torch.float32).cuda(pynccl_comm.rank)
376 else:
377 tensor = torch.empty(16, 1024, 1024, dtype=torch.float32).cuda(pynccl_comm.rank)
378
379 if pynccl_comm.rank == 0:
380 pynccl_comm.send(tensor, dst=(pynccl_comm.rank + 1) % pynccl_comm.world_size)
381 else:
382 pynccl_comm.recv(tensor, src=(pynccl_comm.rank - 1) % pynccl_comm.world_size)
383 torch.accelerator.synchronize()
384 assert torch.all(tensor == 1).cpu().item()
385
386
387 @pytest.mark.skipif(
388 torch.accelerator.device_count() < 2, reason="Need at least 2 GPUs to run the test."
389 )
390 def test_pynccl_send_recv():
391 distributed_run(send_recv_worker_fn, 2)
392
393
394 @worker_fn_wrapper
395 def multiple_send_recv_worker_fn():
396 device = torch.device(f"cuda:{torch.distributed.get_rank()}")
397 if envs.VLLM_DISTRIBUTED_USE_SPLIT_GROUP:
398 group = torch.distributed.split_group(
399 split_ranks=[[0, 2], [1, 3]], backend="cpu:gloo,cuda:nccl"
400 )
401 else:
402 groups = [
403 torch.distributed.new_group(ranks=[0, 2], backend="gloo"),
404 torch.distributed.new_group(ranks=[1, 3], backend="gloo"),
405 ]
406 group = groups[0] if torch.distributed.get_rank() in [0, 2] else groups[1]
407 pynccl_comm = PyNcclCommunicator(group=group, device=device)
408 if torch.distributed.get_rank() == 0:
409 tensor = torch.ones(16, 1024, 1024, dtype=torch.float32, device=device)
410 elif torch.distributed.get_rank() == 1:
411 tensor = 2 * torch.ones(16, 1024, 1024, dtype=torch.float32, device=device)
412 else:
413 tensor = torch.empty(16, 1024, 1024, dtype=torch.float32, device=device)
414 if torch.distributed.get_rank() in [0, 1]:
415 pynccl_comm.send(tensor, dst=(pynccl_comm.rank + 1) % pynccl_comm.world_size)
416 else:
417 pynccl_comm.recv(tensor, src=(pynccl_comm.rank - 1) % pynccl_comm.world_size)
418 torch.accelerator.synchronize()
419 if torch.distributed.get_rank() in [0, 2]:
420 assert torch.all(tensor == 1).cpu().item()
421 else:
422 assert torch.all(tensor == 2).cpu().item()
423
424
425 @pytest.mark.skipif(
426 torch.accelerator.device_count() < 4, reason="Need at least 4 GPUs to run the test."
427 )
428 def test_pynccl_multiple_send_recv():
429 distributed_run(multiple_send_recv_worker_fn, 4)
430
431
432 @pytest.mark.skipif(
433 torch.accelerator.device_count() < 4, reason="Need at least 4 GPUs to run the test."
434 )
435 def test_pynccl_broadcast():
436 distributed_run(broadcast_worker_fn, 4)
437
438
439 @worker_fn_wrapper
440 def broadcast_worker_fn():
441 # Test broadcast for every root rank.
442 # Essentially this is an all-gather operation.
443 pynccl_comm = PyNcclCommunicator(
444 get_world_group().cpu_group, device=get_world_group().device
445 )
446 recv_tensors = [
447 torch.empty(16, 1024, 1024, dtype=torch.float32, device=pynccl_comm.device)
448 for i in range(pynccl_comm.world_size)
449 ]
450 recv_tensors[pynccl_comm.rank] = (
451 torch.ones(16, 1024, 1024, dtype=torch.float32, device=pynccl_comm.device)
452 * pynccl_comm.rank
453 )
454
455 for i in range(pynccl_comm.world_size):
456 pynccl_comm.broadcast(recv_tensors[i], src=i)
457 # the broadcast op might be launched in a different stream
458 # need to synchronize to make sure the tensor is ready
459 torch.accelerator.synchronize()
460 assert torch.all(recv_tensors[i] == i).cpu().item()
461
462
463 def test_ncclGetUniqueId():
464 lib = NCCLLibrary()
465 unique_id = lib.ncclGetUniqueId()
466 # `list(unique_id.internal)` is something like this:
467 # [34, -16, 23, 83, 109, -19, 59, 95, 2, 0, -86, 55, 10, -128, 0, 29, 0,
468 # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
469 # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
470 # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
471 # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
472 # 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
473 # as long as the function doesn't raise an exception, we're good
474 assert unique_id is not None
475
476
477 def test_pynccl_suspend_resume_idempotent():
478 """Repeated suspend/resume (e.g. staged wake-ups) reach NCCL once each."""
479 from unittest.mock import Mock
480
481 comm = object.__new__(PyNcclCommunicator)
482 comm.disabled = False
483 comm._suspended = False
484 comm.comm = object()
485 comm.nccl = Mock()
486
487 comm.suspend()
488 comm.suspend()
489 comm.resume()
490 comm.resume()
491
492 assert comm.nccl.ncclCommSuspend.call_count == 1
493 assert comm.nccl.ncclCommResume.call_count == 1
494
495
496 def test_pynccl_suspend_noop_without_symbol():
497 """RCCL / NCCL < 2.29.7 lack ncclCommSuspend; suspend() must no-op, not
498 crash, and resume() must then no-op too since nothing was suspended."""
499 from unittest.mock import Mock
500
501 comm = object.__new__(PyNcclCommunicator)
502 comm.disabled = False
503 comm._suspended = False
504 comm.comm = object()
505 comm.nccl = Mock()
506 comm.nccl.has_symbol.return_value = False
507
508 comm.suspend()
509 comm.resume()
510
511 comm.nccl.ncclCommSuspend.assert_not_called()
512 comm.nccl.ncclCommResume.assert_not_called()
513 assert comm._suspended is False