Files
wehub-resource-sync 59a0a3844c
PR Test AMD / cancel-on-close (push) Has been skipped
PR Test NVIDIA ARM / scan (push) Has been skipped
PR Test NVIDIA / cancel-on-close (push) Has been skipped
PR Test AMD / scan (push) Has been skipped
PR Test NVIDIA ARM / cancel-on-close (push) Has been skipped
PR Test NVIDIA / scan (push) Has been skipped
Release Docker Images / build (cu129-torch-2.11.0) (push) Has been skipped
Release Docker Images / build (cu130-torch-2.11.0) (push) Has been skipped
Release PyPI / publish (push) Has been skipped
Scheduler Python Test / test (push) Successful in 27m19s
Docs / build (push) Successful in 28m8s
Scheduler C++ Test / test (push) Successful in 28m19s
Scheduler C++ Test / test-flat (push) Successful in 28m18s
Docs / deploy (push) Has been cancelled
PR Test AMD / finish (push) Has been cancelled
PR Test NVIDIA / finish (push) Has been cancelled
PR Test NVIDIA ARM / finish (push) Has been cancelled
PR Test NVIDIA ARM / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test AMD / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
PR Test NVIDIA / ${{ matrix.name }} (${{ matrix.runner }}) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:32:31 +08:00

259 lines
9.2 KiB
Python

