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

388 lines
15 KiB
Python

# Copyright (c) DeepSpeed Team.
# SPDX-License-Identifier: Apache-2.0
# DeepSpeed Team
"""AutoEP universal checkpoint conversion utilities.
Consolidates per-expert checkpoint files (and their optimizer states) into
topology-agnostic universal format for EP resharding support.
"""
import os
import glob
import torch
from .constants import (
PARAM,
CAT_DIM,
EP_IS_EXPERT_PARAM,
EP_NUM_EXPERTS,
FOLDING_METADATA_KEY,
FOLDING_METADATA_VERSION,
FOLDING_TP_SIZE,
FOLDING_TP_RANK,
FOLDING_EP_SIZE,
FOLDING_EP_RANK,
FOLDING_ETP_SIZE,
FOLDING_ETP_RANK,
FOLDING_ZERO_PARTITION_GROUP,
FOLDING_ZERO_PARTITION_RANK,
FOLDING_ZERO_PARTITION_COUNT,
FOLDING_DISPATCH_STRATEGY,
FOLDING_SHARED_EXPERT_PLACEMENT,
FOLDING_FAMILY,
FOLDING_PARAM_FAMILIES,
)
def make_folding_metadata(*,
tp_size,
tp_rank,
ep_size,
ep_rank,
zero_partition_group,
zero_partition_rank,
zero_partition_count,
family,
param_families=None):
metadata = {
"version": FOLDING_METADATA_VERSION,
FOLDING_TP_SIZE: tp_size,
FOLDING_TP_RANK: tp_rank,
FOLDING_EP_SIZE: ep_size,
FOLDING_EP_RANK: ep_rank,
FOLDING_ETP_SIZE: 1,
FOLDING_ETP_RANK: 0,
FOLDING_ZERO_PARTITION_GROUP: zero_partition_group,
FOLDING_ZERO_PARTITION_RANK: zero_partition_rank,
FOLDING_ZERO_PARTITION_COUNT: zero_partition_count,
FOLDING_DISPATCH_STRATEGY: "route_full_partition_dispatch",
FOLDING_SHARED_EXPERT_PLACEMENT: "tp_sharded",
FOLDING_FAMILY: family,
}
if param_families is not None:
metadata[FOLDING_PARAM_FAMILIES] = dict(param_families)
return metadata
def validate_folding_metadata(metadata,
*,
tp_size,
ep_size,
etp_size=1,
tp_rank=None,
ep_rank=None,
etp_rank=None,
zero_partition_group=None,
zero_partition_rank=None,
zero_partition_count=None,
family=None,
param_families=None,
shared_expert_placement=None,
dispatch_strategy=None):
if not isinstance(metadata, dict) or FOLDING_METADATA_KEY not in metadata:
raise RuntimeError("Missing AutoEP+AutoTP folding metadata in folded checkpoint.")
folding = metadata[FOLDING_METADATA_KEY]
if folding.get("version") != FOLDING_METADATA_VERSION:
raise RuntimeError(f"Unsupported folding metadata version: {folding.get('version')}")
expected = {
FOLDING_TP_SIZE: tp_size,
FOLDING_EP_SIZE: ep_size,
FOLDING_ETP_SIZE: etp_size,
}
optional_expected = {
FOLDING_TP_RANK: tp_rank,
FOLDING_EP_RANK: ep_rank,
FOLDING_ETP_RANK: etp_rank,
FOLDING_ZERO_PARTITION_GROUP: zero_partition_group,
FOLDING_ZERO_PARTITION_RANK: zero_partition_rank,
FOLDING_ZERO_PARTITION_COUNT: zero_partition_count,
FOLDING_FAMILY: family,
FOLDING_PARAM_FAMILIES: param_families,
FOLDING_SHARED_EXPERT_PLACEMENT: shared_expert_placement,
FOLDING_DISPATCH_STRATEGY: dispatch_strategy,
}
expected.update({key: value for key, value in optional_expected.items() if value is not None})
for key, value in expected.items():
if folding.get(key) != value:
raise RuntimeError(f"Folding metadata mismatch for {key}: saved={folding.get(key)} runtime={value}")
return folding
def _state_entry(state, param_id):
"""Get optimizer state entry by param id, handling int/str key variants."""
if param_id in state:
return state[param_id]
pid_str = str(param_id)
if pid_str in state:
return state[pid_str]
if isinstance(param_id, str):
try:
pid_int = int(param_id)
except ValueError:
return None
return state.get(pid_int)
return None
def _ordered_param_ids(optim_sd):
"""Return optimizer param ids in param_groups order, deduplicated."""
ordered = []
seen = set()
for group in optim_sd.get('param_groups', []):
for param_id in group.get('params', []):
key = str(param_id)
if key in seen:
continue
seen.add(key)
ordered.append(param_id)
if ordered:
return ordered
# Fallback for unexpected optimizer formats.
state = optim_sd.get('state', {})
return list(state.keys())
def _param_name_to_id(optim_sd):
"""Build optional mapping from parameter name to optimizer param id."""
mapping = {}
for group in optim_sd.get('param_groups', []):
params = group.get('params', [])
param_names = group.get('param_names', None)
if not isinstance(param_names, list):
continue
if len(param_names) != len(params):
continue
for param_id, param_name in zip(params, param_names):
mapping[param_name] = param_id
return mapping
def _is_expert_optimizer_state(param_state, num_local):
for state_key in ('exp_avg', 'exp_avg_sq'):
tensor = param_state.get(state_key)
if tensor is None:
continue
if tensor.dim() == 3 and tensor.shape[0] == num_local:
return True
return False
def resolve_expert_ckpt_path(checkpoint_dir, moe_layer_id, global_expert_id):
"""Find the expert checkpoint file for a given (layer, expert) pair.
Resolves using glob pattern without assuming mp_rank=0.
Returns:
Path to the single matching expert checkpoint file.
Raises:
FileNotFoundError: No matching file found.
NotImplementedError: Multiple matching files found (multi-mp_rank).
"""
pattern = os.path.join(checkpoint_dir, f'layer_{moe_layer_id}_expert_{global_expert_id}_mp_rank_*_model_states.pt')
matches = glob.glob(pattern)
if len(matches) == 0:
raise FileNotFoundError(f"Expert checkpoint file not found: layer_{moe_layer_id} "
f"expert_{global_expert_id} in {checkpoint_dir}")
if len(matches) > 1:
for match in matches:
state = torch.load(match, map_location='cpu', weights_only=False)
if FOLDING_METADATA_KEY in state:
raise NotImplementedError("Universal checkpoint conversion for folded AutoEP+AutoTP expert shards "
"is not supported yet. Load this checkpoint with a matching folded "
"runtime, or consolidate the tensor-parallel expert shards before "
"running ds_to_universal.")
raise NotImplementedError(f"Multiple expert checkpoint files found for layer_{moe_layer_id} "
f"expert_{global_expert_id}: {matches}. Multi-mp_rank expert files "
f"are not yet supported.")
return matches[0]
def consolidate_autoep_expert_files(checkpoint_dir, output_dir, autoep_layers_metadata):
"""Consolidate per-expert checkpoint files into full-expert universal format.
For each AutoEP layer, loads all per-expert files, stacks into
[E_total, H, D] tensors, and saves in universal checkpoint format.
Args:
checkpoint_dir: Path to DeepSpeed checkpoint directory.
output_dir: Path to universal checkpoint output directory.
autoep_layers_metadata: AutoEP metadata list from main checkpoint.
Raises:
FileNotFoundError: If expected expert files are missing.
NotImplementedError: If multiple mp_rank files match one (layer, expert).
RuntimeError: If metadata is missing or malformed.
"""
if autoep_layers_metadata is None:
raise RuntimeError("AutoEP metadata is missing from checkpoint. Cannot consolidate "
"expert files without ds_autoep_layers metadata.")
if not isinstance(autoep_layers_metadata, list):
raise RuntimeError(f"AutoEP metadata is malformed: expected list, got "
f"{type(autoep_layers_metadata).__name__}")
for layer_info in autoep_layers_metadata:
moe_layer_id = layer_info['moe_layer_id']
num_experts = layer_info['num_experts']
prefix = layer_info['expert_key_prefix']
for wname in ('w1', 'w2', 'w3'):
expert_tensors = []
folding_metadata = None
for global_eid in range(num_experts):
ckpt_path = resolve_expert_ckpt_path(checkpoint_dir, moe_layer_id, global_eid)
sd = torch.load(ckpt_path, map_location='cpu', weights_only=False)
if folding_metadata is None:
folding_metadata = sd.get(FOLDING_METADATA_KEY)
key = f"{prefix}.{wname}.{global_eid}"
if key not in sd:
raise RuntimeError(f"Expected key '{key}' not found in {ckpt_path}")
expert_tensors.append(sd[key])
# Stack to full fused tensor [E_total, H, D]
full_tensor = torch.stack(expert_tensors, dim=0)
# Save in universal format
param_name = f"{prefix}.{wname}"
param_dir = os.path.join(output_dir, "zero", param_name)
os.makedirs(param_dir, exist_ok=True)
universal_state = {
PARAM: full_tensor,
CAT_DIM: 0,
EP_IS_EXPERT_PARAM: True,
EP_NUM_EXPERTS: num_experts,
}
if folding_metadata is not None:
universal_state[FOLDING_METADATA_KEY] = folding_metadata
torch.save(universal_state, os.path.join(param_dir, "fp32.pt"))
def consolidate_autoep_optimizer_states(checkpoint_dir, output_dir, autoep_layers_metadata, ep_size):
"""Consolidate expert optimizer states from expp_rank files into universal format.
Loads optimizer states from all expp_rank_*_optim_states.pt files,
extracts per-expert-parameter states (exp_avg, exp_avg_sq, etc.),
concatenates along the expert dimension (dim 0) to form full
[E_total, H, D] optimizer states, and saves alongside the model
parameter in universal format.
Args:
checkpoint_dir: Path to DeepSpeed checkpoint directory.
output_dir: Path to universal checkpoint output directory.
autoep_layers_metadata: AutoEP metadata list from main checkpoint.
ep_size: Expert parallel world size (number of expp_rank files to load).
Raises:
FileNotFoundError: If expected optimizer state files are missing.
RuntimeError: If expert parameter states cannot be extracted.
"""
if autoep_layers_metadata is None:
raise RuntimeError("AutoEP metadata is missing. Cannot consolidate optimizer states.")
# Load all expp_rank optimizer states
optim_states = []
for rank in range(ep_size):
pattern = os.path.join(checkpoint_dir, f'expp_rank_{rank}_mp_rank_*_optim_states.pt')
matches = glob.glob(pattern)
if not matches:
# No optimizer state files (e.g., ZeRO handles optimizer differently)
return
optim_path = matches[0]
sd = torch.load(optim_path, map_location='cpu', weights_only=False)
optim_states.append(sd)
if not optim_states:
return
# Extract optimizer state dict
optim_sd = optim_states[0].get('optimizer')
if optim_sd is None:
return
state = optim_sd.get('state', {})
if not state:
return
ordered_param_ids = _ordered_param_ids(optim_sd)
name_to_param_id = _param_name_to_id(optim_sd)
consumed_param_ids = set()
# For each AutoEP layer, extract and consolidate optimizer states
for layer_info in autoep_layers_metadata:
prefix = layer_info['expert_key_prefix']
num_experts = layer_info['num_experts']
num_local = layer_info['num_local_experts']
layer_param_ids = {}
# If optimizer state carries param names, map weights by exact identity.
for wname in ('w1', 'w2', 'w3'):
param_name = f"{prefix}.{wname}"
param_id = name_to_param_id.get(param_name)
if param_id is None:
continue
layer_param_ids[wname] = param_id
consumed_param_ids.add(str(param_id))
# Fallback: consume expert-like params in optimizer param_groups order.
missing_wnames = [w for w in ('w1', 'w2', 'w3') if w not in layer_param_ids]
if missing_wnames:
candidates = []
for param_id in ordered_param_ids:
if str(param_id) in consumed_param_ids:
continue
param_state = _state_entry(state, param_id)
if param_state is None:
continue
if not _is_expert_optimizer_state(param_state, num_local):
continue
candidates.append(param_id)
for wname, param_id in zip(missing_wnames, candidates):
layer_param_ids[wname] = param_id
consumed_param_ids.add(str(param_id))
for wname in ('w1', 'w2', 'w3'):
param_name = f"{prefix}.{wname}"
param_dir = os.path.join(output_dir, "zero", param_name)
os.makedirs(param_dir, exist_ok=True)
param_id = layer_param_ids.get(wname)
if param_id is None:
continue
# Consolidate optimizer states for this specific expert parameter id.
for state_key in ('exp_avg', 'exp_avg_sq'):
rank_tensors = []
for rank in range(ep_size):
rank_optim_sd = optim_states[rank].get('optimizer', {})
rank_state = rank_optim_sd.get('state', {})
param_state = _state_entry(rank_state, param_id)
if param_state is None:
rank_tensors = []
break
tensor = param_state.get(state_key)
if tensor is None:
rank_tensors = []
break
if tensor.dim() != 3 or tensor.shape[0] != num_local:
rank_tensors = []
break
rank_tensors.append(tensor)
if len(rank_tensors) == ep_size:
full_tensor = torch.cat(rank_tensors, dim=0)
torch.save(
{
PARAM: full_tensor,
CAT_DIM: 0,
EP_IS_EXPERT_PARAM: True,
EP_NUM_EXPERTS: num_experts,
}, os.path.join(param_dir, f"{state_key}.pt"))