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

469 lines
16 KiB
Python

# Copyright (c) 2024 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 os
from typing import TYPE_CHECKING
import numpy as np
import paddle
import paddle.distributed as dist
from paddle import Tensor
from paddle.autograd import PyLayer
from .placement_type import check_placements_equal, to_dim_map
from .static.reshard_funcs.base_reshard_func import choose_reshard_func
from .static.reshard_funcs.nd_mesh_reshard_func import get_1D_sub_process_mesh
from .static.utils import split_mesh
if TYPE_CHECKING:
from paddle.distributed import Placement
from paddle.distributed.auto_parallel.process_mesh import ProcessMesh
def _specific_alltoall_dim(
dist_tensor: Tensor, mesh: ProcessMesh, placements: list[Placement]
):
"""
Get the specific dimension for alltoall communication in nd_mesh reshard.
"""
if not os.getenv("FLAGS_enable_moe_utils") == "true":
return None
mesh_dim = None
if paddle.in_dynamic_mode():
src_mesh = dist_tensor.process_mesh
src_placements = dist_tensor.placements
elif paddle.framework.in_pir_mode():
src_mesh = dist_tensor.process_mesh
src_placements = dist_tensor.dist_attr().placements_attr
if src_mesh != mesh or src_mesh.ndim == 1:
return None
if any(p.is_partial() for p in src_placements):
return None
if any(p.is_partial() for p in placements):
return None
for i in range(min(len(src_placements), len(placements))):
src_p = src_placements[i]
dst_p = placements[i]
if src_p.is_shard() and dst_p.is_shard() and src_p != dst_p:
# reshard from shard to shard, needs alltoall
# now only supports reshard on one dimension
src_dim = src_p.get_dim()
dst_dim = dst_p.get_dim()
if mesh_dim is not None or abs(src_dim - dst_dim) != 1:
return None
else:
mesh_dim = i
return mesh_dim
def _dtensor_from_local(
local_tensor, mesh, placements, local_tensor_shape=None
):
# assume the each rank has the same tensor shape for now, just use the local shape to calculate the global shape
global_dims = list(local_tensor.shape)
if local_tensor_shape is not None:
global_dims = local_tensor_shape
for idx, placement in enumerate(placements):
if placement.is_shard():
shard_dim = placement.get_dim()
local_dim_size = global_dims[shard_dim]
global_dims[shard_dim] = local_dim_size * mesh.shape[idx]
if paddle.in_dynamic_mode():
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
return paddle.Tensor(
local_tensor,
dims=global_dims,
process_mesh=mesh,
placements=placements,
place=place,
)
# TODO Adopt Mix2Dist Pass to allow the program could be executed actually.
elif paddle.framework.in_pir_mode():
assert isinstance(local_tensor, (type(None), paddle.pir.Value)), (
"input tensor is not pir value."
)
assert local_tensor.is_dense_tensor_type(), (
"dtensor_from_local() are only supported dense tensor type right."
)
sharding_specs = (
paddle.distributed.auto_parallel.placement_type.get_shard_spec(
mesh, placements, local_tensor.ndim
)
)
dims_mapping = paddle.distributed.auto_parallel.static.utils.convert_to_dims_mapping(
sharding_specs, mesh
)
local_shape = local_tensor.shape
global_tensor_type = paddle.pir.create_shaped_type(
local_tensor.type(), global_dims
)
dist_dense_tensor_type = paddle.base.libpaddle.pir.create_dist_dense_tensor_type_by_dense_tensor(
global_tensor_type, local_shape, mesh, dims_mapping
)
local_tensor.set_type(dist_dense_tensor_type)
return local_tensor
else:
raise RuntimeError(
"dtensor_from_local() are only supported in dynamic or pir mode."
)
def _pir_nd_mesh_all2all(src_value, dst_type, mesh, placements, dim):
"""
Use all to all communication in nd_mesh reshard.
"""
# create value on sub 1D mesh
sub_value = paddle._C_ops.share_data(src_value)
sub_mesh = get_1D_sub_process_mesh(mesh, dim)
sub_placements = [src_value.dist_attr().placements_attr[dim]]
sub_value_shape = dist.auto_parallel.api._cal_global_shape(
src_value._local_shape, sub_mesh, sub_placements
)
sub_value_type = paddle.pir.create_shaped_type(
sub_value.type(), sub_value_shape
)
sub_dims_mapping, partial_status = to_dim_map(
sub_placements, len(sub_value_shape)
)
sub_value_dist_attr = (
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
sub_mesh, sub_dims_mapping, partial_status
)
)
sub_value_dist_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
sub_value_type, sub_value_dist_attr
)
sub_value.set_type(sub_value_dist_type)
# 1D mesh reshard
dst_placements = [placements[dim]]
sub_dst_dims_mapping, partial_status = to_dim_map(
dst_placements, len(sub_value_shape)
)
sub_dst_type = paddle.pir.create_shaped_type(
sub_value.type(), sub_value_shape
)
sub_dst_dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
sub_mesh, sub_dst_dims_mapping, partial_status
)
sub_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
sub_dst_type, sub_dst_dist_attr
)
reshard_func = choose_reshard_func(sub_value_dist_attr, sub_dst_dist_attr)
out = reshard_func.reshard(
sub_value_dist_attr, sub_dst_dist_attr, sub_value, sub_dst_type
)
# set the type of the output value with global mesh
if out is not None:
out.set_type(dst_type)
return out
class _NdMeshAlltoAll(PyLayer):
@staticmethod
def forward(
ctx,
dist_tensor: Tensor,
mesh: ProcessMesh,
placements: list[Placement],
dim: int,
):
sub_mesh = get_1D_sub_process_mesh(mesh, dim)
ctx.alltoall_dim = dim
ctx.x_mesh = copy.deepcopy(dist_tensor.process_mesh)
ctx.x_placements = copy.deepcopy(dist_tensor.placements)
ctx.out_mesh = copy.deepcopy(mesh)
ctx.out_placements = copy.deepcopy(placements)
local_shape = _cal_local_shape(
dist_tensor.shape, sub_mesh, [dist_tensor.placements[dim]]
)
out = _dtensor_from_local(
dist_tensor._local_value(),
sub_mesh,
[dist_tensor.placements[dim]],
local_shape,
)
out = dist.reshard(out, sub_mesh, [placements[dim]])
local_shape = _cal_local_shape(out.shape, sub_mesh, out.placements)
out = _dtensor_from_local(
out._local_value(), mesh, placements, local_shape
)
out.stop_gradient = dist_tensor.stop_gradient
return out
@staticmethod
def backward(ctx, out_grad):
if not check_placements_equal(ctx.out_placements, out_grad.placements):
out = dist.reshard(out_grad, ctx.out_mesh, ctx.out_placements)
out = _NdMeshAlltoAll.apply(
out_grad, ctx.x_mesh, ctx.x_placements, ctx.alltoall_dim
)
return out
def _cal_local_shape(global_shape, mesh, placements):
local_shape = list(global_shape)
for idx, placement in enumerate(placements):
if placement.is_shard():
shard_dim = placement.get_dim()
local_shape[shard_dim] = local_shape[shard_dim] // mesh.shape[idx]
return local_shape
def infer_positive_shape(src_shape, tgt_shape):
if isinstance(tgt_shape, (list, tuple)):
ret_shape = np.array(tgt_shape)
else:
ret_shape = tgt_shape.copy()
minus_one_idx = np.where(ret_shape == -1)[0]
if minus_one_idx.size > 0:
assert minus_one_idx.size <= 1, (
"At most one -1 is allowed in target shape."
)
nelem = np.prod(src_shape)
ret_shape[minus_one_idx[0]] = 1
ret_shape[minus_one_idx[0]] = nelem // np.prod(ret_shape)
return list(ret_shape)
class _local_reshape(PyLayer):
@staticmethod
def forward(
ctx,
dist_tensor: Tensor,
global_shape: list,
local_shape: list,
mesh: ProcessMesh,
placements: list[Placement],
):
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
if dist_tensor._local_value()._is_initialized():
local_tensor = dist_tensor._local_value().clone()
else:
local_tensor = dist_tensor._local_value()
ctx.x_global_shape = copy.deepcopy(dist_tensor.shape)
ctx.x_local_shape = copy.deepcopy(local_tensor.shape)
ctx.x_mesh = copy.deepcopy(dist_tensor.process_mesh)
ctx.x_placements = copy.deepcopy(dist_tensor.placements)
local_tensor = local_tensor.reshape(local_shape)
out = paddle.Tensor(
local_tensor,
dims=global_shape,
process_mesh=mesh,
placements=placements,
place=place,
)
out.stop_gradient = dist_tensor.stop_gradient
return out
@staticmethod
def backward(ctx, out_grad):
place = paddle.framework._current_expected_place()
place = paddle.framework._get_paddle_place(place)
if out_grad._local_value()._is_initialized():
local_grad = out_grad._local_value().clone()
x_local_shape = ctx.x_local_shape
else:
local_grad = out_grad._local_value()
x_local_shape = [0]
local_grad = local_grad.reshape(x_local_shape)
ret = paddle.Tensor(
local_grad,
dims=ctx.x_global_shape,
process_mesh=ctx.x_mesh,
placements=ctx.x_placements,
place=place,
)
return ret
def _dist_reshape(
dist_tensor: Tensor,
global_shape: list,
mesh: ProcessMesh,
placements: list[Placement],
):
"""
Reshape the local tensors of the dist tensor on each rank,
and manually set the process_mesh and placements of the output.
"""
tgt_global_shape = infer_positive_shape(dist_tensor.shape, global_shape)
tgt_local_shape = _cal_local_shape(tgt_global_shape, mesh, placements)
if paddle.in_dynamic_mode():
src_local_shape = dist_tensor._local_value().shape
if not dist_tensor._local_value()._is_initialized():
tgt_local_shape = dist_tensor._local_value().shape
elif paddle.framework.in_pir_mode():
# src_local_shape = dist_tensor._local_shape
src_local_shape = _cal_local_shape(
dist_tensor.shape,
dist_tensor.dist_attr().process_mesh,
dist_tensor.dist_attr().placements_attr,
)
else:
raise NotImplementedError(
"dist_reshape is only supported in dynamic and pir mode."
)
assert np.prod(tgt_local_shape) == np.prod(src_local_shape), (
f"The local shapes {src_local_shape} and {tgt_local_shape} are mismatched."
)
if paddle.in_dynamic_mode():
return _local_reshape.apply(
dist_tensor, tgt_global_shape, tgt_local_shape, mesh, placements
)
elif paddle.framework.in_pir_mode():
return paddle._C_ops.dist_reshape(
dist_tensor,
dist_tensor.placements,
tgt_global_shape,
tgt_local_shape,
mesh,
placements,
)
def shard_submesh_and_slice(mesh, tensor_slice, tensor_dim, mesh_dim):
new_sub_meshes = split_mesh(mesh, mesh_dim)
num_shards = len(new_sub_meshes)
total_size = tensor_slice[tensor_dim][1] - tensor_slice[tensor_dim][0]
shard_size = (total_size + num_shards - 1) // num_shards
effective_size = shard_size * (num_shards - 1)
last_shard_size = total_size - effective_size
new_slices = []
for i in range(num_shards):
start = tensor_slice[tensor_dim][0] + i * shard_size
if i == num_shards - 1:
end = min(start + last_shard_size, tensor_slice[tensor_dim][1])
else:
end = min(start + shard_size, tensor_slice[tensor_dim][1])
new_slice = list(tensor_slice)
new_slice[tensor_dim] = (start, end)
new_slices.append(new_slice)
return new_sub_meshes, new_slices
def get_rank2tensor_indices(sub_mesh_indices_info, sub_mesh_partial_info):
rank2tensor_indices = {}
for sub_mesh, slice_info in sub_mesh_indices_info.items():
for rank in sub_mesh.process_ids:
rank2tensor_indices[rank] = {
'slice': slice_info,
'partial': sub_mesh_partial_info,
}
return rank2tensor_indices
def get_local_slices(tensor, mesh, placements):
# TODO(nieyuntao): Temporarily disable this check to bypass certain special cases (shard one tensor dim by many mesh dim)
# if len(mesh.shape) < len(placements):
# raise ValueError(
# f"placements length ({len(placements)}) must be smaller or equal to mesh_shape({len(mesh.shape)})"
# )
if len(placements) < len(mesh.shape):
for _ in range(len(mesh.shape) - len(placements)):
placements.append(dist.Replicate())
sub_mesh_indices_info = {mesh: [(0, s) for s in tensor.shape]}
sub_mesh_partial_info = {}
for mesh_dim, placement in enumerate(placements):
if placement.is_shard():
tensor_dim = placement.get_dim()
tmp = {}
while sub_mesh_indices_info:
sub_mesh, slice_info = sub_mesh_indices_info.popitem()
new_sub_meshes, new_slices = shard_submesh_and_slice(
sub_mesh, slice_info, tensor_dim, mesh_dim
)
tmp.update(dict(zip(new_sub_meshes, new_slices)))
sub_mesh_indices_info.update(tmp)
if hasattr(placement, 'is_partial') and placement.is_partial():
sub_mesh_partial_info[mesh_dim] = placement.reduce_type()
return get_rank2tensor_indices(sub_mesh_indices_info, sub_mesh_partial_info)
def _only_reshard_mesh_shape(
dist_tensor: Tensor, mesh: ProcessMesh, placements: list[Placement]
):
if not os.getenv("FLAGS_enable_moe_utils") == "true":
return False
if paddle.in_dynamic_mode():
src_placements = dist_tensor.placements
src_mesh = dist_tensor.process_mesh
elif paddle.framework.in_pir_mode():
src_placements = dist_tensor.dist_attr().placements_attr
src_mesh = dist_tensor.dist_attr().process_mesh
else:
raise NotImplementedError(
"_only_reshard_mesh_shape is only supported in dynamic and pir mode."
)
if src_mesh == mesh or src_mesh.process_ids != mesh.process_ids:
return False
src_rank2tensor_indices = get_local_slices(
dist_tensor, src_mesh, src_placements
)
dst_rank2tensor_indices = get_local_slices(dist_tensor, mesh, placements)
if src_rank2tensor_indices != dst_rank2tensor_indices:
return False
return True
def _reshard_mesh_shape(
dist_tensor: Tensor, mesh: ProcessMesh, placements: list[Placement]
):
if not os.getenv("FLAGS_enable_moe_utils") == "true":
return False
src_mesh = dist_tensor.process_mesh
if src_mesh == mesh or src_mesh.process_ids != mesh.process_ids:
return False
# only the mesh shapes are different,
# if the placements are all replicate,
# then we can reshard the mesh shapes
if not all(p.is_replicated() for p in dist_tensor.placements + placements):
return False
return True