Files
ray-project--ray/python/ray/tests/test_experimental_collective.py
T
2026-07-13 13:17:40 +08:00

261 lines
8.8 KiB
Python

import sys
import pytest
import torch
import ray
import ray.experimental.collective
SHAPE = (2, 2)
DTYPE = torch.float16
@ray.remote
class Actor:
def __init__(self, shape, dtype):
self.tensor = torch.zeros(shape, dtype=dtype)
def make_tensor(self, shape, dtype):
self.tensor = torch.randn(shape, dtype=dtype)
def get_tensor(self):
return self.tensor
@pytest.fixture
def collective_actors():
world_size = 3
actors = [Actor.remote(SHAPE, DTYPE) for _ in range(world_size)]
group = ray.experimental.collective.create_collective_group(
actors, backend="torch_gloo"
)
return group.name, actors
def test_api_basic(ray_start_regular_shared):
world_size = 3
actors = [Actor.remote(SHAPE, DTYPE) for _ in range(world_size)]
# Check no groups on start up.
for actor in actors:
groups = ray.experimental.collective.get_collective_groups([actor])
assert groups == []
groups = ray.experimental.collective.get_collective_groups(actors)
assert groups == []
# Check that the collective group is created with the correct actors and
# ranks.
group = ray.experimental.collective.create_collective_group(
actors, backend="torch_gloo", name="test"
)
assert group.name == "test"
for i, actor in enumerate(actors):
assert group.get_rank(actor) == i
# Check that we can look up the created collective by actor handle(s).
for actor in actors:
groups = ray.experimental.collective.get_collective_groups([actor])
assert groups == [group]
groups = ray.experimental.collective.get_collective_groups(actors)
assert groups == [group]
# Check that the group is destroyed.
ray.experimental.collective.destroy_collective_group(group)
for actor in actors:
groups = ray.experimental.collective.get_collective_groups([actor])
assert groups == []
groups = ray.experimental.collective.get_collective_groups(actors)
assert groups == []
# Check that we can recreate the group with the same name and actors.
ray.experimental.collective.create_collective_group(
actors, backend="torch_gloo", name="test"
)
def test_api_exceptions(ray_start_regular_shared):
world_size = 3
actors = [Actor.remote(SHAPE, DTYPE) for _ in range(world_size)]
with pytest.raises(ValueError, match="All actors must be unique"):
ray.experimental.collective.create_collective_group(
actors + [actors[0]], "torch_gloo"
)
ray.experimental.collective.create_collective_group(actors, backend="torch_gloo")
# Check that we cannot create another group using the same actors.
with pytest.raises(RuntimeError, match="already in group"):
ray.experimental.collective.create_collective_group(
actors, backend="torch_gloo"
)
with pytest.raises(RuntimeError, match="already in group"):
ray.experimental.collective.create_collective_group(
actors[:2], backend="torch_gloo"
)
with pytest.raises(RuntimeError, match="already in group"):
ray.experimental.collective.create_collective_group(
actors[1:], backend="torch_gloo"
)
def test_allreduce(ray_start_regular_shared, collective_actors):
group_name, actors = collective_actors
[actor.make_tensor.remote(SHAPE, DTYPE) for actor in actors]
tensors = ray.get([actor.get_tensor.remote() for actor in actors])
expected_sum = sum(tensors)
def do_allreduce(self, group_name):
ray.util.collective.allreduce(self.tensor, group_name=group_name)
ray.get([actor.__ray_call__.remote(do_allreduce, group_name) for actor in actors])
tensors = ray.get([actor.get_tensor.remote() for actor in actors])
for tensor in tensors:
assert torch.allclose(tensor, expected_sum, atol=1e-2)
def test_barrier(ray_start_regular_shared, collective_actors):
group_name, actors = collective_actors
def do_barrier(self, group_name):
ray.util.collective.barrier(group_name=group_name)
barriers = []
for actor in actors:
if barriers:
with pytest.raises(ray.exceptions.GetTimeoutError):
ray.get(barriers, timeout=0.1)
barriers.append(actor.__ray_call__.remote(do_barrier, group_name))
ray.get(barriers)
def test_allgather(ray_start_regular_shared, collective_actors):
group_name, actors = collective_actors
[actor.make_tensor.remote(SHAPE, DTYPE) for actor in actors]
tensors = ray.get([actor.get_tensor.remote() for actor in actors])
def do_allgather(self, world_size, group_name):
tensor_list = [torch.zeros(SHAPE, dtype=DTYPE) for _ in range(world_size)]
ray.util.collective.allgather(tensor_list, self.tensor, group_name=group_name)
return tensor_list
all_tensor_lists = ray.get(
[
actor.__ray_call__.remote(do_allgather, len(actors), group_name)
for actor in actors
]
)
for tensor_list in all_tensor_lists:
for tensor, expected_tensor in zip(tensors, tensor_list):
assert torch.allclose(tensor, expected_tensor)
def test_broadcast(ray_start_regular_shared, collective_actors):
group_name, actors = collective_actors
actors[0].make_tensor.remote(SHAPE, DTYPE)
expected_tensor = ray.get(actors[0].get_tensor.remote())
def do_broadcast(self, src_rank, group_name):
ray.util.collective.broadcast(self.tensor, src_rank, group_name=group_name)
[actor.__ray_call__.remote(do_broadcast, 0, group_name) for actor in actors]
tensors = ray.get([actor.get_tensor.remote() for actor in actors])
for tensor in tensors:
assert torch.allclose(tensor, expected_tensor)
def test_reduce(ray_start_regular_shared, collective_actors):
group_name, actors = collective_actors
[actor.make_tensor.remote(SHAPE, DTYPE) for actor in actors]
tensors = ray.get([actor.get_tensor.remote() for actor in actors])
expected_sum = sum(tensors)
def do_reduce(self, dst_rank, group_name):
ray.util.collective.reduce(self.tensor, dst_rank, group_name)
dst_rank = 0
ray.get(
[actor.__ray_call__.remote(do_reduce, dst_rank, group_name) for actor in actors]
)
tensor = ray.get(actors[dst_rank].get_tensor.remote())
assert torch.allclose(tensor, expected_sum, atol=1e-2)
def test_reducescatter(ray_start_regular_shared, collective_actors):
group_name, actors = collective_actors
[actor.make_tensor.remote((len(actors), *SHAPE), DTYPE) for actor in actors]
tensors = ray.get([actor.get_tensor.remote() for actor in actors])
expected_sum = sum(tensors)
expected_tensors = list(expected_sum)
def do_reducescatter(self, world_size, group_name):
tensor = torch.zeros(SHAPE, dtype=DTYPE)
tensor_list = list(self.tensor)
ray.util.collective.reducescatter(tensor, tensor_list, group_name)
return tensor
tensors = ray.get(
[
actor.__ray_call__.remote(do_reducescatter, len(actors), group_name)
for actor in actors
]
)
for tensor, expected in zip(tensors, expected_tensors):
assert torch.allclose(tensor, expected, atol=1e-2)
def test_send_recv(ray_start_regular_shared, collective_actors):
group_name, actors = collective_actors
def do_send(self, group_name, dst_rank):
ray.util.collective.send(self.tensor, dst_rank, group_name=group_name)
def do_recv(self, group_name, src_rank):
ray.util.collective.recv(self.tensor, src_rank, group_name=group_name)
for ranks in [(0, 1), (1, 2), (2, 0)]:
src_rank, dst_rank = ranks
src, dst = actors[src_rank], actors[dst_rank]
src.make_tensor.remote(SHAPE, DTYPE)
tensor = ray.get(src.get_tensor.remote())
ray.get(
[
src.__ray_call__.remote(do_send, group_name, dst_rank),
dst.__ray_call__.remote(do_recv, group_name, src_rank),
]
)
assert torch.allclose(tensor, ray.get(src.get_tensor.remote()))
assert torch.allclose(tensor, ray.get(dst.get_tensor.remote()))
def test_send_recv_exceptions(ray_start_regular_shared, collective_actors):
group_name, actors = collective_actors
def do_send(self, group_name, dst_rank):
ray.util.collective.send(self.tensor, dst_rank, group_name=group_name)
def do_recv(self, group_name, src_rank):
ray.util.collective.recv(self.tensor, src_rank, group_name=group_name)
# Actors cannot send to/recv from themselves.
for rank in range(len(actors)):
with pytest.raises(RuntimeError):
ray.get(actors[rank].__ray_call__.remote(do_send, group_name, rank))
with pytest.raises(RuntimeError):
ray.get(actors[rank].__ray_call__.remote(do_recv, group_name, rank))
if __name__ == "__main__":
sys.exit(pytest.main(["-sv", __file__]))