190 lines
6.6 KiB
Python
190 lines
6.6 KiB
Python
"""API wrapping NCCL primitives."""
|
|
|
|
import torch
|
|
import torch.distributed as dist
|
|
|
|
|
|
def sparse_all_to_all_push(idx, value, partition):
|
|
"""Perform an all-to-all-v operation, where by all processors send out
|
|
a set of indices and corresponding values. Indices and values,
|
|
corresponding to the current process, will copied into the output
|
|
arrays.
|
|
|
|
Note: This method requires 'torch.distributed.get_backend() == "nccl"'.
|
|
|
|
Parameters
|
|
----------
|
|
idx : torch.Tensor
|
|
The 1D set of indices to send to other processors.
|
|
value : torch.Tensor
|
|
The multi-dimension set of values to send to other processors.
|
|
The first dimension must match that of `idx`.
|
|
partition : NDArrayPartition
|
|
The object containing information for assigning indices to
|
|
processors.
|
|
|
|
Returns
|
|
-------
|
|
torch.Tensor
|
|
The 1D tensor of the recieved indices.
|
|
torch.Tensor
|
|
The set of recieved values.
|
|
|
|
Examples
|
|
--------
|
|
|
|
To perform a sparse_all_to_all_push(), a partition object must be
|
|
provided. A partition of a homgeonous graph, where the vertices are
|
|
striped across processes can be generated via:
|
|
|
|
>>> from dgl.partition import NDArrayPartition
|
|
>>> part = NDArrayPartition(g.num_nodes(), world_size, mode='remainder')
|
|
|
|
With this partition, each processor can send values to be associatd
|
|
with vertices in the graph. So if we have an array `global_idxs` of all of
|
|
the neighbors updated during mini-batch processing, and an array
|
|
`global_values` containing the new values associated with the neighbors,
|
|
we communicate them to the own processes via:
|
|
|
|
>>> my_idxs, my_values = nccl.sparse_all_to_all_push(global_idxs, global_values, part)
|
|
|
|
This communication pattern is common when communicating gradient
|
|
updates for node embeddings.
|
|
|
|
Indices the current process owns, do not need to treated specially,
|
|
as internally they will be copied to the output array. If we have a
|
|
set of indices in process 0 '[0, 3, 8, 9, 10]` and for process 1
|
|
'[0, 2, 4, 5, 8, 8, 9]'. Using a remainder partition will result
|
|
indices for processe 0 of '[0, 8, 10, 0, 2, 4, 8, 8]', and for
|
|
process 1 of '[3, 9, 5, 9]'.
|
|
"""
|
|
if not dist.is_initialized() or dist.get_world_size() == 1:
|
|
return idx, value
|
|
assert (
|
|
dist.get_backend() == "nccl"
|
|
), "requires NCCL backend to communicate CUDA tensors."
|
|
|
|
perm, send_splits = partition.generate_permutation(idx)
|
|
perm = perm.long()
|
|
|
|
# Get receive splits.
|
|
recv_splits = torch.empty_like(send_splits)
|
|
dist.all_to_all_single(recv_splits, send_splits)
|
|
|
|
# Use pinned memory to speedup D2H copy.
|
|
recv_splits = recv_splits.to("cpu", non_blocking=True)
|
|
send_splits = send_splits.to("cpu", non_blocking=True)
|
|
send_idx = idx[perm]
|
|
send_value = value[perm]
|
|
# Wait D2H copy finish.
|
|
torch.cuda.current_stream().synchronize()
|
|
recv_sum = recv_splits.sum()
|
|
recv_splits = recv_splits.tolist()
|
|
send_splits = send_splits.tolist()
|
|
|
|
# Send idx.
|
|
recv_idx = torch.empty((recv_sum,), dtype=idx.dtype, device=idx.device)
|
|
dist.all_to_all_single(recv_idx, send_idx, recv_splits, send_splits)
|
|
|
|
# Send value.
|
|
recv_value = torch.empty(
|
|
(recv_sum, *value.shape[1:]), dtype=value.dtype, device=value.device
|
|
)
|
|
dist.all_to_all_single(recv_value, send_value, recv_splits, send_splits)
|
|
|
|
return recv_idx, recv_value
|
|
|
|
|
|
def sparse_all_to_all_pull(req_idx, value, partition):
|
|
"""Perform an all-to-all-v operation, where by all processors request
|
|
the values corresponding to their set of indices.
|
|
|
|
Note: This method requires 'torch.distributed.get_backend() == "nccl"'.
|
|
|
|
Parameters
|
|
----------
|
|
req_idx : torch.Tensor
|
|
The set of indices this processor is requesting.
|
|
value : torch.Tensor
|
|
The multi-dimension set of values that can be requested from
|
|
this processor.
|
|
partition : NDArrayPartition
|
|
The object containing information for assigning indices to
|
|
processors.
|
|
|
|
Returns
|
|
-------
|
|
torch.Tensor
|
|
The set of recieved values, corresponding to `req_idx`.
|
|
|
|
Examples
|
|
--------
|
|
|
|
To perform a sparse_all_to_all_pull(), a partition object must be
|
|
provided. A partition of a homgeonous graph, where the vertices are
|
|
striped across processes can be generated via:
|
|
|
|
>>> from dgl.partition import NDArrayPartition
|
|
>>> part = NDArrayPartition(g.num_nodes(), world_size, mode='remainder')
|
|
|
|
With this partition, each processor can request values/features
|
|
associated with vertices in the graph. So in the case where we have
|
|
a set of neighbors 'nbr_idxs' we need features for, and each process
|
|
has a tensor 'node_feat' storing the features of nodes it owns in
|
|
the partition, the features can be requested via:
|
|
|
|
>>> nbr_values = nccl.sparse_all_to_all_pull(nbr_idxs, node_feat, part)
|
|
|
|
Then two the arrays 'nbr_idxs' and 'nbr_values' forms the sparse
|
|
set of features, where 'nbr_idxs[i]' is the global node id, and
|
|
'nbr_values[i]' is the feature vector for that node. This
|
|
communication pattern is useful for node features or node
|
|
embeddings.
|
|
"""
|
|
if not dist.is_initialized() or dist.get_world_size() == 1:
|
|
return value[req_idx.long()]
|
|
assert (
|
|
dist.get_backend() == "nccl"
|
|
), "requires NCCL backend to communicate CUDA tensors."
|
|
|
|
perm, req_splits = partition.generate_permutation(req_idx)
|
|
perm = perm.long()
|
|
|
|
# Get response splits.
|
|
resp_splits = torch.empty_like(req_splits)
|
|
dist.all_to_all_single(resp_splits, req_splits)
|
|
|
|
# Use pinned memory to speedup D2H copy.
|
|
resp_splits = resp_splits.to("cpu", non_blocking=True)
|
|
req_splits = req_splits.to("cpu", non_blocking=True)
|
|
req_idx = req_idx[perm]
|
|
# Wait D2H copy finish.
|
|
torch.cuda.current_stream().synchronize()
|
|
resp_sum = resp_splits.sum()
|
|
resp_splits = resp_splits.tolist()
|
|
req_splits = req_splits.tolist()
|
|
|
|
# Gather requested indices.
|
|
resp_idx = torch.empty(
|
|
(resp_sum,), dtype=req_idx.dtype, device=req_idx.device
|
|
)
|
|
dist.all_to_all_single(resp_idx, req_idx, resp_splits, req_splits)
|
|
|
|
# Convert requested indices to local indices depending on partition.
|
|
if resp_sum > 0:
|
|
resp_idx = partition.map_to_local(resp_idx)
|
|
|
|
# Collect the request value.
|
|
req_value = torch.empty(
|
|
(req_idx.size(0), *value.shape[1:]),
|
|
dtype=value.dtype,
|
|
device=value.device,
|
|
)
|
|
dist.all_to_all_single(req_value, value[resp_idx], req_splits, resp_splits)
|
|
|
|
# Permute the value back into the requested order.
|
|
return_value = torch.empty_like(req_value)
|
|
return_value[perm] = req_value
|
|
|
|
return return_value
|