chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:40:42 +08:00
commit e25996e7db
15472 changed files with 3536181 additions and 0 deletions
@@ -0,0 +1,145 @@
# 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 .allreduce_matmul_grad_overlapping import ( # noqa: F401
AllreduceMatmulGradOverlappingPass,
)
from .auto_parallel_amp import ( # noqa: F401
AMPLists,
AMPPass,
AMPState,
)
from .auto_parallel_c_embedding import ( # noqa: F401
AutoParallelCEmbeddingPass,
)
from .auto_parallel_data_parallel_optimization import ( # noqa: F401
DataParallelOptimizationPass,
GradientsGroup,
)
from .auto_parallel_fp16 import ( # noqa: F401
FP16Pass,
FP16State,
cast_startup_program,
set_auto_cast_attr,
set_op_dtype_to_fp16,
)
from .auto_parallel_fused_linear_promotion import ( # noqa: F401
FusedLinearPromotionPass,
)
from .auto_parallel_grad_clip import ( # noqa: F401
ClipGradByGlobalNormPass,
ClipHelper,
)
from .auto_parallel_gradient_merge import ( # noqa: F401
GradientMergePass,
)
from .auto_parallel_master_grad import ( # noqa: F401
MasterGradPass,
get_output_in_varlist,
)
from .auto_parallel_quantization import QuantizationPass # noqa: F401
from .auto_parallel_recompute import ( # noqa: F401
RecomputePass,
RecomputeState,
)
from .auto_parallel_recompute_pir import ( # noqa: F401
AutoParallelRecomputePIRPass,
)
from .auto_parallel_replace_with_parallel_cross_entropy import ( # noqa: F401
AutoParallelReplaceWithParallelCrossEntropyPass,
)
from .auto_parallel_sequence_parallel_optimization import ( # noqa: F401
SequenceParallelOptimizationPass,
)
from .auto_parallel_sharding import ( # noqa: F401
ShardingInfo,
ShardingPass,
VarGroup,
group_param,
is_sharding_param_broadcast_op,
partition_by_greedy_even,
partition_by_use_order,
partition_parameters,
re_order_program,
)
from .auto_parallel_supplement_explicit_dependencies import ( # noqa: F401
AutoParalSupplementDepPass,
)
from .auto_parallel_sync_shared_params import ( # noqa: F401
AutoParallelSyncSharedParamsPass,
)
from .cpp_pass import ( # noqa: F401
BuildCINNPass,
FuseAdamWPass,
FuseBatchNormActPass,
FuseBatchNormAddActPass,
FusedAttentionPass,
FusedFeedforwardPass,
FuseDotProductAttentionPass,
FuseElementwiseAddActPass,
FuseGemmEpiloguePass,
FuseOptimizerPass,
FuseReluDepthwiseConvPass,
FuseResUnitPass,
)
# InplaceAddtoOpPass,
from .fuse_all_reduce import ( # noqa: F401
FuseAllReducePass,
filter_all_collective_op_indices,
find_adjacent_match_sequences,
find_all_fuse_all_reduce_groups,
has_same_attrs,
insert_coalesce_tensor_ops,
insert_fuse_all_reduce_by_memory_size,
insert_fuse_all_reduce_ops,
split_fuse_all_reduce_groups_by_deps,
)
from .pass_base import PassContext, PassManager, new_pass
from .pipeline_scheduler_pass import ( # noqa: F401
Pipeline1F1BPass,
PipelineEager1F1BPass,
PipelineFThenBPass,
PipelineVirtualPipelinePass,
PipelineZeroBubblePipelinePass,
apply_pass,
)
from .ps_server_pass import ( # noqa: F401
AddGeoOptimizerPass,
AddListenAndServPass,
AddLrDecayTablePass,
AddOptimizerPass,
AddRpcGlobalFlagsPass,
BuildPserverStartupProgramPass,
DeleteUnusedInStartupPass,
)
from .ps_trainer_pass import ( # noqa: F401
AppendSendOpsPass,
DeleteExtraOptimizerPass,
DeleteOptimizesPass,
DistributedOpsPass,
FakeInitOpsPass,
PsGpuPass,
PsTranspilePass,
SetHeterPipelineOptPass,
SplitFlOpsPass,
SplitHeterWorkerOpsPass,
SplitTrainerOpsPass,
)
__all__ = [
'new_pass',
'PassManager',
'PassContext',
]
@@ -0,0 +1,160 @@
# 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 logging
from ..auto_parallel.static.utils import (
get_logger,
)
from .pass_base import PassBase, register_pass
from .pass_utils import AutoParallelStreamType, split_matmul_grad_to_matmul
logger = get_logger(logging.INFO)
# For allreduce pattern in the backward phase of column parallel linear:
# dX, dY = matmul_grad(X, Y, dOut)
# dX = all_reduce_sum(dX)
# Split matmul_grad to 2 matmul:
# dX = matmul(dOut, Y^T)
# dX = all_reduce_sum(dX)
# dY = matmul(X^T, dOut)
#
# Then the all_reduce sum can overlap with the compute of dY.
@register_pass("allreduce_matmul_grad_overlapping")
class AllreduceMatmulGradOverlappingPass(PassBase):
def __init__(self):
super().__init__()
self.op_namescope = "/auto_parallel/allreduce_matmul_grad_overlapping"
self.set_attr("dist_context", None)
def _check_self(self):
if self.get_attr("dist_context") is None:
return False
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, context):
self.dist_context = self.get_attr("dist_context")
block = main_program.global_block()
matmul_grad_id_to_allreduce_id = (
self._get_all_matmul_grad_and_allreduce_pairs(block)
)
logger.info(
f"overlap matmul_grad and allreduce: {matmul_grad_id_to_allreduce_id}"
)
self._split_matmul_grad_and_multi_streaming_allreduce(
block, matmul_grad_id_to_allreduce_id
)
def _get_all_matmul_grad_and_allreduce_pairs(self, block):
ops = block.ops
op_num = len(ops)
matmul_grad_id_to_allreduce_id = collections.OrderedDict()
for i, op_i in enumerate(ops):
if (
op_i.type == 'matmul_v2_grad'
and op_i.attr("trans_x") is False
and op_i.attr("trans_y") is False
):
x_grad = op_i.output("X@GRAD")
for j in range(i + 1, op_num):
op_j = ops[j]
if (
op_j.type == 'c_allreduce_sum'
and op_j.input("X") == x_grad
):
matmul_grad_id_to_allreduce_id[i] = j
return matmul_grad_id_to_allreduce_id
def _split_matmul_grad_and_multi_streaming_allreduce(
self, block, matmul_grad_id_to_allreduce_id
):
ops = block.ops
for matmul_grad_id, allreduce_id in reversed(
matmul_grad_id_to_allreduce_id.items()
):
matmul_grad_op = ops[matmul_grad_id]
allreduce_op = ops[allreduce_id]
# NOTE(Sonder): When there are ops between matmul_grad and allreduce, we should check whether
# these ops rely on the output of the intermediate ops. If so, we should not split the matmul_grad.
# Otherwise, the output of the intermediate ops will get wrong results.
skip_overlapping = False
moved_ops_output = []
matmul_grad_output = matmul_grad_op.output('Y@GRAD')[0]
for idx in range(matmul_grad_id + 1, allreduce_id):
if matmul_grad_output in ops[idx].desc.input_arg_names():
moved_ops_output.extend(ops[idx].desc.output_arg_names())
else:
for input_name in ops[idx].desc.input_arg_names():
if input_name in moved_ops_output:
skip_overlapping = True
if skip_overlapping:
continue
# matmul_grad_op => matmul_v2 + reshape + reshape + matmul_v2 + reshape
split_matmul_grad_to_matmul(
block, matmul_grad_id, self.dist_context, self.op_namescope
)
# NOTE(Ruibiao): Required OP scheduling order: matmul(dOut, Y^T) -> all_reduce_sum(dX) -> matmul(X^T, dOut).
# all_reduce_sum(dX) and matmul(X^T, dOut) cannot be swapped. Otherwise, after buffer_shared_inplace_pass
# adding share_buffer OP before all_reduce_sum, all_reduce_sum will synchronous with comp-stream, and then
# the matmul op before it cannot be overlapped.
allreduce_op_dist_attr = (
self.dist_context.get_op_dist_attr_for_program(allreduce_op)
)
allreduce_op_dist_attr.execution_stream = (
AutoParallelStreamType.MP_STREAM.value
)
allreduce_op_inputs = allreduce_op.desc.input_names()
allreduce_op_outputs = allreduce_op.desc.output_names()
allreduce_op_inputs = {
name: allreduce_op.input(name) for name in allreduce_op_inputs
}
allreduce_op_outputs = {
name: allreduce_op.output(name) for name in allreduce_op_outputs
}
# matmul_v2 + reshape + reshape + matmul_v2 + reshape + ... + original all_reduce_sum
# =>
# matmul_v2 + new all_reduce_sum + reshape + reshape + matmul_v2 + reshape + ... + original all_reduce_sum
#
# NOTE(liym27): new all_reduce_sum must be inserted to "the next of the first matmul_v2", otherwise another
# pass fused_linear_param_grad_add will not work.
allreduce_op = block._insert_op_without_sync(
index=matmul_grad_id + 1,
type=allreduce_op.type,
inputs=allreduce_op_inputs,
outputs=allreduce_op_outputs,
attrs=allreduce_op.all_attrs(),
)
self.dist_context.set_op_dist_attr_for_program(
allreduce_op, allreduce_op_dist_attr
)
# Remove the original allreduce op
block._remove_op(allreduce_id + 5, sync=False)
block._sync_with_cpp()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,345 @@
# 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 re
import warnings
import paddle
import paddle.distributed as dist
from paddle.base.core import TensorDistAttr
from paddle.distributed import fleet
from paddle.distributed.auto_parallel.static.dist_attribute import (
DistTensorSpec,
)
from paddle.distributed.fleet.meta_optimizers.common import OpRole
from paddle.framework import core
from .pass_base import PassBase, register_pass
@register_pass("auto_parallel_c_embedding_pass")
class AutoParallelCEmbeddingPass(PassBase):
def __init__(self):
super().__init__()
def _check_self(self):
hcg = fleet.get_hybrid_communicate_group()
mp_size = hcg.get_model_parallel_world_size()
if mp_size > 1:
return True
warnings.warn("c_embedding pass is only applicable to tnesor parallel.")
return False
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, context):
concrete_program = self.get_attr("concrete_program")
ops = main_program.global_block().ops
for i, op in enumerate(ops):
if op.name() == 'pd_op.embedding':
# update weight dims mapping
mp_axis = self._update_weight(op, concrete_program)
# update startup_program
self._update_startup_program(startup_program, mp_axis)
# replace embedding with c_embedding
c_emb_op = self._replace_embedding_with_c_embedding(op)
# insert allreduce reshard
comm_op = self._insert_allreduce_reshard(c_emb_op)
# update dims_mapping before c_embedding
self._update_before_dims_mapping(c_emb_op)
# update dims_mapping after c_embedding
self._update_after_dims_mapping(comm_op)
def _update_weight(self, op, concrete_program):
# update weight dims_mapping concrete_program
placements = op.operand(1).source().placements
dim_map, partial_status = dist.auto_parallel.placement_type.to_dim_map(
placements, op.operand(1).source().ndim
)
# mp_axis is used to specify the axis for row parallel
mp_axis = -1
dim_map = [-1, -1]
hcg = fleet.get_hybrid_communicate_group()
mp_size = hcg.get_model_parallel_world_size()
if mp_size > 1:
strategy = fleet.DistributedStrategy()
# get mp_axis from DistributedStrategy
mp_axis = strategy.hybrid_configs['mp_degree']
dim_map = [mp_axis, -1]
dist_attr_w = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
op.operand(1).source().process_mesh,
dim_map,
partial_status,
)
dist_type_input0 = paddle.base.libpaddle.pir.cvt_to_dist_type(
op.operand(1).source().type(), dist_attr_w
)
op.operand(1).source().set_type(dist_type_input0)
# update c_embedding weight dynamic parameters
dy_params = concrete_program.parameters[0]
pattern = re.compile(r'embedding_.*\.w_0\.dist')
for index, param in enumerate(dy_params):
if pattern.match(param.name):
var_dist_attr = TensorDistAttr()
var_dist_attr.process_mesh = dist_attr_w.process_mesh
var_dist_attr.dims_mapping = dist_attr_w.dims_mapping
tmp = paddle.base.core.reshard(param, var_dist_attr)
param.get_tensor()._share_data_with(tmp.get_tensor())
return mp_axis
def _replace_embedding_with_c_embedding(self, op):
paddle.pir.set_insertion_point(op)
num_embeddings = op.operand(1).source().type().shape[0]
hcg = fleet.get_hybrid_communicate_group()
# compute the start_index using the MP's world size and rank
mp_size = hcg.get_model_parallel_world_size()
mp_rank = hcg.get_model_parallel_rank()
per_part_size = num_embeddings // mp_size
vocab_start_index = mp_rank * per_part_size
t_op = paddle._C_ops.c_embedding(
op.operand(1).source(),
op.operand(0).source(),
vocab_start_index,
num_embeddings,
)
t_op.get_defining_op().op_role = int(OpRole.Forward)
new_op = t_op.get_defining_op()
op.result(0).replace_all_uses_with(t_op)
op.erase()
return new_op
def _insert_allreduce_reshard(self, c_emb_op):
result = c_emb_op.result(0)
paddle.pir.set_insertion_point_after(c_emb_op)
placements = result.dist_attr().placements
dim_map, partial_status = dist.auto_parallel.placement_type.to_dim_map(
placements, result.ndim
)
partial_status = {}
dist_attr_new = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
result.process_mesh,
dim_map,
partial_status,
)
# insert allreduce by inserting reshard with an empty partial.
comm_op_t = paddle._C_ops.reshard_v2(result, dist_attr_new)
comm_op_t.get_defining_op().op_role = int(OpRole.Forward)
result.replace_all_uses_with(comm_op_t)
comm_op = comm_op_t.get_defining_op()
comm_op.operand(0).set_source(result)
return comm_op
def _update_before_dims_mapping(self, new_op):
placements = new_op.operand(0).source().placements
stack = [new_op.operand(0).source().get_defining_op()]
# adjust all ops before c_embedding until parameters input
while stack:
op = stack.pop()
operands, results = [], []
if op.num_results() > 0:
for result, result_dist in zip(
op.results(), op.dist_attr.results()
):
placements_dist = (
result_dist.as_tensor_dist_attr().placements
)
if placements != placements_dist:
dim_map, partial_status = (
dist.auto_parallel.placement_type.to_dim_map(
placements, result.ndim
)
)
dist_attr_new = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
result.process_mesh,
dim_map,
partial_status,
)
dist_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
result.type(), dist_attr_new
)
result.set_type(dist_type)
results.append(dist_attr_new)
sub_name = op.name().split('.')[1]
if op.num_operands() > 0:
assert sub_name != "cast", (
"Need to add support for {sub_name}."
)
operands.append(dist_attr_new)
next_op = op.operand(0).source().get_defining_op()
stack.append(next_op)
process_mesh = (
op.results()[0].process_mesh
if op.num_results() > 0
else op.operand(0).source().process_mesh
)
op.dist_attr = (
paddle.base.libpaddle.pir.create_op_dist_attribute(
process_mesh,
operands,
results,
)
)
def _update_after_dims_mapping(self, new_op):
placements = new_op.result(0).placements
pre_id = new_op.id()
stack = list(new_op.result(0).all_used_ops())
# adjust all ops after c_embedding until the placements are consistent
while stack:
op = stack.pop()
operands, results = [], []
if op.num_operands() > 0:
for operand, operand_dist in zip(
op.operands_source(), op.dist_attr.operands()
):
if operand.get_defining_op().id() != pre_id:
continue
placements_dist = (
operand_dist.as_tensor_dist_attr().placements
)
if placements != placements_dist:
dim_map, partial_status = (
dist.auto_parallel.placement_type.to_dim_map(
placements, operand.ndim
)
)
dist_attr_new = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
operand.process_mesh,
dim_map,
partial_status,
)
dist_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
operand.type(), dist_attr_new
)
operand.set_type(dist_type)
operands.append(dist_attr_new)
sub_name = op.name().split('.')[1]
if sub_name == 'reshard':
# only change reshards inputs
placements_out0 = op.results()[0].placements
dim_map_out0, partial_status_out0 = (
dist.auto_parallel.placement_type.to_dim_map(
placements_out0,
op.results()[0].ndim,
)
)
dist_attr_out0 = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
op.results()[0].process_mesh,
dim_map_out0,
partial_status_out0,
)
results.append(dist_attr_out0)
elif core.contains_spmd_rule(sub_name):
# redo the infer spmd_rule
rule = core.get_phi_spmd_rule(sub_name)
tensor_dist_attr = TensorDistAttr()
tensor_dist_attr.dims_mapping = dim_map
partial_dims = []
for i, p in enumerate(placements):
if isinstance(p, dist.Partial):
partial_dims.append(i)
if len(partial_dims) > 0:
tensor_dist_attr._set_partial_dims(partial_dims)
tensor_dist_attr.process_mesh = operand.process_mesh
inputs = DistTensorSpec(
operand.shape, tensor_dist_attr
)
attr_names = op.get_attr_names()
input_specs = []
input_specs.append(inputs)
for attr_name in attr_names:
input_specs.append(op.attrs()[attr_name])
inferred_dist_attrs = rule.infer_forward(
*input_specs
)
dims_mapping_new_out = inferred_dist_attrs[1][
0
].dims_mapping
partial_status = {}
if inferred_dist_attrs[1][0]._is_partial():
partial_dims = inferred_dist_attrs[1][
0
]._partial_dims()
for i in partial_dims:
partial_status[i] = (
paddle.base.core.ReduceType.kRedSum
)
dist_attr_new_out = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
operand.process_mesh,
dims_mapping_new_out,
partial_status,
)
dist_type = (
paddle.base.libpaddle.pir.cvt_to_dist_type(
op.result(0).type(), dist_attr_new_out
)
)
op.result(0).set_type(dist_type)
results.append(dist_attr_new_out)
next_op = op.results()[0].all_used_ops()[0]
stack.append(next_op)
pre_id = op.id()
placements = dist_attr_new_out.placements
else:
results.append(dist_attr_new)
next_op = op.results()[0].all_used_ops()[0]
stack.append(next_op)
pre_id = op.id()
process_mesh = (
op.results()[0].process_mesh
if op.num_results() > 0
else op.operand(0).source().process_mesh
)
op.dist_attr = (
paddle.base.libpaddle.pir.create_op_dist_attribute(
process_mesh,
operands,
results,
)
)
def _update_startup_program(self, startup_program, mp_axis):
# modify the startup_program because the optimizer needs to use
startup_block = startup_program.global_block()
for op in startup_block.ops:
if op.name() == 'pd_op.full':
next_op = op.result(0).all_used_ops()[0]
parameter_name = next_op.str_attr("parameter_name")
pattern = re.compile(r'embedding_.*\.w_0\.dist')
if pattern.match(parameter_name):
placements = op.results()[0].placements
dim_map, partial_status = (
dist.auto_parallel.placement_type.to_dim_map(
placements, len(placements)
)
)
dim_map = [mp_axis, -1]
dist_attr = (
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
op.results()[0].process_mesh,
dim_map,
partial_status,
)
)
dist_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
op.results()[0].type(), dist_attr
)
op.results()[0].set_type(dist_type)
op.dist_attr = (
paddle.base.libpaddle.pir.create_op_dist_attribute(
op.results()[0].process_mesh, [], [dist_attr]
)
)
@@ -0,0 +1,766 @@
# 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
import paddle
from paddle.distributed.auto_parallel.process_mesh import ProcessMesh
from paddle.distributed.auto_parallel.static.dist_attribute import (
OperatorDistAttr,
TensorDistAttr,
)
from paddle.distributed.auto_parallel.static.operators.common import (
is_data_parallel_reduce_op,
is_data_parallel_scale_op,
)
from paddle.distributed.auto_parallel.static.utils import (
find_higher_order_backward_op,
get_var_numel,
insert_dependencies_for_vars,
is_forward_op,
is_loss_grad_op,
is_optimize_op,
ring_id_to_process_group,
)
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
from paddle.static import default_main_program
from paddle.utils import unique_name
from .pass_base import PassBase, PassType, register_pass
# add new optimizers supporting rescale_grad here
__rescale_grad_supported_opts__ = [
'lars_momentum',
'sparse_momentum',
'dgc_momentum',
'momentum',
'merge_momentum',
]
# a heuristic number
__max_stream_num_allow__ = 16
@register_pass("auto_parallel_data_parallel_optimization")
class DataParallelOptimizationPass(PassBase):
"""
Apply Optimizations that specialized for data parallelism in Auto Parallel.
1. prune grad scaling
2. overlap comm and calc
3. fuse allreduce
"""
def __init__(self):
super().__init__()
# NOTE not use dependence on loss and param_grads
self.set_attr("dist_context", None)
self.set_attr("global_rank", -1)
self.set_attr("use_sharding", False)
# {grad1: group1, grad2: group1, grad3: group2}
# record the order for fuse grad data memory
self._grad_name_to_group_map = OrderedDict()
# {group1:[grad1, grad2] , group2:[grad3]}
self._group_to_grad_name_map = OrderedDict()
self._support_rescale_grad = False
def _check_self(self):
if self.get_attr("dist_context") is None:
return False
if (not isinstance(self.get_attr("global_rank"), int)) or self.get_attr(
"global_rank"
) < 0:
return False
return True
def _check_conflict(self, other_pass):
return True
def _type(self):
return PassType.COMM_OPT
def _apply_single_impl(self, main_program, startup_program, context):
self.dist_context = self.get_attr("dist_context")
self.global_rank = int(self.get_attr("global_rank"))
self.use_sharding = self.get_attr("use_sharding")
self.coalesce_prefix = 'coalesce_grad'
self.gradient_sync_stream = "gradient_sync_stream"
with paddle.static.program_guard(main_program, startup_program):
self._analyze_program()
# TODO refactor here to first fuse then overlap
if self.is_data_parallel_applied():
self._prune_grad_scaling()
self._calc_comm_overlap()
grad_group = self._fuse_allreduce()
self._add_dependencies(grad_group)
self.summary(grad_group)
def _prune_grad_scaling(self):
if not self._could_be_prune():
return
if self._all_dp_groups_same_degree():
self._scale_backward_initial_grad()
else:
self._update_opt_rescale_grad()
self._remove_grad_scaling()
def _calc_comm_overlap(self):
if not self._could_be_overlap():
return
self._comms_overlap_calc()
self._calc_wait_comms()
def _fuse_allreduce(self):
if not self._could_be_fuse():
return []
grad_group = self._group_grads()
self._update_program(grad_group)
return grad_group
def _analyze_program(self):
"""
build two maps
{param_grad_name: data_parallel_group}
{pdata_parallel_group: aram_grad_name}
"""
block = default_main_program().global_block()
ops = block.ops
scaled_grads = []
for op in ops:
if is_data_parallel_reduce_op(op):
grad_name = op.output_arg_names[0]
if grad_name in self._grad_name_to_group_map:
continue
assert op.has_attr("ring_id"), (
f"Unexpected: comm op [{op}] has NOT ring id."
)
group = ring_id_to_process_group(op.attr("ring_id"))
assert group is not None, (
f"Unexpected: data parallel group of [{grad_name}] from op [{op}] is None"
)
self._grad_name_to_group_map[grad_name] = group
if group not in self._group_to_grad_name_map:
self._group_to_grad_name_map[group] = [grad_name]
else:
self._group_to_grad_name_map[group].append(grad_name)
elif is_data_parallel_scale_op(op):
grad_name = op.output_arg_names[0]
scaled_grads.append(grad_name)
# TODO support multiple optimizers in on network in future.
# here we assume that the optimizer is unique in network.
elif (
is_optimize_op(op)
and op.type in __rescale_grad_supported_opts__
):
self._support_rescale_grad = True
not_synchronized_grads = []
for grad_name in scaled_grads:
if grad_name not in self._grad_name_to_group_map:
not_synchronized_grads.append(grad_name)
assert len(not_synchronized_grads) == 0, (
f"Unexpected: gradients [{not_synchronized_grads}] is scaled BUT NOT synchronized."
)
def is_data_parallel_applied(self):
return len(self._group_to_grad_name_map) > 0
def _could_be_prune(self):
return self.dist_context.gradient_scale and (
self._support_rescale_grad or self._all_dp_groups_same_degree()
)
def _all_dp_groups_same_degree(self):
return (
len(
{
len(group.ranks)
for group in self._group_to_grad_name_map.keys()
}
)
== 1
)
def _scale_backward_initial_grad(self):
block = default_main_program().global_block()
dp_degree = len(next(iter(self._group_to_grad_name_map.keys())).ranks)
for idx, op in reversed(list(enumerate(block.ops))):
if is_loss_grad_op(op):
assert op.type == 'fill_constant', (
"loss_grad_op must be fill_constant op, "
f"but this op is {op.type}"
)
assert op.has_attr('value')
loss_scale = float(op.attr('value'))
loss_scale = loss_scale / dp_degree
op._set_attr('value', loss_scale)
break
def _remove_grad_scaling(self):
block = default_main_program().global_block()
for op_idx, op in reversed(list(enumerate(block.ops))):
if is_data_parallel_scale_op(op):
block._remove_op(op_idx, False)
block._sync_with_cpp()
def _update_opt_rescale_grad(self):
block = default_main_program().global_block()
scaled_grads = set()
for idx, op in reversed(list(enumerate(block.ops))):
if (
is_optimize_op(op)
and op.type in __rescale_grad_supported_opts__
):
assert op.has_attr('rescale_grad'), (
f"Unexpected: op [{op}] is supported to have [rescale_grad] attribute."
)
assert len(op.input("Grad")) == 1, (
f"Unexpected: op [{op}] is supported to have only one input grad var."
)
grad_name = op.input("Grad")[0]
dp_degree = len(
list(self._grad_name_to_group_map[grad_name].ranks)
)
scaled_grads.add(grad_name)
rescale_grad = float(op.attr('rescale_grad')) / dp_degree
op._set_attr('rescale_grad', rescale_grad)
assert scaled_grads == set(self._grad_name_to_group_map.keys()), (
f"Unexpected: gradients [{set(self._grad_name_to_group_map.keys()) - scaled_grads}] are unscaled."
)
def _could_be_overlap(self):
# NOTE current different nccl comm will use different cuda stream
# so if there too many dp group there will be too many stream need to be
# created and sync.
# revise here when framework support custom stream in static graph mode.
num_dp_comm_stream = len(set(self._group_to_grad_name_map.keys()))
if num_dp_comm_stream > __max_stream_num_allow__:
return False
if self.use_sharding:
return False
return True
def _comms_overlap_calc(self):
# TODO support InterpreterCore executor for overlap.
# InterpreterCore has a different logic for overlapping
# which is different from use_calc_stream
block = default_main_program().global_block()
# comm wait calc to finish
for idx, op in reversed(list(enumerate(block.ops))):
if is_data_parallel_reduce_op(op):
assert op.has_attr('ring_id')
op._set_attr('use_calc_stream', False)
ring_id = op.attr("ring_id")
block._insert_op_without_sync(
idx,
type='c_wait_compute',
inputs={'X': []},
outputs={'Out': []},
attrs={'op_role': OpRole.Backward, 'ring_id': ring_id},
)
block._sync_with_cpp()
def _calc_wait_comms(self):
return
block = default_main_program().global_block()
# NOTE the naive overlap implement in static hybrid parallel only sync comm stream
# at the end of Backward phase, based on a strong constraint that
# all communicating gradient would NOT be used after communication in Backward phase.
# BUT this constraint will fail for scenario like Weight-Sharing and Higher-Order Differentiation,
# where gradient will be involved in other calculation between data-parallel allreduce kernel submitted
# into comm streams and the synchronization of comm stream at the end of Backward phase.
# synchronization of comm stream should add according to the usage of communicating gradients
# to support Overlapping for Weight-Sharing and Higher-Order Differentiation.
ring_id_to_un_sync_grad_map = {}
op_idx_to_sync_ring_id_map = {}
for group in self._group_to_grad_name_map.keys():
ring_id_to_un_sync_grad_map[group.id] = []
# analyze the where need to sync
for i, op in enumerate(block.ops):
if is_data_parallel_reduce_op(op):
ring_id = op.attr("ring_id")
grad_name = op.output_arg_names[0]
ring_id_to_un_sync_grad_map[ring_id].append(grad_name)
elif is_data_parallel_scale_op(op):
continue
# other ops that might use communicating grad
else:
for input_var_name in op.input_arg_names:
for (
ring_id,
unsync_grad_names,
) in ring_id_to_un_sync_grad_map.items():
if input_var_name in unsync_grad_names:
# need to sync before op_i
if i in op_idx_to_sync_ring_id_map:
op_idx_to_sync_ring_id_map[i].append(ring_id)
else:
op_idx_to_sync_ring_id_map[i] = [ring_id]
# all grads in this comm stream are synced
ring_id_to_un_sync_grad_map[ring_id] = []
# insert synchronization
indices = list(op_idx_to_sync_ring_id_map.keys())
# TODO the synchronization could be optimized
# we should record the event of a gradient is communicating and
# only wait for that event to be completed.
# BUT paddle static currently not support op api for event record only, so
# here we try to wait for all kernel in that comm stream to be finish which is not that optimized.
for i in sorted(indices, reverse=True):
for ring_id in op_idx_to_sync_ring_id_map[i]:
block._insert_op_without_sync(
i,
type='c_wait_comm',
inputs={'X': []},
outputs={'Out': []},
attrs={'op_role': OpRole.Backward, 'ring_id': ring_id},
)
block._sync_with_cpp()
def _could_be_fuse(self):
# TODO support gradient fuse higher order gradient.
# should analyse the dependencies of gradient in backward.
if find_higher_order_backward_op(default_main_program()):
return False
if self.use_sharding:
return False
return True
def _group_grads(self):
"""
conditions for gradients to be grouped:
1. group size < max_fuse_numel
2. same dp group
3. same dtype
4. dependency: grad would NOT be used by other ops within group segment
gradients inside same group would be fuse into one coalesce tensor
"""
block = default_main_program().global_block()
ops = block.ops
# group individual grad vars
# TODO consider fuse gradient for sharding reduce
# TODO let user to set fuse_grad_size
# emb = 50000 * h, ffn = 8 * h * h, mha = 4 * h * h
h = 2048
ffn_numel = 2 * (4 * h) * h
mha_numel = 3 * h * h + h * h
max_fuse_numel = ffn_numel + mha_numel
grad_groups = []
cur_group = GradientsGroup(ops, max_fuse_numel)
grouped_grad_names = set()
def collect_group(cur_group, grad_var, ring_id, i):
if len(cur_group.gradients) == 0:
cur_group = None
else:
cur_group.finalize()
grad_groups.append(cur_group)
new_group = GradientsGroup(ops, max_fuse_numel)
if grad_var:
new_group.add(grad_var, ring_id, i)
grouped_grad_names.add(grad_var.name)
return new_group
def op_depend_on_group(op, group):
vars_ = set(op.input_arg_names + op.output_arg_names)
grad_names = {grad.name for grad in group.gradients}
return len(vars_.intersection(grad_names)) > 0
for i, op in enumerate(ops):
if is_data_parallel_reduce_op(op):
ring_id = op.attr("ring_id")
grad_name = op.output_arg_names[0]
grad_var = block.var(grad_name)
grad_numel = get_var_numel(grad_var)
if cur_group.acceptable(grad_var, ring_id):
assert grad_name not in grouped_grad_names
grouped_grad_names.add(grad_name)
cur_group.add(grad_var, ring_id, i)
else:
cur_group = collect_group(cur_group, grad_var, ring_id, i)
else:
if op_depend_on_group(op, cur_group):
cur_group = collect_group(cur_group, None, None, None)
# collect last group
collect_group(cur_group, None, None, None)
return grad_groups
def _update_program(self, grad_groups):
block = default_main_program().global_block()
remove_op_types = [
'scale',
'all_reduce',
'c_wait_compute',
]
for i, group in enumerate(grad_groups[::-1]):
# skip unfused big tensor
if len(group.gradients) <= 1:
group.coalesce_var = group.gradients[0]
continue
ref_process_mesh = set()
concated_shapes = []
concated_ranks = []
for grad_ in group.gradients:
grad_dist_attr = (
self.dist_context.get_tensor_dist_attr_for_program(grad_)
)
ref_process_mesh.update(
set(grad_dist_attr.process_mesh.process_ids)
)
shape = grad_.shape
concated_shapes.extend(shape)
concated_ranks.append(len(shape))
# create coalesce tensor
group.coalesce_var = block.create_var(
name=unique_name.generate(self.coalesce_prefix + f'_{i}'),
dtype=group.dtype,
persistable=False,
stop_gradient=True,
)
tensor_dist_attr = TensorDistAttr()
tensor_dist_attr.process_mesh = ProcessMesh(list(ref_process_mesh))
tensor_dist_attr.dims_mapping = []
self.dist_context.set_tensor_dist_attr_for_program(
group.coalesce_var, tensor_dist_attr
)
# update allreduce & scale op
if group.scale_op_idx != -1:
scale_op = block.ops[group.scale_op_idx]
assert scale_op.type == 'scale', (
f"should found scale op but found {scale_op}"
)
scale_op._rename_input(
scale_op.input_arg_names[0], group.coalesce_var.name
)
scale_op._rename_output(
scale_op.output_arg_names[0], group.coalesce_var.name
)
allreduce_op = block.ops[group.allreduce_op_idx]
assert (
allreduce_op.type == 'all_reduce'
and allreduce_op.attr('reduce_type')
== paddle.distributed.ReduceOp.SUM
), f"should found all_reduce sum op but found {allreduce_op}"
allreduce_op_dist_attr = (
self.dist_context.get_op_dist_attr_for_program(allreduce_op)
)
old_in_name = allreduce_op.input_arg_names[0]
new_in_name = group.coalesce_var.name
allreduce_op._rename_input(old_in_name, new_in_name)
input_dist_attr = allreduce_op_dist_attr.get_input_dist_attr(
old_in_name
)
allreduce_op_dist_attr.set_input_dist_attr(
new_in_name, input_dist_attr
)
old_out_name = allreduce_op.output_arg_names[0]
new_out_name = group.coalesce_var.name
allreduce_op._rename_output(old_out_name, new_out_name)
out_dist_attr = allreduce_op_dist_attr.get_output_dist_attr(
old_out_name
)
allreduce_op_dist_attr.set_output_dist_attr(
new_out_name, out_dist_attr
)
# remove un-used op
remove_op_indices = (
group.remove_wait_op_indices
+ group.remove_allreduce_op_indices
+ group.remove_scale_op_indices
)
for idx in sorted(remove_op_indices, reverse=True):
assert block.ops[idx].type in remove_op_types, (
f"Unexpected: try to remove op {block.ops[idx]}"
)
block._remove_op(idx, False)
# insert coalesce op
grad_names = [grad.name for grad in group.gradients]
coalesce_op = block._insert_op_without_sync(
group.coalesce_op_idx,
type="coalesce_tensor",
inputs={"Input": grad_names},
outputs={
"Output": grad_names,
"FusedOutput": group.coalesce_var,
},
attrs={
"copy_data": False,
"use_align": True,
"dtype": group.dtype,
"concated_shapes": concated_shapes,
"concated_ranks": concated_ranks,
OP_ROLE_KEY: OpRole.Backward,
},
)
op_dist_attr = OperatorDistAttr()
op_dist_attr.impl_idx = 0
op_dist_attr.impl_type = "default"
op_dist_attr.process_mesh = ProcessMesh(list(ref_process_mesh))
for in_name in coalesce_op.input_arg_names:
in_var = block.var(in_name)
in_var_dist_attr = (
self.dist_context.get_tensor_dist_attr_for_program(in_var)
)
op_dist_attr.set_input_dims_mapping(
in_name, in_var_dist_attr.dims_mapping
)
for out_name in coalesce_op.output_arg_names:
out_var = block.var(out_name)
out_var_dist_attr = (
self.dist_context.get_tensor_dist_attr_for_program(out_var)
)
op_dist_attr.set_output_dims_mapping(
out_name, out_var_dist_attr.dims_mapping
)
self.dist_context.set_op_dist_attr_for_program(
coalesce_op, op_dist_attr
)
block._sync_with_cpp()
def _add_dependencies(self, grad_groups):
# NOTE Currently, auto_parallel need to adopt for two executors: Sequential executor (old exe) and Graph based
# multiple stream executor(standalone exe). This function just for standalone exe. Refactor here
# in future when only one executor stay.
if len(grad_groups) == 0:
return
block = default_main_program().global_block()
# Build maps
coalesce_to_vars_map = {}
for group in grad_groups:
coalesce_to_vars_map[group.coalesce_var.name] = group
# analyze dependencies
dep_map = {}
for idx, op in reversed(list(enumerate(block.ops))):
if is_forward_op(op):
break
if is_optimize_op(op):
continue
if is_data_parallel_reduce_op(op):
coalesce_var_name = op.output_arg_names[0]
if self.coalesce_prefix in coalesce_var_name:
group = coalesce_to_vars_map[coalesce_var_name]
dep_map[idx] = [
(
idx,
group.gradients[-1],
group.coalesce_var,
op.attr(OP_ROLE_KEY),
)
]
dep_map[idx].append(
(
idx + 1,
group.coalesce_var,
group.gradients,
op.attr(OP_ROLE_KEY),
)
)
# insert dependency op
indice = sorted(dep_map.keys(), reverse=True)
for i in indice:
for idx, prior_vars, post_vars, op_role in dep_map[i][::-1]:
depend_op = insert_dependencies_for_vars(
block,
idx,
prior_vars,
post_vars,
self.dist_context,
op_role,
is_recompute=False,
sync=False,
op_namescope="data_parallel_overlap_dep",
)
depend_op.dist_attr.execution_stream = self.gradient_sync_stream
block._sync_with_cpp()
# remove naive synchronization & assign allreduce stream
def remove_cond(op):
if op.type != "c_wait_compute":
return False
if len(op.input_arg_names) != 0:
return False
if len(op.output_arg_names) != 0:
return False
return True
for idx, op in reversed(list(enumerate(block.ops))):
if is_data_parallel_reduce_op(op):
op._set_attr('use_calc_stream', True)
op.dist_attr.execution_stream = self.gradient_sync_stream
if remove_cond(op):
block._remove_op(idx, sync=False)
block._sync_with_cpp()
def summary(self, grad_groups=[]):
# TODO: add logger module
import logging
self._logger = logging.getLogger()
self._logger.propagate = False
if not self._logger.handlers:
self._logger.setLevel(logging.INFO)
log_handler = logging.StreamHandler()
log_format = logging.Formatter(
'[%(levelname)s %(asctime)s %(filename)s:%(lineno)d] %(message)s'
)
log_handler.setFormatter(log_format)
self._logger.addHandler(log_handler)
if len(grad_groups) > 0:
self._logger.info("Data Parallel Optimization: ")
self._logger.info(
f" {len(self._grad_name_to_group_map.keys())} Allreduce ops are fused into {len(grad_groups)} coalesce allreduce ops."
)
self._logger.debug("gradient fusing group are following: ")
fused_grads = set()
for i, group in enumerate(grad_groups):
self._logger.debug(
f"coalesce gradient [{i}] is composed by: {[grad.name for grad in group.gradients]}"
)
fused_grads.update([grad.name for grad in group.gradients])
individual_grads = set(self._grad_name_to_group_map.keys()) - set(
fused_grads
)
self._logger.debug(
f"the following [{len(individual_grads)}] gradients are not fused: "
)
self._logger.debug(f"individual gradient {individual_grads}")
class GradientsGroup:
def __init__(self, ops, max_group_size):
self.max_group_size = max_group_size
self.ops = ops
self.gradients = []
self.numel = 0
self.dtype = None
self.ring_id = None
self.coalesce_var = None
self.coalesce_op_idx = -1
self.allreduce_op_idx = -1
self.scale_op_idx = -1
self.remove_wait_op_indices = []
self.remove_allreduce_op_indices = []
self.remove_scale_op_indices = []
def acceptable(self, grad_var, ring_id):
if len(self.gradients) == 0:
return True
if ring_id != self.ring_id:
return False
if get_var_numel(grad_var) + self.numel > self.max_group_size:
return False
if grad_var.dtype != self.dtype:
return False
return True
def add(self, grad_var, ring_id, i):
self.gradients.append(grad_var)
self.ring_id = ring_id
self.dtype = grad_var.dtype
self.numel += get_var_numel(grad_var)
# remove auxiliary ops in non-fuse dp allreduce
self.remove_allreduce_op_indices.append(i)
# NOTE this pass rely on the original synchronization add in previous passes
# (same stream or calc_wait_comm & comm_wait_calc)
# to guarantee the correctness of comm_calc execution order.
# so the calc_wait_comm should be keep.
grad_op_idx = i - 1
if i > 0 and self.ops[i - 1].type == 'c_wait_compute':
self.remove_wait_op_indices.append(i - 1)
grad_op_idx -= 1
if i + 1 < len(self.ops) and is_data_parallel_scale_op(self.ops[i - 1]):
self.remove_scale_op_indices.append(i + 1)
if len(self.gradients) == 1:
# TODO Remove this is a temporary hack for Tensor Parallel. the logic
# for find grad_op should be more general.
if (
self.ops[grad_op_idx].type == "all_reduce"
and self.ops[grad_op_idx].attr("reduce_type")
== paddle.distributed.ReduceOp.SUM
):
grad_op_idx -= 1
grad_op = self.ops[grad_op_idx]
assert grad_var.name in grad_op.output_arg_names, (
f"grad [{grad_var.name}] should be output of {grad_op}"
)
self.coalesce_op_idx = grad_op_idx
def finalize(self):
self.allreduce_op_idx = self.remove_allreduce_op_indices.pop()
if len(self.remove_wait_op_indices) > 1:
self.remove_wait_op_indices.pop()
if len(self.remove_scale_op_indices) > 1:
self.scale_op_idx = self.remove_scale_op_indices.pop()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,862 @@
# 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 numpy as np
import paddle
from paddle.distributed.auto_parallel.static.utils import (
is_optimize_op,
is_recompute_op,
naive_set_dist_op_attr_for_program_by_mesh_and_mapping,
set_var_dist_attr,
)
from paddle.utils import unique_name
from ..utils.log_utils import get_logger
from .auto_parallel_sharding import (
_inference_data_parallel_group_for_operator,
_is_reshard_op,
_skip_ops,
is_forward_op,
)
from .pass_base import PassBase, register_pass
logger = get_logger(logging.INFO, "FusedLinearPromotionPass")
_supported_optimizer_type = [
"adam",
"adamax",
"adamw",
"decayed_adagrad",
"momentum",
"dgc_momentum",
"lars_momentum",
"merged_momentum",
"lamb",
"sgd",
]
FUSED_LINEAR_SOURCE_PATTERNS_LIST = [
# amp_level == 'o2' or 'o3'
{ # only MP
"forward": ["matmul_v2", "all_reduce", "elementwise_add"],
"backward": ["elementwise_add_grad", "matmul_v2_grad"],
},
{ # MP + SP
"forward": ["matmul_v2", "reduce_scatter", "elementwise_add"],
"backward": [
"elementwise_add_grad",
"all_reduce",
"scale",
"all_gather",
"matmul_v2_grad",
"all_gather",
],
},
{ # DP + MP
"forward": ["matmul_v2", "all_reduce", "elementwise_add"],
"backward": [
"elementwise_add_grad",
"all_reduce",
"scale",
"matmul_v2_grad",
],
},
{ # DP + MP + SP
"forward": ["matmul_v2", "reduce_scatter", "elementwise_add"],
"backward": [
"elementwise_add_grad",
"all_reduce",
"scale",
"all_reduce",
"scale",
"all_gather",
"matmul_v2_grad",
"all_gather",
],
},
# amp_level == 'o1'
{
"forward": ["matmul_v2", "all_reduce", "cast", "elementwise_add"],
"backward": ["elementwise_add_grad", "matmul_v2_grad"],
},
{
"forward": ["matmul_v2", "reduce_scatter", "cast", "elementwise_add"],
"backward": [
"elementwise_add_grad",
"all_reduce",
"scale",
"all_gather",
"all_gather",
"matmul_v2_grad",
],
},
{
"forward": ["matmul_v2", "all_reduce", "cast", "elementwise_add"],
"backward": [
"elementwise_add_grad",
"all_reduce",
"scale",
"matmul_v2_grad",
],
},
{
"forward": ["matmul_v2", "reduce_scatter", "cast", "elementwise_add"],
"backward": [
"elementwise_add_grad",
"all_reduce",
"scale",
"all_reduce",
"scale",
"all_gather",
"matmul_v2_grad",
"all_gather",
],
},
]
@register_pass("auto_parallel_fused_linear_promotion")
class FusedLinearPromotionPass(PassBase):
"""
Apply pre-promotion that specialized for fused_linear_pass in tensor parallelism or sequence parallelism in Auto Parallel.
"""
def __init__(self):
super().__init__()
self.set_attr("dist_context", None)
self.set_attr("global_rank", -1)
self.set_attr("enable_sp", False)
self.set_attr("amp_level", "o0")
self.set_attr("params_grads", None)
def _check_self(self):
if self.get_attr("dist_context") is None:
return False
if (not isinstance(self.get_attr("global_rank"), int)) or self.get_attr(
"global_rank"
) < 0:
return False
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, context):
self._dist_context = self.get_attr("dist_context")
self._global_rank = int(self.get_attr("global_rank"))
self._params_grads = self.get_attr("params_grads")
self._amp_level = self.get_attr("amp_level")
self._enable_sp = self.get_attr("enable_sp")
self._is_amp_o1 = self._amp_level == 'o1'
self._source_patterns = {}
self._enable_dp, self._enable_mp = self._is_enable_dp_mp(
self._dist_context
)
pattern_offset = 4 if self._is_amp_o1 else 0
if self._enable_sp:
if self._enable_dp:
self._source_patterns = FUSED_LINEAR_SOURCE_PATTERNS_LIST[
3 + pattern_offset
]
else:
self._source_patterns = FUSED_LINEAR_SOURCE_PATTERNS_LIST[
1 + pattern_offset
]
elif self._enable_mp:
if self._enable_dp:
self._source_patterns = FUSED_LINEAR_SOURCE_PATTERNS_LIST[
2 + pattern_offset
]
else:
self._source_patterns = FUSED_LINEAR_SOURCE_PATTERNS_LIST[
0 + pattern_offset
]
else:
logger.warning("Neither of sp and mp is enabled, skip this pass")
return
dp_group = None
if self._enable_dp:
dp_group = self._collective_data_parallel_groups(
main_program.global_block()
)
# 1. get whether the current rank is first rank in mp
self._is_first_rank = self._is_tp_sp_first_rank(
self._dist_context, self._global_rank
)
logger.debug(f"before main_program: {main_program}")
# 2. get the forward and backward op list indexes in source patterns
(
forward_segments,
backward_segments,
) = self._get_forward_backward_op_segments(main_program)
if len(forward_segments) == 0 or len(backward_segments) == 0:
logger.warning(
"No forward and backward op segments, skip this pass"
)
return
# 3 transform the forward ops
rename_var_names_map, deleted_bias_names = self._transform_forward(
main_program,
forward_segments,
backward_segments,
self._is_first_rank,
self._enable_sp,
self._is_amp_o1,
)
# 4 transform the backward ops
self._transform_backward(
main_program,
backward_segments,
rename_var_names_map,
self._is_first_rank,
self._enable_sp,
)
# 5. transform the optimizer ops
self._transform_opt(
main_program,
deleted_bias_names,
self._params_grads,
self._is_first_rank,
self._is_amp_o1,
)
logger.info(f"deleted_bias_names: {deleted_bias_names}")
logger.debug(f"after main_program: {main_program}")
# 6. transform the startup program
self._transform_startup_program(
startup_program, deleted_bias_names, dp_group, self._is_first_rank
)
def _is_tp_sp_first_rank(self, dist_context, rank):
for process_mesh in dist_context.process_meshes:
inner_mesh_shape = process_mesh.shape
inner_mesh = (np.array(process_mesh.process_ids)).reshape(
inner_mesh_shape
)
if len(inner_mesh_shape) == 1:
return rank == min(process_mesh.process_ids)
elif len(inner_mesh.shape) == 2:
for id0 in range(inner_mesh_shape[0]):
if rank == min(inner_mesh[id0, :]):
return True
elif len(inner_mesh.shape) == 3:
for id0 in range(inner_mesh_shape[0]):
for id1 in range(inner_mesh_shape[1]):
if rank == min(inner_mesh[id0, id1, :]):
return True
else:
raise ValueError("inner mesh shape is not supported")
return False
def _is_enable_dp_mp(self, dist_context):
for process_mesh in dist_context.process_meshes:
inner_mesh_shape = process_mesh.shape
inner_mesh = (np.array(process_mesh.process_ids)).reshape(
inner_mesh_shape
)
if len(inner_mesh_shape) == 1:
return False, inner_mesh_shape[0] > 1
else:
# DP * MP
return inner_mesh_shape[-2] > 1, inner_mesh_shape[-1] > 1
return False, False
def _get_forward_backward_op_segments(self, main_program):
"""
Get the operator segments according to the source patterns.
"""
def can_match_pattern(
ops, start_id, pattern, forward_matmul_inputs, is_backward=False
):
"""
Check whether the ops in the range [start_id, start_id + len(pattern)] can match the pattern.
If the ops is in forward pass, check it directly. However, when the ops is in backward pass,
we need to additionally check whether the input of the last op in pattern is in forward_matmul_inputs to
deal the case of enabling recompute.
"""
new_id = start_id
if not is_backward:
for op_name in pattern:
if ops[new_id].type != op_name:
return False
new_id += 1
forward_matmul_inputs.extend(ops[start_id].input_arg_names)
return True
else:
for op_name in pattern:
if ops[new_id].type != op_name:
return False
new_id += 1
matmul_grad_input_names = ops[new_id - 1].input_arg_names
# for refined-recompute
if (
matmul_grad_input_names[1] not in forward_matmul_inputs
and matmul_grad_input_names[2] not in forward_matmul_inputs
):
return False
return True
global_block = main_program.global_block()
forward_segments = []
backward_segments = []
ops_len = len(global_block.ops)
self._forward_patterns_len = len(self._source_patterns["forward"])
self._backward_patterns_len = len(self._source_patterns["backward"])
forward_matmul_inputs = []
for id, op in enumerate(global_block.ops):
if id > ops_len - self._backward_patterns_len:
break
if int(op.desc.attr('op_role')) == 0 or (
is_recompute_op(op) and not op.type.endswith("_grad")
): # forward
if can_match_pattern(
global_block.ops,
id,
self._source_patterns["forward"],
forward_matmul_inputs,
is_backward=False,
):
forward_segments.append(
[id, id + self._forward_patterns_len]
)
elif int(op.desc.attr('op_role')) == 1: # backward
if can_match_pattern(
global_block.ops,
id,
self._source_patterns["backward"],
forward_matmul_inputs,
is_backward=True,
):
backward_segments.append(
[id, id + self._backward_patterns_len]
)
else:
pass
assert len(forward_segments) >= len(backward_segments), (
"The number of forward segments should be not shorter than the number of backward segments."
)
logger.info(f"forward_segments: {forward_segments}")
logger.info(f"backward_segments: {backward_segments}")
return forward_segments, backward_segments
def _collective_data_parallel_groups(self, main_block):
for op in main_block.ops:
if not is_forward_op(op) or op.type in _skip_ops:
continue
# NOTE: there aren't dist_attr in the ops which reshard insert,
# and should be skip in sharding.
if _is_reshard_op(op):
continue
group = _inference_data_parallel_group_for_operator(
self._global_rank, op, self._dist_context
)
if group is not None:
return group
return None
def _transform_forward(
self,
main_program,
forward_segments,
backward_segments,
is_first_rank,
is_sp,
is_amp_o1,
):
"""
Transform the forward pass.
"""
def _transform_forward_segment(
global_block,
forward_segment,
backward_segments,
is_first_rank,
is_sp,
is_amp_o1,
):
"""
Transform one forward segment.
"""
# 1. prepare the forward_segment
# 1.1 check whether the forward_segment is right
origin_matmul_op = global_block.ops[forward_segment[0]]
origin_comm_op = global_block.ops[forward_segment[0] + 1]
origin_add_op = global_block.ops[forward_segment[1] - 1]
origin_cast_op = (
global_block.ops[forward_segment[1] - 2] if is_amp_o1 else None
)
origin_matmul_output_name = origin_matmul_op.output_arg_names[0]
origin_comm_input_name = origin_comm_op.input_arg_names[0]
assert origin_matmul_output_name == origin_comm_input_name, (
f"The 0th op output name {origin_matmul_output_name} is not equal to the 1st op input name {origin_comm_input_name}"
)
origin_comm_output_name = origin_comm_op.output_arg_names[0]
origin_add_input_names = origin_add_op.input_arg_names
assert origin_comm_output_name == origin_add_input_names[0], (
f"The 1st op output name {origin_comm_output_name} is not equal to the 2nd op input name {origin_add_input_names[0]}"
)
# 1.2 get the origin dist_attr
origin_add_dist_attr = (
self._dist_context.get_op_dist_attr_for_program(origin_add_op)
)
assert origin_add_dist_attr is not None, (
f"Origin add op {origin_add_op.type} has no dist attr"
)
ref_mesh = origin_add_dist_attr.process_mesh
in_var_dist_attr = origin_add_dist_attr.get_input_dist_attr(
origin_add_op.input_arg_names[0]
)
ref_mapping = in_var_dist_attr.dims_mapping
# 2. deal matmul_v2 op
origin_matmul_output_new_name = unique_name.generate(
origin_matmul_output_name + "@promote"
)
origin_matmul_output_new_var = global_block.create_var(
name=origin_matmul_output_new_name,
dtype=global_block.var(origin_matmul_output_name).dtype,
shape=global_block.var(origin_matmul_output_name).shape,
persistable=False,
stop_gradient=False,
)
set_var_dist_attr(
self._dist_context,
origin_matmul_output_new_var,
ref_mapping,
ref_mesh,
)
rename_vars_map[origin_matmul_output_name] = (
origin_matmul_output_new_name
)
origin_matmul_op._rename_output(
origin_matmul_output_name, origin_matmul_output_new_name
)
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
origin_matmul_op, ref_mesh, ref_mapping, self._dist_context
)
# 3. deal add op and cast op
if is_first_rank:
# insert the "elementwise_add" op before reduce_sum
new_add_op = global_block._insert_op_without_sync(
forward_segment[0] + 1,
type="nop",
)
new_op_desc = new_add_op.desc
new_op_desc.copy_from(origin_add_op.desc)
# create new var of new_add_op output
origin_add_output_name = origin_add_op.output_arg_names[0]
new_add_op_output_name = unique_name.generate(
origin_add_output_name + "@promote"
)
new_shape_var_name = (
origin_add_output_name
if not is_sp
else origin_matmul_output_name
)
global_block.create_var(
name=new_add_op_output_name,
dtype=global_block.var(origin_add_output_name).dtype,
shape=global_block.var(new_shape_var_name).shape,
persistable=False,
stop_gradient=False,
)
global_block._remove_var(
origin_matmul_output_name
) # We can remove the origin_matmul_output now.
global_block._remove_var(origin_add_output_name)
new_add_op._rename_output(
origin_add_output_name, new_add_op_output_name
)
rename_vars_map[origin_add_op.input_arg_names[0]] = (
origin_matmul_output_new_name
)
new_add_op._rename_input(
origin_add_op.input_arg_names[0],
origin_matmul_output_new_name,
)
# deal dist_attr
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
new_add_op, ref_mesh, ref_mapping, self._dist_context
)
# 'cast' op also need to adjust
if is_amp_o1:
new_cast_op = global_block._insert_op_without_sync(
forward_segment[0] + 1,
type="nop",
)
new_op_desc = new_cast_op.desc
new_op_desc.copy_from(origin_cast_op.desc)
if (
new_cast_op.input_arg_names[0]
not in delete_bias_vars_name
): # fp16 = cast(fp32)
delete_bias_vars_name.append(
new_cast_op.input_arg_names[0]
)
else:
if (
new_add_op.input_arg_names[1]
not in delete_bias_vars_name
):
delete_bias_vars_name.append(
new_add_op.input_arg_names[1]
)
else:
# We can remove the origin_matmul_output now.
origin_add_output_name = origin_add_op.output_arg_names[0]
global_block._remove_var(origin_add_output_name)
global_block._remove_var(origin_matmul_output_name)
# 4. deal comm op
# The input of all_reduce_sum only be used once, so we don't need add it in the rename_vars_map
if is_first_rank:
origin_comm_op._rename_input(
origin_comm_op.input_arg_names[0],
new_add_op.output_arg_names[0],
)
else:
origin_comm_op._rename_input(
origin_comm_op.input_arg_names[0],
origin_matmul_output_new_name,
)
if (
origin_comm_op.type == "all_reduce"
and origin_comm_op.attr("reduce_type")
== paddle.distributed.ReduceOp.SUM
):
new_comm_var_name = origin_comm_op.input_arg_names[0]
else:
new_comm_var_name = unique_name.generate(
origin_comm_output_name + "@promote"
)
global_block.create_var(
name=new_comm_var_name,
dtype=global_block.var(origin_comm_output_name).dtype,
shape=global_block.var(origin_comm_output_name).shape,
persistable=False,
stop_gradient=False,
)
rename_vars_map[origin_comm_output_name] = new_comm_var_name
if global_block.has_var(origin_comm_output_name):
global_block._remove_var(origin_comm_output_name)
rename_vars_map[origin_add_output_name] = (
new_comm_var_name # the output of comm op inplace the output of add op for next ops
)
origin_comm_op._rename_output(
origin_comm_output_name, new_comm_var_name
)
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
origin_comm_op, ref_mesh, ref_mapping, self._dist_context
)
# 5. remove elementwise_add op and cast op
if is_first_rank:
if is_amp_o1:
global_block._remove_op(forward_segment[0] + 5)
global_block._remove_op(forward_segment[0] + 4)
else:
global_block._remove_op(forward_segment[0] + 3)
else:
global_block._remove_op(
forward_segment[1] - 1
) # remove elementwise_add op
if is_amp_o1:
if (
origin_cast_op.input_arg_names[0]
not in delete_bias_vars_name
):
delete_bias_vars_name.append(
origin_cast_op.input_arg_names[0]
)
global_block._remove_var(origin_cast_op.output_arg_names[0])
global_block._remove_op(
forward_segment[1] - 2
) # remove cast op
else:
if origin_add_input_names[1] not in delete_bias_vars_name:
delete_bias_vars_name.append(origin_add_input_names[1])
# update backward forward_segment
for back_seg in reversed(backward_segments):
if is_amp_o1:
if back_seg[0] > forward_segment[0]:
back_seg[0] -= 2
back_seg[1] -= 2
else:
break
else:
if back_seg[0] > forward_segment[0]:
back_seg[0] -= 1
back_seg[1] -= 1
else:
break
global_block = main_program.global_block()
rename_vars_map = {} # origin_name -> new_name
delete_bias_vars_name = []
for segment in reversed(forward_segments):
_transform_forward_segment(
global_block,
segment,
backward_segments,
is_first_rank,
is_sp,
is_amp_o1,
)
global_block._sync_with_cpp()
return rename_vars_map, delete_bias_vars_name
def _transform_backward(
self,
main_program,
backward_segments,
rename_var_names_map,
is_first_rank,
is_sp,
):
global_block = main_program.global_block()
to_delete_grad_of_param = []
if is_first_rank:
if is_sp:
# place the comm_op(all_gather) before the elementwise_add_grad
for segment in reversed(backward_segments):
add_grad_op = global_block.ops[segment[0]]
matmul_grad_op = global_block.ops[segment[-1] - 1]
origin_comm_op_id = segment[-1] - 2
origin_comm_op = global_block.ops[origin_comm_op_id]
new_comm_op = global_block._insert_op(
segment[0],
type="nop",
)
new_comm_op.desc.copy_from(origin_comm_op.desc)
# rename input and output
new_comm_op._rename_input(
origin_comm_op.input_arg_names[0],
add_grad_op.input_arg_names[0],
)
add_grad_op._rename_input(
add_grad_op.input_arg_names[0],
new_comm_op.output_arg_names[0],
)
matmul_grad_op._rename_input(
matmul_grad_op.input_arg_names[0],
add_grad_op.output_arg_names[0],
)
global_block._remove_op(segment[-1] - 1)
if self._enable_dp:
global_block._remove_op(segment[0] + 5) # scale
global_block._remove_op(
segment[0] + 4
) # all_reduce_sum
else:
global_block._remove_op(segment[0] + 3) # scale
global_block._remove_op(
segment[0] + 2
) # all_reduce_sum
global_block._sync_with_cpp()
else: # not is_first_rank_in tp or sp
# need to delete the grad op associated with the deleted bias var
if not is_sp:
for segment in reversed(backward_segments):
add_grad_op = global_block.ops[segment[0]]
rename_var_names_map[add_grad_op.output_arg_names[0]] = (
add_grad_op.input_arg_names[0]
)
global_block._remove_var(add_grad_op.output_arg_names[0])
to_delete_grad_of_param.append(
add_grad_op.output_arg_names[1]
)
if self._enable_dp:
global_block._remove_op(segment[0] + 2) # scale op
global_block._remove_op(
segment[0] + 1
) # all_reduce_sum op
global_block._remove_op(segment[0])
global_block._sync_with_cpp()
else:
for segment in reversed(backward_segments):
add_grad_op = global_block.ops[segment[0]]
origin_comm_op = global_block.ops[segment[-1] - 2]
rename_var_names_map[add_grad_op.output_arg_names[0]] = (
add_grad_op.input_arg_names[0]
)
origin_comm_op._rename_input(
origin_comm_op.input_arg_names[0],
add_grad_op.input_arg_names[0],
)
global_block._remove_var(add_grad_op.output_arg_names[0])
to_delete_grad_of_param.append(
add_grad_op.output_arg_names[1]
)
if self._enable_dp: # DP
global_block._remove_op(
segment[0] + 4
) # scale op for dp
global_block._remove_op(
segment[0] + 3
) # all_reduce_sum op for dp
global_block._remove_op(segment[0] + 2) # scale op for sp
global_block._remove_op(
segment[0] + 1
) # all_reduce_sum op for sp
global_block._remove_op(
segment[0]
) # elementwise_add_grad op
global_block._sync_with_cpp()
# rename input vars in global_block
for op in global_block.ops:
if is_optimize_op(op):
continue
for var_name in op.input_arg_names:
if var_name in rename_var_names_map:
op._rename_input(var_name, rename_var_names_map[var_name])
if self._is_amp_o1:
for var_name in to_delete_grad_of_param:
global_block._remove_var(var_name)
global_block._sync_with_cpp()
def _transform_opt(
self,
main_program,
deleted_bias_names,
params_grads,
is_first_rank,
is_amp_o1,
):
if is_first_rank:
return
deleted_bias_grads_names = []
to_delete_params_grads = []
for id, (param, grad) in enumerate(params_grads):
if param.name in deleted_bias_names:
deleted_bias_grads_names.append(grad.name)
to_delete_params_grads.append(id)
to_delete_op_ids = []
for id in reversed(range(len(main_program.global_block().ops))):
global_block = main_program.global_block()
op = global_block.ops[id]
op_input_names = op.input_arg_names
for op_input in op_input_names:
if op_input in deleted_bias_grads_names:
if op.type in _supported_optimizer_type:
for output_var in op.output_arg_names:
global_block._remove_var(output_var)
grad_var = op.input('Grad')[0]
global_block._remove_var(grad_var)
to_delete_op_ids.append(id)
if (
op.type == "squared_l2_norm"
or op.type == "clip_by_norm"
):
output_var_name = op.output_arg_names[0]
global_block._remove_var(output_var_name)
to_delete_op_ids.append(id)
for intra_id in range(id + 1, len(global_block.ops)):
intra_op = global_block.ops[intra_id]
if (
output_var_name in intra_op.input_arg_names
and intra_op.type == "stack"
):
origin_vars = intra_op.input("X")
origin_vars.remove(output_var_name)
intra_op.desc.set_input("X", origin_vars)
break
if op.type == "elementwise_mul":
to_delete_op_ids.append(id)
# check_finite_and_unscale and update_loss_scaling
if (
op.type == "check_finite_and_unscale"
or op.type == "update_loss_scaling"
):
origin_vars = op.input("X")
origin_vars.remove(op_input)
op.desc.set_input("X", origin_vars)
origin_vars = op.output("Out")
origin_vars.remove(op_input)
op.desc.set_output("Out", origin_vars)
if is_amp_o1:
for output_name in op.output_arg_names:
if (
output_name in deleted_bias_grads_names
and op.type == 'cast'
):
to_delete_op_ids.append(id)
for id in to_delete_op_ids:
global_block._remove_op(id)
main_program.global_block()._sync_with_cpp()
for id in reversed(to_delete_params_grads):
del params_grads[id]
return
def _transform_startup_program(
self, startup_program, deleted_bias_names, dp_group, is_first_rank
):
"""
Delete the vars and ops associated with deleted_bias_names in startup program.
"""
logger.debug(f"Before transform startup_program: {startup_program}")
cur_glock = startup_program.global_block()
to_delete_op_ids = []
# for variables associated with deleted_bias_names in amp-o2, such as 'opt_linear_1.b_0_fp32_master_0'
to_delete_extra_vars = []
for id, op in enumerate(cur_glock.ops):
if not is_first_rank:
output_var = op.output_arg_names[0]
if output_var in deleted_bias_names:
to_delete_op_ids.append(id)
else:
for var_name in deleted_bias_names:
if var_name in output_var:
to_delete_op_ids.append(id)
if output_var not in to_delete_extra_vars:
to_delete_extra_vars.append(output_var)
else:
if op.type == "broadcast":
input_vars = op.input_arg_names
if (
input_vars[0] in deleted_bias_names
and id not in to_delete_op_ids
):
if dp_group is None or (
dp_group is not None
and op.attr("ring_id") != dp_group.id
):
to_delete_op_ids.append(id)
for to_delete_id in reversed(to_delete_op_ids):
cur_glock._remove_op(to_delete_id)
if not is_first_rank:
for var_name in deleted_bias_names:
cur_glock._remove_var(var_name)
for var_name in to_delete_extra_vars:
if cur_glock.has_var(var_name):
cur_glock._remove_var(var_name)
cur_glock._sync_with_cpp()
logger.debug(f"After transform startup_program: {startup_program}")
@@ -0,0 +1,538 @@
# 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 numpy as np
import paddle
import paddle.distributed as dist
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
from ..auto_parallel.process_mesh import ProcessMesh
from ..auto_parallel.static.dist_attribute import (
OperatorDistAttr,
TensorDistAttr,
)
from ..auto_parallel.static.operators.common import (
SyncMode,
is_data_parallel_reduce_op,
)
from ..auto_parallel.static.process_group import (
get_all_process_groups,
get_world_process_group,
)
from ..auto_parallel.static.reshard import Resharder
from ..auto_parallel.static.utils import (
_get_comm_group,
insert_dependencies_for_vars,
is_gradient_clip_op,
is_optimize_op,
is_reshard_op,
)
from .auto_parallel_sharding import ShardingPass
from .pass_base import PassBase, register_pass
def _get_params_grads(block):
params_grads = []
for op in reversed(block.ops):
if not is_optimize_op(op):
break
if "Param" in op.input_names and "Grad" in op.input_names:
param_name = op.input("Param")[0]
grad_name = op.input("Grad")[0]
param = block.var(param_name)
grad = block.var(grad_name)
params_grads.append((param, grad))
return params_grads
def _get_dpmp_topology(origin_topology, sharding_group):
"""
Get dpmp topology from origin_topology
Example:
the parallel strategy: dp4-mp2-sharding2
the complete process_mesh:
topology: [4, 2]
processes: [0, 1, 2, 3, 4, 5, 6, 7]
the dpmp topology: [2, 2]
the sharding axis: 1
"""
sharding_axis = 1
dp_sharding_topology = [
origin_topology[0] // sharding_group.nranks,
sharding_group.nranks,
]
if dp_sharding_topology[0] == 1:
sharding_axis = 0
dp_sharding_topology = dp_sharding_topology[1:]
product_dp_sharding = reduce(lambda x, y: x * y, dp_sharding_topology, 1)
product_topology = reduce(lambda x, y: x * y, origin_topology, 1)
if product_topology == product_dp_sharding:
dpmp_topology = dp_sharding_topology
else:
assert product_topology % product_dp_sharding == 0
mp_degree = product_topology // product_dp_sharding
dpmp_topology = [*dp_sharding_topology, mp_degree]
return dpmp_topology, sharding_axis
def _get_dpmp_process_mesh(rank_id, topology, processes, sharding_group):
"""
Get dpmp process_mesh from the complete process_mesh which apply sharding.
Example:
the parallel strategy: dp4-mp2-sharding2
the complete process_mesh:
topology: [4, 2]
processes: [0, 1, 2, 3, 4, 5, 6, 7]
the dpmp process_mesh is:
1) topology: [2, 2], processes: [0, 1, 4, 5]
2) topology: [2, 2], processes: [2, 3, 6, 7]
"""
if sharding_group is None:
return topology, processes
# get dpmp_topology
dpmp_topology, sharding_axis = _get_dpmp_topology(topology, sharding_group)
# get all sharding_groups of ranks
sharding_groups = []
for rank in processes:
group = _get_comm_group(processes, dpmp_topology, sharding_axis, rank)
if group not in sharding_groups:
sharding_groups.append(group)
# get dpmp_processes
sharding_groups = np.array(sharding_groups)
dpmp_processes_in_sharding = None
for i in range(sharding_groups.shape[-1]):
if rank_id in sharding_groups[:, i]:
dpmp_processes_in_sharding = sharding_groups[:, i]
assert dpmp_processes_in_sharding is not None
return dpmp_topology, list(dpmp_processes_in_sharding)
def _is_about_global_norm(
rank_id, tensor_shape, topology, processes, dims_mapping, sharding_group
):
# get current process_mesh where the parameter exist.
dpmp_topology, dpmp_processes = _get_dpmp_process_mesh(
rank_id, topology, processes, sharding_group
)
complete_shape = Resharder.compute_complete_shape(
tensor_shape, dpmp_topology, dims_mapping
)
complete_partitions = []
complete_param_ranks = []
for process in dpmp_processes:
partition_index = Resharder.compute_partition_index(
process, complete_shape, dims_mapping, dpmp_topology, dpmp_processes
)
if partition_index not in complete_partitions:
complete_partitions.append(partition_index)
complete_param_ranks.append(process)
return rank_id in complete_param_ranks
class ClipHelper:
def __init__(
self, params_grads, rank_id, block, dist_context, pass_context
):
params, _ = zip(*params_grads)
self.params = list(params)
self.params_name = [p.name for p in self.params]
self.rank_id = rank_id
self.block = block
self.dist_context = dist_context
self.pass_context = pass_context
self.sharding_group = None
self.world_ranks = get_world_process_group().ranks
if hasattr(dist_context, '_sharding_group'):
self.sharding_group = dist_context._sharding_group
self.world_nranks = len(self.world_ranks)
self.pure_data_parallel = self._is_pure_data_parallel()
self.rank_to_params = self._partition_parameters(params)
def is_calculate_norm(self, name):
"""
whether the param_name@GRAD participate in the calculation of global_norm
"""
if not self.is_local_param(name):
return False
param = self.params[self.params_name.index(name)]
if not self.pure_data_parallel:
dist_attr = self._get_dist_attr(name)
topology = dist_attr.process_mesh.shape
processes = dist_attr.process_mesh.process_ids
dims_mapping = dist_attr.dims_mapping
return _is_about_global_norm(
self.rank_id,
param.shape,
topology,
processes,
dims_mapping,
self.sharding_group,
)
else:
return param.name in self.rank_to_params[self.rank_id]
def is_local_param(self, name):
"""
whether the param_name is updated with opt in cur_rank
"""
if name not in self.params_name:
return False
return True
def _get_dist_attr(self, name):
var = self.block.vars[name]
return self.dist_context.get_tensor_dist_attr_for_program(var)
def is_local_var_with_dist_attr(self, name):
"""
whether the var_name is belong to cur_rank
"""
dist_attr = self._get_dist_attr(name)
assert dist_attr is not None
return self.rank_id in dist_attr.process_mesh.process_ids
def _init_dist_attr(self, op):
op_dist_attr = OperatorDistAttr()
op_dist_attr.process_mesh = ProcessMesh(self.world_ranks)
for in_name in op.input_arg_names:
in_var = self.block.vars[in_name]
in_dist_attr = TensorDistAttr()
in_dist_attr.process_mesh = ProcessMesh(self.world_ranks)
in_dist_attr.dims_mapping = [-1 for i in in_var.shape]
self.dist_context.set_tensor_dist_attr_for_program(
in_var, in_dist_attr
)
op_dist_attr.set_input_dist_attr(in_name, in_dist_attr)
for out_name in op.output_arg_names:
out_var = self.block.vars[out_name]
out_dist_attr = TensorDistAttr()
out_dist_attr.process_mesh = ProcessMesh(self.world_ranks)
out_dist_attr.dims_mapping = [-1 for i in out_var.shape]
self.dist_context.set_tensor_dist_attr_for_program(
out_var, out_dist_attr
)
op_dist_attr.set_output_dist_attr(out_name, out_dist_attr)
self.dist_context.set_op_dist_attr_for_program(op, op_dist_attr)
def _is_pure_data_parallel(self):
for applied_pass in self.pass_context.passes:
if isinstance(applied_pass, ShardingPass):
return False
groups = get_all_process_groups()
for g in groups:
if g.nranks != self.world_nranks:
return False
for op in self.block.ops:
if (
(
op.type == "reduce"
and op.desc.attr("reduce_type") == dist.ReduceOp.SUM
)
or (
op.type == "all_reduce"
and op.desc.attr("reduce_type") == dist.ReduceOp.SUM
)
and not is_data_parallel_reduce_op(op)
):
return False
if op.type in ["send_v2", "recv_v2"]:
return False
return True
def _partition_parameters(self, params):
"""
build rank_id_to_params by the param's numel
to guarantee params in every rank of dp_group as even as possible.
"""
mapping = {}
if not self.pure_data_parallel:
for rank_ in range(self.world_nranks):
mapping[rank_] = [p.name for p in params]
else:
for rank_ in range(self.world_nranks):
mapping[rank_] = []
sizes = [0] * self.world_nranks
for param in params:
rank = sizes.index(min(sizes))
mapping[rank].append(param.name)
numel = reduce(lambda x, y: x * y, param.shape, 1)
assert numel > 0, (
f"param [{param.name}] should larger than 0, but it is [{numel}]"
)
sizes[rank] += numel
return mapping
@register_pass("auto_parallel_grad_clip")
class ClipGradByGlobalNormPass(PassBase):
"""
1. Remove norm-compute op and grad-scale op when the grad is not in current rank
or is independent of the calculation of norm.
2. Each rank computes its own norm value, then gets global_norm by allreduce_sum only once.
"""
def __init__(self):
super().__init__()
self.set_attr("rank_id", None)
self.set_attr("dist_context", None)
self.set_attr("params_grads", None)
def _check_self(self):
if self.get_attr("dist_context") is None:
return False
dist_context = self.get_attr("dist_context")
if dist_context._serial_optimizer._grad_clip is None:
return False
if self.get_attr("params_grads") is None:
return False
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, context):
dist_context = self.get_attr("dist_context", None)
rank_id = self.get_attr("rank_id", None)
block = main_program.global_block()
dist_params_grads = self.get_attr("params_grads", None)
# dist_params_grads = _get_params_grads(block)
self.clip_helper = ClipHelper(
dist_params_grads, rank_id, block, dist_context, context
)
self._remove_no_need_ops_vars(block)
def _remove_no_need_ops_vars(self, block):
removed_op_out_type = [
'squared_l2_norm',
'square',
'reduce_sum',
]
removed_op_idx = set()
removed_tmp_var = set()
for idx, op in enumerate(block.ops):
if not is_gradient_clip_op(op):
continue
if op.type == 'clip_by_norm':
# remove 'clip_by_norm' op if the param is not updated with opt in current rank
input_name = op.input("X")[0]
if input_name.find("@GRAD") != -1:
param_name = input_name[: input_name.find("@GRAD")]
is_local = self.clip_helper.is_local_param(param_name)
if not is_local:
removed_op_idx.add(idx)
removed_tmp_var.update(set(op.output_arg_names))
elif op.type in removed_op_out_type:
input_name = op.input("X")[0]
if input_name.find("@GRAD") != -1:
# remove 'squared_l2_norm' and 'square' ops,
# if the param@GRAD in cur_rank does not participate in the calculation of global_norm
param_name = input_name[: input_name.find("@GRAD")]
is_local = self.clip_helper.is_local_param(param_name)
is_calculate = self.clip_helper.is_calculate_norm(
param_name
)
if not is_local or not is_calculate:
removed_op_idx.add(idx)
removed_tmp_var.update(set(op.output_arg_names))
else:
# 'reduce_sum' must be behind 'square'
if idx - 1 in removed_op_idx:
removed_op_idx.add(idx)
removed_tmp_var.update(set(op.output_arg_names))
elif op.type == 'elementwise_mul':
# 'elementwise_mul' scale the param@GRAD with global_norm
# remove 'elementwise_mul' op if the param is not updated with opt in current rank
input_name = op.input("X")[0]
if input_name.find("@GRAD") != -1:
param_name = input_name[: input_name.find("@GRAD")]
is_local = self.clip_helper.is_local_param(param_name)
if not is_local:
removed_op_idx.add(idx)
if block.ops[idx - 1].type == 'cast':
removed_op_idx.add(idx - 1)
removed_tmp_var.update(
set(block.ops[idx - 1].output_arg_names)
)
elif op.type == 'sum':
# 'sum' op is used to calculate global_norm, and need to filter inputs which is not in cur_rank
reserved_vars = []
for input_name in op.input_arg_names:
if (
input_name not in removed_tmp_var
and self.clip_helper.is_local_var_with_dist_attr(
input_name
)
):
reserved_vars.append(input_name)
if not reserved_vars:
removed_op_idx.add(idx)
removed_tmp_var.update(set(op.output_arg_names))
if block.ops[idx + 1].type == 'cast':
removed_op_idx.add(idx + 1)
removed_tmp_var.update(
set(block.ops[idx + 1].output_arg_names)
)
else:
op.desc.set_input("X", reserved_vars)
elif op.type == 'stack':
# 'stack' op is also used to calculate global_norm ('stack' + 'reduce_sum'), and need to filter inputs which is not in cur_rank
reserved_vars = []
for input_name in op.input_arg_names:
if (
input_name not in removed_tmp_var
and self.clip_helper.is_local_var_with_dist_attr(
input_name
)
):
reserved_vars.append(input_name)
if not reserved_vars:
removed_op_idx.add(idx)
removed_tmp_var.update(set(op.output_arg_names))
if block.ops[idx + 1].type == 'reduce_sum':
removed_op_idx.add(idx + 1)
removed_tmp_var.update(
set(block.ops[idx + 1].output_arg_names)
)
if block.ops[idx + 2].type == 'cast':
removed_op_idx.add(idx + 2)
removed_tmp_var.update(
set(block.ops[idx + 2].output_arg_names)
)
else:
op.desc.set_input("X", reserved_vars)
for idx, op in reversed(list(enumerate(block.ops))):
if not (is_optimize_op(op) or is_reshard_op(op)):
break
if not is_gradient_clip_op(op):
continue
if idx in removed_op_idx:
block._remove_op(idx, sync=False)
for idx, op in reversed(list(enumerate(block.ops))):
if not (is_optimize_op(op) or is_reshard_op(op)):
break
if not is_gradient_clip_op(op):
continue
if op.type == 'sqrt':
input_name = op.input("X")[0]
input_var = block.vars[input_name]
insert_leaf_fill_constant_node = False
if paddle.distributed.get_world_size() > 1:
offset = 0
if input_name in removed_tmp_var:
removed_tmp_var.remove(input_name)
fill_constant_op = block._insert_op(
idx,
type='fill_constant',
inputs={},
outputs={'Out': [input_var]},
attrs={
'shape': [],
'dtype': input_var.dtype,
'value': 0,
'force_cpu': False,
OP_ROLE_KEY: OpRole.Optimize,
},
)
fill_constant_op._set_attr(
'op_namescope', "/gradient_clip_pass"
)
offset += 1
self.clip_helper._init_dist_attr(fill_constant_op)
insert_leaf_fill_constant_node = True
allreduce_op = block._insert_op(
idx + offset,
type='all_reduce',
inputs={'x': [input_var]},
outputs={'out': [input_var]},
attrs={
'ring_id': 0,
'reduce_type': paddle.distributed.ReduceOp.SUM,
OP_ROLE_KEY: OpRole.Optimize,
},
)
# TODO better regular the usage of op namescope
allreduce_op._set_attr(
'op_namescope', '/' + SyncMode.GlobalNormSync
)
self.clip_helper._init_dist_attr(allreduce_op)
if insert_leaf_fill_constant_node:
# NOTE add naive deps for global norm sync in graph exe
j = idx - 1
prior_op = None
while j > 0:
op_type = block.ops[j].type
if op_type in [
'update_loss_scaling',
'check_finite_and_unscale',
] or op_type.endswith("_grad"):
prior_op = block.ops[j]
break
j -= 1
assert prior_op is not None, (
"Unexpected: ClipByGlobalNorm could not find priory depend op"
)
prior_var = block.vars[prior_op.output_arg_names[0]]
assert prior_var is not None, (
"Unexpected: ClipByGlobalNorm could not find priory depend var"
)
insert_dependencies_for_vars(
block,
idx,
prior_var,
input_var,
self.clip_helper.dist_context,
OpRole.Optimize,
process_mesh=[
-1
], # hack to avoid initialize the dist attr for coalesce var
is_recompute=False,
sync=False,
op_namescope="grad_clip_fill_constant_dep",
)
for varname in removed_tmp_var:
block._remove_var(varname, sync=False)
block._sync_with_cpp()
@@ -0,0 +1,346 @@
# 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 paddle
from paddle.distributed.auto_parallel.static.process_group import (
get_world_process_group,
)
from paddle.distributed.fleet.meta_optimizers.common import (
OpRole,
)
from paddle.framework import (
_current_expected_place_ as _get_device,
)
from .pass_base import PassBase, PassType, register_pass
world_process_group = get_world_process_group()
def _move_used_grad_op(used_grad_op, grad):
move_to_opt_block_flag = True
move_to_opt_ops = []
cannot_move_op = ["pd_op.send_v2", "pd_op.send"]
def find_move_op(backward_op):
nonlocal move_to_opt_block_flag
if not move_to_opt_block_flag or backward_op in move_to_opt_ops:
return
if backward_op.name() in cannot_move_op:
move_to_opt_block_flag = False
return
if backward_op.num_operands() == 1:
move_to_opt_block_flag = True
move_to_opt_ops.append(backward_op)
elif backward_op.name() == "pd_op.slice":
move_to_opt_ops.append(backward_op)
for i in range(0, backward_op.num_operands()):
if not grad.is_same(backward_op.operand_source(i)):
move_to_opt_ops.append(
backward_op.operand_source(i).get_defining_op()
)
move_to_opt_block_flag = True
else:
# NOTE(zhangwl):temp only consider one operand op
move_to_opt_block_flag = False
return
for op_result in backward_op.results():
for next_op in op_result.all_used_ops():
if next_op.op_role != int(OpRole.Optimize):
find_move_op(next_op)
find_move_op(used_grad_op)
if move_to_opt_block_flag:
for move_op in move_to_opt_ops:
move_op.op_role = int(OpRole.Optimize)
def _pir_append_gradient_merge_backward_op(
main_program,
startup_program,
params_grads,
):
main_block = main_program.global_block()
startup_block = startup_program.global_block()
# {param: gradient_merge_var} to insert scale op and fill_constant op
new_params_grads = []
place = _get_device()
if isinstance(place, paddle.framework.CUDAPlace):
place = paddle.framework.CUDAPlace(
paddle.distributed.ParallelEnv().dev_id
)
cur_place = paddle.base.libpaddle.Place()
cur_place.set_place(place)
for param, grad in params_grads:
if grad is None:
continue
assert not param.is_selected_row_type(), (
"SELECTED_ROWS is not supported in GradientMergeOptimizer for now"
)
grad_dtype = grad.dtype
grad_type = grad.type()
for op in grad.all_used_ops():
if op.has_attr("master_grad_cast"):
grad_dtype = op.result(0).dtype
grad_type = op.result(0).type()
# step1: create gradient_merge var and init with 0
# Add persistable gradient variables in startup_program
paddle.pir.set_insertion_point_to_block_end(startup_block)
gradient_merge_var = paddle.full(
shape=grad._local_shape, fill_value=0.0, dtype=grad_dtype
)
gradient_merge_var.persistable = True
paddle.pir.set_insertion_point_after(
gradient_merge_var.get_defining_op()
)
paddle._C_ops.set_persistable_value(
gradient_merge_var, param.name + "@GRAD@MERGE"
)
# step2: Accumulate persistable gradient variables in main_program
# NOTE(zhaoyingli): inplace operation must be 'a = a + b', cannot be 'a = b + a'
grad_defining_op = grad.get_defining_op()
paddle.pir.set_insertion_point_after(grad_defining_op)
new_gradient_merge_var = main_block.add_kwarg(
param.name + "@GRAD@MERGE", grad_type
)
new_gradient_merge_var.persistable = True
new_gradient_merge_var.place_attr = cur_place
new_gradient_merge_var_add = paddle._C_ops.add_(
new_gradient_merge_var, grad
)
new_gradient_merge_var_add_op = (
new_gradient_merge_var_add.get_defining_op()
)
new_gradient_merge_var_add_op.op_role = grad_defining_op.op_role
new_gradient_merge_var_add_op.dist_attr = (
paddle.base.libpaddle.pir.create_op_dist_attribute(
grad_defining_op.dist_attr.process_mesh,
grad_defining_op.dist_attr.operands(),
grad_defining_op.dist_attr.results(),
grad_defining_op.dist_attr.chunk_id,
)
)
new_gradient_merge_var_add_op.set_bool_attr("grad_merge_add", True)
# NOTE(zhangweilong): grad may in different device in auto_parallel, so need consider all_gather/all_reduce/split/... op
for used_grad_op in grad.all_used_ops():
_move_used_grad_op(used_grad_op, grad)
opt_ops_use_grad = [
op
for op in grad.all_used_ops()
if op.op_role == int(OpRole.Optimize)
]
grad.replace_grad_users_with(
new_gradient_merge_var, set(opt_ops_use_grad)
)
# reset gradient merge var to zero after finishing optimization
paddle.pir.set_insertion_point_to_block_end(main_block)
set_value = paddle.full(
shape=[1], fill_value=float(0), dtype=grad_dtype
)
new_gradient_merge_var_zero = paddle._C_ops.set_value_with_tensor_(
new_gradient_merge_var, set_value, [], [], [], [], [], []
)
set_value_op = new_gradient_merge_var_zero.get_defining_op()
set_value_op.op_role = int(OpRole.Optimize)
for id in range(1, set_value_op.num_operands()):
op_input = set_value_op.operand_source(id)
op_input.get_defining_op().op_role = int(OpRole.Optimize)
# step3: Construct new_params_grads and grad_to_gradient_merge
new_params_grads.append((param, new_gradient_merge_var))
return new_params_grads
def _pir_move_reduce_to_backward_stage(main_program):
pass
def _pir_remove_cast_for_master_grad(main_program, params_grads):
for op in main_program.global_block().ops:
if op.has_attr("master_grad_cast"):
op.result(0).replace_all_uses_with(op.operand_source(0))
op.erase()
def _find_trivial_optimizer_ops(block):
optimizer_ops = []
for op in block.ops:
if "adam" in op.name() or "sgd" in op.name():
optimizer_ops.append(op)
return optimizer_ops
def _get_prev_op(block, optimizer_op):
found = False
for op in reversed(block.ops):
if found:
return op
if op.id == optimizer_op.id:
found = True
return None
def _insert_scale_op_after(target_value, optimizer_op, scale, bias=0.0):
scaled_grad = paddle._C_ops.scale_(target_value, scale, bias, False)
scale_op = scaled_grad.get_defining_op()
scale_op.op_role = int(OpRole.Optimize)
full_op = scale_op.operand_source(1).get_defining_op()
assert full_op.name() == "pd_op.full", (
f"The defining op of the scale value should be `pd_op.full`, but got {full_op.name()}"
)
full_op.op_role = int(OpRole.Optimize)
if "adam" in optimizer_op.name():
optimizer_op.operand(1).set_source(scaled_grad)
elif "sgd" in optimizer_op.name():
optimizer_op.operand(2).set_source(scaled_grad)
def _append_scale_op_before_comm(block, new_params_to_grads, k_steps):
for op in reversed(block.ops):
if op.op_role == int(OpRole.Backward):
paddle.pir.set_insertion_point_after(op)
break
for _, new_grad in new_params_to_grads:
new_grad = paddle._C_ops.scale_(new_grad, 1.0 / k_steps, 0.0, False)
scale_op = new_grad.get_defining_op()
scale_op.op_role = int(OpRole.Optimize)
full_op = scale_op.operand_source(1).get_defining_op()
assert full_op.name() == "pd_op.full", (
f"The defining op of the scale value should be `pd_op.full`, but got {full_op.name()}"
)
full_op.op_role = int(OpRole.Optimize)
paddle.pir.set_insertion_point_to_block_end(block)
def _append_scale_op_after_comm(block, optimizer_ops, k_steps):
for optimizer_op in optimizer_ops:
target_value = None
if "adam" in optimizer_op.name(): # adam and adamw are included
target_value = optimizer_op.operand_source(1)
elif "sgd" in optimizer_op.name():
target_value = optimizer_op.operand_source(2)
else:
raise NotImplementedError(
f"We yet support adamw, adam and sgd, but got {optimizer_op.name()}"
)
assert target_value is not None, (
"target_value is not expected to be None"
)
insertion_point = target_value.get_defining_op()
if insertion_point is None:
# target_value is a gradient_merge_var, which hasn't defining_op
# so we find the prev op of optimizer_op, inserting a scale op behind.
insertion_point = _get_prev_op(block, optimizer_op)
paddle.pir.set_insertion_point_after(insertion_point)
_insert_scale_op_after(target_value, optimizer_op, 1.0 / k_steps)
paddle.pir.set_insertion_point_to_block_end(block)
def _pir_append_scale_op(program, new_params_to_grads, k_steps):
block = program.global_block()
optimizer_ops = _find_trivial_optimizer_ops(block)
if len(optimizer_ops) > 0:
_append_scale_op_after_comm(block, optimizer_ops, k_steps)
else:
_append_scale_op_before_comm(block, new_params_to_grads, k_steps)
def _pir_parse_program(
main_program,
startup_program,
params_grads,
k_steps,
avg,
gradient_sync_after_accumulate,
):
# step1: append gradient merge backward op to main_program
new_params_to_grads = _pir_append_gradient_merge_backward_op(
main_program, startup_program, params_grads
)
# step2: move back reduce op to backward stage
if not gradient_sync_after_accumulate:
_pir_move_reduce_to_backward_stage(main_program, params_grads)
# _pir_remove_cast_for_master_grad(main_program, params_grads)
# step3: append scale op
if avg:
_pir_append_scale_op(main_program, new_params_to_grads, k_steps)
@register_pass("auto_parallel_gradient_merge_pass")
class GradientMergePass(PassBase):
def __init__(self):
super().__init__()
self.set_attr("k_steps", -1)
self.set_attr("avg", True)
self._in_pir_mode = paddle.base.framework.get_flags(
"FLAGS_enable_pir_api"
)["FLAGS_enable_pir_api"]
def _check_self(self):
if self.get_attr("k_steps") < 1:
return False
return True
def _check_conflict(self, other_pass):
return True
def _type(self):
return PassType.COMM_OPT
def _apply_single_impl(self, main_program, startup_program, context):
k_steps = self.get_attr("k_steps", -1)
avg = self.get_attr("avg", False)
params_grads = self.get_attr("params_grads")
gradient_sync_after_accumulate = self.get_attr(
"gradient_sync_after_accumulate", False
)
if self._in_pir_mode:
with paddle.static.program_guard(main_program, startup_program):
_pir_parse_program(
main_program,
startup_program,
params_grads,
k_steps,
avg,
gradient_sync_after_accumulate,
)
else:
raise NotImplementedError(
"auto_parallel_gradient_merge_pass() only support PIR now."
)
@@ -0,0 +1,286 @@
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import copy
import logging
from collections import OrderedDict
from typing import TYPE_CHECKING
import paddle
from paddle.distributed.auto_parallel.static.utils import (
is_backward_op,
is_gradient_clip_op,
is_optimize_op,
naive_set_dist_op_attr_for_program_by_mesh_and_mapping,
set_var_dist_attr,
)
from paddle.distributed.fleet.meta_optimizers.common import (
OP_ROLE_KEY,
OpRole,
)
from paddle.framework import core
from paddle.static import program_guard
from ..utils.log_utils import get_logger
from .pass_base import PassBase, register_pass
if TYPE_CHECKING:
from paddle.base import Variable
_supported_optimizer_type = [
"adam",
"adamax",
"adamw",
"decayed_adagrad",
"momentum",
"dgc_momentum",
"lars_momentum",
"merged_momentum",
"lamb",
"sgd",
]
logger = get_logger(logging.INFO, "MasterGradPass")
def _is_master_grad_cast_op(block, op):
op_name = op.type
if op_name != "cast":
return False
input_names = op.input_arg_names
output_names = op.output_arg_names
assert len(input_names) == 1
assert len(output_names) == 1
input_var_name = input_names[0]
return (
"@master_grad_fp16" in input_var_name
or "@master_grad_bf16" in input_var_name
)
def get_output_in_varlist(op, var_names) -> list[str]:
grad_names = []
for output_name in op.output_arg_names:
if output_name in var_names:
grad_names.append(output_name)
return grad_names
@register_pass("auto_parallel_master_grad_pass")
class MasterGradPass(PassBase):
"""
Use the high precision gradient to replace the low precision gradient in optimizer to avoid inf/nan values of low precision.
The high precision gradient 'master grad' will be used by communication operator, `update_loss_scaling`, `GradClip` and `optimizer`.
"""
def __init__(self):
super().__init__()
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, context):
self._completer = self.get_attr("completer")
dist_context = self.get_attr("dist_context")
params_grads = self.get_attr("params_grads")
logger.debug(f"Origin main_program: {main_program}")
self._add_master_grad(main_program, params_grads, dist_context)
self._regenerate_optimizer(
main_program, startup_program, params_grads, dist_context
)
logger.debug(f"After main program: {main_program}")
def _add_cast_op(self, cur_block, grad_names: list[str], dist_context):
grad_first_ids = OrderedDict()
for idx, op in enumerate(cur_block.ops):
if is_optimize_op(op):
break
elif is_backward_op(op):
var_names = get_output_in_varlist(op, grad_names)
for var_name in var_names:
if var_name not in grad_first_ids:
grad_first_ids[var_name] = idx
# Communication operators such as 'allreduce_sum' use input var as output.
else:
pass
# insert cast op
for grad_name, idx in reversed(grad_first_ids.items()):
grad_var = cur_block.var(grad_name)
if (
grad_var.dtype == paddle.float16
or grad_var.dtype == paddle.bfloat16
):
is_fp16 = grad_var.dtype == paddle.float16
producer_op = cur_block.ops[idx]
producer_op_dist_attr = (
dist_context.get_op_dist_attr_for_program(producer_op)
)
assert producer_op_dist_attr is not None, (
f"The op: '{producer_op}' should be distributed"
)
ref_output_dist_attr = (
producer_op_dist_attr.get_output_dist_attr(grad_name)
)
assert ref_output_dist_attr is not None, (
f"The output: '{grad_name}' should be distributed"
)
ref_mesh = ref_output_dist_attr.process_mesh
ref_dims_mapping = ref_output_dist_attr.dims_mapping
ref_chunk_id = producer_op_dist_attr.chunk_id
grad_half_precision_name = (
grad_name + '@master_grad_fp16'
if is_fp16
else grad_name + '@master_grad_bf16'
)
grad_half_precision = cur_block.create_var(
name=grad_half_precision_name,
dtype=grad_var.dtype,
shape=grad_var.shape,
persistable=False,
stop_gradient=False,
)
set_var_dist_attr(
dist_context,
grad_half_precision,
ref_dims_mapping,
ref_mesh,
chunk_id=ref_chunk_id,
)
producer_op_dist_attr = (
dist_context.get_op_dist_attr_for_program(producer_op)
)
origin_out_dims_mapping = (
producer_op_dist_attr.get_output_dims_mapping(grad_name)
)
producer_op._rename_output(grad_name, grad_half_precision.name)
producer_op_dist_attr.set_output_dims_mapping(
grad_half_precision.name, origin_out_dims_mapping
)
grad_var.desc.set_dtype(core.VarDesc.VarType.FP32)
cast_op = cur_block._insert_op_without_sync(
idx + 1,
type="cast",
inputs={"X": grad_half_precision},
outputs={"Out": grad_var},
attrs={
"in_dtype": grad_half_precision.dtype,
"out_dtype": grad_var.dtype,
},
)
cast_op._set_attr(OP_ROLE_KEY, OpRole.Backward)
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
cast_op,
ref_mesh,
ref_dims_mapping,
dist_context,
chunk_id=ref_chunk_id,
)
cur_block._sync_with_cpp()
def _regenerate_optimizer(
self,
main_program,
startup_program,
params_grads: list[tuple[Variable, Variable]],
dist_context,
):
grad_names = [g.name for _, g in params_grads]
# 1. delete the origin optimizer op
# 1.1 delete the var and op associated with the optimizer op in main_program
main_ops = main_program.global_block().ops
main_ops_len = len(main_ops)
first_optimize_idx = main_ops_len
for idx, op in enumerate(main_ops):
# We don't delete the operators for check_nan_inf
if is_optimize_op(op) and is_gradient_clip_op(op):
first_optimize_idx = idx
break
assert first_optimize_idx < main_ops_len, (
"The first optimizer op is not found!"
)
deleted_temp_var_names = []
deleted_persist_var_names = []
reserved_var_names = []
for idx in range(main_ops_len - 1, first_optimize_idx - 1, -1):
op = main_ops[idx]
inout_arg_names = op.input_arg_names + op.output_arg_names
if op.type in _supported_optimizer_type:
param_names = op.input("Param")
skip_update_names = op.input("SkipUpdate")
for reserved_name in param_names + skip_update_names:
if reserved_name not in reserved_var_names:
reserved_var_names.append(reserved_name)
for input_name in inout_arg_names:
if input_name in grad_names:
continue
var = main_program.global_block().var(input_name)
if (
var.persistable
and input_name not in deleted_persist_var_names
):
deleted_persist_var_names.append(input_name)
elif (
not var.persistable
and input_name not in deleted_temp_var_names
):
deleted_temp_var_names.append(input_name)
main_program.global_block()._remove_op(idx)
for var_name in deleted_temp_var_names + deleted_persist_var_names:
if var_name not in reserved_var_names:
main_program.global_block()._remove_var(var_name)
main_program.global_block()._sync_with_cpp()
# 1.2 delete the var and op in startup_program
for reserved_name in reserved_var_names:
if reserved_name in deleted_persist_var_names:
deleted_persist_var_names.remove(reserved_name)
startup_global_block = startup_program.global_block()
for var_name in deleted_persist_var_names:
if startup_global_block.has_var(var_name):
startup_global_block._remove_var(var_name)
for idx, op in reversed(list(enumerate(startup_global_block.ops))):
inout_arg_names = op.input_arg_names + op.output_arg_names
for var_name in inout_arg_names:
if var_name in deleted_persist_var_names:
startup_program.global_block()._remove_op(idx)
break
# 2. re-generate new optimizer op
serial_optimizer = copy.deepcopy(dist_context._serial_optimizer)
serial_optimizer._learning_rate = (
dist_context._serial_optimizer._learning_rate
)
serial_optimizer._sorted = False
with (
program_guard(main_program, startup_program),
main_program.switch_name_generator_guard("opt_"),
):
_ = serial_optimizer.apply_gradients(params_grads)
self._completer.complete_update_annotation(main_program)
def _add_master_grad(self, main_program, params_grads, dist_context):
grad_names = [g.name for _, g in params_grads]
for sub_block in main_program.blocks:
self._add_cast_op(sub_block, grad_names, dist_context)
@@ -0,0 +1,463 @@
# 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 numpy as np
import paddle
from paddle.framework import IrGraph, core
from paddle.static.quantization import (
AddQuantDequantForInferencePass,
AddQuantDequantPassV2,
OutScaleForTrainingPass,
QuantizationTransformPassV2,
quant_config,
)
from ..auto_parallel.static.converter import Converter
from ..auto_parallel.static.dist_attribute import (
OperatorDistAttr,
TensorDistAttr,
)
from .pass_base import PassBase, register_pass
TRANSFORM_PASS_OP_TYPES = list(
quant_config.SUPPORT_WEIGHT_QUANTIZATION_OP_DICT.keys()
)
QUANT_DEQUANT_PASS_OP_TYPES = list(
quant_config.SUPPORT_ACT_QUANTIZATION_OP_DICT.keys()
)
def _node_id(node):
return (node.node.graph_id(), node.node.id())
@register_pass("auto_parallel_quantization")
class QuantizationPass(PassBase):
def __init__(self):
super().__init__()
self.set_attr("dist_context", None)
self.set_attr("params_grads", None)
self.set_attr("mode", "train")
self.set_attr("loss", None)
def _check_self(self):
if self.get_attr("dist_context") is None:
return False
if self.get_attr("params_grads") is None:
return False
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, context):
dist_context = self.get_attr("dist_context")
params_grads = self.get_attr("params_grads")
mode = self.get_attr("mode")
loss = self.get_attr("loss")
# TODO: scope and place will be removed,
# cause params should be initialized by engine module.
scope = paddle.static.global_scope()
place = paddle.framework.CUDAPlace(
paddle.distributed.ParallelEnv().dev_id
)
# 0. record the relation among blocks
parent_idx_dict = {}
for block in main_program.blocks:
parent_idx_dict[block.idx] = block.parent_idx
is_test = True if mode != "train" else False
# 1. Program convert to Graph, and this pass is only for train mode
main_graph = IrGraph(
core.Graph(main_program.desc), for_test=mode != "train"
)
# 2. Prepare inputs
transform_pass_ops = []
quant_dequant_ops = []
quantize_op_types = [
'conv2d',
'depthwise_conv2d',
'mul',
'matmul',
'matmul_v2',
]
for op_type in quantize_op_types:
if op_type in TRANSFORM_PASS_OP_TYPES:
transform_pass_ops.append(op_type)
elif op_type in QUANT_DEQUANT_PASS_OP_TYPES:
quant_dequant_ops.append(op_type)
weight_quantize_type = (
"channel_wise_abs_max"
if self.get_attr('channel_wise_abs_max')
else "abs_max"
)
# 3. Add quant op for ops which have parameters
if len(transform_pass_ops) > 0:
transform_pass = QuantizationTransformPassV2(
scope=scope,
place=place,
weight_bits=self.get_attr('weight_bits'),
activation_bits=self.get_attr('activation_bits'),
skip_pattern=self.get_attr('not_quant_pattern'),
activation_quantize_type="moving_average_abs_max",
quantizable_op_type=transform_pass_ops,
weight_quantize_type=weight_quantize_type,
weight_quantize_func=None,
act_quantize_func=None,
weight_preprocess_func=None,
act_preprocess_func=None,
optimizer_func=None,
executor=None,
is_test=is_test,
)
for sub_graph in main_graph.all_sub_graphs():
transform_pass.apply(sub_graph)
# 4. Add quant op for ops which don't have parameter
if len(quant_dequant_ops) > 0:
quant_dequant_pass = AddQuantDequantPassV2(
scope=scope,
place=place,
quant_bits=self.get_attr('activation_bits'),
skip_pattern=self.get_attr('not_quant_pattern'),
quantizable_op_type=quant_dequant_ops,
is_test=is_test,
)
for sub_graph in main_graph.all_sub_graphs():
quant_dequant_pass.apply(sub_graph)
# 5. Gather quantitative information for the output
out_scale_training_pass = OutScaleForTrainingPass(
scope=scope, place=place, is_test=is_test
)
for sub_graph in main_graph.all_sub_graphs():
out_scale_training_pass.apply(sub_graph)
# 6. When export quant model, traverse to find the output of each op, and insert the quant/dequant op after it.
if mode != "train" and self.get_attr('onnx_format'):
try:
out_scale_infer_pass = AddQuantDequantForInferencePass(
scope=scope,
place=place,
quant_bits=self.get_attr('activation_bits'),
)
# for sub_graph in main_graph.all_sub_graphs():
# out_scale_infer_pass.apply(sub_graph)
except:
logging.warning(
"Unable to convert quant model with onnx_format=True, please update PaddlePaddle >= 2.4.0"
)
# 7. Convert Graph back to Program
quant_program = main_graph.to_program()
quant_program = self.move_persist_var_to_global_block(quant_program)
# 8.1 get new prams_grads from quant_program
new_params_grads = []
for param, grad in params_grads:
if param.name not in quant_program.global_block().vars:
continue
new_param = quant_program.global_block().vars[param.name]
new_grad = quant_program.global_block().vars[grad.name]
new_params_grads.append((new_param, new_grad))
# 8.2 get new loss var
new_loss = None
if loss:
new_loss = quant_program.global_block().vars[loss.name]
# 8.3 recover the relation among blocks
for block in quant_program.blocks:
block.desc._set_forward_block_idx(parent_idx_dict[block.idx])
# 9. complete distributed attribution
self.set_dist_attr_for_qat_program(
quant_program, main_program, dist_context
)
# 10. reset scale var value with dist_attr
self.reset_scope_var(quant_program, dist_context, scope, place)
context.set_attr("main_program", quant_program)
context.set_attr("startup_program", startup_program)
context.set_attr("params_grads", new_params_grads)
context.set_attr("loss", new_loss)
def move_persist_var_to_global_block(self, program):
global_block = program.global_block()
for _op in global_block.ops:
if _op.type == "while":
_block_id = _op.attr("sub_block").id
_block = program.block(_block_id)
persistables = []
for _name, _var in _block.vars.items():
if _var.persistable:
global_block._clone_variable(_var)
persistables.append(_name)
for _name in persistables:
_block._remove_var(_name)
persistables.extend(_op.input('X'))
_op.desc.set_input("X", persistables)
return program
def reset_scope_var(self, quant_program, dist_context, scope, place):
# The var_value, created by quantization_passes, should has same shape with the value after parallel.
for var in quant_program.list_vars():
scope_var = scope.find_var(var.name)
if not (scope_var and scope_var.get_tensor()._is_initialized()):
continue
tensor = scope_var.get_tensor()
if var.shape == tensor.shape:
continue
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 tensor_value with dist_attr
sliced_tensor = Converter.slice_with_dist_attr(
np.array(tensor), dist_attr
)
tensor._clear()
tensor.set(sliced_tensor, place)
def set_dist_attr_for_qat_program(
self, quant_program, main_program, dist_context
):
# NOTE: hack implement, upgrading soon
for ib, block in enumerate(quant_program.blocks):
# recover origin ops' dist_attr and set quant ops' dist_attr
qat_offset = 0
for ip, quant_op in enumerate(block.ops):
quant_op_dist_attr = OperatorDistAttr()
if (
"quantize" in quant_op.type
or quant_op.type == "moving_average_abs_max_scale"
):
# set all quantization ops' dist_attr by quantified op
input_name = quant_op.desc.input('X')[0]
if "quantize" in input_name:
input_name = input_name[
: input_name.index(".quantized")
]
if (
quant_op.type == "moving_average_abs_max_scale"
or ip - qat_offset >= len(main_program.blocks[ib].ops)
):
consume_op = (
main_program.blocks[ib]
._var_recursive(input_name)
.op
)
else:
consume_op = main_program.blocks[ib].ops[
ip - qat_offset
]
consume_op_dist_attr = dist_context.get_dist_op_for_program(
consume_op
).dist_attr
ref_process_mesh = consume_op_dist_attr.process_mesh
if input_name in consume_op_dist_attr.outputs_dist_attrs:
consume_input_dist_attr = (
consume_op_dist_attr.outputs_dist_attrs[input_name]
)
else:
consume_input_dist_attr = (
consume_op_dist_attr.inputs_dist_attrs[input_name]
)
quant_op_dist_attr.impl_idx = 0
quant_op_dist_attr.impl_type = "default"
quant_op_dist_attr.process_mesh = ref_process_mesh
quant_op_dist_attr.set_input_dist_attr(
quant_op.desc.input('X')[0], consume_input_dist_attr
)
for slot_name in quant_op.desc.input_names():
in_name = quant_op.desc.input(slot_name)[0]
input_var = block._var_recursive(in_name)
ref_dims_mapping = [-1 for i in input_var.shape]
if slot_name == "X":
continue
elif slot_name in ['Scale', 'ZeroPoint']:
if (
quant_op.has_attr('quant_axis')
and quant_op.attr('quant_axis') != -1
):
x_name = quant_op.desc.input('X')[0]
x_var = block._var_recursive(x_name)
x_dist_attr = (
quant_op_dist_attr.get_input_dist_attr(
x_name
)
)
quant_axis = quant_op.attr('quant_axis')
ref_dims_mapping = [
x_dist_attr.dims_mapping[quant_axis]
]
tensor_dist_attr = TensorDistAttr()
tensor_dist_attr.process_mesh = ref_process_mesh
tensor_dist_attr.dims_mapping = ref_dims_mapping
dist_context.set_tensor_dist_attr_for_program(
input_var, tensor_dist_attr
)
quant_op_dist_attr.set_input_dist_attr(
in_name, tensor_dist_attr
)
for slot_name in quant_op.desc.output_names():
output_name = quant_op.desc.output(slot_name)[0]
output_var = block._var_recursive(output_name)
ref_dims_mapping = [-1 for i in output_var.shape]
if slot_name == "Y":
dist_context.set_tensor_dist_attr_for_program(
output_var, consume_input_dist_attr
)
quant_op_dist_attr.set_output_dist_attr(
output_name, consume_input_dist_attr
)
continue
elif slot_name == "OutScale":
if (
quant_op.has_attr('quant_axis')
and quant_op.attr('quant_axis') != -1
):
x_name = quant_op.desc.input('X')[0]
x_var = block._var_recursive(x_name)
x_dist_attr = (
quant_op_dist_attr.get_input_dist_attr(
x_name
)
)
quant_axis = quant_op.attr('quant_axis')
ref_dims_mapping = [
x_dist_attr.dims_mapping[quant_axis]
]
tensor_dist_attr = TensorDistAttr()
tensor_dist_attr.process_mesh = ref_process_mesh
tensor_dist_attr.dims_mapping = ref_dims_mapping
dist_context.set_tensor_dist_attr_for_program(
output_var, tensor_dist_attr
)
quant_op_dist_attr.set_output_dist_attr(
output_name, tensor_dist_attr
)
quant_op._set_attr("op_device", "")
qat_offset += 1
else:
# recover origin ops' dist_attr
origin_op = main_program.blocks[ib].ops[ip - qat_offset]
quant_op.desc.set_original_id(origin_op.desc.original_id())
dist_origin_op = dist_context.get_dist_op_for_program(
origin_op
)
assert dist_origin_op is not None, (
"origin op must have dist attr."
)
origin_op_dist_attr = dist_origin_op.dist_attr
quant_op_dist_attr.impl_idx = origin_op_dist_attr.impl_idx
quant_op_dist_attr.impl_type = origin_op_dist_attr.impl_type
quant_op_dist_attr.process_mesh = (
origin_op_dist_attr.process_mesh
)
scale_offset = 0
for idx, input_name in enumerate(quant_op.input_arg_names):
if (
origin_op.type == "while"
and input_name not in origin_op.input_arg_names
):
assert (
"@scale" in input_name
or "@zero_point" in input_name
)
scale_offset += 1
continue
idx -= scale_offset
origin_input_name = origin_op.input_arg_names[idx]
origin_input_dist_attr = (
origin_op_dist_attr.inputs_dist_attrs[
origin_input_name
]
)
quant_op_dist_attr.set_input_dist_attr(
input_name, origin_input_dist_attr
)
for idx, output_name in enumerate(
quant_op.output_arg_names
):
origin_output_name = origin_op.output_arg_names[idx]
origin_output_dist_attr = (
origin_op_dist_attr.outputs_dist_attrs[
origin_output_name
]
)
quant_op_dist_attr.set_output_dist_attr(
output_name, origin_output_dist_attr
)
if not main_program.blocks[ib]._find_var_recursive(
output_name
):
origin_output_var = main_program.blocks[
ib
]._var_recursive(origin_output_name)
origin_out_tensor_dist_attr = (
dist_context.get_dist_tensor_for_program(
origin_output_var
).dist_attr
)
quant_output_var = block._var_recursive(output_name)
dist_context.set_tensor_dist_attr_for_program(
quant_output_var, origin_out_tensor_dist_attr
)
dist_context.set_op_dist_attr_for_program(
quant_op, quant_op_dist_attr
)
# recover vars' dist_attr
for name, dst_var in block.vars.items():
if name in main_program.blocks[ib].vars:
src_var = main_program.blocks[ib].vars[name]
dist_tensor = dist_context.get_dist_tensor_for_program(
src_var
)
if not dist_tensor:
continue
dist_context.set_tensor_dist_attr_for_program(
dst_var, dist_tensor.dist_attr
)
@@ -0,0 +1,628 @@
# 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 re
import paddle
from paddle.base.backward import (
ProgramStats,
_append_grad_suffix_,
_find_op_path_,
_get_no_grad_set_name,
_rename_arg_,
)
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
from paddle.framework import core
from paddle.utils import unique_name
from ..auto_parallel.static.dist_attribute import OperatorDistAttr
from ..auto_parallel.static.utils import (
get_loss_op,
insert_dependencies_for_two_ops,
is_backward_op,
is_recompute_exclude_op,
is_recompute_op,
naive_set_dist_op_attr_for_program_by_mesh_and_mapping,
set_dist_op_desc_original_id,
set_var_dist_attr,
)
from ..utils.log_utils import get_logger
from .pass_base import PassBase, register_pass
logger = get_logger(logging.INFO)
class RecomputeState(ProgramStats):
def __init__(self, block, ops):
super().__init__(block=block, ops=ops)
self.seg_op_deps = {}
self._checkpoints = []
self._reserved_vars = []
@property
def checkpoints(self):
return self._checkpoints
@property
def reserved_vars(self):
return self._reserved_vars
def is_recompute(self):
return any(is_recompute_op(op) for op in self.ops)
def build_states(self):
for i, op in enumerate(self.ops):
if is_backward_op(op):
break
for name in op.input_arg_names:
if name in self.var_op_deps:
self.var_op_deps[name]["var_as_input_ops"].extend([i])
else:
self.var_op_deps[name] = {}
self.var_op_deps[name]["var_as_input_ops"] = [i]
self.var_op_deps[name]["var_as_output_ops"] = []
for name in op.output_arg_names:
if name in self.var_op_deps:
self.var_op_deps[name]["var_as_output_ops"].extend([i])
else:
self.var_op_deps[name] = {}
self.var_op_deps[name]["var_as_input_ops"] = []
self.var_op_deps[name]["var_as_output_ops"] = [i]
if not is_recompute_op(op):
self._checkpoints.extend(op.output_arg_names)
if not is_recompute_exclude_op(op):
continue
seg_name = op.attr('op_namescope')
res = re.search("/auto_parallel/rc_[0-9]*", seg_name)
seg_name = res.group(0)
if seg_name not in self.seg_op_deps:
self.seg_op_deps[seg_name] = [i]
else:
assert self.seg_op_deps[seg_name][-1] + 1 == i, (
"The recompute segment's ops should be continuous"
)
self.seg_op_deps[seg_name].extend([i])
def get_recompute_segments(self, no_recompute_segments=[]):
segments = []
for segment_idx in self.seg_op_deps.values():
if len(segment_idx) == 1:
continue
segments.append([segment_idx[0], segment_idx[-1] + 1])
self._checkpoints.extend(self.ops[segment_idx[-1]].output_arg_names)
for i in sorted(no_recompute_segments, reverse=True):
assert i < len(segments), (
f"the no_recompute_segments idx [{i}] should be lower the number of segment [{len(segments)}]"
)
segments.pop(i)
return segments
def modify_forward_desc_for_recompute(self, dist_context):
"""
If program's forward part has 'dropout' op, this function will insert
a seed op before it to guarantee that two dropout op have the same outputs.
"""
op_types = [op.type for op in self.ops]
if "dropout" not in op_types and "fused_dropout_add" not in op_types:
return
op_idx = 0
while op_idx < len(self.ops):
cur_op = self.ops[op_idx]
if "grad" in cur_op.type:
break
if cur_op.type == "seed":
self._reserved_vars.extend(cur_op.output_arg_names)
op_idx += 1
continue
if cur_op.type not in ["dropout", "fused_dropout_add"]:
op_idx += 1
continue
seed_tensor_name = (
"seed_tensor" if cur_op.type == "fused_dropout_add" else "Seed"
)
if cur_op.input(seed_tensor_name) is not None and len(
cur_op.input(seed_tensor_name)
):
op_idx += 1
continue
cur_op_dist_attr = dist_context.get_op_dist_attr_for_program(cur_op)
# insert seed op to guarantee that two dropout op have the same outputs
# NOTE Hack for adopt recompute for random control, for more info see dist_dropout.py
# new seed added by recompute should have a prefix to distinguish with seed added by user or other module.
op_unique_name = unique_name.generate("rc_seed")
var_unique_name = unique_name.generate_with_ignorable_key(
".".join([op_unique_name, 'tmp'])
)
self._reserved_vars.append(var_unique_name)
seed_var = self.block.create_var(
name=var_unique_name,
dtype='int32',
type=core.VarDesc.VarType.DENSE_TENSOR,
persistable=False,
stop_gradient=False,
)
# set new seed_var's dist_attr
ref_dims_mapping = [-1]
ref_process_mesh = cur_op_dist_attr.process_mesh
seed_var_dist_attr = set_var_dist_attr(
dist_context,
seed_var,
ref_dims_mapping,
ref_process_mesh,
chunk_id=cur_op_dist_attr.chunk_id,
)
seed = (
0
if cur_op.attr("fix_seed") is False
else int(cur_op.attr("seed"))
)
# TODO add dependency for seed op to ensure it be issued just before recompute.
seed_op = self.block._insert_op_without_sync(
index=cur_op.idx,
type="seed",
inputs={},
outputs={"Out": seed_var},
attrs={"seed": seed, "force_cpu": True},
)
seed_op._set_attr('op_namescope', cur_op.attr('op_namescope'))
# set new seed op's dist_attr
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
seed_op,
ref_process_mesh,
ref_dims_mapping,
dist_context,
chunk_id=cur_op_dist_attr.chunk_id,
)
# modify dropout op's desc
self.ops.insert(op_idx, seed_op)
cur_op.desc.set_input(seed_tensor_name, [var_unique_name])
cur_op.desc._set_attr("fix_seed", False)
cur_op.desc._set_attr("seed", 0)
cur_op_dist_attr.set_input_dist_attr(
seed_var.name, seed_var_dist_attr
)
op_idx += 2
self.block._sync_with_cpp()
def _find_op_index(block, cur_op):
for idx in range(block.desc.op_size()):
if cur_op.desc == block.desc.op(idx):
return idx
return -1
def _get_stop_gradients(program, no_grad_set=None):
"""get no grad var"""
if no_grad_set is None:
no_grad_set = set()
else:
no_grad_set = _get_no_grad_set_name(no_grad_set)
no_grad_set_name = set()
for var in program.list_vars():
if "@GRAD" in var.name:
break
if var.stop_gradient:
no_grad_set_name.add(_append_grad_suffix_(var.name))
no_grad_set_name.update(list(map(_append_grad_suffix_, no_grad_set)))
return no_grad_set_name
def _add_needed_descs_to_block(
descs, block, main_block, vars_should_be_hold, dist_context
):
"""
Get the recomputed ops which will insert the backward part
"""
if len(descs) == 0:
return []
result_descs = []
for desc in descs:
# if isinstance(desc, framework.Operator):
if isinstance(desc, paddle.static.Operator):
desc = desc.desc
if isinstance(desc, tuple):
desc = desc[0]
is_needed = False
for name in desc.output_arg_names():
if main_block.has_var(name) and main_block.var(name).persistable:
continue
if name not in vars_should_be_hold:
is_needed = True
if is_needed:
new_op_desc = block.desc.append_op()
new_op_desc.copy_from(desc)
set_dist_op_desc_original_id(new_op_desc, desc, dist_context)
new_op_desc._set_attr(OP_ROLE_KEY, OpRole.Backward)
result_descs.append(new_op_desc)
return result_descs
def _find_op_path(main_program, loss, no_grad_set=None):
no_grad_set_name = _get_stop_gradients(main_program, no_grad_set)
op_path = _find_op_path_(
main_program.global_block(), [loss], [], no_grad_set_name
)
return op_path
@register_pass("auto_parallel_recompute")
class RecomputePass(PassBase):
def __init__(self):
super().__init__()
self.set_attr("loss", None)
self.set_attr("dist_context", None)
self.set_attr("no_grad_set", None)
self.set_attr("no_recompute_segments", [])
def _check_self(self):
if self.get_attr("dist_context") is None:
return False
if self.get_attr("loss") is None:
return False
return True
def _check_conflict(self, other_pass):
return True
def get_ops_per_device(self, ops, all_ops_process_meshes, sr=0):
"""
Get ops and op_names of each process mesh excluding ops within the first "sr" chunks
"""
def reset_recompute_op(op):
if is_recompute_op(op) or is_recompute_exclude_op(op):
op._set_attr("op_namescope", "")
all_process_meshes_count = len(all_ops_process_meshes)
ops_of_stages = [[] for _ in range(all_process_meshes_count)]
op_names_of_stages = [[] for _ in range(all_process_meshes_count)]
pushed_ops_count = 0
reset_ops_count = 0
chunk_id = 0
for op_id, op in enumerate(ops):
if chunk_id // all_process_meshes_count < sr:
reset_ops_count += 1
reset_recompute_op(op)
if (
op_id < len(ops) - 1
and op.dist_attr.process_mesh
!= ops[op_id + 1].dist_attr.process_mesh
):
chunk_id += 1
if chunk_id // all_process_meshes_count < sr:
continue
for id, process_mesh in enumerate(all_ops_process_meshes):
if op.dist_attr.process_mesh == process_mesh:
pushed_ops_count += 1
ops_of_stages[id].append(op)
op_names_of_stages[id].append(op.type)
assert len(ops) == reset_ops_count + pushed_ops_count, (
f"The sum of pushed_ops_count and reset_ops_count must be the same as length of ops, but the sum is {reset_ops_count + pushed_ops_count} while length of ops is {len(ops)}"
)
return ops_of_stages, op_names_of_stages
def _apply_single_impl(self, main_program, startup_program, context):
loss = self.get_attr("loss")
no_grad_set = self.get_attr("no_grad_set")
no_recompute_segments = self.get_attr("no_recompute_segments")
self._dist_context = self.get_attr("dist_context")
self._sr = self.get_attr("sr", 0)
self._refined_ops_patterns = self.get_attr("refined_ops_patterns", [])
# 0. get op_path which is related to loss
main_block = main_program.global_block()
op_path = _find_op_path(main_program, loss, no_grad_set)
# 1. mark exclude ops for refined-recompute according to ops-patterns(mainly linear and flash_attn)
# 1.1 get all process_meshes in op_path
all_ops_process_meshes = []
for op in op_path:
if op.dist_attr.process_mesh not in all_ops_process_meshes:
all_ops_process_meshes.append(op.dist_attr.process_mesh)
# 1.2 get ops_devices and op_names_devices
ops_devices, op_names_devices = self.get_ops_per_device(
op_path, all_ops_process_meshes, self._sr
)
all_ops_len = len(op_path)
all_exclude_ops_ids = [[] for _ in op_names_devices]
# 1.3 find exclude ops for refined-recompute according to ops-patterns
for refined_ops_pattern in self._refined_ops_patterns:
num = refined_ops_pattern['num']
num = (
num if num >= 0 else all_ops_len
) # 'num == -1' represents to all ops
main_ops = refined_ops_pattern['main_ops']
pre_ops = refined_ops_pattern['pre_ops']
suf_ops = refined_ops_pattern['suf_ops']
main_start_id = len(pre_ops)
main_ops_len = len(main_ops)
pattern_ops = pre_ops + main_ops + suf_ops
pattern_ops_len = len(pattern_ops)
for id, op_names_device in enumerate(op_names_devices):
pattern_count = 0
ops_len_device = len(op_names_device)
for i in range(ops_len_device - pattern_ops_len + 1):
if (
op_names_device[i : i + pattern_ops_len] == pattern_ops
and pattern_count < num
):
pattern_count += 1
all_exclude_ops_ids[id].extend(
list(
range(
i + main_start_id,
i + main_start_id + main_ops_len,
)
)
)
logger.info(
f"The excluded ops in recompute segments are:\n{all_exclude_ops_ids}"
)
# 1.4 mark exclude ops in exclude_ops_ids
for id, exclude_ops_ids in enumerate(all_exclude_ops_ids):
for op_id in exclude_ops_ids:
if is_recompute_op(ops_devices[id][op_id]):
rc_mark_str = ops_devices[id][op_id].attr("op_namescope")
ops_devices[id][op_id]._set_attr(
"op_namescope", rc_mark_str + "_exclude_rc"
)
# 2. build recompute state
rc_state = RecomputeState(main_block, op_path)
if not rc_state.is_recompute():
return
# 3. get the segments to be recomputed
rc_state.modify_forward_desc_for_recompute(self._dist_context)
rc_state.build_states()
segments = rc_state.get_recompute_segments(no_recompute_segments)
if segments == []:
return
for i, (idx1, idx2) in enumerate(segments):
logger.debug(f"recompute segment[{i + 1}/{len(segments)}]")
logger.debug(
f"segment start op: [{rc_state.ops[idx1].type}]: [{rc_state.ops[idx1].input_arg_names}] [{rc_state.ops[idx1].output_arg_names}]"
)
logger.debug(
f"segment end op: [{rc_state.ops[idx2 - 1].type}]: [{rc_state.ops[idx2 - 1].input_arg_names}] [{rc_state.ops[idx2 - 1].output_arg_names}]"
)
# 4. get vars that should be hold in memory
# list of var_names
vars_should_be_hold = []
for segment in segments:
vars_should_be_hold.extend(
rc_state.get_out_of_subgraph_vars(segment[0], segment[1])
)
cross_vars = set(vars_should_be_hold) - set(rc_state.checkpoints)
logger.debug(
f"found [{len(cross_vars)}] vars which cross recompute segment: [{cross_vars}],"
"better checkpoints might be set to reduce those vars"
)
vars_should_be_hold.extend(rc_state.reserved_vars)
vars_should_be_hold.extend(rc_state.get_input_nodes())
vars_should_be_hold = list(
set(vars_should_be_hold) | set(rc_state.checkpoints)
)
# 5. get the fwd ops desc to be recomputed.
var_name_dict = {} # varname --> varname.subprog_XXX
ckpt_ops_dict = {} # ckpt_op_id --> segment_descs
buffer_block = main_block.program._create_block()
for i, segment in enumerate(segments[::-1]):
fwd_ops = op_path[segment[0] : segment[1]]
var_suffix = f".subprog_{i}"
for op in fwd_ops:
input_and_output_names = []
input_and_output_names.extend(op.input_arg_names)
input_and_output_names.extend(op.output_arg_names)
cur_op_dist_attr = (
self._dist_context.get_op_dist_attr_for_program(op)
)
assert cur_op_dist_attr is not None
for name in input_and_output_names:
if (
main_block.var(name).persistable
or name in vars_should_be_hold
):
continue
if name not in var_name_dict:
ref_process_mesh = cur_op_dist_attr.process_mesh
if name in op.input_arg_names:
ref_dims_mapping = (
cur_op_dist_attr.get_input_dims_mapping(name)
)
else:
ref_dims_mapping = (
cur_op_dist_attr.get_output_dims_mapping(name)
)
# record recomputed var's old_name and new_name (old_name.subprog_XXX)
# create new var with new name
var_name_dict[name] = name + var_suffix
ref_var = main_block.var(name)
rc_var = main_block.create_var(
name=var_name_dict[name],
shape=ref_var.shape,
dtype=ref_var.dtype,
type=ref_var.type,
persistable=ref_var.persistable,
stop_gradient=ref_var.stop_gradient,
)
# set new recomputed var's dist attr
set_var_dist_attr(
self._dist_context,
rc_var,
ref_dims_mapping,
ref_process_mesh,
chunk_id=cur_op_dist_attr.chunk_id,
)
# get recomputed segment's descs
segment_descs = _add_needed_descs_to_block(
fwd_ops,
buffer_block,
main_block,
vars_should_be_hold,
self._dist_context,
)
# rename recomputed ops' input and output var name
for key in var_name_dict:
_rename_arg_(segment_descs, key, var_name_dict[key])
# NOTE: one forward op could be correspond to multiple xxx_grad op.
# When traversing all grad_ops in reverse, need to set a flag to indicate
# whether the ckpt and its segment_descs can be used.
ckpt_op = op_path[segment[1] - 1]
ckpt_ops_dict[ckpt_op.desc.original_id()] = [True, segment_descs]
# 6. insert recomputed fwd ops into backward parse
ops = main_block.ops
loss_op = get_loss_op(main_block)
loss_op_idx = _find_op_index(main_block, loss_op)
dist_op_context = self._dist_context.dist_op_context
assert loss_op_idx != -1
# Traversing all grad_ops in reverse, and if the fwd op corresponding to reverse op is checkpoints,
# segments ops should be inserted.
for i in range(len(ops) - 1, loss_op_idx, -1):
grad_op = ops[i]
input_and_output_names = []
input_and_output_names.extend(grad_op.input_arg_names)
input_and_output_names.extend(grad_op.output_arg_names)
for varname in var_name_dict:
if varname not in input_and_output_names:
continue
self.reset_op_dist_attr(grad_op, var_name_dict)
_rename_arg_([grad_op.desc], varname, var_name_dict[varname])
# insert recomputed ops
original_id = grad_op.desc.original_id()
if original_id in dist_op_context.grad_op_id_to_op_id:
fwd_op_id = dist_op_context.grad_op_id_to_op_id[original_id]
if fwd_op_id in ckpt_ops_dict and ckpt_ops_dict[fwd_op_id][0]:
idx = grad_op.idx
while idx - 1 >= 0 and ops[idx - 1].type == "sum":
idx -= 1
segment_descs = ckpt_ops_dict[fwd_op_id][1]
rc_op = None
for _, op_desc in reversed(list(enumerate(segment_descs))):
rc_op = main_block._insert_op_without_sync(
idx, type='nop'
)
rc_desc = rc_op.desc
rc_desc.copy_from(op_desc)
rc_desc.set_original_id(rc_desc.id())
# set recomputed ops' dist attr
fwd_op_dist_attr = self._dist_context.get_op_dist_attr_for_program_with_id(
op_desc.original_id()
)
assert fwd_op_dist_attr is not None
self.set_op_dist_attr(
rc_op, fwd_op_dist_attr, var_name_dict
)
ckpt_ops_dict[fwd_op_id][0] = False
if rc_op:
prior_op = main_block.ops[rc_op.idx - 1]
posterior_op = rc_op
prior_mesh = (
self._dist_context.get_op_dist_attr_for_program(
prior_op
).process_mesh
)
posterior_mesh = (
self._dist_context.get_op_dist_attr_for_program(
posterior_op
).process_mesh
)
# NOTE if two recompute segments across two pipeline stages
# not need dependencies for it
if prior_mesh == posterior_mesh:
insert_dependencies_for_two_ops(
main_block,
idx,
prior_op,
posterior_op,
self._dist_context,
is_recompute=True,
sync=False,
op_namescope="recompute_segment_dep",
)
main_program._sync_with_cpp()
def reset_op_dist_attr(self, op, var_name_dict):
op_dist_attr = self._dist_context.get_op_dist_attr_for_program(op)
assert op_dist_attr is not None
for input in op.input_arg_names:
if input in var_name_dict.keys():
in_dist_attr = op_dist_attr.get_input_dist_attr(input)
op_dist_attr.set_input_dist_attr(
var_name_dict[input], in_dist_attr
)
for output in op.output_arg_names:
if output in var_name_dict.keys():
out_dist_attr = op_dist_attr.get_output_dist_attr(output)
op_dist_attr.set_output_dist_attr(
var_name_dict[output], out_dist_attr
)
def set_op_dist_attr(self, op, old_dist_attr, var_name_dict):
new_dist_attr = OperatorDistAttr()
new_dist_attr.is_recompute = True
new_dist_attr.impl_idx = old_dist_attr.impl_idx
new_dist_attr.impl_type = old_dist_attr.impl_type
new_dist_attr.process_mesh = old_dist_attr.process_mesh
new_dist_attr.chunk_id = old_dist_attr.chunk_id
for input in old_dist_attr.inputs_dist_attrs.keys():
if input in var_name_dict.keys():
in_dist_attr = old_dist_attr.inputs_dist_attrs[input]
new_dist_attr.set_input_dist_attr(
var_name_dict[input], in_dist_attr
)
else:
in_dist_attr = old_dist_attr.inputs_dist_attrs[input]
new_dist_attr.set_input_dist_attr(input, in_dist_attr)
for output in old_dist_attr.outputs_dist_attrs.keys():
if output in var_name_dict.keys():
out_dist_attr = old_dist_attr.outputs_dist_attrs[output]
new_dist_attr.set_output_dist_attr(
var_name_dict[output], out_dist_attr
)
else:
out_dist_attr = old_dist_attr.outputs_dist_attrs[output]
new_dist_attr.set_output_dist_attr(output, out_dist_attr)
self._dist_context.set_op_dist_attr_for_program(op, new_dist_attr)
@@ -0,0 +1,289 @@
# 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
from paddle.base import core
OpRole = core.op_proto_and_checker_maker.OpRole
from paddle.autograd import backward_utils
from ..auto_parallel.static.utils import (
get_logger,
)
from .pass_base import PassBase, register_pass
logger = get_logger(logging.INFO)
@register_pass("auto_parallel_recompute_pir")
class AutoParallelRecomputePIRPass(PassBase):
def __init__(self):
super().__init__()
self.program_ops = []
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def get_fwd_bwd_ops(self):
fwd_ops = []
bwd_ops = []
for op in self.program_ops:
if op.op_role == int(OpRole.Forward):
fwd_ops.append(op)
elif op.op_role == int(OpRole.Backward):
bwd_ops.append(op)
assert len(fwd_ops) and len(bwd_ops)
return fwd_ops, bwd_ops
def get_first_bwd_used_op(self, fwd_op, bwd_ops):
# Find the first user op of the op result in backward op list.
first_op = bwd_ops[-1]
for res in fwd_op.results():
for user_op in res.all_used_ops():
if user_op in bwd_ops and first_op.id() >= user_op.id():
first_op = user_op
return first_op
def is_seed_used_by_dropout(self, seed_op):
# Ensure that the random operator has the same output in backward recompute.
if seed_op.name() != "seed":
return False
seed_value = seed_op.results()[0]
dropout_ops = ["pd_op.dropout", "pd_op.fused_dropout_add"]
return any(
True
for used_op in seed_value.all_used_ops()
if used_op.name() in dropout_ops
)
def remove_outgoing_op(self, segment):
# An OP is considered an outgoing OP if all of results' user OPs are not in segment.
# These OPs do not participate in the backward gradient computation and therefore
# do not need to have a recomputation during backward.
segment_ops = [self.program_ops[idx] for idx in segment]
segment_len = len(segment)
for idx in range(segment_len - 1, 0, -1):
op = segment_ops[idx]
user_ops = set()
for res in op.results():
user_ops = user_ops | set(res.all_used_ops())
if user_ops & set(segment_ops):
continue
segment.pop(idx)
logger.info(
f"Remove outgoing OP '{op.name()}' from the segment for recomputation, as it does not participate in the backward."
)
return segment
def get_segments(self):
# `fwd_recompute_id` indicates the ID assigned to the segment for
# which the OP requires recompute.
# A segment comprises all OPs within a program, ranging from the OP
# with the minimum index to the OP with the maximum index, and all
# these operations share the same `fwd_recompute_id`.
segment_beg = {}
segment_end = {}
max_op_id = len(self.program_ops)
for idx, op in enumerate(self.program_ops):
# 1. Find the OPs marked with `fwd_recompute_id`.
if not op.has_attr("fwd_recompute_id"):
continue
# 2. Delineate the segment range marked by `fwd_recompute_id`.
# Note: there may be some unmarked OPs in between.
rc_id = op.attrs()["fwd_recompute_id"]
if rc_id not in segment_beg:
segment_beg[rc_id] = max_op_id
segment_end[rc_id] = 0
segment_beg[rc_id] = min(segment_beg[rc_id], idx)
segment_end[rc_id] = max(segment_end[rc_id], idx)
# 3. Aggregate all segment information into a dictionary.
# The key is the id of the segment, which is used to uniquely identify each segment.
# The value is a list of indices of the segment OPs in `self.program_ops`.
segments = {}
assert len(segment_beg.keys()) == len(segment_end.keys())
for segment_id, beg_id in segment_beg.items():
assert segment_id in segment_end.keys()
end_id = segment_end[segment_id]
assert beg_id <= end_id
segment = list(range(beg_id, end_id + 1))
# 4. Remove the outgoing OPs from the segment, as these OPs
# do not participate in the backward gradient computation.
segments[segment_id] = self.remove_outgoing_op(segment)
logger.info(
f"Segment ID {segment_id} contains {len(segment)} OPs, all of which will be recomputed."
)
return segments
def get_op_name(self, op):
return op.name().split('.')[1]
def match_pattern(
self,
op,
visit,
fetch_id,
fetch_pattern,
target_pattern,
pre_len,
main_len,
count,
max_count,
):
if count >= max_count:
return max_count
if len(fetch_pattern) > len(target_pattern):
return count
if self.get_op_name(op) != target_pattern[fetch_id]:
return count
if fetch_id == len(target_pattern) - 1:
for idx in range(pre_len, pre_len + main_len):
fetch_op = fetch_pattern[idx]
visit[fetch_op] = -1
refined_segment = list(set(visit.values()))
refined_segment.sort()
refined_segment = [idx for idx in refined_segment if idx != -1]
return count + 1
for res_val in op.results():
for user_op in res_val.all_used_ops():
fetch_pattern[fetch_id + 1] = user_op
count = self.match_pattern(
op=user_op,
visit=visit,
fetch_id=fetch_id + 1,
fetch_pattern=fetch_pattern,
target_pattern=target_pattern,
pre_len=pre_len,
main_len=main_len,
count=count,
max_count=max_count,
)
return count
def _apply_single_impl(self, main_program, startup_program, context=None):
self.program_ops = list(main_program.global_block().ops)
# 1. Get the recompute segments information form program.
segments = self.get_segments()
assert len(segments) > 0, (
"No segment found in the PIR recompute pass.\n \
Please disable 'recompute.enable' or check 'recompute()' usage in model code."
)
# 2. Get the forward and backward OPs from program.
fwd_ops, bwd_ops = self.get_fwd_bwd_ops()
# 3. Refine the segments based on the patterns.
refined_ops_patterns = self.get_attr("refined_ops_patterns")
for refined_ops_pattern in refined_ops_patterns:
# 3.1 get the refined pattern information.
# refined_ops_patterns = pre_ops + main_ops + suf_ops
# `main_ops` pattern: it does not participate in backward recomputation
# and needs to be removed from the segment.
# `pre_ops` pattern: it serve only as markers and do require recomputation.
# `suf_ops` pattern: it serve only as markers and do require recomputation.
# `num` : it limits the maximum number of `main_ops` patterns identified
# within each segment. A value of -1 represents all patterns.
num = int(refined_ops_pattern['num'])
num = num if num >= 0 else len(fwd_ops)
main_ops = refined_ops_pattern['main_ops']
pre_ops = refined_ops_pattern['pre_ops']
suf_ops = refined_ops_pattern['suf_ops']
pattern_ops = pre_ops + main_ops + suf_ops
for rc_id in segments.keys():
# 3.2 Identify and mark the first 'num' patterns in each segment.
# The dictionary 'op_idx_map' has keys as OP information.
# If an OP belongs to a pattern, its value in the dictionary is marked as -1.
op_idx_map = {
self.program_ops[idx]: idx for idx in segments[rc_id]
}
pattern_count = 0
fetch_pattern = [None] * len(pattern_ops)
for idx in segments[rc_id]:
op = self.program_ops[idx]
fetch_pattern[0] = op
pattern_count = self.match_pattern(
op=self.program_ops[idx],
visit=op_idx_map,
fetch_id=0,
fetch_pattern=fetch_pattern,
target_pattern=pattern_ops,
pre_len=len(pre_ops),
main_len=len(main_ops),
count=pattern_count,
max_count=num,
)
# 3.3 Refined segment to exclude the specified pattern.
refined_segment = list(set(op_idx_map.values()))
refined_segment.sort()
refined_segment = [idx for idx in refined_segment if idx != -1]
segments[rc_id] = refined_segment
# 4. Construct the segment for backward recomputation.
# 4.1 Build IrMapping to eplace forward value with backward recompute value.
input_value = main_program.list_vars()
value_map = paddle.pir.IrMapping()
for val in input_value:
value_map.add(val, val)
for rc_id, segment in segments.items():
# 4.2 Find the insertion position for the backward segment,
# which should be before backward gradient computation.
first_bwd_used_op = bwd_ops[-1]
for idx in segment:
op = self.program_ops[idx]
bwd_used_op = self.get_first_bwd_used_op(op, bwd_ops)
if first_bwd_used_op.id() > bwd_used_op.id():
first_bwd_used_op = bwd_used_op
ori_segment_outputs = backward_utils.ValueSet()
paddle.pir.set_insertion_point(first_bwd_used_op)
# 4.3 Clone the segment OPs and replace the forward
# value with backward recompute value.
for idx in segment:
op = self.program_ops[idx]
ori_segment_outputs.update(op.results())
# Random OPs should produce the same output before and after recomputation.
if self.is_seed_used_by_dropout(op):
continue
rc_op = op.clone(
value_map, paddle.pir.CloneOptions(False, True, True)
)
# The forward segment and the backward segment have the same segment ID.
if rc_op.has_attr("fwd_recompute_id"):
rc_op.erase_attr("fwd_recompute_id")
rc_op.set_int_attr("bwd_recompute_id", rc_id)
# Updtate attributes.
if first_bwd_used_op.has_attr('op_role'):
rc_op.set_int_attr("op_role", first_bwd_used_op.op_role)
if first_bwd_used_op.has_attr('chunk_id'):
rc_op.set_int_attr("chunk_id", first_bwd_used_op.chunk_id)
# 4.4 Replace the forward value with backward recompute value.
for ori_value in ori_segment_outputs:
rc_value = value_map.look_up(ori_value)
ori_value.replace_grad_users_with(rc_value, set(bwd_ops))
@@ -0,0 +1,101 @@
# 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.distributed.auto_parallel.static.process_group import (
new_process_group,
)
from ..auto_parallel.static.utils import (
get_logger,
)
from .pass_base import PassBase, register_pass
logger = get_logger(logging.INFO)
@register_pass("replace_with_parallel_cross_entropy")
class AutoParallelReplaceWithParallelCrossEntropyPass(PassBase):
def __init__(self):
super().__init__()
hcg = dist.fleet.get_hybrid_communicate_group()
self.model_parallel_group = hcg.get_model_parallel_group()
self.tensor_parallel_degree = hcg.get_model_parallel_world_size()
def _check_self(self):
# The activation of this pass requires adopting a model parallel strategy.
if self.tensor_parallel_degree < 2:
return False
return True
def _check_conflict(self, other_pass):
return True
def _check_user(self, value):
placement1 = value.placements
for user in value.all_used_ops():
for operand in user.operands_source():
if operand.get_defining_op() != value.get_defining_op():
continue
placement2 = operand.placements
if placement1 != placement2:
return False
break
return True
def _apply_single_impl(self, main_program, startup_program, context):
del_ops = []
new_ops = []
for block in main_program.blocks:
for op in reversed(block.ops):
if op.name() == 'pd_op.cross_entropy_with_softmax':
operand1 = op.operand_source(0)
operand2 = op.operand_source(1)
# The `logit` input of the `cross_stropy_with_stoftmax` operator
# meed split along the column.
placement1 = operand1.placements
if not placement1[1].is_shard():
return
process_ids = operand1.dist_attr().process_mesh.process_ids
group = new_process_group(sorted(process_ids))
ring_id = group.id
nranks = group.nranks
rank = paddle.distributed.get_rank()
ignore_index = op.attrs()["ignore_index"]
paddle.pir.set_insertion_point(op)
softmax, loss = paddle._C_ops.c_softmax_with_cross_entropy(
operand1, operand2, ignore_index, ring_id, rank, nranks
)
op.result(0).replace_all_uses_with(softmax)
op.result(1).replace_all_uses_with(loss)
del_ops.append(op)
new_ops.append(softmax.get_defining_op())
for op in del_ops:
for result in op.results():
assert result.use_empty()
op.erase()
# In the forward program, the placements of the newly added OP
# output should be consistent with the placements of the user OP input
for op in new_ops:
for result in op.results():
assert self._check_user(result)
return
@@ -0,0 +1,171 @@
# 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 paddle
from paddle.distributed.auto_parallel.static.utils import (
naive_set_dist_op_attr_for_program_by_mesh,
)
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY
from paddle.distributed.utils.stream_utils import ExecutionStreamType
from paddle.static import default_main_program
from .auto_parallel_sharding import _is_reshard_op
from .pass_base import PassBase, PassType, register_pass
# NOTE we add the "auto_parallel" prefix to the pass in order to
# indicate that this pass should obey some constrains by auto_parallel
# for example all ops and vars should has dist attr before and after pass
# should use dist op instead of custom comm op
@register_pass("auto_parallel_sequence_parallel_optimization")
class SequenceParallelOptimizationPass(PassBase):
"""
This pass is used to optimize the sequence parallel.
1. Fuse the allreduce + split into reducescatter.
2. Trade off communication for memory in the row_parallel_linear output.
3. Overlap communication with computation in backward computation.
"""
def __init__(self):
super().__init__()
self.set_attr("dist_context", None)
def _check_self(self):
if self.get_attr("dist_context") is None:
return False
if (not isinstance(self.get_attr("global_rank"), int)) or self.get_attr(
"global_rank"
) < 0:
return False
if not self.get_attr("dist_context").strategy.sp_optimization.enable:
return False
return True
def _check_conflict(self, other_pass):
return True
def _type(self):
return PassType.COMM_OPT
def _apply_single_impl(self, main_program, startup_program, context):
self.dist_context = self.get_attr("dist_context")
self.global_rank = int(self.get_attr("global_rank"))
with paddle.static.program_guard(main_program, startup_program):
# TODO remove this pass when we use local reshard for all communication
self._fuse_allreduce_split()
self._memory_optimization()
self._overlap()
def _fuse_allreduce_split(self):
# allreduce is added by dist op and split is added by reshard, so we need this pass to fuse them as reducescatter.
# reducescatter should be inferred by local reshard in future.
block = default_main_program().global_block()
# record valid split ops
valid_split_op_indices = []
def is_valid_split_op(idx, block):
op = block.ops[idx]
if not op.type == "split":
return False
pre_op = block.ops[idx - 1]
if not (
pre_op.type == "all_reduce"
and pre_op.attr("reduce_type")
== paddle.distributed.ReduceOp.SUM
):
return False
pre_output_name = pre_op.output_arg_names[0]
cur_input_name = op.input_arg_names[0]
if (
pre_output_name == cur_input_name
and _is_reshard_op(op)
and op.attr("axis") == 0
):
return True
return False
for i in range(len(block.ops)):
if is_valid_split_op(i, block):
valid_split_op_indices.append(i)
# modify program
remove_varnames = []
for i in sorted(valid_split_op_indices, reverse=True):
allreduce_op = block.ops[i - 1]
split_op = block.ops[i]
consumer_op = block.ops[i + 1]
allreduce_input_name = allreduce_op.input("X")[0]
ring_id = int(allreduce_op.attr("ring_id"))
split_output_names = split_op.output("Out")
nranks = len(split_output_names)
consumer_input_names = consumer_op.input_arg_names
intersection = set(split_output_names).intersection(
set(consumer_input_names)
)
assert len(intersection) == 1, (
f"Sequence Parallel ReduceScatter Output more than 1: {intersection}."
)
keep_output_name = intersection.pop()
split_output_names.remove(keep_output_name)
remove_varnames.extend(split_output_names)
# replace ops
new_op = block._insert_op_without_sync(
index=i + 1,
type="reduce_scatter",
inputs={'x': [allreduce_input_name]},
outputs={'out': [keep_output_name]},
attrs={
'ring_id': ring_id,
'nranks': nranks,
'op_namescope': allreduce_op.attr("op_namescope"),
OP_ROLE_KEY: consumer_op.attr(OP_ROLE_KEY),
},
)
new_op.dist_attr.execution_stream = (
ExecutionStreamType.DefaultStream.value
)
block._remove_op(i, False)
block._remove_op(i - 1, False)
# set dist attr
allreduce_input_dist_attr = (
self.dist_context.get_tensor_dist_attr_for_program(
block.vars[allreduce_input_name]
)
)
ref_process_mesh = allreduce_input_dist_attr.process_mesh
naive_set_dist_op_attr_for_program_by_mesh(
new_op,
ref_process_mesh,
self.dist_context,
chunk_id=allreduce_input_dist_attr.chunk_id,
)
# remove vars
for varname in remove_varnames:
block._remove_var(varname, sync=False)
block._sync_with_cpp()
def _memory_optimization(self):
pass
def _overlap(self):
pass
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,159 @@
# 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.auto_parallel.static.operators.common import (
is_amp_flag_sync_op,
is_data_parallel_reduce_op,
is_global_norm_sync_op,
)
from paddle.distributed.auto_parallel.static.utils import (
OpRole,
insert_dependencies_for_vars,
is_comm_op,
)
from .auto_parallel_sharding import ShardingPass, _supported_optimizer_type
from .pass_base import PassBase, register_pass
def _sharding_pass_applied(pass_ctx):
for applied_pass in pass_ctx.passes:
if isinstance(applied_pass, ShardingPass):
return True
return False
# NOTE we add the "auto_parallel" prefix to the pass in order to
# indicate that this pass should obey some constrains by auto_parallel
# for example all ops and vars should has dist attr before and after pass
# should use dist op instead of custom comm op
@register_pass("auto_parallel_supplement_explicit_dependencies")
class AutoParalSupplementDepPass(PassBase):
"""
Functional Concern.
for strategies like amp & global norm, there is a collective communication to sync gradient information in every rank.
after partition the gradients to each rank, the order of that collective communication is different in each rank
and might cause hang problem in graph based random order executor. here supplement explicit dependencies for those cases.
TODO Performance Concern.
global collective will introduce global synchronization which forces the fast workers to wait for slow ones.
therefore we should conduct this collective when all the ranks reach a same stage.
BUT the depend API offered by executor could only ensure "conduct-not-before" but not "conduct-right-after".
Some ranks might call the collectives first than other ranks while they still some local could be performed to wait for slow peers.
IR Pass currently could not have the fully control of time the to perform these global collectives.
"""
def __init__(self):
super().__init__()
self.set_attr("dist_context", None)
def _check_self(self):
if self.get_attr("dist_context") is None:
return False
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, context):
# TODO general this pass for all case.
if not _sharding_pass_applied(context):
return
self._dist_context = self.get_attr("dist_context", None)
self.flags_sync_stream = "flags_sync_stream"
main_block = main_program.global_block()
startup_block = startup_program.global_block()
# last dp grad communication
last_dp_reduce_op_idx = -1
last_dp_reduce_varname = None
for idx, op in reversed(list(enumerate(main_block.ops))):
if is_data_parallel_reduce_op(op):
last_dp_reduce_op_idx = idx
last_dp_reduce_varname = op.output_arg_names[0]
break
assert last_dp_reduce_op_idx > 0
assert last_dp_reduce_varname is not None
# analyze deps for amp & global norm
deps_map = {}
prior_varname = last_dp_reduce_varname
for idx, op in enumerate(main_block.ops):
if is_amp_flag_sync_op(op) or is_global_norm_sync_op(op):
op_namescope = None
if is_amp_flag_sync_op(op):
op_namescope = "amp_flag_sync_dep"
op.dist_attr.execution_stream = self.flags_sync_stream
elif is_global_norm_sync_op(op):
op_namescope = "global_norm_sync_dep"
deps_map[idx] = (prior_varname, op.input("X")[0], op_namescope)
prior_varname = op.output("Out")[0]
# analyze deps for check_finite_and_unscale
# ensure it is performed after last backward computation, therefore reduce the
# straggling of the amp-flag-sync
first_check_op = True
for idx, op in enumerate(main_block.ops):
if op.type == "check_finite_and_unscale":
if first_check_op:
last_backward_op = None
for last_idx in range(idx - 1, 0, -1):
if not is_comm_op(main_block.ops[last_idx]):
last_backward_op = main_block.ops[last_idx]
break
prior_varname = last_backward_op.output_arg_names[0]
first_check_op = False
deps_map[idx] = (
prior_varname,
op.input("Scale")[0],
"check_finite_dep",
)
# analyze deps for optimizer
# optimizers order should be fixed to allow broadcast to overlap with optimizer
first_optimizer_op = True
for idx, op in enumerate(main_block.ops):
if op.type in _supported_optimizer_type:
if first_optimizer_op:
first_optimizer_op = False
else:
deps_map[idx] = (
prior_varname,
op.input("Param")[0],
"optimizer_order_dep",
)
prior_varname = op.output("ParamOut")[0]
# insert deps
indice = sorted(deps_map.keys(), reverse=True)
for idx in indice:
prior_var = main_block.var(deps_map[idx][0])
post_var = main_block.var(deps_map[idx][1])
op_namescope = deps_map[idx][2]
depend_op = insert_dependencies_for_vars(
main_block,
idx,
prior_var,
post_var,
self._dist_context,
OpRole.Optimize,
is_recompute=False,
sync=False,
op_namescope=op_namescope,
)
main_block._sync_with_cpp()
@@ -0,0 +1,302 @@
# 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.base.core import TensorDistAttr
from paddle.base.executor import global_scope
from paddle.base.framework import auto_complete_op_role
from paddle.distributed.auto_parallel.static.process_group import (
new_process_group,
)
from paddle.distributed.auto_parallel.static.utils import (
get_pp_stage_by_process_mesh,
)
from paddle.distributed.fleet.meta_optimizers.common import OpRole
from paddle.static.pir_io import get_pir_parameters
from ..auto_parallel.static.utils import (
get_logger,
)
from .pass_base import PassBase, register_pass
logger = get_logger(logging.INFO)
@register_pass("auto_parallel_sync_shared_params")
class AutoParallelSyncSharedParamsPass(PassBase):
def __init__(self):
super().__init__()
self.params_maybe_shared = []
self.src_ranks = []
self.dst_ranks = []
self.comm_group = {}
def _check_self(self):
pipeline_strategy = self.get_attr('pipeline_strategy')
if (not pipeline_strategy.enable) or pipeline_strategy.pp_degree <= 1:
return False
return True
def _check_conflict(self, other_pass):
return True
def _find_fist_opt_user(self, main_program):
for op in main_program.global_block().ops:
if op.op_role == 2:
return op
def _get_comm_group(self, ranks=[]):
ranks = sorted(ranks)
if tuple(ranks) in self.comm_group:
return self.comm_group[tuple(ranks)]
# The communication group of this `all_reduce` op satisfies len (ranks)==2.
# When `force_new_group=False` is set, the `send&recv` group will be returned,
# At this point, `all_reduce` and `send&recv` share the same group, and
# the process will hang up.
group = new_process_group(ranks, force_new_group=True)
self.comm_group[tuple(ranks)] = group.id
return group.id
def sync_shared_parameters(self, main_program, startup_program):
if not self._check_self():
logger.info(
"AutoParallelSyncSharedParamsPass need support pipeline parallel, skip pass."
)
return []
new_shared_params = []
params, _ = get_pir_parameters(main_program)
for param in params:
users = param.all_used_ops()
for user_op in users:
if user_op.name() == "dist_op.reshard":
reshard_op = user_op
dist_attr = reshard_op.dist_attr
src_dist_attr = dist_attr.operand(0).as_tensor_dist_attr()
dst_dist_attr = dist_attr.result(0).as_tensor_dist_attr()
src_mesh = src_dist_attr.process_mesh
dst_mesh = dst_dist_attr.process_mesh
# Shared parameter needs reshard on diff stage.
pipeline_strategy = self.get_attr('pipeline_strategy')
pp_degree = pipeline_strategy.pp_degree
src_stage = get_pp_stage_by_process_mesh(
src_mesh, pp_degree
)
dst_stage = get_pp_stage_by_process_mesh(
dst_mesh, pp_degree
)
if (
src_stage is None
or dst_stage is None
or src_stage == dst_stage
):
continue
# Get shared parameter name
param_name = param.get_defining_op().str_attr(
'parameter_name'
)
# Add shared parameter builtin.parameter with "shared_" prefix.
with (
auto_complete_op_role(main_program, OpRole.Forward),
paddle.static.program_guard(
main_program, startup_program
),
):
shared_param = paddle.pir.core.create_parameter(
dtype=param.dtype,
shape=param.shape,
name="shared_" + param_name,
process_mesh=dst_mesh,
placements=src_dist_attr.placements,
initializer=paddle.nn.initializer.Constant(value=0),
)
main_program.set_parameters_from(startup_program)
# Record new shared parameter.
new_shared_params.append("shared_" + param_name)
# Set value for new shared parameter.
concrete_program = self.get_attr("concrete_program")
dy_params = concrete_program.parameters[0]
dy_param = None
for tmp_param in dy_params:
if tmp_param.name == param_name:
dy_param = tmp_param
break
assert dy_param is not None, (
f"The parameter {param_name} was not found in the concrete_degram"
)
new_dist_attr = TensorDistAttr()
new_dist_attr.process_mesh = dst_mesh
new_dist_attr.dims_mapping = src_dist_attr.dims_mapping
with paddle.no_grad():
dy_shared_param = paddle.base.core.reshard(
dy_param, new_dist_attr
)
paddle.device.synchronize()
if dy_shared_param._is_initialized():
pir_shared_param = (
global_scope()
.var("shared_" + param_name)
.get_tensor()
)
pir_shared_param._share_data_with(
dy_shared_param.get_tensor().get_tensor()
)
# record in params_maybe_shared
self.params_maybe_shared.append(
{
'src_mesh': src_mesh,
'dst_mesh': dst_mesh,
'src_dist_attr': src_dist_attr,
'dst_dist_attr': dst_dist_attr,
'param_name': param_name,
}
)
# New shared parameter must has same dist_attr with shared parameter
new_src_dist_attr = (
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
dst_dist_attr.process_mesh,
src_dist_attr.dims_mapping,
src_dist_attr.partial_status,
)
)
if new_src_dist_attr == dst_dist_attr:
# Remove useless reshared op.
reshard_op.result(0).replace_all_uses_with(shared_param)
reshard_op.erase()
else:
# Update reshard op dist_attr.
reshard_op.dist_attr = (
paddle.base.libpaddle.pir.create_op_dist_attribute(
dst_mesh,
[new_src_dist_attr],
[dst_dist_attr],
-1,
)
)
reshard_op.operand(0).set_source(shared_param)
self.src_ranks.extend(src_mesh.process_ids)
self.dst_ranks.extend(dst_mesh.process_ids)
if len(self.params_maybe_shared) == 0:
logger.info("No parameter need to share, skip pass.")
return []
# Must initialize the redundant communication group for the allreduce op here.
# Otherwise, it will hang during gradient synchronization.
for idx in range(len(self.src_ranks)):
rank_1 = self.src_ranks[idx]
rank_2 = self.dst_ranks[idx]
new_process_group(sorted([rank_1, rank_2]))
self._get_comm_group([rank_1, rank_2])
return new_shared_params
def sync_shared_parameter_gradient(
self, main_program, startup_program, params_grads
):
if not self._check_self():
logger.info(
"AutoParallelSyncSharedParamsPass need support pipeline parallel, skip pass."
)
return params_grads
if len(self.params_maybe_shared) == 0:
logger.info("No parameter need to share, skip pass.")
return params_grads
# Only support one shared parameter.
# TODO: support more shared parameters
assert len(self.params_maybe_shared) == 1, (
"Currently, only one shared parameter is supported, and it cannot support more at the moment."
)
cur_rank = paddle.distributed.get_rank()
if cur_rank not in self.src_ranks and cur_rank not in self.dst_ranks:
return params_grads
pre_name = ""
if cur_rank in self.dst_ranks:
pre_name = "shared_"
for param_mess in self.params_maybe_shared:
param_name = pre_name + param_mess['param_name']
src_mesh_ids = param_mess['src_mesh'].process_ids
dst_mesh_ids = param_mess['dst_mesh'].process_ids
# Get (param, grad) value
param_value = main_program.get_parameter_value_by_name(param_name)
grad_idx = None
for p_idx, (p_param, _) in enumerate(params_grads):
if p_param.is_same(param_value):
grad_idx = p_idx
break
assert grad_idx is not None, (
f"Parameter {param_name} not found in params_grades, unable to find corresponding gradient value."
)
grad_value = params_grads[p_idx][1]
# Create allreduce op comm group.
cur_rank = paddle.distributed.get_rank()
if cur_rank in self.src_ranks:
idx = src_mesh_ids.index(cur_rank)
peer_rank = dst_mesh_ids[idx]
if cur_rank in self.dst_ranks:
idx = dst_mesh_ids.index(cur_rank)
peer_rank = src_mesh_ids[idx]
ar_group_id = self._get_comm_group([cur_rank, peer_rank])
# Insert allreduce op in the end of backward.
insert_pos = self._find_fist_opt_user(main_program)
paddle.pir.set_insertion_point(insert_pos)
# Build allreduce op to sync gradient.
with auto_complete_op_role(main_program, OpRole.Backward):
allreduce_val = paddle._C_ops.all_reduce(
grad_value,
ar_group_id,
dist.ReduceOp.SUM,
)
allreduce_val.update_dist_attr(grad_value.dist_attr())
allreduce_op = allreduce_val.get_defining_op()
# Update all_used_ops
for user in grad_value.all_used_ops():
if user.name() == "pd_op.all_reduce":
continue
for idx, operand in enumerate(user.operands()):
if user.operand_source(idx).is_same(grad_value):
user.operand(idx).set_source(allreduce_val)
# Update (param, grad) value
params_grads[p_idx] = (param_value, allreduce_val)
return params_grads
def _apply_single_impl(self, main_program, startup_program, context):
return
+251
View File
@@ -0,0 +1,251 @@
# 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.framework import (
_apply_pass as _apply_cpp_pass,
core,
)
from paddle.static import Executor
from .pass_base import CPPPassWrapper, PassType, register_pass
@register_pass("fuse_elewise_add_act")
class FuseElementwiseAddActPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fuse_elewise_add_act_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fuse_bn_act")
class FuseBatchNormActPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fuse_bn_act_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fuse_bn_add_act")
class FuseBatchNormAddActPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fuse_bn_add_act_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fuse_relu_depthwise_conv")
class FuseReluDepthwiseConvPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fuse_relu_depthwise_conv_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fused_attention")
class FusedAttentionPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fused_attention_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fused_feedforward")
class FusedFeedforwardPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fused_feedforward_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fuse_gemm_epilogue")
class FuseGemmEpiloguePass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fuse_gemm_epilogue_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fuse_adamw")
class FuseAdamWPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fuse_adamw_op_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fuse_dot_product_attention")
class FuseDotProductAttentionPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fuse_dot_product_attention_pass"
def _type(self):
return PassType.FUSION_OPT
@register_pass("fuse_optimizer")
class FuseOptimizerPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return [
"fuse_adam_op_pass",
"fuse_sgd_op_pass",
"fuse_momentum_op_pass",
]
def _type(self):
return PassType.FUSION_OPT
# @register_pass("inplace_addto_op")
# class InplaceAddtoOpPass(CPPPassWrapper):
# def __init__(self):
# super().__init__()
# @property
# def cpp_name(self):
# return "inplace_addto_op_pass"
# def _type(self):
# return PassType.CALC_OPT
@register_pass("fuse_resunit")
class FuseResUnitPass(CPPPassWrapper):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
return "fuse_resunit_pass"
def _type(self):
return PassType.FUSION_OPT
def _set_cinn_op_flag(flag_name, extra_ops):
values = core.globals()[flag_name]
values = [v.strip() for v in values.split(";") if v.strip()]
values.extend(extra_ops)
core.globals()[flag_name] = ";".join(values)
@register_pass("build_cinn")
class BuildCINNPass(CPPPassWrapper):
def __init__(self):
super().__init__()
self.set_attr("allow_ops", [])
self.set_attr("deny_ops", [])
@property
def cpp_name(self):
return "build_cinn_pass"
def _type(self):
return PassType.CALC_OPT
def _apply_single_impl(self, main_program, startup_program, context):
assert 'FLAGS_allow_cinn_ops' in core.globals(), (
"PaddlePaddle is not compiled with CINN support"
)
old_allow_ops = core.globals()['FLAGS_allow_cinn_ops']
old_deny_ops = core.globals()['FLAGS_deny_cinn_ops']
try:
_set_cinn_op_flag(
'FLAGS_allow_cinn_ops', self.get_attr("allow_ops")
)
_set_cinn_op_flag('FLAGS_deny_cinn_ops', self.get_attr("deny_ops"))
feed = self.get_attr('feed', [])
fetch_list = self.get_attr('fetch_list', [])
prune_program = self.get_attr('prune_program', True)
if prune_program:
tmp_main_program = Executor._prune_program(
main_program, feed, fetch_list, []
)
tmp_main_program = Executor._add_fetch_ops(
tmp_main_program, fetch_list, 'fetch'
)
else:
tmp_main_program = Executor._add_fetch_ops(
main_program, fetch_list, 'fetch'
)
_apply_cpp_pass(
tmp_main_program,
startup_program,
self.cpp_name,
{},
self.cpp_attr_types,
)
tmp_main_program = Executor._remove_fetch_ops(tmp_main_program)
tmp_main_program = core.ProgramDesc(tmp_main_program.desc)
main_program._rebuild_from_desc(tmp_main_program)
finally:
core.globals()['FLAGS_allow_cinn_ops'] = old_allow_ops
core.globals()['FLAGS_deny_cinn_ops'] = old_deny_ops
+387
View File
@@ -0,0 +1,387 @@
# 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 numpy as np
import paddle
from paddle.framework import core
from paddle.utils import unique_name
from .pass_base import PassBase, PassType, register_pass
def find_adjacent_match_sequences(
iterable, filter_func, adjacent_filter_func=None
):
n = len(iterable)
match_sequences = []
if adjacent_filter_func is None:
adjacent_filter_func = lambda ref_op, new_op: True
i = 0
while True:
while i < n and not filter_func(iterable[i]):
i += 1
j = i + 1
while (
j < n
and filter_func(iterable[j])
and adjacent_filter_func(iterable[i], iterable[j])
):
j += 1
if i < n and j <= n:
match_sequences.append((i, j))
i = j + 1
if i >= n:
break
return match_sequences
def insert_fuse_all_reduce_ops(
block, reversed_op_indices, input_var_names, output_var_names, dtype, attrs
):
fused_var = block.create_var(
name=unique_name.generate(f"FusedOutput_{input_var_names[0]}"),
dtype=dtype,
)
# FIXME(zengjinle): here we assume that we use
# c_sync_calc_stream/c_sync_comm_stream to do sync.
# But someone may use c_wait_compute/c_wait_comm instead.
if not attrs["use_calc_stream"]:
ring_id = attrs["ring_id"]
new_op_indices = list(reversed_op_indices)
for i, op_idx in enumerate(reversed_op_indices):
prev_op_idx = op_idx - 1
while (
prev_op_idx >= 0
and block.ops[prev_op_idx].type == "c_sync_calc_stream"
):
new_op_indices.append(prev_op_idx)
prev_op_idx -= 1
if i > 0:
next_op_idx = op_idx + 1
n = len(block.ops)
while (
next_op_idx < n
and block.ops[next_op_idx].type == "c_sync_comm_stream"
):
assert block.ops[next_op_idx].attr("ring_id") == ring_id
new_op_indices.append(next_op_idx)
new_op_indices = list(set(new_op_indices))
new_op_indices.sort(reverse=True)
reversed_op_indices = new_op_indices
insert_idx = reversed_op_indices[0] + 1
op_role_key = core.op_proto_and_checker_maker.kOpRoleAttrName()
concated_shapes = []
concated_ranks = []
for var_name in output_var_names:
shape = block._find_var_recursive(var_name).shape
concated_shapes.extend(shape)
concated_ranks.append(len(shape))
coalesce_tensor_op_kwargs = {
"type": "coalesce_tensor",
"inputs": {
"Input": input_var_names,
},
"outputs": {
"Output": output_var_names,
"FusedOutput": fused_var,
},
"attrs": {
"use_align": True,
"dtype": dtype,
"concated_shapes": concated_shapes,
"concated_ranks": concated_ranks,
op_role_key: attrs[op_role_key],
},
}
if not attrs["use_calc_stream"]:
block._insert_op_without_sync(
insert_idx,
type="c_sync_calc_stream",
inputs={"X": fused_var},
outputs={"Out": fused_var, op_role_key: attrs[op_role_key]},
)
insert_idx += 1
# all_reduce sum should insert
attrs["reduce_type"] = paddle.distributed.ReduceOp.SUM
block._insert_op_without_sync(
insert_idx,
type="all_reduce",
inputs={"x": fused_var},
outputs={"out": fused_var},
attrs=attrs,
)
for op_idx in reversed_op_indices:
block._remove_op(op_idx)
return coalesce_tensor_op_kwargs
def has_same_attrs(op1, op2, attr_names):
for attr_name in attr_names:
if op1.attr(attr_name) != op2.attr(attr_name):
return False
return True
def filter_all_collective_op_indices(block):
# NOTE: should add more collective ops
all_collective_ops = {
"c_broadcast",
"broadcast",
"all_gather",
"all_reduce",
}
match_op_indices = []
for i, op in enumerate(block.ops):
if op.type in all_collective_ops:
match_op_indices.append(i)
return match_op_indices
def find_all_fuse_all_reduce_groups(block):
collective_op_indices = filter_all_collective_op_indices(block)
collective_ops = [block.ops[i] for i in collective_op_indices]
def is_valid_allreduce_op(op):
if op.type != "c_allreduce_sum" or op.attr("use_model_parallel"):
return False
in_var_name = op.input("X")[0]
out_var_name = op.output("Out")[0]
if in_var_name != out_var_name:
return False
in_var = block._find_var_recursive(in_var_name)
assert in_var is not None
if in_var.type != core.VarDesc.VarType.DENSE_TENSOR:
return False
shape = in_var.shape
if any(s <= 0 for s in shape):
return False
return True
same_attr_names = [
"ring_id",
"use_calc_stream",
core.op_proto_and_checker_maker.kOpRoleAttrName(),
core.op_proto_and_checker_maker.kOpDeviceAttrName(),
]
def is_same_adjacent_op(ref_op, new_op):
if not has_same_attrs(ref_op, new_op, same_attr_names):
return False
ref_op_in_var = block._find_var_recursive(ref_op.input("X")[0])
new_op_in_var = block._find_var_recursive(new_op.input("X")[0])
if ref_op_in_var.dtype != new_op_in_var.dtype:
return False
return True
match_seqs = find_adjacent_match_sequences(
collective_ops, is_valid_allreduce_op, is_same_adjacent_op
)
new_match_seqs = []
for i, j in match_seqs:
new_match_seqs.append([collective_op_indices[k] for k in range(i, j)])
return new_match_seqs
def split_fuse_all_reduce_groups_by_deps(block, groups, op_deps):
new_groups = []
def insert_new_group(op_indices, start_idx, end_idx):
if end_idx - start_idx > 1:
new_groups.append(op_indices[start_idx:end_idx])
for op_indices in groups:
n = len(op_indices)
assert n > 0
if n == 1:
continue
start_idx = 0
k = start_idx + 1
while k < n:
found_group = False
for prev_idx in range(start_idx, k):
dep = op_deps[op_indices[prev_idx]][op_indices[k]]
if dep == core.Node.Dep.NoDep:
continue
# [start_idx, k) is valid groups
insert_new_group(op_indices, start_idx, k)
start_idx = k
break
k += 1
insert_new_group(op_indices, start_idx, k)
return new_groups
def insert_coalesce_tensor_ops(block, coalesce_ops_kwargs):
if not coalesce_ops_kwargs:
return
var_infos = {}
for idx, op in enumerate(block.ops):
for var in op.input_arg_names:
if var not in var_infos:
var_infos[var] = [idx, True]
for var in op.output_arg_names:
if var not in var_infos:
var_infos[var] = [idx, False]
n = len(block.ops)
insert_idx_and_kwargs = []
for group_idx, kwargs in enumerate(coalesce_ops_kwargs):
all_vars = kwargs["inputs"]["Input"] + kwargs["outputs"]["Output"]
min_op_idx = n
copy_data = False
for var in all_vars:
if var not in var_infos:
copy_data = True
min_idx = 0
break
op_idx, is_input = var_infos[var]
if is_input:
copy_data = True
min_op_idx = min(min_op_idx, op_idx)
kwargs["attrs"]["copy_data"] = copy_data
insert_idx_and_kwargs.append((min_op_idx, kwargs))
insert_idx_and_kwargs.sort(key=lambda element: element[0], reverse=True)
for idx, kwargs in insert_idx_and_kwargs:
block._insert_op_without_sync(idx, **kwargs)
def insert_fuse_all_reduce_by_memory_size(block, groups, max_memory_size):
op_role_key = core.op_proto_and_checker_maker.kOpRoleAttrName()
op_role_var_key = core.op_proto_and_checker_maker.kOpRoleVarAttrName()
op_device_key = core.op_proto_and_checker_maker.kOpDeviceAttrName()
coalesce_ops_kwargs = []
for group in reversed(groups):
first_op = block.ops[group[0]]
ring_id = first_op.attr("ring_id")
use_calc_stream = first_op.attr("use_calc_stream")
use_model_parallel = first_op.attr("use_model_parallel")
op_role = first_op.attr(op_role_key)
op_device = first_op.attr(op_device_key)
attrs = {
"ring_id": ring_id,
"use_calc_stream": use_calc_stream,
"use_model_parallel": use_model_parallel,
op_role_key: op_role,
op_device_key: op_device,
}
dtype = block._find_var_recursive(first_op.input("X")[0]).dtype
sizeof = core.size_of_dtype(dtype)
cur_mem_size = 0
op_role_vars = []
recorded_op_indices = []
in_var_names = []
out_var_names = []
for op_idx in reversed(group):
op = block.ops[op_idx]
in_var_name = op.input("X")[0]
out_var_name = op.output("Out")[0]
in_var = block._find_var_recursive(in_var_name)
mem_size = int(np.prod(in_var.shape)) * sizeof
if cur_mem_size + mem_size > max_memory_size:
if len(recorded_op_indices) > 1:
attrs[op_role_var_key] = op_role_vars
coalesce_op_kwargs = insert_fuse_all_reduce_ops(
block,
recorded_op_indices,
in_var_names,
out_var_names,
dtype,
attrs,
)
coalesce_ops_kwargs.append(coalesce_op_kwargs)
cur_mem_size = 0
op_role_vars = []
recorded_op_indices = []
in_var_names = []
out_var_names = []
cur_mem_size += mem_size
recorded_op_indices.append(op_idx)
in_var_names.append(in_var_name)
out_var_names.append(out_var_name)
if op.has_attr(op_role_var_key):
op_role_vars.extend(op.attr(op_role_var_key))
if len(recorded_op_indices) > 1:
attrs[op_role_var_key] = op_role_vars
coalesce_op_kwargs = insert_fuse_all_reduce_ops(
block,
recorded_op_indices,
in_var_names,
out_var_names,
dtype,
attrs,
)
coalesce_ops_kwargs.append(coalesce_op_kwargs)
block._sync_with_cpp()
insert_coalesce_tensor_ops(block, coalesce_ops_kwargs)
@register_pass("fuse_all_reduce")
class FuseAllReducePass(PassBase):
def __init__(self):
super().__init__()
self.set_attr("max_memory_size", -1)
def _check_self(self):
max_memory_size = self.get_attr("max_memory_size")
return max_memory_size > 0
def _check_conflict(self, other_pass):
return True
def _type(self):
return PassType.COMM_OPT
# NOTE: why FuseAllReducePass can override apply_single_impl instead of
# apply_impl? AllReduce is a collective operation, so the program of each
# rank inside the same communication group should have the same
# all_reduce sum operations. Therefore, FuseAllReducePass can override
# apply_single_impl directly.
def _apply_single_impl(self, main_program, startup_program, context):
max_memory_size = self.get_attr("max_memory_size")
op_deps = main_program.desc.get_op_deps()
num_blocks = main_program.num_blocks
for i in range(num_blocks):
block = main_program.block(i)
groups = find_all_fuse_all_reduce_groups(block)
groups = split_fuse_all_reduce_groups_by_deps(
block, groups, op_deps[i]
)
insert_fuse_all_reduce_by_memory_size(
block, groups, max_memory_size
)
main_program._sync_with_cpp()
+378
View File
@@ -0,0 +1,378 @@
# 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 abc import ABC, abstractmethod
from paddle.framework import _apply_pass as _apply_cpp_pass
class PassContext:
def __init__(self):
self._applied_passes = []
self._attrs = {}
def set_attr(self, key, value):
self._attrs[key] = value
def get_attr(self, key, default=None):
return self._attrs.get(key, default)
@property
def passes(self):
return self._applied_passes
def _add_pass(self, pass_obj):
self._applied_passes.append(pass_obj)
def _pop_pass(self):
del self._applied_passes[-1]
class PassType:
UNKNOWN = 0
COMM_OPT = 1
CALC_OPT = 2
PARALLEL_OPT = 3
FUSION_OPT = 4
class PassBase(ABC):
_REGISTERED_PASSES = {}
_COMMON_RULES = []
_BEFORE_WHITE_LISTS_DICT = {}
_AFTER_WHITE_LISTS_DICT = {}
_PASS_PROCESS_ORDER_LIST = []
name = None
@staticmethod
def _register(pass_name, pass_class):
assert issubclass(pass_class, PassBase)
PassBase._REGISTERED_PASSES[pass_name] = pass_class
def __init__(self):
self._attrs = {}
def set_attr(self, key, value):
self._attrs[key] = value
return self
def get_attr(self, key, default=None):
return self._attrs.get(key, default)
@abstractmethod
def _check_self(self):
pass
@abstractmethod
def _check_conflict(self, other_pass):
pass
def _type(self):
return PassType.UNKNOWN
def _check_conflict_including_common_rules(self, other_pass):
return self._check_conflict(other_pass) and all(
r(other_pass, self) for r in PassBase._COMMON_RULES
)
def apply(self, main_programs, startup_programs, context=None):
if context is None:
context = PassContext()
if not self._check_self():
return context
if not all(
self._check_conflict_including_common_rules(p)
for p in context.passes
):
return context
assert isinstance(main_programs, list)
assert isinstance(startup_programs, list)
assert len(main_programs) == len(startup_programs)
self._apply_impl(main_programs, startup_programs, context)
context._add_pass(self)
return context
def _apply_impl(self, main_programs, startup_programs, context):
for main_program, startup_program in zip(
main_programs, startup_programs
):
self._apply_single_impl(main_program, startup_program, context)
@abstractmethod
def _apply_single_impl(self, main_program, startup_program, context):
pass
def register_pass(name):
def impl(cls):
PassBase._register(name, cls)
cls.name = name
return cls
return impl
def new_pass(name, pass_attrs={}):
pass_class = PassBase._REGISTERED_PASSES.get(name)
assert pass_class is not None, f"Pass {name} is not registered"
pass_obj = pass_class()
for k, v in pass_attrs.items():
pass_obj.set_attr(k, v)
return pass_obj
class CPPPassWrapper(PassBase):
def __init__(self):
super().__init__()
@property
def cpp_name(self):
raise NotImplementedError
@property
def cpp_attr_types(self):
return {}
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, context):
_apply_cpp_pass(
main_program,
startup_program,
self.cpp_name,
self._attrs,
self.cpp_attr_types,
)
def _fusion_opt_last_rule(pass_before, pass_after):
if (
pass_before._type() == PassType.FUSION_OPT
and pass_after._type() != PassType.FUSION_OPT
):
return False
else:
return True
def _fusion_opt_list_rule(pass_before, pass_after):
if (
pass_before._type() == PassType.FUSION_OPT
and pass_after._type() == PassType.FUSION_OPT
):
return _get_list_index(pass_before) < _get_list_index(pass_after)
else:
return True
def _make_rule_from_white_lists_dict(
before_white_lists_dict, after_white_lists_dict
):
def collect_pass_names(white_lists_dict, result):
for k, v in white_lists_dict.items():
result.add(k)
assert isinstance(v, (list, tuple))
for pass_name in v:
assert isinstance(pass_name, (bytes, str))
result.add(pass_name)
all_pass_names = set()
collect_pass_names(before_white_lists_dict, all_pass_names)
collect_pass_names(after_white_lists_dict, all_pass_names)
compatible_pass_dict = {}
for pass_name in all_pass_names:
compatible_pass_dict[pass_name] = set()
for k, v in before_white_lists_dict.items():
for pass_name in v:
compatible_pass_dict[k].add(pass_name)
for k, v in after_white_lists_dict.items():
for pass_name in v:
compatible_pass_dict[pass_name].add(k)
def rule(pass_before, pass_after):
all_passes_after = compatible_pass_dict.get(pass_before.name)
if (
all_passes_after is None
or pass_after.name not in compatible_pass_dict
):
return True
else:
return pass_after.name in all_passes_after
return rule
def _get_list_index(in_pass):
assert in_pass.name in PassBase._PASS_PROCESS_ORDER_LIST, (
f"Pass {in_pass.name} is not in _PASS_PROCESS_ORDER_LIST"
)
return PassBase._PASS_PROCESS_ORDER_LIST.index(in_pass.name)
# The key-value pair (k, [v1, v2, ..., vn]) means the pass k can be
# applied before any of pass [v1, v2, ..., vn] is applied
PassBase._BEFORE_WHITE_LISTS_DICT = {
"fuse_gradient_merge": ["fuse_all_reduce"],
# Add more white lists here
}
# The key-value pair (k, [v1, v2, ..., vn]) means the pass k can be
# applied after any of pass [v1, v2, ..., vn] is applied
PassBase._AFTER_WHITE_LISTS_DICT = {
# Add more white lists here
}
# The index of pass in this list represent the order in which the pass is processed.
PassBase._PASS_PROCESS_ORDER_LIST = [
"fuse_resunit",
"fuse_relu_depthwise_conv",
"fuse_bn_add_act",
"fuse_bn_act",
"fused_attention",
"fused_feedforward",
"fuse_gemm_epilogue",
"fuse_adamw",
"fuse_optimizer",
]
PassBase._COMMON_RULES = [
_fusion_opt_last_rule,
_fusion_opt_list_rule,
lambda pass_before, pass_after: type(pass_before) != type(pass_after),
_make_rule_from_white_lists_dict(
PassBase._BEFORE_WHITE_LISTS_DICT, PassBase._AFTER_WHITE_LISTS_DICT
),
# Add more common rules here
]
def _find_longest_path(edges):
n = len(edges)
paths = [None] * n
dists = [None] * n
min_path = []
min_dist = 0
for i in range(n):
paths[i] = [None] * n
dists[i] = [None] * n
for j in range(n):
assert isinstance(edges[i][j], bool)
if not edges[i][j]:
dists[i][j] = n # inf
paths[i][j] = []
else:
assert edges[i][j] is True
dists[i][j] = -1
paths[i][j] = [i, j]
if dists[i][j] < min_dist:
min_dist = -1
min_path = paths[i][j]
for k in range(n):
for i in range(n):
for j in range(n):
if dists[i][j] > dists[i][k] + dists[k][j]:
dists[i][j] = dists[i][k] + dists[k][j]
if paths[i][k]:
assert paths[i][k][-1] == k
else:
continue
if paths[k][j]:
assert paths[k][j][0] == k
else:
continue
paths[i][j] = (
paths[i][k] + paths[k][j][1:] if paths[k][j] else []
)
if dists[i][j] < min_dist:
min_dist = dists[i][j]
min_path = paths[i][j]
return min_path if min_path else [0]
def _solve_pass_conflict(passes, context):
passes = [p for p in passes if p._check_self()]
if not passes:
return []
old_passes = passes
passes = []
for p in old_passes:
if all(
p._check_conflict_including_common_rules(applied_p)
for applied_p in context.passes
):
passes.append(p)
if not passes:
return []
n = len(passes)
adjacent_matrix = []
for _ in range(n):
adjacent_matrix.append([None] * n)
for i in range(n):
for j in range(n):
adjacent_matrix[i][j] = passes[
j
]._check_conflict_including_common_rules(passes[i])
longest_path = _find_longest_path(adjacent_matrix)
return [passes[idx] for idx in longest_path]
class PassManager:
def __init__(self, passes, context=None, auto_solve_conflict=True):
if context is None:
context = PassContext()
self._context = context
if auto_solve_conflict:
self._passes = _solve_pass_conflict(passes, context)
else:
self._passes = list(passes)
def apply(self, main_programs, startup_programs):
context = self._context
for p in self._passes:
context = p.apply(main_programs, startup_programs, context)
self._context = context
return context
@property
def context(self):
return self._context
@property
def names(self):
return [p.name for p in self.passes]
@property
def passes(self):
return tuple(self._passes)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,54 @@
# 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 os
from ..pass_base import PassContext, new_pass
from .pipeline_1f1b import Pipeline1F1BPass # noqa: F401
from .pipeline_eager_1f1b import PipelineEager1F1BPass # noqa: F401
from .pipeline_fthenb import PipelineFThenBPass # noqa: F401
from .pipeline_vpp import PipelineVirtualPipelinePass # noqa: F401
from .pipeline_zero_bubble import (
PipelineZeroBubblePipelinePass, # noqa: F401
PipelineZeroBubbleVirtualPipelinePass, # noqa: F401
)
__all__ = []
def apply_pass(main_program, startup_program, pass_name, pass_attr={}):
assert pass_name in [
"FThenB",
"1F1B",
"Eager1F1B",
"VPP",
"ZBH1",
"ZBVPP",
], (
f"pipeline scheduler only support FThenB, 1F1B, Eager1F1B, VPP and ZBH1, but receive {pass_name}"
)
if pass_name == "1F1B":
# TODO(Ruibiao): Move FLAGS_1f1b_backward_forward_overlap and
# FLAGS_mp_async_allreduce_in_backward to auto parallel Strategy
# after these two optimizations are available.
pass_attr["enable_backward_forward_overlap"] = int(
os.environ.get("FLAGS_1f1b_backward_forward_overlap", 0)
)
pipeline_pass = new_pass("pipeline_scheduler_" + pass_name, pass_attr)
pass_context = PassContext()
pipeline_pass.apply([main_program], [startup_program], pass_context)
plan = pass_context.get_attr("plan")
return plan
@@ -0,0 +1,341 @@
# 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
from paddle.base import core
from paddle.framework import (
_current_expected_place_ as _get_device,
)
from ...utils.log_utils import get_logger
from ..pass_base import register_pass
from ..pass_utils import (
AutoParallelStreamType,
forward_complete_op_role,
split_program,
)
from .pipeline_pass_base import PipelinePassBase
logger = get_logger(logging.INFO)
@register_pass("pipeline_scheduler_1F1B")
class Pipeline1F1BPass(PipelinePassBase):
def __init__(self):
super().__init__()
self.jobs_in_stable_phase = [self.BACKWARD, self.FORWARD]
self.jobs_in_stable_phase_in_pir = [
self.BACKWARD,
self.RECV_FORWARD,
self.SEND_BACKWARD,
self.FORWARD,
]
self.set_attr("enable_backward_forward_overlap", 0)
def _create_job_list(self):
if self._in_pir_mode:
return self._create_job_list_in_pir()
else:
raise NotImplementedError(
"_create_job_list() only support PIR now."
)
def _create_job_list_in_pir(self):
num_micro_batches = self.get_attr("num_micro_batches")
pp_stage = self.get_attr("pp_stage")
pp_degree = self.get_attr("pp_degree")
job_list = []
assert pp_degree <= num_micro_batches, (
"Num of micro batches should larger than or equal to pp degree."
)
micro_batch_in_warmup = pp_degree - pp_stage
micro_batch_in_1f1b = num_micro_batches - micro_batch_in_warmup
forward_micro_batch_id = 0
for i in range(micro_batch_in_warmup):
recv_fwd_job = core.Job(self.RECV_FORWARD)
recv_fwd_job.set_micro_batch_id(forward_micro_batch_id)
job_list.append(recv_fwd_job)
forward_job = core.Job(self.FORWARD)
forward_job.set_micro_batch_id(forward_micro_batch_id)
job_list.append(forward_job)
forward_micro_batch_id += 1
backward_micro_batch_id = 0
for i in range(micro_batch_in_1f1b):
for job_type in self.jobs_in_stable_phase_in_pir:
job = core.Job(job_type)
micro_batch_id = (
forward_micro_batch_id
if job_type.startswith(self.FORWARD)
or job_type.startswith(self.RECV_FORWARD)
else backward_micro_batch_id
)
job.set_micro_batch_id(micro_batch_id)
job_list.append(job)
forward_micro_batch_id += 1
backward_micro_batch_id += 1
for i in range(micro_batch_in_warmup):
backward_job = core.Job(self.BACKWARD)
backward_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_job)
send_bwd_job = core.Job(self.SEND_BACKWARD)
send_bwd_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(send_bwd_job)
backward_micro_batch_id += 1
opt_job = core.Job(self.OPT)
opt_job.set_micro_batch_id(0)
job_list.append(opt_job)
return job_list
def _partial_programs(self, program):
raise NotImplementedError("pipeline_1f1b_pass() only support PIR now.")
def _partial_pir_programs(self, program):
enable_send_recv_overlap = self.get_attr("enable_send_recv_overlap")
assert not enable_send_recv_overlap, (
"PIR does not support 1F1B with enable_send_recv_overlap yet."
)
self._overlap_send_recv(program)
forward_complete_op_role(program)
job_types = [
self.RECV_FORWARD,
self.FORWARD,
self.BACKWARD,
self.SEND_BACKWARD,
self.OPT,
]
programs = {}
for job_type in job_types:
programs[job_type] = program.clone()
complete_ops = program.global_block().ops
ops_dict = {
key: prog.global_block().ops for key, prog in programs.items()
}
blocks_dict = {
key: prog.global_block() for key, prog in programs.items()
}
region = "opt"
for op_idx in range(len(complete_ops) - 1, -1, -1):
op = complete_ops[op_idx]
if op.op_role != -1:
if op.op_role == 1:
region = "bwd"
elif op.op_role == 0:
region = "fwd"
elif op.op_role == 2:
region = "opt"
if region == "opt":
self._erase_op_from_other_programs(
op_idx, self.OPT, ops_dict, job_types
)
elif region == "bwd" and op.name() == "pd_op.send_v2":
self._handle_func(
op_idx,
self.SEND_BACKWARD,
job_types[4:],
complete_ops,
ops_dict,
blocks_dict,
)
self._erase_op_from_other_programs(
op_idx, self.SEND_BACKWARD, ops_dict, job_types
)
elif region == "bwd" and op.name() != "pd_op.send_v2":
self._handle_func(
op_idx,
self.BACKWARD,
job_types[3:],
complete_ops,
ops_dict,
blocks_dict,
)
self._erase_op_from_other_programs(
op_idx, self.BACKWARD, ops_dict, job_types
)
elif region == "fwd" and op.name() != "pd_op.recv_v2":
self._handle_func(
op_idx,
self.FORWARD,
job_types[2:],
complete_ops,
ops_dict,
blocks_dict,
)
self._erase_op_from_other_programs(
op_idx, self.FORWARD, ops_dict, job_types
)
elif region == "fwd" and op.name() == "pd_op.recv_v2":
self._handle_func(
op_idx,
self.RECV_FORWARD,
job_types[1:],
complete_ops,
ops_dict,
blocks_dict,
)
self._erase_op_from_other_programs(
op_idx, self.RECV_FORWARD, ops_dict, job_types
)
sub_program_list = []
for job_type in job_types:
sub_program_list.append(programs[job_type])
for i in range(len(job_types)):
logger.debug(
f"type = {job_types[i]}, sub_programs = {sub_program_list[i]}\n"
)
logger.debug(
f"jobs_in_stable_phase = {self.jobs_in_stable_phase_in_pir}"
)
return job_types, sub_program_list
def _split_program_for_overlapping(self, job_type, program, split_points):
assert job_type in [
self.FORWARD,
self.BACKWARD,
], f"job_type should be one of {[self.FORWARD, self.BACKWARD]}"
split_programs, __, __ = split_program(program, split_points)
split_job_types = []
num_split_programs = len(split_programs)
for idx in range(num_split_programs):
split_job_types.append(f"{job_type}(chunk{idx})")
return split_job_types, split_programs
def is_comm_op_valid_to_overlap(self, op):
return (
op.type == "all_reduce"
and op.attr("reduce_type") == paddle.distributed.ReduceOp.SUM
and op.dist_attr.execution_stream
== AutoParallelStreamType.CALC_STREAM.value
)
def _handle_func(
self,
op_idx,
cur_job_type,
suffixed_job_types,
complete_ops,
ops_dict,
blocks_dict,
):
for idx in range(complete_ops[op_idx].num_results()):
if self._result_is_used(suffixed_job_types, op_idx, idx, ops_dict):
var_name = self._get_or_create_var_name(
ops_dict[cur_job_type], op_idx, idx, complete_ops
)
for job_type in suffixed_job_types:
if self._result_is_used([job_type], op_idx, idx, ops_dict):
self._add_dependency_if_necessary(
ops_dict, cur_job_type, job_type, op_idx, idx, var_name
)
self._add_kwarg_and_replace(
blocks_dict[job_type],
ops_dict[job_type],
op_idx,
idx,
var_name,
)
def _result_is_used(self, job_types, op_idx, rst_idx, ops_dict):
is_used = False
for job_type in job_types:
is_used = (
is_used
or ops_dict[job_type][op_idx].result(rst_idx).use_empty()
is False
)
return is_used
def _get_or_create_var_name(
self, cur_sub_ops, op_idx, rst_idx, complete_ops
):
var_name = None
# case1: get var_name in current sub-program
op = cur_sub_ops[op_idx]
if op.name() == "pd_op.data" or op.name() == "builtin.parameter":
var_name = op.result(rst_idx).name
else:
# case2: get var_name from shadow_output in complete program
result_var = complete_ops[op_idx].result(rst_idx)
shadow_output_op = None
for used_op in result_var.all_used_ops():
if used_op.name() == "builtin.shadow_output":
shadow_output_op = used_op
if shadow_output_op is not None:
var_name = shadow_output_op.attrs()["output_name"]
if var_name is None:
# case3: create var_name in current sub-program
paddle.pir.set_insertion_point_after(op)
var_name = f"var_{op_idx}_{complete_ops[op_idx].name()}_{rst_idx}"
paddle._C_ops.set_persistable_value(op.result(rst_idx), var_name)
return var_name
def _add_kwarg_and_replace(self, block, ops, op_idx, rst_idx, var_name):
ori_result = ops[op_idx].result(rst_idx)
new_result_var = block.add_kwarg(var_name, ori_result.type())
new_result_var.place_attr = self._get_cur_place()
new_result_var.persistable = ori_result.persistable
ops[op_idx].result(rst_idx).replace_all_uses_with(new_result_var)
def _overlap_send_recv(self, program):
for block in program.blocks:
for op in block.ops:
if op.name() == "pd_op.send_v2":
op.set_bool_attr("dynamic_shape", False)
op.set_bool_attr("use_calc_stream", True)
ring_id = op.attrs()["ring_id"]
op.set_execution_stream("send_recv_stream")
op.set_scheduling_priority(0)
elif op.name() == "pd_op.recv_v2":
op.set_bool_attr("dynamic_shape", False)
op.set_bool_attr("use_calc_stream", True)
op.set_execution_stream("send_recv_stream")
op.set_scheduling_priority(0)
def _erase_op_from_other_programs(
self, op_idx, keep_job_type, ops_dict, job_types
):
for job_type in job_types:
if job_type != keep_job_type:
ops_dict[job_type][op_idx].erase()
def _get_cur_place(self):
place = _get_device()
if isinstance(place, paddle.framework.CUDAPlace):
place = paddle.framework.CUDAPlace(
paddle.distributed.ParallelEnv().dev_id
)
cur_place = paddle.base.libpaddle.Place()
cur_place.set_place(place)
return cur_place
@@ -0,0 +1,73 @@
# 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 import core
from ...utils.log_utils import get_logger
from ..pass_base import register_pass
from .pipeline_pass_base import PipelinePassBase
logger = get_logger(logging.INFO)
@register_pass("pipeline_scheduler_Eager1F1B")
class PipelineEager1F1BPass(PipelinePassBase):
def __init__(self):
super().__init__()
def _create_job_list(self):
num_micro_batches = self.get_attr("num_micro_batches")
pp_stage = self.get_attr("pp_stage")
pp_degree = self.get_attr("pp_degree")
job_list = []
assert 2 * (pp_degree - pp_stage) - 1 <= num_micro_batches, (
"Num of micro batches should larger than 2 * (pp_degree - pp_stage) - 1."
)
micro_batch_in_warmup = 2 * (pp_degree - pp_stage) - 1
micro_batch_in_1f1b = num_micro_batches - micro_batch_in_warmup
forward_micro_batch_id = 0
for _ in range(micro_batch_in_warmup):
forward_job = core.Job(self.FORWARD)
forward_job.set_micro_batch_id(forward_micro_batch_id)
job_list.append(forward_job)
forward_micro_batch_id += 1
backward_micro_batch_id = 0
for _ in range(micro_batch_in_1f1b):
backward_job = core.Job(self.BACKWARD)
backward_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_job)
backward_micro_batch_id += 1
forward_job = core.Job(self.FORWARD)
forward_job.set_micro_batch_id(forward_micro_batch_id)
job_list.append(forward_job)
forward_micro_batch_id += 1
for _ in range(micro_batch_in_warmup):
backward_job = core.Job(self.BACKWARD)
backward_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_job)
backward_micro_batch_id += 1
opt_job = core.Job(self.OPT)
job_list.append(opt_job)
return job_list
def _partial_programs(self, program):
raise NotImplementedError("Not support old IR for Eager1f1b")
@@ -0,0 +1,66 @@
# 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 import core
from ...utils.log_utils import get_logger
from ..pass_base import register_pass
from ..pass_utils import (
_split_program_into_forward_backward_optimize,
)
from .pipeline_pass_base import PipelinePassBase
logger = get_logger(logging.INFO)
@register_pass("pipeline_scheduler_FThenB")
class PipelineFThenBPass(PipelinePassBase):
def __init__(self):
super().__init__()
def _create_job_list(self):
num_micro_batches = self.get_attr("num_micro_batches")
job_list = []
for i in range(num_micro_batches):
forward_job = core.Job(self.FORWARD)
forward_job.set_micro_batch_id(i)
job_list.append(forward_job)
for i in range(num_micro_batches):
backward_job = core.Job(self.BACKWARD)
backward_job.set_micro_batch_id(i)
job_list.append(backward_job)
opt_job = core.Job(self.OPT)
opt_job.set_micro_batch_id(0)
job_list.append(opt_job)
return job_list
def _partial_programs(self, program):
raise NotImplementedError(
"pipeline_fthenb_pass() only support PIR now."
)
def _partial_pir_programs(self, program):
# NOTE: The flag "enable_send_recv_overlap" may increase the reserved memory of GPUs.
enable_send_recv_overlap = self.get_attr("enable_send_recv_overlap")
types = [self.FORWARD, self.BACKWARD, self.OPT]
sub_program_list = _split_program_into_forward_backward_optimize(
program, enable_send_recv_overlap
)
return types, sub_program_list
@@ -0,0 +1,157 @@
# 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 paddle
from paddle.base import core
from ...utils.log_utils import get_logger
from ..pass_base import PassBase
from ..pass_utils import (
set_skip_gc_vars,
)
logger = get_logger(logging.INFO)
class PipelinePassBase(PassBase):
# Pipeline stages
RECV_FORWARD = "recv_forward"
SEND_BACKWARD = "send_backward"
FORWARD = "forward"
BACKWARD = "backward"
OPT = "optimizer"
def __init__(self):
super().__init__()
self._in_pir_mode = paddle.base.framework.get_flags(
"FLAGS_enable_pir_api"
)["FLAGS_enable_pir_api"]
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _create_job_list(self):
"""
An interface that MUST be implemented by subclasses.
"""
pass
def _partial_programs(self, program):
"""
An interface that MUST be implemented by subclasses.
The return value MUST be two lists, one is a list of types(str), another
is a list of sub programs.
For example:
return [FORWARD, BACKWARD, OPT], [fwd_prog, bwd_prog, opt_prog]
or
return [FORWARD], [fwd_prog]
"""
pass
def _apply_impl(self, main_programs, startup_programs, context):
for main_program, startup_program in zip(
main_programs, startup_programs
):
if self._in_pir_mode:
self._apply_pir_single_impl(
main_program, startup_program, context
)
else:
self._apply_single_impl(main_program, startup_program, context)
def _partial_pir_programs(self, program):
"""
An interface that MUST be implemented by subclasses.
The return value MUST be two lists, one is a list of types(str), another
is a list of sub programs.
For example:
return [FORWARD, BACKWARD, OPT], [fwd_prog, bwd_prog, opt_prog]
or
return [FORWARD], [fwd_prog]
"""
pass
def _apply_single_impl(self, main_program, startup_program, context):
"""
The shared process is implemented in this function and new subclass only need
to implement two interfaces above, 'create_job_list' and 'partial_programs'.
"""
raise NotImplementedError("Not support for old IR")
def _apply_pir_single_impl(self, main_program, startup_program, context):
"""
The shared process is implemented in this function and new subclass only need
to implement two interfaces above, 'create_job_list' and 'partial_programs'.
"""
job_types, sub_programs = self._partial_pir_programs(main_program)
for i in range(len(job_types)):
logger.debug(
f"sub_program type: {job_types[i]}, sub_program:\n{sub_programs[i]}"
)
jobs = self._create_job_list()
type_to_program = set_skip_gc_vars(
self.get_attr("num_micro_batches"), job_types, sub_programs, jobs
)
plan = core.Plan(jobs, type_to_program)
context.set_attr("plan", plan)
def _add_dependency(self, recorder_op, waiter_op, name):
'''
Add the extra event dependency of the two operators.
This function mainly aims for the cross-programs in pipeline parallelism,
especial for the 'send_v2' 'recv_v2' etc.
'''
if not recorder_op.has_attr("force_record_event"):
recorder_op.set_bool_attr("force_record_event", True)
recorder_op.set_str_attr("event_to_record", name)
waiter_op.set_str_array_attr("events_to_wait", [name])
def _add_dependency_if_necessary(
self,
type_to_ops,
cur_job_type,
next_job_type,
op_idx,
rst_idx,
var_name,
):
if not (
("backward" in cur_job_type and "send_backward" in next_job_type)
or ("recv_forward" in cur_job_type and "forward" in next_job_type)
):
return
first_used_idx = None
first_used_op = None
for used_op in (
type_to_ops[next_job_type][op_idx].result(rst_idx).all_used_ops()
):
used_idx = type_to_ops[next_job_type].index(used_op)
if first_used_idx is None or used_idx < first_used_idx:
first_used_idx = used_idx
first_used_op = used_op
if first_used_op is not None:
self._add_dependency(
type_to_ops[cur_job_type][op_idx], first_used_op, var_name
)
@@ -0,0 +1,603 @@
# 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 collections import OrderedDict
import paddle
from paddle.base import core
from ...auto_parallel.static.utils import OpRole
from ...utils.log_utils import get_logger
from ..pass_base import register_pass
from ..pass_utils import (
_create_program_and_ops,
_get_device,
_pir_get_backward_op_type,
_pir_overlap_send_recv,
_pir_split_matmul_grad_to_matmul,
infer_chunk_id,
)
from .pipeline_pass_base import PipelinePassBase
logger = get_logger(logging.INFO)
@register_pass("pipeline_scheduler_VPP")
class PipelineVirtualPipelinePass(PipelinePassBase):
def __init__(self):
super().__init__()
self._real_overlap_sharding_reduce = False
self.reduce_comm_suffix = "_reduce"
self._forward_micro_step_counter = {}
self._backward_micro_step_counter = {}
self.jobs_in_stable_phase_in_pir = [
self.BACKWARD,
self.RECV_FORWARD,
self.SEND_BACKWARD,
self.FORWARD,
]
def _record_fwd_micro_step(self, virtual_pp_rank):
real_micro_step = self._forward_micro_step_counter[virtual_pp_rank]
self._forward_micro_step_counter[virtual_pp_rank] += 1
return real_micro_step
def _record_bwd_micro_step(self, virtual_pp_rank):
real_micro_step = self._backward_micro_step_counter[virtual_pp_rank]
self._backward_micro_step_counter[virtual_pp_rank] += 1
return real_micro_step
def _create_job_list(self):
if self._in_pir_mode:
return self._pir_create_job_list()
accumulate_steps = self.get_attr("num_micro_batches")
stage_id = self.get_attr("pp_stage")
num_stages = self.get_attr("pp_degree")
num_model_chunks = self.get_attr("vpp_degree")
split_backward = self.get_attr("split_backward", False)
remainder = accumulate_steps % num_stages
for i in range(num_model_chunks):
self._forward_micro_step_counter[i] = 0
self._backward_micro_step_counter[i] = 0
assert accumulate_steps >= num_stages
def _get_virtual_pp_rank(micro_step, forward):
virtual_pp_stage = micro_step % (num_stages * num_model_chunks)
if micro_step <= (accumulate_steps // num_stages) * (
num_stages * num_model_chunks
):
virtual_pp_stage = virtual_pp_stage // num_stages
else:
virtual_pp_stage = virtual_pp_stage // remainder
if not forward:
virtual_pp_stage = num_model_chunks - virtual_pp_stage - 1
return virtual_pp_stage
total_num_steps = accumulate_steps * num_model_chunks
if accumulate_steps == num_stages:
warmup_steps = total_num_steps
else:
warmup_steps = (num_stages - stage_id - 1) * 2
warmup_steps += (num_model_chunks - 1) * num_stages
warmup_steps = min(warmup_steps, total_num_steps)
steady_steps = total_num_steps - warmup_steps
real_split_backward = (
accumulate_steps == num_stages
) and split_backward
job_list = []
for micro_step in range(warmup_steps):
virtual_pp_rank = _get_virtual_pp_rank(micro_step, forward=True)
micro_batch_id = self._record_fwd_micro_step(virtual_pp_rank)
fw_job = core.Job(self.FORWARD + str(virtual_pp_rank))
fw_job.set_micro_batch_id(micro_batch_id)
job_list.append(fw_job)
for micro_step in range(steady_steps):
fwd_micro_step = micro_step + warmup_steps
fwd_virtual_pp_rank = _get_virtual_pp_rank(
fwd_micro_step, forward=True
)
fwd_micro_batch_id = self._record_fwd_micro_step(
fwd_virtual_pp_rank
)
fwd_job = core.Job(self.FORWARD + str(fwd_virtual_pp_rank))
fwd_job.set_micro_batch_id(fwd_micro_batch_id)
job_list.append(fwd_job)
bw_micro_step = micro_step
bwd_virtual_pp_rank = _get_virtual_pp_rank(
bw_micro_step, forward=False
)
bwd_micro_batch_id = self._record_bwd_micro_step(
bwd_virtual_pp_rank
)
if real_split_backward:
bwd_job = core.Job(
self.BACKWARD + "_b" + str(bwd_virtual_pp_rank)
)
else:
bwd_job = core.Job(self.BACKWARD + str(bwd_virtual_pp_rank))
bwd_job.set_micro_batch_id(bwd_micro_batch_id)
job_list.append(bwd_job)
for micro_step in range(steady_steps, total_num_steps):
virtual_pp_rank = _get_virtual_pp_rank(micro_step, forward=False)
micro_batch_id = self._record_bwd_micro_step(virtual_pp_rank)
if real_split_backward:
bwd_job = core.Job(self.BACKWARD + "_b" + str(virtual_pp_rank))
else:
bwd_job = core.Job(self.BACKWARD + str(virtual_pp_rank))
bwd_job.set_micro_batch_id(micro_batch_id)
job_list.append(bwd_job)
# TODO(lizhiyu): Inserting 'backward_b' and 'backward_w' interleavedly can decrease the memory,
# but it reduces the speed. We should find the better way to use the code here.
# next_virtual_pp_rank = _get_virtual_pp_rank(micro_step + 1, forward=False)
# if next_virtual_pp_rank != virtual_pp_rank:
# for micro_batch_id in range(0, accumulate_steps):
# w_job = core.Job(BACKWARD + "_w" + str(virtual_pp_rank))
# w_job.set_micro_batch_id(micro_batch_id)
# job_list.append(w_job)
if real_split_backward:
for chunk_id in range(num_model_chunks - 1, -1, -1):
for micro_batch_id in range(0, accumulate_steps):
if (
self._real_overlap_sharding_reduce
and micro_batch_id == accumulate_steps - 1
):
w_job = core.Job(
self.BACKWARD
+ "_w"
+ str(chunk_id)
+ self.reduce_comm_suffix
)
else:
w_job = core.Job(self.BACKWARD + "_w" + str(chunk_id))
w_job.set_micro_batch_id(micro_batch_id)
job_list.append(w_job)
job_types = [job.type() for job in job_list]
logger.debug(f"The VPP job list: {job_types}")
opt_job = core.Job(self.OPT)
job_list.append(opt_job)
return job_list
def _pir_create_job_list(self):
accumulate_steps = self.get_attr("num_micro_batches")
stage_id = self.get_attr("pp_stage")
num_stages = self.get_attr("pp_degree")
num_model_chunks = self.get_attr("vpp_degree")
split_backward = self.get_attr("split_backward", False)
remainder = accumulate_steps % num_stages
for i in range(num_model_chunks):
self._forward_micro_step_counter[i] = 0
self._backward_micro_step_counter[i] = 0
assert accumulate_steps >= num_stages
def _get_virtual_pp_rank(micro_step, forward):
virtual_pp_stage = micro_step % (num_stages * num_model_chunks)
if micro_step <= (accumulate_steps // num_stages) * (
num_stages * num_model_chunks
):
virtual_pp_stage = virtual_pp_stage // num_stages
else:
virtual_pp_stage = virtual_pp_stage // remainder
if not forward:
virtual_pp_stage = num_model_chunks - virtual_pp_stage - 1
return virtual_pp_stage
total_num_steps = accumulate_steps * num_model_chunks
if accumulate_steps == num_stages:
warmup_steps = total_num_steps
else:
warmup_steps = (num_stages - stage_id - 1) * 2
warmup_steps += (num_model_chunks - 1) * num_stages
warmup_steps = min(warmup_steps, total_num_steps)
real_split_backward = (
accumulate_steps == num_stages
) and split_backward
if not real_split_backward:
warmup_steps = min(total_num_steps, warmup_steps + 1)
steady_steps = total_num_steps - warmup_steps
job_list = []
for micro_step in range(warmup_steps):
virtual_pp_rank = _get_virtual_pp_rank(micro_step, forward=True)
micro_batch_id = self._record_fwd_micro_step(virtual_pp_rank)
if not real_split_backward:
recv_fwd_job = core.Job(
self.RECV_FORWARD + str(virtual_pp_rank)
)
recv_fwd_job.set_micro_batch_id(micro_batch_id)
job_list.append(recv_fwd_job)
fw_job = core.Job(self.FORWARD + str(virtual_pp_rank))
fw_job.set_micro_batch_id(micro_batch_id)
job_list.append(fw_job)
if real_split_backward:
for micro_step in range(steady_steps):
fwd_micro_step = micro_step + warmup_steps
fwd_virtual_pp_rank = _get_virtual_pp_rank(
fwd_micro_step, forward=True
)
fwd_micro_batch_id = self._record_fwd_micro_step(
fwd_virtual_pp_rank
)
fwd_job = core.Job(self.FORWARD + str(fwd_virtual_pp_rank))
fwd_job.set_micro_batch_id(fwd_micro_batch_id)
job_list.append(fwd_job)
bw_micro_step = micro_step
bwd_virtual_pp_rank = _get_virtual_pp_rank(
bw_micro_step, forward=False
)
bwd_micro_batch_id = self._record_bwd_micro_step(
bwd_virtual_pp_rank
)
bwd_job = core.Job(
self.BACKWARD + "_b" + str(bwd_virtual_pp_rank)
)
bwd_job.set_micro_batch_id(bwd_micro_batch_id)
job_list.append(bwd_job)
else:
for micro_step in range(steady_steps):
fwd_micro_step = micro_step + warmup_steps
fwd_virtual_pp_rank = _get_virtual_pp_rank(
fwd_micro_step, forward=True
)
fwd_micro_batch_id = self._record_fwd_micro_step(
fwd_virtual_pp_rank
)
bw_micro_step = micro_step
bwd_virtual_pp_rank = _get_virtual_pp_rank(
bw_micro_step, forward=False
)
bwd_micro_batch_id = self._record_bwd_micro_step(
bwd_virtual_pp_rank
)
for job_type in self.jobs_in_stable_phase_in_pir:
if job_type.startswith(self.FORWARD) or job_type.startswith(
self.RECV_FORWARD
):
job = core.Job(job_type + str(fwd_virtual_pp_rank))
job.set_micro_batch_id(fwd_micro_batch_id)
else:
job = core.Job(job_type + str(bwd_virtual_pp_rank))
job.set_micro_batch_id(bwd_micro_batch_id)
job_list.append(job)
for micro_step in range(steady_steps, total_num_steps):
virtual_pp_rank = _get_virtual_pp_rank(micro_step, forward=False)
micro_batch_id = self._record_bwd_micro_step(virtual_pp_rank)
if real_split_backward:
bwd_job = core.Job(self.BACKWARD + "_b" + str(virtual_pp_rank))
bwd_job.set_micro_batch_id(micro_batch_id)
job_list.append(bwd_job)
else:
bwd_job = core.Job(self.BACKWARD + str(virtual_pp_rank))
send_bwd_job = core.Job(
self.SEND_BACKWARD + str(virtual_pp_rank)
)
bwd_job.set_micro_batch_id(micro_batch_id)
send_bwd_job.set_micro_batch_id(micro_batch_id)
job_list.append(bwd_job)
job_list.append(send_bwd_job)
# TODO(lizhiyu): Inserting 'backward_b' and 'backward_w' interleavedly can decrease the memory,
# but it reduces the speed. We should find the better way to use the code here.
# next_virtual_pp_rank = _get_virtual_pp_rank(micro_step + 1, forward=False)
# if next_virtual_pp_rank != virtual_pp_rank:
# for micro_batch_id in range(0, accumulate_steps):
# w_job = core.Job(BACKWARD + "_w" + str(virtual_pp_rank))
# w_job.set_micro_batch_id(micro_batch_id)
# job_list.append(w_job)
if real_split_backward:
for chunk_id in range(num_model_chunks - 1, -1, -1):
for micro_batch_id in range(0, accumulate_steps):
if (
self._real_overlap_sharding_reduce
and micro_batch_id == accumulate_steps - 1
):
w_job = core.Job(
self.BACKWARD
+ "_w"
+ str(chunk_id)
+ self.reduce_comm_suffix
)
else:
w_job = core.Job(self.BACKWARD + "_w" + str(chunk_id))
w_job.set_micro_batch_id(micro_batch_id)
job_list.append(w_job)
job_types = [job.type() for job in job_list]
logger.debug(f"The VPP job list: {job_types}")
opt_job = core.Job(self.OPT)
job_list.append(opt_job)
return job_list
def _pir_split_matmul_grad_ops_to_matmul(self, program):
for block in program.blocks:
matmul_grad_op_idx = []
ops = block.ops
for i, op_i in enumerate(ops):
if (
op_i.name() == "pd_op.matmul_grad"
and not op_i.has_attr("trans_x")
and not op_i.has_attr("trans_y")
):
matmul_grad_op_idx.append(i)
for matmul_grad_id in reversed(matmul_grad_op_idx):
_pir_split_matmul_grad_to_matmul(block, matmul_grad_id)
def _partial_programs(self, program):
raise RuntimeError("Not support old IR for VPP")
def _partial_pir_programs(self, program):
num_model_chunks = self.get_attr("vpp_degree")
enable_send_recv_overlap = self.get_attr("enable_send_recv_overlap")
split_backward = self.get_attr("split_backward", False)
accumulate_steps = self.get_attr("num_micro_batches")
num_stages = self.get_attr("pp_degree")
if accumulate_steps != num_stages:
split_backward = False
assert not enable_send_recv_overlap, (
"PIR does not support VPP with enable_send_recv_overlap yet."
)
if split_backward:
self._pir_split_matmul_grad_ops_to_matmul(program)
types, sub_program_list = self._pir_program_for_vpp(
program, num_model_chunks, split_backward, enable_send_recv_overlap
)
for i in range(len(types)):
logger.debug(
f"type = {types[i]}, sub_programs = {sub_program_list[i]}\n"
)
return types, sub_program_list
def _pir_program_for_vpp(
self,
program,
num_model_chunks,
split_bw=False,
enable_send_recv_overlap=False,
):
_pir_overlap_send_recv(program)
oprole_names = [
"recv_forward",
"forward",
"backward",
"send_backward",
"optimizer",
]
if split_bw:
oprole_names = ["forward", "backward_b", "backward_w", "optimizer"]
program_types, programs = self._split_program_for_vpp(
program, num_model_chunks, oprole_names, split_bw=split_bw
)
return program_types, programs
def _split_program_for_vpp(
self, program, num_model_chunks, oprole_names, split_bw=False
):
place = _get_device()
if isinstance(place, paddle.framework.CUDAPlace):
place = paddle.framework.CUDAPlace(
paddle.distributed.ParallelEnv().dev_id
)
cur_place = paddle.base.libpaddle.Place()
cur_place.set_place(place)
def get_var_name(op_idx, result_idx):
result_value = all_ops[op_idx].result(result_idx)
all_used_ops = result_value.all_used_ops()
shadow_output_op_used = None
for op in all_used_ops:
if op.name() == "builtin.shadow_output":
shadow_output_op_used = op
if shadow_output_op_used is not None:
var_name = shadow_output_op_used.attrs()["output_name"]
else:
var_name = f"var_{op_idx}_{all_ops[op_idx].name()}_{result_idx}"
return var_name
def add_persistable_var(op_idx, program_type):
all_program_types = list(type_to_program.keys())
following_program_types = all_program_types[
all_program_types.index(program_type) + 1 :
]
op_num_results = type_to_ops[program_type][op_idx].num_results()
op_name = type_to_ops[program_type][op_idx].name()
for idx in range(op_num_results):
var_name = None
for type in reversed(following_program_types):
op_result = type_to_ops[type][op_idx].result(idx)
if op_result.use_empty():
continue
# if this op's output is used, create the persistable
# var to be used in other programs.
if var_name is None:
if op_name in ["pd_op.data", "builtin.parameter"]:
var_name = op_result.name
else:
var_name = get_var_name(op_idx, idx)
if "var_" in var_name:
paddle.pir.set_insertion_point_after(
type_to_ops[program_type][op_idx]
)
paddle._C_ops.set_persistable_value(
type_to_ops[program_type][op_idx].result(
idx
),
var_name,
)
self._add_dependency_if_necessary(
type_to_ops, program_type, type, op_idx, idx, var_name
)
program_block = type_to_program[type].global_block()
new_result_var = program_block.add_kwarg(
var_name, op_result.type()
)
new_result_var.place_attr = cur_place
new_result_var.persistable = op_result.persistable
type_to_ops[type][op_idx].result(idx).replace_all_uses_with(
new_result_var
)
for type in following_program_types:
type_to_ops[type][op_idx].erase()
type_to_program = OrderedDict()
type_to_ops = OrderedDict()
# Step1: create programs and ops for each type
if not split_bw:
chunk_ids = list(range(num_model_chunks))
# Forward process
for chunk_id in chunk_ids:
for job_type in ["recv_forward", "forward"]:
name, prog, ops = _create_program_and_ops(
program, job_type, chunk_id
)
type_to_program[name] = prog
type_to_ops[name] = ops
# Backward process
for chunk_id in reversed(chunk_ids):
for job_type in ["backward", "send_backward"]:
name, prog, ops = _create_program_and_ops(
program, job_type, chunk_id
)
type_to_program[name] = prog
type_to_ops[name] = ops
# Optimizer
name, prog, ops = _create_program_and_ops(program, "optimizer")
type_to_program[name] = prog
type_to_ops[name] = ops
else:
for type in oprole_names:
if type == "optimizer":
type_to_program["optimizer"] = program.clone()
type_to_ops["optimizer"] = (
type_to_program["optimizer"].global_block().ops
)
else:
chunk_ids = list(range(num_model_chunks))
if "backward" in type:
chunk_ids.reverse()
for chunk_id in chunk_ids:
type_to_program[type + str(chunk_id)] = program.clone()
type_to_ops[type + str(chunk_id)] = (
type_to_program[type + str(chunk_id)]
.global_block()
.ops
)
# Step2: delete the ops not belong to the type
# 1. delete ops
# 2. add persistable var used between multiple programs
all_ops = program.global_block().ops
chunk_ids = list(range(num_model_chunks))
bwd_pattern_ops_type = []
for idx in range(len(all_ops) - 1, -1, -1):
op = all_ops[idx]
op_role = op.op_role
op_chunk_id = op.chunk_id
# Step2.1: infer chunk_id for ops that don't have chunk_id
if op_role != int(OpRole.Optimize) and op_chunk_id == -1:
op_chunk_id = infer_chunk_id(idx, all_ops, False)
if op_chunk_id == -1:
raise ValueError(
f"Cannot infer chunk_id for op {op.name()} at index {idx}"
)
# Step2.2: identify the job_type of the op
if op_role == int(OpRole.Optimize):
job_type = "optimizer"
elif op_role == int(OpRole.Backward) and split_bw:
if len(bwd_pattern_ops_type) == 0:
bwd_pattern_ops_type = _pir_get_backward_op_type(
all_ops, idx
)
job_type = bwd_pattern_ops_type.pop()
elif op_role == int(OpRole.Backward) and (not split_bw):
if op.name() == "pd_op.send_v2":
job_type = "send_backward"
else:
job_type = "backward"
elif op_role == int(OpRole.Forward):
if op.name() == "pd_op.recv_v2" and (not split_bw):
job_type = "recv_forward"
else:
job_type = "forward"
else:
raise ValueError(
f"The op[{op.name()}]'s op role: {op_role} isn't one of recv_forward, forward, backward, send_backward or Optimizer."
)
# Step2.3: delete ops not belong to the type
if not split_bw:
current_type = (
job_type
if job_type == "optimizer"
else job_type + str(op_chunk_id)
)
# Get the position of the current type in type_to_program
all_types = list(type_to_ops.keys())
current_idx = all_types.index(current_type)
# Delete all ops before the current type
for type_name in all_types[:current_idx]:
type_to_ops[type_name][idx].erase()
else:
for type in oprole_names:
if type == job_type:
break
if type != "optimizer":
for chunk_id in chunk_ids:
type_to_ops[type + str(chunk_id)][idx].erase()
else:
type_to_ops[type][idx].erase()
chunk_order = range(0, op_chunk_id)
if "backward" in job_type:
chunk_order = range(num_model_chunks - 1, op_chunk_id, -1)
for chunk_id in chunk_order:
type_to_ops[job_type + str(chunk_id)][idx].erase()
# Step2.4: add persistable var used between multiple programs
if job_type != "optimizer":
add_persistable_var(idx, job_type + str(op_chunk_id))
return list(type_to_program.keys()), list(type_to_program.values())
@@ -0,0 +1,903 @@
# 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 collections import deque
from paddle.base import core
from ...utils.log_utils import get_logger
from ..pass_base import register_pass
from .pipeline_pass_base import PipelinePassBase
logger = get_logger(logging.INFO)
class PipelineZeroBubbleBase(PipelinePassBase):
def __init__(self):
super().__init__()
self.set_attr("enable_optimizer_post_validation", 0)
@register_pass("pipeline_scheduler_ZBH1")
class PipelineZeroBubblePipelinePass(PipelineZeroBubbleBase):
def __init__(self):
super().__init__()
def _create_job_list(self):
num_micro_batches = self.get_attr("num_micro_batches")
pp_stage = self.get_attr("pp_stage")
pp_degree = self.get_attr("pp_degree")
job_list = []
assert pp_degree <= num_micro_batches, (
"Num of micro batches should larger than or equal to pp degree."
)
micro_batch_in_warmup = pp_degree - pp_stage
micro_batch_in_zero_bubble = num_micro_batches - pp_degree
forward_micro_batch_id = 0
for _ in range(micro_batch_in_warmup):
forward_job = core.Job(self.FORWARD)
forward_job.set_micro_batch_id(forward_micro_batch_id)
job_list.append(forward_job)
forward_micro_batch_id += 1
backward_micro_batch_id = 0
for _ in range(pp_stage):
backward_b_job = core.Job(self.BACKWARD + "_b")
backward_b_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_b_job)
backward_micro_batch_id += 1
forward_job = core.Job(self.FORWARD)
forward_job.set_micro_batch_id(forward_micro_batch_id)
job_list.append(forward_job)
forward_micro_batch_id += 1
for _ in range(micro_batch_in_zero_bubble):
backward_job = core.Job(self.BACKWARD)
backward_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_job)
forward_job = core.Job(self.FORWARD)
forward_job.set_micro_batch_id(forward_micro_batch_id)
job_list.append(forward_job)
forward_micro_batch_id += 1
backward_micro_batch_id += 1
for _ in range(micro_batch_in_warmup - 1):
backward_job = core.Job(self.BACKWARD)
backward_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_job)
backward_micro_batch_id += 1
if pp_stage > 0:
backward_b_job = core.Job(self.BACKWARD + "_b")
backward_b_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_b_job)
backward_w_job = core.Job(self.BACKWARD + "_w")
backward_w_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_w_job)
else:
backward_job = core.Job(self.BACKWARD)
backward_job.set_micro_batch_id(backward_micro_batch_id)
job_list.append(backward_job)
backward_micro_batch_id += 1
for i in range(pp_stage):
backward_w_job = core.Job(self.BACKWARD + "_w")
backward_w_job.set_micro_batch_id(i)
job_list.append(backward_w_job)
opt_job = core.Job(self.OPT)
opt_job.set_micro_batch_id(0)
job_list.append(opt_job)
return job_list
def _partial_programs(self, program):
raise NotImplementedError("Not support old IR for ZeroBubble")
@register_pass("pipeline_scheduler_ZBVPP")
class PipelineZeroBubbleVirtualPipelinePass(PipelineZeroBubblePipelinePass):
def __init__(self):
super().__init__()
self.set_attr("enable_optimizer_post_validation", 0)
self.set_attr("program_runtimes", [61, 72, 71, 34, 3])
self.set_attr("memory_limit_times", -1)
self.program_mem_usages = []
self.program_max_mem_usages = []
self.base_memory = []
self.program_runtime = {}
def _create_job_list(self):
pp_degree = self.get_attr("pp_degree")
num_micro_batches = self.get_attr("num_micro_batches")
num_model_chunks = self.get_attr("vpp_degree")
assert num_micro_batches % pp_degree == 0
# TODO(luchang): Fix the gradient explosion issue when num_model_chunks(accumulate steps) > pp_degree
assert num_micro_batches <= pp_degree, (
"zbvpp now only supports accumulate steps <= pp degree. It will cause gradient exploitation when accumulate steps > pp degree."
)
program_runtimes = self.get_attr("program_runtimes")
self.program_runtime = {
"forward": program_runtimes[0],
"backward_b": program_runtimes[1],
"backward_w": program_runtimes[2],
"loss": program_runtimes[3],
"communication": program_runtimes[4],
}
v_scheduler = VScheduleCreator(
pp_degree,
num_micro_batches,
num_model_chunks,
self.program_mem_usages,
self.program_max_mem_usages,
self.base_memory,
self.program_runtime,
self._get_max_memory(),
)
schedule, end_time = None, None
for fill_w_before_b in [True, False]:
for fill_w_before_f in [True, False]:
for fill_loss_stage in [True, False]:
if schedule is None:
schedule, end_time, _ = v_scheduler.create_v_schedule(
fill_w_before_b=fill_w_before_b,
fill_w_before_f=fill_w_before_f,
fill_loss_stage=fill_loss_stage,
)
else:
(
new_schedule,
new_end_time,
_,
) = v_scheduler.create_v_schedule(
fill_w_before_b=fill_w_before_b,
fill_w_before_f=fill_w_before_f,
fill_loss_stage=fill_loss_stage,
)
if max(new_end_time) < max(end_time):
schedule, end_time = new_schedule, new_end_time
stage_schedule = schedule[self.get_attr("pp_stage")]
job_list = []
for job_info in stage_schedule:
job = core.Job(f"{job_info['type']}{job_info['chunk']}")
job.set_micro_batch_id(job_info["micro_batch"])
job_list.append(job)
opt_job = core.Job(self.OPT)
opt_job.set_micro_batch_id(0)
job_list.append(opt_job)
return job_list
def _partial_programs(self, program):
raise NotImplementedError("Not support old IR for ZeroBubbleVPP")
class VScheduleCreator:
def __init__(
self,
num_stage,
num_micro_batch,
num_model_chunks,
program_mem_usages,
program_max_mem_usages,
base_memory,
program_runtime,
max_memory=None,
):
self.num_stage = num_stage
self.num_micro_batch = num_micro_batch
self.num_model_chunks = num_model_chunks
self.num_nodes = num_model_chunks * num_stage * num_micro_batch * 3
self.program_mem_usages = program_mem_usages
self.program_max_mem_usages = program_max_mem_usages
self.job_types = ["forward", "backward_w", "backward_b"]
self.program_runtime = program_runtime
self.base_memory = base_memory
self.max_memory = max_memory
if max_memory is None:
self.max_memory = float("inf")
self.calculate_loss_stage = (
0 if num_model_chunks % 2 == 0 else num_stage - 1
)
def init_schedule(self):
job_counter = {}
for job_type in self.job_types:
for chunk_id in range(self.num_model_chunks):
job_counter[f"{job_type}{chunk_id}"] = 0
self._job_counters = [job_counter.copy() for _ in range(self.num_stage)]
self._job_end_times = [-1] * self.num_nodes
self._stage_current_time = [0] * self.num_stage
self._stage_mem_usage = self.base_memory.copy()
self._pending_w = [deque() for _ in range(self.num_stage)]
self._stage_job_schedule = [[] for _ in range(self.num_stage)]
self._stage_bubbles = [0] * self.num_stage
def create_v_schedule(
self,
fill_w_before_f=True,
fill_w_before_b=True,
fill_loss_stage=True,
approved_bubbles=None,
):
self.init_schedule()
if approved_bubbles is None:
approved_bubbles = [-1] * self.num_stage
max_approved_bubble = max(approved_bubbles)
self._insert_forward_jobs_before_forward1()
self._insert_forward_jobs_before_backward_b()
self._insert_jobs_after_backward_start(
fill_w_before_f, fill_w_before_b, fill_loss_stage, approved_bubbles
)
schedule = self._stage_job_schedule.copy()
end_time = self._job_end_times.copy()
max_bubble = self._get_max_stage_bubble()
if max_approved_bubble < 0 or max_bubble < max_approved_bubble:
new_schedule, new_end_time, new_max_bubble = self.create_v_schedule(
fill_w_before_f,
fill_w_before_b,
fill_loss_stage,
self._stage_bubbles,
)
if max(new_end_time) < max(end_time):
return new_schedule, new_end_time, new_max_bubble
return schedule, end_time, max_bubble
def _insert_forward_jobs_before_forward1(self):
# Step1: Insert forward jobs with chunk_id=0 into the schedule
for i in range(self.num_stage):
self._put_job_into_schedule("forward", chunk_id=0, stage_id=i)
# Step2: Insert forward jobs with chunk_id=1 into the schedule
self._fill_forward_before_one_job(
"forward", 1, [0], forward_insert_order="down"
)
def _insert_forward_jobs_before_backward_b(self):
chunk_to_insert_order = {
0: "down",
1: "up",
}
# Insert the rest chunk_id forward jobs
for chunk_id in range(2, self.num_model_chunks):
fill_chunk_ids = list(range(0, chunk_id))
forward_insert_order = chunk_to_insert_order[fill_chunk_ids[-1] % 2]
self._fill_forward_before_one_job(
"forward", chunk_id, fill_chunk_ids, forward_insert_order
)
# Insert forward jobs to fill the bubble before backward_b0
fill_chunk_ids = list(range(0, self.num_model_chunks))
forward_insert_order = chunk_to_insert_order[fill_chunk_ids[-1] % 2]
self._fill_forward_before_one_job(
"backward_b",
0,
list(range(0, self.num_model_chunks)),
forward_insert_order,
insert_end_point_job=False,
)
def _fill_forward_before_one_job(
self,
end_point_job_type,
end_point_chunk_id,
fill_chunk_ids,
forward_insert_order,
insert_end_point_job=True,
):
stage_order = list(range(self.num_stage))
if forward_insert_order == "down":
stage_order.reverse()
stage_last_job = self._stage_job_schedule[stage_order[0]][-1]
end_point_job_start_time = self._job_end_times[
self._get_job_id(
stage_last_job["type"],
stage_last_job["chunk"],
stage_order[0],
stage_last_job["micro_batch"],
)
]
if insert_end_point_job:
self._put_job_into_schedule(
end_point_job_type, end_point_chunk_id, stage_order[0]
)
for stage_id in stage_order[1:]:
stage_last_job = self._stage_job_schedule[stage_id][-1]
start_fill_chunk = (stage_last_job["chunk"] + 1) % len(
fill_chunk_ids
)
end_point_job_start_time = (
end_point_job_start_time
+ self.program_runtime["communication"]
+ self._get_program_runtime(
end_point_job_type, stage_id, end_point_chunk_id
)
)
self._fill_bubble_with_forward(
stage_id,
fill_chunk_ids,
start_fill_chunk,
end_point_job_start_time,
insert_order=forward_insert_order,
)
if insert_end_point_job:
self._put_job_into_schedule(
end_point_job_type, end_point_chunk_id, stage_id
)
def _insert_jobs_after_backward_start(
self,
fill_w_before_f,
fill_w_before_b,
fill_loss_stage,
approved_bubbles,
):
backward_b_job_number = self.num_model_chunks * self.num_micro_batch
first_backward_b_stage = (
0 if self.num_model_chunks % 2 == 0 else self.num_stage - 1
)
while (
self._get_stage_backward_b_number(first_backward_b_stage)
< backward_b_job_number
):
# Step1: Check memory usage, if not enough, put pending backward_w job into schedule
for stage_id in range(self.num_stage):
while not self._memory_check("backward_b", 0, stage_id):
if len(self._pending_w[stage_id]) == 0:
raise ValueError(
f"No pending backward_w job and backward_b0 job exceeds the memory limit at stage {stage_id}."
)
self._put_w_job_into_schedule(stage_id)
# Step2: Insert backward_b job for each stage
# b_ranks = [[] for _ in range(self.num_model_chunks)]
backward_insert_order = range(self.num_stage)
if self.num_model_chunks % 2:
backward_insert_order = range(self.num_stage - 1, -1, -1)
for stage_id in backward_insert_order:
for chunk_id in range(0, self.num_model_chunks):
if self._can_schedule_b_task(stage_id, chunk_id):
dependency_job_end_time = (
self._get_dependency_job_end_time(
"backward_b",
chunk_id,
stage_id,
self._job_counters[stage_id][
f"backward_b{chunk_id}"
],
)
)
while len(
self._pending_w[stage_id]
) and dependency_job_end_time + self.program_runtime[
"communication"
] >= self._stage_current_time[
stage_id
] + self._get_program_runtime(
"backward_w",
stage_id,
self._pending_w[stage_id][0][0],
):
self._put_w_job_into_schedule(stage_id)
if (
stage_id == self.calculate_loss_stage
and fill_loss_stage
):
while (
len(self._pending_w[stage_id])
and dependency_job_end_time
+ self.program_runtime["communication"]
>= self._stage_current_time[stage_id]
+ self._get_program_runtime(
"backward_w",
stage_id,
self._pending_w[stage_id][0][0],
)
* 0.2
):
self._put_w_job_into_schedule(stage_id)
max_stage_bubble = self._get_max_stage_bubble(
stage_id, approved_bubbles
)
stage_bubble = self._stage_bubbles[stage_id]
if (
len(self._pending_w[stage_id])
and dependency_job_end_time
+ self.program_runtime["communication"]
- self._stage_current_time[stage_id]
> max_stage_bubble - stage_bubble
):
if fill_w_before_b:
self._put_w_job_into_schedule(stage_id)
self._put_job_into_schedule(
"backward_b", chunk_id, stage_id
)
break
# Step3: Insert forward jobs after backward_b
forward_insert_order = range(self.num_stage)
if self.num_model_chunks % 2:
forward_insert_order = range(self.num_stage - 1, -1, -1)
for stage_id in forward_insert_order:
for chunk_id in range(self.num_model_chunks - 1, -1, -1):
if self._can_schedule_f_task(stage_id, chunk_id):
while (
self._stage_mem_usage[stage_id]
+ self.program_max_mem_usages[stage_id][
f"forward{chunk_id}"
]
> self.max_memory
):
if len(self._pending_w[stage_id]) == 0:
raise ValueError(
"No pending backward_w job and forward job exceeds the memory limit."
)
self._put_w_job_into_schedule(stage_id)
dependency_job_end_time = (
self._get_dependency_job_end_time(
"forward",
chunk_id,
stage_id,
self._job_counters[stage_id][
f"forward{chunk_id}"
],
)
)
while len(
self._pending_w[stage_id]
) and dependency_job_end_time + self.program_runtime[
"communication"
] >= self._stage_current_time[
stage_id
] + self._get_program_runtime(
"backward_w",
stage_id,
self._pending_w[stage_id][0][0],
):
self._put_w_job_into_schedule(stage_id)
max_stage_bubble = self._get_max_stage_bubble(
stage_id, approved_bubbles
)
stage_bubble = self._stage_bubbles[stage_id]
if (
len(self._pending_w[stage_id])
and dependency_job_end_time
+ self.program_runtime["communication"]
- self._stage_current_time[stage_id]
> max_stage_bubble - stage_bubble
):
if fill_w_before_f:
self._put_w_job_into_schedule(stage_id)
self._put_job_into_schedule(
"forward", chunk_id, stage_id
)
break
for stage_id in range(self.num_stage):
while len(self._pending_w[stage_id]):
self._put_w_job_into_schedule(stage_id)
def _can_schedule_f_task(self, stage_id, chunk_id):
return self._can_schedule_task("forward", chunk_id, stage_id)
def _can_schedule_b_task(self, stage_id, chunk_id):
return self._can_schedule_task("backward_b", chunk_id, stage_id)
def _can_schedule_task(self, job_type, chunk_id, stage_id):
if job_type == "forward":
current_key = f"forward{chunk_id}"
prev_key = f"forward{chunk_id - 1}"
elif job_type == "backward_b":
current_key = f"backward_b{chunk_id}"
prev_chunk_id = chunk_id + 1
if prev_chunk_id >= self.num_model_chunks:
prev_key = f"forward{self.num_model_chunks - 1}"
else:
prev_key = f"backward_b{chunk_id + 1}"
micro_batch_id = self._job_counters[stage_id][current_key]
if micro_batch_id >= self.num_micro_batch:
return False
if (job_type == "forward" and chunk_id > 0) or (
job_type == "backward_b"
):
prev_chunk_count = self._job_counters[stage_id][prev_key]
current_chunk_count = self._job_counters[stage_id][current_key]
if prev_chunk_count <= current_chunk_count:
return False
prev_stage_job_end_time = self._get_dependency_job_end_time(
job_type, chunk_id, stage_id, micro_batch_id
)
if prev_stage_job_end_time < 0:
return False
return True
def _get_stage_micro_batch_id(self, stage_id, job_type):
micro_batch_id = 0
for chunk_id in range(self.num_model_chunks):
micro_batch_id += self._job_counters[stage_id][
f"{job_type}{chunk_id}"
]
return micro_batch_id
def _fill_bubble_with_forward(
self,
stage_id,
chunk_ids,
start_chunk_id,
next_job_start_time,
insert_order="down",
):
chunk_id = start_chunk_id
less_forward_number = 0
stage_order = range(0, stage_id + 1)
if insert_order == "up":
less_forward_number = self._get_stage_forward_number(0, chunk_ids)
stage_order = range(self.num_stage - 1, stage_id - 1, -1)
while self._check_before_insert(
chunk_ids, stage_order, next_job_start_time, less_forward_number
):
available_memory = self.max_memory - self._stage_mem_usage[stage_id]
# After insert forward job, we need to check whether we can insert backward_b job
available_memory -= self.program_max_mem_usages[stage_id][
f"backward_b{chunk_id}"
]
# Check whether we can insert all chunk_id forward jobs
for i in range(1, self.num_model_chunks):
if self._job_counters[stage_id][f"forward{i}"] == 0:
available_memory -= self.program_max_mem_usages[stage_id][
f"forward{i}"
]
if (
available_memory
< self.program_max_mem_usages[stage_id][f"forward{chunk_id}"]
):
break
for i in stage_order:
if self._can_schedule_f_task(i, chunk_id):
stage_forward_number = self._get_stage_forward_number(
i, chunk_ids
)
if stage_forward_number >= less_forward_number:
if not self._time_check(
"forward", chunk_id, i, next_job_start_time
):
continue
self._put_job_into_schedule("forward", chunk_id, i)
chunk_id = (chunk_id + 1) % len(chunk_ids)
def _check_before_insert(
self, chunk_ids, stage_order, next_job_start_time, less_forward_number
):
stage_id = stage_order[-1]
if (
self._get_stage_forward_number(stage_id, chunk_ids)
< less_forward_number
):
return True
job_numbers = []
for chunk_id in chunk_ids:
job_numbers.append(
self._job_counters[stage_id][f"forward{chunk_id}"]
)
micro_batch_id_check = False
for number in job_numbers:
if number < self.num_micro_batch:
micro_batch_id_check = True
break
can_insert = False
for chunk_id in chunk_ids:
for i in stage_order:
if self._can_schedule_f_task(i, chunk_id):
stage_forward_number = self._get_stage_forward_number(
i, chunk_ids
)
if stage_forward_number >= less_forward_number:
if not self._time_check(
"forward", chunk_id, i, next_job_start_time
):
continue
can_insert = True
break
return (
self._memory_check("forward", chunk_id, stage_id)
and micro_batch_id_check
and can_insert
)
def _memory_check(self, job_type, chunk_id, stage_id):
if (
self._stage_mem_usage[stage_id]
+ self.program_max_mem_usages[stage_id][f"{job_type}{chunk_id}"]
> self.max_memory
):
return False
return True
def _time_check(self, job_type, chunk_id, stage_id, next_job_start_time):
dependency_job_end_time = self._get_dependency_job_end_time(
job_type,
chunk_id,
stage_id,
self._job_counters[stage_id][f"{job_type}{chunk_id}"],
)
job_end_time = max(
self._stage_current_time[stage_id],
dependency_job_end_time + self.program_runtime["communication"],
) + self._get_program_runtime(job_type, stage_id, chunk_id)
if job_end_time > next_job_start_time:
return False
return True
def _micro_batch_id_check(self, job_type, chunk_id, stage_id):
if (
self._job_counters[stage_id][f"{job_type}{chunk_id}"]
>= self.num_micro_batch
):
return False
return True
def _put_job_into_schedule(
self,
job_type,
chunk_id,
stage_id,
):
program_runtime = self._get_program_runtime(
job_type, stage_id, chunk_id
)
task_end_time = self._stage_current_time[stage_id] + program_runtime
micro_batch_id = self._job_counters[stage_id][f"{job_type}{chunk_id}"]
if micro_batch_id >= self.num_micro_batch:
raise ValueError(
f"Job {job_type}{chunk_id} exceeds the limit of micro batches."
)
if (
self._stage_mem_usage[stage_id]
+ self.program_max_mem_usages[stage_id][f"{job_type}{chunk_id}"]
> self.max_memory
):
raise ValueError(
f"Job {job_type}{chunk_id} exceeds the memory limit at stage {stage_id}."
)
self._check_job_chunk_order(
job_type, chunk_id, stage_id, micro_batch_id
)
if job_type in ["forward", "backward_b"]:
dependency_job_end_time = self._get_dependency_job_end_time(
job_type, chunk_id, stage_id, micro_batch_id
)
if dependency_job_end_time < 0:
prev_stage_id = self._get_prev_stage_id(
job_type, chunk_id, stage_id
)
raise ValueError(
f"Job {job_type}{chunk_id}_{micro_batch_id} at stage {stage_id} depends on unscheduled job {job_type}{chunk_id}_{micro_batch_id} at stage {prev_stage_id}."
)
task_end_time = max(
task_end_time,
dependency_job_end_time
+ self.program_runtime["communication"]
+ program_runtime,
)
job_id = self._get_job_id(job_type, chunk_id, stage_id, micro_batch_id)
if self._job_counters[stage_id]["forward0"] > 0:
self._stage_bubbles[stage_id] += (
task_end_time
- self._stage_current_time[stage_id]
- program_runtime
)
self._job_end_times[job_id] = task_end_time
self._stage_current_time[stage_id] = task_end_time
self._stage_mem_usage[stage_id] += self.program_mem_usages[stage_id][
f"{job_type}{chunk_id}"
]
job_info = {
"type": job_type,
"chunk": chunk_id,
"micro_batch": micro_batch_id,
}
self._stage_job_schedule[stage_id].append(job_info)
if job_type == "backward_b":
self._pending_w[stage_id].append((chunk_id, micro_batch_id))
self._job_counters[stage_id][f"{job_type}{chunk_id}"] += 1
def _put_w_job_into_schedule(self, stage_id):
if not len(self._pending_w[stage_id]):
raise ValueError("No pending backward_w job.")
chunk_id, _ = self._pending_w[stage_id].popleft()
self._put_job_into_schedule("backward_w", chunk_id, stage_id)
def _check_job_chunk_order(
self, job_type, chunk_id, stage_id, micro_batch_id
):
if job_type == "forward":
if chunk_id > 0:
prev_job_end_time = self._job_end_times[
self._get_job_id(
"forward", chunk_id - 1, stage_id, micro_batch_id
)
]
if prev_job_end_time < 0:
raise ValueError(
f"Job {job_type}{chunk_id}_{micro_batch_id} depends on unfinished {job_type}{chunk_id - 1}_{micro_batch_id} job."
)
elif job_type == "backward_b":
if chunk_id < self.num_model_chunks - 1:
prev_job_end_time = self._job_end_times[
self._get_job_id(
job_type, chunk_id + 1, stage_id, micro_batch_id
)
]
if prev_job_end_time < 0:
raise ValueError(
f"Job {job_type}{chunk_id}_{micro_batch_id} depends on unfinished {job_type}{chunk_id + 1}_{micro_batch_id} job."
)
elif job_type == "backward_w":
prev_job_id = self._get_job_id(
"backward_b", chunk_id, stage_id, micro_batch_id
)
if self._job_end_times[prev_job_id] < 0:
raise ValueError(
f"Job {job_type}{chunk_id}_{micro_batch_id} at stage {stage_id} depends on unfinished backward_b{chunk_id}_{micro_batch_id} job."
)
def _get_dependency_job_end_time(
self, job_type, chunk_id, stage_id, micro_batch_id
):
prev_stage_id = self._get_prev_stage_id(job_type, chunk_id, stage_id)
if prev_stage_id < 0 or prev_stage_id >= self.num_stage:
return 0
prev_stage_job_id = self._get_job_id(
job_type, chunk_id, prev_stage_id, micro_batch_id
)
prev_job_end_time = self._job_end_times[prev_stage_job_id]
return prev_job_end_time
def _get_prev_stage_id(self, job_type, chunk_id, stage_id):
if job_type == "forward":
if chunk_id % 2:
return stage_id + 1
else:
return stage_id - 1
elif job_type in ["backward_b", "backward_w"]:
if chunk_id % 2:
return stage_id - 1
else:
return stage_id + 1
def _get_max_stage_bubble(self, stage_id=-1, approved_bubbles=None):
max_stage_bubble = max(self._stage_bubbles)
if stage_id >= 0:
max_approved_bubble = max(approved_bubbles)
max_stage_bubble = max(
max_stage_bubble,
max_approved_bubble - approved_bubbles[stage_id],
)
return max_stage_bubble
def _get_job_id(self, job_type, chunk_id, stage_id, job_micro_id):
return (
self.job_types.index(job_type)
* self.num_model_chunks
* self.num_stage
* self.num_micro_batch
+ chunk_id * self.num_stage * self.num_micro_batch
+ stage_id * self.num_micro_batch
+ job_micro_id
)
def _get_bubble_rate(self):
max_bubble = self._get_max_stage_bubble()
fbw_cost = (
self.program_runtime["forward"]
+ self.program_runtime["backward_w"]
+ self.program_runtime["communication"]
)
expected_time = fbw_cost * self.num_micro_batch * self.num_model_chunks
bubble_rate = max_bubble / expected_time
return bubble_rate
def _get_stage_forward_number(self, stage, chunk_ids=None):
job_number = 0
if chunk_ids is None:
chunk_ids = range(self.num_model_chunks)
for chunk_id in chunk_ids:
job_number += self._job_counters[stage][f"forward{chunk_id}"]
return job_number
def _get_stage_backward_b_number(self, stage, chunk_ids=None):
job_number = 0
if chunk_ids is None:
chunk_ids = range(self.num_model_chunks)
for chunk_id in chunk_ids:
job_number += self._job_counters[stage][f"backward_b{chunk_id}"]
return job_number
def _get_program_runtime(self, job_type, stage_id, chunk_id):
program_runtime = self.program_runtime[job_type]
if job_type == "communication":
return program_runtime
if (
stage_id == self.calculate_loss_stage
and chunk_id == self.num_model_chunks - 1
):
program_runtime += self.program_runtime["loss"]
return program_runtime
+274
View File
@@ -0,0 +1,274 @@
# 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 paddle
from paddle.optimizer.lr import (
ExponentialDecay,
InverseTimeDecay,
LRScheduler,
NaturalExpDecay,
NoamDecay,
exponential_decay,
inverse_time_decay,
noam_decay,
)
from ..ps.utils.public import (
get_optimize_ops,
get_ps_endpoint,
get_role_id,
get_trainers,
)
from .pass_base import PassBase, register_pass
@register_pass("add_lr_decay_table_pass")
class AddLrDecayTablePass(PassBase):
def __init__(self):
super().__init__()
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _add_tensor_table(
self,
attrs,
feed_var_name,
fetch_var_name="",
startup_program=None,
main_program=None,
tensor_table_class="",
):
tensor_table_dict = {}
tensor_table_dict[feed_var_name] = {}
tensor_table_dict[feed_var_name]["feed_var_name"] = feed_var_name
tensor_table_dict[feed_var_name]["fetch_var_name"] = fetch_var_name
tensor_table_dict[feed_var_name]["startup_program"] = startup_program
tensor_table_dict[feed_var_name]["main_program"] = main_program
tensor_table_dict[feed_var_name]["tensor_table_class"] = (
tensor_table_class
)
attrs['tensor_table'] = tensor_table_dict
def _get_lr_scheduler_program(self, lr_scheduler, lr_decay_steps):
scheduler_decay = [
'NoamDecay',
'NaturalExpDecay',
'InverseTimeDecay',
'ExponentialDecay',
]
decay_main_program = paddle.static.Program()
decay_startup_program = paddle.static.Program()
lr_name = ""
if isinstance(lr_scheduler, ExponentialDecay):
with paddle.static.program_guard(
decay_main_program, decay_startup_program
):
lr = exponential_decay(
1.0, lr_decay_steps, lr_scheduler.gamma, True
)
lr_name = lr.name
logging.warning(
f"ExponentialDecay is set, staircase = True, global learning rate decay step is [ {lr_decay_steps} ], Change decay steps as follow: \n"
"\t strategy = paddle.distributed.fleet.DistributedStrategy() \n "
"\t strategy.a_sync = True \n"
"\t strategy.a_sync_configs= { 'lr_decay_steps' : YOUR_DECAY_STEP } \n"
)
elif isinstance(lr_scheduler, NoamDecay):
with paddle.static.program_guard(
decay_main_program, decay_startup_program
):
lr = noam_decay(
lr_scheduler.d_model, lr_scheduler.warmup_steps, 1.0
)
lr_name = lr.name
logging.warning(
f"NoamDecay is set, warmup steps is [ {lr_scheduler.warmup_steps} ]"
)
elif isinstance(lr_scheduler, NaturalExpDecay):
with paddle.static.program_guard(
decay_main_program, decay_startup_program
):
lr = paddle.optimizer.lr.NaturalExpDecay(
1.0, lr_scheduler.gamma
).get_lr()
lr_name = lr.name
logging.warning(
f"NaturalExpDecay is set, staircase = True, global learning rate decay step is [ {lr_decay_steps} ], Change decay steps as follow: \n"
"\t strategy = paddle.distributed.fleet.DistributedStrategy() \n "
"\t strategy.a_sync = True \n"
"\t strategy.a_sync_configs= { 'lr_decay_steps' : YOUR_DECAY_STEP } \n"
)
elif isinstance(lr_scheduler, InverseTimeDecay):
with paddle.static.program_guard(
decay_main_program, decay_startup_program
):
lr = inverse_time_decay(
1.0, lr_decay_steps, lr_scheduler.gamma, True
)
lr_name = lr.name
logging.warning(
f"InverseTimeDecay is set, staircase = True, global learning rate decay step is [ {lr_decay_steps} ], Change decay steps as follow: \n"
"\t strategy = paddle.distributed.fleet.DistributedStrategy() \n "
"\t strategy.a_sync = True \n"
"\t strategy.a_sync_configs= { 'lr_decay_steps' : YOUR_DECAY_STEP } \n"
)
else:
raise ValueError(
f"Not supported current LearningRate strategy, please use follow decay strategy: {scheduler_decay}"
)
return decay_main_program, decay_startup_program, lr_name
def _apply_single_impl(self, main_program, startup_program, pass_ctx):
attrs = pass_ctx._attrs
if not hasattr(attrs['origin_main_program'], 'lr_scheduler'):
return
assert isinstance(
attrs['origin_main_program'].lr_scheduler, LRScheduler
), "must be LRScheduler"
ops = get_optimize_ops(attrs['origin_main_program'])
(
lr_decay_main_program,
lr_decay_startup_program,
lr_name,
) = self._get_lr_scheduler_program(
attrs['origin_main_program'].lr_scheduler, attrs['lr_decay_steps']
)
self._add_tensor_table(
attrs,
"@LR_DECAY_COUNTER@",
lr_name,
lr_decay_startup_program,
lr_decay_main_program,
"GlobalStepTable",
)
return
@register_pass("add_listen_and_serv_pass")
class AddListenAndServPass(PassBase):
def __init__(self):
super().__init__()
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, pass_ctx):
attrs = pass_ctx._attrs
opt = {
"grad_to_block_id": None,
"sparse_grad_to_param": None,
"lr_decay_block_id": None,
"dense_optimize_blocks": None,
"sparse_optimize_blocks": None,
# runtime attribute
"endpoint": get_ps_endpoint(attrs['role_maker']),
"pserver_id": get_role_id(attrs['role_maker']),
"Fanin": get_trainers(attrs['role_maker']),
"distributed_mode": attrs['ps_mode'],
"rpc_get_thread_num": -1,
"rpc_send_thread_num": -1,
"rpc_prefetch_thread_num": -1,
}
main_program.global_block().append_op(
type="listen_and_serv", inputs={'X': []}, outputs={}, attrs=opt
)
@register_pass("add_rpc_global_flags_pass")
class AddRpcGlobalFlagsPass(PassBase):
def __init__(self):
super().__init__()
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, pass_ctx):
pass
@register_pass("add_optimizer_pass")
class AddOptimizerPass(PassBase):
def __init__(self):
super().__init__()
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, pass_ctx):
pass
@register_pass("add_geo_optimizer_pass")
class AddGeoOptimizerPass(PassBase):
def __init__(self):
super().__init__()
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, pass_ctx):
pass
@register_pass("build_pserver_startup_program_pass")
class BuildPserverStartupProgramPass(PassBase):
def __init__(self):
super().__init__()
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, pass_ctx):
pass
@register_pass("delete_unused_in_startup_pass")
class DeleteUnusedInStartupPass(PassBase):
def __init__(self):
super().__init__()
def _check_self(self):
return True
def _check_conflict(self, other_pass):
return True
def _apply_single_impl(self, main_program, startup_program, pass_ctx):
pass
File diff suppressed because it is too large Load Diff