Files
paddlepaddle--paddle/python/paddle/incubate/optimizer/pipeline.py
T
2026-07-13 12:40:42 +08:00

1968 lines
82 KiB
Python

# Copyright (c) 2019 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import warnings
from collections import defaultdict
from functools import cmp_to_key, reduce
import numpy as np
import paddle
from paddle.base import core, unique_name
from paddle.base.framework import (
Parameter,
Program,
default_startup_program,
in_dygraph_mode,
)
__all__ = []
class PipelineOptimizer:
"""
:api_attr: Static Graph
Pipeline Optimizer: Make a program to run as pipeline, that is splitting a
program into multiple sections (sub-programs) and each section run on a
device to enable the training of large scale models and the use of
heterogeneous devices. Meanwhile, all sections run in the stype of pipeline.
Args:
optimizer (Optimizer): The optimizer to use, such as SGD.
num_microbatches (int): Number of microbatches. [Optional. Default:1].
start_cpu_core_id (int): The first cpu core id to use. [Optional. Default:0].
Examples:
.. code-block:: pycon
>>> import paddle
>>> import paddle.base as base
>>> import paddle.base.layers as layers
>>> import numpy as np
>>> paddle.enable_static()
>>> with base.device_guard("gpu:0"):
... x = paddle.static.data(name='x', shape=[-1, 1], dtype='int64')
... y = paddle.static.data(name='y', shape=[-1, 1], dtype='int64')
... data_loader = base.io.DataLoader.from_generator(
... feed_list=[x, y],
... capacity=64,
... use_double_buffer=True,
... iterable=False,
... )
... emb_x = layers.embedding(input=x, param_attr=base.ParamAttr(name="embx"), size=[10,2], is_sparse=False)
... emb_y = layers.embedding(input=y, param_attr=base.ParamAttr(name="emby",learning_rate=0.9), size=[10,2], is_sparse=False)
>>> with base.device_guard("gpu:1"):
... concat = layers.concat([emb_x, emb_y], axis=1)
... fc = paddle.static.nn.fc(x=concat, name="fc", size=1, num_flatten_dims=1, bias_attr=False)
... loss = paddle.mean(fc)
>>> optimizer = paddle.optimizer.SGD(learning_rate=0.5)
>>> optimizer = paddle.incubate.optimizer.PipelineOptimizer(optimizer)
>>> optimizer.minimize(loss)
>>> def train_reader():
... for _ in range(4):
... x = np.random.random(size=[1]).astype('int64')
... y = np.random.random(size=[1]).astype('int64')
... yield x, y
>>> data_loader.set_sample_generator(train_reader, batch_size=1)
>>> place = paddle.CUDAPlace(0)
>>> exe = paddle.static.Executor(place)
>>> exe.run(paddle.static.default_startup_program())
>>> batch_size = 1
>>> data_loader.start()
>>> exe.train_from_dataset(paddle.static.default_main_program())
>>> data_loader.reset()
"""
def __init__(self, optimizer, num_microbatches=1, start_cpu_core_id=0):
self._device = 'cpu'
if core.is_compiled_with_cuda():
self._device = "gpu"
if in_dygraph_mode():
raise Exception("In dygraph, don't support PipelineOptimizer.")
valid_optimizers = (
paddle.optimizer.Optimizer,
paddle.static.amp.decorator.OptimizerWithMixedPrecision,
)
if not isinstance(optimizer, valid_optimizers):
raise ValueError(
"The 'optimizer' parameter for "
"PipelineOptimizer must be an instance of "
f"{valid_optimizers}, but the given type is {type(optimizer)}."
)
self._optimizer = optimizer
# Get the original optimizer defined by users, such as SGD
self._origin_optimizer = self._optimizer
while hasattr(self._origin_optimizer, "inner_opt"):
self._origin_optimizer = self._origin_optimizer.inner_opt
assert num_microbatches >= 1, (
"num_microbatches must be a positive value."
)
self._num_microbatches = num_microbatches
assert start_cpu_core_id >= 0, (
"start_cpu_core_id must be a non-negative integer."
)
self._start_cpu_core_id = start_cpu_core_id
self._place_list = None
op_maker = core.op_proto_and_checker_maker
self._op_role = op_maker.OpRole
self._op_role_key = op_maker.kOpRoleAttrName()
self._op_role_var_key = op_maker.kOpRoleVarAttrName()
self._op_device_key = op_maker.kOpDeviceAttrName()
self._param_device_map = None
self._pipeline_pair = []
self._pp_ring_map = {}
self.output_var_to_op = None
self.input_var_to_op = None
# insert allreduce op to sync global information for global
# gradient clip and amp
def _insert_allreduce_op(self, op_idx, block):
"""
Insert allreduce op to sync global information for global
gradient clip and amp.
"""
op = block.ops[op_idx]
out_name = op.desc.output_arg_names()[0]
out_var = block.var(out_name)
offset = 0
if op.type == "reduce_any":
# cast the bool var to int32 to use allreduce_max op
temp_var_name = unique_name.generate(out_name + "_cast_int32")
temp_var = block.create_var(
name=temp_var_name, shape=[1], dtype="int32"
)
block._insert_op(
op_idx + 1 + offset,
type='cast',
inputs={'X': out_var},
outputs={'Out': temp_var},
attrs={
'in_dtype': out_var.dtype,
'out_dtype': temp_var.dtype,
self._op_role_key: self._op_role.Optimize,
},
)
offset += 1
block._insert_op(
op_idx + 1 + offset,
type='all_reduce',
inputs={'x': temp_var if op.type == "reduce_any" else out_var},
outputs={'out': temp_var if op.type == "reduce_any" else out_var},
attrs={
'ring_id': self.global_ring_id,
self._op_role_key: self._op_role.Optimize,
'reduce_type': (
paddle.distributed.ReduceOp.MAX
if op.type == "reduce_any"
else paddle.distributed.ReduceOp.SUM
),
},
)
offset += 1
if op.type == "reduce_any":
block._insert_op(
op_idx + 1 + offset,
type='cast',
inputs={'X': temp_var},
outputs={'Out': out_var},
attrs={
'in_dtype': temp_var.dtype,
'out_dtype': out_var.dtype,
self._op_role_key: self._op_role.Optimize,
},
)
offset += 1
return offset
def _create_vars(self, block, ori_block):
# Create vars for block, copied from ori_block
used_var_set = set()
added_op_num = 0
op_idx = 0
op_size = block.desc.op_size()
while op_idx < op_size + added_op_num:
# Whether to insert allreduce_sum or allreduce_max op.
# For amp and global gradient clip strategies, we should
# get the global information, so allreduce op is needed.
should_insert = False
op = block.ops[op_idx]
# For op process vars on all devices, remove its input
# vars not in this block
reserved_x = []
if op.type == 'reduce_any' and self._is_optimize_op(op):
should_insert = True
elif op.type == 'concat' and self._is_optimize_op(op):
for input_name in op.desc.input("X"):
if block._find_var_recursive(input_name):
reserved_x.append(input_name)
op.desc.set_input('X', reserved_x)
elif op.type == 'update_loss_scaling':
for input_name in op.desc.input("X"):
if block._find_var_recursive(input_name):
reserved_x.append(input_name)
op.desc.set_input('X', reserved_x)
op.desc.set_output('Out', reserved_x)
elif op.type == 'check_finite_and_unscale':
for input_name in op.desc.input("X"):
if block._find_var_recursive(input_name):
reserved_x.append(input_name)
op.desc.set_input('X', reserved_x)
op.desc.set_output('Out', reserved_x)
if len(reserved_x) == 0:
block._remove_op(op_idx)
op_size -= 1
continue
elif op.type == 'sum' and self._is_gradient_clip_op(op):
for input_name in op.desc.input("X"):
if block._find_var_recursive(input_name):
reserved_x.append(input_name)
op.desc.set_input('X', reserved_x)
should_insert = True
vars = op.desc.input_arg_names() + op.desc.output_arg_names()
for var in vars:
# a var whose name contains "blocking_queue"
# only exists in startup program
if var in used_var_set or "_blocking_queue" in var:
continue
used_var_set.add(var)
if block._find_var_recursive(str(var)):
continue
source_var = ori_block._var_recursive(str(var))
if source_var.type == core.VarDesc.VarType.READER:
dest_var = block.create_var(
name=var,
type=core.VarDesc.VarType.READER,
persistable=source_var.persistable,
)
elif isinstance(source_var, Parameter):
dest_var = block.create_parameter(
name=source_var.name,
shape=source_var.shape,
dtype=source_var.dtype,
type=source_var.type,
lod_level=source_var.lod_level,
stop_gradient=source_var.stop_gradient,
trainable=source_var.trainable,
optimize_attr=source_var.optimize_attr,
regularizer=source_var.regularizer,
error_clip=source_var.error_clip,
)
else:
dest_var = block._clone_variable(source_var, False)
self._clone_var_attr(dest_var, source_var)
# When use with sharding, allreduce_sum and allreduce_max
# used for global gradient clip and amp will be added by sharding.
op_idx += 1
if self.use_sharding or not should_insert:
continue
inserted_ops = self._insert_allreduce_op(op_idx - 1, block)
added_op_num += inserted_ops
op_idx += inserted_ops
block._sync_with_cpp()
def _is_loss_grad_op(self, op):
assert self._op_role_key in op.attr_names
op_role = int(op.attr(self._op_role_key))
return op_role & int(self._op_role.Backward) and op_role & int(
self._op_role.Loss
)
def _is_forward_op(self, op):
return self._op_role_key in op.attr_names and (
int(op.attr(self._op_role_key)) == int(self._op_role.Forward)
)
def _is_backward_op(self, op):
return self._op_role_key in op.attr_names and (
int(op.attr(self._op_role_key)) & int(self._op_role.Backward)
)
def _is_loss_op(self, op):
assert self._op_role_key in op.attr_names
return int(op.attr(self._op_role_key)) == int(self._op_role.Loss)
def _is_optimize_op(self, op):
return self._op_role_key in op.attr_names and (
int(op.attr(self._op_role_key)) & int(self._op_role.Optimize)
)
def _is_update_op(self, op):
return (
'Param' in op.input_names
and 'Grad' in op.input_names
and ("LearningRate" in op.input_names)
)
def _split_program(self, main_program, devices):
"""
Split a program into sections according to devices that ops run on.
The op whose op_device attr is "gpu:all" is copied to all sections.
Args:
main_program (Program): the main program
devices: all used devices
"""
# Map from device to its corresponding section program info
device_program_map = defaultdict(Program)
block = main_program.block(0)
for op in block.ops:
device = op.attr(self._op_device_key)
# Copy ops whose op_device set to "gpu:all" to all sections.
if device == f"{self._device}:all":
for device in devices:
program = device_program_map[device]
op_desc = op.desc
ap_op = program.global_block().desc.append_op()
ap_op.copy_from(op_desc)
ap_op._set_attr(self._op_device_key, "")
else:
program = device_program_map[device]
op_desc = op.desc
ap_op = program.global_block().desc.append_op()
ap_op.copy_from(op_desc)
ap_op._set_attr(self._op_device_key, "")
program_list = []
for key in devices:
program = device_program_map[key]
program._sync_with_cpp()
program_list.append(program)
return program_list
def _get_op_device_for_startup_program(self, var_name):
"""
For adam optimizer, it will add accumulators and initialize them
with fill_constant, and force the op device to cpu. Hence, we should
get the real op_device attribute of the fill_constant as the device
where the corresponding parameters on.
"""
assert "beta1_pow_acc" in var_name or "beta2_pow_acc" in var_name, (
'For accumulators for Adam, the name must contain beta1_pow_acc '
'or beta2_pow_acc.'
)
param_name = var_name[0 : var_name.index('_beta')]
device = self._param_device_map[param_name]
return device
def _split_startup_program(self, startup_program, device_id):
block = startup_program.global_block()
new_startup_program = Program()
for op in block.ops:
device = op.attr(self._op_device_key)
if device == "cpu":
assert op.type == "fill_constant", (
"For ops in startup program with the op_device attribute "
"of cpu, they must be of type fill_constant."
)
output_var = op.output_arg_names[0]
device = self._get_op_device_for_startup_program(output_var)
if device:
device_index = int(device.split(':')[1])
else:
# LR related ops
device = None
if device and device_index != device_id:
continue
op_desc = op.desc
ap_op = new_startup_program.global_block().desc.append_op()
ap_op.copy_from(op_desc)
ap_op._set_attr(self._op_device_key, "")
new_startup_program._sync_with_cpp()
self._create_vars(new_startup_program.global_block(), block)
return new_startup_program
def _find_post_op(self, index, var_name):
"""
Find the post op that has variable named var_name as input.
"""
# bugfix for uniform hybrid parallelism
if '.cast_fp32' in var_name:
var_name = var_name.replace('.cast_fp32', '')
if '.cast_fp16' in var_name:
var_name = var_name.replace('.cast_fp16', '')
post_ops = self.input_var_to_op[var_name]
if post_ops is None:
return None
result_op = None
for post_op, post_idx in reversed(post_ops):
if post_idx > index:
result_op = post_op
break
return result_op
def _find_prev_op(self, index, var_name):
"""
Find the previous op of op with index that outputs
variable named var_name.
"""
prev_ops = self.output_var_to_op[var_name]
if prev_ops is None:
return None
result_op = None
for prev_op, prev_idx in reversed(prev_ops):
if prev_idx < index:
result_op = prev_op
break
return result_op
def _rename_arg(self, op, old_name, new_name):
op._rename_input(old_name, new_name)
op._rename_output(old_name, new_name)
def _create_var(self, block, ref_var, name, dtype=None):
"""
Create a new var for block, which has the same type,
shape and dtype as ref_var, then rename it with the
name `name`.
"""
new_var = block.create_var(
name=name,
shape=ref_var.shape,
dtype=ref_var.dtype if dtype is None else dtype,
type=ref_var.type,
lod_level=ref_var.lod_level,
persistable=ref_var.persistable,
is_data=ref_var.is_data,
need_check_feed=ref_var.desc.need_check_feed(),
)
self._clone_var_attr(new_var, ref_var)
return new_var
def _clone_var_attr(self, dest, src):
dest.stop_gradient = src.stop_gradient
if hasattr(src, 'is_distributed'):
dest.is_distributed = src.is_distributed
def _strip_grad_suffix(self, name):
"""
Strip the grad suffix from the given variable name
"""
pos = name.find(core.grad_var_suffix())
return name[:pos] if pos != -1 else name
def _append_grad_suffix(self, name):
"""
Append grad suffix to the given variable name
"""
return name + core.grad_var_suffix()
def _get_op_device_attr(self, op):
"""
Get the op_device attribute of a op.
"""
device = (
op.attr(self._op_device_key)
if op.has_attr(self._op_device_key)
else None
)
if device:
assert device[0:3] == 'gpu', (
"Now, only gpu devices are supported in pipeline parallelism."
)
return device
def _add_op_device_attr_for_op(self, op, idx, block):
"""
Add op_device attribute for ops that have not that attribute set.
We use "gpu:all" to represent the op should be put on all
sub-programs, such as lr-related ops. Note that: "gpu:all"
is only used by pipeline as an indicator.
"""
lrsched_role = int(self._op_role.LRSched)
if op.attr(self._op_role_key) == lrsched_role:
# For LRSched ops, we should put them on all sub-programs to
# make sure each sub-program update the lr correctly
op._set_attr(self._op_device_key, f"{self._device}:all")
# bugfix in hybrid parallelism
elif op.type == "sum" and self._is_backward_op(op):
# For sum ops that compute the sum of @RENAMED@ vars
for name in op.desc.input_arg_names():
assert '@RENAME@' in name, (
"The op must be sum used to accumulate renamed vars."
)
assert len(op.desc.output_arg_names()) == 1
out_name = op.desc.output_arg_names()[0]
post_op = self._find_post_op(idx, out_name)
assert post_op.has_attr('op_device'), (
f"{post_op.type} has no op_device attr for var {out_name}"
)
device = post_op.attr(self._op_device_key)
assert device, "The post op must have op_device set."
op._set_attr(self._op_device_key, device)
elif (op.type == "cast" or op.type == "scale") and (
self._is_backward_op(op) or self._is_forward_op(op)
):
prev_op = self._find_prev_op(idx, op.desc.input("X")[0])
op._set_attr(self._op_device_key, prev_op.attr(self._op_device_key))
elif op.type == "memcpy" and not self._is_optimize_op(op):
# for checkpoint offloading
assert (
len(op.input_arg_names) == 1 and len(op.output_arg_names) == 1
)
input_name = op.input_arg_names[0]
output_name = op.output_arg_names[0]
if '@Fetch' in output_name:
post_op = self._find_post_op(idx, output_name)
op._set_attr(
self._op_device_key, post_op.attr(self._op_device_key)
)
else:
prev_op = self._find_prev_op(idx, op.desc.input("X")[0])
op._set_attr(
self._op_device_key, prev_op.attr(self._op_device_key)
)
elif self._is_loss_op(op):
# For loss * loss_scaling op added by AMP
offset = 1
while not block.ops[idx + offset].has_attr(
self._op_device_key
) or not block.ops[idx + offset].attr(self._op_device_key):
offset += 1
device = block.ops[idx + offset].attr(self._op_device_key)
assert device, "Please put you program within device_guard scope."
for i in range(offset):
block.ops[idx + i]._set_attr(self._op_device_key, device)
elif self._is_optimize_op(op) and op.type == "cast":
# For fp16-->fp32 cast added by AMP
grad_name = op.output('Out')
assert len(grad_name) == 1
param_name = self._strip_grad_suffix(grad_name[0])
device = self._param_device_map[param_name]
op._set_attr(self._op_device_key, device)
elif self._is_gradient_clip_op(op) or self._is_regularization_op(op):
# For gradient clip and regularization ops, we set their op_device
# attribute to the device where their corresponding parameters on.
assert self._op_role_var_key in op.attr_names, (
"gradient_clip "
"and regularization ops must have op_role_var attribute."
)
op_role_var = op.attr(self._op_role_var_key)
assert len(op_role_var) == 2, (
"op_role_var for gradient_clip "
"regularization ops must have two elements."
)
param_name = op_role_var[0]
device = self._param_device_map[param_name]
# For sum op added by global gradient clip, it must be
# put on all devices
if (
op.type == 'sum'
or op.type == 'sqrt'
or op.type == 'fill_constant'
or op.type == 'elementwise_max'
or op.type == 'elementwise_div'
):
device = f"{self._device}:all"
op._set_attr(self._op_device_key, device)
elif op.type == "alloc_float_status" or op.type == "clear_float_status":
op._set_attr(self._op_device_key, f"{self._device}:all")
# NOTE(wangxi): NPU should only clear the float status
# once at each batch step
op._set_attr(self._op_role_key, self._op_role.LRSched)
float_status_name = op.output_arg_names[0]
float_status_var = block.var(float_status_name)
# FIXME(wangxi): pipeline lr schedule will exec on sub_scope(0)
# while update will exec on sub_scope(last_micro_step), should
# set persistable to use global scope
float_status_var.persistable = True
else:
other_known_ops = [
'update_loss_scaling',
'reduce_any',
'concat',
'sum',
'check_finite_and_unscale',
'memcpy',
]
assert op.type in other_known_ops, (
"For other ops without "
f"op_device set, they must be one of {other_known_ops}, but it "
f"is {op.type}"
)
assert self._is_optimize_op(op)
op._set_attr(self._op_device_key, f"{self._device}:all")
def _add_op_device_attr(self, block):
"""
Add op_device attribute for ops in block that have
not that attribute set.
"""
for idx, op in enumerate(list(block.ops)):
if (
op.type == "create_py_reader"
or op.type == "read"
or op.type == "create_double_buffer_reader"
):
# Copy read related ops to all section to make them exit
# after each epoch.
# We use "gpu:all" to represent the op should be put on all
# sub-programs, such as lr-related ops. Note that: "gpu:all"
# is only used by pipeline as an indicator.
op._set_attr(self._op_device_key, f"{self._device}:all")
continue
# op_device attribute has been set
if self._get_op_device_attr(op):
continue
self._add_op_device_attr_for_op(op, idx, block)
def _check_validation(self, block):
"""
Check whether ops in a block have both the op_device and the
op_role attributes set.
Then, return all devices in order.
"""
device_list = []
# Section worker only supports the following op_role
valid_op_role_value = [
int(self._op_role.LRSched),
int(self._op_role.Forward),
int(self._op_role.Backward),
int(self._op_role.Loss),
int(self._op_role.Optimize),
int(self._op_role.Backward) | int(self._op_role.Loss),
]
for op in block.ops:
if not op._has_kernel(op.type):
assert op.type == "conditional_block" and (
op.attr(self._op_role_key) == int(self._op_role.LRSched)
), (
"Now, the only supported op without kernel is "
"conditional_block, and its op role must be LRSched."
)
assert op.has_attr(self._op_role_key), (
f"op ({op.type}) has no {self._op_role_key} attribute."
)
op_role = op.attr(self._op_role_key)
assert int(op_role) in valid_op_role_value, (
f"op_role {op_role} for op {op.type} must be one of {valid_op_role_value}"
)
assert op.has_attr(self._op_device_key), (
f"op ({op.type}) has no {self._op_device_key} attribute."
)
device = op.attr(self._op_device_key)
assert device, (
f"op_device attribute for op {op.type} has not been set."
)
if device == f"{self._device}:all":
continue
dev_type = device.split(':')[0]
assert dev_type == "gpu", (
"Now only gpu devices are supported for pipeline parallelism."
)
if device not in device_list:
device_list.append(device)
return device_list
def _insert_sendrecv_ops_for_boundaries(self, block):
"""
Insert a pair of send and recv ops for every two
consecutive ops on different devices.
"""
# A map from var to device where op takes it as input,
# avoiding multiple send and recv ops.
input_var_to_device = {}
# bugfix hybrid parallelism
first_optimize_index = None
for index, op in enumerate(list(block.ops)):
if self._is_optimize_op(op):
first_optimize_index = index
break
extra_index_info = {
'index': 0,
'first_optimize_index': first_optimize_index,
}
for index, op in enumerate(list(block.ops)):
cur_device = op.attr(self._op_device_key)
if cur_device == f"{self._device}:all":
continue
for var_name in op.input_arg_names:
var = block.var(var_name)
# skip data var
if var.is_data:
continue
prev_device = None
prev_op = self._find_prev_op(index, var_name)
if prev_op is None:
if var_name not in self._param_device_map:
continue
prev_device = self._param_device_map[var_name]
if not prev_device:
prev_device = (
prev_op.attr(self._op_device_key) if prev_op else None
)
if prev_device is None or prev_device == f"{self._device}:all":
continue
if prev_device == cur_device:
continue
if var_name not in input_var_to_device:
input_var_to_device[var_name] = []
if (cur_device, prev_device) in input_var_to_device[var_name]:
continue
device_type = cur_device.split(':')[0] + ':'
def _check_stage(cur_id, prev_id):
# check send/recv stage valid
is_forward = self._is_forward_op(op)
is_backward = self._is_backward_op(op)
assert is_forward or is_backward, (
'send/recv in pipeline should only be inserted in forward or backward,'
f'please check the op_role of op={op}'
)
if is_forward:
assert prev_id < cur_id, (
"In forward, send/recv can only be passed forward, but now "
f"prev_stage={prev_id} great than cur_stage={cur_id}, please check op_device of op={op}"
)
elif is_backward:
assert prev_id > cur_id, (
"In backward, send/recv can only be passed backward, but now "
f"prev_stage={prev_id} less than cur_stage={cur_id}, please check op_device of op={op}"
)
def _insert_send_recv(cur_id, prev_id):
cur_dev = device_type + str(cur_id)
prev_dev = device_type + str(prev_id)
if (cur_dev, prev_dev) in input_var_to_device[var_name]:
return
if cur_id - prev_id > 1:
_insert_send_recv(cur_id - 1, prev_id)
_insert_send_recv(cur_id, cur_id - 1)
input_var_to_device[var_name].append(
(cur_dev, prev_dev)
)
return
elif cur_id - prev_id < -1:
_insert_send_recv(cur_id + 1, prev_id)
_insert_send_recv(cur_id, cur_id + 1)
input_var_to_device[var_name].append(
(cur_dev, prev_dev)
)
return
assert abs(cur_id - prev_id) == 1
input_var_to_device[var_name].append((cur_dev, prev_dev))
op_role = op.attr(self._op_role_key)
var = block.vars[var_name]
pair = (prev_id, cur_id)
# 1000 is just a magic number
pair_key = prev_id * 1000 + cur_id
if pair not in self._pipeline_pair:
self._pipeline_pair.append(pair)
self._pp_ring_map[pair_key] = self.ring_id
ring_id = self.ring_id
self.ring_id += 1
else:
ring_id = self._pp_ring_map[pair_key]
if self.schedule_mode == 'F-then-B': # F-then-B
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type='send_v2',
inputs={'X': var},
attrs={
self._op_device_key: prev_dev,
self._op_role_key: op_role,
'use_calc_stream': True,
'peer': 1,
'ring_id': ring_id,
},
)
extra_index_info['index'] += 1
var_shape = list(var.shape)
var_shape[0] = (
self.micro_batch_size
if var_shape[0] < 0
else var_shape[0]
)
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type='recv_v2',
outputs={'Out': [var]},
attrs={
'out_shape': var_shape,
'dtype': var.dtype,
self._op_device_key: cur_dev,
self._op_role_key: op_role,
'use_calc_stream': True,
'peer': 0,
'ring_id': ring_id,
},
)
extra_index_info['index'] += 1
elif self.schedule_mode == '1F1B': # 1F1B
var_shape = list(var.shape)
var_shape[0] = (
self.micro_batch_size
if var_shape[0] < 0
else var_shape[0]
)
numel = np.prod(var_shape)
use_mp = (self.mp_degree > 1) and (
numel % self.mp_degree == 0
)
if 'subprog' in var.name:
# For recompute, if the checkpoints var is layer_norm_6.tmp_2
# this var will be sent twice, layer_norm_6.tmp_2 for forward pass,
# layer_norm_6.tmp_2.subprog_* for recompute pass.
# We can store the first sent var and copy the value to the
# second one to reduce one send/recv op.
# The origin_ckpt_name is layer_norm_6.tmp_2, which will be used
# to find the stored var for the forward pass.
origin_name = var.name.split('subprog')[0][0:-1]
associate_var = block.var(origin_name)
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type='assign',
inputs={'X': [associate_var]},
outputs={'Out': [var]},
attrs={
'out_shape': var_shape,
'dtype': var.dtype,
self._op_device_key: cur_dev,
self._op_role_key: op_role,
'use_calc_stream': True,
},
)
extra_index_info['index'] += 1
return
_check_stage(cur_id, prev_id)
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type='c_sync_calc_stream',
inputs={'X': [var]},
outputs={'Out': [var]},
attrs={
self._op_device_key: prev_dev,
self._op_role_key: op_role,
},
)
extra_index_info['index'] += 1
prefix_name = var.name.split('@')[0]
prefix_var = block.var(prefix_name)
is_param = (
True if isinstance(prefix_var, Parameter) else False
)
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type=(
'send_v2'
if not use_mp or is_param
else 'partial_send'
),
inputs={'X': var},
attrs={
self._op_device_key: prev_dev,
self._op_role_key: op_role,
'use_calc_stream': False,
'ring_id': ring_id,
'peer': 1,
# if send_v2, num&id attr is not in op_attrs, will not insert
'num': self.mp_degree,
'id': self.mp_rank,
},
)
extra_index_info['index'] += 1
insert_index = None
if int(op_role) == int(self._op_role.Backward):
insert_index = extra_index_info[
'first_optimize_index'
]
new_op_role = self._op_role.Optimize
else:
insert_index = index
new_op_role = self._op_role.Backward
sync_comm_op = block._insert_op_without_sync(
index=insert_index + extra_index_info['index'],
type='c_sync_comm_stream',
inputs={'X': [var]},
outputs={'Out': [var]},
attrs={
self._op_device_key: prev_dev,
self._op_role_key: new_op_role,
'ring_id': ring_id,
},
)
if int(op_role) == int(self._op_role.Forward):
sync_comm_op._set_attr('pipeline_flag', '')
extra_index_info['index'] += 1
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type=(
'recv_v2'
if not use_mp or is_param
else 'partial_recv'
),
outputs={'Out': [var]},
attrs={
'out_shape': var_shape,
'dtype': var.dtype,
self._op_device_key: cur_dev,
self._op_role_key: op_role,
'use_calc_stream': True,
'peer': 0,
'ring_id': ring_id,
# if recv_v2, num&id attr is not in op_attrs, will not insert
'num': self.mp_degree,
'id': self.mp_rank,
},
)
extra_index_info['index'] += 1
if use_mp and not is_param:
block._insert_op_without_sync(
index=index + extra_index_info['index'],
type='partial_allgather',
inputs={'X': [var]},
outputs={'Out': [var]},
attrs={
self._op_device_key: cur_dev,
self._op_role_key: op_role,
'use_calc_stream': True,
'ring_id': 0,
# if recv_v2, num&id attr is not in op_attrs, will not insert
'nranks': self.mp_degree,
'rank': self.mp_rank,
},
)
extra_index_info['index'] += 1
else:
raise ValueError(
"Now only 'F-then-B' and '1F1B' are supported."
f"The given value is {self.schedule_mode}."
)
_insert_send_recv(
int(cur_device.split(':')[1]),
int(prev_device.split(':')[1]),
)
block._sync_with_cpp()
def _insert_loss_scale(self, block):
"""
Scale the loss corresponding to number of micro-batches.
"""
if self._num_microbatches == 1:
return
for index, op in reversed(tuple(enumerate(list(block.ops)))):
if self._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 / self._num_microbatches
op._set_attr('value', loss_scale)
break
def _rename_gradient_var_name(self, block):
for index, op in enumerate(block.ops):
if not self._is_optimize_op(op):
continue
input_names = op.input_arg_names
output_names = op.output_arg_names
in_out_names = input_names + output_names
if op.type == 'cast' or op.type == "c_sync_comm_stream":
continue
# append "MERGED" to the names of parameter gradients,
# and modify the op_role_var attribute (by rename_arg func).
for name in in_out_names:
if core.grad_var_suffix() not in name:
continue
param_name = name.strip(core.grad_var_suffix())
new_grad_name = name + "@MERGED"
self._rename_arg(op, name, new_grad_name)
def _accumulate_gradients(
self, block, pp_allreduce_in_optimize=False, strategy=None, shard=None
):
"""
Create a new merged gradient for each parameter and accumulate the
corresponding gradient to it.
"""
fp16_allreduce = strategy.fp16_allreduce if strategy else False
if strategy and strategy.fuse_grad_merge:
fused_gradient_names = self._accumulate_gradients_with_fuse(
block, fp16_allreduce, strategy.fuse_grad_size_in_MB, shard
)
return fused_gradient_names
merged_gradient_names = []
first_opt_op_idx = None
merged_suffix = '@MERGED@FP16' if fp16_allreduce else '@MERGED'
dtype = paddle.float16 if fp16_allreduce else None
for index, op in reversed(tuple(enumerate(list(block.ops)))):
# remove the cast op of fp16 grad to fp32 grad
if self._is_optimize_op(op) and op.type == 'cast':
in_name = op.input_arg_names[0]
out_name = op.output_arg_names[0]
if out_name.strip('@GRAD') in self._param_device_map:
assert in_name.replace('.cast_fp16', '') == out_name
block._remove_op(index)
continue
if self._is_backward_op(op) and first_opt_op_idx is None:
first_opt_op_idx = index + 1
# maybe have no optimize
# if first_opt_op_idx == len(block.ops): return
if self._is_backward_op(op) and (
self._op_role_var_key in op.attr_names
):
op_role_var = op.attr(self._op_role_var_key)
if len(op_role_var) == 0:
continue
assert len(op_role_var) % 2 == 0
for i in range(0, len(op_role_var), 2):
offset = 0
param_name = op_role_var[i]
if not block.has_var(param_name):
continue
if '@BroadCast' in param_name:
continue
param_grad_name = param_name + core.grad_var_suffix()
merged_param_grad_name = param_grad_name + merged_suffix
if not block.has_var(merged_param_grad_name):
self._create_var(
block,
block.vars[param_name],
merged_param_grad_name,
dtype,
)
assert block.has_var(merged_param_grad_name)
param_grad_var = block.var(param_grad_name)
merged_param_grad_var = block.var(merged_param_grad_name)
merged_param_grad_var.persistable = True
block._insert_op(
index=first_opt_op_idx + offset,
type='fill_constant',
inputs={},
outputs={'Out': [merged_param_grad_var]},
attrs={
'shape': merged_param_grad_var.shape,
'dtype': merged_param_grad_var.dtype,
'value': float(0),
# a trick to run this op once per mini-batch
self._op_role_key: self._op_role.Optimize.LRSched,
},
)
offset += 1
grad_name = op_role_var[i + 1]
grad_var = block.vars[grad_name]
is_fp16_grad = 'cast_fp16' in grad_name
need_cast = is_fp16_grad is not fp16_allreduce
if need_cast:
# if fp16_allreduce:
# cast grad to fp16 to accumulate to merged gradient
# else:
# cast grad to fp32 to accumulate to merged gradient
cast_grad_var_name = param_grad_name + '@TMP'
cast_grad_var = self._create_var(
block, param_grad_var, cast_grad_var_name, dtype
)
cast_grad_var.persistable = False
block._insert_op(
index=first_opt_op_idx + offset,
type='cast',
inputs={'X': grad_var},
outputs={'Out': cast_grad_var},
attrs={
'in_dtype': grad_var.dtype,
'out_dtype': cast_grad_var.dtype,
self._op_role_key: self._op_role.Backward,
},
)
offset += 1
grad_var = cast_grad_var
block._insert_op(
index=first_opt_op_idx + offset,
type='sum',
inputs={'X': [merged_param_grad_var, grad_var]},
outputs={'Out': merged_param_grad_var},
attrs={
self._op_role_key: self._op_role.Backward,
},
)
offset += 1
merged_gradient_names.append(merged_param_grad_name)
if not fp16_allreduce:
return merged_gradient_names
first_opt_op_idx = None
for index, op in reversed(tuple(enumerate(list(block.ops)))):
if self._is_backward_op(op) and first_opt_op_idx is None:
first_opt_op_idx = index + 1
break
assert first_opt_op_idx is not None
# insert cast op from fp16->fp32
# FIXME(wangxi): maybe put in sharding is better, for some grad
# is not in sharding device.
for fp16_grad_name in merged_gradient_names:
grad_name = fp16_grad_name.replace('@FP16', '')
param_name = fp16_grad_name.replace('@GRAD@MERGED@FP16', '')
if not block.has_var(grad_name):
self._create_var(block, block.vars[param_name], grad_name)
assert block.has_var(grad_name)
fp16_grad_var = block.var(fp16_grad_name)
grad_var = block.var(grad_name)
grad_var.persistable = False
block._insert_op(
index=first_opt_op_idx,
type='cast',
inputs={'X': fp16_grad_var},
outputs={'Out': grad_var},
attrs={
'in_dtype': fp16_grad_var.dtype,
'out_dtype': grad_var.dtype,
self._op_role_key: self._op_role.Optimize,
},
)
return merged_gradient_names
def _insert_accumulate_gradients_with_fuse(
self, main_block, fp16, fused_size, grad_param_pairs, first_opt_op_idx
):
grad_param_pairs = self._sort_grad_param_by_dtype(
main_block, grad_param_pairs
)
grad_param_segments = []
merged_suffix = '@MERGED@FP16' if fp16 else '@MERGED'
dtype = paddle.float16 if fp16 else paddle.float32
cur_size = 0.0
last_dtype = None
# split the grad based on dtype and fused size
for grad, param in grad_param_pairs:
real_grad = main_block.var(grad)
# create the gradient merged var for each grad
merged_grad_var = main_block.create_var(
name=param + core.grad_var_suffix() + merged_suffix,
dtype=dtype,
shape=real_grad.shape,
persistable=True,
stop_gradient=False,
)
real_param = main_block.var(param)
if hasattr(real_param, 'is_distributed'):
merged_grad_var.is_distributed = real_param.is_distributed
tmp_size = self._get_var_size(real_grad)
# two strategies for splitting the grad
# 1. the current segment's size reach the user defined grad_size_in_MB
# 2. the upcoming grad holds different dtype compared with grads in current segment
if (
len(grad_param_segments) == 0
or cur_size + tmp_size > fused_size
or real_grad.dtype != last_dtype
):
grad_param_segments.append(
([real_grad], [real_param], [merged_grad_var])
)
last_dtype = real_grad.dtype
cur_size = 0.0
else:
grad_param_segments[-1][0].append(real_grad)
grad_param_segments[-1][1].append(real_param)
grad_param_segments[-1][2].append(merged_grad_var)
cur_size += tmp_size
fused_gradients = []
fused_merged_gradients = []
# create fused vars for grad and param
for grad_param_segment in grad_param_segments:
grad_segment = grad_param_segment[0]
merged_grad_segment = grad_param_segment[2]
fused_grad = main_block.create_var(
name=f'FusedGrad_{grad_segment[0].name}',
dtype=grad_segment[0].dtype,
persistable=False,
stop_gradient=False,
)
# keep the '.cast_fp16' info in the fuse var name
fused_merged_grad_name_prefix = (
'FusedMergedGrad.cast_fp16.'
if merged_grad_segment[0].dtype == paddle.float16
else 'FusedMergedGrad'
)
fused_merged_grad_name = (
fused_merged_grad_name_prefix
+ f'_{merged_grad_segment[0].name}'
)
fused_merged_grad = main_block.create_var(
name=fused_merged_grad_name,
dtype=merged_grad_segment[0].dtype,
persistable=True,
stop_gradient=False,
)
fused_gradients.append(fused_grad)
fused_merged_gradients.append(fused_merged_grad)
assert len(fused_gradients) == len(grad_param_segments)
assert len(fused_merged_gradients) == len(grad_param_segments)
# insert coalesce op at the start of the backward pass
# use param as the coalesce input to make sure the two Fused vars are in same shape
first_back_op_idx = None
for index, op in enumerate(main_block.ops):
if self._is_backward_op(op) and first_back_op_idx is None:
first_back_op_idx = index
break
assert first_back_op_idx is not None
offset = 0
for i in range(len(grad_param_segments)):
fused_grad = fused_gradients[i]
fused_merged_grad = fused_merged_gradients[i]
grads = grad_param_segments[i][0]
params = grad_param_segments[i][1]
merged_grads = grad_param_segments[i][2]
main_block._insert_op_without_sync(
first_back_op_idx + offset,
type="coalesce_tensor",
inputs={"Input": params},
outputs={"Output": grads, "FusedOutput": fused_grad},
attrs={
# Explanation of user_defined_size_of_dtype:
# In coalesce op, the align size is 256 bytes
# the float takes 4 bytes while fp16 takes 2 bytes.
# To meet the requirement, 128 fp16 or 64 float will be aligned
# Think the total shape of the input tensors if [64],
# if the dtype is float, then the shape of the fuse var is [64]
# however if the dtype if fp16, the shape of the fuse var is [128],
# which will cause the fused vars' shape vary between each other.
# To make sure the shape of the fused vars are identical,
# we set the dtype of float and fp16 both to 2.
# Under this way, the fused vars' shape for float and fp16 are all [128]
"user_defined_size_of_dtype": 2,
"copy_data": False,
"use_align": True,
"dtype": grads[0].dtype,
self._op_role_key: self._op_role.Backward,
# On npu, the nan/inf check login is different with gpu.
# If there are some not initialized sections in the fused var,
# and the value in those sections are nan/inf, it will trigger the nan/inf check.
# To avoid these problematic triggers, set constant is needed for npu
"set_constant": core.is_compiled_with_custom_device('npu'),
"constant": 0.0,
},
)
offset += 1
# For the gradient_merged_fused_var, given a init value during the coalesce op
# this will remove a problematic fill_constant op. This op role of this coalesce
# is set to be LRSched to make this coalesce (with init) only run once
main_block._insert_op_without_sync(
first_back_op_idx + offset,
type="coalesce_tensor",
inputs={"Input": params},
outputs={
"Output": merged_grads,
"FusedOutput": fused_merged_grad,
},
attrs={
"user_defined_size_of_dtype": 2,
"set_constant": True,
"constant": 0.0,
"copy_data": False,
"use_align": True,
"dtype": merged_grads[0].dtype,
self._op_role_key: self._op_role.Optimize.LRSched,
},
)
offset += 1
# insert gradient merge relating ops
first_opt_op_idx += offset
offset = 0
for i in range(len(fused_gradients)):
fused_grad = fused_gradients[i]
fused_merged_grad = fused_merged_gradients[i]
is_fp16_grad = 'cast_fp16' in fused_grad.name
need_cast = is_fp16_grad is not fp16
if need_cast:
# for fp16 allreduce, cast fp32 grad to fp16
# for fp32 allreduce, cast fp16 grad to fp32
cast_grad_var_name = fused_grad.name + '@TMP'
cast_grad_var = main_block.create_var(
name=cast_grad_var_name,
dtype=dtype,
persistable=False,
stop_gradient=False,
)
main_block._insert_op(
index=first_opt_op_idx + offset,
type='cast',
inputs={'X': fused_grad},
outputs={'Out': cast_grad_var},
attrs={
'in_dtype': fused_grad.dtype,
'out_dtype': cast_grad_var.dtype,
self._op_role_key: self._op_role.Backward,
},
)
offset += 1
fused_grad = cast_grad_var
main_block._insert_op(
index=first_opt_op_idx + offset,
type='sum',
inputs={'X': [fused_merged_grad, fused_grad]},
outputs={'Out': fused_merged_grad},
attrs={self._op_role_key: self._op_role.Backward},
)
offset += 1
if fp16:
# if using fp16 allreduce, the optimizer needs fp32 grads, cast them back to fp32
for grad, param in grad_param_pairs:
real_grad = main_block.var(grad)
fp16_grad_name = param + core.grad_var_suffix() + '@MERGED@FP16'
assert main_block.has_var(fp16_grad_name)
fp16_grad = main_block.var(fp16_grad_name)
fp32_grad_name = param + core.grad_var_suffix() + '@MERGED'
fp32_grad = main_block.create_var(
name=fp32_grad_name,
dtype=paddle.float32,
shape=real_grad.shape,
persistable=False,
stop_gradient=False,
)
main_block._insert_op(
index=first_opt_op_idx + offset,
type='cast',
inputs={'X': fp16_grad},
outputs={'Out': fp32_grad},
attrs={
'in_dtype': paddle.float16,
'out_dtype': paddle.float32,
self._op_role_key: self._op_role.Optimize,
},
)
offset += 1
# replace the var with it's name, which will be used for inserting allreduce
for i in range(len(fused_merged_gradients)):
fused_merged_gradients[i] = fused_merged_gradients[i].name
return fused_merged_gradients, first_opt_op_idx
def _accumulate_gradients_with_fuse(
self, main_block, fp16, fused_size, shard=None
):
first_opt_op_idx = None
grad_param_pairs = []
# obtain all param/grad pairs that needed to be fused
for index, op in reversed(tuple(enumerate(list(main_block.ops)))):
# remove the cast op of fp16 grad to fp32 grad
if self._is_optimize_op(op) and op.type == 'cast':
in_name = op.input_arg_names[0]
out_name = op.output_arg_names[0]
if out_name.strip('@GRAD') in self._param_device_map:
assert in_name.replace('.cast_fp16', '') == out_name
main_block._remove_op(index)
continue
if self._is_backward_op(op) and first_opt_op_idx is None:
first_opt_op_idx = index + 1
# no optimize phase
if first_opt_op_idx == len(main_block.ops):
return
if self._is_backward_op(op) and (
self._op_role_var_key in op.attr_names
):
op_role_var = op.attr(self._op_role_var_key)
if len(op_role_var) == 0:
continue
assert len(op_role_var) % 2 == 0
for i in range(0, len(op_role_var), 2):
param_name = op_role_var[i]
if not main_block.has_var(param_name):
continue
if '@BroadCast' in param_name:
continue
grad_param_pairs.append(
(op_role_var[i + 1], op_role_var[i])
)
if len(grad_param_pairs) == 0:
return
nranks = shard.worker_num if shard else 1
device_to_pairs = [[] for _ in range(nranks)]
for pair in grad_param_pairs:
root_id = shard.device(pair[1]) if shard else 0
assert 0 <= root_id < nranks
device_to_pairs[root_id].append(pair)
all_fused_merged_gradients = []
for pairs in device_to_pairs:
(
fused_merged_gradients,
first_opt_op_idx,
) = self._insert_accumulate_gradients_with_fuse(
main_block, fp16, fused_size, pairs, first_opt_op_idx
)
all_fused_merged_gradients += fused_merged_gradients
main_block._sync_with_cpp()
return all_fused_merged_gradients
def _sort_grad_param_by_dtype(self, main_block, grad_param_pairs):
# sort the grad param paris by the dtype
fp16_pairs = []
fp32_pairs = []
other_pairs = []
for pairs in grad_param_pairs:
dtype = main_block.var(pairs[0]).dtype
if dtype == paddle.float32:
fp32_pairs.append(pairs)
elif dtype == paddle.float16:
fp16_pairs.append(pairs)
else:
other_pairs.append(pairs)
sorted_pairs = fp16_pairs
sorted_pairs.extend(fp32_pairs)
sorted_pairs.extend(other_pairs)
return sorted_pairs
def _get_var_size(self, var):
dtype_to_size = {
core.VarDesc.VarType.FP16: 2,
core.VarDesc.VarType.BF16: 2,
core.VarDesc.VarType.FP32: 4,
core.VarDesc.VarType.FP64: 8,
core.VarDesc.VarType.INT16: 2,
core.VarDesc.VarType.INT32: 4,
core.VarDesc.VarType.INT64: 8,
core.VarDesc.VarType.BOOL: 1,
core.VarDesc.VarType.UINT8: 1,
}
assert -1 not in var.shape
return (
reduce(lambda x, y: x * y, var.shape, 1)
* dtype_to_size[var.dtype]
/ 1024.0
/ 1024.0
)
def _add_sub_blocks(self, main_block, program_list):
main_program = main_block.program
for prog in program_list:
for op in prog.block(0).ops:
if not op.has_attr('sub_block'):
continue
origin_sub_block_id = op.attr('sub_block').id
origin_sub_block = main_program.block(origin_sub_block_id)
new_sub_block = prog._create_block(parent_idx=0)
for sub_op in origin_sub_block.ops:
op_desc = sub_op.desc
ap_op = new_sub_block.desc.append_op()
ap_op.copy_from(op_desc)
new_sub_block._sync_with_cpp()
self._create_vars(new_sub_block, origin_sub_block)
op._set_attr('sub_block', new_sub_block)
def _get_device_info(self, block):
for op in block.ops:
if not op._has_kernel(op.type):
continue
op_device = op.attr(self._op_device_key)
return op_device
def _process_persistable_vars_in_multi_sections(
self, main_program, startup_prog, program_list
):
"""
Special Case: process persistable vars that exist in
multiple sections, e.g., shared weight
"""
# var_info = {var_name: [program1, program2...]},
# persistable var only
var_info = {}
for prog in program_list:
block = prog.block(0)
for var_name in block.vars:
if var_name == "double_buffer_0":
continue
var = block.var(var_name)
if not var.persistable:
continue
if var_name not in var_info:
var_info[var_name] = []
if prog not in var_info[var_name]:
var_info[var_name].append(prog)
for var_name in list(var_info.keys()):
if len(var_info[var_name]) == 1:
var_info.pop(var_name)
# write_info = {var_name: program}, where program is the only program
# in which the var named var_name is written.
write_info = {}
for var_name in var_info.keys():
for prog in var_info[var_name]:
block = prog.block(0)
for op in block.ops:
if (
op.type == "recv_v2"
or op.type == "create_py_reader"
or op.type == "read"
or op.type == "update_loss_scaling"
):
continue
# We have processed lr related vars
if op.attr(self._op_role_key) == int(
self._op_role.Optimize.LRSched
):
continue
if var_name in op.desc.output_arg_names():
assert var_name not in write_info, (
f"two sections write the same var({var_name}): second "
f"op {op}."
)
write_info[var_name] = prog
break
for var_name in var_info.keys():
# Case 1: read only variables, no special process
if var_name not in write_info:
continue
# Case 2: one write multiple reads
write_prog = write_info[var_name]
write_block = write_prog.block(0)
write_device = self._get_device_info(write_block)
write_dev_index = int(write_device.split(':')[1])
all_progs = var_info[var_name]
for prog in all_progs:
if prog == write_prog:
continue
read_block = prog.block(0)
read_device = self._get_device_info(read_block)
read_dev_index = int(read_device.split(':')[1])
pair = (write_dev_index, read_dev_index)
pair_key = write_dev_index * 1000 + read_dev_index
if pair not in self._pipeline_pair:
self._pipeline_pair.append(pair)
self._pp_ring_map[pair_key] = self.ring_id
ring_id = self.ring_id
self.ring_id += 1
else:
ring_id = self._pp_ring_map[pair_key]
write_block._insert_op(
index=0,
type='send_v2',
inputs={
'X': write_block.var(var_name),
},
attrs={
self._op_device_key: write_device,
'use_calc_stream': False,
# A trick to make the role LRSched to avoid copy every
# microbatch
self._op_role_key: self._op_role.LRSched,
'peer': read_dev_index,
'ring_id': ring_id,
},
)
read_block._insert_op(
index=0,
type='recv_v2',
outputs={'Out': [read_block.var(var_name)]},
attrs={
'out_shape': read_block.var(var_name).shape,
'dtype': read_block.var(var_name).dtype,
self._op_device_key: read_device,
'use_calc_stream': False,
# A trick to make the role LRSched to avoid copy every
# microbatch
self._op_role_key: self._op_role.LRSched,
'peer': write_dev_index,
'ring_id': ring_id,
},
)
read_block._insert_op(
index=1,
type='c_sync_comm_stream',
inputs={'X': [read_block.var(var_name)]},
outputs={'Out': [read_block.var(var_name)]},
attrs={
self._op_device_key: read_device,
# A trick to make the role LRSched to avoid copy every
# microbatch
self._op_role_key: self._op_role.LRSched,
'ring_id': ring_id,
},
)
def _is_gradient_clip_op(self, op):
return op.desc.has_attr("op_namescope") and op.desc.attr(
"op_namescope"
).startswith("/gradient_clip")
def _is_regularization_op(self, op):
return op.desc.has_attr("op_namescope") and op.desc.attr(
"op_namescope"
).startswith("/regularization")
def _is_weight_decay_op(self, op):
# in AdamW namescope is /optimizer_*/weight decay/
return op.desc.has_attr(
"op_namescope"
) and 'weight decay' in op.desc.attr("op_namescope")
def _get_input_output_info(self, block):
'''
Get info of op input and output.
'''
# A map from output var to op which generate it.
output_var_to_op = defaultdict(list)
# A map from var to op which takes it as input.
input_var_to_op = defaultdict(list)
for index, op in enumerate(block.ops):
for var_name in op.input_arg_names:
input_var_to_op[var_name].append([op, index])
for var_name in op.output_arg_names:
output_var_to_op[var_name].append([op, index])
return output_var_to_op, input_var_to_op
def _optimize_forward_send_sync(self, program):
"""
optimize forward send's sync_comm_stream schedule
"""
if self.schedule_mode != '1F1B':
return
block = program.block(0)
recv_type = 'recv_v2' if self.mp_degree == 1 else 'partial_recv'
backward_recv_index = None
for index, op in enumerate(block.ops):
if op.type == recv_type and self._is_backward_op(op):
backward_recv_index = index
break
# last pipeline stage
if backward_recv_index is None:
return
offset = 0
for index, op in enumerate(list(block.ops)):
if index >= backward_recv_index:
break
if op.type == 'c_sync_comm_stream' and op.has_attr('pipeline_flag'):
var_name = op.input_arg_names[0]
var = block.var(var_name)
block._remove_op(index + offset, sync=False)
offset -= 1
# NOTE:
# 1. When the backward recv is completed, it indicates
# that the forward send is completed too. So we only need
# to use the NOP op to prevent memory release.
# 2. Because we removed sync_comm_op,
# we will insert NOP after recv_op.
block._insert_op_without_sync(
index=backward_recv_index,
type='nop',
inputs={'X': [var]},
outputs={'Out': [var]},
attrs={self._op_role_key: self._op_role.Backward},
)
block._sync_with_cpp()
def _mv_head_recv(self, program):
"""
A pass to move the recv op to the beginning of
the forward/backward phase
"""
forward_insert_index = 0
backward_insert_index = None
block = program.global_block()
num_ops = len(program.global_block().ops)
for i in range(num_ops):
insert_index = None
op = program.global_block().ops[i]
op_role = int(op.attr(self._op_role_key))
if (
op_role == int(self._op_role.Backward)
and backward_insert_index is None
):
backward_insert_index = i
if (
op.type != "partial_recv"
and op.type != "partial_allgather"
and op.type != "nop"
and op.type != "recv_v2"
):
continue
if op_role == int(self._op_role.Forward):
if i == forward_insert_index:
forward_insert_index += 1
continue
insert_index = forward_insert_index
elif op_role == int(self._op_role.Backward):
if i == backward_insert_index:
backward_insert_index += 1
continue
insert_index = backward_insert_index
else:
raise ValueError(f"Unknown op_role: {op_role}")
op_inputs = {}
for name in op.input_names:
op_inputs[name] = op.input(name)
op_outputs = {}
for name in op.output_names:
op_outputs[name] = op.output(name)
block._insert_op_without_sync(
index=insert_index,
type=op.type,
inputs=op_inputs,
outputs=op_outputs,
attrs=op.all_attrs(),
)
block._remove_op(i + 1)
if op_role == int(self._op_role.Forward):
forward_insert_index += 1
elif op_role == int(self._op_role.Backward):
backward_insert_index += 1
block._sync_with_cpp()
def _check_pipeline_persist_var(self, program):
"""
Pipeline may need multiple forward before
"""
block = program.global_block()
persist_output = set()
used_in_backward = set()
for op in block.ops:
if self._is_forward_op(op):
for var_name in op.output_arg_names:
var = block.vars[var_name]
if var.persistable:
persist_output.add(var_name)
elif self._is_backward_op(op):
for var_name in op.input_arg_names:
if var_name in persist_output:
used_in_backward.add(var_name)
if len(used_in_backward) == 0:
return
warnings.warn(
"The pipeline requires multiple forward calculations before backward, "
"so when the persistable var is changed in the forward, it may cause "
"errors in the backward calculation who using this persistable var. "
"However, some backward op don't need this var(NoNeedBufferVars), "
"there will be no error at this time.\n"
"So please check these persistable vars which changed in "
f"forward and used in backward:\n{used_in_backward}"
)
def minimize(
self, loss, startup_program=None, parameter_list=None, no_grad_set=None
):
main_block = loss.block
self.origin_main_block = main_block
main_program = main_block.program
if startup_program is None:
startup_program = default_startup_program()
pipeline_opt = main_program._pipeline_opt
assert pipeline_opt, 'Please use pipeline with fleet.'
required_keys = [
'local_rank',
'schedule_mode',
'micro_batch_size',
'ring_id',
'global_ring_id',
'use_sharding',
'mp_degree',
'mp_rank',
]
for key in required_keys:
assert key in pipeline_opt, (
f'Please use pipeline with fleet to use {key}.'
)
self.local_rank = pipeline_opt['local_rank']
self.schedule_mode = pipeline_opt['schedule_mode']
self.micro_batch_size = pipeline_opt['micro_batch_size']
self.use_sharding = pipeline_opt['use_sharding']
self.ring_id = pipeline_opt['ring_id']
self.global_ring_id = pipeline_opt['global_ring_id']
self.mp_degree = pipeline_opt['mp_degree']
self.mp_rank = pipeline_opt['mp_rank']
self.scale_gradient = pipeline_opt.get('scale_gradient', False)
assert self.mp_degree >= 1
assert 0 <= self.mp_rank < self.mp_degree
optimize_ops, params_grads = self._optimizer.minimize(
loss, startup_program, parameter_list, no_grad_set
)
self._param_device_map = self._origin_optimizer._param_device_map
(
self.output_var_to_op,
self.input_var_to_op,
) = self._get_input_output_info(main_block)
# Step1: add default op_device attribute for ops.
self._add_op_device_attr(main_block)
device_list = self._check_validation(main_block)
def device_cmp(device1, device2):
dev1_id = int(device1.split(':')[1])
dev2_id = int(device2.split(':')[1])
if dev1_id < dev2_id:
return -1
elif dev1_id > dev2_id:
return 1
else:
return 0
sorted_device_list = sorted(device_list, key=cmp_to_key(device_cmp))
assert sorted_device_list == device_list, (
"With pipeline parallelism, you must use gpu devices one after "
"another in the order of their ids."
)
# Step2: add send and recv ops between section boundaries
self._insert_sendrecv_ops_for_boundaries(main_block)
# Step3: split program into sections and add pairs of
# send and recv ops for data var.
main_program = main_block.program
program_list = self._split_program(main_program, device_list)
for p in program_list:
self._create_vars(p.global_block(), main_block)
if os.getenv("PADDLE_MANUAL_PIPELINE_STAGE", None):
self.local_rank = int(os.getenv("PADDLE_MANUAL_PIPELINE_STAGE"))
assert self.local_rank < len(device_list), (
"Manually specified "
"pipeline stage must be less than total number of pipeline "
"stages."
)
else:
self.local_rank %= len(device_list)
# Step3.5: optimize forward send sync_comm to overlap send and recv
self._optimize_forward_send_sync(program_list[self.local_rank])
# Step4: Special Case: process persistable vars that exist in
# multiple sections
# FIXME
# self._process_persistable_vars_in_multi_sections(
# main_program, startup_program, program_list)
# Step5: Add sub blocks for section programs
self._add_sub_blocks(main_block, program_list)
place_list = []
for dev in device_list:
dev_index = int(dev.split(":")[1])
if core.is_compiled_with_cuda():
place_list.append(core.CUDAPlace(dev_index % 1))
# Step6: Split startup program
new_startup_program = self._split_startup_program(
startup_program, self.local_rank
)
startup_program._pipeline_opt = {
"startup_program": new_startup_program,
}
real_block = program_list[self.local_rank].global_block()
if not self.scale_gradient:
self._insert_loss_scale(real_block)
if not self.use_sharding:
# Step7: clear gradients before each mini-batch and
# accumulate gradients during backward
self._rename_gradient_var_name(real_block)
real_block._sync_with_cpp()
self._accumulate_gradients(real_block)
real_block._sync_with_cpp()
if core.is_compiled_with_cuda():
place_id = int(os.getenv("FLAGS_selected_gpus", "0"))
# A pass to move the recv op to the beginning of
# the forward/backward phase
self._mv_head_recv(program_list[self.local_rank])
# A pass to check pipeline persist var which changed in
# forward and used in backward
self._check_pipeline_persist_var(program_list[self.local_rank])
main_program._pipeline_opt = {
"trainer": "PipelineTrainer",
"device_worker": "Section",
"pipeline_stage": self.local_rank,
"num_pipeline_stages": len(device_list),
"schedule_mode": self.schedule_mode,
"inner_parallelism": len(device_list),
"section_program": program_list[self.local_rank],
"place": place_list[self.local_rank],
"place_id": place_id,
"sync_steps": -1,
"num_microbatches": self._num_microbatches,
"start_cpu_core_id": self._start_cpu_core_id,
}
return (
optimize_ops,
params_grads,
program_list,
self._pipeline_pair,
self._pp_ring_map,
)