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

913 lines
40 KiB
Python
Executable File

#!/usr/bin/env python
# Copyright (c) Microsoft Corporation.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
from functools import partial
from itertools import chain
import argparse
import glob
import itertools
import math
from concurrent.futures import ProcessPoolExecutor
import os
import re
import shutil
import torch
import tqdm
#from pprint import pprint
from deepspeed.checkpoint import DeepSpeedCheckpoint
from deepspeed.checkpoint import (
OPTIMIZER_STATE_DICT,
ZERO_STAGE,
BASE_OPTIMIZER_STATE,
SINGLE_PARTITION_OF_FP32_GROUPS,
PARAM_GROUPS,
PARAM_SLICE_MAPPINGS,
PARAM_SHAPES,
PARAM,
CAT_DIM,
PARAM_N_SUB_PARAMS,
SUB_PARAM_SHAPE,
VOCAB_TENSOR,
UNIVERSAL_CHECKPOINT_INFO,
UNIVERSAL_CHECKPOINT_VERSION_KEY,
UNIVERSAL_CHECKPOINT_VERSION_VALUE,
VOCABULARY_PARAMETER_PATTERNS,
PIPELINE_REPLICATED_PARAMETER_PATTERNS,
TP_REPLICATED_PARAMETER_PATTERNS,
PARAMETER_TO_AVERAGE_PATTERNS,
PARAMETER_WITH_ROW_PARALLELISM_PATTERNS,
PARAMETER_WITH_2_SUB_PARAMS_CAT_DIM_0,
PARAMETER_WITH_SUB_PARAMS,
AUTOEP_LAYERS_KEY,
AUTOEP_LAYERS_KEY_LEGACY,
EP_IS_EXPERT_PARAM,
EP_NUM_EXPERTS,
EXPERT_PARAMETER_PATTERNS,
SubparamShape,
)
from deepspeed.checkpoint.autoep_zero3_metadata import (
is_autoep_zero3_partitioned_entry,
validate_autoep_zero3_partitioned_metadata,
)
def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument('--input_folder', type=str, required=True, help='Input DeepSpeed Checkpoint folder')
parser.add_argument('--output_folder', type=str, required=True, help='Output DeepSpeed checkpoint folder')
parser.add_argument('--num_extract_workers',
default=4,
type=int,
help='How many parallel processes to extract zero shards')
parser.add_argument(
'--num_merge_workers',
default=2,
type=int,
help=
'How many parallel processes to merge tp slices (more memory intensive, use much fewer than --num_extract_workers))'
)
parser.add_argument('--keep_temp_folder',
action='store_true',
help='Preserve temporary folder of intermediate checkpoint slice files. Useful for debugging.')
parser.add_argument('--no_strict',
dest='strict',
action='store_false',
help='Do not perform validity checks on converted checkpoint.')
parser.add_argument('--inject_missing_state',
action='store_true',
help='Inject missing checkpoint state into the checkpoint if it is absent.')
args = parser.parse_args()
print(f'args = {args}')
return args
def atoi(text):
return int(text) if text.isdigit() else text
def natural_keys(text):
'''
alist.sort(key=natural_keys) sorts in human order
http://nedbatchelder.com/blog/200712/human_sorting.html
(See Toothy's implementation in the comments)
'''
return [atoi(c) for c in re.split(r'(\d+)', text)]
def _create_checkpoint_paths(base_folder, iteration, tp_degree, pp_degree):
path_list = []
iter_folder = f'iter_{iteration:07d}'
for i in range(0, tp_degree):
path_list.append([])
for j in range(0, pp_degree):
rank_folder = f'mp_rank_{i:02d}' if pp_degree == 1 else f'mp_rank_{i:02d}_{j:03d}'
ckpt_path = os.path.join(rank_folder, 'model_optim_rng.pt')
path_list[i].append(os.path.join(base_folder, iter_folder, ckpt_path))
return path_list
def _save_checkpoint(file_path, chkpt_sd):
dir, _ = os.path.split(file_path)
os.makedirs(dir, exist_ok=True)
torch.save(chkpt_sd, file_path)
def extract_zero_shards(dir, ds_checkpoint, indices_3D):
pp_index, tp_index, dp_index = indices_3D
sd = ds_checkpoint.get_zero_checkpoint_state(pp_index=pp_index, tp_index=tp_index, dp_index=dp_index)
# pprint(f"Processing {dp_index=} {pp_index=}, {tp_index=}")
optim_sd = sd[OPTIMIZER_STATE_DICT]
param_slice_mappings = optim_sd[PARAM_SLICE_MAPPINGS]
universal_checkpoint_info = ds_checkpoint.get_checkpoint_info(UNIVERSAL_CHECKPOINT_INFO)
pipeline_replicated_params = universal_checkpoint_info.get(PIPELINE_REPLICATED_PARAMETER_PATTERNS, [])
# print(f'{pipeline_replicated_params=}')
# dict
state_groups = optim_sd[BASE_OPTIMIZER_STATE]["state"]
# list
fp32_groups = optim_sd[SINGLE_PARTITION_OF_FP32_GROUPS]
param_groups_cnt = len(state_groups)
for param_group_id in range(param_groups_cnt):
flat_state = dict(
exp_avg=state_groups[param_group_id]["exp_avg"],
exp_avg_sq=state_groups[param_group_id]["exp_avg_sq"],
fp32=fp32_groups[param_group_id],
)
if "step" in state_groups[param_group_id]:
flat_state["step"] = state_groups[param_group_id]["step"]
for name, fragment_mapping in param_slice_mappings[param_group_id].items():
if pp_index > 0 and any(re.match(pattern, name) for pattern in pipeline_replicated_params):
# Skip tied weights that are replicated in first and last pp stages
continue
# pprint(f"dpt{dp_index}{pp_index}{tp_index} {param_group_id} {name} => {fragment_mapping.start}:{fragment_mapping.numel}")
for state_key in flat_state.keys():
dump_param_fragment(dir, tp_index, dp_index, state_key, flat_state[state_key], name,
fragment_mapping.start, fragment_mapping.numel)
def extract_zero_shards_stage3(optim_files, param_shapes, dp_degree, temp_dir, dp_index, exclude_param_names=None):
exclude_param_names = exclude_param_names or set()
state_dict = torch.load(optim_files[dp_index], map_location='cpu', weights_only=False)
optim_sd = state_dict[OPTIMIZER_STATE_DICT]
partition_groups = optim_sd.get('ds_zero_partition_groups') or []
for idx, sub_group_shape in enumerate(param_shapes):
flat_state = dict(
exp_avg=optim_sd['optimizer_state_dict']['state'][idx]["exp_avg"],
exp_avg_sq=optim_sd['optimizer_state_dict']['state'][idx]["exp_avg_sq"],
fp32=optim_sd['fp32_flat_groups'][idx],
)
partition_metadata = partition_groups[idx] if idx < len(partition_groups) else {}
partition_count = partition_metadata.get('partition_count', dp_degree)
partition_rank = partition_metadata.get('partition_rank', dp_index)
offset = 0
for name, shape in sub_group_shape.items():
unpartitioned_numel = _shape_numel(shape)
partitioned_numel, _ = _zero_partitioned_param_info(unpartitioned_numel, partition_count)
padding_free_numel = max(0, min(partitioned_numel,
unpartitioned_numel - partition_rank * partitioned_numel))
if name not in exclude_param_names:
for state_key in flat_state.keys():
dump_param_fragment(temp_dir, 0, dp_index, state_key, flat_state[state_key], name, offset,
padding_free_numel)
offset += partitioned_numel
cnt = 0
def dp_index_to_str(dp_index):
return f"{dp_index:0>2d}"
def dump_param_fragment(dir, tp_index, dp_index, state_name, state_flat_tensor, param_name, offset, numel):
global cnt # temp hack
param_base_path = os.path.join(dir, param_name, str(tp_index))
os.makedirs(param_base_path, exist_ok=True)
cnt += 1
path = os.path.join(param_base_path, f"{state_name}.{dp_index_to_str(dp_index)}")
#print(f"{param_name}: {offset}: {numel} => {path}")
# State might be a python int or a tensor
if state_name != "step" and torch.is_tensor(state_flat_tensor):
state_flat_tensor = state_flat_tensor.narrow(0, offset, numel).clone()
_save_checkpoint(path, state_flat_tensor)
def _merge_zero_shards(param_base_path, state, tp_degree, slice_shape=None):
slices = []
for tp_index in range(tp_degree):
prefix_path = os.path.join(param_base_path, str(tp_index), f"{state}")
paths = glob.glob(f"{prefix_path}.*")
if len(paths) == 0:
continue
pattern = re.compile(f"{prefix_path}\\.([0-9]+)")
dp_indices = set()
for p in paths:
m = pattern.match(p)
if m:
dp_indices.add(int(m.group(1)))
else:
raise ValueError(f"Cannot parse dp_rank from {p}")
paths = [f"{prefix_path}.{dp_index_to_str(dp_index)}" for dp_index in sorted(list(dp_indices))]
shards = [torch.load(p, weights_only=False) for p in paths]
if state == "step":
assert all(v == shards[0] for v in shards), "All shards must have the same step value"
slice = shards[0]
else:
if slice_shape is None:
slice = torch.cat(shards, dim=0)
else:
slice = torch.cat(shards, dim=0).reshape(slice_shape)
slices.append(slice)
return slices
def merge_tp_slices(ds_checkpoint, dir, slice_dir, tp_degree, name_and_shape):
name, shape = name_and_shape
slice_base_path = os.path.join(slice_dir, name)
param_base_path = os.path.join(dir, name)
universal_checkpoint_info = ds_checkpoint.get_checkpoint_info(UNIVERSAL_CHECKPOINT_INFO)
replicated_parameters = universal_checkpoint_info.get(TP_REPLICATED_PARAMETER_PATTERNS, [])
parameters_to_average = universal_checkpoint_info.get(PARAMETER_TO_AVERAGE_PATTERNS, [])
parameters_with_row_parallelism = universal_checkpoint_info.get(PARAMETER_WITH_ROW_PARALLELISM_PATTERNS, [])
vocabulary_parameters = universal_checkpoint_info.get(VOCABULARY_PARAMETER_PATTERNS, [])
parameters_with_2_sub_params_cat_dim_0 = universal_checkpoint_info.get(PARAMETER_WITH_2_SUB_PARAMS_CAT_DIM_0, [])
parameter_with_sub_params = universal_checkpoint_info.get(PARAMETER_WITH_SUB_PARAMS, [])
unmatched_patterns = set(replicated_parameters + parameters_to_average + parameters_with_row_parallelism +
vocabulary_parameters + parameters_with_2_sub_params_cat_dim_0)
unmatched_patterns.update(chain.from_iterable(SubparamShape(**s).patterns for s in parameter_with_sub_params))
def get_matched_pattern(patterns_, name_):
matched_ = [pattern_ for pattern_ in patterns_ if re.match(pattern_, name_)]
assert len(matched_) <= 1, f'Got more than one matching patterns={matched_} for {name_}'
if matched_:
pattern_ = matched_[0]
unmatched_patterns.discard(pattern_)
return pattern_
return None
def get_matched_sub_params_pattern(name_):
for subparam_shape_dict in parameter_with_sub_params:
subparam_shape = SubparamShape(**subparam_shape_dict)
for pattern_ in subparam_shape.patterns:
if re.match(pattern_, name_):
unmatched_patterns.discard(pattern_)
return subparam_shape
return None
matched_sub_params_shape = get_matched_sub_params_pattern(name)
step_merged = _merge_zero_shards(slice_base_path, "step", tp_degree, shape)
if step_merged:
_save_checkpoint(os.path.join(param_base_path, "step.pt"), step_merged[0])
for state in ("fp32", "exp_avg", "exp_avg_sq"):
slices = _merge_zero_shards(slice_base_path, state, tp_degree, shape)
final_path = os.path.join(param_base_path, f"{state}.pt")
#print(f"Expected shape: {shape}")
#print(f"Fragment sizes:", list(frag.shape for frag in slices))
ckpt_dict = {}
if get_matched_pattern(replicated_parameters, name):
if len(slices) > 1:
assert all([slices[0].equal(other_slice) for other_slice in slices[1:]])
param = slices[0]
# print(f'replicate {name} using first slice')
elif get_matched_pattern(parameters_to_average, name):
param = sum(slices) / len(slices)
# print(f'merge {name} using average')
elif get_matched_pattern(parameters_with_2_sub_params_cat_dim_0, name):
cat_dim = 0
chunked_slices = [torch.chunk(s, 2, dim=cat_dim) for s in slices]
merged_chunks_0 = torch.cat([s[0] for s in chunked_slices], dim=cat_dim)
merged_chunks_1 = torch.cat([s[1] for s in chunked_slices], dim=cat_dim)
param = torch.cat([merged_chunks_0, merged_chunks_1], dim=cat_dim)
ckpt_dict[CAT_DIM] = cat_dim
ckpt_dict[PARAM_N_SUB_PARAMS] = 2
elif matched_sub_params_shape:
merged_chunks = []
partition_dim = matched_sub_params_shape.partition_dim
sub_dim_sizes = matched_sub_params_shape.shape[partition_dim]
if not isinstance(sub_dim_sizes, tuple):
sub_dim_sizes = (sub_dim_sizes, )
partition_shape = [sum(d) if isinstance(d, tuple) else d for d in matched_sub_params_shape.shape]
partition_shape = [d // tp_degree if i == partition_dim else d for i, d in enumerate(partition_shape)]
slices = [s.view(partition_shape) for s in slices]
offset = 0
for sub_dim_size in sub_dim_sizes:
part_sub_dim_size = sub_dim_size // tp_degree
merged_chunks.append(
torch.cat([s.narrow(partition_dim, offset, part_sub_dim_size) for s in slices], dim=partition_dim))
offset += part_sub_dim_size
param = torch.cat(merged_chunks, dim=partition_dim)
ckpt_dict[SUB_PARAM_SHAPE] = matched_sub_params_shape
else:
cat_dim = 1 if get_matched_pattern(parameters_with_row_parallelism, name) else 0
# print(f"merge {name} with CAT DIM: {cat_dim}")
param = torch.cat(slices, dim=cat_dim)
ckpt_dict[CAT_DIM] = cat_dim
if get_matched_pattern(vocabulary_parameters, name):
#print(f"Before {param.shape=}")
# strip padding
original_vocab_size = universal_checkpoint_info['original_vocab_size']
param = param[:original_vocab_size, :]
ckpt_dict[VOCAB_TENSOR] = True
#print(f"After {param.shape=}")
#print(f"Final shape: {param.shape}")
ckpt_dict[PARAM] = param
_save_checkpoint(final_path, ckpt_dict)
return unmatched_patterns
def merge_zero3_slices(dp_degree, dir, slice_dir, name):
slice_base_path = os.path.join(slice_dir, name)
param_base_path = os.path.join(dir, name)
for state in ("fp32", "exp_avg", "exp_avg_sq"):
slices = _merge_zero_shards(slice_base_path, state, 1)
final_path = os.path.join(param_base_path, f"{state}.pt")
_save_checkpoint(final_path, slices[0])
def _do_parallel_work(do_work, work_chunks, num_workers):
results = []
if num_workers > 1:
with ProcessPoolExecutor(max_workers=num_workers) as executor:
future_list = [executor.submit(do_work, work) for work in work_chunks]
for f in tqdm.tqdm(future_list):
results.append(f.result())
else:
# No parallel pass for unit testing
# We can't create child processes in tests
for work in tqdm.tqdm(work_chunks):
results.append(do_work(work))
return results
def _extract_zero_shard_files(args, ds_checkpoint, temp_dir):
_3d_range_list = list(
itertools.product(range(ds_checkpoint.pp_degree), range(ds_checkpoint.tp_degree),
range(ds_checkpoint.dp_degree)))
#pprint(f'{_3d_range_list=}')
do_work = partial(extract_zero_shards, temp_dir, ds_checkpoint)
_do_parallel_work(do_work, _3d_range_list, args.num_extract_workers)
def _extract_zero_shard_files_stage3(args, optim_files, param_shapes, dp_degree, temp_dir, exclude_param_names=None):
do_work = partial(extract_zero_shards_stage3,
optim_files,
param_shapes,
dp_degree,
temp_dir,
exclude_param_names=exclude_param_names)
_do_parallel_work(do_work, list(range(dp_degree)), args.num_extract_workers)
def _merge_tp_slice_files(args, ds_checkpoint, slice_shapes, temp_dir):
zero_output_folder = os.path.join(args.output_folder, "zero")
do_work = partial(merge_tp_slices, ds_checkpoint, zero_output_folder, temp_dir, ds_checkpoint.tp_degree)
unmatched_patterns_lists = _do_parallel_work(do_work, list(slice_shapes.items()), args.num_merge_workers)
# verify that all patterns were used
# if a pattern was not used by any of the workers, then it was not used at all -> assert/alert
sets = [set(lst) for lst in unmatched_patterns_lists]
unmatched_patterns = list(set.intersection(*sets))
if args.strict:
assert not unmatched_patterns, f'Unused patterns={unmatched_patterns} while merging tp slices'
elif unmatched_patterns:
print(f'Warning: Unused patterns={unmatched_patterns} while merging tp slices')
def _merge_zero3_slice_files(args, param_keys, dp_degree, temp_dir):
zero_output_folder = os.path.join(args.output_folder, "zero")
do_work = partial(merge_zero3_slices, dp_degree, zero_output_folder, temp_dir)
_do_parallel_work(do_work, param_keys, args.num_merge_workers)
def _zero_partitioned_param_info(unpartitioned_numel, world_size):
remainder = unpartitioned_numel % world_size
padding_numel = (world_size - remainder) if remainder else 0
partitioned_numel = math.ceil(unpartitioned_numel / world_size)
return partitioned_numel, padding_numel
def _shape_numel(shape):
if hasattr(shape, "numel"):
return shape.numel()
return math.prod(shape)
def _zero3_rank_from_file(path):
match = re.search(r'(?:bf16_)?zero_pp_rank_([0-9]+)_mp_rank_', os.path.basename(path))
if match is None:
raise ValueError(f"Cannot parse ZeRO rank from checkpoint file name: {path}")
return int(match.group(1))
def _get_autoep_metadata(model_state):
autoep_metadata = model_state.get(AUTOEP_LAYERS_KEY)
if autoep_metadata is None:
autoep_metadata = model_state.get(AUTOEP_LAYERS_KEY_LEGACY)
return autoep_metadata
def _uses_zero3_partitioned_autoep_metadata(autoep_metadata):
if not isinstance(autoep_metadata, list):
return False
_validate_zero3_partitioned_autoep_metadata(autoep_metadata, require_partitioned=False)
return any(is_autoep_zero3_partitioned_entry(entry) for entry in autoep_metadata)
def _validate_zero3_partitioned_autoep_metadata(autoep_metadata, require_partitioned=True):
validate_autoep_zero3_partitioned_metadata(autoep_metadata,
require_partitioned=require_partitioned,
version_context="This converter")
def _autoep_expert_param_info(autoep_metadata):
info = {}
if not isinstance(autoep_metadata, list):
return info
_validate_zero3_partitioned_autoep_metadata(autoep_metadata)
for entry in autoep_metadata:
if not isinstance(entry, dict):
continue
if not is_autoep_zero3_partitioned_entry(entry):
continue
prefix = entry.get('expert_key_prefix')
if not prefix:
continue
for wname in ('w1', 'w2', 'w3'):
info[f"{prefix}.{wname}"] = entry
return info
def _autoep_expert_param_names_by_rank(model_files):
expert_param_names = set()
metadata_by_rank = {}
for model_file in model_files:
rank = _zero3_rank_from_file(model_file)
model_state = torch.load(model_file, map_location=torch.device('cpu'), weights_only=False)
autoep_metadata = _get_autoep_metadata(model_state)
if autoep_metadata is not None:
metadata_by_rank[rank] = autoep_metadata
if _uses_zero3_partitioned_autoep_metadata(autoep_metadata):
expert_param_names.update(_autoep_expert_param_info(autoep_metadata))
return expert_param_names, metadata_by_rank
def _rank_map_from_files(files, description):
rank_map = {}
for path in files:
rank = _zero3_rank_from_file(path)
if rank in rank_map:
raise RuntimeError(f"Duplicate ZeRO rank {rank} in {description} files: "
f"{rank_map[rank]} and {path}")
rank_map[rank] = path
return rank_map
def _validate_zero3_model_optim_rank_sets(model_files, optim_files):
model_rank_map = _rank_map_from_files(model_files, "model-state")
optim_rank_map = _rank_map_from_files(optim_files, "optimizer-state")
model_ranks = set(model_rank_map)
optim_ranks = set(optim_rank_map)
if model_ranks != optim_ranks:
raise RuntimeError("ZeRO-3 checkpoint model/optimizer rank sets do not match: "
f"model_only={sorted(model_ranks - optim_ranks)}, "
f"optim_only={sorted(optim_ranks - model_ranks)}")
if not model_ranks:
raise RuntimeError("ZeRO-3 checkpoint has no model/optimizer rank files")
return model_rank_map, optim_rank_map
def _validate_autoep_expert_shapes(model_states_by_rank, metadata_by_rank):
for rank, autoep_metadata in metadata_by_rank.items():
if not _uses_zero3_partitioned_autoep_metadata(autoep_metadata):
continue
expert_info = _autoep_expert_param_info(autoep_metadata)
param_shapes = model_states_by_rank[rank][PARAM_SHAPES]
zero_shape_names = {name for sub_group_shape in param_shapes for name in sub_group_shape}
missing = set(expert_info) - zero_shape_names
if missing:
raise RuntimeError(f"AutoEP expert parameters are missing from rank {rank} ZeRO param_shapes: "
f"{sorted(missing)}")
frozen_shapes = model_states_by_rank[rank].get('frozen_param_shapes') or {}
frozen_experts = set(expert_info).intersection(frozen_shapes)
if frozen_experts:
raise RuntimeError("AutoEP frozen expert parameters cannot be converted from the ZeRO-3 "
f"partition-native format yet: {sorted(frozen_experts)}")
def _save_zero3_autoep_universal_tensor(output_dir, param_name, state_key, tensor, num_experts):
param_dir = os.path.join(output_dir, "zero", param_name)
os.makedirs(param_dir, exist_ok=True)
_save_checkpoint(
os.path.join(param_dir, f"{state_key}.pt"),
{
PARAM: tensor,
CAT_DIM: 0,
EP_IS_EXPERT_PARAM: True,
EP_NUM_EXPERTS: num_experts,
},
)
def _consolidate_zero3_autoep_expert_states(output_dir, model_files, optim_files):
model_rank_map, optim_rank_map = _validate_zero3_model_optim_rank_sets(model_files, optim_files)
model_states_by_rank = {
rank: torch.load(model_file, map_location=torch.device('cpu'), weights_only=False)
for rank, model_file in model_rank_map.items()
}
optim_states_by_rank = {
rank: torch.load(optim_file, map_location=torch.device('cpu'), weights_only=False)
for rank, optim_file in optim_rank_map.items()
}
metadata_by_rank = {
rank: _get_autoep_metadata(model_state)
for rank, model_state in model_states_by_rank.items() if _get_autoep_metadata(model_state) is not None
}
_validate_autoep_expert_shapes(model_states_by_rank, metadata_by_rank)
expert_fragments = {}
num_experts_by_param = {}
expected_dp_world_by_param_rank = {}
expected_ep_ranks_by_param = {}
for rank, model_state in model_states_by_rank.items():
optim_state = optim_states_by_rank.get(rank)
if optim_state is None:
raise FileNotFoundError(f"Missing ZeRO optimizer checkpoint for rank {rank}")
autoep_metadata = _get_autoep_metadata(model_state)
if not _uses_zero3_partitioned_autoep_metadata(autoep_metadata):
continue
expert_info = _autoep_expert_param_info(autoep_metadata)
param_shapes = model_state[PARAM_SHAPES]
zero_optim_state = optim_state[OPTIMIZER_STATE_DICT]
partition_groups = zero_optim_state.get('ds_zero_partition_groups') or []
for sub_group_id, sub_group_shape in enumerate(param_shapes):
optimizer_sub_state = zero_optim_state['optimizer_state_dict']['state'][sub_group_id]
flat_state = {
'fp32': zero_optim_state['fp32_flat_groups'][sub_group_id],
'exp_avg': optimizer_sub_state.get('exp_avg'),
'exp_avg_sq': optimizer_sub_state.get('exp_avg_sq'),
}
partition_metadata = partition_groups[sub_group_id] if sub_group_id < len(partition_groups) else {}
partition_count = partition_metadata.get('partition_count', len(model_states_by_rank))
partition_rank = partition_metadata.get('partition_rank', rank)
offset = 0
for param_name, shape in sub_group_shape.items():
unpartitioned_numel = _shape_numel(shape)
partitioned_numel, _ = _zero_partitioned_param_info(unpartitioned_numel, partition_count)
padding_free_numel = max(
0, min(partitioned_numel, unpartitioned_numel - partition_rank * partitioned_numel))
layer_info = expert_info.get(param_name)
if layer_info is not None:
ep_rank = layer_info['ep_rank']
num_experts_by_param[param_name] = layer_info['num_experts']
expected_dp_world_by_param_rank[(param_name,
ep_rank)] = layer_info['expert_data_parallel_world_size']
expected_ep_ranks_by_param[param_name] = set(range(layer_info['ep_size']))
for state_key, flat_tensor in flat_state.items():
if flat_tensor is None:
raise RuntimeError(f"Missing optimizer state '{state_key}' for AutoEP expert "
f"parameter {param_name} on ZeRO rank {rank}")
fragment = flat_tensor.narrow(0, offset, padding_free_numel).clone()
key = (param_name, state_key, ep_rank)
expert_fragments.setdefault(key, []).append((partition_rank, fragment, shape))
offset += partitioned_numel
grouped_by_param = {}
for (param_name, state_key, ep_rank), fragments in expert_fragments.items():
grouped_by_param.setdefault((param_name, state_key), {})[ep_rank] = fragments
for (param_name, state_key), ep_rank_fragments in grouped_by_param.items():
missing_ep_ranks = expected_ep_ranks_by_param[param_name] - set(ep_rank_fragments)
if missing_ep_ranks:
raise RuntimeError(f"Missing AutoEP universal fragments for {param_name}/{state_key} EP ranks: "
f"{sorted(missing_ep_ranks)}")
ep_tensors = []
for ep_rank in sorted(ep_rank_fragments):
fragments = sorted(ep_rank_fragments[ep_rank], key=lambda item: item[0])
expected_dp_world = expected_dp_world_by_param_rank[(param_name, ep_rank)]
partition_ranks = [partition_rank for partition_rank, _, _ in fragments]
if len(partition_ranks) != len(set(partition_ranks)):
raise RuntimeError(f"Duplicate AutoEP expert-DP partition ranks for {param_name}/{state_key} "
f"EP rank {ep_rank}: {partition_ranks}")
if set(partition_ranks) != set(range(expected_dp_world)):
raise RuntimeError(f"Incomplete AutoEP expert-DP fragments for {param_name}/{state_key} "
f"EP rank {ep_rank}: got {sorted(partition_ranks)}, "
f"expected {list(range(expected_dp_world))}")
shape = fragments[0][2]
if any(tuple(fragment_shape) != tuple(shape) for _, _, fragment_shape in fragments):
raise RuntimeError(f"Inconsistent AutoEP expert fragment shapes for {param_name}/{state_key} "
f"EP rank {ep_rank}")
full_flat = torch.cat([fragment for _, fragment, _ in fragments], dim=0)[:_shape_numel(shape)]
ep_tensors.append(full_flat.view(shape))
if not ep_tensors:
continue
full_expert_tensor = torch.cat(ep_tensors, dim=0)
if full_expert_tensor.shape[0] != num_experts_by_param[param_name]:
raise RuntimeError(f"AutoEP universal tensor for {param_name}/{state_key} has wrong expert dimension: "
f"got {full_expert_tensor.shape[0]}, expected {num_experts_by_param[param_name]}")
_save_zero3_autoep_universal_tensor(output_dir, param_name, state_key, full_expert_tensor,
num_experts_by_param[param_name])
def _parse_model_states_stage3(files):
return torch.load(files[0], map_location=torch.device('cpu'), weights_only=False)[PARAM_SHAPES]
def _save_optimizer_state(args, ds_checkpoint):
sharded_states = [BASE_OPTIMIZER_STATE, PARAM_SLICE_MAPPINGS, SINGLE_PARTITION_OF_FP32_GROUPS]
sd = ds_checkpoint.get_zero_checkpoint_state(pp_index=0, tp_index=0, dp_index=0)
optim_sd = sd[OPTIMIZER_STATE_DICT]
output_sd = {k: v for k, v in optim_sd.items() if k not in sharded_states}
output_sd[PARAM_GROUPS] = optim_sd[BASE_OPTIMIZER_STATE][PARAM_GROUPS]
zero_output_folder = os.path.join(args.output_folder, "zero")
output_file_path = os.path.join(zero_output_folder, "optimizer_state.pt")
_save_checkpoint(output_file_path, output_sd)
def _save_optimizer_state_stage3(args, optim_files):
sd = torch.load(optim_files[0], map_location=torch.device('cpu'), weights_only=False)
output_sd = sd[OPTIMIZER_STATE_DICT]
output_sd[PARAM_GROUPS] = output_sd[OPTIMIZER_STATE_DICT][PARAM_GROUPS]
zero_output_folder = os.path.join(args.output_folder, "zero")
output_file_path = os.path.join(zero_output_folder, "optimizer_state.pt")
_save_checkpoint(output_file_path, output_sd)
def _get_optim_files(checkpoint_dir):
return _get_checkpoint_files(checkpoint_dir, "*_optim_states.pt")
def _filter_zero3_optim_files(optim_files):
return [f for f in optim_files if re.match(r'(?:bf16_)?zero_pp_rank_', os.path.basename(f))]
def _get_model_state_files(checkpoint_dir):
return _get_checkpoint_files(checkpoint_dir, "*_model_states.pt")
def _is_expert_model_state_file(checkpoint_file):
basename = os.path.basename(checkpoint_file)
return basename.startswith('layer_') and '_expert_' in basename
def _get_zero3_model_state_files(checkpoint_dir):
model_files = [f for f in _get_model_state_files(checkpoint_dir) if not _is_expert_model_state_file(f)]
if len(model_files) == 0:
raise FileNotFoundError(f"can't find ZeRO Stage 3 model state files in directory '{checkpoint_dir}'")
return model_files
def _raise_if_stage3_autoep_universal_conversion(model_files):
for model_file in model_files:
model_state = torch.load(model_file, map_location=torch.device('cpu'), weights_only=False)
autoep_metadata = model_state.get(AUTOEP_LAYERS_KEY)
if autoep_metadata is None:
autoep_metadata = model_state.get(AUTOEP_LAYERS_KEY_LEGACY)
if autoep_metadata is not None:
raise NotImplementedError("Stage 3 universal checkpoint conversion with AutoEP is not supported. "
"Use regular same-topology ZeRO-3 checkpoint load for AutoEP checkpoints.")
def _get_checkpoint_files(checkpoint_dir, glob_pattern):
ckpt_files = sorted(glob.glob(os.path.join(checkpoint_dir, glob_pattern)), key=natural_keys)
if len(ckpt_files) == 0:
raise FileNotFoundError(f"can't find {glob_pattern} files in directory '{checkpoint_dir}'")
return ckpt_files
def _get_zero_stage(optim_files):
state_dict = torch.load(optim_files[0], map_location=torch.device('cpu'), weights_only=False)
optimizer_state = state_dict[OPTIMIZER_STATE_DICT]
zero_stage = optimizer_state.get(ZERO_STAGE, 1)
return zero_stage
def _inject_missing_state(ds_checkpoint):
if UNIVERSAL_CHECKPOINT_INFO not in ds_checkpoint.global_state:
sd = torch.load(ds_checkpoint.mp_rank_files[0], map_location=torch.device('cpu'), weights_only=False)
if UNIVERSAL_CHECKPOINT_INFO not in sd:
ds_checkpoint.global_state[UNIVERSAL_CHECKPOINT_INFO] = {}
ds_checkpoint.global_state[UNIVERSAL_CHECKPOINT_INFO][
UNIVERSAL_CHECKPOINT_VERSION_KEY] = UNIVERSAL_CHECKPOINT_VERSION_VALUE
def _check_for_required_state(ds_checkpoint):
universal_checkpoint_info = ds_checkpoint.get_checkpoint_info(UNIVERSAL_CHECKPOINT_INFO)
assert universal_checkpoint_info is not None, f'Required {UNIVERSAL_CHECKPOINT_INFO} state is missing in checkpoint. Verify that client creates this state.'
def _classify_autoep_expert_file_consolidation(autoep_metadata, expert_files):
if autoep_metadata is not None:
return 'autoep'
if expert_files:
return 'native_moe'
return 'none'
def main(args):
print('Convert DeepSpeed Checkpoint to Universal Checkpoint')
print(f'Converting DeepSpeed checkpoint in {args.input_folder} to Universal checkpoint in {args.output_folder}')
optim_files = _get_optim_files(args.input_folder)
zero3_optim_files = _filter_zero3_optim_files(optim_files)
zero_stage = _get_zero_stage(zero3_optim_files or optim_files)
if zero_stage > 2 and zero3_optim_files:
optim_files = zero3_optim_files
if zero_stage <= 2:
ds_checkpoint = DeepSpeedCheckpoint(args.input_folder)
if args.inject_missing_state:
_inject_missing_state(ds_checkpoint)
else:
_check_for_required_state(ds_checkpoint)
iteration = ds_checkpoint.get_iteration()
#_create_latest_file(args.output_folder, iteration)
checkpoint_paths = _create_checkpoint_paths(args.output_folder, iteration, ds_checkpoint.tp_degree,
ds_checkpoint.pp_degree)
slice_shapes = []
for mp_rank_file in ds_checkpoint.mp_rank_files:
mp_sd = torch.load(mp_rank_file, map_location=torch.device('cpu'), weights_only=False)
slice_shapes += mp_sd[PARAM_SHAPES]
# fix back to normal flat dict, merge duplicates for tp>1
slice_shapes = dict((k, v) for d in slice_shapes for k, v in d.items())
temp_dir = os.path.join(args.output_folder, 'tmp')
print('*** 1. Extracting ZeRO fragments')
_extract_zero_shard_files(args, ds_checkpoint, temp_dir)
print('*** 2. Merging slices .....')
_merge_tp_slice_files(args, ds_checkpoint, slice_shapes, temp_dir)
print('*** 2.5. Consolidating AutoEP expert files')
from deepspeed.checkpoint.autoep_universal import (
consolidate_autoep_expert_files,
consolidate_autoep_optimizer_states,
)
# Load AutoEP metadata from main checkpoint
main_sd = torch.load(ds_checkpoint.mp_rank_files[0], map_location=torch.device('cpu'), weights_only=False)
autoep_metadata = main_sd.get(AUTOEP_LAYERS_KEY)
if autoep_metadata is None:
autoep_metadata = main_sd.get(AUTOEP_LAYERS_KEY_LEGACY)
# Check for expert files in checkpoint directory
expert_files = glob.glob(os.path.join(args.input_folder, 'layer_*_expert_*_model_states.pt'))
autoep_expert_file_type = _classify_autoep_expert_file_consolidation(autoep_metadata, expert_files)
if autoep_expert_file_type == 'autoep':
consolidate_autoep_expert_files(args.input_folder, args.output_folder, autoep_metadata)
ep_size = autoep_metadata[0]['ep_size'] if autoep_metadata else 1
consolidate_autoep_optimizer_states(args.input_folder, args.output_folder, autoep_metadata, ep_size)
print(f' Consolidated {len(autoep_metadata)} AutoEP layer(s)')
elif autoep_expert_file_type == 'native_moe':
print(f' Found {len(expert_files)} expert checkpoint file(s) but no AutoEP metadata; '
'assuming native DeepSpeed MoE and skipping AutoEP consolidation')
else:
print(' No AutoEP layers found, skipping')
print('*** 3. Saving common optimizer states')
_save_optimizer_state(args, ds_checkpoint)
if not args.keep_temp_folder:
shutil.rmtree(temp_dir, ignore_errors=True)
# Copy mp* files into output folder, injecting AutoEP metadata into UNIVERSAL_CHECKPOINT_INFO
for f in glob.glob(os.path.join(args.input_folder, 'mp*')):
if autoep_metadata is not None:
# Load -> update with AutoEP metadata -> save
mp_sd = torch.load(f, map_location=torch.device('cpu'), weights_only=False)
if UNIVERSAL_CHECKPOINT_INFO not in mp_sd:
mp_sd[UNIVERSAL_CHECKPOINT_INFO] = {}
mp_sd[UNIVERSAL_CHECKPOINT_INFO][EXPERT_PARAMETER_PATTERNS] = [r'\.experts\.w[123]$']
mp_sd[UNIVERSAL_CHECKPOINT_INFO][AUTOEP_LAYERS_KEY] = autoep_metadata
out_path = os.path.join(args.output_folder, os.path.basename(f))
torch.save(mp_sd, out_path)
else:
shutil.copy2(f, args.output_folder)
else:
# Stage 3 path
model_files = _get_zero3_model_state_files(args.input_folder)
autoep_expert_param_names, autoep_metadata_by_rank = _autoep_expert_param_names_by_rank(model_files)
has_autoep_metadata = any(metadata is not None for metadata in autoep_metadata_by_rank.values())
has_zero3_partitioned_autoep = any(
_uses_zero3_partitioned_autoep_metadata(metadata) for metadata in autoep_metadata_by_rank.values())
if has_autoep_metadata and not has_zero3_partitioned_autoep:
raise NotImplementedError("Stage 3 universal checkpoint conversion for AutoEP requires the "
"partition-native AutoEP ZeRO-3 checkpoint format.")
if not has_zero3_partitioned_autoep:
autoep_expert_param_names = set()
else:
_validate_zero3_model_optim_rank_sets(model_files, optim_files)
param_shapes = _parse_model_states_stage3(model_files)
dp_degree = len(model_files)
temp_dir = os.path.join(args.output_folder, 'tmp')
print('*** 1. Extracting ZeRO fragments')
_extract_zero_shard_files_stage3(args,
optim_files,
param_shapes,
dp_degree,
temp_dir,
exclude_param_names=autoep_expert_param_names)
print('*** 2. Merging slices .....')
param_keys = {key for sub_group_shapes in param_shapes for key in sub_group_shapes.keys()}
param_keys -= autoep_expert_param_names
_merge_zero3_slice_files(args, param_keys, dp_degree, temp_dir)
if has_zero3_partitioned_autoep:
print('*** 2.5. Consolidating AutoEP ZeRO-3 expert states')
_consolidate_zero3_autoep_expert_states(args.output_folder, model_files, optim_files)
print('*** 3. Saving common optimizer states')
_save_optimizer_state_stage3(args, optim_files)
if not args.keep_temp_folder:
shutil.rmtree(temp_dir, ignore_errors=True)
# Copy *model_states files into output folder, filtering out expert files
for f in glob.glob(os.path.join(args.input_folder, '*model_states.pt')):
if _is_expert_model_state_file(f):
continue
if has_zero3_partitioned_autoep:
model_state = torch.load(f, map_location=torch.device('cpu'), weights_only=False)
autoep_metadata = _get_autoep_metadata(model_state)
if UNIVERSAL_CHECKPOINT_INFO not in model_state:
model_state[UNIVERSAL_CHECKPOINT_INFO] = {}
model_state[UNIVERSAL_CHECKPOINT_INFO][EXPERT_PARAMETER_PATTERNS] = [r'.*\.experts\.w[123]$']
model_state[UNIVERSAL_CHECKPOINT_INFO][AUTOEP_LAYERS_KEY] = autoep_metadata
torch.save(model_state, os.path.join(args.output_folder, os.path.basename(f)))
else:
shutil.copy2(f, args.output_folder)
# Update latest to output folder
checkpoint_root_folder, step_folder = os.path.split(args.output_folder)
latest_file = os.path.join(checkpoint_root_folder, 'latest_universal')
with open(latest_file, "w") as f:
f.write(step_folder)
print('*** Done!')
if __name__ == "__main__":
args = parse_arguments()
main(args)