chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
from ray.util.collective.backend_registry import (
|
||||
register_collective_backend,
|
||||
)
|
||||
from ray.util.collective.collective import (
|
||||
allgather,
|
||||
allgather_multigpu,
|
||||
allreduce,
|
||||
allreduce_multigpu,
|
||||
barrier,
|
||||
broadcast,
|
||||
broadcast_multigpu,
|
||||
create_collective_group,
|
||||
destroy_collective_group,
|
||||
get_collective_group_size,
|
||||
get_group_handle,
|
||||
get_rank,
|
||||
gloo_available,
|
||||
init_collective_group,
|
||||
is_backend_available,
|
||||
is_group_initialized,
|
||||
nccl_available,
|
||||
recv,
|
||||
recv_multigpu,
|
||||
reduce,
|
||||
reduce_multigpu,
|
||||
reducescatter,
|
||||
reducescatter_multigpu,
|
||||
send,
|
||||
send_multigpu,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"nccl_available",
|
||||
"gloo_available",
|
||||
"is_backend_available",
|
||||
"is_group_initialized",
|
||||
"init_collective_group",
|
||||
"destroy_collective_group",
|
||||
"create_collective_group",
|
||||
"get_rank",
|
||||
"get_collective_group_size",
|
||||
"allreduce",
|
||||
"allreduce_multigpu",
|
||||
"barrier",
|
||||
"reduce",
|
||||
"reduce_multigpu",
|
||||
"broadcast",
|
||||
"broadcast_multigpu",
|
||||
"allgather",
|
||||
"allgather_multigpu",
|
||||
"reducescatter",
|
||||
"reducescatter_multigpu",
|
||||
"send",
|
||||
"send_multigpu",
|
||||
"recv",
|
||||
"recv_multigpu",
|
||||
"get_group_handle",
|
||||
"register_collective_backend",
|
||||
]
|
||||
@@ -0,0 +1,122 @@
|
||||
from typing import Dict, Type
|
||||
|
||||
from .collective_group.base_collective_group import BaseGroup
|
||||
from ray.util.annotations import PublicAPI
|
||||
|
||||
|
||||
class BackendRegistry:
|
||||
_instance = None
|
||||
_map: Dict[str, Type[BaseGroup]]
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(BackendRegistry, cls).__new__(cls)
|
||||
cls._instance._map = {}
|
||||
return cls._instance
|
||||
|
||||
def put(self, name: str, group_cls: Type[BaseGroup]) -> None:
|
||||
if not issubclass(group_cls, BaseGroup):
|
||||
raise TypeError(f"{group_cls} is not a subclass of BaseGroup")
|
||||
if name.upper() in self._map:
|
||||
raise ValueError(f"Backend {name.upper()} already registered")
|
||||
self._map[name.upper()] = group_cls
|
||||
|
||||
def get(self, name: str) -> Type[BaseGroup]:
|
||||
name = name.upper()
|
||||
if name not in self._map:
|
||||
raise ValueError(f"Backend {name} not registered")
|
||||
return self._map[name]
|
||||
|
||||
def is_registered(self, name: str) -> bool:
|
||||
"""Check if a backend is registered (regardless of availability)."""
|
||||
return name.upper() in self._map
|
||||
|
||||
def check(self, name: str) -> bool:
|
||||
"""Check if a backend is both registered and available."""
|
||||
try:
|
||||
cls = self.get(name)
|
||||
return cls.check_backend_availability()
|
||||
except (ValueError, AttributeError):
|
||||
return False
|
||||
|
||||
|
||||
_global_registry = BackendRegistry()
|
||||
|
||||
|
||||
@PublicAPI(stability="alpha")
|
||||
def register_collective_backend(name: str, group_cls: Type[BaseGroup]):
|
||||
"""Register a custom collective backend with Ray.
|
||||
|
||||
This function registers a custom backend class that can be used for
|
||||
collective operations. The backend must be a subclass of
|
||||
:class:`~ray.util.collective.collective_group.base_collective_group.BaseGroup`
|
||||
and implement all required collective operations.
|
||||
|
||||
Important: The backend must be registered on both the driver and all
|
||||
actors before creating collective groups. This is because each process
|
||||
(driver and each actor) needs to know about your backend class to
|
||||
instantiate it.
|
||||
|
||||
Args:
|
||||
name: The name of the backend (e.g., "MY_BACKEND"). This will be
|
||||
automatically added to the Backend enum as Backend.MY_BACKEND.
|
||||
group_cls: The backend class, which must be a subclass of
|
||||
:class:`~ray.util.collective.collective_group.base_collective_group.BaseGroup`.
|
||||
|
||||
Example:
|
||||
>>> import ray
|
||||
>>> from ray.util.collective import create_collective_group, init_collective_group
|
||||
>>> from ray.util.collective.backend_registry import register_collective_backend
|
||||
>>> from ray.util.collective.collective_group.base_collective_group import BaseGroup
|
||||
>>>
|
||||
>>> class MyCustomBackend(BaseGroup):
|
||||
... def __init__(self, world_size, rank, group_name):
|
||||
... super().__init__(world_size, rank, group_name)
|
||||
... @classmethod
|
||||
... def backend(cls):
|
||||
... return "MY_BACKEND"
|
||||
... @classmethod
|
||||
... def check_backend_availability(cls) -> bool:
|
||||
... return True
|
||||
... def allreduce(self, tensor, allreduce_options=None):
|
||||
... pass
|
||||
... def broadcast(self, tensor, broadcast_options=None):
|
||||
... pass
|
||||
... def barrier(self, barrier_options=None):
|
||||
... pass
|
||||
>>>
|
||||
>>> # Register on the driver
|
||||
>>> register_collective_backend("MY_BACKEND", MyCustomBackend)
|
||||
>>>
|
||||
>>> ray.init()
|
||||
>>>
|
||||
>>> @ray.remote
|
||||
... class Worker:
|
||||
... def __init__(self, rank):
|
||||
... self.rank = rank
|
||||
... def setup(self, world_size):
|
||||
... # IMPORTANT: Register on each worker too
|
||||
... register_collective_backend("MY_BACKEND", MyCustomBackend)
|
||||
... init_collective_group(
|
||||
... world_size=world_size,
|
||||
... rank=self.rank,
|
||||
... backend="MY_BACKEND",
|
||||
... group_name="default",
|
||||
... )
|
||||
>>>
|
||||
>>> actors = [Worker.remote(rank=i) for i in range(2)]
|
||||
>>> create_collective_group(
|
||||
... actors=actors,
|
||||
... world_size=2,
|
||||
... ranks=[0, 1],
|
||||
... backend="MY_BACKEND",
|
||||
... group_name="default",
|
||||
... )
|
||||
>>> ray.get([a.setup.remote(2) for a in actors])
|
||||
"""
|
||||
_global_registry.put(name, group_cls)
|
||||
from . import types
|
||||
|
||||
upper_name = name.upper()
|
||||
if not hasattr(types.Backend, upper_name):
|
||||
setattr(types.Backend, upper_name, upper_name)
|
||||
@@ -0,0 +1,816 @@
|
||||
"""APIs exposed under the namespace ray.util.collective."""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import threading
|
||||
from typing import Any, List, Tuple
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ray
|
||||
from . import types
|
||||
from ray._common.network_utils import find_free_port, is_ipv6
|
||||
from ray.util.collective.backend_registry import (
|
||||
_global_registry,
|
||||
register_collective_backend,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from ray.util.collective.collective_group.nccl_collective_group import NCCLGroup
|
||||
|
||||
register_collective_backend("NCCL", NCCLGroup)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
try:
|
||||
from ray.util.collective.collective_group.torch_gloo_collective_group import (
|
||||
TorchGLOOGroup,
|
||||
)
|
||||
|
||||
register_collective_backend("GLOO", TorchGLOOGroup)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def nccl_available():
|
||||
return is_backend_available("NCCL")
|
||||
|
||||
|
||||
def gloo_available():
|
||||
return is_backend_available("GLOO")
|
||||
|
||||
|
||||
def is_backend_available(backend: str) -> bool:
|
||||
"""Check if a collective backend is available.
|
||||
|
||||
Args:
|
||||
backend: The name of the backend to check (e.g., "NCCL", "GLOO").
|
||||
|
||||
Returns:
|
||||
True if the backend is available, False otherwise.
|
||||
"""
|
||||
return _global_registry.check(backend)
|
||||
|
||||
|
||||
def get_address_and_port() -> Tuple[str, int]:
|
||||
"""Returns the IP address and a free port on this node."""
|
||||
addr = ray.util.get_node_ip_address()
|
||||
port = find_free_port(socket.AF_INET6 if is_ipv6(addr) else socket.AF_INET)
|
||||
return addr, port
|
||||
|
||||
|
||||
class GroupManager(object):
|
||||
"""Use this class to manage the collective groups we created so far.
|
||||
|
||||
Each process will have an instance of `GroupManager`. Each process
|
||||
could belong to multiple collective groups. The membership information
|
||||
and other metadata are stored in the global `_group_mgr` object.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._name_group_map = {}
|
||||
self._registry = _global_registry
|
||||
|
||||
def create_collective_group(
|
||||
self, backend, world_size, rank, group_name, gloo_timeout=None
|
||||
):
|
||||
"""The entry to create new collective groups in the manager.
|
||||
|
||||
Put the registration and the group information into the manager
|
||||
metadata as well.
|
||||
"""
|
||||
backend = backend.upper()
|
||||
backend_cls = self._registry.get(backend)
|
||||
|
||||
if not backend_cls.check_backend_availability():
|
||||
raise RuntimeError(
|
||||
f"Backend {backend} is not available. Please check the installation."
|
||||
)
|
||||
|
||||
if backend == "GLOO":
|
||||
logger.debug(
|
||||
"Creating torch.distributed GLOO group: '{}'...".format(group_name)
|
||||
)
|
||||
g = backend_cls(world_size, rank, group_name, gloo_timeout)
|
||||
else:
|
||||
g = backend_cls(world_size, rank, group_name)
|
||||
|
||||
self._name_group_map[group_name] = g
|
||||
return self._name_group_map[group_name]
|
||||
|
||||
def is_group_exist(self, group_name):
|
||||
return group_name in self._name_group_map
|
||||
|
||||
def get_group_by_name(self, group_name):
|
||||
"""Get the collective group handle by its name."""
|
||||
if not self.is_group_exist(group_name):
|
||||
logger.warning("The group '{}' is not initialized.".format(group_name))
|
||||
return None
|
||||
return self._name_group_map[group_name]
|
||||
|
||||
def destroy_collective_group(self, group_name):
|
||||
"""Group destructor."""
|
||||
if not self.is_group_exist(group_name):
|
||||
logger.warning("The group '{}' does not exist.".format(group_name))
|
||||
return
|
||||
|
||||
# release the collective group resource
|
||||
g = self._name_group_map[group_name]
|
||||
# clean up the dicts
|
||||
del self._name_group_map[group_name]
|
||||
# Release the communicator resources
|
||||
g.destroy_group()
|
||||
|
||||
# Release the detached actors spawned by `create_collective_group()`
|
||||
name = "info_" + group_name
|
||||
try:
|
||||
store = ray.get_actor(name)
|
||||
ray.kill(store)
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
|
||||
_group_mgr = GroupManager()
|
||||
# This lock is used to make external calls to the _group_mgr thread-safe.
|
||||
_group_mgr_lock = threading.Lock()
|
||||
|
||||
|
||||
def is_group_initialized(group_name):
|
||||
"""Check if the group is initialized in this process by the group name."""
|
||||
global _group_mgr
|
||||
global _group_mgr_lock
|
||||
with _group_mgr_lock:
|
||||
return _group_mgr.is_group_exist(group_name)
|
||||
|
||||
|
||||
def init_collective_group(
|
||||
world_size: int,
|
||||
rank: int,
|
||||
backend: types.Backend = types.Backend.NCCL,
|
||||
group_name: str = "default",
|
||||
gloo_timeout: int = 30000,
|
||||
):
|
||||
"""Initialize a collective group inside an actor process.
|
||||
|
||||
Args:
|
||||
world_size: the total number of processes in the group.
|
||||
rank: the rank of the current process.
|
||||
backend: the CCL backend to use, NCCL or GLOO.
|
||||
group_name: the name of the collective group.
|
||||
gloo_timeout: timeout in milliseconds for GLOO operations.
|
||||
"""
|
||||
_check_inside_actor()
|
||||
|
||||
global _group_mgr
|
||||
global _group_mgr_lock
|
||||
|
||||
# TODO(Hao): implement a group auto-counter.
|
||||
if not group_name:
|
||||
raise ValueError("group_name '{}' needs to be a string.".format(group_name))
|
||||
|
||||
with _group_mgr_lock:
|
||||
if _group_mgr.is_group_exist(group_name):
|
||||
raise RuntimeError("Trying to initialize a group a second time.")
|
||||
|
||||
assert world_size > 0
|
||||
assert rank >= 0
|
||||
assert rank < world_size
|
||||
_group_mgr.create_collective_group(
|
||||
backend, world_size, rank, group_name, gloo_timeout
|
||||
)
|
||||
|
||||
|
||||
def create_collective_group(
|
||||
actors: List[Any],
|
||||
world_size: int,
|
||||
ranks: List[int],
|
||||
backend: types.Backend = types.Backend.NCCL,
|
||||
group_name: str = "default",
|
||||
gloo_timeout: int = 30000,
|
||||
):
|
||||
"""Declare a list of actors as a collective group.
|
||||
|
||||
Note: This function should be called in a driver process.
|
||||
|
||||
Args:
|
||||
actors: a list of actors to be set in a collective group.
|
||||
world_size: the total number of processes in the group.
|
||||
ranks: the rank of each actor.
|
||||
backend: the CCL backend to use, NCCL or GLOO.
|
||||
group_name: the name of the collective group.
|
||||
gloo_timeout: timeout in milliseconds for GLOO operations.
|
||||
"""
|
||||
|
||||
name = "info_" + group_name
|
||||
try:
|
||||
ray.get_actor(name)
|
||||
raise RuntimeError("Trying to initialize a group twice.")
|
||||
except ValueError:
|
||||
pass
|
||||
|
||||
if len(ranks) != len(actors):
|
||||
raise RuntimeError(
|
||||
"Each actor should correspond to one rank. Got '{}' "
|
||||
"ranks but '{}' actors".format(len(ranks), len(actors))
|
||||
)
|
||||
|
||||
if set(ranks) != set(range(len(ranks))):
|
||||
raise RuntimeError(
|
||||
"Ranks must be a permutation from 0 to '{}'. Got '{}'.".format(
|
||||
len(ranks), "".join([str(r) for r in ranks])
|
||||
)
|
||||
)
|
||||
|
||||
if world_size <= 0:
|
||||
raise RuntimeError(
|
||||
"World size must be greater than zero. Got '{}'.".format(world_size)
|
||||
)
|
||||
if not all(ranks) >= 0:
|
||||
raise RuntimeError("Ranks must be non-negative.")
|
||||
if not all(ranks) < world_size:
|
||||
raise RuntimeError("Ranks cannot be greater than world_size.")
|
||||
|
||||
# Check if backend is registered and available
|
||||
backend_upper = backend.upper()
|
||||
if not _global_registry.is_registered(backend_upper):
|
||||
raise RuntimeError(
|
||||
f"Backend {backend_upper} is not registered. "
|
||||
f"Please register it using register_collective_backend('{backend_upper}', YourBackendClass)."
|
||||
)
|
||||
if not _global_registry.check(backend_upper):
|
||||
raise RuntimeError(
|
||||
f"Backend {backend_upper} is registered but not available. "
|
||||
f"Please check the installation requirements for this backend."
|
||||
)
|
||||
|
||||
# avoid a circular dependency
|
||||
from ray.util.collective.util import Info
|
||||
|
||||
# store the information into a NamedActor that can be accessed later.
|
||||
name = "info_" + group_name
|
||||
actors_id = [a._ray_actor_id for a in actors]
|
||||
# TODO (Dacheng): how do we recycle this name actor?
|
||||
info = Info.options(name=name, lifetime="detached").remote()
|
||||
ray.get([info.set_info.remote(actors_id, world_size, ranks, backend, gloo_timeout)])
|
||||
|
||||
|
||||
# TODO (we need a declarative destroy() API here.)
|
||||
def destroy_collective_group(group_name: str = "default") -> None:
|
||||
"""Destroy a collective group given its group name."""
|
||||
_check_inside_actor()
|
||||
global _group_mgr
|
||||
global _group_mgr_lock
|
||||
with _group_mgr_lock:
|
||||
_group_mgr.destroy_collective_group(group_name)
|
||||
|
||||
|
||||
def get_rank(group_name: str = "default") -> int:
|
||||
"""Return the rank of this process in the given group.
|
||||
|
||||
Args:
|
||||
group_name: the name of the group to query
|
||||
|
||||
Returns:
|
||||
the rank of this process in the named group,
|
||||
-1 if the group does not exist or the process does
|
||||
not belong to the group.
|
||||
"""
|
||||
_check_inside_actor()
|
||||
|
||||
global _group_mgr
|
||||
global _group_mgr_lock
|
||||
with _group_mgr_lock:
|
||||
if not _group_mgr.is_group_exist(group_name):
|
||||
return -1
|
||||
g = _group_mgr.get_group_by_name(group_name)
|
||||
return g.rank
|
||||
|
||||
|
||||
def get_collective_group_size(group_name: str = "default") -> int:
|
||||
"""Return the size of the collective group with the given name.
|
||||
|
||||
Args:
|
||||
group_name: the name of the group to query
|
||||
|
||||
Returns:
|
||||
The world size of the collective group, -1 if the group does
|
||||
not exist or the process does not belong to the group.
|
||||
"""
|
||||
_check_inside_actor()
|
||||
global _group_mgr
|
||||
global _group_mgr_lock
|
||||
with _group_mgr_lock:
|
||||
if not _group_mgr.is_group_exist(group_name):
|
||||
return -1
|
||||
g = _group_mgr.get_group_by_name(group_name)
|
||||
return g.world_size
|
||||
|
||||
|
||||
def allreduce(
|
||||
tensor: Any,
|
||||
group_name: str = "default",
|
||||
op: types.ReduceOp = types.ReduceOp.SUM,
|
||||
):
|
||||
"""Collective allreduce the tensor across the group.
|
||||
|
||||
Args:
|
||||
tensor: the tensor to be all-reduced on this process.
|
||||
group_name: the collective group name to perform allreduce.
|
||||
op: The reduce operation.
|
||||
"""
|
||||
_check_single_tensor_input(tensor)
|
||||
g = get_group_handle(group_name)
|
||||
opts = types.AllReduceOptions
|
||||
opts.reduceOp = op
|
||||
g.allreduce([tensor], opts)
|
||||
|
||||
|
||||
def allreduce_multigpu(
|
||||
tensor_list: list,
|
||||
group_name: str = "default",
|
||||
op: types.ReduceOp = types.ReduceOp.SUM,
|
||||
):
|
||||
"""Collective allreduce a list of tensors across the group.
|
||||
|
||||
Args:
|
||||
tensor_list: list of tensors to be allreduced, each on a GPU.
|
||||
group_name: the collective group name to perform allreduce.
|
||||
op: The reduce operation.
|
||||
"""
|
||||
if not types.cupy_available():
|
||||
raise RuntimeError("Multigpu calls requires NCCL and Cupy.")
|
||||
_check_tensor_list_input(tensor_list)
|
||||
g = get_group_handle(group_name)
|
||||
opts = types.AllReduceOptions
|
||||
opts.reduceOp = op
|
||||
g.allreduce(tensor_list, opts)
|
||||
|
||||
|
||||
def barrier(group_name: str = "default"):
|
||||
"""Barrier all processes in the collective group.
|
||||
|
||||
Args:
|
||||
group_name: the name of the group to barrier.
|
||||
"""
|
||||
g = get_group_handle(group_name)
|
||||
g.barrier()
|
||||
|
||||
|
||||
def reduce(
|
||||
tensor: Any,
|
||||
dst_rank: int = 0,
|
||||
group_name: str = "default",
|
||||
op: types.ReduceOp = types.ReduceOp.SUM,
|
||||
):
|
||||
"""Reduce the tensor across the group to the destination rank.
|
||||
|
||||
Args:
|
||||
tensor: the tensor to be reduced on this process.
|
||||
dst_rank: the rank of the destination process.
|
||||
group_name: the collective group name to perform reduce.
|
||||
op: The reduce operation.
|
||||
"""
|
||||
_check_single_tensor_input(tensor)
|
||||
g = get_group_handle(group_name)
|
||||
|
||||
# check dst rank
|
||||
_check_rank_valid(g, dst_rank)
|
||||
opts = types.ReduceOptions()
|
||||
opts.reduceOp = op
|
||||
opts.root_rank = dst_rank
|
||||
opts.root_tensor = 0
|
||||
g.reduce([tensor], opts)
|
||||
|
||||
|
||||
def reduce_multigpu(
|
||||
tensor_list: list,
|
||||
dst_rank: int = 0,
|
||||
dst_tensor: int = 0,
|
||||
group_name: str = "default",
|
||||
op: types.ReduceOp = types.ReduceOp.SUM,
|
||||
):
|
||||
"""Reduce the tensor across the group to the destination rank
|
||||
and destination tensor.
|
||||
|
||||
Args:
|
||||
tensor_list: the list of tensors to be reduced on this process;
|
||||
each tensor located on a GPU.
|
||||
dst_rank: the rank of the destination process.
|
||||
dst_tensor: the index of GPU at the destination.
|
||||
group_name: the collective group name to perform reduce.
|
||||
op: The reduce operation.
|
||||
"""
|
||||
if not types.cupy_available():
|
||||
raise RuntimeError("Multigpu calls requires NCCL and Cupy.")
|
||||
_check_tensor_list_input(tensor_list)
|
||||
g = get_group_handle(group_name)
|
||||
|
||||
# check dst rank
|
||||
_check_rank_valid(g, dst_rank)
|
||||
_check_root_tensor_valid(len(tensor_list), dst_tensor)
|
||||
opts = types.ReduceOptions()
|
||||
opts.reduceOp = op
|
||||
opts.root_rank = dst_rank
|
||||
opts.root_tensor = dst_tensor
|
||||
g.reduce(tensor_list, opts)
|
||||
|
||||
|
||||
def broadcast(tensor: Any, src_rank: int = 0, group_name: str = "default"):
|
||||
"""Broadcast the tensor from a source process to all others.
|
||||
|
||||
Args:
|
||||
tensor: the tensor to be broadcasted (src) or received (destination).
|
||||
src_rank: the rank of the source process.
|
||||
group_name: the collective group name to perform broadcast.
|
||||
"""
|
||||
_check_single_tensor_input(tensor)
|
||||
g = get_group_handle(group_name)
|
||||
|
||||
# check src rank
|
||||
_check_rank_valid(g, src_rank)
|
||||
opts = types.BroadcastOptions()
|
||||
opts.root_rank = src_rank
|
||||
opts.root_tensor = 0
|
||||
g.broadcast([tensor], opts)
|
||||
|
||||
|
||||
def broadcast_multigpu(
|
||||
tensor_list: list,
|
||||
src_rank: int = 0,
|
||||
src_tensor: int = 0,
|
||||
group_name: str = "default",
|
||||
):
|
||||
"""Broadcast the tensor from a source GPU to all other GPUs.
|
||||
|
||||
Args:
|
||||
tensor_list: the tensors to broadcast (src) or receive (dst).
|
||||
src_rank: the rank of the source process.
|
||||
src_tensor: the index of the source GPU on the source process.
|
||||
group_name: the collective group name to perform broadcast.
|
||||
"""
|
||||
if not types.cupy_available():
|
||||
raise RuntimeError("Multigpu calls requires NCCL and Cupy.")
|
||||
_check_tensor_list_input(tensor_list)
|
||||
g = get_group_handle(group_name)
|
||||
|
||||
# check src rank
|
||||
_check_rank_valid(g, src_rank)
|
||||
_check_root_tensor_valid(len(tensor_list), src_tensor)
|
||||
opts = types.BroadcastOptions()
|
||||
opts.root_rank = src_rank
|
||||
opts.root_tensor = src_tensor
|
||||
g.broadcast(tensor_list, opts)
|
||||
|
||||
|
||||
def allgather(tensor_list: list, tensor: Any, group_name: str = "default"):
|
||||
"""Allgather tensors from each process of the group into a list.
|
||||
|
||||
Args:
|
||||
tensor_list: the results, stored as a list of tensors.
|
||||
tensor: the tensor (to be gathered) in the current process
|
||||
group_name: the name of the collective group.
|
||||
"""
|
||||
_check_single_tensor_input(tensor)
|
||||
_check_tensor_list_input(tensor_list)
|
||||
g = get_group_handle(group_name)
|
||||
if len(tensor_list) != g.world_size:
|
||||
# Typically CLL lib requires len(tensor_list) >= world_size;
|
||||
# Here we make it more strict: len(tensor_list) == world_size.
|
||||
raise RuntimeError(
|
||||
"The length of the tensor list operands to allgather "
|
||||
"must be equal to world_size."
|
||||
)
|
||||
opts = types.AllGatherOptions()
|
||||
g.allgather([tensor_list], [tensor], opts)
|
||||
|
||||
|
||||
def allgather_multigpu(
|
||||
output_tensor_lists: list, input_tensor_list: list, group_name: str = "default"
|
||||
):
|
||||
"""Allgather tensors from each gpus of the group into lists.
|
||||
|
||||
Args:
|
||||
output_tensor_lists: gathered results, with shape
|
||||
must be num_gpus * world_size * shape(tensor).
|
||||
input_tensor_list: a list of tensors, with shape
|
||||
num_gpus * shape(tensor).
|
||||
group_name: the name of the collective group.
|
||||
"""
|
||||
if not types.cupy_available():
|
||||
raise RuntimeError("Multigpu calls requires NCCL and Cupy.")
|
||||
_check_tensor_lists_input(output_tensor_lists)
|
||||
_check_tensor_list_input(input_tensor_list)
|
||||
g = get_group_handle(group_name)
|
||||
opts = types.AllGatherOptions()
|
||||
g.allgather(output_tensor_lists, input_tensor_list, opts)
|
||||
|
||||
|
||||
def reducescatter(
|
||||
tensor: Any,
|
||||
tensor_list: list,
|
||||
group_name: str = "default",
|
||||
op: types.ReduceOp = types.ReduceOp.SUM,
|
||||
):
|
||||
"""Reducescatter a list of tensors across the group.
|
||||
|
||||
Reduce the list of the tensors across each process in the group, then
|
||||
scatter the reduced list of tensors -- one tensor for each process.
|
||||
|
||||
Args:
|
||||
tensor: the resulted tensor on this process.
|
||||
tensor_list: The list of tensors to be reduced and scattered.
|
||||
group_name: the name of the collective group.
|
||||
op: The reduce operation.
|
||||
"""
|
||||
_check_single_tensor_input(tensor)
|
||||
_check_tensor_list_input(tensor_list)
|
||||
g = get_group_handle(group_name)
|
||||
opts = types.ReduceScatterOptions()
|
||||
opts.reduceOp = op
|
||||
if len(tensor_list) != g.world_size:
|
||||
raise RuntimeError(
|
||||
"The length of the tensor list operands to reducescatter "
|
||||
"must not be equal to world_size."
|
||||
)
|
||||
g.reducescatter([tensor], [tensor_list], opts)
|
||||
|
||||
|
||||
def reducescatter_multigpu(
|
||||
output_tensor_list: list,
|
||||
input_tensor_lists: list,
|
||||
group_name: str = "default",
|
||||
op: types.ReduceOp = types.ReduceOp.SUM,
|
||||
):
|
||||
"""Reducescatter a list of tensors across all GPUs.
|
||||
|
||||
Args:
|
||||
output_tensor_list: the resulted list of tensors, with
|
||||
shape: num_gpus * shape(tensor).
|
||||
input_tensor_lists: the original tensors, with shape:
|
||||
num_gpus * world_size * shape(tensor).
|
||||
group_name: the name of the collective group.
|
||||
op: The reduce operation.
|
||||
"""
|
||||
if not types.cupy_available():
|
||||
raise RuntimeError("Multigpu calls requires NCCL and Cupy.")
|
||||
_check_tensor_lists_input(input_tensor_lists)
|
||||
_check_tensor_list_input(output_tensor_list)
|
||||
g = get_group_handle(group_name)
|
||||
opts = types.ReduceScatterOptions()
|
||||
opts.reduceOp = op
|
||||
g.reducescatter(output_tensor_list, input_tensor_lists, opts)
|
||||
|
||||
|
||||
def send(tensor: Any, dst_rank: int, group_name: str = "default"):
|
||||
"""Send a tensor to a remote process synchronously.
|
||||
|
||||
Args:
|
||||
tensor: the tensor to send.
|
||||
dst_rank: the rank of the destination process.
|
||||
group_name: the name of the collective group.
|
||||
"""
|
||||
_check_single_tensor_input(tensor)
|
||||
g = get_group_handle(group_name)
|
||||
_check_rank_valid(g, dst_rank)
|
||||
if dst_rank == g.rank:
|
||||
raise RuntimeError("The destination rank '{}' is self.".format(dst_rank))
|
||||
opts = types.SendOptions()
|
||||
opts.dst_rank = dst_rank
|
||||
g.send([tensor], opts)
|
||||
|
||||
|
||||
def send_multigpu(
|
||||
tensor: Any,
|
||||
dst_rank: int,
|
||||
dst_gpu_index: int,
|
||||
group_name: str = "default",
|
||||
n_elements: int = 0,
|
||||
):
|
||||
"""Send a tensor to a remote GPU synchronously.
|
||||
|
||||
The function assumes each process owns >1 GPUs, and the sender
|
||||
process and receiver process has equal number of GPUs.
|
||||
|
||||
Args:
|
||||
tensor: the tensor to send, located on a GPU.
|
||||
dst_rank: the rank of the destination process.
|
||||
dst_gpu_index: the destination gpu index.
|
||||
group_name: the name of the collective group.
|
||||
n_elements: if specified, send the next n elements
|
||||
from the starting address of tensor.
|
||||
"""
|
||||
if not types.cupy_available():
|
||||
raise RuntimeError("send_multigpu call requires NCCL.")
|
||||
_check_single_tensor_input(tensor)
|
||||
g = get_group_handle(group_name)
|
||||
_check_rank_valid(g, dst_rank)
|
||||
if dst_rank == g.rank:
|
||||
raise RuntimeError(
|
||||
"The dst_rank '{}' is self. Considering "
|
||||
"doing GPU to GPU memcpy instead?".format(dst_rank)
|
||||
)
|
||||
if n_elements < 0:
|
||||
raise RuntimeError("The n_elements '{}' should >= 0.".format(n_elements))
|
||||
opts = types.SendOptions()
|
||||
opts.dst_rank = dst_rank
|
||||
opts.dst_gpu_index = dst_gpu_index
|
||||
opts.n_elements = n_elements
|
||||
g.send([tensor], opts)
|
||||
|
||||
|
||||
def recv(tensor: Any, src_rank: int, group_name: str = "default"):
|
||||
"""Receive a tensor from a remote process synchronously.
|
||||
|
||||
Args:
|
||||
tensor: the received tensor.
|
||||
src_rank: the rank of the source process.
|
||||
group_name: the name of the collective group.
|
||||
"""
|
||||
_check_single_tensor_input(tensor)
|
||||
g = get_group_handle(group_name)
|
||||
_check_rank_valid(g, src_rank)
|
||||
if src_rank == g.rank:
|
||||
raise RuntimeError("The destination rank '{}' is self.".format(src_rank))
|
||||
opts = types.RecvOptions()
|
||||
opts.src_rank = src_rank
|
||||
g.recv([tensor], opts)
|
||||
|
||||
|
||||
def recv_multigpu(
|
||||
tensor: Any,
|
||||
src_rank: int,
|
||||
src_gpu_index: int,
|
||||
group_name: str = "default",
|
||||
n_elements: int = 0,
|
||||
):
|
||||
"""Receive a tensor from a remote GPU synchronously.
|
||||
|
||||
The function asssume each process owns >1 GPUs, and the sender
|
||||
process and receiver process has equal nubmer of GPUs.
|
||||
|
||||
Args:
|
||||
tensor: The received tensor, located on a GPU.
|
||||
src_rank: The rank of the source process.
|
||||
src_gpu_index: The index of the source GPU on the src process.
|
||||
group_name: The name of the collective group.
|
||||
n_elements: if specified, receive the next n elements
|
||||
from the starting address of tensor.
|
||||
"""
|
||||
if not types.cupy_available():
|
||||
raise RuntimeError("recv_multigpu call requires NCCL.")
|
||||
_check_single_tensor_input(tensor)
|
||||
g = get_group_handle(group_name)
|
||||
_check_rank_valid(g, src_rank)
|
||||
if src_rank == g.rank:
|
||||
raise RuntimeError(
|
||||
"The dst_rank '{}' is self. Considering "
|
||||
"doing GPU to GPU memcpy instead?".format(src_rank)
|
||||
)
|
||||
if n_elements < 0:
|
||||
raise RuntimeError("The n_elements '{}' should be >= 0.".format(n_elements))
|
||||
opts = types.RecvOptions()
|
||||
opts.src_rank = src_rank
|
||||
opts.src_gpu_index = src_gpu_index
|
||||
opts.n_elements = n_elements
|
||||
g.recv([tensor], opts)
|
||||
|
||||
|
||||
def synchronize(gpu_id: int):
|
||||
"""Synchronize the current process to a give device.
|
||||
|
||||
Args:
|
||||
gpu_id: the GPU device id to synchronize.
|
||||
"""
|
||||
if not types.cupy_available():
|
||||
raise RuntimeError("synchronize call requires CUDA and NCCL.")
|
||||
import cupy as cp
|
||||
|
||||
cp.cuda.Device(gpu_id).synchronize()
|
||||
|
||||
|
||||
def get_group_handle(group_name: str = "default"):
|
||||
"""Check if the group is initialized and return the group handle.
|
||||
|
||||
Args:
|
||||
group_name: the name of the collective group.
|
||||
|
||||
Returns:
|
||||
The collective group handle.
|
||||
"""
|
||||
_check_inside_actor()
|
||||
global _group_mgr
|
||||
global _group_mgr_lock
|
||||
with _group_mgr_lock:
|
||||
if not _group_mgr.is_group_exist(group_name):
|
||||
# try loading from remote info store
|
||||
try:
|
||||
# if the information is stored in an Info object,
|
||||
# get and create the group.
|
||||
name = "info_" + group_name
|
||||
mgr = ray.get_actor(name=name)
|
||||
ids, world_size, rank, backend, gloo_timeout = ray.get(
|
||||
mgr.get_info.remote()
|
||||
)
|
||||
worker = ray._private.worker.global_worker
|
||||
id_ = worker.core_worker.get_actor_id()
|
||||
r = rank[ids.index(id_)]
|
||||
_group_mgr.create_collective_group(
|
||||
backend, world_size, r, group_name, gloo_timeout
|
||||
)
|
||||
except ValueError as exc:
|
||||
# check if this group is initialized using options()
|
||||
if (
|
||||
"collective_group_name" in os.environ
|
||||
and os.environ["collective_group_name"] == group_name
|
||||
):
|
||||
rank = int(os.environ["collective_rank"])
|
||||
world_size = int(os.environ["collective_world_size"])
|
||||
backend = os.environ["collective_backend"]
|
||||
gloo_timeout = int(os.getenv("collective_gloo_timeout", 30000))
|
||||
_group_mgr.create_collective_group(
|
||||
backend, world_size, rank, group_name, gloo_timeout
|
||||
)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"The collective group '{}' is not "
|
||||
"initialized in the process.".format(group_name)
|
||||
) from exc
|
||||
g = _group_mgr.get_group_by_name(group_name)
|
||||
return g
|
||||
|
||||
|
||||
def _check_single_tensor_input(tensor):
|
||||
"""Check if the tensor is with a supported type."""
|
||||
if isinstance(tensor, np.ndarray):
|
||||
return
|
||||
if types.cupy_available():
|
||||
if isinstance(tensor, types.cp.ndarray):
|
||||
return
|
||||
if types.torch_available():
|
||||
if isinstance(tensor, types.th.Tensor):
|
||||
return
|
||||
raise RuntimeError(
|
||||
"Unrecognized tensor type '{}'. Supported types are: "
|
||||
"np.ndarray, torch.Tensor, cupy.ndarray.".format(type(tensor))
|
||||
)
|
||||
|
||||
|
||||
def _check_inside_actor():
|
||||
"""Check if currently it is inside a Ray actor/task."""
|
||||
worker = ray._private.worker.global_worker
|
||||
if worker.mode == ray.WORKER_MODE:
|
||||
return
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"The collective APIs shall be only used inside a Ray actor or task."
|
||||
)
|
||||
|
||||
|
||||
def _check_rank_valid(g, rank: int):
|
||||
"""Check the rank: 0 <= rank < world_size."""
|
||||
if rank < 0:
|
||||
raise ValueError("rank '{}' is negative.".format(rank))
|
||||
if rank >= g.world_size:
|
||||
raise ValueError(
|
||||
"rank '{}' must be less than world size '{}'".format(rank, g.world_size)
|
||||
)
|
||||
|
||||
|
||||
def _check_tensor_list_input(tensor_list):
|
||||
"""Check if the input is a list of supported tensor types."""
|
||||
if not isinstance(tensor_list, list):
|
||||
raise RuntimeError(
|
||||
"The input must be a list of tensors. "
|
||||
"Got '{}'.".format(type(tensor_list))
|
||||
)
|
||||
if not tensor_list:
|
||||
raise RuntimeError("Got an empty list of tensors.")
|
||||
for t in tensor_list:
|
||||
_check_single_tensor_input(t)
|
||||
|
||||
|
||||
def _check_tensor_lists_input(tensor_lists):
|
||||
"""Check if the input is a list of lists of supported tensor types."""
|
||||
if not isinstance(tensor_lists, list):
|
||||
raise RuntimeError(
|
||||
"The input must be a list of lists of tensors. "
|
||||
"Got '{}'.".format(type(tensor_lists))
|
||||
)
|
||||
if not tensor_lists:
|
||||
raise RuntimeError(f"Did not receive tensors. Got: {tensor_lists}")
|
||||
for t in tensor_lists:
|
||||
_check_tensor_list_input(t)
|
||||
|
||||
|
||||
def _check_root_tensor_valid(length, root_tensor):
|
||||
"""Check the root_tensor device is 0 <= root_tensor < length"""
|
||||
if root_tensor < 0:
|
||||
raise ValueError("root_tensor '{}' is negative.".format(root_tensor))
|
||||
if root_tensor >= length:
|
||||
raise ValueError(
|
||||
"root_tensor '{}' is greater than the number of GPUs: "
|
||||
"'{}'".format(root_tensor, length)
|
||||
)
|
||||
@@ -0,0 +1,91 @@
|
||||
"""Abstract class for collective groups."""
|
||||
from abc import ABCMeta, abstractmethod
|
||||
|
||||
from ray.util.collective.types import (
|
||||
AllGatherOptions,
|
||||
AllReduceOptions,
|
||||
BarrierOptions,
|
||||
BroadcastOptions,
|
||||
RecvOptions,
|
||||
ReduceOptions,
|
||||
ReduceScatterOptions,
|
||||
SendOptions,
|
||||
)
|
||||
|
||||
|
||||
class BaseGroup(metaclass=ABCMeta):
|
||||
def __init__(self, world_size: int, rank: int, group_name: str):
|
||||
"""Init the process group with basic information.
|
||||
|
||||
Args:
|
||||
world_size: The total number of processes in the group.
|
||||
rank: The rank of the current process.
|
||||
group_name: The group name.
|
||||
"""
|
||||
self._world_size = world_size
|
||||
self._rank = rank
|
||||
self._group_name = group_name
|
||||
|
||||
@property
|
||||
def rank(self):
|
||||
"""Return the rank of the current process."""
|
||||
return self._rank
|
||||
|
||||
@property
|
||||
def world_size(self):
|
||||
"""Return the number of processes in this group."""
|
||||
return self._world_size
|
||||
|
||||
@property
|
||||
def group_name(self):
|
||||
"""Return the group name of this group."""
|
||||
return self._group_name
|
||||
|
||||
def destroy_group(self):
|
||||
"""GC the communicators."""
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def backend(cls):
|
||||
"""The backend of this collective group."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
@abstractmethod
|
||||
def check_backend_availability(cls) -> bool:
|
||||
"""Check if the backend is available."""
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def allreduce(self, tensor, allreduce_options=AllReduceOptions()):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def barrier(self, barrier_options=BarrierOptions()):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def reduce(self, tensor, reduce_options=ReduceOptions()):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def allgather(self, tensor_list, tensor, allgather_options=AllGatherOptions()):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def broadcast(self, tensor, broadcast_options=BroadcastOptions()):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def reducescatter(
|
||||
self, tensor, tensor_list, reducescatter_options=ReduceScatterOptions()
|
||||
):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def send(self, tensor, send_options: SendOptions):
|
||||
raise NotImplementedError()
|
||||
|
||||
@abstractmethod
|
||||
def recv(self, tensor, recv_options: RecvOptions):
|
||||
raise NotImplementedError()
|
||||
@@ -0,0 +1,95 @@
|
||||
import logging
|
||||
import threading
|
||||
|
||||
import cupy
|
||||
|
||||
from ray.util.collective.collective_group import nccl_util
|
||||
from ray.util.collective.const import ENV
|
||||
|
||||
NCCL_STREAM_POOL_SIZE = 32
|
||||
MAX_GPU_PER_ACTOR = 16
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class StreamPool:
|
||||
"""The class that represents a stream pool associated with a GPU.
|
||||
|
||||
When multistream is enabled, we will allocate a pool of streams for each
|
||||
GPU, and get available stream from this pool when a collective kernel is
|
||||
initialized. This enables overlapping computation/communication kernels
|
||||
using multiple CUDA streams, given that the streams a appropriately
|
||||
synchronized. The class is thread-safe.
|
||||
|
||||
|
||||
Args:
|
||||
device_idx: the absolute index of the device for this pool.
|
||||
"""
|
||||
|
||||
def __init__(self, device_idx: int):
|
||||
self.device_idx = device_idx
|
||||
|
||||
self._initialized = False
|
||||
self._initialized_lock = threading.Lock()
|
||||
|
||||
self._pool = [None] * NCCL_STREAM_POOL_SIZE
|
||||
self._counter = 0
|
||||
self._pool_lock = threading.Lock()
|
||||
|
||||
def get_stream(self):
|
||||
"""Get an available stream from the pool.
|
||||
|
||||
The function locks the stream pool and releases the lock before
|
||||
returning.
|
||||
|
||||
Returns:
|
||||
stream (cupy.cuda.Stream): the returned stream from pool.
|
||||
"""
|
||||
|
||||
# check the flag
|
||||
self._initialized_lock.acquire()
|
||||
if not self._initialized:
|
||||
self._init_once()
|
||||
self._initialized_lock.release()
|
||||
|
||||
# Get the stream from the pool.
|
||||
self._pool_lock.acquire()
|
||||
stream = self._pool[self._counter]
|
||||
self._counter = (self._counter + 1) % NCCL_STREAM_POOL_SIZE
|
||||
self._pool_lock.release()
|
||||
return stream
|
||||
|
||||
def _init_once(self):
|
||||
"""Initialize the stream pool only for once."""
|
||||
with nccl_util.Device(self.device_idx):
|
||||
for i in range(NCCL_STREAM_POOL_SIZE):
|
||||
# this is the only place where self._pool will be written.
|
||||
if ENV.NCCL_USE_MULTISTREAM.val:
|
||||
logger.debug("NCCL multistream enabled.")
|
||||
self._pool[i] = cupy.cuda.Stream(null=False, non_blocking=False)
|
||||
else:
|
||||
logger.debug("NCCL multistream disabled.")
|
||||
self._pool[i] = cupy.cuda.Stream.null
|
||||
self._init_flag = True
|
||||
|
||||
|
||||
# This is a map from GPU index to its stream pool.
|
||||
# It is supposed to be READ-ONLY out of this file
|
||||
_device_stream_pool_map = dict()
|
||||
|
||||
|
||||
def _init_stream_pool():
|
||||
global _device_stream_pool_map
|
||||
for i in range(MAX_GPU_PER_ACTOR):
|
||||
_device_stream_pool_map[i] = StreamPool(i)
|
||||
|
||||
|
||||
def get_stream_pool(device_idx):
|
||||
"""Get the CUDA stream pool of a GPU device."""
|
||||
# In case there will be multiple threads writing to the pool.
|
||||
lock = threading.Lock()
|
||||
lock.acquire()
|
||||
if not _device_stream_pool_map:
|
||||
_init_stream_pool()
|
||||
lock.release()
|
||||
return _device_stream_pool_map[device_idx]
|
||||
@@ -0,0 +1,850 @@
|
||||
import datetime
|
||||
import logging
|
||||
import time
|
||||
from typing import Callable, List, Optional
|
||||
|
||||
import ray
|
||||
from ray.util.collective.collective_group import nccl_util
|
||||
from ray.util.collective.collective_group.base_collective_group import BaseGroup
|
||||
from ray.util.collective.collective_group.cuda_stream import get_stream_pool
|
||||
from ray.util.collective.const import ENV, get_store_name
|
||||
from ray.util.collective.types import (
|
||||
AllGatherOptions,
|
||||
AllReduceOptions,
|
||||
Backend,
|
||||
BarrierOptions,
|
||||
BroadcastOptions,
|
||||
RecvOptions,
|
||||
ReduceOptions,
|
||||
ReduceScatterOptions,
|
||||
SendOptions,
|
||||
torch_available,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import cupy
|
||||
import torch
|
||||
|
||||
_NCCL_AVAILABLE = True
|
||||
_LOG_NCCL_WARNING = False
|
||||
except ImportError:
|
||||
_NCCL_AVAILABLE = False
|
||||
_LOG_NCCL_WARNING = True
|
||||
|
||||
|
||||
class Rendezvous:
|
||||
"""A rendezvous class for different actor/task processes to meet.
|
||||
|
||||
To initialize an NCCL collective communication group, different
|
||||
actors/tasks spawned in Ray in a collective group needs to meet
|
||||
each other to synchronize the NCCLUniqueID. This class guarantees
|
||||
they meet via the NCCLUniqueIDStore, initialized on the rank=0
|
||||
process.
|
||||
|
||||
Args:
|
||||
store_key: the unique store key, usually as a concatanation
|
||||
of group_name and communicator key. See `get_nccl_communicator`
|
||||
for more details.
|
||||
"""
|
||||
|
||||
def __init__(self, store_key: str):
|
||||
if not store_key:
|
||||
raise ValueError(
|
||||
"Invalid store_key. The store_key is a concatenation of "
|
||||
"'group_name' and the 'communicator_key'. See the "
|
||||
"docstring of `get_nccl_communicator` for details."
|
||||
)
|
||||
self._store_key = store_key
|
||||
self._store_name = None
|
||||
self._store = None
|
||||
|
||||
def meet(self, timeout_s: int = 180):
|
||||
"""Meet at the named actor store.
|
||||
|
||||
Args:
|
||||
timeout_s: timeout in seconds.
|
||||
"""
|
||||
if timeout_s <= 0:
|
||||
raise ValueError(
|
||||
"The 'timeout' argument must be positive. "
|
||||
"Got '{}'.".format(timeout_s)
|
||||
)
|
||||
self._store_name = get_store_name(self._store_key)
|
||||
timeout_delta = datetime.timedelta(seconds=timeout_s)
|
||||
elapsed = datetime.timedelta(seconds=0)
|
||||
start_time = datetime.datetime.now()
|
||||
while elapsed < timeout_delta:
|
||||
try:
|
||||
logger.debug(
|
||||
"Trying to meet at the store '{}'".format(self._store_name)
|
||||
)
|
||||
self._store = ray.get_actor(self._store_name)
|
||||
except ValueError:
|
||||
logger.debug(
|
||||
"Failed to meet at the store '{}'."
|
||||
"Trying again...".format(self._store_name)
|
||||
)
|
||||
time.sleep(1)
|
||||
elapsed = datetime.datetime.now() - start_time
|
||||
continue
|
||||
logger.debug("Successful rendezvous!")
|
||||
break
|
||||
if not self._store:
|
||||
raise RuntimeError(
|
||||
"Unable to meet other processes "
|
||||
"at the rendezvous store. If you are using "
|
||||
"P2P communication, please check if tensors "
|
||||
"are put in the correct GPU. "
|
||||
)
|
||||
|
||||
@property
|
||||
def store(self):
|
||||
return self._store
|
||||
|
||||
def get_nccl_id(self, timeout_s: int = 180):
|
||||
"""Get the NCCLUniqueID from the store through Ray.
|
||||
|
||||
Args:
|
||||
timeout_s: timeout in seconds.
|
||||
|
||||
Returns:
|
||||
uid: the NCCLUniqueID if successful.
|
||||
"""
|
||||
if not self._store:
|
||||
raise ValueError("Rendezvous store is not setup.")
|
||||
try:
|
||||
uid = ray.get(self._store.wait_and_get_id.remote(), timeout=timeout_s)
|
||||
except ray.exceptions.GetTimeoutError:
|
||||
raise RuntimeError(
|
||||
f"Unable to get the NCCLUniqueID from the store within {timeout_s} seconds."
|
||||
) from None
|
||||
return uid
|
||||
|
||||
|
||||
class NCCLGroup(BaseGroup):
|
||||
def __init__(self, world_size: int, rank: int, group_name: str):
|
||||
"""Init an NCCL collective group."""
|
||||
super(NCCLGroup, self).__init__(world_size, rank, group_name)
|
||||
|
||||
# communicator and stream cache.
|
||||
# TODO (Hao): we need a lock here...
|
||||
self._dev_comm_map = {}
|
||||
self._dev_streams_map = {}
|
||||
|
||||
# record the used GPU IDs.
|
||||
self._used_gpu_indices = set()
|
||||
|
||||
# TODO(Fu): might need an event map
|
||||
self._dev_event_map = {}
|
||||
|
||||
if nccl_util.get_nccl_build_version() < 2000:
|
||||
raise RuntimeError("NCCL in Ray requires NCCL >= 2.0.")
|
||||
if nccl_util.get_nccl_runtime_version() < 2704:
|
||||
logger.warning("NCCL send/recv calls requires NCCL>=2.7.4")
|
||||
|
||||
def destroy_group(self):
|
||||
"""Destroy the group and release NCCL communicators."""
|
||||
if len(self._dev_comm_map.keys()) > 0:
|
||||
# TODO(Hao): check this barrier call
|
||||
# self.barrier()
|
||||
|
||||
# Destroy the communicators and streams.
|
||||
for comm_key, comms in self._dev_comm_map.items():
|
||||
for c in comms:
|
||||
c.destroy()
|
||||
self._dev_comm_map[comm_key] = None
|
||||
|
||||
if self.rank == 0:
|
||||
for comm_key in self._dev_comm_map:
|
||||
assert not self._dev_comm_map[comm_key]
|
||||
group_key = self._generate_group_key(comm_key)
|
||||
self._destroy_store(group_key)
|
||||
self._barrier_tensor = None
|
||||
self._dev_comm_map = None
|
||||
self._dev_streams_map = None
|
||||
super(NCCLGroup, self).destroy_group()
|
||||
|
||||
@classmethod
|
||||
def backend(cls):
|
||||
return Backend.NCCL
|
||||
|
||||
@classmethod
|
||||
def check_backend_availability(cls) -> bool:
|
||||
global _LOG_NCCL_WARNING, _NCCL_AVAILABLE
|
||||
if _LOG_NCCL_WARNING:
|
||||
logger.warning(
|
||||
"NCCL is not available. Please install Cupy "
|
||||
"following the guide at: "
|
||||
"https://docs.cupy.dev/en/stable/install.html."
|
||||
)
|
||||
_LOG_NCCL_WARNING = False
|
||||
return _NCCL_AVAILABLE
|
||||
|
||||
def allreduce(
|
||||
self,
|
||||
tensors: list,
|
||||
allreduce_options: AllReduceOptions = AllReduceOptions(),
|
||||
):
|
||||
"""AllReduce tensors across the collective group following options.
|
||||
|
||||
Args:
|
||||
tensors: the list of tensors to be reduced. Each tensor must
|
||||
reside on one GPU of the current process.
|
||||
allreduce_options: allreduce options.
|
||||
"""
|
||||
|
||||
def collective_fn(input_tensor, output_tensor, comm, stream):
|
||||
comm.allReduce(
|
||||
nccl_util.get_tensor_ptr(input_tensor),
|
||||
nccl_util.get_tensor_ptr(output_tensor),
|
||||
nccl_util.get_tensor_n_elements(input_tensor),
|
||||
nccl_util.get_nccl_tensor_dtype(input_tensor),
|
||||
nccl_util.get_nccl_reduce_op(allreduce_options.reduceOp),
|
||||
stream.ptr,
|
||||
)
|
||||
|
||||
self._collective(tensors, tensors, collective_fn)
|
||||
|
||||
def barrier(self, barrier_options: BarrierOptions = BarrierOptions()):
|
||||
"""Blocks until all processes reach this barrier.
|
||||
|
||||
Args:
|
||||
barrier_options: barrier options.
|
||||
"""
|
||||
# Get the device list.
|
||||
if self._used_gpu_indices:
|
||||
devices = list(self._used_gpu_indices)
|
||||
else:
|
||||
devices = list(range(nccl_util.get_num_gpus()))
|
||||
barrier_tensors = [None] * len(devices)
|
||||
for i, d in enumerate(devices):
|
||||
with nccl_util.Device(d):
|
||||
barrier_tensors[i] = cupy.array([1])
|
||||
self.allreduce(barrier_tensors)
|
||||
|
||||
def reduce(self, tensors: list, reduce_options: ReduceOptions = ReduceOptions()):
|
||||
"""Reduce tensors to a destination gpu following options.
|
||||
|
||||
Args:
|
||||
tensors: the list of tensors to be reduced, each tensor
|
||||
must reside on one gpu of the current process.
|
||||
reduce_options: reduce options.
|
||||
"""
|
||||
root_rank = len(tensors) * reduce_options.root_rank + reduce_options.root_tensor
|
||||
|
||||
def collective_fn(input_tensor, output_tensor, comm, stream):
|
||||
comm.reduce(
|
||||
nccl_util.get_tensor_ptr(input_tensor),
|
||||
nccl_util.get_tensor_ptr(output_tensor),
|
||||
nccl_util.get_tensor_n_elements(input_tensor),
|
||||
nccl_util.get_nccl_tensor_dtype(input_tensor),
|
||||
nccl_util.get_nccl_reduce_op(reduce_options.reduceOp),
|
||||
root_rank,
|
||||
stream.ptr,
|
||||
)
|
||||
|
||||
self._collective(tensors, tensors, collective_fn)
|
||||
|
||||
def broadcast(
|
||||
self,
|
||||
tensors: list,
|
||||
broadcast_options: BroadcastOptions = BroadcastOptions(),
|
||||
):
|
||||
"""Broadcast tensors to all other gpus following options.
|
||||
|
||||
Args:
|
||||
tensors: tensors to be broadcast or received.
|
||||
broadcast_options: broadcast options.
|
||||
"""
|
||||
root_rank = (
|
||||
len(tensors) * broadcast_options.root_rank + broadcast_options.root_tensor
|
||||
)
|
||||
|
||||
def collective_fn(input_tensor, output_tensor, comm, stream):
|
||||
comm.broadcast(
|
||||
nccl_util.get_tensor_ptr(input_tensor),
|
||||
nccl_util.get_tensor_ptr(output_tensor),
|
||||
nccl_util.get_tensor_n_elements(input_tensor),
|
||||
nccl_util.get_nccl_tensor_dtype(input_tensor),
|
||||
root_rank,
|
||||
stream.ptr,
|
||||
)
|
||||
|
||||
self._collective(tensors, tensors, collective_fn)
|
||||
|
||||
def allgather(
|
||||
self,
|
||||
tensor_lists: list,
|
||||
tensors: list,
|
||||
allgather_options: AllGatherOptions = AllGatherOptions(),
|
||||
):
|
||||
"""Allgather tensors across gpus into a list of tensors.
|
||||
|
||||
Args:
|
||||
tensor_lists: allgathered tensors.
|
||||
tensors: the list of tensors to allgather across the group.
|
||||
Each tensor must lolcate on a GPU of the process.
|
||||
allgather_options: allgather options.
|
||||
"""
|
||||
|
||||
def collective_fn(input_tensor, output_tensor, comm, stream):
|
||||
comm.allGather(
|
||||
nccl_util.get_tensor_ptr(input_tensor),
|
||||
nccl_util.get_tensor_ptr(output_tensor),
|
||||
nccl_util.get_tensor_n_elements(input_tensor),
|
||||
nccl_util.get_nccl_tensor_dtype(input_tensor),
|
||||
stream.ptr,
|
||||
)
|
||||
|
||||
_check_inputs_compatibility_for_scatter_gather(tensors, tensor_lists)
|
||||
output_flattened = [
|
||||
_flatten_for_scatter_gather(tensor_list, copy=False)
|
||||
for tensor_list in tensor_lists
|
||||
]
|
||||
|
||||
def postprocess_fn(stream):
|
||||
# TODO(Hao): designate a copy stream.
|
||||
for i, tensor_list in enumerate(tensor_lists):
|
||||
for j, tensor in enumerate(tensor_list):
|
||||
nccl_util.copy_tensor(tensor, output_flattened[i][j])
|
||||
|
||||
self._collective(
|
||||
tensors, output_flattened, collective_fn, postprocess_fn=postprocess_fn
|
||||
)
|
||||
|
||||
def reducescatter(
|
||||
self,
|
||||
tensors: list,
|
||||
tensor_lists: list,
|
||||
reducescatter_options: ReduceScatterOptions = ReduceScatterOptions(),
|
||||
):
|
||||
"""Reduce then scatter a list of tensors across the group.
|
||||
|
||||
Args:
|
||||
tensors: the output tensors (could be unspecified), each
|
||||
located on a GPU of the current process.
|
||||
tensor_lists: the list of tensors to be reduced then
|
||||
scattered.
|
||||
reducescatter_options: reduce-scatter options.
|
||||
"""
|
||||
|
||||
def collective_fn(input_tensor, output_tensor, comm, stream):
|
||||
comm.reduceScatter(
|
||||
nccl_util.get_tensor_ptr(input_tensor),
|
||||
nccl_util.get_tensor_ptr(output_tensor),
|
||||
nccl_util.get_tensor_n_elements(output_tensor),
|
||||
nccl_util.get_nccl_tensor_dtype(output_tensor),
|
||||
nccl_util.get_nccl_reduce_op(reducescatter_options.reduceOp),
|
||||
stream.ptr,
|
||||
)
|
||||
|
||||
_check_inputs_compatibility_for_scatter_gather(tensors, tensor_lists)
|
||||
input_flattened = [
|
||||
_flatten_for_scatter_gather(tensor_list, copy=False)
|
||||
for tensor_list in tensor_lists
|
||||
]
|
||||
|
||||
def preprocess_fn(stream):
|
||||
for i, tensor_list in enumerate(tensor_lists):
|
||||
for j, tensor in enumerate(tensor_list):
|
||||
nccl_util.copy_tensor(input_flattened[i][j], tensor)
|
||||
|
||||
self._collective(
|
||||
input_flattened, tensors, collective_fn, preprocess_fn=preprocess_fn
|
||||
)
|
||||
|
||||
def send(self, tensors: list, send_options: SendOptions = SendOptions()):
|
||||
"""Send a tensor to a destination gpu in the group.
|
||||
|
||||
Args:
|
||||
tensors: the tensor to send.
|
||||
send_options: send options.
|
||||
"""
|
||||
|
||||
def p2p_fn(tensor, comm, stream, peer):
|
||||
comm.send(
|
||||
nccl_util.get_tensor_ptr(tensor),
|
||||
send_options.n_elements
|
||||
if send_options.n_elements > 0
|
||||
else nccl_util.get_tensor_n_elements(tensor),
|
||||
nccl_util.get_nccl_tensor_dtype(tensor),
|
||||
peer,
|
||||
stream.ptr,
|
||||
)
|
||||
|
||||
self._point2point(
|
||||
tensors, p2p_fn, send_options.dst_rank, send_options.dst_gpu_index
|
||||
)
|
||||
|
||||
def recv(self, tensors: list, recv_options: RecvOptions = RecvOptions()):
|
||||
"""Receive a tensor from a source gpu in the group.
|
||||
|
||||
Args:
|
||||
tensors: the received tensor.
|
||||
recv_options: Receive options.
|
||||
"""
|
||||
|
||||
def p2p_fn(tensor, comm, stream, peer):
|
||||
comm.recv(
|
||||
nccl_util.get_tensor_ptr(tensor),
|
||||
recv_options.n_elements
|
||||
if recv_options.n_elements > 0
|
||||
else nccl_util.get_tensor_n_elements(tensor),
|
||||
nccl_util.get_nccl_tensor_dtype(tensor),
|
||||
peer,
|
||||
stream.ptr,
|
||||
)
|
||||
|
||||
self._point2point(
|
||||
tensors, p2p_fn, recv_options.src_rank, recv_options.src_gpu_index
|
||||
)
|
||||
|
||||
def _get_nccl_collective_communicator(self, comm_key: str, device_list: list):
|
||||
"""Create or retrieve an NCCL communicator from cache.
|
||||
|
||||
If the communicator is found in cache, return the communicator. If not,
|
||||
a communicator and a stream will be created and put in cache.
|
||||
TODO(Hao): this function is not thread-safe now.
|
||||
|
||||
Args:
|
||||
comm_key: the key to query the communicator cache.
|
||||
device_list: a list of GPU devices of the current process
|
||||
that participates into the collective.
|
||||
|
||||
Returns:
|
||||
communicator: the NCCL communicator corresponded to the devices.
|
||||
"""
|
||||
if not comm_key:
|
||||
raise RuntimeError("Got empty communicator key.")
|
||||
for d in device_list:
|
||||
self._used_gpu_indices.add(d)
|
||||
|
||||
# TODO(Hao): lock the _dev_comm_map here.
|
||||
if comm_key in self._dev_comm_map:
|
||||
return self._dev_comm_map[comm_key]
|
||||
|
||||
group_key = self._generate_group_key(comm_key)
|
||||
if self.rank == 0:
|
||||
nccl_uid = self._generate_nccl_uid(group_key)
|
||||
else:
|
||||
rendezvous = Rendezvous(group_key)
|
||||
rendezvous.meet()
|
||||
nccl_uid = rendezvous.get_nccl_id()
|
||||
|
||||
# Now create the communicators
|
||||
actual_world_size = len(device_list) * self.world_size
|
||||
comms = [None] * len(device_list)
|
||||
streams = [None] * len(device_list)
|
||||
events = [None] * len(device_list)
|
||||
nccl_util.groupStart()
|
||||
for i, device in enumerate(device_list):
|
||||
actual_rank = self.rank * len(device_list) + i
|
||||
with nccl_util.Device(device):
|
||||
comms[i] = nccl_util.create_nccl_communicator(
|
||||
actual_world_size, nccl_uid, actual_rank
|
||||
)
|
||||
# request a stream from the pool
|
||||
# note the device_idx is absolute index.
|
||||
streams[i] = get_stream_pool(device).get_stream()
|
||||
# TODO(Fu): double check the parameters
|
||||
events[i] = cupy.cuda.Event()
|
||||
nccl_util.groupEnd()
|
||||
# TODO(Fu): lock
|
||||
self._dev_comm_map[comm_key] = comms
|
||||
self._dev_streams_map[comm_key] = streams
|
||||
self._dev_event_map[comm_key] = events
|
||||
return comms
|
||||
|
||||
@staticmethod
|
||||
def _sync_streams(device_list, events, streams):
|
||||
"""Let NCCL streams wait for current streams for every device."""
|
||||
# TODO(Fu): recordStream besides calling this function?
|
||||
if ENV.NCCL_USE_MULTISTREAM.val:
|
||||
for i, device in enumerate(device_list):
|
||||
with nccl_util.Device(device):
|
||||
events[i].record(cupy.cuda.get_current_stream())
|
||||
streams[i].wait_event(events[i])
|
||||
|
||||
def _get_nccl_p2p_communicator(
|
||||
self,
|
||||
comm_key: str,
|
||||
my_gpu_idx: int,
|
||||
peer_rank: int,
|
||||
peer_gpu_idx: int,
|
||||
):
|
||||
"""Create or retrieve an NCCL communicator for p2p tasks.
|
||||
|
||||
Note(Hao): this function is not thread-safe now.
|
||||
|
||||
Args:
|
||||
comm_key: communicator key.
|
||||
my_gpu_idx: the gpu index on the current process.
|
||||
peer_rank: the rank of the destination process.
|
||||
peer_gpu_idx: the gpu index on the peer process.
|
||||
Returns:
|
||||
communicator
|
||||
"""
|
||||
if not comm_key:
|
||||
raise RuntimeError("Got empty communicator key.")
|
||||
|
||||
# TODO(Hao): lock the _dev_comm_map here.
|
||||
if comm_key in self._dev_comm_map:
|
||||
return self._dev_comm_map[comm_key]
|
||||
|
||||
# Note (Hao): This is a bit complex so I decide to take a note here.
|
||||
# Here we need to consider three cases:
|
||||
# Case 1: src_rank != dst_rank, hence the send and recv happen on
|
||||
# different process (actors/tasks); each process makes independent
|
||||
# collective calls and manages corresponding communicators.
|
||||
# Case 2: src_rank == dst_rank, src_gpu_idx == dst_gpu_idx; for
|
||||
# this case, we simply throw a RuntimeError;
|
||||
# Case 3: src_rank == dst_rank, src_gpu_idx != dst_gpu_idx, which
|
||||
# means the send and recv will be called on the same process. We
|
||||
# DO NOT support this case for now. We need to properly scope:
|
||||
# (1) communicators creation, and
|
||||
# (2) send/recv calls
|
||||
# using groupStart(( and groupEnd() calls to avoid deadlocks.
|
||||
if self.rank < peer_rank:
|
||||
my_p2p_rank = 0
|
||||
elif self.rank > peer_rank:
|
||||
my_p2p_rank = 1
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Send and recv happens on the same process! "
|
||||
"ray.util.collective does not support this case as of now. "
|
||||
"Alternatively, consider doing GPU to GPU memcpy?"
|
||||
)
|
||||
|
||||
group_key = self._generate_group_key(comm_key)
|
||||
if my_p2p_rank == 0:
|
||||
nccl_uid = self._generate_nccl_uid(group_key)
|
||||
else:
|
||||
rendezvous = Rendezvous(group_key)
|
||||
rendezvous.meet()
|
||||
nccl_uid = rendezvous.get_nccl_id()
|
||||
|
||||
# create the p2p communicators
|
||||
with nccl_util.Device(my_gpu_idx):
|
||||
comm = nccl_util.create_nccl_communicator(2, nccl_uid, my_p2p_rank)
|
||||
stream = get_stream_pool(my_gpu_idx).get_stream()
|
||||
event = cupy.cuda.Event()
|
||||
|
||||
# TODO(Fu): lock and might need to add event
|
||||
self._dev_comm_map[comm_key] = [comm]
|
||||
self._dev_streams_map[comm_key] = [stream]
|
||||
self._dev_event_map[comm_key] = [event]
|
||||
return [comm]
|
||||
|
||||
def _generate_group_key(self, comm_key):
|
||||
"""Generate a unique key used to initialize the KV store.
|
||||
|
||||
The group key is a concatenation of the communicator key and
|
||||
the group name, following: [comm_key]@[group_name].
|
||||
"""
|
||||
return comm_key + "@" + self.group_name
|
||||
|
||||
@staticmethod
|
||||
def _destroy_store(group_key: str):
|
||||
"""Destroy the KV store (Ray named actor).
|
||||
|
||||
Args:
|
||||
group_key: the unique key to retrieve the KV store.
|
||||
"""
|
||||
store_name = get_store_name(group_key)
|
||||
store = ray.get_actor(store_name)
|
||||
# ray.get([store.__ray_terminate__.remote()])
|
||||
ray.kill(store)
|
||||
|
||||
def _generate_nccl_uid(self, key: str):
|
||||
"""Generate an NCCL unique ID for initializing communicators.
|
||||
|
||||
The method will also create a KV store using Ray named actor and store
|
||||
the NCCLUniqueID in the store. The store needs to be garbage collected
|
||||
when destroying the collective group.
|
||||
|
||||
Args:
|
||||
key: the key of the .
|
||||
|
||||
Returns:
|
||||
NCCLUniqueID (str): NCCL unique ID.
|
||||
"""
|
||||
group_uid = nccl_util.get_nccl_unique_id()
|
||||
store_name = get_store_name(key)
|
||||
# Avoid a potential circular dependency in ray/actor.py
|
||||
from ray.util.collective.util import NCCLUniqueIDStore
|
||||
|
||||
store = NCCLUniqueIDStore.options(name=store_name, lifetime="detached").remote(
|
||||
store_name
|
||||
)
|
||||
ray.get([store.set_id.remote(group_uid)])
|
||||
return group_uid
|
||||
|
||||
def _collective(
|
||||
self,
|
||||
input_tensors: list,
|
||||
output_tensors: list,
|
||||
collective_fn: Callable,
|
||||
preprocess_fn: Optional[Callable] = None,
|
||||
postprocess_fn: Optional[Callable] = None,
|
||||
):
|
||||
"""A method to encapsulate all collective calls.
|
||||
|
||||
Args:
|
||||
input_tensors: the list of the input tensors.
|
||||
output_tensors: the list of the output tensors.
|
||||
collective_fn: the collective function call.
|
||||
preprocess_fn: preprocess procedures before collective calls.
|
||||
postprocess_fn: postprocess procedures after collective calls.
|
||||
"""
|
||||
_check_gpu_tensors(input_tensors)
|
||||
_check_gpu_tensors(output_tensors)
|
||||
|
||||
devices = nccl_util.get_tensor_device_list(input_tensors)
|
||||
key = _get_comm_key_from_devices(devices)
|
||||
comms = self._get_nccl_collective_communicator(key, devices)
|
||||
streams = self._dev_streams_map[key]
|
||||
events = self._dev_event_map[key]
|
||||
|
||||
# TODO(Hao): sync streams and events
|
||||
self._sync_streams(devices, events, streams)
|
||||
|
||||
# Make the collective call
|
||||
if preprocess_fn:
|
||||
preprocess_fn(streams)
|
||||
|
||||
nccl_util.groupStart()
|
||||
# TODO(Fu): how to recordStreams as there are no library functions
|
||||
# We also need to make sure input tensors are not freed before their
|
||||
# usages on ncclStreams finish. This can be achieved by calling
|
||||
# c10::cuda::CUDACachingAllocator::recordStream, which remembers the
|
||||
# usage stream (ncclStream), creates an event on the usage stream
|
||||
# when GC attempts to free the input tensor, and delays GC until that
|
||||
# event is done.
|
||||
for i, tensor in enumerate(input_tensors):
|
||||
collective_fn(tensor, output_tensors[i], comms[i], streams[i])
|
||||
nccl_util.groupEnd()
|
||||
if postprocess_fn:
|
||||
postprocess_fn(streams)
|
||||
|
||||
def _point2point(
|
||||
self,
|
||||
tensors: list,
|
||||
p2p_fn: Callable,
|
||||
peer_rank: int,
|
||||
peer_gpu_idx: int,
|
||||
):
|
||||
"""A method to encapsulate all peer-to-peer calls (i.e., send/recv).
|
||||
|
||||
Args:
|
||||
tensors: the tensor to send or receive.
|
||||
p2p_fn: the p2p function call.
|
||||
peer_rank: the rank of the peer process.
|
||||
peer_gpu_idx: the index of the gpu on the peer process.
|
||||
"""
|
||||
# check send/recv availability.
|
||||
if nccl_util.get_nccl_runtime_version() < 2704:
|
||||
raise RuntimeError(
|
||||
"P2p send/recv requires NCCL >= 2.7.4. "
|
||||
"Got '{}'.".format(nccl_util.get_nccl_runtime_version())
|
||||
)
|
||||
_check_gpu_tensors(tensors)
|
||||
|
||||
# we currently only support single device to single device send/recv.
|
||||
assert len(tensors) == 1
|
||||
my_gpu_idx = nccl_util.get_tensor_device(tensors[0])
|
||||
comm_key = _get_comm_key_send_recv(
|
||||
self.rank, my_gpu_idx, peer_rank, peer_gpu_idx
|
||||
)
|
||||
comms = self._get_nccl_p2p_communicator(
|
||||
comm_key, my_gpu_idx, peer_rank, peer_gpu_idx
|
||||
)
|
||||
streams = self._dev_streams_map[comm_key]
|
||||
events = self._dev_event_map[comm_key]
|
||||
|
||||
# TODO(Hao): sync streams and events
|
||||
self._sync_streams([my_gpu_idx], events, streams)
|
||||
|
||||
# We have made sure that self.rank != peer_rank during API check.
|
||||
peer_p2p_rank = 0 if self.rank > peer_rank else 1
|
||||
for i, tensor in enumerate(tensors):
|
||||
p2p_fn(tensor, comms[i], streams[i], peer_p2p_rank)
|
||||
# Record the stream to avoid tensor being freed before the send/recv is completed.
|
||||
torch_stream = torch.cuda.ExternalStream(streams[i].ptr)
|
||||
tensor.record_stream(torch_stream)
|
||||
|
||||
|
||||
def _flatten_for_scatter_gather(tensor_list: list, copy: bool = False):
|
||||
"""Flatten the tensor for gather/scatter operations.
|
||||
|
||||
Args:
|
||||
tensor_list: the list of tensors to be scattered/gathered.
|
||||
copy: whether the copy the tensors in tensor_list into the buffer.
|
||||
|
||||
Returns:
|
||||
The flattened tensor buffer.
|
||||
"""
|
||||
if not tensor_list:
|
||||
raise RuntimeError("Received an empty list.")
|
||||
t = tensor_list[0]
|
||||
buffer_shape = [len(tensor_list)] + nccl_util.get_tensor_shape(t)
|
||||
|
||||
# TODO(wuxibin): cupy doesn't support bfloat16 for now,
|
||||
# once it is supported, we can eliminate this if statement.
|
||||
#
|
||||
# Allocate using the same backend as the tensors in `tensor_list`.
|
||||
# Use torch only when the tensors are torch.Tensor; otherwise fall back to CuPy.
|
||||
use_torch = False
|
||||
if torch_available():
|
||||
try:
|
||||
import torch
|
||||
|
||||
use_torch = isinstance(t, torch.Tensor)
|
||||
except ImportError:
|
||||
use_torch = False
|
||||
|
||||
if use_torch:
|
||||
buffer = torch.empty(tuple(buffer_shape), dtype=t.dtype, device=t.device)
|
||||
else:
|
||||
# note we need a cupy dtype here.
|
||||
dtype = nccl_util.get_cupy_tensor_dtype(t)
|
||||
device = nccl_util.get_tensor_device(t)
|
||||
with nccl_util.Device(device):
|
||||
buffer = cupy.empty(buffer_shape, dtype=dtype)
|
||||
|
||||
if copy:
|
||||
for i, tensor in enumerate(tensor_list):
|
||||
nccl_util.copy_tensor(buffer[i], tensor)
|
||||
return buffer
|
||||
|
||||
|
||||
def _check_inputs_compatibility_for_scatter_gather(tensors, tensor_lists):
|
||||
"""Check the compatibility between tensor input and tensor list input."""
|
||||
if not tensors or not isinstance(tensors, list):
|
||||
raise RuntimeError("The first argument 'tensors' expects a list of tensors.")
|
||||
if not tensor_lists or not isinstance(tensor_lists, list):
|
||||
raise RuntimeError(
|
||||
"The second argument 'tensor_lists' expects a list of tensor list."
|
||||
)
|
||||
dtype = nccl_util.get_nccl_tensor_dtype(tensors[0])
|
||||
shape = nccl_util.get_tensor_shape(tensors[0])
|
||||
for i, tensor_list in enumerate(tensor_lists):
|
||||
# check all tensor in `tensors` match.
|
||||
dt = nccl_util.get_nccl_tensor_dtype(tensors[i])
|
||||
if dt != dtype:
|
||||
raise RuntimeError(
|
||||
"All tensor operands to scatter/gather must "
|
||||
"have the same dtype. Got '{}' and '{}'.".format(dt, dtype)
|
||||
)
|
||||
# Note: typically CCL libraries only requires they have the same
|
||||
# number of elements; Here we make it more strict -- we require
|
||||
# exact shape match.
|
||||
s = nccl_util.get_tensor_shape(tensors[i])
|
||||
if s != shape:
|
||||
raise RuntimeError(
|
||||
"All tensor operands to scatter/gather must "
|
||||
"have the same shape. Got '{}' and '{}'.".format(s, shape)
|
||||
)
|
||||
# check all tensors in `tensor_lists` match.
|
||||
for t in tensor_lists[i]:
|
||||
# check dtype
|
||||
dt = nccl_util.get_nccl_tensor_dtype(t)
|
||||
if dt != dtype:
|
||||
raise RuntimeError(
|
||||
"All tensor operands to scatter/gather must "
|
||||
"have the same dtype. Got '{}' and '{}'.".format(dt, dtype)
|
||||
)
|
||||
s = nccl_util.get_tensor_shape(t)
|
||||
if s != shape:
|
||||
raise RuntimeError(
|
||||
"All tensor operands to scatter/gather must "
|
||||
"have the same shape. Got '{}' and '{}'.".format(s, shape)
|
||||
)
|
||||
|
||||
|
||||
def _check_gpu_tensors(tensors):
|
||||
"""Check all tensors are distributed on different GPUs."""
|
||||
if not tensors or not isinstance(tensors, list):
|
||||
raise RuntimeError("'tensors' must be a nonempty list.")
|
||||
if len(tensors) > nccl_util.get_num_gpus():
|
||||
raise RuntimeError(
|
||||
"Tensor list cannot be larger than the number"
|
||||
"of available GPUs. Got {} > {}.".format(
|
||||
len(tensors), nccl_util.get_num_gpus()
|
||||
)
|
||||
)
|
||||
t0 = tensors[0]
|
||||
dt = nccl_util.get_nccl_tensor_dtype(t0)
|
||||
s = nccl_util.get_tensor_shape(t0)
|
||||
d = nccl_util.get_tensor_device(t0)
|
||||
for i, t in enumerate(tensors):
|
||||
if i == 0:
|
||||
continue
|
||||
# We need to check the following:
|
||||
# (1) tensor is cuda (already checked during API)
|
||||
# (2) tensor dtype
|
||||
# (3) tensor shape match
|
||||
# (4) each tensor is on a different GPU
|
||||
dtype = nccl_util.get_nccl_tensor_dtype(t)
|
||||
if dt != dtype:
|
||||
raise RuntimeError(
|
||||
"Tensors must have identical dtype. Got: '{}'.".format(dtype)
|
||||
)
|
||||
shape = nccl_util.get_tensor_shape(t)
|
||||
if s != shape:
|
||||
raise RuntimeError(
|
||||
"Tensor must have identical shape. Got: '{}'.".format(shape)
|
||||
)
|
||||
device = nccl_util.get_tensor_device(t)
|
||||
if device == d:
|
||||
raise RuntimeError("Tensor must be on distinct GPUs.")
|
||||
|
||||
|
||||
def _get_comm_key_from_devices(devices: List[int]):
|
||||
"""Return a key from a list of devices for collective calls.
|
||||
|
||||
For example, if the tensors are on gpus 0, 1, 2, 3,
|
||||
then the key would be "0,1,2,3".
|
||||
|
||||
Args:
|
||||
devices: a list of GPU device indices
|
||||
|
||||
Returns:
|
||||
str: a string represents the key to query the communicator cache.
|
||||
|
||||
"""
|
||||
return ",".join([str(d) for d in devices])
|
||||
|
||||
|
||||
def _get_comm_key_send_recv(
|
||||
my_rank: int, my_gpu_idx: int, peer_rank: int, peer_gpu_idx: int
|
||||
):
|
||||
"""Return a key given source and destination ranks for p2p tasks.
|
||||
|
||||
The p2p key is in the following form:
|
||||
[min_rank]_[gpu_index]:[max_rank]_[gpu_index].
|
||||
|
||||
Args:
|
||||
my_rank: the rank of the source process.
|
||||
my_gpu_idx: the source gpu index on the process.
|
||||
peer_rank: the rank of the destination process.
|
||||
peer_gpu_idx: the destination gpu index on the process.
|
||||
|
||||
Returns:
|
||||
comm_key: a string key to query the communication cache.
|
||||
"""
|
||||
if my_rank < peer_rank:
|
||||
lower_key = str(my_rank) + "_" + str(my_gpu_idx)
|
||||
higher_key = str(peer_rank) + "_" + str(peer_gpu_idx)
|
||||
elif my_rank > peer_rank:
|
||||
lower_key = str(peer_rank) + "_" + str(peer_gpu_idx)
|
||||
higher_key = str(my_rank) + "_" + str(my_gpu_idx)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"Send and recv happens on the same process. ray.util.collective "
|
||||
"does not support this case as of now. Alternatively, consider "
|
||||
"doing GPU to GPU memcpy?"
|
||||
)
|
||||
comm_key = lower_key + ":" + higher_key
|
||||
return comm_key
|
||||
@@ -0,0 +1,298 @@
|
||||
"""Code to wrap some NCCL API calls."""
|
||||
from typing import Any, List
|
||||
|
||||
import numpy
|
||||
|
||||
try:
|
||||
import cupy
|
||||
from cupy.cuda import (
|
||||
Device, # noqa: F401
|
||||
nccl,
|
||||
)
|
||||
from cupy.cuda.nccl import (
|
||||
NcclCommunicator,
|
||||
get_build_version,
|
||||
get_version,
|
||||
groupEnd, # noqa: F401
|
||||
groupStart, # noqa: F401
|
||||
)
|
||||
except ImportError:
|
||||
raise ImportError("NCCL in Ray requires Cupy being available!")
|
||||
|
||||
from ray.util.collective.types import ReduceOp, torch_available
|
||||
|
||||
NCCL_REDUCE_OP_MAP = {
|
||||
ReduceOp.SUM: nccl.NCCL_SUM,
|
||||
ReduceOp.PRODUCT: nccl.NCCL_PROD,
|
||||
ReduceOp.MIN: nccl.NCCL_MIN,
|
||||
ReduceOp.MAX: nccl.NCCL_MAX,
|
||||
}
|
||||
|
||||
# cupy types are the same with numpy types
|
||||
NUMPY_NCCL_DTYPE_MAP = {
|
||||
# INT types
|
||||
numpy.int_: nccl.NCCL_INT64,
|
||||
numpy.uint8: nccl.NCCL_UINT8,
|
||||
numpy.uint32: nccl.NCCL_UINT32,
|
||||
numpy.uint64: nccl.NCCL_UINT64,
|
||||
numpy.int8: nccl.NCCL_INT8,
|
||||
numpy.int32: nccl.NCCL_INT32,
|
||||
numpy.int64: nccl.NCCL_INT64,
|
||||
# FLOAT types
|
||||
numpy.half: nccl.NCCL_HALF,
|
||||
numpy.float16: nccl.NCCL_FLOAT16,
|
||||
numpy.float32: nccl.NCCL_FLOAT32,
|
||||
numpy.float64: nccl.NCCL_FLOAT64,
|
||||
numpy.double: nccl.NCCL_DOUBLE,
|
||||
}
|
||||
|
||||
if torch_available():
|
||||
import torch
|
||||
import torch.utils.dlpack
|
||||
|
||||
TORCH_NCCL_DTYPE_MAP = {
|
||||
torch.bool: nccl.NCCL_INT8,
|
||||
# INT types
|
||||
torch.int: nccl.NCCL_INT,
|
||||
torch.uint8: nccl.NCCL_UINT8,
|
||||
torch.int8: nccl.NCCL_INT8,
|
||||
torch.int32: nccl.NCCL_INT32,
|
||||
torch.int64: nccl.NCCL_INT64,
|
||||
torch.long: nccl.NCCL_INT64,
|
||||
# FLOAT types
|
||||
torch.half: nccl.NCCL_HALF,
|
||||
torch.float: nccl.NCCL_FLOAT,
|
||||
torch.float16: nccl.NCCL_FLOAT16,
|
||||
torch.float32: nccl.NCCL_FLOAT32,
|
||||
torch.float64: nccl.NCCL_FLOAT64,
|
||||
torch.double: nccl.NCCL_DOUBLE,
|
||||
}
|
||||
|
||||
# Older versions of cupy don't support bfloat16.
|
||||
if hasattr(nccl, "NCCL_BFLOAT16"):
|
||||
TORCH_NCCL_DTYPE_MAP[torch.bfloat16] = nccl.NCCL_BFLOAT16
|
||||
|
||||
TORCH_NUMPY_DTYPE_MAP = {
|
||||
# INT types
|
||||
torch.int: numpy.int32,
|
||||
torch.uint8: numpy.uint8,
|
||||
torch.int8: numpy.int8,
|
||||
torch.int32: numpy.int32,
|
||||
torch.int64: numpy.int64,
|
||||
torch.long: numpy.int64,
|
||||
# FLOAT types
|
||||
torch.half: numpy.half,
|
||||
torch.float: numpy.float32,
|
||||
torch.float16: numpy.float16,
|
||||
torch.float32: numpy.float32,
|
||||
torch.float64: numpy.float64,
|
||||
}
|
||||
|
||||
|
||||
def get_num_gpus():
|
||||
"""Returns the number of compute-capable GPUs."""
|
||||
return cupy.cuda.runtime.getDeviceCount()
|
||||
|
||||
|
||||
def get_nccl_build_version():
|
||||
return get_build_version()
|
||||
|
||||
|
||||
def get_nccl_runtime_version():
|
||||
return get_version()
|
||||
|
||||
|
||||
def get_nccl_unique_id():
|
||||
return nccl.get_unique_id()
|
||||
|
||||
|
||||
def create_nccl_communicator(world_size: int, nccl_unique_id: bytes, rank: int):
|
||||
"""Create an NCCL communicator using NCCL APIs.
|
||||
|
||||
Args:
|
||||
world_size: the number of processes of this communicator group.
|
||||
nccl_unique_id: the NCCLUniqueID for this group.
|
||||
rank: the rank of this process.
|
||||
|
||||
Returns:
|
||||
comm: an NCCL communicator.
|
||||
"""
|
||||
comm = NcclCommunicator(world_size, nccl_unique_id, rank)
|
||||
return comm
|
||||
|
||||
|
||||
def get_nccl_reduce_op(reduce_op: ReduceOp):
|
||||
"""Map the reduce op to NCCL reduce op type.
|
||||
|
||||
Args:
|
||||
reduce_op: ReduceOp Enum (SUM/PRODUCT/MIN/MAX).
|
||||
|
||||
Returns:
|
||||
the mapped NCCL reduce op.
|
||||
"""
|
||||
if reduce_op not in NCCL_REDUCE_OP_MAP:
|
||||
raise RuntimeError("NCCL does not support reduce op: '{}'.".format(reduce_op))
|
||||
return NCCL_REDUCE_OP_MAP[reduce_op]
|
||||
|
||||
|
||||
def get_nccl_tensor_dtype(tensor):
|
||||
"""Return the corresponded NCCL dtype given a tensor."""
|
||||
if isinstance(tensor, cupy.ndarray):
|
||||
return NUMPY_NCCL_DTYPE_MAP[tensor.dtype.type]
|
||||
if torch_available():
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
return TORCH_NCCL_DTYPE_MAP[tensor.dtype]
|
||||
raise ValueError(
|
||||
"Unsupported tensor type. Got: {}. Supported "
|
||||
"GPU tensor types are: torch.Tensor, "
|
||||
"cupy.ndarray.".format(type(tensor))
|
||||
)
|
||||
|
||||
|
||||
def get_cupy_tensor_dtype(tensor):
|
||||
"""Return the corresponded Cupy dtype given a tensor."""
|
||||
if isinstance(tensor, cupy.ndarray):
|
||||
return tensor.dtype.type
|
||||
if torch_available():
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
return TORCH_NUMPY_DTYPE_MAP[tensor.dtype]
|
||||
raise ValueError(
|
||||
"Unsupported tensor type. Got: {}. Supported "
|
||||
"GPU tensor types are: torch.Tensor, "
|
||||
"cupy.ndarray.".format(type(tensor))
|
||||
)
|
||||
|
||||
|
||||
def get_tensor_ptr(tensor):
|
||||
"""Return the pointer to the underlying memory storage of a tensor."""
|
||||
if isinstance(tensor, cupy.ndarray):
|
||||
return tensor.data.ptr
|
||||
if isinstance(tensor, numpy.ndarray):
|
||||
return tensor.data
|
||||
if torch_available():
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
if not tensor.is_cuda:
|
||||
raise RuntimeError(
|
||||
"Torch tensor must be on GPU when using NCCL collectives."
|
||||
)
|
||||
return tensor.data_ptr()
|
||||
raise ValueError(
|
||||
"Unsupported tensor type. Got: {}. Supported "
|
||||
"GPU tensor types are: torch.Tensor, "
|
||||
"cupy.ndarray.".format(type(tensor))
|
||||
)
|
||||
|
||||
|
||||
def get_tensor_n_elements(tensor):
|
||||
"""Return the number of elements in a tensor."""
|
||||
if isinstance(tensor, cupy.ndarray) or isinstance(tensor, numpy.ndarray):
|
||||
return tensor.size
|
||||
if torch_available():
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
return torch.numel(tensor)
|
||||
raise ValueError(
|
||||
"Unsupported tensor type. Got: {}. Supported "
|
||||
"GPU tensor types are: torch.Tensor, "
|
||||
"cupy.ndarray.".format(type(tensor))
|
||||
)
|
||||
|
||||
|
||||
def get_tensor_shape(tensor):
|
||||
"""Return the shape of the tensor as a list."""
|
||||
if isinstance(tensor, cupy.ndarray):
|
||||
return list(tensor.shape)
|
||||
if torch_available():
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
return list(tensor.size())
|
||||
raise ValueError(
|
||||
"Unsupported tensor type. Got: {}. Supported "
|
||||
"GPU tensor types are: torch.Tensor, "
|
||||
"cupy.ndarray.".format(type(tensor))
|
||||
)
|
||||
|
||||
|
||||
def get_tensor_strides(tensor):
|
||||
"""Return the strides of the tensor as a list."""
|
||||
if isinstance(tensor, cupy.ndarray):
|
||||
return [int(stride / tensor.dtype.itemsize) for stride in tensor.strides]
|
||||
if torch_available():
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
return list(tensor.stride())
|
||||
raise ValueError(
|
||||
"Unsupported tensor type. Got: {}. Supported "
|
||||
"GPU tensor types are: torch.Tensor, "
|
||||
"cupy.ndarray.".format(type(tensor))
|
||||
)
|
||||
|
||||
|
||||
def get_tensor_device(tensor):
|
||||
"""Return the GPU index of a tensor."""
|
||||
if isinstance(tensor, cupy.ndarray):
|
||||
try:
|
||||
device = tensor.device.id
|
||||
except AttributeError as exec:
|
||||
raise RuntimeError("The tensor is not on a valid GPU.") from exec
|
||||
elif torch_available() and isinstance(tensor, torch.Tensor):
|
||||
device = tensor.device.index
|
||||
if not isinstance(device, int):
|
||||
raise RuntimeError("The tensor is not on a valid GPU.")
|
||||
else:
|
||||
raise ValueError("Unsupported tensor type. Got: {}.".format(type(tensor)))
|
||||
return device
|
||||
|
||||
|
||||
def copy_tensor(dst_tensor: Any, src_tensor: Any):
|
||||
"""Copy the content from src_tensor to dst_tensor.
|
||||
|
||||
Args:
|
||||
dst_tensor: the tensor to copy from.
|
||||
src_tensor: the tensor to copy to.
|
||||
"""
|
||||
copied = True
|
||||
if isinstance(dst_tensor, cupy.ndarray) and isinstance(src_tensor, cupy.ndarray):
|
||||
cupy.copyto(dst_tensor, src_tensor)
|
||||
elif torch_available():
|
||||
if isinstance(dst_tensor, torch.Tensor) and isinstance(
|
||||
src_tensor, torch.Tensor
|
||||
):
|
||||
dst_tensor.copy_(src_tensor)
|
||||
elif isinstance(dst_tensor, torch.Tensor) and isinstance(
|
||||
src_tensor, cupy.ndarray
|
||||
):
|
||||
t = torch.utils.dlpack.from_dlpack(src_tensor.toDlpack())
|
||||
dst_tensor.copy_(t)
|
||||
elif isinstance(dst_tensor, cupy.ndarray) and isinstance(
|
||||
src_tensor, torch.Tensor
|
||||
):
|
||||
t = cupy.fromDlpack(torch.utils.dlpack.to_dlpack(src_tensor))
|
||||
cupy.copyto(dst_tensor, t)
|
||||
else:
|
||||
copied = False
|
||||
else:
|
||||
copied = False
|
||||
if not copied:
|
||||
raise ValueError(
|
||||
"Unsupported tensor type. Got: {} and {}. Supported "
|
||||
"GPU tensor types are: torch.Tensor, cupy.ndarray.".format(
|
||||
type(dst_tensor), type(src_tensor)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_tensor_device_list(tensors: List[Any]):
|
||||
"""Returns the gpu devices of the list of input tensors.
|
||||
|
||||
Args:
|
||||
tensors: a list of tensors, each locates on a GPU.
|
||||
|
||||
Returns:
|
||||
list: the list of GPU devices.
|
||||
|
||||
"""
|
||||
if not isinstance(tensors, list):
|
||||
raise RuntimeError(
|
||||
"Expect a list of tensors each locates on a GPU device. "
|
||||
"Got: '{}'.".format(type(tensors))
|
||||
)
|
||||
devices = [get_tensor_device(t) for t in tensors]
|
||||
return devices
|
||||
@@ -0,0 +1,290 @@
|
||||
import logging
|
||||
import os
|
||||
import socket
|
||||
import time
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
import ray
|
||||
import ray.experimental.internal_kv as internal_kv
|
||||
from ray._common.network_utils import find_free_port, is_ipv6
|
||||
from ray.util.collective.collective_group.base_collective_group import BaseGroup
|
||||
from ray.util.collective.types import (
|
||||
AllGatherOptions,
|
||||
AllReduceOptions,
|
||||
Backend,
|
||||
BarrierOptions,
|
||||
BroadcastOptions,
|
||||
RecvOptions,
|
||||
ReduceOp,
|
||||
ReduceOptions,
|
||||
ReduceScatterOptions,
|
||||
SendOptions,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import torch
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
import torch.distributed as dist
|
||||
|
||||
_TORCH_DISTRIBUTED_AVAILABLE = True
|
||||
TORCH_REDUCE_OP_MAP = {
|
||||
ReduceOp.SUM: dist.ReduceOp.SUM,
|
||||
ReduceOp.PRODUCT: dist.ReduceOp.PRODUCT,
|
||||
ReduceOp.MIN: dist.ReduceOp.MIN,
|
||||
ReduceOp.MAX: dist.ReduceOp.MAX,
|
||||
}
|
||||
except ImportError:
|
||||
_TORCH_DISTRIBUTED_AVAILABLE = False
|
||||
TORCH_REDUCE_OP_MAP = None
|
||||
|
||||
|
||||
def get_master_address_metadata_key(group_name: str):
|
||||
return f"collective_group_master_address_{group_name}"
|
||||
|
||||
|
||||
def get_address_and_port() -> Tuple[str, int]:
|
||||
"""Returns the IP address and a free port on this node."""
|
||||
addr = ray.util.get_node_ip_address()
|
||||
port = find_free_port(socket.AF_INET6 if is_ipv6(addr) else socket.AF_INET)
|
||||
return addr, port
|
||||
|
||||
|
||||
class TorchGLOOGroup(BaseGroup):
|
||||
def __init__(
|
||||
self,
|
||||
world_size: int,
|
||||
rank: int,
|
||||
group_name: str,
|
||||
gloo_timeout: Optional[int] = None,
|
||||
):
|
||||
# Initialize the default process group only once per process.
|
||||
if not dist.is_initialized():
|
||||
# Rendezvous: ensure a MASTER_ADDR:MASTER_PORT is published in internal_kv.
|
||||
self._rendezvous(group_name, rank, gloo_timeout)
|
||||
|
||||
metadata_key = get_master_address_metadata_key(group_name)
|
||||
try:
|
||||
metadata = internal_kv._internal_kv_get(metadata_key)
|
||||
except ValueError:
|
||||
raise RuntimeError(
|
||||
f"TorchGLOOGroup expected metadata in internal_kv with name `{metadata_key}`. "
|
||||
"TorchGLOOGroup should not be instantiated directly. "
|
||||
"Use ray.experimental.collective.create_collective_group to create a group."
|
||||
)
|
||||
if metadata is None:
|
||||
raise RuntimeError(
|
||||
f"Missing rendezvous metadata for group `{group_name}` under key `{metadata_key}`."
|
||||
)
|
||||
metadata = metadata.decode()
|
||||
master_addr, master_port = metadata.split(":")
|
||||
os.environ["MASTER_ADDR"] = master_addr
|
||||
os.environ["MASTER_PORT"] = master_port
|
||||
|
||||
dist.init_process_group(
|
||||
backend="gloo", init_method="env://", world_size=world_size, rank=rank
|
||||
)
|
||||
|
||||
# Clean up rendezvous metadata after all ranks have initialized.
|
||||
# dist.init_process_group is synchronous, so all ranks have read the metadata by now.
|
||||
if rank == 0:
|
||||
try:
|
||||
internal_kv._internal_kv_del(metadata_key)
|
||||
except Exception as e:
|
||||
# Ignore errors during cleanup (e.g., key already deleted)
|
||||
logger.warning(
|
||||
f"Failed to delete rendezvous key '{metadata_key}' during init: {e}"
|
||||
)
|
||||
|
||||
super().__init__(world_size, rank, group_name)
|
||||
|
||||
# Create a subgroup for this logical group. For the default group, use WORLD.
|
||||
self._is_default_group = group_name == "default"
|
||||
if self._is_default_group:
|
||||
self._pg = dist.group.WORLD
|
||||
else:
|
||||
# All ranks participate in this subgroup with global ranks [0..world_size-1].
|
||||
ranks = list(range(world_size))
|
||||
self._pg = dist.new_group(ranks=ranks, backend="gloo")
|
||||
|
||||
# Compatibility shim for legacy tests expecting a pygloo context with getTimeout().
|
||||
# Store the rendezvous timeout in milliseconds, defaulting to 30000 if unspecified.
|
||||
class _GlooCompatContext:
|
||||
def __init__(self, timeout_ms: int):
|
||||
self._timeout_ms = timeout_ms
|
||||
|
||||
def getTimeout(self) -> int:
|
||||
return self._timeout_ms
|
||||
|
||||
self._gloo_context = _GlooCompatContext(
|
||||
gloo_timeout if gloo_timeout is not None else 30000
|
||||
)
|
||||
|
||||
def _rendezvous(
|
||||
self, group_name: str, rank: int, gloo_timeout: Optional[int]
|
||||
) -> None:
|
||||
"""Rendezvous: ensure a MASTER_ADDR:MASTER_PORT is published in internal_kv.
|
||||
|
||||
Rank 0 publishes the address and port, other ranks wait for it.
|
||||
"""
|
||||
metadata_key = get_master_address_metadata_key(group_name)
|
||||
if rank == 0:
|
||||
addr, port = get_address_and_port()
|
||||
internal_kv._internal_kv_put(metadata_key, f"{addr}:{port}")
|
||||
else:
|
||||
# Wait until rank 0 publishes the metadata or timeout.
|
||||
deadline_s = time.time() + (gloo_timeout / 1000.0 if gloo_timeout else 30.0)
|
||||
while True:
|
||||
meta = internal_kv._internal_kv_get(metadata_key)
|
||||
if meta is not None:
|
||||
break
|
||||
if time.time() > deadline_s:
|
||||
raise TimeoutError(
|
||||
f"Timed out waiting for GLOO rendezvous metadata for group '{group_name}'."
|
||||
)
|
||||
time.sleep(0.05)
|
||||
|
||||
def destroy_group(self):
|
||||
"""GC the communicators."""
|
||||
# Destroy only the subgroup for non-default groups. Allow default to be torn down explicitly.
|
||||
if self._is_default_group:
|
||||
# Destroy default process group to allow re-init in tests that recreate the same group.
|
||||
dist.destroy_process_group()
|
||||
else:
|
||||
# Destroy just this subgroup.
|
||||
if self._pg is not None:
|
||||
dist.destroy_process_group(self._pg)
|
||||
|
||||
@classmethod
|
||||
def backend(cls):
|
||||
"""The backend of this collective group."""
|
||||
return Backend.GLOO
|
||||
|
||||
@classmethod
|
||||
def check_backend_availability(cls) -> bool:
|
||||
return _TORCH_DISTRIBUTED_AVAILABLE
|
||||
|
||||
def _check_tensor_input(self, tensor: List["torch.Tensor"]) -> "torch.Tensor":
|
||||
"""ray.util.collective wraps tensor arguments in a list.
|
||||
Accept a single torch.Tensor or numpy.ndarray and unwrap/convert it.
|
||||
"""
|
||||
assert isinstance(tensor, list) and len(tensor) == 1
|
||||
t = tensor[0]
|
||||
if isinstance(t, torch.Tensor):
|
||||
return t
|
||||
if isinstance(t, np.ndarray):
|
||||
return torch.from_numpy(t)
|
||||
raise ValueError(
|
||||
f"torch_gloo group only accepts torch.Tensor or numpy.ndarray, received {type(t)}"
|
||||
)
|
||||
|
||||
def _check_tensor_list_input(
|
||||
self, tensor_list: List[List["torch.Tensor"]]
|
||||
) -> List["torch.Tensor"]:
|
||||
"""ray.util.collective wraps tensor arguments in a list.
|
||||
Accept a single list containing torch.Tensors or numpy.ndarrays and
|
||||
unwrap/convert items as needed.
|
||||
"""
|
||||
assert isinstance(tensor_list, list) and len(tensor_list) == 1
|
||||
tensor_list = tensor_list[0]
|
||||
converted_tensor_list = []
|
||||
for tensor in tensor_list:
|
||||
if isinstance(tensor, np.ndarray):
|
||||
tensor = torch.from_numpy(tensor)
|
||||
converted_tensor_list.append(tensor)
|
||||
elif isinstance(tensor, torch.Tensor):
|
||||
converted_tensor_list.append(tensor)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"torch_gloo group only accepts torch.Tensor or numpy.ndarray types, received tensor list with value {tensor}"
|
||||
)
|
||||
return converted_tensor_list
|
||||
|
||||
def allreduce(
|
||||
self,
|
||||
tensor: List["torch.Tensor"],
|
||||
allreduce_options: Optional[AllReduceOptions] = None,
|
||||
) -> None:
|
||||
if allreduce_options is None:
|
||||
allreduce_options = AllReduceOptions()
|
||||
tensor = self._check_tensor_input(tensor)
|
||||
torch_reduce_op = TORCH_REDUCE_OP_MAP[allreduce_options.reduceOp]
|
||||
dist.all_reduce(tensor, op=torch_reduce_op, group=self._pg)
|
||||
|
||||
def barrier(self, barrier_options=BarrierOptions()) -> None:
|
||||
dist.barrier(group=self._pg)
|
||||
|
||||
def reduce(
|
||||
self,
|
||||
tensor: List["torch.Tensor"],
|
||||
reduce_options: Optional[ReduceOptions] = None,
|
||||
) -> None:
|
||||
if reduce_options is None:
|
||||
reduce_options = ReduceOptions()
|
||||
t = self._check_tensor_input(tensor)
|
||||
torch_reduce_op = TORCH_REDUCE_OP_MAP[reduce_options.reduceOp]
|
||||
# Avoid mutating non-root ranks' user tensors to match util.collective semantics.
|
||||
if self._rank == reduce_options.root_rank:
|
||||
dist.reduce(
|
||||
t, dst=reduce_options.root_rank, op=torch_reduce_op, group=self._pg
|
||||
)
|
||||
else:
|
||||
tmp = t.detach().clone()
|
||||
dist.reduce(
|
||||
tmp, dst=reduce_options.root_rank, op=torch_reduce_op, group=self._pg
|
||||
)
|
||||
|
||||
def allgather(
|
||||
self,
|
||||
tensor_list: List[List["torch.Tensor"]],
|
||||
tensor: List["torch.Tensor"],
|
||||
allgather_options: Optional[AllGatherOptions] = None,
|
||||
) -> None:
|
||||
if allgather_options is None:
|
||||
allgather_options = AllGatherOptions()
|
||||
tensor_list = self._check_tensor_list_input(tensor_list)
|
||||
tensor = self._check_tensor_input(tensor)
|
||||
dist.all_gather(tensor_list, tensor, group=self._pg)
|
||||
|
||||
def broadcast(
|
||||
self, tensor: List["torch.Tensor"], broadcast_options=BroadcastOptions()
|
||||
) -> None:
|
||||
tensor = self._check_tensor_input(tensor)
|
||||
dist.broadcast(tensor, src=broadcast_options.root_rank, group=self._pg)
|
||||
|
||||
def reducescatter(
|
||||
self,
|
||||
output_tensor: List["torch.Tensor"],
|
||||
tensor_list: List[List["torch.Tensor"]],
|
||||
reducescatter_options: Optional[ReduceScatterOptions] = None,
|
||||
) -> None:
|
||||
if reducescatter_options is None:
|
||||
reducescatter_options = ReduceScatterOptions()
|
||||
tensor_list = self._check_tensor_list_input(tensor_list)
|
||||
output_tensor = self._check_tensor_input(output_tensor)
|
||||
if output_tensor.shape != tensor_list[self._rank].shape:
|
||||
raise ValueError(
|
||||
"Output tensor has wrong shape {output_tensor.shape}, expected {tensor_list[self._rank].shape}"
|
||||
)
|
||||
torch_reduce_op = TORCH_REDUCE_OP_MAP[reducescatter_options.reduceOp]
|
||||
|
||||
# torch.distributed gloo doesn't support reducescatter. Implement a
|
||||
# simple version using allreduce.
|
||||
for tensor in tensor_list:
|
||||
dist.all_reduce(tensor, op=torch_reduce_op, group=self._pg)
|
||||
|
||||
if output_tensor.data_ptr() != tensor_list[self._rank].data_ptr():
|
||||
output_tensor.copy_(tensor_list[self._rank])
|
||||
|
||||
def send(self, tensor: List["torch.Tensor"], send_options: SendOptions) -> None:
|
||||
tensor = self._check_tensor_input(tensor)
|
||||
dist.send(tensor, dst=send_options.dst_rank)
|
||||
|
||||
def recv(self, tensor: List["torch.Tensor"], recv_options: RecvOptions) -> None:
|
||||
tensor = self._check_tensor_input(tensor)
|
||||
dist.recv(tensor, src=recv_options.src_rank)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Constants.
|
||||
|
||||
Contains constants used to setup collective groups.
|
||||
"""
|
||||
import hashlib
|
||||
import os
|
||||
from enum import Enum, auto
|
||||
|
||||
|
||||
def get_store_name(group_name: str):
|
||||
"""Generate the unique name for the NCCLUniqueID store (named actor).
|
||||
|
||||
Args:
|
||||
group_name: unique user name for the store.
|
||||
|
||||
Returns:
|
||||
SHA256-hexlified name for the store.
|
||||
"""
|
||||
if not group_name:
|
||||
raise ValueError("group_name is None.")
|
||||
hexlified_name = hashlib.sha256(group_name.encode()).hexdigest()
|
||||
return hexlified_name
|
||||
|
||||
|
||||
class ENV(Enum):
|
||||
"""ray.util.collective environment variables."""
|
||||
|
||||
NCCL_USE_MULTISTREAM = auto(), lambda v: (v or "True") == "True"
|
||||
|
||||
@property
|
||||
def val(self):
|
||||
"""Return the output of the lambda against the system's env value."""
|
||||
_, default_fn = self.value
|
||||
return default_fn(os.getenv(self.name))
|
||||
@@ -0,0 +1,529 @@
|
||||
import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import numpy as np
|
||||
|
||||
import ray
|
||||
import ray.experimental.internal_kv as internal_kv
|
||||
from ray.util.collective import (
|
||||
allreduce,
|
||||
broadcast,
|
||||
create_collective_group,
|
||||
init_collective_group,
|
||||
)
|
||||
from ray.util.collective.backend_registry import (
|
||||
_global_registry,
|
||||
register_collective_backend,
|
||||
)
|
||||
from ray.util.collective.collective_group.base_collective_group import BaseGroup
|
||||
from ray.util.collective.types import (
|
||||
AllGatherOptions,
|
||||
AllReduceOptions,
|
||||
BarrierOptions,
|
||||
BroadcastOptions,
|
||||
RecvOptions,
|
||||
ReduceOp,
|
||||
ReduceOptions,
|
||||
ReduceScatterOptions,
|
||||
SendOptions,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
|
||||
def _unregister_collective_backend(name: str) -> None:
|
||||
"""Helper function to unregister a backend for testing purposes."""
|
||||
upper_name = name.upper()
|
||||
if upper_name in _global_registry._map:
|
||||
del _global_registry._map[upper_name]
|
||||
|
||||
|
||||
def get_data_key(group_name: str, rank: int, op_name: str):
|
||||
return f"collective_mock_{group_name}_{op_name}_rank_{rank}"
|
||||
|
||||
|
||||
def get_barrier_key(group_name: str, barrier_id: int):
|
||||
return f"collective_mock_{group_name}_barrier_{barrier_id}"
|
||||
|
||||
|
||||
class MockInternalKVGroup(BaseGroup):
|
||||
def __init__(self, world_size: int, rank: int, group_name: str):
|
||||
super().__init__(world_size, rank, group_name)
|
||||
self._barrier_counter = 0
|
||||
|
||||
@classmethod
|
||||
def backend(cls):
|
||||
return "MOCK"
|
||||
|
||||
@classmethod
|
||||
def check_backend_availability(cls) -> bool:
|
||||
return True
|
||||
|
||||
def _check_tensor_input(self, tensor):
|
||||
assert isinstance(tensor, list) and len(tensor) == 1
|
||||
t = tensor[0]
|
||||
if isinstance(t, np.ndarray):
|
||||
return t
|
||||
try:
|
||||
import torch
|
||||
|
||||
if isinstance(t, torch.Tensor):
|
||||
return t
|
||||
except ImportError:
|
||||
pass
|
||||
raise ValueError(
|
||||
f"MockInternalKVGroup only only accepts numpy.ndarray or torch.Tensor, received {type(t)}"
|
||||
)
|
||||
|
||||
def _serialize_tensor(self, tensor):
|
||||
if isinstance(tensor, np.ndarray):
|
||||
return tensor.tobytes(), tensor.shape, tensor.dtype
|
||||
try:
|
||||
import torch
|
||||
|
||||
if isinstance(tensor, torch.Tensor):
|
||||
return (
|
||||
tensor.cpu().numpy().tobytes(),
|
||||
tensor.shape,
|
||||
tensor.cpu().numpy().dtype,
|
||||
)
|
||||
except ImportError:
|
||||
pass
|
||||
raise ValueError(f"Unsupported tensor type: {type(tensor)}")
|
||||
|
||||
def _deserialize_tensor(self, data: bytes, shape, dtype, target_tensor):
|
||||
if isinstance(target_tensor, np.ndarray):
|
||||
np_array = np.frombuffer(data, dtype=dtype).reshape(shape)
|
||||
target_tensor[:] = np_array
|
||||
else:
|
||||
try:
|
||||
import torch
|
||||
|
||||
if isinstance(target_tensor, torch.Tensor):
|
||||
np_array = np.frombuffer(data, dtype=dtype).reshape(shape)
|
||||
target_tensor.copy_(torch.from_numpy(np_array))
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def broadcast(self, tensor, broadcast_options=BroadcastOptions()):
|
||||
tensor = self._check_tensor_input(tensor)
|
||||
root_rank = broadcast_options.root_rank
|
||||
|
||||
data_key = get_data_key(self._group_name, root_rank, "broadcast")
|
||||
|
||||
if self._rank == root_rank:
|
||||
data, shape, dtype = self._serialize_tensor(tensor)
|
||||
internal_kv._internal_kv_put(data_key, data)
|
||||
internal_kv._internal_kv_put(f"{data_key}_shape", str(shape))
|
||||
internal_kv._internal_kv_put(f"{data_key}_dtype", dtype.name)
|
||||
else:
|
||||
deadline_s = time.time() + 30.0
|
||||
while True:
|
||||
data = internal_kv._internal_kv_get(data_key)
|
||||
if data is not None:
|
||||
break
|
||||
if time.time() > deadline_s:
|
||||
raise TimeoutError(
|
||||
f"Timed out waiting for broadcast data from rank {root_rank}"
|
||||
)
|
||||
time.sleep(0.01)
|
||||
|
||||
deadline_s = time.time() + 30.0
|
||||
while True:
|
||||
shape_data = internal_kv._internal_kv_get(f"{data_key}_shape")
|
||||
dtype_data = internal_kv._internal_kv_get(f"{data_key}_dtype")
|
||||
if shape_data is not None and dtype_data is not None:
|
||||
break
|
||||
if time.time() > deadline_s:
|
||||
raise TimeoutError(
|
||||
f"Timed out waiting for broadcast metadata from rank {root_rank}"
|
||||
)
|
||||
time.sleep(0.01)
|
||||
|
||||
shape_str = shape_data.decode()
|
||||
shape = eval(shape_str)
|
||||
dtype_name = dtype_data.decode()
|
||||
dtype = np.dtype(dtype_name)
|
||||
|
||||
self._deserialize_tensor(data, shape, dtype, tensor)
|
||||
|
||||
def allreduce(self, tensor, allreduce_options=AllReduceOptions()):
|
||||
tensor = self._check_tensor_input(tensor)
|
||||
reduce_op = allreduce_options.reduceOp
|
||||
|
||||
data_key = get_data_key(self._group_name, self._rank, "allreduce")
|
||||
done_key = (
|
||||
f"collective_mock_{self._group_name}_allreduce_done_rank_{self._rank}"
|
||||
)
|
||||
|
||||
data, shape, dtype = self._serialize_tensor(tensor)
|
||||
internal_kv._internal_kv_put(data_key, data)
|
||||
internal_kv._internal_kv_put(f"{data_key}_shape", str(shape))
|
||||
internal_kv._internal_kv_put(f"{data_key}_dtype", dtype.name)
|
||||
internal_kv._internal_kv_put(done_key, b"1")
|
||||
|
||||
deadline_s = time.time() + 30.0
|
||||
while True:
|
||||
all_done = True
|
||||
for r in range(self._world_size):
|
||||
key = f"collective_mock_{self._group_name}_allreduce_done_rank_{r}"
|
||||
if internal_kv._internal_kv_get(key) is None:
|
||||
all_done = False
|
||||
break
|
||||
if all_done:
|
||||
break
|
||||
if time.time() > deadline_s:
|
||||
raise TimeoutError(
|
||||
"Timed out waiting for allreduce data from all ranks"
|
||||
)
|
||||
time.sleep(0.01)
|
||||
|
||||
result = None
|
||||
|
||||
for r in range(self._world_size):
|
||||
rank_data_key = get_data_key(self._group_name, r, "allreduce")
|
||||
rank_data = internal_kv._internal_kv_get(rank_data_key)
|
||||
rank_shape_data = internal_kv._internal_kv_get(f"{rank_data_key}_shape")
|
||||
rank_dtype_data = internal_kv._internal_kv_get(f"{rank_data_key}_dtype")
|
||||
|
||||
rank_shape_str = rank_shape_data.decode()
|
||||
rank_shape = eval(rank_shape_str)
|
||||
rank_dtype_name = rank_dtype_data.decode()
|
||||
rank_dtype = np.dtype(rank_dtype_name)
|
||||
|
||||
if isinstance(tensor, np.ndarray):
|
||||
rank_tensor = np.frombuffer(rank_data, dtype=rank_dtype).reshape(
|
||||
rank_shape
|
||||
)
|
||||
else:
|
||||
import torch
|
||||
|
||||
rank_np = np.frombuffer(rank_data, dtype=rank_dtype).reshape(rank_shape)
|
||||
rank_tensor = torch.from_numpy(rank_np)
|
||||
|
||||
if result is None:
|
||||
result = (
|
||||
rank_tensor.copy()
|
||||
if isinstance(rank_tensor, np.ndarray)
|
||||
else rank_tensor.clone()
|
||||
)
|
||||
else:
|
||||
if reduce_op == ReduceOp.SUM:
|
||||
result += rank_tensor
|
||||
elif reduce_op == ReduceOp.PRODUCT:
|
||||
result *= rank_tensor
|
||||
elif reduce_op == ReduceOp.MAX:
|
||||
if isinstance(result, np.ndarray):
|
||||
result = np.maximum(result, rank_tensor)
|
||||
else:
|
||||
import torch
|
||||
|
||||
result = torch.maximum(result, rank_tensor)
|
||||
elif reduce_op == ReduceOp.MIN:
|
||||
if isinstance(result, np.ndarray):
|
||||
result = np.minimum(result, rank_tensor)
|
||||
else:
|
||||
import torch
|
||||
|
||||
result = torch.minimum(result, rank_tensor)
|
||||
|
||||
if isinstance(tensor, np.ndarray):
|
||||
tensor[:] = result
|
||||
else:
|
||||
import torch
|
||||
|
||||
if isinstance(result, np.ndarray):
|
||||
tensor.copy_(torch.from_numpy(result))
|
||||
else:
|
||||
tensor.copy_(result)
|
||||
|
||||
def barrier(self, barrier_options=BarrierOptions()):
|
||||
barrier_id = self._barrier_counter
|
||||
barrier_key = get_barrier_key(self._group_name, barrier_id)
|
||||
rank_key = f"{barrier_key}_rank_{self._rank}"
|
||||
|
||||
internal_kv._internal_kv_put(rank_key, b"1")
|
||||
|
||||
deadline_s = time.time() + 30.0
|
||||
while True:
|
||||
all_arrived = True
|
||||
for r in range(self._world_size):
|
||||
key = f"{barrier_key}_rank_{r}"
|
||||
if internal_kv._internal_kv_get(key) is None:
|
||||
all_arrived = False
|
||||
break
|
||||
if all_arrived:
|
||||
break
|
||||
if time.time() > deadline_s:
|
||||
raise TimeoutError("Timed out waiting for barrier")
|
||||
time.sleep(0.01)
|
||||
|
||||
self._barrier_counter += 1
|
||||
|
||||
def reduce(self, tensor, reduce_options=ReduceOptions()):
|
||||
raise NotImplementedError("reduce is not implemented in MockInternalKVGroup")
|
||||
|
||||
def allgather(self, tensor_list, tensor, allgather_options=AllGatherOptions()):
|
||||
raise NotImplementedError("allgather is not implemented in MockInternalKVGroup")
|
||||
|
||||
def reducescatter(
|
||||
self, tensor, tensor_list, reducescatter_options=ReduceScatterOptions()
|
||||
):
|
||||
raise NotImplementedError(
|
||||
"reducescatter is not implemented in MockInternalKVGroup"
|
||||
)
|
||||
|
||||
def send(self, tensor, send_options: SendOptions):
|
||||
raise NotImplementedError("send is not implemented in MockInternalKVGroup")
|
||||
|
||||
def recv(self, tensor, recv_options: RecvOptions):
|
||||
raise NotImplementedError("recv is not implemented in MockInternalKVGroup")
|
||||
|
||||
|
||||
def test_mock_backend_create_group():
|
||||
"""Test using create_collective_group (driver-managed approach).
|
||||
|
||||
In this approach:
|
||||
- Driver calls create_collective_group() to declare the group
|
||||
- Workers only need to register the backend
|
||||
- Workers do NOT call init_collective_group()
|
||||
- The group is automatically initialized when workers call collective ops
|
||||
"""
|
||||
|
||||
ray.init()
|
||||
register_collective_backend("MOCK", MockInternalKVGroup)
|
||||
|
||||
@ray.remote
|
||||
class Worker:
|
||||
def __init__(self, rank):
|
||||
self.rank = rank
|
||||
|
||||
def setup(self):
|
||||
from ray.util.collective.backend_registry import register_collective_backend
|
||||
|
||||
register_collective_backend("MOCK", MockInternalKVGroup)
|
||||
|
||||
def broadcast_test(self):
|
||||
if self.rank == 0:
|
||||
tensor = np.array([42.0, 43.0, 44.0], dtype=np.float32)
|
||||
else:
|
||||
tensor = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
|
||||
broadcast(tensor, src_rank=0)
|
||||
return tensor.tolist()
|
||||
|
||||
def allreduce_test(self):
|
||||
tensor = np.array([float(self.rank + 1)], dtype=np.float32)
|
||||
allreduce(tensor, op=ReduceOp.SUM)
|
||||
return tensor.item()
|
||||
|
||||
actors = [Worker.remote(rank=i) for i in range(3)]
|
||||
|
||||
create_collective_group(
|
||||
actors=actors,
|
||||
world_size=3,
|
||||
ranks=[0, 1, 2],
|
||||
backend="MOCK",
|
||||
group_name="default",
|
||||
)
|
||||
|
||||
ray.get([a.setup.remote() for a in actors])
|
||||
|
||||
results = ray.get([a.broadcast_test.remote() for a in actors])
|
||||
expected = [[42.0, 43.0, 44.0]] * 3
|
||||
if results == expected:
|
||||
print("Broadcast test passed!")
|
||||
else:
|
||||
print(f"Broadcast test failed! Expected {expected}, got {results}")
|
||||
|
||||
results = ray.get([a.allreduce_test.remote() for a in actors])
|
||||
|
||||
if results == [6.0, 6.0, 6.0]:
|
||||
print("AllReduce test passed!")
|
||||
else:
|
||||
print(f"AllReduce test failed! Expected [6.0, 6.0, 6.0], got {results}")
|
||||
|
||||
ray.shutdown()
|
||||
_unregister_collective_backend("MOCK")
|
||||
print("test_mock_backend_create_group completed!")
|
||||
|
||||
|
||||
def test_mock_backend_init_group():
|
||||
"""Test using init_collective_group (worker-managed approach).
|
||||
|
||||
In this approach:
|
||||
- Workers call init_collective_group() inside their setup method
|
||||
- Driver does NOT call create_collective_group()
|
||||
- Each worker explicitly initializes its own group membership
|
||||
"""
|
||||
|
||||
ray.init()
|
||||
|
||||
@ray.remote
|
||||
class Worker:
|
||||
def __init__(self, rank):
|
||||
self.rank = rank
|
||||
|
||||
def setup(self, world_size):
|
||||
from ray.util.collective.backend_registry import register_collective_backend
|
||||
|
||||
register_collective_backend("MOCK", MockInternalKVGroup)
|
||||
|
||||
init_collective_group(
|
||||
world_size=world_size,
|
||||
rank=self.rank,
|
||||
backend="MOCK",
|
||||
group_name="default",
|
||||
)
|
||||
|
||||
def broadcast_test(self):
|
||||
if self.rank == 0:
|
||||
tensor = np.array([42.0, 43.0, 44.0], dtype=np.float32)
|
||||
else:
|
||||
tensor = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
|
||||
broadcast(tensor, src_rank=0)
|
||||
return tensor.tolist()
|
||||
|
||||
def allreduce_test(self):
|
||||
tensor = np.array([float(self.rank + 1)], dtype=np.float32)
|
||||
allreduce(tensor, op=ReduceOp.SUM)
|
||||
return tensor.item()
|
||||
|
||||
actors = [Worker.remote(rank=i) for i in range(3)]
|
||||
|
||||
# Do NOT call create_collective_group here
|
||||
ray.get([a.setup.remote(3) for a in actors])
|
||||
|
||||
results = ray.get([a.broadcast_test.remote() for a in actors])
|
||||
expected = [[42.0, 43.0, 44.0]] * 3
|
||||
if results == expected:
|
||||
print("Broadcast test passed!")
|
||||
else:
|
||||
print(f"Broadcast test failed! Expected {expected}, got {results}")
|
||||
|
||||
results = ray.get([a.allreduce_test.remote() for a in actors])
|
||||
|
||||
if results == [6.0, 6.0, 6.0]:
|
||||
print("AllReduce test passed!")
|
||||
else:
|
||||
print(f"AllReduce test failed! Expected [6.0, 6.0, 6.0], got {results}")
|
||||
|
||||
ray.shutdown()
|
||||
_unregister_collective_backend("MOCK")
|
||||
print("test_mock_backend_init_group completed!")
|
||||
|
||||
|
||||
def test_mock_backend_worker_not_registered():
|
||||
"""Test error handling when backend is not registered in worker.
|
||||
|
||||
This test uses create_collective_group (driver-managed approach).
|
||||
The driver registers the backend, but workers do not.
|
||||
When workers try to call collective ops, they should fail.
|
||||
|
||||
Note: We use world_size=1 to avoid "Unhandled error" messages
|
||||
from multiple workers failing simultaneously.
|
||||
"""
|
||||
|
||||
ray.init()
|
||||
register_collective_backend("MOCK", MockInternalKVGroup)
|
||||
|
||||
@ray.remote
|
||||
class Worker:
|
||||
def __init__(self, rank):
|
||||
self.rank = rank
|
||||
|
||||
def broadcast_test(self):
|
||||
tensor = np.array([0.0, 0.0, 0.0], dtype=np.float32)
|
||||
broadcast(tensor, src_rank=0)
|
||||
return tensor.tolist()
|
||||
|
||||
# Use single actor to avoid multiple "Unhandled error" messages
|
||||
actors = [Worker.remote(rank=0)]
|
||||
|
||||
create_collective_group(
|
||||
actors=actors,
|
||||
world_size=1,
|
||||
ranks=[0],
|
||||
backend="MOCK",
|
||||
group_name="default",
|
||||
)
|
||||
|
||||
test_passed = False
|
||||
try:
|
||||
ray.get([a.broadcast_test.remote() for a in actors])
|
||||
print("ERROR: Should have raised an exception for missing registration!")
|
||||
except Exception as e:
|
||||
if "not registered" in str(e) or "not initialized" in str(e):
|
||||
print(
|
||||
"Test passed! Correctly raised error for missing worker registration."
|
||||
)
|
||||
test_passed = True
|
||||
else:
|
||||
print(f"ERROR: Unexpected error: {e}")
|
||||
|
||||
ray.shutdown()
|
||||
_unregister_collective_backend("MOCK")
|
||||
|
||||
if not test_passed:
|
||||
print("Test failed!")
|
||||
|
||||
|
||||
def test_mock_backend_driver_not_registered():
|
||||
"""Test error handling when backend is not registered on driver.
|
||||
|
||||
This test uses create_collective_group, but the driver doesn't
|
||||
register the backend first, so it should fail immediately.
|
||||
"""
|
||||
|
||||
ray.init()
|
||||
|
||||
@ray.remote
|
||||
class Worker:
|
||||
def __init__(self, rank):
|
||||
self.rank = rank
|
||||
|
||||
actors = [Worker.remote(rank=i) for i in range(2)]
|
||||
|
||||
try:
|
||||
create_collective_group(
|
||||
actors=actors,
|
||||
world_size=2,
|
||||
ranks=[0, 1],
|
||||
backend="MOCK",
|
||||
group_name="default",
|
||||
)
|
||||
print("ERROR: Should have raised an exception for missing registration!")
|
||||
except Exception as e:
|
||||
if "not registered" in str(e):
|
||||
print(
|
||||
"Test passed! Correctly raised error for missing driver registration."
|
||||
)
|
||||
else:
|
||||
print(f"ERROR: Unexpected error: {e}")
|
||||
|
||||
ray.shutdown()
|
||||
_unregister_collective_backend("MOCK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("=" * 60)
|
||||
print("Test 1: create_collective_group approach (driver-managed)")
|
||||
print("=" * 60)
|
||||
test_mock_backend_create_group()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test 2: init_collective_group approach (worker-managed)")
|
||||
print("=" * 60)
|
||||
test_mock_backend_init_group()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test 3: Error handling - worker not registered")
|
||||
print("=" * 60)
|
||||
test_mock_backend_worker_not_registered()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("Test 4: Error handling - driver not registered")
|
||||
print("=" * 60)
|
||||
test_mock_backend_driver_not_registered()
|
||||
@@ -0,0 +1,37 @@
|
||||
import cupy as cp
|
||||
|
||||
import ray
|
||||
import ray.util.collective as collective
|
||||
|
||||
|
||||
@ray.remote(num_gpus=1)
|
||||
class Worker:
|
||||
def __init__(self):
|
||||
self.send = cp.ones((4,), dtype=cp.float32)
|
||||
self.recv = cp.zeros((4,), dtype=cp.float32)
|
||||
|
||||
def setup(self, world_size, rank):
|
||||
collective.init_collective_group(world_size, rank, "nccl", "default")
|
||||
return True
|
||||
|
||||
def compute(self):
|
||||
collective.allreduce(self.send, "default")
|
||||
return self.send
|
||||
|
||||
def destroy(self):
|
||||
collective.destroy_group()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
send = cp.ones((4,), dtype=cp.float32)
|
||||
ray.init(num_gpus=2)
|
||||
num_workers = 2
|
||||
workers = []
|
||||
init_rets = []
|
||||
for i in range(num_workers):
|
||||
w = Worker.remote()
|
||||
workers.append(w)
|
||||
init_rets.append(w.setup.remote(num_workers, i))
|
||||
_ = ray.get(init_rets)
|
||||
results = ray.get([w.compute.remote() for w in workers])
|
||||
ray.shutdown()
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
import cupy as cp
|
||||
|
||||
import ray
|
||||
import ray.util.collective as collective
|
||||
|
||||
|
||||
@ray.remote(num_gpus=1)
|
||||
class Worker:
|
||||
def __init__(self):
|
||||
self.send = cp.ones((4,), dtype=cp.float32)
|
||||
|
||||
def compute(self):
|
||||
collective.allreduce(self.send, "177")
|
||||
return self.send
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ray.init(num_gpus=2)
|
||||
|
||||
num_workers = 2
|
||||
workers = []
|
||||
for i in range(num_workers):
|
||||
w = Worker.remote()
|
||||
workers.append(w)
|
||||
_options = {
|
||||
"group_name": "177",
|
||||
"world_size": 2,
|
||||
"ranks": [0, 1],
|
||||
"backend": "nccl",
|
||||
}
|
||||
collective.create_collective_group(workers, **_options)
|
||||
results = ray.get([w.compute.remote() for w in workers])
|
||||
ray.shutdown()
|
||||
@@ -0,0 +1,43 @@
|
||||
import cupy as cp
|
||||
from cupy.cuda import Device
|
||||
|
||||
import ray
|
||||
import ray.util.collective as collective
|
||||
|
||||
|
||||
@ray.remote(num_gpus=2)
|
||||
class Worker:
|
||||
def __init__(self):
|
||||
with Device(0):
|
||||
self.send1 = cp.ones((4,), dtype=cp.float32)
|
||||
with Device(1):
|
||||
self.send2 = cp.ones((4,), dtype=cp.float32) * 2
|
||||
|
||||
self.recv = cp.zeros((4,), dtype=cp.float32)
|
||||
|
||||
def setup(self, world_size, rank):
|
||||
collective.init_collective_group(world_size, rank, "nccl", "177")
|
||||
return True
|
||||
|
||||
def compute(self):
|
||||
collective.allreduce_multigpu([self.send1, self.send2], "177")
|
||||
return [self.send1, self.send2], self.send1.device, self.send2.device
|
||||
|
||||
def destroy(self):
|
||||
collective.destroy_collective_group("177")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ray.init(address="auto")
|
||||
num_workers = 2
|
||||
workers = []
|
||||
init_rets = []
|
||||
for i in range(num_workers):
|
||||
w = Worker.remote()
|
||||
workers.append(w)
|
||||
init_rets.append(w.setup.remote(num_workers, i))
|
||||
a = ray.get(init_rets)
|
||||
results = ray.get([w.compute.remote() for w in workers])
|
||||
print(results)
|
||||
ray.get([w.destroy.remote() for w in workers])
|
||||
ray.shutdown()
|
||||
@@ -0,0 +1,53 @@
|
||||
import cupy as cp
|
||||
from cupy.cuda import Device
|
||||
|
||||
import ray
|
||||
import ray.util.collective as collective
|
||||
|
||||
|
||||
@ray.remote(num_gpus=2)
|
||||
class Worker:
|
||||
def __init__(self):
|
||||
with Device(0):
|
||||
self.send1 = cp.ones((4,), dtype=cp.float32)
|
||||
with Device(1):
|
||||
self.send2 = cp.ones((4,), dtype=cp.float32) * 2
|
||||
|
||||
with Device(0):
|
||||
self.recv1 = cp.zeros((4,), dtype=cp.float32)
|
||||
with Device(1):
|
||||
self.recv2 = cp.zeros((4,), dtype=cp.float32)
|
||||
self.rank = -1
|
||||
|
||||
def setup(self, world_size, rank):
|
||||
self.rank = rank
|
||||
collective.init_collective_group(world_size, rank, "nccl", "8")
|
||||
return True
|
||||
|
||||
def compute(self):
|
||||
if self.rank == 0:
|
||||
with Device(0):
|
||||
collective.send_multigpu(self.send1 * 2, 1, 1, "8")
|
||||
else:
|
||||
# with Device(1):
|
||||
collective.recv_multigpu(self.recv2, 0, 0, "8")
|
||||
return self.recv2
|
||||
|
||||
def destroy(self):
|
||||
collective.destroy_collective_group("8")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
ray.init(address="auto")
|
||||
num_workers = 2
|
||||
workers = []
|
||||
init_rets = []
|
||||
for i in range(num_workers):
|
||||
w = Worker.remote()
|
||||
workers.append(w)
|
||||
init_rets.append(w.setup.remote(num_workers, i))
|
||||
a = ray.get(init_rets)
|
||||
results = ray.get([w.compute.remote() for w in workers])
|
||||
print(results)
|
||||
ray.get([w.destroy.remote() for w in workers])
|
||||
ray.shutdown()
|
||||
@@ -0,0 +1 @@
|
||||
cupy-cuda100
|
||||
@@ -0,0 +1,106 @@
|
||||
"""Some fixtures for collective tests."""
|
||||
import logging
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
|
||||
try:
|
||||
from ray.util.collective.collective_group.nccl_collective_group import (
|
||||
_get_comm_key_from_devices,
|
||||
_get_comm_key_send_recv,
|
||||
)
|
||||
except Exception: # Cupy/NCCL may be unavailable on CPU-only setups
|
||||
_get_comm_key_from_devices = None
|
||||
_get_comm_key_send_recv = None
|
||||
from ray.util.collective.const import get_store_name
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel("INFO")
|
||||
|
||||
|
||||
# TODO (Hao): remove this clean_up function as it sometimes crashes Ray.
|
||||
def clean_up():
|
||||
# If NCCL helpers are unavailable (e.g., no cupy), skip cleanup.
|
||||
if _get_comm_key_from_devices is None or _get_comm_key_send_recv is None:
|
||||
return
|
||||
group_names = ["default", "test", "123?34!", "default2", "random"]
|
||||
group_names.extend([str(i) for i in range(10)])
|
||||
max_world_size = 4
|
||||
all_keys = []
|
||||
for name in group_names:
|
||||
devices = [[0], [0, 1], [1, 0]]
|
||||
for d in devices:
|
||||
collective_communicator_key = _get_comm_key_from_devices(d)
|
||||
all_keys.append(collective_communicator_key + "@" + name)
|
||||
for i in range(max_world_size):
|
||||
for j in range(max_world_size):
|
||||
if i < j:
|
||||
p2p_communicator_key = _get_comm_key_send_recv(i, 0, j, 0)
|
||||
all_keys.append(p2p_communicator_key + "@" + name)
|
||||
for group_key in all_keys:
|
||||
store_name = get_store_name(group_key)
|
||||
try:
|
||||
actor = ray.get_actor(store_name)
|
||||
except ValueError:
|
||||
actor = None
|
||||
if actor:
|
||||
logger.debug(
|
||||
"Killing actor with group_key: '{}' and store: '{}'.".format(
|
||||
group_key, store_name
|
||||
)
|
||||
)
|
||||
ray.kill(actor)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_start_single_node_2_gpus():
|
||||
# Please start this fixture in a cluster with 2 GPUs.
|
||||
address_info = ray.init(num_gpus=2)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
# Hao: this fixture is a bit tricky.
|
||||
# I use a bash script to start a ray cluster on
|
||||
# my own on-premise cluster before run this fixture.
|
||||
@pytest.fixture
|
||||
def ray_start_distributed_2_nodes_4_gpus():
|
||||
# The cluster has a setup of 2 nodes, each node with 2
|
||||
# GPUs. Each actor will be allocated 1 GPU.
|
||||
ray.init("auto")
|
||||
yield
|
||||
clean_up()
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_start_distributed_multigpu_2_nodes_4_gpus():
|
||||
# The cluster has a setup of 2 nodes, each node with 2
|
||||
# GPUs. Each actor will be allocated 2 GPUs.
|
||||
ray.init("auto")
|
||||
yield
|
||||
clean_up()
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_start_single_node():
|
||||
address_info = ray.init(num_cpus=8)
|
||||
yield address_info
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def ray_start_distributed_2_nodes():
|
||||
# The cluster has a setup of 2 nodes.
|
||||
# no GPUs!
|
||||
ray.init("auto")
|
||||
yield
|
||||
ray.shutdown()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def shutdown_only():
|
||||
yield None
|
||||
ray.shutdown()
|
||||
@@ -0,0 +1,137 @@
|
||||
import logging
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
import ray
|
||||
import ray.util.collective as col
|
||||
from ray.util.collective.types import Backend, ReduceOp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ray.remote(num_cpus=1)
|
||||
class Worker:
|
||||
def __init__(self):
|
||||
self.buffer = None
|
||||
self.list_buffer = None
|
||||
|
||||
def init_tensors(self):
|
||||
self.buffer = np.ones((10,), dtype=np.float32)
|
||||
self.list_buffer = [np.ones((10,), dtype=np.float32) for _ in range(2)]
|
||||
return True
|
||||
|
||||
def init_group(self, world_size, rank, backend=Backend.NCCL, group_name="default"):
|
||||
col.init_collective_group(world_size, rank, backend, group_name)
|
||||
return True
|
||||
|
||||
def set_buffer(self, data):
|
||||
self.buffer = data
|
||||
return self.buffer
|
||||
|
||||
def get_buffer(self):
|
||||
return self.buffer
|
||||
|
||||
def set_list_buffer(self, list_of_arrays, copy=False):
|
||||
if copy:
|
||||
copy_list = []
|
||||
for tensor in list_of_arrays:
|
||||
if isinstance(tensor, np.ndarray):
|
||||
copy_list.append(tensor.copy())
|
||||
elif isinstance(tensor, torch.Tensor):
|
||||
copy_list.append(tensor.clone().detach())
|
||||
self.list_buffer = copy_list
|
||||
else:
|
||||
self.list_buffer = list_of_arrays
|
||||
return self.list_buffer
|
||||
|
||||
def do_allreduce(self, group_name="default", op=ReduceOp.SUM):
|
||||
col.allreduce(self.buffer, group_name, op)
|
||||
return self.buffer
|
||||
|
||||
def do_reduce(self, group_name="default", dst_rank=0, op=ReduceOp.SUM):
|
||||
col.reduce(self.buffer, dst_rank, group_name, op)
|
||||
return self.buffer
|
||||
|
||||
def do_broadcast(self, group_name="default", src_rank=0):
|
||||
col.broadcast(self.buffer, src_rank, group_name)
|
||||
return self.buffer
|
||||
|
||||
def do_allgather(self, group_name="default"):
|
||||
col.allgather(self.list_buffer, self.buffer, group_name)
|
||||
return self.list_buffer
|
||||
|
||||
def do_reducescatter(self, group_name="default", op=ReduceOp.SUM):
|
||||
col.reducescatter(self.buffer, self.list_buffer, group_name, op)
|
||||
return self.buffer
|
||||
|
||||
def do_send(self, group_name="default", dst_rank=0):
|
||||
col.send(self.buffer, dst_rank, group_name)
|
||||
return self.buffer
|
||||
|
||||
def do_recv(self, group_name="default", src_rank=0):
|
||||
col.recv(self.buffer, src_rank, group_name)
|
||||
return self.buffer
|
||||
|
||||
def destroy_group(self, group_name="default"):
|
||||
col.destroy_collective_group(group_name)
|
||||
return True
|
||||
|
||||
def report_rank(self, group_name="default"):
|
||||
rank = col.get_rank(group_name)
|
||||
return rank
|
||||
|
||||
def report_world_size(self, group_name="default"):
|
||||
ws = col.get_collective_group_size(group_name)
|
||||
return ws
|
||||
|
||||
def report_nccl_availability(self):
|
||||
avail = col.nccl_available()
|
||||
return avail
|
||||
|
||||
def report_gloo_availability(self):
|
||||
avail = col.gloo_available()
|
||||
return avail
|
||||
|
||||
def report_is_group_initialized(self, group_name="default"):
|
||||
is_init = col.is_group_initialized(group_name)
|
||||
return is_init
|
||||
|
||||
|
||||
def create_collective_workers(num_workers=2, group_name="default", backend="nccl"):
|
||||
actors = [None] * num_workers
|
||||
for i in range(num_workers):
|
||||
actor = Worker.remote()
|
||||
ray.get([actor.init_tensors.remote()])
|
||||
actors[i] = actor
|
||||
world_size = num_workers
|
||||
init_results = ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
return actors, init_results
|
||||
|
||||
|
||||
def init_tensors_for_gather_scatter(
|
||||
actors, array_size=10, dtype=np.float32, tensor_backend="numpy"
|
||||
):
|
||||
world_size = len(actors)
|
||||
for i, a in enumerate(actors):
|
||||
if tensor_backend == "numpy":
|
||||
t = np.ones(array_size, dtype=dtype) * (i + 1)
|
||||
elif tensor_backend == "torch":
|
||||
t = torch.ones(array_size, dtype=torch.float32) * (i + 1)
|
||||
else:
|
||||
raise RuntimeError("Unsupported tensor backend.")
|
||||
ray.get([a.set_buffer.remote(t)])
|
||||
if tensor_backend == "numpy":
|
||||
list_buffer = [np.ones(array_size, dtype=dtype) for _ in range(world_size)]
|
||||
elif tensor_backend == "torch":
|
||||
list_buffer = [
|
||||
torch.ones(array_size, dtype=torch.float32) for _ in range(world_size)
|
||||
]
|
||||
else:
|
||||
raise RuntimeError("Unsupported tensor backend.")
|
||||
ray.get([a.set_list_buffer.remote(list_buffer, copy=True) for a in actors])
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Test the allgather API on a distributed Ray cluster."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import (
|
||||
create_collective_workers,
|
||||
init_tensors_for_gather_scatter,
|
||||
)
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("tensor_backend", ["numpy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_allgather_different_array_size(
|
||||
ray_start_distributed_2_nodes, array_size, tensor_backend, backend
|
||||
):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
if tensor_backend == "numpy":
|
||||
assert (
|
||||
results[i][j] == np.ones(array_size, dtype=np.float32) * (j + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j]
|
||||
== torch.ones(array_size, dtype=torch.float32) * (j + 1)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dtype", [np.uint8, np.float16, np.float32, np.float64])
|
||||
def test_allgather_different_dtype(ray_start_distributed_2_nodes, dtype, backend):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(actors, dtype=dtype)
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i][j] == np.ones(10, dtype=dtype) * (j + 1)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("length", [0, 1, 3, 4, 7, 8])
|
||||
def test_unmatched_tensor_list_length(ray_start_distributed_2_nodes, length, backend):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
list_buffer = [np.ones(10, dtype=np.float32) for _ in range(length)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer, copy=True) for a in actors])
|
||||
if length != world_size:
|
||||
with pytest.raises(RuntimeError):
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
else:
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("shape", [10, 20, [4, 5], [1, 3, 5, 7]])
|
||||
def test_unmatched_tensor_shape(ray_start_distributed_2_nodes, shape, backend):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(actors, array_size=10)
|
||||
list_buffer = [np.ones(shape, dtype=np.float32) for _ in range(world_size)]
|
||||
ray.get([a.set_list_buffer.remote(list_buffer, copy=True) for a in actors])
|
||||
if shape != 10:
|
||||
with pytest.raises(RuntimeError):
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
else:
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allgather_torch_numpy(ray_start_distributed_2_nodes, backend):
|
||||
world_size = 8
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
# tensor is pytorch, list is numpy
|
||||
for i, a in enumerate(actors):
|
||||
t = torch.ones(shape, dtype=torch.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [np.ones(shape, dtype=np.float32) for _ in range(world_size)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer, copy=True)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i][j] == np.ones(shape, dtype=np.float32) * (j + 1)).all()
|
||||
|
||||
# tensor is numpy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [
|
||||
torch.ones(shape, dtype=torch.float32) for _ in range(world_size)
|
||||
]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (
|
||||
results[i][j] == torch.ones(shape, dtype=torch.float32) * (j + 1)
|
||||
).all()
|
||||
|
||||
# some tensors in the list are pytorch, some are numpy
|
||||
for i, a in enumerate(actors):
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32))
|
||||
else:
|
||||
list_buffer.append(np.ones(shape, dtype=np.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer, copy=True)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
assert (
|
||||
results[i][j] == torch.ones(shape, dtype=torch.float32) * (j + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j] == np.ones(shape, dtype=np.float32) * (j + 1)
|
||||
).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,179 @@
|
||||
"""Test the collective allreduice API on a distributed Ray cluster."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import create_collective_workers
|
||||
from ray.util.collective.types import Backend, ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("world_size", [5, 6, 7, 8])
|
||||
def test_allreduce_different_name(
|
||||
ray_start_distributed_2_nodes, group_name, world_size, backend
|
||||
):
|
||||
actors, _ = create_collective_workers(
|
||||
num_workers=world_size, group_name=group_name, backend=backend
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(group_name) for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
def test_allreduce_different_array_size(
|
||||
ray_start_distributed_2_nodes, array_size, backend
|
||||
):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[a.set_buffer.remote(np.ones(array_size, dtype=np.float32)) for a in actors]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((array_size,), dtype=np.float32) * world_size).all()
|
||||
assert (results[1] == np.ones((array_size,), dtype=np.float32) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allreduce_destroy(
|
||||
ray_start_distributed_2_nodes, backend, group_name="default"
|
||||
):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
|
||||
# destroy the group and try do work, should fail
|
||||
ray.get([a.destroy_group.remote() for a in actors])
|
||||
with pytest.raises(RuntimeError):
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
|
||||
# reinit the same group and all reduce
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (
|
||||
results[0] == np.ones((10,), dtype=np.float32) * world_size * world_size
|
||||
).all()
|
||||
assert (
|
||||
results[1] == np.ones((10,), dtype=np.float32) * world_size * world_size
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allreduce_multiple_group(ray_start_distributed_2_nodes, backend, num_groups=5):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
for group_name in range(1, num_groups):
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, str(group_name))
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for i in range(num_groups):
|
||||
group_name = "default" if i == 0 else str(i)
|
||||
results = ray.get([a.do_allreduce.remote(group_name) for a in actors])
|
||||
assert (
|
||||
results[0] == np.ones((10,), dtype=np.float32) * (world_size ** (i + 1))
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allreduce_different_op(ray_start_distributed_2_nodes, backend):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
# check product
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.PRODUCT) for a in actors])
|
||||
product = 1
|
||||
for i in range(world_size):
|
||||
product = product * (i + 2)
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * product).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * product).all()
|
||||
|
||||
# check min
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.MIN) for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * 2).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * 2).all()
|
||||
|
||||
# check max
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.MAX) for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * 9).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * 9).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dtype", [np.uint8, np.float16, np.float32, np.float64])
|
||||
def test_allreduce_different_dtype(ray_start_distributed_2_nodes, dtype, backend):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait([a.set_buffer.remote(np.ones(10, dtype=dtype)) for a in actors])
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=dtype) * world_size).all()
|
||||
assert (results[1] == np.ones((10,), dtype=dtype) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allreduce_torch_numpy(ray_start_distributed_2_nodes, backend):
|
||||
# import torch
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((10,)) * world_size).all()
|
||||
|
||||
ray.wait(
|
||||
[
|
||||
actors[0].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
ray.wait([actors[1].set_buffer.remote(np.ones(10, dtype=np.float32))])
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
"""Test the collective group APIs."""
|
||||
from random import shuffle
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import Worker, create_collective_workers
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("world_size", [2, 3, 4])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
def test_init_two_actors(
|
||||
ray_start_distributed_2_nodes, world_size, group_name, backend
|
||||
):
|
||||
actors, results = create_collective_workers(world_size, group_name, backend=backend)
|
||||
for i in range(world_size):
|
||||
assert results[i]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("world_size", [2, 3, 4])
|
||||
def test_init_multiple_groups(ray_start_distributed_2_nodes, world_size, backend):
|
||||
num_groups = 5
|
||||
actors = [Worker.remote() for _ in range(world_size)]
|
||||
for i in range(num_groups):
|
||||
group_name = str(i)
|
||||
init_results = ray.get(
|
||||
[
|
||||
actor.init_group.remote(
|
||||
world_size, i, group_name=group_name, backend=backend
|
||||
)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for j in range(world_size):
|
||||
assert init_results[j]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("world_size", [5, 6, 7, 8])
|
||||
def test_get_rank(ray_start_distributed_2_nodes, world_size, backend):
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote())
|
||||
assert actor0_rank == 0
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote())
|
||||
assert actor1_rank == 1
|
||||
|
||||
# create a second group with a different name, and different
|
||||
# orders of ranks.
|
||||
new_group_name = "default2"
|
||||
ranks = list(range(world_size))
|
||||
shuffle(ranks)
|
||||
_ = ray.get(
|
||||
[
|
||||
actor.init_group.remote(
|
||||
world_size, ranks[i], group_name=new_group_name, backend=backend
|
||||
)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote(new_group_name))
|
||||
assert actor0_rank == ranks[0]
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote(new_group_name))
|
||||
assert actor1_rank == ranks[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("world_size", [5, 6, 7, 8])
|
||||
def test_get_world_size(ray_start_distributed_2_nodes, world_size, backend):
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
actor0_world_size = ray.get(actors[0].report_world_size.remote())
|
||||
actor1_world_size = ray.get(actors[1].report_world_size.remote())
|
||||
assert actor0_world_size == actor1_world_size == world_size
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_is_group_initialized(ray_start_distributed_2_nodes, backend):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
# check group is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("random"))
|
||||
assert not actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("123"))
|
||||
assert not actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote("456"))
|
||||
assert not actor1_is_init
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_destroy_group(ray_start_distributed_2_nodes, backend):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
# Now destroy the group at actor0
|
||||
ray.wait([actors[0].destroy_group.remote()])
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert not actor0_is_init
|
||||
|
||||
# should go well as the group `random` does not exist at all
|
||||
ray.wait([actors[0].destroy_group.remote("random")])
|
||||
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("random")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("default")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert not actor1_is_init
|
||||
for i in range(2, world_size):
|
||||
ray.wait([actors[i].destroy_group.remote("default")])
|
||||
|
||||
# Now reconstruct the group using the same name
|
||||
init_results = ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend=backend)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert init_results[i]
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Test the broadcast API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import create_collective_workers
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("src_rank", [0, 2, 5, 6, 7])
|
||||
def test_broadcast_different_name(
|
||||
ray_start_distributed_2_nodes, group_name, src_rank, backend
|
||||
):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(
|
||||
num_workers=world_size, group_name=group_name, backend=backend
|
||||
)
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones((10,), dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_broadcast.remote(group_name=group_name, src_rank=src_rank)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * (src_rank + 2)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("src_rank", [0, 2, 5, 6, 7])
|
||||
def test_broadcast_different_array_size(
|
||||
ray_start_distributed_2_nodes, array_size, src_rank, backend
|
||||
):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(array_size, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (
|
||||
results[i] == np.ones((array_size,), dtype=np.float32) * (src_rank + 2)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("src_rank", [0, 2, 5, 6, 7])
|
||||
def test_broadcast_torch_numpy(ray_start_distributed_2_nodes, src_rank, backend):
|
||||
import torch
|
||||
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
* world_size
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
if src_rank == 0:
|
||||
assert (results[0] == np.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,))).all()
|
||||
else:
|
||||
assert (results[0] == np.ones((10,)) * world_size).all()
|
||||
assert (results[1] == torch.ones((10,)) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_broadcast_invalid_rank(ray_start_distributed_2_nodes, backend, src_rank=9):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
with pytest.raises(ValueError):
|
||||
_ = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,147 @@
|
||||
"""Test the reduce API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import create_collective_workers
|
||||
from ray.util.collective.types import Backend, ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 2, 5, 6, 7])
|
||||
def test_reduce_different_name(
|
||||
ray_start_distributed_2_nodes, group_name, backend, dst_rank
|
||||
):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(
|
||||
num_workers=world_size, group_name=group_name, backend=backend
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(group_name, dst_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 2, 5, 6, 7])
|
||||
def test_reduce_different_array_size(
|
||||
ray_start_distributed_2_nodes, backend, array_size, dst_rank
|
||||
):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[a.set_buffer.remote(np.ones(array_size, dtype=np.float32)) for a in actors]
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (
|
||||
results[i] == np.ones((array_size,), dtype=np.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((array_size,), dtype=np.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 2, 5, 6, 7])
|
||||
def test_reduce_different_op(ray_start_distributed_2_nodes, backend, dst_rank):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
# check product
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.PRODUCT) for a in actors]
|
||||
)
|
||||
|
||||
product = 1
|
||||
for i in range(world_size):
|
||||
product = product * (i + 2)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * product).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * (i + 2)).all()
|
||||
# check min
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.MIN) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * 2).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * (i + 2)).all()
|
||||
|
||||
# check max
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.MAX) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (
|
||||
results[i] == np.ones((10,), dtype=np.float32) * (world_size + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * (i + 2)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 2, 5, 6, 7])
|
||||
def test_reduce_torch_numpy(ray_start_distributed_2_nodes, backend, dst_rank):
|
||||
import torch
|
||||
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.get(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
if dst_rank == 0:
|
||||
assert (results[0] == np.ones((10,)) * world_size).all()
|
||||
assert (results[1] == torch.ones((10,))).all()
|
||||
else:
|
||||
assert (results[0] == np.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,))).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_reduce_invalid_rank(ray_start_distributed_2_nodes, backend, dst_rank=9):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
with pytest.raises(ValueError):
|
||||
_ = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
+131
@@ -0,0 +1,131 @@
|
||||
"""Test the collective reducescatter API on a distributed Ray cluster."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import (
|
||||
create_collective_workers,
|
||||
init_tensors_for_gather_scatter,
|
||||
)
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("tensor_backend", ["numpy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_reducescatter_different_array_size(
|
||||
ray_start_distributed_2_nodes, array_size, tensor_backend, backend
|
||||
):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if tensor_backend == "numpy":
|
||||
assert (
|
||||
results[i] == np.ones(array_size, dtype=np.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i] == torch.ones(array_size, dtype=torch.float32) * world_size
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dtype", [np.uint8, np.float16, np.float32, np.float64])
|
||||
def test_reducescatter_different_dtype(ray_start_distributed_2_nodes, dtype, backend):
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(actors, dtype=dtype)
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i] == np.ones(10, dtype=dtype) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_reducescatter_torch_numpy(ray_start_distributed_2_nodes, backend):
|
||||
world_size = 8
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
# tensor is pytorch, list is numpy
|
||||
for i, a in enumerate(actors):
|
||||
t = torch.ones(shape, dtype=torch.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [np.ones(shape, dtype=np.float32) for _ in range(world_size)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (results[i] == torch.ones(shape, dtype=torch.float32) * world_size).all()
|
||||
|
||||
# tensor is numpy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [
|
||||
torch.ones(shape, dtype=torch.float32) for _ in range(world_size)
|
||||
]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (results[i] == np.ones(shape, dtype=np.float32) * world_size).all()
|
||||
|
||||
# some tensors in the list are pytorch, some are numpy
|
||||
for i, a in enumerate(actors):
|
||||
if i % 2 == 0:
|
||||
t = torch.ones(shape, dtype=torch.float32) * (i + 1)
|
||||
else:
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32))
|
||||
else:
|
||||
list_buffer.append(np.ones(shape, dtype=np.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if i % 2 == 0:
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == np.ones(shape, dtype=np.float32) * world_size).all()
|
||||
|
||||
# mixed case
|
||||
for i, a in enumerate(actors):
|
||||
if i % 2 == 0:
|
||||
t = torch.ones(shape, dtype=torch.float32) * (i + 1)
|
||||
else:
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(np.ones(shape, dtype=np.float32))
|
||||
else:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if i % 2 == 0:
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == np.ones(shape, dtype=np.float32) * world_size).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,52 @@
|
||||
"""Test the send/recv API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import create_collective_workers
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1, 3, 6])
|
||||
@pytest.mark.parametrize("src_rank", [0, 2, 4, 7])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2**10, 2**15, 2**20, [2, 2], [5, 9, 10, 85]]
|
||||
)
|
||||
def test_sendrecv(
|
||||
ray_start_distributed_2_nodes, group_name, array_size, src_rank, dst_rank, backend
|
||||
):
|
||||
if src_rank == dst_rank:
|
||||
return
|
||||
world_size = 8
|
||||
actors, _ = create_collective_workers(
|
||||
num_workers=world_size, group_name=group_name, backend=backend
|
||||
)
|
||||
ray.get(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(array_size, dtype=np.float32) * (i + 1))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
refs = []
|
||||
for i in range(world_size):
|
||||
refs.append(actors[i].get_buffer.remote())
|
||||
refs[src_rank] = actors[src_rank].do_send.remote(group_name, dst_rank)
|
||||
refs[dst_rank] = actors[dst_rank].do_recv.remote(group_name, src_rank)
|
||||
results = ray.get(refs)
|
||||
assert (
|
||||
results[src_rank] == np.ones(array_size, dtype=np.float32) * (src_rank + 1)
|
||||
).all()
|
||||
assert (
|
||||
results[dst_rank] == np.ones(array_size, dtype=np.float32) * (src_rank + 1)
|
||||
).all()
|
||||
ray.get([a.destroy_group.remote(group_name) for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Test the allgather API on a distributed Ray cluster."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import (
|
||||
create_collective_workers,
|
||||
init_tensors_for_gather_scatter,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tensor_backend", ["cupy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_allgather_different_array_size(
|
||||
ray_start_distributed_2_nodes_4_gpus, array_size, tensor_backend
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
if tensor_backend == "cupy":
|
||||
assert (
|
||||
results[i][j] == cp.ones(array_size, dtype=cp.float32) * (j + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j]
|
||||
== torch.ones(array_size, dtype=torch.float32).cuda() * (j + 1)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [cp.uint8, cp.float16, cp.float32, cp.float64])
|
||||
def test_allgather_different_dtype(ray_start_distributed_2_nodes_4_gpus, dtype):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(actors, dtype=dtype)
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i][j] == cp.ones(10, dtype=dtype) * (j + 1)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("length", [0, 1, 3, 4, 7, 8])
|
||||
def test_unmatched_tensor_list_length(ray_start_distributed_2_nodes_4_gpus, length):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
list_buffer = [cp.ones(10, dtype=cp.float32) for _ in range(length)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer) for a in actors])
|
||||
if length != world_size:
|
||||
with pytest.raises(RuntimeError):
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
else:
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [10, 20, [4, 5], [1, 3, 5, 7]])
|
||||
def test_unmatched_tensor_shape(ray_start_distributed_2_nodes_4_gpus, shape):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(actors, array_size=10)
|
||||
list_buffer = [cp.ones(shape, dtype=cp.float32) for _ in range(world_size)]
|
||||
ray.get([a.set_list_buffer.remote(list_buffer) for a in actors])
|
||||
if shape != 10:
|
||||
with pytest.raises(RuntimeError):
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
else:
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
|
||||
|
||||
def test_allgather_torch_cupy(ray_start_distributed_2_nodes_4_gpus):
|
||||
world_size = 4
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
# tensor is pytorch, list is cupy
|
||||
for i, a in enumerate(actors):
|
||||
t = torch.ones(shape, dtype=torch.float32).cuda() * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [cp.ones(shape, dtype=cp.float32) for _ in range(world_size)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i][j] == cp.ones(shape, dtype=cp.float32) * (j + 1)).all()
|
||||
|
||||
# tensor is cupy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [
|
||||
torch.ones(shape, dtype=torch.float32).cuda() for _ in range(world_size)
|
||||
]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (
|
||||
results[i][j] == torch.ones(shape, dtype=torch.float32).cuda() * (j + 1)
|
||||
).all()
|
||||
|
||||
# some tensors in the list are pytorch, some are cupy
|
||||
for i, a in enumerate(actors):
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32).cuda())
|
||||
else:
|
||||
list_buffer.append(cp.ones(shape, dtype=cp.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
assert (
|
||||
results[i][j]
|
||||
== torch.ones(shape, dtype=torch.float32).cuda() * (j + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j] == cp.ones(shape, dtype=cp.float32) * (j + 1)
|
||||
).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Test the collective allreduice API on a distributed Ray cluster."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_workers
|
||||
from ray.util.collective.types import ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("world_size", [2, 3, 4])
|
||||
def test_allreduce_different_name(
|
||||
ray_start_distributed_2_nodes_4_gpus, group_name, world_size
|
||||
):
|
||||
actors, _ = create_collective_workers(num_workers=world_size, group_name=group_name)
|
||||
results = ray.get([a.do_allreduce.remote(group_name) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
def test_allreduce_different_array_size(
|
||||
ray_start_distributed_2_nodes_4_gpus, array_size
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[a.set_buffer.remote(cp.ones(array_size, dtype=cp.float32)) for a in actors]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((array_size,), dtype=cp.float32) * world_size).all()
|
||||
assert (results[1] == cp.ones((array_size,), dtype=cp.float32) * world_size).all()
|
||||
|
||||
|
||||
def test_allreduce_destroy(
|
||||
ray_start_distributed_2_nodes_4_gpus, backend="nccl", group_name="default"
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
|
||||
# destroy the group and try do work, should fail
|
||||
ray.get([a.destroy_group.remote() for a in actors])
|
||||
with pytest.raises(RuntimeError):
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
|
||||
# reinit the same group and all reduce
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (
|
||||
results[0] == cp.ones((10,), dtype=cp.float32) * world_size * world_size
|
||||
).all()
|
||||
assert (
|
||||
results[1] == cp.ones((10,), dtype=cp.float32) * world_size * world_size
|
||||
).all()
|
||||
|
||||
|
||||
def test_allreduce_multiple_group(
|
||||
ray_start_distributed_2_nodes_4_gpus, backend="nccl", num_groups=5
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
for group_name in range(1, num_groups):
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, str(group_name))
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for i in range(num_groups):
|
||||
group_name = "default" if i == 0 else str(i)
|
||||
results = ray.get([a.do_allreduce.remote(group_name) for a in actors])
|
||||
assert (
|
||||
results[0] == cp.ones((10,), dtype=cp.float32) * (world_size ** (i + 1))
|
||||
).all()
|
||||
|
||||
|
||||
def test_allreduce_different_op(ray_start_distributed_2_nodes_4_gpus):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
# check product
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.PRODUCT) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 120).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 120).all()
|
||||
|
||||
# check min
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.MIN) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
|
||||
# check max
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.MAX) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 5).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 5).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [cp.uint8, cp.float16, cp.float32, cp.float64])
|
||||
def test_allreduce_different_dtype(ray_start_distributed_2_nodes_4_gpus, dtype):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait([a.set_buffer.remote(cp.ones(10, dtype=dtype)) for a in actors])
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=dtype) * world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=dtype) * world_size).all()
|
||||
|
||||
|
||||
def test_allreduce_torch_cupy(ray_start_distributed_2_nodes_4_gpus):
|
||||
# import torch
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
).cuda()
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,)) * world_size).all()
|
||||
|
||||
ray.wait(
|
||||
[
|
||||
actors[0].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
cp.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
with pytest.raises(RuntimeError):
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
"""Test the collective group APIs."""
|
||||
from random import shuffle
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import Worker, create_collective_workers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("world_size", [2, 3, 4])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
def test_init_two_actors(ray_start_distributed_2_nodes_4_gpus, world_size, group_name):
|
||||
actors, results = create_collective_workers(world_size, group_name)
|
||||
for i in range(world_size):
|
||||
assert results[i]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("world_size", [2, 3, 4])
|
||||
def test_init_multiple_groups(ray_start_distributed_2_nodes_4_gpus, world_size):
|
||||
num_groups = 1
|
||||
actors = [Worker.remote() for _ in range(world_size)]
|
||||
for i in range(num_groups):
|
||||
group_name = str(i)
|
||||
init_results = ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, k, group_name=group_name)
|
||||
for k, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for j in range(world_size):
|
||||
assert init_results[j]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("world_size", [2, 3, 4])
|
||||
def test_get_rank(ray_start_distributed_2_nodes_4_gpus, world_size):
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote())
|
||||
assert actor0_rank == 0
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote())
|
||||
assert actor1_rank == 1
|
||||
|
||||
# create a second group with a different name, and different
|
||||
# orders of ranks.
|
||||
new_group_name = "default2"
|
||||
ranks = list(range(world_size))
|
||||
shuffle(ranks)
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, ranks[i], group_name=new_group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote(new_group_name))
|
||||
assert actor0_rank == ranks[0]
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote(new_group_name))
|
||||
assert actor1_rank == ranks[1]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("world_size", [2, 3, 4])
|
||||
def test_get_collective_group_size(ray_start_distributed_2_nodes_4_gpus, world_size):
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
actor0_world_size = ray.get(actors[0].report_world_size.remote())
|
||||
actor1_world_size = ray.get(actors[1].report_world_size.remote())
|
||||
assert actor0_world_size == actor1_world_size == world_size
|
||||
|
||||
|
||||
def test_is_group_initialized(ray_start_distributed_2_nodes_4_gpus):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
# check group is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("random"))
|
||||
assert not actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("123"))
|
||||
assert not actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote("456"))
|
||||
assert not actor1_is_init
|
||||
|
||||
|
||||
def test_destroy_group(ray_start_distributed_2_nodes_4_gpus):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
# Now destroy the group at actor0
|
||||
ray.wait([actors[0].destroy_group.remote()])
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert not actor0_is_init
|
||||
|
||||
# should go well as the group `random` does not exist at all
|
||||
ray.wait([actors[0].destroy_group.remote("random")])
|
||||
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("random")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("default")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert not actor1_is_init
|
||||
for i in [2, 3]:
|
||||
ray.wait([actors[i].destroy_group.remote("default")])
|
||||
|
||||
# Now reconstruct the group using the same name
|
||||
init_results = ray.get(
|
||||
[actor.init_group.remote(world_size, i) for i, actor in enumerate(actors)]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert init_results[i]
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,81 @@
|
||||
"""Test the broadcast API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_workers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1, 2, 3])
|
||||
def test_broadcast_different_name(
|
||||
ray_start_distributed_2_nodes_4_gpus, group_name, src_rank
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(num_workers=world_size, group_name=group_name)
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones((10,), dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_broadcast.remote(group_name=group_name, src_rank=src_rank)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * (src_rank + 2)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1, 2, 3])
|
||||
def test_broadcast_different_array_size(
|
||||
ray_start_distributed_2_nodes_4_gpus, array_size, src_rank
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(array_size, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (
|
||||
results[i] == cp.ones((array_size,), dtype=cp.float32) * (src_rank + 2)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
def test_broadcast_torch_cupy(ray_start_distributed_2_nodes_4_gpus, src_rank):
|
||||
import torch
|
||||
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
).cuda()
|
||||
* world_size
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
if src_rank == 0:
|
||||
assert (results[0] == cp.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda()).all()
|
||||
else:
|
||||
assert (results[0] == cp.ones((10,)) * world_size).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda() * world_size).all()
|
||||
|
||||
|
||||
def test_broadcast_invalid_rank(ray_start_distributed_2_nodes_4_gpus, src_rank=3):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
with pytest.raises(ValueError):
|
||||
ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
@@ -0,0 +1,127 @@
|
||||
"""Test the reduce API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_workers
|
||||
from ray.util.collective.types import ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1, 2, 3])
|
||||
def test_reduce_different_name(
|
||||
ray_start_distributed_2_nodes_4_gpus, group_name, dst_rank
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(num_workers=world_size, group_name=group_name)
|
||||
results = ray.get([a.do_reduce.remote(group_name, dst_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1, 2, 3])
|
||||
def test_reduce_different_array_size(
|
||||
ray_start_distributed_2_nodes_4_gpus, array_size, dst_rank
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[a.set_buffer.remote(cp.ones(array_size, dtype=cp.float32)) for a in actors]
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (
|
||||
results[i] == cp.ones((array_size,), dtype=cp.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((array_size,), dtype=cp.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1, 2, 3])
|
||||
def test_reduce_different_op(ray_start_distributed_2_nodes_4_gpus, dst_rank):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
# check product
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.PRODUCT) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * 120).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * (i + 2)).all()
|
||||
|
||||
# check min
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.MIN) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * (i + 2)).all()
|
||||
|
||||
# check max
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.MAX) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * 5).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * (i + 2)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_torch_cupy(ray_start_distributed_2_nodes_4_gpus, dst_rank):
|
||||
import torch
|
||||
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
).cuda()
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
if dst_rank == 0:
|
||||
assert (results[0] == cp.ones((10,)) * world_size).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda()).all()
|
||||
else:
|
||||
assert (results[0] == cp.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda() * world_size).all()
|
||||
|
||||
|
||||
def test_reduce_invalid_rank(ray_start_distributed_2_nodes_4_gpus, dst_rank=7):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
with pytest.raises(ValueError):
|
||||
ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
"""Test the collective reducescatter API on a distributed Ray cluster."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import (
|
||||
create_collective_workers,
|
||||
init_tensors_for_gather_scatter,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tensor_backend", ["cupy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_reducescatter_different_array_size(
|
||||
ray_start_distributed_2_nodes_4_gpus, array_size, tensor_backend
|
||||
):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if tensor_backend == "cupy":
|
||||
assert (
|
||||
results[i] == cp.ones(array_size, dtype=cp.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i]
|
||||
== torch.ones(array_size, dtype=torch.float32).cuda() * world_size
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [cp.uint8, cp.float16, cp.float32, cp.float64])
|
||||
def test_reducescatter_different_dtype(ray_start_distributed_2_nodes_4_gpus, dtype):
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(actors, dtype=dtype)
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i] == cp.ones(10, dtype=dtype) * world_size).all()
|
||||
|
||||
|
||||
def test_reducescatter_torch_cupy(ray_start_distributed_2_nodes_4_gpus):
|
||||
world_size = 4
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
# tensor is pytorch, list is cupy
|
||||
for i, a in enumerate(actors):
|
||||
t = torch.ones(shape, dtype=torch.float32).cuda() * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [cp.ones(shape, dtype=cp.float32) for _ in range(world_size)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32).cuda() * world_size
|
||||
).all()
|
||||
|
||||
# tensor is cupy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [
|
||||
torch.ones(shape, dtype=torch.float32).cuda() for _ in range(world_size)
|
||||
]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (results[i] == cp.ones(shape, dtype=cp.float32) * world_size).all()
|
||||
|
||||
# some tensors in the list are pytorch, some are cupy
|
||||
for i, a in enumerate(actors):
|
||||
if i % 2 == 0:
|
||||
t = torch.ones(shape, dtype=torch.float32).cuda() * (i + 1)
|
||||
else:
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32).cuda())
|
||||
else:
|
||||
list_buffer.append(cp.ones(shape, dtype=cp.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if i % 2 == 0:
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32).cuda() * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones(shape, dtype=cp.float32) * world_size).all()
|
||||
|
||||
# mixed case
|
||||
for i, a in enumerate(actors):
|
||||
if i % 2 == 0:
|
||||
t = torch.ones(shape, dtype=torch.float32).cuda() * (i + 1)
|
||||
else:
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(cp.ones(shape, dtype=cp.float32))
|
||||
else:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32).cuda())
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if i % 2 == 0:
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32).cuda() * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones(shape, dtype=cp.float32) * world_size).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Test the send/recv API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_workers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1, 2, 3])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1, 2, 3])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2**10, 2**15, 2**20, [2, 2], [5, 9, 10, 85]]
|
||||
)
|
||||
def test_sendrecv(
|
||||
ray_start_distributed_2_nodes_4_gpus, group_name, array_size, src_rank, dst_rank
|
||||
):
|
||||
if src_rank == dst_rank:
|
||||
return
|
||||
world_size = 4
|
||||
actors, _ = create_collective_workers(num_workers=world_size, group_name=group_name)
|
||||
ray.get(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(array_size, dtype=cp.float32) * (i + 1))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
refs = []
|
||||
for i in range(world_size):
|
||||
refs.append(actors[i].get_buffer.remote())
|
||||
refs[src_rank] = actors[src_rank].do_send.remote(group_name, dst_rank)
|
||||
refs[dst_rank] = actors[dst_rank].do_recv.remote(group_name, src_rank)
|
||||
results = ray.get(refs)
|
||||
assert (
|
||||
results[src_rank] == cp.ones(array_size, dtype=cp.float32) * (src_rank + 1)
|
||||
).all()
|
||||
assert (
|
||||
results[dst_rank] == cp.ones(array_size, dtype=cp.float32) * (src_rank + 1)
|
||||
).all()
|
||||
ray.get([a.destroy_group.remote(group_name) for a in actors])
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
"""Test the allgather API on a distributed Ray cluster."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import (
|
||||
create_collective_multigpu_workers,
|
||||
init_tensors_for_gather_scatter_multigpu,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tensor_backend", ["cupy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_allgather_different_array_size(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, array_size, tensor_backend
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
init_tensors_for_gather_scatter_multigpu(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_allgather_multigpu.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
for k in range(actual_world_size):
|
||||
if tensor_backend == "cupy":
|
||||
assert (
|
||||
results[i][j][k] == cp.ones(array_size, dtype=cp.float32)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j][k]
|
||||
== torch.ones(array_size, dtype=torch.float32).cuda(j)
|
||||
).all()
|
||||
|
||||
|
||||
def test_allgather_torch_cupy(ray_start_distributed_multigpu_2_nodes_4_gpus):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
|
||||
# tensor is pytorch, list is cupy
|
||||
for i, a in enumerate(actors):
|
||||
ray.get(
|
||||
[a.set_buffer.remote(shape, tensor_type0="torch", tensor_type1="torch")]
|
||||
)
|
||||
ray.get(
|
||||
[a.set_list_buffer.remote(shape, tensor_type0="cupy", tensor_type1="cupy")]
|
||||
)
|
||||
results = ray.get([a.do_allgather_multigpu.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
for k in range(actual_world_size):
|
||||
assert (results[i][j][k] == cp.ones(shape, dtype=cp.float32)).all()
|
||||
|
||||
# tensor is cupy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
ray.get([a.set_buffer.remote(shape, tensor_type0="cupy", tensor_type1="cupy")])
|
||||
ray.get(
|
||||
[
|
||||
a.set_list_buffer.remote(
|
||||
shape, tensor_type0="torch", tensor_type1="torch"
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allgather_multigpu.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
for k in range(actual_world_size):
|
||||
assert (
|
||||
results[i][j][k] == torch.ones(shape, dtype=torch.float32).cuda(j)
|
||||
).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
+166
@@ -0,0 +1,166 @@
|
||||
"""Test the collective allreduice API on a distributed Ray cluster."""
|
||||
import logging
|
||||
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_multigpu_workers
|
||||
from ray.util.collective.types import ReduceOp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel("DEBUG")
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
def test_allreduce_multigpu_different_name(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, group_name
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(
|
||||
num_workers=world_size, group_name=group_name
|
||||
)
|
||||
results = ray.get([a.do_allreduce_multigpu.remote(group_name) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * actual_world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * actual_world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
def test_allreduce_multigpu_different_array_size(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, array_size
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
ray.get([a.set_buffer.remote(array_size) for a in actors])
|
||||
results = ray.get([a.do_allreduce_multigpu.remote() for a in actors])
|
||||
assert (
|
||||
results[0] == cp.ones((array_size,), dtype=cp.float32) * actual_world_size
|
||||
).all()
|
||||
assert (
|
||||
results[1] == cp.ones((array_size,), dtype=cp.float32) * actual_world_size
|
||||
).all()
|
||||
|
||||
|
||||
def test_allreduce_multigpu_destroy(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, backend="nccl", group_name="default"
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
|
||||
results = ray.get([a.do_allreduce_multigpu.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * actual_world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * actual_world_size).all()
|
||||
|
||||
# destroy the group and try do work, should fail
|
||||
ray.get([a.destroy_group.remote() for a in actors])
|
||||
with pytest.raises(RuntimeError):
|
||||
results = ray.get([a.do_allreduce_multigpu.remote() for a in actors])
|
||||
|
||||
# reinit the same group and all reduce
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce_multigpu.remote() for a in actors])
|
||||
assert (
|
||||
results[0]
|
||||
== cp.ones((10,), dtype=cp.float32) * actual_world_size * actual_world_size
|
||||
).all()
|
||||
assert (
|
||||
results[1]
|
||||
== cp.ones((10,), dtype=cp.float32) * actual_world_size * actual_world_size
|
||||
).all()
|
||||
|
||||
|
||||
def test_allreduce_multigpu_multiple_group(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, backend="nccl", num_groups=5
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
for group_name in range(1, num_groups):
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, str(group_name))
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for i in range(num_groups):
|
||||
group_name = "default" if i == 0 else str(i)
|
||||
results = ray.get([a.do_allreduce_multigpu.remote(group_name) for a in actors])
|
||||
assert (
|
||||
results[0]
|
||||
== cp.ones((10,), dtype=cp.float32) * (actual_world_size ** (i + 1))
|
||||
).all()
|
||||
|
||||
|
||||
def test_allreduce_multigpu_different_op(ray_start_distributed_multigpu_2_nodes_4_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
# check product
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote([10], value0=4, value1=5))
|
||||
results = ray.get(
|
||||
[a.do_allreduce_multigpu.remote(op=ReduceOp.PRODUCT) for a in actors]
|
||||
)
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 120).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 120).all()
|
||||
|
||||
# check min
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote([10], value0=4, value1=5))
|
||||
results = ray.get([a.do_allreduce_multigpu.remote(op=ReduceOp.MIN) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
|
||||
# check max
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote([10], value0=4, value1=5))
|
||||
results = ray.get([a.do_allreduce_multigpu.remote(op=ReduceOp.MAX) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 5).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 5).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [cp.uint8, cp.float16, cp.float32, cp.float64])
|
||||
def test_allreduce_multigpu_different_dtype(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, dtype
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
ray.get([a.set_buffer.remote([10], dtype=dtype) for a in actors])
|
||||
results = ray.get([a.do_allreduce_multigpu.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=dtype) * actual_world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=dtype) * actual_world_size).all()
|
||||
|
||||
|
||||
def test_allreduce_torch_cupy(ray_start_distributed_multigpu_2_nodes_4_gpus):
|
||||
# import torch
|
||||
world_size = 2
|
||||
actual_world_size = 4
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
ray.get(actors[0].set_buffer.remote([10]))
|
||||
ray.get(
|
||||
actors[1].set_buffer.remote([10], tensor_type0="torch", tensor_type1="torch")
|
||||
)
|
||||
results = ray.get([a.do_allreduce_multigpu.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,)) * actual_world_size).all()
|
||||
|
||||
ray.get(
|
||||
actors[0].set_buffer.remote([10], tensor_type0="cupy", tensor_type1="torch")
|
||||
)
|
||||
ray.get(
|
||||
actors[1].set_buffer.remote([10], tensor_type0="torch", tensor_type1="cupy")
|
||||
)
|
||||
results = ray.get([a.do_allreduce_multigpu.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,)) * actual_world_size).all()
|
||||
+103
@@ -0,0 +1,103 @@
|
||||
"""Test the collective group APIs."""
|
||||
from random import shuffle
|
||||
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_multigpu_workers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
def test_init_two_actors(ray_start_distributed_multigpu_2_nodes_4_gpus, group_name):
|
||||
world_size = 2
|
||||
actors, results = create_collective_multigpu_workers(world_size, group_name)
|
||||
for i in range(world_size):
|
||||
assert results[i]
|
||||
|
||||
|
||||
def test_report_num_gpus(ray_start_distributed_multigpu_2_nodes_4_gpus):
|
||||
world_size = 2
|
||||
actors, results = create_collective_multigpu_workers(world_size)
|
||||
num_gpus = ray.get([actor.report_num_gpus.remote() for actor in actors])
|
||||
assert num_gpus == [2, 2]
|
||||
|
||||
|
||||
def test_get_rank(ray_start_distributed_multigpu_2_nodes_4_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote())
|
||||
assert actor0_rank == 0
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote())
|
||||
assert actor1_rank == 1
|
||||
|
||||
# create a second group with a different name, and different
|
||||
# orders of ranks.
|
||||
new_group_name = "default2"
|
||||
ranks = list(range(world_size))
|
||||
shuffle(ranks)
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, ranks[i], group_name=new_group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote(new_group_name))
|
||||
assert actor0_rank == ranks[0]
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote(new_group_name))
|
||||
assert actor1_rank == ranks[1]
|
||||
|
||||
|
||||
def test_is_group_initialized(ray_start_distributed_multigpu_2_nodes_4_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
# check group is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("random"))
|
||||
assert not actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("123"))
|
||||
assert not actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote("456"))
|
||||
assert not actor1_is_init
|
||||
|
||||
|
||||
def test_destroy_group(ray_start_distributed_multigpu_2_nodes_4_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
# Now destroy the group at actor0
|
||||
ray.wait([actors[0].destroy_group.remote()])
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert not actor0_is_init
|
||||
|
||||
# should go well as the group `random` does not exist at all
|
||||
ray.wait([actors[0].destroy_group.remote("random")])
|
||||
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("random")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("default")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert not actor1_is_init
|
||||
|
||||
# Now reconstruct the group using the same name
|
||||
init_results = ray.get(
|
||||
[actor.init_group.remote(world_size, i) for i, actor in enumerate(actors)]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert init_results[i]
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
"""Test the broadcast API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_multigpu_workers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
@pytest.mark.parametrize("src_gpu_index", [0, 1])
|
||||
def test_broadcast_different_name(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, group_name, src_rank, src_gpu_index
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actors, _ = create_collective_multigpu_workers(
|
||||
num_workers=world_size, group_name=group_name
|
||||
)
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote([10], value0=4, value1=5))
|
||||
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_broadcast_multigpu.remote(
|
||||
group_name=group_name, src_rank=src_rank, src_gpu_index=src_gpu_index
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
val = (src_rank + 1) * 2 + src_gpu_index
|
||||
assert (results[i][j] == cp.ones([10], dtype=cp.float32) * val).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
@pytest.mark.parametrize("src_gpu_index", [0, 1])
|
||||
def test_broadcast_different_array_size(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, array_size, src_rank, src_gpu_index
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
ray.get(actors[0].set_buffer.remote([array_size], value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote([array_size], value0=4, value1=5))
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_broadcast_multigpu.remote(
|
||||
src_rank=src_rank, src_gpu_index=src_gpu_index
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
val = (src_rank + 1) * 2 + src_gpu_index
|
||||
assert (
|
||||
results[i][j] == cp.ones((array_size,), dtype=cp.float32) * val
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
@pytest.mark.parametrize("src_gpu_index", [0, 1])
|
||||
def test_broadcast_torch_cupy(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, src_rank, src_gpu_index
|
||||
):
|
||||
import torch
|
||||
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(
|
||||
actors[1].set_buffer.remote(
|
||||
[10], value0=4, value1=5, tensor_type0="torch", tensor_type1="torch"
|
||||
)
|
||||
)
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_broadcast_multigpu.remote(
|
||||
src_rank=src_rank, src_gpu_index=src_gpu_index
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
val = (src_rank + 1) * 2 + src_gpu_index
|
||||
if i == 0:
|
||||
assert (results[i][j] == cp.ones([10], dtype=cp.float32) * val).all()
|
||||
else:
|
||||
assert (results[i][j] == torch.ones([10]).cuda(j) * val).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("src_rank", [3, 4])
|
||||
@pytest.mark.parametrize("src_gpu_index", [2, 3])
|
||||
def test_broadcast_invalid_rank(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, src_rank, src_gpu_index
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
with pytest.raises(ValueError):
|
||||
_ = ray.get(
|
||||
[
|
||||
a.do_broadcast_multigpu.remote(
|
||||
src_rank=src_rank, src_gpu_index=src_gpu_index
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
"""Test the reduce API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_multigpu_workers
|
||||
from ray.util.collective.types import ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
@pytest.mark.parametrize("dst_gpu_index", [0, 1])
|
||||
def test_reduce_different_name(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, group_name, dst_rank, dst_gpu_index
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(
|
||||
num_workers=world_size, group_name=group_name
|
||||
)
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_reduce_multigpu.remote(
|
||||
group_name, dst_rank=dst_rank, dst_gpu_index=dst_gpu_index
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
if i == dst_rank and j == dst_gpu_index:
|
||||
assert (
|
||||
results[i][j]
|
||||
== cp.ones((10,), dtype=cp.float32) * actual_world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i][j] == cp.ones((10,), dtype=cp.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
@pytest.mark.parametrize("dst_gpu_index", [0, 1])
|
||||
def test_reduce_different_array_size(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, array_size, dst_rank, dst_gpu_index
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(num_workers=world_size)
|
||||
|
||||
ray.get(actors[0].set_buffer.remote(array_size))
|
||||
ray.get(actors[1].set_buffer.remote(array_size))
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_reduce_multigpu.remote(dst_rank=dst_rank, dst_gpu_index=dst_gpu_index)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
if i == dst_rank and j == dst_gpu_index:
|
||||
assert (
|
||||
results[i][j]
|
||||
== cp.ones((array_size,), dtype=cp.float32) * actual_world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i][j] == cp.ones((array_size,), dtype=cp.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
@pytest.mark.parametrize("dst_gpu_index", [0, 1])
|
||||
def test_reduce_different_op(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, dst_rank, dst_gpu_index
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
|
||||
# check product
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote([10], value0=4, value1=5))
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_reduce_multigpu.remote(
|
||||
dst_rank=dst_rank, dst_gpu_index=dst_gpu_index, op=ReduceOp.PRODUCT
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
if i == dst_rank and j == dst_gpu_index:
|
||||
assert (results[i][j] == cp.ones((10,), dtype=cp.float32) * 120).all()
|
||||
else:
|
||||
val = (i + 1) * 2 + j
|
||||
assert (results[i][j] == cp.ones((10,), dtype=cp.float32) * val).all()
|
||||
|
||||
# check min
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote([10], value0=4, value1=5))
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_reduce_multigpu.remote(
|
||||
dst_rank=dst_rank, dst_gpu_index=dst_gpu_index, op=ReduceOp.MIN
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
if i == dst_rank and j == dst_gpu_index:
|
||||
assert (results[i][j] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
else:
|
||||
val = (i + 1) * 2 + j
|
||||
assert (results[i][j] == cp.ones((10,), dtype=cp.float32) * val).all()
|
||||
|
||||
# check max
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote([10], value0=4, value1=5))
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_reduce_multigpu.remote(
|
||||
dst_rank=dst_rank, dst_gpu_index=dst_gpu_index, op=ReduceOp.MAX
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
if i == dst_rank and j == dst_gpu_index:
|
||||
assert (results[i][j] == cp.ones((10,), dtype=cp.float32) * 5).all()
|
||||
else:
|
||||
val = (i + 1) * 2 + j
|
||||
assert (results[i][j] == cp.ones((10,), dtype=cp.float32) * val).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
@pytest.mark.parametrize("dst_gpu_index", [0, 1])
|
||||
def test_reduce_torch_cupy(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, dst_rank, dst_gpu_index
|
||||
):
|
||||
import torch
|
||||
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
ray.get(actors[0].set_buffer.remote([10], value0=2, value1=3))
|
||||
ray.get(
|
||||
actors[1].set_buffer.remote(
|
||||
[10], value0=4, value1=5, tensor_type0="torch", tensor_type1="torch"
|
||||
)
|
||||
)
|
||||
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_reduce_multigpu.remote(dst_rank=dst_rank, dst_gpu_index=dst_gpu_index)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
val = (i + 1) * 2 + j
|
||||
if dst_rank == i and dst_gpu_index == j:
|
||||
if i == 0:
|
||||
assert (results[i][j] == cp.ones([10], dtype=cp.float32) * 14).all()
|
||||
else:
|
||||
assert (results[i][j] == torch.ones([10]).cuda(j) * 14).all()
|
||||
else:
|
||||
if i == 0:
|
||||
assert (
|
||||
results[i][j] == cp.ones([10], dtype=cp.float32) * val
|
||||
).all()
|
||||
else:
|
||||
assert (results[i][j] == torch.ones([10]).cuda(j) * val).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [3, 4])
|
||||
@pytest.mark.parametrize("dst_gpu_index", [2, 3])
|
||||
def test_reduce_invalid_rank(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, dst_rank, dst_gpu_index
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
with pytest.raises(ValueError):
|
||||
_ = ray.get(
|
||||
[
|
||||
a.do_reduce_multigpu.remote(
|
||||
dst_rank=dst_rank, dst_gpu_index=dst_gpu_index
|
||||
)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
"""Test the collective reducescatter API on a distributed Ray cluster."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import (
|
||||
create_collective_multigpu_workers,
|
||||
init_tensors_for_gather_scatter_multigpu,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tensor_backend", ["cupy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_reducescatter_different_array_size(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus, array_size, tensor_backend
|
||||
):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
|
||||
init_tensors_for_gather_scatter_multigpu(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_reducescatter_multigpu.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
if tensor_backend == "cupy":
|
||||
assert (
|
||||
results[i][j]
|
||||
== cp.ones(array_size, dtype=cp.float32) * actual_world_size
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j]
|
||||
== torch.ones(array_size, dtype=torch.float32).cuda(j)
|
||||
* actual_world_size
|
||||
).all()
|
||||
|
||||
|
||||
def test_reducescatter_torch_cupy(ray_start_distributed_multigpu_2_nodes_4_gpus):
|
||||
world_size = 2
|
||||
num_gpu_per_worker = 2
|
||||
actual_world_size = world_size * num_gpu_per_worker
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_multigpu_workers(world_size)
|
||||
|
||||
# tensor is pytorch, list is cupy
|
||||
for i, a in enumerate(actors):
|
||||
ray.get(
|
||||
[a.set_buffer.remote(shape, tensor_type0="torch", tensor_type1="torch")]
|
||||
)
|
||||
ray.get(
|
||||
[a.set_list_buffer.remote(shape, tensor_type0="cupy", tensor_type1="cupy")]
|
||||
)
|
||||
results = ray.get([a.do_reducescatter_multigpu.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
assert (
|
||||
results[i][j]
|
||||
== torch.ones(shape, dtype=torch.float32).cuda(j) * actual_world_size
|
||||
).all()
|
||||
|
||||
# tensor is cupy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
ray.get([a.set_buffer.remote(shape, tensor_type0="cupy", tensor_type1="cupy")])
|
||||
ray.get(
|
||||
[
|
||||
a.set_list_buffer.remote(
|
||||
shape, tensor_type0="torch", tensor_type1="torch"
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_reducescatter_multigpu.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(num_gpu_per_worker):
|
||||
assert (
|
||||
results[i][j] == cp.ones(shape, dtype=cp.float32) * actual_world_size
|
||||
).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
"""Test the send/recv API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_multigpu_workers
|
||||
|
||||
|
||||
# @pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
@pytest.mark.parametrize("dst_gpu_index", [0, 1])
|
||||
@pytest.mark.parametrize("src_gpu_index", [0, 1])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2**10, 2**15, 2**20, [2, 2], [5, 9, 10, 85]]
|
||||
)
|
||||
def test_sendrecv(
|
||||
ray_start_distributed_multigpu_2_nodes_4_gpus,
|
||||
array_size,
|
||||
src_rank,
|
||||
dst_rank,
|
||||
src_gpu_index,
|
||||
dst_gpu_index,
|
||||
):
|
||||
if src_rank == dst_rank:
|
||||
return
|
||||
world_size = 2
|
||||
actors, _ = create_collective_multigpu_workers(num_workers=world_size)
|
||||
|
||||
ray.get(actors[0].set_buffer.remote(array_size, value0=2, value1=3))
|
||||
ray.get(actors[1].set_buffer.remote(array_size, value0=4, value1=5))
|
||||
|
||||
refs = []
|
||||
for i in range(world_size):
|
||||
refs.append(actors[i].get_buffer.remote())
|
||||
refs[src_rank][src_gpu_index] = actors[src_rank].do_send_multigpu.remote(
|
||||
dst_rank=dst_rank, dst_gpu_index=dst_gpu_index, src_gpu_index=src_gpu_index
|
||||
)
|
||||
refs[dst_rank][dst_gpu_index] = actors[dst_rank].do_recv_multigpu.remote(
|
||||
src_rank=src_rank, src_gpu_index=src_gpu_index, dst_gpu_index=dst_gpu_index
|
||||
)
|
||||
results = []
|
||||
results_flattend = ray.get(refs[0] + refs[1])
|
||||
results.append([results_flattend[0], results_flattend[1]])
|
||||
results.append([results_flattend[2], results_flattend[3]])
|
||||
assert (
|
||||
results[src_rank][src_gpu_index]
|
||||
== cp.ones(array_size, dtype=cp.float32) * ((src_rank + 1) * 2 + src_gpu_index)
|
||||
).all()
|
||||
assert (
|
||||
results[dst_rank][dst_gpu_index]
|
||||
== cp.ones(array_size, dtype=cp.float32) * ((src_rank + 1) * 2 + src_gpu_index)
|
||||
).all()
|
||||
ray.get([a.destroy_group.remote() for a in actors])
|
||||
@@ -0,0 +1,143 @@
|
||||
"""Test the collective allgather API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import (
|
||||
create_collective_workers,
|
||||
init_tensors_for_gather_scatter,
|
||||
)
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("tensor_backend", ["numpy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_allgather_different_array_size(
|
||||
ray_start_single_node, array_size, tensor_backend, backend
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
if tensor_backend == "numpy":
|
||||
assert (
|
||||
results[i][j] == np.ones(array_size, dtype=np.float32) * (j + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j]
|
||||
== torch.ones(array_size, dtype=torch.float32) * (j + 1)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dtype", [np.uint8, np.float16, np.float32, np.float64])
|
||||
def test_allgather_different_dtype(ray_start_single_node, dtype, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(actors, dtype=dtype)
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i][j] == np.ones(10, dtype=dtype) * (j + 1)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("length", [0, 1, 2, 3])
|
||||
def test_unmatched_tensor_list_length(ray_start_single_node, length, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
list_buffer = [np.ones(10, dtype=np.float32) for _ in range(length)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer, copy=True) for a in actors])
|
||||
if length != world_size:
|
||||
with pytest.raises(RuntimeError):
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
else:
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("shape", [10, 20, [4, 5], [1, 3, 5, 7]])
|
||||
def test_unmatched_tensor_shape(ray_start_single_node, shape, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(actors, array_size=10)
|
||||
list_buffer = [np.ones(shape, dtype=np.float32) for _ in range(world_size)]
|
||||
ray.get([a.set_list_buffer.remote(list_buffer, copy=True) for a in actors])
|
||||
if shape != 10:
|
||||
with pytest.raises(RuntimeError):
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
else:
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allgather_torch_numpy(ray_start_single_node, backend):
|
||||
world_size = 2
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
# tensor is pytorch, list is numpy
|
||||
for i, a in enumerate(actors):
|
||||
t = torch.ones(shape, dtype=torch.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [np.ones(shape, dtype=np.float32) for _ in range(world_size)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer, copy=True)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i][j] == np.ones(shape, dtype=np.float32) * (j + 1)).all()
|
||||
|
||||
# tensor is numpy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [
|
||||
torch.ones(shape, dtype=torch.float32) for _ in range(world_size)
|
||||
]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer, copy=True)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (
|
||||
results[i][j] == torch.ones(shape, dtype=torch.float32) * (j + 1)
|
||||
).all()
|
||||
|
||||
# some tensors in the list are pytorch, some are numpy
|
||||
for i, a in enumerate(actors):
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32))
|
||||
else:
|
||||
list_buffer.append(np.ones(shape, dtype=np.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer, copy=True)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
assert (
|
||||
results[i][j] == torch.ones(shape, dtype=torch.float32) * (j + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j] == np.ones(shape, dtype=np.float32) * (j + 1)
|
||||
).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,166 @@
|
||||
"""Test the collective allreduice API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import create_collective_workers
|
||||
from ray.util.collective.types import Backend, ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
def test_allreduce_different_name(ray_start_single_node, group_name, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(
|
||||
num_workers=world_size, group_name=group_name, backend=backend
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(group_name) for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
def test_allreduce_different_array_size(ray_start_single_node, array_size, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[a.set_buffer.remote(np.ones(array_size, dtype=np.float32)) for a in actors]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((array_size,), dtype=np.float32) * world_size).all()
|
||||
assert (results[1] == np.ones((array_size,), dtype=np.float32) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allreduce_destroy(ray_start_single_node, backend, group_name="default"):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
|
||||
# destroy the group and try do work, should fail
|
||||
ray.get([a.destroy_group.remote() for a in actors])
|
||||
with pytest.raises(RuntimeError):
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
|
||||
# reinit the same group and all reduce
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * world_size * 2).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * world_size * 2).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allreduce_multiple_group(ray_start_single_node, backend, num_groups=5):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
for group_name in range(1, num_groups):
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, str(group_name))
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for i in range(num_groups):
|
||||
group_name = "default" if i == 0 else str(i)
|
||||
results = ray.get([a.do_allreduce.remote(group_name) for a in actors])
|
||||
assert (
|
||||
results[0] == np.ones((10,), dtype=np.float32) * (world_size ** (i + 1))
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allreduce_different_op(ray_start_single_node, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
# check product
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.PRODUCT) for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * 6).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * 6).all()
|
||||
|
||||
# check min
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.MIN) for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * 2).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * 2).all()
|
||||
|
||||
# check max
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.MAX) for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=np.float32) * 3).all()
|
||||
assert (results[1] == np.ones((10,), dtype=np.float32) * 3).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dtype", [np.uint8, np.float16, np.float32, np.float64])
|
||||
def test_allreduce_different_dtype(ray_start_single_node, dtype, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait([a.set_buffer.remote(np.ones(10, dtype=dtype)) for a in actors])
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((10,), dtype=dtype) * world_size).all()
|
||||
assert (results[1] == np.ones((10,), dtype=dtype) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_allreduce_torch_numpy(ray_start_single_node, backend):
|
||||
# import torch
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == np.ones((10,)) * world_size).all()
|
||||
|
||||
ray.wait(
|
||||
[
|
||||
actors[0].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
ray.wait([actors[1].set_buffer.remote(np.ones(10, dtype=np.float32))])
|
||||
ray.get([a.do_allreduce.remote() for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Test the collective group APIs."""
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import Worker, create_collective_workers
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
def test_init_two_actors(ray_start_single_node, group_name, backend):
|
||||
world_size = 2
|
||||
actors, results = create_collective_workers(world_size, group_name, backend=backend)
|
||||
for i in range(world_size):
|
||||
assert results[i]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_init_multiple_groups(ray_start_single_node, backend):
|
||||
world_size = 2
|
||||
num_groups = 10
|
||||
actors = [Worker.remote() for i in range(world_size)]
|
||||
for i in range(num_groups):
|
||||
group_name = str(i)
|
||||
init_results = ray.get(
|
||||
[
|
||||
actor.init_group.remote(
|
||||
world_size, k, group_name=group_name, backend=backend
|
||||
)
|
||||
for k, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for j in range(world_size):
|
||||
assert init_results[j]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_get_rank(ray_start_single_node, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote())
|
||||
assert actor0_rank == 0
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote())
|
||||
assert actor1_rank == 1
|
||||
|
||||
# create a second group with a different name,
|
||||
# and different order of ranks.
|
||||
new_group_name = "default2"
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(
|
||||
world_size,
|
||||
world_size - 1 - i,
|
||||
group_name=new_group_name,
|
||||
backend=backend,
|
||||
)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote(new_group_name))
|
||||
assert actor0_rank == 1
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote(new_group_name))
|
||||
assert actor1_rank == 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_get_world_size(ray_start_single_node, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
actor0_world_size = ray.get(actors[0].report_world_size.remote())
|
||||
actor1_world_size = ray.get(actors[1].report_world_size.remote())
|
||||
assert actor0_world_size == actor1_world_size == world_size
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_is_group_initialized(ray_start_single_node, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
# check group is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("random"))
|
||||
assert not actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("123"))
|
||||
assert not actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote("456"))
|
||||
assert not actor1_is_init
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_destroy_group(ray_start_single_node, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
# Now destroy the group at actor0
|
||||
ray.wait([actors[0].destroy_group.remote()])
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert not actor0_is_init
|
||||
|
||||
# should go well as the group `random` does not exist at all
|
||||
ray.wait([actors[0].destroy_group.remote("random")])
|
||||
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("random")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("default")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert not actor1_is_init
|
||||
|
||||
# Now reconstruct the group using the same name
|
||||
init_results = ray.get(
|
||||
[actor.init_group.remote(world_size, i) for i, actor in enumerate(actors)]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert init_results[i]
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,94 @@
|
||||
"""Test the broadcast API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import create_collective_workers
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
def test_broadcast_different_name(ray_start_single_node, group_name, src_rank, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(
|
||||
num_workers=world_size, group_name=group_name, backend=backend
|
||||
)
|
||||
ray.get(
|
||||
[
|
||||
a.set_buffer.remote(np.ones((10,), dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_broadcast.remote(group_name=group_name, src_rank=src_rank)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * (src_rank + 2)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
def test_broadcast_different_array_size(
|
||||
ray_start_single_node, array_size, src_rank, backend
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.get(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(array_size, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (
|
||||
results[i] == np.ones((array_size,), dtype=np.float32) * (src_rank + 2)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
def test_broadcast_torch_numpy(ray_start_single_node, src_rank, backend):
|
||||
import torch
|
||||
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
* world_size
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
if src_rank == 0:
|
||||
assert (results[0] == np.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,))).all()
|
||||
else:
|
||||
assert (results[0] == np.ones((10,)) * world_size).all()
|
||||
assert (results[1] == torch.ones((10,)) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_broadcast_invalid_rank(ray_start_single_node, backend, src_rank=3):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
with pytest.raises(ValueError):
|
||||
ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,67 @@
|
||||
import time
|
||||
|
||||
import ray
|
||||
import ray.util.collective as col
|
||||
from ray.util.collective.collective_group.torch_gloo_collective_group import (
|
||||
TorchGLOOGroup as GLOOGroup,
|
||||
)
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@ray.remote
|
||||
class Worker:
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
def init_gloo_group(
|
||||
self, world_size: int, rank: int, group_name: str, gloo_timeout: int = 30000
|
||||
):
|
||||
col.init_collective_group(
|
||||
world_size, rank, Backend.GLOO, group_name, gloo_timeout
|
||||
)
|
||||
return True
|
||||
|
||||
def get_gloo_timeout(self, group_name: str) -> int:
|
||||
g = col.get_group_handle(group_name)
|
||||
# Check if the group is initialized correctly
|
||||
assert isinstance(g, GLOOGroup)
|
||||
return g._gloo_context.getTimeout()
|
||||
|
||||
|
||||
def test_two_groups_in_one_cluster(ray_start_single_node):
|
||||
name1 = "name_1"
|
||||
name2 = "name_2"
|
||||
time1 = 40000
|
||||
time2 = 60000
|
||||
w1 = Worker.remote()
|
||||
ret1 = w1.init_gloo_group.remote(1, 0, name1, time1)
|
||||
w2 = Worker.remote()
|
||||
ret2 = w2.init_gloo_group.remote(1, 0, name2, time2)
|
||||
assert ray.get(ret1)
|
||||
assert ray.get(ret2)
|
||||
assert ray.get(w1.get_gloo_timeout.remote(name1)) == time1
|
||||
assert ray.get(w2.get_gloo_timeout.remote(name2)) == time2
|
||||
|
||||
|
||||
def test_failure_when_initializing(shutdown_only):
|
||||
# job1
|
||||
ray.init()
|
||||
w1 = Worker.remote()
|
||||
ret1 = w1.init_gloo_group.remote(2, 0, "name_1")
|
||||
ray.wait([ret1], timeout=1)
|
||||
time.sleep(5)
|
||||
ray.shutdown()
|
||||
|
||||
# job2
|
||||
ray.init()
|
||||
w2 = Worker.remote()
|
||||
ret2 = w2.init_gloo_group.remote(1, 0, "name_1")
|
||||
assert ray.get(ret2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Test the reduce API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import create_collective_workers
|
||||
from ray.util.collective.types import Backend, ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_different_name(ray_start_single_node, group_name, dst_rank, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(
|
||||
num_workers=world_size, group_name=group_name, backend=backend
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(group_name, dst_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * world_size).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_different_array_size(
|
||||
ray_start_single_node, array_size, dst_rank, backend
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[a.set_buffer.remote(np.ones(array_size, dtype=np.float32)) for a in actors]
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (
|
||||
results[i] == np.ones((array_size,), dtype=np.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((array_size,), dtype=np.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_multiple_group(ray_start_single_node, dst_rank, backend, num_groups=5):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
for group_name in range(1, num_groups):
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, str(group_name))
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for i in range(num_groups):
|
||||
group_name = "default" if i == 0 else str(i)
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_reduce.remote(dst_rank=dst_rank, group_name=group_name)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for j in range(world_size):
|
||||
if j == dst_rank:
|
||||
assert (results[j] == np.ones((10,), dtype=np.float32) * (i + 2)).all()
|
||||
else:
|
||||
assert (results[j] == np.ones((10,), dtype=np.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_different_op(ray_start_single_node, dst_rank, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
# check product
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.PRODUCT) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * 6).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * (i + 2)).all()
|
||||
|
||||
# check min
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.MIN) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * 2).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * (i + 2)).all()
|
||||
|
||||
# check max
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(10, dtype=np.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.MAX) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * 3).all()
|
||||
else:
|
||||
assert (results[i] == np.ones((10,), dtype=np.float32) * (i + 2)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_torch_numpy(ray_start_single_node, dst_rank, backend):
|
||||
import torch
|
||||
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
if dst_rank == 0:
|
||||
assert (results[0] == np.ones((10,)) * world_size).all()
|
||||
assert (results[1] == torch.ones((10,))).all()
|
||||
else:
|
||||
assert (results[0] == np.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,)) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_reduce_invalid_rank(ray_start_single_node, backend, dst_rank=3):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
with pytest.raises(ValueError):
|
||||
ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Test the collective reducescatter API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import (
|
||||
create_collective_workers,
|
||||
init_tensors_for_gather_scatter,
|
||||
)
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("tensor_backend", ["numpy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_reducescatter_different_array_size(
|
||||
ray_start_single_node, array_size, tensor_backend, backend
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if tensor_backend == "numpy":
|
||||
assert (
|
||||
results[i] == np.ones(array_size, dtype=np.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i] == torch.ones(array_size, dtype=torch.float32) * world_size
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dtype", [np.uint8, np.float16, np.float32, np.float64])
|
||||
def test_reducescatter_different_dtype(ray_start_single_node, dtype, backend):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
init_tensors_for_gather_scatter(actors, dtype=dtype)
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i] == np.ones(10, dtype=dtype) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_reducescatter_torch_numpy(ray_start_single_node, backend):
|
||||
world_size = 2
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
|
||||
# tensor is pytorch, list is numpy
|
||||
for i, a in enumerate(actors):
|
||||
t = torch.ones(shape, dtype=torch.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [np.ones(shape, dtype=np.float32) for _ in range(world_size)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (results[i] == torch.ones(shape, dtype=torch.float32) * world_size).all()
|
||||
|
||||
# tensor is numpy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [
|
||||
torch.ones(shape, dtype=torch.float32) for _ in range(world_size)
|
||||
]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (results[i] == np.ones(shape, dtype=np.float32) * world_size).all()
|
||||
|
||||
# some tensors in the list are pytorch, some are numpy
|
||||
for i, a in enumerate(actors):
|
||||
if i % 2 == 0:
|
||||
t = torch.ones(shape, dtype=torch.float32) * (i + 1)
|
||||
else:
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32))
|
||||
else:
|
||||
list_buffer.append(np.ones(shape, dtype=np.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if i % 2 == 0:
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == np.ones(shape, dtype=np.float32) * world_size).all()
|
||||
|
||||
# mixed case
|
||||
for i, a in enumerate(actors):
|
||||
if i % 2 == 0:
|
||||
t = torch.ones(shape, dtype=torch.float32) * (i + 1)
|
||||
else:
|
||||
t = np.ones(shape, dtype=np.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(np.ones(shape, dtype=np.float32))
|
||||
else:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if i % 2 == 0:
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == np.ones(shape, dtype=np.float32) * world_size).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,92 @@
|
||||
"""Test the send/recv API."""
|
||||
import numpy as np
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.cpu_util import create_collective_workers
|
||||
from ray.util.collective.types import Backend
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 9, 10, 85]]
|
||||
)
|
||||
def test_reduce_different_name(
|
||||
ray_start_single_node, group_name, array_size, dst_rank, backend
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(
|
||||
num_workers=world_size, group_name=group_name, backend=backend
|
||||
)
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(np.ones(array_size, dtype=np.float32) * (i + 1))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
src_rank = 1 - dst_rank
|
||||
refs = []
|
||||
for i, actor in enumerate(actors):
|
||||
if i != dst_rank:
|
||||
ref = actor.do_send.remote(group_name, dst_rank)
|
||||
else:
|
||||
ref = actor.do_recv.remote(group_name, src_rank)
|
||||
refs.append(ref)
|
||||
results = ray.get(refs)
|
||||
for i in range(world_size):
|
||||
assert (
|
||||
results[i] == np.ones(array_size, dtype=np.float32) * (src_rank + 1)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_sendrecv_torch_numpy(ray_start_single_node, dst_rank, backend):
|
||||
import torch
|
||||
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
* 2
|
||||
)
|
||||
]
|
||||
)
|
||||
src_rank = 1 - dst_rank
|
||||
|
||||
refs = []
|
||||
for i, actor in enumerate(actors):
|
||||
if i != dst_rank:
|
||||
ref = actor.do_send.remote(dst_rank=dst_rank)
|
||||
else:
|
||||
ref = actor.do_recv.remote(src_rank=src_rank)
|
||||
refs.append(ref)
|
||||
results = ray.get(refs)
|
||||
if dst_rank == 0:
|
||||
assert (results[0] == np.ones((10,)) * 2).all()
|
||||
assert (results[1] == torch.ones((10,)) * 2).all()
|
||||
else:
|
||||
assert (results[0] == np.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,))).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("backend", [Backend.GLOO])
|
||||
def test_sendrecv_invalid_rank(ray_start_single_node, backend, dst_rank=3):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size, backend=backend)
|
||||
with pytest.raises(ValueError):
|
||||
ray.get([a.do_send.remote(dst_rank=dst_rank) for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,138 @@
|
||||
"""Test the collective allgather API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import (
|
||||
create_collective_workers,
|
||||
init_tensors_for_gather_scatter,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tensor_backend", ["cupy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_allgather_different_array_size(
|
||||
ray_start_single_node_2_gpus, array_size, tensor_backend
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
if tensor_backend == "cupy":
|
||||
assert (
|
||||
results[i][j] == cp.ones(array_size, dtype=cp.float32) * (j + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j]
|
||||
== torch.ones(array_size, dtype=torch.float32).cuda() * (j + 1)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [cp.uint8, cp.float16, cp.float32, cp.float64])
|
||||
def test_allgather_different_dtype(ray_start_single_node_2_gpus, dtype):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(actors, dtype=dtype)
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i][j] == cp.ones(10, dtype=dtype) * (j + 1)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("length", [0, 1, 2, 3])
|
||||
def test_unmatched_tensor_list_length(ray_start_single_node_2_gpus, length):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
list_buffer = [cp.ones(10, dtype=cp.float32) for _ in range(length)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer) for a in actors])
|
||||
if length != world_size:
|
||||
with pytest.raises(RuntimeError):
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
else:
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
|
||||
|
||||
@pytest.mark.parametrize("shape", [10, 20, [4, 5], [1, 3, 5, 7]])
|
||||
def test_unmatched_tensor_shape(ray_start_single_node_2_gpus, shape):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(actors, array_size=10)
|
||||
list_buffer = [cp.ones(shape, dtype=cp.float32) for _ in range(world_size)]
|
||||
ray.get([a.set_list_buffer.remote(list_buffer) for a in actors])
|
||||
if shape != 10:
|
||||
with pytest.raises(RuntimeError):
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
else:
|
||||
ray.get([a.do_allgather.remote() for a in actors])
|
||||
|
||||
|
||||
def test_allgather_torch_cupy(ray_start_single_node_2_gpus):
|
||||
world_size = 2
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
# tensor is pytorch, list is cupy
|
||||
for i, a in enumerate(actors):
|
||||
t = torch.ones(shape, dtype=torch.float32).cuda() * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [cp.ones(shape, dtype=cp.float32) for _ in range(world_size)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i][j] == cp.ones(shape, dtype=cp.float32) * (j + 1)).all()
|
||||
|
||||
# tensor is cupy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [
|
||||
torch.ones(shape, dtype=torch.float32).cuda() for _ in range(world_size)
|
||||
]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (
|
||||
results[i][j] == torch.ones(shape, dtype=torch.float32).cuda() * (j + 1)
|
||||
).all()
|
||||
|
||||
# some tensors in the list are pytorch, some are cupy
|
||||
for i, a in enumerate(actors):
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32).cuda())
|
||||
else:
|
||||
list_buffer.append(cp.ones(shape, dtype=cp.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_allgather.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
assert (
|
||||
results[i][j]
|
||||
== torch.ones(shape, dtype=torch.float32).cuda() * (j + 1)
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i][j] == cp.ones(shape, dtype=cp.float32) * (j + 1)
|
||||
).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,170 @@
|
||||
"""Test the collective allreduice API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_workers
|
||||
from ray.util.collective.types import ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
def test_allreduce_different_name(ray_start_single_node_2_gpus, group_name):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(num_workers=world_size, group_name=group_name)
|
||||
results = ray.get([a.do_allreduce.remote(group_name) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
def test_allreduce_different_array_size(ray_start_single_node_2_gpus, array_size):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[a.set_buffer.remote(cp.ones(array_size, dtype=cp.float32)) for a in actors]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((array_size,), dtype=cp.float32) * world_size).all()
|
||||
assert (results[1] == cp.ones((array_size,), dtype=cp.float32) * world_size).all()
|
||||
|
||||
|
||||
def test_allreduce_destroy(
|
||||
ray_start_single_node_2_gpus, backend="nccl", group_name="default"
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
|
||||
# destroy the group and try do work, should fail
|
||||
ray.get([a.destroy_group.remote() for a in actors])
|
||||
with pytest.raises(RuntimeError):
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
|
||||
# reinit the same group and all reduce
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * world_size * 2).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * world_size * 2).all()
|
||||
|
||||
|
||||
def test_allreduce_multiple_group(
|
||||
ray_start_single_node_2_gpus, backend="nccl", num_groups=5
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
for group_name in range(1, num_groups):
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, str(group_name))
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for i in range(num_groups):
|
||||
group_name = "default" if i == 0 else str(i)
|
||||
results = ray.get([a.do_allreduce.remote(group_name) for a in actors])
|
||||
assert (
|
||||
results[0] == cp.ones((10,), dtype=cp.float32) * (world_size ** (i + 1))
|
||||
).all()
|
||||
|
||||
|
||||
def test_allreduce_different_op(ray_start_single_node_2_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
# check product
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.PRODUCT) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 6).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 6).all()
|
||||
|
||||
# check min
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.MIN) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
|
||||
# check max
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote(op=ReduceOp.MAX) for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=cp.float32) * 3).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=cp.float32) * 3).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [cp.uint8, cp.float16, cp.float32, cp.float64])
|
||||
def test_allreduce_different_dtype(ray_start_single_node_2_gpus, dtype):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait([a.set_buffer.remote(cp.ones(10, dtype=dtype)) for a in actors])
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,), dtype=dtype) * world_size).all()
|
||||
assert (results[1] == cp.ones((10,), dtype=dtype) * world_size).all()
|
||||
|
||||
|
||||
def test_allreduce_torch_cupy(ray_start_single_node_2_gpus):
|
||||
# import torch
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
).cuda()
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
assert (results[0] == cp.ones((10,)) * world_size).all()
|
||||
|
||||
ray.wait(
|
||||
[
|
||||
actors[0].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
cp.ones(
|
||||
10,
|
||||
)
|
||||
)
|
||||
]
|
||||
)
|
||||
with pytest.raises(RuntimeError):
|
||||
results = ray.get([a.do_allreduce.remote() for a in actors])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,118 @@
|
||||
"""Test the collective group APIs."""
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import Worker, create_collective_workers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
def test_init_two_actors(ray_start_single_node_2_gpus, group_name):
|
||||
world_size = 2
|
||||
actors, results = create_collective_workers(world_size, group_name)
|
||||
for i in range(world_size):
|
||||
assert results[i]
|
||||
|
||||
|
||||
def test_init_multiple_groups(ray_start_single_node_2_gpus):
|
||||
world_size = 2
|
||||
num_groups = 10
|
||||
actors = [Worker.remote() for i in range(world_size)]
|
||||
for i in range(num_groups):
|
||||
group_name = str(i)
|
||||
init_results = ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, k, group_name=group_name)
|
||||
for k, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for j in range(world_size):
|
||||
assert init_results[j]
|
||||
|
||||
|
||||
def test_get_rank(ray_start_single_node_2_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote())
|
||||
assert actor0_rank == 0
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote())
|
||||
assert actor1_rank == 1
|
||||
|
||||
# create a second group with a different name,
|
||||
# and different order of ranks.
|
||||
new_group_name = "default2"
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(
|
||||
world_size, world_size - 1 - i, group_name=new_group_name
|
||||
)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
actor0_rank = ray.get(actors[0].report_rank.remote(new_group_name))
|
||||
assert actor0_rank == 1
|
||||
actor1_rank = ray.get(actors[1].report_rank.remote(new_group_name))
|
||||
assert actor1_rank == 0
|
||||
|
||||
|
||||
def test_get_collective_group_size(ray_start_single_node_2_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
actor0_world_size = ray.get(actors[0].report_world_size.remote())
|
||||
actor1_world_size = ray.get(actors[1].report_world_size.remote())
|
||||
assert actor0_world_size == actor1_world_size == world_size
|
||||
|
||||
|
||||
def test_is_group_initialized(ray_start_single_node_2_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
# check group is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("random"))
|
||||
assert not actor0_is_init
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote("123"))
|
||||
assert not actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote("456"))
|
||||
assert not actor1_is_init
|
||||
|
||||
|
||||
def test_destroy_group(ray_start_single_node_2_gpus):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
# Now destroy the group at actor0
|
||||
ray.wait([actors[0].destroy_group.remote()])
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert not actor0_is_init
|
||||
|
||||
# should go well as the group `random` does not exist at all
|
||||
ray.wait([actors[0].destroy_group.remote("random")])
|
||||
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("random")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
ray.wait([actors[1].destroy_group.remote("default")])
|
||||
actor1_is_init = ray.get(actors[1].report_is_group_initialized.remote())
|
||||
assert not actor1_is_init
|
||||
|
||||
# Now reconstruct the group using the same name
|
||||
init_results = ray.get(
|
||||
[actor.init_group.remote(world_size, i) for i, actor in enumerate(actors)]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert init_results[i]
|
||||
actor0_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor0_is_init
|
||||
actor1_is_init = ray.get(actors[0].report_is_group_initialized.remote())
|
||||
assert actor1_is_init
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Test the broadcast API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_workers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
def test_broadcast_different_name(ray_start_single_node_2_gpus, group_name, src_rank):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(num_workers=world_size, group_name=group_name)
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones((10,), dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_broadcast.remote(group_name=group_name, src_rank=src_rank)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for i in range(world_size):
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * (src_rank + 2)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
def test_broadcast_different_array_size(
|
||||
ray_start_single_node_2_gpus, array_size, src_rank
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(array_size, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (
|
||||
results[i] == cp.ones((array_size,), dtype=cp.float32) * (src_rank + 2)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("src_rank", [0, 1])
|
||||
def test_broadcast_torch_cupy(ray_start_single_node_2_gpus, src_rank):
|
||||
import torch
|
||||
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
).cuda()
|
||||
* world_size
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
if src_rank == 0:
|
||||
assert (results[0] == cp.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda()).all()
|
||||
else:
|
||||
assert (results[0] == cp.ones((10,)) * world_size).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda() * world_size).all()
|
||||
|
||||
|
||||
def test_broadcast_invalid_rank(ray_start_single_node_2_gpus, src_rank=3):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
with pytest.raises(ValueError):
|
||||
ray.get([a.do_broadcast.remote(src_rank=src_rank) for a in actors])
|
||||
@@ -0,0 +1,151 @@
|
||||
"""Test the reduce API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_workers
|
||||
from ray.util.collective.types import ReduceOp
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_different_name(ray_start_single_node_2_gpus, group_name, dst_rank):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(num_workers=world_size, group_name=group_name)
|
||||
results = ray.get([a.do_reduce.remote(group_name, dst_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * world_size).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("array_size", [2, 2**5, 2**10, 2**15, 2**20])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_different_array_size(
|
||||
ray_start_single_node_2_gpus, array_size, dst_rank
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[a.set_buffer.remote(cp.ones(array_size, dtype=cp.float32)) for a in actors]
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (
|
||||
results[i] == cp.ones((array_size,), dtype=cp.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((array_size,), dtype=cp.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_multiple_group(ray_start_single_node_2_gpus, dst_rank, num_groups=5):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
for group_name in range(1, num_groups):
|
||||
ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, "nccl", str(group_name))
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
for i in range(num_groups):
|
||||
group_name = "default" if i == 0 else str(i)
|
||||
results = ray.get(
|
||||
[
|
||||
a.do_reduce.remote(dst_rank=dst_rank, group_name=group_name)
|
||||
for a in actors
|
||||
]
|
||||
)
|
||||
for j in range(world_size):
|
||||
if j == dst_rank:
|
||||
assert (results[j] == cp.ones((10,), dtype=cp.float32) * (i + 2)).all()
|
||||
else:
|
||||
assert (results[j] == cp.ones((10,), dtype=cp.float32)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_different_op(ray_start_single_node_2_gpus, dst_rank):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
# check product
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.PRODUCT) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * 6).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * (i + 2)).all()
|
||||
|
||||
# check min
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.MIN) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * 2).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * (i + 2)).all()
|
||||
|
||||
# check max
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(10, dtype=cp.float32) * (i + 2))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
results = ray.get(
|
||||
[a.do_reduce.remote(dst_rank=dst_rank, op=ReduceOp.MAX) for a in actors]
|
||||
)
|
||||
for i in range(world_size):
|
||||
if i == dst_rank:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * 3).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones((10,), dtype=cp.float32) * (i + 2)).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_reduce_torch_cupy(ray_start_single_node_2_gpus, dst_rank):
|
||||
import torch
|
||||
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
).cuda()
|
||||
)
|
||||
]
|
||||
)
|
||||
results = ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
if dst_rank == 0:
|
||||
assert (results[0] == cp.ones((10,)) * world_size).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda()).all()
|
||||
else:
|
||||
assert (results[0] == cp.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda() * world_size).all()
|
||||
|
||||
|
||||
def test_reduce_invalid_rank(ray_start_single_node_2_gpus, dst_rank=3):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
with pytest.raises(ValueError):
|
||||
ray.get([a.do_reduce.remote(dst_rank=dst_rank) for a in actors])
|
||||
@@ -0,0 +1,130 @@
|
||||
"""Test the collective reducescatter API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import (
|
||||
create_collective_workers,
|
||||
init_tensors_for_gather_scatter,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tensor_backend", ["cupy", "torch"])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 5, 5]]
|
||||
)
|
||||
def test_reducescatter_different_array_size(
|
||||
ray_start_single_node_2_gpus, array_size, tensor_backend
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(
|
||||
actors, array_size=array_size, tensor_backend=tensor_backend
|
||||
)
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if tensor_backend == "cupy":
|
||||
assert (
|
||||
results[i] == cp.ones(array_size, dtype=cp.float32) * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (
|
||||
results[i]
|
||||
== torch.ones(array_size, dtype=torch.float32).cuda() * world_size
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dtype", [cp.uint8, cp.float16, cp.float32, cp.float64])
|
||||
def test_reducescatter_different_dtype(ray_start_single_node_2_gpus, dtype):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
init_tensors_for_gather_scatter(actors, dtype=dtype)
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
for j in range(world_size):
|
||||
assert (results[i] == cp.ones(10, dtype=dtype) * world_size).all()
|
||||
|
||||
|
||||
def test_reducescatter_torch_cupy(ray_start_single_node_2_gpus):
|
||||
world_size = 2
|
||||
shape = [10, 10]
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
|
||||
# tensor is pytorch, list is cupy
|
||||
for i, a in enumerate(actors):
|
||||
t = torch.ones(shape, dtype=torch.float32).cuda() * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [cp.ones(shape, dtype=cp.float32) for _ in range(world_size)]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32).cuda() * world_size
|
||||
).all()
|
||||
|
||||
# tensor is cupy, list is pytorch
|
||||
for i, a in enumerate(actors):
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = [
|
||||
torch.ones(shape, dtype=torch.float32).cuda() for _ in range(world_size)
|
||||
]
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
assert (results[i] == cp.ones(shape, dtype=cp.float32) * world_size).all()
|
||||
|
||||
# some tensors in the list are pytorch, some are cupy
|
||||
for i, a in enumerate(actors):
|
||||
if i % 2 == 0:
|
||||
t = torch.ones(shape, dtype=torch.float32).cuda() * (i + 1)
|
||||
else:
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32).cuda())
|
||||
else:
|
||||
list_buffer.append(cp.ones(shape, dtype=cp.float32))
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if i % 2 == 0:
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32).cuda() * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones(shape, dtype=cp.float32) * world_size).all()
|
||||
|
||||
# mixed case
|
||||
for i, a in enumerate(actors):
|
||||
if i % 2 == 0:
|
||||
t = torch.ones(shape, dtype=torch.float32).cuda() * (i + 1)
|
||||
else:
|
||||
t = cp.ones(shape, dtype=cp.float32) * (i + 1)
|
||||
ray.wait([a.set_buffer.remote(t)])
|
||||
list_buffer = []
|
||||
for j in range(world_size):
|
||||
if j % 2 == 0:
|
||||
list_buffer.append(cp.ones(shape, dtype=cp.float32))
|
||||
else:
|
||||
list_buffer.append(torch.ones(shape, dtype=torch.float32).cuda())
|
||||
ray.wait([a.set_list_buffer.remote(list_buffer)])
|
||||
results = ray.get([a.do_reducescatter.remote() for a in actors])
|
||||
for i in range(world_size):
|
||||
if i % 2 == 0:
|
||||
assert (
|
||||
results[i] == torch.ones(shape, dtype=torch.float32).cuda() * world_size
|
||||
).all()
|
||||
else:
|
||||
assert (results[i] == cp.ones(shape, dtype=cp.float32) * world_size).all()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
|
||||
sys.exit(pytest.main(["-v", "-x", __file__]))
|
||||
@@ -0,0 +1,78 @@
|
||||
"""Test the send/recv API."""
|
||||
import cupy as cp
|
||||
import pytest
|
||||
|
||||
import ray
|
||||
from ray.util.collective.tests.util import create_collective_workers
|
||||
|
||||
|
||||
@pytest.mark.parametrize("group_name", ["default", "test", "123?34!"])
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
@pytest.mark.parametrize(
|
||||
"array_size", [2, 2**5, 2**10, 2**15, 2**20, [2, 2], [5, 9, 10, 85]]
|
||||
)
|
||||
def test_reduce_different_name(
|
||||
ray_start_single_node_2_gpus, group_name, array_size, dst_rank
|
||||
):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(num_workers=world_size, group_name=group_name)
|
||||
ray.wait(
|
||||
[
|
||||
a.set_buffer.remote(cp.ones(array_size, dtype=cp.float32) * (i + 1))
|
||||
for i, a in enumerate(actors)
|
||||
]
|
||||
)
|
||||
src_rank = 1 - dst_rank
|
||||
refs = []
|
||||
for i, actor in enumerate(actors):
|
||||
if i != dst_rank:
|
||||
ref = actor.do_send.remote(group_name, dst_rank)
|
||||
else:
|
||||
ref = actor.do_recv.remote(group_name, src_rank)
|
||||
refs.append(ref)
|
||||
results = ray.get(refs)
|
||||
for i in range(world_size):
|
||||
assert (
|
||||
results[i] == cp.ones(array_size, dtype=cp.float32) * (src_rank + 1)
|
||||
).all()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("dst_rank", [0, 1])
|
||||
def test_sendrecv_torch_cupy(ray_start_single_node_2_gpus, dst_rank):
|
||||
import torch
|
||||
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
ray.wait(
|
||||
[
|
||||
actors[1].set_buffer.remote(
|
||||
torch.ones(
|
||||
10,
|
||||
).cuda()
|
||||
* 2
|
||||
)
|
||||
]
|
||||
)
|
||||
src_rank = 1 - dst_rank
|
||||
|
||||
refs = []
|
||||
for i, actor in enumerate(actors):
|
||||
if i != dst_rank:
|
||||
ref = actor.do_send.remote(dst_rank=dst_rank)
|
||||
else:
|
||||
ref = actor.do_recv.remote(src_rank=src_rank)
|
||||
refs.append(ref)
|
||||
results = ray.get(refs)
|
||||
if dst_rank == 0:
|
||||
assert (results[0] == cp.ones((10,)) * 2).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda() * 2).all()
|
||||
else:
|
||||
assert (results[0] == cp.ones((10,))).all()
|
||||
assert (results[1] == torch.ones((10,)).cuda()).all()
|
||||
|
||||
|
||||
def test_sendrecv_invalid_rank(ray_start_single_node_2_gpus, dst_rank=3):
|
||||
world_size = 2
|
||||
actors, _ = create_collective_workers(world_size)
|
||||
with pytest.raises(ValueError):
|
||||
ray.get([a.do_send.remote(dst_rank=dst_rank) for a in actors])
|
||||
@@ -0,0 +1,373 @@
|
||||
import logging
|
||||
|
||||
import cupy as cp
|
||||
import torch
|
||||
|
||||
import ray
|
||||
import ray.util.collective as col
|
||||
from ray.util.collective.collective_group.nccl_util import get_num_gpus
|
||||
from ray.util.collective.types import Backend, ReduceOp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ray.remote(num_gpus=1)
|
||||
class Worker:
|
||||
def __init__(self):
|
||||
self.buffer = None
|
||||
self.list_buffer = None
|
||||
|
||||
def init_tensors(self):
|
||||
self.buffer = cp.ones((10,), dtype=cp.float32)
|
||||
self.list_buffer = [cp.ones((10,), dtype=cp.float32) for _ in range(2)]
|
||||
cp.cuda.Stream.null.synchronize()
|
||||
return True
|
||||
|
||||
def init_group(self, world_size, rank, backend=Backend.NCCL, group_name="default"):
|
||||
col.init_collective_group(world_size, rank, backend, group_name)
|
||||
return True
|
||||
|
||||
def set_buffer(self, data):
|
||||
self.buffer = data
|
||||
return self.buffer
|
||||
|
||||
def get_buffer(self):
|
||||
return self.buffer
|
||||
|
||||
def set_list_buffer(self, list_of_arrays):
|
||||
self.list_buffer = list_of_arrays
|
||||
return self.list_buffer
|
||||
|
||||
def do_allreduce(self, group_name="default", op=ReduceOp.SUM):
|
||||
col.allreduce(self.buffer, group_name, op)
|
||||
return self.buffer
|
||||
|
||||
def do_reduce(self, group_name="default", dst_rank=0, op=ReduceOp.SUM):
|
||||
col.reduce(self.buffer, dst_rank, group_name, op)
|
||||
return self.buffer
|
||||
|
||||
def do_broadcast(self, group_name="default", src_rank=0):
|
||||
col.broadcast(self.buffer, src_rank, group_name)
|
||||
return self.buffer
|
||||
|
||||
def do_allgather(self, group_name="default"):
|
||||
col.allgather(self.list_buffer, self.buffer, group_name)
|
||||
return self.list_buffer
|
||||
|
||||
def do_reducescatter(self, group_name="default", op=ReduceOp.SUM):
|
||||
col.reducescatter(self.buffer, self.list_buffer, group_name, op)
|
||||
return self.buffer
|
||||
|
||||
def do_send(self, group_name="default", dst_rank=0):
|
||||
col.send(self.buffer, dst_rank, group_name)
|
||||
return self.buffer
|
||||
|
||||
def do_recv(self, group_name="default", src_rank=0):
|
||||
col.recv(self.buffer, src_rank, group_name)
|
||||
return self.buffer
|
||||
|
||||
def destroy_group(self, group_name="default"):
|
||||
col.destroy_collective_group(group_name)
|
||||
return True
|
||||
|
||||
def report_rank(self, group_name="default"):
|
||||
rank = col.get_rank(group_name)
|
||||
return rank
|
||||
|
||||
def report_world_size(self, group_name="default"):
|
||||
ws = col.get_collective_group_size(group_name)
|
||||
return ws
|
||||
|
||||
def report_nccl_availability(self):
|
||||
avail = col.nccl_available()
|
||||
return avail
|
||||
|
||||
def report_gloo_availability(self):
|
||||
avail = col.gloo_available()
|
||||
return avail
|
||||
|
||||
def report_is_group_initialized(self, group_name="default"):
|
||||
is_init = col.is_group_initialized(group_name)
|
||||
return is_init
|
||||
|
||||
|
||||
def create_collective_workers(num_workers=2, group_name="default", backend="nccl"):
|
||||
actors = [None] * num_workers
|
||||
for i in range(num_workers):
|
||||
actor = Worker.remote()
|
||||
ray.get([actor.init_tensors.remote()])
|
||||
actors[i] = actor
|
||||
world_size = num_workers
|
||||
init_results = ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
return actors, init_results
|
||||
|
||||
|
||||
def init_tensors_for_gather_scatter(
|
||||
actors, array_size=10, dtype=cp.float32, tensor_backend="cupy"
|
||||
):
|
||||
world_size = len(actors)
|
||||
for i, a in enumerate(actors):
|
||||
if tensor_backend == "cupy":
|
||||
t = cp.ones(array_size, dtype=dtype) * (i + 1)
|
||||
elif tensor_backend == "torch":
|
||||
t = torch.ones(array_size, dtype=torch.float32).cuda() * (i + 1)
|
||||
else:
|
||||
raise RuntimeError("Unsupported tensor backend.")
|
||||
ray.get([a.set_buffer.remote(t)])
|
||||
if tensor_backend == "cupy":
|
||||
list_buffer = [cp.ones(array_size, dtype=dtype) for _ in range(world_size)]
|
||||
elif tensor_backend == "torch":
|
||||
list_buffer = [
|
||||
torch.ones(array_size, dtype=torch.float32).cuda()
|
||||
for _ in range(world_size)
|
||||
]
|
||||
else:
|
||||
raise RuntimeError("Unsupported tensor backend.")
|
||||
ray.get([a.set_list_buffer.remote(list_buffer) for a in actors])
|
||||
|
||||
|
||||
@ray.remote(num_gpus=2)
|
||||
class MultiGPUWorker:
|
||||
def __init__(self):
|
||||
self.buffer0 = None
|
||||
self.buffer1 = None
|
||||
self.list_buffer0 = None
|
||||
self.list_buffer1 = None
|
||||
|
||||
def __del__(self):
|
||||
self.buffer0 = None
|
||||
self.buffer1 = None
|
||||
self.list_buffer0 = None
|
||||
self.list_buffer1 = None
|
||||
|
||||
def init_tensors(self):
|
||||
with cp.cuda.Device(0):
|
||||
self.buffer0 = cp.ones((10,), dtype=cp.float32)
|
||||
self.list_buffer0 = [cp.ones((10,), dtype=cp.float32) for _ in range(4)]
|
||||
with cp.cuda.Device(1):
|
||||
self.buffer1 = cp.ones((10,), dtype=cp.float32)
|
||||
self.list_buffer1 = [cp.ones((10,), dtype=cp.float32) for _ in range(4)]
|
||||
cp.cuda.Stream.null.synchronize()
|
||||
return True
|
||||
|
||||
def init_group(self, world_size, rank, backend=Backend.NCCL, group_name="default"):
|
||||
col.init_collective_group(world_size, rank, backend, group_name)
|
||||
return True
|
||||
|
||||
def set_buffer(
|
||||
self,
|
||||
size,
|
||||
value0=1.0,
|
||||
value1=1.0,
|
||||
dtype=cp.float32,
|
||||
tensor_type0="cupy",
|
||||
tensor_type1="cupy",
|
||||
):
|
||||
if tensor_type0 == "cupy":
|
||||
with cp.cuda.Device(0):
|
||||
self.buffer0 = cp.ones(size, dtype=dtype) * value0
|
||||
elif tensor_type0 == "torch":
|
||||
self.buffer0 = torch.ones(size, dtype=torch.float32).cuda(0) * value0
|
||||
else:
|
||||
raise RuntimeError()
|
||||
|
||||
if tensor_type1 == "cupy":
|
||||
with cp.cuda.Device(1):
|
||||
self.buffer1 = cp.ones(size, dtype=dtype) * value1
|
||||
elif tensor_type1 == "torch":
|
||||
self.buffer1 = torch.ones(size, dtype=torch.float32).cuda(1) * value1
|
||||
else:
|
||||
raise RuntimeError()
|
||||
cp.cuda.Device(0).synchronize()
|
||||
cp.cuda.Device(1).synchronize()
|
||||
# cp.cuda.Stream.null.synchronize()
|
||||
return True
|
||||
|
||||
def set_list_buffer(
|
||||
self,
|
||||
size,
|
||||
value0=1.0,
|
||||
value1=1.0,
|
||||
dtype=cp.float32,
|
||||
tensor_type0="cupy",
|
||||
tensor_type1="cupy",
|
||||
):
|
||||
if tensor_type0 == "cupy":
|
||||
with cp.cuda.Device(0):
|
||||
self.list_buffer0 = [
|
||||
cp.ones(size, dtype=dtype) * value0 for _ in range(4)
|
||||
]
|
||||
elif tensor_type0 == "torch":
|
||||
self.list_buffer0 = [
|
||||
torch.ones(size, dtype=torch.float32).cuda(0) * value0 for _ in range(4)
|
||||
]
|
||||
else:
|
||||
raise RuntimeError()
|
||||
|
||||
if tensor_type1 == "cupy":
|
||||
with cp.cuda.Device(1):
|
||||
self.list_buffer1 = [
|
||||
cp.ones(size, dtype=dtype) * value1 for _ in range(4)
|
||||
]
|
||||
elif tensor_type1 == "torch":
|
||||
self.list_buffer1 = [
|
||||
torch.ones(size, dtype=torch.float32).cuda(1) * value1 for _ in range(4)
|
||||
]
|
||||
else:
|
||||
raise RuntimeError()
|
||||
cp.cuda.Device(0).synchronize()
|
||||
cp.cuda.Device(1).synchronize()
|
||||
return True
|
||||
|
||||
@ray.method(num_returns=2)
|
||||
def get_buffer(self):
|
||||
return self.buffer0, self.buffer1
|
||||
|
||||
def do_allreduce_multigpu(self, group_name="default", op=ReduceOp.SUM):
|
||||
col.allreduce_multigpu([self.buffer0, self.buffer1], group_name, op)
|
||||
cp.cuda.Device(0).synchronize()
|
||||
cp.cuda.Device(1).synchronize()
|
||||
return self.buffer0
|
||||
|
||||
def do_reduce_multigpu(
|
||||
self, group_name="default", dst_rank=0, dst_gpu_index=0, op=ReduceOp.SUM
|
||||
):
|
||||
col.reduce_multigpu(
|
||||
[self.buffer0, self.buffer1], dst_rank, dst_gpu_index, group_name, op
|
||||
)
|
||||
cp.cuda.Device(0).synchronize()
|
||||
cp.cuda.Device(1).synchronize()
|
||||
return self.buffer0, self.buffer1
|
||||
|
||||
def do_broadcast_multigpu(self, group_name="default", src_rank=0, src_gpu_index=0):
|
||||
col.broadcast_multigpu(
|
||||
[self.buffer0, self.buffer1], src_rank, src_gpu_index, group_name
|
||||
)
|
||||
return self.buffer0, self.buffer1
|
||||
|
||||
def do_allgather_multigpu(self, group_name="default"):
|
||||
col.allgather_multigpu(
|
||||
[self.list_buffer0, self.list_buffer1],
|
||||
[self.buffer0, self.buffer1],
|
||||
group_name,
|
||||
)
|
||||
cp.cuda.Device(0).synchronize()
|
||||
cp.cuda.Device(1).synchronize()
|
||||
return self.list_buffer0, self.list_buffer1
|
||||
|
||||
def do_reducescatter_multigpu(self, group_name="default", op=ReduceOp.SUM):
|
||||
col.reducescatter_multigpu(
|
||||
[self.buffer0, self.buffer1],
|
||||
[self.list_buffer0, self.list_buffer1],
|
||||
group_name,
|
||||
op,
|
||||
)
|
||||
cp.cuda.Device(0).synchronize()
|
||||
cp.cuda.Device(1).synchronize()
|
||||
return self.buffer0, self.buffer1
|
||||
|
||||
def do_send_multigpu(
|
||||
self, group_name="default", dst_rank=0, dst_gpu_index=0, src_gpu_index=0
|
||||
):
|
||||
if src_gpu_index == 0:
|
||||
col.send_multigpu(self.buffer0, dst_rank, dst_gpu_index, group_name)
|
||||
cp.cuda.Device(0).synchronize()
|
||||
return self.buffer0
|
||||
elif src_gpu_index == 1:
|
||||
col.send_multigpu(self.buffer1, dst_rank, dst_gpu_index, group_name)
|
||||
cp.cuda.Device(1).synchronize()
|
||||
return self.buffer1
|
||||
else:
|
||||
raise RuntimeError()
|
||||
|
||||
def do_recv_multigpu(
|
||||
self, group_name="default", src_rank=0, src_gpu_index=0, dst_gpu_index=0
|
||||
):
|
||||
if dst_gpu_index == 0:
|
||||
col.recv_multigpu(self.buffer0, src_rank, src_gpu_index, group_name)
|
||||
cp.cuda.Device(0).synchronize()
|
||||
return self.buffer0
|
||||
elif dst_gpu_index == 1:
|
||||
col.recv_multigpu(self.buffer1, src_rank, src_gpu_index, group_name)
|
||||
cp.cuda.Device(1).synchronize()
|
||||
return self.buffer1
|
||||
else:
|
||||
raise RuntimeError()
|
||||
|
||||
def destroy_group(self, group_name="default"):
|
||||
col.destroy_collective_group(group_name)
|
||||
return True
|
||||
|
||||
def report_rank(self, group_name="default"):
|
||||
rank = col.get_rank(group_name)
|
||||
return rank
|
||||
|
||||
def report_world_size(self, group_name="default"):
|
||||
ws = col.get_collective_group_size(group_name)
|
||||
return ws
|
||||
|
||||
def report_nccl_availability(self):
|
||||
avail = col.nccl_available()
|
||||
return avail
|
||||
|
||||
def report_gloo_availability(self):
|
||||
avail = col.gloo_available()
|
||||
return avail
|
||||
|
||||
def report_is_group_initialized(self, group_name="default"):
|
||||
is_init = col.is_group_initialized(group_name)
|
||||
return is_init
|
||||
|
||||
def report_num_gpus(self):
|
||||
n_gpus = get_num_gpus()
|
||||
return n_gpus
|
||||
|
||||
|
||||
def create_collective_multigpu_workers(
|
||||
num_workers=2, group_name="default", backend="nccl"
|
||||
):
|
||||
actors = [None] * num_workers
|
||||
for i in range(num_workers):
|
||||
actor = MultiGPUWorker.remote()
|
||||
ray.get([actor.set_buffer.remote([10])], timeout=10)
|
||||
ray.get([actor.set_list_buffer.remote([10])], timeout=10)
|
||||
actors[i] = actor
|
||||
world_size = num_workers
|
||||
init_results = ray.get(
|
||||
[
|
||||
actor.init_group.remote(world_size, i, backend, group_name)
|
||||
for i, actor in enumerate(actors)
|
||||
]
|
||||
)
|
||||
return actors, init_results
|
||||
|
||||
|
||||
def init_tensors_for_gather_scatter_multigpu(
|
||||
actors, array_size=10, tensor_backend="cupy"
|
||||
):
|
||||
for i, a in enumerate(actors):
|
||||
if tensor_backend == "cupy":
|
||||
ray.get([a.set_buffer.remote(array_size)])
|
||||
ray.get([a.set_list_buffer.remote(array_size)])
|
||||
elif tensor_backend == "torch":
|
||||
ray.get(
|
||||
[
|
||||
a.set_buffer.remote(
|
||||
array_size, tensor_type0="torch", tensor_type1="torch"
|
||||
)
|
||||
]
|
||||
)
|
||||
ray.get(
|
||||
[
|
||||
a.set_list_buffer.remote(
|
||||
array_size, tensor_type0="torch", tensor_type1="torch"
|
||||
)
|
||||
]
|
||||
)
|
||||
else:
|
||||
raise RuntimeError("Unsupported tensor backend.")
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Types conversion between different backends."""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
_NUMPY_AVAILABLE = True
|
||||
_TORCH_AVAILABLE = True
|
||||
_CUPY_AVAILABLE = True
|
||||
|
||||
if TYPE_CHECKING:
|
||||
pass
|
||||
|
||||
try:
|
||||
import torch as th # noqa: F401
|
||||
except ImportError:
|
||||
_TORCH_AVAILABLE = False
|
||||
|
||||
try:
|
||||
import cupy as cp # noqa: F401
|
||||
except ImportError:
|
||||
_CUPY_AVAILABLE = False
|
||||
|
||||
|
||||
def cupy_available():
|
||||
return _CUPY_AVAILABLE
|
||||
|
||||
|
||||
def torch_available():
|
||||
return _TORCH_AVAILABLE
|
||||
|
||||
|
||||
class Backend(object):
|
||||
"""A class to represent different backends."""
|
||||
|
||||
NCCL = "NCCL"
|
||||
GLOO = "GLOO"
|
||||
UNRECOGNIZED = "unrecognized"
|
||||
|
||||
def __new__(cls, name: str):
|
||||
upper_name = name.upper()
|
||||
backend = getattr(Backend, upper_name, Backend.UNRECOGNIZED)
|
||||
if backend == Backend.UNRECOGNIZED:
|
||||
if upper_name == "TORCH_GLOO":
|
||||
return Backend.GLOO
|
||||
raise ValueError(
|
||||
"Unrecognized backend: '{}'. Only NCCL and GLOO are supported".format(
|
||||
name
|
||||
)
|
||||
)
|
||||
return backend
|
||||
|
||||
|
||||
class ReduceOp(Enum):
|
||||
SUM = 0
|
||||
PRODUCT = 1
|
||||
MIN = 2
|
||||
MAX = 3
|
||||
|
||||
|
||||
unset_timeout_ms = timedelta(milliseconds=-1)
|
||||
|
||||
|
||||
@dataclass
|
||||
class AllReduceOptions:
|
||||
reduceOp = ReduceOp.SUM
|
||||
timeout_ms = unset_timeout_ms
|
||||
|
||||
|
||||
@dataclass
|
||||
class BarrierOptions:
|
||||
timeout_ms = unset_timeout_ms
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReduceOptions:
|
||||
reduceOp = ReduceOp.SUM
|
||||
root_rank = 0
|
||||
root_tensor = 0 # index for multi-gpu reduce operations
|
||||
timeout_ms = unset_timeout_ms
|
||||
|
||||
|
||||
@dataclass
|
||||
class AllGatherOptions:
|
||||
timeout_ms = unset_timeout_ms
|
||||
|
||||
|
||||
#
|
||||
# @dataclass
|
||||
# class GatherOptions:
|
||||
# root_rank = 0
|
||||
# timeout = unset_timeout
|
||||
|
||||
|
||||
@dataclass
|
||||
class BroadcastOptions:
|
||||
root_rank = 0
|
||||
root_tensor = 0
|
||||
timeout_ms = unset_timeout_ms
|
||||
|
||||
|
||||
@dataclass
|
||||
class ReduceScatterOptions:
|
||||
reduceOp = ReduceOp.SUM
|
||||
timeout_ms = unset_timeout_ms
|
||||
|
||||
|
||||
@dataclass
|
||||
class SendOptions:
|
||||
dst_rank = 0
|
||||
dst_gpu_index = 0
|
||||
n_elements = 0
|
||||
timeout_ms = unset_timeout_ms
|
||||
|
||||
|
||||
@dataclass
|
||||
class RecvOptions:
|
||||
src_rank = 0
|
||||
src_gpu_index = 0
|
||||
n_elements = 0
|
||||
unset_timeout_ms = unset_timeout_ms
|
||||
@@ -0,0 +1,85 @@
|
||||
"""Some utility class for Collectives."""
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import ray
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@ray.remote
|
||||
class NCCLUniqueIDStore:
|
||||
"""NCCLUniqueID Store as a named actor class.
|
||||
|
||||
Args:
|
||||
name: the unique name for this named actor.
|
||||
|
||||
Attributes:
|
||||
name: the unique name for this named actor.
|
||||
nccl_id: the NCCLUniqueID held in this store.
|
||||
"""
|
||||
|
||||
def __init__(self, name: str):
|
||||
self.name = name
|
||||
self.nccl_id = None
|
||||
self.event = asyncio.Event()
|
||||
|
||||
async def set_id(self, uid: bytes):
|
||||
"""
|
||||
Initialize the NCCL unique ID for this store.
|
||||
|
||||
Args:
|
||||
uid: the unique ID generated via the NCCL generate_communicator_id API.
|
||||
|
||||
Returns:
|
||||
The NCCL unique ID set.
|
||||
"""
|
||||
self.nccl_id = uid
|
||||
self.event.set()
|
||||
return uid
|
||||
|
||||
async def wait_and_get_id(self):
|
||||
"""Wait for the NCCL unique ID to be set and return it."""
|
||||
await self.event.wait()
|
||||
return self.nccl_id
|
||||
|
||||
def get_id(self):
|
||||
"""Get the NCCL unique ID held in this store."""
|
||||
if not self.nccl_id:
|
||||
logger.warning(
|
||||
"The NCCL ID has not been set yet for store {}.".format(self.name)
|
||||
)
|
||||
return self.nccl_id
|
||||
|
||||
|
||||
@ray.remote
|
||||
class Info:
|
||||
"""Store the group information created via `create_collective_group`.
|
||||
|
||||
Note: Should be used as a NamedActor.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.ids = None
|
||||
self.world_size = -1
|
||||
self.rank = -1
|
||||
self.backend = None
|
||||
self.gloo_timeout = 30000
|
||||
|
||||
def set_info(self, ids, world_size, rank, backend, gloo_timeout):
|
||||
"""Store collective information."""
|
||||
self.ids = ids
|
||||
self.world_size = world_size
|
||||
self.rank = rank
|
||||
self.backend = backend
|
||||
self.gloo_timeout = gloo_timeout
|
||||
|
||||
def get_info(self):
|
||||
"""Get previously stored collective information."""
|
||||
return (
|
||||
self.ids,
|
||||
self.world_size,
|
||||
self.rank,
|
||||
self.backend,
|
||||
self.gloo_timeout,
|
||||
)
|
||||
Reference in New Issue
Block a user