# Copyright (c) 2026 LightSeek Foundation
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
"""NCCL communication backend.
Looks up pre-created process groups from pg_manager. Optionally uses
PyNccl communicators for better performance. Supports torch.compile
via custom ops.
"""
import torch
import torch.distributed
from tokenspeed.runtime.distributed.comm_backend.base import CommBackend, Group
class NcclBackend(CommBackend):
"""Backend using NCCL via PyNccl or torch.distributed.
Caches per-group resources (process group handle, PyNccl comm)
keyed by group tuple. Process groups are looked up from pg_manager
on first use.
"""
def __init__(self):
self._resources = {} # group_tuple → {pynccl_comm, device_group, world_size}
self._use_pynccl = False
def configure(self, use_pynccl: bool = False) -> None:
self._use_pynccl = use_pynccl
def _get_or_create_resources(self, group: Group):
if group in self._resources:
return self._resources[group]
from tokenspeed.runtime.distributed.process_group_manager import (
process_group_manager as pg_manager,
)
device_group = pg_manager.get_process_group("nccl", group)
world_size = len(group)
pynccl_comm = None
if self._use_pynccl and world_size > 1:
try:
from tokenspeed.runtime.distributed.device_communicators.pynccl import (
PyNcclCommunicator,
)
gloo_group = pg_manager.get_process_group("gloo", group)
pynccl_comm = PyNcclCommunicator(
group=gloo_group,
device=torch.device(f"cuda:{torch.cuda.current_device()}"),
)
except Exception:
pynccl_comm = None
self._resources[group] = {
"pynccl_comm": pynccl_comm,
"device_group": device_group,
"world_size": world_size,
}
return self._resources[group]
# ---- Public CommBackend interface ----
def all_reduce(self, tensor: torch.Tensor, group: Group, op=None) -> torch.Tensor:
res = self._get_or_create_resources(group)
if res["world_size"] == 1:
return tensor
if op is None:
op = torch.distributed.ReduceOp.SUM
pynccl = res["pynccl_comm"]
if pynccl is not None and not pynccl.disabled:
pynccl.all_reduce(tensor, op=op)
else:
torch.distributed.all_reduce(tensor, op=op, group=res["device_group"])
return tensor
def all_gather(
self, tensor: torch.Tensor, group: Group, dim: int = 0
) -> torch.Tensor:
res = self._get_or_create_resources(group)
ws = res["world_size"]
if ws == 1:
return tensor
if dim < 0:
dim += tensor.dim()
input_size = tensor.size()
output_size = (input_size[0] * ws,) + input_size[1:]
output_tensor = torch.empty(
output_size, dtype=tensor.dtype, device=tensor.device
)
self.all_gather_into_tensor(output_tensor, tensor, group)
output_tensor = output_tensor.reshape((ws,) + input_size)
output_tensor = output_tensor.movedim(0, dim)
output_tensor = output_tensor.reshape(
input_size[:dim] + (ws * input_size[dim],) + input_size[dim + 1 :]
)
return output_tensor
def all_gather_into_tensor(
self, output: torch.Tensor, input: torch.Tensor, group: Group
) -> None:
res = self._get_or_create_resources(group)
pynccl = res["pynccl_comm"]
if pynccl is not None and not pynccl.disabled:
pynccl.all_gather(output, input)
else:
torch.distributed.all_gather_into_tensor(
output, input, group=res["device_group"]
)
def all_to_all_single(
self, output: torch.Tensor, input: torch.Tensor, group: Group
) -> None:
res = self._get_or_create_resources(group)
ws = res["world_size"]
if ws == 1:
output.copy_(input)
return
# PyNccl has no all_to_all wrapper
torch.distributed.all_to_all_single(output, input, group=res["device_group"])
def reduce_scatter(self, tensor: torch.Tensor, group: Group) -> torch.Tensor:
res = self._get_or_create_resources(group)
ws = res["world_size"]
if ws == 1:
return tensor
input_size = tuple(tensor.size())
output_tensor = torch.empty(
(input_size[0] // ws,) + input_size[1:],
dtype=tensor.dtype,
device=tensor.device,
)
pynccl = res["pynccl_comm"]
if pynccl is not None and not pynccl.disabled:
pynccl.reduce_scatter(output_tensor, tensor)
else:
torch.distributed.reduce_scatter_tensor(
output_tensor, tensor, group=res["device_group"]
)
return output_tensor
def send(self, tensor: torch.Tensor, dst: int, group: Group) -> None:
res = self._get_or_create_resources(group)
pynccl = res["pynccl_comm"]
if pynccl is not None and not pynccl.disabled:
pynccl.send(tensor, dst)
else:
torch.distributed.send(tensor, group[dst], group=res["device_group"])
def recv(
self,
size: torch.Size,
dtype: torch.dtype,
device: torch.device,
src: int,
group: Group,
) -> torch.Tensor:
res = self._get_or_create_resources(group)
tensor = torch.empty(size, dtype=dtype, device=device)
pynccl = res["pynccl_comm"]
if pynccl is not None and not pynccl.disabled:
pynccl.recv(tensor, src)
else:
torch.distributed.recv(tensor, group[src], group=res["device_group"])
return tensor
def token_all_gather(
self,
tensor: torch.Tensor,
group: Group,
scattered_num_tokens: list[int],
) -> torch.Tensor:
"""NCCL token_all_gather with padding for uneven token distribution.
Pads each rank's slice to max_tokens rows, all-gathers, then strips padding.
"""
tp_size = len(scattered_num_tokens)
max_tokens = max(scattered_num_tokens)
hidden = tensor.size(-1)
local_tokens = tensor.size(0)
if local_tokens < max_tokens:
pad = torch.zeros(
max_tokens - local_tokens,
hidden,
dtype=tensor.dtype,
device=tensor.device,
)
padded = torch.cat([tensor, pad], dim=0)
else:
padded = tensor
output = torch.empty(
tp_size * max_tokens, hidden, dtype=tensor.dtype, device=tensor.device
)
self.all_gather_into_tensor(output, padded.contiguous(), group)
chunks = []
for i, n in enumerate(scattered_num_tokens):
chunks.append(output[i * max_tokens : i * max_tokens + n])
return torch.cat(chunks, dim=0)
def token_reduce_scatter(
self,
tensor: torch.Tensor,
group: Group,
scattered_num_tokens: list[int],
) -> torch.Tensor:
"""NCCL token_reduce_scatter with padding for uneven token distribution.
Pads the gathered tensor to a uniform layout, reduce-scatters, then strips padding.
"""
tp_size = len(scattered_num_tokens)
max_tokens = max(scattered_num_tokens)
hidden = tensor.size(-1)
padded_input = torch.zeros(
tp_size * max_tokens, hidden, dtype=tensor.dtype, device=tensor.device
)
offset = 0
for i, n in enumerate(scattered_num_tokens):
padded_input[i * max_tokens : i * max_tokens + n].copy_(
tensor[offset : offset + n]
)
offset += n
output = torch.empty(
max_tokens, hidden, dtype=tensor.dtype, device=tensor.device
)
res = self._get_or_create_resources(group)
torch.distributed.reduce_scatter_tensor(
output, padded_input.contiguous(), group=res["device_group"]
)
rank = group.index(torch.distributed.get_rank())
return output[: scattered_num_tokens[rank]].contiguous()