470 lines
14 KiB
Python
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
|