327 lines
10 KiB
Python
327 lines
10 KiB
Python
# Copyright (c) 2025 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 logging
|
|
from typing import Any
|
|
|
|
import paddle
|
|
import paddle.distributed as dist
|
|
from paddle.distributed import Replicate, Shard
|
|
from paddle.distributed.auto_parallel.api import (
|
|
dtensor_from_local,
|
|
dtensor_to_local,
|
|
)
|
|
from paddle.utils import flatten, map_structure, pack_sequence_as
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Default chunking dimension is 0. This is used for the case where the user did
|
|
# not specify a chunking dimension.
|
|
DEFAULT_CHUNK_DIM = 0
|
|
|
|
|
|
def _split_tensor(x, num_chunks, split_axis=0):
|
|
if not x.is_dist():
|
|
chunk_tensors = paddle.tensor_split(x, num_chunks, split_axis)
|
|
# dp_degree > 1 , placements of model input is [S(0), R, ...]
|
|
else:
|
|
if dist.in_auto_parallel_align_mode():
|
|
|
|
def _reorder_data_for_align():
|
|
nonlocal x
|
|
assert x.placements[0] == dist.Shard(0), (
|
|
"inputs should be placed on S(0)."
|
|
)
|
|
|
|
shardings = x.process_mesh.shape[0]
|
|
|
|
rows_per_shard = x.shape[0] // shardings
|
|
new_indices = []
|
|
for s_id in range(shardings):
|
|
for row_in_shard in range(rows_per_shard):
|
|
new_indices.append(s_id + row_in_shard * shardings)
|
|
tmp = x[new_indices]
|
|
x = dist.reshard(tmp, x.process_mesh, x.placements)
|
|
|
|
_reorder_data_for_align()
|
|
mesh = x.process_mesh
|
|
placements = x.placements
|
|
dense_x = dtensor_to_local(x, mesh, placements)
|
|
chunk_tensors = paddle.tensor_split(dense_x, num_chunks, split_axis)
|
|
for i in range(num_chunks):
|
|
chunk_tensors[i] = dtensor_from_local(
|
|
chunk_tensors[i], mesh, placements
|
|
)
|
|
return chunk_tensors
|
|
|
|
|
|
def _concat_tensor(chunk_tensors, axis=0):
|
|
chunk0 = chunk_tensors[0]
|
|
if not chunk0.is_dist():
|
|
out = paddle.concat(chunk_tensors, axis)
|
|
|
|
else:
|
|
# loss_fun(out, labels), placements of labels is [S(0), R, ...]
|
|
mesh = chunk0.process_mesh
|
|
placements = [Replicate() for _ in range(mesh.ndim)]
|
|
dp_index = mesh.dim_names.index("dp") if "dp" in mesh.dim_names else 0
|
|
placements[dp_index] = Shard(0)
|
|
|
|
for i in range(len(chunk_tensors)):
|
|
chunk_tensors[i] = dist.reshard(chunk_tensors[i], mesh, placements)
|
|
|
|
chunk_tensors[i] = dtensor_to_local(
|
|
chunk_tensors[i], mesh, placements
|
|
)
|
|
out = paddle.concat(chunk_tensors, axis)
|
|
out = dtensor_from_local(out, mesh, placements)
|
|
return out
|
|
|
|
|
|
class TensorChunkSpec:
|
|
"""
|
|
Class used to specify chunking of inputs
|
|
"""
|
|
|
|
def __init__(self, split_axis):
|
|
self.split_axis = split_axis
|
|
|
|
split_axis: int
|
|
|
|
def __repr__(self):
|
|
return f"{self.__class__.__module__}.{self.__class__.__name__}({self.split_axis})"
|
|
|
|
def __str__(self):
|
|
return f"TensorChunkSpec({self.split_axis})"
|
|
|
|
|
|
def _split_args_helper(
|
|
args_dict,
|
|
args_chunk_spec,
|
|
num_chunks,
|
|
):
|
|
"""
|
|
A helper function of split_args_kwargs_into_chunks.
|
|
"""
|
|
assert len(args_dict) == len(args_chunk_spec), (
|
|
f"args_dict.keys() = {list(args_dict.keys())} args_chunk_spec.keys() = {list(args_chunk_spec.keys())}"
|
|
)
|
|
|
|
shared_args_dict_flat = {}
|
|
# handle args one by one
|
|
for arg_key, arg in args_dict.items():
|
|
arg_flat = flatten(arg)
|
|
|
|
chunk_spec = args_chunk_spec[arg_key]
|
|
assert chunk_spec is not None
|
|
|
|
chunk_spec_flat = flatten(chunk_spec)
|
|
assert len(chunk_spec_flat) == len(arg_flat), (
|
|
f"{arg_key} {len(arg_flat)} != {len(chunk_spec_flat)}"
|
|
)
|
|
|
|
shard_arg_flat = []
|
|
|
|
for v, chunk_v in zip(arg_flat, chunk_spec_flat):
|
|
if not isinstance(v, paddle.Tensor):
|
|
shard_arg_flat.append([v] * num_chunks)
|
|
elif isinstance(chunk_v, TensorChunkSpec):
|
|
v_split_axis_size = v.shape[chunk_v.split_axis]
|
|
|
|
if v_split_axis_size < num_chunks:
|
|
raise ValueError(
|
|
f"Arg {arg_key} on chunking dimension has a size of {v_split_axis_size}, "
|
|
f"smaller than the number of chunks {num_chunks}. "
|
|
"Please adjust your num_chunks setting."
|
|
)
|
|
# split tensor v
|
|
chunk_tensors = _split_tensor(v, num_chunks, chunk_v.split_axis)
|
|
|
|
shard_arg_flat.append(chunk_tensors)
|
|
else:
|
|
raise TypeError(f"Unrecognized chunk spec: {chunk_v}")
|
|
|
|
shared_args_dict_flat[arg_key] = shard_arg_flat
|
|
|
|
# the structure of each element in args_split is the same as the original args_dict
|
|
args_split = []
|
|
for idx in range(num_chunks):
|
|
chunk_args = {}
|
|
for key, arg in shared_args_dict_flat.items():
|
|
last_arg = None if not arg else arg[0][idx]
|
|
arg_of_curr_chunk = (
|
|
[v[idx] for v in arg] if len(arg) > 1 else last_arg
|
|
)
|
|
chunk_args[key] = arg_of_curr_chunk
|
|
|
|
# flatten chunk_args first, and then pack chunk_args as the origin args_dict
|
|
flatten_chunk_args = [x for x in flatten(chunk_args) if x is not None]
|
|
chunk_args = pack_sequence_as(args_dict, flatten_chunk_args)
|
|
args_split.append(chunk_args)
|
|
return args_split
|
|
|
|
|
|
def split_args_kwargs_into_chunks(
|
|
args: tuple[Any, ...],
|
|
kwargs: dict[str, Any] | None,
|
|
chunks: int,
|
|
args_chunk_spec: (
|
|
tuple[
|
|
tuple[TensorChunkSpec, ...]
|
|
| list[TensorChunkSpec, ...]
|
|
| TensorChunkSpec,
|
|
...,
|
|
]
|
|
| None
|
|
) = None,
|
|
kwargs_chunk_spec: (
|
|
dict[
|
|
str,
|
|
tuple[TensorChunkSpec, ...]
|
|
| list[TensorChunkSpec, ...]
|
|
| TensorChunkSpec,
|
|
]
|
|
| None
|
|
) = None,
|
|
) -> tuple[list[tuple], list[dict]]:
|
|
"""
|
|
Given a sequence of args and kwargs, split them into a number of chunks
|
|
according to their respective chunking specs.
|
|
|
|
Args:
|
|
args: tuple of args
|
|
kwargs: dict of kwargs
|
|
chunks: Number of chunks to split the args and kwargs into
|
|
args_chunk_spec: chunking specs for args, in same shape as args
|
|
kwargs_chunk_spec: chunking specs for kwargs, in same shape as kwargs
|
|
|
|
Returns:
|
|
args_split: list of sharded args
|
|
kwargs_split: list of sharded kwargs
|
|
"""
|
|
|
|
if kwargs is None:
|
|
kwargs = {}
|
|
|
|
if args_chunk_spec is None:
|
|
args_chunk_spec = map_structure(
|
|
lambda _: TensorChunkSpec(DEFAULT_CHUNK_DIM), args
|
|
)
|
|
|
|
if kwargs_chunk_spec is None:
|
|
kwargs_chunk_spec = map_structure(
|
|
lambda _: TensorChunkSpec(DEFAULT_CHUNK_DIM), kwargs
|
|
)
|
|
|
|
args_split_dict = _split_args_helper(
|
|
dict(enumerate(args)),
|
|
dict(enumerate(args_chunk_spec)),
|
|
chunks,
|
|
)
|
|
kwargs_split = _split_args_helper(
|
|
kwargs,
|
|
kwargs_chunk_spec,
|
|
chunks,
|
|
)
|
|
|
|
assert len(args_split_dict) == len(kwargs_split), (
|
|
"args and kwargs are split into difference number of chunks: "
|
|
f"{len(args_split_dict)}, {len(kwargs_split)}"
|
|
)
|
|
|
|
# the form of each args_chunk should be tuple
|
|
args_split = [
|
|
tuple(args_chunk[i] for i in range(len(args_chunk)))
|
|
for args_chunk in args_split_dict
|
|
]
|
|
|
|
return args_split, kwargs_split
|
|
|
|
|
|
def merge_chunks(
|
|
chunks: list[Any],
|
|
chunk_spec,
|
|
):
|
|
"""
|
|
Given a list of chunks, merge them into a single chunk according to
|
|
the chunk spec.
|
|
|
|
Args:
|
|
chunks: list of chunks
|
|
chunk_spec: Chunking spec for the chunks
|
|
|
|
Returns:
|
|
chunk: chunks merged value
|
|
"""
|
|
if len(chunks) == 0:
|
|
logger.warning("No chunks to merge.")
|
|
return chunks
|
|
|
|
if chunk_spec is None:
|
|
chunk_spec = map_structure(
|
|
lambda _: TensorChunkSpec(DEFAULT_CHUNK_DIM), chunks[0]
|
|
)
|
|
|
|
chunks_flat = []
|
|
# flatten chunk_spec first
|
|
chunk_spec = flatten(chunk_spec)
|
|
for chunk in chunks:
|
|
chunk_flat = flatten(chunk)
|
|
assert len(chunk_flat) == len(chunk_spec), (
|
|
f"Chunk {chunk} did not match chunk spec {chunk_spec}"
|
|
)
|
|
chunks_flat.append(chunk_flat)
|
|
|
|
def _merge_non_tensor_type_arg(chunks, idx, chunk_spec_of_arg=None):
|
|
# use the first chunk's value as the merged result
|
|
arg_0 = chunks[0][idx]
|
|
for chunk_idx in range(1, len(chunks)):
|
|
assert chunks[chunk_idx][idx] == arg_0, (
|
|
f"Cannot merge chunks with index 0 and {idx} with different values,"
|
|
f"When the arg's TensorChunkSpec is {chunk_spec_of_arg}"
|
|
)
|
|
return arg_0
|
|
|
|
args_flat = []
|
|
for arg_idx, chunk_spec_of_arg in enumerate(chunk_spec):
|
|
if isinstance(chunk_spec_of_arg, TensorChunkSpec):
|
|
if isinstance(chunks_flat[0][arg_idx], paddle.Tensor):
|
|
arg_chunks_to_merge = [
|
|
chunks_flat[chunk_idx][arg_idx]
|
|
for chunk_idx in range(len(chunks_flat))
|
|
]
|
|
merged_arg = _concat_tensor(
|
|
arg_chunks_to_merge, axis=chunk_spec_of_arg.split_axis
|
|
)
|
|
else:
|
|
logger.warning(
|
|
f"Cannot merge chunks with TensorChunkSpec {chunk_spec_of_arg}."
|
|
"The TensorChunkSpec only supports paddle.Tensor type."
|
|
)
|
|
|
|
merged_arg = _merge_non_tensor_type_arg(
|
|
chunks_flat, arg_idx, chunk_spec_of_arg
|
|
)
|
|
else:
|
|
merged_arg = _merge_non_tensor_type_arg(
|
|
chunks_flat, arg_idx, chunk_spec_of_arg
|
|
)
|
|
|
|
args_flat.append(merged_arg)
|
|
|
|
# pack args_flat as the input chunks[0]
|
|
return pack_sequence_as(chunks[0], args_flat)
|