# 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