Files
2026-07-13 12:40:42 +08:00

295 lines
9.9 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.
# strategy_conversion_engine.py
import argparse
import hashlib
import paddle
import paddle.distributed as dist
from paddle import nn
from paddle.distributed import fleet
from paddle.distributed.fleet.layers.mpu import (
ColumnParallelLinear,
RowParallelLinear,
)
# ==============================================================================
# 1. Model Definitions
# A model zoo with simple models supporting different parallelism strategies.
# ==============================================================================
class MLPBlock(nn.Layer):
"""
A basic building block compatible with Tensor Parallelism,
mimicking a transformer's FFN layer.
"""
def __init__(self, hidden_size=32):
super().__init__()
self.linear1 = ColumnParallelLinear(
hidden_size, hidden_size * 4, has_bias=True, gather_output=False
)
self.relu = nn.ReLU()
self.linear2 = RowParallelLinear(
hidden_size * 4, hidden_size, has_bias=True, input_is_parallel=True
)
def forward(self, x):
return self.linear2(self.relu(self.linear1(x)))
class UnifiedMLP(nn.Sequential):
"""
A unified model composed of multiple MLPBlocks.
This sequential structure is suitable for all parallelism types:
- TP is handled inside each MLPBlock.
- PP wraps this entire Sequential model.
- DP/EP treats this entire Sequential model as a single unit.
"""
def __init__(self, hidden_size=32, num_blocks=4):
super().__init__(*[MLPBlock(hidden_size) for _ in range(num_blocks)])
class Top1Router(nn.Layer):
"""A simple Top-1 Gating network for MoE."""
def __init__(self, d_model, num_experts):
super().__init__()
self.gate = nn.Linear(d_model, num_experts)
def forward(self, x):
gate_logits = self.gate(x)
expert_weights, expert_indices = paddle.topk(gate_logits, k=1, axis=-1)
return nn.functional.softmax(expert_weights, axis=-1), expert_indices
class MoELayer(nn.Layer):
"""
A more robust MoE layer that handles both EP > 1 (distributed)
and EP = 1 (local) scenarios.
"""
def __init__(self, d_model, num_experts, num_blocks=2, moe_group=None):
super().__init__()
self.d_model = d_model
self.num_experts = num_experts
self.moe_group = moe_group
self.ep_world_size = moe_group.nranks if moe_group else 1
self.router = Top1Router(d_model, num_experts)
self.experts = nn.LayerList(
[UnifiedMLP(d_model, num_blocks) for _ in range(self.num_experts)]
)
def forward(self, x):
original_shape = x.shape
x = x.reshape([-1, self.d_model])
expert_weights, expert_indices = self.router(x)
final_output = paddle.zeros_like(x)
if self.ep_world_size > 1:
# Simplified distributed routing for testing purposes.
ep_rank = dist.get_rank(self.moe_group)
for i in range(self.num_experts):
if i % self.ep_world_size == ep_rank:
mask = (expert_indices == i).astype('float32')
expert_output = self.experts[i](x)
final_output += expert_output * mask
else:
# Local routing for EP = 1
for i in range(self.num_experts):
token_mask = (expert_indices == i).squeeze(-1)
if not token_mask.any():
continue
selected_tokens = x[token_mask]
selected_weights = expert_weights[token_mask]
expert_output = self.experts[i](selected_tokens)
indices_to_scatter = paddle.where(token_mask)[0]
final_output = paddle.scatter(
final_output,
indices_to_scatter,
expert_output * selected_weights,
overwrite=False,
)
return final_output.reshape(original_shape)
# ==============================================================================
# 2. Core Logic (Environment Setup, Execution, and Verification)
# ==============================================================================
def get_model_and_strategy(args, hcg):
"""Builds model and DistributedStrategy based on parsed arguments."""
strategy = fleet.DistributedStrategy()
strategy.hybrid_configs = {
"dp_degree": args.dp,
"mp_degree": args.tp,
"pp_degree": args.pp,
}
if args.model_type == "moe":
model = MoELayer(d_model=32, num_experts=4)
else:
model = UnifiedMLP()
if args.ep > 1:
model = MoELayer(
d_model=32, num_experts=4, moe_group=hcg.get_data_parallel_group()
)
strategy.hybrid_configs["ep_degree"] = args.ep
elif args.pp > 1:
# For PP, the model must be wrapped by PipelineLayer
model = fleet.meta_parallel.PipelineLayer(
layers=model, num_stages=args.pp, topology=hcg.topology()
)
return model, strategy
def setup_execution_environment(config_args):
"""A unified function to initialize Fleet and the model."""
strategy = fleet.DistributedStrategy()
strategy.hybrid_configs = {
"dp_degree": config_args.dp,
"mp_degree": config_args.tp,
"pp_degree": config_args.pp,
}
fleet.init(is_collective=True, strategy=strategy)
hcg = fleet.get_hybrid_communicate_group()
model, strategy = get_model_and_strategy(config_args, hcg)
# Re-initialize with the final strategy (in case ep_degree was added)
fleet.init(is_collective=True, strategy=strategy)
return model
def verify_by_md5(sd1, sd2):
"""Compares two state_dicts by the MD5 hash of each parameter."""
def get_tensor_md5(tensor):
return hashlib.md5(tensor.numpy().tobytes()).hexdigest()
assert sd1.keys() == sd2.keys(), (
f"State dicts have different keys! Got {sd1.keys()} vs {sd2.keys()}"
)
for key in sd1.keys():
md5_1 = get_tensor_md5(sd1[key])
md5_2 = get_tensor_md5(sd2[key])
assert md5_1 == md5_2, (
f"MD5 mismatch for param '{key}': baseline={md5_1} vs roundtrip={md5_2}"
)
def run_step1_save_source(args):
"""Step 1: In the source configuration, save a distributed checkpoint."""
model = setup_execution_environment(args.src)
dist.save_state_dict(model.sharded_state_dict(), args.src_ckpt_path)
def run_step2_convert(args):
"""Step 2: In the target configuration, load the source checkpoint and resave."""
model = setup_execution_environment(args.tgt)
dist.load_state_dict(model.sharded_state_dict(), args.src_ckpt_path)
dist.save_state_dict(model.sharded_state_dict(), args.tgt_ckpt_path)
def run_step3_verify(args):
"""Step 3: In the source configuration, load both checkpoints and compare them."""
# 1. Create the "round-trip" model by loading the target checkpoint
model_roundtrip = setup_execution_environment(args.src)
dist.load_state_dict(
model_roundtrip.sharded_state_dict(), args.tgt_ckpt_path
)
# 2. Create the "baseline" model by loading the original source checkpoint
model_baseline = setup_execution_environment(args.src)
dist.load_state_dict(
model_baseline.sharded_state_dict(), args.src_ckpt_path
)
dist.barrier()
# 3. Each rank verifies its own part of the state_dict.
# This works for all strategies, including Pipeline Parallelism.
final_sd = model_roundtrip.state_dict()
initial_sd = model_baseline.state_dict()
if final_sd and initial_sd:
verify_by_md5(initial_sd, final_sd)
# ==============================================================================
# 3. Main Entry Point
# ==============================================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument(
"--step",
type=str,
required=True,
choices=["save_source", "convert", "verify"],
)
parser.add_argument("--src_ckpt_path", type=str)
parser.add_argument("--tgt_ckpt_path", type=str)
parser.add_argument(
"--model_type",
default="mlp",
choices=["mlp", "moe"],
help="Model architecture.",
)
# Add all strategy parameters dynamically for source and target
for prefix in ["src", "tgt"]:
for p in ["world_size", "tp", "dp", "pp", "ep"]:
parser.add_argument(f"--{prefix}_{p}", type=int, default=0)
args = parser.parse_args()
# Reorganize parsed args into src/tgt namespaces
def organize_args(prefix):
config = {
p: getattr(args, f"{prefix}_{p}")
for p in ["world_size", "tp", "dp", "pp", "ep"]
}
config["model_type"] = args.model_type
# Default parallelism degree to 1 if not specified
if config["tp"] == 0:
config["tp"] = 1
if config["dp"] == 0:
config["dp"] = 1
if config["pp"] == 0:
config["pp"] = 1
if config["ep"] == 0:
config["ep"] = 1
return argparse.Namespace(**config)
args.src = organize_args("src")
args.tgt = organize_args("tgt")
# Execute the requested step
engine = {
"save_source": run_step1_save_source,
"convert": run_step2_convert,
"verify": run_step3_verify,
}
engine[args.step](args)