Files
paddlepaddle--paddle/python/paddle/distributed/flex_checkpoint/dcp/utils.py
T
2026-07-13 12:40:42 +08:00

756 lines
24 KiB
Python

# Copyright (c) 2023 PaddlePaddle Authors. All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import annotations
import ast
import copy
import os
import re
from collections import defaultdict
from dataclasses import replace
from typing import TYPE_CHECKING
import numpy as np
from safetensors.numpy import safe_open
import paddle
from paddle.distributed.fleet.utils.log_util import logger
from ..aoa.aoa_engine import (
postprocess_transpose,
)
from .metadata import (
LocalTensorIndex,
LocalTensorMetadata,
Metadata,
)
from .sharded_weight import (
ShardedWeight,
ShardedWeightDesc,
)
if TYPE_CHECKING:
from paddle.framework import core
def get_coordinator(mesh: np.array | list[list[int]], rank: int):
mesh = paddle.to_tensor(mesh)
rand_coordinator = (mesh == rank).nonzero()
assert rand_coordinator.shape[0] in (
0,
1,
), f"rand_coordinator.shape: {rand_coordinator.shape}"
return (
rand_coordinator[0].tolist() if rand_coordinator.shape[0] > 0 else None
)
# NOTE(zhangbo): Refer to the BalancedSplit function in the reshard_utils.cc file.
def balanced_split(total_nums, num_of_pieces):
has_remainder = total_nums % num_of_pieces != 0
result = [(total_nums + num_of_pieces - 1) // num_of_pieces] * num_of_pieces
if has_remainder:
last_value = result[-1]
result[-1] = last_value - (last_value * num_of_pieces - total_nums)
return result
def compute_local_shape_and_global_offset(
global_shape: list[int],
process_mesh: core.ProcessMesh,
placements: list[core.Placement],
) -> tuple[tuple[int], tuple[int]]:
from paddle.distributed.auto_parallel.placement_type import (
placemetns_to_dist_status,
)
mesh = np.array(process_mesh.process_ids).reshape(process_mesh.shape)
# deal with cross mesh case
if paddle.distributed.get_rank() not in mesh:
return (None, None)
rank_coordinator = get_coordinator(mesh, paddle.distributed.get_rank())
local_shape = copy.copy(global_shape)
global_offset = [0 for _ in global_shape]
dims_mapping, _ = placemetns_to_dist_status(placements, len(global_shape))
for tensor_dim, mesh_dims in enumerate(dims_mapping):
if len(mesh_dims) == 0:
continue
local_offset = [0] * len(global_shape)
for mesh_dim in mesh_dims:
chunk_idx = rank_coordinator[mesh_dim]
chunks = balanced_split(
local_shape[tensor_dim], process_mesh.shape[mesh_dim]
)
local_shape[tensor_dim] = chunks[chunk_idx]
local_offset[tensor_dim] = sum(chunks[:chunk_idx])
if global_offset[tensor_dim] <= local_offset[tensor_dim]:
global_offset[tensor_dim] = local_offset[tensor_dim]
else:
global_offset[tensor_dim] += local_offset[tensor_dim]
return tuple(local_shape), tuple(global_offset)
def flatten_state_dict(state_dict):
"""
Flatten the nested dict to a flat dict.
{"model": {"w0": xxx}} -> {model.w0: xxx}
"""
flatten_state_dict = {}
mapping = {}
def _flatten(key, value):
nonlocal _flatten
if isinstance(value, dict):
for k, v in value.items():
assert isinstance(k, str), f"The key should be str, but is {k}"
_flatten((*key, k), v)
elif isinstance(value, (paddle.Tensor, ShardedWeight)):
flatten_key_str = ".".join(key)
flatten_state_dict[flatten_key_str] = value
mapping[flatten_key_str] = key
else:
raise ValueError(
f"The value should be dict or paddle.Tensor, but is {value}"
)
_flatten((), state_dict)
del _flatten # force python gc of recursive closure
return flatten_state_dict, mapping
def unflatten_state_dict(flat_state_dict, mapping):
"""
Unflatten the flat dict to a nested dict.
{model.w0: xxx} -> {"model": {"w0": xxx}}
"""
state_dict = {}
for key, value in flat_state_dict.items():
key_tuple = mapping[key]
assert isinstance(key_tuple, tuple), (
f"The key should be tuple, but is {key_tuple}"
)
tmp = state_dict
for i in range(len(key_tuple) - 1):
key = key_tuple[i]
tmp = tmp.setdefault(key, {})
tmp[key_tuple[-1]] = value
return state_dict
def get_max_id(path):
numbers = [0]
pattern = re.compile(r"^(\d+)_(\d+)\.distcp$")
files = os.listdir(path)
for file in files:
match = pattern.match(file)
if match:
numbers.append(int(match.group(2)))
return max(numbers) if numbers else None
def check_unique_id(unique_id, process_group):
all_unique_id = []
paddle.distributed.all_gather_object(
all_unique_id, unique_id, process_group
)
for id in all_unique_id[1:]:
assert id == all_unique_id[0], f"id:{id} != all_unique_id[0]"
def ravel_index(indices, shape):
idx = 0
for i, dim in zip(indices, shape):
idx = idx * dim + i
return idx
def unravel_index(idx, shape):
indices = []
for dim in reversed(shape):
indices.append(idx % dim)
idx //= dim
return tuple(reversed(indices))
def minimal_nd_slice(shape, flat_start, flat_end):
start_idx = unravel_index(flat_start, shape)
end_idx = unravel_index(flat_end - 1, shape)
min_slices = []
for axis in range(len(shape)):
if axis == 0:
s = start_idx[axis]
e = end_idx[axis] + 1
else:
if start_idx[axis - 1] == end_idx[axis - 1]:
s = min(start_idx[axis], end_idx[axis])
e = max(start_idx[axis], end_idx[axis]) + 1
else:
s = 0
e = shape[axis]
min_slices.append((s, e))
return min_slices, start_idx, end_idx
def flat_range_in_min_slice(shape, min_slices, flat_start, flat_end):
min_starts = tuple(s[0] for s in min_slices)
min_flat_start = ravel_index(min_starts, shape)
return flat_start - min_flat_start, flat_end - min_flat_start
def is_sharded_state_dict(state_dict, use_dist=True, process_group=None):
values = list(state_dict.values())
is_all_sharded = all(isinstance(v, ShardedWeight) for v in values)
has_sharded = any(isinstance(v, ShardedWeight) for v in values)
if has_sharded and not is_all_sharded:
raise TypeError(
"All values must be ShardedWeight if any value is ShardedWeight."
)
if not use_dist:
return is_all_sharded
if is_all_sharded:
flag = 1
elif len(values) == 0:
flag = 0
else:
flag = -1
all_flags = []
paddle.distributed.all_gather_object(all_flags, flag, process_group)
assert all(f >= 0 for f in all_flags) or all(f <= 0 for f in all_flags), (
"Not support mixed type of ShardedWeight and non-ShardedWeight in the same state_dict!"
)
return all(f >= 0 for f in all_flags)
def get_overlap_region(desc_offset, desc_shape, shard_offset, shard_shape):
ndim = len(desc_offset)
overlap_offset = []
overlap_shape = []
desc_starts = []
shard_starts = []
for i in range(ndim):
desc_lo = desc_offset[i]
desc_hi = desc_offset[i] + desc_shape[i]
shard_lo = shard_offset[i]
shard_hi = shard_offset[i] + shard_shape[i]
# overlap
lo = max(desc_lo, shard_lo)
hi = min(desc_hi, shard_hi)
if lo >= hi:
return False, None, None, None, None
overlap_offset.append(lo)
overlap_shape.append(hi - lo)
desc_starts.append(lo - desc_lo)
shard_starts.append(lo - shard_lo)
return True, overlap_offset, overlap_shape, desc_starts, shard_starts
def assign_sharded_slice(
src_desc, src_shard, dst_desc, dst_shard, postprocess_list=None
):
src_has, _, overlap_shape, src_desc_starts, src_shard_starts = (
get_overlap_region(
src_desc.global_offset,
src_desc.local_shape,
src_shard.global_offset,
src_shard.local_shape,
)
)
dst_has, _, overlap_shape2, dst_desc_starts, dst_shard_starts = (
get_overlap_region(
dst_desc.global_offset,
dst_desc.local_shape,
dst_shard.global_offset,
dst_shard.local_shape,
)
)
assert src_has or dst_has, "no overlap!"
if overlap_shape != overlap_shape2:
assert postprocess_list is not None, (
"only post transpose operation could make overlap shape mismatch"
)
transposed_src_overlap_shape = postprocess_transpose(
overlap_shape, postprocess_list
)
assert transposed_src_overlap_shape == overlap_shape2, (
f"overlap shape mismatch: {transposed_src_overlap_shape} vs {overlap_shape2}"
)
axes = list(range(len(transposed_src_overlap_shape)))
src_tensor_slice = paddle.slice(
src_shard.local_tensor,
axes=axes,
starts=src_shard_starts,
ends=[s + o for s, o in zip(src_shard_starts, overlap_shape)],
)
dst_tensor_slice = paddle.slice(
dst_shard.local_tensor,
axes=axes,
starts=dst_shard_starts,
ends=[s + o for s, o in zip(dst_shard_starts, overlap_shape2)],
)
else:
axes = list(range(len(overlap_shape)))
src_tensor_slice = paddle.slice(
src_shard.local_tensor,
axes=axes,
starts=src_shard_starts,
ends=[s + o for s, o in zip(src_shard_starts, overlap_shape)],
)
dst_tensor_slice = paddle.slice(
dst_shard.local_tensor,
axes=axes,
starts=dst_shard_starts,
ends=[s + o for s, o in zip(dst_shard_starts, overlap_shape)],
)
if postprocess_list is not None:
for ps in postprocess_list:
is_list, result = is_list_string(ps)
if is_list:
src_tensor_slice = paddle.transpose(src_tensor_slice, result)
else:
if isinstance(ps, str):
src_tensor_slice = paddle.cast(src_tensor_slice, ps)
paddle.assign(src_tensor_slice, dst_tensor_slice)
def merge_shard_info_list(list_of_dicts):
merged = defaultdict(list)
for info in list_of_dicts:
for k, v in info.items():
merged[k].extend(v)
return dict(merged)
def build_shard_desc(val):
return ShardedWeightDesc(
key=val.key,
local_shape=tuple(val.local_shape),
global_shape=tuple(val.global_shape),
global_offset=tuple(val.global_offset),
dtype=str(val.local_tensor.dtype).split(".")[-1],
)
def is_list_string(s):
try:
result = ast.literal_eval(s)
return (True, result) if isinstance(result, list) else (False, None)
except:
return False, None
def write_to_file_if_empty(data, path):
lock_path = f"{path}.lock"
try:
fd = os.open(lock_path, os.O_CREAT | os.O_EXCL | os.O_WRONLY)
os.close(fd)
try:
if os.path.exists(path) and os.path.getsize(path) > 0:
logger.info(
f"Process {os.getpid()} found the metadata file already written."
)
return
paddle.save(data, path)
logger.info(
f"Process {os.getpid()} successfully wrote the metadata to the file."
)
finally:
if os.path.exists(lock_path):
os.remove(lock_path)
except FileExistsError:
logger.info(
f"Process {os.getpid()} could not acquire the lock; another process is writing or has written the metadata."
)
def build_global_state_shard_info(sharded_state_dict, process_group):
state_shard_info = defaultdict(list)
for key, val in sharded_state_dict.items():
desc = build_shard_desc(val)
state_shard_info[key].append(desc)
gathered_info = []
use_dist = True if paddle.distributed.get_world_size() > 1 else False
if use_dist:
paddle.distributed.all_gather_object(
gathered_info, dict(state_shard_info), process_group
)
else:
gathered_info = [dict(state_shard_info)]
return merge_shard_info_list(gathered_info)
def merge_state_dict_metadata(global_state_dict_metadata):
assert isinstance(global_state_dict_metadata, list), (
"The global_state_dict should be a list."
)
out = {}
for state_dict in global_state_dict_metadata:
for key, val in state_dict.items():
if key not in out:
out[key] = []
if isinstance(val, list):
for item in val:
if item not in out[key]:
out[key].append(item)
else:
if val not in out[key]:
out[key].append(val)
return out
def recover_shard_tensor_from_shards(sharded_weights: list, sw):
def _assign_slice(dst_tensor, dst_starts, dst_ends, src_tensor):
axes = list(range(len(dst_starts)))
view = paddle.slice(
dst_tensor, axes=axes, starts=dst_starts, ends=dst_ends
)
paddle.assign(src_tensor, output=view)
return dst_tensor
dims = len(sw.global_offset)
sw_glo_start = sw.global_offset
sw_glo_end = [sw.global_offset[i] + sw.local_shape[i] for i in range(dims)]
sw_shape = sw.local_shape
for s in sharded_weights:
s_glo_start = s.global_offset
s_glo_end = [s.global_offset[i] + s.local_shape[i] for i in range(dims)]
overlap = []
for i in range(dims):
ol_start = max(s_glo_start[i], sw_glo_start[i])
ol_end = min(s_glo_end[i], sw_glo_end[i])
if ol_start >= ol_end:
break
overlap.append((ol_start, ol_end))
else:
s_starts = [ol[0] - s_glo_start[i] for i, ol in enumerate(overlap)]
s_ends = [ol[1] - s_glo_start[i] for i, ol in enumerate(overlap)]
sw_starts = [
ol[0] - sw_glo_start[i] for i, ol in enumerate(overlap)
]
sw_ends = [ol[1] - sw_glo_start[i] for i, ol in enumerate(overlap)]
axes = list(range(len(s_starts)))
src = paddle.slice(
s.local_tensor, axes=axes, starts=s_starts, ends=s_ends
)
_assign_slice(sw.local_tensor, sw_starts, sw_ends, src)
return sw
def create_hf_ckpt_metadata(
ckpt_path: str,
process_group=None,
):
dtype_mapping = {
'U16': 'bfloat16',
'U8': 'uint8',
'I8': 'int8',
'I16': 'int16',
'BOOL': 'bool',
'F16': 'float16',
'F32': 'float32',
'F64': 'float64',
'BF16': 'bfloat16',
'I64': 'int64',
}
use_dist = paddle.distributed.get_world_size() > 1
cur_rank = paddle.distributed.get_rank() if use_dist else 0
accessible_files = os.listdir(ckpt_path)
safetensors_files = [
file for file in accessible_files if file.endswith(".safetensors")
]
if use_dist:
rank_visible_files = []
local_files = {cur_rank: safetensors_files}
paddle.distributed.all_gather_object(
rank_visible_files, local_files, process_group
)
rank_visible_files = {
rank: files for d in rank_visible_files for rank, files in d.items()
}
else:
rank_visible_files = {0: safetensors_files}
def assign_files(
rank_visible_files: dict[int, list[str]],
) -> dict[int, list[str]]:
all_files = set()
for files in rank_visible_files.values():
all_files.update(files)
all_files = list(all_files)
file2ranks = defaultdict(list)
for rank, files in rank_visible_files.items():
for f in files:
file2ranks[f].append(rank)
result = defaultdict(list)
all_files.sort(key=lambda f: (len(file2ranks[f]), f))
rank_load = dict.fromkeys(rank_visible_files, 0)
for f in all_files:
candidates = file2ranks[f]
min_rank = min(candidates, key=lambda r: (rank_load[r], r))
result[min_rank].append(f)
rank_load[min_rank] += 1
return {rank: result.get(rank, []) for rank in rank_visible_files}
rank2file = assign_files(rank_visible_files)
need_handle_files = rank2file[cur_rank]
local_state_dict_metadata = defaultdict(set)
local_storage_metadata = {}
for file_name in need_handle_files:
file_path = os.path.join(ckpt_path, file_name)
with safe_open(file_path, framework="np") as f:
for key in f.keys():
t_s = f.get_slice(key)
shape = tuple(t_s.get_shape())
dtype = t_s.get_dtype()
assert dtype in dtype_mapping, f"{dtype} is not supported yet."
dtype = dtype_mapping[dtype]
ltm = LocalTensorMetadata(
global_offset=(0,) * len(shape),
local_shape=shape,
dtype=dtype,
global_shape=shape,
is_flattened=False,
)
lti = LocalTensorIndex(
tensor_key=key,
global_offset=(0,) * len(shape),
is_flattened=False,
local_shape=shape,
)
local_state_dict_metadata[key].add(ltm)
local_storage_metadata[lti] = file_name
if use_dist:
global_state_dict_metadata = []
global_storage_metadata = []
paddle.distributed.all_gather_object(
global_state_dict_metadata,
dict(local_state_dict_metadata),
process_group,
)
paddle.distributed.all_gather_object(
global_storage_metadata, local_storage_metadata, process_group
)
else:
global_state_dict_metadata = [dict(local_state_dict_metadata)]
global_storage_metadata = [local_storage_metadata]
state_dict_metadata = defaultdict(set)
for md in global_state_dict_metadata:
for k, v in md.items():
state_dict_metadata[k].update(v)
state_dict_metadata = {k: list(v) for k, v in state_dict_metadata.items()}
storage_metadata = {}
for md in global_storage_metadata:
storage_metadata.update(md)
metadata = Metadata(
state_dict_metadata=state_dict_metadata,
storage_metadata=storage_metadata,
)
METADATA_FILE_NAME = "flex-ckpt.auto_generated.metadata"
write_to_file_if_empty(
metadata, os.path.join(ckpt_path, METADATA_FILE_NAME)
)
if use_dist:
paddle.distributed.barrier(process_group)
def get_target_tensor(target_state_dict, read_item):
use_dist = paddle.distributed.get_world_size() > 1
if any(isinstance(k, tuple) for k in target_state_dict):
key = (read_item.tensor_name, read_item.dst_global_offset)
else:
key = read_item.tensor_name
tensor = target_state_dict[key]
return tensor._local_value() if use_dist and tensor.is_dist() else tensor
def slice_tensor(tensor, slice_begin, slice_shape):
if not slice_shape:
assert not tensor.shape, (
"Only 0-dimensional tensor supports empty slice_shape."
)
return tensor
slice_end = [
start + length for start, length in zip(slice_begin, slice_shape)
]
axes = list(range(tensor.ndim))
return paddle.slice(tensor, axes=axes, starts=slice_begin, ends=slice_end)
def extract_tensor_metadata(val):
if isinstance(val, paddle.Tensor):
# Case1: not initialized means this tensor is placed in another mesh which do not contain this rank
if not val._is_initialized():
return None, None
if val.is_dist():
local_tensor = val._local_value()
# Note: The local_tensor must keep the same name with the original tensor. Otherwise, the StructuredToParameterName@@ mapping will be wrong.
local_tensor.name = val.name
# when val is scalar, the shape is []
(
local_shape,
global_offset,
) = (
compute_local_shape_and_global_offset(
val.shape,
val.process_mesh,
val.placements,
)
if len(val.shape) > 0
else ((), ())
)
global_shape = val.shape
if local_shape is None or global_offset is None:
return None, None
else:
local_shape = tuple(val.shape)
global_offset = (
tuple([0] * len(val.shape)) if len(val.shape) > 0 else ()
)
global_shape = local_shape
local_tensor = val
is_flattened = False
flattened_range = None
elif isinstance(val, ShardedWeight):
local_tensor = val.local_tensor
local_shape = val.local_shape
global_offset = val.global_offset
global_shape = val.global_shape
is_flattened = val.is_flattened
flattened_range = val.flattened_range
else:
raise ValueError(
f"The value of state_dict should be a paddle.Tensor, but got: {val}"
)
local_tensor_dtype = str(local_tensor.dtype).split('.')[1]
if flattened_range is not None:
flattened_range = (flattened_range.start, flattened_range.stop)
else:
flattened_range = None
local_tensor_metadata = LocalTensorMetadata(
tuple(global_offset),
tuple(local_shape),
local_tensor_dtype,
tuple(global_shape),
is_flattened,
flattened_range,
)
assert (local_tensor is None) == (local_tensor_metadata is None), (
"local_tensor and local_tensor_metadata must both be None or both not None!"
)
return local_tensor, local_tensor_metadata
def check_resumable_locally(
path, state_dict, metadata_manager, use_dist, process_group
):
local_load = True
rank = paddle.distributed.get_rank() if use_dist else 0
checkpoint_file = f"{rank}_0.distcp"
file_path = os.path.join(path, checkpoint_file)
if not os.path.isfile(file_path):
local_load = False
state_dict_metadata = {}
for key, value in state_dict.items():
_, local_tensor_metadata = extract_tensor_metadata(value)
if local_tensor_metadata is not None:
state_dict_metadata[key] = local_tensor_metadata
if local_load:
file_storage_info = metadata_manager.get_file_storage_info()
cur_file_storage = {
replace(index, replica_id=None)
for index in file_storage_info.get(checkpoint_file, [])
}
for key, local_tensor_metadata in state_dict_metadata.items():
local_tensor_index = LocalTensorIndex(
tensor_key=key,
global_offset=local_tensor_metadata.global_offset,
is_flattened=local_tensor_metadata.is_flattened,
flattened_range=local_tensor_metadata.flattened_range,
local_shape=local_tensor_metadata.local_shape,
replica_id=None,
)
if local_tensor_index not in cur_file_storage:
local_load = False
break
if use_dist:
global_local_loads = []
paddle.distributed.all_gather_object(
global_local_loads, local_load, process_group
)
return all(global_local_loads)
else:
return local_load
def need_transpose(postprocess_list):
if postprocess_list is None:
return False
for pp in postprocess_list:
if "[" in pp:
return True
else:
return False