2808 lines
96 KiB
Python
2808 lines
96 KiB
Python
# Copyright (c) 2021 PaddlePaddle Authors. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License
|
|
|
|
import copy
|
|
import logging
|
|
import os
|
|
import threading
|
|
import warnings
|
|
from functools import reduce
|
|
|
|
import numpy as np
|
|
|
|
import paddle
|
|
from paddle.base.framework import use_pir_api
|
|
from paddle.base.libpaddle import pir
|
|
from paddle.base.wrapped_decorator import (
|
|
wrap_decorator,
|
|
)
|
|
from paddle.framework import core
|
|
from paddle.framework.io_utils import is_belong_to_optimizer, is_parameter
|
|
from paddle.static import Variable
|
|
|
|
from ..process_mesh import ProcessMesh, merge_process_meshes
|
|
from .dist_attribute import DistTensorSpec, OperatorDistAttr, TensorDistAttr
|
|
|
|
OpRole = core.op_proto_and_checker_maker.OpRole
|
|
OP_ROLE_KEY = core.op_proto_and_checker_maker.kOpRoleAttrName()
|
|
|
|
__no_shape_var_type__ = [
|
|
core.VarDesc.VarType.READER,
|
|
core.VarDesc.VarType.STEP_SCOPES,
|
|
core.VarDesc.VarType.DENSE_TENSOR_ARRAY,
|
|
core.VarDesc.VarType.FEED_MINIBATCH,
|
|
core.VarDesc.VarType.FETCH_LIST,
|
|
]
|
|
|
|
__not_naive_data_parallel_op__ = ["expand_v2"]
|
|
_g_gradient_clip_ops = [
|
|
"sum",
|
|
"sqrt",
|
|
"fill_constant",
|
|
"elementwise_max",
|
|
"elementwise_div",
|
|
"stack",
|
|
"reduce_sum",
|
|
]
|
|
|
|
partition_skip_op_list = [
|
|
"builtin.combine",
|
|
"builtin.split",
|
|
"pd_op.pylayer",
|
|
"cf.yield",
|
|
"cf.tuple_push",
|
|
"cf.tuple_pop",
|
|
"cf.stack_create",
|
|
"cf.has_elements",
|
|
]
|
|
|
|
|
|
def get_logger(log_level, name="auto_parallel"):
|
|
logger = logging.getLogger(name)
|
|
logger.propagate = False
|
|
if not logger.handlers:
|
|
logger.setLevel(log_level)
|
|
log_handler = logging.StreamHandler()
|
|
log_format = logging.Formatter(
|
|
'%(levelname)s %(asctime)s %(filename)s:%(lineno)d] %(message)s'
|
|
)
|
|
log_handler.setFormatter(log_format)
|
|
logger.addHandler(log_handler)
|
|
else:
|
|
logger.setLevel(log_level)
|
|
return logger
|
|
|
|
|
|
def is_valid_list_index(list, index):
|
|
if index >= -len(list) and index < len(list):
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def is_dim_shard(mapping):
|
|
if mapping != -1:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def is_dim_replicate(mapping):
|
|
if mapping == -1:
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
|
|
def verify_dims_mapping(dims_mapping, process_mesh):
|
|
if dims_mapping is None:
|
|
return False
|
|
if not all(isinstance(d, int) for d in dims_mapping):
|
|
return False
|
|
for i in range(len(dims_mapping)):
|
|
if dims_mapping[i] < -1 or dims_mapping[i] >= len(process_mesh.shape):
|
|
return False
|
|
for i in range(len(process_mesh.shape)):
|
|
if dims_mapping.count(i) > 1:
|
|
return False
|
|
return True
|
|
|
|
|
|
def convert_to_dims_mapping(shard_spec, process_mesh):
|
|
dims_mapping = []
|
|
for shard in shard_spec:
|
|
if shard is None:
|
|
dims_mapping.append(-1)
|
|
elif process_mesh.shape[process_mesh.dim_names.index(shard)] == 1:
|
|
dims_mapping.append(-1)
|
|
else:
|
|
dims_mapping.append(process_mesh.dim_names.index(shard))
|
|
return dims_mapping
|
|
|
|
|
|
def convert_to_shard_spec(dims_mapping, process_mesh):
|
|
shard_spec = []
|
|
for dim_mapping in dims_mapping:
|
|
if dim_mapping == -1:
|
|
shard_spec.append(None)
|
|
else:
|
|
shard_spec.append(process_mesh.dim_names[dim_mapping])
|
|
return shard_spec
|
|
|
|
|
|
def verify_shard_spec(shard_spec, tensor_shape, process_mesh):
|
|
if len(shard_spec) != len(tensor_shape):
|
|
return False
|
|
for shard in shard_spec:
|
|
if shard is not None and not isinstance(shard, str):
|
|
return False
|
|
if shard is not None and shard not in process_mesh.dim_names:
|
|
return False
|
|
dims_mapping = convert_to_dims_mapping(shard_spec, process_mesh)
|
|
if not verify_dims_mapping(dims_mapping, process_mesh):
|
|
return False
|
|
for i in range(len(tensor_shape)):
|
|
if (
|
|
dims_mapping[i] != -1
|
|
and tensor_shape[i] > 0
|
|
and tensor_shape[i] % process_mesh.shape[dims_mapping[i]] != 0
|
|
):
|
|
return False
|
|
return True
|
|
|
|
|
|
def compute_compatible_dim_mapping(dim_mappings):
|
|
if not dim_mappings:
|
|
return None
|
|
compatible_mapping = dim_mappings[0]
|
|
for mapping in dim_mappings:
|
|
if compatible_mapping == -1:
|
|
compatible_mapping = mapping
|
|
elif mapping == -1:
|
|
continue
|
|
elif compatible_mapping == mapping:
|
|
continue
|
|
else:
|
|
return None
|
|
return compatible_mapping
|
|
|
|
|
|
def compute_compatible_dims_mapping(dims_mapping_list):
|
|
if not dims_mapping_list:
|
|
return None
|
|
length = len(dims_mapping_list[0])
|
|
for dims_mapping in dims_mapping_list:
|
|
assert dims_mapping is not None, (
|
|
"Dims mapping must not be None for compatible computation"
|
|
)
|
|
assert len(dims_mapping) == length, (
|
|
"The length of dims_mapping in list must be same for compatible computation."
|
|
)
|
|
compatible_result = []
|
|
for dim_mappings in zip(*dims_mapping_list):
|
|
compatible_dim_mapping = compute_compatible_dim_mapping(
|
|
list(dim_mappings)
|
|
)
|
|
if compatible_dim_mapping is None:
|
|
return None
|
|
compatible_result.append(compatible_dim_mapping)
|
|
return compatible_result
|
|
|
|
|
|
def compute_compatible_process_mesh(process_mesh_list):
|
|
compatible_process_mesh = None
|
|
if not process_mesh_list:
|
|
return compatible_process_mesh
|
|
for process_mesh in process_mesh_list:
|
|
if process_mesh is not None:
|
|
if (
|
|
compatible_process_mesh is None
|
|
or compatible_process_mesh == process_mesh
|
|
):
|
|
compatible_process_mesh = process_mesh
|
|
else:
|
|
return None
|
|
return compatible_process_mesh
|
|
|
|
|
|
def compute_compatible_and_update_dim_mapping(dims_mapping_list, index_list):
|
|
assert len(dims_mapping_list) == len(index_list)
|
|
changed = False
|
|
dim_mappings = []
|
|
for i in range(len(dims_mapping_list)):
|
|
assert is_valid_list_index(dims_mapping_list[i], index_list[i])
|
|
dim_mappings.append(dims_mapping_list[i][index_list[i]])
|
|
compatible_dim_mapping = compute_compatible_dim_mapping(dim_mappings)
|
|
if compatible_dim_mapping is None:
|
|
return False
|
|
for i in range(len(dims_mapping_list)):
|
|
if compatible_dim_mapping != dims_mapping_list[i][index_list[i]]:
|
|
dims_mapping_list[i][index_list[i]] = compatible_dim_mapping
|
|
changed = True
|
|
return changed
|
|
|
|
|
|
def append_distributed_attr_suffix(name):
|
|
"""
|
|
Append auto parallel suffix for distributed attribute name.
|
|
"""
|
|
return name + core.kAutoParallelSuffix()
|
|
|
|
|
|
def remove_distributed_attr_suffix(name):
|
|
"""
|
|
Remove auto parallel suffix from distributed attribute name.
|
|
"""
|
|
return name.strip(core.kAutoParallelSuffix())
|
|
|
|
|
|
def check_distributed_attr_for_program(program, dist_context=None):
|
|
from .dist_context import get_default_distributed_context
|
|
|
|
if dist_context is None:
|
|
dist_context = get_default_distributed_context()
|
|
assert dist_context.is_initialized_for_program(), (
|
|
"Distributed attributes must be initialized before check."
|
|
)
|
|
for block in program.blocks:
|
|
for tensor in block.vars.values():
|
|
dist_tensor = dist_context.get_dist_tensor_for_graph(tensor)
|
|
tensor_dist_attr = dist_context.get_tensor_dist_attr_for_program(
|
|
tensor
|
|
)
|
|
if (tensor_dist_attr is not None) and (not dist_tensor.is_valid()):
|
|
return False
|
|
for op in block.ops:
|
|
dist_op = dist_context.get_dist_op_for_graph(tensor)
|
|
op_dist_attr = dist_context.get_op_dist_attr_for_program(op)
|
|
if (op_dist_attr is not None) and (not dist_op.is_valid()):
|
|
return False
|
|
return True
|
|
|
|
|
|
def print_program_with_dist_attr(program, dist_context=None):
|
|
"""
|
|
This function reuses the original program output ability with a distributed context.
|
|
Using lock can avoid multiple threads change the default distributed context simultaneously.
|
|
"""
|
|
lock = threading.Lock()
|
|
lock.acquire()
|
|
from .dist_context import (
|
|
get_default_distributed_context,
|
|
set_default_distributed_context,
|
|
)
|
|
|
|
if dist_context is None:
|
|
dist_context = get_default_distributed_context()
|
|
print(program, flush=True)
|
|
else:
|
|
original_default_context = get_default_distributed_context()
|
|
set_default_distributed_context(dist_context)
|
|
print(program, flush=True)
|
|
set_default_distributed_context(original_default_context)
|
|
lock.release()
|
|
|
|
|
|
def _get_comm_group(processes, shape, axis, rank):
|
|
"""
|
|
Given a rank and the processes mesh the rank belongs to,
|
|
compute the communication peers of the rank based on the give axis in the mesh.
|
|
|
|
Example: 16 processes managed in a 4-Dimensional mesh with shape of [2, 2, 2, 2].
|
|
the rank communication peers of rank 0 (included) are following:
|
|
in axis 0: [0, 1]
|
|
in axis 1: [0, 2]
|
|
in axis 2: [0, 4]
|
|
in axis 3: [0, 8]
|
|
"""
|
|
|
|
# NOTE _linear_idx2coordinate assume processes mesh start with 0 and continuous
|
|
# tricks to support processes mesh when it is not start with 0 or continuous
|
|
assert rank in processes, (
|
|
f"rank [{rank}] is NOT in processes group {processes}"
|
|
)
|
|
rank_relative = processes.index(rank)
|
|
coordinate = _linear_idx2coordinate(shape, rank_relative)
|
|
coordinates_in_group = [coordinate[:] for i in range(shape[axis])]
|
|
|
|
# select comm group
|
|
for i in range(shape[axis]):
|
|
coordinates_in_group[i][axis] = i
|
|
|
|
ranks_in_group_relative = [
|
|
_coordinate2linear_idx(shape, coordinate)
|
|
for coordinate in coordinates_in_group
|
|
]
|
|
ranks_in_group = [processes[idx] for idx in ranks_in_group_relative]
|
|
|
|
return sorted(ranks_in_group)
|
|
|
|
|
|
def _get_idx_in_axis(processes, shape, axis, rank):
|
|
"""
|
|
Given a rank and the processes mesh the rank belongs to,
|
|
compute the index of the rank in given axis.
|
|
|
|
Example: 27 processes managed in a 3-Dimensional mesh with shape of [3, 3, 3].
|
|
the index of rank 22 are:
|
|
in axis 0: 1
|
|
in axis 1: 1
|
|
in axis 2: 2
|
|
"""
|
|
|
|
# NOTE _linear_idx2coordinate assume processes mesh start with 0 and continuous
|
|
# tricks to support processes mesh when it is not start with 0 or continuous
|
|
rank_relative = processes.index(rank)
|
|
coordinate = _linear_idx2coordinate(shape, rank_relative)
|
|
return coordinate[axis]
|
|
|
|
|
|
def _coordinate2linear_idx(mesh_shape, coordinate):
|
|
"""
|
|
convert a coordinate in multidimensional mesh space into a scala idx in linear space.
|
|
|
|
it use Row-major order for dimension conversion.
|
|
so it has: [most_significant_dim, ..., least_significant_dim]
|
|
assume:
|
|
|
|
the size of i-th dimension to be: S[i]
|
|
the index of j-th dimension is: I[j]
|
|
|
|
linear_idx of a n dimensional coordinate is:
|
|
|
|
I[n-1] * (S[n-2] * S[n-3] * S[n-4] * .... S[0]) +
|
|
I[n-2] * ( S[n-3] * S[n-4] * .... S[0]) +
|
|
I[n-3] * ( S[n-4] * .... S[0]) +
|
|
...
|
|
I[1] * ( S[0]) +
|
|
I[0]
|
|
|
|
"""
|
|
# NOTE the following function work based on a strong an assumption
|
|
# that the processes in mesh are
|
|
# 1. starts from 0
|
|
# 2. continuous
|
|
# it will be wrong if the above condition does not meet,
|
|
# e.g. process_mesh = { process_groups = [7, 8, 9,10, 12, 13, 14, 15], mesh = [2, 4]}
|
|
# if you want a more general mapping, you should use cartesian product
|
|
|
|
assert len(mesh_shape) == len(coordinate), (
|
|
f"coordinate should have the same size as mesh shape, but got shape: {mesh_shape}, coordinate: {coordinate}"
|
|
)
|
|
for i in range(len(mesh_shape)):
|
|
assert coordinate[i] >= 0, (
|
|
f"index in dimension [{i}] is least than zero. coordinate: {coordinate}"
|
|
)
|
|
assert coordinate[i] < mesh_shape[i], (
|
|
f"index beyond extent in dimension [{i}]. shape: {mesh_shape}, coordinate: {coordinate}"
|
|
)
|
|
|
|
base = mesh_shape[-1]
|
|
linear_idx = coordinate[-1]
|
|
|
|
# row major order
|
|
for i in range(len(mesh_shape) - 2, -1, -1):
|
|
linear_idx += base * coordinate[i]
|
|
base *= mesh_shape[i]
|
|
|
|
return linear_idx
|
|
|
|
|
|
def _linear_idx2coordinate(mesh_shape, linear_idx):
|
|
"""
|
|
mapping a linear scala into multidimensional mesh space, return it coordinate in that space.
|
|
|
|
it is the inverse function of _coordinate2linear_idx.
|
|
assume:
|
|
|
|
the size of i-th dimension to be: S[i]
|
|
the index of j-th dimension is: I[j]
|
|
|
|
the coordinate given linear_idx is:
|
|
|
|
I[0] = linear_idx % S[0]
|
|
I[0] = (linear_idx / S[0]) % S[1]
|
|
I[0] = (linear_idx / (S[0] * S[1])) % S[2]
|
|
....
|
|
|
|
"""
|
|
|
|
assert linear_idx >= 0, f"linear index [{linear_idx}] is least than zero"
|
|
assert linear_idx < np.prod(mesh_shape), (
|
|
f"linear index beyond the extent of mesh shape. shape: {mesh_shape}, linear index: {linear_idx}"
|
|
)
|
|
|
|
base = 1
|
|
coordinate = [-1] * len(mesh_shape)
|
|
|
|
for i in reversed(range(len(mesh_shape))):
|
|
offset = linear_idx / base
|
|
coordinate[i] = int(offset % mesh_shape[i])
|
|
base *= mesh_shape[i]
|
|
|
|
# row major order
|
|
return coordinate
|
|
|
|
|
|
def _get_corresponding_rank(dist_context, target_mesh, rank):
|
|
# TODO(JZ-LIANG) a hack method to support varying mesh in Pipeline parallelism case.
|
|
# we assume that all mesh are evenly divide from a parent mesh and should have same size.
|
|
# to revise this in future.
|
|
|
|
coordinate = None
|
|
for mesh in dist_context.process_meshes:
|
|
if rank in mesh.process_ids and mesh.shape == target_mesh.shape:
|
|
coordinate = _linear_idx2coordinate(
|
|
mesh.shape, mesh.process_ids.index(rank)
|
|
)
|
|
break
|
|
|
|
# assert coordinate is not None, "could NOT found rank [{}] in any registered mesh".format(
|
|
# rank)
|
|
if coordinate is not None:
|
|
return target_mesh.process_ids[
|
|
_coordinate2linear_idx(mesh.shape, coordinate)
|
|
]
|
|
else:
|
|
return target_mesh.process_ids[0]
|
|
|
|
|
|
def _get_unshard_dist_shape(var, dist_attr):
|
|
var_shape = var.shape
|
|
mapping = dist_attr.dims_mapping
|
|
mesh = dist_attr.process_mesh.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:
|
|
new_shape.append(var_shape[idx] * mesh[mapping[idx]])
|
|
|
|
return new_shape
|
|
|
|
|
|
def make_data_unshard(dist_main_prog, dist_startup_prog, dist_context=None):
|
|
from .dist_context import get_default_distributed_context
|
|
|
|
if dist_context is None:
|
|
dist_context = get_default_distributed_context()
|
|
|
|
for var in dist_main_prog.list_vars():
|
|
if var.is_data:
|
|
tensor_dist_attr = dist_context.get_tensor_dist_attr_for_program(
|
|
var
|
|
)
|
|
inverse_shape = _get_unshard_dist_shape(var, tensor_dist_attr)
|
|
var.desc.set_shape(inverse_shape)
|
|
dim_mapping = tensor_dist_attr.dims_mapping
|
|
dim_mapping = [-1] * len(dim_mapping)
|
|
tensor_dist_attr.dims_mapping = dim_mapping
|
|
dist_context.set_tensor_dist_attr_for_program(var, tensor_dist_attr)
|
|
|
|
|
|
def _update_addition_info(addition_info):
|
|
"""Update default addition_info with inputs"""
|
|
add_info = {"epoch": 0, "batch": 0, "batch_size": 0}
|
|
if not addition_info:
|
|
return add_info
|
|
elif not isinstance(addition_info, dict):
|
|
raise TypeError(
|
|
"The type of 'addition_info' should be 'dict', "
|
|
f"but got '{type(addition_info)}'."
|
|
)
|
|
else:
|
|
for item, value in addition_info.items():
|
|
if item not in ["epoch", "batch", "batch_size"]:
|
|
raise ValueError(
|
|
"The key of 'addition_info' should be one of the "
|
|
f"['epoch', 'batch', 'batch_size'], but got '{item}'."
|
|
)
|
|
if not isinstance(value, int):
|
|
raise ValueError(
|
|
"The value of 'addition_info' should be 'int', "
|
|
f"but got '{type(value)}'."
|
|
)
|
|
add_info[item] = value
|
|
return add_info
|
|
|
|
|
|
def _check_valid_path(file_path):
|
|
"""Validity check of input file path"""
|
|
if not file_path:
|
|
return file_path
|
|
elif isinstance(file_path, list):
|
|
for file in file_path:
|
|
if not isinstance(file, str):
|
|
raise TypeError(
|
|
"The type of file path should be 'str', "
|
|
f"but got '{type(file)}'."
|
|
)
|
|
if not os.path.exists(file):
|
|
raise ValueError(f"The file path '{file}' does not exist.")
|
|
return file_path
|
|
else:
|
|
raise TypeError(
|
|
"The type of file path should be 'list', "
|
|
f"but got '{type(file_path)}'."
|
|
)
|
|
|
|
|
|
def _check_param_dict(param_dict):
|
|
if not param_dict:
|
|
raise ValueError("'param_dict' cannot be None.")
|
|
elif not isinstance(param_dict, dict):
|
|
raise TypeError(
|
|
"The type of 'param_dict' should be 'dict', "
|
|
f"but got '{type(param_dict)}'."
|
|
)
|
|
else:
|
|
for name, value in param_dict.items():
|
|
if not isinstance(name, str):
|
|
raise TypeError(
|
|
"The type of key of 'param_dict' should be 'str', "
|
|
f"but got '{type(name)}'."
|
|
)
|
|
if not isinstance(value, paddle.base.DenseTensor):
|
|
raise TypeError(
|
|
"The type of value of 'param_dict' should be 'DenseTensor', "
|
|
f"but got '{type(value)}'."
|
|
)
|
|
return param_dict
|
|
|
|
|
|
def _check_dist_attr(dist_attr):
|
|
if not dist_attr:
|
|
return dist_attr
|
|
elif not isinstance(dist_attr, dict):
|
|
raise TypeError(
|
|
"The type of 'dist_attr' should be 'dict', "
|
|
f"but got '{type(dist_attr)}'."
|
|
)
|
|
else:
|
|
for name, value in dist_attr.items():
|
|
if not isinstance(name, str):
|
|
raise TypeError(
|
|
"The type of param name of 'dist_attr' should be 'str', "
|
|
f"but got '{type(name)}'."
|
|
)
|
|
if not isinstance(value, dict):
|
|
raise TypeError(
|
|
"The type of distributed attribute should be 'dict', "
|
|
f"but got '{type(value)}'"
|
|
)
|
|
attr = [
|
|
'process_shape',
|
|
'process_group',
|
|
'dims_mapping',
|
|
'dim_names',
|
|
]
|
|
if list(value.keys()) != attr:
|
|
raise ValueError(
|
|
"The key of distributed attribute should be "
|
|
"'['process_shape', 'process_group', 'dims_mapping']', "
|
|
f"but got {value.keys()}."
|
|
)
|
|
return dist_attr
|
|
|
|
|
|
def save_distributed_checkpoint(
|
|
program,
|
|
checkpoint_path,
|
|
dist_attr_path,
|
|
addition_info=None,
|
|
is_integrated=False,
|
|
dist_context=None,
|
|
):
|
|
"""
|
|
Save model parameter state, optimizer state, distributed attribute and
|
|
additional information of each rank.
|
|
|
|
Args:
|
|
program(Program): The program to be saved.
|
|
checkpoint_path(str): The path of the checkpoint file to be saved.
|
|
dist_attr_path(str): The path of distributed attribute file to be saved.
|
|
addition_info(dict, optional): Additional information, key should be selected in ['epoch', 'batch', 'batch_size'].
|
|
Default values are 0, when 'addition_info' is None. Default: None.
|
|
is_integrated(bool, optional): Whether to integrate param before save. Default: False.
|
|
dist_context(DistributedContext ,optional): collect related distributed information for program
|
|
|
|
Returns:
|
|
None
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import os
|
|
>>> from paddle.distributed.auto_parallel.static.utils import save_distributed_checkpoint
|
|
|
|
>>> step = 16000
|
|
>>> global_batch_size = 32
|
|
>>> path = os.path.join("./output", "step_%d" % step)
|
|
>>> os.makedirs(path, exist_ok=True)
|
|
>>> program = paddle.static.Program()
|
|
|
|
>>> add_info = {'batch': step, "batch_size": global_batch_size}
|
|
>>> save_distributed_checkpoint(program, path, path, add_info)
|
|
|
|
"""
|
|
from .dist_context import get_default_distributed_context
|
|
|
|
assert isinstance(program, paddle.static.Program)
|
|
assert isinstance(is_integrated, bool)
|
|
if dist_context is None:
|
|
dist_context = get_default_distributed_context()
|
|
addition_info = _update_addition_info(addition_info)
|
|
|
|
if not is_integrated:
|
|
_save_distributed_state_dict(program, addition_info, checkpoint_path)
|
|
_save_distributed_attribute(program, dist_attr_path, dist_context)
|
|
else:
|
|
# TODO: integrate param before save
|
|
raise NotImplementedError(
|
|
"Integrating parameter has not been implemented."
|
|
)
|
|
|
|
|
|
def load_distributed_checkpoint(checkpoint_path, dist_attr_path):
|
|
"""
|
|
Load parameter, optimizer, distributed attribute and addition_info.
|
|
|
|
Args:
|
|
checkpoint_path(list[str]): model parameter file path, must be in order of rank id.
|
|
dist_attr_path(list[str]): distributed attribute file path, must be in order of rank id.
|
|
|
|
Returns:
|
|
param_dict(dict): parameters' value of all ranks.
|
|
dist_attr(dict): parameters' distributed attribute.
|
|
addition_info(dict): additional information user saved in last training.
|
|
|
|
Notes:
|
|
The return, 'addition_info', is belonging to the first file of checkpoint_path by default.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> # doctest: +SKIP('Depends on external files.')
|
|
>>> from paddle.distributed.auto_parallel.static.utils import load_distributed_checkpoint
|
|
|
|
>>> ckpt_path = [
|
|
... './model_state_rank0.pdmodel',
|
|
... './model_state_rank1.pdmodel',
|
|
... ]
|
|
>>> dist_attr_path = [
|
|
... './dist_attr_rank0.pdattr',
|
|
... './dist_attr_rank1.pdattr',
|
|
... ]
|
|
>>> param_dict, dist_attr, add_info = load_distributed_checkpoint(
|
|
... ckpt_path,
|
|
... dist_attr_path,
|
|
... )
|
|
"""
|
|
assert _check_valid_path(checkpoint_path), (
|
|
"'checkpoint_path' cannot be None."
|
|
)
|
|
assert _check_valid_path(dist_attr_path), "'dist_attr_path' cannot be None."
|
|
|
|
state_dict_info = _load_distributed_state_dict(checkpoint_path)
|
|
dist_attr = _load_distributed_attribute(dist_attr_path)
|
|
param_dict = state_dict_info["model"]
|
|
addition_info = state_dict_info["addition_info"]
|
|
return param_dict, dist_attr, addition_info
|
|
|
|
|
|
def load_checkpoint_into_program(
|
|
checkpoint_path, dist_attr_path, program, dist_context=None
|
|
):
|
|
"""
|
|
Load parameter, optimizer, distributed attribute and addition_info into model.
|
|
|
|
Args:
|
|
checkpoint_path(list[str]): model parameter file path, must be in order of rank id.
|
|
dist_attr_path(list[str]): distributed attribute file path, must be in order of rank id.
|
|
program(Program): the program to be updated with checkpoint_path.
|
|
dist_context(DistributedContext ,optional): collect related distributed information for program
|
|
|
|
Returns:
|
|
addition_info(dict): user saved in last train.
|
|
|
|
Notes:
|
|
The return, 'addition_info', is belonging to the first file of checkpoint_path by default.
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> # doctest: +SKIP('Depends on external files.')
|
|
>>> from paddle.distributed.auto_parallel.static.utils import load_checkpoint_into_program
|
|
|
|
>>> exe.run(startup_program)
|
|
>>> ckpt_path = [
|
|
... './model_state_rank0.pdmodel',
|
|
... './model_state_rank1.pdmodel',
|
|
... ]
|
|
>>> dist_attr_path = [
|
|
... './dist_attr_rank0.pdattr',
|
|
... './dist_attr_rank1.pdattr',
|
|
... ]
|
|
>>> load_checkpoint_into_program(ckpt_path, dist_attr_path, main_program)
|
|
"""
|
|
from .dist_context import get_default_distributed_context
|
|
|
|
assert isinstance(program, paddle.static.Program)
|
|
assert _check_valid_path(checkpoint_path), (
|
|
"'checkpoint_path' cannot be None."
|
|
)
|
|
assert _check_valid_path(dist_attr_path), "'dist_attr_path' cannot be None."
|
|
if dist_context is None:
|
|
dist_context = get_default_distributed_context()
|
|
all_state_dict_info = _load_distributed_state_dict(checkpoint_path)
|
|
all_pre_dist_attr = _load_distributed_attribute(dist_attr_path)
|
|
all_cur_dist_attr = get_dist_attr(program, dist_context)
|
|
all_param_dict = all_state_dict_info["model"]
|
|
addition_info = all_state_dict_info["addition_info"]
|
|
sliced_param_dict = merge_and_slice_parameter(
|
|
all_param_dict, all_pre_dist_attr, all_cur_dist_attr
|
|
)
|
|
load_parameter_into_program(sliced_param_dict, program)
|
|
|
|
return addition_info
|
|
|
|
|
|
def load_parameter_into_program(param_dict, program):
|
|
"""
|
|
Load parameters into program.
|
|
|
|
Args:
|
|
param_dict(dict): parameters' name and value.
|
|
program(Program): the program to be updated
|
|
"""
|
|
assert isinstance(param_dict, dict)
|
|
assert program and isinstance(program, paddle.static.Program)
|
|
if not param_dict:
|
|
return
|
|
program.set_state_dict(param_dict)
|
|
|
|
|
|
def _save_distributed_attribute(program, dist_attr_path, dist_context):
|
|
"""Save distributed attribute of all parameters"""
|
|
# TODO: just save a complete distributed attribute file
|
|
rank_id = paddle.distributed.get_rank()
|
|
dist_attr_name = os.path.join(
|
|
dist_attr_path, f"dist_attr_rank{rank_id}.pdattr"
|
|
)
|
|
dist_attr_dict = {
|
|
"model": get_dist_attr(program, dist_context),
|
|
"world_size": paddle.distributed.get_world_size(),
|
|
}
|
|
paddle.save(dist_attr_dict, dist_attr_name)
|
|
logging.info(f"Already saved distributed attribute to '{dist_attr_path}'.")
|
|
|
|
|
|
def _load_distributed_attribute(dist_attr_path):
|
|
"""Load parameters' distributed attribute from dist_attr_path"""
|
|
total_dist_attr = {}
|
|
for dist_attr_file in dist_attr_path:
|
|
dist_attr = paddle.load(dist_attr_file)
|
|
pre_world_size = dist_attr["world_size"]
|
|
assert pre_world_size == len(dist_attr_path), (
|
|
"The number of 'dist_attr_path' must be equal to the last training world size."
|
|
)
|
|
for name, attr in dist_attr["model"].items():
|
|
if name not in total_dist_attr:
|
|
total_dist_attr[name] = attr
|
|
|
|
return total_dist_attr
|
|
|
|
|
|
def _save_distributed_state_dict(program, addition_info, checkpoint_path):
|
|
"""Save parameters' state_dict"""
|
|
rank = paddle.distributed.get_rank()
|
|
ckpt_file_name = os.path.join(
|
|
checkpoint_path, f"model_state_rank{rank}.pdmodel"
|
|
)
|
|
state_dict = {
|
|
"model": program.state_dict(),
|
|
"world_size": paddle.distributed.get_world_size(),
|
|
"addition_info": addition_info,
|
|
}
|
|
paddle.save(state_dict, ckpt_file_name)
|
|
logging.info(f"Already saved model to '{checkpoint_path}'.")
|
|
|
|
|
|
def _load_distributed_state_dict(checkpoint_path):
|
|
"""Load parameters' state_dict from checkpoint_path"""
|
|
all_state_dict = {}
|
|
for idx, ckpt_file in enumerate(checkpoint_path):
|
|
state_dict_info = paddle.load(ckpt_file, return_numpy=True)
|
|
pre_world_size = state_dict_info["world_size"]
|
|
assert pre_world_size == len(checkpoint_path), (
|
|
"The number of 'checkpoint_path' must be equal to the last training world size."
|
|
)
|
|
if idx == 0:
|
|
addition_info = state_dict_info["addition_info"]
|
|
for name, value in state_dict_info["model"].items():
|
|
if name in all_state_dict:
|
|
all_state_dict[name].append(np.array(value))
|
|
else:
|
|
all_state_dict[name] = [np.array(value)]
|
|
|
|
all_state_dict_info = {
|
|
"model": all_state_dict,
|
|
"addition_info": addition_info,
|
|
}
|
|
return all_state_dict_info
|
|
|
|
|
|
def get_dist_attr(program, dist_context=None):
|
|
"""
|
|
Get distributed attribute of current rank.
|
|
|
|
Args:
|
|
program(Program): main program for training
|
|
"""
|
|
dist_attr = {}
|
|
if use_pir_api():
|
|
ops = program.global_block().ops
|
|
for op in ops:
|
|
if op.name() == "builtin.parameter" or (
|
|
op.name() == "pd_op.data"
|
|
and op.has_attr("persistable")
|
|
and op.attrs()["persistable"]
|
|
):
|
|
op_dist_attr = op.dist_attr
|
|
var_dist_attr = op_dist_attr.result(0).as_tensor_dist_attr()
|
|
var_name = (
|
|
op.str_attr("parameter_name")
|
|
if op.name() == "builtin.parameter"
|
|
else op.str_attr("name")
|
|
)
|
|
process_mesh = var_dist_attr.process_mesh
|
|
dist_attr[var_name] = {
|
|
"process_shape": process_mesh.shape,
|
|
"process_group": process_mesh.process_ids,
|
|
"dims_mapping": var_dist_attr.dims_mapping,
|
|
"dim_names": process_mesh.dim_names,
|
|
}
|
|
else:
|
|
from .dist_context import get_default_distributed_context
|
|
|
|
assert isinstance(program, paddle.static.Program)
|
|
if dist_context is None:
|
|
dist_context = get_default_distributed_context()
|
|
for var in program.list_vars():
|
|
if is_parameter(var) or is_belong_to_optimizer(var):
|
|
tensor_dist_attr = (
|
|
dist_context.get_tensor_dist_attr_for_program(var)
|
|
)
|
|
process_mesh = tensor_dist_attr.process_mesh
|
|
dims_mapping = tensor_dist_attr.dims_mapping
|
|
dim_names = tensor_dist_attr.process_mesh.dim_names
|
|
dist_attr[var.name] = {
|
|
"process_shape": process_mesh.shape,
|
|
"process_group": process_mesh.process_ids,
|
|
"dims_mapping": dims_mapping,
|
|
"dim_names": dim_names,
|
|
}
|
|
return dist_attr
|
|
|
|
|
|
def merge_and_slice_parameter(dist_param_dict, pre_dist_attr, cur_dist_attr):
|
|
"""
|
|
Merge parameters with previous dist_attr and slice parameters with current dist_attr
|
|
|
|
Args:
|
|
dist_param_dict(dict): parameters' value of all ranks.
|
|
pre_dist_attr(dict): parameters' dist_attr of last training process.
|
|
cur_dist_attr(dict): parameters' dist_attr of current training process.
|
|
|
|
Returns:
|
|
dist_param_dict(dict): parameters' value of current rank.
|
|
"""
|
|
assert _check_dist_attr(pre_dist_attr), "'pre_dist_attr' cannot be None."
|
|
assert isinstance(dist_param_dict, dict), (
|
|
f"The type of 'dist_param_dict' should be 'dict', but got {type(dist_param_dict)}."
|
|
)
|
|
for name, value in dist_param_dict.items():
|
|
if not isinstance(name, str):
|
|
raise TypeError(
|
|
"The key of 'dist_param_dict' is parameter's name, "
|
|
f"and its type should be 'str', but got {type(name)}."
|
|
)
|
|
if not isinstance(value, list) or not all(
|
|
isinstance(v, np.ndarray) for v in value
|
|
):
|
|
raise TypeError(
|
|
"The value of 'dist_param_dict' is parameter's value of all ranks, "
|
|
"and its type should be 'list(numpy.ndarray)'."
|
|
)
|
|
|
|
if cur_dist_attr is None:
|
|
return {}
|
|
|
|
param_not_in_pre = []
|
|
param_not_in_cur = []
|
|
logging.info("Start to merge and slice parameters.")
|
|
for var_name in cur_dist_attr.keys():
|
|
if var_name not in pre_dist_attr:
|
|
param_not_in_pre.append(var_name)
|
|
continue
|
|
|
|
pre_attr = pre_dist_attr[var_name]
|
|
cur_attr = cur_dist_attr[var_name]
|
|
if pre_attr == cur_attr:
|
|
# skip merge and slice
|
|
rank_id = paddle.distributed.get_rank()
|
|
index = cur_attr["process_group"].index(rank_id)
|
|
param = dist_param_dict[var_name][index]
|
|
dist_param_dict[var_name] = param
|
|
continue
|
|
|
|
pre_param = dist_param_dict[var_name]
|
|
pre_dims_mapping = pre_attr["dims_mapping"]
|
|
cur_dims_mapping = cur_attr["dims_mapping"]
|
|
if len(set(pre_dims_mapping)) > 1 or -1 not in pre_dims_mapping:
|
|
complete_param = _merge_parameter_with_dist_attr(
|
|
pre_param, pre_attr
|
|
)
|
|
dist_param_dict[var_name] = complete_param
|
|
else:
|
|
complete_param = pre_param[0]
|
|
dist_param_dict[var_name] = complete_param
|
|
|
|
if len(set(cur_dims_mapping)) > 1 or -1 not in cur_dims_mapping:
|
|
sliced_param = _slice_parameter_with_dist_attr(
|
|
complete_param, cur_attr
|
|
)
|
|
dist_param_dict[var_name] = sliced_param
|
|
|
|
for var_name in pre_dist_attr:
|
|
if var_name not in cur_dist_attr:
|
|
param_not_in_cur.append(var_name)
|
|
dist_param_dict.pop(var_name)
|
|
|
|
if param_not_in_pre:
|
|
warnings.warn(
|
|
f"Parameters '{param_not_in_pre}' are not found in last training process."
|
|
)
|
|
if param_not_in_cur:
|
|
warnings.warn(
|
|
f"Parameters '{param_not_in_cur}' are not found in current training process."
|
|
)
|
|
|
|
return dist_param_dict
|
|
|
|
|
|
def _merge_parameter_with_dist_attr(param_list, dist_attr):
|
|
"""Merge parameter 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 parameter
|
|
complete_shape = Resharder.compute_complete_shape(
|
|
param_list[0].shape, process_shape, dims_mapping
|
|
)
|
|
# merge the parameter with dist_attr
|
|
partition_param_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)
|
|
_merge_parameter(
|
|
partition_param_list,
|
|
param_list[index],
|
|
partition_index,
|
|
complete_shape,
|
|
)
|
|
|
|
assert len(partition_param_list) == 1 or not partition_param_list, (
|
|
"Fail to merge parameter"
|
|
)
|
|
complete_param = partition_param_list[0][0]
|
|
return complete_param
|
|
|
|
|
|
def _slice_parameter_with_dist_attr(param, dist_attr):
|
|
"""Slice parameter with distributed attribute"""
|
|
param = (
|
|
np.array(param) if isinstance(param, paddle.base.DenseTensor) else param
|
|
)
|
|
dims_mapping = dist_attr["dims_mapping"]
|
|
process_shape = dist_attr["process_shape"]
|
|
process_group = dist_attr["process_group"]
|
|
# slice the parameter with dist_attr
|
|
partition_index_list = _get_split_indices(
|
|
param.shape, dims_mapping, process_shape, process_group
|
|
)
|
|
sliced_param_list = _slice_parameter(
|
|
param, partition_index_list, len(partition_index_list)
|
|
)
|
|
# get the current parameter's index in sliced_param_list
|
|
rank_id = paddle.distributed.get_rank()
|
|
sliced_param_index = _get_sliced_param_index(
|
|
rank_id, param.shape, dims_mapping, process_shape, process_group
|
|
)
|
|
sliced_param = sliced_param_list[sliced_param_index]
|
|
return sliced_param
|
|
|
|
|
|
def _merge_parameter(
|
|
partition_param_list, param, partition_index, complete_shape
|
|
):
|
|
"""
|
|
Merge partial parameters to a complete one.
|
|
|
|
Returns:
|
|
None
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import numpy as np
|
|
>>> from paddle.distributed.auto_parallel.static.utils import _merge_parameter
|
|
|
|
>>> partition_param_list = [(np.array([[[1.11, 1.12]]]), [[0, 1], [0, 1], [0, 2]])]
|
|
>>> param = np.array([[[1.13, 1.14]]])
|
|
>>> partition_index = [[0, 1], [0, 1], [2, 4]]
|
|
>>> complete_shape = [2, 2, 4]
|
|
|
|
>>> _merge_parameter(partition_param_list, param, partition_index, complete_shape)
|
|
>>> print(partition_param_list)
|
|
[(array([[[1.11, 1.12, 1.13, 1.14]]]), [[0, 1],[0, 1],[0, 4]])]
|
|
|
|
"""
|
|
from .reshard import Resharder
|
|
|
|
if len(partition_param_list) == 1:
|
|
is_complete_data = True
|
|
for idx, item in enumerate(partition_param_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_param_list:
|
|
partition_param_list.append((param, partition_index))
|
|
else:
|
|
i = 0
|
|
while i < len(partition_param_list):
|
|
(
|
|
concat_axis,
|
|
first_order,
|
|
new_partition,
|
|
) = Resharder.compute_concat_info(
|
|
partition_param_list[i][1], partition_index
|
|
)
|
|
if concat_axis != -1:
|
|
if first_order == 0:
|
|
new_param = np.concatenate(
|
|
(partition_param_list[i][0], param), axis=concat_axis
|
|
)
|
|
else:
|
|
new_param = np.concatenate(
|
|
(param, partition_param_list[i][0]), axis=concat_axis
|
|
)
|
|
|
|
partition_param_list.pop(i)
|
|
_merge_parameter(
|
|
partition_param_list,
|
|
new_param,
|
|
new_partition,
|
|
complete_shape,
|
|
)
|
|
break
|
|
i += 1
|
|
|
|
|
|
def _complete_op_dist_attr(program, block=None):
|
|
if block is None:
|
|
block = program.global_block()
|
|
for op in block.ops:
|
|
for sub_block in op.blocks():
|
|
_complete_op_dist_attr(program, block=sub_block)
|
|
if op.name() in partition_skip_op_list:
|
|
continue
|
|
|
|
if op.dist_attr is None:
|
|
meshes = []
|
|
operand_attrs = []
|
|
result_attrs = []
|
|
for operand in op.operands_source():
|
|
tmp_attr = operand.dist_attr()
|
|
if tmp_attr is None:
|
|
operand_attrs.append(pir.Attribute())
|
|
value_mesh = None
|
|
tmp_op_dist_attr = operand.get_defining_op().dist_attr
|
|
if tmp_op_dist_attr is not None:
|
|
value_mesh = tmp_op_dist_attr.process_mesh
|
|
else:
|
|
operand_attrs.append(tmp_attr)
|
|
value_mesh = tmp_attr.process_mesh
|
|
if value_mesh is not None and value_mesh not in meshes:
|
|
meshes.append(value_mesh)
|
|
|
|
for result in op.results():
|
|
tmp_attr = result.dist_attr()
|
|
if tmp_attr is None:
|
|
result_attrs.append(pir.Attribute())
|
|
else:
|
|
result_attrs.append(tmp_attr)
|
|
if tmp_attr.process_mesh not in meshes:
|
|
meshes.append(tmp_attr.process_mesh)
|
|
if len(meshes) > 0:
|
|
if len(meshes) == 1:
|
|
mesh = meshes[0]
|
|
else:
|
|
mesh = merge_process_meshes(meshes)
|
|
op.dist_attr = pir.create_op_dist_attribute(
|
|
mesh,
|
|
operand_attrs,
|
|
result_attrs,
|
|
)
|
|
|
|
|
|
def _slice_parameter(complete_param, partition_index_list, length):
|
|
"""
|
|
Slice a complete parameter.
|
|
|
|
Returns:
|
|
sliced_param_list(list): sliced parameters with 'partition_index_list'
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import numpy as np
|
|
>>> from paddle.distributed.auto_parallel.static.utils import _slice_parameter
|
|
|
|
>>> complete_param = 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_param_list = _slice_parameter(complete_param, [[], [], [2, 4]], 3)
|
|
>>> print(sliced_param_list)
|
|
[array([[[1.11, 1.12]]]), array([[[1.13, 1.14]]]), array([[[1.15, 1.16]]])]
|
|
|
|
"""
|
|
sliced_param_list = []
|
|
axis = len(complete_param.shape) - length
|
|
sliced_param = np.split(
|
|
complete_param, partition_index_list[axis], axis=axis
|
|
)
|
|
if length == 1:
|
|
return sliced_param
|
|
for param in sliced_param:
|
|
sliced_param_list.extend(
|
|
_slice_parameter(param, partition_index_list, length - 1)
|
|
)
|
|
return sliced_param_list
|
|
|
|
|
|
def _get_sliced_param_index(
|
|
rank, complete_shape, dims_mapping, process_shape, process_group
|
|
):
|
|
"""
|
|
Get sliced_param's index of current rank in all sliced parameters list.
|
|
|
|
Returns:
|
|
sliced_param_index(int): the index of sliced param in sliced_param_list
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import numpy as np
|
|
>>> from paddle.distributed.auto_parallel.static.utils import _get_sliced_param_index
|
|
|
|
>>> complete_param = 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]
|
|
|
|
>>> slice_param = _slice_parameter(complete_param, [[], [], [2, 4]], 3)
|
|
>>> print(slice_param)
|
|
[array([[[1.11, 1.12]]]), array([[[1.13, 1.14]]]), array([[[1.15, 1.16]]])]
|
|
|
|
>>> index = _get_sliced_param_index(
|
|
... rank,
|
|
... complete_shape,
|
|
... dims_mapping,
|
|
... process_shape,
|
|
... process_group,
|
|
... )
|
|
>>> print(index)
|
|
2
|
|
"""
|
|
from .reshard import Resharder
|
|
|
|
partition_index = Resharder.compute_partition_index(
|
|
rank, complete_shape, dims_mapping, process_shape, process_group
|
|
)
|
|
sliced_param_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_param_index = sliced_param_index * (shape // slice_shape) + index
|
|
return sliced_param_index
|
|
|
|
|
|
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 parameter
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> import numpy as np
|
|
>>> from paddle.distributed.auto_parallel.static.utils import _get_split_indices
|
|
|
|
>>> complete_param = 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
|
|
|
|
|
|
def is_forward_op(op):
|
|
op_role = int(op.attr('op_role'))
|
|
return OP_ROLE_KEY in op.attr_names and (
|
|
op_role == int(OpRole.Forward) or op_role == int(OpRole.Loss)
|
|
)
|
|
|
|
|
|
def is_backward_op(op):
|
|
return OP_ROLE_KEY in op.attr_names and int(
|
|
op.all_attrs()[OP_ROLE_KEY]
|
|
) & int(OpRole.Backward)
|
|
|
|
|
|
def is_optimize_op(op):
|
|
return OP_ROLE_KEY in op.attr_names and int(
|
|
op.all_attrs()[OP_ROLE_KEY]
|
|
) & int(OpRole.Optimize)
|
|
|
|
|
|
def is_lr_sched_op(op):
|
|
return OP_ROLE_KEY in op.attr_names and int(
|
|
op.all_attrs()[OP_ROLE_KEY]
|
|
) & int(OpRole.Optimize.LRSched)
|
|
|
|
|
|
def is_loss_op(op):
|
|
return OP_ROLE_KEY in op.attr_names and int(
|
|
op.all_attrs()[OP_ROLE_KEY]
|
|
) == (int(OpRole.Forward) | int(OpRole.Loss))
|
|
|
|
|
|
def is_loss_grad_op(op):
|
|
if OP_ROLE_KEY not in op.attr_names:
|
|
return False
|
|
op_role = int(op.all_attrs()[OP_ROLE_KEY])
|
|
return op_role & int(OpRole.Backward) and op_role & int(OpRole.Loss)
|
|
|
|
|
|
def is_gradient_clip_op(op):
|
|
return op.desc.has_attr("op_namescope") and op.desc.attr(
|
|
"op_namescope"
|
|
).startswith("/gradient_clip")
|
|
|
|
|
|
def is_reshard_op(op):
|
|
return op.desc.has_attr(
|
|
"op_namescope"
|
|
) and "/auto_parallel/reshard" in op.desc.attr('op_namescope')
|
|
|
|
|
|
def is_prim_op(op):
|
|
return op.type.endswith("_p")
|
|
|
|
|
|
def is_comm_op(op):
|
|
return op.has_attr("ring_id")
|
|
|
|
|
|
def get_loss_op(block):
|
|
loss_ops = []
|
|
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)
|
|
|
|
assert len(loss_ops) == 1, "num of loss op is not equal to one"
|
|
return loss_ops[0]
|
|
|
|
|
|
def set_var_dist_attr(dist_context, var, dims_mapping, process_mesh, **kwargs):
|
|
tensor_dist_attr = TensorDistAttr()
|
|
tensor_dist_attr.dims_mapping = dims_mapping
|
|
# TODO get global mesh group
|
|
if isinstance(process_mesh, (list, np.ndarray)):
|
|
tensor_dist_attr.process_mesh = ProcessMesh(process_mesh)
|
|
elif isinstance(process_mesh, core.ProcessMesh):
|
|
tensor_dist_attr.process_mesh = process_mesh
|
|
else:
|
|
raise ValueError(
|
|
f"{process_mesh} must be a instance of ProcessMesh or list, but receive {type(process_mesh)}"
|
|
)
|
|
if kwargs.get("mark_annotated"):
|
|
tensor_dist_attr.mark_annotated("dims_mapping")
|
|
tensor_dist_attr.mark_annotated("process_mesh")
|
|
if kwargs.get("chunk_id"):
|
|
tensor_dist_attr.chunk_id = kwargs["chunk_id"]
|
|
dist_context.set_tensor_dist_attr_for_program(var, tensor_dist_attr)
|
|
return tensor_dist_attr
|
|
|
|
|
|
def naive_set_dist_op_attr_for_program_by_mesh_and_mapping(
|
|
new_op, process_mesh, ref_mapping, ctx, **kwargs
|
|
):
|
|
assert process_mesh is not None
|
|
assert ref_mapping is not None
|
|
|
|
new_op_dist_attr = OperatorDistAttr()
|
|
|
|
for input_varname in new_op.desc.input_arg_names():
|
|
new_op_dist_attr.set_input_dims_mapping(input_varname, ref_mapping)
|
|
for output_varname in new_op.desc.output_arg_names():
|
|
new_op_dist_attr.set_output_dims_mapping(output_varname, ref_mapping)
|
|
|
|
new_op_dist_attr.process_mesh = process_mesh
|
|
if kwargs.get("chunk_id"):
|
|
new_op_dist_attr.chunk_id = kwargs["chunk_id"]
|
|
ctx.set_op_dist_attr_for_program(new_op, new_op_dist_attr)
|
|
|
|
|
|
def naive_set_dist_op_attr_for_program_by_mesh(
|
|
new_op, process_mesh, ctx, **kwargs
|
|
):
|
|
assert process_mesh is not None
|
|
|
|
new_op_dist_attr = OperatorDistAttr()
|
|
|
|
for input_varname in new_op.desc.input_arg_names():
|
|
var = new_op.block.var(input_varname)
|
|
mapping = ctx.get_tensor_dist_attr_for_program(var).dims_mapping
|
|
new_op_dist_attr.set_input_dims_mapping(input_varname, mapping)
|
|
for output_varname in new_op.desc.output_arg_names():
|
|
var = new_op.block.var(output_varname)
|
|
mapping = ctx.get_tensor_dist_attr_for_program(var).dims_mapping
|
|
new_op_dist_attr.set_output_dims_mapping(output_varname, mapping)
|
|
|
|
new_op_dist_attr.process_mesh = process_mesh
|
|
if "is_recompute" in kwargs:
|
|
new_op_dist_attr.is_recompute = kwargs["is_recompute"]
|
|
if "chunk_id" in kwargs:
|
|
new_op_dist_attr.chunk_id = kwargs["chunk_id"]
|
|
ctx.set_op_dist_attr_for_program(new_op, new_op_dist_attr)
|
|
|
|
|
|
def update_op_dims_mapping_by_default_dist_impl(dist_op):
|
|
changed = False
|
|
op_dist_attr = dist_op.dist_attr
|
|
op_desc = dist_op.serial_op.desc
|
|
# The following statement will be replaced by a more elegant way
|
|
if op_desc.type() == "shape" or op_desc.type() == "slice":
|
|
return False
|
|
output_names = op_desc.output_names()
|
|
xshape_arg_names = []
|
|
if "XShape" in output_names:
|
|
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 len(dims_mapping) > 1:
|
|
for idx, mapping in enumerate(dims_mapping[1:]):
|
|
assert mapping == -1, (
|
|
f"{op_desc.type()} only the batch dimension (0-dim) can be sharded, but the dimension {idx} is sharded by {mapping} part."
|
|
)
|
|
if len(dims_mapping) >= 1:
|
|
batch_dim_mappings.append(dims_mapping[0])
|
|
for arg_name in op_desc.output_arg_names():
|
|
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 xshape_arg_names:
|
|
if len(dims_mapping) > 1:
|
|
for idx, mapping in enumerate(dims_mapping[1:]):
|
|
assert mapping == -1, (
|
|
f"{op_desc.type()} only the batch dimension (0-dim) can be sharded, but the dimension {idx} is sharded by {mapping} part."
|
|
)
|
|
if len(dims_mapping) >= 1:
|
|
batch_dim_mappings.append(dims_mapping[0])
|
|
else:
|
|
assert dims_mapping[0] == -1, (
|
|
f"{op_desc.type()} only the batch dimension (1-dim) of XShape can be sharded, but the dimension 0 is sharded by {mapping} part."
|
|
)
|
|
if len(dims_mapping) > 2:
|
|
for idx, mapping in enumerate(dims_mapping[2:]):
|
|
assert mapping == -1, (
|
|
f"{op_desc.type()} only the batch dimension (1-dim) of XShape can be sharded, but the dimension {idx} is sharded by {mapping} part."
|
|
)
|
|
batch_dim_mappings.append(dims_mapping[1])
|
|
|
|
compatible_dim_mapping = compute_compatible_dim_mapping(batch_dim_mappings)
|
|
assert compatible_dim_mapping is not None, (
|
|
"There is no compatible dim mapping."
|
|
)
|
|
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 len(dims_mapping) >= 1 and compatible_dim_mapping != dims_mapping[0]:
|
|
dims_mapping[0] = compatible_dim_mapping
|
|
changed = True
|
|
for arg_name in op_desc.output_arg_names():
|
|
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 xshape_arg_names:
|
|
if (
|
|
len(dims_mapping) >= 1
|
|
and compatible_dim_mapping != dims_mapping[0]
|
|
):
|
|
dims_mapping[0] = compatible_dim_mapping
|
|
changed = True
|
|
else:
|
|
if compatible_dim_mapping != dims_mapping[1]:
|
|
dims_mapping[1] = compatible_dim_mapping
|
|
changed = True
|
|
|
|
return changed
|
|
|
|
|
|
def update_op_dims_mapping_by_elementwise_like_dist_impl(dist_op):
|
|
changed = False
|
|
op_dist_attr = dist_op.dist_attr
|
|
op_desc = dist_op.serial_op.desc
|
|
input_arg_names = op_desc.input_arg_names()
|
|
input_dims_mapping_dict = {}
|
|
input_dims_mapping_lens = {}
|
|
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)
|
|
input_dims_mapping_dict[arg_name] = dims_mapping
|
|
input_dims_mapping_lens[arg_name] = len(dims_mapping)
|
|
|
|
dims_mapping_list = []
|
|
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(max_dims_mapping_len)]
|
|
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[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()
|
|
for arg_name in output_arg_names:
|
|
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
|
assert len(dims_mapping) == max_dims_mapping_len
|
|
dims_mapping_list.append(dims_mapping)
|
|
|
|
compatible_dims_mapping = compute_compatible_dims_mapping(dims_mapping_list)
|
|
assert compatible_dims_mapping is not None, (
|
|
"There is no compatible dim mapping."
|
|
)
|
|
|
|
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:
|
|
dims_mapping = op_dist_attr.get_output_dims_mapping(arg_name)
|
|
if compatible_dims_mapping != dims_mapping:
|
|
op_dist_attr.set_output_dims_mapping(
|
|
arg_name, compatible_dims_mapping
|
|
)
|
|
changed = True
|
|
|
|
return changed
|
|
|
|
|
|
def get_all_distributed_main_program(
|
|
serial_program_info, dist_context, parallelizer
|
|
):
|
|
"Get all distributed main programs by dist_context."
|
|
from .dist_context import DistributedOperatorContext
|
|
|
|
cluster = serial_program_info.cluster
|
|
copied_parallelizer = copy.deepcopy(parallelizer)
|
|
all_dist_main_program = []
|
|
ranks = (
|
|
paddle.distributed.get_world_size()
|
|
if cluster is None
|
|
else len(cluster.get_all_devices("GPU"))
|
|
)
|
|
for rank_id in range(ranks):
|
|
used_dist_context = copy.deepcopy(dist_context)
|
|
used_dist_context._dist_op_context = DistributedOperatorContext()
|
|
(
|
|
_,
|
|
_,
|
|
dist_startup_program,
|
|
dist_main_program,
|
|
_,
|
|
) = copied_parallelizer._get_dist_program(rank_id, used_dist_context)
|
|
all_dist_main_program.append(dist_main_program)
|
|
|
|
return all_dist_main_program
|
|
|
|
|
|
class SerialProgramInfo:
|
|
def __init__(
|
|
self, train_program, startup_program, loss, optimizer, cluster=None
|
|
):
|
|
self._train_program = train_program
|
|
self._startup_program = startup_program
|
|
self._loss = loss
|
|
self._optimizer = optimizer
|
|
self._cluster = cluster
|
|
|
|
@property
|
|
def train_program(self):
|
|
return self._train_program
|
|
|
|
@property
|
|
def startup_program(self):
|
|
return self._startup_program
|
|
|
|
@property
|
|
def loss(self):
|
|
return self._loss
|
|
|
|
@property
|
|
def optimizer(self):
|
|
return self._optimizer
|
|
|
|
@property
|
|
def cluster(self):
|
|
return self._cluster
|
|
|
|
|
|
def get_standalone_cost_data(distributed_programs):
|
|
def _compute_runtime(op_cost, op, vars):
|
|
runtime = 0
|
|
try:
|
|
runtime = float(op_cost["op_time"])
|
|
except:
|
|
return runtime
|
|
op_config = op_cost["config"]
|
|
total_static_input_size = 0
|
|
total_actual_input_size = 0
|
|
parsed_info = op_config.split("\n")
|
|
variable = "(Variable)"
|
|
for info in parsed_info:
|
|
variable = (
|
|
"(Variable)" if "(Variable)" in info else "(list<Variable>"
|
|
)
|
|
if variable in info:
|
|
arg_name_lower = info[: info.find(variable) - 1]
|
|
shape_left_boundary = info.find("[")
|
|
shape_right_boundary = info.find("]")
|
|
assert (
|
|
shape_left_boundary > 0
|
|
and shape_right_boundary > 0
|
|
and shape_right_boundary > shape_left_boundary
|
|
), "Get shape failed."
|
|
shape = info[
|
|
shape_left_boundary + 1 : shape_right_boundary
|
|
].split(",")
|
|
shape = [int(x.strip()) for x in shape]
|
|
dtype_factor = 1
|
|
total_static_input_size += reduce(lambda x, y: x * y, shape, 1)
|
|
if op.type == "c_embedding":
|
|
arg_name_lower = (
|
|
"w" if arg_name_lower == "weight" else "ids"
|
|
)
|
|
for arg_name in op.input_names:
|
|
if arg_name.lower() == arg_name_lower:
|
|
for var_name in op.input(arg_name):
|
|
var = vars[var_name]
|
|
total_actual_input_size += reduce(
|
|
lambda x, y: x * y, var.shape
|
|
)
|
|
break
|
|
assert total_static_input_size > 0 and total_actual_input_size > 0, (
|
|
"Get input size failed."
|
|
)
|
|
|
|
actual_runtime = (
|
|
total_actual_input_size / total_static_input_size * runtime
|
|
)
|
|
return actual_runtime
|
|
|
|
import paddle.cost_model as cm
|
|
|
|
cost_model = cm.CostModel()
|
|
cost_model.static_cost_data()
|
|
DEFAULT_MULTIPLE = 2
|
|
OP_NAME_MAPPING = {
|
|
"c_embedding": "embedding",
|
|
"matmul_v2": "matmul",
|
|
"transpose2": "transpose",
|
|
"reshape2": "reshape",
|
|
"unsqueeze2": "unsqueeze",
|
|
"reduce_sum": "sum",
|
|
"elementwise_div": "divide",
|
|
}
|
|
|
|
standalone_cost_data = []
|
|
# skip ops
|
|
not_enum_ops = [
|
|
"create_py_reader",
|
|
"create_double_buffer_reader",
|
|
"read",
|
|
"assign",
|
|
]
|
|
for distributed_program in distributed_programs:
|
|
cost_data = {}
|
|
vars = distributed_program.global_block().vars
|
|
for op in distributed_program.global_block().ops:
|
|
runtime = 0
|
|
if op.type in not_enum_ops:
|
|
cost_data[op.desc.id()] = runtime
|
|
continue
|
|
dtype = (
|
|
str(vars[op.input_arg_names[0]].dtype)
|
|
if op.input_arg_names
|
|
else "float32"
|
|
)
|
|
if int(op.attr('op_role')) == int(OpRole.Backward):
|
|
if "_grad" in op.type:
|
|
forward_op_name = op.type[:-5]
|
|
if forward_op_name in OP_NAME_MAPPING.keys():
|
|
forward_op_name = OP_NAME_MAPPING[forward_op_name]
|
|
op_cost = cost_model.get_static_op_time(
|
|
forward_op_name, forward=False, dtype=dtype
|
|
)
|
|
if op_cost:
|
|
runtime = _compute_runtime(op_cost, op, vars)
|
|
else:
|
|
op_cost = cost_model.get_static_op_time(
|
|
forward_op_name, dtype=dtype
|
|
)
|
|
if op_cost:
|
|
runtime = 2 * _compute_runtime(op_cost, op, vars)
|
|
elif int(op.attr('op_role')) == int(OpRole.Forward):
|
|
op_name = (
|
|
OP_NAME_MAPPING[op.type]
|
|
if op.type in OP_NAME_MAPPING.keys()
|
|
else op.type
|
|
)
|
|
op_cost = cost_model.get_static_op_time(op_name)
|
|
if op_cost:
|
|
runtime = _compute_runtime(op_cost, op, vars)
|
|
|
|
cost_data[op.desc.id()] = runtime
|
|
|
|
standalone_cost_data.append(cost_data)
|
|
|
|
return standalone_cost_data
|
|
|
|
|
|
def set_dist_op_desc_original_id(dist_op_desc, op_desc, dist_context):
|
|
op_id = op_desc.id()
|
|
op_original_id = op_desc.original_id()
|
|
# First, try to set the original id to the id of the op_desc
|
|
if op_id in dist_context._dist_ops_for_program:
|
|
dist_op_desc.set_original_id(op_id)
|
|
return
|
|
# Second, try to set the original id to the original_id of the op_desc
|
|
elif op_original_id in dist_context._dist_ops_for_program:
|
|
dist_op_desc.set_original_id(op_original_id)
|
|
return
|
|
# Third, print error information if we cannot find the original id
|
|
else:
|
|
raise AssertionError(
|
|
"Cannot find the original id in the distributed context"
|
|
)
|
|
|
|
|
|
def to_list(value):
|
|
if value is None:
|
|
return value
|
|
if isinstance(value, (list, tuple)):
|
|
return list(value)
|
|
return [value]
|
|
|
|
|
|
def debug_program(program, path, name):
|
|
filename = os.path.join(
|
|
path, f"{name}_program.{paddle.distributed.get_rank()}"
|
|
)
|
|
with open(filename, 'w') as f:
|
|
f.write(str(program))
|
|
|
|
|
|
def ring_id_to_process_group(ring_id):
|
|
from .process_group import get_all_process_groups
|
|
|
|
for g in get_all_process_groups():
|
|
if g.id == ring_id:
|
|
return g
|
|
return None
|
|
|
|
|
|
def find_higher_order_backward_op(program):
|
|
higher_order_op_suffix = ['_grad_grad', 'triple_grad']
|
|
for block in program.blocks:
|
|
for op in block.ops:
|
|
for suffix in higher_order_op_suffix:
|
|
if suffix in op.type:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def get_var_numel(var):
|
|
"""
|
|
input:
|
|
- var: variable
|
|
return:
|
|
number of element in var
|
|
"""
|
|
assert isinstance(var, Variable)
|
|
assert -1 not in var.shape
|
|
return reduce(lambda x, y: x * y, var.shape, 1)
|
|
|
|
|
|
def get_lr(optimizer):
|
|
if isinstance(optimizer, paddle.optimizer.Optimizer):
|
|
return optimizer.get_lr()
|
|
elif isinstance(optimizer, paddle.static.Optimizer):
|
|
if isinstance(optimizer._learning_rate, float):
|
|
return optimizer._learning_rate
|
|
else:
|
|
return optimizer._learning_rate()
|
|
else:
|
|
raise TypeError(
|
|
"'optimizer' must be object of class `paddle.optimizer.Optimizer`"
|
|
f" or `paddle.static.Optimizer`, but got {type(optimizer)}."
|
|
)
|
|
|
|
|
|
def initialize_pg_in_full_mode(all_process_groups, cur_rank):
|
|
import socket
|
|
|
|
from ...collective import _get_global_env
|
|
|
|
has_recv_by_socket = []
|
|
# This is a magic number
|
|
magic_num = 500
|
|
genv = _get_global_env()
|
|
cur_rank_ip, cur_rank_port = genv.current_endpoint.split(":")
|
|
cur_rank_recv_port = int(cur_rank_port) + magic_num
|
|
server_socket = None
|
|
# Large enough for recv rank
|
|
buff_size = 1024
|
|
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
server_socket.bind((cur_rank_ip, cur_rank_recv_port))
|
|
# The 10 is an empirical value
|
|
server_socket.listen(10)
|
|
client_sockets = {}
|
|
for process_group in all_process_groups:
|
|
if cur_rank not in process_group.ranks:
|
|
continue
|
|
if len(process_group.ranks) == 2:
|
|
index = process_group.ranks.index(cur_rank)
|
|
is_send = True if index == 0 else False
|
|
if is_send:
|
|
recv_rank = process_group.ranks[1]
|
|
recv_rank_ip, recv_rank_port = genv.trainer_endpoints[
|
|
recv_rank
|
|
].split(":")
|
|
connect_port = int(recv_rank_port) + magic_num
|
|
client_socket = socket.socket(
|
|
socket.AF_INET, socket.SOCK_STREAM
|
|
)
|
|
client_socket.connect((recv_rank_ip, connect_port))
|
|
client_socket.send(str(cur_rank).encode('utf-8'))
|
|
rank = client_socket.recv(buff_size).decode('utf-8')
|
|
rank = int(rank)
|
|
if rank != recv_rank:
|
|
raise ValueError(
|
|
f"Please check comm pair, the recv rank should be {recv_rank} but got {rank}."
|
|
)
|
|
else:
|
|
print(
|
|
f"It is able to instantiate {process_group.ranks} as sender now."
|
|
)
|
|
client_socket.close()
|
|
else:
|
|
send_rank = process_group.ranks[0]
|
|
while True:
|
|
if send_rank not in has_recv_by_socket:
|
|
client_socket, recv_addr = server_socket.accept()
|
|
rank = int(client_socket.recv(buff_size).decode())
|
|
client_sockets[rank] = client_socket
|
|
has_recv_by_socket.append(rank)
|
|
else:
|
|
client_sockets[send_rank].send(
|
|
str(cur_rank).encode("utf-8")
|
|
)
|
|
client_sockets[send_rank].close()
|
|
print(
|
|
f"It is able to instantiate {process_group.ranks} as receiver now."
|
|
)
|
|
break
|
|
process_group.instantiate()
|
|
server_socket.close()
|
|
|
|
|
|
def is_recompute_op(op):
|
|
return (
|
|
op.has_attr('op_namescope')
|
|
and "/auto_parallel/rc" in op.attr('op_namescope')
|
|
and 'exclude_rc' not in op.attr('op_namescope')
|
|
)
|
|
|
|
|
|
def is_recompute_exclude_op(op):
|
|
return op.has_attr('op_namescope') and 'exclude_rc' in op.attr(
|
|
'op_namescope'
|
|
)
|
|
|
|
|
|
def set_recompute_segments(model, losses, strategy, program):
|
|
from ...passes.auto_parallel_recompute import RecomputeState
|
|
|
|
if not losses:
|
|
return
|
|
|
|
recompute = strategy.recompute
|
|
if not recompute.enable:
|
|
return
|
|
|
|
# NOTE: hack to enable recompute in engine api for GPT-3
|
|
# TODO support more PaddleNLP/CV models here
|
|
# extract ckpts by specific model
|
|
ckpts = []
|
|
if isinstance(model, paddle.nn.Layer):
|
|
if (
|
|
hasattr(model, "gpt")
|
|
and model.__class__.__name__
|
|
in [
|
|
'GPTForPretraining',
|
|
'GPTForPretrainingAuto',
|
|
]
|
|
and hasattr(model.gpt, "checkpoints")
|
|
):
|
|
ckpts = model.gpt.checkpoints
|
|
# last recompute segment is not need to recompute
|
|
if len(ckpts) > 2:
|
|
ckpts.pop()
|
|
else:
|
|
ckpts = recompute.checkpoints
|
|
else:
|
|
ckpts = recompute.checkpoints
|
|
|
|
if not ckpts:
|
|
return
|
|
|
|
block = program.global_block()
|
|
rc_state = RecomputeState(block, block.ops)
|
|
rc_state.build_stats()
|
|
checkpoints = rc_state.sort_checkpoints(ckpts)
|
|
|
|
segments = []
|
|
start_idx = -1
|
|
pre_segment_end_idx = -1
|
|
while start_idx + 1 < len(checkpoints):
|
|
if start_idx == -1:
|
|
ckpt_name = checkpoints[start_idx + 1]
|
|
if ckpt_name not in rc_state.var_op_deps:
|
|
start_idx += 1
|
|
continue
|
|
op_idx_list = rc_state.var_op_deps[ckpt_name]["var_as_output_ops"]
|
|
if op_idx_list and max(op_idx_list) > 0:
|
|
segments.append([0, max(op_idx_list) + 1])
|
|
else:
|
|
flag, min_idx, max_idx = rc_state.is_subgraph(
|
|
[checkpoints[start_idx]], [checkpoints[start_idx + 1]]
|
|
)
|
|
if flag:
|
|
min_idx = rc_state._update_segment_start(
|
|
min_idx, pre_segment_end_idx
|
|
)
|
|
segments.append([min_idx, max_idx + 1])
|
|
else:
|
|
logging.debug(
|
|
f"Could not recompute op range [{min_idx}] - [{max_idx + 1}] "
|
|
)
|
|
start_idx += 1
|
|
|
|
for i, segment in enumerate(segments):
|
|
for j in range(segment[0], segment[1]):
|
|
block.ops[j]._set_attr(
|
|
'op_namescope', "/auto_parallel/rc_" + str(i)
|
|
)
|
|
|
|
|
|
def get_input_split_info(cur_rank, var, dist_context):
|
|
# deduce how the input data is split among the cluster
|
|
tensor_dist_attr = dist_context.get_tensor_dist_attr_for_program(var)
|
|
process_mesh = tensor_dist_attr.process_mesh
|
|
dims_mapping = tensor_dist_attr.dims_mapping
|
|
|
|
if cur_rank not in process_mesh.process_ids:
|
|
rank_id = _get_corresponding_rank(dist_context, process_mesh, cur_rank)
|
|
else:
|
|
rank_id = cur_rank
|
|
|
|
batch_size_axis = dims_mapping[0]
|
|
if batch_size_axis > -1 and process_mesh.shape[batch_size_axis] > 1:
|
|
group_ranks = _get_comm_group(
|
|
process_mesh.process_ids,
|
|
process_mesh.shape,
|
|
batch_size_axis,
|
|
rank_id,
|
|
)
|
|
return len(group_ranks), group_ranks.index(rank_id)
|
|
|
|
return 1, 0
|
|
|
|
|
|
def validate_opt(optimizer):
|
|
if optimizer is not None:
|
|
optimizer._parameter_list = None
|
|
optimizer._param_groups = None
|
|
if optimizer._grad_clip and isinstance(
|
|
optimizer._grad_clip, paddle.nn.ClipGradByGlobalNorm
|
|
):
|
|
optimizer._grad_clip._async_add_n = True
|
|
return optimizer
|
|
|
|
|
|
def set_data_parallel(x):
|
|
from ..interface import ProcessMesh, shard_tensor
|
|
from .process_group import get_world_process_group
|
|
|
|
world_ranks = get_world_process_group().ranks
|
|
process_mesh = ProcessMesh(world_ranks, ['dp'])
|
|
shard_spec = ['dp' if len(world_ranks) > 1 else None] + [
|
|
None for _ in range(len(x.shape) - 1)
|
|
]
|
|
|
|
return shard_tensor(x, process_mesh, shard_spec)
|
|
|
|
|
|
def is_naive_data_parallel(dist_context):
|
|
# Naive data parallel only completes dist_attr once from the front to back.
|
|
if not dist_context.data_parallel:
|
|
return False
|
|
|
|
ops_type = [
|
|
op.type
|
|
for op in dist_context._original_serial_main_program.global_block().ops
|
|
]
|
|
if (
|
|
not set(ops_type) & set(__not_naive_data_parallel_op__)
|
|
) and dist_context.data_parallel:
|
|
return True
|
|
return False
|
|
|
|
|
|
def _copy_tensor_dist_attr_to_cpp(cpp_dist_attr, py_dist_attr):
|
|
py_process_mesh = py_dist_attr.process_mesh
|
|
if py_process_mesh is not None:
|
|
cpp_dist_attr.process_mesh = core.ProcessMesh(
|
|
py_process_mesh.shape,
|
|
py_process_mesh.process_ids,
|
|
["d" + str(i) for i in range(len(py_process_mesh.shape))],
|
|
)
|
|
cpp_dist_attr.dims_mapping = py_dist_attr.dims_mapping
|
|
cpp_dist_attr.annotated = py_dist_attr.annotated
|
|
|
|
|
|
def _copy_tensor_dist_attr_from_cpp(cpp_dist_attr, py_dist_attr):
|
|
from ..process_mesh import ProcessMesh
|
|
|
|
cpp_process_mesh = cpp_dist_attr.process_mesh
|
|
if cpp_process_mesh is not None:
|
|
py_dist_attr.process_mesh = ProcessMesh(
|
|
shape=cpp_process_mesh.shape,
|
|
process_ids=cpp_process_mesh.process_ids,
|
|
)
|
|
py_dist_attr.dims_mapping = cpp_dist_attr.dims_mapping
|
|
py_dist_attr.annotated = cpp_dist_attr.annotated
|
|
|
|
|
|
def _copy_op_dist_attr_to_cpp(cpp_dist_attr, py_dist_attr):
|
|
py_process_mesh = py_dist_attr.process_mesh
|
|
if py_process_mesh is not None:
|
|
cpp_dist_attr.process_mesh = core.ProcessMesh(
|
|
py_process_mesh.shape,
|
|
py_process_mesh.process_ids,
|
|
["d" + str(i) for i in range(len(py_process_mesh.shape))],
|
|
)
|
|
cpp_dist_attr.impl_type = py_dist_attr.impl_type
|
|
cpp_dist_attr.impl_idx = py_dist_attr.impl_idx
|
|
cpp_dist_attr.is_recompute = py_dist_attr.is_recompute
|
|
cpp_dist_attr.annotated = py_dist_attr.annotated
|
|
for name, py_tensor_dist_attr in py_dist_attr.inputs_dist_attrs.items():
|
|
cpp_tensor_dist_attr = cpp_dist_attr.get_input_dist_attr(name)
|
|
_copy_tensor_dist_attr_to_cpp(cpp_tensor_dist_attr, py_tensor_dist_attr)
|
|
for name, py_tensor_dist_attr in py_dist_attr.outputs_dist_attrs.items():
|
|
cpp_tensor_dist_attr = cpp_dist_attr.get_output_dist_attr(name)
|
|
_copy_tensor_dist_attr_to_cpp(cpp_tensor_dist_attr, py_tensor_dist_attr)
|
|
|
|
|
|
def _copy_op_dist_attr_from_cpp(cpp_dist_attr, py_dist_attr):
|
|
from ..process_mesh import ProcessMesh
|
|
|
|
cpp_process_mesh = cpp_dist_attr.process_mesh
|
|
if cpp_process_mesh is not None:
|
|
py_dist_attr.process_mesh = ProcessMesh(
|
|
shape=cpp_process_mesh.shape,
|
|
process_ids=cpp_process_mesh.process_ids,
|
|
)
|
|
py_dist_attr.impl_type = cpp_dist_attr.impl_type
|
|
py_dist_attr.impl_idx = cpp_dist_attr.impl_idx
|
|
py_dist_attr.is_recompute = cpp_dist_attr.is_recompute
|
|
py_dist_attr.annotated = cpp_dist_attr.annotated
|
|
for name, cpp_tensor_dist_attr in cpp_dist_attr.inputs_dist_attrs.items():
|
|
py_tensor_dist_attr = py_dist_attr.get_input_dist_attr(name)
|
|
_copy_tensor_dist_attr_from_cpp(
|
|
cpp_tensor_dist_attr, py_tensor_dist_attr
|
|
)
|
|
for name, cpp_tensor_dist_attr in cpp_dist_attr.outputs_dist_attrs.items():
|
|
py_tensor_dist_attr = py_dist_attr.get_output_dist_attr(name)
|
|
_copy_tensor_dist_attr_from_cpp(
|
|
cpp_tensor_dist_attr, py_tensor_dist_attr
|
|
)
|
|
|
|
|
|
def _copy_dist_attr_to_cpp(dist_context):
|
|
for dist_tensor in dist_context._dist_tensors_for_program.values():
|
|
_copy_tensor_dist_attr_to_cpp(
|
|
dist_tensor.serial_tensor.dist_attr, dist_tensor.dist_attr
|
|
)
|
|
|
|
for dist_op in dist_context._dist_ops_for_program.values():
|
|
_copy_op_dist_attr_to_cpp(
|
|
dist_op.serial_op.dist_attr, dist_op.dist_attr
|
|
)
|
|
|
|
|
|
def _copy_dist_attr_from_cpp(dist_context):
|
|
for dist_tensor in dist_context._dist_tensors_for_program.values():
|
|
_copy_tensor_dist_attr_from_cpp(
|
|
dist_tensor.serial_tensor.dist_attr, dist_tensor.dist_attr
|
|
)
|
|
|
|
for dist_op in dist_context._dist_ops_for_program.values():
|
|
_copy_op_dist_attr_from_cpp(
|
|
dist_op.serial_op.dist_attr, dist_op.dist_attr
|
|
)
|
|
|
|
|
|
def _copy_dist_attr_to_cpp_for_graph(dist_context):
|
|
for node in dist_context.serial_ordered_nodes:
|
|
if node.is_var() and node.var() is not None:
|
|
py_dist_attr = dist_context.get_tensor_dist_attr_for_graph(node)
|
|
cpp_dist_attr = node.var().dist_attr
|
|
_copy_tensor_dist_attr_to_cpp(cpp_dist_attr, py_dist_attr)
|
|
if node.is_op() and node.op() is not None:
|
|
py_dist_attr = dist_context.get_op_dist_attr_for_graph(node)
|
|
cpp_dist_attr = node.op().dist_attr
|
|
_copy_op_dist_attr_to_cpp(cpp_dist_attr, py_dist_attr)
|
|
|
|
|
|
def _copy_dist_attr_from_cpp_for_graph(dist_context):
|
|
for node in dist_context.serial_ordered_nodes:
|
|
if node.is_var() and node.var() is not None:
|
|
py_dist_attr = dist_context.get_tensor_dist_attr_for_graph(node)
|
|
cpp_dist_attr = node.var().dist_attr
|
|
_copy_tensor_dist_attr_from_cpp(cpp_dist_attr, py_dist_attr)
|
|
if node.is_op() and node.op() is not None:
|
|
py_dist_attr = dist_context.get_op_dist_attr_for_graph(node)
|
|
cpp_dist_attr = node.op().dist_attr
|
|
_copy_op_dist_attr_from_cpp(cpp_dist_attr, py_dist_attr)
|
|
|
|
|
|
def insert_dependencies_for_two_ops(
|
|
block,
|
|
idx,
|
|
prior_op,
|
|
posterior_op,
|
|
dist_context,
|
|
is_recompute=False,
|
|
sync=False,
|
|
op_namescope=None,
|
|
):
|
|
"""
|
|
dependency: prior_op should be run before posterior_op
|
|
"""
|
|
|
|
if is_sequential_run():
|
|
return
|
|
|
|
assert len(prior_op.output_arg_names) >= 1, (
|
|
f"first op of dependency should at least have one output. [{prior_op}]"
|
|
)
|
|
assert len(posterior_op.input_arg_names) >= 1, (
|
|
f"second op of dependency should at least have one input. [{posterior_op}]"
|
|
)
|
|
prior_op_mesh = dist_context.get_op_dist_attr_for_program(
|
|
prior_op
|
|
).process_mesh
|
|
posterior_mesh = dist_context.get_op_dist_attr_for_program(
|
|
posterior_op
|
|
).process_mesh
|
|
assert prior_op_mesh == posterior_mesh, (
|
|
f"two ops of dependency should have same mesh but got [{prior_op_mesh}] and [{posterior_mesh}]"
|
|
)
|
|
|
|
def _select_best_depend_var(vars):
|
|
# parameter should not be dep var since it maybe partition in sharding pass
|
|
vars = [var for var in vars if not var.is_parameter]
|
|
assert len(vars) > 0
|
|
vars_with_numels = [(var, get_var_numel(var)) for var in vars]
|
|
vars_with_numels.sort(key=lambda x: x[1])
|
|
|
|
return vars_with_numels[-1][0]
|
|
|
|
first_var = _select_best_depend_var(
|
|
[block.var(name) for name in prior_op.output_arg_names]
|
|
)
|
|
second_var = _select_best_depend_var(
|
|
[block.var(name) for name in posterior_op.input_arg_names]
|
|
)
|
|
|
|
return insert_dependencies_for_vars(
|
|
block,
|
|
idx,
|
|
first_var,
|
|
second_var,
|
|
dist_context,
|
|
OpRole.Backward,
|
|
process_mesh=prior_op_mesh,
|
|
is_recompute=is_recompute,
|
|
sync=sync,
|
|
op_namescope=op_namescope,
|
|
use_nop=False,
|
|
)
|
|
|
|
|
|
def insert_dependencies_for_vars(
|
|
block,
|
|
idx,
|
|
prior_vars,
|
|
post_vars,
|
|
dist_context,
|
|
oprole,
|
|
process_mesh=None,
|
|
is_recompute=False,
|
|
sync=False,
|
|
op_namescope=None,
|
|
use_nop=False,
|
|
skip_insert_when_sequential_run=True,
|
|
):
|
|
"""
|
|
dependency: op that generates prior_vars should be run before op that generates post_vars
|
|
"""
|
|
|
|
if skip_insert_when_sequential_run and is_sequential_run():
|
|
return
|
|
|
|
if isinstance(prior_vars, Variable):
|
|
prior_vars = [prior_vars]
|
|
if isinstance(post_vars, Variable):
|
|
post_vars = [post_vars]
|
|
for prior_var in prior_vars:
|
|
assert block.has_var(prior_var.name)
|
|
for post_var in post_vars:
|
|
assert block.has_var(post_var.name)
|
|
|
|
post_dist_attr = dist_context.get_tensor_dist_attr_for_program(post_vars[0])
|
|
if process_mesh is None:
|
|
process_mesh = post_dist_attr.process_mesh
|
|
assert process_mesh is not None
|
|
|
|
use_nop = True
|
|
if use_nop:
|
|
depend_op = block._insert_op_without_sync(
|
|
idx,
|
|
type='nop',
|
|
inputs={
|
|
"X": prior_vars,
|
|
},
|
|
outputs={"Out": post_vars},
|
|
)
|
|
else:
|
|
depend_op = block._insert_op_without_sync(
|
|
idx,
|
|
type='depend',
|
|
inputs={
|
|
"X": post_vars,
|
|
"Dep": prior_vars,
|
|
},
|
|
outputs={"Out": post_vars},
|
|
)
|
|
depend_op._set_attr(OP_ROLE_KEY, oprole)
|
|
|
|
# TODO: condition can be removed when add correct dist_attr for coalesce vars and ops in sharding_pass
|
|
if is_recompute or process_mesh != [-1]:
|
|
depend_op_dist_attr = OperatorDistAttr()
|
|
depend_op_dist_attr.impl_idx = 0
|
|
depend_op_dist_attr.impl_type = "default"
|
|
depend_op_dist_attr.process_mesh = process_mesh
|
|
depend_op_dist_attr.is_recompute = is_recompute
|
|
depend_op_dist_attr.chunk_id = post_dist_attr.chunk_id
|
|
for input_varname in depend_op.desc.input_arg_names():
|
|
var = block.var(input_varname)
|
|
mapping = dist_context.get_tensor_dist_attr_for_program(
|
|
var
|
|
).dims_mapping
|
|
depend_op_dist_attr.set_input_dims_mapping(input_varname, mapping)
|
|
for output_varname in depend_op.desc.output_arg_names():
|
|
var = block.var(output_varname)
|
|
mapping = dist_context.get_tensor_dist_attr_for_program(
|
|
var
|
|
).dims_mapping
|
|
depend_op_dist_attr.set_output_dims_mapping(output_varname, mapping)
|
|
dist_context.set_op_dist_attr_for_program(
|
|
depend_op, depend_op_dist_attr
|
|
)
|
|
|
|
if op_namescope is not None:
|
|
depend_op._set_attr('op_namescope', f"/{op_namescope}")
|
|
|
|
if sync:
|
|
block._sync_with_cpp()
|
|
|
|
return depend_op
|
|
|
|
|
|
def is_dep_skip_op(op):
|
|
if "c_" in op.type:
|
|
return True
|
|
|
|
return False
|
|
|
|
|
|
def _dygraph_guard_(func):
|
|
def __impl__(*args, **kwargs):
|
|
if paddle.framework.in_dynamic_mode():
|
|
return func(*args, **kwargs)
|
|
else:
|
|
with paddle.base.dygraph.guard():
|
|
return func(*args, **kwargs)
|
|
|
|
return __impl__
|
|
|
|
|
|
dygraph_guard = wrap_decorator(_dygraph_guard_)
|
|
|
|
|
|
def is_sequential_run():
|
|
return bool(
|
|
paddle.get_flags("FLAGS_new_executor_sequential_run")[
|
|
"FLAGS_new_executor_sequential_run"
|
|
]
|
|
)
|
|
|
|
|
|
def get_pp_degree(dist_context):
|
|
if len(dist_context.process_meshes) < 2:
|
|
return 0, []
|
|
|
|
sub_process_meshes = get_sub_process_mesh(dist_context.process_meshes)
|
|
return len(sub_process_meshes), sub_process_meshes
|
|
|
|
|
|
def get_sub_process_mesh_by_program(dist_program):
|
|
all_ops = dist_program.global_block().ops
|
|
process_meshes = []
|
|
|
|
for idx, op in enumerate(all_ops):
|
|
if "pd_op" in op.name() and op.dist_attr:
|
|
process_mesh = op.dist_attr.process_mesh
|
|
if process_mesh not in process_meshes:
|
|
process_meshes.append(process_mesh)
|
|
|
|
sub_process_meshes = get_sub_process_mesh(process_meshes)
|
|
sub_process_meshes = sorted(
|
|
sub_process_meshes, key=lambda x: x.process_ids[0]
|
|
)
|
|
|
|
return sub_process_meshes
|
|
|
|
|
|
def get_sub_process_mesh(process_meshes):
|
|
process_ids = set()
|
|
sub_process_meshes = copy.deepcopy(process_meshes)
|
|
|
|
for pm in sub_process_meshes:
|
|
process_ids |= set(pm.process_ids)
|
|
|
|
global_pm_idx = []
|
|
has_sub_pm = False
|
|
for idx, pm in enumerate(sub_process_meshes):
|
|
if len(set(pm.process_ids)) == len(process_ids):
|
|
global_pm_idx.append(idx)
|
|
elif set(pm.process_ids) < process_ids:
|
|
has_sub_pm = True
|
|
|
|
if has_sub_pm:
|
|
for idx in reversed(global_pm_idx):
|
|
sub_process_meshes.pop(idx)
|
|
|
|
return sub_process_meshes
|
|
|
|
|
|
def get_pp_stage(dist_context, rank):
|
|
pp_idx = None
|
|
for idx, process_mesh in enumerate(dist_context.process_meshes):
|
|
if rank in process_mesh.process_ids:
|
|
pp_idx = idx
|
|
break
|
|
return pp_idx
|
|
|
|
|
|
def get_pp_stage_by_pp_degree(pp_degree):
|
|
cur_rank = paddle.distributed.get_rank()
|
|
return get_pp_stage_by_rank(cur_rank, pp_degree)
|
|
|
|
|
|
def get_pp_stage_by_process_mesh(process_mesh, pp_degree):
|
|
pp_stage_for_process_mesh = None
|
|
for rank in process_mesh.process_ids:
|
|
pp_stage = get_pp_stage_by_rank(rank, pp_degree)
|
|
if pp_stage_for_process_mesh is not None:
|
|
if pp_stage != pp_stage_for_process_mesh:
|
|
return None
|
|
assert pp_stage == pp_stage_for_process_mesh, (
|
|
f"Can't get pp_stage by process_mesh with different pp_stage {pp_stage} and {pp_stage_for_process_mesh}"
|
|
)
|
|
pp_stage_for_process_mesh = pp_stage
|
|
|
|
return pp_stage_for_process_mesh
|
|
|
|
|
|
def get_pp_stage_by_rank(rank, pp_degree):
|
|
word_size = paddle.distributed.get_world_size()
|
|
pp_group_size = word_size // pp_degree
|
|
pp_stage = rank // pp_group_size
|
|
return pp_stage
|
|
|
|
|
|
def wrap_data_for_completion(
|
|
dist_op, input_names: list, output_names: list, attr_names: list
|
|
):
|
|
"""
|
|
Get data used in inferring distributed attributes, including:
|
|
1. DistTensorSpec for each input and output tensor of this dist_op.
|
|
2. Operator attributes of this dist_op, e.g. transpose_x in matmul op.
|
|
|
|
Args:
|
|
dist_op: the DistributedOperator
|
|
input_names: list, name of the dist_op's input tensors
|
|
output_names: list, name of the dist_op's output tensors
|
|
attr_names: list, attribute name of the dist_op's corresponding serial op
|
|
|
|
Returns:
|
|
input_specs: list, DistTensorSpec for each input tensor of the dist_op
|
|
output_specs: list, DistTensorSpec for each output tensor of the dist_op
|
|
attrs: dict, attribute map of the dist op
|
|
|
|
Examples:
|
|
.. code-block:: pycon
|
|
|
|
>>> # doctest: +SKIP('Depends on other ops.')
|
|
>>> from paddle.distributed.auto_parallel.static.utils import wrap_data_for_completion
|
|
|
|
>>> op_desc = dist_op.serial_op.desc
|
|
>>> input_name_list = []
|
|
>>> output_name_list = []
|
|
|
|
>>> input_name_list.append(op_desc.input('X')[0]) # 'X' is the arg name for op
|
|
>>> input_name_list.append(op_desc.input('Y')[0])
|
|
>>> output_name_list.append(op_desc.output('Out')[0])
|
|
|
|
>>> attr_name_list = ['trans_x', 'trans_y']
|
|
>>> input_specs, output_specs, attrs = wrap_data_for_completion(
|
|
... dist_op,
|
|
... input_name_list,
|
|
... output_name_list,
|
|
... attr_name_list,
|
|
... )
|
|
|
|
"""
|
|
|
|
input_specs = []
|
|
output_specs = []
|
|
attrs = {}
|
|
|
|
serial_op = dist_op.serial_op
|
|
|
|
# Construct each input tensor's DistTensorSpec with shape and dist_attr
|
|
for name in input_names:
|
|
tensor_dist_attr = dist_op.dist_attr.get_input_dist_attr(name)
|
|
var = serial_op.block._var_recursive(name)
|
|
tensor_shape = var.shape
|
|
dist_spec = DistTensorSpec(tensor_shape, tensor_dist_attr)
|
|
input_specs.append(dist_spec)
|
|
|
|
# Construct each output tensor's DistTensorSpec with shape and dist_attr
|
|
for name in output_names:
|
|
tensor_dist_attr = dist_op.dist_attr.get_output_dist_attr(name)
|
|
var = serial_op.block._var_recursive(name)
|
|
tensor_shape = var.shape
|
|
dist_spec = DistTensorSpec(tensor_shape, tensor_dist_attr)
|
|
output_specs.append(dist_spec)
|
|
|
|
for attr_name in attr_names:
|
|
attrs[attr_name] = serial_op.desc.attr(attr_name)
|
|
|
|
return input_specs, output_specs, attrs
|
|
|
|
|
|
def get_dist_tensor_spec(dist_op, name, is_input=True):
|
|
tensor_shape = dist_op.serial_op.block._var_recursive(name).shape
|
|
if is_input:
|
|
tensor_dist_attr = dist_op.dist_attr.get_input_dist_attr(name)
|
|
else:
|
|
tensor_dist_attr = dist_op.dist_attr.get_output_dist_attr(name)
|
|
return DistTensorSpec(tensor_shape, tensor_dist_attr)
|
|
|
|
|
|
# get grad_var_to_var from distributed context, recording the mapping from backward grad variable to forward variable
|
|
# which is used for decomposing backward ops when enabling prim after distributed
|
|
def get_grad_var_to_var(dist_context):
|
|
# get grad_var_to_var in distributed context
|
|
grad_var_to_var_map = dist_context._dist_op_context.grad_var_to_var
|
|
assert len(grad_var_to_var_map.keys()) == 1, "invalid grad_var_to_var"
|
|
grad_var_to_var = grad_var_to_var_map[1]
|
|
return grad_var_to_var
|
|
|
|
|
|
# update grad_var_to_var manually according to different distributed pass or strategy, thus recording complete and correct mapping between backward to forward
|
|
def update_grad_var_to_var(program, strategy, grad_var_to_var):
|
|
from paddle.distributed.fleet.meta_optimizers.common import (
|
|
OP_ROLE_KEY,
|
|
OpRole,
|
|
)
|
|
|
|
# update grad_var_to_var according to different distributed pass
|
|
first_backward_op_idx = -1
|
|
for idx, op in enumerate(program.global_block().ops):
|
|
# process @RESHARD variable in distributed training
|
|
if (
|
|
op.has_attr("op_namescope")
|
|
and op.attr("op_namescope") == "/auto_parallel/reshard"
|
|
):
|
|
reshard_op_types = [
|
|
"split",
|
|
"assign",
|
|
"cast",
|
|
"c_concat",
|
|
"concat",
|
|
"slice",
|
|
"all_gather",
|
|
]
|
|
if op.desc.type() in reshard_op_types:
|
|
input_names = op.desc.input_names()
|
|
if (
|
|
"X" in input_names
|
|
or "Input" in input_names
|
|
or "x" in input_names
|
|
):
|
|
inputs = (
|
|
op.desc.input("X")
|
|
if "X" in input_names
|
|
else (
|
|
op.desc.input("Input")
|
|
if "Input" in input_names
|
|
else op.desc.input("x")
|
|
)
|
|
)
|
|
output_names = op.desc.output_names()
|
|
if "Out" in output_names or "out" in output_names:
|
|
outputs = (
|
|
op.desc.output("Out")
|
|
if "Out" in output_names
|
|
else op.desc.output("out")
|
|
)
|
|
if inputs[0] in grad_var_to_var.keys():
|
|
for output in outputs:
|
|
grad_var_to_var[output] = grad_var_to_var[inputs[0]]
|
|
|
|
# process amp pass in distributed training
|
|
if (
|
|
strategy.amp.enable
|
|
and op.has_attr(OP_ROLE_KEY)
|
|
and (op.attr(OP_ROLE_KEY) & int(OpRole.Backward))
|
|
and (op.attr(OP_ROLE_KEY) & int(OpRole.Loss))
|
|
):
|
|
first_backward_op_idx = idx
|
|
|
|
# process amp pass in distributed training
|
|
if first_backward_op_idx != -1:
|
|
scale_loss_op = program.global_block().ops[first_backward_op_idx - 1]
|
|
scale_loss_var_name = scale_loss_op.desc.output("Out")[0]
|
|
first_backward_op = program.global_block().ops[first_backward_op_idx]
|
|
scale_loss_grad_var_name = first_backward_op.desc.output("Out")[0]
|
|
if scale_loss_grad_var_name not in grad_var_to_var.keys():
|
|
grad_var_to_var[scale_loss_grad_var_name] = scale_loss_var_name
|
|
|
|
|
|
def set_all_ops_op_role(block, op_role):
|
|
all_ops = block.ops
|
|
for op in all_ops:
|
|
if op.op_role == -1:
|
|
op.op_role = op_role
|
|
for sub_block in op.blocks():
|
|
set_all_ops_op_role(sub_block, op_role)
|
|
|
|
|
|
def fuse_param_func(
|
|
fuse_params, is_qkv=False, num_heads=None, num_key_value_heads=None
|
|
):
|
|
"""fuse function for fusing weights
|
|
|
|
(1) fuse_attention_qkv
|
|
q => [q1,q2,q3,q4]
|
|
k => [k1,k2,k3,k4] or [k1,k2] for GQA
|
|
v => [v1,v2,v3,v4] or [v1,v2] for GQA
|
|
fused weight => [q1,k1,v1,q2,k2,v2,q3,k3,v3,q4,k4,v4]
|
|
or for GQA [q1,q2,k1,v1,q3,q4,k2,v2]
|
|
(2) fuse_attention_ffn
|
|
directly fuse weights to 1 parts
|
|
[gate_weight], [up_weight] => [gate_weight, up_weight]
|
|
|
|
Args:
|
|
fuse_params (_type_): to be fused weights
|
|
is_qkv (bool, optional): for attention qkv weights. Defaults to False.
|
|
num_heads (_type_, optional): query heads. Defaults to None.
|
|
num_key_value_heads (_type_, optional): key and value heads. Defaults to None.
|
|
|
|
Returns:
|
|
_type_: fused weights
|
|
"""
|
|
concat_fn = paddle.concat
|
|
split_fn = paddle.split
|
|
|
|
if is_qkv:
|
|
# fuse_attention_qkv
|
|
assert num_heads, (
|
|
f"num_heads should be number of heads for Q, but got {num_heads}"
|
|
)
|
|
assert num_key_value_heads, (
|
|
f"num_key_value_heads should be number of key_value_heads for K and V, but got {num_key_value_heads}"
|
|
)
|
|
assert len(fuse_params) == 3, (
|
|
f"fuse_params length is not equal 3, it should be Q K V list. but got length {len(fuse_params)}"
|
|
)
|
|
num_query_groups = num_heads // num_key_value_heads
|
|
q_list = split_fn(fuse_params[0], num_heads, axis=-1)
|
|
k_list = split_fn(fuse_params[1], num_key_value_heads, axis=-1)
|
|
v_list = split_fn(fuse_params[2], num_key_value_heads, axis=-1)
|
|
|
|
qkv_pairs = []
|
|
for i in range(num_key_value_heads):
|
|
qkv_pairs += q_list[
|
|
i * num_query_groups : (i + 1) * num_query_groups
|
|
]
|
|
qkv_pairs.append(k_list[i])
|
|
qkv_pairs.append(v_list[i])
|
|
return concat_fn(qkv_pairs, axis=-1)
|
|
else:
|
|
# fuse_attention_ffn
|
|
return concat_fn(fuse_params, axis=-1)
|
|
|
|
|
|
def split_param_func(
|
|
fused_param,
|
|
split_nums=2,
|
|
is_qkv=False,
|
|
num_heads=None,
|
|
num_key_value_heads=None,
|
|
):
|
|
"""split function for splitting weights
|
|
|
|
(1) fuse_attention_qkv
|
|
fused weight => [q1,k1,v1,q2,k2,v2,q3,k3,v3,q4,k4,v4]
|
|
or for GQA [q1,q2,k1,v1,q3,q4,k2,v2]
|
|
after split
|
|
q => [q1,q2,q3,q4]
|
|
k => [k1,k2,k3,k4] or [k1,k2] for GQA
|
|
v => [v1,v2,v3,v4] or [v1,v2] for GQA
|
|
(2) fuse_attention_ffn
|
|
directly split weight to 2 parts
|
|
[gate_weight, up_weight] => [gate_weight], [up_weight]
|
|
|
|
Args:
|
|
fused_param (_type_): len(fused_param)=1, only one weight to be split
|
|
split_nums (int, optional): split_nums. Defaults to 2.
|
|
is_qkv (bool, optional): for attention qkv weights. Defaults to False.
|
|
num_heads (_type_, optional): query heads. Defaults to None.
|
|
num_key_value_heads (_type_, optional): key and value heads. Defaults to None.
|
|
|
|
Returns:
|
|
_type_: split weights
|
|
"""
|
|
concat_fn = paddle.concat
|
|
split_fn = paddle.split
|
|
|
|
if is_qkv:
|
|
# fuse_attention_qkv
|
|
assert num_heads, (
|
|
f"num_heads should be number of heads for Q, but got {num_heads}"
|
|
)
|
|
assert num_key_value_heads, (
|
|
f"num_key_value_heads should be number of key_value_heads for K and V, but got {num_key_value_heads}"
|
|
)
|
|
num_query_groups = num_heads // num_key_value_heads
|
|
q_list, k_list, v_list = [], [], []
|
|
split_heads = split_fn(
|
|
fused_param, num_heads + 2 * num_key_value_heads, axis=-1
|
|
)
|
|
for i in range(num_key_value_heads):
|
|
q_list += split_heads[
|
|
i * (num_query_groups + 2) : (i + 1) * (num_query_groups + 2)
|
|
- 2
|
|
]
|
|
k_list.append(split_heads[(i + 1) * (num_query_groups + 2) - 2])
|
|
v_list.append(split_heads[(i + 1) * (num_query_groups + 2) - 1])
|
|
return (
|
|
concat_fn(q_list, axis=-1),
|
|
concat_fn(k_list, axis=-1),
|
|
concat_fn(v_list, axis=-1),
|
|
)
|
|
else:
|
|
# fuse_attention_ffn
|
|
return split_fn(fused_param, split_nums, axis=-1)
|
|
|
|
|
|
def split_mesh(global_mesh: ProcessMesh, sub_mesh_dim: int):
|
|
mesh_shape = global_mesh.shape
|
|
mesh_ndim = len(mesh_shape)
|
|
if sub_mesh_dim >= mesh_ndim or (
|
|
sub_mesh_dim < 0 and -sub_mesh_dim > mesh_ndim
|
|
):
|
|
raise ValueError(
|
|
f"The sub_mesh_dim should between (-{mesh_ndim}, {mesh_ndim}]"
|
|
)
|
|
if sub_mesh_dim < 0:
|
|
sub_mesh_dim += mesh_ndim
|
|
|
|
process_ids = np.array(global_mesh.process_ids).reshape(mesh_shape)
|
|
split_process_ids = np.split(
|
|
process_ids, mesh_shape[sub_mesh_dim], axis=sub_mesh_dim
|
|
)
|
|
sub_mesh_list = []
|
|
for sub_process_ids in split_process_ids:
|
|
sub_mesh_list.append(
|
|
ProcessMesh(sub_process_ids, global_mesh.dim_names)
|
|
)
|
|
|
|
return sub_mesh_list
|
|
|
|
|
|
# Note: This function is intended for internal use within the PaddlePaddle framework for optimizing computational graphs.
|
|
def update_pylayer_output(trivial_value):
|
|
"""
|
|
Update the subblock within a pylayer operation by modifying its output argument.
|
|
|
|
This function optimizes a pylayer operation by removing unnecessary outputs from the 'cf.yield' step.
|
|
|
|
Args:
|
|
trivale_value (pir::Value): The output argument of the pylayer operation to be modified.
|
|
|
|
Example:
|
|
(1) Original pylayer operation:
|
|
(%1, %2) = "pd_op.pylayer" (%0) {
|
|
() = "cf.tuple_pop" [id:1]
|
|
(%3, %4) = "dist_op.xxx" [id:2]
|
|
() = "cf.yield" [id:3] (%3, %4)
|
|
}
|
|
(2) After calling `update_pylayer_output(%4)`, the updated pylayer operation removes the unused output:
|
|
(%1) = "pd_op.pylayer" (%0) {
|
|
() = "cf.tuple_pop" [id:1]
|
|
(%3) = "dist_op.xxx" [id:2]
|
|
() = "cf.yield" [id:3] (%3)
|
|
}
|
|
|
|
Args:
|
|
trivale_value(pir::Value): The output argument of the pylayer op to be updated.
|
|
"""
|
|
define_op = trivial_value.get_defining_op()
|
|
if define_op.get_parent_block().parent_op.name() != "pd_op.pylayer":
|
|
return
|
|
paddle.pir.set_insertion_point(define_op)
|
|
fake_value = paddle.static.data(
|
|
name="_fake_pylayer_out",
|
|
shape=trivial_value.shape,
|
|
dtype=trivial_value.dtype,
|
|
)
|
|
fake_value.set_type(trivial_value.type())
|
|
trivial_value.replace_all_uses_with(fake_value)
|