chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
""" CUDA wrappers """
|
||||
from .. import backend as F
|
||||
|
||||
from .gpu_cache import GPUCache
|
||||
|
||||
if F.get_preferred_backend() == "pytorch":
|
||||
from . import nccl
|
||||
@@ -0,0 +1,86 @@
|
||||
"""API wrapping HugeCTR gpu_cache."""
|
||||
# Copyright (c) 2022, NVIDIA Corporation
|
||||
# All rights reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
#
|
||||
# @file gpu_cache.py
|
||||
# @brief API for managing a GPU Cache
|
||||
|
||||
from .. import backend as F
|
||||
from .._ffi.function import _init_api
|
||||
|
||||
|
||||
class GPUCache(object):
|
||||
"""High-level wrapper for GPU embedding cache"""
|
||||
|
||||
def __init__(self, num_items, num_feats, idtype=F.int64):
|
||||
assert idtype in [F.int32, F.int64]
|
||||
self._cache = _CAPI_DGLGpuCacheCreate(
|
||||
num_items, num_feats, 32 if idtype == F.int32 else 64
|
||||
)
|
||||
self.idtype = idtype
|
||||
self.total_miss = 0
|
||||
self.total_queries = 0
|
||||
|
||||
def query(self, keys):
|
||||
"""Queries the GPU cache.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
keys : Tensor
|
||||
The keys to query the GPU cache with.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple(Tensor, Tensor, Tensor)
|
||||
A tuple containing (values, missing_indices, missing_keys) where
|
||||
values[missing_indices] corresponds to cache misses that should be
|
||||
filled by quering another source with missing_keys.
|
||||
"""
|
||||
self.total_queries += keys.shape[0]
|
||||
keys = F.astype(keys, self.idtype)
|
||||
values, missing_index, missing_keys = _CAPI_DGLGpuCacheQuery(
|
||||
self._cache, F.to_dgl_nd(keys)
|
||||
)
|
||||
self.total_miss += missing_keys.shape[0]
|
||||
return (
|
||||
F.from_dgl_nd(values),
|
||||
F.from_dgl_nd(missing_index),
|
||||
F.from_dgl_nd(missing_keys),
|
||||
)
|
||||
|
||||
def replace(self, keys, values):
|
||||
"""Inserts key-value pairs into the GPU cache using the Least-Recently
|
||||
Used (LRU) algorithm to remove old key-value pairs if it is full.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
keys: Tensor
|
||||
The keys to insert to the GPU cache.
|
||||
values: Tensor
|
||||
The values to insert to the GPU cache.
|
||||
"""
|
||||
keys = F.astype(keys, self.idtype)
|
||||
values = F.astype(values, F.float32)
|
||||
_CAPI_DGLGpuCacheReplace(
|
||||
self._cache, F.to_dgl_nd(keys), F.to_dgl_nd(values)
|
||||
)
|
||||
|
||||
@property
|
||||
def miss_rate(self):
|
||||
"""Returns the cache miss rate since creation."""
|
||||
return self.total_miss / self.total_queries
|
||||
|
||||
|
||||
_init_api("dgl.cuda", __name__)
|
||||
@@ -0,0 +1,189 @@
|
||||
"""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
|
||||
Reference in New Issue
Block a user