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

1451 lines
52 KiB
Python

# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import logging
from collections import OrderedDict
from enum import Enum
from functools import reduce
import paddle
from paddle.base import core
from paddle.base.framework import Parameter, Program
from paddle.distributed.auto_parallel.static.dist_attribute import (
OperatorDistAttr,
)
from paddle.distributed.auto_parallel.static.utils import (
get_logger,
is_backward_op,
is_optimize_op,
naive_set_dist_op_attr_for_program_by_mesh_and_mapping,
)
from paddle.framework import (
_current_expected_place_ as _get_device,
)
from ..auto_parallel.static.utils import OpRole
__not_shape_var_type__ = [
core.VarDesc.VarType.READER,
core.VarDesc.VarType.STEP_SCOPES,
core.VarDesc.VarType.DENSE_TENSOR_ARRAY,
core.VarDesc.VarType.FEED_MINIBATCH,
core.VarDesc.VarType.FETCH_LIST,
]
logger = get_logger(logging.INFO)
# NOTE: Here stream is just a presentation with different name,
# it is up to executor to create the exact streams given the name.
class AutoParallelStreamType(Enum):
CALC_STREAM = "default"
MP_STREAM = "auto_parallel_mp"
SHARDING_STREAM = "auto_parallel_sharding"
def list_to_ordered_dict(list_obj, ordered_dict=None):
if ordered_dict is None:
ordered_dict = OrderedDict()
else:
assert isinstance(ordered_dict, OrderedDict)
for obj in list_obj:
if obj not in ordered_dict:
ordered_dict[obj] = True
return ordered_dict
# The inputs of a program are the variables
# that first occur as the input of the op.
def get_inputs_of_program(program):
visited_vars = set()
input_vars = []
for op in program.global_block().ops:
for in_var_name in op.input_arg_names:
if in_var_name not in visited_vars:
input_vars.append(in_var_name)
visited_vars.add(in_var_name)
for out_var_name in op.output_arg_names:
visited_vars.add(out_var_name)
return input_vars
def get_outputs_of_program(program):
output_vars = OrderedDict()
for op in program.global_block().ops:
list_to_ordered_dict(op.output_arg_names, output_vars)
return list(output_vars.keys())
def prune_program(program, start_op_idx, end_op_idx):
op_num = len(program.global_block().ops)
if start_op_idx < 0:
start_op_idx += op_num
assert start_op_idx >= 0 and start_op_idx < op_num
if end_op_idx < 0:
end_op_idx += op_num
assert end_op_idx >= 0 and end_op_idx <= op_num, end_op_idx
assert start_op_idx < end_op_idx
program = program.clone()
for idx in range(op_num - 1, end_op_idx - 1, -1):
program.global_block()._remove_op(idx, sync=False)
for idx in range(start_op_idx - 1, -1, -1):
program.global_block()._remove_op(idx, sync=False)
program._sync_with_cpp()
valid_vars = set()
for op in program.global_block().ops:
for in_var_name in op.input_arg_names:
valid_vars.add(in_var_name)
for out_var_name in op.output_arg_names:
valid_vars.add(out_var_name)
vars_to_remove = []
for var in program.global_block().vars:
if var not in valid_vars:
vars_to_remove.append(var)
for var in vars_to_remove:
program.global_block()._remove_var(var, sync=False)
program._sync_with_cpp()
return program
def split_program(program, op_indices):
"""
Split the program by op_indices.
For examples, a program has 100 ops, and op_indices = [25, 60].
Then the program is split into 3 parts, containing 25, 35 and 40
ops respectively.
The return values are a tuple with 3 elements: the split program
list, the input var names of each split program, and the output
var names of each split program.
"""
assert op_indices, "op_indices cannot be empty"
op_num = len(program.global_block().ops)
assert op_num > 0, "program cannot be empty"
op_indices = [idx if idx >= 0 else idx + op_num for idx in op_indices]
if op_indices[0] != 0:
op_indices = [0, *op_indices]
if op_indices[-1] != op_num:
op_indices.append(op_num)
for idx in range(len(op_indices) - 1):
assert op_indices[idx] < op_indices[idx + 1], (
"op_indices must be strictly sorted"
)
split_programs = []
for idx in range(len(op_indices) - 1):
new_split = prune_program(program, op_indices[idx], op_indices[idx + 1])
split_programs.append(new_split)
num_split = len(split_programs)
input_vars = [get_inputs_of_program(p) for p in split_programs]
output_vars = [
list_to_ordered_dict(get_outputs_of_program(p)) for p in split_programs
]
valid_output_vars = [OrderedDict() for _ in range(num_split)]
valid_output_vars[-1] = output_vars[-1]
for i in range(1, num_split):
for in_var_name in input_vars[i]:
for j in reversed(range(i)):
if in_var_name in output_vars[j]:
valid_output_vars[j][in_var_name] = True
break
valid_output_vars = [list(item.keys()) for item in valid_output_vars]
return split_programs, input_vars, valid_output_vars
class OpInOutInfo:
"""
Record unused buffer input_vars of op and other var_names except unused buffer input_vars
"""
def __init__(self):
self._is_build = False
self._no_need_buffer_slots = set()
self._other_arg_names_set = set()
@property
def is_build(self):
return self._is_build
def _get_op_attrs(self, op):
inputs = {}
for input_name in op.input_names:
inputs[input_name] = op.input(input_name)
outputs = {}
for output_name in op.output_names:
outputs[output_name] = op.output(output_name)
attrs = {}
for attr_name in op.attr_names:
attrs[attr_name] = op.attr(attr_name)
return inputs, outputs, attrs
def build_info(self, op):
inputs, outputs, attrs = self._get_op_attrs(op)
self._no_need_buffer_slots = core.infer_no_need_buffer_slots(
op.type, inputs, outputs, attrs
)
if len(self._no_need_buffer_slots) == 0:
return
for slot_name in op.input_names:
if slot_name not in self._no_need_buffer_slots:
for in_name in op.input(slot_name):
self._other_arg_names_set.add(in_name)
for slot_name in op.output_names:
if slot_name not in self._no_need_buffer_slots:
for out_name in op.output(slot_name):
self._other_arg_names_set.add(out_name)
self._is_build = True
def is_needed(self, arg_name):
return (
len(self._no_need_buffer_slots) == 0
or arg_name in self._other_arg_names_set
)
def var_can_be_deleted(var_name, block):
var = block._find_var_recursive(var_name)
return var is not None and not var.persistable
def _get_required_vars_of_program(program):
"""
Get all vars in the program that are non-persistable and not in op's no_need_buffer.
"""
required_vars = set()
for block in program.blocks:
for op in block.ops:
if op.type in [
"c_sync_comm_stream",
"conditional_block",
"data",
"nop",
"while",
]:
continue
op_info = OpInOutInfo()
op_info.build_info(op)
for arg_name in op.input_arg_names + op.output_arg_names:
if var_can_be_deleted(arg_name, block) and op_info.is_needed(
arg_name
):
required_vars.add(arg_name)
return required_vars
def set_skip_gc_vars(num_micro_batches, job_types, sub_programs, jobs):
"""
Set `skip_gc_vars` for every job in jobs.
A whole_program is split up into sub_programs according to the schedule mode,
thus a sub_program's vars might be used as the op's input of the later sub_program,
and these vars cannot be gc after executing current sub_program.
"""
if paddle.base.framework.get_flags("FLAGS_enable_pir_api")[
"FLAGS_enable_pir_api"
]:
return _set_skip_gc_vars_in_pir(
num_micro_batches, job_types, sub_programs, jobs
)
else:
return _set_skip_gc_vars_in_old_ir(
num_micro_batches, job_types, sub_programs, jobs
)
def _set_skip_gc_vars_in_old_ir(
num_micro_batches, job_types, sub_programs, jobs
):
assert num_micro_batches >= 1, "num_micro_batches needs to be >= 1"
type_to_program = dict(zip(job_types, sub_programs))
# step1: Get all vars of every sub_program that are non-persistable and not in op's no_need_buffer.
type_to_required_vars = {}
for type, program in type_to_program.items():
type_to_required_vars[type] = _get_required_vars_of_program(program)
# step2: Set `skip_gc_vars` for each job
suffixed_required_vars = [set() for i in range(num_micro_batches)]
num_jobs = len(jobs)
for job_id in reversed(range(num_jobs)):
job = jobs[job_id]
job_type = job.type()
required_vars = type_to_required_vars[job_type]
micro_batch_id = job.micro_batch_id()
skip_gc_vars = required_vars & suffixed_required_vars[micro_batch_id]
logger.debug(
f"Skip gc vars for {job_type}-({micro_batch_id}): {skip_gc_vars}"
)
if job_type in ["backward", "backward_w"]:
assert len(skip_gc_vars) == 0, (
f"When enabling pipeline parallelism strategy, the skip_gc_vars for {job_type} subprogram must be empty, but it is {skip_gc_vars}."
)
job.set_skip_gc_vars(skip_gc_vars)
suffixed_required_vars[micro_batch_id] |= required_vars
return type_to_program
def _set_skip_gc_vars_in_pir(num_micro_batches, job_types, sub_programs, jobs):
assert num_micro_batches >= 1, "num_micro_batches needs to be >= 1"
type_to_program = dict(zip(job_types, sub_programs))
# step1: Get all required vars of every sub_program that are non-persistable and not in op's no_need_buffer.
type_to_required_vars = {}
no_need_buffer_vars = core.get_no_need_buffer_values(type_to_program)
for job_type, program in type_to_program.items():
required_vars = set()
persistable_vars = set()
for key in program.global_block().kwargs():
required_vars.add(key)
for op in program.global_block().ops:
for var in op.operands_source():
if var.has_name:
required_vars.add(var.name)
if var.persistable:
persistable_vars.add(var.name)
for var in op.results():
if var.has_name:
required_vars.add(var.name)
if var.persistable:
persistable_vars.add(var.name)
if job_type in no_need_buffer_vars:
required_vars -= no_need_buffer_vars[job_type]
required_vars -= persistable_vars
type_to_required_vars[job_type] = required_vars
# step2: Set `skip_gc_vars` for each job
suffixed_required_vars = [set() for i in range(num_micro_batches)]
num_jobs = len(jobs)
for job_id in reversed(range(num_jobs)):
job = jobs[job_id]
job_type = job.type()
required_vars = type_to_required_vars[job_type]
micro_batch_id = job.micro_batch_id()
skip_gc_vars = required_vars & suffixed_required_vars[micro_batch_id]
logger.debug(
f"Skip gc vars for {job_type}-({micro_batch_id}): {skip_gc_vars}"
)
if job_type in ["send_backward", "backward_w"]:
assert len(skip_gc_vars) == 0, (
f"When enabling pipeline parallelism strategy, the skip_gc_vars for {job_type} subprogram must be empty, but it is {skip_gc_vars}."
)
job.set_skip_gc_vars(skip_gc_vars)
suffixed_required_vars[micro_batch_id] |= required_vars
return type_to_program
def _create_param(dst_block, src_var):
copied_kwargs = {}
copied_kwargs['trainable'] = src_var.trainable
copied_kwargs['optimize_attr'] = src_var.optimize_attr
copied_kwargs['regularizer'] = src_var.regularizer
copied_kwargs['do_model_average'] = src_var.do_model_average
copied_kwargs['need_clip'] = src_var.need_clip
Parameter(
block=dst_block,
type=src_var.type,
name=src_var.name,
shape=src_var.shape,
dtype=src_var.dtype,
lod_level=src_var.lod_level,
error_clip=src_var.error_clip,
stop_gradient=src_var.stop_gradient,
is_data=src_var.is_data,
belong_to_optimizer=src_var.belong_to_optimizer,
**copied_kwargs,
)
def _create_inter(dst_block, src_var):
dst_block.create_var(
type=src_var.type,
name=src_var.name,
shape=src_var.shape,
dtype=src_var.dtype,
lod_level=src_var.lod_level,
persistable=src_var.persistable,
error_clip=src_var.error_clip,
stop_gradient=src_var.stop_gradient,
is_data=src_var.is_data,
belong_to_optimizer=src_var.belong_to_optimizer,
)
def _create_var(src_block, dst_block, src_varname, force_create=False):
if not force_create:
src_var = src_block.var(src_varname)
else:
src_var = src_block._var_recursive(src_varname)
if src_var.type in __not_shape_var_type__:
persist = getattr(src_var, 'persistable', False)
dst_block.create_var(
type=src_var.type,
name=src_var.name,
persistable=persist,
error_clip=src_var.error_clip,
stop_gradient=src_var.stop_gradient,
is_data=src_var.is_data,
belong_to_optimizer=src_var.belong_to_optimizer,
)
else:
if isinstance(src_var, Parameter):
_create_param(dst_block, src_var)
else:
_create_inter(dst_block, src_var)
def _create_program(src_block, dst_block, src_op, force_create=False):
dst_op_desc = dst_block.desc.append_op()
dst_op_desc.copy_from(src_op.desc)
for input_varname in src_op.input_arg_names:
if src_block.has_var(input_varname) or (
force_create and src_block._find_var_recursive(input_varname)
):
_create_var(src_block, dst_block, input_varname, force_create)
for output_varname in src_op.output_arg_names:
if src_block.has_var(output_varname) or (
force_create and src_block._find_var_recursive(output_varname)
):
_create_var(src_block, dst_block, output_varname, force_create)
def _pir_overlap_send_recv(program):
"""
This function is used to replace the function '_insert_sync_for_fthenb_1f1b'.
The finally target of this function is as follows:
1. no need to insert the 'c_sync_calc' and 'c_sync_calc' operators
2. 'send_v2' operator uses 'dist_attr.execution_stream' to set stream of its own.
3. 'recv_v2' operator uses 'dist_attr.execution_stream' to set stream of its own.
"""
for block in program.blocks:
for op in block.ops:
if op.name() == "pd_op.send_v2":
op.set_bool_attr("dynamic_shape", False)
op.set_bool_attr("use_calc_stream", True)
ring_id = op.attrs()["ring_id"]
op.set_execution_stream(f"send_stream_{ring_id}")
op.set_scheduling_priority(0)
elif op.name() == "pd_op.recv_v2":
op.set_bool_attr("dynamic_shape", False)
op.set_bool_attr("use_calc_stream", True)
op.set_execution_stream("recv_stream")
op.set_scheduling_priority(0)
def _insert_sync_for_fthenb_1f1b(program, dist_context=None):
"""
This implementation refers to lots of Paddle/python/paddle/base/optimizer.py.
The difference between this function with 'PipelineOptimizer' is that
'send_v2' op and 'recv_v2' op have been inserted in program by 'reshard'.
"""
for block in program.blocks:
offset = 0
first_optimize_index = None
for index, op in enumerate(list(block.ops)):
if is_optimize_op(op):
first_optimize_index = index
break
# insert sync ops
for index, op in enumerate(list(block.ops)):
# NOTE: pipeline might hang when dynamic_shape is True
if op.type in ['send_v2', 'recv_v2']:
op._set_attr("dynamic_shape", False)
# set send op on comm stream
if op.type == 'send_v2':
# step1: set 'use_calc_stream' False
op._set_attr("use_calc_stream", False)
op_role = op.attr('op_role')
ring_id = op.attr('ring_id')
# step2: insert 'c_sync_calc_stream' op before 'send_v2' op
var_name = op.input_arg_names[0]
var = block.var(var_name)
sync_calc_op = block._insert_op_without_sync(
index=index + offset,
type="c_sync_calc_stream",
inputs={'X': [var]},
outputs={'Out': [var]},
attrs={'op_role': op_role},
)
offset += 1
# step3: insert 'c_sync_comm_stream' op after 'send_v2' op or
# before the first optimize op
insert_index = None
new_op_role = None
if int(op_role) == int(OpRole.Backward):
insert_index = first_optimize_index + offset
new_op_role = OpRole.Optimize
else:
insert_index = index + offset + 1
new_op_role = OpRole.Backward
sync_comm_op = block._insert_op_without_sync(
index=insert_index,
type="c_sync_comm_stream",
inputs={'X': [var]},
outputs={'Out': [var]},
attrs={
'op_role': new_op_role,
'ring_id': ring_id,
},
)
if dist_context:
dist_op = dist_context.get_dist_op_for_program(op)
if dist_op:
out_dist_attr = dist_op.dist_attr.get_input_dist_attr(
var_name
)
op_dist_attr = OperatorDistAttr()
op_dist_attr.process_mesh = (
dist_op.dist_attr.process_mesh
)
op_dist_attr.chunk_id = dist_op.dist_attr.chunk_id
op_dist_attr.set_input_dist_attr(
var_name, out_dist_attr
)
op_dist_attr.set_output_dist_attr(
var_name, out_dist_attr
)
dist_context.set_op_dist_attr_for_program(
sync_calc_op, op_dist_attr
)
dist_context.set_op_dist_attr_for_program(
sync_comm_op, op_dist_attr
)
# step4: If 'send_v2' op in forward parse, set 'pipeline_flag' to distinguish
# whether the 'c_sync_comm_stream' op is inserted for pipeline.
if int(op_role) == int(OpRole.Forward):
sync_comm_op._set_attr('pipeline_flag', '')
offset += 1
block._sync_with_cpp()
offset = 0
backward_recv_index = None
for index, op in enumerate(block.ops):
if op.type == "recv_v2" and is_backward_op(op):
backward_recv_index = index
break
if backward_recv_index is None:
continue
# replace 'c_sync_comm_stream' op with 'nop' op
# use nop op for gc
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.output_arg_names[0]
var = block.var(var_name)
block._remove_op(index + offset, sync=False)
offset -= 1
block._sync_with_cpp()
def _add_ops_into_block(src_block, dst_block, ops):
for op in ops:
_create_program(src_block, dst_block, op)
def _is_fetch_op(op):
return op.type in ["fetch", "fetch_v2"]
def forward_complete_op_role(main_program):
all_ops = main_program.global_block().ops
ops_len = len(all_ops)
if len(all_ops) == 0:
return
iop = 0
first_left_op_role = None
first_right_op_role = None
while iop < ops_len:
if all_ops[iop].op_role != -1:
first_left_op_role = all_ops[iop].op_role
iop += 1
continue
else:
right_idx = iop + 1
while right_idx < ops_len and all_ops[right_idx].op_role == -1:
right_idx += 1
if right_idx >= ops_len: # [first_left_op_role, xx, xx, xx, xx]
assert first_left_op_role == -1, (
"first_left_op_role can't be -1."
)
for idx in range(iop, right_idx):
all_ops[idx].op_role = first_left_op_role
break
else: # [first_left_op_role, xx, xx, xx, xx, first_right_op_role]
first_right_op_role = all_ops[right_idx].op_role
assert (
first_left_op_role == -1
or first_left_op_role == first_right_op_role
), (
f"The left and right operators of (idx[{iop}]) have different op_role."
)
for idx in range(iop, right_idx):
all_ops[idx].op_role = first_right_op_role
iop = right_idx + 1
if first_left_op_role == -1 and first_right_op_role == -1:
raise ValueError("all the ops don't have the op_role.")
def infer_chunk_id(op_idx, ops, with_dist=True):
def get_chunk_id(op_idx):
if op_idx < 0 or op_idx >= len(ops):
return -1
op = ops[op_idx]
if with_dist:
if op.dist_attr is None:
return -1
else:
return op.dist_attr.chunk_id
else:
if op.has_attr("chunk_id"):
return op.chunk_id
else:
return -1
prev_op_chunk_id = get_chunk_id(op_idx - 1)
next_op_chunk_id = get_chunk_id(op_idx + 1)
if prev_op_chunk_id == next_op_chunk_id:
return prev_op_chunk_id
next_next_op_chunk_id = get_chunk_id(op_idx + 2)
if next_op_chunk_id == next_next_op_chunk_id:
return next_op_chunk_id
if ops[op_idx].name() in ["builtin.combine", "builtin.split"]:
result_var = ops[op_idx].result(0)
all_used_ops = result_var.all_used_ops()
for used_op in all_used_ops:
if used_op.dist_attr and used_op.dist_attr.chunk_id != -1:
return used_op.dist_attr.chunk_id != -1
elif used_op.has_attr("chunk_id") and used_op.chunk_id != -1:
return used_op.chunk_id
return -1
def find_var_used_op_chunk_id(var):
visited = set()
def dfs(var):
all_used_ops = var.all_used_ops()
for used_op in all_used_ops:
if used_op in visited:
return -1
visited.add(used_op)
if used_op.dist_attr and used_op.dist_attr.chunk_id != -1:
return used_op.dist_attr.chunk_id
else:
for output_var in used_op.results():
chunk_id = dfs(output_var)
if chunk_id != -1:
return chunk_id
return -1
return dfs(var)
def _split_program_into_forward_backward_optimize(
main_program, enable_send_recv_overlap=False
):
_pir_overlap_send_recv(main_program)
forward_complete_op_role(main_program)
complete_ops = main_program.global_block().ops
fwd_program = main_program.clone()
bwd_program = main_program.clone()
opt_program = main_program.clone()
fwd_ops = fwd_program.global_block().ops
bwd_ops = bwd_program.global_block().ops
opt_ops = opt_program.global_block().ops
opt_block = opt_program.global_block()
bwd_block = bwd_program.global_block()
place = _get_device()
if isinstance(place, paddle.framework.CUDAPlace):
place = paddle.framework.CUDAPlace(
paddle.distributed.ParallelEnv().dev_id
)
cur_place = paddle.base.libpaddle.Place()
cur_place.set_place(place)
region = "opt"
for op_idx in range(len(complete_ops) - 1, -1, -1):
if complete_ops[op_idx].op_role != -1:
if complete_ops[op_idx].op_role == 1:
region = "bwd"
elif complete_ops[op_idx].op_role == 0:
region = "fwd"
elif complete_ops[op_idx].op_role == 2:
region = "opt"
if region == "opt":
# in optimize program, both forward and backward ops should be removed
fwd_ops[op_idx].erase()
bwd_ops[op_idx].erase()
elif region == "bwd":
fwd_ops[op_idx].erase()
for idx in range(opt_ops[op_idx].num_results()):
# if this op's output is used, create the persistable
# var to be used in other programs.
result_in_opt = opt_ops[op_idx].result(idx)
if result_in_opt.use_empty() is False:
name = f"var_{op_idx}_{complete_ops[op_idx].name()}_{idx}"
paddle.pir.set_insertion_point_after(bwd_ops[op_idx])
paddle._C_ops.set_persistable_value(
bwd_ops[op_idx].result(idx), name
)
new_result_var_in_opt = opt_block.add_kwarg(
name, result_in_opt.type()
)
new_result_var_in_opt.place_attr = cur_place
new_result_var_in_opt.persistable = (
result_in_opt.persistable
)
opt_ops[op_idx].result(idx).replace_all_uses_with(
new_result_var_in_opt
)
opt_ops[op_idx].erase()
else:
# in backward program, only the forward ops should be removed
for idx in range(opt_ops[op_idx].num_results()):
# if this op's output is used, create the persistable
# var to be used in other programs.
result_in_opt = opt_ops[op_idx].result(idx)
result_in_bwd = bwd_ops[op_idx].result(idx)
if (
result_in_opt.use_empty() is False
or result_in_bwd.use_empty() is False
):
if (
fwd_ops[op_idx].name() == "pd_op.data"
or fwd_ops[op_idx].name() == "builtin.parameter"
):
name = fwd_ops[op_idx].result(idx).name
# fwd_ops[op_idx].result(idx).persistable = True
else:
result_value = complete_ops[op_idx].result(idx)
used_ops = result_value.all_used_ops()
shadow_output_op_used = None
for used_op in used_ops:
if used_op.name() == "builtin.shadow_output":
shadow_output_op_used = used_op
if shadow_output_op_used is not None:
name = shadow_output_op_used.attrs()["output_name"]
# fwd_ops[op_idx].result(idx).persistable = True
else:
name = f"var_{op_idx}_{complete_ops[op_idx].name()}_{idx}"
paddle.pir.set_insertion_point_after(
fwd_ops[op_idx]
)
paddle._C_ops.set_persistable_value(
fwd_ops[op_idx].result(idx), name
)
# fwd_ops[op_idx].result(idx).persistable = True
if result_in_opt.use_empty() is False:
new_result_var_in_opt = opt_block.add_kwarg(
name, result_in_opt.type()
)
new_result_var_in_opt.place_attr = cur_place
new_result_var_in_opt.persistable = (
result_in_opt.persistable
)
opt_ops[op_idx].result(idx).replace_all_uses_with(
new_result_var_in_opt
)
if result_in_bwd.use_empty() is False:
new_result_var_in_bwd = bwd_block.add_kwarg(
name, result_in_bwd.type()
)
new_result_var_in_bwd.place_attr = cur_place
new_result_var_in_bwd.persistable = (
result_in_bwd.persistable
)
bwd_ops[op_idx].result(idx).replace_all_uses_with(
new_result_var_in_bwd
)
opt_ops[op_idx].erase()
bwd_ops[op_idx].erase()
return fwd_program, bwd_program, opt_program
def _pir_get_backward_op_type(all_ops, op_idx):
cur_op = all_ops[op_idx]
# deal the ops pattern:
# [reshape, reshape, matmul, reshape, add(grad_merge)]
def is_reshape_matmul_pattern():
ops_pattern = [
"pd_op.full_int_array",
"pd_op.reshape",
"pd_op.full_int_array",
"pd_op.reshape",
"pd_op.matmul",
"pd_op.full_int_array",
"pd_op.reshape",
"pd_op.add_",
]
if not cur_op.has_attr("grad_merge_add"):
return False
if op_idx < 8:
return False
for i in range(8):
if all_ops[op_idx - i].name() != ops_pattern[7 - i]:
return False
return True
def used_by_grad_merge_add(value):
for op in value.all_used_ops():
if op.has_attr("grad_merge_add"):
return True
return False
# For the cur_op doesn't have output such as 'send_v2', it should be backward_b.
if cur_op.num_results() == 0:
return ["backward_b"]
if is_reshape_matmul_pattern():
return ["backward_w"] * 8
if cur_op.has_attr("grad_merge_add"):
return ["backward_w"]
# backward_w type op should only output grad of parameters
for output in cur_op.results():
if not used_by_grad_merge_add(output):
return ["backward_b"]
return ["backward_w"]
def _create_program_and_ops(program, job_type, chunk_id=None):
if chunk_id is not None:
program_name = f"{job_type}{chunk_id}"
else:
program_name = job_type
cloned_program = program.clone()
ops = cloned_program.global_block().ops
return program_name, cloned_program, ops
def _build_vpp_sub_programs(program, split_method):
type_to_program = OrderedDict()
for ib, src_block in enumerate(program.blocks):
type_to_ops = split_method(src_block)
fetch_ops = type_to_ops.pop("fetch", [])
dst_blocks = []
if ib == 0:
for type, ops in type_to_ops.items():
type_to_program[type] = Program()
dst_block = type_to_program[type].block(0)
_add_ops_into_block(src_block, dst_block, ops)
dst_blocks.append(dst_block)
else:
for type, ops in type_to_ops.items():
if len(ops) > 0:
dst_block = type_to_program[type]._create_block(
parent_idx=src_block.parent_idx
)
dst_block._set_forward_block_idx(
src_block.forward_block_idx
)
_add_ops_into_block(src_block, dst_block, ops)
dst_blocks.append(dst_block)
for fetch_op in fetch_ops:
in_name = fetch_op.input('X')[0]
fetch_block = None
for dst_block in dst_blocks:
if dst_block._find_var_recursive(in_name):
fetch_block = dst_block
break
if fetch_block:
_create_program(src_block, fetch_block, fetch_op)
return type_to_program
def _add_event_dependency(recorder_op, waiter_op):
'''
Add the extra event dependency of the two operators.
This function mainly aims for the cross-programs in pipeline parallelism,
especial for the 'send_v2' 'recv_v2' etc.
'''
if not recorder_op.dist_attr.force_record_event:
recorder_op.dist_attr.force_record_event = True
# NOTE(lizhiyu): Here is the copy of 'waiter_op.dist_attr.events_to_wait' not the reference,
# because the type of 'events_to_wait' is 'const vector<string>&' while the type of
# 'waiter_wait_list' is python list.
waiter_wait_list = waiter_op.dist_attr.events_to_wait
if recorder_op.dist_attr.event_to_record not in waiter_wait_list:
waiter_wait_list.append(recorder_op.dist_attr.event_to_record)
waiter_op.dist_attr.events_to_wait = waiter_wait_list
def _insert_reshape_op(
block,
index,
x,
shape,
op_role,
chunk_id,
dist_context,
out=None,
op_namescope="/",
):
var_x = block.var(x[0])
x_dist_attr = dist_context.get_tensor_dist_attr_for_program(var_x)
if out is None:
out = block.create_var(
name=f"{x[0]}@reshape.out",
dtype=var_x.dtype,
persistable=False,
)
dist_context.set_tensor_dist_attr_for_program(out, x_dist_attr)
x_shape = block.create_var(name=f"{x[0]}@reshape.xshape", dtype=var_x.dtype)
dist_context.set_tensor_dist_attr_for_program(x_shape, x_dist_attr)
reshape_op = block._insert_op_without_sync(
index=index,
type="reshape2",
inputs={"X": x},
outputs={"Out": out, "XShape": x_shape},
attrs={
"shape": shape,
"op_role": op_role,
'op_namescope': op_namescope,
},
)
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
reshape_op,
process_mesh=x_dist_attr.process_mesh,
ref_mapping=x_dist_attr.dims_mapping,
ctx=dist_context,
chunk_id=chunk_id,
)
return out
def split_matmul_grad_to_matmul(
block, matmul_grad_id, dist_context, op_namescope="/"
):
ops = block.ops
matmul_grad_op = ops[matmul_grad_id]
tran_x = matmul_grad_op.attr("trans_x")
assert not tran_x, (
f"matmul_grad(id={matmul_grad_id}) with tran_x == True is not supported for splitting matmul_grad to matmul"
)
tran_y = matmul_grad_op.attr("trans_y")
assert not tran_y, (
f"matmul_grad(id={matmul_grad_id}) with tran_y == True is not supported for splitting matmul_grad to matmul"
)
x = matmul_grad_op.input("X")
y = matmul_grad_op.input("Y")
out_grad = matmul_grad_op.input("Out@GRAD")
x_grad = matmul_grad_op.output("X@GRAD")
y_grad = matmul_grad_op.output("Y@GRAD")
op_role = matmul_grad_op.attr("op_role")
var_x = block.var(x[0])
var_out_grad = block.var(out_grad[0])
var_y_grad = block.var(y_grad[0])
x_dims = var_x.shape
out_grad_dims = var_out_grad.shape
y_grad_dims = var_y_grad.shape
assert len(x_dims) == len(out_grad_dims), (
f"The rank of x must be equal to that of out_grad, but got x rank = {len(x_dims)} and out_grad rank = {len(out_grad_dims)}."
)
if len(x_dims) > 2:
assert x_dims[0:2] == out_grad_dims[0:2], (
f"The first two dimensions of x must be equal to that of out_grad, but got x_dims:{x_dims} and out_grad_dims:{out_grad_dims}."
)
new_x_dims = [x_dims[0] * x_dims[1], *list(x_dims[2:])]
new_out_grad_dims = [
out_grad_dims[0] * out_grad_dims[1],
*out_grad_dims[2:],
]
# NOTE(Ruibiao): Why insert reshape op here?
# When the rank of input matrix is 3, MatmulGradKernel use reshape to fold the first two dimensions of x and out_grad (see FoldInitDims in matmul_grad_kernel_impl.h), and then calls blas.Matmul to calculate y_grad.
# If we directly append matmul op to calculate y_grad without FoldInitDims, blas.BatchedGEMM is actually called in MatmulKernel, which has a larger cost than using blas.Matmul after dimension folding.
# Therefore, we imitate MatmulGradKernel here by inserting reshape op before matmul.
chunk_id = dist_context.get_op_dist_attr_for_program(
matmul_grad_op
).chunk_id
new_x = _insert_reshape_op(
block,
matmul_grad_id + 1,
x,
new_x_dims,
op_role,
chunk_id=chunk_id,
dist_context=dist_context,
op_namescope=op_namescope,
)
new_out_grad = _insert_reshape_op(
block,
matmul_grad_id + 2,
out_grad,
new_out_grad_dims,
op_role,
chunk_id=chunk_id,
dist_context=dist_context,
op_namescope=op_namescope,
)
new_y_grad = block.create_var(
name=f"{y_grad[0]}@reshape.out",
dtype=var_y_grad.dtype,
persistable=False,
)
dist_context.set_tensor_dist_attr_for_program(
new_y_grad,
dist_context.get_tensor_dist_attr_for_program(var_y_grad),
)
matmul_grad_dist_attr = dist_context.get_op_dist_attr_for_program(
matmul_grad_op
)
matmul_grad_dist_attr.set_input_dist_attr(
new_x.name, dist_context.get_tensor_dist_attr_for_program(var_x)
)
matmul_grad_dist_attr.set_input_dist_attr(
new_out_grad.name,
dist_context.get_tensor_dist_attr_for_program(var_out_grad),
)
matmul_grad_dist_attr.set_output_dist_attr(
new_y_grad.name,
dist_context.get_tensor_dist_attr_for_program(var_y_grad),
)
matmul_op = block._insert_op_without_sync(
index=matmul_grad_id + 3,
type="matmul_v2",
inputs={"X": new_x, "Y": new_out_grad},
outputs={"Out": new_y_grad},
attrs={
"trans_x": True,
"trans_y": False,
"op_role": op_role,
'op_namescope': op_namescope,
},
)
dist_context.set_op_dist_attr_for_program(matmul_op, matmul_grad_dist_attr)
_insert_reshape_op(
block,
matmul_grad_id + 4,
[new_y_grad.name],
y_grad_dims,
op_role,
chunk_id=chunk_id,
dist_context=dist_context,
out=y_grad,
op_namescope=op_namescope,
)
matmul_op = block._insert_op_without_sync(
index=matmul_grad_id + 1,
type="matmul_v2",
inputs={"X": out_grad, "Y": y},
outputs={"Out": x_grad},
attrs={
"trans_x": False,
"trans_y": True,
"op_role": op_role,
'op_namescope': op_namescope,
},
)
dist_context.set_op_dist_attr_for_program(
matmul_op, dist_context.get_op_dist_attr_for_program(matmul_grad_op)
)
block._remove_op(matmul_grad_id, sync=False)
def _pir_split_matmul_grad_to_matmul(block, matmul_grad_id):
ops = block.ops
matmul_grad_op = ops[matmul_grad_id]
assert not matmul_grad_op.has_attr("trans_x"), (
f"matmul_grad(id={matmul_grad_id}) with tran_x == True is not supported for splitting matmul_grad to matmul"
)
assert not matmul_grad_op.has_attr("trans_y"), (
f"matmul_grad(id={matmul_grad_id}) with tran_y == True is not supported for splitting matmul_grad to matmul"
)
x = matmul_grad_op.operand_source(0)
y = matmul_grad_op.operand_source(1)
out_grad = matmul_grad_op.operand_source(2)
x_grad = matmul_grad_op.result(0)
y_grad = matmul_grad_op.result(1)
op_role = matmul_grad_op.op_role
x_dims = x.shape
out_grad_dims = out_grad.shape
y_grad_dims = y_grad.shape
assert len(x_dims) == len(out_grad_dims), (
f"The rank of x must be equal to that of out_grad, but got x rank = {len(x_dims)} and out_grad rank = {len(out_grad_dims)}."
)
if len(x_dims) > 2:
assert x_dims[0:2] == out_grad_dims[0:2], (
f"The first two dimensions of x must be equal to that of out_grad, but got x_dims:{x_dims} and out_grad_dims:{out_grad_dims}."
)
new_x_dims = [x_dims[0] * x_dims[1], *list(x_dims[2:])]
new_out_grad_dims = [
out_grad_dims[0] * out_grad_dims[1],
*out_grad_dims[2:],
]
# NOTE(Ruibiao): Why insert reshape op here?
# When the rank of input matrix is 3, MatmulGradKernel use reshape to fold the first two dimensions of x and out_grad (see FoldInitDims in matmul_grad_kernel_impl.h), and then calls blas.Matmul to calculate y_grad.
# If we directly append matmul op to calculate y_grad without FoldInitDims, blas.BatchedGEMM is actually called in MatmulKernel, which has a larger cost than using blas.Matmul after dimension folding.
# Therefore, we imitate MatmulGradKernel here by inserting reshape op before matmul.
chunk_id = matmul_grad_op.chunk_id
paddle.pir.set_insertion_point_after(matmul_grad_op)
new_x = paddle._C_ops.reshape(x, new_x_dims)
x_reshape_op = new_x.get_defining_op()
x_reshape_op.op_role = op_role
x_reshape_op.set_int_attr("chunk_id", chunk_id)
x_reshape_op.operand_source(1).get_defining_op().op_role = op_role
x_reshape_op.operand_source(1).get_defining_op().set_int_attr(
"chunk_id", chunk_id
)
paddle.pir.set_insertion_point_after(x_reshape_op)
new_out_grad = paddle._C_ops.reshape(out_grad, new_out_grad_dims)
out_grad_reshape_op = new_out_grad.get_defining_op()
out_grad_reshape_op.op_role = op_role
out_grad_reshape_op.set_int_attr("chunk_id", chunk_id)
out_grad_reshape_op.operand_source(1).get_defining_op().op_role = op_role
out_grad_reshape_op.operand_source(1).get_defining_op().set_int_attr(
"chunk_id", chunk_id
)
paddle.pir.set_insertion_point_after(out_grad_reshape_op)
new_y_grad = paddle._C_ops.matmul(new_x, new_out_grad, True, False)
new_matmul_op = new_y_grad.get_defining_op()
new_matmul_op.op_role = op_role
new_matmul_op.set_int_attr("chunk_id", chunk_id)
paddle.pir.set_insertion_point_after(new_matmul_op)
new_y_grad_reshape = paddle._C_ops.reshape(new_y_grad, y_grad_dims)
y_grad_reshape_op = new_y_grad_reshape.get_defining_op()
y_grad_reshape_op.op_role = op_role
y_grad_reshape_op.set_int_attr("chunk_id", chunk_id)
y_grad_reshape_op.operand_source(1).get_defining_op().op_role = op_role
y_grad_reshape_op.operand_source(1).get_defining_op().set_int_attr(
"chunk_id", chunk_id
)
paddle.pir.set_insertion_point_after(matmul_grad_op)
new_x_grad = paddle._C_ops.matmul(out_grad, y, False, True)
new_x_grad.get_defining_op().op_role = op_role
new_x_grad.get_defining_op().set_int_attr("chunk_id", chunk_id)
x_grad.replace_all_uses_with(new_x_grad)
y_grad.replace_all_uses_with(new_y_grad_reshape)
matmul_grad_op.erase()
class PipelineMemoryEstimator:
def __init__(self):
self.type_to_skip_gc_vars = {}
self.program_types = []
self.logger = logging.getLogger(__name__)
def set_program_skip_gc_vars(self, type_to_program, program_types):
"""
Get the skip_gc_vars for each type of program.
The order of program_types is the same as the order in the pipeline's micro batch.
For example, in 1F1B pipeline, the order of program_types is ['forward', 'backward'].
"""
self.program_types = program_types
type_to_required_vars = {}
for type, program in type_to_program.items():
type_to_required_vars[type] = _get_required_vars_of_program(program)
self.type_to_skip_gc_vars[type] = {}
suffixed_required_vars = set()
for job_type in reversed(program_types):
required_vars = type_to_required_vars[job_type]
skip_gc_vars = required_vars & suffixed_required_vars
if job_type in ["backward", "backward_w"]:
assert len(skip_gc_vars) == 0, (
f"When enabling pipeline parallelism strategy, the skip_gc_vars for {job_type} subprogram must be empty, but it is {skip_gc_vars}."
)
skip_gc_vars = dict(zip(skip_gc_vars, [-1] * len(skip_gc_vars)))
self.type_to_skip_gc_vars[job_type] = skip_gc_vars
suffixed_required_vars |= required_vars
def estimate_memory(self, program, program_type, dist_context):
if program_type not in self.type_to_skip_gc_vars:
raise ValueError(
f"Please set the skip_gc_vars before estimating memory for {program_type} program."
)
ordered_ops = [
[op.desc.id(), op] for block in program.blocks for op in block.ops
]
ordered_ops.sort(key=lambda x: x[0])
# Step1: Process operations to get the var info
var_info = self._get_program_var_info(ordered_ops, dist_context)
for var_name in self.type_to_skip_gc_vars[program_type]:
if var_name not in var_info:
continue
self.type_to_skip_gc_vars[program_type][var_name] = var_info[
var_name
]["size"]
# Step2: Record the visited vars in the previous program
visited_vars = {}
skip_gc_vars = self.type_to_skip_gc_vars[program_type]
if self.program_types.index(program_type) >= 1:
prev_program_type = self.program_types[
self.program_types.index(program_type) - 1
]
visited_vars = self.type_to_skip_gc_vars[prev_program_type]
# Step3: Estimate the max memory usage during the program execution
mem_usage, max_memory = self._estimate_max_memory(
ordered_ops, var_info, skip_gc_vars, visited_vars
)
return mem_usage, max_memory
def _estimate_max_memory(
self, ordered_ops, var_info, skip_gc_vars, visited_vars
):
mem_usage = 0
max_memory = 0
has_used_vars = set()
# no need to allocate memory for the variables
# that are already allocated in the previous program
for var_name in visited_vars:
has_used_vars.add(var_name)
for _, op in ordered_ops:
if op.type in [
"create_py_reader",
"create_double_buffer_reader",
"read",
]:
continue
last_use_vars = []
for var_name in op.input_arg_names + op.output_arg_names:
if var_name not in var_info:
continue
var_info[var_name]["count"] -= 1
if var_name not in has_used_vars and not self._is_persistable(
var_name, var_info
):
has_used_vars.add(var_name)
self.logger.debug(
f"add {var_name}, var size: {var_info[var_name]['size']},"
f"count: {var_info[var_name]['count']},"
f"mem_usage: {mem_usage} -> {mem_usage + var_info[var_name]['size']},"
f"op type: {op.type}, input_arg_names: {op.input_arg_names}, output_arg_names: {op.output_arg_names}"
)
mem_usage += var_info[var_name]["size"]
max_memory = max(max_memory, mem_usage)
if self._is_last_used(var_name, var_info):
if (
not self._is_persistable(var_name, var_info)
and var_name not in skip_gc_vars
):
last_use_vars.append(var_name)
max_memory = max(max_memory, mem_usage)
# Release the memory of the variables that are not used anymore
for var_name in set(last_use_vars):
self.logger.debug(
f"remove {var_name}, var size: {var_info[var_name]['size']},"
f"count: {var_info[var_name]['count']},"
f"mem_usage: {mem_usage} -> {mem_usage - var_info[var_name]['size']},"
f"op type: {op.type}, input_arg_names: {op.input_arg_names}, output_arg_names: {op.output_arg_names}"
)
mem_usage -= var_info[var_name]["size"]
if var_name in visited_vars:
visited_vars[var_name] -= var_info[var_name]["size"]
for var_name in visited_vars:
if var_name not in skip_gc_vars:
mem_usage -= visited_vars[var_name]
return mem_usage, max_memory
def _get_increase_memory(self, program_type):
"""
For a given type of program, calculate the increase memory usage.
The increase memory usage is the memory usage of the variables that are setting to skip_gc_vars.
Persistable variables are not included in the increase memory usage because they are allocated when
running the startup program.
"""
skip_gc_vars = self.type_to_skip_gc_vars[program_type]
increase_memory = sum([mem for _, mem in skip_gc_vars.items()])
if increase_memory < 0:
raise ValueError(
"No size info for skip_gc_vars, please run estimate_memory to get var size info."
)
return increase_memory
def _get_program_var_info(self, ordered_ops, dist_context):
var_info = {}
for _, op in ordered_ops:
if op.type in [
"create_py_reader",
"create_double_buffer_reader",
"read",
]:
continue
op_info = OpInOutInfo()
op_info.build_info(op)
for var_name in op.input_arg_names + op.output_arg_names:
if not op_info.is_needed(var_name):
continue
dist_op = dist_context.get_dist_op_for_program(op)
if dist_op:
self._update_var_info(
var_name,
dist_op,
var_info,
is_input=var_name in op.input_arg_names,
)
return var_info
def _update_var_info(self, var_name, dist_op, var_info, is_input):
var = (
dist_op.get_serial_input(var_name)
if is_input
else dist_op.get_serial_output(var_name)
)
if var_name not in var_info:
var_info.setdefault(
var_name, {"size": 0, "count": 1, "persistable": False}
)
if var.persistable:
var_info[var_name]["persistable"] = True
return
var_size = self._get_var_size(var)
var_info[var_name]["size"] = var_size
else:
var_info[var_name]["count"] += 1
def _get_var_size(self, var):
var_shape = [1 if dim == -1 else dim for dim in var.shape]
return self._calculate_bytes(var_shape, var.dtype)
def _calculate_bytes(self, var_shape, dtype):
dtype_to_size = {
paddle.float64: 8,
paddle.int64: 8,
paddle.float32: 4,
paddle.int32: 4,
paddle.float16: 2,
paddle.bfloat16: 2,
paddle.int16: 2,
paddle.int8: 1,
paddle.uint8: 1,
}
total_count = (
reduce(lambda x, y: x * y, var_shape, 1) if var_shape else 0
)
dtype_factor = dtype_to_size.get(dtype, 4)
return total_count * dtype_factor
def _is_last_used(self, var_name, var_info):
if var_name not in var_info:
return False
return var_info[var_name]["count"] == 0
def _is_persistable(self, var_name, var_info):
if var_name not in var_info:
return False
return var_info[var_name]["persistable"]