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

4539 lines
184 KiB
Python

# 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 copy
import logging
import os
import warnings
from collections import OrderedDict
from types import MethodType
from typing import TYPE_CHECKING, Any, Literal, TypedDict
import numpy as np
import paddle
import paddle.distributed as dist
from paddle import _C_ops, nn, pir
from paddle.amp.auto_cast import amp_global_state
from paddle.amp.grad_scaler import OptimizerState
from paddle.autograd import PyLayer
from paddle.base import unique_name
from paddle.base.dygraph.base import switch_to_static_graph
from paddle.base.framework import (
EagerParamBase,
Variable,
default_main_program,
in_dygraph_mode,
in_pir_mode,
use_pir_api,
)
from paddle.distributed import fleet
from paddle.distributed.auto_parallel import Engine, strategy as auto_strategy
from paddle.distributed.auto_parallel.interface import (
shard_tensor as shard_tensor_static,
)
from paddle.distributed.auto_parallel.process_mesh import ProcessMesh
from paddle.distributed.auto_parallel.static.completion import (
mark_as_sharding_propagation_skip_op,
)
from paddle.distributed.auto_parallel.static.dist_context import (
get_default_distributed_context,
)
from paddle.distributed.auto_parallel.static.dist_op import DistributedOperator
from paddle.distributed.auto_parallel.static.utils import (
convert_to_dims_mapping,
fuse_param_func,
get_dist_attr,
split_mesh,
split_param_func,
to_list,
)
from paddle.distributed.fleet.utils.tensor_fusion_helper import (
align,
alignment,
get_current_device_type,
)
from paddle.framework import core
from paddle.io.dataloader.batch_sampler import (
DistributedBatchSampler,
_InfiniteIterableSampler,
)
from paddle.optimizer import Optimizer
from .auto_dp_utils import (
_enable_auto_dp,
_fake_replicate_grad_to_partial,
in_auto_dp_mode,
)
from .moe_utils import (
_cal_local_shape,
_dist_reshape,
_dtensor_from_local,
_NdMeshAlltoAll,
_only_reshard_mesh_shape,
_reshard_mesh_shape,
_specific_alltoall_dim,
)
from .placement_type import (
check_placements_equal,
get_shard_spec,
placemetns_to_dist_status,
to_dim_map,
to_placements,
)
from .random import determinate_rng, rng_state
from .sharding import (
ShardingOptimizerStage1,
get_mesh_comm_list,
get_placement_with_sharding,
)
if TYPE_CHECKING:
from collections.abc import Callable, Sequence
from typing import TypeAlias
from paddle import Tensor
from paddle._typing import (
DTypeLike,
NestedNumericSequence,
PlaceLike,
TensorLike,
)
from paddle.amp import GradScaler
from paddle.base.framework import Program
from paddle.distributed import Placement
from paddle.distributed.auto_parallel.static.dist_input_spec import (
DistributedInputSpec,
)
from paddle.io import DataLoader
from paddle.metric import Metric
from paddle.nn import Layer
from .constants import (
_AMPConfig,
_DPOptimizationConfig,
_FusedPassesConfig,
_GradientMergeConfig,
_MPOptimizationConfig,
_PipelineConfig,
_RecomputeConfig,
_ShardingConfig,
_SPOptimizationConfig,
)
_Mode: TypeAlias = Literal['train', 'eval', 'predict']
class _Config(TypedDict, total=False):
sharding: _ShardingConfig
fused_passes: _FusedPassesConfig
gradient_merge: _GradientMergeConfig
pipeline: _PipelineConfig
amp: _AMPConfig
recompute: _RecomputeConfig
mp_optimization: _MPOptimizationConfig
dp_optimization: _DPOptimizationConfig
sp_optimization: _SPOptimizationConfig
# There are the auto parallel API of the unified version of dynamic and static mode.
# Some APIs have the same name with the previous APIs implementation, which are
# a temporary state, and the APIs here will eventually be used.
# Part1: Shard attributes related APIs
def _to_lodtensor(tensor: paddle.Tensor):
lodtensor = core.DenseTensor()
if tensor.is_dist():
if tensor._is_initialized():
lodtensor._share_data_with(tensor._local_value().get_tensor())
else:
lodtensor = None
else:
lodtensor._share_data_with(tensor.get_tensor())
return lodtensor
def _get_suffix(s, prefix):
if s.startswith(prefix):
return s[len(prefix) :]
else:
return None
class DistAttr(core.TensorDistAttr):
"""
DistAttr specifies how tensors are distributed or sliced on ProcessMesh.
Args:
mesh(paddle.distributed.ProcessMesh): The `ProcessMesh` object describes the Cartesian topology of the used processes.
sharding_specs(list[str|None]): The specification describing how to shard the Tensor.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([[2, 4, 5], [0, 1, 3]], dim_names=['x', 'y'])
>>> dist_attr = dist.DistAttr(mesh=mesh, sharding_specs=['x', 'y'])
>>> print(dist_attr)
"""
def __init__(self, mesh, sharding_specs):
# 1. inputs checking
if not isinstance(mesh, core.ProcessMesh):
raise ValueError(
"The mesh must be an instance of paddle.distributed.ProcessMesh."
)
if not isinstance(sharding_specs, list):
raise ValueError("The sharding_specs must be an instance of list.")
assert all(
isinstance(dim_name, str) or dim_name is None
for dim_name in sharding_specs
), 'The dimension name in sharding_specs must be an instance of str.'
self._sharding_specs = sharding_specs
dims_mapping = []
for dim_name in sharding_specs:
if dim_name is None:
dims_mapping.append(-1)
else:
if dim_name not in mesh.dim_names:
raise ValueError(
f"Invalid sharding dimension '{dim_name}'. "
f"Available dimensions in mesh are: {mesh.dim_names}."
)
dims_mapping.append(mesh.dim_names.index(dim_name))
# 2. init core.TensorDistAttr
core.TensorDistAttr.__init__(self)
self.process_mesh = mesh
self.dims_mapping = dims_mapping
self.mark_annotated("process_mesh")
self.mark_annotated("dims_mapping")
@property
def sharding_specs(self):
"""
Get sharding_specs of the dist_attr
Returns:
list[str]: sharding_specs
"""
return self._sharding_specs
# Part2: DistTensor construction related APIs
def shard_tensor(
data: Tensor | TensorLike | NestedNumericSequence,
mesh: ProcessMesh,
placements: Sequence[Placement],
dtype: DTypeLike | None = None,
place: PlaceLike | None = None,
stop_gradient: bool | None = None,
) -> Tensor:
"""
Creates a distributed Tensor (i.e., Tensor with distributed attributes or DistTensor for short)
from the input data, which can be a scalar, tuple, list, numpy.ndarray, or paddle.Tensor.
If the ``data`` is already a Tensor, it will be transformed into a distributed Tensor.
Args:
data(scalar|tuple|list|ndarray|Tensor): Initial data for the tensor.
Can be a scalar, list, tuple, numpy.ndarray, paddle.Tensor.
mesh(paddle.distributed.ProcessMesh): The `ProcessMesh` object describes the Cartesian topology of the used processes.
placements(list[paddle.distributed.Placement]): the placements describe how to place the tensor on ProcessMesh, it can
be Shard, Replicate and Partial.
dtype(str|paddle.dtype|np.dtype, optional): The desired data type of returned tensor.
It Can be 'bool' , 'float16' , 'float32' , 'float64' , 'int8' , 'int16' , 'int32' , 'int64' , 'uint8',
'complex64' , 'complex128'. Default: None. If None, the the dtype is inferred from ``data``
except for python float number, in which case the dtype is inferred from ``get_default_type`` .
place(CPUPlace|CUDAPinnedPlace|CUDAPlace|str, optional): The place to allocate Tensor. Can be
CPUPlace, CUDAPinnedPlace, CUDAPlace. Default: None, means global place. If ``place`` is
string, It can be ``cpu``, ``gpu:x`` and ``gpu_pinned``, where ``x`` is the index of the GPUs.
stop_gradient(bool, optional): Whether to block the gradient propagation of Autograd. If
``stop_gradient`` is None, set the returned Tensor's ``stop_gradient`` identical as the
``data.stop_gradient`` when ``data`` has ``stop_gradient`` attribute and True otherwise.
Default: None.
Returns:
Tensor: A Tensor constructed from ``data`` with distributed attributes.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([[2, 4, 5], [0, 1, 3]], dim_names=['x', 'y'])
>>> # dense tensor
>>> a = paddle.to_tensor(
... [
... [1, 2, 3],
... [5, 6, 7],
... ]
... )
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> # distributed tensor
>>> d_tensor = dist.shard_tensor(a, mesh, [dist.Shard(0), dist.Shard(1)])
>>> print(d_tensor)
"""
if place is None:
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
# 1. create dense tensor
if stop_gradient is None:
stop_gradient = getattr(data, "stop_gradient", True)
if paddle.framework.in_pir_mode():
assert isinstance(data, (type(None), pir.Value)), (
"input tensor is not pir value."
)
assert data.is_dense_tensor_type(), (
"shard_tensor() input data only supported dense tensor type right."
)
tensor = data
else:
if isinstance(data, EagerParamBase) and not data._is_initialized():
assert data._init_func is not None, (
"Get an uninitialized param with an unregistered init_func."
)
tensor = data
elif isinstance(data, paddle.Tensor) and dtype is None:
# if place is not equal, it is handled in paddle.Tensor()
tensor = data
else:
# `paddle.to_tensor` supports both dynamic and static mode
tensor = paddle.to_tensor(
data, dtype=dtype, place=place, stop_gradient=stop_gradient
)
if paddle.in_dynamic_mode():
# here the dist tensor is deep copy constructed
if isinstance(data, EagerParamBase):
def lazy_init_hook(param, origin_hook):
for placement in param.placements:
assert not placement.is_partial(), (
"Lazy init not support partial reshard. Notice that: shard a param to partial "
"won't save any memory, but will increase the communication cost!"
)
# lazy init hook with randomness controlling
def _init_func(var, block):
if dist.get_rank() not in param.process_mesh.process_ids:
# None calc rank, just return no init.
return
# get the unique rng name
rng_name = determinate_rng(
dist.get_rank(),
process_mesh=param.process_mesh,
placements=param.placements,
)
# real call the init function
with rng_state(rng_name):
origin_hook(var, block)
return _init_func
dist_param = EagerParamBase.from_tensor(
tensor,
process_mesh=mesh,
placements=placements,
**tensor.__dict__,
)
dist_param.stop_gradient = tensor.stop_gradient
if tensor._init_func is not None:
origin_init_func = tensor._init_func
dist_param.set_init_func(
lazy_init_hook(dist_param, origin_init_func)
)
return dist_param
else:
dist_tensor = paddle.Tensor(
tensor, process_mesh=mesh, placements=placements, place=place
)
# InitDistTensorWithTensor won't pass the stop gradient attribute,
# have to pass it manually.
dist_tensor.stop_gradient = tensor.stop_gradient
return dist_tensor
elif paddle.framework.in_pir_mode():
dist_tensor = paddle._C_ops.shard_tensor(tensor, mesh, placements)
dist_tensor.stop_gradient = tensor.stop_gradient
dist_tensor.persistable = tensor.persistable
return dist_tensor
else:
# TODO(zhiqiu): we need to refine the static shard_tensor
sharding_specs = get_shard_spec(mesh, placements, tensor.ndim)
return shard_tensor_static(tensor, mesh, sharding_specs)
class _moe_global_mesh_tensor(PyLayer):
@staticmethod
def forward(
ctx,
local_tensor_list,
local_mesh_list,
local_placements,
mesh,
placements,
global_dims,
idx=None,
):
# NOTE: _local_value/Paddle.Tensor is only supported in dynamic mode
if paddle.in_dynamic_mode():
local_tensor = local_tensor_list[idx]
if local_tensor.is_dist():
local_mesh = local_tensor.process_mesh
local_val = local_tensor._local_value()
else:
local_val = local_tensor
local_mesh = None
ctx.save_for_backward(
copy.deepcopy(mesh), # global_mesh
local_tensor.shape, # local_dims
copy.deepcopy(local_mesh_list), # local_mesh_list
copy.deepcopy(local_placements), # local_placements
)
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
global_tensor = paddle.Tensor(
local_val,
dims=global_dims,
process_mesh=mesh,
placements=placements,
place=place,
)
global_tensor.stop_gradient = local_tensor.stop_gradient
return global_tensor
else:
ctx.save_for_backward(
copy.deepcopy(mesh), # global_mesh
copy.deepcopy(placements), # global_placements
copy.deepcopy(local_mesh_list), # local_mesh_list
copy.deepcopy(local_placements), # local_placements
)
dist_tensor = paddle._C_ops.moe_global_mesh_tensor(
local_tensor_list,
local_mesh_list,
local_placements,
mesh,
placements,
global_dims,
)
dist_tensor.stop_gradient = local_tensor_list[0].stop_gradient
dist_tensor.persistable = local_tensor_list[0].persistable
return dist_tensor
@staticmethod
def backward(ctx, grad_tensor):
if paddle.in_dynamic_mode():
global_mesh, local_dims, local_mesh_list, local_placements = (
ctx.saved_tensor()
)
if local_mesh_list is None:
return grad_tensor._local_value()
else:
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
out = []
for i, local_mesh in enumerate(local_mesh_list):
out.append(
paddle.Tensor(
grad_tensor._local_value(),
dims=local_dims,
process_mesh=local_mesh,
placements=local_placements,
place=place,
)
)
out[-1].get_tensor()._unsafe_set_skip_check_mesh(True)
return out
else:
(
global_mesh,
global_placements,
local_mesh_list,
local_placements,
) = ctx.saved_tensor()
return paddle._C_ops.moe_sub_mesh_tensors(
grad_tensor,
local_mesh_list,
local_placements,
global_mesh,
global_placements,
)
def _get_sub_meshes_and_local_placements(
global_mesh, global_placements, sub_mesh_dim
):
if global_mesh is None or sub_mesh_dim is None or global_placements is None:
raise ValueError(
"the args global_mesh, global_placements and local_mesh_dim should all be set."
)
sub_mesh_list = split_mesh(global_mesh, sub_mesh_dim)
local_placements = list(global_placements)
if sub_mesh_dim < len(local_placements):
local_placements[sub_mesh_dim] = dist.Replicate()
return sub_mesh_list, local_placements
def _cal_global_shape(local_shape, mesh, placements):
# assume the each rank has the same tensor shape for now,
# just use the local shape to calculate the global shape
global_shape = list(local_shape)
for idx, placement in enumerate(placements):
if placement.is_shard():
shard_dim = placement.get_dim()
if global_shape[shard_dim] == -1:
continue
local_dim_size = global_shape[shard_dim]
global_shape[shard_dim] = local_dim_size * mesh.shape[idx]
return global_shape
def moe_global_mesh_tensor(
local_tensor_list, mesh, placements, local_mesh_dim=-1
):
placements = copy.deepcopy(placements)
local_mesh_list, local_placements = _get_sub_meshes_and_local_placements(
mesh, placements, local_mesh_dim
)
process_ids = np.array(mesh.process_ids).reshape(mesh.shape)
local_coord = np.where(process_ids == dist.get_rank())
# when rank is not in current mesh, local_coord is empty, so we should calculate the
# local tensor's shape.
if local_coord[0].size == 0:
local_tensor_idx = 0
else:
local_tensor_idx = local_coord[local_mesh_dim][0]
local_tensor = local_tensor_list[local_tensor_idx]
if paddle.in_dynamic_mode():
# NOTE: _local_value and Paddle.Tensor() is only supported in dynamic mode
if local_coord[0].size == 0:
local_tensor_shape = _cal_local_shape(
local_tensor_list[0].shape, local_mesh_list[0], local_placements
)
else:
local_tensor_shape = (
local_tensor_list[local_tensor_idx]._local_value().shape
)
global_dims = _cal_global_shape(local_tensor_shape, mesh, placements)
resharded_local_tensor_list = []
for i, tensor in enumerate(local_tensor_list):
tensor.get_tensor()._unsafe_set_skip_check_mesh(True)
if (
not check_placements_equal(tensor.placements, local_placements)
or tensor.process_mesh != local_mesh_list[i]
):
resharded_local_tensor_list.append(
reshard(tensor, local_mesh_list[i], local_placements)
)
resharded_local_tensor_list[
-1
].get_tensor()._unsafe_set_skip_check_mesh(True)
else:
resharded_local_tensor_list.append(tensor)
return _moe_global_mesh_tensor.apply(
resharded_local_tensor_list,
local_mesh_list,
local_placements,
mesh,
placements,
global_dims,
local_tensor_idx,
)
elif paddle.framework.in_pir_mode():
global_dims = _cal_global_shape(
local_tensor._local_shape, mesh, placements
)
dist_tensor = paddle._C_ops.moe_global_mesh_tensor(
local_tensor_list,
local_mesh_list,
local_placements,
mesh,
placements,
global_dims,
)
dist_tensor.stop_gradient = local_tensor_list[0].stop_gradient
dist_tensor.persistable = local_tensor_list[0].persistable
return dist_tensor
else:
raise NotImplementedError(
"dtensor_from_local_list() are only supported in dynamic and pir mode."
)
class _moe_sub_mesh_tensors(PyLayer):
@staticmethod
def forward(
ctx,
dist_tensor,
local_mesh_list=None,
local_placements=None,
local_mesh_dim=None,
global_mesh=None,
global_placements=None,
):
ctx.save_for_backward(
copy.deepcopy(local_mesh_list), # local_mesh_list,
local_placements, # local_placements,
local_mesh_dim, # local_mesh_dim,
copy.deepcopy(global_mesh), # global_mesh,
global_placements, # global_placements,
dist_tensor.shape, # global_shape,
)
if paddle.in_dynamic_mode():
if global_mesh is None and global_placements is None:
return dist_tensor._local_value()
else:
if global_mesh is None or global_placements is None:
raise ValueError(
"the args global_mesh and global_placements should be set together"
)
ori_mesh = dist_tensor.process_mesh
if global_mesh != dist_tensor.process_mesh:
raise ValueError(
"the global_mesh should be the same as dist_tensor's process_mesh."
)
assert check_placements_equal(
global_placements, dist_tensor.placements
), (
f"the global_placements ({global_placements}) is not equal to dist_tensor's placements ({dist_tensor.placements})."
)
local_shape = _cal_local_shape(
dist_tensor.shape, global_mesh, global_placements
)
for idx, placement in enumerate(local_placements):
if placement.is_shard():
shard_dim = placement.get_dim()
local_dim_size = local_shape[shard_dim]
local_shape[shard_dim] = (
local_dim_size * local_mesh_list[0].shape[idx]
)
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
local_tensor_list = []
for i, local_mesh in enumerate(local_mesh_list):
local_tensor = paddle.Tensor(
dist_tensor._local_value(),
dims=local_shape,
process_mesh=local_mesh,
placements=local_placements,
place=place,
)
local_tensor.get_tensor()._unsafe_set_skip_check_mesh(True)
local_tensor.stop_gradient = dist_tensor.stop_gradient
local_tensor_list.append(local_tensor)
return local_tensor_list
elif paddle.framework.in_pir_mode():
local_tensors = paddle._C_ops.moe_sub_mesh_tensors(
dist_tensor,
local_mesh_list,
local_placements,
global_mesh,
global_placements,
)
for local_tensor in local_tensors:
local_tensor.stop_gradient = dist_tensor.stop_gradient
local_tensor.persistable = dist_tensor.persistable
return local_tensors
@staticmethod
def backward(ctx, *grad_tensor):
(
local_mesh_list,
local_placements,
local_mesh_dim,
global_mesh,
global_placements,
global_shape,
) = ctx.saved_tensor()
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
mesh = global_mesh
process_ids = np.array(mesh.process_ids).reshape(mesh.shape)
local_coord = np.where(process_ids == dist.get_rank())
if local_coord[0].size == 0:
local_tensor_idx = 0
else:
local_tensor_idx = local_coord[local_mesh_dim][0]
local_grad = grad_tensor[local_tensor_idx]
if paddle.in_dynamic_mode():
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
global_tensor = paddle.Tensor(
local_grad._local_value(),
dims=global_shape,
process_mesh=mesh,
placements=global_placements,
place=place,
)
return global_tensor
elif paddle.framework.in_pir_mode():
global_dims = _cal_global_shape(
local_grad._local_shape, mesh, global_placements
)
return paddle._C_ops.moe_global_mesh_tensor(
grad_tensor,
local_mesh_list,
local_placements,
global_mesh,
global_placements,
global_dims,
)
def moe_sub_mesh_tensors(
dist_tensor, global_mesh=None, local_mesh_dim=None, global_placements=None
):
"""
Get the local part of the ``dist_tensor`` on the specific ``local_mesh_dim``.
"""
global_placements = copy.deepcopy(global_placements)
local_mesh_list, local_placements = _get_sub_meshes_and_local_placements(
global_mesh, global_placements, local_mesh_dim
)
if paddle.framework.in_dynamic_mode():
return _moe_sub_mesh_tensors.apply(
dist_tensor,
local_mesh_list,
local_placements,
local_mesh_dim,
global_mesh,
global_placements,
)
elif paddle.framework.in_pir_mode():
local_tensors = paddle._C_ops.moe_sub_mesh_tensors(
dist_tensor,
local_mesh_list,
local_placements,
global_mesh,
global_placements,
)
for local_tensor in local_tensors:
local_tensor.stop_gradient = dist_tensor.stop_gradient
local_tensor.persistable = dist_tensor.persistable
return local_tensors
else:
raise NotImplementedError(
"moe_sub_mesh_tensors is only supported in dynamic mode."
)
def dtensor_from_local(local_tensor, mesh, placements):
if paddle.in_dynamic_mode():
if local_tensor.is_dist() is True and local_tensor._is_initialized():
raise ValueError("The input should be a local tensor.")
return paddle.base.core.dtensor_from_local(
local_tensor, mesh, placements
)
# TODO Adopt Mix2Dist Pass to allow the program could be executed actually.
elif paddle.framework.in_pir_mode():
return paddle._C_ops.dtensor_from_local(local_tensor, mesh, placements)
else:
raise RuntimeError(
"dtensor_from_local() are only supported in dynamic or pir mode."
)
def dtensor_to_local(dist_tensor, mesh, placements):
if paddle.in_dynamic_mode():
if dist_tensor.is_dist() is False:
raise ValueError("The input should be a distributed tensor.")
return paddle.base.core.dtensor_to_local(dist_tensor, mesh, placements)
elif paddle.framework.in_pir_mode():
return paddle._C_ops.dtensor_to_local(dist_tensor, mesh, placements)
else:
raise RuntimeError(
"dtensor_to_local() are only supported in dynamic or pir mode."
)
def dtensor_from_fn(
fn: Callable[..., Tensor],
mesh: ProcessMesh,
placements: Sequence[Placement],
*args: Any,
**kwargs: Any,
) -> Tensor:
"""
Construct a Distributed Tensor from a function of arguments.
Args:
fn (callable): A callable function that creates and returns a tensor, such as paddle.ones, paddle.zeros, etc.
mesh(paddle.distributed.ProcessMesh): The `ProcessMesh` object describes the Cartesian topology of the used processes.
placements(list[paddle.distributed.Placement]): the placements describe how to place the tensor on ProcessMesh, it can
be Shard, Replicate and Partial.
*args (tuple): A tuple of arguments to be passed to the ``fn`` function.
**kwargs (dict): A dict of arguments to be passed to the ``fn`` function.
Returns:
Tensor: A Tensor constructed from ``fn`` with distributed attributes.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> # Create a distributed attribute
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> # Call the function dtensor_from_fn with dist_attr parameter
>>> d_tensor = dist.dtensor_from_fn(paddle.ones, mesh, [dist.Replicate()], shape=[1])
>>> print(d_tensor)
"""
tensor = fn(*args, **kwargs)
return shard_tensor(tensor, mesh, placements)
# Part3: Data conversion related APIs
def reshard(
dist_tensor: Tensor, mesh: ProcessMesh, placements: Sequence[Placement]
) -> Tensor:
"""
Reshard a distributed ``paddle.Tensor`` with given distributed attributes.
Args:
dist_tensor(Tensor): the distributed tensor to be resharded.
mesh(paddle.distributed.ProcessMesh): The `ProcessMesh` object describes the Cartesian topology of the used processes.
placements(list[paddle.distributed.Placement]): the placements describe how to place the tensor on ProcessMesh, it can
be Shard, Replicate and Partial.
Returns:
Tensor: A Distributed Tensor resharded with distributed attributes.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> # dense tensor
>>> a = paddle.ones([10, 20])
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> # distributed tensor
>>> d_tensor = dist.shard_tensor(a, mesh, [dist.Partial()])
>>> out_d_tensor = dist.reshard(d_tensor, mesh, [dist.Replicate()])
>>> print(out_d_tensor)
"""
if _only_reshard_mesh_shape(dist_tensor, mesh, placements):
return _dist_reshape(dist_tensor, dist_tensor.shape, mesh, placements)
if paddle.framework.in_dynamic_mode():
# TODO(LiYuRio): static logic here, reshard should be changed for dygraph logic
# when reshard has been changed align dygraph logic, delete it.
dims_mapping, partial_status, split_factor = placemetns_to_dist_status(
placements, dist_tensor.ndim, return_split_factor=True
)
dist_attr = core.TensorDistAttr()
dist_attr.multi_dims_mapping = dims_mapping
dist_attr.process_mesh = mesh
dist_attr.mark_annotated("process_mesh")
dist_attr.mark_annotated("dims_mapping")
if len(split_factor) > 0:
for dim, sf in split_factor.items():
dist_attr._set_split_factor(dim, sf)
if len(partial_status) > 0:
dims = []
for dim, _ in partial_status.items():
dims.append(dim)
dist_attr._set_partial_dims(dims)
alltoall_dim = _specific_alltoall_dim(dist_tensor, mesh, placements)
if alltoall_dim is not None:
return _NdMeshAlltoAll.apply(
dist_tensor, mesh, placements, alltoall_dim
)
if _reshard_mesh_shape(dist_tensor, mesh, placements):
return _dist_reshape(
dist_tensor, dist_tensor.shape, mesh, placements
)
return paddle.base.core.reshard(dist_tensor, dist_attr)
elif in_pir_mode():
return paddle._C_ops.reshard(dist_tensor, mesh, placements)
else:
assert isinstance(dist_tensor, Variable), (
f"in dy2static mode, reshard's input should be Variable, but got [{dist_tensor}]"
)
sharding_specs = get_shard_spec(mesh, placements, dist_tensor.ndim)
main_program = default_main_program()
default_dist_ctx = get_default_distributed_context()
# output variable
out_var = main_program.current_block().create_var(
name=unique_name.generate_with_ignorable_key(
".".join(['reshard_api', 'tmp'])
),
dtype=dist_tensor.dtype,
shape=dist_tensor.shape,
type=dist_tensor.type,
persistable=dist_tensor.persistable,
stop_gradient=dist_tensor.stop_gradient,
)
# transition op
# optimization in future to remove redundant D2D memory copy
target_dims_mapping = convert_to_dims_mapping(sharding_specs, mesh)
trans_op = main_program.current_block().append_op(
type='assign',
inputs={'X': [dist_tensor]},
outputs={'Out': [out_var]},
)
dist_op = DistributedOperator(trans_op)
dist_op.dist_attr.process_mesh = mesh
dist_op.dist_attr.mark_annotated("process_mesh")
dist_op.dist_attr.chunk_id = 0
input_dist_attr = dist_op.dist_attr.get_input_dist_attr(
dist_tensor.name
)
input_dist_attr.dims_mapping = target_dims_mapping
input_dist_attr.mark_annotated("dims_mapping")
output_dist_attr = dist_op.dist_attr.get_output_dist_attr(out_var.name)
output_dist_attr.dims_mapping = target_dims_mapping
output_dist_attr.mark_annotated("dims_mapping")
default_dist_ctx.add_dist_op_for_program(dist_op)
mark_as_sharding_propagation_skip_op(trans_op)
# trans_op = shard_op_static(paddle.assign, mesh, [sharding_specs])
# out_var = trans_op(dist_tensor)
return out_var
def shard_layer(
layer: Layer,
process_mesh: ProcessMesh,
shard_fn: Callable[[str, Layer, ProcessMesh], None] | None = None,
input_fn: Callable[[Any, ProcessMesh], list[Tensor]] | None = None,
output_fn: Callable[[Any, ProcessMesh], list[Tensor]] | None = None,
) -> Layer:
"""
Converts all layer's parameters to DistTensor parameters according to
the `shard_fn` specified. It could also control the conversion of input
or output of the layer by specifying the `input_fn` and `output_fn`.
(i.e. convert the input to `paddle.Tensor` with distributed attributes,
convert output back to `paddle.Tensor` without distributed attributes.)
The `shard_fn` should have the following signature:
def shard_fn(layer_name, layer, process_mesh) -> None
The `input_fn` should have the following signature:
def input_fn(inputs, process_mesh) -> list(paddle.Tensor)
In general, the type of `input_fn` return value is paddle.Tensor with distributed attributes.
The `output_fn` should have the following signature:
def output_fn(outputs, process_mesh) -> list(paddle.Tensor)
In general, the type of `output_fn` return value is paddle.Tensor with distributed attributes.
Args:
layer (paddle.nn.Layer): The Layer object to be shard.
process_mesh (paddle.distributed.ProcessMesh): The `ProcessMesh` information
to be place the input `layer`.
shard_fn (Callable): The function to shard layer parameters across
the `process_mesh`. If not specified, by default we replicate
all parameters of the layer across the `process_mesh`.
input_fn (Callable): Specify how the input of the layer is sharded.
The `input_fn` will be registered for the Layer as a `forward pre-hook`.
By default we do not shard the input.
output_fn (Callable): Specify how the output of the layer is sharded or
convert it back to `paddle.Tensor` without distributed attributes.
The `output_fn` will be registered for the Layer as `forward post-hook`.
By default we do not shard or convert the output.
Returns:
Layer: A layer that contains parameters/buffers
that are all `paddle.Tensor` with distributed attributes.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> class MLP(paddle.nn.Layer):
... def __init__(self):
... super().__init__()
... self.fc1 = paddle.nn.Linear(8, 8)
... self.fc2 = paddle.nn.Linear(8, 8)
...
... def forward(self, input):
... return self.fc2(self.fc1(input))
>>> def shard_fn(layer_name, layer, process_mesh):
... if layer_name == 'fc1':
... layer.weight = dist.shard_tensor(layer.weight, process_mesh, [dist.Shard(0)])
>>> layer = MLP()
>>> layer = dist.shard_layer(layer, mesh, shard_fn)
>>> print(layer)
>>> # This case need to be executed in multi-card environment
>>> # export CUDA_VISIBLE_DEVICES=0,1
>>> # python -m paddle.distributed.launch {test_case}.py
"""
# Ensure that process_mesh is not an empty object
if process_mesh is None:
raise ValueError("The argument `process_mesh` cannot be empty.")
# Check the legality of process_mesh
if not isinstance(process_mesh, ProcessMesh):
raise ValueError(
"The argument `process_mesh` is not `dist.ProcessMesh` type."
)
def replicate_layer_params_and_buffers(
layer: nn.Layer, mesh: ProcessMesh
) -> None:
for key, param in layer._parameters.items():
if param is not None and not param.is_dist():
placements = [
paddle.distributed.Replicate()
for _ in range(len(param.shape))
]
layer.add_parameter(
key,
shard_tensor(param, mesh, placements),
)
else:
# do nothing, the dist parameters has already been shard by shard_fn
pass
for key, buffer in layer._buffers.items():
if buffer is not None and not buffer.is_dist():
placements = [
paddle.distributed.Replicate()
for _ in range(len(buffer.shape))
]
layer.register_buffer(
key,
shard_tensor(buffer, mesh, placements),
)
else:
# do nothing, the dist buffers has already been shard by shard_fn
pass
if paddle.in_dynamic_mode():
if shard_fn is None:
# if shard_fn not specified, by default replicate
# all layer's parameters and buffers
for name, sublayers in layer.named_sublayers(include_self=True):
replicate_layer_params_and_buffers(sublayers, process_mesh)
else:
# apply shard_fn to sublayers, contains self
for name, sublayers in layer.named_sublayers(include_self=True):
shard_fn(name, sublayers, process_mesh)
# shard_fn may not deal with all parameters and buffers,
# the parameters and buffers that are not shard by shard_fn
# still need to be shard to replicated
replicate_layer_params_and_buffers(sublayers, process_mesh)
# register input_fn as layer's forward pre hook
if input_fn is not None:
layer.register_forward_pre_hook(
lambda _, inputs: input_fn(inputs, process_mesh)
)
# register output_fn as layer's forward post hook
if output_fn is not None:
layer.register_forward_post_hook(
lambda _, inputs, outputs: output_fn(outputs, process_mesh)
)
return layer
else:
# TODO(chenweihang): Support static mode branch later.
raise NotImplementedError(
"`paddle.distributed.shard_layer` only supports dynamic graph mode."
)
def is_dist_tensor(tensor) -> bool:
"""
Check if an input is a dist_tensor in both dynamic and static modes.
Args:
tensor: The input to check
Returns:
bool: True if the input is a dist_tensor, False otherwise
"""
if paddle.in_dynamic_mode():
return (
isinstance(tensor, paddle.Tensor)
and hasattr(tensor, 'is_dist')
and tensor.is_dist()
)
else:
return (
isinstance(tensor, paddle.base.libpaddle.pir.Value)
and tensor.dist_attr() is not None
)
class _ShardOptimizer(Optimizer):
def __init__(self, optimizer, shard_fn=None, gradient_accumulation_steps=1):
assert optimizer is not None, (
"The argument `optimizer` cannot be empty."
)
assert isinstance(
optimizer, (paddle.optimizer.AdamW, paddle.optimizer.SGD)
), (
"`paddle.distributed.ShardOptimizer` only supports AdamW and SGD optimizer for now."
)
# self.target_block = (
# paddle.base.framework.default_main_program().global_block()
# )
optimizer.helper = paddle.base.layer_helper.LayerHelper(
optimizer.__class__.__name__
)
self.__dict__["_inner_opt"] = optimizer
self._shard_clip = False
if (
hasattr(optimizer, "_grad_clip")
and optimizer._grad_clip is not None
and isinstance(optimizer._grad_clip, paddle.nn.ClipGradByGlobalNorm)
):
self._shard_clip = True
self._shard_fn = shard_fn
self._sharding_axis = None
self._sharding_degree = None
self.gradient_accumulation_steps = gradient_accumulation_steps
if self._shard_fn is None:
self._shard_fn = _ShardingStage0(0)
assert isinstance(
self._shard_fn,
(_ShardingStage0, ShardingStage1, ShardingStage2, ShardingStage3),
), (
"shard_fn must be an instance of one of: _ShardingStage0, ShardingStage1, ShardingStage2, ShardingStage3"
)
if isinstance(
self._shard_fn, (ShardingStage1, ShardingStage2, ShardingStage3)
):
self._set_and_check_sharding_prop_from_param()
self._shard_fn._set_sharding_axis(self._sharding_axis)
# Invoke register hook for sharding stage 2 strategy
if isinstance(self._shard_fn, ShardingStage2) and not in_auto_dp_mode():
for param in self._inner_opt._parameter_list:
self._shard_fn._register_hook_for_param_grad(param)
# Invoke shard_parameter in sharding stage 3 strategy
if isinstance(self._shard_fn, ShardingStage3):
for param in self._inner_opt._parameter_list:
self._shard_fn._shard_parameter(param)
for param in self._inner_opt._parameter_list:
self._shard_fn._register_hook_for_param_grad(param)
os.environ["skip_sharding3_output_reshard"] = "1"
self.fuse_param_view = []
self.param_storage = []
self.grad_storage = []
self._sharding_group = None
self._mp_group = None
self.do_tensor_fusion_once = True
self._strategy = Strategy()
self.enable_tensor_fusion = False
self.enable_sharding_overlap = False
def get_lr_dtype(self):
return self._inner_opt.get_lr_dtype()
def _set_and_check_sharding_prop_from_param(self):
global_mesh = fleet.auto.get_mesh()
if global_mesh:
self._sharding_degree = global_mesh.get_dim_size(
self._shard_fn._sharding_mesh_dim
)
elif self._shard_fn._mesh:
self._sharding_degree = self._shard_fn._mesh.get_dim_size(
self._shard_fn._sharding_mesh_dim
)
else:
raise ValueError(
"The global mesh or shard_fn mesh should be set for the sharding strategy."
)
# Note(luchang): Now we suggest using 0 axis as sharding axis.
self._sharding_axis = 0
# check the placement on sharding axis is Replicate
param_list = self._inner_opt._parameter_list
for param in param_list:
if not param.is_dist():
continue
mesh = param.process_mesh
placements = param.placements
if not isinstance(placements[self._sharding_axis], dist.Replicate):
# try to infer the sharding axis
for dim, placement in enumerate(placements):
if isinstance(placement, dist.Replicate):
self._sharding_axis = dim
# check the placement on sharding axis is Replicate
assert isinstance(
placements[self._sharding_axis], dist.Replicate
), "The placement on sharding_axis should be Replicate"
# check the sharding degree since it has already been set,
# skip check when mesh is true subset of global_mesh
if global_mesh:
if set(mesh.process_ids) < set(global_mesh.process_ids):
continue
elif self._shard_fn._mesh:
if set(mesh.process_ids) < set(
self._shard_fn._mesh.process_ids
):
continue
else:
assert (
mesh.dim_size(self._sharding_axis) == self._sharding_degree
), (
"The sharding degree of all parameters must be equal currently."
)
def _shard_accumulator(self, param):
# Note (luchang): Some models may have parameters whose first dimension is 1,
# such as modulation parameters in DiT models. These parameters can not be sharded.
if param.shape[0] == 1:
return
target_name = param.name
if param.name in self._inner_opt._master_weights.keys():
master_weight = self._inner_opt._master_weights[param.name]
target_name = master_weight.name
# shard the master weight
if isinstance(self._shard_fn, (ShardingStage1, ShardingStage2)):
self._inner_opt._master_weights[param.name] = (
self._shard_fn.shard_master_weight(param, master_weight)
)
self._inner_opt._master_weights[param.name].name = target_name
# shard the accumulators
for key in self._inner_opt._accumulators.keys():
accumulator = self._inner_opt._accumulators[key][target_name]
if accumulator.is_dist() and not isinstance(accumulator, pir.Value):
continue
if paddle.in_dynamic_mode():
origin_accumulator_name = accumulator.name
if isinstance(
self._shard_fn, (ShardingStage1, ShardingStage2, ShardingStage3)
):
self._inner_opt._accumulators[key][target_name] = (
self._shard_fn(key, param, accumulator)
)
else:
if param.is_dist():
if 'beta' not in key:
# If param is a dist tensor should keep the shard info
# for accumulators except beta.
placements = param.placements
else:
# The beta should be replicated cross param's mesh
placements = [
dist.Replicate()
for _ in range(len(param.process_mesh.shape))
]
self._inner_opt._accumulators[key][target_name] = (
shard_tensor(
accumulator,
mesh=param.process_mesh,
placements=placements,
)
)
if paddle.in_dynamic_mode():
self._inner_opt._accumulators[key][
target_name
].name = origin_accumulator_name
def _reset_placements(self, param):
if param.is_dist() and isinstance(
self._shard_fn, (ShardingStage1, ShardingStage2)
):
# in pir mode, reshard pass will automatically handle inplace case, so no extra work is required here.
if not isinstance(param, pir.Value):
new_placement = param.placements
new_placement[self._sharding_axis] = dist.Replicate()
out_param = dist.reshard(
param, param.process_mesh, new_placement
)
param.get_tensor()._share_data_with(out_param.get_tensor())
def _create_accumulators(self, block, parameters):
if isinstance(parameters, dict):
parameters = parameters.get('params')
# NOTE(zhiqiu): we need to create and shard accumulators for parameters one by one,
# to avoid OOM caused by replcated accumulators.
for p in parameters:
self._inner_opt._create_accumulators(block, [p])
self._shard_accumulator(p)
def _finish_update(self, block, parameters_and_grads):
self._inner_opt._finish_update(block, parameters_and_grads)
if self.enable_tensor_fusion:
# zero the grad storage for add_ op in inplace_master_grad
for grad_storage in self.grad_storage:
grad_storage.zero_()
grad_storage.check_in = 0
if not self.enable_sharding_overlap:
for i in range(len(self.fuse_param_view)):
shard_size = (
self.param_storage[i]._numel()
// self._sharding_group.nranks
)
begin = shard_size * max(self._sharding_group.rank, 0)
end = begin + shard_size
slice_buffer = paddle._C_ops.view_slice(
self.param_storage[i], begin, end
)
self._sharding_group.process_group.all_gather(
slice_buffer, self.param_storage[i]
).wait()
else:
if not isinstance(parameters_and_grads, list):
parameters_and_grads = parameters_and_grads['params']
# reset the parameter and grad to right placements
for p, _ in parameters_and_grads:
if amp_global_state().use_master_grad and isinstance(
self._shard_fn, (ShardingStage2, ShardingStage3)
):
p.main_grad = None
self._reset_placements(p)
def apply_gradients(self, params_grads):
new_params_grads = []
for param, grad in params_grads:
new_params_grads.append(
(param, self._shard_fn("grad", param, grad))
)
return Optimizer.apply_gradients(self, new_params_grads)
def state_dict(self):
"""
Create and shard the optimizer states e.g., accumulators and master_weights before load_state_dict.
If training has already started or the optimizer states are already created and sharded, do nothing.
"""
state_dict = self._inner_opt.state_dict()
# training has already started.
param_list = []
if isinstance(self._inner_opt._parameter_list[0], dict):
for param_group in self._inner_opt._parameter_list:
param_list += param_group["params"]
else:
param_list = self._inner_opt._parameter_list
for param in param_list:
if param.stop_gradient:
continue
if hasattr(param, "main_grad"):
if param.main_grad is not None:
return state_dict
else:
if param.grad is not None:
return state_dict
# TODO(pangengzheng): deal with master_weights and LR_Scheduler later
# the optimizer states are already created and sharded
if any(
v.is_dist()
for k, v in state_dict.items()
if k not in ["master_weights", "LR_Scheduler"]
):
return state_dict
# create and shard the optimizer states
# fake the parameter gradient and invoke step to implicitly create the optimizer states.
if not isinstance(self._inner_opt._parameter_list[0], dict):
for param in self._inner_opt._parameter_list:
if param.stop_gradient:
continue
if hasattr(param, "main_grad"):
if param.main_grad is not None:
raise ValueError(
f"gradient should be None, but is {param.main_grad}"
)
param.main_grad = paddle.zeros_like(
param, dtype=paddle.float32
)
else:
if param.grad is not None:
raise ValueError(
f"gradient should be None, but is {param.grad}"
)
param.grad = paddle.zeros_like(param, dtype=param.dtype)
else:
for param_group in self._inner_opt._param_groups:
for param in param_group['params']:
if param.stop_gradient:
continue
if hasattr(param, "main_grad"):
if param.main_grad is not None:
raise ValueError(
f"gradient should be None, but is {param.main_grad}"
)
param.main_grad = paddle.zeros_like(
param, dtype=paddle.float32
)
else:
if param.grad is not None:
raise ValueError(
f"gradient should be None, but is {param.grad}"
)
param.grad = paddle.zeros_like(param, dtype=param.dtype)
self.step()
# clear the parameter gradient
self._inner_opt.clear_grad(set_to_zero=False)
return self._inner_opt.state_dict()
def _append_optimize_op(self, block, param_and_grad):
if (
in_auto_parallel_align_mode() # In align mode, we use enable_delay_scale_loss by default
and param_and_grad[1].is_dist()
):
placements = param_and_grad[1].placements
meshs = param_and_grad[1].process_mesh
grad = param_and_grad[1]
grad_mesh = grad.process_mesh
def get_mesh(pp_idx=0):
"""
获得pp_idx的mesh
"""
mesh = fleet.auto.get_mesh()
if "pp" in mesh.dim_names:
mesh = mesh.get_mesh_with_dim("pp", pp_idx)
return mesh
ipp = 0
global_mesh = fleet.auto.get_mesh()
if "pp" in global_mesh.dim_names:
pp_degree = global_mesh.get_dim_size("pp")
for i in range(pp_degree):
if meshs.process_ids == get_mesh(i).process_ids:
ipp = i
break
change_mesh = False
if any(
isinstance(placement, dist.Partial) for placement in placements
) and (
(meshs.process_ids == get_mesh(ipp).process_ids)
and (meshs.dim_names != get_mesh(ipp).dim_names)
):
change_mesh = True
if change_mesh:
grad = dist.auto_parallel.moe_utils._dist_reshape(
grad,
grad.shape,
get_mesh(ipp),
[
dist.Partial(dist.ReduceType.kRedSum),
dist.Partial(dist.ReduceType.kRedSum),
],
)
placements = grad.placements
for i in range(len(placements) - 1, -1, -1):
if isinstance(placements[i], dist.Partial):
placements[i] = dist.Replicate()
grad = dist.reshard(grad, grad.process_mesh, placements)
if self.gradient_accumulation_steps > 1 and in_dygraph_mode():
grad /= self.gradient_accumulation_steps
if change_mesh:
grad = dist.auto_parallel.moe_utils._dist_reshape(
grad, grad.shape, grad_mesh, [dist.Replicate()]
)
param_and_grad = (param_and_grad[0], grad)
self._inner_opt._append_optimize_op(block, param_and_grad)
if self.enable_sharding_overlap:
# overlap the first param all_gather with optimizer pass
if hasattr(param_and_grad[0], 'last_idx'):
idx = param_and_grad[0].last_idx
if param_and_grad[0].last_idx == 0:
shard_size = (
self.param_storage[idx]._numel()
// self._sharding_group.nranks
)
begin = shard_size * max(self._sharding_group.rank, 0)
end = begin + shard_size
slice_buffer = paddle._C_ops.view_slice(
self.param_storage[idx], begin, end
)
task = paddle.distributed.all_gather(
self.param_storage[idx],
slice_buffer,
group=self._sharding_group,
sync_op=False,
)
self.param_storage[idx].is_sync = True
else:
self.param_storage[idx].is_sync = False
def _enable_tensor_fusion(self):
os.environ["FLAGS_enable_tensor_fusion"] = "1"
self.enable_tensor_fusion = True
self._shard_fn._enable_tensor_fusion()
def _enable_sharding_overlap(self, layers):
if hasattr(layers, 'config') and layers.config.get("to_static", False):
return
self.enable_sharding_overlap = True
if not isinstance(layers, paddle.nn.Layer):
raise RuntimeError(
f"`layers` must be `paddle.nn.Layer` but got {type(layers)}"
)
self._layers = layers
def _reduce_scatter_gradients(self, grad_storage):
shard_size = grad_storage._numel() // self._sharding_group.nranks
begin = shard_size * max(self._sharding_group.rank, 0)
end = begin + shard_size
reduce_scattered = paddle._C_ops.view_slice(grad_storage, begin, end)
paddle.distributed.reduce_scatter(
reduce_scattered,
grad_storage,
op=paddle.distributed.ReduceOp.SUM,
group=self._sharding_group,
sync_op=False,
).wait()
def _async_sharding_comm(self):
if not self._layers:
raise RuntimeError(
"Sharding overlap requires an initialized model. "
"Call `_enable_sharding_overlap()` to set model."
)
param2layer = {}
for layer in self._layers.sublayers():
for p in layer.parameters(include_sublayers=False):
param2layer[id(p)] = layer
if len(self.fuse_param_view) != len(self.grad_storage):
raise RuntimeError(
f"Length mismatch: fuse_param_view ({len(self.fuse_param_view)}) vs grad_storage ({len(self.grad_storage)})"
)
for i in range(len(self.fuse_param_view)):
self._reduce_scatter_gradients(self.grad_storage[i])
def fuse_comm_hook_func(param_group_len, grad_storage, comm_group):
@paddle.autograd.no_grad()
def fuse_comm(*_):
# Ensures all gards in grad_storage have be checked in
grad_storage.check_in += 1
if grad_storage.check_in == param_group_len:
shard_size = grad_storage._numel() // comm_group.nranks
begin = shard_size * max(comm_group.rank, 0)
end = begin + shard_size
reduce_scattered = paddle._C_ops.view_slice(
grad_storage, begin, end
)
task = paddle.distributed.reduce_scatter(
reduce_scattered,
grad_storage,
op=paddle.distributed.ReduceOp.SUM,
group=comm_group,
sync_op=False,
)
grad_storage.comm_task = task
return fuse_comm
def fuse_all_gather_hook_func(param_storage, comm_group):
@paddle.autograd.no_grad()
def fuse_comm(*_):
# Ensures all_gather param just once per nosync param_storage
if not param_storage.is_sync:
shard_size = param_storage._numel() // comm_group.nranks
begin = shard_size * max(comm_group.rank, 0)
end = begin + shard_size
slice_buffer = paddle._C_ops.view_slice(
param_storage, begin, end
)
task = paddle.distributed.all_gather(
param_storage,
slice_buffer,
group=comm_group,
sync_op=False,
)
param_storage.is_sync = True
return fuse_comm
# Register reduce_scatter hooks on all parameters in this group
param_group_len = (
len(self.fuse_param_view[i]) * self.gradient_accumulation_steps
)
if "pp" in fleet.auto.get_mesh().dim_names:
param_group_len = (
param_group_len * fleet.auto.get_mesh().get_dim_size("pp")
)
for name, view in self.fuse_param_view[i].items():
view['param']._register_backward_hook(
fuse_comm_hook_func(
param_group_len,
self.grad_storage[i],
self._sharding_group,
)
)
# Register all_gather hooks for next chuck's parameters
# (Uses i+1 because we need to prefetch parameters for next layer)
if i < len(self.fuse_param_view) - 1:
first_param = next(iter(self.fuse_param_view[i].values()))[
'param'
]
layer = param2layer.get(id(first_param))
layer.register_forward_pre_hook(
fuse_all_gather_hook_func(
self.param_storage[i + 1],
self._sharding_group,
)
)
def _build_fuse_param_view(
self,
params_and_grads,
sharding_degree,
):
def get_padded_size(param):
size = np.prod(param._local_shape)
align_size = (
alignment[get_current_device_type()]
// align[param.dtype]
* sharding_degree
)
return ((size + align_size - 1) // align_size) * align_size
# Calculate total buffer size needed (with padding)
total_buffer_size = 0
param2index = {}
for param, _ in params_and_grads:
param2index[param.name] = total_buffer_size
total_buffer_size += get_padded_size(param)
# Create fused buffers
param_buffer = paddle.zeros(
shape=[total_buffer_size], dtype=params_and_grads[0][0].dtype
)
param_buffer.is_sync = False
grad_dtype = paddle.float32
grad_buffer = paddle.zeros(shape=[total_buffer_size], dtype=grad_dtype)
grad_buffer.check_in = 0
grad_buffer.comm_task = None
# Create views into the fused buffers
views = {}
for param, grad in params_and_grads:
padded_size = get_padded_size(param)
views[param.name] = {
'param': param,
'index': param2index[param.name],
}
index = param2index[param.name]
param_shape = param.shape
stop_gradient = param.stop_gradient
param.stop_gradient = True
param._local_value().flatten_()
paddle.assign(
param._local_value(),
param_buffer._slice(
index,
index + param._numel(),
),
)
param.stop_gradient = stop_gradient
tmp_param = paddle._C_ops.view_slice(
param_buffer,
index,
index + param._numel(),
)
tmp_param.get_tensor()._set_dims(param._local_shape)
tmp_param = _dtensor_from_local(
tmp_param,
param.process_mesh,
param.placements,
)
param.get_tensor()._share_data_with(tmp_param.get_tensor())
paddle.assign(
grad._local_value(),
grad_buffer._slice(
index,
index + grad._local_value()._numel(),
),
)
tmp_grad = paddle._C_ops.view_slice(
grad_buffer,
index,
index + grad._local_value()._numel(),
)
tmp_grad.get_tensor()._set_dims(grad._local_shape)
tmp_grad = _dtensor_from_local(
tmp_grad,
grad.process_mesh,
grad.placements,
)
param.main_grad = tmp_grad
# Clean up original gradient storage
grad.get_tensor()._clear()
paddle.device.cuda.empty_cache()
return (views, param_buffer, grad_buffer)
def _tensor_fusion(self, params_grads):
"""
1. Tensor Fusion
- Groups params/grads into contiguous param_storage/grad_storage buffers
- Supports non-uniform partitioning across GPUs
- Uses view_slice to access individual params/grads each step
2. Reduce_scatter Overlap
- Overlaps grad reduce_scatter with backward
3. All_gather Overlap
- Overlaps param all_gather with forward
- Strategically scatters all_gather during forward
(Launching all all_gather at once blocks overlap with other sync/comm ops)
"""
if self.do_tensor_fusion_once:
# Execute only once during first step
# Groups params/grads and registers hooks for comm overlap
mesh = dist.auto_parallel.get_mesh()
shard_groups = get_mesh_comm_list(mesh, "dp")
for group in shard_groups:
comm_group = dist.new_group(sorted(group))
if dist.get_rank() in group:
self._sharding_group = comm_group
if "mp" in mesh._dim_names:
mp_mesh_axis = mesh._dim_names.index("mp")
self._mp_degree = mesh._shape[mp_mesh_axis]
mp_groups = get_mesh_comm_list(mesh, "mp")
for group in mp_groups:
comm_group = dist.new_group(sorted(group))
if dist.get_rank() in group:
self._mp_group = comm_group
self.do_tensor_fusion_once = False
parameters = [p_g[0] for p_g in params_grads]
comm_buffer_size_MB = self._strategy.sharding.comm_buffer_size_MB
if comm_buffer_size_MB < 0:
comm_buffer_size_MB = 256
group_size = comm_buffer_size_MB * 1024 * 1024
is_sparse_gradient = [False] * len(parameters)
shape_dict = {param.name: param.shape for param in parameters}
dense_params = [param._local_value() for param in parameters]
# group params according to comm_buffer_size_MB
group_indices = core.eager_assign_group_by_size(
dense_params, is_sparse_gradient, [group_size, group_size]
)
var_groups = OrderedDict()
for group_idx, indices in enumerate(group_indices):
for i in indices:
var_groups.setdefault(group_idx, []).append(params_grads[i])
# create fuse_param_view, param_storage, grad_storage with groups
for group_idx, params_and_grads in var_groups.items():
(
fuse_param_view,
param_storage,
grad_storage,
) = self._build_fuse_param_view(
params_and_grads,
self._sharding_group.nranks,
)
self.fuse_param_view.append(fuse_param_view)
self.param_storage.append(param_storage)
self.grad_storage.append(grad_storage)
if self.enable_sharding_overlap:
# overlap reduce_scatter with backward
# overlap all_gather with forward
self._async_sharding_comm()
# Configure gradient clipping for sharding
if self._inner_opt._grad_clip is not None:
self._inner_opt._grad_clip.should_comm_on_shard_dim = True
self._inner_opt._grad_clip.sharding_group = self._sharding_group
if "mp" in mesh._dim_names and self._mp_degree > 1:
self._inner_opt._grad_clip.mp_group = self._mp_group
new_params = []
new_grads = []
for i in range(len(self.fuse_param_view)):
if not self.enable_sharding_overlap:
self._reduce_scatter_gradients(self.grad_storage[i])
for name, view in self.fuse_param_view[i].items():
param = view['param']
index = view['index']
shard_size = (
self.param_storage[i]._numel()
// self._sharding_group.nranks
)
rank_begin = shard_size * max(self._sharding_group.rank, 0)
rank_end = rank_begin + shard_size
param_begin = max(index, rank_begin)
param_end = min(index + param._numel(), rank_end)
if param_begin >= param_end:
continue
# get new_param from param_storage
new_param = paddle._C_ops.view_slice(
self.param_storage[i], param_begin, param_end
)
new_param = _dtensor_from_local(
new_param,
param.process_mesh,
[dist.Replicate()],
)
new_param.name = name
new_param.stop_gradient = param.stop_gradient
new_param.need_clip = param.need_clip
new_param.persistable = True
new_param.trainable = param.trainable
new_param.stop_gradient = param.stop_gradient
new_param.optimize_attr = param.optimize_attr
new_param.regularizer = param.regularizer
new_param.do_model_average = param.do_model_average
new_param.is_distributed = param.is_distributed
new_params.append(new_param)
# get new_grad from grad_storage
new_grad = paddle._C_ops.view_slice(
self.grad_storage[i], param_begin, param_end
)
new_grad = _dtensor_from_local(
new_grad, param.process_mesh, [dist.Replicate()]
)
new_grads.append(new_grad)
if self.enable_sharding_overlap:
# last_idx marks the last param, start asyn comm
new_params[-1].last_idx = i
if self.grad_storage[i].comm_task is not None:
self.grad_storage[i].comm_task.wait()
new_params_grads = list(zip(new_params, new_grads))
return new_params_grads
def _fused_comm_before_apply_optimize(self, params_grads):
'''
Optimizes gradient placements for parameters in dynamic sharding mode to minimize redundant allreduce
operations during gradient clipping. This function adjusts tensor placements across mesh axes based
on priority rules, prioritizing sharding for dimensions marked in `_sharding_axis`.
For each axis in the mesh:
1. Preserves existing `Shard(dim)` placements for any axis.
2. Converts `Partial()` placements to Shard(dim) where possible, falling back to `Replicate()` if sharding isn't feasible.
3. Maintains `Replicate()` placements unchanged.
Processes axes in order of `_sharding_axis` first before other mesh axes in their natural order.
e.g.
a) sharding_axis = 0, tensor rank = 2,
placements: [Partial(), Partial(), Repliacate()] -> [Shard(0), Shard(1), Repliacate()]
b) sharding_axis = 0, tensor rank = 2,
placements: [Partial(), Shard(0), Partial() ] -> [Shard(1), Shard(0), Repliacate()]
'''
new_params_grads = []
# Get the first non-shard tensor_dim of tensor shape in ascending order.
# `shard_dims_set` records if tensor_dim is marked as shard in placement.
def get_first_can_shard_dim(tensor_shape, shard_dims_set):
for tensor_dim in range(len(tensor_shape)):
# The rank of the current dimension of the tensor is 1, so there is no need to shard it.
if tensor_shape[tensor_dim] == 1:
continue
if tensor_dim not in shard_dims_set:
return tensor_dim
return -1
for param, grad in params_grads:
new_placements = copy.deepcopy(grad.placements)
new_grad = grad
tensor_shape = grad._local_shape
shard_dims_set = set()
mesh_shape = grad.process_mesh.shape
# 1. `shard_dims_set` records dims marked as shard in placement.
for placement in grad.placements:
if placement.is_shard():
tensor_dim = placement.get_dim()
shard_dims_set.add(tensor_dim)
# 2. Prioritize process `_sharding_axis`.
tensor_dim = get_first_can_shard_dim(tensor_shape, shard_dims_set)
# 2.1 Preserves existing shard status placements.
if not grad.placements[self._sharding_axis].is_shard():
# 2.2 Default to maintain replicate status.
new_placements[self._sharding_axis] = dist.Replicate()
# 2.3 Converts partial status to shard status where possible.
if tensor_dim != -1 and mesh_shape[self._sharding_axis] != 1:
shard_dims_set.add(tensor_dim)
new_placements[self._sharding_axis] = dist.Shard(tensor_dim)
# 3. Processes other mesh axes in their natural order.
for mesh_axis, placement in enumerate(grad.placements):
if mesh_axis == self._sharding_axis:
continue
# 3.1 No sharding is needed as single-device mesh axis.
if mesh_shape[mesh_axis] == 1:
new_placements[mesh_axis] = dist.Replicate()
continue
# 3.2 Keep shard states in placements unchanged.
if not placement.is_shard():
new_placements[mesh_axis] = dist.Replicate()
tensor_dim = get_first_can_shard_dim(
tensor_shape, shard_dims_set
)
# 3.3 When in partial state, convert to shard state as much as possible.
if placement.is_partial():
if tensor_dim == -1:
new_placements[mesh_axis] = dist.Replicate()
else:
# 3.4 Default to maintain replicate status.
shard_dims_set.add(tensor_dim)
new_placements[mesh_axis] = dist.Shard(tensor_dim)
# 4. Update placements.
if grad.placements != new_placements:
new_grad = dist.reshard(grad, grad.process_mesh, new_placements)
new_params_grads.append((param, new_grad))
return new_params_grads
def _apply_optimize(
self, loss, startup_program, params_grads, param_group_idx=0
):
if paddle.in_dynamic_mode() and isinstance(
self._shard_fn, ShardingStage1
):
if self.enable_tensor_fusion:
# tensor fusion fuse params/grads into large chunks, no need _fused_comm_before_apply_optimize.
params_grads = self._tensor_fusion(params_grads)
else:
params_grads = self._fused_comm_before_apply_optimize(
params_grads
)
return super()._apply_optimize(
loss, startup_program, params_grads, param_group_idx
)
def __getattr__(self, item):
if "_inner_opt" in self.__dict__:
if item == "_inner_opt":
return self.__dict__[item]
return getattr(self.__dict__["_inner_opt"], item)
else:
raise AttributeError
def __setattr__(self, item, value):
if item == '_inner_opt':
msg = f'{type(self).__name__}._inner_opt is READ ONLY'
raise AttributeError(msg)
return setattr(self._inner_opt, item, value)
class _ShardingStageBase:
def __init__(self, mesh, sharding_mesh_dim):
self._mesh = mesh
self._sharding_axis = 0
self._sharding_mesh_dim = sharding_mesh_dim
self.enable_tensor_fusion = False
def _set_sharding_axis(self, sharding_axis):
self._sharding_axis = sharding_axis
def _enable_tensor_fusion(self):
self.enable_tensor_fusion = True
def shard_master_weight(
self, param: Tensor, master_weight: Tensor
) -> Tensor:
if param.is_dist():
if self.enable_tensor_fusion:
placements = param.placements
else:
placements = get_placement_with_sharding(
param, self._sharding_axis
)
if isinstance(master_weight, pir.Value):
data_op = master_weight.get_defining_op()
assert data_op.name() == "pd_op.data", (
"The master weight must be a result of data op."
)
dim_map, partial_status = to_dim_map(
placements, len(master_weight.shape)
)
dist_attr = (
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
param.process_mesh, dim_map, partial_status
)
)
dist_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
master_weight.type(), dist_attr
)
master_weight.set_type(dist_type)
data_op.dist_attr = (
paddle.base.libpaddle.pir.create_op_dist_attribute(
param.process_mesh, [], [dist_attr]
)
)
if paddle.in_dynamic_mode() and master_weight.is_dist():
master_weight = reshard(
master_weight,
mesh=param.process_mesh,
placements=placements,
)
return master_weight
def _init_dist_attr(self, tensor: Tensor, param: Tensor, placements: list):
dim_map, partial_status = to_dim_map(placements, len(tensor.shape))
dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
param.process_mesh, dim_map, partial_status
)
dist_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
tensor.type(), dist_attr
)
tensor.set_type(dist_type)
op_dist_attr = paddle.base.libpaddle.pir.create_op_dist_attribute(
param.process_mesh, [], [dist_attr]
)
tensor.get_defining_op().dist_attr = op_dist_attr
def _apply_placement(
self, tensor: Tensor, param: Tensor, placements: list
) -> Tensor:
if tensor.is_dist():
op = tensor.get_defining_op()
if op.name() == "pd_op.data":
self._init_dist_attr(tensor, param, placements)
return tensor
return dist.reshard(tensor, param.process_mesh, placements)
return shard_tensor(
tensor,
mesh=param.process_mesh,
placements=placements,
)
def _reshard_fake_replicate_grad_to_partial(self, grad: Tensor) -> Tensor:
return _fake_replicate_grad_to_partial(grad, self._sharding_axis)
def _register_hook_for_param_grad(self, param):
def _reshard_grad(grad):
# do reshard only if the grad is dist tensor and in partial status
if grad.is_dist():
partial_mesh_axis = None
for mesh_axis, placement in enumerate(grad.placements):
if isinstance(placement, dist.Partial):
partial_mesh_axis = mesh_axis
if partial_mesh_axis is not None:
new_placements = get_placement_with_sharding(
grad, partial_mesh_axis
)
return reshard(grad, grad.process_mesh, new_placements)
return grad
def _main_grad_hook(grad):
tmp_grad = paddle.cast(grad, paddle.float32)
grad._clear_data()
if param.main_grad is None:
param.main_grad = _reshard_grad(tmp_grad)
else:
param.main_grad.add_(_reshard_grad(tmp_grad))
if amp_global_state().use_master_grad:
param.main_grad = None
param.register_hook(_main_grad_hook)
amp_global_state().already_register_final_backward_hook = True
else:
param.register_hook(_reshard_grad)
class _ShardingStage0(_ShardingStageBase):
def __init__(
self, sharding_mesh_dim: int | str, mesh: ProcessMesh | None = None
) -> None:
super().__init__(mesh, sharding_mesh_dim)
self.sharding_axis = 0
def __call__(self, key: str, param: Tensor, tensor: Tensor) -> Tensor:
if key == "grad" and in_auto_dp_mode():
return self._reshard_fake_replicate_grad_to_partial(tensor)
return tensor
class ShardingStage1(_ShardingStageBase):
"""
A builtin shard_fn for shard_optimizer interface, users can pass it to shard_optimizer to implement sharding optimization with stage 1.
Args:
sharding_mesh_dim(int|str): The sharding dimension in the mesh.
mesh(None|paddle.distributed.ProcessMesh): If mesh is not None, the `ProcessMesh` object describes the Cartesian topology of the used processes for dense type parameters. Note: Currently, only one mesh configuration is supported for all dense parameters. If there is a need for multiple mesh configurations, please configure them yourself in the upper layer networking code.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> class MLP(paddle.nn.Layer):
... def __init__(self):
... super().__init__()
... self.fc1 = paddle.nn.Linear(8, 8)
... self.fc2 = paddle.nn.Linear(8, 8)
...
... def forward(self, input):
... return self.fc2(self.fc1(input))
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> layer = MLP()
>>> batch = paddle.rand(shape=[8, 8])
>>> opt = paddle.optimizer.AdamW(parameters=layer.parameters())
>>> opt = dist.shard_optimizer(opt, dist.ShardingStage1("x", mesh))
>>> for _ in range(5):
>>> loss = layer(batch)
>>> loss.backward()
>>> opt.step()
>>> opt.clear_grad()
>>> # This case need to be executed in multi-card environment
>>> # python -m paddle.distributed.launch --gpus=0,1 {test_case}.py
"""
def __init__(
self,
sharding_mesh_dim: int | str,
mesh: ProcessMesh | None = None,
) -> None:
super().__init__(mesh, sharding_mesh_dim)
def __call__(self, key: str, param: Tensor, tensor: Tensor) -> Tensor:
if not param.is_dist():
return tensor
# Only deal with momentum in optimizer, beta should be replicated cross param's mesh
if not self.enable_tensor_fusion and 'beta' not in key:
placements = get_placement_with_sharding(param, self._sharding_axis)
else:
placements = [
dist.Replicate() for _ in range(len(param.process_mesh.shape))
]
if key == "grad" and in_auto_dp_mode():
tensor = self._reshard_fake_replicate_grad_to_partial(tensor)
return self._apply_placement(tensor, param, placements)
class ShardingStage2(_ShardingStageBase):
"""
A builtin shard_fn for shard_optimizer interface, users can pass it to shard_optimizer to implement sharding optimization with stage 2.
Args:
sharding_mesh_dim(int|str): The sharding dimension name in the mesh.
mesh(None|paddle.distributed.ProcessMesh): If mesh is not None, the `ProcessMesh` object describes the Cartesian topology of the used processes for dense type parameters. Note: Currently, only one mesh configuration is supported for all dense parameters. If there is a need for multiple mesh configurations, please configure them yourself in the upper layer networking code.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> class MLP(paddle.nn.Layer):
... def __init__(self):
... super().__init__()
... self.fc1 = paddle.nn.Linear(8, 8)
... self.fc2 = paddle.nn.Linear(8, 8)
...
... def forward(self, input):
... return self.fc2(self.fc1(input))
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> layer = MLP()
>>> batch = paddle.rand(shape=[8, 8])
>>> opt = paddle.optimizer.AdamW(parameters=layer.parameters())
>>> opt = dist.shard_optimizer(opt, dist.ShardingStage2("x", mesh))
>>> for _ in range(5):
>>> loss = layer(batch)
>>> loss.backward()
>>> opt.step()
>>> opt.clear_grad()
>>> # This case need to be executed in multi-card environment
>>> # python -m paddle.distributed.launch --gpus=0,1 {test_case}.py
"""
def __init__(
self,
sharding_mesh_dim: int | str,
mesh: ProcessMesh | None = None,
) -> None:
super().__init__(mesh, sharding_mesh_dim)
def __call__(self, key: str, param: Tensor, tensor: Tensor) -> Tensor:
if param.is_dist():
# Only deal with momentum in optimizer, beta should be replicated cross param's mesh
if 'beta' not in key:
placements = get_placement_with_sharding(
param, self._sharding_axis
)
else:
placements = [
dist.Replicate()
for _ in range(len(param.process_mesh.shape))
]
return shard_tensor(
tensor,
mesh=param.process_mesh,
placements=placements,
)
return tensor
class ShardingStage3(_ShardingStageBase):
"""
A builtin shard_fn for shard_optimizer interface, users can pass it to shard_optimizer to implement sharding optimization with stage 3.
Args:
sharding_mesh_dim(int|str): The sharding dimension name in the mesh.
mesh(None|paddle.distributed.ProcessMesh): If mesh is not None, the `ProcessMesh` object describes the Cartesian topology of the used processes for dense type parameters. Note: Currently, only one mesh configuration is supported for all dense parameters. If there is a need for multiple mesh configurations, please configure them yourself in the upper layer networking code.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> class MLP(paddle.nn.Layer):
... def __init__(self):
... super().__init__()
... self.fc1 = paddle.nn.Linear(8, 8)
... self.fc2 = paddle.nn.Linear(8, 8)
...
... def forward(self, input):
... return self.fc2(self.fc1(input))
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> layer = MLP()
>>> batch = paddle.rand(shape=[8, 8])
>>> opt = paddle.optimizer.AdamW(parameters=layer.parameters())
>>> opt = dist.shard_optimizer(opt, dist.ShardingStage3("x", mesh))
>>> for _ in range(5):
>>> loss = layer(batch)
>>> loss.backward()
>>> opt.step()
>>> opt.clear_grad()
>>> # This case need to be executed in multi-card environment
>>> # python -m paddle.distributed.launch --gpus=0,1 {test_case}.py
"""
def __init__(
self,
sharding_mesh_dim: int | str,
mesh: ProcessMesh | None = None,
) -> None:
super().__init__(mesh, sharding_mesh_dim)
def _shard_parameter(self, param):
if param.is_dense() and self._mesh is not None:
placements = []
for _ in range(len(self._mesh.shape)):
placements.append(dist.Replicate())
param._to_dist_(placements, self._mesh)
if param.is_dist():
new_placements = get_placement_with_sharding(
param, self._sharding_axis
)
shard_param = dist.reshard(
param, param.process_mesh, new_placements
)
# change the holder of param to new shard_param
param.get_tensor()._share_data_with(shard_param.get_tensor())
def _unshard_parameter(self, param):
if param.is_dist():
new_placements = param.placements
if isinstance(new_placements[self._sharding_axis], dist.Shard):
new_placements[self._sharding_axis] = dist.Replicate()
new_param = dist.reshard(param, param.process_mesh, new_placements)
param.get_tensor()._share_data_with(new_param.get_tensor())
def __call__(self, key: str, param: Tensor, tensor: Tensor) -> Tensor:
if not param.is_dist():
return tensor
if key == "grad" and in_auto_dp_mode():
raise RuntimeError(
"Sharding Stage 3 does not support auto dp mode yet."
)
if 'beta' not in key:
placements = param.placements
if all(isinstance(p, dist.Replicate) for p in placements):
placements = get_placement_with_sharding(
param, self._sharding_axis
)
else:
placements = [dist.Replicate() for _ in param.process_mesh.shape]
return self._apply_placement(tensor, param, placements)
def shard_optimizer(
optimizer: Optimizer,
shard_fn: Callable[[str, Tensor, Tensor], Tensor] | None = None,
gradient_accumulation_steps: int = 1,
) -> _ShardOptimizer:
"""
Warp the global view optimizer to distributed view.
Note:
The `shard_fn` should have the following signature:
def shard_fn(accumulator_name, param, accumulator) -> sharded_accumulator
Args:
optimizer (paddle.optimizer.Optimizer): The optimizer to be sharded.
shard_fn (Callable, optional): The function to shard accumulators. If not specified,
we simply pass down the dist attr of the params.
Returns:
An optimizer with distributed view.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> class MLP(paddle.nn.Layer):
... def __init__(self):
... super().__init__()
... self.fc1 = paddle.nn.Linear(8, 8)
... self.fc2 = paddle.nn.Linear(8, 8)
...
... def forward(self, input):
... return self.fc2(self.fc1(input))
>>> layer = MLP()
>>> batch = paddle.rand(shape=[8, 8])
>>> opt = paddle.optimizer.AdamW(parameters=layer.parameters())
>>> opt = dist.shard_optimizer(opt)
>>> for _ in range(5):
>>> loss = layer(batch)
>>> loss.backward()
>>> opt.step()
>>> opt.clear_grad()
>>> # This case need to be executed in multi-card environment
>>> # python -m paddle.distributed.launch --gpus=0,1 {test_case}.py
"""
return _ShardOptimizer(optimizer, shard_fn, gradient_accumulation_steps)
def shard_scaler(scaler: GradScaler) -> GradScaler:
"""
Warp the global view grad_scaler to distributed view.
Args:
scaler (paddle.amp.GradScaler): The GradScaler to be sharded.
Returns:
A GradScaler with distributed view.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> class MLP(paddle.nn.Layer):
... def __init__(self):
... super().__init__()
... self.fc1 = paddle.nn.Linear(8, 8)
... self.fc2 = paddle.nn.Linear(8, 8)
...
... def forward(self, input):
... return self.fc2(self.fc1(input))
>>> layer = MLP()
>>> batch = paddle.rand(shape=[8, 8])
>>> opt = paddle.optimizer.AdamW(parameters=layer.parameters())
>>> layer, opt = paddle.amp.decorate(layer, opt, level='O2')
>>> scaler = paddle.amp.GradScaler(init_loss_scaling=1024)
>>> scaler = dist.shard_scaler(scaler)
>>> opt = dist.shard_optimizer(opt)
>>> for _ in range(5):
>>> with paddle.amp.auto_cast(True):
>>> loss = layer(batch)
>>> scaled = scaler.scale(loss)
>>> scaled.backward()
>>> scaler.step(opt)
>>> scaler.update()
>>> opt.clear_grad()
>>> # This case need to be executed in multi-card environment
>>> # python -m paddle.distributed.launch --gpus=0,1 {test_case}.py
"""
def unscale_method(self, optimizer):
if not self._enable:
return
optimizer_state = self._optimizer_states[id(optimizer)]
if optimizer_state["state"] is OptimizerState.UNSCALED:
raise RuntimeError(
"unscale_() has already been called on this optimizer since the last update()."
)
elif optimizer_state["state"] is OptimizerState.STEPPED:
raise RuntimeError("unscale_() is being called after step().")
src_mesh = None
current_process_mesh = None
self._found_inf = paddle.to_tensor(np.array([0]).astype(np.bool_))
mesh2param_grads = {}
if getattr(optimizer, '_param_groups', None) and isinstance(
optimizer._param_groups[0], dict
):
for group in optimizer._param_groups:
for param in group['params']:
tgt_grad = param._grad_ivar()
if (
tgt_grad is not None
and getattr(
tgt_grad, '_is_initialized', lambda: False
)()
):
if (
src_mesh is None
and tgt_grad.process_mesh is not None
):
src_mesh = tgt_grad.process_mesh
else:
pass
if (
current_process_mesh is None
and tgt_grad._is_initialized()
and tgt_grad.process_mesh is not None
):
current_process_mesh = tgt_grad.process_mesh
if tgt_grad.process_mesh not in mesh2param_grads:
mesh2param_grads[tgt_grad.process_mesh] = [tgt_grad]
else:
mesh2param_grads[tgt_grad.process_mesh].append(
tgt_grad
)
else:
for param in optimizer._parameter_list:
tgt_grad = param._grad_ivar()
if (
tgt_grad is not None
and getattr(tgt_grad, '_is_initialized', lambda: False)()
):
if src_mesh is None:
src_mesh = tgt_grad.process_mesh
if (
current_process_mesh is None
and tgt_grad._is_initialized()
):
current_process_mesh = tgt_grad.process_mesh
if tgt_grad.process_mesh not in mesh2param_grads:
mesh2param_grads[tgt_grad.process_mesh] = [tgt_grad]
else:
mesh2param_grads[tgt_grad.process_mesh].append(tgt_grad)
for _, param_grads in mesh2param_grads.items():
temp_param_grads_half = []
temp_param_grads_fp32 = []
temp_found_inf = paddle.to_tensor(np.array([0]).astype(np.bool_))
temp_found_inf_half = paddle.to_tensor(
np.array([0]).astype(np.bool_)
)
temp_found_inf_fp32 = paddle.to_tensor(
np.array([0]).astype(np.bool_)
)
if self._scale.is_dist():
temp_scale = self._scale._local_value()
else:
temp_scale = self._scale
for grad in param_grads:
if grad.dtype in [
core.VarDesc.VarType.FP16,
paddle.float16,
core.VarDesc.VarType.BF16,
paddle.bfloat16,
]:
temp_param_grads_half.append(grad)
else:
temp_param_grads_fp32.append(grad)
if len(temp_param_grads_half):
_, temp_found_inf_half = _C_ops.check_finite_and_unscale_(
temp_param_grads_half,
temp_scale,
)
# AllReduce for "bool" is not supported on XPU
if "xpu" in paddle.device.get_device():
temp_param_grads_half = paddle.cast(
temp_param_grads_half, "int32"
)
temp_param_grads_half = paddle.sum(temp_param_grads_half)
temp_param_grads_half = paddle.cast(
temp_param_grads_half, "bool"
)
temp_found_inf = _C_ops.bitwise_or(
temp_found_inf, temp_found_inf_half
)
if len(temp_param_grads_fp32):
_, temp_found_inf_fp32 = _C_ops.check_finite_and_unscale_(
temp_param_grads_fp32,
temp_scale,
)
# AllReduce for "bool" is not supported on XPU
if "xpu" in paddle.device.get_device():
temp_found_inf_fp32 = paddle.cast(
temp_found_inf_fp32, "int32"
)
temp_found_inf_fp32 = paddle.sum(temp_found_inf_fp32)
temp_found_inf_fp32 = paddle.cast(
temp_found_inf_fp32, "bool"
)
temp_found_inf = _C_ops.bitwise_or(
temp_found_inf, temp_found_inf_fp32
)
# All the 'temp_found_inf' will be `resharded` to `src_mesh` to calculate the value of `self._found_inf`.
temp_found_inf = dist.reshard(
temp_found_inf, src_mesh, temp_found_inf.placements
)
self._found_inf = _C_ops.bitwise_or(self._found_inf, temp_found_inf)
# The rank of src_mesh, should not overwrite the original variable `self._found_inf`
if self._found_inf.process_mesh == current_process_mesh:
for process_mesh in mesh2param_grads.keys():
_ = dist.reshard(
self._found_inf, process_mesh, self._found_inf.placements
)
else:
if current_process_mesh is None or not hasattr(
current_process_mesh, "ranks"
):
raise ValueError(
"Invalid current_process_mesh: must be a valid ProcessMesh."
)
# The rank of other mesh, should overwrite the original variable `self._found_inf`
self._found_inf = dist.reshard(
self._found_inf,
current_process_mesh,
self._found_inf.placements,
)
optimizer_state["state"] = OptimizerState.UNSCALED
scaler._unscale = MethodType(unscale_method, scaler)
return scaler
# Part4: Convert To Static Graph related APIs
class FusePasses:
"""
A helper class for users to configure the fuse passes.
"""
enable: bool
gemm_epilogue: bool
dropout_add: bool
def __init__(self, config_dict=None):
self.enable = False
self.gemm_epilogue = False
self.dropout_add = False
if config_dict is not None:
for key, value in config_dict.items():
if hasattr(self, key):
setattr(self, key, value)
else:
raise ValueError(f"Unknown fuse pass {key}")
class Strategy(auto_strategy.BaseConfig):
"""
The `Strategy` object is used to configure the parallelization
and optimization strategies for static graph. Currently supports
configuring ``sharding``, ``fused_passes``, ``gradient_merge``
and ``pipeline``. More strategies will be supported in the future.
``sharding`` is used to configure the sharding states of the optimizer,
for saving the GPU memory.
``fused_passes`` is used to configure the fusion of the computation in
the model.
``gradient_merge`` is used to configure the gradient merge strategy in
training.
``pipeline`` is used to configure the pipeline parallelism strategy.
Args:
config(dict|None, optional): The user-defined configurations.
If ``config`` is None, use default configurations. If it is
a dict, the items inside the dict will be used to set the
configurations, and the others remain the default values.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> strategy = dist.Strategy()
>>> strategy.sharding.enable = True
>>> strategy.sharding.stage = 2
>>> strategy.sharding.degree = 2
>>> strategy.gradient_merge.enable = True
>>> strategy.gradient_merge.k_steps = 2
>>> strategy.gradient_merge.avg = False
>>> strategy.pipeline.enable = True
>>> strategy.pipeline.schedule_mode = "1F1B" # default is "1F1B"
>>> strategy.pipeline.micro_batch_size = 2
"""
def __init__(self, config: _Config | None = None) -> None:
if config is not None:
if isinstance(config, dict):
self._config_dict = copy.deepcopy(config)
else:
raise ValueError(
f"Expected a dictionary. But received: {config}"
)
else:
self._config_dict = {}
category = auto_strategy.constants.BASE
super().__init__(category, self._config_dict)
config_dict = self._config_dict.get(
auto_strategy.constants.SHARDING, None
)
self._sharding = auto_strategy.ShardingConfig(config_dict)
config_dict = self._config_dict.get(
auto_strategy.constants.GRADIENT_MERGE, None
)
self._gradient_merge = auto_strategy.GradientMergeConfig(config_dict)
config_dict = self._config_dict.get(
auto_strategy.constants.PIPELINE, None
)
self._pipeline = auto_strategy.PipelineConfig(config_dict)
config_dict = self._config_dict.get(auto_strategy.constants.AMP, None)
self._amp = auto_strategy.AMPConfig(config_dict)
config_dict = self._config_dict.get(
auto_strategy.constants.FUSED_PASSES, None
)
self._fused_passes = FusePasses(config_dict)
# template interface
config_dict = self._config_dict.get(
auto_strategy.constants.RECOMPUTE, None
)
self._recompute = auto_strategy.RecomputeConfig(config_dict)
config_dict = self._config_dict.get(
auto_strategy.constants.MP_OPTIMIZATION, None
)
self._mp_optimization = auto_strategy.MPOptimizationConfig(config_dict)
config_dict = self._config_dict.get(
auto_strategy.constants.DP_OPTIMIZATION, None
)
self._dp_optimization = auto_strategy.DPOptimizationConfig(config_dict)
config_dict = self._config_dict.get(
auto_strategy.constants.SP_OPTIMIZATION, None
)
self._sp_optimization = auto_strategy.SPOptimizationConfig(config_dict)
self._full_graph = self._config_dict.get("full_graph", True)
def _from_legacy_strategy(self, legacy_strategy):
"""
NOTE(lizhiyu): This is a template function to get `dist.Strategy` from `fleet.auto.Strategy`.
"""
import copy
category = auto_strategy.constants.BASE
base_config = auto_strategy.constants.get_category_default_config(
category
)
for key in base_config.keys():
setattr(self, key, getattr(legacy_strategy, key))
self._fused_passes.enable = legacy_strategy.fused_passes.enable
if (
"fused_gemm_epilogue_pass"
in legacy_strategy.fused_passes.fused_passes_list
):
self._fused_passes.gemm_epilogue = True
if (
"fused_dropout_add_pass"
in legacy_strategy.fused_passes.fused_passes_list
):
self._fused_passes.dropout_add = True
self._amp = copy.deepcopy(legacy_strategy.amp)
self._sharding = copy.deepcopy(legacy_strategy.sharding)
self._gradient_merge = copy.deepcopy(legacy_strategy.gradient_merge)
self._pipeline = copy.deepcopy(legacy_strategy.pipeline)
# The below are template interfaces
self._recompute = copy.deepcopy(legacy_strategy.recompute)
self._mp_optimization = copy.deepcopy(legacy_strategy.mp_optimization)
self._dp_optimization = copy.deepcopy(legacy_strategy.dp_optimization)
self._sp_optimization = copy.deepcopy(legacy_strategy.sp_optimization)
@property
def full_graph(self) -> bool:
"""
Whether to use AST mode.
"""
return self._full_graph
@property
def sharding(self) -> auto_strategy.ShardingConfig:
"""
``sharding`` is used to configure the sharding states of the optimizer,
containing following configs:
``enable`` (bool): whether to enable sharding. Default: False.
``stage`` (int): can be set to 1, 2 or 3. 1 indicates the optimizer states segmentation,
2 indicates optimizer states and gradient segmentation, 3 indicates the segmentation
of optimizer states, gradient and parameters. Default: 1.
``degree`` (int): the number of segmentation pieces. Default: 8.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> strategy = dist.Strategy()
>>> strategy.sharding.enable = True
>>> strategy.sharding.stage = 2
>>> strategy.sharding.degree = 2
"""
return self._sharding
@property
def gradient_merge(self) -> auto_strategy.GradientMergeConfig:
"""
``gradient_merge`` is used to configure the gradient merge strategy in
training, containing following configs:
``enable`` (bool): whether to enable gradient merge. Default: False.
``k_steps`` (int): the number of steps for merging gradients. Default: 1.
``avg`` (bool): whether to average the gradients of each step. Default: True.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> strategy = dist.Strategy()
>>> strategy.gradient_merge.enable = True
>>> strategy.gradient_merge.k_steps = 2
>>> strategy.gradient_merge.avg = True
"""
return self._gradient_merge
@property
def fused_passes(self) -> FusePasses:
"""
``fused_passes`` is used to configure the fusion of the computation in
the model, containing following configs:
``enable`` (bool): whether to enable fused passes. Default: False.
``gemm_epilogue`` (bool): whether to fuse ``matmul`` and ``add`` computation
in the ``Linear`` layer. Default: False
"dropout_add" (bool): whether to fuse ``dropout`` and ``add`` computation. Default: False.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> strategy = dist.Strategy()
>>> strategy.fused_passes.enable = True
>>> strategy.fused_passes.gemm_spilogue = True
>>> strategy.fused_passes.dropout_add = True
"""
return self._fused_passes
@property
def pipeline(self) -> auto_strategy.PipelineConfig:
"""
``pipeline`` is used to configure the pipeline parallelism,
containing following configs:
``enable`` (bool): whether to enable pipeline parallelism. Default: False.
``schedule_mode`` (str): the scheduling mode of pipeline parallelism. Default: "1F1B".
``micro_batch_size`` (int): the size of each micro-batch inside a mini-batch. Default: 1.
``accumulate_steps`` (int): number of steps for accumulating. Default: 1.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> strategy = dist.Strategy()
>>> strategy.pipeline.enable = True
>>> strategy.pipeline.micro_batch_size = 2
"""
return self._pipeline
@property
def amp(self) -> auto_strategy.AMPConfig:
"""
``amp`` is used to configure the amp,
containing following configs:
``enable`` (bool): whether to enable AMP. Default: False.
``dtype``, (str): the data type of AMP. Default: "float16".
``level``, (str): the level of AMP. Default: "O1".
``init_loss_scaling``, (float): the initial value of loss scaling. Default: 32768.0
``incr_every_n_steps``, (int): the number of steps for increasing loss scaling. Default: 1000
``decr_every_n_nan_or_inf``, (int): the number of steps for decreasing loss scaling. Default: 2
``incr_ratio``, (float): the ratio for increasing loss scaling. Default: 2.0
``decr_ratio``, (float): the ratio for decreasing loss scaling. Default: 2.0
``use_dynamic_loss_scaling``, (bool): whether to use dynamic loss scaling. Default: False
``custom_white_list``, (list): the list of names for which AMP will be applied. Default: []
``custom_black_list``, (list): the list of names for which AMP will not be applied. Default: []
``custom_black_varnames``, (list): the list of names for which AMP will not be applied. Default: []
``use_fp16_guard``, (bool): whether to use fp16 guard. Default: False
``use_bf16_guard``, (bool): whether to use bf16 guard. Default: False
``use_master_grad``, (bool): whether to use master grad. Default: False
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> strategy = dist.Strategy()
>>> strategy.amp.enable = True
>>> strategy.amp.dtype = "float16"
>>> strategy.amp.level = "O2"
"""
return self._amp
class DistModel:
"""
`DistModel` is the model converted from a ``paddle.nn.layer`` with distributed
tensors as its parameters. It contains the static graph converted from a
``paddle.nn.layer`` whose parameters are distributed tensors (constructed
from ``paddle.distributed.shard_tensor``), and provides the APIs for training,
evaluation and prediction with the static graph.
It is suggested to generate DistModel by ``paddle.distributed.to_static``,
not directly by ``paddle.distributed.DistModel``.
Please first set the DistModel to "train", "eval" or "predict" mode with
``train()/eval()/predict()`` method and then use the ``__call__`` method for
training, evaluation and prediction respectively.
For more details of the usage, please refer to the sample code in
``paddle.distributed.to_static``.
Args:
layer(paddle.nn.Layer): The layer in dygraph mode, whose parameters
are distributed tensors generated by ``shard_tensor``.
loader(ShardDataLoader|paddle.io.DataLoader): The data loader used in dygraph mode,
used to infer inputs_spec and labels_spec.
loss(Loss|Callable|None, optional): The loss function for training
or evaluating the model. Can be a `paddle.nn.Layer` instance or
any callable function. If loss is not None, DistModel will be set
to "train" (when the optimizer is also not None) or "eval" mode
(when optimizer is None) in default. If it is None, DistModel will
be set to "predict" mode in default. Default: None.
optimizer(paddle.optimizer.Optimizer|None, optional): The optimizer
for training. If both optimizer and loss are set, DistModel will
be set to "train" mode in default. Default: None.
strategy(paddle.distributed.Strategy|None, optional): Configs for
parallel strategies and optimization settings (e.g. sharding,
pipeline parallelism). Default: None.
input_spec(list[list[paddle.distributed.DistributedInputSpec]]|None, optional):
The custom input specs specify the shape, dtype, and name information
of model inputs and labels. If it is not None, the input specs and
label specs will be inferred from the custom input specs. The custom
input specs should be a list containing two sublists: the first
sublist represents theinput specs, and the second sublist represents
the label specs. Default: None.
"""
def __init__(
self,
layer: Layer,
loader: ShardDataloader | DataLoader,
loss: Layer | Callable[..., Any] | None = None,
optimizer: Optimizer | None = None,
strategy: Strategy | None = None,
metrics: list[Metric] | None = None,
input_spec: list[list[DistributedInputSpec]] | None = None,
) -> None:
self._inner_strategy = self.__convert_strategy(strategy)
self._structured_to_parameter_name = {
k: v.name for k, v in layer.state_dict().items()
}
self._parameter_to_structured_name = {
v: k for k, v in self._structured_to_parameter_name.items()
}
if os.getenv("POD_NAME"):
dist.utils.log_utils.get_logger(logging.INFO).info(
"Distribute training by paddle.distributed.launch"
)
dist.fleet.init(is_collective=True)
if (
strategy
and strategy.sharding.enable_tensor_fusion
and isinstance(optimizer, _ShardOptimizer)
and hasattr(optimizer, '_shard_fn')
and hasattr(optimizer, '_inner_opt')
and use_pir_api()
):
assert isinstance(optimizer._shard_fn, ShardingStage1), (
"The shard_fn should be ShardingStage1 "
"when stage1 tensor fusion is enabled."
)
if isinstance(optimizer._shard_fn, ShardingStage1):
shard_fn = optimizer._shard_fn
inner_opt = optimizer._inner_opt
optimizer = ShardingOptimizerStage1(
inner_opt, shard_fn, self._inner_strategy
)
else:
logging.warning(
"Sharding tensor fusion only support ShardingStage1 now."
)
self._engine = Engine(
layer, loss, optimizer, metrics, strategy=self._inner_strategy
)
self._mode = None
self._feed_name_list = {}
# convert dygraph model to static model
if input_spec is not None:
self._engine._inputs_spec = input_spec[0]
self._engine._labels_spec = input_spec[1]
elif isinstance(loader, ShardDataloader):
(
self._engine._inputs_spec,
self._engine._labels_spec,
) = self._engine._prepare_data_spec_from_dataloader(loader)
else:
batch_size = loader.batch_sampler.batch_size
(
self._engine._inputs_spec,
self._engine._labels_spec,
) = self._engine._prepare_data_spec(
loader.dataset, None, batch_size
)
# paddle.enable_static() will be called implicitly in self._engine.prepare.
# call paddle.disable_static to keep the outside of DistModel in dynamic graph mode
# set the default mode
self._in_pir_mode = paddle.base.framework.get_flags(
"FLAGS_enable_pir_api"
)["FLAGS_enable_pir_api"]
if (
not self._in_pir_mode
): # TODO (2024-Q2) remove this when pir mode is fully constructed.
if optimizer is not None and loss is not None:
self.train()
elif loss is not None:
self.eval()
else:
self.predict()
def train(self) -> None:
"""
Set the DistModel to "train" mode. In "train" mode,
executing ``__call__`` method will update the
parameters of the model and return the loss.
"""
if not self._engine._has_prepared["train"]:
self._engine._prepare_program(mode="train", init_parameters=False)
self._mode = "train"
self._engine.to_mode("train")
paddle.disable_static()
def eval(self) -> None:
"""
Set the mode of DistModel to "eval". In "eval" mode,
executing ``__call__`` will return the loss.
"""
if not self._engine._has_prepared["eval"]:
self._engine._prepare_program(mode="eval", init_parameters=False)
self._mode = "eval"
self._engine.to_mode("eval")
paddle.disable_static()
def predict(self) -> None:
"""
Set the mode of DistModel to "predict". In "predict" mode,
executing ``__call__`` returns a dict that contains the
outputs of the model.
"""
if not self._engine._has_prepared["predict"]:
self._engine.prepare(
copy.deepcopy(self._engine._inputs_spec),
None,
mode="predict",
init_parameters=False,
)
self._mode = "predict"
self._engine.to_mode("predict")
paddle.disable_static()
def __validate_mode(self, mode):
if mode is None and self._mode is None:
raise ValueError(
"Please set the mode or call train()/eval()/predict() first."
)
if mode is None:
mode = self._mode
if mode not in ["train", "eval", "predict"]:
raise ValueError("mode can only be 'train', 'eval' or 'predict'.")
return mode
def dist_main_program(self, mode: _Mode | None = None) -> Program:
"""
Get the distributed main program of the specified ``mode``. Each
'mode' has its own distributed main program, ``dist_main_program``
returns the corresponding distributed main program of ``mode``.
Args:
mode (str|None, optional): Can be 'train' , 'eval' , 'predict' or None.
'train' : Return the distributed main program for training.
'eval' : Return the distributed main program for evaluation.
'predict' : Return the distributed main program for prediction.
None : The current mode of the DistModel will be used.
Default : None.
Returns:
The distributed main program of ``mode``.
"""
mode = self.__validate_mode(mode)
return self._engine.get_dist_main_program(mode)
def dist_startup_program(self, mode: _Mode | None = None) -> Program:
"""
Get the corresponding distributed startup program of ``mode``,
which is used for initializing the parameters.
Args:
mode (str|None, optional): Can be 'train' , 'eval' , 'predict' or None.
'train' : Return the distributed startup program for training.
'eval' : Return the distributed startup program for evaluation.
'predict' : Return the distributed startup program for prediction.
None: The current mode of the DistModel will be used.
Default : None.
Returns:
The distributed startup program of ``mode``.
"""
mode = self.__validate_mode(mode)
return self._engine.get_dist_startup_program(mode)
def serial_main_program(self, mode: _Mode | None = None) -> Program:
"""
Get the corresponding serial main program of ``mode``, containing
the whole variables and operators of the given ``layer``.
Args:
mode (str|None, optional): Can be 'train', 'eval', 'predict' or None.
'train' : Return the main program for training.
'eval' : Return the main program for evaluation.
'predict' : Return the main program for prediction.
None : The current mode of the DistModel will be used.
Default : None.
Returns:
The serial main program of ``mode``.
"""
mode = self.__validate_mode(mode)
return self._engine.get_serial_main_program(mode)
def serial_startup_program(self, mode: _Mode | None = None) -> Program:
"""
Get the corresponding serial startup program of ``mode``.
Args:
mode (str|None, optional): Can be 'train' , 'eval' , 'predict' or None.
'train' : Return the serial startup program for training.
'eval' : Return the serial startup program for evaluation.
'predict' : Return the serial startup program for prediction.
None : The current mode of the DistModel will be used.
Default : None.
Returns:
The serial startup program of ``mode``.
"""
mode = self.__validate_mode(mode)
return self._engine.get_serial_startup_program(mode)
def _make_feeds(self, data_list):
if (
self._mode not in self._feed_name_list
or self._feed_name_list[self._mode] == []
):
self._feed_name_list[self._mode] = self._engine.get_feed_name_list()
feed_name_list = self._feed_name_list[self._mode]
if len(feed_name_list) != len(data_list):
raise ValueError(
"The input data and feed_list are not consistent."
f"The model takes {feed_name_list} as input"
)
feed_list = []
no_data_ids = []
# If the feed_var is None, its feed_name should be deleted.
# This scenario is very common if using `PipeLine Parallelism`.
for idx, data in enumerate(data_list):
if isinstance(data, paddle.Tensor):
feed_var = _to_lodtensor(data)
if feed_var is None:
no_data_ids.append(idx)
else:
feed_list.append(feed_var)
else:
feed_list.append(data)
feed_name_list_with_data = []
for idx, feed_name in enumerate(feed_name_list):
if idx not in no_data_ids:
feed_name_list_with_data.append(feed_name)
return dict(zip(feed_name_list_with_data, feed_list))
def __convert_strategy(self, strategy):
import copy
if strategy is None:
return None
inner_strategy = auto_strategy.Strategy()
category = auto_strategy.constants.BASE
base_config = auto_strategy.constants.get_category_default_config(
category
)
for key in base_config.keys():
setattr(inner_strategy, key, getattr(strategy, key))
inner_strategy.fused_passes.enable = strategy.fused_passes.enable
if getattr(strategy.fused_passes, "gemm_epilogue", False):
inner_strategy.fused_passes.fused_passes_list.append(
"fused_gemm_epilogue_pass"
)
if getattr(strategy.fused_passes, "dropout_add", False):
inner_strategy.fused_passes.fused_passes_list.append(
"fused_dropout_add_pass"
)
inner_strategy.amp = copy.deepcopy(strategy.amp)
inner_strategy.sharding = copy.deepcopy(strategy.sharding)
inner_strategy.gradient_merge = copy.deepcopy(strategy.gradient_merge)
inner_strategy.pipeline = copy.deepcopy(strategy.pipeline)
# The below are template interfaces
if hasattr(strategy, "_recompute"):
inner_strategy.recompute = copy.deepcopy(strategy._recompute)
if hasattr(strategy, "_mp_optimization"):
inner_strategy.mp_optimization = copy.deepcopy(
strategy._mp_optimization
)
if hasattr(strategy, "_dp_optimization"):
inner_strategy.dp_optimization = copy.deepcopy(
strategy._dp_optimization
)
if hasattr(strategy, "_sp_optimization"):
inner_strategy.sp_optimization = copy.deepcopy(
strategy._sp_optimization
)
return inner_strategy
@switch_to_static_graph
def __call__(self, *args: Sequence[Any] | Tensor) -> Any:
if self._mode is None:
raise ValueError("Please call train()/eval()/predict() first.")
if self._mode == "train":
if self._engine._optimizer is None or self._engine._loss is None:
raise ValueError(
"Please set optimizer and loss function before training."
)
if self._mode == "eval":
if self._engine._loss is None:
raise ValueError("Please set loss function before evaluation.")
feed_list = []
for feed_item in list(args):
if isinstance(feed_item, (list, tuple)):
feed_list += list(feed_item)
elif isinstance(feed_item, (paddle.Tensor, core.DenseTensor)):
feed_list += [feed_item]
else:
raise TypeError(
f"The inputs of DistModel should be list or tensor, but got {type(feed_item)}"
)
feeds = self._make_feeds(feed_list)
outs = self._engine.run(feeds)
self.outs = outs
if self._mode == "predict":
if "outputs" in self.outs:
return self.outs["outputs"]
else:
return None
else:
if "loss" in self.outs:
return self.outs["loss"]
else:
return None
def _fetch_value(self, value, name=None):
"""
Get the value of the variable with the given name.
Args:
value (pir.Value): The pir Value to fetch.
name (str|None, optional): The user-defined name of
the fetched result. If None, the order of the Value
in the fetch list will be used. Default: None.
"""
self._engine._pir_fetch_values.append(value)
if name is None:
name = len(self._engine._pir_fetch_values) - 1
self._engine._pir_user_defined_fetch_names.append(name)
def state_dict(
self,
mode: Literal['opt', 'param', 'all'] = "all",
split_fusion: bool = True,
) -> dict[str, Tensor]:
"""
Get the state dict of model and optimizer.
Args:
mode (str): Can be ['opt', 'param', 'all'],
'opt' : The return value only contains the variable in the optimizer.
'param' : The return value only contains the variable in the network, not the variable in the optimizer.
'all' : The return value contains the variable in the network and optimizer.
Default: 'all'
"""
if use_pir_api():
scope = paddle.static.global_scope()
local_state_dict = self.dist_main_program(
mode=self._engine._mode
).state_dict(mode, scope)
else:
local_state_dict = self.dist_main_program(
mode=self._engine._mode
).state_dict(mode)
dist_state_dict = self._build_distributed_state_dict(local_state_dict)
# The parameters fused in the ffn and qkv fusion pass will be split back into their original, unfused state.
if self._engine.fused_ffn_qkv is not None and split_fusion:
with paddle.base.dygraph.guard():
# Traverse each fusion structure, the key could be ffn or qkv.
for key, pat_list in self._engine.fused_ffn_qkv.items():
# Traverse each fusion pattern dict, such as: fused_p1_p2:[p1, p2].
for fusion_map in pat_list:
((fused_param, ori_params_meta),) = fusion_map.items()
origin_params = list(dist_state_dict.keys())
for param in origin_params:
suffix = _get_suffix(param, fused_param)
if suffix is not None:
value = dist_state_dict[param]
assert value.is_dist(), (
f"key {param} value:{value} is not a dist tensor."
)
mesh = value.process_mesh
placements = value.placements
if "_pow_acc" in suffix:
out = (value._local_value(),) * len(
ori_params_meta
)
else:
if len(ori_params_meta) == 3:
is_qkv = True
num_heads = ori_params_meta[
0
].local_num_head
num_key_value_heads = ori_params_meta[
1
].local_num_head
else:
is_qkv = False
num_heads = None
num_key_value_heads = None
out = split_param_func(
value._local_value(),
split_nums=len(ori_params_meta),
is_qkv=is_qkv,
num_heads=num_heads,
num_key_value_heads=num_key_value_heads,
)
for i in range(len(ori_params_meta)):
dist_tensor = dtensor_from_local(
out[i], mesh, placements
)
paddle.assign(
out[i], dist_tensor._local_value()
)
dist_state_dict[
ori_params_meta[i].name + suffix
] = dist_tensor
dist_state_dict.pop(param)
mapping_names = [
(
self._parameter_to_structured_name[k]
if k in self._parameter_to_structured_name
else k
)
for k in dist_state_dict.keys()
]
dist_state_dict = dict(
zip(mapping_names, list(dist_state_dict.values()))
)
return dist_state_dict
def _build_distributed_state_dict(self, local_state_dict):
"""
Args:
local_state_dict(Dict[str, libpaddle.Tensor]): The state dict from program.
"""
dist_main_program = self.dist_main_program(mode=self._engine._mode)
if use_pir_api():
dist_attrs = get_dist_attr(dist_main_program)
else:
# Dict[var.name, Dict["process_shape": process_mesh.shape, "process_group": process_mesh.process_ids, "dims_mapping": dims_mapping]]
dist_attrs = get_dist_attr(
dist_main_program, self._engine._dist_contexts[self._mode]
)
def build_distributed_tensor(local_tensor, dist_attr):
assert isinstance(
local_tensor, (paddle.Tensor, np.ndarray, paddle.base.Tensor)
)
if not isinstance(local_tensor, paddle.Tensor):
local_tensor = paddle.Tensor(local_tensor)
assert isinstance(local_tensor, paddle.Tensor), (
f"local tensor:{local_tensor} type {type(local_tensor)} is not paddle.Tensor."
)
assert len(local_tensor.shape) == len(dist_attr["dims_mapping"]), (
f"local tensor shape {local_tensor.shape} not equal to dims_mapping shape {dist_attr['dims_mapping']}."
)
global_shape = local_tensor.shape
mesh = ProcessMesh(
np.array(dist_attr["process_group"]).reshape(
dist_attr["process_shape"]
),
dim_names=dist_attr["dim_names"],
)
placements = to_placements(dist_attr["dims_mapping"], mesh)
dist_tensor = dtensor_from_local(local_tensor, mesh, placements)
assert dist_tensor._local_value().shape == local_tensor.shape, (
f"local tensor shape {dist_tensor._local_value().shape} not equal to local_tensor.shape:{local_tensor.shape}"
)
paddle.assign(local_tensor, dist_tensor._local_value())
return dist_tensor
global_state_dict = {}
with paddle.base.dygraph.guard():
for var_name, tensor in local_state_dict.items():
assert var_name in dist_attrs, (
f"var {var_name} not in dist attrs:{dist_attrs}."
)
global_state_dict[var_name] = build_distributed_tensor(
tensor, dist_attrs[var_name]
)
return global_state_dict
def set_state_dict(self, state_dict: dict[str, Tensor]) -> None:
local_state_dict = {}
dist_main_program = self.dist_main_program(mode=self._engine._mode)
cur_state_dict = self.state_dict(split_fusion=False)
copy_tensor = False
# When using the tensor-fusion strategy, model parameters are shared with
# slice@ parameters. When setting the state_dict, we must copy the tensor
# instead of changing the handle directly, as this could cause errors in
# the slice@ parameters and increase memory usage.
enable_tensor_fusion = (
self._inner_strategy.sharding.enable_tensor_fusion
if self._inner_strategy
else False
)
if self._engine._optimizer is not None and enable_tensor_fusion:
copy_tensor = True
for k, v in state_dict.items():
assert v.is_dist(), f"key {k} value:{v} is not a dist tensor."
if k in cur_state_dict:
cur_v = cur_state_dict[k]
assert v.process_mesh == cur_state_dict[
k
].process_mesh or check_placements_equal(
v.placements, cur_v.placements
), (
f"process_mesh:{v.process_mesh} != {cur_v.process_mesh} or placements:{v.placements} != {cur_v.placements} not match"
)
param_name = (
self._structured_to_parameter_name[k]
if k in self._structured_to_parameter_name
else k
)
local_state_dict[param_name] = _to_lodtensor(v._local_value())
# The structure of ffn and qkv in the network has been fused, and the unfused parameters in the original state_dict are fused.
if self._engine.fused_ffn_qkv is not None:
with paddle.base.dygraph.guard():
# Traverse each fusion structure, the key could be ffn or qkv.
for key, pat_list in self._engine.fused_ffn_qkv.items():
# Traverse each fusion pattern dict, such as: fused_p1_p2:[p1, p2].
for fusion_map in pat_list:
((fused_param, ori_params_meta),) = fusion_map.items()
# Obtain all the parameters to be fused, differentiated by suffixes, such as: beta1_pow_acc_0, _fp32_master_0_moment1_0.
suffix_names = []
for k, v in local_state_dict.items():
suffix = _get_suffix(ori_params_meta[0].name, k)
if suffix is not None:
suffix_names.append(suffix)
if len(suffix_names) == 0:
continue
# Traverse through each parameter for fusion, insert the fused parameters, and delete the pre-fusion parameters.
for suffix in suffix_names:
concat_tensors = []
for ori_p in ori_params_meta:
if ori_p.name + suffix not in local_state_dict:
warnings.warn(
f"{ori_p.name + suffix} is not in state_dict."
)
break
else:
concat_tensors.append(
local_state_dict[ori_p.name + suffix]
)
if len(concat_tensors) == len(ori_params_meta):
if "_pow_acc" in suffix:
fused_w = concat_tensors[0]
else:
if len(ori_params_meta) == 3:
is_qkv = True
num_heads = ori_params_meta[
0
].local_num_head
num_key_value_heads = ori_params_meta[
1
].local_num_head
else:
is_qkv = False
num_heads = None
num_key_value_heads = None
fused_w = fuse_param_func(
concat_tensors,
is_qkv=is_qkv,
num_heads=num_heads,
num_key_value_heads=num_key_value_heads,
)
local_state_dict[fused_param + suffix] = (
_to_lodtensor(fused_w)
)
for ori_p in ori_params_meta:
local_state_dict.pop(ori_p + suffix)
if use_pir_api():
dist_main_program.set_state_dict(
local_state_dict, paddle.static.global_scope(), copy_tensor
)
else:
dist_main_program.set_state_dict(
local_state_dict, paddle.static.global_scope()
)
def _get_shard_stage1_optimizer(self):
optimizer = self._engine._optimizer
if optimizer is None:
return optimizer
if isinstance(
optimizer,
paddle.static.amp.decorator.OptimizerWithMixedPrecision,
):
optimizer = optimizer._optimizer
assert isinstance(optimizer, ShardingOptimizerStage1), (
"The optimizer should be ShardingOptimizerStage1 when stage1 tensor fusion is enabled."
)
return optimizer
def _convert_state_dict_tensor_fusion(self, state_dict, optimizer_function):
enable_tensor_fusion = (
self._inner_strategy.sharding.enable_tensor_fusion
if self._inner_strategy
else False
)
assert enable_tensor_fusion, (
"Can only convert state_dict when tensor fusion is enabled."
)
optimizer = self._get_shard_stage1_optimizer()
assert optimizer is not None, "The optimizer should not be None."
parameter_names = [
(
self._structured_to_parameter_name[k]
if k in self._structured_to_parameter_name
else k
)
for k in state_dict.keys()
]
state_dict = dict(zip(parameter_names, list(state_dict.values())))
optimizer_function(optimizer, state_dict)
structured_names = [
(
self._parameter_to_structured_name[k]
if k in self._parameter_to_structured_name
else k
)
for k in state_dict.keys()
]
state_dict = dict(zip(structured_names, list(state_dict.values())))
return state_dict
def _convert_state_dict_with_rank_unique_name(self, state_dict):
def optimizer_function(optimizer, state_dict):
optimizer.convert_state_dict_with_rank_unique_name(state_dict)
return self._convert_state_dict_tensor_fusion(
state_dict, optimizer_function
)
def _convert_state_dict_without_tensor_fusion_param(self, state_dict):
def optimizer_function(optimizer, state_dict):
optimizer.convert_state_dict_without_tensor_fusion_param(state_dict)
return self._convert_state_dict_tensor_fusion(
state_dict, optimizer_function
)
def _convert_state_dict_with_tensor_fusion_param(self, state_dict):
def optimizer_function(optimizer, state_dict):
optimizer.convert_state_dict_with_tensor_fusion_param(state_dict)
return self._convert_state_dict_tensor_fusion(
state_dict, optimizer_function
)
def _convert_state_dict_with_origin_name(self, state_dict):
def optimizer_function(optimizer, state_dict):
optimizer.convert_state_dict_with_origin_name(state_dict)
return self._convert_state_dict_tensor_fusion(
state_dict, optimizer_function
)
def to_static(
layer: Layer,
loader: ShardDataloader | DataLoader | None = None,
loss: Layer | Callable[..., Any] | None = None,
optimizer: Optimizer | None = None,
strategy: Strategy | None = None,
input_spec: list[list[DistributedInputSpec]] | None = None,
) -> DistModel:
"""
Converts the ``layer`` with distributed tensor (constructed from
``paddle.distributed.shard_tensor``) to a static graph. ``to_static``
returns a DistModel instance containing the static graph for
distributed training, evaluation and prediction.
Args:
layer(paddle.nn.Layer): The layer in dygraph mode, the parameters
or its inputs can be distributed tensors.
loader(ShardDataloader|paddle.io.DataLoader): The data loader used in dygraph mode,
used to infer inputs_spec and labels_spec.
loss(Loss|Callable|None, optional): The loss function for training
or evaluating the model. Can be a `paddle.nn.Layer` instance or
any callable function. Default: None.
optimizer(paddle.optimizer.Optimizer|_ShardOptimizer|None, optional):
The optimizer for training. It can `paddle.optimizer.Optimizer`
or `_ShardOptimizer` wrapped by `shard_optimizer`. Default: None.
strategy(paddle.distributed.Strategy|None, optional): Configs for
parallel strategies and optimization settings (e.g. sharding,
pipeline parallelism). Default: None.
input_spec(list[list[paddle.distributed.DistributedInputSpec]]|None, optional):
The custom input specs specify the shape, dtype, and name information
of model inputs and labels. If it is not None, the input specs and
label specs will be inferred from the custom input specs. The custom
input specs should be a list containing two sublists: the first
sublist represents theinput specs, and the second sublist represents
the label specs. Default: None.
Returns:
DistModel: A ``DistModel`` instance converted the input ``layer``.
Examples:
.. code-block:: pycon
>>> import numpy as np
>>> import paddle
>>> import paddle.distributed as dist
>>> from paddle import nn
>>> from paddle.distributed import Replicate, Shard
>>> BATCH_SIZE = 4
>>> BATCH_NUM = 4
>>> IMAGE_SIZE = 16
>>> CLASS_NUM = 8
>>> class RandomDataset(paddle.io.Dataset): # type: ignore[type-arg]
... def __init__(self, images, labels, num_samples):
... self.images = images
... self.labels = labels
... self.num_samples = num_samples
...
... def __getitem__(self, idx):
... return self.images[idx], self.labels[idx]
...
... def __len__(self):
... return self.num_samples
>>> class DemoNet(nn.Layer):
... def __init__(self, mesh):
... super().__init__()
... self._mesh = mesh
... self.linear_0 = nn.Linear(IMAGE_SIZE, IMAGE_SIZE)
... self.linear_1 = nn.Linear(IMAGE_SIZE, CLASS_NUM)
... self.relu = nn.ReLU()
... # shard the weights of this layer
... self.linear_0.weight = dist.shard_tensor(
... self.linear_0.weight,
... self._mesh,
... [Shard(1)],
... stop_gradient=False,
... )
... self.linear_1.weight = dist.shard_tensor(
... self.linear_1.weight,
... self._mesh,
... [Shard(0)],
... stop_gradient=False,
... )
...
... def forward(self, x):
... out = self.linear_0(x)
... out = self.relu(out)
... out = self.linear_1(out)
... return out
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> images = np.random.rand(BATCH_SIZE, IMAGE_SIZE).astype('float32')
>>> labels = np.random.rand(BATCH_SIZE, CLASS_NUM).astype('float32')
>>> dataset = RandomDataset(images, labels, BATCH_SIZE)
>>> loader = paddle.io.DataLoader(dataset, batch_size=BATCH_SIZE)
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> layer = DemoNet(mesh)
>>> opt = paddle.optimizer.SGD(
... learning_rate=0.1,
... parameters=layer.parameters(),
... )
>>> loss_fn = nn.MSELoss()
>>> dist_loader = dist.shard_dataloader(loader, meshes=[mesh])
>>> dist_model = dist.to_static(
... layer,
... dist_loader,
... loss_fn,
... opt,
... )
>>> # training
>>> dist_model.train()
>>> for batch_id, (image, label) in enumerate(dist_loader()):
... # in train mode, executing the __call__ method will
... # update the parameters of the model and return the
... # loss
... loss = dist_model(image, label)
>>> # evaluation
>>> dist_model.eval()
>>> for batch_id, (image, label) in enumerate(dist_loader()):
... # in eval mode, executing the __call__ method will
... # return the loss
... loss = dist_model(image, label)
>>> # prediction
>>> dist_model.predict()
>>> for batch_id, (image, label) in enumerate(dist_loader()):
... # in predict mode, executing the __call__ method will
... # return a dict that contains the outputs of the model,
... # where the value of "out0" is the first output.
... outs = dist_model(image)
>>> # This case need to be executed in multi-card environment
>>> # export CUDA_VISIBLE_DEVICES=0,1
>>> # python -m paddle.distributed.launch {test_case}.py
"""
if isinstance(optimizer, _ShardOptimizer) and not use_pir_api():
shard_fn = optimizer._shard_fn
sharding_degree = optimizer._sharding_degree
optimizer = optimizer._inner_opt
if shard_fn is not None:
strategy = dist.Strategy() if strategy is None else strategy
# Deduce sharding degree for static
# Note: Because limitation of architecture, we need to ensure that
# all parameters are sharded by the same mesh axis
assert sharding_degree is not None, (
"Sharding degree can not be None."
)
if isinstance(shard_fn, ShardingStage1):
strategy.sharding.enable = True
strategy.sharding.stage = 1
strategy.sharding.degree = sharding_degree
elif isinstance(shard_fn, ShardingStage2):
strategy.sharding.enable = True
strategy.sharding.stage = 2
strategy.sharding.degree = sharding_degree
elif isinstance(shard_fn, ShardingStage3):
strategy.sharding.enable = True
strategy.sharding.stage = 3
strategy.sharding.degree = sharding_degree
for param in optimizer._parameter_list:
shard_fn._unshard_parameter(param)
else:
raise NotImplementedError(
"Only sharding stage 1, 2 and 3 can to_static for now. User-defined shard_fn will be supported later."
)
if strategy is None or strategy.full_graph:
dist_model = DistModel(
layer, loader, loss, optimizer, strategy, input_spec=input_spec
)
return dist_model
else:
layer = paddle.jit.to_static(layer, full_graph=False)
return layer
def unshard_dtensor(dist_tensor: Tensor) -> Tensor:
"""
Converts a distributed tensor to a dense tensor. ``unshard_dtensor``
first make the ``dist_tensor`` be ``Replicate`` state on all processes and
then converts it to a dense ``paddle.Tensor``. It can be treated as a
reverse operation of ``shard_tensor``.
Args:
dist_tensor (paddle.Tensor): The distributed tensor which is constructed
from a dense tensor with ``shard_tensor``.
Returns:
paddle.Tensor: The original dense tensor of the input ``dist_tensor``.
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.distributed as dist
>>> from paddle.distributed import Replicate, Shard
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> mesh = dist.ProcessMesh([0, 1], dim_names=["x"])
>>> original_tensor = paddle.rand([4, 1024, 512])
>>> dist_tensor = dist.shard_tensor(original_tensor, mesh, [Shard(0)])
>>> # dense_tensor's shape is the same as original_tensor
>>> dense_tensor = dist.unshard_dtensor(dist_tensor)
"""
if paddle.in_dynamic_mode():
# if the input is not a distributed
# tensor, return it directly
if dist_tensor.is_dist() is False:
raise ValueError("The input should be a distributed tensor.")
mesh = dist_tensor.process_mesh
placements = dist_tensor.placements
replicate_placements = [dist.Replicate()] * len(placements)
r_dist_tensor = reshard(dist_tensor, mesh, replicate_placements)
if isinstance(dist_tensor, EagerParamBase):
return EagerParamBase.from_tensor(
r_dist_tensor._local_value(),
**dist_tensor.__dict__,
)
else:
return paddle.Tensor(r_dist_tensor._local_value())
elif paddle.framework.in_pir_mode():
# in pir mode, we define the logic of unshard_tensor as dist_tensor_type --> dense_tensor_type with global shape.
dense_tensor_type = paddle.pir.create_shaped_type(
dist_tensor.type(), dist_tensor.shape
)
dist_tensor.set_type(dense_tensor_type)
return dist_tensor
else:
raise NotImplementedError(
"`unshard_dtensor()` only supported in dynamic and pir mode."
)
class ShardDataloader:
"""
ShardDataloader converts a dataloader to a new dataloader which provided two capabilities:
1. split dataloader by shard_dim to do data parallel.
2. reshard the output of dataloader to distributed tensor.
if is_dataset_splitted is True, just need to do reshard.
Args:
dataloader (paddle.io.DataLoader): The dataloader to be sharded.
meshes (ProcessMesh|list[ProcessMesh]|tuple[ProcessMesh]): The mesh list of the dataloader.
Identify which mesh the input is on. if len(meshes) == 1 or type(meshes) == ProcessMesh,
all the inputs are on the same mesh.
input_keys (list[str]|tuple[str]): if the iteration result of dataloader is a dict of tensors,
input_keys is the keys of this dict, identify which tensor is located on which mesh,
one-to-one correspondence with meshes. i.e. dict[input_keys[i]] is on meshes[i].
Default: None, which means the outputs is a list, and the i'th input is on meshes[i].
shard_dims (list|tuple|str|int]): The mesh dimension to shard the dataloader.
Users can specify the shard_dim of each mesh or specify a single shard_dim for all meshes.
Default: None, which means the data loader will not be split, i.e. mp.
is_dataset_splitted (bool): Whether the dataset has been split.
dense_tensor_idx (list): A paired 2D list specifies the index of the dense_tensor in the output of dataloader.
It allows users to identify which elements within each output batch are dense_tensor.
first dense_tensor: the dense_tensor return by dataloader.
second dense_tensor: num_or_sections specifies how to split first tensor: evenly (if a number) or unevenly (if a list).
Default: None, meaning all outputs are dist_tensors.
Note: For dense_tensor_idx settings, the idx must be paired.
"""
def __init__(
self,
dataloader: paddle.io.DataLoader,
meshes: ProcessMesh | list[ProcessMesh] | tuple[ProcessMesh],
input_keys: list[str] | tuple[str] | None = None,
shard_dims: list | tuple | str | int | None = None,
is_dataset_splitted: bool = False,
dense_tensor_idx: list[list[int]] | None = None,
):
# do some check
if is_dataset_splitted is True and shard_dims is None:
raise ValueError(
"shard_dims must be set when is_dataset_splitted is True"
)
self._meshes = to_list(meshes)
if self._meshes is None or len(self._meshes) == 0:
raise ValueError("meshes must be set")
process_id = dist.get_rank()
if self._process_id_in_multi_meshes(process_id):
raise ValueError(
f"process_id {process_id} is in more than one mesh, the meshes are {self._meshes}"
)
self._all_inputs_in_one_mesh = len(self._meshes) == 1
self._input_keys = input_keys
self._shard_dims = self._process_shard_dims(shard_dims)
mesh, shard_dim = self._get_mesh_and_shard_dim(process_id)
if mesh is None:
mesh = to_list(self._meshes[0])[0]
shard_dim = to_list(self._shard_dims[0])[0]
dp_rank = 0
dp_world_size = mesh.get_dim_size(shard_dim)
else:
dp_rank = mesh.get_rank_by_dim_and_process_id(shard_dim, process_id)
dp_world_size = mesh.get_dim_size(shard_dim)
if is_dataset_splitted is True or shard_dims is None:
self._dataloader = dataloader
self.batch_size = dataloader.batch_sampler.batch_size
elif isinstance(dataloader.batch_sampler, DistributedBatchSampler):
self.batch_size = dataloader.batch_sampler.batch_size
self.batch_sampler = dataloader.batch_sampler
self._dataloader = dataloader
else:
self.batch_size = int(
dataloader.batch_sampler.batch_size / dp_world_size
)
if isinstance(dataloader.batch_sampler, _InfiniteIterableSampler):
shuffle = False
drop_last = False
else:
shuffle = dataloader.batch_sampler.shuffle
drop_last = dataloader.batch_sampler.drop_last
self.batch_sampler = DistributedBatchSampler(
dataset=dataloader.dataset,
batch_size=self.batch_size,
num_replicas=dp_world_size,
rank=dp_rank,
shuffle=shuffle,
drop_last=drop_last,
)
self.batch_sampler._acc_steps = dataloader.batch_sampler._acc_steps
self._dataloader = paddle.io.DataLoader(
dataset=dataloader.dataset,
batch_sampler=self.batch_sampler,
feed_list=dataloader.feed_list,
places=dataloader.places,
return_list=dataloader.return_list,
collate_fn=dataloader.collate_fn,
num_workers=dataloader.num_workers,
use_buffer_reader=dataloader.use_buffer_reader,
prefetch_factor=dataloader.prefetch_factor,
use_shared_memory=dataloader.use_shared_memory,
timeout=dataloader.timeout,
worker_init_fn=dataloader.worker_init_fn,
persistent_workers=dataloader._persistent_workers,
)
# Note(lizhiyu): In dygraph mode, the flag "pin_memory" is default "True", but it decrease the speed of `AutoParallel`
self._dataloader.pin_memory = False
self.iter = None
self.dense_tensor_idx = dense_tensor_idx
def _process_shard_dims(self, shard_dims):
if isinstance(shard_dims, (int, str)) or shard_dims is None:
res = []
for i in range(len(self._meshes)):
if isinstance(self._meshes[i], (list, tuple)):
res.append([shard_dims] * len(self._meshes[i]))
else:
res.append(shard_dims)
return res
else:
if len(shard_dims) != len(self._meshes):
raise ValueError(
f"shard_dims must be the same length as meshes, but got {len(shard_dims)} != {len(self._meshes)}"
)
return shard_dims
def _get_mesh_and_shard_dim(self, process_id):
for i in range(len(self._meshes)):
if isinstance(self._meshes[i], (list, tuple)):
for j in range(len(self._meshes[i])):
if process_id in self._meshes[i][j]._process_ids:
return self._meshes[i][j], self._shard_dims[i][j]
else:
if process_id in self._meshes[i]._process_ids:
return self._meshes[i], self._shard_dims[i]
return None, None
def _process_id_in_multi_meshes(self, process_id):
count = 0
flatten_meshes = []
for mesh in self._meshes:
if isinstance(mesh, (list, tuple)):
flatten_meshes.extend(mesh)
else:
flatten_meshes.append(mesh)
# NOTE(zhengzhonghui): User may set the same mesh for different inputs, so we need to unique the meshes
unique_meshes = list(set(flatten_meshes))
for mesh in unique_meshes:
if process_id in mesh._process_ids:
count += 1
return count > 1
def __len__(self):
return len(self._dataloader)
def __iter__(self):
# Reset iterator state to allow restarting iteration
self.iter = None
return self
def _get_mesh_and_placement(self, index):
shard_dim = (
self._shard_dims[0]
if self._all_inputs_in_one_mesh
else self._shard_dims[index]
)
if shard_dim is not None and not in_auto_dp_mode():
placements = [dist.Shard(0)]
else:
placements = [dist.Replicate()]
mesh = (
self._meshes[0]
if self._all_inputs_in_one_mesh
else self._meshes[index]
)
for _ in range(1, len(mesh._shape)):
placements.append(dist.Replicate())
return mesh, placements
def _get_meshes_and_placements_for_list_input(self, index, length):
if self._all_inputs_in_one_mesh:
meshes = [self._meshes[0]] * length
shard_dims = [self._shard_dims[0]] * length
else:
meshes = self._meshes[index]
if isinstance(meshes, (list, tuple)):
assert len(meshes) == length
else:
meshes = [meshes] * length
shard_dims = self._shard_dims[index]
if isinstance(shard_dims, (list, tuple)):
assert len(shard_dims) == length
else:
shard_dims = [shard_dims] * length
placements = []
for i in range(length):
if shard_dims[i] is not None and not in_auto_dp_mode():
placement = [dist.Shard(0)]
else:
placement = [dist.Replicate()]
for _ in range(1, len(meshes[i]._shape)):
placement.append(dist.Replicate())
placements.append(placement)
return meshes, placements
def _dtensors_from_list_input(
self, list_tensors, meshes, placements, dense_tensor_idx=None
):
dist_data = []
for j in range(len(list_tensors)):
if (
dense_tensor_idx is not None and j in dense_tensor_idx
) or not isinstance(list_tensors[j], paddle.Tensor):
dist_data.append(list_tensors[j])
else:
dist_data.append(
dtensor_from_local(
list_tensors[j], meshes[j], placements[j]
)
)
return dist_data
def _get_batch(self, batch_data):
if isinstance(batch_data, (list, tuple)):
if self._all_inputs_in_one_mesh is False:
assert len(batch_data) == len(self._meshes)
dist_batch_data = []
for i in range(len(batch_data)):
input_data = batch_data[i]
if isinstance(input_data, (list, tuple)):
(
meshes,
placements,
) = self._get_meshes_and_placements_for_list_input(
i, len(input_data)
)
_dense_tensor_idx = (
None
if self.dense_tensor_idx is None
else self.dense_tensor_idx[i]
)
dist_batch_data.append(
self._dtensors_from_list_input(
input_data, meshes, placements, _dense_tensor_idx
)
)
elif isinstance(input_data, paddle.Tensor):
if (
self.dense_tensor_idx is not None
and self.dense_tensor_idx[i] != []
):
dist_batch_data.append(input_data)
else:
mesh, placements = self._get_mesh_and_placement(i)
dist_batch_data.append(
dtensor_from_local(input_data, mesh, placements)
)
else:
raise ValueError(
f"Unsupported input_data type {type(input_data)}"
)
return dist_batch_data
elif isinstance(batch_data, dict):
input_keys = (
batch_data.keys()
if self._input_keys is None
else self._input_keys
)
if self._all_inputs_in_one_mesh is False:
assert len(input_keys) == len(self._meshes)
dist_batch_data = {}
for i, key in enumerate(input_keys):
input_data = batch_data[key]
if isinstance(input_data, (list, tuple)):
(
meshes,
placements,
) = self._get_meshes_and_placements_for_list_input(
i, len(input_data)
)
_dense_tensor_idx = (
None
if self.dense_tensor_idx is None
else self.dense_tensor_idx[i]
)
dist_batch_data[key] = self._dtensors_from_list_input(
input_data, meshes, placements, _dense_tensor_idx
)
elif isinstance(input_data, paddle.Tensor):
if (
self.dense_tensor_idx is not None
and self.dense_tensor_idx[i] != []
):
dist_batch_data[key] = input_data
else:
mesh, placements = self._get_mesh_and_placement(i)
dist_batch_data[key] = dtensor_from_local(
batch_data[key], mesh, placements
)
else:
dist_batch_data[key] = input_data
return dist_batch_data
elif isinstance(batch_data, paddle.Tensor):
mesh, placements = self._get_mesh_and_placement(0)
return dtensor_from_local(batch_data, mesh, placements)
else:
raise ValueError(f"Unsupported batch_data type {type(batch_data)}")
def __next__(self):
if self.iter is None:
self.iter = self._dataloader.__iter__()
batch_data = next(self.iter)
return self._get_batch(batch_data)
def __call__(self):
# Reset iterator state to allow restarting iteration
self.iter = None
return self
def shard_dataloader(
dataloader: DataLoader,
meshes: ProcessMesh | Sequence[ProcessMesh],
input_keys: Sequence[str] | None = None,
shard_dims: Sequence[str] | Sequence[int] | str | int | None = None,
is_dataset_splitted: bool = False,
dense_tensor_idx: list[list[int]] | None = None,
) -> ShardDataloader:
"""
Convert the dataloader to a ShardDataloader which provided two capabilities:
1. split dataloader by shard_dim to do data parallel if it it not None.
2. reshard the output of dataloader to distributed tensor.
if is_dataset_splitted is True, it means that the dataset has been split by users, and just need to do reshard.
only if is_dataset_splitted is False and shard_dims is not None, it will do split.
Args:
dataloader (paddle.io.DataLoader): The dataloader to be sharded. the output of dataloader
must be a list or dict of paddle.Tensor with 2 elements, i.e. [input_data, label] or
{"input_data": input_data, "label": label}, input_data and label can be a list to support multiple inputs.
meshes (ProcessMesh|list[ProcessMesh]|tuple[ProcessMesh]): The mesh list of the dataloader.
Identify which mesh the input is on. if len(meshes) == 1 or type(meshes) == ProcessMesh,
all the inputs are on the same mesh.
input_keys (list[str]|tuple[str]): if the iteration result of dataloader is a dict of tensors,
input_keys is the keys of this dict, identify which tensor is located on which mesh,
one-to-one correspondence with meshes. i.e. dict[input_keys[i]] is on meshes[i].
Default: None, which means the outputs is a list, and the i'th input is on meshes[i].
shard_dims (list(str)|tuple(str)|list(int)|tuple(int)|str|int]):
The mesh dimension to shard the dataloader.
Users can specify the shard_dim of each mesh or specify a single shard_dim for all meshes.
Default: None, which means the data loader will not be split, i.e. mp.
is_dataset_splitted (bool): Whether the dataset has been split, Default: False.
dense_tensor_idx (list): A paired 2D list specifies the index of the dense_tensor in the output of dataloader.
It allows users to identify which elements within each output batch are dense_tensor.
first dense_tensor: the dense_tensor return by dataloader.
second dense_tensor: num_or_sections specifies how to split first tensor: evenly (if a number) or unevenly (if a list).
Default: None, meaning all outputs are dist_tensors.
Note: For dense_tensor_idx settings, the idx must be paired.
Returns:
ShardDataloader: The sharded dataloader.
Examples:
.. code-block:: pycon
:name: example-1
>>> import os
>>> import numpy as np
>>> import paddle
>>> import paddle.distributed as dist
>>> from paddle.io import BatchSampler, DataLoader, Dataset
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> mesh0 = dist.ProcessMesh([[0, 1], [2, 3]], dim_names=['x', 'y'])
>>> mesh1 = dist.ProcessMesh([[4, 5], [6, 7]], dim_names=['x', 'y'])
>>> paddle.seed(1024)
>>> np.random.seed(1024)
>>> class RandomDataset(Dataset): # type: ignore[type-arg]
>>> def __init__(self, seq_len, hidden, num_samples=8):
... super().__init__()
... self.seq_len = seq_len
... self.hidden = hidden
... self.num_samples = num_samples
... self.inputs = [np.random.uniform(size=[self.seq_len, self.hidden]).astype("float32") for _ in range(num_samples)]
... self.labels = [np.array(index, dtype="float32") for index in range(num_samples)]
... def __getitem__(self, index):
... return self.inputs[index], self.labels[index]
... def __len__(self):
... return self.num_samples
>>> class MlpModel(paddle.nn.Layer):
... def __init__(self):
... super(MlpModel, self).__init__()
... self.w0 = dist.shard_tensor(
... self.create_parameter(shape=[8, 8]),
... mesh0,
... [dist.Replicate(), dist.Shard(1)],
... )
... self.w1 = dist.shard_tensor(
... self.create_parameter(shape=[8, 8]),
... mesh1,
... [dist.Replicate(), dist.Shard(0)],
... )
... def forward(self, x):
... y = paddle.matmul(x, self.w0)
... y = dist.reshard(y, mesh1, [dist.Shard(0), dist.Shard(2)])
... z = paddle.matmul(y, self.w1)
... return z
>>> model = MlpModel()
>>> dataset = RandomDataset(4, 8)
>>> sampler = BatchSampler(
... dataset,
... batch_size=2,
... )
>>> dataloader = DataLoader(
... dataset,
... batch_sampler=sampler,
... )
>>> dist_dataloader = dist.shard_dataloader(
... dataloader=dataloader,
... meshes=[mesh0, mesh1],
... shard_dims="x",
... )
>>> opt = paddle.optimizer.AdamW(learning_rate=0.001, parameters=model.parameters())
>>> dist_opt = dist.shard_optimizer(opt)
>>> def loss_fn(logits, label):
... # logits: [bs, seq_len, hidden], label: [bs]
... loss = paddle.nn.MSELoss(reduction="sum")
... logits = paddle.sum(logits, axis=[1, 2])
... return loss(logits, label)
>>> RUN_STATIC = eval(os.environ['RUN_STATIC'])
>>> def run_dynamic():
... for step, (input, label) in enumerate(dist_dataloader()):
... logits = model(input)
... loss = loss_fn(logits, label)
... print("step:{}, loss:{}".format(step, loss))
... loss.backward()
... dist_opt.step()
... dist_opt.clear_grad()
>>> def run_static():
... dist_model = dist.to_static(
... model,
... dist_dataloader,
... loss_fn,
... opt,
... )
... dist_model.train()
... for step, (input, label) in enumerate(dist_dataloader()):
... print("label:", label)
... loss = dist_model(input, label)
... print("step:{}, loss:{}".format(step, loss))
>>> if RUN_STATIC == 0:
... run_dynamic()
... else:
... run_static()
>>> # This case need to be executed in multi-card environment
>>> # export CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7
>>> # RUN_STATIC=1 python -u -m paddle.distributed.launch --gpus "0,1,2,3,4,5,6,7" {test_case}.py
>>> # RUN_STATIC=0 python -u -m paddle.distributed.launch --gpus "0,1,2,3,4,5,6,7" {test_case}.py
.. code-block:: pycon
:name: example-2
>>> import paddle
>>> import paddle.distributed as dist
>>> from paddle.io import BatchSampler, DataLoader, Dataset
>>> import numpy as np
>>> mesh0 = dist.ProcessMesh([[0, 1], [2, 3]], dim_names=['dp', 'mp'])
>>> mesh1 = dist.ProcessMesh([[4, 5], [6, 7]], dim_names=['dp', 'mp'])
>>> class RandomDataset(Dataset): # type: ignore[type-arg]
... def __init__(self, seq_len, hidden, num_samples=8):
... super().__init__()
... self.seq_len = seq_len
... self.hidden = hidden
... self.num_samples = num_samples
... self.inputs1 = [
... np.random.uniform(size=[self.seq_len, self.hidden]).astype("float32") for _ in range(num_samples)
... ]
... self.inputs2 = [
... np.random.uniform(size=[self.seq_len, self.hidden]).astype("float32") for _ in range(num_samples)
... ]
... self.labels = [np.array(index, dtype="float32") for index in range(num_samples)]
...
... def __getitem__(self, index):
... return {
... "inputs": [self.inputs1[index], self.inputs2[index]],
... "label": self.labels[index],
... }
...
... def __len__(self):
... return self.num_samples
>>> dataset = RandomDataset(4, 8)
>>> sampler = BatchSampler(
... dataset,
... batch_size=2,
... )
>>> dataloader = DataLoader(
... dataset,
... batch_sampler=sampler,
... )
>>> dist_dataloader = dist.shard_dataloader(
... dataloader=dataloader,
... meshes=[mesh0, mesh1], # or [[mesh0, mesh0], mesh1]
... shard_dims="dp",
... input_keys=["inputs", "label"],
... )
"""
return ShardDataloader(
dataloader,
meshes,
input_keys,
shard_dims,
is_dataset_splitted,
dense_tensor_idx,
)
def in_auto_parallel_align_mode():
return paddle.base.framework.get_flags(
"FLAGS_enable_auto_parallel_align_mode"
)["FLAGS_enable_auto_parallel_align_mode"]
def enable_auto_dp():
"""
Enables an automated Data Parallel (DP) setup for auto-parallel training.
This function simplifies the process of implementing vanilla (standard) Data
Parallelism within the auto-parallel framework. By calling ``enable_auto_dp()``,
users can achieve data parallel training without needing to manually configure
``paddle.distributed.shard_dataloader`` (or a similar distributed dataloader
interface) for DP-specific data sharding or distribution. This mode automates
the setup required for DP communication and data handling.
The function works by setting the related environment variable
to ``1``. This signals to the auto-parallel system that it should
automatically manage the data parallelism aspects of the training process
according to a predefined strategy.
A significant advantage of this automated DP mode is its inherent robustness
and ability to handle scenarios that can be challenging for manual or other
standard DP configurations. For instance, it is particularly effective for:
- Training models where input data may have non-uniform shapes across
different data parallel ranks (e.g., certain video generation models
like Wanx). In such cases, where traditional DP might lead to program
hangs due to shape mismatches during communication, this automated mode
employs strategies (like adjusting data representation and gradient
synchronization) to ensure smooth training.
In essence, ``enable_auto_dp()`` provides two key benefits:
1. **Simplified DP Setup:** Automates the configuration for basic data
parallelism, reducing manual setup effort (e.g., no need for manual
``shard_dataloader`` DP configuration).
2. **Robustness for Complex Cases:** Effectively handles advanced scenarios
like non-uniform input shapes.
Note:
This function should typically be called at the very beginning of your
training script, prior to initializing Paddle's distributed environment
or any auto-parallel components. The underlying auto-parallel framework,
including its data loading and optimizer components, must be designed to
recognize and act upon the environment variable.
Examples:
.. code-block:: pycon
>>> import numpy as np
>>> import paddle
>>> from paddle import nn
>>> import paddle.distributed as dist
>>> from paddle.io import Dataset, DataLoader
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
>>> dist.enable_auto_dp()
>>> BATCH_SIZE = 32
>>> CLASS_NUM = 10
>>> INPUT_DIM = 256
>>> STEPS = 100
>>> class RandomDataset(Dataset): # type: ignore[type-arg]
... def __init__(self, num_samples):
... rank = dist.get_rank() if dist.get_world_size() > 1 else 0
... np.random.seed(42 + rank)
... self.num_samples = num_samples
...
... def __getitem__(self, idx):
... x = np.random.rand(INPUT_DIM).astype('float32')
... y = np.random.randint(0, CLASS_NUM, (1,)).astype('int64')
... return x, y
...
... def __len__(self):
... return self.num_samples
>>> class SimpleNet(nn.Layer):
... def __init__(self):
... super().__init__()
... self.net = nn.Sequential(
... nn.Linear(INPUT_DIM, 102400),
... nn.Linear(102400, INPUT_DIM),
... nn.Linear(INPUT_DIM, CLASS_NUM),
... )
...
... def forward(self, x):
... return self.net(x)
>>> model = SimpleNet()
>>> optimizer = paddle.optimizer.AdamW(learning_rate=1e-3, parameters=model.parameters())
>>> dataset = RandomDataset(num_samples=STEPS * BATCH_SIZE)
>>> loader = DataLoader(dataset, batch_size=BATCH_SIZE, shuffle=False, drop_last=True)
>>> model.train()
>>> for step, (x, y) in enumerate(loader):
... y.stop_gradient = True
... loss = paddle.mean(model(x))
... loss.backward()
... optimizer.step()
... model.clear_gradients()
... if step % 5 == 0:
... print(f"[step {step}] loss: {loss.item():.4f}")
>>> # This case need to be executed in multi-card environment
>>> # export CUDA_VISIBLE_DEVICES=0,1
>>> # python -m paddle.distributed.launch {test_case}.py
"""
_enable_auto_dp()