Files
2026-07-13 12:40:42 +08:00

2208 lines
84 KiB
Python
Executable File

# Copyright (c) 2020 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import paddle
from paddle.base import core
from paddle.incubate.optimizer import PipelineOptimizer
from paddle.static import (
create_global_var,
default_startup_program,
device_guard,
)
from paddle.utils import unique_name
from ..utils.log_util import logger
from .common import (
OP_ROLE_KEY,
OP_ROLE_VAR_KEY,
CollectiveHelper,
OpRole,
is_backward_op,
is_optimizer_op,
is_update_op,
)
from .meta_optimizer_base import MetaOptimizerBase
from .sharding import utils
from .sharding.fp16_helper import FP16Utils
from .sharding.gradient_clip_helper import GradientClipHelper
from .sharding.offload_helper import OffloadHelper
from .sharding.prune import ProgramDeps
from .sharding.shard import ProgramSegment, Shard
from .sharding.utils import (
get_first_optimize_op_idx,
get_grad_device,
get_var_size,
insert_allreduce_ops,
insert_broadcast_ops,
insert_cast_ops,
insert_fill_constant_ops,
insert_reduce_ops,
insert_scale_loss_grad_ops,
insert_sync_calc_op,
insert_sync_comm_ops,
)
from .sharding.weight_decay_helper import WeightDecayHelper
__all__ = []
class ShardingOptimizer(MetaOptimizerBase):
"""Sharding Optimizer."""
def __init__(self, optimizer):
super().__init__(optimizer)
self.inner_opt = optimizer
self.meta_optimizers_white_list = [
"RecomputeOptimizer",
"AMPOptimizer",
"LarsOptimizer",
"LambOptimizer",
"ASPOptimizer",
# "ModelParallelOptimizer",
# "PipelineOptimizer",
]
self.meta_optimizers_black_list = []
self._main_program = None
self._startup_program = None
self._segments = []
# params and fp16 params is for broadcast
self._params = set()
self._broadcast_vars = set()
# reduced grads to param name
self._reduced_grads_to_param = {}
self._shard = Shard()
self._verbose = False
self._thread_mode = False
self._use_calc_stream = False
# use sharding as outer parallelism (e.g. inner:Megatron & outer sharding)
self.mp_degree = 1
def _can_apply(self):
if not self.role_maker._is_collective:
return False
if self.role_maker._worker_num() <= 1:
return False
return self.user_defined_strategy.sharding
def _disable_strategy(self, dist_strategy):
dist_strategy.sharding = False
dist_strategy.sharding_configs = {}
def _enable_strategy(self, dist_strategy, context):
dist_strategy.sharding = True
dist_strategy.sharding_configs = {"segment_broadcast_MB": 32}
def _get_sharding_segment_strategy(self):
"""get
self._sharding_segment_strategy
1. if by_size: self._broadcast_MB
2. if by_anchors: self._sharding_segment_anchors
self._backward_remain_anchors
self._forward_remain_anchors
"""
strategy = self.user_defined_strategy
sharding_configs = strategy.sharding_configs
segment_strategy = str(sharding_configs["sharding_segment_strategy"])
if segment_strategy == "segment_broadcast_MB":
self._broadcast_MB = sharding_configs["segment_broadcast_MB"]
assert self._broadcast_MB > 0, (
"segment size should larger than zero !"
)
elif segment_strategy == "segment_anchors":
self._sharding_segment_anchors = sharding_configs["segment_anchors"]
assert len(self._sharding_segment_anchors) > 0, (
"you should set the sharding segment anchors !"
)
self._backward_remain_anchors = self._sharding_segment_anchors[:]
self._forward_remain_anchors = []
else:
raise NotImplementedError(
f"the sharding segment strategy [{segment_strategy}] is not implemented"
)
self._sharding_segment_strategy = segment_strategy
def _get_hybrid_degree(self):
"""get
self.hybrid_dp
self.sharding_degree
self.mp_degree
self.pp_degree
self.dp_degree
"""
strategy = self.user_defined_strategy
sharding_configs = strategy.sharding_configs
# parallelism
sharding_degree = int(sharding_configs["sharding_degree"])
mp_degree = int(sharding_configs["mp_degree"])
pp_degree = int(sharding_configs["pp_degree"])
dp_degree = int(sharding_configs['dp_degree'])
global_world_size = self.role_maker._worker_num()
assert sharding_degree > 0, "sharding degree must be larger than zero"
# pipeline setting
# TODO (JZ-LIANG) should revise here for support mix parallelism with pipeline
if pp_degree > 1:
assert strategy.pipeline is True
if os.getenv("PADDLE_MANUAL_PIPELINE_STAGE", None):
assert pp_degree == 2, (
"For manually set pipeline, only pp_degree = 2 is supported."
)
assert (
global_world_size == mp_degree * sharding_degree * dp_degree
), (
f"global work size [{global_world_size}], mp_degree [{mp_degree}], sharding_degree [{sharding_degree}], dp_degree [{dp_degree}]."
)
else:
assert (
global_world_size
== mp_degree * sharding_degree * pp_degree * dp_degree
), (
f"global work size [{global_world_size}], mp_degree [{mp_degree}], sharding_degree [{sharding_degree}], pp_degree [{pp_degree}], dp_degree [{dp_degree}]."
)
# FIXME (JZ-LIANG) deprecated hybrid_dp
if sharding_configs["hybrid_dp"]:
logger.warning(
"[hybrid_dp] API setting is deprecated. Now when "
"dp_degree >= 2, its will be in hybrid dp mode automatically"
)
assert dp_degree >= 1
self.hybrid_dp = True if dp_degree > 1 else False
self.sharding_degree = sharding_degree
self.mp_degree = mp_degree
self.pp_degree = pp_degree
self.dp_degree = dp_degree
def _get_hybrid_dp_mode(self):
"""get
self.hybrid_dp_mode = 'pp_hybrid_dp' or 'sharding_hybrid_dp'
self.gradient_merge_mode = 'pp_gm' or 'sharding_gm'
self._gradient_merge_acc_step
self.pp_allreduce_in_optimize
self._optimizer_sharding
"""
strategy = self.user_defined_strategy
sharding_configs = strategy.sharding_configs
# NOTE (JZ-LIANG)
# There 2 kind of modes for gradient-merge and hybrid-dp in mixed parallelism [sharding] and [pipeline].
# We distinguish this two modes since the gm/hybrid-dp related allreduce should be insert in different place
# according different mode to have best performance:
# sharding: communication within node, and therefore should insert within backward segment
# to overlap with bw calc, conduct every micro step.
# pipeline: communication across nodes, and therefore should insert in update segment,
# conduct just once per global step.
dp_mode = None
# dp here is the pure dp as the outermost parallelism
if self.hybrid_dp:
if self.pp_degree > 1:
dp_mode = "pp_hybrid_dp"
else:
assert self.sharding_degree > 1, (
"by now we only support five kind of hybrid dp: sharding_hybrid_dp, "
"mp_sharding_hybrid_dp, pp_hybrid_dp, mp_sharding_pp_hybrid_dp, sharding_pp_hybrid_dp."
)
dp_mode = "sharding_hybrid_dp"
# gradient merge
gm_mode = None
gm_acc_step = int(sharding_configs["gradient_merge_acc_step"])
if self.pp_degree <= 1:
gm_mode = "sharding_gm"
self._grad2merged_grad = {}
else:
gm_mode = "pp_gm"
gm_acc_step = strategy.pipeline_configs['accumulate_steps']
gradient_scale_configs = strategy.gradient_scale_configs
assert gradient_scale_configs['scale_strategy'] == 'avg', (
'For pipeline mode, the '
'gradient scale mode should '
'be "avg", but got {}'.format(
gradient_scale_configs['scale_strategy']
)
)
# Note (Yuang Liu): this avg_loss flag determines where to do the average op for grad merge.
# If True, will do sum firstly for gradient merge, then do scale by gm_acc_step.
# If False, will scale loss by gm_acc_step first, then do sum for gradient merge.
self.scale_gradient = gradient_scale_configs['scale_gradient']
if gm_acc_step > 1:
logger.info(
f"Gradient merge in [{gm_mode}], acc step = [{gm_acc_step}]"
)
optimizer_sharding = False
# TODO(wangxi): need support dp_as_opt_sharding with sharding
# need support without pp in future
if (
self.sharding_degree == 1
and self.dp_degree > 1
and sharding_configs['_dp_as_optimizer_sharding']
and self.pp_degree > 1
):
optimizer_sharding = True
self.hybrid_dp_mode = dp_mode
self.gradient_merge_mode = gm_mode
self._gradient_merge_acc_step = gm_acc_step
self._optimizer_sharding = optimizer_sharding
# this feature is design for ascend, and should NOT be used in GPU training
self.pp_allreduce_in_optimize = sharding_configs[
"pp_allreduce_in_optimize"
]
def _inner_opt_minimize(
self, loss, startup_program, parameter_list, no_grad_set
):
pipeline_configs = self.user_defined_strategy.pipeline_configs
if self.inner_opt is None:
raise ValueError(
"self.inner_opt of ShardingOptimizer should not be None."
)
if self.pp_degree > 1:
pp_optimizer = PipelineOptimizer(
self.inner_opt, self._gradient_merge_acc_step
)
self._pp_optimizer = pp_optimizer
global_rank = self.role_maker._worker_index()
schedule_mode = pipeline_configs['schedule_mode']
pipeline_opt = {
'schedule_mode': schedule_mode,
'micro_batch_size': pipeline_configs['micro_batch_size'],
'local_rank': self.pp_rank,
'global_rank': global_rank,
'use_sharding': True,
# TODO (JZ-LIANG) should revise here for support mix parallelism with pipeline
'ring_id': 20,
'global_ring_id': 3,
'mp_degree': self.mp_degree,
'mp_rank': global_rank % self.mp_degree,
'scale_gradient': self.scale_gradient,
}
main_program = loss.block.program
main_program._pipeline_opt = pipeline_opt
(
optimize_ops,
params_grads,
program_list,
self.pipeline_pair,
self.pp_ring_map,
) = pp_optimizer.minimize(
loss, startup_program, parameter_list, no_grad_set
)
assert self.pp_degree == len(program_list)
else:
optimize_ops, params_grads = self.inner_opt.minimize(
loss, startup_program, parameter_list, no_grad_set
)
if startup_program is None:
startup_program = default_startup_program()
if self.pp_degree > 1:
startup_program = startup_program._pipeline_opt['startup_program']
print("pp_rank:", self.pp_rank)
if os.getenv("PADDLE_MANUAL_PIPELINE_STAGE", None):
main_program = program_list[
int(os.getenv("PADDLE_MANUAL_PIPELINE_STAGE"))
]
else:
main_program = program_list[self.pp_rank]
with open(f"main_{self.role_maker._worker_index()}", 'w') as f:
f.writelines(str(main_program))
main_block = main_program.global_block()
new_params_grads = []
for param, grad in params_grads:
if main_block.has_var(param.name):
new_params_grads.append((param, grad))
params_grads = new_params_grads
else:
main_block = loss.block
startup_block = startup_program.global_block()
self._main_program = main_block.program
self._startup_program = startup_program
if self.pp_degree > 1:
pp_optimizer._rename_gradient_var_name(main_block)
with open(f"main_{self.role_maker._worker_index()}", 'w') as f:
f.writelines(str(main_program))
return optimize_ops, params_grads
def _apply_sharding_pass(self, params_grads):
if self.sharding_degree == 1:
return
main_block = self._main_program.global_block()
startup_block = self._startup_program.global_block()
# step1: build shard
self._build_shard(
params_grads, self.sharding_rank, self.sharding_degree
)
# step2: split_program
self._split_program(main_block)
# step3: add broadcast and reduce ops
self._add_broadcast_allreduce(main_block)
main_block._sync_with_cpp()
startup_block._sync_with_cpp()
# step4: remove unneeded ops and vars from block
self._prune_main_program(
main_block,
self._shard,
[self.mp_ring_id, self.sharding_ring_id, self.pp_ring_id],
)
self._prune_startup_program(startup_block, self._shard)
def _apply_opt_sharding_pass(self, params_grads):
"""outer dp as optimizer sharding"""
if self._optimizer_sharding is False:
return
main_block = self._main_program.global_block()
startup_block = self._startup_program.global_block()
# step1: build shard
self._build_shard(params_grads, self.dp_rank, self.dp_degree)
# NOTE(wangxi): prune_main_program will prune cast if not add this
for param, grad in params_grads:
self._reduced_grads_to_param[grad.name] = param.name
# step4: remove unneeded ops and vars from block
self._prune_main_program(
main_block,
self._shard,
[self.mp_ring_id, self.pp_ring_id, self.dp_ring_id],
)
self._prune_startup_program(startup_block, self._shard)
def _insert_allreduce_for_pp(self, params_grads):
if self.pp_degree == 1:
return
strategy = self.user_defined_strategy
sharding_configs = strategy.sharding_configs
main_block = self._main_program.global_block()
startup_block = self._startup_program.global_block()
# sharding-pp related logic
# pp_optimizer._rename_gradient_var_name(main_block)
# crop ops
if self.sharding_degree > 1:
for idx, op in reversed(list(enumerate(main_block.ops))):
if is_update_op(op):
op_role_var = op.attr('op_role_var')
param_name = op_role_var[0]
if not self._shard.has_param(param_name):
main_block._remove_op(idx)
for idx, op in reversed(list(enumerate(main_block.ops))):
if op.type != 'cast':
continue
in_name = op.input_arg_names[0]
if in_name not in self._params:
continue
# if self._shard.has_param(param_name): continue
if in_name not in main_block.vars:
main_block._remove_op(idx)
if self._optimizer_sharding:
# TODO(wangxi): support fp16_allreduce with optimizer sharding
strategy.fp16_allreduce = False
shard = self._shard if self._optimizer_sharding else None
accumulated_grad_names = self._pp_optimizer._accumulate_gradients(
main_block, strategy=strategy, shard=shard
)
len_of_ops = len(main_block.ops)
if self.scale_gradient:
self._avg_grad_merge_after_sum(main_block, accumulated_grad_names)
first_optimize_op_index = get_first_optimize_op_idx(main_block)
if self.pp_allreduce_in_optimize:
logger.info(
f"Pipeline Persistable grad is {accumulated_grad_names}"
)
# FIXME(wangxi): accumulated_grad get from pipeline is not
# include sharding's param@BroadCast grad when
# pp_allreduce_in_optimize
accumulated_grad_names = insert_reduce_ops(
main_block,
first_optimize_op_index,
self.sharding_ring_id,
accumulated_grad_names,
self._shard,
core.op_proto_and_checker_maker.OpRole.Optimize,
use_calc_stream=True,
rank=self.sharding_rank,
)
logger.info(f"PP-Sharding grad is {accumulated_grad_names}")
first_optimize_op_index += len(main_block.ops) - len_of_ops
len_of_ops = len(main_block.ops)
if self._optimizer_sharding:
accumulated_grad_names = utils.insert_reduce_ops(
main_block,
first_optimize_op_index,
self.dp_ring_id,
accumulated_grad_names,
self._shard,
OpRole.Optimize,
use_calc_stream=True,
rank=self.dp_rank,
strategy=strategy,
)
logger.info(f"Optimizer grad in this rank {accumulated_grad_names}")
first_optimize_op_index += len(main_block.ops) - len_of_ops
len_of_ops = len(main_block.ops)
# NOTE(wangxi): we fused after optimize_cast
optimize_cast = sharding_configs['optimize_cast']
optimizer_param = utils.insert_broadcast_param_ops(
main_block,
len_of_ops,
self.dp_ring_id,
[x[0].name for x in params_grads],
self._shard,
OpRole.Optimize,
use_calc_stream=True,
rank=self.dp_rank,
strategy=None if optimize_cast else strategy,
)
logger.info(f"Optimizer param in this rank {optimizer_param}")
if not strategy.fuse_grad_merge and not optimize_cast:
assert len(accumulated_grad_names) == len(optimizer_param)
elif self.hybrid_dp and self.hybrid_dp_mode == "pp_hybrid_dp":
insert_allreduce_ops(
main_block,
first_optimize_op_index,
self.dp_ring_id,
accumulated_grad_names,
core.op_proto_and_checker_maker.OpRole.Optimize,
use_calc_stream=True,
user_defined_strategy=strategy,
)
first_optimize_op_index += len(main_block.ops) - len_of_ops
len_of_ops = len(main_block.ops)
# FIXME(wangxi): if fp16_allreduce, put cast fp16->fp32 to there?
def _avg_grad_merge_after_sum(self, main_block, accumulated_grad_names):
if (
self.user_defined_strategy.amp
and self.user_defined_strategy.amp_configs[
'use_dynamic_loss_scaling'
]
):
# For AMP, if using dynamic loss scaling the avg
# operation can be simple done by modify the LossScaling op.
for idx, op in enumerate(main_block.ops):
if op.type == 'check_finite_and_unscale':
loss_scale_name = op.input('Scale')[0]
loss_scaling_var = main_block.var(loss_scale_name)
loss_scale_tmp_var_name = loss_scale_name + '@TMP'
loss_scale_tmp_var = main_block.create_var(
name=loss_scale_tmp_var_name,
shape=loss_scaling_var.shape,
dtype=loss_scaling_var.dtype,
)
main_block._insert_op_without_sync(
idx,
type='scale',
inputs={'X': loss_scaling_var},
outputs={'Out': loss_scale_tmp_var},
attrs={
'scale': self._gradient_merge_acc_step,
'bias': 0.0,
'bias_after_scale': False,
OP_ROLE_KEY: OpRole.Optimize,
},
)
op._rename_input(loss_scale_name, loss_scale_tmp_var_name)
break
else:
# For pp, do the avg operation for gradient merge after merging
# the gradient to meet the logic for gradient merge under pure dp.
tmp_first_opt_idx = None
for idx, op in enumerate(main_block.ops):
if is_optimizer_op(op) and op.type != 'c_sync_comm_stream':
tmp_first_opt_idx = idx
break
assert tmp_first_opt_idx is not None, (
'Occurs some errors, no optimize ops'
)
for grad in accumulated_grad_names:
main_block._insert_op_without_sync(
tmp_first_opt_idx,
type='scale',
inputs={'X': grad},
outputs={'Out': grad},
attrs={
'scale': 1.0 / self._gradient_merge_acc_step,
'bias': 0.0,
'bias_after_scale': False,
OP_ROLE_KEY: OpRole.Optimize,
},
)
def _adapt_amp_clip_without_sharding(self):
# if not use sharding, adapt amp/clip, for remain parallelism.
# cast --> amp --> clip --> opt
if self.sharding_degree > 1:
return
if self._optimizer_sharding:
return
main_block = self._main_program.global_block()
startup_block = self._startup_program.global_block()
# amp inf_var & clip global_norm_var
rings = [self.mp_ring_id, self.pp_ring_id]
FP16Utils.sync_amp_check_nan_inf(main_block, rings)
gradient_clip_helper = GradientClipHelper(None)
gradient_clip_helper.sync_global_norm(
main_block, [self.mp_ring_id, self.pp_ring_id], self.mp_rank
)
def _insert_loss_grad_scale_op(self):
main_block = self._main_program.global_block()
# step6: loss div dp_degree
global_dp_degree = self.sharding_degree * self.dp_degree
assert int(global_dp_degree) == global_dp_degree
if global_dp_degree > 1:
insert_scale_loss_grad_ops(main_block, scale=global_dp_degree)
main_block._sync_with_cpp()
def _apply_optimize_offload_pass(self, params_grads):
strategy = self.user_defined_strategy
sharding_configs = strategy.sharding_configs
main_block = self._main_program.global_block()
startup_block = self._startup_program.global_block()
mp_ring_id = self.mp_ring_id if self.mp_degree > 1 else None
dp_ring_id = self.dp_ring_id if self.dp_degree > 1 else None
offload_helper = OffloadHelper(
mp_ring_id=mp_ring_id, dp_ring_id=dp_ring_id
)
# optimize offload should be enable while gradient merge is enable and
# acc_step is quite large (e.g. >> 100). Since its memcpy could not be
# overlap with calc, otherwise it will slower down training severely.
if sharding_configs["optimize_offload"]:
logger.info("Sharding with optimize offload !")
offload_helper.offload(main_block, startup_block)
# The optimize_cast is already included in offload_fp32param
offload_helper.offload_fp32param(main_block, startup_block)
elif sharding_configs['optimize_cast']:
logger.info("Sharding with optimize cast !")
# NOTE(wangxi): optimize_cast will persist fp16 param, it
# will take more memory, but will be faster. Trade space for time.
if self._optimizer_sharding:
offload_helper.opt_sharding_cast_fp32param(
main_block, startup_block, [x[0].name for x in params_grads]
)
# NOTE(wangxi): fused after optimize_cast
utils.fuse_opt_broadcast_param_ops(
main_block, dp_ring_id, self._shard, strategy=strategy
)
else:
offload_helper.cast_fp32param_in_optimize(
main_block, startup_block
)
def _dump_program_for_debug(self):
main_block = self._main_program.global_block()
startup_block = self._startup_program.global_block()
with open(
f"start_sharding_{self.role_maker._worker_index()}", 'w'
) as f:
f.writelines(str(startup_block.program))
with open(f"main_sharding_{self.role_maker._worker_index()}", 'w') as f:
f.writelines(str(main_block.program))
def minimize_impl(
self, loss, startup_program=None, parameter_list=None, no_grad_set=None
):
# TODO: (JZ-LIANG) support multiple comm in future
# self._nrings = self.user_defined_strategy.nccl_comm_num
self._nrings_sharding = 1
self._nrings_dp = 1
self._get_sharding_segment_strategy()
self._get_hybrid_degree()
self._get_hybrid_dp_mode()
# config sharding & dp groups
self._build_groups()
# inner optimize minimize
optimize_ops, params_grads = self._inner_opt_minimize(
loss, startup_program, parameter_list, no_grad_set
)
self._init_comm()
self._apply_sharding_pass(params_grads)
self._apply_opt_sharding_pass(params_grads)
self._insert_allreduce_for_pp(params_grads)
self._adapt_amp_clip_without_sharding()
# loss div dp_degree
self._insert_loss_grad_scale_op()
# apply optimize offload or optimize cast
self._apply_optimize_offload_pass(params_grads)
# step6: (optional) sharding gradient merge
self._sharding_gradient_merge()
# # check op dependency
# FIXME (JZ-LIANG) enable checking in future.
# check_broadcast(main_block)
# check_allreduce_sum(main_block, self._shard, self.sharding_ring_id,
# self.dp_ring_id)
# NOTE(JZ-LIANG) ensure in both sharding_hybrid_dp & pp_hybrid_dp
# init param broadcast should be called after startup pruning
self._initialization_broadcast()
# NOTE(wangxi): if param is not persistable, program.clone will
# failed, so we remove no persistable param, recreate param as a var
self._recreate_not_persist_param_as_var()
self._dump_program_for_debug()
return optimize_ops, params_grads
def _init_pair_comm(self, pair, ring_id):
pp_group_endpoints = [
self.pp_group_endpoints[pair[0]],
self.pp_group_endpoints[pair[1]],
]
pp_rank = 0 if self.pp_rank == pair[0] else 1
if os.getenv("PADDLE_MANUAL_PIPELINE_STAGE", None) is None:
self._collective_helper._init_communicator(
self._startup_program,
self.current_endpoint,
pp_group_endpoints,
pp_rank,
ring_id,
False,
sync=False,
)
def _init_pipeline_comm(self, startup_block):
# TODO (JZ-LIANG) to unify pp_rank_ and pp_rank
if os.getenv("PADDLE_MANUAL_PIPELINE_STAGE", None) is None:
self._collective_helper._init_communicator(
self._startup_program,
self.current_endpoint,
self.pp_group_endpoints,
self.pp_rank,
self.pp_ring_id,
False,
sync=False,
)
# GPU
for pair in self.pipeline_pair:
pair_key = pair[0] * 1000 + pair[1]
ring_id = self.pp_ring_map[pair_key]
logger.info(f"pp pair:{pair}, ring_id: {ring_id}")
if self.pp_rank in pair:
self._init_pair_comm(pair, ring_id)
def _init_comm(self):
# sync var
startup_block = self._startup_program.global_block()
# mp ring
if self.mp_degree > 1:
self._collective_helper._init_communicator(
self._startup_program,
self.current_endpoint,
self.mp_group_endpoints,
self.mp_rank,
self.mp_ring_id,
False,
sync=False,
)
# sharding ring
if self.sharding_degree > 1:
self._collective_helper._init_communicator(
self._startup_program,
self.current_endpoint,
self.sharding_group_endpoints,
self.sharding_rank,
self.sharding_ring_id,
False,
sync=False,
)
# pp ring
if self.pp_degree > 1:
self._init_pipeline_comm(startup_block)
# pure dp ring
if self.dp_degree > 1:
self._collective_helper._init_communicator(
self._startup_program,
self.current_endpoint,
self.dp_group_endpoints,
self.dp_rank,
self.dp_ring_id,
False,
sync=False,
)
startup_block._sync_with_cpp()
def _build_shard(self, params_grads, shard_rank, shard_size):
# step 2: split params
self._params = {x[0].name for x in params_grads}
self._shard.setup(params_grads, shard_rank, shard_size)
# step 3: get broadcast vars
self._broadcast_vars = self._shard.find_broadcast_params(
self._main_program.global_block()
)
def _wait(
self,
):
endpoints = self.global_endpoints[:]
current_endpoint = endpoints[self.global_rank]
if self.global_rank == 0:
self._collective_helper._wait(current_endpoint, endpoints)
def collect_segment(self, segment, op_idx, block):
segment._start_idx = op_idx + 1
self._segments.insert(0, segment)
new_segment = ProgramSegment(block)
new_segment._end_idx = op_idx + 1
return new_segment
def _split_program(self, block):
for op_idx, op in reversed(list(enumerate(block.ops))):
if int(op.attr('op_role')) != int(OpRole.Optimize):
last_backward_op_idx = op_idx + 1
break
var2broadcast_time = {}
segment = ProgramSegment(block)
segment._end_idx = last_backward_op_idx
for op_idx in reversed(range(last_backward_op_idx)):
op = block.ops[op_idx]
assert int(op.attr('op_role')) != int(OpRole.Optimize)
if self._sharding_segment_strategy == "segment_broadcast_MB":
if segment._param_mem >= self._broadcast_MB:
segment = self.collect_segment(segment, op_idx, block)
elif self._sharding_segment_strategy == "segment_anchors":
if int(op.attr('op_role')) == int(OpRole.Backward):
for input_name in op.desc.input_arg_names():
# NOTE (JZ-LIANG) naive rule to support amp, if amp change, should modify here accordingly
if self.user_defined_strategy.amp:
if ".cast_fp16@GRAD" not in input_name:
continue
else:
input_name = input_name[
: input_name.find(".cast_fp16@GRAD")
]
if input_name in self._backward_remain_anchors:
segment = self.collect_segment(
segment, op_idx, block
)
assert (
input_name not in self._forward_remain_anchors
), f"segment anchor [{input_name}] met twice !"
self._backward_remain_anchors.remove(input_name)
self._forward_remain_anchors.append(input_name)
elif int(op.attr('op_role')) == int(OpRole.Forward):
for output_name in op.desc.output_arg_names():
if output_name in self._forward_remain_anchors:
segment = self.collect_segment(
segment, op_idx, block
)
self._forward_remain_anchors.remove(output_name)
# find broadcast vars
for input_name in op.desc.input_arg_names():
if input_name not in self._broadcast_vars:
continue
if input_name in segment._param2broadcast:
# skip broadcast because it reuse the old broadcast var
broadcast_name = segment._param2broadcast[input_name]
if input_name != broadcast_name:
op._rename_input(input_name, broadcast_name)
continue
if self._shard.has_param(input_name):
broadcast_var_name = input_name
else:
broadcast_var_name = unique_name.generate(
input_name + "@BroadCast"
)
if not self._thread_mode:
segment._fill_constant_vars.append(broadcast_var_name)
# (JZ-LIANG) should use Param base name ?
broadcast_var_base_name = input_name
if "subprog" in broadcast_var_base_name:
# remove suffix
broadcast_var_base_name = broadcast_var_base_name[
: broadcast_var_base_name.find(".subprog")
]
var2broadcast_time[broadcast_var_base_name] = (
var2broadcast_time.get(broadcast_var_base_name, 0) + 1
)
segment._param2broadcast[input_name] = broadcast_var_name
segment._broadcast_vars.append(
(broadcast_var_name, self._shard.device(input_name))
)
segment._param_mem += get_var_size(
self._main_program.global_block().var(input_name)
)
# find reduce vars
if self.pp_degree > 1 and self.pp_allreduce_in_optimize:
# place pipeline gradient allreduce in optimize
pass
else:
if is_backward_op(op) and OP_ROLE_VAR_KEY in op.attr_names:
op_role_var = op.all_attrs()[OP_ROLE_VAR_KEY]
if len(op_role_var) != 0:
assert len(op_role_var) % 2 == 0
for i in range(0, len(op_role_var), 2):
param, reduced_grad = (
op_role_var[i],
op_role_var[i + 1],
)
segment._allreduce_vars.append(reduced_grad)
assert (
reduced_grad not in self._reduced_grads_to_param
)
self._reduced_grads_to_param[reduced_grad] = param
# find cast op
if FP16Utils.is_fp16_cast_op(block, op, self._params):
fp32_param = op.desc.input_arg_names()[0]
fp16_param = op.desc.output_arg_names()[0]
if self._shard.has_param(fp32_param):
segment._cast_ops[fp16_param] = fp32_param
if segment._param_mem > 0:
segment._start_idx = 0
self._segments.insert(0, segment)
if self._sharding_segment_strategy == "segment_anchors":
assert len(self._forward_remain_anchors) == 0, (
f"remain anchors {self._forward_remain_anchors}"
)
assert len(self._backward_remain_anchors) == 0, (
f"remain anchors {self._backward_remain_anchors}"
)
if self._verbose:
for varname in sorted(
var2broadcast_time, key=var2broadcast_time.get, reverse=True
):
logger.info(
f"Sharding broadcast: [{var2broadcast_time[varname]}] times [{varname}]"
)
for idx_ in range(len(self._segments)):
logger.info(f"segment [{idx_}] :")
logger.info(
"start op: [{}] [{}]".format(
block.ops[self._segments[idx_]._start_idx].desc.type(),
block.ops[
self._segments[idx_]._start_idx
].desc.input_arg_names(),
)
)
logger.info(
"end op: [{}] [{}]".format(
block.ops[self._segments[idx_]._end_idx].desc.type(),
block.ops[
self._segments[idx_]._end_idx
].desc.input_arg_names(),
)
)
def _prune_main_program(self, block, shard, rings):
"""
calculate deps from allreduce op to optimize op,
remove ops and vars not needed in this worker
1. prune regularization (weight decay)
2. prune cast_fp32_to_fp16; update amp_inline_checking
3. prune gradient_clip related; update global_norm_sum
4. prune optimizer op + param + gradient
"""
weight_decay_helper = WeightDecayHelper()
weight_decay_helper.prune_weight_decay(block, shard)
# FIXME(wangxi): mp should prune duplicated param_grads
# NOTE (JZ-LIANG) the sync of FoundInfinite should among one entire Model Parallelism
# group. and each Data Parallelism group should have its own sync of FoundInfinite
# amp could use global group for sync
FP16Utils.prune_fp16(block, shard, self._reduced_grads_to_param, rings)
# clipbyglobalnorm should only use the Model parallelism group (mp-sharding-pp)
gradient_clip_helper = GradientClipHelper(None)
gradient_clip_helper.prune_gradient_clip(block, shard, rings)
# build prog deps
reduced_grads = []
for idx, op in enumerate(block.ops):
input_names = op.desc.input_arg_names()
output_names = op.desc.output_arg_names()
# FIXME(wangxi): need use grads, pipeline grad is @GRAD@MERGE
if (
op.type == "all_reduce"
and op.attr('reduce_type') == paddle.distributed.ReduceOp.SUM
):
assert len(output_names) == 1
output_name = output_names[0]
reduced_grads.append(output_name)
# prune optimizer state and param
pruned_opti_vars = []
for var_name in list(block.vars.keys()):
if shard.is_opti_var(var_name) and not shard.has_opt_var(var_name):
pruned_opti_vars.append(var_name)
program_deps = ProgramDeps(block, reduced_grads, pruned_opti_vars)
# Init
for var_name in program_deps._end_vars:
program_deps._should_removed_var.add(var_name)
# Prune
for idx, op in reversed(list(enumerate(block.ops))):
if op.type in [
"all_reduce",
"c_sync_comm_stream",
"c_calc_comm_stream",
"c_gen_nccl_id",
"c_gen_bkcl_id",
"c_gen_xccl_id",
"c_comm_init",
'send_v2',
'recv_v2',
]:
pass
elif op.type == "conditional_block":
assert op.desc.has_attr("sub_block")
subblock_idx = op.desc.attr("sub_block").id
subblock_deps = program_deps.get_sub_block_deps(subblock_idx)
# only prune amp subblock
if subblock_deps is None or not self._is_amp_subblock(op):
continue
# init
reversed_output_vars = []
for output_name in op.desc.output("Out"):
if output_name in program_deps._should_removed_var:
subblock_deps._should_removed_var.add(output_name)
program_deps.crop_output_var_from_op(idx, output_name)
else:
reversed_output_vars.append(output_name)
# prune
for sub_op_idx, _ in reversed(
list(enumerate(subblock_deps._block.ops))
):
if subblock_deps.should_remove_op(sub_op_idx):
subblock_deps.remove_op(sub_op_idx)
reversed_input_vars = []
for input_name in op.desc.input('Input'):
if input_name not in subblock_deps._should_removed_var:
reversed_input_vars.append(input_name)
else:
program_deps.crop_input_var_from_op(idx, input_name)
op.desc.set_input('Input', reversed_input_vars)
op.desc.set_output('Out', reversed_output_vars)
else:
# if all outputs of this op are in _should_removed_var
# _should_removed_var: opt state not cur shard
if program_deps.should_remove_op(idx):
# NOTE(wangxi): need reserve all param in optimizer_sharding
reserved_vars = (
self._params if self._optimizer_sharding else None
)
program_deps.remove_op(idx, reserved_vars)
# NOTE (JZ-LIANG) revise and unify logic here
# sharding support fp16_allreduce logic
block._sync_with_cpp()
for idx, op in reversed(list(enumerate(block.ops))):
if op.type == 'concat' and is_optimizer_op(op):
# remove inputs that not on this card
reserved_x = []
for var_name in op.desc.input("X"):
if block.has_var(var_name):
reserved_x.append(var_name)
op.desc.set_input('X', reserved_x)
block._sync_with_cpp()
def _add_broadcast_allreduce(self, block):
"""
add broadcast allreduce op
if enable gradient_merge, insert related ops
if combined with pipeline(grad accumulate),
the grad allreduce should be done in optimize role
"""
if len(self._segments) < 1:
return
# sharding
if self.pp_degree > 1 and self.pp_allreduce_in_optimize:
for idx in range(len(self._segments)):
assert len(self._segments[idx]._allreduce_vars) == 0
# NOTE (JZ-LIANG) revise and unify logic here
# fix the _end_idx for segments[-1] if pp is used.
new_end_idx = self._segments[-1]._end_idx
for idx in range(
self._segments[-1]._end_idx - 1,
self._segments[-1]._start_idx - 1,
-1,
):
op = block.ops[idx]
if op.type == "fill_constant" or op.type == "sum":
if "MERGED" in op.output_arg_names[0]:
new_end_idx = idx + 1
elif op.type == "cast":
if "@TMP" in op.output_arg_names[0]:
new_end_idx = idx + 1
self._segments[-1]._end_idx = new_end_idx
if self._segments[-1]._allreduce_vars:
shard_allreduce_vars = self._shard.filter_grads(
self._segments[-1]._allreduce_vars
)
if (
self.gradient_merge_mode != "sharding_gm"
or self._gradient_merge_acc_step <= 1
):
if (
self.hybrid_dp
and self.hybrid_dp_mode == "sharding_hybrid_dp"
and len(shard_allreduce_vars) >= 1
):
if not self._use_calc_stream:
insert_sync_comm_ops(
block,
self._segments[-1]._end_idx,
self.dp_ring_id,
shard_allreduce_vars,
)
insert_allreduce_ops(
block,
self._segments[-1]._end_idx,
self.dp_ring_id,
shard_allreduce_vars,
user_defined_strategy=self.user_defined_strategy,
use_calc_stream=self._use_calc_stream,
)
# gradient merge
elif (
self.gradient_merge_mode == "sharding_gm"
and self._gradient_merge_acc_step > 1
):
self.create_persistable_gradients_and_insert_merge_ops(
block,
self._startup_program.global_block(),
self._segments[-1]._end_idx,
shard_allreduce_vars,
self._shard,
)
if not self._use_calc_stream:
insert_sync_comm_ops(
block,
self._segments[-1]._end_idx,
self.sharding_ring_id,
self._segments[-1]._allreduce_vars,
)
# allreduce --> reduce
insert_reduce_ops(
block,
self._segments[-1]._end_idx,
self.sharding_ring_id,
self._segments[-1]._allreduce_vars,
self._shard,
op_role=OpRole.Backward,
use_calc_stream=self._use_calc_stream,
)
for idx, segment in reversed(list(enumerate(self._segments))):
allreduce_vars = (
self._segments[idx - 1]._allreduce_vars if idx > 0 else []
)
broadcast_vars = (
self._segments[idx + 1]._broadcast_vars
if idx < len(self._segments) - 1
else []
)
fill_constant_vars = (
self._segments[idx + 2]._fill_constant_vars
if idx < len(self._segments) - 2
else []
)
cast_ops = (
self._segments[idx + 2]._cast_ops
if idx < len(self._segments) - 2
else {}
)
for op_idx in reversed(range(segment._start_idx, segment._end_idx)):
op = block.ops[op_idx]
for input_name in op.desc.input_arg_names():
if (
input_name in segment._param2broadcast
and input_name != segment._param2broadcast[input_name]
):
op._rename_input(
input_name, segment._param2broadcast[input_name]
)
for param_name, broadcast_name in segment._param2broadcast.items():
if param_name != broadcast_name:
block.create_var(
name=broadcast_name,
shape=self._main_program.global_block()
.var(param_name)
.shape,
dtype=self._main_program.global_block()
.var(param_name)
.dtype,
persistable=False,
)
# step1: remove cast ops
block._sync_with_cpp()
segment._end_idx += FP16Utils.remove_cast_op(
block, self._params, segment, 0
)
# step2: add Sync ops
shard_allreduce_vars = self._shard.filter_grads(allreduce_vars)
if (
self.gradient_merge_mode != "sharding_gm"
or self._gradient_merge_acc_step <= 1
):
if (
self.hybrid_dp
and self.hybrid_dp_mode == "sharding_hybrid_dp"
and len(shard_allreduce_vars) >= 1
):
if not self._use_calc_stream:
insert_sync_comm_ops(
block,
segment._end_idx,
self.dp_ring_id,
shard_allreduce_vars,
)
broad_cast_vars = [x[0] for x in broadcast_vars]
if not self._use_calc_stream and len(broad_cast_vars) > 0:
insert_sync_comm_ops(
block,
segment._end_idx,
self.sharding_ring_id,
broad_cast_vars,
)
else:
comm_dep_vars = allreduce_vars + [
x[0] for x in broadcast_vars
]
if not self._use_calc_stream and len(comm_dep_vars) > 0:
insert_sync_comm_ops(
block,
segment._end_idx,
self.sharding_ring_id,
comm_dep_vars,
)
# gradient merge
elif (
self.gradient_merge_mode == "sharding_gm"
and self._gradient_merge_acc_step > 1
):
broad_cast_vars = [x[0] for x in broadcast_vars]
if not self._use_calc_stream and len(broad_cast_vars) > 0:
insert_sync_comm_ops(
block,
segment._end_idx,
self.sharding_ring_id,
broad_cast_vars,
)
calc_dep_vars = (
fill_constant_vars
+ [k for k, v in cast_ops.items()]
+ self._segments[idx]._allreduce_vars
)
if not self._use_calc_stream and len(calc_dep_vars) > 0:
insert_sync_calc_op(
block, segment._end_idx, [calc_dep_vars[-1]]
)
# step3: insert `fill_constant` ops
insert_fill_constant_ops(
block, segment._end_idx, fill_constant_vars
)
# step4: add `cast` ops
insert_cast_ops(block, segment._end_idx, cast_ops)
# step5: add broadcast ops
# gradient merge
if (
self.gradient_merge_mode == "sharding_gm"
and self._gradient_merge_acc_step > 1
):
self.create_persistable_gradients_and_insert_merge_ops(
block,
self._startup_program.global_block(),
segment._start_idx,
shard_allreduce_vars,
self._shard,
)
insert_broadcast_ops(
block,
segment._start_idx,
self.sharding_ring_id,
broadcast_vars,
self._use_calc_stream,
)
# step6: add all_reduce ops
# dp
if (
self.gradient_merge_mode != "sharding_gm"
or self._gradient_merge_acc_step <= 1
):
if (
self.hybrid_dp
and self.hybrid_dp_mode == "sharding_hybrid_dp"
and len(shard_allreduce_vars) >= 1
):
insert_allreduce_ops(
block,
segment._start_idx,
self.dp_ring_id,
shard_allreduce_vars,
user_defined_strategy=self.user_defined_strategy,
use_calc_stream=self._use_calc_stream,
)
if not self._use_calc_stream:
insert_sync_comm_ops(
block,
segment._start_idx,
self.sharding_ring_id,
allreduce_vars,
)
# gradient merge
elif (
self.gradient_merge_mode == "sharding_gm"
and self._gradient_merge_acc_step > 1
):
if not self._use_calc_stream:
insert_sync_comm_ops(
block,
segment._start_idx,
self.sharding_ring_id,
allreduce_vars,
)
# sharding
# allreduce --> reduce
# TODO temp change
if len(allreduce_vars) > 0:
insert_reduce_ops(
block,
segment._start_idx,
self.sharding_ring_id,
allreduce_vars,
self._shard,
op_role=OpRole.Backward,
use_calc_stream=self._use_calc_stream,
)
block._sync_with_cpp()
if self._segments[0]._broadcast_vars:
broadcast_vars = [x[0] for x in self._segments[0]._broadcast_vars]
if not self._use_calc_stream:
insert_sync_comm_ops(
block,
self._segments[0]._start_idx,
self.sharding_ring_id,
broadcast_vars,
)
insert_broadcast_ops(
block,
self._segments[0]._start_idx,
self.sharding_ring_id,
self._segments[0]._broadcast_vars,
self._use_calc_stream,
)
fill_constant_vars = []
for x in self._segments[:2]:
fill_constant_vars += x._fill_constant_vars
# Join
cast_ops = {}
for x in self._segments[:2]:
for k, v in x._cast_ops.items():
cast_ops[k] = v
calc_deps_vars = fill_constant_vars + [k for k, v in cast_ops.items()]
if not self._use_calc_stream and (fill_constant_vars or cast_ops):
insert_sync_calc_op(
block, self._segments[0]._start_idx, [calc_deps_vars[-1]]
)
if fill_constant_vars:
insert_fill_constant_ops(
block, self._segments[0]._start_idx, fill_constant_vars
)
if cast_ops:
insert_cast_ops(block, self._segments[0]._start_idx, cast_ops)
return
def _prune_startup_program(self, block, shard):
for idx, op in reversed(list(enumerate(block.ops))):
for output_name in op.desc.output_arg_names():
if shard.has_var(output_name):
continue
if self._optimizer_sharding and shard.is_param(output_name):
continue
# TODO why do we remove op, when only one var is removed
block._remove_op(idx, sync=False)
break
for var_name in list(block.vars.keys()):
if shard.has_var(var_name):
continue
if self._optimizer_sharding and shard.is_param(var_name):
continue
block._remove_var(var_name, sync=False)
block._sync_with_cpp()
def _build_groups(self):
"""
pre-assign ring ids
mp: 0
sharding: 1
pure-dp: 2
global: 3
pp: 4
pp-pair: >= 20
if one parallelism is not enable: -1
and only support parallelism hierarchy: mp --> sharding --> pp --> dp
"""
# step 1: initialize nccl
self.global_word_size = self.role_maker._worker_num()
self.global_rank = self.role_maker._worker_index()
self.global_endpoints = self.role_maker._get_trainer_endpoints()
if self._thread_mode:
self.current_endpoint = self.global_endpoints[
self.role_maker._role_id()
]
else:
self.current_endpoint = self.global_endpoints[self.global_rank]
self._collective_helper = CollectiveHelper(
self.role_maker, nrings=self._nrings_sharding
)
assert self.global_word_size % self.mp_degree == 0, (
f"global_word_size: {self.global_word_size} should be divisible to the mp_degree: {self.mp_degree}"
)
assert self.global_word_size % self.sharding_degree == 0, (
f"global_word_size: {self.global_word_size} should be divisible to the sharding_degree: {self.sharding_degree}"
)
assert self.global_word_size % self.pp_degree == 0, (
f"global_word_size: {self.global_word_size} should be divisible to the pp_degree: {self.pp_degree}"
)
assert self.global_word_size % self.dp_degree == 0, (
f"global_word_size: {self.global_word_size} should be divisible to the dp_degree: {self.dp_degree}"
)
# mp group
if self.mp_degree > 1:
self.mp_ring_id = 0
self.mp_rank = self.global_rank % self.mp_degree
self.mp_group_id = self.global_rank // self.mp_degree
self.mp_group_endpoints = [
ep
for idx, ep in enumerate(self.global_endpoints)
if idx // self.mp_degree == self.mp_group_id
]
assert self.current_endpoint in self.mp_group_endpoints
assert len(self.mp_group_endpoints) == self.mp_degree, (
f"num of mp worker in group is [{len(self.mp_group_endpoints)}], but mp group size is [{self.mp_degree}]"
)
else:
self.mp_degree = 1
self.mp_ring_id = -1
self.mp_rank = -1
self.mp_group_id = -1
self.mp_group_endpoints = []
# sharding
if self.sharding_degree > 1:
self.sharding_ring_id = 1
self.sharding_rank = (
self.global_rank // self.mp_degree
) % self.sharding_degree
self.sharding_group_id = self.global_rank // (
self.mp_degree * self.sharding_degree
)
# mp + sharding + ...
if self.mp_degree > 1:
self.sharding_group_endpoints = [
ep
for idx, ep in enumerate(self.global_endpoints)
if (idx // (self.mp_degree * self.sharding_degree))
== self.sharding_group_id
and idx % self.mp_degree == self.mp_rank
]
# sharding + ...
else:
self.sharding_group_endpoints = [
ep
for idx, ep in enumerate(self.global_endpoints)
if (idx // (self.mp_degree * self.sharding_degree))
== self.sharding_group_id
]
assert self.current_endpoint in self.sharding_group_endpoints
else:
self.sharding_degree = 1
self.sharding_ring_id = -1
self.sharding_rank = -1
self.sharding_group_id = -1
self.sharding_group_endpoints = []
# pp
if self.pp_degree > 1:
self.pp_pair_ring_id = 20
# pipeline global ring_id set to 4 for sharding0, mp1, dp2, global3
self.pp_ring_id = 4
self.pp_rank = (
self.global_rank
// (self.sharding_degree * self.mp_degree)
% self.pp_degree
)
# (NOTE): Already adjust for (outer-pure) dp
self.pp_group_id = self.global_rank // (
self.mp_degree * self.sharding_degree * self.pp_degree
)
pp_first_stage_idx = self.global_rank % (
self.sharding_degree * self.mp_degree
) + self.pp_group_id * (
self.mp_degree * self.sharding_degree * self.pp_degree
)
pp_stage_offset = self.sharding_degree * self.mp_degree
self.pp_group_endpoints = []
for i in range(self.pp_degree):
self.pp_group_endpoints.append(
self.global_endpoints[
pp_first_stage_idx + pp_stage_offset * i
]
)
assert self.current_endpoint in self.pp_group_endpoints
else:
self.pp_ring_id = -1
self.pp_degree = 1
self.pp_pair_ring_id = -1
self.pp_rank = -1
self.pp_group_id = -1
self.pp_group_endpoints = []
# outer-pure-dp group
# NOTE (JZ-LIANG) support outer-pure-dp to scale the throughput in 3D parallelism
# e.g. mp-sharding-pp-dp
# sharding-hybrid-dp as one scenario of outer-pure-dp
local_pp_degree = self.pp_degree
if os.getenv("PADDLE_MANUAL_PIPELINE_STAGE", None):
assert self.pp_degree == 2, (
"For manually set pipeline, only pp_degree = 2 is supported."
)
assert (
self.global_word_size
== self.mp_degree * self.sharding_degree * self.dp_degree
), (
f"global work size [{self.global_word_size}], mp_degree [{self.mp_degree}], sharding_degree [{self.sharding_degree}], dp_degree [{self.dp_degree}]."
)
local_pp_degree = 1
else:
assert (
self.global_word_size
== self.mp_degree
* self.sharding_degree
* self.pp_degree
* self.dp_degree
), (
f"mp_degree: [{self.mp_degree}], sharding_degree: [{self.sharding_degree}], pp_degree: [{self.pp_degree}], dp_degree: [{self.dp_degree}]; BUT global nrank: [{self.global_word_size}]"
)
if self.dp_degree > 1:
self.dp_ring_id = 2
self.dp_rank = self.global_rank // (
self.sharding_degree * self.mp_degree * local_pp_degree
)
dp_first_rank_idx = self.global_rank % (
self.sharding_degree * self.mp_degree * local_pp_degree
)
dp_offset = self.sharding_degree * self.mp_degree * local_pp_degree
self.dp_group_endpoints = []
for i in range(self.dp_degree):
self.dp_group_endpoints.append(
self.global_endpoints[dp_first_rank_idx + dp_offset * i]
)
assert self.current_endpoint in self.dp_group_endpoints
logger.info("Hybrid DP mode turn on !")
else:
self.dp_ring_id = -1
self.dp_rank = -1
self.dp_group_endpoints = []
# global group
# use for gen_nccl_comm_sync, amp check nan inf, clip by global norm
# NOTE (JZ-LIANG) when use global ring for calc global norm and dp_degree > 1, the allreduce result should be divided by dp_degree
self.global_ring_id = 3
logger.info(f"global word size: {self.global_word_size}")
logger.info(f"global rank: {self.global_rank}")
logger.info(f"global endpoints: {self.global_endpoints}")
logger.info(f"global ring id: {self.global_ring_id}")
logger.info("#####" * 6)
logger.info(f"mp group size: {self.mp_degree}")
logger.info(f"mp rank: {self.mp_rank}")
logger.info(f"mp group id: {self.mp_group_id}")
logger.info(f"mp group endpoints: {self.mp_group_endpoints}")
logger.info(f"mp ring id: {self.mp_ring_id}")
logger.info("#####" * 6)
logger.info(f"sharding group size: {self.sharding_degree}")
logger.info(f"sharding rank: {self.sharding_rank}")
logger.info(f"sharding group id: {self.sharding_group_id}")
logger.info(
f"sharding group endpoints: {self.sharding_group_endpoints}"
)
logger.info(f"sharding ring id: {self.sharding_ring_id}")
logger.info("#####" * 6)
logger.info(f"pp group size: {self.pp_degree}")
logger.info(f"pp rank: {self.pp_rank}")
logger.info(f"pp group id: {self.pp_group_id}")
logger.info(f"pp group endpoints: {self.pp_group_endpoints}")
logger.info(f"pp ring id: {self.pp_ring_id}")
logger.info("#####" * 6)
logger.info(f"pure dp group size: {self.dp_degree}")
logger.info(f"pure dp rank: {self.dp_rank}")
logger.info(f"pure dp group endpoints: {self.dp_group_endpoints}")
logger.info(f"pure dp ring id: {self.dp_ring_id}")
logger.info("#####" * 6)
def _recreate_not_persist_param_as_var(self):
def recreate_not_persist_param_as_var(program):
block = program.global_block()
params = block.all_parameters()
for param in params:
if param.persistable:
continue
name = param.name
shape = param.shape
dtype = param.dtype
type = param.type
lod_level = param.lod_level
stop_gradient = param.stop_gradient
trainable = param.trainable
optimize_attr = param.optimize_attr
regularizer = param.regularizer
have_dist_attr = False
is_distributed = False
if hasattr(param, 'is_distributed'):
have_dist_attr = True
is_distributed = param.is_distributed
block._remove_var(name, sync=False)
var = block.create_var(
name=name,
shape=shape,
dtype=dtype,
type=type,
lod_level=lod_level,
stop_gradient=stop_gradient,
trainable=trainable,
persistable=False,
)
if have_dist_attr:
var.is_distributed = is_distributed
block._sync_with_cpp()
recreate_not_persist_param_as_var(self._startup_program)
recreate_not_persist_param_as_var(self._main_program)
def _initialization_broadcast(self):
"""
this function is to ensure the initialization between dp group to be
identical when hybrid-dp is used, and the initialization of
not distributed param between mp group to be identical.
"""
if self.dp_degree <= 1 and self.mp_degree <= 1:
return
startup_block = self._startup_program.global_block()
params = startup_block.all_parameters()
params_name = []
not_dist_param_name = set()
for param in params:
params_name.append(param.name)
if not hasattr(param, 'is_distributed') or not param.is_distributed:
not_dist_param_name.add(param.name)
# offload and optimize_cast will insert broadcast op
broadcast_params = set()
for op in startup_block.ops:
if op.type == 'broadcast':
broadcast_params.add(op.desc.output_arg_names()[0])
for param in params_name:
if param in broadcast_params:
continue
rings = []
# need sync not distributed param in mp group
if self.mp_degree > 1 and param in not_dist_param_name:
rings.append(self.mp_ring_id)
if self.dp_degree > 1:
rings.append(self.dp_ring_id)
for ring in rings:
startup_block.append_op(
type='broadcast',
inputs={'x': param},
outputs={'out': param},
attrs={
'ring_id': ring,
'root': 0,
OP_ROLE_KEY: OpRole.Forward,
},
)
startup_block._sync_with_cpp()
# sharding gradient merge
def create_persistable_gradients_and_insert_merge_ops(
self, main_block, startup_block, insert_idx, grad_names, shard
):
for grad_name in grad_names:
assert get_grad_device(grad_name, shard) == shard.worker_idx, (
f"try to merge gradient not belong to current shard: [{grad_name}]"
)
persistable_grad_name = grad_name + '@GradientMerge'
assert grad_name not in self._grad2merged_grad, (
f"grad [{grad_name}] already in grad2merged_grad, maybe you meet sharing weight case !"
)
self._grad2merged_grad[grad_name] = persistable_grad_name
grad_var = main_block.var(grad_name)
# create var
gradient_merge_var = main_block.create_var(
name=persistable_grad_name,
shape=grad_var.shape,
dtype=grad_var.dtype,
persistable=True,
)
startup_gradient_merge_var = startup_block.create_var(
name=persistable_grad_name,
shape=grad_var.shape,
dtype=grad_var.dtype,
persistable=True,
)
# merge gradient
main_block._insert_op_without_sync(
insert_idx,
type="elementwise_add",
inputs={'X': grad_name, 'Y': gradient_merge_var},
outputs={'Out': gradient_merge_var},
attrs={
'axis': -1,
OP_ROLE_KEY: OpRole.Backward,
},
)
# startup initialization
startup_block.append_op(
type="fill_constant",
outputs={"Out": startup_gradient_merge_var},
attrs={
"shape": grad_var.shape,
"dtype": grad_var.dtype,
"value": float(0),
},
)
main_block._sync_with_cpp()
startup_block._sync_with_cpp()
def _create_gm_cond(self, main_block):
# Add const var
acc_step_var = create_global_var(
name="gradient_merge_acc_step",
shape=[1],
value=int(self._gradient_merge_acc_step),
dtype='int32',
persistable=True,
force_cpu=True,
)
zero_var = create_global_var(
name="gradient_merge_zero",
shape=[1],
value=0,
dtype='int32',
persistable=True,
force_cpu=True,
)
# Add step var & cond var
current_step_var = create_global_var(
name="gradient_merge_current_step",
shape=[1],
value=0,
dtype='int32',
persistable=True,
force_cpu=True,
)
cond_var = main_block.create_var(
name="gradient_merge_cond", shape=[1], dtype='bool'
)
with device_guard("cpu"):
# step_var = (step_var + 1) % k_step
main_block.append_op(
type='increment',
inputs={'X': [current_step_var]},
outputs={'Out': [current_step_var]},
attrs={'step': float(1), OP_ROLE_KEY: OpRole.Optimize},
)
main_block.append_op(
type='elementwise_mod',
inputs={'X': current_step_var, 'Y': acc_step_var},
outputs={'Out': current_step_var},
attrs={
'axis': -1,
OP_ROLE_KEY: OpRole.Optimize,
},
)
# cond_var = (step_var == 0)
main_block.append_op(
type='equal',
inputs={'X': current_step_var, 'Y': zero_var},
outputs={'Out': cond_var},
attrs={OP_ROLE_KEY: OpRole.Optimize},
)
# paddle.static.Print(current_step_var, message="in FWBW last conditional")
return cond_var
def _true_apply_gradient(self):
"""
allreduce grad@gradientmerge in dp group
grad@gradientmerge / acc_step
re-create all optimize ops of origin main block and rename them
cast(backward)
amp
clip
opt
# fill constant grad@gradientmerge
"""
# current conditional block
main_block = self._main_program.global_block()
cur_block_idx = self._main_program.current_block_idx
cur_block = self._main_program.current_block()
self.cond_block = self._main_program.current_block()
# cur_block's forward_block & backward_block is itself
cur_block._set_forward_block_idx(cur_block_idx)
# allreduce grad@gradientmerge
if self.hybrid_dp:
assert self.dp_ring_id >= 0, (
"dp_ring_id should larger than 0 when in sharding&DP mode"
)
for grad, merged_grad in self._grad2merged_grad.items():
merged_grad_var = main_block.var(merged_grad)
cur_block.append_op(
type='all_reduce',
inputs={'X': merged_grad_var},
outputs={'Out': merged_grad_var},
attrs={
'ring_id': self.dp_ring_id,
'reduce_type': paddle.distributed.ReduceOp.SUM,
OP_ROLE_KEY: OpRole.Optimize,
},
)
# grad@gradientmerge / acc_step
for grad, merged_grad in self._grad2merged_grad.items():
# grad /= k_steps
merged_grad_var = main_block.var(merged_grad)
cur_block.append_op(
type='scale',
inputs={'X': merged_grad_var},
outputs={'Out': merged_grad_var},
attrs={
'scale': 1.0 / float(self._gradient_merge_acc_step),
'bias': 0.0,
'bias_after_scale': False,
OP_ROLE_KEY: OpRole.Optimize,
},
)
# re-create optimize ops
already_moved_var_names = []
for op_desc in self.original_optimize_ops_desc:
new_op_desc = cur_block.desc.append_op()
new_op_desc.copy_from(op_desc)
for input_name in new_op_desc.input_arg_names():
if input_name in self._grad2merged_grad:
new_op_desc._rename_input(
input_name, self._grad2merged_grad[input_name]
)
for output_name in new_op_desc.output_arg_names():
if output_name in self._grad2merged_grad:
new_op_desc._rename_output(
output_name, self._grad2merged_grad[output_name]
)
# move non temp optimize vars from block0 to cond block
if (
output_name not in already_moved_var_names
and output_name not in self._grad2merged_grad.keys()
):
var_ = self._main_program.global_block().var(output_name)
if not var_.persistable:
# move
name_ = var_.name
shape_ = var_.shape
type_ = var_.dtype
self._main_program.global_block()._remove_var(
var_.name, sync=False
)
self.cond_block.create_var(
name=name_,
shape=shape_,
dtype=type_,
persistable=False,
)
already_moved_var_names.append(name_)
self._main_program.global_block()._sync_with_cpp()
cur_block._sync_with_cpp()
# fill zero to grad@gradientmerge
for grad, merged_grad in self._grad2merged_grad.items():
merged_grad_var = main_block.var(merged_grad)
cur_block.append_op(
type='fill_constant',
outputs={'Out': merged_grad_var},
attrs={
"shape": merged_grad_var.shape,
"dtype": merged_grad_var.dtype,
"value": float(0),
OP_ROLE_KEY: OpRole.Optimize,
},
)
# lr_var = main_block.var("gradient_merge_current_step")
# paddle.static.Print(lr_var, message="in OPTIMIZE last conditional")
def _sharding_gradient_merge(self):
"""
copy all optimize ops in origin main block
remove all optimize ops in origin main block
create cond block
"""
if (
self.gradient_merge_mode != "sharding_gm"
or self._gradient_merge_acc_step <= 1
):
return
main_block = self._main_program.global_block()
# copy original optimize ops to temp ops desc list
# remove them from block 0
tmp_copy_block = self._main_program._create_block()
self.original_optimize_ops_desc = []
for op_idx, op in reversed(list(enumerate(main_block.ops))):
if int(op.attr('op_role')) != int(OpRole.Optimize):
continue
else:
tmp_op_desc = tmp_copy_block.desc.append_op()
tmp_op_desc.copy_from(op.desc)
self.original_optimize_ops_desc.append(tmp_op_desc)
main_block._remove_op(op_idx, sync=False)
tmp_copy_block._sync_with_cpp()
self.original_optimize_ops_desc = list(
reversed(self.original_optimize_ops_desc)
)
# back to block 0
self._main_program._rollback()
# create cond vars and ops at the end of block 0
cond = self._create_gm_cond(main_block)
# create cond block
cond_block = self._main_program._create_block()
self._true_apply_gradient()
# back to block 0
self._main_program._rollback()
# cond op
step_scope = self._main_program.global_block().create_var(
type=core.VarDesc.VarType.STEP_SCOPES
)
conditional_block_op = self._main_program.global_block().append_op(
type='conditional_block',
inputs={
'Cond': cond,
'Input': [],
},
outputs={'Out': [], 'Scope': [step_scope]},
attrs={
'sub_block': cond_block,
'is_scalar_condition': True,
},
)
class ThreadShardingOptimizer(ShardingOptimizer):
"""Sharding Optimizer."""
def __init__(self, optimizer):
super().__init__(optimizer)
self.inner_opt = optimizer
self.meta_optimizers_white_list = [
"ParameterServerOptimizer",
"RecomputeOptimizer",
"AMPOptimizer",
"LarsOptimizer",
"LambOptimizer",
"ASPOptimizer",
# "ModelParallelOptimizer",
# "PipelineOptimizer",
]
self._thread_mode = True
self._use_calc_stream = False
op_maker = core.op_proto_and_checker_maker
self.op_role_key = op_maker.kOpRoleAttrName()
def _prune_main_program(self, block, shard, rings):
"""
rename BroadCast param
"""
var_names = set()
for idx, op in enumerate(block.ops):
for input_name in op.desc.input_arg_names():
pos = input_name.find("@BroadCast")
if pos <= 0:
continue
new_name = input_name[0:pos]
op.desc._rename_input(input_name, new_name)
var_names.add(input_name)
for output_name in op.desc.output_arg_names():
pos = output_name.find("@BroadCast")
if pos <= 0:
continue
new_name = output_name[0:pos]
op.desc._rename_output(output_name, new_name)
var_names.add(output_name)
for var_name in var_names:
block._remove_var(var_name, sync=False)
print("remove broadcast param count=", len(var_names))
block._sync_with_cpp()
def _prune_startup_program(self, block, shard):
"""
not need process
"""
block._sync_with_cpp()
def minimize_impl(
self, loss, startup_program=None, parameter_list=None, no_grad_set=None
):
"""
reset start program and main program
"""
sharding_configs = self.user_defined_strategy.sharding_configs
if "use_calc_stream" in sharding_configs:
self._use_calc_stream = sharding_configs["use_calc_stream"]
optimize_ops, params_grads = super().minimize_impl(
loss, startup_program, parameter_list, no_grad_set
)
# main_block = self._main_program.global_block()
# startup_block = self._startup_program.global_block()
loss.block.program = self._main_program
from paddle import fluid
fluid.framework.switch_startup_program(self._startup_program)
return optimize_ops, params_grads
def _init_comm(self):
# sync var
self.role_id = self.role_maker._role_id()
self.node_nums = self.role_maker._node_num()
startup_block = self._startup_program.global_block()
node_nums = len(self.global_endpoints)
assert self.node_nums == node_nums, "end points not equal node nums"
self.current_endpoint = self.global_endpoints[self.role_id]
# mp ring
if self.mp_degree > 1:
self._init_communicator(
self._startup_program,
self.current_endpoint,
self.mp_group_endpoints,
self.role_id,
self.mp_ring_id,
)
# sharding ring
if self.sharding_degree > 1:
self._init_communicator(
self._startup_program,
self.current_endpoint,
self.sharding_group_endpoints,
self.role_id,
self.sharding_ring_id,
)
# pure dp ring
if self.dp_degree > 1:
self._init_communicator(
self._startup_program,
self.current_endpoint,
self.dp_group_endpoints,
self.role_id,
self.dp_ring_id,
)
startup_block._sync_with_cpp()
def _wait(self):
if len(self.global_endpoints) <= 1:
return
endpoints = self.global_endpoints[:]
current_endpoint = endpoints[self.role_maker._role_id()]
if self.global_rank == 0:
from paddle.fluid.transpiler.details import wait_server_ready
endpoints.remove(current_endpoint)
wait_server_ready(endpoints)
def _init_communicator(
self, program, current_endpoint, endpoints, role_id, ring_id
):
nranks = len(endpoints)
block = program.global_block()
# init multi node nccl
if nranks > 1:
other_endpoints = endpoints[:]
other_endpoints.remove(current_endpoint)
nccl_id_var = block.create_var(
name=unique_name.generate('nccl_id'),
persistable=True,
type=core.VarDesc.VarType.RAW,
)
block.append_op(
type='c_gen_nccl_id',
inputs={},
outputs={'Out': nccl_id_var},
attrs={
'rank': role_id,
'endpoint': current_endpoint,
'other_endpoints': other_endpoints,
self.op_role_key: OpRole.Forward,
},
)
block.append_op(
type='c_comm_init_multitrainer',
inputs={'X': nccl_id_var},
outputs={},
attrs={
'ntrainers': nranks,
'trainer_id': role_id,
'ring_id': ring_id,
self.op_role_key: OpRole.Forward,
},
)
else:
block.append_op(type='comm_init_all', attrs={'ring_id': ring_id})