chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 13:17:40 +08:00
commit f1825c8ceb
10096 changed files with 2364182 additions and 0 deletions
@@ -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)