388 lines
13 KiB
Python
Executable File
388 lines
13 KiB
Python
Executable File
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import numpy as np
|
|
|
|
import paddle
|
|
from paddle.framework import core
|
|
from paddle.utils import unique_name
|
|
|
|
from .pass_base import PassBase, PassType, register_pass
|
|
|
|
|
|
def find_adjacent_match_sequences(
|
|
iterable, filter_func, adjacent_filter_func=None
|
|
):
|
|
n = len(iterable)
|
|
match_sequences = []
|
|
if adjacent_filter_func is None:
|
|
adjacent_filter_func = lambda ref_op, new_op: True
|
|
i = 0
|
|
while True:
|
|
while i < n and not filter_func(iterable[i]):
|
|
i += 1
|
|
j = i + 1
|
|
while (
|
|
j < n
|
|
and filter_func(iterable[j])
|
|
and adjacent_filter_func(iterable[i], iterable[j])
|
|
):
|
|
j += 1
|
|
if i < n and j <= n:
|
|
match_sequences.append((i, j))
|
|
i = j + 1
|
|
if i >= n:
|
|
break
|
|
return match_sequences
|
|
|
|
|
|
def insert_fuse_all_reduce_ops(
|
|
block, reversed_op_indices, input_var_names, output_var_names, dtype, attrs
|
|
):
|
|
fused_var = block.create_var(
|
|
name=unique_name.generate(f"FusedOutput_{input_var_names[0]}"),
|
|
dtype=dtype,
|
|
)
|
|
|
|
# FIXME(zengjinle): here we assume that we use
|
|
# c_sync_calc_stream/c_sync_comm_stream to do sync.
|
|
# But someone may use c_wait_compute/c_wait_comm instead.
|
|
if not attrs["use_calc_stream"]:
|
|
ring_id = attrs["ring_id"]
|
|
new_op_indices = list(reversed_op_indices)
|
|
|
|
for i, op_idx in enumerate(reversed_op_indices):
|
|
prev_op_idx = op_idx - 1
|
|
while (
|
|
prev_op_idx >= 0
|
|
and block.ops[prev_op_idx].type == "c_sync_calc_stream"
|
|
):
|
|
new_op_indices.append(prev_op_idx)
|
|
prev_op_idx -= 1
|
|
|
|
if i > 0:
|
|
next_op_idx = op_idx + 1
|
|
n = len(block.ops)
|
|
while (
|
|
next_op_idx < n
|
|
and block.ops[next_op_idx].type == "c_sync_comm_stream"
|
|
):
|
|
assert block.ops[next_op_idx].attr("ring_id") == ring_id
|
|
new_op_indices.append(next_op_idx)
|
|
|
|
new_op_indices = list(set(new_op_indices))
|
|
new_op_indices.sort(reverse=True)
|
|
reversed_op_indices = new_op_indices
|
|
|
|
insert_idx = reversed_op_indices[0] + 1
|
|
op_role_key = core.op_proto_and_checker_maker.kOpRoleAttrName()
|
|
|
|
concated_shapes = []
|
|
concated_ranks = []
|
|
for var_name in output_var_names:
|
|
shape = block._find_var_recursive(var_name).shape
|
|
concated_shapes.extend(shape)
|
|
concated_ranks.append(len(shape))
|
|
|
|
coalesce_tensor_op_kwargs = {
|
|
"type": "coalesce_tensor",
|
|
"inputs": {
|
|
"Input": input_var_names,
|
|
},
|
|
"outputs": {
|
|
"Output": output_var_names,
|
|
"FusedOutput": fused_var,
|
|
},
|
|
"attrs": {
|
|
"use_align": True,
|
|
"dtype": dtype,
|
|
"concated_shapes": concated_shapes,
|
|
"concated_ranks": concated_ranks,
|
|
op_role_key: attrs[op_role_key],
|
|
},
|
|
}
|
|
|
|
if not attrs["use_calc_stream"]:
|
|
block._insert_op_without_sync(
|
|
insert_idx,
|
|
type="c_sync_calc_stream",
|
|
inputs={"X": fused_var},
|
|
outputs={"Out": fused_var, op_role_key: attrs[op_role_key]},
|
|
)
|
|
insert_idx += 1
|
|
|
|
# all_reduce sum should insert
|
|
attrs["reduce_type"] = paddle.distributed.ReduceOp.SUM
|
|
block._insert_op_without_sync(
|
|
insert_idx,
|
|
type="all_reduce",
|
|
inputs={"x": fused_var},
|
|
outputs={"out": fused_var},
|
|
attrs=attrs,
|
|
)
|
|
|
|
for op_idx in reversed_op_indices:
|
|
block._remove_op(op_idx)
|
|
|
|
return coalesce_tensor_op_kwargs
|
|
|
|
|
|
def has_same_attrs(op1, op2, attr_names):
|
|
for attr_name in attr_names:
|
|
if op1.attr(attr_name) != op2.attr(attr_name):
|
|
return False
|
|
return True
|
|
|
|
|
|
def filter_all_collective_op_indices(block):
|
|
# NOTE: should add more collective ops
|
|
all_collective_ops = {
|
|
"c_broadcast",
|
|
"broadcast",
|
|
"all_gather",
|
|
"all_reduce",
|
|
}
|
|
|
|
match_op_indices = []
|
|
for i, op in enumerate(block.ops):
|
|
if op.type in all_collective_ops:
|
|
match_op_indices.append(i)
|
|
return match_op_indices
|
|
|
|
|
|
def find_all_fuse_all_reduce_groups(block):
|
|
collective_op_indices = filter_all_collective_op_indices(block)
|
|
collective_ops = [block.ops[i] for i in collective_op_indices]
|
|
|
|
def is_valid_allreduce_op(op):
|
|
if op.type != "c_allreduce_sum" or op.attr("use_model_parallel"):
|
|
return False
|
|
in_var_name = op.input("X")[0]
|
|
out_var_name = op.output("Out")[0]
|
|
if in_var_name != out_var_name:
|
|
return False
|
|
in_var = block._find_var_recursive(in_var_name)
|
|
assert in_var is not None
|
|
if in_var.type != core.VarDesc.VarType.DENSE_TENSOR:
|
|
return False
|
|
shape = in_var.shape
|
|
if any(s <= 0 for s in shape):
|
|
return False
|
|
return True
|
|
|
|
same_attr_names = [
|
|
"ring_id",
|
|
"use_calc_stream",
|
|
core.op_proto_and_checker_maker.kOpRoleAttrName(),
|
|
core.op_proto_and_checker_maker.kOpDeviceAttrName(),
|
|
]
|
|
|
|
def is_same_adjacent_op(ref_op, new_op):
|
|
if not has_same_attrs(ref_op, new_op, same_attr_names):
|
|
return False
|
|
ref_op_in_var = block._find_var_recursive(ref_op.input("X")[0])
|
|
new_op_in_var = block._find_var_recursive(new_op.input("X")[0])
|
|
if ref_op_in_var.dtype != new_op_in_var.dtype:
|
|
return False
|
|
return True
|
|
|
|
match_seqs = find_adjacent_match_sequences(
|
|
collective_ops, is_valid_allreduce_op, is_same_adjacent_op
|
|
)
|
|
new_match_seqs = []
|
|
for i, j in match_seqs:
|
|
new_match_seqs.append([collective_op_indices[k] for k in range(i, j)])
|
|
return new_match_seqs
|
|
|
|
|
|
def split_fuse_all_reduce_groups_by_deps(block, groups, op_deps):
|
|
new_groups = []
|
|
|
|
def insert_new_group(op_indices, start_idx, end_idx):
|
|
if end_idx - start_idx > 1:
|
|
new_groups.append(op_indices[start_idx:end_idx])
|
|
|
|
for op_indices in groups:
|
|
n = len(op_indices)
|
|
assert n > 0
|
|
if n == 1:
|
|
continue
|
|
|
|
start_idx = 0
|
|
k = start_idx + 1
|
|
while k < n:
|
|
found_group = False
|
|
for prev_idx in range(start_idx, k):
|
|
dep = op_deps[op_indices[prev_idx]][op_indices[k]]
|
|
if dep == core.Node.Dep.NoDep:
|
|
continue
|
|
# [start_idx, k) is valid groups
|
|
insert_new_group(op_indices, start_idx, k)
|
|
start_idx = k
|
|
break
|
|
k += 1
|
|
|
|
insert_new_group(op_indices, start_idx, k)
|
|
|
|
return new_groups
|
|
|
|
|
|
def insert_coalesce_tensor_ops(block, coalesce_ops_kwargs):
|
|
if not coalesce_ops_kwargs:
|
|
return
|
|
|
|
var_infos = {}
|
|
for idx, op in enumerate(block.ops):
|
|
for var in op.input_arg_names:
|
|
if var not in var_infos:
|
|
var_infos[var] = [idx, True]
|
|
|
|
for var in op.output_arg_names:
|
|
if var not in var_infos:
|
|
var_infos[var] = [idx, False]
|
|
|
|
n = len(block.ops)
|
|
insert_idx_and_kwargs = []
|
|
for group_idx, kwargs in enumerate(coalesce_ops_kwargs):
|
|
all_vars = kwargs["inputs"]["Input"] + kwargs["outputs"]["Output"]
|
|
min_op_idx = n
|
|
copy_data = False
|
|
for var in all_vars:
|
|
if var not in var_infos:
|
|
copy_data = True
|
|
min_idx = 0
|
|
break
|
|
op_idx, is_input = var_infos[var]
|
|
if is_input:
|
|
copy_data = True
|
|
min_op_idx = min(min_op_idx, op_idx)
|
|
kwargs["attrs"]["copy_data"] = copy_data
|
|
insert_idx_and_kwargs.append((min_op_idx, kwargs))
|
|
|
|
insert_idx_and_kwargs.sort(key=lambda element: element[0], reverse=True)
|
|
for idx, kwargs in insert_idx_and_kwargs:
|
|
block._insert_op_without_sync(idx, **kwargs)
|
|
|
|
|
|
def insert_fuse_all_reduce_by_memory_size(block, groups, max_memory_size):
|
|
op_role_key = core.op_proto_and_checker_maker.kOpRoleAttrName()
|
|
op_role_var_key = core.op_proto_and_checker_maker.kOpRoleVarAttrName()
|
|
op_device_key = core.op_proto_and_checker_maker.kOpDeviceAttrName()
|
|
coalesce_ops_kwargs = []
|
|
for group in reversed(groups):
|
|
first_op = block.ops[group[0]]
|
|
ring_id = first_op.attr("ring_id")
|
|
use_calc_stream = first_op.attr("use_calc_stream")
|
|
use_model_parallel = first_op.attr("use_model_parallel")
|
|
op_role = first_op.attr(op_role_key)
|
|
op_device = first_op.attr(op_device_key)
|
|
|
|
attrs = {
|
|
"ring_id": ring_id,
|
|
"use_calc_stream": use_calc_stream,
|
|
"use_model_parallel": use_model_parallel,
|
|
op_role_key: op_role,
|
|
op_device_key: op_device,
|
|
}
|
|
dtype = block._find_var_recursive(first_op.input("X")[0]).dtype
|
|
sizeof = core.size_of_dtype(dtype)
|
|
|
|
cur_mem_size = 0
|
|
op_role_vars = []
|
|
recorded_op_indices = []
|
|
in_var_names = []
|
|
out_var_names = []
|
|
for op_idx in reversed(group):
|
|
op = block.ops[op_idx]
|
|
in_var_name = op.input("X")[0]
|
|
out_var_name = op.output("Out")[0]
|
|
in_var = block._find_var_recursive(in_var_name)
|
|
mem_size = int(np.prod(in_var.shape)) * sizeof
|
|
if cur_mem_size + mem_size > max_memory_size:
|
|
if len(recorded_op_indices) > 1:
|
|
attrs[op_role_var_key] = op_role_vars
|
|
coalesce_op_kwargs = insert_fuse_all_reduce_ops(
|
|
block,
|
|
recorded_op_indices,
|
|
in_var_names,
|
|
out_var_names,
|
|
dtype,
|
|
attrs,
|
|
)
|
|
coalesce_ops_kwargs.append(coalesce_op_kwargs)
|
|
|
|
cur_mem_size = 0
|
|
op_role_vars = []
|
|
recorded_op_indices = []
|
|
in_var_names = []
|
|
out_var_names = []
|
|
|
|
cur_mem_size += mem_size
|
|
recorded_op_indices.append(op_idx)
|
|
in_var_names.append(in_var_name)
|
|
out_var_names.append(out_var_name)
|
|
if op.has_attr(op_role_var_key):
|
|
op_role_vars.extend(op.attr(op_role_var_key))
|
|
|
|
if len(recorded_op_indices) > 1:
|
|
attrs[op_role_var_key] = op_role_vars
|
|
coalesce_op_kwargs = insert_fuse_all_reduce_ops(
|
|
block,
|
|
recorded_op_indices,
|
|
in_var_names,
|
|
out_var_names,
|
|
dtype,
|
|
attrs,
|
|
)
|
|
coalesce_ops_kwargs.append(coalesce_op_kwargs)
|
|
block._sync_with_cpp()
|
|
insert_coalesce_tensor_ops(block, coalesce_ops_kwargs)
|
|
|
|
|
|
@register_pass("fuse_all_reduce")
|
|
class FuseAllReducePass(PassBase):
|
|
def __init__(self):
|
|
super().__init__()
|
|
self.set_attr("max_memory_size", -1)
|
|
|
|
def _check_self(self):
|
|
max_memory_size = self.get_attr("max_memory_size")
|
|
return max_memory_size > 0
|
|
|
|
def _check_conflict(self, other_pass):
|
|
return True
|
|
|
|
def _type(self):
|
|
return PassType.COMM_OPT
|
|
|
|
# NOTE: why FuseAllReducePass can override apply_single_impl instead of
|
|
# apply_impl? AllReduce is a collective operation, so the program of each
|
|
# rank inside the same communication group should have the same
|
|
# all_reduce sum operations. Therefore, FuseAllReducePass can override
|
|
# apply_single_impl directly.
|
|
def _apply_single_impl(self, main_program, startup_program, context):
|
|
max_memory_size = self.get_attr("max_memory_size")
|
|
op_deps = main_program.desc.get_op_deps()
|
|
num_blocks = main_program.num_blocks
|
|
for i in range(num_blocks):
|
|
block = main_program.block(i)
|
|
groups = find_all_fuse_all_reduce_groups(block)
|
|
groups = split_fuse_all_reduce_groups_by_deps(
|
|
block, groups, op_deps[i]
|
|
)
|
|
insert_fuse_all_reduce_by_memory_size(
|
|
block, groups, max_memory_size
|
|
)
|
|
main_program._sync_with_cpp()
|