chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
@@ -0,0 +1,32 @@
# 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 .all_gather import all_gather, all_gather_object # noqa: F401
from .all_reduce import all_reduce # noqa: F401
from .all_to_all import alltoall, alltoall_single # noqa: F401
from .batch_isend_irecv import P2POp, batch_isend_irecv # noqa: F401
from .broadcast import broadcast, broadcast_object_list # noqa: F401
from .gather import gather # noqa: F401
from .group import ( # noqa: F401
barrier,
destroy_process_group,
get_backend,
get_group,
is_initialized,
wait,
)
from .recv import irecv, recv, recv_object_list # noqa: F401
from .reduce import ReduceOp, reduce # noqa: F401
from .reduce_scatter import reduce_scatter # noqa: F401
from .scatter import scatter, scatter_object_list # noqa: F401
from .send import isend, send, send_object_list # noqa: F401
@@ -0,0 +1,156 @@
# Copyright (c) 2020 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
from typing import TYPE_CHECKING, TypeVar
import numpy as np
import paddle
from paddle import framework
from paddle.distributed.communication import stream
from .serialization_utils import (
convert_object_to_tensor,
convert_tensor_to_object,
)
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
_T = TypeVar("_T")
def all_gather(
tensor_list: list[Tensor],
tensor: Tensor,
group: Group | None = None,
sync_op: bool = True,
) -> task | None:
"""
Gather tensors from all participators and all get the result. As shown
below, one process is started with a GPU and the data of this process is represented
by its group rank. Through the all_gather operator, each GPU will have data
from all GPUs.
.. image:: https://githubraw.cdn.bcebos.com/PaddlePaddle/docs/develop/docs/api/paddle/distributed/img/allgather.png
:width: 800
:alt: all_gather
:align: center
Args:
tensor_list (list): A list of output Tensors. Every element in the list must be a Tensor whose data type
should be float16, float32, float64, int32, int64, int8, uint8, bool, bfloat16, complex64 or complex128.
tensor (Tensor): The Tensor to send. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool, bfloat16, complex64 or complex128.
group (Group|None, optional): The group instance return by new_group or None for global default group.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
None.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> tensor_list = [] # type: ignore
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> dist.all_gather(tensor_list, data)
>>> print(tensor_list)
>>> # [[[4, 5, 6], [4, 5, 6]], [[1, 2, 3], [1, 2, 3]]] (2 GPUs)
"""
return stream.all_gather(tensor_list, tensor, group, sync_op)
def all_gather_object(
object_list: list[_T] | list[None], obj: _T, group: Group = None
) -> None:
"""
Gather picklable objects from all participators and all get the result. Similar to all_gather(), but python object can be passed in.
After the call, ``object_list[i]`` holds the object gathered from rank ``i``. Both
initialization styles below are supported and produce the same result, which is
consistent with :func:`torch.distributed.all_gather_object`:
- Pre-allocated list of length ``world_size`` (PyTorch style):
``object_list = [None for _ in range(dist.get_world_size())]``
- Empty list (Paddle legacy style): ``object_list = []`` - the list is extended in
place to hold ``world_size`` items.
Args:
object_list (list): A list of output object. The datatype of every element in the list is same as the input obj.
obj (Any): The picklable object to send.
group (Group): The group instance return by new_group or None for global default group.
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()
>>> object_list = [None for _ in range(dist.get_world_size())]
>>> if dist.get_rank() == 0:
... obj = {"foo": [1, 2, 3]}
>>> else:
... obj = {"bar": [4, 5, 6]}
>>> dist.all_gather_object(object_list, obj)
>>> print(object_list)
>>> # [{'foo': [1, 2, 3]}, {'bar': [4, 5, 6]}] (2 GPUs)
"""
assert framework.in_dynamic_mode(), (
"all_gather_object doesn't support static graph mode."
)
tensor, len_of_tensor = convert_object_to_tensor(obj)
# gather len_of_tensor from all ranks
list_len_of_tensor = []
all_gather(list_len_of_tensor, len_of_tensor, group)
# get the max length from list
max_len_of_tensor = int(max(list_len_of_tensor).item())
# resize the input tensor to max length avoid hang in all gather
# Note(liyurui): Maybe we should support various length all_gather?
# Now this operation is efficient for we don't support resize in python.
numpy_data = tensor.numpy()
numpy_data = np.resize(numpy_data, [max_len_of_tensor])
input_tensor = paddle.to_tensor(numpy_data)
tensor_list = []
all_gather(tensor_list, input_tensor, group)
# Ensure object_list has enough slots for all gathered objects
while len(object_list) < len(tensor_list):
object_list.append(None)
for i, tensor in enumerate(tensor_list):
object_list[i] = convert_tensor_to_object(tensor, list_len_of_tensor[i])
@@ -0,0 +1,91 @@
# 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
from typing import TYPE_CHECKING
import paddle
from paddle.distributed.communication import stream
from paddle.distributed.communication.reduce import ReduceOp
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
from paddle.distributed.communication.reduce import _ReduceOp
def all_reduce(
tensor: Tensor,
op: _ReduceOp = ReduceOp.SUM,
group: Group | None = None,
sync_op: bool = True,
) -> task:
"""
Reduce a tensor over all ranks so that all get the result.
As shown below, one process is started with a GPU and the data of this process is represented
by its group rank. The reduce operator is sum. Through all_reduce operator,
each GPU will have the sum of the data from all GPUs.
.. image:: https://githubraw.cdn.bcebos.com/PaddlePaddle/docs/develop/docs/api/paddle/distributed/img/allreduce.png
:width: 800
:alt: all_reduce
:align: center
Args:
tensor (Tensor): The input Tensor. It also works as the output Tensor. Its data type
should be float16, float32, float64, int32, int64, int8, uint8 or bool.
op (ReduceOp.SUM|ReduceOp.MAX|ReduceOp.MIN|ReduceOp.PROD|ReduceOp.AVG, optional): The operation used. Default value is ReduceOp.SUM.
group (Group|None, optional): The group instance return by new_group or None for global default group.
sync_op (bool, optional): Whether this op is a sync op. Default value is True.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> dist.all_reduce(data)
>>> print(data)
>>> # [[5, 7, 9], [5, 7, 9]] (2 GPUs)
"""
# AVG is only supported when nccl >= 2.10
if op == ReduceOp.AVG and paddle.base.core.nccl_version() < 21000:
group = (
paddle.distributed.collective._get_global_group()
if group is None
else group
)
tensor.scale_(1.0 / group.nranks)
return stream.all_reduce(
tensor,
op=ReduceOp.SUM,
group=group,
sync_op=sync_op,
use_calc_stream=False,
)
return stream.all_reduce(
tensor, op=op, group=group, sync_op=sync_op, use_calc_stream=False
)
@@ -0,0 +1,178 @@
# 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
from typing import TYPE_CHECKING
from paddle.distributed.communication import stream
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def alltoall(
out_tensor_list: list[Tensor],
in_tensor_list: list[Tensor],
group: Group | None = None,
sync_op: bool = True,
) -> task:
"""
Scatter tensors in in_tensor_list to all participators averagely and gather the result tensors in out_tensor_list.
As shown below, the in_tensor_list in GPU0 includes 0_0 and 0_1, and GPU1 includes 1_0 and 1_1.
Through alltoall operator, the 0_0 in GPU0 will be sent to GPU0 and 0_1 to GPU1, 1_0 in GPU1 sent to GPU0 and 1_1 to GPU1.
Finally the out_tensor_list in GPU0 includes 0_0 and 1_0, and GPU1 includes 0_1 and 1_1.
.. image:: https://githubraw.cdn.bcebos.com/PaddlePaddle/docs/develop/docs/api/paddle/distributed/img/alltoall.png
:width: 800
:alt: alltoall
:align: center
Args:
out_tensor_list (List[Tensor]): List of tensors to be gathered one per rank. The data type of each tensor should be the same as the input tensors.
in_tensor_list (List[Tensor]): List of tensors to scatter one per rank. The data type of each tensor
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
group (Group, optional): The group instance return by new_group or None for global default group. Default: None.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> # all_to_all with equal split sizes
>>> out_tensor_list = [] # type: ignore
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([[1, 2, 3], [4, 5, 6]])
... data2 = paddle.to_tensor([[7, 8, 9], [10, 11, 12]])
>>> else:
... data1 = paddle.to_tensor([[13, 14, 15], [16, 17, 18]])
... data2 = paddle.to_tensor([[19, 20, 21], [22, 23, 24]])
>>> dist.alltoall(out_tensor_list, [data1, data2])
>>> print(out_tensor_list)
>>> # [[[1, 2, 3], [4, 5, 6]], [[13, 14, 15], [16, 17, 18]]] (2 GPUs, out for rank 0)
>>> # [[[7, 8, 9], [10, 11, 12]], [[19, 20, 21], [22, 23, 24]]] (2 GPUs, out for rank 1)
>>> # all_to_all with unequal split sizes
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([[1, 2, 3], [4, 5, 6]]) # shape: (2, 3)
... data2 = paddle.to_tensor([7]) # shape: (1, )
... out_data1 = paddle.empty((2, 3), dtype=data1.dtype)
... out_data2 = paddle.empty((3, 2), dtype=data1.dtype)
>>> else:
... data1 = paddle.to_tensor([[8, 9], [10, 11], [12, 13]]) # shape: (3, 2)
... data2 = paddle.to_tensor([[14, 15, 16, 17]]) # shape: (1, 4)
... out_data1 = paddle.empty((1,), dtype=data1.dtype)
... out_data2 = paddle.empty((1, 4), dtype=data1.dtype)
>>> dist.alltoall([out_data1, out_data2], [data1, data2])
>>> print([out_data1, out_data2])
>>> # [[[1, 2, 3], [4, 5, 6]], [[8, 9], [10, 11], [12, 13]]] (2 GPUs, out for rank 0)
>>> # [[7], [[14, 15, 16, 17]]] (2 GPUs, out for rank 1)
"""
return stream.alltoall(
out_tensor_list, in_tensor_list, group, sync_op, False
)
def alltoall_single(
out_tensor: Tensor,
in_tensor: Tensor,
in_split_sizes: list[int] | None = None,
out_split_sizes: list[int] | None = None,
group: Group | None = None,
sync_op: bool = True,
) -> task:
"""
Scatter a single input tensor to all participators and gather the received tensors in out_tensor.
Note:
``alltoall_single`` is only supported in eager mode.
Args:
out_tensor (Tensor): Output Tensor. The data type should be the same as the data type of the input Tensor.
in_tensor (Tensor): Input tensor. The data type should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
in_split_sizes (list[int]|None, optional): Split sizes of ``in_tensor`` for dim[0]. If not given, dim[0] of ``in_tensor``
must be divisible by group size and ``in_tensor`` will be scattered averagely to all participators. Default: None.
out_split_sizes (list[int]|None, optional): Split sizes of ``out_tensor`` for dim[0]. If not given, dim[0] of ``out_tensor``
must be divisible by group size and ``out_tensor`` will be gathered averagely from all participators. Default: None.
group (Group|None, optional): The group instance return by ``new_group`` or None for global default group. Default: None.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> rank = dist.get_rank()
>>> size = dist.get_world_size()
>>> # case 1 (2 GPUs)
>>> data = paddle.arange(2, dtype='int64') + rank * 2
>>> # data for rank 0: [0, 1]
>>> # data for rank 1: [2, 3]
>>> output = paddle.empty([2], dtype='int64')
>>> dist.alltoall_single(output, data)
>>> print(output)
>>> # output for rank 0: [0, 2]
>>> # output for rank 1: [1, 3]
>>> # case 2 (2 GPUs)
>>> in_split_sizes = [i + 1 for i in range(size)]
>>> # in_split_sizes for rank 0: [1, 2]
>>> # in_split_sizes for rank 1: [1, 2]
>>> out_split_sizes = [rank + 1 for i in range(size)]
>>> # out_split_sizes for rank 0: [1, 1]
>>> # out_split_sizes for rank 1: [2, 2]
>>> data = paddle.ones([sum(in_split_sizes), size], dtype='float32') * rank
>>> # data for rank 0: [[0., 0.], [0., 0.], [0., 0.]]
>>> # data for rank 1: [[1., 1.], [1., 1.], [1., 1.]]
>>> output = paddle.empty([(rank + 1) * size, size], dtype='float32')
>>> group = dist.new_group([0, 1])
>>> task = dist.alltoall_single(
... data,
... output,
... in_split_sizes,
... out_split_sizes,
... sync_op=False,
... group=group,
... )
>>> task.wait()
>>> print(output)
>>> # output for rank 0: [[0., 0.], [1., 1.]]
>>> # output for rank 1: [[0., 0.], [0., 0.], [1., 1.], [1., 1.]]
"""
return stream.alltoall_single(
out_tensor,
in_tensor,
out_split_sizes,
in_split_sizes,
group,
sync_op,
False,
)
@@ -0,0 +1,204 @@
# 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 contextlib
from typing import TYPE_CHECKING
import paddle.distributed as dist
from paddle import framework
from paddle.distributed.communication.group import (
_get_global_group,
_warn_cur_rank_not_in_group,
)
if TYPE_CHECKING:
from collections.abc import Callable, Generator, Sequence
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed import Group
_P2POpType = Callable[[Tensor, int, Group], task]
class P2POp:
"""
A class that makes point-to-point operations for "batch_isend_irecv".
This class creates the type of P2P operation, communication buffer, peer rank,
Group. Instances of this class will be passed to
``paddle.distributed.batch_isend_irecv`` for point-to-point communication.
Args:
op (callable): A function to send data to or receive data from a peer process.
The type of ``op`` is either ``paddle.distributed.isend`` or ``paddle.distributed.irecv``.
tensor (Tensor): Tensor to send or receive.
peer (int): The destination or source rank.
group (Group, optional): The group instance return by new_group or None for global
default group. Default: None.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> rank = dist.get_rank()
>>> world_size = dist.get_world_size()
>>> send_t = paddle.arange(2) + rank
>>> # paddle.tensor([0, 1]) # Rank-0
>>> # paddle.tensor([1, 2]) # Rank-1
>>> recv_t = paddle.empty(shape=[2], dtype=send_t.dtype)
>>> send_op = dist.P2POp(dist.isend, send_t, (rank + 1) % world_size)
>>> recv_op = dist.P2POp(dist.irecv, recv_t, (rank - 1 + world_size) % world_size)
"""
op: _P2POpType
tensor: Tensor
peer: int
group: Group | None
def __init__(
self,
op: _P2POpType,
tensor: Tensor,
peer: int,
group: Group | None = None,
) -> None:
if op not in [dist.isend, dist.irecv]:
raise RuntimeError(
"Invalid ``op`` function. Expected ``op`` "
"to be of type ``paddle.distributed.isend`` or "
"``paddle.distributed.irecv``."
)
self.op = op
self.tensor = tensor
self.peer = peer
self.group = _get_global_group() if group is None else group
@contextlib.contextmanager
def _coalescing_manager(
group: Group, tasks: task | None = None
) -> Generator[None, None, None]:
group = _get_global_group() if group is None else group
pg = group.process_group
pg._start_coalescing()
try:
yield
finally:
if tasks is None or len(tasks) == 0:
pg._end_coalescing()
else:
pg._end_coalescing(tasks)
def _check_p2p_op_list(p2p_op_list: Sequence[P2POp]) -> None:
"""
Helper to check that the ``p2p_op_list`` is a list of P2POp instances and
all ops use the same backend.
"""
if not isinstance(p2p_op_list, list) or not all(
isinstance(p2p_op, P2POp) for p2p_op in p2p_op_list
):
raise RuntimeError(
"Invalid ``p2p_op_list``. Each op is expected to "
"to be of type ``paddle.distributed.P2POp``."
)
backend = p2p_op_list[0].group.backend
if not all(backend == p2p_op.group.backend for p2p_op in p2p_op_list):
raise RuntimeError("All groups need to use the same backend.")
def batch_isend_irecv(p2p_op_list: list[P2POp]) -> list[task]:
"""
Send or Receive a batch of tensors asynchronously and return a list of requests.
Process each of the point-to-point operations in ``p2p_op_list`` and return the
corresponding tasks. NCCL are currently supported.
Args:
p2p_op_list (List[P2POp]): A list of point-to-point operations(type of each operator is
``paddle.distributed.P2POp``). The order of the isend/irecv in the list
matters and it needs to match with corresponding isend/irecv on the
remote end.
Returns:
A list of distributed tasks returned by calling the corresponding
op in the op_list.
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()
>>> rank = dist.get_rank()
>>> world_size = dist.get_world_size()
>>> send_t = paddle.arange(2) + rank
>>> # paddle.tensor([0, 1]) # Rank-0
>>> # paddle.tensor([1, 2]) # Rank-1
>>> recv_t = paddle.empty(shape=[2], dtype=send_t.dtype)
>>> send_op = dist.P2POp(dist.isend, send_t, (rank + 1) % world_size)
>>> recv_op = dist.P2POp(dist.irecv, recv_t, (rank - 1 + world_size) % world_size)
>>> tasks = dist.batch_isend_irecv([send_op, recv_op])
>>> for task in tasks:
... task.wait()
>>> print(recv_t)
>>> # paddle.tensor([1, 2]) # Rank-0
>>> # paddle.tensor([0, 1]) # Rank-1
"""
_check_p2p_op_list(p2p_op_list)
group = p2p_op_list[0].group
if _warn_cur_rank_not_in_group(group):
return
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
backend = group.backend
tasks = []
with _coalescing_manager(group, tasks):
for p2p_op in p2p_op_list:
op = p2p_op.op
tensor = p2p_op.tensor
peer = p2p_op.peer
comm_group = p2p_op.group
task = op(tensor, peer, comm_group)
if task is not None:
tasks.append(task)
return tasks
else:
raise RuntimeError("Don't support static graph mode currently.")
@@ -0,0 +1,149 @@
# 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
from typing import TYPE_CHECKING, Any
import paddle
import paddle.distributed as dist
from paddle import framework
from paddle.distributed.communication import stream
from .serialization_utils import (
convert_object_to_tensor,
convert_tensor_to_object,
)
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def broadcast(
tensor: Tensor, src: int, group: Group | None = None, sync_op: bool = True
) -> task:
"""
Broadcast a tensor from the source to all others.
As shown below, one process is started with a GPU and GPU0 owns data 0. Through broadcast operator,
data 0 will be sent to all GPUs from GPU0.
.. image:: https://githubraw.cdn.bcebos.com/PaddlePaddle/docs/develop/docs/api/paddle/distributed/img/broadcast.png
:width: 800
:alt: broadcast
:align: center
Args:
tensor (Tensor): The tensor to send if current rank is the source, or the tensor to receive otherwise. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool, bfloat16, complex64 or complex128.
src (int): The source rank in global view.
group (Group, optional): The group instance return by new_group or None for global default group.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> dist.broadcast(data, src=1)
>>> print(data)
>>> # [[1, 2, 3], [1, 2, 3]] (2 GPUs)
"""
return stream.broadcast(
tensor,
src,
group=group,
sync_op=sync_op,
use_calc_stream=False,
)
def broadcast_object_list(
object_list: list[Any], src: int, group: Group | None = None
) -> None:
"""
Broadcast picklable objects from the source to all others. Similar to broadcast(), but python object can be passed in.
Args:
object_list (list): The list of objects to send if current rank is the source, or the list of objects to receive otherwise.
src (int): The source rank in global view.
group (Group): The group instance return by new_group or None for global default group.
Returns:
None.
Warning:
This API only supports the dygraph mode.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... object_list = [{"foo": [1, 2, 3]}]
>>> else:
... object_list = [{"bar": [4, 5, 6]}]
>>> dist.broadcast_object_list(object_list, src=1)
>>> print(object_list)
>>> # [{"bar": [4, 5, 6]}] (2 GPUs)
"""
assert framework.in_dynamic_mode(), (
"broadcast_object_list doesn't support static graph mode."
)
rank = dist.get_rank()
obj_tensors = []
obj_nums = len(object_list)
if rank == src:
obj_sizes = []
for obj in object_list:
obj_tensor, obj_size = convert_object_to_tensor(obj)
obj_tensors.append(obj_tensor)
obj_sizes.append(obj_size)
obj_size_tensor = paddle.stack(obj_sizes)
else:
obj_size_tensor = paddle.empty([obj_nums], dtype="int64")
broadcast(obj_size_tensor, src, group)
if rank == src:
# cast to uint8 to keep the same dtype
obj_data_tensor = paddle.concat(obj_tensors).cast("uint8")
else:
data_len = paddle.sum(obj_size_tensor).item()
obj_data_tensor = paddle.empty([data_len], dtype="uint8")
broadcast(obj_data_tensor, src, group)
offset = 0
for i in range(obj_nums):
data_len = obj_size_tensor[i]
object_list[i] = convert_tensor_to_object(
obj_data_tensor[offset : offset + data_len], data_len
)
offset += data_len
@@ -0,0 +1,31 @@
# Copyright (c) 2025 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 .buffer import Buffer, M2NBuffer
from .utils import (
EventOverlap,
get_event_from_calc_stream,
get_event_from_comm_stream,
get_event_from_custom_stream,
)
__all__ = [
"Buffer",
"M2NBuffer",
"EventOverlap",
"get_event_from_calc_stream",
"get_event_from_comm_stream",
"get_event_from_custom_stream",
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,109 @@
# Copyright (c) 2025 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.
# The file has been adapted from DeepSeek DeepEP project
# Copyright (c) 2025 DeepSeek
# Licensed under the MIT License - https://github.com/deepseek-ai/DeepEP/blob/main/LICENSE
from __future__ import annotations
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
import paddle
from paddle.base.core import EventHandle
import paddle
class EventOverlap:
"""
A wrapper class to manage CUDA events, also for better overlapping convenience.
Attributes:
event: the CUDA event captured.
extra_tensors: an easier way to simulate tensor `record_stream`, may be useful with CUDA graph.
"""
def __init__(
self,
event: EventHandle | None = None,
extra_tensors: tuple[paddle.Tensor] | None = None,
) -> None:
"""
Initialize the class.
Arguments:
event: the CUDA event captured.
extra_tensors: an easier way to simulate tensor `record_stream`, may be useful with CUDA graph.
"""
self.event = event
# NOTES: we use extra tensors to achieve stream recording, otherwise,
# stream recording will be incompatible with CUDA graph.
self.extra_tensors = extra_tensors
def current_stream_wait(self) -> None:
"""
The current stream waits for the event to be finished.
"""
assert self.event is not None
self.event.current_stream_wait()
def calc_stream_wait(self, group_idx) -> None:
self.event.calc_stream_wait(group_idx)
def comm_stream_wait(self, group_idx) -> None:
self.event.comm_stream_wait(group_idx)
def __enter__(self) -> Any:
"""
Utility for overlapping and Python `with` syntax.
You can overlap the kernels on the current stream with the following example:
```python
event_overlap = event_after_all_to_all_kernels()
with event_overlap():
do_something_on_current_stream()
# After exiting the `with` scope, the current stream with wait the event to be finished.
```
"""
return self
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
"""
Utility for overlapping and Python `with` syntax.
Please follow the example in the `__enter__` function.
"""
if self.event is not None:
self.event.current_stream_wait()
def get_event_from_calc_stream(group_id: int) -> EventOverlap:
return EventOverlap(
event=paddle.base.core.get_event_handle_from_calc_stream(group_id)
)
def get_event_from_comm_stream(group_id: int) -> EventOverlap:
return EventOverlap(
event=paddle.base.core.get_event_handle_from_comm_stream(group_id)
)
def get_event_from_custom_stream(stream) -> EventOverlap:
return EventOverlap(
event=paddle.base.core.get_event_handle_from_custom_stream(stream)
)
@@ -0,0 +1,75 @@
# Copyright (c) 2023 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
from typing import TYPE_CHECKING
from paddle import framework
from paddle.distributed.communication import stream
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def gather(
tensor: Tensor,
gather_list: list[Tensor] | None = None,
dst: int = 0,
group: Group | None = None,
sync_op: bool = True,
) -> task | None:
"""
Gather tensors from all participators.
Args:
tensor (Tensor): The input Tensor. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
gather_list (list): A list of Tensors to hold the gathered tensors. Every element in the list must be a Tensor whose data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16. Default value is None.
dst (int): The dst rank id. Default value is 0.
group (Group, optional): The group instance return by new_group or None for global default group.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
Async work handle,which can be wait on, if async_op is set to True.
None, if not async_op
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> gather_list = [] # type: ignore
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([1, 2, 3])
... dist.gather(data, gather_list, dst=0)
>>> else:
... data = paddle.to_tensor([4, 5, 6])
... dist.gather(data, gather_list, dst=0)
>>> print(gather_list)
>>> # [[1, 2, 3], [4, 5, 6]] (2 GPUs, out for rank 0)
>>> # [] (2 GPUs, out for rank 1)
"""
assert framework.in_dynamic_mode(), (
"gather doesn't support static graph mode yet."
)
return stream.gather(tensor, gather_list, dst, group, sync_op)
@@ -0,0 +1,469 @@
# 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
@@ -0,0 +1,178 @@
# 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
from typing import TYPE_CHECKING, Any
import paddle
from paddle.distributed.communication import stream
from paddle.distributed.communication.group import (
_get_global_group,
_warn_cur_rank_not_in_group,
)
from paddle.distributed.communication.serialization_utils import (
convert_tensor_to_object,
)
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def recv(
tensor: Tensor,
src: int = 0,
group: Group | None = None,
sync_op: bool = True,
) -> task:
"""
Receive a tensor to the sender.
Args:
tensor (Tensor): The tensor to receive. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
src (int): The source rank id.
group (Group, optional): The group instance return by new_group or None for global default group. Default: None.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([7, 8, 9])
... dist.send(data, dst=1)
>>> else:
... data = paddle.to_tensor([1, 2, 3])
... dist.recv(data, src=0)
>>> print(data)
>>> # [7, 8, 9] (2 GPUs)
"""
return stream.recv(
tensor, src=src, group=group, sync_op=sync_op, use_calc_stream=False
)
def irecv(
tensor: Tensor, src: int | None = None, group: Group | None = None
) -> task:
"""
Receive a tensor to the sender.
Args:
tensor (Tensor): The Tensor to receive. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
src (int): The source rank id.
group (Group, optional): The group instance return by new_group or None for global default group. Default: None.
Returns:
Return a task object.
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()
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([7, 8, 9])
... task = dist.isend(data, dst=1)
>>> else:
... data = paddle.to_tensor([1, 2, 3])
... task = dist.irecv(data, src=0)
>>> task.wait() # type: ignore[union-attr]
>>> print(data)
>>> # [7, 8, 9] (2 GPUs)
"""
return recv(tensor, src, group, sync_op=False)
def recv_object_list(
object_list: list[Any],
src: int | None = None,
group: Group | None = None,
src_in_group: int | None = None,
):
"""
Receive a list of Python objects from the sender.
Args:
object_list (list): The list to store received objects. Must be pre-allocated with correct size.
src (int, optional): The source rank id. Default: 0.
group (Group, optional): The group instance return by new_group or None for global default group. Default: None.
src_in_group (int, optional): The source rank within the group. Cannot be specified together with src. Default: None.
Returns:
This function does not return any value.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data = ["hello", {"key": 100}, [1, 2, 3]]
... dist.send_object_list(data, dst=1)
>>> else:
... data = [None] * 3
... dist.recv_object_list(data, src=0)
>>> print(data)
>>> # ["hello", {"key": 100}, [1, 2, 3]] (2 GPUs)
"""
if object_list is None or len(object_list) == 0:
raise ValueError("object_list cannot be None or empty")
group = _get_global_group() if group is None else group
if _warn_cur_rank_not_in_group(group):
return
if src_in_group is not None:
if src is not None:
raise ValueError(
"Cannot specify both 'src' and 'src_in_group' arguments."
)
src = group.get_global_rank(src_in_group)
else:
src = 0 if src is None else src
object_sizes_tensor = paddle.empty((len(object_list),), dtype='int64')
recv(object_sizes_tensor, src=src, group=group)
total_size = paddle.sum(object_sizes_tensor).item()
object_tensor = paddle.empty((total_size,), dtype=paddle.uint8)
recv(object_tensor, src=src, group=group)
offset = 0
for i, obj_size in enumerate(object_sizes_tensor):
obj_size = obj_size.item()
obj_view = object_tensor[offset : offset + obj_size]
object_list[i] = convert_tensor_to_object(obj_view, obj_size)
offset += obj_size
@@ -0,0 +1,194 @@
# 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
from typing import TYPE_CHECKING, ClassVar, Literal
import paddle
from paddle import framework
from paddle.distributed.communication import stream
if TYPE_CHECKING:
from typing import TypeAlias
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
_ReduceOp: TypeAlias = Literal[0, 1, 2, 3, 4]
class ReduceOp:
"""
Specify the type of operation used for element-wise reductions.
It should be one of the following values:
ReduceOp.SUM
ReduceOp.MAX
ReduceOp.MIN
ReduceOp.PROD
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> dist.all_reduce(data, op=dist.ReduceOp.SUM)
>>> print(data)
>>> # [[5, 7, 9], [5, 7, 9]] (2 GPUs)
"""
SUM: ClassVar[Literal[0]] = 0
MAX: ClassVar[Literal[1]] = 1
MIN: ClassVar[Literal[2]] = 2
PROD: ClassVar[Literal[3]] = 3
AVG: ClassVar[Literal[4]] = 4
def _get_reduce_op(reduce_op):
if reduce_op == ReduceOp.SUM:
return framework.core.ReduceOp.SUM
elif reduce_op == ReduceOp.MAX:
return framework.core.ReduceOp.MAX
elif reduce_op == ReduceOp.MIN:
return framework.core.ReduceOp.MIN
elif reduce_op == ReduceOp.PROD:
return framework.core.ReduceOp.PRODUCT
elif reduce_op == ReduceOp.AVG:
return framework.core.ReduceOp.AVG
raise ValueError(f"Unknown reduce_op type for {reduce_op}.")
def _to_inplace_op(op_name):
return f"{op_name}_"
def reduce(
tensor: Tensor,
dst: int,
op: _ReduceOp = ReduceOp.SUM,
group: Group | None = None,
sync_op: bool = True,
) -> task:
"""
Reduce a tensor to the destination from all others. As shown below, one process is started with a GPU and the data of this process is represented
by its group rank. The destination of the reduce operator is GPU0 and the process is sum. Through reduce operator,
the GPU0 will owns the sum of all data from all GPUs.
.. image:: https://githubraw.cdn.bcebos.com/PaddlePaddle/docs/develop/docs/api/paddle/distributed/img/reduce.png
:width: 800
:alt: reduce
:align: center
Args:
tensor (Tensor): The output Tensor for the destination and the input Tensor otherwise. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
dst (int): The destination rank id.
op (ReduceOp.SUM|ReduceOp.MAX|ReduceOp.MIN|ReduceOp.PROD|ReduceOp.AVG, optional): The operation used. Default value is ReduceOp.SUM.
group (Group|None, optional): The group instance return by new_group or None for global default group.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> dist.reduce(data, dst=0)
>>> print(data)
>>> # [[5, 7, 9], [5, 7, 9]] (2 GPUs, out for rank 0)
>>> # [[1, 2, 3], [1, 2, 3]] (2 GPUs, out for rank 1)
"""
# AVG is only supported when nccl >= 2.10
if op == ReduceOp.AVG and (not is_avg_reduce_op_supported()):
group = (
paddle.distributed.collective._get_global_group()
if group is None
else group
)
tensor.scale_(1.0 / group.nranks)
return stream.reduce(
tensor,
dst=dst,
op=ReduceOp.SUM,
group=group,
sync_op=sync_op,
use_calc_stream=False,
)
return stream.reduce(
tensor,
dst=dst,
op=op,
group=group,
sync_op=sync_op,
use_calc_stream=False,
)
# code below will be removed after we remove the old dygraph
if group is not None and not group.is_member():
return
use_calc_stream = sync_op
ring_id = 0 if group is None else group.id
gdst = dst if group is None else group.get_group_rank(dst)
assert gdst >= 0, "dst rank out of group, need global rank"
if (
op == ReduceOp.SUM
or op == ReduceOp.MAX
or op == ReduceOp.MIN
or op == ReduceOp.PROD
or op == ReduceOp.AVG
):
return paddle._C_ops.reduce(
tensor,
tensor,
'ring_id',
ring_id,
'root_id',
gdst,
'reduce_type',
op,
)
else:
raise ValueError(f"Unknown parameter: {op}.")
def is_avg_reduce_op_supported() -> bool:
if paddle.is_compiled_with_cuda():
return paddle.base.core.nccl_version() >= 21000
else:
return False
@@ -0,0 +1,168 @@
# 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
from typing import TYPE_CHECKING
import paddle
from paddle.distributed.communication import stream
from paddle.distributed.communication.reduce import ReduceOp
from paddle.distributed.communication.stream.reduce_scatter import (
_reduce_scatter_base as _reduce_scatter_base_stream,
)
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
from paddle.distributed.communication.reduce import _ReduceOp
def reduce_scatter(
tensor: Tensor,
tensor_list: list[Tensor],
op: _ReduceOp = ReduceOp.SUM,
group: Group | None = None,
sync_op: bool = True,
) -> task:
"""
Reduces, then scatters a list of tensors to all processes in a group
Args:
tensor (Tensor): The output tensor on each rank. The result will overwrite this tenor after communication. Support
float16, float32, float64, int32, int64, int8, uint8 or bool as the input data type.
tensor_list (List[Tensor]]): List of tensors to reduce and scatter. Every element in the list must be a Tensor whose data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
op (ReduceOp.SUM|ReduceOp.MAX|ReduceOp.MIN|ReduceOp.PROD|ReduceOp.AVG, optional): The reduction used. If none is given, use ReduceOp.SUM as default.
group (Group, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
Returns:
Return a task object.
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()
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([0, 1])
... data2 = paddle.to_tensor([2, 3])
>>> else:
... data1 = paddle.to_tensor([4, 5])
... data2 = paddle.to_tensor([6, 7])
>>> dist.reduce_scatter(data1, [data1, data2])
>>> print(data1)
>>> # [4, 6] (2 GPUs, out for rank 0)
>>> # [8, 10] (2 GPUs, out for rank 1)
"""
if op not in [
ReduceOp.AVG,
ReduceOp.MAX,
ReduceOp.MIN,
ReduceOp.PROD,
ReduceOp.SUM,
]:
raise RuntimeError(
"Invalid ``op`` function. Expected ``op`` to be of type ``ReduceOp.SUM``, ``ReduceOp.Max``, ``ReduceOp.MIN``, ``ReduceOp.PROD`` or ``ReduceOp.AVG``."
)
# AVG is only supported when nccl >= 2.10
if op == ReduceOp.AVG and paddle.base.core.nccl_version() < 21000:
group = (
paddle.distributed.collective._get_global_group()
if group is None
else group
)
tensor.scale_(1.0 / group.nranks)
return stream.reduce_scatter(
tensor,
tensor_list,
op=ReduceOp.SUM,
group=group,
sync_op=sync_op,
use_calc_stream=False,
)
return stream.reduce_scatter(
tensor,
tensor_list,
op=op,
group=group,
sync_op=sync_op,
use_calc_stream=False,
)
def _reduce_scatter_base(
output: Tensor,
input: Tensor,
op: _ReduceOp = ReduceOp.SUM,
group: Group | None = None,
sync_op: bool = True,
) -> task | None:
"""
Reduces, then scatters a flattened tensor to all processes in a group.
Args:
output (Tensor): Output tensor. Its data type should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
input (Tensor): Input tensor that is of size output tensor size times world size. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
op (ReduceOp.SUM|ReduceOp.MAX|ReduceOp.MIN|ReduceOp.PROD): Optional. The operation used. Default: ReduceOp.SUM.
group (ProcessGroup, optional): The process group to work on. If None,
the default process group will be used.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
Async task handle, if sync_op is set to False.
None, if sync_op or if not part of the group.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> rank = dist.get_rank()
>>> data = paddle.arange(4) + rank
>>> # [0, 1, 2, 3] (2 GPUs, for rank 0)
>>> # [1, 2, 3, 4] (2 GPUs, for rank 1)
>>> output = paddle.empty(shape=[2], dtype=data.dtype)
>>> dist.collective._reduce_scatter_base(output, data)
>>> print(output)
>>> # [1, 3] (2 GPUs, out for rank 0)
>>> # [5, 7] (2 GPUs, out for rank 1)
"""
if op not in [ReduceOp.MAX, ReduceOp.MIN, ReduceOp.PROD, ReduceOp.SUM]:
raise RuntimeError(
"Invalid ``op`` function. Expected ``op`` to be of type ``ReduceOp.SUM``, ``ReduceOp.Max``, ``ReduceOp.MIN`` or ``ReduceOp.PROD``."
)
return _reduce_scatter_base_stream(
output,
input,
op=op,
group=group,
sync_op=sync_op,
use_calc_stream=False,
)
@@ -0,0 +1,165 @@
# 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
from typing import TYPE_CHECKING, Any
if TYPE_CHECKING:
from collections.abc import Sequence
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
import numpy as np
import paddle
import paddle.distributed as dist
from paddle import framework
from paddle.distributed.communication import stream
from .serialization_utils import (
convert_object_to_tensor,
convert_tensor_to_object,
)
def scatter(
tensor: Tensor,
tensor_list: Sequence[Tensor] | None = None,
src: int = 0,
group: Group | None = None,
sync_op: bool = True,
) -> task | None:
"""
Scatter a tensor to all participators. As shown below, one process is started with a GPU and the source of the scatter
is GPU0. Through scatter operator, the data in GPU0 will be sent to all GPUs averagely.
.. image:: https://githubraw.cdn.bcebos.com/PaddlePaddle/docs/develop/docs/api/paddle/distributed/img/scatter.png
:width: 800
:alt: scatter
:align: center
Args:
tensor (Tensor): The output Tensor. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
tensor_list (list|tuple): A list/tuple of Tensors to scatter. Every element in the list must be a Tensor whose data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16. Default value is None.
src (int): The source rank id. Default value is 0.
group (Group, optional): The group instance return by new_group or None for global default group.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
None.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([7, 8, 9])
... data2 = paddle.to_tensor([10, 11, 12])
... dist.scatter(data1, src=1)
>>> else:
... data1 = paddle.to_tensor([1, 2, 3])
... data2 = paddle.to_tensor([4, 5, 6])
... dist.scatter(data1, tensor_list=[data1, data2], src=1)
>>> print(data1, data2)
>>> # [1, 2, 3] [10, 11, 12] (2 GPUs, out for rank 0)
>>> # [4, 5, 6] [4, 5, 6] (2 GPUs, out for rank 1)
"""
return stream.scatter(tensor, tensor_list, src, group, sync_op)
def scatter_object_list(
out_object_list: list[Any],
in_object_list: list[Any] | None = None,
src: int = 0,
group: Group | None = None,
) -> None:
"""
Scatter picklable objects from the source to all others. Similar to scatter(), but python object can be passed in.
Args:
out_object_list (list): The list of objects to store the scattered objects.
in_object_list (list): The list of objects to scatter. Only objects on the src rank will be scattered.
src (int): The source rank in global view.
group (Group): The group instance return by new_group or None for global default group.
Returns:
None.
Warning:
This API only supports the dygraph mode.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> out_object_list = [] # type: ignore
>>> if dist.get_rank() == 0:
... in_object_list = [{'foo': [1, 2, 3]}, {'foo': [4, 5, 6]}]
>>> else:
... in_object_list = [{'bar': [1, 2, 3]}, {'bar': [4, 5, 6]}]
>>> dist.scatter_object_list(out_object_list, in_object_list, src=1)
>>> print(out_object_list)
>>> # [{'bar': [1, 2, 3]}] (2 GPUs, out for rank 0)
>>> # [{'bar': [4, 5, 6]}] (2 GPUs, out for rank 1)
"""
assert framework.in_dynamic_mode(), (
"scatter_object_list doesn't support static graph mode."
)
rank = dist.get_rank()
in_obj_tensors = []
in_obj_sizes = []
if rank == src:
for obj in in_object_list:
obj_tensor, obj_size = convert_object_to_tensor(obj)
in_obj_tensors.append(obj_tensor)
in_obj_sizes.append(obj_size)
max_obj_size_tensor = max(in_obj_sizes)
else:
max_obj_size_tensor = paddle.empty([], dtype="int64")
stream.broadcast(max_obj_size_tensor, src)
max_obj_size = int(max_obj_size_tensor.item())
# resize to the same size
in_tensor_list = []
for tensor in in_obj_tensors:
numpy_data = tensor.numpy()
numpy_data = np.resize(numpy_data, [max_obj_size])
in_tensor = paddle.to_tensor(numpy_data)
in_tensor_list.append(in_tensor)
out_tensor = paddle.empty([max_obj_size], dtype="uint8")
scatter(out_tensor, in_tensor_list if rank == src else None, src, group)
out_tensor_size = paddle.empty([], dtype="int64")
scatter(out_tensor_size, in_obj_sizes if rank == src else None, src, group)
out_object_list.clear()
out_object_list.append(
convert_tensor_to_object(out_tensor, out_tensor_size.item())
)
@@ -0,0 +1,180 @@
# 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
from typing import TYPE_CHECKING, Any
import paddle
from paddle.distributed.communication import stream
from paddle.distributed.communication.group import (
_get_global_group,
_warn_cur_rank_not_in_group,
)
from paddle.distributed.communication.serialization_utils import (
convert_object_to_tensor,
)
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def send(
tensor: Tensor,
dst: int = 0,
group: Group | None = None,
sync_op: bool = True,
) -> task | None:
"""
Send a tensor to the receiver.
Args:
tensor (Tensor): The Tensor to send. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
dst (int): The destination rank id.
group (Group, optional): The group instance return by new_group or None for global default group. Default: None.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([7, 8, 9])
... dist.send(data, dst=1)
>>> else:
... data = paddle.to_tensor([1, 2, 3])
... dist.recv(data, src=0)
>>> print(data)
>>> # [7, 8, 9] (2 GPUs)
"""
return stream.send(
tensor, dst=dst, group=group, sync_op=sync_op, use_calc_stream=False
)
def isend(tensor: Tensor, dst: int, group: Group | None = None) -> task | None:
"""
Send tensor asynchronously
Args:
tensor (Tensor): The Tensor to send. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
dst (int): The destination rank.
group (Group, optional): The group instance return by new_group or None for global default group. Default: None.
Returns:
Return a task object.
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()
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([7, 8, 9])
... task = dist.isend(data, dst=1)
>>> else:
... data = paddle.to_tensor([1, 2, 3])
... task = dist.irecv(data, src=0)
>>> task.wait() # type: ignore[union-attr]
>>> print(data)
>>> # [7, 8, 9] (2 GPUs)
"""
return send(tensor, dst, group, sync_op=False)
def send_object_list(
object_list: list[Any],
dst: int | None = None,
group: Group | None = None,
dst_in_group: int | None = None,
):
"""
Send a list of Python objects to the receiver.
Args:
object_list (list): The list of Python objects to send.
dst (int, optional): The destination rank id. Default: 0.
group (Group, optional): The group instance return by new_group or None for global default group. Default: None.
dst_in_group (int, optional): The destination rank within the group. Cannot be specified together with dst. Default: None.
Returns:
This function does not return any value.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data = ["hello", {"key": 100}, [1, 2, 3]]
... dist.send_object_list(data, dst=1)
>>> else:
... data = [None] * 3
... dist.recv_object_list(data, src=0)
>>> print(data)
>>> # ["hello", {"key": 100}, [1, 2, 3]] (2 GPUs)
"""
if object_list is None or len(object_list) == 0:
raise ValueError("object_list cannot be None or empty")
group = _get_global_group() if group is None else group
if _warn_cur_rank_not_in_group(group):
return
if dst_in_group is not None:
if dst is not None:
raise ValueError(
"Cannot specify both 'dst' and 'dst_in_group' arguments."
)
dst = group.get_global_rank(dst_in_group)
else:
dst = 0 if dst is None else dst
# Convert objects to tensors and get their sizes
tensor_list, size_list = zip(
*[convert_object_to_tensor(obj) for obj in object_list]
)
size_list_values = [size.item() for size in size_list]
# Send sizes first
object_sizes_tensor = paddle.to_tensor(size_list_values, dtype='int64')
send(object_sizes_tensor, dst=dst, group=group)
# Send object data
if len(tensor_list) == 1:
object_tensor = tensor_list[0]
else:
object_tensor = paddle.concat(tensor_list)
send(object_tensor, dst=dst, group=group)
@@ -0,0 +1,34 @@
# 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.
import io
import pickle
import numpy as np
import paddle
def convert_object_to_tensor(obj):
_pickler = pickle.Pickler
f = io.BytesIO()
_pickler(f).dump(obj)
data = np.frombuffer(f.getvalue(), dtype=np.uint8)
tensor = paddle.to_tensor(data)
return tensor, tensor.numel()
def convert_tensor_to_object(tensor, len_of_tensor):
_unpickler = pickle.Unpickler
return _unpickler(io.BytesIO(tensor.numpy()[:len_of_tensor])).load()
@@ -0,0 +1,38 @@
# 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 .all_gather import all_gather
from .all_reduce import all_reduce
from .all_to_all import alltoall, alltoall_single
from .broadcast import broadcast
from .gather import gather
from .recv import recv
from .reduce import reduce
from .reduce_scatter import reduce_scatter
from .scatter import scatter
from .send import send
__all__ = [
"all_gather",
"all_reduce",
"alltoall",
"alltoall_single",
"broadcast",
"reduce",
"reduce_scatter",
"recv",
"scatter",
"send",
"gather",
]
@@ -0,0 +1,220 @@
# 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
from typing import TYPE_CHECKING
import paddle
import paddle.distributed as dist
from paddle import framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import _get_global_group
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
from paddle.distributed.utils.stream_utils import ExecutionStreamType
def _all_gather_into_tensor_in_dygraph(
out_tensor: Tensor,
in_tensor: Tensor,
group: Group,
sync_op: bool,
use_calc_stream: bool,
) -> task:
group = _get_global_group() if group is None else group
if use_calc_stream:
return group.process_group.all_gather_into_tensor_on_calc_stream(
out_tensor,
in_tensor,
)
task = group.process_group.all_gather_into_tensor(
out_tensor, in_tensor, sync_op
)
if sync_op:
task.wait()
return task
def _all_gather_in_dygraph(
tensor_list: list[Tensor],
tensor: Tensor,
group: Group,
sync_op: bool,
use_calc_stream: bool,
) -> task:
group = _get_global_group() if group is None else group
if len(tensor_list) == 0:
tensor_list += [paddle.empty_like(tensor) for _ in range(group.nranks)]
if use_calc_stream:
return group.process_group.all_gather_on_calc_stream(
tensor_list, tensor
)
task = group.process_group.all_gather(tensor_list, tensor, sync_op)
if sync_op:
task.wait()
return task
def _all_gather_in_static_mode(
tensor_list: list[Tensor], tensor: Tensor, group: Group, sync_op: bool
) -> None:
op_type = 'all_gather'
helper = framework.LayerHelper(op_type, **locals())
out = helper.create_variable_for_type_inference(dtype=tensor.dtype)
for elem in tensor_list:
data_feeder.check_variable_and_dtype(
elem,
'tensor_list',
[
'float16',
'float32',
'float64',
'int32',
'int64',
'bool',
'int8',
'uint8',
'complex64',
'complex128',
],
'all_gather',
)
data_feeder.check_variable_and_dtype(
tensor,
'tensor',
[
'float16',
'float32',
'float64',
'int32',
'int64',
'bool',
'int8',
'uint8',
'complex64',
'complex128',
],
'all_gather',
)
ring_id = 0 if group is None else group.id
nranks = dist.get_world_size()
op = helper.append_op(
type=op_type,
inputs={'x': [tensor]},
outputs={'out': [out]},
attrs={
'ring_id': ring_id,
'nranks': nranks,
},
)
if sync_op:
op.dist_attr.execution_stream = ExecutionStreamType.DefaultStream.value
tensor_list.clear()
# 0-D use stack/unstack while others use concat/split
if len(tensor.shape) == 0:
tensor_list.extend(paddle.unstack(out, 0))
else:
tensor_list.extend(paddle.split(out, nranks, 0))
def all_gather(
tensor_or_tensor_list: Tensor | list[Tensor],
tensor: Tensor,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Gather tensors across devices to a correctly-sized tensor or a tensor list.
Args:
tensor_or_tensor_list (Union[Tensor, List[Tensor]]): The output. If it is a tensor, it should be correctly-sized. If it is a list, it
should be empty or contain correctly-sized tensors.
tensor (Tensor): The input tensor on each rank. The result will overwrite this tenor after communication. Support
float16, float32, float64, int32, int64, int8, uint, bool, complex64 or complex128 as the input data type.
group (Group, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Warning:
This API only supports the dygraph mode now.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> local_rank = dist.get_rank()
>>> tensor_list = [] # type: ignore
>>> if local_rank == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> task = dist.stream.all_gather(tensor_list, data, sync_op=False)
>>> task.wait() # type: ignore[union-attr]
>>> print(tensor_list)
[[[4, 5, 6], [4, 5, 6]], [[1, 2, 3], [1, 2, 3]]] (2 GPUs)
"""
if group is not None and not group.is_member():
raise RuntimeError(
"The group should not be None and all ranks which invoke this operation should be the member of this group."
)
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
if framework.in_dynamic_mode():
if paddle.is_tensor(tensor_or_tensor_list):
return _all_gather_into_tensor_in_dygraph(
tensor_or_tensor_list, tensor, group, sync_op, use_calc_stream
)
else:
return _all_gather_in_dygraph(
tensor_or_tensor_list, tensor, group, sync_op, use_calc_stream
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
if paddle.is_tensor(tensor_or_tensor_list):
raise RuntimeError(
"Only support passing a tensor list to `all_gather` in static graph mode now."
)
else:
return _all_gather_in_static_mode(
tensor_or_tensor_list, tensor, group, sync_op
)
@@ -0,0 +1,166 @@
# 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
from typing import TYPE_CHECKING
from paddle import _C_ops, framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import (
_get_global_group,
_warn_cur_rank_not_in_group,
)
from paddle.distributed.communication.reduce import (
ReduceOp,
_get_reduce_op,
_to_inplace_op,
)
from paddle.framework import in_pir_mode
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
from ..all_reduce import _ReduceOp
def _all_reduce_in_dygraph(
tensor: Tensor,
op: _ReduceOp,
group: Group,
sync_op: bool,
use_calc_stream: bool,
) -> task:
op_type = _get_reduce_op(op)
if use_calc_stream:
return group.process_group.all_reduce_on_calc_stream(tensor, op_type)
task = group.process_group.all_reduce(tensor, op_type, sync_op)
if sync_op:
task.wait()
return task
def _all_reduce_in_static_mode(
tensor: Tensor,
op: _ReduceOp,
group: Group,
sync_op: bool,
use_calc_stream: bool,
) -> None:
data_feeder.check_variable_and_dtype(
tensor,
'tensor',
[
'float16',
'float32',
'float64',
'int32',
'int64',
'int8',
'uint8',
'bool',
'uint16',
],
'all_reduce',
)
ring_id = 0 if group is None else group.id
if not isinstance(ring_id, int):
raise ValueError("The type of 'ring_id' for all_reduce should be int.")
if in_pir_mode():
op_type: str = _to_inplace_op(op)
_C_ops.all_reduce_(tensor, ring_id, op)
return
# TODO: Support task and use task.wait in static graph mode
# Use use_calc_stream rather than sync_op
op_type = _get_reduce_op(op)
helper = framework.LayerHelper(op_type, **locals())
helper.append_op(
type=op_type,
inputs={'X': [tensor]},
outputs={'Out': [tensor]},
attrs={'ring_id': ring_id, 'use_calc_stream': sync_op},
)
def all_reduce(
tensor: Tensor,
op: _ReduceOp = ReduceOp.SUM,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Perform specific reduction (for example, sum, max) on inputs across devices.
Args:
tensor (Tensor): The input tensor on each rank. The result will overwrite this tenor after communication. Support
float16, float32, float64, int32, int64, int8, uint8 or bool as the input data type.
op (ReduceOp.SUM|ReduceOp.MAX|ReduceOp.MIN|ReduceOp.PROD, optional): The reduction used. If none is given, use ReduceOp.SUM as default.
group (Group|None, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> local_rank = dist.get_rank()
>>> data = None
>>> if local_rank == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> task = dist.stream.all_reduce(data, sync_op=False)
>>> task.wait() # type: ignore[union-attr]
>>> out = data
>>> print(out)
[[5, 7, 9], [5, 7, 9]]
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
return _all_reduce_in_dygraph(
tensor, op, group, sync_op, use_calc_stream
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
return _all_reduce_in_static_mode(
tensor, op, group, sync_op, use_calc_stream
)
@@ -0,0 +1,386 @@
# 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
from typing import TYPE_CHECKING
import paddle
import paddle.distributed as dist
from paddle import framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import (
_get_global_group,
_warn_cur_rank_not_in_group,
)
if TYPE_CHECKING:
from collections.abc import Sequence
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def _all_to_all_tensor_in_dygraph(
out_tensor: Tensor,
in_tensor: Tensor,
group: Group,
sync_op: bool,
use_calc_stream: bool,
) -> task:
if use_calc_stream:
return group.process_group.all_to_all_tensor_on_calc_stream(
out_tensor, in_tensor
)
task = group.process_group.all_to_all_tensor(out_tensor, in_tensor, sync_op)
if sync_op:
task.wait()
return task
def _all_to_all_in_dygraph(
out_tensor_list: Sequence[Tensor],
in_tensor_list: Sequence[Tensor],
group: Group,
sync_op: bool,
use_calc_stream: bool,
) -> task:
if len(in_tensor_list) == 0:
raise RuntimeError("The input tensor_list should not be empty.")
if len(out_tensor_list) == 0:
out_tensor_list += [
paddle.empty_like(tensor) for tensor in in_tensor_list
]
if use_calc_stream:
return group.process_group.all_to_all_on_calc_stream(
out_tensor_list, in_tensor_list
)
task = group.process_group.all_to_all(
out_tensor_list, in_tensor_list, sync_op
)
if sync_op:
task.wait()
return task
def _all_to_all_in_static_mode(
out_tensor_or_tensor_list: Tensor | Sequence[Tensor],
in_tensor_or_tensor_list: Tensor | Sequence[Tensor],
group: Group,
sync_op: bool,
use_calc_stream: bool,
) -> None:
op_type = 'all_to_all'
ring_id = 0 if group is None else group.id
nranks = dist.get_world_size()
helper = framework.LayerHelper(op_type, **locals())
in_tensor = in_tensor_or_tensor_list
if isinstance(in_tensor_or_tensor_list, list):
if len(in_tensor_or_tensor_list) == 0:
raise RuntimeError("The input tensor_list should not be empty.")
# 0-D use stack/unstack while others use concat/split
if len(in_tensor_or_tensor_list[0].shape) == 0:
in_tensor = paddle.stack(in_tensor_or_tensor_list, axis=0)
else:
in_tensor = paddle.concat(in_tensor_or_tensor_list, axis=0)
out_tensor = out_tensor_or_tensor_list
if isinstance(out_tensor_or_tensor_list, list):
if len(out_tensor_or_tensor_list) != 0:
raise ValueError(
"The 'out_tensor_list' for all_to_all must be an empty list."
)
out_tensor = helper.create_variable_for_type_inference(
dtype=in_tensor.dtype
)
data_feeder.check_variable_and_dtype(
in_tensor,
'in_tensor',
['float16', 'float32', 'float64', 'int32', 'int64', 'uint16'],
'all_to_all',
)
op = helper.append_op(
type=op_type,
inputs={'x': [in_tensor]},
outputs={'out': [out_tensor]},
attrs={
'ring_id': ring_id,
},
)
if sync_op:
op.dist_attr.execution_stream = "default"
# NOTE(liyurui): If the argument `out_tensor_or_tensor_list` is a tensor_list,
# we need to split the result. So we should wait the result of all_to_all
# before split if the communication is not on calc stream.
if isinstance(out_tensor_or_tensor_list, list):
if not sync_op:
dist.wait(out_tensor, use_calc_stream=False)
# 0-D use stack/unstack while others use concat/split
if len(in_tensor_or_tensor_list[0].shape) == 0:
out_tensor_or_tensor_list.extend(paddle.unstack(out_tensor, 0))
else:
out_tensor_or_tensor_list.extend(
paddle.split(out_tensor, nranks, 0)
)
def alltoall(
out_tensor_or_tensor_list: Tensor | Sequence[Tensor],
in_tensor_or_tensor_list: Tensor | Sequence[Tensor],
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Scatter a tensor (or a tensor list) across devices and gather outputs to another tensor (or a tensor list, respectively).
Args:
out_tensor_or_tensor_list (Union[Tensor, List[Tensor]]): The output. If it is a tensor, it should be correctly-sized.
If it is a list, it should be empty or contain correctly-sized tensors. Its data type should be the same as the input.
in_tensor_or_tensor_list (Union[Tensor, List[Tensor]]): The input to scatter (must be specified on the source rank).
If it is a tensor, it should be correctly-sized. If it is a list, it should contain correctly-sized tensors. Support
float16, float32, float64, int32, int64, int8, uint8 or bool as the input data type.
group (Group|None, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> # all_to_all with equal split sizes
>>> out_tensor_list = [] # type: ignore[var-annotated]
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([[1, 2, 3], [4, 5, 6]])
... data2 = paddle.to_tensor([[7, 8, 9], [10, 11, 12]])
>>> else:
... data1 = paddle.to_tensor([[13, 14, 15], [16, 17, 18]])
... data2 = paddle.to_tensor([[19, 20, 21], [22, 23, 24]])
>>> task = dist.stream.alltoall(out_tensor_list, [data1, data2], sync_op=False)
>>> task.wait() # type: ignore[union-attr]
>>> print(out_tensor_list)
>>> # [[[1, 2, 3], [4, 5, 6]], [[13, 14, 15], [16, 17, 18]]] (2 GPUs, out for rank 0)
>>> # [[[7, 8, 9], [10, 11, 12]], [[19, 20, 21], [22, 23, 24]]] (2 GPUs, out for rank 1)
>>> # all_to_all with unequal split sizes
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([[1, 2, 3], [4, 5, 6]]) # shape: (2, 3)
... data2 = paddle.to_tensor([7]) # shape: (1, )
... out_data1 = paddle.empty((2, 3), dtype=data1.dtype)
... out_data2 = paddle.empty((3, 2), dtype=data1.dtype)
>>> else:
... data1 = paddle.to_tensor([[8, 9], [10, 11], [12, 13]]) # shape: (3, 2)
... data2 = paddle.to_tensor([[14, 15, 16, 17]]) # shape: (1, 4)
... out_data1 = paddle.empty((1,), dtype=data1.dtype)
... out_data2 = paddle.empty((1, 4), dtype=data1.dtype)
>>> dist.alltoall([out_data1, out_data2], [data1, data2])
>>> print([out_data1, out_data2])
>>> # [[[1, 2, 3], [4, 5, 6]], [[8, 9], [10, 11], [12, 13]]] (2 GPUs, out for rank 0)
>>> # [[7], [[14, 15, 16, 17]]] (2 GPUs, out for rank 1)
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
if out_tensor_or_tensor_list is None:
raise RuntimeError("The output should be specified.")
if in_tensor_or_tensor_list is None:
raise RuntimeError("The input should be specified.")
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
out_is_tensor = paddle.is_tensor(out_tensor_or_tensor_list)
in_is_tensor = paddle.is_tensor(in_tensor_or_tensor_list)
if out_is_tensor and in_is_tensor:
return _all_to_all_tensor_in_dygraph(
out_tensor_or_tensor_list,
in_tensor_or_tensor_list,
group,
sync_op,
use_calc_stream,
)
elif not out_is_tensor and not in_is_tensor:
return _all_to_all_in_dygraph(
out_tensor_or_tensor_list,
in_tensor_or_tensor_list,
group,
sync_op,
use_calc_stream,
)
else:
raise RuntimeError(
"The output and input should be both tensor or tensor list."
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
return _all_to_all_in_static_mode(
out_tensor_or_tensor_list,
in_tensor_or_tensor_list,
group,
sync_op,
use_calc_stream,
)
def _alltoall_single_in_dygraph(
out_tensor: Tensor,
in_tensor: Tensor,
out_split_sizes: list[int],
in_split_sizes: list[int],
group: Group,
sync_op: bool,
use_calc_stream: bool,
) -> task:
if out_split_sizes is None:
out_split_sizes = []
if in_split_sizes is None:
in_split_sizes = []
if use_calc_stream:
return group.process_group.all_to_all_single_on_calc_stream(
out_tensor, in_tensor, out_split_sizes, in_split_sizes
)
task = group.process_group.all_to_all_single(
out_tensor, in_tensor, out_split_sizes, in_split_sizes, sync_op
)
if sync_op:
task.wait()
return task
def alltoall_single(
out_tensor: Tensor,
in_tensor: Tensor,
out_split_sizes: list[int] | None = None,
in_split_sizes: list[int] | None = None,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task:
"""
Split and Scatter the split input tensor to the out tensor across devices.
Args:
out_tensor(Tensor): The output tensor. Its data type should be the same as the input.
in_tensor (Tensor): The input tensor. Its data type should be float16, float32, float64, int32, int64, int8, uint8 or bool.
out_split_sizes (List[int]|None, optional): Split sizes of out_tensor for dim[0]. If not given, dim[0] of out_tensor must be divisible
by group size and out_tensor will be gathered averagely from all participators. If none is given, use a empty list as default.
in_split_sizes (List[int]|None, optional): Split sizes of in_tensor for dim[0]. If not given, dim[0] of in_tensor must be divisible
by group size and in_tensor will be scattered averagely to all participators. If none is given, use a empty list as default.
group (Group|None, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Warning:
This API only supports the dygraph mode now.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> local_rank = dist.get_rank()
>>> # case 1
>>> output = paddle.empty([2], dtype="int64")
>>> if local_rank == 0:
... data = paddle.to_tensor([0, 1])
>>> else:
... data = paddle.to_tensor([2, 3])
>>> task = dist.stream.alltoall_single(output, data, sync_op=False)
>>> task.wait()
>>> out = output.numpy()
>>> print(out)
>>> # [0, 2] (2 GPUs, out for rank 0)
>>> # [1, 3] (2 GPUs, out for rank 1)
>>> # case 2
>>> size = dist.get_world_size()
>>> output = paddle.empty([(local_rank + 1) * size, size], dtype='float32')
>>> if local_rank == 0:
... data = paddle.to_tensor([[0.0, 0.0], [0.0, 0.0], [0.0, 0.0]])
>>> else:
... data = paddle.to_tensor([[1., 1.], [1., 1.], [1., 1.]])
>>> out_split_sizes = [local_rank + 1 for i in range(size)]
>>> in_split_sizes = [i + 1 for i in range(size)]
>>> task = dist.stream.alltoall_single(
... output,
... data,
... out_split_sizes,
... in_split_sizes,
... sync_op=False,
... )
>>> task.wait()
>>> out = output.numpy()
>>> print(out)
>>> # [[0., 0.], [1., 1.]] (2 GPUs, out for rank 0)
>>> # [[0., 0.], [0., 0.], [1., 1.], [1., 1.]] (2 GPUs, out for rank 1)
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
return _alltoall_single_in_dygraph(
out_tensor,
in_tensor,
out_split_sizes,
in_split_sizes,
group,
sync_op,
use_calc_stream,
)
raise RuntimeError(
"paddle.distributed.stream.alltoall_single is only supported in dygraph mode now."
)
@@ -0,0 +1,156 @@
# 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
from typing import TYPE_CHECKING
from paddle import _C_ops, framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import (
_get_global_group,
_get_or_throw_group_rank,
_warn_cur_rank_not_in_group,
)
from paddle.distributed.communication.reduce import _to_inplace_op
from paddle.framework import in_pir_mode
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def _broadcast_in_dygraph(
tensor, src_rank_in_group, group, sync_op, use_calc_stream
):
if use_calc_stream:
return group.process_group.broadcast_on_calc_stream(
tensor, src_rank_in_group
)
task = group.process_group.broadcast(tensor, src_rank_in_group, sync_op)
if sync_op:
task.wait()
return task
def _broadcast_in_static_mode(
tensor, src_rank_in_group, group, sync_op, use_calc_stream
):
data_feeder.check_variable_and_dtype(
tensor,
'tensor',
[
'float16',
'float32',
'float64',
'int32',
'int64',
'int8',
'uint8',
'bool',
],
'broadcast',
)
op_type = 'broadcast'
helper = framework.LayerHelper(op_type, **locals())
ring_id = 0 if group is None else group.id
if in_pir_mode():
op_type = _to_inplace_op(op_type)
getattr(_C_ops, op_type)(tensor, ring_id, src_rank_in_group, sync_op)
return
op = helper.append_op(
type=op_type,
inputs={'x': [tensor]},
outputs={'out': [tensor]},
attrs={
'root': src_rank_in_group,
'ring_id': ring_id,
},
)
if sync_op:
op.dist_attr.execution_stream = "default"
def broadcast(
tensor: Tensor,
src: int,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Broadcast a tensor to all devices.
Args:
tensor (Tensor): The tensor to broadcast. Support float16, float32, float64, int32, int64, int8, uint8 or bool as its data type.
src (int, optional): Rank of the source device.
group (Group|None, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Warning:
This API only supports the dygraph mode now.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> local_rank = dist.get_rank()
>>> if local_rank == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> task = dist.stream.broadcast(data, src=1, sync_op=False)
>>> task.wait() # type: ignore[union-attr]
>>> out = data.numpy()
>>> print(out)
>>> # [[1, 2, 3], [1, 2, 3]] (2 GPUs)
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be True in sync op behavior."
)
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
src_rank_in_group = _get_or_throw_group_rank(src, group)
return _broadcast_in_dygraph(
tensor, src_rank_in_group, group, sync_op, use_calc_stream
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
return _broadcast_in_static_mode(
tensor, src, group, sync_op, use_calc_stream
)
@@ -0,0 +1,138 @@
# Copyright (c) 2023 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
import paddle
import paddle.distributed as dist
from paddle import framework
from paddle.distributed.communication.group import (
_get_global_group,
_get_or_throw_group_rank,
_warn_cur_rank_not_in_group,
)
if TYPE_CHECKING:
from collections.abc import Sequence
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def _gather_in_dygraph(
tensor, gather_list, dst_rank_in_group, group, sync_op, use_calc_stream
):
nranks = group.nranks
if group.rank == dst_rank_in_group:
if len(gather_list) == 0:
gather_list += [paddle.empty_like(tensor) for _ in range(nranks)]
else:
gather_list = [tensor for _ in range(nranks)]
assert len(gather_list) == nranks, (
f" gather_list length {len(gather_list)} and nrankd {nranks} not equal"
)
task = group.process_group.gather(
tensor, gather_list, dst_rank_in_group, sync_op, use_calc_stream
)
if sync_op:
task.wait()
return task
def gather(
tensor: Tensor,
gather_list: Sequence[Tensor] | None = None,
dst: int = 0,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Gather tensors from all participators.
Args:
tensor (Tensor): The input Tensor. Its data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16.
gather_list (list|None): A list of Tensors to hold the gathered tensors. Every element in the list must be a Tensor whose data type
should be float16, float32, float64, int32, int64, int8, uint8, bool or bfloat16. Default value is None.
dst (int): The dst rank id. Default value is 0.
group (Group|None, optional): The group instance return by new_group or None for global default group.
sync_op (bool, optional): Whether this op is a sync op. The default value is True.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Async work handle,which can be wait on, if async_op is set to True.
None, if not async_op
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> gather_list = [] # type: ignore[var-annotated]
>>> if dist.get_rank() == 0:
... data = paddle.to_tensor([1, 2, 3])
... dist.stream.gather(data, gather_list, dst=0)
>>> else:
... data = paddle.to_tensor([4, 5, 6])
... dist.stream.gather(data, gather_list, dst=0)
>>> print(gather_list)
>>> # [[1, 2, 3], [4, 5, 6]] (2 GPUs, out for rank 0)
>>> # [] (2 GPUs, out for rank 1)
"""
assert framework.in_dynamic_mode(), (
"gather doesn't support static graph mode yet."
)
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
# NOTE(liuzhenhai): Only the dst rank needs to specific the gather_list argument.
# Other ranks which pass this argument in will be ignored with a warning.
# The passed in type for non-dst rank is meaningless, for it will be ignored.
if dst != dist.get_rank():
if gather_list is not None:
warnings.warn(
"Specific `gather_list` is meaningless for rank which is not dst."
)
gather_list = []
else:
assert gather_list is not None, (
"gather_list must not be none for dst rank"
)
group = _get_global_group() if group is None else group
dst_rank_in_group = _get_or_throw_group_rank(dst, group)
return _gather_in_dygraph(
tensor, gather_list, dst_rank_in_group, group, sync_op, use_calc_stream
)
@@ -0,0 +1,136 @@
# 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
from typing import TYPE_CHECKING
from paddle import framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import (
_get_global_group,
_get_or_throw_group_rank,
_warn_cur_rank_not_in_group,
)
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def _recv_in_dygraph(
tensor, src_rank_in_group, group, sync_op, use_calc_stream
):
if use_calc_stream:
return group.process_group.recv_on_calc_stream(
tensor, src_rank_in_group
)
task = group.process_group.recv(tensor, src_rank_in_group, sync_op)
if sync_op:
task.wait()
return task
def _recv_in_static_mode(
tensor, src_rank_in_group, group, sync_op, use_calc_stream
):
op_type = 'recv_v2'
data_feeder.check_variable_and_dtype(
tensor,
'tensor',
['float16', 'float32', 'float64', 'int32', 'int64', 'uint16'],
'recv',
)
ring_id = 0 if group is None else group.id
helper = framework.LayerHelper(op_type, **locals())
helper.append_op(
type=op_type,
outputs={'Out': [tensor]},
attrs={
'ring_id': ring_id,
'peer': src_rank_in_group,
'out_shape': tensor.shape,
'dtype': tensor.dtype,
'use_calc_stream': sync_op,
},
)
def recv(
tensor: Tensor,
src: int = 0,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Receive a tensor from the source device.
Args:
tensor (Tensor): The tensor to receive. Support float16, float32, float64, int32, int64, int8, uint8 or bool as its data type.
src (int, optional): Rank of the source device. If none is given, use `0` as default.
group (Group|None, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> local_rank = dist.get_rank()
>>> if local_rank == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
... task = dist.stream.send(data, dst=1, sync_op=False)
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
... task = dist.stream.recv(data, src=0, sync_op=False)
>>> task.wait() # type: ignore[union-attr]
>>> out = data.numpy()
>>> print(out)
>>> # [[4, 5, 6], [4, 5, 6]] (2 GPUs)
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be True in sync op behavior."
)
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
src_rank_in_group = _get_or_throw_group_rank(src, group)
return _recv_in_dygraph(
tensor, src_rank_in_group, group, sync_op, use_calc_stream
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
return _recv_in_static_mode(
tensor, src, group, sync_op, use_calc_stream
)
@@ -0,0 +1,156 @@
# 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
from typing import TYPE_CHECKING
from paddle import framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import (
_get_global_group,
_get_or_throw_group_rank,
_warn_cur_rank_not_in_group,
)
from paddle.distributed.communication.reduce import ReduceOp, _get_reduce_op
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
from paddle.distributed.communication.reduce import _ReduceOp
def _reduce_in_dygraph(
tensor, dst_rank_in_group, op, group, sync_op, use_calc_stream
):
op_type = _get_reduce_op(op)
if use_calc_stream:
return group.process_group.reduce_on_calc_stream(
tensor, dst_rank_in_group, op_type
)
task = group.process_group.reduce(
tensor, dst_rank_in_group, op_type, sync_op
)
if sync_op:
task.wait()
return task
def _reduce_in_static_mode(
tensor, dst_rank_in_group, reduce_type, group, sync_op, use_calc_stream
):
data_feeder.check_variable_and_dtype(
tensor,
'tensor',
[
'float16',
'float32',
'float64',
'int32',
'int64',
'int8',
'uint8',
'bool',
],
'reduce',
)
op_type = "reduce"
ring_id = 0 if group is None else group.id
helper = framework.LayerHelper(op_type, **locals())
helper.append_op(
type=op_type,
inputs={'x': [tensor]},
outputs={'out': [tensor]},
attrs={
'ring_id': ring_id,
'root_id': dst_rank_in_group,
'reduce_type': int(reduce_type),
},
)
def reduce(
tensor: Tensor,
dst: int = 0,
op: _ReduceOp = ReduceOp.SUM,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Perform specific reduction (for example, sum, max) on a tensor across devices and send to the destination device.
Args:
tensor (Tensor): The input tensor on each rank. The result will overwrite this tenor after communication. Support
float16, float32, float64, int32, int64, int8, uint8 or bool as the input data type.
dst (int, optional): Rank of the destination device. If none is given, use `0` as default.
op (ReduceOp.SUM|ReduceOp.MAX|ReduceOp.MIN|ReduceOp.PROD, optional): The reduction used. If none is given, use ReduceOp.SUM as default.
group (Group|None, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Warning:
This API only supports the dygraph mode now.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> local_rank = dist.get_rank()
>>> if local_rank == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
>>> task = dist.stream.reduce(data, dst=0, sync_op=False)
>>> task.wait() # type: ignore[union-attr]
>>> out = data.numpy()
>>> print(out)
>>> # [[5, 7, 9], [5, 7, 9]] (2 GPUs, out for rank 0)
>>> # [[1, 2, 3], [1, 2, 3]] (2 GPUs, out for rank 1)
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
dst_rank_in_group = _get_or_throw_group_rank(dst, group)
return _reduce_in_dygraph(
tensor, dst_rank_in_group, op, group, sync_op, use_calc_stream
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
return _reduce_in_static_mode(
tensor, dst, op, group, sync_op, use_calc_stream
)
@@ -0,0 +1,273 @@
# 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
from typing import TYPE_CHECKING
import paddle
import paddle.distributed as dist
from paddle import framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import (
_get_global_group,
_warn_cur_rank_not_in_group,
)
from paddle.distributed.communication.reduce import ReduceOp, _get_reduce_op
if TYPE_CHECKING:
from collections.abc import Sequence
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
from paddle.distributed.communication.reduce import _ReduceOp
def _reduce_scatter_tensor_in_dygraph(
out_tensor,
in_tensor,
op,
group,
sync_op,
use_calc_stream,
caller="reduce_scatter",
):
op_type = _get_reduce_op(op)
if use_calc_stream:
return group.process_group.reduce_scatter_tensor_on_calc_stream(
out_tensor, in_tensor, op_type
)
task = group.process_group.reduce_scatter_tensor(
out_tensor, in_tensor, op_type, sync_op
)
if sync_op:
task.wait()
return task
def _reduce_scatter_in_dygraph(
tensor, tensor_list, op, group, sync_op, use_calc_stream
):
op_type = _get_reduce_op(op)
if use_calc_stream:
return group.process_group.reduce_scatter_on_calc_stream(
tensor, tensor_list, op_type
)
task = group.process_group.reduce_scatter(
tensor, tensor_list, op_type, sync_op
)
if sync_op:
task.wait()
return task
def _reduce_scatter_in_static_mode(tensor, tensor_or_tensor_list, group):
op_type = 'reduce_scatter'
data_feeder.check_variable_and_dtype(
tensor,
'tensor',
[
'float16',
'float32',
'float64',
'int32',
'int64',
'int8',
'uint8',
'bool',
'uint16',
],
op_type,
)
helper = framework.LayerHelper(op_type, **locals())
ring_id = 0 if group is None else group.id
nranks = dist.get_world_size()
helper.append_op(
type=op_type,
inputs={'x': [tensor_or_tensor_list]},
outputs={'out': [tensor]},
attrs={
'ring_id': ring_id,
'nranks': nranks,
},
)
def reduce_scatter(
tensor: Tensor,
tensor_or_tensor_list: Tensor | Sequence[Tensor],
op: _ReduceOp = ReduceOp.SUM,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Reduce, then scatter a tensor (or a tensor list) across devices.
Args:
tensor (Tensor): The output tensor on each rank. The result will overwrite this tenor after communication. Support
float16, float32, float64, int32, int64, int8, uint8 or bool as the input data type.
tensor_or_tensor_list (Union[Tensor, List[Tensor]]): The input to scatter.
If it is a tensor, it should be correctly-sized. If it is a list, it should contain correctly-sized tensors.
op (ReduceOp.SUM|ReduceOp.MAX|ReduceOp.MIN|ReduceOp.PROD, optional): The reduction used. If none is given, use ReduceOp.SUM as default.
group (Group|None, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Warning:
This API only supports the dygraph mode now.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([0, 1])
... data2 = paddle.to_tensor([2, 3])
>>> else:
... data1 = paddle.to_tensor([4, 5])
... data2 = paddle.to_tensor([6, 7])
>>> dist.stream.reduce_scatter(data1, [data1, data2])
>>> out = data1.numpy()
>>> print(out)
>>> # [4, 6] (2 GPUs, out for rank 0)
>>> # [8, 10] (2 GPUs, out for rank 1)
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
if paddle.is_tensor(tensor_or_tensor_list):
return _reduce_scatter_tensor_in_dygraph(
tensor,
tensor_or_tensor_list,
op,
group,
sync_op,
use_calc_stream,
)
else:
return _reduce_scatter_in_dygraph(
tensor,
tensor_or_tensor_list,
op,
group,
sync_op,
use_calc_stream,
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
return _reduce_scatter_in_static_mode(
tensor, tensor_or_tensor_list, group
)
def _reduce_scatter_base(
out_tensor,
in_tensor,
op=ReduceOp.SUM,
group=None,
sync_op=True,
use_calc_stream=False,
):
"""
Reduce, then scatter a flattened tensor across devices.
Args:
out_tensor (Tensor): The output tensor on each rank. The result will overwrite this tenor after communication. Support
float16, float32, float64, int32 or int64 as the input data type.
in_tensor (Tensor): The input tensor to reduce and scatter.
op (ReduceOp.SUM|ReduceOp.MAX|ReduceOp.MIN|ReduceOp.PROD, optional): The reduction used. If none is given, use ReduceOp.SUM as default.
group (Group, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Warning:
This API will be deprecated in the future, and only supports the dygraph mode now.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([7, 8, 9])
... data2 = paddle.to_tensor([10, 11, 12])
... dist.stream.scatter(data1, src=1)
>>> else:
... data1 = paddle.to_tensor([1, 2, 3])
... data2 = paddle.to_tensor([4, 5, 6])
... dist.stream.scatter(data1, [data1, data2], src=1)
>>> out = data1.numpy()
>>> print(out)
>>> # [1, 2, 3] (2 GPUs, out for rank 0)
>>> # [4, 5, 6] (2 GPUs, out for rank 1)
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
return _reduce_scatter_tensor_in_dygraph(
out_tensor,
in_tensor,
op,
group,
sync_op,
use_calc_stream,
"_reduce_scatter_base",
)
raise RuntimeError(
"paddle.distributed.stream._reduce_scatter_base is only supported in dygraph mode now."
)
@@ -0,0 +1,246 @@
# 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
import paddle
import paddle.distributed as dist
from paddle import framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import (
_get_global_group,
_get_or_throw_group_rank,
_warn_cur_rank_not_in_group,
)
if TYPE_CHECKING:
from collections.abc import Sequence
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def _scatter_tensor_in_dygraph(
out_tensor, in_tensor, src_rank_in_group, group, sync_op, use_calc_stream
):
nranks = group.nranks
if use_calc_stream:
return group.process_group.scatter_tensor_on_calc_stream(
out_tensor, in_tensor, src_rank_in_group
)
task = group.process_group.scatter_tensor(
out_tensor, in_tensor, src_rank_in_group, sync_op
)
if sync_op:
task.wait()
return task
def _scatter_in_dygraph(
tensor, tensor_list, src_rank_in_group, group, sync_op, use_calc_stream
):
nranks = group.nranks
if group.rank == src_rank_in_group:
if len(tensor_list) == 0:
raise RuntimeError(
"The tensor_list should not be empty on src rank."
)
else:
tensor_list = [tensor for _ in range(nranks)]
if use_calc_stream:
return group.process_group.scatter_on_calc_stream(
tensor, tensor_list, src_rank_in_group
)
task = group.process_group.scatter(
tensor, tensor_list, src_rank_in_group, sync_op
)
if sync_op:
task.wait()
return task
def _scatter_in_static_mode(
tensor,
tensor_or_tensor_list,
src_rank_in_group,
group,
sync_op,
use_calc_stream,
):
nranks = dist.get_world_size() if group is None else group.nranks
rank = dist.get_rank()
input_tensor = tensor_or_tensor_list
if isinstance(tensor_or_tensor_list, list):
tensor_list = tensor_or_tensor_list
if rank == src_rank_in_group:
if len(tensor_list) == 0:
raise RuntimeError(
"The tensor_list should not be empty on src rank."
)
else:
tensor_list = [tensor for _ in range(nranks)]
# 0-D use stack/unstack while others use concat/split
if len(tensor_list[0].shape) == 0:
input_tensor = paddle.stack(tensor_list, axis=0)
else:
input_tensor = paddle.concat(tensor_list, axis=0)
ring_id = 0 if group is None else group.id
data_feeder.check_variable_and_dtype(
tensor,
'tensor',
[
'float16',
'float32',
'float64',
'int32',
'int64',
'int8',
'uint8',
'bool',
],
'scatter',
)
op_type = 'c_scatter'
helper = framework.LayerHelper(op_type, **locals())
helper.append_op(
type=op_type,
inputs={'X': [input_tensor]},
outputs={'Out': [tensor]},
attrs={
'ring_id': ring_id,
'root': src_rank_in_group,
'use_calc_stream': sync_op,
'nranks': nranks,
},
)
def scatter(
tensor: Tensor,
tensor_or_tensor_list: Tensor | Sequence[Tensor] | None = None,
src: int = 0,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Scatter a tensor (or a tensor list) across devices.
Args:
tensor (Tensor): The output tensor on each rank. The result will overwrite this tenor after communication. Support
float16, float32, float64, int32, int64, int8, uint8 or bool as the input data type.
tensor_or_tensor_list (Union[Tensor, List[Tensor]]): The input to scatter (default is `None`, must be specified on the source rank).
If it is a tensor, it should be correctly-sized. If it is a list, it should contain correctly-sized tensors.
src (int, optional): Rank of the source device. If none is given, use `0` as default.
group (Group|None, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Warning:
This API only supports the dygraph mode now.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> if dist.get_rank() == 0:
... data1 = paddle.to_tensor([7, 8, 9])
... data2 = paddle.to_tensor([10, 11, 12])
... dist.stream.scatter(data1, src=1)
>>> else:
... data1 = paddle.to_tensor([1, 2, 3])
... data2 = paddle.to_tensor([4, 5, 6])
... dist.stream.scatter(data1, [data1, data2], src=1)
>>> out = data1.numpy()
>>> print(out)
>>> # [1, 2, 3] (2 GPUs, out for rank 0)
>>> # [4, 5, 6] (2 GPUs, out for rank 1)
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be true in sync op behavior."
)
# NOTE(liyurui): Only the source rank needs to specific the tensor_or_tensor_list argument.
# Other ranks which pass this argument in will be ignored with a warning.
# If a tensor_list passed in, we need to concat it to a tensor before invoke C++ API.
# If a tensor passed in, concat is not needed.
# The passed in type for non-src rank is meaningless, for it will be ignored.
if src != dist.get_rank():
if tensor_or_tensor_list is not None:
warnings.warn(
"Specific `tensor_or_tensor_list` is meaningless for rank which is not src."
)
tensor_or_tensor_list = []
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
src_rank_in_group = _get_or_throw_group_rank(src, group)
if paddle.is_tensor(tensor_or_tensor_list):
return _scatter_tensor_in_dygraph(
tensor,
tensor_or_tensor_list,
src_rank_in_group,
group,
sync_op,
use_calc_stream,
)
else:
return _scatter_in_dygraph(
tensor,
tensor_or_tensor_list,
src_rank_in_group,
group,
sync_op,
use_calc_stream,
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
return _scatter_in_static_mode(
tensor,
tensor_or_tensor_list,
src,
group,
sync_op,
use_calc_stream,
)
@@ -0,0 +1,135 @@
# 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
from typing import TYPE_CHECKING
from paddle import framework
from paddle.base import data_feeder
from paddle.distributed.communication.group import (
_get_global_group,
_get_or_throw_group_rank,
_warn_cur_rank_not_in_group,
)
if TYPE_CHECKING:
from paddle import Tensor
from paddle.base.core import task
from paddle.distributed.communication.group import Group
def _send_in_dygraph(
tensor, dst_rank_in_group, group, sync_op, use_calc_stream
):
if use_calc_stream:
return group.process_group.send_on_calc_stream(
tensor, dst_rank_in_group
)
task = group.process_group.send(tensor, dst_rank_in_group, sync_op)
if sync_op:
task.wait()
return task
def _send_in_static_mode(
tensor, dst_rank_in_group, group, sync_op, use_calc_stream
):
op_type = 'send_v2'
data_feeder.check_variable_and_dtype(
tensor,
'tensor',
['float16', 'float32', 'float64', 'int32', 'int64', 'uint16'],
'send',
)
ring_id = 0 if group is None else group.id
helper = framework.LayerHelper(op_type, **locals())
helper.append_op(
type=op_type,
inputs={'X': [tensor]},
attrs={
'ring_id': ring_id,
'peer': dst_rank_in_group,
'use_calc_stream': sync_op,
},
)
def send(
tensor: Tensor,
dst: int = 0,
group: Group | None = None,
sync_op: bool = True,
use_calc_stream: bool = False,
) -> task | None:
"""
Send a tensor to the destination device.
Args:
tensor (Tensor): The tensor to send. Support float16, float32, float64, int32, int64, int8, uint8 or bool as its data type.
dst (int, optional): Rank of the destination device. If none is given, use `0` as default.
group (Group, optional): Communicate in which group. If none is given, use the global group as default.
sync_op (bool, optional): Indicate whether the communication is sync or not. If none is given, use true as default.
use_calc_stream (bool, optional): Indicate whether the communication is done on calculation stream. If none is given, use false as default. This
option is designed for high performance demand, be careful to turn it on except you are clearly know its meaning.
Returns:
Return a task object.
Examples:
.. code-block:: pycon
>>> # doctest: +REQUIRES(env: DISTRIBUTED)
>>> import paddle
>>> import paddle.distributed as dist
>>> dist.init_parallel_env()
>>> local_rank = dist.get_rank()
>>> if local_rank == 0:
... data = paddle.to_tensor([[4, 5, 6], [4, 5, 6]])
... task = dist.stream.send(data, dst=1, sync_op=False)
>>> else:
... data = paddle.to_tensor([[1, 2, 3], [1, 2, 3]])
... task = dist.stream.recv(data, src=0, sync_op=False)
>>> task.wait() # type: ignore[union-attr]
>>> out = data.numpy()
>>> print(out)
[[4, 5, 6], [4, 5, 6]]
"""
if _warn_cur_rank_not_in_group(group):
return
if not sync_op and use_calc_stream:
raise RuntimeError(
"use_calc_stream can only be True in sync op behavior."
)
if framework.in_dynamic_mode():
group = _get_global_group() if group is None else group
dst_rank_in_group = _get_or_throw_group_rank(dst, group)
return _send_in_dygraph(
tensor, dst_rank_in_group, group, sync_op, use_calc_stream
)
else:
assert group is None, (
"Group can not be used in static graph mode for now."
)
return _send_in_static_mode(
tensor, dst, group, sync_op, use_calc_stream
)