295 lines
9.9 KiB
Python
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)
|