Files
2026-07-13 13:18:33 +08:00

428 lines
18 KiB
Python
Executable File

# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
import torch
import os
import copy
import collections
import json
from abc import ABC, abstractmethod
from deepspeed.utils import logger
from deepspeed.runtime.checkpoint_engine.torch_checkpoint_engine import TorchCheckpointEngine
from .weight_quantizer import WeightQuantization
AUTO_MODULE_KEY = 'auto'
class SDLoaderFactory:
@staticmethod
def get_sd_loader_json(json_file, checkpoint_engine):
if isinstance(json_file, str):
with open(json_file) as f:
data = json.load(f)
else:
assert isinstance(json_file, dict)
data = json_file
sd_type = data['type']
ckpt_list = data['checkpoints']
version = data['version']
ckpt_type = data.get('parallelization', 'pp')
mp_size = data.get('mp_size', 0)
if sd_type.lower() in ['bloom', 'ds_model']:
return data
return SDLoaderFactory.get_sd_loader(ckpt_list, checkpoint_engine, sd_type, version)
@staticmethod
def get_sd_loader(ckpt_list, checkpoint_engine, sd_type='Megatron', version=None):
if sd_type == 'Megatron':
return MegatronSDLoader(ckpt_list, version, checkpoint_engine)
else:
assert False, '{} checkpoint type is not supported'.format(sd_type)
class SDLoaderBase(ABC):
def __init__(self, ckpt_list, version, checkpoint_engine):
self.module_key = None
self.ckpt_list = ckpt_list
self.version = version
self.checkpoint_engine = TorchCheckpointEngine() if checkpoint_engine is None else checkpoint_engine
self.check_ckpt_list()
def load(self,
mp_world_size,
mp_rank,
module_key=AUTO_MODULE_KEY,
is_pipe_parallel=False,
quantize=False,
quantize_bits=8,
quantize_groups=64,
mlp_extra_grouping=True):
self.module_key = module_key
num_ckpt = len(self.ckpt_list)
idx = mp_rank * num_ckpt // mp_world_size
""" We have multiple cases to handle here for both training and inference:
1. PipeModule loading mp_rank_*.pt files, is_pipe_parallel=True, module_key is not None
a. if no mp_size/pp_size resizing occurs, for both training & inference, loading
the mp_rank related checkpoint directly.
b. if has mp_size/pp_size resizing, only Megatron model inference is supported,
in this case each mp_rank_*.pt have same content, we will load the first checkpoint
file (idx=0), to avoid idx exceeding file list boundary.
2. PipeModule loading layer_*.pt files, is_pipe_parallel=True, module_key is None
a. if no mp_size resizing occurs, for both training & inference, loading
the mp_rank related checkpoint directly.
b. if has mp_size resizing, only Megatron model inference is supported,
checkpoint file(s) will be merged/split according to mp_rank, mp_world_size and
checkpoint file list.
3. Non-PipeModule loading mp_rank_*.pt files, is_pipe_parallel=False
Same with case (2).
"""
if is_pipe_parallel and module_key is not None and mp_world_size != num_ckpt:
mp_world_size = num_ckpt
idx = 0
load_path = self.ckpt_list[idx]
merge_count = 1
if num_ckpt == mp_world_size:
assert os.path.exists(load_path)
#logger.info(f'rank: {mp_rank} loading checkpoint: {load_path}')
sd = self.checkpoint_engine.load(load_path, map_location=lambda storage, \
loc: storage)
if quantize:
quantizer = WeightQuantization(mlp_extra_grouping=mlp_extra_grouping, mp_size=mp_world_size)
sd_module, all_scales = quantizer.sd_quantize_megatron(self.get_module(sd), quantize_bits,
quantize_groups)
self.set_module(sd, sd_module)
else:
all_scales = None
elif num_ckpt > mp_world_size:
sd, all_scales, merge_count = self.merge_state_dict(mp_world_size, mp_rank, quantize, \
quantize_bits, quantize_groups, mlp_extra_grouping)
else:
sd, all_scales = self.split_state_dict(mp_world_size, mp_rank, quantize, quantize_bits, \
quantize_groups, mlp_extra_grouping)
return load_path, sd, (all_scales, merge_count)
def get_merge_state_dicts(self, mp_world_size, mp_rank):
num_ckpt = len(self.ckpt_list)
assert num_ckpt % mp_world_size == 0, 'Invalid checkpoints and world size for sd merge'
num_to_merge = num_ckpt // mp_world_size
ckpt_list = [self.ckpt_list[i] for i in range(num_to_merge * mp_rank, num_to_merge * (mp_rank + 1))]
logger.info(f"mp_rank: {mp_rank}, ckpt_list: {ckpt_list}")
sd_list = [self.checkpoint_engine.load(ckpt, map_location=lambda storage, loc: storage) for ckpt in ckpt_list]
return sd_list
def get_split_state_dict(self, mp_world_size, mp_rank):
num_ckpt = len(self.ckpt_list)
assert mp_world_size % num_ckpt == 0, 'Invalid checkpoints and world size for sd split'
num_to_split = mp_world_size // num_ckpt
ckpt_index = mp_rank // num_to_split
ckpt_offset = mp_rank % num_to_split
logger.info(f"mp_rank: {mp_rank}, ckpt_list: {self.ckpt_list[ckpt_index]}, offset: {ckpt_offset}")
sd = self.checkpoint_engine.load(self.ckpt_list[ckpt_index], map_location=lambda storage, loc: storage)
return sd, num_to_split, ckpt_offset
def _choose_module_key(self, sd):
assert not ('module' in sd
and 'model' in sd), "checkpoint has both 'model' and 'module' keys, not sure how to proceed"
assert 'module' in sd or 'model' in sd, "checkpoint contains neither 'model' or 'module' keys, not sure how to proceed"
if 'module' in sd:
return 'module'
elif 'model' in sd:
return 'model'
def get_module(self, sd):
if self.module_key is None:
return sd
elif self.module_key == AUTO_MODULE_KEY:
return sd[self._choose_module_key(sd)]
else:
return sd[self.module_key]
def set_module(self, sd, module):
if self.module_key is None:
sd = module
elif self.module_key == AUTO_MODULE_KEY:
sd[self._choose_module_key(sd)] = module
else:
sd[self.module_key] = module
return sd
def check_ckpt_list(self):
#logger.info(f'checkpoint file list: {self.ckpt_list}')
assert len(self.ckpt_list) > 0
sd = self.checkpoint_engine.load(self.ckpt_list[0], map_location=lambda storage, loc: storage)
# check checkpoint count is same with saved mp_world_size
if 'mp_world_size' in sd.keys():
assert len(self.ckpt_list) == sd[
'mp_world_size'], f"checkpoint count {len(self.ckpt_list)} is different from saved mp_world_size {sd['mp_world_size']}"
@abstractmethod
def merge_state_dict(self, mp_world_size, mp_rank, quantize, quantize_bits, groups, mlp_extra_grouping):
pass
@abstractmethod
def split_state_dict(self, mp_world_size, mp_rank, quantize, quantize_bits, groups, mlp_extra_grouping):
pass
@abstractmethod
def sanity_check(self, ckpt_file_name):
pass
class MegatronSDLoader(SDLoaderBase):
def __init__(self, ckpt_list, version, checkpoint_engine):
super().__init__(ckpt_list, version, checkpoint_engine)
"""
## Q/K/V data need special processing
key: transformer.layers.0.attention.query_key_value.weight, shape: torch.Size([3192, 4256])
key: transformer.layers.0.attention.query_key_value.bias, shape: torch.Size([3192])
## merge or split on axis=0
key: word_embeddings.weight, shape: torch.Size([12672, 4256])
key: transformer.layers.0.mlp.dense_h_to_4h.bias, shape: torch.Size([4256])
key: transformer.layers.0.mlp.dense_h_to_4h.weight, shape: torch.Size([4256, 4256])
## merge or split on axis=1
key: transformer.layers.0.attention.dense.weight, shape: torch.Size([4256, 1064])
key: transformer.layers.0.mlp.dense_4h_to_h.weight, shape: torch.Size([4256, 4256])
## no change required
key: transformer.layers.0.mlp.dense_4h_to_h.bias, shape: torch.Size([4256])
key: transformer.final_layernorm.weight, shape: torch.Size([4256])
key: transformer.final_layernorm.bias, shape: torch.Size([4256])
key: transformer.layers.0.attention.dense.bias, shape: torch.Size([4256])
key: transformer.layers.0.post_attention_layernorm.weight, shape: torch.Size([4256])
key: transformer.layers.0.post_attention_layernorm.bias, shape: torch.Size([4256])
key: transformer.layers.0.input_layernorm.weight, shape: torch.Size([4256])
key: transformer.layers.0.input_layernorm.bias, shape: torch.Size([4256])
key: position_embeddings.weight, shape: torch.Size([1024, 4256])
"""
def merge_query_key_value(self, param_list, ckpt_ver):
"""
Up to now we found 3 Q/K/V parameter formats in different Megatron checkpoint versions:
1. version 0, there is no version information saved in checkpoint.
format: [(3 * np * hn), h]
2. version 1.0
format: [(np * hn * 3), h]
3. version 2.0
format: [(np * 3 * hn), h]
h: hidden size
n: number of attention heads
p: number of model parallel partitions
np: n/p
hn: h/n
"""
new_qkv = None
if ckpt_ver == 0:
# [(3 * np * hn), h]
assert param_list[0].shape[0] % 3 == 0
size_qkv = param_list[0].shape[0] // 3
split_tensors = [torch.split(param, size_qkv, dim=0) for param in param_list]
tensors = []
for i in range(3):
tensor_tuple = [t[i] for t in split_tensors]
tensors.append(torch.cat(tensor_tuple, axis=0))
new_qkv = torch.cat(tensors, axis=0)
elif ckpt_ver == 1.0 or ckpt_ver == 2.0:
# [(np * hn * 3), h] or [(np * 3 * hn), h]
new_qkv = torch.cat(param_list, axis=0)
else:
assert False, f'checkpoint version: {ckpt_ver} is not supported'
return new_qkv
def split_query_key_value(self, param, num_to_split, offset, ckpt_ver):
"""
Up to now we found 3 Q/K/V parameter formats in different Megatron checkpoint versions:
1. version 0, there is no version information saved in checkpoint.
format: [(3 * np * hn), h]
2. version 1.0
format: [(np * hn * 3), h]
3. version 2.0
format: [(np * 3 * hn), h]
h: hidden size
n: number of attention heads
p: number of model parallel partitions
np: n/p
hn: h/n
"""
new_qkv = None
if ckpt_ver == 0:
# [(3 * np * hn), h]
assert param.shape[0] % 3 == 0
size_qkv = param.shape[0] // 3
split_tensors = torch.split(param, size_qkv, dim=0)
assert split_tensors[0].shape[0] % num_to_split == 0
split_size = split_tensors[0].shape[0] // num_to_split
tensors = []
for i in range(3):
tensors.append(torch.split(split_tensors[i], split_size, dim=0)[offset])
new_qkv = torch.cat(tensors, axis=0)
elif ckpt_ver == 1.0 or ckpt_ver == 2.0:
# [(np * hn * 3), h] or [(np * 3 * hn), h]
assert param.shape[0] % num_to_split == 0
size_qkv = param.shape[0] // num_to_split
split_tensors = torch.split(param, size_qkv, dim=0)
new_qkv = split_tensors[offset]
else:
assert False, f'checkpoint version: {ckpt_ver} is not supported'
return new_qkv
def merge_state_dict(self,
mp_world_size,
mp_rank,
quantize=False,
quantize_bits=8,
groups=64,
mlp_extra_grouping=True):
self.sanity_check(self.ckpt_list[0])
sd_list = self.get_merge_state_dicts(mp_world_size, mp_rank)
ds_sd = copy.deepcopy(sd_list[0])
new_client_sd = collections.OrderedDict()
client_sd_list = [self.get_module(sd) for sd in sd_list]
keys = client_sd_list[0].keys()
ckpt_ver = self.get_checkpoint_version(ds_sd)
logger.info(f"checkpoint version: {ckpt_ver}")
if quantize:
quantizer = WeightQuantization(mlp_extra_grouping=mlp_extra_grouping, mp_size=mp_world_size)
for key in keys:
value_list = [sd[key] for sd in client_sd_list]
if "attention.dense.weight" in key or "mlp.dense_4h_to_h.weight" in key:
if quantize:
value_list = quantizer.Quantize(value_list, quantize_bits, groups, key=key, merge_dim=1)
new_client_sd[key] = torch.cat(value_list, axis=1)
elif "attention.query_key_value" in key:
if quantize and "attention.query_key_value.weight" in key:
value_list = quantizer.Quantize(value_list, quantize_bits, groups, key=key)
new_client_sd[key] = torch.cat(value_list, axis=0)
else:
if quantize:
new_client_sd[key] = torch.cat(value_list, axis=0)
else:
new_client_sd[key] = self.merge_query_key_value(value_list, ckpt_ver)
elif "mlp.dense_h_to_4h.weight" in key or "word_embeddings.weight" in key or "mlp.dense_h_to_4h.bias" in key:
if quantize and "mlp.dense_h_to_4h.weight" in key:
value_list = quantizer.Quantize(value_list, quantize_bits, groups, key=key)
new_client_sd[key] = torch.cat(value_list, axis=0)
else:
new_client_sd[key] = value_list[0]
if quantize:
all_scales = quantizer.merge_scales()
ds_sd = self.set_module(ds_sd, new_client_sd)
return ds_sd, (all_scales if quantize else None), len(client_sd_list)
def split_state_dict(self,
mp_world_size,
mp_rank,
quantize=False,
quantize_bits=8,
groups=64,
mlp_extra_grouping=True):
#self.sanity_check(self.ckpt_list[0])
sd, num_to_split, ckpt_offset = self.get_split_state_dict(mp_world_size, mp_rank)
ds_sd = copy.deepcopy(sd)
new_client_sd = collections.OrderedDict()
client_sd = self.get_module(sd)
ckpt_ver = self.get_checkpoint_version(ds_sd)
logger.info(f"checkpoint version: {ckpt_ver}")
if quantize:
quantizer = WeightQuantization(mlp_extra_grouping=mlp_extra_grouping, mp_size=mp_world_size)
for key in client_sd.keys():
value = client_sd[key]
if "attention.dense.weight" in key or "mlp.dense_4h_to_h.weight" in key:
assert value.shape[1] % num_to_split == 0
split_size = value.shape[1] // num_to_split
if quantize:
q_vals = quantizer.Quantize([value], quantize_bits, groups, key)
value = q_vals[0]
new_client_sd[key] = torch.split(value, split_size, dim=1)[ckpt_offset]
elif "attention.query_key_value" in key:
if quantize and "attention.query_key_value.weight" in key:
q_vals = quantizer.Quantize([value], quantize_bits, groups, key)
value = q_vals[0]
new_client_sd[key] = self.split_query_key_value(value, num_to_split, ckpt_offset, ckpt_ver)
elif "mlp.dense_h_to_4h.weight" in key or "word_embeddings.weight" in key or "mlp.dense_h_to_4h.bias" in key or "final_linear.weight" in key:
assert value.shape[0] % num_to_split == 0
split_size = value.shape[0] // num_to_split
if quantize and "mlp.dense_h_to_4h.weight" in key:
q_vals = quantizer.Quantize([value], quantize_bits, groups, key)
value = q_vals[0]
new_client_sd[key] = torch.split(value, split_size, dim=0)[ckpt_offset]
else:
new_client_sd[key] = value
if quantize:
all_scales = quantizer.merge_scales_split(num_to_split)
ds_sd = self.set_module(ds_sd, new_client_sd)
return ds_sd, (all_scales if quantize else None)
def sanity_check(self, ckpt_file_name):
keys_to_check = [
"attention.dense.weight", "mlp.dense_4h_to_h.weight", "attention.query_key_value",
"mlp.dense_h_to_4h.weight", "mlp.dense_h_to_4h.bias"
]
sd = self.checkpoint_engine.load(ckpt_file_name, map_location=lambda storage, loc: storage)
# partial_key is a sub-string of one key in the sd
def check_key_exist(partial_key, sd):
keys = sd.keys()
found = False
for k in keys:
if partial_key in k:
found = True
break
return found
for key in keys_to_check:
assert check_key_exist(key,
self.get_module(sd)), f'key: {key} is not found in the checkpoint {ckpt_file_name}'
def get_checkpoint_version(self, state_dict):
# Use 0 if version info doesn't exist
return self.version if self.version is not None else state_dict.get('checkpoint_version', 0)