chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
# 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.
|
||||
@@ -0,0 +1,532 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import collections
|
||||
import copy
|
||||
import os
|
||||
import pickle
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.base import core
|
||||
from paddle.base.framework import Program
|
||||
from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
from paddle.distributed.auto_parallel.static.dist_context import (
|
||||
get_default_distributed_context,
|
||||
)
|
||||
from paddle.distributed.auto_parallel.static.utils import (
|
||||
is_backward_op,
|
||||
is_forward_op,
|
||||
is_loss_op,
|
||||
)
|
||||
from paddle.static.io import deserialize_program
|
||||
|
||||
_valid_types = [
|
||||
core.VarDesc.VarType.DENSE_TENSOR,
|
||||
core.VarDesc.VarType.SELECTED_ROWS,
|
||||
core.VarDesc.VarType.DENSE_TENSOR_ARRAY,
|
||||
]
|
||||
|
||||
paddle.enable_static()
|
||||
|
||||
|
||||
class AutoAlignTool:
|
||||
"""
|
||||
This is an automatic parallel precision alignment tool。
|
||||
"""
|
||||
|
||||
def __init__(self, program: Program, step=1, fetch_list=None):
|
||||
"""Set some initialization information of the tool.
|
||||
step: Step when returning a specific variable name。
|
||||
fetch_list: initialization fetch_list.When a specific step is not reached, return this.
|
||||
It can combine with Engine class。
|
||||
example:in Engine.fit function,like this
|
||||
try:
|
||||
fetch_list = []
|
||||
align_tool = AutoAlignTool(self.main_program, 0, fetch_names)
|
||||
level = 0
|
||||
fetch_list = align_tool.get_var(level, step)
|
||||
outs = self._executor.run(
|
||||
self.main_program,
|
||||
fetch_list=fetch_list,
|
||||
use_program_cache=self._strategy.use_cache,
|
||||
return_numpy=self._strategy.return_numpy,
|
||||
)
|
||||
if fetch_list != fetch_names:
|
||||
align_tool.save(dir_path, outs, fetch_list, self._dist_contexts["train"], self.serial)
|
||||
exit(0)
|
||||
except core.EOFException:
|
||||
break
|
||||
"""
|
||||
assert isinstance(program, Program)
|
||||
self._program = program
|
||||
self._blocks = program.blocks
|
||||
self._step = step
|
||||
self._fetch_list = fetch_list
|
||||
assert self._blocks is not None
|
||||
|
||||
def set_step(self, step):
|
||||
self._step = step
|
||||
|
||||
def get_var(self, level, step):
|
||||
"""
|
||||
level must be in [0,1,2,3,4,5].
|
||||
"""
|
||||
if step != self._step or step == -1:
|
||||
return self._fetch_list
|
||||
if level == 0:
|
||||
return self.get_loss_lr_var()
|
||||
elif level == 1:
|
||||
return self.get_data_var()
|
||||
elif level == 2:
|
||||
return self.get_param_var()
|
||||
elif level == 3:
|
||||
return self.get_param_grad_var()
|
||||
elif level == 4:
|
||||
return self.get_forward_tmp_var()
|
||||
elif level == 5:
|
||||
return self.get_backward_tmp_var()
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
def set_program(self, program: Program):
|
||||
assert isinstance(program, Program)
|
||||
self._program = program
|
||||
self._blocks = program.blocks
|
||||
assert self._blocks is not None
|
||||
|
||||
def get_loss_lr_var(self):
|
||||
"""
|
||||
Returns the variable name of learning rate and loss
|
||||
"""
|
||||
fetch_set = set()
|
||||
loss_ops = []
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_loss_op(op):
|
||||
assert len(op.desc.output_arg_names()) == 1, (
|
||||
"loss op should only output loss var"
|
||||
)
|
||||
loss_ops.append(op)
|
||||
|
||||
for block in self._blocks:
|
||||
for varname in block.vars:
|
||||
var = block._find_var_recursive(varname)
|
||||
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
|
||||
if "learning_rate" in var.name:
|
||||
fetch_set.add(var.name)
|
||||
|
||||
for loss_op in loss_ops:
|
||||
fetch_set.add(loss_op.output_arg_names[0])
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def get_data_var(self):
|
||||
"""
|
||||
Returns the variable name of data.
|
||||
"""
|
||||
fetch_set = set()
|
||||
for block in self._blocks:
|
||||
for varname in block.vars:
|
||||
var = block._find_var_recursive(varname)
|
||||
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
|
||||
if var.is_data:
|
||||
fetch_set.add(var.name)
|
||||
return list(fetch_set)
|
||||
|
||||
def get_param_var(self):
|
||||
"""
|
||||
Returns the variable name of parameters.
|
||||
"""
|
||||
fetch_set = set()
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_backward_op(op):
|
||||
break
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
var = block._find_var_recursive(varname)
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
if var.is_parameter:
|
||||
fetch_set.add(varname)
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def get_param_grad_var(self):
|
||||
"""
|
||||
Returns the variable name of parameters' gradient.
|
||||
"""
|
||||
fetch_set = set()
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_forward_op(op):
|
||||
continue
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if "@GRAD" not in varname:
|
||||
continue
|
||||
fwd_varname = varname.split("@GRAD")[0]
|
||||
fwd_var = block._find_var_recursive(fwd_varname)
|
||||
if fwd_var is None or fwd_var.type not in _valid_types:
|
||||
continue
|
||||
if fwd_var.is_parameter is False:
|
||||
continue
|
||||
var = block._find_var_recursive(varname)
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
fetch_set.add(varname)
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def get_forward_tmp_var(self):
|
||||
"""
|
||||
Returns the name of the temporary variable in the forward propagation
|
||||
"""
|
||||
fetch_set = set()
|
||||
loss_lr_list = self.get_loss_lr_var()
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_backward_op(op):
|
||||
break
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if varname in loss_lr_list:
|
||||
continue
|
||||
var = block._find_var_recursive(varname)
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
if var.is_data or var.is_parameter:
|
||||
continue
|
||||
fetch_set.add(varname)
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def get_backward_tmp_var(self):
|
||||
"""
|
||||
Returns the name of a temporary variable in back-propagation
|
||||
"""
|
||||
fetch_set = set()
|
||||
loss_lr_list = self.get_loss_lr_var()
|
||||
forward_tmp_list = self.get_forward_tmp_var()
|
||||
for block in self._blocks:
|
||||
for op in block.ops:
|
||||
if is_backward_op(op):
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if (
|
||||
varname in loss_lr_list
|
||||
or varname in forward_tmp_list
|
||||
):
|
||||
continue
|
||||
if "@GRAD" in varname:
|
||||
fwd_varname = varname.split("@GRAD")[0]
|
||||
fwd_var = block._find_var_recursive(fwd_varname)
|
||||
if (
|
||||
fwd_var is not None
|
||||
and fwd_var.type in _valid_types
|
||||
):
|
||||
if fwd_var.is_parameter:
|
||||
continue
|
||||
var = block._find_var_recursive(varname)
|
||||
if var is None or var.type not in _valid_types:
|
||||
continue
|
||||
if var.is_data or var.is_parameter:
|
||||
continue
|
||||
fetch_set.add(varname)
|
||||
|
||||
return list(fetch_set)
|
||||
|
||||
def save(self, save_dir, vars, fetch_list, dist_context=None):
|
||||
"""
|
||||
save fetch variables, distributed properties of variables and program.
|
||||
"""
|
||||
if os.path.exists(save_dir) is False:
|
||||
os.mkdir(save_dir)
|
||||
if dist_context is None:
|
||||
dist_context = get_default_distributed_context()
|
||||
assert os.path.exists(save_dir)
|
||||
if dist.get_world_size() == 1:
|
||||
vars_path = os.path.join(save_dir, "vars.pkl")
|
||||
program_path = os.path.join(save_dir, "program.pdmodel")
|
||||
dist_attr_path = os.path.join(save_dir, "dist_attr.pkl")
|
||||
else:
|
||||
vars_path = os.path.join(
|
||||
save_dir, f"vars_rank{dist.get_rank()}.pkl"
|
||||
)
|
||||
program_path = os.path.join(
|
||||
save_dir, f"program_rank{dist.get_rank()}.pdmodel"
|
||||
)
|
||||
dist_attr_path = os.path.join(
|
||||
save_dir, f"dist_attr_rank{dist.get_rank()}.pkl"
|
||||
)
|
||||
if vars is not None:
|
||||
vars_dict = {}
|
||||
assert len(fetch_list) == len(vars)
|
||||
for i in range(len(fetch_list)):
|
||||
if vars[i] is None:
|
||||
continue
|
||||
vars_dict[fetch_list[i]] = vars[i]
|
||||
with open(vars_path, "wb") as f:
|
||||
pickle.dump(vars_dict, f)
|
||||
dist_attr = {}
|
||||
for var in self._program.list_vars():
|
||||
if var.name not in fetch_list:
|
||||
continue
|
||||
tensor_dist_attr = (
|
||||
dist_context.get_tensor_dist_attr_for_program(var)
|
||||
)
|
||||
if tensor_dist_attr is None:
|
||||
continue
|
||||
process_mesh = tensor_dist_attr.process_mesh
|
||||
dims_mapping = tensor_dist_attr.dims_mapping
|
||||
dist_attr[var.name] = {
|
||||
"process_shape": process_mesh.shape,
|
||||
"process_group": process_mesh.process_ids,
|
||||
"dims_mapping": dims_mapping,
|
||||
}
|
||||
if len(dist_attr) > 0:
|
||||
with open(dist_attr_path, "wb") as f:
|
||||
pickle.dump(dist_attr, f)
|
||||
if self._program is not None:
|
||||
with open(program_path, "wb") as f:
|
||||
f.write(self._program.desc.serialize_to_string())
|
||||
|
||||
@staticmethod
|
||||
def load(save_dir):
|
||||
assert os.path.exists(save_dir)
|
||||
filename_list = sorted(os.listdir(save_dir))
|
||||
vars_list = []
|
||||
program_list = []
|
||||
dist_attr_list = []
|
||||
for filename in filename_list:
|
||||
filepath = os.path.join(save_dir, filename)
|
||||
assert os.path.isfile(filepath)
|
||||
if "vars" in filename:
|
||||
assert filename.endswith("pkl")
|
||||
with open(filepath, "rb") as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
vars_list.append(safe_load_pickle(f))
|
||||
elif "program" in filename:
|
||||
assert filename.endswith("pdmodel")
|
||||
with open(filepath, "rb") as f:
|
||||
program_string = f.read()
|
||||
program_list.append(deserialize_program(program_string))
|
||||
elif "dist_attr" in filename:
|
||||
assert filename.endswith("pkl")
|
||||
with open(filepath, "rb") as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
dist_attr_list.append(safe_load_pickle(f))
|
||||
|
||||
dist_attr_map = {}
|
||||
for dist_attrs in dist_attr_list:
|
||||
for dist_attr_name in dist_attrs.keys():
|
||||
if dist_attr_name not in dist_attr_map:
|
||||
dist_attr_map[dist_attr_name] = dist_attrs[dist_attr_name]
|
||||
assert len(vars_list) == len(program_list)
|
||||
return vars_list, program_list, dist_attr_map
|
||||
|
||||
@staticmethod
|
||||
def convert_src_tensor_2_dst_tensor(vars_list, src_attr_map, dst_attr_map):
|
||||
"""
|
||||
Converter is a class object for auto parallel to convert tensors from
|
||||
one parallel strategy to another one. Tensors will merge and slice value
|
||||
with their strategy when strategies are different.
|
||||
But like dp to pp or dp to serial is not supported.
|
||||
"""
|
||||
assert len(vars_list) >= 1
|
||||
# if dist_attr_map is None or len(dist_attr_map) == 0 or len(vars_list) == 1:
|
||||
if src_attr_map is None or len(src_attr_map) == 0:
|
||||
return vars_list[0]
|
||||
|
||||
dst_strategies = {}
|
||||
src_strategies = {}
|
||||
tensors_dict = {}
|
||||
|
||||
convert_tensor_dict = None
|
||||
for var_name in src_attr_map.keys():
|
||||
assert var_name not in dst_strategies
|
||||
dist_vars = []
|
||||
for vars in vars_list:
|
||||
if var_name in vars.keys():
|
||||
dist_vars.append(vars[var_name])
|
||||
if len(dist_vars) == 0:
|
||||
continue
|
||||
|
||||
if var_name in dst_attr_map and var_name in src_attr_map:
|
||||
dst_strategies[var_name] = copy.deepcopy(dst_attr_map[var_name])
|
||||
src_strategies[var_name] = copy.deepcopy(src_attr_map[var_name])
|
||||
tensors_dict[var_name] = dist_vars
|
||||
|
||||
if src_attr_map == dst_attr_map:
|
||||
return tensors_dict
|
||||
converter = Converter(tensors_dict, src_strategies, dst_strategies)
|
||||
convert_tensor_dict = converter.convert()
|
||||
|
||||
return convert_tensor_dict
|
||||
|
||||
@staticmethod
|
||||
def find_diff_vars(fixed_vars_map, query_vars_map):
|
||||
"""
|
||||
Found two variable names with different variable lists
|
||||
"""
|
||||
diff_var_name_list = set()
|
||||
for var_name in fixed_vars_map.keys():
|
||||
if var_name in query_vars_map:
|
||||
fixed_vars = fixed_vars_map[var_name]
|
||||
query_vars = query_vars_map[var_name]
|
||||
if isinstance(fixed_vars, np.ndarray):
|
||||
fixed_vars = [fixed_vars]
|
||||
if isinstance(query_vars, np.ndarray):
|
||||
query_vars = [query_vars]
|
||||
|
||||
length = min(len(fixed_vars), len(query_vars))
|
||||
if len(fixed_vars) != len(query_vars):
|
||||
print()
|
||||
for i in range(length):
|
||||
if not np.allclose(fixed_vars[i], query_vars[i]):
|
||||
diff_var_name_list.add(var_name)
|
||||
return diff_var_name_list
|
||||
|
||||
@staticmethod
|
||||
def diff_information(right_dir, wrong_dir):
|
||||
"""
|
||||
Find the corresponding operator according to the variable name.
|
||||
"""
|
||||
(
|
||||
right_vars_list,
|
||||
right_program_list,
|
||||
right_dist_attr_map,
|
||||
) = AutoAlignTool.load(right_dir)
|
||||
(
|
||||
wrong_vars_list,
|
||||
wrong_program_list,
|
||||
wrong_dist_attr_map,
|
||||
) = AutoAlignTool.load(wrong_dir)
|
||||
right_tensors_dict = AutoAlignTool.convert_src_tensor_2_dst_tensor(
|
||||
right_vars_list, right_dist_attr_map, right_dist_attr_map
|
||||
)
|
||||
wrong_tensors_dict = AutoAlignTool.convert_src_tensor_2_dst_tensor(
|
||||
wrong_vars_list, wrong_dist_attr_map, right_dist_attr_map
|
||||
)
|
||||
|
||||
diff_var_name_list = AutoAlignTool.find_diff_vars(
|
||||
right_tensors_dict, wrong_tensors_dict
|
||||
)
|
||||
|
||||
diff_ops_varname_dict = collections.OrderedDict()
|
||||
|
||||
for program in wrong_program_list:
|
||||
for block in program.blocks:
|
||||
for op in block.ops:
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if varname in diff_var_name_list:
|
||||
if len(diff_ops_varname_dict) == 0:
|
||||
print(
|
||||
"first different op:\n",
|
||||
op,
|
||||
f"\ndifferent varname is:{varname}",
|
||||
)
|
||||
if op not in diff_ops_varname_dict:
|
||||
diff_ops_varname_dict[op] = [varname]
|
||||
else:
|
||||
diff_ops_varname_dict[op].append(varname)
|
||||
|
||||
return diff_ops_varname_dict
|
||||
|
||||
@staticmethod
|
||||
def diff_information_from_dirs(right_dirs, wrong_dirs):
|
||||
right_vars_list = []
|
||||
right_program_list = []
|
||||
right_dist_attr_map = {}
|
||||
for right_dir in right_dirs:
|
||||
(
|
||||
tmp_vars_list,
|
||||
right_program_list,
|
||||
tmp_dist_attr_map,
|
||||
) = AutoAlignTool.load(right_dir)
|
||||
if len(right_vars_list) == 0:
|
||||
right_vars_list = tmp_vars_list
|
||||
else:
|
||||
for i in range(len(tmp_vars_list)):
|
||||
vars_list = tmp_vars_list[i]
|
||||
for key in vars_list.keys():
|
||||
if key not in right_vars_list[i].keys():
|
||||
right_vars_list[i][key] = vars_list[key]
|
||||
|
||||
for key in tmp_dist_attr_map.keys():
|
||||
if key not in right_dist_attr_map:
|
||||
right_dist_attr_map[key] = tmp_dist_attr_map[key]
|
||||
|
||||
wrong_vars_list = []
|
||||
wrong_program_list = []
|
||||
wrong_dist_attr_map = {}
|
||||
for wrong_dir in wrong_dirs:
|
||||
(
|
||||
tmp_vars_list,
|
||||
wrong_program_list,
|
||||
tmp_dist_attr_map,
|
||||
) = AutoAlignTool.load(wrong_dir)
|
||||
if len(wrong_vars_list) == 0:
|
||||
wrong_vars_list = tmp_vars_list
|
||||
else:
|
||||
for i in range(len(tmp_vars_list)):
|
||||
vars_list = tmp_vars_list[i]
|
||||
for key in vars_list.keys():
|
||||
if key not in wrong_vars_list[i].keys():
|
||||
wrong_vars_list[i][key] = vars_list[key]
|
||||
|
||||
for key in tmp_dist_attr_map.keys():
|
||||
if key not in wrong_dist_attr_map:
|
||||
wrong_dist_attr_map[key] = tmp_dist_attr_map[key]
|
||||
|
||||
right_tensors_dict = AutoAlignTool.convert_src_tensor_2_dst_tensor(
|
||||
right_vars_list, right_dist_attr_map, right_dist_attr_map
|
||||
)
|
||||
wrong_tensors_dict = AutoAlignTool.convert_src_tensor_2_dst_tensor(
|
||||
wrong_vars_list, wrong_dist_attr_map, right_dist_attr_map
|
||||
)
|
||||
diff_var_name_list = AutoAlignTool.find_diff_vars(
|
||||
right_tensors_dict, wrong_tensors_dict
|
||||
)
|
||||
|
||||
diff_ops_varname_dict = collections.OrderedDict()
|
||||
|
||||
for program in wrong_program_list:
|
||||
for block in program.blocks:
|
||||
for op in block.ops:
|
||||
for varname in op.input_arg_names + op.output_arg_names:
|
||||
if varname in diff_var_name_list:
|
||||
if len(diff_ops_varname_dict) == 0:
|
||||
print(
|
||||
"first different op:\n",
|
||||
op,
|
||||
f"\ndifferent varname is:{varname}",
|
||||
)
|
||||
if op not in diff_ops_varname_dict:
|
||||
diff_ops_varname_dict[op] = [varname]
|
||||
else:
|
||||
diff_ops_varname_dict[op].append(varname)
|
||||
|
||||
return diff_ops_varname_dict
|
||||
@@ -0,0 +1,244 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import os
|
||||
import time
|
||||
|
||||
import paddle
|
||||
from paddle.hapi.callbacks import (
|
||||
Callback,
|
||||
CallbackList,
|
||||
LRScheduler,
|
||||
ModelCheckpoint,
|
||||
ProgBarLogger,
|
||||
)
|
||||
|
||||
from ..interface import CollectionNames, get_collection
|
||||
|
||||
|
||||
def config_callbacks(
|
||||
callbacks=None,
|
||||
engine=None,
|
||||
batch_size=None,
|
||||
epochs=None,
|
||||
steps=None,
|
||||
log_freq=2,
|
||||
verbose=2,
|
||||
save_freq=1,
|
||||
save_dir=None,
|
||||
metrics=None,
|
||||
acc_step=1,
|
||||
mode='train',
|
||||
):
|
||||
cbks = callbacks or []
|
||||
cbks = cbks if isinstance(cbks, (list, tuple)) else [cbks]
|
||||
|
||||
if not any(isinstance(k, ProgBarLogger) for k in cbks) and verbose:
|
||||
cbks = [ProgBarLoggerAuto(log_freq, verbose=verbose), *cbks]
|
||||
|
||||
if not any(isinstance(k, LRScheduler) for k in cbks):
|
||||
cbks = [LRSchedulerAuto(), *cbks]
|
||||
|
||||
if not any(isinstance(k, ModelCheckpoint) for k in cbks):
|
||||
cbks = [*cbks, ModelCheckpointAuto(save_freq, save_dir)]
|
||||
|
||||
if not any(isinstance(k, Profiler) for k in cbks) and verbose == 3:
|
||||
cbks = [*cbks, Profiler(timer_only=True)]
|
||||
|
||||
if not any(isinstance(k, History) for k in cbks):
|
||||
cbks = [*cbks, History()]
|
||||
|
||||
for i, k in enumerate(cbks):
|
||||
if isinstance(k, ProgBarLogger):
|
||||
cbks[i] = ProgBarLoggerAuto(k.log_freq, k.verbose)
|
||||
if isinstance(k, LRScheduler):
|
||||
cbks[i] = LRSchedulerAuto(k.by_step, k.by_epoch)
|
||||
if isinstance(k, ModelCheckpoint):
|
||||
cbks[i] = ModelCheckpointAuto(k.save_freq, k.save_dir)
|
||||
|
||||
cbk_list = CallbackList(cbks)
|
||||
cbk_list.set_model(engine)
|
||||
metrics = metrics or [] if mode != 'test' else []
|
||||
params = {
|
||||
'batch_size': batch_size,
|
||||
'epochs': epochs,
|
||||
'steps': steps,
|
||||
'verbose': verbose,
|
||||
'metrics': metrics,
|
||||
'acc_step': acc_step,
|
||||
}
|
||||
cbk_list.set_params(params)
|
||||
return cbk_list
|
||||
|
||||
|
||||
class ProgBarLoggerAuto(ProgBarLogger):
|
||||
def __init__(self, log_freq=1, verbose=2):
|
||||
super().__init__(log_freq, verbose)
|
||||
|
||||
def _is_print(self):
|
||||
return True
|
||||
|
||||
def _updates(self, logs, mode):
|
||||
values = []
|
||||
metrics = getattr(self, f'{mode}_metrics')
|
||||
progbar = getattr(self, f'{mode}_progbar')
|
||||
steps = getattr(self, f'{mode}_step')
|
||||
|
||||
for k in metrics:
|
||||
if k in logs:
|
||||
values.append((k, logs[k]))
|
||||
|
||||
if 'lr' in logs:
|
||||
values.append(('lr', logs['lr']))
|
||||
|
||||
fetches_logs = logs.get('fetches', {})
|
||||
collect_logging = get_collection(CollectionNames.LOGGING)
|
||||
for name, var in collect_logging:
|
||||
k = name or var.name
|
||||
if k in fetches_logs:
|
||||
values.append((k, fetches_logs[k]))
|
||||
|
||||
out_logs = logs.get('outputs', {})
|
||||
for k in out_logs:
|
||||
values.append((k, out_logs[k]))
|
||||
|
||||
if self.verbose == 3 and hasattr(self, f'_{mode}_timer'):
|
||||
timer = getattr(self, f'_{mode}_timer')
|
||||
cnt = timer['count'] if timer['count'] > 0 else 1.0
|
||||
samples = timer['samples'] if timer['samples'] > 0 else 1.0
|
||||
values.append(
|
||||
('avg_reader_cost', "%.5f sec" % (timer['data_time'] / cnt))
|
||||
)
|
||||
values.append(
|
||||
('avg_batch_cost', "%.5f sec" % (timer['batch_time'] / cnt))
|
||||
)
|
||||
values.append(
|
||||
(
|
||||
'ips',
|
||||
"%.5f samples/sec"
|
||||
% (samples / (timer['data_time'] + timer['batch_time'])),
|
||||
)
|
||||
)
|
||||
timer['count'] = 0
|
||||
timer['samples'] = 0
|
||||
timer['data_time'] = 0.0
|
||||
timer['batch_time'] = 0.0
|
||||
|
||||
progbar.update(steps, values)
|
||||
|
||||
def on_eval_batch_end(self, step, logs=None):
|
||||
logs = logs or {}
|
||||
self.eval_step += 1
|
||||
samples = self.params['batch_size']
|
||||
self.evaled_samples += samples
|
||||
|
||||
self._eval_timer['batch_time'] += (
|
||||
time.time() - self._eval_timer['batch_data_end_time']
|
||||
)
|
||||
self._eval_timer['count'] += 1
|
||||
samples = self.params['batch_size']
|
||||
self._eval_timer['samples'] += samples
|
||||
|
||||
if self._is_print() and self.eval_step % self.log_freq == 0:
|
||||
if self.eval_steps is None or self.eval_step < self.eval_steps:
|
||||
self._updates(logs, 'eval')
|
||||
|
||||
self._eval_timer['batch_start_time'] = time.time()
|
||||
|
||||
|
||||
class LRSchedulerAuto(LRScheduler):
|
||||
def __init__(self, by_step=True, by_epoch=False):
|
||||
super().__init__(by_step, by_epoch)
|
||||
|
||||
def on_epoch_begin(self, epoch=None, logs=None):
|
||||
self.acc_step = self.params["acc_step"]
|
||||
self.epoch = epoch
|
||||
self.train_step = 0
|
||||
|
||||
def on_train_batch_end(self, step, logs=None):
|
||||
self.train_step += 1
|
||||
|
||||
if self.by_step and self.train_step % self.acc_step == 0:
|
||||
if (
|
||||
self.model.optimizer
|
||||
and hasattr(self.model.optimizer, '_learning_rate')
|
||||
and isinstance(
|
||||
self.model.optimizer._learning_rate,
|
||||
paddle.optimizer.lr.LRScheduler,
|
||||
)
|
||||
):
|
||||
self.model.optimizer._learning_rate.step()
|
||||
|
||||
|
||||
class History(Callback):
|
||||
def __init__(self):
|
||||
self.history = {}
|
||||
|
||||
def on_train_begin(self, logs=None):
|
||||
self.epoch = []
|
||||
|
||||
def on_epoch_end(self, epoch, logs=None):
|
||||
logs = logs or {}
|
||||
self.epoch.append(epoch)
|
||||
for k, v in logs.items():
|
||||
self.history.setdefault(k, []).append(v)
|
||||
|
||||
self.model.history = self
|
||||
|
||||
|
||||
class Profiler(Callback):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.prof = paddle.profiler.Profiler(*args, **kwargs)
|
||||
|
||||
def on_epoch_begin(self, epoch=None, logs=None):
|
||||
self.epoch = epoch
|
||||
self.train_step = 0
|
||||
self.batch_size = self.params["batch_size"]
|
||||
self.steps = self.params['steps']
|
||||
|
||||
def on_train_begin(self, logs=None):
|
||||
self.prof.start()
|
||||
|
||||
def on_train_batch_end(self, step, logs=None):
|
||||
self.train_step += 1
|
||||
self.prof.step(num_samples=self.batch_size)
|
||||
print(
|
||||
"step {}:{}".format(
|
||||
self.train_step, self.prof.step_info(unit='samples')
|
||||
)
|
||||
)
|
||||
|
||||
def on_train_end(self, logs=None):
|
||||
self.prof.stop()
|
||||
self.prof.summary()
|
||||
|
||||
|
||||
class ModelCheckpointAuto(ModelCheckpoint):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def _is_save(self):
|
||||
return self.model and self.save_dir
|
||||
|
||||
def on_epoch_end(self, epoch, logs=None):
|
||||
if self._is_save() and (self.epoch + 1) % self.save_freq == 0:
|
||||
path = f'{self.save_dir}/epoch{epoch}'
|
||||
print(f'save checkpoint at {os.path.abspath(path)}')
|
||||
self.model.save(path)
|
||||
|
||||
def on_train_end(self, logs=None):
|
||||
if self._is_save():
|
||||
path = f'{self.save_dir}/final'
|
||||
print(f'save checkpoint at {os.path.abspath(path)}')
|
||||
self.model.save(path)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,129 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from enum import IntEnum, unique
|
||||
|
||||
import numpy as np
|
||||
|
||||
from paddle.framework import core
|
||||
|
||||
|
||||
@unique
|
||||
class DeviceType(IntEnum):
|
||||
UNKNOWN = 0
|
||||
CPU = 1
|
||||
GPU = 2
|
||||
XPU = 3
|
||||
DCU = 5
|
||||
NIC = 6
|
||||
|
||||
|
||||
@unique
|
||||
class LinkType(IntEnum):
|
||||
UNKNOWN = 0
|
||||
LOC = 1
|
||||
SYS = 2
|
||||
PHB = 3
|
||||
PIX = 4
|
||||
PIB = 5
|
||||
NVL = 6
|
||||
NVB = 7
|
||||
NET = 8
|
||||
|
||||
|
||||
class DeviceMesh(core.DeviceMesh):
|
||||
r"""
|
||||
The class `DeviceMesh` describes the topology of physical devices.
|
||||
|
||||
Args:
|
||||
mesh (list|numpy.array): an N-dimensional array describes the topology
|
||||
of logical processes.
|
||||
dim_names (list, optional): the i-th element of this list gives the name of the
|
||||
i-th dimension.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
|
||||
>>> paddle.enable_static()
|
||||
|
||||
>>> mesh = dist.DeviceMesh([[2, 4, 5], [0, 1, 3]])
|
||||
>>> assert mesh.shape == [2, 3]
|
||||
>>> assert mesh.device_ids == [2, 4, 5, 0, 1, 3]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, name, mesh, dim_names=None):
|
||||
self._name = name
|
||||
|
||||
if not isinstance(mesh, list) and not isinstance(mesh, np.ndarray):
|
||||
raise ValueError(
|
||||
'The mesh must be an instance of list or np.ndarray.'
|
||||
)
|
||||
if isinstance(mesh, list):
|
||||
mesh = np.array(mesh)
|
||||
|
||||
self._mesh = mesh
|
||||
|
||||
self._shape = list(self._mesh.shape)
|
||||
|
||||
self._device_ids = self._mesh.flatten().tolist()
|
||||
assert all(isinstance(p, int) for p in self._device_ids), (
|
||||
"All elements of the mesh be integer"
|
||||
)
|
||||
assert min(self._device_ids) >= 0, (
|
||||
'All elements of the mesh must be >= 0.'
|
||||
)
|
||||
unique_device_ids = set(self._device_ids)
|
||||
assert len(unique_device_ids) == len(self._device_ids), (
|
||||
'All elements of the mesh must be unique.'
|
||||
)
|
||||
|
||||
if dim_names is not None:
|
||||
assert len(dim_names) == len(self._shape), (
|
||||
"The length of dims_names must be same as the shape of the mesh."
|
||||
)
|
||||
self._dim_names = dim_names
|
||||
else:
|
||||
self._dim_names = ["d" + str(i) for i in range(len(self._shape))]
|
||||
|
||||
# Follow the requirement for using pybind11
|
||||
core.DeviceMesh.__init__(
|
||||
self, self._name, self._shape, self._device_ids, self._dim_names
|
||||
)
|
||||
|
||||
@property
|
||||
def mesh(self):
|
||||
return self._mesh
|
||||
|
||||
|
||||
# class Cluster:
|
||||
# """
|
||||
# The cluster represents the hardware resource.
|
||||
# """
|
||||
|
||||
# def __init__(self):
|
||||
# self._device_meshes = {}
|
||||
|
||||
# def device_mesh(self, device_mesh_name):
|
||||
# return self._device_meshes[device_mesh_name]
|
||||
|
||||
# def add_device_mesh(self, device_mesh):
|
||||
# self._device_meshes[device_mesh.name] = device_mesh
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,543 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
|
||||
|
||||
class Converter:
|
||||
"""
|
||||
Converter is a class object for auto parallel to convert tensors from
|
||||
one parallel strategy to another one. Tensors will merge and slice value
|
||||
with their strategy when strategies are different.
|
||||
"""
|
||||
|
||||
def __init__(self, tensors_dict, pre_strategy, cur_strategy):
|
||||
"""
|
||||
Args:
|
||||
tensors_dict(dict): tensors' value of all ranks that to be converted.
|
||||
key is tensor's name(str), value is all ranks' data(list(numpy.ndarray))
|
||||
pre_strategy(dict): tensors' distributed attribute of last training process.
|
||||
key is tensor's name(str), value is tensor's distributed attribute in last
|
||||
training process.
|
||||
cur_strategy(dict): tensors' distributed attribute of current rank.
|
||||
key is tensor's name(str), value is tensor's distributed attribute in current
|
||||
rank.
|
||||
"""
|
||||
self._tensors_dict = self._check_tensor_dict(tensors_dict)
|
||||
self._pre_strategy = self._check_pre_strategy(pre_strategy)
|
||||
self._cur_strategy = self._check_cur_strategy(cur_strategy)
|
||||
self._logger = get_logger(logging.INFO)
|
||||
|
||||
def _check_tensor_dict(self, tensors_dict):
|
||||
if not tensors_dict:
|
||||
raise ValueError(
|
||||
"'tensors_dict' is None, "
|
||||
"the tensors to be converted cannot be None."
|
||||
)
|
||||
if not isinstance(tensors_dict, dict):
|
||||
raise TypeError(
|
||||
f"The type of 'tensors_dict' should be 'dict', but got '{type(tensors_dict)}'."
|
||||
)
|
||||
return tensors_dict
|
||||
|
||||
def _check_pre_strategy(self, pre_strategy):
|
||||
if not pre_strategy:
|
||||
raise ValueError(
|
||||
"'pre_strategy' is None, there are not tensors in pre process."
|
||||
)
|
||||
if not isinstance(pre_strategy, dict):
|
||||
raise TypeError(
|
||||
"The type of 'pre_strategy' should be 'dict', "
|
||||
f"but got '{type(pre_strategy)}'."
|
||||
)
|
||||
return pre_strategy
|
||||
|
||||
def _check_cur_strategy(self, cur_strategy):
|
||||
if not cur_strategy:
|
||||
warnings.warn(
|
||||
"'cur_strategy' is None, there are not tensors in cur process"
|
||||
)
|
||||
if not isinstance(cur_strategy, dict):
|
||||
raise TypeError(
|
||||
"The type of 'cur_strategy' should be 'dict', "
|
||||
f"but got '{type(cur_strategy)}'."
|
||||
)
|
||||
return cur_strategy
|
||||
|
||||
def convert(self, strict=True):
|
||||
"""
|
||||
Convert tensors
|
||||
|
||||
Args:
|
||||
strict(bool): whether to strict convert tensor with tensor's name. If False, it will
|
||||
convert tensors by prefix matching. Otherwise, tensors will be converted with
|
||||
their name strictly.
|
||||
|
||||
Returns:
|
||||
converted tensors(dict)
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
>>> complete_tensors = np.arange(4).reshape([2, 2])
|
||||
>>> partial_tensors = np.split(complete_tensors, 2, axis=0)
|
||||
>>> name = "tmp_0"
|
||||
>>> tensors_dict = {name: partial_tensors}
|
||||
>>> strategy_1 = {
|
||||
... name: {
|
||||
... "process_shape": [2],
|
||||
... "process_group": [0, 1],
|
||||
... "dims_mapping": [0, -1],
|
||||
... },
|
||||
... }
|
||||
>>> strategy_2 = {
|
||||
... name: {
|
||||
... "process_shape": [2],
|
||||
... "process_group": [0, 1],
|
||||
... "dims_mapping": [-1, -1],
|
||||
... },
|
||||
... }
|
||||
>>> converter = Converter(tensors_dict, strategy_1, strategy_2)
|
||||
>>> result = converter.convert()
|
||||
>>> # the result's value is equal to `complete_tensors`
|
||||
"""
|
||||
tensors_dict = {}
|
||||
# the name which is in cur_process but not in pre_process
|
||||
tensor_not_in_pre = []
|
||||
# the name which is in pre_process but not in cur_process
|
||||
tensor_not_in_cur = []
|
||||
# the name which is in strategy but not in ckpt files
|
||||
tensor_not_in_ckpt = []
|
||||
self._logger.info("Start to convert tensors.")
|
||||
for tensor_name in self._cur_strategy:
|
||||
if tensor_name not in self._pre_strategy:
|
||||
tensor_not_in_pre.append(tensor_name)
|
||||
continue
|
||||
if tensor_name not in self._tensors_dict:
|
||||
tensor_not_in_ckpt.append(tensor_name)
|
||||
continue
|
||||
self._pre_name = tensor_name
|
||||
self._cur_name = tensor_name
|
||||
tensor_list = self._tensors_dict[tensor_name]
|
||||
pre_dist_attr = self._pre_strategy[tensor_name]
|
||||
cur_dist_attr = self._cur_strategy[tensor_name]
|
||||
try:
|
||||
tensors_dict[tensor_name] = Converter.merge_and_slice(
|
||||
tensor_list, pre_dist_attr, cur_dist_attr
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ValueError(
|
||||
f"Fail to convert tensor '{tensor_name}'. {err}"
|
||||
)
|
||||
|
||||
for tensor_name in self._pre_strategy:
|
||||
if tensor_name not in self._cur_strategy:
|
||||
tensor_not_in_cur.append(tensor_name)
|
||||
|
||||
if not strict:
|
||||
(
|
||||
tensors_dict,
|
||||
tensor_match_with_pre,
|
||||
tensor_match_with_cur,
|
||||
) = self.convert_with_prefix_match(
|
||||
tensors_dict, tensor_not_in_pre, tensor_not_in_cur
|
||||
)
|
||||
else:
|
||||
tensors_dict, tensor_match_with_pre, tensor_match_with_cur = (
|
||||
tensors_dict,
|
||||
[],
|
||||
[],
|
||||
)
|
||||
|
||||
tensor_not_in_pre = set(tensor_not_in_pre) - set(tensor_match_with_pre)
|
||||
tensor_not_in_cur = set(tensor_not_in_cur) - set(tensor_match_with_cur)
|
||||
if tensor_not_in_pre:
|
||||
warnings.warn(
|
||||
f"tensors [{tensor_not_in_pre}] are not found in last training strategy."
|
||||
)
|
||||
if tensor_not_in_cur:
|
||||
warnings.warn(
|
||||
f"tensors [{tensor_not_in_cur}] are not found in current training strategy."
|
||||
)
|
||||
if tensor_not_in_ckpt:
|
||||
warnings.warn(
|
||||
f"tensors [{tensor_not_in_ckpt}] are found in pre_strategy, but are not found"
|
||||
"in checkpoint files, please check your checkpoint files."
|
||||
)
|
||||
|
||||
return tensors_dict
|
||||
|
||||
def convert_with_prefix_match(
|
||||
self, tensors_dict, tensor_not_in_pre, tensor_not_in_cur
|
||||
):
|
||||
# the name which in cur_process and can match with pre_process
|
||||
tensor_match_with_pre = []
|
||||
# the name which in pre_process and can match with cur_process
|
||||
tensor_match_with_cur = []
|
||||
for cur_name in tensor_not_in_pre:
|
||||
prefix_name = cur_name
|
||||
while prefix_name.find("_") != -1:
|
||||
prefix_name = prefix_name[: prefix_name.rfind("_")]
|
||||
for pre_name in tensor_not_in_cur:
|
||||
if prefix_name in pre_name:
|
||||
# 'cur_name' of cur_process can match with 'pre_name' of pre_process
|
||||
self._pre_name = pre_name
|
||||
self._cur_name = cur_name
|
||||
pre_tensor_list = self._tensors_dict[pre_name]
|
||||
pre_dist_attr = self._pre_strategy[pre_name]
|
||||
cur_dist_attr = self._cur_strategy[cur_name]
|
||||
try:
|
||||
tensors_dict[cur_name] = Converter.merge_and_slice(
|
||||
pre_tensor_list, pre_dist_attr, cur_dist_attr
|
||||
)
|
||||
except ValueError as err:
|
||||
raise ValueError(
|
||||
f"Fail to convert tensor '{cur_name}' by '{pre_name}'. {err}"
|
||||
)
|
||||
self._logger.info(
|
||||
f"tensor [{cur_name}] is matched with tensor [{pre_name}]"
|
||||
)
|
||||
tensor_match_with_pre.append(cur_name)
|
||||
tensor_match_with_cur.append(pre_name)
|
||||
break
|
||||
break
|
||||
|
||||
return tensors_dict, tensor_match_with_pre, tensor_match_with_cur
|
||||
|
||||
@staticmethod
|
||||
def merge_and_slice(tensor_list, pre_dist_attr, cur_dist_attr):
|
||||
"""
|
||||
Merge tensors with previous dist_attr and slice tensors with current dist_attr
|
||||
|
||||
Returns:
|
||||
tensor(numpy.narray): a tensor's value of current rank.
|
||||
"""
|
||||
assert isinstance(tensor_list, list)
|
||||
assert all(isinstance(p, np.ndarray) for p in tensor_list)
|
||||
|
||||
if pre_dist_attr == cur_dist_attr:
|
||||
# skip merge and slice tensor
|
||||
rank_id = paddle.distributed.get_rank()
|
||||
index = cur_dist_attr["process_group"].index(rank_id)
|
||||
tensor = tensor_list[index]
|
||||
else:
|
||||
pre_dims_mapping = pre_dist_attr["dims_mapping"]
|
||||
cur_dims_mapping = cur_dist_attr["dims_mapping"]
|
||||
|
||||
if len(pre_dims_mapping) and (
|
||||
len(set(pre_dims_mapping)) > 1 or -1 not in pre_dims_mapping
|
||||
):
|
||||
# merge tensor
|
||||
tensor = Converter.merge_with_dist_attr(
|
||||
tensor_list, pre_dist_attr
|
||||
)
|
||||
else:
|
||||
# skip merge tensor
|
||||
tensor = tensor_list[0]
|
||||
|
||||
if len(cur_dims_mapping) and (
|
||||
len(set(cur_dims_mapping)) > 1 or -1 not in cur_dims_mapping
|
||||
):
|
||||
# slice tensor
|
||||
tensor = Converter.slice_with_dist_attr(tensor, cur_dist_attr)
|
||||
|
||||
return tensor
|
||||
|
||||
@staticmethod
|
||||
def merge_with_dist_attr(tensor_list, dist_attr):
|
||||
"""Merge tensor with distributed attribute"""
|
||||
from .reshard import Resharder
|
||||
|
||||
dims_mapping = dist_attr["dims_mapping"]
|
||||
process_shape = dist_attr["process_shape"]
|
||||
process_group = dist_attr["process_group"]
|
||||
# get the complete shape of the tensor
|
||||
complete_shape = Resharder.compute_complete_shape(
|
||||
tensor_list[0].shape, process_shape, dims_mapping
|
||||
)
|
||||
# merge the tensor with dist_attr
|
||||
partition_tensor_list = []
|
||||
merged_partition = []
|
||||
for process in process_group:
|
||||
partition_index = Resharder.compute_partition_index(
|
||||
process,
|
||||
complete_shape,
|
||||
dims_mapping,
|
||||
process_shape,
|
||||
process_group,
|
||||
)
|
||||
index = process_group.index(process)
|
||||
if partition_index not in merged_partition:
|
||||
merged_partition.append(partition_index)
|
||||
Converter.merge(
|
||||
partition_tensor_list,
|
||||
tensor_list[index],
|
||||
partition_index,
|
||||
complete_shape,
|
||||
)
|
||||
|
||||
if len(partition_tensor_list) != 1:
|
||||
raise ValueError(
|
||||
f"Fail to merge tensor with dist_attr '{dist_attr}'."
|
||||
)
|
||||
complete_tensor = partition_tensor_list[0][0]
|
||||
return complete_tensor
|
||||
|
||||
@staticmethod
|
||||
def slice_with_dist_attr(tensor, dist_attr):
|
||||
"""Slice tensor with distributed attribute"""
|
||||
dims_mapping = dist_attr["dims_mapping"]
|
||||
if len(dims_mapping) == 0:
|
||||
# NOTE: scalar tensor no need to split
|
||||
return tensor
|
||||
process_shape = dist_attr["process_shape"]
|
||||
process_group = dist_attr["process_group"]
|
||||
# slice the tensor with dist_attr
|
||||
partition_index_list = Converter._get_split_indices(
|
||||
tensor.shape, dims_mapping, process_shape, process_group
|
||||
)
|
||||
sliced_tensor_list = Converter.split(
|
||||
tensor, partition_index_list, len(partition_index_list)
|
||||
)
|
||||
# get the current tensor's index in sliced_tensor_list
|
||||
rank_id = paddle.distributed.get_rank()
|
||||
sliced_tensor_index = Converter._get_sliced_index(
|
||||
rank_id, tensor.shape, dims_mapping, process_shape, process_group
|
||||
)
|
||||
if sliced_tensor_index not in range(len(sliced_tensor_list)):
|
||||
raise ValueError(
|
||||
f"Fail to slice tensor with dist_attr '{dist_attr}'."
|
||||
)
|
||||
sliced_tensor = sliced_tensor_list[sliced_tensor_index]
|
||||
return sliced_tensor
|
||||
|
||||
@staticmethod
|
||||
def merge(partition_tensor_list, tensor, partition_index, complete_shape):
|
||||
"""
|
||||
Merge partial tensors to a complete.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> import paddle
|
||||
>>> from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
>>> partition_tensor_list = [(np.array([[[1.11, 1.12]]]), [[0, 1], [0, 1], [0, 2]])]
|
||||
>>> tensor = np.array([[[1.13, 1.14]]])
|
||||
>>> partition_index = [[0, 1], [0, 1], [2, 4]]
|
||||
>>> complete_shape = [3, 2]
|
||||
|
||||
>>> Converter.merge(partition_tensor_list, tensor, partition_index, complete_shape)
|
||||
>>> print(partition_tensor_list)
|
||||
[(array([[[1.11, 1.12, 1.13, 1.14]]]), [[0, 1], [0, 1], [0, 4]])]
|
||||
"""
|
||||
from .reshard import Resharder
|
||||
|
||||
if len(partition_tensor_list) == 1:
|
||||
is_complete_data = True
|
||||
for idx, item in enumerate(partition_tensor_list[0][1]):
|
||||
if item[0] != 0 or item[1] != complete_shape[idx]:
|
||||
is_complete_data = False
|
||||
break
|
||||
if is_complete_data:
|
||||
return
|
||||
|
||||
if not partition_tensor_list:
|
||||
partition_tensor_list.append((tensor, partition_index))
|
||||
else:
|
||||
i = 0
|
||||
while i < len(partition_tensor_list):
|
||||
(
|
||||
concat_axis,
|
||||
first_order,
|
||||
new_partition,
|
||||
) = Resharder.compute_concat_info(
|
||||
partition_tensor_list[i][1], partition_index
|
||||
)
|
||||
if concat_axis != -1:
|
||||
if first_order == 0:
|
||||
new_tensor = np.concatenate(
|
||||
(partition_tensor_list[i][0], tensor),
|
||||
axis=concat_axis,
|
||||
)
|
||||
else:
|
||||
new_tensor = np.concatenate(
|
||||
(tensor, partition_tensor_list[i][0]),
|
||||
axis=concat_axis,
|
||||
)
|
||||
|
||||
partition_tensor_list.pop(i)
|
||||
Converter.merge(
|
||||
partition_tensor_list,
|
||||
new_tensor,
|
||||
new_partition,
|
||||
complete_shape,
|
||||
)
|
||||
break
|
||||
i += 1
|
||||
|
||||
@staticmethod
|
||||
def split(complete_tensor, partition_index_list, length):
|
||||
"""
|
||||
Slice a complete tensor.
|
||||
|
||||
Returns:
|
||||
sliced_tensor_list(list): sliced tensors with 'partition_index_list'
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
>>> complete_tensor = np.array([[[1.11, 1.12, 1.13, 1.14, 1.15, 1.16]]])
|
||||
>>> rank = 2
|
||||
>>> complete_shape = [1, 1, 6]
|
||||
>>> dims_mapping = [-1, -1, 0]
|
||||
>>> process_shape = [3]
|
||||
>>> process_group = [0, 1, 2]
|
||||
|
||||
>>> sliced_tensor_list = Converter.split(complete_tensor, [[], [], [2, 4]], 3)
|
||||
>>> print(sliced_tensor_list)
|
||||
[array([[[1.11, 1.12]]]), array([[[1.13, 1.14]]]), array([[[1.15, 1.16]]])]
|
||||
"""
|
||||
sliced_tensor_list = []
|
||||
axis = len(complete_tensor.shape) - length
|
||||
sliced_tensor = np.split(
|
||||
complete_tensor, partition_index_list[axis], axis=axis
|
||||
)
|
||||
if length == 1:
|
||||
return sliced_tensor
|
||||
for tensor in sliced_tensor:
|
||||
sliced_tensor_list.extend(
|
||||
Converter.split(tensor, partition_index_list, length - 1)
|
||||
)
|
||||
return sliced_tensor_list
|
||||
|
||||
@staticmethod
|
||||
def _get_split_indices(
|
||||
complete_shape, dims_mapping, process_shape, process_group
|
||||
):
|
||||
"""
|
||||
Get split indices of every dimension.
|
||||
|
||||
Returns:
|
||||
split_indices_list(list): the split indices of every dimension of the tensor
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> from paddle.distributed.auto_parallel.static.utils import _get_split_indices
|
||||
>>> complete_tensor = np.array([[[1.11, 1.12, 1.13, 1.14, 1.15, 1.16]]])
|
||||
>>> complete_shape = [1, 1, 6]
|
||||
>>> dims_mapping = [-1, -1, 0]
|
||||
>>> process_shape = [3]
|
||||
>>> process_group = [0, 1, 2]
|
||||
|
||||
>>> index = _get_split_indices(complete_shape, dims_mapping, process_shape, process_group)
|
||||
>>> print(index)
|
||||
[[], [], [2, 4]]
|
||||
"""
|
||||
from .reshard import Resharder
|
||||
|
||||
split_indices_list = []
|
||||
for process in process_group:
|
||||
partition_index = Resharder.compute_partition_index(
|
||||
process,
|
||||
complete_shape,
|
||||
dims_mapping,
|
||||
process_shape,
|
||||
process_group,
|
||||
)
|
||||
if split_indices_list:
|
||||
for dim in range(len(partition_index)):
|
||||
split_indices_list[dim].extend(partition_index[dim])
|
||||
else:
|
||||
split_indices_list = partition_index
|
||||
split_indices_list = list(
|
||||
map(
|
||||
lambda x, y: list(set(x) - {y} - {0}),
|
||||
split_indices_list,
|
||||
complete_shape,
|
||||
)
|
||||
)
|
||||
split_indices_list = [sorted(x) for x in split_indices_list]
|
||||
return split_indices_list
|
||||
|
||||
@staticmethod
|
||||
def _get_sliced_index(
|
||||
rank_id, complete_shape, dims_mapping, process_shape, process_group
|
||||
):
|
||||
"""
|
||||
Get sliced_tensor's index of current rank in all sliced tensors list.
|
||||
|
||||
Returns:
|
||||
sliced_tensor_index(int): the index of sliced tensor in sliced_tensor_list
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> # doctest: +REQUIRES(env:DISTRIBUTED)
|
||||
>>> import numpy as np
|
||||
>>> from paddle.distributed.auto_parallel.static.converter import Converter
|
||||
>>> complete_tensor = np.array([[[1.11, 1.12, 1.13, 1.14, 1.15, 1.16]]])
|
||||
>>> rank = 2
|
||||
>>> complete_shape = [1, 1, 6]
|
||||
>>> dims_mapping = [-1, -1, 0]
|
||||
>>> process_shape = [3]
|
||||
>>> process_group = [0, 1, 2]
|
||||
|
||||
>>> index = Converter._get_sliced_index(
|
||||
... rank,
|
||||
... complete_shape,
|
||||
... dims_mapping,
|
||||
... process_shape,
|
||||
... process_group,
|
||||
... )
|
||||
>>> print(index)
|
||||
2
|
||||
"""
|
||||
from .reshard import Resharder
|
||||
|
||||
partition_index = Resharder.compute_partition_index(
|
||||
rank_id, complete_shape, dims_mapping, process_shape, process_group
|
||||
)
|
||||
sliced_index = 0
|
||||
for i, shape in enumerate(complete_shape):
|
||||
if dims_mapping[i] == -1:
|
||||
slice_shape = shape
|
||||
else:
|
||||
slice_shape = shape // process_shape[dims_mapping[i]]
|
||||
if slice_shape == 1:
|
||||
index = partition_index[i][0]
|
||||
else:
|
||||
index = (partition_index[i][0] + 1) // slice_shape
|
||||
sliced_index = sliced_index * (shape // slice_shape) + index
|
||||
return sliced_index
|
||||
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from .base_cost import ( # noqa: F401
|
||||
CommContext,
|
||||
Cost,
|
||||
_g_op_cost_factory,
|
||||
build_comm_costs_from_descs,
|
||||
build_comm_desc,
|
||||
build_comm_desc_from_dist_op,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_comp_desc_str_for_predict,
|
||||
build_dp_costs,
|
||||
calc_time_by_cost_model,
|
||||
)
|
||||
from .comm_op_cost import ( # noqa: F401
|
||||
AllgatherOpCost,
|
||||
AllReduceOpCost,
|
||||
AllreduceSumOpCost,
|
||||
BroadcastOpCost,
|
||||
IdentityOpCost,
|
||||
RecvOpCost,
|
||||
SendOpCost,
|
||||
)
|
||||
from .comp_op_cost import ( # noqa: F401
|
||||
ConcatOpCost,
|
||||
EmbeddingGradOpCost,
|
||||
EmbeddingOpCost,
|
||||
FillConstantBatchSizeLikeOpCost,
|
||||
MatmulGradOpCost,
|
||||
MatmulOpCost,
|
||||
MatmulV2GradOpCost,
|
||||
MatmulV2OpCost,
|
||||
MulGradOpCost,
|
||||
MulOpCost,
|
||||
Reshape2GradOpCost,
|
||||
Reshape2OpCost,
|
||||
SliceOpCost,
|
||||
SoftmaxGradOpCost,
|
||||
SoftmaxOpCost,
|
||||
SplitOpCost,
|
||||
Transpose2GradOpCost,
|
||||
Transpose2OpCost,
|
||||
)
|
||||
from .estimate_cost import CostEstimator # noqa: F401
|
||||
from .op_runtime_cost import ( # noqa: F401
|
||||
check_if_op_supports_runtime_profiling,
|
||||
measure_program_real_op_cost,
|
||||
)
|
||||
from .tensor_cost import TensorCost # noqa: F401
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,316 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import math
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
|
||||
from .base_cost import CommOpCost, register_op_cost
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AllreduceSumOpCost(CommOpCost):
|
||||
OP_TYPE = "c_allreduce_sum"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
# use tree if cross machine and use ring if in a single machine
|
||||
time = None
|
||||
cluster = self.comm_context.cluster
|
||||
if not cluster.cross_machine(self.group_ranks):
|
||||
time = self.calc_time_ring()
|
||||
else:
|
||||
time = self.calc_time_tree()
|
||||
|
||||
return time
|
||||
|
||||
def calc_time_ring(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
alpha += (
|
||||
2
|
||||
* (self.rank_count - self.machine_count)
|
||||
* self.comm_context.intra_ring
|
||||
)
|
||||
alpha += (
|
||||
2
|
||||
* (self.machine_count - 1)
|
||||
* (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = (
|
||||
alpha
|
||||
+ 2
|
||||
* (self.rank_count - 1)
|
||||
/ self.rank_count
|
||||
* self.comm_count
|
||||
* beta
|
||||
)
|
||||
|
||||
return time
|
||||
|
||||
def calc_time_tree(self):
|
||||
alpha = self.comm_context.base_tree
|
||||
alpha += (
|
||||
2
|
||||
* (self.rank_count / self.machine_count - 1)
|
||||
* self.comm_context.intra_tree
|
||||
)
|
||||
alpha += math.log2(self.machine_count) * (
|
||||
self.comm_context.inter_tree + self.hops * self.comm_context.switch
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
|
||||
time = alpha + 2 * self.comm_count * beta
|
||||
|
||||
return time
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AllReduceOpCost(CommOpCost):
|
||||
OP_TYPE = "all_reduce"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
# use tree if cross machine and use ring if in a single machine
|
||||
time = None
|
||||
cluster = self.comm_context.cluster
|
||||
if not cluster.cross_machine(self.group_ranks):
|
||||
time = self.calc_time_ring()
|
||||
else:
|
||||
time = self.calc_time_tree()
|
||||
|
||||
return time
|
||||
|
||||
def calc_time_ring(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
alpha += (
|
||||
2
|
||||
* (self.rank_count - self.machine_count)
|
||||
* self.comm_context.intra_ring
|
||||
)
|
||||
alpha += (
|
||||
2
|
||||
* (self.machine_count - 1)
|
||||
* (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = (
|
||||
alpha
|
||||
+ 2
|
||||
* (self.rank_count - 1)
|
||||
/ self.rank_count
|
||||
* self.comm_count
|
||||
* beta
|
||||
)
|
||||
|
||||
return time
|
||||
|
||||
def calc_time_tree(self):
|
||||
alpha = self.comm_context.base_tree
|
||||
alpha += (
|
||||
2
|
||||
* (self.rank_count / self.machine_count - 1)
|
||||
* self.comm_context.intra_tree
|
||||
)
|
||||
alpha += math.log2(self.machine_count) * (
|
||||
self.comm_context.inter_tree + self.hops * self.comm_context.switch
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
|
||||
time = alpha + 2 * self.comm_count * beta
|
||||
|
||||
return time
|
||||
|
||||
@property
|
||||
def comm_count(self):
|
||||
from ..reshard import get_var_with_recursion
|
||||
|
||||
if self._comm_count is None:
|
||||
dtype = None
|
||||
shape = None
|
||||
if self.op is not None:
|
||||
vars = self.op.block.vars
|
||||
try:
|
||||
var_name = self.op.input("x")[0]
|
||||
except:
|
||||
var_name = self.op.output("out")[0]
|
||||
var = get_var_with_recursion(
|
||||
var_name, self.op.block, self.op.block.program
|
||||
)
|
||||
dtype = var.dtype
|
||||
shape = var.shape
|
||||
elif self.op_desc is not None:
|
||||
dtype = self.op_desc["inputs"]["x"][0][0]
|
||||
shape = self.op_desc["inputs"]["x"][0][1]
|
||||
|
||||
factor = None
|
||||
if dtype == paddle.float32 or dtype == paddle.int32:
|
||||
factor = 4
|
||||
else:
|
||||
raise ValueError(f"Unsupported comm dtype {dtype}")
|
||||
comm_count = int(np.prod(shape)) * factor
|
||||
self._comm_count = comm_count
|
||||
|
||||
return self._comm_count
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AllgatherOpCost(CommOpCost):
|
||||
OP_TYPE = "all_gather"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
time = self.calc_time_ring()
|
||||
return time
|
||||
|
||||
def calc_time_ring(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
alpha += (
|
||||
self.rank_count - self.machine_count
|
||||
) * self.comm_context.intra_ring
|
||||
alpha += (self.machine_count - 1) * (
|
||||
self.comm_context.inter_ring + self.hops * self.comm_context.switch
|
||||
)
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = (
|
||||
alpha
|
||||
+ (self.rank_count - 1) / self.rank_count * self.comm_count * beta
|
||||
)
|
||||
return time
|
||||
|
||||
@property
|
||||
def comm_count(self):
|
||||
from ..reshard import get_var_with_recursion
|
||||
|
||||
if self._comm_count is None:
|
||||
dtype = None
|
||||
shape = None
|
||||
if self.op is not None:
|
||||
vars = self.op.block.vars
|
||||
try:
|
||||
var_name = self.op.input("x")[0]
|
||||
except:
|
||||
var_name = self.op.output("out")[0]
|
||||
var = get_var_with_recursion(
|
||||
var_name, self.op.block, self.op.block.program
|
||||
)
|
||||
dtype = var.dtype
|
||||
shape = var.shape
|
||||
elif self.op_desc is not None:
|
||||
dtype = self.op_desc["inputs"]["X"][0][0]
|
||||
shape = self.op_desc["inputs"]["X"][0][1]
|
||||
|
||||
factor = None
|
||||
if dtype == paddle.float32 or dtype == paddle.int32:
|
||||
factor = 4
|
||||
else:
|
||||
raise ValueError(f"Unsupported comm dtype {dtype}")
|
||||
comm_count = int(np.prod(shape)) * factor
|
||||
self._comm_count = comm_count
|
||||
|
||||
return self._comm_count
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class BroadcastOpCost(CommOpCost):
|
||||
OP_TYPE = "broadcast"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
time = self.calc_time_ring()
|
||||
return time
|
||||
|
||||
def calc_time_ring(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
if self.machine_count > 1:
|
||||
alpha += (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
else:
|
||||
alpha += self.comm_context.intra_ring
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = alpha + self.comm_count * beta
|
||||
|
||||
return time
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class IdentityOpCost(CommOpCost):
|
||||
OP_TYPE = "c_identity"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
return self.comm_count * 1 / (144 * 1e3)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class RecvOpCost(CommOpCost):
|
||||
OP_TYPE = "recv_v2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
if self.machine_count > 1:
|
||||
alpha += (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
else:
|
||||
alpha += self.comm_context.intra_ring
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = alpha + self.comm_count * beta
|
||||
return time
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SendOpCost(CommOpCost):
|
||||
OP_TYPE = "send_v2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, comm_context=None):
|
||||
super().__init__(op=op, op_desc=op_desc, comm_context=comm_context)
|
||||
|
||||
def calc_time(self):
|
||||
alpha = self.comm_context.base_ring
|
||||
if self.machine_count > 1:
|
||||
alpha += (
|
||||
self.comm_context.inter_ring
|
||||
+ self.hops * self.comm_context.switch
|
||||
)
|
||||
else:
|
||||
alpha += self.comm_context.intra_ring
|
||||
beta = self.comm_context.get_max_beta(self.group_ranks)
|
||||
time = alpha + self.comm_count * beta
|
||||
|
||||
return time
|
||||
@@ -0,0 +1,591 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from .base_cost import CompOpCost, register_op_cost
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AdamOpCost(CompOpCost):
|
||||
OP_TYPE = "adam"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ArgsortOpCost(CompOpCost):
|
||||
OP_TYPE = "argsort"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AssignOpCost(CompOpCost):
|
||||
OP_TYPE = "assign"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class AssignValueOpCost(CompOpCost):
|
||||
OP_TYPE = "assign_value"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class BeamSearchOpCost(CompOpCost):
|
||||
OP_TYPE = "beam_search"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class BeamSearchDecodeOpCost(CompOpCost):
|
||||
OP_TYPE = "beam_search_decode"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class CastOpCost(CompOpCost):
|
||||
OP_TYPE = "cast"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ConcatOpCost(CompOpCost):
|
||||
OP_TYPE = "concat"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class DropoutOpCost(CompOpCost):
|
||||
OP_TYPE = "dropout"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class DropoutGradOpCost(CompOpCost):
|
||||
OP_TYPE = "dropout_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseAddOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_add"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseAddGradOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_add_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseDivOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_div"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseDivGradOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_div_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseMulOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_mul"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseMulGradOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_mul_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseSubOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_sub"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ElementwiseSubGradOpCost(CompOpCost):
|
||||
OP_TYPE = "elementwise_sub_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class EqualOpCost(CompOpCost):
|
||||
OP_TYPE = "equal"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class EmbeddingOpCost(CompOpCost):
|
||||
OP_TYPE = "c_embedding"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class EmbeddingGradOpCost(CompOpCost):
|
||||
OP_TYPE = "c_embedding_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class FillConstantOpCost(CompOpCost):
|
||||
OP_TYPE = "fill_constant"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class FillConstantBatchSizeLikeOpCost(CompOpCost):
|
||||
OP_TYPE = "fill_constant_batch_size_like"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class FusedSoftmaxMaskUpperTriangleOpCost(CompOpCost):
|
||||
OP_TYPE = "fused_softmax_mask_upper_triangle"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class FusedSoftmaxMaskUpperTriangleGradOpCost(CompOpCost):
|
||||
OP_TYPE = "fused_softmax_mask_upper_triangle_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class GatherOpCost(CompOpCost):
|
||||
OP_TYPE = "gather"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class GeluOpCost(CompOpCost):
|
||||
OP_TYPE = "gelu"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class GeluGradOpCost(CompOpCost):
|
||||
OP_TYPE = "gelu_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class GreaterEqualOpCost(CompOpCost):
|
||||
OP_TYPE = "greater_equal"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class IncrementOpCost(CompOpCost):
|
||||
OP_TYPE = "increment"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class IsEmptyOpCost(CompOpCost):
|
||||
OP_TYPE = "is_empty"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LayerNormOpCost(CompOpCost):
|
||||
OP_TYPE = "layer_norm"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LayerNormGradOpCost(CompOpCost):
|
||||
OP_TYPE = "layer_norm_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LessThanOpCost(CompOpCost):
|
||||
OP_TYPE = "less_than"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LogicalNotOpCost(CompOpCost):
|
||||
OP_TYPE = "logical_not"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LogicalAndOpCost(CompOpCost):
|
||||
OP_TYPE = "logical_and"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LodResetOpCost(CompOpCost):
|
||||
OP_TYPE = "lod_reset"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LogOpCost(CompOpCost):
|
||||
OP_TYPE = "log"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LookupTableV2OpCost(CompOpCost):
|
||||
OP_TYPE = "lookup_table_v2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class LookupTableV2GradOpCost(CompOpCost):
|
||||
OP_TYPE = "lookup_table_v2_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MatmulOpCost(CompOpCost):
|
||||
OP_TYPE = "matmul"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MatmulGradOpCost(CompOpCost):
|
||||
OP_TYPE = "matmul_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MatmulV2OpCost(CompOpCost):
|
||||
OP_TYPE = "matmul_v2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MatmulV2GradOpCost(CompOpCost):
|
||||
OP_TYPE = "matmul_v2_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MemcpyOpCost(CompOpCost):
|
||||
OP_TYPE = "memcpy"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MulOpCost(CompOpCost):
|
||||
OP_TYPE = "mul"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class MulGradOpCost(CompOpCost):
|
||||
OP_TYPE = "mul_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class OneHotOpCost(CompOpCost):
|
||||
OP_TYPE = "one_hot"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReadFromArrayOpCost(CompOpCost):
|
||||
OP_TYPE = "read_from_array"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReduceSumOpCost(CompOpCost):
|
||||
OP_TYPE = "reduce_sum"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReduceSumGradOpCost(CompOpCost):
|
||||
OP_TYPE = "reduce_sum_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Reshape2OpCost(CompOpCost):
|
||||
OP_TYPE = "reshape2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Reshape2GradOpCost(CompOpCost):
|
||||
OP_TYPE = "reshape2_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReduceMeanOpCost(CompOpCost):
|
||||
OP_TYPE = "reduce_mean"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ReduceMeanGradOpCost(CompOpCost):
|
||||
OP_TYPE = "reduce_mean_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ScaleOpCost(CompOpCost):
|
||||
OP_TYPE = "scale"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class ShapeOpCost(CompOpCost):
|
||||
OP_TYPE = "shape"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SliceOpCost(CompOpCost):
|
||||
OP_TYPE = "slice"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SoftmaxOpCost(CompOpCost):
|
||||
OP_TYPE = "softmax"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SoftmaxGradOpCost(CompOpCost):
|
||||
OP_TYPE = "softmax_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SoftmaxWithCrossEntropyOpCost(CompOpCost):
|
||||
OP_TYPE = "softmax_with_cross_entropy"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SoftmaxWithCrossEntropyGradOpCost(CompOpCost):
|
||||
OP_TYPE = "softmax_with_cross_entropy_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SplitOpCost(CompOpCost):
|
||||
OP_TYPE = "split"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Squeeze2OpCost(CompOpCost):
|
||||
OP_TYPE = "squeeze2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SquareOpCost(CompOpCost):
|
||||
OP_TYPE = "square"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SquareGradOpCost(CompOpCost):
|
||||
OP_TYPE = "square_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class SumOpCost(CompOpCost):
|
||||
OP_TYPE = "sum"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class TopKOpCost(CompOpCost):
|
||||
OP_TYPE = "top_k"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Transpose2OpCost(CompOpCost):
|
||||
OP_TYPE = "transpose2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Transpose2GradOpCost(CompOpCost):
|
||||
OP_TYPE = "transpose2_grad"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class Unsqueeze2OpCost(CompOpCost):
|
||||
OP_TYPE = "unsqueeze2"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
|
||||
|
||||
@register_op_cost
|
||||
class WriteToArrayOpCost(CompOpCost):
|
||||
OP_TYPE = "write_to_array"
|
||||
|
||||
def __init__(self, op=None, op_desc=None, cluster=None, rank=None):
|
||||
super().__init__(op=op, op_desc=op_desc, cluster=cluster, rank=rank)
|
||||
@@ -0,0 +1,671 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from collections import OrderedDict
|
||||
from functools import reduce
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..dist_tensor import DistributedTensor
|
||||
from ..operators.common import get_distributed_operator_impl_container
|
||||
from .base_cost import Cost
|
||||
|
||||
|
||||
class CostEstimator:
|
||||
_special_op_type = ["fused_attention", "fused_feedforward"]
|
||||
|
||||
def __init__(
|
||||
self, program, cluster, mode="modeling", rank=None, loop_count=10
|
||||
):
|
||||
self._program = program
|
||||
self._cluster = cluster
|
||||
self._check_mode(mode)
|
||||
self._mode = mode
|
||||
self._rank = rank if rank is not None else paddle.distributed.get_rank()
|
||||
self._loop_count = loop_count
|
||||
self._global_cost = Cost()
|
||||
self._local_cost_mapping = {}
|
||||
self._detailed_cost = OrderedDict() # {`op_id`: {"reshard": [], "dist_op": [], "local_cost": local_cost}}}
|
||||
self._bubble_time_mapping = {}
|
||||
self._ordered_ops = []
|
||||
self.max_memories = {}
|
||||
self.max_memory = None
|
||||
|
||||
@property
|
||||
def loop_count(self):
|
||||
return self._loop_count
|
||||
|
||||
@property
|
||||
def detailed_cost(self):
|
||||
return self._detailed_cost
|
||||
|
||||
@property
|
||||
def program(self):
|
||||
return self._program
|
||||
|
||||
@property
|
||||
def rank(self):
|
||||
return self._rank
|
||||
|
||||
@property
|
||||
def dist_context(self):
|
||||
return self._dist_context
|
||||
|
||||
@property
|
||||
def cluster(self):
|
||||
return self._cluster
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return self._mode
|
||||
|
||||
@property
|
||||
def global_cost(self):
|
||||
max_time = 0
|
||||
memory = 0
|
||||
flops = 0
|
||||
for rank in self._local_cost_mapping:
|
||||
cost = self._local_cost_mapping[rank]
|
||||
if cost.time > max_time:
|
||||
max_time = cost.time
|
||||
memory += cost.memory
|
||||
flops += cost.flops
|
||||
self._global_cost.time = max_time
|
||||
self._global_cost.memory = memory
|
||||
self._global_cost.flops = flops
|
||||
return self._global_cost
|
||||
|
||||
def local_cost(self, rank=None):
|
||||
rank = self.rank if rank is None else rank
|
||||
if rank not in self._local_cost_mapping:
|
||||
self._local_cost_mapping[rank] = Cost()
|
||||
|
||||
return self._local_cost_mapping[rank]
|
||||
|
||||
def local_bubble_time(self, rank=None):
|
||||
rank = self.rank if rank is None else rank
|
||||
return self._bubble_time_mapping[rank]
|
||||
|
||||
def _check_mode(self, mode):
|
||||
if mode not in ["modeling", "profiling"]:
|
||||
raise ValueError(
|
||||
f"Just support modeling and profiling, but got {mode}"
|
||||
)
|
||||
|
||||
def _is_special_var_name(self, var_name):
|
||||
special_var_name = ["lod_tensor_blocking_queue_0"]
|
||||
if var_name in special_var_name:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _estimate_core(self, dist_context, resharder, block):
|
||||
from ..reshard import get_var_with_recursion
|
||||
|
||||
ops = block.ops
|
||||
loop_count = None
|
||||
if block.desc.id != self.program.global_block().desc.id:
|
||||
loop_count = self.loop_count
|
||||
else:
|
||||
loop_count = 1
|
||||
for i in range(loop_count):
|
||||
for op in ops:
|
||||
self._detailed_cost[op.desc.id()] = OrderedDict()
|
||||
# If in the while sub block, the detail of cost is the last cost
|
||||
detail = self._detailed_cost[op.desc.id()]
|
||||
detail["reshard_cost"] = OrderedDict() #
|
||||
detail["dist_op_cost"] = []
|
||||
if int(op.attr('op_role')) == int(OpRole.Optimize):
|
||||
continue
|
||||
if op.type in [
|
||||
"create_py_reader",
|
||||
"create_double_buffer_reader",
|
||||
"read",
|
||||
]:
|
||||
continue
|
||||
|
||||
# NOTE: It does not support nested loop and just supports while op when op has sub block now.
|
||||
if op.type == "while":
|
||||
while_block = self.program.blocks[op.attr("sub_block").id]
|
||||
self._estimate_core(dist_context, resharder, while_block)
|
||||
continue
|
||||
|
||||
for var_name in op.input_arg_names:
|
||||
if self._is_special_var_name(var_name):
|
||||
continue
|
||||
var = get_var_with_recursion(var_name, block, self.program)
|
||||
reshard_cost = resharder.get_cost(op, var, self.cluster)
|
||||
|
||||
# Calc reshard cost
|
||||
if reshard_cost is not None:
|
||||
detail["reshard_cost"][var_name] = reshard_cost
|
||||
|
||||
comm_costs = reshard_cost[0]
|
||||
local_comp_cost = reshard_cost[1]
|
||||
for comm_cost in comm_costs:
|
||||
# Time is cumulative in global cost and local cost, but memory and flops just are cumulative in global cost.
|
||||
# Comm sync
|
||||
for item in comm_cost:
|
||||
group_ranks, cost = item
|
||||
max_time = None
|
||||
cost_time = {}
|
||||
for rank in group_ranks:
|
||||
rank_cost = self.local_cost(rank)
|
||||
cost_time[rank] = rank_cost.time
|
||||
if max_time is None:
|
||||
max_time = rank_cost.time
|
||||
else:
|
||||
if max_time < rank_cost.time:
|
||||
max_time = rank_cost.time
|
||||
|
||||
for rank in group_ranks:
|
||||
self.local_cost(rank).time = (
|
||||
max_time + cost.time
|
||||
)
|
||||
|
||||
if rank not in self._bubble_time_mapping:
|
||||
self._bubble_time_mapping[rank] = 0
|
||||
|
||||
self._bubble_time_mapping[rank] += (
|
||||
max_time - cost_time[rank]
|
||||
)
|
||||
|
||||
for rank in local_comp_cost:
|
||||
for comp_cost in local_comp_cost[rank]:
|
||||
self.local_cost(rank).time += comp_cost.time
|
||||
|
||||
# Calc dist op cost
|
||||
dist_op = dist_context.get_dist_op_for_program(op)
|
||||
if not dist_op:
|
||||
continue
|
||||
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
processes = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
container = get_distributed_operator_impl_container(
|
||||
op_dist_attr.impl_type
|
||||
)
|
||||
dist_impl = container.impls[op_dist_attr.impl_idx]
|
||||
|
||||
dist_op_cost = dist_impl.calc_cost(
|
||||
op.attr('op_role'), dist_op, dist_context, self.cluster
|
||||
)
|
||||
detail["dist_op_cost"] = dist_op_cost
|
||||
|
||||
if dist_op_cost is None:
|
||||
assert (
|
||||
dist_op.serial_op.type in CostEstimator._special_op_type
|
||||
)
|
||||
continue
|
||||
for item in dist_op_cost:
|
||||
if isinstance(item, list):
|
||||
# Comm sync
|
||||
for comm_op_cost in item:
|
||||
max_time = None
|
||||
cost_time = {}
|
||||
group_ranks = comm_op_cost.group_ranks
|
||||
for rank in comm_op_cost.group_ranks:
|
||||
rank_cost = self.local_cost(rank)
|
||||
cost_time[rank] = rank_cost.time
|
||||
if max_time is None:
|
||||
max_time = rank_cost.time
|
||||
else:
|
||||
if max_time < rank_cost.time:
|
||||
max_time = rank_cost.time
|
||||
for rank in group_ranks:
|
||||
self.local_cost(rank).time = (
|
||||
max_time + comm_op_cost.time
|
||||
if op.attr('op_role') != OpRole.Backward
|
||||
else max_time + 0.9 * comm_op_cost.time
|
||||
)
|
||||
if rank not in self._bubble_time_mapping:
|
||||
self._bubble_time_mapping[rank] = 0
|
||||
self._bubble_time_mapping[rank] += (
|
||||
max_time - cost_time[rank]
|
||||
)
|
||||
elif isinstance(item, dict):
|
||||
# Op just one
|
||||
for rank in processes:
|
||||
# DP+PP+MP
|
||||
if rank not in item:
|
||||
continue
|
||||
self.local_cost(rank).time += item[rank].time
|
||||
|
||||
def prepare(self):
|
||||
self._global_cost = Cost()
|
||||
self._local_cost_mapping = {}
|
||||
self._detailed_cost = OrderedDict()
|
||||
self._bubble_time_mapping = {}
|
||||
|
||||
def _calculate_bytes(self, sizes, dtype):
|
||||
if sizes:
|
||||
total_count = reduce(lambda x, y: x * y, sizes, 1)
|
||||
else:
|
||||
total_count = 0
|
||||
|
||||
if dtype == paddle.float64 or dtype == paddle.int64:
|
||||
dtype_factor = 8
|
||||
elif dtype == paddle.float32 or dtype == paddle.int32:
|
||||
dtype_factor = 4
|
||||
elif (
|
||||
dtype == paddle.float16
|
||||
or dtype == paddle.bfloat16
|
||||
or dtype == paddle.int16
|
||||
):
|
||||
dtype_factor = 2
|
||||
elif dtype == paddle.int8 or dtype == paddle.uint8:
|
||||
dtype_factor = 1
|
||||
else:
|
||||
dtype_factor = 8
|
||||
|
||||
memory = total_count * dtype_factor
|
||||
return memory
|
||||
|
||||
def _estimate_max_memory_by_dist_op(self, dist_context):
|
||||
# This estimation will be improved, now reshard and inplace are not considered.
|
||||
# Persist var is not free.
|
||||
def _convert_pm_and_dm_to_str(process_mesh, dims_mapping):
|
||||
processes = ",".join([str(x) for x in process_mesh.process_ids])
|
||||
topology = ",".join([str(x) for x in process_mesh.shape])
|
||||
dims_mapping = ",".join([str(x) for x in dims_mapping])
|
||||
result = processes + topology + dims_mapping
|
||||
return result
|
||||
|
||||
memories = {}
|
||||
self.max_memories = {}
|
||||
var_info = {} # var_name: [[process_mesh, dims_mapping], [id]], [[process_mesh, dims_mapping], [id]]}
|
||||
|
||||
for block in self.program.blocks:
|
||||
for op in block.ops:
|
||||
self._ordered_ops.append([op.desc.id(), op])
|
||||
self._ordered_ops.sort(key=lambda x: x[0])
|
||||
|
||||
parameters = set()
|
||||
for op_id, op in self._ordered_ops:
|
||||
if op.type in [
|
||||
"create_py_reader",
|
||||
"create_double_buffer_reader",
|
||||
"read",
|
||||
]:
|
||||
continue
|
||||
dist_op = dist_context.get_dist_op_for_program(op)
|
||||
if not dist_op:
|
||||
continue
|
||||
process_mesh = dist_op.dist_attr.process_mesh
|
||||
for var_name in op.input_arg_names:
|
||||
input_dims_mapping = dist_op.dist_attr.get_input_dims_mapping(
|
||||
var_name
|
||||
)
|
||||
|
||||
if var_name not in var_info:
|
||||
var_info[var_name] = {}
|
||||
key = _convert_pm_and_dm_to_str(
|
||||
process_mesh, input_dims_mapping
|
||||
)
|
||||
if key not in var_info[var_name]:
|
||||
var_info[var_name][key] = {}
|
||||
# It is even partition now
|
||||
if "position" not in var_info[var_name][key]:
|
||||
var_info[var_name][key]["position"] = []
|
||||
var_info[var_name][key]["position"].append(op_id)
|
||||
|
||||
if "memory" not in var_info[var_name][key]:
|
||||
var = dist_op.get_serial_input(var_name)
|
||||
global_sizes = var.shape
|
||||
dtype = var.dtype
|
||||
sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes,
|
||||
input_dims_mapping,
|
||||
process_mesh.shape,
|
||||
process_mesh.process_ids,
|
||||
)
|
||||
var_info[var_name][key]["memory"] = self._calculate_bytes(
|
||||
sizes, dtype
|
||||
)
|
||||
if var.persistable:
|
||||
name = var_name + key
|
||||
if name not in parameters:
|
||||
parameters.add(name)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in memories:
|
||||
memories[process] = 0
|
||||
memories[process] += var_info[var_name][key][
|
||||
"memory"
|
||||
]
|
||||
|
||||
for var_name in op.output_arg_names:
|
||||
output_dims_mapping = dist_op.dist_attr.get_output_dims_mapping(
|
||||
var_name
|
||||
)
|
||||
if var_name not in var_info:
|
||||
var_info[var_name] = {}
|
||||
key = _convert_pm_and_dm_to_str(
|
||||
process_mesh, output_dims_mapping
|
||||
)
|
||||
if key not in var_info[var_name]:
|
||||
var_info[var_name][key] = {}
|
||||
if "position" not in var_info[var_name][key]:
|
||||
var_info[var_name][key]["position"] = []
|
||||
var_info[var_name][key]["position"].append(op_id)
|
||||
|
||||
if "memory" not in var_info[var_name][key]:
|
||||
var = dist_op.get_serial_output(var_name)
|
||||
global_sizes = var.shape
|
||||
dtype = var.dtype
|
||||
sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes,
|
||||
output_dims_mapping,
|
||||
process_mesh.shape,
|
||||
process_mesh.process_ids,
|
||||
)
|
||||
var_info[var_name][key]["memory"] = self._calculate_bytes(
|
||||
sizes, dtype
|
||||
)
|
||||
if var.persistable:
|
||||
name = var_name + key
|
||||
if name not in parameters:
|
||||
parameters.add(name)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in memories:
|
||||
memories[process] = 0
|
||||
memories[process] += var_info[var_name][key][
|
||||
"memory"
|
||||
]
|
||||
|
||||
has_used_vars = set()
|
||||
not_calc_vars = set()
|
||||
for op_id, op in self._ordered_ops:
|
||||
if op.type in [
|
||||
"create_py_reader",
|
||||
"create_double_buffer_reader",
|
||||
"read",
|
||||
]:
|
||||
continue
|
||||
can_free_memories = {}
|
||||
can_free_vars = set()
|
||||
dist_op = dist_context.get_dist_op_for_program(op)
|
||||
if not dist_op:
|
||||
continue
|
||||
process_mesh = dist_op.dist_attr.process_mesh
|
||||
for var_name in op.input_arg_names:
|
||||
input_dims_mapping = dist_op.dist_attr.get_input_dims_mapping(
|
||||
var_name
|
||||
)
|
||||
key = _convert_pm_and_dm_to_str(
|
||||
process_mesh, input_dims_mapping
|
||||
)
|
||||
has_used_var = var_name + key
|
||||
var = dist_op.get_serial_input(var_name)
|
||||
# Not used
|
||||
if (
|
||||
has_used_var not in has_used_vars
|
||||
and has_used_var not in parameters
|
||||
):
|
||||
if has_used_var in not_calc_vars:
|
||||
continue
|
||||
has_used_vars.add(has_used_var)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in memories:
|
||||
memories[process] = 0
|
||||
memories[process] += var_info[var_name][key]["memory"]
|
||||
# Used
|
||||
if op_id == var_info[var_name][key]["position"][-1]:
|
||||
if (
|
||||
has_used_var not in can_free_vars
|
||||
and not var.persistable
|
||||
):
|
||||
can_free_vars.add(has_used_var)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in can_free_memories:
|
||||
can_free_memories[process] = 0
|
||||
can_free_memories[process] += var_info[var_name][
|
||||
key
|
||||
]["memory"]
|
||||
|
||||
for var_name in op.output_arg_names:
|
||||
output_dims_mapping = dist_op.dist_attr.get_output_dims_mapping(
|
||||
var_name
|
||||
)
|
||||
key = _convert_pm_and_dm_to_str(
|
||||
process_mesh, output_dims_mapping
|
||||
)
|
||||
has_used_var = var_name + key
|
||||
var = dist_op.get_serial_output(var_name)
|
||||
if (
|
||||
op.type == "reshape2"
|
||||
or op.type == "transpose2"
|
||||
or op.type == "elementwise_add"
|
||||
):
|
||||
not_calc_vars.add(has_used_var)
|
||||
continue
|
||||
# Not used
|
||||
if (
|
||||
has_used_var not in has_used_vars
|
||||
and has_used_var not in parameters
|
||||
):
|
||||
has_used_vars.add(has_used_var)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in memories:
|
||||
memories[process] = 0
|
||||
memories[process] += var_info[var_name][key]["memory"]
|
||||
# Used
|
||||
if op_id == var_info[var_name][key]["position"][-1]:
|
||||
if (
|
||||
has_used_var not in can_free_vars
|
||||
and not var.persistable
|
||||
):
|
||||
can_free_vars.add(has_used_var)
|
||||
for process in process_mesh.process_ids:
|
||||
if process not in can_free_memories:
|
||||
can_free_memories[process] = 0
|
||||
can_free_memories[process] += var_info[var_name][
|
||||
key
|
||||
]["memory"]
|
||||
|
||||
# Calc peak memory
|
||||
for process in memories:
|
||||
if process not in self.max_memories:
|
||||
self.max_memories[process] = memories[process]
|
||||
else:
|
||||
if memories[process] > self.max_memories[process]:
|
||||
self.max_memories[process] = memories[process]
|
||||
# Free memory
|
||||
for process in can_free_memories:
|
||||
if process in memories:
|
||||
memories[process] -= can_free_memories[process]
|
||||
|
||||
# Calculate the max memory in all ranks
|
||||
max_memory = max(self.max_memories.values())
|
||||
self.max_memory = max_memory
|
||||
|
||||
return max_memory
|
||||
|
||||
def estimate(self, dist_context, resharder=None):
|
||||
self.prepare()
|
||||
from ..reshard import Resharder
|
||||
|
||||
resharder = (
|
||||
Resharder(self.program, None, self.rank, dist_context, [])
|
||||
if resharder is None
|
||||
else resharder
|
||||
)
|
||||
|
||||
block = self.program.global_block()
|
||||
self._estimate_core(dist_context, resharder, block)
|
||||
|
||||
return self.global_cost
|
||||
|
||||
def _print_tag(self, max_len, length):
|
||||
tag = "+" + "-" * max_len
|
||||
for i in range(length):
|
||||
print(tag, end="")
|
||||
if i == length - 1:
|
||||
print("+")
|
||||
|
||||
def _print_vals(self, vals, max_len):
|
||||
for idx, val in enumerate(vals):
|
||||
s = "|" + str(val).center(max_len)
|
||||
print(s, end="")
|
||||
if idx == len(vals) - 1:
|
||||
print("|")
|
||||
|
||||
def _pretty_print_memory_cost(self):
|
||||
"""Print memory of every rank prettily."""
|
||||
if not self.max_memories or not self.max_memory:
|
||||
raise ValueError("Please calculate memory cost before print.")
|
||||
|
||||
# Padding automatically
|
||||
max_len = 0
|
||||
header = ["Rank", "Memory(MiB)"]
|
||||
memories = [
|
||||
int(item // 1e6) for item in list(self.max_memories.values())
|
||||
]
|
||||
for memory in memories + header:
|
||||
if len(str(memory)) > max_len:
|
||||
max_len = len(str(memory))
|
||||
max_len += 4 # for pretty print of center
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
# Print header
|
||||
self._print_vals(header, max_len)
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
# Print rank and its memory
|
||||
for i in range(len(self.max_memories)):
|
||||
memory = memories[i]
|
||||
vals = [i, memory]
|
||||
self._print_vals(vals, max_len)
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
def _pretty_print_global(self):
|
||||
"""Print global execution time and max memory prettily."""
|
||||
if not self.max_memories or not self.max_memory:
|
||||
raise ValueError("Please calculate cost before print.")
|
||||
|
||||
# Padding automatically
|
||||
max_len = 0
|
||||
header = ["Execution Time(us)", "Max Memory(MiB)"]
|
||||
vals = [round(self.global_cost.time, 3), int(self.max_memory // 1e6)]
|
||||
for memory in vals + header:
|
||||
if len(str(memory)) > max_len:
|
||||
max_len = len(str(memory))
|
||||
max_len += 4 # for pretty print of center
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
# Print header
|
||||
self._print_vals(header, max_len)
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
# Print exec time and max memory
|
||||
self._print_vals(vals, max_len)
|
||||
|
||||
# Print tag
|
||||
self._print_tag(max_len, len(header))
|
||||
|
||||
def pretty_print_cost(self):
|
||||
"""Print cost prettily."""
|
||||
print("The global execution time and max memory are as follows:")
|
||||
self._pretty_print_global()
|
||||
print("The memory of every rank is as follows:")
|
||||
self._pretty_print_memory_cost()
|
||||
|
||||
|
||||
def get_cost_from_engine(engine, mode):
|
||||
import copy
|
||||
|
||||
from ..utils import to_list
|
||||
|
||||
# Construct cost estimator by original main program
|
||||
serial_main_prog = (
|
||||
engine._fwd_main_progs[mode].clone()
|
||||
if mode in engine._fwd_main_progs
|
||||
else engine._orig_main_prog.clone()
|
||||
)
|
||||
|
||||
serial_startup_prog = (
|
||||
engine._fwd_dist_contexts[mode]._original_serial_main_program.clone()
|
||||
if mode in engine._fwd_dist_contexts
|
||||
else engine._orig_startup_prog.clone()
|
||||
)
|
||||
losses = (
|
||||
to_list(engine._loss)
|
||||
if (
|
||||
not isinstance(engine._loss, paddle.nn.Layer)
|
||||
and not callable(engine._loss)
|
||||
)
|
||||
else engine._losses
|
||||
)
|
||||
serial_optimizer = copy.deepcopy(engine._orig_optimizer)
|
||||
if mode in engine._fwd_dist_contexts:
|
||||
dist_context = copy.deepcopy(engine._fwd_dist_contexts[mode])
|
||||
else:
|
||||
from ..dist_context import DistributedContext
|
||||
|
||||
dist_context = DistributedContext(
|
||||
serial_main_prog,
|
||||
serial_startup_prog,
|
||||
serial_optimizer,
|
||||
losses,
|
||||
{},
|
||||
{"loss": losses},
|
||||
engine._cluster,
|
||||
engine._strategy,
|
||||
)
|
||||
from ..completion import Completer
|
||||
|
||||
completer = Completer(dist_context)
|
||||
completer.complete_forward_annotation()
|
||||
dist_context.block_state.parse_forward_blocks(
|
||||
dist_context.serial_main_program
|
||||
)
|
||||
|
||||
if mode == "eval" or mode == "predict":
|
||||
cost_estimator = CostEstimator(serial_main_prog, engine._cluster)
|
||||
elif mode == "train":
|
||||
from ..parallelizer_v2 import Parallelizer
|
||||
|
||||
# Get serial main program with backward
|
||||
parallelizer = Parallelizer(mode, completer, dist_context)
|
||||
# Generate backward
|
||||
loss_name = dist_context.serial_loss.name
|
||||
serial_loss = serial_main_prog.global_block()._var_recursive(loss_name)
|
||||
params_grads = parallelizer._generate_backward(
|
||||
serial_main_prog, serial_startup_prog, serial_loss
|
||||
)
|
||||
|
||||
# Generate optimizer
|
||||
optimizer_ops = parallelizer._generate_optimizer(
|
||||
serial_main_prog,
|
||||
serial_startup_prog,
|
||||
serial_optimizer,
|
||||
params_grads,
|
||||
)
|
||||
cost_estimator = CostEstimator(serial_main_prog, engine._cluster)
|
||||
|
||||
# Estimate global_cost and max memory
|
||||
global_cost = cost_estimator.estimate(dist_context)
|
||||
max_memory = cost_estimator._estimate_max_memory_by_dist_op(dist_context)
|
||||
|
||||
# Print the cost
|
||||
cost_estimator.pretty_print_cost()
|
||||
|
||||
return global_cost, max_memory
|
||||
@@ -0,0 +1,320 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.base import core
|
||||
from paddle.base.data_feeder import convert_dtype
|
||||
from paddle.base.executor import (
|
||||
_as_lodtensor,
|
||||
_StandaloneExecutor,
|
||||
check_feed_shape_type,
|
||||
)
|
||||
from paddle.base.framework import Operator, Program
|
||||
from paddle.distributed.auto_parallel.static.utils import get_logger, is_comm_op
|
||||
|
||||
|
||||
def check_if_op_supports_runtime_profiling(op):
|
||||
return not is_comm_op(op)
|
||||
|
||||
|
||||
def _measure_program_real_op_cost_multipass(program, place, run_iters, verbose):
|
||||
'''
|
||||
Run op profiling for a single pass. Internal function, do not call this directly.
|
||||
'''
|
||||
|
||||
# clone the program to avoid accidental change made to the vanilla program.
|
||||
cloned_program = program.clone()
|
||||
cloned_main_block = cloned_program.global_block()
|
||||
|
||||
# We will run the executor in a newly created scope, so that our
|
||||
# executor will not pollute the global scope when running. Since
|
||||
# we created a brand new scope, we need to manually create input
|
||||
# tensors and network parameters and feed fake data into them.
|
||||
scope = core.Scope()
|
||||
|
||||
logger = get_logger(log_level=logging.INFO)
|
||||
|
||||
def _analyze_graph_and_collect_all_vars_with_zero_in_degree():
|
||||
var_in_degree = {}
|
||||
|
||||
def _collect_op_input_var_names(op: Operator):
|
||||
input_var_names = []
|
||||
for input_name in op.input_names:
|
||||
input_var_names += op.input(input_name)
|
||||
return input_var_names
|
||||
|
||||
def _collect_op_output_var_names(op: Operator):
|
||||
output_var_names = []
|
||||
for output_name in op.output_names:
|
||||
output_var_names += op.output(output_name)
|
||||
return output_var_names
|
||||
|
||||
def _record_op_output_vars_in_degree(in_var_names, out_var_names):
|
||||
for out_var_name in out_var_names:
|
||||
if out_var_name in in_var_names:
|
||||
# NOTE (liuchenghao): if an op's input var is its output var,
|
||||
# this means this var forms an in-place connection to itself,
|
||||
# in this situation we need to ignore this variable, this way
|
||||
# we can ensure that vars with zero in-degree are dangling vars
|
||||
# and they should be created manually before program executes.
|
||||
continue
|
||||
var_in_degree[out_var_name] += 1
|
||||
|
||||
def _filter_vars_with_zero_in_degree_and_ignore_feed_fetch_vars():
|
||||
filtered_vars = []
|
||||
for var_name in var_in_degree:
|
||||
if var_name in ['feed', 'fetch']:
|
||||
continue
|
||||
if var_in_degree[var_name] == 0:
|
||||
filtered_vars.append(var_name)
|
||||
return filtered_vars
|
||||
|
||||
for op in cloned_main_block.ops:
|
||||
op: Operator
|
||||
if is_comm_op(op):
|
||||
# ignore communication op from graph, because sometimes we want to profile a sub-graph
|
||||
# and these dangling operators will not work (no graph to communicate to/from)
|
||||
continue
|
||||
input_var_names, output_var_names = (
|
||||
_collect_op_input_var_names(op),
|
||||
_collect_op_output_var_names(op),
|
||||
)
|
||||
for var_name in input_var_names + output_var_names:
|
||||
if var_name not in var_in_degree:
|
||||
var_in_degree[var_name] = 0
|
||||
_record_op_output_vars_in_degree(input_var_names, output_var_names)
|
||||
return _filter_vars_with_zero_in_degree_and_ignore_feed_fetch_vars()
|
||||
|
||||
def _alloc_and_fill_var(var_name):
|
||||
supported_var_dtypes = [
|
||||
"paddle.float16",
|
||||
"paddle.float32",
|
||||
"paddle.float64",
|
||||
"paddle.int8",
|
||||
"paddle.int16",
|
||||
"paddle.int32",
|
||||
"paddle.int64",
|
||||
"paddle.bool",
|
||||
]
|
||||
var = cloned_main_block.var(var_name)
|
||||
var_shape = var.shape
|
||||
var_dtype = var.dtype
|
||||
assert str(var_dtype) in supported_var_dtypes, (
|
||||
'Found unsupported variable dtype: "{}", current supported '
|
||||
'dtype(s) is/are: [{}]. '.format(
|
||||
str(var_dtype), ", ".join(supported_var_dtypes)
|
||||
)
|
||||
)
|
||||
(
|
||||
logger.info(
|
||||
f'[+] var: "{var_name}", shape={var_shape}, dtype="{var_dtype}".\n'
|
||||
)
|
||||
if verbose
|
||||
else None
|
||||
)
|
||||
np_dtype = (
|
||||
convert_dtype(var_dtype)
|
||||
if isinstance(var_dtype, core.VarDesc.VarType)
|
||||
else var_dtype
|
||||
)
|
||||
if str(var_dtype).find('int') != -1:
|
||||
# target variable's type is int* (uint*, int*), it is highly possible that
|
||||
# the target variable contains indices (such as lookup_table op's input var)
|
||||
# for safety we need to fill it with all one instead of random numbers
|
||||
# NOTE (liuchenghao): filling with zero will generate "division by zero" error
|
||||
# in mod ops, so filling with one seems to be the simplest way to make it work,
|
||||
# although it is possible that for array with only one element, index "1" is
|
||||
# invalid, that situation is very rare and we don't need to care about it now.
|
||||
new_tensor = np.array(np.ones(var_shape)).astype(np_dtype)
|
||||
else:
|
||||
# target variable's type is float*, we treat it as an ordinary tensor, fill it
|
||||
# with random gaussian numbers
|
||||
new_tensor = np.array(np.random.randn(*var_shape)).astype(np_dtype)
|
||||
new_tensor = _as_lodtensor(new_tensor, place, var_dtype)
|
||||
check_feed_shape_type(var, new_tensor)
|
||||
core.set_variable(scope, new_tensor, var_name)
|
||||
return new_tensor
|
||||
|
||||
def _configure_feed_ops_and_return_feed_names():
|
||||
"""
|
||||
configure feed op,
|
||||
1. alloc feed op output var storage
|
||||
2. fill feed op's input var
|
||||
return feed var names
|
||||
"""
|
||||
|
||||
feed_names = []
|
||||
has_feed_op = False
|
||||
for op in cloned_main_block.ops:
|
||||
if op.type == "feed":
|
||||
has_feed_op = True
|
||||
out_var_name = op.desc.output('Out')[0]
|
||||
in_var_name = op.desc.input('X')[0] # this is usually "feed"
|
||||
input_index = op.desc.attr('col')
|
||||
new_tensor = _alloc_and_fill_var(out_var_name)
|
||||
core.set_feed_variable(
|
||||
scope, new_tensor, in_var_name, input_index
|
||||
)
|
||||
feed_names.append(out_var_name)
|
||||
if not has_feed_op:
|
||||
(
|
||||
logger.info("WARNING: program does not have any feed op.\n")
|
||||
if verbose
|
||||
else None
|
||||
)
|
||||
return feed_names
|
||||
|
||||
for var_name in _analyze_graph_and_collect_all_vars_with_zero_in_degree():
|
||||
_alloc_and_fill_var(var_name)
|
||||
feed_names = _configure_feed_ops_and_return_feed_names()
|
||||
|
||||
# build a simple plan from program and run profiling
|
||||
plan = core.Plan([core.Job("default")], {"default": cloned_program.desc})
|
||||
exe = _StandaloneExecutor(place, plan, scope)
|
||||
|
||||
num_ops = len(cloned_main_block.ops)
|
||||
prof_results = [[None for _ in range(run_iters)] for _ in range(num_ops)]
|
||||
|
||||
for iter_id in range(run_iters):
|
||||
# for each iteration, run profiling and retrieve modified version of program desc
|
||||
program_desc = exe.run_profile(feed_names)
|
||||
|
||||
# rebuild program object from the new program desc
|
||||
temp_program = cloned_program.clone()
|
||||
temp_program._rebuild_from_desc(program_desc)
|
||||
temp_main_block = temp_program.global_block()
|
||||
|
||||
# collect profiling result
|
||||
for op_id, temp_op in zip(
|
||||
range(len(temp_main_block.ops)), temp_main_block.ops
|
||||
):
|
||||
run_time_us = temp_op.dist_attr.run_time_us
|
||||
prof_results[op_id][iter_id] = (
|
||||
run_time_us
|
||||
if check_if_op_supports_runtime_profiling(temp_op)
|
||||
and run_time_us >= 0.0
|
||||
else None
|
||||
)
|
||||
return prof_results
|
||||
|
||||
|
||||
def measure_program_real_op_cost(
|
||||
program: paddle.static.Program,
|
||||
run_iters: int = 8,
|
||||
place=paddle.base.framework._current_expected_place(),
|
||||
verbose_level: int = 0,
|
||||
):
|
||||
'''
|
||||
Description
|
||||
-----------
|
||||
Measuring real op run time (us) with respect to the given "program" and "place".
|
||||
|
||||
Parameters
|
||||
-----------
|
||||
@param program: paddle.static.Program
|
||||
The program object waiting to be executed.
|
||||
@param run_iters: int
|
||||
Specify how many iterations will be run during profiling. Larger value tends
|
||||
to give more accurate profiling result but requires more time.
|
||||
@param place: paddle.CPUPlace | paddle.CUDAPlace
|
||||
Where the program is going to be executed.
|
||||
@param verbose_level: int
|
||||
Set up verbose level during profiling. Can be set to one of the following:
|
||||
0 = turn off, don't output anything,
|
||||
1 = output profiling messages only,
|
||||
2 = output profiling and debug messages.
|
||||
|
||||
Returns
|
||||
-----------
|
||||
Nothing to return. This API will write op run time directly into program object.
|
||||
For example, to retrieve the run time for the first op in program, use:
|
||||
>>> program.global_block().ops[0].dist_attr.run_time_us
|
||||
|
||||
Note
|
||||
-----------
|
||||
Not all ops support runtime profiling. Currently communication ops do not support
|
||||
runtime profiling feature since their execution times rely on other ops. To check
|
||||
if an op supports runtime profiling, use:
|
||||
>>> check_if_op_supports_runtime_profiling(op)
|
||||
where "op" is an instance of "paddle.base.framework.Operator".
|
||||
|
||||
Example
|
||||
-----------
|
||||
* Profiling a simple program from scratch:
|
||||
>>> from paddle.distributed.auto_parallel.static.utils import (
|
||||
... measure_program_real_op_cost,
|
||||
... )
|
||||
>>> program = ... # build your own program object here.
|
||||
>>> measure_program_real_op_cost(
|
||||
>>> program, verbose_level=1
|
||||
>>> )
|
||||
* Profiling a program which is already embedded into an Executor or some other class instance:
|
||||
>>> import paddle
|
||||
>>> from paddle.distributed.auto_parallel.static.utils import (
|
||||
... measure_program_real_op_cost,
|
||||
... )
|
||||
>>> place: str = paddle.device.get_device() # here we assume place = "cuda:x"
|
||||
>>> place = paddle.CUDAPlace(int(place.split(':')[1]))
|
||||
>>> # here "program" is an inner object that has already been built before
|
||||
>>> measure_program_real_op_cost(program, verbose_level=1)
|
||||
'''
|
||||
|
||||
assert isinstance(program, Program), (
|
||||
f'"program" should be a instance of "paddle.base.framework.Program" but got type "{type(program).__name__}".'
|
||||
)
|
||||
supported_places = [
|
||||
paddle.CUDAPlace,
|
||||
]
|
||||
assert any(
|
||||
isinstance(place, supported_place)
|
||||
for supported_place in supported_places
|
||||
), (
|
||||
f'Current place ({place}) does not support runtime profiling. "place" should be one of the following: {supported_places}.'
|
||||
)
|
||||
assert isinstance(run_iters, int) and run_iters >= 1, (
|
||||
'Invalid parameter run_iters set. run_iters should be an integer >= 1.'
|
||||
)
|
||||
if run_iters == 1:
|
||||
warnings.warn(
|
||||
'run_iters was set to 1, profiling results might be inaccurate due to outliers.'
|
||||
)
|
||||
|
||||
logger = get_logger(log_level=logging.INFO)
|
||||
|
||||
# run profiling multiple times and record op run time of each run
|
||||
prof_results = _measure_program_real_op_cost_multipass(
|
||||
program, place, run_iters, verbose=(verbose_level >= 2)
|
||||
)
|
||||
op_num = len(prof_results)
|
||||
for op_id, op in zip(range(op_num), program.global_block().ops):
|
||||
op_runtime_us_final = None
|
||||
if prof_results[op_id][0] is not None:
|
||||
op_runtime_us_final = np.median(prof_results[op_id])
|
||||
if (
|
||||
op_runtime_us_final is not None
|
||||
and check_if_op_supports_runtime_profiling(op)
|
||||
):
|
||||
op.dist_attr.run_time_us = op_runtime_us_final
|
||||
(
|
||||
logger.info(
|
||||
f"{op_id!s:>4} {op.type!s:>32} {op_runtime_us_final:.1f} us"
|
||||
)
|
||||
if verbose_level >= 1
|
||||
else None
|
||||
)
|
||||
@@ -0,0 +1,110 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from functools import reduce
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.auto_parallel.static.dist_tensor import (
|
||||
DistributedTensor,
|
||||
)
|
||||
from paddle.static import Variable
|
||||
|
||||
from .base_cost import Cost
|
||||
|
||||
|
||||
class TensorCost:
|
||||
def __init__(self, tensor=None, dist_tensor=None, shape=None, dtype=None):
|
||||
self._check_args(tensor, dist_tensor, shape, dtype)
|
||||
self._tensor = tensor
|
||||
self._dist_tensor = dist_tensor
|
||||
self._shape = shape
|
||||
self._dtype = dtype
|
||||
self._cost = self.calc_cost()
|
||||
|
||||
@property
|
||||
def tensor(self):
|
||||
return self._tensor
|
||||
|
||||
@property
|
||||
def dist_tensor(self):
|
||||
return self._dist_tensor
|
||||
|
||||
@property
|
||||
def shape(self):
|
||||
return self._shape
|
||||
|
||||
@property
|
||||
def dtype(self):
|
||||
return self._dtype
|
||||
|
||||
def _check_args(self, tensor, dist_tensor, shape, dtype):
|
||||
if tensor is not None:
|
||||
assert shape is None and dist_tensor is None and dtype is None
|
||||
|
||||
if not isinstance(tensor, Variable):
|
||||
raise TypeError(
|
||||
f"Please check tensor type is Variable, but got {type(tensor)}"
|
||||
)
|
||||
|
||||
elif dist_tensor is not None:
|
||||
assert tensor is None and shape is None
|
||||
if not isinstance(dist_tensor, DistributedTensor):
|
||||
raise TypeError(
|
||||
f"Please check dist_tensor type is DistributedTensor, but got {type(dist_tensor)}"
|
||||
)
|
||||
|
||||
elif shape is not None:
|
||||
assert tensor is None and dist_tensor is None and dtype is not None
|
||||
if not isinstance(shape, (list, set)):
|
||||
raise TypeError(
|
||||
f"Please check shape type is list or set, but got {type(shape)}"
|
||||
)
|
||||
|
||||
elif dtype is not None:
|
||||
assert tensor is None and dist_tensor is None and shape is not None
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
return self._cost
|
||||
|
||||
def calc_cost(self):
|
||||
dtype = None
|
||||
shape = None
|
||||
|
||||
if self.dist_tensor:
|
||||
shape = self.dist_tensor.local_sizes()
|
||||
dtype = self.dist_tensor.serial_tensor.dtype
|
||||
elif self.tensor:
|
||||
shape = self.tensor.shape
|
||||
dtype = self.tensor.dtype
|
||||
elif self.shape and self.dtype:
|
||||
shape = self.shape
|
||||
dtype = self.dtype
|
||||
|
||||
total_count = reduce(lambda x, y: x * y, shape, 1)
|
||||
|
||||
if dtype == paddle.float32 or dtype == paddle.int32:
|
||||
dtype_factor = 4
|
||||
elif dtype == paddle.int64:
|
||||
dtype_factor = 8
|
||||
elif dtype == paddle.uint8:
|
||||
dtype_factor = 1
|
||||
else:
|
||||
dtype_factor = 2
|
||||
|
||||
memory = total_count * dtype_factor
|
||||
assert memory >= 0
|
||||
cost = Cost(memory=memory)
|
||||
|
||||
return cost
|
||||
@@ -0,0 +1,855 @@
|
||||
# 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 copy
|
||||
import queue
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
from paddle.framework import core
|
||||
|
||||
SUCC = 0 # successor
|
||||
PRED = 1 # predecessor
|
||||
|
||||
|
||||
class CostNodeType(Enum):
|
||||
DEFAULT = 0
|
||||
COMPUTATION = 1
|
||||
COMMUNICATION = 2
|
||||
VARIABLE = 3
|
||||
MERGED = 4
|
||||
NOP = 5
|
||||
|
||||
|
||||
class Cost:
|
||||
def __init__(self):
|
||||
self.runtime = None
|
||||
self.static_mem = None
|
||||
self.peak_mem = None
|
||||
|
||||
|
||||
class CostModelMode(Enum):
|
||||
DEFAULT = 0
|
||||
BENCHMARKING = 1 # costs based on trial runs
|
||||
ANALYSIS = 2 # costs based on analysis
|
||||
MIXED = 3
|
||||
|
||||
|
||||
class CostNode:
|
||||
def __init__(self, node, node_type, id=None):
|
||||
self.id = id
|
||||
self.node = node
|
||||
self.type = node_type
|
||||
self._cost = 0
|
||||
self.is_optim = False
|
||||
self.is_bwd = False
|
||||
|
||||
@property
|
||||
def cost(self):
|
||||
return self._cost
|
||||
|
||||
@cost.setter
|
||||
def cost(self, cost):
|
||||
if cost < 0:
|
||||
raise ValueError('Cost must be above 0.')
|
||||
self._cost = cost
|
||||
|
||||
|
||||
class MergedOpsCostNode(CostNode):
|
||||
def __init__(self, node_type, id=None, base_node_list=None, is_bwd=False):
|
||||
super().__init__(None, node_type, id)
|
||||
self.node_list = base_node_list
|
||||
self.is_bwd = is_bwd
|
||||
|
||||
|
||||
class CommOpCostNode(CostNode):
|
||||
def __init__(
|
||||
self, node, node_type, id=None, comm_node_list=None, is_bwd=False
|
||||
):
|
||||
super().__init__(node, node_type, id)
|
||||
self.node_list = comm_node_list
|
||||
self.ranks = []
|
||||
self.comm_type = node.type
|
||||
self.is_bwd = is_bwd
|
||||
|
||||
def set_ranks(self, ranks):
|
||||
self.ranks = ranks
|
||||
|
||||
def set_shapes(self, input_shape, output_shape):
|
||||
self.input_shape = input_shape
|
||||
self.output_shape = output_shape
|
||||
|
||||
def init_comm_cost(self, cluster=None):
|
||||
# ref: https://github.com/NVIDIA/nccl-tests/blob/master/doc/PERFORMANCE.md
|
||||
# should get from `cluster`
|
||||
BANDWIDTH = 32 * 1024 / 1000 # MB/ms, V100 PCIe
|
||||
num_ranks = len(self.ranks)
|
||||
comm_volume = np.prod(self.input_shape) * 4
|
||||
|
||||
if 'allreduce' in self.comm_type:
|
||||
self._cost = comm_volume / (
|
||||
BANDWIDTH * num_ranks / (2 * (num_ranks - 1))
|
||||
)
|
||||
elif 'gather' in self.comm_type:
|
||||
self._cost = comm_volume / (BANDWIDTH * num_ranks / (num_ranks - 1))
|
||||
elif 'broadcast' in self.comm_type:
|
||||
self._cost = comm_volume / BANDWIDTH
|
||||
elif 'send' in self.comm_type or 'recv' in self.comm_type:
|
||||
self._cost = comm_volume / BANDWIDTH
|
||||
else:
|
||||
self._cost = 0
|
||||
|
||||
|
||||
class TensorCostNode(CostNode):
|
||||
def __init__(
|
||||
self,
|
||||
node,
|
||||
node_type,
|
||||
id=None,
|
||||
base_node_list=None,
|
||||
batch_size=None,
|
||||
shared_node_id=None,
|
||||
):
|
||||
super().__init__(node, node_type, id)
|
||||
if node.name == "create_py_reader_0" or node.name == "double_buffer_0":
|
||||
self.shape = [2, 2]
|
||||
self.dtype = paddle.float32
|
||||
else:
|
||||
self.shape = node.shape
|
||||
self.dtype = node.dtype
|
||||
self.dtype_factor = 1
|
||||
self.persistable = None
|
||||
self.shared_node_id = shared_node_id
|
||||
if self.dtype == paddle.float32 or node.dtype == paddle.int32:
|
||||
self.dtype_factor *= 4
|
||||
elif node.dtype == paddle.int64:
|
||||
self.dtype_factor *= 8
|
||||
elif node.dtype == paddle.uint8:
|
||||
self.dtype_factor = 1
|
||||
else:
|
||||
self.dtype_factor = 2
|
||||
# raise NotImplementedError("{} not counted".format(node.dtype))
|
||||
self.batch_size = None
|
||||
if batch_size is not None:
|
||||
self.batch_size = batch_size
|
||||
|
||||
def get_size(self):
|
||||
p = 1
|
||||
for i in self.node.shape:
|
||||
if i == -1: # deal with placeholder
|
||||
assert self.batch_size is not None, "Batch size not decided."
|
||||
i = self.batch_size
|
||||
p *= i
|
||||
return p
|
||||
|
||||
|
||||
class CompOpCostNode(CostNode):
|
||||
def __init__(self, node, node_type, id=None, is_bwd=False, is_optim=False):
|
||||
super().__init__(node, node_type, id)
|
||||
self.is_bwd = is_bwd
|
||||
self.is_optim = is_optim
|
||||
|
||||
def init_comp_cost(self, cost_data):
|
||||
# TODO: improve base.CostModel for more specific cost_data
|
||||
op_id = self.node.desc.id()
|
||||
if op_id in cost_data.keys():
|
||||
self.cost = cost_data[op_id]
|
||||
else:
|
||||
self.cost = 0.0
|
||||
|
||||
|
||||
class PipeEvent:
|
||||
def __init__(self, stage_id, event_name, duration, start_time=-1):
|
||||
self.stage_id = stage_id
|
||||
self.name = event_name
|
||||
self.duration = duration
|
||||
self.s_time = start_time
|
||||
self.e_time = -1
|
||||
|
||||
|
||||
class CostModel:
|
||||
def __init__(
|
||||
self,
|
||||
mode=CostModelMode.BENCHMARKING,
|
||||
cluster=None,
|
||||
batch_size=1,
|
||||
microbatch_num=1,
|
||||
opcall_overhead=0,
|
||||
standalone_cost_data=None,
|
||||
pipeline_config=None,
|
||||
):
|
||||
self.mode = mode
|
||||
|
||||
# parameters
|
||||
self.opcall_overhead = opcall_overhead
|
||||
self.batch_size = batch_size
|
||||
self.microbatch_num = microbatch_num
|
||||
|
||||
self.nodes = {} # name -> node
|
||||
|
||||
self.origin_graph = {} # original graph
|
||||
self.op_graph = {} # op graph (no variables nodes)
|
||||
self.runtime_graph = {} # runtime graph, for simulation
|
||||
|
||||
self.cluster = cluster
|
||||
self.cost_data = standalone_cost_data
|
||||
self.pp2rank = pipeline_config
|
||||
if self.pp2rank is not None:
|
||||
self.rank2pp = {}
|
||||
for stage_idx, ranks in enumerate(self.pp2rank):
|
||||
for rank in ranks:
|
||||
self.rank2pp[rank] = stage_idx
|
||||
else:
|
||||
self.rank2pp = None
|
||||
|
||||
self.ring2rank = {}
|
||||
|
||||
self.fwd_time = []
|
||||
self.bwd_time = []
|
||||
self.optim_time = []
|
||||
|
||||
def _parse_sub_program(self, program, nodes, graph, cost_data, sub_idx):
|
||||
assert len(program.blocks) == 1, (
|
||||
"Program more than 1 block not supported."
|
||||
)
|
||||
block = program.blocks[0]
|
||||
|
||||
var_id = "lod_tensor_blocking_queue_0"
|
||||
new_var = program.global_block().create_var(
|
||||
name=var_id,
|
||||
dtype=paddle.float32,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
)
|
||||
nodes[var_id] = TensorCostNode(
|
||||
new_var, CostNodeType.VARIABLE, "lod_tensor_blocking_queue_0"
|
||||
)
|
||||
for var in block.vars.values():
|
||||
var_id = var.name
|
||||
# if var.name == "create_py_reader_0" or var.name == "double_buffer_0":
|
||||
# continue
|
||||
nodes[var_id] = TensorCostNode(var, CostNodeType.VARIABLE, var_id)
|
||||
graph[var_id] = [[], []]
|
||||
|
||||
for op in block.ops:
|
||||
op_id = op.type + "_" + str(op.idx)
|
||||
if (
|
||||
op.type.startswith('c_')
|
||||
or op.type.startswith('send')
|
||||
or op.type.startswith('recv')
|
||||
):
|
||||
is_bwd = False
|
||||
if (
|
||||
op.type.startswith('c_')
|
||||
and op.type != "c_sync_calc_stream"
|
||||
and not op.type.startswith('c_embedding')
|
||||
):
|
||||
ring_id = op.attr('ring_id')
|
||||
if ring_id not in self.ring2rank:
|
||||
self.ring2rank[ring_id] = set()
|
||||
self.ring2rank[ring_id].add(sub_idx)
|
||||
is_bwd = '@GRAD' in op.output('Out')[0]
|
||||
elif op.type.startswith('recv'):
|
||||
is_bwd = '@GRAD' in op.output('Out')[0]
|
||||
elif op.type.startswith('send'):
|
||||
is_bwd = '@GRAD' in op.input('X')[0]
|
||||
op_node = CommOpCostNode(
|
||||
op, CostNodeType.COMMUNICATION, op_id, is_bwd
|
||||
)
|
||||
else:
|
||||
is_bwd = (
|
||||
int(op.attr('op_role')) == int(OpRole.Backward)
|
||||
) or "@GRAD" in op.input_arg_names
|
||||
is_optim = 'LearningRate' in op.input_names
|
||||
op_node = CompOpCostNode(
|
||||
op, CostNodeType.COMPUTATION, op_id, is_bwd, is_optim
|
||||
)
|
||||
op_node.init_comp_cost(cost_data)
|
||||
|
||||
nodes[op_id] = op_node
|
||||
graph[op_id] = [[], []]
|
||||
|
||||
comm_input_shape = [0]
|
||||
comm_output_shape = [0]
|
||||
for i in range(len(op.input_names)):
|
||||
try:
|
||||
var_id = op.input(op.input_names[i])[0]
|
||||
var_node = nodes[var_id]
|
||||
graph[op_id][PRED].append(var_node.id)
|
||||
graph[var_id][SUCC].append(op_node.id)
|
||||
comm_input_shape = var_node.shape
|
||||
except:
|
||||
continue
|
||||
|
||||
for i in range(len(op.output_names)):
|
||||
try:
|
||||
var_id = op.output(op.output_names[i])[0]
|
||||
var_node = nodes[var_id]
|
||||
graph[op_id][SUCC].append(var_node.id)
|
||||
graph[var_id][PRED].append(op_node.id)
|
||||
comm_output_shape = var_node.shape
|
||||
except:
|
||||
continue
|
||||
if op_node.type == CostNodeType.COMMUNICATION:
|
||||
op_node.set_shapes(comm_input_shape, comm_output_shape)
|
||||
|
||||
# resolve hazard: rename the r/w hazard variable nodes to ensure self.origin_graph is a DAG
|
||||
new_var_dict = {}
|
||||
for node_id, node in nodes.items():
|
||||
if node.type == CostNodeType.VARIABLE and node.node.persistable:
|
||||
write_op_cnt = 0
|
||||
for pred_id in graph[node_id][PRED]:
|
||||
pred = nodes[pred_id]
|
||||
if pred.type == CostNodeType.COMPUTATION and (
|
||||
pred_id in graph[node_id][SUCC]
|
||||
):
|
||||
graph[pred_id][SUCC].remove(node_id)
|
||||
graph[node_id][PRED].remove(pred_id)
|
||||
|
||||
write_op_cnt += 1
|
||||
new_var_id = node_id + f'_write_{write_op_cnt}'
|
||||
new_var = TensorCostNode(
|
||||
node.node,
|
||||
CostNodeType.VARIABLE,
|
||||
new_var_id,
|
||||
shared_node_id=node_id,
|
||||
)
|
||||
|
||||
graph[new_var_id] = [[], []]
|
||||
graph[pred_id][SUCC].append(new_var_id)
|
||||
graph[new_var_id][PRED].append(pred_id)
|
||||
|
||||
new_var_dict[new_var_id] = new_var
|
||||
for k, v in new_var_dict.items():
|
||||
nodes[k] = v
|
||||
return nodes
|
||||
|
||||
def parse_program(self, distributed_program):
|
||||
self.distributed_program = distributed_program
|
||||
self.total_rank = len(self.distributed_program)
|
||||
sub_prog_cnt = len(distributed_program)
|
||||
self.nodes = [] * sub_prog_cnt
|
||||
self.origin_graph = [] * sub_prog_cnt # original graph
|
||||
self.op_graph = [] * sub_prog_cnt # op graph (no variables nodes)
|
||||
self.runtime_graph = [] * sub_prog_cnt # runtime graph, for simulation
|
||||
|
||||
for sub_idx, sub_prog in enumerate(distributed_program):
|
||||
self.nodes.append({})
|
||||
self.origin_graph.append({})
|
||||
self.op_graph.append({})
|
||||
self.runtime_graph.append({})
|
||||
self._parse_sub_program(
|
||||
sub_prog,
|
||||
self.nodes[sub_idx],
|
||||
self.origin_graph[sub_idx],
|
||||
self.cost_data[
|
||||
0 if self.rank2pp is None else self.rank2pp[sub_idx]
|
||||
],
|
||||
sub_idx,
|
||||
)
|
||||
return self.nodes
|
||||
|
||||
def _find_succ_op(self, node_id, sub_idx=0):
|
||||
succ_ops_id = []
|
||||
for succ_id in self.origin_graph[sub_idx][node_id][SUCC]:
|
||||
succ = self.nodes[sub_idx][succ_id]
|
||||
if (
|
||||
succ.type == CostNodeType.COMMUNICATION
|
||||
or succ.type == CostNodeType.COMPUTATION
|
||||
):
|
||||
succ_ops_id.append(succ_id)
|
||||
elif succ.type == CostNodeType.VARIABLE:
|
||||
succ_ops_id = succ_ops_id + self._find_succ_op(succ_id, sub_idx)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'This type of node not supported yet:{succ.type}'
|
||||
)
|
||||
return succ_ops_id
|
||||
|
||||
def build_op_graph(self):
|
||||
for sub_idx in range(self.total_rank):
|
||||
op_nodes_id = []
|
||||
for node_id, node in self.nodes[sub_idx].items():
|
||||
if node.type == CostNodeType.VARIABLE:
|
||||
continue
|
||||
self.op_graph[sub_idx][node_id] = [[], []]
|
||||
op_nodes_id.append(node_id)
|
||||
for op_id in op_nodes_id:
|
||||
succ_nodes_id = self._find_succ_op(op_id, sub_idx)
|
||||
|
||||
self.op_graph[sub_idx][op_id][SUCC] = succ_nodes_id
|
||||
for succ_id in succ_nodes_id:
|
||||
self.op_graph[sub_idx][succ_id][PRED].append(op_id)
|
||||
|
||||
def build_runtime_graph(self):
|
||||
self.runtime_graph = copy.deepcopy(self.op_graph)
|
||||
|
||||
def eliminate_multi_edges(self, graph=None):
|
||||
for node_id, edges in graph.items():
|
||||
graph[node_id][PRED] = list(set(edges[PRED]))
|
||||
graph[node_id][SUCC] = list(set(edges[SUCC]))
|
||||
|
||||
def merge_comm(self):
|
||||
for sub_idx in range(self.total_rank):
|
||||
for node_id, edges in self.op_graph[sub_idx].items():
|
||||
node = self.nodes[sub_idx][node_id]
|
||||
if (
|
||||
node_id.startswith('c_')
|
||||
and not node.id.startswith("c_sync_calc_stream")
|
||||
and not node.id.startswith('c_embedding')
|
||||
):
|
||||
ring_id = node.node.attr('ring_id')
|
||||
node.set_ranks(list(self.ring2rank[ring_id]))
|
||||
node.init_comm_cost(self.cluster)
|
||||
elif node_id.startswith('send') or node_id.startswith('recv'):
|
||||
peer_rank = node.node.attr('peer')
|
||||
node.set_ranks([sub_idx, peer_rank])
|
||||
node.init_comm_cost(self.cluster)
|
||||
else:
|
||||
pass # Not communication op
|
||||
|
||||
def _merge_node(self, to_merge_node_list, merge_type='linear', nodes=None):
|
||||
nodes_list = []
|
||||
node_cost = 0
|
||||
for node in to_merge_node_list:
|
||||
if isinstance(node, MergedOpsCostNode):
|
||||
nodes_list += node.node_list
|
||||
else:
|
||||
nodes_list.append(node.id)
|
||||
if merge_type == 'linear':
|
||||
node_cost += node.cost
|
||||
elif merge_type == 'branch':
|
||||
node_cost = max(node_cost, node.cost)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'This type of merging is not supported:{merge_type}'
|
||||
)
|
||||
merged_node_id = 'merged_' + str(len(nodes))
|
||||
is_bwd = to_merge_node_list[0].is_bwd
|
||||
merged_node = MergedOpsCostNode(
|
||||
CostNodeType.MERGED,
|
||||
id=merged_node_id,
|
||||
base_node_list=nodes_list,
|
||||
is_bwd=is_bwd,
|
||||
)
|
||||
merged_node.cost = node_cost
|
||||
return merged_node_id, merged_node
|
||||
|
||||
def merge_linear(self):
|
||||
r'''
|
||||
This method does the following:
|
||||
If X depends on Y only, they must be run sequentially.
|
||||
[ e.g. A ->- C ->- D D and E depends on C only.]
|
||||
[ B ->-/ \->- E C depends on A and B. ]
|
||||
We merge X and Y into a new node and sum up their cost time.
|
||||
'''
|
||||
cnt = 0
|
||||
for sub_idx in range(self.total_rank):
|
||||
cnt += self._merge_linear(
|
||||
self.nodes[sub_idx], self.runtime_graph[sub_idx], is_bwd=False
|
||||
)
|
||||
cnt += self._merge_linear(
|
||||
self.nodes[sub_idx], self.runtime_graph[sub_idx], is_bwd=True
|
||||
)
|
||||
return cnt
|
||||
|
||||
def merge_branch(self):
|
||||
r'''
|
||||
This method does the following:
|
||||
If a node has more than one successor, there is *branch*.
|
||||
[ e.g. A ->- B ->- D ]
|
||||
[ \->- C ->- / , B and C can be run at the same time ]
|
||||
case 1: if B or C is null (or D is directly dependent on A),
|
||||
it's equivalent to A->C->D or A->B->D, fall back to self.merge_linear
|
||||
case 2: if both B and C are some op,
|
||||
merged_cost = max(cost(B), cost(C))
|
||||
'''
|
||||
cnt = 0
|
||||
for sub_idx in range(self.total_rank):
|
||||
cnt += self._merge_branch(
|
||||
self.nodes[sub_idx], self.runtime_graph[sub_idx], is_bwd=False
|
||||
)
|
||||
cnt += self._merge_branch(
|
||||
self.nodes[sub_idx], self.runtime_graph[sub_idx], is_bwd=True
|
||||
)
|
||||
return cnt
|
||||
|
||||
def _merge_linear(self, nodes, runtime_graph, is_bwd=False):
|
||||
reduct_cnt = 0
|
||||
rt_nodes_id = list(runtime_graph.keys())
|
||||
for node_id in rt_nodes_id:
|
||||
if node_id not in runtime_graph.keys():
|
||||
continue
|
||||
node = nodes[node_id]
|
||||
if not is_bwd == node.is_bwd or node.is_optim:
|
||||
continue
|
||||
edges = runtime_graph[node_id]
|
||||
ind = len(edges[PRED]) # in_degree
|
||||
if ind == 1: # only depend on one node
|
||||
pred_id = edges[PRED][0]
|
||||
pred = nodes[pred_id]
|
||||
merged_node_id, merged_node = self._merge_node(
|
||||
[node, pred], merge_type='linear', nodes=nodes
|
||||
)
|
||||
nodes[merged_node_id] = merged_node
|
||||
runtime_graph[merged_node_id] = [[], []]
|
||||
|
||||
# delete edges and add new edges
|
||||
succ = None
|
||||
try:
|
||||
runtime_graph[merged_node_id][SUCC] = copy.deepcopy(
|
||||
edges[SUCC]
|
||||
)
|
||||
|
||||
if len(runtime_graph[pred_id][SUCC]) > 1:
|
||||
# predecessor has more than 1 successor
|
||||
# the merged_node is to inherit the rest of its successors
|
||||
succ = runtime_graph[pred_id][SUCC]
|
||||
succ.remove(node_id)
|
||||
runtime_graph[merged_node_id][SUCC] += succ
|
||||
runtime_graph[merged_node_id][PRED] = runtime_graph[
|
||||
pred_id
|
||||
][PRED]
|
||||
except:
|
||||
pass
|
||||
try:
|
||||
for i in runtime_graph[pred_id][PRED]:
|
||||
try:
|
||||
runtime_graph[i][SUCC].remove(pred_id)
|
||||
except:
|
||||
continue
|
||||
runtime_graph[i][SUCC].append(merged_node_id)
|
||||
except:
|
||||
pass
|
||||
|
||||
try:
|
||||
for i in edges[SUCC]:
|
||||
runtime_graph[i][PRED].remove(node_id)
|
||||
runtime_graph[i][PRED].append(merged_node_id)
|
||||
except:
|
||||
pass
|
||||
if succ is not None:
|
||||
for i in succ:
|
||||
try:
|
||||
runtime_graph[i][PRED].remove(pred_id)
|
||||
except:
|
||||
continue
|
||||
runtime_graph[i][PRED].append(merged_node_id)
|
||||
|
||||
runtime_graph.pop(node_id)
|
||||
try:
|
||||
runtime_graph.pop(pred_id)
|
||||
except:
|
||||
continue
|
||||
reduct_cnt += 1
|
||||
self.eliminate_multi_edges(runtime_graph)
|
||||
break
|
||||
return reduct_cnt # the number of nodes that have been reduced
|
||||
|
||||
def _merge_branch(self, nodes, runtime_graph, is_bwd=False):
|
||||
reduct_cnt = 0
|
||||
rt_nodes_id = list(runtime_graph.keys())
|
||||
for node_id in rt_nodes_id:
|
||||
node = nodes[node_id]
|
||||
if not is_bwd == node.is_bwd or node.is_optim:
|
||||
continue
|
||||
edges = runtime_graph[node_id]
|
||||
outd = len(edges[SUCC]) # out_degree
|
||||
if outd > 1: # branch out
|
||||
succ_nodes_id = edges[SUCC]
|
||||
|
||||
succ_to_elim = []
|
||||
for succ_id in succ_nodes_id:
|
||||
for succ_2_id in succ_nodes_id:
|
||||
try:
|
||||
tmp = runtime_graph[succ_2_id][SUCC]
|
||||
except:
|
||||
continue
|
||||
if succ_id in tmp:
|
||||
succ_to_elim.append(succ_id)
|
||||
break
|
||||
for id in succ_to_elim:
|
||||
edges[SUCC].remove(id)
|
||||
runtime_graph[id][PRED].remove(node_id)
|
||||
reduct_cnt += 1
|
||||
|
||||
to_merge = True
|
||||
try:
|
||||
if (
|
||||
len(edges[SUCC]) < 1
|
||||
or len(runtime_graph[edges[SUCC][0]][SUCC]) < 1
|
||||
):
|
||||
continue
|
||||
except:
|
||||
continue
|
||||
end_node_id = runtime_graph[edges[SUCC][0]][SUCC][0]
|
||||
for i in succ_nodes_id:
|
||||
try:
|
||||
if (
|
||||
len(runtime_graph[i][SUCC]) != 1
|
||||
or runtime_graph[i][SUCC][0] != end_node_id
|
||||
):
|
||||
to_merge = False # if branches has different end node, we don't merge them
|
||||
break
|
||||
except:
|
||||
continue
|
||||
if to_merge and len(succ_nodes_id) > 1:
|
||||
to_merge_node_list = [nodes[i] for i in succ_nodes_id]
|
||||
merged_node_id, merged_node = self._merge_node(
|
||||
to_merge_node_list, merge_type='branch', nodes=nodes
|
||||
)
|
||||
nodes[merged_node_id] = merged_node
|
||||
runtime_graph[merged_node_id] = [[], []]
|
||||
|
||||
# delete edges and add new edges
|
||||
runtime_graph[merged_node_id][SUCC] = [end_node_id]
|
||||
runtime_graph[merged_node_id][PRED] = edges[PRED]
|
||||
|
||||
runtime_graph[end_node_id][PRED] = [merged_node_id]
|
||||
runtime_graph[node_id][SUCC] = [merged_node_id]
|
||||
|
||||
try:
|
||||
for i in succ_nodes_id:
|
||||
runtime_graph.pop(i)
|
||||
reduct_cnt += len(to_merge_node_list) - 1
|
||||
break
|
||||
except:
|
||||
pass
|
||||
return reduct_cnt
|
||||
|
||||
def get_runtime_cost(self):
|
||||
def get_node_cost(node):
|
||||
node_cost = node.cost + self.opcall_overhead
|
||||
if isinstance(node, MergedOpsCostNode):
|
||||
for it in node.node_list:
|
||||
node_cost += self.opcall_overhead
|
||||
return node_cost
|
||||
|
||||
for sub_idx in range(self.total_rank):
|
||||
fwd_cost = 0
|
||||
bwd_cost = 0
|
||||
optim_cost = 0
|
||||
for node_id in self.runtime_graph[sub_idx].keys():
|
||||
node = self.nodes[sub_idx][node_id]
|
||||
if node.is_optim:
|
||||
optim_cost += get_node_cost(node)
|
||||
elif node.is_bwd:
|
||||
bwd_cost += get_node_cost(node)
|
||||
else:
|
||||
fwd_cost += get_node_cost(node)
|
||||
self.fwd_time.append(fwd_cost)
|
||||
self.bwd_time.append(bwd_cost)
|
||||
self.optim_time.append(optim_cost)
|
||||
return self.fwd_time, self.bwd_time, self.optim_time
|
||||
|
||||
def get_mem(self):
|
||||
static_list = []
|
||||
top_list = []
|
||||
for sub_idx in range(self.total_rank):
|
||||
static_mem, cur_mem, top_mem = self._simulate_mem(
|
||||
self.nodes[sub_idx], self.origin_graph[sub_idx]
|
||||
)
|
||||
static_list.append(static_mem)
|
||||
top_list.append(top_mem)
|
||||
return static_list, top_list
|
||||
|
||||
def _simulate_mem(self, nodes, origin_graph):
|
||||
q = queue.Queue(1024)
|
||||
sim_graph = copy.deepcopy(origin_graph)
|
||||
for node_id, node in nodes.items():
|
||||
if len(sim_graph[node_id][PRED]) == 0:
|
||||
q.put(node_id)
|
||||
|
||||
q.put('nop')
|
||||
cur_mem = 0
|
||||
top_mem = -1
|
||||
static_mem = 0
|
||||
while not q.empty():
|
||||
node_id = q.get()
|
||||
node = None
|
||||
size = 0
|
||||
if node_id == 'nop':
|
||||
top_mem = max(cur_mem, top_mem)
|
||||
if q.empty():
|
||||
break
|
||||
else:
|
||||
q.put(node_id)
|
||||
continue
|
||||
else:
|
||||
node = nodes[node_id]
|
||||
if node.type == CostNodeType.VARIABLE:
|
||||
size = node.get_size()
|
||||
if node.node.persistable:
|
||||
static_mem += size
|
||||
cur_mem += size
|
||||
edges = sim_graph[node_id]
|
||||
if not (
|
||||
node.type == CostNodeType.VARIABLE and node.node.persistable
|
||||
):
|
||||
for succ_id in edges[SUCC]:
|
||||
sim_graph[succ_id][PRED].remove(node_id)
|
||||
if len(sim_graph[succ_id][PRED]) == 0:
|
||||
q.put(succ_id)
|
||||
for pred_id in edges[PRED]:
|
||||
pred = nodes
|
||||
if pred.type == CostNodeType.VARIABLE:
|
||||
sim_graph[pred_id][SUCC].remove(node_id)
|
||||
if (
|
||||
len(sim_graph[pred_id][SUCC]) == 0
|
||||
and not pred.node.persistable
|
||||
):
|
||||
cur_mem -= pred.get_size()
|
||||
return static_mem, cur_mem, top_mem
|
||||
|
||||
def get_pipeline_time(self):
|
||||
if self.pp2rank is None:
|
||||
return self.fwd_time[0] + self.bwd_time[0] + self.optim_time[0]
|
||||
else:
|
||||
return self._simulate_pipeline()
|
||||
|
||||
def _simulate_pipeline(self):
|
||||
stage_num = len(self.pp2rank)
|
||||
event_list = []
|
||||
global_time = [0] * stage_num
|
||||
total_time = 0
|
||||
fwd_cnt = list(range(stage_num, 0, -1))
|
||||
bwd_cnt = [self.microbatch_num] * stage_num
|
||||
q = queue.Queue(1024)
|
||||
|
||||
for i in range(self.microbatch_num):
|
||||
q.put(PipeEvent(0, 'fwd', self.fwd_time[0]))
|
||||
|
||||
while not q.empty():
|
||||
e = q.get()
|
||||
stid = e.stage_id
|
||||
if e.name == 'fwd':
|
||||
if fwd_cnt[stid] > 0:
|
||||
e.s_time = max(global_time[stid], e.s_time)
|
||||
e.e_time = e.s_time + e.duration
|
||||
event_list.append(e)
|
||||
if stid != stage_num - 1:
|
||||
q.put(
|
||||
PipeEvent(
|
||||
stid + 1,
|
||||
'fwd',
|
||||
self.fwd_time[stid + 1],
|
||||
start_time=e.e_time,
|
||||
)
|
||||
)
|
||||
else:
|
||||
q.put(
|
||||
PipeEvent(
|
||||
stid,
|
||||
'bwd',
|
||||
self.bwd_time[stid],
|
||||
start_time=e.e_time,
|
||||
)
|
||||
)
|
||||
fwd_cnt[stid] -= 1
|
||||
global_time[stid] = e.e_time
|
||||
else:
|
||||
q.put(e)
|
||||
elif e.name == 'bwd':
|
||||
e.s_time = max(global_time[stid], e.s_time)
|
||||
e.e_time = e.s_time + e.duration
|
||||
event_list.append(e)
|
||||
if stid != 0:
|
||||
q.put(
|
||||
PipeEvent(
|
||||
stid - 1,
|
||||
'bwd',
|
||||
self.bwd_time[stid - 1],
|
||||
start_time=e.e_time,
|
||||
)
|
||||
)
|
||||
fwd_cnt[stid] += 1
|
||||
bwd_cnt[stid] -= 1
|
||||
if bwd_cnt[stid] == 0:
|
||||
q.put(
|
||||
PipeEvent(
|
||||
stid,
|
||||
'optim',
|
||||
self.optim_time[stid],
|
||||
start_time=e.e_time,
|
||||
)
|
||||
)
|
||||
global_time[stid] = e.e_time
|
||||
elif e.name == 'optim':
|
||||
e.s_time = max(global_time[stid], e.s_time)
|
||||
e.e_time = e.s_time + e.duration
|
||||
event_list.append(e)
|
||||
global_time[stid] = e.e_time
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f'This type of pipe event is not supported yet.{e.name}'
|
||||
)
|
||||
|
||||
for t in global_time:
|
||||
total_time = max(total_time, t)
|
||||
return total_time
|
||||
|
||||
def get_cost(self):
|
||||
cost = Cost()
|
||||
static_mem, peak_mem = self.get_mem()
|
||||
cost.static_mem = static_mem
|
||||
cost.peak_mem = peak_mem
|
||||
self.merge_comm()
|
||||
while True:
|
||||
cnt = 0
|
||||
cnt += self.merge_linear()
|
||||
cnt += self.merge_branch()
|
||||
if cnt == 0: # can't be further merged
|
||||
break
|
||||
self.get_runtime_cost()
|
||||
cost.runtime = self.get_pipeline_time()
|
||||
return cost
|
||||
|
||||
def init(self, distributed_program):
|
||||
self.parse_program(distributed_program)
|
||||
self.build_op_graph()
|
||||
for sub_idx in range(self.total_rank):
|
||||
self.eliminate_multi_edges(self.op_graph[sub_idx])
|
||||
self.build_runtime_graph()
|
||||
|
||||
|
||||
def estimate_cost(
|
||||
distributed_program,
|
||||
cluster,
|
||||
pipeline_config,
|
||||
standalone_cost_data,
|
||||
batch_size,
|
||||
):
|
||||
"""
|
||||
Estimated cost from distributed program, cluster model and distributed settings.
|
||||
|
||||
Args:
|
||||
distributed_program(list): list of paddle programs
|
||||
cluster(Cluster): cluster model
|
||||
standalone_cost_data(CostData): cost data given by paddle.core
|
||||
batch_size(int): batch size of the training workload
|
||||
pipeline_config(list): configuration of pipeline stage allocation
|
||||
"""
|
||||
# the following line is left for now, cluster model will be involved in the future
|
||||
assert cluster is None, "For now, cluster remains None"
|
||||
cm_ctx = CostModel(
|
||||
cluster=cluster,
|
||||
batch_size=batch_size,
|
||||
standalone_cost_data=standalone_cost_data,
|
||||
pipeline_config=pipeline_config,
|
||||
)
|
||||
cm_ctx.init(distributed_program)
|
||||
cost = cm_ctx.get_cost()
|
||||
return cost
|
||||
@@ -0,0 +1,19 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from paddle.base.core import ( # noqa: F401
|
||||
DistTensorSpec,
|
||||
OperatorDistAttr,
|
||||
TensorDistAttr,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,62 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
|
||||
from paddle.static import InputSpec
|
||||
|
||||
from ..placement_type import get_shard_spec
|
||||
from .utils import convert_to_dims_mapping
|
||||
|
||||
|
||||
class DistributedInputSpec(InputSpec):
|
||||
def __init__(
|
||||
self,
|
||||
shape,
|
||||
dtype='float32',
|
||||
name=None,
|
||||
stop_gradient=False,
|
||||
mesh=None,
|
||||
placements=None,
|
||||
local_shape=None,
|
||||
):
|
||||
super().__init__(shape, dtype, name, stop_gradient)
|
||||
self.mesh = copy.deepcopy(mesh)
|
||||
sharding_specs = get_shard_spec(mesh, placements, len(self.shape))
|
||||
self.dims_mapping = convert_to_dims_mapping(sharding_specs, mesh)
|
||||
self.local_shape = local_shape
|
||||
|
||||
@classmethod
|
||||
def from_dtensor(cls, dtensor, name=None, shape=None):
|
||||
"""
|
||||
Generates a DistributedInputSpec based on dist tensor.
|
||||
|
||||
Args:
|
||||
dtensor: the dist tensor.
|
||||
|
||||
Returns:
|
||||
A DistributedInputSpec instance generated from dtensor.
|
||||
"""
|
||||
return cls(
|
||||
shape=dtensor.shape if shape is None else shape,
|
||||
dtype=dtensor.dtype,
|
||||
name=name,
|
||||
stop_gradient=dtensor.stop_gradient,
|
||||
mesh=dtensor.process_mesh,
|
||||
placements=dtensor.placements,
|
||||
local_shape=dtensor._local_value().shape,
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{super().__repr__()}, mesh:{self.mesh}, placements:{self.dims_mapping}"
|
||||
@@ -0,0 +1,290 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import abc
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.io import BatchSampler, IterableDataset
|
||||
from paddle.io.dataloader.batch_sampler import (
|
||||
DistributedBatchSampler,
|
||||
_InfiniteIterableSampler,
|
||||
)
|
||||
from paddle.io.dataloader.dataloader_iter import (
|
||||
_DatasetKind,
|
||||
default_collate_fn,
|
||||
default_convert_fn,
|
||||
)
|
||||
|
||||
|
||||
class DistributedDataLoaderBase(metaclass=abc.ABCMeta):
|
||||
@abc.abstractmethod
|
||||
def __iter__(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class DistributedDataLoaderFromGenerator(DistributedDataLoaderBase):
|
||||
def __init__(
|
||||
self,
|
||||
dataset,
|
||||
feed_list=None,
|
||||
capacity=None,
|
||||
use_double_buffer=True,
|
||||
iterable=True,
|
||||
return_list=False,
|
||||
use_multiprocess=False,
|
||||
drop_last=True,
|
||||
places=None,
|
||||
batch_size=1,
|
||||
epochs=1,
|
||||
steps_per_epoch=None,
|
||||
collate_fn=None,
|
||||
split_data=True,
|
||||
data_parallel_world_size=[],
|
||||
data_parallel_rank=[],
|
||||
acc_steps=1,
|
||||
):
|
||||
self.dataset = dataset
|
||||
self.feed_list = feed_list
|
||||
self.capacity = capacity
|
||||
self.use_double_buffer = use_double_buffer
|
||||
self.iterable = iterable
|
||||
self.return_list = return_list
|
||||
self.use_multiprocess = use_multiprocess
|
||||
self.drop_last = drop_last
|
||||
self.places = places
|
||||
self.batch_size = batch_size
|
||||
self.epochs = epochs
|
||||
self.steps_per_epoch = steps_per_epoch
|
||||
self.collate_fn = collate_fn
|
||||
self.split_data = split_data
|
||||
assert len(data_parallel_world_size) == len(feed_list)
|
||||
assert len(data_parallel_rank) == len(feed_list)
|
||||
self.dp_world_sizes = data_parallel_world_size
|
||||
self.dp_ranks = data_parallel_rank
|
||||
self.acc_steps = acc_steps
|
||||
|
||||
if isinstance(dataset, IterableDataset):
|
||||
self.dataset_kind = _DatasetKind.ITER
|
||||
else:
|
||||
self.dataset_kind = _DatasetKind.MAP
|
||||
|
||||
if self.batch_size is None:
|
||||
self.batch_sampler = None
|
||||
else:
|
||||
if isinstance(dataset, IterableDataset):
|
||||
self.batch_sampler = _InfiniteIterableSampler(
|
||||
dataset, batch_size
|
||||
)
|
||||
else:
|
||||
self.batch_sampler = BatchSampler(
|
||||
dataset,
|
||||
batch_size=batch_size,
|
||||
shuffle=False,
|
||||
drop_last=drop_last,
|
||||
)
|
||||
|
||||
self.auto_collate_batch = self.batch_sampler is not None
|
||||
self.sampler_iter = iter(self.index_sampler)
|
||||
|
||||
if self.auto_collate_batch:
|
||||
self.collate_fn = collate_fn or default_collate_fn
|
||||
else:
|
||||
self.collate_fn = collate_fn or default_convert_fn
|
||||
|
||||
self.dataset_fetcher = _DatasetKind.create_fetcher(
|
||||
self.dataset_kind,
|
||||
self.dataset,
|
||||
self.auto_collate_batch,
|
||||
self.collate_fn,
|
||||
self.drop_last,
|
||||
)
|
||||
|
||||
self._steps = self._infer_steps()
|
||||
self._inner_dataloader = self._create_inner_dataloader()
|
||||
|
||||
def __iter__(self):
|
||||
self._cur_step = 0
|
||||
self._inner_dataloader.start()
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if not self._steps:
|
||||
self._cur_step += 1
|
||||
return None
|
||||
elif self._cur_step < self._steps:
|
||||
self._cur_step += 1
|
||||
return None
|
||||
else:
|
||||
self._inner_dataloader.reset()
|
||||
self.sampler_iter = iter(self.index_sampler)
|
||||
raise StopIteration
|
||||
|
||||
def _infer_steps(self):
|
||||
if isinstance(self.steps_per_epoch, int) and self.steps_per_epoch > 0:
|
||||
return self.steps_per_epoch
|
||||
try:
|
||||
if isinstance(self.dataset, IterableDataset):
|
||||
steps_per_epoch = None
|
||||
elif self.batch_size is None:
|
||||
steps_per_epoch = len(self.dataset) // self.acc_steps
|
||||
else:
|
||||
steps_per_epoch = (
|
||||
len(self.dataset) // self.batch_size // self.acc_steps
|
||||
)
|
||||
except:
|
||||
raise ValueError(
|
||||
"Please set `steps_per_epoch` or implement `__len__` method in dataset class."
|
||||
)
|
||||
return steps_per_epoch
|
||||
|
||||
@property
|
||||
def index_sampler(self):
|
||||
if self.auto_collate_batch:
|
||||
return self.batch_sampler
|
||||
else:
|
||||
if self.dataset_kind == _DatasetKind.MAP:
|
||||
return list(range(len(self.dataset)))
|
||||
else:
|
||||
return _InfiniteIterableSampler(self.dataset, 1)
|
||||
|
||||
def _create_inner_dataloader(self):
|
||||
def data_generator():
|
||||
while True:
|
||||
try:
|
||||
indices = next(self.sampler_iter)
|
||||
batch = self.dataset_fetcher.fetch(indices)
|
||||
if batch is None:
|
||||
break
|
||||
except StopIteration:
|
||||
self.dataset_fetcher = _DatasetKind.create_fetcher(
|
||||
self.dataset_kind,
|
||||
self.dataset,
|
||||
self.auto_collate_batch,
|
||||
self.collate_fn,
|
||||
self.drop_last,
|
||||
)
|
||||
break
|
||||
|
||||
partial_data = []
|
||||
for i, d in enumerate(batch):
|
||||
array = np.array(d)
|
||||
if not self.split_data:
|
||||
partial_data.append(array)
|
||||
continue
|
||||
|
||||
batch_size = array.shape[0]
|
||||
assert batch_size % self.dp_world_sizes[i] == 0, (
|
||||
f"batch_size [{batch_size}] is not divisible by dp_world_size [{self.dp_world_sizes[i]}]"
|
||||
)
|
||||
partial_data.append(
|
||||
np.split(array, self.dp_world_sizes[i])[
|
||||
self.dp_ranks[i]
|
||||
]
|
||||
)
|
||||
|
||||
yield partial_data
|
||||
|
||||
dataloader = paddle.base.io.DataLoader.from_generator(
|
||||
feed_list=self.feed_list,
|
||||
capacity=self.capacity,
|
||||
use_double_buffer=self.use_double_buffer,
|
||||
# iterable=self.iterable,
|
||||
iterable=False,
|
||||
return_list=self.return_list,
|
||||
use_multiprocess=self.use_multiprocess,
|
||||
drop_last=self.drop_last,
|
||||
)
|
||||
dataloader.set_batch_generator(data_generator, self.places)
|
||||
|
||||
return dataloader
|
||||
|
||||
|
||||
class DistributedDataLoader(DistributedDataLoaderBase):
|
||||
def __init__(
|
||||
self,
|
||||
dataset,
|
||||
feed_list=None,
|
||||
places=None,
|
||||
return_list=True,
|
||||
batch_size=1,
|
||||
shuffle=False,
|
||||
drop_last=False,
|
||||
collate_fn=None,
|
||||
num_workers=0,
|
||||
use_buffer_reader=True,
|
||||
use_shared_memory=True,
|
||||
timeout=0,
|
||||
worker_init_fn=None,
|
||||
epochs=1,
|
||||
steps_per_epoch=None,
|
||||
split_data=True,
|
||||
data_parallel_world_size=[],
|
||||
data_parallel_rank=[],
|
||||
):
|
||||
self.dataset = dataset
|
||||
self.feed_list = feed_list
|
||||
self.return_list = return_list
|
||||
self.places = places
|
||||
self.batch_size = batch_size
|
||||
self.shuffle = shuffle
|
||||
self.drop_last = drop_last
|
||||
self.collate_fn = collate_fn
|
||||
self.num_workers = num_workers
|
||||
self.use_buffer_reader = use_buffer_reader
|
||||
self.use_shared_memory = use_shared_memory
|
||||
self.timeout = timeout
|
||||
self.worker_init_fn = worker_init_fn
|
||||
self.epochs = epochs
|
||||
self.steps_per_epoch = steps_per_epoch
|
||||
self.dp_world_sizes = data_parallel_world_size
|
||||
self.dp_ranks = data_parallel_rank
|
||||
self.split_data = split_data
|
||||
|
||||
if self.batch_size is None:
|
||||
self.batch_sampler = None
|
||||
else:
|
||||
self.batch_sampler = DistributedBatchSampler(
|
||||
dataset=self.dataset,
|
||||
batch_size=self.batch_size,
|
||||
num_replicas=self.dp_world_sizes[0],
|
||||
rank=self.dp_ranks[0],
|
||||
shuffle=self.shuffle,
|
||||
drop_last=self.drop_last,
|
||||
)
|
||||
|
||||
self._dataloader = paddle.io.DataLoader(
|
||||
self.dataset,
|
||||
feed_list=self.feed_list,
|
||||
places=self.places,
|
||||
return_list=self.return_list,
|
||||
batch_sampler=self.batch_sampler,
|
||||
batch_size=1 if self.batch_sampler else self.batch_size,
|
||||
collate_fn=self.collate_fn,
|
||||
num_workers=self.num_workers,
|
||||
use_buffer_reader=self.use_buffer_reader,
|
||||
use_shared_memory=self.use_shared_memory,
|
||||
timeout=self.timeout,
|
||||
worker_init_fn=self.worker_init_fn,
|
||||
)
|
||||
|
||||
def __len__(self):
|
||||
return len(self._dataloader)
|
||||
|
||||
def __iter__(self):
|
||||
return self._dataloader.__iter__()
|
||||
|
||||
def __call__(self):
|
||||
return self._dataloader.__iter__()
|
||||
@@ -0,0 +1,323 @@
|
||||
# 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 copy
|
||||
|
||||
import paddle
|
||||
from paddle.static import Variable
|
||||
|
||||
from .dist_attribute import OperatorDistAttr
|
||||
from .utils import (
|
||||
__no_shape_var_type__,
|
||||
convert_to_shard_spec,
|
||||
verify_shard_spec,
|
||||
)
|
||||
|
||||
|
||||
class DistributedOperator:
|
||||
def __init__(self, serial_op, dist_attr=None):
|
||||
self._serial_op = serial_op
|
||||
if dist_attr is not None and isinstance(dist_attr, OperatorDistAttr):
|
||||
# TODO: remove this deepcopy after we fix the issue
|
||||
self._dist_attr = copy.deepcopy(dist_attr)
|
||||
# self._dist_attr = dist_attr
|
||||
# TODO: Do we really need to write back to serial op?
|
||||
self._serial_op.dist_attr = dist_attr
|
||||
else:
|
||||
assert dist_attr is None, f"{dist_attr}"
|
||||
# Use the dist attr of serial_op to do the initialization
|
||||
self._dist_attr = self._serial_op.dist_attr
|
||||
self._serial_inputs = {}
|
||||
self._serial_outputs = {}
|
||||
|
||||
@property
|
||||
def serial_op(self):
|
||||
return self._serial_op
|
||||
|
||||
@property
|
||||
def dist_attr(self):
|
||||
return self._dist_attr
|
||||
|
||||
@dist_attr.setter
|
||||
def dist_attr(self, dist_attr):
|
||||
self._dist_attr = dist_attr
|
||||
# TODO: Do we really need to write back to serial op?
|
||||
self._serial_op.dist_attr = dist_attr
|
||||
|
||||
def get_serial_input(self, name):
|
||||
if self._serial_op.type == "create_py_reader":
|
||||
tensor = None
|
||||
elif self._serial_op.block._find_var_recursive(name) is not None:
|
||||
tensor = self._serial_op.block._var_recursive(name)
|
||||
else:
|
||||
tensor = None
|
||||
return tensor
|
||||
|
||||
def get_serial_output(self, name):
|
||||
tensor = self._serial_op.block._var_recursive(name)
|
||||
return tensor
|
||||
|
||||
def validate_dist_attr(self):
|
||||
if "read" in self.serial_op.type or "while" == self.serial_op.type:
|
||||
return True
|
||||
for name in self.serial_op.input_arg_names:
|
||||
input_dist_attr = self.dist_attr.get_input_dist_attr(name)
|
||||
dims_mapping = input_dist_attr.dims_mapping
|
||||
if self.get_serial_input(name).type in __no_shape_var_type__:
|
||||
shape = []
|
||||
else:
|
||||
shape = self.get_serial_input(name).shape
|
||||
if len(shape) != len(dims_mapping):
|
||||
return False
|
||||
for i in range(len(dims_mapping)):
|
||||
if dims_mapping[i] < -1 or dims_mapping[i] >= len(
|
||||
self.dist_attr.process_mesh.shape
|
||||
):
|
||||
return False
|
||||
for i in range(len(self.dist_attr.process_mesh.shape)):
|
||||
if dims_mapping.count(i) > 1:
|
||||
return False
|
||||
if self.dist_attr.process_mesh != input_dist_attr.process_mesh:
|
||||
return False
|
||||
|
||||
for name in self.serial_op.output_arg_names:
|
||||
output_dist_attr = self.dist_attr.get_output_dist_attr(name)
|
||||
dims_mapping = output_dist_attr.dims_mapping
|
||||
if self.get_serial_output(name).type in __no_shape_var_type__:
|
||||
shape = []
|
||||
else:
|
||||
shape = self.get_serial_output(name).shape
|
||||
if len(shape) != len(dims_mapping):
|
||||
return False
|
||||
for i in range(len(dims_mapping)):
|
||||
if dims_mapping[i] < -1 or dims_mapping[i] >= len(
|
||||
self.dist_attr.process_mesh.shape
|
||||
):
|
||||
return False
|
||||
for i in range(len(self.dist_attr.process_mesh.shape)):
|
||||
if dims_mapping.count(i) > 1:
|
||||
return False
|
||||
if self.dist_attr.process_mesh != output_dist_attr.process_mesh:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __str__(self):
|
||||
str = f"{{op type: {self.serial_op.desc.type()}, op id: {self.serial_op.desc.id()}, op original_id: {self.serial_op.desc.original_id()}"
|
||||
|
||||
# str += ", {}".format(self.dist_attr)
|
||||
# return str
|
||||
|
||||
if self.dist_attr.is_annotated("process_mesh"):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
str += (
|
||||
f", process_mesh ({annotated_str}): {self.dist_attr.process_mesh}"
|
||||
)
|
||||
|
||||
str += f" , execution_stream: {self.dist_attr.execution_stream}"
|
||||
|
||||
for arg_name in self.serial_op.desc.input_arg_names():
|
||||
try:
|
||||
dims_mapping = self.dist_attr.get_input_dims_mapping(arg_name)
|
||||
except IndexError:
|
||||
raise IndexError(
|
||||
f"There is not input var '{arg_name}''s dist_attr in current op '{self.serial_op.desc.type()}'"
|
||||
)
|
||||
if self.dist_attr.is_annotated_input_dims_mapping(arg_name):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
if self.get_serial_input(arg_name) is not None:
|
||||
if self.get_serial_input(arg_name).is_parameter:
|
||||
is_parameter_str = "parameter"
|
||||
else:
|
||||
is_parameter_str = "non-parameter"
|
||||
else:
|
||||
is_parameter_str = "non-parameter"
|
||||
|
||||
# partial
|
||||
input_dist_attr = self.dist_attr.get_input_dist_attr(arg_name)
|
||||
partial_dims = sorted(input_dist_attr._partial_dims())
|
||||
|
||||
str += f"; {arg_name}'s dims_mapping (input, {annotated_str}, {is_parameter_str}): {dims_mapping}, partial on dims: {partial_dims}"
|
||||
|
||||
for arg_name in self.serial_op.desc.output_arg_names():
|
||||
try:
|
||||
dims_mapping = self.dist_attr.get_output_dims_mapping(arg_name)
|
||||
except IndexError:
|
||||
raise IndexError(
|
||||
f"There is not output var '{arg_name}''s dist_attr in current op '{self.serial_op.desc.type()}'"
|
||||
)
|
||||
if self.dist_attr.is_annotated_output_dims_mapping(arg_name):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
if self.get_serial_output(arg_name) is not None:
|
||||
if self.get_serial_output(arg_name).is_parameter:
|
||||
is_parameter_str = "parameter"
|
||||
else:
|
||||
is_parameter_str = "non-parameter"
|
||||
else:
|
||||
is_parameter_str = "non-parameter"
|
||||
|
||||
# partial
|
||||
output_dist_attr = self.dist_attr.get_output_dist_attr(arg_name)
|
||||
partial_dims = sorted(output_dist_attr._partial_dims())
|
||||
|
||||
str += f"; {arg_name}'s dims_mapping (output, {annotated_str}, {is_parameter_str}): {dims_mapping}, partial on dims: {partial_dims}"
|
||||
|
||||
str += f", dist_impl idx: {self.dist_attr.impl_idx} , dist_impl type: {self.dist_attr.impl_type}, chunk_id: {self.dist_attr.chunk_id} }}"
|
||||
|
||||
return str
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
memo[id(self)] = result
|
||||
for k, v in self.__dict__.items():
|
||||
if (
|
||||
k == "_serial_op"
|
||||
or k == "_serial_inputs"
|
||||
or k == "_serial_outputs"
|
||||
):
|
||||
setattr(result, k, v)
|
||||
else:
|
||||
setattr(result, k, copy.deepcopy(v, memo))
|
||||
return result
|
||||
|
||||
|
||||
class DistributedOperatorHelper:
|
||||
def __init__(
|
||||
self,
|
||||
serial_op,
|
||||
process_mesh,
|
||||
in_dims_mappings,
|
||||
out_dims_mappings,
|
||||
kwargs,
|
||||
):
|
||||
self._serial_op = serial_op
|
||||
self._process_mesh = process_mesh
|
||||
self._in_dims_mappings = in_dims_mappings
|
||||
self._out_dims_mappings = out_dims_mappings
|
||||
self._chunk_id = kwargs["chunk_id"] if "chunk_id" in kwargs else 0
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
tensor_to_dims_mapping = {}
|
||||
index = 0
|
||||
if self._in_dims_mappings:
|
||||
assert len(args) + len(kwargs) == len(self._in_dims_mappings), (
|
||||
f"The length of dims_mapping {len(self._in_dims_mappings)} does not matching the length output {len(args) + len(kwargs)}."
|
||||
)
|
||||
for arg in args:
|
||||
if isinstance(arg, Variable) and self._in_dims_mappings:
|
||||
tensor_to_dims_mapping[arg.name] = self._in_dims_mappings[index]
|
||||
index += 1
|
||||
for arg in kwargs.values() and self._in_dims_mappings:
|
||||
if isinstance(arg, Variable):
|
||||
tensor_to_dims_mapping[arg.name] = self._in_dims_mappings[index]
|
||||
index += 1
|
||||
|
||||
default_prog = paddle.static.default_main_program()
|
||||
cur_block = default_prog.current_block()
|
||||
op_size = len(cur_block.ops)
|
||||
if paddle.base.dygraph.base.in_to_static_mode():
|
||||
output = paddle.jit.dy2static.convert_call_func.convert_call(
|
||||
self._serial_op
|
||||
)(*args, **kwargs)
|
||||
else:
|
||||
output = self._serial_op(*args, **kwargs)
|
||||
new_op_size = len(cur_block.ops)
|
||||
|
||||
if isinstance(output, (tuple, list)):
|
||||
new_output = list(output)
|
||||
elif isinstance(output, Variable):
|
||||
new_output = [output]
|
||||
else:
|
||||
raise ValueError("Unrecognized output.")
|
||||
|
||||
if self._out_dims_mappings:
|
||||
assert len(new_output) == len(self._out_dims_mappings), (
|
||||
f"The length of dims_mapping {len(self._out_dims_mappings)} does not matching the length output {len(new_output)}."
|
||||
)
|
||||
for i, item in enumerate(new_output):
|
||||
if isinstance(item, Variable) and self._out_dims_mappings:
|
||||
tensor_to_dims_mapping[item.name] = self._out_dims_mappings[i]
|
||||
|
||||
from .dist_context import get_default_distributed_context
|
||||
|
||||
default_dist_ctx = get_default_distributed_context()
|
||||
for idx in range(op_size, new_op_size):
|
||||
op = cur_block.ops[idx]
|
||||
dist_op = DistributedOperator(op)
|
||||
for name in dist_op.serial_op.input_arg_names:
|
||||
if name in tensor_to_dims_mapping.keys():
|
||||
tensor = dist_op.get_serial_input(name)
|
||||
tensor_dist_attr = dist_op.dist_attr.get_input_dist_attr(
|
||||
name
|
||||
)
|
||||
dims_mapping = tensor_to_dims_mapping[name]
|
||||
if tensor is None:
|
||||
tensor_shape = []
|
||||
else:
|
||||
if tensor.type in __no_shape_var_type__:
|
||||
tensor_shape = []
|
||||
else:
|
||||
tensor_shape = tensor.shape
|
||||
if dims_mapping is not None:
|
||||
dims_mapping = tensor_to_dims_mapping[name]
|
||||
shard_spec = convert_to_shard_spec(
|
||||
dims_mapping, self._process_mesh
|
||||
)
|
||||
assert verify_shard_spec(
|
||||
shard_spec, tensor_shape, self._process_mesh
|
||||
), (
|
||||
f"For tensor {name}, shard_spec {shard_spec} is invalid with tensor_shape {tensor_shape} and process_mesh {self._process_mesh}."
|
||||
)
|
||||
tensor_dist_attr.dims_mapping = dims_mapping
|
||||
tensor_dist_attr.mark_annotated("dims_mapping")
|
||||
for name in dist_op.serial_op.output_arg_names:
|
||||
if name in tensor_to_dims_mapping.keys():
|
||||
tensor = dist_op.get_serial_output(name)
|
||||
tensor_dist_attr = dist_op.dist_attr.get_output_dist_attr(
|
||||
name
|
||||
)
|
||||
dims_mapping = tensor_to_dims_mapping[name]
|
||||
if tensor is None:
|
||||
tensor_shape = []
|
||||
else:
|
||||
if tensor.type in __no_shape_var_type__:
|
||||
tensor_shape = []
|
||||
else:
|
||||
tensor_shape = tensor.shape
|
||||
if dims_mapping is not None:
|
||||
dims_mapping = tensor_to_dims_mapping[name]
|
||||
shard_spec = convert_to_shard_spec(
|
||||
dims_mapping, self._process_mesh
|
||||
)
|
||||
assert verify_shard_spec(
|
||||
shard_spec, tensor_shape, self._process_mesh
|
||||
), (
|
||||
f"For tensor {name}, shard_spec {shard_spec} is invalid with tensor_shape {tensor_shape} and process_mesh {self._process_mesh}."
|
||||
)
|
||||
tensor_dist_attr.dims_mapping = dims_mapping
|
||||
tensor_dist_attr.mark_annotated("dims_mapping")
|
||||
dist_op.dist_attr.process_mesh = self._process_mesh
|
||||
dist_op.dist_attr.chunk_id = self._chunk_id
|
||||
if self._process_mesh is not None:
|
||||
dist_op.dist_attr.mark_annotated("process_mesh")
|
||||
default_dist_ctx.add_dist_op_for_program(dist_op)
|
||||
default_dist_ctx.add_process_mesh(self._process_mesh)
|
||||
|
||||
return output
|
||||
@@ -0,0 +1,261 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import errno
|
||||
import logging
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
|
||||
import numpy as np
|
||||
|
||||
import paddle
|
||||
from paddle.framework import core
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
from .process_group import _g_process_group_map
|
||||
from .utils import get_dist_attr
|
||||
|
||||
|
||||
def check_filename(re_exp, filename):
|
||||
if re.search(re_exp, filename):
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def _process_path(path):
|
||||
filename = os.path.basename(path)
|
||||
if filename == "":
|
||||
raise ValueError(
|
||||
"path should be of 'dirname/filename' format, but received filename is empty string"
|
||||
)
|
||||
try:
|
||||
dirname = os.path.dirname(path)
|
||||
os.makedirs(dirname)
|
||||
except OSError as e:
|
||||
if e.errno != errno.EEXIST:
|
||||
raise
|
||||
return dirname, filename
|
||||
|
||||
|
||||
class DistributedSaver:
|
||||
def __init__(self):
|
||||
self._logger = get_logger(logging.INFO)
|
||||
|
||||
def save(self, path, serial_program, dist_main_program, dist_context):
|
||||
def _save_state(program, path, mode="param"):
|
||||
state = {
|
||||
k: np.array(v) for k, v in program.state_dict(mode).items()
|
||||
}
|
||||
with open(path, "wb") as f:
|
||||
pickle.dump(state, f)
|
||||
|
||||
dirname, filename = _process_path(path)
|
||||
|
||||
rank_id = paddle.distributed.get_rank()
|
||||
# save serial program when rank id is 0
|
||||
if rank_id == 0:
|
||||
self._save_rank_mapping(dirname)
|
||||
serial_model_filename = filename + "_serial.pdmodel"
|
||||
serial_model_path = os.path.join(dirname, serial_model_filename)
|
||||
with open(serial_model_path, "wb") as f:
|
||||
f.write(serial_program.desc.serialize_to_string())
|
||||
|
||||
# save distributed main program
|
||||
dist_model_filename = filename + "_dist" + str(rank_id) + ".pdmodel"
|
||||
dist_model_path = os.path.join(dirname, dist_model_filename)
|
||||
with open(dist_model_path, "wb") as f:
|
||||
f.write(dist_main_program.desc.serialize_to_string())
|
||||
|
||||
# save distributed attribute
|
||||
dist_attr_filename = filename + "_dist" + str(rank_id) + ".pdattr"
|
||||
dist_attr_path = os.path.join(dirname, dist_attr_filename)
|
||||
dist_attrs = get_dist_attr(dist_main_program, dist_context)
|
||||
with open(dist_attr_path, "wb") as f:
|
||||
pickle.dump(dist_attrs, f)
|
||||
|
||||
# save distributed params
|
||||
dist_param_filename = filename + "_dist" + str(rank_id) + ".pdparams"
|
||||
dist_param_path = os.path.join(dirname, dist_param_filename)
|
||||
_save_state(dist_main_program, dist_param_path)
|
||||
|
||||
# save distributed opt states
|
||||
dist_opt_filename = filename + "_dist" + str(rank_id) + ".pdopt"
|
||||
dist_opt_path = os.path.join(dirname, dist_opt_filename)
|
||||
_save_state(dist_main_program, dist_opt_path, "opt")
|
||||
|
||||
# TODO:save cluster.json
|
||||
|
||||
def load(self, path, load_optimizer=True):
|
||||
# TODO: if `program` is None, load `path.pdmodel`.
|
||||
def _load_file(filename, dirname, suffix="pdparams"):
|
||||
file_list = []
|
||||
for file in os.listdir(dirname):
|
||||
if check_filename(f'{filename}(.*)_dist(.*).{suffix}', file):
|
||||
file_list.append(os.path.join(dirname, file))
|
||||
file_list.sort()
|
||||
return file_list
|
||||
|
||||
def _load_state(filename, dirname, suffix="pdparams"):
|
||||
file_list = _load_file(filename, dirname, suffix)
|
||||
state_dict = {}
|
||||
for file in file_list:
|
||||
with open(file, 'rb') as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
state_dict_info = safe_load_pickle(f, encoding='latin1')
|
||||
for name, value in state_dict_info.items():
|
||||
if name in state_dict:
|
||||
state_dict[name].append(np.array(value))
|
||||
else:
|
||||
state_dict[name] = [np.array(value)]
|
||||
self._logger.info(f"Load param file: {file_list}")
|
||||
return state_dict
|
||||
|
||||
filename = os.path.basename(path)
|
||||
if filename == "":
|
||||
raise ValueError(
|
||||
"path should be of 'dirname/filename' format, but received filename is empty string"
|
||||
)
|
||||
dirname = os.path.dirname(path)
|
||||
|
||||
# load path.pdparam and path.pdopt
|
||||
param_state_dict = _load_state(filename, dirname)
|
||||
opt_state_dict = (
|
||||
_load_state(filename, dirname, "pdopt") if load_optimizer else {}
|
||||
)
|
||||
state_dict = dict(param_state_dict, **opt_state_dict)
|
||||
|
||||
# load path.pdattr
|
||||
dist_attr_file_list = _load_file(filename, dirname, "pdattr")
|
||||
self._logger.info(
|
||||
f"Load distributed attribute file: {dist_attr_file_list}"
|
||||
)
|
||||
dist_attr = {}
|
||||
for dist_attr_file in dist_attr_file_list:
|
||||
with open(dist_attr_file, 'rb') as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
dist_attr_info = safe_load_pickle(f, encoding='latin1')
|
||||
for name, attr in dist_attr_info.items():
|
||||
if name not in dist_attr:
|
||||
dist_attr[name] = attr
|
||||
|
||||
return state_dict, dist_attr
|
||||
|
||||
def save_inference_model(self, path, feed_vars, fetch_vars, exe, **kwargs):
|
||||
dirname, filename = _process_path(path)
|
||||
|
||||
# save distributed inference program
|
||||
rank_id = paddle.distributed.get_rank()
|
||||
if rank_id == 0:
|
||||
self._save_rank_mapping(dirname)
|
||||
op_role_key = core.op_proto_and_checker_maker.kOpRoleAttrName()
|
||||
op_role_forward = int(core.op_proto_and_checker_maker.OpRole.Forward)
|
||||
|
||||
dist_main_prog = kwargs.get('program', None)
|
||||
if not dist_main_prog:
|
||||
dist_main_prog = paddle.static.default_main_program()
|
||||
global_block = dist_main_prog.global_block()
|
||||
|
||||
ops = global_block.ops
|
||||
feed_vars_names = [x.name for x in feed_vars]
|
||||
fetch_vars_names = [x.name for x in fetch_vars]
|
||||
|
||||
last_idx = -1
|
||||
for idx, op in enumerate(ops):
|
||||
if op.attr(op_role_key) != op_role_forward:
|
||||
continue
|
||||
if op.type == "read" or op.type == "feed" or op.type == 'recv_v2':
|
||||
feed_vars_names += op.output("Out")
|
||||
if op.type == "send_v2":
|
||||
fetch_vars_names += op.input("X")
|
||||
last_idx = max(idx, last_idx)
|
||||
for out_name in op.output_arg_names:
|
||||
if out_name in fetch_vars_names:
|
||||
last_idx = max(idx, last_idx)
|
||||
|
||||
used_inputs = []
|
||||
used_outputs = []
|
||||
for idx, op in enumerate(ops):
|
||||
if idx > last_idx:
|
||||
break
|
||||
used_inputs += op.input_arg_names
|
||||
used_outputs += op.output_arg_names
|
||||
|
||||
# delete duplicated elements and keep order
|
||||
feed_vars_names = list({}.fromkeys(feed_vars_names).keys())
|
||||
used_inputs = list({}.fromkeys(used_inputs).keys())
|
||||
fetch_vars_names = list({}.fromkeys(fetch_vars_names).keys())
|
||||
used_outputs = list({}.fromkeys(used_outputs).keys())
|
||||
|
||||
dist_feed_vars_names = [
|
||||
var_name for var_name in feed_vars_names if var_name in used_inputs
|
||||
]
|
||||
dist_fetch_vars_names = [
|
||||
var_name
|
||||
for var_name in fetch_vars_names
|
||||
if var_name in used_outputs
|
||||
]
|
||||
|
||||
dist_feed_vars = list(
|
||||
reversed([global_block.vars[name] for name in dist_feed_vars_names])
|
||||
)
|
||||
dist_fetch_vars = [
|
||||
global_block.vars[name] for name in dist_fetch_vars_names
|
||||
]
|
||||
|
||||
dist_filename = filename + "_dist" + str(rank_id)
|
||||
dist_path = os.path.join(dirname, dist_filename)
|
||||
legacy_format = kwargs.get("legacy_format", False)
|
||||
paddle.static.save_inference_model(
|
||||
dist_path,
|
||||
dist_feed_vars,
|
||||
dist_fetch_vars,
|
||||
exe,
|
||||
program=dist_main_prog,
|
||||
legacy_format=legacy_format,
|
||||
)
|
||||
|
||||
def _save_rank_mapping(self, dirname):
|
||||
path = os.path.join(dirname, 'rank_mapping.csv')
|
||||
f = open(path, 'w')
|
||||
f.write('[ring_id -> ranks]\n')
|
||||
for process_group in _g_process_group_map.values():
|
||||
ring_id = process_group._group_id
|
||||
ranks = [str(rank) for rank in process_group._ranks]
|
||||
id_to_rank = str(ring_id) + "," + ",".join(ranks) + '\n'
|
||||
f.write(id_to_rank)
|
||||
id_to_rank = ""
|
||||
f.write('[rank -> ring_ids]\n')
|
||||
rank_to_id_dict = {}
|
||||
for process_group in _g_process_group_map.values():
|
||||
ring_id = process_group._group_id
|
||||
for rank in process_group._ranks:
|
||||
if rank in rank_to_id_dict:
|
||||
rank_to_id_dict[rank].append(str(ring_id))
|
||||
else:
|
||||
rank_to_id_dict[rank] = [str(ring_id)]
|
||||
rank_to_id = ""
|
||||
for item, val in rank_to_id_dict.items():
|
||||
rank_to_id += str(item) + ","
|
||||
rank_to_id += ",".join(val) + "\n"
|
||||
f.write(rank_to_id)
|
||||
rank_to_id = ""
|
||||
f.close()
|
||||
@@ -0,0 +1,412 @@
|
||||
# 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 copy
|
||||
import inspect
|
||||
|
||||
import paddle
|
||||
from paddle.framework import Block
|
||||
from paddle.static import Parameter, Variable
|
||||
|
||||
from .dist_attribute import TensorDistAttr
|
||||
from .utils import __no_shape_var_type__, _linear_idx2coordinate
|
||||
|
||||
|
||||
class DistributedTensor:
|
||||
"""
|
||||
DistributedTensor represents the distribution of tensor on the process group and
|
||||
local tensors can be created by DistributedTensor.
|
||||
Only support even sharding now and uneven sharding will be supported in the future.
|
||||
Local tensor information can be obtained from the DistributedTensor instance object,
|
||||
or obtained by the static methods provided by DistributedTensor,
|
||||
including shard (i.e. the index in the serial tensor), offsets, and sizes.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def _validate_sizes_and_dist_attr(
|
||||
sizes, dims_mapping, topology, processes, rank=None, shard_sizes=None
|
||||
):
|
||||
if not (
|
||||
isinstance(sizes, (list, tuple))
|
||||
and all(isinstance(x, int) and x >= 0 for x in sizes)
|
||||
):
|
||||
raise ValueError(
|
||||
f"The sizes must be list or tuple and item in sizes must be non-negative integer, but got {sizes}"
|
||||
)
|
||||
if not (
|
||||
isinstance(dims_mapping, (list, tuple))
|
||||
and all(isinstance(x, int) and x >= -1 for x in dims_mapping)
|
||||
):
|
||||
raise ValueError(
|
||||
f"The dims_mapping must be list or tuple and item in dims_mapping must >= -1, but got {dims_mapping}"
|
||||
)
|
||||
if not (
|
||||
isinstance(processes, (list, tuple))
|
||||
and all(isinstance(x, int) and x >= 0 for x in processes)
|
||||
):
|
||||
raise ValueError(
|
||||
f"The processes must be list or tuple and item in processes must be integer, but got {processes}"
|
||||
)
|
||||
if not (
|
||||
isinstance(topology, (list, tuple))
|
||||
and all(isinstance(x, int) and x > 0 for x in topology)
|
||||
):
|
||||
raise ValueError(
|
||||
f"The topology must be list or tuple and item in topology must be non-negative integer, but got {topology}"
|
||||
)
|
||||
if rank is not None and not (isinstance(rank, int) and rank >= 0):
|
||||
raise ValueError(f"The rank must >= 0, but got {rank}")
|
||||
|
||||
# # NOTE: Only support even sharding now
|
||||
# if shard_sizes is not None:
|
||||
# raise ValueError("Only support even sharding now.")
|
||||
|
||||
@staticmethod
|
||||
def get_local_sizes(
|
||||
global_sizes,
|
||||
dims_mapping,
|
||||
topology,
|
||||
processes,
|
||||
rank=None,
|
||||
shard_sizes=None,
|
||||
):
|
||||
DistributedTensor._validate_sizes_and_dist_attr(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
|
||||
local_sizes = []
|
||||
# for even sharding, the local sizes of every rank are equal
|
||||
|
||||
for idx, item in enumerate(global_sizes):
|
||||
# This is a trick to avoid dims_mapping is []
|
||||
val = dims_mapping[idx] if idx < len(dims_mapping) else -1
|
||||
if val == -1:
|
||||
local_sizes.append(item)
|
||||
else:
|
||||
local_sizes.append(item // topology[dims_mapping[idx]])
|
||||
|
||||
return local_sizes
|
||||
|
||||
@staticmethod
|
||||
def get_local_offsets(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes=None
|
||||
):
|
||||
local_sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
local_offsets = []
|
||||
rank_relative = processes.index(rank)
|
||||
coordinate = _linear_idx2coordinate(topology, rank_relative)
|
||||
|
||||
for i in range(len(global_sizes)):
|
||||
if dims_mapping[i] == -1:
|
||||
local_offsets.append(0)
|
||||
else:
|
||||
local_offsets.append(
|
||||
coordinate[dims_mapping[i]] * local_sizes[i]
|
||||
)
|
||||
return local_offsets
|
||||
|
||||
@staticmethod
|
||||
def get_global_sizes(
|
||||
local_sizes,
|
||||
dims_mapping,
|
||||
topology,
|
||||
processes,
|
||||
rank=None,
|
||||
shard_sizes=None,
|
||||
):
|
||||
DistributedTensor._validate_sizes_and_dist_attr(
|
||||
local_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
global_sizes = []
|
||||
for idx, item in enumerate(local_sizes):
|
||||
if dims_mapping[idx] == -1:
|
||||
global_sizes.append(item)
|
||||
else:
|
||||
global_sizes.append(item * topology[dims_mapping[idx]])
|
||||
return global_sizes
|
||||
|
||||
@staticmethod
|
||||
def get_local_shard(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes=None
|
||||
):
|
||||
local_offsets = DistributedTensor.get_local_offsets(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
local_sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes, dims_mapping, topology, processes, rank, shard_sizes
|
||||
)
|
||||
assert len(local_sizes) == len(local_offsets), (
|
||||
f"The length of local_sizes must be equal to local_offsets, but got {len(local_sizes)} and {len(local_offsets)}."
|
||||
)
|
||||
|
||||
local_end_offsets = [
|
||||
x[0] + x[1] for x in zip(local_offsets, local_sizes)
|
||||
]
|
||||
local_shard = list(zip(local_offsets, local_end_offsets))
|
||||
return local_shard
|
||||
|
||||
def __init__(self, serial_tensor, dist_attr=None, dist_context=None):
|
||||
self._serial_tensor = serial_tensor
|
||||
if dist_attr is not None and isinstance(dist_attr, TensorDistAttr):
|
||||
# TODO: remove this deepcopy after we fix the issue
|
||||
self._dist_attr = copy.deepcopy(dist_attr)
|
||||
# self._dist_attr = dist_attr
|
||||
# TODO: Do we really need to write dist_attr back to serial_tensor?
|
||||
self._serial_tensor.dist_attr = dist_attr
|
||||
else:
|
||||
assert dist_attr is None, f"{dist_attr}"
|
||||
# Use the dist attr of serial_tensor to do the initialization
|
||||
self._dist_attr = self._serial_tensor.dist_attr
|
||||
|
||||
self._batch_dim = 0
|
||||
self._local_offsets_map = {}
|
||||
self._local_shard_map = {}
|
||||
self._local_tensor_map = {}
|
||||
|
||||
from .dist_context import get_default_distributed_context
|
||||
|
||||
self._dist_context = (
|
||||
dist_context
|
||||
if dist_context is not None
|
||||
else get_default_distributed_context()
|
||||
)
|
||||
# TODO: Add Automatically to dist_context after initialized and it will be adapted in the future.
|
||||
# self._dist_context.add_dist_tensor_for_program(self)
|
||||
|
||||
@property
|
||||
def serial_tensor(self):
|
||||
return self._serial_tensor
|
||||
|
||||
@property
|
||||
def dist_attr(self):
|
||||
return self._dist_attr
|
||||
|
||||
@dist_attr.setter
|
||||
def dist_attr(self, dist_attr):
|
||||
self._dist_attr = dist_attr
|
||||
# TODO: Do we really need to write back dist_attr to serial_tensor?
|
||||
self._serial_tensor.dist_attr = dist_attr
|
||||
|
||||
@property
|
||||
def dist_context(self):
|
||||
return self._dist_context
|
||||
|
||||
# def _init_default_dist_attr(self):
|
||||
# if self._dist_attr.dims_mapping is None:
|
||||
# if self.serial_tensor.type in __no_shape_var_type__:
|
||||
# tensor_shape = []
|
||||
# else:
|
||||
# tensor_shape = self._serial_tensor.shape
|
||||
# tensor_dims_mapping = [-1 for _ in range(len(tensor_shape))]
|
||||
# self._dist_attr.dims_mapping = tensor_dims_mapping
|
||||
|
||||
def validate_dist_attr(self):
|
||||
if self.serial_tensor.type in __no_shape_var_type__:
|
||||
return True
|
||||
tensor_shape = self.serial_tensor.shape
|
||||
if len(tensor_shape) != len(self.dist_attr.dims_mapping):
|
||||
return False
|
||||
for i in range(len(self.dist_attr.dims_mapping)):
|
||||
if self.dist_attr.dims_mapping[
|
||||
i
|
||||
] < -1 or self.dist_attr.dims_mapping[i] >= len(
|
||||
self.dist_attr.process_mesh.shape
|
||||
):
|
||||
return False
|
||||
for i in range(len(self.dist_attr.process_mesh.shape)):
|
||||
if self.dist_attr.dims_mapping.count(i) > 1:
|
||||
return False
|
||||
return True
|
||||
|
||||
def local_sizes(self, rank=None):
|
||||
"""Get local sizes of the given rank."""
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
global_sizes = self.serial_tensor.shape
|
||||
dims_mapping = self.dist_attr.dims_mapping
|
||||
# shard_sizes = self.dist_attr.shard_sizes
|
||||
processes = self.dist_attr.process_mesh.process_ids
|
||||
topology = self.dist_attr.process_mesh.shape
|
||||
local_sizes = DistributedTensor.get_local_sizes(
|
||||
global_sizes, dims_mapping, topology, processes, rank
|
||||
)
|
||||
|
||||
return local_sizes
|
||||
|
||||
def local_offsets(self, rank=None):
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
local_offsets = None
|
||||
if rank in self._local_offsets_map.keys():
|
||||
local_offsets = self._local_offsets_map[rank]
|
||||
else:
|
||||
global_sizes = self.serial_tensor.shape
|
||||
dims_mapping = self.dist_attr.dims_mapping
|
||||
# shard_sizes = self.dist_attr.shard_sizes
|
||||
processes = self.dist_attr.process_mesh.process_ids
|
||||
topology = self.dist_attr.process_mesh.shape
|
||||
local_offsets = DistributedTensor.get_local_offsets(
|
||||
global_sizes, dims_mapping, topology, processes, rank
|
||||
)
|
||||
self._local_offsets_map[rank] = local_offsets
|
||||
|
||||
return local_offsets
|
||||
|
||||
def global_sizes(self):
|
||||
return self.serial_tensor.shape
|
||||
|
||||
def local_shard(self, rank=None):
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
local_shard = None
|
||||
if rank in self._local_shard_map.keys():
|
||||
local_shard = self._local_shard_map[rank]
|
||||
else:
|
||||
global_sizes = self.serial_tensor.shape
|
||||
dims_mapping = self.dist_attr.dims_mapping
|
||||
# shard_sizes = self.dist_attr.shard_sizes
|
||||
processes = self.dist_attr.process_mesh.process_ids
|
||||
topology = self.dist_attr.process_mesh.shape
|
||||
local_shard = DistributedTensor.get_local_shard(
|
||||
global_sizes, dims_mapping, topology, processes, rank
|
||||
)
|
||||
self._local_shard_map[rank] = local_shard
|
||||
|
||||
return local_shard
|
||||
|
||||
def new_local_tensor(self, block=None, rank=None, name=None):
|
||||
"""
|
||||
Create a new local tensor of serial tensor corresponding to rank.
|
||||
Args:
|
||||
block (Block): The block contains the new tensor. Default value is recommend and it will be created in the block of dist main program corresponding to the serial tensor block id. Default: None.
|
||||
rank (int): The rank id. Default value is recommend and it will be the current rank. Default: None.
|
||||
"""
|
||||
|
||||
def _copy_kwargs(serial_tensor):
|
||||
kwargs = {}
|
||||
no_need_copy_args = ["self", "block", "shape", "name"]
|
||||
arg_spec = inspect.getfullargspec(Variable.__init__)
|
||||
|
||||
for key in arg_spec.args:
|
||||
# TODO: Check the copied attribute from serial tensor whether valid
|
||||
if key in no_need_copy_args:
|
||||
continue
|
||||
elif key not in kwargs:
|
||||
if key == "type":
|
||||
kwargs[key] = serial_tensor.desc.type()
|
||||
elif key == "dtype":
|
||||
kwargs[key] = serial_tensor.desc.dtype()
|
||||
elif key == "lod_level":
|
||||
kwargs[key] = serial_tensor.desc.lod_level()
|
||||
elif key == "persistable":
|
||||
kwargs[key] = serial_tensor.desc.persistable()
|
||||
elif key == "stop_gradient":
|
||||
kwargs[key] = serial_tensor.desc.stop_gradient()
|
||||
elif key == "need_check_feed":
|
||||
kwargs[key] = serial_tensor.desc.need_check_feed()
|
||||
# TODO: Get capacity by framework
|
||||
elif key == "capacity":
|
||||
continue
|
||||
else:
|
||||
kwargs[key] = self.serial_tensor.__dict__[key]
|
||||
|
||||
if isinstance(serial_tensor, Parameter):
|
||||
kwargs["trainable"] = serial_tensor.trainable
|
||||
kwargs["optimize_attr"] = serial_tensor.trainable
|
||||
kwargs["regularizer"] = serial_tensor.regularizer
|
||||
kwargs["do_model_average"] = serial_tensor.do_model_average
|
||||
kwargs["need_clip"] = serial_tensor.need_clip
|
||||
kwargs["is_distributed"] = serial_tensor.is_distributed
|
||||
kwargs["is_parameter"] = serial_tensor.is_parameter
|
||||
|
||||
return kwargs
|
||||
|
||||
if rank is not None and not (isinstance(rank, int) and rank >= 0):
|
||||
raise ValueError(f"The rank must >= 0, but got {rank}")
|
||||
if block is not None and not isinstance(block, Block):
|
||||
raise TypeError(f"The block must be Block, but got {type(block)}.")
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
|
||||
if block is None:
|
||||
block_id = self.serial_tensor.block.idx
|
||||
block = self.dist_context.dist_main_programs[rank].block(block_id)
|
||||
|
||||
# copy serial tensor attribute
|
||||
kwargs = _copy_kwargs(self.serial_tensor)
|
||||
kwargs["name"] = name
|
||||
kwargs["shape"] = self.local_sizes(rank)
|
||||
|
||||
if isinstance(self.serial_tensor, Parameter):
|
||||
kwargs.pop("persistable")
|
||||
local_tensor = Parameter(block=block, **kwargs)
|
||||
else:
|
||||
local_tensor = block.create_var(**kwargs)
|
||||
|
||||
# TODO: Set original id when set original_id is approved
|
||||
local_tensor.desc.set_original_id(self.serial_tensor.desc.id())
|
||||
self._local_tensor_map[rank] = local_tensor
|
||||
return local_tensor
|
||||
|
||||
def local_tensor(self, rank=None):
|
||||
rank = paddle.distributed.get_rank() if rank is None else rank
|
||||
assert rank in self._local_tensor_map, (
|
||||
f"The rank {rank} local tensor has not been created."
|
||||
)
|
||||
return self._local_tensor_map[rank]
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
memo[id(self)] = result
|
||||
for k, v in self.__dict__.items():
|
||||
if k == "_serial_tensor" or k == "_local_tensor_map":
|
||||
setattr(result, k, v)
|
||||
else:
|
||||
setattr(result, k, copy.deepcopy(v, memo))
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
str = f"{{tensor name: {self.serial_tensor.desc.name()}, tensor id: {self.serial_tensor.desc.id()}, tensor original_id {self.serial_tensor.desc.original_id()}"
|
||||
|
||||
# str += ", {}".format(self.dist_attr)
|
||||
# return str
|
||||
|
||||
if self.dist_attr.is_annotated("process_mesh"):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
str += (
|
||||
f", process_mesh ({annotated_str}): {self.dist_attr.process_mesh}"
|
||||
)
|
||||
|
||||
str += f", is_parameter: {self.serial_tensor.is_parameter}"
|
||||
str += f", chunk_id: {self.dist_attr.chunk_id}"
|
||||
|
||||
if self.dist_attr.is_annotated("dims_mapping"):
|
||||
annotated_str = "annotated"
|
||||
else:
|
||||
annotated_str = "non-annotated"
|
||||
str += f", dims_mapping ({annotated_str}): {self.dist_attr.dims_mapping} }}"
|
||||
|
||||
# if self.dist_attr.is_annotated("shard_mask"):
|
||||
# annotated_str = "annotated"
|
||||
# else:
|
||||
# annotated_str = "non-annotated"
|
||||
# str += ", shard_mask ({}): {}".format(annotated_str, None)
|
||||
|
||||
# if self.dist_attr.is_annotated("offload_device"):
|
||||
# annotated_str = "annotated"
|
||||
# else:
|
||||
# annotated_str = "non-annotated"
|
||||
# str += ", offload_device ({}): {} }}".format(annotated_str, None)
|
||||
return str
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,186 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
|
||||
class Node:
|
||||
def __init__(self, id, **attrs):
|
||||
# Each node must has a unique id
|
||||
self._id = id
|
||||
# Attributes for Node
|
||||
self._attrs = {}
|
||||
self._attrs.update(attrs)
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def attrs(self):
|
||||
return self._attrs
|
||||
|
||||
def __getitem__(self, attr_name):
|
||||
return self._attrs[attr_name]
|
||||
|
||||
def __setitem__(self, attr_name, attr_value):
|
||||
self._attrs[attr_name] = attr_value
|
||||
|
||||
def __contains__(self, attr_name):
|
||||
try:
|
||||
return attr_name in self._attrs
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
str = f"(id: {self.id}, attrs: {self.attrs})"
|
||||
return str
|
||||
|
||||
|
||||
class Edge:
|
||||
def __init__(self, src_id, tgt_id, **attrs):
|
||||
# The id of source node in an Edge
|
||||
self._src_id = src_id
|
||||
# The id of target node in an Edge
|
||||
self._tgt_id = tgt_id
|
||||
# Attributes for Edge
|
||||
self._attrs = {}
|
||||
self._attrs.update(attrs)
|
||||
|
||||
@property
|
||||
def src_id(self):
|
||||
return self._src_id
|
||||
|
||||
@property
|
||||
def tgt_id(self):
|
||||
return self._tgt_id
|
||||
|
||||
@property
|
||||
def attrs(self):
|
||||
return self._attrs
|
||||
|
||||
def __getitem__(self, attr_name):
|
||||
return self._attrs[attr_name]
|
||||
|
||||
def __setitem__(self, attr_name, attr_value):
|
||||
self._attrs[attr_name] = attr_value
|
||||
|
||||
def __contains__(self, attr_name):
|
||||
try:
|
||||
return attr_name in self._attrs
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
str = ""
|
||||
str += f"(src_id: {self.src_id}, tgt_id: {self.tgt_id}, attrs: {self._attrs})"
|
||||
return str
|
||||
|
||||
|
||||
class Graph:
|
||||
def __init__(self, **attrs):
|
||||
# _nodes is dict for storing the nodes of the graph.
|
||||
# The key of this dict is the node id.
|
||||
self._nodes = {}
|
||||
# _adjs is a dict of dict for storing the adjacency of the graph.
|
||||
# The key of the outer dict is the node id of the source node and
|
||||
# the key of the inner dict is the node id of the target node.
|
||||
self._adjs = {}
|
||||
# Attributes for Graph
|
||||
self._attrs = {}
|
||||
self._attrs.update(attrs)
|
||||
self._reverse_adjs = {}
|
||||
self._attr_to_nodes = {}
|
||||
|
||||
@property
|
||||
def nodes(self):
|
||||
return self._nodes
|
||||
|
||||
@property
|
||||
def attrs(self):
|
||||
return self._attrs
|
||||
|
||||
@property
|
||||
def adjs(self):
|
||||
return self._adjs
|
||||
|
||||
def add_node(self, node_id, **attrs):
|
||||
if node_id is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
if node_id not in self._nodes:
|
||||
node = Node(node_id, **attrs)
|
||||
self._nodes[node_id] = node
|
||||
self._adjs[node_id] = {}
|
||||
self._reverse_adjs[node_id] = []
|
||||
else:
|
||||
self._nodes[node_id].attrs.update(attrs)
|
||||
|
||||
return self._nodes[node_id]
|
||||
|
||||
def add_edge(self, src_id, tgt_id, **attrs):
|
||||
# add nodes
|
||||
if src_id is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
if tgt_id is None:
|
||||
raise ValueError("None cannot be a node")
|
||||
if src_id not in self._nodes:
|
||||
src_node = Node(src_id)
|
||||
self._nodes[src_id] = src_node
|
||||
# for one tensor to multiple ops
|
||||
self._adjs[src_id] = OrderedDict()
|
||||
self._reverse_adjs[src_id] = []
|
||||
if tgt_id not in self._nodes:
|
||||
tgt_node = Node(tgt_id)
|
||||
self._nodes[tgt_id] = tgt_node
|
||||
# for one tensor to multiple ops
|
||||
self._adjs[tgt_id] = OrderedDict()
|
||||
self._reverse_adjs[tgt_id] = []
|
||||
# add the edge
|
||||
edge = Edge(src_id, tgt_id, **attrs)
|
||||
self._adjs[src_id][tgt_id] = edge
|
||||
|
||||
# add the reverse adj
|
||||
self._reverse_adjs[tgt_id].append(self.nodes[src_id])
|
||||
return edge
|
||||
|
||||
def __len__(self):
|
||||
return len(self._nodes)
|
||||
|
||||
def __iter__(self):
|
||||
return iter(self._nodes.values())
|
||||
|
||||
def __getitem__(self, node_id):
|
||||
# Return the adjacency of a node
|
||||
return self._adjs[node_id]
|
||||
|
||||
def __contains__(self, node_id):
|
||||
# Check whether a node in the graph
|
||||
try:
|
||||
return node_id in self._nodes
|
||||
except TypeError:
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
str = ""
|
||||
str += "**************Nodes**************\n"
|
||||
for node_id in self.nodes:
|
||||
str += f"{self.nodes[node_id]}\n"
|
||||
|
||||
str += "**************Edges**************\n"
|
||||
for src_id in self.adjs:
|
||||
str += f"--------------{src_id}--------------\n"
|
||||
for idx, tgt_id in enumerate(self.adjs[src_id]):
|
||||
str += f"{self.adjs[src_id][tgt_id]}\n"
|
||||
|
||||
return str
|
||||
@@ -0,0 +1,673 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import inspect
|
||||
import logging
|
||||
from collections import defaultdict
|
||||
|
||||
import paddle
|
||||
from paddle import core
|
||||
from paddle.jit import not_to_static, to_static
|
||||
from paddle.jit.dy2static.program_translator import (
|
||||
ProgramTranslator,
|
||||
StaticFunction,
|
||||
)
|
||||
from paddle.jit.dy2static.utils import as_not_paddle_func
|
||||
from paddle.nn import Layer
|
||||
from paddle.static import Parameter, global_scope, program_guard
|
||||
from paddle.static.amp.fp16_utils import (
|
||||
DEFAULT_AMP_OPTIONS,
|
||||
prepare_op_amp_options,
|
||||
)
|
||||
|
||||
from .converter import Converter
|
||||
from .dist_attribute import TensorDistAttr
|
||||
from .process_group import get_world_process_group
|
||||
from .utils import get_logger, to_list
|
||||
|
||||
|
||||
class ProxyLayer(Layer):
|
||||
"""
|
||||
ProxyLayer implements all logic for converting dygraph model into
|
||||
static Program IR. Meanwhile, it provides conventional interfaces for
|
||||
auto parallel to visit feed/fetch/loss/metric variables.
|
||||
"""
|
||||
|
||||
def __init__(self, layer, loss_func, metrics):
|
||||
super().__init__()
|
||||
# NOTE: All verify logics are finished in Engine.Prepare
|
||||
self.inner_layer = layer
|
||||
self.loss_func = loss_func
|
||||
self.metrics = metrics
|
||||
# train / eval / predict
|
||||
self.mode = None
|
||||
|
||||
# generated program vars
|
||||
self._input_vars = defaultdict(list)
|
||||
self._label_vars = defaultdict(list)
|
||||
self._output_vars = defaultdict(list)
|
||||
self._loss_vars = defaultdict(list)
|
||||
self._loss_names = defaultdict(list)
|
||||
self._metric_vars = defaultdict(list)
|
||||
|
||||
# Consider ProxyLayer as not Paddle inner function because it contains
|
||||
# user-defined layer.
|
||||
for fn_name in [
|
||||
"_train",
|
||||
"_eval",
|
||||
"_predict",
|
||||
"call_loss",
|
||||
"call_metrics",
|
||||
]:
|
||||
as_not_paddle_func(
|
||||
f"{inspect.getmodule(ProxyLayer).__name__}.ProxyLayer.{fn_name}"
|
||||
)
|
||||
|
||||
@paddle.jit.not_to_static
|
||||
def append_loss_to_shadow_output(self, mode):
|
||||
name = paddle.utils.unique_name.generate('loss')
|
||||
paddle._C_ops.set_persistable_value(self._loss_vars[mode], name)
|
||||
self._loss_names[mode] = name
|
||||
|
||||
def _train(self, inputs, labels):
|
||||
"""
|
||||
Train process of inner_layer with forward/loss/metric logic.
|
||||
"""
|
||||
# step 1. save feed variables of Program
|
||||
mode = 'train'
|
||||
self._input_vars[mode] = inputs
|
||||
self._label_vars[mode] = labels
|
||||
|
||||
# step 2. call inner_layer.forward
|
||||
self._output_vars[mode] = self.inner_layer(*inputs)
|
||||
|
||||
# step 3. calculate loss if needed
|
||||
new_inputs = self._prepare(self.output_vars, labels)
|
||||
self._loss_vars[mode] = self.call_loss(new_inputs)
|
||||
if paddle.base.framework.get_flags("FLAGS_enable_pir_api")[
|
||||
"FLAGS_enable_pir_api"
|
||||
]:
|
||||
self.append_loss_to_shadow_output(mode)
|
||||
|
||||
# step 4. calculate metrics if needed
|
||||
self._metric_vars[mode] = self.call_metrics(new_inputs)
|
||||
|
||||
def _eval(self, inputs, labels):
|
||||
"""
|
||||
Evaluate process of inner_layer with forward/loss/metric logic.
|
||||
"""
|
||||
# TODO(dev): we can reuse codes with self._train after making
|
||||
# sure if they can.
|
||||
|
||||
# step 1. save feed variables of Program
|
||||
mode = 'eval'
|
||||
self._input_vars[mode] = inputs
|
||||
self._label_vars[mode] = labels
|
||||
|
||||
# step 2. call inner_layer.forward
|
||||
self._output_vars[mode] = self.inner_layer(*inputs)
|
||||
|
||||
# step 3. calculate loss if needed
|
||||
new_inputs = self._prepare(self.output_vars, labels)
|
||||
self._loss_vars[mode] = self.call_loss(new_inputs)
|
||||
if paddle.base.framework.get_flags("FLAGS_enable_pir_api")[
|
||||
"FLAGS_enable_pir_api"
|
||||
]:
|
||||
self.append_loss_to_shadow_output(mode)
|
||||
|
||||
# step 4. calculate metrics if needed
|
||||
self._metric_vars[mode] = self.call_metrics(new_inputs)
|
||||
|
||||
def _predict(self, inputs, labels):
|
||||
"""
|
||||
Predict process of inner_layer with forward logic.
|
||||
"""
|
||||
# step 1. save feed variables of Program
|
||||
mode = 'predict'
|
||||
self._input_vars[mode] = inputs
|
||||
self._label_vars[mode] = labels
|
||||
|
||||
# step 2. call inner_layer.forward
|
||||
self._output_vars[mode] = self.inner_layer(*inputs)
|
||||
|
||||
@not_to_static
|
||||
def _prepare(self, outputs, labels):
|
||||
"""
|
||||
Concat outputs and labels as a single list
|
||||
|
||||
NOTE(dev): We use @not_to_static to avoid AST Analysis.
|
||||
"""
|
||||
return to_list(outputs) + to_list(labels)
|
||||
|
||||
def call_loss(self, inputs):
|
||||
"""
|
||||
Apply Loss Function on outputs and labels.
|
||||
|
||||
Args:
|
||||
inputs: List[Variable]
|
||||
|
||||
Returns: List[Variable]
|
||||
"""
|
||||
res = []
|
||||
if self.loss_func is not None:
|
||||
res = self.loss_func(*inputs)
|
||||
return res
|
||||
|
||||
def call_metrics(self, inputs):
|
||||
"""
|
||||
Apply Metrics Function on outputs and labels.
|
||||
|
||||
Args:
|
||||
inputs: List[Variable]
|
||||
|
||||
Returns: List[Variable]
|
||||
"""
|
||||
outs = []
|
||||
for metric in self.metrics:
|
||||
outs.append(to_list(metric.compute(*inputs)))
|
||||
|
||||
return outs
|
||||
|
||||
def set_mode(self, mode):
|
||||
self.mode = mode
|
||||
self.training = mode == 'train'
|
||||
|
||||
def clone(self):
|
||||
return ProxyLayer(self.inner_layer, self.loss_func, self.metrics)
|
||||
|
||||
@property
|
||||
def input_vars(self):
|
||||
return self._input_vars[self.mode]
|
||||
|
||||
@property
|
||||
def label_vars(self):
|
||||
return self._label_vars[self.mode]
|
||||
|
||||
@property
|
||||
def output_vars(self):
|
||||
return self._output_vars[self.mode]
|
||||
|
||||
@property
|
||||
def loss_vars(self):
|
||||
return self._loss_vars[self.mode]
|
||||
|
||||
@property
|
||||
def loss_names(self):
|
||||
return self._loss_names[self.mode]
|
||||
|
||||
@property
|
||||
def metric_vars(self):
|
||||
return self._metric_vars[self.mode]
|
||||
|
||||
@property
|
||||
def startup_program(self):
|
||||
return self.inner_layer._startup_program()
|
||||
|
||||
|
||||
class BuildInfo:
|
||||
def __init__(self):
|
||||
self.clear()
|
||||
|
||||
def has_cache(self, mode, update=False):
|
||||
is_cache = self.states[mode]
|
||||
if update:
|
||||
self.cache(mode)
|
||||
return is_cache
|
||||
|
||||
def cache(self, mode):
|
||||
self.states[mode] = True
|
||||
|
||||
def clear(self):
|
||||
self.states = defaultdict(bool)
|
||||
|
||||
|
||||
class ProgramHelper:
|
||||
"""
|
||||
A Helper class for Engine to provides different Program IR according specified 'mode'.
|
||||
"""
|
||||
|
||||
def __init__(self, layer, loss_func, metrics, inputs_spec, labels_spec):
|
||||
# original model config information
|
||||
# TODO(Aurelius84): Implement append_backward and optimizer in ProxyLayer
|
||||
# after distribute engine satisfy basic condition.
|
||||
self.proxy_layer = ProxyLayer(layer, loss_func, metrics)
|
||||
self.inputs_spec = inputs_spec
|
||||
self.labels_spec = labels_spec
|
||||
|
||||
self.build_info = BuildInfo()
|
||||
self._logger = get_logger(logging.INFO)
|
||||
self.lazy_init = False
|
||||
self._all_params_dist_attr = {}
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Reset all state of current Object.
|
||||
"""
|
||||
self.build_info.clear()
|
||||
self.proxy_layer = self.proxy_layer.clone()
|
||||
|
||||
def build_program(self, mode):
|
||||
"""
|
||||
Convert dygraph model into static Program IR.
|
||||
"""
|
||||
assert mode in ['train', 'eval', 'predict']
|
||||
self.proxy_layer.set_mode(mode)
|
||||
# skip if we has already built program.
|
||||
if self.build_info.has_cache(mode, True):
|
||||
self._logger.info(
|
||||
f"Already build program with mode = {mode}, use cached program."
|
||||
)
|
||||
return
|
||||
|
||||
self._logger.info(f"start to build program for mode = {mode}.")
|
||||
input_spec = [self.inputs_spec, self.labels_spec]
|
||||
static_func = to_static(
|
||||
self.static_func(), input_spec=input_spec, full_graph=True
|
||||
)
|
||||
|
||||
func_name = '_' + mode
|
||||
setattr(self.proxy_layer, func_name, static_func)
|
||||
|
||||
# NOTE(dev): Because @to_static is a Lazy mechanism, so we explicitly call this to trigger
|
||||
# generating Program IR immediately.
|
||||
concrete_program = getattr(self.proxy_layer, func_name).concrete_program
|
||||
|
||||
# TODO(zhiqiu): prepare_op_amp_options is not supported for PIR program
|
||||
# It will to use dynamic-static unified amp in pir program, and there is
|
||||
# no need to fit for prepare_op_amp_options
|
||||
if not paddle.base.framework.get_flags("FLAGS_enable_pir_api")[
|
||||
"FLAGS_enable_pir_api"
|
||||
]:
|
||||
prepare_op_amp_options(
|
||||
concrete_program.main_program,
|
||||
ProgramTranslator.get_instance()._amp_records,
|
||||
DEFAULT_AMP_OPTIONS,
|
||||
)
|
||||
self._build_startup_program()
|
||||
|
||||
def _build_startup_program(self):
|
||||
"""
|
||||
Create and Sync parameters into startup program.
|
||||
"""
|
||||
startup_program = self.startup_program
|
||||
if len(startup_program.global_block().ops) > 1:
|
||||
self.lazy_init = True
|
||||
return
|
||||
|
||||
for param in self.concrete_program.parameters:
|
||||
Parameter(
|
||||
name=param.name,
|
||||
desc=param,
|
||||
type=param.type,
|
||||
shape=param.shape,
|
||||
dtype=param.dtype,
|
||||
stop_gradient=param.stop_gradient,
|
||||
block=startup_program.global_block(),
|
||||
)
|
||||
|
||||
def apply_optimizer(self, optimizer):
|
||||
"""
|
||||
Append backward and generate optimizer operations.
|
||||
"""
|
||||
self._verify_optimizer(optimizer)
|
||||
self._logger.info(
|
||||
"start to apply optimizer: %s ", type(optimizer).__name__
|
||||
)
|
||||
# clear optimizer parameters
|
||||
original_params = optimizer._parameter_list
|
||||
optimizer._parameter_list = None
|
||||
with program_guard(self.main_program, self.startup_program):
|
||||
res = optimizer.minimize(self.loss_vars[0])
|
||||
|
||||
# restore optimizer parameters
|
||||
optimizer._parameter_list = original_params
|
||||
return res
|
||||
|
||||
def _verify_optimizer(self, optimizer):
|
||||
assert optimizer is not None
|
||||
assert hasattr(optimizer, "minimize"), (
|
||||
"Optimizer must have minimize() method."
|
||||
)
|
||||
assert self.proxy_layer.mode == 'train', (
|
||||
f"Required mode == 'train', but received '{self.proxy_layer.mode}'"
|
||||
)
|
||||
assert len(self.loss_vars) == 1, (
|
||||
f"Required len(loss_vars) == 1, but received len(loss_vars) = {len(self.loss_vars)}"
|
||||
)
|
||||
|
||||
def to(self, mode):
|
||||
"""
|
||||
Switch underly proxy layer mode into target mode.
|
||||
"""
|
||||
assert mode in ['train', 'eval', 'predict']
|
||||
func = getattr(self.proxy_layer, '_' + mode)
|
||||
assert isinstance(func, StaticFunction), (
|
||||
"Please call build_program(mode) firstly."
|
||||
)
|
||||
self.proxy_layer.set_mode(mode)
|
||||
|
||||
def static_func(self):
|
||||
"""
|
||||
Return StaticFunction instance with underly target mode.
|
||||
"""
|
||||
assert self.proxy_layer.mode in [
|
||||
'train',
|
||||
'eval',
|
||||
'predict',
|
||||
], "Please call build_program(mode) firstly."
|
||||
func_name = '_' + self.proxy_layer.mode
|
||||
return getattr(self.proxy_layer, func_name)
|
||||
|
||||
def init_pir(self, main_program, place):
|
||||
# collect all params in current dist program
|
||||
param_values = main_program.global_block().all_parameters()
|
||||
value_name_to_value = {}
|
||||
dy_param_name_to_pir_param_name = {}
|
||||
for value in param_values:
|
||||
value_name_to_value[value.name] = value
|
||||
|
||||
dy_params = self.concrete_program.parameters[0]
|
||||
pir_param = self.concrete_program.parameters[1]
|
||||
|
||||
for i in range(len(pir_param)):
|
||||
if pir_param[i].name in value_name_to_value:
|
||||
dy_param_name_to_pir_param_name[dy_params[i].name] = pir_param[
|
||||
i
|
||||
].name
|
||||
|
||||
is_comm = False
|
||||
for param in dy_params:
|
||||
if param.is_dist():
|
||||
process_mesh, dims_mapping = self._all_params_dist_attr[
|
||||
param.name
|
||||
]
|
||||
var_dist_attr = TensorDistAttr()
|
||||
var_dist_attr.process_mesh = process_mesh
|
||||
var_dist_attr.dims_mapping = dims_mapping
|
||||
is_comm = True
|
||||
with paddle.no_grad():
|
||||
tmp = paddle.base.core.reshard(param, var_dist_attr)
|
||||
if tmp._is_initialized():
|
||||
param.get_tensor()._share_data_with(tmp.get_tensor())
|
||||
else:
|
||||
# Only setting the "param" to "None" can't release the memory
|
||||
param.get_tensor()._clear()
|
||||
param = None
|
||||
|
||||
# create var in scope and share parameters to scope
|
||||
if param is None:
|
||||
continue
|
||||
if param.name not in dy_param_name_to_pir_param_name:
|
||||
# Release the redundant params
|
||||
param.get_tensor()._clear()
|
||||
continue
|
||||
if not param._is_initialized():
|
||||
continue
|
||||
if param.is_dense():
|
||||
value_name = dy_param_name_to_pir_param_name[param.name]
|
||||
value = value_name_to_value[value_name]
|
||||
# get param_var's dist_attr
|
||||
assert value.is_dist_dense_tensor_type(), (
|
||||
f"param [{value.name}] is not dist tensor type"
|
||||
)
|
||||
dist_attr = {
|
||||
"dims_mapping": value.dist_attr().dims_mapping,
|
||||
"process_shape": value.dist_attr().process_mesh.shape,
|
||||
"process_group": value.dist_attr().process_mesh.process_ids,
|
||||
}
|
||||
# slice param_value with dist_attr
|
||||
# share sliced_param_value with param_tensor in global_scope
|
||||
pir_scope_param = global_scope().var(value_name).get_tensor()
|
||||
sliced_param = Converter.slice_with_dist_attr(
|
||||
param.numpy(), dist_attr
|
||||
)
|
||||
pir_scope_param.set(sliced_param, place)
|
||||
param.get_tensor()._clear()
|
||||
|
||||
elif param.is_dist():
|
||||
value_name = dy_param_name_to_pir_param_name[param.name]
|
||||
value = value_name_to_value[value_name]
|
||||
# assert value.is_dist_dense_tensor_type(), "param [{}] is not dist tensor type".format(value.name)
|
||||
pir_scope_param = global_scope().var(value_name).get_tensor()
|
||||
pir_scope_param._share_data_with(
|
||||
param.get_tensor().get_tensor()
|
||||
)
|
||||
param.get_tensor()._clear()
|
||||
|
||||
world_group = get_world_process_group()
|
||||
if (
|
||||
is_comm
|
||||
and world_group.nranks > 1
|
||||
and paddle.distributed.get_world_size() > 1
|
||||
):
|
||||
paddle.disable_static()
|
||||
barrier_tensor = paddle.full([1], 1, dtype="int32")
|
||||
# barrier is not available in xpu for now
|
||||
if not paddle.framework.core.is_compiled_with_xpu():
|
||||
paddle._legacy_C_ops.barrier(
|
||||
barrier_tensor, barrier_tensor, 'ring_id', 0
|
||||
)
|
||||
paddle.enable_static()
|
||||
|
||||
def init(self, main_program, place, dist_context):
|
||||
if self.lazy_init:
|
||||
return
|
||||
|
||||
amp_strategy = dist_context.strategy.amp
|
||||
amp_config = copy.deepcopy(amp_strategy.to_dict())
|
||||
need_cast_parameter = amp_strategy.enable and amp_config["level"] in [
|
||||
"o2",
|
||||
"o3",
|
||||
]
|
||||
is_comm = False
|
||||
for param in self.concrete_program.parameters:
|
||||
if param.is_dist():
|
||||
serial_main_program = self.concrete_program.main_program
|
||||
var = serial_main_program.global_block().vars[param.name]
|
||||
var_dist_attr = dist_context.get_tensor_dist_attr_for_program(
|
||||
var
|
||||
)
|
||||
is_comm = True
|
||||
# No need to construct backward.
|
||||
with paddle.no_grad():
|
||||
tmp = paddle.base.core.reshard(param, var_dist_attr)
|
||||
if tmp._is_initialized():
|
||||
param.get_tensor()._share_data_with(tmp.get_tensor())
|
||||
else:
|
||||
# Only setting the "param" to "None" can't release the memory
|
||||
param.get_tensor()._clear()
|
||||
param = None
|
||||
paddle.device.synchronize()
|
||||
|
||||
# create var in scope and share parameters to scope
|
||||
if param is None:
|
||||
continue
|
||||
if param.name not in main_program.global_block().vars:
|
||||
# Release the redundant params
|
||||
param.get_tensor()._clear()
|
||||
continue
|
||||
if not param._is_initialized():
|
||||
continue
|
||||
if param.is_dense():
|
||||
# get param_var's dist_attr
|
||||
var = main_program.global_block().vars[param.name]
|
||||
var_dist_attr = dist_context.get_tensor_dist_attr_for_program(
|
||||
var
|
||||
)
|
||||
dist_attr = {
|
||||
"dims_mapping": var_dist_attr.dims_mapping,
|
||||
"process_shape": var_dist_attr.process_mesh.shape,
|
||||
"process_group": var_dist_attr.process_mesh.process_ids,
|
||||
}
|
||||
# slice param_value with dist_attr
|
||||
# share sliced_param_value with param_tensor in global_scope
|
||||
param_tensor = global_scope().var(param.name).get_tensor()
|
||||
sliced_param = Converter.slice_with_dist_attr(
|
||||
param.numpy(), dist_attr
|
||||
)
|
||||
param_tensor.set(sliced_param, place)
|
||||
if not need_cast_parameter:
|
||||
param.get_tensor()._clear()
|
||||
elif param.is_dist():
|
||||
dense_tensor = global_scope().var(param.name).get_tensor()
|
||||
dense_tensor._share_data_with(param.get_tensor().get_tensor())
|
||||
|
||||
# transform the parameter in eager mode for amp.
|
||||
if need_cast_parameter:
|
||||
for param in self.concrete_program.parameters:
|
||||
amp_dtype = amp_config["dtype"]
|
||||
scope_var = global_scope().find_var(param.name)
|
||||
# The parameter is not in this rank.
|
||||
if not scope_var:
|
||||
continue
|
||||
# The parameter do not need to transform
|
||||
if param.dtype in [paddle.float16, paddle.bfloat16]:
|
||||
continue
|
||||
scope_tensor = global_scope().var(param.name).get_tensor()
|
||||
assert scope_var and scope_tensor._is_initialized(), (
|
||||
f"Parameter: {param.name} is not put into global_scope or not initialized."
|
||||
)
|
||||
param_used = param
|
||||
# For the params without dist_attr.
|
||||
# NOTE(lizhiyu): In principle, each param should have dist_attr.
|
||||
if param.is_dense():
|
||||
# get param_var's dist_attr
|
||||
var = main_program.global_block().vars[param.name]
|
||||
var_dist_attr = (
|
||||
dist_context.get_tensor_dist_attr_for_program(var)
|
||||
)
|
||||
dist_attr = {
|
||||
"dims_mapping": var_dist_attr.dims_mapping,
|
||||
"process_shape": var_dist_attr.process_mesh.shape,
|
||||
"process_group": var_dist_attr.process_mesh.process_ids,
|
||||
}
|
||||
# slice param_value with dist_attr
|
||||
sliced_param = Converter.slice_with_dist_attr(
|
||||
param.numpy(), dist_attr
|
||||
)
|
||||
with paddle.base.dygraph.guard():
|
||||
param_used = paddle.to_tensor(
|
||||
sliced_param, place=param.place
|
||||
)
|
||||
param.get_tensor()._clear()
|
||||
with paddle.base.dygraph.guard():
|
||||
if amp_dtype == "float16":
|
||||
with (
|
||||
paddle.no_grad(),
|
||||
paddle.base.framework._dygraph_place_guard(
|
||||
place=place
|
||||
),
|
||||
):
|
||||
t_casted = param_used.cast(
|
||||
dtype=core.VarDesc.VarType.FP16
|
||||
)
|
||||
elif amp_dtype == "bfloat16":
|
||||
with (
|
||||
paddle.no_grad(),
|
||||
paddle.base.framework._dygraph_place_guard(
|
||||
place=place
|
||||
),
|
||||
):
|
||||
t_casted = param_used.cast(
|
||||
dtype=core.VarDesc.VarType.BF16
|
||||
)
|
||||
# NOTE(lizhiyu): Clear the origin param. Don't use `param_used.get_tensor().get_tensor()._clear()` to
|
||||
# clear the `DistTensor`, because it can't clear the `_holder`,
|
||||
# which `param_used.get_tensor().get_tensor()` will copy one `DenseTensor`.
|
||||
param_used.get_tensor()._clear()
|
||||
if t_casted.is_dist():
|
||||
scope_tensor._share_data_with(
|
||||
t_casted.get_tensor().get_tensor()
|
||||
)
|
||||
else:
|
||||
scope_tensor._share_data_with(t_casted.get_tensor())
|
||||
|
||||
world_group = get_world_process_group()
|
||||
if (
|
||||
is_comm
|
||||
and world_group.nranks > 1
|
||||
and paddle.distributed.get_world_size() > 1
|
||||
):
|
||||
paddle.disable_static()
|
||||
barrier_tensor = paddle.full([1], 1, dtype="int32")
|
||||
# barrier is not available in xpu for now
|
||||
if not paddle.framework.core.is_compiled_with_xpu():
|
||||
paddle._legacy_C_ops.barrier(
|
||||
barrier_tensor, barrier_tensor, 'ring_id', 0
|
||||
)
|
||||
paddle.enable_static()
|
||||
|
||||
def cache_whole_graph_dist_attr(self, all_params):
|
||||
for param_value in all_params:
|
||||
dist_attr = param_value.dist_attr()
|
||||
if dist_attr:
|
||||
process_mesh = dist_attr.process_mesh
|
||||
dims_mapping = dist_attr.dims_mapping
|
||||
self._all_params_dist_attr[param_value.name] = [
|
||||
process_mesh,
|
||||
dims_mapping,
|
||||
]
|
||||
|
||||
@property
|
||||
def concrete_program(self):
|
||||
return self.static_func().concrete_program
|
||||
|
||||
@property
|
||||
def main_program(self):
|
||||
return self.concrete_program.main_program
|
||||
|
||||
@property
|
||||
def startup_program(self):
|
||||
try:
|
||||
return self.proxy_layer.startup_program
|
||||
except Exception as err:
|
||||
self._logger.warning(
|
||||
"The startup_program is not built by `lazy init`."
|
||||
)
|
||||
if isinstance(err, AssertionError):
|
||||
return self.concrete_program.startup_program
|
||||
raise err
|
||||
|
||||
@property
|
||||
def input_vars(self):
|
||||
return to_list(self.proxy_layer.input_vars)
|
||||
|
||||
@property
|
||||
def output_vars(self):
|
||||
return to_list(self.proxy_layer.output_vars)
|
||||
|
||||
@property
|
||||
def label_vars(self):
|
||||
return to_list(self.proxy_layer.label_vars)
|
||||
|
||||
@property
|
||||
def loss_vars(self):
|
||||
return to_list(self.proxy_layer.loss_vars)
|
||||
|
||||
@property
|
||||
def loss_names(self):
|
||||
return to_list(self.proxy_layer.loss_names)
|
||||
|
||||
@property
|
||||
def metric_vars(self):
|
||||
return to_list(self.proxy_layer.metric_vars)
|
||||
|
||||
def named_parameters(self):
|
||||
static_func = self.static_func()
|
||||
partial_program = static_func.get_concrete_program(
|
||||
self.inputs_spec, self.labels_spec
|
||||
)[-1]
|
||||
# TODO(xiongkun): support pir in the feature.
|
||||
return {param.name: param for param in partial_program._params}
|
||||
@@ -0,0 +1,342 @@
|
||||
# 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 functools
|
||||
import operator
|
||||
import os
|
||||
from collections import deque
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
|
||||
from .cluster import DeviceType
|
||||
from .graph import Graph
|
||||
from .process_group import get_process_group
|
||||
|
||||
|
||||
def is_collective_comm_op(op):
|
||||
comm_list = [
|
||||
"all_gather",
|
||||
"all_reduce",
|
||||
"broadcast",
|
||||
]
|
||||
reduce_type = [
|
||||
dist.ReduceOp.SUM,
|
||||
dist.ReduceOp.MIN,
|
||||
dist.ReduceOp.MAX,
|
||||
dist.ReduceOp.PROD,
|
||||
]
|
||||
if op.type == "reduce" and op.attr("reduce_type") in reduce_type:
|
||||
return True
|
||||
if op.type in comm_list:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def is_p2p_comm_op(op):
|
||||
comm_list = ["send_v2", "recv_v2"]
|
||||
if op.type in comm_list:
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
def get_dtype_bytes(dtype):
|
||||
num_bytes = 0
|
||||
if dtype == paddle.float64:
|
||||
num_bytes = 8
|
||||
elif dtype == paddle.float32:
|
||||
num_bytes = 4
|
||||
elif dtype == paddle.float16:
|
||||
num_bytes = 2
|
||||
elif dtype == paddle.bfloat16:
|
||||
num_bytes = 2
|
||||
elif dtype == paddle.int64:
|
||||
num_bytes = 8
|
||||
elif dtype == paddle.int32:
|
||||
num_bytes = 4
|
||||
elif dtype == paddle.int16:
|
||||
num_bytes = 2
|
||||
elif dtype == paddle.int8:
|
||||
num_bytes = 1
|
||||
elif dtype == paddle.uint8:
|
||||
num_bytes = 1
|
||||
else:
|
||||
raise ValueError(f"Unrecognized dtype {dtype}.")
|
||||
return num_bytes
|
||||
|
||||
|
||||
def get_comm_volume(comm_op, src_rank, tgt_rank):
|
||||
comm_volume = None
|
||||
if src_rank == tgt_rank:
|
||||
return comm_volume
|
||||
comm_op_type = comm_op.type
|
||||
if comm_op_type != "recv_v2":
|
||||
tensor_name = comm_op.input_arg_names[0]
|
||||
else:
|
||||
tensor_name = comm_op.output_arg_names[0]
|
||||
tensor = comm_op.block._find_var_recursive(tensor_name)
|
||||
assert tensor is not None
|
||||
tensor_shape = tensor.shape
|
||||
# Skip the batch dim
|
||||
new_tensor_shape = []
|
||||
for val in tensor_shape:
|
||||
if val == -1:
|
||||
print("Warning: -1 in the tensor shape.")
|
||||
new_tensor_shape.append(1)
|
||||
else:
|
||||
new_tensor_shape.append(val)
|
||||
tensor_size = functools.reduce(operator.mul, new_tensor_shape, 1)
|
||||
tensor_bytes = tensor_size * get_dtype_bytes(tensor.dtype)
|
||||
if "c_allreduce" in comm_op_type or "all_reduce" in comm_op_type:
|
||||
comm_volume = 2 * tensor_bytes
|
||||
elif "all_gather" in comm_op_type:
|
||||
comm_volume = tensor_bytes
|
||||
elif "broadcast" in comm_op_type:
|
||||
if comm_op.attr("root") == src_rank:
|
||||
comm_volume = tensor_bytes
|
||||
else:
|
||||
comm_volume = None
|
||||
elif "c_reduce" in comm_op_type:
|
||||
if comm_op.attr("root_id") == src_rank:
|
||||
comm_volume = None
|
||||
else:
|
||||
comm_volume = tensor_bytes
|
||||
elif "reduce" == comm_op_type:
|
||||
if comm_op.attr("root_id") == src_rank:
|
||||
comm_volume = None
|
||||
else:
|
||||
comm_volume = tensor_bytes
|
||||
elif "send_v2" in comm_op_type:
|
||||
if comm_op.attr("peer") == tgt_rank:
|
||||
comm_volume = tensor_bytes
|
||||
else:
|
||||
comm_volume = None
|
||||
elif "recv_v2" in comm_op_type:
|
||||
comm_volume = None
|
||||
else:
|
||||
raise ValueError("Unrecognized communication operator.")
|
||||
return comm_volume
|
||||
|
||||
|
||||
def analyze_comm_requirements_from_op(op, rank, g_process_group_map):
|
||||
comm_requirements_to_ranks = {}
|
||||
if is_collective_comm_op(op):
|
||||
process_group_id = op.attr("ring_id")
|
||||
process_group = get_process_group(process_group_id, g_process_group_map)
|
||||
if rank not in process_group.ranks:
|
||||
return comm_requirements_to_ranks
|
||||
for tgt_rank in process_group.ranks:
|
||||
comm_volume = get_comm_volume(op, rank, tgt_rank)
|
||||
if comm_volume is not None:
|
||||
comm_requirements_to_ranks[tgt_rank] = {}
|
||||
comm_requirements_to_ranks[tgt_rank]["comm_volume"] = (
|
||||
comm_volume
|
||||
)
|
||||
elif is_p2p_comm_op(op):
|
||||
tgt_rank = op.attr("peer")
|
||||
comm_volume = get_comm_volume(op, rank, tgt_rank)
|
||||
if comm_volume is not None:
|
||||
comm_requirements_to_ranks[tgt_rank] = {}
|
||||
comm_requirements_to_ranks[tgt_rank]["comm_volume"] = comm_volume
|
||||
else:
|
||||
comm_requirements_to_ranks = {}
|
||||
return comm_requirements_to_ranks
|
||||
|
||||
|
||||
def analyze_requirements_for_program(src_info, rank):
|
||||
program = src_info[0]
|
||||
g_process_group_map = src_info[1]
|
||||
resource_requirements = {}
|
||||
comm_requirements_to_ranks = {}
|
||||
# only support device_type and only support GPU for now
|
||||
resource_requirements["device_type"] = DeviceType.GPU
|
||||
for block in program.blocks:
|
||||
for op in block.ops:
|
||||
cur_comm_requirements_to_ranks = analyze_comm_requirements_from_op(
|
||||
op, rank, g_process_group_map
|
||||
)
|
||||
for tgt_rank, link_info in cur_comm_requirements_to_ranks.items():
|
||||
if tgt_rank in comm_requirements_to_ranks:
|
||||
comm_requirements_to_ranks[tgt_rank]["comm_volume"] += (
|
||||
link_info["comm_volume"]
|
||||
)
|
||||
else:
|
||||
comm_requirements_to_ranks[tgt_rank] = {}
|
||||
comm_requirements_to_ranks[tgt_rank]["comm_volume"] = (
|
||||
link_info["comm_volume"]
|
||||
)
|
||||
return resource_requirements, comm_requirements_to_ranks
|
||||
|
||||
|
||||
def build_process_graph(distributed_program):
|
||||
graph = Graph()
|
||||
for src_rank, src_info in distributed_program.items():
|
||||
(
|
||||
resource_requirements,
|
||||
comm_requirements_to_ranks,
|
||||
) = analyze_requirements_for_program(src_info, src_rank)
|
||||
graph.add_node(src_rank, resource_requirements=resource_requirements)
|
||||
for tgt_rank, comm_requirements in comm_requirements_to_ranks.items():
|
||||
graph.add_edge(
|
||||
src_rank, tgt_rank, comm_requirements=comm_requirements
|
||||
)
|
||||
return graph
|
||||
|
||||
|
||||
def build_cluster_graph(cluster):
|
||||
graph = Graph()
|
||||
cuda_visible_devices_env = os.getenv("CUDA_VISIBLE_DEVICES")
|
||||
cuda_visible_devices = []
|
||||
if cuda_visible_devices_env is not None and cuda_visible_devices_env != "":
|
||||
cuda_visible_devices = [
|
||||
int(d.strip()) for d in cuda_visible_devices_env.split(",")
|
||||
]
|
||||
for machine in cluster.machines.values():
|
||||
for device in machine.devices.values():
|
||||
graph.add_node(device.global_id, device=device)
|
||||
if (
|
||||
cuda_visible_devices
|
||||
and device.local_id not in cuda_visible_devices
|
||||
):
|
||||
graph.nodes[device.global_id]["occupied"] = True
|
||||
else:
|
||||
graph.nodes[device.global_id]["occupied"] = False
|
||||
for link in machine.links.values():
|
||||
graph.add_edge(
|
||||
link.source.global_id, link.target.global_id, link=link
|
||||
)
|
||||
return graph
|
||||
|
||||
|
||||
def mapping(distributed_program, cluster):
|
||||
# A very simple mapping algorithm only for GPUs.
|
||||
# Here we assume one process will be mapped to one GPU.
|
||||
# In the future, more mapping configurations and algorithms will be supported.
|
||||
process_graph = build_process_graph(distributed_program)
|
||||
|
||||
cluster_graph = build_cluster_graph(cluster)
|
||||
|
||||
for cur_rank_node in process_graph:
|
||||
cur_rank_node["visited"] = False
|
||||
|
||||
def sort_by_comm_volume(rank_edge):
|
||||
return rank_edge["comm_requirements"]["comm_volume"]
|
||||
|
||||
def sort_by_comm_bandwidth(device_edge):
|
||||
return device_edge["link"].bandwidth
|
||||
|
||||
def select_unvisited_rank_node(rank_node_list):
|
||||
selected_rank_node = None
|
||||
for rank_node in rank_node_list:
|
||||
if rank_node["visited"] is False:
|
||||
selected_rank_node = rank_node
|
||||
return selected_rank_node
|
||||
|
||||
queue = deque()
|
||||
root_rank_node = select_unvisited_rank_node(
|
||||
list(process_graph.nodes.values())
|
||||
)
|
||||
while root_rank_node is not None:
|
||||
queue.append(root_rank_node)
|
||||
while queue:
|
||||
cur_rank_node = queue.popleft()
|
||||
if cur_rank_node["visited"]:
|
||||
continue
|
||||
device_type = cur_rank_node["resource_requirements"]["device_type"]
|
||||
cur_device_node = None
|
||||
for device_node in cluster_graph.nodes.values():
|
||||
if (device_node["device"].type == device_type) and (
|
||||
not device_node["occupied"]
|
||||
):
|
||||
device_node["occupied"] = True
|
||||
cur_rank_node["visited"] = True
|
||||
cur_rank_node["device"] = device_node["device"]
|
||||
cur_device_node = device_node
|
||||
break
|
||||
assert cur_device_node, (
|
||||
"Cannot find a device to satisfy the requirement."
|
||||
)
|
||||
|
||||
nbr_rank_edges = []
|
||||
for nbr_rank_node_id, nbr_rank_edge in process_graph.adjs[
|
||||
cur_rank_node.id
|
||||
].items():
|
||||
assert (
|
||||
nbr_rank_edge.src_id == cur_rank_node.id
|
||||
and nbr_rank_edge.tgt_id == nbr_rank_node_id
|
||||
)
|
||||
queue.append(process_graph.nodes[nbr_rank_node_id])
|
||||
nbr_rank_edges.append(nbr_rank_edge)
|
||||
nbr_rank_edges.sort(key=sort_by_comm_volume)
|
||||
|
||||
nbr_device_edges = []
|
||||
for nbr_device_edge in cluster_graph.adjs[
|
||||
cur_device_node.id
|
||||
].values():
|
||||
nbr_device_edges.append(nbr_device_edge)
|
||||
nbr_device_edges.sort(key=sort_by_comm_bandwidth)
|
||||
|
||||
for nbr_rank_edge in nbr_rank_edges:
|
||||
src_rank_node = process_graph.nodes[nbr_rank_edge.src_id][
|
||||
"visited"
|
||||
]
|
||||
if src_rank_node:
|
||||
continue
|
||||
device_type = src_rank_node["resource_requirements"][
|
||||
"device_type"
|
||||
]
|
||||
nbr_rank_node = process_graph.nodes[nbr_rank_edge.tgt_id]
|
||||
for nbr_device_edge in nbr_device_edges:
|
||||
nbr_device_node = cluster_graph.nodes[
|
||||
nbr_device_edge.tgt_id
|
||||
]
|
||||
if (nbr_device_node["device"].type == device_type) and (
|
||||
not nbr_device_node["occupied"]
|
||||
):
|
||||
nbr_device_node["occupied"] = True
|
||||
nbr_rank_node["visited"] = True
|
||||
nbr_rank_node["device"] = nbr_device_node["device"]
|
||||
break
|
||||
root_rank_node = select_unvisited_rank_node(
|
||||
list(process_graph.nodes.values())
|
||||
)
|
||||
|
||||
rank_mapping = {}
|
||||
for rank, rank_node in process_graph.nodes.items():
|
||||
device = rank_node["device"]
|
||||
machine = device.machine
|
||||
if machine.id in rank_mapping:
|
||||
rank_mapping[machine.id]["hostname"] = machine.hostname
|
||||
rank_mapping[machine.id]["addr"] = machine.addr
|
||||
rank_mapping[machine.id]["port"] = machine.port
|
||||
if rank not in rank_mapping[machine.id]["ranks"]:
|
||||
rank_mapping[machine.id]["ranks"][rank] = []
|
||||
rank_mapping[machine.id]["ranks"][rank].append(device.local_id)
|
||||
else:
|
||||
rank_mapping[machine.id]["ranks"][rank].append(device.local_id)
|
||||
else:
|
||||
rank_mapping[machine.id] = {}
|
||||
rank_mapping[machine.id]["hostname"] = machine.hostname
|
||||
rank_mapping[machine.id]["addr"] = machine.addr
|
||||
rank_mapping[machine.id]["port"] = machine.port
|
||||
rank_mapping[machine.id]["ranks"] = {}
|
||||
rank_mapping[machine.id]["ranks"][rank] = []
|
||||
rank_mapping[machine.id]["ranks"][rank].append(device.local_id)
|
||||
for machine_mapping in rank_mapping.values():
|
||||
for rank_devices in machine_mapping["ranks"].values():
|
||||
rank_devices.sort()
|
||||
|
||||
return rank_mapping
|
||||
@@ -0,0 +1,138 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import paddle
|
||||
|
||||
from .reshard_funcs.base_reshard_func import is_replicated
|
||||
from .utils import _complete_op_dist_attr
|
||||
|
||||
dist_skip_op_list = [
|
||||
"builtin.combine",
|
||||
"builtin.split",
|
||||
"cf.yield",
|
||||
"cf.tuple_push",
|
||||
"cf.tuple_pop",
|
||||
"cf.stack_create",
|
||||
"pd_op.pylayer",
|
||||
]
|
||||
|
||||
|
||||
def verify_dist_block(block):
|
||||
for op in block.ops:
|
||||
if op.name() in dist_skip_op_list:
|
||||
continue
|
||||
if op.name() == "dist_op.shard_tensor":
|
||||
raise RuntimeError("Block still contain shard_tensor_op.")
|
||||
# Note (luchang): Temp fix, remove unused parameter 'op'.
|
||||
# Will be removed in the future.
|
||||
if op.name() == "builtin.parameter":
|
||||
if op.result(0).use_empty():
|
||||
op.erase()
|
||||
continue
|
||||
|
||||
|
||||
def apply_mix2dist_pass(program, block=None):
|
||||
if block is None:
|
||||
block = program.global_block()
|
||||
deleted_ops = []
|
||||
for op in block.ops:
|
||||
for inner_block in op.blocks():
|
||||
apply_mix2dist_pass(program, block=inner_block)
|
||||
if op.name() != "dist_op.shard_tensor":
|
||||
continue
|
||||
shard_operand_value = op.operand_source(0)
|
||||
if not shard_operand_value.has_one_use():
|
||||
raise RuntimeError(
|
||||
f"shard_tensor is supposed to be called right after tensor is created, the use_count of tensor to be sharded is {shard_operand_value.use_count}, which is "
|
||||
"not Supported in right now."
|
||||
)
|
||||
shard_result_value = op.result(0)
|
||||
shard_result_value.replace_all_uses_with(shard_operand_value)
|
||||
deleted_ops.append(op)
|
||||
prev_op = shard_operand_value.get_defining_op()
|
||||
if (
|
||||
prev_op.name() == "builtin.parameter"
|
||||
or prev_op.name() == "pd_op.data"
|
||||
):
|
||||
prev_op.dist_attr = op.dist_attr
|
||||
shard_operand_value.set_type(shard_result_value.type())
|
||||
shard_operand_value.stop_gradient = shard_result_value.stop_gradient
|
||||
shard_operand_value.persistable = shard_result_value.persistable
|
||||
elif (
|
||||
prev_op.name() == "pd_op.randint"
|
||||
or prev_op.name() == "pd_op.gaussian"
|
||||
):
|
||||
mesh = shard_result_value.dist_attr().process_mesh
|
||||
# input
|
||||
shape_value = prev_op.operand_source(0)
|
||||
dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
mesh, [-1 for _ in range(len(shape_value.shape))], {}
|
||||
)
|
||||
shape_value.update_dist_attr(dist_attr)
|
||||
# op
|
||||
prev_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
mesh, [dist_attr], [shard_result_value.dist_attr()]
|
||||
)
|
||||
)
|
||||
# deal with full_int_array op
|
||||
prev_prev_op = shape_value.get_defining_op()
|
||||
prev_prev_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
mesh, [], [dist_attr]
|
||||
)
|
||||
)
|
||||
# output
|
||||
shard_operand_value.set_type(shard_result_value.type())
|
||||
shard_operand_value.stop_gradient = shard_result_value.stop_gradient
|
||||
shard_operand_value.persistable = shard_result_value.persistable
|
||||
else:
|
||||
dist_attr = shard_result_value.dist_attr()
|
||||
if not is_replicated(dist_attr):
|
||||
raise RuntimeError(
|
||||
f"{prev_op} is not support sharded by shard_tensor op in pir mode."
|
||||
)
|
||||
mesh = dist_attr.process_mesh
|
||||
ops_list = [prev_op]
|
||||
while len(ops_list) != 0:
|
||||
cur_op = ops_list.pop()
|
||||
if cur_op.dist_attr is not None:
|
||||
continue
|
||||
operand_attrs = []
|
||||
result_attrs = []
|
||||
for input in cur_op.operands_source():
|
||||
dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
mesh, [-1 for _ in range(len(input.shape))], {}
|
||||
)
|
||||
)
|
||||
operand_attrs.append(dist_attr)
|
||||
ops_list.append(input.get_defining_op())
|
||||
for result in cur_op.results():
|
||||
dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
mesh, [-1 for _ in range(len(result.shape))], {}
|
||||
)
|
||||
)
|
||||
result.update_dist_attr(dist_attr)
|
||||
result_attrs.append(dist_attr)
|
||||
cur_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
mesh, operand_attrs, result_attrs
|
||||
)
|
||||
)
|
||||
for op in deleted_ops:
|
||||
op.erase()
|
||||
_complete_op_dist_attr(program, block=block)
|
||||
verify_dist_block(block)
|
||||
@@ -0,0 +1,61 @@
|
||||
# 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 os
|
||||
|
||||
from . import ( # noqa: F401
|
||||
dist_assign,
|
||||
dist_check_finite_and_unscale,
|
||||
dist_concat,
|
||||
dist_default,
|
||||
dist_dropout,
|
||||
dist_eltwise,
|
||||
dist_embedding,
|
||||
dist_expand_as,
|
||||
dist_fill_constant_batch_size_like,
|
||||
dist_flash_attn,
|
||||
dist_fused_attention,
|
||||
dist_fused_dropout_add,
|
||||
dist_fused_feedforward,
|
||||
dist_fused_rms_norm,
|
||||
dist_fused_rope,
|
||||
dist_gather_nd,
|
||||
dist_layer_norm,
|
||||
dist_matmul,
|
||||
dist_pnorm,
|
||||
dist_reduce_sum_p,
|
||||
dist_reshape,
|
||||
dist_scale,
|
||||
dist_shape,
|
||||
dist_slice,
|
||||
dist_softmax,
|
||||
dist_split,
|
||||
dist_stack,
|
||||
dist_strided_slice,
|
||||
dist_tile,
|
||||
dist_transpose,
|
||||
dist_unsqueeze2,
|
||||
dist_update_loss_scaling,
|
||||
)
|
||||
from .common import ( # noqa: F401
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
find_compatible_distributed_operator_impls,
|
||||
find_distributed_operator_impl_container,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
|
||||
parallel_ce = os.getenv("PARALLEL_CROSS_ENTROPY")
|
||||
if parallel_ce == "true":
|
||||
from . import dist_cross_entropy # noqa: F401
|
||||
@@ -0,0 +1,838 @@
|
||||
# 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 abc
|
||||
import logging
|
||||
import warnings
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.base.log_helper import get_logger
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_dims_mapping,
|
||||
is_optimize_op,
|
||||
set_dist_op_desc_original_id,
|
||||
)
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
_g_distributed_operator_impl_containers = {}
|
||||
|
||||
_g_elementwise_ops = [
|
||||
"assign",
|
||||
"elementwise",
|
||||
"gelu",
|
||||
# "dropout",
|
||||
"scale",
|
||||
"relu",
|
||||
"cast",
|
||||
# "gather",
|
||||
# "concat",
|
||||
"silu",
|
||||
"fused_softmax_mask_upper_triangle",
|
||||
]
|
||||
BACKWARD_ONLY_DIST_OPS = {'check_finite_and_unscale', 'update_loss_scaling'}
|
||||
|
||||
_gradient_sync_by_partial_ops = [
|
||||
"matmul_v2_grad",
|
||||
"elementwise_add_grad",
|
||||
"layer_norm_grad",
|
||||
"lookup_table_v2_grad",
|
||||
# "conv",
|
||||
]
|
||||
|
||||
|
||||
class ParallelMode:
|
||||
"""
|
||||
the parallel mode for communication or auxiliary operator
|
||||
"""
|
||||
|
||||
DataParallel = "auto_parallel/data_parallel"
|
||||
TensorParallel = "auto_parallel/tensor_parallel"
|
||||
PipelineParallel = "auto_parallel/pipeline_parallel"
|
||||
MoEParallel = "auto_parallel/moe_parallel"
|
||||
|
||||
|
||||
class SyncMode:
|
||||
"""
|
||||
the synchronization mode for communication or auxiliary operator
|
||||
"""
|
||||
|
||||
AmpFlagSync = "auto_parallel/amp_flag_synchronization"
|
||||
GlobalNormSync = "auto_parallel/global_norm_synchronization"
|
||||
|
||||
|
||||
def is_elementwise_op(op_type):
|
||||
if op_type in _g_elementwise_ops:
|
||||
return True
|
||||
if "elementwise" in op_type:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class DistributedOperatorImplContainer(abc.ABC):
|
||||
def __init__(self, op_type):
|
||||
self._type = op_type
|
||||
self._impls = []
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@type.setter
|
||||
def type(self, op_type):
|
||||
self._type = op_type
|
||||
|
||||
@property
|
||||
def impls(self):
|
||||
return self._impls
|
||||
|
||||
def register_impl(self, dist_impl):
|
||||
assert self.type == dist_impl.type, (
|
||||
"Op type of container must be same as that of the implementation."
|
||||
)
|
||||
impl_idx = len(self.impls)
|
||||
dist_impl.idx = impl_idx
|
||||
self._impls.append(dist_impl)
|
||||
|
||||
def get_impl(self, impl_idx):
|
||||
return self._impls[impl_idx]
|
||||
|
||||
def get_input_compatible_impls(self, dist_op):
|
||||
compatible_impls = []
|
||||
for impl in self.impls:
|
||||
if impl.is_input_compatible(dist_op):
|
||||
compatible_impls.append(impl)
|
||||
return compatible_impls
|
||||
|
||||
def get_output_compatible_impls(self, dist_op):
|
||||
compatible_impls = []
|
||||
for impl in self.impls:
|
||||
if impl.is_output_compatible(dist_op):
|
||||
compatible_impls.append(impl)
|
||||
return compatible_impls
|
||||
|
||||
def get_compatible_impls(self, dist_op):
|
||||
compatible_impls = []
|
||||
for impl in self.impls:
|
||||
if impl.is_auto_compatible(dist_op):
|
||||
compatible_impls.append(impl)
|
||||
return compatible_impls
|
||||
|
||||
# (NOTE) Currently, both DistributedOperatorImplContainer and DistributedOperatorImpl have update_dims_mapping method.
|
||||
# But this method is supposed to be maintained by DistributedOperatorImplContainer, and we are ongoing adding method
|
||||
# to DistributedOperatorImplContainer and removing those in DistributedOperatorImpl.
|
||||
# @abc.abstractmethod
|
||||
def update_dims_mapping(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
# (NOTE) Currently we has limited DistributedOperatorImpls for an op to deal with different parallel patterns of this op.
|
||||
# This function help to choose the correct DistributedOperatorImpl based on the result from InferSPMD.
|
||||
# @abc.abstractmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
|
||||
class DistributedOperatorImpl(abc.ABC):
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
self._type = None
|
||||
self._idx = None
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = False
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
@name.setter
|
||||
def name(self, name):
|
||||
self._name = name
|
||||
|
||||
@property
|
||||
def type(self):
|
||||
return self._type
|
||||
|
||||
@type.setter
|
||||
def type(self, op_type):
|
||||
self._type = op_type
|
||||
|
||||
@property
|
||||
def idx(self):
|
||||
return self._idx
|
||||
|
||||
@idx.setter
|
||||
def idx(self, impl_idx):
|
||||
self._idx = impl_idx
|
||||
|
||||
# to be deprecated
|
||||
@abc.abstractmethod
|
||||
def is_input_compatible(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
# to be deprecated
|
||||
@abc.abstractmethod
|
||||
def is_output_compatible(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
# to be deprecated
|
||||
@abc.abstractmethod
|
||||
def is_auto_compatible(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def forward(dist_ctx, *args, **kwargs):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
@staticmethod
|
||||
@abc.abstractmethod
|
||||
def backward(dist_ctx, *grad_outputs, **kwargs):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
# to be deprecated
|
||||
def update_dims_mapping(self, dist_op):
|
||||
raise NotImplementedError("Please Implement this method in Subclass.")
|
||||
|
||||
|
||||
def register_distributed_operator_impl_container(container):
|
||||
global _g_distributed_operator_impl_containers
|
||||
_g_distributed_operator_impl_containers[container.type] = container
|
||||
|
||||
|
||||
def get_distributed_operator_impl_container(op_type):
|
||||
global _g_distributed_operator_impl_containers
|
||||
return _g_distributed_operator_impl_containers.get(op_type, None)
|
||||
|
||||
|
||||
def register_distributed_operator_impl(op_type, dist_impl):
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(op_type)
|
||||
if dist_op_impl_container is not None:
|
||||
dist_impl.type = op_type
|
||||
dist_op_impl_container.register_impl(dist_impl)
|
||||
else:
|
||||
raise AssertionError(
|
||||
"Must register distributed operator registry first."
|
||||
)
|
||||
|
||||
|
||||
def find_compatible_distributed_operator_impls(dist_op, fwd=True, partial=True):
|
||||
"""
|
||||
Here just return the first compatible implementation.
|
||||
This will be improved by cost model in the future.
|
||||
"""
|
||||
op_type = dist_op.serial_op.type
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(op_type)
|
||||
dist_op_eltwise_impl_container = get_distributed_operator_impl_container(
|
||||
"elementwise"
|
||||
)
|
||||
dist_op_default_impl_container = get_distributed_operator_impl_container(
|
||||
"default"
|
||||
)
|
||||
compatible_impls = []
|
||||
if partial:
|
||||
if fwd:
|
||||
# First, find impls in the corresponding container
|
||||
if dist_op_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_impl_container.get_input_compatible_impls(dist_op)
|
||||
)
|
||||
# Second, find impls in the elementwise container
|
||||
if dist_op_eltwise_impl_container and is_elementwise_op(op_type):
|
||||
compatible_impls.extend(
|
||||
dist_op_eltwise_impl_container.get_input_compatible_impls(
|
||||
dist_op
|
||||
)
|
||||
)
|
||||
# Third, find impls in the default container
|
||||
if dist_op_default_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_default_impl_container.get_input_compatible_impls(
|
||||
dist_op
|
||||
)
|
||||
)
|
||||
else:
|
||||
# First, find impls in the corresponding container
|
||||
if dist_op_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_impl_container.get_output_compatible_impls(dist_op)
|
||||
)
|
||||
# Second, find impls in the elementwise container
|
||||
if dist_op_eltwise_impl_container and is_elementwise_op(op_type):
|
||||
compatible_impls.extend(
|
||||
dist_op_eltwise_impl_container.get_output_compatible_impls(
|
||||
dist_op
|
||||
)
|
||||
)
|
||||
# Third, find impls in the default container
|
||||
if dist_op_default_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_default_impl_container.get_output_compatible_impls(
|
||||
dist_op
|
||||
)
|
||||
)
|
||||
else:
|
||||
# First, find impls in the corresponding container
|
||||
if dist_op_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_impl_container.get_compatible_impls(dist_op)
|
||||
)
|
||||
# Second, find impls in the elementwise container
|
||||
if dist_op_eltwise_impl_container and is_elementwise_op(op_type):
|
||||
compatible_impls.extend(
|
||||
dist_op_eltwise_impl_container.get_compatible_impls(dist_op)
|
||||
)
|
||||
# Third, find impls in the default container
|
||||
if dist_op_default_impl_container:
|
||||
compatible_impls.extend(
|
||||
dist_op_default_impl_container.get_compatible_impls(dist_op)
|
||||
)
|
||||
|
||||
if compatible_impls:
|
||||
# For now, just return the first compatible impl
|
||||
# best_compatible_impl = compatible_impls[0]
|
||||
best_compatible_impl = compatible_impls
|
||||
else:
|
||||
best_compatible_impl = None
|
||||
return best_compatible_impl
|
||||
|
||||
|
||||
def find_distributed_operator_impl_container(dist_op):
|
||||
"""
|
||||
Return a unique container for dist op.
|
||||
If not specific container found, default container will be return.
|
||||
"""
|
||||
op_type = dist_op.serial_op.type
|
||||
|
||||
# Op has a match container
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(op_type)
|
||||
if dist_op_impl_container is None:
|
||||
# if op is register to elemwise spmd rule and has NO specific container implemented
|
||||
if is_elementwise_op(op_type):
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(
|
||||
"elementwise"
|
||||
)
|
||||
# default container for all bottom line cases
|
||||
else:
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(
|
||||
"default"
|
||||
)
|
||||
|
||||
_logger.debug(
|
||||
f"Op [{op_type}] Complete DistAttr using {type(dist_op_impl_container).__name__}"
|
||||
)
|
||||
return dist_op_impl_container
|
||||
|
||||
|
||||
def is_parameter_related(varname, block, dist_context=None):
|
||||
# TODO(zhaoyingli): maintain a dict in dist_context to record all variables which are be renamed
|
||||
if ".subprog_" in varname:
|
||||
varname = varname[: varname.index(".subprog_")]
|
||||
if ".cast_fp" in varname:
|
||||
varname = varname[: varname.index(".cast_fp")]
|
||||
if ".cast_bf" in varname:
|
||||
varname = varname[: varname.index(".cast_bf")]
|
||||
if ".quantized" in varname:
|
||||
varname = varname[: varname.index(".quantized")]
|
||||
assert block._find_var_recursive(varname), (
|
||||
f"cannot find var {varname} in cur block"
|
||||
)
|
||||
var = block._var_recursive(varname)
|
||||
# NOTE(hack method): to find the param which is resharded
|
||||
if dist_context and "@RESHARD" in varname:
|
||||
varname = varname[: varname.index("@RESHARD")]
|
||||
serial_program = dist_context.serial_main_program
|
||||
var = serial_program.global_block()._find_var_recursive(varname)
|
||||
if var is None:
|
||||
return False
|
||||
# NOTE(liym27): when Y_var is not a parameter, but Y_var is resharded by a parameter.
|
||||
elif "reshard_api" in varname:
|
||||
for op in block.ops:
|
||||
if op.type == "assign" and varname in op.output("Out"):
|
||||
in_varname = op.input("X")[0]
|
||||
var = block._find_var_recursive(in_varname)
|
||||
if var is not None and var.is_parameter:
|
||||
return True
|
||||
return var.is_parameter
|
||||
|
||||
|
||||
def infer_shape(block, src_var, src_var_dist_attr, op_input_dist_attr):
|
||||
var_shape = block._var_recursive(src_var.name).shape
|
||||
var_topology = src_var_dist_attr.process_mesh.shape
|
||||
var_dims_mapping = src_var_dist_attr.dims_mapping
|
||||
|
||||
complete_shape = []
|
||||
for idx, shape in enumerate(var_shape):
|
||||
if var_dims_mapping[idx] == -1:
|
||||
complete_shape.append(shape)
|
||||
else:
|
||||
new_shape = shape * var_topology[var_dims_mapping[idx]]
|
||||
complete_shape.append(new_shape)
|
||||
|
||||
exact_shape = []
|
||||
input_topology = op_input_dist_attr.process_mesh.shape
|
||||
input_dims_mapping = op_input_dist_attr.dims_mapping
|
||||
for idx, shape in enumerate(complete_shape):
|
||||
if input_dims_mapping[idx] == -1:
|
||||
exact_shape.append(shape)
|
||||
else:
|
||||
new_shape = shape // input_topology[input_dims_mapping[idx]]
|
||||
exact_shape.append(new_shape)
|
||||
|
||||
return exact_shape
|
||||
|
||||
|
||||
def set_comm_op_dist_attr_for_program(
|
||||
new_op, process_mesh, tensor_dist_attr, ctx, **kwargs
|
||||
):
|
||||
assert process_mesh is not None
|
||||
assert tensor_dist_attr is not None
|
||||
|
||||
new_op_dist_attr = OperatorDistAttr()
|
||||
new_op_dist_attr.process_mesh = process_mesh
|
||||
if "chunk_id" in kwargs:
|
||||
new_op_dist_attr.chunk_id = kwargs["chunk_id"]
|
||||
for input_varname in new_op.desc.input_arg_names():
|
||||
new_op_dist_attr.set_input_dist_attr(input_varname, tensor_dist_attr)
|
||||
for output_varname in new_op.desc.output_arg_names():
|
||||
new_op_dist_attr.set_output_dist_attr(output_varname, tensor_dist_attr)
|
||||
ctx.set_op_dist_attr_for_program(new_op, new_op_dist_attr)
|
||||
|
||||
|
||||
def naive_copy_op_dist_attr_for_program(new_op, ref_op, ctx):
|
||||
ref_dist_attr = ctx.get_op_dist_attr_for_program(ref_op)
|
||||
new_op_dist_attr = OperatorDistAttr()
|
||||
new_op_dist_attr.process_mesh = ref_dist_attr.process_mesh
|
||||
new_op_dist_attr.impl_type = ref_dist_attr.impl_type
|
||||
new_op_dist_attr.impl_idx = ref_dist_attr.impl_idx
|
||||
new_op_dist_attr.chunk_id = ref_dist_attr.chunk_id
|
||||
|
||||
for input_name in ref_op.input_names:
|
||||
assert input_name in new_op.input_names
|
||||
assert len(ref_op.input(input_name)) == 1
|
||||
assert len(new_op.input(input_name)) == 1
|
||||
|
||||
ref_tensor_dist_attr = ref_dist_attr.get_input_dist_attr(
|
||||
ref_op.input(input_name)[0]
|
||||
)
|
||||
new_op_dist_attr.set_input_dist_attr(
|
||||
new_op.input(input_name)[0], ref_tensor_dist_attr
|
||||
)
|
||||
|
||||
for output_name in ref_op.output_names:
|
||||
assert output_name in new_op.output_names
|
||||
assert len(ref_op.output(output_name)) == 1
|
||||
assert len(new_op.output(output_name)) == 1
|
||||
|
||||
ref_tensor_dist_attr = ref_dist_attr.get_output_dist_attr(
|
||||
ref_op.output(output_name)[0]
|
||||
)
|
||||
new_op_dist_attr.set_output_dist_attr(
|
||||
new_op.output(output_name)[0], ref_tensor_dist_attr
|
||||
)
|
||||
|
||||
ctx.set_op_dist_attr_for_program(new_op, new_op_dist_attr)
|
||||
|
||||
|
||||
def get_data_parallel_group(dist_ctx, op, act_grad_names, rank):
|
||||
"""
|
||||
deduce the data parallel communication group for current operator.
|
||||
|
||||
Args:
|
||||
dist_ctx (DistributedContext): dist context.
|
||||
op (Operator): the current (backward) operator which might need.
|
||||
act_grad_names (list): list of input activation grads variable name to the current operator.
|
||||
rank (int): global ranks index for current process.
|
||||
"""
|
||||
dp_group = None
|
||||
|
||||
op_dist_attr = dist_ctx.get_op_dist_attr_for_program(op)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
mesh_shape = process_mesh.shape
|
||||
# FIXME Hack for Pipeline Parallelism where the current operator
|
||||
# not belong to the mesh the current rank belong to.
|
||||
if rank not in process_mesh.process_ids:
|
||||
rank = _get_corresponding_rank(dist_ctx, process_mesh, rank)
|
||||
|
||||
for var_name in act_grad_names:
|
||||
var_dim_mapping = op_dist_attr.get_input_dims_mapping(var_name)
|
||||
# consider that the variable's shape is [], which is 0-D
|
||||
# TODO utilize the batch_dim attr instead of "0" in future
|
||||
batch_size_axis = var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
batch_size_axis,
|
||||
rank,
|
||||
)
|
||||
dp_group = new_process_group(group_ranks)
|
||||
break
|
||||
if dp_group is not None:
|
||||
return [dp_group]
|
||||
else:
|
||||
return []
|
||||
|
||||
|
||||
def sync_and_scale_gradients(dist_ctx, op, groups, allreduce_var_names):
|
||||
"""
|
||||
insert the allreduce and scale ops for gradients of model
|
||||
parameters for operator in data parallelism.
|
||||
|
||||
Args:
|
||||
dist_ctx (DistributedContext): dist context.
|
||||
op (Operator): the current (backward) operator which might need.
|
||||
allreduce_var_names (list): list of the parameter's grads variable name in the current operator output.
|
||||
"""
|
||||
|
||||
op_dist_attr = dist_ctx.get_op_dist_attr_for_program(op)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
chunk_id = op_dist_attr.chunk_id
|
||||
dist_op_context = dist_ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
|
||||
reduce_type = dist.ReduceOp.SUM
|
||||
need_scale = dist_ctx.gradient_scale
|
||||
|
||||
for group in groups:
|
||||
group_size = len(group.ranks)
|
||||
|
||||
for var_name in allreduce_var_names:
|
||||
added_ops = []
|
||||
grad_var = main_block.var(var_name)
|
||||
allreduce_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': [grad_var]},
|
||||
outputs={'out': [grad_var]},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'reduce_type': reduce_type,
|
||||
OP_ROLE_KEY: OpRole.Backward,
|
||||
},
|
||||
)
|
||||
allreduce_op._set_attr(
|
||||
'op_namescope', '/' + ParallelMode.DataParallel
|
||||
)
|
||||
added_ops.append(allreduce_op)
|
||||
|
||||
if need_scale:
|
||||
scale_op = main_block.append_op(
|
||||
type='scale',
|
||||
inputs={'X': grad_var},
|
||||
outputs={'Out': grad_var},
|
||||
attrs={
|
||||
'scale': 1.0 / group_size,
|
||||
OP_ROLE_KEY: OpRole.Backward,
|
||||
},
|
||||
)
|
||||
scale_op._set_attr(
|
||||
'op_namescope', '/' + ParallelMode.DataParallel
|
||||
)
|
||||
added_ops.append(scale_op)
|
||||
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(grad_var.name)
|
||||
assert dims_mapping is not None, (
|
||||
f"Unexpected: dims_mapping of output [{grad_var.name}] of op [{op_dist_attr.op_type}] is None"
|
||||
)
|
||||
# NOTE auxiliary op's dist attr should follow dist_op not dist_tensor
|
||||
for new_op in added_ops:
|
||||
new_op_attr = OperatorDistAttr()
|
||||
new_op_attr.process_mesh = process_mesh
|
||||
new_op_attr.chunk_id = chunk_id
|
||||
new_op_attr.set_output_dims_mapping(grad_var.name, dims_mapping)
|
||||
new_op_attr.set_input_dims_mapping(grad_var.name, dims_mapping)
|
||||
dist_ctx.set_op_dist_attr_for_program(new_op, new_op_attr)
|
||||
|
||||
|
||||
def get_partial_groups(dist_ctx, op, out_grad_names, rank):
|
||||
"""
|
||||
deduce the partial communication group for current operator output vars.
|
||||
|
||||
Args:
|
||||
dist_ctx (DistributedContext): dist context.
|
||||
op (Operator): the current (backward) operator which might need.
|
||||
out_grad_names (list): list of the output parameter's grads variable name of the current operator.
|
||||
rank (int): global ranks index for current process.
|
||||
"""
|
||||
op_dist_attr = dist_ctx.get_op_dist_attr_for_program(op)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
mesh_shape = process_mesh.shape
|
||||
|
||||
groups = []
|
||||
|
||||
partial_dims = None
|
||||
for var_name in out_grad_names:
|
||||
var_dist_attr = op_dist_attr.get_output_dist_attr(var_name)
|
||||
if partial_dims is None:
|
||||
partial_dims = var_dist_attr._partial_dims()
|
||||
else:
|
||||
assert partial_dims == var_dist_attr._partial_dims(), (
|
||||
f"Partial dims of outputs {out_grad_names} of op [{op.type}] is not consistent"
|
||||
)
|
||||
|
||||
partial_dims = list(partial_dims)
|
||||
partial_dims.sort()
|
||||
|
||||
# FIXME Hack for Pipeline Parallelism where the current operator
|
||||
# not belong to the mesh the current rank belong to.
|
||||
if rank not in process_mesh.process_ids:
|
||||
rank = _get_corresponding_rank(dist_ctx, process_mesh, rank)
|
||||
|
||||
for dim in partial_dims:
|
||||
if mesh_shape[dim] > 1:
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
dim,
|
||||
rank,
|
||||
)
|
||||
groups.append(new_process_group(group_ranks))
|
||||
|
||||
return groups
|
||||
|
||||
|
||||
def gradient_synchronization(
|
||||
dist_ctx, op, act_grad_names, out_grad_names, rank
|
||||
):
|
||||
"""
|
||||
conduct the allreduce and scaling for gradients of model
|
||||
parameters for operator in parallelism train.
|
||||
|
||||
Args:
|
||||
dist_ctx (DistributedContext): dist context.
|
||||
op (Operator): the current (backward) operator which might need.
|
||||
act_grad_names (list): list of input activation grads variable name to the current operator.
|
||||
out_grad_names (list): list of the output parameter's grads variable name of the current operator.
|
||||
rank (int): global ranks index for current process.
|
||||
"""
|
||||
|
||||
if not is_in_backward_phase(dist_ctx):
|
||||
return
|
||||
|
||||
if (
|
||||
is_optimize_op(op)
|
||||
or len(act_grad_names) == 0
|
||||
or len(out_grad_names) == 0
|
||||
):
|
||||
return
|
||||
|
||||
if op.type in _gradient_sync_by_partial_ops:
|
||||
sync_groups = get_partial_groups(dist_ctx, op, out_grad_names, rank)
|
||||
# NOTE we reverse the following old branch to support operators (e.g. fuse operators) that haven't been adopted for partial inferspmd,
|
||||
# and remove this branch after all operators are adopted for partial inferspmd.
|
||||
else:
|
||||
sync_groups = get_data_parallel_group(
|
||||
dist_ctx, op, act_grad_names, rank
|
||||
)
|
||||
|
||||
if len(sync_groups) < 1:
|
||||
return
|
||||
|
||||
sync_and_scale_gradients(dist_ctx, op, sync_groups, out_grad_names)
|
||||
|
||||
|
||||
def is_data_parallel_scale_op(op):
|
||||
return (
|
||||
op.type == "scale"
|
||||
and op.desc.has_attr("op_namescope")
|
||||
and ParallelMode.DataParallel in op.desc.attr("op_namescope")
|
||||
)
|
||||
|
||||
|
||||
def is_data_parallel_reduce_op(op):
|
||||
is_allreduce_op = op.type in [
|
||||
"c_allreduce_sum",
|
||||
"c_allreduce_avg",
|
||||
]
|
||||
is_all_reduce_op = op.type == "all_reduce" and op.desc.attr(
|
||||
"reduce_type"
|
||||
) in [
|
||||
dist.ReduceOp.SUM,
|
||||
dist.ReduceOp.AVG,
|
||||
]
|
||||
is_reduce_op = op.type == "reduce" and op.desc.attr("reduce_type") in [
|
||||
dist.ReduceOp.SUM,
|
||||
dist.ReduceOp.AVG,
|
||||
]
|
||||
return (
|
||||
(is_allreduce_op or is_all_reduce_op or is_reduce_op)
|
||||
and op.desc.has_attr("op_namescope")
|
||||
and ParallelMode.DataParallel in op.desc.attr("op_namescope")
|
||||
)
|
||||
|
||||
|
||||
def is_amp_flag_sync_op(op):
|
||||
return (
|
||||
op.type == "all_reduce"
|
||||
and op.desc.attr("op_type") == paddle.distributed.ReduceOp.MAX
|
||||
and op.desc.has_attr("op_namescope")
|
||||
and SyncMode.AmpFlagSync in op.desc.attr("op_namescope")
|
||||
)
|
||||
|
||||
|
||||
def is_global_norm_sync_op(op):
|
||||
return (
|
||||
op.type == "all_reduce"
|
||||
and op.desc.attr("reduce_type") == dist.ReduceOp.SUM
|
||||
and op.desc.has_attr("op_namescope")
|
||||
and SyncMode.GlobalNormSync in op.desc.attr("op_namescope")
|
||||
)
|
||||
|
||||
|
||||
def is_in_backward_phase(dist_ctx):
|
||||
# NOTE currently high-order differential in Paddle dose NOT distinguish gradient computation operators
|
||||
# in Forward phase and operators in Backward phase (both with op_role=1), which will mislead
|
||||
# auto parallel to add gradient synchronization for gradient computation operators in Forward phase.
|
||||
# we use this FLAG to distinguish these two phases temporarily.
|
||||
|
||||
return dist_ctx.dist_op_context.in_backward_phase()
|
||||
|
||||
|
||||
def merge_forward_backward_dims_mapping(fw_results, bw_results):
|
||||
flatten_fw_inputs = paddle.utils.flatten(fw_results[0])
|
||||
flatten_fw_outputs = paddle.utils.flatten(fw_results[1])
|
||||
flatten_bw_inputs = paddle.utils.flatten(bw_results[0])
|
||||
flatten_bw_outputs = paddle.utils.flatten(bw_results[1])
|
||||
ninputs = len(flatten_fw_inputs)
|
||||
noutputs = len(flatten_fw_outputs)
|
||||
inferred_input_dims_mappings = []
|
||||
inferred_output_dims_mappings = []
|
||||
|
||||
for i in range(ninputs):
|
||||
compatible_dims_mapping = compute_compatible_dims_mapping(
|
||||
[
|
||||
flatten_fw_inputs[i].dims_mapping,
|
||||
flatten_bw_inputs[i].dims_mapping,
|
||||
]
|
||||
)
|
||||
inferred_input_dims_mappings.append(compatible_dims_mapping)
|
||||
|
||||
for i in range(noutputs):
|
||||
compatible_dims_mapping = compute_compatible_dims_mapping(
|
||||
[
|
||||
flatten_fw_outputs[i].dims_mapping,
|
||||
flatten_bw_outputs[i].dims_mapping,
|
||||
]
|
||||
)
|
||||
inferred_output_dims_mappings.append(compatible_dims_mapping)
|
||||
return inferred_input_dims_mappings, inferred_output_dims_mappings
|
||||
|
||||
|
||||
def update_op_dims_mapping(
|
||||
dist_op, input_arg_names, output_arg_names, fw_results, bw_results
|
||||
):
|
||||
(
|
||||
inferred_input_dims_mappings,
|
||||
inferred_output_dims_mappings,
|
||||
) = merge_forward_backward_dims_mapping(fw_results, bw_results)
|
||||
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
changed = False
|
||||
if len(input_arg_names) != len(inferred_input_dims_mappings):
|
||||
warnings.warn(
|
||||
f"dims mapping is NOT Match, inferred [{len(inferred_input_dims_mappings)}], original: [{len(input_arg_names)}]; dist op: [{dist_op}]"
|
||||
)
|
||||
if len(output_arg_names) != len(inferred_output_dims_mappings):
|
||||
warnings.warn(
|
||||
f"dims mapping is NOT Match, inferred [{len(inferred_output_dims_mappings)}], original: [{len(output_arg_names)}]; dist op: [{dist_op}]"
|
||||
)
|
||||
|
||||
for i in range(len(input_arg_names)):
|
||||
original_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
input_arg_names[i]
|
||||
)
|
||||
inferred_dims_mapping = inferred_input_dims_mappings[i]
|
||||
if (inferred_dims_mapping is not None) and (
|
||||
original_dims_mapping != inferred_dims_mapping
|
||||
):
|
||||
_logger.debug(
|
||||
f"Changed: Op [{dist_op.serial_op.type}], name [{input_arg_names[i]}], Original [{original_dims_mapping}], Inferred [{inferred_dims_mapping}]"
|
||||
)
|
||||
changed = True
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
input_arg_names[i], inferred_dims_mapping
|
||||
)
|
||||
# TODO support partial for inputs
|
||||
|
||||
for i in range(len(output_arg_names)):
|
||||
original_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
output_arg_names[i]
|
||||
)
|
||||
inferred_dims_mapping = inferred_output_dims_mappings[i]
|
||||
if (inferred_dims_mapping is not None) and (
|
||||
original_dims_mapping != inferred_dims_mapping
|
||||
):
|
||||
_logger.debug(
|
||||
f"Changed: Op [{dist_op.serial_op.type}], name [{output_arg_names[i]}], Original [{original_dims_mapping}], Inferred [{inferred_dims_mapping}]"
|
||||
)
|
||||
changed = True
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
output_arg_names[i], inferred_dims_mapping
|
||||
)
|
||||
|
||||
# NOTE in partial stage-I, we infer partial for output in infer_forward only
|
||||
output_dist_attr = op_dist_attr.get_output_dist_attr(
|
||||
output_arg_names[i]
|
||||
)
|
||||
output_idx = output_arg_names.index(output_arg_names[i])
|
||||
if (
|
||||
fw_results[1][output_idx]._partial_dims()
|
||||
!= output_dist_attr._partial_dims()
|
||||
):
|
||||
# _logger.info(
|
||||
# "Changed: Op [{}], tensor name [{}], Original partial on [{}], Inferred partial on [{}]".format(
|
||||
# dist_op.serial_op.type,
|
||||
# output_arg_names[i],
|
||||
# output_dist_attr._partial_dims(),
|
||||
# fw_results[1][output_idx]._partial_dims(),
|
||||
# )
|
||||
# )
|
||||
output_dist_attr._clean_partial_status()
|
||||
output_dist_attr._set_partial_dims(
|
||||
list(fw_results[1][0]._partial_dims())
|
||||
)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
|
||||
def get_default_distributed_operator_impl():
|
||||
dist_op_default_impl_container = get_distributed_operator_impl_container(
|
||||
"default"
|
||||
)
|
||||
num_impls = len(dist_op_default_impl_container.impls)
|
||||
assert num_impls == 1, f"Default dist op has [{num_impls}] impls"
|
||||
return dist_op_default_impl_container.get_impl(0)
|
||||
|
||||
|
||||
def copy_op_without_infer_shape(src_op, block, ctx, varname_kwargs):
|
||||
new_op = block.append_op(type='nop')
|
||||
new_op_desc = new_op.desc
|
||||
new_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(new_op_desc, src_op.desc, ctx)
|
||||
for input_name in src_op.desc.input_names():
|
||||
new_op_desc.set_input(input_name, varname_kwargs[input_name])
|
||||
for output_name in src_op.desc.output_names():
|
||||
new_op_desc.set_output(output_name, varname_kwargs[output_name])
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
return new_op
|
||||
@@ -0,0 +1,90 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..utils import compute_compatible_and_update_dim_mapping
|
||||
from .common import DistributedOperatorImpl, DistributedOperatorImplContainer
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedAssign(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
# TODO remove assign dist op
|
||||
# register_distributed_operator_impl_container(DistributedAssign("assign"))
|
||||
|
||||
|
||||
class DistributedAssignImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
# register_distributed_operator_impl("assign", DistributedAssignImpl("assign"))
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
# 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 paddle
|
||||
from paddle.distributed.auto_parallel.static.process_group import (
|
||||
get_world_process_group,
|
||||
)
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
from paddle.framework import core
|
||||
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import set_dist_op_desc_original_id, set_var_dist_attr
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
SyncMode,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
|
||||
world_process_group = get_world_process_group()
|
||||
|
||||
|
||||
class DistributedCheckFiniteAndUnscale(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedCheckFiniteAndUnscale("check_finite_and_unscale")
|
||||
)
|
||||
|
||||
|
||||
class DistributedCheckFiniteAndUnscaleImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._name = name
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's is_input_compatible should not be called !"
|
||||
)
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's is_output_compatible should not be called !"
|
||||
)
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's is_auto_compatible should not be called !"
|
||||
)
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's update_dims_mapping should not be called !"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"DistributedCheckFiniteAndUnscaleImpl's forward should not be called !"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# by now the backward function only insert the gradient allreduce for dist op itself
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.main_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
assert rank_id in dist_attr.process_mesh.process_ids
|
||||
|
||||
assert 'X' in kwargs, "input [{}] is not given".format('X')
|
||||
assert 'Scale' in kwargs, "input [{}] is not given".format('Scale')
|
||||
assert 'Out' in kwargs, "input [{}] is not given".format('Out')
|
||||
assert 'FoundInfinite' in kwargs, "output [{}] is not given".format(
|
||||
'FoundInfinite'
|
||||
)
|
||||
|
||||
assert len(kwargs['Scale']) == 1, (
|
||||
"check_finite_and_unscale input Scale take 1 variable but got {}".format(
|
||||
kwargs['Scale']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['FoundInfinite']) == 1, (
|
||||
"check_finite_and_unscale input FoundInfinite take 1 variable but got {}".format(
|
||||
kwargs['FoundInfinite']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['X']) == len(kwargs['Out']), (
|
||||
"check_finite_and_unscale got [{}] X and [{}] Out, which are supposed to be equal".format(
|
||||
len(kwargs['X']), len(kwargs['Out'])
|
||||
)
|
||||
)
|
||||
|
||||
filter_vars = []
|
||||
for varname in kwargs['X']:
|
||||
if (
|
||||
rank_id
|
||||
in ctx.get_tensor_dist_attr_for_program(
|
||||
main_block._var_recursive(varname)
|
||||
).process_mesh.process_ids
|
||||
):
|
||||
filter_vars.append(varname)
|
||||
|
||||
# replicate op in dist program
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(backward_op.desc)
|
||||
set_dist_op_desc_original_id(dist_op_desc, backward_op.desc, ctx)
|
||||
dist_op_desc.set_input('X', filter_vars)
|
||||
dist_op_desc.set_output('Out', filter_vars)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
# sync result
|
||||
group = new_process_group(world_process_group.ranks)
|
||||
|
||||
inf_var = main_block._var_recursive(kwargs['FoundInfinite'][0])
|
||||
inf_var_int32 = main_block.create_var(
|
||||
name=inf_var.name + "@cast_int32",
|
||||
shape=inf_var.shape,
|
||||
dtype=core.VarDesc.VarType.INT32,
|
||||
)
|
||||
set_var_dist_attr(
|
||||
ctx,
|
||||
inf_var_int32,
|
||||
ctx.get_tensor_dist_attr_for_program(inf_var).dims_mapping,
|
||||
ctx.get_tensor_dist_attr_for_program(inf_var).process_mesh,
|
||||
)
|
||||
cast_op1 = main_block.append_op(
|
||||
type='cast',
|
||||
inputs={'X': inf_var},
|
||||
outputs={'Out': inf_var_int32},
|
||||
attrs={
|
||||
"in_dtype": inf_var.dtype,
|
||||
"out_dtype": inf_var_int32.dtype,
|
||||
OP_ROLE_KEY: OpRole.Optimize,
|
||||
},
|
||||
)
|
||||
allreduce_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': inf_var_int32},
|
||||
outputs={'out': inf_var_int32},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'op_type': paddle.distributed.ReduceOp.MAX,
|
||||
OP_ROLE_KEY: OpRole.Optimize,
|
||||
},
|
||||
)
|
||||
allreduce_op._set_attr('op_namescope', '/' + SyncMode.AmpFlagSync)
|
||||
cast_op2 = main_block.append_op(
|
||||
type='cast',
|
||||
inputs={'X': inf_var_int32},
|
||||
outputs={'Out': inf_var},
|
||||
attrs={
|
||||
"in_dtype": inf_var_int32.dtype,
|
||||
"out_dtype": inf_var.dtype,
|
||||
OP_ROLE_KEY: OpRole.Optimize,
|
||||
},
|
||||
)
|
||||
|
||||
for op in [cast_op1, allreduce_op, cast_op2]:
|
||||
new_op_dist_attr = OperatorDistAttr()
|
||||
for varname in op.input_arg_names:
|
||||
var_dist_attr = ctx.get_tensor_dist_attr_for_program(
|
||||
main_block._var_recursive(varname)
|
||||
)
|
||||
assert var_dist_attr is not None
|
||||
new_op_dist_attr.set_input_dims_mapping(
|
||||
varname, var_dist_attr.dims_mapping
|
||||
)
|
||||
for varname in op.output_arg_names:
|
||||
var_dist_attr = ctx.get_tensor_dist_attr_for_program(
|
||||
main_block._var_recursive(varname)
|
||||
)
|
||||
new_op_dist_attr.set_output_dims_mapping(
|
||||
varname, var_dist_attr.dims_mapping
|
||||
)
|
||||
new_op_dist_attr.process_mesh = var_dist_attr.process_mesh
|
||||
ctx.set_op_dist_attr_for_program(op, new_op_dist_attr)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"check_finite_and_unscale",
|
||||
DistributedCheckFiniteAndUnscaleImpl("check_finite_and_unscale"),
|
||||
)
|
||||
@@ -0,0 +1,76 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedConcat(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
axis_tensor = op_desc.input('AxisTensor')
|
||||
assert len(axis_tensor) == 0, (
|
||||
"Please use axis attr instead of AxisTensor"
|
||||
)
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
input_specs = []
|
||||
for name in input_arg_names:
|
||||
input_specs.append(get_dist_tensor_spec(dist_op, name))
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_names[0], False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("concat")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(input_specs, axis)
|
||||
bw_results = rule.infer_backward(input_specs, output_spec, axis)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names,
|
||||
output_arg_names,
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedConcat("concat"))
|
||||
@@ -0,0 +1,528 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import copy
|
||||
|
||||
from paddle.common_ops_import import check_variable_and_dtype
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
get_dist_tensor_spec,
|
||||
is_dim_shard,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
ParallelMode,
|
||||
copy_op_without_infer_shape,
|
||||
naive_copy_op_dist_attr_for_program,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedCrossEntropy(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
logits_name = op_desc.input('Logits')[0]
|
||||
label_name = op_desc.input('Label')[0]
|
||||
loss_name = op_desc.output('Loss')[0]
|
||||
softmax_name = op_desc.output('Softmax')[0]
|
||||
|
||||
soft_label = op_desc.attr('soft_label')
|
||||
ignore_index = op_desc.attr('ignore_index')
|
||||
numeric_stable_mode = op_desc.attr('numeric_stable_mode')
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
logits_spec = get_dist_tensor_spec(dist_op, logits_name)
|
||||
label_spec = get_dist_tensor_spec(dist_op, label_name)
|
||||
loss_spec = get_dist_tensor_spec(dist_op, loss_name, False)
|
||||
softmax_spec = get_dist_tensor_spec(dist_op, softmax_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("softmax_with_cross_entropy")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(
|
||||
logits_spec,
|
||||
label_spec,
|
||||
soft_label,
|
||||
True,
|
||||
numeric_stable_mode,
|
||||
ignore_index,
|
||||
axis,
|
||||
)
|
||||
bw_results = rule.infer_backward(
|
||||
logits_spec,
|
||||
label_spec,
|
||||
softmax_spec,
|
||||
loss_spec,
|
||||
soft_label,
|
||||
True,
|
||||
numeric_stable_mode,
|
||||
ignore_index,
|
||||
axis,
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[logits_name, label_name],
|
||||
[softmax_name, loss_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
op_dist_attr.impl_type = op_desc.type()
|
||||
|
||||
logits_name = op_desc.input('Logits')[0]
|
||||
|
||||
soft_label = op_desc.attr('soft_label')
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
logits_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_input_dims_mapping(logits_name)
|
||||
)
|
||||
logits_ndim = len(logits_dims_mapping)
|
||||
axis = axis + logits_ndim if axis < 0 else axis
|
||||
|
||||
if is_dim_shard(logits_dims_mapping[axis]):
|
||||
assert soft_label is False, (
|
||||
"parallel_cross_entropy does not support soft_label now."
|
||||
)
|
||||
assert axis == logits_ndim - 1, (
|
||||
"parallel_cross_entropy can only support shard on the last dim now."
|
||||
)
|
||||
op_dist_attr.impl_idx = 1
|
||||
else:
|
||||
op_dist_attr.impl_idx = 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedCrossEntropy("softmax_with_cross_entropy")
|
||||
)
|
||||
|
||||
|
||||
# The softmax_norm axis is not sharded
|
||||
class DistributedCrossEntropyImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Logits' in kwargs, "input [Logits] is not given"
|
||||
assert 'Label' in kwargs, "input [Label] is not given"
|
||||
assert 'Loss' in kwargs, "output [Loss] is not given"
|
||||
assert 'Softmax' in kwargs, "output [Softmax] is not given"
|
||||
|
||||
assert len(kwargs['Logits']) == 1, (
|
||||
"input [Logits] take 1 variable but got {}".format(kwargs['Logits'])
|
||||
)
|
||||
assert len(kwargs['Label']) == 1, (
|
||||
"input [Label] take 1 variable but got {}".format(kwargs['Label'])
|
||||
)
|
||||
|
||||
logits_var = main_block._var_recursive(kwargs['Logits'][0])
|
||||
label_var = main_block._var_recursive(kwargs['Label'][0])
|
||||
loss_var = main_block._var_recursive(kwargs['Loss'][0])
|
||||
softmax_var = main_block._var_recursive(kwargs['Softmax'][0])
|
||||
|
||||
# got dist attribute info
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh_group:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
check_variable_and_dtype(
|
||||
logits_var,
|
||||
'input',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'cross_entropy_with_softmax',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
label_var,
|
||||
'input',
|
||||
['int32', 'int64', 'float32', 'float64'],
|
||||
'cross_entropy_with_softmax',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
loss_var,
|
||||
'output',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'cross_entropy_with_softmax',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
softmax_var,
|
||||
'output',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'cross_entropy_with_softmax',
|
||||
)
|
||||
copy_op_without_infer_shape(src_op, main_block, ctx, kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Softmax' in kwargs, "input [Logits] is not given"
|
||||
assert 'Label' in kwargs, "input [Label] is not given"
|
||||
assert 'Loss@GRAD' in kwargs, "input [Loss@GRAD] is not given"
|
||||
assert 'Logits@GRAD' in kwargs, "output [Logits@GRAD] is not given"
|
||||
|
||||
assert len(kwargs['Softmax']) == 1, (
|
||||
"input [Softmax] take 1 variable but got {}".format(
|
||||
kwargs['Softmax']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Label']) == 1, (
|
||||
"input [Label] take 1 variable but got {}".format(kwargs['Label'])
|
||||
)
|
||||
assert len(kwargs['Loss@GRAD']) == 1, (
|
||||
"input [Loss@GRAD] take 1 variable but got {}".format(kwargs['Out'])
|
||||
)
|
||||
assert len(kwargs['Logits@GRAD']) == 1, (
|
||||
"output [Logits@GRAD] take 1 variable but got {}".format(
|
||||
kwargs['Logits@GRAD']
|
||||
)
|
||||
)
|
||||
# replicate op in dist program
|
||||
copy_op_without_infer_shape(backward_op, main_block, ctx, kwargs)
|
||||
|
||||
|
||||
class DistributedCrossEntropyImpl1(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Logits' in kwargs, "input [Logits] is not given"
|
||||
assert 'Label' in kwargs, "input [Label] is not given"
|
||||
assert 'Loss' in kwargs, "output [Loss] is not given"
|
||||
assert 'Softmax' in kwargs, "output [Softmax] is not given"
|
||||
|
||||
assert len(kwargs['Logits']) == 1, (
|
||||
"input [Logits] take 1 variable but got {}".format(kwargs['Logits'])
|
||||
)
|
||||
assert len(kwargs['Label']) == 1, (
|
||||
"input [Label] take 1 variable but got {}".format(kwargs['Label'])
|
||||
)
|
||||
|
||||
logits_var = main_block._var_recursive(kwargs['Logits'][0])
|
||||
label_var = main_block._var_recursive(kwargs['Label'][0])
|
||||
loss_var = main_block._var_recursive(kwargs['Loss'][0])
|
||||
softmax_var = main_block._var_recursive(kwargs['Softmax'][0])
|
||||
|
||||
# got dist attribute info
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh_group:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
check_variable_and_dtype(
|
||||
logits_var,
|
||||
'input',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'c_softmax_with_cross_entropy',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
label_var,
|
||||
'input',
|
||||
['int32', 'int64', 'float32', 'float64'],
|
||||
'c_softmax_with_cross_entropy',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
loss_var,
|
||||
'output',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'c_softmax_with_cross_entropy',
|
||||
)
|
||||
check_variable_and_dtype(
|
||||
softmax_var,
|
||||
'output',
|
||||
['bfloat16', 'float16', 'float32', 'float64'],
|
||||
'c_softmax_with_cross_entropy',
|
||||
)
|
||||
|
||||
# infer new var shape with op dist attr
|
||||
# the dims mapping in dist_op may be different from that in tensor
|
||||
# so we should
|
||||
loss_dist_attr = ctx.get_tensor_dist_attr_for_program(loss_var)
|
||||
assert loss_dist_attr is not None
|
||||
softmax_dist_attr = ctx.get_tensor_dist_attr_for_program(softmax_var)
|
||||
assert softmax_dist_attr is not None
|
||||
op_dist_attr_loss = op_dist_attr.get_output_dist_attr(loss_var.name)
|
||||
assert op_dist_attr_loss is not None
|
||||
op_dist_attr_softmax = op_dist_attr.get_output_dist_attr(
|
||||
softmax_var.name
|
||||
)
|
||||
assert op_dist_attr_softmax is not None
|
||||
|
||||
# TODO calculate ring id
|
||||
softmax_axis = src_op.desc.attr('axis')
|
||||
logits_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
logits_var.name
|
||||
)
|
||||
parallel_axis = logits_dims_mapping[softmax_axis]
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
c_cross_entropy_op = main_block.append_op(
|
||||
type='c_softmax_with_cross_entropy',
|
||||
inputs={
|
||||
'Logits': logits_var,
|
||||
'Label': label_var,
|
||||
},
|
||||
outputs={
|
||||
'Loss': loss_var,
|
||||
'Softmax': softmax_var,
|
||||
},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'rank': group.local_rank(rank_id),
|
||||
'nranks': group.nranks,
|
||||
'ignore_index': src_op.desc.attr('ignore_index'),
|
||||
OP_ROLE_KEY: src_op.attr('op_role'),
|
||||
},
|
||||
)
|
||||
naive_copy_op_dist_attr_for_program(c_cross_entropy_op, src_op, ctx)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Softmax' in kwargs, "input [Softmax] is not given"
|
||||
assert 'Label' in kwargs, "input [Label] is not given"
|
||||
assert 'Loss@GRAD' in kwargs, "input [Loss@GRAD] is not given"
|
||||
assert 'Logits@GRAD' in kwargs, "output [Logits@GRAD] is not given"
|
||||
|
||||
assert len(kwargs['Softmax']) == 1, (
|
||||
"input [Softmax] take 1 variable but got {}".format(
|
||||
kwargs['Softmax']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Label']) == 1, (
|
||||
"input [Label] take 1 variable but got {}".format(kwargs['Label'])
|
||||
)
|
||||
assert len(kwargs['Loss@GRAD']) == 1, (
|
||||
"input [Loss@GRAD] take 1 variable but got {}".format(
|
||||
kwargs['Loss@GRAD']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Logits@GRAD']) == 1, (
|
||||
"output [Logits@GRAD] take 1 variable but got {}".format(
|
||||
kwargs['Logits@GRAD']
|
||||
)
|
||||
)
|
||||
|
||||
# got dist attribute info
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh_group:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
for op in main_block.ops:
|
||||
# the output value of reduce_mean_grad is 1/numel, so when the
|
||||
# tensor is sharded, we should insert a scale op to make the
|
||||
# grad correct.
|
||||
if (
|
||||
op.type == "reduce_mean_grad"
|
||||
and kwargs['Loss@GRAD'][0] in op.output_arg_names
|
||||
):
|
||||
loss_grad_var = main_block._var_recursive(
|
||||
kwargs['Loss@GRAD'][0]
|
||||
)
|
||||
loss_grad_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
loss_grad_var.name
|
||||
)
|
||||
degree = 1.0
|
||||
for i in range(len(loss_grad_dims_mapping) - 1):
|
||||
if loss_grad_dims_mapping[i] != -1:
|
||||
degree *= process_mesh_shape[loss_grad_dims_mapping[i]]
|
||||
if degree > 1:
|
||||
scale_op = main_block.append_op(
|
||||
type='scale',
|
||||
inputs={'X': loss_grad_var},
|
||||
outputs={'Out': loss_grad_var},
|
||||
attrs={
|
||||
'scale': 1.0 / degree,
|
||||
OP_ROLE_KEY: OpRole.Backward,
|
||||
},
|
||||
)
|
||||
scale_op._set_attr(
|
||||
'op_namescope', '/' + ParallelMode.DataParallel
|
||||
)
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
loss_grad_var.name
|
||||
)
|
||||
scale_op_attr = OperatorDistAttr()
|
||||
scale_op_attr.process_mesh = op_dist_attr.process_mesh
|
||||
scale_op_attr.chunk_id = op_dist_attr.chunk_id
|
||||
scale_op_attr.set_output_dims_mapping(
|
||||
loss_grad_var.name, dims_mapping
|
||||
)
|
||||
scale_op_attr.set_input_dims_mapping(
|
||||
loss_grad_var.name, dims_mapping
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(scale_op, scale_op_attr)
|
||||
|
||||
# TODO calculate ring id
|
||||
softmax_axis = backward_op.desc.attr('axis')
|
||||
# softmax_dims_mapping is the same as logits_dims_mapping
|
||||
softmax_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
kwargs['Softmax'][0]
|
||||
)
|
||||
parallel_axis = softmax_dims_mapping[softmax_axis]
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
cross_entropy_grad_op_desc = main_block.append_op(type='nop').desc
|
||||
cross_entropy_grad_op_desc.set_type("c_softmax_with_cross_entropy_grad")
|
||||
cross_entropy_grad_op_desc.set_input('Softmax', [kwargs['Softmax'][0]])
|
||||
cross_entropy_grad_op_desc.set_input('Label', [kwargs['Label'][0]])
|
||||
cross_entropy_grad_op_desc.set_input(
|
||||
'Loss@GRAD', [kwargs['Loss@GRAD'][0]]
|
||||
)
|
||||
cross_entropy_grad_op_desc.set_output(
|
||||
'Logits@GRAD', [kwargs['Logits@GRAD'][0]]
|
||||
)
|
||||
|
||||
ignore_index = backward_op.desc.attr('ignore_index')
|
||||
# the ignore_index attribute in c_cross_entropy_grad kernel
|
||||
# is int64_t type, so we should set this attribute with
|
||||
# _set_int64_attr. Otherwise ignore_index will be int32 type,
|
||||
# causing an error.
|
||||
cross_entropy_grad_op_desc._set_int64_attr('ignore_index', ignore_index)
|
||||
cross_entropy_grad_op_desc._set_attr('ring_id', group.id)
|
||||
cross_entropy_grad_op_desc._set_attr('rank', group.local_rank(rank_id))
|
||||
cross_entropy_grad_op_desc._set_attr('nranks', group.nranks)
|
||||
cross_entropy_grad_op_desc._set_attr(OP_ROLE_KEY, OpRole.Backward)
|
||||
|
||||
cross_entropy_grad_op = main_block.ops[-1]
|
||||
naive_copy_op_dist_attr_for_program(
|
||||
cross_entropy_grad_op, backward_op, ctx
|
||||
)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"softmax_with_cross_entropy", DistributedCrossEntropyImpl0("cross_entropy")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"softmax_with_cross_entropy",
|
||||
DistributedCrossEntropyImpl1("c_cross_entropy"),
|
||||
)
|
||||
@@ -0,0 +1,681 @@
|
||||
# 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 paddle
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
|
||||
from ..completion import contains_spmd_rule, get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
_g_op_cost_factory,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..dist_attribute import DistTensorSpec, OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
is_prim_op,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
copy_op_without_infer_shape,
|
||||
get_default_distributed_operator_impl,
|
||||
gradient_synchronization,
|
||||
is_parameter_related,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
set_comm_op_dist_attr_for_program,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
__op_not_need_param_init__ = ["while", "cond"]
|
||||
__op_has_shape_attr__ = [
|
||||
"fill_constant_batch_size_like",
|
||||
"fill_constant",
|
||||
"expand_v2",
|
||||
"expand_as_v2",
|
||||
]
|
||||
|
||||
|
||||
def prim_operator_data_parallel_functor(ctx, src_op):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
|
||||
var_name = src_op.output_arg_names[0]
|
||||
if var_name in ctx.grads_params:
|
||||
assert var_name not in ctx.synced_gradient, (
|
||||
f"in primitive mode, grad is already {var_name} synced"
|
||||
)
|
||||
ctx.synced_gradient.add(var_name)
|
||||
sync_group = new_process_group(ctx.data_parallel_group)
|
||||
|
||||
allreduce_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': [var_name]},
|
||||
outputs={'out': [var_name]},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'reduce_type': paddle.distributed.ReduceOp.SUM,
|
||||
OP_ROLE_KEY: OpRole.Backward,
|
||||
},
|
||||
)
|
||||
|
||||
param = ctx.grads_params[var_name]
|
||||
startup_block = dist_op_context.startup_block
|
||||
new_op = startup_block.append_op(
|
||||
type='broadcast',
|
||||
inputs={'x': [param]},
|
||||
outputs={'out': [param]},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'root': 0,
|
||||
OP_ROLE_KEY: OpRole.Forward,
|
||||
},
|
||||
)
|
||||
|
||||
grad_var = main_block._var_recursive(var_name)
|
||||
dims_mapping = ctx.get_tensor_dist_attr_for_program(
|
||||
grad_var
|
||||
).dims_mapping
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
process_mesh = dist_attr.process_mesh
|
||||
op_attr = OperatorDistAttr()
|
||||
op_attr.process_mesh = process_mesh
|
||||
op_attr.set_output_dims_mapping(grad_var.name, dims_mapping)
|
||||
op_attr.set_input_dims_mapping(grad_var.name, dims_mapping)
|
||||
ctx.set_op_dist_attr_for_program(allreduce_op, op_attr)
|
||||
|
||||
|
||||
class DistributedDefault(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
main_block = dist_op.serial_op.block
|
||||
|
||||
num_inputs = len(input_arg_names)
|
||||
input_specs = []
|
||||
for i in range(num_inputs):
|
||||
assert not is_parameter_related(input_arg_names[i], main_block), (
|
||||
f"input {input_arg_names[i]} of op {dist_op.serial_op} is parameter, op should not use default rule."
|
||||
)
|
||||
input_specs.append(
|
||||
get_dist_tensor_spec(dist_op, input_arg_names[i])
|
||||
)
|
||||
num_outputs = len(output_arg_names)
|
||||
output_specs = []
|
||||
for i in range(num_outputs):
|
||||
assert not is_parameter_related(output_arg_names[i], main_block), (
|
||||
f"output {output_arg_names[i]} of op {dist_op.serial_op} is parameter, op should not use default rule."
|
||||
)
|
||||
output_specs.append(
|
||||
get_dist_tensor_spec(dist_op, output_arg_names[i], False)
|
||||
)
|
||||
|
||||
# step2: infer spmd
|
||||
if contains_spmd_rule(dist_op.serial_op.type):
|
||||
# when some inputs are optional, the input_arg_names will be less than input_names
|
||||
# and we can pass empty DistTensorSpec() as argument
|
||||
if len(op_desc.input_names()) > len(op_desc.input_arg_names()):
|
||||
for i in range(
|
||||
len(op_desc.input_names()) - len(op_desc.input_arg_names())
|
||||
):
|
||||
input_specs.append(DistTensorSpec())
|
||||
rule = get_phi_spmd_rule(dist_op.serial_op.type)
|
||||
fw_results = rule.infer_forward(*input_specs)
|
||||
bw_results = rule.infer_backward(*input_specs, output_specs)
|
||||
else:
|
||||
rule = get_phi_spmd_rule('default_')
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(input_specs, output_specs)
|
||||
bw_results = rule.infer_backward(input_specs, output_specs)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, input_arg_names, output_arg_names, fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedDefault("default"))
|
||||
|
||||
|
||||
# Replicated Default
|
||||
class DistributedDefaultImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
"""Calculate the cost by the op role."""
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res_cost = [cost_mapping]
|
||||
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
backward_op = dist_op.serial_op
|
||||
op_type = backward_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
need_gradient_allreduce = True
|
||||
break
|
||||
|
||||
if need_gradient_allreduce:
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
varname
|
||||
)
|
||||
mesh_shape = process_mesh.shape
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
batch_dim_mappings = []
|
||||
input_names = op_desc.input_names()
|
||||
xshape_arg_names = []
|
||||
if "XShape" in input_names:
|
||||
xshape_arg_names = op_desc.input("XShape")
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
serial_tensor = dist_op.get_serial_input(arg_name)
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
for mapping in dims_mapping:
|
||||
if mapping != -1:
|
||||
return False
|
||||
continue
|
||||
if arg_name not in xshape_arg_names:
|
||||
if len(dims_mapping) > 1:
|
||||
for mapping in dims_mapping[1:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
if dims_mapping[0] != -1:
|
||||
return False
|
||||
if len(dims_mapping) > 2:
|
||||
for mapping in dims_mapping[2:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 2:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
if compute_compatible_dim_mapping(batch_dim_mappings) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
output_names = op_desc.output_names()
|
||||
batch_dim_mappings = []
|
||||
xshape_arg_names = []
|
||||
if "XShape" in output_names:
|
||||
xshape_arg_names = op_desc.output("XShape")
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
serial_tensor = dist_op.get_serial_output(arg_name)
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
for mapping in dims_mapping:
|
||||
if mapping != -1:
|
||||
return False
|
||||
continue
|
||||
if arg_name not in xshape_arg_names:
|
||||
if len(dims_mapping) > 1:
|
||||
for mapping in dims_mapping[1:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
if dims_mapping[0] != -1:
|
||||
return False
|
||||
if len(dims_mapping) > 2:
|
||||
for mapping in dims_mapping[2:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 2:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
if compute_compatible_dim_mapping(batch_dim_mappings) is None:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
batch_dim_mappings = []
|
||||
# Check input compatibility
|
||||
input_names = op_desc.input_names()
|
||||
xshape_arg_names = []
|
||||
if "XShape" in input_names:
|
||||
xshape_arg_names = op_desc.input("XShape")
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
serial_tensor = dist_op.get_serial_input(arg_name)
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if serial_tensor is not None and serial_tensor.is_parameter:
|
||||
for mapping in dims_mapping:
|
||||
if mapping != -1:
|
||||
return False
|
||||
continue
|
||||
if arg_name not in xshape_arg_names:
|
||||
if len(dims_mapping) > 1:
|
||||
for mapping in dims_mapping[1:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
if dims_mapping[0] != -1:
|
||||
return False
|
||||
if len(dims_mapping) > 2:
|
||||
for mapping in dims_mapping[2:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 2:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
# Check output compatibility
|
||||
output_names = op_desc.output_names()
|
||||
xshape_arg_names = []
|
||||
if "XShape" in output_names:
|
||||
xshape_arg_names = op_desc.output("XShape")
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
serial_tensor = dist_op.get_serial_output(arg_name)
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if serial_tensor is not None and serial_tensor.is_parameter:
|
||||
for mapping in dims_mapping:
|
||||
if mapping != -1:
|
||||
return False
|
||||
continue
|
||||
if arg_name not in xshape_arg_names:
|
||||
if len(dims_mapping) > 1:
|
||||
for mapping in dims_mapping[1:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
if dims_mapping[0] != -1:
|
||||
return False
|
||||
if len(dims_mapping) > 2:
|
||||
for mapping in dims_mapping[2:]:
|
||||
if mapping != -1:
|
||||
return False
|
||||
if len(dims_mapping) >= 2:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
# Check batch dim mapping compatibility
|
||||
if not all(
|
||||
batch_dim_mappings[0] == dim_mapping
|
||||
for dim_mapping in batch_dim_mappings
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
if op_desc.type() == "while":
|
||||
return False
|
||||
|
||||
input_names = op_desc.input_names()
|
||||
input_xshape_arg_names = []
|
||||
if "XShape" in input_names:
|
||||
input_xshape_arg_names = op_desc.input("XShape")
|
||||
|
||||
output_names = op_desc.output_names()
|
||||
output_xshape_arg_names = []
|
||||
if "XShape" in output_names:
|
||||
output_xshape_arg_names = op_desc.output("XShape")
|
||||
|
||||
batch_dim_mappings = []
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
serial_tensor = dist_op.get_serial_input(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
continue
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if arg_name not in input_xshape_arg_names:
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
if op_desc.type() == 'fill_any_like':
|
||||
input_tensor = dist_op.get_serial_input(
|
||||
op_desc.input_arg_names()[0]
|
||||
)
|
||||
if input_tensor.is_parameter:
|
||||
continue
|
||||
serial_tensor = dist_op.get_serial_output(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
continue
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if arg_name not in output_xshape_arg_names:
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
else:
|
||||
batch_dim_mappings.append(dims_mapping[1])
|
||||
|
||||
if not batch_dim_mappings:
|
||||
return changed
|
||||
|
||||
compatible_dim_mapping = compute_compatible_dim_mapping(
|
||||
batch_dim_mappings
|
||||
)
|
||||
if compatible_dim_mapping is None:
|
||||
return False
|
||||
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
serial_tensor = dist_op.get_serial_input(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
continue
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if arg_name not in input_xshape_arg_names:
|
||||
if (
|
||||
len(dims_mapping) >= 1
|
||||
and compatible_dim_mapping != dims_mapping[0]
|
||||
):
|
||||
dims_mapping[0] = compatible_dim_mapping
|
||||
op_dist_attr.set_input_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
else:
|
||||
if (
|
||||
len(dims_mapping) >= 2
|
||||
and compatible_dim_mapping != dims_mapping[1]
|
||||
):
|
||||
dims_mapping[1] = compatible_dim_mapping
|
||||
op_dist_attr.set_input_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
if op_desc.type() == 'fill_any_like':
|
||||
input_tensor = dist_op.get_serial_input(
|
||||
op_desc.input_arg_names()[0]
|
||||
)
|
||||
if input_tensor.is_parameter:
|
||||
continue
|
||||
if op_desc.type() in ["shape", "slice"]:
|
||||
continue
|
||||
serial_tensor = dist_op.get_serial_output(arg_name)
|
||||
if serial_tensor.is_parameter:
|
||||
continue
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if arg_name not in output_xshape_arg_names:
|
||||
if (
|
||||
len(dims_mapping) >= 1
|
||||
and compatible_dim_mapping != dims_mapping[0]
|
||||
):
|
||||
dims_mapping[0] = compatible_dim_mapping
|
||||
op_dist_attr.set_output_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
else:
|
||||
if (
|
||||
len(dims_mapping) >= 2
|
||||
and compatible_dim_mapping != dims_mapping[1]
|
||||
):
|
||||
dims_mapping[1] = compatible_dim_mapping
|
||||
op_dist_attr.set_output_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
# replicate op in dist program
|
||||
dst_op = copy_op_without_infer_shape(src_op, main_block, ctx, kwargs)
|
||||
|
||||
def get_shape_attr_name():
|
||||
for name in ["shape", "target_shape"]:
|
||||
if src_op.has_attr(name) and src_op.attr(name):
|
||||
return name
|
||||
return None
|
||||
|
||||
shape_attr_name = get_shape_attr_name()
|
||||
if shape_attr_name and src_op.type in __op_has_shape_attr__:
|
||||
shape_list = src_op.attr(shape_attr_name)
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
dim_mapping = op_dist_attr.get_output_dims_mapping(Out_var.name)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
assert len(shape_list) == len(dim_mapping)
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
dst_op.desc._set_attr(shape_attr_name, shape_list)
|
||||
|
||||
# data parallel synchronization for primitive operators
|
||||
from paddle.incubate.autograd import prim_enabled
|
||||
|
||||
if prim_enabled():
|
||||
assert is_prim_op(src_op)
|
||||
prim_operator_data_parallel_functor(ctx, src_op)
|
||||
return
|
||||
|
||||
# param initialization sync
|
||||
if src_op.type in __op_not_need_param_init__:
|
||||
return
|
||||
|
||||
for varname in dst_op.desc.input_arg_names():
|
||||
if (
|
||||
startup_block.has_var(varname)
|
||||
and startup_block.var(varname).is_parameter
|
||||
and varname not in dist_op_context.already_init_sync_vars
|
||||
):
|
||||
dist_op_context.already_init_sync_vars.add(varname)
|
||||
param = startup_block.var(varname)
|
||||
param_dist_attr = ctx.get_tensor_dist_attr_for_program(param)
|
||||
process_mesh = param_dist_attr.process_mesh
|
||||
dims_mapping = param_dist_attr.dims_mapping
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, process_mesh, rank_id
|
||||
)
|
||||
|
||||
# NOTE all not splited axis should be presented in mesh
|
||||
for axis, size in enumerate(process_mesh.shape):
|
||||
if size <= 1 or axis in dims_mapping:
|
||||
pass
|
||||
else:
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
axis,
|
||||
rank_id,
|
||||
)
|
||||
sync_group = new_process_group(group_ranks)
|
||||
|
||||
new_op = startup_block.append_op(
|
||||
type='broadcast',
|
||||
inputs={'x': param},
|
||||
outputs={'out': param},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'root': 0,
|
||||
OP_ROLE_KEY: OpRole.Forward,
|
||||
},
|
||||
)
|
||||
set_comm_op_dist_attr_for_program(
|
||||
new_op,
|
||||
process_mesh,
|
||||
param_dist_attr,
|
||||
ctx,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# by now the backward function only insert the gradient allreduce for dist op itself
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
rank_id = dist_op_context.rank_id
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in backward_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
backward_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in backward_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
backward_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
# replicate op in dist program
|
||||
copy_op_without_infer_shape(backward_op, main_block, ctx, kwargs)
|
||||
|
||||
# data parallel gradient synchronization
|
||||
act_grad_names = []
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
act_grad_names.append(varname)
|
||||
|
||||
out_grad_names = []
|
||||
for output_name in backward_op.desc.output_names():
|
||||
for varname in backward_op.desc.output(output_name):
|
||||
if varname in kwargs["grad_var_to_var"]:
|
||||
fwd_name = kwargs["grad_var_to_var"][varname]
|
||||
if not main_block._find_var_recursive(fwd_name):
|
||||
continue
|
||||
if is_parameter_related(fwd_name, main_block):
|
||||
out_grad_names.append(varname)
|
||||
|
||||
gradient_synchronization(
|
||||
ctx, backward_op, act_grad_names, out_grad_names, rank_id
|
||||
)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"default", DistributedDefaultImpl0("replicate_parallel")
|
||||
)
|
||||
@@ -0,0 +1,238 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
import logging
|
||||
|
||||
import paddle
|
||||
from paddle.base.log_helper import get_logger
|
||||
from paddle.framework import core
|
||||
from paddle.utils import unique_name
|
||||
|
||||
from ...random import determinate_rng, is_enable_auto_rand_ctrl
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import (
|
||||
get_dist_tensor_spec,
|
||||
naive_set_dist_op_attr_for_program_by_mesh_and_mapping,
|
||||
set_var_dist_attr,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
merge_forward_backward_dims_mapping,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_eltwise import DistributedDefaultImpl0, DistributedElementwiseImpl0
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
class DistributedDropout(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
mask_name = op_desc.output('Mask')[0]
|
||||
# seed_name = op_desc.input('Seed')[0] // seed is a scalar and leave it to be unsharded
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("dropout")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec)
|
||||
bw_results = rule.infer_backward(x_spec, output_spec)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
# step5: update mask and seed dropout special
|
||||
if changed:
|
||||
(
|
||||
_,
|
||||
inferred_output_dims_mappings,
|
||||
) = merge_forward_backward_dims_mapping(fw_results, bw_results)
|
||||
dist_op.dist_attr.set_output_dims_mapping(
|
||||
mask_name, inferred_output_dims_mappings[0]
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all dropout op use Dropout with Random Control dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
op_dist_attr.impl_type = "dropout"
|
||||
op_dist_attr.impl_idx = 0
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedDropout("dropout"))
|
||||
|
||||
|
||||
# Dist Dropout with Random Control
|
||||
# Dropout re-use the compatible and cost function of elementwise
|
||||
class DistributedDropoutImpl0(DistributedElementwiseImpl0):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
if is_enable_auto_rand_ctrl() and not op_dist_attr.is_recompute:
|
||||
# check validation of inputs / outputs
|
||||
assert 'X' in kwargs, "input [{}] is not given".format('X')
|
||||
assert len(kwargs['X']) == 1, (
|
||||
"input X should be only one tensor but got {}".format(
|
||||
kwargs['X']
|
||||
)
|
||||
)
|
||||
assert 'Seed' in kwargs, "input [{}] is not given".format('Seed')
|
||||
|
||||
if (
|
||||
src_op.has_attr("fix_seed")
|
||||
and src_op.attr("fix_seed")
|
||||
and src_op.has_attr("seed")
|
||||
and src_op.attr("seed")
|
||||
):
|
||||
_logger.info(
|
||||
f"Auto Parallel Random Control Skipped Since manual seed is set by user: {src_op}"
|
||||
)
|
||||
elif rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
pass
|
||||
# NOTE Adopt for recompute
|
||||
# If user already set seed, We should not modify it. But if the seed is added by recompute pass, it should be under control.
|
||||
# TODO in future recompute pass should happen after parallel partition. and remove this at that time.
|
||||
elif len(kwargs['Seed']) > 0 or len(src_op.input("Seed")) > 0:
|
||||
seed_var_name = kwargs['Seed'][0]
|
||||
if seed_var_name.startswith('rc_seed'):
|
||||
pre_op = main_block.ops[-1]
|
||||
assert (
|
||||
pre_op.type == "seed"
|
||||
and len(pre_op.attr("rng_name")) == 0
|
||||
), f"found exception op {pre_op}"
|
||||
|
||||
# determinate rng
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
X_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
X_var.name
|
||||
)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
rng_name = determinate_rng(
|
||||
rank_id, X_dims_mapping, process_mesh
|
||||
)
|
||||
# make recompute seed under control
|
||||
pre_op._set_attr("rng_name", rng_name)
|
||||
pre_op._set_attr("deterministic", True)
|
||||
pre_op._set_attr("force_cpu", True)
|
||||
else:
|
||||
_logger.info(
|
||||
f"Auto Parallel Random Control Skipped Since manual seed is set by user: {src_op}"
|
||||
)
|
||||
else:
|
||||
# determinate rng
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
X_dims_mapping = op_dist_attr.get_input_dims_mapping(X_var.name)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
|
||||
rng_name = determinate_rng(
|
||||
rank_id, X_dims_mapping, process_mesh
|
||||
)
|
||||
assert rng_name is not None and rng_name != ""
|
||||
|
||||
# insert seed op
|
||||
seed_var = main_block.create_var(
|
||||
name=unique_name.generate_with_ignorable_key(
|
||||
".".join(["tensor_parallel_seed", 'tmp'])
|
||||
),
|
||||
dtype=paddle.int32,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=False,
|
||||
)
|
||||
|
||||
# set new seed_var's dist_attr
|
||||
seed_var_dims_mapping = [-1]
|
||||
seed_var_dist_attr = set_var_dist_attr(
|
||||
ctx,
|
||||
seed_var,
|
||||
seed_var_dims_mapping,
|
||||
process_mesh,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# adopt for recompute
|
||||
# force_cpu to reduce sync copy from CPU->GPU->CPU, and reduce pipeline hang
|
||||
seed_op = main_block.append_op(
|
||||
type='seed',
|
||||
outputs={'Out': seed_var},
|
||||
attrs={
|
||||
'deterministic': True,
|
||||
'rng_name': rng_name,
|
||||
'force_cpu': True,
|
||||
},
|
||||
)
|
||||
seed_op._set_attr('op_namescope', 'auto_tensor_parallel_seed')
|
||||
# set new seed op's dist_attr
|
||||
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
|
||||
seed_op,
|
||||
process_mesh,
|
||||
seed_var_dims_mapping,
|
||||
ctx,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# modify dropout op
|
||||
src_op.desc.set_input("Seed", [seed_var.name])
|
||||
src_op.desc._set_attr("fix_seed", False)
|
||||
src_op.desc._set_attr("seed", 0)
|
||||
op_dist_attr.set_input_dist_attr(
|
||||
seed_var.name, seed_var_dist_attr
|
||||
)
|
||||
kwargs['Seed'] = [seed_var.name]
|
||||
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# dropout backward is deterministic by mask, and not need for random state control
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"dropout", DistributedDropoutImpl0("random_control")
|
||||
)
|
||||
@@ -0,0 +1,400 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
_g_op_cost_factory,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import (
|
||||
compute_compatible_dim_mapping,
|
||||
compute_compatible_dims_mapping,
|
||||
get_dist_tensor_spec,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
is_elementwise_op,
|
||||
is_parameter_related,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedElementwise(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert len(op_desc.input_arg_names()) >= 1, (
|
||||
f"elementwise op [{op_desc.type}] has [{len(op_desc.input_arg_names())}] inputs"
|
||||
)
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
assert len(op_desc.output_arg_names()) == 1, (
|
||||
f"elementwise op [{dist_op.serial_op}] has [{len(op_desc.output_arg_names())}] outputs"
|
||||
)
|
||||
output_arg_name = op_desc.output_arg_names()[0]
|
||||
num_inputs = len(input_arg_names)
|
||||
|
||||
# TODO (zhangyichen) replace dist tensor specs by dist tensor in future.
|
||||
input_specs = []
|
||||
for i in range(num_inputs):
|
||||
input_specs.append(
|
||||
get_dist_tensor_spec(dist_op, input_arg_names[i])
|
||||
)
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
# TODO revise me
|
||||
op_type = op_desc.type()
|
||||
rule = get_phi_spmd_rule(op_type)
|
||||
fw_results = rule.infer_forward(*input_specs)
|
||||
bw_results = rule.infer_backward(*input_specs, output_spec)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, input_arg_names, [output_arg_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
# NOTE this function will be remove once we use local reshard to replace distopimpls
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all elementwise op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedElementwise("elementwise")
|
||||
)
|
||||
|
||||
|
||||
# Replicated Elementwise
|
||||
class DistributedElementwiseImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
"""Calculate the cost by the op role."""
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res_cost = [cost_mapping]
|
||||
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
backward_op = dist_op.serial_op
|
||||
op_type = backward_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
need_gradient_allreduce = True
|
||||
break
|
||||
|
||||
if need_gradient_allreduce:
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
varname
|
||||
)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = var_dim_mapping[0]
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
if not is_elementwise_op(op_desc.type()):
|
||||
return False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
dims_mapping_list = []
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
max_dims_mapping_len = -1
|
||||
for arg_name in input_arg_names:
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if max_dims_mapping_len < len(dims_mapping):
|
||||
max_dims_mapping_len = len(dims_mapping)
|
||||
dims_mapping_list.append(dims_mapping)
|
||||
|
||||
for idx in range(max_dims_mapping_len):
|
||||
dim_mappings = []
|
||||
for dims_mapping in dims_mapping_list:
|
||||
if idx < len(dims_mapping):
|
||||
dim_mappings.append(dims_mapping[-(idx + 1)])
|
||||
if compute_compatible_dim_mapping(dim_mappings) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
if not is_elementwise_op(op_desc.type()):
|
||||
return False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
dims_mapping_list = []
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
max_dims_mapping_len = -1
|
||||
for arg_name in output_arg_names:
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if max_dims_mapping_len < len(dims_mapping):
|
||||
max_dims_mapping_len = len(dims_mapping)
|
||||
dims_mapping_list.append(dims_mapping)
|
||||
|
||||
for idx in range(max_dims_mapping_len):
|
||||
dim_mappings = []
|
||||
for dims_mapping in dims_mapping_list:
|
||||
if idx < len(dims_mapping):
|
||||
dim_mappings.append(dims_mapping[-(idx + 1)])
|
||||
if compute_compatible_dim_mapping(dim_mappings) is None:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
if not is_elementwise_op(op_desc.type()):
|
||||
return False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
dims_mapping_list = []
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
input_max_dims_mapping_len = -1
|
||||
for arg_name in input_arg_names:
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if input_max_dims_mapping_len < len(dims_mapping):
|
||||
input_max_dims_mapping_len = len(dims_mapping)
|
||||
dims_mapping_list.append(dims_mapping)
|
||||
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
output_max_dims_mapping_len = -1
|
||||
for arg_name in output_arg_names:
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if output_max_dims_mapping_len < len(dims_mapping):
|
||||
output_max_dims_mapping_len = len(dims_mapping)
|
||||
dims_mapping_list.append(dims_mapping)
|
||||
|
||||
assert input_max_dims_mapping_len == output_max_dims_mapping_len
|
||||
max_dims_mapping_len = input_max_dims_mapping_len
|
||||
|
||||
for idx in range(max_dims_mapping_len):
|
||||
dim_mappings = []
|
||||
for dims_mapping in dims_mapping_list:
|
||||
if idx < len(dims_mapping):
|
||||
dim_mappings.append(dims_mapping[-(idx + 1)])
|
||||
if not all(
|
||||
dim_mappings[0] == dim_mapping for dim_mapping in dim_mappings
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
dims_mapping_list = []
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
input_dims_mapping_dict = {}
|
||||
input_dims_mapping_lens = {}
|
||||
input_max_dims_mapping_len = -1
|
||||
for arg_name in input_arg_names:
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if input_max_dims_mapping_len < len(dims_mapping):
|
||||
input_max_dims_mapping_len = len(dims_mapping)
|
||||
input_dims_mapping_dict[arg_name] = dims_mapping
|
||||
input_dims_mapping_lens[arg_name] = len(dims_mapping)
|
||||
for arg_name in input_arg_names:
|
||||
if input_dims_mapping_lens[arg_name] < input_max_dims_mapping_len:
|
||||
new_dims_mapping = [
|
||||
-1 for _ in range(input_max_dims_mapping_len)
|
||||
]
|
||||
for i in range(input_dims_mapping_lens[arg_name]):
|
||||
new_idx = (
|
||||
input_max_dims_mapping_len
|
||||
- input_dims_mapping_lens[arg_name]
|
||||
) + i
|
||||
new_dims_mapping[new_idx] = input_dims_mapping_dict[
|
||||
arg_name
|
||||
][i]
|
||||
dims_mapping_list.append(new_dims_mapping)
|
||||
else:
|
||||
dims_mapping_list.append(input_dims_mapping_dict[arg_name])
|
||||
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
output_dims_mapping_dict = {}
|
||||
output_dims_mapping_lens = {}
|
||||
output_max_dims_mapping_len = -1
|
||||
for arg_name in output_arg_names:
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if output_max_dims_mapping_len < len(dims_mapping):
|
||||
output_max_dims_mapping_len = len(dims_mapping)
|
||||
output_dims_mapping_dict[arg_name] = dims_mapping
|
||||
output_dims_mapping_lens[arg_name] = len(dims_mapping)
|
||||
for arg_name in output_arg_names:
|
||||
if output_dims_mapping_lens[arg_name] < output_max_dims_mapping_len:
|
||||
new_dims_mapping = [
|
||||
-1 for _ in range(output_max_dims_mapping_len)
|
||||
]
|
||||
for i in range(output_dims_mapping_lens[arg_name]):
|
||||
new_idx = (
|
||||
output_max_dims_mapping_len
|
||||
- output_dims_mapping_lens[arg_name]
|
||||
) + i
|
||||
new_dims_mapping[new_idx] = output_dims_mapping_dict[
|
||||
arg_name
|
||||
][i]
|
||||
dims_mapping_list.append(new_dims_mapping)
|
||||
else:
|
||||
dims_mapping_list.append(output_dims_mapping_dict[arg_name])
|
||||
|
||||
assert input_max_dims_mapping_len == output_max_dims_mapping_len
|
||||
max_dims_mapping_len = input_max_dims_mapping_len
|
||||
compatible_dims_mapping = compute_compatible_dims_mapping(
|
||||
dims_mapping_list
|
||||
)
|
||||
if compatible_dims_mapping is None:
|
||||
return False
|
||||
|
||||
for arg_name in input_arg_names:
|
||||
if input_dims_mapping_lens[arg_name] < max_dims_mapping_len:
|
||||
new_dims_mapping = [
|
||||
-1 for _ in range(input_dims_mapping_lens[arg_name])
|
||||
]
|
||||
for i in range(input_dims_mapping_lens[arg_name]):
|
||||
new_idx = (
|
||||
max_dims_mapping_len - input_dims_mapping_lens[arg_name]
|
||||
) + i
|
||||
new_dims_mapping[i] = compatible_dims_mapping[new_idx]
|
||||
if new_dims_mapping != input_dims_mapping_dict[arg_name]:
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
arg_name, new_dims_mapping
|
||||
)
|
||||
changed = True
|
||||
else:
|
||||
if compatible_dims_mapping != input_dims_mapping_dict[arg_name]:
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
arg_name, compatible_dims_mapping
|
||||
)
|
||||
changed = True
|
||||
|
||||
for arg_name in output_arg_names:
|
||||
if output_dims_mapping_lens[arg_name] < max_dims_mapping_len:
|
||||
new_dims_mapping = [
|
||||
-1 for _ in range(output_dims_mapping_lens[arg_name])
|
||||
]
|
||||
for i in range(output_dims_mapping_lens[arg_name]):
|
||||
new_idx = (
|
||||
max_dims_mapping_len
|
||||
- output_dims_mapping_lens[arg_name]
|
||||
) + i
|
||||
new_dims_mapping[i] = compatible_dims_mapping[new_idx]
|
||||
if new_dims_mapping != output_dims_mapping_dict[arg_name]:
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
arg_name, new_dims_mapping
|
||||
)
|
||||
changed = True
|
||||
else:
|
||||
if (
|
||||
compatible_dims_mapping
|
||||
!= output_dims_mapping_dict[arg_name]
|
||||
):
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
arg_name, compatible_dims_mapping
|
||||
)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"elementwise", DistributedElementwiseImpl0("replicate_parallel")
|
||||
)
|
||||
@@ -0,0 +1,671 @@
|
||||
# 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 paddle
|
||||
from paddle.common_ops_import import check_variable_and_dtype
|
||||
from paddle.distributed.auto_parallel.static.cost.comm_op_cost import (
|
||||
AllReduceOpCost,
|
||||
IdentityOpCost,
|
||||
)
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
from paddle.framework import core
|
||||
from paddle.utils import unique_name
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
EmbeddingGradOpCost,
|
||||
EmbeddingOpCost,
|
||||
build_comm_costs_from_descs,
|
||||
build_comm_desc_from_dist_op,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
_get_idx_in_axis,
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
is_dim_replicate,
|
||||
is_dim_shard,
|
||||
set_var_dist_attr,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
ParallelMode,
|
||||
get_default_distributed_operator_impl,
|
||||
gradient_synchronization,
|
||||
naive_copy_op_dist_attr_for_program,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
set_comm_op_dist_attr_for_program,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedEmbedding(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert dist_op.serial_op.type == "lookup_table_v2", (
|
||||
f"{dist_op.serial_op.type} is not supported by dist embedding yet."
|
||||
)
|
||||
|
||||
x_name = op_desc.input('Ids')[0]
|
||||
w_name = op_desc.input('W')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
padding_idx = op_desc.attr('padding_idx')
|
||||
is_sparse = op_desc.attr('is_sparse')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
w_spec = get_dist_tensor_spec(dist_op, w_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("embedding")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, w_spec, padding_idx, is_sparse)
|
||||
bw_results = rule.infer_backward(
|
||||
x_spec, w_spec, output_spec, padding_idx, is_sparse
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name, w_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
reverted = False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
op_desc = dist_op.serial_op.desc
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dist_attr = op_dist_attr.get_output_dist_attr(out_name)
|
||||
|
||||
# vocab parallel embedding
|
||||
if out_dist_attr._is_partial():
|
||||
op_dist_attr.impl_type = op_desc.type()
|
||||
op_dist_attr.impl_idx = 0
|
||||
# data parallel or col parallel of weight
|
||||
else:
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return reverted
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedEmbedding("lookup_table_v2")
|
||||
)
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedEmbedding("c_embedding")
|
||||
)
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedEmbedding("lookup_table")
|
||||
)
|
||||
|
||||
|
||||
def adopt_lookup_table_v1(ctx, main_block, src_op, Ids_var):
|
||||
assert len(Ids_var.shape) == 3, (
|
||||
f"input Ids to lookup_table should have 3 dimensions but got [{Ids_var.name}] with shape [{Ids_var.shape}]"
|
||||
)
|
||||
if not Ids_var.stop_gradient:
|
||||
raise NotImplementedError(
|
||||
'Requiring the gradient of Ids of lookup_table(v1) dist op is not currently supported. Please open an issue with details on your use case so that we can prioritize adding this (for instance, adversarial training for language model).'
|
||||
)
|
||||
|
||||
target_shape = list(Ids_var.shape[:-1])
|
||||
intermediate_var_0 = main_block.create_var(
|
||||
name=unique_name.generate_with_ignorable_key(
|
||||
".".join(["dist_reshape", 'tmp'])
|
||||
),
|
||||
dtype=Ids_var.dtype,
|
||||
shape=target_shape,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=True,
|
||||
)
|
||||
|
||||
target_shape = [0, *list(Ids_var.shape[:-1])]
|
||||
xshape_var = main_block.create_var(
|
||||
name=unique_name.generate_with_ignorable_key(
|
||||
".".join(["dist_Xshape", 'tmp'])
|
||||
),
|
||||
dtype=Ids_var.dtype,
|
||||
shape=target_shape,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=True,
|
||||
)
|
||||
|
||||
# TODO use inplace reshape for memory saving
|
||||
reshape_op = main_block.append_op(
|
||||
type='reshape2',
|
||||
inputs={'X': [Ids_var]},
|
||||
outputs={'Out': [intermediate_var_0], 'XShape': [xshape_var]},
|
||||
attrs={
|
||||
"shape": [0, -1],
|
||||
},
|
||||
)
|
||||
|
||||
# set dist attr
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
Ids_var_dist_attr = op_dist_attr.get_input_dist_attr(Ids_var.name)
|
||||
assert Ids_var_dist_attr is not None
|
||||
intermediate_var_0_dist_attr = set_var_dist_attr(
|
||||
ctx,
|
||||
intermediate_var_0,
|
||||
Ids_var_dist_attr.dims_mapping,
|
||||
Ids_var_dist_attr.process_mesh,
|
||||
chunk_id=Ids_var_dist_attr.chunk_id,
|
||||
)
|
||||
set_var_dist_attr(
|
||||
ctx,
|
||||
xshape_var,
|
||||
[-1, *list(Ids_var_dist_attr.dims_mapping)],
|
||||
Ids_var_dist_attr.process_mesh,
|
||||
chunk_id=Ids_var_dist_attr.chunk_id,
|
||||
)
|
||||
# rename src_op's input
|
||||
src_op._rename_input(Ids_var.name, intermediate_var_0.name)
|
||||
op_dist_attr.del_input_dist_attr(Ids_var.name)
|
||||
op_dist_attr.set_input_dist_attr(
|
||||
intermediate_var_0.name, intermediate_var_0_dist_attr
|
||||
)
|
||||
|
||||
new_op_dist_attr = OperatorDistAttr()
|
||||
new_op_dist_attr.process_mesh = Ids_var_dist_attr.process_mesh
|
||||
new_op_dist_attr.impl_type = "default"
|
||||
new_op_dist_attr.impl_idx = 0
|
||||
new_op_dist_attr.chunk_id = Ids_var_dist_attr.chunk_id
|
||||
new_op_dist_attr.set_input_dims_mapping(
|
||||
Ids_var.name, Ids_var_dist_attr.dims_mapping
|
||||
)
|
||||
new_op_dist_attr.set_output_dims_mapping(
|
||||
intermediate_var_0.name, Ids_var_dist_attr.dims_mapping
|
||||
)
|
||||
new_op_dist_attr.set_output_dims_mapping(
|
||||
xshape_var.name, [-1, *list(Ids_var_dist_attr.dims_mapping)]
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(reshape_op, new_op_dist_attr)
|
||||
|
||||
return intermediate_var_0
|
||||
|
||||
|
||||
# RowParallel
|
||||
class DistributedEmbeddingImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
"""Calculate the cost by the op role."""
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Forward):
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
elif int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
# embedding need start_index
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
EmbeddingOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
|
||||
serial_op = dist_op.serial_op
|
||||
parallel_axis = dist_op.dist_attr.get_input_dims_mapping(
|
||||
serial_op.input("W")[0]
|
||||
)[0]
|
||||
attrs = {"use_calc_stream": True, "use_model_parallel": True}
|
||||
var_names = serial_op.output("Out")
|
||||
all_reduce_sum_desc_mapping = build_comm_desc_from_dist_op(
|
||||
"all_reduce",
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs=attrs,
|
||||
parallel_axis=parallel_axis,
|
||||
)
|
||||
|
||||
comm_op_cost_list = build_comm_costs_from_descs(
|
||||
AllReduceOpCost,
|
||||
ctx,
|
||||
processes,
|
||||
all_reduce_sum_desc_mapping,
|
||||
cluster,
|
||||
)
|
||||
|
||||
res_cost = [cost_mapping, comm_op_cost_list]
|
||||
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# by now the backward function only insert the gradient allreduce for dist op itself
|
||||
res = []
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
dist_attr = dist_op.dist_attr
|
||||
|
||||
embedding_row_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
backward_op.input("W")[0]
|
||||
)[0]
|
||||
parallel_axis = embedding_row_dim_mapping
|
||||
attrs = {"use_calc_stream": True, "use_model_parallel": True}
|
||||
var_names = [backward_op.input("Out@GRAD")[0]]
|
||||
c_identity_desc_mapping = build_comm_desc_from_dist_op(
|
||||
"c_identity",
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs=attrs,
|
||||
parallel_axis=parallel_axis,
|
||||
)
|
||||
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
comm_op_cost_list = build_comm_costs_from_descs(
|
||||
IdentityOpCost, ctx, processes, c_identity_desc_mapping, cluster
|
||||
)
|
||||
res.append(comm_op_cost_list)
|
||||
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
EmbeddingGradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
# need gradient allreduce
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
backward_op.input("Ids")[0]
|
||||
)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [backward_op.output('W@GRAD')[0]]
|
||||
build_dp_costs(
|
||||
res, dist_op, ctx, var_names, attrs, parallel_axis, cluster
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
ids_name = op_desc.input('Ids')[0]
|
||||
w_name = op_desc.input('W')[0]
|
||||
ids_dims_mapping = op_dist_attr.get_input_dims_mapping(ids_name)
|
||||
w_dims_mapping = op_dist_attr.get_input_dims_mapping(w_name)
|
||||
if is_dim_replicate(w_dims_mapping[-2]) or is_dim_shard(
|
||||
w_dims_mapping[-1]
|
||||
):
|
||||
return False
|
||||
# Other dimensions must be replicate except the batch dimension
|
||||
for mapping in ids_dims_mapping[1:]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
|
||||
if is_dim_shard(ids_dims_mapping[0]) and is_dim_shard(
|
||||
w_dims_mapping[-2]
|
||||
):
|
||||
if ids_dims_mapping[0] == w_dims_mapping[-2]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
# Other dimensions must be replicate except the batch dimension
|
||||
for mapping in out_dims_mapping[1:]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
ids_name = op_desc.input('Ids')[0]
|
||||
w_name = op_desc.input('W')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
ids_dims_mapping = op_dist_attr.get_input_dims_mapping(ids_name)
|
||||
w_dims_mapping = op_dist_attr.get_input_dims_mapping(w_name)
|
||||
|
||||
if ids_dims_mapping != out_dims_mapping[: len(ids_dims_mapping)]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
ids_name = op_desc.input('Ids')[0]
|
||||
w_name = op_desc.input('W')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
ids_dims_mapping = op_dist_attr.get_input_dims_mapping(ids_name)
|
||||
w_dims_mapping = op_dist_attr.get_input_dims_mapping(w_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for i in range(len(ids_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[ids_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[w_dims_mapping, out_dims_mapping], [-1, -1]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(ids_name, ids_dims_mapping)
|
||||
op_dist_attr.set_input_dims_mapping(w_name, w_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
assert 'Ids' in kwargs, "input [{}] is not given".format('Ids')
|
||||
assert 'W' in kwargs, "input [{}] is not given".format('W')
|
||||
assert 'Out' in kwargs, "output [{}] is not given".format('Out')
|
||||
|
||||
assert len(kwargs['Ids']) == 1, (
|
||||
"row_parallel_embedding input Ids take 1 variable but got {}".format(
|
||||
kwargs['Ids']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['W']) == 1, (
|
||||
"row_parallel_embedding input W take 1 variable but got {}".format(
|
||||
kwargs['W']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Out']) == 1, (
|
||||
"row_parallel_embedding output Out take 1 variable but got {}".format(
|
||||
kwargs['Out']
|
||||
)
|
||||
)
|
||||
|
||||
Ids_var = main_block._var_recursive(kwargs['Ids'][0])
|
||||
Weight_var = main_block._var_recursive(kwargs['W'][0])
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
|
||||
# support lookup_table_v1
|
||||
if src_op.type == 'lookup_table':
|
||||
Ids_var = adopt_lookup_table_v1(ctx, main_block, src_op, Ids_var)
|
||||
|
||||
# got dist attribute info
|
||||
embedding_row_dim_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
Weight_var.name
|
||||
)[0]
|
||||
assert embedding_row_dim_mapping >= 0, (
|
||||
f"row_parallel_embedding's row should be divided by a specific mesh axis, but got [{embedding_row_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh_group:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# A generalized method to calculate embedding offset using cartesian product
|
||||
relative_idx = _get_idx_in_axis(
|
||||
process_mesh_group,
|
||||
process_mesh_shape,
|
||||
embedding_row_dim_mapping,
|
||||
rank_id,
|
||||
)
|
||||
|
||||
per_part_size = Weight_var.shape[0]
|
||||
relative_idx = relative_idx * per_part_size
|
||||
|
||||
# TODO calculate ring id
|
||||
parallel_axis = embedding_row_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# append op
|
||||
check_variable_and_dtype(
|
||||
Ids_var, 'input', ['int32', 'int64'], 'c_embedding'
|
||||
)
|
||||
|
||||
# infer new var shape with op dist attr
|
||||
out_tensor_dist_attr = ctx.get_tensor_dist_attr_for_program(Out_var)
|
||||
assert out_tensor_dist_attr is not None
|
||||
out_var_dist_attr = op_dist_attr.get_output_dist_attr(Out_var.name)
|
||||
assert out_var_dist_attr is not None
|
||||
|
||||
c_embedding_op_desc = main_block.append_op(type='nop').desc
|
||||
c_embedding_op_desc.set_type("c_embedding")
|
||||
c_embedding_op_desc.set_input('Ids', [Ids_var.name])
|
||||
c_embedding_op_desc.set_input('W', [Weight_var.name])
|
||||
c_embedding_op_desc.set_output('Out', [Out_var.name])
|
||||
c_embedding_op_desc._set_attr('start_index', relative_idx)
|
||||
c_embedding_op_desc._set_attr(OP_ROLE_KEY, src_op.attr('op_role'))
|
||||
c_embedding_op = main_block.ops[-1]
|
||||
assert c_embedding_op.type == "c_embedding"
|
||||
naive_copy_op_dist_attr_for_program(c_embedding_op, src_op, ctx)
|
||||
|
||||
# use_model_parallel
|
||||
all_reduce_sum_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': [Out_var]},
|
||||
outputs={'out': [Out_var]},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'reduce_type': paddle.distributed.ReduceOp.SUM,
|
||||
'use_model_parallel': True,
|
||||
OP_ROLE_KEY: src_op.attr('op_role'),
|
||||
},
|
||||
)
|
||||
all_reduce_sum_op._set_attr(
|
||||
'op_namescope', '/' + ParallelMode.TensorParallel
|
||||
)
|
||||
# allreduce
|
||||
set_comm_op_dist_attr_for_program(
|
||||
all_reduce_sum_op,
|
||||
op_dist_attr.process_mesh,
|
||||
out_var_dist_attr,
|
||||
ctx,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# param initialization sync
|
||||
if Weight_var.is_parameter and not op_dist_attr.is_recompute:
|
||||
if Weight_var.name in dist_op_context.already_init_sync_vars:
|
||||
return
|
||||
dist_op_context.already_init_sync_vars.add(Weight_var.name)
|
||||
param = startup_block.var(Weight_var.name)
|
||||
param_dist_attr = ctx.get_tensor_dist_attr_for_program(param)
|
||||
process_mesh = param_dist_attr.process_mesh
|
||||
dim_mapping = param_dist_attr.dims_mapping
|
||||
|
||||
# NOTE all not split axis should be presented in mesh
|
||||
for axis, size in enumerate(process_mesh.shape):
|
||||
if size <= 1 or axis in dim_mapping:
|
||||
pass
|
||||
else:
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh.process_ids,
|
||||
process_mesh.shape,
|
||||
axis,
|
||||
rank_id,
|
||||
)
|
||||
sync_group = new_process_group(group_ranks)
|
||||
|
||||
broadcast_op = startup_block.append_op(
|
||||
type='broadcast',
|
||||
inputs={'x': param},
|
||||
outputs={'out': param},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'root': 0,
|
||||
OP_ROLE_KEY: OpRole.Forward,
|
||||
},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# by now the backward function only insert the gradient allreduce for dist op itself
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
assert 'Ids' in kwargs, "input [{}] is not given".format('Ids')
|
||||
assert 'W' in kwargs, "input [{}] is not given".format('W')
|
||||
assert 'Out@GRAD' in kwargs, "input [{}] is not given".format('Out')
|
||||
assert 'W@GRAD' in kwargs, "output [{}] is not given".format('W@GRAD')
|
||||
|
||||
assert len(kwargs['Ids']) == 1, (
|
||||
"row_parallel_embedding input Ids take 1 variable but got {}".format(
|
||||
kwargs['Ids']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['W']) == 1, (
|
||||
"row_parallel_embedding input Ids take 1 variable but got {}".format(
|
||||
kwargs['W']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['Out@GRAD']) == 1, (
|
||||
"row_parallel_embedding input Ids take 1 variable but got {}".format(
|
||||
kwargs['Out']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['W@GRAD']) == 1, (
|
||||
"row_parallel_embedding output Ids take 1 variable but got {}".format(
|
||||
kwargs['W@GRAD']
|
||||
)
|
||||
)
|
||||
|
||||
Ids_var = main_block._var_recursive(kwargs['Ids'][0])
|
||||
Weight_var = main_block._var_recursive(kwargs['W'][0])
|
||||
Out_grad = main_block._var_recursive(kwargs['Out@GRAD'][0])
|
||||
Weight_grad = main_block._var_recursive(kwargs['W@GRAD'][0])
|
||||
|
||||
embedding_row_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
Weight_var.name
|
||||
)[0]
|
||||
assert embedding_row_dim_mapping >= 0, (
|
||||
f"row_parallel_embedding's row should be divided by a specific mesh axis, but got [{embedding_row_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = dist_attr.process_mesh.shape
|
||||
process_mesh_group = dist_attr.process_mesh.process_ids
|
||||
|
||||
# A generalized method to calculate embedding offset using cartesian product
|
||||
relative_idx = _get_idx_in_axis(
|
||||
process_mesh_group,
|
||||
process_mesh_shape,
|
||||
embedding_row_dim_mapping,
|
||||
rank_id,
|
||||
)
|
||||
per_part_size = Weight_var.shape[0]
|
||||
relative_idx = relative_idx * per_part_size
|
||||
|
||||
c_embedding_grad_op_desc = main_block.append_op(type='nop').desc
|
||||
c_embedding_grad_op_desc.set_type("c_embedding_grad")
|
||||
c_embedding_grad_op_desc.set_input('Ids', [Ids_var.name])
|
||||
c_embedding_grad_op_desc.set_input('W', [Weight_var.name])
|
||||
c_embedding_grad_op_desc.set_input('Out@GRAD', [Out_grad.name])
|
||||
c_embedding_grad_op_desc.set_output('W@GRAD', [Weight_grad.name])
|
||||
c_embedding_grad_op_desc._set_attr('start_index', relative_idx)
|
||||
c_embedding_grad_op_desc._set_attr(OP_ROLE_KEY, OpRole.Backward)
|
||||
|
||||
c_embedding_grad_op = main_block.ops[-1]
|
||||
assert c_embedding_grad_op.type == "c_embedding_grad"
|
||||
naive_copy_op_dist_attr_for_program(
|
||||
c_embedding_grad_op, backward_op, ctx
|
||||
)
|
||||
|
||||
# data parallel gradient synchronization
|
||||
act_grad_names = [Ids_var.name]
|
||||
out_grad_names = [kwargs['W@GRAD'][0]]
|
||||
|
||||
gradient_synchronization(
|
||||
ctx, backward_op, act_grad_names, out_grad_names, rank_id
|
||||
)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"lookup_table_v2", DistributedEmbeddingImpl("row_parallel")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"c_embedding", DistributedEmbeddingImpl("row_parallel")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"lookup_table", DistributedEmbeddingImpl("row_parallel")
|
||||
)
|
||||
@@ -0,0 +1,80 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedExpandAs(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
target_shape = op_desc.attr('target_shape')
|
||||
|
||||
input_specs = []
|
||||
for name in input_arg_names:
|
||||
input_specs.append(get_dist_tensor_spec(dist_op, name))
|
||||
|
||||
assert len(input_specs) == 2
|
||||
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_names[0], False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("expand_as")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(
|
||||
input_specs[0], input_specs[1], target_shape
|
||||
)
|
||||
bw_results = rule.infer_backward(
|
||||
input_specs[0], input_specs[1], output_spec, target_shape
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names,
|
||||
output_arg_names,
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedExpandAs("expand_as_v2")
|
||||
)
|
||||
+144
@@ -0,0 +1,144 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..cost import (
|
||||
FillConstantBatchSizeLikeOpCost,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
)
|
||||
from ..utils import compute_compatible_and_update_dim_mapping
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedFillConstantBatchSizeLike(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedFillConstantBatchSizeLike("fill_constant_batch_size_like")
|
||||
)
|
||||
|
||||
|
||||
class DistributedFillConstantBatchSizeLikeImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
raise ValueError(
|
||||
"The fill_constant_batch_size_like has no grad op."
|
||||
)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
FillConstantBatchSizeLikeOpCost,
|
||||
ctx,
|
||||
processes,
|
||||
desc_mapping,
|
||||
cluster,
|
||||
)
|
||||
|
||||
res_cost = [cost_mapping]
|
||||
return res_cost
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
shape_list = op_desc.attr("shape")
|
||||
|
||||
if len(shape_list) != len(out_dims_mapping):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
in_name = op_desc.input('Input')[0]
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
|
||||
# the dim_mapping of batch dimension should be the same
|
||||
return out_dims_mapping[0] == in_dims_mapping[0]
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
# only the batch size dimension of input and output are relative.
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [0, 0]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"fill_constant_batch_size_like",
|
||||
DistributedFillConstantBatchSizeLikeImpl0("fill_by_shape"),
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ...random import determinate_rng, is_enable_auto_rand_ctrl
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_eltwise import DistributedElementwiseImpl0
|
||||
|
||||
|
||||
class DistributedFlashAttn(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedFlashAttn("flash_attn"))
|
||||
|
||||
|
||||
# Dist FlashAttn with Random Control
|
||||
# NOTE(zhiqiu): trick implementation, copy dist_attr of q,k,v to out
|
||||
class DistributedFlashAttnImpl0(DistributedElementwiseImpl0):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if (
|
||||
is_enable_auto_rand_ctrl()
|
||||
and not op_dist_attr.is_recompute
|
||||
and rank_id in op_dist_attr.process_mesh.process_ids
|
||||
):
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
if (
|
||||
len(kwargs.get('fixed_seed_offset', [])) > 0
|
||||
or len(src_op.input("fixed_seed_offset")) > 0
|
||||
):
|
||||
# TODO(kuizhiqing) recompute should go here
|
||||
pass
|
||||
else:
|
||||
# determinate rng
|
||||
q_var = main_block._var_recursive(kwargs['q'][0])
|
||||
k_var = main_block._var_recursive(kwargs['k'][0])
|
||||
q_dims_mapping = op_dist_attr.get_input_dims_mapping(q_var.name)
|
||||
k_dims_mapping = op_dist_attr.get_input_dims_mapping(k_var.name)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
dims_mapping = [*q_dims_mapping[:3], q_dims_mapping[2]]
|
||||
|
||||
rng_name = determinate_rng(rank_id, dims_mapping, process_mesh)
|
||||
assert rng_name is not None and rng_name != ""
|
||||
|
||||
src_op._set_attr('rng_name', rng_name)
|
||||
|
||||
DistributedElementwiseImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# dropout backward is deterministic by mask, and not need for random state control
|
||||
DistributedElementwiseImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"flash_attn", DistributedFlashAttnImpl0("random_control")
|
||||
)
|
||||
@@ -0,0 +1,235 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
is_dim_replicate,
|
||||
is_dim_shard,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedFusedAttention(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedFusedAttention("fused_attention")
|
||||
)
|
||||
|
||||
|
||||
class DistributedFusedAttentionImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
qkv_w = op_desc.input('QKVW')[0]
|
||||
qkv_bias = op_desc.input('QKVBias')[0]
|
||||
out_w = op_desc.input('OutLinearW')[0]
|
||||
out_bias = op_desc.input('OutLinearBias')[0]
|
||||
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
qkv_w_dims_mapping = op_dist_attr.get_input_dims_mapping(qkv_w)
|
||||
qkv_bias_dims_mapping = op_dist_attr.get_input_dims_mapping(qkv_bias)
|
||||
out_w_dims_mapping = op_dist_attr.get_input_dims_mapping(out_w)
|
||||
out_bias_dims_mapping = op_dist_attr.get_input_dims_mapping(out_bias)
|
||||
|
||||
head_axis = 1
|
||||
for mapping in x_dims_mapping[1:-1]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
if len(qkv_w_dims_mapping) != 4 or is_dim_replicate(
|
||||
qkv_w_dims_mapping[head_axis]
|
||||
):
|
||||
return False
|
||||
if len(qkv_bias_dims_mapping) != 3 or is_dim_replicate(
|
||||
qkv_bias_dims_mapping[head_axis]
|
||||
):
|
||||
return False
|
||||
if is_dim_replicate(out_w_dims_mapping[0]):
|
||||
return False
|
||||
if is_dim_shard(out_bias_dims_mapping[-1]):
|
||||
return False
|
||||
|
||||
replicated_dims = [
|
||||
qkv_w_dims_mapping[0],
|
||||
qkv_w_dims_mapping[-2],
|
||||
qkv_w_dims_mapping[-1],
|
||||
qkv_bias_dims_mapping[0],
|
||||
qkv_bias_dims_mapping[-1],
|
||||
out_w_dims_mapping[-1],
|
||||
out_bias_dims_mapping[-1],
|
||||
]
|
||||
for mapping in replicated_dims:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
if qkv_bias_dims_mapping[head_axis] != qkv_w_dims_mapping[head_axis]:
|
||||
return False
|
||||
if qkv_bias_dims_mapping[head_axis] != out_w_dims_mapping[0]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
# none of output should be sharded
|
||||
for out_name in op_desc.output_names():
|
||||
out = op_desc.output(out_name)[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out)
|
||||
for mapping in out_dims_mapping[1:-1]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Y')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Y')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
out_name, out_dims_mapping
|
||||
)
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# infer logic comm presentation
|
||||
head_axis = 1
|
||||
qkv_w = src_op.input('QKVW')[0]
|
||||
qkv_w_col_dim_mapping = op_dist_attr.get_input_dims_mapping(qkv_w)[
|
||||
head_axis
|
||||
]
|
||||
assert qkv_w_col_dim_mapping >= 0, (
|
||||
f"col_parallel_matmul's row should be divided by a specific mesh axis, but got [{qkv_w_col_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
parallel_axis = qkv_w_col_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# insert op
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
# setting comm id
|
||||
new_op = main_block.ops[-1]
|
||||
assert new_op.type == "fused_attention"
|
||||
new_op._set_attr("ring_id", int(group.id))
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# infer logic comm presentation
|
||||
out_w = src_op.input('OutLinearW')[0]
|
||||
out_w_col_dim_mapping = op_dist_attr.get_input_dims_mapping(out_w)[-1]
|
||||
assert out_w_col_dim_mapping >= 0, (
|
||||
f"col_parallel_matmul's row should be divided by a specific mesh axis, but got [{out_w_col_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
parallel_axis = out_w_col_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# insert op
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
# setting comm id
|
||||
new_op = main_block.ops[-1]
|
||||
assert new_op.type == "fused_attention_grad"
|
||||
new_op._set_attr("ring_id", int(group.id))
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"fused_attention", DistributedFusedAttentionImpl("tensor_parallel")
|
||||
)
|
||||
@@ -0,0 +1,195 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
import logging
|
||||
|
||||
import paddle
|
||||
from paddle.base.log_helper import get_logger
|
||||
from paddle.framework import core
|
||||
from paddle.utils import unique_name
|
||||
|
||||
from ...random import determinate_rng, is_enable_auto_rand_ctrl
|
||||
from ..utils import (
|
||||
naive_set_dist_op_attr_for_program_by_mesh_and_mapping,
|
||||
set_var_dist_attr,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_eltwise import DistributedDefaultImpl0, DistributedElementwiseImpl0
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
class DistributedDropout(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedDropout("fused_dropout_add")
|
||||
)
|
||||
|
||||
|
||||
# Dist Dropout with Random Control
|
||||
# Dropout re-use the compatible and cost function of elementwise
|
||||
class DistributedDropoutImpl0(DistributedElementwiseImpl0):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if is_enable_auto_rand_ctrl() and not op_dist_attr.is_recompute:
|
||||
assert op_dist_attr is not None, (
|
||||
f"forward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
assert 'seed_tensor' in kwargs, "input [{}] is not given".format(
|
||||
'seed_tensor'
|
||||
)
|
||||
|
||||
if (
|
||||
src_op.has_attr("fix_seed")
|
||||
and src_op.attr("fix_seed")
|
||||
and src_op.has_attr("seed")
|
||||
and src_op.attr("seed")
|
||||
):
|
||||
_logger.info(
|
||||
f"Auto Parallel Random Control Skipped Since manual seed is set by user: {src_op}"
|
||||
)
|
||||
elif rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
pass
|
||||
elif (
|
||||
len(kwargs['seed_tensor']) > 0
|
||||
or len(src_op.input("seed_tensor")) > 0
|
||||
):
|
||||
seed_var_name = kwargs['seed_tensor'][0]
|
||||
if seed_var_name.startswith('rc_seed'):
|
||||
pre_op = main_block.ops[-1]
|
||||
assert (
|
||||
pre_op.type == "seed"
|
||||
and len(pre_op.attr("rng_name")) == 0
|
||||
), f"found exception op {pre_op}"
|
||||
|
||||
# determinate rng
|
||||
X_var = main_block._var_recursive(kwargs['x'][0])
|
||||
X_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
X_var.name
|
||||
)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
rng_name = determinate_rng(
|
||||
rank_id, X_dims_mapping, process_mesh
|
||||
)
|
||||
# make recompute seed under control
|
||||
pre_op._set_attr("rng_name", rng_name)
|
||||
pre_op._set_attr("deterministic", True)
|
||||
pre_op._set_attr("force_cpu", True)
|
||||
else:
|
||||
_logger.info(
|
||||
f"Auto Parallel Random Control Skipped Since manual seed is set by user: {src_op}"
|
||||
)
|
||||
else:
|
||||
# determinate rng
|
||||
X_var = main_block._var_recursive(kwargs['x'][0])
|
||||
X_dims_mapping = op_dist_attr.get_input_dims_mapping(X_var.name)
|
||||
process_mesh = op_dist_attr.process_mesh
|
||||
|
||||
rng_name = determinate_rng(
|
||||
rank_id, X_dims_mapping, process_mesh
|
||||
)
|
||||
assert rng_name is not None and rng_name != ""
|
||||
|
||||
# insert seed op
|
||||
seed_var = main_block.create_var(
|
||||
name=unique_name.generate_with_ignorable_key(
|
||||
".".join(["tensor_parallel_seed", 'tmp'])
|
||||
),
|
||||
dtype=paddle.int32,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=False,
|
||||
)
|
||||
|
||||
# set new seed_var's dist_attr
|
||||
seed_var_dims_mapping = [-1]
|
||||
seed_var_dist_attr = set_var_dist_attr(
|
||||
ctx,
|
||||
seed_var,
|
||||
seed_var_dims_mapping,
|
||||
process_mesh,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# adopt for recompute
|
||||
# force_cpu to reduce sync copy from CPU->GPU->CPU, and reduce pipeline hang
|
||||
seed_op = main_block.append_op(
|
||||
type='seed',
|
||||
outputs={'Out': seed_var},
|
||||
attrs={
|
||||
'deterministic': True,
|
||||
'rng_name': rng_name,
|
||||
'force_cpu': True,
|
||||
},
|
||||
)
|
||||
seed_op._set_attr('op_namescope', 'auto_tensor_parallel_seed')
|
||||
# set new seed op's dist_attr
|
||||
naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
|
||||
seed_op,
|
||||
process_mesh,
|
||||
seed_var_dims_mapping,
|
||||
ctx,
|
||||
chunk_id=op_dist_attr.chunk_id,
|
||||
)
|
||||
|
||||
# modify dropout op
|
||||
src_op.desc.set_input("seed_tensor", [seed_var.name])
|
||||
src_op._remove_attr("fix_seed")
|
||||
src_op._remove_attr("seed")
|
||||
op_dist_attr.set_input_dist_attr(
|
||||
seed_var.name, seed_var_dist_attr
|
||||
)
|
||||
kwargs['seed_tensor'] = [seed_var.name]
|
||||
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# dropout backward is deterministic by mask, and not need for random state control
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"fused_dropout_add", DistributedDropoutImpl0("random_control")
|
||||
)
|
||||
@@ -0,0 +1,228 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
is_dim_replicate,
|
||||
is_dim_shard,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedFusedFeedForward(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedFusedFeedForward("fused_feedforward")
|
||||
)
|
||||
|
||||
|
||||
class DistributedFusedFeedForwardImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
linear1_weight = op_desc.input('Linear1Weight')[0]
|
||||
linear1_bias = op_desc.input('Linear1Bias')[0]
|
||||
linear2_weight = op_desc.input('Linear2Weight')[0]
|
||||
linear2_bias = op_desc.input('Linear2Bias')[0]
|
||||
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
linear1_weight_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear1_weight
|
||||
)
|
||||
linear1_bias_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear1_bias
|
||||
)
|
||||
linear2_weight_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear2_weight
|
||||
)
|
||||
linear2_bias_dims_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear2_bias
|
||||
)
|
||||
|
||||
for mapping in x_dims_mapping[1:-1]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
if is_dim_shard(linear1_weight_dims_mapping[-2]) or is_dim_replicate(
|
||||
linear1_weight_dims_mapping[-1]
|
||||
):
|
||||
return False
|
||||
if is_dim_replicate(linear1_bias_dims_mapping[-1]):
|
||||
return False
|
||||
if is_dim_replicate(linear2_weight_dims_mapping[-2]) or is_dim_shard(
|
||||
linear2_weight_dims_mapping[-1]
|
||||
):
|
||||
return False
|
||||
if is_dim_shard(linear2_bias_dims_mapping[-1]):
|
||||
return False
|
||||
if linear1_weight_dims_mapping[-1] != linear2_weight_dims_mapping[-2]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
# none of output should be sharded
|
||||
for out_name in op_desc.output_names():
|
||||
out = op_desc.output(out_name)[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out)
|
||||
for mapping in out_dims_mapping[1:-1]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Out')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Out')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
out_name, out_dims_mapping
|
||||
)
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# infer logic comm presentation
|
||||
linear1_weight = src_op.input('Linear1Weight')[0]
|
||||
linear1_weight_col_dim_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear1_weight
|
||||
)[-1]
|
||||
assert linear1_weight_col_dim_mapping >= 0, (
|
||||
f"col_parallel_matmul's row should be divided by a specific mesh axis, but got [{linear1_weight_col_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
parallel_axis = linear1_weight_col_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# insert op
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
# setting comm id
|
||||
new_op = main_block.ops[-1]
|
||||
assert new_op.type == "fused_feedforward"
|
||||
new_op._set_attr("ring_id", int(group.id))
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
# infer logic comm presentation
|
||||
linear2_weight = src_op.input('Linear2Weight')[0]
|
||||
linear2_weight_col_dim_mapping = op_dist_attr.get_input_dims_mapping(
|
||||
linear2_weight
|
||||
)[-1]
|
||||
assert linear2_weight_col_dim_mapping >= 0, (
|
||||
f"col_parallel_matmul's row should be divided by a specific mesh axis, but got [{linear2_weight_col_dim_mapping}]"
|
||||
)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
|
||||
parallel_axis = linear2_weight_col_dim_mapping
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, parallel_axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
# insert op
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
# setting comm id
|
||||
new_op = main_block.ops[-1]
|
||||
assert new_op.type == "fused_feedforward_grad"
|
||||
new_op._set_attr("ring_id", int(group.id))
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"fused_feedforward", DistributedFusedFeedForwardImpl("tensor_parallel")
|
||||
)
|
||||
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import logging
|
||||
|
||||
from paddle.base.log_helper import get_logger
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
class DistributedLayerNorm(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('x')[0]
|
||||
scale_name = op_desc.input('scale')[0]
|
||||
y_name = op_desc.output('y')[0]
|
||||
invvar_name = op_desc.output('invvar')[0]
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
scale_spec = get_dist_tensor_spec(dist_op, scale_name)
|
||||
|
||||
y_spec = get_dist_tensor_spec(dist_op, y_name, is_input=False)
|
||||
invvar_spec = get_dist_tensor_spec(dist_op, invvar_name, is_input=False)
|
||||
|
||||
epsilon = op_desc.attr('epsilon')
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("fused_rms_norm")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, scale_spec, epsilon)
|
||||
bw_results = rule.infer_backward(
|
||||
x_spec,
|
||||
scale_spec,
|
||||
y_spec,
|
||||
invvar_spec,
|
||||
epsilon,
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[x_name, scale_name],
|
||||
[y_name, invvar_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
# default impl
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedLayerNorm("fused_rms_norm")
|
||||
)
|
||||
@@ -0,0 +1,189 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..dist_attribute import DistTensorSpec
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedFusedRope(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args), build fake spec for optional args
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
input_parameters = op_desc.input_names()
|
||||
output_parameters = op_desc.output_names()
|
||||
is_input_arg_exist = lambda parameter: (
|
||||
parameter in input_parameters and op_desc.input(parameter)
|
||||
)
|
||||
is_output_arg_exist = lambda parameter: (
|
||||
parameter in output_parameters and op_desc.output(parameter)
|
||||
)
|
||||
|
||||
q = op_desc.input('q')[0]
|
||||
k = op_desc.input('k')[0] if is_input_arg_exist('k') else None
|
||||
v = op_desc.input('v')[0] if is_input_arg_exist('v') else None
|
||||
sin = op_desc.input('sin')[0] if is_input_arg_exist('sin') else None
|
||||
cos = op_desc.input('cos')[0] if is_input_arg_exist('cos') else None
|
||||
position_ids = (
|
||||
op_desc.input('position_ids')[0]
|
||||
if is_input_arg_exist('position_ids')
|
||||
else None
|
||||
)
|
||||
out_q = op_desc.output('out_q')[0]
|
||||
out_k = (
|
||||
op_desc.output('out_k')[0] if is_output_arg_exist('out_k') else None
|
||||
)
|
||||
out_v = (
|
||||
op_desc.output('out_v')[0] if is_output_arg_exist('out_v') else None
|
||||
)
|
||||
|
||||
q_spec = get_dist_tensor_spec(dist_op, q)
|
||||
k_spec = (
|
||||
get_dist_tensor_spec(dist_op, k)
|
||||
if k is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
v_spec = (
|
||||
get_dist_tensor_spec(dist_op, v)
|
||||
if v is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
sin_spec = (
|
||||
get_dist_tensor_spec(dist_op, sin)
|
||||
if sin is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
cos_spec = (
|
||||
get_dist_tensor_spec(dist_op, cos)
|
||||
if cos is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
position_ids_spec = (
|
||||
get_dist_tensor_spec(dist_op, position_ids)
|
||||
if position_ids is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
out_q_spec = get_dist_tensor_spec(dist_op, out_q, is_input=False)
|
||||
out_k_spec = (
|
||||
get_dist_tensor_spec(dist_op, out_k, is_input=False)
|
||||
if out_k is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
out_v_spec = (
|
||||
get_dist_tensor_spec(dist_op, out_v, is_input=False)
|
||||
if out_v is not None
|
||||
else DistTensorSpec()
|
||||
)
|
||||
|
||||
use_neox_rotary_style = op_desc.attr("use_neox_rotary_style")
|
||||
time_major = op_desc.attr("time_major")
|
||||
rotary_emb_base = op_desc.attr("rotary_emb_base")
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("fused_rotary_position_embedding")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(
|
||||
q_spec,
|
||||
k_spec,
|
||||
v_spec,
|
||||
sin_spec,
|
||||
cos_spec,
|
||||
position_ids_spec,
|
||||
use_neox_rotary_style,
|
||||
time_major,
|
||||
rotary_emb_base,
|
||||
)
|
||||
bw_results = rule.infer_backward(
|
||||
q_spec,
|
||||
k_spec,
|
||||
v_spec,
|
||||
sin_spec,
|
||||
cos_spec,
|
||||
position_ids_spec,
|
||||
out_q_spec,
|
||||
out_k_spec,
|
||||
out_v_spec,
|
||||
use_neox_rotary_style,
|
||||
time_major,
|
||||
rotary_emb_base,
|
||||
)
|
||||
|
||||
# remove optional args in spmd results
|
||||
input_args = [q, k, v, sin, cos, position_ids]
|
||||
output_args = [out_q, out_k, out_v]
|
||||
fw_and_bw_results_without_optional_arg = []
|
||||
for results in [fw_results, bw_results]:
|
||||
input_results = results[0]
|
||||
output_results = results[1]
|
||||
input_results_without_optional_arg = []
|
||||
output_results_without_optional_arg = []
|
||||
for idx, input_arg in enumerate(input_args):
|
||||
if input_arg is not None:
|
||||
input_results_without_optional_arg.append(
|
||||
input_results[idx]
|
||||
)
|
||||
for idx, output_arg in enumerate(output_args):
|
||||
if output_arg is not None:
|
||||
output_results_without_optional_arg.append(
|
||||
output_results[idx]
|
||||
)
|
||||
fw_and_bw_results_without_optional_arg.append(
|
||||
[
|
||||
input_results_without_optional_arg,
|
||||
output_results_without_optional_arg,
|
||||
]
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names=[
|
||||
input_arg for input_arg in input_args if input_arg is not None
|
||||
],
|
||||
output_arg_names=[
|
||||
output_arg
|
||||
for output_arg in output_args
|
||||
if output_arg is not None
|
||||
],
|
||||
fw_results=fw_and_bw_results_without_optional_arg[0],
|
||||
bw_results=fw_and_bw_results_without_optional_arg[1],
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedFusedRope("fused_rotary_position_embedding")
|
||||
)
|
||||
@@ -0,0 +1,70 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedGatherNd(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
index_name = op_desc.input('Index')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
|
||||
x_specs = get_dist_tensor_spec(dist_op, x_name)
|
||||
index_specs = get_dist_tensor_spec(dist_op, index_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("gather_nd")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_specs, index_specs)
|
||||
bw_results = rule.infer_backward(x_specs, index_specs, output_spec)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[x_name, index_name],
|
||||
[out_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedGatherNd("gather_nd"))
|
||||
@@ -0,0 +1,151 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
import copy
|
||||
import logging
|
||||
|
||||
from paddle.base.log_helper import get_logger
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..dist_attribute import DistTensorSpec, TensorDistAttr
|
||||
from ..utils import get_dist_tensor_spec, is_dim_shard
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
class DistributedLayerNorm(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
scale_name = (
|
||||
op_desc.input('Scale')[0]
|
||||
if len(op_desc.input('Scale')) > 0
|
||||
else None
|
||||
)
|
||||
bias_name = (
|
||||
op_desc.input('Bias')[0] if len(op_desc.input('Bias')) > 0 else None
|
||||
)
|
||||
y_name = op_desc.output('Y')[0]
|
||||
var_name = op_desc.output('Variance')[0]
|
||||
mean_name = op_desc.output('Mean')[0]
|
||||
begin_norm_axis = op_desc.attr('begin_norm_axis')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
scale_spec = (
|
||||
DistTensorSpec([0], TensorDistAttr())
|
||||
if scale_name is None
|
||||
else get_dist_tensor_spec(dist_op, scale_name)
|
||||
)
|
||||
bias_spec = (
|
||||
DistTensorSpec([0], TensorDistAttr())
|
||||
if bias_name is None
|
||||
else get_dist_tensor_spec(dist_op, bias_name)
|
||||
)
|
||||
y_spec = get_dist_tensor_spec(dist_op, y_name, False)
|
||||
var_spec = get_dist_tensor_spec(dist_op, var_name, False)
|
||||
mean_spec = get_dist_tensor_spec(dist_op, mean_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("layer_norm")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(
|
||||
x_spec, scale_spec, bias_spec, 1.0, begin_norm_axis
|
||||
)
|
||||
bw_results = rule.infer_backward(
|
||||
x_spec,
|
||||
scale_spec,
|
||||
bias_spec,
|
||||
y_spec,
|
||||
var_spec,
|
||||
mean_spec,
|
||||
1.0,
|
||||
begin_norm_axis,
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
input_arg_names = [x_name]
|
||||
if scale_name is not None:
|
||||
input_arg_names.append(scale_name)
|
||||
if bias_name is not None:
|
||||
input_arg_names.append(bias_name)
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names,
|
||||
[y_name, var_name, mean_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
begin_norm_axis = op_desc.attr('begin_norm_axis')
|
||||
|
||||
# sharded on begin_norm_axis
|
||||
x_name = op_desc.input('X')[0]
|
||||
x_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_input_dims_mapping(x_name)
|
||||
)
|
||||
if (begin_norm_axis > 0) and is_dim_shard(
|
||||
x_dims_mapping[begin_norm_axis]
|
||||
):
|
||||
# TODO (ljz) support sharding on `begin_norm_axis`
|
||||
_logger.info(
|
||||
"sharding on `begin_norm_axis` is not supported yet, we resharded it as replicated"
|
||||
)
|
||||
x_dims_mapping[begin_norm_axis] = -1
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
|
||||
param_names = [op_desc.input('Scale')[0], op_desc.input('Bias')[0]]
|
||||
for p_name in param_names:
|
||||
p_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_input_dims_mapping(p_name)
|
||||
)
|
||||
p_dims_mapping[begin_norm_axis] = -1
|
||||
op_dist_attr.set_input_dims_mapping(p_name, p_dims_mapping)
|
||||
|
||||
y_name = op_desc.output('Y')[0]
|
||||
y_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_output_dims_mapping(y_name)
|
||||
)
|
||||
y_dims_mapping[begin_norm_axis] = -1
|
||||
op_dist_attr.set_input_dims_mapping(y_name, y_dims_mapping)
|
||||
|
||||
# default impl
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedLayerNorm("layer_norm"))
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,387 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
|
||||
from paddle.common_ops_import import check_dtype, check_variable_and_dtype
|
||||
from paddle.distributed.utils.stream_utils import ExecutionStreamType
|
||||
from paddle.framework import core
|
||||
from paddle.static import Operator
|
||||
|
||||
from ..dist_attribute import OperatorDistAttr, TensorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
_get_comm_group,
|
||||
_get_corresponding_rank,
|
||||
compute_compatible_dim_mapping,
|
||||
is_dim_replicate,
|
||||
is_dim_shard,
|
||||
set_dist_op_desc_original_id,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
|
||||
|
||||
class DistributedPNorm(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedPNorm("p_norm"))
|
||||
|
||||
|
||||
# Data Parallel
|
||||
class DistributedPNormImpl0(DistributedOperatorImpl):
|
||||
"""
|
||||
TODO: p_norm scene
|
||||
|
||||
1. axis == None, isinstance(p, (int, float)), asvector = True
|
||||
1.1 x_dims_mapping == [0, -1, -1]
|
||||
allgather input if it is split by dp group
|
||||
1.2 x_dims_mapping == [-1, 0, -1]
|
||||
allgather, split and concat input if it is split by mp group
|
||||
2. isinstance(axis, int), asvector = False
|
||||
1.1 axis == 0 and x_dims_mapping == [0, -1, -1]
|
||||
allgather input if it's input[0] is splited by dp group.
|
||||
1.2 axis == 1 and x_dims_mapping == [-1, 0, -1]
|
||||
allgather, split and concat input if it's input[1] is split by mp group
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
axis = op_desc.attr('axis')
|
||||
asvector = op_desc.attr('asvector')
|
||||
x_name = op_desc.input('X')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
if is_dim_replicate(x_dims_mapping[0]):
|
||||
return False
|
||||
# Other dimensions must be replicate except the batch dimension
|
||||
for mapping in x_dims_mapping[1:]:
|
||||
if is_dim_shard(mapping):
|
||||
return False
|
||||
if not (axis == -1 and asvector) and not (axis == 0 and not asvector):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (
|
||||
(not self.is_input_compatible(dist_op))
|
||||
or (not self.is_output_compatible(dist_op))
|
||||
or (not self.is_compatible(dist_op))
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
axis = op_desc.attr('axis')
|
||||
keepdim = op_desc.attr('keepdim')
|
||||
|
||||
batch_dim_mappings = []
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if len(dims_mapping) >= 1:
|
||||
batch_dim_mappings.append(dims_mapping[0])
|
||||
|
||||
compatible_dim_mapping = compute_compatible_dim_mapping(
|
||||
batch_dim_mappings
|
||||
)
|
||||
if compatible_dim_mapping is None:
|
||||
return False
|
||||
|
||||
for arg_name in op_desc.input_arg_names():
|
||||
dims_mapping = op_dist_attr.get_input_dims_mapping(arg_name)
|
||||
if (
|
||||
len(dims_mapping) >= 1
|
||||
and compatible_dim_mapping != dims_mapping[0]
|
||||
):
|
||||
dims_mapping[0] = compatible_dim_mapping
|
||||
op_dist_attr.set_input_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
|
||||
if axis == 0 and not keepdim:
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if len(dims_mapping) >= 1 and dims_mapping[0] != -1:
|
||||
dims_mapping[0] = -1
|
||||
op_dist_attr.set_output_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
else:
|
||||
for arg_name in op_desc.output_arg_names():
|
||||
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
||||
if (
|
||||
len(dims_mapping) >= 1
|
||||
and compatible_dim_mapping != dims_mapping[0]
|
||||
):
|
||||
dims_mapping[0] = compatible_dim_mapping
|
||||
op_dist_attr.set_output_dims_mapping(arg_name, dims_mapping)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
if rank_id not in op_dist_attr.process_mesh.process_ids:
|
||||
rank_id = _get_corresponding_rank(
|
||||
ctx, op_dist_attr.process_mesh, rank_id
|
||||
)
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(X_var.name)
|
||||
for axis in range(len(in_dims_mapping)):
|
||||
if in_dims_mapping[axis] != -1:
|
||||
break
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
group_ranks = _get_comm_group(
|
||||
process_mesh_group, process_mesh_shape, axis, rank_id
|
||||
)
|
||||
group = new_process_group(group_ranks)
|
||||
|
||||
check_variable_and_dtype(
|
||||
X_var, 'x', ['float16', 'float32', 'float64'], 'norm'
|
||||
)
|
||||
check_dtype(
|
||||
X_var.dtype, 'dtype', ['float16', 'float32', 'float64'], 'norm'
|
||||
)
|
||||
|
||||
# 2. insert all_gather op
|
||||
# create all_gather output var
|
||||
allgather_out = main_block.create_var(
|
||||
name=".".join(["all_gather", X_var.name]),
|
||||
dtype=X_var.dtype,
|
||||
shape=X_var.shape,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=X_var.stop_gradient,
|
||||
)
|
||||
# set allgather_out tensor dist_attr
|
||||
allgather_out_dist_attr = TensorDistAttr()
|
||||
allgather_out_dist_attr.process_mesh = op_dist_attr.process_mesh
|
||||
allgather_out_dist_attr.chunk_id = op_dist_attr.chunk_id
|
||||
allgather_out_dist_attr.dims_mapping = [
|
||||
-1 for i in range(len(allgather_out.shape))
|
||||
]
|
||||
ctx.set_tensor_dist_attr_for_program(
|
||||
allgather_out, allgather_out_dist_attr
|
||||
)
|
||||
all_gather_op = main_block.append_op(
|
||||
type='all_gather',
|
||||
inputs={'x': [X_var]},
|
||||
outputs={'out': [allgather_out]},
|
||||
attrs={
|
||||
'ring_id': group.id,
|
||||
'use_calc_stream': True,
|
||||
'nranks': group.nranks,
|
||||
'op_role': src_op.attr('op_role'),
|
||||
},
|
||||
)
|
||||
# set all_gather op dist_attr
|
||||
allgather_op_dist_attr = OperatorDistAttr()
|
||||
allgather_op_dist_attr.process_mesh = op_dist_attr.process_mesh
|
||||
allgather_op_dist_attr.chunk_id = op_dist_attr.chunk_id
|
||||
allgather_op_dist_attr.set_input_dims_mapping(
|
||||
X_var.name, in_dims_mapping
|
||||
)
|
||||
allgather_op_dist_attr.set_output_dims_mapping(
|
||||
allgather_out.name, allgather_out_dist_attr.dims_mapping
|
||||
)
|
||||
allgather_op_dist_attr.execution_stream = (
|
||||
ExecutionStreamType.DefaultStream.value
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(all_gather_op, allgather_op_dist_attr)
|
||||
|
||||
# 3. copy p_norm op desc and reset input name
|
||||
# rename input
|
||||
kwargs['X'] = [allgather_out.name]
|
||||
# replicate op in dist program
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(dist_op_desc, src_op.desc, ctx)
|
||||
for input_name in src_op.desc.input_names():
|
||||
dist_op_desc.set_input(input_name, kwargs[input_name])
|
||||
for output_name in src_op.desc.output_names():
|
||||
dist_op_desc.set_output(output_name, kwargs[output_name])
|
||||
pnorm_op = Operator(main_block, dist_op_desc)
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
allgather_out.name, allgather_out_dist_attr.dims_mapping
|
||||
)
|
||||
# Remove the unrelated dist attr
|
||||
op_dist_attr.del_input_dist_attr(X_var.name)
|
||||
ctx.set_op_dist_attr_for_program(pnorm_op, op_dist_attr)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert op_dist_attr is not None
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in backward_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
backward_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in backward_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
backward_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
X_grad_var = main_block._var_recursive(kwargs['X@GRAD'][0])
|
||||
|
||||
# 1. copy p_norm_grad op and reset input name and output name
|
||||
new_kwargs = copy.deepcopy(kwargs)
|
||||
new_kwargs['X'] = [".".join(["all_gather", X_var.name])]
|
||||
new_X_var = main_block._var_recursive(new_kwargs['X'][0])
|
||||
new_X_grad = main_block.create_var(
|
||||
name=".".join(["all_gather", X_grad_var.name]),
|
||||
dtype=X_grad_var.dtype,
|
||||
shape=new_X_var.shape,
|
||||
type=core.VarDesc.VarType.DENSE_TENSOR,
|
||||
persistable=False,
|
||||
stop_gradient=X_grad_var.stop_gradient,
|
||||
)
|
||||
new_kwargs['X@GRAD'] = [new_X_grad.name]
|
||||
new_X_var_dist_attr = ctx.get_tensor_dist_attr_for_program(new_X_var)
|
||||
ctx.set_tensor_dist_attr_for_program(new_X_grad, new_X_var_dist_attr)
|
||||
# replicate op in dist program with new kwargs
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(backward_op.desc)
|
||||
# Refer to the related dist op
|
||||
set_dist_op_desc_original_id(dist_op_desc, backward_op.desc, ctx)
|
||||
for input_name in backward_op.desc.input_names():
|
||||
dist_op_desc.set_input(input_name, new_kwargs[input_name])
|
||||
for output_name in backward_op.desc.output_names():
|
||||
dist_op_desc.set_output(output_name, new_kwargs[output_name])
|
||||
p_norm_grad_op = Operator(main_block, dist_op_desc)
|
||||
op_dist_attr.set_input_dims_mapping(
|
||||
new_X_var.name, new_X_var_dist_attr.dims_mapping
|
||||
)
|
||||
# Store X_grad_var dims_mapping for later use
|
||||
X_grad_var_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
X_grad_var.name
|
||||
)
|
||||
# Remove the unrelated dist attr
|
||||
op_dist_attr.del_input_dist_attr(X_var.name)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
new_X_grad.name, new_X_var_dist_attr.dims_mapping
|
||||
)
|
||||
# Remove the unrelated dist attr
|
||||
op_dist_attr.del_output_dist_attr(X_grad_var.name)
|
||||
ctx.set_op_dist_attr_for_program(p_norm_grad_op, op_dist_attr)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
# 2. insert slice op
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
process_mesh_group = op_dist_attr.process_mesh.process_ids
|
||||
dims_mapping = [0] + [-1 for _ in range(len(new_X_grad.shape) - 1)]
|
||||
from ..reshard import Resharder
|
||||
|
||||
partition_idx = Resharder.compute_partition_index(
|
||||
rank_id,
|
||||
new_X_grad.shape,
|
||||
dims_mapping,
|
||||
process_mesh_shape,
|
||||
process_mesh_group,
|
||||
)
|
||||
slice_starts = []
|
||||
slice_ends = []
|
||||
slices_axes = []
|
||||
for idx, item in enumerate(partition_idx):
|
||||
slice_starts.append(item[0])
|
||||
slice_ends.append(item[1])
|
||||
slices_axes.append(idx)
|
||||
|
||||
infer_flags = [1 for i in range(len(slices_axes))]
|
||||
attrs = {
|
||||
"axes": slices_axes,
|
||||
"starts": slice_starts,
|
||||
"ends": slice_ends,
|
||||
"infer_flags": infer_flags,
|
||||
"op_role": backward_op.attr('op_role'),
|
||||
}
|
||||
slice_op = main_block.append_op(
|
||||
type='slice',
|
||||
inputs={'Input': [new_X_grad]},
|
||||
outputs={'Out': [X_grad_var]},
|
||||
attrs=attrs,
|
||||
)
|
||||
slice_op_dist_attr = OperatorDistAttr()
|
||||
slice_op_dist_attr.process_mesh = op_dist_attr.process_mesh
|
||||
slice_op_dist_attr.chunk_id = op_dist_attr.chunk_id
|
||||
slice_op_dist_attr.set_input_dims_mapping(
|
||||
new_X_grad.name, new_X_var_dist_attr.dims_mapping
|
||||
)
|
||||
slice_op_dist_attr.set_output_dims_mapping(
|
||||
X_grad_var.name, X_grad_var_dims_mapping
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(slice_op, slice_op_dist_attr)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"p_norm", DistributedPNormImpl0("data_parallel")
|
||||
)
|
||||
@@ -0,0 +1,240 @@
|
||||
# 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 copy
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OP_ROLE_KEY, OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..dist_attribute import OperatorDistAttr
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import (
|
||||
get_dist_tensor_spec,
|
||||
is_dim_shard,
|
||||
set_dist_op_desc_original_id,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedReduceSum(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert len(op_desc.input_arg_names()) == 1, (
|
||||
f"reduce_sum op [{op_desc.type}] has [{len(op_desc.input_arg_names())}] inputs"
|
||||
)
|
||||
input_arg_name = op_desc.input_arg_names()[0]
|
||||
assert len(op_desc.output_arg_names()) == 1, (
|
||||
f"reduce_sum op [{op_desc.type}] has [{len(op_desc.output_arg_names())}] outputs"
|
||||
)
|
||||
output_arg_name = op_desc.output_arg_names()[0]
|
||||
keep_dim = op_desc.attr('keep_dim')
|
||||
dims = op_desc.attr('dim')
|
||||
|
||||
# TODO (zhangyichen) replace dist tensor spec by dist tensor in future.
|
||||
input_spec = get_dist_tensor_spec(dist_op, input_arg_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_name, False)
|
||||
# len(dims) == 0 means reduce_all
|
||||
if len(dims) == 0:
|
||||
dims = list(range(len(input_spec.shape)))
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("reduce_sum")
|
||||
fw_results = rule.infer_forward(input_spec, dims, keep_dim)
|
||||
bw_results = rule.infer_backward(
|
||||
input_spec, output_spec, dims, keep_dim
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [input_arg_name], [output_arg_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
# NOTE this function will be remove once we use local reshard to replace distopimpls
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
op_desc = dist_op.serial_op.desc
|
||||
input_name = op_desc.input_arg_names()[0]
|
||||
input_dims_mapping = copy.deepcopy(
|
||||
op_dist_attr.get_input_dims_mapping(input_name)
|
||||
)
|
||||
axes = op_desc.attr('dim')
|
||||
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
reverted = False
|
||||
|
||||
def is_partial_reduce(axes, dims_mapping):
|
||||
# FIXME(ljz) Hack for performance:
|
||||
# if the reduce result is a scalar, it is the loss reduce in GPT case,
|
||||
# and if any axis of reduce input is sharded, the result loss would be partial.
|
||||
# BUT we keep the loss as partial instead of allreduce it for performance, since it would effect the backward.
|
||||
# we should use an optimization pass for the Hack in future.
|
||||
if len(axes) != 0 and (len(axes) < len(dims_mapping)):
|
||||
for axis in axes:
|
||||
if is_dim_shard(dims_mapping[axis]):
|
||||
return True # reverted
|
||||
return False
|
||||
|
||||
# if reduce_axis is sharded, the output is partial and need to be allreduce
|
||||
if is_partial_reduce(axes, input_dims_mapping):
|
||||
# TODO (ljz) support reduce where the reduce_axis is sharded
|
||||
dist_op.dist_attr = original_op_dist_attr
|
||||
reverted = True
|
||||
# if reduce_axis is unsharded, NO extra operator need.
|
||||
else:
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
return reverted
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedReduceSum("reduce_sum"))
|
||||
|
||||
|
||||
class DistributedReduceSumPrimitive(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedReduceSumPrimitive("reduce_sum_p")
|
||||
)
|
||||
|
||||
|
||||
# Batch Dimension ReduceSum Primitive
|
||||
class DistributedReduceSumPrimitiveImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
return len(op_desc.input_arg_names()) == 1
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
outputs = op_desc.output_arg_names()
|
||||
|
||||
if len(outputs) != 1:
|
||||
return False
|
||||
|
||||
output_name = outputs[0]
|
||||
output_var = dist_op.serial_op.block._var_recursive(output_name)
|
||||
if output_var.shape != ():
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
return self.is_input_compatible(dist_op) and self.is_output_compatible(
|
||||
dist_op
|
||||
)
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
startup_block = dist_op_context.startup_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
# replicate op in dist program
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(dist_op_desc, src_op.desc, ctx)
|
||||
for input_name in src_op.desc.input_names():
|
||||
dist_op_desc.set_input(input_name, kwargs[input_name])
|
||||
for output_name in src_op.desc.output_names():
|
||||
dist_op_desc.set_output(output_name, kwargs[output_name])
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
# batch dimension synchronization
|
||||
var_name = src_op.output_arg_names[0]
|
||||
sync_group = new_process_group(ctx.data_parallel_group)
|
||||
allreduce_op = main_block.append_op(
|
||||
type='all_reduce',
|
||||
inputs={'x': [var_name]},
|
||||
outputs={'out': [var_name]},
|
||||
attrs={
|
||||
'ring_id': sync_group.id,
|
||||
'reduce_type': paddle.distributed.ReduceOp.SUM,
|
||||
OP_ROLE_KEY: OpRole.Forward,
|
||||
},
|
||||
)
|
||||
|
||||
# dist attr
|
||||
var = main_block._var_recursive(var_name)
|
||||
tensor_dist_attr = ctx.get_tensor_dist_attr_for_program(var)
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
new_op_attr = OperatorDistAttr()
|
||||
new_op_attr.process_mesh = op_dist_attr.process_mesh
|
||||
new_op_attr.set_output_dims_mapping(
|
||||
var.name, tensor_dist_attr.dims_mapping
|
||||
)
|
||||
new_op_attr.set_input_dims_mapping(
|
||||
var.name, tensor_dist_attr.dims_mapping
|
||||
)
|
||||
ctx.set_op_dist_attr_for_program(allreduce_op, new_op_attr)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
raise RuntimeError("primitive operator does NOT have backward function")
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"reduce_sum_p",
|
||||
DistributedReduceSumPrimitiveImpl0("batch_dimension_reduce_sum_p"),
|
||||
)
|
||||
@@ -0,0 +1,866 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
Reshape2GradOpCost,
|
||||
Reshape2OpCost,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import (
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
is_dim_shard,
|
||||
set_dist_op_desc_original_id,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
is_parameter_related,
|
||||
merge_forward_backward_dims_mapping,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedReshape2(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert dist_op.serial_op.type == "reshape2", (
|
||||
f"{dist_op.serial_op.type} is not supported by dist reshape yet."
|
||||
)
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
xshape_name = op_desc.output('XShape')[0]
|
||||
shape = op_desc.attr('shape')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("reshape")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, shape)
|
||||
bw_results = rule.infer_backward(x_spec, output_spec, shape)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
# step4: update xshape
|
||||
inferred_input_dims_mappings, _ = merge_forward_backward_dims_mapping(
|
||||
fw_results, bw_results
|
||||
)
|
||||
dist_op.dist_attr.set_output_dims_mapping(
|
||||
xshape_name, [-1] + inferred_input_dims_mappings[0]
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
reverted = False
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
|
||||
# all reshape mapping to impl0
|
||||
op_dist_attr.impl_type = "reshape2"
|
||||
op_dist_attr.impl_idx = 0
|
||||
|
||||
return reverted
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedReshape2("reshape2"))
|
||||
|
||||
|
||||
class DistributedReshapeImpl0(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
res = []
|
||||
op = dist_op.serial_op
|
||||
dist_attr = dist_op.dist_attr
|
||||
|
||||
shape_list = op.desc.attr("shape")
|
||||
# got dist attribute info
|
||||
dim_mapping = dist_attr.get_output_dims_mapping(op.output("Out")[0])
|
||||
process_mesh_shape = dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_attr.process_mesh.process_ids
|
||||
for key in desc_mapping:
|
||||
desc_mapping[key]["shape"] = shape_list
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2OpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
return res
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2GradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping) - 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping) - 1:
|
||||
return False
|
||||
|
||||
if is_dim_shard(out_dims_mapping[-1]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for idx, dim_mapping in enumerate(out_dims_mapping[:-1]):
|
||||
if x_dims_mapping[idx] != dim_mapping:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[0] != -1:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[1:] != x_dims_mapping[:]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
x_shape_dims_mapping[i + 1] = x_dims_mapping[i]
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
x_shape_name, x_shape_dims_mapping
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
XShape_var = main_block._var_recursive(kwargs['XShape'][0])
|
||||
shape_list = src_op.desc.attr("shape")
|
||||
ShapeTensor_var_list = []
|
||||
for name in kwargs['ShapeTensor']:
|
||||
ShapeTensor_var_list.append(name)
|
||||
Shape_var_list = []
|
||||
for name in kwargs['Shape']:
|
||||
Shape_var_list.append(name)
|
||||
|
||||
# got dist attribute info
|
||||
dim_mapping = op_dist_attr.get_output_dims_mapping(Out_var.name)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# create op
|
||||
new_op = main_block.append_op(type='nop')
|
||||
new_op_desc = new_op.desc
|
||||
new_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(new_op_desc, src_op.desc, ctx)
|
||||
new_op_desc.set_input('ShapeTensor', ShapeTensor_var_list)
|
||||
new_op_desc.set_input('Shape', Shape_var_list)
|
||||
new_op_desc.set_input('X', [X_var.name])
|
||||
new_op_desc.set_output('XShape', [XShape_var.name])
|
||||
new_op_desc.set_output('Out', [Out_var.name])
|
||||
new_op_desc._set_attr('shape', shape_list)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
class DistributedReshapeImpl1(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
res = []
|
||||
op = dist_op.serial_op
|
||||
dist_attr = dist_op.dist_attr
|
||||
|
||||
shape_list = op.desc.attr("shape")
|
||||
# got dist attribute info
|
||||
dim_mapping = dist_attr.get_output_dims_mapping(op.output("Out")[0])
|
||||
process_mesh_shape = dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_attr.process_mesh.process_ids
|
||||
for key in desc_mapping:
|
||||
desc_mapping[key]["shape"] = shape_list
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2OpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
return res
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2GradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping) + 1:
|
||||
return False
|
||||
|
||||
if is_dim_shard(x_dims_mapping[-1]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping) + 1:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
if is_dim_shard(x_dims_mapping[-1]):
|
||||
return False
|
||||
|
||||
for idx, item in enumerate(x_dims_mapping[:-1]):
|
||||
if out_dims_mapping[idx] != item:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[0] != -1:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[1:] != x_dims_mapping[:]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
for i in range(len(out_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
x_shape_dims_mapping[i + 1] = x_dims_mapping[i]
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
x_shape_name, x_shape_dims_mapping
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
XShape_var = main_block._var_recursive(kwargs['XShape'][0])
|
||||
shape_list = src_op.desc.attr("shape")
|
||||
ShapeTensor_var_list = []
|
||||
for name in kwargs['ShapeTensor']:
|
||||
ShapeTensor_var_list.append(name)
|
||||
Shape_var_list = []
|
||||
for name in kwargs['Shape']:
|
||||
Shape_var_list.append(name)
|
||||
|
||||
# got dist attribute info
|
||||
dim_mapping = op_dist_attr.get_output_dims_mapping(Out_var.name)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# create op
|
||||
new_op = main_block.append_op(type='nop')
|
||||
new_op_desc = new_op.desc
|
||||
new_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(new_op_desc, src_op.desc, ctx)
|
||||
new_op_desc.set_input('ShapeTensor', ShapeTensor_var_list)
|
||||
new_op_desc.set_input('Shape', Shape_var_list)
|
||||
new_op_desc.set_input('X', [X_var.name])
|
||||
new_op_desc.set_output('XShape', [XShape_var.name])
|
||||
new_op_desc.set_output('Out', [Out_var.name])
|
||||
new_op_desc._set_attr('shape', shape_list)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
class DistributedReshapeImpl2(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
res = []
|
||||
op = dist_op.serial_op
|
||||
dist_attr = dist_op.dist_attr
|
||||
|
||||
shape_list = op.desc.attr("shape")
|
||||
# got dist attribute info
|
||||
dim_mapping = dist_attr.get_output_dims_mapping(op.output("Out")[0])
|
||||
process_mesh_shape = dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_attr.process_mesh.process_ids
|
||||
for key in desc_mapping:
|
||||
desc_mapping[key]["shape"] = shape_list
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2OpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
return res
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Reshape2GradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_name = op_desc.input('X')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
for idx, item in enumerate(x_dims_mapping[:-1]):
|
||||
if out_dims_mapping[idx] != item:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[0] != -1:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[1:] != out_dims_mapping[:]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
|
||||
for i in range(len(out_dims_mapping) - 1):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
for i in range(len(out_dims_mapping)):
|
||||
x_shape_dims_mapping[i + 1] = out_dims_mapping[i]
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
x_shape_name, x_shape_dims_mapping
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
"""
|
||||
kwargs: inputname_mapping & outputname_mapping
|
||||
"""
|
||||
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.work_block
|
||||
src_op = dist_op_context.cur_src_op
|
||||
op_dist_attr = ctx.get_op_dist_attr_for_program(src_op)
|
||||
assert op_dist_attr is not None, (
|
||||
f"backward op [{src_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
# check validation of inputs / outputs
|
||||
for input_name in src_op.desc.input_names():
|
||||
assert input_name in kwargs, f"input [{input_name}] is not given"
|
||||
assert len(kwargs[input_name]) == len(
|
||||
src_op.desc.input(input_name)
|
||||
), f"number of tensor for input [{input_name}] is not match"
|
||||
for output_name in src_op.desc.output_names():
|
||||
assert output_name in kwargs, f"input [{output_name}] is not given"
|
||||
assert len(kwargs[output_name]) == len(
|
||||
src_op.desc.output(output_name)
|
||||
), f"number of tensor for input [{output_name}] is not match"
|
||||
|
||||
X_var = main_block._var_recursive(kwargs['X'][0])
|
||||
Out_var = main_block._var_recursive(kwargs['Out'][0])
|
||||
XShape_var = main_block._var_recursive(kwargs['XShape'][0])
|
||||
shape_list = src_op.desc.attr("shape")
|
||||
ShapeTensor_var_list = []
|
||||
for name in kwargs['ShapeTensor']:
|
||||
ShapeTensor_var_list.append(name)
|
||||
Shape_var_list = []
|
||||
for name in kwargs['Shape']:
|
||||
Shape_var_list.append(name)
|
||||
|
||||
# got dist attribute info
|
||||
out_dim_mapping = op_dist_attr.get_output_dims_mapping(Out_var.name)
|
||||
process_mesh_shape = op_dist_attr.process_mesh.shape
|
||||
|
||||
# modify target shape
|
||||
for idx, axis in enumerate(out_dim_mapping):
|
||||
if axis >= 0:
|
||||
if len(shape_list) > idx:
|
||||
shape_list[idx] = (
|
||||
shape_list[idx] // process_mesh_shape[axis]
|
||||
)
|
||||
|
||||
# create op
|
||||
new_op = main_block.append_op(type='nop')
|
||||
new_op_desc = new_op.desc
|
||||
new_op_desc.copy_from(src_op.desc)
|
||||
set_dist_op_desc_original_id(new_op_desc, src_op.desc, ctx)
|
||||
new_op_desc.set_input('ShapeTensor', ShapeTensor_var_list)
|
||||
new_op_desc.set_input('Shape', Shape_var_list)
|
||||
new_op_desc.set_input('X', [X_var.name])
|
||||
new_op_desc.set_output('XShape', [XShape_var.name])
|
||||
new_op_desc.set_output('Out', [Out_var.name])
|
||||
new_op_desc._set_attr('shape', shape_list)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"reshape2", DistributedReshapeImpl0("add_one_dim_back")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"reshape2", DistributedReshapeImpl1("remove_one_dim_back")
|
||||
)
|
||||
register_distributed_operator_impl(
|
||||
"reshape2", DistributedReshapeImpl2("same_dim_shape")
|
||||
)
|
||||
@@ -0,0 +1,192 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..cost import (
|
||||
_g_op_cost_factory,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import compute_compatible_and_update_dim_mapping
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
is_parameter_related,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedScale(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
# TODO remove assign dist op
|
||||
# register_distributed_operator_impl_container(DistributedScale("scale"))
|
||||
# register_distributed_operator_impl_container(DistributedScale("fill_any_like"))
|
||||
# register_distributed_operator_impl_container(DistributedScale("where"))
|
||||
# register_distributed_operator_impl_container(DistributedScale("tanh"))
|
||||
|
||||
|
||||
class DistributedScaleImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
"""Calculate the cost by the op role."""
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res_cost = [cost_mapping]
|
||||
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
backward_op = dist_op.serial_op
|
||||
op_type = backward_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
_g_op_cost_factory[op_type], ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and not is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
need_gradient_allreduce = True
|
||||
break
|
||||
|
||||
if need_gradient_allreduce:
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(
|
||||
varname
|
||||
)
|
||||
mesh_shape = process_mesh.shape
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
return res
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
in_dims_mappings = []
|
||||
for in_name in op_desc.input_arg_names():
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
in_dims_mappings.append(in_dims_mapping)
|
||||
|
||||
for x_dims_mapping in in_dims_mappings:
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
changed = True
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
# register_distributed_operator_impl("scale", DistributedScaleImpl("scale"))
|
||||
# register_distributed_operator_impl(
|
||||
# "fill_any_like", DistributedScaleImpl("fill_any_like")
|
||||
# )
|
||||
# register_distributed_operator_impl("where", DistributedScaleImpl("where"))
|
||||
# register_distributed_operator_impl("tanh", DistributedScaleImpl("tanh"))
|
||||
@@ -0,0 +1,74 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..utils import is_dim_shard
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedShape(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedShape("shape"))
|
||||
|
||||
|
||||
class DistributedShapeImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
assert len(out_dims_mapping) == 1
|
||||
if is_dim_shard(out_dims_mapping[0]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl("shape", DistributedShapeImpl("shape"))
|
||||
@@ -0,0 +1,178 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
from ..utils import compute_compatible_dim_mapping, is_dim_shard
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedSlice(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedSlice("slice"))
|
||||
|
||||
|
||||
class DistributedSliceImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
in_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
in_var = dist_op.serial_op.block._var_recursive(in_name)
|
||||
out_var = dist_op.serial_op.block._var_recursive(out_name)
|
||||
axes = op_desc.attr('axes')
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
for axis in axes:
|
||||
if (
|
||||
is_dim_shard(in_dims_mapping[axis])
|
||||
and in_var.shape[axis] != out_var.shape[axis]
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
in_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
in_var = dist_op.serial_op.block._var_recursive(in_name)
|
||||
out_var = dist_op.serial_op.block._var_recursive(out_name)
|
||||
axes = op_desc.attr('axes')
|
||||
decrease_axis = op_desc.attr('decrease_axis')
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
ref_indices = []
|
||||
for i in range(len(in_dims_mapping)):
|
||||
if i not in decrease_axis:
|
||||
ref_indices.append(i)
|
||||
if ref_indices == []:
|
||||
assert len(out_dims_mapping) == 0
|
||||
else:
|
||||
for i in range(len(out_dims_mapping)):
|
||||
ref_index = ref_indices[i]
|
||||
if (
|
||||
ref_index in axes
|
||||
and is_dim_shard(out_dims_mapping[i])
|
||||
and in_var.shape[ref_index] != out_var.shape[ref_index]
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
in_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
decrease_axis = op_desc.attr('decrease_axis')
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if len(in_dims_mapping) - len(decrease_axis) != 0 and len(
|
||||
out_dims_mapping
|
||||
) != len(in_dims_mapping) - len(decrease_axis):
|
||||
return False
|
||||
|
||||
new_out_dims_mapping = []
|
||||
for i in range(len(in_dims_mapping)):
|
||||
if i not in decrease_axis:
|
||||
new_out_dims_mapping.append(in_dims_mapping[i])
|
||||
if new_out_dims_mapping == []:
|
||||
new_out_dims_mapping = [-1]
|
||||
if new_out_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (
|
||||
(not self.is_input_compatible(dist_op))
|
||||
or (not self.is_output_compatible(dist_op))
|
||||
or (not self.is_compatible(dist_op))
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
in_name = op_desc.input('Input')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
decrease_axis = op_desc.attr('decrease_axis')
|
||||
in_dims_mapping = op_dist_attr.get_input_dims_mapping(in_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
ref_dims_mapping = []
|
||||
ref_indices = []
|
||||
for i in range(len(in_dims_mapping)):
|
||||
if i not in decrease_axis:
|
||||
ref_dims_mapping.append(in_dims_mapping[i])
|
||||
ref_indices.append(i)
|
||||
|
||||
if ref_dims_mapping == []:
|
||||
assert len(ref_dims_mapping) == len(out_dims_mapping)
|
||||
changed = False
|
||||
else:
|
||||
assert len(ref_dims_mapping) == len(out_dims_mapping)
|
||||
for i in range(len(out_dims_mapping)):
|
||||
compatible_dim_mapping = compute_compatible_dim_mapping(
|
||||
[out_dims_mapping[i], ref_dims_mapping[i]]
|
||||
)
|
||||
if compatible_dim_mapping is None:
|
||||
continue
|
||||
if ref_dims_mapping[i] != compatible_dim_mapping:
|
||||
in_dims_mapping[ref_indices[i]] = compatible_dim_mapping
|
||||
changed = True
|
||||
if out_dims_mapping[i] != compatible_dim_mapping:
|
||||
out_dims_mapping[i] = compatible_dim_mapping
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(in_name, in_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"slice", DistributedSliceImpl("decrease_in_axis")
|
||||
)
|
||||
@@ -0,0 +1,200 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..cost import (
|
||||
SoftmaxGradOpCost,
|
||||
SoftmaxOpCost,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import compute_compatible_and_update_dim_mapping, is_dim_shard
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
is_parameter_related,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedSoftmax(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedSoftmax("softmax"))
|
||||
|
||||
|
||||
class DistributedSoftmaxImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = False
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
SoftmaxOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
|
||||
res_cost = [cost_mapping]
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
SoftmaxGradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
|
||||
return res
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
# if axis != -1 and axis != len(x_dims_mapping) - 1:
|
||||
# return False
|
||||
|
||||
if is_dim_shard(x_dims_mapping[axis]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_name = op_desc.output('Out')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
# if axis != -1 and axis != len(out_dims_mapping) - 1:
|
||||
# return False
|
||||
|
||||
if is_dim_shard(out_dims_mapping[axis]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
# if axis != -1 and axis != len(x_dims_mapping) - 1:
|
||||
# return False
|
||||
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"softmax", DistributedSoftmaxImpl("replicate_last_axis")
|
||||
)
|
||||
@@ -0,0 +1,197 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import (
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
is_dim_shard,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedSplit(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
assert len(op_desc.input('AxisTensor')) == 0, (
|
||||
"Attribute AxisTensor is not supported by dist split."
|
||||
)
|
||||
assert len(op_desc.input('SectionsTensorList')) == 0, (
|
||||
"Attribute SectionsTensorList is not supported by dist split."
|
||||
)
|
||||
output_arg_names = op_desc.output('Out')
|
||||
|
||||
num = op_desc.attr('num')
|
||||
sections = op_desc.attr('sections')
|
||||
if num:
|
||||
assert (sections is None) or (len(sections) == 0), (
|
||||
f"Both Attributes of num: {num} and sections: {sections} are specified."
|
||||
)
|
||||
first_attr = num
|
||||
rule_type = "split_with_num"
|
||||
else:
|
||||
assert not num, (
|
||||
f"Both Attributes of num: {num} and sections: {sections} are specified."
|
||||
)
|
||||
first_attr = sections
|
||||
rule_type = "split"
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
num_outputs = len(output_arg_names)
|
||||
output_specs = []
|
||||
for i in range(num_outputs):
|
||||
output_specs.append(
|
||||
get_dist_tensor_spec(dist_op, output_arg_names[i], False)
|
||||
)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule(rule_type)
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, first_attr, axis)
|
||||
bw_results = rule.infer_backward(x_spec, output_specs, first_attr, axis)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], output_arg_names, fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all split op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedSplit("split"))
|
||||
register_distributed_operator_impl_container(DistributedSplit("split_with_num"))
|
||||
|
||||
|
||||
class DistributedSplitImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = True
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
if is_dim_shard(x_dims_mapping[axis]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
out_names = op_desc.output('Out')
|
||||
axis = op_desc.attr('axis')
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if is_dim_shard(out_dims_mapping[axis]):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def is_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
axis = op_desc.attr('axis')
|
||||
out_names = op_desc.output('Out')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
if x_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_names = op_desc.output('Out')
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
|
||||
for out_name in out_names:
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
for i in range(len(x_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[x_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
out_name, out_dims_mapping
|
||||
)
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
return changed
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (
|
||||
(not self.is_input_compatible(dist_op))
|
||||
or (not self.is_output_compatible(dist_op))
|
||||
or (not self.is_compatible(dist_op))
|
||||
):
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"split", DistributedSplitImpl("replicate_in_axis")
|
||||
)
|
||||
@@ -0,0 +1,71 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedStack(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
input_arg_names = op_desc.input_arg_names()
|
||||
output_arg_names = op_desc.output_arg_names()
|
||||
axis = op_desc.attr('axis')
|
||||
|
||||
input_specs = []
|
||||
for name in input_arg_names:
|
||||
input_specs.append(get_dist_tensor_spec(dist_op, name))
|
||||
output_spec = get_dist_tensor_spec(dist_op, output_arg_names[0], False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("stack")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(input_specs, axis)
|
||||
bw_results = rule.infer_backward(input_specs, output_spec, axis)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
input_arg_names,
|
||||
output_arg_names,
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedStack("stack"))
|
||||
@@ -0,0 +1,81 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedStridedSlice(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
x_name = op_desc.input('Input')[0]
|
||||
y_name = op_desc.output('Out')[0]
|
||||
axes = op_desc.attr('axes')
|
||||
starts = op_desc.attr('starts')
|
||||
ends = op_desc.attr('ends')
|
||||
strides = op_desc.attr('strides')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
y_spec = get_dist_tensor_spec(dist_op, y_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("strided_slice")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, axes, starts, ends, strides)
|
||||
bw_results = rule.infer_backward(
|
||||
x_spec,
|
||||
y_spec,
|
||||
axes,
|
||||
starts,
|
||||
ends,
|
||||
strides,
|
||||
)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[x_name],
|
||||
[y_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedStridedSlice("strided_slice")
|
||||
)
|
||||
@@ -0,0 +1,72 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import (
|
||||
get_dist_tensor_spec,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedTile(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert dist_op.serial_op.type == "tile", (
|
||||
f"{dist_op.serial_op.type} is not supported by dist transpose yet."
|
||||
)
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
repeat_times = op_desc.attr('repeat_times')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("tile")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, repeat_times)
|
||||
bw_results = rule.infer_backward(x_spec, output_spec, repeat_times)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all elementwise op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(DistributedTile("tile"))
|
||||
@@ -0,0 +1,270 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from paddle.distributed.fleet.meta_optimizers.common import OpRole
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..cost import (
|
||||
Transpose2GradOpCost,
|
||||
Transpose2OpCost,
|
||||
build_comp_costs_from_descs,
|
||||
build_comp_desc_from_dist_op,
|
||||
build_dp_costs,
|
||||
)
|
||||
from ..utils import (
|
||||
compute_compatible_and_update_dim_mapping,
|
||||
get_dist_tensor_spec,
|
||||
)
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
is_parameter_related,
|
||||
merge_forward_backward_dims_mapping,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
from .dist_default import DistributedDefaultImpl0
|
||||
|
||||
|
||||
class DistributedTranspose2(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
assert dist_op.serial_op.type == "transpose2", (
|
||||
f"{dist_op.serial_op.type} is not supported by dist transpose yet."
|
||||
)
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
xshape_name = op_desc.output('XShape')[0]
|
||||
axes = op_desc.attr('axis')
|
||||
|
||||
x_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("transpose")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(x_spec, axes)
|
||||
bw_results = rule.infer_backward(x_spec, output_spec, axes)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op, [x_name], [out_name], fw_results, bw_results
|
||||
)
|
||||
|
||||
# step4: update xshape
|
||||
inferred_input_dims_mappings, _ = merge_forward_backward_dims_mapping(
|
||||
fw_results, bw_results
|
||||
)
|
||||
dist_op.dist_attr.set_output_dims_mapping(
|
||||
xshape_name, [-1] + inferred_input_dims_mappings[0]
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
# all elementwise op use default dist operator impl.
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedTranspose2("transpose2")
|
||||
)
|
||||
|
||||
|
||||
class DistributedTranspose2Impl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = False
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
return True
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
if (not self.is_input_compatible(dist_op)) or (
|
||||
not self.is_output_compatible(dist_op)
|
||||
):
|
||||
return False
|
||||
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
perm = op_desc.attr('axis')
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
new_dims_mapping = [-1 for i in range(len(x_dims_mapping))]
|
||||
for i in range(len(x_dims_mapping)):
|
||||
new_dims_mapping[i] = x_dims_mapping[perm[i]]
|
||||
|
||||
if len(x_dims_mapping) != len(out_dims_mapping):
|
||||
return False
|
||||
|
||||
if new_dims_mapping != out_dims_mapping:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[0] != -1:
|
||||
return False
|
||||
|
||||
if x_shape_dims_mapping[1:] != x_dims_mapping[:]:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
changed = False
|
||||
op_desc = dist_op.serial_op.desc
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
x_shape_name = op_desc.output('XShape')[0]
|
||||
x_dims_mapping = op_dist_attr.get_input_dims_mapping(x_name)
|
||||
out_dims_mapping = op_dist_attr.get_output_dims_mapping(out_name)
|
||||
x_shape_dims_mapping = op_dist_attr.get_output_dims_mapping(
|
||||
x_shape_name
|
||||
)
|
||||
perm = op_desc.attr('axis')
|
||||
|
||||
assert len(x_dims_mapping) == len(perm)
|
||||
|
||||
new_dims_mapping = [-1 for i in range(len(x_dims_mapping))]
|
||||
for i in range(len(x_dims_mapping)):
|
||||
new_dims_mapping[i] = x_dims_mapping[perm[i]]
|
||||
|
||||
for i in range(len(out_dims_mapping)):
|
||||
dim_changed = compute_compatible_and_update_dim_mapping(
|
||||
[new_dims_mapping, out_dims_mapping], [i, i]
|
||||
)
|
||||
if dim_changed:
|
||||
changed = True
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
if x_dims_mapping[perm[i]] != new_dims_mapping[i]:
|
||||
x_dims_mapping[perm[i]] = new_dims_mapping[i]
|
||||
changed = True
|
||||
|
||||
for i in range(len(x_dims_mapping)):
|
||||
x_shape_dims_mapping[i + 1] = x_dims_mapping[i]
|
||||
|
||||
if changed:
|
||||
op_dist_attr.set_input_dims_mapping(x_name, x_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(out_name, out_dims_mapping)
|
||||
op_dist_attr.set_output_dims_mapping(
|
||||
x_shape_name, x_shape_dims_mapping
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
def calc_cost(self, op_role, dist_op, ctx, cluster):
|
||||
cost = None
|
||||
if int(op_role) == int(OpRole.Backward):
|
||||
cost = self.calc_bwd_cost(dist_op, ctx, cluster)
|
||||
else:
|
||||
cost = self.calc_fwd_cost(dist_op, ctx, cluster)
|
||||
assert cost is not None
|
||||
return cost
|
||||
|
||||
def calc_fwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
processes = dist_op.dist_attr.process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Transpose2OpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
|
||||
res_cost = [cost_mapping]
|
||||
return res_cost
|
||||
|
||||
def calc_bwd_cost(self, dist_op, ctx, cluster):
|
||||
# calc comp op cost
|
||||
res = []
|
||||
desc_mapping = build_comp_desc_from_dist_op(
|
||||
dist_op=dist_op, dist_context=ctx
|
||||
)
|
||||
dist_attr = dist_op.dist_attr
|
||||
process_mesh = dist_attr.process_mesh
|
||||
processes = process_mesh.process_ids
|
||||
op_type = dist_op.serial_op.type
|
||||
cost_mapping = build_comp_costs_from_descs(
|
||||
Transpose2GradOpCost, ctx, processes, desc_mapping, cluster
|
||||
)
|
||||
res.append(cost_mapping)
|
||||
|
||||
backward_op = dist_op.serial_op
|
||||
main_block = backward_op.block
|
||||
need_gradient_allreduce = False
|
||||
for input_name in backward_op.desc.input_names():
|
||||
for varname in backward_op.desc.input(input_name):
|
||||
if "@GRAD" not in varname and is_parameter_related(
|
||||
varname, main_block
|
||||
):
|
||||
# NOTE input var's dim_mapping of backward op should be the same with input var instead of corresponding varname of forward op
|
||||
var_dim_mapping = dist_attr.get_input_dims_mapping(varname)
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
batch_size_axis = (
|
||||
var_dim_mapping[0] if len(var_dim_mapping) > 0 else -1
|
||||
)
|
||||
if batch_size_axis > -1 and mesh_shape[batch_size_axis] > 1:
|
||||
parallel_axis = batch_size_axis
|
||||
attrs = {"use_calc_stream": True}
|
||||
var_names = [varname + "@GRAD"]
|
||||
build_dp_costs(
|
||||
res,
|
||||
dist_op,
|
||||
ctx,
|
||||
var_names,
|
||||
attrs,
|
||||
parallel_axis,
|
||||
cluster,
|
||||
)
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.forward(ctx, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
DistributedDefaultImpl0.backward(ctx, *args, **kwargs)
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"transpose2", DistributedTranspose2Impl("same_mapping_transpose")
|
||||
)
|
||||
@@ -0,0 +1,75 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ..completion import get_phi_spmd_rule
|
||||
from ..utils import get_dist_tensor_spec
|
||||
from .common import (
|
||||
DistributedOperatorImplContainer,
|
||||
get_default_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
update_op_dims_mapping,
|
||||
)
|
||||
|
||||
|
||||
class DistributedUnSqueeze2(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
@staticmethod
|
||||
def update_dims_mapping(dist_op):
|
||||
# step1: prepare inputs need for rule (order args as PHI definition and filter out unnecessary args)
|
||||
op_desc = dist_op.serial_op.desc
|
||||
|
||||
axes_tensor = op_desc.input('AxesTensor')
|
||||
axes_tensor_list = op_desc.input('AxesTensorList')
|
||||
assert len(axes_tensor) == 0 and len(axes_tensor_list) == 0
|
||||
|
||||
x_name = op_desc.input('X')[0]
|
||||
out_name = op_desc.output('Out')[0]
|
||||
axes = op_desc.attr('axes')
|
||||
|
||||
input_spec = get_dist_tensor_spec(dist_op, x_name)
|
||||
output_spec = get_dist_tensor_spec(dist_op, out_name, False)
|
||||
|
||||
# step2: infer spmd
|
||||
rule = get_phi_spmd_rule("unsqueeze2")
|
||||
# tensor order following order in PHI definition
|
||||
fw_results = rule.infer_forward(input_spec, axes)
|
||||
bw_results = rule.infer_backward(input_spec, output_spec, axes)
|
||||
|
||||
# step3: update dist_attr
|
||||
# tensor order following order in PHI definition
|
||||
changed = update_op_dims_mapping(
|
||||
dist_op,
|
||||
[x_name],
|
||||
[out_name],
|
||||
fw_results,
|
||||
bw_results,
|
||||
)
|
||||
|
||||
return changed
|
||||
|
||||
@staticmethod
|
||||
def mapping_to_dist_operator_impl(dist_op, original_op_dist_attr):
|
||||
op_dist_attr = dist_op.dist_attr
|
||||
default_impl = get_default_distributed_operator_impl()
|
||||
op_dist_attr.impl_type = default_impl.type
|
||||
op_dist_attr.impl_idx = default_impl.idx
|
||||
|
||||
return False
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedUnSqueeze2("unsqueeze2")
|
||||
)
|
||||
@@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License
|
||||
|
||||
from ..utils import set_dist_op_desc_original_id
|
||||
from .common import (
|
||||
DistributedOperatorImpl,
|
||||
DistributedOperatorImplContainer,
|
||||
register_distributed_operator_impl,
|
||||
register_distributed_operator_impl_container,
|
||||
)
|
||||
|
||||
|
||||
class DistributedUpdateLossScaling(DistributedOperatorImplContainer):
|
||||
def __init__(self, op_type):
|
||||
super().__init__(op_type)
|
||||
|
||||
|
||||
register_distributed_operator_impl_container(
|
||||
DistributedUpdateLossScaling("update_loss_scaling")
|
||||
)
|
||||
|
||||
|
||||
class DistributedUpdateLossScalingImpl(DistributedOperatorImpl):
|
||||
def __init__(self, name):
|
||||
super().__init__(name)
|
||||
self._name = name
|
||||
self._forward_implemented = False
|
||||
self._backward_implemented = True
|
||||
|
||||
def is_input_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's is_input_compatible should not be called !"
|
||||
)
|
||||
|
||||
def is_output_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's is_output_compatible should not be called !"
|
||||
)
|
||||
|
||||
def is_auto_compatible(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's is_auto_compatible should not be called !"
|
||||
)
|
||||
|
||||
def update_dims_mapping(self, dist_op):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's update_dims_mapping should not be called !"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def forward(ctx, *args, **kwargs):
|
||||
raise RuntimeError(
|
||||
"DistributedUpdateLossScalingImpl's forward should not be called !"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def backward(ctx, *args, **kwargs):
|
||||
# the backward function only filter the gradient with current rank id
|
||||
dist_op_context = ctx.dist_op_context
|
||||
main_block = dist_op_context.main_block
|
||||
backward_op = dist_op_context.cur_src_op
|
||||
rank_id = dist_op_context.rank_id
|
||||
dist_attr = ctx.get_op_dist_attr_for_program(backward_op)
|
||||
assert dist_attr is not None, (
|
||||
f"backward op [{backward_op}] don't have dist attribute !"
|
||||
)
|
||||
|
||||
assert rank_id in dist_attr.process_mesh.process_ids
|
||||
|
||||
assert 'X' in kwargs, "input [{}] is not given".format('X')
|
||||
assert 'FoundInfinite' in kwargs, "input [{}] is not given".format(
|
||||
'FoundInfinite'
|
||||
)
|
||||
assert 'PrevLossScaling' in kwargs, "input [{}] is not given".format(
|
||||
'PrevLossScaling'
|
||||
)
|
||||
assert 'InGoodSteps' in kwargs, "input [{}] is not given".format(
|
||||
'InGoodSteps'
|
||||
)
|
||||
assert 'InBadSteps' in kwargs, "input [{}] is not given".format(
|
||||
'InBadSteps'
|
||||
)
|
||||
|
||||
assert 'Out' in kwargs, "output [{}] is not given".format('Out')
|
||||
assert 'LossScaling' in kwargs, "output [{}] is not given".format(
|
||||
'LossScaling'
|
||||
)
|
||||
assert 'OutGoodSteps' in kwargs, "output [{}] is not given".format(
|
||||
'OutGoodSteps'
|
||||
)
|
||||
assert 'OutBadSteps' in kwargs, "output [{}] is not given".format(
|
||||
'OutBadSteps'
|
||||
)
|
||||
|
||||
assert len(kwargs['FoundInfinite']) == 1, (
|
||||
"update_loss_scaling input FoundInfinite take 1 variable but got {}".format(
|
||||
kwargs['FoundInfinite']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['PrevLossScaling']) == 1, (
|
||||
"update_loss_scaling input PrevLossScaling take 1 variable but got {}".format(
|
||||
kwargs['PrevLossScaling']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['InGoodSteps']) == 1, (
|
||||
"update_loss_scaling input InGoodSteps take 1 variable but got {}".format(
|
||||
kwargs['InGoodSteps']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['InBadSteps']) == 1, (
|
||||
"update_loss_scaling input InBadSteps take 1 variable but got {}".format(
|
||||
kwargs['InBadSteps']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['LossScaling']) == 1, (
|
||||
"update_loss_scaling output LossScaling take 1 variable but got {}".format(
|
||||
kwargs['LossScaling']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['OutGoodSteps']) == 1, (
|
||||
"update_loss_scaling output OutGoodSteps take 1 variable but got {}".format(
|
||||
kwargs['OutGoodSteps']
|
||||
)
|
||||
)
|
||||
assert len(kwargs['OutBadSteps']) == 1, (
|
||||
"update_loss_scaling output OutBadSteps take 1 variable but got {}".format(
|
||||
kwargs['OutBadSteps']
|
||||
)
|
||||
)
|
||||
|
||||
assert len(kwargs['X']) == len(kwargs['Out']), (
|
||||
"update_loss_scaling got [{}] X and [{}] Out, which are supposed to be equal".format(
|
||||
len(kwargs['X']), len(kwargs['Out'])
|
||||
)
|
||||
)
|
||||
|
||||
filter_vars = []
|
||||
for varname in kwargs['X']:
|
||||
if (
|
||||
rank_id
|
||||
in ctx.get_tensor_dist_attr_for_program(
|
||||
main_block._var_recursive(varname)
|
||||
).process_mesh.process_ids
|
||||
):
|
||||
filter_vars.append(varname)
|
||||
|
||||
# replicate op in dist program
|
||||
dist_op = main_block.append_op(type='nop')
|
||||
dist_op_desc = dist_op.desc
|
||||
dist_op_desc.copy_from(backward_op.desc)
|
||||
set_dist_op_desc_original_id(dist_op_desc, backward_op.desc, ctx)
|
||||
dist_op_desc.set_input('X', filter_vars)
|
||||
dist_op_desc.set_output('Out', filter_vars)
|
||||
# TODO: should we add a new dist attr for the new op here?
|
||||
|
||||
|
||||
register_distributed_operator_impl(
|
||||
"update_loss_scaling",
|
||||
DistributedUpdateLossScalingImpl("update_loss_scaling"),
|
||||
)
|
||||
@@ -0,0 +1,539 @@
|
||||
# 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 copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import pickle
|
||||
import shlex
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.passes import PassContext, new_pass
|
||||
from paddle.distributed.utils.log_utils import get_logger
|
||||
from paddle.framework import core
|
||||
from paddle.static import append_backward, program_guard
|
||||
|
||||
from .cluster import Cluster
|
||||
from .completion import Completer
|
||||
from .dist_context import DistributedContext, set_default_distributed_context
|
||||
from .dist_op import DistributedOperator
|
||||
from .dist_tensor import DistributedTensor
|
||||
from .mapper import mapping
|
||||
from .partitioner import Partitioner
|
||||
from .planner import Planner
|
||||
from .process_group import (
|
||||
ProcessGroup,
|
||||
_g_process_group_map,
|
||||
get_all_process_groups,
|
||||
get_process_group,
|
||||
get_world_process_group,
|
||||
)
|
||||
from .reshard import Resharder
|
||||
from .utils import SerialProgramInfo, make_data_unshard
|
||||
|
||||
_logger = get_logger(logging.INFO)
|
||||
|
||||
|
||||
class AutoParallelizer:
|
||||
"""
|
||||
AutoParallelizer is the main controller class to do the auto parallel process.
|
||||
And the auto parallel process will be triggered in the wrapped parallelize function.
|
||||
To facilitate the auto parallelization, it will contain information about program, cluster and the
|
||||
related context. In this basic version, the program information will be retrieved from
|
||||
Fleet object, and the cluster information can be retrieved in the new created Cluster object,
|
||||
and the context information can be retrieved in the new created DistributedContext.
|
||||
"""
|
||||
|
||||
def __init__(self, fleet):
|
||||
self._fleet = fleet
|
||||
self._optimizer = self._fleet.user_defined_optimizer
|
||||
self._dist_strategy = self._fleet._user_defined_strategy
|
||||
self._dist_context = DistributedContext()
|
||||
self._cluster = None
|
||||
self._cluster_topo_path = os.getenv("PADDLE_CLUSTER_TOPO_PATH", None)
|
||||
if self._cluster_topo_path is not None:
|
||||
self._cluster = Cluster()
|
||||
self._cluster.build_from_file(self._cluster_topo_path)
|
||||
# Prepare information for auto mapping
|
||||
self._rank_mapping_path = os.getenv("PADDLE_RANK_MAPPING_PATH", None)
|
||||
enable_auto_mapping_env = os.getenv("PADDLE_ENABLE_AUTO_MAPPING", None)
|
||||
if enable_auto_mapping_env is None:
|
||||
self._enable_auto_mapping = False
|
||||
else:
|
||||
self._enable_auto_mapping = True
|
||||
self._pass_context = PassContext()
|
||||
|
||||
self._need_rank_mapping = os.getenv("PADDLE_NEED_RANK_MAPPING")
|
||||
self._need_rank_mapping = (
|
||||
True
|
||||
if self._need_rank_mapping
|
||||
and self._need_rank_mapping.lower() == 'true'
|
||||
else False
|
||||
)
|
||||
# self._pass_context = None
|
||||
|
||||
def _remove_distributed_attrs(self, main_program):
|
||||
suffix = core.kAutoParallelSuffix()
|
||||
# distributed attributes for variable have been removed
|
||||
# in previous process.
|
||||
for block in main_program.blocks:
|
||||
for op in block.ops:
|
||||
for attr_name in op.attr_names:
|
||||
if suffix in attr_name:
|
||||
op._remove_attr(attr_name)
|
||||
|
||||
def _apply_pre_optimization_passes(
|
||||
self, main_program, startup_program, loss, params_grads, no_grad_set
|
||||
):
|
||||
# apply amp pass
|
||||
if self._dist_strategy.amp:
|
||||
config = copy.deepcopy(self._dist_strategy.amp_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["loss"] = loss
|
||||
if config["use_pure_fp16"]:
|
||||
config["base_opt"] = self._optimizer
|
||||
auto_parallel_fp16_pass = new_pass("auto_parallel_fp16", config)
|
||||
auto_parallel_fp16_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
loss = auto_parallel_fp16_pass.get_loss()
|
||||
else:
|
||||
auto_parallel_amp_pass = new_pass("auto_parallel_amp", config)
|
||||
auto_parallel_amp_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
loss = auto_parallel_amp_pass.get_loss()
|
||||
|
||||
# apply recompute pass
|
||||
if self._dist_strategy.recompute:
|
||||
config = copy.deepcopy(self._dist_strategy.recompute_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["no_grad_set"] = copy.deepcopy(no_grad_set)
|
||||
config["loss"] = loss
|
||||
auto_parallel_recompute_pass = new_pass(
|
||||
"auto_parallel_recompute", config
|
||||
)
|
||||
auto_parallel_recompute_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
def _generate_backward(
|
||||
self,
|
||||
main_program,
|
||||
startup_program,
|
||||
loss,
|
||||
parameter_list,
|
||||
no_grad_set,
|
||||
callbacks,
|
||||
):
|
||||
with program_guard(main_program, startup_program):
|
||||
params_grads = append_backward(
|
||||
loss,
|
||||
parameter_list,
|
||||
no_grad_set,
|
||||
callbacks,
|
||||
distop_context=self._dist_context.dist_op_context,
|
||||
)
|
||||
self._completer = Completer(self._dist_context)
|
||||
self._completer.complete_backward_annotation(main_program)
|
||||
self._dist_context.block_state.parse_backward_blocks(main_program)
|
||||
return params_grads
|
||||
|
||||
def _apply_optimize(self, main_program, startup_program, params_grads):
|
||||
optimizer = copy.deepcopy(self._optimizer)
|
||||
with program_guard(main_program, startup_program):
|
||||
optimize_ops = optimizer.apply_gradients(params_grads)
|
||||
|
||||
self._dist_context._serial_optimizer = optimizer
|
||||
# update completion
|
||||
self._completer = Completer(self._dist_context)
|
||||
self._completer.complete_update_annotation(main_program)
|
||||
|
||||
return optimize_ops
|
||||
|
||||
def _apply_post_optimization_passes(
|
||||
self, main_program, startup_program, rank, params_grads
|
||||
):
|
||||
if self._dist_strategy.sharding:
|
||||
config = copy.deepcopy(self._dist_strategy.sharding_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["global_rank"] = rank
|
||||
auto_parallel_sharding_pass = new_pass(
|
||||
"auto_parallel_sharding", config
|
||||
)
|
||||
auto_parallel_sharding_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
params_grads = self._pass_context.get_attr("params_grads")
|
||||
|
||||
config = copy.deepcopy(self._dist_strategy.sharding_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["rank_id"] = rank
|
||||
auto_parallel_clip_pass = new_pass("auto_parallel_grad_clip", config)
|
||||
auto_parallel_clip_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
if self._dist_strategy.gradient_merge:
|
||||
config = copy.deepcopy(self._dist_strategy.gradient_merge_configs)
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
auto_parallel_gradient_merge_pass = new_pass(
|
||||
"auto_parallel_gradient_merge_pass", config
|
||||
)
|
||||
auto_parallel_gradient_merge_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
def _get_dist_program(self, rank, dist_context=None, relaunch_phase=False):
|
||||
completed_main_program = None
|
||||
serial_main_program = self._main_program.clone()
|
||||
serial_startup_program = self._startup_program.clone()
|
||||
serial_loss = serial_main_program.global_block().var(self._loss.name)
|
||||
|
||||
# generating serial
|
||||
if dist_context is None:
|
||||
# Annotation completion
|
||||
self._dist_context = DistributedContext()
|
||||
_logger.info("Start annotation dist attr.")
|
||||
self._completer = Completer(self._dist_context)
|
||||
completed_main_program = (
|
||||
self._completer.complete_forward_annotation(serial_main_program)
|
||||
)
|
||||
else:
|
||||
completed_main_program = serial_main_program
|
||||
self._dist_context = copy.deepcopy(dist_context)
|
||||
|
||||
# parse forward sub block
|
||||
self._dist_context.block_state.parse_forward_blocks(serial_main_program)
|
||||
|
||||
# serial backward pass
|
||||
params_grads = self._generate_backward(
|
||||
completed_main_program,
|
||||
serial_startup_program,
|
||||
serial_loss,
|
||||
self._parameter_list,
|
||||
self._no_grad_set,
|
||||
self._callbacks,
|
||||
)
|
||||
|
||||
# serial forward pass
|
||||
self._apply_pre_optimization_passes(
|
||||
completed_main_program,
|
||||
serial_startup_program,
|
||||
serial_loss,
|
||||
params_grads,
|
||||
self._no_grad_set,
|
||||
)
|
||||
# Logical partition
|
||||
partitioner = Partitioner(self._dist_context, rank)
|
||||
(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
dist_params_grads,
|
||||
) = partitioner.partition(
|
||||
completed_main_program, serial_startup_program, params_grads
|
||||
)
|
||||
|
||||
# TODO refactor the placement of optimizer
|
||||
# generate optimize program
|
||||
dist_optimize_ops = self._apply_optimize(
|
||||
dist_main_prog, dist_startup_prog, dist_params_grads
|
||||
)
|
||||
|
||||
make_data_unshard(dist_main_prog, dist_startup_prog, self._dist_context)
|
||||
|
||||
resharder = Resharder(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
rank,
|
||||
self._dist_context,
|
||||
dist_params_grads,
|
||||
)
|
||||
resharder.reshard()
|
||||
|
||||
self._apply_post_optimization_passes(
|
||||
dist_main_prog, dist_startup_prog, rank, dist_params_grads
|
||||
)
|
||||
g_process_group_map = None
|
||||
if not relaunch_phase:
|
||||
g_process_group_map = copy.deepcopy(_g_process_group_map)
|
||||
_g_process_group_map.clear()
|
||||
_g_process_group_map[0] = ProcessGroup(0, [])
|
||||
for process_mesh in self._dist_context._process_meshes:
|
||||
_g_process_group_map[0].add_ranks(process_mesh.process_ids)
|
||||
return (
|
||||
dist_optimize_ops,
|
||||
dist_params_grads,
|
||||
dist_startup_prog,
|
||||
dist_main_prog,
|
||||
g_process_group_map,
|
||||
)
|
||||
|
||||
def parallelize(
|
||||
self,
|
||||
loss,
|
||||
startup_program,
|
||||
parameter_list=None,
|
||||
no_grad_set=None,
|
||||
callbacks=None,
|
||||
):
|
||||
assert startup_program is not None
|
||||
self._loss = loss
|
||||
self._startup_program = startup_program
|
||||
self._main_program = loss.block.program
|
||||
self._parameter_list = parameter_list
|
||||
self._no_grad_set = no_grad_set
|
||||
self._callbacks = callbacks
|
||||
|
||||
if self._enable_auto_mapping and self._need_rank_mapping:
|
||||
# Do the mapping pass before parallelization
|
||||
assert self._cluster is not None, (
|
||||
"The cluster must not be none when using auto mapping."
|
||||
)
|
||||
dist_programs = {}
|
||||
world_process_group = get_world_process_group()
|
||||
dist_context = None
|
||||
# auto search
|
||||
if self._dist_strategy.auto_search:
|
||||
logging.info("Start searching dist attr.")
|
||||
serial_program_info = SerialProgramInfo(
|
||||
self._main_program,
|
||||
self._startup_program,
|
||||
self._loss,
|
||||
self._optimizer,
|
||||
self._cluster,
|
||||
)
|
||||
planner = Planner(
|
||||
serial_program_info,
|
||||
self,
|
||||
algorithm_config={"name": "mcmc", "max_search_times": 5},
|
||||
)
|
||||
dist_context, _ = planner.search()
|
||||
logging.info("End searching dist attr.")
|
||||
|
||||
# serialize the dist context by planner
|
||||
if dist_context is not None:
|
||||
logging.info("Start serialize searched dist attr")
|
||||
cwd = pathlib.Path().cwd()
|
||||
searched_dist_context_path = os.path.join(
|
||||
cwd, f"searched_dist_context_{time.time()}.pkl"
|
||||
)
|
||||
saved_dist_context = {}
|
||||
ops_dist_attr = {}
|
||||
tensors_dist_attr = {}
|
||||
for key, dist_op in dist_context._dist_ops_for_program.items():
|
||||
ops_dist_attr[key] = dist_op.dist_attr
|
||||
for (
|
||||
key,
|
||||
dist_tensor,
|
||||
) in dist_context._dist_tensors_for_program.items():
|
||||
tensors_dist_attr[key] = dist_tensor.dist_attr
|
||||
saved_dist_context["ops_dist_attr"] = ops_dist_attr
|
||||
saved_dist_context["tensors_dist_attr"] = tensors_dist_attr
|
||||
saved_dist_context["process_meshes"] = (
|
||||
dist_context._process_meshes
|
||||
)
|
||||
with open(
|
||||
searched_dist_context_path, "wb"
|
||||
) as dist_context_file:
|
||||
pickle.dump(saved_dist_context, dist_context_file)
|
||||
os.environ['PADDLE_SEARCHED_DIST_CONTEXT_PATH'] = (
|
||||
searched_dist_context_path
|
||||
)
|
||||
logging.info(
|
||||
f"End serialize searched dist attr to {searched_dist_context_path}"
|
||||
)
|
||||
|
||||
for rank in world_process_group.ranks:
|
||||
(
|
||||
dist_optimize_ops,
|
||||
dist_params_grads,
|
||||
dist_startup_prog,
|
||||
dist_main_prog,
|
||||
g_process_group_map,
|
||||
) = self._get_dist_program(rank, dist_context)
|
||||
dist_programs[rank] = [dist_main_prog, g_process_group_map]
|
||||
|
||||
# Do the mapping between the distributed program graph and the cluster graph
|
||||
rank_mapping_dict = mapping(dist_programs, self._cluster)
|
||||
rank_mapping = list(rank_mapping_dict.values())
|
||||
|
||||
# Relaunch the training by using the rank mapping file
|
||||
with open(self._rank_mapping_path, "w") as rank_mapping_file:
|
||||
json.dump(rank_mapping, rank_mapping_file)
|
||||
|
||||
enable_elastic = os.getenv("PADDLE_ENABLE_ELASTIC")
|
||||
enable_elastic = (
|
||||
True
|
||||
if enable_elastic and enable_elastic.lower() == 'true'
|
||||
else False
|
||||
)
|
||||
if enable_elastic:
|
||||
print("Auto mapping finished, now do elastic re-launch")
|
||||
sys.exit(
|
||||
paddle.distributed.fleet.elastic.manager.ELASTIC_AUTO_PARALLEL_EXIT_CODE
|
||||
)
|
||||
|
||||
original_cmd_args = os.getenv("PADDLE_ORIGINAL_CMD_ARGS")
|
||||
rank_mapping_args = " ".join(
|
||||
["--rank_mapping_path", self._rank_mapping_path]
|
||||
)
|
||||
if os.environ.get("WITH_COVERAGE", "OFF") == "ON":
|
||||
coverage_args = ["-m", "coverage", "run", "--branch", "-p"]
|
||||
else:
|
||||
coverage_args = []
|
||||
new_cmd_args = (
|
||||
"-m paddle.distributed.fleet.launch"
|
||||
+ " "
|
||||
+ rank_mapping_args
|
||||
+ " "
|
||||
+ original_cmd_args
|
||||
)
|
||||
new_cmd = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
*coverage_args,
|
||||
*shlex.split(new_cmd_args),
|
||||
]
|
||||
new_process = subprocess.Popen(new_cmd)
|
||||
new_process.wait()
|
||||
assert new_process.returncode == 0, (
|
||||
"Launch failed with rank mapping"
|
||||
)
|
||||
print("Successfully do the second launch for auto mapping!")
|
||||
sys.exit(0)
|
||||
else:
|
||||
# Parallelization after the mapping pass
|
||||
rank = paddle.distributed.get_rank()
|
||||
dist_context = None
|
||||
searched_dist_context_path = os.getenv(
|
||||
"PADDLE_SEARCHED_DIST_CONTEXT_PATH", None
|
||||
)
|
||||
if searched_dist_context_path is not None:
|
||||
with open(
|
||||
searched_dist_context_path, "rb"
|
||||
) as dist_context_file:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
saved_dist_context = safe_load_pickle(dist_context_file)
|
||||
dist_context = DistributedContext()
|
||||
for op in self._main_program.global_block().ops:
|
||||
dist_attr = saved_dist_context["ops_dist_attr"][
|
||||
op.desc.id()
|
||||
]
|
||||
dist_op = DistributedOperator(op, dist_attr)
|
||||
dist_context.add_dist_op_for_program(dist_op)
|
||||
|
||||
vars = self._main_program.global_block().vars
|
||||
for var in vars.values():
|
||||
dist_attr = saved_dist_context["tensors_dist_attr"][
|
||||
var.desc.id()
|
||||
]
|
||||
dist_tensor = DistributedTensor(var, dist_attr)
|
||||
dist_context.add_dist_tensor_for_program(dist_tensor)
|
||||
|
||||
dist_context._process_meshes = saved_dist_context[
|
||||
"process_meshes"
|
||||
]
|
||||
|
||||
else:
|
||||
if self._dist_strategy.auto_search:
|
||||
serial_program_info = SerialProgramInfo(
|
||||
self._main_program,
|
||||
self._startup_program,
|
||||
self._loss,
|
||||
self._optimizer,
|
||||
cluster=self._cluster,
|
||||
)
|
||||
planner = Planner(
|
||||
serial_program_info,
|
||||
self,
|
||||
algorithm_config={
|
||||
"name": "mcmc",
|
||||
"max_search_times": 5,
|
||||
},
|
||||
)
|
||||
dist_context, _ = planner.search()
|
||||
|
||||
# rebuild g_process_group
|
||||
if dist_context is not None:
|
||||
pg0 = get_process_group(0)
|
||||
for process_mesh in dist_context._process_meshes:
|
||||
pg0.add_ranks(process_mesh.process_ids)
|
||||
(
|
||||
dist_optimize_ops,
|
||||
dist_params_grads,
|
||||
dist_startup_prog,
|
||||
dist_main_prog,
|
||||
_,
|
||||
) = self._get_dist_program(rank, dist_context, relaunch_phase=True)
|
||||
|
||||
# NOTE: This is a trick to fix hang in pipeline mode when dist context is searched by planner
|
||||
if self._dist_strategy.auto_search:
|
||||
is_pipeline = False
|
||||
for op in dist_main_prog.global_block().ops:
|
||||
if op.type == "send_v2" or op.type == "recv_v2":
|
||||
is_pipeline = True
|
||||
break
|
||||
if is_pipeline:
|
||||
with paddle.static.program_guard(dist_main_prog):
|
||||
paddle.distributed.barrier()
|
||||
|
||||
# Traverse different rank programs and traverse each op of them,
|
||||
# instantiate communication by process_mapping.
|
||||
all_process_groups = get_all_process_groups()
|
||||
for process_group in all_process_groups:
|
||||
process_group.instantiate()
|
||||
|
||||
# Copy distributed info to the default context
|
||||
set_default_distributed_context(self._dist_context)
|
||||
|
||||
# The last step: remove all distributed attributes to be compatible
|
||||
# with inference.
|
||||
self._remove_distributed_attrs(dist_main_prog)
|
||||
|
||||
return (
|
||||
dist_optimize_ops,
|
||||
dist_params_grads,
|
||||
dist_startup_prog,
|
||||
dist_main_prog,
|
||||
)
|
||||
|
||||
def __deepcopy__(self, memo):
|
||||
cls = self.__class__
|
||||
result = cls.__new__(cls)
|
||||
memo[id(self)] = result
|
||||
for k, v in self.__dict__.items():
|
||||
if (
|
||||
k == "_main_program"
|
||||
or k == "_startup_program"
|
||||
or k == "_dist_context"
|
||||
or k == "_fleet"
|
||||
or k == "_loss"
|
||||
):
|
||||
setattr(result, k, v)
|
||||
else:
|
||||
setattr(result, k, copy.deepcopy(v, memo))
|
||||
return result
|
||||
@@ -0,0 +1,553 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
|
||||
from paddle.distributed.passes.pass_base import PassManager, new_pass
|
||||
from paddle.framework import get_flags
|
||||
from paddle.static import append_backward, program_guard
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
from ..random import init_auto_parallel_rng
|
||||
from .partitioner import Partitioner
|
||||
from .process_group import get_world_process_group
|
||||
from .reshard import Resharder
|
||||
from .utils import (
|
||||
get_pp_stage,
|
||||
is_sequential_run,
|
||||
)
|
||||
|
||||
PIR_PASS = [
|
||||
'fused_gemm_epilogue_pass',
|
||||
'fused_linear_param_grad_add_pass',
|
||||
'fuse_allreduce_split_to_reducescatter_pass',
|
||||
'fused_dropout_add_pass',
|
||||
]
|
||||
|
||||
PIR_PYTHON_PASS = [
|
||||
'eliminate_transpose',
|
||||
]
|
||||
|
||||
|
||||
class Parallelizer:
|
||||
def __init__(self, mode, completer, dist_context):
|
||||
self._mode = mode
|
||||
self._completer = completer
|
||||
self._dist_context = dist_context
|
||||
assert self._dist_context._is_initialized
|
||||
self._pass_context = self._dist_context.pass_context
|
||||
self._strategy = self._dist_context.strategy
|
||||
self._logger = get_logger(logging.INFO)
|
||||
|
||||
@property
|
||||
def is_train(self):
|
||||
return self._mode == "train"
|
||||
|
||||
@property
|
||||
def is_test(self):
|
||||
return self._mode in ["eval", "predict"]
|
||||
|
||||
def parallel_all(self, parameter_list=None):
|
||||
world_process_group = get_world_process_group()
|
||||
all_ranks = world_process_group.ranks
|
||||
for rank in all_ranks:
|
||||
# self._dist_context._backup(serial=True, dist=True)
|
||||
self.parallel(rank, parameter_list)
|
||||
# self._dist_context._restore(serial=True, dist=True)
|
||||
|
||||
def parallel(self, rank, parameter_list=None):
|
||||
serial_main_program = self._dist_context.serial_main_program
|
||||
serial_startup_program = self._dist_context.serial_startup_program
|
||||
serial_optimizer = self._dist_context.serial_optimizer
|
||||
if self.is_train and serial_optimizer:
|
||||
# Generate backward
|
||||
serial_loss = self._dist_context.serial_loss
|
||||
params_grads = self._generate_backward(
|
||||
serial_main_program,
|
||||
serial_startup_program,
|
||||
serial_loss,
|
||||
parameter_list,
|
||||
)
|
||||
# Apply pre optimization passes
|
||||
time0 = time.time()
|
||||
(
|
||||
serial_main_program,
|
||||
serial_startup_program,
|
||||
params_grads,
|
||||
) = self._apply_pre_optimization(
|
||||
serial_main_program,
|
||||
serial_startup_program,
|
||||
serial_loss,
|
||||
serial_optimizer,
|
||||
params_grads,
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel apply_pre_optimization time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Do logical partition
|
||||
time0 = time.time()
|
||||
partitioner = Partitioner(self._dist_context, rank)
|
||||
(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
dist_params_grads,
|
||||
) = partitioner.partition(
|
||||
serial_main_program, serial_startup_program, params_grads
|
||||
)
|
||||
|
||||
init_auto_parallel_rng()
|
||||
|
||||
self._logger.debug(
|
||||
f"within parallel partitioner time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Generate optimizer
|
||||
time0 = time.time()
|
||||
self._generate_optimizer(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
serial_optimizer,
|
||||
dist_params_grads,
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel optimizer time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
|
||||
resharder = Resharder(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
rank,
|
||||
self._dist_context,
|
||||
dist_params_grads,
|
||||
)
|
||||
resharder.reshard()
|
||||
self._logger.debug(
|
||||
f"within parallel reshard time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Apply post optimization passes
|
||||
time0 = time.time()
|
||||
self._apply_post_optimization(
|
||||
dist_main_prog, dist_startup_prog, rank, dist_params_grads
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel apply_post_optimization time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
else:
|
||||
# Apply pre optimization passes
|
||||
time0 = time.time()
|
||||
(
|
||||
serial_main_program,
|
||||
serial_startup_program,
|
||||
params_grads,
|
||||
) = self._apply_pre_optimization(
|
||||
serial_main_program, serial_startup_program, None, None, []
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel apply_pre_optimization time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Do logical partition
|
||||
time0 = time.time()
|
||||
partitioner = Partitioner(self._dist_context, rank)
|
||||
(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
dist_params_grads,
|
||||
) = partitioner.partition(
|
||||
serial_main_program, serial_startup_program, []
|
||||
)
|
||||
# Do reshard process
|
||||
self._logger.debug(
|
||||
f"within parallel partitioner time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
time0 = time.time()
|
||||
# Do reshard process
|
||||
micro_bsz = (
|
||||
1
|
||||
if not self._strategy.pipeline.enable
|
||||
else self._strategy.pipeline.micro_batch_size
|
||||
)
|
||||
resharder = Resharder(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
rank,
|
||||
self._dist_context,
|
||||
[],
|
||||
micro_bsz,
|
||||
)
|
||||
resharder.reshard()
|
||||
self._logger.debug(
|
||||
f"within parallel reshard time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
# Apply post optimization passes
|
||||
time0 = time.time()
|
||||
self._apply_post_optimization(
|
||||
dist_main_prog, dist_startup_prog, rank, dist_params_grads
|
||||
)
|
||||
self._logger.debug(
|
||||
f"within parallel apply_post_optimization time: {time.time() - time0}, mode {self._mode}"
|
||||
)
|
||||
|
||||
# Clone program for test
|
||||
if self.is_test:
|
||||
pipeline_opt = dist_main_prog._pipeline_opt
|
||||
dist_main_prog = dist_main_prog.clone(for_test=True)
|
||||
dist_startup_prog = dist_startup_prog.clone(for_test=True)
|
||||
dist_main_prog._pipeline_opt = pipeline_opt
|
||||
|
||||
# Store the distributed programs for further usages
|
||||
self._dist_context.dist_main_programs[rank] = dist_main_prog
|
||||
self._dist_context.dist_startup_programs[rank] = dist_startup_prog
|
||||
|
||||
def _generate_backward(
|
||||
self, main_program, startup_program, loss, parameter_list=None
|
||||
):
|
||||
# NOTE(zhaoyinglia):
|
||||
# Guarantee the order of params_grads is same between dynamic mode and static mode
|
||||
# by making parameter_list equal to model.parameters(),
|
||||
# because the order affect the result of ClipGradByGLobalNorm.
|
||||
# If parameter_list is not None, the order of params_grads is same with parameter_list.
|
||||
# If parameter_list is None, params_grads will be as prog.global_block().all_parameters().
|
||||
with program_guard(main_program, startup_program):
|
||||
params_grads = append_backward(
|
||||
loss,
|
||||
parameter_list=parameter_list,
|
||||
distop_context=self._dist_context.dist_op_context,
|
||||
)
|
||||
self._completer.complete_backward_annotation(main_program)
|
||||
self._dist_context.block_state.parse_backward_blocks(main_program)
|
||||
return params_grads
|
||||
|
||||
def _generate_optimizer(
|
||||
self, main_program, startup_program, optimizer, params_grads
|
||||
):
|
||||
# NOTE:
|
||||
# 1. `apply_gradients` will add an Accumulator for a parameter only once,
|
||||
# but optimizer will be called repeatedly in re-launch, so optimizer need to be copied.
|
||||
# 2. lr_scheduler cannot be deepcopy, cause 'deepcopy' will lead to difference of learning_rate between executor and engine.
|
||||
learning_rate = optimizer._learning_rate
|
||||
new_optimizer = copy.deepcopy(optimizer)
|
||||
new_optimizer._learning_rate = learning_rate
|
||||
new_optimizer._sorted = False
|
||||
self._dist_context._serial_optimizer = optimizer
|
||||
self._dist_context._serial_optimizer._learning_rate = learning_rate
|
||||
|
||||
with (
|
||||
program_guard(main_program, startup_program),
|
||||
main_program.switch_name_generator_guard("opt_"),
|
||||
):
|
||||
optimizer_ops = new_optimizer.apply_gradients(params_grads)
|
||||
self._completer.complete_update_annotation(main_program)
|
||||
return optimizer_ops
|
||||
|
||||
def _apply_pre_optimization(
|
||||
self, main_program, startup_program, loss, optimizer, params_grads
|
||||
):
|
||||
if self._strategy is None:
|
||||
return
|
||||
|
||||
# apply amp pass on train/eval/predict
|
||||
if self._strategy.amp.enable:
|
||||
config = copy.deepcopy(self._strategy.amp.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["loss"] = loss
|
||||
config["input_data"] = (
|
||||
self._dist_context.serial_feed_vars["inputs"]
|
||||
+ self._dist_context.serial_feed_vars["labels"]
|
||||
)
|
||||
self._logger.info(
|
||||
"Applying AMP-{}-{} ...".format(
|
||||
config["dtype"], config['level']
|
||||
),
|
||||
)
|
||||
if config['level'] == "o1":
|
||||
auto_parallel_amp_pass = new_pass("auto_parallel_amp", config)
|
||||
auto_parallel_amp_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
loss = auto_parallel_amp_pass.get_loss()
|
||||
elif config['level'] in ['o2', 'o3']:
|
||||
config["base_opt"] = optimizer
|
||||
auto_parallel_fp16_pass = new_pass("auto_parallel_fp16", config)
|
||||
auto_parallel_fp16_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
loss = auto_parallel_fp16_pass.get_loss()
|
||||
else:
|
||||
raise ValueError("AMP level should be one of o1, o2, o3")
|
||||
|
||||
# apply quantization pass
|
||||
# The pass can be applied when mode must be 'train'
|
||||
if self.is_train and self._strategy.qat.enable:
|
||||
config = copy.deepcopy(self._strategy.qat.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["mode"] = self._mode
|
||||
config["loss"] = loss
|
||||
auto_parallel_quantization_pass = new_pass(
|
||||
"auto_parallel_quantization", config
|
||||
)
|
||||
auto_parallel_quantization_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
main_program = self._pass_context.get_attr("main_program")
|
||||
startup_program = self._pass_context.get_attr("startup_program")
|
||||
params_grads = self._pass_context.get_attr("params_grads")
|
||||
loss = self._pass_context.get_attr("loss")
|
||||
|
||||
# apply recompute pass
|
||||
# recompute is then train-only optimization
|
||||
if self.is_train and self._strategy.recompute.enable:
|
||||
config = copy.deepcopy(self._strategy.recompute.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["no_grad_set"] = None
|
||||
config["loss"] = loss
|
||||
auto_parallel_recompute_pass = new_pass(
|
||||
"auto_parallel_recompute", config
|
||||
)
|
||||
auto_parallel_recompute_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
return main_program, startup_program, params_grads
|
||||
|
||||
def _check_dist_attr(self, program, num_model_chunks, dist_context):
|
||||
for _, block in enumerate(program.blocks):
|
||||
for _, op in enumerate(block.ops):
|
||||
op_dist_attr = dist_context.get_op_dist_attr_for_program(op)
|
||||
if op_dist_attr is None:
|
||||
raise ValueError(
|
||||
f"There is not dist_attr for op[{op.type}]."
|
||||
)
|
||||
|
||||
def _apply_post_optimization(
|
||||
self, main_program, startup_program, rank, params_grads
|
||||
):
|
||||
if self._strategy is None:
|
||||
return
|
||||
|
||||
# sequence parallel optimization
|
||||
if self._strategy.sp_optimization.enable:
|
||||
config = copy.deepcopy(self._strategy.sp_optimization.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["global_rank"] = rank
|
||||
sp_pass = new_pass(
|
||||
"auto_parallel_sequence_parallel_optimization", config
|
||||
)
|
||||
sp_pass.apply([main_program], [startup_program], self._pass_context)
|
||||
|
||||
# apply fused linear promotion pass
|
||||
if (
|
||||
self.is_train
|
||||
and self._strategy.fused_linear_promotion.enable
|
||||
and self._strategy.fused_passes.enable
|
||||
):
|
||||
if (
|
||||
len(self._strategy.fused_passes.fused_passes_list) > 0
|
||||
and "fuse_gemm_epilogue"
|
||||
in self._strategy.fused_passes.fused_passes_list
|
||||
):
|
||||
amp_config = None
|
||||
if self._strategy.amp.enable:
|
||||
amp_config = copy.deepcopy(self._strategy.amp.to_dict())
|
||||
config = {}
|
||||
config["dist_context"] = self._dist_context
|
||||
config["global_rank"] = rank
|
||||
config["enable_sp"] = self._strategy.sp_optimization.enable
|
||||
config["params_grads"] = params_grads
|
||||
config["amp_level"] = (
|
||||
amp_config['level'] if amp_config is not None else "o0"
|
||||
)
|
||||
fused_linear_promotion_pass = new_pass(
|
||||
"auto_parallel_fused_linear_promotion", config
|
||||
)
|
||||
fused_linear_promotion_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
# apply master grad pass
|
||||
if self._strategy.amp.enable:
|
||||
amp_config = copy.deepcopy(self._strategy.amp.to_dict())
|
||||
config = {}
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["completer"] = self._completer
|
||||
if amp_config['level'] == "o2" and amp_config["use_master_grad"]:
|
||||
master_grad_pass = new_pass(
|
||||
"auto_parallel_master_grad_pass", config
|
||||
)
|
||||
master_grad_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
# data parallel optimization
|
||||
if self._strategy.dp_optimization.enable:
|
||||
config = copy.deepcopy(self._strategy.dp_optimization.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["global_rank"] = rank
|
||||
config["use_sharding"] = self._strategy.sharding.enable
|
||||
dp_pass = new_pass(
|
||||
"auto_parallel_data_parallel_optimization", config
|
||||
)
|
||||
dp_pass.apply([main_program], [startup_program], self._pass_context)
|
||||
|
||||
gradient_sync_after_accumulate = (
|
||||
self._strategy.dp_optimization.gradient_sync_after_accumulate
|
||||
)
|
||||
if gradient_sync_after_accumulate:
|
||||
global_params_grads = params_grads
|
||||
|
||||
if self._strategy.sharding.enable:
|
||||
config = copy.deepcopy(self._strategy.sharding.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["global_rank"] = rank
|
||||
config["gradient_sync_after_accumulate"] = (
|
||||
gradient_sync_after_accumulate
|
||||
)
|
||||
if self._strategy.amp.enable:
|
||||
amp_config = copy.deepcopy(self._strategy.amp.to_dict())
|
||||
config["amp_dtype"] = amp_config['dtype']
|
||||
auto_parallel_sharding_pass = new_pass(
|
||||
"auto_parallel_sharding", config
|
||||
)
|
||||
auto_parallel_sharding_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
params_grads = self._pass_context.get_attr("params_grads")
|
||||
|
||||
if self._strategy.mp_optimization.allreduce_matmul_grad_overlapping:
|
||||
if int(os.getenv("CUDA_DEVICE_MAX_CONNECTIONS", "0")) != 1:
|
||||
self._logger.warning(
|
||||
"You set mp_optimization.allreduce_matmul_grad_overlapping=True, but you did not set environment "
|
||||
"variable CUDA_DEVICE_MAX_CONNECTIONS=1, which may leads to performance "
|
||||
"loss. Try to export CUDA_DEVICE_MAX_CONNECTIONS=1 for better performance."
|
||||
)
|
||||
|
||||
config = {
|
||||
"dist_context": self._dist_context,
|
||||
}
|
||||
allreduce_matmul_grad_overlapping_pass = new_pass(
|
||||
"allreduce_matmul_grad_overlapping", config
|
||||
)
|
||||
allreduce_matmul_grad_overlapping_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
if self.is_train:
|
||||
# GradClip is train-only optimization
|
||||
config = copy.deepcopy(self._strategy.sharding.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["params_grads"] = params_grads
|
||||
config["rank_id"] = rank
|
||||
auto_parallel_clip_pass = new_pass(
|
||||
"auto_parallel_grad_clip", config
|
||||
)
|
||||
auto_parallel_clip_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
if not is_sequential_run():
|
||||
# deps for newexe
|
||||
config = {}
|
||||
config["dist_context"] = self._dist_context
|
||||
APSED_pass = new_pass(
|
||||
"auto_parallel_supplement_explicit_dependencies", config
|
||||
)
|
||||
APSED_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
if self.is_train and self._strategy.pipeline.enable:
|
||||
self._strategy.gradient_merge.enable = True
|
||||
self._strategy.gradient_merge.k_steps = (
|
||||
self._strategy.pipeline.accumulate_steps
|
||||
)
|
||||
self._strategy.gradient_merge.avg = True
|
||||
|
||||
# gradient_merge is then train-only optimization
|
||||
grad_to_global_grad = {}
|
||||
if self.is_train and self._strategy.gradient_merge.enable:
|
||||
config = copy.deepcopy(self._strategy.gradient_merge.to_dict())
|
||||
config["dist_context"] = self._dist_context
|
||||
config["grad_to_global_grad"] = grad_to_global_grad
|
||||
config["pipeline_mode"] = self._strategy.pipeline.schedule_mode
|
||||
if gradient_sync_after_accumulate:
|
||||
config["params_grads"] = global_params_grads
|
||||
config["gradient_sync_after_accumulate"] = (
|
||||
gradient_sync_after_accumulate
|
||||
)
|
||||
else:
|
||||
config["params_grads"] = params_grads
|
||||
auto_parallel_gradient_merge_pass = new_pass(
|
||||
"auto_parallel_gradient_merge_pass", config
|
||||
)
|
||||
auto_parallel_gradient_merge_pass.apply(
|
||||
[main_program], [startup_program], self._pass_context
|
||||
)
|
||||
|
||||
self._check_dist_attr(
|
||||
main_program,
|
||||
self._strategy.pipeline.vpp_degree,
|
||||
self._dist_context,
|
||||
)
|
||||
|
||||
enable_ir = get_flags("FLAGS_enable_pir_in_executor")[
|
||||
'FLAGS_enable_pir_in_executor'
|
||||
]
|
||||
ir_pass_list = []
|
||||
if self.is_train and self._strategy.fused_passes.enable:
|
||||
if len(self._strategy.fused_passes.fused_passes_list) > 0:
|
||||
program_pass_list = []
|
||||
for p in self._strategy.fused_passes.fused_passes_list:
|
||||
if enable_ir and p in (PIR_PASS + PIR_PYTHON_PASS):
|
||||
ir_pass_list.append(p)
|
||||
else:
|
||||
program_pass_list.append(new_pass(p))
|
||||
pass_manager = PassManager(program_pass_list)
|
||||
pass_manager.apply([main_program], [startup_program])
|
||||
|
||||
main_program._pass_opt = {}
|
||||
main_program._pass_opt['pass_list'] = ir_pass_list
|
||||
|
||||
if self.is_train and self._strategy.pipeline.enable:
|
||||
enable_send_recv_overlap = (
|
||||
self._strategy.pipeline.enable_send_recv_overlap
|
||||
)
|
||||
if (
|
||||
enable_send_recv_overlap
|
||||
and int(os.getenv("CUDA_DEVICE_MAX_CONNECTIONS", "0")) != 1
|
||||
):
|
||||
self._logger.warning(
|
||||
"You set pipeline.enable_send_recv_overlap=True, but you did not set environment "
|
||||
"variable CUDA_DEVICE_MAX_CONNECTIONS=1, which may leads to performance "
|
||||
"loss. Try to export CUDA_DEVICE_MAX_CONNECTIONS=1 for better performance."
|
||||
)
|
||||
|
||||
main_program._pipeline_opt = {}
|
||||
main_program._pipeline_opt["standalone_opt"] = {
|
||||
"enable_send_recv_overlap": enable_send_recv_overlap,
|
||||
"schedule_mode": self._strategy.pipeline.schedule_mode,
|
||||
"num_micro_batches": self._strategy.pipeline.accumulate_steps,
|
||||
"pp_degree": len(self._dist_context.process_meshes),
|
||||
"pp_stage": get_pp_stage(self._dist_context, rank),
|
||||
"vpp_degree": self._strategy.pipeline.vpp_degree,
|
||||
"dist_context": self._dist_context,
|
||||
"program_runtimes": self._strategy.pipeline.program_runtimes,
|
||||
"memory_limit_times": self._strategy.pipeline.memory_limit_times,
|
||||
"split_backward": self._strategy.pipeline.split_backward,
|
||||
"grad_to_global_grad": grad_to_global_grad,
|
||||
}
|
||||
@@ -0,0 +1,543 @@
|
||||
# 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 copy
|
||||
from collections import defaultdict
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.auto_parallel.static.dist_context import (
|
||||
DistributedContext,
|
||||
)
|
||||
from paddle.distributed.auto_parallel.static.operators.common import (
|
||||
get_distributed_operator_impl_container,
|
||||
)
|
||||
from paddle.framework import Program, core
|
||||
from paddle.static import Parameter
|
||||
|
||||
from .dist_attribute import OperatorDistAttr
|
||||
from .operators.common import BACKWARD_ONLY_DIST_OPS
|
||||
from .utils import (
|
||||
__no_shape_var_type__,
|
||||
is_backward_op,
|
||||
is_forward_op,
|
||||
is_loss_op,
|
||||
is_optimize_op,
|
||||
)
|
||||
|
||||
__varname_not_in_block__ = ["lod_tensor_blocking_queue"]
|
||||
|
||||
|
||||
class Partitioner:
|
||||
"""
|
||||
warning:: Partitioner is experimental and subject to change.
|
||||
|
||||
Partitioner convert a program into another program.
|
||||
Given a serial program which has been auto completed with shard annotation, the Partitioner
|
||||
convert the serial program into a "distributed" program. The Partitioner will modify the serial
|
||||
program in following two ways, which is also the major difference between serial and distributed program:
|
||||
1. partition op: replace a serial op into its corresponding dist op inferred from the shard annotation
|
||||
2. partition var: if a var is sharded, modify the shape of var according to its shard annotation
|
||||
|
||||
Partitioner is supposed to be call by the auto parallel framework, and not supposed to be directly called by user.
|
||||
"""
|
||||
|
||||
def __init__(self, dist_context, rank_id=0):
|
||||
"""
|
||||
Args:
|
||||
dist_context (DistributedContext): used to access the distributed_attr of var & op, every Partitioner object could maintain its own DistributedContext member, and partition program base on that shard scenario.
|
||||
rank_id (int): global rank id to which the partitioned distributed program belong.
|
||||
"""
|
||||
if not isinstance(dist_context, DistributedContext):
|
||||
raise TypeError(
|
||||
f"dist_context be DistributedContext, got {type(dist_context)} here"
|
||||
)
|
||||
|
||||
self._dist_context = dist_context
|
||||
self._rank_id = rank_id
|
||||
self._serial2dist_varname_mapping = defaultdict(
|
||||
dict
|
||||
) # block_id -> serial_varname -> dist_varname
|
||||
self._dist_varname_suffix = ""
|
||||
self._forward_op_id2forward_op = {}
|
||||
|
||||
def partition(
|
||||
self, serial_main_program, serial_startup_program, params_grads
|
||||
):
|
||||
if not isinstance(serial_main_program, (Program)):
|
||||
raise TypeError(
|
||||
f"main_program be paddle.framework.Program, got {type(serial_main_program)} here"
|
||||
)
|
||||
|
||||
# check if shard annotated serial program valid
|
||||
if not self._is_valid_annotated_program(serial_main_program):
|
||||
raise RuntimeError(
|
||||
"Not all vars or ops are annotated in main program !"
|
||||
)
|
||||
|
||||
# init distop helper
|
||||
dist_op_context = self._dist_context.dist_op_context
|
||||
dist_op_context.varname_mapping = self._serial2dist_varname_mapping
|
||||
dist_op_context.rank_id = self._rank_id
|
||||
|
||||
# partition startup program
|
||||
if serial_startup_program is None:
|
||||
partitioned_startup_prog = None
|
||||
else:
|
||||
partitioned_startup_prog = self.partition_startup_program(
|
||||
serial_main_program, serial_startup_program
|
||||
)
|
||||
dist_op_context.dst_startup_program = partitioned_startup_prog
|
||||
|
||||
# partition main program
|
||||
(
|
||||
partitioned_main_prog,
|
||||
partitioned_params_grads,
|
||||
) = self.partition_main_program(serial_main_program, params_grads)
|
||||
|
||||
return (
|
||||
partitioned_main_prog,
|
||||
partitioned_startup_prog,
|
||||
partitioned_params_grads,
|
||||
)
|
||||
|
||||
def partition_startup_program(
|
||||
self, serial_main_program, serial_startup_program
|
||||
):
|
||||
if not isinstance(serial_startup_program, (Program)):
|
||||
raise TypeError(
|
||||
f"dist_context be paddle.framework.Program, got {type(serial_startup_program)} here"
|
||||
)
|
||||
|
||||
partitioned_startup_prog = paddle.framework.Program()
|
||||
partitioned_startup_prog._name_generator = (
|
||||
serial_startup_program._name_generator.clone()
|
||||
)
|
||||
ref_block = serial_main_program.global_block()
|
||||
target_block = partitioned_startup_prog.global_block()
|
||||
var2shape = {}
|
||||
temp_varname_map = {}
|
||||
|
||||
# tensors
|
||||
for var in serial_startup_program.list_vars():
|
||||
assert var.persistable
|
||||
new_name = var.name + self._dist_varname_suffix
|
||||
temp_varname_map[var.name] = new_name
|
||||
target_shape = _partition_var(
|
||||
self._dist_context, ref_block, target_block, var.name, new_name
|
||||
)
|
||||
var2shape[new_name] = target_shape
|
||||
|
||||
# ops
|
||||
for op in serial_startup_program.global_block().ops:
|
||||
# TODO if var not belong to this rank, should be filtered
|
||||
output_vars = op.desc.output_arg_names()
|
||||
assert len(output_vars) == 1, (
|
||||
f"initializer should output only ONE variable, but got [{op.desc}]"
|
||||
)
|
||||
assert temp_varname_map[output_vars[0]] in var2shape, (
|
||||
f"try to initialize [{output_vars[0]}] which is not a persistable var"
|
||||
)
|
||||
new_op_desc = target_block.desc.append_op()
|
||||
new_op_desc.copy_from(op.desc)
|
||||
new_op_desc._rename_output(
|
||||
output_vars[0], temp_varname_map[output_vars[0]]
|
||||
)
|
||||
new_op_desc._set_attr(
|
||||
"shape", var2shape[temp_varname_map[output_vars[0]]]
|
||||
)
|
||||
target_block._sync_with_cpp()
|
||||
|
||||
# set distribute attribute
|
||||
new_op = target_block.ops[-1]
|
||||
assert new_op.type == new_op_desc.type()
|
||||
assert new_op.desc == new_op_desc
|
||||
output_var = target_block.var(output_vars[0])
|
||||
output_var_attr = (
|
||||
self._dist_context.get_tensor_dist_attr_for_program(output_var)
|
||||
)
|
||||
op_attr = OperatorDistAttr()
|
||||
op_attr.process_mesh = output_var_attr.process_mesh
|
||||
op_attr.set_output_dims_mapping(
|
||||
output_var.name, output_var_attr.dims_mapping
|
||||
)
|
||||
op_attr.set_input_dims_mapping(
|
||||
output_var.name, output_var_attr.dims_mapping
|
||||
)
|
||||
self._dist_context.set_op_dist_attr_for_program(new_op, op_attr)
|
||||
|
||||
return partitioned_startup_prog
|
||||
|
||||
def partition_main_program(self, serial_main_program, params_and_grads):
|
||||
"""
|
||||
1. partition variables
|
||||
2. replace local op with corresponding dist op
|
||||
"""
|
||||
|
||||
partitioned_main_prog = paddle.framework.Program()
|
||||
partitioned_main_prog._name_generator = (
|
||||
serial_main_program._name_generator.clone()
|
||||
)
|
||||
dist_op_context = self._dist_context.dist_op_context
|
||||
dist_op_context.dst_main_program = partitioned_main_prog
|
||||
|
||||
for idx in range(self._dist_context.block_state.nblock):
|
||||
ref_block = serial_main_program.blocks[idx]
|
||||
|
||||
if idx == 0:
|
||||
target_block = partitioned_main_prog.blocks[0]
|
||||
else:
|
||||
target_block = partitioned_main_prog._create_block(
|
||||
parent_idx=ref_block.parent_idx
|
||||
)
|
||||
assert ref_block.idx == target_block.idx
|
||||
target_block._set_forward_block_idx(ref_block.forward_block_idx)
|
||||
dist_op_context.work_block = target_block
|
||||
self.partition_block(ref_block, target_block)
|
||||
|
||||
partitioned_main_prog.current_block_idx = 0
|
||||
|
||||
# should reconnect the block_attr ptr to the correct block
|
||||
for block_id in range(self._dist_context.block_state.nblock):
|
||||
block = partitioned_main_prog.block(block_id)
|
||||
for op in block.ops:
|
||||
for attr_name in op.all_attrs():
|
||||
if op.attr_type(attr_name) == core.AttrType.BLOCK:
|
||||
relative_id = op._block_attr_id(attr_name)
|
||||
op._set_attr(
|
||||
attr_name, partitioned_main_prog.block(relative_id)
|
||||
)
|
||||
|
||||
partitioned_params_and_grads = []
|
||||
for p, g in params_and_grads:
|
||||
assert p.name in self._serial2dist_varname_mapping[0]
|
||||
dist_p = self._get_dist_var_by_serial_var(
|
||||
p, partitioned_main_prog, 0
|
||||
)
|
||||
if g is None:
|
||||
dist_g = None
|
||||
else:
|
||||
assert g.name in self._serial2dist_varname_mapping[0]
|
||||
dist_g = self._get_dist_var_by_serial_var(
|
||||
g, partitioned_main_prog, 0
|
||||
)
|
||||
partitioned_params_and_grads.append((dist_p, dist_g))
|
||||
|
||||
return partitioned_main_prog, partitioned_params_and_grads
|
||||
|
||||
def partition_block(self, ref_block, target_block):
|
||||
dist_op_context = self._dist_context.dist_op_context
|
||||
|
||||
last_fwd_op_idx = -1
|
||||
for idx, op in enumerate(ref_block.ops):
|
||||
if is_loss_op(op):
|
||||
last_fwd_op_idx = idx
|
||||
break
|
||||
|
||||
if last_fwd_op_idx == -1:
|
||||
last_fwd_op_idx = len(ref_block.ops)
|
||||
|
||||
for idx in range(len(ref_block.ops)):
|
||||
if idx <= last_fwd_op_idx:
|
||||
self._forward_op_id2forward_op[
|
||||
ref_block.ops[idx].desc.original_id()
|
||||
] = ref_block.ops[idx]
|
||||
|
||||
# partition
|
||||
appended_grad_times = 0
|
||||
for idx, op in enumerate(ref_block.ops):
|
||||
op_dist_attr = self._dist_context.get_op_dist_attr_for_program(op)
|
||||
if is_backward_op(op) and (
|
||||
is_forward_op(ref_block.ops[idx - 1])
|
||||
or is_loss_op(ref_block.ops[idx - 1])
|
||||
):
|
||||
if not op_dist_attr.is_recompute:
|
||||
appended_grad_times += 1
|
||||
|
||||
# partition input variables
|
||||
for serial_input_varname in op.desc.input_arg_names():
|
||||
if (
|
||||
serial_input_varname
|
||||
not in self._serial2dist_varname_mapping[
|
||||
ref_block.forward_block_idx
|
||||
]
|
||||
or serial_input_varname
|
||||
not in self._serial2dist_varname_mapping[ref_block.idx]
|
||||
):
|
||||
new_varname = (
|
||||
serial_input_varname + self._dist_varname_suffix
|
||||
)
|
||||
if ref_block.has_var(serial_input_varname):
|
||||
_partition_var(
|
||||
self._dist_context,
|
||||
ref_block,
|
||||
target_block,
|
||||
serial_input_varname,
|
||||
new_varname,
|
||||
)
|
||||
self._serial2dist_varname_mapping[ref_block.idx][
|
||||
serial_input_varname
|
||||
] = new_varname
|
||||
|
||||
# partition output vars
|
||||
for serial_output_varname in op.desc.output_arg_names():
|
||||
if (
|
||||
serial_output_varname
|
||||
not in self._serial2dist_varname_mapping[
|
||||
ref_block.forward_block_idx
|
||||
]
|
||||
or serial_output_varname
|
||||
not in self._serial2dist_varname_mapping[ref_block.idx]
|
||||
):
|
||||
new_varname = (
|
||||
serial_output_varname + self._dist_varname_suffix
|
||||
)
|
||||
if ref_block.has_var(serial_output_varname):
|
||||
_partition_var(
|
||||
self._dist_context,
|
||||
ref_block,
|
||||
target_block,
|
||||
serial_output_varname,
|
||||
new_varname,
|
||||
)
|
||||
self._serial2dist_varname_mapping[ref_block.idx][
|
||||
serial_output_varname
|
||||
] = new_varname
|
||||
|
||||
# partition op
|
||||
if is_forward_op(op) or op_dist_attr.is_recompute:
|
||||
kinputs, koutputs = dist_op_context.prepare_context(op)
|
||||
dist_op_forward_impl = _get_dist_op_forward_implement(
|
||||
op, self._dist_context
|
||||
)
|
||||
dist_op_forward_impl.forward(
|
||||
self._dist_context, **kinputs, **koutputs
|
||||
)
|
||||
|
||||
elif is_backward_op(op):
|
||||
kinputs, koutputs = dist_op_context.prepare_context(op)
|
||||
dist_op_backward_impl = _get_dist_op_backward_implement(
|
||||
op, self._dist_context, self._forward_op_id2forward_op
|
||||
)
|
||||
grad_var_to_var = (
|
||||
self._dist_context.dist_op_context.grad_var_to_var[
|
||||
appended_grad_times
|
||||
]
|
||||
)
|
||||
dist_op_backward_impl.backward(
|
||||
self._dist_context,
|
||||
**kinputs,
|
||||
**koutputs,
|
||||
**{"grad_var_to_var": grad_var_to_var},
|
||||
)
|
||||
elif is_optimize_op(op):
|
||||
# NOTE: BACKWARD_ONLY_DIST_OPS's op_role must be 2 because of 1F1B PASS
|
||||
kinputs, koutputs = dist_op_context.prepare_context(op)
|
||||
dist_op_opt_impl = _get_dist_op_backward_implement(
|
||||
op, self._dist_context, self._forward_op_id2forward_op
|
||||
)
|
||||
dist_op_opt_impl.backward(
|
||||
self._dist_context,
|
||||
**kinputs,
|
||||
**koutputs,
|
||||
**{"grad_var_to_var": {}},
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"partitioner only support forward and backward, optimize ops, but got {op}"
|
||||
)
|
||||
|
||||
def _is_valid_annotated_program(self, program):
|
||||
# TODO (ZJ-LIANG) should check all block
|
||||
ops = program.global_block().ops
|
||||
vars_ = program.list_vars()
|
||||
op_dist_attrs = [
|
||||
self._dist_context.get_op_dist_attr_for_program(op) for op in ops
|
||||
]
|
||||
var_dist_attrs = [
|
||||
self._dist_context.get_tensor_dist_attr_for_program(var)
|
||||
for var in vars_
|
||||
if (var.type not in __no_shape_var_type__)
|
||||
]
|
||||
|
||||
all_ops_annotated = all(
|
||||
dist_attr is not None for dist_attr in op_dist_attrs
|
||||
)
|
||||
all_vars_annotated = all(
|
||||
dist_attr is not None for dist_attr in var_dist_attrs
|
||||
)
|
||||
|
||||
return all_ops_annotated and all_vars_annotated
|
||||
|
||||
def _get_dist_var_by_serial_var(
|
||||
self, serial_var, partitioned_main_prog, block_id
|
||||
):
|
||||
block_idx = serial_var.block.idx
|
||||
target_block = partitioned_main_prog.blocks[block_idx]
|
||||
dist_var_name = self._serial2dist_varname_mapping[block_id][
|
||||
serial_var.name
|
||||
]
|
||||
assert target_block.has_var(dist_var_name)
|
||||
return target_block.var(dist_var_name)
|
||||
|
||||
|
||||
def _get_dist_shape(var, dist_attr):
|
||||
var_shape = var.shape
|
||||
mapping = dist_attr.dims_mapping
|
||||
mesh = dist_attr.process_mesh.shape
|
||||
if mapping == []:
|
||||
return var_shape
|
||||
|
||||
assert len(var_shape) == len(mapping), (
|
||||
f"variable shape [{var_shape}] and dim_mapping [{mapping}] is NOT match !"
|
||||
)
|
||||
new_shape = []
|
||||
for idx in range(len(var_shape)):
|
||||
if var_shape[idx] == -1 or mapping[idx] == -1:
|
||||
new_shape.append(var_shape[idx])
|
||||
else:
|
||||
assert var_shape[idx] % mesh[mapping[idx]] == 0, (
|
||||
f"un-event partition: var_shape[idx]=[{var_shape[idx]}], mesh[{mesh[mapping[idx]]}], {var.name}, {var_shape}, {mesh}, {mapping}"
|
||||
)
|
||||
new_shape.append(var_shape[idx] // mesh[mapping[idx]])
|
||||
|
||||
return new_shape
|
||||
|
||||
|
||||
def _partition_parameter(
|
||||
dist_context, src_var, dst_block, dst_varname, dst_shape
|
||||
):
|
||||
# NOTE hack to copied Parameter
|
||||
# not initialized parameter, need to initialize it
|
||||
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
|
||||
|
||||
param = Parameter(
|
||||
block=dst_block,
|
||||
type=src_var.type,
|
||||
name=dst_varname,
|
||||
shape=dst_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,
|
||||
)
|
||||
|
||||
return param
|
||||
|
||||
|
||||
def _partition_intermediate_var(
|
||||
dist_context, src_var, dst_block, dst_varname, dst_shape
|
||||
):
|
||||
var = dst_block.create_var(
|
||||
type=src_var.type,
|
||||
name=dst_varname,
|
||||
shape=dst_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,
|
||||
)
|
||||
|
||||
return var
|
||||
|
||||
|
||||
def _partition_var(
|
||||
dist_context, src_block, dst_block, src_varname, dst_varname
|
||||
):
|
||||
"""
|
||||
partition include: split + replicate
|
||||
"""
|
||||
src_var = src_block.var(src_varname)
|
||||
|
||||
if src_var.type in __no_shape_var_type__:
|
||||
persist = getattr(src_var, 'persistable', False)
|
||||
new_var = dst_block.create_var(
|
||||
type=src_var.type,
|
||||
name=dst_varname,
|
||||
persistable=persist,
|
||||
stop_gradient=True,
|
||||
)
|
||||
target_shape = None
|
||||
else:
|
||||
dist_attr = dist_context.get_tensor_dist_attr_for_program(src_var)
|
||||
target_shape = _get_dist_shape(src_var, dist_attr)
|
||||
|
||||
if isinstance(src_var, Parameter):
|
||||
new_var = _partition_parameter(
|
||||
dist_context, src_var, dst_block, dst_varname, target_shape
|
||||
)
|
||||
else:
|
||||
new_var = _partition_intermediate_var(
|
||||
dist_context, src_var, dst_block, dst_varname, target_shape
|
||||
)
|
||||
|
||||
dist_attr = copy.deepcopy(
|
||||
dist_context.get_tensor_dist_attr_for_program(src_var)
|
||||
)
|
||||
assert dist_attr is not None
|
||||
dist_context.set_tensor_dist_attr_for_program(new_var, dist_attr)
|
||||
|
||||
return target_shape
|
||||
|
||||
|
||||
def _get_dist_op_backward_implement(
|
||||
backward_op, dist_context, forward_op_id2forward_op
|
||||
):
|
||||
dist_op_context = dist_context.dist_op_context
|
||||
if backward_op.desc.original_id() in dist_op_context.grad_op_id_to_op_id:
|
||||
forward_op_id = dist_op_context.grad_op_id_to_op_id[
|
||||
backward_op.desc.original_id()
|
||||
]
|
||||
forward_op = forward_op_id2forward_op[forward_op_id]
|
||||
forward_op_dist_attr = dist_context.get_op_dist_attr_for_program(
|
||||
forward_op
|
||||
)
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(
|
||||
forward_op_dist_attr.impl_type
|
||||
)
|
||||
dist_op_impl = dist_op_impl_container.get_impl(
|
||||
forward_op_dist_attr.impl_idx
|
||||
)
|
||||
return dist_op_impl
|
||||
|
||||
# # NOTE trick for dist ops that only have backward implement
|
||||
if backward_op.type in BACKWARD_ONLY_DIST_OPS:
|
||||
op_dist_attr = dist_context.get_op_dist_attr_for_program(backward_op)
|
||||
assert op_dist_attr.impl_idx >= 0
|
||||
dist_op_impl = get_distributed_operator_impl_container(
|
||||
op_dist_attr.impl_type
|
||||
).get_impl(op_dist_attr.impl_idx)
|
||||
return dist_op_impl
|
||||
|
||||
dist_op = get_distributed_operator_impl_container("default")
|
||||
return dist_op.get_impl(0)
|
||||
|
||||
|
||||
def _get_dist_op_forward_implement(forward_op, dist_context):
|
||||
dist_attr = dist_context.get_op_dist_attr_for_program(forward_op)
|
||||
dist_op_impl_container = get_distributed_operator_impl_container(
|
||||
dist_attr.impl_type
|
||||
)
|
||||
dist_op_impl = dist_op_impl_container.get_impl(dist_attr.impl_idx)
|
||||
return dist_op_impl
|
||||
File diff suppressed because it is too large
Load Diff
+1112
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,180 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
import numpy as np
|
||||
|
||||
from paddle.distributed.auto_parallel.process_mesh import ProcessMesh
|
||||
from paddle.distributed.auto_parallel.static.dist_attribute import (
|
||||
OperatorDistAttr,
|
||||
TensorDistAttr,
|
||||
)
|
||||
from paddle.distributed.auto_parallel.static.dist_op import DistributedOperator
|
||||
from paddle.distributed.auto_parallel.static.dist_tensor import (
|
||||
DistributedTensor,
|
||||
)
|
||||
|
||||
from ...utils.log_utils import get_logger
|
||||
from .completion import Completer
|
||||
from .dist_context import get_default_distributed_context
|
||||
from .tuner.parallel_tuner import ParallelTuner
|
||||
from .tuner.rule_based_tuner import RuleBasedTuner
|
||||
from .utils import is_naive_data_parallel
|
||||
|
||||
|
||||
class Planner:
|
||||
def __init__(self, mode, dist_context):
|
||||
self._mode = mode
|
||||
self._dist_context = dist_context
|
||||
self._load = False # load dist_attr from file
|
||||
|
||||
# NOTE: [HighOrderGrad]. There are grad ops in forward phase, and it need
|
||||
# dependency of backward-forward ops in forward completion.
|
||||
default_ctx = get_default_distributed_context()
|
||||
self._dist_context._dist_op_context = default_ctx.dist_op_context
|
||||
self._dist_context.data_parallel = default_ctx.data_parallel
|
||||
if not is_naive_data_parallel(self._dist_context):
|
||||
# Use SSA graph for complex parallelism
|
||||
self._dist_context.initialize(with_graph=True)
|
||||
else:
|
||||
# Use program for data parallel parallelism
|
||||
self._dist_context.initialize(with_graph=False)
|
||||
|
||||
self._completer = Completer(self._dist_context)
|
||||
|
||||
self._strategy = dist_context.strategy
|
||||
# set parallel tuner for auto search
|
||||
if self._strategy.auto_mode == "full_random":
|
||||
self._parallel_tuner = ParallelTuner(
|
||||
self._dist_context, mode=self._mode
|
||||
)
|
||||
elif self._strategy.auto_mode == "full_rule_based":
|
||||
self._parallel_tuner = RuleBasedTuner(
|
||||
self._dist_context, mode=self._mode
|
||||
)
|
||||
|
||||
@property
|
||||
def completer(self):
|
||||
return self._completer
|
||||
|
||||
def plan(self):
|
||||
logger = get_logger(logging.INFO)
|
||||
path = None
|
||||
if self._dist_context._json_config:
|
||||
try:
|
||||
path = self._dist_context._json_config["tuner_load_path"]
|
||||
except:
|
||||
path = None
|
||||
if path and os.path.exists(path):
|
||||
try:
|
||||
with open(path, "rb") as f:
|
||||
from paddle.framework.restricted_unpickler import (
|
||||
safe_load_pickle,
|
||||
)
|
||||
|
||||
dist_attrs = safe_load_pickle(f)
|
||||
tensor_dist_attrs = dist_attrs["tensor"]
|
||||
op_dist_attrs = dist_attrs["op"]
|
||||
process_meshes = dist_attrs["process_meshes"]
|
||||
cluster = dist_attrs["cluster"]
|
||||
last_gpu_model = cluster.machines[0].devices[0].model
|
||||
last_gpu_memory = cluster.machines[0].devices[0].memory
|
||||
last_node_count = len(cluster.machines)
|
||||
last_device_count = len(cluster.get_all_devices("GPU"))
|
||||
|
||||
gpu_model = (
|
||||
self._dist_context.cluster.machines[0].devices[0].model
|
||||
)
|
||||
gpu_memory = (
|
||||
self._dist_context.cluster.machines[0].devices[0].memory
|
||||
)
|
||||
node_count = len(self._dist_context.cluster.machines)
|
||||
device_count = len(
|
||||
self._dist_context.cluster.get_all_devices("GPU")
|
||||
)
|
||||
if (
|
||||
gpu_model != last_gpu_model
|
||||
or gpu_memory != last_gpu_memory
|
||||
or last_node_count != node_count
|
||||
or device_count != last_device_count
|
||||
):
|
||||
logger.info(
|
||||
f"The cluster {node_count} nodes {device_count} {gpu_model} devices is different from the saved last cluster {last_node_count} nodes {last_device_count} {last_gpu_model} devices, so we run the planner again."
|
||||
)
|
||||
need_set_dist_attr = False
|
||||
else:
|
||||
need_set_dist_attr = True
|
||||
except:
|
||||
need_set_dist_attr = False
|
||||
|
||||
if need_set_dist_attr:
|
||||
for key in op_dist_attrs:
|
||||
serial_op = self._dist_context._dist_ops_for_program[
|
||||
key
|
||||
].serial_op
|
||||
# clear dist attr
|
||||
serial_op.dist_attr = OperatorDistAttr(serial_op.desc)
|
||||
serial_op.dist_attr.parse_from_string(op_dist_attrs[key])
|
||||
self._dist_context._dist_ops_for_program[key] = (
|
||||
DistributedOperator(serial_op)
|
||||
)
|
||||
|
||||
for key in tensor_dist_attrs:
|
||||
serial_tensor = (
|
||||
self._dist_context._dist_tensors_for_program[
|
||||
key
|
||||
].serial_tensor
|
||||
)
|
||||
# clear dist attr
|
||||
serial_tensor.dist_attr = TensorDistAttr(serial_tensor.desc)
|
||||
serial_tensor.dist_attr.parse_from_string(
|
||||
tensor_dist_attrs[key]
|
||||
)
|
||||
self._dist_context._dist_tensors_for_program[key] = (
|
||||
DistributedTensor(serial_tensor)
|
||||
)
|
||||
|
||||
process_meshes = []
|
||||
for item in dist_attrs["process_meshes"]:
|
||||
process_ids = item[0]
|
||||
shape = item[1]
|
||||
process_meshes.append(
|
||||
ProcessMesh(
|
||||
np.array(process_ids).reshape(shape).tolist()
|
||||
)
|
||||
)
|
||||
|
||||
self._dist_context.process_meshes = process_meshes
|
||||
self._load = True
|
||||
|
||||
logger.info(
|
||||
f"The parallel strategy has been loaded from {path}"
|
||||
)
|
||||
|
||||
if not self._load:
|
||||
if self._strategy.auto_mode != "semi":
|
||||
self._parallel_tuner.tune()
|
||||
else:
|
||||
self._completer.complete_forward_annotation()
|
||||
|
||||
if os.getenv("PADDLE_AUTO_PARALLEL_STAGE", "run") != "run":
|
||||
sys.exit()
|
||||
|
||||
# parse forward sub block
|
||||
self._dist_context.block_state.parse_forward_blocks(
|
||||
self._dist_context.serial_main_program
|
||||
)
|
||||
@@ -0,0 +1,275 @@
|
||||
# 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 hashlib
|
||||
from collections import OrderedDict
|
||||
|
||||
import paddle
|
||||
from paddle.framework import core
|
||||
|
||||
from ...collective import _get_global_env, _new_ring_id
|
||||
from ...utils.log_utils import get_logger
|
||||
from .utils import dygraph_guard
|
||||
|
||||
logger = get_logger("INFO", __name__)
|
||||
|
||||
|
||||
def get_all_process_groups():
|
||||
global _g_process_group_map
|
||||
return _g_process_group_map.values()
|
||||
|
||||
|
||||
def get_process_group(group_id, g_process_group_map=None):
|
||||
global _g_process_group_map
|
||||
return (
|
||||
_g_process_group_map.get(group_id, None)
|
||||
if g_process_group_map is None
|
||||
else g_process_group_map.get(group_id, None)
|
||||
)
|
||||
|
||||
|
||||
def get_world_process_group():
|
||||
global _g_process_group_map
|
||||
return _g_process_group_map[0]
|
||||
|
||||
|
||||
def clear_all_process_groups():
|
||||
global _g_process_group_map
|
||||
_g_process_group_map = {}
|
||||
_g_process_group_map[0] = ProcessGroup(0, [])
|
||||
|
||||
|
||||
def remove_process_group(ring_id):
|
||||
global _g_process_group_map
|
||||
if ring_id in _g_process_group_map:
|
||||
_g_process_group_map.pop(ring_id)
|
||||
|
||||
|
||||
def new_process_group(
|
||||
ranks, group_id=None, force_new_group=False, group_type=None
|
||||
):
|
||||
global _g_process_group_map
|
||||
if not force_new_group:
|
||||
# A key constructed from ranks is used for avoiding duplication
|
||||
new_key = '_'.join(map(str, ranks))
|
||||
for pg_id, pg in _g_process_group_map.items():
|
||||
cur_key = '_'.join(map(str, pg.ranks))
|
||||
if pg_id != 0 and new_key == cur_key:
|
||||
return pg
|
||||
# If not matching the existing one, construct a new process group
|
||||
num_groups = len(_g_process_group_map)
|
||||
# Note: our process group may interfere with the original implementation
|
||||
# so the created group id should start from the original _new_ring_id()
|
||||
if group_id is None:
|
||||
group_id = _new_ring_id() + num_groups + 1
|
||||
|
||||
new_pg = ProcessGroup(group_id, ranks, group_type)
|
||||
_g_process_group_map[group_id] = new_pg
|
||||
|
||||
return new_pg
|
||||
|
||||
|
||||
# This implementation refers to lots of Paddle/python/paddle/distributed/collective.py,
|
||||
# Fleet also has a collective helper which uses ops to initialize communication in
|
||||
# Paddle/python/paddle/distributed/fleet/meta_optimizers/common.py. We use the first one
|
||||
# because it seems simple. This should be enhanced to manage the process membership and
|
||||
# the instantiation process in a more general way. In the future, the process group may
|
||||
# handle the communication implementation choice.
|
||||
class ProcessGroup:
|
||||
def __init__(self, group_id, ranks, group_type=None):
|
||||
if group_id == 0 and get_process_group(0) is not None:
|
||||
assert group_id != 0, (
|
||||
"Process group id 0 is reserved for all ranks."
|
||||
)
|
||||
self._group_id = group_id
|
||||
self._ranks = ranks
|
||||
# Add the current ranks into group 0
|
||||
if group_id != 0:
|
||||
global _g_process_group_map
|
||||
_g_process_group_map[0].add_ranks(ranks)
|
||||
self._is_instantiate = False
|
||||
self._group_type = group_type
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._group_id
|
||||
|
||||
@property
|
||||
def ranks(self):
|
||||
return self._ranks
|
||||
|
||||
@property
|
||||
def nranks(self):
|
||||
return len(self._ranks)
|
||||
|
||||
@property
|
||||
def group_type(self):
|
||||
return self._group_type
|
||||
|
||||
def add_ranks(self, new_ranks):
|
||||
if set(new_ranks) <= set(self.ranks):
|
||||
return
|
||||
else:
|
||||
assert not self.is_instantiate(), (
|
||||
"Cannot add new ranks after instantiating the process group"
|
||||
)
|
||||
self._ranks.extend(new_ranks)
|
||||
self._ranks = list(set(self.ranks))
|
||||
|
||||
def local_rank(self, global_rank):
|
||||
if global_rank in self.ranks:
|
||||
return self.ranks.index(global_rank)
|
||||
else:
|
||||
raise AssertionError(
|
||||
f"Rank {global_rank} doesn't belong to this group"
|
||||
)
|
||||
|
||||
def is_instantiate(self):
|
||||
return self._is_instantiate
|
||||
|
||||
@dygraph_guard
|
||||
def instantiate(self):
|
||||
if self._is_instantiate:
|
||||
return
|
||||
ring_id = self.id
|
||||
genv = _get_global_env()
|
||||
global_rank = genv.rank
|
||||
|
||||
if self.nranks >= 2 and global_rank in self.ranks:
|
||||
logger.info(
|
||||
f"group_id: {self.id}, ranks: {self.ranks}, nranks: {self.nranks}, trainer_endpoints: {genv.current_endpoint}"
|
||||
)
|
||||
strategy = core.ParallelStrategy()
|
||||
strategy.nranks = self.nranks
|
||||
strategy.local_rank = self.local_rank(global_rank)
|
||||
strategy.trainer_endpoints = [
|
||||
genv.trainer_endpoints[i] for i in self.ranks
|
||||
]
|
||||
strategy.current_endpoint = genv.current_endpoint
|
||||
strategy.nrings = 1
|
||||
if core.is_compiled_with_cuda():
|
||||
place = core.CUDAPlace(genv.device_id)
|
||||
store = core.create_or_get_global_tcp_store()
|
||||
endpoints_str = ""
|
||||
for endpoint in strategy.trainer_endpoints:
|
||||
endpoints_str += endpoint
|
||||
endpoints_str += f"ring_id:{ring_id}"
|
||||
endpoints_str_hash = hashlib.md5(
|
||||
endpoints_str.encode(encoding='UTF-8')
|
||||
).hexdigest()
|
||||
|
||||
core.CommContextManager.set_device_id(genv.device_id)
|
||||
core.CommContextManager.create_nccl_comm_context(
|
||||
store,
|
||||
str(ring_id),
|
||||
strategy.local_rank,
|
||||
strategy.nranks,
|
||||
endpoints_str_hash,
|
||||
)
|
||||
elif core.is_compiled_with_xpu():
|
||||
place = core.XPUPlace(genv.device_id)
|
||||
store = core.create_or_get_global_tcp_store()
|
||||
endpoints_str = ""
|
||||
for endpoint in strategy.trainer_endpoints:
|
||||
endpoints_str += endpoint
|
||||
endpoints_str += f"ring_id:{ring_id}"
|
||||
endpoints_str_hash = hashlib.md5(
|
||||
endpoints_str.encode(encoding='UTF-8')
|
||||
).hexdigest()
|
||||
|
||||
core.CommContextManager.set_device_id(genv.device_id)
|
||||
core.CommContextManager.create_bkcl_comm_context(
|
||||
store,
|
||||
str(ring_id),
|
||||
strategy.local_rank,
|
||||
strategy.nranks,
|
||||
endpoints_str_hash,
|
||||
)
|
||||
elif genv.device_type in core.get_all_custom_device_type():
|
||||
place = core.CustomPlace(genv.device_type, genv.device_id)
|
||||
core.XCCLParallelContext(strategy, place).init_with_ring_id(
|
||||
ring_id
|
||||
)
|
||||
else:
|
||||
raise AssertionError('No CUDA device found')
|
||||
|
||||
if core.is_compiled_with_cuda():
|
||||
paddle.set_device(
|
||||
f'gpu:{paddle.distributed.ParallelEnv().dev_id}'
|
||||
)
|
||||
elif core.is_compiled_with_xpu():
|
||||
paddle.set_device(
|
||||
f'xpu:{paddle.distributed.ParallelEnv().dev_id}'
|
||||
)
|
||||
elif genv.device_type in core.get_all_custom_device_type():
|
||||
paddle.set_device(
|
||||
f'{paddle.distributed.ParallelEnv().device_type!s}:{paddle.distributed.ParallelEnv().dev_id}'
|
||||
)
|
||||
|
||||
# TODO(shenliang03): This is a temporary solution to solve the problem of
|
||||
# hang caused by cross-creation of new_group
|
||||
barrier_tensor = paddle.full([1], 1, dtype="int32")
|
||||
# barrier is not available in xpu for now
|
||||
if not paddle.framework.core.is_compiled_with_xpu():
|
||||
paddle._legacy_C_ops.barrier(
|
||||
barrier_tensor, barrier_tensor, 'ring_id', ring_id
|
||||
)
|
||||
|
||||
# NOTE(zhiqiu): to avoid send/recv hang in lazy init
|
||||
if self._group_type == 'p2p':
|
||||
alltoall_tmp = paddle.empty(
|
||||
shape=[self.nranks, self.nranks], dtype="int32"
|
||||
)
|
||||
paddle._legacy_C_ops.all_to_all(
|
||||
alltoall_tmp, 'use_calc_stream', True, 'ring_id', ring_id
|
||||
)
|
||||
paddle.device.cuda.synchronize()
|
||||
|
||||
if self.nranks > 1:
|
||||
barrier_tensor = paddle.full([1], 1, dtype="int32")
|
||||
# barrier is not available in xpu for now
|
||||
if not paddle.framework.core.is_compiled_with_xpu():
|
||||
paddle._legacy_C_ops.barrier(
|
||||
barrier_tensor, barrier_tensor, 'ring_id', 0
|
||||
)
|
||||
|
||||
self._is_instantiate = True
|
||||
|
||||
def is_member(self):
|
||||
return True
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, ProcessGroup):
|
||||
return False
|
||||
if self.id != other.id:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __str__(self):
|
||||
string = "id: {}, nranks: {}, ranks: {}.".format(
|
||||
self.id, self.nranks, ", ".join(map(str, self.ranks))
|
||||
)
|
||||
return string
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.__str__())
|
||||
|
||||
|
||||
# Note that Process group 0 is reserved for representing all ranks.
|
||||
# At the beginning, group 0 is empty and new ranks will be added automatically.
|
||||
_g_process_group_map = OrderedDict()
|
||||
_g_process_group_map[0] = ProcessGroup(0, [])
|
||||
@@ -0,0 +1,145 @@
|
||||
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import numpy as np
|
||||
|
||||
from paddle.framework import core
|
||||
|
||||
|
||||
class ProcessMesh(core.ProcessMesh):
|
||||
r"""
|
||||
The class `ProcessMesh` describes the topology of logical processes.
|
||||
|
||||
Args:
|
||||
mesh (list|numpy.array): an N-dimensional array describes the topology
|
||||
of logical processes.
|
||||
dim_names (list, optional): the i-th element of this list gives the name of the
|
||||
i-th dimension.
|
||||
|
||||
Returns:
|
||||
None
|
||||
|
||||
Examples:
|
||||
.. code-block:: pycon
|
||||
|
||||
>>> import paddle
|
||||
>>> import paddle.distributed as dist
|
||||
>>> paddle.enable_static()
|
||||
|
||||
>>> mesh = dist.ProcessMesh([[2, 4, 5], [0, 1, 3]])
|
||||
>>> assert mesh.shape == [2, 3]
|
||||
>>> assert mesh.process_ids == [2, 4, 5, 0, 1, 3]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, mesh, dim_names=None):
|
||||
if not isinstance(mesh, list) and not isinstance(mesh, np.ndarray):
|
||||
raise ValueError(
|
||||
'The mesh must be an instance of list or np.ndarray.'
|
||||
)
|
||||
if isinstance(mesh, list):
|
||||
mesh = np.array(mesh)
|
||||
|
||||
self._mesh = mesh
|
||||
|
||||
self._shape = list(self._mesh.shape)
|
||||
|
||||
self._process_ids = self._mesh.flatten().tolist()
|
||||
if not all(isinstance(p, int) for p in self._process_ids):
|
||||
raise ValueError("All elements of the mesh must be integer")
|
||||
|
||||
if min(self._process_ids) < 0:
|
||||
raise ValueError('All elements of the mesh must be >= 0.')
|
||||
|
||||
unique_process_ids = set(self._process_ids)
|
||||
if len(unique_process_ids) != len(self._process_ids):
|
||||
raise ValueError('All elements of the mesh must be unique.')
|
||||
|
||||
if dim_names is not None:
|
||||
assert len(dim_names) == len(self._shape), (
|
||||
"The length of dims_names must be same as the shape of the mesh."
|
||||
)
|
||||
self._dim_names = dim_names
|
||||
else:
|
||||
self._dim_names = ["d" + str(i) for i in range(len(self._shape))]
|
||||
|
||||
# Follow the requirement for using pybind11
|
||||
core.ProcessMesh.__init__(
|
||||
self, self._shape, self._process_ids, self._dim_names
|
||||
)
|
||||
|
||||
@property
|
||||
def mesh(self):
|
||||
return self._mesh
|
||||
|
||||
|
||||
def compute_compatible_process_mesh(process_meshes):
|
||||
"""Compute the compatible process mesh given a list of process meshes."""
|
||||
if not process_meshes:
|
||||
return None
|
||||
|
||||
def _compute_compatible_of_two_process_meshes(pm1, pm2):
|
||||
if pm1 is None:
|
||||
return True, pm2
|
||||
if pm2 is None:
|
||||
return True, pm1
|
||||
if pm1 == pm2:
|
||||
return True, pm1
|
||||
if pm1.process_ids == pm2.process_ids:
|
||||
if len(pm1.shape) >= len(pm2.shape):
|
||||
return True, pm1
|
||||
else:
|
||||
return True, pm2
|
||||
process_set1 = set(pm1.process_ids)
|
||||
process_set2 = set(pm2.process_ids)
|
||||
if process_set1.issubset(process_set2):
|
||||
return True, pm2
|
||||
if process_set2.issubset(process_set1):
|
||||
return True, pm1
|
||||
return False, None
|
||||
|
||||
compatible_result = None
|
||||
for process_mesh in process_meshes:
|
||||
(
|
||||
compatible,
|
||||
compatible_result,
|
||||
) = _compute_compatible_of_two_process_meshes(
|
||||
compatible_result, process_mesh
|
||||
)
|
||||
if not compatible:
|
||||
return None
|
||||
if compatible_result.empty():
|
||||
return None
|
||||
if isinstance(compatible_result, core.ProcessMesh):
|
||||
mesh = np.array(compatible_result.process_ids).reshape(
|
||||
compatible_result.shape
|
||||
)
|
||||
return ProcessMesh(mesh, compatible_result.dim_names)
|
||||
elif isinstance(compatible_result, ProcessMesh):
|
||||
return ProcessMesh(compatible_result.mesh, compatible_result.dim_names)
|
||||
else:
|
||||
raise ValueError("Unrecognized ProcessMesh.")
|
||||
|
||||
|
||||
def merge_process_mesh(process_meshes):
|
||||
"""Merge a list of process meshes."""
|
||||
merged_process_mesh = None
|
||||
merged_process_ids = set()
|
||||
for process_mesh in process_meshes:
|
||||
if process_mesh is not None:
|
||||
process_ids = set(process_mesh.process_ids)
|
||||
merged_process_ids = merged_process_ids.union(process_ids)
|
||||
if len(merged_process_ids) != 0:
|
||||
merged_process_mesh = ProcessMesh(list(merged_process_ids))
|
||||
return merged_process_mesh
|
||||
@@ -0,0 +1,258 @@
|
||||
# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
from argparse import ArgumentParser
|
||||
|
||||
import paddle
|
||||
from paddle.base.log_helper import get_logger
|
||||
|
||||
_logger = get_logger(
|
||||
__name__, logging.INFO, fmt='%(asctime)s-%(levelname)s: %(message)s'
|
||||
)
|
||||
|
||||
|
||||
color_map = {
|
||||
"forward": "thread_state_running", # RGB: 126, 200, 148
|
||||
"backward": "rail_idle", # RGB: 238, 142, 0
|
||||
"optimizer": "rail_response", # RGB: 238, 142, 0
|
||||
"default": "thread_state_unknown", # RGB: 199, 155, 125
|
||||
}
|
||||
|
||||
ignore_job_type = ["recv_forward", "send_backward"]
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = ArgumentParser()
|
||||
device_count = paddle.device.cuda.device_count()
|
||||
all_devices = ",".join([str(i) for i in range(device_count)])
|
||||
parser.add_argument("--devices", type=str, default=all_devices)
|
||||
parser.add_argument("--log_dir", type=str, required=True)
|
||||
parser.add_argument("--multi_machine", action="store_true")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def process_job_log(log_data, device_id, multi_machine_idx=-1):
|
||||
log_pattern = r'.*?Profiler Info: Job \((\d+)\), type = (\w+), micro_batch_id = (\d+), job_start_time = (\d+.\d+), job_end_time = (\d+.\d+)'
|
||||
matches = re.findall(log_pattern, log_data)
|
||||
events = []
|
||||
last_end_time = None
|
||||
|
||||
step_times = []
|
||||
step_start_time = 0
|
||||
step_end_time = 0
|
||||
|
||||
start_job_type = ""
|
||||
|
||||
for i, match in enumerate(matches):
|
||||
job_id, job_type, micro_batch_id, job_start_time, job_end_time = match
|
||||
|
||||
if job_type in ignore_job_type:
|
||||
continue
|
||||
|
||||
if job_type != "default" and start_job_type == "":
|
||||
start_job_type = job_type
|
||||
|
||||
start_time = float(job_start_time.strip()) * 1000
|
||||
end_time = float(job_end_time.strip()) * 1000
|
||||
|
||||
is_start_time_recorded = 0
|
||||
|
||||
if job_type == start_job_type and micro_batch_id == "0":
|
||||
if step_start_time != 0:
|
||||
step_times.append([step_start_time, step_end_time])
|
||||
step_start_time = start_time
|
||||
|
||||
step_end_time = end_time
|
||||
|
||||
tid_name = (
|
||||
"GPU" + str(device_id)
|
||||
if multi_machine_idx == -1
|
||||
else "GPU"
|
||||
+ str(device_id)
|
||||
+ "(machine:"
|
||||
+ str(multi_machine_idx)
|
||||
+ ")"
|
||||
)
|
||||
event_start = {
|
||||
"name": job_type + "_" + str(job_id),
|
||||
"cat": job_type,
|
||||
"ph": "B",
|
||||
"ts": start_time,
|
||||
"pid": 0,
|
||||
"tid": tid_name,
|
||||
}
|
||||
event_end = {
|
||||
"name": job_type + "_" + str(job_id),
|
||||
"cat": job_type,
|
||||
"ph": "E",
|
||||
"pid": 0,
|
||||
"ts": end_time,
|
||||
"tid": tid_name,
|
||||
}
|
||||
if job_type in color_map:
|
||||
event_start["cname"] = color_map[job_type]
|
||||
event_end["cname"] = color_map[job_type]
|
||||
|
||||
events.append(event_start)
|
||||
events.append(event_end)
|
||||
|
||||
last_end_time = end_time
|
||||
|
||||
step_times.append([step_start_time, step_end_time])
|
||||
return events, step_times
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
all_events = []
|
||||
step_infos = []
|
||||
start_step = 0
|
||||
machine_num = 1
|
||||
|
||||
def process_one_machine_log(log_dir, multi_machine_idx=-1):
|
||||
for device_id in args.devices.split(","):
|
||||
_logger.info(f"Process device {device_id}")
|
||||
device_id = int(device_id)
|
||||
log_file = os.path.join(log_dir, "workerlog." + str(device_id))
|
||||
with open(log_file, "r") as f:
|
||||
log_data = f.read()
|
||||
|
||||
start_step_pattern = (
|
||||
r'.*?Schedule Profiler start at step (\d+) and end at step.*'
|
||||
)
|
||||
start_step_match = re.findall(start_step_pattern, log_data)
|
||||
start_step = (
|
||||
int(start_step_match[0]) if len(start_step_match) > 0 else 0
|
||||
)
|
||||
|
||||
events, step_times = process_job_log(
|
||||
log_data, device_id, multi_machine_idx
|
||||
)
|
||||
all_events.extend(events)
|
||||
for i, info in enumerate(step_times):
|
||||
if len(step_infos) <= i:
|
||||
step_infos.append([float("inf"), float("-inf")])
|
||||
step_infos[i][0] = min(step_infos[i][0], info[0])
|
||||
step_infos[i][1] = max(step_infos[i][1], info[1])
|
||||
return start_step
|
||||
|
||||
if args.multi_machine:
|
||||
multi_machine_dirs = os.listdir(args.log_dir)
|
||||
multi_machine_dirs = [
|
||||
os.path.join(args.log_dir, d)
|
||||
for d in multi_machine_dirs
|
||||
if d.startswith("machine")
|
||||
and os.path.isdir(os.path.join(args.log_dir, d))
|
||||
]
|
||||
machine_num = len(multi_machine_dirs)
|
||||
for i, d in enumerate(multi_machine_dirs):
|
||||
_logger.info(f"Process machine {i}")
|
||||
start_step = max(process_one_machine_log(d, i), start_step)
|
||||
else:
|
||||
start_step = process_one_machine_log(args.log_dir)
|
||||
|
||||
for i, info in enumerate(step_infos):
|
||||
start_time = info[0]
|
||||
if i > 0:
|
||||
start_time = max(start_time, step_infos[i - 1][1])
|
||||
event_start = {
|
||||
"name": "step" + str(i + start_step),
|
||||
"cat": "step",
|
||||
"ph": "B",
|
||||
"ts": start_time,
|
||||
"pid": 0,
|
||||
"tid": "Step",
|
||||
"cname": color_map["default"],
|
||||
}
|
||||
event_end = {
|
||||
"name": "step" + str(i + start_step),
|
||||
"cat": "step",
|
||||
"ph": "E",
|
||||
"ts": info[1],
|
||||
"pid": 0,
|
||||
"tid": "Step",
|
||||
"cname": color_map["default"],
|
||||
}
|
||||
|
||||
all_events.append(event_start)
|
||||
all_events.append(event_end)
|
||||
|
||||
save_path = os.path.join(args.log_dir, "pipeline_profile.json")
|
||||
with open(save_path, "w") as f:
|
||||
f.write(json.dumps({"traceEvents": all_events}))
|
||||
_logger.info(f"Save pipeline profile to {save_path}")
|
||||
|
||||
# support Perfetto format
|
||||
save_path = os.path.join(args.log_dir, "pipeline_profile_perfetto.json")
|
||||
all_events.extend(
|
||||
[
|
||||
{
|
||||
"args": {"name": "STEP"},
|
||||
"cat": "__metadata",
|
||||
"name": "thread_name",
|
||||
"ph": "M",
|
||||
"pid": 0,
|
||||
"tid": 2333,
|
||||
"ts": 0,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
for i in range(machine_num):
|
||||
for j in range(len(args.devices.split(","))):
|
||||
if machine_num > 1:
|
||||
name = f"GPU:{j}(machine:{i})"
|
||||
tid = i * len(args.devices.split(",")) + j + 2334
|
||||
else:
|
||||
name = f"GPU:{j}"
|
||||
tid = j + 2334
|
||||
all_events.extend(
|
||||
[
|
||||
{
|
||||
"args": {"name": name},
|
||||
"cat": "__metadata",
|
||||
"name": "thread_name",
|
||||
"ph": "M",
|
||||
"pid": 0,
|
||||
"tid": tid,
|
||||
"ts": 0,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
json_str = json.dumps({"traceEvents": all_events})
|
||||
json_str = json_str.replace('"Step"', '2333')
|
||||
|
||||
for i in range(machine_num):
|
||||
for j in range(len(args.devices.split(","))):
|
||||
if machine_num > 1:
|
||||
json_str = json_str.replace(
|
||||
f'"GPU{j}(machine:{i})"',
|
||||
f'{i * len(args.devices.split(",")) + j + 2334}',
|
||||
)
|
||||
else:
|
||||
json_str = json_str.replace(f'"GPU{j}"', f'{j + 2334}')
|
||||
|
||||
with open(save_path, "w") as f:
|
||||
f.write(json_str)
|
||||
_logger.info(f"Save pipeline profile to {save_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import paddle
|
||||
|
||||
# all registered reshard functions
|
||||
_g_reshard_func_list = []
|
||||
|
||||
|
||||
class ReshardFunction:
|
||||
def is_suitable(self, dist_tensor, dist_attr):
|
||||
raise NotImplementedError
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
def choose_reshard_func(src_dist_attr, dst_dist_attr):
|
||||
global _g_reshard_func_list
|
||||
for reshard_func in _g_reshard_func_list:
|
||||
if reshard_func.is_suitable(src_dist_attr, dst_dist_attr):
|
||||
return reshard_func
|
||||
return None
|
||||
|
||||
|
||||
def register_reshard_func(reshard_func):
|
||||
global _g_reshard_func_list
|
||||
_g_reshard_func_list.append(reshard_func)
|
||||
|
||||
|
||||
def clean_reshard_funcs():
|
||||
global _g_reshard_func_list
|
||||
_g_reshard_func_list.clear()
|
||||
|
||||
|
||||
def is_shard(dist_attr):
|
||||
for v in dist_attr.dims_mapping:
|
||||
if v != -1:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_partial(dist_attr):
|
||||
if len(dist_attr.partial_status) > 0:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_replicated(dist_attr):
|
||||
dims_mapping_set = set(dist_attr.dims_mapping)
|
||||
if len(dist_attr.partial_status) == 0 and (
|
||||
len(dims_mapping_set) == 0
|
||||
or (len(dims_mapping_set) == 1 and -1 in dims_mapping_set)
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def copy_dist_attr_with_new_member(
|
||||
src_dist_attr,
|
||||
new_process_mesh=None,
|
||||
new_dims_mapping=None,
|
||||
new_partial_status=None,
|
||||
):
|
||||
if new_process_mesh is None:
|
||||
new_process_mesh = src_dist_attr.process_mesh
|
||||
if new_dims_mapping is None:
|
||||
new_dims_mapping = src_dist_attr.dims_mapping
|
||||
if new_partial_status is None:
|
||||
new_partial_status = src_dist_attr.partial_status
|
||||
|
||||
return paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
new_process_mesh,
|
||||
new_dims_mapping,
|
||||
new_partial_status,
|
||||
)
|
||||
|
||||
|
||||
def copy_op_attr_with_new_member(
|
||||
src_dist_attr,
|
||||
new_process_mesh=None,
|
||||
new_operands=None,
|
||||
new_results=None,
|
||||
new_chunk_id=None,
|
||||
):
|
||||
if new_process_mesh is None:
|
||||
new_process_mesh = src_dist_attr.process_mesh
|
||||
if new_operands is None:
|
||||
new_operands = src_dist_attr.operands()
|
||||
if new_results is None:
|
||||
new_results = src_dist_attr.results()
|
||||
if new_chunk_id is None:
|
||||
new_chunk_id = src_dist_attr.chunk_id
|
||||
|
||||
return paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
new_process_mesh,
|
||||
new_operands,
|
||||
new_results,
|
||||
new_chunk_id,
|
||||
)
|
||||
|
||||
|
||||
def copy_process_mesh_with_new_member(
|
||||
src_process_mesh,
|
||||
new_shape=None,
|
||||
new_process_ids=None,
|
||||
new_dim_names=None,
|
||||
):
|
||||
if new_shape is None:
|
||||
new_shape = src_process_mesh.shape
|
||||
if new_process_ids is None:
|
||||
new_process_ids = src_process_mesh.process_ids
|
||||
if new_dim_names is None:
|
||||
new_dim_names = src_process_mesh.dim_names
|
||||
|
||||
return paddle.base.libpaddle.pir.create_process_mesh(
|
||||
new_shape,
|
||||
new_process_ids,
|
||||
new_dim_names,
|
||||
)
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import paddle
|
||||
|
||||
from .base_reshard_func import (
|
||||
ReshardFunction,
|
||||
is_replicated,
|
||||
)
|
||||
from .nd_mesh_reshard_func import NdMeshReshardFunction
|
||||
|
||||
|
||||
class GlobalToSubMeshFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
# NOTE we could allow the src_dist_attr is not replicated and reshard it as replicated before go through the global_to_sub logic
|
||||
# but the dst_dist_attr should be replicated otherwise there will be un-defined result when change the mesh.
|
||||
if not is_replicated(dst_dist_attr):
|
||||
return False
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
if in_mesh.ndim > out_mesh.ndim + 1:
|
||||
return False
|
||||
if in_mesh.ndim == out_mesh.ndim:
|
||||
return set(out_mesh.process_ids) < set(in_mesh.process_ids)
|
||||
else:
|
||||
sub_meshes = paddle.base.libpaddle.pir.get_sub_meshes(in_mesh)
|
||||
return out_mesh in sub_meshes
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
# reshard operand as replicated before change the mesh.
|
||||
if not is_replicated(src_dist_attr):
|
||||
tmp_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
src_dist_attr.process_mesh,
|
||||
[-1] * len(src_dist_attr.dims_mapping),
|
||||
{},
|
||||
)
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dist_attr
|
||||
)
|
||||
|
||||
pre_reshard_func = NdMeshReshardFunction()
|
||||
src_value = pre_reshard_func.reshard(
|
||||
src_dist_attr,
|
||||
tmp_dist_attr,
|
||||
src_value,
|
||||
tmp_dst_type,
|
||||
)
|
||||
src_dist_attr = tmp_dist_attr
|
||||
|
||||
if src_value.has_one_use():
|
||||
src_value.update_dist_attr(dst_dist_attr)
|
||||
prev_op = src_value.get_defining_op()
|
||||
op_dist_attr = prev_op.dist_attr
|
||||
op_mesh = op_dist_attr.process_mesh
|
||||
operands = op_dist_attr.operands()
|
||||
results = op_dist_attr.results()
|
||||
chunk_id = op_dist_attr.chunk_id
|
||||
results[src_value.index()] = dst_dist_attr
|
||||
prev_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
op_mesh, operands, results, chunk_id
|
||||
)
|
||||
)
|
||||
return src_value
|
||||
else:
|
||||
dst_value = paddle._C_ops.share_data_(src_value)
|
||||
share_data_op = dst_value.get_defining_op()
|
||||
# set dist type and dist attr
|
||||
dst_value.set_type(dst_type)
|
||||
chunk_id = -1
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = src_value.get_defining_op().dist_attr.chunk_id
|
||||
share_data_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_dist_attr.process_mesh,
|
||||
[src_dist_attr],
|
||||
[dst_dist_attr],
|
||||
chunk_id,
|
||||
)
|
||||
)
|
||||
return dst_value
|
||||
@@ -0,0 +1,365 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.distributed.auto_parallel.static.utils import split_mesh
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from .base_reshard_func import (
|
||||
ReshardFunction,
|
||||
copy_dist_attr_with_new_member,
|
||||
is_partial,
|
||||
)
|
||||
from .p_to_r_reshard_func import PToRReshardFunction
|
||||
from .p_to_s_reshard_func import PToSReshardFunction
|
||||
from .r_to_p_reshard_func import RToPReshardFunction
|
||||
from .r_to_s_reshard_func import RToSReshardFunction
|
||||
from .s_to_r_reshard_func import SToRReshardFunction
|
||||
from .same_status_reshard_func import SameStatusReshardFunction
|
||||
|
||||
|
||||
def find_first_diff_shard_axis(src_dist_attr, dst_dist_attr):
|
||||
src_dims_mapping = src_dist_attr.dims_mapping
|
||||
dst_dims_mapping = dst_dist_attr.dims_mapping
|
||||
ndim = len(src_dims_mapping)
|
||||
for i in range(ndim - 1, -1, -1):
|
||||
if src_dims_mapping[i] != dst_dims_mapping[i]:
|
||||
return i
|
||||
return -1
|
||||
|
||||
|
||||
def get_1D_sub_process_mesh(process_mesh, mesh_dim):
|
||||
"""
|
||||
Get the 1-D sub process mesh on specific mesh_dim which:
|
||||
1) where the reshard should be performed.
|
||||
2) contains current process.
|
||||
|
||||
Args:
|
||||
process_mesh (ProcessMesh): the global process mesh.
|
||||
mesh_dim (int): the mesh dimension where the dist_tensor is
|
||||
sharded or partial.
|
||||
|
||||
e.g.
|
||||
1) process_mesh = [[0, 1, 2], [3, 4, 5]], axis = 0:
|
||||
process rank id returned sub mesh
|
||||
0 or 3 [0, 3]
|
||||
1 or 4 [1, 4]
|
||||
2 or 5 [2, 5]
|
||||
2) process_mesh = [[0, 1, 2], [3, 4, 5]], axis = 1:
|
||||
process rank id returned sub mesh
|
||||
0 or 1 or 2 [0, 1, 2]
|
||||
3 or 4 or 5 [3, 4, 5]
|
||||
"""
|
||||
import numpy as np
|
||||
|
||||
mesh_shape = process_mesh.shape
|
||||
dim_names = process_mesh.dim_names
|
||||
process_ids = np.array(process_mesh.process_ids).reshape(mesh_shape)
|
||||
|
||||
rank_id = dist.get_rank()
|
||||
# FIXME (JZ-LIANG) Remove this hack to support any op mesh group for Pipeline Parallelism
|
||||
if rank_id not in process_mesh.process_ids:
|
||||
rank_id = process_mesh.process_ids[0]
|
||||
coord = list(np.where(process_ids == rank_id))
|
||||
coord[mesh_dim] = range(mesh_shape[mesh_dim])
|
||||
sub_process_ids = process_ids[tuple(coord)].flatten()
|
||||
sub_mesh_name = dim_names[mesh_dim]
|
||||
|
||||
return dist.ProcessMesh(sub_process_ids, [sub_mesh_name])
|
||||
|
||||
|
||||
class NdMeshReshardFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh != out_mesh:
|
||||
return False
|
||||
if out_mesh.ndim <= 1:
|
||||
return False
|
||||
|
||||
# check dims_mapping and partial_status
|
||||
if src_dist_attr == dst_dist_attr:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
"""
|
||||
Reshard on N-d mesh:
|
||||
1. Find the tensor dimensions where the dims_mapping values
|
||||
differ between src_dist_attr and dst_dist_attr.
|
||||
2. From higher to lower, convert the non-replicated dimensions
|
||||
in step1 to replicated using corresponding 1-D mesh functions.
|
||||
3. Convert the replicated dimensions in step2 to the status in
|
||||
dst_dist_attr with corresponding 1-D mesh functions.
|
||||
"""
|
||||
# Step1. find first dimension with different shard status in src_dist_attr
|
||||
# and dst_dist_attr.
|
||||
first_diff_axis = find_first_diff_shard_axis(
|
||||
src_dist_attr, dst_dist_attr
|
||||
)
|
||||
# out_value = src_value # intermediate result
|
||||
# src_type = src_value.type()
|
||||
tensor_ndim = len(src_value.shape)
|
||||
process_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
# Step2. Convert the non-replicated dimensions to replicated.
|
||||
|
||||
# Step2.1 convert shard status to replicated
|
||||
for i in range(first_diff_axis, -1, -1):
|
||||
in_mesh_axis = src_dist_attr.dims_mapping[i]
|
||||
out_mesh_axis = dst_dist_attr.dims_mapping[i]
|
||||
if in_mesh_axis == -1 or in_mesh_axis == out_mesh_axis:
|
||||
continue
|
||||
|
||||
# calculate the dist_attr after converting
|
||||
tmp_dims_mapping = src_dist_attr.dims_mapping
|
||||
tmp_dims_mapping[i] = -1
|
||||
tmp_dst_dist_attr = copy_dist_attr_with_new_member(
|
||||
src_dist_attr, new_dims_mapping=tmp_dims_mapping
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dst_dist_attr
|
||||
)
|
||||
sub_mesh_list = split_mesh(process_mesh, in_mesh_axis)
|
||||
for sub_mesh in sub_mesh_list:
|
||||
new_process_group(sorted(sub_mesh.process_ids))
|
||||
# get the process_mesh on specific axis
|
||||
sub_mesh = get_1D_sub_process_mesh(process_mesh, in_mesh_axis)
|
||||
|
||||
# calculate corresponding 1-D dist_attr of src_dst_attr
|
||||
in_one_dim_dims_mapping = [-1] * tensor_ndim
|
||||
in_one_dim_dims_mapping[i] = 0
|
||||
in_one_dim_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
sub_mesh, in_one_dim_dims_mapping, {}
|
||||
)
|
||||
)
|
||||
|
||||
# calculate corresponding 1-D dist_attr of dst_dst_attr
|
||||
out_one_dim_dims_mapping = [-1] * tensor_ndim
|
||||
out_one_dim_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
sub_mesh, out_one_dim_dims_mapping, {}
|
||||
)
|
||||
)
|
||||
|
||||
one_dim_func = SToRReshardFunction()
|
||||
src_value = one_dim_func.reshard(
|
||||
in_one_dim_dist_attr,
|
||||
out_one_dim_dist_attr,
|
||||
src_value,
|
||||
tmp_dst_type,
|
||||
)
|
||||
src_dist_attr = tmp_dst_dist_attr
|
||||
|
||||
# Step2.2. convert partial status to replicated
|
||||
if is_partial(src_dist_attr):
|
||||
in_partial_status = src_dist_attr.partial_status
|
||||
out_partial_status = dst_dist_attr.partial_status # read-only
|
||||
# convert each partial dim to replicated with corresponding
|
||||
# 1-D mesh function
|
||||
for partial_dim, partial_type in in_partial_status.items():
|
||||
if partial_dim in out_partial_status:
|
||||
if out_partial_status[partial_dim] != partial_type:
|
||||
raise NotImplementedError(
|
||||
f"Reshard tensor from one partial type {partial_type} to another partial type {out_partial_status[partial_dim]} is not supported yet."
|
||||
)
|
||||
continue
|
||||
|
||||
p_to_s = False
|
||||
if partial_dim in dst_dist_attr.dims_mapping:
|
||||
p_to_s = True
|
||||
shard_index = dst_dist_attr.dims_mapping.index(partial_dim)
|
||||
# get the partial status after converting
|
||||
tmp_partial_status = src_dist_attr.partial_status
|
||||
tmp_partial_status.pop(partial_dim)
|
||||
|
||||
tmp_dims_mapping = src_dist_attr.dims_mapping
|
||||
if p_to_s:
|
||||
tmp_dims_mapping[shard_index] = partial_dim
|
||||
|
||||
tmp_dst_dist_attr = copy_dist_attr_with_new_member(
|
||||
src_dist_attr,
|
||||
new_dims_mapping=tmp_dims_mapping,
|
||||
new_partial_status=tmp_partial_status,
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dst_dist_attr
|
||||
)
|
||||
sub_mesh_list = split_mesh(process_mesh, partial_dim)
|
||||
for sub_mesh in sub_mesh_list:
|
||||
new_process_group(sorted(sub_mesh.process_ids))
|
||||
# get the process_mesh on specific axis
|
||||
sub_mesh = get_1D_sub_process_mesh(process_mesh, partial_dim)
|
||||
|
||||
# calculate corresponding 1-D dist_attr of src_dst_attr
|
||||
in_one_dim_partial_status = {0: partial_type}
|
||||
in_one_dim_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
sub_mesh,
|
||||
[-1] * tensor_ndim,
|
||||
in_one_dim_partial_status,
|
||||
)
|
||||
)
|
||||
out_one_dim_dims_mapping = [-1] * tensor_ndim
|
||||
one_dim_func = PToRReshardFunction()
|
||||
if p_to_s:
|
||||
out_one_dim_dims_mapping[shard_index] = 0
|
||||
one_dim_func = PToSReshardFunction()
|
||||
|
||||
# calculate corresponding 1-D dist_attr of dst_dst_attr
|
||||
out_one_dim_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
sub_mesh,
|
||||
out_one_dim_dims_mapping,
|
||||
{},
|
||||
)
|
||||
)
|
||||
|
||||
src_value = one_dim_func.reshard(
|
||||
in_one_dim_dist_attr,
|
||||
out_one_dim_dist_attr,
|
||||
src_value,
|
||||
tmp_dst_type,
|
||||
)
|
||||
src_dist_attr = tmp_dst_dist_attr
|
||||
|
||||
# Step3. Convert the replicated status to the status in dst_dist_attr
|
||||
# Step3.1 convert replicated to partial
|
||||
if is_partial(dst_dist_attr):
|
||||
in_partial_status = src_dist_attr.partial_status
|
||||
out_partial_status = dst_dist_attr.partial_status
|
||||
for partial_dim, partial_type in out_partial_status.items():
|
||||
if partial_dim in in_partial_status:
|
||||
continue
|
||||
|
||||
sub_mesh = get_1D_sub_process_mesh(process_mesh, partial_dim)
|
||||
|
||||
in_one_dim_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
sub_mesh,
|
||||
[-1] * tensor_ndim,
|
||||
{},
|
||||
)
|
||||
)
|
||||
out_one_dim_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
sub_mesh, [-1] * tensor_ndim, {0: partial_type}
|
||||
)
|
||||
)
|
||||
|
||||
tmp_dst_dist_attr = copy_dist_attr_with_new_member(
|
||||
dst_dist_attr,
|
||||
new_partial_status={partial_dim: partial_type},
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dst_dist_attr
|
||||
)
|
||||
|
||||
src_value = RToPReshardFunction().reshard(
|
||||
in_one_dim_dist_attr,
|
||||
out_one_dim_dist_attr,
|
||||
src_value,
|
||||
tmp_dst_type,
|
||||
)
|
||||
src_dist_attr = tmp_dst_dist_attr
|
||||
|
||||
# Step3.2 convert replicated to shard
|
||||
for i in range(first_diff_axis, -1, -1):
|
||||
in_mesh_axis = src_dist_attr.dims_mapping[i]
|
||||
out_mesh_axis = dst_dist_attr.dims_mapping[i]
|
||||
if in_mesh_axis == out_mesh_axis:
|
||||
continue
|
||||
|
||||
# calculate the dist_attr after converting
|
||||
tmp_dims_mapping = src_dist_attr.dims_mapping
|
||||
tmp_dims_mapping[i] = out_mesh_axis
|
||||
tmp_dst_dist_attr = copy_dist_attr_with_new_member(
|
||||
src_dist_attr, new_dims_mapping=tmp_dims_mapping
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dst_dist_attr
|
||||
)
|
||||
|
||||
# get the process_mesh on specific axis
|
||||
sub_mesh = get_1D_sub_process_mesh(process_mesh, out_mesh_axis)
|
||||
|
||||
# calculate the corresponding 1-D input dist attr
|
||||
in_one_dim_dims_mapping = [-1] * tensor_ndim
|
||||
in_one_dim_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
sub_mesh, in_one_dim_dims_mapping, {}
|
||||
)
|
||||
)
|
||||
|
||||
# calculate the corresponding 1-D output dist attr
|
||||
out_one_dim_dims_mapping = [-1] * tensor_ndim
|
||||
out_one_dim_dims_mapping[i] = 0
|
||||
out_one_dim_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
sub_mesh, out_one_dim_dims_mapping, {}
|
||||
)
|
||||
)
|
||||
one_dim_func = RToSReshardFunction()
|
||||
src_value = one_dim_func.reshard(
|
||||
in_one_dim_dist_attr,
|
||||
out_one_dim_dist_attr,
|
||||
src_value,
|
||||
tmp_dst_type,
|
||||
)
|
||||
src_dist_attr = tmp_dst_dist_attr
|
||||
return src_value
|
||||
|
||||
|
||||
class NdMeshReshardFunctionCrossMesh(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh == out_mesh:
|
||||
return False
|
||||
if in_mesh.shape != out_mesh.shape:
|
||||
return False
|
||||
if out_mesh.ndim <= 1:
|
||||
return False
|
||||
if src_dist_attr == dst_dist_attr:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
same_status_func = SameStatusReshardFunction()
|
||||
tmp_dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
dst_dist_attr.process_mesh,
|
||||
src_dist_attr.dims_mapping,
|
||||
src_dist_attr.partial_status,
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dist_attr
|
||||
)
|
||||
src_value = same_status_func.reshard(
|
||||
src_dist_attr, tmp_dist_attr, src_value, tmp_dst_type
|
||||
)
|
||||
|
||||
nd_mesh_func = NdMeshReshardFunction()
|
||||
assert nd_mesh_func.is_suitable(tmp_dist_attr, dst_dist_attr), (
|
||||
f"Invoke the p to r reshard function is not valid from {tmp_dist_attr} to {dst_dist_attr}"
|
||||
)
|
||||
return nd_mesh_func.reshard(
|
||||
tmp_dist_attr, dst_dist_attr, src_value, dst_type
|
||||
)
|
||||
@@ -0,0 +1,113 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import paddle
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from .base_reshard_func import ReshardFunction, is_partial, is_replicated
|
||||
from .same_status_reshard_func import SameStatusReshardFunction
|
||||
|
||||
|
||||
class PToRReshardFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_partial(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_replicated(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh.ndim != 1:
|
||||
return False
|
||||
if out_mesh.ndim != 1:
|
||||
return False
|
||||
if in_mesh != out_mesh:
|
||||
return False
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
src_mesh = src_dist_attr.process_mesh
|
||||
src_reduce_type = src_dist_attr.partial_status[0]
|
||||
# reduce_mean = False
|
||||
# if src_reduce_type == paddle.base.core.ReduceType.kRedAvg:
|
||||
# src_reduce_type = paddle.base.core.ReduceType.kRedSum
|
||||
# reduce_mean = True
|
||||
|
||||
group = new_process_group(sorted(src_mesh.process_ids))
|
||||
reduced_value = paddle._C_ops.all_reduce(
|
||||
src_value, group.id, int(src_reduce_type)
|
||||
)
|
||||
# set dist type and dist attr
|
||||
reduced_value.set_type(dst_type)
|
||||
chunk_id = -1
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = src_value.get_defining_op().dist_attr.chunk_id
|
||||
|
||||
reduced_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_mesh,
|
||||
[src_dist_attr],
|
||||
[dst_dist_attr],
|
||||
chunk_id,
|
||||
)
|
||||
)
|
||||
return reduced_value
|
||||
|
||||
|
||||
class PToRReshardFunctionCrossMesh(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_partial(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_replicated(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if (
|
||||
in_mesh.ndim != 1
|
||||
or out_mesh.ndim != 1
|
||||
or in_mesh.shape != out_mesh.shape
|
||||
):
|
||||
return False
|
||||
|
||||
if in_mesh == out_mesh:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
same_status_func = SameStatusReshardFunction()
|
||||
tmp_dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
dst_dist_attr.process_mesh,
|
||||
src_dist_attr.dims_mapping,
|
||||
src_dist_attr.partial_status,
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dist_attr
|
||||
)
|
||||
src_value = same_status_func.reshard(
|
||||
src_dist_attr, tmp_dist_attr, src_value, tmp_dst_type
|
||||
)
|
||||
|
||||
p_to_r_func = PToRReshardFunction()
|
||||
assert p_to_r_func.is_suitable(tmp_dist_attr, dst_dist_attr), (
|
||||
f"Invoke the p to r reshard function is not valid from {tmp_dist_attr} to {dst_dist_attr}"
|
||||
)
|
||||
return p_to_r_func.reshard(
|
||||
tmp_dist_attr, dst_dist_attr, src_value, dst_type
|
||||
)
|
||||
@@ -0,0 +1,245 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.distributed.utils.stream_utils import ExecutionStreamType
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from .base_reshard_func import (
|
||||
ReshardFunction,
|
||||
copy_dist_attr_with_new_member,
|
||||
is_partial,
|
||||
is_shard,
|
||||
)
|
||||
|
||||
|
||||
class PToSReshardFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_partial(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_shard(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh.ndim != 1:
|
||||
return False
|
||||
if out_mesh.ndim != 1:
|
||||
return False
|
||||
if in_mesh != out_mesh:
|
||||
return False
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
src_mesh = src_dist_attr.process_mesh
|
||||
src_reduce_type = src_dist_attr.partial_status[0]
|
||||
assert src_reduce_type == paddle.base.core.ReduceType.kRedSum, (
|
||||
f"The p to s reshard func only support sum op, but received {src_reduce_type}"
|
||||
)
|
||||
|
||||
chunk_id = -1
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = src_value.get_defining_op().dist_attr.chunk_id
|
||||
|
||||
split_axis = dst_dist_attr.dims_mapping.index(0)
|
||||
num_of_process = len(src_dist_attr.process_mesh.process_ids)
|
||||
remainder_of_padding = src_value.shape[split_axis] % num_of_process
|
||||
is_balanced_split = remainder_of_padding == 0
|
||||
|
||||
permute = False
|
||||
if split_axis != 0:
|
||||
perm = list(range(0, len(src_value.shape)))
|
||||
perm[0] = split_axis
|
||||
perm[split_axis] = 0
|
||||
src_value = paddle._C_ops.transpose(src_value, perm)
|
||||
permute = True
|
||||
tmp_dims_mapping = dst_dist_attr.dims_mapping
|
||||
tmp_dims_mapping[split_axis] = -1
|
||||
tmp_dims_mapping[0] = 0
|
||||
dst_dist_attr = copy_dist_attr_with_new_member(
|
||||
dst_dist_attr, new_dims_mapping=tmp_dims_mapping
|
||||
)
|
||||
|
||||
if is_balanced_split:
|
||||
global_dst_attr = dst_type.as_dist_type().dist_attr()
|
||||
global_dims_mapping = global_dst_attr.dims_mapping
|
||||
axis = global_dims_mapping[0]
|
||||
global_dims_mapping[0] = global_dims_mapping[split_axis]
|
||||
global_dims_mapping[split_axis] = axis
|
||||
global_dist_attr = copy_dist_attr_with_new_member(
|
||||
global_dst_attr, new_dims_mapping=global_dims_mapping
|
||||
)
|
||||
dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), global_dist_attr
|
||||
)
|
||||
group = new_process_group(sorted(src_mesh.process_ids))
|
||||
dst_value = paddle._C_ops.reduce_scatter(
|
||||
src_value, group.id, num_of_process
|
||||
)
|
||||
dst_value.get_defining_op().set_execution_stream(
|
||||
ExecutionStreamType.DefaultStream.value
|
||||
)
|
||||
|
||||
# set dist type and dist attr
|
||||
dst_value.set_type(dst_type)
|
||||
dst_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_mesh, [src_dist_attr], [dst_dist_attr], chunk_id
|
||||
)
|
||||
)
|
||||
|
||||
if split_axis != 0:
|
||||
dst_value = paddle._C_ops.transpose(dst_value, perm)
|
||||
return dst_value
|
||||
else:
|
||||
dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), dst_dist_attr
|
||||
)
|
||||
original_dims_mapping = dst_dist_attr.dims_mapping.copy()
|
||||
original_split_axis = split_axis
|
||||
split_axis = 0
|
||||
avg_size_on_split_axis = int(
|
||||
(src_value.shape[split_axis] + num_of_process - 1)
|
||||
/ num_of_process
|
||||
)
|
||||
padding_num = (
|
||||
avg_size_on_split_axis * num_of_process
|
||||
- src_value.shape[split_axis]
|
||||
)
|
||||
padding_shape = src_value._local_shape
|
||||
padding_shape[split_axis] = padding_num
|
||||
padding_tensor = paddle.full(
|
||||
padding_shape,
|
||||
0.0,
|
||||
src_value.dtype,
|
||||
)
|
||||
tmp_src_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
padding_tensor.type(), src_dist_attr
|
||||
)
|
||||
padding_tensor.set_type(tmp_src_type)
|
||||
padding_tensor.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_dist_attr.process_mesh, [], [src_dist_attr], chunk_id
|
||||
)
|
||||
)
|
||||
concat_value = paddle._C_ops.concat(
|
||||
[src_value, padding_tensor], split_axis
|
||||
)
|
||||
axis_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
src_dist_attr.process_mesh,
|
||||
[-1],
|
||||
{0: paddle.base.core.ReduceType.kRedSum},
|
||||
)
|
||||
)
|
||||
concat_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_dist_attr.process_mesh,
|
||||
[
|
||||
paddle.base.libpaddle.pir.create_array_attribute(
|
||||
[src_dist_attr, src_dist_attr]
|
||||
),
|
||||
axis_dist_attr,
|
||||
],
|
||||
[src_dist_attr],
|
||||
chunk_id,
|
||||
)
|
||||
)
|
||||
|
||||
concat_global_shape = list(src_value.shape)
|
||||
concat_global_shape[split_axis] = (
|
||||
avg_size_on_split_axis * num_of_process
|
||||
)
|
||||
concat_type = paddle.pir.create_shaped_type(
|
||||
src_value.type(), concat_global_shape
|
||||
)
|
||||
concat_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
concat_type, src_dist_attr
|
||||
)
|
||||
concat_value.set_type(concat_type)
|
||||
|
||||
dst_value = self.reshard_p_to_s_with_padding(
|
||||
concat_value,
|
||||
split_axis,
|
||||
src_dist_attr,
|
||||
dst_dist_attr,
|
||||
dst_type,
|
||||
padding_num,
|
||||
)
|
||||
if permute:
|
||||
dst_value = paddle._C_ops.transpose(dst_value, perm)
|
||||
split_axis = original_split_axis
|
||||
return dst_value
|
||||
|
||||
def reshard_p_to_s_with_padding(
|
||||
self,
|
||||
src_value,
|
||||
split_axis,
|
||||
src_dist_attr,
|
||||
dst_dist_attr,
|
||||
dst_type,
|
||||
padding_num=0,
|
||||
):
|
||||
group = new_process_group(
|
||||
sorted(src_dist_attr.process_mesh.process_ids)
|
||||
)
|
||||
dst_value = paddle._C_ops.reduce_scatter(
|
||||
src_value, group.id, len(src_dist_attr.process_mesh.process_ids)
|
||||
)
|
||||
out_global_shape = dst_type.shape
|
||||
out_global_shape[split_axis] = (
|
||||
padding_num + out_global_shape[split_axis]
|
||||
)
|
||||
dst_tmp_type = paddle.pir.create_shaped_type(
|
||||
dst_value.type(), out_global_shape
|
||||
)
|
||||
dst_tmp_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
dst_tmp_type, dst_dist_attr
|
||||
)
|
||||
dst_value.set_type(dst_tmp_type)
|
||||
dst_value.get_defining_op().set_execution_stream(
|
||||
ExecutionStreamType.DefaultStream.value
|
||||
)
|
||||
dst_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_dist_attr.process_mesh,
|
||||
[src_dist_attr],
|
||||
[dst_dist_attr],
|
||||
src_value.get_defining_op().dist_attr.chunk_id,
|
||||
)
|
||||
)
|
||||
if padding_num != 0:
|
||||
if dist.get_rank() == dst_dist_attr.process_mesh.process_ids[-1]:
|
||||
dst_value = paddle._C_ops.split(
|
||||
dst_value,
|
||||
[
|
||||
dst_value.shape[split_axis] - padding_num,
|
||||
padding_num,
|
||||
],
|
||||
0,
|
||||
)[0]
|
||||
dst_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
dst_dist_attr.process_mesh,
|
||||
[dst_dist_attr],
|
||||
[dst_dist_attr],
|
||||
src_value.get_defining_op().dist_attr.chunk_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
dst_value.set_type(dst_type)
|
||||
return dst_value
|
||||
@@ -0,0 +1,73 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import paddle
|
||||
|
||||
from .base_reshard_func import (
|
||||
ReshardFunction,
|
||||
is_partial,
|
||||
is_replicated,
|
||||
is_shard,
|
||||
)
|
||||
|
||||
|
||||
class RToPReshardFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_replicated(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_partial(dst_dist_attr) or is_shard(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh.ndim != 1:
|
||||
return False
|
||||
if out_mesh.ndim != 1:
|
||||
return False
|
||||
if in_mesh != out_mesh:
|
||||
return False
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
dst_mesh = dst_dist_attr.process_mesh
|
||||
dst_reduce_type = dst_dist_attr.partial_status[0]
|
||||
local_rank = paddle.distributed.get_rank()
|
||||
|
||||
assert dst_reduce_type in [
|
||||
paddle.base.core.ReduceType.kRedSum,
|
||||
paddle.distributed.ReduceType.kRedAvg,
|
||||
paddle.distributed.ReduceType.kRedMax,
|
||||
], f"Unsupported reduce type {dst_reduce_type}"
|
||||
|
||||
if (
|
||||
dst_reduce_type == paddle.distributed.ReduceType.kRedSum
|
||||
and local_rank != 0
|
||||
):
|
||||
dst_value = paddle.full(src_value.shape, 0, dtype=src_value.dtype)
|
||||
else:
|
||||
dst_value = paddle.assign(src_value)
|
||||
|
||||
chunk_id = -1
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = src_value.get_defining_op().dist_attr.chunk_id
|
||||
dst_value.set_type(dst_type)
|
||||
dst_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
dst_mesh, [src_dist_attr], [dst_dist_attr], chunk_id
|
||||
)
|
||||
)
|
||||
|
||||
return dst_value
|
||||
@@ -0,0 +1,142 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import paddle
|
||||
|
||||
from .base_reshard_func import ReshardFunction, is_replicated, is_shard
|
||||
from .same_status_reshard_func import SameStatusReshardFunction
|
||||
|
||||
|
||||
class RToSReshardFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_replicated(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_shard(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh.ndim != 1:
|
||||
return False
|
||||
if out_mesh.ndim != 1:
|
||||
return False
|
||||
if in_mesh != out_mesh:
|
||||
return False
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
split_axis = -1
|
||||
mesh_axis = -1
|
||||
for idx, v in enumerate(dst_dist_attr.dims_mapping):
|
||||
if v != -1:
|
||||
split_axis = idx
|
||||
mesh_axis = v
|
||||
|
||||
mesh = src_dist_attr.process_mesh
|
||||
curr_global_rank = paddle.distributed.get_rank()
|
||||
chunk_id = -1
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = src_value.get_defining_op().dist_attr.chunk_id
|
||||
|
||||
if curr_global_rank in mesh.process_ids:
|
||||
total_nums = src_value.shape[split_axis]
|
||||
num_of_pieces = mesh.shape[mesh_axis]
|
||||
if num_of_pieces == 1:
|
||||
dst_value = paddle._C_ops.share_data_(src_value)
|
||||
share_data_op = dst_value.get_defining_op()
|
||||
# set dist type and dist attr
|
||||
dst_value.set_type(dst_type)
|
||||
share_data_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_dist_attr.process_mesh,
|
||||
[src_dist_attr],
|
||||
[dst_dist_attr],
|
||||
chunk_id,
|
||||
)
|
||||
)
|
||||
return dst_value
|
||||
piece_len = (total_nums + num_of_pieces - 1) // num_of_pieces
|
||||
rank_relative = mesh.process_ids.index(curr_global_rank)
|
||||
start = rank_relative * piece_len
|
||||
end = start + piece_len
|
||||
if curr_global_rank == mesh.process_ids[-1]:
|
||||
end = total_nums
|
||||
|
||||
out_value = paddle.slice(src_value, [split_axis], [start], [end])
|
||||
|
||||
out_value.set_type(dst_type)
|
||||
out_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
mesh, [src_dist_attr], [dst_dist_attr], chunk_id
|
||||
)
|
||||
)
|
||||
return out_value
|
||||
# fake var will be removed in remove_other_rank_op_pass.
|
||||
fake_var = paddle._C_ops.reshard_v2(src_value, dst_dist_attr)
|
||||
fake_var.set_type(dst_type)
|
||||
return fake_var
|
||||
|
||||
|
||||
class RToSReshardFunctionCrossMesh(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_replicated(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_shard(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if (
|
||||
in_mesh.ndim != 1
|
||||
or out_mesh.ndim != 1
|
||||
or in_mesh.shape != out_mesh.shape
|
||||
):
|
||||
return False
|
||||
|
||||
if in_mesh == out_mesh:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
same_status_func = SameStatusReshardFunction()
|
||||
tmp_dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
dst_dist_attr.process_mesh,
|
||||
src_dist_attr.dims_mapping,
|
||||
src_dist_attr.partial_status,
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dist_attr
|
||||
)
|
||||
out_value = same_status_func.reshard(
|
||||
src_dist_attr, tmp_dist_attr, src_value, tmp_dst_type
|
||||
)
|
||||
|
||||
if out_value is None:
|
||||
return None
|
||||
|
||||
curr_global_rank = paddle.distributed.get_rank()
|
||||
if curr_global_rank in dst_dist_attr.process_mesh.process_ids:
|
||||
r_to_s_func = RToSReshardFunction()
|
||||
assert r_to_s_func.is_suitable(tmp_dist_attr, dst_dist_attr), (
|
||||
f"Invoke the r to s reshard function is not valid from {tmp_dist_attr} to {dst_dist_attr}"
|
||||
)
|
||||
return r_to_s_func.reshard(
|
||||
tmp_dist_attr, dst_dist_attr, out_value, dst_type
|
||||
)
|
||||
return None
|
||||
@@ -0,0 +1,59 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .base_reshard_func import register_reshard_func
|
||||
from .global_to_sub_mesh_func import GlobalToSubMeshFunction
|
||||
from .nd_mesh_reshard_func import (
|
||||
NdMeshReshardFunction,
|
||||
NdMeshReshardFunctionCrossMesh,
|
||||
)
|
||||
from .p_to_r_reshard_func import (
|
||||
PToRReshardFunction,
|
||||
PToRReshardFunctionCrossMesh,
|
||||
)
|
||||
from .p_to_s_reshard_func import (
|
||||
PToSReshardFunction,
|
||||
)
|
||||
from .r_to_p_reshard_func import RToPReshardFunction
|
||||
from .r_to_s_reshard_func import (
|
||||
RToSReshardFunction,
|
||||
RToSReshardFunctionCrossMesh,
|
||||
)
|
||||
from .s_to_r_reshard_func import (
|
||||
SToRReshardFunction,
|
||||
SToRReshardFunctionCrossMesh,
|
||||
)
|
||||
from .s_to_s_reshard_func import SToSReshardFunction
|
||||
from .same_status_reshard_func import SameStatusReshardFunction
|
||||
from .sub_to_global_mesh_func import SubToGlobalMeshFunction
|
||||
|
||||
|
||||
def register_reshard_funcs():
|
||||
register_reshard_func(PToRReshardFunction())
|
||||
register_reshard_func(PToRReshardFunctionCrossMesh())
|
||||
register_reshard_func(PToSReshardFunction())
|
||||
register_reshard_func(RToSReshardFunction())
|
||||
register_reshard_func(RToSReshardFunctionCrossMesh())
|
||||
register_reshard_func(RToPReshardFunction())
|
||||
register_reshard_func(SameStatusReshardFunction())
|
||||
register_reshard_func(SToRReshardFunction())
|
||||
register_reshard_func(SToRReshardFunctionCrossMesh())
|
||||
register_reshard_func(NdMeshReshardFunction())
|
||||
register_reshard_func(NdMeshReshardFunctionCrossMesh())
|
||||
register_reshard_func(GlobalToSubMeshFunction())
|
||||
register_reshard_func(SubToGlobalMeshFunction())
|
||||
register_reshard_func(SToSReshardFunction())
|
||||
|
||||
|
||||
register_reshard_funcs()
|
||||
@@ -0,0 +1,363 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import paddle
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from .base_reshard_func import (
|
||||
ReshardFunction,
|
||||
copy_op_attr_with_new_member,
|
||||
is_replicated,
|
||||
is_shard,
|
||||
)
|
||||
from .same_status_reshard_func import SameStatusReshardFunction
|
||||
|
||||
|
||||
class SToRReshardFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_shard(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_replicated(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh.ndim != 1:
|
||||
return False
|
||||
if out_mesh.ndim != 1:
|
||||
return False
|
||||
if in_mesh != out_mesh:
|
||||
return False
|
||||
return True
|
||||
|
||||
def infer_allgather_dist_type(self, in_value, split_axis):
|
||||
tensor_ndim = len(in_value.shape)
|
||||
in_dist_attr = in_value.dist_attr()
|
||||
split_mesh_dim = in_dist_attr.dims_mapping[split_axis]
|
||||
mesh = in_dist_attr.process_mesh
|
||||
|
||||
# Calculate local shape. In nd_mesh_reshard, multiple tensor axis
|
||||
# may be shard and it will call this 1-D s_to_r function on each
|
||||
# axis. In this case, we should recompute the local and global shape.
|
||||
out_local_shape = list(in_value.shape)
|
||||
out_local_shape[split_axis] = int(
|
||||
(in_value.shape[split_axis] + mesh.shape[split_mesh_dim] - 1)
|
||||
/ mesh.shape[split_mesh_dim]
|
||||
)
|
||||
out_global_shape = list(out_local_shape)
|
||||
out_global_shape[0] *= mesh.shape[split_mesh_dim]
|
||||
out_type = paddle.pir.create_shaped_type(
|
||||
in_value.type(), out_global_shape
|
||||
)
|
||||
|
||||
out_dims_mapping = list(in_dist_attr.dims_mapping)
|
||||
out_dims_mapping[split_axis] = -1
|
||||
out_dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
mesh, out_dims_mapping, in_dist_attr.partial_status
|
||||
)
|
||||
out_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
out_type, out_dist_attr
|
||||
)
|
||||
return out_type
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
if src_dist_attr.process_mesh.size == 1:
|
||||
dst_value = paddle._C_ops.share_data_(src_value)
|
||||
share_data_op = dst_value.get_defining_op()
|
||||
# set dist type and dist attr
|
||||
dst_value.set_type(dst_type)
|
||||
|
||||
chunk_id = -1
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = src_value.get_defining_op().dist_attr.chunk_id
|
||||
share_data_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_dist_attr.process_mesh,
|
||||
[src_dist_attr],
|
||||
[dst_dist_attr],
|
||||
chunk_id,
|
||||
)
|
||||
)
|
||||
return dst_value
|
||||
|
||||
def get_split_axis_with_dims_mapping(dims_mapping):
|
||||
split_axis = {}
|
||||
for idx, v in enumerate(dims_mapping):
|
||||
if v != -1:
|
||||
split_axis[idx] = v
|
||||
return split_axis
|
||||
|
||||
split_axis_map = get_split_axis_with_dims_mapping(
|
||||
src_dist_attr.dims_mapping
|
||||
)
|
||||
|
||||
split_axis = -1
|
||||
for k, v in split_axis_map.items():
|
||||
split_axis = k
|
||||
break
|
||||
num_of_process = src_dist_attr.process_mesh.size
|
||||
num_of_padding = src_value.shape[split_axis] % num_of_process
|
||||
is_balanced_split = num_of_padding == 0
|
||||
|
||||
if is_balanced_split:
|
||||
new_value = self.reshard_s_to_r_with_padding(
|
||||
src_value,
|
||||
split_axis,
|
||||
src_dist_attr,
|
||||
dst_dist_attr,
|
||||
dst_type,
|
||||
num_of_padding,
|
||||
)
|
||||
return new_value
|
||||
else:
|
||||
# find the last one
|
||||
need_padding = (
|
||||
paddle.distributed.get_rank()
|
||||
== src_dist_attr.process_mesh.process_ids[-1]
|
||||
)
|
||||
|
||||
# get padding_num
|
||||
avg_size_on_split_axis = int(
|
||||
(src_value.shape[split_axis] + num_of_process - 1)
|
||||
/ num_of_process
|
||||
)
|
||||
padding_num = (
|
||||
avg_size_on_split_axis * num_of_process
|
||||
- src_value.shape[split_axis]
|
||||
)
|
||||
if need_padding:
|
||||
# set right _local_shape
|
||||
local_shape_at_split_axis = src_value.shape[
|
||||
split_axis
|
||||
] - avg_size_on_split_axis * (num_of_process - 1)
|
||||
local_shape = src_value._local_shape
|
||||
local_shape[split_axis] = local_shape_at_split_axis
|
||||
tmp_src_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), src_dist_attr, list(local_shape)
|
||||
)
|
||||
src_value.set_type(tmp_src_type)
|
||||
padding_shape = src_value._local_shape
|
||||
padding_shape[split_axis] = padding_num
|
||||
padding_tensor = paddle.full(
|
||||
padding_shape,
|
||||
0.0,
|
||||
src_value.dtype,
|
||||
)
|
||||
tmp_src_type1 = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
padding_tensor.type(), dst_dist_attr
|
||||
)
|
||||
padding_tensor.set_type(tmp_src_type1)
|
||||
padding_tensor.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
dst_dist_attr.process_mesh, [], [dst_dist_attr]
|
||||
)
|
||||
)
|
||||
|
||||
concat_value = paddle._C_ops.concat(
|
||||
[src_value, padding_tensor], split_axis
|
||||
)
|
||||
# set concat dist_attr
|
||||
axis_dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
src_dist_attr.process_mesh, [-1], {}
|
||||
)
|
||||
)
|
||||
concat_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_dist_attr.process_mesh,
|
||||
[
|
||||
paddle.base.libpaddle.pir.create_array_attribute(
|
||||
[src_dist_attr, dst_dist_attr]
|
||||
),
|
||||
axis_dist_attr,
|
||||
],
|
||||
[src_dist_attr],
|
||||
)
|
||||
)
|
||||
# set concat_value type
|
||||
concat_global_shape = list(src_value.shape)
|
||||
concat_global_shape[split_axis] = (
|
||||
avg_size_on_split_axis * num_of_process
|
||||
)
|
||||
concat_type = paddle.pir.create_shaped_type(
|
||||
src_value.type(), concat_global_shape
|
||||
)
|
||||
concat_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
concat_type, src_dist_attr
|
||||
)
|
||||
concat_value.set_type(concat_type)
|
||||
|
||||
new_value = self.reshard_s_to_r_with_padding(
|
||||
concat_value,
|
||||
split_axis,
|
||||
src_dist_attr,
|
||||
dst_dist_attr,
|
||||
dst_type,
|
||||
padding_num,
|
||||
)
|
||||
return new_value
|
||||
else:
|
||||
new_value = self.reshard_s_to_r_with_padding(
|
||||
src_value,
|
||||
split_axis,
|
||||
src_dist_attr,
|
||||
dst_dist_attr,
|
||||
dst_type,
|
||||
padding_num,
|
||||
)
|
||||
return new_value
|
||||
|
||||
def reshard_s_to_r_with_padding(
|
||||
self,
|
||||
src_value,
|
||||
split_axis,
|
||||
src_dist_attr,
|
||||
dst_dist_attr,
|
||||
dst_type,
|
||||
padding_num=0,
|
||||
):
|
||||
src_mesh = src_dist_attr.process_mesh
|
||||
num_of_process = len(src_mesh.process_ids)
|
||||
chunk_id = -1
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = src_value.get_defining_op().dist_attr.chunk_id
|
||||
|
||||
group = new_process_group(sorted(src_mesh.process_ids))
|
||||
allgather_value = paddle._C_ops.all_gather(
|
||||
src_value, group.id, num_of_process
|
||||
)
|
||||
allgather_type = self.infer_allgather_dist_type(src_value, split_axis)
|
||||
allgather_value.set_type(allgather_type)
|
||||
|
||||
# set op_dist_attr
|
||||
new_dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
dst_dist_attr.process_mesh,
|
||||
[-1] * len(dst_dist_attr.dims_mapping),
|
||||
dst_dist_attr.partial_status,
|
||||
)
|
||||
allgather_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_mesh, [src_dist_attr], [new_dist_attr], chunk_id
|
||||
)
|
||||
)
|
||||
|
||||
if split_axis != 0 or padding_num != 0:
|
||||
allgather_op = allgather_value.get_defining_op()
|
||||
split_values = paddle._C_ops.split_with_num(
|
||||
allgather_op.result(0), num_of_process, 0
|
||||
)
|
||||
builtin_split_op = split_values[0].get_defining_op()
|
||||
pd_split_op = builtin_split_op.operand_source(0).get_defining_op()
|
||||
pd_split_op.dist_attr = copy_op_attr_with_new_member(
|
||||
pd_split_op.dist_attr, new_chunk_id=chunk_id
|
||||
)
|
||||
|
||||
# fix the split_with_num dist attribute.
|
||||
new_inner_types = []
|
||||
for sub_value in split_values:
|
||||
new_inner_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
sub_value.type(), allgather_value.dist_attr()
|
||||
)
|
||||
new_inner_types.append(new_inner_type)
|
||||
sub_value.set_type(new_inner_type)
|
||||
vec_type = paddle.base.libpaddle.pir.create_vec_type(
|
||||
new_inner_types
|
||||
)
|
||||
pd_split_op.result(0).set_type(vec_type)
|
||||
|
||||
if padding_num != 0:
|
||||
tmp_split_values = paddle._C_ops.split(
|
||||
split_values[-1],
|
||||
[
|
||||
split_values[-1].shape[split_axis] - padding_num,
|
||||
padding_num,
|
||||
],
|
||||
split_axis,
|
||||
)
|
||||
split_op = tmp_split_values.get_defining_op()
|
||||
split_op.dist_attr = copy_op_attr_with_new_member(
|
||||
split_op.dist_attr, new_chunk_id=chunk_id
|
||||
)
|
||||
split_values[-1] = tmp_split_values[0]
|
||||
concat_value = paddle._C_ops.concat(split_values, split_axis)
|
||||
concat_op = concat_value.get_defining_op()
|
||||
concat_op.dist_attr = copy_op_attr_with_new_member(
|
||||
concat_op.dist_attr, new_chunk_id=chunk_id
|
||||
)
|
||||
return concat_value
|
||||
else:
|
||||
concat_value = paddle._C_ops.concat(split_values, split_axis)
|
||||
# fold builtin.split op and builtin.combine op
|
||||
concat_op = concat_value.get_defining_op()
|
||||
concat_op.dist_attr = copy_op_attr_with_new_member(
|
||||
concat_op.dist_attr, new_chunk_id=chunk_id
|
||||
)
|
||||
builtin_combine_op = concat_op.operand_source(
|
||||
0
|
||||
).get_defining_op()
|
||||
concat_op.operand(0).set_source(pd_split_op.result(0))
|
||||
builtin_combine_op.erase()
|
||||
builtin_split_op.erase()
|
||||
return concat_value
|
||||
|
||||
return allgather_value
|
||||
|
||||
|
||||
class SToRReshardFunctionCrossMesh(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_shard(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_replicated(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if (
|
||||
in_mesh.ndim != 1
|
||||
or out_mesh.ndim != 1
|
||||
or in_mesh.shape != out_mesh.shape
|
||||
):
|
||||
return False
|
||||
|
||||
if in_mesh == out_mesh:
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
same_status_func = SameStatusReshardFunction()
|
||||
tmp_dist_attr = paddle.base.libpaddle.pir.create_tensor_dist_attribute(
|
||||
dst_dist_attr.process_mesh,
|
||||
src_dist_attr.dims_mapping,
|
||||
src_dist_attr.partial_status,
|
||||
)
|
||||
tmp_dst_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), tmp_dist_attr
|
||||
)
|
||||
out_value = same_status_func.reshard(
|
||||
src_dist_attr, tmp_dist_attr, src_value, tmp_dst_type
|
||||
)
|
||||
|
||||
s_to_r_func = SToRReshardFunction()
|
||||
assert s_to_r_func.is_suitable(tmp_dist_attr, dst_dist_attr), (
|
||||
f"Invoke the p to r reshard function is not valid from {tmp_dist_attr} to {dst_dist_attr}"
|
||||
)
|
||||
return s_to_r_func.reshard(
|
||||
tmp_dist_attr, dst_dist_attr, out_value, dst_type
|
||||
)
|
||||
@@ -0,0 +1,124 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
|
||||
import copy
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.utils.stream_utils import ExecutionStreamType
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from .base_reshard_func import (
|
||||
ReshardFunction,
|
||||
is_shard,
|
||||
)
|
||||
|
||||
|
||||
class SToSReshardFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if not is_shard(src_dist_attr):
|
||||
return False
|
||||
|
||||
if not is_shard(dst_dist_attr):
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh.ndim != 1:
|
||||
return False
|
||||
if out_mesh.ndim != 1:
|
||||
return False
|
||||
if in_mesh != out_mesh:
|
||||
return False
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
"""
|
||||
Reshard from shard to shard status on 1D mesh.
|
||||
E.g. tensor shape: [B, S, H], mesh = [0, 1]
|
||||
1. [Shard(0)] --> [Shard(1)], N ranks:
|
||||
1). reshape from [B, S, H] -> [B, N, S/N, H]
|
||||
2). transpose from [B, N, S/N, H] -> [N, B, S/N, H]
|
||||
3). reshape from [N, B, S/N, H] -> [N*B, S/N, H]
|
||||
4). all to all communicate
|
||||
2. [Shard(1)] --> [Shard(0)], N ranks:
|
||||
1). all to all communicate
|
||||
2). reshape from [B, S, H] -> [N, B/N, S, H]
|
||||
3). transpose from [N, B/N, S, H] -> [B/N, N, S/N, H]
|
||||
4). reshape from [B/N, N, S/N, H] -> [B, S, H]
|
||||
"""
|
||||
in_split_axis = src_dist_attr.dims_mapping.index(0)
|
||||
out_split_axis = dst_dist_attr.dims_mapping.index(0)
|
||||
nranks = len(src_dist_attr.process_mesh.process_ids)
|
||||
|
||||
if out_split_axis != 0:
|
||||
pre_shape = copy.copy(src_value.shape)
|
||||
if pre_shape[out_split_axis] != -1:
|
||||
pre_shape[out_split_axis] = pre_shape[out_split_axis] // nranks
|
||||
pre_shape.insert(out_split_axis, nranks)
|
||||
out_reshape1 = paddle._C_ops.reshape(src_value, pre_shape)
|
||||
|
||||
axes = [out_split_axis]
|
||||
for i in range(len(pre_shape)):
|
||||
if i != out_split_axis:
|
||||
axes.append(i)
|
||||
out_transpose = paddle._C_ops.transpose(out_reshape1, axes)
|
||||
|
||||
pre_shape.pop(out_split_axis)
|
||||
if pre_shape[in_split_axis] != -1:
|
||||
pre_shape[in_split_axis] *= nranks
|
||||
in_all2all = paddle._C_ops.reshape(out_transpose, pre_shape)
|
||||
in_all2all_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), dst_dist_attr
|
||||
)
|
||||
in_all2all.set_type(in_all2all_type)
|
||||
else:
|
||||
in_all2all = paddle._C_ops.share_data_(src_value)
|
||||
|
||||
src_mesh = src_dist_attr.process_mesh
|
||||
group = new_process_group(sorted(src_mesh.process_ids))
|
||||
dst_value = paddle._C_ops.all_to_all(in_all2all, group.id)
|
||||
dst_value.get_defining_op().set_execution_stream(
|
||||
ExecutionStreamType.DefaultStream.value
|
||||
)
|
||||
out_all2all_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
in_all2all.type(), src_dist_attr
|
||||
)
|
||||
dst_value.set_type(out_all2all_type)
|
||||
dst_value.get_defining_op().dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_mesh, [src_dist_attr], [dst_dist_attr], -1
|
||||
)
|
||||
)
|
||||
|
||||
if in_split_axis != 0:
|
||||
post_shape = copy.copy(src_value.shape)
|
||||
if post_shape[0] != -1:
|
||||
post_shape[0] = post_shape[0] // nranks
|
||||
post_shape.insert(0, nranks)
|
||||
dst_value = paddle.reshape(dst_value, post_shape)
|
||||
|
||||
axes = list(range(1, len(post_shape)))
|
||||
axes.insert(in_split_axis, 0)
|
||||
dst_value = paddle._C_ops.transpose(dst_value, axes)
|
||||
|
||||
post_shape.pop(0)
|
||||
if post_shape[in_split_axis] != -1:
|
||||
post_shape[in_split_axis] *= nranks
|
||||
dst_value = paddle._C_ops.reshape(dst_value, post_shape)
|
||||
|
||||
dst_value.set_type(dst_type)
|
||||
|
||||
return dst_value
|
||||
+158
@@ -0,0 +1,158 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.passes.pass_utils import find_var_used_op_chunk_id
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from .base_reshard_func import ReshardFunction
|
||||
|
||||
|
||||
class SameStatusReshardFunction(ReshardFunction):
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
if src_dist_attr.dims_mapping != dst_dist_attr.dims_mapping:
|
||||
return False
|
||||
if src_dist_attr.partial_dims != dst_dist_attr.partial_dims:
|
||||
return False
|
||||
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
if in_mesh == out_mesh:
|
||||
return False
|
||||
if in_mesh.shape != out_mesh.shape:
|
||||
return False
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
src_mesh = src_dist_attr.process_mesh
|
||||
dst_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
all_process_ids = list(
|
||||
set(src_mesh.process_ids) | set(dst_mesh.process_ids)
|
||||
)
|
||||
all_process_ids = sorted(all_process_ids)
|
||||
|
||||
cur_global_rank = paddle.distributed.get_rank()
|
||||
|
||||
for src, dst in zip(src_mesh.process_ids, dst_mesh.process_ids):
|
||||
if src != dst:
|
||||
new_process_group([src, dst], group_type="p2p")
|
||||
new_process_group([dst, src], group_type="p2p")
|
||||
|
||||
is_send = True
|
||||
for src, dst in zip(src_mesh.process_ids, dst_mesh.process_ids):
|
||||
if src == cur_global_rank:
|
||||
chunk_id = -1
|
||||
if (
|
||||
src_value.get_defining_op().name() == "pd_op.add_n"
|
||||
and src_value.get_defining_op()
|
||||
.operand_source(0)
|
||||
.get_defining_op()
|
||||
.name()
|
||||
== "builtin.combine"
|
||||
):
|
||||
add_n_op = src_value.get_defining_op()
|
||||
combine_op = add_n_op.operand_source(0).get_defining_op()
|
||||
combine_op_chunk_id_list = []
|
||||
for input in combine_op.operands():
|
||||
if input.source().get_defining_op().dist_attr:
|
||||
combine_op_chunk_id_list.append(
|
||||
input.source()
|
||||
.get_defining_op()
|
||||
.dist_attr.chunk_id
|
||||
)
|
||||
else:
|
||||
combine_op_chunk_id_list.append(-1)
|
||||
# check combine_op operands chunk_id equal
|
||||
assert all(
|
||||
x == combine_op_chunk_id_list[0]
|
||||
for x in combine_op_chunk_id_list
|
||||
), "combine_op's operands has different chunk_id."
|
||||
chunk_id = combine_op_chunk_id_list[0]
|
||||
# reset add_n chunk_id
|
||||
add_n_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
add_n_op.dist_attr.process_mesh,
|
||||
add_n_op.dist_attr.operands(),
|
||||
add_n_op.dist_attr.results(),
|
||||
chunk_id,
|
||||
)
|
||||
)
|
||||
else:
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = (
|
||||
src_value.get_defining_op().dist_attr.chunk_id
|
||||
)
|
||||
|
||||
comm_group = new_process_group([src, dst], group_type="p2p")
|
||||
paddle._C_ops.send_v2(
|
||||
src_value,
|
||||
comm_group.id,
|
||||
comm_group.ranks.index(dst),
|
||||
True,
|
||||
False,
|
||||
)
|
||||
point = paddle.base.libpaddle.pir.get_current_insertion_point()
|
||||
point.prev()
|
||||
new_op = point.get_operation()
|
||||
assert new_op.name() == "pd_op.send_v2"
|
||||
new_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_mesh, [src_dist_attr], [], chunk_id
|
||||
)
|
||||
)
|
||||
break
|
||||
|
||||
elif dst == cur_global_rank:
|
||||
all_used_ops = src_value.all_used_ops()
|
||||
chunk_id = -1
|
||||
for used_op in all_used_ops:
|
||||
var = used_op.result(0)
|
||||
if var.dist_attr().process_mesh == dst_mesh:
|
||||
chunk_id = find_var_used_op_chunk_id(var)
|
||||
|
||||
assert -1 not in dst_type.shape, (
|
||||
"dynamic shape is not supported by pir-auto parallel yet."
|
||||
)
|
||||
|
||||
comm_group = new_process_group([src, dst], group_type="p2p")
|
||||
recv_value = paddle._C_ops.recv_v2(
|
||||
dst_type._local_shape,
|
||||
dst_type.dtype,
|
||||
comm_group.ranks.index(src),
|
||||
comm_group.id,
|
||||
True,
|
||||
False,
|
||||
)
|
||||
new_op = recv_value.get_defining_op()
|
||||
new_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
dst_mesh,
|
||||
[],
|
||||
[dst_dist_attr],
|
||||
chunk_id,
|
||||
)
|
||||
)
|
||||
recv_value.set_type(dst_type)
|
||||
is_send = False
|
||||
break
|
||||
|
||||
if is_send:
|
||||
# fake var will be removed in remove_other_rank_op_pass.
|
||||
fake_var = paddle._C_ops.reshard_v2(src_value, dst_dist_attr)
|
||||
fake_var.set_type(dst_type)
|
||||
return fake_var
|
||||
else:
|
||||
return recv_value
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
# Copyright (c) 2024 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
|
||||
import paddle
|
||||
import paddle.distributed as dist
|
||||
from paddle.distributed.auto_parallel.placement_type import (
|
||||
check_placements_equal,
|
||||
)
|
||||
|
||||
from ..process_group import new_process_group
|
||||
from ..utils import split_mesh
|
||||
from .base_reshard_func import ReshardFunction, copy_dist_attr_with_new_member
|
||||
|
||||
|
||||
def _mesh_equal_ignore_shape_one(mesh1, mesh2, dim: int):
|
||||
"""
|
||||
Check if two process meshes are equal, ignoring the shape value `1`
|
||||
in the specified dimension. This is used when mesh1 is a sub-mesh
|
||||
split from a global mesh, in this case, the shape of mesh1 is `1`
|
||||
in the split dim.
|
||||
E.g, the following two meshes are equal:
|
||||
mesh1: shape = [1,2,2], process_ids = [0,1,2,3]
|
||||
mesh2: shape = [2,2], process_ids = [0,1,2,3]
|
||||
"""
|
||||
assert dim >= 0 and dim < len(mesh1.shape), "invalid dim arg"
|
||||
if mesh1 == mesh2:
|
||||
return True
|
||||
|
||||
if mesh1.process_ids != mesh2.process_ids:
|
||||
return False
|
||||
|
||||
a_shape = copy.copy(mesh1.shape)
|
||||
b_shape = copy.copy(mesh2.shape)
|
||||
|
||||
if a_shape[dim] != 1:
|
||||
return False
|
||||
a_shape.pop(dim)
|
||||
|
||||
return a_shape == b_shape
|
||||
|
||||
|
||||
class SubToGlobalMeshFunction(ReshardFunction):
|
||||
"""
|
||||
Reshard from sub-mesh to global mesh, now only supports
|
||||
both input and output values are replicated, e.g.
|
||||
1. input: mesh:[0], placements:[Replicate()]
|
||||
output: mesh:[0,1], placements:[Replicate()]
|
||||
2. input: mesh:[0,1], placements:[Replicate()]
|
||||
output: mesh:[[0,1],[2,3]], placements:[Replicate(), Replicate()]
|
||||
"""
|
||||
|
||||
def is_suitable(self, src_dist_attr, dst_dist_attr):
|
||||
in_mesh = src_dist_attr.process_mesh
|
||||
out_mesh = dst_dist_attr.process_mesh
|
||||
sub_mesh_dim = paddle.base.core.sub_mesh_dim(out_mesh, in_mesh)
|
||||
if sub_mesh_dim == -1:
|
||||
return False
|
||||
sub_meshes, sub_placements = (
|
||||
dist.auto_parallel.api._get_sub_meshes_and_local_placements(
|
||||
out_mesh, dst_dist_attr.placements_attr, sub_mesh_dim
|
||||
)
|
||||
)
|
||||
if not check_placements_equal(
|
||||
src_dist_attr.placements_attr, sub_placements
|
||||
):
|
||||
return False
|
||||
return True
|
||||
|
||||
def reshard(self, src_dist_attr, dst_dist_attr, src_value, dst_type):
|
||||
src_mesh = src_dist_attr.process_mesh
|
||||
dst_mesh = dst_dist_attr.process_mesh
|
||||
|
||||
sub_mesh_dim = paddle.base.core.sub_mesh_dim(dst_mesh, src_mesh)
|
||||
sub_meshes = split_mesh(dst_mesh, sub_mesh_dim)
|
||||
dst_meshes = [
|
||||
mesh
|
||||
for mesh in sub_meshes
|
||||
if not _mesh_equal_ignore_shape_one(mesh, src_mesh, sub_mesh_dim)
|
||||
]
|
||||
|
||||
comm_group_ids = []
|
||||
root_ranks = []
|
||||
for p_id in src_mesh.process_ids:
|
||||
comm_group_ids.append([p_id])
|
||||
root_ranks.append(p_id)
|
||||
for i, group_ids in enumerate(comm_group_ids):
|
||||
for mesh in dst_meshes:
|
||||
group_ids.append(mesh.process_ids[i])
|
||||
|
||||
other_ranks = copy.copy(dst_mesh.process_ids)
|
||||
for rank in other_ranks:
|
||||
if rank in src_mesh.process_ids:
|
||||
other_ranks.remove(rank)
|
||||
|
||||
cur_rank = paddle.distributed.get_rank()
|
||||
|
||||
if cur_rank in src_mesh.process_ids:
|
||||
# the root rank will broadcast the src_value to other ranks
|
||||
chunk_id = -1
|
||||
if src_value.get_defining_op().dist_attr:
|
||||
chunk_id = src_value.get_defining_op().dist_attr.chunk_id
|
||||
tmp_value = paddle._C_ops.share_data_(src_value)
|
||||
value_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
src_value.type(), src_value.dist_attr()
|
||||
)
|
||||
tmp_value.set_type(value_type)
|
||||
op = tmp_value.get_defining_op()
|
||||
op.dist_attr = paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
src_mesh, [src_dist_attr], [src_dist_attr], chunk_id
|
||||
)
|
||||
elif cur_rank in other_ranks:
|
||||
# create the buffer on other ranks for receiving the data
|
||||
tmp_value = paddle.zeros(dst_type.shape, dst_type.dtype)
|
||||
op = tmp_value.get_defining_op()
|
||||
mesh = paddle.distributed.ProcessMesh(other_ranks)
|
||||
value_dist_attr = copy_dist_attr_with_new_member(
|
||||
dst_dist_attr, new_process_mesh=mesh
|
||||
)
|
||||
value_type = paddle.base.libpaddle.pir.cvt_to_dist_type(
|
||||
dst_type, value_dist_attr
|
||||
)
|
||||
tmp_value.set_type(value_type)
|
||||
op.dist_attr = paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
mesh, [], [value_dist_attr]
|
||||
)
|
||||
else:
|
||||
# do nothing if the current rank is not in src_mesh and dst_mesh.
|
||||
# use reshard_op to create and return a fake value, and the fake
|
||||
# value will be removed 'remove_other_rank_op_pass'.
|
||||
fake_var = paddle._C_ops.reshard_v2(src_value, dst_dist_attr)
|
||||
return fake_var
|
||||
|
||||
# create communication groups
|
||||
for i, group_ids in enumerate(comm_group_ids):
|
||||
comm_group_ids[i] = sorted(group_ids)
|
||||
# the root arg in broadcast is the local index
|
||||
# of the rank in the communication group
|
||||
root_ranks[i] = comm_group_ids[i].index(root_ranks[i])
|
||||
|
||||
comm_groups = []
|
||||
for i, group_ids in enumerate(comm_group_ids):
|
||||
comm_groups.append(new_process_group(group_ids))
|
||||
if cur_rank in group_ids:
|
||||
cur_group_id = i
|
||||
|
||||
broadcast_value = paddle._C_ops.broadcast(
|
||||
tmp_value, comm_groups[cur_group_id].id, root_ranks[cur_group_id]
|
||||
)
|
||||
broadcast_value.set_type(dst_type)
|
||||
|
||||
broadcast_op = broadcast_value.get_defining_op()
|
||||
broadcast_op.dist_attr = (
|
||||
paddle.base.libpaddle.pir.create_op_dist_attribute(
|
||||
dst_mesh, [src_dist_attr], [dst_dist_attr]
|
||||
)
|
||||
)
|
||||
|
||||
return broadcast_value
|
||||
@@ -0,0 +1,17 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
from .profiler import profiler # noqa: F401
|
||||
|
||||
__all__ = []
|
||||
@@ -0,0 +1,241 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import logging
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
from ..utils import get_logger, is_recompute_op
|
||||
from .trial import (
|
||||
OptimizationTunerTrial as Trial,
|
||||
TrialStatus,
|
||||
)
|
||||
|
||||
|
||||
class AlgorithmBase(ABC):
|
||||
"""
|
||||
An Tuning algorithm is a class to find out an optimal configuration
|
||||
given the selected tuning optimization pass(es) and the arguments to be tuned.
|
||||
Different optimization pass(es) will correspond to a different algorithm,
|
||||
where different search space **pruning rules** will applied.
|
||||
|
||||
In another word, the key "algorithm" for this class is the
|
||||
search space pruning rules specific for the given optimization scenario.
|
||||
"""
|
||||
|
||||
_REGISTERED_ALGORITHMS = {}
|
||||
|
||||
name = None
|
||||
|
||||
@staticmethod
|
||||
def _register(algo_name, algo_class):
|
||||
assert issubclass(algo_class, AlgorithmBase)
|
||||
AlgorithmBase._REGISTERED_ALGORITHMS[algo_name] = algo_class
|
||||
|
||||
def __init__(self, config):
|
||||
self._config = config
|
||||
self._init_spaces()
|
||||
self._logger = get_logger(logging.INFO)
|
||||
self._changed_configs = []
|
||||
|
||||
@property
|
||||
def changed_configs(self):
|
||||
return self._changed_configs[:]
|
||||
|
||||
def collect_model_info(self, main_prog, startup_prog):
|
||||
"""
|
||||
Collect the model static info (from programs) that could be used to
|
||||
pruning candidate trials and saving tuning time. For instance,
|
||||
model info like number of model parameters and activation memory could be
|
||||
used to prune candidate trial and decide the next trial.
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _init_spaces(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def next_trial(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def update(self, results):
|
||||
"""
|
||||
Update the algorithm with the results of last trial. Using this information is used to
|
||||
pruning the search space of the future trial.
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_config_from_trial(self, trial):
|
||||
"""
|
||||
Return a new fleet.DistributedStrategy with the configurations in trial.
|
||||
"""
|
||||
assert len(self._changed_configs) > 0
|
||||
new_strategy = copy.deepcopy(self._config.dist_strategy)
|
||||
for name in self._changed_configs:
|
||||
config = getattr(trial.space, name)
|
||||
setattr(new_strategy, name, config)
|
||||
return new_strategy
|
||||
|
||||
|
||||
def register_algor(name):
|
||||
def impl(cls):
|
||||
AlgorithmBase._register(name, cls)
|
||||
cls.name = name
|
||||
return cls
|
||||
|
||||
return impl
|
||||
|
||||
|
||||
def new_algorithm(name, config):
|
||||
algor_class = AlgorithmBase._REGISTERED_ALGORITHMS.get(name)
|
||||
assert algor_class is not None, f"Algorithm {name} is not defined."
|
||||
algor_obj = algor_class(config)
|
||||
return algor_obj
|
||||
|
||||
|
||||
@register_algor("sharding")
|
||||
class ShardingStageAlgorithm(AlgorithmBase):
|
||||
# TODO import trial class & copy strategy
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self._changed_configs = ["sharding"]
|
||||
|
||||
def _init_spaces(self):
|
||||
self._max_stage = 3
|
||||
self._trial_idx = 0
|
||||
|
||||
stage_range = self._config.sharding.get("tuning_range", None)
|
||||
if stage_range:
|
||||
assert set(stage_range).issubset({0, 1, 2, 3}), (
|
||||
f"Sharding Stage should belong into range within 0 - 3 but got {stage_range}."
|
||||
)
|
||||
stage_range.sort(reverse=True)
|
||||
else:
|
||||
stage_range = list(range(self._max_stage + 1))
|
||||
stage_range.sort(reverse=True)
|
||||
self._stage_range = stage_range[:]
|
||||
self._total_num_trial = len(self._stage_range)
|
||||
|
||||
def next_trial(self):
|
||||
if self._trial_idx < self._total_num_trial:
|
||||
stage = self._stage_range[self._trial_idx]
|
||||
|
||||
new_strategy = copy.deepcopy(self._config.dist_strategy)
|
||||
sharding = new_strategy.sharding
|
||||
sharding.stage = stage
|
||||
|
||||
name = f"trial-sharding-stage{stage}"
|
||||
trial = Trial(new_strategy, name, self.changed_configs)
|
||||
|
||||
return trial
|
||||
else:
|
||||
return Trial(None, None, None, status=TrialStatus.STOPPED)
|
||||
|
||||
def update(self, results):
|
||||
et = results.get("ErrorType", None)
|
||||
if et and et == "ResourceExhaustedError":
|
||||
self._trial_idx = self._total_num_trial
|
||||
self._logger.info(
|
||||
"Last trial is failed with OOM, all remaining trials are pruned to save time !"
|
||||
)
|
||||
else:
|
||||
self._trial_idx += 1
|
||||
|
||||
|
||||
@register_algor("recompute")
|
||||
class RecomputeCheckpointAlgorithm(AlgorithmBase):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self._changed_configs = ["recompute"]
|
||||
|
||||
def collect_model_info(self, main_prog, startup_prog):
|
||||
segments = []
|
||||
for op in main_prog.global_block().ops:
|
||||
if not is_recompute_op(op):
|
||||
continue
|
||||
|
||||
seg_name = op.attr('op_namescope')
|
||||
if seg_name not in segments:
|
||||
segments.append(seg_name)
|
||||
|
||||
self._total_num_trial = len(segments)
|
||||
self._tuning_segments = list(range(len(segments)))
|
||||
self._trial_left = 0
|
||||
self._trial_right = len(segments) - 1
|
||||
self._trial_idx = int(0 + (len(segments) - 1) / 2)
|
||||
|
||||
def _init_spaces(self):
|
||||
self._recompute_mode = "all"
|
||||
|
||||
def next_trial(self):
|
||||
if self._trial_idx < self._total_num_trial:
|
||||
if self._recompute_mode == "all":
|
||||
self._recompute_flag = False
|
||||
new_strategy = copy.deepcopy(self._config.dist_strategy)
|
||||
name = "trial-recompute-all-segments"
|
||||
return Trial(new_strategy, name, self.changed_configs)
|
||||
elif self._recompute_mode == "none":
|
||||
self._recompute_flag = False
|
||||
new_strategy = copy.deepcopy(self._config.dist_strategy)
|
||||
recompute = new_strategy.recompute
|
||||
recompute.enable = False
|
||||
name = "trial-recompute-none-segments"
|
||||
return Trial(new_strategy, name, self.changed_configs)
|
||||
elif self._recompute_mode == "part":
|
||||
new_no_recompute = self._tuning_segments[: self._trial_idx]
|
||||
new_strategy = copy.deepcopy(self._config.dist_strategy)
|
||||
recompute = new_strategy.recompute
|
||||
recompute.no_recompute_segments.extend(new_no_recompute)
|
||||
name = f"trial-recompute-part-segments-idx{self._trial_idx}"
|
||||
return Trial(new_strategy, name, self.changed_configs)
|
||||
else:
|
||||
return Trial(None, None, None, status=TrialStatus.STOPPED)
|
||||
|
||||
def update(self, results):
|
||||
et = results.get("ErrorType", None)
|
||||
if self._recompute_mode == "all":
|
||||
if et and et == "ResourceExhaustedError":
|
||||
self._trial_idx = self._total_num_trial
|
||||
self._logger.info(
|
||||
"Recompute all candidate segments is failed with OOM, please reduce model size or batch size."
|
||||
)
|
||||
else:
|
||||
self._recompute_mode = "none"
|
||||
elif self._recompute_mode == "none":
|
||||
if et and et == "ResourceExhaustedError":
|
||||
self._recompute_mode = "part"
|
||||
else:
|
||||
self._trial_idx = self._total_num_trial
|
||||
self._logger.info(
|
||||
"Recompute is unnecessary for this model size, which will reduce the Throughput."
|
||||
)
|
||||
else:
|
||||
if self._trail_left >= self._trail_right:
|
||||
self._trial_idx = self._total_num_trial
|
||||
elif et and et == "ResourceExhaustedError":
|
||||
self._trail_left = self._trail_left
|
||||
self._trail_right = self._trial_idx - 1
|
||||
self._trial_idx = int(
|
||||
self._trail_left
|
||||
+ (self._trail_right - self._trail_left) / 2
|
||||
)
|
||||
else:
|
||||
self._trail_left = self._trial_idx + 1
|
||||
self._trail_right = self._trail_right
|
||||
self._trial_idx = int(
|
||||
self._trail_left
|
||||
+ (self._trail_right - self._trail_left) / 2
|
||||
)
|
||||
@@ -0,0 +1,122 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import os
|
||||
|
||||
from ...strategy import Strategy
|
||||
|
||||
_tuning_supported_passes = ["sharding", "recompute"]
|
||||
|
||||
|
||||
def _get_pass_config(strategy, pass_name):
|
||||
config = getattr(strategy, pass_name)
|
||||
return config
|
||||
|
||||
|
||||
class TuningConfig:
|
||||
"""
|
||||
A uniform config wrap:
|
||||
distributed strategy: the user defined configuration for optimization pass
|
||||
tuning config: configuration for the tuning process: mode (profile or cost model), log dir, extra tuning config for optimization like search range for specific
|
||||
"""
|
||||
|
||||
def __init__(self, strategy):
|
||||
if not isinstance(strategy, Strategy):
|
||||
raise TypeError("'strategy' must be object of class `Strategy`.")
|
||||
|
||||
self._tuning_passes_name = set()
|
||||
self._dist_strategy = copy.deepcopy(strategy)
|
||||
self._mode = None
|
||||
self._profile_start_step = None
|
||||
self._profile_end_step = None
|
||||
self._project_dir = None
|
||||
self._max_num_trial = None
|
||||
self._early_stop = None
|
||||
self._debug = None
|
||||
|
||||
self._initialize()
|
||||
|
||||
@property
|
||||
def mode(self):
|
||||
return self._mode
|
||||
|
||||
@property
|
||||
def profile_start_step(self):
|
||||
return self._profile_start_step
|
||||
|
||||
@property
|
||||
def profile_end_step(self):
|
||||
return self._profile_end_step
|
||||
|
||||
@property
|
||||
def project_dir(self):
|
||||
return self._project_dir
|
||||
|
||||
@property
|
||||
def tuning_passes_name(self):
|
||||
return self._tuning_passes_name
|
||||
|
||||
@property
|
||||
def max_num_trial(self):
|
||||
return self._max_num_trial
|
||||
|
||||
@property
|
||||
def early_stop(self):
|
||||
return self._early_stop
|
||||
|
||||
@property
|
||||
def debug(self):
|
||||
return self._debug
|
||||
|
||||
@property
|
||||
def dist_strategy(self):
|
||||
return self._dist_strategy
|
||||
|
||||
# initialize config with user define value or default value
|
||||
def _initialize(self):
|
||||
tuning_strategy = self._dist_strategy.tuning
|
||||
|
||||
self._mode = tuning_strategy.get("mode", "PROFILE")
|
||||
self._profile_start_step = tuning_strategy.get("profile_start_step", 10)
|
||||
self._profile_end_step = tuning_strategy.get("profile_end_step", 30)
|
||||
self._max_num_trial = tuning_strategy.get("max_num_trial", 50)
|
||||
self._early_stop = tuning_strategy.get("early_stop", None)
|
||||
self._debug = tuning_strategy.get("debug", False)
|
||||
|
||||
project_dir = tuning_strategy.get("project_dir", None)
|
||||
if not project_dir:
|
||||
project_dir = os.path.join(os.getcwd(), "OptimizationTuning")
|
||||
self._project_dir = project_dir
|
||||
|
||||
for p in _tuning_supported_passes:
|
||||
if (
|
||||
getattr(self._dist_strategy, p)
|
||||
and _get_pass_config(self._dist_strategy, p).enable_tuning
|
||||
):
|
||||
# TODO distinguish different args of each passes
|
||||
self._tuning_passes_name.add(p)
|
||||
|
||||
p_strategy = getattr(self._dist_strategy, p)
|
||||
self.__dict__[p] = p_strategy
|
||||
|
||||
# # TODO verify the user defined configs
|
||||
# tuning_config_for_pass = tuning_strategy.get(p, None)
|
||||
# if tuning_config_for_pass:
|
||||
# for k, v in tuning_config_for_pass.items():
|
||||
# self.__dict__[p][k] = v
|
||||
|
||||
# (NOTE)tuning config ONLY wraps dist strategy for pass config which is to be tuned
|
||||
def __getattr__(self, item):
|
||||
return getattr(self._dist_strategy, item)
|
||||
@@ -0,0 +1,643 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
|
||||
# import yaml
|
||||
import os
|
||||
import pathlib
|
||||
import pickle
|
||||
import shlex
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.auto_parallel.static.completion import Completer
|
||||
from paddle.distributed.auto_parallel.static.dist_context import (
|
||||
DistributedContext,
|
||||
)
|
||||
from paddle.distributed.auto_parallel.static.partitioner import Partitioner
|
||||
from paddle.distributed.auto_parallel.static.process_group import (
|
||||
clear_all_process_groups,
|
||||
get_all_process_groups,
|
||||
new_process_group,
|
||||
)
|
||||
from paddle.distributed.auto_parallel.static.reshard import Resharder
|
||||
from paddle.distributed.auto_parallel.static.utils import debug_program
|
||||
from paddle.distributed.passes import PassContext, new_pass
|
||||
from paddle.static import append_backward, program_guard
|
||||
|
||||
from ..utils import get_logger
|
||||
from .algorithms import new_algorithm
|
||||
from .config import TuningConfig
|
||||
from .trial import TrialStatus
|
||||
|
||||
|
||||
def _get_new_params_grads(target_program, ref_program, ref_params_grads):
|
||||
ref_block = ref_program.global_block()
|
||||
target_block = target_program.global_block()
|
||||
target_params_grads = []
|
||||
|
||||
for p, g in ref_params_grads:
|
||||
# NOTE grad var might not be generated
|
||||
assert ref_block.has_var(p.name)
|
||||
assert target_block.has_var(p.name)
|
||||
new_p = target_block.var(p.name)
|
||||
if g:
|
||||
new_g = target_block.var(g.name)
|
||||
else:
|
||||
new_g = None
|
||||
|
||||
target_params_grads.append((new_p, new_g))
|
||||
|
||||
return target_params_grads
|
||||
|
||||
|
||||
def _get_new_loss(target_program, ref_program, loss):
|
||||
ref_block = ref_program.global_block()
|
||||
target_block = target_program.global_block()
|
||||
assert ref_block.has_var(loss.name)
|
||||
|
||||
return target_block.var(loss.name)
|
||||
|
||||
|
||||
def parse_process_groups():
|
||||
group_map = {}
|
||||
all_process_groups = get_all_process_groups()
|
||||
for process_group in all_process_groups:
|
||||
group_map[process_group.id] = process_group.ranks
|
||||
return group_map
|
||||
|
||||
|
||||
def get_metric(results):
|
||||
assert isinstance(results, dict), (
|
||||
f"results should be type of dictionary, but got {type(results)}."
|
||||
)
|
||||
if 'Throughput' in results and isinstance(results['Throughput'], float):
|
||||
return float(results['Throughput'])
|
||||
else:
|
||||
return -1.0
|
||||
|
||||
|
||||
def parse_results(results):
|
||||
if results['Throughput'] > 0:
|
||||
return "Throughput: {} step / s.".format(results['Throughput'])
|
||||
et = results.get("ErrorType", None)
|
||||
if et == "ResourceExhaustedError":
|
||||
return "Fail with OOM"
|
||||
else:
|
||||
return "Fail with UNKNOWN ERROR"
|
||||
|
||||
|
||||
# TODO only dependent on dist context
|
||||
# all env need to be start a new pass are member of dist context
|
||||
def _copy_context(ref_dist_context):
|
||||
# clear all process groups and recover the world process group
|
||||
clear_all_process_groups()
|
||||
ranks = []
|
||||
for process_mesh in ref_dist_context._process_meshes:
|
||||
ranks.extend(process_mesh.process_ids)
|
||||
new_process_group(list(set(ranks)))
|
||||
|
||||
new_dist_context = DistributedContext()
|
||||
new_dist_context._serial_main_program = (
|
||||
ref_dist_context.serial_main_program.clone(for_test=False)
|
||||
)
|
||||
new_dist_context._serial_startup_program = (
|
||||
ref_dist_context.serial_startup_program.clone(for_test=False)
|
||||
)
|
||||
|
||||
# mapping variable into new dist context
|
||||
if getattr(ref_dist_context, '_params_grads', None):
|
||||
new_dist_context._params_grads = _get_new_params_grads(
|
||||
new_dist_context.serial_main_program,
|
||||
ref_dist_context.serial_main_program,
|
||||
ref_dist_context._params_grads,
|
||||
)
|
||||
new_dist_context._serial_loss = _get_new_loss(
|
||||
new_dist_context.serial_main_program,
|
||||
ref_dist_context.serial_main_program,
|
||||
ref_dist_context.serial_loss,
|
||||
)
|
||||
|
||||
for key, var_list in ref_dist_context._serial_feed_vars.items():
|
||||
new_var_list = []
|
||||
for var in var_list:
|
||||
block_idx = var.block.idx
|
||||
var_name = var.name
|
||||
var = new_dist_context._serial_main_program.blocks[
|
||||
block_idx
|
||||
]._var_recursive(var_name)
|
||||
new_var_list.append(var)
|
||||
new_dist_context._serial_feed_vars[key] = new_var_list
|
||||
|
||||
for key, var_list in ref_dist_context._serial_fetch_vars.items():
|
||||
new_var_list = []
|
||||
# metrics is a list of list
|
||||
if key == "metrics":
|
||||
for inner_var_list in var_list:
|
||||
new_inner_var_list = []
|
||||
for var in inner_var_list:
|
||||
block_idx = var.block.idx
|
||||
var_name = var.name
|
||||
var = new_dist_context._serial_main_program.blocks[
|
||||
block_idx
|
||||
]._var_recursive(var_name)
|
||||
new_inner_var_list.append(var)
|
||||
new_var_list.append(new_inner_var_list)
|
||||
else:
|
||||
for var in var_list:
|
||||
block_idx = var.block.idx
|
||||
var_name = var.name
|
||||
var = new_dist_context._serial_main_program.blocks[
|
||||
block_idx
|
||||
]._var_recursive(var_name)
|
||||
new_var_list.append(var)
|
||||
new_dist_context._serial_fetch_vars[key] = new_var_list
|
||||
|
||||
# copy information in forward and backward
|
||||
new_dist_context._serial_optimizer = copy.deepcopy(
|
||||
ref_dist_context.serial_optimizer
|
||||
)
|
||||
new_dist_context._dist_tensors_for_program = copy.deepcopy(
|
||||
ref_dist_context._dist_tensors_for_program
|
||||
)
|
||||
new_dist_context._dist_ops_for_program = copy.deepcopy(
|
||||
ref_dist_context._dist_ops_for_program
|
||||
)
|
||||
for pm in ref_dist_context.process_meshes:
|
||||
new_dist_context.add_process_mesh(pm)
|
||||
new_dist_context._dist_op_context = copy.deepcopy(
|
||||
ref_dist_context._dist_op_context
|
||||
)
|
||||
new_dist_context._block_state = copy.deepcopy(ref_dist_context.block_state)
|
||||
|
||||
return new_dist_context
|
||||
|
||||
|
||||
class OptimizationTuner:
|
||||
"""
|
||||
OptimizationTuner is used to manage the tuning procedure of hyper-parameters (configs)
|
||||
of Optimization Pass in AutoParallel.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
dist_context,
|
||||
dataset,
|
||||
inputs_spec,
|
||||
labels_spec,
|
||||
batch_size,
|
||||
rank,
|
||||
):
|
||||
self._config = TuningConfig(dist_context.strategy)
|
||||
# should not modify dist context from calling function
|
||||
self._baseline_dist_context = _copy_context(dist_context)
|
||||
self._baseline_completer = Completer(self._baseline_dist_context)
|
||||
|
||||
self._rank = rank
|
||||
self._inputs_spec = inputs_spec
|
||||
self._labels_spec = labels_spec
|
||||
self._dataset = dataset
|
||||
self._batch_size = batch_size
|
||||
|
||||
self._finished_trials = []
|
||||
self._best_metric = None
|
||||
self._best_iter = float("-inf")
|
||||
|
||||
self._logger = get_logger(logging.INFO)
|
||||
|
||||
self._build_programs_without_optimization()
|
||||
self._select_tuning_algorithm()
|
||||
|
||||
@property
|
||||
def project_dir(self):
|
||||
dirname = self._config.project_dir
|
||||
if not os.path.exists(dirname):
|
||||
if self.rank == 0:
|
||||
pathlib.Path(dirname).mkdir(parents=True, exist_ok=True)
|
||||
return dirname
|
||||
|
||||
@property
|
||||
def rank(self):
|
||||
return self._rank
|
||||
|
||||
@property
|
||||
def device_id(self):
|
||||
return paddle.distributed.ParallelEnv().device_id
|
||||
|
||||
# TODO Generate complete program with all parts like forward, backward, update
|
||||
# as well as parallelism transformation.
|
||||
def _build_programs_without_optimization(self):
|
||||
serial_main_program = self._baseline_dist_context.serial_main_program
|
||||
serial_startup_program = (
|
||||
self._baseline_dist_context.serial_startup_program
|
||||
)
|
||||
serial_loss = self._baseline_dist_context.serial_loss
|
||||
|
||||
with program_guard(serial_main_program, serial_startup_program):
|
||||
params_grads = append_backward(
|
||||
serial_loss,
|
||||
distop_context=self._baseline_dist_context.dist_op_context,
|
||||
)
|
||||
|
||||
self._baseline_completer.complete_backward_annotation(
|
||||
serial_main_program
|
||||
)
|
||||
self._baseline_dist_context.block_state.parse_backward_blocks(
|
||||
serial_main_program
|
||||
)
|
||||
self._baseline_dist_context._params_grads = params_grads
|
||||
|
||||
if self._config.debug:
|
||||
baseline_dir = os.path.join(self.project_dir, "baseline")
|
||||
if not os.path.exists(baseline_dir):
|
||||
pathlib.Path(baseline_dir).mkdir(parents=True, exist_ok=True)
|
||||
debug_program(
|
||||
self._baseline_dist_context._serial_main_program,
|
||||
baseline_dir,
|
||||
"main",
|
||||
)
|
||||
debug_program(
|
||||
self._baseline_dist_context._serial_startup_program,
|
||||
baseline_dir,
|
||||
"startup",
|
||||
)
|
||||
|
||||
def _select_tuning_algorithm(self):
|
||||
selected_passes_set = self._config.tuning_passes_name
|
||||
algorithm_name = "_".join(sorted(selected_passes_set))
|
||||
self._algorithm = new_algorithm(algorithm_name, self._config)
|
||||
|
||||
def _apply_optimization(self, trial):
|
||||
new_strategy = trial.space
|
||||
dist_context = _copy_context(self._baseline_dist_context)
|
||||
pass_context = PassContext()
|
||||
completer = Completer(dist_context)
|
||||
|
||||
main_program = dist_context.serial_main_program
|
||||
startup_program = dist_context.serial_startup_program
|
||||
|
||||
# applying optimization pass
|
||||
if new_strategy.amp.enable:
|
||||
config = copy.deepcopy(new_strategy.amp.to_dict())
|
||||
config["dist_context"] = dist_context
|
||||
config["params_grads"] = dist_context._params_grads
|
||||
# TODO AMP Pass should not use loss var
|
||||
config["loss"] = dist_context.serial_loss
|
||||
config["input_data"] = (
|
||||
self._baseline_dist_context.serial_feed_vars["inputs"]
|
||||
+ self._baseline_dist_context.serial_feed_vars["labels"]
|
||||
)
|
||||
if config["dtype"] == "float16" and config["level"] == "o2":
|
||||
config["base_opt"] = dist_context.serial_optimizer
|
||||
auto_parallel_fp16_pass = new_pass("auto_parallel_fp16", config)
|
||||
auto_parallel_fp16_pass.apply(
|
||||
[main_program], [startup_program], pass_context
|
||||
)
|
||||
dist_context._serial_loss = auto_parallel_fp16_pass.get_loss()
|
||||
else:
|
||||
auto_parallel_amp_pass = new_pass("auto_parallel_amp", config)
|
||||
auto_parallel_amp_pass.apply(
|
||||
[main_program], [startup_program], pass_context
|
||||
)
|
||||
dist_context._serial_loss = auto_parallel_amp_pass.get_loss()
|
||||
|
||||
if new_strategy.recompute.enable:
|
||||
config = copy.deepcopy(new_strategy.recompute.to_dict())
|
||||
config["dist_context"] = dist_context
|
||||
config["no_grad_set"] = None
|
||||
config["loss"] = dist_context.serial_loss
|
||||
auto_parallel_recompute_pass = new_pass(
|
||||
"auto_parallel_recompute", config
|
||||
)
|
||||
auto_parallel_recompute_pass.apply(
|
||||
[main_program], [startup_program], pass_context
|
||||
)
|
||||
|
||||
# Do logical partition
|
||||
partitioner = Partitioner(dist_context, self.rank)
|
||||
(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
dist_params_grads,
|
||||
) = partitioner.partition(
|
||||
main_program, startup_program, dist_context._params_grads
|
||||
)
|
||||
|
||||
# Generate optimizer
|
||||
# FIXME should be remove from apply pass after pass support optimizers
|
||||
with (
|
||||
program_guard(dist_main_prog, dist_startup_prog),
|
||||
dist_main_prog.switch_name_generator_guard("opt_"),
|
||||
):
|
||||
optimizer_ops = dist_context.serial_optimizer.apply_gradients(
|
||||
dist_params_grads
|
||||
)
|
||||
completer.complete_update_annotation(dist_main_prog)
|
||||
|
||||
resharder = Resharder(
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
self.rank,
|
||||
dist_context,
|
||||
dist_params_grads,
|
||||
)
|
||||
resharder.reshard()
|
||||
|
||||
config = {}
|
||||
config["dist_context"] = dist_context
|
||||
config["global_rank"] = self.rank
|
||||
config["use_sharding"] = new_strategy.sharding.enable
|
||||
dp_pass = new_pass("auto_parallel_data_parallel_optimization", config)
|
||||
dp_pass.apply([dist_main_prog], [dist_startup_prog], pass_context)
|
||||
|
||||
if new_strategy.sharding.enable:
|
||||
config = copy.deepcopy(new_strategy.sharding.to_dict())
|
||||
config["dist_context"] = dist_context
|
||||
config["params_grads"] = dist_params_grads
|
||||
config["global_rank"] = self.rank
|
||||
auto_parallel_sharding_pass = new_pass(
|
||||
"auto_parallel_sharding", config
|
||||
)
|
||||
auto_parallel_sharding_pass.apply(
|
||||
[dist_main_prog], [dist_startup_prog], pass_context
|
||||
)
|
||||
dist_params_grads = pass_context.get_attr("params_grads")
|
||||
|
||||
# gradient clip
|
||||
config = copy.deepcopy(new_strategy.sharding.to_dict())
|
||||
config["dist_context"] = dist_context
|
||||
config["params_grads"] = dist_params_grads
|
||||
config["rank_id"] = self.rank
|
||||
auto_parallel_clip_pass = new_pass("auto_parallel_grad_clip", config)
|
||||
auto_parallel_clip_pass.apply(
|
||||
[dist_main_prog], [dist_startup_prog], pass_context
|
||||
)
|
||||
|
||||
if new_strategy.gradient_merge.enable:
|
||||
config = copy.deepcopy(new_strategy.gradient_merge.to_dict())
|
||||
config["dist_context"] = dist_context
|
||||
config["params_grads"] = dist_params_grads
|
||||
auto_parallel_gradient_merge_pass = new_pass(
|
||||
"auto_parallel_gradient_merge_pass", config
|
||||
)
|
||||
auto_parallel_gradient_merge_pass.apply(
|
||||
[dist_main_prog], [dist_startup_prog], pass_context
|
||||
)
|
||||
trial.main_program, trial.startup_program = (
|
||||
dist_main_prog,
|
||||
dist_startup_prog,
|
||||
)
|
||||
return trial
|
||||
|
||||
def _get_profile_context(self, trial, result_path):
|
||||
profile_ctx = {}
|
||||
|
||||
profile_ctx['distributed_env'] = copy.deepcopy(
|
||||
paddle.distributed.ParallelEnv()
|
||||
)
|
||||
profile_ctx['group_map'] = parse_process_groups()
|
||||
profile_ctx["loss_var_name"] = (
|
||||
self._baseline_dist_context.serial_loss.name
|
||||
)
|
||||
profile_ctx["main_program_decs"] = (
|
||||
trial.main_program.desc.serialize_to_string()
|
||||
)
|
||||
profile_ctx["startup_program_decs"] = (
|
||||
trial.startup_program.desc.serialize_to_string()
|
||||
)
|
||||
self._dataset.batch_size = self._batch_size
|
||||
self._dataset.input_names = self._get_input_names()
|
||||
|
||||
profile_ctx["dataset"] = self._dataset
|
||||
profile_ctx["result_filename"] = result_path
|
||||
|
||||
return profile_ctx
|
||||
|
||||
def _get_input_names(self):
|
||||
input_names = []
|
||||
for input_spec in self._inputs_spec[:] + self._labels_spec[:]:
|
||||
input_names.append(input_spec.name)
|
||||
return input_names
|
||||
|
||||
def _launch_profile(self, ctx_path, trial_dir):
|
||||
if os.environ.get("WITH_COVERAGE", "OFF") == "ON":
|
||||
coverage_args = ["-m", "coverage", "run", "--branch", "-p"]
|
||||
else:
|
||||
coverage_args = []
|
||||
|
||||
profile_args = " ".join(
|
||||
[
|
||||
"--rank",
|
||||
str(self.rank),
|
||||
"--device_id",
|
||||
str(self.device_id),
|
||||
"--ctx_filename",
|
||||
ctx_path,
|
||||
"--profile_start_step",
|
||||
str(self._config.profile_start_step),
|
||||
"--profile_end_step",
|
||||
str(self._config.profile_end_step),
|
||||
]
|
||||
)
|
||||
cmd_args = (
|
||||
"-m paddle.distributed.auto_parallel.static.tuner.profiler"
|
||||
+ " "
|
||||
+ profile_args
|
||||
)
|
||||
cmd = [sys.executable, "-u", *coverage_args, *shlex.split(cmd_args)]
|
||||
|
||||
parent_env = copy.copy(os.environ.copy())
|
||||
# env flags need for profile
|
||||
new_env = {}
|
||||
new_env.update(parent_env)
|
||||
|
||||
# TODO if any rank hang or fail, kill all processes
|
||||
self._logger.debug("Executing cmd:\n{} .".format(" ".join(cmd)))
|
||||
# new_process = subprocess.Popen(cmd, env=new_env)
|
||||
with (
|
||||
open(
|
||||
os.path.join(trial_dir, "stdout.log" + str(self.rank)), "wb"
|
||||
) as out,
|
||||
open(
|
||||
os.path.join(trial_dir, "stderr.log" + str(self.rank)), "wb"
|
||||
) as err,
|
||||
):
|
||||
result = subprocess.Popen(cmd, stdout=out, stderr=err, env=new_env)
|
||||
result.wait()
|
||||
out.flush()
|
||||
err.flush()
|
||||
os.fsync(out)
|
||||
os.fsync(err)
|
||||
|
||||
def _profile_trial(self, trial):
|
||||
# Making working directory
|
||||
trial_dir = self._get_trial_dir(trial)
|
||||
if not os.path.exists(trial_dir):
|
||||
if self.rank == 0:
|
||||
pathlib.Path(trial_dir).mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
while not os.path.exists(trial_dir):
|
||||
pass
|
||||
ctx_filename = "profile_ctx." + str(self.rank)
|
||||
ctx_path = os.path.join(trial_dir, ctx_filename)
|
||||
result_path = os.path.join(trial_dir, "result.json")
|
||||
|
||||
# Prepare Profile Context
|
||||
profile_ctx = self._get_profile_context(trial, result_path)
|
||||
with open(ctx_path, 'wb') as f:
|
||||
pickle.dump(profile_ctx, f, protocol=4)
|
||||
|
||||
if self._config.debug:
|
||||
debug_program(trial.main_program, trial_dir, "main_program")
|
||||
debug_program(trial.startup_program, trial_dir, "startup_program")
|
||||
|
||||
# Run
|
||||
self._launch_profile(ctx_path, trial_dir)
|
||||
|
||||
# Load results
|
||||
try:
|
||||
with open(result_path, 'r') as fp:
|
||||
results = json.load(fp)
|
||||
return results
|
||||
except FileNotFoundError:
|
||||
Error_results = {"Throughput": -1, "ErrorType": 'FatalError'}
|
||||
return Error_results
|
||||
|
||||
def _evaluate_trial(self, trial):
|
||||
self._logger.info(f"Trial {trial.name} evaluation start.")
|
||||
self._apply_optimization(trial)
|
||||
|
||||
if self._config.mode == "PROFILE":
|
||||
results = self._profile_trial(trial)
|
||||
|
||||
elif self._config.mode == "COSTMODEL":
|
||||
raise NotImplementedError(
|
||||
"COSTMODEL mode for optimization tuning is not supported yet!"
|
||||
)
|
||||
else:
|
||||
raise NotImplementedError(
|
||||
f"invalid evaluation mode: {self._config.mode}"
|
||||
)
|
||||
|
||||
self._logger.info(
|
||||
f"Trial {trial.name} evaluation finish with {parse_results(results)}."
|
||||
)
|
||||
return results
|
||||
|
||||
def _update(self, i, trial, results):
|
||||
self._finished_trials.append(trial)
|
||||
|
||||
cur_metric = get_metric(results)
|
||||
if self._best_metric is None or cur_metric > self._best_metric:
|
||||
self._best_metric = cur_metric
|
||||
self._best_iter = i
|
||||
|
||||
def _get_trial_dir(self, trial):
|
||||
return os.path.join(self.project_dir, trial.name)
|
||||
|
||||
def get_best_config(self):
|
||||
"""
|
||||
Return the best optimization configuration found in the tuning.
|
||||
|
||||
Returns:
|
||||
A object of fleet.DistributedStrategy with best configuration.
|
||||
"""
|
||||
assert self._best_iter >= 0, "The best configuration is not found yet !"
|
||||
best_trial = self._finished_trials[self._best_iter]
|
||||
return self._algorithm.get_config_from_trial(best_trial)
|
||||
|
||||
def summary(self):
|
||||
"""
|
||||
Display tuning result summary.
|
||||
"""
|
||||
# TODO summary with the trial_name with metric_of_trial
|
||||
best_trial = self._finished_trials[self._best_iter]
|
||||
summary_ = f"""
|
||||
Tuning Result Summary
|
||||
Run total {len(self._finished_trials)} trials with {(time.time() - self._tuning_start_time) / 60} min.
|
||||
The best trial is: [{best_trial.name}], whose configuration is following:
|
||||
"""
|
||||
summary_ += "\n" + best_trial.summary() + "\n"
|
||||
self._logger.info(summary_)
|
||||
with open(os.path.join(self.project_dir, "summary.txt"), "w+") as fw:
|
||||
fw.writelines(line + "\n" for line in summary_.split("\n"))
|
||||
|
||||
# full_strategy = self.get_best_config()
|
||||
# path = os.path.join(self.project_dir, "tuned_dist_strategy.yaml")
|
||||
# with open(path, 'w') as outfile:
|
||||
# yaml.dump(full_strategy, outfile, default_flow_style=False)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Clear the temporary file generated in tuning procedure.
|
||||
"""
|
||||
# TODO clear up zombie process created by tuning
|
||||
if not self._config.debug:
|
||||
for trial in self._finished_trials:
|
||||
trial_dir = self._get_trial_dir(trial)
|
||||
shutil.rmtree(trial_dir, ignore_errors=True)
|
||||
|
||||
def tune(self):
|
||||
"""
|
||||
Performs the search for best hyperparameter configurations
|
||||
for the selected optimization pass(es).
|
||||
"""
|
||||
|
||||
# step1: collect model info which might be used for
|
||||
# pruning the search space of the algorithm
|
||||
self._tuning_start_time = time.time()
|
||||
self._algorithm.collect_model_info(
|
||||
self._baseline_dist_context.serial_main_program,
|
||||
self._baseline_dist_context.serial_startup_program,
|
||||
)
|
||||
|
||||
# main search loop
|
||||
i = 0
|
||||
while i < self._config.max_num_trial:
|
||||
# step2: create a new trial
|
||||
trial = self._algorithm.next_trial()
|
||||
|
||||
if trial.status == TrialStatus.STOPPED:
|
||||
break
|
||||
|
||||
# step3: evaluate the trial
|
||||
results = self._evaluate_trial(trial)
|
||||
|
||||
# step4: update the algorithm with last result,
|
||||
# which could be used by algorithm to pruning the
|
||||
# remaining search space.
|
||||
self._algorithm.update(results)
|
||||
self._update(i, trial, results)
|
||||
|
||||
# early stop
|
||||
i += 1
|
||||
if (
|
||||
self._config.early_stop
|
||||
and self._config.early_stop <= i - self._best_iter
|
||||
):
|
||||
self._logger.info(
|
||||
f"Early stop the Tuning since there is no better trial found within [{self._config.early_stop}] trials"
|
||||
)
|
||||
break
|
||||
|
||||
# step5: summary the best config and return
|
||||
self.summary()
|
||||
|
||||
self.clear()
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,296 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import paddle
|
||||
from paddle.distributed.auto_parallel.static.dist_loader import (
|
||||
DistributedDataLoaderFromGenerator,
|
||||
)
|
||||
from paddle.distributed.auto_parallel.static.process_group import (
|
||||
get_all_process_groups,
|
||||
new_process_group,
|
||||
)
|
||||
from paddle.distributed.collective import _get_global_env
|
||||
from paddle.framework import Program, _current_expected_place
|
||||
from paddle.static import Operator
|
||||
|
||||
paddle.enable_static()
|
||||
|
||||
|
||||
def str2bool(v):
|
||||
if v.lower() in ('yes', 'true', 't', 'y', '1'):
|
||||
return True
|
||||
elif v.lower() in ('no', 'false', 'f', 'n', '0'):
|
||||
return False
|
||||
else:
|
||||
raise argparse.ArgumentTypeError('Unsupported value encountered.')
|
||||
|
||||
|
||||
def parse_args():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument(
|
||||
"--profile_start_step",
|
||||
default=10,
|
||||
type=int,
|
||||
help="integer indicates the warmup step before starting profile.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--profile_end_step",
|
||||
default=30,
|
||||
type=int,
|
||||
help="integer indicates at the end step of profile.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--rank",
|
||||
type=int,
|
||||
required=True,
|
||||
help="the rank id of the this process.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--device_id",
|
||||
type=int,
|
||||
required=True,
|
||||
help="the device id of the this process.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--ctx_filename",
|
||||
type=str,
|
||||
required=True,
|
||||
help="the filename to the profile context file saved by optimization tuner",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
return args
|
||||
|
||||
|
||||
def init_process_groups(group_map, rank):
|
||||
for group_id, ranks in group_map.items():
|
||||
if group_id == 0:
|
||||
continue
|
||||
new_process_group(ranks=ranks, group_id=group_id)
|
||||
|
||||
# TODO should instantiate global group first
|
||||
all_process_groups = get_all_process_groups()
|
||||
for process_group in all_process_groups:
|
||||
print(process_group)
|
||||
process_group.instantiate()
|
||||
|
||||
|
||||
def get_cpp_error_type(error):
|
||||
msg = str(error).splitlines()
|
||||
cpp_error_types = [
|
||||
'InvalidArgumentError',
|
||||
'NotFoundError',
|
||||
'OutOfRangeError',
|
||||
'AlreadyExistsError',
|
||||
'ResourceExhaustedError',
|
||||
'PreconditionNotMetError',
|
||||
'PermissionDeniedError',
|
||||
'ExecutionTimeoutError',
|
||||
'UnimplementedError',
|
||||
'UnavailableError',
|
||||
'FatalError',
|
||||
'ExternalError',
|
||||
]
|
||||
error_type = 'FatalError'
|
||||
for et in cpp_error_types:
|
||||
for line in msg:
|
||||
if et in line:
|
||||
return et
|
||||
return error_type
|
||||
|
||||
|
||||
def create_dataloader(
|
||||
main_program, startup_program, profile_ctx, epochs=1, steps_per_epoch=None
|
||||
):
|
||||
dataset = profile_ctx["dataset"]
|
||||
main_block = main_program.global_block()
|
||||
feed_list = []
|
||||
for name in dataset.input_names:
|
||||
if name in main_block.vars:
|
||||
feed_list.append(main_block.vars[name])
|
||||
|
||||
# remove the first three ops if multi run fit/evaluate/predict
|
||||
op_size = len(main_block.ops)
|
||||
if main_block.ops[0].type == 'create_py_reader':
|
||||
op_size -= 3
|
||||
for _ in range(3):
|
||||
main_block._remove_op(0, sync=False)
|
||||
|
||||
# insert read op at the end of program
|
||||
places = paddle.static.cuda_places()
|
||||
with paddle.static.program_guard(main_program, startup_program):
|
||||
dataloader = DistributedDataLoaderFromGenerator(
|
||||
dataset=dataset,
|
||||
feed_list=feed_list,
|
||||
capacity=70,
|
||||
places=places,
|
||||
batch_size=dataset.batch_size,
|
||||
epochs=epochs,
|
||||
steps_per_epoch=steps_per_epoch,
|
||||
data_parallel_world_size=dataset.dp_world_size,
|
||||
data_parallel_rank=dataset.dp_rank,
|
||||
)
|
||||
|
||||
# move read op from the end of program to the start of program
|
||||
new_op_size = len(main_block.ops)
|
||||
for _ in range(new_op_size - 1, op_size - 1, -1):
|
||||
op = main_block.ops[new_op_size - 1]
|
||||
new_op_desc = main_block.desc._prepend_op()
|
||||
new_op_desc.copy_from(op.desc)
|
||||
new_op = Operator(main_block, new_op_desc, type=new_op_desc.type())
|
||||
main_block.ops.insert(0, new_op)
|
||||
for _ in range(new_op_size - op_size):
|
||||
main_block._remove_op(new_op_size, sync=False)
|
||||
main_block._sync_with_cpp()
|
||||
return dataloader
|
||||
|
||||
|
||||
def init_comm(profile_ctx):
|
||||
# override the env for current process
|
||||
dist_env = profile_ctx['distributed_env']
|
||||
genv = _get_global_env()
|
||||
genv = dist_env
|
||||
print(
|
||||
f"current process rank: {genv.rank}, device_id: {genv.device_id}, ip: {genv.current_endpoint}."
|
||||
)
|
||||
|
||||
# init nccl comm
|
||||
group_map = profile_ctx['group_map']
|
||||
init_process_groups(group_map, args.rank)
|
||||
|
||||
|
||||
def load_programs(profile_ctx):
|
||||
main_program_desc_str = profile_ctx['main_program_decs']
|
||||
main_program = Program.parse_from_string(main_program_desc_str)
|
||||
|
||||
startup_program_decs_str = profile_ctx['startup_program_decs']
|
||||
startup_program = Program.parse_from_string(startup_program_decs_str)
|
||||
|
||||
loss_var_name = profile_ctx["loss_var_name"]
|
||||
assert main_program.global_block().has_var(loss_var_name)
|
||||
loss_var = main_program.global_block().var(loss_var_name)
|
||||
|
||||
return main_program, startup_program, loss_var
|
||||
|
||||
|
||||
def get_executor():
|
||||
place_type = _current_expected_place()
|
||||
if not isinstance(place_type, paddle.CUDAPlace):
|
||||
raise RuntimeError("OptimizationTuner only support CUDA GPU right now.")
|
||||
|
||||
genv = _get_global_env()
|
||||
place = paddle.CUDAPlace(genv.device_id)
|
||||
exe = paddle.static.Executor(place)
|
||||
return exe
|
||||
|
||||
|
||||
def profiler(args):
|
||||
"""
|
||||
main function to profile experiment for each pass hyper-parameter.
|
||||
"""
|
||||
# load ctx
|
||||
if not os.path.isfile(args.ctx_filename):
|
||||
raise ValueError(
|
||||
f"There is no profile context named {args.ctx_filename}."
|
||||
)
|
||||
with open(args.ctx_filename, 'rb') as f:
|
||||
from paddle.framework.restricted_unpickler import safe_load_pickle
|
||||
|
||||
profile_ctx = safe_load_pickle(f, encoding='latin1')
|
||||
|
||||
init_comm(profile_ctx)
|
||||
|
||||
main_program, startup_program, loss_var = load_programs(profile_ctx)
|
||||
|
||||
data_loader = create_dataloader(main_program, startup_program, profile_ctx)
|
||||
|
||||
result_path = profile_ctx["result_filename"]
|
||||
|
||||
exe = get_executor()
|
||||
|
||||
try:
|
||||
exe.run(startup_program)
|
||||
# profile main
|
||||
duration = 0
|
||||
eval_step = 0
|
||||
data_loader._inner_dataloader.start()
|
||||
while eval_step < args.profile_end_step:
|
||||
start_time = time.time()
|
||||
|
||||
loss = exe.run(
|
||||
main_program,
|
||||
fetch_list=[loss_var],
|
||||
use_program_cache=True,
|
||||
)
|
||||
|
||||
end_time = time.time()
|
||||
|
||||
if eval_step >= args.profile_start_step:
|
||||
duration += end_time - start_time
|
||||
|
||||
print(f"step: {eval_step}, loss_print: {loss[0]:f}")
|
||||
eval_step += 1
|
||||
|
||||
avg_tput = (
|
||||
1.0 * (args.profile_end_step - args.profile_start_step) / duration
|
||||
)
|
||||
|
||||
result_dict = {
|
||||
"Throughput": avg_tput,
|
||||
"ErrorType": None,
|
||||
}
|
||||
|
||||
if paddle.distributed.get_rank() == 0:
|
||||
with open(result_path, 'w') as fp:
|
||||
json.dump(result_dict, fp)
|
||||
|
||||
print(f"profile done! avg speed : {avg_tput} step / s.")
|
||||
|
||||
except paddle.framework.core.EOFException:
|
||||
data_loader._inner_dataloader.reset()
|
||||
|
||||
except Exception as e:
|
||||
error_type = get_cpp_error_type(e)
|
||||
result_dict = {
|
||||
"Throughput": -1,
|
||||
"ErrorType": error_type,
|
||||
}
|
||||
if not os.path.isfile(result_path):
|
||||
with open(result_path, 'w') as fp:
|
||||
json.dump(result_dict, fp)
|
||||
|
||||
print(f"profile failed with error: [{error_type}]")
|
||||
print(e)
|
||||
print(traceback.format_exc())
|
||||
|
||||
data_loader._inner_dataloader.reset()
|
||||
del data_loader._inner_dataloader
|
||||
sys.exit(1)
|
||||
|
||||
data_loader._inner_dataloader.reset()
|
||||
del data_loader._inner_dataloader
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
paddle.framework.set_flags({'FLAGS_new_executor_sequential_run': 1})
|
||||
args = parse_args()
|
||||
profiler(args)
|
||||
@@ -0,0 +1,216 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Notice that the following codes are modified from KerasTuner for a different purpose.
|
||||
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/metrics_tracking.py.
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class MetricRecord:
|
||||
"""
|
||||
One record for a single metric at a given execution step.
|
||||
"""
|
||||
|
||||
def __init__(self, value, step):
|
||||
self._value = value
|
||||
self._step = step
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self._value
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
self._value = value
|
||||
|
||||
@property
|
||||
def step(self):
|
||||
return self._step
|
||||
|
||||
@step.setter
|
||||
def step(self, step):
|
||||
self._step = step
|
||||
|
||||
def mean(self):
|
||||
return np.mean(self.value)
|
||||
|
||||
def get_state(self):
|
||||
return {"value": self.value, "step": self.step}
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
return cls(**state)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, MetricRecord):
|
||||
return False
|
||||
return other.value == self.value and other.step == self.step
|
||||
|
||||
def __repr__(self):
|
||||
return f"MetricRecord(value={self.value}, step={self.step})"
|
||||
|
||||
|
||||
class MetricRecords:
|
||||
"""
|
||||
Records of a single metric across different executions.
|
||||
"""
|
||||
|
||||
def __init__(self, direction="min"):
|
||||
if direction not in {"min", "max"}:
|
||||
raise ValueError(
|
||||
f"direction should be one of {{min, max}}, but got: {direction}."
|
||||
)
|
||||
self._direction = direction
|
||||
self._records = {}
|
||||
|
||||
@property
|
||||
def records(self):
|
||||
return sorted(self._records.values(), key=lambda r: r.step)
|
||||
|
||||
@records.setter
|
||||
def records(self, records):
|
||||
for r in records:
|
||||
self.update(r.value, step=r.step)
|
||||
|
||||
@property
|
||||
def direction(self):
|
||||
return self._direction
|
||||
|
||||
@direction.setter
|
||||
def direction(self, direction):
|
||||
self._direction = direction
|
||||
|
||||
def update(self, value, step=0):
|
||||
if step in self._records:
|
||||
self._records[step].set_value(value)
|
||||
else:
|
||||
self._records[step] = MetricRecord(value, step=step)
|
||||
|
||||
def get_best_value(self):
|
||||
values = [r.mean() for r in self._records.values()]
|
||||
if not values:
|
||||
return None
|
||||
if self._direction == "min":
|
||||
return np.nanmin(values)
|
||||
return np.nanmax(values)
|
||||
|
||||
def get_best_step(self):
|
||||
best_value = self.get_best_value()
|
||||
if best_value is None:
|
||||
return None
|
||||
for r in self._records.values():
|
||||
if r.mean() == best_value:
|
||||
return r.step
|
||||
|
||||
def get_statistics(self):
|
||||
records = self.records
|
||||
records_values = [r.mean() for r in records]
|
||||
if not len(records_values):
|
||||
return {}
|
||||
return {
|
||||
"min": float(np.nanmin(records_values)),
|
||||
"max": float(np.nanmax(records_values)),
|
||||
"mean": float(np.nanmean(records_values)),
|
||||
"median": float(np.nanmedian(records_values)),
|
||||
"var": float(np.nanvar(records_values)),
|
||||
"std": float(np.nanstd(records_values)),
|
||||
}
|
||||
|
||||
def get_state(self):
|
||||
state = {}
|
||||
state["direction"] = self._direction
|
||||
state["records"] = [r.get_state() for r in self.records]
|
||||
return state
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
records = cls(state["direction"])
|
||||
records.records = [MetricRecord.from_state(r) for r in state["records"]]
|
||||
return records
|
||||
|
||||
|
||||
class MetricsRecorder:
|
||||
"""
|
||||
Record the values for all metrics.
|
||||
"""
|
||||
|
||||
def __init__(self, metrics=None):
|
||||
self._records = {}
|
||||
self.register_metrics(metrics)
|
||||
|
||||
@property
|
||||
def records(self):
|
||||
return self._records
|
||||
|
||||
def exists(self, name):
|
||||
return name in self._records
|
||||
|
||||
def register_metrics(self, metrics=None):
|
||||
metrics = metrics or []
|
||||
for metric in metrics:
|
||||
self.register(metric.name)
|
||||
|
||||
def register(self, name, direction=None):
|
||||
if self.exists(name):
|
||||
raise ValueError(f"Metric {name} have been registered.")
|
||||
if direction is None:
|
||||
direction = "min"
|
||||
self._records[name] = MetricRecords(direction)
|
||||
|
||||
def update(self, name, value, step=0):
|
||||
value = float(value)
|
||||
if not self.exists(name):
|
||||
self.register(name)
|
||||
|
||||
prev_best = self._records[name].get_best_value()
|
||||
self._records[name].update(value, step=step)
|
||||
new_best = self._records[name].get_best_value()
|
||||
|
||||
improved = new_best != prev_best
|
||||
return improved
|
||||
|
||||
def get_records(self, name):
|
||||
return self._records[name].records
|
||||
|
||||
def set_records(self, name, records):
|
||||
if not self.exists(name):
|
||||
self.register(name)
|
||||
self._records[name].records = records
|
||||
|
||||
def get_best_value(self, name):
|
||||
return self._records[name].get_best_value()
|
||||
|
||||
def get_best_step(self, name):
|
||||
return self._records[name].get_best_step()
|
||||
|
||||
def get_statistics(self, name):
|
||||
return self._records[name].get_statistics()
|
||||
|
||||
def get_state(self):
|
||||
return {
|
||||
"metrics": {
|
||||
name: metric_records.get_state()
|
||||
for name, metric_records in self._records.items()
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
recorder = cls()
|
||||
recorder._records = {
|
||||
name: MetricRecords.from_state(metric_records)
|
||||
for name, metric_records in state["metrics"].items()
|
||||
}
|
||||
return recorder
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,42 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Notice that the following codes are modified from KerasTuner for a different purpose.
|
||||
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/metrics_tracking.py.
|
||||
|
||||
import json
|
||||
|
||||
|
||||
class Storable:
|
||||
def get_state(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def set_state(self, state):
|
||||
raise NotImplementedError
|
||||
|
||||
def save(self, path):
|
||||
state = self.get_state()
|
||||
state_json = json.dumps(state)
|
||||
try:
|
||||
with open(path, "w") as f:
|
||||
f.write(state_json)
|
||||
return str(path)
|
||||
except OSError as e:
|
||||
raise OSError(f"Failed to save file at {path}: {e}") from e
|
||||
|
||||
def load(self, path):
|
||||
with open(path, "r") as f:
|
||||
state_data = f.read()
|
||||
state = json.loads(state_data)
|
||||
self.set_state(state)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,169 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Notice that the following codes are modified from KerasTuner to implement our own tuner.
|
||||
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/trial.py.
|
||||
|
||||
import hashlib
|
||||
import random
|
||||
import time
|
||||
|
||||
from .recorder import MetricsRecorder
|
||||
from .storable import Storable
|
||||
from .tunable_space import TunableSpace
|
||||
|
||||
|
||||
class TrialStatus:
|
||||
RUNNING = "RUNNING"
|
||||
COMPLETED = "COMPLETED"
|
||||
STOPPED = "STOPPED"
|
||||
INVALID = "INVALID"
|
||||
|
||||
|
||||
class Trial(Storable):
|
||||
def __init__(
|
||||
self, tunable_space, trial_id=None, status=TrialStatus.RUNNING
|
||||
):
|
||||
self._id = _generate_trial_id() if trial_id is None else trial_id
|
||||
self._space = tunable_space
|
||||
self._recorder = MetricsRecorder()
|
||||
self._score = None
|
||||
self._best_step = None
|
||||
self._status = status
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return self._id
|
||||
|
||||
@property
|
||||
def space(self):
|
||||
return self._space
|
||||
|
||||
@property
|
||||
def recorder(self):
|
||||
return self._recorder
|
||||
|
||||
@property
|
||||
def score(self):
|
||||
return self._score
|
||||
|
||||
@score.setter
|
||||
def score(self, score):
|
||||
self._score = score
|
||||
|
||||
@property
|
||||
def best_step(self):
|
||||
return self._best_step
|
||||
|
||||
@best_step.setter
|
||||
def best_step(self, best_step):
|
||||
self._best_step = best_step
|
||||
|
||||
@property
|
||||
def status(self):
|
||||
return self._status
|
||||
|
||||
@status.setter
|
||||
def status(self, status):
|
||||
self._status = status
|
||||
|
||||
def summary(self):
|
||||
print("Tunable space:")
|
||||
if self.space.values:
|
||||
for tv, value in self.space.values.items():
|
||||
print(tv + ":", value)
|
||||
|
||||
if self.score is not None:
|
||||
print(f"Score: {self.score}")
|
||||
|
||||
def get_state(self):
|
||||
return {
|
||||
"id": self.id,
|
||||
"space": self.space.get_state(),
|
||||
"recorder": self.recorder.get_state(),
|
||||
"score": self.score,
|
||||
"best_step": self.best_step,
|
||||
"status": self.status,
|
||||
}
|
||||
|
||||
def set_state(self, state):
|
||||
self._id = state["id"]
|
||||
self._space = TunableSpace.from_state(state["space"])
|
||||
self._recorder = MetricsRecorder.from_state(state["recorder"])
|
||||
self._score = state["score"]
|
||||
self._best_step = state["best_step"]
|
||||
self._status = state["status"]
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
trial = cls(tunable_space=None)
|
||||
trial.set_state(state)
|
||||
return trial
|
||||
|
||||
|
||||
class OptimizationTunerTrial(Trial):
|
||||
def __init__(
|
||||
self,
|
||||
config,
|
||||
name,
|
||||
changed_configs,
|
||||
trial_id=None,
|
||||
status=TrialStatus.RUNNING,
|
||||
):
|
||||
super().__init__(config, trial_id, status)
|
||||
self._name = name
|
||||
self._changed_configs = changed_configs
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return self._name
|
||||
|
||||
def summary(self):
|
||||
spacing = 2
|
||||
max_k = 38
|
||||
max_v = 38
|
||||
|
||||
length = max_k + max_v + spacing
|
||||
|
||||
h1_format = " " + f"|{{:^{length}s}}|\n"
|
||||
h2_format = " " + "|{{:>{}s}}{}{{:^{}s}}|\n".format(
|
||||
max_k, " " * spacing, max_v
|
||||
)
|
||||
|
||||
border = " +" + "".join(["="] * length) + "+"
|
||||
line = " +" + "".join(["-"] * length) + "+"
|
||||
|
||||
draws = border + "\n"
|
||||
draws += h1_format.format("")
|
||||
draws += h1_format.format("Tuned Configurations Overview")
|
||||
draws += h1_format.format("")
|
||||
|
||||
for name in self._changed_configs:
|
||||
draws += border + "\n"
|
||||
draws += h1_format.format(f"{name} auto=True <-> {name}")
|
||||
draws += line + "\n"
|
||||
my_configs = getattr(self.space, name)
|
||||
keys = my_configs.to_dict().keys()
|
||||
for key in keys:
|
||||
draws += h2_format.format(
|
||||
key, str(my_configs.to_dict().get(key, None))
|
||||
)
|
||||
|
||||
result_res = draws + border
|
||||
return result_res
|
||||
|
||||
|
||||
def _generate_trial_id():
|
||||
s = str(time.time()) + str(random.randint(1, int(1e7)))
|
||||
return hashlib.sha256(s.encode("utf-8")).hexdigest()[:32]
|
||||
@@ -0,0 +1,156 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Notice that the following codes are modified from KerasTuner to implement our own tuner.
|
||||
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/hyperparameters.py.
|
||||
|
||||
from .tunable_variable import Boolean, Choice, Fixed, FloatRange, IntRange
|
||||
|
||||
|
||||
class TunableSpace:
|
||||
"""
|
||||
A TunableSpace is constructed by the tunable variables.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
# Tunable variables for this tunable variables
|
||||
self._variables = {}
|
||||
# Specific values corresponding to each tunable variable
|
||||
self._values = {}
|
||||
|
||||
@property
|
||||
def variables(self):
|
||||
return self._variables
|
||||
|
||||
@variables.setter
|
||||
def variables(self, variables):
|
||||
self._variables = variables
|
||||
|
||||
@property
|
||||
def values(self):
|
||||
return self._values
|
||||
|
||||
@values.setter
|
||||
def values(self, values):
|
||||
self._values = values
|
||||
|
||||
def get_value(self, name):
|
||||
if name in self.values:
|
||||
return self.values[name]
|
||||
else:
|
||||
raise KeyError(f"{name} does not exist.")
|
||||
|
||||
def set_value(self, name, value):
|
||||
if name in self.values:
|
||||
self.values[name] = value
|
||||
else:
|
||||
raise KeyError(f"{name} does not exist.")
|
||||
|
||||
def _exists(self, name):
|
||||
if name in self._variables:
|
||||
return True
|
||||
return False
|
||||
|
||||
def _retrieve(self, tv):
|
||||
tv = tv.__class__.from_state(tv.get_state())
|
||||
if self._exists(tv.name):
|
||||
return self.get_value(tv.name)
|
||||
return self._register(tv)
|
||||
|
||||
def _register(self, tv):
|
||||
self._variables[tv.name] = tv
|
||||
if tv.name not in self.values:
|
||||
self.values[tv.name] = tv.default
|
||||
return self.values[tv.name]
|
||||
|
||||
def __getitem__(self, name):
|
||||
return self.get_value(name)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
self.set_value(name, value)
|
||||
|
||||
def __contains__(self, name):
|
||||
try:
|
||||
self.get_value(name)
|
||||
return True
|
||||
except (KeyError, ValueError):
|
||||
return False
|
||||
|
||||
def fixed(self, name, default):
|
||||
tv = Fixed(name=name, default=default)
|
||||
return self._retrieve(tv)
|
||||
|
||||
def boolean(self, name, default=False):
|
||||
tv = Boolean(name=name, default=default)
|
||||
return self._retrieve(tv)
|
||||
|
||||
def choice(self, name, values, default=None):
|
||||
tv = Choice(name=name, values=values, default=default)
|
||||
return self._retrieve(tv)
|
||||
|
||||
def int_range(self, name, start, stop, step=1, default=None):
|
||||
tv = IntRange(
|
||||
name=name, start=start, stop=stop, step=step, default=default
|
||||
)
|
||||
return self._retrieve(tv)
|
||||
|
||||
def float_range(self, name, start, stop, step=None, default=None):
|
||||
tv = FloatRange(
|
||||
name=name, start=start, stop=stop, step=step, default=default
|
||||
)
|
||||
return self._retrieve(tv)
|
||||
|
||||
def get_state(self):
|
||||
return {
|
||||
"variables": [
|
||||
{"class_name": v.__class__.__name__, "state": v.get_state()}
|
||||
for v in self._variables.values()
|
||||
],
|
||||
"values": dict(self.values.items()),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
ts = cls()
|
||||
for v in state["variables"]:
|
||||
v = _deserialize_tunable_variable(v)
|
||||
ts._variables[v.name] = v
|
||||
ts._values = dict(state["values"].items())
|
||||
return ts
|
||||
|
||||
|
||||
def _deserialize_tunable_variable(state):
|
||||
classes = (Boolean, Fixed, Choice, IntRange, FloatRange)
|
||||
cls_name_to_cls = {cls.__name__: cls for cls in classes}
|
||||
|
||||
if isinstance(state, classes):
|
||||
return state
|
||||
|
||||
if (
|
||||
not isinstance(state, dict)
|
||||
or "class_name" not in state
|
||||
or "state" not in state
|
||||
):
|
||||
raise ValueError(
|
||||
f"Expect state to be a python dict containing class_name and state as keys, but found {state}"
|
||||
)
|
||||
|
||||
cls_name = state["class_name"]
|
||||
cls = cls_name_to_cls[cls_name]
|
||||
if cls is None:
|
||||
raise ValueError(f"Unknown class name {cls_name}")
|
||||
|
||||
cls_state = state["state"]
|
||||
deserialized_object = cls.from_state(cls_state)
|
||||
return deserialized_object
|
||||
@@ -0,0 +1,240 @@
|
||||
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# Notice that the following codes are modified from KerasTuner to implement our own tuner.
|
||||
# Please refer to https://github.com/keras-team/keras-tuner/blob/master/keras_tuner/engine/hyperparameters.py.
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TunableVariable:
|
||||
"""
|
||||
TunableVariable base class.
|
||||
"""
|
||||
|
||||
def __init__(self, name, default=None):
|
||||
self.name = name
|
||||
self._default = default
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
return self._default
|
||||
|
||||
def get_state(self):
|
||||
return {"name": self.name, "default": self.default}
|
||||
|
||||
@classmethod
|
||||
def from_state(cls, state):
|
||||
return cls(**state)
|
||||
|
||||
|
||||
class Fixed(TunableVariable):
|
||||
"""
|
||||
Fixed variable which cannot be changed.
|
||||
"""
|
||||
|
||||
def __init__(self, name, default):
|
||||
super().__init__(name=name, default=default)
|
||||
self.name = name
|
||||
if not isinstance(default, (str, int, float, bool)):
|
||||
raise ValueError(
|
||||
f"Fixed must be an str, int, float or bool, but found {default}"
|
||||
)
|
||||
self._default = default
|
||||
|
||||
def random(self, seed=None):
|
||||
return self._default
|
||||
|
||||
def __repr__(self):
|
||||
return f"Fixed(name: {self.name}, value: {self.default})"
|
||||
|
||||
|
||||
class Boolean(TunableVariable):
|
||||
"""
|
||||
Choice between True and False.
|
||||
"""
|
||||
|
||||
def __init__(self, name, default=False):
|
||||
super().__init__(name=name, default=default)
|
||||
if default not in {True, False}:
|
||||
raise ValueError(
|
||||
f"default must be a Python boolean, but got {default}"
|
||||
)
|
||||
|
||||
def random(self, seed=None):
|
||||
rng = np.random.default_rng(seed)
|
||||
return rng.choice((True, False))
|
||||
|
||||
def __repr__(self):
|
||||
return f'Boolean(name: "{self.name}", default: {self.default})'
|
||||
|
||||
|
||||
class Choice(TunableVariable):
|
||||
def __init__(self, name, values, default=None):
|
||||
super().__init__(name=name, default=default)
|
||||
|
||||
types = {type(v) for v in values}
|
||||
if len(types) > 1:
|
||||
raise TypeError(
|
||||
f"Choice can contain only one type of value, but found values: {values} with types: {types}."
|
||||
)
|
||||
self._is_unknown_type = False
|
||||
|
||||
if isinstance(values[0], str):
|
||||
values = [str(v) for v in values]
|
||||
if default is not None:
|
||||
default = str(default)
|
||||
elif isinstance(values[0], int):
|
||||
values = [int(v) for v in values]
|
||||
if default is not None:
|
||||
default = int(default)
|
||||
elif isinstance(values[0], float):
|
||||
values = [float(v) for v in values]
|
||||
if default is not None:
|
||||
default = float(default)
|
||||
elif isinstance(values[0], bool):
|
||||
values = [bool(v) for v in values]
|
||||
if default is not None:
|
||||
default = bool(default)
|
||||
else:
|
||||
self._is_unknown_type = True
|
||||
self._indices = list(range(len(values)))
|
||||
self.values = values
|
||||
|
||||
if default is not None and default not in values:
|
||||
raise ValueError(
|
||||
f"The default value should be one of the choices {values}, but found {default}"
|
||||
)
|
||||
self._default = default
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
if self._default is None:
|
||||
if None in self.values:
|
||||
return None
|
||||
return self.values[0]
|
||||
return self._default
|
||||
|
||||
def random(self, seed=None):
|
||||
rng = np.random.default_rng(seed)
|
||||
if self._is_unknown_type:
|
||||
indice = rng.choice(self._indices)
|
||||
return self.values[indice]
|
||||
else:
|
||||
return rng.choice(self.values)
|
||||
|
||||
def get_state(self):
|
||||
state = super().get_state()
|
||||
state["values"] = self.values
|
||||
return state
|
||||
|
||||
def __repr__(self):
|
||||
return f'Choice(name: "{self.name}", values: {self.values}, default: {self.default})'
|
||||
|
||||
|
||||
class IntRange(TunableVariable):
|
||||
"""
|
||||
Integer range.
|
||||
"""
|
||||
|
||||
def __init__(self, name, start, stop, step=1, default=None, endpoint=False):
|
||||
super().__init__(name=name, default=default)
|
||||
self.start = self._check_int(start)
|
||||
self.stop = self._check_int(stop)
|
||||
self.step = self._check_int(step)
|
||||
self._default = default
|
||||
self.endpoint = endpoint
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
if self._default is not None:
|
||||
return self._default
|
||||
return self.start
|
||||
|
||||
def random(self, seed=None):
|
||||
rng = np.random.default_rng(seed)
|
||||
value = (self.stop - self.start) * rng.random() + self.start
|
||||
if self.step is not None:
|
||||
if self.endpoint:
|
||||
values = np.arange(self.start, self.stop + 1e-7, step=self.step)
|
||||
else:
|
||||
values = np.arange(self.start, self.stop, step=self.step)
|
||||
closest_index = np.abs(values - value).argmin()
|
||||
value = values[closest_index]
|
||||
return int(value)
|
||||
|
||||
def get_state(self):
|
||||
state = super().get_state()
|
||||
state["start"] = self.start
|
||||
state["stop"] = self.stop
|
||||
state["step"] = self.step
|
||||
state["default"] = self._default
|
||||
return state
|
||||
|
||||
def _check_int(self, val):
|
||||
int_val = int(val)
|
||||
if int_val != val:
|
||||
raise ValueError(f"Expects val is an int, but found: {val}.")
|
||||
return int_val
|
||||
|
||||
def __repr__(self):
|
||||
return f"IntRange(name: {self.name}, start: {self.start}, stop: {self.stop}, step: {self.step}, default: {self.default})"
|
||||
|
||||
|
||||
class FloatRange(TunableVariable):
|
||||
"""
|
||||
Float range.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, name, start, stop, step=None, default=None, endpoint=False
|
||||
):
|
||||
super().__init__(name=name, default=default)
|
||||
self.stop = float(stop)
|
||||
self.start = float(start)
|
||||
if step is not None:
|
||||
self.step = float(step)
|
||||
else:
|
||||
self.step = None
|
||||
self._default = default
|
||||
self.endpoint = endpoint
|
||||
|
||||
@property
|
||||
def default(self):
|
||||
if self._default is not None:
|
||||
return self._default
|
||||
return self.start
|
||||
|
||||
def random(self, seed=None):
|
||||
rng = np.random.default_rng(seed)
|
||||
value = (self.stop - self.start) * rng.random() + self.start
|
||||
if self.step is not None:
|
||||
if self.endpoint:
|
||||
values = np.arange(self.start, self.stop + 1e-7, step=self.step)
|
||||
else:
|
||||
values = np.arange(self.start, self.stop, step=self.step)
|
||||
closest_index = np.abs(values - value).argmin()
|
||||
value = values[closest_index]
|
||||
return value
|
||||
|
||||
def get_state(self):
|
||||
state = super().get_state()
|
||||
state["start"] = self.start
|
||||
state["stop"] = self.stop
|
||||
state["step"] = self.step
|
||||
state["endpoint"] = self.endpoint
|
||||
return state
|
||||
|
||||
def __repr__(self):
|
||||
return f"FloatRange(name: {self.name}, start: {self.start}, stop: {self.stop}, step: {self.step}, default: {self.default}, endpoint: {self.endpoint})"
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user