Files
wehub-resource-sync 2aaeece67c
Codestyle Check / Lint (push) Has been cancelled
Codestyle Check / Check bypass (push) Has been cancelled
Pipelines-Test / Pipelines-Test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:37:14 +08:00

1667 lines
65 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Copyright (c) 2022 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import inspect
import json
import os
import re
from copy import deepcopy
from dataclasses import dataclass
from functools import partial
from typing import (
TYPE_CHECKING,
Callable,
Dict,
List,
Optional,
Tuple,
Type,
TypeVar,
Union,
)
import numpy as np
import paddle
from numpy import allclose, ndarray, transpose
from paddle import Tensor
from paddle.nn import Layer
from paddlenlp.utils.distributed import distributed_allgather, distributed_gather
from paddlenlp.utils.env import CONFIG_NAME, PYTORCH_WEIGHTS_NAME
from paddlenlp.utils.import_utils import (
is_package_available,
is_torch_available,
is_transformers_available,
)
from paddlenlp.utils.log import logger
from paddlenlp.utils.serialization import load_torch
from paddlenlp.utils.tools import get_env_device
if TYPE_CHECKING:
from paddlenlp.transformers import PretrainedConfig, PretrainedModel
from ..utils import device_guard
# the type hinting for pytorch model & layer & tensor
Module = TypeVar("Module")
PytorchTensor = TypeVar("PytorchTensor")
def add_quant_mapping(name_action_mappings, quantization_config, is_optim=False):
mapping_keys = list(name_action_mappings.keys())
pattern = r"^(?:.*\.)?layers(\.[a-zA-Z0-9_]+)*\.weight$"
for key in mapping_keys:
if re.match(pattern, key):
quant_key = key.replace("weight", "quant_weight")
quant_scale_key = key.replace("weight", "quant_scale")
fn = name_action_mappings.pop(key)
if is_optim:
name_action_mappings[quant_key] = fn
else:
if isinstance(fn, partial):
if "is_column" in fn.keywords:
old_value = fn.keywords["is_column"]
new_value = not old_value
name_action_mappings[quant_key] = partial(
fn.func, *fn.args, **{**fn.keywords, "is_column": new_value}
)
if quantization_config.weight_quantize_algo not in ["fp8linear"] and old_value:
name_action_mappings[quant_scale_key] = partial(
fn.func, *fn.args, **{**fn.keywords, "is_column": new_value}
)
elif "is_quant" in fn.keywords:
old_value = fn.keywords["is_quant"]
new_value = not old_value
name_action_mappings[quant_key] = partial(
fn.func, *fn.args, **{**fn.keywords, "is_quant": new_value}
)
if quantization_config.weight_quantize_algo not in ["fp8linear"]:
name_action_mappings[quant_scale_key] = split_or_merge_func(
is_split=fn.keywords["tensor_parallel_degree"],
tensor_parallel_degree=fn.keywords["tensor_parallel_degree"],
tensor_parallel_rank=fn.keywords["tensor_parallel_rank"],
num_attention_heads=fn.keywords["num_attention_head"],
)
return name_action_mappings
def tensor_summary(tensor: Union[str, Tensor, PytorchTensor, tuple, list, ndarray]):
"""get summary of values which can be some of different values
Args:
tensor (ndarray): the source data of tensor which can be: string, Paddle Tensor, Pytorch Tensor, tuple/list tensor, ndarray
Returns:
str: the summary info
"""
if tensor is None:
return "None"
if isinstance(tensor, str):
return tensor
# Modeling Output from paddlenlp/transformers
if isinstance(tensor, dict):
tensor = list(tensor.values())
if isinstance(tensor, (tuple, list)):
infos = []
for item in tensor:
infos.append(tensor_summary(item))
return "\n".join(infos)
# check whether contains `.numpy` method
# numpy is wrapped from C++, so it will be the `builtin` method
if hasattr(tensor, "numpy") and inspect.isbuiltin(getattr(tensor, "numpy")):
tensor = tensor.detach().cpu().numpy()
tensor = np.reshape(tensor, [-1])
top_3_tensor = str(tensor[1:4])
return top_3_tensor
return str(tensor)
def compare_model_weights(first_state_dict: Dict[str, ndarray], second_state_dict: Dict[str, ndarray]) -> List[str]:
"""compare the values of two state_dict.
This function has an assumption: the keys between `first_state_dict` and `second_state_dict` are exactly the same.
Args:
first_state_dict (Dict[str, ndarray]): first state_dict
second_state_dict (Dict[str, ndarray]): second state_dict
Returns:
mismatched keys (List[str]): the mismatched keys of state_dict because of some reason
"""
mismatched_keys = []
for key in first_state_dict.keys():
is_close = np.allclose(first_state_dict[key], second_state_dict[key], atol=1e-4)
if not is_close:
mismatched_keys.append(key)
return mismatched_keys
def state_dict_contains_prefix(state_dict: Dict[str, ndarray], prefix: str) -> bool:
"""check whether state-dict contains `prefix`"""
prefix_count = sum([1 for key in state_dict.keys() if key.startswith(prefix)])
return prefix_count > 0
def init_name_mappings(mappings: list[StateDictNameMapping]) -> list[StateDictNameMapping]:
"""init name mapping which are simple mappings"""
for index in range(len(mappings)):
sub_mapping = mappings[index]
# if sub_mapping is `str`, so repeat it. eg: [ "word_embedding.weight", ["layer_norm", "LayerNorm"] ]
if isinstance(sub_mapping, str):
sub_mapping = [sub_mapping]
if len(sub_mapping) == 1:
sub_mapping = sub_mapping * 2
elif sub_mapping[1] is None:
sub_mapping[1] = sub_mapping[0]
mappings[index] = sub_mapping
class StateDictKeysChecker:
"""State Dict Keys Checker"""
def __init__(
self,
model_or_state_dict: Union[Layer, Dict[str, ndarray]],
loaded_state_dict: Dict[str, ndarray],
check_shape: bool = True,
base_model_prefix: Optional[str] = None,
ignore_keys: Optional[List[str]] = None,
) -> None:
if isinstance(model_or_state_dict, Layer):
base_model_prefix = base_model_prefix or getattr(model_or_state_dict, "base_model_prefix", None)
model_or_state_dict = {
key: value.detach().cpu().numpy() for key, value in model_or_state_dict.state_dict().items()
}
self.model_state_dict = model_or_state_dict
self.loaded_state_dict = loaded_state_dict
self.check_shape = check_shape
self.ignore_keys = ignore_keys or []
self.base_model_prefix = base_model_prefix
def change_base_downstream_mismatched_keys(self):
"""when model is base-model, loaded state-dict is downstream-model,
it should re-change the downstream state-dict.
eg: init `BertModel` with `BertForTokenClassification` state-dict
# <model-base>-<loaded-downstream>
# remove base-prefix
"""
for key in list(self.loaded_state_dict.keys()):
if key.startswith(self.base_model_prefix):
value = self.loaded_state_dict.pop(key)
new_key = key.replace(f"{self.base_model_prefix}.", "")
self.loaded_state_dict[new_key] = value
def change_downstream_base_mismatched_keys(self):
"""when model is downstream-model, loaded state-dict is base-model,
it should re-change the downstream state-dict.
eg: init `BertModel` with `BertForTokenClassification` state-dict
# <model>-<loaded>: <downstream>-<base>
"""
for key in list(self.model_state_dict.keys()):
if key.startswith(self.base_model_prefix):
key_in_loaded = key.replace(f"{self.base_model_prefix}.", "")
assert key_in_loaded in self.loaded_state_dict
# check loaded keys
value = self.loaded_state_dict.pop(key_in_loaded)
self.loaded_state_dict[key] = value
def change_diff_keys(self) -> List[str]:
"""change the loaded-state-dict by base-model & base_model_prefix
Returns:
List[str]: the diff keys between models and loaded-state-dict
"""
# 1. is absolute same
all_diff_keys, not_in_model_keys, not_in_loaded_keys = self.get_diff_keys(return_all_diff=True)
if len(all_diff_keys) == 0:
return []
if self.base_model_prefix is None:
return all_diff_keys
# 2. <model>-<loaded>: <base>-<downstream>
if not state_dict_contains_prefix(self.model_state_dict, self.base_model_prefix):
# the base-static must be same
if not state_dict_contains_prefix(self.loaded_state_dict, self.base_model_prefix):
error_msg = ["also the base model, but contains the diff keys: \n"]
if not_in_model_keys:
error_msg.append(f"in loaded state-dict, not in model keys: <{not_in_model_keys}>\n")
if not_in_loaded_keys:
error_msg.append(f"in model keys, not in loaded state-dict keys: <{not_in_model_keys}>\n")
logger.error(error_msg)
return []
self.change_base_downstream_mismatched_keys()
elif not state_dict_contains_prefix(self.loaded_state_dict, self.base_model_prefix):
# <model>-<loaded>: <downstream>-<base>
self.change_downstream_base_mismatched_keys()
def get_unexpected_keys(self):
"""get unexpected keys which are not in model"""
self.change_diff_keys()
_, unexpected_keys, _ = self.get_diff_keys(True)
return unexpected_keys
def get_mismatched_keys(self):
"""get mismatched keys which not found in loaded state-dict"""
self.change_diff_keys()
_, _, mismatched_keys = self.get_diff_keys(True)
return mismatched_keys
def get_diff_keys(self, return_all_diff: bool = False) -> List[str]:
"""get diff keys
Args:
return_all_diff (bool, optional): return. Defaults to False.
Returns:
List[str]: the diff keys betweens model and loaded state-dict
"""
mismatched_keys = set(self.model_state_dict.keys()) - set(self.loaded_state_dict.keys())
unexpected_keys = set(self.loaded_state_dict.keys()) - set(self.model_state_dict.keys())
all_diff_keys = mismatched_keys | unexpected_keys
if return_all_diff:
return all_diff_keys, unexpected_keys, mismatched_keys
return all_diff_keys
def naive_fuse_merge_tp(weight_list, is_column=True, fuse_tensor_parts=2):
"""
[A1 B1],[A2 B2] => [A1, A2, B1, B2]
Args:
weight_list (List[np.ndarray]): The splited tensor parallel weight list.
is_column (bool, optional): Is ColumnLinear or RowLinear. Defaults to True.
Returns:
weight (np.ndarray): the merged weight.
"""
if is_column:
axis = -1
else:
axis = 0
reorder = []
if isinstance(weight_list[0], np.ndarray):
for item in weight_list:
reorder.extend(np.split(item, fuse_tensor_parts, axis=axis))
else:
for item in weight_list:
reorder.extend(paddle.split(item, fuse_tensor_parts, axis=axis))
# 0 1 2 3 -> 0 2 1 3
index = (
np.transpose(np.arange(len(reorder)).reshape([len(weight_list), fuse_tensor_parts]), [1, 0])
.reshape(-1)
.tolist()
)
if isinstance(weight_list[0], np.ndarray):
return np.concatenate([reorder[i] for i in index], axis=axis)
else:
tensor = paddle.concat([reorder[i] for i in index], axis=axis)
if tensor.place.is_gpu_place():
tensor = tensor._copy_to(paddle.CUDAPinnedPlace(), False)
return tensor
def naive_fuse_split_tp(
weight, tensor_parallel_degree, tensor_parallel_rank=None, is_column=True, fuse_tensor_parts=2
):
"""
[A1, A2, B1, B2] => [A1 B1],[A2 B2]
Args:
weight (numpy.ndarray): the tensor weight,
tensor_parallel_degree (int): tensor_parallel_degree
tensor_parallel_rank (int): tensor_parallel_rank
is_column (bool, optional): is ColumnLinear . Defaults to True.
Returns:
tensor (numpy.ndarray): splited weight.
"""
axis = -1 if is_column else 0
if "PySafeSlice" in str(type(weight)):
size = weight.get_shape()[axis]
block_size = size // (fuse_tensor_parts * tensor_parallel_degree)
splited = []
if tensor_parallel_rank is None:
begin, end, step = 0, fuse_tensor_parts * tensor_parallel_degree, 1
else:
begin, end, step = tensor_parallel_rank, fuse_tensor_parts * tensor_parallel_degree, tensor_parallel_degree
for rank in range(begin, end, step):
start = rank * block_size
stop = (rank + 1) * block_size
if axis == 0 or len(weight.get_shape()) == 1:
tensor = weight[start:stop]
else:
tensor = weight[:, start:stop]
splited.append(tensor)
if tensor_parallel_rank is None:
ret = []
for tensor_parallel_rank in range(tensor_parallel_degree):
ret.append(np.concatenate(splited[tensor_parallel_rank::tensor_parallel_degree], axis=axis))
return ret
return np.concatenate(splited, axis=axis)
if isinstance(weight, paddle.Tensor):
def slice_concat_by_axis(weight, fuse_tensor_parts, tensor_parallel_degree, tensor_parallel_rank, axis=0):
total_splits = fuse_tensor_parts * tensor_parallel_degree
dim_size = weight.shape[axis]
split_size = dim_size // total_splits
slices = []
for idx in range(tensor_parallel_rank, total_splits, tensor_parallel_degree):
start = idx * split_size
end = (start + split_size) if (idx != total_splits - 1) else dim_size
slice_idx = [slice(None)] * len(weight.shape)
slice_idx[axis] = slice(start, end)
block = weight[tuple(slice_idx)]
slices.append(block)
result = paddle.concat(slices, axis=axis)
return result
if tensor_parallel_rank is not None:
return slice_concat_by_axis(
weight, fuse_tensor_parts, tensor_parallel_degree, tensor_parallel_rank, axis=axis
)
else:
splited = paddle.split(weight, fuse_tensor_parts * tensor_parallel_degree, axis=axis)
ret = []
for tensor_parallel_rank in range(tensor_parallel_degree):
ret.append(paddle.concat(splited[tensor_parallel_rank::tensor_parallel_degree], axis=axis))
return ret
else:
splited = np.split(weight, fuse_tensor_parts * tensor_parallel_degree, axis=axis)
if tensor_parallel_rank is None:
ret = []
for tensor_parallel_rank in range(tensor_parallel_degree):
ret.append(np.concatenate(splited[tensor_parallel_rank::tensor_parallel_degree], axis=axis))
return ret
return np.concatenate(splited[tensor_parallel_rank::tensor_parallel_degree], axis=axis)
def normal_fuse_merge_tp(weight_list, is_column=True):
"""
[A1],[A2] => [A1, A2]
Args:
weight_list (List[np.ndarray]): The splited tensor parallel weight list.
is_column (bool, optional): Is ColumnLinear or RowLinear. Defaults to True.
Returns:
weight (np.ndarray): the merged weight.
"""
if is_column:
if isinstance(weight_list[0], np.ndarray):
return np.concatenate(weight_list, axis=-1)
else:
tensor = paddle.concat(weight_list, axis=-1)
if tensor.place.is_gpu_place():
tensor = tensor._copy_to(paddle.CUDAPinnedPlace(), False)
return tensor
else:
if isinstance(weight_list[0], np.ndarray):
return np.concatenate(weight_list, axis=0)
else:
tensor = paddle.concat(weight_list, axis=0)
if tensor.place.is_gpu_place():
tensor = tensor._copy_to(paddle.CUDAPinnedPlace(), False)
return tensor
def normal_fuse_split_tp(weight, tensor_parallel_degree, tensor_parallel_rank=None, is_column=True):
"""
[A1, A2] => [A1],[A2]
Args:
weight (numpy.ndarray): the tensor weight,
tensor_parallel_degree (int): tensor_parallel_degree
tensor_parallel_rank (int): tensor_parallel_rank
is_column (bool, optional): is ColumnLinear . Defaults to True.
Returns:
tensor (numpy.ndarray): splited weight.
"""
dim = -1 if is_column else 0
if "PySafeSlice" in str(type(weight)):
size = weight.get_shape()[dim]
block_size = size // tensor_parallel_degree
if tensor_parallel_rank is None:
begin, end, step = 0, tensor_parallel_degree, 1
else:
begin, end, step = tensor_parallel_rank, tensor_parallel_rank + 1, 1
splited = []
for rank in range(begin, end, step):
start = rank * block_size
stop = (rank + 1) * block_size
if dim == 0 or len(weight.get_shape()) == 1:
tensor = weight[start:stop]
elif dim == -1:
tensor = weight[:, start:stop]
else:
raise NotImplementedError("Let's make that generic when needed")
if tensor_parallel_rank is not None:
return tensor
splited.append(tensor)
return splited
size = weight.shape[dim]
assert (
size % tensor_parallel_degree == 0
), f"The chosen size {size} is not compatible with sharding on {tensor_parallel_degree} shards. for tensor shape {weight.shape}"
if is_column:
total_size = weight.shape[-1]
chunk_size = total_size // tensor_parallel_degree
if tensor_parallel_rank is not None:
start = tensor_parallel_rank * chunk_size
end = (tensor_parallel_rank + 1) * chunk_size
if isinstance(weight, paddle.Tensor):
splited_weights = weight[..., start:end].clone()
else:
splited_weights = weight[..., start:end]
return splited_weights
else:
splited_weights = [
weight[..., i * chunk_size : (i + 1) * chunk_size] for i in range(tensor_parallel_degree)
]
return splited_weights
else:
total_size = weight.shape[0]
chunk_size = total_size // tensor_parallel_degree
if tensor_parallel_rank is not None:
start = tensor_parallel_rank * chunk_size
end = (tensor_parallel_rank + 1) * chunk_size
if isinstance(weight, paddle.Tensor):
splited_weights = weight[start:end, ...].clone()
else:
splited_weights = weight[start:end, ...]
return splited_weights
else:
splited_weights = [
weight[i * chunk_size : (i + 1) * chunk_size, ...] for i in range(tensor_parallel_degree)
]
return splited_weights
"""
There're three types of MultiHeadAttention QKV Layout in Transformers
tensor_parallel_qkv = [q1, k1, v1, q2, k2, v2]
naive_merged_qkv = [q1, q1, k1, k2, v1, v2]
splited_qkv = [q1, q1], [k1, k2], [v1, v2]
naive_merged_qkv -> tensor_parallel_qkv
: naive_merged_qkv_to_tensor_parallel_qkv
splited_qkv -> tensor_parallel_qkv
: splited_qkv_to_tensor_parallel_qkv
"""
def tensor_parallel_qkv_to_naive_merged_qkv(weight, num_attention_heads):
"""
[q1, k1, v1, q2, k2, v2] => [q1, q1, k1, k2, v1, v2]
"""
qkvs = []
partition_dim = -1
split_heads = np.split(weight, 3 * num_attention_heads, axis=partition_dim)
qkv_weight_num = 3
for i in range(qkv_weight_num):
qkv = np.concatenate(split_heads[i::qkv_weight_num], axis=partition_dim)
qkvs.append(qkv)
return np.concatenate(qkvs, axis=partition_dim)
def naive_merged_qkv_to_tensor_parallel_qkv(weight, num_attention_heads):
"""
[q1, q1, k1, k2, v1, v2] => [q1, k1, v1, q2, k2, v2]
"""
qkv_pairs = []
partition_dim = -1
if isinstance(weight, paddle.Tensor):
split_heads = paddle.split(weight, 3 * num_attention_heads, axis=partition_dim)
for i in range(num_attention_heads):
qkv_pair = paddle.concat(split_heads[i::num_attention_heads], axis=partition_dim)
qkv_pairs.append(qkv_pair)
return paddle.concat(qkv_pairs, axis=partition_dim)
else:
split_heads = np.split(weight, 3 * num_attention_heads, axis=partition_dim)
for i in range(num_attention_heads):
qkv_pair = np.concatenate(split_heads[i::num_attention_heads], axis=partition_dim)
qkv_pairs.append(qkv_pair)
return np.concatenate(qkv_pairs, axis=partition_dim)
def splited_qkv_to_tensor_parallel_qkv(weight_list, num_attention_heads):
"""
[q1, k1, v1], [q2, k2, v2] => [q1, q1, k1, k2, v1, v2]
Args:
weight_list (_type_): [Q,K,V] tensor list
"""
assert len(
weight_list
), f"weight_list length is not equal 3, it should be Q K V list. but got length {len(weight_list)}"
weight = np.concatenate(weight_list, axis=-1)
return naive_merged_qkv_to_tensor_parallel_qkv(weight)
def fuse_param_func():
def fn(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 = np.concatenate
split_fn = np.split
if isinstance(fuse_params[0], paddle.Tensor):
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)
return fn
def split_param_func():
def fn(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 = np.concatenate
split_fn = np.split
if isinstance(fused_param, paddle.Tensor):
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)
return fn
def split_or_fuse_func(is_fuse=True):
return fuse_param_func() if is_fuse else split_param_func()
def get_tensor_parallel_merge_func(tensor_parallel_degree, tensor_parallel_rank, num_attention_heads=None):
def fn(
x,
is_column=True,
transpose=False,
is_old_qkv=False,
is_naive_2fuse=False,
is_naive_3fuse=False,
):
if x is None:
return None
if is_naive_2fuse:
return naive_fuse_merge_tp(x, is_column=is_column, fuse_tensor_parts=2)
elif is_naive_3fuse:
return naive_fuse_merge_tp(x, is_column=is_column, fuse_tensor_parts=3)
else:
x = normal_fuse_merge_tp(x, is_column=is_column)
if is_old_qkv:
assert is_column, "QKV tensor should be column parallel linear."
assert num_attention_heads is not None, "is_old_qkv need num_attention_heads"
x = tensor_parallel_qkv_to_naive_merged_qkv(x, num_attention_heads)
if transpose:
x = np.transpose(x, [1, 0])
return x
return fn
def get_tensor_parallel_split_func(tensor_parallel_degree, tensor_parallel_rank, num_attention_heads=None):
def fn(x, is_column=True, transpose=False, is_old_qkv=False, is_naive_2fuse=False, is_naive_3fuse=False):
if x is None:
return None
if transpose:
if isinstance(x, paddle.Tensor):
x = paddle.transpose(x, [1, 0])
else:
x = np.transpose(x, [1, 0])
if is_old_qkv:
assert is_column, "QKV tensor should be column parallel linear."
assert num_attention_heads is not None, "is_old_qkv need num_attention_heads"
x = naive_merged_qkv_to_tensor_parallel_qkv(x, num_attention_heads)
if is_naive_2fuse:
return naive_fuse_split_tp(
x, tensor_parallel_degree, tensor_parallel_rank, is_column=is_column, fuse_tensor_parts=2
)
if is_naive_3fuse:
return naive_fuse_split_tp(
x, tensor_parallel_degree, tensor_parallel_rank, is_column=is_column, fuse_tensor_parts=3
)
return normal_fuse_split_tp(x, tensor_parallel_degree, tensor_parallel_rank, is_column=is_column)
return fn
def split_or_merge_func(is_split, tensor_parallel_degree, tensor_parallel_rank, num_attention_heads=None):
if is_split:
return get_tensor_parallel_split_func(tensor_parallel_degree, tensor_parallel_rank, num_attention_heads)
return get_tensor_parallel_merge_func(tensor_parallel_degree, tensor_parallel_rank, num_attention_heads)
@dataclass
class StateDictNameMapping:
"""NameMapping of StateDict between two models"""
source_name: str
target_name: str = None
action: Optional[str] = None # the value can be: transpose, merge_last_two_dim
index: Optional[int] = None
slots: list[str] = None
def __post_init__(self):
self.target_name = self.target_name or self.source_name
def should_transpose(self) -> bool:
return self.action == "transpose"
def should_merge_last_two_dim(self) -> bool:
"""check that whether merge last two dim"""
return self.action == "merge_last_two_dim"
def run(self, state_dict: dict[str, ndarray], name: str) -> ndarray:
"""run some custom operation on ndarray, eg: transpose, merge_last_two_dim
Args:
tensor (ndarray): the source of the tensor data
Returns:
ndarray: the final tensor
"""
tensor = state_dict.pop(name)
if callable(self.action):
return self.action(tensor)
if self.action == "transpose":
return transpose(tensor, [1, 0])
if self.action == "merge_last_two_dim":
shape = tensor.shape
assert len(shape) == 3
return np.reshape(tensor, [shape[0], -1])
if self.action == "split":
assert self.index is not None, "when action is `split`, index field is required."
# FIXME if the order of split starts from index=2, no tensor left.
if self.index < 2:
state_dict[name] = tensor
# qkv is stored in same tensor, so it should be split into 3 arr
tensors = np.split(tensor, 3, axis=-1)
return tensors[self.index]
return tensor
def matched(self, text: str) -> bool:
"""check whether the layer_name match the current pattern
Args:
text (str): the name of layer
Returns:
bool: whether the
"""
if text == self.source_name:
return True
if not self.slots:
return False
class TensorInfoSaver:
def __init__(self) -> None:
self.series = {}
def add(self, state_dict_key: str, key: str, values: Union[float, ndarray, Tensor, PytorchTensor]):
"""add
Args:
state_dict_key (str): the state_dict key to compare, eg: embedding.weight
key (str): the field to compare, eg: paddle_input
values (Union[float, ndarray, Tensor]): the tensor
"""
if state_dict_key not in self.series:
self.series[state_dict_key] = {}
if state_dict_key not in self.series[state_dict_key]:
self.series[state_dict_key]["state_dict_key"] = state_dict_key
self.series[state_dict_key][key] = tensor_summary(values)
def summary(self, output_path: Optional[str] = None):
"""output the summary info into different terminal
Args:
output_path (Optional[str], optional): the dir/file of summary file. Defaults to None.
"""
if output_path and os.path.isdir(output_path):
output_path = os.path.join(output_path, "tensor_summary.xlsx")
self.summary_to_excel(output_path)
self.summary_to_terminal()
def summary_to_excel(self, file: str):
if not is_package_available("pandas"):
return False
if not is_package_available("openpyxl"):
logger.warning(
"detect that pandas is installed, but openpyxl is not installed so can't save info into excel file. "
"you can run command: `pip install openpyxl` to get the great feature"
)
return False
import pandas as pd
with pd.ExcelWriter(file, "a", engine="openpyxl", if_sheet_exists="new") as writer:
pd.DataFrame(list(self.series.values())).to_excel(writer, index=False)
def summary_to_terminal(self):
"""print table info into terminal with tabulate"""
from tabulate import tabulate
headers = {key: key for key in self.series.keys()}
print(tabulate(list(self.series.values()), tablefmt="grid", headers=headers))
def clear(self):
"""clear the series data"""
self.series.clear()
class LogitHooker:
"""hooks for pytorch model and paddle model, used to generate the logits of element layers"""
def __init__(self, mappings: List[StateDictNameMapping], tensor_info_saver: Optional[TensorInfoSaver] = None):
"""register the logit hooks to compare the inputs * outputs model
Args:
mappings (List[StateDictNameMapping]): the mappings between paddle & pytorch model
tensor_info_saver (Optional[TensorInfoSaver], optional): the saver for model logit. Defaults to None.
"""
self.mappings = mappings
self.tensor_info_saver = tensor_info_saver or TensorInfoSaver()
def _paddle_hooks(self, layer: Layer, inputs: Tuple[Tensor], outputs: Union[Tensor, Tuple[Tensor]]):
"""internal paddle hooks to save the logit of paddle layer
Args:
layer (Layer): the layer of paddle element
inputs (Tuple[Tensor]): the inputs of paddle layer
outputs (Union[Tensor, Tuple[Tensor]]): the outputs of paddle layer
"""
state_dict_name = layer.__state_dict_name__
self.tensor_info_saver.add(state_dict_name, "paddle-input", inputs)
self.tensor_info_saver.add(state_dict_name, "paddle-outputs", outputs)
def _pytorch_hooks(
self,
layer: Layer,
inputs: Tuple[PytorchTensor],
outputs: Union[Dict[str, PytorchTensor], Tuple[PytorchTensor]],
):
"""internal pytorch hooks to save the logit of pytorch module
Args:
layer (torch.nn.Module): the module of pytorch model
inputs (Tuple[PytorchTensor]): the inputs of pytorch layer
outputs (Union[Dict[str, PytorchTensor], Tuple[PytorchTensor]]): the outputs of pytorch layer
"""
state_dict_name = layer.__state_dict_name__
self.tensor_info_saver.add(
state_dict_name,
"pytorch-input",
inputs,
)
self.tensor_info_saver.add(state_dict_name, "pytorch-outputs", outputs)
def register_paddle_model_hooks(self, model: Layer):
"""register post forward hook to save the inputs & outputs of paddle model
Args:
model (Layer): paddle model
"""
# 1. register paddle model hook to save the logits of target layer
def register_hook_by_name(model: Layer, mapping: StateDictNameMapping, hook: Callable[..., None]):
"""register hook by name of state_dict, eg: encoder.layers.0.linear1.bias
Args:
model (Layer): the source model
mapping (StateDictNameMapping): the name mapping object
hook (Callable[..., None]): the hook for paddle model
"""
name = mapping.target_name
attributes = name.split(".")
last_layer: Layer = model
for attribute in attributes:
if getattr(model, attribute, None) is not None:
model = getattr(model, attribute)
if isinstance(model, Layer):
last_layer = model
if (
hasattr(last_layer, "register_forward_post_hook")
and getattr(last_layer, "__state_dict_name__", None) is None
):
last_layer.register_forward_post_hook(hook)
# set state_dict key into layer as the private attribute
last_layer.__state_dict_name__ = name
for mapping in self.mappings:
register_hook_by_name(model, mapping, self._paddle_hooks)
def register_pytorch_model_hooks(self, model: Module):
"""register hook for pytorch model to save the inputs & outputs of pytorch model
Args:
model (_type_): pytorch model
"""
from torch import nn
# 1. register paddle model hook to save the logits of target layer
def register_hook_by_name(model: Module, mapping: StateDictNameMapping, hook: Callable[..., None]):
name = mapping.source_name
attributes, index = name.split("."), 0
last_layer: Module = model
while index < len(attributes):
attribute = attributes[index]
if getattr(model, attribute, None) is not None:
if isinstance(model, nn.ModuleList) and attribute.isdigit():
model = model[int(attribute)]
last_layer = model
else:
model = getattr(model, attribute)
if isinstance(model, nn.Module):
last_layer = model
index += 1
if (
hasattr(last_layer, "register_forward_hook")
and getattr(last_layer, "__state_dict_name__", None) is None
):
last_layer.register_forward_hook(hook)
# set state_dict key into layer as the private attribute
last_layer.__state_dict_name__ = mapping.target_name
for mapping in self.mappings:
register_hook_by_name(model, mapping, self._pytorch_hooks)
def summary(self):
"""print the summary info to terminal/excel to analysis"""
self.tensor_info_saver.summary()
class LogitComparer:
"""Model Weight Converter for developer to convert pytorch/tensorflow/jax pretrained model weight to paddle.
* you can convert model weight in online/offline mode.
* you can convert weight and config file.
* you can convert weight/config file in some customization ways.
"""
_ignore_state_dict_keys = []
num_layer_regex = r"\.\d+\."
num_layer_key: str = "num_hidden_layers"
# when field-name is same as hf models, so you only need to
# change this attribute to map the configuration
config_fields_to_be_removed: List[str] = ["transformers_version"]
architectures: Dict[str, Type[PretrainedModel]] = {}
def __init__(self, input_dir: str) -> None:
self.input_dir = input_dir
def get_paddle_pytorch_model_classes(self) -> Tuple[object, object]:
"""return the [PaddleModelClass, PytorchModelClass] to
1. generate paddle model automatically
2. compare the logits from pytorch model and paddle model automatically
Returns:
Tuple[object, object]: [PaddleModelClass, PytorchModelClass]
"""
raise NotImplementedError
def get_inputs(self):
"""the numpy inputs for paddle & pytorch model"""
input_ids = paddle.arange(600, 700)
input_ids = paddle.unsqueeze(input_ids, axis=0).detach().cpu().numpy()
return [input_ids]
def resolve_paddle_output_logits(self, paddle_outputs: Tuple[Tensor]):
"""resolve the logit from paddle model which can be `last_hidden_state`"""
output = None
if isinstance(paddle_outputs, (tuple, list)):
output = paddle_outputs[0]
elif paddle.is_tensor(paddle_outputs):
output = paddle_outputs
if output is None:
raise NotImplementedError("can't resolve paddle model outputs")
return output.detach().cpu().reshape([-1]).numpy()
def resolve_pytorch_output_logits(self, pytorch_outputs: Module):
"""resolve the logit from pytorch model which can be `last_hidden_state`"""
output = pytorch_outputs[0]
if output is None:
raise NotImplementedError("can't resolve paddle model outputs")
return output.detach().cpu().reshape([-1]).numpy()
@staticmethod
def get_model_state_dict(model: Union[Layer, Module], copy: bool = False) -> Dict[str, ndarray]:
"""get the state_dict of pytorch/paddle model
Args:
model (Union[Layer, Module]): can be paddle/pytorch model
Returns:
Dict[str, ndarray]: the final state_dict data
"""
from torch import nn
assert isinstance(model, (Layer, nn.Module))
state_dict = {key: value.detach().cpu().numpy() for key, value in model.state_dict().items()}
if copy:
state_dict = deepcopy(state_dict)
return state_dict
def compare_model_state_dicts(
self,
paddle_model: Union[Layer, Dict[str, ndarray]],
pytorch_model: Union[Module, Dict[str, ndarray]],
name_mappings: List[StateDictNameMapping],
):
"""compare the pytorch and paddle model state with name mappings
Args:
paddle_model (Union[Layer, Dict[str, ndarray]]): paddle model instance
pytorch_model (Union[Module, Dict[str, ndarray]]): pytorch model instance
name_mappings (List[StateDictNameMapping]): the name mappings
"""
if not isinstance(paddle_model, dict):
paddle_state_dict = {key: value.detach().cpu().numpy() for key, value in paddle_model.state_dict().items()}
else:
paddle_state_dict = paddle_model
if not isinstance(pytorch_model, dict):
pytorch_state_dict = {
key: value.detach().cpu().numpy() for key, value in pytorch_model.state_dict().items()
}
else:
pytorch_state_dict = pytorch_model
model_state_saver = TensorInfoSaver()
for name_mapping in name_mappings:
model_state_saver.add(name_mapping.target_name, "pytorch_key", name_mapping.source_name)
if name_mapping.target_name in paddle_state_dict:
paddle_numpy = paddle_state_dict.pop(name_mapping.target_name)
model_state_saver.add(name_mapping.target_name, "paddle", paddle_numpy)
model_state_saver.add(name_mapping.target_name, "paddle-shape", str(paddle_numpy.shape))
if name_mapping.source_name in pytorch_state_dict:
pytorch_numpy = pytorch_state_dict.pop(name_mapping.source_name)
model_state_saver.add(name_mapping.target_name, "pytorch", pytorch_numpy)
model_state_saver.add(name_mapping.target_name, "pytorch-shape", str(pytorch_numpy.shape))
model_state_saver.summary()
def compare_logits(self) -> bool:
"""compare the logit of pytorch & paddle model
Returns:
bool: if the logits is absolutely same
"""
PaddleModel, PytorchModel = self.get_paddle_pytorch_model_classes()
paddle_model = PaddleModel.from_pretrained(self.input_dir)
# 0. init the name_mapping & tensor_info_saver & logit_hooker
name_mappings = self.get_name_mapping(paddle_model.config)
tensor_info_saver = TensorInfoSaver()
logit_hooker = LogitHooker(name_mappings, tensor_info_saver)
inputs = self.get_inputs()
# 1. get the logits of paddle model
logit_hooker.register_paddle_model_hooks(paddle_model)
paddle_inputs = [paddle.to_tensor(input_item) for input_item in inputs]
paddle_model.eval()
paddle_outputs = paddle_model(*paddle_inputs)
# remove paddle_model and free gpu memory
paddle_model_state_dict = self.get_model_state_dict(paddle_model)
del paddle_model
paddle_logits = self.resolve_paddle_output_logits(paddle_outputs)
logger.info("===============the summary of paddle Model logits: ===============")
logger.info(tensor_summary(paddle_logits))
# 2. get the logits of pytorch model
import torch
pytorch_model = PytorchModel.from_pretrained(self.input_dir)
logit_hooker.register_pytorch_model_hooks(pytorch_model)
pytorch_model.eval()
pytorch_inputs = [torch.tensor(input_item) for input_item in inputs]
torch_outputs = pytorch_model(*pytorch_inputs)
# remove paddle_model and free gpu memory
pytorch_model_state_dict = self.get_model_state_dict(pytorch_model)
del pytorch_model
pytorch_logits = self.resolve_pytorch_output_logits(torch_outputs)
logger.info("===============the summary of pytorch Model logits: ===============")
logger.info(tensor_summary(pytorch_logits))
# 3. compare the logits
result = allclose(paddle_logits[1:4], pytorch_logits[1:4], atol=1e-4)
if not result:
print("============================== compare model state dict ==============================")
self.compare_model_state_dicts(paddle_model_state_dict, pytorch_model_state_dict, name_mappings)
print("============================== compare model inputs & outputs ==============================")
logit_hooker.summary()
return result
def on_converted(self):
PaddleModelClass, PytorchModelClass = self.get_paddle_pytorch_model_classes()
# 1. try to compare two loaded paddle weight file
first_paddle_model = PaddleModelClass.from_pretrained(self.input_dir)
second_paddle_model = PaddleModelClass.from_pretrained(self.input_dir)
mismatched_keys = compare_model_weights(
self.get_model_state_dict(first_paddle_model),
self.get_model_state_dict(second_paddle_model),
)
for key in mismatched_keys:
logger.error(f"the key<{key}> is not set correctly with weight")
# 2. try to compare logits between paddle & pytorch model
if is_torch_available() and is_transformers_available():
result = self.compare_logits()
if result is True:
logger.info("the logits between pytorch model and paddle model is absolutely same")
else:
logger.error(
"the logits between pytorch model and paddle model is not same, please check it out more carefully."
)
else:
logger.warning(
"you don't install `torch` and `transformers` package, so we can't compare the logits between paddle & pytorch model"
)
class ConversionMixin:
@classmethod
def support_conversion(cls, config: PretrainedConfig) -> bool:
"""check whether the model support conversion"""
try:
# try to get the name-mapping info
_ = cls._get_name_mappings(config)
except NotImplementedError:
return False
finally:
return True
@classmethod
def convert(cls, weight_file: str, config: PretrainedConfig, cache_dir: str) -> None:
"""the entry of converting config and converting model file
Args:
input_dir (str | None): the input dir which contains `pytorch_model.bin` and `config.json` file
config (PretrainedConfig): the PretrainedConfig instance of model
"""
# FIXME(wj-Mcat): add compatibility with downstream models
name_mappings = cls._get_name_mappings(config)
if weight_file.endswith(".index.json"):
if ".safetensors." in weight_file:
files = [file for file in os.listdir(os.path.dirname(weight_file)) if file.startswith("model-")]
else:
files = [
file for file in os.listdir(os.path.dirname(weight_file)) if file.startswith("pytorch_model-")
]
state_dict = {}
for file in sorted(files):
sub_state_dict = load_torch(os.path.join(os.path.dirname(weight_file), file))
state_dict.update(sub_state_dict)
else:
state_dict = load_torch(weight_file)
# 3. convert state_dict
all_layer_names = set(state_dict.keys())
for name_mapping in name_mappings:
if name_mapping.source_name not in state_dict:
logger.warning(f"key<{name_mapping.source_name}> not in the pytorch weight file.")
continue
state_dict[name_mapping.target_name] = name_mapping.run(state_dict, name_mapping.source_name)
if name_mapping.source_name in all_layer_names:
all_layer_names.remove(name_mapping.source_name)
if all_layer_names:
logger.warning(f"There are {len(all_layer_names)} tensors not initialized:")
logger.warning(f"Keys: {all_layer_names}")
return state_dict
@classmethod
def _get_name_mappings(cls, config: PretrainedConfig) -> List[StateDictNameMapping]:
"""get name mapping of PretrainedModel
Args:
config (PretrainedConfig): the configuration of name-mapping
Raises:
NotImplementedError:
Returns:
List[StateDictNameMapping]: the name-mappings of pretrained model
"""
raise NotImplementedError
@classmethod
def get_tensor_parallel_convert_actions(
cls,
config: PretrainedConfig,
loaded_state_dict_keys,
is_split=True,
ignore_error=False,
base_model_prefix=None,
post_quantize=False,
is_optim=False,
):
name_action_mappings = cls._get_tensor_parallel_mappings(config, is_split=is_split)
if config.quantization_config.is_weight_quantize() and not post_quantize:
name_action_mappings = add_quant_mapping(name_action_mappings, config.quantization_config, is_optim)
state_keys_map = cls._resolve_prefix_keys(
name_action_mappings.keys(), loaded_state_dict_keys, ignore_error, base_model_prefix=base_model_prefix
)
for k, v in state_keys_map.items():
if k not in name_action_mappings:
continue
name_action_mappings[v] = name_action_mappings.pop(k)
return name_action_mappings
@classmethod
def convert_tensor_parallel(
cls, weight_file: str, config: PretrainedConfig, state_dict=None, ignore_error=False
) -> None:
"""the entry of converting config and converting model file
Args:
weight_file (str | None): the weight file path of `model_state.pdparams` file
config (PretrainedConfig): the PretrainedConfig instance of model
"""
name_action_mappings = cls._get_tensor_parallel_mappings(config)
if config.quantization_config.is_weight_quantize():
name_action_mappings = add_quant_mapping(name_action_mappings, config.quantization_config)
if state_dict is None:
with device_guard("cpu"):
state_dict = paddle.load(weight_file, return_numpy=False)
logger.info("Starting to convert original state_dict to tensor parallel state_dict.")
state_keys_map = cls._resolve_prefix_keys(name_action_mappings.keys(), state_dict.keys(), ignore_error)
for k, v in state_keys_map.items():
name_action_mappings[v] = name_action_mappings.pop(k)
for name, action in name_action_mappings.items():
if name not in state_dict:
if not ignore_error:
logger.warning(f"Key <{name}> not in the model state weight file.")
continue
tensor = state_dict.pop(name)
new_tensor = action(tensor)
with device_guard("cpu"):
state_dict[name] = paddle.Tensor(new_tensor, zero_copy=True)
return state_dict
@classmethod
def merge_tensor_parallel(cls, state_dict, config) -> None:
"""the entry of converting config and converting model file
Args:
input_dir (str | None): the input dir which contains `pytorch_model.bin` and `config.json` file
config (PretrainedConfig): the PretrainedConfig instance of model
"""
name_action_mappings = cls._get_tensor_parallel_mappings(config, is_split=False)
if config.quantization_config.is_weight_quantize():
name_action_mappings = add_quant_mapping(name_action_mappings, config.quantization_config)
state_keys_map = cls._resolve_prefix_keys(name_action_mappings.keys(), state_dict.keys())
for k, v in state_keys_map.items():
name_action_mappings[v] = name_action_mappings.pop(k)
state_dict_to_save = {}
hcg = paddle.distributed.fleet.get_hybrid_communicate_group()
mp_group = hcg.get_model_parallel_group()
is_dst = paddle.distributed.get_rank(mp_group) == 0
for key in state_dict.keys():
tensor = state_dict[key]
if key in name_action_mappings:
if get_env_device() == "xpu":
ret = distributed_allgather(tensor, group=mp_group, offload=True)
else:
ret = distributed_gather(tensor, group=mp_group, offload=True)
action = name_action_mappings.pop(key)
tensor = action(ret) if is_dst else None
else:
tensor = tensor.cpu().numpy() if is_dst else None
# keep state dict use paddle.tensor
if isinstance(tensor, np.ndarray):
with device_guard("cpu"):
tensor = paddle.Tensor(tensor, zero_copy=True)
state_dict_to_save[key] = tensor
if len(name_action_mappings) > 0:
for x in name_action_mappings.keys():
logger.debug(f"key <{x}> need to merge tensor parallel but we can't find in model state.")
return state_dict_to_save
@classmethod
def _get_tensor_parallel_mappings(cls, config: PretrainedConfig, is_split=True) -> List[StateDictNameMapping]:
"""get name mapping of PretrainedModel
Args:
config (PretrainedConfig): the configuration of name-mapping
Raises:
NotImplementedError:
Returns:
List[StateDictNameMapping]: the name-mappings for tensor_parallel
"""
raise NotImplementedError
@staticmethod
def _resolve_prefix_keys(state_keys_base, state_keys_real, ignore_error=False, base_model_prefix=None):
# state_keys_map base to real
state_keys_map = {}
if base_model_prefix:
for k in state_keys_real:
if k.startswith("lm_head."):
continue
# remove real key name `base_model_prefix` + '.'
state_keys_map[k[len(base_model_prefix + ".") :]] = k
return state_keys_map
# sorted by lengthmatch from long to short for A.key B.key ...
state_keys_base = sorted(state_keys_base, key=lambda x: len(x), reverse=True)
state_keys_real = set(state_keys_real)
for key in state_keys_base:
for x in state_keys_real:
if x.endswith(key):
state_keys_map[key] = x
break
if key not in state_keys_map:
if not ignore_error:
logger.debug(f"tensor parallel conversion: could not find name {key} in loaded state dict!")
else:
state_keys_real.remove(state_keys_map[key])
return state_keys_map
@classmethod
def convert_fuse_and_split(cls, config: PretrainedConfig, state_dict, tp_actions=None):
loaded_keys = state_dict.keys()
# collect and convert fuse/split action
fused_and_split_keys = []
convert_with_same_keys = []
fuse_actions, resume_keys = cls.get_fuse_or_split_param_convert_actions(config, loaded_keys, is_fuse=True)
for keys, action in fuse_actions.items():
if keys[-1] in keys[:-1]:
assert len(keys) == 2, "only 2 keys can be converted with the same name"
convert_with_same_keys.append(keys[-1])
origin_states = [state_dict.pop(key) for key in keys[:-1]]
state_dict[keys[-1]] = action(origin_states)
fused_and_split_keys.append(keys[-1])
logger.debug(f"Fusing parameter: {keys[:-1]} into {keys[-1]}")
split_actions, _ = cls.get_fuse_or_split_param_convert_actions(config, loaded_keys, is_fuse=False)
for keys, action in split_actions.items():
if keys[-1] in keys[:-1]:
assert len(keys) == 2, "only 2 keys can be converted with the same name"
convert_with_same_keys.append(keys[-1])
origin_state = state_dict.pop(keys[-1])
split_states = action(origin_state)
for key_idx, key in enumerate(keys[:-1]):
state_dict[key] = split_states[key_idx]
fused_and_split_keys.append(key)
logger.debug(f"Splitting parameter: {keys[-1]} into {keys[:-1]}")
if tp_actions is not None:
for key in fused_and_split_keys:
if key in convert_with_same_keys:
continue
for name in tp_actions.keys():
if key.endswith(name):
with device_guard():
state_dict[key] = paddle.Tensor(tp_actions[name](state_dict.pop(key)), zero_copy=True)
break
# when shard file split the weight as follows, some weights need to be resumed for next shard file
# shard-001-file: q_weight, k_weight
# shard_002-file: v_weight
resume_state_dict = {k: state_dict[k] for k in resume_keys if k in state_dict}
return state_dict, resume_state_dict
@classmethod
def get_fuse_or_split_param_convert_actions(
cls,
config: PretrainedConfig,
loaded_state_dict_keys,
is_fuse=True,
ignore_error=False,
):
name_action_mappings = cls._get_fuse_or_split_param_mappings(config, is_fuse)
state_keys_map = cls._resolve_prefix_keys_for_fuse_and_split(
name_action_mappings.keys(), loaded_state_dict_keys, ignore_error, is_fuse
)
for k, v in state_keys_map.items():
name_action_mappings[v] = name_action_mappings.pop(k)
# filter name_action_mappings with corresponding weights
# fusing: verify all of the keys in name_action_mappings are in loaded_state_dict_keys
# splitting: verify the last key in name_action_mappings is in loaded_state_dict_keys
filter_name_action = {}
resume_keys = []
if is_fuse:
for k, v in name_action_mappings.items():
cond = True
if not all(item in loaded_state_dict_keys for item in k[:-1]):
# resume keys for next fuse
resume_keys += k[:-1]
cond = False
if cond:
filter_name_action[k] = v
else:
for k, v in name_action_mappings.items():
if k[-1] in loaded_state_dict_keys:
filter_name_action[k] = v
return filter_name_action, resume_keys
@classmethod
def _get_fuse_or_split_param_mappings(cls, config: PretrainedConfig, is_fuse=True) -> List[StateDictNameMapping]:
"""get fused parameter mapping of PretrainedModel
Args:
config (PretrainedConfig): the configuration of name-mapping
Raises:
NotImplementedError:
Returns:
List[StateDictNameMapping]: the name-mappings for tensor_parallel
"""
# raise NotImplementedError(
# f"`_get_fuse_or_split_param_mappings` is not implemented for {cls.__name__}`. To implement it, you should "
# f"overwrite this method in the class {cls.__name__} in `{cls.__module__}.py`"
# )
return {}
@staticmethod
def _resolve_prefix_keys_for_fuse_and_split(state_keys_base, state_keys_real, ignore_error=False, is_fuse=True):
state_keys_map = {}
# use the tuple (x1,x2,x3,x4) as one key, and the prefix of x1,x2,x3 is used as a new key x4 or
# the last key x4 is used as new keys x1,x2,x3. And, the tuple also could be (a) (x1, x1) -> convert x1 to x1;
# (b) (x1,x2,x3) -> fuse x1 and x2 to x3; (c) (x1,x2,x3,x4) -> fuse x1, x2 and x3 to x4.
# is_fuse: True -> fuse, False -> split
# True: (x1,x2,x3,x4) -> [x1,x2,x3] are exist in state_keys_real, x4 is not exist in state_keys_real
# False: (x1,x2,x3,x4) -> [x1,x2,x3] are not exist in state_keys_real, x4 is exist in state_keys_real
for keys in state_keys_base:
prefix = ""
if is_fuse:
for x in state_keys_real:
for base_key in keys[:-1]:
if x.endswith(base_key):
prefix = x.replace(base_key, "")
break
if prefix != "":
break
else:
base_key = keys[-1]
for x in state_keys_real:
if x.endswith(base_key):
prefix = x.replace(base_key, "")
break
new_keys = tuple([prefix + key for key in keys])
state_keys_map[keys] = new_keys
return state_keys_map
class Converter(ConversionMixin, LogitComparer):
"""some converters are implemented in ppdiffusers, so if remove it directly, it will make ppdiffusers down.
TODO(wj-Mcat): this class will be removed after v2.6
"""
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
logger.warning(
"`paddlenlp.utils.converter` module will be deprecated soon, you "
"should change it to `paddlenlp.transformers.conversion_utils`"
)
@classmethod
def resolve_num_layer(cls, config_or_num_layers: Union[dict, int] = None) -> int:
"""resolve the number of transformer layer based on the key of model config, eg: `num_hidden_layers` in BertModel
Args:
config_or_num_layers (Union[dict, int], optional): the instance of config or num_layers. Defaults to None.
Raises:
ValueError: when `config_or_num_layers` is not dict/int, it will raise the error
Returns:
int: the number of transformer layer
"""
from paddlenlp.transformers.configuration_utils import PretrainedConfig
if isinstance(config_or_num_layers, (dict, PretrainedConfig)):
num_layer = config_or_num_layers[cls.num_layer_key]
elif isinstance(config_or_num_layers, int):
num_layer = config_or_num_layers
else:
raise ValueError(f"the type of config_or_num_layers<{config_or_num_layers}> should be one of <dict, int>")
return num_layer
def convert(self, input_dir: str | None = None) -> None:
"""the entry of converting config and converting model file
Args:
input_dir (str | None): the input dir which contains `pytorch_model.bin` and `config.json` file
"""
input_dir = input_dir or getattr(self, "input_dir", None)
os.makedirs(input_dir, exist_ok=True)
# 1. get pytorch weight file
weight_file = os.path.join(input_dir, PYTORCH_WEIGHTS_NAME)
if not os.path.exists(weight_file):
raise FileNotFoundError(f"pytorch weight file<{weight_file}> not found")
config_file = os.path.join(input_dir, CONFIG_NAME)
if not os.path.exists(config_file):
raise FileNotFoundError(f"config file<{weight_file}> not found")
# 2. construct name mapping
# TODO(wj-Mcat): when AutoConfig is ready, construct config from AutoConfig.
with open(config_file, "r", encoding="utf-8") as f:
config = json.load(f)
state_dict = load_torch(weight_file)
# FIXME(wj-Mcat): add compatibility with downstream models
name_mappings = self.get_name_mapping(config)
# 3. convert state_dict
all_layer_names = set(state_dict.keys())
for name_mapping in name_mappings:
if name_mapping.source_name not in state_dict:
logger.warning(f"key<{name_mapping.source_name}> not in the pytorch weight file.")
continue
state_dict[name_mapping.target_name] = name_mapping.run(state_dict.pop(name_mapping.source_name))
all_layer_names.remove(name_mapping.source_name)
if all_layer_names:
logger.warning(f"There are {len(all_layer_names)} tensors not initialized:")
logger.warning(f"Keys: {all_layer_names}")
return state_dict