chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,253 @@
|
||||
# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import atexit # noqa: F401
|
||||
|
||||
from .value_patch import monkey_patch_value_in_dist
|
||||
|
||||
monkey_patch_value_in_dist()
|
||||
from paddle.base.core import Placement, ProcessGroup, ReduceType
|
||||
from paddle.distributed.fleet.base.topology import (
|
||||
ParallelMode,
|
||||
create_nccl_config,
|
||||
)
|
||||
from paddle.distributed.fleet.dataset import InMemoryDataset, QueueDataset
|
||||
|
||||
from . import (
|
||||
cloud_utils, # noqa: F401
|
||||
io,
|
||||
rpc, # noqa: F401
|
||||
)
|
||||
from .auto_parallel import shard_op # noqa: F401
|
||||
from .auto_parallel.api import (
|
||||
DistAttr,
|
||||
DistModel,
|
||||
ShardingStage1,
|
||||
ShardingStage2,
|
||||
ShardingStage3,
|
||||
Strategy,
|
||||
dtensor_from_fn,
|
||||
enable_auto_dp, # noqa: F401
|
||||
in_auto_parallel_align_mode, # noqa: F401
|
||||
reshard,
|
||||
shard_dataloader,
|
||||
shard_layer,
|
||||
shard_optimizer,
|
||||
shard_scaler,
|
||||
shard_tensor,
|
||||
to_static,
|
||||
unshard_dtensor,
|
||||
)
|
||||
from .auto_parallel.high_level_api import to_distributed
|
||||
from .auto_parallel.interface import get_mesh, set_mesh
|
||||
from .auto_parallel.intermediate.context_parallel import (
|
||||
ContextParallel,
|
||||
PrepareContextParallel,
|
||||
)
|
||||
from .auto_parallel.intermediate.parallelize import parallelize
|
||||
from .auto_parallel.intermediate.pipeline_parallel import SplitPoint
|
||||
from .auto_parallel.intermediate.tensor_parallel import (
|
||||
ColWiseParallel,
|
||||
ConvParallel,
|
||||
PrepareLayerInput,
|
||||
PrepareLayerOutput,
|
||||
RowWiseParallel,
|
||||
SequenceParallelBegin,
|
||||
SequenceParallelDisable,
|
||||
SequenceParallelEnable,
|
||||
SequenceParallelEnd,
|
||||
)
|
||||
from .auto_parallel.local_layer import LocalLayer
|
||||
from .auto_parallel.local_map import local_map
|
||||
from .auto_parallel.placement_type import (
|
||||
Partial,
|
||||
Replicate,
|
||||
Shard,
|
||||
)
|
||||
from .auto_parallel.process_mesh import ProcessMesh
|
||||
from .collective import (
|
||||
is_available,
|
||||
new_group,
|
||||
restart_process_group,
|
||||
shutdown_process_group,
|
||||
split,
|
||||
)
|
||||
from .communication import ( # noqa: F401
|
||||
P2POp,
|
||||
ReduceOp,
|
||||
all_gather,
|
||||
all_gather_object,
|
||||
all_reduce,
|
||||
alltoall,
|
||||
alltoall_single,
|
||||
barrier,
|
||||
batch_isend_irecv,
|
||||
broadcast,
|
||||
broadcast_object_list,
|
||||
destroy_process_group,
|
||||
gather,
|
||||
get_backend,
|
||||
get_group,
|
||||
irecv,
|
||||
is_initialized,
|
||||
isend,
|
||||
recv,
|
||||
recv_object_list,
|
||||
reduce,
|
||||
reduce_scatter,
|
||||
scatter,
|
||||
scatter_object_list,
|
||||
send,
|
||||
send_object_list,
|
||||
stream,
|
||||
wait,
|
||||
)
|
||||
|
||||
# Import the namespace class directly from the submodule so it does not
|
||||
# shadow ``communication.group`` (the submodule) inside the package.
|
||||
from .communication.group import _DistGroupNamespace as group
|
||||
from .entry_attr import (
|
||||
CountFilterEntry,
|
||||
ProbabilityEntry,
|
||||
ShowClickEntry,
|
||||
)
|
||||
from .flex_checkpoint.dcp.load_state_dict import (
|
||||
load_merged_state_dict,
|
||||
load_state_dict,
|
||||
)
|
||||
from .flex_checkpoint.dcp.save_state_dict import save_state_dict
|
||||
from .flex_checkpoint.dcp.sharded_weight import (
|
||||
ShardedStateDict,
|
||||
ShardedWeight,
|
||||
build_sharded_state_dict,
|
||||
shard_weight,
|
||||
)
|
||||
from .launch.main import launch
|
||||
from .parallel import ( # noqa: F401
|
||||
DataParallel,
|
||||
ParallelEnv,
|
||||
get_rank,
|
||||
get_world_size,
|
||||
init_parallel_env,
|
||||
init_process_group,
|
||||
)
|
||||
from .parallel_with_gloo import (
|
||||
gloo_barrier,
|
||||
gloo_init_parallel_env,
|
||||
gloo_release,
|
||||
)
|
||||
from .sharding import ( # noqa: F401
|
||||
group_sharded_parallel,
|
||||
save_group_sharded_model,
|
||||
)
|
||||
from .spawn import spawn
|
||||
|
||||
__all__ = [
|
||||
"io",
|
||||
"spawn",
|
||||
"launch",
|
||||
"scatter",
|
||||
"gather",
|
||||
"scatter_object_list",
|
||||
"broadcast",
|
||||
"broadcast_object_list",
|
||||
"ParallelEnv",
|
||||
"new_group",
|
||||
"shutdown_process_group",
|
||||
"restart_process_group",
|
||||
"init_parallel_env",
|
||||
"init_process_group",
|
||||
"group",
|
||||
"ProcessGroup",
|
||||
"gloo_init_parallel_env",
|
||||
"gloo_barrier",
|
||||
"gloo_release",
|
||||
"QueueDataset",
|
||||
"split",
|
||||
"CountFilterEntry",
|
||||
"ShowClickEntry",
|
||||
"get_world_size",
|
||||
"get_group",
|
||||
"all_gather",
|
||||
"all_gather_object",
|
||||
"InMemoryDataset",
|
||||
"barrier",
|
||||
"all_reduce",
|
||||
"alltoall",
|
||||
"alltoall_single",
|
||||
"send",
|
||||
"reduce",
|
||||
"recv",
|
||||
"ReduceOp",
|
||||
"wait",
|
||||
"get_rank",
|
||||
"ProbabilityEntry",
|
||||
"ParallelMode",
|
||||
"is_initialized",
|
||||
"destroy_process_group",
|
||||
"isend",
|
||||
"irecv",
|
||||
"send_object_list",
|
||||
"recv_object_list",
|
||||
"reduce_scatter",
|
||||
"is_available",
|
||||
"get_backend",
|
||||
"ProcessMesh",
|
||||
"DistAttr",
|
||||
"shard_tensor",
|
||||
"dtensor_from_fn",
|
||||
"reshard",
|
||||
"shard_layer",
|
||||
"shard_dataloader",
|
||||
"ReduceType",
|
||||
"Placement",
|
||||
"Shard",
|
||||
"Replicate",
|
||||
"Partial",
|
||||
"save_state_dict",
|
||||
"load_state_dict",
|
||||
"load_merged_state_dict",
|
||||
"shard_optimizer",
|
||||
"shard_scaler",
|
||||
"ShardingStage1",
|
||||
"ShardingStage2",
|
||||
"ShardingStage3",
|
||||
"to_static",
|
||||
"Strategy",
|
||||
"DistModel",
|
||||
"LocalLayer",
|
||||
"local_map",
|
||||
"unshard_dtensor",
|
||||
"parallelize",
|
||||
"SequenceParallelEnd",
|
||||
"SequenceParallelBegin",
|
||||
"SequenceParallelEnable",
|
||||
"SequenceParallelDisable",
|
||||
"ColWiseParallel",
|
||||
"RowWiseParallel",
|
||||
"PrepareLayerOutput",
|
||||
"PrepareLayerInput",
|
||||
"SplitPoint",
|
||||
"set_mesh",
|
||||
"get_mesh",
|
||||
"to_distributed",
|
||||
"ConvParallel",
|
||||
"ContextParallel",
|
||||
"PrepareContextParallel",
|
||||
"create_nccl_config",
|
||||
"ShardedWeight",
|
||||
"ShardedStateDict",
|
||||
"shard_weight",
|
||||
"build_sharded_state_dict",
|
||||
]
|
||||
@@ -0,0 +1,32 @@
|
||||
# Copyright (c) 2021 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 .interface import ( # noqa: F401
|
||||
create_mesh,
|
||||
exclude_ops_in_recompute,
|
||||
fetch,
|
||||
get_mesh,
|
||||
recompute,
|
||||
set_mesh,
|
||||
shard_op,
|
||||
shard_tensor,
|
||||
)
|
||||
from .process_mesh import ProcessMesh # noqa: F401
|
||||
from .random import parallel_manual_seed # noqa: F401
|
||||
from .ring_attention import RingFlashAttention # noqa: F401
|
||||
from .ring_conv import RingConv2d # noqa: F401
|
||||
from .static.engine import Engine # noqa: F401
|
||||
from .strategy import Strategy # noqa: F401
|
||||
|
||||
__all__ = []
|
||||
@@ -0,0 +1,98 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from functools import wraps
|
||||
|
||||
import paddle
|
||||
|
||||
|
||||
# NOTE(zhengtianyu): align ClipGradByGlobalNorm in auto_parallel_align_mode.
|
||||
# In old dygraph semi-auto parallel, each rank has parameter and gradient information
|
||||
# from other ranks. To align with this behavior, this decorator ensures auto_hybrid_pp
|
||||
# uses the same logic as old dygraph semi-auto parallel for ClipGradByGlobalNorm in align mode.
|
||||
# Pay attention to the auto_hybrid_pp's default logic matches dynamic manual-parallel,
|
||||
# Refer to NOTE: Fix grad_clip in auto_hybrid_pp mode
|
||||
def _patch_grads_for_step(
|
||||
amp_master_grad=False,
|
||||
):
|
||||
"""
|
||||
Only for auto parallel align mode, use this decorator to handle None gradients in optimizer step.
|
||||
|
||||
This decorator is applied to optimizer step methods to handle cases where parameters
|
||||
have None gradients. It creates zero gradients for parameters that need gradients
|
||||
but currently have None gradients.
|
||||
|
||||
Args:
|
||||
amp_master_grad (bool, optional): Whether to use master gradient mode.
|
||||
If True, gradients will be created as float32 regardless of parameter dtype.
|
||||
If False, gradients will be created with the same dtype as the parameter.
|
||||
Default is False.
|
||||
|
||||
Returns:
|
||||
function: Decorated step method that handles None gradients.
|
||||
|
||||
Example:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from __future__ import annotations
|
||||
>>> import paddle.distributed as dist
|
||||
>>> import types
|
||||
>>> from paddle.distributed.auto_parallel._utils import _patch_grads_for_step
|
||||
|
||||
>>> opt = paddle.optimizer.AdamW(
|
||||
... learning_rate=0.001,
|
||||
... parameters=self.model.parameters(),
|
||||
... grad_clip=paddle.nn.ClipGradByGlobalNorm(1.0),
|
||||
... )
|
||||
>>> if dist.in_auto_parallel_align_mode():
|
||||
>>> orig_step = (
|
||||
... opt.step.__func__ if hasattr(opt.step, "__func__") else opt.step
|
||||
... )
|
||||
>>> decorator = (
|
||||
... _patch_grads_for_step(
|
||||
... amp_master_grad=True
|
||||
... )
|
||||
... )
|
||||
>>> new_step = decorator(orig_step)
|
||||
>>> opt.step = types.MethodType(new_step, opt)
|
||||
|
||||
"""
|
||||
|
||||
def decorator(step_method):
|
||||
@wraps(step_method)
|
||||
def wrapper(self, *args, **kwargs):
|
||||
# Helper function to set gradient for a parameter
|
||||
def set_param_grad(param):
|
||||
if param.stop_gradient or param.grad is not None:
|
||||
return
|
||||
|
||||
if hasattr(param, "main_grad"):
|
||||
param.main_grad = paddle.zeros_like(
|
||||
param, dtype=paddle.float32
|
||||
)
|
||||
else:
|
||||
dtype = paddle.float32 if amp_master_grad else param.dtype
|
||||
param.grad = paddle.zeros_like(param, dtype=dtype)
|
||||
|
||||
if not isinstance(self._parameter_list[0], dict):
|
||||
for param in self._parameter_list:
|
||||
set_param_grad(param)
|
||||
else:
|
||||
for param_group in self._param_groups:
|
||||
for param in param_group['params']:
|
||||
set_param_grad(param)
|
||||
return step_method(self, *args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,95 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
|
||||
_enable_auto_dp_mode = False
|
||||
|
||||
|
||||
def _fake_replicate_grad_to_partial(grad, partial_axis):
|
||||
new_placements = grad.placements
|
||||
assert new_placements[partial_axis] == dist.Replicate(), (
|
||||
"when reshard fake replicated grad to partial, the partial axis of grad should be Replicate"
|
||||
)
|
||||
|
||||
new_placements[partial_axis] = dist.Partial(dist.ReduceType.kRedSum)
|
||||
|
||||
grad_mesh = grad.process_mesh
|
||||
grad = dist.auto_parallel.api.dtensor_to_local(
|
||||
grad, grad_mesh, grad.placements
|
||||
)
|
||||
grad = dist.auto_parallel.api.dtensor_from_local(
|
||||
grad, grad_mesh, new_placements
|
||||
)
|
||||
return grad
|
||||
|
||||
|
||||
def _convert_fake_replicate_grad_to_partial(params_grads):
|
||||
# skip non-parallel cases
|
||||
world_size = paddle.distributed.get_world_size()
|
||||
if world_size == 1:
|
||||
return
|
||||
|
||||
if isinstance(params_grads, list):
|
||||
for idx in range(len(params_grads)):
|
||||
param, grad = params_grads[idx][0], params_grads[idx][1]
|
||||
if grad.is_dist():
|
||||
grad_placements = grad.placements
|
||||
if not isinstance(grad_placements[0], dist.Partial):
|
||||
grad = _fake_replicate_grad_to_partial(grad, 0)
|
||||
else:
|
||||
default_grad_placements = [
|
||||
dist.Partial(dist.ReduceType.kRedSum)
|
||||
]
|
||||
default_grad_mesh = dist.ProcessMesh(
|
||||
list(range(0, world_size)), dim_names=["dp"]
|
||||
)
|
||||
grad = dist.auto_parallel.api.dtensor_from_local(
|
||||
grad, default_grad_mesh, default_grad_placements
|
||||
)
|
||||
params_grads[idx] = (param, grad)
|
||||
else:
|
||||
for idx in range(len(params_grads['params'])):
|
||||
grad = params_grads['params'][idx][1]
|
||||
if grad.is_dist():
|
||||
grad_placements = grad.placements
|
||||
if not isinstance(grad_placements[0], dist.Partial):
|
||||
grad = _fake_replicate_grad_to_partial(grad, 0)
|
||||
else:
|
||||
default_grad_placements = [
|
||||
dist.Partial(dist.ReduceType.kRedSum)
|
||||
]
|
||||
default_grad_mesh = dist.ProcessMesh(
|
||||
list(range(0, world_size)), dim_names=["dp"]
|
||||
)
|
||||
grad = dist.auto_parallel.api.dtensor_from_local(
|
||||
grad, default_grad_mesh, default_grad_placements
|
||||
)
|
||||
params_grads['params'][idx] = (params_grads['params'][idx][0], grad)
|
||||
|
||||
|
||||
def in_auto_dp_mode():
|
||||
world_size = paddle.distributed.get_world_size()
|
||||
if world_size <= 1:
|
||||
return False
|
||||
|
||||
global _enable_auto_dp_mode
|
||||
return _enable_auto_dp_mode
|
||||
|
||||
|
||||
def _enable_auto_dp():
|
||||
global _enable_auto_dp_mode
|
||||
_enable_auto_dp_mode = True
|
||||
@@ -0,0 +1,370 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import defaultdict
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from paddle import Tensor
|
||||
from paddle._typing.dtype_like import _DTypeLiteral
|
||||
|
||||
# _g_default_config[category][field] = default_value
|
||||
_g_default_config = defaultdict(dict)
|
||||
|
||||
|
||||
def get_category_default_config(category):
|
||||
return _g_default_config[category]
|
||||
|
||||
|
||||
def set_category_default_config(category, default_value):
|
||||
_g_default_config[category] = default_value
|
||||
|
||||
|
||||
def get_field_default_config(category, field):
|
||||
return _g_default_config[category][field]
|
||||
|
||||
|
||||
def set_field_default_config(category, field, default_value):
|
||||
_g_default_config[category][field] = default_value
|
||||
|
||||
|
||||
NOT_FOUND = "not_found"
|
||||
|
||||
#########################################
|
||||
# base configuration
|
||||
#########################################
|
||||
BASE = "base"
|
||||
set_field_default_config(BASE, "auto_mode", "semi")
|
||||
set_field_default_config(BASE, "gradient_scale", True)
|
||||
set_field_default_config(BASE, "gradient_scale_using_allreduce_avg", False)
|
||||
set_field_default_config(BASE, "use_cache", True)
|
||||
set_field_default_config(BASE, "return_numpy", True)
|
||||
set_field_default_config(BASE, "all_ranks", False)
|
||||
set_field_default_config(BASE, "split_data", True)
|
||||
set_field_default_config(BASE, "seed", None)
|
||||
set_field_default_config(BASE, "reinit", False) # Only for debug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _BaseConfig(TypedDict, total=False): # noqa: PYI049
|
||||
auto_mode: str
|
||||
gradient_scale: bool
|
||||
gradient_scale_using_allreduce_avg: bool
|
||||
use_cache: bool
|
||||
return_numpy: bool
|
||||
all_ranks: bool
|
||||
split_data: bool
|
||||
seed: int | None
|
||||
reinit: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# recompute configuration
|
||||
#########################################
|
||||
RECOMPUTE = "recompute"
|
||||
set_field_default_config(RECOMPUTE, "enable", False)
|
||||
set_field_default_config(RECOMPUTE, "checkpoints", [])
|
||||
set_field_default_config(RECOMPUTE, "no_recompute_segments", [])
|
||||
set_field_default_config(RECOMPUTE, "sr", 0)
|
||||
set_field_default_config(RECOMPUTE, "refined_ops_patterns", []) # List[Dict]
|
||||
set_field_default_config(RECOMPUTE, "enable_tuning", False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _RefinedOpsPatterns(TypedDict, total=False):
|
||||
main_ops: list[str]
|
||||
num: int
|
||||
pre_ops: list[str]
|
||||
suf_ops: list[str]
|
||||
|
||||
class _RecomputeConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
checkpoints: list[Tensor]
|
||||
no_recompute_segments: list[int]
|
||||
sr: int
|
||||
refined_ops_patterns: list[_RefinedOpsPatterns]
|
||||
enable_tuning: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# AMP configuration
|
||||
#########################################
|
||||
AMP = "amp"
|
||||
set_field_default_config(AMP, "enable", False)
|
||||
set_field_default_config(AMP, "dtype", "float16")
|
||||
set_field_default_config(AMP, "level", "o1")
|
||||
set_field_default_config(AMP, "init_loss_scaling", 32768.0)
|
||||
set_field_default_config(AMP, "incr_every_n_steps", 1000)
|
||||
set_field_default_config(AMP, "decr_every_n_nan_or_inf", 2)
|
||||
set_field_default_config(AMP, "incr_ratio", 2.0)
|
||||
set_field_default_config(AMP, "decr_ratio", 0.8)
|
||||
set_field_default_config(AMP, "use_dynamic_loss_scaling", True)
|
||||
set_field_default_config(AMP, "custom_white_list", [])
|
||||
set_field_default_config(AMP, "custom_black_list", [])
|
||||
set_field_default_config(AMP, "custom_black_varnames", [])
|
||||
set_field_default_config(AMP, "use_fp16_guard", False)
|
||||
set_field_default_config(AMP, "use_bf16_guard", False)
|
||||
set_field_default_config(AMP, "use_master_grad", False)
|
||||
set_field_default_config(AMP, "use_promote", True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _AMPConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
dtype: _DTypeLiteral
|
||||
level: str
|
||||
init_loss_scaling: float
|
||||
incr_every_n_steps: int
|
||||
decr_every_n_nan_or_inf: int
|
||||
incr_ratio: float
|
||||
decr_ratio: float
|
||||
use_dynamic_loss_scaling: bool
|
||||
custom_white_list: list[str]
|
||||
custom_black_list: list[str]
|
||||
custom_black_varnames: list[str]
|
||||
use_fp16_guard: bool
|
||||
use_bf16_guard: bool
|
||||
use_master_grad: bool
|
||||
use_promote: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# sharding configuration
|
||||
#########################################
|
||||
SHARDING = "sharding"
|
||||
set_field_default_config(SHARDING, "enable", False)
|
||||
set_field_default_config(SHARDING, "stage", 1)
|
||||
set_field_default_config(SHARDING, "degree", 8)
|
||||
set_field_default_config(SHARDING, "enable_overlap", False)
|
||||
set_field_default_config(SHARDING, "param_comm_stream_num", 1)
|
||||
set_field_default_config(SHARDING, "grad_comm_stream_num", 1)
|
||||
set_field_default_config(SHARDING, "param_bucket_size_numel", 1)
|
||||
set_field_default_config(SHARDING, "grad_bucket_size_numel", 1)
|
||||
set_field_default_config(SHARDING, "enable_hierarchical_comm", False)
|
||||
set_field_default_config(SHARDING, "partition_algor", "greedy_even")
|
||||
set_field_default_config(SHARDING, "enable_tuning", False)
|
||||
set_field_default_config(SHARDING, "tuning_range", [])
|
||||
set_field_default_config(SHARDING, "release_gradients", False)
|
||||
set_field_default_config(SHARDING, "comm_buffer_size_MB", 256)
|
||||
set_field_default_config(SHARDING, "enable_tensor_fusion", False)
|
||||
set_field_default_config(SHARDING, "save_unbalanced_param", True)
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _ShardingConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
stage: int
|
||||
degree: int
|
||||
enable_overlap: bool
|
||||
param_comm_stream_num: int
|
||||
grad_comm_stream_num: int
|
||||
param_bucket_size_numel: int
|
||||
grad_bucket_size_numel: int
|
||||
enable_hierarchical_comm: bool
|
||||
partition_algor: str
|
||||
enable_tuning: bool
|
||||
tuning_range: list[int] | tuple[int, int]
|
||||
|
||||
|
||||
#########################################
|
||||
# gradient merge configuration
|
||||
#########################################
|
||||
GRADIENT_MERGE = "gradient_merge"
|
||||
set_field_default_config(GRADIENT_MERGE, "enable", False)
|
||||
set_field_default_config(GRADIENT_MERGE, "k_steps", 1)
|
||||
set_field_default_config(GRADIENT_MERGE, "avg", True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _GradientMergeConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
k_steps: int
|
||||
avg: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# pipeline configuration
|
||||
#########################################
|
||||
PIPELINE = "pipeline"
|
||||
set_field_default_config(PIPELINE, "enable", False)
|
||||
set_field_default_config(PIPELINE, "schedule_mode", "1F1B")
|
||||
set_field_default_config(PIPELINE, "pp_degree", 1)
|
||||
set_field_default_config(PIPELINE, "vpp_degree", 1)
|
||||
set_field_default_config(PIPELINE, "vpp_seg_method", "")
|
||||
set_field_default_config(PIPELINE, "micro_batch_size", 1)
|
||||
set_field_default_config(PIPELINE, "accumulate_steps", 1)
|
||||
set_field_default_config(PIPELINE, "generation_batch_size", 1)
|
||||
set_field_default_config(PIPELINE, "enable_send_recv_overlap", False)
|
||||
set_field_default_config(PIPELINE, "job_schedule_profiler_start", -1)
|
||||
set_field_default_config(PIPELINE, "job_schedule_profiler_stop", -1)
|
||||
set_field_default_config(PIPELINE, "program_runtimes", [61, 72, 71, 34, 3])
|
||||
set_field_default_config(PIPELINE, "memory_limit_times", -1)
|
||||
set_field_default_config(PIPELINE, "split_backward", False)
|
||||
set_field_default_config(PIPELINE, "auto_parallel_sync_shared_params", False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _PipelineConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
schedule_mode: str
|
||||
pp_degree: int
|
||||
vpp_degree: int
|
||||
vpp_seg_method: str
|
||||
micro_batch_size: int
|
||||
accumulate_steps: int
|
||||
generation_batch_size: int
|
||||
enable_send_recv_overlap: bool
|
||||
job_schedule_profiler_start: int
|
||||
job_schedule_profiler_stop: int
|
||||
split_backward: bool
|
||||
auto_parallel_sync_shared_params: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# quantization configuration
|
||||
#########################################
|
||||
QAT = "qat"
|
||||
set_field_default_config(QAT, "enable", False)
|
||||
set_field_default_config(QAT, "channel_wise_abs_max", True)
|
||||
set_field_default_config(QAT, "weight_bits", 8)
|
||||
set_field_default_config(QAT, "activation_bits", 8)
|
||||
set_field_default_config(QAT, "not_quant_pattern", ['skip_quant'])
|
||||
set_field_default_config(QAT, "algo", None)
|
||||
set_field_default_config(QAT, "onnx_format", True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _QATConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
channel_wise_abs_max: bool
|
||||
weight_bits: int
|
||||
activation_bits: int
|
||||
not_quant_pattern: list[str]
|
||||
algo: str | None
|
||||
onnx_format: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# auto tuning configuration
|
||||
#########################################
|
||||
TUNING = "tuning"
|
||||
set_field_default_config(TUNING, "enable", False)
|
||||
set_field_default_config(TUNING, "profile_start_step", 1)
|
||||
set_field_default_config(TUNING, "profile_end_step", 1)
|
||||
set_field_default_config(TUNING, "run_after_tuning", True)
|
||||
set_field_default_config(TUNING, "debug", False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _TuningConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
profile_start_step: int
|
||||
profile_end_step: int
|
||||
run_after_tuning: bool
|
||||
debug: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# dataset configuration
|
||||
#########################################
|
||||
DATASET = "dataset"
|
||||
set_field_default_config(DATASET, "enable", False)
|
||||
set_field_default_config(DATASET, "num_shards", 1)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _DatasetConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
num_shards: int
|
||||
|
||||
|
||||
# #########################################
|
||||
# # offload configuration
|
||||
# #########################################
|
||||
FUSEDLINEARPROMOTION = "fused_linear_promotion"
|
||||
set_field_default_config(FUSEDLINEARPROMOTION, "enable", False)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _FusedLinearPromotionConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# fused passes configuration
|
||||
#########################################
|
||||
FUSED_PASSES = "fused_passes"
|
||||
set_field_default_config(FUSED_PASSES, "enable", False)
|
||||
set_field_default_config(FUSED_PASSES, "fused_passes_list", [])
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _FusedPassesConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
fused_passes_list: list[str]
|
||||
|
||||
|
||||
#########################################
|
||||
# data parallel configuration
|
||||
#########################################
|
||||
DP_OPTIMIZATION = "dp_optimization"
|
||||
set_field_default_config(DP_OPTIMIZATION, "enable", False)
|
||||
set_field_default_config(DP_OPTIMIZATION, "fuse_all_reduce_ops", True)
|
||||
set_field_default_config(DP_OPTIMIZATION, "fuse_grad_size_in_MB", 32)
|
||||
set_field_default_config(DP_OPTIMIZATION, "overlap_comm_cacl", True)
|
||||
set_field_default_config(
|
||||
DP_OPTIMIZATION, "gradient_sync_after_accumulate", False
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _DPOptimizationConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
fuse_all_reduce_ops: bool
|
||||
fuse_grad_size_in_MB: int
|
||||
overlap_comm_cacl: bool
|
||||
gradient_sync_after_accumulate: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# model parallel configuration
|
||||
#########################################
|
||||
MP_OPTIMIZATION = "mp_optimization"
|
||||
set_field_default_config(
|
||||
MP_OPTIMIZATION, "allreduce_matmul_grad_overlapping", False
|
||||
)
|
||||
set_field_default_config(MP_OPTIMIZATION, "replace_with_c_embedding", False)
|
||||
|
||||
set_field_default_config(
|
||||
MP_OPTIMIZATION, "replace_with_parallel_cross_entropy", False
|
||||
)
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _MPOptimizationConfig(TypedDict, total=False): # noqa: PYI049
|
||||
allreduce_matmul_grad_overlapping: bool
|
||||
|
||||
|
||||
#########################################
|
||||
# sequence parallel configuration
|
||||
#########################################
|
||||
SP_OPTIMIZATION = "sp_optimization"
|
||||
set_field_default_config(SP_OPTIMIZATION, "enable", True)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
class _SPOptimizationConfig(TypedDict, total=False): # noqa: PYI049
|
||||
enable: bool
|
||||
@@ -0,0 +1,13 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,199 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import os
|
||||
from types import MethodType
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.autograd import PyLayer
|
||||
|
||||
from .auto_dp_utils import in_auto_dp_mode
|
||||
from .fully_shard_fusion import FullyShardFusion
|
||||
|
||||
|
||||
def shard_accumulators(parameters_and_grads, optimizer, target_block):
|
||||
if getattr(optimizer, "_has_sharded_accumulators", False):
|
||||
return
|
||||
optimizer._has_sharded_accumulators = True
|
||||
for param, _ in parameters_and_grads:
|
||||
optimizer._create_accumulators(
|
||||
target_block,
|
||||
[param],
|
||||
)
|
||||
target_name = param.name
|
||||
if param.name in optimizer._master_weights.keys():
|
||||
master_weight = optimizer._master_weights[param.name]
|
||||
target_name = master_weight.name
|
||||
for key in optimizer._accumulators.keys():
|
||||
accumulator = optimizer._accumulators[key][target_name]
|
||||
if accumulator.is_dist():
|
||||
continue
|
||||
origin_accumulator_name = accumulator.name
|
||||
|
||||
if 'beta' not in key:
|
||||
placements = copy.deepcopy(param.placements)
|
||||
else:
|
||||
placements = [
|
||||
dist.Replicate()
|
||||
for _ in range(len(param.process_mesh.shape))
|
||||
]
|
||||
optimizer._accumulators[key][target_name] = dist.shard_tensor(
|
||||
accumulator,
|
||||
mesh=param.process_mesh,
|
||||
placements=placements,
|
||||
)
|
||||
optimizer._accumulators[key][
|
||||
target_name
|
||||
].name = origin_accumulator_name
|
||||
|
||||
def _finish_update_impl(self, block, parameters_and_grads):
|
||||
if not isinstance(parameters_and_grads, list):
|
||||
parameters_and_grads = parameters_and_grads['params']
|
||||
for param, _ in parameters_and_grads:
|
||||
param.main_grad = None
|
||||
|
||||
optimizer._finish_update = MethodType(_finish_update_impl, optimizer)
|
||||
|
||||
|
||||
class FullyShardAuto:
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
mesh,
|
||||
enable_tensor_fusion_and_overlap=True,
|
||||
fsdp_unit_layers=None,
|
||||
moe_layers_name=None,
|
||||
):
|
||||
if enable_tensor_fusion_and_overlap:
|
||||
FullyShardFusion(model, mesh, fsdp_unit_layers, moe_layers_name)
|
||||
else:
|
||||
self.model = model
|
||||
self.mesh = mesh
|
||||
# use first dims as sharding axis
|
||||
self._shard_fn = dist.ShardingStage3(0, self.mesh)
|
||||
for param in self.model.parameters():
|
||||
param._need_shard_auto = True
|
||||
self._shard_fn._shard_parameter(param)
|
||||
if not in_auto_dp_mode():
|
||||
self._shard_fn._register_hook_for_param_grad(param)
|
||||
if in_auto_dp_mode():
|
||||
self._register_comm_hook(self.model)
|
||||
os.environ["skip_sharding3_output_reshard"] = "1"
|
||||
|
||||
def _register_comm_hook(self, model):
|
||||
def _pre_forward_hook(sublayers):
|
||||
@paddle.autograd.no_grad()
|
||||
def gather_comm(*_):
|
||||
dp_axis = dist.auto_parallel.get_mesh().dim_names.index('dp')
|
||||
for key, param in sublayers._parameters.items():
|
||||
if param.placements[dp_axis] != dist.Replicate():
|
||||
new_placements = copy.deepcopy(param.placements)
|
||||
new_placements[dp_axis] = dist.Replicate()
|
||||
replicate_param = dist.reshard(
|
||||
param, param.process_mesh, new_placements
|
||||
)
|
||||
param.get_tensor()._share_data_with(
|
||||
replicate_param.get_tensor()
|
||||
)
|
||||
|
||||
return gather_comm
|
||||
|
||||
def _post_forward_hook(sublayers):
|
||||
@paddle.autograd.no_grad()
|
||||
def shard_comm(*_):
|
||||
dp_axis = dist.auto_parallel.get_mesh().dim_names.index('dp')
|
||||
for key, param in sublayers._parameters.items():
|
||||
if (
|
||||
param.trainable
|
||||
and param.placements[dp_axis] == dist.Replicate()
|
||||
):
|
||||
new_placements = copy.deepcopy(param.placements)
|
||||
new_placements[dp_axis] = dist.Shard(dp_axis)
|
||||
shard_param = dist.reshard(
|
||||
param, param.process_mesh, new_placements
|
||||
)
|
||||
param.get_tensor()._share_data_with(
|
||||
shard_param.get_tensor()
|
||||
)
|
||||
|
||||
return shard_comm
|
||||
|
||||
def _post_backward_hook(param):
|
||||
def shard_comm(grad):
|
||||
dp_axis = dist.auto_parallel.get_mesh().dim_names.index('dp')
|
||||
if param.placements[dp_axis] == dist.Replicate():
|
||||
new_placements = copy.deepcopy(param.placements)
|
||||
new_placements[dp_axis] = dist.Shard(dp_axis)
|
||||
shard_param = dist.reshard(
|
||||
param, param.process_mesh, new_placements
|
||||
)
|
||||
param.get_tensor()._share_data_with(
|
||||
shard_param.get_tensor()
|
||||
)
|
||||
return grad
|
||||
|
||||
param.register_hook(shard_comm)
|
||||
|
||||
# register forward hooks
|
||||
for name, sublayers in model.named_sublayers(include_self=True):
|
||||
sublayers.register_forward_pre_hook(_pre_forward_hook(sublayers))
|
||||
sublayers.register_forward_post_hook(_post_forward_hook(sublayers))
|
||||
|
||||
# register backward hooks
|
||||
for param in model.parameters():
|
||||
if param.trainable:
|
||||
_post_backward_hook(param)
|
||||
|
||||
# register layer hooks for param sync in tie weights
|
||||
self._register_layer_hooks(model)
|
||||
|
||||
def _register_layer_hooks(self, layer, name="last_layer"):
|
||||
def _forward_post_hook(layer, inputs, outputs):
|
||||
return LayerHook.apply(
|
||||
outputs,
|
||||
layer=layer,
|
||||
)
|
||||
|
||||
if layer.parameters(include_sublayers=False):
|
||||
layer.register_forward_post_hook(_forward_post_hook)
|
||||
for name, sub_layer in layer.named_children():
|
||||
self._register_layer_hooks(sub_layer, name)
|
||||
|
||||
|
||||
class LayerHook(PyLayer):
|
||||
@staticmethod
|
||||
def forward(ctx, inputs, layer):
|
||||
ctx.layer = layer
|
||||
return inputs
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args):
|
||||
layer = ctx.layer
|
||||
dp_axis = dist.auto_parallel.get_mesh().dim_names.index('dp')
|
||||
for param in layer.parameters(include_sublayers=False):
|
||||
if (
|
||||
param.trainable
|
||||
and param.placements[dp_axis] != dist.Replicate()
|
||||
):
|
||||
new_placements = copy.deepcopy(param.placements)
|
||||
new_placements[dp_axis] = dist.Replicate()
|
||||
replicate_param = dist.reshard(
|
||||
param, param.process_mesh, new_placements
|
||||
)
|
||||
param.get_tensor()._share_data_with(
|
||||
replicate_param.get_tensor()
|
||||
)
|
||||
return args
|
||||
@@ -0,0 +1,809 @@
|
||||
# Copyright (c) 2026 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 collections import OrderedDict
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.autograd import PyLayer
|
||||
from paddle.distributed.fleet.utils.tensor_fusion_helper import (
|
||||
align,
|
||||
alignment,
|
||||
get_current_device_type,
|
||||
)
|
||||
|
||||
# Global registry for fsdp_context
|
||||
_g_fsdp_context = None
|
||||
|
||||
|
||||
def register_fsdp_context(context):
|
||||
global _g_fsdp_context
|
||||
_g_fsdp_context = context
|
||||
|
||||
|
||||
def get_fsdp_context():
|
||||
return _g_fsdp_context
|
||||
|
||||
|
||||
class BufferState(Enum):
|
||||
# Buffer status for lazy double buffer mechanism
|
||||
#
|
||||
# State transitions:
|
||||
# FREED ──all_gather──> USING ──computation done──> READY ──release──> FREED
|
||||
# ^ │
|
||||
# │ (reuse) │
|
||||
# └────────────────────────────┘
|
||||
|
||||
FREED = 1 # Released, buffer data is sharded, tmp_buffer not allocated
|
||||
USING = 2 # Unsharded and actively in use
|
||||
READY = 3 # Unsharded, marked for lazy release, can be reused
|
||||
SYNCING = 4 # Communication in progress
|
||||
|
||||
|
||||
@dataclass
|
||||
class BufferGroup:
|
||||
params: list = field(default_factory=list)
|
||||
dtype: object = None
|
||||
trainable: bool = None
|
||||
fsdp_unit_id: int = None
|
||||
is_tie: bool = False
|
||||
is_expert_param: bool = False
|
||||
fsdp_group: object = None
|
||||
params_buffer: 'TensorFusionBuffer' = None
|
||||
grads_buffer: 'TensorFusionBuffer' = None
|
||||
params_use_sum: int = 0
|
||||
params_use_cnt: int = 0
|
||||
grads_use_sum: int = 0
|
||||
grads_use_cnt: int = 0
|
||||
|
||||
|
||||
def _dtensor_from_local(local_tensor, mesh, placements):
|
||||
global_dims = list(local_tensor.shape)
|
||||
for idx, placement in enumerate(placements):
|
||||
if placement.is_shard():
|
||||
global_dims[placement.get_dim()] = (
|
||||
global_dims[placement.get_dim()] * mesh.shape[idx]
|
||||
)
|
||||
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,
|
||||
)
|
||||
|
||||
|
||||
class TensorFusionBuffer:
|
||||
def __init__(
|
||||
self,
|
||||
group_id,
|
||||
params,
|
||||
fsdp_degree,
|
||||
dtype,
|
||||
is_params=False,
|
||||
main_grad_dtype=None,
|
||||
):
|
||||
# Calculate total buffer size needed (with padding)
|
||||
self.group_id = group_id
|
||||
self.fsdp_degree = fsdp_degree
|
||||
self.dtype = dtype
|
||||
self.main_grad_dtype = (
|
||||
main_grad_dtype if main_grad_dtype is not None else dtype
|
||||
)
|
||||
self.total_buffer_size = 0
|
||||
self.param_offsets = {}
|
||||
self.tmp_data_buffer = None
|
||||
self.comm_task = None
|
||||
self.trainable = params[0].trainable
|
||||
|
||||
for param in params:
|
||||
self.param_offsets[param.name] = self.total_buffer_size
|
||||
self.total_buffer_size += self.get_padded_size(param)
|
||||
|
||||
if is_params:
|
||||
# Create fused params_buffer
|
||||
# TODO(lizhenxing): Build full params_buffer on CPU and only move shards to GPU to minimize mem peaks
|
||||
self.data_buffer = paddle.zeros(
|
||||
shape=[self.total_buffer_size],
|
||||
dtype=dtype,
|
||||
)
|
||||
# Use BufferState enum instead of is_shard boolean, initial state is FREED (sharded)
|
||||
self.status = BufferState.FREED
|
||||
|
||||
for param in params:
|
||||
offset = self.param_offsets[param.name]
|
||||
stop_gradient = param.stop_gradient
|
||||
local_shape = param._local_shape
|
||||
param.stop_gradient = True
|
||||
param._local_value().flatten_()
|
||||
paddle.assign(
|
||||
param._local_value(),
|
||||
self.data_buffer._slice(
|
||||
offset,
|
||||
offset + param._numel(),
|
||||
),
|
||||
)
|
||||
|
||||
param._clear_data()
|
||||
param.stop_gradient = stop_gradient
|
||||
param._local_value().get_tensor()._set_dims(local_shape)
|
||||
paddle.device.cuda.empty_cache()
|
||||
|
||||
mesh = dist.auto_parallel.get_mesh()
|
||||
curr_global_rank = paddle.distributed.get_rank()
|
||||
if curr_global_rank in mesh.process_ids:
|
||||
total_nums = self.data_buffer.shape[0]
|
||||
num_of_pieces = mesh.shape[0]
|
||||
piece_len = (total_nums + num_of_pieces - 1) // num_of_pieces
|
||||
rank_relative = mesh.process_ids.index(curr_global_rank)
|
||||
start = rank_relative * piece_len
|
||||
end = min(start + piece_len, total_nums)
|
||||
self.data_buffer = paddle.slice(
|
||||
self.data_buffer, [0], [start], [end]
|
||||
).clone()
|
||||
|
||||
# Init params_buffer attr
|
||||
self.data_buffer.name = "fuse_params_" + str(group_id)
|
||||
self.data_buffer.stop_gradient = params[0].stop_gradient
|
||||
self.data_buffer.optimize_attr = params[0].optimize_attr
|
||||
else:
|
||||
# Create fused grads_buffer with shard
|
||||
self.data_buffer = paddle.zeros(
|
||||
shape=[self.total_buffer_size // self.fsdp_degree],
|
||||
dtype=self.main_grad_dtype,
|
||||
)
|
||||
|
||||
# Register get_main_grad method for each param, returns view_slice of grad_buffer
|
||||
for param in params:
|
||||
if param.trainable:
|
||||
param._fusion_buffer = self
|
||||
param._param_offsets = self.param_offsets
|
||||
|
||||
def get_grad_from_tmp_buf(param):
|
||||
tmp_buffer = param._fusion_buffer.get_tmp_buffer()
|
||||
offset = param._param_offsets[param.name]
|
||||
main_grad = paddle._C_ops.view_slice(
|
||||
tmp_buffer,
|
||||
offset,
|
||||
offset + param._numel(),
|
||||
)
|
||||
return main_grad
|
||||
|
||||
param.get_main_grad = get_grad_from_tmp_buf.__get__(param)
|
||||
|
||||
def get_padded_size(self, param):
|
||||
size = np.prod(param.shape)
|
||||
align_size = (
|
||||
alignment[get_current_device_type()]
|
||||
// align[param.dtype]
|
||||
* self.fsdp_degree
|
||||
)
|
||||
return ((size + align_size - 1) // align_size) * align_size
|
||||
|
||||
def get_tmp_buffer(self):
|
||||
# Reuse tmp_buffer if exists, else create
|
||||
if self.tmp_data_buffer is None:
|
||||
self.tmp_data_buffer = paddle.zeros(
|
||||
shape=[self.total_buffer_size], dtype=self.dtype
|
||||
)
|
||||
return self.tmp_data_buffer
|
||||
|
||||
def clear_tmp_buffer(self):
|
||||
if self.tmp_data_buffer is not None:
|
||||
self.tmp_data_buffer._clear_data()
|
||||
self.tmp_data_buffer = None
|
||||
# paddle.device.cuda.empty_cache()
|
||||
|
||||
|
||||
class FSDPBufferManager:
|
||||
def __init__(
|
||||
self, model, mesh, fsdp_unit_layers=None, moe_layers_name=None
|
||||
):
|
||||
self.model = model
|
||||
self._fsdp_group = mesh.get_group("dp")
|
||||
self.main_grad_dtype = paddle.float32
|
||||
|
||||
# Get EP group if "ep" dimension exists in mesh
|
||||
if "ep" in mesh.dim_names:
|
||||
self._ep_fsdp_group = mesh.get_group("ep")
|
||||
else:
|
||||
self._ep_fsdp_group = self._fsdp_group
|
||||
|
||||
topk = None
|
||||
if hasattr(self.model, 'config') and hasattr(
|
||||
self.model.config, 'num_experts_per_tok'
|
||||
):
|
||||
topk = self.model.config.num_experts_per_tok
|
||||
|
||||
# Layer types to wrap as FSDP sharding layers
|
||||
# Note: 'Qwen3VLTextDecoderLayer' is temporary; fleet models all use 'TransformerLayer'
|
||||
self.fsdp_unit_layers = fsdp_unit_layers or [
|
||||
'TransformerLayer',
|
||||
'Qwen3VLTextDecoderLayer',
|
||||
'Qwen3MoeDecoderLayer',
|
||||
]
|
||||
# Layer types to identify MoE expert layers
|
||||
self.moe_layers_name = moe_layers_name or [
|
||||
'StandardMLPExpert',
|
||||
]
|
||||
|
||||
# Get tie_param_name if using tie_weights
|
||||
self.tie_param_name = None
|
||||
# Note: need add get_input_embeddings in fleet modeling
|
||||
# if hasattr(self.model, "get_input_embeddings"):
|
||||
# self.tie_param_name = self.model.get_input_embeddings().weight.name
|
||||
|
||||
# Create buffer_groups
|
||||
grouped_params, group_is_expert = self._build_groups()
|
||||
self.buffer_groups = []
|
||||
self.param_to_buffer_id = {}
|
||||
|
||||
# Create params_buffer, grads_buffer with groups
|
||||
for gid, params in grouped_params.items():
|
||||
is_expert = group_is_expert.get(gid, False)
|
||||
# Use EP group for expert params, DP group for regular params
|
||||
fsdp_group = self._ep_fsdp_group if is_expert else self._fsdp_group
|
||||
|
||||
params_buffer = TensorFusionBuffer(
|
||||
gid,
|
||||
params,
|
||||
fsdp_group.nranks,
|
||||
params[0].dtype,
|
||||
is_params=True,
|
||||
)
|
||||
|
||||
if not params[0].stop_gradient:
|
||||
grads_buffer = TensorFusionBuffer(
|
||||
gid,
|
||||
params,
|
||||
fsdp_group.nranks,
|
||||
params[0].dtype,
|
||||
main_grad_dtype=self.main_grad_dtype,
|
||||
)
|
||||
else:
|
||||
grads_buffer = None
|
||||
|
||||
if is_expert:
|
||||
_params_use_sum = topk
|
||||
_grads_use_sum = topk
|
||||
else:
|
||||
_params_use_sum = len(params)
|
||||
_grads_use_sum = len(params)
|
||||
self.buffer_groups.append(
|
||||
BufferGroup(
|
||||
params=params,
|
||||
dtype=params[0].dtype,
|
||||
trainable=params[0].trainable,
|
||||
is_expert_param=is_expert,
|
||||
fsdp_group=fsdp_group,
|
||||
params_buffer=params_buffer,
|
||||
grads_buffer=grads_buffer,
|
||||
params_use_sum=_params_use_sum,
|
||||
params_use_cnt=0,
|
||||
grads_use_sum=_grads_use_sum,
|
||||
grads_use_cnt=0,
|
||||
)
|
||||
)
|
||||
|
||||
for param in params:
|
||||
self.param_to_buffer_id[param.name] = gid
|
||||
|
||||
def _build_groups(self):
|
||||
parameters = self.model.parameters()
|
||||
grouped_params = OrderedDict()
|
||||
group_is_expert = {}
|
||||
curr_gid = 0
|
||||
|
||||
param_to_unit_id = {}
|
||||
for unit_id, module in enumerate(self.model.modules()):
|
||||
if type(module).__name__ in self.fsdp_unit_layers:
|
||||
for param in module.parameters():
|
||||
param_to_unit_id[param.name] = unit_id
|
||||
if type(module).__name__ in self.moe_layers_name:
|
||||
for param in module.parameters():
|
||||
param.is_moe_param = True
|
||||
|
||||
temp_groups = []
|
||||
for param in parameters:
|
||||
name = param.name
|
||||
is_expert = getattr(param, "is_moe_param", False)
|
||||
if is_expert:
|
||||
continue
|
||||
is_tie = (
|
||||
self.tie_param_name is not None and name == self.tie_param_name
|
||||
)
|
||||
|
||||
param_attrs = {
|
||||
"dtype": param.dtype,
|
||||
"trainable": param.trainable,
|
||||
"fsdp_unit_id": param_to_unit_id.get(name),
|
||||
"is_tie": is_tie,
|
||||
"is_expert_param": is_expert,
|
||||
}
|
||||
|
||||
found_group = False
|
||||
for param_group in temp_groups:
|
||||
if (
|
||||
param_group.dtype == param_attrs["dtype"]
|
||||
and param_group.trainable == param_attrs["trainable"]
|
||||
and param_group.fsdp_unit_id == param_attrs["fsdp_unit_id"]
|
||||
and param_group.is_tie == param_attrs["is_tie"]
|
||||
and param_group.is_expert_param
|
||||
== param_attrs["is_expert_param"]
|
||||
):
|
||||
param_group.params.append(param)
|
||||
found_group = True
|
||||
break
|
||||
|
||||
# Create new group if no matching
|
||||
if not found_group:
|
||||
temp_groups.append(BufferGroup(params=[param], **param_attrs))
|
||||
|
||||
def group_sort_key(group):
|
||||
priority = 0 if group.is_tie else (1 if not group.trainable else 2)
|
||||
return (
|
||||
priority,
|
||||
group.fsdp_unit_id
|
||||
if group.fsdp_unit_id is not None
|
||||
else float('inf'),
|
||||
)
|
||||
|
||||
sorted_groups = sorted(temp_groups, key=group_sort_key)
|
||||
|
||||
# For each sorted parameter group, buffer them by execution order
|
||||
for param_group in sorted_groups:
|
||||
cur_params = param_group.params
|
||||
if len(cur_params) == 0:
|
||||
continue
|
||||
for p in cur_params:
|
||||
grouped_params.setdefault(curr_gid, []).append(p)
|
||||
group_is_expert[curr_gid] = param_group.is_expert_param
|
||||
curr_gid += 1
|
||||
|
||||
return grouped_params, group_is_expert
|
||||
|
||||
|
||||
class FSDPCommManager:
|
||||
def __init__(
|
||||
self,
|
||||
buffer_manager,
|
||||
enable_overlap=True,
|
||||
double_buffer_limit=2,
|
||||
):
|
||||
self.buffer_manager = buffer_manager
|
||||
self.enable_overlap = enable_overlap
|
||||
self.grad_reduce_queue = []
|
||||
|
||||
# for double buffer mechanism config
|
||||
self.double_buffer_limit = double_buffer_limit
|
||||
self.buffer_cnt_in_using = 0
|
||||
self._need_zero_grads = True
|
||||
|
||||
def _release_one_buffer_if_needed(self):
|
||||
# Release a buffer with the READY status if needed
|
||||
while self.buffer_cnt_in_using >= self.double_buffer_limit:
|
||||
found = False
|
||||
for group in self.buffer_manager.buffer_groups:
|
||||
if group.params_buffer.status == BufferState.READY:
|
||||
group.params_buffer.status = BufferState.FREED
|
||||
group.params_buffer.clear_tmp_buffer()
|
||||
self.buffer_cnt_in_using -= 1
|
||||
found = True
|
||||
break
|
||||
if not found:
|
||||
break
|
||||
|
||||
def _next_buffer_id(self, gid, is_backward):
|
||||
# Get next buffer id for prefetch
|
||||
if is_backward:
|
||||
next_gid = gid - 1
|
||||
# Search backward for trainable buffer_groups
|
||||
while (
|
||||
next_gid >= 0
|
||||
and not self.buffer_manager.buffer_groups[
|
||||
next_gid
|
||||
].params_buffer.trainable
|
||||
):
|
||||
next_gid -= 1
|
||||
return max(next_gid, 0)
|
||||
else:
|
||||
return min(gid + 1, len(self.buffer_manager.buffer_groups) - 1)
|
||||
|
||||
def all_gather_params(self, params, is_backward=False):
|
||||
if len(params) == 0:
|
||||
return
|
||||
for param in params:
|
||||
if hasattr(param, "is_moe_param"):
|
||||
continue
|
||||
gid = self.buffer_manager.param_to_buffer_id[param.name]
|
||||
group = self.buffer_manager.buffer_groups[gid]
|
||||
group.params_use_cnt += 1
|
||||
params_buffer = group.params_buffer
|
||||
# Use group-specific fsdp_group
|
||||
fsdp_group = group.fsdp_group or self.buffer_manager._fsdp_group
|
||||
|
||||
# Double buffer: reuse buffer if status is READY
|
||||
if params_buffer.status == BufferState.READY:
|
||||
# Reuse: READY -> USING, no need to all_gather again
|
||||
params_buffer.status = BufferState.USING
|
||||
|
||||
# Overlap prefetch comm
|
||||
if self.enable_overlap:
|
||||
next_gid = self._next_buffer_id(gid, is_backward)
|
||||
next_group = self.buffer_manager.buffer_groups[next_gid]
|
||||
next_params_buffer = next_group.params_buffer
|
||||
next_fsdp_group = (
|
||||
next_group.fsdp_group or self.buffer_manager._fsdp_group
|
||||
)
|
||||
if next_params_buffer.status == BufferState.FREED:
|
||||
# Check double_buffer_limit before prefetch
|
||||
self._release_one_buffer_if_needed()
|
||||
next_params_buffer.status = BufferState.SYNCING
|
||||
tmp_buffer_prefetch = next_params_buffer.get_tmp_buffer()
|
||||
next_params_buffer.comm_task = (
|
||||
paddle.distributed.all_gather(
|
||||
tmp_buffer_prefetch,
|
||||
next_params_buffer.data_buffer,
|
||||
group=next_fsdp_group,
|
||||
sync_op=False,
|
||||
)
|
||||
)
|
||||
self.buffer_cnt_in_using += 1
|
||||
|
||||
# Wait for async comm to complete: SYNCING -> USING
|
||||
if params_buffer.status == BufferState.SYNCING:
|
||||
params_buffer.status = BufferState.USING
|
||||
params_buffer.comm_task.wait()
|
||||
params_buffer.comm_task = None
|
||||
|
||||
tmp_buffer = params_buffer.get_tmp_buffer()
|
||||
# Do all_gather in sync: FREED -> USING
|
||||
if params_buffer.status == BufferState.FREED:
|
||||
fsdp_group.process_group.all_gather(
|
||||
params_buffer.data_buffer, tmp_buffer
|
||||
).wait()
|
||||
params_buffer.status = BufferState.USING
|
||||
self.buffer_cnt_in_using += 1
|
||||
|
||||
# Bind the unsharded param to the real param
|
||||
offset = params_buffer.param_offsets[param.name]
|
||||
tmp_param = paddle._C_ops.view_slice(
|
||||
tmp_buffer,
|
||||
offset,
|
||||
offset + param._numel(),
|
||||
)
|
||||
tmp_param.get_tensor()._set_dims(param.shape)
|
||||
tmp_param = _dtensor_from_local(
|
||||
tmp_param,
|
||||
param.process_mesh,
|
||||
param.placements,
|
||||
)
|
||||
param.get_tensor()._share_data_with(tmp_param.get_tensor())
|
||||
|
||||
def shard_params(self, params, is_backward=False):
|
||||
affected_gids = set()
|
||||
for param in params:
|
||||
if hasattr(param, "is_moe_param"):
|
||||
continue
|
||||
gid = self.buffer_manager.param_to_buffer_id.get(param.name)
|
||||
|
||||
group = self.buffer_manager.buffer_groups[gid]
|
||||
stop_gradient = param.stop_gradient
|
||||
local_shape = param._local_shape
|
||||
param._clear_data()
|
||||
param.stop_gradient = stop_gradient
|
||||
param._local_value().get_tensor()._set_dims(local_shape)
|
||||
|
||||
affected_gids.add(gid)
|
||||
|
||||
for gid in affected_gids:
|
||||
group = self.buffer_manager.buffer_groups[gid]
|
||||
if group.params_buffer.status == BufferState.USING:
|
||||
group.params_buffer.status = BufferState.READY
|
||||
|
||||
def reduce_scatter_grads(self, param):
|
||||
if self._need_zero_grads:
|
||||
self._need_zero_grads = False
|
||||
for group in self.buffer_manager.buffer_groups:
|
||||
if group.grads_buffer is not None:
|
||||
group.grads_buffer.data_buffer.zero_()
|
||||
gid = self.buffer_manager.param_to_buffer_id.get(param.name)
|
||||
group = self.buffer_manager.buffer_groups[gid]
|
||||
group.grads_use_cnt += 1
|
||||
fsdp_group = group.fsdp_group or self.buffer_manager._fsdp_group
|
||||
param.main_grad = None
|
||||
|
||||
if group.grads_use_cnt == group.grads_use_sum:
|
||||
group.grads_use_cnt = 0
|
||||
|
||||
# reduce_scatter from tmp_grad_buffer into grads_buffer
|
||||
grads_buffer = group.grads_buffer
|
||||
|
||||
# Grad queue mechanism: wait and release completed reduce_scatter async tasks
|
||||
self._wait_for_grad_comm()
|
||||
|
||||
tmp_buffer = grads_buffer.get_tmp_buffer()
|
||||
shard_size = grads_buffer.data_buffer.shape[0]
|
||||
grad_buffer_shard = tmp_buffer._slice(0, shard_size)
|
||||
if self.enable_overlap:
|
||||
# Comm grads async and check all comm_task before optimizer update
|
||||
grads_buffer.comm_task = paddle.distributed.reduce_scatter(
|
||||
grad_buffer_shard,
|
||||
tmp_buffer,
|
||||
op=paddle.distributed.ReduceOp.SUM,
|
||||
group=fsdp_group,
|
||||
sync_op=False,
|
||||
)
|
||||
|
||||
# Add async task to queue
|
||||
self.grad_reduce_queue.append(grads_buffer)
|
||||
else:
|
||||
paddle.distributed.reduce_scatter(
|
||||
grad_buffer_shard,
|
||||
tmp_buffer,
|
||||
op=paddle.distributed.ReduceOp.SUM,
|
||||
group=fsdp_group,
|
||||
sync_op=False,
|
||||
).wait()
|
||||
grads_buffer.data_buffer.add_(grad_buffer_shard)
|
||||
grads_buffer.clear_tmp_buffer()
|
||||
|
||||
def _wait_for_grad_comm(self, queue_limit=2):
|
||||
# Wait for async reduce_scatter tasks to complete and release resources
|
||||
# queue_limit: max queue size, default use 2, 0 means wait for all
|
||||
while len(self.grad_reduce_queue) > queue_limit:
|
||||
grads_buffer = self.grad_reduce_queue.pop(0)
|
||||
if grads_buffer.comm_task is not None:
|
||||
grads_buffer.comm_task.wait()
|
||||
grads_buffer.comm_task = None
|
||||
tmp_buffer = grads_buffer.get_tmp_buffer()
|
||||
shard_size = grads_buffer.data_buffer.shape[0]
|
||||
grad_buffer_shard = tmp_buffer._slice(0, shard_size)
|
||||
grads_buffer.data_buffer.add_(grad_buffer_shard)
|
||||
grads_buffer.clear_tmp_buffer()
|
||||
|
||||
def _finish_grads_sync(self):
|
||||
# Wait for all async reduce_scatter tasks, call before optimizer.step()
|
||||
self._wait_for_grad_comm(queue_limit=0)
|
||||
|
||||
def _reset_params_buffer_status(self):
|
||||
for group in self.buffer_manager.buffer_groups:
|
||||
params_buffer = group.params_buffer
|
||||
if params_buffer.status in (BufferState.READY, BufferState.USING):
|
||||
# Clear stale tmp_buffer to force re-all_gather with updated data_buffer
|
||||
params_buffer.clear_tmp_buffer()
|
||||
params_buffer.status = BufferState.FREED
|
||||
if self.buffer_cnt_in_using > 0:
|
||||
self.buffer_cnt_in_using -= 1
|
||||
|
||||
|
||||
class FusionBackwardHook(PyLayer):
|
||||
@staticmethod
|
||||
def forward(ctx, *inputs, layer, comm_manager, recursive=False):
|
||||
ctx.layer = layer
|
||||
ctx.comm_manager = comm_manager
|
||||
ctx.recursive = recursive
|
||||
return inputs if len(inputs) > 1 else inputs[0]
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args):
|
||||
trainable_params = []
|
||||
|
||||
for param in ctx.layer.parameters(include_sublayers=ctx.recursive):
|
||||
if param.trainable:
|
||||
trainable_params.append(param)
|
||||
|
||||
ctx.comm_manager.all_gather_params(trainable_params, is_backward=True)
|
||||
return args
|
||||
|
||||
|
||||
class FusionForwardHook(PyLayer):
|
||||
@staticmethod
|
||||
def forward(ctx, *inputs, layer, comm_manager, recursive=False):
|
||||
ctx.layer = layer
|
||||
ctx.comm_manager = comm_manager
|
||||
ctx.recursive = recursive
|
||||
return inputs
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args):
|
||||
ctx.comm_manager.shard_params(
|
||||
ctx.layer.parameters(include_sublayers=ctx.recursive),
|
||||
is_backward=True,
|
||||
)
|
||||
return args
|
||||
|
||||
|
||||
class FullyShardFusion:
|
||||
def __init__(
|
||||
self, model, mesh, fsdp_unit_layers=None, moe_layers_name=None
|
||||
):
|
||||
self.model = model
|
||||
self.mesh = self._check_mesh(mesh)
|
||||
self._shard_all_params()
|
||||
self.buffer_manager = FSDPBufferManager(
|
||||
self.model, self.mesh, fsdp_unit_layers, moe_layers_name
|
||||
)
|
||||
self.comm_manager = FSDPCommManager(self.buffer_manager)
|
||||
self.register_tensor_fusion_hooks(self.model)
|
||||
register_fsdp_context(self)
|
||||
|
||||
def _check_mesh(self, mesh, pp_idx=0):
|
||||
if "pp" in mesh.dim_names:
|
||||
mesh = mesh.get_mesh_with_dim("pp", pp_idx)
|
||||
return mesh
|
||||
|
||||
def _shard_all_params(self):
|
||||
def shard_layer_param(layer):
|
||||
for param_name in list(layer._parameters.keys()):
|
||||
param = getattr(layer, param_name)
|
||||
if param is not None:
|
||||
param_placements = [
|
||||
dist.Replicate() for _ in range(len(self.mesh.shape))
|
||||
]
|
||||
if not param.is_dist():
|
||||
param = dist.shard_tensor(
|
||||
param, self.mesh, param_placements
|
||||
)
|
||||
setattr(layer, param_name, param)
|
||||
|
||||
for name, layer in self.model.named_sublayers(include_self=True):
|
||||
shard_layer_param(layer)
|
||||
|
||||
def comm_sync_and_reset_status(self):
|
||||
self.comm_manager._finish_grads_sync()
|
||||
self.comm_manager._reset_params_buffer_status()
|
||||
self.comm_manager._need_zero_grads = True
|
||||
# Reset main_grad for all trainable parameters
|
||||
for param in self.model.parameters():
|
||||
if param.trainable:
|
||||
param.main_grad = None
|
||||
|
||||
def register_tensor_fusion_hooks(self, model):
|
||||
def _pre_forward_hook(sublayers, recursive=False):
|
||||
comm_manager = self.comm_manager
|
||||
|
||||
@paddle.autograd.no_grad()
|
||||
def all_gather_comm(*_):
|
||||
comm_manager.all_gather_params(
|
||||
sublayers.parameters(include_sublayers=recursive)
|
||||
)
|
||||
|
||||
return all_gather_comm
|
||||
|
||||
def _post_forward_hook(sublayers, recursive=False):
|
||||
comm_manager = self.comm_manager
|
||||
|
||||
@paddle.autograd.no_grad()
|
||||
def shard_comm(*_):
|
||||
comm_manager.shard_params(
|
||||
sublayers.parameters(include_sublayers=recursive)
|
||||
)
|
||||
|
||||
return shard_comm
|
||||
|
||||
def _update_main_grad_hook(param):
|
||||
comm_manager = self.comm_manager
|
||||
|
||||
@paddle.autograd.no_grad()
|
||||
def comm_hook(grad):
|
||||
if grad is not None and grad._is_initialized():
|
||||
# Share mem with grads_tmp_buffer
|
||||
_main_grad = param.get_main_grad()
|
||||
_main_grad.get_tensor()._set_dims(grad._local_shape)
|
||||
param.main_grad = _dtensor_from_local(
|
||||
_main_grad,
|
||||
grad.process_mesh,
|
||||
grad.placements,
|
||||
)
|
||||
param.main_grad._local_value().copy_(grad._local_value())
|
||||
grad._clear_data()
|
||||
comm_manager.shard_params([param], is_backward=True)
|
||||
comm_manager.reduce_scatter_grads(param)
|
||||
|
||||
return comm_hook
|
||||
|
||||
def _post_backward_hook(param):
|
||||
param.main_grad = None
|
||||
if hasattr(param, "get_main_grad"):
|
||||
param._register_grad_hook(_update_main_grad_hook(param))
|
||||
|
||||
for param in model.parameters():
|
||||
if param.trainable:
|
||||
_post_backward_hook(param)
|
||||
|
||||
def _register_recursive(layer):
|
||||
is_unit = (
|
||||
type(layer).__name__ in self.buffer_manager.fsdp_unit_layers
|
||||
)
|
||||
|
||||
if is_unit:
|
||||
# For FSDP Unit, register recursive hooks and stop recursion
|
||||
layer.register_forward_pre_hook(
|
||||
_pre_forward_hook(layer, recursive=True)
|
||||
)
|
||||
layer.register_forward_post_hook(
|
||||
_post_forward_hook(layer, recursive=True)
|
||||
)
|
||||
self._register_fusion_layer_hooks(layer, recursive=True)
|
||||
return
|
||||
|
||||
if layer.parameters(include_sublayers=False):
|
||||
layer.register_forward_pre_hook(
|
||||
_pre_forward_hook(layer, recursive=False)
|
||||
)
|
||||
layer.register_forward_post_hook(
|
||||
_post_forward_hook(layer, recursive=False)
|
||||
)
|
||||
self._register_fusion_layer_hooks(layer, recursive=False)
|
||||
|
||||
for child in layer.children():
|
||||
_register_recursive(child)
|
||||
|
||||
_register_recursive(model)
|
||||
|
||||
def _register_fusion_layer_hooks(self, layer, recursive=False):
|
||||
def _forward_post_hook(layer, inputs, outputs):
|
||||
if isinstance(outputs, dict):
|
||||
for key, value in outputs.items():
|
||||
if (
|
||||
isinstance(value, paddle.Tensor)
|
||||
and not value.stop_gradient
|
||||
):
|
||||
outputs[key] = FusionBackwardHook.apply(
|
||||
value,
|
||||
layer=layer,
|
||||
comm_manager=self.comm_manager,
|
||||
recursive=recursive,
|
||||
)
|
||||
return outputs
|
||||
elif isinstance(outputs, tuple):
|
||||
result = FusionBackwardHook.apply(
|
||||
*outputs,
|
||||
layer=layer,
|
||||
comm_manager=self.comm_manager,
|
||||
recursive=recursive,
|
||||
)
|
||||
if not isinstance(result, tuple):
|
||||
result = (result,)
|
||||
return result
|
||||
else:
|
||||
return FusionBackwardHook.apply(
|
||||
outputs,
|
||||
layer=layer,
|
||||
comm_manager=self.comm_manager,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
def _forward_pre_hook(layer, inputs):
|
||||
return FusionForwardHook.apply(
|
||||
*inputs,
|
||||
layer=layer,
|
||||
comm_manager=self.comm_manager,
|
||||
recursive=recursive,
|
||||
)
|
||||
|
||||
layer.register_forward_post_hook(_forward_post_hook)
|
||||
|
||||
# Register an additional hook for tie_weights shard_params
|
||||
for param in layer.parameters(include_sublayers=False):
|
||||
if param.name == self.comm_manager.buffer_manager.tie_param_name:
|
||||
layer.register_forward_pre_hook(_forward_pre_hook)
|
||||
@@ -0,0 +1,933 @@
|
||||
# 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 logging
|
||||
import math
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.base.framework import in_dygraph_mode
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToDistributedConfig:
|
||||
def __init__(self):
|
||||
self.input_spec = None
|
||||
self.sequence_parallel = False
|
||||
|
||||
|
||||
def cost_model(matched_programs, device_num, node_num):
|
||||
# TODO(jeff41404): multi-node will be supported later
|
||||
assert node_num == 1, (
|
||||
"we only support single node now, multi-node will be supported later"
|
||||
)
|
||||
|
||||
# TODO(jeff41404): will evaluate the best combination of parallel strategies
|
||||
# based on cost_model and return global_mesh, currently using pre-defined parallel strategy
|
||||
if device_num % 2 == 0:
|
||||
if device_num == 8:
|
||||
return dist.ProcessMesh(
|
||||
np.arange(device_num).reshape(2, 2, 2).tolist(),
|
||||
dim_names=["pp", "dp", "mp"],
|
||||
)
|
||||
elif device_num == 6:
|
||||
return dist.ProcessMesh(
|
||||
np.arange(device_num).reshape(3, 2).tolist(),
|
||||
dim_names=["dp", "mp"],
|
||||
)
|
||||
elif device_num == 4:
|
||||
return dist.ProcessMesh(
|
||||
np.arange(device_num).reshape(2, 2).tolist(),
|
||||
dim_names=["dp", "mp"],
|
||||
)
|
||||
elif device_num == 2:
|
||||
return dist.ProcessMesh(list(range(device_num)), dim_names=["dp"])
|
||||
else:
|
||||
raise ValueError(
|
||||
f"device_num must be an even number to be able to use at least 2 parallel strategies, but got: {device_num}"
|
||||
)
|
||||
else:
|
||||
logger.debug(
|
||||
f'device_num must be an even number to be able to use at least 2 parallel strategies, but got: {device_num}, only use data parallel.'
|
||||
)
|
||||
return dist.ProcessMesh(list(range(device_num)), dim_names=["dp"])
|
||||
|
||||
|
||||
def record_program_ops_pre_hook(layer, inputs):
|
||||
"""
|
||||
A pre-hook to mark op numbers before enter layer.forward.
|
||||
"""
|
||||
if not in_dygraph_mode():
|
||||
# Because ir_guard._switch_to_pir() will change default_main_program in python/paddle/__init__.py.
|
||||
# In order to avoid errors, we import default_main_program until this hook running.
|
||||
# After fully switching to pir, can move this import to the beginning of the file.
|
||||
from paddle.base import default_main_program
|
||||
|
||||
if layer._op_recorder.start < 0:
|
||||
layer._op_recorder.start = len(
|
||||
default_main_program().global_block().ops
|
||||
)
|
||||
layer._op_recorder.is_valid = True
|
||||
else:
|
||||
layer._op_recorder.is_valid = False
|
||||
warnings.warn(
|
||||
f"{layer._full_name} has recorded the op information before. Please check whether you call this layer twice."
|
||||
)
|
||||
|
||||
|
||||
def transpose_reshard_embedding_layer_output(layer, inputs, outputs):
|
||||
if hasattr(layer, "current_mesh"):
|
||||
current_mesh = layer.__getattr__("current_mesh")
|
||||
new_output = paddle.transpose(outputs, [1, 0, 2])
|
||||
new_output = dist.reshard(
|
||||
new_output, current_mesh, [dist.Shard(1), dist.Shard(0)]
|
||||
)
|
||||
return new_output
|
||||
|
||||
|
||||
def reshard_transpose_attention_layer_input(layer, inputs):
|
||||
new_inputs = list(inputs)
|
||||
x = new_inputs[0]
|
||||
if hasattr(layer, "current_mesh"):
|
||||
current_mesh = layer.__getattr__("current_mesh")
|
||||
new_x = dist.reshard(x, current_mesh, [dist.Shard(1), dist.Replicate()])
|
||||
new_x = paddle.transpose(new_x, [1, 0, 2])
|
||||
new_inputs[0] = new_x
|
||||
return tuple(new_inputs)
|
||||
|
||||
|
||||
def transpose_reshard_attention_layer_output(layer, inputs, outputs):
|
||||
attn_out = outputs
|
||||
if hasattr(layer, "current_mesh"):
|
||||
current_mesh = layer.__getattr__("current_mesh")
|
||||
new_attn_out = paddle.transpose(attn_out, [1, 0, 2])
|
||||
new_attn_out = dist.reshard(
|
||||
new_attn_out, current_mesh, [dist.Shard(1), dist.Shard(0)]
|
||||
)
|
||||
return new_attn_out
|
||||
|
||||
|
||||
def reshard_mlp_layer_input(layer, inputs):
|
||||
new_inputs = list(inputs)
|
||||
mlp_input = new_inputs[0]
|
||||
if hasattr(layer, "current_mesh"):
|
||||
current_mesh = layer.__getattr__("current_mesh")
|
||||
new_mlp_input = dist.reshard(
|
||||
mlp_input, current_mesh, [dist.Shard(1), dist.Replicate()]
|
||||
)
|
||||
new_inputs[0] = new_mlp_input
|
||||
return tuple(new_inputs)
|
||||
|
||||
|
||||
def reshard_mlp_layer_output(layer, inputs, outputs):
|
||||
mlp_out = outputs
|
||||
if hasattr(layer, "current_mesh"):
|
||||
current_mesh = layer.__getattr__("current_mesh")
|
||||
new_mlp_out = dist.reshard(
|
||||
mlp_out, current_mesh, [dist.Shard(1), dist.Shard(0)]
|
||||
)
|
||||
return new_mlp_out
|
||||
|
||||
|
||||
def reshard_transpose_rms_norm_layer_output(layer, inputs, outputs):
|
||||
if hasattr(layer, "current_mesh"):
|
||||
current_mesh = layer.__getattr__("current_mesh")
|
||||
new_output = dist.reshard(
|
||||
outputs, current_mesh, [dist.Shard(1), dist.Replicate()]
|
||||
)
|
||||
new_output = paddle.transpose(new_output, [1, 0, 2])
|
||||
return new_output
|
||||
|
||||
|
||||
def reshard_all_inputs(layer, inputs):
|
||||
if hasattr(layer, "current_mesh"):
|
||||
current_mesh = layer.__getattr__("current_mesh")
|
||||
if type(inputs) is tuple:
|
||||
new_inputs = []
|
||||
for input in inputs:
|
||||
if paddle.is_tensor(input):
|
||||
if input.is_dist():
|
||||
new_input = dist.reshard(
|
||||
input,
|
||||
current_mesh,
|
||||
input.placements,
|
||||
)
|
||||
else:
|
||||
new_input = dist.shard_tensor(
|
||||
input,
|
||||
current_mesh,
|
||||
[dist.Shard(0), dist.Replicate()],
|
||||
)
|
||||
new_inputs.append(new_input)
|
||||
else:
|
||||
new_inputs.append(input)
|
||||
return tuple(new_inputs)
|
||||
else:
|
||||
if input.is_dist():
|
||||
new_input = dist.reshard(
|
||||
input, current_mesh, [dist.Shard(0), dist.Replicate()]
|
||||
)
|
||||
else:
|
||||
new_input = dist.shard_tensor(
|
||||
input, current_mesh, [dist.Shard(0), dist.Replicate()]
|
||||
)
|
||||
return new_input
|
||||
|
||||
|
||||
def reshard_all_outputs(layer, inputs, outputs):
|
||||
if hasattr(layer, "next_mesh"):
|
||||
next_mesh = layer.__getattr__("next_mesh")
|
||||
if type(outputs) is tuple:
|
||||
new_outputs = []
|
||||
for output in outputs:
|
||||
if paddle.is_tensor(output):
|
||||
new_output = dist.reshard(
|
||||
output, next_mesh, [dist.Shard(0), dist.Replicate()]
|
||||
)
|
||||
new_outputs.append(new_output)
|
||||
else:
|
||||
new_outputs.append(output)
|
||||
return new_outputs
|
||||
else:
|
||||
new_output = dist.reshard(
|
||||
outputs, next_mesh, [dist.Shard(0), dist.Replicate()]
|
||||
)
|
||||
return new_output
|
||||
|
||||
|
||||
def record_program_ops_post_hook(layer, inputs, outputs):
|
||||
"""
|
||||
A post-hook to mark op numbers after enter layer.forward, and record corresponding ops of the layer.
|
||||
"""
|
||||
if not in_dygraph_mode():
|
||||
# Because ir_guard._switch_to_pir() will change default_main_program in python/paddle/__init__.py.
|
||||
# In order to avoid errors, we import default_main_program until this hook running.
|
||||
# After fully switching to pir, can move this import to the beginning of the file.
|
||||
from paddle.base import default_main_program
|
||||
|
||||
assert (
|
||||
layer._op_recorder.start >= 0
|
||||
and layer._op_recorder.is_valid is True
|
||||
), (
|
||||
f"{layer._full_name} has not recorded the start of the corresponding ops before"
|
||||
)
|
||||
end = len(default_main_program().global_block().ops)
|
||||
# some layers, such as rotary_embedding, will not add new ops to program
|
||||
# assert end > layer._op_recorder.start, f"{layer._full_name} has not added new ops to the program"
|
||||
ops = []
|
||||
if end > layer._op_recorder.start:
|
||||
layer._op_recorder.end = end
|
||||
ops = (
|
||||
default_main_program()
|
||||
.global_block()
|
||||
.ops[layer._op_recorder.start : layer._op_recorder.end]
|
||||
)
|
||||
logger.debug(
|
||||
f'start: {layer._op_recorder.start}, end: {layer._op_recorder.end}, ops: {ops}'
|
||||
)
|
||||
layer._op_recorder.ops = ops
|
||||
|
||||
|
||||
def get_layer_pp_info(mesh, num_hidden_layers, layer_index):
|
||||
if "pp" in mesh.dim_names:
|
||||
pp_degree = mesh.get_dim_size("pp")
|
||||
layer_per_stage = math.ceil(num_hidden_layers / pp_degree)
|
||||
return layer_index // layer_per_stage
|
||||
else:
|
||||
# return None, False
|
||||
return None
|
||||
|
||||
|
||||
def to_distributed(
|
||||
model: paddle.nn.Layer,
|
||||
optimizer: paddle.optimizer.Optimizer,
|
||||
dataloader: paddle.io.DataLoader,
|
||||
device_num: int,
|
||||
node_num: int | None = 1,
|
||||
config: ToDistributedConfig | None = None,
|
||||
) -> tuple[
|
||||
paddle.nn.Layer,
|
||||
paddle.optimizer.Optimizer,
|
||||
paddle.distributed.auto_parallel.ShardDataloader,
|
||||
]:
|
||||
"""
|
||||
`to_distributed` can automatically convert neural networks, optimizer, and dataloader
|
||||
that do not contain any distributed code into neural networks, optimizers, and dataloader
|
||||
that are suitable for distributed training and ensure their correctness.
|
||||
At the same time, during the transformation process, the optimal distributed strategy
|
||||
will be automatically selected based on `node_num` and `device_num` to maximize performance.
|
||||
|
||||
Args:
|
||||
model(paddle.nn.Layer): The model in dygraph mode, whose parameters
|
||||
are ordinary tensors, do not contain any distributed code.
|
||||
If one device has sufficient memory, it can train directly.
|
||||
optimizer(paddle.optimizer.Optimizer): The optimizer for training.
|
||||
one instance of a regular optimizer, e.g. `paddle.optimizer.Adam` etc.
|
||||
dataloader(paddle.io.DataLoader): The dataloader used in dygraph mode,
|
||||
It is instantiated through regular `paddle.io.Dataset` and `paddle.io.Sampler`,
|
||||
not `paddle.io.DistributedBatchSampler`.
|
||||
device_num(int): the number of devices on each node or machine.
|
||||
node_num(int|None, optional): the number of nodes or machines.
|
||||
config(ToDistributedConfig| None = None): Configs for input_spec and sequence_parallel.
|
||||
The custom input specs specify the most likely shape, dtype, and name information
|
||||
of each model inputs. If it is not None, the input specs and
|
||||
will be inferred from the custom input specs. If it is None, will use default with
|
||||
shape of [BATCH_SIZE=4, SEQ_LENGTH=1024], The custom
|
||||
input specs should be a list of `paddle.static.InputSpec`. Default: None.
|
||||
sequence_parallel indicates whether to use sequence parallel. Default: False.
|
||||
|
||||
Returns:
|
||||
model. The model in dygraph mode but contain distributed attributes.
|
||||
|
||||
optimizer. The optimizer for training and may be sharded states.
|
||||
|
||||
dataloader. The dataloader can be used in distributed training.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +SKIP('run in distributed env')
|
||||
>>> import math
|
||||
>>> import numpy as np
|
||||
>>> import paddle
|
||||
>>> import paddle.nn.functional as F
|
||||
>>> from paddle import nn
|
||||
>>> from paddle.distributed import to_distributed
|
||||
>>> from paddle.distributed.auto_parallel.high_level_api import ToDistributedConfig
|
||||
|
||||
>>> EPOCHS = 1
|
||||
>>> VOCAB_SIZE = 8000
|
||||
>>> BATCH_NUM = 2
|
||||
>>> BATCH_SIZE = 4
|
||||
>>> HIDDEN_SIZE = 2048
|
||||
>>> INTERMEDIATE_SIZE = 4096
|
||||
>>> SEQ_LENGTH = 1024
|
||||
>>> N_HEAD = 32
|
||||
>>> NUM_HIDDEN_LAYERS = 4
|
||||
>>> class RandomDataset(paddle.io.Dataset): # type: ignore[type-arg]
|
||||
... def __init__(self, inputs, labels, num_samples):
|
||||
... self.inputs = inputs
|
||||
... self.labels = labels
|
||||
... self.num_samples = num_samples
|
||||
...
|
||||
... def __getitem__(self, idx):
|
||||
... return self.inputs[idx], self.labels[idx]
|
||||
...
|
||||
... def __len__(self):
|
||||
... return self.num_samples
|
||||
|
||||
>>> class RotaryEmbedding(nn.Layer):
|
||||
... def __init__(self, dim, max_position_embeddings=2048, base=10000):
|
||||
... super().__init__()
|
||||
... self.dim = dim
|
||||
... self.max_position_embeddings = max_position_embeddings
|
||||
... self.base = base
|
||||
... self.inv_freq = 1.0 / (self.base ** (paddle.cast(paddle.arange(0, self.dim, 2), dtype="float32") / self.dim))
|
||||
... self._set_cos_sin_cache(seq_len=max_position_embeddings)
|
||||
|
||||
... def _set_cos_sin_cache(self, seq_len):
|
||||
... self.max_seq_len_cached = seq_len
|
||||
... t = paddle.arange(seq_len, dtype="float32")
|
||||
... freqs = paddle.einsum("i,j->ij", t, self.inv_freq)
|
||||
... emb = paddle.concat([freqs, freqs], axis=-1)
|
||||
... self.cos_cached = emb.cos()[None, :, None, :]
|
||||
... self.sin_cached = emb.sin()[None, :, None, :]
|
||||
|
||||
... def forward(self, x, seq_len=None):
|
||||
... cos = self.cos_cached[:, :seq_len, :, :]
|
||||
... sin = self.sin_cached[:, :seq_len, :, :]
|
||||
... return (
|
||||
... cos.cast(x.dtype) if cos.dtype != x.dtype else cos,
|
||||
... sin.cast(x.dtype) if sin.dtype != x.dtype else sin,
|
||||
... )
|
||||
|
||||
>>> def rotate_half(x):
|
||||
... x1 = x[..., : x.shape[-1] // 2]
|
||||
... x2 = x[..., x.shape[-1] // 2 :]
|
||||
... return paddle.concat([-x2, x1], axis=-1)
|
||||
|
||||
>>> def apply_rotary_pos_emb(q, k, cos, sin, position_ids):
|
||||
... if position_ids is None:
|
||||
... cos = cos[:, : q.shape[1], :, :]
|
||||
... sin = sin[:, : q.shape[1], :, :]
|
||||
... else:
|
||||
... cos = cos.squeeze(axis=[0, 2])
|
||||
... sin = sin.squeeze(axis=[0, 2])
|
||||
... cos = cos[position_ids].unsqueeze(2)
|
||||
... sin = sin[position_ids].unsqueeze(2)
|
||||
... q_embed = (q * cos) + (rotate_half(q) * sin)
|
||||
... k_embed = (k * cos) + (rotate_half(k) * sin)
|
||||
... return q_embed, k_embed
|
||||
|
||||
>>> def scaled_dot_product_attention(
|
||||
... query_states,
|
||||
... key_states,
|
||||
... value_states,
|
||||
... attention_mask,
|
||||
... ):
|
||||
... bsz, q_len, num_heads, head_dim = query_states.shape
|
||||
... _, kv_seq_len, _, _ = value_states.shape
|
||||
... query_states = paddle.transpose(query_states, [0, 2, 1, 3])
|
||||
... key_states = paddle.transpose(key_states, [0, 2, 1, 3])
|
||||
... value_states = paddle.transpose(value_states, [0, 2, 1, 3])
|
||||
... attn_weights = paddle.matmul(query_states / math.sqrt(head_dim), key_states.transpose([0, 1, 3, 2]))
|
||||
... attention_mask = attention_mask.reshape([bsz, 1, q_len, kv_seq_len])
|
||||
... attn_weights = attn_weights + attention_mask
|
||||
... if not paddle.in_dynamic_mode():
|
||||
... attn_weights = F.softmax(attn_weights, axis=-1, dtype="float32").astype(query_states.dtype)
|
||||
... else:
|
||||
... with paddle.amp.auto_cast(False):
|
||||
... attn_weights = F.softmax(attn_weights, axis=-1, dtype="float32").astype(query_states.dtype)
|
||||
... attn_output = paddle.matmul(attn_weights, value_states)
|
||||
... attn_output = attn_output.transpose([0, 2, 1, 3])
|
||||
... attn_output = attn_output.reshape([bsz, q_len, head_dim * num_heads])
|
||||
... return attn_output
|
||||
|
||||
>>> class Attention(nn.Layer):
|
||||
... def __init__(self, hidden_size=HIDDEN_SIZE, n_head=N_HEAD):
|
||||
... super().__init__()
|
||||
... self.hidden_size = hidden_size
|
||||
... self.num_heads = n_head
|
||||
... self.head_dim = hidden_size // n_head
|
||||
... self.q_proj = nn.Linear(hidden_size, hidden_size, bias_attr=False)
|
||||
... self.k_proj = nn.Linear(hidden_size, hidden_size, bias_attr=False)
|
||||
... self.v_proj = nn.Linear(hidden_size, hidden_size, bias_attr=False)
|
||||
... self.o_proj = nn.Linear(hidden_size, hidden_size, bias_attr=False)
|
||||
... self.rotary_emb = RotaryEmbedding(self.head_dim, max_position_embeddings=SEQ_LENGTH, base=10000)
|
||||
|
||||
... def forward(
|
||||
... self,
|
||||
... hidden_states,
|
||||
... position_ids=None,
|
||||
... attention_mask=None,
|
||||
... ):
|
||||
... query_states = self.q_proj(hidden_states)
|
||||
... key_states = self.k_proj(hidden_states)
|
||||
... value_states = self.v_proj(hidden_states)
|
||||
... target_query_shape = [0, 0, self.num_heads, self.head_dim]
|
||||
... target_key_value_shape = [0, 0, self.num_heads, self.head_dim]
|
||||
... query_states = query_states.reshape(shape=target_query_shape)
|
||||
... key_states = key_states.reshape(shape=target_key_value_shape)
|
||||
... value_states = value_states.reshape(shape=target_key_value_shape)
|
||||
... kv_seq_len = key_states.shape[-3]
|
||||
... cos, sin = self.rotary_emb(value_states, seq_len=kv_seq_len)
|
||||
... query_states, key_states = apply_rotary_pos_emb(
|
||||
... query_states, key_states, cos, sin, position_ids
|
||||
... )
|
||||
... output = scaled_dot_product_attention(
|
||||
... query_states,
|
||||
... key_states,
|
||||
... value_states,
|
||||
... attention_mask,
|
||||
... )
|
||||
... attn_output = output
|
||||
... attn_output = self.o_proj(attn_output)
|
||||
... return attn_output
|
||||
|
||||
>>> class Mlp(nn.Layer):
|
||||
... def __init__(
|
||||
... self,
|
||||
... hidden_size=HIDDEN_SIZE,
|
||||
... intermediate_size=INTERMEDIATE_SIZE,
|
||||
... ):
|
||||
... super().__init__()
|
||||
... self.hidden_size = hidden_size
|
||||
... self.intermediate_size = intermediate_size
|
||||
... self.gate_proj = nn.Linear(hidden_size, intermediate_size, bias_attr=False)
|
||||
... self.up_proj = nn.Linear(hidden_size, intermediate_size, bias_attr=False)
|
||||
... self.down_proj = nn.Linear(intermediate_size, hidden_size, bias_attr=False)
|
||||
|
||||
... def forward(self, x):
|
||||
... x = paddle.nn.functional.swiglu(
|
||||
... self.gate_proj(x), self.up_proj(x)
|
||||
... )
|
||||
... out = self.down_proj(x)
|
||||
... return out
|
||||
|
||||
>>> class RMSNorm(nn.Layer):
|
||||
... def __init__(self, hidden_size=HIDDEN_SIZE):
|
||||
... super().__init__()
|
||||
... self.hidden_size = hidden_size
|
||||
... self.weight = paddle.create_parameter(
|
||||
... shape=[self.hidden_size],
|
||||
... dtype=paddle.get_default_dtype(),
|
||||
... default_initializer=nn.initializer.Constant(1.0),
|
||||
... )
|
||||
... self.variance_epsilon = 1.0
|
||||
|
||||
... def forward(self, hidden_states):
|
||||
... with paddle.amp.auto_cast(False):
|
||||
... hidden_states = hidden_states.astype("float32")
|
||||
... variance = hidden_states.pow(2).mean(-1, keepdim=True)
|
||||
... hidden_states = (
|
||||
... paddle.rsqrt(variance + self.variance_epsilon) * hidden_states
|
||||
... )
|
||||
... if self.weight.dtype in [paddle.float16, paddle.bfloat16]:
|
||||
... hidden_states = paddle.cast(hidden_states, self.weight.dtype)
|
||||
... return hidden_states * self.weight
|
||||
|
||||
>>> class DecoderLayer(nn.Layer):
|
||||
... def __init__(
|
||||
... self,
|
||||
... hidden_size=HIDDEN_SIZE,
|
||||
... intermediate_size=INTERMEDIATE_SIZE,
|
||||
... ):
|
||||
... super().__init__()
|
||||
... self.hidden_size = hidden_size
|
||||
... self.intermediate_size = intermediate_size
|
||||
... self.self_attn = Attention(hidden_size)
|
||||
... self.mlp = Mlp()
|
||||
... self.input_layernorm = RMSNorm(hidden_size)
|
||||
... self.post_attn_layernorm = RMSNorm(hidden_size)
|
||||
|
||||
... def forward(
|
||||
... self,
|
||||
... hidden_states,
|
||||
... position_ids=None,
|
||||
... attention_mask=None,
|
||||
... ):
|
||||
... residual = hidden_states
|
||||
... hidden_states = self.input_layernorm(hidden_states)
|
||||
... hidden_states = self.self_attn(
|
||||
... hidden_states, position_ids, attention_mask
|
||||
... )
|
||||
... hidden_states = residual + hidden_states
|
||||
... residual = hidden_states
|
||||
... hidden_states = self.post_attn_layernorm(hidden_states)
|
||||
... hidden_states = self.mlp(hidden_states)
|
||||
... hidden_states = residual + hidden_states
|
||||
... return hidden_states
|
||||
|
||||
>>> def _prepare_decoder_attention_mask(attention_mask, input_shape, dtype):
|
||||
... batch_size, src_length = attention_mask.shape[0], attention_mask.shape[-1]
|
||||
... batch_size, target_length = input_shape
|
||||
... attention_mask = attention_mask[:, None, None, :].astype("bool")
|
||||
... attention_mask.stop_gradient = True
|
||||
... expanded_attn_mask = attention_mask.expand([batch_size, 1, target_length, src_length])
|
||||
... mask = paddle.tril(paddle.ones((target_length, target_length), dtype="bool"))
|
||||
... combined_attention_mask = mask[None, None, :, :].expand([batch_size, 1, target_length, target_length])
|
||||
... expanded_attn_mask = expanded_attn_mask & combined_attention_mask
|
||||
... expanded_attn_mask = paddle.where(expanded_attn_mask, 0.0, paddle.finfo(dtype).min).astype(dtype)
|
||||
... return expanded_attn_mask
|
||||
|
||||
>>> class Model(nn.Layer):
|
||||
... def __init__(
|
||||
... self,
|
||||
... vocab_size=VOCAB_SIZE,
|
||||
... hidden_size=HIDDEN_SIZE,
|
||||
... intermediate_size=INTERMEDIATE_SIZE,
|
||||
... ):
|
||||
... super().__init__()
|
||||
... self.vocab_size = vocab_size
|
||||
... self.hidden_size = hidden_size
|
||||
... self.intermediate_size = intermediate_size
|
||||
... self.embed_tokens = nn.Embedding(
|
||||
... vocab_size,
|
||||
... hidden_size,
|
||||
... )
|
||||
... self.layers = nn.LayerList([DecoderLayer() for i in range(NUM_HIDDEN_LAYERS)])
|
||||
... self.norm = RMSNorm(hidden_size)
|
||||
... self.weight = self.create_parameter(
|
||||
... shape=[hidden_size, vocab_size],
|
||||
... dtype=paddle.get_default_dtype(),
|
||||
... )
|
||||
... self.ignore_index = -100
|
||||
... self.loss_func = paddle.nn.CrossEntropyLoss(reduction="none", ignore_index=self.ignore_index)
|
||||
|
||||
... def forward(
|
||||
... self,
|
||||
... input_ids=None,
|
||||
... position_ids=None,
|
||||
... attention_mask=None,
|
||||
... labels=None,
|
||||
... ):
|
||||
... batch_size, seq_length = input_ids.shape
|
||||
... inputs_embeds = self.embed_tokens(input_ids)
|
||||
... attention_mask = paddle.ones(
|
||||
... (batch_size, seq_length), dtype=paddle.bool
|
||||
... )
|
||||
... if position_ids is None:
|
||||
... position_ids = paddle.arange(seq_length, dtype="int64").expand(
|
||||
... (batch_size, seq_length)
|
||||
... )
|
||||
... attention_mask = _prepare_decoder_attention_mask(
|
||||
... attention_mask,
|
||||
... (batch_size, seq_length),
|
||||
... inputs_embeds.dtype,
|
||||
... )
|
||||
... hidden_states = inputs_embeds
|
||||
... for idx, (decoder_layer) in enumerate(self.layers):
|
||||
... layer_outputs = decoder_layer(
|
||||
... hidden_states,
|
||||
... position_ids,
|
||||
... attention_mask,
|
||||
... )
|
||||
... hidden_states = layer_outputs
|
||||
... hidden_states = self.norm(hidden_states)
|
||||
... logits = paddle.matmul(hidden_states, self.weight)
|
||||
... loss = None
|
||||
... if labels is not None:
|
||||
... masked_lm_loss = self.loss_func(
|
||||
... logits.astype("float32"),
|
||||
... labels.unsqueeze(2),
|
||||
... )
|
||||
... binary_sequence = paddle.where(
|
||||
... masked_lm_loss > 0,
|
||||
... paddle.ones_like(masked_lm_loss),
|
||||
... paddle.zeros_like(masked_lm_loss),
|
||||
... )
|
||||
... count = paddle.sum(binary_sequence)
|
||||
... if count == 0:
|
||||
... loss = paddle.sum(masked_lm_loss * binary_sequence)
|
||||
... else:
|
||||
... loss = paddle.sum(masked_lm_loss * binary_sequence) / count
|
||||
... return (loss, logits)
|
||||
|
||||
>>> model = Model() # There is no distributed code or markup in Model
|
||||
>>> input_seqs = np.random.randint(low=0, high=1024, size=(BATCH_SIZE * BATCH_NUM, SEQ_LENGTH)).astype("int64")
|
||||
>>> labels = np.random.randint(low=0, high=1024, size=(BATCH_SIZE * BATCH_NUM, SEQ_LENGTH)).astype("int64")
|
||||
>>> dataset = RandomDataset(input_seqs, labels, BATCH_SIZE * BATCH_NUM)
|
||||
>>> sampler = paddle.io.BatchSampler(
|
||||
... dataset,
|
||||
... batch_size=BATCH_SIZE,
|
||||
... shuffle=False,
|
||||
... drop_last=True,
|
||||
... )
|
||||
>>> loader = paddle.io.DataLoader(dataset, batch_sampler=sampler)
|
||||
>>> opt = paddle.optimizer.SGD(learning_rate=0.1, parameters=model.parameters())
|
||||
>>> input_seq_spec = paddle.static.InputSpec([BATCH_SIZE, SEQ_LENGTH], 'float32', 'input_seq', True)
|
||||
>>> dist_config = ToDistributedConfig()
|
||||
>>> dist_config.sequence_parallel = True
|
||||
|
||||
>>> # wrap model, opt, dataloader by using **to_distributed**
|
||||
>>> dist_model, dist_opt, dist_loader = to_distributed(
|
||||
... model,
|
||||
... opt,
|
||||
... loader,
|
||||
... device_num=8,
|
||||
... node_num=1,
|
||||
... config=dist_config,
|
||||
... )
|
||||
|
||||
>>> for epoch in range(EPOCHS):
|
||||
... dist_model.train()
|
||||
... for i, data in enumerate(dist_loader()):
|
||||
... inputs, labels = data
|
||||
... loss, _ = dist_model(inputs, labels=labels)
|
||||
... print(f"epoch {epoch}, step {i}: loss {loss}")
|
||||
... loss.backward()
|
||||
... dist_opt.step()
|
||||
... dist_opt.clear_grad()
|
||||
>>> # This case need to be executed in multi-card environment
|
||||
>>> # python -m paddle.distributed.launch --gpus=0,1,2,3,4,5,6,7 {test_case}.py
|
||||
"""
|
||||
# Because some API(`paddle.randn` etc.) will be used when building pattern,
|
||||
# In order to avoid circle import, we import get_pattern until function running.
|
||||
from .static.tuner.to_distributed_api_patterns import (
|
||||
clear_used_patterns,
|
||||
get_pattern,
|
||||
match_all_patterns,
|
||||
register_used_patterns,
|
||||
)
|
||||
|
||||
logger.debug(f'input model: {model}')
|
||||
# paddle.distributed.init_parallel_env()
|
||||
|
||||
# step 1: identifying network structure and pattern recogincation
|
||||
# step 1.1: register pre-hooks and post-hooks, thus recording corresponding static ops in following paddle.jit.to_static
|
||||
for layer in model.sublayers():
|
||||
pre_hook_helper = layer.register_forward_pre_hook(
|
||||
record_program_ops_pre_hook
|
||||
)
|
||||
post_hook_helper = layer.register_forward_post_hook(
|
||||
record_program_ops_post_hook
|
||||
)
|
||||
layer._op_recorder.hooks.append(pre_hook_helper)
|
||||
layer._op_recorder.hooks.append(post_hook_helper)
|
||||
|
||||
# step 1.2: call @to_static, get program, and corresponding static ops of each layer
|
||||
custom_input_spec = (
|
||||
config.input_spec
|
||||
if config.input_spec
|
||||
else [paddle.static.InputSpec([4, 1024], 'float32', 'input_seq', True)]
|
||||
)
|
||||
static_func = paddle.jit.to_static(
|
||||
model.forward, input_spec=custom_input_spec, full_graph=True
|
||||
)
|
||||
program = static_func.concrete_program.main_program
|
||||
# currently, paddle.jit.to_static has side effects that will affect model.
|
||||
# After fixing it, one line of code below can be dropped
|
||||
static_func.rollback()
|
||||
logger.debug(
|
||||
f'Converted model to pir program: {program}, for pattern matching'
|
||||
)
|
||||
|
||||
# step 1.3: get the mapping [dynamic-layers : static ops]
|
||||
op_to_id = {}
|
||||
for idx, op in enumerate(program.global_block().ops):
|
||||
op_to_id[op] = idx
|
||||
|
||||
ops_id_to_layer = {}
|
||||
op_id_to_layer = {}
|
||||
for layer in model.sublayers():
|
||||
layer_ops = layer._op_recorder.ops
|
||||
logger.debug(
|
||||
f'layer name: {layer.__class__.__name__}, layer_ops: {layer_ops}'
|
||||
)
|
||||
ops_id = []
|
||||
for op in layer_ops:
|
||||
assert op in op_to_id.keys(), f"{op.name} is not in program"
|
||||
op_id = op_to_id[op]
|
||||
op_id_to_layer[op_id] = layer
|
||||
ops_id.append(op_id)
|
||||
ops_id_to_layer[tuple(ops_id)] = layer
|
||||
logger.debug(f'ops_id_to_layer is: {ops_id_to_layer}')
|
||||
|
||||
# step 1.4: pattern recogincation
|
||||
DECODER_LAYER_NAME = 'decoder_layer'
|
||||
register_used_patterns(DECODER_LAYER_NAME)
|
||||
results = match_all_patterns(program)
|
||||
logger.debug(f'Matched decoder layer patterns are: {results}')
|
||||
|
||||
matched_programs = {}
|
||||
for pattern_name, matched_patterns in results.items():
|
||||
# process one pattern
|
||||
pattern_ops_dist_infos = get_pattern(pattern_name).ops_dist_infos
|
||||
assert pattern_ops_dist_infos is not None, (
|
||||
f"{pattern_name} does not contain ops_dist_infos, cannot reshard, please check"
|
||||
)
|
||||
processed_patterns = []
|
||||
for matched_pattern in matched_patterns:
|
||||
# convert pattern_ops_dist_infos to program_ops_dist_infos
|
||||
program_ops_dist_infos = {}
|
||||
for pattern_ops_id, op_dist_info in pattern_ops_dist_infos.items():
|
||||
program_ops_id = []
|
||||
for pattern_op_id in pattern_ops_id:
|
||||
assert pattern_op_id in matched_pattern.keys(), (
|
||||
f"please check ops_dist_infos of {pattern_name}, {pattern_op_id} not in matched_pattern: {matched_pattern.keys()}"
|
||||
)
|
||||
program_op_id = matched_pattern[pattern_op_id]
|
||||
program_ops_id.append(program_op_id)
|
||||
program_ops_dist_infos[tuple(program_ops_id)] = op_dist_info
|
||||
processed_patterns.append(program_ops_dist_infos)
|
||||
matched_programs[pattern_name] = processed_patterns
|
||||
logger.debug(f'Matched decoder layer patterns are: {matched_programs}')
|
||||
|
||||
# step 2: calculate the optimal parallel strategies based on the network structure
|
||||
mesh = cost_model(matched_programs, device_num, node_num)
|
||||
logger.debug(f'mesh: {mesh}')
|
||||
|
||||
with_pp = True if "pp" in mesh.dim_names else False
|
||||
with_mp = True if "mp" in mesh.dim_names else False
|
||||
with_dp = True if "dp" in mesh.dim_names else False
|
||||
with_sp = (
|
||||
True if "mp" in mesh.dim_names and config.sequence_parallel else False
|
||||
)
|
||||
|
||||
# step 3: processing tensor parallel if necessary, according to the optimal parallel strategies shard weight tensors in decoder blocks
|
||||
if with_mp:
|
||||
num_hidden_layers = len(matched_programs[DECODER_LAYER_NAME])
|
||||
for pattern_name, processed_patterns in matched_programs.items():
|
||||
assert len(processed_patterns) == num_hidden_layers, (
|
||||
"transformer patterns matched are incomplete"
|
||||
)
|
||||
for idx, processed_pattern in enumerate(processed_patterns):
|
||||
local_mesh = mesh
|
||||
if with_pp:
|
||||
pp_stage_id = get_layer_pp_info(
|
||||
mesh, num_hidden_layers, idx
|
||||
)
|
||||
local_mesh = mesh.get_mesh_with_dim("pp", pp_stage_id)
|
||||
|
||||
for program_ops_id, dist_infos in processed_pattern.items():
|
||||
assert program_ops_id in ops_id_to_layer.keys(), (
|
||||
f"program_ops: {program_ops_id} is not corresponding to a dynamic layer"
|
||||
)
|
||||
dynamic_layer = ops_id_to_layer[program_ops_id]
|
||||
mesh_num_dims = len(local_mesh.shape)
|
||||
sharding_info = dist_infos.get_dist_info(mesh_num_dims)
|
||||
dynamic_layer.weight = dist.shard_tensor(
|
||||
dynamic_layer.weight, local_mesh, sharding_info[0]
|
||||
)
|
||||
if dynamic_layer.bias is not None:
|
||||
dynamic_layer.bias = dist.shard_tensor(
|
||||
dynamic_layer.bias, local_mesh, sharding_info[1]
|
||||
)
|
||||
logger.debug(f'after tensor parallel, model: {model}')
|
||||
|
||||
# step 4: processing pipeline parallel if necessary, reshard inputs of decoder blocks to next pp mesh b when switching from pp stage a to pp stage b
|
||||
if with_pp:
|
||||
decoder_layers = []
|
||||
for pattern_name, matched_all_patterns in results.items():
|
||||
if pattern_name == DECODER_LAYER_NAME:
|
||||
for matched_pattern in matched_all_patterns:
|
||||
program_ops_id = []
|
||||
for a, b in matched_pattern.items():
|
||||
program_ops_id.append(b)
|
||||
if tuple(sorted(program_ops_id)) in ops_id_to_layer.keys():
|
||||
decoder_layers.append(
|
||||
ops_id_to_layer[tuple(sorted(program_ops_id))]
|
||||
)
|
||||
|
||||
if decoder_layers is not None:
|
||||
num_decoder_blocks = len(decoder_layers)
|
||||
assert num_decoder_blocks == num_hidden_layers, (
|
||||
f"decoder pattern layers matched are incomplete, num_decoder_blocks: {num_decoder_blocks} should be equal to num_hidden_layers: {num_hidden_layers}"
|
||||
)
|
||||
|
||||
pp_degree = mesh.get_dim_size("pp")
|
||||
num_blocks_per_stage = num_decoder_blocks // pp_degree
|
||||
for i in range(num_decoder_blocks):
|
||||
pp_stage_id = get_layer_pp_info(mesh, num_decoder_blocks, i)
|
||||
current_mesh = mesh.get_mesh_with_dim("pp", pp_stage_id)
|
||||
decoder_layer = decoder_layers[i]
|
||||
decoder_layer.__setattr__("current_mesh", current_mesh)
|
||||
pre_hook_helper = decoder_layer.register_forward_pre_hook(
|
||||
reshard_all_inputs
|
||||
)
|
||||
logger.debug(f'after pipeline parallel, model: {model}')
|
||||
|
||||
# step 5: processing sequence parallel if necessary, reshard or transpose sequence dims for inputs of attention/mlp inputs
|
||||
if with_sp:
|
||||
clear_used_patterns()
|
||||
EMBEDDING_LAYER_NAME = "embedding"
|
||||
ATTENTION_LAYER_NAME = "attention"
|
||||
MLP_LAYER_NAME = "mlp_3_with_swiglu"
|
||||
RMS_NORM_LAYER_NAME = "rmsnorm"
|
||||
used_patterns = [
|
||||
EMBEDDING_LAYER_NAME,
|
||||
ATTENTION_LAYER_NAME,
|
||||
MLP_LAYER_NAME,
|
||||
RMS_NORM_LAYER_NAME,
|
||||
]
|
||||
register_used_patterns(used_patterns)
|
||||
results = match_all_patterns(program)
|
||||
|
||||
matched_layers = {}
|
||||
for pattern_name, matched_all_patterns in results.items():
|
||||
if pattern_name in used_patterns:
|
||||
for matched_pattern in matched_all_patterns:
|
||||
program_ops_id = []
|
||||
for a, b in matched_pattern.items():
|
||||
program_ops_id.append(b)
|
||||
if tuple(sorted(program_ops_id)) in ops_id_to_layer.keys():
|
||||
if pattern_name in matched_layers.keys():
|
||||
matched_layers[pattern_name].append(
|
||||
ops_id_to_layer[tuple(sorted(program_ops_id))]
|
||||
)
|
||||
else:
|
||||
matched_layers[pattern_name] = [
|
||||
ops_id_to_layer[tuple(sorted(program_ops_id))]
|
||||
]
|
||||
|
||||
logger.debug(f'Matched attention/mlp layers are: {matched_layers}')
|
||||
# init mesh
|
||||
GLOBAL_MESH = []
|
||||
if with_pp:
|
||||
pp_degree = mesh.get_dim_size("pp")
|
||||
for i in range(pp_degree):
|
||||
local_mesh = mesh.get_mesh_with_dim("pp", i)
|
||||
GLOBAL_MESH.append(local_mesh)
|
||||
else:
|
||||
GLOBAL_MESH.append(mesh)
|
||||
|
||||
# embedding: from [b/dp_degree, s, h] reshard+transpose to [s/mp_degree, b/dp_degree, h]
|
||||
embedding_layer = matched_layers[EMBEDDING_LAYER_NAME][0]
|
||||
embedding_layer_mesh = GLOBAL_MESH[0]
|
||||
embedding_layer.__setattr__("current_mesh", embedding_layer_mesh)
|
||||
post_hook_helper = embedding_layer.register_forward_post_hook(
|
||||
transpose_reshard_embedding_layer_output
|
||||
)
|
||||
|
||||
# attention: input from [s/mp_degree, b/dp_degree, h] to [b/dp_degree, s, h], output from [b/dp_degree, s, h] to [s/mp_degree, b/dp_degree, h]
|
||||
attention_layers = matched_layers[ATTENTION_LAYER_NAME]
|
||||
num_attention_layers = len(attention_layers)
|
||||
if attention_layers is not None:
|
||||
for i in range(num_attention_layers):
|
||||
current_mesh = GLOBAL_MESH[0]
|
||||
if with_pp:
|
||||
pp_stage_id = get_layer_pp_info(
|
||||
mesh, num_attention_layers, i
|
||||
)
|
||||
current_mesh = GLOBAL_MESH[pp_stage_id]
|
||||
attention_layer = attention_layers[i]
|
||||
attention_layer.__setattr__("current_mesh", current_mesh)
|
||||
pre_hook_helper = attention_layer.register_forward_pre_hook(
|
||||
reshard_transpose_attention_layer_input
|
||||
)
|
||||
post_hook_helper = attention_layer.register_forward_post_hook(
|
||||
transpose_reshard_attention_layer_output
|
||||
)
|
||||
|
||||
# mlp: input from [s/mp_degree, b/dp_degree, h] to [s, b/dp_degree, h], output from [s, b/dp_degree, h] to [s/mp_degree, b/dp_degree, h]
|
||||
mlp_layers = matched_layers[MLP_LAYER_NAME]
|
||||
num_mlp_layers = len(mlp_layers)
|
||||
if mlp_layers is not None:
|
||||
for i in range(num_mlp_layers):
|
||||
current_mesh = GLOBAL_MESH[0]
|
||||
if with_pp:
|
||||
pp_stage_id = get_layer_pp_info(
|
||||
mesh, num_attention_layers, i
|
||||
)
|
||||
current_mesh = GLOBAL_MESH[pp_stage_id]
|
||||
mlp_layer = mlp_layers[i]
|
||||
mlp_layer.__setattr__("current_mesh", current_mesh)
|
||||
pre_hook_helper = mlp_layer.register_forward_pre_hook(
|
||||
reshard_mlp_layer_input
|
||||
)
|
||||
post_hook_helper = mlp_layer.register_forward_post_hook(
|
||||
reshard_mlp_layer_output
|
||||
)
|
||||
|
||||
# rms norm: for the last rms norm (after decoder blocks), input from [s/mp_degree, b/dp_degree, h] to [b, s, h]
|
||||
rms_norm_layers = matched_layers[RMS_NORM_LAYER_NAME]
|
||||
if rms_norm_layers is not None:
|
||||
last_rms_norm_layer = rms_norm_layers[-1]
|
||||
current_mesh = GLOBAL_MESH[-1]
|
||||
last_rms_norm_layer.__setattr__("current_mesh", current_mesh)
|
||||
post_hook_helper = last_rms_norm_layer.register_forward_post_hook(
|
||||
reshard_transpose_rms_norm_layer_output
|
||||
)
|
||||
|
||||
# step 6: processing data parallel if necessary, shard dataloader
|
||||
# TODO(jeff41404): shard optimizer
|
||||
if with_dp:
|
||||
if with_pp:
|
||||
first_stage_mesh = mesh.get_mesh_with_dim("pp", 0)
|
||||
last_stage_mesh = mesh.get_mesh_with_dim("pp", 1)
|
||||
loader = dist.shard_dataloader(
|
||||
dataloader,
|
||||
meshes=[first_stage_mesh, last_stage_mesh],
|
||||
shard_dims="dp",
|
||||
)
|
||||
else:
|
||||
loader = dist.shard_dataloader(
|
||||
dataloader, meshes=[mesh], shard_dims="dp"
|
||||
)
|
||||
else:
|
||||
loader = dist.shard_dataloader(
|
||||
dataloader, meshes=[mesh], shard_dims=None
|
||||
)
|
||||
|
||||
# step 7: clean layer_op recorder hooks
|
||||
for layer in model.sublayers():
|
||||
for hook_helper in layer._op_recorder.hooks:
|
||||
hook_helper.remove()
|
||||
|
||||
return model, optimizer, loader
|
||||
@@ -0,0 +1,402 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
from functools import reduce
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.framework import core
|
||||
|
||||
from .process_mesh import ProcessMesh, get_current_process_mesh
|
||||
from .static.dist_context import get_default_distributed_context
|
||||
from .static.dist_op import DistributedOperatorHelper
|
||||
from .static.dist_tensor import DistributedTensor
|
||||
from .static.utils import (
|
||||
__no_shape_var_type__,
|
||||
convert_to_dims_mapping,
|
||||
verify_shard_spec,
|
||||
)
|
||||
|
||||
|
||||
def shard_tensor(x, process_mesh=None, shard_spec=None):
|
||||
"""
|
||||
Shard a tensor on a process mesh according to the shard specification.
|
||||
|
||||
Args:
|
||||
x (Tensor): the tensor to be sharded.
|
||||
process_mesh (ProcessMesh, optional): An instance of ProcessMesh describes a mesh
|
||||
topology of the used logical processes where the tensor is sharded. If it is None,
|
||||
the found current process mesh will be used. And an error will be raised if the
|
||||
current process mesh cannot be found. Default: None.
|
||||
shard_spec (list, optional): a list to describe the sharding mapping between `x` and `process_mesh`,
|
||||
which means the dimension `i` of `x` is split across the dimension `shard_spec[i]` of `process_mesh`,
|
||||
where `None` means that tensor dimension is not split. For example, given a tensor with
|
||||
the shape [6, 12] and a process mesh with the shape [2, 3] and the dimension names ["x", "y"]:
|
||||
If `shard_spec=["x", "y"]`, each shard of the tensor will have a shape [3, 4];
|
||||
If `shard_spec=["y", "x"]`, each shard of the tensor will have a shape [2, 6];
|
||||
If `shard_spec=["x", None]`, each shard of the tensor will have a shape [3, 12];
|
||||
If `shard_spec=[None, "x"]`, each shard of the tensor will have a shape [6, 4];
|
||||
If `shard_spec=["y", None]`, each shard of the tensor will have a shape [2, 12];
|
||||
If `shard_spec=[None, "y"]`, each shard of the tensor will have a shape [6, 4];
|
||||
If `shard_spec=[None, None]`, each shard of the tensor will have a shape [6, 12];
|
||||
If the `shard_spec` is None, the tensor will be replicated across all the processes of `process_mesh`.
|
||||
In the above example, the `shard_spec=None` is same as 'shard_spec=[None, None]'. Defaults: None.
|
||||
|
||||
Returns:
|
||||
Tensor: the tensor `x` annotated with sharding information.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import paddle
|
||||
>>> from paddle.distributed.fleet import auto
|
||||
|
||||
>>> mesh = auto.ProcessMesh([[0, 1], [2, 3]], dim_names=["x", "y"])
|
||||
>>> x = paddle.ones([4, 6])
|
||||
>>> shard_spec = ["x", "y"]
|
||||
>>> auto.shard_tensor(x, mesh, shard_spec)
|
||||
|
||||
"""
|
||||
|
||||
if process_mesh is not None:
|
||||
assert isinstance(process_mesh, core.ProcessMesh), (
|
||||
f"Argument process_mesh {process_mesh} is not an instance of ProcessMesh"
|
||||
)
|
||||
else:
|
||||
process_mesh = get_current_process_mesh()
|
||||
assert process_mesh is not None, (
|
||||
"Specify the process mesh argument or use ProcessMesh context manager first."
|
||||
)
|
||||
assert isinstance(shard_spec, list), (
|
||||
f"Argument shard_spec {shard_spec} is not an instance of list"
|
||||
)
|
||||
if isinstance(x, str):
|
||||
x = (
|
||||
paddle.static.default_main_program()
|
||||
.global_block()
|
||||
._var_recursive(x)
|
||||
)
|
||||
dist_tensor = DistributedTensor(x)
|
||||
else:
|
||||
dist_tensor = DistributedTensor(x)
|
||||
serial_tensor = dist_tensor.serial_tensor
|
||||
dist_tensor.dist_attr.process_mesh = process_mesh
|
||||
if serial_tensor.type in __no_shape_var_type__:
|
||||
tensor_shape = []
|
||||
else:
|
||||
tensor_shape = serial_tensor.shape
|
||||
if shard_spec is not None:
|
||||
valid_dims = (
|
||||
process_mesh.get_dim_names()
|
||||
if hasattr(process_mesh, "get_dim_names")
|
||||
else process_mesh.dim_names
|
||||
)
|
||||
for i, dim in enumerate(shard_spec):
|
||||
if dim is not None and (
|
||||
not isinstance(dim, str) or dim not in valid_dims
|
||||
):
|
||||
raise ValueError(
|
||||
f"Invalid shard_spec at index {i}: '{dim}' "
|
||||
f"is not a valid dimension name in process_mesh {valid_dims}."
|
||||
)
|
||||
assert verify_shard_spec(shard_spec, tensor_shape, process_mesh), (
|
||||
f"For tensor {serial_tensor.name}, shard_spec {shard_spec} is invalid with tensor_shape {tensor_shape} and process_mesh {process_mesh}."
|
||||
)
|
||||
dist_tensor.dist_attr.dims_mapping = convert_to_dims_mapping(
|
||||
shard_spec, process_mesh
|
||||
)
|
||||
if process_mesh is not None:
|
||||
dist_tensor.dist_attr.mark_annotated("process_mesh")
|
||||
if shard_spec is not None:
|
||||
dist_tensor.dist_attr.mark_annotated("dims_mapping")
|
||||
default_dist_ctx = get_default_distributed_context()
|
||||
default_dist_ctx.add_dist_tensor_for_program(dist_tensor)
|
||||
dist_tensor = default_dist_ctx.get_dist_tensor_for_program(x)
|
||||
default_dist_ctx.add_process_mesh(process_mesh)
|
||||
return x
|
||||
|
||||
|
||||
def shard_op(
|
||||
op, process_mesh=None, in_shard_specs=None, out_shard_specs=None, **kwargs
|
||||
):
|
||||
"""
|
||||
Shard an operation on a process mesh according to its input and output shard specification.
|
||||
|
||||
Args:
|
||||
op (Callable): a callable operator or module to be sharded.
|
||||
process_mesh (ProcessMesh, optional): An instance of ProcessMesh describes a mesh
|
||||
topology of the used logical processes where the op is sharded. All of its inputs and
|
||||
outputs are sharded by this process mesh. If it is None, the found current process mesh
|
||||
will be used. And an error will be raised if the current process mesh cannot be found.
|
||||
Default: None.
|
||||
in_shard_specs (list of list, optional): a list of list to describe the sharding specifications
|
||||
for the inputs. Each item of `in_shard_specs` is a `shard_spec` between the corresponding input
|
||||
and `process_mesh`. If one item is None, the corresponding input is replicated across all processes
|
||||
If it is None, all inputs are replicated across all processes. Note that the length of the
|
||||
`in_shard_specs` should be equal to the actual number of inputs when calling this operation.
|
||||
Default: None.
|
||||
out_shard_specs (list of list, optional): a list of list to describe the sharding specifications
|
||||
for the outputs. Each item of `out_shard_specs` is a `shard_spec` between the corresponding output
|
||||
and `process_mesh`. If one item is None, the corresponding output is replicated across all processes
|
||||
If it is None, all outputs are replicated across all processes. Note that the length of the
|
||||
`in_shard_specs` should be equal to the actual number of inputs when calling this operation.
|
||||
Default: None. Default: None.
|
||||
|
||||
Returns:
|
||||
Outputs of `op`, each of which is annotated with sharding information.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> from paddle.distributed.fleet import auto
|
||||
|
||||
>>> x = paddle.ones([4, 6])
|
||||
>>> y = paddle.zeros([4, 6])
|
||||
>>> mesh = auto.ProcessMesh([[0, 1], [2, 3]], dim_names=["x", "y"])
|
||||
>>> dist_add = auto.shard_op(
|
||||
... paddle.add,
|
||||
... mesh,
|
||||
... in_shard_specs=[["x", "y"], ["y", None]],
|
||||
... out_shard_specs=[[None, "x"]],
|
||||
... )
|
||||
>>> dist_add(x, y)
|
||||
|
||||
"""
|
||||
|
||||
if process_mesh is not None:
|
||||
assert isinstance(process_mesh, ProcessMesh), (
|
||||
f"Argument process_mesh {process_mesh} is not an instance of ProcessMesh"
|
||||
)
|
||||
else:
|
||||
process_mesh = get_current_process_mesh()
|
||||
assert process_mesh is not None, (
|
||||
"Specify the process mesh argument or use ProcessMesh context manager first."
|
||||
)
|
||||
in_dims_mappings = []
|
||||
if in_shard_specs is not None:
|
||||
assert all(
|
||||
(isinstance(shard_spec, list) or shard_spec is None)
|
||||
for shard_spec in in_shard_specs
|
||||
), f"in_shard_spec {in_shard_specs} is not a list of list or None"
|
||||
for shard_spec in in_shard_specs:
|
||||
if shard_spec is not None:
|
||||
in_dims_mappings.append(
|
||||
convert_to_dims_mapping(shard_spec, process_mesh)
|
||||
)
|
||||
else:
|
||||
in_dims_mappings.append(None)
|
||||
out_dims_mappings = []
|
||||
if out_shard_specs is not None:
|
||||
assert all(
|
||||
(isinstance(shard_spec, list) or shard_spec is None)
|
||||
for shard_spec in out_shard_specs
|
||||
), f"out_shard_spec {out_shard_specs} is not a list of list or None"
|
||||
for shard_spec in out_shard_specs:
|
||||
if shard_spec is not None:
|
||||
out_dims_mappings.append(
|
||||
convert_to_dims_mapping(shard_spec, process_mesh)
|
||||
)
|
||||
else:
|
||||
out_dims_mappings.append(None)
|
||||
op = DistributedOperatorHelper(
|
||||
op, process_mesh, in_dims_mappings, out_dims_mappings, kwargs
|
||||
)
|
||||
return op
|
||||
|
||||
|
||||
_g_recompute_idx = -1
|
||||
|
||||
|
||||
def recompute(op):
|
||||
global _g_recompute_idx
|
||||
_g_recompute_idx += 1
|
||||
|
||||
class RecomputeOperator:
|
||||
def __init__(self, op):
|
||||
self._op = op
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
block = paddle.static.default_main_program().global_block()
|
||||
rc_begin_id = len(block.ops)
|
||||
|
||||
with paddle.static.name_scope(
|
||||
f'/auto_parallel/rc_{_g_recompute_idx}'
|
||||
):
|
||||
if paddle.base.dygraph.base.in_to_static_mode():
|
||||
output = (
|
||||
paddle.jit.dy2static.convert_call_func.convert_call(
|
||||
self._op
|
||||
)(*args, **kwargs)
|
||||
)
|
||||
else:
|
||||
output = self._op(*args, **kwargs)
|
||||
|
||||
if paddle.framework.in_pir_mode():
|
||||
block = paddle.static.default_main_program().global_block()
|
||||
rc_end_id = len(block.ops)
|
||||
for idx in range(rc_begin_id, rc_end_id):
|
||||
rc_op = block.ops[idx]
|
||||
rc_op.set_int_attr("fwd_recompute_id", _g_recompute_idx)
|
||||
|
||||
return output
|
||||
|
||||
return RecomputeOperator(op)
|
||||
|
||||
|
||||
def exclude_ops_in_recompute(run_function):
|
||||
"""
|
||||
Exclude some operators in recompute segments.
|
||||
Args:
|
||||
run_function (callable): The callable function to be excluded.
|
||||
|
||||
Returns:
|
||||
ExcludeOperator: The callable object.
|
||||
|
||||
"""
|
||||
|
||||
class ExcludeOperator:
|
||||
def __init__(self, run_function):
|
||||
self._run_function = run_function
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
with paddle.static.name_scope('/exclude_rc'):
|
||||
if paddle.base.dygraph.base.in_to_static_mode():
|
||||
output = (
|
||||
paddle.jit.dy2static.convert_call_func.convert_call(
|
||||
self._run_function
|
||||
)(*args, **kwargs)
|
||||
)
|
||||
else:
|
||||
output = self._run_function(*args, **kwargs)
|
||||
|
||||
return output
|
||||
|
||||
return ExcludeOperator(run_function)
|
||||
|
||||
|
||||
_g_collections = {}
|
||||
|
||||
|
||||
class CollectionNames:
|
||||
FETCHES = "fetches"
|
||||
LOGGING = "logging"
|
||||
|
||||
|
||||
def get_collection(name):
|
||||
collection = _g_collections.get(name, None)
|
||||
if collection is None:
|
||||
collection = []
|
||||
_g_collections[name] = collection
|
||||
return _g_collections[name]
|
||||
|
||||
|
||||
def add_to_collection(collection_name, value, name=None):
|
||||
if collection_name not in _g_collections:
|
||||
_g_collections[collection_name] = []
|
||||
if name is not None:
|
||||
for _, v in _g_collections[collection_name]:
|
||||
if v == value:
|
||||
return
|
||||
_g_collections[collection_name].append((name, value))
|
||||
else:
|
||||
for _, v in _g_collections[collection_name]:
|
||||
if v == value:
|
||||
return
|
||||
_g_collections[collection_name].append((None, value))
|
||||
|
||||
|
||||
def fetch(tensor, name=None, logging=False):
|
||||
if isinstance(tensor, paddle.static.Variable):
|
||||
tensor = tensor.name
|
||||
elif isinstance(tensor, str):
|
||||
tensor = tensor
|
||||
else:
|
||||
raise TypeError(
|
||||
f"Only support fetch `Variable` or `str`[`Variable`'s name], but got `{type(tensor)}`"
|
||||
)
|
||||
add_to_collection(CollectionNames.FETCHES, tensor, name)
|
||||
if logging:
|
||||
add_to_collection(CollectionNames.LOGGING, tensor, name)
|
||||
|
||||
|
||||
_g_mesh = None
|
||||
|
||||
|
||||
def get_mesh() -> paddle.distributed.ProcessMesh:
|
||||
"""
|
||||
Get the global mesh set by set_mesh.
|
||||
|
||||
Returns:
|
||||
mesh (paddle.distributed.ProcessMesh): the global mesh.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
>>> mesh = dist.ProcessMesh([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], dim_names=["dp", "mp", "pp"])
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> dist.auto_parallel.set_mesh(mesh)
|
||||
>>> mesh = dist.auto_parallel.get_mesh()
|
||||
>>> # This case need to be executed in multi-card environment
|
||||
>>> # python -m paddle.distributed.launch --gpus=0,1,2,3,4,5,6,7 {test_case}.py
|
||||
"""
|
||||
global _g_mesh
|
||||
return _g_mesh
|
||||
|
||||
|
||||
def set_mesh(mesh: paddle.distributed.ProcessMesh) -> None:
|
||||
"""
|
||||
Set the global mesh.
|
||||
|
||||
Args:
|
||||
mesh (paddle.distributed.ProcessMesh): global mesh to be set.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
>>> mesh = dist.ProcessMesh([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], dim_names=["dp", "mp", "pp"])
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> dist.auto_parallel.set_mesh(mesh)
|
||||
>>> # This case need to be executed in multi-card environment
|
||||
>>> # python -m paddle.distributed.launch --gpus=0,1,2,3,4,5,6,7 {test_case}.py
|
||||
"""
|
||||
global _g_mesh
|
||||
_g_mesh = mesh
|
||||
|
||||
|
||||
def create_mesh(mesh_dims: list[tuple[str, int]]):
|
||||
"""
|
||||
Create a global process_mesh for auto parallel.
|
||||
|
||||
Args:
|
||||
mesh_dims (list[tuple[str, int]]): A list of tuple, each element is (dim_name, dim_degree).
|
||||
"""
|
||||
global _g_mesh
|
||||
dim_names = [mesh_dim[0] for mesh_dim in mesh_dims]
|
||||
mesh_shape = [mesh_dim[1] for mesh_dim in mesh_dims]
|
||||
mesh_arr = np.arange(0, reduce(lambda x, y: x * y, mesh_shape, 1)).reshape(
|
||||
mesh_shape
|
||||
)
|
||||
_g_mesh = ProcessMesh(mesh_arr, dim_names)
|
||||
return _g_mesh
|
||||
@@ -0,0 +1,15 @@
|
||||
# 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.
|
||||
|
||||
__all__ = []
|
||||
@@ -0,0 +1,391 @@
|
||||
# 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 logging
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.distributed.auto_parallel.ring_attention import (
|
||||
shard_seq_load_balance,
|
||||
)
|
||||
|
||||
from .tensor_parallel import PlanBase
|
||||
|
||||
|
||||
class PrepareContextParallel(PlanBase):
|
||||
"""
|
||||
|
||||
Prepare Input for context parallel optimizations.
|
||||
|
||||
This will work for Layer that calls like whole-llama Layer which is the first layer in the network.
|
||||
|
||||
Users can set backend='p2p/all2all' for different context parallel strategys.
|
||||
|
||||
backend='p2p' will use Ring FlashAttention strategy which segments input with balance in the sequence dimension before whole-llama Layer.
|
||||
backend='all2all' will use Deepspeed Ulysses strategy(Paddle SegmentParallel strategy) which segments input in the sequence dimension before whole-llama Layer.
|
||||
|
||||
Args:
|
||||
backend (string): select strategy for context parallel, now support 'p2p' and 'all2all'.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class SDPALayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
...
|
||||
... def forward(self, q, k, v):
|
||||
... return paddle.nn.functional.scaled_dot_product_attention(q, k, v)
|
||||
>>>
|
||||
>>> class AttentionLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.hidden_size = 64
|
||||
... self.num_key_value_heads = 10
|
||||
... self.head_dim = 64
|
||||
... self.sdpa = SDPALayer()
|
||||
... self.q = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.hidden_size,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
... self.k = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.num_key_value_heads * self.head_dim,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
... self.v = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.num_key_value_heads * self.head_dim,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... q = self.q(input)
|
||||
... k = self.k(input)
|
||||
... v = self.v(input)
|
||||
... return self.sdpa(q, k, v)
|
||||
>>>
|
||||
>>> class LlamaLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.attention = AttentionLayer()
|
||||
...
|
||||
... def forward(self, input, label):
|
||||
... return self.attention(input)
|
||||
>>>
|
||||
>>> class LlamaForCausalLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.llama = LlamaLayer()
|
||||
... self.weight = self.create_parameter(shape=[64, 1024])
|
||||
... self.loss_func = paddle.nn.CrossEntropyLoss()
|
||||
...
|
||||
... def forward(self, input, label):
|
||||
... out = self.llama(input, label)
|
||||
... logits = paddle.matmul(out, self.weight)
|
||||
... loss = self.loss_func(logits, label)
|
||||
... return logits
|
||||
>>>
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = LlamaForCausalLayer()
|
||||
>>> mp_config = {
|
||||
... 'llama': dist.PrepareContextParallel('p2p'),
|
||||
... 'sdpa': dist.ContextParallel('p2p'),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, backend: str = 'p2p') -> None:
|
||||
super().__init__()
|
||||
self.backend = backend
|
||||
assert self.backend in [
|
||||
'p2p',
|
||||
'all2all',
|
||||
], f"backend must be 'p2p' or 'all2all', but got {self.backend}"
|
||||
|
||||
def all2all_split_input_pre_hook(self, process_mesh):
|
||||
def shard_tensor(input_tensor, seq_dim):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
placements = input_tensor.placements
|
||||
if placements is None:
|
||||
placements = [
|
||||
dist.Replicate() for _ in range(len(process_mesh.shape))
|
||||
]
|
||||
# split sequence dim
|
||||
placements[cp_index] = dist.Shard(seq_dim)
|
||||
reshard_input = dist.reshard(input_tensor, process_mesh, placements)
|
||||
return reshard_input
|
||||
|
||||
def all2all_split_input(layer, args):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
# check input_ids
|
||||
if isinstance(args, (list, tuple)):
|
||||
all_args = []
|
||||
for input_tensor in args:
|
||||
assert input_tensor.is_dist(), (
|
||||
"Input tensor must be a distributed tensor."
|
||||
)
|
||||
assert len(input_tensor.shape) == 2, (
|
||||
f"input_ids should be [batch_size, seq_len], but got {input_tensor.shape}"
|
||||
)
|
||||
_, seq_len = input_tensor.shape
|
||||
assert seq_len % cp_degree == 0, (
|
||||
f"sequence length {seq_len} must be divisible by cp degree {cp_degree}"
|
||||
)
|
||||
reshard_input = shard_tensor(input_tensor, 1)
|
||||
all_args.append(reshard_input)
|
||||
new_args = tuple(all_args)
|
||||
return new_args
|
||||
elif isinstance(args, paddle.Tensor):
|
||||
reshard_input = shard_tensor(args, 1)
|
||||
return reshard_input
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported argument type: {type(args)}. Expected list of tensors or single tensor."
|
||||
)
|
||||
|
||||
return all2all_split_input
|
||||
|
||||
def p2p_split_input_pre_hook(self, process_mesh):
|
||||
def p2p_split_input(layer, args):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
if isinstance(args, (list, tuple)):
|
||||
all_args = []
|
||||
for input_tensor in args:
|
||||
# check input_ids
|
||||
assert input_tensor.is_dist(), (
|
||||
"Input tensor must be a distributed tensor."
|
||||
)
|
||||
assert len(input_tensor.shape) == 2, (
|
||||
f"input_ids should be [batch_size, seq_len], but got {input_tensor.shape}"
|
||||
)
|
||||
placements = input_tensor.placements
|
||||
if placements is None:
|
||||
placements = [
|
||||
dist.Replicate()
|
||||
for _ in range(len(process_mesh.shape))
|
||||
]
|
||||
assert placements[cp_index] == dist.Replicate(), (
|
||||
"Input tensor must be a replicated tensor in cp mesh."
|
||||
)
|
||||
reshard_input = shard_seq_load_balance(input_tensor, 1)
|
||||
all_args.append(reshard_input)
|
||||
new_args = tuple(all_args)
|
||||
return new_args
|
||||
elif isinstance(args, paddle.Tensor):
|
||||
reshard_input = shard_seq_load_balance(input_tensor, 1)
|
||||
return reshard_input
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported argument type: {type(args)}. Expected list of tensors or single tensor."
|
||||
)
|
||||
|
||||
return p2p_split_input
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
if self.backend == 'all2all':
|
||||
# Deepspeed Ulysses
|
||||
layer.register_forward_pre_hook(
|
||||
self.all2all_split_input_pre_hook(process_mesh)
|
||||
)
|
||||
elif self.backend == 'p2p':
|
||||
# Ring FlashAttention
|
||||
layer.register_forward_pre_hook(
|
||||
self.p2p_split_input_pre_hook(process_mesh)
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f'{self.backend} is not supported backend for context parallel'
|
||||
)
|
||||
|
||||
|
||||
class ContextParallel(PlanBase):
|
||||
"""
|
||||
|
||||
Applies context parallel optimizations to the attention layer.
|
||||
|
||||
This will work for Layer that calls paddle.nn.functional.scaled_dot_product_attention).
|
||||
|
||||
Users can set backend='p2p/all2all' for different context parallel strategys.
|
||||
|
||||
backend='p2p' will use Ring FlashAttention strategy which segments q/k/v in the sequence dimension and communicates k/v between ranks.
|
||||
backend='all2all' will use Deepspeed Ulysses strategy(Paddle SegmentParallel strategy) which inserts all2all before and after sdpa compute.
|
||||
|
||||
Note:
|
||||
|
||||
|
||||
Args:
|
||||
backend (string): select strategy for context parallel, now support 'p2p' and 'all2all'.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class SDPALayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
...
|
||||
... def forward(self, q, k, v):
|
||||
... return paddle.nn.functional.scaled_dot_product_attention(q, k, v)
|
||||
>>>
|
||||
>>> class AttentionLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.hidden_size = 64
|
||||
... self.num_key_value_heads = 10
|
||||
... self.head_dim = 64
|
||||
... self.sdpa = SDPALayer()
|
||||
... self.q = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.hidden_size,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
... self.k = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.num_key_value_heads * self.head_dim,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
... self.v = paddle.nn.Linear(
|
||||
... self.hidden_size,
|
||||
... self.num_key_value_heads * self.head_dim,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... q = self.q(input)
|
||||
... k = self.k(input)
|
||||
... v = self.v(input)
|
||||
... return self.sdpa(q, k, v)
|
||||
>>>
|
||||
>>> class LlamaLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.attention = AttentionLayer()
|
||||
...
|
||||
... def forward(self, input, label):
|
||||
... return self.attention(input)
|
||||
>>>
|
||||
>>> class LlamaForCausalLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.llama = LlamaLayer()
|
||||
... self.weight = self.create_parameter(shape=[64, 1024])
|
||||
... self.loss_func = paddle.nn.CrossEntropyLoss()
|
||||
...
|
||||
... def forward(self, input, label):
|
||||
... out = self.llama(input, label)
|
||||
... logits = paddle.matmul(out, self.weight)
|
||||
... loss = self.loss_func(logits, label)
|
||||
... return logits
|
||||
>>>
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = LlamaForCausalLayer()
|
||||
>>> mp_config = {
|
||||
... 'llama': dist.PrepareContextParallel('p2p'),
|
||||
... 'sdpa': dist.ContextParallel('p2p'),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, backend: str = 'p2p') -> None:
|
||||
super().__init__()
|
||||
self.backend = backend
|
||||
|
||||
def all2all_reshard_pre_hook(self, process_mesh):
|
||||
def all2all_reshard_hook(layer, args):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
all_args = []
|
||||
for arg in args:
|
||||
# check q k v
|
||||
assert arg.is_dist(), f"arg {arg} must be a distributed tensor."
|
||||
assert len(arg.shape) == 3 or len(arg.shape) == 4
|
||||
placements = arg.placements
|
||||
assert placements[cp_index] == dist.Shard(1), (
|
||||
f"arg {arg} must be sharded in sequence dimension."
|
||||
)
|
||||
# reshard [batch_size,seq_len/sep,num_head,head_dim] -> [batch_size,seq_len,num_head/sep,head_dim]
|
||||
placements[cp_index] = dist.Shard(2)
|
||||
target_arg = dist.reshard(arg, process_mesh, placements)
|
||||
all_args.append(target_arg)
|
||||
new_args = tuple(all_args)
|
||||
return new_args
|
||||
|
||||
return all2all_reshard_hook
|
||||
|
||||
def all2all_reshard_post_hook(self, process_mesh):
|
||||
def all2all_reshard_hook(layer, input, output):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
placements = output.placements
|
||||
assert output.is_dist(), (
|
||||
f"output {output} must be a distributed tensor."
|
||||
)
|
||||
assert len(output.shape) == 4 or len(output.shape) == 3
|
||||
assert placements[cp_index] == dist.Shard(2), (
|
||||
f"output {output} must be Shard(2) in sequence dimension."
|
||||
)
|
||||
# reshard [batch_size,seq_len,num_head/seq,head_dim] -> [batch_size,seq_len/sep,num_head,head_dim]
|
||||
placements[cp_index] = dist.Shard(1)
|
||||
target_output = dist.reshard(output, process_mesh, placements)
|
||||
return target_output
|
||||
|
||||
return all2all_reshard_hook
|
||||
|
||||
def p2p_reshard_pre_hook(self, process_mesh):
|
||||
def input_hook(layer, args, kwargs):
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
for arg in args:
|
||||
# check q k v
|
||||
assert arg.is_dist(), (
|
||||
"Input tensor must be a distributed tensor."
|
||||
)
|
||||
assert len(arg.shape) == 3 or len(arg.shape) == 4
|
||||
placements = arg.placements
|
||||
assert placements[cp_index] == dist.Shard(1), (
|
||||
f"arg {arg} must be Shard(1) in sequence dimension."
|
||||
)
|
||||
# edit kwarg backend to 'p2p'
|
||||
new_kwargs = kwargs
|
||||
new_kwargs['backend'] = 'p2p'
|
||||
return args, new_kwargs
|
||||
|
||||
return input_hook
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
if self.backend == 'all2all':
|
||||
# Deepspeed Ulysses
|
||||
layer.register_forward_pre_hook(
|
||||
self.all2all_reshard_pre_hook(process_mesh)
|
||||
)
|
||||
layer.register_forward_post_hook(
|
||||
self.all2all_reshard_post_hook(process_mesh)
|
||||
)
|
||||
elif self.backend == 'p2p':
|
||||
# Ring FlashAttention
|
||||
layer.register_forward_pre_hook(
|
||||
self.p2p_reshard_pre_hook(process_mesh), with_kwargs=True
|
||||
)
|
||||
else:
|
||||
logging.warning(
|
||||
f'{self.backend} is not supported backend for context parallel'
|
||||
)
|
||||
@@ -0,0 +1,294 @@
|
||||
# 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.
|
||||
import logging
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle import pir
|
||||
from paddle.base.framework import (
|
||||
in_dygraph_mode,
|
||||
in_pir_mode,
|
||||
)
|
||||
from paddle.distributed import fleet
|
||||
from paddle.nn import Layer
|
||||
from paddle.optimizer import Optimizer
|
||||
|
||||
|
||||
def is_tensor(tensor):
|
||||
if in_dygraph_mode():
|
||||
return isinstance(tensor, paddle.Tensor)
|
||||
elif in_pir_mode():
|
||||
return isinstance(tensor, pir.Value)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"PipelineParallel are only supported in dynamic or pir mode."
|
||||
)
|
||||
|
||||
|
||||
class ParallelOptimizer:
|
||||
def __init__(
|
||||
self,
|
||||
optimizer,
|
||||
level=None,
|
||||
sharding_mesh_dim=None,
|
||||
):
|
||||
self.level = None
|
||||
self.sharding_mesh_dim = None
|
||||
self.optimizer = None
|
||||
|
||||
if isinstance(optimizer, ParallelOptimizer):
|
||||
self.optimizer = optimizer.optimizer
|
||||
if level is None:
|
||||
self.level = optimizer.level
|
||||
self.sharding_mesh_dim = optimizer.sharding_mesh_dim
|
||||
else:
|
||||
if isinstance(level, int):
|
||||
level = str(level)
|
||||
assert level in ("0", "1", "2", "3", None)
|
||||
if optimizer.level is not None:
|
||||
assert level == optimizer.level, (
|
||||
f"The level passed in is not identical with previous level. Current level is {level}, previous level is {optimizer.level}"
|
||||
)
|
||||
self.level = level
|
||||
self.sharding_mesh_dim = sharding_mesh_dim
|
||||
else:
|
||||
assert isinstance(optimizer, Optimizer)
|
||||
self.optimizer = optimizer
|
||||
if isinstance(level, int):
|
||||
level = str(level)
|
||||
assert level in ("0", "1", "2", "3", None)
|
||||
# level=0 and level=None are all mean pure dp
|
||||
self.level = level
|
||||
self.sharding_mesh_dim = sharding_mesh_dim
|
||||
|
||||
self.is_initialized = False
|
||||
|
||||
def parallelize(self):
|
||||
assert self.optimizer is not None
|
||||
if self.is_initialized:
|
||||
return self.optimizer
|
||||
|
||||
mesh = fleet.auto.get_mesh()
|
||||
if self.level == "1":
|
||||
self.optimizer = dist.shard_optimizer(
|
||||
self.optimizer,
|
||||
dist.ShardingStage1(self.sharding_mesh_dim, mesh),
|
||||
)
|
||||
elif self.level == "2":
|
||||
self.optimizer = dist.shard_optimizer(
|
||||
self.optimizer,
|
||||
dist.ShardingStage2(self.sharding_mesh_dim, mesh),
|
||||
)
|
||||
elif self.level == "3":
|
||||
self.optimizer = dist.shard_optimizer(
|
||||
self.optimizer,
|
||||
dist.ShardingStage3(self.sharding_mesh_dim, mesh),
|
||||
)
|
||||
else:
|
||||
self.optimizer = dist.shard_optimizer(self.optimizer, None)
|
||||
self.is_initialized = True
|
||||
|
||||
return self.optimizer
|
||||
|
||||
def update_param_list(self, parallelized_parameters):
|
||||
self.optimizer._parameter_list = parallelized_parameters
|
||||
if isinstance(parallelized_parameters[0], dict):
|
||||
self.optimizer._param_groups = []
|
||||
for param_group in self.parallelized_parameters:
|
||||
self.optimizer._add_param_group(param_group.copy())
|
||||
else:
|
||||
self.optimizer._param_groups = self.optimizer._parameter_list
|
||||
|
||||
|
||||
class ParallelModel:
|
||||
def __init__(self, model):
|
||||
super().__init__()
|
||||
self.pp_parallelizer = None
|
||||
self.tp_parallelizer = None
|
||||
self.sharding_parallelizer = None
|
||||
self.model = None
|
||||
self.share_param_list = {}
|
||||
self.layer_param_placements = {}
|
||||
if isinstance(model, ParallelModel):
|
||||
self.pp_parallelizer = model.pp_parallelizer
|
||||
self.tp_parallelizer = model.tp_parallelizer
|
||||
self.sharding_parallelizer = model.sharding_parallelizer
|
||||
self.model = model.model
|
||||
else:
|
||||
assert isinstance(model, Layer)
|
||||
self.model = model
|
||||
|
||||
self.is_parallelized = False
|
||||
|
||||
def get_mesh(self, pp_idx=0):
|
||||
mesh = fleet.auto.get_mesh()
|
||||
if "pp" in mesh.dim_names:
|
||||
mesh = mesh.get_mesh_with_dim("pp", pp_idx)
|
||||
return mesh
|
||||
|
||||
def parallelize_model(self):
|
||||
assert self.model is not None
|
||||
if self.is_parallelized:
|
||||
return self.model
|
||||
|
||||
if self.pp_parallelizer is not None:
|
||||
assert callable(self.pp_parallelizer)
|
||||
self.model = self.pp_parallelizer(self.model)
|
||||
|
||||
if self.tp_parallelizer is not None:
|
||||
assert callable(self.tp_parallelizer)
|
||||
self.model, self.layer_param_placements = self.tp_parallelizer(
|
||||
self.model
|
||||
)
|
||||
if self.sharding_parallelizer is not None:
|
||||
assert callable(self.sharding_parallelizer)
|
||||
self.model = self.sharding_parallelizer(self.model)
|
||||
self._shard_all_param(self.model)
|
||||
self.is_parallelized = True
|
||||
|
||||
return self.model
|
||||
|
||||
def _process_share_weight_layer(
|
||||
self, layer, origin_weight, param_name, param_placements
|
||||
):
|
||||
ipp = (
|
||||
layer.pipeline_stage_index
|
||||
if hasattr(layer, "pipeline_stage_index")
|
||||
else 0
|
||||
)
|
||||
|
||||
def create_pre_hook(origin_weight, param_name):
|
||||
def forward_pre_hook(layer, input):
|
||||
setattr(
|
||||
layer,
|
||||
param_name,
|
||||
None,
|
||||
)
|
||||
delattr(layer, param_name)
|
||||
mesh = self.get_mesh(ipp)
|
||||
share_weight = dist.reshard(
|
||||
origin_weight,
|
||||
mesh,
|
||||
param_placements,
|
||||
)
|
||||
setattr(
|
||||
layer,
|
||||
param_name,
|
||||
share_weight,
|
||||
)
|
||||
|
||||
return forward_pre_hook
|
||||
|
||||
def create_post_hook(origin_weight, param_name):
|
||||
def forward_post_hook(layer, input, output):
|
||||
setattr(
|
||||
layer,
|
||||
param_name,
|
||||
origin_weight,
|
||||
)
|
||||
|
||||
return forward_post_hook
|
||||
|
||||
layer.register_forward_pre_hook(
|
||||
create_pre_hook(origin_weight, param_name)
|
||||
)
|
||||
layer.register_forward_post_hook(
|
||||
create_post_hook(origin_weight, param_name)
|
||||
)
|
||||
|
||||
def _shard_all_param(self, model):
|
||||
param_name_to_shard_param = {}
|
||||
param_name_to_pp_stage = {}
|
||||
|
||||
def shard_layer_param(layer):
|
||||
if self.pp_parallelizer is not None:
|
||||
assert hasattr(layer, "pipeline_stage_index")
|
||||
for param_name in list(layer._parameters.keys()):
|
||||
param = getattr(layer, param_name)
|
||||
if param is not None:
|
||||
param_full_name = param.name
|
||||
ipp = (
|
||||
layer.pipeline_stage_index
|
||||
if hasattr(layer, "pipeline_stage_index")
|
||||
else 0
|
||||
)
|
||||
mesh = self.get_mesh(ipp)
|
||||
param_placements = [
|
||||
dist.Replicate() for _ in range(len(mesh._shape))
|
||||
]
|
||||
if layer in self.layer_param_placements:
|
||||
if param_name in self.layer_param_placements[layer]:
|
||||
param_placements = (
|
||||
self.layer_param_placements[layer][param_name]
|
||||
if self.layer_param_placements[layer][
|
||||
param_name
|
||||
]
|
||||
is not None
|
||||
else param_placements
|
||||
)
|
||||
if not param.is_dist():
|
||||
if param_full_name in param_name_to_shard_param:
|
||||
setattr(
|
||||
layer,
|
||||
param_name,
|
||||
param_name_to_shard_param[param_full_name],
|
||||
)
|
||||
if ipp != param_name_to_pp_stage[param_full_name]:
|
||||
self._process_share_weight_layer(
|
||||
layer,
|
||||
param_name_to_shard_param[param_full_name],
|
||||
param_name,
|
||||
param_placements,
|
||||
)
|
||||
else:
|
||||
param = dist.shard_tensor(
|
||||
param, mesh, param_placements
|
||||
)
|
||||
param_name_to_shard_param[param_full_name] = param
|
||||
param_name_to_pp_stage[param_full_name] = ipp
|
||||
setattr(layer, param_name, param)
|
||||
else:
|
||||
if (
|
||||
param_full_name in param_name_to_shard_param
|
||||
and ipp != param_name_to_pp_stage[param_full_name]
|
||||
):
|
||||
self._process_share_weight_layer(
|
||||
layer,
|
||||
param_name_to_shard_param[param_full_name],
|
||||
param_name,
|
||||
param_placements,
|
||||
)
|
||||
elif param_full_name not in param_name_to_shard_param:
|
||||
param_name_to_shard_param[param_full_name] = param
|
||||
param_name_to_pp_stage[param_full_name] = ipp
|
||||
|
||||
for name, layer in model.named_sublayers():
|
||||
shard_layer_param(layer)
|
||||
|
||||
|
||||
def parallelize_model_and_optimizer(model, optimizer=None):
|
||||
if not isinstance(model, ParallelModel):
|
||||
assert not isinstance(optimizer, ParallelOptimizer)
|
||||
logging.warning(
|
||||
"The method `parallelize_model_and_optimizer` won't do anything since the model is not parallelized."
|
||||
)
|
||||
return model, optimizer
|
||||
parallelized_model = model.parallelize_model()
|
||||
parallelized_optimizer = None
|
||||
if optimizer is not None:
|
||||
assert isinstance(optimizer, ParallelOptimizer)
|
||||
optimizer.update_param_list(parallelized_model.parameters())
|
||||
parallelized_optimizer = optimizer.parallelize()
|
||||
|
||||
return parallelized_model, parallelized_optimizer
|
||||
@@ -0,0 +1,385 @@
|
||||
# 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 warnings
|
||||
from typing import TYPE_CHECKING, TypedDict
|
||||
|
||||
from typing_extensions import NotRequired
|
||||
|
||||
from paddle.distributed import fleet
|
||||
from paddle.framework import core
|
||||
|
||||
from .parallel_base import ParallelOptimizer, parallelize_model_and_optimizer
|
||||
from .pipeline_parallel import pipeline_parallel
|
||||
from .sharded_data_parallel import sharded_data_parallel
|
||||
from .tensor_parallel import tensor_parallel
|
||||
|
||||
if TYPE_CHECKING:
|
||||
import paddle
|
||||
|
||||
from .pipeline_parallel import SplitPoint
|
||||
from .tensor_parallel import PlanBase
|
||||
|
||||
class _DPConfig(TypedDict):
|
||||
sharding_level: str | int
|
||||
|
||||
class _MPConfig(TypedDict):
|
||||
parallelize_plan: dict[str, PlanBase | list[PlanBase]]
|
||||
|
||||
class _PPConfig(TypedDict):
|
||||
split_spec: str | dict[str, SplitPoint]
|
||||
global_spec: NotRequired[str]
|
||||
|
||||
class _ParallelizeConfig(TypedDict):
|
||||
dp_config: NotRequired[_DPConfig]
|
||||
mp_config: NotRequired[_MPConfig]
|
||||
pp_config: NotRequired[_PPConfig]
|
||||
|
||||
|
||||
def parallelize(
|
||||
model: paddle.nn.Layer,
|
||||
optimizer: paddle.optimizer.Optimizer | None = None,
|
||||
mesh: paddle.distributed.ProcessMesh | None = None,
|
||||
config: _ParallelizeConfig | None = None,
|
||||
) -> tuple[paddle.nn.Layer, paddle.optimizer.Optimizer]:
|
||||
"""
|
||||
|
||||
Parallelize the model and optimizer from a single card version to a distributed version.
|
||||
|
||||
Args:
|
||||
model (paddle.nn.Layer): the model to be parallelized.
|
||||
optimizer (paddle.optimizer.Optimizer, optional): the optimizer to be parallelized.
|
||||
Could be `None` if no optimizer to be parallelized.
|
||||
mesh (paddle.distributed.ProcessMesh, optional): the process mesh for parallelize the model and the optimizer.
|
||||
Best practice: calling `dist.auto_parallel.set_mesh` to set the global mesh ahead of calling `parallelize`
|
||||
and keep the `mesh` parameter as `None.
|
||||
If the `mesh` is not None, the mesh passed to `parallelize` will overwrite the mesh set by `set_mesh`.
|
||||
config (dict, optional): a dict contains the parallel config.
|
||||
The keys of the dict can be chosen from `dp_config`, `mp_config` and `pp_config` which will be used to
|
||||
determine the parallel method for data parallel, tensor parallel and pipeline parallel separately.
|
||||
A valid config can be like this: {"dp_config": for more information refer the `dp_config` section of
|
||||
this doc, "mp_config": for more information refer the `mp_config` section of this doc, "pp_config":
|
||||
for more information refer the `pp_config` section of this doc}.
|
||||
|
||||
dp_config (dict): a dict specifying the data parallel config. The keys of `dp_config` is `sharding_level`.
|
||||
The value of `sharding_level` can be chosen from 0/1/2/3, which means pure data parallel, sharding
|
||||
parallel stage 1, sharding parallel stage 2 and sharding parallel stage 3 separately. A valid
|
||||
dp_config can be like this: {"sharding_level": 2}.
|
||||
|
||||
mp_config (dict): a dict specifying the tensor parallel config. The keys of `mp_config` is
|
||||
`parallelize_plan`. The value of `parallelize_plan` is another dict, mapping a layer name or a param
|
||||
name to a specific parallel plan. Note that the layer name could be written in regular format. If
|
||||
mapping a param name to a specific plan, the name of the param must be ended with `weight` or `bias`.
|
||||
And all valid parallel plan is `ColWiseParallel`, `RowWiseParallel`, `SequenceParallelBegin,
|
||||
`SequenceParallelDisable`, `SequenceParallelEnable`, `SequenceParallelEnd`, `PrepareLayerInput` and
|
||||
`PrepareLayerOutput`. A valid mp_config can be like this: {"llama.embed_tokens": dist.ColWiseParallel(),
|
||||
"llama.norm": dist.SequenceParallelEnable(), "lm_head.weight": dist.ColWiseParallel()}.
|
||||
|
||||
pp_config (dict): a dict specifying the pipeline parallel config. The keys of `pp_config` is `split_spec`
|
||||
and `global_spec`. The `split_spec` can be a dict or a string. If the `split_spec` is a dict, it maps
|
||||
a layer name to a `SplitPoint`, note that the layer name could be written in regular format. The
|
||||
pipeline parallel will exactly split the model at the point indicated by the map. If the `split_spec`
|
||||
is a string, it contains the prefix of a set of layers. The pipeline parallel will automatically split
|
||||
the model evenly at target layer. The `global_spec` is a string indicating a layer that contains global
|
||||
tensors, which will be duplicated through all stages of the pipeline parallel. Some valid pp_config
|
||||
can be list these: {"split_spec": "llama.layers", "global_spec": "llama.global_layer"}
|
||||
or {"split_spec": {"llama.layers.1": SplitPoint.END}}.
|
||||
|
||||
cp_config (dict): a dict specifying the context parallel config. The keys of `cp_config` is
|
||||
`parallelize_plan`. The value of `parallelize_plan` is another dict, mapping a layer name or a param
|
||||
name to a specific parallel plan. All valid parallel plan is `ContextParallel` and `PrepareContextParallel`.
|
||||
A valid cp_config can be like this: {"llama": dist.PrepareContextParallel('p2p'),
|
||||
"llama.sdpa": dist.ContextParallel('p2p')}.
|
||||
|
||||
Note:
|
||||
If the mesh is `None` or neither of `dp_config`, `mp_config`, `pp_config` and `cp_config` is in the config, this
|
||||
api will do nothing but return the model and optimizer passed in.
|
||||
|
||||
Returns:
|
||||
model, optimizer: the model and the optimizer after parallelize
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class ModelConfig:
|
||||
... def __init__(self):
|
||||
... self.vocab_size = 10
|
||||
... self.hidden_size = 20
|
||||
... self.intermediate_size = 20
|
||||
... self.num_layers = 2
|
||||
|
||||
>>> model_config = ModelConfig()
|
||||
|
||||
>>> class LlamaRMSNorm(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.weight = paddle.create_parameter(
|
||||
... shape=[model_config.hidden_size],
|
||||
... dtype=paddle.get_default_dtype(),
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaAttention(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
...
|
||||
... self.qkv_proj = paddle.nn.Linear(
|
||||
... model_config.hidden_size,
|
||||
... model_config.hidden_size * 3,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... self.o_proj = paddle.nn.Linear(
|
||||
... model_config.hidden_size,
|
||||
... model_config.hidden_size,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaMLP(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.gate_up_proj = paddle.nn.Linear(
|
||||
... model_config.hidden_size,
|
||||
... model_config.intermediate_size * 2,
|
||||
... bias_attr=False,
|
||||
... )
|
||||
...
|
||||
... self.down_proj = paddle.nn.Linear(model_config.intermediate_size, model_config.hidden_size, bias_attr=False)
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaDecoderLayer(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.self_attn = LlamaAttention()
|
||||
... self.mlp = LlamaMLP()
|
||||
... self.input_layernorm = LlamaRMSNorm()
|
||||
... self.post_attention_layernorm = LlamaRMSNorm()
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaModel(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.embedding = paddle.nn.Embedding(model_config.vocab_size, model_config.hidden_size)
|
||||
... decoder_layers = []
|
||||
... for _ in range(model_config.num_layers):
|
||||
... decoder_layers.append(LlamaDecoderLayer())
|
||||
...
|
||||
... self.layers = paddle.nn.LayerList(decoder_layers)
|
||||
... self.norm = LlamaRMSNorm()
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaLMHead(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.weight = self.create_parameter(
|
||||
... shape=[model_config.hidden_size, model_config.vocab_size],
|
||||
... dtype=paddle.get_default_dtype(),
|
||||
... )
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> class LlamaForCausalLM(paddle.nn.Layer):
|
||||
... def __init__(self):
|
||||
... super().__init__()
|
||||
... self.llama = LlamaModel()
|
||||
... self.lm_head = LlamaLMHead()
|
||||
...
|
||||
... def forward(self, input):
|
||||
... pass
|
||||
|
||||
>>> mesh = dist.ProcessMesh([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], dim_names=["dp", "mp", "pp"])
|
||||
>>> dist.auto_parallel.set_mesh(mesh)
|
||||
>>> parallel_config = {
|
||||
... "dp_config": {'sharding_level': 1},
|
||||
... "mp_config": {
|
||||
... "parallelize_plan": {
|
||||
... "llama.embed_tokens": [
|
||||
... dist.ColWiseParallel(),
|
||||
... dist.SequenceParallelBegin(),
|
||||
... ],
|
||||
... "llama.position_embedding": [
|
||||
... dist.ColWiseParallel(),
|
||||
... dist.SequenceParallelBegin(),
|
||||
... ],
|
||||
... "llama.layers.*.self_attn.qkv_proj": dist.ColWiseParallel(),
|
||||
... "llama.layers.*.self_attn.o_proj": dist.RowWiseParallel(),
|
||||
... "llama.layers.*.self_attn": dist.SequenceParallelDisable(),
|
||||
... "llama.layers.*.mlp.gate_up_proj": dist.ColWiseParallel(),
|
||||
... "llama.layers.*.mlp.down_proj": dist.RowWiseParallel(),
|
||||
... "llama.layers.*.mlp": dist.SequenceParallelDisable(need_transpose=False),
|
||||
... "lm_head.weight": dist.ColWiseParallel(),
|
||||
... "lm_head": dist.SequenceParallelEnd(),
|
||||
... }
|
||||
... },
|
||||
... "pp_config": {'split_spec': "llama.layers"},
|
||||
... }
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> model = LlamaForCausalLM()
|
||||
>>> optimizer = paddle.optimizer.AdamW(parameters=model.parameters())
|
||||
>>> dist_model, dist_optimizer = dist.parallelize(model, optimizer, config=parallel_config) # type: ignore[arg-type]
|
||||
>>> # This case need to be executed in multi-card environment
|
||||
>>> # python -m paddle.distributed.launch --gpus=0,1,2,3,4,5,6,7 {test_case}.py
|
||||
|
||||
"""
|
||||
if config is None:
|
||||
warnings.warn(
|
||||
"The `parallelize will do nothing since the config is `None`."
|
||||
)
|
||||
return model, optimizer
|
||||
assert isinstance(config, dict)
|
||||
if mesh is not None:
|
||||
assert isinstance(mesh, core.ProcessMesh), (
|
||||
"The mesh must be an instance of paddle.distributed.ProcessMesh."
|
||||
)
|
||||
g_mesh = fleet.auto.get_mesh()
|
||||
if g_mesh is not None and g_mesh != mesh:
|
||||
warnings.warn(
|
||||
"The mesh set by `fleet.auto.set_mesh` is different with the mesh pass to "
|
||||
"`parallelize`. Will overwrite the previous mesh"
|
||||
)
|
||||
fleet.auto.set_mesh(mesh)
|
||||
pp_config = config.get('pp_config')
|
||||
mp_config = config.get('mp_config')
|
||||
dp_config = config.get('dp_config')
|
||||
cp_config = config.get('cp_config')
|
||||
if pp_config is not None:
|
||||
assert isinstance(pp_config, dict)
|
||||
model, optimizer = pipeline_parallel(
|
||||
model,
|
||||
optimizer,
|
||||
pp_config,
|
||||
)
|
||||
if mp_config is not None:
|
||||
assert isinstance(mp_config, dict)
|
||||
if cp_config is not None:
|
||||
assert isinstance(cp_config, dict)
|
||||
assert "parallelize_plan" in cp_config.keys()
|
||||
assert "parallelize_plan" in mp_config.keys()
|
||||
mp_config['parallelize_plan'].update(cp_config['parallelize_plan'])
|
||||
model, optimizer = tensor_parallel(model, optimizer, mp_config)
|
||||
elif cp_config is not None:
|
||||
assert isinstance(cp_config, dict)
|
||||
model, optimizer = tensor_parallel(
|
||||
model,
|
||||
optimizer,
|
||||
cp_config,
|
||||
)
|
||||
if dp_config is not None:
|
||||
assert isinstance(dp_config, dict)
|
||||
if 'sharding_level' not in dp_config.keys():
|
||||
warnings.warn(
|
||||
"The dp_config doesn't contain sharding_level, will run under dp."
|
||||
)
|
||||
model, optimizer = sharded_data_parallel(
|
||||
model,
|
||||
optimizer,
|
||||
config=dp_config,
|
||||
)
|
||||
model, optimizer = parallelize_model_and_optimizer(model, optimizer)
|
||||
return model, optimizer
|
||||
|
||||
|
||||
has_parallelized_model = False
|
||||
|
||||
|
||||
def parallelize_model(model, mesh=None, config=None):
|
||||
if config is None:
|
||||
warnings.warn(
|
||||
"The `parallelize_model will do nothing since the config is `None`."
|
||||
)
|
||||
return model
|
||||
assert isinstance(config, dict)
|
||||
if mesh is not None:
|
||||
assert isinstance(mesh, core.ProcessMesh), (
|
||||
"The mesh must be an instance of paddle.distributed.ProcessMesh."
|
||||
)
|
||||
g_mesh = fleet.auto.get_mesh()
|
||||
if g_mesh is not None and g_mesh != mesh:
|
||||
warnings.warn(
|
||||
"The mesh set by `fleet.auto.set_mesh` is different with the mesh pass to "
|
||||
"`parallelize_model`. Will overwrite the previous mesh"
|
||||
)
|
||||
fleet.auto.set_mesh(mesh)
|
||||
global has_parallelized_model
|
||||
has_parallelized_model = True
|
||||
model, _ = parallelize(model, None, mesh, config)
|
||||
return model
|
||||
|
||||
|
||||
def parallelize_optimizer(optimizer, mesh=None, config=None):
|
||||
if config is None:
|
||||
warnings.warn(
|
||||
"The `parallelize_optimizer will do nothing since the config is `None`."
|
||||
)
|
||||
return optimizer
|
||||
assert isinstance(config, dict)
|
||||
if mesh is not None:
|
||||
assert isinstance(mesh, core.ProcessMesh), (
|
||||
"The mesh must be an instance of paddle.distributed.ProcessMesh."
|
||||
)
|
||||
g_mesh = fleet.auto.get_mesh()
|
||||
if g_mesh is not None and g_mesh != mesh:
|
||||
warnings.warn(
|
||||
"The mesh set by `fleet.auto.set_mesh` is different with the mesh pass to "
|
||||
"`parallelize_optimizer`. Will overwrite the previous mesh"
|
||||
)
|
||||
fleet.auto.set_mesh(mesh)
|
||||
|
||||
global has_parallelized_model
|
||||
assert has_parallelized_model, (
|
||||
"Please parallelize the model before parallelize optimizer."
|
||||
)
|
||||
param_list = optimizer._parameter_list
|
||||
if isinstance(param_list[0], dict):
|
||||
for param_group in param_list:
|
||||
for param in param_group['params']:
|
||||
assert param.is_dist(), (
|
||||
"Please use model after parallelize to create optimizer."
|
||||
)
|
||||
else:
|
||||
for param in param_list:
|
||||
assert param.is_dist(), (
|
||||
"Please use model after parallelize to create optimizer."
|
||||
)
|
||||
|
||||
dp_config = config.get('dp_config')
|
||||
level = None
|
||||
sharding_mesh_dim = None
|
||||
if dp_config is not None:
|
||||
if 'sharding_level' not in dp_config.keys():
|
||||
warnings.warn(
|
||||
"The dp_config doesn't contain sharding_level, will run under dp."
|
||||
)
|
||||
level = dp_config.get('sharding_level')
|
||||
sharding_mesh_dim = dp_config.get('sharding_mesh_dim', "dp")
|
||||
optimizer = ParallelOptimizer(optimizer, level, sharding_mesh_dim)
|
||||
optimizer = optimizer.parallelize()
|
||||
return optimizer
|
||||
@@ -0,0 +1,419 @@
|
||||
# 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.
|
||||
|
||||
import itertools
|
||||
import logging
|
||||
import re
|
||||
from collections import OrderedDict
|
||||
from enum import Enum
|
||||
|
||||
import paddle.distributed as dist
|
||||
from paddle.distributed import fleet
|
||||
from paddle.distributed.utils.log_utils import get_logger
|
||||
|
||||
from .parallel_base import ParallelModel, ParallelOptimizer, is_tensor
|
||||
|
||||
logger = get_logger("INFO", __name__)
|
||||
|
||||
|
||||
class SplitPoint(Enum):
|
||||
"""
|
||||
Marking the position of the split.
|
||||
BEGINNING: will split the model before the specified layer.
|
||||
END: will split the model after the specified layer.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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()
|
||||
>>> pp_config = {
|
||||
... 'fc1': dist.SplitPoint.END,
|
||||
... }
|
||||
"""
|
||||
|
||||
BEGINNING = 0
|
||||
END = 1
|
||||
|
||||
|
||||
class PipelineParallel(ParallelModel):
|
||||
def __init__(self, model, split_spec, global_spec, pipeline_layers=None):
|
||||
super().__init__(model)
|
||||
self.split_spec = split_spec
|
||||
self.global_spec = global_spec
|
||||
self.pipeline_layers = pipeline_layers
|
||||
self.pp_parallelizer = self.pipeline_parallel_fn
|
||||
self.name_to_layer = {}
|
||||
for layer_name, layer in model.named_sublayers():
|
||||
self.name_to_layer[layer_name] = layer
|
||||
|
||||
def get_layer_by_name(self, name):
|
||||
assert name in self.name_to_layer, (
|
||||
f"layer name:{name} not in the model, please check the split_spec"
|
||||
)
|
||||
return self.name_to_layer[name]
|
||||
|
||||
def pipeline_parallel_fn(self, model):
|
||||
mesh = fleet.auto.get_mesh()
|
||||
pipeline_stage_num = mesh.get_dim_size("pp")
|
||||
assert len(self.split_spec) == pipeline_stage_num - 1
|
||||
|
||||
def forward_post_hook(layer, input, output):
|
||||
pipeline_stage_index = layer.pipeline_stage_index
|
||||
split_point = layer.split_point
|
||||
assert split_point == SplitPoint.END
|
||||
# reshard to next pipeline stage
|
||||
if isinstance(output, (dict, OrderedDict)):
|
||||
for key, tensor in output.items():
|
||||
assert is_tensor(tensor)
|
||||
output[key] = dist.reshard(
|
||||
tensor,
|
||||
self.get_mesh(pipeline_stage_index + 1),
|
||||
tensor.placements,
|
||||
)
|
||||
elif isinstance(output, list):
|
||||
for i in range(len(output)):
|
||||
assert is_tensor(output[i])
|
||||
output[i] = dist.reshard(
|
||||
output[i],
|
||||
self.get_mesh(pipeline_stage_index + 1),
|
||||
output[i].placements,
|
||||
)
|
||||
elif isinstance(output, tuple):
|
||||
output = list(output)
|
||||
for i in range(len(output)):
|
||||
assert is_tensor(output[i])
|
||||
output[i] = dist.reshard(
|
||||
output[i],
|
||||
self.get_mesh(pipeline_stage_index + 1),
|
||||
output[i].placements,
|
||||
)
|
||||
output = tuple(output)
|
||||
elif is_tensor(output):
|
||||
output = dist.reshard(
|
||||
output,
|
||||
self.get_mesh(pipeline_stage_index + 1),
|
||||
output.placements,
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"output between pp stages should be a dict of tensors or list of tensors or tuple of tensors or tensor, but {type(output)}"
|
||||
)
|
||||
return output
|
||||
|
||||
def forward_pre_hook(layer, input):
|
||||
split_point = layer.split_point
|
||||
assert split_point == SplitPoint.BEGINNING
|
||||
# TODO(deepllz): support in the future
|
||||
return input
|
||||
|
||||
# step1: set every layer's own pipeline_stage_index
|
||||
split_layer_names = list(self.split_spec.keys())
|
||||
sublayer_names = [name for name, _ in model.named_sublayers()]
|
||||
# Mark which layer is the next pipeline stage
|
||||
pipeline_layer_mark = [0 for _ in range(len(sublayer_names))]
|
||||
for split_layer_name in split_layer_names:
|
||||
split_point = self.split_spec[split_layer_name]
|
||||
index = sublayer_names.index(split_layer_name)
|
||||
if split_point == SplitPoint.END:
|
||||
is_valid = False
|
||||
for i in range(index + 1, len(sublayer_names)):
|
||||
if not sublayer_names[i].startswith(split_layer_name):
|
||||
pipeline_layer_mark[i] = 1
|
||||
is_valid = True
|
||||
break
|
||||
assert is_valid, (
|
||||
f"the last layer:{split_layer_name} must not be SplitPoint.END, please check the split_spec"
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"SplitPoint.BEGINNING is not supported currently"
|
||||
)
|
||||
pipeline_layer_mark[index] = 1
|
||||
# the inclusiveSum of pipeline_layer_mark is the pipeline stage index
|
||||
pipeline_stage_index = list(itertools.accumulate(pipeline_layer_mark))
|
||||
for index, (name, layer) in enumerate(model.named_sublayers()):
|
||||
layer.pipeline_stage_index = pipeline_stage_index[index]
|
||||
|
||||
# step2: insert reshard
|
||||
for name in split_layer_names:
|
||||
layer = self.get_layer_by_name(name)
|
||||
split_point = self.split_spec[name]
|
||||
layer.split_point = split_point
|
||||
if split_point == SplitPoint.END:
|
||||
layer.register_forward_post_hook(forward_post_hook)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
"SplitPoint.BEGINNING is not supported currently"
|
||||
)
|
||||
layer.register_forward_pre_hook(forward_pre_hook)
|
||||
|
||||
if self.global_spec:
|
||||
self.process_global_mesh_layers()
|
||||
|
||||
return model
|
||||
|
||||
def process_global_mesh_layers(self):
|
||||
g_mesh = fleet.auto.get_mesh()
|
||||
g_mesh = g_mesh.get_mesh_with_dim("pp")
|
||||
|
||||
def forward_post_hook(layer, input, output):
|
||||
if isinstance(output, (list, tuple)):
|
||||
global_output = list(output)
|
||||
for ind in range(len(global_output)):
|
||||
output_i = global_output[ind]
|
||||
if is_tensor(output_i):
|
||||
if output_i.is_dist():
|
||||
global_output[ind] = dist.reshard(
|
||||
output_i,
|
||||
g_mesh,
|
||||
[
|
||||
dist.Replicate()
|
||||
for _ in range(len(g_mesh._shape))
|
||||
],
|
||||
)
|
||||
else:
|
||||
global_output[ind] = dist.shard_tensor(
|
||||
output_i,
|
||||
g_mesh,
|
||||
[
|
||||
dist.Replicate()
|
||||
for _ in range(len(g_mesh._shape))
|
||||
],
|
||||
)
|
||||
|
||||
if isinstance(output, tuple):
|
||||
global_output = tuple(global_output)
|
||||
return global_output
|
||||
elif is_tensor(output):
|
||||
if output.is_dist():
|
||||
return dist.reshard(
|
||||
output,
|
||||
g_mesh,
|
||||
[dist.Replicate() for _ in range(len(g_mesh._shape))],
|
||||
)
|
||||
else:
|
||||
return dist.shard_tensor(
|
||||
output,
|
||||
g_mesh,
|
||||
[dist.Replicate() for _ in range(len(g_mesh._shape))],
|
||||
)
|
||||
else:
|
||||
raise TypeError(
|
||||
"layer output can only be tensor or list/tuple of tensor"
|
||||
)
|
||||
|
||||
def forward_pre_hook(layer, args, kwargs):
|
||||
pp_idx = getattr(layer, "pipeline_stage_index", 0)
|
||||
new_args = []
|
||||
new_kwargs = {}
|
||||
|
||||
def reshard_not_mesh_match_tensor(arg):
|
||||
cur_pp_mesh = self.get_mesh(pp_idx)
|
||||
if (
|
||||
arg is not None
|
||||
and is_tensor(arg)
|
||||
and arg.is_dist()
|
||||
and arg.process_mesh != cur_pp_mesh
|
||||
):
|
||||
return dist.reshard(
|
||||
arg,
|
||||
cur_pp_mesh,
|
||||
[dist.Replicate(), dist.Replicate()],
|
||||
)
|
||||
return arg
|
||||
|
||||
for arg in args:
|
||||
new_args.append(reshard_not_mesh_match_tensor(arg))
|
||||
|
||||
for key, arg in kwargs.items():
|
||||
new_kwargs[key] = reshard_not_mesh_match_tensor(arg)
|
||||
|
||||
return (tuple(new_args), new_kwargs)
|
||||
|
||||
# wa because of pir in vpp mode send receive bug
|
||||
for layer_name in self.global_spec:
|
||||
layer = self.get_layer_by_name(layer_name)
|
||||
layer.register_forward_post_hook(forward_post_hook)
|
||||
|
||||
if self.pipeline_layers is not None:
|
||||
for layer_name in self.pipeline_layers:
|
||||
layer = self.get_layer_by_name(layer_name)
|
||||
layer.register_forward_pre_hook(
|
||||
forward_pre_hook, with_kwargs=True
|
||||
)
|
||||
else:
|
||||
for layer in self.name_to_layer.values():
|
||||
layer.register_forward_pre_hook(
|
||||
forward_pre_hook, with_kwargs=True
|
||||
)
|
||||
|
||||
|
||||
def pipeline_parallel(model, optimizer=None, config=None):
|
||||
"""
|
||||
pipeline_parallel converts model and optimizer to pipelined distributed model
|
||||
|
||||
Args:
|
||||
model (paddle.nn.Layer): A single card model to be distributed
|
||||
optimizer (paddle.optimizer.Optimizer): An optimizer to be distributed
|
||||
config (dict): {
|
||||
"split_spec": OrderedDict|dict|str|list(str), The pipeline parallel split point.
|
||||
if split_spec is a string or list, such as "llama.layer" or ["llama.layerA", "llama.layerB"], Then the layer with same prefix a will be divided equally according to the size of pipeline degree.
|
||||
if split_spec is a OrderedDict|dict, key is the layer name, and the value is the split position that can be SplitPoint.BEGINNING or SplitPoint.END, the order of the keys is the order of the pipeline stage.
|
||||
NOTE: dict is also ordered after python3.7, so use dict at this time.
|
||||
"global_spec": str|list(str), make the output tensor of specific layers on global mesh.
|
||||
}
|
||||
|
||||
Returns:
|
||||
PipelineParallel: a distributed model
|
||||
ParallelOptimizer: a distributed optimizer
|
||||
"""
|
||||
|
||||
split_spec = config.get("split_spec")
|
||||
if split_spec is None:
|
||||
logging.warning("No split_spec, pipeline parallel won't do anything.")
|
||||
return model, optimizer
|
||||
|
||||
mesh = fleet.auto.get_mesh()
|
||||
assert mesh is not None, (
|
||||
"global mesh must not be None, please call fleet.auto.set_mesh(global_mesh) firstly"
|
||||
)
|
||||
assert "pp" in mesh.dim_names, (
|
||||
"pp must in the mesh dim_names when use pipeline_parallel"
|
||||
)
|
||||
|
||||
global_spec = config.get("global_spec")
|
||||
if isinstance(split_spec, str):
|
||||
split_spec = [split_spec]
|
||||
|
||||
matched_layer_name = None
|
||||
if isinstance(split_spec, (list, tuple)):
|
||||
# match layer_name with split_spec following by a dot and numbers and no other characters
|
||||
# such as split_spec = ["llama.layer"], then llama.layer.0 is matched, llama.layer.0.mlp is not matched
|
||||
patterns = [rf"{prefix}\.\d+$" for prefix in split_spec]
|
||||
|
||||
def is_match(layer_name):
|
||||
for pattern in patterns:
|
||||
if re.match(pattern, layer_name) or layer_name in split_spec:
|
||||
return True
|
||||
return False
|
||||
|
||||
def filter_matched_layer(matched_layer_name):
|
||||
# remove the base name if it has a numbered suffix
|
||||
string_set = set(matched_layer_name)
|
||||
to_remove = set()
|
||||
|
||||
numbered_pattern = re.compile(r'^(.+)\.\d+$')
|
||||
for s in matched_layer_name:
|
||||
match = numbered_pattern.match(s)
|
||||
if match:
|
||||
base_name = match.group(1)
|
||||
if base_name in string_set:
|
||||
to_remove.add(base_name)
|
||||
|
||||
res = []
|
||||
for s in matched_layer_name:
|
||||
if s not in to_remove:
|
||||
res.append(s)
|
||||
return res
|
||||
|
||||
matched_layer_name = [
|
||||
name for name, _ in model.named_sublayers() if is_match(name)
|
||||
]
|
||||
matched_layer_name = filter_matched_layer(matched_layer_name)
|
||||
pp_size = mesh.get_dim_size("pp")
|
||||
layer_num = len(matched_layer_name)
|
||||
assert layer_num > 0, (
|
||||
"No layer match the split_spec, please check its correctness"
|
||||
)
|
||||
assert layer_num >= pp_size, (
|
||||
"The number of layers must not be less than the pp size"
|
||||
)
|
||||
if layer_num % pp_size != 0:
|
||||
logger.warning(
|
||||
f"The number of layers({layer_num}) must be divisible by the pp size({pp_size}), but got {layer_num} and {pp_size}"
|
||||
)
|
||||
|
||||
def divide_list_indices(n, k):
|
||||
base_size = n // k
|
||||
extra = n % k
|
||||
|
||||
indices = []
|
||||
current_index = -1
|
||||
|
||||
for i in range(k - 1):
|
||||
current_index += base_size
|
||||
if i < extra:
|
||||
current_index += 1
|
||||
indices.append(current_index)
|
||||
return indices
|
||||
|
||||
indices = divide_list_indices(layer_num, pp_size)
|
||||
split_spec_dict = OrderedDict(
|
||||
[
|
||||
(matched_layer_name[indices[i]], SplitPoint.END)
|
||||
for i in range(pp_size - 1)
|
||||
]
|
||||
)
|
||||
else:
|
||||
layers_per_rank = layer_num // pp_size
|
||||
split_spec_dict = OrderedDict(
|
||||
[
|
||||
(
|
||||
matched_layer_name[i * layers_per_rank - 1],
|
||||
SplitPoint.END,
|
||||
)
|
||||
for i in range(1, pp_size)
|
||||
]
|
||||
)
|
||||
else:
|
||||
sublayer_names = [name for name, _ in model.named_sublayers()]
|
||||
split_spec_dict = split_spec
|
||||
for key, value in split_spec_dict.items():
|
||||
assert key in sublayer_names, (
|
||||
f"wrong split layer, expected one of {sublayer_names}"
|
||||
)
|
||||
assert value is SplitPoint.END, "not supported split point at now."
|
||||
|
||||
if global_spec:
|
||||
if isinstance(global_spec, str):
|
||||
global_spec = [global_spec]
|
||||
else:
|
||||
assert isinstance(global_spec, (list, tuple)), (
|
||||
f"global_spec can only be list or list(str), but got:{type(global_spec)}"
|
||||
)
|
||||
|
||||
logger.info(
|
||||
f"split_spec_dict: {split_spec_dict}, global_spec: {global_spec}, matched_layer_name: {matched_layer_name}"
|
||||
)
|
||||
|
||||
model = PipelineParallel(
|
||||
model, split_spec_dict, global_spec, matched_layer_name
|
||||
)
|
||||
if optimizer is not None:
|
||||
optimizer = ParallelOptimizer(optimizer)
|
||||
|
||||
return model, optimizer
|
||||
@@ -0,0 +1,88 @@
|
||||
# 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 paddle.distributed import fleet
|
||||
|
||||
from .parallel_base import ParallelModel, ParallelOptimizer
|
||||
|
||||
|
||||
class ShardedDataParallel(ParallelModel):
|
||||
"""
|
||||
ShardedDataParallel converts a single card model to a distributed data parallel model
|
||||
|
||||
Args:
|
||||
model (paddle.nn.Layer): A single card model to be distributed.
|
||||
optimizer (paddle.optimizer.Optimizer): an optimizer to be distributed.
|
||||
level (str): Zero stage, can be the following values:
|
||||
0: no sharding (pure dp)
|
||||
1: Zero Stage1
|
||||
2: Zero Stage2
|
||||
3: Zero Stage3
|
||||
Default: None, which means optimizer is replicated among all process.
|
||||
offload (bool): whether enable cpu offload strategy, not implemented currently.
|
||||
exclude_layer (list): Specify which layers do not use the zero stage strategy, not implemented currently.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model,
|
||||
offload=False,
|
||||
exclude_layer=None,
|
||||
):
|
||||
super().__init__(model)
|
||||
assert offload is False
|
||||
assert exclude_layer is None
|
||||
|
||||
self.sharding_parallelizer = self.sharding_parallelizer_func
|
||||
|
||||
def sharding_parallelizer_func(self, model):
|
||||
return model
|
||||
|
||||
|
||||
def sharded_data_parallel(model, optimizer=None, config=None):
|
||||
"""
|
||||
sharded_data_parallel converts model and optimizer to distributed and supports set zero stage1/2/3
|
||||
|
||||
Args:
|
||||
model (paddle.nn.Layer): A single card model to be distributed
|
||||
optimizer (paddle.optimizer.Optimizer): an optimizer to be distributed
|
||||
config (dict): {
|
||||
"sharding_level": 0,
|
||||
"offload": False,
|
||||
"exclude_layer": None,
|
||||
"sharding_mesh_dim": "dp",
|
||||
}
|
||||
|
||||
Returns:
|
||||
ShardedDataParallel: a distributed model
|
||||
ParallelOptimizer: a distributed optimizer
|
||||
"""
|
||||
sdp_model = ShardedDataParallel(
|
||||
model, bool(config.get('offload')), config.get('exclude_layer')
|
||||
)
|
||||
if optimizer is not None:
|
||||
level = config.get('sharding_level')
|
||||
sharding_mesh_dim = config.get('sharding_mesh_dim', "dp")
|
||||
optimizer = ParallelOptimizer(optimizer, level, sharding_mesh_dim)
|
||||
|
||||
# check global_mesh
|
||||
mesh = fleet.auto.get_mesh()
|
||||
assert mesh is not None, (
|
||||
"global mesh must not be None, please call fleet.auto.set_mesh(global_mesh) firstly"
|
||||
)
|
||||
assert "dp" in mesh.dim_names, (
|
||||
"dp must in the mesh dim_names when use sharded_data_parallel"
|
||||
)
|
||||
return sdp_model, optimizer
|
||||
@@ -0,0 +1,954 @@
|
||||
# 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 logging
|
||||
import re
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
|
||||
from .parallel_base import ParallelModel, ParallelOptimizer, is_tensor
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from paddle import Tensor
|
||||
from paddle.distributed import ProcessMesh
|
||||
from paddle.nn import Layer
|
||||
|
||||
|
||||
def c_split(x, process_mesh, need_transpose, split_type="sp"):
|
||||
mp_index = process_mesh.dim_names.index('mp') # get the axis for the split
|
||||
dp_index = process_mesh.dim_names.index('dp')
|
||||
if isinstance(x, tuple):
|
||||
target_x = x[0]
|
||||
else:
|
||||
target_x = x
|
||||
assert is_tensor(target_x)
|
||||
assert len(target_x.shape) == 3
|
||||
if need_transpose:
|
||||
target_x = paddle.transpose(target_x, perm=[1, 0, 2])
|
||||
placements = target_x.placements
|
||||
if placements is None:
|
||||
placements = [dist.Replicate() for _ in range(len(process_mesh.shape))]
|
||||
if split_type == "sp":
|
||||
if placements[dp_index] == dist.Shard(0):
|
||||
# NOTE(zhangwl):if shard(0) , input shape should be [b,s,h]
|
||||
split_dims = dist.Shard(1)
|
||||
elif placements[dp_index] == dist.Shard(1):
|
||||
# NOTE(zhangwl):if shard(1) , input shape should be [s,b,h]
|
||||
split_dims = dist.Shard(0)
|
||||
else:
|
||||
logging.warning(
|
||||
f"parallel api don't know {target_x.shape} which dimension is batch, default is to cut to the 0th dimension"
|
||||
)
|
||||
split_dims = dist.Shard(0)
|
||||
elif split_type == "mp":
|
||||
split_dims = dist.Shard(2) # split h [b,s,h]
|
||||
else:
|
||||
raise ValueError(f"Unsupported split type {split_type}")
|
||||
|
||||
placements[mp_index] = split_dims
|
||||
target_x = dist.reshard(target_x, process_mesh, placements)
|
||||
if isinstance(x, tuple):
|
||||
x = list(x)
|
||||
x[0] = target_x
|
||||
x = tuple(x)
|
||||
else:
|
||||
x = target_x
|
||||
|
||||
return x
|
||||
|
||||
|
||||
def c_concat(x, process_mesh, need_transpose):
|
||||
index = process_mesh.dim_names.index('mp') # get the axis for the split
|
||||
if isinstance(x, tuple):
|
||||
target_x = x[0]
|
||||
else:
|
||||
target_x = x
|
||||
assert is_tensor(target_x)
|
||||
assert len(target_x.shape) == 3
|
||||
placements = target_x.placements
|
||||
if placements is None:
|
||||
placements = [dist.Replicate() for _ in range(len(process_mesh.shape))]
|
||||
placements[index] = dist.Replicate()
|
||||
target_x = dist.reshard(target_x, process_mesh, placements)
|
||||
if need_transpose:
|
||||
target_x = paddle.transpose(target_x, perm=[1, 0, 2])
|
||||
if isinstance(x, tuple):
|
||||
x = list(x)
|
||||
x[0] = target_x
|
||||
x = tuple(x)
|
||||
else:
|
||||
x = target_x
|
||||
|
||||
return x
|
||||
|
||||
|
||||
class PlanBase:
|
||||
def __init__(self):
|
||||
self.share_param_list = {}
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
raise NotImplementedError("Don't call the PlanBase directly.")
|
||||
|
||||
|
||||
class ColWiseParallel(PlanBase):
|
||||
"""
|
||||
Col wise parallel plan for mp config.
|
||||
Will try to split weight on the second dim and the bias on the first dim.
|
||||
This api is designed for paddle.nn.Linear or paddle.nn.Embedding.
|
||||
If any other instance of paddle.nn.Layer is passed,
|
||||
this plan will try to split `layer.weight` and `layer.bias` if it has.
|
||||
|
||||
Note:
|
||||
1. `layer.weight` should have two dims.
|
||||
2. `layer.bias` should have one dim.
|
||||
|
||||
Args:
|
||||
gather_output (bool): Whether gather the output to change it from a local tensor to a global tensor.
|
||||
If gather the local tensor to global, an extra communication will be called.
|
||||
The default value is `False`, which means keeping the output as a local tensor.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.ColWiseParallel(),
|
||||
... }
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, gather_output: bool = False) -> None:
|
||||
super().__init__()
|
||||
self.gather_output = gather_output
|
||||
|
||||
def gather_output_hook(self, process_mesh):
|
||||
def gather_hook(layer, input, output):
|
||||
assert output is not None
|
||||
return c_concat(output, process_mesh, False)
|
||||
|
||||
return gather_hook
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
index = process_mesh.dim_names.index('mp') # get the axis for the split
|
||||
size = len(process_mesh.shape)
|
||||
placement = [dist.Replicate() for _ in range(size)]
|
||||
param_placements = {}
|
||||
assert isinstance(layer, paddle.nn.Layer)
|
||||
if not isinstance(layer, (paddle.nn.Linear, paddle.nn.Embedding)):
|
||||
logging.warning(
|
||||
f"ColWiseParallel is designed to handle Linear and Embedding. "
|
||||
f"But got {layer.__class__.__name__}. "
|
||||
f"Will try to shard weight and bias if the layer contains one."
|
||||
)
|
||||
shard_param_list = set(shard_param_list)
|
||||
if len(shard_param_list) == 0:
|
||||
shard_param_list.add("weight")
|
||||
shard_param_list.add("bias")
|
||||
|
||||
def shard_param(param_name):
|
||||
if (
|
||||
hasattr(layer, param_name)
|
||||
and getattr(layer, param_name) is not None
|
||||
):
|
||||
layer_param = getattr(layer, param_name)
|
||||
|
||||
if layer_param.is_dist():
|
||||
return
|
||||
|
||||
if len(layer_param.shape) == 2:
|
||||
placement[index] = dist.Shard(1)
|
||||
elif len(layer_param.shape) == 1:
|
||||
placement[index] = dist.Shard(0)
|
||||
else:
|
||||
raise ValueError(f"{layer_param} should have 1 or 2 dims.")
|
||||
# NOTE(zhangweilong):for share parameter, the parameter should be handled uniformly in the end
|
||||
if (
|
||||
self.share_param_list is not None
|
||||
and layer_param.name in self.share_param_list
|
||||
and self.share_param_list[layer_param.name] > 1
|
||||
):
|
||||
param_placements.update({param_name: placement})
|
||||
else:
|
||||
layer_param = dist.shard_tensor(
|
||||
layer_param,
|
||||
process_mesh,
|
||||
placement,
|
||||
)
|
||||
setattr(layer, param_name, layer_param)
|
||||
|
||||
for param_name in shard_param_list:
|
||||
shard_param(param_name)
|
||||
if self.gather_output:
|
||||
layer.register_forward_post_hook(
|
||||
self.gather_output_hook(process_mesh)
|
||||
)
|
||||
return param_placements
|
||||
|
||||
|
||||
class RowWiseParallel(PlanBase):
|
||||
"""
|
||||
Row wise parallel plan for mp config.
|
||||
Will try to split weight on the first dim.
|
||||
This api is designed for paddle.nn.Linear or paddle.nn.Embedding.
|
||||
If any other instance of paddle.nn.Layer is passed, this plan will try to split `layer.weight` if it has.
|
||||
|
||||
Note:
|
||||
`layer.weight` should have two dims.
|
||||
|
||||
Args:
|
||||
is_input_parallel (bool): Whether the input is a local tensor or a global tensor. If the input is a
|
||||
global tensor, an extra split will be called. The default value is `True`,
|
||||
which means the input is a local tensor.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.RowWiseParallel(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, is_input_parallel: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.is_input_parallel = is_input_parallel
|
||||
|
||||
def split_input_hook(self, process_mesh):
|
||||
def split_hook(layer, input):
|
||||
return c_split(input, process_mesh, False, split_type="mp")
|
||||
|
||||
return split_hook
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
index = process_mesh.dim_names.index('mp') # get the axis for the split
|
||||
size = len(process_mesh.shape)
|
||||
placement = [dist.Replicate() for _ in range(size)]
|
||||
placement[index] = dist.Shard(0)
|
||||
param_placements = {}
|
||||
assert isinstance(layer, paddle.nn.Layer)
|
||||
if not isinstance(layer, (paddle.nn.Linear, paddle.nn.Embedding)):
|
||||
logging.warning(
|
||||
f"RowWiseParallel is designed to handle Linear and Embedding. "
|
||||
f"But got {layer.__class__.__name__}. "
|
||||
f"Will try to shard weight if the layer contains one."
|
||||
)
|
||||
shard_param_list = set(shard_param_list)
|
||||
shard_param_list.discard("bias")
|
||||
if len(shard_param_list) == 0:
|
||||
shard_param_list.add("weight")
|
||||
|
||||
def shard_param(param_name):
|
||||
if (
|
||||
hasattr(layer, param_name)
|
||||
and getattr(layer, param_name) is not None
|
||||
):
|
||||
layer_param = getattr(layer, param_name)
|
||||
if layer_param.is_dist():
|
||||
return
|
||||
if len(layer_param.shape) != 2:
|
||||
raise ValueError(f"{layer_param} should have 2 dims.")
|
||||
# NOTE(zhangweilong):for share parameter, the parameter should be handled uniformly in the end
|
||||
if (
|
||||
self.share_param_list is not None
|
||||
and layer_param.name in self.share_param_list
|
||||
and self.share_param_list[layer_param.name] > 1
|
||||
):
|
||||
param_placements.update({param_name: placement})
|
||||
else:
|
||||
layer_param = dist.shard_tensor(
|
||||
layer_param,
|
||||
process_mesh,
|
||||
placement,
|
||||
)
|
||||
setattr(layer, param_name, layer_param)
|
||||
|
||||
for param_name in shard_param_list:
|
||||
shard_param(param_name)
|
||||
if not self.is_input_parallel:
|
||||
layer.register_forward_pre_hook(self.split_input_hook(process_mesh))
|
||||
return param_placements
|
||||
|
||||
|
||||
class PrepareLayerInput(PlanBase):
|
||||
"""
|
||||
Prepare the input of specific layer. User should provide one callable function.
|
||||
|
||||
Args:
|
||||
fn (callable): A function that prepare the layer input. The function should take exactly
|
||||
one parameter named `process_mesh` and return the pre hook.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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 layer_input_hook(process_mesh):
|
||||
... def hook(layer, input, output):
|
||||
... return input
|
||||
...
|
||||
... return hook
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.PrepareLayerOutput(layer_input_hook),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fn: (
|
||||
Callable[
|
||||
[ProcessMesh],
|
||||
Callable[
|
||||
[Layer, tuple[Tensor], tuple[Tensor]], [tuple[Tensor]]
|
||||
],
|
||||
]
|
||||
| None
|
||||
) = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
assert callable(fn)
|
||||
self.fn = fn
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_pre_hook(self.fn(process_mesh=process_mesh))
|
||||
|
||||
|
||||
class PrepareLayerOutput(PlanBase):
|
||||
"""
|
||||
Prepare the output of specific layer. User should provide one callable function.
|
||||
|
||||
Args:
|
||||
fn (callable): A function that prepare the layer input. The function should take exactly
|
||||
one parameter named `process_mesh` and return the post hook.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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 layer_output_hook(process_mesh):
|
||||
... def hook(layer, input, output):
|
||||
... return output
|
||||
...
|
||||
... return hook
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> layer = MLP()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.PrepareLayerOutput(layer_output_hook),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
fn: (
|
||||
Callable[
|
||||
[ProcessMesh],
|
||||
Callable[
|
||||
[Layer, tuple[Tensor], tuple[Tensor]], [tuple[Tensor]]
|
||||
],
|
||||
]
|
||||
| None
|
||||
) = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
assert callable(fn)
|
||||
self.fn = fn
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_post_hook(self.fn(process_mesh=process_mesh))
|
||||
|
||||
|
||||
class SequenceParallelBegin(PlanBase):
|
||||
"""
|
||||
Sequence parallel plan for mp config.
|
||||
This plan marks the beginning of the sp and should be added to the LAST layer before the sp range.
|
||||
|
||||
Note:
|
||||
DON'T mark any layer in the sp range.
|
||||
|
||||
Args:
|
||||
need_transpose (bool): the default value is `True`. With `need_transpose=True`, this plan will transfer
|
||||
the output from [b, s, h] to [s/mp, b, h]. With `need_transpose=False`, this plan will transfer
|
||||
the output from [s, b, h] to [s/mp, b, h].
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.SequenceParallelBegin(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, need_transpose: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.need_transpose = need_transpose
|
||||
|
||||
def sequence_parallel_begin(self, process_mesh):
|
||||
def begin(layer, input, output):
|
||||
assert output is not None
|
||||
return c_split(output, process_mesh, self.need_transpose)
|
||||
|
||||
return begin
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_post_hook(
|
||||
self.sequence_parallel_begin(process_mesh)
|
||||
)
|
||||
|
||||
|
||||
class SequenceParallelEnd(PlanBase):
|
||||
"""
|
||||
Sequence parallel plan for mp config.
|
||||
This plan marks the ending of the sp and should be added to the FIRST layer after the sp range.
|
||||
|
||||
Note:
|
||||
DON'T mark any layer in the sp range.
|
||||
|
||||
Args:
|
||||
need_transpose (bool): the default value is `True`. With `need_transpose=True`, this plan will transfer
|
||||
the input from [s/mp, b, h] to [b, s, h]. With `need_transpose=False`, this plan will transfer the
|
||||
input from [s/mp, b, h] to [s, b, h].
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.SequenceParallelEnd(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, need_transpose: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.need_transpose = need_transpose
|
||||
|
||||
def sequence_parallel_end(self, process_mesh):
|
||||
def end(layer, input, output=None):
|
||||
assert input is not None
|
||||
return c_concat(input, process_mesh, self.need_transpose)
|
||||
|
||||
return end
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_pre_hook(
|
||||
self.sequence_parallel_end(process_mesh)
|
||||
)
|
||||
|
||||
|
||||
class SequenceParallelEnable(PlanBase):
|
||||
"""
|
||||
Sequence parallel plan for mp config.
|
||||
Do sequence parallel on the layer. Note the input should be in [b, s, h] format.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.SequenceParallelEnable(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
def sequence_parallel_begin(self, process_mesh):
|
||||
def begin(layer, input, output=None):
|
||||
assert input is not None
|
||||
return c_split(input, process_mesh, True)
|
||||
|
||||
return begin
|
||||
|
||||
def sequence_parallel_end(self, process_mesh):
|
||||
def end(layer, input, output):
|
||||
assert output is not None
|
||||
return c_concat(output, process_mesh, True)
|
||||
|
||||
return end
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
logging.warning(
|
||||
"Sequence parallel with the usage of SequenceParallel may not reach the best throughput. "
|
||||
"Try to use SequenceParallelBegin/End to achieve better performance"
|
||||
)
|
||||
layer.register_forward_pre_hook(
|
||||
self.sequence_parallel_begin(process_mesh)
|
||||
)
|
||||
layer.register_forward_post_hook(
|
||||
self.sequence_parallel_end(process_mesh)
|
||||
)
|
||||
|
||||
|
||||
class SequenceParallelDisable(PlanBase):
|
||||
"""
|
||||
Sequence parallel plan for mp config.
|
||||
Disable sequence parallel on the layer.
|
||||
|
||||
Args:
|
||||
need_transpose (bool): the default value is `True`. If the need_transpose is `True`: this plan will transfer
|
||||
the input from [s/mp, b, h] to [b, s, h] and then transfer the output from [b, s, h] to [s/mp, b, h].
|
||||
If the need_transpose is `False`: this plan will transfer the input from [s/mp, b, h] to [s, b, h] and
|
||||
then transfer the output from [s, b, h] to [s/mp, b, h].
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> 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()
|
||||
>>> mp_config = {
|
||||
... 'fc1': dist.SequenceParallelDisable(),
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self, need_transpose: bool = True) -> None:
|
||||
super().__init__()
|
||||
self.need_transpose = need_transpose
|
||||
|
||||
def sequence_parallel_begin(self, process_mesh):
|
||||
def begin(layer, input, output=None):
|
||||
return c_split(output, process_mesh, self.need_transpose)
|
||||
|
||||
return begin
|
||||
|
||||
def sequence_parallel_end(self, process_mesh):
|
||||
def end(layer, input, output=None):
|
||||
return c_concat(input, process_mesh, self.need_transpose)
|
||||
|
||||
return end
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_pre_hook(
|
||||
self.sequence_parallel_end(process_mesh)
|
||||
)
|
||||
|
||||
layer.register_forward_post_hook(
|
||||
self.sequence_parallel_begin(process_mesh)
|
||||
)
|
||||
|
||||
|
||||
class ConvParallel(PlanBase):
|
||||
"""
|
||||
A strategy for enabling spatial parallelism on ``paddle.nn.Conv2D`` layers
|
||||
by sharding the input tensor along its Width (W) dimension.
|
||||
|
||||
When this ``ConvParallel`` configuration is applied to a ``Conv2D`` layer,
|
||||
the layer's input tensor will have its width dimension split across devices
|
||||
in the model parallel group. This can help reduce memory usage from activations,
|
||||
especially when dealing with inputs that have a large width.
|
||||
|
||||
To enable width-wise input sharding correctly, make sure your `Conv2D` layer
|
||||
satisfies the following conditions along the width dimension:
|
||||
|
||||
- **Dilation** must be set to `1`.
|
||||
- **If no width padding is used:**
|
||||
- The input width must be evenly divisible by the stride width.
|
||||
- The stride width must be equal to the kernel width.
|
||||
- **If width padding is used:**
|
||||
- The stride width must be `1`.
|
||||
- The total input width must be at least half the kernel width.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.nn as nn
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> class SimpleConvNet(nn.Layer):
|
||||
... def __init__(self, data_format="NCHW"):
|
||||
... super().__init__()
|
||||
... self.conv1 = nn.Conv2D(
|
||||
... 3,
|
||||
... 8,
|
||||
... kernel_size=3,
|
||||
... padding=1,
|
||||
... data_format=data_format,
|
||||
... )
|
||||
... self.relu = nn.ReLU()
|
||||
...
|
||||
... def forward(self, x):
|
||||
... x = self.conv1(x)
|
||||
... return self.relu(x)
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> model = SimpleConvNet(data_format="NCHW")
|
||||
>>> mp_config = {
|
||||
... "parallelize_plan": {
|
||||
... "conv1": dist.ConvParallel(),
|
||||
... },
|
||||
... }
|
||||
"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
|
||||
@staticmethod
|
||||
def _is_supported(
|
||||
input_size,
|
||||
kernel_size,
|
||||
stride,
|
||||
padding,
|
||||
dilation,
|
||||
data_format,
|
||||
mp_group_size,
|
||||
):
|
||||
idx_w_input = -1
|
||||
idx_w_kernel = -1
|
||||
|
||||
if data_format == "NCHW":
|
||||
idx_w_input = 3
|
||||
idx_w_kernel = 3
|
||||
elif data_format == "NHWC":
|
||||
idx_w_input = 2
|
||||
idx_w_kernel = 3
|
||||
else:
|
||||
return False
|
||||
|
||||
if input_size[idx_w_input] % mp_group_size != 0:
|
||||
return False
|
||||
|
||||
dilation_w = dilation[1]
|
||||
padding_w = padding[1]
|
||||
stride_w = stride[1]
|
||||
|
||||
input_w = input_size[idx_w_input]
|
||||
kernel_w = kernel_size[idx_w_kernel]
|
||||
|
||||
if dilation_w != 1:
|
||||
# RingConv2d only supports dilation=1.
|
||||
# Larger dilation would require enlarged halo regions and more complex communication.
|
||||
return False
|
||||
|
||||
if padding_w == 0:
|
||||
# To avoid halo exchange when padding=0, we require:
|
||||
# - input_w must be divisible by stride_w, so partitions align evenly across ranks.
|
||||
# - stride_w == kernel_w, so each kernel operates on disjoint local regions.
|
||||
if input_w % stride_w != 0:
|
||||
return False
|
||||
if stride_w != kernel_w:
|
||||
return False
|
||||
|
||||
else:
|
||||
# When padding > 0, halo exchange is needed.
|
||||
# To simplify halo logic, we require:
|
||||
# - stride_w == 1: ensures each output element is computed from overlapping input,
|
||||
# and no input region is skipped, simplifying halo construction.
|
||||
# - kernel_w // 2 <= input_w: prevents the kernel from exceeding local input.
|
||||
if stride_w != 1:
|
||||
return False
|
||||
if kernel_w // 2 > input_w:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def conv_parallel_start(self, process_mesh, data_format):
|
||||
def start(layer, input, output=None):
|
||||
if data_format == "NCHW":
|
||||
shard_w_dim = 3
|
||||
elif data_format == "NHWC":
|
||||
shard_w_dim = 2
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported data_format: {data_format}. "
|
||||
"Only NCHW and NHWC are supported."
|
||||
)
|
||||
|
||||
if isinstance(input, tuple):
|
||||
x = input[0]
|
||||
else:
|
||||
x = input
|
||||
|
||||
placements = x.placements
|
||||
mp_index = process_mesh.dim_names.index('mp')
|
||||
mp_group_size = process_mesh.get_dim_size('mp')
|
||||
|
||||
# Note(luchang): for intermediate api, when this ConvLayer is
|
||||
# not supported, we just skip apply parallelization.
|
||||
if not ConvParallel._is_supported(
|
||||
x.shape,
|
||||
layer.weight.shape,
|
||||
layer._stride,
|
||||
layer._updated_padding,
|
||||
layer._dilation,
|
||||
data_format,
|
||||
mp_group_size,
|
||||
):
|
||||
return input
|
||||
|
||||
if placements is None:
|
||||
placements = [
|
||||
dist.Replicate() for _ in range(len(process_mesh.shape))
|
||||
]
|
||||
if placements[mp_index] == dist.Shard(shard_w_dim):
|
||||
return input
|
||||
|
||||
placements[mp_index] = dist.Shard(shard_w_dim)
|
||||
|
||||
if not x.is_dist():
|
||||
x = dist.shard_tensor(x, process_mesh, placements)
|
||||
else:
|
||||
x = dist.reshard(x, process_mesh, placements)
|
||||
|
||||
if isinstance(input, tuple):
|
||||
input = list(input)
|
||||
input[0] = x
|
||||
input = tuple(input)
|
||||
else:
|
||||
input = x
|
||||
return input
|
||||
|
||||
return start
|
||||
|
||||
def apply(self, layer, process_mesh, shard_param_list):
|
||||
layer.register_forward_pre_hook(
|
||||
self.conv_parallel_start(process_mesh, layer._data_format)
|
||||
)
|
||||
|
||||
|
||||
class TensorParallel(ParallelModel):
|
||||
def __init__(self, model, parallelize_plan=None):
|
||||
super().__init__(model)
|
||||
if parallelize_plan is not None:
|
||||
assert isinstance(parallelize_plan, dict)
|
||||
for key, plan in parallelize_plan.items():
|
||||
assert isinstance(key, str), (
|
||||
"The key of the parallelize plan should be a string."
|
||||
)
|
||||
if not isinstance(plan, list):
|
||||
plan = [plan]
|
||||
for p in plan:
|
||||
assert isinstance(p, PlanBase), (
|
||||
"The value the the parallelize plan should be a instance of PlanBase or a list of PlanBase."
|
||||
)
|
||||
|
||||
self.global_mesh = dist.auto_parallel.get_mesh()
|
||||
self.parallelize_plan = parallelize_plan
|
||||
self.tp_parallelizer = self.tensor_parallelizer_fn
|
||||
|
||||
def match_layer(self, layer, name):
|
||||
# Match the layer to a plan.
|
||||
# Will return the plan if the layer hits one, otherwise return None.
|
||||
plans = []
|
||||
for key, plan in self.parallelize_plan.items():
|
||||
attr_name = key.split('.')[-1]
|
||||
shard_param_list = []
|
||||
# Find some plan for specific parameter, such as
|
||||
# "lm_head.weight": ColWiseParallel()
|
||||
# "qkv_proj.lora_A" ColWiseParallel()
|
||||
# if there is no plan for specific parameter, layer will be sharded by default: layer.weight and layer.bias
|
||||
if key.endswith(f".{attr_name}"):
|
||||
if hasattr(layer, attr_name) and is_tensor(
|
||||
getattr(layer, attr_name)
|
||||
):
|
||||
key = key.replace(f".{attr_name}", "")
|
||||
shard_param_list.append(attr_name)
|
||||
re_find = re.match(key, name)
|
||||
if key == name or (
|
||||
re_find is not None
|
||||
and int(re_find.end()) - int(re_find.start()) == len(name)
|
||||
):
|
||||
if isinstance(plan, PlanBase):
|
||||
plan = [plan]
|
||||
plans.append([plan, shard_param_list])
|
||||
return plans
|
||||
|
||||
def tensor_parallelizer_fn(self, model):
|
||||
if self.parallelize_plan is None:
|
||||
return
|
||||
layer_param_placements = {}
|
||||
share_param_list = {}
|
||||
for name, layer in model.named_sublayers():
|
||||
for param_name in list(layer._parameters.keys()):
|
||||
param = getattr(layer, param_name)
|
||||
if param.name not in share_param_list:
|
||||
share_param_list[param.name] = 1
|
||||
continue
|
||||
share_param_list[param.name] += 1
|
||||
for name, layer in model.named_sublayers():
|
||||
plans = self.match_layer(layer, name)
|
||||
layer_param_placements[layer] = {}
|
||||
if len(plans) > 0:
|
||||
pp_idx = getattr(layer, "pipeline_stage_index", 0)
|
||||
for plan in plans:
|
||||
real_plan, shard_param_list = plan
|
||||
for p in real_plan:
|
||||
p.share_param_list = share_param_list
|
||||
param_placements = p.apply(
|
||||
layer, self.get_mesh(pp_idx), shard_param_list
|
||||
)
|
||||
if param_placements is not None and param_placements:
|
||||
layer_param_placements[layer].update(
|
||||
param_placements
|
||||
)
|
||||
return model, layer_param_placements
|
||||
|
||||
|
||||
def tensor_parallel(model, optimizer=None, config=None):
|
||||
"""
|
||||
Tensor parallel.
|
||||
Args:
|
||||
model (paddle.nn.Layer): the model to be shard into tensor parallel.
|
||||
optimizer (paddle.optimizer.Optimizer): the optimizer.
|
||||
config (dict): {
|
||||
"parallelize_plan": dict, the plan to shard the layer.
|
||||
}
|
||||
Returns:
|
||||
model: model after tp
|
||||
optimizer: optimizer after tp
|
||||
|
||||
NOTE: the plan should be a dict maps layer name or parameter name to a split_plan,
|
||||
which will be used to split the layer or the parameter. The name can be written in regular format.
|
||||
|
||||
An example for the plan is:
|
||||
```
|
||||
plan = {
|
||||
"llama.embed_tokens": ColWiseParallel(),
|
||||
"llama.layers.*.self_attn.q_proj": ColWiseParallel(),
|
||||
"llama.layers.*.self_attn.k_proj": ColWiseParallel(),
|
||||
"llama.layers.*.self_attn.v_proj": ColWiseParallel(),
|
||||
"llama.layers.*.self_attn.o_proj": RowWiseParallel(),
|
||||
"llama.layers.*.mlp.gate_proj": ColWiseParallel(),
|
||||
"llama.layers.*.mlp.up_proj": ColWiseParallel(),
|
||||
"llama.layers.*.mlp.down_proj": RowWiseParallel(),
|
||||
"lm_head.weight": ColWiseParallel(),
|
||||
}
|
||||
```
|
||||
"""
|
||||
parallelize_plan = config.get("parallelize_plan")
|
||||
if parallelize_plan is None:
|
||||
# Do nothing if no plan.
|
||||
logging.warning(
|
||||
"No parallelize plan, tensor parallel won't do anything."
|
||||
)
|
||||
return model, optimizer
|
||||
|
||||
global_mesh = dist.auto_parallel.get_mesh()
|
||||
|
||||
assert global_mesh is not None, (
|
||||
"global mesh must not be None, please call fleet.auto.set_mesh(global_mesh) firstly"
|
||||
)
|
||||
assert "mp" in global_mesh.dim_names, (
|
||||
"mp must in the mesh dim_names when use tensor_parallel"
|
||||
)
|
||||
|
||||
model = TensorParallel(model, parallelize_plan)
|
||||
if optimizer is not None:
|
||||
optimizer = ParallelOptimizer(optimizer)
|
||||
|
||||
return model, optimizer
|
||||
@@ -0,0 +1,159 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.nn import Layer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from paddle.distributed import Placement, ProcessMesh
|
||||
|
||||
|
||||
class LocalLayer(Layer):
|
||||
"""
|
||||
The `LocalLayer` class is a specialized `Layer` for managing distributed tensors during
|
||||
forward and backward passes in a parallelized training environment. It converts distributed tensors
|
||||
to local tensors for computation and then back to distributed tensors as output, ensuring seamless
|
||||
integration with distributed parallelism frameworks.
|
||||
|
||||
Args:
|
||||
out_dist_attrs (list[tuple[ProcessMesh, list[Placement]]]):
|
||||
A list where each entry is a tuple containing the `ProcessMesh` and the list of `Placement`
|
||||
attributes for the corresponding output tensors. These attributes define the distribution
|
||||
strategy for the outputs.
|
||||
grad_dist_attrs (list[tuple[ProcessMesh, list[Placement]]]):
|
||||
Similar to `out_dist_attrs` but for gradient tensors. The tuple in the list can be None, indicating that the dist_attr of the gradient tensor is same as the corresponding input tensor.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from __future__ import annotations
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
>>> from paddle import Tensor
|
||||
>>> from paddle.distributed import ProcessMesh
|
||||
|
||||
>>> class CustomLayer(dist.LocalLayer):
|
||||
... def __init__(self, out_dist_attrs, grad_dist_attrs):
|
||||
... super().__init__(out_dist_attrs, grad_dist_attrs)
|
||||
... self.local_result = paddle.to_tensor(0.0)
|
||||
|
||||
... def forward(self, x):
|
||||
... mask = paddle.zeros_like(x)
|
||||
... if dist.get_rank() == 0:
|
||||
... mask[1:3] = 1
|
||||
... else:
|
||||
... mask[4:7] = 1
|
||||
|
||||
... x = x * mask
|
||||
... mask_sum = paddle.sum(x)
|
||||
... mask_sum = mask_sum / mask.sum()
|
||||
... self.local_result = mask_sum
|
||||
... return mask_sum
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> dist.init_parallel_env()
|
||||
>>> mesh = ProcessMesh([0, 1], dim_names=["x"])
|
||||
>>> dist_attrs = [
|
||||
... (mesh, [dist.Partial(dist.ReduceType.kRedSum)]),
|
||||
... ]
|
||||
>>> local_input = paddle.arange(0, 10, dtype="float32")
|
||||
>>> local_input = local_input + dist.get_rank()
|
||||
>>> input_dist = dist.auto_parallel.api.dtensor_from_local(
|
||||
... local_input,
|
||||
... mesh,
|
||||
... [dist.Shard(0)],
|
||||
... )
|
||||
>>> custom_layer = CustomLayer(dist_attrs, dist_attrs)
|
||||
>>> output_dist = custom_layer(input_dist)
|
||||
|
||||
>>> local_value = custom_layer.local_result
|
||||
>>> gathered_values: list[Tensor] = []
|
||||
>>> dist.all_gather(gathered_values, local_value)
|
||||
|
||||
>>> print(f"[Rank 0] local_loss={gathered_values[0]}")
|
||||
[Rank 0] local_loss=1.5
|
||||
>>> print(f"[Rank 1] local_loss={gathered_values[1]}")
|
||||
[Rank 1] local_loss=6.0
|
||||
>>> print(f"global_loss (distributed)={output_dist}")
|
||||
global_loss (distributed)=7.5
|
||||
|
||||
>>> # This case needs to be executed in a multi-card environment
|
||||
>>> # export CUDA_VISIBLE_DEVICES=0,1
|
||||
>>> # python -m paddle.distributed.launch {test_case}.py
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
out_dist_attrs: list[tuple[ProcessMesh, list[Placement]]],
|
||||
grad_dist_attrs: list[tuple[ProcessMesh, list[Placement]]],
|
||||
) -> None:
|
||||
super().__init__()
|
||||
self.out_dist_attrs = out_dist_attrs
|
||||
self.grad_dist_attrs = grad_dist_attrs
|
||||
|
||||
def __call__(self, *inputs: Any, **kwargs: Any) -> Any:
|
||||
"""
|
||||
Overrides the base `Layer`'s `__call__` method. Transforms distributed tensors to local tensors
|
||||
before computation, invokes the parent class's `__call__` method, and then transforms the
|
||||
outputs back to distributed tensors based on the specified distribution attributes.
|
||||
"""
|
||||
inputs = list(inputs)
|
||||
assert len(inputs) == len(self.grad_dist_attrs), (
|
||||
f"The number of inputs ({len(inputs)}) does not match the number of grad_dist_attrs ({len(self.grad_dist_attrs)})."
|
||||
)
|
||||
for idx in range(len(inputs)):
|
||||
if inputs[idx].is_dist():
|
||||
if self.grad_dist_attrs[idx] is None:
|
||||
if paddle.in_dynamic_mode():
|
||||
mesh, placement = (
|
||||
inputs[idx].process_mesh,
|
||||
inputs[idx].placements,
|
||||
)
|
||||
else:
|
||||
mesh, placement = (
|
||||
inputs[idx].dist_attr().process_mesh,
|
||||
inputs[idx].dist_attr().placements,
|
||||
)
|
||||
else:
|
||||
mesh, placement = (
|
||||
self.grad_dist_attrs[idx][0],
|
||||
self.grad_dist_attrs[idx][1],
|
||||
)
|
||||
|
||||
inputs[idx] = dist.auto_parallel.api.dtensor_to_local(
|
||||
inputs[idx], mesh, placement
|
||||
)
|
||||
|
||||
outputs = Layer.__call__(self, *inputs, **kwargs)
|
||||
list_outs = paddle.utils.flatten(outputs)
|
||||
assert len(list_outs) == len(self.out_dist_attrs), (
|
||||
f"The number of outputs ({len(list_outs)}) does not match the number of distribution attributes ({len(self.out_dist_attrs)})."
|
||||
)
|
||||
|
||||
dist_outs = []
|
||||
for idx in range(len(list_outs)):
|
||||
dist_outs.append(
|
||||
dist.auto_parallel.api.dtensor_from_local(
|
||||
list_outs[idx],
|
||||
self.out_dist_attrs[idx][0],
|
||||
self.out_dist_attrs[idx][1],
|
||||
)
|
||||
)
|
||||
return paddle.utils.pack_sequence_as(outputs, dist_outs)
|
||||
@@ -0,0 +1,256 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from __future__ import annotations
|
||||
|
||||
import functools
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.utils import flatten, pack_sequence_as
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
from paddle.distributed import ProcessMesh
|
||||
|
||||
|
||||
def local_map(
|
||||
func: Callable[..., Any],
|
||||
out_placements: list[list[dist.Placement]],
|
||||
in_placements: list[list[dist.Placement]] | None = None,
|
||||
process_mesh: ProcessMesh | None = None,
|
||||
reshard_inputs: bool = False,
|
||||
) -> Callable[..., Any]:
|
||||
"""
|
||||
The `local_map` API allows users to pass dist_tensors to a function that is written
|
||||
to be applied on ``paddle.Tensor`` s. It works by extracting the local components
|
||||
of dist_tensors, calling the function, and wrapping the outputs as dist_tensors
|
||||
according to the ``out_placements``.
|
||||
|
||||
Args:
|
||||
func (Callable): The function to be applied on each local shard of dist_tensors.
|
||||
|
||||
out_placements (list[list[dist.Placement]]):
|
||||
The desired placements for each output tensor. Must be a list where each element
|
||||
is a list of Placement objects specifying the distribution strategy for that
|
||||
output tensor. The length of the outer list must match the number of outputs
|
||||
from ``func``. For non-tensor outputs, the corresponding placement must be None.
|
||||
When there are no dist_tensor inputs, process_mesh must be specified to use
|
||||
non-None placements.
|
||||
|
||||
in_placements (Optional[list[list[dist.Placement]]], optional):
|
||||
The required placements for each input tensor. If specified, must be a list
|
||||
where each element is a list of Placement objects defining the distribution
|
||||
strategy for that input tensor. The length of the outer list must match the
|
||||
number of input tensors.
|
||||
Default: None
|
||||
|
||||
process_mesh (ProcessMesh, optional):
|
||||
The process mesh that all dist_tensors are placed on. If not specified,
|
||||
this will be inferred from the input dist_tensors' process mesh.
|
||||
local_map requires all dist_tensors to be placed on the same process mesh.
|
||||
Must be specified when there are no dist_tensor inputs but out_placements
|
||||
contains non-None values.
|
||||
Default: None
|
||||
|
||||
reshard_inputs (bool, optional):
|
||||
the bool value indicating whether to reshard the input :dist_tensors when
|
||||
their placements are different from the required input placements. If this
|
||||
value is ``False`` and some :dist_tensor input has a different placement,
|
||||
an exception will be raised. Default: False.
|
||||
|
||||
Returns:
|
||||
Callable: A function that applies func to local shards of input dist_tensors and returns dist_tensors or original values.
|
||||
|
||||
Example:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> from __future__ import annotations
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
>>> from paddle import Tensor
|
||||
>>> from paddle.distributed import ProcessMesh
|
||||
|
||||
>>> def custom_function(x):
|
||||
... mask = paddle.zeros_like(x)
|
||||
... if dist.get_rank() == 0:
|
||||
... mask[1:3] = 1
|
||||
... else:
|
||||
... mask[4:7] = 1
|
||||
... x = x * mask
|
||||
... mask_sum = paddle.sum(x)
|
||||
... mask_sum = mask_sum / mask.sum()
|
||||
... return mask_sum
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> dist.init_parallel_env()
|
||||
>>> mesh = ProcessMesh([0, 1], dim_names=["x"])
|
||||
>>> local_input = paddle.arange(0, 10, dtype="float32")
|
||||
>>> local_input = local_input + dist.get_rank()
|
||||
>>> input_dist = dist.auto_parallel.api.dtensor_from_local(local_input, mesh, [dist.Shard(0)])
|
||||
>>> wrapped_func = dist.local_map(
|
||||
... custom_function,
|
||||
... out_placements=[[dist.Partial(dist.ReduceType.kRedSum)]],
|
||||
... in_placements=[[dist.Shard(0)]],
|
||||
... process_mesh=mesh,
|
||||
... )
|
||||
>>> output_dist = wrapped_func(input_dist)
|
||||
|
||||
>>> local_value = output_dist._local_value()
|
||||
>>> gathered_values: list[Tensor] = []
|
||||
>>> dist.all_gather(gathered_values, local_value)
|
||||
|
||||
>>> print(f"[Rank 0] local_value={gathered_values[0].item()}")
|
||||
[Rank 0] local_value=1.5
|
||||
>>> print(f"[Rank 1] local_value={gathered_values[1].item()}")
|
||||
[Rank 1] local_value=6.0
|
||||
>>> print(f"global_value (distributed)={output_dist.item()}")
|
||||
global_value (distributed)=7.5
|
||||
|
||||
>>> # This case needs to be executed in a multi-card environment
|
||||
>>> # export CUDA_VISIBLE_DEVICES=0,1
|
||||
>>> # python -m paddle.distributed.launch {test_case}.py
|
||||
"""
|
||||
|
||||
def wrapped(process_mesh: ProcessMesh | None, *args, **kwargs):
|
||||
# Process input arguments
|
||||
flat_dist_args = flatten(args)
|
||||
if in_placements is not None:
|
||||
assert len(in_placements) == len(flat_dist_args), (
|
||||
f"in_placements length {len(in_placements)} does not match "
|
||||
f"number of input args {len(flat_dist_args)}!"
|
||||
)
|
||||
|
||||
flat_local_args = []
|
||||
seen_dist_tensor = False
|
||||
|
||||
for idx, arg in enumerate(flat_dist_args):
|
||||
if dist.auto_parallel.api.is_dist_tensor(arg):
|
||||
dist_tensor = arg
|
||||
if process_mesh is None:
|
||||
if paddle.in_dynamic_mode():
|
||||
process_mesh = dist_tensor.process_mesh
|
||||
else:
|
||||
process_mesh = dist_tensor.dist_attr().process_mesh
|
||||
|
||||
seen_dist_tensor = True
|
||||
|
||||
if in_placements is not None:
|
||||
in_placement = in_placements[idx]
|
||||
if in_placement is None:
|
||||
if paddle.in_dynamic_mode():
|
||||
in_placement = dist_tensor.placements
|
||||
else:
|
||||
in_placement = dist_tensor.dist_attr().placements
|
||||
else:
|
||||
if paddle.in_dynamic_mode():
|
||||
if in_placement != dist_tensor.placements:
|
||||
if reshard_inputs:
|
||||
dist_tensor = dist.reshard(
|
||||
dist_tensor, process_mesh, in_placement
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"in_placement {in_placement} does not match dist_tensor.placements {dist_tensor.placements}"
|
||||
)
|
||||
|
||||
else:
|
||||
if (
|
||||
in_placement
|
||||
!= dist_tensor.dist_attr().placements
|
||||
):
|
||||
if reshard_inputs:
|
||||
dist_tensor = dist.reshard(
|
||||
dist_tensor, process_mesh, in_placement
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
f"in_placement {in_placement} does not match dist_tensor.dist_attr().placements {dist_tensor.dist_attr().placements}"
|
||||
"If reshard_inputs is wanted, set "
|
||||
"reshard_inputs=True to local_map."
|
||||
)
|
||||
local_tensor = dist.auto_parallel.api.dtensor_to_local(
|
||||
dist_tensor, process_mesh, in_placement
|
||||
)
|
||||
flat_local_args.append(local_tensor)
|
||||
else:
|
||||
flat_local_args.append(arg)
|
||||
|
||||
local_args = pack_sequence_as(args, flat_local_args)
|
||||
out = func(*local_args, **kwargs)
|
||||
original_out = out
|
||||
if seen_dist_tensor:
|
||||
flat_out = flatten(out)
|
||||
assert len(flat_out) == len(out_placements), (
|
||||
"local_map requires one PlacementType for each output value, "
|
||||
f"got {len(out_placements)} placements but expected "
|
||||
f"{len(flat_out)}!"
|
||||
)
|
||||
|
||||
flat_dist_and_arg_out = []
|
||||
for out, out_placement in zip(flat_out, out_placements):
|
||||
if paddle.in_dynamic_mode():
|
||||
if isinstance(out, paddle.Tensor):
|
||||
assert not dist.auto_parallel.api.is_dist_tensor(out), (
|
||||
f"Expected dense tensor output but got {type(out)}: {out}"
|
||||
)
|
||||
|
||||
flat_dist_and_arg_out.append(
|
||||
dist.auto_parallel.api.dtensor_from_local(
|
||||
out, process_mesh, out_placement
|
||||
)
|
||||
)
|
||||
else:
|
||||
assert out_placement is None, (
|
||||
f"Expected None placements for non-tensor output {out} "
|
||||
f"but got {out_placement}!"
|
||||
)
|
||||
flat_dist_and_arg_out.append(out)
|
||||
else:
|
||||
if isinstance(out, paddle.base.libpaddle.pir.Value):
|
||||
assert not dist.auto_parallel.api.is_dist_tensor(out), (
|
||||
f"Expected dense tensor output but got {type(out)}: {out}"
|
||||
)
|
||||
|
||||
flat_dist_and_arg_out.append(
|
||||
dist.auto_parallel.api.dtensor_from_local(
|
||||
out, process_mesh, out_placement
|
||||
)
|
||||
)
|
||||
else:
|
||||
assert out_placement is None, (
|
||||
f"Expected None placements for non-tensor output {out} "
|
||||
f"but got {out_placement}!"
|
||||
)
|
||||
flat_dist_and_arg_out.append(out)
|
||||
return pack_sequence_as(original_out, flat_dist_and_arg_out)
|
||||
else:
|
||||
flat_out = flatten(out)
|
||||
flat_dist_and_arg_out = []
|
||||
for out, out_placement in zip(flat_out, out_placements):
|
||||
if out_placement is not None:
|
||||
assert process_mesh is not None, (
|
||||
"process_mesh must be specified when out_placements is not None"
|
||||
)
|
||||
flat_dist_and_arg_out.append(
|
||||
dist.auto_parallel.api.dtensor_from_local(
|
||||
out, process_mesh, out_placement
|
||||
)
|
||||
)
|
||||
else:
|
||||
flat_dist_and_arg_out.append(out)
|
||||
return pack_sequence_as(original_out, flat_dist_and_arg_out)
|
||||
|
||||
return functools.partial(wrapped, process_mesh)
|
||||
@@ -0,0 +1,468 @@
|
||||
# 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
|
||||
@@ -0,0 +1,101 @@
|
||||
# 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
|
||||
|
||||
import logging
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
|
||||
_logger = get_logger(logging.INFO)
|
||||
from ..random import determinate_rng, is_enable_auto_rand_ctrl
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_eltwise import DistributedDefaultImpl0, DistributedElementwiseImpl0
|
||||
|
||||
|
||||
class DistributedFlashAttn(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedFlashAttn("flash_attn"))
|
||||
|
||||
|
||||
# Dist FlashAttn with Random Control
|
||||
class DistributedFlashAttnImpl0(DistributedElementwiseImpl0):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if (
|
||||
is_enable_auto_rand_ctrl()
|
||||
and not op_dist_attr.is_recompute
|
||||
and rank_id in op_dist_attr.process_mesh.process_ids
|
||||
):
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
if (
|
||||
len(kwargs.get('fixed_seed_offset', [])) > 0
|
||||
or len(src_op.input("fixed_seed_offset")) > 0
|
||||
):
|
||||
# TODO(kuizhiqing) recompute should go here
|
||||
pass
|
||||
else:
|
||||
# determinate rng
|
||||
q_var = main_block._var_recursive(kwargs['q'][0])
|
||||
k_var = main_block._var_recursive(kwargs['k'][0])
|
||||
q_dims_mapping = op_dist_attr.get_input_dims_mapping(q_var.name)
|
||||
k_dims_mapping = op_dist_attr.get_input_dims_mapping(k_var.name)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
dims_mapping = [*q_dims_mapping[:3], q_dims_mapping[2]]
|
||||
|
||||
rng_name = determinate_rng(rank_id, dims_mapping, process_mesh)
|
||||
assert rng_name is not None and rng_name != ""
|
||||
|
||||
src_op._set_attr('rng_name', rng_name)
|
||||
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# dropout backward is deterministic by mask, and not need for random state control
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"flash_attn", DistributedFlashAttnImpl0("random_control")
|
||||
)
|
||||
@@ -0,0 +1,15 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
__all__ = []
|
||||
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
import paddle
|
||||
|
||||
from .utils import _map_debug_info
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def stage_backward_input(
|
||||
stage_outputs_or_loss: list[paddle.Tensor],
|
||||
output_grads: list[paddle.Tensor] | None,
|
||||
input_values: list[paddle.Tensor],
|
||||
weights: Iterator[paddle.Tensor],
|
||||
) -> tuple[tuple[paddle.Tensor | None, ...], list[dict[str, Any]]]:
|
||||
raise NotImplementedError("stage_backward_input is not implemented yet")
|
||||
|
||||
|
||||
def stage_backward_weight(
|
||||
weights: Iterator[paddle.Tensor],
|
||||
param_groups: list[dict[str, Any]],
|
||||
retain_graph=False,
|
||||
) -> tuple[paddle.Tensor | None, ...]:
|
||||
raise NotImplementedError("stage_backward_weight is not implemented yet")
|
||||
|
||||
|
||||
def stage_backward(
|
||||
stage_output,
|
||||
output_grads,
|
||||
input_values,
|
||||
) -> tuple[paddle.Tensor | None, ...]:
|
||||
"""
|
||||
This is a helper function to:
|
||||
1. compute the gradients for the stage inputs, and
|
||||
2. accumulate gradients for the stage module's parameters.
|
||||
|
||||
Given the input value(s) and the corresponding gradient for the output
|
||||
value(s), compute and accumulate gradients for all parameter values (leaves
|
||||
in the autograd trace) as well as return a list of the gradients for the
|
||||
input values
|
||||
|
||||
"""
|
||||
|
||||
try:
|
||||
# stage_output may be a composite datatype like dict. Extract all individual
|
||||
# tensor values here
|
||||
stage_output_tensors: list[paddle.Tensor] = []
|
||||
output_grad_tensors: list[paddle.Tensor | None] = []
|
||||
|
||||
def extract_tensors_with_grads(
|
||||
output_val,
|
||||
grad_val,
|
||||
extract_tensors_with_grads,
|
||||
):
|
||||
if isinstance(output_val, paddle.Tensor):
|
||||
if output_val.stop_gradient and output_val.grad_fn is None:
|
||||
return
|
||||
assert isinstance(grad_val, (paddle.Tensor, type(None))), (
|
||||
f"Expected Tensor or None gradient but got {type(grad_val)}"
|
||||
)
|
||||
stage_output_tensors.append(output_val)
|
||||
output_grad_tensors.append(grad_val)
|
||||
elif isinstance(output_val, (tuple, list)):
|
||||
if grad_val is None:
|
||||
return
|
||||
assert isinstance(grad_val, (tuple, list)), (
|
||||
f"grad_value expected to have type {type(output_val)} but got {type(grad_val)}"
|
||||
)
|
||||
assert len(output_val) == len(grad_val)
|
||||
for ov, gv in zip(output_val, grad_val):
|
||||
extract_tensors_with_grads(
|
||||
ov,
|
||||
gv,
|
||||
extract_tensors_with_grads,
|
||||
)
|
||||
elif isinstance(output_val, dict):
|
||||
if grad_val is None:
|
||||
return
|
||||
assert isinstance(grad_val, dict)
|
||||
assert set(output_val.keys()) == set(grad_val.keys())
|
||||
for k in output_val.keys():
|
||||
extract_tensors_with_grads(
|
||||
output_val[k], grad_val[k], extract_tensors_with_grads
|
||||
)
|
||||
else:
|
||||
# Output is a non-tensor type; just ignore it
|
||||
pass
|
||||
|
||||
# Note: ref cycle
|
||||
# break a ref cycle that would keep tensors alive until GC runs
|
||||
# 1. extract_tensors_with_grads refers to a cell that holds refs to any vars defined in stage_backward
|
||||
# and used in extract_tensors_with_grads
|
||||
# 2. extract_tensors_with_grads referred to both stage_output_tensors, output_grad_tensors,
|
||||
# and to itself (extract_tensors_with_grads) since it makes a recursive call
|
||||
# 3. stage_output_tensors was kept alive by the above refcycle, and it holds activation tensors, which is bad
|
||||
# fix -> explicitly pass in the ref to the fn, so there is no gc cycle anymore
|
||||
extract_tensors_with_grads(
|
||||
stage_output, output_grads, extract_tensors_with_grads
|
||||
)
|
||||
# Deactivate auto mixed precision context in the backward phase
|
||||
with paddle.amp.auto_cast(enable=False):
|
||||
paddle.autograd.backward(
|
||||
stage_output_tensors,
|
||||
grad_tensors=output_grad_tensors,
|
||||
)
|
||||
|
||||
# Extract gradients wrt the input values
|
||||
grad_inputs: list[paddle.Tensor | None] = []
|
||||
for val in input_values:
|
||||
if isinstance(val, paddle.Tensor):
|
||||
grad_inputs.append(val.grad)
|
||||
else:
|
||||
grad_inputs.append(None)
|
||||
|
||||
except Exception as e:
|
||||
exc_msg = f"""
|
||||
Failed to run stage backward:
|
||||
Stage output: {_map_debug_info(stage_output)}
|
||||
Output gradient: {_map_debug_info(output_grads)}
|
||||
Input: {_map_debug_info(input_values)}
|
||||
"""
|
||||
raise RuntimeError(exc_msg) from e
|
||||
|
||||
return tuple(grad_inputs)
|
||||
@@ -0,0 +1,326 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.distributed import Replicate, Shard
|
||||
from paddle.distributed.auto_parallel.api import (
|
||||
dtensor_from_local,
|
||||
dtensor_to_local,
|
||||
)
|
||||
from paddle.utils import flatten, map_structure, pack_sequence_as
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Default chunking dimension is 0. This is used for the case where the user did
|
||||
# not specify a chunking dimension.
|
||||
DEFAULT_CHUNK_DIM = 0
|
||||
|
||||
|
||||
def _split_tensor(x, num_chunks, split_axis=0):
|
||||
if not x.is_dist():
|
||||
chunk_tensors = paddle.tensor_split(x, num_chunks, split_axis)
|
||||
# dp_degree > 1 , placements of model input is [S(0), R, ...]
|
||||
else:
|
||||
if dist.in_auto_parallel_align_mode():
|
||||
|
||||
def _reorder_data_for_align():
|
||||
nonlocal x
|
||||
assert x.placements[0] == dist.Shard(0), (
|
||||
"inputs should be placed on S(0)."
|
||||
)
|
||||
|
||||
shardings = x.process_mesh.shape[0]
|
||||
|
||||
rows_per_shard = x.shape[0] // shardings
|
||||
new_indices = []
|
||||
for s_id in range(shardings):
|
||||
for row_in_shard in range(rows_per_shard):
|
||||
new_indices.append(s_id + row_in_shard * shardings)
|
||||
tmp = x[new_indices]
|
||||
x = dist.reshard(tmp, x.process_mesh, x.placements)
|
||||
|
||||
_reorder_data_for_align()
|
||||
mesh = x.process_mesh
|
||||
placements = x.placements
|
||||
dense_x = dtensor_to_local(x, mesh, placements)
|
||||
chunk_tensors = paddle.tensor_split(dense_x, num_chunks, split_axis)
|
||||
for i in range(num_chunks):
|
||||
chunk_tensors[i] = dtensor_from_local(
|
||||
chunk_tensors[i], mesh, placements
|
||||
)
|
||||
return chunk_tensors
|
||||
|
||||
|
||||
def _concat_tensor(chunk_tensors, axis=0):
|
||||
chunk0 = chunk_tensors[0]
|
||||
if not chunk0.is_dist():
|
||||
out = paddle.concat(chunk_tensors, axis)
|
||||
|
||||
else:
|
||||
# loss_fun(out, labels), placements of labels is [S(0), R, ...]
|
||||
mesh = chunk0.process_mesh
|
||||
placements = [Replicate() for _ in range(mesh.ndim)]
|
||||
dp_index = mesh.dim_names.index("dp") if "dp" in mesh.dim_names else 0
|
||||
placements[dp_index] = Shard(0)
|
||||
|
||||
for i in range(len(chunk_tensors)):
|
||||
chunk_tensors[i] = dist.reshard(chunk_tensors[i], mesh, placements)
|
||||
|
||||
chunk_tensors[i] = dtensor_to_local(
|
||||
chunk_tensors[i], mesh, placements
|
||||
)
|
||||
out = paddle.concat(chunk_tensors, axis)
|
||||
out = dtensor_from_local(out, mesh, placements)
|
||||
return out
|
||||
|
||||
|
||||
class TensorChunkSpec:
|
||||
"""
|
||||
Class used to specify chunking of inputs
|
||||
"""
|
||||
|
||||
def __init__(self, split_axis):
|
||||
self.split_axis = split_axis
|
||||
|
||||
split_axis: int
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.__class__.__module__}.{self.__class__.__name__}({self.split_axis})"
|
||||
|
||||
def __str__(self):
|
||||
return f"TensorChunkSpec({self.split_axis})"
|
||||
|
||||
|
||||
def _split_args_helper(
|
||||
args_dict,
|
||||
args_chunk_spec,
|
||||
num_chunks,
|
||||
):
|
||||
"""
|
||||
A helper function of split_args_kwargs_into_chunks.
|
||||
"""
|
||||
assert len(args_dict) == len(args_chunk_spec), (
|
||||
f"args_dict.keys() = {list(args_dict.keys())} args_chunk_spec.keys() = {list(args_chunk_spec.keys())}"
|
||||
)
|
||||
|
||||
shared_args_dict_flat = {}
|
||||
# handle args one by one
|
||||
for arg_key, arg in args_dict.items():
|
||||
arg_flat = flatten(arg)
|
||||
|
||||
chunk_spec = args_chunk_spec[arg_key]
|
||||
assert chunk_spec is not None
|
||||
|
||||
chunk_spec_flat = flatten(chunk_spec)
|
||||
assert len(chunk_spec_flat) == len(arg_flat), (
|
||||
f"{arg_key} {len(arg_flat)} != {len(chunk_spec_flat)}"
|
||||
)
|
||||
|
||||
shard_arg_flat = []
|
||||
|
||||
for v, chunk_v in zip(arg_flat, chunk_spec_flat):
|
||||
if not isinstance(v, paddle.Tensor):
|
||||
shard_arg_flat.append([v] * num_chunks)
|
||||
elif isinstance(chunk_v, TensorChunkSpec):
|
||||
v_split_axis_size = v.shape[chunk_v.split_axis]
|
||||
|
||||
if v_split_axis_size < num_chunks:
|
||||
raise ValueError(
|
||||
f"Arg {arg_key} on chunking dimension has a size of {v_split_axis_size}, "
|
||||
f"smaller than the number of chunks {num_chunks}. "
|
||||
"Please adjust your num_chunks setting."
|
||||
)
|
||||
# split tensor v
|
||||
chunk_tensors = _split_tensor(v, num_chunks, chunk_v.split_axis)
|
||||
|
||||
shard_arg_flat.append(chunk_tensors)
|
||||
else:
|
||||
raise TypeError(f"Unrecognized chunk spec: {chunk_v}")
|
||||
|
||||
shared_args_dict_flat[arg_key] = shard_arg_flat
|
||||
|
||||
# the structure of each element in args_split is the same as the original args_dict
|
||||
args_split = []
|
||||
for idx in range(num_chunks):
|
||||
chunk_args = {}
|
||||
for key, arg in shared_args_dict_flat.items():
|
||||
last_arg = None if not arg else arg[0][idx]
|
||||
arg_of_curr_chunk = (
|
||||
[v[idx] for v in arg] if len(arg) > 1 else last_arg
|
||||
)
|
||||
chunk_args[key] = arg_of_curr_chunk
|
||||
|
||||
# flatten chunk_args first, and then pack chunk_args as the origin args_dict
|
||||
flatten_chunk_args = [x for x in flatten(chunk_args) if x is not None]
|
||||
chunk_args = pack_sequence_as(args_dict, flatten_chunk_args)
|
||||
args_split.append(chunk_args)
|
||||
return args_split
|
||||
|
||||
|
||||
def split_args_kwargs_into_chunks(
|
||||
args: tuple[Any, ...],
|
||||
kwargs: dict[str, Any] | None,
|
||||
chunks: int,
|
||||
args_chunk_spec: (
|
||||
tuple[
|
||||
tuple[TensorChunkSpec, ...]
|
||||
| list[TensorChunkSpec, ...]
|
||||
| TensorChunkSpec,
|
||||
...,
|
||||
]
|
||||
| None
|
||||
) = None,
|
||||
kwargs_chunk_spec: (
|
||||
dict[
|
||||
str,
|
||||
tuple[TensorChunkSpec, ...]
|
||||
| list[TensorChunkSpec, ...]
|
||||
| TensorChunkSpec,
|
||||
]
|
||||
| None
|
||||
) = None,
|
||||
) -> tuple[list[tuple], list[dict]]:
|
||||
"""
|
||||
Given a sequence of args and kwargs, split them into a number of chunks
|
||||
according to their respective chunking specs.
|
||||
|
||||
Args:
|
||||
args: tuple of args
|
||||
kwargs: dict of kwargs
|
||||
chunks: Number of chunks to split the args and kwargs into
|
||||
args_chunk_spec: chunking specs for args, in same shape as args
|
||||
kwargs_chunk_spec: chunking specs for kwargs, in same shape as kwargs
|
||||
|
||||
Returns:
|
||||
args_split: list of sharded args
|
||||
kwargs_split: list of sharded kwargs
|
||||
"""
|
||||
|
||||
if kwargs is None:
|
||||
kwargs = {}
|
||||
|
||||
if args_chunk_spec is None:
|
||||
args_chunk_spec = map_structure(
|
||||
lambda _: TensorChunkSpec(DEFAULT_CHUNK_DIM), args
|
||||
)
|
||||
|
||||
if kwargs_chunk_spec is None:
|
||||
kwargs_chunk_spec = map_structure(
|
||||
lambda _: TensorChunkSpec(DEFAULT_CHUNK_DIM), kwargs
|
||||
)
|
||||
|
||||
args_split_dict = _split_args_helper(
|
||||
dict(enumerate(args)),
|
||||
dict(enumerate(args_chunk_spec)),
|
||||
chunks,
|
||||
)
|
||||
kwargs_split = _split_args_helper(
|
||||
kwargs,
|
||||
kwargs_chunk_spec,
|
||||
chunks,
|
||||
)
|
||||
|
||||
assert len(args_split_dict) == len(kwargs_split), (
|
||||
"args and kwargs are split into difference number of chunks: "
|
||||
f"{len(args_split_dict)}, {len(kwargs_split)}"
|
||||
)
|
||||
|
||||
# the form of each args_chunk should be tuple
|
||||
args_split = [
|
||||
tuple(args_chunk[i] for i in range(len(args_chunk)))
|
||||
for args_chunk in args_split_dict
|
||||
]
|
||||
|
||||
return args_split, kwargs_split
|
||||
|
||||
|
||||
def merge_chunks(
|
||||
chunks: list[Any],
|
||||
chunk_spec,
|
||||
):
|
||||
"""
|
||||
Given a list of chunks, merge them into a single chunk according to
|
||||
the chunk spec.
|
||||
|
||||
Args:
|
||||
chunks: list of chunks
|
||||
chunk_spec: Chunking spec for the chunks
|
||||
|
||||
Returns:
|
||||
chunk: chunks merged value
|
||||
"""
|
||||
if len(chunks) == 0:
|
||||
logger.warning("No chunks to merge.")
|
||||
return chunks
|
||||
|
||||
if chunk_spec is None:
|
||||
chunk_spec = map_structure(
|
||||
lambda _: TensorChunkSpec(DEFAULT_CHUNK_DIM), chunks[0]
|
||||
)
|
||||
|
||||
chunks_flat = []
|
||||
# flatten chunk_spec first
|
||||
chunk_spec = flatten(chunk_spec)
|
||||
for chunk in chunks:
|
||||
chunk_flat = flatten(chunk)
|
||||
assert len(chunk_flat) == len(chunk_spec), (
|
||||
f"Chunk {chunk} did not match chunk spec {chunk_spec}"
|
||||
)
|
||||
chunks_flat.append(chunk_flat)
|
||||
|
||||
def _merge_non_tensor_type_arg(chunks, idx, chunk_spec_of_arg=None):
|
||||
# use the first chunk's value as the merged result
|
||||
arg_0 = chunks[0][idx]
|
||||
for chunk_idx in range(1, len(chunks)):
|
||||
assert chunks[chunk_idx][idx] == arg_0, (
|
||||
f"Cannot merge chunks with index 0 and {idx} with different values,"
|
||||
f"When the arg's TensorChunkSpec is {chunk_spec_of_arg}"
|
||||
)
|
||||
return arg_0
|
||||
|
||||
args_flat = []
|
||||
for arg_idx, chunk_spec_of_arg in enumerate(chunk_spec):
|
||||
if isinstance(chunk_spec_of_arg, TensorChunkSpec):
|
||||
if isinstance(chunks_flat[0][arg_idx], paddle.Tensor):
|
||||
arg_chunks_to_merge = [
|
||||
chunks_flat[chunk_idx][arg_idx]
|
||||
for chunk_idx in range(len(chunks_flat))
|
||||
]
|
||||
merged_arg = _concat_tensor(
|
||||
arg_chunks_to_merge, axis=chunk_spec_of_arg.split_axis
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
f"Cannot merge chunks with TensorChunkSpec {chunk_spec_of_arg}."
|
||||
"The TensorChunkSpec only supports paddle.Tensor type."
|
||||
)
|
||||
|
||||
merged_arg = _merge_non_tensor_type_arg(
|
||||
chunks_flat, arg_idx, chunk_spec_of_arg
|
||||
)
|
||||
else:
|
||||
merged_arg = _merge_non_tensor_type_arg(
|
||||
chunks_flat, arg_idx, chunk_spec_of_arg
|
||||
)
|
||||
|
||||
args_flat.append(merged_arg)
|
||||
|
||||
# pack args_flat as the input chunks[0]
|
||||
return pack_sequence_as(chunks[0], args_flat)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,177 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
import paddle
|
||||
from paddle.distributed import fleet
|
||||
from paddle.distributed.auto_parallel.api import (
|
||||
dtensor_from_local,
|
||||
)
|
||||
from paddle.utils import map_structure
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _detach_and_requires_grad(x):
|
||||
o = x.detach()
|
||||
o.stop_gradient = False
|
||||
return o
|
||||
|
||||
|
||||
def _detach_and_keep_grad(x):
|
||||
o = x.detach_()
|
||||
o.stop_gradient = x.stop_gradient
|
||||
return o
|
||||
|
||||
|
||||
def _zero_initialize_with_meta(meta, mesh):
|
||||
assert isinstance(meta, TensorMeta)
|
||||
x = paddle.zeros(
|
||||
meta._local_shape if meta._local_shape else meta.shape, dtype=meta.dtype
|
||||
)
|
||||
if meta.placements:
|
||||
x = dtensor_from_local(x, mesh, meta.placements)
|
||||
return x
|
||||
|
||||
|
||||
def _flatten_args(args):
|
||||
"""
|
||||
Flatten the args into a list form.
|
||||
"""
|
||||
flat_args = []
|
||||
|
||||
def extract_tensor_args(a):
|
||||
nonlocal flat_args
|
||||
if isinstance(a, paddle.Tensor):
|
||||
flat_args.append(a)
|
||||
return a
|
||||
|
||||
paddle.utils.map_structure(
|
||||
extract_tensor_args,
|
||||
args,
|
||||
)
|
||||
|
||||
return flat_args
|
||||
|
||||
|
||||
class PipeliningShapeError(RuntimeError):
|
||||
"""Shape mismatch between configured and runtime values."""
|
||||
|
||||
|
||||
def _validate_tensor_metadata(desc, expected, given):
|
||||
if not expected.shape == given.shape:
|
||||
raise PipeliningShapeError(
|
||||
f"{desc} has a shape mismatch: expected {expected.shape} actual {given.shape}"
|
||||
)
|
||||
if not expected.dtype == given.dtype:
|
||||
raise PipeliningShapeError(
|
||||
f"{desc} has a dtype mismatch: expected {expected.dtype} actual {given.dtype}"
|
||||
)
|
||||
|
||||
|
||||
def _validate_tensors_metadata(
|
||||
desc,
|
||||
expected_tensors: list[paddle.Tensor] | tuple[paddle.Tensor, ...],
|
||||
actual_tensors: list[paddle.Tensor] | tuple[paddle.Tensor, ...],
|
||||
):
|
||||
if len(expected_tensors) != len(actual_tensors):
|
||||
raise PipeliningShapeError(
|
||||
f"{desc}: Number of values ({len(actual_tensors)}) does not match expected number ({len(expected_tensors)})"
|
||||
)
|
||||
for i in range(len(expected_tensors)):
|
||||
_validate_tensor_metadata(
|
||||
f"{desc}: value {i}", expected_tensors[i], actual_tensors[i]
|
||||
)
|
||||
|
||||
|
||||
NestedStruct = list[Any] | tuple[Any, ...] | dict[Any, Any]
|
||||
|
||||
|
||||
def _map_structure_only(
|
||||
type_: Any, fn: Callable[[Any], Any], structure: NestedStruct
|
||||
) -> NestedStruct:
|
||||
"""
|
||||
Apply `fn` to each entry which matches `type_` in `structure` and return a new structure with the same shape.
|
||||
"""
|
||||
return map_structure(
|
||||
lambda x: fn(x) if isinstance(x, type_) else x, structure
|
||||
)
|
||||
|
||||
|
||||
class TensorMeta:
|
||||
def __init__(self, tensor: paddle.Tensor):
|
||||
if tensor.is_dist():
|
||||
self.shape = tensor.shape
|
||||
self._local_shape = tensor._local_shape
|
||||
else:
|
||||
self.shape = tensor.shape
|
||||
self._local_shape = None
|
||||
self.dtype = tensor.dtype
|
||||
self.placements = None if not tensor.is_dist() else tensor.placements
|
||||
self.stop_gradient = tensor.stop_gradient
|
||||
|
||||
def __repr__(self):
|
||||
return f"TensorMeta(global_shape={self.shape},local_shape={self._local_shape}, dtype={self.dtype}, placements={self.placements})"
|
||||
|
||||
|
||||
def _get_pp_mesh(pp_idx=0, pp_dim_names="pp"):
|
||||
"""
|
||||
Get the mesh of the {pp_idx}th PipelineStage.
|
||||
"""
|
||||
mesh = fleet.auto.get_mesh()
|
||||
assert mesh is not None, (
|
||||
"the mesh is None, please call fleet.auto.set_mesh first."
|
||||
)
|
||||
if "pp" in mesh.dim_names:
|
||||
mesh = mesh.get_mesh_with_dim("pp", pp_idx)
|
||||
else:
|
||||
logger.warning(
|
||||
f"The dim name of pp {pp_dim_names} not exist in global mesh {mesh}"
|
||||
)
|
||||
return mesh
|
||||
|
||||
|
||||
def _get_stage_mesh(stage_index, pp_group_size, style=None):
|
||||
if style == "v":
|
||||
raise NotImplementedError
|
||||
if style is not None:
|
||||
raise ValueError(f"Unknown style: {style}, style can be None, v.")
|
||||
else:
|
||||
pp_idx = stage_index % pp_group_size
|
||||
return _get_pp_mesh(pp_idx)
|
||||
|
||||
|
||||
def _friendly_debug_info(v):
|
||||
"""
|
||||
Helper function to print out debug info in a friendly way.
|
||||
"""
|
||||
if isinstance(v, paddle.Tensor):
|
||||
return f"Tensor({v.shape}, stop_gradient={v.stop_gradient}, dtype={v.dtype})"
|
||||
else:
|
||||
return str(v)
|
||||
|
||||
|
||||
def _map_debug_info(a):
|
||||
"""
|
||||
Helper function to apply `friendly_debug_info` to items in `a`.
|
||||
`a` may be a list, tuple, or dict.
|
||||
"""
|
||||
return map_structure(_friendly_debug_info, a)
|
||||
@@ -0,0 +1,214 @@
|
||||
# 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 typing import cast
|
||||
|
||||
import paddle
|
||||
from paddle.base.core import Partial, Replicate, Shard
|
||||
|
||||
|
||||
def dims_mapping_to_placements(dim_map, mesh, partial_idx=[], split_factor={}):
|
||||
"""
|
||||
convert dim_map to placements.
|
||||
|
||||
Args:
|
||||
dim_map(List[int]): a list of integer that represents sharding on each tensor dimension.
|
||||
mesh(paddle.distributed.ProcessMesh): The `ProcessMesh` object describes the Cartesian topology of the used processes.
|
||||
partial_idx(List[int], Optional): a list of integer that represents the DTensor have pending sum on which device mesh dimension
|
||||
|
||||
Returns:
|
||||
List[Placement]: a list contains some `paddle.distributed.Placement`.
|
||||
"""
|
||||
placements = [Replicate() for _ in mesh.shape]
|
||||
|
||||
for s in partial_idx:
|
||||
placements[s] = Partial()
|
||||
|
||||
for tensor_dim, mesh_dims in enumerate(dim_map):
|
||||
if len(mesh_dims) <= 0:
|
||||
continue
|
||||
is_co_shard = len(mesh_dims) > 1
|
||||
for shard_order, mesh_dim in enumerate(mesh_dims):
|
||||
p = placements[mesh_dim]
|
||||
if p.is_shard():
|
||||
raise Exception(
|
||||
f"ProcessMesh dimension can not be mapped to two dimension of same tensor: {tensor_dim} and {p.get_dim()}."
|
||||
)
|
||||
elif p.is_partial():
|
||||
raise Exception(
|
||||
f"ProcessMesh dimension {mesh_dim} can not be both shard and partial!"
|
||||
)
|
||||
|
||||
shard = (
|
||||
Shard(tensor_dim, co_shard_order=shard_order)
|
||||
if is_co_shard
|
||||
else Shard(tensor_dim)
|
||||
)
|
||||
placements[mesh_dim] = shard
|
||||
|
||||
if len(split_factor) > 1:
|
||||
raise RuntimeError("At now only support to rearrange at one mesh dim.")
|
||||
|
||||
for k, v in split_factor.items():
|
||||
placements[k].set_split_factor(v)
|
||||
|
||||
return placements
|
||||
|
||||
|
||||
def to_placements(dim_map, mesh, partial_idx=[]):
|
||||
"""
|
||||
convert dim_map to placements.
|
||||
Args:
|
||||
dim_map(List[int]): a list of integer that represents sharding on each tensor dimension.
|
||||
mesh(paddle.distributed.ProcessMesh): The `ProcessMesh` object describes the Cartesian topology of the used processes.
|
||||
partial_idx(List[int], Optional): a list of integer that represents the DTensor have pending sum on which device mesh dimension
|
||||
Returns:
|
||||
List[Placement]: a list contains some `paddle.distributed.Placement`.
|
||||
"""
|
||||
if isinstance(mesh, paddle.base.libpaddle.ProcessMesh):
|
||||
shape = mesh.shape
|
||||
else:
|
||||
shape = mesh.mesh.shape
|
||||
placements = [Replicate() for _ in range(len(shape))]
|
||||
|
||||
for s in partial_idx:
|
||||
placements[s] = Partial()
|
||||
|
||||
for i, m in enumerate(dim_map):
|
||||
if m >= 0:
|
||||
p = placements[m]
|
||||
if p.is_shard():
|
||||
p = cast("Shard", p)
|
||||
raise Exception(
|
||||
f"ProcessMesh dimension can not be mapped to two dimension of same tensor: {i} and {p.get_dim()}."
|
||||
)
|
||||
elif p.is_partial():
|
||||
raise Exception(
|
||||
f"ProcessMesh dimension {m} can not be both shard and partial!"
|
||||
)
|
||||
placements[m] = Shard(i)
|
||||
|
||||
return placements
|
||||
|
||||
|
||||
def check_placements_equal(this, that):
|
||||
assert isinstance(this, list) and isinstance(that, list)
|
||||
small_placements = this if len(this) < len(that) else that
|
||||
large_placements = that if len(this) < len(that) else this
|
||||
for i in range(len(large_placements)):
|
||||
if i < len(small_placements):
|
||||
if small_placements[i] != large_placements[i]:
|
||||
return False
|
||||
else:
|
||||
if large_placements[i] != Replicate():
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def placemetns_to_dist_status(
|
||||
placements, tensor_dims, return_split_factor=False
|
||||
):
|
||||
"""
|
||||
convert placements to dim_map.
|
||||
|
||||
Args:
|
||||
placements(List[Placement]): a list contains some `paddle.distributed.Placement`.
|
||||
tensor_dims(int): the dimension of dist_tensor.
|
||||
|
||||
Returns:
|
||||
List[int]: a list of integer that represents sharding on each tensor dimension.
|
||||
"""
|
||||
output_list = []
|
||||
dim_map = [[] for _ in range(tensor_dims)]
|
||||
partial_status = {}
|
||||
split_factor_map = {}
|
||||
for i, placement in enumerate(placements):
|
||||
if placement.is_shard():
|
||||
shard_dim = cast("Shard", placement).get_dim()
|
||||
dim_map[shard_dim].append(i)
|
||||
if cast("Shard", placement).get_split_factor() > 1:
|
||||
split_factor_map[i] = cast(
|
||||
"Shard", placement
|
||||
).get_split_factor()
|
||||
assert len(split_factor_map) == 1, (
|
||||
"only support to rerrange at one mesh dim."
|
||||
)
|
||||
if placement.is_partial():
|
||||
partial_status[i] = cast("Partial", placement).reduce_type()
|
||||
|
||||
for shard_dim in dim_map:
|
||||
if len(shard_dim) > 0:
|
||||
shard_dim.sort(key=lambda idx: placements[idx].get_co_shard_order())
|
||||
|
||||
output_list.append(dim_map)
|
||||
output_list.append(partial_status)
|
||||
|
||||
if return_split_factor:
|
||||
output_list.append(split_factor_map)
|
||||
|
||||
return output_list
|
||||
|
||||
|
||||
def to_dim_map(placements, tensor_dims):
|
||||
"""
|
||||
convert placements to dim_map.
|
||||
|
||||
Args:
|
||||
placements(List[Placement]): a list contains some `paddle.distributed.Placement`.
|
||||
tensor_dims(int): the dimension of dist_tensor.
|
||||
|
||||
Returns:
|
||||
List[int]: a list of integer that represents sharding on each tensor dimension.
|
||||
"""
|
||||
dim_map = [-1] * tensor_dims
|
||||
partial_status = {}
|
||||
for i, placement in enumerate(placements):
|
||||
if placement.is_shard():
|
||||
shard_dim = cast("Shard", placement).get_dim()
|
||||
if dim_map[shard_dim] > -1:
|
||||
import logging
|
||||
|
||||
logging.warning(
|
||||
f"Tensor dim {shard_dim} is already sharded on mesh dim {dim_map[shard_dim]}."
|
||||
)
|
||||
|
||||
dim_map[shard_dim] = i
|
||||
if placement.is_partial():
|
||||
partial_status[i] = cast("Partial", placement).reduce_type()
|
||||
|
||||
return dim_map, partial_status
|
||||
|
||||
|
||||
# TODO(lfw): delete it in future.
|
||||
def get_shard_spec(mesh, placements, tensor_dims):
|
||||
"""to get shard_spec for construct DistAttr for static API."""
|
||||
dim_map = [-1] * tensor_dims
|
||||
for i, placement in enumerate(placements):
|
||||
if placement.is_shard():
|
||||
shard_dim = cast("Shard", placement).get_dim()
|
||||
if dim_map[shard_dim] > -1:
|
||||
import logging
|
||||
|
||||
logging.warning(
|
||||
f"Tensor dim {shard_dim} is already sharded on mesh dim {dim_map[shard_dim]}."
|
||||
)
|
||||
|
||||
dim_map[shard_dim] = i
|
||||
|
||||
mesh_dim_names = mesh.dim_names
|
||||
shard_spec = [None] * len(dim_map)
|
||||
for i, d in enumerate(dim_map):
|
||||
if d > -1:
|
||||
shard_spec[i] = mesh_dim_names[d]
|
||||
|
||||
return shard_spec
|
||||
@@ -0,0 +1,603 @@
|
||||
# Copyright (c) 2021 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
|
||||
from typing import TYPE_CHECKING, Any, SupportsIndex
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.distributed import fleet
|
||||
from paddle.distributed.collective import _get_group_map
|
||||
from paddle.distributed.communication.group import is_initialized
|
||||
from paddle.framework import core
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterable, Sequence
|
||||
from types import TracebackType
|
||||
|
||||
import numpy.typing as npt
|
||||
|
||||
from paddle._typing import NestedNumericSequence
|
||||
|
||||
_NumpyShapeLike = SupportsIndex | Sequence[SupportsIndex]
|
||||
|
||||
|
||||
# Use to store the previous and current process mesh
|
||||
_g_previous_process_mesh = None
|
||||
_g_current_process_mesh = None
|
||||
# {shape_process_ids : unique_id}
|
||||
_g_unique_process_mesh_map = {}
|
||||
_g_group_map = {}
|
||||
|
||||
|
||||
def get_current_process_mesh():
|
||||
global _g_current_process_mesh
|
||||
return _g_current_process_mesh
|
||||
|
||||
|
||||
def set_current_process_mesh(process_mesh):
|
||||
global _g_previous_process_mesh
|
||||
global _g_current_process_mesh
|
||||
_g_previous_process_mesh = _g_current_process_mesh
|
||||
_g_current_process_mesh = process_mesh
|
||||
|
||||
|
||||
def reset_current_process_mesh():
|
||||
global _g_previous_process_mesh
|
||||
global _g_current_process_mesh
|
||||
_g_current_process_mesh = _g_previous_process_mesh
|
||||
|
||||
|
||||
def get_unique_id_for_process_mesh(shape, process_ids):
|
||||
key = f"shape {shape}, process_ids {process_ids}"
|
||||
global _g_unique_process_mesh_map
|
||||
if key in _g_unique_process_mesh_map:
|
||||
unique_id = _g_unique_process_mesh_map[key]
|
||||
else:
|
||||
unique_id = len(_g_unique_process_mesh_map) + 1
|
||||
_g_unique_process_mesh_map[key] = unique_id
|
||||
|
||||
return unique_id
|
||||
|
||||
|
||||
def retrieve_unique_id_for_process_mesh(shape, process_ids):
|
||||
key = f"shape {shape}, process_ids {process_ids}"
|
||||
global _g_unique_process_mesh_map
|
||||
assert key in _g_unique_process_mesh_map
|
||||
return _g_unique_process_mesh_map[key]
|
||||
|
||||
|
||||
def get_unique_process_mesh_map():
|
||||
global _g_unique_process_mesh_map
|
||||
return _g_unique_process_mesh_map
|
||||
|
||||
|
||||
def init_group_by_process_mesh(dim_names):
|
||||
global _g_group_map
|
||||
if dim_names is None:
|
||||
dim_names = []
|
||||
assert isinstance(dim_names, list), "dim_names must be a list."
|
||||
for dim_name in dim_names:
|
||||
if dim_name in _g_group_map:
|
||||
continue
|
||||
_g_group_map[dim_name] = {}
|
||||
|
||||
|
||||
def get_group_map_by_dim_name(dim_name):
|
||||
global _g_group_map
|
||||
if dim_name not in _g_group_map:
|
||||
raise RuntimeError(f'No group found for dim_name {dim_name}')
|
||||
return _g_group_map[dim_name]
|
||||
|
||||
|
||||
class ProcessMesh(core.ProcessMesh):
|
||||
"""
|
||||
The `ProcessMesh` object describes the Cartesian topology of the used processes.
|
||||
|
||||
Args:
|
||||
mesh (list|numpy.array): an n-dimensional array describes the topology
|
||||
of the processes.
|
||||
dim_names (list, optional): the i-th element of this list gives the name of the
|
||||
i-th dimension of the mesh.
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> mesh = dist.ProcessMesh([[2, 4, 5], [0, 1, 3]], dim_names=["x", "y"])
|
||||
>>> assert mesh.shape == [2, 3]
|
||||
>>> assert mesh.process_ids == [2, 4, 5, 0, 1, 3]
|
||||
|
||||
"""
|
||||
|
||||
shape: list[int]
|
||||
process_ids: list[int]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mesh: npt.NDArray[Any] | NestedNumericSequence | None = None,
|
||||
dim_names: list[str] | None = None,
|
||||
shape: _NumpyShapeLike | None = None,
|
||||
process_ids: Iterable[Any] | None = None,
|
||||
) -> None:
|
||||
paddle.base.framework.global_var._in_auto_parallel_ = True
|
||||
|
||||
# Use shape and process_ids just for compatibility
|
||||
# Users should not use these directly
|
||||
if mesh is None:
|
||||
assert shape is not None
|
||||
assert process_ids is not None
|
||||
mesh = np.array(process_ids).reshape(shape)
|
||||
|
||||
if not isinstance(mesh, list) and not isinstance(mesh, np.ndarray):
|
||||
raise ValueError(
|
||||
'The mesh must be an instance of list or np.ndarray.'
|
||||
)
|
||||
if isinstance(mesh, list):
|
||||
mesh = np.array(mesh)
|
||||
|
||||
if dim_names is not None and not isinstance(dim_names, list):
|
||||
raise ValueError('The dim_names must be an instance of list.')
|
||||
|
||||
self._mesh = mesh
|
||||
self._shape = list(self._mesh.shape)
|
||||
self._process_ids = self._mesh.flatten().tolist()
|
||||
|
||||
assert all(isinstance(p, int) for p in self._process_ids), (
|
||||
"All elements of the mesh must be integer"
|
||||
)
|
||||
assert min(self._process_ids) >= 0, (
|
||||
'All elements of the mesh must be >= 0.'
|
||||
)
|
||||
unique_process_ids = set(self._process_ids)
|
||||
assert len(unique_process_ids) == len(self._process_ids), (
|
||||
'All elements of the mesh must be unique.'
|
||||
)
|
||||
|
||||
if dim_names is not None:
|
||||
assert len(dim_names) == len(self._shape), (
|
||||
"The length of dims_names must be same as the shape of the mesh."
|
||||
)
|
||||
self._dim_names = copy.deepcopy(dim_names)
|
||||
else:
|
||||
self._dim_names = ["d" + str(i) for i in range(len(self._shape))]
|
||||
unique_dim_names = set(self._dim_names)
|
||||
assert len(unique_dim_names) == len(self._dim_names), (
|
||||
f'All dim_names {dim_names} must be unique.'
|
||||
)
|
||||
|
||||
# Follow the requirement for using pybind11
|
||||
core.ProcessMesh.__init__(
|
||||
self, self._shape, self._process_ids, self._dim_names
|
||||
)
|
||||
|
||||
# Store all process meshes
|
||||
from .static.dist_context import get_default_distributed_context
|
||||
|
||||
default_dist_cxt = get_default_distributed_context()
|
||||
default_dist_cxt.add_process_mesh(self)
|
||||
# Add new processes to process group 0
|
||||
from .static.process_group import get_process_group
|
||||
|
||||
pg0 = get_process_group(0)
|
||||
pg0.add_ranks(self.process_ids)
|
||||
|
||||
# Unique Mesh Id
|
||||
self._unique_id = get_unique_id_for_process_mesh(
|
||||
self._shape, self._process_ids
|
||||
)
|
||||
init_group_by_process_mesh(self._dim_names)
|
||||
|
||||
@property
|
||||
def mesh(self) -> npt.NDArray[Any]:
|
||||
"""
|
||||
Get the underlying mesh of ProcessMesh.
|
||||
"""
|
||||
return self._mesh
|
||||
|
||||
@property
|
||||
def dim_names(self) -> list[str]:
|
||||
"""
|
||||
Get the underlying dimension names of ProcessMesh.
|
||||
"""
|
||||
return self._dim_names
|
||||
|
||||
@property
|
||||
def unique_id(self) -> int:
|
||||
"""
|
||||
Get the unique id of ProcessMesh.
|
||||
NOTE
|
||||
Unique id only take process_ids and shape into account.
|
||||
Different ProcessMesh with same process_ids and shape have same unique id.
|
||||
"""
|
||||
return self._unique_id
|
||||
|
||||
def __getitem__(
|
||||
self, index: slice | tuple[slice, ...] | str | SupportsIndex
|
||||
) -> ProcessMesh:
|
||||
if isinstance(index, tuple):
|
||||
new_dim_names = []
|
||||
for i, item in enumerate(index):
|
||||
if isinstance(item, slice):
|
||||
new_dim_names.append(self._dim_names[i])
|
||||
new_mesh = self._mesh[index]
|
||||
if new_mesh.shape:
|
||||
return ProcessMesh(new_mesh, new_dim_names)
|
||||
else:
|
||||
# Wrap a scalar into a list but without dim_names
|
||||
return ProcessMesh([new_mesh])
|
||||
elif isinstance(index, slice):
|
||||
new_mesh = self._mesh[index]
|
||||
new_dim_names = self._dim_names
|
||||
return ProcessMesh(new_mesh, new_dim_names)
|
||||
elif isinstance(index, str):
|
||||
return self.get_submesh_with_dim(index)
|
||||
else:
|
||||
new_mesh = self._mesh[index]
|
||||
new_dim_names = self._dim_names[1:]
|
||||
if new_mesh.shape:
|
||||
return ProcessMesh(new_mesh, new_dim_names)
|
||||
else:
|
||||
return ProcessMesh([new_mesh])
|
||||
|
||||
def get_rank_by_dim_and_process_id(
|
||||
self, dim: str | int, process_id: int
|
||||
) -> int:
|
||||
# do some check
|
||||
if process_id not in self._process_ids:
|
||||
# -1 means invalid rank
|
||||
return -1
|
||||
|
||||
if dim is None:
|
||||
# if dim is None, all process's rank is 0
|
||||
return 0
|
||||
|
||||
if isinstance(dim, int):
|
||||
dim_name = self._dim_names[dim]
|
||||
elif isinstance(dim, str):
|
||||
dim_name = dim
|
||||
else:
|
||||
raise ValueError("dim must be a string or an integer.")
|
||||
dim_name_index = self._dim_names.index(dim_name)
|
||||
rank_index = np.where(self._mesh == process_id)[dim_name_index]
|
||||
return int(rank_index.item())
|
||||
|
||||
def get_dim_size(self, dim: str | int) -> int:
|
||||
if dim is None:
|
||||
return 1
|
||||
|
||||
if isinstance(dim, int):
|
||||
dim_name = self._dim_names[dim]
|
||||
elif isinstance(dim, str):
|
||||
dim_name = dim
|
||||
else:
|
||||
raise ValueError("dim must be a string or an integer.")
|
||||
assert dim_name in self._dim_names
|
||||
return self._shape[self._dim_names.index(dim_name)]
|
||||
|
||||
def get_mesh_with_dim(
|
||||
self,
|
||||
dim_name: str,
|
||||
index: slice | tuple[slice, ...] | SupportsIndex | None = None,
|
||||
) -> ProcessMesh:
|
||||
assert dim_name in self._dim_names, (
|
||||
f'{dim_name} is not a valid dim name.'
|
||||
)
|
||||
index_axis = self._dim_names.index(dim_name)
|
||||
new_order = [index_axis] + [
|
||||
i for i in range(len(self._dim_names)) if i != index_axis
|
||||
]
|
||||
new_dim_names = [dim_name] + [
|
||||
dim for dim in self._dim_names if dim != dim_name
|
||||
]
|
||||
new_mesh = self._mesh.transpose(new_order)
|
||||
|
||||
if index is not None:
|
||||
if len(new_dim_names[1:]) > 0:
|
||||
return ProcessMesh(new_mesh[index], new_dim_names[1:])
|
||||
# satisfy the single dimension mesh case
|
||||
else:
|
||||
return ProcessMesh([new_mesh[index]], new_dim_names)
|
||||
return ProcessMesh(new_mesh, new_dim_names)
|
||||
|
||||
def get_submesh_with_dim(
|
||||
self,
|
||||
dim_name: str,
|
||||
) -> ProcessMesh:
|
||||
"""
|
||||
Slice the current ProcessMesh based on the dim_name given to create a submesh with single dimension remained.
|
||||
|
||||
Args:
|
||||
dim_name (str): the name of the mesh dimension of the ProcessMesh to create the submesh for.
|
||||
Returns:
|
||||
A :class:`ProcessMesh` object
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> dist.init_parallel_env()
|
||||
>>> mesh_2d = dist.ProcessMesh([[0, 1, 2, 3], [4, 5, 6, 7]], dim_names=["dp", "tp"])
|
||||
|
||||
>>> dp_mesh = mesh_2d.get_submesh_with_dim("dp")
|
||||
>>> # ProcessMesh:([0, 4]) on rank 0, 4
|
||||
>>> # ProcessMesh:([1, 5]) on rank 1, 5
|
||||
>>> # ProcessMesh:([2, 6]) on rank 2, 6
|
||||
>>> # ProcessMesh:([3, 7]) on rank 3, 7
|
||||
|
||||
>>> tp_mesh = mesh_2d.get_submesh_with_dim("tp")
|
||||
>>> # ProcessMesh:([0, 1, 2, 3]) on rank 0, 1, 2, 3
|
||||
>>> # ProcessMesh:([4, 5, 6, 7]) on rank 4, 5, 6, 7
|
||||
|
||||
>>> mesh_3d = dist.ProcessMesh([[[0, 1], [2, 3]], [[4, 5], [6, 7]]], dim_names=["pp", "dp", "tp"])
|
||||
|
||||
>>> pp_mesh = mesh_3d.get_submesh_with_dim("pp")
|
||||
>>> # ProcessMesh:([0, 4]) on rank 0, 4
|
||||
>>> # ProcessMesh:([1, 5]) on rank 1, 5
|
||||
>>> # ProcessMesh:([2, 6]) on rank 2, 6
|
||||
>>> # ProcessMesh:([3, 7]) on rank 3, 7
|
||||
|
||||
>>> dp_mesh = mesh_3d.get_submesh_with_dim("dp")
|
||||
>>> # ProcessMesh:([0, 2]) on rank 0, 2
|
||||
>>> # ProcessMesh:([1, 3]) on rank 1, 3
|
||||
>>> # ProcessMesh:([4, 6]) on rank 4, 6
|
||||
>>> # ProcessMesh:([5, 7]) on rank 5, 7
|
||||
|
||||
>>> tp_mesh = mesh_3d.get_submesh_with_dim("tp")
|
||||
>>> # ProcessMesh:([0, 1]) on rank 0, 1
|
||||
>>> # ProcessMesh:([2, 3]) on rank 2, 3
|
||||
>>> # ProcessMesh:([4, 5]) on rank 4, 5
|
||||
>>> # ProcessMesh:([6, 7]) on rank 6, 7
|
||||
"""
|
||||
|
||||
reorder_mesh = self.get_mesh_with_dim(dim_name)._mesh.reshape(
|
||||
self.get_dim_size(dim_name), -1
|
||||
)
|
||||
curr_rank = paddle.distributed.get_rank()
|
||||
if curr_rank not in self._process_ids:
|
||||
logger.warning(
|
||||
f"Rank {curr_rank} is not in the process mesh, just return None"
|
||||
)
|
||||
return None
|
||||
# find curr_rank in reorder_mesh, get the column index
|
||||
col_idx = np.argmax(reorder_mesh == curr_rank) % reorder_mesh.shape[-1]
|
||||
sub_mesh = ProcessMesh(reorder_mesh[:, col_idx], [dim_name])
|
||||
return sub_mesh
|
||||
|
||||
def _get_group(
|
||||
self,
|
||||
dim_name: str | None = None,
|
||||
) -> paddle.distributed.communication.group.Group:
|
||||
""" """
|
||||
assert is_initialized(), (
|
||||
"When you want to get a group from the ProcessMesh."
|
||||
" Call paddle.distributed.init_parallel_env first "
|
||||
"to initialize the distributed environment."
|
||||
)
|
||||
if len(self._dim_names) > 1 and dim_name is None:
|
||||
raise ValueError(
|
||||
"You should specify the dim_name when the ProcessMesh has more than one dimensions."
|
||||
)
|
||||
reorder_mesh = self.get_mesh_with_dim(dim_name)._mesh.reshape(
|
||||
self.get_dim_size(dim_name), -1
|
||||
)
|
||||
curr_rank = paddle.distributed.get_rank()
|
||||
groups = get_group_map_by_dim_name(dim_name)
|
||||
|
||||
for rank in self._process_ids:
|
||||
col_idx = np.argmax(reorder_mesh == rank) % reorder_mesh.shape[-1]
|
||||
if col_idx in groups:
|
||||
continue
|
||||
pg = paddle.distributed.new_group(reorder_mesh[:, col_idx])
|
||||
groups[col_idx] = pg
|
||||
|
||||
cur_col_idx = (
|
||||
np.argmax(reorder_mesh == curr_rank) % reorder_mesh.shape[-1]
|
||||
)
|
||||
return groups[cur_col_idx]
|
||||
|
||||
def get_group(
|
||||
self,
|
||||
dim_name: str | None = None,
|
||||
) -> paddle.distributed.communication.group.Group:
|
||||
"""
|
||||
Convert single dimension ProcessMesh to the corresponding Group.
|
||||
|
||||
Args:
|
||||
dim_name (str, optional): it can be the name of the mesh dimension. Default is None.
|
||||
|
||||
Returns:
|
||||
A :class:`Group` object.
|
||||
"""
|
||||
|
||||
# check parallel environment whether ready or not
|
||||
assert is_initialized(), (
|
||||
"When you want to get a group from the ProcessMesh."
|
||||
" Call paddle.distributed.init_parallel_env first "
|
||||
"to initialize the distributed environment."
|
||||
)
|
||||
if len(self._dim_names) > 1 and dim_name is None:
|
||||
raise ValueError(
|
||||
"You should specify the dim_name when the ProcessMesh has more than one dimensions."
|
||||
)
|
||||
if len(self._dim_names) == 1:
|
||||
if dim_name is not None and dim_name not in self._dim_names:
|
||||
raise ValueError(
|
||||
f"{dim_name} not in the dimension names {self._dim_names}"
|
||||
)
|
||||
else:
|
||||
if hasattr(fleet.fleet, "_hcg"):
|
||||
hcg = fleet.get_hybrid_communicate_group()
|
||||
if hcg is not None:
|
||||
parallel_group_map = {
|
||||
"pp": hcg.get_pipe_parallel_group,
|
||||
"dp": hcg.get_data_parallel_group,
|
||||
"mp": hcg.get_model_parallel_group,
|
||||
"sep": hcg.get_sep_parallel_group,
|
||||
"sharding": hcg.get_sharding_parallel_group,
|
||||
}
|
||||
|
||||
if dim_name not in parallel_group_map:
|
||||
raise ValueError(
|
||||
f"{dim_name} is not a valid dim name."
|
||||
)
|
||||
|
||||
return parallel_group_map[dim_name]()
|
||||
group_map = _get_group_map()
|
||||
for group in group_map.values():
|
||||
if set(group.ranks) == set(self._process_ids):
|
||||
return group
|
||||
return paddle.distributed.new_group(self._process_ids)
|
||||
else:
|
||||
if dim_name not in self._dim_names:
|
||||
raise ValueError(
|
||||
f"{dim_name} not in the dimension names {self._dim_names}"
|
||||
)
|
||||
sub_mesh = self.get_submesh_with_dim(dim_name)
|
||||
return sub_mesh.get_group(dim_name)
|
||||
|
||||
def __enter__(self) -> None:
|
||||
set_current_process_mesh(self)
|
||||
default_prog = paddle.static.default_main_program()
|
||||
cur_block = default_prog.current_block()
|
||||
self._old_var_names = list(cur_block.vars.keys())
|
||||
self._old_op_size = len(cur_block.ops)
|
||||
|
||||
def __exit__(
|
||||
self,
|
||||
exc_type: type[BaseException] | None,
|
||||
exc_value: BaseException | None,
|
||||
traceback: TracebackType | None,
|
||||
) -> None:
|
||||
from .static.dist_op import DistributedOperator
|
||||
from .static.dist_tensor import DistributedTensor
|
||||
|
||||
default_prog = paddle.static.default_main_program()
|
||||
cur_block = default_prog.current_block()
|
||||
new_var_names = list(cur_block.vars.keys())
|
||||
new_op_size = len(cur_block.ops)
|
||||
from .static.dist_context import get_default_distributed_context
|
||||
|
||||
default_dist_ctx = get_default_distributed_context()
|
||||
for name in new_var_names:
|
||||
if name not in self._old_var_names:
|
||||
tensor = cur_block.vars[name]
|
||||
dist_tensor = default_dist_ctx.get_dist_tensor_for_program(
|
||||
tensor
|
||||
)
|
||||
if dist_tensor is None:
|
||||
dist_tensor = DistributedTensor(cur_block.vars[name])
|
||||
dist_tensor.dist_attr.process_mesh = self
|
||||
dist_tensor.dist_attr.mark_annotated("process_mesh")
|
||||
default_dist_ctx.add_dist_tensor_for_program(dist_tensor)
|
||||
else:
|
||||
if dist_tensor.dist_attr.process_mesh is None:
|
||||
dist_tensor.dist_attr.process_mesh = self
|
||||
dist_tensor.dist_attr.mark_annotated("process_mesh")
|
||||
|
||||
for idx in range(self._old_op_size, new_op_size):
|
||||
op = cur_block.ops[idx]
|
||||
dist_op = default_dist_ctx.get_dist_op_for_program(op)
|
||||
if dist_op is None:
|
||||
dist_op = DistributedOperator(op)
|
||||
dist_op.dist_attr.process_mesh = self
|
||||
dist_op.dist_attr.mark_annotated("process_mesh")
|
||||
default_dist_ctx.add_dist_op_for_program(dist_op)
|
||||
else:
|
||||
if dist_op.dist_attr.process_mesh is None:
|
||||
dist_op.dist_attr.process_mesh = self
|
||||
dist_op.dist_attr.mark_annotated("process_mesh")
|
||||
reset_current_process_mesh()
|
||||
|
||||
def __deepcopy__(self, memo: Any) -> ProcessMesh:
|
||||
if id(self) in memo:
|
||||
return memo[id(self)]
|
||||
new_process_mesh = ProcessMesh(np.array(self.mesh), self.dim_names)
|
||||
memo[id(self)] = new_process_mesh
|
||||
return new_process_mesh
|
||||
|
||||
def __eq__(self, other: ProcessMesh | core.ProcessMesh) -> bool:
|
||||
if not isinstance(other, (ProcessMesh, core.ProcessMesh)):
|
||||
return False
|
||||
if self.shape != other.shape or self.process_ids != other.process_ids:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other: ProcessMesh | core.ProcessMesh) -> None:
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self) -> str:
|
||||
str = f"shape {self.shape}, process_ids {self.process_ids}, dim_names {self.dim_names}"
|
||||
return str
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return super().__hash__()
|
||||
|
||||
|
||||
def compute_compatible_process_mesh(process_mesh_list):
|
||||
"""Compute the compatible process mesh given a list of process meshes."""
|
||||
if not process_mesh_list:
|
||||
return None
|
||||
|
||||
def _compute_compatible_process_mesh_of_two(pm1, pm2):
|
||||
if pm1 is None:
|
||||
return True, pm2
|
||||
if pm2 is None:
|
||||
return True, pm1
|
||||
if pm1 == pm2:
|
||||
return True, pm1
|
||||
if pm1.process_ids == pm2.process_ids:
|
||||
if len(pm1.shape) >= len(pm2.shape):
|
||||
return True, pm1
|
||||
else:
|
||||
return True, pm2
|
||||
process_set1 = set(pm1.process_ids)
|
||||
process_set2 = set(pm2.process_ids)
|
||||
if process_set1.issubset(process_set2):
|
||||
return True, pm2
|
||||
if process_set2.issubset(process_set1):
|
||||
return True, pm1
|
||||
return False, None
|
||||
|
||||
compatible_result = None
|
||||
for process_mesh in process_mesh_list:
|
||||
compatible, compatible_result = _compute_compatible_process_mesh_of_two(
|
||||
compatible_result, process_mesh
|
||||
)
|
||||
if not compatible:
|
||||
return None
|
||||
return copy.deepcopy(compatible_result)
|
||||
|
||||
|
||||
def merge_process_meshes(process_meshes):
|
||||
"""Merge a list of process meshes."""
|
||||
merged_process_mesh = None
|
||||
merged_process_ids = set()
|
||||
for process_mesh in process_meshes:
|
||||
if process_mesh is not None:
|
||||
process_ids = set(process_mesh.process_ids)
|
||||
merged_process_ids = merged_process_ids.union(process_ids)
|
||||
if len(merged_process_ids) != 0:
|
||||
merged_process_mesh = ProcessMesh(list(merged_process_ids))
|
||||
return merged_process_mesh
|
||||
@@ -0,0 +1,176 @@
|
||||
# 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 contextlib
|
||||
import logging
|
||||
|
||||
import paddle
|
||||
|
||||
from ..utils.log_utils import get_logger
|
||||
from .process_mesh import retrieve_unique_id_for_process_mesh
|
||||
from .static.utils import _get_idx_in_axis
|
||||
|
||||
_logger = get_logger(logging.INFO)
|
||||
|
||||
_rng_name_to_seed = {}
|
||||
_rng_name_to_states = {}
|
||||
_inited_rng_name_to_seed = {}
|
||||
_enable_random_control = False
|
||||
_basic_seed = 42
|
||||
_basic_name = ""
|
||||
|
||||
# use Prime number as offset to avoid conflict
|
||||
_mesh_offset = 173
|
||||
_dim_offsets = [11, 23, 37, 73]
|
||||
|
||||
|
||||
def is_enable_auto_rand_ctrl():
|
||||
global _enable_random_control
|
||||
return _enable_random_control
|
||||
|
||||
|
||||
def enable_auto_rand_ctrl():
|
||||
global _enable_random_control
|
||||
_enable_random_control = True
|
||||
|
||||
|
||||
def parallel_manual_seed(seed: int, name: str = "") -> None:
|
||||
"""Enable auto parallel random control.
|
||||
Random control maintain the randomness when tensor is distributed across devices on a Mesh(any order).
|
||||
* Independency: If tensor is **Sharded** on a Mesh dimension, Devices along that Mesh dimension should have Different randomness.
|
||||
|
||||
* Consistency: Meanwhile if the tensor is **Replicated** on another Mesh dimension, randomness of Devices along that Mesh dimension should be Consistent.
|
||||
|
||||
For instance: rank0 ~ rank7 consist a Mesh of shape of [2, 4]; A 2D tensor is distributed in that Mesh using dims_mapping [-1, 1].
|
||||
Randomness for rank0-rank1-rank2-rank3 (rank4-rank5-rank6-rank7) should be Independent;
|
||||
Randomness for rank0 and rank4 (rank1 and rank5, ...) should be Consistent.
|
||||
|
||||
This function should be called only once before auto parallel compiles the computation graph (e.g. auto_parallel.engine.prepare() or fit()).
|
||||
|
||||
This seed only affects how randomness-relative **operators** (dropout, fuse op with dropout inside, etc) are execute among mesh, and would NOT affect other process like Parameter initialization.
|
||||
|
||||
Examples:
|
||||
# seed relative to training step
|
||||
auto_parallel_random_seed((step + 13) * 257)
|
||||
...
|
||||
engine.prepare()
|
||||
"""
|
||||
|
||||
enable_auto_rand_ctrl()
|
||||
global _basic_seed
|
||||
_basic_seed = seed
|
||||
global _basic_name
|
||||
_basic_name = name
|
||||
|
||||
|
||||
def determinate_rng(
|
||||
rank, dims_mapping=None, process_mesh=None, placements=None
|
||||
):
|
||||
assert process_mesh is not None, "Must provide process mesh"
|
||||
assert dims_mapping is not None or placements is not None, (
|
||||
"Must provide one of dims mapping or placements."
|
||||
)
|
||||
assert not (dims_mapping is not None and placements is not None), (
|
||||
"Cannot provide dims mapping and placements at same time."
|
||||
)
|
||||
# TODO(JZ-LIANG) Support Mesh with any high rank
|
||||
# use a string to unique integer hashing algorithm for seed computation.
|
||||
# instead of using offsets to coordinate seed across devices.
|
||||
if len(process_mesh.shape) > 4:
|
||||
raise NotImplementedError(
|
||||
f"Auto Parallel Random Control for Mesh's rank > 4 is NOT supported! Got {process_mesh}"
|
||||
)
|
||||
global _basic_seed
|
||||
seed_ = _basic_seed
|
||||
global _basic_name
|
||||
name_ = _basic_name
|
||||
|
||||
if name_:
|
||||
name_ += "_"
|
||||
|
||||
# FIXME
|
||||
# unique_id = process_mesh.unique_id
|
||||
unique_id = retrieve_unique_id_for_process_mesh(
|
||||
process_mesh.shape, process_mesh.process_ids
|
||||
)
|
||||
sharding_expr = name_ + f'mesh:{unique_id}'
|
||||
seed_ += _mesh_offset * (unique_id + 1)
|
||||
|
||||
for i in range(len(process_mesh.shape)):
|
||||
if (dims_mapping is not None and i not in dims_mapping) or (
|
||||
placements is not None and not placements[i].is_shard()
|
||||
):
|
||||
relative_idx = -1
|
||||
else:
|
||||
relative_idx = _get_idx_in_axis(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
i,
|
||||
rank,
|
||||
)
|
||||
|
||||
sharding_expr += f"_dim{i}:{relative_idx}"
|
||||
seed_ += _dim_offsets[i] * (relative_idx + 1)
|
||||
|
||||
global _rng_name_to_seed
|
||||
global _rng_name_to_states
|
||||
if sharding_expr in _rng_name_to_seed:
|
||||
assert _rng_name_to_seed[sharding_expr] == seed_
|
||||
else:
|
||||
assert seed_ not in _rng_name_to_seed.values(), (
|
||||
f"Seed Conflict! current seed: {seed_}, current sharding expr: {sharding_expr}, generated seed: {_rng_name_to_seed}"
|
||||
)
|
||||
_rng_name_to_seed[sharding_expr] = seed_
|
||||
if paddle.in_dynamic_mode():
|
||||
# for dygraph, just init the seed when meeting a new seed
|
||||
orig_rng_state = paddle.get_rng_state()
|
||||
paddle.seed(seed_)
|
||||
_rng_name_to_states[sharding_expr] = paddle.get_rng_state()
|
||||
paddle.set_rng_state(orig_rng_state)
|
||||
return sharding_expr
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def rng_state(name):
|
||||
global _rng_name_to_states
|
||||
assert name in _rng_name_to_states, (
|
||||
f"The rng state name {name} haven't been init. "
|
||||
)
|
||||
orig_rng_state = paddle.get_rng_state()
|
||||
paddle.set_rng_state(_rng_name_to_states[name])
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
_rng_name_to_states[name] = paddle.get_rng_state()
|
||||
paddle.set_rng_state(orig_rng_state)
|
||||
|
||||
|
||||
def init_auto_parallel_rng():
|
||||
if not is_enable_auto_rand_ctrl():
|
||||
return
|
||||
|
||||
global _rng_name_to_seed
|
||||
# NOTE init rng maybe call multiple times, avoid init same rng twice
|
||||
global _inited_rng_name_to_seed
|
||||
|
||||
for rng_name, seed in _rng_name_to_seed.items():
|
||||
if rng_name in _inited_rng_name_to_seed:
|
||||
assert _inited_rng_name_to_seed[rng_name] == seed
|
||||
else:
|
||||
_logger.info(
|
||||
f"Init Auto Parallel RNG: {rng_name}, with seed {seed}"
|
||||
)
|
||||
paddle.framework.random.set_random_seed_generator(rng_name, seed)
|
||||
_inited_rng_name_to_seed[rng_name] = seed
|
||||
@@ -0,0 +1,542 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
import paddle.nn.functional as F
|
||||
from paddle import _C_ops
|
||||
|
||||
|
||||
def shard_seq_load_balance(tensor, seq_dim):
|
||||
# dtensor Replicate() -> reorder -> Shard(seq_dim)
|
||||
placements = tensor.placements
|
||||
process_mesh = tensor.process_mesh
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
if cp_degree > 1:
|
||||
# split
|
||||
sliced_datas = paddle.split(
|
||||
tensor, num_or_sections=cp_degree * 2, axis=seq_dim
|
||||
)
|
||||
# resort [q0,q1,q2,q3] -> [q0,q3,q1,q2]
|
||||
indices = []
|
||||
for i in range(cp_degree):
|
||||
indices.append(i)
|
||||
indices.append(cp_degree * 2 - 1 - i)
|
||||
reorder_indices = indices
|
||||
reordered = [sliced_datas[i] for i in reorder_indices]
|
||||
reordered_tensor = paddle.concat(reordered, axis=seq_dim)
|
||||
# reshard q/k/v -> Shard(seq_dim)
|
||||
placements[cp_index] = paddle.distributed.Shard(seq_dim) # seq_dim:1
|
||||
tensor = paddle.distributed.reshard(
|
||||
reordered_tensor, process_mesh, placements
|
||||
)
|
||||
return tensor
|
||||
|
||||
|
||||
def unshard_seq_load_balance(tensor, seq_dim):
|
||||
# dtensor Shard(seq_dim) -> Replicate() -> reorder
|
||||
placements = tensor.placements
|
||||
process_mesh = tensor.process_mesh
|
||||
cp_index = process_mesh.dim_names.index('sep')
|
||||
cp_degree = process_mesh.shape[cp_index]
|
||||
all_tensor = dist.reshard(tensor, process_mesh, [dist.Replicate()])
|
||||
sliced_datas = paddle.split(
|
||||
all_tensor, num_or_sections=cp_degree * 2, axis=seq_dim
|
||||
)
|
||||
reorder_indices = []
|
||||
for i in range(cp_degree):
|
||||
reorder_indices.append(i)
|
||||
reorder_indices.append(cp_degree * 2 - 1 - i)
|
||||
inverse_indices = [0] * len(reorder_indices)
|
||||
for idx, v in enumerate(reorder_indices):
|
||||
inverse_indices[v] = idx
|
||||
restored = [sliced_datas[i] for i in inverse_indices]
|
||||
return paddle.concat(restored, axis=seq_dim)
|
||||
|
||||
|
||||
class RingCommunicator:
|
||||
def __init__(self, group, local_key, local_value):
|
||||
self._k_buffer = [
|
||||
local_key.clone().contiguous(),
|
||||
local_key.clone().contiguous(),
|
||||
]
|
||||
self._v_buffer = [
|
||||
local_value.clone().contiguous(),
|
||||
local_value.clone().contiguous(),
|
||||
]
|
||||
|
||||
self._next_buffer_idx = 0
|
||||
|
||||
self.group = group
|
||||
mesh = dist.auto_parallel.get_mesh()
|
||||
process_id = dist.get_rank()
|
||||
self.group_rank = mesh.get_rank_by_dim_and_process_id("sep", process_id)
|
||||
self.cp_size = mesh.get_dim_size("sep")
|
||||
cp_index = mesh.dim_names.index("sep")
|
||||
|
||||
self.send_rank = self.group.ranks[
|
||||
(self.group_rank + 1) % self.cp_size
|
||||
] # 1%2=1
|
||||
self.recv_rank = self.group.ranks[(self.group_rank - 1) % self.cp_size]
|
||||
|
||||
self._reqs = []
|
||||
|
||||
def wait(self):
|
||||
paddle.device.synchronize()
|
||||
|
||||
def add_to_buffers(self, key, value):
|
||||
if key.shape != self._k_buffer[self._next_buffer_idx].shape:
|
||||
self._k_buffer[self._next_buffer_idx][:, : key.shape[1], :, :].add_(
|
||||
key
|
||||
)
|
||||
self._v_buffer[self._next_buffer_idx][:, : key.shape[1], :, :].add_(
|
||||
value
|
||||
)
|
||||
else:
|
||||
self._k_buffer[self._next_buffer_idx].add_(key)
|
||||
self._v_buffer[self._next_buffer_idx].add_(value)
|
||||
|
||||
def get_buffers(self):
|
||||
return (
|
||||
self._k_buffer[self._next_buffer_idx],
|
||||
self._v_buffer[self._next_buffer_idx],
|
||||
)
|
||||
|
||||
def send_recv(self):
|
||||
send_k_op = dist.P2POp(
|
||||
dist.isend,
|
||||
self._k_buffer[self._next_buffer_idx].contiguous(),
|
||||
self.send_rank,
|
||||
self.group,
|
||||
)
|
||||
send_v_op = dist.P2POp(
|
||||
dist.isend,
|
||||
self._v_buffer[self._next_buffer_idx].contiguous(),
|
||||
self.send_rank,
|
||||
self.group,
|
||||
)
|
||||
recv_k_op = dist.P2POp(
|
||||
dist.irecv,
|
||||
self._k_buffer[(self._next_buffer_idx + 1) % 2],
|
||||
self.recv_rank,
|
||||
self.group,
|
||||
)
|
||||
recv_v_op = dist.P2POp(
|
||||
dist.irecv,
|
||||
self._v_buffer[(self._next_buffer_idx + 1) % 2],
|
||||
self.recv_rank,
|
||||
self.group,
|
||||
)
|
||||
|
||||
self._next_buffer_idx = (self._next_buffer_idx + 1) % 2
|
||||
|
||||
ops = [send_k_op, send_v_op, recv_k_op, recv_v_op]
|
||||
|
||||
self._reqs = dist.batch_isend_irecv(ops)
|
||||
|
||||
|
||||
def update_out_and_lse(
|
||||
old_out, old_lse, block_out, block_lse, second_chunk_only=False
|
||||
):
|
||||
if second_chunk_only:
|
||||
second_chunk_out = old_out[:, old_out.shape[1] // 2 :, :, :]
|
||||
second_chunk_lse = old_lse[:, old_lse.shape[1] // 2 :, :, :]
|
||||
second_chunk_out, second_chunk_lse = update_out_and_lse(
|
||||
second_chunk_out, second_chunk_lse, block_out, block_lse
|
||||
)
|
||||
old_out[:, old_out.shape[1] // 2 :, :, :] = second_chunk_out
|
||||
old_lse[:, old_lse.shape[1] // 2 :, :, :] = second_chunk_lse
|
||||
return old_out, old_lse
|
||||
else:
|
||||
block_out, block_lse = (
|
||||
paddle.cast(block_out, "float32"),
|
||||
paddle.cast(block_lse, "float32"),
|
||||
)
|
||||
with paddle.amp.auto_cast(enable=False):
|
||||
return old_out - (old_out - block_out) * F.sigmoid(
|
||||
block_lse - old_lse
|
||||
), old_lse - F.log_sigmoid(old_lse - block_lse)
|
||||
|
||||
|
||||
def get_chunk_id(rank, cp_size):
|
||||
return rank, (2 * cp_size - 1 - rank)
|
||||
|
||||
|
||||
def concat_masks(attn_masks_list, rank, cp_size):
|
||||
assert len(attn_masks_list) == 2 * cp_size
|
||||
first_chunk_id, second_chunk_id = get_chunk_id(rank, cp_size)
|
||||
return paddle.concat(
|
||||
[attn_masks_list[first_chunk_id], attn_masks_list[second_chunk_id]],
|
||||
axis=3,
|
||||
)
|
||||
|
||||
|
||||
def ring_flash_attention_forward_func(
|
||||
group,
|
||||
local_query,
|
||||
local_key,
|
||||
local_value,
|
||||
attn_mask=None,
|
||||
dropout=0.0,
|
||||
is_causal=False,
|
||||
fixed_seed_offset=None,
|
||||
training=True,
|
||||
):
|
||||
cp_size = group.world_size
|
||||
group_rank = group.rank
|
||||
|
||||
comm_buffer = RingCommunicator(group, local_key, local_value)
|
||||
local_q_seq_len = local_query.shape[1]
|
||||
if attn_mask is not None:
|
||||
attn_masks_list = paddle.split(
|
||||
attn_mask, num_or_sections=cp_size * 2, axis=3
|
||||
)
|
||||
if is_causal:
|
||||
local_query_second_chunk = local_query[
|
||||
:, local_q_seq_len // 2 :, :, :
|
||||
].contiguous()
|
||||
for step in range(cp_size):
|
||||
block_k, block_v = comm_buffer.get_buffers()
|
||||
if step != cp_size - 1:
|
||||
comm_buffer.send_recv()
|
||||
if not is_causal:
|
||||
# out [bs, seq, nhead, headdim]
|
||||
# lse [bs, nhead, seq]
|
||||
block_out, _, block_lse, _ = _C_ops.flash_attn(
|
||||
local_query,
|
||||
block_k,
|
||||
block_v,
|
||||
fixed_seed_offset,
|
||||
(
|
||||
None
|
||||
if attn_mask is None
|
||||
else concat_masks(
|
||||
attn_masks_list, (group_rank - step) % cp_size, cp_size
|
||||
)
|
||||
),
|
||||
dropout,
|
||||
False,
|
||||
False,
|
||||
not training,
|
||||
"",
|
||||
)
|
||||
paddle.unsqueeze_(paddle.transpose_(block_lse, [0, 2, 1]), axis=-1)
|
||||
|
||||
if step == 0:
|
||||
out, lse = block_out, block_lse
|
||||
else:
|
||||
out, lse = update_out_and_lse(out, lse, block_out, block_lse)
|
||||
else:
|
||||
if step == 0:
|
||||
block_out, _, block_lse, _ = _C_ops.flash_attn(
|
||||
local_query,
|
||||
block_k,
|
||||
block_v,
|
||||
fixed_seed_offset,
|
||||
None,
|
||||
dropout,
|
||||
True,
|
||||
False,
|
||||
not training,
|
||||
"",
|
||||
)
|
||||
paddle.unsqueeze_(
|
||||
paddle.transpose_(block_lse, [0, 2, 1]), axis=-1
|
||||
)
|
||||
out, lse = block_out, block_lse
|
||||
elif step > group_rank:
|
||||
block_out, _, block_lse, _ = _C_ops.flash_attn(
|
||||
local_query_second_chunk,
|
||||
block_k,
|
||||
block_v,
|
||||
fixed_seed_offset,
|
||||
None,
|
||||
dropout,
|
||||
False,
|
||||
False,
|
||||
not training,
|
||||
"",
|
||||
)
|
||||
block_lse = block_lse[:, :, 0 : (local_q_seq_len // 2)]
|
||||
paddle.unsqueeze_(
|
||||
paddle.transpose_(block_lse, [0, 2, 1]), axis=-1
|
||||
)
|
||||
out, lse = update_out_and_lse(
|
||||
out, lse, block_out, block_lse, True
|
||||
)
|
||||
else:
|
||||
block_out, _, block_lse, _ = _C_ops.flash_attn(
|
||||
local_query,
|
||||
block_k[:, : local_q_seq_len // 2, :, :],
|
||||
block_v[:, : local_q_seq_len // 2, :, :],
|
||||
fixed_seed_offset,
|
||||
None,
|
||||
dropout,
|
||||
False,
|
||||
False,
|
||||
not training,
|
||||
"",
|
||||
)
|
||||
paddle.unsqueeze_(
|
||||
paddle.transpose_(block_lse, [0, 2, 1]), axis=-1
|
||||
)
|
||||
out, lse = update_out_and_lse(out, lse, block_out, block_lse)
|
||||
|
||||
paddle.device.synchronize()
|
||||
out = paddle.cast(out, local_query.dtype)
|
||||
lse = paddle.transpose_(paddle.squeeze(lse, axis=-1), [0, 2, 1])
|
||||
return out, lse
|
||||
|
||||
|
||||
def ring_flash_attention_backward_func(
|
||||
group,
|
||||
local_out_grad,
|
||||
local_query,
|
||||
local_key,
|
||||
local_value,
|
||||
local_out,
|
||||
lse,
|
||||
attn_mask,
|
||||
dropout=0.0,
|
||||
is_causal=False,
|
||||
fixed_seed_offset=None,
|
||||
):
|
||||
cp_size = group.world_size
|
||||
group_rank = group.rank
|
||||
|
||||
lse = lse.contiguous()
|
||||
|
||||
local_q_seq_len = local_query.shape[1]
|
||||
query_grad_buffer = paddle.zeros_like(local_query)
|
||||
key_grad_buffer = paddle.zeros_like(local_key)
|
||||
value_grad_buffer = paddle.zeros_like(local_value)
|
||||
|
||||
kv_comm_buffer = RingCommunicator(group, local_key, local_value)
|
||||
grad_comm_buffer = RingCommunicator(
|
||||
group, key_grad_buffer, value_grad_buffer
|
||||
)
|
||||
if is_causal:
|
||||
local_query_second_chunk = local_query[:, local_q_seq_len // 2 :, :, :]
|
||||
local_out_second_chunk = local_out[:, local_q_seq_len // 2 :, :, :]
|
||||
lse_second_chunk = lse[:, :, local_q_seq_len // 2 :].contiguous()
|
||||
out_grad_second_chunk = local_out_grad[:, local_q_seq_len // 2 :, :, :]
|
||||
|
||||
if attn_mask is not None:
|
||||
attn_masks_list = paddle.split(
|
||||
attn_mask, num_or_sections=cp_size * 2, axis=3
|
||||
)
|
||||
for step in range(cp_size):
|
||||
block_k, block_v = kv_comm_buffer.get_buffers()
|
||||
if step != cp_size - 1:
|
||||
kv_comm_buffer.send_recv()
|
||||
|
||||
if not is_causal:
|
||||
block_q_grad, block_k_grad, block_v_grad = _C_ops.flash_attn_grad(
|
||||
local_query,
|
||||
block_k,
|
||||
block_v,
|
||||
local_out,
|
||||
lse,
|
||||
fixed_seed_offset,
|
||||
(
|
||||
None
|
||||
if attn_mask is None
|
||||
else concat_masks(
|
||||
attn_masks_list, (group_rank - step) % cp_size, cp_size
|
||||
)
|
||||
),
|
||||
local_out_grad,
|
||||
dropout,
|
||||
False,
|
||||
)
|
||||
query_grad_buffer.add_(block_q_grad)
|
||||
else:
|
||||
if step == 0:
|
||||
block_q_grad, block_k_grad, block_v_grad = (
|
||||
_C_ops.flash_attn_grad(
|
||||
local_query,
|
||||
block_k,
|
||||
block_v,
|
||||
local_out,
|
||||
lse,
|
||||
fixed_seed_offset,
|
||||
None,
|
||||
local_out_grad,
|
||||
dropout,
|
||||
True,
|
||||
)
|
||||
)
|
||||
query_grad_buffer.add_(block_q_grad)
|
||||
elif step > group_rank:
|
||||
block_q_grad, block_k_grad, block_v_grad = (
|
||||
_C_ops.flash_attn_grad(
|
||||
local_query_second_chunk,
|
||||
block_k,
|
||||
block_v,
|
||||
local_out_second_chunk,
|
||||
lse_second_chunk,
|
||||
fixed_seed_offset,
|
||||
None,
|
||||
out_grad_second_chunk,
|
||||
dropout,
|
||||
False,
|
||||
)
|
||||
)
|
||||
query_grad_buffer[:, local_q_seq_len // 2 :, :, :].add_(
|
||||
block_q_grad
|
||||
)
|
||||
else:
|
||||
block_q_grad, block_k_grad, block_v_grad = (
|
||||
_C_ops.flash_attn_grad(
|
||||
local_query,
|
||||
block_k[:, : local_q_seq_len // 2, :, :],
|
||||
block_v[:, : local_q_seq_len // 2, :, :],
|
||||
local_out,
|
||||
lse,
|
||||
fixed_seed_offset,
|
||||
None,
|
||||
local_out_grad,
|
||||
dropout,
|
||||
False,
|
||||
)
|
||||
)
|
||||
query_grad_buffer.add_(block_q_grad)
|
||||
paddle.device.synchronize()
|
||||
|
||||
grad_comm_buffer.add_to_buffers(
|
||||
block_k_grad.contiguous(), block_v_grad.contiguous()
|
||||
)
|
||||
grad_comm_buffer.send_recv()
|
||||
|
||||
grad_comm_buffer.wait()
|
||||
key_grad_buffer, value_grad_buffer = grad_comm_buffer.get_buffers()
|
||||
|
||||
return query_grad_buffer, key_grad_buffer, value_grad_buffer
|
||||
|
||||
|
||||
class RingFlashAttention(paddle.autograd.PyLayer):
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
query,
|
||||
key,
|
||||
value,
|
||||
attn_mask=None,
|
||||
dropout=0.0,
|
||||
is_causal=False,
|
||||
fixed_seed_offset=None,
|
||||
training=True,
|
||||
):
|
||||
if dropout > 0.0:
|
||||
raise NotImplementedError(
|
||||
"Dropout is not supported in ring attention yet."
|
||||
)
|
||||
mesh = dist.auto_parallel.get_mesh()
|
||||
cp_index = mesh.dim_names.index('sep')
|
||||
process_id = dist.get_rank()
|
||||
rank = mesh.get_rank_by_dim_and_process_id("sep", process_id)
|
||||
dist.init_parallel_env()
|
||||
|
||||
group = mesh._get_group("sep")
|
||||
local_query = dist.auto_parallel.api.dtensor_to_local(
|
||||
query, query.process_mesh, query.placements
|
||||
)
|
||||
local_key = dist.auto_parallel.api.dtensor_to_local(
|
||||
key, key.process_mesh, key.placements
|
||||
)
|
||||
local_value = dist.auto_parallel.api.dtensor_to_local(
|
||||
value, value.process_mesh, value.placements
|
||||
)
|
||||
if attn_mask is not None:
|
||||
is_causal = False
|
||||
|
||||
out, lse = ring_flash_attention_forward_func(
|
||||
group,
|
||||
local_query,
|
||||
local_key,
|
||||
local_value,
|
||||
attn_mask,
|
||||
dropout,
|
||||
is_causal,
|
||||
fixed_seed_offset,
|
||||
training,
|
||||
)
|
||||
ctx.save_for_backward(group, query, key, value, out, lse, attn_mask)
|
||||
ctx.fixed_seed_offset = fixed_seed_offset
|
||||
ctx.dropout = dropout
|
||||
ctx.is_causal = is_causal
|
||||
out_dtensor = dist.auto_parallel.api.dtensor_from_local(
|
||||
out, query.process_mesh, query.placements
|
||||
)
|
||||
return out_dtensor.contiguous()
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, out_grad):
|
||||
mesh = dist.auto_parallel.get_mesh()
|
||||
cp_index = mesh.dim_names.index('sep')
|
||||
group, query, key, value, out, lse, attn_mask = ctx.saved_tensor()
|
||||
fixed_seed_offset = ctx.fixed_seed_offset
|
||||
dropout = ctx.dropout
|
||||
is_causal = ctx.is_causal
|
||||
|
||||
if fixed_seed_offset is None:
|
||||
fixed_seed_offset = paddle.to_tensor(
|
||||
[0, 0], place=paddle.CPUPlace(), dtype=paddle.int64
|
||||
)
|
||||
local_query = dist.auto_parallel.api.dtensor_to_local(
|
||||
query, query.process_mesh, query.placements
|
||||
)
|
||||
local_key = dist.auto_parallel.api.dtensor_to_local(
|
||||
key, key.process_mesh, key.placements
|
||||
)
|
||||
local_value = dist.auto_parallel.api.dtensor_to_local(
|
||||
value, value.process_mesh, value.placements
|
||||
)
|
||||
local_out_grad = dist.auto_parallel.api.dtensor_to_local(
|
||||
out_grad, out_grad.process_mesh, out_grad.placements
|
||||
)
|
||||
query_grad, key_grad, value_grad = ring_flash_attention_backward_func(
|
||||
group,
|
||||
local_out_grad,
|
||||
local_query,
|
||||
local_key,
|
||||
local_value,
|
||||
out,
|
||||
lse,
|
||||
attn_mask,
|
||||
dropout,
|
||||
is_causal,
|
||||
fixed_seed_offset,
|
||||
)
|
||||
query_grad_dtensor = dist.auto_parallel.api.dtensor_from_local(
|
||||
query_grad, query.process_mesh, query.placements
|
||||
)
|
||||
key_grad_dtensor = dist.auto_parallel.api.dtensor_from_local(
|
||||
key_grad, key.process_mesh, key.placements
|
||||
)
|
||||
value_grad_dtensor = dist.auto_parallel.api.dtensor_from_local(
|
||||
value_grad, value.process_mesh, value.placements
|
||||
)
|
||||
|
||||
if attn_mask is not None and not attn_mask.stop_gradient:
|
||||
return (
|
||||
query_grad_dtensor,
|
||||
key_grad_dtensor,
|
||||
value_grad_dtensor,
|
||||
None,
|
||||
)
|
||||
else:
|
||||
return query_grad_dtensor, key_grad_dtensor, value_grad_dtensor
|
||||
@@ -0,0 +1,740 @@
|
||||
# Copyright (c) 2025 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import itertools
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
import paddle.nn.functional as F
|
||||
|
||||
|
||||
def _get_comm_group_by_dim(mesh, dim):
|
||||
dim_names = mesh.dim_names
|
||||
assert dim in dim_names, f"dim '{dim}' not in mesh.dim_names {dim_names}"
|
||||
|
||||
shape = mesh.shape
|
||||
dim_idx = dim_names.index(dim)
|
||||
ids = mesh.process_ids
|
||||
|
||||
def nest(flat, shape):
|
||||
if not shape:
|
||||
return flat[0]
|
||||
step = int(len(flat) // shape[0])
|
||||
return [
|
||||
nest(flat[i * step : (i + 1) * step], shape[1:])
|
||||
for i in range(shape[0])
|
||||
]
|
||||
|
||||
mesh_nd = nest(ids, shape)
|
||||
|
||||
other_axes = [i for i in range(len(shape)) if i != dim_idx]
|
||||
other_ranges = [range(shape[i]) for i in other_axes]
|
||||
|
||||
comm_groups = []
|
||||
for index in itertools.product(*other_ranges):
|
||||
group = []
|
||||
for i in range(shape[dim_idx]):
|
||||
idx = list(index)
|
||||
idx.insert(dim_idx, i)
|
||||
val = mesh_nd
|
||||
for j in idx:
|
||||
val = val[j]
|
||||
group.append(val)
|
||||
comm_groups.append(group)
|
||||
|
||||
return comm_groups
|
||||
|
||||
|
||||
def _get_conv_tp_group(x_mesh, x_placements, data_format):
|
||||
if data_format == "NCHW":
|
||||
shard_axis = 3
|
||||
else:
|
||||
shard_axis = 2
|
||||
|
||||
axis_name = None
|
||||
for i, placement in enumerate(x_placements):
|
||||
if placement == dist.Shard(shard_axis):
|
||||
axis_name = x_mesh.dim_names[i]
|
||||
break
|
||||
|
||||
if not axis_name:
|
||||
raise ValueError(
|
||||
f"Input tensor placements {x_placements} do not contain a Shard on W axis:{shard_axis}."
|
||||
)
|
||||
|
||||
tp_groups = _get_comm_group_by_dim(x_mesh, axis_name)
|
||||
rank = dist.get_rank()
|
||||
|
||||
for group in tp_groups:
|
||||
if rank in group:
|
||||
return axis_name, group
|
||||
|
||||
raise RuntimeError(
|
||||
f"Rank {rank} not found in any tensor parallel group for mesh {x_mesh}."
|
||||
)
|
||||
|
||||
|
||||
def _ring_conv_halo_exchange(
|
||||
local_input_tensor,
|
||||
halo_width_to_receive_from_left,
|
||||
halo_width_to_receive_from_right,
|
||||
left_neighbor_rank,
|
||||
right_neighbor_rank,
|
||||
current_rank,
|
||||
conv_tp_group,
|
||||
data_format,
|
||||
):
|
||||
if len(conv_tp_group) == 1:
|
||||
return local_input_tensor
|
||||
|
||||
if not (
|
||||
len(local_input_tensor.shape) == 4
|
||||
): # Assuming 4D tensors like NCHW/NHWC
|
||||
raise ValueError(
|
||||
f"Input tensor is expected to be 4D for NCHW/NHWC formats, "
|
||||
f"but got {len(local_input_tensor.shape)}D."
|
||||
)
|
||||
|
||||
if data_format == "NCHW":
|
||||
width_dim_idx = 3
|
||||
elif data_format == "NHWC":
|
||||
width_dim_idx = 2
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported data_format: {data_format}. Must be 'NCHW' or 'NHWC'."
|
||||
)
|
||||
|
||||
# Segment to send to the right neighbor (right_neighbor_rank)
|
||||
slices_for_send_right = [slice(None)] * 4
|
||||
slices_for_send_right[width_dim_idx] = slice(
|
||||
-halo_width_to_receive_from_left, None
|
||||
)
|
||||
segment_to_send_right = local_input_tensor[
|
||||
tuple(slices_for_send_right)
|
||||
].contiguous()
|
||||
|
||||
# Segment to send to the left neighbor (left_neighbor_rank)
|
||||
slices_for_send_left = [slice(None)] * 4
|
||||
slices_for_send_left[width_dim_idx] = slice(
|
||||
None, halo_width_to_receive_from_right
|
||||
)
|
||||
segment_to_send_left = local_input_tensor[
|
||||
tuple(slices_for_send_left)
|
||||
].contiguous()
|
||||
|
||||
buffer_for_halo_from_right = paddle.zeros_like(segment_to_send_left)
|
||||
buffer_for_halo_from_left = paddle.zeros_like(segment_to_send_right)
|
||||
|
||||
op_isend_to_right = dist.P2POp(
|
||||
dist.isend, segment_to_send_right, right_neighbor_rank
|
||||
)
|
||||
op_isend_to_left = dist.P2POp(
|
||||
dist.isend, segment_to_send_left, left_neighbor_rank
|
||||
)
|
||||
|
||||
op_irecv_from_right = dist.P2POp(
|
||||
dist.irecv, buffer_for_halo_from_right, right_neighbor_rank
|
||||
)
|
||||
op_irecv_from_left = dist.P2POp(
|
||||
dist.irecv, buffer_for_halo_from_left, left_neighbor_rank
|
||||
)
|
||||
|
||||
p2p_requests = dist.batch_isend_irecv(
|
||||
[
|
||||
op_isend_to_right,
|
||||
op_isend_to_left,
|
||||
op_irecv_from_left,
|
||||
op_irecv_from_right,
|
||||
]
|
||||
)
|
||||
for req in p2p_requests:
|
||||
req.wait()
|
||||
|
||||
# Concatenate received halo regions with the local tensor
|
||||
if current_rank == conv_tp_group[0]:
|
||||
# First rank: original tensor || halo_from_right
|
||||
reconstructed_tensor = paddle.concat(
|
||||
[local_input_tensor, buffer_for_halo_from_right], axis=width_dim_idx
|
||||
)
|
||||
elif current_rank == conv_tp_group[-1]:
|
||||
# Last rank: halo_from_left || original tensor
|
||||
reconstructed_tensor = paddle.concat(
|
||||
[buffer_for_halo_from_left, local_input_tensor], axis=width_dim_idx
|
||||
)
|
||||
else:
|
||||
# Middle ranks: halo_from_left || original tensor || halo_from_right
|
||||
reconstructed_tensor = paddle.concat(
|
||||
[
|
||||
buffer_for_halo_from_left,
|
||||
local_input_tensor,
|
||||
buffer_for_halo_from_right,
|
||||
],
|
||||
axis=width_dim_idx,
|
||||
)
|
||||
|
||||
return reconstructed_tensor.contiguous()
|
||||
|
||||
|
||||
def _ring_conv_halo_aggregate(
|
||||
local_gradient_tensor,
|
||||
halo_width_send_left,
|
||||
halo_width_send_right,
|
||||
left_neighbor_rank,
|
||||
right_neighbor_rank,
|
||||
current_process_rank,
|
||||
conv_tp_group,
|
||||
data_format,
|
||||
):
|
||||
if len(conv_tp_group) == 1:
|
||||
return local_gradient_tensor
|
||||
|
||||
if data_format == "NCHW":
|
||||
width_dim_idx = 3
|
||||
elif data_format == "NHWC":
|
||||
width_dim_idx = 2
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported data_format: {data_format}. Must be 'NCHW' or 'NHWC'."
|
||||
)
|
||||
|
||||
# Prepare gradient segments to send
|
||||
slices_for_send_right = [slice(None)] * 4
|
||||
slices_for_send_right[width_dim_idx] = slice(
|
||||
-halo_width_send_right, None
|
||||
) # Send the rightmost part
|
||||
segment_to_send_right = local_gradient_tensor[
|
||||
tuple(slices_for_send_right)
|
||||
].contiguous()
|
||||
|
||||
slices_for_send_left = [slice(None)] * 4
|
||||
slices_for_send_left[width_dim_idx] = slice(
|
||||
None, halo_width_send_left
|
||||
) # Send the leftmost part
|
||||
segment_to_send_left = local_gradient_tensor[
|
||||
tuple(slices_for_send_left)
|
||||
].contiguous()
|
||||
|
||||
# Buffers for receiving gradients
|
||||
buffer_for_gradient_from_left = paddle.zeros_like(segment_to_send_right)
|
||||
buffer_for_gradient_from_right = paddle.zeros_like(segment_to_send_left)
|
||||
|
||||
op_isend_to_right = dist.P2POp(
|
||||
dist.isend, segment_to_send_right, right_neighbor_rank
|
||||
)
|
||||
op_isend_to_left = dist.P2POp(
|
||||
dist.isend, segment_to_send_left, left_neighbor_rank
|
||||
)
|
||||
op_irecv_from_right = dist.P2POp(
|
||||
dist.irecv, buffer_for_gradient_from_right, right_neighbor_rank
|
||||
)
|
||||
op_irecv_from_left = dist.P2POp(
|
||||
dist.irecv, buffer_for_gradient_from_left, left_neighbor_rank
|
||||
)
|
||||
|
||||
p2p_requests = dist.batch_isend_irecv(
|
||||
[
|
||||
op_isend_to_right,
|
||||
op_isend_to_left,
|
||||
op_irecv_from_left,
|
||||
op_irecv_from_right,
|
||||
]
|
||||
)
|
||||
for req in p2p_requests:
|
||||
req.wait()
|
||||
|
||||
processed_gradient_tensor = local_gradient_tensor
|
||||
# Crop local tensor and aggregate received gradients
|
||||
if current_process_rank == conv_tp_group[0]:
|
||||
# Crop the part sent to the right neighbor
|
||||
crop_slices = [slice(None)] * 4
|
||||
crop_slices[width_dim_idx] = slice(None, -halo_width_send_right)
|
||||
processed_gradient_tensor = processed_gradient_tensor[
|
||||
tuple(crop_slices)
|
||||
]
|
||||
|
||||
# Aggregate gradient received from the right neighbor
|
||||
# This is added to the new rightmost part of the processed_gradient_tensor
|
||||
agg_slices = [slice(None)] * 4
|
||||
agg_slices[width_dim_idx] = slice(-halo_width_send_left, None)
|
||||
|
||||
target_slice = processed_gradient_tensor[tuple(agg_slices)]
|
||||
target_slice.add_(buffer_for_gradient_from_right)
|
||||
|
||||
elif current_process_rank == conv_tp_group[-1]:
|
||||
# Crop the part sent to the left neighbor
|
||||
crop_slices = [slice(None)] * 4
|
||||
crop_slices[width_dim_idx] = slice(halo_width_send_left, None)
|
||||
processed_gradient_tensor = processed_gradient_tensor[
|
||||
tuple(crop_slices)
|
||||
]
|
||||
|
||||
# Aggregate gradient received from the left neighbor
|
||||
agg_slices = [slice(None)] * 4
|
||||
agg_slices[width_dim_idx] = slice(None, halo_width_send_right)
|
||||
|
||||
target_slice = processed_gradient_tensor[tuple(agg_slices)]
|
||||
target_slice.add_(buffer_for_gradient_from_left)
|
||||
|
||||
else:
|
||||
# Crop parts sent to both left and right neighbors
|
||||
crop_slices = [slice(None)] * 4
|
||||
crop_slices[width_dim_idx] = slice(
|
||||
halo_width_send_left, -halo_width_send_right
|
||||
)
|
||||
processed_gradient_tensor = processed_gradient_tensor[
|
||||
tuple(crop_slices)
|
||||
]
|
||||
|
||||
# Aggregate gradient received from the right neighbor
|
||||
agg_slices_right_edge = [slice(None)] * 4
|
||||
agg_slices_right_edge[width_dim_idx] = slice(
|
||||
-halo_width_send_left, None
|
||||
)
|
||||
target_slice_right = processed_gradient_tensor[
|
||||
tuple(agg_slices_right_edge)
|
||||
]
|
||||
target_slice_right.add_(buffer_for_gradient_from_right)
|
||||
|
||||
# Aggregate gradient received from the left neighbor
|
||||
agg_slices_left_edge = [slice(None)] * 4
|
||||
agg_slices_left_edge[width_dim_idx] = slice(None, halo_width_send_right)
|
||||
target_slice_left = processed_gradient_tensor[
|
||||
tuple(agg_slices_left_edge)
|
||||
]
|
||||
target_slice_left.add_(buffer_for_gradient_from_left)
|
||||
|
||||
return processed_gradient_tensor.contiguous()
|
||||
|
||||
|
||||
class RingConv2d(paddle.autograd.PyLayer):
|
||||
@staticmethod
|
||||
def _is_supported(
|
||||
input_size, kernel_size, stride, padding, dilation, data_format="NCHW"
|
||||
):
|
||||
idx_w_input = -1
|
||||
idx_w_kernel = -1
|
||||
|
||||
if data_format == "NCHW":
|
||||
# input_size: (N, C, H, W)
|
||||
# kernel_size: (OutChannels, InChannels/Groups, KernelH, KernelW)
|
||||
idx_w_input = 3
|
||||
idx_w_kernel = 3
|
||||
elif data_format == "NHWC":
|
||||
# input_size: (N, H, W, C)
|
||||
# kernel_size: (OutChannels, InChannels/Groups, KernelH, KernelW)
|
||||
idx_w_input = 2
|
||||
idx_w_kernel = 3
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported data_format '{data_format}'. Expected 'NCHW' or 'NHWC'."
|
||||
)
|
||||
|
||||
dilation_w = dilation[1]
|
||||
padding_w = padding[1]
|
||||
stride_w = stride[1]
|
||||
|
||||
input_w = input_size[idx_w_input]
|
||||
kernel_w = kernel_size[idx_w_kernel]
|
||||
|
||||
if dilation_w != 1:
|
||||
# RingConv2d only supports dilation=1.
|
||||
# Larger dilation would require enlarged halo regions and more complex communication.
|
||||
raise RuntimeError(
|
||||
f"Only dilation=1 on the W-dimension is supported for tensor-parallel convolution. "
|
||||
f"Got dilation_w={dilation_w} (data_format='{data_format}')."
|
||||
)
|
||||
|
||||
if padding_w == 0:
|
||||
# To avoid halo exchange when padding=0, we require:
|
||||
# - input_w must be divisible by stride_w, so partitions align evenly across ranks.
|
||||
# - stride_w == kernel_w, so each kernel operates on disjoint local regions.
|
||||
if input_w % stride_w != 0:
|
||||
raise RuntimeError(
|
||||
f"When padding_w=0, input_W={input_w} must be divisible by stride_W={stride_w} "
|
||||
f"for tensor-parallel convolution (data_format='{data_format}')."
|
||||
)
|
||||
if stride_w != kernel_w:
|
||||
raise RuntimeError(
|
||||
f"When padding_w=0, stride_W={stride_w} must equal kernel_W={kernel_w} "
|
||||
f"to avoid halo exchange (data_format='{data_format}')."
|
||||
)
|
||||
|
||||
else:
|
||||
# When padding > 0, halo exchange is needed.
|
||||
# To simplify halo logic, we require:
|
||||
# - stride_w == 1: ensures each output element is computed from overlapping input,
|
||||
# and no input region is skipped, simplifying halo construction.
|
||||
# - kernel_w // 2 <= input_w: prevents the kernel from exceeding local input.
|
||||
if stride_w != 1:
|
||||
raise RuntimeError(
|
||||
f"When padding_w={padding_w}, stride_W must be 1 for tensor-parallel convolution. "
|
||||
f"Got stride_W={stride_w} (data_format='{data_format}')."
|
||||
)
|
||||
if kernel_w // 2 > input_w:
|
||||
raise RuntimeError(
|
||||
f"Half of kernel_W ({kernel_w // 2}) must not exceed input_W={input_w} "
|
||||
f"to ensure halo region fits (data_format='{data_format}')."
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(
|
||||
ctx,
|
||||
x,
|
||||
weight,
|
||||
bias=None,
|
||||
stride=1,
|
||||
padding=0,
|
||||
padding_algorithm=None,
|
||||
dilation=1,
|
||||
groups=1,
|
||||
data_format="NCHW",
|
||||
channel_dim=1,
|
||||
):
|
||||
rank = dist.get_rank()
|
||||
|
||||
assert RingConv2d._is_supported(
|
||||
x.shape, weight.shape, stride, padding, dilation, data_format
|
||||
)
|
||||
assert x.is_dist(), "Input tensor `x` must be a distributed tensor."
|
||||
|
||||
if not weight.is_dist():
|
||||
weight_placements = [
|
||||
dist.Replicate() for _ in range(len(x.placements))
|
||||
]
|
||||
weight = dist.auto_parallel.api.dtensor_from_local(
|
||||
weight, x.process_mesh, weight_placements
|
||||
)
|
||||
|
||||
if bias is not None and not bias.is_dist():
|
||||
bias_placements = [
|
||||
dist.Replicate() for _ in range(len(x.placements))
|
||||
]
|
||||
bias = dist.auto_parallel.api.dtensor_from_local(
|
||||
bias, x.process_mesh, bias_placements
|
||||
)
|
||||
|
||||
ctx.save_for_backward(x, weight, bias)
|
||||
|
||||
x_mesh = x.process_mesh
|
||||
x_placements = x.placements
|
||||
x = dist.auto_parallel.api.dtensor_to_local(x, x_mesh, x_placements)
|
||||
|
||||
weight = dist.auto_parallel.api.dtensor_to_local(
|
||||
weight, weight.process_mesh, weight.placements
|
||||
)
|
||||
if bias is not None:
|
||||
bias = dist.auto_parallel.api.dtensor_to_local(
|
||||
bias, bias.process_mesh, bias.placements
|
||||
)
|
||||
|
||||
ctx.attrs = (
|
||||
stride,
|
||||
padding,
|
||||
padding_algorithm,
|
||||
dilation,
|
||||
groups,
|
||||
data_format,
|
||||
)
|
||||
|
||||
mesh_axis_name, conv_tp_group = _get_conv_tp_group(
|
||||
x_mesh, x_placements, data_format
|
||||
)
|
||||
if padding[1] == 0 or len(conv_tp_group) <= 1:
|
||||
final_local_results = paddle._C_ops.conv2d(
|
||||
x,
|
||||
weight,
|
||||
stride,
|
||||
padding,
|
||||
padding_algorithm,
|
||||
dilation,
|
||||
groups,
|
||||
data_format,
|
||||
)
|
||||
else:
|
||||
# step 0: calculate the required overlap (halo) pixels for the input tensor
|
||||
if data_format == "NCHW":
|
||||
kernel_width_dim_idx = 3
|
||||
output_width_dim_idx = 3
|
||||
elif data_format == "NHWC":
|
||||
kernel_width_dim_idx = 3
|
||||
output_width_dim_idx = 2
|
||||
else:
|
||||
raise ValueError(
|
||||
f"Unsupported data_format: {data_format}. Must be 'NCHW' or 'NHWC'."
|
||||
)
|
||||
|
||||
kernel_width = weight.shape[kernel_width_dim_idx]
|
||||
kernel_total_halo_span = kernel_width - 1
|
||||
left_halo_width = kernel_total_halo_span // 2
|
||||
right_halo_width = kernel_total_halo_span - left_halo_width
|
||||
assert left_halo_width + right_halo_width == kernel_total_halo_span
|
||||
|
||||
ctx.mesh_axis_name = mesh_axis_name
|
||||
rank_idx = conv_tp_group.index(rank)
|
||||
next_rank = conv_tp_group[(rank_idx + 1) % len(conv_tp_group)]
|
||||
prev_rank = conv_tp_group[(rank_idx - 1) % len(conv_tp_group)]
|
||||
|
||||
# step 1: reconstruct the local input tensor including halo regions via ring communication
|
||||
# `x` is updated here, now including halo data received from neighboring ranks.
|
||||
x = _ring_conv_halo_exchange(
|
||||
x,
|
||||
left_halo_width,
|
||||
right_halo_width,
|
||||
prev_rank,
|
||||
next_rank,
|
||||
rank,
|
||||
conv_tp_group,
|
||||
data_format,
|
||||
)
|
||||
|
||||
# step 2: feed the reconstructed local input tensor to the actual computation (op_call)
|
||||
local_results_with_halo = paddle._C_ops.conv2d(
|
||||
x,
|
||||
weight,
|
||||
stride,
|
||||
padding,
|
||||
padding_algorithm,
|
||||
dilation,
|
||||
groups,
|
||||
data_format,
|
||||
)
|
||||
|
||||
# step 3: remove extra output portions from the results, generated from processing halo regions
|
||||
# `padding[1]` (from outer scope) is assumed here to be the width of the halo/overlap
|
||||
# that needs to be trimmed from each side of the output.
|
||||
output_halo_trim_width = padding[1]
|
||||
width_before_trimming = local_results_with_halo.shape[
|
||||
output_width_dim_idx
|
||||
]
|
||||
|
||||
if data_format == "NCHW":
|
||||
if rank == conv_tp_group[0]:
|
||||
final_local_results = local_results_with_halo[
|
||||
:,
|
||||
:,
|
||||
:,
|
||||
: width_before_trimming - output_halo_trim_width,
|
||||
]
|
||||
elif rank == conv_tp_group[-1]:
|
||||
final_local_results = local_results_with_halo[
|
||||
:, :, :, output_halo_trim_width:
|
||||
]
|
||||
else:
|
||||
final_local_results = local_results_with_halo[
|
||||
:,
|
||||
:,
|
||||
:,
|
||||
output_halo_trim_width : width_before_trimming
|
||||
- output_halo_trim_width,
|
||||
]
|
||||
else:
|
||||
if rank == conv_tp_group[0]:
|
||||
final_local_results = local_results_with_halo[
|
||||
:,
|
||||
:,
|
||||
: width_before_trimming - output_halo_trim_width,
|
||||
:,
|
||||
]
|
||||
elif rank == conv_tp_group[-1]:
|
||||
final_local_results = local_results_with_halo[
|
||||
:, :, output_halo_trim_width:, :
|
||||
]
|
||||
else:
|
||||
final_local_results = local_results_with_halo[
|
||||
:,
|
||||
:,
|
||||
output_halo_trim_width : width_before_trimming
|
||||
- output_halo_trim_width,
|
||||
:,
|
||||
]
|
||||
|
||||
ctx.left_halo_width = left_halo_width
|
||||
ctx.right_halo_width = right_halo_width
|
||||
ctx.output_halo_trim_width = output_halo_trim_width
|
||||
ctx.output_width_dim_idx = output_width_dim_idx
|
||||
|
||||
final_local_results = dist.auto_parallel.api.dtensor_from_local(
|
||||
final_local_results, x_mesh, x_placements
|
||||
)
|
||||
|
||||
return final_local_results.contiguous()
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, grad_out):
|
||||
current_rank = dist.get_rank()
|
||||
x, weight, bias = ctx.saved_tensor()
|
||||
|
||||
x_stop_gradient = x.stop_gradient
|
||||
weight_stop_gradient = weight.stop_gradient
|
||||
bias_stop_gradient = bias.stop_gradient if bias is not None else True
|
||||
|
||||
x_mesh = x.process_mesh
|
||||
x_placements = x.placements
|
||||
x = dist.auto_parallel.api.dtensor_to_local(x, x_mesh, x_placements)
|
||||
|
||||
weight_mesh = weight.process_mesh
|
||||
weight_placements = weight.placements
|
||||
weight = dist.auto_parallel.api.dtensor_to_local(
|
||||
weight, weight_mesh, weight_placements
|
||||
)
|
||||
|
||||
grad_out = dist.auto_parallel.api.dtensor_to_local(
|
||||
grad_out, grad_out.process_mesh, grad_out.placements
|
||||
)
|
||||
|
||||
if bias is not None:
|
||||
bias_mesh = bias.process_mesh
|
||||
bias_placements = bias.placements
|
||||
bias = dist.auto_parallel.api.dtensor_to_local(
|
||||
bias, bias_mesh, bias_placements
|
||||
)
|
||||
|
||||
conv_attrs = ctx.attrs
|
||||
data_format = conv_attrs[-1]
|
||||
padding = conv_attrs[1]
|
||||
|
||||
grad_x = None
|
||||
grad_weight = None
|
||||
grad_bias = None
|
||||
|
||||
_, conv_tp_group = _get_conv_tp_group(x_mesh, x_placements, data_format)
|
||||
|
||||
if padding[1] == 0 or len(conv_tp_group) <= 1:
|
||||
grad_x, grad_weight = paddle._C_ops.conv2d_grad(
|
||||
x, weight, grad_out, *conv_attrs
|
||||
)
|
||||
else:
|
||||
rank_idx = conv_tp_group.index(current_rank)
|
||||
next_rank = conv_tp_group[(rank_idx + 1) % len(conv_tp_group)]
|
||||
prev_rank = conv_tp_group[(rank_idx - 1) % len(conv_tp_group)]
|
||||
|
||||
left_halo_width = ctx.left_halo_width
|
||||
right_halo_width = ctx.right_halo_width
|
||||
output_halo_trim_width = ctx.output_halo_trim_width
|
||||
output_width_dim_idx = ctx.output_width_dim_idx
|
||||
|
||||
# Step 1: Reconstruct `in_tensor_augmented` (original input to local conv in forward)
|
||||
in_tensor_augmented = _ring_conv_halo_exchange(
|
||||
x,
|
||||
left_halo_width,
|
||||
right_halo_width,
|
||||
prev_rank,
|
||||
next_rank,
|
||||
current_rank,
|
||||
conv_tp_group,
|
||||
data_format,
|
||||
)
|
||||
|
||||
# Step 2: Pad `grad_out` to match the output shape of conv on augmented input
|
||||
padding_w = padding[1]
|
||||
if data_format == "NCHW":
|
||||
if current_rank == conv_tp_group[0]:
|
||||
padding_list = [0, padding_w]
|
||||
elif current_rank == conv_tp_group[-1]:
|
||||
padding_list = [padding_w, 0]
|
||||
else:
|
||||
padding_list = [padding_w, padding_w]
|
||||
else:
|
||||
if current_rank == conv_tp_group[0]:
|
||||
padding_list = [0, padding_w, 0, 0]
|
||||
elif current_rank == conv_tp_group[-1]:
|
||||
padding_list = [padding_w, 0, 0, 0]
|
||||
else:
|
||||
padding_list = [padding_w, padding_w, 0, 0]
|
||||
|
||||
grad_out_padded = F.pad(
|
||||
grad_out,
|
||||
padding_list,
|
||||
mode="constant",
|
||||
value=0.0,
|
||||
data_format=data_format,
|
||||
)
|
||||
|
||||
# Step 3: Local backward computation using augmented/padded tensors
|
||||
# `padding` here is the original conv padding from forward.
|
||||
grad_x_augmented, grad_weight = paddle._C_ops.conv2d_grad(
|
||||
in_tensor_augmented, weight, grad_out_padded, *conv_attrs
|
||||
)
|
||||
|
||||
# Step 4: Aggregate "halo" regions for grad_input
|
||||
if not x_stop_gradient:
|
||||
grad_x = _ring_conv_halo_aggregate(
|
||||
grad_x_augmented,
|
||||
left_halo_width,
|
||||
right_halo_width,
|
||||
prev_rank,
|
||||
next_rank,
|
||||
current_rank,
|
||||
conv_tp_group,
|
||||
data_format,
|
||||
)
|
||||
|
||||
if bias is not None:
|
||||
sum_axes = [0, 2, 3] if data_format == "NCHW" else [0, 1, 2]
|
||||
grad_bias = paddle.sum(grad_out, axis=sum_axes, keepdim=True)
|
||||
grad_bias = grad_bias.reshape(bias.shape)
|
||||
|
||||
if grad_x is not None:
|
||||
grad_x = dist.auto_parallel.api.dtensor_from_local(
|
||||
grad_x, x_mesh, x_placements
|
||||
)
|
||||
|
||||
# Note(luchang): With input X sharded along tp_axis_name and weight W replicated,
|
||||
# the locally computed grad_weight is only a partial sum for the full dL/dW,
|
||||
# as dL/dW depends on contributions from all input shards.
|
||||
# Aggregation across TP ranks is therefore necessary. Partial(ReduceSum)
|
||||
# declares this averaging intent, and reshard to Replicate() executes
|
||||
# the AllReduce-average, making the correct averaged grad_weight available
|
||||
# and replicated on all TP ranks.
|
||||
tp_axis_name, _ = _get_conv_tp_group(x_mesh, x_placements, data_format)
|
||||
for idx, axis_name in enumerate(weight_mesh.dim_names):
|
||||
if axis_name == tp_axis_name:
|
||||
weight_placements[idx] = dist.Partial(dist.ReduceType.kRedSum)
|
||||
if bias is not None:
|
||||
bias_placements[idx] = dist.Partial(dist.ReduceType.kRedSum)
|
||||
|
||||
grad_weight = dist.auto_parallel.api.dtensor_from_local(
|
||||
grad_weight, weight_mesh, weight_placements
|
||||
)
|
||||
# do allreduce to get right grad_weight
|
||||
grad_weight = dist.reshard(
|
||||
grad_weight,
|
||||
weight_mesh,
|
||||
[dist.Replicate() for _ in range(len(weight_placements))],
|
||||
)
|
||||
|
||||
if bias is not None:
|
||||
grad_bias = dist.auto_parallel.api.dtensor_from_local(
|
||||
grad_bias, bias_mesh, bias_placements
|
||||
)
|
||||
# do allreduce to get right grad_bias
|
||||
grad_bias = dist.reshard(
|
||||
grad_bias,
|
||||
weight_mesh,
|
||||
[dist.Replicate() for _ in range(len(bias_placements))],
|
||||
)
|
||||
|
||||
if x_stop_gradient:
|
||||
grad_x = None
|
||||
if weight_stop_gradient:
|
||||
grad_weight = None
|
||||
if bias_stop_gradient:
|
||||
grad_bias = None
|
||||
|
||||
if bias is not None:
|
||||
return grad_x, grad_weight, grad_bias
|
||||
|
||||
return grad_x, grad_weight
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
# Copyright (c) 2021 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.
|
||||
@@ -0,0 +1,532 @@
|
||||
# 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.
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.base import core
|
||||
from paddle.base.framework import Program
|
||||
from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
from paddle.distributed.auto_parallel.static.dist_context import (
|
||||
get_default_distributed_context,
|
||||
)
|
||||
from paddle.distributed.auto_parallel.static.utils import (
|
||||
is_backward_op,
|
||||
is_forward_op,
|
||||
is_loss_op,
|
||||
)
|
||||
from paddle.static.io import deserialize_program
|
||||
|
||||
_valid_types = [
|
||||
core.VarDesc.VarType.DENSE_TENSOR,
|
||||
core.VarDesc.VarType.SELECTED_ROWS,
|
||||
core.VarDesc.VarType.DENSE_TENSOR_ARRAY,
|
||||
]
|
||||
|
||||
paddle.enable_static()
|
||||
|
||||
|
||||
class AutoAlignTool:
|
||||
"""
|
||||
This is an automatic parallel precision alignment tool。
|
||||
"""
|
||||
|
||||
def __init__(self, program: Program, step=1, fetch_list=None):
|
||||
"""Set some initialization information of the tool.
|
||||
step: Step when returning a specific variable name。
|
||||
fetch_list: initialization fetch_list.When a specific step is not reached, return this.
|
||||
It can combine with Engine class。
|
||||
example:in Engine.fit function,like this
|
||||
try:
|
||||
fetch_list = []
|
||||
align_tool = AutoAlignTool(self.main_program, 0, fetch_names)
|
||||
level = 0
|
||||
fetch_list = align_tool.get_var(level, step)
|
||||
outs = self._executor.run(
|
||||
self.main_program,
|
||||
fetch_list=fetch_list,
|
||||
use_program_cache=self._strategy.use_cache,
|
||||
return_numpy=self._strategy.return_numpy,
|
||||
)
|
||||
if fetch_list != fetch_names:
|
||||
align_tool.save(dir_path, outs, fetch_list, self._dist_contexts["train"], self.serial)
|
||||
exit(0)
|
||||
except core.EOFException:
|
||||
break
|
||||
"""
|
||||
assert isinstance(program, Program)
|
||||
self._program = program
|
||||
self._blocks = program.blocks
|
||||
self._step = step
|
||||
self._fetch_list = fetch_list
|
||||
assert self._blocks is not None
|
||||
|
||||
def set_step(self, step):
|
||||
self._step = step
|
||||
|
||||
def get_var(self, level, step):
|
||||
"""
|
||||
level must be in [0,1,2,3,4,5].
|
||||
"""
|
||||
if step != self._step or step == -1:
|
||||
return self._fetch_list
|
||||
if level == 0:
|
||||
return self.get_loss_lr_var()
|
||||
elif level == 1:
|
||||
return self.get_data_var()
|
||||
elif level == 2:
|
||||
return self.get_param_var()
|
||||
elif level == 3:
|
||||
return self.get_param_grad_var()
|
||||
elif level == 4:
|
||||
return self.get_forward_tmp_var()
|
||||
elif level == 5:
|
||||
return self.get_backward_tmp_var()
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
def set_program(self, program: Program):
|
||||
assert isinstance(program, Program)
|
||||
self._program = program
|
||||
self._blocks = program.blocks
|
||||
assert self._blocks is not None
|
||||
|
||||
def get_loss_lr_var(self):
|
||||
"""
|
||||
Returns the variable name of learning rate and loss
|
||||
"""
|
||||
fetch_set = set()
|
||||
loss_ops = []
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_loss_op(op):
|
||||
assert len(op.desc.output_arg_names()) == 1, (
|
||||
"loss op should only output loss var"
|
||||
)
|
||||
loss_ops.append(op)
|
||||
|
||||
for block in self._blocks:
|
||||
for varname in block.vars:
|
||||
var = block._find_var_recursive(varname)
|
||||
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
|
||||
if "learning_rate" in var.name:
|
||||
fetch_set.add(var.name)
|
||||
|
||||
for loss_op in loss_ops:
|
||||
fetch_set.add(loss_op.output_arg_names[0])
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def get_data_var(self):
|
||||
"""
|
||||
Returns the variable name of data.
|
||||
"""
|
||||
fetch_set = set()
|
||||
for block in self._blocks:
|
||||
for varname in block.vars:
|
||||
var = block._find_var_recursive(varname)
|
||||
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
|
||||
if var.is_data:
|
||||
fetch_set.add(var.name)
|
||||
return list(fetch_set)
|
||||
|
||||
def get_param_var(self):
|
||||
"""
|
||||
Returns the variable name of parameters.
|
||||
"""
|
||||
fetch_set = set()
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_backward_op(op):
|
||||
break
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
var = block._find_var_recursive(varname)
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
if var.is_parameter:
|
||||
fetch_set.add(varname)
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def get_param_grad_var(self):
|
||||
"""
|
||||
Returns the variable name of parameters' gradient.
|
||||
"""
|
||||
fetch_set = set()
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_forward_op(op):
|
||||
continue
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if "@GRAD" not in varname:
|
||||
continue
|
||||
fwd_varname = varname.split("@GRAD")[0]
|
||||
fwd_var = block._find_var_recursive(fwd_varname)
|
||||
if fwd_var is None or fwd_var.type not in _valid_types:
|
||||
continue
|
||||
if fwd_var.is_parameter is False:
|
||||
continue
|
||||
var = block._find_var_recursive(varname)
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
fetch_set.add(varname)
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def get_forward_tmp_var(self):
|
||||
"""
|
||||
Returns the name of the temporary variable in the forward propagation
|
||||
"""
|
||||
fetch_set = set()
|
||||
loss_lr_list = self.get_loss_lr_var()
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_backward_op(op):
|
||||
break
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if varname in loss_lr_list:
|
||||
continue
|
||||
var = block._find_var_recursive(varname)
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
if var.is_data or var.is_parameter:
|
||||
continue
|
||||
fetch_set.add(varname)
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def get_backward_tmp_var(self):
|
||||
"""
|
||||
Returns the name of a temporary variable in back-propagation
|
||||
"""
|
||||
fetch_set = set()
|
||||
loss_lr_list = self.get_loss_lr_var()
|
||||
forward_tmp_list = self.get_forward_tmp_var()
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_backward_op(op):
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if (
|
||||
varname in loss_lr_list
|
||||
or varname in forward_tmp_list
|
||||
):
|
||||
continue
|
||||
if "@GRAD" in varname:
|
||||
fwd_varname = varname.split("@GRAD")[0]
|
||||
fwd_var = block._find_var_recursive(fwd_varname)
|
||||
if (
|
||||
fwd_var is not None
|
||||
and fwd_var.type in _valid_types
|
||||
):
|
||||
if fwd_var.is_parameter:
|
||||
continue
|
||||
var = block._find_var_recursive(varname)
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
if var.is_data or var.is_parameter:
|
||||
continue
|
||||
fetch_set.add(varname)
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def save(self, save_dir, vars, fetch_list, dist_context=None):
|
||||
"""
|
||||
save fetch variables, distributed properties of variables and program.
|
||||
"""
|
||||
if os.path.exists(save_dir) is False:
|
||||
os.mkdir(save_dir)
|
||||
if dist_context is None:
|
||||
dist_context = get_default_distributed_context()
|
||||
assert os.path.exists(save_dir)
|
||||
if dist.get_world_size() == 1:
|
||||
vars_path = os.path.join(save_dir, "vars.pkl")
|
||||
program_path = os.path.join(save_dir, "program.pdmodel")
|
||||
dist_attr_path = os.path.join(save_dir, "dist_attr.pkl")
|
||||
else:
|
||||
vars_path = os.path.join(
|
||||
save_dir, f"vars_rank{dist.get_rank()}.pkl"
|
||||
)
|
||||
program_path = os.path.join(
|
||||
save_dir, f"program_rank{dist.get_rank()}.pdmodel"
|
||||
)
|
||||
dist_attr_path = os.path.join(
|
||||
save_dir, f"dist_attr_rank{dist.get_rank()}.pkl"
|
||||
)
|
||||
if vars is not None:
|
||||
vars_dict = {}
|
||||
assert len(fetch_list) == len(vars)
|
||||
for i in range(len(fetch_list)):
|
||||
if vars[i] is None:
|
||||
continue
|
||||
vars_dict[fetch_list[i]] = vars[i]
|
||||
with open(vars_path, "wb") as f:
|
||||
pickle.dump(vars_dict, f)
|
||||
dist_attr = {}
|
||||
for var in self._program.list_vars():
|
||||
if var.name not in fetch_list:
|
||||
continue
|
||||
tensor_dist_attr = (
|
||||
dist_context.get_tensor_dist_attr_for_program(var)
|
||||
)
|
||||
if tensor_dist_attr is None:
|
||||
continue
|
||||
process_mesh = tensor_dist_attr.process_mesh
|
||||
dims_mapping = tensor_dist_attr.dims_mapping
|
||||
dist_attr[var.name] = {
|
||||
"process_shape": process_mesh.shape,
|
||||
"process_group": process_mesh.process_ids,
|
||||
"dims_mapping": dims_mapping,
|
||||
}
|
||||
if len(dist_attr) > 0:
|
||||
with open(dist_attr_path, "wb") as f:
|
||||
pickle.dump(dist_attr, f)
|
||||
if self._program is not None:
|
||||
with open(program_path, "wb") as f:
|
||||
f.write(self._program.desc.serialize_to_string())
|
||||
|
||||
@staticmethod
|
||||
def load(save_dir):
|
||||
assert os.path.exists(save_dir)
|
||||
filename_list = sorted(os.listdir(save_dir))
|
||||
vars_list = []
|
||||
program_list = []
|
||||
dist_attr_list = []
|
||||
for filename in filename_list:
|
||||
filepath = os.path.join(save_dir, filename)
|
||||
assert os.path.isfile(filepath)
|
||||
if "vars" in filename:
|
||||
assert filename.endswith("pkl")
|
||||
with open(filepath, "rb") as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
vars_list.append(safe_load_pickle(f))
|
||||
elif "program" in filename:
|
||||
assert filename.endswith("pdmodel")
|
||||
with open(filepath, "rb") as f:
|
||||
program_string = f.read()
|
||||
program_list.append(deserialize_program(program_string))
|
||||
elif "dist_attr" in filename:
|
||||
assert filename.endswith("pkl")
|
||||
with open(filepath, "rb") as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
dist_attr_list.append(safe_load_pickle(f))
|
||||
|
||||
dist_attr_map = {}
|
||||
for dist_attrs in dist_attr_list:
|
||||
for dist_attr_name in dist_attrs.keys():
|
||||
if dist_attr_name not in dist_attr_map:
|
||||
dist_attr_map[dist_attr_name] = dist_attrs[dist_attr_name]
|
||||
assert len(vars_list) == len(program_list)
|
||||
return vars_list, program_list, dist_attr_map
|
||||
|
||||
@staticmethod
|
||||
def convert_src_tensor_2_dst_tensor(vars_list, src_attr_map, dst_attr_map):
|
||||
"""
|
||||
Converter is a class object for auto parallel to convert tensors from
|
||||
one parallel strategy to another one. Tensors will merge and slice value
|
||||
with their strategy when strategies are different.
|
||||
But like dp to pp or dp to serial is not supported.
|
||||
"""
|
||||
assert len(vars_list) >= 1
|
||||
# if dist_attr_map is None or len(dist_attr_map) == 0 or len(vars_list) == 1:
|
||||
if src_attr_map is None or len(src_attr_map) == 0:
|
||||
return vars_list[0]
|
||||
|
||||
dst_strategies = {}
|
||||
src_strategies = {}
|
||||
tensors_dict = {}
|
||||
|
||||
convert_tensor_dict = None
|
||||
for var_name in src_attr_map.keys():
|
||||
assert var_name not in dst_strategies
|
||||
dist_vars = []
|
||||
for vars in vars_list:
|
||||
if var_name in vars.keys():
|
||||
dist_vars.append(vars[var_name])
|
||||
if len(dist_vars) == 0:
|
||||
continue
|
||||
|
||||
if var_name in dst_attr_map and var_name in src_attr_map:
|
||||
dst_strategies[var_name] = copy.deepcopy(dst_attr_map[var_name])
|
||||
src_strategies[var_name] = copy.deepcopy(src_attr_map[var_name])
|
||||
tensors_dict[var_name] = dist_vars
|
||||
|
||||
if src_attr_map == dst_attr_map:
|
||||
return tensors_dict
|
||||
converter = Converter(tensors_dict, src_strategies, dst_strategies)
|
||||
convert_tensor_dict = converter.convert()
|
||||
|
||||
return convert_tensor_dict
|
||||
|
||||
@staticmethod
|
||||
def find_diff_vars(fixed_vars_map, query_vars_map):
|
||||
"""
|
||||
Found two variable names with different variable lists
|
||||
"""
|
||||
diff_var_name_list = set()
|
||||
for var_name in fixed_vars_map.keys():
|
||||
if var_name in query_vars_map:
|
||||
fixed_vars = fixed_vars_map[var_name]
|
||||
query_vars = query_vars_map[var_name]
|
||||
if isinstance(fixed_vars, np.ndarray):
|
||||
fixed_vars = [fixed_vars]
|
||||
if isinstance(query_vars, np.ndarray):
|
||||
query_vars = [query_vars]
|
||||
|
||||
length = min(len(fixed_vars), len(query_vars))
|
||||
if len(fixed_vars) != len(query_vars):
|
||||
print()
|
||||
for i in range(length):
|
||||
if not np.allclose(fixed_vars[i], query_vars[i]):
|
||||
diff_var_name_list.add(var_name)
|
||||
return diff_var_name_list
|
||||
|
||||
@staticmethod
|
||||
def diff_information(right_dir, wrong_dir):
|
||||
"""
|
||||
Find the corresponding operator according to the variable name.
|
||||
"""
|
||||
(
|
||||
right_vars_list,
|
||||
right_program_list,
|
||||
right_dist_attr_map,
|
||||
) = AutoAlignTool.load(right_dir)
|
||||
(
|
||||
wrong_vars_list,
|
||||
wrong_program_list,
|
||||
wrong_dist_attr_map,
|
||||
) = AutoAlignTool.load(wrong_dir)
|
||||
right_tensors_dict = AutoAlignTool.convert_src_tensor_2_dst_tensor(
|
||||
right_vars_list, right_dist_attr_map, right_dist_attr_map
|
||||
)
|
||||
wrong_tensors_dict = AutoAlignTool.convert_src_tensor_2_dst_tensor(
|
||||
wrong_vars_list, wrong_dist_attr_map, right_dist_attr_map
|
||||
)
|
||||
|
||||
diff_var_name_list = AutoAlignTool.find_diff_vars(
|
||||
right_tensors_dict, wrong_tensors_dict
|
||||
)
|
||||
|
||||
diff_ops_varname_dict = collections.OrderedDict()
|
||||
|
||||
for program in wrong_program_list:
|
||||
for block in program.blocks:
|
||||
for op in block.ops:
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if varname in diff_var_name_list:
|
||||
if len(diff_ops_varname_dict) == 0:
|
||||
print(
|
||||
"first different op:\n",
|
||||
op,
|
||||
f"\ndifferent varname is:{varname}",
|
||||
)
|
||||
if op not in diff_ops_varname_dict:
|
||||
diff_ops_varname_dict[op] = [varname]
|
||||
else:
|
||||
diff_ops_varname_dict[op].append(varname)
|
||||
|
||||
return diff_ops_varname_dict
|
||||
|
||||
@staticmethod
|
||||
def diff_information_from_dirs(right_dirs, wrong_dirs):
|
||||
right_vars_list = []
|
||||
right_program_list = []
|
||||
right_dist_attr_map = {}
|
||||
for right_dir in right_dirs:
|
||||
(
|
||||
tmp_vars_list,
|
||||
right_program_list,
|
||||
tmp_dist_attr_map,
|
||||
) = AutoAlignTool.load(right_dir)
|
||||
if len(right_vars_list) == 0:
|
||||
right_vars_list = tmp_vars_list
|
||||
else:
|
||||
for i in range(len(tmp_vars_list)):
|
||||
vars_list = tmp_vars_list[i]
|
||||
for key in vars_list.keys():
|
||||
if key not in right_vars_list[i].keys():
|
||||
right_vars_list[i][key] = vars_list[key]
|
||||
|
||||
for key in tmp_dist_attr_map.keys():
|
||||
if key not in right_dist_attr_map:
|
||||
right_dist_attr_map[key] = tmp_dist_attr_map[key]
|
||||
|
||||
wrong_vars_list = []
|
||||
wrong_program_list = []
|
||||
wrong_dist_attr_map = {}
|
||||
for wrong_dir in wrong_dirs:
|
||||
(
|
||||
tmp_vars_list,
|
||||
wrong_program_list,
|
||||
tmp_dist_attr_map,
|
||||
) = AutoAlignTool.load(wrong_dir)
|
||||
if len(wrong_vars_list) == 0:
|
||||
wrong_vars_list = tmp_vars_list
|
||||
else:
|
||||
for i in range(len(tmp_vars_list)):
|
||||
vars_list = tmp_vars_list[i]
|
||||
for key in vars_list.keys():
|
||||
if key not in wrong_vars_list[i].keys():
|
||||
wrong_vars_list[i][key] = vars_list[key]
|
||||
|
||||
for key in tmp_dist_attr_map.keys():
|
||||
if key not in wrong_dist_attr_map:
|
||||
wrong_dist_attr_map[key] = tmp_dist_attr_map[key]
|
||||
|
||||
right_tensors_dict = AutoAlignTool.convert_src_tensor_2_dst_tensor(
|
||||
right_vars_list, right_dist_attr_map, right_dist_attr_map
|
||||
)
|
||||
wrong_tensors_dict = AutoAlignTool.convert_src_tensor_2_dst_tensor(
|
||||
wrong_vars_list, wrong_dist_attr_map, right_dist_attr_map
|
||||
)
|
||||
diff_var_name_list = AutoAlignTool.find_diff_vars(
|
||||
right_tensors_dict, wrong_tensors_dict
|
||||
)
|
||||
|
||||
diff_ops_varname_dict = collections.OrderedDict()
|
||||
|
||||
for program in wrong_program_list:
|
||||
for block in program.blocks:
|
||||
for op in block.ops:
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if varname in diff_var_name_list:
|
||||
if len(diff_ops_varname_dict) == 0:
|
||||
print(
|
||||
"first different op:\n",
|
||||
op,
|
||||
f"\ndifferent varname is:{varname}",
|
||||
)
|
||||
if op not in diff_ops_varname_dict:
|
||||
diff_ops_varname_dict[op] = [varname]
|
||||
else:
|
||||
diff_ops_varname_dict[op].append(varname)
|
||||
|
||||
return diff_ops_varname_dict
|
||||
@@ -0,0 +1,244 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import paddle
|
||||
from paddle.hapi.callbacks import (
|
||||
Callback,
|
||||
CallbackList,
|
||||
LRScheduler,
|
||||
ModelCheckpoint,
|
||||
ProgBarLogger,
|
||||
)
|
||||
|
||||
from ..interface import CollectionNames, get_collection
|
||||
|
||||
|
||||
def config_callbacks(
|
||||
callbacks=None,
|
||||
engine=None,
|
||||
batch_size=None,
|
||||
epochs=None,
|
||||
steps=None,
|
||||
log_freq=2,
|
||||
verbose=2,
|
||||
save_freq=1,
|
||||
save_dir=None,
|
||||
metrics=None,
|
||||
acc_step=1,
|
||||
mode='train',
|
||||
):
|
||||
cbks = callbacks or []
|
||||
cbks = cbks if isinstance(cbks, (list, tuple)) else [cbks]
|
||||
|
||||
if not any(isinstance(k, ProgBarLogger) for k in cbks) and verbose:
|
||||
cbks = [ProgBarLoggerAuto(log_freq, verbose=verbose), *cbks]
|
||||
|
||||
if not any(isinstance(k, LRScheduler) for k in cbks):
|
||||
cbks = [LRSchedulerAuto(), *cbks]
|
||||
|
||||
if not any(isinstance(k, ModelCheckpoint) for k in cbks):
|
||||
cbks = [*cbks, ModelCheckpointAuto(save_freq, save_dir)]
|
||||
|
||||
if not any(isinstance(k, Profiler) for k in cbks) and verbose == 3:
|
||||
cbks = [*cbks, Profiler(timer_only=True)]
|
||||
|
||||
if not any(isinstance(k, History) for k in cbks):
|
||||
cbks = [*cbks, History()]
|
||||
|
||||
for i, k in enumerate(cbks):
|
||||
if isinstance(k, ProgBarLogger):
|
||||
cbks[i] = ProgBarLoggerAuto(k.log_freq, k.verbose)
|
||||
if isinstance(k, LRScheduler):
|
||||
cbks[i] = LRSchedulerAuto(k.by_step, k.by_epoch)
|
||||
if isinstance(k, ModelCheckpoint):
|
||||
cbks[i] = ModelCheckpointAuto(k.save_freq, k.save_dir)
|
||||
|
||||
cbk_list = CallbackList(cbks)
|
||||
cbk_list.set_model(engine)
|
||||
metrics = metrics or [] if mode != 'test' else []
|
||||
params = {
|
||||
'batch_size': batch_size,
|
||||
'epochs': epochs,
|
||||
'steps': steps,
|
||||
'verbose': verbose,
|
||||
'metrics': metrics,
|
||||
'acc_step': acc_step,
|
||||
}
|
||||
cbk_list.set_params(params)
|
||||
return cbk_list
|
||||
|
||||
|
||||
class ProgBarLoggerAuto(ProgBarLogger):
|
||||
def __init__(self, log_freq=1, verbose=2):
|
||||
super().__init__(log_freq, verbose)
|
||||
|
||||
def _is_print(self):
|
||||
return True
|
||||
|
||||
def _updates(self, logs, mode):
|
||||
values = []
|
||||
metrics = getattr(self, f'{mode}_metrics')
|
||||
progbar = getattr(self, f'{mode}_progbar')
|
||||
steps = getattr(self, f'{mode}_step')
|
||||
|
||||
for k in metrics:
|
||||
if k in logs:
|
||||
values.append((k, logs[k]))
|
||||
|
||||
if 'lr' in logs:
|
||||
values.append(('lr', logs['lr']))
|
||||
|
||||
fetches_logs = logs.get('fetches', {})
|
||||
collect_logging = get_collection(CollectionNames.LOGGING)
|
||||
for name, var in collect_logging:
|
||||
k = name or var.name
|
||||
if k in fetches_logs:
|
||||
values.append((k, fetches_logs[k]))
|
||||
|
||||
out_logs = logs.get('outputs', {})
|
||||
for k in out_logs:
|
||||
values.append((k, out_logs[k]))
|
||||
|
||||
if self.verbose == 3 and hasattr(self, f'_{mode}_timer'):
|
||||
timer = getattr(self, f'_{mode}_timer')
|
||||
cnt = timer['count'] if timer['count'] > 0 else 1.0
|
||||
samples = timer['samples'] if timer['samples'] > 0 else 1.0
|
||||
values.append(
|
||||
('avg_reader_cost', "%.5f sec" % (timer['data_time'] / cnt))
|
||||
)
|
||||
values.append(
|
||||
('avg_batch_cost', "%.5f sec" % (timer['batch_time'] / cnt))
|
||||
)
|
||||
values.append(
|
||||
(
|
||||
'ips',
|
||||
"%.5f samples/sec"
|
||||
% (samples / (timer['data_time'] + timer['batch_time'])),
|
||||
)
|
||||
)
|
||||
timer['count'] = 0
|
||||
timer['samples'] = 0
|
||||
timer['data_time'] = 0.0
|
||||
timer['batch_time'] = 0.0
|
||||
|
||||
progbar.update(steps, values)
|
||||
|
||||
def on_eval_batch_end(self, step, logs=None):
|
||||
logs = logs or {}
|
||||
self.eval_step += 1
|
||||
samples = self.params['batch_size']
|
||||
self.evaled_samples += samples
|
||||
|
||||
self._eval_timer['batch_time'] += (
|
||||
time.time() - self._eval_timer['batch_data_end_time']
|
||||
)
|
||||
self._eval_timer['count'] += 1
|
||||
samples = self.params['batch_size']
|
||||
self._eval_timer['samples'] += samples
|
||||
|
||||
if self._is_print() and self.eval_step % self.log_freq == 0:
|
||||
if self.eval_steps is None or self.eval_step < self.eval_steps:
|
||||
self._updates(logs, 'eval')
|
||||
|
||||
self._eval_timer['batch_start_time'] = time.time()
|
||||
|
||||
|
||||
class LRSchedulerAuto(LRScheduler):
|
||||
def __init__(self, by_step=True, by_epoch=False):
|
||||
super().__init__(by_step, by_epoch)
|
||||
|
||||
def on_epoch_begin(self, epoch=None, logs=None):
|
||||
self.acc_step = self.params["acc_step"]
|
||||
self.epoch = epoch
|
||||
self.train_step = 0
|
||||
|
||||
def on_train_batch_end(self, step, logs=None):
|
||||
self.train_step += 1
|
||||
|
||||
if self.by_step and self.train_step % self.acc_step == 0:
|
||||
if (
|
||||
self.model.optimizer
|
||||
and hasattr(self.model.optimizer, '_learning_rate')
|
||||
and isinstance(
|
||||
self.model.optimizer._learning_rate,
|
||||
paddle.optimizer.lr.LRScheduler,
|
||||
)
|
||||
):
|
||||
self.model.optimizer._learning_rate.step()
|
||||
|
||||
|
||||
class History(Callback):
|
||||
def __init__(self):
|
||||
self.history = {}
|
||||
|
||||
def on_train_begin(self, logs=None):
|
||||
self.epoch = []
|
||||
|
||||
def on_epoch_end(self, epoch, logs=None):
|
||||
logs = logs or {}
|
||||
self.epoch.append(epoch)
|
||||
for k, v in logs.items():
|
||||
self.history.setdefault(k, []).append(v)
|
||||
|
||||
self.model.history = self
|
||||
|
||||
|
||||
class Profiler(Callback):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.prof = paddle.profiler.Profiler(*args, **kwargs)
|
||||
|
||||
def on_epoch_begin(self, epoch=None, logs=None):
|
||||
self.epoch = epoch
|
||||
self.train_step = 0
|
||||
self.batch_size = self.params["batch_size"]
|
||||
self.steps = self.params['steps']
|
||||
|
||||
def on_train_begin(self, logs=None):
|
||||
self.prof.start()
|
||||
|
||||
def on_train_batch_end(self, step, logs=None):
|
||||
self.train_step += 1
|
||||
self.prof.step(num_samples=self.batch_size)
|
||||
print(
|
||||
"step {}:{}".format(
|
||||
self.train_step, self.prof.step_info(unit='samples')
|
||||
)
|
||||
)
|
||||
|
||||
def on_train_end(self, logs=None):
|
||||
self.prof.stop()
|
||||
self.prof.summary()
|
||||
|
||||
|
||||
class ModelCheckpointAuto(ModelCheckpoint):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _is_save(self):
|
||||
return self.model and self.save_dir
|
||||
|
||||
def on_epoch_end(self, epoch, logs=None):
|
||||
if self._is_save() and (self.epoch + 1) % self.save_freq == 0:
|
||||
path = f'{self.save_dir}/epoch{epoch}'
|
||||
print(f'save checkpoint at {os.path.abspath(path)}')
|
||||
self.model.save(path)
|
||||
|
||||
def on_train_end(self, logs=None):
|
||||
if self._is_save():
|
||||
path = f'{self.save_dir}/final'
|
||||
print(f'save checkpoint at {os.path.abspath(path)}')
|
||||
self.model.save(path)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from enum import IntEnum, unique
|
||||
|
||||
import numpy as np
|
||||
|
||||
from paddle.framework import core
|
||||
|
||||
|
||||
@unique
|
||||
class DeviceType(IntEnum):
|
||||
UNKNOWN = 0
|
||||
CPU = 1
|
||||
GPU = 2
|
||||
XPU = 3
|
||||
DCU = 5
|
||||
NIC = 6
|
||||
|
||||
|
||||
@unique
|
||||
class LinkType(IntEnum):
|
||||
UNKNOWN = 0
|
||||
LOC = 1
|
||||
SYS = 2
|
||||
PHB = 3
|
||||
PIX = 4
|
||||
PIB = 5
|
||||
NVL = 6
|
||||
NVB = 7
|
||||
NET = 8
|
||||
|
||||
|
||||
class DeviceMesh(core.DeviceMesh):
|
||||
r"""
|
||||
The class `DeviceMesh` describes the topology of physical devices.
|
||||
|
||||
Args:
|
||||
mesh (list|numpy.array): an N-dimensional array describes the topology
|
||||
of logical processes.
|
||||
dim_names (list, optional): the i-th element of this list gives the name of the
|
||||
i-th dimension.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> paddle.enable_static()
|
||||
|
||||
>>> mesh = dist.DeviceMesh([[2, 4, 5], [0, 1, 3]])
|
||||
>>> assert mesh.shape == [2, 3]
|
||||
>>> assert mesh.device_ids == [2, 4, 5, 0, 1, 3]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, mesh, dim_names=None):
|
||||
self._name = name
|
||||
|
||||
if not isinstance(mesh, list) and not isinstance(mesh, np.ndarray):
|
||||
raise ValueError(
|
||||
'The mesh must be an instance of list or np.ndarray.'
|
||||
)
|
||||
if isinstance(mesh, list):
|
||||
mesh = np.array(mesh)
|
||||
|
||||
self._mesh = mesh
|
||||
|
||||
self._shape = list(self._mesh.shape)
|
||||
|
||||
self._device_ids = self._mesh.flatten().tolist()
|
||||
assert all(isinstance(p, int) for p in self._device_ids), (
|
||||
"All elements of the mesh be integer"
|
||||
)
|
||||
assert min(self._device_ids) >= 0, (
|
||||
'All elements of the mesh must be >= 0.'
|
||||
)
|
||||
unique_device_ids = set(self._device_ids)
|
||||
assert len(unique_device_ids) == len(self._device_ids), (
|
||||
'All elements of the mesh must be unique.'
|
||||
)
|
||||
|
||||
if dim_names is not None:
|
||||
assert len(dim_names) == len(self._shape), (
|
||||
"The length of dims_names must be same as the shape of the mesh."
|
||||
)
|
||||
self._dim_names = dim_names
|
||||
else:
|
||||
self._dim_names = ["d" + str(i) for i in range(len(self._shape))]
|
||||
|
||||
# Follow the requirement for using pybind11
|
||||
core.DeviceMesh.__init__(
|
||||
self, self._name, self._shape, self._device_ids, self._dim_names
|
||||
)
|
||||
|
||||
@property
|
||||
def mesh(self):
|
||||
return self._mesh
|
||||
|
||||
|
||||
# class Cluster:
|
||||
# """
|
||||
# The cluster represents the hardware resource.
|
||||
# """
|
||||
|
||||
# def __init__(self):
|
||||
# self._device_meshes = {}
|
||||
|
||||
# def device_mesh(self, device_mesh_name):
|
||||
# return self._device_meshes[device_mesh_name]
|
||||
|
||||
# def add_device_mesh(self, device_mesh):
|
||||
# self._device_meshes[device_mesh.name] = device_mesh
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,543 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
|
||||
|
||||
class Converter:
|
||||
"""
|
||||
Converter is a class object for auto parallel to convert tensors from
|
||||
one parallel strategy to another one. Tensors will merge and slice value
|
||||
with their strategy when strategies are different.
|
||||
"""
|
||||
|
||||
def __init__(self, tensors_dict, pre_strategy, cur_strategy):
|
||||
"""
|
||||
Args:
|
||||
tensors_dict(dict): tensors' value of all ranks that to be converted.
|
||||
key is tensor's name(str), value is all ranks' data(list(numpy.ndarray))
|
||||
pre_strategy(dict): tensors' distributed attribute of last training process.
|
||||
key is tensor's name(str), value is tensor's distributed attribute in last
|
||||
training process.
|
||||
cur_strategy(dict): tensors' distributed attribute of current rank.
|
||||
key is tensor's name(str), value is tensor's distributed attribute in current
|
||||
rank.
|
||||
"""
|
||||
self._tensors_dict = self._check_tensor_dict(tensors_dict)
|
||||
self._pre_strategy = self._check_pre_strategy(pre_strategy)
|
||||
self._cur_strategy = self._check_cur_strategy(cur_strategy)
|
||||
self._logger = get_logger(logging.INFO)
|
||||
|
||||
def _check_tensor_dict(self, tensors_dict):
|
||||
if not tensors_dict:
|
||||
raise ValueError(
|
||||
"'tensors_dict' is None, "
|
||||
"the tensors to be converted cannot be None."
|
||||
)
|
||||
if not isinstance(tensors_dict, dict):
|
||||
raise TypeError(
|
||||
f"The type of 'tensors_dict' should be 'dict', but got '{type(tensors_dict)}'."
|
||||
)
|
||||
return tensors_dict
|
||||
|
||||
def _check_pre_strategy(self, pre_strategy):
|
||||
if not pre_strategy:
|
||||
raise ValueError(
|
||||
"'pre_strategy' is None, there are not tensors in pre process."
|
||||
)
|
||||
if not isinstance(pre_strategy, dict):
|
||||
raise TypeError(
|
||||
"The type of 'pre_strategy' should be 'dict', "
|
||||
f"but got '{type(pre_strategy)}'."
|
||||
)
|
||||
return pre_strategy
|
||||
|
||||
def _check_cur_strategy(self, cur_strategy):
|
||||
if not cur_strategy:
|
||||
warnings.warn(
|
||||
"'cur_strategy' is None, there are not tensors in cur process"
|
||||
)
|
||||
if not isinstance(cur_strategy, dict):
|
||||
raise TypeError(
|
||||
"The type of 'cur_strategy' should be 'dict', "
|
||||
f"but got '{type(cur_strategy)}'."
|
||||
)
|
||||
return cur_strategy
|
||||
|
||||
def convert(self, strict=True):
|
||||
"""
|
||||
Convert tensors
|
||||
|
||||
Args:
|
||||
strict(bool): whether to strict convert tensor with tensor's name. If False, it will
|
||||
convert tensors by prefix matching. Otherwise, tensors will be converted with
|
||||
their name strictly.
|
||||
|
||||
Returns:
|
||||
converted tensors(dict)
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
>>> complete_tensors = np.arange(4).reshape([2, 2])
|
||||
>>> partial_tensors = np.split(complete_tensors, 2, axis=0)
|
||||
>>> name = "tmp_0"
|
||||
>>> tensors_dict = {name: partial_tensors}
|
||||
>>> strategy_1 = {
|
||||
... name: {
|
||||
... "process_shape": [2],
|
||||
... "process_group": [0, 1],
|
||||
... "dims_mapping": [0, -1],
|
||||
... },
|
||||
... }
|
||||
>>> strategy_2 = {
|
||||
... name: {
|
||||
... "process_shape": [2],
|
||||
... "process_group": [0, 1],
|
||||
... "dims_mapping": [-1, -1],
|
||||
... },
|
||||
... }
|
||||
>>> converter = Converter(tensors_dict, strategy_1, strategy_2)
|
||||
>>> result = converter.convert()
|
||||
>>> # the result's value is equal to `complete_tensors`
|
||||
"""
|
||||
tensors_dict = {}
|
||||
# the name which is in cur_process but not in pre_process
|
||||
tensor_not_in_pre = []
|
||||
# the name which is in pre_process but not in cur_process
|
||||
tensor_not_in_cur = []
|
||||
# the name which is in strategy but not in ckpt files
|
||||
tensor_not_in_ckpt = []
|
||||
self._logger.info("Start to convert tensors.")
|
||||
for tensor_name in self._cur_strategy:
|
||||
if tensor_name not in self._pre_strategy:
|
||||
tensor_not_in_pre.append(tensor_name)
|
||||
continue
|
||||
if tensor_name not in self._tensors_dict:
|
||||
tensor_not_in_ckpt.append(tensor_name)
|
||||
continue
|
||||
self._pre_name = tensor_name
|
||||
self._cur_name = tensor_name
|
||||
tensor_list = self._tensors_dict[tensor_name]
|
||||
pre_dist_attr = self._pre_strategy[tensor_name]
|
||||
cur_dist_attr = self._cur_strategy[tensor_name]
|
||||
try:
|
||||
tensors_dict[tensor_name] = Converter.merge_and_slice(
|
||||
tensor_list, pre_dist_attr, cur_dist_attr
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ValueError(
|
||||
f"Fail to convert tensor '{tensor_name}'. {err}"
|
||||
)
|
||||
|
||||
for tensor_name in self._pre_strategy:
|
||||
if tensor_name not in self._cur_strategy:
|
||||
tensor_not_in_cur.append(tensor_name)
|
||||
|
||||
if not strict:
|
||||
(
|
||||
tensors_dict,
|
||||
tensor_match_with_pre,
|
||||
tensor_match_with_cur,
|
||||
) = self.convert_with_prefix_match(
|
||||
tensors_dict, tensor_not_in_pre, tensor_not_in_cur
|
||||
)
|
||||
else:
|
||||
tensors_dict, tensor_match_with_pre, tensor_match_with_cur = (
|
||||
tensors_dict,
|
||||
[],
|
||||
[],
|
||||
)
|
||||
|
||||
tensor_not_in_pre = set(tensor_not_in_pre) - set(tensor_match_with_pre)
|
||||
tensor_not_in_cur = set(tensor_not_in_cur) - set(tensor_match_with_cur)
|
||||
if tensor_not_in_pre:
|
||||
warnings.warn(
|
||||
f"tensors [{tensor_not_in_pre}] are not found in last training strategy."
|
||||
)
|
||||
if tensor_not_in_cur:
|
||||
warnings.warn(
|
||||
f"tensors [{tensor_not_in_cur}] are not found in current training strategy."
|
||||
)
|
||||
if tensor_not_in_ckpt:
|
||||
warnings.warn(
|
||||
f"tensors [{tensor_not_in_ckpt}] are found in pre_strategy, but are not found"
|
||||
"in checkpoint files, please check your checkpoint files."
|
||||
)
|
||||
|
||||
return tensors_dict
|
||||
|
||||
def convert_with_prefix_match(
|
||||
self, tensors_dict, tensor_not_in_pre, tensor_not_in_cur
|
||||
):
|
||||
# the name which in cur_process and can match with pre_process
|
||||
tensor_match_with_pre = []
|
||||
# the name which in pre_process and can match with cur_process
|
||||
tensor_match_with_cur = []
|
||||
for cur_name in tensor_not_in_pre:
|
||||
prefix_name = cur_name
|
||||
while prefix_name.find("_") != -1:
|
||||
prefix_name = prefix_name[: prefix_name.rfind("_")]
|
||||
for pre_name in tensor_not_in_cur:
|
||||
if prefix_name in pre_name:
|
||||
# 'cur_name' of cur_process can match with 'pre_name' of pre_process
|
||||
self._pre_name = pre_name
|
||||
self._cur_name = cur_name
|
||||
pre_tensor_list = self._tensors_dict[pre_name]
|
||||
pre_dist_attr = self._pre_strategy[pre_name]
|
||||
cur_dist_attr = self._cur_strategy[cur_name]
|
||||
try:
|
||||
tensors_dict[cur_name] = Converter.merge_and_slice(
|
||||
pre_tensor_list, pre_dist_attr, cur_dist_attr
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ValueError(
|
||||
f"Fail to convert tensor '{cur_name}' by '{pre_name}'. {err}"
|
||||
)
|
||||
self._logger.info(
|
||||
f"tensor [{cur_name}] is matched with tensor [{pre_name}]"
|
||||
)
|
||||
tensor_match_with_pre.append(cur_name)
|
||||
tensor_match_with_cur.append(pre_name)
|
||||
break
|
||||
break
|
||||
|
||||
return tensors_dict, tensor_match_with_pre, tensor_match_with_cur
|
||||
|
||||
@staticmethod
|
||||
def merge_and_slice(tensor_list, pre_dist_attr, cur_dist_attr):
|
||||
"""
|
||||
Merge tensors with previous dist_attr and slice tensors with current dist_attr
|
||||
|
||||
Returns:
|
||||
tensor(numpy.narray): a tensor's value of current rank.
|
||||
"""
|
||||
assert isinstance(tensor_list, list)
|
||||
assert all(isinstance(p, np.ndarray) for p in tensor_list)
|
||||
|
||||
if pre_dist_attr == cur_dist_attr:
|
||||
# skip merge and slice tensor
|
||||
rank_id = paddle.distributed.get_rank()
|
||||
index = cur_dist_attr["process_group"].index(rank_id)
|
||||
tensor = tensor_list[index]
|
||||
else:
|
||||
pre_dims_mapping = pre_dist_attr["dims_mapping"]
|
||||
cur_dims_mapping = cur_dist_attr["dims_mapping"]
|
||||
|
||||
if len(pre_dims_mapping) and (
|
||||
len(set(pre_dims_mapping)) > 1 or -1 not in pre_dims_mapping
|
||||
):
|
||||
# merge tensor
|
||||
tensor = Converter.merge_with_dist_attr(
|
||||
tensor_list, pre_dist_attr
|
||||
)
|
||||
else:
|
||||
# skip merge tensor
|
||||
tensor = tensor_list[0]
|
||||
|
||||
if len(cur_dims_mapping) and (
|
||||
len(set(cur_dims_mapping)) > 1 or -1 not in cur_dims_mapping
|
||||
):
|
||||
# slice tensor
|
||||
tensor = Converter.slice_with_dist_attr(tensor, cur_dist_attr)
|
||||
|
||||
return tensor
|
||||
|
||||
@staticmethod
|
||||
def merge_with_dist_attr(tensor_list, dist_attr):
|
||||
"""Merge tensor with distributed attribute"""
|
||||
from .reshard import Resharder
|
||||
|
||||
dims_mapping = dist_attr["dims_mapping"]
|
||||
process_shape = dist_attr["process_shape"]
|
||||
process_group = dist_attr["process_group"]
|
||||
# get the complete shape of the tensor
|
||||
complete_shape = Resharder.compute_complete_shape(
|
||||
tensor_list[0].shape, process_shape, dims_mapping
|
||||
)
|
||||
# merge the tensor with dist_attr
|
||||
partition_tensor_list = []
|
||||
merged_partition = []
|
||||
for process in process_group:
|
||||
partition_index = Resharder.compute_partition_index(
|
||||
process,
|
||||
complete_shape,
|
||||
dims_mapping,
|
||||
process_shape,
|
||||
process_group,
|
||||
)
|
||||
index = process_group.index(process)
|
||||
if partition_index not in merged_partition:
|
||||
merged_partition.append(partition_index)
|
||||
Converter.merge(
|
||||
partition_tensor_list,
|
||||
tensor_list[index],
|
||||
partition_index,
|
||||
complete_shape,
|
||||
)
|
||||
|
||||
if len(partition_tensor_list) != 1:
|
||||
raise ValueError(
|
||||
f"Fail to merge tensor with dist_attr '{dist_attr}'."
|
||||
)
|
||||
complete_tensor = partition_tensor_list[0][0]
|
||||
return complete_tensor
|
||||
|
||||
@staticmethod
|
||||
def slice_with_dist_attr(tensor, dist_attr):
|
||||
"""Slice tensor with distributed attribute"""
|
||||
dims_mapping = dist_attr["dims_mapping"]
|
||||
if len(dims_mapping) == 0:
|
||||
# NOTE: scalar tensor no need to split
|
||||
return tensor
|
||||
process_shape = dist_attr["process_shape"]
|
||||
process_group = dist_attr["process_group"]
|
||||
# slice the tensor with dist_attr
|
||||
partition_index_list = Converter._get_split_indices(
|
||||
tensor.shape, dims_mapping, process_shape, process_group
|
||||
)
|
||||
sliced_tensor_list = Converter.split(
|
||||
tensor, partition_index_list, len(partition_index_list)
|
||||
)
|
||||
# get the current tensor's index in sliced_tensor_list
|
||||
rank_id = paddle.distributed.get_rank()
|
||||
sliced_tensor_index = Converter._get_sliced_index(
|
||||
rank_id, tensor.shape, dims_mapping, process_shape, process_group
|
||||
)
|
||||
if sliced_tensor_index not in range(len(sliced_tensor_list)):
|
||||
raise ValueError(
|
||||
f"Fail to slice tensor with dist_attr '{dist_attr}'."
|
||||
)
|
||||
sliced_tensor = sliced_tensor_list[sliced_tensor_index]
|
||||
return sliced_tensor
|
||||
|
||||
@staticmethod
|
||||
def merge(partition_tensor_list, tensor, partition_index, complete_shape):
|
||||
"""
|
||||
Merge partial tensors to a complete.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> import paddle
|
||||
>>> from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
>>> partition_tensor_list = [(np.array([[[1.11, 1.12]]]), [[0, 1], [0, 1], [0, 2]])]
|
||||
>>> tensor = np.array([[[1.13, 1.14]]])
|
||||
>>> partition_index = [[0, 1], [0, 1], [2, 4]]
|
||||
>>> complete_shape = [3, 2]
|
||||
|
||||
>>> Converter.merge(partition_tensor_list, tensor, partition_index, complete_shape)
|
||||
>>> print(partition_tensor_list)
|
||||
[(array([[[1.11, 1.12, 1.13, 1.14]]]), [[0, 1], [0, 1], [0, 4]])]
|
||||
"""
|
||||
from .reshard import Resharder
|
||||
|
||||
if len(partition_tensor_list) == 1:
|
||||
is_complete_data = True
|
||||
for idx, item in enumerate(partition_tensor_list[0][1]):
|
||||
if item[0] != 0 or item[1] != complete_shape[idx]:
|
||||
is_complete_data = False
|
||||
break
|
||||
if is_complete_data:
|
||||
return
|
||||
|
||||
if not partition_tensor_list:
|
||||
partition_tensor_list.append((tensor, partition_index))
|
||||
else:
|
||||
i = 0
|
||||
while i < len(partition_tensor_list):
|
||||
(
|
||||
concat_axis,
|
||||
first_order,
|
||||
new_partition,
|
||||
) = Resharder.compute_concat_info(
|
||||
partition_tensor_list[i][1], partition_index
|
||||
)
|
||||
if concat_axis != -1:
|
||||
if first_order == 0:
|
||||
new_tensor = np.concatenate(
|
||||
(partition_tensor_list[i][0], tensor),
|
||||
axis=concat_axis,
|
||||
)
|
||||
else:
|
||||
new_tensor = np.concatenate(
|
||||
(tensor, partition_tensor_list[i][0]),
|
||||
axis=concat_axis,
|
||||
)
|
||||
|
||||
partition_tensor_list.pop(i)
|
||||
Converter.merge(
|
||||
partition_tensor_list,
|
||||
new_tensor,
|
||||
new_partition,
|
||||
complete_shape,
|
||||
)
|
||||
break
|
||||
i += 1
|
||||
|
||||
@staticmethod
|
||||
def split(complete_tensor, partition_index_list, length):
|
||||
"""
|
||||
Slice a complete tensor.
|
||||
|
||||
Returns:
|
||||
sliced_tensor_list(list): sliced tensors with 'partition_index_list'
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
>>> complete_tensor = np.array([[[1.11, 1.12, 1.13, 1.14, 1.15, 1.16]]])
|
||||
>>> rank = 2
|
||||
>>> complete_shape = [1, 1, 6]
|
||||
>>> dims_mapping = [-1, -1, 0]
|
||||
>>> process_shape = [3]
|
||||
>>> process_group = [0, 1, 2]
|
||||
|
||||
>>> sliced_tensor_list = Converter.split(complete_tensor, [[], [], [2, 4]], 3)
|
||||
>>> print(sliced_tensor_list)
|
||||
[array([[[1.11, 1.12]]]), array([[[1.13, 1.14]]]), array([[[1.15, 1.16]]])]
|
||||
"""
|
||||
sliced_tensor_list = []
|
||||
axis = len(complete_tensor.shape) - length
|
||||
sliced_tensor = np.split(
|
||||
complete_tensor, partition_index_list[axis], axis=axis
|
||||
)
|
||||
if length == 1:
|
||||
return sliced_tensor
|
||||
for tensor in sliced_tensor:
|
||||
sliced_tensor_list.extend(
|
||||
Converter.split(tensor, partition_index_list, length - 1)
|
||||
)
|
||||
return sliced_tensor_list
|
||||
|
||||
@staticmethod
|
||||
def _get_split_indices(
|
||||
complete_shape, dims_mapping, process_shape, process_group
|
||||
):
|
||||
"""
|
||||
Get split indices of every dimension.
|
||||
|
||||
Returns:
|
||||
split_indices_list(list): the split indices of every dimension of the tensor
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> from paddle.distributed.auto_parallel.static.utils import _get_split_indices
|
||||
>>> complete_tensor = np.array([[[1.11, 1.12, 1.13, 1.14, 1.15, 1.16]]])
|
||||
>>> complete_shape = [1, 1, 6]
|
||||
>>> dims_mapping = [-1, -1, 0]
|
||||
>>> process_shape = [3]
|
||||
>>> process_group = [0, 1, 2]
|
||||
|
||||
>>> index = _get_split_indices(complete_shape, dims_mapping, process_shape, process_group)
|
||||
>>> print(index)
|
||||
[[], [], [2, 4]]
|
||||
"""
|
||||
from .reshard import Resharder
|
||||
|
||||
split_indices_list = []
|
||||
for process in process_group:
|
||||
partition_index = Resharder.compute_partition_index(
|
||||
process,
|
||||
complete_shape,
|
||||
dims_mapping,
|
||||
process_shape,
|
||||
process_group,
|
||||
)
|
||||
if split_indices_list:
|
||||
for dim in range(len(partition_index)):
|
||||
split_indices_list[dim].extend(partition_index[dim])
|
||||
else:
|
||||
split_indices_list = partition_index
|
||||
split_indices_list = list(
|
||||
map(
|
||||
lambda x, y: list(set(x) - {y} - {0}),
|
||||
split_indices_list,
|
||||
complete_shape,
|
||||
)
|
||||
)
|
||||
split_indices_list = [sorted(x) for x in split_indices_list]
|
||||
return split_indices_list
|
||||
|
||||
@staticmethod
|
||||
def _get_sliced_index(
|
||||
rank_id, complete_shape, dims_mapping, process_shape, process_group
|
||||
):
|
||||
"""
|
||||
Get sliced_tensor's index of current rank in all sliced tensors list.
|
||||
|
||||
Returns:
|
||||
sliced_tensor_index(int): the index of sliced tensor in sliced_tensor_list
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
>>> complete_tensor = np.array([[[1.11, 1.12, 1.13, 1.14, 1.15, 1.16]]])
|
||||
>>> rank = 2
|
||||
>>> complete_shape = [1, 1, 6]
|
||||
>>> dims_mapping = [-1, -1, 0]
|
||||
>>> process_shape = [3]
|
||||
>>> process_group = [0, 1, 2]
|
||||
|
||||
>>> index = Converter._get_sliced_index(
|
||||
... rank,
|
||||
... complete_shape,
|
||||
... dims_mapping,
|
||||
... process_shape,
|
||||
... process_group,
|
||||
... )
|
||||
>>> print(index)
|
||||
2
|
||||
"""
|
||||
from .reshard import Resharder
|
||||
|
||||
partition_index = Resharder.compute_partition_index(
|
||||
rank_id, complete_shape, dims_mapping, process_shape, process_group
|
||||
)
|
||||
sliced_index = 0
|
||||
for i, shape in enumerate(complete_shape):
|
||||
if dims_mapping[i] == -1:
|
||||
slice_shape = shape
|
||||
else:
|
||||
slice_shape = shape // process_shape[dims_mapping[i]]
|
||||
if slice_shape == 1:
|
||||
index = partition_index[i][0]
|
||||
else:
|
||||
index = (partition_index[i][0] + 1) // slice_shape
|
||||
sliced_index = sliced_index * (shape // slice_shape) + index
|
||||
return sliced_index
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from .base_cost import ( # noqa: F401
|
||||
CommContext,
|
||||
Cost,
|
||||
_g_op_cost_factory,
|
||||
build_comm_costs_from_descs,
|
||||
build_comm_desc,
|
||||
build_comm_desc_from_dist_op,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_comp_desc_str_for_predict,
|
||||
build_dp_costs,
|
||||
calc_time_by_cost_model,
|
||||
)
|
||||
from .comm_op_cost import ( # noqa: F401
|
||||
AllgatherOpCost,
|
||||
AllReduceOpCost,
|
||||
AllreduceSumOpCost,
|
||||
BroadcastOpCost,
|
||||
IdentityOpCost,
|
||||
RecvOpCost,
|
||||
SendOpCost,
|
||||
)
|
||||
from .comp_op_cost import ( # noqa: F401
|
||||
ConcatOpCost,
|
||||
EmbeddingGradOpCost,
|
||||
EmbeddingOpCost,
|
||||
FillConstantBatchSizeLikeOpCost,
|
||||
MatmulGradOpCost,
|
||||
MatmulOpCost,
|
||||
MatmulV2GradOpCost,
|
||||
MatmulV2OpCost,
|
||||
MulGradOpCost,
|
||||
MulOpCost,
|
||||
Reshape2GradOpCost,
|
||||
Reshape2OpCost,
|
||||
SliceOpCost,
|
||||
SoftmaxGradOpCost,
|
||||
SoftmaxOpCost,
|
||||
SplitOpCost,
|
||||
Transpose2GradOpCost,
|
||||
Transpose2OpCost,
|
||||
)
|
||||
from .estimate_cost import CostEstimator # noqa: F401
|
||||
from .op_runtime_cost import ( # noqa: F401
|
||||
check_if_op_supports_runtime_profiling,
|
||||
measure_program_real_op_cost,
|
||||
)
|
||||
from .tensor_cost import TensorCost # noqa: F401
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,316 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
|
||||
from .base_cost import CommOpCost, register_op_cost
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AllreduceSumOpCost(CommOpCost):
|
||||
OP_TYPE = "c_allreduce_sum"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
# use tree if cross machine and use ring if in a single machine
|
||||
time = None
|
||||
cluster = self.comm_context.cluster
|
||||
if not cluster.cross_machine(self.group_ranks):
|
||||
time = self.calc_time_ring()
|
||||
else:
|
||||
time = self.calc_time_tree()
|
||||
|
||||
return time
|
||||
|
||||
def calc_time_ring(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
alpha += (
|
||||
2
|
||||
* (self.rank_count - self.machine_count)
|
||||
* self.comm_context.intra_ring
|
||||
)
|
||||
alpha += (
|
||||
2
|
||||
* (self.machine_count - 1)
|
||||
* (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = (
|
||||
alpha
|
||||
+ 2
|
||||
* (self.rank_count - 1)
|
||||
/ self.rank_count
|
||||
* self.comm_count
|
||||
* beta
|
||||
)
|
||||
|
||||
return time
|
||||
|
||||
def calc_time_tree(self):
|
||||
alpha = self.comm_context.base_tree
|
||||
alpha += (
|
||||
2
|
||||
* (self.rank_count / self.machine_count - 1)
|
||||
* self.comm_context.intra_tree
|
||||
)
|
||||
alpha += math.log2(self.machine_count) * (
|
||||
self.comm_context.inter_tree + self.hops * self.comm_context.switch
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
|
||||
time = alpha + 2 * self.comm_count * beta
|
||||
|
||||
return time
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AllReduceOpCost(CommOpCost):
|
||||
OP_TYPE = "all_reduce"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
# use tree if cross machine and use ring if in a single machine
|
||||
time = None
|
||||
cluster = self.comm_context.cluster
|
||||
if not cluster.cross_machine(self.group_ranks):
|
||||
time = self.calc_time_ring()
|
||||
else:
|
||||
time = self.calc_time_tree()
|
||||
|
||||
return time
|
||||
|
||||
def calc_time_ring(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
alpha += (
|
||||
2
|
||||
* (self.rank_count - self.machine_count)
|
||||
* self.comm_context.intra_ring
|
||||
)
|
||||
alpha += (
|
||||
2
|
||||
* (self.machine_count - 1)
|
||||
* (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = (
|
||||
alpha
|
||||
+ 2
|
||||
* (self.rank_count - 1)
|
||||
/ self.rank_count
|
||||
* self.comm_count
|
||||
* beta
|
||||
)
|
||||
|
||||
return time
|
||||
|
||||
def calc_time_tree(self):
|
||||
alpha = self.comm_context.base_tree
|
||||
alpha += (
|
||||
2
|
||||
* (self.rank_count / self.machine_count - 1)
|
||||
* self.comm_context.intra_tree
|
||||
)
|
||||
alpha += math.log2(self.machine_count) * (
|
||||
self.comm_context.inter_tree + self.hops * self.comm_context.switch
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
|
||||
time = alpha + 2 * self.comm_count * beta
|
||||
|
||||
return time
|
||||
|
||||
@property
|
||||
def comm_count(self):
|
||||
from ..reshard import get_var_with_recursion
|
||||
|
||||
if self._comm_count is None:
|
||||
dtype = None
|
||||
shape = None
|
||||
if self.op is not None:
|
||||
vars = self.op.block.vars
|
||||
try:
|
||||
var_name = self.op.input("x")[0]
|
||||
except:
|
||||
var_name = self.op.output("out")[0]
|
||||
var = get_var_with_recursion(
|
||||
var_name, self.op.block, self.op.block.program
|
||||
)
|
||||
dtype = var.dtype
|
||||
shape = var.shape
|
||||
elif self.op_desc is not None:
|
||||
dtype = self.op_desc["inputs"]["x"][0][0]
|
||||
shape = self.op_desc["inputs"]["x"][0][1]
|
||||
|
||||
factor = None
|
||||
if dtype == paddle.float32 or dtype == paddle.int32:
|
||||
factor = 4
|
||||
else:
|
||||
raise ValueError(f"Unsupported comm dtype {dtype}")
|
||||
comm_count = int(np.prod(shape)) * factor
|
||||
self._comm_count = comm_count
|
||||
|
||||
return self._comm_count
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AllgatherOpCost(CommOpCost):
|
||||
OP_TYPE = "all_gather"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
time = self.calc_time_ring()
|
||||
return time
|
||||
|
||||
def calc_time_ring(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
alpha += (
|
||||
self.rank_count - self.machine_count
|
||||
) * self.comm_context.intra_ring
|
||||
alpha += (self.machine_count - 1) * (
|
||||
self.comm_context.inter_ring + self.hops * self.comm_context.switch
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = (
|
||||
alpha
|
||||
+ (self.rank_count - 1) / self.rank_count * self.comm_count * beta
|
||||
)
|
||||
return time
|
||||
|
||||
@property
|
||||
def comm_count(self):
|
||||
from ..reshard import get_var_with_recursion
|
||||
|
||||
if self._comm_count is None:
|
||||
dtype = None
|
||||
shape = None
|
||||
if self.op is not None:
|
||||
vars = self.op.block.vars
|
||||
try:
|
||||
var_name = self.op.input("x")[0]
|
||||
except:
|
||||
var_name = self.op.output("out")[0]
|
||||
var = get_var_with_recursion(
|
||||
var_name, self.op.block, self.op.block.program
|
||||
)
|
||||
dtype = var.dtype
|
||||
shape = var.shape
|
||||
elif self.op_desc is not None:
|
||||
dtype = self.op_desc["inputs"]["X"][0][0]
|
||||
shape = self.op_desc["inputs"]["X"][0][1]
|
||||
|
||||
factor = None
|
||||
if dtype == paddle.float32 or dtype == paddle.int32:
|
||||
factor = 4
|
||||
else:
|
||||
raise ValueError(f"Unsupported comm dtype {dtype}")
|
||||
comm_count = int(np.prod(shape)) * factor
|
||||
self._comm_count = comm_count
|
||||
|
||||
return self._comm_count
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class BroadcastOpCost(CommOpCost):
|
||||
OP_TYPE = "broadcast"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
time = self.calc_time_ring()
|
||||
return time
|
||||
|
||||
def calc_time_ring(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
if self.machine_count > 1:
|
||||
alpha += (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
else:
|
||||
alpha += self.comm_context.intra_ring
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = alpha + self.comm_count * beta
|
||||
|
||||
return time
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class IdentityOpCost(CommOpCost):
|
||||
OP_TYPE = "c_identity"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
return self.comm_count * 1 / (144 * 1e3)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class RecvOpCost(CommOpCost):
|
||||
OP_TYPE = "recv_v2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
if self.machine_count > 1:
|
||||
alpha += (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
else:
|
||||
alpha += self.comm_context.intra_ring
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = alpha + self.comm_count * beta
|
||||
return time
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SendOpCost(CommOpCost):
|
||||
OP_TYPE = "send_v2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
if self.machine_count > 1:
|
||||
alpha += (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
else:
|
||||
alpha += self.comm_context.intra_ring
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = alpha + self.comm_count * beta
|
||||
|
||||
return time
|
||||
@@ -0,0 +1,591 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from .base_cost import CompOpCost, register_op_cost
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AdamOpCost(CompOpCost):
|
||||
OP_TYPE = "adam"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ArgsortOpCost(CompOpCost):
|
||||
OP_TYPE = "argsort"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AssignOpCost(CompOpCost):
|
||||
OP_TYPE = "assign"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AssignValueOpCost(CompOpCost):
|
||||
OP_TYPE = "assign_value"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class BeamSearchOpCost(CompOpCost):
|
||||
OP_TYPE = "beam_search"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class BeamSearchDecodeOpCost(CompOpCost):
|
||||
OP_TYPE = "beam_search_decode"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class CastOpCost(CompOpCost):
|
||||
OP_TYPE = "cast"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ConcatOpCost(CompOpCost):
|
||||
OP_TYPE = "concat"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class DropoutOpCost(CompOpCost):
|
||||
OP_TYPE = "dropout"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class DropoutGradOpCost(CompOpCost):
|
||||
OP_TYPE = "dropout_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseAddOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_add"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseAddGradOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_add_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseDivOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_div"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseDivGradOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_div_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseMulOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_mul"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseMulGradOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_mul_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseSubOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_sub"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseSubGradOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_sub_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class EqualOpCost(CompOpCost):
|
||||
OP_TYPE = "equal"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class EmbeddingOpCost(CompOpCost):
|
||||
OP_TYPE = "c_embedding"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class EmbeddingGradOpCost(CompOpCost):
|
||||
OP_TYPE = "c_embedding_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class FillConstantOpCost(CompOpCost):
|
||||
OP_TYPE = "fill_constant"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class FillConstantBatchSizeLikeOpCost(CompOpCost):
|
||||
OP_TYPE = "fill_constant_batch_size_like"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class FusedSoftmaxMaskUpperTriangleOpCost(CompOpCost):
|
||||
OP_TYPE = "fused_softmax_mask_upper_triangle"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class FusedSoftmaxMaskUpperTriangleGradOpCost(CompOpCost):
|
||||
OP_TYPE = "fused_softmax_mask_upper_triangle_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class GatherOpCost(CompOpCost):
|
||||
OP_TYPE = "gather"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class GeluOpCost(CompOpCost):
|
||||
OP_TYPE = "gelu"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class GeluGradOpCost(CompOpCost):
|
||||
OP_TYPE = "gelu_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class GreaterEqualOpCost(CompOpCost):
|
||||
OP_TYPE = "greater_equal"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class IncrementOpCost(CompOpCost):
|
||||
OP_TYPE = "increment"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class IsEmptyOpCost(CompOpCost):
|
||||
OP_TYPE = "is_empty"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LayerNormOpCost(CompOpCost):
|
||||
OP_TYPE = "layer_norm"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LayerNormGradOpCost(CompOpCost):
|
||||
OP_TYPE = "layer_norm_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LessThanOpCost(CompOpCost):
|
||||
OP_TYPE = "less_than"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LogicalNotOpCost(CompOpCost):
|
||||
OP_TYPE = "logical_not"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LogicalAndOpCost(CompOpCost):
|
||||
OP_TYPE = "logical_and"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LodResetOpCost(CompOpCost):
|
||||
OP_TYPE = "lod_reset"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LogOpCost(CompOpCost):
|
||||
OP_TYPE = "log"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LookupTableV2OpCost(CompOpCost):
|
||||
OP_TYPE = "lookup_table_v2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LookupTableV2GradOpCost(CompOpCost):
|
||||
OP_TYPE = "lookup_table_v2_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MatmulOpCost(CompOpCost):
|
||||
OP_TYPE = "matmul"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MatmulGradOpCost(CompOpCost):
|
||||
OP_TYPE = "matmul_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MatmulV2OpCost(CompOpCost):
|
||||
OP_TYPE = "matmul_v2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MatmulV2GradOpCost(CompOpCost):
|
||||
OP_TYPE = "matmul_v2_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MemcpyOpCost(CompOpCost):
|
||||
OP_TYPE = "memcpy"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MulOpCost(CompOpCost):
|
||||
OP_TYPE = "mul"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MulGradOpCost(CompOpCost):
|
||||
OP_TYPE = "mul_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class OneHotOpCost(CompOpCost):
|
||||
OP_TYPE = "one_hot"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReadFromArrayOpCost(CompOpCost):
|
||||
OP_TYPE = "read_from_array"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReduceSumOpCost(CompOpCost):
|
||||
OP_TYPE = "reduce_sum"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReduceSumGradOpCost(CompOpCost):
|
||||
OP_TYPE = "reduce_sum_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Reshape2OpCost(CompOpCost):
|
||||
OP_TYPE = "reshape2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Reshape2GradOpCost(CompOpCost):
|
||||
OP_TYPE = "reshape2_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReduceMeanOpCost(CompOpCost):
|
||||
OP_TYPE = "reduce_mean"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReduceMeanGradOpCost(CompOpCost):
|
||||
OP_TYPE = "reduce_mean_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ScaleOpCost(CompOpCost):
|
||||
OP_TYPE = "scale"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ShapeOpCost(CompOpCost):
|
||||
OP_TYPE = "shape"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SliceOpCost(CompOpCost):
|
||||
OP_TYPE = "slice"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SoftmaxOpCost(CompOpCost):
|
||||
OP_TYPE = "softmax"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SoftmaxGradOpCost(CompOpCost):
|
||||
OP_TYPE = "softmax_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SoftmaxWithCrossEntropyOpCost(CompOpCost):
|
||||
OP_TYPE = "softmax_with_cross_entropy"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SoftmaxWithCrossEntropyGradOpCost(CompOpCost):
|
||||
OP_TYPE = "softmax_with_cross_entropy_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SplitOpCost(CompOpCost):
|
||||
OP_TYPE = "split"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Squeeze2OpCost(CompOpCost):
|
||||
OP_TYPE = "squeeze2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SquareOpCost(CompOpCost):
|
||||
OP_TYPE = "square"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SquareGradOpCost(CompOpCost):
|
||||
OP_TYPE = "square_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SumOpCost(CompOpCost):
|
||||
OP_TYPE = "sum"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class TopKOpCost(CompOpCost):
|
||||
OP_TYPE = "top_k"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Transpose2OpCost(CompOpCost):
|
||||
OP_TYPE = "transpose2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Transpose2GradOpCost(CompOpCost):
|
||||
OP_TYPE = "transpose2_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Unsqueeze2OpCost(CompOpCost):
|
||||
OP_TYPE = "unsqueeze2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class WriteToArrayOpCost(CompOpCost):
|
||||
OP_TYPE = "write_to_array"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
@@ -0,0 +1,671 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import reduce
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..dist_tensor import DistributedTensor
|
||||
from ..operators.common import get_distributed_operator_impl_container
|
||||
from .base_cost import Cost
|
||||
|
||||
|
||||
class CostEstimator:
|
||||
_special_op_type = ["fused_attention", "fused_feedforward"]
|
||||
|
||||
def __init__(
|
||||
self, program, cluster, mode="modeling", rank=None, loop_count=10
|
||||
):
|
||||
self._program = program
|
||||
self._cluster = cluster
|
||||
self._check_mode(mode)
|
||||
self._mode = mode
|
||||
self._rank = rank if rank is not None else paddle.distributed.get_rank()
|
||||
self._loop_count = loop_count
|
||||
self._global_cost = Cost()
|
||||
self._local_cost_mapping = {}
|
||||
self._detailed_cost = OrderedDict() # {`op_id`: {"reshard": [], "dist_op": [], "local_cost": local_cost}}}
|
||||
self._bubble_time_mapping = {}
|
||||
self._ordered_ops = []
|
||||
self.max_memories = {}
|
||||
self.max_memory = None
|
||||
|
||||
@property
|
||||
def loop_count(self):
|
||||
return self._loop_count
|
||||
|
||||
@property
|
||||
def detailed_cost(self):
|
||||
return self._detailed_cost
|
||||
|
||||
@property
|
||||
def program(self):
|
||||
return self._program
|
||||
|
||||
@property
|
||||
def rank(self):
|
||||
return self._rank
|
||||
|
||||
@property
|
||||
def dist_context(self):
|
||||
return self._dist_context
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
return self._cluster
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return self._mode
|
||||
|
||||
@property
|
||||
def global_cost(self):
|
||||
max_time = 0
|
||||
memory = 0
|
||||
flops = 0
|
||||
for rank in self._local_cost_mapping:
|
||||
cost = self._local_cost_mapping[rank]
|
||||
if cost.time > max_time:
|
||||
max_time = cost.time
|
||||
memory += cost.memory
|
||||
flops += cost.flops
|
||||
self._global_cost.time = max_time
|
||||
self._global_cost.memory = memory
|
||||
self._global_cost.flops = flops
|
||||
return self._global_cost
|
||||
|
||||
def local_cost(self, rank=None):
|
||||
rank = self.rank if rank is None else rank
|
||||
if rank not in self._local_cost_mapping:
|
||||
self._local_cost_mapping[rank] = Cost()
|
||||
|
||||
return self._local_cost_mapping[rank]
|
||||
|
||||
def local_bubble_time(self, rank=None):
|
||||
rank = self.rank if rank is None else rank
|
||||
return self._bubble_time_mapping[rank]
|
||||
|
||||
def _check_mode(self, mode):
|
||||
if mode not in ["modeling", "profiling"]:
|
||||
raise ValueError(
|
||||
f"Just support modeling and profiling, but got {mode}"
|
||||
)
|
||||
|
||||
def _is_special_var_name(self, var_name):
|
||||
special_var_name = ["lod_tensor_blocking_queue_0"]
|
||||
if var_name in special_var_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _estimate_core(self, dist_context, resharder, block):
|
||||
from ..reshard import get_var_with_recursion
|
||||
|
||||
ops = block.ops
|
||||
loop_count = None
|
||||
if block.desc.id != self.program.global_block().desc.id:
|
||||
loop_count = self.loop_count
|
||||
else:
|
||||
loop_count = 1
|
||||
for i in range(loop_count):
|
||||
for op in ops:
|
||||
self._detailed_cost[op.desc.id()] = OrderedDict()
|
||||
# If in the while sub block, the detail of cost is the last cost
|
||||
detail = self._detailed_cost[op.desc.id()]
|
||||
detail["reshard_cost"] = OrderedDict() #
|
||||
detail["dist_op_cost"] = []
|
||||
if int(op.attr('op_role')) == int(OpRole.Optimize):
|
||||
continue
|
||||
if op.type in [
|
||||
"create_py_reader",
|
||||
"create_double_buffer_reader",
|
||||
"read",
|
||||
]:
|
||||
continue
|
||||
|
||||
# NOTE: It does not support nested loop and just supports while op when op has sub block now.
|
||||
if op.type == "while":
|
||||
while_block = self.program.blocks[op.attr("sub_block").id]
|
||||
self._estimate_core(dist_context, resharder, while_block)
|
||||
continue
|
||||
|
||||
for var_name in op.input_arg_names:
|
||||
if self._is_special_var_name(var_name):
|
||||
continue
|
||||
var = get_var_with_recursion(var_name, block, self.program)
|
||||
reshard_cost = resharder.get_cost(op, var, self.cluster)
|
||||
|
||||
# Calc reshard cost
|
||||
if reshard_cost is not None:
|
||||
detail["reshard_cost"][var_name] = reshard_cost
|
||||
|
||||
comm_costs = reshard_cost[0]
|
||||
local_comp_cost = reshard_cost[1]
|
||||
for comm_cost in comm_costs:
|
||||
# Time is cumulative in global cost and local cost, but memory and flops just are cumulative in global cost.
|
||||
# Comm sync
|
||||
for item in comm_cost:
|
||||
group_ranks, cost = item
|
||||
max_time = None
|
||||
cost_time = {}
|
||||
for rank in group_ranks:
|
||||
rank_cost = self.local_cost(rank)
|
||||
cost_time[rank] = rank_cost.time
|
||||
if max_time is None:
|
||||
max_time = rank_cost.time
|
||||
else:
|
||||
if max_time < rank_cost.time:
|
||||
max_time = rank_cost.time
|
||||
|
||||
for rank in group_ranks:
|
||||
self.local_cost(rank).time = (
|
||||
max_time + cost.time
|
||||
)
|
||||
|
||||
if rank not in self._bubble_time_mapping:
|
||||
self._bubble_time_mapping[rank] = 0
|
||||
|
||||
self._bubble_time_mapping[rank] += (
|
||||
max_time - cost_time[rank]
|
||||
)
|
||||
|
||||
for rank in local_comp_cost:
|
||||
for comp_cost in local_comp_cost[rank]:
|
||||
self.local_cost(rank).time += comp_cost.time
|
||||
|
||||
# Calc dist op cost
|
||||
dist_op = dist_context.get_dist_op_for_program(op)
|
||||
if not dist_op:
|
||||
continue
|
||||
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
processes = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
container = get_distributed_operator_impl_container(
|
||||
op_dist_attr.impl_type
|
||||
)
|
||||
dist_impl = container.impls[op_dist_attr.impl_idx]
|
||||
|
||||
dist_op_cost = dist_impl.calc_cost(
|
||||
op.attr('op_role'), dist_op, dist_context, self.cluster
|
||||
)
|
||||
detail["dist_op_cost"] = dist_op_cost
|
||||
|
||||
if dist_op_cost is None:
|
||||
assert (
|
||||
dist_op.serial_op.type in CostEstimator._special_op_type
|
||||
)
|
||||
continue
|
||||
for item in dist_op_cost:
|
||||
if isinstance(item, list):
|
||||
# Comm sync
|
||||
for comm_op_cost in item:
|
||||
max_time = None
|
||||
cost_time = {}
|
||||
group_ranks = comm_op_cost.group_ranks
|
||||
for rank in comm_op_cost.group_ranks:
|
||||
rank_cost = self.local_cost(rank)
|
||||
cost_time[rank] = rank_cost.time
|
||||
if max_time is None:
|
||||
max_time = rank_cost.time
|
||||
else:
|
||||
if max_time < rank_cost.time:
|
||||
max_time = rank_cost.time
|
||||
for rank in group_ranks:
|
||||
self.local_cost(rank).time = (
|
||||
max_time + comm_op_cost.time
|
||||
if op.attr('op_role') != OpRole.Backward
|
||||
else max_time + 0.9 * comm_op_cost.time
|
||||
)
|
||||
if rank not in self._bubble_time_mapping:
|
||||
self._bubble_time_mapping[rank] = 0
|
||||
self._bubble_time_mapping[rank] += (
|
||||
max_time - cost_time[rank]
|
||||
)
|
||||
elif isinstance(item, dict):
|
||||
# Op just one
|
||||
for rank in processes:
|
||||
# DP+PP+MP
|
||||
if rank not in item:
|
||||
continue
|
||||
self.local_cost(rank).time += item[rank].time
|
||||
|
||||
def prepare(self):
|
||||
self._global_cost = Cost()
|
||||
self._local_cost_mapping = {}
|
||||
self._detailed_cost = OrderedDict()
|
||||
self._bubble_time_mapping = {}
|
||||
|
||||
def _calculate_bytes(self, sizes, dtype):
|
||||
if sizes:
|
||||
total_count = reduce(lambda x, y: x * y, sizes, 1)
|
||||
else:
|
||||
total_count = 0
|
||||
|
||||
if dtype == paddle.float64 or dtype == paddle.int64:
|
||||
dtype_factor = 8
|
||||
elif dtype == paddle.float32 or dtype == paddle.int32:
|
||||
dtype_factor = 4
|
||||
elif (
|
||||
dtype == paddle.float16
|
||||
or dtype == paddle.bfloat16
|
||||
or dtype == paddle.int16
|
||||
):
|
||||
dtype_factor = 2
|
||||
elif dtype == paddle.int8 or dtype == paddle.uint8:
|
||||
dtype_factor = 1
|
||||
else:
|
||||
dtype_factor = 8
|
||||
|
||||
memory = total_count * dtype_factor
|
||||
return memory
|
||||
|
||||
def _estimate_max_memory_by_dist_op(self, dist_context):
|
||||
# This estimation will be improved, now reshard and inplace are not considered.
|
||||
# Persist var is not free.
|
||||
def _convert_pm_and_dm_to_str(process_mesh, dims_mapping):
|
||||
processes = ",".join([str(x) for x in process_mesh.process_ids])
|
||||
topology = ",".join([str(x) for x in process_mesh.shape])
|
||||
dims_mapping = ",".join([str(x) for x in dims_mapping])
|
||||
result = processes + topology + dims_mapping
|
||||
return result
|
||||
|
||||
memories = {}
|
||||
self.max_memories = {}
|
||||
var_info = {} # var_name: [[process_mesh, dims_mapping], [id]], [[process_mesh, dims_mapping], [id]]}
|
||||
|
||||
for block in self.program.blocks:
|
||||
for op in block.ops:
|
||||
self._ordered_ops.append([op.desc.id(), op])
|
||||
self._ordered_ops.sort(key=lambda x: x[0])
|
||||
|
||||
parameters = set()
|
||||
for op_id, op in self._ordered_ops:
|
||||
if op.type in [
|
||||
"create_py_reader",
|
||||
"create_double_buffer_reader",
|
||||
"read",
|
||||
]:
|
||||
continue
|
||||
dist_op = dist_context.get_dist_op_for_program(op)
|
||||
if not dist_op:
|
||||
continue
|
||||
process_mesh = dist_op.dist_attr.process_mesh
|
||||
for var_name in op.input_arg_names:
|
||||
input_dims_mapping = dist_op.dist_attr.get_input_dims_mapping(
|
||||
var_name
|
||||
)
|
||||
|
||||
if var_name not in var_info:
|
||||
var_info[var_name] = {}
|
||||
key = _convert_pm_and_dm_to_str(
|
||||
process_mesh, input_dims_mapping
|
||||
)
|
||||
if key not in var_info[var_name]:
|
||||
var_info[var_name][key] = {}
|
||||
# It is even partition now
|
||||
if "position" not in var_info[var_name][key]:
|
||||
var_info[var_name][key]["position"] = []
|
||||
var_info[var_name][key]["position"].append(op_id)
|
||||
|
||||
if "memory" not in var_info[var_name][key]:
|
||||
var = dist_op.get_serial_input(var_name)
|
||||
global_sizes = var.shape
|
||||
dtype = var.dtype
|
||||
sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes,
|
||||
input_dims_mapping,
|
||||
process_mesh.shape,
|
||||
process_mesh.process_ids,
|
||||
)
|
||||
var_info[var_name][key]["memory"] = self._calculate_bytes(
|
||||
sizes, dtype
|
||||
)
|
||||
if var.persistable:
|
||||
name = var_name + key
|
||||
if name not in parameters:
|
||||
parameters.add(name)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in memories:
|
||||
memories[process] = 0
|
||||
memories[process] += var_info[var_name][key][
|
||||
"memory"
|
||||
]
|
||||
|
||||
for var_name in op.output_arg_names:
|
||||
output_dims_mapping = dist_op.dist_attr.get_output_dims_mapping(
|
||||
var_name
|
||||
)
|
||||
if var_name not in var_info:
|
||||
var_info[var_name] = {}
|
||||
key = _convert_pm_and_dm_to_str(
|
||||
process_mesh, output_dims_mapping
|
||||
)
|
||||
if key not in var_info[var_name]:
|
||||
var_info[var_name][key] = {}
|
||||
if "position" not in var_info[var_name][key]:
|
||||
var_info[var_name][key]["position"] = []
|
||||
var_info[var_name][key]["position"].append(op_id)
|
||||
|
||||
if "memory" not in var_info[var_name][key]:
|
||||
var = dist_op.get_serial_output(var_name)
|
||||
global_sizes = var.shape
|
||||
dtype = var.dtype
|
||||
sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes,
|
||||
output_dims_mapping,
|
||||
process_mesh.shape,
|
||||
process_mesh.process_ids,
|
||||
)
|
||||
var_info[var_name][key]["memory"] = self._calculate_bytes(
|
||||
sizes, dtype
|
||||
)
|
||||
if var.persistable:
|
||||
name = var_name + key
|
||||
if name not in parameters:
|
||||
parameters.add(name)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in memories:
|
||||
memories[process] = 0
|
||||
memories[process] += var_info[var_name][key][
|
||||
"memory"
|
||||
]
|
||||
|
||||
has_used_vars = set()
|
||||
not_calc_vars = set()
|
||||
for op_id, op in self._ordered_ops:
|
||||
if op.type in [
|
||||
"create_py_reader",
|
||||
"create_double_buffer_reader",
|
||||
"read",
|
||||
]:
|
||||
continue
|
||||
can_free_memories = {}
|
||||
can_free_vars = set()
|
||||
dist_op = dist_context.get_dist_op_for_program(op)
|
||||
if not dist_op:
|
||||
continue
|
||||
process_mesh = dist_op.dist_attr.process_mesh
|
||||
for var_name in op.input_arg_names:
|
||||
input_dims_mapping = dist_op.dist_attr.get_input_dims_mapping(
|
||||
var_name
|
||||
)
|
||||
key = _convert_pm_and_dm_to_str(
|
||||
process_mesh, input_dims_mapping
|
||||
)
|
||||
has_used_var = var_name + key
|
||||
var = dist_op.get_serial_input(var_name)
|
||||
# Not used
|
||||
if (
|
||||
has_used_var not in has_used_vars
|
||||
and has_used_var not in parameters
|
||||
):
|
||||
if has_used_var in not_calc_vars:
|
||||
continue
|
||||
has_used_vars.add(has_used_var)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in memories:
|
||||
memories[process] = 0
|
||||
memories[process] += var_info[var_name][key]["memory"]
|
||||
# Used
|
||||
if op_id == var_info[var_name][key]["position"][-1]:
|
||||
if (
|
||||
has_used_var not in can_free_vars
|
||||
and not var.persistable
|
||||
):
|
||||
can_free_vars.add(has_used_var)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in can_free_memories:
|
||||
can_free_memories[process] = 0
|
||||
can_free_memories[process] += var_info[var_name][
|
||||
key
|
||||
]["memory"]
|
||||
|
||||
for var_name in op.output_arg_names:
|
||||
output_dims_mapping = dist_op.dist_attr.get_output_dims_mapping(
|
||||
var_name
|
||||
)
|
||||
key = _convert_pm_and_dm_to_str(
|
||||
process_mesh, output_dims_mapping
|
||||
)
|
||||
has_used_var = var_name + key
|
||||
var = dist_op.get_serial_output(var_name)
|
||||
if (
|
||||
op.type == "reshape2"
|
||||
or op.type == "transpose2"
|
||||
or op.type == "elementwise_add"
|
||||
):
|
||||
not_calc_vars.add(has_used_var)
|
||||
continue
|
||||
# Not used
|
||||
if (
|
||||
has_used_var not in has_used_vars
|
||||
and has_used_var not in parameters
|
||||
):
|
||||
has_used_vars.add(has_used_var)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in memories:
|
||||
memories[process] = 0
|
||||
memories[process] += var_info[var_name][key]["memory"]
|
||||
# Used
|
||||
if op_id == var_info[var_name][key]["position"][-1]:
|
||||
if (
|
||||
has_used_var not in can_free_vars
|
||||
and not var.persistable
|
||||
):
|
||||
can_free_vars.add(has_used_var)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in can_free_memories:
|
||||
can_free_memories[process] = 0
|
||||
can_free_memories[process] += var_info[var_name][
|
||||
key
|
||||
]["memory"]
|
||||
|
||||
# Calc peak memory
|
||||
for process in memories:
|
||||
if process not in self.max_memories:
|
||||
self.max_memories[process] = memories[process]
|
||||
else:
|
||||
if memories[process] > self.max_memories[process]:
|
||||
self.max_memories[process] = memories[process]
|
||||
# Free memory
|
||||
for process in can_free_memories:
|
||||
if process in memories:
|
||||
memories[process] -= can_free_memories[process]
|
||||
|
||||
# Calculate the max memory in all ranks
|
||||
max_memory = max(self.max_memories.values())
|
||||
self.max_memory = max_memory
|
||||
|
||||
return max_memory
|
||||
|
||||
def estimate(self, dist_context, resharder=None):
|
||||
self.prepare()
|
||||
from ..reshard import Resharder
|
||||
|
||||
resharder = (
|
||||
Resharder(self.program, None, self.rank, dist_context, [])
|
||||
if resharder is None
|
||||
else resharder
|
||||
)
|
||||
|
||||
block = self.program.global_block()
|
||||
self._estimate_core(dist_context, resharder, block)
|
||||
|
||||
return self.global_cost
|
||||
|
||||
def _print_tag(self, max_len, length):
|
||||
tag = "+" + "-" * max_len
|
||||
for i in range(length):
|
||||
print(tag, end="")
|
||||
if i == length - 1:
|
||||
print("+")
|
||||
|
||||
def _print_vals(self, vals, max_len):
|
||||
for idx, val in enumerate(vals):
|
||||
s = "|" + str(val).center(max_len)
|
||||
print(s, end="")
|
||||
if idx == len(vals) - 1:
|
||||
print("|")
|
||||
|
||||
def _pretty_print_memory_cost(self):
|
||||
"""Print memory of every rank prettily."""
|
||||
if not self.max_memories or not self.max_memory:
|
||||
raise ValueError("Please calculate memory cost before print.")
|
||||
|
||||
# Padding automatically
|
||||
max_len = 0
|
||||
header = ["Rank", "Memory(MiB)"]
|
||||
memories = [
|
||||
int(item // 1e6) for item in list(self.max_memories.values())
|
||||
]
|
||||
for memory in memories + header:
|
||||
if len(str(memory)) > max_len:
|
||||
max_len = len(str(memory))
|
||||
max_len += 4 # for pretty print of center
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
# Print header
|
||||
self._print_vals(header, max_len)
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
# Print rank and its memory
|
||||
for i in range(len(self.max_memories)):
|
||||
memory = memories[i]
|
||||
vals = [i, memory]
|
||||
self._print_vals(vals, max_len)
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
def _pretty_print_global(self):
|
||||
"""Print global execution time and max memory prettily."""
|
||||
if not self.max_memories or not self.max_memory:
|
||||
raise ValueError("Please calculate cost before print.")
|
||||
|
||||
# Padding automatically
|
||||
max_len = 0
|
||||
header = ["Execution Time(us)", "Max Memory(MiB)"]
|
||||
vals = [round(self.global_cost.time, 3), int(self.max_memory // 1e6)]
|
||||
for memory in vals + header:
|
||||
if len(str(memory)) > max_len:
|
||||
max_len = len(str(memory))
|
||||
max_len += 4 # for pretty print of center
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
# Print header
|
||||
self._print_vals(header, max_len)
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
# Print exec time and max memory
|
||||
self._print_vals(vals, max_len)
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
def pretty_print_cost(self):
|
||||
"""Print cost prettily."""
|
||||
print("The global execution time and max memory are as follows:")
|
||||
self._pretty_print_global()
|
||||
print("The memory of every rank is as follows:")
|
||||
self._pretty_print_memory_cost()
|
||||
|
||||
|
||||
def get_cost_from_engine(engine, mode):
|
||||
import copy
|
||||
|
||||
from ..utils import to_list
|
||||
|
||||
# Construct cost estimator by original main program
|
||||
serial_main_prog = (
|
||||
engine._fwd_main_progs[mode].clone()
|
||||
if mode in engine._fwd_main_progs
|
||||
else engine._orig_main_prog.clone()
|
||||
)
|
||||
|
||||
serial_startup_prog = (
|
||||
engine._fwd_dist_contexts[mode]._original_serial_main_program.clone()
|
||||
if mode in engine._fwd_dist_contexts
|
||||
else engine._orig_startup_prog.clone()
|
||||
)
|
||||
losses = (
|
||||
to_list(engine._loss)
|
||||
if (
|
||||
not isinstance(engine._loss, paddle.nn.Layer)
|
||||
and not callable(engine._loss)
|
||||
)
|
||||
else engine._losses
|
||||
)
|
||||
serial_optimizer = copy.deepcopy(engine._orig_optimizer)
|
||||
if mode in engine._fwd_dist_contexts:
|
||||
dist_context = copy.deepcopy(engine._fwd_dist_contexts[mode])
|
||||
else:
|
||||
from ..dist_context import DistributedContext
|
||||
|
||||
dist_context = DistributedContext(
|
||||
serial_main_prog,
|
||||
serial_startup_prog,
|
||||
serial_optimizer,
|
||||
losses,
|
||||
{},
|
||||
{"loss": losses},
|
||||
engine._cluster,
|
||||
engine._strategy,
|
||||
)
|
||||
from ..completion import Completer
|
||||
|
||||
completer = Completer(dist_context)
|
||||
completer.complete_forward_annotation()
|
||||
dist_context.block_state.parse_forward_blocks(
|
||||
dist_context.serial_main_program
|
||||
)
|
||||
|
||||
if mode == "eval" or mode == "predict":
|
||||
cost_estimator = CostEstimator(serial_main_prog, engine._cluster)
|
||||
elif mode == "train":
|
||||
from ..parallelizer_v2 import Parallelizer
|
||||
|
||||
# Get serial main program with backward
|
||||
parallelizer = Parallelizer(mode, completer, dist_context)
|
||||
# Generate backward
|
||||
loss_name = dist_context.serial_loss.name
|
||||
serial_loss = serial_main_prog.global_block()._var_recursive(loss_name)
|
||||
params_grads = parallelizer._generate_backward(
|
||||
serial_main_prog, serial_startup_prog, serial_loss
|
||||
)
|
||||
|
||||
# Generate optimizer
|
||||
optimizer_ops = parallelizer._generate_optimizer(
|
||||
serial_main_prog,
|
||||
serial_startup_prog,
|
||||
serial_optimizer,
|
||||
params_grads,
|
||||
)
|
||||
cost_estimator = CostEstimator(serial_main_prog, engine._cluster)
|
||||
|
||||
# Estimate global_cost and max memory
|
||||
global_cost = cost_estimator.estimate(dist_context)
|
||||
max_memory = cost_estimator._estimate_max_memory_by_dist_op(dist_context)
|
||||
|
||||
# Print the cost
|
||||
cost_estimator.pretty_print_cost()
|
||||
|
||||
return global_cost, max_memory
|
||||
@@ -0,0 +1,320 @@
|
||||
# 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.
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.base import core
|
||||
from paddle.base.data_feeder import convert_dtype
|
||||
from paddle.base.executor import (
|
||||
_as_lodtensor,
|
||||
_StandaloneExecutor,
|
||||
check_feed_shape_type,
|
||||
)
|
||||
from paddle.base.framework import Operator, Program
|
||||
from paddle.distributed.auto_parallel.static.utils import get_logger, is_comm_op
|
||||
|
||||
|
||||
def check_if_op_supports_runtime_profiling(op):
|
||||
return not is_comm_op(op)
|
||||
|
||||
|
||||
def _measure_program_real_op_cost_multipass(program, place, run_iters, verbose):
|
||||
'''
|
||||
Run op profiling for a single pass. Internal function, do not call this directly.
|
||||
'''
|
||||
|
||||
# clone the program to avoid accidental change made to the vanilla program.
|
||||
cloned_program = program.clone()
|
||||
cloned_main_block = cloned_program.global_block()
|
||||
|
||||
# We will run the executor in a newly created scope, so that our
|
||||
# executor will not pollute the global scope when running. Since
|
||||
# we created a brand new scope, we need to manually create input
|
||||
# tensors and network parameters and feed fake data into them.
|
||||
scope = core.Scope()
|
||||
|
||||
logger = get_logger(log_level=logging.INFO)
|
||||
|
||||
def _analyze_graph_and_collect_all_vars_with_zero_in_degree():
|
||||
var_in_degree = {}
|
||||
|
||||
def _collect_op_input_var_names(op: Operator):
|
||||
input_var_names = []
|
||||
for input_name in op.input_names:
|
||||
input_var_names += op.input(input_name)
|
||||
return input_var_names
|
||||
|
||||
def _collect_op_output_var_names(op: Operator):
|
||||
output_var_names = []
|
||||
for output_name in op.output_names:
|
||||
output_var_names += op.output(output_name)
|
||||
return output_var_names
|
||||
|
||||
def _record_op_output_vars_in_degree(in_var_names, out_var_names):
|
||||
for out_var_name in out_var_names:
|
||||
if out_var_name in in_var_names:
|
||||
# NOTE (liuchenghao): if an op's input var is its output var,
|
||||
# this means this var forms an in-place connection to itself,
|
||||
# in this situation we need to ignore this variable, this way
|
||||
# we can ensure that vars with zero in-degree are dangling vars
|
||||
# and they should be created manually before program executes.
|
||||
continue
|
||||
var_in_degree[out_var_name] += 1
|
||||
|
||||
def _filter_vars_with_zero_in_degree_and_ignore_feed_fetch_vars():
|
||||
filtered_vars = []
|
||||
for var_name in var_in_degree:
|
||||
if var_name in ['feed', 'fetch']:
|
||||
continue
|
||||
if var_in_degree[var_name] == 0:
|
||||
filtered_vars.append(var_name)
|
||||
return filtered_vars
|
||||
|
||||
for op in cloned_main_block.ops:
|
||||
op: Operator
|
||||
if is_comm_op(op):
|
||||
# ignore communication op from graph, because sometimes we want to profile a sub-graph
|
||||
# and these dangling operators will not work (no graph to communicate to/from)
|
||||
continue
|
||||
input_var_names, output_var_names = (
|
||||
_collect_op_input_var_names(op),
|
||||
_collect_op_output_var_names(op),
|
||||
)
|
||||
for var_name in input_var_names + output_var_names:
|
||||
if var_name not in var_in_degree:
|
||||
var_in_degree[var_name] = 0
|
||||
_record_op_output_vars_in_degree(input_var_names, output_var_names)
|
||||
return _filter_vars_with_zero_in_degree_and_ignore_feed_fetch_vars()
|
||||
|
||||
def _alloc_and_fill_var(var_name):
|
||||
supported_var_dtypes = [
|
||||
"paddle.float16",
|
||||
"paddle.float32",
|
||||
"paddle.float64",
|
||||
"paddle.int8",
|
||||
"paddle.int16",
|
||||
"paddle.int32",
|
||||
"paddle.int64",
|
||||
"paddle.bool",
|
||||
]
|
||||
var = cloned_main_block.var(var_name)
|
||||
var_shape = var.shape
|
||||
var_dtype = var.dtype
|
||||
assert str(var_dtype) in supported_var_dtypes, (
|
||||
'Found unsupported variable dtype: "{}", current supported '
|
||||
'dtype(s) is/are: [{}]. '.format(
|
||||
str(var_dtype), ", ".join(supported_var_dtypes)
|
||||
)
|
||||
)
|
||||
(
|
||||
logger.info(
|
||||
f'[+] var: "{var_name}", shape={var_shape}, dtype="{var_dtype}".\n'
|
||||
)
|
||||
if verbose
|
||||
else None
|
||||
)
|
||||
np_dtype = (
|
||||
convert_dtype(var_dtype)
|
||||
if isinstance(var_dtype, core.VarDesc.VarType)
|
||||
else var_dtype
|
||||
)
|
||||
if str(var_dtype).find('int') != -1:
|
||||
# target variable's type is int* (uint*, int*), it is highly possible that
|
||||
# the target variable contains indices (such as lookup_table op's input var)
|
||||
# for safety we need to fill it with all one instead of random numbers
|
||||
# NOTE (liuchenghao): filling with zero will generate "division by zero" error
|
||||
# in mod ops, so filling with one seems to be the simplest way to make it work,
|
||||
# although it is possible that for array with only one element, index "1" is
|
||||
# invalid, that situation is very rare and we don't need to care about it now.
|
||||
new_tensor = np.array(np.ones(var_shape)).astype(np_dtype)
|
||||
else:
|
||||
# target variable's type is float*, we treat it as an ordinary tensor, fill it
|
||||
# with random gaussian numbers
|
||||
new_tensor = np.array(np.random.randn(*var_shape)).astype(np_dtype)
|
||||
new_tensor = _as_lodtensor(new_tensor, place, var_dtype)
|
||||
check_feed_shape_type(var, new_tensor)
|
||||
core.set_variable(scope, new_tensor, var_name)
|
||||
return new_tensor
|
||||
|
||||
def _configure_feed_ops_and_return_feed_names():
|
||||
"""
|
||||
configure feed op,
|
||||
1. alloc feed op output var storage
|
||||
2. fill feed op's input var
|
||||
return feed var names
|
||||
"""
|
||||
|
||||
feed_names = []
|
||||
has_feed_op = False
|
||||
for op in cloned_main_block.ops:
|
||||
if op.type == "feed":
|
||||
has_feed_op = True
|
||||
out_var_name = op.desc.output('Out')[0]
|
||||
in_var_name = op.desc.input('X')[0] # this is usually "feed"
|
||||
input_index = op.desc.attr('col')
|
||||
new_tensor = _alloc_and_fill_var(out_var_name)
|
||||
core.set_feed_variable(
|
||||
scope, new_tensor, in_var_name, input_index
|
||||
)
|
||||
feed_names.append(out_var_name)
|
||||
if not has_feed_op:
|
||||
(
|
||||
logger.info("WARNING: program does not have any feed op.\n")
|
||||
if verbose
|
||||
else None
|
||||
)
|
||||
return feed_names
|
||||
|
||||
for var_name in _analyze_graph_and_collect_all_vars_with_zero_in_degree():
|
||||
_alloc_and_fill_var(var_name)
|
||||
feed_names = _configure_feed_ops_and_return_feed_names()
|
||||
|
||||
# build a simple plan from program and run profiling
|
||||
plan = core.Plan([core.Job("default")], {"default": cloned_program.desc})
|
||||
exe = _StandaloneExecutor(place, plan, scope)
|
||||
|
||||
num_ops = len(cloned_main_block.ops)
|
||||
prof_results = [[None for _ in range(run_iters)] for _ in range(num_ops)]
|
||||
|
||||
for iter_id in range(run_iters):
|
||||
# for each iteration, run profiling and retrieve modified version of program desc
|
||||
program_desc = exe.run_profile(feed_names)
|
||||
|
||||
# rebuild program object from the new program desc
|
||||
temp_program = cloned_program.clone()
|
||||
temp_program._rebuild_from_desc(program_desc)
|
||||
temp_main_block = temp_program.global_block()
|
||||
|
||||
# collect profiling result
|
||||
for op_id, temp_op in zip(
|
||||
range(len(temp_main_block.ops)), temp_main_block.ops
|
||||
):
|
||||
run_time_us = temp_op.dist_attr.run_time_us
|
||||
prof_results[op_id][iter_id] = (
|
||||
run_time_us
|
||||
if check_if_op_supports_runtime_profiling(temp_op)
|
||||
and run_time_us >= 0.0
|
||||
else None
|
||||
)
|
||||
return prof_results
|
||||
|
||||
|
||||
def measure_program_real_op_cost(
|
||||
program: paddle.static.Program,
|
||||
run_iters: int = 8,
|
||||
place=paddle.base.framework._current_expected_place(),
|
||||
verbose_level: int = 0,
|
||||
):
|
||||
'''
|
||||
Description
|
||||
-----------
|
||||
Measuring real op run time (us) with respect to the given "program" and "place".
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
@param program: paddle.static.Program
|
||||
The program object waiting to be executed.
|
||||
@param run_iters: int
|
||||
Specify how many iterations will be run during profiling. Larger value tends
|
||||
to give more accurate profiling result but requires more time.
|
||||
@param place: paddle.CPUPlace | paddle.CUDAPlace
|
||||
Where the program is going to be executed.
|
||||
@param verbose_level: int
|
||||
Set up verbose level during profiling. Can be set to one of the following:
|
||||
0 = turn off, don't output anything,
|
||||
1 = output profiling messages only,
|
||||
2 = output profiling and debug messages.
|
||||
|
||||
Returns
|
||||
-----------
|
||||
Nothing to return. This API will write op run time directly into program object.
|
||||
For example, to retrieve the run time for the first op in program, use:
|
||||
>>> program.global_block().ops[0].dist_attr.run_time_us
|
||||
|
||||
Note
|
||||
-----------
|
||||
Not all ops support runtime profiling. Currently communication ops do not support
|
||||
runtime profiling feature since their execution times rely on other ops. To check
|
||||
if an op supports runtime profiling, use:
|
||||
>>> check_if_op_supports_runtime_profiling(op)
|
||||
where "op" is an instance of "paddle.base.framework.Operator".
|
||||
|
||||
Example
|
||||
-----------
|
||||
* Profiling a simple program from scratch:
|
||||
>>> from paddle.distributed.auto_parallel.static.utils import (
|
||||
... measure_program_real_op_cost,
|
||||
... )
|
||||
>>> program = ... # build your own program object here.
|
||||
>>> measure_program_real_op_cost(
|
||||
>>> program, verbose_level=1
|
||||
>>> )
|
||||
* Profiling a program which is already embedded into an Executor or some other class instance:
|
||||
>>> import paddle
|
||||
>>> from paddle.distributed.auto_parallel.static.utils import (
|
||||
... measure_program_real_op_cost,
|
||||
... )
|
||||
>>> place: str = paddle.device.get_device() # here we assume place = "cuda:x"
|
||||
>>> place = paddle.CUDAPlace(int(place.split(':')[1]))
|
||||
>>> # here "program" is an inner object that has already been built before
|
||||
>>> measure_program_real_op_cost(program, verbose_level=1)
|
||||
'''
|
||||
|
||||
assert isinstance(program, Program), (
|
||||
f'"program" should be a instance of "paddle.base.framework.Program" but got type "{type(program).__name__}".'
|
||||
)
|
||||
supported_places = [
|
||||
paddle.CUDAPlace,
|
||||
]
|
||||
assert any(
|
||||
isinstance(place, supported_place)
|
||||
for supported_place in supported_places
|
||||
), (
|
||||
f'Current place ({place}) does not support runtime profiling. "place" should be one of the following: {supported_places}.'
|
||||
)
|
||||
assert isinstance(run_iters, int) and run_iters >= 1, (
|
||||
'Invalid parameter run_iters set. run_iters should be an integer >= 1.'
|
||||
)
|
||||
if run_iters == 1:
|
||||
warnings.warn(
|
||||
'run_iters was set to 1, profiling results might be inaccurate due to outliers.'
|
||||
)
|
||||
|
||||
logger = get_logger(log_level=logging.INFO)
|
||||
|
||||
# run profiling multiple times and record op run time of each run
|
||||
prof_results = _measure_program_real_op_cost_multipass(
|
||||
program, place, run_iters, verbose=(verbose_level >= 2)
|
||||
)
|
||||
op_num = len(prof_results)
|
||||
for op_id, op in zip(range(op_num), program.global_block().ops):
|
||||
op_runtime_us_final = None
|
||||
if prof_results[op_id][0] is not None:
|
||||
op_runtime_us_final = np.median(prof_results[op_id])
|
||||
if (
|
||||
op_runtime_us_final is not None
|
||||
and check_if_op_supports_runtime_profiling(op)
|
||||
):
|
||||
op.dist_attr.run_time_us = op_runtime_us_final
|
||||
(
|
||||
logger.info(
|
||||
f"{op_id!s:>4} {op.type!s:>32} {op_runtime_us_final:.1f} us"
|
||||
)
|
||||
if verbose_level >= 1
|
||||
else None
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from functools import reduce
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.auto_parallel.static.dist_tensor import (
|
||||
DistributedTensor,
|
||||
)
|
||||
from paddle.static import Variable
|
||||
|
||||
from .base_cost import Cost
|
||||
|
||||
|
||||
class TensorCost:
|
||||
def __init__(self, tensor=None, dist_tensor=None, shape=None, dtype=None):
|
||||
self._check_args(tensor, dist_tensor, shape, dtype)
|
||||
self._tensor = tensor
|
||||
self._dist_tensor = dist_tensor
|
||||
self._shape = shape
|
||||
self._dtype = dtype
|
||||
self._cost = self.calc_cost()
|
||||
|
||||
@property
|
||||
def tensor(self):
|
||||
return self._tensor
|
||||
|
||||
@property
|
||||
def dist_tensor(self):
|
||||
return self._dist_tensor
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return self._shape
|
||||
|
||||
@property
|
||||
def dtype(self):
|
||||
return self._dtype
|
||||
|
||||
def _check_args(self, tensor, dist_tensor, shape, dtype):
|
||||
if tensor is not None:
|
||||
assert shape is None and dist_tensor is None and dtype is None
|
||||
|
||||
if not isinstance(tensor, Variable):
|
||||
raise TypeError(
|
||||
f"Please check tensor type is Variable, but got {type(tensor)}"
|
||||
)
|
||||
|
||||
elif dist_tensor is not None:
|
||||
assert tensor is None and shape is None
|
||||
if not isinstance(dist_tensor, DistributedTensor):
|
||||
raise TypeError(
|
||||
f"Please check dist_tensor type is DistributedTensor, but got {type(dist_tensor)}"
|
||||
)
|
||||
|
||||
elif shape is not None:
|
||||
assert tensor is None and dist_tensor is None and dtype is not None
|
||||
if not isinstance(shape, (list, set)):
|
||||
raise TypeError(
|
||||
f"Please check shape type is list or set, but got {type(shape)}"
|
||||
)
|
||||
|
||||
elif dtype is not None:
|
||||
assert tensor is None and dist_tensor is None and shape is not None
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
return self._cost
|
||||
|
||||
def calc_cost(self):
|
||||
dtype = None
|
||||
shape = None
|
||||
|
||||
if self.dist_tensor:
|
||||
shape = self.dist_tensor.local_sizes()
|
||||
dtype = self.dist_tensor.serial_tensor.dtype
|
||||
elif self.tensor:
|
||||
shape = self.tensor.shape
|
||||
dtype = self.tensor.dtype
|
||||
elif self.shape and self.dtype:
|
||||
shape = self.shape
|
||||
dtype = self.dtype
|
||||
|
||||
total_count = reduce(lambda x, y: x * y, shape, 1)
|
||||
|
||||
if dtype == paddle.float32 or dtype == paddle.int32:
|
||||
dtype_factor = 4
|
||||
elif dtype == paddle.int64:
|
||||
dtype_factor = 8
|
||||
elif dtype == paddle.uint8:
|
||||
dtype_factor = 1
|
||||
else:
|
||||
dtype_factor = 2
|
||||
|
||||
memory = total_count * dtype_factor
|
||||
assert memory >= 0
|
||||
cost = Cost(memory=memory)
|
||||
|
||||
return cost
|
||||
@@ -0,0 +1,855 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import queue
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
from paddle.framework import core
|
||||
|
||||
SUCC = 0 # successor
|
||||
PRED = 1 # predecessor
|
||||
|
||||
|
||||
class CostNodeType(Enum):
|
||||
DEFAULT = 0
|
||||
COMPUTATION = 1
|
||||
COMMUNICATION = 2
|
||||
VARIABLE = 3
|
||||
MERGED = 4
|
||||
NOP = 5
|
||||
|
||||
|
||||
class Cost:
|
||||
def __init__(self):
|
||||
self.runtime = None
|
||||
self.static_mem = None
|
||||
self.peak_mem = None
|
||||
|
||||
|
||||
class CostModelMode(Enum):
|
||||
DEFAULT = 0
|
||||
BENCHMARKING = 1 # costs based on trial runs
|
||||
ANALYSIS = 2 # costs based on analysis
|
||||
MIXED = 3
|
||||
|
||||
|
||||
class CostNode:
|
||||
def __init__(self, node, node_type, id=None):
|
||||
self.id = id
|
||||
self.node = node
|
||||
self.type = node_type
|
||||
self._cost = 0
|
||||
self.is_optim = False
|
||||
self.is_bwd = False
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
return self._cost
|
||||
|
||||
@cost.setter
|
||||
def cost(self, cost):
|
||||
if cost < 0:
|
||||
raise ValueError('Cost must be above 0.')
|
||||
self._cost = cost
|
||||
|
||||
|
||||
class MergedOpsCostNode(CostNode):
|
||||
def __init__(self, node_type, id=None, base_node_list=None, is_bwd=False):
|
||||
super().__init__(None, node_type, id)
|
||||
self.node_list = base_node_list
|
||||
self.is_bwd = is_bwd
|
||||
|
||||
|
||||
class CommOpCostNode(CostNode):
|
||||
def __init__(
|
||||
self, node, node_type, id=None, comm_node_list=None, is_bwd=False
|
||||
):
|
||||
super().__init__(node, node_type, id)
|
||||
self.node_list = comm_node_list
|
||||
self.ranks = []
|
||||
self.comm_type = node.type
|
||||
self.is_bwd = is_bwd
|
||||
|
||||
def set_ranks(self, ranks):
|
||||
self.ranks = ranks
|
||||
|
||||
def set_shapes(self, input_shape, output_shape):
|
||||
self.input_shape = input_shape
|
||||
self.output_shape = output_shape
|
||||
|
||||
def init_comm_cost(self, cluster=None):
|
||||
# ref: https://github.com/NVIDIA/nccl-tests/blob/master/doc/PERFORMANCE.md
|
||||
# should get from `cluster`
|
||||
BANDWIDTH = 32 * 1024 / 1000 # MB/ms, V100 PCIe
|
||||
num_ranks = len(self.ranks)
|
||||
comm_volume = np.prod(self.input_shape) * 4
|
||||
|
||||
if 'allreduce' in self.comm_type:
|
||||
self._cost = comm_volume / (
|
||||
BANDWIDTH * num_ranks / (2 * (num_ranks - 1))
|
||||
)
|
||||
elif 'gather' in self.comm_type:
|
||||
self._cost = comm_volume / (BANDWIDTH * num_ranks / (num_ranks - 1))
|
||||
elif 'broadcast' in self.comm_type:
|
||||
self._cost = comm_volume / BANDWIDTH
|
||||
elif 'send' in self.comm_type or 'recv' in self.comm_type:
|
||||
self._cost = comm_volume / BANDWIDTH
|
||||
else:
|
||||
self._cost = 0
|
||||
|
||||
|
||||
class TensorCostNode(CostNode):
|
||||
def __init__(
|
||||
self,
|
||||
node,
|
||||
node_type,
|
||||
id=None,
|
||||
base_node_list=None,
|
||||
batch_size=None,
|
||||
shared_node_id=None,
|
||||
):
|
||||
super().__init__(node, node_type, id)
|
||||
if node.name == "create_py_reader_0" or node.name == "double_buffer_0":
|
||||
self.shape = [2, 2]
|
||||
self.dtype = paddle.float32
|
||||
else:
|
||||
self.shape = node.shape
|
||||
self.dtype = node.dtype
|
||||
self.dtype_factor = 1
|
||||
self.persistable = None
|
||||
self.shared_node_id = shared_node_id
|
||||
if self.dtype == paddle.float32 or node.dtype == paddle.int32:
|
||||
self.dtype_factor *= 4
|
||||
elif node.dtype == paddle.int64:
|
||||
self.dtype_factor *= 8
|
||||
elif node.dtype == paddle.uint8:
|
||||
self.dtype_factor = 1
|
||||
else:
|
||||
self.dtype_factor = 2
|
||||
# raise NotImplementedError("{} not counted".format(node.dtype))
|
||||
self.batch_size = None
|
||||
if batch_size is not None:
|
||||
self.batch_size = batch_size
|
||||
|
||||
def get_size(self):
|
||||
p = 1
|
||||
for i in self.node.shape:
|
||||
if i == -1: # deal with placeholder
|
||||
assert self.batch_size is not None, "Batch size not decided."
|
||||
i = self.batch_size
|
||||
p *= i
|
||||
return p
|
||||
|
||||
|
||||
class CompOpCostNode(CostNode):
|
||||
def __init__(self, node, node_type, id=None, is_bwd=False, is_optim=False):
|
||||
super().__init__(node, node_type, id)
|
||||
self.is_bwd = is_bwd
|
||||
self.is_optim = is_optim
|
||||
|
||||
def init_comp_cost(self, cost_data):
|
||||
# TODO: improve base.CostModel for more specific cost_data
|
||||
op_id = self.node.desc.id()
|
||||
if op_id in cost_data.keys():
|
||||
self.cost = cost_data[op_id]
|
||||
else:
|
||||
self.cost = 0.0
|
||||
|
||||
|
||||
class PipeEvent:
|
||||
def __init__(self, stage_id, event_name, duration, start_time=-1):
|
||||
self.stage_id = stage_id
|
||||
self.name = event_name
|
||||
self.duration = duration
|
||||
self.s_time = start_time
|
||||
self.e_time = -1
|
||||
|
||||
|
||||
class CostModel:
|
||||
def __init__(
|
||||
self,
|
||||
mode=CostModelMode.BENCHMARKING,
|
||||
cluster=None,
|
||||
batch_size=1,
|
||||
microbatch_num=1,
|
||||
opcall_overhead=0,
|
||||
standalone_cost_data=None,
|
||||
pipeline_config=None,
|
||||
):
|
||||
self.mode = mode
|
||||
|
||||
# parameters
|
||||
self.opcall_overhead = opcall_overhead
|
||||
self.batch_size = batch_size
|
||||
self.microbatch_num = microbatch_num
|
||||
|
||||
self.nodes = {} # name -> node
|
||||
|
||||
self.origin_graph = {} # original graph
|
||||
self.op_graph = {} # op graph (no variables nodes)
|
||||
self.runtime_graph = {} # runtime graph, for simulation
|
||||
|
||||
self.cluster = cluster
|
||||
self.cost_data = standalone_cost_data
|
||||
self.pp2rank = pipeline_config
|
||||
if self.pp2rank is not None:
|
||||
self.rank2pp = {}
|
||||
for stage_idx, ranks in enumerate(self.pp2rank):
|
||||
for rank in ranks:
|
||||
self.rank2pp[rank] = stage_idx
|
||||
else:
|
||||
self.rank2pp = None
|
||||
|
||||
self.ring2rank = {}
|
||||
|
||||
self.fwd_time = []
|
||||
self.bwd_time = []
|
||||
self.optim_time = []
|
||||
|
||||
def _parse_sub_program(self, program, nodes, graph, cost_data, sub_idx):
|
||||
assert len(program.blocks) == 1, (
|
||||
"Program more than 1 block not supported."
|
||||
)
|
||||
block = program.blocks[0]
|
||||
|
||||
var_id = "lod_tensor_blocking_queue_0"
|
||||
new_var = program.global_block().create_var(
|
||||
name=var_id,
|
||||
dtype=paddle.float32,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
)
|
||||
nodes[var_id] = TensorCostNode(
|
||||
new_var, CostNodeType.VARIABLE, "lod_tensor_blocking_queue_0"
|
||||
)
|
||||
for var in block.vars.values():
|
||||
var_id = var.name
|
||||
# if var.name == "create_py_reader_0" or var.name == "double_buffer_0":
|
||||
# continue
|
||||
nodes[var_id] = TensorCostNode(var, CostNodeType.VARIABLE, var_id)
|
||||
graph[var_id] = [[], []]
|
||||
|
||||
for op in block.ops:
|
||||
op_id = op.type + "_" + str(op.idx)
|
||||
if (
|
||||
op.type.startswith('c_')
|
||||
or op.type.startswith('send')
|
||||
or op.type.startswith('recv')
|
||||
):
|
||||
is_bwd = False
|
||||
if (
|
||||
op.type.startswith('c_')
|
||||
and op.type != "c_sync_calc_stream"
|
||||
and not op.type.startswith('c_embedding')
|
||||
):
|
||||
ring_id = op.attr('ring_id')
|
||||
if ring_id not in self.ring2rank:
|
||||
self.ring2rank[ring_id] = set()
|
||||
self.ring2rank[ring_id].add(sub_idx)
|
||||
is_bwd = '@GRAD' in op.output('Out')[0]
|
||||
elif op.type.startswith('recv'):
|
||||
is_bwd = '@GRAD' in op.output('Out')[0]
|
||||
elif op.type.startswith('send'):
|
||||
is_bwd = '@GRAD' in op.input('X')[0]
|
||||
op_node = CommOpCostNode(
|
||||
op, CostNodeType.COMMUNICATION, op_id, is_bwd
|
||||
)
|
||||
else:
|
||||
is_bwd = (
|
||||
int(op.attr('op_role')) == int(OpRole.Backward)
|
||||
) or "@GRAD" in op.input_arg_names
|
||||
is_optim = 'LearningRate' in op.input_names
|
||||
op_node = CompOpCostNode(
|
||||
op, CostNodeType.COMPUTATION, op_id, is_bwd, is_optim
|
||||
)
|
||||
op_node.init_comp_cost(cost_data)
|
||||
|
||||
nodes[op_id] = op_node
|
||||
graph[op_id] = [[], []]
|
||||
|
||||
comm_input_shape = [0]
|
||||
comm_output_shape = [0]
|
||||
for i in range(len(op.input_names)):
|
||||
try:
|
||||
var_id = op.input(op.input_names[i])[0]
|
||||
var_node = nodes[var_id]
|
||||
graph[op_id][PRED].append(var_node.id)
|
||||
graph[var_id][SUCC].append(op_node.id)
|
||||
comm_input_shape = var_node.shape
|
||||
except:
|
||||
continue
|
||||
|
||||
for i in range(len(op.output_names)):
|
||||
try:
|
||||
var_id = op.output(op.output_names[i])[0]
|
||||
var_node = nodes[var_id]
|
||||
graph[op_id][SUCC].append(var_node.id)
|
||||
graph[var_id][PRED].append(op_node.id)
|
||||
comm_output_shape = var_node.shape
|
||||
except:
|
||||
continue
|
||||
if op_node.type == CostNodeType.COMMUNICATION:
|
||||
op_node.set_shapes(comm_input_shape, comm_output_shape)
|
||||
|
||||
# resolve hazard: rename the r/w hazard variable nodes to ensure self.origin_graph is a DAG
|
||||
new_var_dict = {}
|
||||
for node_id, node in nodes.items():
|
||||
if node.type == CostNodeType.VARIABLE and node.node.persistable:
|
||||
write_op_cnt = 0
|
||||
for pred_id in graph[node_id][PRED]:
|
||||
pred = nodes[pred_id]
|
||||
if pred.type == CostNodeType.COMPUTATION and (
|
||||
pred_id in graph[node_id][SUCC]
|
||||
):
|
||||
graph[pred_id][SUCC].remove(node_id)
|
||||
graph[node_id][PRED].remove(pred_id)
|
||||
|
||||
write_op_cnt += 1
|
||||
new_var_id = node_id + f'_write_{write_op_cnt}'
|
||||
new_var = TensorCostNode(
|
||||
node.node,
|
||||
CostNodeType.VARIABLE,
|
||||
new_var_id,
|
||||
shared_node_id=node_id,
|
||||
)
|
||||
|
||||
graph[new_var_id] = [[], []]
|
||||
graph[pred_id][SUCC].append(new_var_id)
|
||||
graph[new_var_id][PRED].append(pred_id)
|
||||
|
||||
new_var_dict[new_var_id] = new_var
|
||||
for k, v in new_var_dict.items():
|
||||
nodes[k] = v
|
||||
return nodes
|
||||
|
||||
def parse_program(self, distributed_program):
|
||||
self.distributed_program = distributed_program
|
||||
self.total_rank = len(self.distributed_program)
|
||||
sub_prog_cnt = len(distributed_program)
|
||||
self.nodes = [] * sub_prog_cnt
|
||||
self.origin_graph = [] * sub_prog_cnt # original graph
|
||||
self.op_graph = [] * sub_prog_cnt # op graph (no variables nodes)
|
||||
self.runtime_graph = [] * sub_prog_cnt # runtime graph, for simulation
|
||||
|
||||
for sub_idx, sub_prog in enumerate(distributed_program):
|
||||
self.nodes.append({})
|
||||
self.origin_graph.append({})
|
||||
self.op_graph.append({})
|
||||
self.runtime_graph.append({})
|
||||
self._parse_sub_program(
|
||||
sub_prog,
|
||||
self.nodes[sub_idx],
|
||||
self.origin_graph[sub_idx],
|
||||
self.cost_data[
|
||||
0 if self.rank2pp is None else self.rank2pp[sub_idx]
|
||||
],
|
||||
sub_idx,
|
||||
)
|
||||
return self.nodes
|
||||
|
||||
def _find_succ_op(self, node_id, sub_idx=0):
|
||||
succ_ops_id = []
|
||||
for succ_id in self.origin_graph[sub_idx][node_id][SUCC]:
|
||||
succ = self.nodes[sub_idx][succ_id]
|
||||
if (
|
||||
succ.type == CostNodeType.COMMUNICATION
|
||||
or succ.type == CostNodeType.COMPUTATION
|
||||
):
|
||||
succ_ops_id.append(succ_id)
|
||||
elif succ.type == CostNodeType.VARIABLE:
|
||||
succ_ops_id = succ_ops_id + self._find_succ_op(succ_id, sub_idx)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'This type of node not supported yet:{succ.type}'
|
||||
)
|
||||
return succ_ops_id
|
||||
|
||||
def build_op_graph(self):
|
||||
for sub_idx in range(self.total_rank):
|
||||
op_nodes_id = []
|
||||
for node_id, node in self.nodes[sub_idx].items():
|
||||
if node.type == CostNodeType.VARIABLE:
|
||||
continue
|
||||
self.op_graph[sub_idx][node_id] = [[], []]
|
||||
op_nodes_id.append(node_id)
|
||||
for op_id in op_nodes_id:
|
||||
succ_nodes_id = self._find_succ_op(op_id, sub_idx)
|
||||
|
||||
self.op_graph[sub_idx][op_id][SUCC] = succ_nodes_id
|
||||
for succ_id in succ_nodes_id:
|
||||
self.op_graph[sub_idx][succ_id][PRED].append(op_id)
|
||||
|
||||
def build_runtime_graph(self):
|
||||
self.runtime_graph = copy.deepcopy(self.op_graph)
|
||||
|
||||
def eliminate_multi_edges(self, graph=None):
|
||||
for node_id, edges in graph.items():
|
||||
graph[node_id][PRED] = list(set(edges[PRED]))
|
||||
graph[node_id][SUCC] = list(set(edges[SUCC]))
|
||||
|
||||
def merge_comm(self):
|
||||
for sub_idx in range(self.total_rank):
|
||||
for node_id, edges in self.op_graph[sub_idx].items():
|
||||
node = self.nodes[sub_idx][node_id]
|
||||
if (
|
||||
node_id.startswith('c_')
|
||||
and not node.id.startswith("c_sync_calc_stream")
|
||||
and not node.id.startswith('c_embedding')
|
||||
):
|
||||
ring_id = node.node.attr('ring_id')
|
||||
node.set_ranks(list(self.ring2rank[ring_id]))
|
||||
node.init_comm_cost(self.cluster)
|
||||
elif node_id.startswith('send') or node_id.startswith('recv'):
|
||||
peer_rank = node.node.attr('peer')
|
||||
node.set_ranks([sub_idx, peer_rank])
|
||||
node.init_comm_cost(self.cluster)
|
||||
else:
|
||||
pass # Not communication op
|
||||
|
||||
def _merge_node(self, to_merge_node_list, merge_type='linear', nodes=None):
|
||||
nodes_list = []
|
||||
node_cost = 0
|
||||
for node in to_merge_node_list:
|
||||
if isinstance(node, MergedOpsCostNode):
|
||||
nodes_list += node.node_list
|
||||
else:
|
||||
nodes_list.append(node.id)
|
||||
if merge_type == 'linear':
|
||||
node_cost += node.cost
|
||||
elif merge_type == 'branch':
|
||||
node_cost = max(node_cost, node.cost)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'This type of merging is not supported:{merge_type}'
|
||||
)
|
||||
merged_node_id = 'merged_' + str(len(nodes))
|
||||
is_bwd = to_merge_node_list[0].is_bwd
|
||||
merged_node = MergedOpsCostNode(
|
||||
CostNodeType.MERGED,
|
||||
id=merged_node_id,
|
||||
base_node_list=nodes_list,
|
||||
is_bwd=is_bwd,
|
||||
)
|
||||
merged_node.cost = node_cost
|
||||
return merged_node_id, merged_node
|
||||
|
||||
def merge_linear(self):
|
||||
r'''
|
||||
This method does the following:
|
||||
If X depends on Y only, they must be run sequentially.
|
||||
[ e.g. A ->- C ->- D D and E depends on C only.]
|
||||
[ B ->-/ \->- E C depends on A and B. ]
|
||||
We merge X and Y into a new node and sum up their cost time.
|
||||
'''
|
||||
cnt = 0
|
||||
for sub_idx in range(self.total_rank):
|
||||
cnt += self._merge_linear(
|
||||
self.nodes[sub_idx], self.runtime_graph[sub_idx], is_bwd=False
|
||||
)
|
||||
cnt += self._merge_linear(
|
||||
self.nodes[sub_idx], self.runtime_graph[sub_idx], is_bwd=True
|
||||
)
|
||||
return cnt
|
||||
|
||||
def merge_branch(self):
|
||||
r'''
|
||||
This method does the following:
|
||||
If a node has more than one successor, there is *branch*.
|
||||
[ e.g. A ->- B ->- D ]
|
||||
[ \->- C ->- / , B and C can be run at the same time ]
|
||||
case 1: if B or C is null (or D is directly dependent on A),
|
||||
it's equivalent to A->C->D or A->B->D, fall back to self.merge_linear
|
||||
case 2: if both B and C are some op,
|
||||
merged_cost = max(cost(B), cost(C))
|
||||
'''
|
||||
cnt = 0
|
||||
for sub_idx in range(self.total_rank):
|
||||
cnt += self._merge_branch(
|
||||
self.nodes[sub_idx], self.runtime_graph[sub_idx], is_bwd=False
|
||||
)
|
||||
cnt += self._merge_branch(
|
||||
self.nodes[sub_idx], self.runtime_graph[sub_idx], is_bwd=True
|
||||
)
|
||||
return cnt
|
||||
|
||||
def _merge_linear(self, nodes, runtime_graph, is_bwd=False):
|
||||
reduct_cnt = 0
|
||||
rt_nodes_id = list(runtime_graph.keys())
|
||||
for node_id in rt_nodes_id:
|
||||
if node_id not in runtime_graph.keys():
|
||||
continue
|
||||
node = nodes[node_id]
|
||||
if not is_bwd == node.is_bwd or node.is_optim:
|
||||
continue
|
||||
edges = runtime_graph[node_id]
|
||||
ind = len(edges[PRED]) # in_degree
|
||||
if ind == 1: # only depend on one node
|
||||
pred_id = edges[PRED][0]
|
||||
pred = nodes[pred_id]
|
||||
merged_node_id, merged_node = self._merge_node(
|
||||
[node, pred], merge_type='linear', nodes=nodes
|
||||
)
|
||||
nodes[merged_node_id] = merged_node
|
||||
runtime_graph[merged_node_id] = [[], []]
|
||||
|
||||
# delete edges and add new edges
|
||||
succ = None
|
||||
try:
|
||||
runtime_graph[merged_node_id][SUCC] = copy.deepcopy(
|
||||
edges[SUCC]
|
||||
)
|
||||
|
||||
if len(runtime_graph[pred_id][SUCC]) > 1:
|
||||
# predecessor has more than 1 successor
|
||||
# the merged_node is to inherit the rest of its successors
|
||||
succ = runtime_graph[pred_id][SUCC]
|
||||
succ.remove(node_id)
|
||||
runtime_graph[merged_node_id][SUCC] += succ
|
||||
runtime_graph[merged_node_id][PRED] = runtime_graph[
|
||||
pred_id
|
||||
][PRED]
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for i in runtime_graph[pred_id][PRED]:
|
||||
try:
|
||||
runtime_graph[i][SUCC].remove(pred_id)
|
||||
except:
|
||||
continue
|
||||
runtime_graph[i][SUCC].append(merged_node_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
for i in edges[SUCC]:
|
||||
runtime_graph[i][PRED].remove(node_id)
|
||||
runtime_graph[i][PRED].append(merged_node_id)
|
||||
except:
|
||||
pass
|
||||
if succ is not None:
|
||||
for i in succ:
|
||||
try:
|
||||
runtime_graph[i][PRED].remove(pred_id)
|
||||
except:
|
||||
continue
|
||||
runtime_graph[i][PRED].append(merged_node_id)
|
||||
|
||||
runtime_graph.pop(node_id)
|
||||
try:
|
||||
runtime_graph.pop(pred_id)
|
||||
except:
|
||||
continue
|
||||
reduct_cnt += 1
|
||||
self.eliminate_multi_edges(runtime_graph)
|
||||
break
|
||||
return reduct_cnt # the number of nodes that have been reduced
|
||||
|
||||
def _merge_branch(self, nodes, runtime_graph, is_bwd=False):
|
||||
reduct_cnt = 0
|
||||
rt_nodes_id = list(runtime_graph.keys())
|
||||
for node_id in rt_nodes_id:
|
||||
node = nodes[node_id]
|
||||
if not is_bwd == node.is_bwd or node.is_optim:
|
||||
continue
|
||||
edges = runtime_graph[node_id]
|
||||
outd = len(edges[SUCC]) # out_degree
|
||||
if outd > 1: # branch out
|
||||
succ_nodes_id = edges[SUCC]
|
||||
|
||||
succ_to_elim = []
|
||||
for succ_id in succ_nodes_id:
|
||||
for succ_2_id in succ_nodes_id:
|
||||
try:
|
||||
tmp = runtime_graph[succ_2_id][SUCC]
|
||||
except:
|
||||
continue
|
||||
if succ_id in tmp:
|
||||
succ_to_elim.append(succ_id)
|
||||
break
|
||||
for id in succ_to_elim:
|
||||
edges[SUCC].remove(id)
|
||||
runtime_graph[id][PRED].remove(node_id)
|
||||
reduct_cnt += 1
|
||||
|
||||
to_merge = True
|
||||
try:
|
||||
if (
|
||||
len(edges[SUCC]) < 1
|
||||
or len(runtime_graph[edges[SUCC][0]][SUCC]) < 1
|
||||
):
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
end_node_id = runtime_graph[edges[SUCC][0]][SUCC][0]
|
||||
for i in succ_nodes_id:
|
||||
try:
|
||||
if (
|
||||
len(runtime_graph[i][SUCC]) != 1
|
||||
or runtime_graph[i][SUCC][0] != end_node_id
|
||||
):
|
||||
to_merge = False # if branches has different end node, we don't merge them
|
||||
break
|
||||
except:
|
||||
continue
|
||||
if to_merge and len(succ_nodes_id) > 1:
|
||||
to_merge_node_list = [nodes[i] for i in succ_nodes_id]
|
||||
merged_node_id, merged_node = self._merge_node(
|
||||
to_merge_node_list, merge_type='branch', nodes=nodes
|
||||
)
|
||||
nodes[merged_node_id] = merged_node
|
||||
runtime_graph[merged_node_id] = [[], []]
|
||||
|
||||
# delete edges and add new edges
|
||||
runtime_graph[merged_node_id][SUCC] = [end_node_id]
|
||||
runtime_graph[merged_node_id][PRED] = edges[PRED]
|
||||
|
||||
runtime_graph[end_node_id][PRED] = [merged_node_id]
|
||||
runtime_graph[node_id][SUCC] = [merged_node_id]
|
||||
|
||||
try:
|
||||
for i in succ_nodes_id:
|
||||
runtime_graph.pop(i)
|
||||
reduct_cnt += len(to_merge_node_list) - 1
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return reduct_cnt
|
||||
|
||||
def get_runtime_cost(self):
|
||||
def get_node_cost(node):
|
||||
node_cost = node.cost + self.opcall_overhead
|
||||
if isinstance(node, MergedOpsCostNode):
|
||||
for it in node.node_list:
|
||||
node_cost += self.opcall_overhead
|
||||
return node_cost
|
||||
|
||||
for sub_idx in range(self.total_rank):
|
||||
fwd_cost = 0
|
||||
bwd_cost = 0
|
||||
optim_cost = 0
|
||||
for node_id in self.runtime_graph[sub_idx].keys():
|
||||
node = self.nodes[sub_idx][node_id]
|
||||
if node.is_optim:
|
||||
optim_cost += get_node_cost(node)
|
||||
elif node.is_bwd:
|
||||
bwd_cost += get_node_cost(node)
|
||||
else:
|
||||
fwd_cost += get_node_cost(node)
|
||||
self.fwd_time.append(fwd_cost)
|
||||
self.bwd_time.append(bwd_cost)
|
||||
self.optim_time.append(optim_cost)
|
||||
return self.fwd_time, self.bwd_time, self.optim_time
|
||||
|
||||
def get_mem(self):
|
||||
static_list = []
|
||||
top_list = []
|
||||
for sub_idx in range(self.total_rank):
|
||||
static_mem, cur_mem, top_mem = self._simulate_mem(
|
||||
self.nodes[sub_idx], self.origin_graph[sub_idx]
|
||||
)
|
||||
static_list.append(static_mem)
|
||||
top_list.append(top_mem)
|
||||
return static_list, top_list
|
||||
|
||||
def _simulate_mem(self, nodes, origin_graph):
|
||||
q = queue.Queue(1024)
|
||||
sim_graph = copy.deepcopy(origin_graph)
|
||||
for node_id, node in nodes.items():
|
||||
if len(sim_graph[node_id][PRED]) == 0:
|
||||
q.put(node_id)
|
||||
|
||||
q.put('nop')
|
||||
cur_mem = 0
|
||||
top_mem = -1
|
||||
static_mem = 0
|
||||
while not q.empty():
|
||||
node_id = q.get()
|
||||
node = None
|
||||
size = 0
|
||||
if node_id == 'nop':
|
||||
top_mem = max(cur_mem, top_mem)
|
||||
if q.empty():
|
||||
break
|
||||
else:
|
||||
q.put(node_id)
|
||||
continue
|
||||
else:
|
||||
node = nodes[node_id]
|
||||
if node.type == CostNodeType.VARIABLE:
|
||||
size = node.get_size()
|
||||
if node.node.persistable:
|
||||
static_mem += size
|
||||
cur_mem += size
|
||||
edges = sim_graph[node_id]
|
||||
if not (
|
||||
node.type == CostNodeType.VARIABLE and node.node.persistable
|
||||
):
|
||||
for succ_id in edges[SUCC]:
|
||||
sim_graph[succ_id][PRED].remove(node_id)
|
||||
if len(sim_graph[succ_id][PRED]) == 0:
|
||||
q.put(succ_id)
|
||||
for pred_id in edges[PRED]:
|
||||
pred = nodes
|
||||
if pred.type == CostNodeType.VARIABLE:
|
||||
sim_graph[pred_id][SUCC].remove(node_id)
|
||||
if (
|
||||
len(sim_graph[pred_id][SUCC]) == 0
|
||||
and not pred.node.persistable
|
||||
):
|
||||
cur_mem -= pred.get_size()
|
||||
return static_mem, cur_mem, top_mem
|
||||
|
||||
def get_pipeline_time(self):
|
||||
if self.pp2rank is None:
|
||||
return self.fwd_time[0] + self.bwd_time[0] + self.optim_time[0]
|
||||
else:
|
||||
return self._simulate_pipeline()
|
||||
|
||||
def _simulate_pipeline(self):
|
||||
stage_num = len(self.pp2rank)
|
||||
event_list = []
|
||||
global_time = [0] * stage_num
|
||||
total_time = 0
|
||||
fwd_cnt = list(range(stage_num, 0, -1))
|
||||
bwd_cnt = [self.microbatch_num] * stage_num
|
||||
q = queue.Queue(1024)
|
||||
|
||||
for i in range(self.microbatch_num):
|
||||
q.put(PipeEvent(0, 'fwd', self.fwd_time[0]))
|
||||
|
||||
while not q.empty():
|
||||
e = q.get()
|
||||
stid = e.stage_id
|
||||
if e.name == 'fwd':
|
||||
if fwd_cnt[stid] > 0:
|
||||
e.s_time = max(global_time[stid], e.s_time)
|
||||
e.e_time = e.s_time + e.duration
|
||||
event_list.append(e)
|
||||
if stid != stage_num - 1:
|
||||
q.put(
|
||||
PipeEvent(
|
||||
stid + 1,
|
||||
'fwd',
|
||||
self.fwd_time[stid + 1],
|
||||
start_time=e.e_time,
|
||||
)
|
||||
)
|
||||
else:
|
||||
q.put(
|
||||
PipeEvent(
|
||||
stid,
|
||||
'bwd',
|
||||
self.bwd_time[stid],
|
||||
start_time=e.e_time,
|
||||
)
|
||||
)
|
||||
fwd_cnt[stid] -= 1
|
||||
global_time[stid] = e.e_time
|
||||
else:
|
||||
q.put(e)
|
||||
elif e.name == 'bwd':
|
||||
e.s_time = max(global_time[stid], e.s_time)
|
||||
e.e_time = e.s_time + e.duration
|
||||
event_list.append(e)
|
||||
if stid != 0:
|
||||
q.put(
|
||||
PipeEvent(
|
||||
stid - 1,
|
||||
'bwd',
|
||||
self.bwd_time[stid - 1],
|
||||
start_time=e.e_time,
|
||||
)
|
||||
)
|
||||
fwd_cnt[stid] += 1
|
||||
bwd_cnt[stid] -= 1
|
||||
if bwd_cnt[stid] == 0:
|
||||
q.put(
|
||||
PipeEvent(
|
||||
stid,
|
||||
'optim',
|
||||
self.optim_time[stid],
|
||||
start_time=e.e_time,
|
||||
)
|
||||
)
|
||||
global_time[stid] = e.e_time
|
||||
elif e.name == 'optim':
|
||||
e.s_time = max(global_time[stid], e.s_time)
|
||||
e.e_time = e.s_time + e.duration
|
||||
event_list.append(e)
|
||||
global_time[stid] = e.e_time
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'This type of pipe event is not supported yet.{e.name}'
|
||||
)
|
||||
|
||||
for t in global_time:
|
||||
total_time = max(total_time, t)
|
||||
return total_time
|
||||
|
||||
def get_cost(self):
|
||||
cost = Cost()
|
||||
static_mem, peak_mem = self.get_mem()
|
||||
cost.static_mem = static_mem
|
||||
cost.peak_mem = peak_mem
|
||||
self.merge_comm()
|
||||
while True:
|
||||
cnt = 0
|
||||
cnt += self.merge_linear()
|
||||
cnt += self.merge_branch()
|
||||
if cnt == 0: # can't be further merged
|
||||
break
|
||||
self.get_runtime_cost()
|
||||
cost.runtime = self.get_pipeline_time()
|
||||
return cost
|
||||
|
||||
def init(self, distributed_program):
|
||||
self.parse_program(distributed_program)
|
||||
self.build_op_graph()
|
||||
for sub_idx in range(self.total_rank):
|
||||
self.eliminate_multi_edges(self.op_graph[sub_idx])
|
||||
self.build_runtime_graph()
|
||||
|
||||
|
||||
def estimate_cost(
|
||||
distributed_program,
|
||||
cluster,
|
||||
pipeline_config,
|
||||
standalone_cost_data,
|
||||
batch_size,
|
||||
):
|
||||
"""
|
||||
Estimated cost from distributed program, cluster model and distributed settings.
|
||||
|
||||
Args:
|
||||
distributed_program(list): list of paddle programs
|
||||
cluster(Cluster): cluster model
|
||||
standalone_cost_data(CostData): cost data given by paddle.core
|
||||
batch_size(int): batch size of the training workload
|
||||
pipeline_config(list): configuration of pipeline stage allocation
|
||||
"""
|
||||
# the following line is left for now, cluster model will be involved in the future
|
||||
assert cluster is None, "For now, cluster remains None"
|
||||
cm_ctx = CostModel(
|
||||
cluster=cluster,
|
||||
batch_size=batch_size,
|
||||
standalone_cost_data=standalone_cost_data,
|
||||
pipeline_config=pipeline_config,
|
||||
)
|
||||
cm_ctx.init(distributed_program)
|
||||
cost = cm_ctx.get_cost()
|
||||
return cost
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2021 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 paddle.base.core import ( # noqa: F401
|
||||
DistTensorSpec,
|
||||
OperatorDistAttr,
|
||||
TensorDistAttr,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
# 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.
|
||||
|
||||
import copy
|
||||
|
||||
from paddle.static import InputSpec
|
||||
|
||||
from ..placement_type import get_shard_spec
|
||||
from .utils import convert_to_dims_mapping
|
||||
|
||||
|
||||
class DistributedInputSpec(InputSpec):
|
||||
def __init__(
|
||||
self,
|
||||
shape,
|
||||
dtype='float32',
|
||||
name=None,
|
||||
stop_gradient=False,
|
||||
mesh=None,
|
||||
placements=None,
|
||||
local_shape=None,
|
||||
):
|
||||
super().__init__(shape, dtype, name, stop_gradient)
|
||||
self.mesh = copy.deepcopy(mesh)
|
||||
sharding_specs = get_shard_spec(mesh, placements, len(self.shape))
|
||||
self.dims_mapping = convert_to_dims_mapping(sharding_specs, mesh)
|
||||
self.local_shape = local_shape
|
||||
|
||||
@classmethod
|
||||
def from_dtensor(cls, dtensor, name=None, shape=None):
|
||||
"""
|
||||
Generates a DistributedInputSpec based on dist tensor.
|
||||
|
||||
Args:
|
||||
dtensor: the dist tensor.
|
||||
|
||||
Returns:
|
||||
A DistributedInputSpec instance generated from dtensor.
|
||||
"""
|
||||
return cls(
|
||||
shape=dtensor.shape if shape is None else shape,
|
||||
dtype=dtensor.dtype,
|
||||
name=name,
|
||||
stop_gradient=dtensor.stop_gradient,
|
||||
mesh=dtensor.process_mesh,
|
||||
placements=dtensor.placements,
|
||||
local_shape=dtensor._local_value().shape,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{super().__repr__()}, mesh:{self.mesh}, placements:{self.dims_mapping}"
|
||||
@@ -0,0 +1,290 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import abc
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.io import BatchSampler, IterableDataset
|
||||
from paddle.io.dataloader.batch_sampler import (
|
||||
DistributedBatchSampler,
|
||||
_InfiniteIterableSampler,
|
||||
)
|
||||
from paddle.io.dataloader.dataloader_iter import (
|
||||
_DatasetKind,
|
||||
default_collate_fn,
|
||||
default_convert_fn,
|
||||
)
|
||||
|
||||
|
||||
class DistributedDataLoaderBase(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def __iter__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DistributedDataLoaderFromGenerator(DistributedDataLoaderBase):
|
||||
def __init__(
|
||||
self,
|
||||
dataset,
|
||||
feed_list=None,
|
||||
capacity=None,
|
||||
use_double_buffer=True,
|
||||
iterable=True,
|
||||
return_list=False,
|
||||
use_multiprocess=False,
|
||||
drop_last=True,
|
||||
places=None,
|
||||
batch_size=1,
|
||||
epochs=1,
|
||||
steps_per_epoch=None,
|
||||
collate_fn=None,
|
||||
split_data=True,
|
||||
data_parallel_world_size=[],
|
||||
data_parallel_rank=[],
|
||||
acc_steps=1,
|
||||
):
|
||||
self.dataset = dataset
|
||||
self.feed_list = feed_list
|
||||
self.capacity = capacity
|
||||
self.use_double_buffer = use_double_buffer
|
||||
self.iterable = iterable
|
||||
self.return_list = return_list
|
||||
self.use_multiprocess = use_multiprocess
|
||||
self.drop_last = drop_last
|
||||
self.places = places
|
||||
self.batch_size = batch_size
|
||||
self.epochs = epochs
|
||||
self.steps_per_epoch = steps_per_epoch
|
||||
self.collate_fn = collate_fn
|
||||
self.split_data = split_data
|
||||
assert len(data_parallel_world_size) == len(feed_list)
|
||||
assert len(data_parallel_rank) == len(feed_list)
|
||||
self.dp_world_sizes = data_parallel_world_size
|
||||
self.dp_ranks = data_parallel_rank
|
||||
self.acc_steps = acc_steps
|
||||
|
||||
if isinstance(dataset, IterableDataset):
|
||||
self.dataset_kind = _DatasetKind.ITER
|
||||
else:
|
||||
self.dataset_kind = _DatasetKind.MAP
|
||||
|
||||
if self.batch_size is None:
|
||||
self.batch_sampler = None
|
||||
else:
|
||||
if isinstance(dataset, IterableDataset):
|
||||
self.batch_sampler = _InfiniteIterableSampler(
|
||||
dataset, batch_size
|
||||
)
|
||||
else:
|
||||
self.batch_sampler = BatchSampler(
|
||||
dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=False,
|
||||
drop_last=drop_last,
|
||||
)
|
||||
|
||||
self.auto_collate_batch = self.batch_sampler is not None
|
||||
self.sampler_iter = iter(self.index_sampler)
|
||||
|
||||
if self.auto_collate_batch:
|
||||
self.collate_fn = collate_fn or default_collate_fn
|
||||
else:
|
||||
self.collate_fn = collate_fn or default_convert_fn
|
||||
|
||||
self.dataset_fetcher = _DatasetKind.create_fetcher(
|
||||
self.dataset_kind,
|
||||
self.dataset,
|
||||
self.auto_collate_batch,
|
||||
self.collate_fn,
|
||||
self.drop_last,
|
||||
)
|
||||
|
||||
self._steps = self._infer_steps()
|
||||
self._inner_dataloader = self._create_inner_dataloader()
|
||||
|
||||
def __iter__(self):
|
||||
self._cur_step = 0
|
||||
self._inner_dataloader.start()
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if not self._steps:
|
||||
self._cur_step += 1
|
||||
return None
|
||||
elif self._cur_step < self._steps:
|
||||
self._cur_step += 1
|
||||
return None
|
||||
else:
|
||||
self._inner_dataloader.reset()
|
||||
self.sampler_iter = iter(self.index_sampler)
|
||||
raise StopIteration
|
||||
|
||||
def _infer_steps(self):
|
||||
if isinstance(self.steps_per_epoch, int) and self.steps_per_epoch > 0:
|
||||
return self.steps_per_epoch
|
||||
try:
|
||||
if isinstance(self.dataset, IterableDataset):
|
||||
steps_per_epoch = None
|
||||
elif self.batch_size is None:
|
||||
steps_per_epoch = len(self.dataset) // self.acc_steps
|
||||
else:
|
||||
steps_per_epoch = (
|
||||
len(self.dataset) // self.batch_size // self.acc_steps
|
||||
)
|
||||
except:
|
||||
raise ValueError(
|
||||
"Please set `steps_per_epoch` or implement `__len__` method in dataset class."
|
||||
)
|
||||
return steps_per_epoch
|
||||
|
||||
@property
|
||||
def index_sampler(self):
|
||||
if self.auto_collate_batch:
|
||||
return self.batch_sampler
|
||||
else:
|
||||
if self.dataset_kind == _DatasetKind.MAP:
|
||||
return list(range(len(self.dataset)))
|
||||
else:
|
||||
return _InfiniteIterableSampler(self.dataset, 1)
|
||||
|
||||
def _create_inner_dataloader(self):
|
||||
def data_generator():
|
||||
while True:
|
||||
try:
|
||||
indices = next(self.sampler_iter)
|
||||
batch = self.dataset_fetcher.fetch(indices)
|
||||
if batch is None:
|
||||
break
|
||||
except StopIteration:
|
||||
self.dataset_fetcher = _DatasetKind.create_fetcher(
|
||||
self.dataset_kind,
|
||||
self.dataset,
|
||||
self.auto_collate_batch,
|
||||
self.collate_fn,
|
||||
self.drop_last,
|
||||
)
|
||||
break
|
||||
|
||||
partial_data = []
|
||||
for i, d in enumerate(batch):
|
||||
array = np.array(d)
|
||||
if not self.split_data:
|
||||
partial_data.append(array)
|
||||
continue
|
||||
|
||||
batch_size = array.shape[0]
|
||||
assert batch_size % self.dp_world_sizes[i] == 0, (
|
||||
f"batch_size [{batch_size}] is not divisible by dp_world_size [{self.dp_world_sizes[i]}]"
|
||||
)
|
||||
partial_data.append(
|
||||
np.split(array, self.dp_world_sizes[i])[
|
||||
self.dp_ranks[i]
|
||||
]
|
||||
)
|
||||
|
||||
yield partial_data
|
||||
|
||||
dataloader = paddle.base.io.DataLoader.from_generator(
|
||||
feed_list=self.feed_list,
|
||||
capacity=self.capacity,
|
||||
use_double_buffer=self.use_double_buffer,
|
||||
# iterable=self.iterable,
|
||||
iterable=False,
|
||||
return_list=self.return_list,
|
||||
use_multiprocess=self.use_multiprocess,
|
||||
drop_last=self.drop_last,
|
||||
)
|
||||
dataloader.set_batch_generator(data_generator, self.places)
|
||||
|
||||
return dataloader
|
||||
|
||||
|
||||
class DistributedDataLoader(DistributedDataLoaderBase):
|
||||
def __init__(
|
||||
self,
|
||||
dataset,
|
||||
feed_list=None,
|
||||
places=None,
|
||||
return_list=True,
|
||||
batch_size=1,
|
||||
shuffle=False,
|
||||
drop_last=False,
|
||||
collate_fn=None,
|
||||
num_workers=0,
|
||||
use_buffer_reader=True,
|
||||
use_shared_memory=True,
|
||||
timeout=0,
|
||||
worker_init_fn=None,
|
||||
epochs=1,
|
||||
steps_per_epoch=None,
|
||||
split_data=True,
|
||||
data_parallel_world_size=[],
|
||||
data_parallel_rank=[],
|
||||
):
|
||||
self.dataset = dataset
|
||||
self.feed_list = feed_list
|
||||
self.return_list = return_list
|
||||
self.places = places
|
||||
self.batch_size = batch_size
|
||||
self.shuffle = shuffle
|
||||
self.drop_last = drop_last
|
||||
self.collate_fn = collate_fn
|
||||
self.num_workers = num_workers
|
||||
self.use_buffer_reader = use_buffer_reader
|
||||
self.use_shared_memory = use_shared_memory
|
||||
self.timeout = timeout
|
||||
self.worker_init_fn = worker_init_fn
|
||||
self.epochs = epochs
|
||||
self.steps_per_epoch = steps_per_epoch
|
||||
self.dp_world_sizes = data_parallel_world_size
|
||||
self.dp_ranks = data_parallel_rank
|
||||
self.split_data = split_data
|
||||
|
||||
if self.batch_size is None:
|
||||
self.batch_sampler = None
|
||||
else:
|
||||
self.batch_sampler = DistributedBatchSampler(
|
||||
dataset=self.dataset,
|
||||
batch_size=self.batch_size,
|
||||
num_replicas=self.dp_world_sizes[0],
|
||||
rank=self.dp_ranks[0],
|
||||
shuffle=self.shuffle,
|
||||
drop_last=self.drop_last,
|
||||
)
|
||||
|
||||
self._dataloader = paddle.io.DataLoader(
|
||||
self.dataset,
|
||||
feed_list=self.feed_list,
|
||||
places=self.places,
|
||||
return_list=self.return_list,
|
||||
batch_sampler=self.batch_sampler,
|
||||
batch_size=1 if self.batch_sampler else self.batch_size,
|
||||
collate_fn=self.collate_fn,
|
||||
num_workers=self.num_workers,
|
||||
use_buffer_reader=self.use_buffer_reader,
|
||||
use_shared_memory=self.use_shared_memory,
|
||||
timeout=self.timeout,
|
||||
worker_init_fn=self.worker_init_fn,
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._dataloader)
|
||||
|
||||
def __iter__(self):
|
||||
return self._dataloader.__iter__()
|
||||
|
||||
def __call__(self):
|
||||
return self._dataloader.__iter__()
|
||||
@@ -0,0 +1,323 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import copy
|
||||
|
||||
import paddle
|
||||
from paddle.static import Variable
|
||||
|
||||
from .dist_attribute import OperatorDistAttr
|
||||
from .utils import (
|
||||
__no_shape_var_type__,
|
||||
convert_to_shard_spec,
|
||||
verify_shard_spec,
|
||||
)
|
||||
|
||||
|
||||
class DistributedOperator:
|
||||
def __init__(self, serial_op, dist_attr=None):
|
||||
self._serial_op = serial_op
|
||||
if dist_attr is not None and isinstance(dist_attr, OperatorDistAttr):
|
||||
# TODO: remove this deepcopy after we fix the issue
|
||||
self._dist_attr = copy.deepcopy(dist_attr)
|
||||
# self._dist_attr = dist_attr
|
||||
# TODO: Do we really need to write back to serial op?
|
||||
self._serial_op.dist_attr = dist_attr
|
||||
else:
|
||||
assert dist_attr is None, f"{dist_attr}"
|
||||
# Use the dist attr of serial_op to do the initialization
|
||||
self._dist_attr = self._serial_op.dist_attr
|
||||
self._serial_inputs = {}
|
||||
self._serial_outputs = {}
|
||||
|
||||
@property
|
||||
def serial_op(self):
|
||||
return self._serial_op
|
||||
|
||||
@property
|
||||
def dist_attr(self):
|
||||
return self._dist_attr
|
||||
|
||||
@dist_attr.setter
|
||||
def dist_attr(self, dist_attr):
|
||||
self._dist_attr = dist_attr
|
||||
# TODO: Do we really need to write back to serial op?
|
||||
self._serial_op.dist_attr = dist_attr
|
||||
|
||||
def get_serial_input(self, name):
|
||||
if self._serial_op.type == "create_py_reader":
|
||||
tensor = None
|
||||
elif self._serial_op.block._find_var_recursive(name) is not None:
|
||||
tensor = self._serial_op.block._var_recursive(name)
|
||||
else:
|
||||
tensor = None
|
||||
return tensor
|
||||
|
||||
def get_serial_output(self, name):
|
||||
tensor = self._serial_op.block._var_recursive(name)
|
||||
return tensor
|
||||
|
||||
def validate_dist_attr(self):
|
||||
if "read" in self.serial_op.type or "while" == self.serial_op.type:
|
||||
return True
|
||||
for name in self.serial_op.input_arg_names:
|
||||
input_dist_attr = self.dist_attr.get_input_dist_attr(name)
|
||||
dims_mapping = input_dist_attr.dims_mapping
|
||||
if self.get_serial_input(name).type in __no_shape_var_type__:
|
||||
shape = []
|
||||
else:
|
||||
shape = self.get_serial_input(name).shape
|
||||
if len(shape) != len(dims_mapping):
|
||||
return False
|
||||
for i in range(len(dims_mapping)):
|
||||
if dims_mapping[i] < -1 or dims_mapping[i] >= len(
|
||||
self.dist_attr.process_mesh.shape
|
||||
):
|
||||
return False
|
||||
for i in range(len(self.dist_attr.process_mesh.shape)):
|
||||
if dims_mapping.count(i) > 1:
|
||||
return False
|
||||
if self.dist_attr.process_mesh != input_dist_attr.process_mesh:
|
||||
return False
|
||||
|
||||
for name in self.serial_op.output_arg_names:
|
||||
output_dist_attr = self.dist_attr.get_output_dist_attr(name)
|
||||
dims_mapping = output_dist_attr.dims_mapping
|
||||
if self.get_serial_output(name).type in __no_shape_var_type__:
|
||||
shape = []
|
||||
else:
|
||||
shape = self.get_serial_output(name).shape
|
||||
if len(shape) != len(dims_mapping):
|
||||
return False
|
||||
for i in range(len(dims_mapping)):
|
||||
if dims_mapping[i] < -1 or dims_mapping[i] >= len(
|
||||
self.dist_attr.process_mesh.shape
|
||||
):
|
||||
return False
|
||||
for i in range(len(self.dist_attr.process_mesh.shape)):
|
||||
if dims_mapping.count(i) > 1:
|
||||
return False
|
||||
if self.dist_attr.process_mesh != output_dist_attr.process_mesh:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
str = f"{{op type: {self.serial_op.desc.type()}, op id: {self.serial_op.desc.id()}, op original_id: {self.serial_op.desc.original_id()}"
|
||||
|
||||
# str += ", {}".format(self.dist_attr)
|
||||
# return str
|
||||
|
||||
if self.dist_attr.is_annotated("process_mesh"):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
str += (
|
||||
f", process_mesh ({annotated_str}): {self.dist_attr.process_mesh}"
|
||||
)
|
||||
|
||||
str += f" , execution_stream: {self.dist_attr.execution_stream}"
|
||||
|
||||
for arg_name in self.serial_op.desc.input_arg_names():
|
||||
try:
|
||||
dims_mapping = self.dist_attr.get_input_dims_mapping(arg_name)
|
||||
except IndexError:
|
||||
raise IndexError(
|
||||
f"There is not input var '{arg_name}''s dist_attr in current op '{self.serial_op.desc.type()}'"
|
||||
)
|
||||
if self.dist_attr.is_annotated_input_dims_mapping(arg_name):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
if self.get_serial_input(arg_name) is not None:
|
||||
if self.get_serial_input(arg_name).is_parameter:
|
||||
is_parameter_str = "parameter"
|
||||
else:
|
||||
is_parameter_str = "non-parameter"
|
||||
else:
|
||||
is_parameter_str = "non-parameter"
|
||||
|
||||
# partial
|
||||
input_dist_attr = self.dist_attr.get_input_dist_attr(arg_name)
|
||||
partial_dims = sorted(input_dist_attr._partial_dims())
|
||||
|
||||
str += f"; {arg_name}'s dims_mapping (input, {annotated_str}, {is_parameter_str}): {dims_mapping}, partial on dims: {partial_dims}"
|
||||
|
||||
for arg_name in self.serial_op.desc.output_arg_names():
|
||||
try:
|
||||
dims_mapping = self.dist_attr.get_output_dims_mapping(arg_name)
|
||||
except IndexError:
|
||||
raise IndexError(
|
||||
f"There is not output var '{arg_name}''s dist_attr in current op '{self.serial_op.desc.type()}'"
|
||||
)
|
||||
if self.dist_attr.is_annotated_output_dims_mapping(arg_name):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
if self.get_serial_output(arg_name) is not None:
|
||||
if self.get_serial_output(arg_name).is_parameter:
|
||||
is_parameter_str = "parameter"
|
||||
else:
|
||||
is_parameter_str = "non-parameter"
|
||||
else:
|
||||
is_parameter_str = "non-parameter"
|
||||
|
||||
# partial
|
||||
output_dist_attr = self.dist_attr.get_output_dist_attr(arg_name)
|
||||
partial_dims = sorted(output_dist_attr._partial_dims())
|
||||
|
||||
str += f"; {arg_name}'s dims_mapping (output, {annotated_str}, {is_parameter_str}): {dims_mapping}, partial on dims: {partial_dims}"
|
||||
|
||||
str += f", dist_impl idx: {self.dist_attr.impl_idx} , dist_impl type: {self.dist_attr.impl_type}, chunk_id: {self.dist_attr.chunk_id} }}"
|
||||
|
||||
return str
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
memo[id(self)] = result
|
||||
for k, v in self.__dict__.items():
|
||||
if (
|
||||
k == "_serial_op"
|
||||
or k == "_serial_inputs"
|
||||
or k == "_serial_outputs"
|
||||
):
|
||||
setattr(result, k, v)
|
||||
else:
|
||||
setattr(result, k, copy.deepcopy(v, memo))
|
||||
return result
|
||||
|
||||
|
||||
class DistributedOperatorHelper:
|
||||
def __init__(
|
||||
self,
|
||||
serial_op,
|
||||
process_mesh,
|
||||
in_dims_mappings,
|
||||
out_dims_mappings,
|
||||
kwargs,
|
||||
):
|
||||
self._serial_op = serial_op
|
||||
self._process_mesh = process_mesh
|
||||
self._in_dims_mappings = in_dims_mappings
|
||||
self._out_dims_mappings = out_dims_mappings
|
||||
self._chunk_id = kwargs["chunk_id"] if "chunk_id" in kwargs else 0
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
tensor_to_dims_mapping = {}
|
||||
index = 0
|
||||
if self._in_dims_mappings:
|
||||
assert len(args) + len(kwargs) == len(self._in_dims_mappings), (
|
||||
f"The length of dims_mapping {len(self._in_dims_mappings)} does not matching the length output {len(args) + len(kwargs)}."
|
||||
)
|
||||
for arg in args:
|
||||
if isinstance(arg, Variable) and self._in_dims_mappings:
|
||||
tensor_to_dims_mapping[arg.name] = self._in_dims_mappings[index]
|
||||
index += 1
|
||||
for arg in kwargs.values() and self._in_dims_mappings:
|
||||
if isinstance(arg, Variable):
|
||||
tensor_to_dims_mapping[arg.name] = self._in_dims_mappings[index]
|
||||
index += 1
|
||||
|
||||
default_prog = paddle.static.default_main_program()
|
||||
cur_block = default_prog.current_block()
|
||||
op_size = len(cur_block.ops)
|
||||
if paddle.base.dygraph.base.in_to_static_mode():
|
||||
output = paddle.jit.dy2static.convert_call_func.convert_call(
|
||||
self._serial_op
|
||||
)(*args, **kwargs)
|
||||
else:
|
||||
output = self._serial_op(*args, **kwargs)
|
||||
new_op_size = len(cur_block.ops)
|
||||
|
||||
if isinstance(output, (tuple, list)):
|
||||
new_output = list(output)
|
||||
elif isinstance(output, Variable):
|
||||
new_output = [output]
|
||||
else:
|
||||
raise ValueError("Unrecognized output.")
|
||||
|
||||
if self._out_dims_mappings:
|
||||
assert len(new_output) == len(self._out_dims_mappings), (
|
||||
f"The length of dims_mapping {len(self._out_dims_mappings)} does not matching the length output {len(new_output)}."
|
||||
)
|
||||
for i, item in enumerate(new_output):
|
||||
if isinstance(item, Variable) and self._out_dims_mappings:
|
||||
tensor_to_dims_mapping[item.name] = self._out_dims_mappings[i]
|
||||
|
||||
from .dist_context import get_default_distributed_context
|
||||
|
||||
default_dist_ctx = get_default_distributed_context()
|
||||
for idx in range(op_size, new_op_size):
|
||||
op = cur_block.ops[idx]
|
||||
dist_op = DistributedOperator(op)
|
||||
for name in dist_op.serial_op.input_arg_names:
|
||||
if name in tensor_to_dims_mapping.keys():
|
||||
tensor = dist_op.get_serial_input(name)
|
||||
tensor_dist_attr = dist_op.dist_attr.get_input_dist_attr(
|
||||
name
|
||||
)
|
||||
dims_mapping = tensor_to_dims_mapping[name]
|
||||
if tensor is None:
|
||||
tensor_shape = []
|
||||
else:
|
||||
if tensor.type in __no_shape_var_type__:
|
||||
tensor_shape = []
|
||||
else:
|
||||
tensor_shape = tensor.shape
|
||||
if dims_mapping is not None:
|
||||
dims_mapping = tensor_to_dims_mapping[name]
|
||||
shard_spec = convert_to_shard_spec(
|
||||
dims_mapping, self._process_mesh
|
||||
)
|
||||
assert verify_shard_spec(
|
||||
shard_spec, tensor_shape, self._process_mesh
|
||||
), (
|
||||
f"For tensor {name}, shard_spec {shard_spec} is invalid with tensor_shape {tensor_shape} and process_mesh {self._process_mesh}."
|
||||
)
|
||||
tensor_dist_attr.dims_mapping = dims_mapping
|
||||
tensor_dist_attr.mark_annotated("dims_mapping")
|
||||
for name in dist_op.serial_op.output_arg_names:
|
||||
if name in tensor_to_dims_mapping.keys():
|
||||
tensor = dist_op.get_serial_output(name)
|
||||
tensor_dist_attr = dist_op.dist_attr.get_output_dist_attr(
|
||||
name
|
||||
)
|
||||
dims_mapping = tensor_to_dims_mapping[name]
|
||||
if tensor is None:
|
||||
tensor_shape = []
|
||||
else:
|
||||
if tensor.type in __no_shape_var_type__:
|
||||
tensor_shape = []
|
||||
else:
|
||||
tensor_shape = tensor.shape
|
||||
if dims_mapping is not None:
|
||||
dims_mapping = tensor_to_dims_mapping[name]
|
||||
shard_spec = convert_to_shard_spec(
|
||||
dims_mapping, self._process_mesh
|
||||
)
|
||||
assert verify_shard_spec(
|
||||
shard_spec, tensor_shape, self._process_mesh
|
||||
), (
|
||||
f"For tensor {name}, shard_spec {shard_spec} is invalid with tensor_shape {tensor_shape} and process_mesh {self._process_mesh}."
|
||||
)
|
||||
tensor_dist_attr.dims_mapping = dims_mapping
|
||||
tensor_dist_attr.mark_annotated("dims_mapping")
|
||||
dist_op.dist_attr.process_mesh = self._process_mesh
|
||||
dist_op.dist_attr.chunk_id = self._chunk_id
|
||||
if self._process_mesh is not None:
|
||||
dist_op.dist_attr.mark_annotated("process_mesh")
|
||||
default_dist_ctx.add_dist_op_for_program(dist_op)
|
||||
default_dist_ctx.add_process_mesh(self._process_mesh)
|
||||
|
||||
return output
|
||||
@@ -0,0 +1,261 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.framework import core
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
from .process_group import _g_process_group_map
|
||||
from .utils import get_dist_attr
|
||||
|
||||
|
||||
def check_filename(re_exp, filename):
|
||||
if re.search(re_exp, filename):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _process_path(path):
|
||||
filename = os.path.basename(path)
|
||||
if filename == "":
|
||||
raise ValueError(
|
||||
"path should be of 'dirname/filename' format, but received filename is empty string"
|
||||
)
|
||||
try:
|
||||
dirname = os.path.dirname(path)
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
return dirname, filename
|
||||
|
||||
|
||||
class DistributedSaver:
|
||||
def __init__(self):
|
||||
self._logger = get_logger(logging.INFO)
|
||||
|
||||
def save(self, path, serial_program, dist_main_program, dist_context):
|
||||
def _save_state(program, path, mode="param"):
|
||||
state = {
|
||||
k: np.array(v) for k, v in program.state_dict(mode).items()
|
||||
}
|
||||
with open(path, "wb") as f:
|
||||
pickle.dump(state, f)
|
||||
|
||||
dirname, filename = _process_path(path)
|
||||
|
||||
rank_id = paddle.distributed.get_rank()
|
||||
# save serial program when rank id is 0
|
||||
if rank_id == 0:
|
||||
self._save_rank_mapping(dirname)
|
||||
serial_model_filename = filename + "_serial.pdmodel"
|
||||
serial_model_path = os.path.join(dirname, serial_model_filename)
|
||||
with open(serial_model_path, "wb") as f:
|
||||
f.write(serial_program.desc.serialize_to_string())
|
||||
|
||||
# save distributed main program
|
||||
dist_model_filename = filename + "_dist" + str(rank_id) + ".pdmodel"
|
||||
dist_model_path = os.path.join(dirname, dist_model_filename)
|
||||
with open(dist_model_path, "wb") as f:
|
||||
f.write(dist_main_program.desc.serialize_to_string())
|
||||
|
||||
# save distributed attribute
|
||||
dist_attr_filename = filename + "_dist" + str(rank_id) + ".pdattr"
|
||||
dist_attr_path = os.path.join(dirname, dist_attr_filename)
|
||||
dist_attrs = get_dist_attr(dist_main_program, dist_context)
|
||||
with open(dist_attr_path, "wb") as f:
|
||||
pickle.dump(dist_attrs, f)
|
||||
|
||||
# save distributed params
|
||||
dist_param_filename = filename + "_dist" + str(rank_id) + ".pdparams"
|
||||
dist_param_path = os.path.join(dirname, dist_param_filename)
|
||||
_save_state(dist_main_program, dist_param_path)
|
||||
|
||||
# save distributed opt states
|
||||
dist_opt_filename = filename + "_dist" + str(rank_id) + ".pdopt"
|
||||
dist_opt_path = os.path.join(dirname, dist_opt_filename)
|
||||
_save_state(dist_main_program, dist_opt_path, "opt")
|
||||
|
||||
# TODO:save cluster.json
|
||||
|
||||
def load(self, path, load_optimizer=True):
|
||||
# TODO: if `program` is None, load `path.pdmodel`.
|
||||
def _load_file(filename, dirname, suffix="pdparams"):
|
||||
file_list = []
|
||||
for file in os.listdir(dirname):
|
||||
if check_filename(f'{filename}(.*)_dist(.*).{suffix}', file):
|
||||
file_list.append(os.path.join(dirname, file))
|
||||
file_list.sort()
|
||||
return file_list
|
||||
|
||||
def _load_state(filename, dirname, suffix="pdparams"):
|
||||
file_list = _load_file(filename, dirname, suffix)
|
||||
state_dict = {}
|
||||
for file in file_list:
|
||||
with open(file, 'rb') as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
state_dict_info = safe_load_pickle(f, encoding='latin1')
|
||||
for name, value in state_dict_info.items():
|
||||
if name in state_dict:
|
||||
state_dict[name].append(np.array(value))
|
||||
else:
|
||||
state_dict[name] = [np.array(value)]
|
||||
self._logger.info(f"Load param file: {file_list}")
|
||||
return state_dict
|
||||
|
||||
filename = os.path.basename(path)
|
||||
if filename == "":
|
||||
raise ValueError(
|
||||
"path should be of 'dirname/filename' format, but received filename is empty string"
|
||||
)
|
||||
dirname = os.path.dirname(path)
|
||||
|
||||
# load path.pdparam and path.pdopt
|
||||
param_state_dict = _load_state(filename, dirname)
|
||||
opt_state_dict = (
|
||||
_load_state(filename, dirname, "pdopt") if load_optimizer else {}
|
||||
)
|
||||
state_dict = dict(param_state_dict, **opt_state_dict)
|
||||
|
||||
# load path.pdattr
|
||||
dist_attr_file_list = _load_file(filename, dirname, "pdattr")
|
||||
self._logger.info(
|
||||
f"Load distributed attribute file: {dist_attr_file_list}"
|
||||
)
|
||||
dist_attr = {}
|
||||
for dist_attr_file in dist_attr_file_list:
|
||||
with open(dist_attr_file, 'rb') as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
dist_attr_info = safe_load_pickle(f, encoding='latin1')
|
||||
for name, attr in dist_attr_info.items():
|
||||
if name not in dist_attr:
|
||||
dist_attr[name] = attr
|
||||
|
||||
return state_dict, dist_attr
|
||||
|
||||
def save_inference_model(self, path, feed_vars, fetch_vars, exe, **kwargs):
|
||||
dirname, filename = _process_path(path)
|
||||
|
||||
# save distributed inference program
|
||||
rank_id = paddle.distributed.get_rank()
|
||||
if rank_id == 0:
|
||||
self._save_rank_mapping(dirname)
|
||||
op_role_key = core.op_proto_and_checker_maker.kOpRoleAttrName()
|
||||
op_role_forward = int(core.op_proto_and_checker_maker.OpRole.Forward)
|
||||
|
||||
dist_main_prog = kwargs.get('program', None)
|
||||
if not dist_main_prog:
|
||||
dist_main_prog = paddle.static.default_main_program()
|
||||
global_block = dist_main_prog.global_block()
|
||||
|
||||
ops = global_block.ops
|
||||
feed_vars_names = [x.name for x in feed_vars]
|
||||
fetch_vars_names = [x.name for x in fetch_vars]
|
||||
|
||||
last_idx = -1
|
||||
for idx, op in enumerate(ops):
|
||||
if op.attr(op_role_key) != op_role_forward:
|
||||
continue
|
||||
if op.type == "read" or op.type == "feed" or op.type == 'recv_v2':
|
||||
feed_vars_names += op.output("Out")
|
||||
if op.type == "send_v2":
|
||||
fetch_vars_names += op.input("X")
|
||||
last_idx = max(idx, last_idx)
|
||||
for out_name in op.output_arg_names:
|
||||
if out_name in fetch_vars_names:
|
||||
last_idx = max(idx, last_idx)
|
||||
|
||||
used_inputs = []
|
||||
used_outputs = []
|
||||
for idx, op in enumerate(ops):
|
||||
if idx > last_idx:
|
||||
break
|
||||
used_inputs += op.input_arg_names
|
||||
used_outputs += op.output_arg_names
|
||||
|
||||
# delete duplicated elements and keep order
|
||||
feed_vars_names = list({}.fromkeys(feed_vars_names).keys())
|
||||
used_inputs = list({}.fromkeys(used_inputs).keys())
|
||||
fetch_vars_names = list({}.fromkeys(fetch_vars_names).keys())
|
||||
used_outputs = list({}.fromkeys(used_outputs).keys())
|
||||
|
||||
dist_feed_vars_names = [
|
||||
var_name for var_name in feed_vars_names if var_name in used_inputs
|
||||
]
|
||||
dist_fetch_vars_names = [
|
||||
var_name
|
||||
for var_name in fetch_vars_names
|
||||
if var_name in used_outputs
|
||||
]
|
||||
|
||||
dist_feed_vars = list(
|
||||
reversed([global_block.vars[name] for name in dist_feed_vars_names])
|
||||
)
|
||||
dist_fetch_vars = [
|
||||
global_block.vars[name] for name in dist_fetch_vars_names
|
||||
]
|
||||
|
||||
dist_filename = filename + "_dist" + str(rank_id)
|
||||
dist_path = os.path.join(dirname, dist_filename)
|
||||
legacy_format = kwargs.get("legacy_format", False)
|
||||
paddle.static.save_inference_model(
|
||||
dist_path,
|
||||
dist_feed_vars,
|
||||
dist_fetch_vars,
|
||||
exe,
|
||||
program=dist_main_prog,
|
||||
legacy_format=legacy_format,
|
||||
)
|
||||
|
||||
def _save_rank_mapping(self, dirname):
|
||||
path = os.path.join(dirname, 'rank_mapping.csv')
|
||||
f = open(path, 'w')
|
||||
f.write('[ring_id -> ranks]\n')
|
||||
for process_group in _g_process_group_map.values():
|
||||
ring_id = process_group._group_id
|
||||
ranks = [str(rank) for rank in process_group._ranks]
|
||||
id_to_rank = str(ring_id) + "," + ",".join(ranks) + '\n'
|
||||
f.write(id_to_rank)
|
||||
id_to_rank = ""
|
||||
f.write('[rank -> ring_ids]\n')
|
||||
rank_to_id_dict = {}
|
||||
for process_group in _g_process_group_map.values():
|
||||
ring_id = process_group._group_id
|
||||
for rank in process_group._ranks:
|
||||
if rank in rank_to_id_dict:
|
||||
rank_to_id_dict[rank].append(str(ring_id))
|
||||
else:
|
||||
rank_to_id_dict[rank] = [str(ring_id)]
|
||||
rank_to_id = ""
|
||||
for item, val in rank_to_id_dict.items():
|
||||
rank_to_id += str(item) + ","
|
||||
rank_to_id += ",".join(val) + "\n"
|
||||
f.write(rank_to_id)
|
||||
rank_to_id = ""
|
||||
f.close()
|
||||
@@ -0,0 +1,412 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
|
||||
import paddle
|
||||
from paddle.framework import Block
|
||||
from paddle.static import Parameter, Variable
|
||||
|
||||
from .dist_attribute import TensorDistAttr
|
||||
from .utils import __no_shape_var_type__, _linear_idx2coordinate
|
||||
|
||||
|
||||
class DistributedTensor:
|
||||
"""
|
||||
DistributedTensor represents the distribution of tensor on the process group and
|
||||
local tensors can be created by DistributedTensor.
|
||||
Only support even sharding now and uneven sharding will be supported in the future.
|
||||
Local tensor information can be obtained from the DistributedTensor instance object,
|
||||
or obtained by the static methods provided by DistributedTensor,
|
||||
including shard (i.e. the index in the serial tensor), offsets, and sizes.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _validate_sizes_and_dist_attr(
|
||||
sizes, dims_mapping, topology, processes, rank=None, shard_sizes=None
|
||||
):
|
||||
if not (
|
||||
isinstance(sizes, (list, tuple))
|
||||
and all(isinstance(x, int) and x >= 0 for x in sizes)
|
||||
):
|
||||
raise ValueError(
|
||||
f"The sizes must be list or tuple and item in sizes must be non-negative integer, but got {sizes}"
|
||||
)
|
||||
if not (
|
||||
isinstance(dims_mapping, (list, tuple))
|
||||
and all(isinstance(x, int) and x >= -1 for x in dims_mapping)
|
||||
):
|
||||
raise ValueError(
|
||||
f"The dims_mapping must be list or tuple and item in dims_mapping must >= -1, but got {dims_mapping}"
|
||||
)
|
||||
if not (
|
||||
isinstance(processes, (list, tuple))
|
||||
and all(isinstance(x, int) and x >= 0 for x in processes)
|
||||
):
|
||||
raise ValueError(
|
||||
f"The processes must be list or tuple and item in processes must be integer, but got {processes}"
|
||||
)
|
||||
if not (
|
||||
isinstance(topology, (list, tuple))
|
||||
and all(isinstance(x, int) and x > 0 for x in topology)
|
||||
):
|
||||
raise ValueError(
|
||||
f"The topology must be list or tuple and item in topology must be non-negative integer, but got {topology}"
|
||||
)
|
||||
if rank is not None and not (isinstance(rank, int) and rank >= 0):
|
||||
raise ValueError(f"The rank must >= 0, but got {rank}")
|
||||
|
||||
# # NOTE: Only support even sharding now
|
||||
# if shard_sizes is not None:
|
||||
# raise ValueError("Only support even sharding now.")
|
||||
|
||||
@staticmethod
|
||||
def get_local_sizes(
|
||||
global_sizes,
|
||||
dims_mapping,
|
||||
topology,
|
||||
processes,
|
||||
rank=None,
|
||||
shard_sizes=None,
|
||||
):
|
||||
DistributedTensor._validate_sizes_and_dist_attr(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
|
||||
local_sizes = []
|
||||
# for even sharding, the local sizes of every rank are equal
|
||||
|
||||
for idx, item in enumerate(global_sizes):
|
||||
# This is a trick to avoid dims_mapping is []
|
||||
val = dims_mapping[idx] if idx < len(dims_mapping) else -1
|
||||
if val == -1:
|
||||
local_sizes.append(item)
|
||||
else:
|
||||
local_sizes.append(item // topology[dims_mapping[idx]])
|
||||
|
||||
return local_sizes
|
||||
|
||||
@staticmethod
|
||||
def get_local_offsets(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes=None
|
||||
):
|
||||
local_sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
local_offsets = []
|
||||
rank_relative = processes.index(rank)
|
||||
coordinate = _linear_idx2coordinate(topology, rank_relative)
|
||||
|
||||
for i in range(len(global_sizes)):
|
||||
if dims_mapping[i] == -1:
|
||||
local_offsets.append(0)
|
||||
else:
|
||||
local_offsets.append(
|
||||
coordinate[dims_mapping[i]] * local_sizes[i]
|
||||
)
|
||||
return local_offsets
|
||||
|
||||
@staticmethod
|
||||
def get_global_sizes(
|
||||
local_sizes,
|
||||
dims_mapping,
|
||||
topology,
|
||||
processes,
|
||||
rank=None,
|
||||
shard_sizes=None,
|
||||
):
|
||||
DistributedTensor._validate_sizes_and_dist_attr(
|
||||
local_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
global_sizes = []
|
||||
for idx, item in enumerate(local_sizes):
|
||||
if dims_mapping[idx] == -1:
|
||||
global_sizes.append(item)
|
||||
else:
|
||||
global_sizes.append(item * topology[dims_mapping[idx]])
|
||||
return global_sizes
|
||||
|
||||
@staticmethod
|
||||
def get_local_shard(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes=None
|
||||
):
|
||||
local_offsets = DistributedTensor.get_local_offsets(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
local_sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
assert len(local_sizes) == len(local_offsets), (
|
||||
f"The length of local_sizes must be equal to local_offsets, but got {len(local_sizes)} and {len(local_offsets)}."
|
||||
)
|
||||
|
||||
local_end_offsets = [
|
||||
x[0] + x[1] for x in zip(local_offsets, local_sizes)
|
||||
]
|
||||
local_shard = list(zip(local_offsets, local_end_offsets))
|
||||
return local_shard
|
||||
|
||||
def __init__(self, serial_tensor, dist_attr=None, dist_context=None):
|
||||
self._serial_tensor = serial_tensor
|
||||
if dist_attr is not None and isinstance(dist_attr, TensorDistAttr):
|
||||
# TODO: remove this deepcopy after we fix the issue
|
||||
self._dist_attr = copy.deepcopy(dist_attr)
|
||||
# self._dist_attr = dist_attr
|
||||
# TODO: Do we really need to write dist_attr back to serial_tensor?
|
||||
self._serial_tensor.dist_attr = dist_attr
|
||||
else:
|
||||
assert dist_attr is None, f"{dist_attr}"
|
||||
# Use the dist attr of serial_tensor to do the initialization
|
||||
self._dist_attr = self._serial_tensor.dist_attr
|
||||
|
||||
self._batch_dim = 0
|
||||
self._local_offsets_map = {}
|
||||
self._local_shard_map = {}
|
||||
self._local_tensor_map = {}
|
||||
|
||||
from .dist_context import get_default_distributed_context
|
||||
|
||||
self._dist_context = (
|
||||
dist_context
|
||||
if dist_context is not None
|
||||
else get_default_distributed_context()
|
||||
)
|
||||
# TODO: Add Automatically to dist_context after initialized and it will be adapted in the future.
|
||||
# self._dist_context.add_dist_tensor_for_program(self)
|
||||
|
||||
@property
|
||||
def serial_tensor(self):
|
||||
return self._serial_tensor
|
||||
|
||||
@property
|
||||
def dist_attr(self):
|
||||
return self._dist_attr
|
||||
|
||||
@dist_attr.setter
|
||||
def dist_attr(self, dist_attr):
|
||||
self._dist_attr = dist_attr
|
||||
# TODO: Do we really need to write back dist_attr to serial_tensor?
|
||||
self._serial_tensor.dist_attr = dist_attr
|
||||
|
||||
@property
|
||||
def dist_context(self):
|
||||
return self._dist_context
|
||||
|
||||
# def _init_default_dist_attr(self):
|
||||
# if self._dist_attr.dims_mapping is None:
|
||||
# if self.serial_tensor.type in __no_shape_var_type__:
|
||||
# tensor_shape = []
|
||||
# else:
|
||||
# tensor_shape = self._serial_tensor.shape
|
||||
# tensor_dims_mapping = [-1 for _ in range(len(tensor_shape))]
|
||||
# self._dist_attr.dims_mapping = tensor_dims_mapping
|
||||
|
||||
def validate_dist_attr(self):
|
||||
if self.serial_tensor.type in __no_shape_var_type__:
|
||||
return True
|
||||
tensor_shape = self.serial_tensor.shape
|
||||
if len(tensor_shape) != len(self.dist_attr.dims_mapping):
|
||||
return False
|
||||
for i in range(len(self.dist_attr.dims_mapping)):
|
||||
if self.dist_attr.dims_mapping[
|
||||
i
|
||||
] < -1 or self.dist_attr.dims_mapping[i] >= len(
|
||||
self.dist_attr.process_mesh.shape
|
||||
):
|
||||
return False
|
||||
for i in range(len(self.dist_attr.process_mesh.shape)):
|
||||
if self.dist_attr.dims_mapping.count(i) > 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def local_sizes(self, rank=None):
|
||||
"""Get local sizes of the given rank."""
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
global_sizes = self.serial_tensor.shape
|
||||
dims_mapping = self.dist_attr.dims_mapping
|
||||
# shard_sizes = self.dist_attr.shard_sizes
|
||||
processes = self.dist_attr.process_mesh.process_ids
|
||||
topology = self.dist_attr.process_mesh.shape
|
||||
local_sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes, dims_mapping, topology, processes, rank
|
||||
)
|
||||
|
||||
return local_sizes
|
||||
|
||||
def local_offsets(self, rank=None):
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
local_offsets = None
|
||||
if rank in self._local_offsets_map.keys():
|
||||
local_offsets = self._local_offsets_map[rank]
|
||||
else:
|
||||
global_sizes = self.serial_tensor.shape
|
||||
dims_mapping = self.dist_attr.dims_mapping
|
||||
# shard_sizes = self.dist_attr.shard_sizes
|
||||
processes = self.dist_attr.process_mesh.process_ids
|
||||
topology = self.dist_attr.process_mesh.shape
|
||||
local_offsets = DistributedTensor.get_local_offsets(
|
||||
global_sizes, dims_mapping, topology, processes, rank
|
||||
)
|
||||
self._local_offsets_map[rank] = local_offsets
|
||||
|
||||
return local_offsets
|
||||
|
||||
def global_sizes(self):
|
||||
return self.serial_tensor.shape
|
||||
|
||||
def local_shard(self, rank=None):
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
local_shard = None
|
||||
if rank in self._local_shard_map.keys():
|
||||
local_shard = self._local_shard_map[rank]
|
||||
else:
|
||||
global_sizes = self.serial_tensor.shape
|
||||
dims_mapping = self.dist_attr.dims_mapping
|
||||
# shard_sizes = self.dist_attr.shard_sizes
|
||||
processes = self.dist_attr.process_mesh.process_ids
|
||||
topology = self.dist_attr.process_mesh.shape
|
||||
local_shard = DistributedTensor.get_local_shard(
|
||||
global_sizes, dims_mapping, topology, processes, rank
|
||||
)
|
||||
self._local_shard_map[rank] = local_shard
|
||||
|
||||
return local_shard
|
||||
|
||||
def new_local_tensor(self, block=None, rank=None, name=None):
|
||||
"""
|
||||
Create a new local tensor of serial tensor corresponding to rank.
|
||||
Args:
|
||||
block (Block): The block contains the new tensor. Default value is recommend and it will be created in the block of dist main program corresponding to the serial tensor block id. Default: None.
|
||||
rank (int): The rank id. Default value is recommend and it will be the current rank. Default: None.
|
||||
"""
|
||||
|
||||
def _copy_kwargs(serial_tensor):
|
||||
kwargs = {}
|
||||
no_need_copy_args = ["self", "block", "shape", "name"]
|
||||
arg_spec = inspect.getfullargspec(Variable.__init__)
|
||||
|
||||
for key in arg_spec.args:
|
||||
# TODO: Check the copied attribute from serial tensor whether valid
|
||||
if key in no_need_copy_args:
|
||||
continue
|
||||
elif key not in kwargs:
|
||||
if key == "type":
|
||||
kwargs[key] = serial_tensor.desc.type()
|
||||
elif key == "dtype":
|
||||
kwargs[key] = serial_tensor.desc.dtype()
|
||||
elif key == "lod_level":
|
||||
kwargs[key] = serial_tensor.desc.lod_level()
|
||||
elif key == "persistable":
|
||||
kwargs[key] = serial_tensor.desc.persistable()
|
||||
elif key == "stop_gradient":
|
||||
kwargs[key] = serial_tensor.desc.stop_gradient()
|
||||
elif key == "need_check_feed":
|
||||
kwargs[key] = serial_tensor.desc.need_check_feed()
|
||||
# TODO: Get capacity by framework
|
||||
elif key == "capacity":
|
||||
continue
|
||||
else:
|
||||
kwargs[key] = self.serial_tensor.__dict__[key]
|
||||
|
||||
if isinstance(serial_tensor, Parameter):
|
||||
kwargs["trainable"] = serial_tensor.trainable
|
||||
kwargs["optimize_attr"] = serial_tensor.trainable
|
||||
kwargs["regularizer"] = serial_tensor.regularizer
|
||||
kwargs["do_model_average"] = serial_tensor.do_model_average
|
||||
kwargs["need_clip"] = serial_tensor.need_clip
|
||||
kwargs["is_distributed"] = serial_tensor.is_distributed
|
||||
kwargs["is_parameter"] = serial_tensor.is_parameter
|
||||
|
||||
return kwargs
|
||||
|
||||
if rank is not None and not (isinstance(rank, int) and rank >= 0):
|
||||
raise ValueError(f"The rank must >= 0, but got {rank}")
|
||||
if block is not None and not isinstance(block, Block):
|
||||
raise TypeError(f"The block must be Block, but got {type(block)}.")
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
|
||||
if block is None:
|
||||
block_id = self.serial_tensor.block.idx
|
||||
block = self.dist_context.dist_main_programs[rank].block(block_id)
|
||||
|
||||
# copy serial tensor attribute
|
||||
kwargs = _copy_kwargs(self.serial_tensor)
|
||||
kwargs["name"] = name
|
||||
kwargs["shape"] = self.local_sizes(rank)
|
||||
|
||||
if isinstance(self.serial_tensor, Parameter):
|
||||
kwargs.pop("persistable")
|
||||
local_tensor = Parameter(block=block, **kwargs)
|
||||
else:
|
||||
local_tensor = block.create_var(**kwargs)
|
||||
|
||||
# TODO: Set original id when set original_id is approved
|
||||
local_tensor.desc.set_original_id(self.serial_tensor.desc.id())
|
||||
self._local_tensor_map[rank] = local_tensor
|
||||
return local_tensor
|
||||
|
||||
def local_tensor(self, rank=None):
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
assert rank in self._local_tensor_map, (
|
||||
f"The rank {rank} local tensor has not been created."
|
||||
)
|
||||
return self._local_tensor_map[rank]
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
memo[id(self)] = result
|
||||
for k, v in self.__dict__.items():
|
||||
if k == "_serial_tensor" or k == "_local_tensor_map":
|
||||
setattr(result, k, v)
|
||||
else:
|
||||
setattr(result, k, copy.deepcopy(v, memo))
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
str = f"{{tensor name: {self.serial_tensor.desc.name()}, tensor id: {self.serial_tensor.desc.id()}, tensor original_id {self.serial_tensor.desc.original_id()}"
|
||||
|
||||
# str += ", {}".format(self.dist_attr)
|
||||
# return str
|
||||
|
||||
if self.dist_attr.is_annotated("process_mesh"):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
str += (
|
||||
f", process_mesh ({annotated_str}): {self.dist_attr.process_mesh}"
|
||||
)
|
||||
|
||||
str += f", is_parameter: {self.serial_tensor.is_parameter}"
|
||||
str += f", chunk_id: {self.dist_attr.chunk_id}"
|
||||
|
||||
if self.dist_attr.is_annotated("dims_mapping"):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
str += f", dims_mapping ({annotated_str}): {self.dist_attr.dims_mapping} }}"
|
||||
|
||||
# if self.dist_attr.is_annotated("shard_mask"):
|
||||
# annotated_str = "annotated"
|
||||
# else:
|
||||
# annotated_str = "non-annotated"
|
||||
# str += ", shard_mask ({}): {}".format(annotated_str, None)
|
||||
|
||||
# if self.dist_attr.is_annotated("offload_device"):
|
||||
# annotated_str = "annotated"
|
||||
# else:
|
||||
# annotated_str = "non-annotated"
|
||||
# str += ", offload_device ({}): {} }}".format(annotated_str, None)
|
||||
return str
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
# Copyright (c) 2021 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 collections import OrderedDict
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, id, **attrs):
|
||||
# Each node must has a unique id
|
||||
self._id = id
|
||||
# Attributes for Node
|
||||
self._attrs = {}
|
||||
self._attrs.update(attrs)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def attrs(self):
|
||||
return self._attrs
|
||||
|
||||
def __getitem__(self, attr_name):
|
||||
return self._attrs[attr_name]
|
||||
|
||||
def __setitem__(self, attr_name, attr_value):
|
||||
self._attrs[attr_name] = attr_value
|
||||
|
||||
def __contains__(self, attr_name):
|
||||
try:
|
||||
return attr_name in self._attrs
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
str = f"(id: {self.id}, attrs: {self.attrs})"
|
||||
return str
|
||||
|
||||
|
||||
class Edge:
|
||||
def __init__(self, src_id, tgt_id, **attrs):
|
||||
# The id of source node in an Edge
|
||||
self._src_id = src_id
|
||||
# The id of target node in an Edge
|
||||
self._tgt_id = tgt_id
|
||||
# Attributes for Edge
|
||||
self._attrs = {}
|
||||
self._attrs.update(attrs)
|
||||
|
||||
@property
|
||||
def src_id(self):
|
||||
return self._src_id
|
||||
|
||||
@property
|
||||
def tgt_id(self):
|
||||
return self._tgt_id
|
||||
|
||||
@property
|
||||
def attrs(self):
|
||||
return self._attrs
|
||||
|
||||
def __getitem__(self, attr_name):
|
||||
return self._attrs[attr_name]
|
||||
|
||||
def __setitem__(self, attr_name, attr_value):
|
||||
self._attrs[attr_name] = attr_value
|
||||
|
||||
def __contains__(self, attr_name):
|
||||
try:
|
||||
return attr_name in self._attrs
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
str = ""
|
||||
str += f"(src_id: {self.src_id}, tgt_id: {self.tgt_id}, attrs: {self._attrs})"
|
||||
return str
|
||||
|
||||
|
||||
class Graph:
|
||||
def __init__(self, **attrs):
|
||||
# _nodes is dict for storing the nodes of the graph.
|
||||
# The key of this dict is the node id.
|
||||
self._nodes = {}
|
||||
# _adjs is a dict of dict for storing the adjacency of the graph.
|
||||
# The key of the outer dict is the node id of the source node and
|
||||
# the key of the inner dict is the node id of the target node.
|
||||
self._adjs = {}
|
||||
# Attributes for Graph
|
||||
self._attrs = {}
|
||||
self._attrs.update(attrs)
|
||||
self._reverse_adjs = {}
|
||||
self._attr_to_nodes = {}
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
return self._nodes
|
||||
|
||||
@property
|
||||
def attrs(self):
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def adjs(self):
|
||||
return self._adjs
|
||||
|
||||
def add_node(self, node_id, **attrs):
|
||||
if node_id is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
if node_id not in self._nodes:
|
||||
node = Node(node_id, **attrs)
|
||||
self._nodes[node_id] = node
|
||||
self._adjs[node_id] = {}
|
||||
self._reverse_adjs[node_id] = []
|
||||
else:
|
||||
self._nodes[node_id].attrs.update(attrs)
|
||||
|
||||
return self._nodes[node_id]
|
||||
|
||||
def add_edge(self, src_id, tgt_id, **attrs):
|
||||
# add nodes
|
||||
if src_id is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
if tgt_id is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
if src_id not in self._nodes:
|
||||
src_node = Node(src_id)
|
||||
self._nodes[src_id] = src_node
|
||||
# for one tensor to multiple ops
|
||||
self._adjs[src_id] = OrderedDict()
|
||||
self._reverse_adjs[src_id] = []
|
||||
if tgt_id not in self._nodes:
|
||||
tgt_node = Node(tgt_id)
|
||||
self._nodes[tgt_id] = tgt_node
|
||||
# for one tensor to multiple ops
|
||||
self._adjs[tgt_id] = OrderedDict()
|
||||
self._reverse_adjs[tgt_id] = []
|
||||
# add the edge
|
||||
edge = Edge(src_id, tgt_id, **attrs)
|
||||
self._adjs[src_id][tgt_id] = edge
|
||||
|
||||
# add the reverse adj
|
||||
self._reverse_adjs[tgt_id].append(self.nodes[src_id])
|
||||
return edge
|
||||
|
||||
def __len__(self):
|
||||
return len(self._nodes)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._nodes.values())
|
||||
|
||||
def __getitem__(self, node_id):
|
||||
# Return the adjacency of a node
|
||||
return self._adjs[node_id]
|
||||
|
||||
def __contains__(self, node_id):
|
||||
# Check whether a node in the graph
|
||||
try:
|
||||
return node_id in self._nodes
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
str = ""
|
||||
str += "**************Nodes**************\n"
|
||||
for node_id in self.nodes:
|
||||
str += f"{self.nodes[node_id]}\n"
|
||||
|
||||
str += "**************Edges**************\n"
|
||||
for src_id in self.adjs:
|
||||
str += f"--------------{src_id}--------------\n"
|
||||
for idx, tgt_id in enumerate(self.adjs[src_id]):
|
||||
str += f"{self.adjs[src_id][tgt_id]}\n"
|
||||
|
||||
return str
|
||||
@@ -0,0 +1,673 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import paddle
|
||||
from paddle import core
|
||||
from paddle.jit import not_to_static, to_static
|
||||
from paddle.jit.dy2static.program_translator import (
|
||||
ProgramTranslator,
|
||||
StaticFunction,
|
||||
)
|
||||
from paddle.jit.dy2static.utils import as_not_paddle_func
|
||||
from paddle.nn import Layer
|
||||
from paddle.static import Parameter, global_scope, program_guard
|
||||
from paddle.static.amp.fp16_utils import (
|
||||
DEFAULT_AMP_OPTIONS,
|
||||
prepare_op_amp_options,
|
||||
)
|
||||
|
||||
from .converter import Converter
|
||||
from .dist_attribute import TensorDistAttr
|
||||
from .process_group import get_world_process_group
|
||||
from .utils import get_logger, to_list
|
||||
|
||||
|
||||
class ProxyLayer(Layer):
|
||||
"""
|
||||
ProxyLayer implements all logic for converting dygraph model into
|
||||
static Program IR. Meanwhile, it provides conventional interfaces for
|
||||
auto parallel to visit feed/fetch/loss/metric variables.
|
||||
"""
|
||||
|
||||
def __init__(self, layer, loss_func, metrics):
|
||||
super().__init__()
|
||||
# NOTE: All verify logics are finished in Engine.Prepare
|
||||
self.inner_layer = layer
|
||||
self.loss_func = loss_func
|
||||
self.metrics = metrics
|
||||
# train / eval / predict
|
||||
self.mode = None
|
||||
|
||||
# generated program vars
|
||||
self._input_vars = defaultdict(list)
|
||||
self._label_vars = defaultdict(list)
|
||||
self._output_vars = defaultdict(list)
|
||||
self._loss_vars = defaultdict(list)
|
||||
self._loss_names = defaultdict(list)
|
||||
self._metric_vars = defaultdict(list)
|
||||
|
||||
# Consider ProxyLayer as not Paddle inner function because it contains
|
||||
# user-defined layer.
|
||||
for fn_name in [
|
||||
"_train",
|
||||
"_eval",
|
||||
"_predict",
|
||||
"call_loss",
|
||||
"call_metrics",
|
||||
]:
|
||||
as_not_paddle_func(
|
||||
f"{inspect.getmodule(ProxyLayer).__name__}.ProxyLayer.{fn_name}"
|
||||
)
|
||||
|
||||
@paddle.jit.not_to_static
|
||||
def append_loss_to_shadow_output(self, mode):
|
||||
name = paddle.utils.unique_name.generate('loss')
|
||||
paddle._C_ops.set_persistable_value(self._loss_vars[mode], name)
|
||||
self._loss_names[mode] = name
|
||||
|
||||
def _train(self, inputs, labels):
|
||||
"""
|
||||
Train process of inner_layer with forward/loss/metric logic.
|
||||
"""
|
||||
# step 1. save feed variables of Program
|
||||
mode = 'train'
|
||||
self._input_vars[mode] = inputs
|
||||
self._label_vars[mode] = labels
|
||||
|
||||
# step 2. call inner_layer.forward
|
||||
self._output_vars[mode] = self.inner_layer(*inputs)
|
||||
|
||||
# step 3. calculate loss if needed
|
||||
new_inputs = self._prepare(self.output_vars, labels)
|
||||
self._loss_vars[mode] = self.call_loss(new_inputs)
|
||||
if paddle.base.framework.get_flags("FLAGS_enable_pir_api")[
|
||||
"FLAGS_enable_pir_api"
|
||||
]:
|
||||
self.append_loss_to_shadow_output(mode)
|
||||
|
||||
# step 4. calculate metrics if needed
|
||||
self._metric_vars[mode] = self.call_metrics(new_inputs)
|
||||
|
||||
def _eval(self, inputs, labels):
|
||||
"""
|
||||
Evaluate process of inner_layer with forward/loss/metric logic.
|
||||
"""
|
||||
# TODO(dev): we can reuse codes with self._train after making
|
||||
# sure if they can.
|
||||
|
||||
# step 1. save feed variables of Program
|
||||
mode = 'eval'
|
||||
self._input_vars[mode] = inputs
|
||||
self._label_vars[mode] = labels
|
||||
|
||||
# step 2. call inner_layer.forward
|
||||
self._output_vars[mode] = self.inner_layer(*inputs)
|
||||
|
||||
# step 3. calculate loss if needed
|
||||
new_inputs = self._prepare(self.output_vars, labels)
|
||||
self._loss_vars[mode] = self.call_loss(new_inputs)
|
||||
if paddle.base.framework.get_flags("FLAGS_enable_pir_api")[
|
||||
"FLAGS_enable_pir_api"
|
||||
]:
|
||||
self.append_loss_to_shadow_output(mode)
|
||||
|
||||
# step 4. calculate metrics if needed
|
||||
self._metric_vars[mode] = self.call_metrics(new_inputs)
|
||||
|
||||
def _predict(self, inputs, labels):
|
||||
"""
|
||||
Predict process of inner_layer with forward logic.
|
||||
"""
|
||||
# step 1. save feed variables of Program
|
||||
mode = 'predict'
|
||||
self._input_vars[mode] = inputs
|
||||
self._label_vars[mode] = labels
|
||||
|
||||
# step 2. call inner_layer.forward
|
||||
self._output_vars[mode] = self.inner_layer(*inputs)
|
||||
|
||||
@not_to_static
|
||||
def _prepare(self, outputs, labels):
|
||||
"""
|
||||
Concat outputs and labels as a single list
|
||||
|
||||
NOTE(dev): We use @not_to_static to avoid AST Analysis.
|
||||
"""
|
||||
return to_list(outputs) + to_list(labels)
|
||||
|
||||
def call_loss(self, inputs):
|
||||
"""
|
||||
Apply Loss Function on outputs and labels.
|
||||
|
||||
Args:
|
||||
inputs: List[Variable]
|
||||
|
||||
Returns: List[Variable]
|
||||
"""
|
||||
res = []
|
||||
if self.loss_func is not None:
|
||||
res = self.loss_func(*inputs)
|
||||
return res
|
||||
|
||||
def call_metrics(self, inputs):
|
||||
"""
|
||||
Apply Metrics Function on outputs and labels.
|
||||
|
||||
Args:
|
||||
inputs: List[Variable]
|
||||
|
||||
Returns: List[Variable]
|
||||
"""
|
||||
outs = []
|
||||
for metric in self.metrics:
|
||||
outs.append(to_list(metric.compute(*inputs)))
|
||||
|
||||
return outs
|
||||
|
||||
def set_mode(self, mode):
|
||||
self.mode = mode
|
||||
self.training = mode == 'train'
|
||||
|
||||
def clone(self):
|
||||
return ProxyLayer(self.inner_layer, self.loss_func, self.metrics)
|
||||
|
||||
@property
|
||||
def input_vars(self):
|
||||
return self._input_vars[self.mode]
|
||||
|
||||
@property
|
||||
def label_vars(self):
|
||||
return self._label_vars[self.mode]
|
||||
|
||||
@property
|
||||
def output_vars(self):
|
||||
return self._output_vars[self.mode]
|
||||
|
||||
@property
|
||||
def loss_vars(self):
|
||||
return self._loss_vars[self.mode]
|
||||
|
||||
@property
|
||||
def loss_names(self):
|
||||
return self._loss_names[self.mode]
|
||||
|
||||
@property
|
||||
def metric_vars(self):
|
||||
return self._metric_vars[self.mode]
|
||||
|
||||
@property
|
||||
def startup_program(self):
|
||||
return self.inner_layer._startup_program()
|
||||
|
||||
|
||||
class BuildInfo:
|
||||
def __init__(self):
|
||||
self.clear()
|
||||
|
||||
def has_cache(self, mode, update=False):
|
||||
is_cache = self.states[mode]
|
||||
if update:
|
||||
self.cache(mode)
|
||||
return is_cache
|
||||
|
||||
def cache(self, mode):
|
||||
self.states[mode] = True
|
||||
|
||||
def clear(self):
|
||||
self.states = defaultdict(bool)
|
||||
|
||||
|
||||
class ProgramHelper:
|
||||
"""
|
||||
A Helper class for Engine to provides different Program IR according specified 'mode'.
|
||||
"""
|
||||
|
||||
def __init__(self, layer, loss_func, metrics, inputs_spec, labels_spec):
|
||||
# original model config information
|
||||
# TODO(Aurelius84): Implement append_backward and optimizer in ProxyLayer
|
||||
# after distribute engine satisfy basic condition.
|
||||
self.proxy_layer = ProxyLayer(layer, loss_func, metrics)
|
||||
self.inputs_spec = inputs_spec
|
||||
self.labels_spec = labels_spec
|
||||
|
||||
self.build_info = BuildInfo()
|
||||
self._logger = get_logger(logging.INFO)
|
||||
self.lazy_init = False
|
||||
self._all_params_dist_attr = {}
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset all state of current Object.
|
||||
"""
|
||||
self.build_info.clear()
|
||||
self.proxy_layer = self.proxy_layer.clone()
|
||||
|
||||
def build_program(self, mode):
|
||||
"""
|
||||
Convert dygraph model into static Program IR.
|
||||
"""
|
||||
assert mode in ['train', 'eval', 'predict']
|
||||
self.proxy_layer.set_mode(mode)
|
||||
# skip if we has already built program.
|
||||
if self.build_info.has_cache(mode, True):
|
||||
self._logger.info(
|
||||
f"Already build program with mode = {mode}, use cached program."
|
||||
)
|
||||
return
|
||||
|
||||
self._logger.info(f"start to build program for mode = {mode}.")
|
||||
input_spec = [self.inputs_spec, self.labels_spec]
|
||||
static_func = to_static(
|
||||
self.static_func(), input_spec=input_spec, full_graph=True
|
||||
)
|
||||
|
||||
func_name = '_' + mode
|
||||
setattr(self.proxy_layer, func_name, static_func)
|
||||
|
||||
# NOTE(dev): Because @to_static is a Lazy mechanism, so we explicitly call this to trigger
|
||||
# generating Program IR immediately.
|
||||
concrete_program = getattr(self.proxy_layer, func_name).concrete_program
|
||||
|
||||
# TODO(zhiqiu): prepare_op_amp_options is not supported for PIR program
|
||||
# It will to use dynamic-static unified amp in pir program, and there is
|
||||
# no need to fit for prepare_op_amp_options
|
||||
if not paddle.base.framework.get_flags("FLAGS_enable_pir_api")[
|
||||
"FLAGS_enable_pir_api"
|
||||
]:
|
||||
prepare_op_amp_options(
|
||||
concrete_program.main_program,
|
||||
ProgramTranslator.get_instance()._amp_records,
|
||||
DEFAULT_AMP_OPTIONS,
|
||||
)
|
||||
self._build_startup_program()
|
||||
|
||||
def _build_startup_program(self):
|
||||
"""
|
||||
Create and Sync parameters into startup program.
|
||||
"""
|
||||
startup_program = self.startup_program
|
||||
if len(startup_program.global_block().ops) > 1:
|
||||
self.lazy_init = True
|
||||
return
|
||||
|
||||
for param in self.concrete_program.parameters:
|
||||
Parameter(
|
||||
name=param.name,
|
||||
desc=param,
|
||||
type=param.type,
|
||||
shape=param.shape,
|
||||
dtype=param.dtype,
|
||||
stop_gradient=param.stop_gradient,
|
||||
block=startup_program.global_block(),
|
||||
)
|
||||
|
||||
def apply_optimizer(self, optimizer):
|
||||
"""
|
||||
Append backward and generate optimizer operations.
|
||||
"""
|
||||
self._verify_optimizer(optimizer)
|
||||
self._logger.info(
|
||||
"start to apply optimizer: %s ", type(optimizer).__name__
|
||||
)
|
||||
# clear optimizer parameters
|
||||
original_params = optimizer._parameter_list
|
||||
optimizer._parameter_list = None
|
||||
with program_guard(self.main_program, self.startup_program):
|
||||
res = optimizer.minimize(self.loss_vars[0])
|
||||
|
||||
# restore optimizer parameters
|
||||
optimizer._parameter_list = original_params
|
||||
return res
|
||||
|
||||
def _verify_optimizer(self, optimizer):
|
||||
assert optimizer is not None
|
||||
assert hasattr(optimizer, "minimize"), (
|
||||
"Optimizer must have minimize() method."
|
||||
)
|
||||
assert self.proxy_layer.mode == 'train', (
|
||||
f"Required mode == 'train', but received '{self.proxy_layer.mode}'"
|
||||
)
|
||||
assert len(self.loss_vars) == 1, (
|
||||
f"Required len(loss_vars) == 1, but received len(loss_vars) = {len(self.loss_vars)}"
|
||||
)
|
||||
|
||||
def to(self, mode):
|
||||
"""
|
||||
Switch underly proxy layer mode into target mode.
|
||||
"""
|
||||
assert mode in ['train', 'eval', 'predict']
|
||||
func = getattr(self.proxy_layer, '_' + mode)
|
||||
assert isinstance(func, StaticFunction), (
|
||||
"Please call build_program(mode) firstly."
|
||||
)
|
||||
self.proxy_layer.set_mode(mode)
|
||||
|
||||
def static_func(self):
|
||||
"""
|
||||
Return StaticFunction instance with underly target mode.
|
||||
"""
|
||||
assert self.proxy_layer.mode in [
|
||||
'train',
|
||||
'eval',
|
||||
'predict',
|
||||
], "Please call build_program(mode) firstly."
|
||||
func_name = '_' + self.proxy_layer.mode
|
||||
return getattr(self.proxy_layer, func_name)
|
||||
|
||||
def init_pir(self, main_program, place):
|
||||
# collect all params in current dist program
|
||||
param_values = main_program.global_block().all_parameters()
|
||||
value_name_to_value = {}
|
||||
dy_param_name_to_pir_param_name = {}
|
||||
for value in param_values:
|
||||
value_name_to_value[value.name] = value
|
||||
|
||||
dy_params = self.concrete_program.parameters[0]
|
||||
pir_param = self.concrete_program.parameters[1]
|
||||
|
||||
for i in range(len(pir_param)):
|
||||
if pir_param[i].name in value_name_to_value:
|
||||
dy_param_name_to_pir_param_name[dy_params[i].name] = pir_param[
|
||||
i
|
||||
].name
|
||||
|
||||
is_comm = False
|
||||
for param in dy_params:
|
||||
if param.is_dist():
|
||||
process_mesh, dims_mapping = self._all_params_dist_attr[
|
||||
param.name
|
||||
]
|
||||
var_dist_attr = TensorDistAttr()
|
||||
var_dist_attr.process_mesh = process_mesh
|
||||
var_dist_attr.dims_mapping = dims_mapping
|
||||
is_comm = True
|
||||
with paddle.no_grad():
|
||||
tmp = paddle.base.core.reshard(param, var_dist_attr)
|
||||
if tmp._is_initialized():
|
||||
param.get_tensor()._share_data_with(tmp.get_tensor())
|
||||
else:
|
||||
# Only setting the "param" to "None" can't release the memory
|
||||
param.get_tensor()._clear()
|
||||
param = None
|
||||
|
||||
# create var in scope and share parameters to scope
|
||||
if param is None:
|
||||
continue
|
||||
if param.name not in dy_param_name_to_pir_param_name:
|
||||
# Release the redundant params
|
||||
param.get_tensor()._clear()
|
||||
continue
|
||||
if not param._is_initialized():
|
||||
continue
|
||||
if param.is_dense():
|
||||
value_name = dy_param_name_to_pir_param_name[param.name]
|
||||
value = value_name_to_value[value_name]
|
||||
# get param_var's dist_attr
|
||||
assert value.is_dist_dense_tensor_type(), (
|
||||
f"param [{value.name}] is not dist tensor type"
|
||||
)
|
||||
dist_attr = {
|
||||
"dims_mapping": value.dist_attr().dims_mapping,
|
||||
"process_shape": value.dist_attr().process_mesh.shape,
|
||||
"process_group": value.dist_attr().process_mesh.process_ids,
|
||||
}
|
||||
# slice param_value with dist_attr
|
||||
# share sliced_param_value with param_tensor in global_scope
|
||||
pir_scope_param = global_scope().var(value_name).get_tensor()
|
||||
sliced_param = Converter.slice_with_dist_attr(
|
||||
param.numpy(), dist_attr
|
||||
)
|
||||
pir_scope_param.set(sliced_param, place)
|
||||
param.get_tensor()._clear()
|
||||
|
||||
elif param.is_dist():
|
||||
value_name = dy_param_name_to_pir_param_name[param.name]
|
||||
value = value_name_to_value[value_name]
|
||||
# assert value.is_dist_dense_tensor_type(), "param [{}] is not dist tensor type".format(value.name)
|
||||
pir_scope_param = global_scope().var(value_name).get_tensor()
|
||||
pir_scope_param._share_data_with(
|
||||
param.get_tensor().get_tensor()
|
||||
)
|
||||
param.get_tensor()._clear()
|
||||
|
||||
world_group = get_world_process_group()
|
||||
if (
|
||||
is_comm
|
||||
and world_group.nranks > 1
|
||||
and paddle.distributed.get_world_size() > 1
|
||||
):
|
||||
paddle.disable_static()
|
||||
barrier_tensor = paddle.full([1], 1, dtype="int32")
|
||||
# barrier is not available in xpu for now
|
||||
if not paddle.framework.core.is_compiled_with_xpu():
|
||||
paddle._legacy_C_ops.barrier(
|
||||
barrier_tensor, barrier_tensor, 'ring_id', 0
|
||||
)
|
||||
paddle.enable_static()
|
||||
|
||||
def init(self, main_program, place, dist_context):
|
||||
if self.lazy_init:
|
||||
return
|
||||
|
||||
amp_strategy = dist_context.strategy.amp
|
||||
amp_config = copy.deepcopy(amp_strategy.to_dict())
|
||||
need_cast_parameter = amp_strategy.enable and amp_config["level"] in [
|
||||
"o2",
|
||||
"o3",
|
||||
]
|
||||
is_comm = False
|
||||
for param in self.concrete_program.parameters:
|
||||
if param.is_dist():
|
||||
serial_main_program = self.concrete_program.main_program
|
||||
var = serial_main_program.global_block().vars[param.name]
|
||||
var_dist_attr = dist_context.get_tensor_dist_attr_for_program(
|
||||
var
|
||||
)
|
||||
is_comm = True
|
||||
# No need to construct backward.
|
||||
with paddle.no_grad():
|
||||
tmp = paddle.base.core.reshard(param, var_dist_attr)
|
||||
if tmp._is_initialized():
|
||||
param.get_tensor()._share_data_with(tmp.get_tensor())
|
||||
else:
|
||||
# Only setting the "param" to "None" can't release the memory
|
||||
param.get_tensor()._clear()
|
||||
param = None
|
||||
paddle.device.synchronize()
|
||||
|
||||
# create var in scope and share parameters to scope
|
||||
if param is None:
|
||||
continue
|
||||
if param.name not in main_program.global_block().vars:
|
||||
# Release the redundant params
|
||||
param.get_tensor()._clear()
|
||||
continue
|
||||
if not param._is_initialized():
|
||||
continue
|
||||
if param.is_dense():
|
||||
# get param_var's dist_attr
|
||||
var = main_program.global_block().vars[param.name]
|
||||
var_dist_attr = dist_context.get_tensor_dist_attr_for_program(
|
||||
var
|
||||
)
|
||||
dist_attr = {
|
||||
"dims_mapping": var_dist_attr.dims_mapping,
|
||||
"process_shape": var_dist_attr.process_mesh.shape,
|
||||
"process_group": var_dist_attr.process_mesh.process_ids,
|
||||
}
|
||||
# slice param_value with dist_attr
|
||||
# share sliced_param_value with param_tensor in global_scope
|
||||
param_tensor = global_scope().var(param.name).get_tensor()
|
||||
sliced_param = Converter.slice_with_dist_attr(
|
||||
param.numpy(), dist_attr
|
||||
)
|
||||
param_tensor.set(sliced_param, place)
|
||||
if not need_cast_parameter:
|
||||
param.get_tensor()._clear()
|
||||
elif param.is_dist():
|
||||
dense_tensor = global_scope().var(param.name).get_tensor()
|
||||
dense_tensor._share_data_with(param.get_tensor().get_tensor())
|
||||
|
||||
# transform the parameter in eager mode for amp.
|
||||
if need_cast_parameter:
|
||||
for param in self.concrete_program.parameters:
|
||||
amp_dtype = amp_config["dtype"]
|
||||
scope_var = global_scope().find_var(param.name)
|
||||
# The parameter is not in this rank.
|
||||
if not scope_var:
|
||||
continue
|
||||
# The parameter do not need to transform
|
||||
if param.dtype in [paddle.float16, paddle.bfloat16]:
|
||||
continue
|
||||
scope_tensor = global_scope().var(param.name).get_tensor()
|
||||
assert scope_var and scope_tensor._is_initialized(), (
|
||||
f"Parameter: {param.name} is not put into global_scope or not initialized."
|
||||
)
|
||||
param_used = param
|
||||
# For the params without dist_attr.
|
||||
# NOTE(lizhiyu): In principle, each param should have dist_attr.
|
||||
if param.is_dense():
|
||||
# get param_var's dist_attr
|
||||
var = main_program.global_block().vars[param.name]
|
||||
var_dist_attr = (
|
||||
dist_context.get_tensor_dist_attr_for_program(var)
|
||||
)
|
||||
dist_attr = {
|
||||
"dims_mapping": var_dist_attr.dims_mapping,
|
||||
"process_shape": var_dist_attr.process_mesh.shape,
|
||||
"process_group": var_dist_attr.process_mesh.process_ids,
|
||||
}
|
||||
# slice param_value with dist_attr
|
||||
sliced_param = Converter.slice_with_dist_attr(
|
||||
param.numpy(), dist_attr
|
||||
)
|
||||
with paddle.base.dygraph.guard():
|
||||
param_used = paddle.to_tensor(
|
||||
sliced_param, place=param.place
|
||||
)
|
||||
param.get_tensor()._clear()
|
||||
with paddle.base.dygraph.guard():
|
||||
if amp_dtype == "float16":
|
||||
with (
|
||||
paddle.no_grad(),
|
||||
paddle.base.framework._dygraph_place_guard(
|
||||
place=place
|
||||
),
|
||||
):
|
||||
t_casted = param_used.cast(
|
||||
dtype=core.VarDesc.VarType.FP16
|
||||
)
|
||||
elif amp_dtype == "bfloat16":
|
||||
with (
|
||||
paddle.no_grad(),
|
||||
paddle.base.framework._dygraph_place_guard(
|
||||
place=place
|
||||
),
|
||||
):
|
||||
t_casted = param_used.cast(
|
||||
dtype=core.VarDesc.VarType.BF16
|
||||
)
|
||||
# NOTE(lizhiyu): Clear the origin param. Don't use `param_used.get_tensor().get_tensor()._clear()` to
|
||||
# clear the `DistTensor`, because it can't clear the `_holder`,
|
||||
# which `param_used.get_tensor().get_tensor()` will copy one `DenseTensor`.
|
||||
param_used.get_tensor()._clear()
|
||||
if t_casted.is_dist():
|
||||
scope_tensor._share_data_with(
|
||||
t_casted.get_tensor().get_tensor()
|
||||
)
|
||||
else:
|
||||
scope_tensor._share_data_with(t_casted.get_tensor())
|
||||
|
||||
world_group = get_world_process_group()
|
||||
if (
|
||||
is_comm
|
||||
and world_group.nranks > 1
|
||||
and paddle.distributed.get_world_size() > 1
|
||||
):
|
||||
paddle.disable_static()
|
||||
barrier_tensor = paddle.full([1], 1, dtype="int32")
|
||||
# barrier is not available in xpu for now
|
||||
if not paddle.framework.core.is_compiled_with_xpu():
|
||||
paddle._legacy_C_ops.barrier(
|
||||
barrier_tensor, barrier_tensor, 'ring_id', 0
|
||||
)
|
||||
paddle.enable_static()
|
||||
|
||||
def cache_whole_graph_dist_attr(self, all_params):
|
||||
for param_value in all_params:
|
||||
dist_attr = param_value.dist_attr()
|
||||
if dist_attr:
|
||||
process_mesh = dist_attr.process_mesh
|
||||
dims_mapping = dist_attr.dims_mapping
|
||||
self._all_params_dist_attr[param_value.name] = [
|
||||
process_mesh,
|
||||
dims_mapping,
|
||||
]
|
||||
|
||||
@property
|
||||
def concrete_program(self):
|
||||
return self.static_func().concrete_program
|
||||
|
||||
@property
|
||||
def main_program(self):
|
||||
return self.concrete_program.main_program
|
||||
|
||||
@property
|
||||
def startup_program(self):
|
||||
try:
|
||||
return self.proxy_layer.startup_program
|
||||
except Exception as err:
|
||||
self._logger.warning(
|
||||
"The startup_program is not built by `lazy init`."
|
||||
)
|
||||
if isinstance(err, AssertionError):
|
||||
return self.concrete_program.startup_program
|
||||
raise err
|
||||
|
||||
@property
|
||||
def input_vars(self):
|
||||
return to_list(self.proxy_layer.input_vars)
|
||||
|
||||
@property
|
||||
def output_vars(self):
|
||||
return to_list(self.proxy_layer.output_vars)
|
||||
|
||||
@property
|
||||
def label_vars(self):
|
||||
return to_list(self.proxy_layer.label_vars)
|
||||
|
||||
@property
|
||||
def loss_vars(self):
|
||||
return to_list(self.proxy_layer.loss_vars)
|
||||
|
||||
@property
|
||||
def loss_names(self):
|
||||
return to_list(self.proxy_layer.loss_names)
|
||||
|
||||
@property
|
||||
def metric_vars(self):
|
||||
return to_list(self.proxy_layer.metric_vars)
|
||||
|
||||
def named_parameters(self):
|
||||
static_func = self.static_func()
|
||||
partial_program = static_func.get_concrete_program(
|
||||
self.inputs_spec, self.labels_spec
|
||||
)[-1]
|
||||
# TODO(xiongkun): support pir in the feature.
|
||||
return {param.name: param for param in partial_program._params}
|
||||
@@ -0,0 +1,342 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import functools
|
||||
import operator
|
||||
import os
|
||||
from collections import deque
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
|
||||
from .cluster import DeviceType
|
||||
from .graph import Graph
|
||||
from .process_group import get_process_group
|
||||
|
||||
|
||||
def is_collective_comm_op(op):
|
||||
comm_list = [
|
||||
"all_gather",
|
||||
"all_reduce",
|
||||
"broadcast",
|
||||
]
|
||||
reduce_type = [
|
||||
dist.ReduceOp.SUM,
|
||||
dist.ReduceOp.MIN,
|
||||
dist.ReduceOp.MAX,
|
||||
dist.ReduceOp.PROD,
|
||||
]
|
||||
if op.type == "reduce" and op.attr("reduce_type") in reduce_type:
|
||||
return True
|
||||
if op.type in comm_list:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_p2p_comm_op(op):
|
||||
comm_list = ["send_v2", "recv_v2"]
|
||||
if op.type in comm_list:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_dtype_bytes(dtype):
|
||||
num_bytes = 0
|
||||
if dtype == paddle.float64:
|
||||
num_bytes = 8
|
||||
elif dtype == paddle.float32:
|
||||
num_bytes = 4
|
||||
elif dtype == paddle.float16:
|
||||
num_bytes = 2
|
||||
elif dtype == paddle.bfloat16:
|
||||
num_bytes = 2
|
||||
elif dtype == paddle.int64:
|
||||
num_bytes = 8
|
||||
elif dtype == paddle.int32:
|
||||
num_bytes = 4
|
||||
elif dtype == paddle.int16:
|
||||
num_bytes = 2
|
||||
elif dtype == paddle.int8:
|
||||
num_bytes = 1
|
||||
elif dtype == paddle.uint8:
|
||||
num_bytes = 1
|
||||
else:
|
||||
raise ValueError(f"Unrecognized dtype {dtype}.")
|
||||
return num_bytes
|
||||
|
||||
|
||||
def get_comm_volume(comm_op, src_rank, tgt_rank):
|
||||
comm_volume = None
|
||||
if src_rank == tgt_rank:
|
||||
return comm_volume
|
||||
comm_op_type = comm_op.type
|
||||
if comm_op_type != "recv_v2":
|
||||
tensor_name = comm_op.input_arg_names[0]
|
||||
else:
|
||||
tensor_name = comm_op.output_arg_names[0]
|
||||
tensor = comm_op.block._find_var_recursive(tensor_name)
|
||||
assert tensor is not None
|
||||
tensor_shape = tensor.shape
|
||||
# Skip the batch dim
|
||||
new_tensor_shape = []
|
||||
for val in tensor_shape:
|
||||
if val == -1:
|
||||
print("Warning: -1 in the tensor shape.")
|
||||
new_tensor_shape.append(1)
|
||||
else:
|
||||
new_tensor_shape.append(val)
|
||||
tensor_size = functools.reduce(operator.mul, new_tensor_shape, 1)
|
||||
tensor_bytes = tensor_size * get_dtype_bytes(tensor.dtype)
|
||||
if "c_allreduce" in comm_op_type or "all_reduce" in comm_op_type:
|
||||
comm_volume = 2 * tensor_bytes
|
||||
elif "all_gather" in comm_op_type:
|
||||
comm_volume = tensor_bytes
|
||||
elif "broadcast" in comm_op_type:
|
||||
if comm_op.attr("root") == src_rank:
|
||||
comm_volume = tensor_bytes
|
||||
else:
|
||||
comm_volume = None
|
||||
elif "c_reduce" in comm_op_type:
|
||||
if comm_op.attr("root_id") == src_rank:
|
||||
comm_volume = None
|
||||
else:
|
||||
comm_volume = tensor_bytes
|
||||
elif "reduce" == comm_op_type:
|
||||
if comm_op.attr("root_id") == src_rank:
|
||||
comm_volume = None
|
||||
else:
|
||||
comm_volume = tensor_bytes
|
||||
elif "send_v2" in comm_op_type:
|
||||
if comm_op.attr("peer") == tgt_rank:
|
||||
comm_volume = tensor_bytes
|
||||
else:
|
||||
comm_volume = None
|
||||
elif "recv_v2" in comm_op_type:
|
||||
comm_volume = None
|
||||
else:
|
||||
raise ValueError("Unrecognized communication operator.")
|
||||
return comm_volume
|
||||
|
||||
|
||||
def analyze_comm_requirements_from_op(op, rank, g_process_group_map):
|
||||
comm_requirements_to_ranks = {}
|
||||
if is_collective_comm_op(op):
|
||||
process_group_id = op.attr("ring_id")
|
||||
process_group = get_process_group(process_group_id, g_process_group_map)
|
||||
if rank not in process_group.ranks:
|
||||
return comm_requirements_to_ranks
|
||||
for tgt_rank in process_group.ranks:
|
||||
comm_volume = get_comm_volume(op, rank, tgt_rank)
|
||||
if comm_volume is not None:
|
||||
comm_requirements_to_ranks[tgt_rank] = {}
|
||||
comm_requirements_to_ranks[tgt_rank]["comm_volume"] = (
|
||||
comm_volume
|
||||
)
|
||||
elif is_p2p_comm_op(op):
|
||||
tgt_rank = op.attr("peer")
|
||||
comm_volume = get_comm_volume(op, rank, tgt_rank)
|
||||
if comm_volume is not None:
|
||||
comm_requirements_to_ranks[tgt_rank] = {}
|
||||
comm_requirements_to_ranks[tgt_rank]["comm_volume"] = comm_volume
|
||||
else:
|
||||
comm_requirements_to_ranks = {}
|
||||
return comm_requirements_to_ranks
|
||||
|
||||
|
||||
def analyze_requirements_for_program(src_info, rank):
|
||||
program = src_info[0]
|
||||
g_process_group_map = src_info[1]
|
||||
resource_requirements = {}
|
||||
comm_requirements_to_ranks = {}
|
||||
# only support device_type and only support GPU for now
|
||||
resource_requirements["device_type"] = DeviceType.GPU
|
||||
for block in program.blocks:
|
||||
for op in block.ops:
|
||||
cur_comm_requirements_to_ranks = analyze_comm_requirements_from_op(
|
||||
op, rank, g_process_group_map
|
||||
)
|
||||
for tgt_rank, link_info in cur_comm_requirements_to_ranks.items():
|
||||
if tgt_rank in comm_requirements_to_ranks:
|
||||
comm_requirements_to_ranks[tgt_rank]["comm_volume"] += (
|
||||
link_info["comm_volume"]
|
||||
)
|
||||
else:
|
||||
comm_requirements_to_ranks[tgt_rank] = {}
|
||||
comm_requirements_to_ranks[tgt_rank]["comm_volume"] = (
|
||||
link_info["comm_volume"]
|
||||
)
|
||||
return resource_requirements, comm_requirements_to_ranks
|
||||
|
||||
|
||||
def build_process_graph(distributed_program):
|
||||
graph = Graph()
|
||||
for src_rank, src_info in distributed_program.items():
|
||||
(
|
||||
resource_requirements,
|
||||
comm_requirements_to_ranks,
|
||||
) = analyze_requirements_for_program(src_info, src_rank)
|
||||
graph.add_node(src_rank, resource_requirements=resource_requirements)
|
||||
for tgt_rank, comm_requirements in comm_requirements_to_ranks.items():
|
||||
graph.add_edge(
|
||||
src_rank, tgt_rank, comm_requirements=comm_requirements
|
||||
)
|
||||
return graph
|
||||
|
||||
|
||||
def build_cluster_graph(cluster):
|
||||
graph = Graph()
|
||||
cuda_visible_devices_env = os.getenv("CUDA_VISIBLE_DEVICES")
|
||||
cuda_visible_devices = []
|
||||
if cuda_visible_devices_env is not None and cuda_visible_devices_env != "":
|
||||
cuda_visible_devices = [
|
||||
int(d.strip()) for d in cuda_visible_devices_env.split(",")
|
||||
]
|
||||
for machine in cluster.machines.values():
|
||||
for device in machine.devices.values():
|
||||
graph.add_node(device.global_id, device=device)
|
||||
if (
|
||||
cuda_visible_devices
|
||||
and device.local_id not in cuda_visible_devices
|
||||
):
|
||||
graph.nodes[device.global_id]["occupied"] = True
|
||||
else:
|
||||
graph.nodes[device.global_id]["occupied"] = False
|
||||
for link in machine.links.values():
|
||||
graph.add_edge(
|
||||
link.source.global_id, link.target.global_id, link=link
|
||||
)
|
||||
return graph
|
||||
|
||||
|
||||
def mapping(distributed_program, cluster):
|
||||
# A very simple mapping algorithm only for GPUs.
|
||||
# Here we assume one process will be mapped to one GPU.
|
||||
# In the future, more mapping configurations and algorithms will be supported.
|
||||
process_graph = build_process_graph(distributed_program)
|
||||
|
||||
cluster_graph = build_cluster_graph(cluster)
|
||||
|
||||
for cur_rank_node in process_graph:
|
||||
cur_rank_node["visited"] = False
|
||||
|
||||
def sort_by_comm_volume(rank_edge):
|
||||
return rank_edge["comm_requirements"]["comm_volume"]
|
||||
|
||||
def sort_by_comm_bandwidth(device_edge):
|
||||
return device_edge["link"].bandwidth
|
||||
|
||||
def select_unvisited_rank_node(rank_node_list):
|
||||
selected_rank_node = None
|
||||
for rank_node in rank_node_list:
|
||||
if rank_node["visited"] is False:
|
||||
selected_rank_node = rank_node
|
||||
return selected_rank_node
|
||||
|
||||
queue = deque()
|
||||
root_rank_node = select_unvisited_rank_node(
|
||||
list(process_graph.nodes.values())
|
||||
)
|
||||
while root_rank_node is not None:
|
||||
queue.append(root_rank_node)
|
||||
while queue:
|
||||
cur_rank_node = queue.popleft()
|
||||
if cur_rank_node["visited"]:
|
||||
continue
|
||||
device_type = cur_rank_node["resource_requirements"]["device_type"]
|
||||
cur_device_node = None
|
||||
for device_node in cluster_graph.nodes.values():
|
||||
if (device_node["device"].type == device_type) and (
|
||||
not device_node["occupied"]
|
||||
):
|
||||
device_node["occupied"] = True
|
||||
cur_rank_node["visited"] = True
|
||||
cur_rank_node["device"] = device_node["device"]
|
||||
cur_device_node = device_node
|
||||
break
|
||||
assert cur_device_node, (
|
||||
"Cannot find a device to satisfy the requirement."
|
||||
)
|
||||
|
||||
nbr_rank_edges = []
|
||||
for nbr_rank_node_id, nbr_rank_edge in process_graph.adjs[
|
||||
cur_rank_node.id
|
||||
].items():
|
||||
assert (
|
||||
nbr_rank_edge.src_id == cur_rank_node.id
|
||||
and nbr_rank_edge.tgt_id == nbr_rank_node_id
|
||||
)
|
||||
queue.append(process_graph.nodes[nbr_rank_node_id])
|
||||
nbr_rank_edges.append(nbr_rank_edge)
|
||||
nbr_rank_edges.sort(key=sort_by_comm_volume)
|
||||
|
||||
nbr_device_edges = []
|
||||
for nbr_device_edge in cluster_graph.adjs[
|
||||
cur_device_node.id
|
||||
].values():
|
||||
nbr_device_edges.append(nbr_device_edge)
|
||||
nbr_device_edges.sort(key=sort_by_comm_bandwidth)
|
||||
|
||||
for nbr_rank_edge in nbr_rank_edges:
|
||||
src_rank_node = process_graph.nodes[nbr_rank_edge.src_id][
|
||||
"visited"
|
||||
]
|
||||
if src_rank_node:
|
||||
continue
|
||||
device_type = src_rank_node["resource_requirements"][
|
||||
"device_type"
|
||||
]
|
||||
nbr_rank_node = process_graph.nodes[nbr_rank_edge.tgt_id]
|
||||
for nbr_device_edge in nbr_device_edges:
|
||||
nbr_device_node = cluster_graph.nodes[
|
||||
nbr_device_edge.tgt_id
|
||||
]
|
||||
if (nbr_device_node["device"].type == device_type) and (
|
||||
not nbr_device_node["occupied"]
|
||||
):
|
||||
nbr_device_node["occupied"] = True
|
||||
nbr_rank_node["visited"] = True
|
||||
nbr_rank_node["device"] = nbr_device_node["device"]
|
||||
break
|
||||
root_rank_node = select_unvisited_rank_node(
|
||||
list(process_graph.nodes.values())
|
||||
)
|
||||
|
||||
rank_mapping = {}
|
||||
for rank, rank_node in process_graph.nodes.items():
|
||||
device = rank_node["device"]
|
||||
machine = device.machine
|
||||
if machine.id in rank_mapping:
|
||||
rank_mapping[machine.id]["hostname"] = machine.hostname
|
||||
rank_mapping[machine.id]["addr"] = machine.addr
|
||||
rank_mapping[machine.id]["port"] = machine.port
|
||||
if rank not in rank_mapping[machine.id]["ranks"]:
|
||||
rank_mapping[machine.id]["ranks"][rank] = []
|
||||
rank_mapping[machine.id]["ranks"][rank].append(device.local_id)
|
||||
else:
|
||||
rank_mapping[machine.id]["ranks"][rank].append(device.local_id)
|
||||
else:
|
||||
rank_mapping[machine.id] = {}
|
||||
rank_mapping[machine.id]["hostname"] = machine.hostname
|
||||
rank_mapping[machine.id]["addr"] = machine.addr
|
||||
rank_mapping[machine.id]["port"] = machine.port
|
||||
rank_mapping[machine.id]["ranks"] = {}
|
||||
rank_mapping[machine.id]["ranks"][rank] = []
|
||||
rank_mapping[machine.id]["ranks"][rank].append(device.local_id)
|
||||
for machine_mapping in rank_mapping.values():
|
||||
for rank_devices in machine_mapping["ranks"].values():
|
||||
rank_devices.sort()
|
||||
|
||||
return rank_mapping
|
||||
@@ -0,0 +1,138 @@
|
||||
# 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.
|
||||
|
||||
import paddle
|
||||
|
||||
from .reshard_funcs.base_reshard_func import is_replicated
|
||||
from .utils import _complete_op_dist_attr
|
||||
|
||||
dist_skip_op_list = [
|
||||
"builtin.combine",
|
||||
"builtin.split",
|
||||
"cf.yield",
|
||||
"cf.tuple_push",
|
||||
"cf.tuple_pop",
|
||||
"cf.stack_create",
|
||||
"pd_op.pylayer",
|
||||
]
|
||||
|
||||
|
||||
def verify_dist_block(block):
|
||||
for op in block.ops:
|
||||
if op.name() in dist_skip_op_list:
|
||||
continue
|
||||
if op.name() == "dist_op.shard_tensor":
|
||||
raise RuntimeError("Block still contain shard_tensor_op.")
|
||||
# Note (luchang): Temp fix, remove unused parameter 'op'.
|
||||
# Will be removed in the future.
|
||||
if op.name() == "builtin.parameter":
|
||||
if op.result(0).use_empty():
|
||||
op.erase()
|
||||
continue
|
||||
|
||||
|
||||
def apply_mix2dist_pass(program, block=None):
|
||||
if block is None:
|
||||
block = program.global_block()
|
||||
deleted_ops = []
|
||||
for op in block.ops:
|
||||
for inner_block in op.blocks():
|
||||
apply_mix2dist_pass(program, block=inner_block)
|
||||
if op.name() != "dist_op.shard_tensor":
|
||||
continue
|
||||
shard_operand_value = op.operand_source(0)
|
||||
if not shard_operand_value.has_one_use():
|
||||
raise RuntimeError(
|
||||
f"shard_tensor is supposed to be called right after tensor is created, the use_count of tensor to be sharded is {shard_operand_value.use_count}, which is "
|
||||
"not Supported in right now."
|
||||
)
|
||||
shard_result_value = op.result(0)
|
||||
shard_result_value.replace_all_uses_with(shard_operand_value)
|
||||
deleted_ops.append(op)
|
||||
prev_op = shard_operand_value.get_defining_op()
|
||||
if (
|
||||
prev_op.name() == "builtin.parameter"
|
||||
or prev_op.name() == "pd_op.data"
|
||||
):
|
||||
prev_op.dist_attr = op.dist_attr
|
||||
shard_operand_value.set_type(shard_result_value.type())
|
||||
shard_operand_value.stop_gradient = shard_result_value.stop_gradient
|
||||
shard_operand_value.persistable = shard_result_value.persistable
|
||||
elif (
|
||||
prev_op.name() == "pd_op.randint"
|
||||
or prev_op.name() == "pd_op.gaussian"
|
||||
):
|
||||
mesh = shard_result_value.dist_attr().process_mesh
|
||||
# input
|
||||
shape_value = prev_op.operand_source(0)
|
||||
dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
mesh, [-1 for _ in range(len(shape_value.shape))], {}
|
||||
)
|
||||
shape_value.update_dist_attr(dist_attr)
|
||||
# op
|
||||
prev_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
mesh, [dist_attr], [shard_result_value.dist_attr()]
|
||||
)
|
||||
)
|
||||
# deal with full_int_array op
|
||||
prev_prev_op = shape_value.get_defining_op()
|
||||
prev_prev_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
mesh, [], [dist_attr]
|
||||
)
|
||||
)
|
||||
# output
|
||||
shard_operand_value.set_type(shard_result_value.type())
|
||||
shard_operand_value.stop_gradient = shard_result_value.stop_gradient
|
||||
shard_operand_value.persistable = shard_result_value.persistable
|
||||
else:
|
||||
dist_attr = shard_result_value.dist_attr()
|
||||
if not is_replicated(dist_attr):
|
||||
raise RuntimeError(
|
||||
f"{prev_op} is not support sharded by shard_tensor op in pir mode."
|
||||
)
|
||||
mesh = dist_attr.process_mesh
|
||||
ops_list = [prev_op]
|
||||
while len(ops_list) != 0:
|
||||
cur_op = ops_list.pop()
|
||||
if cur_op.dist_attr is not None:
|
||||
continue
|
||||
operand_attrs = []
|
||||
result_attrs = []
|
||||
for input in cur_op.operands_source():
|
||||
dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
mesh, [-1 for _ in range(len(input.shape))], {}
|
||||
)
|
||||
)
|
||||
operand_attrs.append(dist_attr)
|
||||
ops_list.append(input.get_defining_op())
|
||||
for result in cur_op.results():
|
||||
dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
mesh, [-1 for _ in range(len(result.shape))], {}
|
||||
)
|
||||
)
|
||||
result.update_dist_attr(dist_attr)
|
||||
result_attrs.append(dist_attr)
|
||||
cur_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
mesh, operand_attrs, result_attrs
|
||||
)
|
||||
)
|
||||
for op in deleted_ops:
|
||||
op.erase()
|
||||
_complete_op_dist_attr(program, block=block)
|
||||
verify_dist_block(block)
|
||||
@@ -0,0 +1,61 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
import os
|
||||
|
||||
from . import ( # noqa: F401
|
||||
dist_assign,
|
||||
dist_check_finite_and_unscale,
|
||||
dist_concat,
|
||||
dist_default,
|
||||
dist_dropout,
|
||||
dist_eltwise,
|
||||
dist_embedding,
|
||||
dist_expand_as,
|
||||
dist_fill_constant_batch_size_like,
|
||||
dist_flash_attn,
|
||||
dist_fused_attention,
|
||||
dist_fused_dropout_add,
|
||||
dist_fused_feedforward,
|
||||
dist_fused_rms_norm,
|
||||
dist_fused_rope,
|
||||
dist_gather_nd,
|
||||
dist_layer_norm,
|
||||
dist_matmul,
|
||||
dist_pnorm,
|
||||
dist_reduce_sum_p,
|
||||
dist_reshape,
|
||||
dist_scale,
|
||||
dist_shape,
|
||||
dist_slice,
|
||||
dist_softmax,
|
||||
dist_split,
|
||||
dist_stack,
|
||||
dist_strided_slice,
|
||||
dist_tile,
|
||||
dist_transpose,
|
||||
dist_unsqueeze2,
|
||||
dist_update_loss_scaling,
|
||||
)
|
||||
from .common import ( # noqa: F401
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
find_compatible_distributed_operator_impls,
|
||||
find_distributed_operator_impl_container,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
|
||||
parallel_ce = os.getenv("PARALLEL_CROSS_ENTROPY")
|
||||
if parallel_ce == "true":
|
||||
from . import dist_cross_entropy # noqa: F401
|
||||
@@ -0,0 +1,838 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import abc
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.base.log_helper import get_logger
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_dims_mapping,
|
||||
is_optimize_op,
|
||||
set_dist_op_desc_original_id,
|
||||
)
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
_g_distributed_operator_impl_containers = {}
|
||||
|
||||
_g_elementwise_ops = [
|
||||
"assign",
|
||||
"elementwise",
|
||||
"gelu",
|
||||
# "dropout",
|
||||
"scale",
|
||||
"relu",
|
||||
"cast",
|
||||
# "gather",
|
||||
# "concat",
|
||||
"silu",
|
||||
"fused_softmax_mask_upper_triangle",
|
||||
]
|
||||
BACKWARD_ONLY_DIST_OPS = {'check_finite_and_unscale', 'update_loss_scaling'}
|
||||
|
||||
_gradient_sync_by_partial_ops = [
|
||||
"matmul_v2_grad",
|
||||
"elementwise_add_grad",
|
||||
"layer_norm_grad",
|
||||
"lookup_table_v2_grad",
|
||||
# "conv",
|
||||
]
|
||||
|
||||
|
||||
class ParallelMode:
|
||||
"""
|
||||
the parallel mode for communication or auxiliary operator
|
||||
"""
|
||||
|
||||
DataParallel = "auto_parallel/data_parallel"
|
||||
TensorParallel = "auto_parallel/tensor_parallel"
|
||||
PipelineParallel = "auto_parallel/pipeline_parallel"
|
||||
MoEParallel = "auto_parallel/moe_parallel"
|
||||
|
||||
|
||||
class SyncMode:
|
||||
"""
|
||||
the synchronization mode for communication or auxiliary operator
|
||||
"""
|
||||
|
||||
AmpFlagSync = "auto_parallel/amp_flag_synchronization"
|
||||
GlobalNormSync = "auto_parallel/global_norm_synchronization"
|
||||
|
||||
|
||||
def is_elementwise_op(op_type):
|
||||
if op_type in _g_elementwise_ops:
|
||||
return True
|
||||
if "elementwise" in op_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DistributedOperatorImplContainer(abc.ABC):
|
||||
def __init__(self, op_type):
|
||||
self._type = op_type
|
||||
self._impls = []
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@type.setter
|
||||
def type(self, op_type):
|
||||
self._type = op_type
|
||||
|
||||
@property
|
||||
def impls(self):
|
||||
return self._impls
|
||||
|
||||
def register_impl(self, dist_impl):
|
||||
assert self.type == dist_impl.type, (
|
||||
"Op type of container must be same as that of the implementation."
|
||||
)
|
||||
impl_idx = len(self.impls)
|
||||
dist_impl.idx = impl_idx
|
||||
self._impls.append(dist_impl)
|
||||
|
||||
def get_impl(self, impl_idx):
|
||||
return self._impls[impl_idx]
|
||||
|
||||
def get_input_compatible_impls(self, dist_op):
|
||||
compatible_impls = []
|
||||
for impl in self.impls:
|
||||
if impl.is_input_compatible(dist_op):
|
||||
compatible_impls.append(impl)
|
||||
return compatible_impls
|
||||
|
||||
def get_output_compatible_impls(self, dist_op):
|
||||
compatible_impls = []
|
||||
for impl in self.impls:
|
||||
if impl.is_output_compatible(dist_op):
|
||||
compatible_impls.append(impl)
|
||||
return compatible_impls
|
||||
|
||||
def get_compatible_impls(self, dist_op):
|
||||
compatible_impls = []
|
||||
for impl in self.impls:
|
||||
if impl.is_auto_compatible(dist_op):
|
||||
compatible_impls.append(impl)
|
||||
return compatible_impls
|
||||
|
||||
# (NOTE) Currently, both DistributedOperatorImplContainer and DistributedOperatorImpl have update_dims_mapping method.
|
||||
# But this method is supposed to be maintained by DistributedOperatorImplContainer, and we are ongoing adding method
|
||||
# to DistributedOperatorImplContainer and removing those in DistributedOperatorImpl.
|
||||
# @abc.abstractmethod
|
||||
def update_dims_mapping(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
# (NOTE) Currently we has limited DistributedOperatorImpls for an op to deal with different parallel patterns of this op.
|
||||
# This function help to choose the correct DistributedOperatorImpl based on the result from InferSPMD.
|
||||
# @abc.abstractmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
|
||||
class DistributedOperatorImpl(abc.ABC):
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._type = None
|
||||
self._idx = None
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name):
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@type.setter
|
||||
def type(self, op_type):
|
||||
self._type = op_type
|
||||
|
||||
@property
|
||||
def idx(self):
|
||||
return self._idx
|
||||
|
||||
@idx.setter
|
||||
def idx(self, impl_idx):
|
||||
self._idx = impl_idx
|
||||
|
||||
# to be deprecated
|
||||
@abc.abstractmethod
|
||||
def is_input_compatible(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
# to be deprecated
|
||||
@abc.abstractmethod
|
||||
def is_output_compatible(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
# to be deprecated
|
||||
@abc.abstractmethod
|
||||
def is_auto_compatible(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def forward(dist_ctx, *args, **kwargs):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def backward(dist_ctx, *grad_outputs, **kwargs):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
# to be deprecated
|
||||
def update_dims_mapping(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
|
||||
def register_distributed_operator_impl_container(container):
|
||||
global _g_distributed_operator_impl_containers
|
||||
_g_distributed_operator_impl_containers[container.type] = container
|
||||
|
||||
|
||||
def get_distributed_operator_impl_container(op_type):
|
||||
global _g_distributed_operator_impl_containers
|
||||
return _g_distributed_operator_impl_containers.get(op_type, None)
|
||||
|
||||
|
||||
def register_distributed_operator_impl(op_type, dist_impl):
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(op_type)
|
||||
if dist_op_impl_container is not None:
|
||||
dist_impl.type = op_type
|
||||
dist_op_impl_container.register_impl(dist_impl)
|
||||
else:
|
||||
raise AssertionError(
|
||||
"Must register distributed operator registry first."
|
||||
)
|
||||
|
||||
|
||||
def find_compatible_distributed_operator_impls(dist_op, fwd=True, partial=True):
|
||||
"""
|
||||
Here just return the first compatible implementation.
|
||||
This will be improved by cost model in the future.
|
||||
"""
|
||||
op_type = dist_op.serial_op.type
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(op_type)
|
||||
dist_op_eltwise_impl_container = get_distributed_operator_impl_container(
|
||||
"elementwise"
|
||||
)
|
||||
dist_op_default_impl_container = get_distributed_operator_impl_container(
|
||||
"default"
|
||||
)
|
||||
compatible_impls = []
|
||||
if partial:
|
||||
if fwd:
|
||||
# First, find impls in the corresponding container
|
||||
if dist_op_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_impl_container.get_input_compatible_impls(dist_op)
|
||||
)
|
||||
# Second, find impls in the elementwise container
|
||||
if dist_op_eltwise_impl_container and is_elementwise_op(op_type):
|
||||
compatible_impls.extend(
|
||||
dist_op_eltwise_impl_container.get_input_compatible_impls(
|
||||
dist_op
|
||||
)
|
||||
)
|
||||
# Third, find impls in the default container
|
||||
if dist_op_default_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_default_impl_container.get_input_compatible_impls(
|
||||
dist_op
|
||||
)
|
||||
)
|
||||
else:
|
||||
# First, find impls in the corresponding container
|
||||
if dist_op_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_impl_container.get_output_compatible_impls(dist_op)
|
||||
)
|
||||
# Second, find impls in the elementwise container
|
||||
if dist_op_eltwise_impl_container and is_elementwise_op(op_type):
|
||||
compatible_impls.extend(
|
||||
dist_op_eltwise_impl_container.get_output_compatible_impls(
|
||||
dist_op
|
||||
)
|
||||
)
|
||||
# Third, find impls in the default container
|
||||
if dist_op_default_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_default_impl_container.get_output_compatible_impls(
|
||||
dist_op
|
||||
)
|
||||
)
|
||||
else:
|
||||
# First, find impls in the corresponding container
|
||||
if dist_op_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_impl_container.get_compatible_impls(dist_op)
|
||||
)
|
||||
# Second, find impls in the elementwise container
|
||||
if dist_op_eltwise_impl_container and is_elementwise_op(op_type):
|
||||
compatible_impls.extend(
|
||||
dist_op_eltwise_impl_container.get_compatible_impls(dist_op)
|
||||
)
|
||||
# Third, find impls in the default container
|
||||
if dist_op_default_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_default_impl_container.get_compatible_impls(dist_op)
|
||||
)
|
||||
|
||||
if compatible_impls:
|
||||
# For now, just return the first compatible impl
|
||||
# best_compatible_impl = compatible_impls[0]
|
||||
best_compatible_impl = compatible_impls
|
||||
else:
|
||||
best_compatible_impl = None
|
||||
return best_compatible_impl
|
||||
|
||||
|
||||
def find_distributed_operator_impl_container(dist_op):
|
||||
"""
|
||||
Return a unique container for dist op.
|
||||
If not specific container found, default container will be return.
|
||||
"""
|
||||
op_type = dist_op.serial_op.type
|
||||
|
||||
# Op has a match container
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(op_type)
|
||||
if dist_op_impl_container is None:
|
||||
# if op is register to elemwise spmd rule and has NO specific container implemented
|
||||
if is_elementwise_op(op_type):
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(
|
||||
"elementwise"
|
||||
)
|
||||
# default container for all bottom line cases
|
||||
else:
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(
|
||||
"default"
|
||||
)
|
||||
|
||||
_logger.debug(
|
||||
f"Op [{op_type}] Complete DistAttr using {type(dist_op_impl_container).__name__}"
|
||||
)
|
||||
return dist_op_impl_container
|
||||
|
||||
|
||||
def is_parameter_related(varname, block, dist_context=None):
|
||||
# TODO(zhaoyingli): maintain a dict in dist_context to record all variables which are be renamed
|
||||
if ".subprog_" in varname:
|
||||
varname = varname[: varname.index(".subprog_")]
|
||||
if ".cast_fp" in varname:
|
||||
varname = varname[: varname.index(".cast_fp")]
|
||||
if ".cast_bf" in varname:
|
||||
varname = varname[: varname.index(".cast_bf")]
|
||||
if ".quantized" in varname:
|
||||
varname = varname[: varname.index(".quantized")]
|
||||
assert block._find_var_recursive(varname), (
|
||||
f"cannot find var {varname} in cur block"
|
||||
)
|
||||
var = block._var_recursive(varname)
|
||||
# NOTE(hack method): to find the param which is resharded
|
||||
if dist_context and "@RESHARD" in varname:
|
||||
varname = varname[: varname.index("@RESHARD")]
|
||||
serial_program = dist_context.serial_main_program
|
||||
var = serial_program.global_block()._find_var_recursive(varname)
|
||||
if var is None:
|
||||
return False
|
||||
# NOTE(liym27): when Y_var is not a parameter, but Y_var is resharded by a parameter.
|
||||
elif "reshard_api" in varname:
|
||||
for op in block.ops:
|
||||
if op.type == "assign" and varname in op.output("Out"):
|
||||
in_varname = op.input("X")[0]
|
||||
var = block._find_var_recursive(in_varname)
|
||||
if var is not None and var.is_parameter:
|
||||
return True
|
||||
return var.is_parameter
|
||||
|
||||
|
||||
def infer_shape(block, src_var, src_var_dist_attr, op_input_dist_attr):
|
||||
var_shape = block._var_recursive(src_var.name).shape
|
||||
var_topology = src_var_dist_attr.process_mesh.shape
|
||||
var_dims_mapping = src_var_dist_attr.dims_mapping
|
||||
|
||||
complete_shape = []
|
||||
for idx, shape in enumerate(var_shape):
|
||||
if var_dims_mapping[idx] == -1:
|
||||
complete_shape.append(shape)
|
||||
else:
|
||||
new_shape = shape * var_topology[var_dims_mapping[idx]]
|
||||
complete_shape.append(new_shape)
|
||||
|
||||
exact_shape = []
|
||||
input_topology = op_input_dist_attr.process_mesh.shape
|
||||
input_dims_mapping = op_input_dist_attr.dims_mapping
|
||||
for idx, shape in enumerate(complete_shape):
|
||||
if input_dims_mapping[idx] == -1:
|
||||
exact_shape.append(shape)
|
||||
else:
|
||||
new_shape = shape // input_topology[input_dims_mapping[idx]]
|
||||
exact_shape.append(new_shape)
|
||||
|
||||
return exact_shape
|
||||
|
||||
|
||||
def set_comm_op_dist_attr_for_program(
|
||||
new_op, process_mesh, tensor_dist_attr, ctx, **kwargs
|
||||
):
|
||||
assert process_mesh is not None
|
||||
assert tensor_dist_attr is not None
|
||||
|
||||
new_op_dist_attr = OperatorDistAttr()
|
||||
new_op_dist_attr.process_mesh = process_mesh
|
||||
if "chunk_id" in kwargs:
|
||||
new_op_dist_attr.chunk_id = kwargs["chunk_id"]
|
||||
for input_varname in new_op.desc.input_arg_names():
|
||||
new_op_dist_attr.set_input_dist_attr(input_varname, tensor_dist_attr)
|
||||
for output_varname in new_op.desc.output_arg_names():
|
||||
new_op_dist_attr.set_output_dist_attr(output_varname, tensor_dist_attr)
|
||||
ctx.set_op_dist_attr_for_program(new_op, new_op_dist_attr)
|
||||
|
||||
|
||||
def naive_copy_op_dist_attr_for_program(new_op, ref_op, ctx):
|
||||
ref_dist_attr = ctx.get_op_dist_attr_for_program(ref_op)
|
||||
new_op_dist_attr = OperatorDistAttr()
|
||||
new_op_dist_attr.process_mesh = ref_dist_attr.process_mesh
|
||||
new_op_dist_attr.impl_type = ref_dist_attr.impl_type
|
||||
new_op_dist_attr.impl_idx = ref_dist_attr.impl_idx
|
||||
new_op_dist_attr.chunk_id = ref_dist_attr.chunk_id
|
||||
|
||||
for input_name in ref_op.input_names:
|
||||
assert input_name in new_op.input_names
|
||||
assert len(ref_op.input(input_name)) == 1
|
||||
assert len(new_op.input(input_name)) == 1
|
||||
|
||||
ref_tensor_dist_attr = ref_dist_attr.get_input_dist_attr(
|
||||
ref_op.input(input_name)[0]
|
||||
)
|
||||
new_op_dist_attr.set_input_dist_attr(
|
||||
new_op.input(input_name)[0], ref_tensor_dist_attr
|
||||
)
|
||||
|
||||
for output_name in ref_op.output_names:
|
||||
assert output_name in new_op.output_names
|
||||
assert len(ref_op.output(output_name)) == 1
|
||||
assert len(new_op.output(output_name)) == 1
|
||||
|
||||
ref_tensor_dist_attr = ref_dist_attr.get_output_dist_attr(
|
||||
ref_op.output(output_name)[0]
|
||||
)
|
||||
new_op_dist_attr.set_output_dist_attr(
|
||||
new_op.output(output_name)[0], ref_tensor_dist_attr
|
||||
)
|
||||
|
||||
ctx.set_op_dist_attr_for_program(new_op, new_op_dist_attr)
|
||||
|
||||
|
||||
def get_data_parallel_group(dist_ctx, op, act_grad_names, rank):
|
||||
"""
|
||||
deduce the data parallel communication group for current operator.
|
||||
|
||||
Args:
|
||||
dist_ctx (DistributedContext): dist context.
|
||||
op (Operator): the current (backward) operator which might need.
|
||||
act_grad_names (list): list of input activation grads variable name to the current operator.
|
||||
rank (int): global ranks index for current process.
|
||||
"""
|
||||
dp_group = None
|
||||
|
||||
op_dist_attr = dist_ctx.get_op_dist_attr_for_program(op)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
mesh_shape = process_mesh.shape
|
||||
# FIXME Hack for Pipeline Parallelism where the current operator
|
||||
# not belong to the mesh the current rank belong to.
|
||||
if rank not in process_mesh.process_ids:
|
||||
rank = _get_corresponding_rank(dist_ctx, process_mesh, rank)
|
||||
|
||||
for var_name in act_grad_names:
|
||||
var_dim_mapping = op_dist_attr.get_input_dims_mapping(var_name)
|
||||
# consider that the variable's shape is [], which is 0-D
|
||||
# TODO utilize the batch_dim attr instead of "0" in future
|
||||
batch_size_axis = var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
batch_size_axis,
|
||||
rank,
|
||||
)
|
||||
dp_group = new_process_group(group_ranks)
|
||||
break
|
||||
if dp_group is not None:
|
||||
return [dp_group]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def sync_and_scale_gradients(dist_ctx, op, groups, allreduce_var_names):
|
||||
"""
|
||||
insert the allreduce and scale ops for gradients of model
|
||||
parameters for operator in data parallelism.
|
||||
|
||||
Args:
|
||||
dist_ctx (DistributedContext): dist context.
|
||||
op (Operator): the current (backward) operator which might need.
|
||||
allreduce_var_names (list): list of the parameter's grads variable name in the current operator output.
|
||||
"""
|
||||
|
||||
op_dist_attr = dist_ctx.get_op_dist_attr_for_program(op)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
chunk_id = op_dist_attr.chunk_id
|
||||
dist_op_context = dist_ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
|
||||
reduce_type = dist.ReduceOp.SUM
|
||||
need_scale = dist_ctx.gradient_scale
|
||||
|
||||
for group in groups:
|
||||
group_size = len(group.ranks)
|
||||
|
||||
for var_name in allreduce_var_names:
|
||||
added_ops = []
|
||||
grad_var = main_block.var(var_name)
|
||||
allreduce_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': [grad_var]},
|
||||
outputs={'out': [grad_var]},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'reduce_type': reduce_type,
|
||||
OP_ROLE_KEY: OpRole.Backward,
|
||||
},
|
||||
)
|
||||
allreduce_op._set_attr(
|
||||
'op_namescope', '/' + ParallelMode.DataParallel
|
||||
)
|
||||
added_ops.append(allreduce_op)
|
||||
|
||||
if need_scale:
|
||||
scale_op = main_block.append_op(
|
||||
type='scale',
|
||||
inputs={'X': grad_var},
|
||||
outputs={'Out': grad_var},
|
||||
attrs={
|
||||
'scale': 1.0 / group_size,
|
||||
OP_ROLE_KEY: OpRole.Backward,
|
||||
},
|
||||
)
|
||||
scale_op._set_attr(
|
||||
'op_namescope', '/' + ParallelMode.DataParallel
|
||||
)
|
||||
added_ops.append(scale_op)
|
||||
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(grad_var.name)
|
||||
assert dims_mapping is not None, (
|
||||
f"Unexpected: dims_mapping of output [{grad_var.name}] of op [{op_dist_attr.op_type}] is None"
|
||||
)
|
||||
# NOTE auxiliary op's dist attr should follow dist_op not dist_tensor
|
||||
for new_op in added_ops:
|
||||
new_op_attr = OperatorDistAttr()
|
||||
new_op_attr.process_mesh = process_mesh
|
||||
new_op_attr.chunk_id = chunk_id
|
||||
new_op_attr.set_output_dims_mapping(grad_var.name, dims_mapping)
|
||||
new_op_attr.set_input_dims_mapping(grad_var.name, dims_mapping)
|
||||
dist_ctx.set_op_dist_attr_for_program(new_op, new_op_attr)
|
||||
|
||||
|
||||
def get_partial_groups(dist_ctx, op, out_grad_names, rank):
|
||||
"""
|
||||
deduce the partial communication group for current operator output vars.
|
||||
|
||||
Args:
|
||||
dist_ctx (DistributedContext): dist context.
|
||||
op (Operator): the current (backward) operator which might need.
|
||||
out_grad_names (list): list of the output parameter's grads variable name of the current operator.
|
||||
rank (int): global ranks index for current process.
|
||||
"""
|
||||
op_dist_attr = dist_ctx.get_op_dist_attr_for_program(op)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
mesh_shape = process_mesh.shape
|
||||
|
||||
groups = []
|
||||
|
||||
partial_dims = None
|
||||
for var_name in out_grad_names:
|
||||
var_dist_attr = op_dist_attr.get_output_dist_attr(var_name)
|
||||
if partial_dims is None:
|
||||
partial_dims = var_dist_attr._partial_dims()
|
||||
else:
|
||||
assert partial_dims == var_dist_attr._partial_dims(), (
|
||||
f"Partial dims of outputs {out_grad_names} of op [{op.type}] is not consistent"
|
||||
)
|
||||
|
||||
partial_dims = list(partial_dims)
|
||||
partial_dims.sort()
|
||||
|
||||
# FIXME Hack for Pipeline Parallelism where the current operator
|
||||
# not belong to the mesh the current rank belong to.
|
||||
if rank not in process_mesh.process_ids:
|
||||
rank = _get_corresponding_rank(dist_ctx, process_mesh, rank)
|
||||
|
||||
for dim in partial_dims:
|
||||
if mesh_shape[dim] > 1:
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
dim,
|
||||
rank,
|
||||
)
|
||||
groups.append(new_process_group(group_ranks))
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
def gradient_synchronization(
|
||||
dist_ctx, op, act_grad_names, out_grad_names, rank
|
||||
):
|
||||
"""
|
||||
conduct the allreduce and scaling for gradients of model
|
||||
parameters for operator in parallelism train.
|
||||
|
||||
Args:
|
||||
dist_ctx (DistributedContext): dist context.
|
||||
op (Operator): the current (backward) operator which might need.
|
||||
act_grad_names (list): list of input activation grads variable name to the current operator.
|
||||
out_grad_names (list): list of the output parameter's grads variable name of the current operator.
|
||||
rank (int): global ranks index for current process.
|
||||
"""
|
||||
|
||||
if not is_in_backward_phase(dist_ctx):
|
||||
return
|
||||
|
||||
if (
|
||||
is_optimize_op(op)
|
||||
or len(act_grad_names) == 0
|
||||
or len(out_grad_names) == 0
|
||||
):
|
||||
return
|
||||
|
||||
if op.type in _gradient_sync_by_partial_ops:
|
||||
sync_groups = get_partial_groups(dist_ctx, op, out_grad_names, rank)
|
||||
# NOTE we reverse the following old branch to support operators (e.g. fuse operators) that haven't been adopted for partial inferspmd,
|
||||
# and remove this branch after all operators are adopted for partial inferspmd.
|
||||
else:
|
||||
sync_groups = get_data_parallel_group(
|
||||
dist_ctx, op, act_grad_names, rank
|
||||
)
|
||||
|
||||
if len(sync_groups) < 1:
|
||||
return
|
||||
|
||||
sync_and_scale_gradients(dist_ctx, op, sync_groups, out_grad_names)
|
||||
|
||||
|
||||
def is_data_parallel_scale_op(op):
|
||||
return (
|
||||
op.type == "scale"
|
||||
and op.desc.has_attr("op_namescope")
|
||||
and ParallelMode.DataParallel in op.desc.attr("op_namescope")
|
||||
)
|
||||
|
||||
|
||||
def is_data_parallel_reduce_op(op):
|
||||
is_allreduce_op = op.type in [
|
||||
"c_allreduce_sum",
|
||||
"c_allreduce_avg",
|
||||
]
|
||||
is_all_reduce_op = op.type == "all_reduce" and op.desc.attr(
|
||||
"reduce_type"
|
||||
) in [
|
||||
dist.ReduceOp.SUM,
|
||||
dist.ReduceOp.AVG,
|
||||
]
|
||||
is_reduce_op = op.type == "reduce" and op.desc.attr("reduce_type") in [
|
||||
dist.ReduceOp.SUM,
|
||||
dist.ReduceOp.AVG,
|
||||
]
|
||||
return (
|
||||
(is_allreduce_op or is_all_reduce_op or is_reduce_op)
|
||||
and op.desc.has_attr("op_namescope")
|
||||
and ParallelMode.DataParallel in op.desc.attr("op_namescope")
|
||||
)
|
||||
|
||||
|
||||
def is_amp_flag_sync_op(op):
|
||||
return (
|
||||
op.type == "all_reduce"
|
||||
and op.desc.attr("op_type") == paddle.distributed.ReduceOp.MAX
|
||||
and op.desc.has_attr("op_namescope")
|
||||
and SyncMode.AmpFlagSync in op.desc.attr("op_namescope")
|
||||
)
|
||||
|
||||
|
||||
def is_global_norm_sync_op(op):
|
||||
return (
|
||||
op.type == "all_reduce"
|
||||
and op.desc.attr("reduce_type") == dist.ReduceOp.SUM
|
||||
and op.desc.has_attr("op_namescope")
|
||||
and SyncMode.GlobalNormSync in op.desc.attr("op_namescope")
|
||||
)
|
||||
|
||||
|
||||
def is_in_backward_phase(dist_ctx):
|
||||
# NOTE currently high-order differential in Paddle dose NOT distinguish gradient computation operators
|
||||
# in Forward phase and operators in Backward phase (both with op_role=1), which will mislead
|
||||
# auto parallel to add gradient synchronization for gradient computation operators in Forward phase.
|
||||
# we use this FLAG to distinguish these two phases temporarily.
|
||||
|
||||
return dist_ctx.dist_op_context.in_backward_phase()
|
||||
|
||||
|
||||
def merge_forward_backward_dims_mapping(fw_results, bw_results):
|
||||
flatten_fw_inputs = paddle.utils.flatten(fw_results[0])
|
||||
flatten_fw_outputs = paddle.utils.flatten(fw_results[1])
|
||||
flatten_bw_inputs = paddle.utils.flatten(bw_results[0])
|
||||
flatten_bw_outputs = paddle.utils.flatten(bw_results[1])
|
||||
ninputs = len(flatten_fw_inputs)
|
||||
noutputs = len(flatten_fw_outputs)
|
||||
inferred_input_dims_mappings = []
|
||||
inferred_output_dims_mappings = []
|
||||
|
||||
for i in range(ninputs):
|
||||
compatible_dims_mapping = compute_compatible_dims_mapping(
|
||||
[
|
||||
flatten_fw_inputs[i].dims_mapping,
|
||||
flatten_bw_inputs[i].dims_mapping,
|
||||
]
|
||||
)
|
||||
inferred_input_dims_mappings.append(compatible_dims_mapping)
|
||||
|
||||
for i in range(noutputs):
|
||||
compatible_dims_mapping = compute_compatible_dims_mapping(
|
||||
[
|
||||
flatten_fw_outputs[i].dims_mapping,
|
||||
flatten_bw_outputs[i].dims_mapping,
|
||||
]
|
||||
)
|
||||
inferred_output_dims_mappings.append(compatible_dims_mapping)
|
||||
return inferred_input_dims_mappings, inferred_output_dims_mappings
|
||||
|
||||
|
||||
def update_op_dims_mapping(
|
||||
dist_op, input_arg_names, output_arg_names, fw_results, bw_results
|
||||
):
|
||||
(
|
||||
inferred_input_dims_mappings,
|
||||
inferred_output_dims_mappings,
|
||||
) = merge_forward_backward_dims_mapping(fw_results, bw_results)
|
||||
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
changed = False
|
||||
if len(input_arg_names) != len(inferred_input_dims_mappings):
|
||||
warnings.warn(
|
||||
f"dims mapping is NOT Match, inferred [{len(inferred_input_dims_mappings)}], original: [{len(input_arg_names)}]; dist op: [{dist_op}]"
|
||||
)
|
||||
if len(output_arg_names) != len(inferred_output_dims_mappings):
|
||||
warnings.warn(
|
||||
f"dims mapping is NOT Match, inferred [{len(inferred_output_dims_mappings)}], original: [{len(output_arg_names)}]; dist op: [{dist_op}]"
|
||||
)
|
||||
|
||||
for i in range(len(input_arg_names)):
|
||||
original_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
input_arg_names[i]
|
||||
)
|
||||
inferred_dims_mapping = inferred_input_dims_mappings[i]
|
||||
if (inferred_dims_mapping is not None) and (
|
||||
original_dims_mapping != inferred_dims_mapping
|
||||
):
|
||||
_logger.debug(
|
||||
f"Changed: Op [{dist_op.serial_op.type}], name [{input_arg_names[i]}], Original [{original_dims_mapping}], Inferred [{inferred_dims_mapping}]"
|
||||
)
|
||||
changed = True
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
input_arg_names[i], inferred_dims_mapping
|
||||
)
|
||||
# TODO support partial for inputs
|
||||
|
||||
for i in range(len(output_arg_names)):
|
||||
original_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
output_arg_names[i]
|
||||
)
|
||||
inferred_dims_mapping = inferred_output_dims_mappings[i]
|
||||
if (inferred_dims_mapping is not None) and (
|
||||
original_dims_mapping != inferred_dims_mapping
|
||||
):
|
||||
_logger.debug(
|
||||
f"Changed: Op [{dist_op.serial_op.type}], name [{output_arg_names[i]}], Original [{original_dims_mapping}], Inferred [{inferred_dims_mapping}]"
|
||||
)
|
||||
changed = True
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
output_arg_names[i], inferred_dims_mapping
|
||||
)
|
||||
|
||||
# NOTE in partial stage-I, we infer partial for output in infer_forward only
|
||||
output_dist_attr = op_dist_attr.get_output_dist_attr(
|
||||
output_arg_names[i]
|
||||
)
|
||||
output_idx = output_arg_names.index(output_arg_names[i])
|
||||
if (
|
||||
fw_results[1][output_idx]._partial_dims()
|
||||
!= output_dist_attr._partial_dims()
|
||||
):
|
||||
# _logger.info(
|
||||
# "Changed: Op [{}], tensor name [{}], Original partial on [{}], Inferred partial on [{}]".format(
|
||||
# dist_op.serial_op.type,
|
||||
# output_arg_names[i],
|
||||
# output_dist_attr._partial_dims(),
|
||||
# fw_results[1][output_idx]._partial_dims(),
|
||||
# )
|
||||
# )
|
||||
output_dist_attr._clean_partial_status()
|
||||
output_dist_attr._set_partial_dims(
|
||||
list(fw_results[1][0]._partial_dims())
|
||||
)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def get_default_distributed_operator_impl():
|
||||
dist_op_default_impl_container = get_distributed_operator_impl_container(
|
||||
"default"
|
||||
)
|
||||
num_impls = len(dist_op_default_impl_container.impls)
|
||||
assert num_impls == 1, f"Default dist op has [{num_impls}] impls"
|
||||
return dist_op_default_impl_container.get_impl(0)
|
||||
|
||||
|
||||
def copy_op_without_infer_shape(src_op, block, ctx, varname_kwargs):
|
||||
new_op = block.append_op(type='nop')
|
||||
new_op_desc = new_op.desc
|
||||
new_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(new_op_desc, src_op.desc, ctx)
|
||||
for input_name in src_op.desc.input_names():
|
||||
new_op_desc.set_input(input_name, varname_kwargs[input_name])
|
||||
for output_name in src_op.desc.output_names():
|
||||
new_op_desc.set_output(output_name, varname_kwargs[output_name])
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
return new_op
|
||||
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..utils import compute_compatible_and_update_dim_mapping
|
||||
from .common import DistributedOperatorImpl, DistributedOperatorImplContainer
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedAssign(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
# TODO remove assign dist op
|
||||
# register_distributed_operator_impl_container(DistributedAssign("assign"))
|
||||
|
||||
|
||||
class DistributedAssignImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
# register_distributed_operator_impl("assign", DistributedAssignImpl("assign"))
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.auto_parallel.static.process_group import (
|
||||
get_world_process_group,
|
||||
)
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
from paddle.framework import core
|
||||
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import set_dist_op_desc_original_id, set_var_dist_attr
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
SyncMode,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
|
||||
world_process_group = get_world_process_group()
|
||||
|
||||
|
||||
class DistributedCheckFiniteAndUnscale(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedCheckFiniteAndUnscale("check_finite_and_unscale")
|
||||
)
|
||||
|
||||
|
||||
class DistributedCheckFiniteAndUnscaleImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._name = name
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's is_input_compatible should not be called !"
|
||||
)
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's is_output_compatible should not be called !"
|
||||
)
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's is_auto_compatible should not be called !"
|
||||
)
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's update_dims_mapping should not be called !"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's forward should not be called !"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# by now the backward function only insert the gradient allreduce for dist op itself
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.main_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
assert rank_id in dist_attr.process_mesh.process_ids
|
||||
|
||||
assert 'X' in kwargs, "input [{}] is not given".format('X')
|
||||
assert 'Scale' in kwargs, "input [{}] is not given".format('Scale')
|
||||
assert 'Out' in kwargs, "input [{}] is not given".format('Out')
|
||||
assert 'FoundInfinite' in kwargs, "output [{}] is not given".format(
|
||||
'FoundInfinite'
|
||||
)
|
||||
|
||||
assert len(kwargs['Scale']) == 1, (
|
||||
"check_finite_and_unscale input Scale take 1 variable but got {}".format(
|
||||
kwargs['Scale']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['FoundInfinite']) == 1, (
|
||||
"check_finite_and_unscale input FoundInfinite take 1 variable but got {}".format(
|
||||
kwargs['FoundInfinite']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['X']) == len(kwargs['Out']), (
|
||||
"check_finite_and_unscale got [{}] X and [{}] Out, which are supposed to be equal".format(
|
||||
len(kwargs['X']), len(kwargs['Out'])
|
||||
)
|
||||
)
|
||||
|
||||
filter_vars = []
|
||||
for varname in kwargs['X']:
|
||||
if (
|
||||
rank_id
|
||||
in ctx.get_tensor_dist_attr_for_program(
|
||||
main_block._var_recursive(varname)
|
||||
).process_mesh.process_ids
|
||||
):
|
||||
filter_vars.append(varname)
|
||||
|
||||
# replicate op in dist program
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(backward_op.desc)
|
||||
set_dist_op_desc_original_id(dist_op_desc, backward_op.desc, ctx)
|
||||
dist_op_desc.set_input('X', filter_vars)
|
||||
dist_op_desc.set_output('Out', filter_vars)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
# sync result
|
||||
group = new_process_group(world_process_group.ranks)
|
||||
|
||||
inf_var = main_block._var_recursive(kwargs['FoundInfinite'][0])
|
||||
inf_var_int32 = main_block.create_var(
|
||||
name=inf_var.name + "@cast_int32",
|
||||
shape=inf_var.shape,
|
||||
dtype=core.VarDesc.VarType.INT32,
|
||||
)
|
||||
set_var_dist_attr(
|
||||
ctx,
|
||||
inf_var_int32,
|
||||
ctx.get_tensor_dist_attr_for_program(inf_var).dims_mapping,
|
||||
ctx.get_tensor_dist_attr_for_program(inf_var).process_mesh,
|
||||
)
|
||||
cast_op1 = main_block.append_op(
|
||||
type='cast',
|
||||
inputs={'X': inf_var},
|
||||
outputs={'Out': inf_var_int32},
|
||||
attrs={
|
||||
"in_dtype": inf_var.dtype,
|
||||
"out_dtype": inf_var_int32.dtype,
|
||||
OP_ROLE_KEY: OpRole.Optimize,
|
||||
},
|
||||
)
|
||||
allreduce_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': inf_var_int32},
|
||||
outputs={'out': inf_var_int32},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'op_type': paddle.distributed.ReduceOp.MAX,
|
||||
OP_ROLE_KEY: OpRole.Optimize,
|
||||
},
|
||||
)
|
||||
allreduce_op._set_attr('op_namescope', '/' + SyncMode.AmpFlagSync)
|
||||
cast_op2 = main_block.append_op(
|
||||
type='cast',
|
||||
inputs={'X': inf_var_int32},
|
||||
outputs={'Out': inf_var},
|
||||
attrs={
|
||||
"in_dtype": inf_var_int32.dtype,
|
||||
"out_dtype": inf_var.dtype,
|
||||
OP_ROLE_KEY: OpRole.Optimize,
|
||||
},
|
||||
)
|
||||
|
||||
for op in [cast_op1, allreduce_op, cast_op2]:
|
||||
new_op_dist_attr = OperatorDistAttr()
|
||||
for varname in op.input_arg_names:
|
||||
var_dist_attr = ctx.get_tensor_dist_attr_for_program(
|
||||
main_block._var_recursive(varname)
|
||||
)
|
||||
assert var_dist_attr is not None
|
||||
new_op_dist_attr.set_input_dims_mapping(
|
||||
varname, var_dist_attr.dims_mapping
|
||||
)
|
||||
for varname in op.output_arg_names:
|
||||
var_dist_attr = ctx.get_tensor_dist_attr_for_program(
|
||||
main_block._var_recursive(varname)
|
||||
)
|
||||
new_op_dist_attr.set_output_dims_mapping(
|
||||
varname, var_dist_attr.dims_mapping
|
||||
)
|
||||
new_op_dist_attr.process_mesh = var_dist_attr.process_mesh
|
||||
ctx.set_op_dist_attr_for_program(op, new_op_dist_attr)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"check_finite_and_unscale",
|
||||
DistributedCheckFiniteAndUnscaleImpl("check_finite_and_unscale"),
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
# 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 ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedConcat(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
axis_tensor = op_desc.input('AxisTensor')
|
||||
assert len(axis_tensor) == 0, (
|
||||
"Please use axis attr instead of AxisTensor"
|
||||
)
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
input_specs = []
|
||||
for name in input_arg_names:
|
||||
input_specs.append(get_dist_tensor_spec(dist_op, name))
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_names[0], False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("concat")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(input_specs, axis)
|
||||
bw_results = rule.infer_backward(input_specs, output_spec, axis)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names,
|
||||
output_arg_names,
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedConcat("concat"))
|
||||
@@ -0,0 +1,528 @@
|
||||
# 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
|
||||
|
||||
import copy
|
||||
|
||||
from paddle.common_ops_import import check_variable_and_dtype
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
get_dist_tensor_spec,
|
||||
is_dim_shard,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
ParallelMode,
|
||||
copy_op_without_infer_shape,
|
||||
naive_copy_op_dist_attr_for_program,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedCrossEntropy(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
logits_name = op_desc.input('Logits')[0]
|
||||
label_name = op_desc.input('Label')[0]
|
||||
loss_name = op_desc.output('Loss')[0]
|
||||
softmax_name = op_desc.output('Softmax')[0]
|
||||
|
||||
soft_label = op_desc.attr('soft_label')
|
||||
ignore_index = op_desc.attr('ignore_index')
|
||||
numeric_stable_mode = op_desc.attr('numeric_stable_mode')
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
logits_spec = get_dist_tensor_spec(dist_op, logits_name)
|
||||
label_spec = get_dist_tensor_spec(dist_op, label_name)
|
||||
loss_spec = get_dist_tensor_spec(dist_op, loss_name, False)
|
||||
softmax_spec = get_dist_tensor_spec(dist_op, softmax_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("softmax_with_cross_entropy")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(
|
||||
logits_spec,
|
||||
label_spec,
|
||||
soft_label,
|
||||
True,
|
||||
numeric_stable_mode,
|
||||
ignore_index,
|
||||
axis,
|
||||
)
|
||||
bw_results = rule.infer_backward(
|
||||
logits_spec,
|
||||
label_spec,
|
||||
softmax_spec,
|
||||
loss_spec,
|
||||
soft_label,
|
||||
True,
|
||||
numeric_stable_mode,
|
||||
ignore_index,
|
||||
axis,
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[logits_name, label_name],
|
||||
[softmax_name, loss_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
op_dist_attr.impl_type = op_desc.type()
|
||||
|
||||
logits_name = op_desc.input('Logits')[0]
|
||||
|
||||
soft_label = op_desc.attr('soft_label')
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
logits_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_input_dims_mapping(logits_name)
|
||||
)
|
||||
logits_ndim = len(logits_dims_mapping)
|
||||
axis = axis + logits_ndim if axis < 0 else axis
|
||||
|
||||
if is_dim_shard(logits_dims_mapping[axis]):
|
||||
assert soft_label is False, (
|
||||
"parallel_cross_entropy does not support soft_label now."
|
||||
)
|
||||
assert axis == logits_ndim - 1, (
|
||||
"parallel_cross_entropy can only support shard on the last dim now."
|
||||
)
|
||||
op_dist_attr.impl_idx = 1
|
||||
else:
|
||||
op_dist_attr.impl_idx = 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedCrossEntropy("softmax_with_cross_entropy")
|
||||
)
|
||||
|
||||
|
||||
# The softmax_norm axis is not sharded
|
||||
class DistributedCrossEntropyImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Logits' in kwargs, "input [Logits] is not given"
|
||||
assert 'Label' in kwargs, "input [Label] is not given"
|
||||
assert 'Loss' in kwargs, "output [Loss] is not given"
|
||||
assert 'Softmax' in kwargs, "output [Softmax] is not given"
|
||||
|
||||
assert len(kwargs['Logits']) == 1, (
|
||||
"input [Logits] take 1 variable but got {}".format(kwargs['Logits'])
|
||||
)
|
||||
assert len(kwargs['Label']) == 1, (
|
||||
"input [Label] take 1 variable but got {}".format(kwargs['Label'])
|
||||
)
|
||||
|
||||
logits_var = main_block._var_recursive(kwargs['Logits'][0])
|
||||
label_var = main_block._var_recursive(kwargs['Label'][0])
|
||||
loss_var = main_block._var_recursive(kwargs['Loss'][0])
|
||||
softmax_var = main_block._var_recursive(kwargs['Softmax'][0])
|
||||
|
||||
# got dist attribute info
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh_group:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
check_variable_and_dtype(
|
||||
logits_var,
|
||||
'input',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'cross_entropy_with_softmax',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
label_var,
|
||||
'input',
|
||||
['int32', 'int64', 'float32', 'float64'],
|
||||
'cross_entropy_with_softmax',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
loss_var,
|
||||
'output',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'cross_entropy_with_softmax',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
softmax_var,
|
||||
'output',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'cross_entropy_with_softmax',
|
||||
)
|
||||
copy_op_without_infer_shape(src_op, main_block, ctx, kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Softmax' in kwargs, "input [Logits] is not given"
|
||||
assert 'Label' in kwargs, "input [Label] is not given"
|
||||
assert 'Loss@GRAD' in kwargs, "input [Loss@GRAD] is not given"
|
||||
assert 'Logits@GRAD' in kwargs, "output [Logits@GRAD] is not given"
|
||||
|
||||
assert len(kwargs['Softmax']) == 1, (
|
||||
"input [Softmax] take 1 variable but got {}".format(
|
||||
kwargs['Softmax']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Label']) == 1, (
|
||||
"input [Label] take 1 variable but got {}".format(kwargs['Label'])
|
||||
)
|
||||
assert len(kwargs['Loss@GRAD']) == 1, (
|
||||
"input [Loss@GRAD] take 1 variable but got {}".format(kwargs['Out'])
|
||||
)
|
||||
assert len(kwargs['Logits@GRAD']) == 1, (
|
||||
"output [Logits@GRAD] take 1 variable but got {}".format(
|
||||
kwargs['Logits@GRAD']
|
||||
)
|
||||
)
|
||||
# replicate op in dist program
|
||||
copy_op_without_infer_shape(backward_op, main_block, ctx, kwargs)
|
||||
|
||||
|
||||
class DistributedCrossEntropyImpl1(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Logits' in kwargs, "input [Logits] is not given"
|
||||
assert 'Label' in kwargs, "input [Label] is not given"
|
||||
assert 'Loss' in kwargs, "output [Loss] is not given"
|
||||
assert 'Softmax' in kwargs, "output [Softmax] is not given"
|
||||
|
||||
assert len(kwargs['Logits']) == 1, (
|
||||
"input [Logits] take 1 variable but got {}".format(kwargs['Logits'])
|
||||
)
|
||||
assert len(kwargs['Label']) == 1, (
|
||||
"input [Label] take 1 variable but got {}".format(kwargs['Label'])
|
||||
)
|
||||
|
||||
logits_var = main_block._var_recursive(kwargs['Logits'][0])
|
||||
label_var = main_block._var_recursive(kwargs['Label'][0])
|
||||
loss_var = main_block._var_recursive(kwargs['Loss'][0])
|
||||
softmax_var = main_block._var_recursive(kwargs['Softmax'][0])
|
||||
|
||||
# got dist attribute info
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh_group:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
check_variable_and_dtype(
|
||||
logits_var,
|
||||
'input',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'c_softmax_with_cross_entropy',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
label_var,
|
||||
'input',
|
||||
['int32', 'int64', 'float32', 'float64'],
|
||||
'c_softmax_with_cross_entropy',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
loss_var,
|
||||
'output',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'c_softmax_with_cross_entropy',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
softmax_var,
|
||||
'output',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'c_softmax_with_cross_entropy',
|
||||
)
|
||||
|
||||
# infer new var shape with op dist attr
|
||||
# the dims mapping in dist_op may be different from that in tensor
|
||||
# so we should
|
||||
loss_dist_attr = ctx.get_tensor_dist_attr_for_program(loss_var)
|
||||
assert loss_dist_attr is not None
|
||||
softmax_dist_attr = ctx.get_tensor_dist_attr_for_program(softmax_var)
|
||||
assert softmax_dist_attr is not None
|
||||
op_dist_attr_loss = op_dist_attr.get_output_dist_attr(loss_var.name)
|
||||
assert op_dist_attr_loss is not None
|
||||
op_dist_attr_softmax = op_dist_attr.get_output_dist_attr(
|
||||
softmax_var.name
|
||||
)
|
||||
assert op_dist_attr_softmax is not None
|
||||
|
||||
# TODO calculate ring id
|
||||
softmax_axis = src_op.desc.attr('axis')
|
||||
logits_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
logits_var.name
|
||||
)
|
||||
parallel_axis = logits_dims_mapping[softmax_axis]
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
c_cross_entropy_op = main_block.append_op(
|
||||
type='c_softmax_with_cross_entropy',
|
||||
inputs={
|
||||
'Logits': logits_var,
|
||||
'Label': label_var,
|
||||
},
|
||||
outputs={
|
||||
'Loss': loss_var,
|
||||
'Softmax': softmax_var,
|
||||
},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'rank': group.local_rank(rank_id),
|
||||
'nranks': group.nranks,
|
||||
'ignore_index': src_op.desc.attr('ignore_index'),
|
||||
OP_ROLE_KEY: src_op.attr('op_role'),
|
||||
},
|
||||
)
|
||||
naive_copy_op_dist_attr_for_program(c_cross_entropy_op, src_op, ctx)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Softmax' in kwargs, "input [Softmax] is not given"
|
||||
assert 'Label' in kwargs, "input [Label] is not given"
|
||||
assert 'Loss@GRAD' in kwargs, "input [Loss@GRAD] is not given"
|
||||
assert 'Logits@GRAD' in kwargs, "output [Logits@GRAD] is not given"
|
||||
|
||||
assert len(kwargs['Softmax']) == 1, (
|
||||
"input [Softmax] take 1 variable but got {}".format(
|
||||
kwargs['Softmax']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Label']) == 1, (
|
||||
"input [Label] take 1 variable but got {}".format(kwargs['Label'])
|
||||
)
|
||||
assert len(kwargs['Loss@GRAD']) == 1, (
|
||||
"input [Loss@GRAD] take 1 variable but got {}".format(
|
||||
kwargs['Loss@GRAD']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Logits@GRAD']) == 1, (
|
||||
"output [Logits@GRAD] take 1 variable but got {}".format(
|
||||
kwargs['Logits@GRAD']
|
||||
)
|
||||
)
|
||||
|
||||
# got dist attribute info
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh_group:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
for op in main_block.ops:
|
||||
# the output value of reduce_mean_grad is 1/numel, so when the
|
||||
# tensor is sharded, we should insert a scale op to make the
|
||||
# grad correct.
|
||||
if (
|
||||
op.type == "reduce_mean_grad"
|
||||
and kwargs['Loss@GRAD'][0] in op.output_arg_names
|
||||
):
|
||||
loss_grad_var = main_block._var_recursive(
|
||||
kwargs['Loss@GRAD'][0]
|
||||
)
|
||||
loss_grad_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
loss_grad_var.name
|
||||
)
|
||||
degree = 1.0
|
||||
for i in range(len(loss_grad_dims_mapping) - 1):
|
||||
if loss_grad_dims_mapping[i] != -1:
|
||||
degree *= process_mesh_shape[loss_grad_dims_mapping[i]]
|
||||
if degree > 1:
|
||||
scale_op = main_block.append_op(
|
||||
type='scale',
|
||||
inputs={'X': loss_grad_var},
|
||||
outputs={'Out': loss_grad_var},
|
||||
attrs={
|
||||
'scale': 1.0 / degree,
|
||||
OP_ROLE_KEY: OpRole.Backward,
|
||||
},
|
||||
)
|
||||
scale_op._set_attr(
|
||||
'op_namescope', '/' + ParallelMode.DataParallel
|
||||
)
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
loss_grad_var.name
|
||||
)
|
||||
scale_op_attr = OperatorDistAttr()
|
||||
scale_op_attr.process_mesh = op_dist_attr.process_mesh
|
||||
scale_op_attr.chunk_id = op_dist_attr.chunk_id
|
||||
scale_op_attr.set_output_dims_mapping(
|
||||
loss_grad_var.name, dims_mapping
|
||||
)
|
||||
scale_op_attr.set_input_dims_mapping(
|
||||
loss_grad_var.name, dims_mapping
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(scale_op, scale_op_attr)
|
||||
|
||||
# TODO calculate ring id
|
||||
softmax_axis = backward_op.desc.attr('axis')
|
||||
# softmax_dims_mapping is the same as logits_dims_mapping
|
||||
softmax_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
kwargs['Softmax'][0]
|
||||
)
|
||||
parallel_axis = softmax_dims_mapping[softmax_axis]
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
cross_entropy_grad_op_desc = main_block.append_op(type='nop').desc
|
||||
cross_entropy_grad_op_desc.set_type("c_softmax_with_cross_entropy_grad")
|
||||
cross_entropy_grad_op_desc.set_input('Softmax', [kwargs['Softmax'][0]])
|
||||
cross_entropy_grad_op_desc.set_input('Label', [kwargs['Label'][0]])
|
||||
cross_entropy_grad_op_desc.set_input(
|
||||
'Loss@GRAD', [kwargs['Loss@GRAD'][0]]
|
||||
)
|
||||
cross_entropy_grad_op_desc.set_output(
|
||||
'Logits@GRAD', [kwargs['Logits@GRAD'][0]]
|
||||
)
|
||||
|
||||
ignore_index = backward_op.desc.attr('ignore_index')
|
||||
# the ignore_index attribute in c_cross_entropy_grad kernel
|
||||
# is int64_t type, so we should set this attribute with
|
||||
# _set_int64_attr. Otherwise ignore_index will be int32 type,
|
||||
# causing an error.
|
||||
cross_entropy_grad_op_desc._set_int64_attr('ignore_index', ignore_index)
|
||||
cross_entropy_grad_op_desc._set_attr('ring_id', group.id)
|
||||
cross_entropy_grad_op_desc._set_attr('rank', group.local_rank(rank_id))
|
||||
cross_entropy_grad_op_desc._set_attr('nranks', group.nranks)
|
||||
cross_entropy_grad_op_desc._set_attr(OP_ROLE_KEY, OpRole.Backward)
|
||||
|
||||
cross_entropy_grad_op = main_block.ops[-1]
|
||||
naive_copy_op_dist_attr_for_program(
|
||||
cross_entropy_grad_op, backward_op, ctx
|
||||
)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"softmax_with_cross_entropy", DistributedCrossEntropyImpl0("cross_entropy")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"softmax_with_cross_entropy",
|
||||
DistributedCrossEntropyImpl1("c_cross_entropy"),
|
||||
)
|
||||
@@ -0,0 +1,681 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
|
||||
from ..completion import contains_spmd_rule, get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
_g_op_cost_factory,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..dist_attribute import DistTensorSpec, OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
is_prim_op,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
copy_op_without_infer_shape,
|
||||
get_default_distributed_operator_impl,
|
||||
gradient_synchronization,
|
||||
is_parameter_related,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
set_comm_op_dist_attr_for_program,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
__op_not_need_param_init__ = ["while", "cond"]
|
||||
__op_has_shape_attr__ = [
|
||||
"fill_constant_batch_size_like",
|
||||
"fill_constant",
|
||||
"expand_v2",
|
||||
"expand_as_v2",
|
||||
]
|
||||
|
||||
|
||||
def prim_operator_data_parallel_functor(ctx, src_op):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
|
||||
var_name = src_op.output_arg_names[0]
|
||||
if var_name in ctx.grads_params:
|
||||
assert var_name not in ctx.synced_gradient, (
|
||||
f"in primitive mode, grad is already {var_name} synced"
|
||||
)
|
||||
ctx.synced_gradient.add(var_name)
|
||||
sync_group = new_process_group(ctx.data_parallel_group)
|
||||
|
||||
allreduce_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': [var_name]},
|
||||
outputs={'out': [var_name]},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'reduce_type': paddle.distributed.ReduceOp.SUM,
|
||||
OP_ROLE_KEY: OpRole.Backward,
|
||||
},
|
||||
)
|
||||
|
||||
param = ctx.grads_params[var_name]
|
||||
startup_block = dist_op_context.startup_block
|
||||
new_op = startup_block.append_op(
|
||||
type='broadcast',
|
||||
inputs={'x': [param]},
|
||||
outputs={'out': [param]},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'root': 0,
|
||||
OP_ROLE_KEY: OpRole.Forward,
|
||||
},
|
||||
)
|
||||
|
||||
grad_var = main_block._var_recursive(var_name)
|
||||
dims_mapping = ctx.get_tensor_dist_attr_for_program(
|
||||
grad_var
|
||||
).dims_mapping
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
process_mesh = dist_attr.process_mesh
|
||||
op_attr = OperatorDistAttr()
|
||||
op_attr.process_mesh = process_mesh
|
||||
op_attr.set_output_dims_mapping(grad_var.name, dims_mapping)
|
||||
op_attr.set_input_dims_mapping(grad_var.name, dims_mapping)
|
||||
ctx.set_op_dist_attr_for_program(allreduce_op, op_attr)
|
||||
|
||||
|
||||
class DistributedDefault(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
main_block = dist_op.serial_op.block
|
||||
|
||||
num_inputs = len(input_arg_names)
|
||||
input_specs = []
|
||||
for i in range(num_inputs):
|
||||
assert not is_parameter_related(input_arg_names[i], main_block), (
|
||||
f"input {input_arg_names[i]} of op {dist_op.serial_op} is parameter, op should not use default rule."
|
||||
)
|
||||
input_specs.append(
|
||||
get_dist_tensor_spec(dist_op, input_arg_names[i])
|
||||
)
|
||||
num_outputs = len(output_arg_names)
|
||||
output_specs = []
|
||||
for i in range(num_outputs):
|
||||
assert not is_parameter_related(output_arg_names[i], main_block), (
|
||||
f"output {output_arg_names[i]} of op {dist_op.serial_op} is parameter, op should not use default rule."
|
||||
)
|
||||
output_specs.append(
|
||||
get_dist_tensor_spec(dist_op, output_arg_names[i], False)
|
||||
)
|
||||
|
||||
# step2: infer spmd
|
||||
if contains_spmd_rule(dist_op.serial_op.type):
|
||||
# when some inputs are optional, the input_arg_names will be less than input_names
|
||||
# and we can pass empty DistTensorSpec() as argument
|
||||
if len(op_desc.input_names()) > len(op_desc.input_arg_names()):
|
||||
for i in range(
|
||||
len(op_desc.input_names()) - len(op_desc.input_arg_names())
|
||||
):
|
||||
input_specs.append(DistTensorSpec())
|
||||
rule = get_phi_spmd_rule(dist_op.serial_op.type)
|
||||
fw_results = rule.infer_forward(*input_specs)
|
||||
bw_results = rule.infer_backward(*input_specs, output_specs)
|
||||
else:
|
||||
rule = get_phi_spmd_rule('default_')
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(input_specs, output_specs)
|
||||
bw_results = rule.infer_backward(input_specs, output_specs)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, input_arg_names, output_arg_names, fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedDefault("default"))
|
||||
|
||||
|
||||
# Replicated Default
|
||||
class DistributedDefaultImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
"""Calculate the cost by the op role."""
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res_cost = [cost_mapping]
|
||||
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
backward_op = dist_op.serial_op
|
||||
op_type = backward_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
need_gradient_allreduce = True
|
||||
break
|
||||
|
||||
if need_gradient_allreduce:
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
varname
|
||||
)
|
||||
mesh_shape = process_mesh.shape
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
batch_dim_mappings = []
|
||||
input_names = op_desc.input_names()
|
||||
xshape_arg_names = []
|
||||
if "XShape" in input_names:
|
||||
xshape_arg_names = op_desc.input("XShape")
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
serial_tensor = dist_op.get_serial_input(arg_name)
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
for mapping in dims_mapping:
|
||||
if mapping != -1:
|
||||
return False
|
||||
continue
|
||||
if arg_name not in xshape_arg_names:
|
||||
if len(dims_mapping) > 1:
|
||||
for mapping in dims_mapping[1:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
if dims_mapping[0] != -1:
|
||||
return False
|
||||
if len(dims_mapping) > 2:
|
||||
for mapping in dims_mapping[2:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 2:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
if compute_compatible_dim_mapping(batch_dim_mappings) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
output_names = op_desc.output_names()
|
||||
batch_dim_mappings = []
|
||||
xshape_arg_names = []
|
||||
if "XShape" in output_names:
|
||||
xshape_arg_names = op_desc.output("XShape")
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
serial_tensor = dist_op.get_serial_output(arg_name)
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
for mapping in dims_mapping:
|
||||
if mapping != -1:
|
||||
return False
|
||||
continue
|
||||
if arg_name not in xshape_arg_names:
|
||||
if len(dims_mapping) > 1:
|
||||
for mapping in dims_mapping[1:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
if dims_mapping[0] != -1:
|
||||
return False
|
||||
if len(dims_mapping) > 2:
|
||||
for mapping in dims_mapping[2:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 2:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
if compute_compatible_dim_mapping(batch_dim_mappings) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
batch_dim_mappings = []
|
||||
# Check input compatibility
|
||||
input_names = op_desc.input_names()
|
||||
xshape_arg_names = []
|
||||
if "XShape" in input_names:
|
||||
xshape_arg_names = op_desc.input("XShape")
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
serial_tensor = dist_op.get_serial_input(arg_name)
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if serial_tensor is not None and serial_tensor.is_parameter:
|
||||
for mapping in dims_mapping:
|
||||
if mapping != -1:
|
||||
return False
|
||||
continue
|
||||
if arg_name not in xshape_arg_names:
|
||||
if len(dims_mapping) > 1:
|
||||
for mapping in dims_mapping[1:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
if dims_mapping[0] != -1:
|
||||
return False
|
||||
if len(dims_mapping) > 2:
|
||||
for mapping in dims_mapping[2:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 2:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
# Check output compatibility
|
||||
output_names = op_desc.output_names()
|
||||
xshape_arg_names = []
|
||||
if "XShape" in output_names:
|
||||
xshape_arg_names = op_desc.output("XShape")
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
serial_tensor = dist_op.get_serial_output(arg_name)
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if serial_tensor is not None and serial_tensor.is_parameter:
|
||||
for mapping in dims_mapping:
|
||||
if mapping != -1:
|
||||
return False
|
||||
continue
|
||||
if arg_name not in xshape_arg_names:
|
||||
if len(dims_mapping) > 1:
|
||||
for mapping in dims_mapping[1:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
if dims_mapping[0] != -1:
|
||||
return False
|
||||
if len(dims_mapping) > 2:
|
||||
for mapping in dims_mapping[2:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 2:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
# Check batch dim mapping compatibility
|
||||
if not all(
|
||||
batch_dim_mappings[0] == dim_mapping
|
||||
for dim_mapping in batch_dim_mappings
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
if op_desc.type() == "while":
|
||||
return False
|
||||
|
||||
input_names = op_desc.input_names()
|
||||
input_xshape_arg_names = []
|
||||
if "XShape" in input_names:
|
||||
input_xshape_arg_names = op_desc.input("XShape")
|
||||
|
||||
output_names = op_desc.output_names()
|
||||
output_xshape_arg_names = []
|
||||
if "XShape" in output_names:
|
||||
output_xshape_arg_names = op_desc.output("XShape")
|
||||
|
||||
batch_dim_mappings = []
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
serial_tensor = dist_op.get_serial_input(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
continue
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if arg_name not in input_xshape_arg_names:
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
if op_desc.type() == 'fill_any_like':
|
||||
input_tensor = dist_op.get_serial_input(
|
||||
op_desc.input_arg_names()[0]
|
||||
)
|
||||
if input_tensor.is_parameter:
|
||||
continue
|
||||
serial_tensor = dist_op.get_serial_output(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
continue
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if arg_name not in output_xshape_arg_names:
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
if not batch_dim_mappings:
|
||||
return changed
|
||||
|
||||
compatible_dim_mapping = compute_compatible_dim_mapping(
|
||||
batch_dim_mappings
|
||||
)
|
||||
if compatible_dim_mapping is None:
|
||||
return False
|
||||
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
serial_tensor = dist_op.get_serial_input(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
continue
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if arg_name not in input_xshape_arg_names:
|
||||
if (
|
||||
len(dims_mapping) >= 1
|
||||
and compatible_dim_mapping != dims_mapping[0]
|
||||
):
|
||||
dims_mapping[0] = compatible_dim_mapping
|
||||
op_dist_attr.set_input_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
else:
|
||||
if (
|
||||
len(dims_mapping) >= 2
|
||||
and compatible_dim_mapping != dims_mapping[1]
|
||||
):
|
||||
dims_mapping[1] = compatible_dim_mapping
|
||||
op_dist_attr.set_input_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
if op_desc.type() == 'fill_any_like':
|
||||
input_tensor = dist_op.get_serial_input(
|
||||
op_desc.input_arg_names()[0]
|
||||
)
|
||||
if input_tensor.is_parameter:
|
||||
continue
|
||||
if op_desc.type() in ["shape", "slice"]:
|
||||
continue
|
||||
serial_tensor = dist_op.get_serial_output(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
continue
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if arg_name not in output_xshape_arg_names:
|
||||
if (
|
||||
len(dims_mapping) >= 1
|
||||
and compatible_dim_mapping != dims_mapping[0]
|
||||
):
|
||||
dims_mapping[0] = compatible_dim_mapping
|
||||
op_dist_attr.set_output_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
else:
|
||||
if (
|
||||
len(dims_mapping) >= 2
|
||||
and compatible_dim_mapping != dims_mapping[1]
|
||||
):
|
||||
dims_mapping[1] = compatible_dim_mapping
|
||||
op_dist_attr.set_output_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
# replicate op in dist program
|
||||
dst_op = copy_op_without_infer_shape(src_op, main_block, ctx, kwargs)
|
||||
|
||||
def get_shape_attr_name():
|
||||
for name in ["shape", "target_shape"]:
|
||||
if src_op.has_attr(name) and src_op.attr(name):
|
||||
return name
|
||||
return None
|
||||
|
||||
shape_attr_name = get_shape_attr_name()
|
||||
if shape_attr_name and src_op.type in __op_has_shape_attr__:
|
||||
shape_list = src_op.attr(shape_attr_name)
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
dim_mapping = op_dist_attr.get_output_dims_mapping(Out_var.name)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
assert len(shape_list) == len(dim_mapping)
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
dst_op.desc._set_attr(shape_attr_name, shape_list)
|
||||
|
||||
# data parallel synchronization for primitive operators
|
||||
from paddle.incubate.autograd import prim_enabled
|
||||
|
||||
if prim_enabled():
|
||||
assert is_prim_op(src_op)
|
||||
prim_operator_data_parallel_functor(ctx, src_op)
|
||||
return
|
||||
|
||||
# param initialization sync
|
||||
if src_op.type in __op_not_need_param_init__:
|
||||
return
|
||||
|
||||
for varname in dst_op.desc.input_arg_names():
|
||||
if (
|
||||
startup_block.has_var(varname)
|
||||
and startup_block.var(varname).is_parameter
|
||||
and varname not in dist_op_context.already_init_sync_vars
|
||||
):
|
||||
dist_op_context.already_init_sync_vars.add(varname)
|
||||
param = startup_block.var(varname)
|
||||
param_dist_attr = ctx.get_tensor_dist_attr_for_program(param)
|
||||
process_mesh = param_dist_attr.process_mesh
|
||||
dims_mapping = param_dist_attr.dims_mapping
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, process_mesh, rank_id
|
||||
)
|
||||
|
||||
# NOTE all not splited axis should be presented in mesh
|
||||
for axis, size in enumerate(process_mesh.shape):
|
||||
if size <= 1 or axis in dims_mapping:
|
||||
pass
|
||||
else:
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
axis,
|
||||
rank_id,
|
||||
)
|
||||
sync_group = new_process_group(group_ranks)
|
||||
|
||||
new_op = startup_block.append_op(
|
||||
type='broadcast',
|
||||
inputs={'x': param},
|
||||
outputs={'out': param},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'root': 0,
|
||||
OP_ROLE_KEY: OpRole.Forward,
|
||||
},
|
||||
)
|
||||
set_comm_op_dist_attr_for_program(
|
||||
new_op,
|
||||
process_mesh,
|
||||
param_dist_attr,
|
||||
ctx,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# by now the backward function only insert the gradient allreduce for dist op itself
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
rank_id = dist_op_context.rank_id
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in backward_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
backward_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in backward_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
backward_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
# replicate op in dist program
|
||||
copy_op_without_infer_shape(backward_op, main_block, ctx, kwargs)
|
||||
|
||||
# data parallel gradient synchronization
|
||||
act_grad_names = []
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
act_grad_names.append(varname)
|
||||
|
||||
out_grad_names = []
|
||||
for output_name in backward_op.desc.output_names():
|
||||
for varname in backward_op.desc.output(output_name):
|
||||
if varname in kwargs["grad_var_to_var"]:
|
||||
fwd_name = kwargs["grad_var_to_var"][varname]
|
||||
if not main_block._find_var_recursive(fwd_name):
|
||||
continue
|
||||
if is_parameter_related(fwd_name, main_block):
|
||||
out_grad_names.append(varname)
|
||||
|
||||
gradient_synchronization(
|
||||
ctx, backward_op, act_grad_names, out_grad_names, rank_id
|
||||
)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"default", DistributedDefaultImpl0("replicate_parallel")
|
||||
)
|
||||
@@ -0,0 +1,238 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
import logging
|
||||
|
||||
import paddle
|
||||
from paddle.base.log_helper import get_logger
|
||||
from paddle.framework import core
|
||||
from paddle.utils import unique_name
|
||||
|
||||
from ...random import determinate_rng, is_enable_auto_rand_ctrl
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import (
|
||||
get_dist_tensor_spec,
|
||||
naive_set_dist_op_attr_for_program_by_mesh_and_mapping,
|
||||
set_var_dist_attr,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
merge_forward_backward_dims_mapping,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_eltwise import DistributedDefaultImpl0, DistributedElementwiseImpl0
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
class DistributedDropout(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
mask_name = op_desc.output('Mask')[0]
|
||||
# seed_name = op_desc.input('Seed')[0] // seed is a scalar and leave it to be unsharded
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("dropout")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec)
|
||||
bw_results = rule.infer_backward(x_spec, output_spec)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
# step5: update mask and seed dropout special
|
||||
if changed:
|
||||
(
|
||||
_,
|
||||
inferred_output_dims_mappings,
|
||||
) = merge_forward_backward_dims_mapping(fw_results, bw_results)
|
||||
dist_op.dist_attr.set_output_dims_mapping(
|
||||
mask_name, inferred_output_dims_mappings[0]
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all dropout op use Dropout with Random Control dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
op_dist_attr.impl_type = "dropout"
|
||||
op_dist_attr.impl_idx = 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedDropout("dropout"))
|
||||
|
||||
|
||||
# Dist Dropout with Random Control
|
||||
# Dropout re-use the compatible and cost function of elementwise
|
||||
class DistributedDropoutImpl0(DistributedElementwiseImpl0):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
if is_enable_auto_rand_ctrl() and not op_dist_attr.is_recompute:
|
||||
# check validation of inputs / outputs
|
||||
assert 'X' in kwargs, "input [{}] is not given".format('X')
|
||||
assert len(kwargs['X']) == 1, (
|
||||
"input X should be only one tensor but got {}".format(
|
||||
kwargs['X']
|
||||
)
|
||||
)
|
||||
assert 'Seed' in kwargs, "input [{}] is not given".format('Seed')
|
||||
|
||||
if (
|
||||
src_op.has_attr("fix_seed")
|
||||
and src_op.attr("fix_seed")
|
||||
and src_op.has_attr("seed")
|
||||
and src_op.attr("seed")
|
||||
):
|
||||
_logger.info(
|
||||
f"Auto Parallel Random Control Skipped Since manual seed is set by user: {src_op}"
|
||||
)
|
||||
elif rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
pass
|
||||
# NOTE Adopt for recompute
|
||||
# If user already set seed, We should not modify it. But if the seed is added by recompute pass, it should be under control.
|
||||
# TODO in future recompute pass should happen after parallel partition. and remove this at that time.
|
||||
elif len(kwargs['Seed']) > 0 or len(src_op.input("Seed")) > 0:
|
||||
seed_var_name = kwargs['Seed'][0]
|
||||
if seed_var_name.startswith('rc_seed'):
|
||||
pre_op = main_block.ops[-1]
|
||||
assert (
|
||||
pre_op.type == "seed"
|
||||
and len(pre_op.attr("rng_name")) == 0
|
||||
), f"found exception op {pre_op}"
|
||||
|
||||
# determinate rng
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
X_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
X_var.name
|
||||
)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
rng_name = determinate_rng(
|
||||
rank_id, X_dims_mapping, process_mesh
|
||||
)
|
||||
# make recompute seed under control
|
||||
pre_op._set_attr("rng_name", rng_name)
|
||||
pre_op._set_attr("deterministic", True)
|
||||
pre_op._set_attr("force_cpu", True)
|
||||
else:
|
||||
_logger.info(
|
||||
f"Auto Parallel Random Control Skipped Since manual seed is set by user: {src_op}"
|
||||
)
|
||||
else:
|
||||
# determinate rng
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
X_dims_mapping = op_dist_attr.get_input_dims_mapping(X_var.name)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
|
||||
rng_name = determinate_rng(
|
||||
rank_id, X_dims_mapping, process_mesh
|
||||
)
|
||||
assert rng_name is not None and rng_name != ""
|
||||
|
||||
# insert seed op
|
||||
seed_var = main_block.create_var(
|
||||
name=unique_name.generate_with_ignorable_key(
|
||||
".".join(["tensor_parallel_seed", 'tmp'])
|
||||
),
|
||||
dtype=paddle.int32,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=False,
|
||||
)
|
||||
|
||||
# set new seed_var's dist_attr
|
||||
seed_var_dims_mapping = [-1]
|
||||
seed_var_dist_attr = set_var_dist_attr(
|
||||
ctx,
|
||||
seed_var,
|
||||
seed_var_dims_mapping,
|
||||
process_mesh,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# adopt for recompute
|
||||
# force_cpu to reduce sync copy from CPU->GPU->CPU, and reduce pipeline hang
|
||||
seed_op = main_block.append_op(
|
||||
type='seed',
|
||||
outputs={'Out': seed_var},
|
||||
attrs={
|
||||
'deterministic': True,
|
||||
'rng_name': rng_name,
|
||||
'force_cpu': True,
|
||||
},
|
||||
)
|
||||
seed_op._set_attr('op_namescope', 'auto_tensor_parallel_seed')
|
||||
# set new seed op's dist_attr
|
||||
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
|
||||
seed_op,
|
||||
process_mesh,
|
||||
seed_var_dims_mapping,
|
||||
ctx,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# modify dropout op
|
||||
src_op.desc.set_input("Seed", [seed_var.name])
|
||||
src_op.desc._set_attr("fix_seed", False)
|
||||
src_op.desc._set_attr("seed", 0)
|
||||
op_dist_attr.set_input_dist_attr(
|
||||
seed_var.name, seed_var_dist_attr
|
||||
)
|
||||
kwargs['Seed'] = [seed_var.name]
|
||||
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# dropout backward is deterministic by mask, and not need for random state control
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"dropout", DistributedDropoutImpl0("random_control")
|
||||
)
|
||||
@@ -0,0 +1,400 @@
|
||||
# Copyright (c) 2021 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 paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
_g_op_cost_factory,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import (
|
||||
compute_compatible_dim_mapping,
|
||||
compute_compatible_dims_mapping,
|
||||
get_dist_tensor_spec,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
is_elementwise_op,
|
||||
is_parameter_related,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedElementwise(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert len(op_desc.input_arg_names()) >= 1, (
|
||||
f"elementwise op [{op_desc.type}] has [{len(op_desc.input_arg_names())}] inputs"
|
||||
)
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
assert len(op_desc.output_arg_names()) == 1, (
|
||||
f"elementwise op [{dist_op.serial_op}] has [{len(op_desc.output_arg_names())}] outputs"
|
||||
)
|
||||
output_arg_name = op_desc.output_arg_names()[0]
|
||||
num_inputs = len(input_arg_names)
|
||||
|
||||
# TODO (zhangyichen) replace dist tensor specs by dist tensor in future.
|
||||
input_specs = []
|
||||
for i in range(num_inputs):
|
||||
input_specs.append(
|
||||
get_dist_tensor_spec(dist_op, input_arg_names[i])
|
||||
)
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
# TODO revise me
|
||||
op_type = op_desc.type()
|
||||
rule = get_phi_spmd_rule(op_type)
|
||||
fw_results = rule.infer_forward(*input_specs)
|
||||
bw_results = rule.infer_backward(*input_specs, output_spec)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, input_arg_names, [output_arg_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
# NOTE this function will be remove once we use local reshard to replace distopimpls
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all elementwise op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedElementwise("elementwise")
|
||||
)
|
||||
|
||||
|
||||
# Replicated Elementwise
|
||||
class DistributedElementwiseImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
"""Calculate the cost by the op role."""
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res_cost = [cost_mapping]
|
||||
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
backward_op = dist_op.serial_op
|
||||
op_type = backward_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
need_gradient_allreduce = True
|
||||
break
|
||||
|
||||
if need_gradient_allreduce:
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
varname
|
||||
)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = var_dim_mapping[0]
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
if not is_elementwise_op(op_desc.type()):
|
||||
return False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
dims_mapping_list = []
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
max_dims_mapping_len = -1
|
||||
for arg_name in input_arg_names:
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if max_dims_mapping_len < len(dims_mapping):
|
||||
max_dims_mapping_len = len(dims_mapping)
|
||||
dims_mapping_list.append(dims_mapping)
|
||||
|
||||
for idx in range(max_dims_mapping_len):
|
||||
dim_mappings = []
|
||||
for dims_mapping in dims_mapping_list:
|
||||
if idx < len(dims_mapping):
|
||||
dim_mappings.append(dims_mapping[-(idx + 1)])
|
||||
if compute_compatible_dim_mapping(dim_mappings) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
if not is_elementwise_op(op_desc.type()):
|
||||
return False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
dims_mapping_list = []
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
max_dims_mapping_len = -1
|
||||
for arg_name in output_arg_names:
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if max_dims_mapping_len < len(dims_mapping):
|
||||
max_dims_mapping_len = len(dims_mapping)
|
||||
dims_mapping_list.append(dims_mapping)
|
||||
|
||||
for idx in range(max_dims_mapping_len):
|
||||
dim_mappings = []
|
||||
for dims_mapping in dims_mapping_list:
|
||||
if idx < len(dims_mapping):
|
||||
dim_mappings.append(dims_mapping[-(idx + 1)])
|
||||
if compute_compatible_dim_mapping(dim_mappings) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
if not is_elementwise_op(op_desc.type()):
|
||||
return False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
dims_mapping_list = []
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
input_max_dims_mapping_len = -1
|
||||
for arg_name in input_arg_names:
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if input_max_dims_mapping_len < len(dims_mapping):
|
||||
input_max_dims_mapping_len = len(dims_mapping)
|
||||
dims_mapping_list.append(dims_mapping)
|
||||
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
output_max_dims_mapping_len = -1
|
||||
for arg_name in output_arg_names:
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if output_max_dims_mapping_len < len(dims_mapping):
|
||||
output_max_dims_mapping_len = len(dims_mapping)
|
||||
dims_mapping_list.append(dims_mapping)
|
||||
|
||||
assert input_max_dims_mapping_len == output_max_dims_mapping_len
|
||||
max_dims_mapping_len = input_max_dims_mapping_len
|
||||
|
||||
for idx in range(max_dims_mapping_len):
|
||||
dim_mappings = []
|
||||
for dims_mapping in dims_mapping_list:
|
||||
if idx < len(dims_mapping):
|
||||
dim_mappings.append(dims_mapping[-(idx + 1)])
|
||||
if not all(
|
||||
dim_mappings[0] == dim_mapping for dim_mapping in dim_mappings
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
dims_mapping_list = []
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
input_dims_mapping_dict = {}
|
||||
input_dims_mapping_lens = {}
|
||||
input_max_dims_mapping_len = -1
|
||||
for arg_name in input_arg_names:
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if input_max_dims_mapping_len < len(dims_mapping):
|
||||
input_max_dims_mapping_len = len(dims_mapping)
|
||||
input_dims_mapping_dict[arg_name] = dims_mapping
|
||||
input_dims_mapping_lens[arg_name] = len(dims_mapping)
|
||||
for arg_name in input_arg_names:
|
||||
if input_dims_mapping_lens[arg_name] < input_max_dims_mapping_len:
|
||||
new_dims_mapping = [
|
||||
-1 for _ in range(input_max_dims_mapping_len)
|
||||
]
|
||||
for i in range(input_dims_mapping_lens[arg_name]):
|
||||
new_idx = (
|
||||
input_max_dims_mapping_len
|
||||
- input_dims_mapping_lens[arg_name]
|
||||
) + i
|
||||
new_dims_mapping[new_idx] = input_dims_mapping_dict[
|
||||
arg_name
|
||||
][i]
|
||||
dims_mapping_list.append(new_dims_mapping)
|
||||
else:
|
||||
dims_mapping_list.append(input_dims_mapping_dict[arg_name])
|
||||
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
output_dims_mapping_dict = {}
|
||||
output_dims_mapping_lens = {}
|
||||
output_max_dims_mapping_len = -1
|
||||
for arg_name in output_arg_names:
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if output_max_dims_mapping_len < len(dims_mapping):
|
||||
output_max_dims_mapping_len = len(dims_mapping)
|
||||
output_dims_mapping_dict[arg_name] = dims_mapping
|
||||
output_dims_mapping_lens[arg_name] = len(dims_mapping)
|
||||
for arg_name in output_arg_names:
|
||||
if output_dims_mapping_lens[arg_name] < output_max_dims_mapping_len:
|
||||
new_dims_mapping = [
|
||||
-1 for _ in range(output_max_dims_mapping_len)
|
||||
]
|
||||
for i in range(output_dims_mapping_lens[arg_name]):
|
||||
new_idx = (
|
||||
output_max_dims_mapping_len
|
||||
- output_dims_mapping_lens[arg_name]
|
||||
) + i
|
||||
new_dims_mapping[new_idx] = output_dims_mapping_dict[
|
||||
arg_name
|
||||
][i]
|
||||
dims_mapping_list.append(new_dims_mapping)
|
||||
else:
|
||||
dims_mapping_list.append(output_dims_mapping_dict[arg_name])
|
||||
|
||||
assert input_max_dims_mapping_len == output_max_dims_mapping_len
|
||||
max_dims_mapping_len = input_max_dims_mapping_len
|
||||
compatible_dims_mapping = compute_compatible_dims_mapping(
|
||||
dims_mapping_list
|
||||
)
|
||||
if compatible_dims_mapping is None:
|
||||
return False
|
||||
|
||||
for arg_name in input_arg_names:
|
||||
if input_dims_mapping_lens[arg_name] < max_dims_mapping_len:
|
||||
new_dims_mapping = [
|
||||
-1 for _ in range(input_dims_mapping_lens[arg_name])
|
||||
]
|
||||
for i in range(input_dims_mapping_lens[arg_name]):
|
||||
new_idx = (
|
||||
max_dims_mapping_len - input_dims_mapping_lens[arg_name]
|
||||
) + i
|
||||
new_dims_mapping[i] = compatible_dims_mapping[new_idx]
|
||||
if new_dims_mapping != input_dims_mapping_dict[arg_name]:
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
arg_name, new_dims_mapping
|
||||
)
|
||||
changed = True
|
||||
else:
|
||||
if compatible_dims_mapping != input_dims_mapping_dict[arg_name]:
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
arg_name, compatible_dims_mapping
|
||||
)
|
||||
changed = True
|
||||
|
||||
for arg_name in output_arg_names:
|
||||
if output_dims_mapping_lens[arg_name] < max_dims_mapping_len:
|
||||
new_dims_mapping = [
|
||||
-1 for _ in range(output_dims_mapping_lens[arg_name])
|
||||
]
|
||||
for i in range(output_dims_mapping_lens[arg_name]):
|
||||
new_idx = (
|
||||
max_dims_mapping_len
|
||||
- output_dims_mapping_lens[arg_name]
|
||||
) + i
|
||||
new_dims_mapping[i] = compatible_dims_mapping[new_idx]
|
||||
if new_dims_mapping != output_dims_mapping_dict[arg_name]:
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
arg_name, new_dims_mapping
|
||||
)
|
||||
changed = True
|
||||
else:
|
||||
if (
|
||||
compatible_dims_mapping
|
||||
!= output_dims_mapping_dict[arg_name]
|
||||
):
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
arg_name, compatible_dims_mapping
|
||||
)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"elementwise", DistributedElementwiseImpl0("replicate_parallel")
|
||||
)
|
||||
@@ -0,0 +1,671 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import paddle
|
||||
from paddle.common_ops_import import check_variable_and_dtype
|
||||
from paddle.distributed.auto_parallel.static.cost.comm_op_cost import (
|
||||
AllReduceOpCost,
|
||||
IdentityOpCost,
|
||||
)
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
from paddle.framework import core
|
||||
from paddle.utils import unique_name
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
EmbeddingGradOpCost,
|
||||
EmbeddingOpCost,
|
||||
build_comm_costs_from_descs,
|
||||
build_comm_desc_from_dist_op,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
_get_idx_in_axis,
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
is_dim_replicate,
|
||||
is_dim_shard,
|
||||
set_var_dist_attr,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
ParallelMode,
|
||||
get_default_distributed_operator_impl,
|
||||
gradient_synchronization,
|
||||
naive_copy_op_dist_attr_for_program,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
set_comm_op_dist_attr_for_program,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedEmbedding(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert dist_op.serial_op.type == "lookup_table_v2", (
|
||||
f"{dist_op.serial_op.type} is not supported by dist embedding yet."
|
||||
)
|
||||
|
||||
x_name = op_desc.input('Ids')[0]
|
||||
w_name = op_desc.input('W')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
padding_idx = op_desc.attr('padding_idx')
|
||||
is_sparse = op_desc.attr('is_sparse')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
w_spec = get_dist_tensor_spec(dist_op, w_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("embedding")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, w_spec, padding_idx, is_sparse)
|
||||
bw_results = rule.infer_backward(
|
||||
x_spec, w_spec, output_spec, padding_idx, is_sparse
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name, w_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
reverted = False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
op_desc = dist_op.serial_op.desc
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dist_attr = op_dist_attr.get_output_dist_attr(out_name)
|
||||
|
||||
# vocab parallel embedding
|
||||
if out_dist_attr._is_partial():
|
||||
op_dist_attr.impl_type = op_desc.type()
|
||||
op_dist_attr.impl_idx = 0
|
||||
# data parallel or col parallel of weight
|
||||
else:
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return reverted
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedEmbedding("lookup_table_v2")
|
||||
)
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedEmbedding("c_embedding")
|
||||
)
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedEmbedding("lookup_table")
|
||||
)
|
||||
|
||||
|
||||
def adopt_lookup_table_v1(ctx, main_block, src_op, Ids_var):
|
||||
assert len(Ids_var.shape) == 3, (
|
||||
f"input Ids to lookup_table should have 3 dimensions but got [{Ids_var.name}] with shape [{Ids_var.shape}]"
|
||||
)
|
||||
if not Ids_var.stop_gradient:
|
||||
raise NotImplementedError(
|
||||
'Requiring the gradient of Ids of lookup_table(v1) dist op is not currently supported. Please open an issue with details on your use case so that we can prioritize adding this (for instance, adversarial training for language model).'
|
||||
)
|
||||
|
||||
target_shape = list(Ids_var.shape[:-1])
|
||||
intermediate_var_0 = main_block.create_var(
|
||||
name=unique_name.generate_with_ignorable_key(
|
||||
".".join(["dist_reshape", 'tmp'])
|
||||
),
|
||||
dtype=Ids_var.dtype,
|
||||
shape=target_shape,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=True,
|
||||
)
|
||||
|
||||
target_shape = [0, *list(Ids_var.shape[:-1])]
|
||||
xshape_var = main_block.create_var(
|
||||
name=unique_name.generate_with_ignorable_key(
|
||||
".".join(["dist_Xshape", 'tmp'])
|
||||
),
|
||||
dtype=Ids_var.dtype,
|
||||
shape=target_shape,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=True,
|
||||
)
|
||||
|
||||
# TODO use inplace reshape for memory saving
|
||||
reshape_op = main_block.append_op(
|
||||
type='reshape2',
|
||||
inputs={'X': [Ids_var]},
|
||||
outputs={'Out': [intermediate_var_0], 'XShape': [xshape_var]},
|
||||
attrs={
|
||||
"shape": [0, -1],
|
||||
},
|
||||
)
|
||||
|
||||
# set dist attr
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
Ids_var_dist_attr = op_dist_attr.get_input_dist_attr(Ids_var.name)
|
||||
assert Ids_var_dist_attr is not None
|
||||
intermediate_var_0_dist_attr = set_var_dist_attr(
|
||||
ctx,
|
||||
intermediate_var_0,
|
||||
Ids_var_dist_attr.dims_mapping,
|
||||
Ids_var_dist_attr.process_mesh,
|
||||
chunk_id=Ids_var_dist_attr.chunk_id,
|
||||
)
|
||||
set_var_dist_attr(
|
||||
ctx,
|
||||
xshape_var,
|
||||
[-1, *list(Ids_var_dist_attr.dims_mapping)],
|
||||
Ids_var_dist_attr.process_mesh,
|
||||
chunk_id=Ids_var_dist_attr.chunk_id,
|
||||
)
|
||||
# rename src_op's input
|
||||
src_op._rename_input(Ids_var.name, intermediate_var_0.name)
|
||||
op_dist_attr.del_input_dist_attr(Ids_var.name)
|
||||
op_dist_attr.set_input_dist_attr(
|
||||
intermediate_var_0.name, intermediate_var_0_dist_attr
|
||||
)
|
||||
|
||||
new_op_dist_attr = OperatorDistAttr()
|
||||
new_op_dist_attr.process_mesh = Ids_var_dist_attr.process_mesh
|
||||
new_op_dist_attr.impl_type = "default"
|
||||
new_op_dist_attr.impl_idx = 0
|
||||
new_op_dist_attr.chunk_id = Ids_var_dist_attr.chunk_id
|
||||
new_op_dist_attr.set_input_dims_mapping(
|
||||
Ids_var.name, Ids_var_dist_attr.dims_mapping
|
||||
)
|
||||
new_op_dist_attr.set_output_dims_mapping(
|
||||
intermediate_var_0.name, Ids_var_dist_attr.dims_mapping
|
||||
)
|
||||
new_op_dist_attr.set_output_dims_mapping(
|
||||
xshape_var.name, [-1, *list(Ids_var_dist_attr.dims_mapping)]
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(reshape_op, new_op_dist_attr)
|
||||
|
||||
return intermediate_var_0
|
||||
|
||||
|
||||
# RowParallel
|
||||
class DistributedEmbeddingImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
"""Calculate the cost by the op role."""
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Forward):
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
elif int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
# embedding need start_index
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
EmbeddingOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
|
||||
serial_op = dist_op.serial_op
|
||||
parallel_axis = dist_op.dist_attr.get_input_dims_mapping(
|
||||
serial_op.input("W")[0]
|
||||
)[0]
|
||||
attrs = {"use_calc_stream": True, "use_model_parallel": True}
|
||||
var_names = serial_op.output("Out")
|
||||
all_reduce_sum_desc_mapping = build_comm_desc_from_dist_op(
|
||||
"all_reduce",
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs=attrs,
|
||||
parallel_axis=parallel_axis,
|
||||
)
|
||||
|
||||
comm_op_cost_list = build_comm_costs_from_descs(
|
||||
AllReduceOpCost,
|
||||
ctx,
|
||||
processes,
|
||||
all_reduce_sum_desc_mapping,
|
||||
cluster,
|
||||
)
|
||||
|
||||
res_cost = [cost_mapping, comm_op_cost_list]
|
||||
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# by now the backward function only insert the gradient allreduce for dist op itself
|
||||
res = []
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
dist_attr = dist_op.dist_attr
|
||||
|
||||
embedding_row_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
backward_op.input("W")[0]
|
||||
)[0]
|
||||
parallel_axis = embedding_row_dim_mapping
|
||||
attrs = {"use_calc_stream": True, "use_model_parallel": True}
|
||||
var_names = [backward_op.input("Out@GRAD")[0]]
|
||||
c_identity_desc_mapping = build_comm_desc_from_dist_op(
|
||||
"c_identity",
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs=attrs,
|
||||
parallel_axis=parallel_axis,
|
||||
)
|
||||
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
comm_op_cost_list = build_comm_costs_from_descs(
|
||||
IdentityOpCost, ctx, processes, c_identity_desc_mapping, cluster
|
||||
)
|
||||
res.append(comm_op_cost_list)
|
||||
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
EmbeddingGradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
# need gradient allreduce
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
backward_op.input("Ids")[0]
|
||||
)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [backward_op.output('W@GRAD')[0]]
|
||||
build_dp_costs(
|
||||
res, dist_op, ctx, var_names, attrs, parallel_axis, cluster
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
ids_name = op_desc.input('Ids')[0]
|
||||
w_name = op_desc.input('W')[0]
|
||||
ids_dims_mapping = op_dist_attr.get_input_dims_mapping(ids_name)
|
||||
w_dims_mapping = op_dist_attr.get_input_dims_mapping(w_name)
|
||||
if is_dim_replicate(w_dims_mapping[-2]) or is_dim_shard(
|
||||
w_dims_mapping[-1]
|
||||
):
|
||||
return False
|
||||
# Other dimensions must be replicate except the batch dimension
|
||||
for mapping in ids_dims_mapping[1:]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
|
||||
if is_dim_shard(ids_dims_mapping[0]) and is_dim_shard(
|
||||
w_dims_mapping[-2]
|
||||
):
|
||||
if ids_dims_mapping[0] == w_dims_mapping[-2]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
# Other dimensions must be replicate except the batch dimension
|
||||
for mapping in out_dims_mapping[1:]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
ids_name = op_desc.input('Ids')[0]
|
||||
w_name = op_desc.input('W')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
ids_dims_mapping = op_dist_attr.get_input_dims_mapping(ids_name)
|
||||
w_dims_mapping = op_dist_attr.get_input_dims_mapping(w_name)
|
||||
|
||||
if ids_dims_mapping != out_dims_mapping[: len(ids_dims_mapping)]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
ids_name = op_desc.input('Ids')[0]
|
||||
w_name = op_desc.input('W')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
ids_dims_mapping = op_dist_attr.get_input_dims_mapping(ids_name)
|
||||
w_dims_mapping = op_dist_attr.get_input_dims_mapping(w_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for i in range(len(ids_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[ids_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[w_dims_mapping, out_dims_mapping], [-1, -1]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(ids_name, ids_dims_mapping)
|
||||
op_dist_attr.set_input_dims_mapping(w_name, w_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Ids' in kwargs, "input [{}] is not given".format('Ids')
|
||||
assert 'W' in kwargs, "input [{}] is not given".format('W')
|
||||
assert 'Out' in kwargs, "output [{}] is not given".format('Out')
|
||||
|
||||
assert len(kwargs['Ids']) == 1, (
|
||||
"row_parallel_embedding input Ids take 1 variable but got {}".format(
|
||||
kwargs['Ids']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['W']) == 1, (
|
||||
"row_parallel_embedding input W take 1 variable but got {}".format(
|
||||
kwargs['W']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Out']) == 1, (
|
||||
"row_parallel_embedding output Out take 1 variable but got {}".format(
|
||||
kwargs['Out']
|
||||
)
|
||||
)
|
||||
|
||||
Ids_var = main_block._var_recursive(kwargs['Ids'][0])
|
||||
Weight_var = main_block._var_recursive(kwargs['W'][0])
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
|
||||
# support lookup_table_v1
|
||||
if src_op.type == 'lookup_table':
|
||||
Ids_var = adopt_lookup_table_v1(ctx, main_block, src_op, Ids_var)
|
||||
|
||||
# got dist attribute info
|
||||
embedding_row_dim_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
Weight_var.name
|
||||
)[0]
|
||||
assert embedding_row_dim_mapping >= 0, (
|
||||
f"row_parallel_embedding's row should be divided by a specific mesh axis, but got [{embedding_row_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh_group:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# A generalized method to calculate embedding offset using cartesian product
|
||||
relative_idx = _get_idx_in_axis(
|
||||
process_mesh_group,
|
||||
process_mesh_shape,
|
||||
embedding_row_dim_mapping,
|
||||
rank_id,
|
||||
)
|
||||
|
||||
per_part_size = Weight_var.shape[0]
|
||||
relative_idx = relative_idx * per_part_size
|
||||
|
||||
# TODO calculate ring id
|
||||
parallel_axis = embedding_row_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# append op
|
||||
check_variable_and_dtype(
|
||||
Ids_var, 'input', ['int32', 'int64'], 'c_embedding'
|
||||
)
|
||||
|
||||
# infer new var shape with op dist attr
|
||||
out_tensor_dist_attr = ctx.get_tensor_dist_attr_for_program(Out_var)
|
||||
assert out_tensor_dist_attr is not None
|
||||
out_var_dist_attr = op_dist_attr.get_output_dist_attr(Out_var.name)
|
||||
assert out_var_dist_attr is not None
|
||||
|
||||
c_embedding_op_desc = main_block.append_op(type='nop').desc
|
||||
c_embedding_op_desc.set_type("c_embedding")
|
||||
c_embedding_op_desc.set_input('Ids', [Ids_var.name])
|
||||
c_embedding_op_desc.set_input('W', [Weight_var.name])
|
||||
c_embedding_op_desc.set_output('Out', [Out_var.name])
|
||||
c_embedding_op_desc._set_attr('start_index', relative_idx)
|
||||
c_embedding_op_desc._set_attr(OP_ROLE_KEY, src_op.attr('op_role'))
|
||||
c_embedding_op = main_block.ops[-1]
|
||||
assert c_embedding_op.type == "c_embedding"
|
||||
naive_copy_op_dist_attr_for_program(c_embedding_op, src_op, ctx)
|
||||
|
||||
# use_model_parallel
|
||||
all_reduce_sum_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': [Out_var]},
|
||||
outputs={'out': [Out_var]},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'reduce_type': paddle.distributed.ReduceOp.SUM,
|
||||
'use_model_parallel': True,
|
||||
OP_ROLE_KEY: src_op.attr('op_role'),
|
||||
},
|
||||
)
|
||||
all_reduce_sum_op._set_attr(
|
||||
'op_namescope', '/' + ParallelMode.TensorParallel
|
||||
)
|
||||
# allreduce
|
||||
set_comm_op_dist_attr_for_program(
|
||||
all_reduce_sum_op,
|
||||
op_dist_attr.process_mesh,
|
||||
out_var_dist_attr,
|
||||
ctx,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# param initialization sync
|
||||
if Weight_var.is_parameter and not op_dist_attr.is_recompute:
|
||||
if Weight_var.name in dist_op_context.already_init_sync_vars:
|
||||
return
|
||||
dist_op_context.already_init_sync_vars.add(Weight_var.name)
|
||||
param = startup_block.var(Weight_var.name)
|
||||
param_dist_attr = ctx.get_tensor_dist_attr_for_program(param)
|
||||
process_mesh = param_dist_attr.process_mesh
|
||||
dim_mapping = param_dist_attr.dims_mapping
|
||||
|
||||
# NOTE all not split axis should be presented in mesh
|
||||
for axis, size in enumerate(process_mesh.shape):
|
||||
if size <= 1 or axis in dim_mapping:
|
||||
pass
|
||||
else:
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
axis,
|
||||
rank_id,
|
||||
)
|
||||
sync_group = new_process_group(group_ranks)
|
||||
|
||||
broadcast_op = startup_block.append_op(
|
||||
type='broadcast',
|
||||
inputs={'x': param},
|
||||
outputs={'out': param},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'root': 0,
|
||||
OP_ROLE_KEY: OpRole.Forward,
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# by now the backward function only insert the gradient allreduce for dist op itself
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
assert 'Ids' in kwargs, "input [{}] is not given".format('Ids')
|
||||
assert 'W' in kwargs, "input [{}] is not given".format('W')
|
||||
assert 'Out@GRAD' in kwargs, "input [{}] is not given".format('Out')
|
||||
assert 'W@GRAD' in kwargs, "output [{}] is not given".format('W@GRAD')
|
||||
|
||||
assert len(kwargs['Ids']) == 1, (
|
||||
"row_parallel_embedding input Ids take 1 variable but got {}".format(
|
||||
kwargs['Ids']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['W']) == 1, (
|
||||
"row_parallel_embedding input Ids take 1 variable but got {}".format(
|
||||
kwargs['W']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Out@GRAD']) == 1, (
|
||||
"row_parallel_embedding input Ids take 1 variable but got {}".format(
|
||||
kwargs['Out']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['W@GRAD']) == 1, (
|
||||
"row_parallel_embedding output Ids take 1 variable but got {}".format(
|
||||
kwargs['W@GRAD']
|
||||
)
|
||||
)
|
||||
|
||||
Ids_var = main_block._var_recursive(kwargs['Ids'][0])
|
||||
Weight_var = main_block._var_recursive(kwargs['W'][0])
|
||||
Out_grad = main_block._var_recursive(kwargs['Out@GRAD'][0])
|
||||
Weight_grad = main_block._var_recursive(kwargs['W@GRAD'][0])
|
||||
|
||||
embedding_row_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
Weight_var.name
|
||||
)[0]
|
||||
assert embedding_row_dim_mapping >= 0, (
|
||||
f"row_parallel_embedding's row should be divided by a specific mesh axis, but got [{embedding_row_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = dist_attr.process_mesh.shape
|
||||
process_mesh_group = dist_attr.process_mesh.process_ids
|
||||
|
||||
# A generalized method to calculate embedding offset using cartesian product
|
||||
relative_idx = _get_idx_in_axis(
|
||||
process_mesh_group,
|
||||
process_mesh_shape,
|
||||
embedding_row_dim_mapping,
|
||||
rank_id,
|
||||
)
|
||||
per_part_size = Weight_var.shape[0]
|
||||
relative_idx = relative_idx * per_part_size
|
||||
|
||||
c_embedding_grad_op_desc = main_block.append_op(type='nop').desc
|
||||
c_embedding_grad_op_desc.set_type("c_embedding_grad")
|
||||
c_embedding_grad_op_desc.set_input('Ids', [Ids_var.name])
|
||||
c_embedding_grad_op_desc.set_input('W', [Weight_var.name])
|
||||
c_embedding_grad_op_desc.set_input('Out@GRAD', [Out_grad.name])
|
||||
c_embedding_grad_op_desc.set_output('W@GRAD', [Weight_grad.name])
|
||||
c_embedding_grad_op_desc._set_attr('start_index', relative_idx)
|
||||
c_embedding_grad_op_desc._set_attr(OP_ROLE_KEY, OpRole.Backward)
|
||||
|
||||
c_embedding_grad_op = main_block.ops[-1]
|
||||
assert c_embedding_grad_op.type == "c_embedding_grad"
|
||||
naive_copy_op_dist_attr_for_program(
|
||||
c_embedding_grad_op, backward_op, ctx
|
||||
)
|
||||
|
||||
# data parallel gradient synchronization
|
||||
act_grad_names = [Ids_var.name]
|
||||
out_grad_names = [kwargs['W@GRAD'][0]]
|
||||
|
||||
gradient_synchronization(
|
||||
ctx, backward_op, act_grad_names, out_grad_names, rank_id
|
||||
)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"lookup_table_v2", DistributedEmbeddingImpl("row_parallel")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"c_embedding", DistributedEmbeddingImpl("row_parallel")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"lookup_table", DistributedEmbeddingImpl("row_parallel")
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
# 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 ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedExpandAs(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
target_shape = op_desc.attr('target_shape')
|
||||
|
||||
input_specs = []
|
||||
for name in input_arg_names:
|
||||
input_specs.append(get_dist_tensor_spec(dist_op, name))
|
||||
|
||||
assert len(input_specs) == 2
|
||||
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_names[0], False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("expand_as")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(
|
||||
input_specs[0], input_specs[1], target_shape
|
||||
)
|
||||
bw_results = rule.infer_backward(
|
||||
input_specs[0], input_specs[1], output_spec, target_shape
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names,
|
||||
output_arg_names,
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedExpandAs("expand_as_v2")
|
||||
)
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2021 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 paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..cost import (
|
||||
FillConstantBatchSizeLikeOpCost,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
)
|
||||
from ..utils import compute_compatible_and_update_dim_mapping
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedFillConstantBatchSizeLike(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedFillConstantBatchSizeLike("fill_constant_batch_size_like")
|
||||
)
|
||||
|
||||
|
||||
class DistributedFillConstantBatchSizeLikeImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
raise ValueError(
|
||||
"The fill_constant_batch_size_like has no grad op."
|
||||
)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
FillConstantBatchSizeLikeOpCost,
|
||||
ctx,
|
||||
processes,
|
||||
desc_mapping,
|
||||
cluster,
|
||||
)
|
||||
|
||||
res_cost = [cost_mapping]
|
||||
return res_cost
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
shape_list = op_desc.attr("shape")
|
||||
|
||||
if len(shape_list) != len(out_dims_mapping):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
in_name = op_desc.input('Input')[0]
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
|
||||
# the dim_mapping of batch dimension should be the same
|
||||
return out_dims_mapping[0] == in_dims_mapping[0]
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
# only the batch size dimension of input and output are relative.
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [0, 0]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"fill_constant_batch_size_like",
|
||||
DistributedFillConstantBatchSizeLikeImpl0("fill_by_shape"),
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
# 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 ...random import determinate_rng, is_enable_auto_rand_ctrl
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_eltwise import DistributedElementwiseImpl0
|
||||
|
||||
|
||||
class DistributedFlashAttn(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedFlashAttn("flash_attn"))
|
||||
|
||||
|
||||
# Dist FlashAttn with Random Control
|
||||
# NOTE(zhiqiu): trick implementation, copy dist_attr of q,k,v to out
|
||||
class DistributedFlashAttnImpl0(DistributedElementwiseImpl0):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if (
|
||||
is_enable_auto_rand_ctrl()
|
||||
and not op_dist_attr.is_recompute
|
||||
and rank_id in op_dist_attr.process_mesh.process_ids
|
||||
):
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
if (
|
||||
len(kwargs.get('fixed_seed_offset', [])) > 0
|
||||
or len(src_op.input("fixed_seed_offset")) > 0
|
||||
):
|
||||
# TODO(kuizhiqing) recompute should go here
|
||||
pass
|
||||
else:
|
||||
# determinate rng
|
||||
q_var = main_block._var_recursive(kwargs['q'][0])
|
||||
k_var = main_block._var_recursive(kwargs['k'][0])
|
||||
q_dims_mapping = op_dist_attr.get_input_dims_mapping(q_var.name)
|
||||
k_dims_mapping = op_dist_attr.get_input_dims_mapping(k_var.name)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
dims_mapping = [*q_dims_mapping[:3], q_dims_mapping[2]]
|
||||
|
||||
rng_name = determinate_rng(rank_id, dims_mapping, process_mesh)
|
||||
assert rng_name is not None and rng_name != ""
|
||||
|
||||
src_op._set_attr('rng_name', rng_name)
|
||||
|
||||
DistributedElementwiseImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# dropout backward is deterministic by mask, and not need for random state control
|
||||
DistributedElementwiseImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"flash_attn", DistributedFlashAttnImpl0("random_control")
|
||||
)
|
||||
@@ -0,0 +1,235 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
is_dim_replicate,
|
||||
is_dim_shard,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedFusedAttention(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedFusedAttention("fused_attention")
|
||||
)
|
||||
|
||||
|
||||
class DistributedFusedAttentionImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
qkv_w = op_desc.input('QKVW')[0]
|
||||
qkv_bias = op_desc.input('QKVBias')[0]
|
||||
out_w = op_desc.input('OutLinearW')[0]
|
||||
out_bias = op_desc.input('OutLinearBias')[0]
|
||||
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
qkv_w_dims_mapping = op_dist_attr.get_input_dims_mapping(qkv_w)
|
||||
qkv_bias_dims_mapping = op_dist_attr.get_input_dims_mapping(qkv_bias)
|
||||
out_w_dims_mapping = op_dist_attr.get_input_dims_mapping(out_w)
|
||||
out_bias_dims_mapping = op_dist_attr.get_input_dims_mapping(out_bias)
|
||||
|
||||
head_axis = 1
|
||||
for mapping in x_dims_mapping[1:-1]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
if len(qkv_w_dims_mapping) != 4 or is_dim_replicate(
|
||||
qkv_w_dims_mapping[head_axis]
|
||||
):
|
||||
return False
|
||||
if len(qkv_bias_dims_mapping) != 3 or is_dim_replicate(
|
||||
qkv_bias_dims_mapping[head_axis]
|
||||
):
|
||||
return False
|
||||
if is_dim_replicate(out_w_dims_mapping[0]):
|
||||
return False
|
||||
if is_dim_shard(out_bias_dims_mapping[-1]):
|
||||
return False
|
||||
|
||||
replicated_dims = [
|
||||
qkv_w_dims_mapping[0],
|
||||
qkv_w_dims_mapping[-2],
|
||||
qkv_w_dims_mapping[-1],
|
||||
qkv_bias_dims_mapping[0],
|
||||
qkv_bias_dims_mapping[-1],
|
||||
out_w_dims_mapping[-1],
|
||||
out_bias_dims_mapping[-1],
|
||||
]
|
||||
for mapping in replicated_dims:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
if qkv_bias_dims_mapping[head_axis] != qkv_w_dims_mapping[head_axis]:
|
||||
return False
|
||||
if qkv_bias_dims_mapping[head_axis] != out_w_dims_mapping[0]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
# none of output should be sharded
|
||||
for out_name in op_desc.output_names():
|
||||
out = op_desc.output(out_name)[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out)
|
||||
for mapping in out_dims_mapping[1:-1]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Y')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Y')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
out_name, out_dims_mapping
|
||||
)
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# infer logic comm presentation
|
||||
head_axis = 1
|
||||
qkv_w = src_op.input('QKVW')[0]
|
||||
qkv_w_col_dim_mapping = op_dist_attr.get_input_dims_mapping(qkv_w)[
|
||||
head_axis
|
||||
]
|
||||
assert qkv_w_col_dim_mapping >= 0, (
|
||||
f"col_parallel_matmul's row should be divided by a specific mesh axis, but got [{qkv_w_col_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
parallel_axis = qkv_w_col_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# insert op
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
# setting comm id
|
||||
new_op = main_block.ops[-1]
|
||||
assert new_op.type == "fused_attention"
|
||||
new_op._set_attr("ring_id", int(group.id))
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# infer logic comm presentation
|
||||
out_w = src_op.input('OutLinearW')[0]
|
||||
out_w_col_dim_mapping = op_dist_attr.get_input_dims_mapping(out_w)[-1]
|
||||
assert out_w_col_dim_mapping >= 0, (
|
||||
f"col_parallel_matmul's row should be divided by a specific mesh axis, but got [{out_w_col_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
parallel_axis = out_w_col_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# insert op
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
# setting comm id
|
||||
new_op = main_block.ops[-1]
|
||||
assert new_op.type == "fused_attention_grad"
|
||||
new_op._set_attr("ring_id", int(group.id))
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"fused_attention", DistributedFusedAttentionImpl("tensor_parallel")
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
import logging
|
||||
|
||||
import paddle
|
||||
from paddle.base.log_helper import get_logger
|
||||
from paddle.framework import core
|
||||
from paddle.utils import unique_name
|
||||
|
||||
from ...random import determinate_rng, is_enable_auto_rand_ctrl
|
||||
from ..utils import (
|
||||
naive_set_dist_op_attr_for_program_by_mesh_and_mapping,
|
||||
set_var_dist_attr,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_eltwise import DistributedDefaultImpl0, DistributedElementwiseImpl0
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
class DistributedDropout(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedDropout("fused_dropout_add")
|
||||
)
|
||||
|
||||
|
||||
# Dist Dropout with Random Control
|
||||
# Dropout re-use the compatible and cost function of elementwise
|
||||
class DistributedDropoutImpl0(DistributedElementwiseImpl0):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if is_enable_auto_rand_ctrl() and not op_dist_attr.is_recompute:
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
assert 'seed_tensor' in kwargs, "input [{}] is not given".format(
|
||||
'seed_tensor'
|
||||
)
|
||||
|
||||
if (
|
||||
src_op.has_attr("fix_seed")
|
||||
and src_op.attr("fix_seed")
|
||||
and src_op.has_attr("seed")
|
||||
and src_op.attr("seed")
|
||||
):
|
||||
_logger.info(
|
||||
f"Auto Parallel Random Control Skipped Since manual seed is set by user: {src_op}"
|
||||
)
|
||||
elif rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
pass
|
||||
elif (
|
||||
len(kwargs['seed_tensor']) > 0
|
||||
or len(src_op.input("seed_tensor")) > 0
|
||||
):
|
||||
seed_var_name = kwargs['seed_tensor'][0]
|
||||
if seed_var_name.startswith('rc_seed'):
|
||||
pre_op = main_block.ops[-1]
|
||||
assert (
|
||||
pre_op.type == "seed"
|
||||
and len(pre_op.attr("rng_name")) == 0
|
||||
), f"found exception op {pre_op}"
|
||||
|
||||
# determinate rng
|
||||
X_var = main_block._var_recursive(kwargs['x'][0])
|
||||
X_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
X_var.name
|
||||
)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
rng_name = determinate_rng(
|
||||
rank_id, X_dims_mapping, process_mesh
|
||||
)
|
||||
# make recompute seed under control
|
||||
pre_op._set_attr("rng_name", rng_name)
|
||||
pre_op._set_attr("deterministic", True)
|
||||
pre_op._set_attr("force_cpu", True)
|
||||
else:
|
||||
_logger.info(
|
||||
f"Auto Parallel Random Control Skipped Since manual seed is set by user: {src_op}"
|
||||
)
|
||||
else:
|
||||
# determinate rng
|
||||
X_var = main_block._var_recursive(kwargs['x'][0])
|
||||
X_dims_mapping = op_dist_attr.get_input_dims_mapping(X_var.name)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
|
||||
rng_name = determinate_rng(
|
||||
rank_id, X_dims_mapping, process_mesh
|
||||
)
|
||||
assert rng_name is not None and rng_name != ""
|
||||
|
||||
# insert seed op
|
||||
seed_var = main_block.create_var(
|
||||
name=unique_name.generate_with_ignorable_key(
|
||||
".".join(["tensor_parallel_seed", 'tmp'])
|
||||
),
|
||||
dtype=paddle.int32,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=False,
|
||||
)
|
||||
|
||||
# set new seed_var's dist_attr
|
||||
seed_var_dims_mapping = [-1]
|
||||
seed_var_dist_attr = set_var_dist_attr(
|
||||
ctx,
|
||||
seed_var,
|
||||
seed_var_dims_mapping,
|
||||
process_mesh,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# adopt for recompute
|
||||
# force_cpu to reduce sync copy from CPU->GPU->CPU, and reduce pipeline hang
|
||||
seed_op = main_block.append_op(
|
||||
type='seed',
|
||||
outputs={'Out': seed_var},
|
||||
attrs={
|
||||
'deterministic': True,
|
||||
'rng_name': rng_name,
|
||||
'force_cpu': True,
|
||||
},
|
||||
)
|
||||
seed_op._set_attr('op_namescope', 'auto_tensor_parallel_seed')
|
||||
# set new seed op's dist_attr
|
||||
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
|
||||
seed_op,
|
||||
process_mesh,
|
||||
seed_var_dims_mapping,
|
||||
ctx,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# modify dropout op
|
||||
src_op.desc.set_input("seed_tensor", [seed_var.name])
|
||||
src_op._remove_attr("fix_seed")
|
||||
src_op._remove_attr("seed")
|
||||
op_dist_attr.set_input_dist_attr(
|
||||
seed_var.name, seed_var_dist_attr
|
||||
)
|
||||
kwargs['seed_tensor'] = [seed_var.name]
|
||||
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# dropout backward is deterministic by mask, and not need for random state control
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"fused_dropout_add", DistributedDropoutImpl0("random_control")
|
||||
)
|
||||
@@ -0,0 +1,228 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
is_dim_replicate,
|
||||
is_dim_shard,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedFusedFeedForward(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedFusedFeedForward("fused_feedforward")
|
||||
)
|
||||
|
||||
|
||||
class DistributedFusedFeedForwardImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
linear1_weight = op_desc.input('Linear1Weight')[0]
|
||||
linear1_bias = op_desc.input('Linear1Bias')[0]
|
||||
linear2_weight = op_desc.input('Linear2Weight')[0]
|
||||
linear2_bias = op_desc.input('Linear2Bias')[0]
|
||||
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
linear1_weight_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear1_weight
|
||||
)
|
||||
linear1_bias_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear1_bias
|
||||
)
|
||||
linear2_weight_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear2_weight
|
||||
)
|
||||
linear2_bias_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear2_bias
|
||||
)
|
||||
|
||||
for mapping in x_dims_mapping[1:-1]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
if is_dim_shard(linear1_weight_dims_mapping[-2]) or is_dim_replicate(
|
||||
linear1_weight_dims_mapping[-1]
|
||||
):
|
||||
return False
|
||||
if is_dim_replicate(linear1_bias_dims_mapping[-1]):
|
||||
return False
|
||||
if is_dim_replicate(linear2_weight_dims_mapping[-2]) or is_dim_shard(
|
||||
linear2_weight_dims_mapping[-1]
|
||||
):
|
||||
return False
|
||||
if is_dim_shard(linear2_bias_dims_mapping[-1]):
|
||||
return False
|
||||
if linear1_weight_dims_mapping[-1] != linear2_weight_dims_mapping[-2]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
# none of output should be sharded
|
||||
for out_name in op_desc.output_names():
|
||||
out = op_desc.output(out_name)[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out)
|
||||
for mapping in out_dims_mapping[1:-1]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Out')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Out')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
out_name, out_dims_mapping
|
||||
)
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# infer logic comm presentation
|
||||
linear1_weight = src_op.input('Linear1Weight')[0]
|
||||
linear1_weight_col_dim_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear1_weight
|
||||
)[-1]
|
||||
assert linear1_weight_col_dim_mapping >= 0, (
|
||||
f"col_parallel_matmul's row should be divided by a specific mesh axis, but got [{linear1_weight_col_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
parallel_axis = linear1_weight_col_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# insert op
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
# setting comm id
|
||||
new_op = main_block.ops[-1]
|
||||
assert new_op.type == "fused_feedforward"
|
||||
new_op._set_attr("ring_id", int(group.id))
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# infer logic comm presentation
|
||||
linear2_weight = src_op.input('Linear2Weight')[0]
|
||||
linear2_weight_col_dim_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear2_weight
|
||||
)[-1]
|
||||
assert linear2_weight_col_dim_mapping >= 0, (
|
||||
f"col_parallel_matmul's row should be divided by a specific mesh axis, but got [{linear2_weight_col_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
parallel_axis = linear2_weight_col_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# insert op
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
# setting comm id
|
||||
new_op = main_block.ops[-1]
|
||||
assert new_op.type == "fused_feedforward_grad"
|
||||
new_op._set_attr("ring_id", int(group.id))
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"fused_feedforward", DistributedFusedFeedForwardImpl("tensor_parallel")
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
# 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
|
||||
|
||||
import logging
|
||||
|
||||
from paddle.base.log_helper import get_logger
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
class DistributedLayerNorm(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('x')[0]
|
||||
scale_name = op_desc.input('scale')[0]
|
||||
y_name = op_desc.output('y')[0]
|
||||
invvar_name = op_desc.output('invvar')[0]
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
scale_spec = get_dist_tensor_spec(dist_op, scale_name)
|
||||
|
||||
y_spec = get_dist_tensor_spec(dist_op, y_name, is_input=False)
|
||||
invvar_spec = get_dist_tensor_spec(dist_op, invvar_name, is_input=False)
|
||||
|
||||
epsilon = op_desc.attr('epsilon')
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("fused_rms_norm")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, scale_spec, epsilon)
|
||||
bw_results = rule.infer_backward(
|
||||
x_spec,
|
||||
scale_spec,
|
||||
y_spec,
|
||||
invvar_spec,
|
||||
epsilon,
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[x_name, scale_name],
|
||||
[y_name, invvar_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
# default impl
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedLayerNorm("fused_rms_norm")
|
||||
)
|
||||
@@ -0,0 +1,189 @@
|
||||
# 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 ..completion import get_phi_spmd_rule
|
||||
from ..dist_attribute import DistTensorSpec
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedFusedRope(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args), build fake spec for optional args
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
input_parameters = op_desc.input_names()
|
||||
output_parameters = op_desc.output_names()
|
||||
is_input_arg_exist = lambda parameter: (
|
||||
parameter in input_parameters and op_desc.input(parameter)
|
||||
)
|
||||
is_output_arg_exist = lambda parameter: (
|
||||
parameter in output_parameters and op_desc.output(parameter)
|
||||
)
|
||||
|
||||
q = op_desc.input('q')[0]
|
||||
k = op_desc.input('k')[0] if is_input_arg_exist('k') else None
|
||||
v = op_desc.input('v')[0] if is_input_arg_exist('v') else None
|
||||
sin = op_desc.input('sin')[0] if is_input_arg_exist('sin') else None
|
||||
cos = op_desc.input('cos')[0] if is_input_arg_exist('cos') else None
|
||||
position_ids = (
|
||||
op_desc.input('position_ids')[0]
|
||||
if is_input_arg_exist('position_ids')
|
||||
else None
|
||||
)
|
||||
out_q = op_desc.output('out_q')[0]
|
||||
out_k = (
|
||||
op_desc.output('out_k')[0] if is_output_arg_exist('out_k') else None
|
||||
)
|
||||
out_v = (
|
||||
op_desc.output('out_v')[0] if is_output_arg_exist('out_v') else None
|
||||
)
|
||||
|
||||
q_spec = get_dist_tensor_spec(dist_op, q)
|
||||
k_spec = (
|
||||
get_dist_tensor_spec(dist_op, k)
|
||||
if k is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
v_spec = (
|
||||
get_dist_tensor_spec(dist_op, v)
|
||||
if v is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
sin_spec = (
|
||||
get_dist_tensor_spec(dist_op, sin)
|
||||
if sin is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
cos_spec = (
|
||||
get_dist_tensor_spec(dist_op, cos)
|
||||
if cos is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
position_ids_spec = (
|
||||
get_dist_tensor_spec(dist_op, position_ids)
|
||||
if position_ids is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
out_q_spec = get_dist_tensor_spec(dist_op, out_q, is_input=False)
|
||||
out_k_spec = (
|
||||
get_dist_tensor_spec(dist_op, out_k, is_input=False)
|
||||
if out_k is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
out_v_spec = (
|
||||
get_dist_tensor_spec(dist_op, out_v, is_input=False)
|
||||
if out_v is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
|
||||
use_neox_rotary_style = op_desc.attr("use_neox_rotary_style")
|
||||
time_major = op_desc.attr("time_major")
|
||||
rotary_emb_base = op_desc.attr("rotary_emb_base")
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("fused_rotary_position_embedding")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(
|
||||
q_spec,
|
||||
k_spec,
|
||||
v_spec,
|
||||
sin_spec,
|
||||
cos_spec,
|
||||
position_ids_spec,
|
||||
use_neox_rotary_style,
|
||||
time_major,
|
||||
rotary_emb_base,
|
||||
)
|
||||
bw_results = rule.infer_backward(
|
||||
q_spec,
|
||||
k_spec,
|
||||
v_spec,
|
||||
sin_spec,
|
||||
cos_spec,
|
||||
position_ids_spec,
|
||||
out_q_spec,
|
||||
out_k_spec,
|
||||
out_v_spec,
|
||||
use_neox_rotary_style,
|
||||
time_major,
|
||||
rotary_emb_base,
|
||||
)
|
||||
|
||||
# remove optional args in spmd results
|
||||
input_args = [q, k, v, sin, cos, position_ids]
|
||||
output_args = [out_q, out_k, out_v]
|
||||
fw_and_bw_results_without_optional_arg = []
|
||||
for results in [fw_results, bw_results]:
|
||||
input_results = results[0]
|
||||
output_results = results[1]
|
||||
input_results_without_optional_arg = []
|
||||
output_results_without_optional_arg = []
|
||||
for idx, input_arg in enumerate(input_args):
|
||||
if input_arg is not None:
|
||||
input_results_without_optional_arg.append(
|
||||
input_results[idx]
|
||||
)
|
||||
for idx, output_arg in enumerate(output_args):
|
||||
if output_arg is not None:
|
||||
output_results_without_optional_arg.append(
|
||||
output_results[idx]
|
||||
)
|
||||
fw_and_bw_results_without_optional_arg.append(
|
||||
[
|
||||
input_results_without_optional_arg,
|
||||
output_results_without_optional_arg,
|
||||
]
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names=[
|
||||
input_arg for input_arg in input_args if input_arg is not None
|
||||
],
|
||||
output_arg_names=[
|
||||
output_arg
|
||||
for output_arg in output_args
|
||||
if output_arg is not None
|
||||
],
|
||||
fw_results=fw_and_bw_results_without_optional_arg[0],
|
||||
bw_results=fw_and_bw_results_without_optional_arg[1],
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedFusedRope("fused_rotary_position_embedding")
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
# 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 ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedGatherNd(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
index_name = op_desc.input('Index')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
|
||||
x_specs = get_dist_tensor_spec(dist_op, x_name)
|
||||
index_specs = get_dist_tensor_spec(dist_op, index_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("gather_nd")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_specs, index_specs)
|
||||
bw_results = rule.infer_backward(x_specs, index_specs, output_spec)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[x_name, index_name],
|
||||
[out_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedGatherNd("gather_nd"))
|
||||
@@ -0,0 +1,151 @@
|
||||
# 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
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from paddle.base.log_helper import get_logger
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..dist_attribute import DistTensorSpec, TensorDistAttr
|
||||
from ..utils import get_dist_tensor_spec, is_dim_shard
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
class DistributedLayerNorm(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
scale_name = (
|
||||
op_desc.input('Scale')[0]
|
||||
if len(op_desc.input('Scale')) > 0
|
||||
else None
|
||||
)
|
||||
bias_name = (
|
||||
op_desc.input('Bias')[0] if len(op_desc.input('Bias')) > 0 else None
|
||||
)
|
||||
y_name = op_desc.output('Y')[0]
|
||||
var_name = op_desc.output('Variance')[0]
|
||||
mean_name = op_desc.output('Mean')[0]
|
||||
begin_norm_axis = op_desc.attr('begin_norm_axis')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
scale_spec = (
|
||||
DistTensorSpec([0], TensorDistAttr())
|
||||
if scale_name is None
|
||||
else get_dist_tensor_spec(dist_op, scale_name)
|
||||
)
|
||||
bias_spec = (
|
||||
DistTensorSpec([0], TensorDistAttr())
|
||||
if bias_name is None
|
||||
else get_dist_tensor_spec(dist_op, bias_name)
|
||||
)
|
||||
y_spec = get_dist_tensor_spec(dist_op, y_name, False)
|
||||
var_spec = get_dist_tensor_spec(dist_op, var_name, False)
|
||||
mean_spec = get_dist_tensor_spec(dist_op, mean_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("layer_norm")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(
|
||||
x_spec, scale_spec, bias_spec, 1.0, begin_norm_axis
|
||||
)
|
||||
bw_results = rule.infer_backward(
|
||||
x_spec,
|
||||
scale_spec,
|
||||
bias_spec,
|
||||
y_spec,
|
||||
var_spec,
|
||||
mean_spec,
|
||||
1.0,
|
||||
begin_norm_axis,
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
input_arg_names = [x_name]
|
||||
if scale_name is not None:
|
||||
input_arg_names.append(scale_name)
|
||||
if bias_name is not None:
|
||||
input_arg_names.append(bias_name)
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names,
|
||||
[y_name, var_name, mean_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
begin_norm_axis = op_desc.attr('begin_norm_axis')
|
||||
|
||||
# sharded on begin_norm_axis
|
||||
x_name = op_desc.input('X')[0]
|
||||
x_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_input_dims_mapping(x_name)
|
||||
)
|
||||
if (begin_norm_axis > 0) and is_dim_shard(
|
||||
x_dims_mapping[begin_norm_axis]
|
||||
):
|
||||
# TODO (ljz) support sharding on `begin_norm_axis`
|
||||
_logger.info(
|
||||
"sharding on `begin_norm_axis` is not supported yet, we resharded it as replicated"
|
||||
)
|
||||
x_dims_mapping[begin_norm_axis] = -1
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
|
||||
param_names = [op_desc.input('Scale')[0], op_desc.input('Bias')[0]]
|
||||
for p_name in param_names:
|
||||
p_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_input_dims_mapping(p_name)
|
||||
)
|
||||
p_dims_mapping[begin_norm_axis] = -1
|
||||
op_dist_attr.set_input_dims_mapping(p_name, p_dims_mapping)
|
||||
|
||||
y_name = op_desc.output('Y')[0]
|
||||
y_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_output_dims_mapping(y_name)
|
||||
)
|
||||
y_dims_mapping[begin_norm_axis] = -1
|
||||
op_dist_attr.set_input_dims_mapping(y_name, y_dims_mapping)
|
||||
|
||||
# default impl
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedLayerNorm("layer_norm"))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
|
||||
from paddle.common_ops_import import check_dtype, check_variable_and_dtype
|
||||
from paddle.distributed.utils.stream_utils import ExecutionStreamType
|
||||
from paddle.framework import core
|
||||
from paddle.static import Operator
|
||||
|
||||
from ..dist_attribute import OperatorDistAttr, TensorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_dim_mapping,
|
||||
is_dim_replicate,
|
||||
is_dim_shard,
|
||||
set_dist_op_desc_original_id,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
|
||||
|
||||
class DistributedPNorm(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedPNorm("p_norm"))
|
||||
|
||||
|
||||
# Data Parallel
|
||||
class DistributedPNormImpl0(DistributedOperatorImpl):
|
||||
"""
|
||||
TODO: p_norm scene
|
||||
|
||||
1. axis == None, isinstance(p, (int, float)), asvector = True
|
||||
1.1 x_dims_mapping == [0, -1, -1]
|
||||
allgather input if it is split by dp group
|
||||
1.2 x_dims_mapping == [-1, 0, -1]
|
||||
allgather, split and concat input if it is split by mp group
|
||||
2. isinstance(axis, int), asvector = False
|
||||
1.1 axis == 0 and x_dims_mapping == [0, -1, -1]
|
||||
allgather input if it's input[0] is splited by dp group.
|
||||
1.2 axis == 1 and x_dims_mapping == [-1, 0, -1]
|
||||
allgather, split and concat input if it's input[1] is split by mp group
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
axis = op_desc.attr('axis')
|
||||
asvector = op_desc.attr('asvector')
|
||||
x_name = op_desc.input('X')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
if is_dim_replicate(x_dims_mapping[0]):
|
||||
return False
|
||||
# Other dimensions must be replicate except the batch dimension
|
||||
for mapping in x_dims_mapping[1:]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
if not (axis == -1 and asvector) and not (axis == 0 and not asvector):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (
|
||||
(not self.is_input_compatible(dist_op))
|
||||
or (not self.is_output_compatible(dist_op))
|
||||
or (not self.is_compatible(dist_op))
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
axis = op_desc.attr('axis')
|
||||
keepdim = op_desc.attr('keepdim')
|
||||
|
||||
batch_dim_mappings = []
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
|
||||
compatible_dim_mapping = compute_compatible_dim_mapping(
|
||||
batch_dim_mappings
|
||||
)
|
||||
if compatible_dim_mapping is None:
|
||||
return False
|
||||
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if (
|
||||
len(dims_mapping) >= 1
|
||||
and compatible_dim_mapping != dims_mapping[0]
|
||||
):
|
||||
dims_mapping[0] = compatible_dim_mapping
|
||||
op_dist_attr.set_input_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
|
||||
if axis == 0 and not keepdim:
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if len(dims_mapping) >= 1 and dims_mapping[0] != -1:
|
||||
dims_mapping[0] = -1
|
||||
op_dist_attr.set_output_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
else:
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if (
|
||||
len(dims_mapping) >= 1
|
||||
and compatible_dim_mapping != dims_mapping[0]
|
||||
):
|
||||
dims_mapping[0] = compatible_dim_mapping
|
||||
op_dist_attr.set_output_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(X_var.name)
|
||||
for axis in range(len(in_dims_mapping)):
|
||||
if in_dims_mapping[axis] != -1:
|
||||
break
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
check_variable_and_dtype(
|
||||
X_var, 'x', ['float16', 'float32', 'float64'], 'norm'
|
||||
)
|
||||
check_dtype(
|
||||
X_var.dtype, 'dtype', ['float16', 'float32', 'float64'], 'norm'
|
||||
)
|
||||
|
||||
# 2. insert all_gather op
|
||||
# create all_gather output var
|
||||
allgather_out = main_block.create_var(
|
||||
name=".".join(["all_gather", X_var.name]),
|
||||
dtype=X_var.dtype,
|
||||
shape=X_var.shape,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=X_var.stop_gradient,
|
||||
)
|
||||
# set allgather_out tensor dist_attr
|
||||
allgather_out_dist_attr = TensorDistAttr()
|
||||
allgather_out_dist_attr.process_mesh = op_dist_attr.process_mesh
|
||||
allgather_out_dist_attr.chunk_id = op_dist_attr.chunk_id
|
||||
allgather_out_dist_attr.dims_mapping = [
|
||||
-1 for i in range(len(allgather_out.shape))
|
||||
]
|
||||
ctx.set_tensor_dist_attr_for_program(
|
||||
allgather_out, allgather_out_dist_attr
|
||||
)
|
||||
all_gather_op = main_block.append_op(
|
||||
type='all_gather',
|
||||
inputs={'x': [X_var]},
|
||||
outputs={'out': [allgather_out]},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'use_calc_stream': True,
|
||||
'nranks': group.nranks,
|
||||
'op_role': src_op.attr('op_role'),
|
||||
},
|
||||
)
|
||||
# set all_gather op dist_attr
|
||||
allgather_op_dist_attr = OperatorDistAttr()
|
||||
allgather_op_dist_attr.process_mesh = op_dist_attr.process_mesh
|
||||
allgather_op_dist_attr.chunk_id = op_dist_attr.chunk_id
|
||||
allgather_op_dist_attr.set_input_dims_mapping(
|
||||
X_var.name, in_dims_mapping
|
||||
)
|
||||
allgather_op_dist_attr.set_output_dims_mapping(
|
||||
allgather_out.name, allgather_out_dist_attr.dims_mapping
|
||||
)
|
||||
allgather_op_dist_attr.execution_stream = (
|
||||
ExecutionStreamType.DefaultStream.value
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(all_gather_op, allgather_op_dist_attr)
|
||||
|
||||
# 3. copy p_norm op desc and reset input name
|
||||
# rename input
|
||||
kwargs['X'] = [allgather_out.name]
|
||||
# replicate op in dist program
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(dist_op_desc, src_op.desc, ctx)
|
||||
for input_name in src_op.desc.input_names():
|
||||
dist_op_desc.set_input(input_name, kwargs[input_name])
|
||||
for output_name in src_op.desc.output_names():
|
||||
dist_op_desc.set_output(output_name, kwargs[output_name])
|
||||
pnorm_op = Operator(main_block, dist_op_desc)
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
allgather_out.name, allgather_out_dist_attr.dims_mapping
|
||||
)
|
||||
# Remove the unrelated dist attr
|
||||
op_dist_attr.del_input_dist_attr(X_var.name)
|
||||
ctx.set_op_dist_attr_for_program(pnorm_op, op_dist_attr)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert op_dist_attr is not None
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in backward_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
backward_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in backward_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
backward_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
X_grad_var = main_block._var_recursive(kwargs['X@GRAD'][0])
|
||||
|
||||
# 1. copy p_norm_grad op and reset input name and output name
|
||||
new_kwargs = copy.deepcopy(kwargs)
|
||||
new_kwargs['X'] = [".".join(["all_gather", X_var.name])]
|
||||
new_X_var = main_block._var_recursive(new_kwargs['X'][0])
|
||||
new_X_grad = main_block.create_var(
|
||||
name=".".join(["all_gather", X_grad_var.name]),
|
||||
dtype=X_grad_var.dtype,
|
||||
shape=new_X_var.shape,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=X_grad_var.stop_gradient,
|
||||
)
|
||||
new_kwargs['X@GRAD'] = [new_X_grad.name]
|
||||
new_X_var_dist_attr = ctx.get_tensor_dist_attr_for_program(new_X_var)
|
||||
ctx.set_tensor_dist_attr_for_program(new_X_grad, new_X_var_dist_attr)
|
||||
# replicate op in dist program with new kwargs
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(backward_op.desc)
|
||||
# Refer to the related dist op
|
||||
set_dist_op_desc_original_id(dist_op_desc, backward_op.desc, ctx)
|
||||
for input_name in backward_op.desc.input_names():
|
||||
dist_op_desc.set_input(input_name, new_kwargs[input_name])
|
||||
for output_name in backward_op.desc.output_names():
|
||||
dist_op_desc.set_output(output_name, new_kwargs[output_name])
|
||||
p_norm_grad_op = Operator(main_block, dist_op_desc)
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
new_X_var.name, new_X_var_dist_attr.dims_mapping
|
||||
)
|
||||
# Store X_grad_var dims_mapping for later use
|
||||
X_grad_var_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
X_grad_var.name
|
||||
)
|
||||
# Remove the unrelated dist attr
|
||||
op_dist_attr.del_input_dist_attr(X_var.name)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
new_X_grad.name, new_X_var_dist_attr.dims_mapping
|
||||
)
|
||||
# Remove the unrelated dist attr
|
||||
op_dist_attr.del_output_dist_attr(X_grad_var.name)
|
||||
ctx.set_op_dist_attr_for_program(p_norm_grad_op, op_dist_attr)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
# 2. insert slice op
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
dims_mapping = [0] + [-1 for _ in range(len(new_X_grad.shape) - 1)]
|
||||
from ..reshard import Resharder
|
||||
|
||||
partition_idx = Resharder.compute_partition_index(
|
||||
rank_id,
|
||||
new_X_grad.shape,
|
||||
dims_mapping,
|
||||
process_mesh_shape,
|
||||
process_mesh_group,
|
||||
)
|
||||
slice_starts = []
|
||||
slice_ends = []
|
||||
slices_axes = []
|
||||
for idx, item in enumerate(partition_idx):
|
||||
slice_starts.append(item[0])
|
||||
slice_ends.append(item[1])
|
||||
slices_axes.append(idx)
|
||||
|
||||
infer_flags = [1 for i in range(len(slices_axes))]
|
||||
attrs = {
|
||||
"axes": slices_axes,
|
||||
"starts": slice_starts,
|
||||
"ends": slice_ends,
|
||||
"infer_flags": infer_flags,
|
||||
"op_role": backward_op.attr('op_role'),
|
||||
}
|
||||
slice_op = main_block.append_op(
|
||||
type='slice',
|
||||
inputs={'Input': [new_X_grad]},
|
||||
outputs={'Out': [X_grad_var]},
|
||||
attrs=attrs,
|
||||
)
|
||||
slice_op_dist_attr = OperatorDistAttr()
|
||||
slice_op_dist_attr.process_mesh = op_dist_attr.process_mesh
|
||||
slice_op_dist_attr.chunk_id = op_dist_attr.chunk_id
|
||||
slice_op_dist_attr.set_input_dims_mapping(
|
||||
new_X_grad.name, new_X_var_dist_attr.dims_mapping
|
||||
)
|
||||
slice_op_dist_attr.set_output_dims_mapping(
|
||||
X_grad_var.name, X_grad_var_dims_mapping
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(slice_op, slice_op_dist_attr)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"p_norm", DistributedPNormImpl0("data_parallel")
|
||||
)
|
||||
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import copy
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
get_dist_tensor_spec,
|
||||
is_dim_shard,
|
||||
set_dist_op_desc_original_id,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedReduceSum(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert len(op_desc.input_arg_names()) == 1, (
|
||||
f"reduce_sum op [{op_desc.type}] has [{len(op_desc.input_arg_names())}] inputs"
|
||||
)
|
||||
input_arg_name = op_desc.input_arg_names()[0]
|
||||
assert len(op_desc.output_arg_names()) == 1, (
|
||||
f"reduce_sum op [{op_desc.type}] has [{len(op_desc.output_arg_names())}] outputs"
|
||||
)
|
||||
output_arg_name = op_desc.output_arg_names()[0]
|
||||
keep_dim = op_desc.attr('keep_dim')
|
||||
dims = op_desc.attr('dim')
|
||||
|
||||
# TODO (zhangyichen) replace dist tensor spec by dist tensor in future.
|
||||
input_spec = get_dist_tensor_spec(dist_op, input_arg_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_name, False)
|
||||
# len(dims) == 0 means reduce_all
|
||||
if len(dims) == 0:
|
||||
dims = list(range(len(input_spec.shape)))
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("reduce_sum")
|
||||
fw_results = rule.infer_forward(input_spec, dims, keep_dim)
|
||||
bw_results = rule.infer_backward(
|
||||
input_spec, output_spec, dims, keep_dim
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [input_arg_name], [output_arg_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
# NOTE this function will be remove once we use local reshard to replace distopimpls
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
op_desc = dist_op.serial_op.desc
|
||||
input_name = op_desc.input_arg_names()[0]
|
||||
input_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_input_dims_mapping(input_name)
|
||||
)
|
||||
axes = op_desc.attr('dim')
|
||||
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
reverted = False
|
||||
|
||||
def is_partial_reduce(axes, dims_mapping):
|
||||
# FIXME(ljz) Hack for performance:
|
||||
# if the reduce result is a scalar, it is the loss reduce in GPT case,
|
||||
# and if any axis of reduce input is sharded, the result loss would be partial.
|
||||
# BUT we keep the loss as partial instead of allreduce it for performance, since it would effect the backward.
|
||||
# we should use an optimization pass for the Hack in future.
|
||||
if len(axes) != 0 and (len(axes) < len(dims_mapping)):
|
||||
for axis in axes:
|
||||
if is_dim_shard(dims_mapping[axis]):
|
||||
return True # reverted
|
||||
return False
|
||||
|
||||
# if reduce_axis is sharded, the output is partial and need to be allreduce
|
||||
if is_partial_reduce(axes, input_dims_mapping):
|
||||
# TODO (ljz) support reduce where the reduce_axis is sharded
|
||||
dist_op.dist_attr = original_op_dist_attr
|
||||
reverted = True
|
||||
# if reduce_axis is unsharded, NO extra operator need.
|
||||
else:
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
return reverted
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedReduceSum("reduce_sum"))
|
||||
|
||||
|
||||
class DistributedReduceSumPrimitive(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedReduceSumPrimitive("reduce_sum_p")
|
||||
)
|
||||
|
||||
|
||||
# Batch Dimension ReduceSum Primitive
|
||||
class DistributedReduceSumPrimitiveImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
return len(op_desc.input_arg_names()) == 1
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
outputs = op_desc.output_arg_names()
|
||||
|
||||
if len(outputs) != 1:
|
||||
return False
|
||||
|
||||
output_name = outputs[0]
|
||||
output_var = dist_op.serial_op.block._var_recursive(output_name)
|
||||
if output_var.shape != ():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
return self.is_input_compatible(dist_op) and self.is_output_compatible(
|
||||
dist_op
|
||||
)
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
# replicate op in dist program
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(dist_op_desc, src_op.desc, ctx)
|
||||
for input_name in src_op.desc.input_names():
|
||||
dist_op_desc.set_input(input_name, kwargs[input_name])
|
||||
for output_name in src_op.desc.output_names():
|
||||
dist_op_desc.set_output(output_name, kwargs[output_name])
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
# batch dimension synchronization
|
||||
var_name = src_op.output_arg_names[0]
|
||||
sync_group = new_process_group(ctx.data_parallel_group)
|
||||
allreduce_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': [var_name]},
|
||||
outputs={'out': [var_name]},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'reduce_type': paddle.distributed.ReduceOp.SUM,
|
||||
OP_ROLE_KEY: OpRole.Forward,
|
||||
},
|
||||
)
|
||||
|
||||
# dist attr
|
||||
var = main_block._var_recursive(var_name)
|
||||
tensor_dist_attr = ctx.get_tensor_dist_attr_for_program(var)
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
new_op_attr = OperatorDistAttr()
|
||||
new_op_attr.process_mesh = op_dist_attr.process_mesh
|
||||
new_op_attr.set_output_dims_mapping(
|
||||
var.name, tensor_dist_attr.dims_mapping
|
||||
)
|
||||
new_op_attr.set_input_dims_mapping(
|
||||
var.name, tensor_dist_attr.dims_mapping
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(allreduce_op, new_op_attr)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
raise RuntimeError("primitive operator does NOT have backward function")
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"reduce_sum_p",
|
||||
DistributedReduceSumPrimitiveImpl0("batch_dimension_reduce_sum_p"),
|
||||
)
|
||||
@@ -0,0 +1,866 @@
|
||||
# Copyright (c) 2021 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 paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
Reshape2GradOpCost,
|
||||
Reshape2OpCost,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import (
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
is_dim_shard,
|
||||
set_dist_op_desc_original_id,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
is_parameter_related,
|
||||
merge_forward_backward_dims_mapping,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedReshape2(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert dist_op.serial_op.type == "reshape2", (
|
||||
f"{dist_op.serial_op.type} is not supported by dist reshape yet."
|
||||
)
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
xshape_name = op_desc.output('XShape')[0]
|
||||
shape = op_desc.attr('shape')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("reshape")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, shape)
|
||||
bw_results = rule.infer_backward(x_spec, output_spec, shape)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
# step4: update xshape
|
||||
inferred_input_dims_mappings, _ = merge_forward_backward_dims_mapping(
|
||||
fw_results, bw_results
|
||||
)
|
||||
dist_op.dist_attr.set_output_dims_mapping(
|
||||
xshape_name, [-1] + inferred_input_dims_mappings[0]
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
reverted = False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
# all reshape mapping to impl0
|
||||
op_dist_attr.impl_type = "reshape2"
|
||||
op_dist_attr.impl_idx = 0
|
||||
|
||||
return reverted
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedReshape2("reshape2"))
|
||||
|
||||
|
||||
class DistributedReshapeImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
res = []
|
||||
op = dist_op.serial_op
|
||||
dist_attr = dist_op.dist_attr
|
||||
|
||||
shape_list = op.desc.attr("shape")
|
||||
# got dist attribute info
|
||||
dim_mapping = dist_attr.get_output_dims_mapping(op.output("Out")[0])
|
||||
process_mesh_shape = dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_attr.process_mesh.process_ids
|
||||
for key in desc_mapping:
|
||||
desc_mapping[key]["shape"] = shape_list
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2OpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
return res
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2GradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping) - 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping) - 1:
|
||||
return False
|
||||
|
||||
if is_dim_shard(out_dims_mapping[-1]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for idx, dim_mapping in enumerate(out_dims_mapping[:-1]):
|
||||
if x_dims_mapping[idx] != dim_mapping:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[0] != -1:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[1:] != x_dims_mapping[:]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
x_shape_dims_mapping[i + 1] = x_dims_mapping[i]
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
x_shape_name, x_shape_dims_mapping
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
XShape_var = main_block._var_recursive(kwargs['XShape'][0])
|
||||
shape_list = src_op.desc.attr("shape")
|
||||
ShapeTensor_var_list = []
|
||||
for name in kwargs['ShapeTensor']:
|
||||
ShapeTensor_var_list.append(name)
|
||||
Shape_var_list = []
|
||||
for name in kwargs['Shape']:
|
||||
Shape_var_list.append(name)
|
||||
|
||||
# got dist attribute info
|
||||
dim_mapping = op_dist_attr.get_output_dims_mapping(Out_var.name)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# create op
|
||||
new_op = main_block.append_op(type='nop')
|
||||
new_op_desc = new_op.desc
|
||||
new_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(new_op_desc, src_op.desc, ctx)
|
||||
new_op_desc.set_input('ShapeTensor', ShapeTensor_var_list)
|
||||
new_op_desc.set_input('Shape', Shape_var_list)
|
||||
new_op_desc.set_input('X', [X_var.name])
|
||||
new_op_desc.set_output('XShape', [XShape_var.name])
|
||||
new_op_desc.set_output('Out', [Out_var.name])
|
||||
new_op_desc._set_attr('shape', shape_list)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
class DistributedReshapeImpl1(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
res = []
|
||||
op = dist_op.serial_op
|
||||
dist_attr = dist_op.dist_attr
|
||||
|
||||
shape_list = op.desc.attr("shape")
|
||||
# got dist attribute info
|
||||
dim_mapping = dist_attr.get_output_dims_mapping(op.output("Out")[0])
|
||||
process_mesh_shape = dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_attr.process_mesh.process_ids
|
||||
for key in desc_mapping:
|
||||
desc_mapping[key]["shape"] = shape_list
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2OpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
return res
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2GradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping) + 1:
|
||||
return False
|
||||
|
||||
if is_dim_shard(x_dims_mapping[-1]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping) + 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
if is_dim_shard(x_dims_mapping[-1]):
|
||||
return False
|
||||
|
||||
for idx, item in enumerate(x_dims_mapping[:-1]):
|
||||
if out_dims_mapping[idx] != item:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[0] != -1:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[1:] != x_dims_mapping[:]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
for i in range(len(out_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
x_shape_dims_mapping[i + 1] = x_dims_mapping[i]
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
x_shape_name, x_shape_dims_mapping
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
XShape_var = main_block._var_recursive(kwargs['XShape'][0])
|
||||
shape_list = src_op.desc.attr("shape")
|
||||
ShapeTensor_var_list = []
|
||||
for name in kwargs['ShapeTensor']:
|
||||
ShapeTensor_var_list.append(name)
|
||||
Shape_var_list = []
|
||||
for name in kwargs['Shape']:
|
||||
Shape_var_list.append(name)
|
||||
|
||||
# got dist attribute info
|
||||
dim_mapping = op_dist_attr.get_output_dims_mapping(Out_var.name)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# create op
|
||||
new_op = main_block.append_op(type='nop')
|
||||
new_op_desc = new_op.desc
|
||||
new_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(new_op_desc, src_op.desc, ctx)
|
||||
new_op_desc.set_input('ShapeTensor', ShapeTensor_var_list)
|
||||
new_op_desc.set_input('Shape', Shape_var_list)
|
||||
new_op_desc.set_input('X', [X_var.name])
|
||||
new_op_desc.set_output('XShape', [XShape_var.name])
|
||||
new_op_desc.set_output('Out', [Out_var.name])
|
||||
new_op_desc._set_attr('shape', shape_list)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
class DistributedReshapeImpl2(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
res = []
|
||||
op = dist_op.serial_op
|
||||
dist_attr = dist_op.dist_attr
|
||||
|
||||
shape_list = op.desc.attr("shape")
|
||||
# got dist attribute info
|
||||
dim_mapping = dist_attr.get_output_dims_mapping(op.output("Out")[0])
|
||||
process_mesh_shape = dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_attr.process_mesh.process_ids
|
||||
for key in desc_mapping:
|
||||
desc_mapping[key]["shape"] = shape_list
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2OpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
return res
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2GradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_name = op_desc.input('X')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
for idx, item in enumerate(x_dims_mapping[:-1]):
|
||||
if out_dims_mapping[idx] != item:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[0] != -1:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[1:] != out_dims_mapping[:]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
for i in range(len(out_dims_mapping) - 1):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
for i in range(len(out_dims_mapping)):
|
||||
x_shape_dims_mapping[i + 1] = out_dims_mapping[i]
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
x_shape_name, x_shape_dims_mapping
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
XShape_var = main_block._var_recursive(kwargs['XShape'][0])
|
||||
shape_list = src_op.desc.attr("shape")
|
||||
ShapeTensor_var_list = []
|
||||
for name in kwargs['ShapeTensor']:
|
||||
ShapeTensor_var_list.append(name)
|
||||
Shape_var_list = []
|
||||
for name in kwargs['Shape']:
|
||||
Shape_var_list.append(name)
|
||||
|
||||
# got dist attribute info
|
||||
out_dim_mapping = op_dist_attr.get_output_dims_mapping(Out_var.name)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(out_dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# create op
|
||||
new_op = main_block.append_op(type='nop')
|
||||
new_op_desc = new_op.desc
|
||||
new_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(new_op_desc, src_op.desc, ctx)
|
||||
new_op_desc.set_input('ShapeTensor', ShapeTensor_var_list)
|
||||
new_op_desc.set_input('Shape', Shape_var_list)
|
||||
new_op_desc.set_input('X', [X_var.name])
|
||||
new_op_desc.set_output('XShape', [XShape_var.name])
|
||||
new_op_desc.set_output('Out', [Out_var.name])
|
||||
new_op_desc._set_attr('shape', shape_list)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"reshape2", DistributedReshapeImpl0("add_one_dim_back")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"reshape2", DistributedReshapeImpl1("remove_one_dim_back")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"reshape2", DistributedReshapeImpl2("same_dim_shape")
|
||||
)
|
||||
@@ -0,0 +1,192 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..cost import (
|
||||
_g_op_cost_factory,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import compute_compatible_and_update_dim_mapping
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
is_parameter_related,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedScale(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
# TODO remove assign dist op
|
||||
# register_distributed_operator_impl_container(DistributedScale("scale"))
|
||||
# register_distributed_operator_impl_container(DistributedScale("fill_any_like"))
|
||||
# register_distributed_operator_impl_container(DistributedScale("where"))
|
||||
# register_distributed_operator_impl_container(DistributedScale("tanh"))
|
||||
|
||||
|
||||
class DistributedScaleImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
"""Calculate the cost by the op role."""
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res_cost = [cost_mapping]
|
||||
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
backward_op = dist_op.serial_op
|
||||
op_type = backward_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
need_gradient_allreduce = True
|
||||
break
|
||||
|
||||
if need_gradient_allreduce:
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
varname
|
||||
)
|
||||
mesh_shape = process_mesh.shape
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
return res
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
in_dims_mappings = []
|
||||
for in_name in op_desc.input_arg_names():
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
in_dims_mappings.append(in_dims_mapping)
|
||||
|
||||
for x_dims_mapping in in_dims_mappings:
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
# register_distributed_operator_impl("scale", DistributedScaleImpl("scale"))
|
||||
# register_distributed_operator_impl(
|
||||
# "fill_any_like", DistributedScaleImpl("fill_any_like")
|
||||
# )
|
||||
# register_distributed_operator_impl("where", DistributedScaleImpl("where"))
|
||||
# register_distributed_operator_impl("tanh", DistributedScaleImpl("tanh"))
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..utils import is_dim_shard
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedShape(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedShape("shape"))
|
||||
|
||||
|
||||
class DistributedShapeImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
assert len(out_dims_mapping) == 1
|
||||
if is_dim_shard(out_dims_mapping[0]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl("shape", DistributedShapeImpl("shape"))
|
||||
@@ -0,0 +1,178 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from ..utils import compute_compatible_dim_mapping, is_dim_shard
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedSlice(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedSlice("slice"))
|
||||
|
||||
|
||||
class DistributedSliceImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
in_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
in_var = dist_op.serial_op.block._var_recursive(in_name)
|
||||
out_var = dist_op.serial_op.block._var_recursive(out_name)
|
||||
axes = op_desc.attr('axes')
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
for axis in axes:
|
||||
if (
|
||||
is_dim_shard(in_dims_mapping[axis])
|
||||
and in_var.shape[axis] != out_var.shape[axis]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
in_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
in_var = dist_op.serial_op.block._var_recursive(in_name)
|
||||
out_var = dist_op.serial_op.block._var_recursive(out_name)
|
||||
axes = op_desc.attr('axes')
|
||||
decrease_axis = op_desc.attr('decrease_axis')
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
ref_indices = []
|
||||
for i in range(len(in_dims_mapping)):
|
||||
if i not in decrease_axis:
|
||||
ref_indices.append(i)
|
||||
if ref_indices == []:
|
||||
assert len(out_dims_mapping) == 0
|
||||
else:
|
||||
for i in range(len(out_dims_mapping)):
|
||||
ref_index = ref_indices[i]
|
||||
if (
|
||||
ref_index in axes
|
||||
and is_dim_shard(out_dims_mapping[i])
|
||||
and in_var.shape[ref_index] != out_var.shape[ref_index]
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
in_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
decrease_axis = op_desc.attr('decrease_axis')
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if len(in_dims_mapping) - len(decrease_axis) != 0 and len(
|
||||
out_dims_mapping
|
||||
) != len(in_dims_mapping) - len(decrease_axis):
|
||||
return False
|
||||
|
||||
new_out_dims_mapping = []
|
||||
for i in range(len(in_dims_mapping)):
|
||||
if i not in decrease_axis:
|
||||
new_out_dims_mapping.append(in_dims_mapping[i])
|
||||
if new_out_dims_mapping == []:
|
||||
new_out_dims_mapping = [-1]
|
||||
if new_out_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (
|
||||
(not self.is_input_compatible(dist_op))
|
||||
or (not self.is_output_compatible(dist_op))
|
||||
or (not self.is_compatible(dist_op))
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
in_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
decrease_axis = op_desc.attr('decrease_axis')
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
ref_dims_mapping = []
|
||||
ref_indices = []
|
||||
for i in range(len(in_dims_mapping)):
|
||||
if i not in decrease_axis:
|
||||
ref_dims_mapping.append(in_dims_mapping[i])
|
||||
ref_indices.append(i)
|
||||
|
||||
if ref_dims_mapping == []:
|
||||
assert len(ref_dims_mapping) == len(out_dims_mapping)
|
||||
changed = False
|
||||
else:
|
||||
assert len(ref_dims_mapping) == len(out_dims_mapping)
|
||||
for i in range(len(out_dims_mapping)):
|
||||
compatible_dim_mapping = compute_compatible_dim_mapping(
|
||||
[out_dims_mapping[i], ref_dims_mapping[i]]
|
||||
)
|
||||
if compatible_dim_mapping is None:
|
||||
continue
|
||||
if ref_dims_mapping[i] != compatible_dim_mapping:
|
||||
in_dims_mapping[ref_indices[i]] = compatible_dim_mapping
|
||||
changed = True
|
||||
if out_dims_mapping[i] != compatible_dim_mapping:
|
||||
out_dims_mapping[i] = compatible_dim_mapping
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(in_name, in_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"slice", DistributedSliceImpl("decrease_in_axis")
|
||||
)
|
||||
@@ -0,0 +1,200 @@
|
||||
# Copyright (c) 2021 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 paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..cost import (
|
||||
SoftmaxGradOpCost,
|
||||
SoftmaxOpCost,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import compute_compatible_and_update_dim_mapping, is_dim_shard
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
is_parameter_related,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedSoftmax(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedSoftmax("softmax"))
|
||||
|
||||
|
||||
class DistributedSoftmaxImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
SoftmaxOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
|
||||
res_cost = [cost_mapping]
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
SoftmaxGradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
# if axis != -1 and axis != len(x_dims_mapping) - 1:
|
||||
# return False
|
||||
|
||||
if is_dim_shard(x_dims_mapping[axis]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
# if axis != -1 and axis != len(out_dims_mapping) - 1:
|
||||
# return False
|
||||
|
||||
if is_dim_shard(out_dims_mapping[axis]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
# if axis != -1 and axis != len(x_dims_mapping) - 1:
|
||||
# return False
|
||||
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"softmax", DistributedSoftmaxImpl("replicate_last_axis")
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import (
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
is_dim_shard,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedSplit(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
assert len(op_desc.input('AxisTensor')) == 0, (
|
||||
"Attribute AxisTensor is not supported by dist split."
|
||||
)
|
||||
assert len(op_desc.input('SectionsTensorList')) == 0, (
|
||||
"Attribute SectionsTensorList is not supported by dist split."
|
||||
)
|
||||
output_arg_names = op_desc.output('Out')
|
||||
|
||||
num = op_desc.attr('num')
|
||||
sections = op_desc.attr('sections')
|
||||
if num:
|
||||
assert (sections is None) or (len(sections) == 0), (
|
||||
f"Both Attributes of num: {num} and sections: {sections} are specified."
|
||||
)
|
||||
first_attr = num
|
||||
rule_type = "split_with_num"
|
||||
else:
|
||||
assert not num, (
|
||||
f"Both Attributes of num: {num} and sections: {sections} are specified."
|
||||
)
|
||||
first_attr = sections
|
||||
rule_type = "split"
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
num_outputs = len(output_arg_names)
|
||||
output_specs = []
|
||||
for i in range(num_outputs):
|
||||
output_specs.append(
|
||||
get_dist_tensor_spec(dist_op, output_arg_names[i], False)
|
||||
)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule(rule_type)
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, first_attr, axis)
|
||||
bw_results = rule.infer_backward(x_spec, output_specs, first_attr, axis)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], output_arg_names, fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all split op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedSplit("split"))
|
||||
register_distributed_operator_impl_container(DistributedSplit("split_with_num"))
|
||||
|
||||
|
||||
class DistributedSplitImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
if is_dim_shard(x_dims_mapping[axis]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_names = op_desc.output('Out')
|
||||
axis = op_desc.attr('axis')
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if is_dim_shard(out_dims_mapping[axis]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
out_names = op_desc.output('Out')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Out')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
out_name, out_dims_mapping
|
||||
)
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
return changed
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (
|
||||
(not self.is_input_compatible(dist_op))
|
||||
or (not self.is_output_compatible(dist_op))
|
||||
or (not self.is_compatible(dist_op))
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"split", DistributedSplitImpl("replicate_in_axis")
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
# 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 ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedStack(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
input_specs = []
|
||||
for name in input_arg_names:
|
||||
input_specs.append(get_dist_tensor_spec(dist_op, name))
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_names[0], False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("stack")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(input_specs, axis)
|
||||
bw_results = rule.infer_backward(input_specs, output_spec, axis)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names,
|
||||
output_arg_names,
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedStack("stack"))
|
||||
@@ -0,0 +1,81 @@
|
||||
# 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 ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedStridedSlice(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('Input')[0]
|
||||
y_name = op_desc.output('Out')[0]
|
||||
axes = op_desc.attr('axes')
|
||||
starts = op_desc.attr('starts')
|
||||
ends = op_desc.attr('ends')
|
||||
strides = op_desc.attr('strides')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
y_spec = get_dist_tensor_spec(dist_op, y_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("strided_slice")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, axes, starts, ends, strides)
|
||||
bw_results = rule.infer_backward(
|
||||
x_spec,
|
||||
y_spec,
|
||||
axes,
|
||||
starts,
|
||||
ends,
|
||||
strides,
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[x_name],
|
||||
[y_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedStridedSlice("strided_slice")
|
||||
)
|
||||
@@ -0,0 +1,72 @@
|
||||
# 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 ..completion import get_phi_spmd_rule
|
||||
from ..utils import (
|
||||
get_dist_tensor_spec,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedTile(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert dist_op.serial_op.type == "tile", (
|
||||
f"{dist_op.serial_op.type} is not supported by dist transpose yet."
|
||||
)
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
repeat_times = op_desc.attr('repeat_times')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("tile")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, repeat_times)
|
||||
bw_results = rule.infer_backward(x_spec, output_spec, repeat_times)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all elementwise op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedTile("tile"))
|
||||
@@ -0,0 +1,270 @@
|
||||
# Copyright (c) 2021 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 paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
Transpose2GradOpCost,
|
||||
Transpose2OpCost,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import (
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
is_parameter_related,
|
||||
merge_forward_backward_dims_mapping,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedTranspose2(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert dist_op.serial_op.type == "transpose2", (
|
||||
f"{dist_op.serial_op.type} is not supported by dist transpose yet."
|
||||
)
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
xshape_name = op_desc.output('XShape')[0]
|
||||
axes = op_desc.attr('axis')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("transpose")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, axes)
|
||||
bw_results = rule.infer_backward(x_spec, output_spec, axes)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
# step4: update xshape
|
||||
inferred_input_dims_mappings, _ = merge_forward_backward_dims_mapping(
|
||||
fw_results, bw_results
|
||||
)
|
||||
dist_op.dist_attr.set_output_dims_mapping(
|
||||
xshape_name, [-1] + inferred_input_dims_mappings[0]
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all elementwise op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedTranspose2("transpose2")
|
||||
)
|
||||
|
||||
|
||||
class DistributedTranspose2Impl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = False
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
perm = op_desc.attr('axis')
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
new_dims_mapping = [-1 for i in range(len(x_dims_mapping))]
|
||||
for i in range(len(x_dims_mapping)):
|
||||
new_dims_mapping[i] = x_dims_mapping[perm[i]]
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping):
|
||||
return False
|
||||
|
||||
if new_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[0] != -1:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[1:] != x_dims_mapping[:]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
perm = op_desc.attr('axis')
|
||||
|
||||
assert len(x_dims_mapping) == len(perm)
|
||||
|
||||
new_dims_mapping = [-1 for i in range(len(x_dims_mapping))]
|
||||
for i in range(len(x_dims_mapping)):
|
||||
new_dims_mapping[i] = x_dims_mapping[perm[i]]
|
||||
|
||||
for i in range(len(out_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[new_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
if x_dims_mapping[perm[i]] != new_dims_mapping[i]:
|
||||
x_dims_mapping[perm[i]] = new_dims_mapping[i]
|
||||
changed = True
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
x_shape_dims_mapping[i + 1] = x_dims_mapping[i]
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
x_shape_name, x_shape_dims_mapping
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Transpose2OpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
|
||||
res_cost = [cost_mapping]
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Transpose2GradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"transpose2", DistributedTranspose2Impl("same_mapping_transpose")
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedUnSqueeze2(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
axes_tensor = op_desc.input('AxesTensor')
|
||||
axes_tensor_list = op_desc.input('AxesTensorList')
|
||||
assert len(axes_tensor) == 0 and len(axes_tensor_list) == 0
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
axes = op_desc.attr('axes')
|
||||
|
||||
input_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("unsqueeze2")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(input_spec, axes)
|
||||
bw_results = rule.infer_backward(input_spec, output_spec, axes)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[x_name],
|
||||
[out_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedUnSqueeze2("unsqueeze2")
|
||||
)
|
||||
@@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2021 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 ..utils import set_dist_op_desc_original_id
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
|
||||
|
||||
class DistributedUpdateLossScaling(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedUpdateLossScaling("update_loss_scaling")
|
||||
)
|
||||
|
||||
|
||||
class DistributedUpdateLossScalingImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._name = name
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's is_input_compatible should not be called !"
|
||||
)
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's is_output_compatible should not be called !"
|
||||
)
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's is_auto_compatible should not be called !"
|
||||
)
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's update_dims_mapping should not be called !"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's forward should not be called !"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# the backward function only filter the gradient with current rank id
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.main_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
assert rank_id in dist_attr.process_mesh.process_ids
|
||||
|
||||
assert 'X' in kwargs, "input [{}] is not given".format('X')
|
||||
assert 'FoundInfinite' in kwargs, "input [{}] is not given".format(
|
||||
'FoundInfinite'
|
||||
)
|
||||
assert 'PrevLossScaling' in kwargs, "input [{}] is not given".format(
|
||||
'PrevLossScaling'
|
||||
)
|
||||
assert 'InGoodSteps' in kwargs, "input [{}] is not given".format(
|
||||
'InGoodSteps'
|
||||
)
|
||||
assert 'InBadSteps' in kwargs, "input [{}] is not given".format(
|
||||
'InBadSteps'
|
||||
)
|
||||
|
||||
assert 'Out' in kwargs, "output [{}] is not given".format('Out')
|
||||
assert 'LossScaling' in kwargs, "output [{}] is not given".format(
|
||||
'LossScaling'
|
||||
)
|
||||
assert 'OutGoodSteps' in kwargs, "output [{}] is not given".format(
|
||||
'OutGoodSteps'
|
||||
)
|
||||
assert 'OutBadSteps' in kwargs, "output [{}] is not given".format(
|
||||
'OutBadSteps'
|
||||
)
|
||||
|
||||
assert len(kwargs['FoundInfinite']) == 1, (
|
||||
"update_loss_scaling input FoundInfinite take 1 variable but got {}".format(
|
||||
kwargs['FoundInfinite']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['PrevLossScaling']) == 1, (
|
||||
"update_loss_scaling input PrevLossScaling take 1 variable but got {}".format(
|
||||
kwargs['PrevLossScaling']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['InGoodSteps']) == 1, (
|
||||
"update_loss_scaling input InGoodSteps take 1 variable but got {}".format(
|
||||
kwargs['InGoodSteps']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['InBadSteps']) == 1, (
|
||||
"update_loss_scaling input InBadSteps take 1 variable but got {}".format(
|
||||
kwargs['InBadSteps']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['LossScaling']) == 1, (
|
||||
"update_loss_scaling output LossScaling take 1 variable but got {}".format(
|
||||
kwargs['LossScaling']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['OutGoodSteps']) == 1, (
|
||||
"update_loss_scaling output OutGoodSteps take 1 variable but got {}".format(
|
||||
kwargs['OutGoodSteps']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['OutBadSteps']) == 1, (
|
||||
"update_loss_scaling output OutBadSteps take 1 variable but got {}".format(
|
||||
kwargs['OutBadSteps']
|
||||
)
|
||||
)
|
||||
|
||||
assert len(kwargs['X']) == len(kwargs['Out']), (
|
||||
"update_loss_scaling got [{}] X and [{}] Out, which are supposed to be equal".format(
|
||||
len(kwargs['X']), len(kwargs['Out'])
|
||||
)
|
||||
)
|
||||
|
||||
filter_vars = []
|
||||
for varname in kwargs['X']:
|
||||
if (
|
||||
rank_id
|
||||
in ctx.get_tensor_dist_attr_for_program(
|
||||
main_block._var_recursive(varname)
|
||||
).process_mesh.process_ids
|
||||
):
|
||||
filter_vars.append(varname)
|
||||
|
||||
# replicate op in dist program
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(backward_op.desc)
|
||||
set_dist_op_desc_original_id(dist_op_desc, backward_op.desc, ctx)
|
||||
dist_op_desc.set_input('X', filter_vars)
|
||||
dist_op_desc.set_output('Out', filter_vars)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"update_loss_scaling",
|
||||
DistributedUpdateLossScalingImpl("update_loss_scaling"),
|
||||
)
|
||||
@@ -0,0 +1,539 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import pickle
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.passes import PassContext, new_pass
|
||||
from paddle.distributed.utils.log_utils import get_logger
|
||||
from paddle.framework import core
|
||||
from paddle.static import append_backward, program_guard
|
||||
|
||||
from .cluster import Cluster
|
||||
from .completion import Completer
|
||||
from .dist_context import DistributedContext, set_default_distributed_context
|
||||
from .dist_op import DistributedOperator
|
||||
from .dist_tensor import DistributedTensor
|
||||
from .mapper import mapping
|
||||
from .partitioner import Partitioner
|
||||
from .planner import Planner
|
||||
from .process_group import (
|
||||
ProcessGroup,
|
||||
_g_process_group_map,
|
||||
get_all_process_groups,
|
||||
get_process_group,
|
||||
get_world_process_group,
|
||||
)
|
||||
from .reshard import Resharder
|
||||
from .utils import SerialProgramInfo, make_data_unshard
|
||||
|
||||
_logger = get_logger(logging.INFO)
|
||||
|
||||
|
||||
class AutoParallelizer:
|
||||
"""
|
||||
AutoParallelizer is the main controller class to do the auto parallel process.
|
||||
And the auto parallel process will be triggered in the wrapped parallelize function.
|
||||
To facilitate the auto parallelization, it will contain information about program, cluster and the
|
||||
related context. In this basic version, the program information will be retrieved from
|
||||
Fleet object, and the cluster information can be retrieved in the new created Cluster object,
|
||||
and the context information can be retrieved in the new created DistributedContext.
|
||||
"""
|
||||
|
||||
def __init__(self, fleet):
|
||||
self._fleet = fleet
|
||||
self._optimizer = self._fleet.user_defined_optimizer
|
||||
self._dist_strategy = self._fleet._user_defined_strategy
|
||||
self._dist_context = DistributedContext()
|
||||
self._cluster = None
|
||||
self._cluster_topo_path = os.getenv("PADDLE_CLUSTER_TOPO_PATH", None)
|
||||
if self._cluster_topo_path is not None:
|
||||
self._cluster = Cluster()
|
||||
self._cluster.build_from_file(self._cluster_topo_path)
|
||||
# Prepare information for auto mapping
|
||||
self._rank_mapping_path = os.getenv("PADDLE_RANK_MAPPING_PATH", None)
|
||||
enable_auto_mapping_env = os.getenv("PADDLE_ENABLE_AUTO_MAPPING", None)
|
||||
if enable_auto_mapping_env is None:
|
||||
self._enable_auto_mapping = False
|
||||
else:
|
||||
self._enable_auto_mapping = True
|
||||
self._pass_context = PassContext()
|
||||
|
||||
self._need_rank_mapping = os.getenv("PADDLE_NEED_RANK_MAPPING")
|
||||
self._need_rank_mapping = (
|
||||
True
|
||||
if self._need_rank_mapping
|
||||
and self._need_rank_mapping.lower() == 'true'
|
||||
else False
|
||||
)
|
||||
# self._pass_context = None
|
||||
|
||||
def _remove_distributed_attrs(self, main_program):
|
||||
suffix = core.kAutoParallelSuffix()
|
||||
# distributed attributes for variable have been removed
|
||||
# in previous process.
|
||||
for block in main_program.blocks:
|
||||
for op in block.ops:
|
||||
for attr_name in op.attr_names:
|
||||
if suffix in attr_name:
|
||||
op._remove_attr(attr_name)
|
||||
|
||||
def _apply_pre_optimization_passes(
|
||||
self, main_program, startup_program, loss, params_grads, no_grad_set
|
||||
):
|
||||
# apply amp pass
|
||||
if self._dist_strategy.amp:
|
||||
config = copy.deepcopy(self._dist_strategy.amp_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["loss"] = loss
|
||||
if config["use_pure_fp16"]:
|
||||
config["base_opt"] = self._optimizer
|
||||
auto_parallel_fp16_pass = new_pass("auto_parallel_fp16", config)
|
||||
auto_parallel_fp16_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
loss = auto_parallel_fp16_pass.get_loss()
|
||||
else:
|
||||
auto_parallel_amp_pass = new_pass("auto_parallel_amp", config)
|
||||
auto_parallel_amp_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
loss = auto_parallel_amp_pass.get_loss()
|
||||
|
||||
# apply recompute pass
|
||||
if self._dist_strategy.recompute:
|
||||
config = copy.deepcopy(self._dist_strategy.recompute_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["no_grad_set"] = copy.deepcopy(no_grad_set)
|
||||
config["loss"] = loss
|
||||
auto_parallel_recompute_pass = new_pass(
|
||||
"auto_parallel_recompute", config
|
||||
)
|
||||
auto_parallel_recompute_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
def _generate_backward(
|
||||
self,
|
||||
main_program,
|
||||
startup_program,
|
||||
loss,
|
||||
parameter_list,
|
||||
no_grad_set,
|
||||
callbacks,
|
||||
):
|
||||
with program_guard(main_program, startup_program):
|
||||
params_grads = append_backward(
|
||||
loss,
|
||||
parameter_list,
|
||||
no_grad_set,
|
||||
callbacks,
|
||||
distop_context=self._dist_context.dist_op_context,
|
||||
)
|
||||
self._completer = Completer(self._dist_context)
|
||||
self._completer.complete_backward_annotation(main_program)
|
||||
self._dist_context.block_state.parse_backward_blocks(main_program)
|
||||
return params_grads
|
||||
|
||||
def _apply_optimize(self, main_program, startup_program, params_grads):
|
||||
optimizer = copy.deepcopy(self._optimizer)
|
||||
with program_guard(main_program, startup_program):
|
||||
optimize_ops = optimizer.apply_gradients(params_grads)
|
||||
|
||||
self._dist_context._serial_optimizer = optimizer
|
||||
# update completion
|
||||
self._completer = Completer(self._dist_context)
|
||||
self._completer.complete_update_annotation(main_program)
|
||||
|
||||
return optimize_ops
|
||||
|
||||
def _apply_post_optimization_passes(
|
||||
self, main_program, startup_program, rank, params_grads
|
||||
):
|
||||
if self._dist_strategy.sharding:
|
||||
config = copy.deepcopy(self._dist_strategy.sharding_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["global_rank"] = rank
|
||||
auto_parallel_sharding_pass = new_pass(
|
||||
"auto_parallel_sharding", config
|
||||
)
|
||||
auto_parallel_sharding_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
params_grads = self._pass_context.get_attr("params_grads")
|
||||
|
||||
config = copy.deepcopy(self._dist_strategy.sharding_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["rank_id"] = rank
|
||||
auto_parallel_clip_pass = new_pass("auto_parallel_grad_clip", config)
|
||||
auto_parallel_clip_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
if self._dist_strategy.gradient_merge:
|
||||
config = copy.deepcopy(self._dist_strategy.gradient_merge_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
auto_parallel_gradient_merge_pass = new_pass(
|
||||
"auto_parallel_gradient_merge_pass", config
|
||||
)
|
||||
auto_parallel_gradient_merge_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
def _get_dist_program(self, rank, dist_context=None, relaunch_phase=False):
|
||||
completed_main_program = None
|
||||
serial_main_program = self._main_program.clone()
|
||||
serial_startup_program = self._startup_program.clone()
|
||||
serial_loss = serial_main_program.global_block().var(self._loss.name)
|
||||
|
||||
# generating serial
|
||||
if dist_context is None:
|
||||
# Annotation completion
|
||||
self._dist_context = DistributedContext()
|
||||
_logger.info("Start annotation dist attr.")
|
||||
self._completer = Completer(self._dist_context)
|
||||
completed_main_program = (
|
||||
self._completer.complete_forward_annotation(serial_main_program)
|
||||
)
|
||||
else:
|
||||
completed_main_program = serial_main_program
|
||||
self._dist_context = copy.deepcopy(dist_context)
|
||||
|
||||
# parse forward sub block
|
||||
self._dist_context.block_state.parse_forward_blocks(serial_main_program)
|
||||
|
||||
# serial backward pass
|
||||
params_grads = self._generate_backward(
|
||||
completed_main_program,
|
||||
serial_startup_program,
|
||||
serial_loss,
|
||||
self._parameter_list,
|
||||
self._no_grad_set,
|
||||
self._callbacks,
|
||||
)
|
||||
|
||||
# serial forward pass
|
||||
self._apply_pre_optimization_passes(
|
||||
completed_main_program,
|
||||
serial_startup_program,
|
||||
serial_loss,
|
||||
params_grads,
|
||||
self._no_grad_set,
|
||||
)
|
||||
# Logical partition
|
||||
partitioner = Partitioner(self._dist_context, rank)
|
||||
(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
dist_params_grads,
|
||||
) = partitioner.partition(
|
||||
completed_main_program, serial_startup_program, params_grads
|
||||
)
|
||||
|
||||
# TODO refactor the placement of optimizer
|
||||
# generate optimize program
|
||||
dist_optimize_ops = self._apply_optimize(
|
||||
dist_main_prog, dist_startup_prog, dist_params_grads
|
||||
)
|
||||
|
||||
make_data_unshard(dist_main_prog, dist_startup_prog, self._dist_context)
|
||||
|
||||
resharder = Resharder(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
rank,
|
||||
self._dist_context,
|
||||
dist_params_grads,
|
||||
)
|
||||
resharder.reshard()
|
||||
|
||||
self._apply_post_optimization_passes(
|
||||
dist_main_prog, dist_startup_prog, rank, dist_params_grads
|
||||
)
|
||||
g_process_group_map = None
|
||||
if not relaunch_phase:
|
||||
g_process_group_map = copy.deepcopy(_g_process_group_map)
|
||||
_g_process_group_map.clear()
|
||||
_g_process_group_map[0] = ProcessGroup(0, [])
|
||||
for process_mesh in self._dist_context._process_meshes:
|
||||
_g_process_group_map[0].add_ranks(process_mesh.process_ids)
|
||||
return (
|
||||
dist_optimize_ops,
|
||||
dist_params_grads,
|
||||
dist_startup_prog,
|
||||
dist_main_prog,
|
||||
g_process_group_map,
|
||||
)
|
||||
|
||||
def parallelize(
|
||||
self,
|
||||
loss,
|
||||
startup_program,
|
||||
parameter_list=None,
|
||||
no_grad_set=None,
|
||||
callbacks=None,
|
||||
):
|
||||
assert startup_program is not None
|
||||
self._loss = loss
|
||||
self._startup_program = startup_program
|
||||
self._main_program = loss.block.program
|
||||
self._parameter_list = parameter_list
|
||||
self._no_grad_set = no_grad_set
|
||||
self._callbacks = callbacks
|
||||
|
||||
if self._enable_auto_mapping and self._need_rank_mapping:
|
||||
# Do the mapping pass before parallelization
|
||||
assert self._cluster is not None, (
|
||||
"The cluster must not be none when using auto mapping."
|
||||
)
|
||||
dist_programs = {}
|
||||
world_process_group = get_world_process_group()
|
||||
dist_context = None
|
||||
# auto search
|
||||
if self._dist_strategy.auto_search:
|
||||
logging.info("Start searching dist attr.")
|
||||
serial_program_info = SerialProgramInfo(
|
||||
self._main_program,
|
||||
self._startup_program,
|
||||
self._loss,
|
||||
self._optimizer,
|
||||
self._cluster,
|
||||
)
|
||||
planner = Planner(
|
||||
serial_program_info,
|
||||
self,
|
||||
algorithm_config={"name": "mcmc", "max_search_times": 5},
|
||||
)
|
||||
dist_context, _ = planner.search()
|
||||
logging.info("End searching dist attr.")
|
||||
|
||||
# serialize the dist context by planner
|
||||
if dist_context is not None:
|
||||
logging.info("Start serialize searched dist attr")
|
||||
cwd = pathlib.Path().cwd()
|
||||
searched_dist_context_path = os.path.join(
|
||||
cwd, f"searched_dist_context_{time.time()}.pkl"
|
||||
)
|
||||
saved_dist_context = {}
|
||||
ops_dist_attr = {}
|
||||
tensors_dist_attr = {}
|
||||
for key, dist_op in dist_context._dist_ops_for_program.items():
|
||||
ops_dist_attr[key] = dist_op.dist_attr
|
||||
for (
|
||||
key,
|
||||
dist_tensor,
|
||||
) in dist_context._dist_tensors_for_program.items():
|
||||
tensors_dist_attr[key] = dist_tensor.dist_attr
|
||||
saved_dist_context["ops_dist_attr"] = ops_dist_attr
|
||||
saved_dist_context["tensors_dist_attr"] = tensors_dist_attr
|
||||
saved_dist_context["process_meshes"] = (
|
||||
dist_context._process_meshes
|
||||
)
|
||||
with open(
|
||||
searched_dist_context_path, "wb"
|
||||
) as dist_context_file:
|
||||
pickle.dump(saved_dist_context, dist_context_file)
|
||||
os.environ['PADDLE_SEARCHED_DIST_CONTEXT_PATH'] = (
|
||||
searched_dist_context_path
|
||||
)
|
||||
logging.info(
|
||||
f"End serialize searched dist attr to {searched_dist_context_path}"
|
||||
)
|
||||
|
||||
for rank in world_process_group.ranks:
|
||||
(
|
||||
dist_optimize_ops,
|
||||
dist_params_grads,
|
||||
dist_startup_prog,
|
||||
dist_main_prog,
|
||||
g_process_group_map,
|
||||
) = self._get_dist_program(rank, dist_context)
|
||||
dist_programs[rank] = [dist_main_prog, g_process_group_map]
|
||||
|
||||
# Do the mapping between the distributed program graph and the cluster graph
|
||||
rank_mapping_dict = mapping(dist_programs, self._cluster)
|
||||
rank_mapping = list(rank_mapping_dict.values())
|
||||
|
||||
# Relaunch the training by using the rank mapping file
|
||||
with open(self._rank_mapping_path, "w") as rank_mapping_file:
|
||||
json.dump(rank_mapping, rank_mapping_file)
|
||||
|
||||
enable_elastic = os.getenv("PADDLE_ENABLE_ELASTIC")
|
||||
enable_elastic = (
|
||||
True
|
||||
if enable_elastic and enable_elastic.lower() == 'true'
|
||||
else False
|
||||
)
|
||||
if enable_elastic:
|
||||
print("Auto mapping finished, now do elastic re-launch")
|
||||
sys.exit(
|
||||
paddle.distributed.fleet.elastic.manager.ELASTIC_AUTO_PARALLEL_EXIT_CODE
|
||||
)
|
||||
|
||||
original_cmd_args = os.getenv("PADDLE_ORIGINAL_CMD_ARGS")
|
||||
rank_mapping_args = " ".join(
|
||||
["--rank_mapping_path", self._rank_mapping_path]
|
||||
)
|
||||
if os.environ.get("WITH_COVERAGE", "OFF") == "ON":
|
||||
coverage_args = ["-m", "coverage", "run", "--branch", "-p"]
|
||||
else:
|
||||
coverage_args = []
|
||||
new_cmd_args = (
|
||||
"-m paddle.distributed.fleet.launch"
|
||||
+ " "
|
||||
+ rank_mapping_args
|
||||
+ " "
|
||||
+ original_cmd_args
|
||||
)
|
||||
new_cmd = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
*coverage_args,
|
||||
*shlex.split(new_cmd_args),
|
||||
]
|
||||
new_process = subprocess.Popen(new_cmd)
|
||||
new_process.wait()
|
||||
assert new_process.returncode == 0, (
|
||||
"Launch failed with rank mapping"
|
||||
)
|
||||
print("Successfully do the second launch for auto mapping!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
# Parallelization after the mapping pass
|
||||
rank = paddle.distributed.get_rank()
|
||||
dist_context = None
|
||||
searched_dist_context_path = os.getenv(
|
||||
"PADDLE_SEARCHED_DIST_CONTEXT_PATH", None
|
||||
)
|
||||
if searched_dist_context_path is not None:
|
||||
with open(
|
||||
searched_dist_context_path, "rb"
|
||||
) as dist_context_file:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
saved_dist_context = safe_load_pickle(dist_context_file)
|
||||
dist_context = DistributedContext()
|
||||
for op in self._main_program.global_block().ops:
|
||||
dist_attr = saved_dist_context["ops_dist_attr"][
|
||||
op.desc.id()
|
||||
]
|
||||
dist_op = DistributedOperator(op, dist_attr)
|
||||
dist_context.add_dist_op_for_program(dist_op)
|
||||
|
||||
vars = self._main_program.global_block().vars
|
||||
for var in vars.values():
|
||||
dist_attr = saved_dist_context["tensors_dist_attr"][
|
||||
var.desc.id()
|
||||
]
|
||||
dist_tensor = DistributedTensor(var, dist_attr)
|
||||
dist_context.add_dist_tensor_for_program(dist_tensor)
|
||||
|
||||
dist_context._process_meshes = saved_dist_context[
|
||||
"process_meshes"
|
||||
]
|
||||
|
||||
else:
|
||||
if self._dist_strategy.auto_search:
|
||||
serial_program_info = SerialProgramInfo(
|
||||
self._main_program,
|
||||
self._startup_program,
|
||||
self._loss,
|
||||
self._optimizer,
|
||||
cluster=self._cluster,
|
||||
)
|
||||
planner = Planner(
|
||||
serial_program_info,
|
||||
self,
|
||||
algorithm_config={
|
||||
"name": "mcmc",
|
||||
"max_search_times": 5,
|
||||
},
|
||||
)
|
||||
dist_context, _ = planner.search()
|
||||
|
||||
# rebuild g_process_group
|
||||
if dist_context is not None:
|
||||
pg0 = get_process_group(0)
|
||||
for process_mesh in dist_context._process_meshes:
|
||||
pg0.add_ranks(process_mesh.process_ids)
|
||||
(
|
||||
dist_optimize_ops,
|
||||
dist_params_grads,
|
||||
dist_startup_prog,
|
||||
dist_main_prog,
|
||||
_,
|
||||
) = self._get_dist_program(rank, dist_context, relaunch_phase=True)
|
||||
|
||||
# NOTE: This is a trick to fix hang in pipeline mode when dist context is searched by planner
|
||||
if self._dist_strategy.auto_search:
|
||||
is_pipeline = False
|
||||
for op in dist_main_prog.global_block().ops:
|
||||
if op.type == "send_v2" or op.type == "recv_v2":
|
||||
is_pipeline = True
|
||||
break
|
||||
if is_pipeline:
|
||||
with paddle.static.program_guard(dist_main_prog):
|
||||
paddle.distributed.barrier()
|
||||
|
||||
# Traverse different rank programs and traverse each op of them,
|
||||
# instantiate communication by process_mapping.
|
||||
all_process_groups = get_all_process_groups()
|
||||
for process_group in all_process_groups:
|
||||
process_group.instantiate()
|
||||
|
||||
# Copy distributed info to the default context
|
||||
set_default_distributed_context(self._dist_context)
|
||||
|
||||
# The last step: remove all distributed attributes to be compatible
|
||||
# with inference.
|
||||
self._remove_distributed_attrs(dist_main_prog)
|
||||
|
||||
return (
|
||||
dist_optimize_ops,
|
||||
dist_params_grads,
|
||||
dist_startup_prog,
|
||||
dist_main_prog,
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
memo[id(self)] = result
|
||||
for k, v in self.__dict__.items():
|
||||
if (
|
||||
k == "_main_program"
|
||||
or k == "_startup_program"
|
||||
or k == "_dist_context"
|
||||
or k == "_fleet"
|
||||
or k == "_loss"
|
||||
):
|
||||
setattr(result, k, v)
|
||||
else:
|
||||
setattr(result, k, copy.deepcopy(v, memo))
|
||||
return result
|
||||
@@ -0,0 +1,553 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from paddle.distributed.passes.pass_base import PassManager, new_pass
|
||||
from paddle.framework import get_flags
|
||||
from paddle.static import append_backward, program_guard
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
from ..random import init_auto_parallel_rng
|
||||
from .partitioner import Partitioner
|
||||
from .process_group import get_world_process_group
|
||||
from .reshard import Resharder
|
||||
from .utils import (
|
||||
get_pp_stage,
|
||||
is_sequential_run,
|
||||
)
|
||||
|
||||
PIR_PASS = [
|
||||
'fused_gemm_epilogue_pass',
|
||||
'fused_linear_param_grad_add_pass',
|
||||
'fuse_allreduce_split_to_reducescatter_pass',
|
||||
'fused_dropout_add_pass',
|
||||
]
|
||||
|
||||
PIR_PYTHON_PASS = [
|
||||
'eliminate_transpose',
|
||||
]
|
||||
|
||||
|
||||
class Parallelizer:
|
||||
def __init__(self, mode, completer, dist_context):
|
||||
self._mode = mode
|
||||
self._completer = completer
|
||||
self._dist_context = dist_context
|
||||
assert self._dist_context._is_initialized
|
||||
self._pass_context = self._dist_context.pass_context
|
||||
self._strategy = self._dist_context.strategy
|
||||
self._logger = get_logger(logging.INFO)
|
||||
|
||||
@property
|
||||
def is_train(self):
|
||||
return self._mode == "train"
|
||||
|
||||
@property
|
||||
def is_test(self):
|
||||
return self._mode in ["eval", "predict"]
|
||||
|
||||
def parallel_all(self, parameter_list=None):
|
||||
world_process_group = get_world_process_group()
|
||||
all_ranks = world_process_group.ranks
|
||||
for rank in all_ranks:
|
||||
# self._dist_context._backup(serial=True, dist=True)
|
||||
self.parallel(rank, parameter_list)
|
||||
# self._dist_context._restore(serial=True, dist=True)
|
||||
|
||||
def parallel(self, rank, parameter_list=None):
|
||||
serial_main_program = self._dist_context.serial_main_program
|
||||
serial_startup_program = self._dist_context.serial_startup_program
|
||||
serial_optimizer = self._dist_context.serial_optimizer
|
||||
if self.is_train and serial_optimizer:
|
||||
# Generate backward
|
||||
serial_loss = self._dist_context.serial_loss
|
||||
params_grads = self._generate_backward(
|
||||
serial_main_program,
|
||||
serial_startup_program,
|
||||
serial_loss,
|
||||
parameter_list,
|
||||
)
|
||||
# Apply pre optimization passes
|
||||
time0 = time.time()
|
||||
(
|
||||
serial_main_program,
|
||||
serial_startup_program,
|
||||
params_grads,
|
||||
) = self._apply_pre_optimization(
|
||||
serial_main_program,
|
||||
serial_startup_program,
|
||||
serial_loss,
|
||||
serial_optimizer,
|
||||
params_grads,
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel apply_pre_optimization time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Do logical partition
|
||||
time0 = time.time()
|
||||
partitioner = Partitioner(self._dist_context, rank)
|
||||
(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
dist_params_grads,
|
||||
) = partitioner.partition(
|
||||
serial_main_program, serial_startup_program, params_grads
|
||||
)
|
||||
|
||||
init_auto_parallel_rng()
|
||||
|
||||
self._logger.debug(
|
||||
f"within parallel partitioner time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Generate optimizer
|
||||
time0 = time.time()
|
||||
self._generate_optimizer(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
serial_optimizer,
|
||||
dist_params_grads,
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel optimizer time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
|
||||
resharder = Resharder(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
rank,
|
||||
self._dist_context,
|
||||
dist_params_grads,
|
||||
)
|
||||
resharder.reshard()
|
||||
self._logger.debug(
|
||||
f"within parallel reshard time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Apply post optimization passes
|
||||
time0 = time.time()
|
||||
self._apply_post_optimization(
|
||||
dist_main_prog, dist_startup_prog, rank, dist_params_grads
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel apply_post_optimization time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
else:
|
||||
# Apply pre optimization passes
|
||||
time0 = time.time()
|
||||
(
|
||||
serial_main_program,
|
||||
serial_startup_program,
|
||||
params_grads,
|
||||
) = self._apply_pre_optimization(
|
||||
serial_main_program, serial_startup_program, None, None, []
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel apply_pre_optimization time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Do logical partition
|
||||
time0 = time.time()
|
||||
partitioner = Partitioner(self._dist_context, rank)
|
||||
(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
dist_params_grads,
|
||||
) = partitioner.partition(
|
||||
serial_main_program, serial_startup_program, []
|
||||
)
|
||||
# Do reshard process
|
||||
self._logger.debug(
|
||||
f"within parallel partitioner time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
time0 = time.time()
|
||||
# Do reshard process
|
||||
micro_bsz = (
|
||||
1
|
||||
if not self._strategy.pipeline.enable
|
||||
else self._strategy.pipeline.micro_batch_size
|
||||
)
|
||||
resharder = Resharder(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
rank,
|
||||
self._dist_context,
|
||||
[],
|
||||
micro_bsz,
|
||||
)
|
||||
resharder.reshard()
|
||||
self._logger.debug(
|
||||
f"within parallel reshard time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Apply post optimization passes
|
||||
time0 = time.time()
|
||||
self._apply_post_optimization(
|
||||
dist_main_prog, dist_startup_prog, rank, dist_params_grads
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel apply_post_optimization time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
|
||||
# Clone program for test
|
||||
if self.is_test:
|
||||
pipeline_opt = dist_main_prog._pipeline_opt
|
||||
dist_main_prog = dist_main_prog.clone(for_test=True)
|
||||
dist_startup_prog = dist_startup_prog.clone(for_test=True)
|
||||
dist_main_prog._pipeline_opt = pipeline_opt
|
||||
|
||||
# Store the distributed programs for further usages
|
||||
self._dist_context.dist_main_programs[rank] = dist_main_prog
|
||||
self._dist_context.dist_startup_programs[rank] = dist_startup_prog
|
||||
|
||||
def _generate_backward(
|
||||
self, main_program, startup_program, loss, parameter_list=None
|
||||
):
|
||||
# NOTE(zhaoyinglia):
|
||||
# Guarantee the order of params_grads is same between dynamic mode and static mode
|
||||
# by making parameter_list equal to model.parameters(),
|
||||
# because the order affect the result of ClipGradByGLobalNorm.
|
||||
# If parameter_list is not None, the order of params_grads is same with parameter_list.
|
||||
# If parameter_list is None, params_grads will be as prog.global_block().all_parameters().
|
||||
with program_guard(main_program, startup_program):
|
||||
params_grads = append_backward(
|
||||
loss,
|
||||
parameter_list=parameter_list,
|
||||
distop_context=self._dist_context.dist_op_context,
|
||||
)
|
||||
self._completer.complete_backward_annotation(main_program)
|
||||
self._dist_context.block_state.parse_backward_blocks(main_program)
|
||||
return params_grads
|
||||
|
||||
def _generate_optimizer(
|
||||
self, main_program, startup_program, optimizer, params_grads
|
||||
):
|
||||
# NOTE:
|
||||
# 1. `apply_gradients` will add an Accumulator for a parameter only once,
|
||||
# but optimizer will be called repeatedly in re-launch, so optimizer need to be copied.
|
||||
# 2. lr_scheduler cannot be deepcopy, cause 'deepcopy' will lead to difference of learning_rate between executor and engine.
|
||||
learning_rate = optimizer._learning_rate
|
||||
new_optimizer = copy.deepcopy(optimizer)
|
||||
new_optimizer._learning_rate = learning_rate
|
||||
new_optimizer._sorted = False
|
||||
self._dist_context._serial_optimizer = optimizer
|
||||
self._dist_context._serial_optimizer._learning_rate = learning_rate
|
||||
|
||||
with (
|
||||
program_guard(main_program, startup_program),
|
||||
main_program.switch_name_generator_guard("opt_"),
|
||||
):
|
||||
optimizer_ops = new_optimizer.apply_gradients(params_grads)
|
||||
self._completer.complete_update_annotation(main_program)
|
||||
return optimizer_ops
|
||||
|
||||
def _apply_pre_optimization(
|
||||
self, main_program, startup_program, loss, optimizer, params_grads
|
||||
):
|
||||
if self._strategy is None:
|
||||
return
|
||||
|
||||
# apply amp pass on train/eval/predict
|
||||
if self._strategy.amp.enable:
|
||||
config = copy.deepcopy(self._strategy.amp.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["loss"] = loss
|
||||
config["input_data"] = (
|
||||
self._dist_context.serial_feed_vars["inputs"]
|
||||
+ self._dist_context.serial_feed_vars["labels"]
|
||||
)
|
||||
self._logger.info(
|
||||
"Applying AMP-{}-{} ...".format(
|
||||
config["dtype"], config['level']
|
||||
),
|
||||
)
|
||||
if config['level'] == "o1":
|
||||
auto_parallel_amp_pass = new_pass("auto_parallel_amp", config)
|
||||
auto_parallel_amp_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
loss = auto_parallel_amp_pass.get_loss()
|
||||
elif config['level'] in ['o2', 'o3']:
|
||||
config["base_opt"] = optimizer
|
||||
auto_parallel_fp16_pass = new_pass("auto_parallel_fp16", config)
|
||||
auto_parallel_fp16_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
loss = auto_parallel_fp16_pass.get_loss()
|
||||
else:
|
||||
raise ValueError("AMP level should be one of o1, o2, o3")
|
||||
|
||||
# apply quantization pass
|
||||
# The pass can be applied when mode must be 'train'
|
||||
if self.is_train and self._strategy.qat.enable:
|
||||
config = copy.deepcopy(self._strategy.qat.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["mode"] = self._mode
|
||||
config["loss"] = loss
|
||||
auto_parallel_quantization_pass = new_pass(
|
||||
"auto_parallel_quantization", config
|
||||
)
|
||||
auto_parallel_quantization_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
main_program = self._pass_context.get_attr("main_program")
|
||||
startup_program = self._pass_context.get_attr("startup_program")
|
||||
params_grads = self._pass_context.get_attr("params_grads")
|
||||
loss = self._pass_context.get_attr("loss")
|
||||
|
||||
# apply recompute pass
|
||||
# recompute is then train-only optimization
|
||||
if self.is_train and self._strategy.recompute.enable:
|
||||
config = copy.deepcopy(self._strategy.recompute.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["no_grad_set"] = None
|
||||
config["loss"] = loss
|
||||
auto_parallel_recompute_pass = new_pass(
|
||||
"auto_parallel_recompute", config
|
||||
)
|
||||
auto_parallel_recompute_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
return main_program, startup_program, params_grads
|
||||
|
||||
def _check_dist_attr(self, program, num_model_chunks, dist_context):
|
||||
for _, block in enumerate(program.blocks):
|
||||
for _, op in enumerate(block.ops):
|
||||
op_dist_attr = dist_context.get_op_dist_attr_for_program(op)
|
||||
if op_dist_attr is None:
|
||||
raise ValueError(
|
||||
f"There is not dist_attr for op[{op.type}]."
|
||||
)
|
||||
|
||||
def _apply_post_optimization(
|
||||
self, main_program, startup_program, rank, params_grads
|
||||
):
|
||||
if self._strategy is None:
|
||||
return
|
||||
|
||||
# sequence parallel optimization
|
||||
if self._strategy.sp_optimization.enable:
|
||||
config = copy.deepcopy(self._strategy.sp_optimization.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["global_rank"] = rank
|
||||
sp_pass = new_pass(
|
||||
"auto_parallel_sequence_parallel_optimization", config
|
||||
)
|
||||
sp_pass.apply([main_program], [startup_program], self._pass_context)
|
||||
|
||||
# apply fused linear promotion pass
|
||||
if (
|
||||
self.is_train
|
||||
and self._strategy.fused_linear_promotion.enable
|
||||
and self._strategy.fused_passes.enable
|
||||
):
|
||||
if (
|
||||
len(self._strategy.fused_passes.fused_passes_list) > 0
|
||||
and "fuse_gemm_epilogue"
|
||||
in self._strategy.fused_passes.fused_passes_list
|
||||
):
|
||||
amp_config = None
|
||||
if self._strategy.amp.enable:
|
||||
amp_config = copy.deepcopy(self._strategy.amp.to_dict())
|
||||
config = {}
|
||||
config["dist_context"] = self._dist_context
|
||||
config["global_rank"] = rank
|
||||
config["enable_sp"] = self._strategy.sp_optimization.enable
|
||||
config["params_grads"] = params_grads
|
||||
config["amp_level"] = (
|
||||
amp_config['level'] if amp_config is not None else "o0"
|
||||
)
|
||||
fused_linear_promotion_pass = new_pass(
|
||||
"auto_parallel_fused_linear_promotion", config
|
||||
)
|
||||
fused_linear_promotion_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
# apply master grad pass
|
||||
if self._strategy.amp.enable:
|
||||
amp_config = copy.deepcopy(self._strategy.amp.to_dict())
|
||||
config = {}
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["completer"] = self._completer
|
||||
if amp_config['level'] == "o2" and amp_config["use_master_grad"]:
|
||||
master_grad_pass = new_pass(
|
||||
"auto_parallel_master_grad_pass", config
|
||||
)
|
||||
master_grad_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
# data parallel optimization
|
||||
if self._strategy.dp_optimization.enable:
|
||||
config = copy.deepcopy(self._strategy.dp_optimization.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["global_rank"] = rank
|
||||
config["use_sharding"] = self._strategy.sharding.enable
|
||||
dp_pass = new_pass(
|
||||
"auto_parallel_data_parallel_optimization", config
|
||||
)
|
||||
dp_pass.apply([main_program], [startup_program], self._pass_context)
|
||||
|
||||
gradient_sync_after_accumulate = (
|
||||
self._strategy.dp_optimization.gradient_sync_after_accumulate
|
||||
)
|
||||
if gradient_sync_after_accumulate:
|
||||
global_params_grads = params_grads
|
||||
|
||||
if self._strategy.sharding.enable:
|
||||
config = copy.deepcopy(self._strategy.sharding.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["global_rank"] = rank
|
||||
config["gradient_sync_after_accumulate"] = (
|
||||
gradient_sync_after_accumulate
|
||||
)
|
||||
if self._strategy.amp.enable:
|
||||
amp_config = copy.deepcopy(self._strategy.amp.to_dict())
|
||||
config["amp_dtype"] = amp_config['dtype']
|
||||
auto_parallel_sharding_pass = new_pass(
|
||||
"auto_parallel_sharding", config
|
||||
)
|
||||
auto_parallel_sharding_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
params_grads = self._pass_context.get_attr("params_grads")
|
||||
|
||||
if self._strategy.mp_optimization.allreduce_matmul_grad_overlapping:
|
||||
if int(os.getenv("CUDA_DEVICE_MAX_CONNECTIONS", "0")) != 1:
|
||||
self._logger.warning(
|
||||
"You set mp_optimization.allreduce_matmul_grad_overlapping=True, but you did not set environment "
|
||||
"variable CUDA_DEVICE_MAX_CONNECTIONS=1, which may leads to performance "
|
||||
"loss. Try to export CUDA_DEVICE_MAX_CONNECTIONS=1 for better performance."
|
||||
)
|
||||
|
||||
config = {
|
||||
"dist_context": self._dist_context,
|
||||
}
|
||||
allreduce_matmul_grad_overlapping_pass = new_pass(
|
||||
"allreduce_matmul_grad_overlapping", config
|
||||
)
|
||||
allreduce_matmul_grad_overlapping_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
if self.is_train:
|
||||
# GradClip is train-only optimization
|
||||
config = copy.deepcopy(self._strategy.sharding.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["rank_id"] = rank
|
||||
auto_parallel_clip_pass = new_pass(
|
||||
"auto_parallel_grad_clip", config
|
||||
)
|
||||
auto_parallel_clip_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
if not is_sequential_run():
|
||||
# deps for newexe
|
||||
config = {}
|
||||
config["dist_context"] = self._dist_context
|
||||
APSED_pass = new_pass(
|
||||
"auto_parallel_supplement_explicit_dependencies", config
|
||||
)
|
||||
APSED_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
if self.is_train and self._strategy.pipeline.enable:
|
||||
self._strategy.gradient_merge.enable = True
|
||||
self._strategy.gradient_merge.k_steps = (
|
||||
self._strategy.pipeline.accumulate_steps
|
||||
)
|
||||
self._strategy.gradient_merge.avg = True
|
||||
|
||||
# gradient_merge is then train-only optimization
|
||||
grad_to_global_grad = {}
|
||||
if self.is_train and self._strategy.gradient_merge.enable:
|
||||
config = copy.deepcopy(self._strategy.gradient_merge.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["grad_to_global_grad"] = grad_to_global_grad
|
||||
config["pipeline_mode"] = self._strategy.pipeline.schedule_mode
|
||||
if gradient_sync_after_accumulate:
|
||||
config["params_grads"] = global_params_grads
|
||||
config["gradient_sync_after_accumulate"] = (
|
||||
gradient_sync_after_accumulate
|
||||
)
|
||||
else:
|
||||
config["params_grads"] = params_grads
|
||||
auto_parallel_gradient_merge_pass = new_pass(
|
||||
"auto_parallel_gradient_merge_pass", config
|
||||
)
|
||||
auto_parallel_gradient_merge_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
self._check_dist_attr(
|
||||
main_program,
|
||||
self._strategy.pipeline.vpp_degree,
|
||||
self._dist_context,
|
||||
)
|
||||
|
||||
enable_ir = get_flags("FLAGS_enable_pir_in_executor")[
|
||||
'FLAGS_enable_pir_in_executor'
|
||||
]
|
||||
ir_pass_list = []
|
||||
if self.is_train and self._strategy.fused_passes.enable:
|
||||
if len(self._strategy.fused_passes.fused_passes_list) > 0:
|
||||
program_pass_list = []
|
||||
for p in self._strategy.fused_passes.fused_passes_list:
|
||||
if enable_ir and p in (PIR_PASS + PIR_PYTHON_PASS):
|
||||
ir_pass_list.append(p)
|
||||
else:
|
||||
program_pass_list.append(new_pass(p))
|
||||
pass_manager = PassManager(program_pass_list)
|
||||
pass_manager.apply([main_program], [startup_program])
|
||||
|
||||
main_program._pass_opt = {}
|
||||
main_program._pass_opt['pass_list'] = ir_pass_list
|
||||
|
||||
if self.is_train and self._strategy.pipeline.enable:
|
||||
enable_send_recv_overlap = (
|
||||
self._strategy.pipeline.enable_send_recv_overlap
|
||||
)
|
||||
if (
|
||||
enable_send_recv_overlap
|
||||
and int(os.getenv("CUDA_DEVICE_MAX_CONNECTIONS", "0")) != 1
|
||||
):
|
||||
self._logger.warning(
|
||||
"You set pipeline.enable_send_recv_overlap=True, but you did not set environment "
|
||||
"variable CUDA_DEVICE_MAX_CONNECTIONS=1, which may leads to performance "
|
||||
"loss. Try to export CUDA_DEVICE_MAX_CONNECTIONS=1 for better performance."
|
||||
)
|
||||
|
||||
main_program._pipeline_opt = {}
|
||||
main_program._pipeline_opt["standalone_opt"] = {
|
||||
"enable_send_recv_overlap": enable_send_recv_overlap,
|
||||
"schedule_mode": self._strategy.pipeline.schedule_mode,
|
||||
"num_micro_batches": self._strategy.pipeline.accumulate_steps,
|
||||
"pp_degree": len(self._dist_context.process_meshes),
|
||||
"pp_stage": get_pp_stage(self._dist_context, rank),
|
||||
"vpp_degree": self._strategy.pipeline.vpp_degree,
|
||||
"dist_context": self._dist_context,
|
||||
"program_runtimes": self._strategy.pipeline.program_runtimes,
|
||||
"memory_limit_times": self._strategy.pipeline.memory_limit_times,
|
||||
"split_backward": self._strategy.pipeline.split_backward,
|
||||
"grad_to_global_grad": grad_to_global_grad,
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import copy
|
||||
from collections import defaultdict
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.auto_parallel.static.dist_context import (
|
||||
DistributedContext,
|
||||
)
|
||||
from paddle.distributed.auto_parallel.static.operators.common import (
|
||||
get_distributed_operator_impl_container,
|
||||
)
|
||||
from paddle.framework import Program, core
|
||||
from paddle.static import Parameter
|
||||
|
||||
from .dist_attribute import OperatorDistAttr
|
||||
from .operators.common import BACKWARD_ONLY_DIST_OPS
|
||||
from .utils import (
|
||||
__no_shape_var_type__,
|
||||
is_backward_op,
|
||||
is_forward_op,
|
||||
is_loss_op,
|
||||
is_optimize_op,
|
||||
)
|
||||
|
||||
__varname_not_in_block__ = ["lod_tensor_blocking_queue"]
|
||||
|
||||
|
||||
class Partitioner:
|
||||
"""
|
||||
warning:: Partitioner is experimental and subject to change.
|
||||
|
||||
Partitioner convert a program into another program.
|
||||
Given a serial program which has been auto completed with shard annotation, the Partitioner
|
||||
convert the serial program into a "distributed" program. The Partitioner will modify the serial
|
||||
program in following two ways, which is also the major difference between serial and distributed program:
|
||||
1. partition op: replace a serial op into its corresponding dist op inferred from the shard annotation
|
||||
2. partition var: if a var is sharded, modify the shape of var according to its shard annotation
|
||||
|
||||
Partitioner is supposed to be call by the auto parallel framework, and not supposed to be directly called by user.
|
||||
"""
|
||||
|
||||
def __init__(self, dist_context, rank_id=0):
|
||||
"""
|
||||
Args:
|
||||
dist_context (DistributedContext): used to access the distributed_attr of var & op, every Partitioner object could maintain its own DistributedContext member, and partition program base on that shard scenario.
|
||||
rank_id (int): global rank id to which the partitioned distributed program belong.
|
||||
"""
|
||||
if not isinstance(dist_context, DistributedContext):
|
||||
raise TypeError(
|
||||
f"dist_context be DistributedContext, got {type(dist_context)} here"
|
||||
)
|
||||
|
||||
self._dist_context = dist_context
|
||||
self._rank_id = rank_id
|
||||
self._serial2dist_varname_mapping = defaultdict(
|
||||
dict
|
||||
) # block_id -> serial_varname -> dist_varname
|
||||
self._dist_varname_suffix = ""
|
||||
self._forward_op_id2forward_op = {}
|
||||
|
||||
def partition(
|
||||
self, serial_main_program, serial_startup_program, params_grads
|
||||
):
|
||||
if not isinstance(serial_main_program, (Program)):
|
||||
raise TypeError(
|
||||
f"main_program be paddle.framework.Program, got {type(serial_main_program)} here"
|
||||
)
|
||||
|
||||
# check if shard annotated serial program valid
|
||||
if not self._is_valid_annotated_program(serial_main_program):
|
||||
raise RuntimeError(
|
||||
"Not all vars or ops are annotated in main program !"
|
||||
)
|
||||
|
||||
# init distop helper
|
||||
dist_op_context = self._dist_context.dist_op_context
|
||||
dist_op_context.varname_mapping = self._serial2dist_varname_mapping
|
||||
dist_op_context.rank_id = self._rank_id
|
||||
|
||||
# partition startup program
|
||||
if serial_startup_program is None:
|
||||
partitioned_startup_prog = None
|
||||
else:
|
||||
partitioned_startup_prog = self.partition_startup_program(
|
||||
serial_main_program, serial_startup_program
|
||||
)
|
||||
dist_op_context.dst_startup_program = partitioned_startup_prog
|
||||
|
||||
# partition main program
|
||||
(
|
||||
partitioned_main_prog,
|
||||
partitioned_params_grads,
|
||||
) = self.partition_main_program(serial_main_program, params_grads)
|
||||
|
||||
return (
|
||||
partitioned_main_prog,
|
||||
partitioned_startup_prog,
|
||||
partitioned_params_grads,
|
||||
)
|
||||
|
||||
def partition_startup_program(
|
||||
self, serial_main_program, serial_startup_program
|
||||
):
|
||||
if not isinstance(serial_startup_program, (Program)):
|
||||
raise TypeError(
|
||||
f"dist_context be paddle.framework.Program, got {type(serial_startup_program)} here"
|
||||
)
|
||||
|
||||
partitioned_startup_prog = paddle.framework.Program()
|
||||
partitioned_startup_prog._name_generator = (
|
||||
serial_startup_program._name_generator.clone()
|
||||
)
|
||||
ref_block = serial_main_program.global_block()
|
||||
target_block = partitioned_startup_prog.global_block()
|
||||
var2shape = {}
|
||||
temp_varname_map = {}
|
||||
|
||||
# tensors
|
||||
for var in serial_startup_program.list_vars():
|
||||
assert var.persistable
|
||||
new_name = var.name + self._dist_varname_suffix
|
||||
temp_varname_map[var.name] = new_name
|
||||
target_shape = _partition_var(
|
||||
self._dist_context, ref_block, target_block, var.name, new_name
|
||||
)
|
||||
var2shape[new_name] = target_shape
|
||||
|
||||
# ops
|
||||
for op in serial_startup_program.global_block().ops:
|
||||
# TODO if var not belong to this rank, should be filtered
|
||||
output_vars = op.desc.output_arg_names()
|
||||
assert len(output_vars) == 1, (
|
||||
f"initializer should output only ONE variable, but got [{op.desc}]"
|
||||
)
|
||||
assert temp_varname_map[output_vars[0]] in var2shape, (
|
||||
f"try to initialize [{output_vars[0]}] which is not a persistable var"
|
||||
)
|
||||
new_op_desc = target_block.desc.append_op()
|
||||
new_op_desc.copy_from(op.desc)
|
||||
new_op_desc._rename_output(
|
||||
output_vars[0], temp_varname_map[output_vars[0]]
|
||||
)
|
||||
new_op_desc._set_attr(
|
||||
"shape", var2shape[temp_varname_map[output_vars[0]]]
|
||||
)
|
||||
target_block._sync_with_cpp()
|
||||
|
||||
# set distribute attribute
|
||||
new_op = target_block.ops[-1]
|
||||
assert new_op.type == new_op_desc.type()
|
||||
assert new_op.desc == new_op_desc
|
||||
output_var = target_block.var(output_vars[0])
|
||||
output_var_attr = (
|
||||
self._dist_context.get_tensor_dist_attr_for_program(output_var)
|
||||
)
|
||||
op_attr = OperatorDistAttr()
|
||||
op_attr.process_mesh = output_var_attr.process_mesh
|
||||
op_attr.set_output_dims_mapping(
|
||||
output_var.name, output_var_attr.dims_mapping
|
||||
)
|
||||
op_attr.set_input_dims_mapping(
|
||||
output_var.name, output_var_attr.dims_mapping
|
||||
)
|
||||
self._dist_context.set_op_dist_attr_for_program(new_op, op_attr)
|
||||
|
||||
return partitioned_startup_prog
|
||||
|
||||
def partition_main_program(self, serial_main_program, params_and_grads):
|
||||
"""
|
||||
1. partition variables
|
||||
2. replace local op with corresponding dist op
|
||||
"""
|
||||
|
||||
partitioned_main_prog = paddle.framework.Program()
|
||||
partitioned_main_prog._name_generator = (
|
||||
serial_main_program._name_generator.clone()
|
||||
)
|
||||
dist_op_context = self._dist_context.dist_op_context
|
||||
dist_op_context.dst_main_program = partitioned_main_prog
|
||||
|
||||
for idx in range(self._dist_context.block_state.nblock):
|
||||
ref_block = serial_main_program.blocks[idx]
|
||||
|
||||
if idx == 0:
|
||||
target_block = partitioned_main_prog.blocks[0]
|
||||
else:
|
||||
target_block = partitioned_main_prog._create_block(
|
||||
parent_idx=ref_block.parent_idx
|
||||
)
|
||||
assert ref_block.idx == target_block.idx
|
||||
target_block._set_forward_block_idx(ref_block.forward_block_idx)
|
||||
dist_op_context.work_block = target_block
|
||||
self.partition_block(ref_block, target_block)
|
||||
|
||||
partitioned_main_prog.current_block_idx = 0
|
||||
|
||||
# should reconnect the block_attr ptr to the correct block
|
||||
for block_id in range(self._dist_context.block_state.nblock):
|
||||
block = partitioned_main_prog.block(block_id)
|
||||
for op in block.ops:
|
||||
for attr_name in op.all_attrs():
|
||||
if op.attr_type(attr_name) == core.AttrType.BLOCK:
|
||||
relative_id = op._block_attr_id(attr_name)
|
||||
op._set_attr(
|
||||
attr_name, partitioned_main_prog.block(relative_id)
|
||||
)
|
||||
|
||||
partitioned_params_and_grads = []
|
||||
for p, g in params_and_grads:
|
||||
assert p.name in self._serial2dist_varname_mapping[0]
|
||||
dist_p = self._get_dist_var_by_serial_var(
|
||||
p, partitioned_main_prog, 0
|
||||
)
|
||||
if g is None:
|
||||
dist_g = None
|
||||
else:
|
||||
assert g.name in self._serial2dist_varname_mapping[0]
|
||||
dist_g = self._get_dist_var_by_serial_var(
|
||||
g, partitioned_main_prog, 0
|
||||
)
|
||||
partitioned_params_and_grads.append((dist_p, dist_g))
|
||||
|
||||
return partitioned_main_prog, partitioned_params_and_grads
|
||||
|
||||
def partition_block(self, ref_block, target_block):
|
||||
dist_op_context = self._dist_context.dist_op_context
|
||||
|
||||
last_fwd_op_idx = -1
|
||||
for idx, op in enumerate(ref_block.ops):
|
||||
if is_loss_op(op):
|
||||
last_fwd_op_idx = idx
|
||||
break
|
||||
|
||||
if last_fwd_op_idx == -1:
|
||||
last_fwd_op_idx = len(ref_block.ops)
|
||||
|
||||
for idx in range(len(ref_block.ops)):
|
||||
if idx <= last_fwd_op_idx:
|
||||
self._forward_op_id2forward_op[
|
||||
ref_block.ops[idx].desc.original_id()
|
||||
] = ref_block.ops[idx]
|
||||
|
||||
# partition
|
||||
appended_grad_times = 0
|
||||
for idx, op in enumerate(ref_block.ops):
|
||||
op_dist_attr = self._dist_context.get_op_dist_attr_for_program(op)
|
||||
if is_backward_op(op) and (
|
||||
is_forward_op(ref_block.ops[idx - 1])
|
||||
or is_loss_op(ref_block.ops[idx - 1])
|
||||
):
|
||||
if not op_dist_attr.is_recompute:
|
||||
appended_grad_times += 1
|
||||
|
||||
# partition input variables
|
||||
for serial_input_varname in op.desc.input_arg_names():
|
||||
if (
|
||||
serial_input_varname
|
||||
not in self._serial2dist_varname_mapping[
|
||||
ref_block.forward_block_idx
|
||||
]
|
||||
or serial_input_varname
|
||||
not in self._serial2dist_varname_mapping[ref_block.idx]
|
||||
):
|
||||
new_varname = (
|
||||
serial_input_varname + self._dist_varname_suffix
|
||||
)
|
||||
if ref_block.has_var(serial_input_varname):
|
||||
_partition_var(
|
||||
self._dist_context,
|
||||
ref_block,
|
||||
target_block,
|
||||
serial_input_varname,
|
||||
new_varname,
|
||||
)
|
||||
self._serial2dist_varname_mapping[ref_block.idx][
|
||||
serial_input_varname
|
||||
] = new_varname
|
||||
|
||||
# partition output vars
|
||||
for serial_output_varname in op.desc.output_arg_names():
|
||||
if (
|
||||
serial_output_varname
|
||||
not in self._serial2dist_varname_mapping[
|
||||
ref_block.forward_block_idx
|
||||
]
|
||||
or serial_output_varname
|
||||
not in self._serial2dist_varname_mapping[ref_block.idx]
|
||||
):
|
||||
new_varname = (
|
||||
serial_output_varname + self._dist_varname_suffix
|
||||
)
|
||||
if ref_block.has_var(serial_output_varname):
|
||||
_partition_var(
|
||||
self._dist_context,
|
||||
ref_block,
|
||||
target_block,
|
||||
serial_output_varname,
|
||||
new_varname,
|
||||
)
|
||||
self._serial2dist_varname_mapping[ref_block.idx][
|
||||
serial_output_varname
|
||||
] = new_varname
|
||||
|
||||
# partition op
|
||||
if is_forward_op(op) or op_dist_attr.is_recompute:
|
||||
kinputs, koutputs = dist_op_context.prepare_context(op)
|
||||
dist_op_forward_impl = _get_dist_op_forward_implement(
|
||||
op, self._dist_context
|
||||
)
|
||||
dist_op_forward_impl.forward(
|
||||
self._dist_context, **kinputs, **koutputs
|
||||
)
|
||||
|
||||
elif is_backward_op(op):
|
||||
kinputs, koutputs = dist_op_context.prepare_context(op)
|
||||
dist_op_backward_impl = _get_dist_op_backward_implement(
|
||||
op, self._dist_context, self._forward_op_id2forward_op
|
||||
)
|
||||
grad_var_to_var = (
|
||||
self._dist_context.dist_op_context.grad_var_to_var[
|
||||
appended_grad_times
|
||||
]
|
||||
)
|
||||
dist_op_backward_impl.backward(
|
||||
self._dist_context,
|
||||
**kinputs,
|
||||
**koutputs,
|
||||
**{"grad_var_to_var": grad_var_to_var},
|
||||
)
|
||||
elif is_optimize_op(op):
|
||||
# NOTE: BACKWARD_ONLY_DIST_OPS's op_role must be 2 because of 1F1B PASS
|
||||
kinputs, koutputs = dist_op_context.prepare_context(op)
|
||||
dist_op_opt_impl = _get_dist_op_backward_implement(
|
||||
op, self._dist_context, self._forward_op_id2forward_op
|
||||
)
|
||||
dist_op_opt_impl.backward(
|
||||
self._dist_context,
|
||||
**kinputs,
|
||||
**koutputs,
|
||||
**{"grad_var_to_var": {}},
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"partitioner only support forward and backward, optimize ops, but got {op}"
|
||||
)
|
||||
|
||||
def _is_valid_annotated_program(self, program):
|
||||
# TODO (ZJ-LIANG) should check all block
|
||||
ops = program.global_block().ops
|
||||
vars_ = program.list_vars()
|
||||
op_dist_attrs = [
|
||||
self._dist_context.get_op_dist_attr_for_program(op) for op in ops
|
||||
]
|
||||
var_dist_attrs = [
|
||||
self._dist_context.get_tensor_dist_attr_for_program(var)
|
||||
for var in vars_
|
||||
if (var.type not in __no_shape_var_type__)
|
||||
]
|
||||
|
||||
all_ops_annotated = all(
|
||||
dist_attr is not None for dist_attr in op_dist_attrs
|
||||
)
|
||||
all_vars_annotated = all(
|
||||
dist_attr is not None for dist_attr in var_dist_attrs
|
||||
)
|
||||
|
||||
return all_ops_annotated and all_vars_annotated
|
||||
|
||||
def _get_dist_var_by_serial_var(
|
||||
self, serial_var, partitioned_main_prog, block_id
|
||||
):
|
||||
block_idx = serial_var.block.idx
|
||||
target_block = partitioned_main_prog.blocks[block_idx]
|
||||
dist_var_name = self._serial2dist_varname_mapping[block_id][
|
||||
serial_var.name
|
||||
]
|
||||
assert target_block.has_var(dist_var_name)
|
||||
return target_block.var(dist_var_name)
|
||||
|
||||
|
||||
def _get_dist_shape(var, dist_attr):
|
||||
var_shape = var.shape
|
||||
mapping = dist_attr.dims_mapping
|
||||
mesh = dist_attr.process_mesh.shape
|
||||
if mapping == []:
|
||||
return var_shape
|
||||
|
||||
assert len(var_shape) == len(mapping), (
|
||||
f"variable shape [{var_shape}] and dim_mapping [{mapping}] is NOT match !"
|
||||
)
|
||||
new_shape = []
|
||||
for idx in range(len(var_shape)):
|
||||
if var_shape[idx] == -1 or mapping[idx] == -1:
|
||||
new_shape.append(var_shape[idx])
|
||||
else:
|
||||
assert var_shape[idx] % mesh[mapping[idx]] == 0, (
|
||||
f"un-event partition: var_shape[idx]=[{var_shape[idx]}], mesh[{mesh[mapping[idx]]}], {var.name}, {var_shape}, {mesh}, {mapping}"
|
||||
)
|
||||
new_shape.append(var_shape[idx] // mesh[mapping[idx]])
|
||||
|
||||
return new_shape
|
||||
|
||||
|
||||
def _partition_parameter(
|
||||
dist_context, src_var, dst_block, dst_varname, dst_shape
|
||||
):
|
||||
# NOTE hack to copied Parameter
|
||||
# not initialized parameter, need to initialize it
|
||||
copied_kwargs = {}
|
||||
copied_kwargs['trainable'] = src_var.trainable
|
||||
copied_kwargs['optimize_attr'] = src_var.optimize_attr
|
||||
copied_kwargs['regularizer'] = src_var.regularizer
|
||||
copied_kwargs['do_model_average'] = src_var.do_model_average
|
||||
copied_kwargs['need_clip'] = src_var.need_clip
|
||||
|
||||
param = Parameter(
|
||||
block=dst_block,
|
||||
type=src_var.type,
|
||||
name=dst_varname,
|
||||
shape=dst_shape,
|
||||
dtype=src_var.dtype,
|
||||
lod_level=src_var.lod_level,
|
||||
error_clip=src_var.error_clip,
|
||||
stop_gradient=src_var.stop_gradient,
|
||||
is_data=src_var.is_data,
|
||||
belong_to_optimizer=src_var.belong_to_optimizer,
|
||||
**copied_kwargs,
|
||||
)
|
||||
|
||||
return param
|
||||
|
||||
|
||||
def _partition_intermediate_var(
|
||||
dist_context, src_var, dst_block, dst_varname, dst_shape
|
||||
):
|
||||
var = dst_block.create_var(
|
||||
type=src_var.type,
|
||||
name=dst_varname,
|
||||
shape=dst_shape,
|
||||
dtype=src_var.dtype,
|
||||
lod_level=src_var.lod_level,
|
||||
persistable=src_var.persistable,
|
||||
error_clip=src_var.error_clip,
|
||||
stop_gradient=src_var.stop_gradient,
|
||||
is_data=src_var.is_data,
|
||||
belong_to_optimizer=src_var.belong_to_optimizer,
|
||||
)
|
||||
|
||||
return var
|
||||
|
||||
|
||||
def _partition_var(
|
||||
dist_context, src_block, dst_block, src_varname, dst_varname
|
||||
):
|
||||
"""
|
||||
partition include: split + replicate
|
||||
"""
|
||||
src_var = src_block.var(src_varname)
|
||||
|
||||
if src_var.type in __no_shape_var_type__:
|
||||
persist = getattr(src_var, 'persistable', False)
|
||||
new_var = dst_block.create_var(
|
||||
type=src_var.type,
|
||||
name=dst_varname,
|
||||
persistable=persist,
|
||||
stop_gradient=True,
|
||||
)
|
||||
target_shape = None
|
||||
else:
|
||||
dist_attr = dist_context.get_tensor_dist_attr_for_program(src_var)
|
||||
target_shape = _get_dist_shape(src_var, dist_attr)
|
||||
|
||||
if isinstance(src_var, Parameter):
|
||||
new_var = _partition_parameter(
|
||||
dist_context, src_var, dst_block, dst_varname, target_shape
|
||||
)
|
||||
else:
|
||||
new_var = _partition_intermediate_var(
|
||||
dist_context, src_var, dst_block, dst_varname, target_shape
|
||||
)
|
||||
|
||||
dist_attr = copy.deepcopy(
|
||||
dist_context.get_tensor_dist_attr_for_program(src_var)
|
||||
)
|
||||
assert dist_attr is not None
|
||||
dist_context.set_tensor_dist_attr_for_program(new_var, dist_attr)
|
||||
|
||||
return target_shape
|
||||
|
||||
|
||||
def _get_dist_op_backward_implement(
|
||||
backward_op, dist_context, forward_op_id2forward_op
|
||||
):
|
||||
dist_op_context = dist_context.dist_op_context
|
||||
if backward_op.desc.original_id() in dist_op_context.grad_op_id_to_op_id:
|
||||
forward_op_id = dist_op_context.grad_op_id_to_op_id[
|
||||
backward_op.desc.original_id()
|
||||
]
|
||||
forward_op = forward_op_id2forward_op[forward_op_id]
|
||||
forward_op_dist_attr = dist_context.get_op_dist_attr_for_program(
|
||||
forward_op
|
||||
)
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(
|
||||
forward_op_dist_attr.impl_type
|
||||
)
|
||||
dist_op_impl = dist_op_impl_container.get_impl(
|
||||
forward_op_dist_attr.impl_idx
|
||||
)
|
||||
return dist_op_impl
|
||||
|
||||
# # NOTE trick for dist ops that only have backward implement
|
||||
if backward_op.type in BACKWARD_ONLY_DIST_OPS:
|
||||
op_dist_attr = dist_context.get_op_dist_attr_for_program(backward_op)
|
||||
assert op_dist_attr.impl_idx >= 0
|
||||
dist_op_impl = get_distributed_operator_impl_container(
|
||||
op_dist_attr.impl_type
|
||||
).get_impl(op_dist_attr.impl_idx)
|
||||
return dist_op_impl
|
||||
|
||||
dist_op = get_distributed_operator_impl_container("default")
|
||||
return dist_op.get_impl(0)
|
||||
|
||||
|
||||
def _get_dist_op_forward_implement(forward_op, dist_context):
|
||||
dist_attr = dist_context.get_op_dist_attr_for_program(forward_op)
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(
|
||||
dist_attr.impl_type
|
||||
)
|
||||
dist_op_impl = dist_op_impl_container.get_impl(dist_attr.impl_idx)
|
||||
return dist_op_impl
|
||||
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user