# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project import random import pytest import ray import torch import torch.distributed as dist from vllm.distributed.communication_op import tensor_model_parallel_all_reduce # noqa from vllm.distributed.device_communicators import custom_all_reduce as car from vllm.distributed.parallel_state import get_tp_group, graph_capture from vllm.platforms import current_platform from ..utils import ( ensure_model_parallel_initialized, init_test_distributed_environment, multi_process_parallel, ) random.seed(42) test_sizes = [random.randint(1024, 2048 * 1024) for _ in range(8)] for i, v in enumerate(test_sizes): test_sizes[i] -= v % 8 @ray.remote(num_gpus=1, max_calls=1) def _all_reduce_mhc(monkeypatch, tp_size, pp_size, rank, distributed_init_port): from vllm.model_executor.kernels.mhc.tilelang import ( mhc_post_tilelang, mhc_pre_delayed_tilelang, ) with monkeypatch.context() as m: m.delenv("CUDA_VISIBLE_DEVICES", raising=False) device = torch.device(f"cuda:{rank}") torch.accelerator.set_device_index(device) init_test_distributed_environment(tp_size, pp_size, rank, distributed_init_port) ensure_model_parallel_initialized(tp_size, pp_size) comm = get_tp_group().device_communicator.ca_comm assert comm is not None and comm.mnnvl_lamport_ag_multicast_ptr fn = torch.zeros(24, 20480, device=device) scale = torch.ones(3, device=device) base = torch.zeros(24, device=device) # Changing shapes and interleaving all-gather exercise the shared # Lamport stages, including cleanup of a larger previous payload. def run(n): torch.manual_seed(42 + rank) x = torch.randn(n, 5120, device=device, dtype=torch.bfloat16) # Packed +0/-0 pairs collide with the Lamport sentinel. x[:, :16] = 0 x[:, 9:16:2] = -0.0 torch.manual_seed(123) residual = torch.randn(n, 4, 5120, device=device, dtype=torch.bfloat16) post = torch.rand(n, 4, device=device) comb = torch.randn(n, 4, 4, device=device) * 0.1 pre = torch.rand(n, 4, device=device) weight = torch.randn(5120, device=device, dtype=torch.bfloat16) output = torch.empty_like(residual) normalized = torch.empty_like(x) def fused(): torch.ops._C_custom_ar.all_reduce_mhc( x, residual, post, comb, pre, weight, output, normalized, comm.mnnvl_lamport_ag_local_ptr, comm.mnnvl_lamport_ag_multicast_ptr, comm.mnnvl_lamport_epochs[0], rank, comm.mnnvl_buffer_size, 1e-6, ) def check(): gathered = comm.custom_all_gather(x) assert gathered is not None peers = gathered.view(tp_size, n, 5120).float() reduced = peers[0].clone() for peer in peers[1:]: reduced.add_(peer) expected = mhc_post_tilelang( reduced.bfloat16(), residual, post.unsqueeze(-1), comb ) expected_norm = mhc_pre_delayed_tilelang( expected, fn, scale, base, 1e-6, 1e-6, 1e-6, 2.0, 20, pre_mix=pre, norm_weight=weight, )[2] torch.testing.assert_close(output, expected, rtol=0.008, atol=1e-6) torch.testing.assert_close( normalized, expected_norm, rtol=0.008, atol=0.008 ) fused() check() graph = torch.cuda.CUDAGraph() with torch.cuda.graph(graph): for _ in range(6): fused() for _ in range(20): graph.replay() check() x.mul_(0.5) residual.mul_(2) graph.replay() check() # Cover fixed Q6 and shrinking/growing adaptive verification batches. for n in (1, 6, 12, 8, 16, 3, 5, 2, 4, 1): run(n) @pytest.mark.skipif( not current_platform.is_device_capability_family(100), reason="Requires SM100" ) def test_all_reduce_mhc_preserves_bf16_boundaries_and_graph_replay(monkeypatch): if torch.accelerator.device_count() < 4: pytest.skip("Requires four GPUs with NVLink multicast") multi_process_parallel(monkeypatch, 4, 1, _all_reduce_mhc) @pytest.mark.parametrize( ("dtype", "expected"), [ (torch.float32, True), (torch.float16, True), (torch.bfloat16, True), (torch.int8, False), (torch.float8_e4m3fn, False), ], ) def test_custom_allreduce_filters_dtype( dtype: torch.dtype, expected: bool, ) -> None: communicator = car.CustomAllreduce.__new__(car.CustomAllreduce) communicator.disabled = False communicator.world_size = 2 communicator.max_size = 1024 communicator._ptr = 0 assert communicator.should_custom_ar(torch.empty(16, dtype=dtype)) is expected @pytest.mark.parametrize( ("major", "local_multicast", "expected"), [ (8, True, False), (9, True, False), (10, False, False), (10, True, True), ], ) def test_cross_node_mnnvl_gate_checks_generation_and_multicast( monkeypatch, major, local_multicast, expected, ): def has_device_capability(capability, device_id): assert capability == 100 assert device_id == 3 return major >= 10 monkeypatch.setattr( car.current_platform, "has_device_capability", has_device_capability, ) monkeypatch.setattr( car, "_has_local_multicast_support", lambda _device: local_multicast, ) monkeypatch.setattr(car.dist, "all_reduce", lambda *_args, **_kwargs: None) assert car._group_can_attempt_mnnvl(object(), torch.device("cuda:3")) is expected def test_cross_node_mnnvl_gate_requires_support_on_every_rank(monkeypatch): monkeypatch.setattr( car.current_platform, "has_device_capability", lambda *_args: True, ) monkeypatch.setattr( car, "_has_local_multicast_support", lambda _device: True, ) def report_unsupported_peer(support, **_kwargs): support.zero_() monkeypatch.setattr(car.dist, "all_reduce", report_unsupported_peer) assert not car._group_can_attempt_mnnvl(object(), torch.device("cuda:0")) def test_local_multicast_support_rejects_non_cuda(monkeypatch): monkeypatch.setattr(car.current_platform, "is_cuda", lambda: False) assert not car._has_local_multicast_support(torch.device("cuda:0")) @ray.remote(num_gpus=1, max_calls=1) def graph_allreduce( monkeypatch: pytest.MonkeyPatch, tp_size, pp_size, rank, distributed_init_port, ): with monkeypatch.context() as m: m.delenv("CUDA_VISIBLE_DEVICES", raising=False) m.delenv("HIP_VISIBLE_DEVICES", raising=False) device = torch.device(f"cuda:{rank}") torch.accelerator.set_device_index(device) init_test_distributed_environment(tp_size, pp_size, rank, distributed_init_port) ensure_model_parallel_initialized(tp_size, pp_size) group = get_tp_group().device_group # A small all_reduce for warmup. # this is needed because device communicators might be created lazily # (e.g. NCCL). This will ensure that the communicator is initialized # before any communication happens, so that this group can be used for # graph capture immediately. data = torch.zeros(1) data = data.to(device=device) torch.distributed.all_reduce(data, group=group) torch.accelerator.synchronize() del data # we use the first group to communicate once # and the second group to communicate twice # and so on # this is used to demonstrate that each group can # communicate independently num_communication = rank // tp_size + 1 for sz in test_sizes: for dtype in [torch.float32, torch.float16, torch.bfloat16]: with graph_capture(device=device) as graph_capture_context: # use integers so result matches NCCL exactly device_idx = torch.accelerator.current_device_index() inp1 = torch.randint(1, 16, (sz,), dtype=dtype, device=device_idx) inp2 = torch.randint(1, 16, (sz,), dtype=dtype, device=device_idx) torch.accelerator.synchronize() graph = torch.cuda.CUDAGraph() with torch.cuda.graph(graph, stream=graph_capture_context.stream): for i in range(num_communication): out1 = tensor_model_parallel_all_reduce(inp1) # the input buffer is immediately modified to test # synchronization dist.all_reduce(inp1, group=group) out2 = tensor_model_parallel_all_reduce(inp2) dist.all_reduce(inp2, group=group) graph.replay() torch.testing.assert_close(out1, inp1) torch.testing.assert_close(out2, inp2) @ray.remote(num_gpus=1, max_calls=1) def eager_allreduce( monkeypatch: pytest.MonkeyPatch, tp_size, pp_size, rank, distributed_init_port, ): with monkeypatch.context() as m: m.delenv("CUDA_VISIBLE_DEVICES", raising=False) m.delenv("HIP_VISIBLE_DEVICES", raising=False) device = torch.device(f"cuda:{rank}") torch.accelerator.set_device_index(device) init_test_distributed_environment(tp_size, pp_size, rank, distributed_init_port) # we use the first group to communicate once # and the second group to communicate twice # and so on # this is used to demonstrate that each group can # communicate independently num_communication = rank // tp_size + 1 sz = 1024 fa = get_tp_group().device_communicator.ca_comm inp = torch.ones(sz, dtype=torch.float32, device=device) out = inp for _ in range(num_communication): out = fa.all_reduce(out, registered=False) torch.testing.assert_close(out, inp * (tp_size**num_communication)) inp = torch.ones(sz * 4, dtype=torch.bfloat16, device=device) out = inp for _ in range(num_communication): out = fa.all_reduce(out, registered=False) torch.testing.assert_close(out, inp * (tp_size**num_communication)) @pytest.mark.parametrize("tp_size", [2]) @pytest.mark.parametrize("pipeline_parallel_size", [1, 2]) @pytest.mark.parametrize("test_target", [eager_allreduce, graph_allreduce]) def test_custom_allreduce( monkeypatch: pytest.MonkeyPatch, tp_size, pipeline_parallel_size, test_target, ): world_size = tp_size * pipeline_parallel_size if world_size > torch.accelerator.device_count(): pytest.skip("Not enough GPUs to run the test.") multi_process_parallel(monkeypatch, tp_size, pipeline_parallel_size, test_target)