Files
paddlepaddle--paddle/python/paddle/distributed/communication/group.py
T
2026-07-13 12:40:42 +08:00

470 lines
14 KiB
Python

# Copyright (c) 2022 PaddlePaddle Authors. 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.
from __future__ import annotations
import warnings
from typing import TYPE_CHECKING, Literal
import paddle
import paddle.distributed as dist
from paddle import framework
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import ProcessGroup
class Group:
"""
The abstract representation of group.
"""
def __init__(
self,
rank_in_group: int,
id: int,
ranks: list[int],
pg: ProcessGroup | None = None,
name: str | None = None,
) -> None:
self._rank_in_group = rank_in_group
self._world_size = len(ranks) if rank_in_group >= 0 else -1
self._id = id
self._ranks = ranks
self._pg = pg
self._name = name
@property
def rank(self) -> int:
return self._rank_in_group
@property
def ranks(self) -> list[int]:
return self._ranks
@property
def nranks(self) -> int:
return len(self._ranks)
@property
def name(self) -> str | None:
return self._name
@property
def process_group(self) -> ProcessGroup:
return self._pg
@property
def world_size(self) -> int:
return self._world_size
@property
def backend(self) -> str:
return self._pg.name()
@property
def id(self) -> int:
return self._id
def is_member(self) -> bool:
if self.rank < 0:
return False
if self.nranks < 2:
return False
return True
def get_group_rank(self, rank: int) -> int | Literal[-1]:
if self.is_member():
return self.ranks.index(rank)
else:
return -1
def get_global_rank(self, rank: int) -> int | Literal[-1]:
"""
Get the global rank of a process within a group.
Args:
rank (int): The local rank within the group.
Returns:
If the current process is a member of the group, returns the corresponding global rank;
otherwise returns -1.
"""
if self.is_member():
return self.ranks[rank]
else:
return -1
def __repr__(self) -> str:
debug_str = (
f"rank: {self.rank}, nranks: {self.nranks}, id: {self.id}, ranks: "
)
debug_str += ", ".join(map(str, self.ranks))
debug_str += "; name: "
debug_str += self.name if self.name else "None"
return debug_str
class _GroupManager:
global_group_id = 0
group_map_by_id = {}
class _DistGroupMeta(type):
"""Metaclass exposing :attr:`group.WORLD` as a dynamic class property."""
@property
def WORLD(cls) -> Group | None:
try:
return _get_global_group()
except RuntimeError:
return None
@WORLD.setter
def WORLD(cls, value: Group | None) -> None:
# Validate before mutating any registry so a rejected assignment
# leaves the existing default group intact.
if value is not None:
if not isinstance(value, Group):
raise TypeError(
"group.WORLD must be a Group instance or None, got "
f"{type(value).__name__}"
)
if value.id != _GroupManager.global_group_id:
raise ValueError(
f"group.WORLD expects a Group with id="
f"{_GroupManager.global_group_id}, got id={value.id}"
)
# Lazy import: ``collective`` imports from this module at its top.
from paddle.distributed import collective as _coll
prev = _GroupManager.group_map_by_id.pop(
_GroupManager.global_group_id, None
)
_coll._group_map.pop(_coll._global_env_gid, None)
_coll._group_map_by_name.pop(_coll._default_group_name, None)
if prev is not None:
_coll._group_map_backend.pop(prev, None)
if value is None:
return
_GroupManager.group_map_by_id[_GroupManager.global_group_id] = value
_coll._group_map[_coll._global_env_gid] = value
_coll._group_map_by_name[_coll._default_group_name] = value
if value._pg is not None:
# ``ProcessGroup.name()`` returns the C++ backend name in upper
# case (e.g. ``NCCL``); the registry is keyed by the lower-case
# Python form used in ``_valid_backend_list``.
_coll._group_map_backend[value] = value._pg.name().lower()
class _DistGroupNamespace(metaclass=_DistGroupMeta):
"""Namespace exposing :attr:`WORLD`, re-exported as
:data:`paddle.distributed.group`.
"""
def _get_global_group():
if _GroupManager.global_group_id not in _GroupManager.group_map_by_id:
raise RuntimeError("The global group is not initialized.")
return _GroupManager.group_map_by_id[_GroupManager.global_group_id]
def _add_new_group(group):
if group.id in _GroupManager.group_map_by_id:
raise RuntimeError(f"The group with id {group.id} already exist.")
_GroupManager.group_map_by_id[group.id] = group
def _is_global_group(group):
return group.id == _GroupManager.global_group_id
def _warn_cur_rank_not_in_group(group):
global_rank = dist.get_rank()
if group and not group.is_member():
return True
return False
def _get_or_throw_group_rank(global_rank, group):
group_rank = group.get_group_rank(global_rank)
assert group_rank >= 0, (
f"The input rank {global_rank} can not be found inside the group {group.name}"
)
return group_rank
def is_initialized() -> bool:
"""
Check whether the distributed environment has been initialized
Returns:
`True` if distributed environment has been initialized, otherwise `False`.
Warning:
This API only supports the dygraph mode.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> print(paddle.distributed.is_initialized())
False
>>> paddle.distributed.init_parallel_env()
>>> print(paddle.distributed.is_initialized())
True
"""
return _GroupManager.global_group_id in _GroupManager.group_map_by_id
def destroy_process_group(group: Group | None = None) -> None:
"""
Destroy a given group for communication
Args:
group (Group, optional): The group to be destroyed. All of process groups, including
the default group, will be destroyed and the distributed
environment will be deinitialized.
Returns : None
Warning:
This API only supports the dygraph mode.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> group = dist.new_group([0, 1])
>>> dist.destroy_process_group(group)
>>> print(dist.is_initialized())
True
>>> dist.destroy_process_group()
>>> print(dist.is_initialized())
False
"""
group = _get_global_group() if group is None else group
assert group.id in _GroupManager.group_map_by_id, (
f"Destroy group with id {group.id} is invalid."
)
if _is_global_group(group):
_GroupManager.group_map_by_id.clear()
# The default group is also registered in the collective-layer
# registries by ``init_parallel_env``; clear those slots too so a
# follow-up ``init_process_group`` re-creates the default group
# rather than hitting ``init_parallel_env``'s early-return path.
from paddle.distributed import collective as _coll
_coll._group_map.pop(_coll._global_env_gid, None)
_coll._group_map_by_name.pop(_coll._default_group_name, None)
_coll._group_map_backend.pop(group, None)
else:
del _GroupManager.group_map_by_id[group.id]
def get_group(id: int = 0) -> Group:
"""
Get group instance by group id.
Args:
id (int): the group id. Default value is 0.
Returns:
Group: the group instance.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> gid = paddle.distributed.new_group([2, 4, 6])
>>> paddle.distributed.get_group(gid.id)
"""
if id in _GroupManager.group_map_by_id:
return _GroupManager.group_map_by_id[id]
warnings.warn(f"Group {id} is not initialized.")
return None
def _sync_calc_stream(tensor):
if framework.in_dynamic_mode():
return paddle._C_ops.sync_calc_stream(tensor)
else:
op_type = 'c_sync_calc_stream'
helper = framework.LayerHelper(op_type, **locals())
helper.append_op(
type=op_type,
inputs={'X': [tensor]},
outputs={'Out': [tensor]},
)
def _sync_comm_stream(tensor, ring_id=0):
if framework.in_dynamic_mode():
return paddle._C_ops.sync_comm_stream([tensor], ring_id)
else:
op_type = 'c_sync_comm_stream'
helper = framework.LayerHelper(op_type, **locals())
helper.append_op(
type=op_type,
inputs={'X': [tensor]},
outputs={'Out': [tensor]},
attrs={'ring_id': ring_id},
)
def wait(
tensor: Tensor, group: Group | None = None, use_calc_stream: bool = True
) -> None:
"""
wait to sync stream for group.
Args:
tensor (Tensor): The Tensor used before sync.
group (Group): The Group instance to perform sync.
use_calc_stream (bool): Whether to use calculation stream (True) or communication stream (False).
Default to True.
Returns:
None.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> paddle.distributed.init_parallel_env()
>>> tindata = paddle.randn(shape=[2, 3])
>>> paddle.distributed.all_reduce(tindata, sync_op=True)
>>> paddle.distributed.wait(tindata)
"""
if group is not None and not group.is_member():
return
if use_calc_stream:
_sync_calc_stream(tensor)
else:
ring_id = 0 if group is None else group.id
_sync_comm_stream(tensor, ring_id)
def barrier(group: Group | None = None) -> None:
"""
Barrier among all participators in the group.
Args:
group (Group): The group instance return by new_group or None for global default group.
Returns:
None.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> from paddle.distributed import init_parallel_env
>>> paddle.set_device(f'gpu:{paddle.distributed.ParallelEnv().dev_id}')
>>> init_parallel_env()
>>> paddle.distributed.barrier()
"""
if group is not None and not group.is_member():
return
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
place = framework._current_expected_place()
if isinstance(place, framework.CPUPlace):
task = group.process_group.barrier()
else:
device_id = place.get_device_id()
task = group.process_group.barrier(device_id)
task.wait()
return
ring_id = 0 if group is None else group.id
barrier_tensor = paddle.full([1], 1, dtype="int32")
if framework.in_dynamic_mode():
# barrier is not available in xpu for now
if not paddle.framework.core.is_compiled_with_xpu():
return paddle._legacy_C_ops.barrier(
barrier_tensor, barrier_tensor, 'ring_id', ring_id
)
else:
op_type = 'barrier'
if not isinstance(ring_id, int):
raise ValueError("The type of 'group' for barrier must be int.")
helper = framework.LayerHelper(op_type, **locals())
helper.append_op(
type=op_type,
inputs={'X': [barrier_tensor]},
outputs={'Out': [barrier_tensor]},
attrs={'ring_id': ring_id},
)
def get_backend(group: Group | None = None) -> str:
"""
Get the backend of given group.
Args:
group (Group): The group to work on. Use the global group as default.
Returns:
Returns the name of the given group backend.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> paddle.distributed.init_parallel_env()
>>> paddle.distributed.get_backend()
NCCL
"""
if _warn_cur_rank_not_in_group(group):
raise RuntimeError("Invalid group specified")
group = _get_global_group() if group is None else group
return group.backend