Source code for torchfoo.distributed.collective
__all__ = [
"all_reduce_",
"all_reduce_sum_",
"all_equal",
"all_concat_jagged",
"all_concat",
]
import torch
from torch import Tensor
import torch.distributed as dist
from .distributed import get_world_size, get_rank
[docs]
def all_reduce_(x: Tensor) -> Tensor:
r"""Perform an in-place differentiable "all reduce" operation
Args:
x: a contiguous Tensor on this device
Returns:
The input tensor, modified in-place with the all-reduced mean.
"""
if not x.is_contiguous():
raise ValueError("input must be contiguous")
return _AllReduce.apply(x)
[docs]
def all_reduce_sum_(x: Tensor) -> Tensor:
r"""Perform an in-place differentiable "all reduce sum" operation
Args:
x: a contiguous Tensor on this device
Returns:
The input tensor, modified in-place with the all-reduced sum.
"""
if not x.is_contiguous():
raise ValueError("input must be contiguous")
return _AllReduceSum.apply(x)
[docs]
def all_equal(x: Tensor) -> bool:
r"""Check if x is the same on all GPUs
Args:
x: a contiguous Tensor on this device
Returns:
True if x is equal across all processes.
"""
if not x.is_contiguous():
raise ValueError("input must be contiguous")
nworld = get_world_size()
if nworld == 1:
return True
tensor_list = [torch.zeros_like(x) for _ in range(nworld)]
dist.all_gather(tensor_list, x)
return all((val == x).all() for val in tensor_list)
[docs]
def all_concat_jagged(x: Tensor) -> Tensor:
r"""Concatenate a Tensor with varying dim 0 from multiple GPUs
Not differentiable.
Args:
x: a Tensor on this device
Returns:
Concatenated tensor from all processes along dim 0.
"""
nworld = get_world_size()
if nworld == 1:
return x
output = [None for _ in range(nworld)]
dist.all_gather_object(output, x)
return torch.concat(output) # type: ignore
[docs]
def all_concat(x: Tensor) -> Tensor:
r"""Concatenate a Tensor distributed on multiple GPUs. Backprop-able.
Args:
x: a Tensor on this device
Returns:
Concatenated tensor from all processes along dim 0.
"""
return _AllConcat.apply(x)
# Inspired by:
# https://github.com/facebookresearch/ijepa/blob/52c1ae95d05f743e000e8f10a1f3a79b10cff048/src/utils/distributed.py
class _AllConcat(torch.autograd.Function):
@staticmethod
def forward(ctx, x):
if get_world_size() > 1:
x = x.contiguous()
outputs = [torch.zeros_like(x) for _ in range(dist.get_world_size())]
dist.all_gather(outputs, x)
return torch.cat(outputs, 0)
return x
@staticmethod
def backward(_ctx, grads):
nworld = get_world_size()
if nworld > 1:
rank = get_rank()
s = (grads.shape[0] // nworld) * rank
e = (grads.shape[0] // nworld) * (rank + 1)
grads = grads.contiguous()
dist.all_reduce(grads)
return grads[s:e]
return grads
class _AllReduceSum(torch.autograd.Function):
@staticmethod
def forward(ctx, x: Tensor):
n = get_world_size()
ctx.n = n
if n > 1:
dist.all_reduce(x)
return x
@staticmethod
def backward(ctx, grads):
# gradient will be averaged over the world in loss.backward()
# so, we scale it up here to counter that
if ctx.n > 1:
return grads * ctx.n
return grads
class _AllReduce(torch.autograd.Function):
@staticmethod
def forward(_ctx, x: Tensor):
nworld = get_world_size()
if nworld > 1:
x.div_(nworld)
dist.all_reduce(x)
return x
@staticmethod
def backward(_ctx, grads):
return grads