605 lines
20 KiB
Python
605 lines
20 KiB
Python
# 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 argparse
|
|
import math
|
|
import re
|
|
import shutil
|
|
from collections import OrderedDict
|
|
|
|
import paddle
|
|
|
|
|
|
class ParallelConfig:
|
|
def __init__(self, mp: int, pp: int, vpp: int = 1, sharding: int = 1):
|
|
self.mp = mp
|
|
self.pp = pp
|
|
self.vpp = vpp
|
|
self.sharding = sharding
|
|
|
|
def pipe_parallel_group(self, i: int, j: int):
|
|
ans = []
|
|
for k in range(self.pp):
|
|
ans.append((i, j, k))
|
|
return ans
|
|
|
|
|
|
class LayerReNamingHelper:
|
|
def __init__(self, template: str):
|
|
self._template = template
|
|
self._i = -1
|
|
self._last_old_layer_name = None
|
|
|
|
def get_new_layer_name(self, old_layer_name: str):
|
|
old_layer_name = old_layer_name.split(".")[0]
|
|
if (
|
|
self._last_old_layer_name is None
|
|
or old_layer_name != self._last_old_layer_name
|
|
):
|
|
self._i = self._i + 1
|
|
self._last_old_layer_name = old_layer_name
|
|
return self._template.format(self._i)
|
|
|
|
|
|
class LayerReNamingManager:
|
|
def __init__(self):
|
|
self._renaming_helpers = OrderedDict()
|
|
self._renaming_helpers["linear"] = LayerReNamingHelper("linear_{}")
|
|
self._renaming_helpers["layer_norm"] = LayerReNamingHelper(
|
|
"layer_norm_{}"
|
|
)
|
|
self._renaming_helpers["embedding"] = LayerReNamingHelper(
|
|
"embedding_{}"
|
|
)
|
|
|
|
def get_new_layer_name(self, old_name: str):
|
|
layer_name = ""
|
|
for k, v in self._renaming_helpers.items():
|
|
if old_name.startswith(k):
|
|
layer_name = v.get_new_layer_name(old_name)
|
|
break
|
|
return layer_name
|
|
|
|
def get_new_param_name(self, old_name: str):
|
|
names = old_name.split(".")
|
|
layer_name = self.get_new_layer_name(names[0])
|
|
assert layer_name, f"can not rename layer {names[0]}"
|
|
names[0] = layer_name
|
|
return ".".join(names)
|
|
|
|
|
|
class PipeLineModelAdaptor:
|
|
def __init__(
|
|
self,
|
|
src_parallel_config: ParallelConfig,
|
|
dst_parallel_config: ParallelConfig,
|
|
transformer_layer_num: int,
|
|
segment_method: str = "layer",
|
|
):
|
|
self._src_parallel_config = src_parallel_config
|
|
self._dst_parallel_config = dst_parallel_config
|
|
self._transformer_layer_num = transformer_layer_num
|
|
self._segment_method = segment_method
|
|
|
|
def apply(self, src_model_path: str, dst_model_path: str):
|
|
for i in range(self._src_parallel_config.mp):
|
|
for j in range(self._src_parallel_config.sharding):
|
|
# TODO(liuzhenhai): use multiple process
|
|
layers = []
|
|
|
|
# 1、extract layers in the same pp group
|
|
group = self._src_parallel_config.pipe_parallel_group(i, j)
|
|
src_dirs = [
|
|
"{}/mp_{:0>2d}_sharding_{:0>2d}_pp_{:0>2d}".format(
|
|
src_model_path, *e
|
|
)
|
|
for e in group
|
|
]
|
|
# first rank extract shared layer
|
|
with_shared = True
|
|
for dir in src_dirs:
|
|
print(f"extract layer params in dir {dir}")
|
|
layers.extend(self.extract_layers(dir, with_shared))
|
|
with_shared = False
|
|
# 2、sort and unique layers
|
|
layers = self.sort_layers(layers)
|
|
|
|
# 3、resplit layers among pp group according new pp config
|
|
layer_segments = self.segment_layers(
|
|
layers, self._dst_parallel_config, self._segment_method
|
|
)
|
|
dst_group = self._dst_parallel_config.pipe_parallel_group(i, j)
|
|
dst_dirs = [
|
|
"{}/mp_{:0>2d}_sharding_{:0>2d}_pp_{:0>2d}".format(
|
|
dst_model_path, *e
|
|
)
|
|
for e in dst_group
|
|
]
|
|
|
|
# 4、merge layers belonging to the same node
|
|
for layer_segment, dir_ in zip(layer_segments, dst_dirs):
|
|
print(f"merge {len(layer_segment)} layers to {dir_}")
|
|
self.merge_layers(layer_segment, dir_)
|
|
|
|
# 5、copy meta_state.pdopt
|
|
for src_dir, dst_dir in zip(src_dirs, dst_dirs):
|
|
shutil.copyfile(
|
|
f"{src_dir}/meta_state.pdopt",
|
|
f"{dst_dir}/meta_state.pdopt",
|
|
)
|
|
|
|
def peek_model(self, model_dir: str):
|
|
for i in range(self._src_parallel_config.mp):
|
|
for j in range(self._src_parallel_config.sharding):
|
|
group = self._src_parallel_config.pipe_parallel_group(i, j)
|
|
dirs = [
|
|
"{}/mp_{:0>2d}_sharding_{:0>2d}_pp_{:0>2d}".format(
|
|
model_dir, *e
|
|
)
|
|
for e in group
|
|
]
|
|
for dir in dirs:
|
|
print(f"peek partial model in {dir}:")
|
|
self.peek_partial_model(dir)
|
|
|
|
def peek_partial_model(self, sub_dir: str):
|
|
state_dict = paddle.load(f"{sub_dir}/model.pdparams")
|
|
for k, v in state_dict.items():
|
|
print(f"\t{k} -> {v.name}")
|
|
|
|
def extract_layers(self, dir: str, with_shared: bool):
|
|
opt = paddle.load(dir + "/model_state.pdopt")
|
|
params = paddle.load(dir + "/model.pdparams")
|
|
shared_layer_parsed = False
|
|
# tname -> (layer, param_name)
|
|
tname_to_layer_and_pname = {}
|
|
for k, v in params.items():
|
|
layer = self._extract_layer_name(k)
|
|
assert layer
|
|
# special treatment for embedding layer, skip duplicated shared layer
|
|
# shared layer may exist or not, if it exist it share weight with _layers.0
|
|
# _layers.shared_layers.embed.word_embeddings.weight -> embedding_0.w_0
|
|
# _layers.shared_layers.embed.position_embeddings.weight -> embedding_1.w_0
|
|
# _layers.0.word_embeddings.weight -> embedding_0.w_0
|
|
# _layers.0.position_embeddings.weight -> embedding_1.w_0
|
|
shared_layer_parsed = shared_layer_parsed or (
|
|
"_layers.shared_layers" in layer
|
|
)
|
|
if (
|
|
"_layers.shared_layers" not in layer
|
|
and ("word_embeddings" in k or "position_embeddings" in k)
|
|
and shared_layer_parsed
|
|
):
|
|
continue
|
|
tname_to_layer_and_pname[v.name] = (layer, k)
|
|
|
|
# get opt-> param mapping
|
|
tensor_names = list(tname_to_layer_and_pname.keys())
|
|
opt_names = [
|
|
e for e in opt.keys() if e not in ["master_weights", "LR_Scheduler"]
|
|
]
|
|
opt_to_t = self._opt_name_to_tname(tensor_names, opt_names)
|
|
# gather tensors belonging to one layer together
|
|
layers = OrderedDict()
|
|
for k, v in params.items():
|
|
layer, p = tname_to_layer_and_pname[v.name]
|
|
if layer not in layers:
|
|
layers[layer] = {}
|
|
layers[layer]["opt"] = OrderedDict()
|
|
layers[layer]["params"] = OrderedDict()
|
|
layers[layer]["master_weights"] = OrderedDict()
|
|
layers[layer]["params"][p] = v
|
|
|
|
for k, v in opt.items():
|
|
if k in ["master_weights", "LR_Scheduler"]:
|
|
continue
|
|
layer, _ = tname_to_layer_and_pname[opt_to_t[v.name]]
|
|
layers[layer]["opt"][k] = v
|
|
|
|
if "master_weights" in opt:
|
|
for k, v in opt["master_weights"].items():
|
|
layer, _ = tname_to_layer_and_pname[k]
|
|
layers[layer]["master_weights"][k] = v
|
|
|
|
if "LR_Scheduler" in opt:
|
|
for layer in layers:
|
|
layers[layer]["LR_Scheduler"] = opt["LR_Scheduler"]
|
|
|
|
ans = []
|
|
|
|
for layer_name, layer in layers.items():
|
|
# special treatment for embedding layer
|
|
if (not with_shared) and "shared_layers" in layer_name:
|
|
continue
|
|
file_name = f"./tmp_layer_files/{layer_name}.tmp"
|
|
paddle.save(layer, file_name)
|
|
ans.append((layer_name, file_name))
|
|
print(f"save layer {layer_name} to {file_name}")
|
|
return ans
|
|
|
|
def sort_layers(self, layers: list):
|
|
def priority(elem):
|
|
layer_name = elem[0]
|
|
if "shared_layers" in layer_name:
|
|
return -0.5
|
|
match = re.search(
|
|
r"^_layers((\.\d+)+|(\.shared_layers\.[^\.]+))", layer_name
|
|
)
|
|
assert match, f"{layer_name} not a valid layer name"
|
|
return float(match.group(1).lstrip("."))
|
|
|
|
# strictly sort layers
|
|
print("before sort {}".format("|".join([e[0] for e in layers])))
|
|
layers.sort(key=priority)
|
|
# unique
|
|
unique_layers = []
|
|
for e in layers:
|
|
if unique_layers and e[0] == unique_layers[-1][0]:
|
|
continue
|
|
unique_layers.append(e)
|
|
print("after sort {} ".format("|".join([e[0] for e in unique_layers])))
|
|
return unique_layers
|
|
|
|
def segment_layers(
|
|
self,
|
|
layers: list,
|
|
config: ParallelConfig,
|
|
segment_method: str = "layer",
|
|
):
|
|
layer_num = len(layers)
|
|
stage_num = config.pp * config.vpp
|
|
|
|
# segment by weights
|
|
def segment_by_layer():
|
|
# assume model is of the structure below
|
|
# embedding -> n*(transformer layer) -> [optional output layer]
|
|
# segment index
|
|
weights = [0 for _ in range(layer_num)]
|
|
non_zero_layers = range(1, layer_num - 1)
|
|
# input layer is embedding
|
|
if self._transformer_layer_num:
|
|
assert self._transformer_layer_num < layer_num
|
|
non_zero_layers = range(1, 1 + self._transformer_layer_num)
|
|
for i in non_zero_layers:
|
|
weights[i] = 1
|
|
|
|
part_size = sum(weights) // stage_num
|
|
result = [0 for _ in range(stage_num + 1)]
|
|
memory_counter = 0
|
|
result_idx = 1
|
|
for idx, weight in enumerate(weights):
|
|
memory_counter += weight
|
|
if memory_counter == part_size:
|
|
result[result_idx] = idx + 1
|
|
result_idx += 1
|
|
memory_counter = 0
|
|
result[stage_num] = layer_num
|
|
return result
|
|
|
|
def segment_uniform():
|
|
result = [0 for _ in range(stage_num + 1)]
|
|
part_size = math.floor(layer_num / stage_num)
|
|
extra_layers = layer_num % stage_num
|
|
for i in range(1, stage_num):
|
|
offset = 1 if i > (stage_num - extra_layers) else 0
|
|
result[i] = int(
|
|
min(result[i - 1] + part_size + offset, layer_num)
|
|
)
|
|
result[stage_num] = layer_num
|
|
return result
|
|
|
|
result = (
|
|
segment_uniform()
|
|
if (segment_method == "uniform")
|
|
else segment_by_layer()
|
|
)
|
|
index_segments = [[] for _ in range(config.pp)]
|
|
for i in range(stage_num):
|
|
index_segments[i % config.pp].append((result[i], result[i + 1]))
|
|
|
|
# name layers
|
|
segments = [[] for i in range(config.pp)]
|
|
for i in range(config.pp):
|
|
for start, end in index_segments[i]:
|
|
for j in range(start, end):
|
|
if config.vpp > 1:
|
|
segments[i].append(
|
|
(
|
|
[f"_layers.{start}.{j - start}"],
|
|
layers[j][1],
|
|
)
|
|
)
|
|
else:
|
|
segments[i].append(([f"_layers.{j}"], layers[j][1]))
|
|
|
|
shared_layer_exist = any(
|
|
"_layers.shared_layers" in e[0] for e in layers
|
|
)
|
|
if shared_layer_exist:
|
|
# special treatment for shared layer
|
|
if config.vpp > 1:
|
|
segments[0] = [
|
|
([layers[0][0], segments[0][0][0][0]], layers[0][1]),
|
|
*segments[0][1:],
|
|
]
|
|
else:
|
|
segments[0] = [([layers[0][0]], layers[0][1]), *segments[0][1:]]
|
|
|
|
for i in range(1, config.pp):
|
|
segments[i] = [([layers[0][0]], layers[0][1])] + segments[i]
|
|
|
|
for pp_rank, segs in enumerate(segments):
|
|
print(f"segment result for pp_rank {pp_rank}:")
|
|
print(50 * "=")
|
|
for seg in segs:
|
|
print(f"{seg[0]} => {seg[1]}")
|
|
return segments
|
|
|
|
def merge_layers(self, layers_segment: list, save_dir: str):
|
|
params = OrderedDict()
|
|
opt = OrderedDict()
|
|
master_weights = OrderedDict()
|
|
renaming_manager = LayerReNamingManager()
|
|
|
|
def merge(src, dst, map_k=None):
|
|
for k, v in src.items():
|
|
k = map_k(k) if map_k is not None else k
|
|
dst[k] = v
|
|
|
|
lr_scheduler = None
|
|
for layer_names, file_path in layers_segment:
|
|
print(f"load {file_path}")
|
|
layer = paddle.load(file_path)
|
|
|
|
def get_param_name_mapper(layer_name):
|
|
# replace layer name
|
|
def map_param_name(param_name):
|
|
layer_pre = self._extract_layer_name(param_name)
|
|
return layer_name + param_name[len(layer_pre) :]
|
|
|
|
return map_param_name
|
|
|
|
(
|
|
layer_params,
|
|
layer_opt,
|
|
layer_master_weight,
|
|
) = self._map_tensor_names(
|
|
layer["params"],
|
|
layer["opt"],
|
|
layer["master_weights"],
|
|
renaming_manager,
|
|
)
|
|
for layer_name in layer_names:
|
|
merge(layer_params, params, get_param_name_mapper(layer_name))
|
|
merge(layer_opt, opt)
|
|
merge(layer_master_weight, master_weights)
|
|
lr_scheduler = layer["LR_Scheduler"]
|
|
|
|
opt = self._pack_opt_state_dict(opt, master_weights, lr_scheduler)
|
|
paddle.save(params, save_dir + "/model.pdparams")
|
|
paddle.save(opt, save_dir + "/model_state.pdopt")
|
|
|
|
def _pack_opt_state_dict(self, opt, master_weights, lr_scheduler):
|
|
opt["master_weights"] = master_weights
|
|
opt["LR_Scheduler"] = lr_scheduler
|
|
return opt
|
|
|
|
def _extract_layer_name(self, param_name: str):
|
|
match = re.search(
|
|
r"^_layers((\.\d+)+|(\.shared_layers\.[^\.]+))", param_name
|
|
)
|
|
layer_name = ""
|
|
return "" if (not match) else match.group()
|
|
|
|
# map opt names to tensor name
|
|
def _opt_name_to_tname(self, tensor_names, opt_names):
|
|
tensor_names = set(tensor_names)
|
|
all_names = []
|
|
all_names.extend(list(tensor_names))
|
|
all_names.extend(opt_names)
|
|
all_names.sort()
|
|
pre_t_name = ""
|
|
opt_to_t = {}
|
|
for n in all_names:
|
|
if n in tensor_names:
|
|
# we get a param
|
|
pre_t_name = n
|
|
else:
|
|
assert pre_t_name
|
|
opt_to_t[n] = pre_t_name
|
|
return opt_to_t
|
|
|
|
def _map_tensor_names(self, params, opt, master_weights, renaming_manager):
|
|
opt_renamed = OrderedDict()
|
|
master_weights_renamed = OrderedDict()
|
|
# old name to new name
|
|
t_name_mapping = {}
|
|
# map tensor names
|
|
for k, v in params.items():
|
|
t_name_mapping[v.name] = renaming_manager.get_new_param_name(v.name)
|
|
v.name = t_name_mapping[v.name]
|
|
# map opt names
|
|
opt_to_tname = self._opt_name_to_tname(
|
|
t_name_mapping.keys(), opt.keys()
|
|
)
|
|
for k, v in opt.items():
|
|
old_t_name = opt_to_tname[k]
|
|
t_name = t_name_mapping[old_t_name]
|
|
opt_name = t_name + k[len(old_t_name) :]
|
|
v.name = opt_name
|
|
opt_renamed[opt_name] = v
|
|
|
|
# map master names
|
|
for k, v in master_weights.items():
|
|
t_name = t_name_mapping[k]
|
|
v.name = t_name + v.name[len(k) :]
|
|
master_weights_renamed[t_name] = v
|
|
return (params, opt_renamed, master_weights_renamed)
|
|
|
|
|
|
def parse_args():
|
|
parser = argparse.ArgumentParser(
|
|
prog='model converter', description='converter a model'
|
|
)
|
|
parser.add_argument(
|
|
'--src_path',
|
|
type=str,
|
|
default="./output/epoch_0_step_30",
|
|
help='path of the model to convert',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--dst_path',
|
|
type=str,
|
|
default="./test_adapt",
|
|
help='path to saved the converted model',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--src_mp',
|
|
type=int,
|
|
default=2,
|
|
help='mp degree of the origin training task that dumped this model',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--src_pp',
|
|
type=int,
|
|
default=2,
|
|
help='pp degree of the origin training task that dumped this model',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--src_vp',
|
|
type=int,
|
|
default=2,
|
|
help='vp degree of the origin training task that dumped this model',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--dst_mp',
|
|
type=int,
|
|
default=None,
|
|
help='mp degree of the origin training task that dumped this model',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--dst_pp',
|
|
type=int,
|
|
default=None,
|
|
help='pp degree of the expected training task that would recover this model',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--dst_vp',
|
|
type=int,
|
|
default=2,
|
|
help='vp degree of the expected training task that would recover this model',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--sharding',
|
|
type=int,
|
|
default=1,
|
|
help=" sharding degree of both the origin training task that dumped this model and the expected training task that would recover this model",
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--method',
|
|
type=str,
|
|
default="adapt_model",
|
|
help='vp degree of the expected training task that would recover this model',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--segment_method',
|
|
type=str,
|
|
default="layer",
|
|
help='method to segment layers to pp or vp stages',
|
|
)
|
|
|
|
parser.add_argument(
|
|
'--transformer_layer_num',
|
|
type=int,
|
|
default=0,
|
|
help='transformer_layer_num of the model',
|
|
)
|
|
# assume model is of the structure below
|
|
# embedding -> n*[transformer layer] -> optional output layer
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.dst_mp is None:
|
|
args.dst_mp = args.src_mp
|
|
if args.dst_pp is None:
|
|
args.dst_pp = args.src_pp
|
|
|
|
assert args.src_mp == args.dst_mp, (
|
|
f"src mp {args.src_mp} dst mp {args.dst_mp}"
|
|
)
|
|
|
|
assert args.method in [
|
|
'peek_model',
|
|
'adapt_model',
|
|
], "method should be in ['peek_model', 'adapt_model']"
|
|
assert args.segment_method in [
|
|
"uniform",
|
|
"layer",
|
|
], "segment_method should be 'uniform' or 'layer"
|
|
|
|
print(
|
|
f"adapt model dumped by task with pp degree:{args.src_pp}, vp degree:{args.src_vp}, mp degree:{args.src_mp} to task with pp degree:{args.dst_pp}, vp degree:{args.dst_vp}, mp degree:{args.dst_mp}"
|
|
)
|
|
|
|
return args
|
|
|
|
|
|
def adaptor_from_args(args):
|
|
src_parallel_config = ParallelConfig(
|
|
args.src_mp, args.src_pp, args.src_vp, args.sharding
|
|
)
|
|
|
|
dst_parallel_config = ParallelConfig(
|
|
args.dst_mp, args.dst_pp, args.dst_vp, args.sharding
|
|
)
|
|
|
|
adaptor = PipeLineModelAdaptor(
|
|
src_parallel_config,
|
|
dst_parallel_config,
|
|
args.transformer_layer_num,
|
|
args.segment_method,
|
|
)
|
|
return adaptor
|
|
|
|
|
|
def main():
|
|
args = parse_args()
|
|
adaptor = adaptor_from_args(args)
|
|
if args.method == "peek_model":
|
|
adaptor.peek_model(args.dst_path)
|
|
elif args.method == "adapt_model":
|
|
adaptor.apply(args.src_path, args.dst_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
"""
|
|
Usage:
|
|
python pp_parallel_adaptor.py --src_mp xxx --src_path xxx --method \
|
|
adapt_model/peek_model --dst_path xxx --sharding xxx --segment_method xxx --transformer_layer_num xxx
|
|
|
|
for the meaning of a specific arg, please use:
|
|
python pp_parallel_adaptor.py -h
|
|
"""
|
|
main()
|