[diffusion] fix: stop reserving nccl device buffers for single-rank groups (#35538)

This commit is contained in:
Mick
2026-08-20 09:04:35 +08:00
committed by GitHub
parent e805a8f98e
commit 1f87d8f512
5 changed files with 62 additions and 20 deletions
@@ -77,7 +77,9 @@ def _patch_cache_dit_similarity():
tp_sp_group = getattr(self, "_sglang_tp_sp_group", None)
target_group = tp_sp_group or sp_group or tp_group
if target_group is None:
# Averaging over a one-rank group returns the input, so skip the
# collective rather than pay for a round trip that cannot change it.
if target_group is None or dist.get_world_size(target_group) == 1:
return _original_similarity(
self,
t1,
@@ -123,6 +123,18 @@ class GraphCaptureContext:
stream: torch.cuda.Stream | None
def new_device_group(ranks, backend=None):
"""Create a process group for device collectives.
A single-rank group never runs one: every collective short-circuits on
world_size == 1. NCCL would still allocate its per-channel device buffers
for it, which costs ~390 MiB a group.
"""
return torch.distributed.new_group(
ranks, backend="gloo" if len(ranks) == 1 else backend
)
class GroupCoordinator:
"""
PyTorch ProcessGroup wrapper for a group of processes.
@@ -169,9 +181,7 @@ class GroupCoordinator:
self.cpu_group = None
for ranks in group_ranks:
device_group = torch.distributed.new_group(
ranks, backend=torch_distributed_backend
)
device_group = new_device_group(ranks, torch_distributed_backend)
# a group with `gloo` backend, to allow direct coordination between
# processes through the CPU.
with suppress_stdout():
@@ -863,9 +873,7 @@ class PipelineGroupCoordinator(GroupCoordinator):
self.device_groups = []
if len(group_ranks[0]) > 2 or len(group_ranks[0]) == 1:
for ranks in group_ranks:
device_group = torch.distributed.new_group(
ranks, backend=torch_distributed_backend
)
device_group = new_device_group(ranks, torch_distributed_backend)
# a group with `gloo` backend, to allow direct coordination between
# processes through the CPU.
with suppress_stdout():
@@ -927,9 +935,7 @@ class PipelineGroupCoordinator(GroupCoordinator):
] = None
self.skip_device_group = None
for ranks in group_ranks:
skip_device_group = torch.distributed.new_group(
ranks, backend=torch_distributed_backend
)
skip_device_group = new_device_group(ranks, torch_distributed_backend)
if self.rank in ranks:
self.skip_device_group = skip_device_group
assert self.skip_device_group is not None
@@ -1,7 +1,7 @@
# Reference: https://github.com/feifeibear/long-context-attention/blob/main/yunchang/globals.py
import torch
from .group_coordinator import new_device_group
class Singleton:
@@ -56,34 +56,34 @@ def set_seq_parallel_pg_by_sp_groups(
def _map_indices_to_ranks(ranks: list[int], indices: list[int]) -> list[int]:
return [ranks[i] for i in indices]
# Important: call torch.distributed.new_group in the same order on all ranks.
# Important: create the groups in the same order on all ranks.
for sp_ranks in sp_groups:
if use_ulysses_low:
for i in range(num_ulysses_pgs):
idx = list(range(i * sp_ulysses_degree, (i + 1) * sp_ulysses_degree))
ulysses_ranks = _map_indices_to_ranks(sp_ranks, idx)
group = torch.distributed.new_group(ulysses_ranks)
group = new_device_group(ulysses_ranks)
if rank in ulysses_ranks:
ulyssess_pg = group
for i in range(num_ring_pgs):
idx = list(range(i, sp_degree, num_ring_pgs))
ring_ranks = _map_indices_to_ranks(sp_ranks, idx)
group = torch.distributed.new_group(ring_ranks)
group = new_device_group(ring_ranks)
if rank in ring_ranks:
ring_pg = group
else:
for i in range(num_ring_pgs):
idx = list(range(i * sp_ring_degree, (i + 1) * sp_ring_degree))
ring_ranks = _map_indices_to_ranks(sp_ranks, idx)
group = torch.distributed.new_group(ring_ranks)
group = new_device_group(ring_ranks)
if rank in ring_ranks:
ring_pg = group
for i in range(num_ulysses_pgs):
idx = list(range(i, sp_degree, num_ulysses_pgs))
ulysses_ranks = _map_indices_to_ranks(sp_ranks, idx)
group = torch.distributed.new_group(ulysses_ranks)
group = new_device_group(ulysses_ranks)
if rank in ulysses_ranks:
ulyssess_pg = group
@@ -56,6 +56,7 @@ from .group_coordinator import (
PipelineGroupCoordinator,
SequenceParallelGroupCoordinator,
get_local_torch_device,
new_device_group,
)
logger = init_logger(__name__)
@@ -999,9 +1000,7 @@ def init_dit_group(
) -> None:
global _DIT
assert _DIT is None, "DIT group is already initialized"
_DIT = torch.distributed.new_group(
ranks=list(range(dit_parallel_size)), backend=backend
)
_DIT = new_device_group(list(range(dit_parallel_size)), backend)
def get_dit_group() -> ProcessGroup:
@@ -1018,7 +1017,7 @@ def init_vae_group(
global _VAE
assert _VAE is None, "VAE parallel group is already initialized"
vae_ranks = list(range(dit_parallel_size, dit_parallel_size + vae_parallel_size))
_VAE = torch.distributed.new_group(ranks=vae_ranks, backend=backend)
_VAE = new_device_group(vae_ranks, backend)
def destroy_model_parallel() -> None:
@@ -0,0 +1,35 @@
"""Single-rank groups get gloo, so NCCL does not reserve device buffers for them."""
import unittest
from unittest.mock import patch
from sglang.multimodal_gen.runtime.distributed.group_coordinator import (
new_device_group,
)
NEW_GROUP_PATH = "torch.distributed.new_group"
class TestSingleRankDeviceGroup(unittest.TestCase):
def test_single_rank_group_avoids_the_device_backend(self):
for ranks, requested in [([0], "nccl"), ([3], "hccl"), ([0], None)]:
with self.subTest(ranks=ranks, requested=requested):
with patch(NEW_GROUP_PATH) as new_group:
new_device_group(ranks, requested)
new_group.assert_called_once_with(ranks, backend="gloo")
def test_multi_rank_group_keeps_the_requested_backend(self):
for ranks, requested in [([0, 1], "nccl"), ([0, 1, 2, 3], None)]:
with self.subTest(ranks=ranks, requested=requested):
with patch(NEW_GROUP_PATH) as new_group:
new_device_group(ranks, requested)
new_group.assert_called_once_with(ranks, backend=requested)
def test_backend_defaults_to_none_for_multi_rank(self):
with patch(NEW_GROUP_PATH) as new_group:
new_device_group([0, 1])
new_group.assert_called_once_with([0, 1], backend=None)
if __name__ == "__main__":
unittest.main()