已开启
[bug] PP+FSDP PTA后端 和standalone 进行精度比较时,loss偏低 #193
MengXY107创建于  6月8日
MengXY107
MengXY107成员
6月8日 创建

该问题是怎么引起的?

重现步骤

parallel_config.py

"""
并行策略配置模块
基于 accelerate_config.yaml 提取,适配单机 8 卡环境
"""

from dataclasses import dataclass, field
from typing import List, Optional, Dict, Any


@dataclass
class TensorParallelConfig:
    """张量并行配置"""
    enabled: bool = True
    world_size: int = 2  # 单机 8 卡:TP=2
    overlap_method: str = "mc2"  # MC2 通信计算重叠


@dataclass
class ExpertParallelConfig:
    """专家并行配置"""
    enabled: bool = True
    world_size: int = 4  # 单机 8 卡:EP=4
    inner_group_size: int = 2  # EP 内部分组
    token_dispatcher_type: str = "pangu_dropless"  # 无丢弃路由
    grouped_gemm: bool = True  # 分组 GEMM
    ep_over_sp: bool = True  # EP 优先于 SP
    shared_expert_sp: bool = True  # 共享专家启用 SP
    overlap_all2all: bool = False
    overlap_1f1b: bool = True


@dataclass
class SequenceParallelConfig:
    """序列并行配置"""
    enabled: bool = True
    use_sequence_parallel: bool = True


@dataclass
class DDPConfig:
    """DDP 配置"""
    use_distributed_optimizer: bool = True
    overlap_param_gather: bool = True
    overlap_grad_reduce: bool = True
    force_bucketing: bool = True
    align_optim_buffer: bool = True
    ddp_bucket_size: int = 83886080  # 80MB
    disable_gloo_group: bool = True


@dataclass
class PrecisionConfig:
    """精度配置"""
    compute_dtype: str = "bfloat16"
    softmax_compute_dtype: str = "float32"
    logits_compute_dtype: str = "float32"
    grad_accumulation_dtype: str = "float32"
    residual_connection_dtype: str = "bfloat16"
    
    fused_operators: List[str] = field(default_factory=lambda: [
        "flash_attention",
        "rmsnorm",
        "swiglu",
        "rope",
        "moe_permute_unpermute",
        "fused_mome"
    ])
    
    nofused_operators: List[str] = field(default_factory=lambda: [
        "masked_softmax"
    ])


@dataclass
class RecomputeConfig:
    """重计算配置"""
    enabled: bool = True
    moe_recompute: List[str] = field(default_factory=lambda: [
        "moe_permute",
        "moe_activation",
        "moe_rmsnorm"
    ])
    attention_recompute: List[str] = field(default_factory=lambda: [
        "moe_mla_qkv",
        "dense_mla_qkv"
    ])


@dataclass
class GCConfig:
    """垃圾回收配置"""
    manual: bool = True
    interval: int = 200  # 每 200 步


@dataclass
class ParallelConfig:
    """
    完整并行配置
    
    单机 8 卡策略:
    - TP (张量并行) = 2
    - EP (专家并行) = 4
    - SP (序列并行) = True
    - PP (流水线并行) = 1 (禁用)
    
    总 GPU 数 = TP × EP = 2 × 4 = 8
    """
    # 并行策略
    tensor_parallel: TensorParallelConfig = field(default_factory=TensorParallelConfig)
    expert_parallel: ExpertParallelConfig = field(default_factory=ExpertParallelConfig)
    sequence_parallel: SequenceParallelConfig = field(default_factory=SequenceParallelConfig)
    ddp: DDPConfig = field(default_factory=DDPConfig)
    
    # 精度
    precision: PrecisionConfig = field(default_factory=PrecisionConfig)
    
    # 重计算
    recompute: RecomputeConfig = field(default_factory=RecomputeConfig)
    
    # GC
    gc: GCConfig = field(default_factory=GCConfig)
    
    # 微批次大小
    micro_batch_size: int = 3
    
    # 序列分块
    chunk_seq_num: int = 4
    
    # 日志
    log_throughput: bool = True
    
    def __post_init__(self):
        # 验证配置
        total_gpus = self.tensor_parallel.world_size * self.expert_parallel.world_size
        if total_gpus != 8:
            print(f"警告:配置的总 GPU 数 ({total_gpus}) 不等于 8")
    
    def get_world_size(self) -> int:
        """获取总 GPU 数"""
        return self.tensor_parallel.world_size * self.expert_parallel.world_size
    
    def get_tp_rank(self) -> int:
        """获取 TP 秩"""
        return self.tensor_parallel.world_size
    
    def get_ep_rank(self) -> int:
        """获取 EP 秩"""
        return self.expert_parallel.world_size
    
    def is_sequence_parallel(self) -> bool:
        """是否启用序列并行"""
        return self.sequence_parallel.enabled and self.sequence_parallel.use_sequence_parallel


# ==================== 预设配置 ====================

def get_single_node_8gpu_config() -> ParallelConfig:
    """
    获取单机 8 卡预设配置
    
    配置说明:
    - TP=2 (张量并行 2 路)
    - EP=4 (专家并行 4 路)
    - SP=True (序列并行启用)
    - MBS=3 (微批次大小 3)
    - BF16 主计算精度
    """
    return ParallelConfig(
        tensor_parallel=TensorParallelConfig(
            enabled=True,
            world_size=2,
            overlap_method="mc2"
        ),
        expert_parallel=ExpertParallelConfig(
            enabled=True,
            world_size=4,
            inner_group_size=2,
            token_dispatcher_type="pangu_dropless",
            grouped_gemm=True,
            ep_over_sp=True,
            shared_expert_sp=True
        ),
        sequence_parallel=SequenceParallelConfig(
            enabled=True,
            use_sequence_parallel=True
        ),
        micro_batch_size=3,
        chunk_seq_num=4
    )


def get_single_node_4gpu_config() -> ParallelConfig:
    """
    获取单机 4 卡预设配置
    
    配置说明:
    - TP=2 (张量并行 2 路)
    - EP=2 (专家并行 2 路)
    - SP=True (序列并行启用)
    """
    config = get_single_node_8gpu_config()
    config.expert_parallel.world_size = 2
    config.expert_parallel.inner_group_size = 1
    return config


def get_single_gpu_config() -> ParallelConfig:
    """
    获取单机单卡配置 (调试用)
    
    所有并行策略禁用
    """
    return ParallelConfig(
        tensor_parallel=TensorParallelConfig(enabled=False, world_size=1),
        expert_parallel=ExpertParallelConfig(enabled=False, world_size=1),
        sequence_parallel=SequenceParallelConfig(enabled=False),
        micro_batch_size=1,
        chunk_seq_num=1
    )


# ==================== 使用示例 ====================

if __name__ == "__main__":
    print("=" * 60)
    print("并行配置示例")
    print("=" * 60)
    
    # 单机 8 卡配置
    config = get_single_node_8gpu_config()
    
    print(f"\n并行策略:")
    print(f"  - TP (张量并行): {config.get_tp_rank()}")
    print(f"  - EP (专家并行): {config.get_ep_rank()}")
    print(f"  - SP (序列并行): {config.is_sequence_parallel()}")
    print(f"  - 总 GPU 数:{config.get_world_size()}")
    
    print(f"\n精度配置:")
    print(f"  - 计算精度:{config.precision.compute_dtype}")
    print(f"  - Softmax 精度:{config.precision.softmax_compute_dtype}")
    print(f"  - 融合算子:{len(config.precision.fused_operators)} 个")
    
    print(f"\n优化特性:")
    print(f"  - 重计算:{config.recompute.enabled}")
    print(f"  - Grouped GEMM: {config.expert_parallel.grouped_gemm}")
    print(f"  - 手动 GC: {config.gc.manual} (间隔:{config.gc.interval}步)")
    print(f"  - 微批次大小:{config.micro_batch_size}")
    print(f"  - 序列分块:{config.chunk_seq_num}")
    
    print("\n" + "=" * 60)

moe_transformer_parallel_demo.py

"""
PanGu MoE Transformer 并行示例 (单机 8 卡)
基于 accelerate_config.yaml 配置抽象

核心特性:
- 单机 8 卡并行 (TP=2, EP=4)
- 序列并行 (SP)
- 分布式 DDP
- MoE (Mixture of Experts): 192 专家选 10
- MLA (Multi-Head Latent Attention): 低秩注意力
- SwiGLU 激活
- RMSNorm 归一化
"""

import math
import torch
import torch.nn as nn
import torch.nn.functional as F
from typing import List, Tuple, Optional
from parallel_config import ParallelConfig, get_single_node_8gpu_config, TensorParallelConfig, ExpertParallelConfig


# ==================== 配置类 ====================

class MoEConfig:
    """MoE 配置 (对应 yaml 的 moe 部分)"""
    num_experts: int = 192
    top_k: int = 10
    expert_hidden_size: int = 512
    shared_hidden_size: int = 1024
    load_balancing: str = 'noaux_tc'
    aux_loss_coeff: float = 0.0002
    sigmoid_gating: bool = True
    routed_scaling: float = 2.5
    layers_replaced_as_dense: List[int] = [0]


class ModelConfig:
    """模型配置 (从 yaml 提取关键参数)"""
    vocab_size: int = 153600
    hidden_size: int = 2560
    num_layers: int = 45
    seq_len: int = 4096
    num_heads: int = 32
    q_lora_rank: int = 768
    kv_lora_rank: int = 512
    qk_rope_head_dim: int = 64
    ffn_hidden: int = 6144
    activation: str = 'swiglu'
    moe: MoEConfig = MoEConfig()
    parallel: Optional[ParallelConfig] = None


# ==================== 分布式工具函数 ====================

def get_rank():
    """获取当前 GPU 秩"""
    if torch.distributed.is_initialized():
        return torch.distributed.get_rank()
    return 0

def get_world_size():
    """获取总 GPU 数"""
    if torch.distributed.is_initialized():
        return torch.distributed.get_world_size()
    return 1

def is_main_process():
    """是否为主进程"""
    return get_rank() == 0


# ==================== 核心组件 ====================

class RMSNorm(nn.Module):
    """RMS 归一化"""
    def __init__(self, hidden_size: int, eps: float = 1e-5):
        super().__init__()
        self.weight = nn.Parameter(torch.ones(hidden_size))
        self.eps = eps
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
        return x / rms * self.weight


class MoEGating(nn.Module):
    """MoE 路由器"""
    def __init__(self, config: MoEConfig, hidden_size: int):
        super().__init__()
        self.config = config
        self.gate = nn.Linear(hidden_size, config.num_experts, bias=False)
        if config.sigmoid_gating:
            self.sigmoid = nn.Sigmoid()
    
    def forward(self, x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
        gate_logits = self.gate(x)
        if self.config.sigmoid_gating:
            gate_logits = self.sigmoid(gate_logits)
        topk_weights, topk_indices = torch.topk(gate_logits, self.config.top_k, dim=-1)
        topk_weights = F.softmax(topk_weights, dim=-1) * self.config.routed_scaling
        return topk_indices, topk_weights


class Expert(nn.Module):
    """单个专家网络"""
    def __init__(self, config: MoEConfig, hidden_size: int):
        super().__init__()
        self.gate_proj = nn.Linear(hidden_size, config.expert_hidden_size)
        self.up_proj = nn.Linear(hidden_size, config.expert_hidden_size)
        self.down_proj = nn.Linear(config.expert_hidden_size, hidden_size)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))


class SharedExpert(nn.Module):
    """共享专家"""
    def __init__(self, config: MoEConfig, hidden_size: int):
        super().__init__()
        self.gate_proj = nn.Linear(hidden_size, config.shared_hidden_size)
        self.up_proj = nn.Linear(hidden_size, config.shared_hidden_size)
        self.down_proj = nn.Linear(config.shared_hidden_size, hidden_size)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))


class DenseFFN(nn.Module):
    """稠密 FFN 层 (第 0 层)"""
    def __init__(self, config: ModelConfig):
        super().__init__()
        self.gate_proj = nn.Linear(config.hidden_size, config.ffn_hidden)
        self.up_proj = nn.Linear(config.hidden_size, config.ffn_hidden)
        self.down_proj = nn.Linear(config.ffn_hidden, config.hidden_size)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x))


class MoELayer(nn.Module):
    """MoE 层 (支持专家并行)"""
    def __init__(self, config: MoEConfig, hidden_size: int, ffn_hidden: int = None,
                 ep_rank: int = 0, ep_world_size: int = 1):
        super().__init__()
        self.config = config
        self.hidden_size = hidden_size
        self.ffn_hidden = ffn_hidden or config.expert_hidden_size
        self.ep_rank = ep_rank
        self.ep_world_size = ep_world_size
        
        # 计算当前 EP 组负责的专家范围
        experts_per_ep = config.num_experts // ep_world_size
        self.local_expert_start = ep_rank * experts_per_ep
        self.local_expert_end = self.local_expert_start + experts_per_ep
        
        # 本地专家 (只创建当前 EP 组负责的专家)
        self.local_experts = nn.ModuleList([
            Expert(config, hidden_size) 
            for _ in range(experts_per_ep)
        ])
        
        # 共享专家 (所有 EP 组都有)
        self.shared_expert = SharedExpert(config, hidden_size)
        
        # 路由器
        self.gate = MoEGating(config, hidden_size)
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        seq_len, batch, hidden = x.shape
        x_flat = x.reshape(-1, hidden)
        
        # 路由决策
        topk_indices, topk_weights = self.gate(x_flat)
        
        # 初始化输出
        output = torch.zeros_like(x_flat)
        
        # 只处理本地专家范围内的 token
        for local_idx in range(len(self.local_experts)):
            expert_idx = self.local_expert_start + local_idx
            expert = self.local_experts[local_idx]
            
            # 找到选择此专家的 token
            token_mask = (topk_indices == expert_idx)
            
            if token_mask.any():
                selected_tokens = x_flat[token_mask.any(dim=1)]
                token_weights = topk_weights[token_mask]
                expert_out = expert(selected_tokens)
                output[token_mask.any(dim=1)] += expert_out * token_weights.unsqueeze(-1)
        
        # 加上共享专家输出
        shared_out = self.shared_expert(x_flat)
        output = output + shared_out
        
        return output.reshape(seq_len, batch, hidden)


class MLAAttention(nn.Module):
    """简化版 MLA 注意力 (支持张量并行)"""
    def __init__(self, config: ModelConfig, tp_rank: int = 0, tp_world_size: int = 1):
        super().__init__()
        self.hidden_size = config.hidden_size
        self.num_heads = config.num_heads
        self.head_dim = config.qk_rope_head_dim
        self.tp_rank = tp_rank
        self.tp_world_size = tp_world_size
        
        # TP: 将头维度分块
        self.local_heads = self.num_heads // tp_world_size
        local_head_dim = self.local_heads * self.head_dim
        
        self.q_lora_down = nn.Linear(config.hidden_size, config.q_lora_rank)
        self.q_lora_up = nn.Linear(config.q_lora_rank, local_head_dim)
        
        self.kv_lora_down = nn.Linear(config.hidden_size, config.kv_lora_rank)
        self.kv_lora_up = nn.Linear(config.kv_lora_rank, local_head_dim * 2)
        
        self.out_proj = nn.Linear(local_head_dim, config.hidden_size)
    
    def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
        seq_len, batch, _ = x.shape
        
        q_latent = self.q_lora_down(x)
        q = self.q_lora_up(q_latent)
        
        kv_latent = self.kv_lora_down(x)
        kv = self.kv_lora_up(kv_latent)
        
        k, v = kv.chunk(2, dim=-1)
        
        # 重塑为多头
        q = q.view(seq_len, batch, self.local_heads, self.head_dim).transpose(0, 1).transpose(1, 2)
        k = k.view(seq_len, batch, self.local_heads, self.head_dim).transpose(0, 1).transpose(1, 2)
        v = v.view(seq_len, batch, self.local_heads, self.head_dim).transpose(0, 1).transpose(1, 2)
        
        scores = torch.matmul(q, k.transpose(-2, -1)) / math.sqrt(self.head_dim)
        
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        
        attn = F.softmax(scores, dim=-1)
        out = torch.matmul(attn, v)
        
        out = out.transpose(1, 2).contiguous().view(seq_len, batch, -1)
        
        return self.out_proj(out)


class TransformerBlock(nn.Module):
    """Transformer 块 (支持 TP + EP)"""
    def __init__(self, config: ModelConfig, layer_idx: int,
                 tp_rank: int = 0, tp_world_size: int = 1,
                 ep_rank: int = 0, ep_world_size: int = 1):
        super().__init__()
        self.norm1 = RMSNorm(config.hidden_size)
        self.norm2 = RMSNorm(config.hidden_size)
        self.attention = MLAAttention(config, tp_rank, tp_world_size)
        
        if layer_idx in config.moe.layers_replaced_as_dense:
            self.moe = DenseFFN(config)
        else:
            self.moe = MoELayer(config.moe, config.hidden_size, 
                               config.ffn_hidden, ep_rank, ep_world_size)
    
    def forward(self, x: torch.Tensor, mask: Optional[torch.Tensor] = None) -> torch.Tensor:
        h = x + self.attention(self.norm1(x), mask)
        out = h + self.moe(self.norm2(h))
        return out


class PanGuMoETransformer(nn.Module):
    """PanGu MoE Transformer (支持并行)"""
    def __init__(self, config: ModelConfig):
        super().__init__()
        self.config = config
        
        tp_cfg = config.parallel.tensor_parallel if config.parallel else None
        ep_cfg = config.parallel.expert_parallel if config.parallel else None
        
        tp_rank = get_rank() % tp_cfg.world_size if tp_cfg and tp_cfg.enabled else 0
        tp_world_size = tp_cfg.world_size if tp_cfg and tp_cfg.enabled else 1
        
        ep_rank = get_rank() // tp_cfg.world_size if tp_cfg and tp_cfg.enabled else 0
        ep_world_size = ep_cfg.world_size if ep_cfg and ep_cfg.enabled else 1
        
        self.tp_rank = tp_rank
        self.tp_world_size = tp_world_size
        self.ep_rank = ep_rank
        self.ep_world_size = ep_world_size
        
        self.embedding = nn.Embedding(config.vocab_size, config.hidden_size)
        
        self.layers = nn.ModuleList([
            TransformerBlock(config, i, tp_rank, tp_world_size, ep_rank, ep_world_size)
            for i in range(config.num_layers)
        ])
        
        self.final_norm = RMSNorm(config.hidden_size)
        self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
    
    def forward(self, input_ids: torch.Tensor) -> torch.Tensor:
        x = self.embedding(input_ids).transpose(0, 1)
        seq_len = input_ids.shape[1]
        mask = torch.tril(torch.ones(seq_len, seq_len, device=input_ids.device))
        
        for layer in self.layers:
            x = layer(x, mask)
        
        x = self.final_norm(x).transpose(0, 1)
        return self.lm_head(x)


# ==================== 使用示例 ====================

if __name__ == '__main__':
    print("=" * 60)
    print("PanGu MoE Transformer 并行示例 (单机 8 卡)")
    print("=" * 60)
    
    # 获取单机 8 卡配置
    parallel_config = get_single_node_8gpu_config()
    
    print(f"\n并行配置:")
    print(f"  - TP (张量并行): {parallel_config.get_tp_rank()}")
    print(f"  - EP (专家并行): {parallel_config.get_ep_rank()}")
    print(f"  - SP (序列并行): {parallel_config.is_sequence_parallel()}")
    print(f"  - 总 GPU 数:{parallel_config.get_world_size()}")
    
    # 创建模型配置
    print("\n创建模型...")
    demo_config = ModelConfig()
    demo_config.num_layers = 2
    demo_config.vocab_size = 1000
    demo_config.hidden_size = 256
    demo_config.moe.num_experts = 16  # 16 专家 (EP=4, 每卡 4 专家)
    demo_config.moe.top_k = 4
    demo_config.parallel = parallel_config
    
    model = PanGuMoETransformer(demo_config)
    
    total_params = sum(p.numel() for p in model.parameters())
    print(f"模型参数量:{total_params / 1e6:.2f}M")
    
    # 打印并行信息
    print(f"\n并行信息:")
    print(f"  - TP Rank: {model.tp_rank}, World Size: {model.tp_world_size}")
    print(f"  - EP Rank: {model.ep_rank}, World Size: {model.ep_world_size}")
    print(f"  - 每 EP 组专家数:{demo_config.moe.num_experts // model.ep_world_size}")
    
    # 前向传播
    print("\n前向传播测试:")
    batch_size = 2
    seq_len = 32
    input_ids = torch.randint(0, demo_config.vocab_size, (batch_size, seq_len))
    
    with torch.no_grad():
        logits = model(input_ids)
    
    print(f"  输入形状:{input_ids.shape}")
    print(f"  输出形状:{logits.shape}")
    print(f"  [OK] 前向传播成功!")
    
    print("\n" + "=" * 60)
    print("示例完成!支持单机 8 卡并行训练")
    print("=" * 60)


test_pp.py

报错信息

likedislike
MengXY107
MengXY107成员
6月8日 评论:
import os
import sys
os.environ["HYPER_PARALLEL_PLATFORM"] = "torch"
sys.path.insert(0, os.path.abspath(os.path.join(__file__, "../../..")))
import time
import numpy as np
import logging
import torch
import torch.nn as nn
import torch.distributed as dist
from hyper_parallel import init_device_mesh
from hyper_parallel import SkipDTensorDispatch
from hyper_parallel.core.pipeline_parallel import (PipelineStage,
                                                   ScheduleGPipe,
                                                   Schedule1F1B,
                                                   ScheduleInterleaved1F1B,
                                                   BatchDimSpec,
                                                   )
from hyper_parallel.core.pipeline_parallel.stage import SharedParameterInfo
from hyper_parallel.platform.mindspore.autograd_compat import enable_mindspore_backward_compat
from moe_transformer_parallel_demo import (PanGuMoETransformer,
                                           ModelConfig,
                                           get_single_node_8gpu_config,
                                           TransformerBlock,
                                           RMSNorm,
                                           get_rank,
                                           get_world_size)
from hyper_parallel import init_device_mesh, ContextParallel, parallelize_module, ColwiseParallel, RowwiseParallel
from hyper_parallel.core.fully_shard.api import fully_shard
from hyper_parallel.core.expert_parallel.expert_parallel import ExpertParallel

enable_mindspore_backward_compat()


def compare_nparray(self, data_expected, data_me, rtol, atol, equal_nan=True):
    if np.any(np.isnan(data_expected)):
        assert np.allclose(data_expected, data_me, rtol, atol, equal_nan=equal_nan)
    elif not np.allclose(data_expected, data_me, rtol, atol, equal_nan=equal_nan):
        assert data_expected.shape == data_me.shape
        total_count = len(data_expected.flatten())
        error = np.abs(data_expected - data_me)
        greater = np.greater(error, atol + np.abs(data_me) * rtol)
        nan_diff = np.not_equal(np.isnan(data_expected), np.isnan(data_me))
        inf_diff = np.not_equal(np.isinf(data_expected), np.isinf(data_me))
        # ICKTGQ
        if data_expected.dtype in ('complex64', 'complex128'):
            greater = greater + nan_diff + inf_diff
        else:
            neginf_diff = np.not_equal(np.isneginf(data_expected), np.isneginf(data_me))
            greater = greater + nan_diff + inf_diff + neginf_diff
        loss_count = np.count_nonzero(greater)
        assert (loss_count / total_count) < rtol, \
            "\ndata_expected_std:{0}\ndata_me_error:{1}\nloss:{2}". \
                format(data_expected[greater], data_me[greater], error[greater])
    else:
        assert np.array(data_expected).shape == np.array(data_me).shape


class PanGuMoETransformerSingle(PanGuMoETransformer):
    """
    Pipeline 感知版 PanGuMoETransformer
    每个 stage 只持有部分层,通过 pipeline_stage 和 num_stages 参数控制
    """

    def __init__(self, config: ModelConfig):
        super().__init__(config=config)

    def forward(self, input_ids, labels=None):
        x = self.embedding(input_ids).transpose(0, 1).contiguous()
        seq_len = input_ids.shape[1]
        mask = torch.tril(torch.ones(seq_len, seq_len, device=input_ids.device))

        for layer in self.layers:
            x = layer(x, mask)

        x = self.final_norm(x).transpose(0, 1)
        x = self.lm_head(x)
        x = nn.functional.cross_entropy(x.reshape(-1, x.shape[-1]), labels.reshape(-1), reduction='mean')
        return x


# ==================== PanGuMoETransformerPP 模型定义 ====================

class PanGuMoETransformerPP(nn.Module):
    """
    Pipeline 感知版 PanGuMoETransformer
    每个 stage 只持有部分层,通过 pipeline_stage 和 num_stages 参数控制
    """

    def __init__(self, config, pipeline_stage=0, num_stages=1):
        super().__init__()
        self.config = config
        self.pipeline_stage = pipeline_stage
        self.num_stages = num_stages

        tp_cfg = config.parallel.tensor_parallel if config.parallel else None
        ep_cfg = config.parallel.expert_parallel if config.parallel else None

        tp_rank = 0
        tp_world_size = 1
        ep_rank = 0
        ep_world_size = 1
        if tp_cfg and tp_cfg.enabled:
            tp_rank = get_rank() % tp_cfg.world_size
            tp_world_size = tp_cfg.world_size
        if ep_cfg and ep_cfg.enabled:
            ep_rank = get_rank() // tp_cfg.world_size
            ep_world_size = ep_cfg.world_size

        self.tp_rank = tp_rank
        self.tp_world_size = tp_world_size
        self.ep_rank = ep_rank
        self.ep_world_size = ep_world_size

        # 计算当前 stage 负责的层范围
        num_layers = config.num_layers
        layers_per_stage = (num_layers + num_stages - 1) // num_stages
        start_idx = pipeline_stage * layers_per_stage
        end_idx = min(start_idx + layers_per_stage, num_layers)

        # Stage 0: 创建 embedding
        if pipeline_stage == 0:
            self.embedding = nn.Embedding(config.vocab_size, config.hidden_size)

        # 当前 stage 对应的 layers
        self.layers = nn.ModuleList([
            TransformerBlock(config, i, tp_rank, tp_world_size, ep_rank, ep_world_size)
            for i in range(start_idx, end_idx)
        ])

        # 最后一个 stage: 创建 final_norm 和 lm_head
        if pipeline_stage == num_stages - 1:
            self.final_norm = RMSNorm(config.hidden_size)
            self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)

        self.transpose = ops_Transpose()
        self.seq_len = config.seq_len
        self.register_buffer("mask", torch.tril(torch.ones((self.seq_len, self.seq_len))))

    def forward(self, input_ids, labels=None):
        # Stage 0: 接收 input_ids
        if self.pipeline_stage == 0:
            x = self.embedding(input_ids)
            x = self.transpose(x, (1, 0, 2)).contiguous()
        else:
            # 其他 stage: 接收上一个 stage 的 hidden states
            x = input_ids
        # 执行当前 stage 的 layers
        for layer in self.layers:
            x = layer(x, self.mask)
        # 最后一个 stage: 执行 final_norm + lm_head
        if self.pipeline_stage == self.num_stages - 1:
            x = self.final_norm(x)
            x = self.transpose(x, (1, 0, 2))
            x = self.lm_head(x)
            if labels is not None:
                x = nn.functional.cross_entropy(x.reshape(-1, x.shape[-1]), labels.reshape(-1), reduction='mean')
                return x
        return x


def ops_Transpose():
    """包装 torch.transpose 操作"""
    return TransposeOp()


class TransposeOp:
    """类似于 MindSpore ops.Transpose 的转置操作"""

    def __call__(self, x, perms):
        return x.permute(perms).contiguous()

# ==================== 配置和工具函数 ====================

def get_model_config(num_layers=4, vocab_size=1000, hidden_size=256, seq_len=32, num_experts=16, top_k=4,
                     moe_enabled=False):
    """获取测试模型配置"""
    parallel_config = get_single_node_8gpu_config()
    parallel_config.tensor_parallel.enabled = False
    parallel_config.expert_parallel.enabled = False
    parallel_config.sequence_parallel.enabled = False
    parallel_config.micro_batch_size = 8

    model_config = ModelConfig()
    model_config.num_layers = num_layers
    model_config.vocab_size = vocab_size
    model_config.hidden_size = hidden_size
    model_config.seq_len = seq_len
    model_config.moe.num_experts = num_experts
    model_config.moe.top_k = top_k

    if not moe_enabled:
        model_config.moe.layers_replaced_as_dense = list(range(num_layers))

    model_config.parallel = parallel_config

    logging.warning(f"ModelConfig====> 模型配置:")
    logging.warning(f"ModelConfig====>  层数->num_layers:{model_config.num_layers}")
    logging.warning(f"ModelConfig====>  隐藏层->hidden_size:{model_config.hidden_size}")
    if moe_enabled:
        logging.warning(f"ModelConfig====>  专家数->num_experts:{model_config.moe.num_experts}")
        logging.warning(f"ModelConfig====>  Top-K->top_k: {model_config.moe.top_k}")

    return model_config


def setup_distributed_logging(rank, world_size):
    # 主进程打印到 stdout
    if rank == 0:
        handler = logging.StreamHandler()
    else:
        handler = logging.FileHandler(f"rank{rank}.log")

    formatter = logging.Formatter(
        f"[R{rank}/{world_size}] %(asctime)s %(levelname)s %(message)s"
    )
    handler.setFormatter(formatter)

    root = logging.getLogger()
    root.setLevel(logging.WARNING)
    root.addHandler(handler)


def setup_function():
    """测试前准备"""
    torch.manual_seed(42)
    np.random.seed(42)
    os.environ['PYTHONHASHSEED'] = '42'
    torch_npu.npu.manual_seed(42)
    torch_npu.npu.manual_seed_all(42)  # 多 NPU 卡
    # CANN 确定性模式(核心)
    os.environ['HYPER_THREAD'] = '0'
    os.environ['ENABLE_MPSANDBOX'] = '0'

    # 启用确定性算子
    torch_npu.npu.set_compile_mode(jit_compile=False)

    torch.use_deterministic_algorithms(True)
    """初始化分布式环境"""
    if not dist.is_initialized():
        dist.init_process_group(backend='hccl')
    rank = dist.get_rank()
    world = dist.get_world_size()
    torch.npu.set_device(rank)

    setup_distributed_logging(rank, world)


def teardown_function():
    """测试后清理"""
    if dist.is_initialized():
        dist.destroy_process_group()


class GeneratorDataset:
    """简单的 PyTorch 数据集模拟"""

    def __init__(self, data, column_names, shuffle=False):
        self.data = data
        self.column_names = column_names
        self.shuffle = shuffle

    def __iter__(self):
        indices = list(range(len(self.data)))
        if self.shuffle:
            np.random.shuffle(indices)

        for idx in indices:
            input_ids = self.data[idx][0]
            labels = self.data[idx][1]
            yield input_ids, labels


def generator_dataset(size, batch_size=1000, seq_len=32, dynamic=False, dtype=np.int32):
    """生成训练数据"""
    np.random.seed(0)
    for _ in range(size):
        if dynamic:
            batch_size = 8 * np.random.randint(1, 32)
        input_ids = np.random.randint(0, batch_size, size=(batch_size, seq_len)).astype(dtype)
        input_mask = np.random.randint(0, batch_size, size=(batch_size, seq_len)).astype(dtype)
        yield input_ids, input_mask


def run_train_with_moe(model, train_dataset, epochs=10, max_steps=10, schedule=None, pp_rank=0, pp_size=4,
                       stages_param=None, clip_norm=False, amp=False, micro_batch=8):
    """PyTorch 版本的训练循环"""
    # 获取可训练参数
    if stages_param is not None:
        trainable_params = stages_param
    else:
        trainable_params = list(filter(lambda p: p.requires_grad, model.parameters()))

    optimizer = torch.optim.Adam(trainable_params, lr=1e-4)

    loss_list = []
    step = 0
    for _ in range(epochs):
        for _, (input_ids, labels) in enumerate(train_dataset):
            input_ids = torch.tensor(input_ids, dtype=torch.long).npu()
            labels = torch.tensor(labels, dtype=torch.long).npu()

            # 前向传播
            optimizer.zero_grad()
            with SkipDTensorDispatch():
                if schedule is not None:
                    # Pipeline parallel 训练
                    if amp:
                        with torch.cuda.amp.autocast():
                            loss = schedule.run(input_ids) if pp_rank == 0 else schedule.run(labels=labels)
                    else:
                        loss = schedule.run(input_ids) if pp_rank == 0 else schedule.run(labels=labels)
                    if pp_rank == pp_size - 1:
                        # 获取 loss
                        if isinstance(loss, (list, tuple)):
                            loss_values = [ls.item() if hasattr(ls, 'item') else ls for ls in loss]
                            loss = np.mean(loss_values)
                            print(f"hyper step: {step}  loss: {loss}", flush=True)

                    # 从 stage 参数获取梯度
                    if clip_norm:
                        torch.nn.utils.clip_grad_norm_(trainable_params, max_norm=1.0)
                    # ClipGradNorm(max_norm=1.0)(tuple(p.grad for p in stages_param))
                    with torch.no_grad():
                        for p in stages_param:
                            if p.grad is not None:
                                p.grad.div_(micro_batch)
                else:
                    # 单卡训练
                    if amp:
                        with torch.cuda.amp.autocast():
                            loss = model(input_ids, labels)
                    else:
                        loss = model(input_ids, labels)
                    loss.backward()
                    if get_rank() == 0:
                        print(f"standalone step: {step} loss: {loss}", flush=True)

                    if clip_norm:
                        torch.nn.utils.clip_grad_norm_(trainable_params, max_norm=1.0)

                optimizer.step()

                if isinstance(loss, (list, tuple)):
                    loss = float(np.mean([l.item() if hasattr(l, 'item') else l for l in loss]))
                loss_list.append(loss if isinstance(loss, float) else loss.item())
                step += 1
                if step >= max_steps:
                    break
            if step >= max_steps:
                break
    return loss_list, []


def run_train_with_moe_multi_model(models, train_dataset, epochs=10, max_steps=10, schedule=None, pp_rank=0, pp_size=4, micro_batch=8):
    """多模型版本的训练循环 (用于 ScheduleInterleaved1F1B)"""
    model_params = [tuple(p for p in model.parameters() if p.requires_grad) for model in models]
    optimizers = [torch.optim.Adam(_params, lr=1e-4) for _params in model_params]

    loss_list = []
    step = 0
    time_list = []
    for _ in range(epochs):
        for _, (input_ids, labels) in enumerate(train_dataset):
            input_ids = torch.tensor(input_ids, dtype=torch.long).npu()
            labels = torch.tensor(labels, dtype=torch.long).npu()

            start = time.time()

            # 清零梯度
            for m_optimizer in optimizers:
                m_optimizer.zero_grad()

            loss = schedule.run(input_ids) if pp_rank == 0 else schedule.run(labels=labels)
            if pp_rank == pp_size - 1:
                # 获取 loss
                if isinstance(loss, (list, tuple)):
                    loss_values = [ls.item() if hasattr(ls, 'item') else ls for ls in loss]
                    loss = np.mean(loss_values)

            # 更新每个模型的参数
            for m_optimizer, m_params in zip(optimizers, model_params):
                for p in m_params:
                    if p.grad is not None:
                        p.grad.div_(micro_batch)
                m_optimizer.step()

            end = time.time()
            if step != 0:
                time_list.append(end - start)
            loss_list.append(loss)
            step += 1
            if step >= max_steps:
                break
        if step >= max_steps:
            break
    return loss_list, time_list

# ==================== 测试用例 ====================

def check_loss_converge(loss_list, slope_threshold=-0.01, compare_ratio=0.95):
    """
    判断loss是否呈收敛趋势                                                                                                                                                                                                              方法1: 线性回归斜率 < threshold                                                                                                                                                                                                     方法2: 后50%平均loss / 前50%平均loss < ratio                                                                                                                                                                                        两种方法同时满足才判定为收敛                                                                                                                                                                                                        """
    loss = np.array(loss_list)
    if len(loss) < 4:
        return False, "loss数据点太少"

    n = len(loss)
    x = np.arange(n)

    # 方法1: 线性回归斜率
    coeffs = np.polyfit(x, loss, 1)
    slope = coeffs[0]
    if slope >= slope_threshold:
        return False, f"线性斜率 {slope:.6f} >= {slope_threshold},不收敛"

    # 方法2: 前后半段比较
    mid = n // 2
    first_half_avg = np.mean(loss[:mid])
    second_half_avg = np.mean(loss[mid:])
    ratio_val = second_half_avg / first_half_avg if first_half_avg > 0 else 1.0
    if ratio_val >= compare_ratio:
        return False, f"前后半段比值 {ratio_val:.4f} >= {compare_ratio},不收敛"

    logging.warning(f"MindSporeTest====> 收敛: 斜率={slope:.6f}, 前后比值={ratio_val:.4f}")
    return True


# ==================== 测试用例 ====================


"""
TEST_SUMMARY: 并行特性交互:FSDP+pp

Test Steps:
1. 构建测试网络
2. 初始化mesh
mesh = init_device_mesh(device_type="npu", mesh_shape=(4, 2), mesh_dim_names=("pp", "dp"))
pp_mesh = mesh["pp"]
3.设置 fsdp+pp 并行:
ScheduleGPipe + batch_size=256, micro_batch=8, stage_num=4
4.执行 10 step 训练

Expected Result:
1.验证不 hang、loss 正确
"""


def test_pipeline_parallel_feature_interaction_torch_check_004():
    # 获取 device 信息
    rank = get_rank()
    world_size = get_world_size()
    rank = get_rank()

    # 训练参数
    epochs = 100
    max_steps = 500
    batch_size = 256
    seq_len = 32
    micro_batch_num = 8

    pp_size = 4
    dp_size = world_size // pp_size

    # 生成训练数据
    all_data = list(generator_dataset(size=1000, batch_size=batch_size, seq_len=seq_len))
    train_dataset_1 = GeneratorDataset(all_data[:], ["data", "label"], shuffle=False)
    train_dataset_2 = GeneratorDataset(all_data[:], ["data", "label"], shuffle=False)

    # 创建配置
    model_config = get_model_config()
    # 创建单卡训练模型
    single_model = PanGuMoETransformerSingle(model_config)
    for param in single_model.parameters():
        param.data = param.data.npu()
    single_model = single_model.to('npu')

    # 创建pp训练模型
    mesh = init_device_mesh(device_type="npu", mesh_shape=(pp_size, dp_size), mesh_dim_names=("pp", "dp"))
    dp_mesh = mesh["dp"]
    pp_mesh = mesh["pp"]
    pp_rank = pp_mesh.get_local_rank()
    chunk_model = PanGuMoETransformerPP(model_config, pipeline_stage=pp_rank, num_stages=pp_size)

    # 权重同步
    # 先同步单卡模型到NPU,确保state_dict的权重在NPU上
    single_state = single_model.state_dict()
    chunk_state = chunk_model.state_dict()
    for key in chunk_state:
        s_key = key.replace("layers.0.", f"layers.{rank // dp_size}.")
        if s_key in single_state:
            chunk_state[key] = single_state[s_key]
    chunk_model.load_state_dict(chunk_state)
    # 同步chunk_model参数和buffer到NPU
    for param in chunk_model.parameters():
        param.data = param.data.npu()

    chunk_model = chunk_model.to('npu')
    for name, buf in chunk_model.named_buffers():
        chunk_model.register_buffer(name, single_state[name] if name in single_state else buf.to("npu"))

    # FSDP: 对模型应用数据并行分片
    fully_shard(chunk_model.layers[0], mesh=mesh["dp"])
    fully_shard(chunk_model, mesh=mesh["dp"])

    chunk_param = [param for param in chunk_model.parameters() if param.requires_grad]

    # 执行端到端训练
    logging.warning("MindSporeTest====> 开始单卡训练")
    run_train_with_moe(single_model, train_dataset_1, epochs=epochs, max_steps=max_steps)

    stages = [PipelineStage(chunk_model, stage_index=rank // dp_size, stage_num=pp_size, group=pp_mesh.get_group(),
                            mesh=pp_mesh, device='npu')]
    schedule = ScheduleGPipe(stages, micro_batch_num=micro_batch_num)
    logging.warning("MindSporeTest====> 开始 pp 训练")
    run_train_with_moe(chunk_model, train_dataset_2, epochs=epochs, max_steps=max_steps,
                       schedule=schedule, pp_rank=pp_rank, stages_param=chunk_param, micro_batch=micro_batch_num)

    # 对比同名参数的梯度
    for name, p in single_model.named_parameters():
        if p.grad is not None:
            for name2, p2 in chunk_model.named_parameters():
                s_name = name2.replace("layers.0.", f"layers.{rank // dp_size}.")
                if name == s_name and p2.grad is not None:
                    # single_model 没有分片,直接取全量梯度
                    local_grad = p.grad.cpu().numpy()

                    # FSDP 梯度是 DTensor,all_gather 拼成全量梯度
                    g_local = p2.grad.to_local() if hasattr(p2.grad, "to_local") else p2.grad
                    full_grad_list = [torch.zeros_like(g_local) for _ in range(dp_size)]
                    torch.distributed.all_gather(full_grad_list, g_local, group=dp_mesh.get_group())
                    full_grad = torch.cat(full_grad_list, dim=0).cpu().numpy()

                    # 比较单卡全量梯度和 FSDP 收集后的全量梯度
                    compare_nparray(full_grad, local_grad, 0.002, 0.002)




if __name__ == "__main__":
    import torch_npu
    torch_npu.npu.is_available = lambda: True
    logging.warning("MindSporeTest::set torch_npu.npu.is_available=True")

    setup_function()
    test_pipeline_parallel_feature_interaction_torch_check_004()
    teardown_function()

likedislike
MengXY107
MengXY107成员
6月8日 评论:

Standalone 方式构建梯度累加:

    per = batch // mb  # micro-batch size, matches the PP/GPipe micro split
    is_owner = (pp_rank == pp_size - 1) and (dp_mesh.get_local_rank() == 0)
    f_std = f_pp = None
    if is_owner:
        f_std = open("standalone.txt", "w")
        f_pp = open("pp_fsdp.txt", "w")

    single_params = [p for p in single.parameters() if p.requires_grad]
    for step in range(STEPS):
        x = all_data[step][0]
        y = all_data[step][1]
        xt = torch.tensor(x, dtype=torch.long).npu()
        yt = torch.tensor(y, dtype=torch.long).npu()

        # ---- standalone in the SAME micro-batch accumulation口径 as PP+GPipe ----
        # 8 micro-batches of `per`, mean-CE each, accumulate grads, then div_(mb)
        # (mirrors the PP path's p.grad.div_(micro_batch)); loss = mean of micro means.
        sopt.zero_grad()
        s_micro = []
        for i in range(mb):
            xs = xt[i * per:(i + 1) * per]
            ys = yt[i * per:(i + 1) * per]
            l = single(xs, ys)
            l.backward()
            s_micro.append(l.item())
        with torch.no_grad():
            for p in single_params:
                if p.grad is not None:
                    p.grad.div_(mb)
        sval = float(np.mean(s_micro))
likedislike
MengXY107
MengXY107成员
6月10日 评论:

确认是问题,master 当前实现,torch侧的fully_shard的reduce_params只处理TP引入的mirrors AllReduce。因此导致sharded_param.grad并没有被挂载上。导致了精度出现问题。

likedislike
MengXY107
MengXY107成员
6月12日 评论:

moe_transformer_demo_mindspore.py

"""
简化版 MoE Transformer 示例 - MindSpore 版本

核心特性:
- MoE (Mixture of Experts): 192 专家选 10
- MLA (Multi-Head Latent Attention): 低秩注意力
- SwiGLU 激活
- RMSNorm 归一化
"""

import math
import numpy as np
import mindspore as ms
import mindspore.nn as nn
import mindspore.ops as ops
from mindspore import Tensor, dtype as mstype
from typing import List, Tuple, Optional


# ==================== 配置类 ====================

class MoEConfig:
    """MoE 配置 (对应 yaml 的 moe 部分)"""
    num_experts: int = 192          # 专家总数
    top_k: int = 10                 # 激活专家数
    expert_hidden_size: int = 512   # 专家隐藏层维度
    shared_hidden_size: int = 1024  # 共享专家维度
    load_balancing: str = 'noaux_tc'
    aux_loss_coeff: float = 0.0002
    sigmoid_gating: bool = True
    routed_scaling: float = 2.5
    layers_replaced_as_dense: List[int] = [0]  # 保持稠密的层 (默认第 0 层)


class ModelConfig:
    """模型配置 (从 yaml 提取关键参数)"""
    # 基础
    vocab_size: int = 153600
    hidden_size: int = 2560
    num_layers: int = 45
    seq_len: int = 4096
    
    # Attention
    num_heads: int = 32
    q_lora_rank: int = 768
    kv_lora_rank: int = 512
    qk_rope_head_dim: int = 64
    
    # FFN
    ffn_hidden: int = 6144
    activation: str = 'swiglu'
    
    # MoE
    moe: MoEConfig = MoEConfig()


# ==================== 核心组件 ====================

class RMSNorm(nn.Cell):
    """RMS 归一化 (替代 LayerNorm)"""
    def __init__(self, hidden_size: int, eps: float = 1e-5):
        super().__init__()
        self.weight = ms.Parameter(Tensor(np.ones(hidden_size), mstype.float32))
        self.eps = eps
    
    def construct(self, x: Tensor) -> Tensor:
        return ops.rms_norm(x, self.weight, self.eps)[0]


class MoEGating(nn.Cell):
    """MoE 路由器 (Router) - 决定 token 分配给哪些专家"""
    def __init__(self, config: MoEConfig, hidden_size: int):
        super().__init__()
        self.config = config
        self.gate = nn.Dense(hidden_size, config.num_experts, has_bias=False)
        
        if config.sigmoid_gating:
            self.sigmoid = ops.Sigmoid()
        
        self.top_k = config.top_k
        self.routed_scaling = config.routed_scaling
    
    def construct(self, x: Tensor) -> Tuple[Tensor, Tensor]:
        """
        Args:
            x: [seq_len * batch, hidden_size]
        Returns:
            topk_indices: [seq_len * batch, top_k] 专家索引
            topk_weights: [seq_len * batch, top_k] 路由权重
        """
        # 计算路由分数
        gate_logits = self.gate(x)  # [N, num_experts]
        
        if self.config.sigmoid_gating:
            gate_logits = self.sigmoid(gate_logits)
        
        # Top-K 选择
        topk_weights, topk_indices = ops.TopK()(gate_logits, self.top_k)
        
        # 归一化权重 (softmax over top-k)
        topk_weights = ops.Softmax(axis=-1)(topk_weights) * self.routed_scaling
        
        return topk_indices, topk_weights


class Expert(nn.Cell):
    """单个专家网络 (简单的 MLP)"""
    def __init__(self, config: MoEConfig, hidden_size: int):
        super().__init__()
        self.gate_proj = nn.Dense(hidden_size, config.expert_hidden_size)
        self.up_proj = nn.Dense(hidden_size, config.expert_hidden_size)
        self.down_proj = nn.Dense(config.expert_hidden_size, hidden_size)
        self.silu = ops.Sigmoid()
    
    def construct(self, x: Tensor) -> Tensor:
        return self.down_proj(self.silu(self.gate_proj(x)) * self.up_proj(x))


class SharedExpert(nn.Cell):
    """共享专家 (所有 token 都经过)"""
    def __init__(self, config: MoEConfig, hidden_size: int):
        super().__init__()
        self.gate_proj = nn.Dense(hidden_size, config.shared_hidden_size)
        self.up_proj = nn.Dense(hidden_size, config.shared_hidden_size)
        self.down_proj = nn.Dense(config.shared_hidden_size, hidden_size)
        self.silu = ops.Sigmoid()
    
    def construct(self, x: Tensor) -> Tensor:
        return self.down_proj(self.silu(self.gate_proj(x)) * self.up_proj(x))


class DenseFFN(nn.Cell):
    """稠密 FFN 层 (用于第 0 层)"""
    def __init__(self, config: ModelConfig):
        super().__init__()
        self.gate_proj = nn.Dense(config.hidden_size, config.ffn_hidden)
        self.up_proj = nn.Dense(config.hidden_size, config.ffn_hidden)
        self.down_proj = nn.Dense(config.ffn_hidden, config.hidden_size)
        self.silu = ops.Sigmoid()
    
    def construct(self, x: Tensor) -> Tensor:
        return self.down_proj(self.silu(self.gate_proj(x)) * self.up_proj(x))


class MoELayer(nn.Cell):
    """
    MoE 层 - 核心组件
    
    工作流程:
    1. Router 为每个 token 选择 top-k 专家
    2. 每个专家独立处理分配给它的 token
    3. 加权组合专家输出 + 共享专家输出
    """
    def __init__(self, config: MoEConfig, hidden_size: int, ffn_hidden: int = None):
        super().__init__()
        self.config = config
        self.hidden_size = hidden_size
        self.ffn_hidden = ffn_hidden or config.expert_hidden_size
        
        # 路由器
        self.gate = MoEGating(config, hidden_size)
        
        # 192 个专家 (用 CellList 管理)
        self.experts = nn.CellList([
            Expert(config, hidden_size) for _ in range(config.num_experts)
        ])
        
        # 共享专家 (始终激活)
        self.shared_expert = SharedExpert(config, hidden_size)
    
    def construct(self, x: Tensor) -> Tensor:
        """
        Args:
            x: [seq_len, batch, hidden_size]
        Returns:
            output: [seq_len, batch, hidden_size]
        """
        seq_len, batch, hidden = x.shape
        
        # 展平为 [seq*batch, hidden] 方便路由
        x_flat = x.reshape(-1, hidden)
        
        # 1. 路由决策
        topk_indices, topk_weights = self.gate(x_flat)
        
        # 2. 初始化输出
        output = ops.zeros_like(x_flat)
        
        # 3. 对每个专家,处理分配给它的 token
        for expert_idx, expert in enumerate(self.experts):
            # 找到选择此专家的 token
            token_mask = (topk_indices == expert_idx)  # [N, top_k]
            
            if ops.any(token_mask):
                # 获取对应的 token 和权重
                selected_mask = ops.any(token_mask, axis=1)
                selected_tokens = x_flat[selected_mask]
                
                # 专家处理
                expert_out = expert(selected_tokens)
                
                # 加权累加 - 简化处理
                output[selected_mask] += expert_out * ops.mean(topk_weights[token_mask], -1, True)
        
        # 4. 加上共享专家输出 (所有 token 都经过)
        shared_out = self.shared_expert(x_flat)
        output = output + shared_out
        
        # 恢复形状
        return output.reshape(seq_len, batch, hidden)


class MLAAttention(nn.Cell):
    """
    简化版 MLA (Multi-Head Latent Attention)
    
    核心思想:用低秩压缩减少 KV cache 大小
    """
    def __init__(self, config: ModelConfig):
        super().__init__()
        self.hidden_size = config.hidden_size
        self.num_heads = config.num_heads
        self.head_dim = config.qk_rope_head_dim
        
        # LoRA 低秩投影
        self.q_lora_down = nn.Dense(config.hidden_size, config.q_lora_rank)
        self.q_lora_up = nn.Dense(config.q_lora_rank, config.num_heads * self.head_dim)
        
        self.kv_lora_down = nn.Dense(config.hidden_size, config.kv_lora_rank)
        self.kv_lora_up = nn.Dense(config.kv_lora_rank, 
                                     config.num_heads * (self.head_dim + self.head_dim))
        
        # 输出投影
        self.out_proj = nn.Dense(config.num_heads * self.head_dim, config.hidden_size)
        
        self.softmax = ops.Softmax(axis=-1)
        self.transpose = ops.Transpose()
        self.split = ops.Split(axis=-1, output_num=2)
    
    def construct(self, x: Tensor, mask: Optional[Tensor] = None) -> Tensor:
        """
        Args:
            x: [seq_len, batch, hidden_size]
        """
        seq_len, batch, _ = x.shape
        
        # 低秩投影
        q_latent = self.q_lora_down(x)
        q = self.q_lora_up(q_latent)
        
        kv_latent = self.kv_lora_down(x)
        kv = self.kv_lora_up(kv_latent)
        
        # 拆分 K 和 V
        k, v = self.split(kv)
        
        # 重塑为多头
        q = self.transpose(q.view(seq_len, batch, self.num_heads, self.head_dim), (1, 2, 0, 3))
        k = self.transpose(k.view(seq_len, batch, self.num_heads, self.head_dim), (1, 2, 0, 3))
        v = self.transpose(v.view(seq_len, batch, self.num_heads, self.head_dim), (1, 2, 0, 3))
        
        # 注意力计算
        scores = ops.matmul(q, self.transpose(k, (0, 1, 3, 2))) / math.sqrt(self.head_dim)
        
        if mask is not None:
            scores = scores.masked_fill(mask == 0, -1e9)
        
        attn = self.softmax(scores)
        out = ops.matmul(attn, v)
        
        # 合并头
        out = self.transpose(out, (0, 2, 1, 3)).reshape(seq_len, batch, -1)
        
        return self.out_proj(out)


class TransformerBlock(nn.Cell):
    """Transformer 块 (包含 MLA + MoE)"""
    def __init__(self, config: ModelConfig, layer_idx: int):
        super().__init__()
        self.layer_idx = layer_idx
        
        # 归一化
        self.norm1 = RMSNorm(config.hidden_size)
        self.norm2 = RMSNorm(config.hidden_size)
        
        # 注意力
        self.attention = MLAAttention(config)
        
        # MoE 层 (第 0 层是稠密层)
        if layer_idx in config.moe.layers_replaced_as_dense:
            self.moe = DenseFFN(config)
        else:
            self.moe = MoELayer(config.moe, config.hidden_size, config.ffn_hidden)
    
    def construct(self, x: Tensor, mask: Optional[Tensor] = None) -> Tensor:
        # Pre-Norm 架构
        h = x + self.attention(self.norm1(x), mask)
        out = h + self.moe(self.norm2(h))
        return out


class PanGuMoETransformer(nn.Cell):
    """
     MoE Transformer 简化版
    
    架构:
    Embedding -> [TransformerBlock × 45] -> RMSNorm -> LM Head
    """
    def __init__(self, config: ModelConfig):
        super().__init__()
        self.config = config
        
        # 词嵌入
        self.embedding = nn.Embedding(config.vocab_size, config.hidden_size)
        
        # Transformer 层
        self.layers = nn.CellList([
            TransformerBlock(config, i) for i in range(config.num_layers)
        ])
        
        # 最终归一化
        self.final_norm = RMSNorm(config.hidden_size)
        
        # LM Head
        self.lm_head = nn.Dense(config.hidden_size, config.vocab_size, has_bias=False)
        
        self.transpose = ops.Transpose()
    
    def construct(self, input_ids: Tensor) -> Tensor:
        """
        Args:
            input_ids: [batch, seq_len]
        Returns:
            logits: [batch, seq_len, vocab_size]
        """
        # 嵌入
        x = self.embedding(input_ids)
        x = self.transpose(x, (1, 0, 2))  # [seq, batch, hidden]
        
        # 生成因果 mask
        seq_len = input_ids.shape[1]
        mask = ops.tril(ops.ones((seq_len, seq_len), mstype.float32))
        
        # 通过所有层
        for layer in self.layers:
            x = layer(x, mask)
        
        # 最终归一化
        x = self.final_norm(x)
        
        # LM Head
        x = self.transpose(x, (1, 0, 2))
        logits = self.lm_head(x)
        
        return logits


# ==================== 使用示例 ====================

if __name__ == '__main__':
    print("=" * 60)
    print(" MoE Transformer 简化示例 - MindSpore 版本")
    print("=" * 60)
    
    # 设置 MindSpore 环境
    ms.set_context(mode=ms.PYNATIVE_MODE, device_target="Ascend")
    
    # 1. 创建配置
    config = ModelConfig()
    print(f"\n模型配置:")
    print(f"  - 隐藏层维度:{config.hidden_size}")
    print(f"  - 层数:{config.num_layers}")
    print(f"  - 专家数:{config.moe.num_experts}")
    print(f"  - 激活专家:{config.moe.top_k}")
    print(f"  - 词表大小:{config.vocab_size}")
    
    # 2. 创建模型 (用小配置演示)
    print("\n创建模型...")
    demo_config = ModelConfig()
    demo_config.num_layers = 2
    demo_config.vocab_size = 1000
    demo_config.hidden_size = 256
    demo_config.moe.num_experts = 8
    demo_config.moe.top_k = 2
    
    model = PanGuMoETransformer(demo_config)
    
    # 打印参数量
    total_params = sum(p.size for p in model.get_parameters())
    print(f"模型参数量:{total_params / 1e6:.2f}M")
    
    # 3. 前向传播示例
    print("\n前向传播测试:")
    batch_size = 2
    seq_len = 32
    input_ids = Tensor(np.random.randint(0, demo_config.vocab_size, (batch_size, seq_len)), mstype.int32)
    
    logits = model(input_ids)
    
    print(f"  输入形状:{input_ids.shape}")
    print(f"  输出形状:{logits.shape}")
    print(f"  [OK] 前向传播成功!")
    
    print("\n" + "=" * 60)
    print("示例完成!")
    print("=" * 60)

likedislike
MengXY107
MengXY107成员
6月12日 评论:
"""
Test suite for fully_shard ZeRO-2/ZeRO-3 on MindSpore.

包含:
- 共享基础函数 (模型配置、数据集、训练函数、权重同步、结果对比)
- 12 个主测试用例 (test_fully_shard_ms_check_001 ~ 012)

注意:
- 使用 network/mindspore/pangu/moe_transformer_parallel_demo_mindspore.py 中的 PanGuMoETransformer
- 大部分用例在测试函数中应用 fully_shard
- 只有特殊用例 (如 check_003 单层测试) 在模型 __init__ 中对 layer 调用 fully_shard
"""

import os
import sys
import time
import pytest
import numpy as np
import mindspore as ms
import mindspore.nn as nn
import mindspore.ops.operations as P
from mindspore import mint
from mindspore import ops
from mindspore import log as logger
from mindspore.common import _no_grad
from mindspore.communication import get_group_size, get_rank, init
from mindspore.dataset import GeneratorDataset
from mindspore import value_and_grad
from mindspore._c_expression import NoFallbackGuard
from ...share.context_base import contextbase
from hyper_parallel import init_device_mesh, fully_shard, SkipDTensorDispatch
from hyper_parallel.platform.mindspore.autograd_compat import enable_mindspore_backward_compat
from ..network.mindspore.pangu.moe_transformer_parallel_demo_mindspore import (
    PanGuMoETransformer, ModelConfig, get_single_node_8gpu_config,
    TransformerBlock, RMSNorm, get_rank_fun, MoEConfig
)
from ..network.mindspore.clip_grad import ClipGradNorm
from ...share.marker import (
    Author, Level0, Level1, Manual, Env_Cards,
    SKIP_ENV_CPU, SKIP_ENV_910A, SKIP_MODE_GRAPH
)
from hyper_parallel.core.fully_shard.utils import CPUOffloadPolicy,MixedPrecisionPolicy
from hyper_parallel.core.dtensor.placement_types import Shard

enable_mindspore_backward_compat()
ms.runtime.launch_blocking()


def setup_function():
    """每个测试函数执行前的 setup"""
    contextbase.case_prepare()
    ms.set_deterministic(True)
    ms.set_seed(0)
    np.random.seed(0)


def teardown_function():
    """每个测试函数执行后的 teardown"""
    contextbase.case_cleanup()
    try:
        from mindspore.communication import destroy_group
        destroy_group()
    except:
        pass


def get_model_config(num_layers=4, vocab_size=1000, hidden_size=256, seq_len=32,
                     num_experts=16, top_k=4, moe_enabled=False, tp=False, ep=False, sp=False):
    """
    创建模型配置
    Args:
        num_layers: Transformer 层数
        vocab_size: 词表大小
        hidden_size: 隐藏层维度
        seq_len: 序列长度
        num_experts: MoE 专家数量
        top_k: MoE top-k 选择数
        moe_enabled: 是否启用 MoE
    Returns:
        ModelConfig 对象
    """
    parallel_config = get_single_node_8gpu_config()
    parallel_config.tensor_parallel.enabled = tp
    parallel_config.expert_parallel.enabled = ep
    parallel_config.sequence_parallel.enabled = sp
    parallel_config.micro_batch_size = 8
    model_config = ModelConfig()
    model_config.num_layers = num_layers
    model_config.vocab_size = vocab_size
    model_config.hidden_size = hidden_size
    model_config.seq_len = seq_len
    model_config.moe.num_experts = num_experts
    model_config.moe.top_k = top_k
    if not moe_enabled:
        model_config.moe.layers_replaced_as_dense = list(range(num_layers))
    model_config.parallel = parallel_config
    logger.warning(f"MindSporeTest====>模型配置:")
    logger.warning(f"MindSporeTest====>  层数->num_layers: {model_config.num_layers}")
    logger.warning(f"MindSporeTest====>  隐藏层->hidden_size: {model_config.hidden_size}")
    if moe_enabled:
        logger.warning(f"MindSporeTest====>  专家数->num_experts: {model_config.moe.num_experts}")
        logger.warning(f"MindSporeTest====>  Top-K->top_k: {model_config.moe.top_k}")
    return model_config


def generator_dataset(size, batch_size=2, seq_len=32, vocab_size=1000, dynamic=False, dtype=np.int32):
    """
    生成训练数据 (参考 moe_transformer_parallel_demo_mindspore.py main 方法)
    Args:
        size: 数据样本数
        batch_size: 批次大小
        seq_len: 序列长度
        vocab_size: 词表大小 (用于生成 input_ids 和 labels)
        dynamic: 是否动态 batch_size
        dtype: 数据类型
    Yields:
        input_ids, labels
    """
    np.random.seed(0)
    for _ in range(size):
        if dynamic:
            batch_size = 8 * np.random.randint(1, 32)
        input_ids = np.random.randint(0, vocab_size, size=(batch_size, seq_len)).astype(dtype)
        labels = np.random.randint(0, vocab_size, size=(batch_size, seq_len)).astype(dtype)
        yield input_ids, labels



class PanGuMoETransformerSingle(PanGuMoETransformer):
    """
    单卡参考模型 - 用于对比验证
    重写 construct 以支持 labels 参数计算 loss
    """


    def __init__(self, config: ModelConfig):
        super().__init__(config=config)
        ms.set_seed(0)
        np.random.seed(0)


    def construct(self, input_ids, labels=None):
        x = self.embedding(input_ids)
        x = self.transpose(x, (1, 0, 2))
        
        seq_len = input_ids.shape[1]
        mask = ops.tril(ops.ones((seq_len, seq_len), ms.float32))
        for layer in self.layers:
            x = layer(x, mask)
        x = self.final_norm(x)
        x = self.transpose(x, (1, 0, 2))
        logits = self.lm_head(x)
        if labels is not None:
            loss =  mint.nn.functional.cross_entropy(logits.reshape(-1, logits.shape[-1]), labels.reshape(-1), reduction='mean')
            return loss
        return logits


class PanGuMoETransformerFSInit(PanGuMoETransformer):
    """
    fully_shard 版本模型 - 在 __init__ 中对每个 layer 调用 fully_shard
    
    特殊用例使用 (如 check_003 单层测试、check_004 多层嵌套测试)
    重写 construct 以支持 labels 参数计算 loss
    """


    def __init__(self, config: ModelConfig, mesh=None, reshard_after_forward=True,
                 apply_to_layers=False, **fully_shard_kwargs):
        super().__init__(config)
        ms.set_seed(0)
        np.random.seed(0)
        self.fs_mesh = mesh
        self.fs_reshard = reshard_after_forward
        # 特殊用例:在 __init__ 中对每个 layer 应用 fully_shard
        if apply_to_layers and mesh is not None:
            for idx, layer in enumerate(self.layers):
                logger.warning(f'\nlayer: {layer}')
                fully_shard(layer, mesh=mesh, reshard_after_forward=reshard_after_forward, **fully_shard_kwargs)
                layer.set_requires_gradient_sync(True)


    def construct(self, input_ids, labels=None):
        x = self.embedding(input_ids)
        x = self.transpose(x, (1, 0, 2))
        
        seq_len = input_ids.shape[1]
        mask = ops.tril(ops.ones((seq_len, seq_len), ms.float32))
        for layer in self.layers:
            x = layer(x, mask)
        x = self.final_norm(x)
        x = self.transpose(x, (1, 0, 2))
        logits = self.lm_head(x)
        if labels is not None:
            loss =  mint.nn.functional.cross_entropy(logits.reshape(-1, logits.shape[-1]), labels.reshape(-1), reduction='mean')
            return loss
        return logits


# ============================================================================
# 共享训练函数
# ============================================================================

def run_train_single(model, train_dataset, epochs=1, max_steps=10, 
                     rank=0, monitor_memory=False):
    """
    执行单卡训练 (用于对比)
    
    Args:
        model: 待训练模型
        train_dataset: 训练数据集
        epochs: 训练轮数
        max_steps: 最大步数
        rank: 进程 rank (用于日志)
        monitor_memory: 是否监控显存
    
    Returns:
        loss_list, grad_dict, memory_list (if monitor_memory=True)
    """
    # 创建优化器
    trainable_params = list(filter(lambda p: p.requires_grad, model.get_parameters()))
    optimizer = nn.Adam(trainable_params, learning_rate=1e-4)
    #optimizer = nn.Adam(model.trainable_params(), learning_rate=1e-4)
    
    # 定义训练 step - 单卡训练使用 value_and_grad
    def train_step(inputs, labels):
        def _zero_grad(params) -> None:
            for param in params:
                param.grad = None
        _zero_grad([param for param in model.trainable_params()])
        loss = model(inputs, labels)
        with SkipDTensorDispatch():
          loss.backward()
          grads = tuple(p.grad for p in model.trainable_params())
        with _no_grad():
            optimizer(grads)
        return loss
    loss_list = []
    grad_dict = {}
    memory_list = []
    step = 0
    if rank == 0 and monitor_memory:
        logger.warning(f'\nStarting single-card training for {max_steps} steps...')
    for _ in range(epochs):
        for _, (input_ids, labels) in enumerate(train_dataset):
            # Record memory usage before step
            if monitor_memory:
                mem_before = get_memory_usage()
                if rank == 0:
                    print_memory_usage(f'Step {step+1} - Before', mem_before)
            loss = train_step(input_ids, labels)
            # Record memory usage after step
            if monitor_memory:
                mem_after = get_memory_usage()
                if rank == 0:
                    print_memory_usage(f'Step {step+1} - After', mem_after)
                memory_list.append((mem_before, mem_after))
            loss_list.append(loss.item())
            # 打印 loss (仅 rank 0)
            if rank == 0:
                logger.warning(f'  Step {step+1}: Loss = {loss.item():.6f}')
            # 保存梯度用于对比
            if step == 0:
                for i, param in enumerate(optimizer.parameters):
                    grad_dict[f'layer_{i}'] = param.grad.asnumpy() if param.grad is not None else None
            step += 1
            if step >= max_steps:
                break
        if step >= max_steps:
            break
    del optimizer
    if monitor_memory:
        return loss_list, grad_dict, memory_list
    return loss_list, grad_dict


def policy_fn(ctx, op, *args, **kwargs):
    return CheckpointPolicy.MUST_RECOMPUTE

def apply_fully_shard(model, mesh=None, reshard_after_forward=True,
                      apply_root_fs=True, apply_to_layers=True,
                      comm_fusion_zero_copy=False, mp_policy=None,
                      replicate_params=None, ignored_params=None,
                      offload_policy=None, outer_comm_fusion=False,
                      recompute_policy=None):
    """
    对模型应用 fully_shard 配置
    
    Args:
        model: 待应用 fully_shard 的模型
        mesh: 设备网格 (None 表示 ZeRO-1 模式)
        reshard_after_forward: 是否 ZeRO-3 模式 (False 表示 ZeRO-1/ZeRO-2)
        apply_root_fs: 是否对整网应用 fully_shard
        apply_to_layers: 是否对每个 layer 单独应用 (特殊用例)
        comm_fusion_zero_copy: 是否启用 comm_fusion_zero_copy (默认 False)
        mp_policy: MixedPrecisionPolicy 配置 (默认 None)
        replicate_params: 复制参数集合 (lambda m: {m.embedding} 或 None 表示全分片)
        ignored_params: 忽略参数集合 (lambda m: {m.w1} 或 None 表示全部分片)
        offload_policy: CPU offload 策略 (默认 None)
        outer_comm_fusion: 最外层 comm_fusion 配置 (默认 False)
        recompute_policy: 重计算策略 ("MUST_RECOMPUTE", "MUST_SWAP", etc., 默认 None)
    """
    # ZeRO-1: mesh=None,只对模型整体应用
    if mesh is None:
        # mp_policy 为 None 时不传递该参数
        if mp_policy is None:
            fully_shard(model, reshard_after_forward=reshard_after_forward, 
                       comm_fusion_zero_copy=comm_fusion_zero_copy,
                       replicate_params=replicate_params, ignored_params=ignored_params,
                       offload_policy=offload_policy)
        else:
            fully_shard(model, reshard_after_forward=reshard_after_forward, 
                       comm_fusion_zero_copy=comm_fusion_zero_copy, mp_policy=mp_policy,
                       replicate_params=replicate_params, ignored_params=ignored_params,
                       offload_policy=offload_policy)
    else:
        # ZeRO-2/ZeRO-3: mesh 不为 None
        if apply_to_layers:
            # 对每个 layer 应用 fully_shard
            for idx, layer in enumerate(model.layers):
                if mp_policy is None:
                    fully_shard(layer, mesh=mesh, reshard_after_forward=reshard_after_forward, 
                               comm_fusion_zero_copy=comm_fusion_zero_copy,
                               replicate_params=replicate_params, ignored_params=ignored_params,
                               offload_policy=offload_policy)
                else:
                    fully_shard(layer, mesh=mesh, reshard_after_forward=reshard_after_forward, 
                               comm_fusion_zero_copy=comm_fusion_zero_copy, mp_policy=mp_policy,
                               replicate_params=replicate_params, ignored_params=ignored_params,
                               offload_policy=offload_policy)
        
        # 对整个模型应用 fully_shard,最外层可配置 comm_fusion
        if apply_root_fs:
            if mp_policy is None:
                fully_shard(model, mesh=mesh, reshard_after_forward=reshard_after_forward, 
                           comm_fusion_zero_copy=comm_fusion_zero_copy,
                           replicate_params=replicate_params, ignored_params=ignored_params,
                           offload_policy=offload_policy)
            else:
                fully_shard(model, mesh=mesh, reshard_after_forward=reshard_after_forward, 
                           comm_fusion_zero_copy=comm_fusion_zero_copy,
                           mp_policy=mp_policy,
                           replicate_params=replicate_params, ignored_params=ignored_params,
                           offload_policy=offload_policy)


def apply_fs_to_model_with_layer_override(model, mesh, reshard_after_forward=True,
                                           mp_policy=None, target_layer_idx=0,
                                           layer_comm_fusion_zero_copy=True,
                                           layer_shard_placement_fn=None):
    """
    对模型应用 fully_shard,并对指定 layer 应用特殊配置
    用于 test_fully_shard_ms_check_002 等需要覆盖某 layer 配置的用例
    Args:
        model: 待应用 fully_shard 的模型
        mesh: 设备网格
        reshard_after_forward: 是否 ZeRO-3 模式
        mp_policy: MixedPrecisionPolicy 配置
        target_layer_idx: 目标 layer 索引 (对该 layer 应用特殊配置)
        layer_comm_fusion_zero_copy: 目标 layer 的 comm_fusion_zero_copy 配置
        layer_shard_placement_fn: 目标 layer 的 shard_placement_fn 配置
    """
    # 对每个 layer 应用 fully_shard
    for idx, layer in enumerate(model.layers):
        if idx == target_layer_idx and layer_shard_placement_fn is not None:
            # 对目标 layer 应用特殊配置
            fully_shard(
                layer, 
                mesh=mesh, 
                reshard_after_forward=reshard_after_forward,
                mp_policy=mp_policy,
                comm_fusion_zero_copy=layer_comm_fusion_zero_copy,
                shard_placement_fn=layer_shard_placement_fn
            )
        else:
            # 其他 layer 使用默认配置
            fully_shard(
                layer, 
                mesh=mesh, 
                reshard_after_forward=reshard_after_forward,
                mp_policy=mp_policy
            )
    # 对整个模型应用 fully_shard
    fully_shard(
        model, 
        mesh=mesh, 
        reshard_after_forward=reshard_after_forward,
        mp_policy=mp_policy
    )
    model.set_requires_gradient_sync(True)


def run_train_with_fully_shard(model, train_dataset, epochs=1, max_steps=10, 
                                mesh=None, reshard_after_forward=True,
                                clip_norm=False, apply_fs=True, apply_to_layers=True,
                                comm_fusion_zero_copy=False, mp_policy=None,
                                replicate_params=None, ignored_params=None,
                                offload_policy=None, outer_comm_fusion=False,
                                recompute_policy=None,  # "MUST_RECOMPUTE", "MUST_SWAP", etc.
                                set_gradient_sync_at_step=None,
                                set_gradient_sync_value=True,  # True=启用梯度同步,False=关闭
                                set_reshard_after_forward_at_step=None,
                                set_is_last_backward_at_step=None,
                                set_reshard_after_backward=None,
                                rank=0, monitor_memory=True,
                                loss_scale_manager=None,
                                set_gradient_sync_at_first=False):
    """
    执行 fully_shard 训练
    
    Args:
        model: 待训练模型
        train_dataset: 训练数据集
        epochs: 训练轮数
        max_steps: 最大步数
        mesh: 设备网格 (None 表示 ZeRO-1 模式)
        reshard_after_forward: 是否 ZeRO-3 模式 (False 表示 ZeRO-1/ZeRO-2)
        clip_norm: 是否梯度裁剪
        apply_fs: 是否应用 fully_shard
        apply_to_layers: 是否对每个 layer 单独应用 (特殊用例)
        comm_fusion_zero_copy: 是否启用 comm_fusion_zero_copy (默认 False)
        mp_policy: MixedPrecisionPolicy 配置 (默认 None)
        replicate_params: 复制参数集合 (lambda m: {m.embedding} 或 None 表示全分片)
        ignored_params: 忽略参数集合 (lambda m: {m.w1} 或 None 表示全部分片)
        offload_policy: CPU offload 策略 (默认 None)
        outer_comm_fusion: 最外层 comm_fusion 配置 (默认 False)
        recompute_policy: 重计算策略 ("MUST_RECOMPUTE", "MUST_SWAP", etc., 默认 None)
        set_gradient_sync_at_step: 在第 N 个 step 调用 set_requires_gradient_sync (None 表示训练前调用)
        set_gradient_sync_value: set_requires_gradient_sync 的值 (True=启用,False=关闭,默认 True)
        set_reshard_after_forward_at_step: 在第 N 个 step 后调用 set_reshard_after_forward(False) (None 表示不切换)
        set_is_last_backward_at_step: 在第 N 个 step 后调用 set_is_last_backward(True) (None 表示不设置)
        set_reshard_after_backward: 设置 reshard_after_backward 值 (None 表示使用默认值)
        rank: 进程 rank (用于日志)
        monitor_memory: 是否监控显存
    Returns:
        loss_list, grad_dict, memory_list (if monitor_memory=True)
    """
    
    # 应用 fully_shard (普通用例:在训练前应用)
    if apply_fs:
        apply_fully_shard(
            model=model,
            mesh=mesh,
            reshard_after_forward=reshard_after_forward,
            apply_root_fs=True,
            apply_to_layers=apply_to_layers,
            comm_fusion_zero_copy=comm_fusion_zero_copy,
            mp_policy=mp_policy,
            replicate_params=replicate_params,
            ignored_params=ignored_params,
            offload_policy=offload_policy,
            outer_comm_fusion=outer_comm_fusion,
            recompute_policy=recompute_policy
        )
    if set_reshard_after_backward is not None:
        model.set_reshard_after_backward(set_reshard_after_backward)
        logger.warning(f'set_reshard_after_backward: {set_reshard_after_backward}')
    if apply_fs and set_gradient_sync_at_step is None and set_gradient_sync_at_first:
        model.set_requires_gradient_sync(set_gradient_sync_value)
        logger.warning(f'set_requires_gradient_sync: {set_gradient_sync_value}')
    trainable_params = list(filter(lambda p: p.requires_grad, model.get_parameters()))
    optimizer = nn.Adam(trainable_params, learning_rate=1e-4)


    def train_step(inputs, labels):
        for param in trainable_params:
            param.grad = None
        loss = model(inputs, labels)
        with SkipDTensorDispatch():
          loss.backward()
          if loss_scale_manager is None:
              grads = tuple(p.grad for p in model.trainable_params())
              with _no_grad():
                optimizer(grads)
          else:
              loss_scale = loss_scale_manager.get_loss_scale()
              scaled_grads = tuple(p.grad for p in model.trainable_params())
              grad_finite = all(ops.isfinite(g).all().asnumpy() for g in scaled_grads)
              overflow = not grad_finite
              if grad_finite:
                  with _no_grad():
                        optimizer(scaled_grads)
              loss_scale_manager.update_loss_scale(overflow)
        return loss


    loss_list = []
    grad_dict = {}
    memory_list = []  # [(before, after), ...]
    step = 0
    gradient_sync_set = False  # 标记是否已调用 set_requires_gradient_sync
    reshard_switched = False  # 标记是否已切换 reshard_after_forward
    is_last_backward_set = False  # 标记是否已设置 is_last_backward
    if rank == 0 and monitor_memory:
        logger.warning(f'\nStarting fully_shard training for {max_steps} steps...')
        logger.warning('  Note: Recording memory from both npu-smi and runtime.memory_allocated()')
        if set_gradient_sync_at_step:
            logger.warning(f'  Will call set_requires_gradient_sync(True) at step {set_gradient_sync_at_step}')
        if set_reshard_after_forward_at_step:
            logger.warning(f'  Will call set_reshard_after_forward(False) after step {set_reshard_after_forward_at_step}')
        if set_is_last_backward_at_step:
            logger.warning(f'  Will call set_is_last_backward(True) after step {set_is_last_backward_at_step}')
    for _ in range(epochs):
        for _, (input_ids, labels) in enumerate(train_dataset):
            # 在指定 step 调用 set_requires_gradient_sync(value)
            if set_gradient_sync_at_step is not None and step + 1 == set_gradient_sync_at_step and not gradient_sync_set:
                model.set_requires_gradient_sync(set_gradient_sync_value)
                if rank == 0:
                    logger.warning(f'  Step {step+1}: Called set_requires_gradient_sync({set_gradient_sync_value})')
                gradient_sync_set = True

            # 在指定 step 后切换 reshard_after_forward (ZeRO-3 -> ZeRO-2)
            if set_reshard_after_forward_at_step is not None and step == set_reshard_after_forward_at_step and not reshard_switched:
                model.set_reshard_after_forward(False)
                if rank == 0:
                    logger.warning(f'  Step {step}: Called set_reshard_after_forward(False) - switching from ZeRO-3 to ZeRO-2')
                reshard_switched = True
            # 在指定 step 后设置 is_last_backward=True (通知最后一步反向,触发资源清理)
            if set_is_last_backward_at_step is not None and step == set_is_last_backward_at_step and not is_last_backward_set:
                model.set_is_last_backward(True)
                if rank == 0:
                    logger.warning(f'  Step {step}: Called set_is_last_backward(True) - signaling last backward pass')
                is_last_backward_set = True
            # Record memory usage before step (both methods)
            if monitor_memory:
                mem_before = get_memory_usage(rank=rank)
                if rank == 0:
                    print_memory_usage(f'Step {step+1} - Before', mem_before)
            loss = train_step(input_ids, labels)
            # Record memory usage after step (both methods)
            if monitor_memory:
                mem_after = get_memory_usage(rank=rank)
                if rank == 0:
                    print_memory_usage(f'Step {step+1} - After', mem_after)
                memory_list.append((mem_before, mem_after))
            loss_list.append(loss.item())
            # 打印 loss (仅 rank 0)
            if rank == 0:
                logger.warning(f'  Step {step+1}: Loss = {loss.item():.6f}')
            # 保存梯度用于对比 (从 optimizer 参数获取)
            if step == 0:
                for i, param in enumerate(optimizer.parameters):
                    grad_dict[f'layer_{i}'] = param.grad.asnumpy() if param.grad is not None else None
            step += 1
            if step >= max_steps:
                break
        if step >= max_steps:
            break
    if monitor_memory:
        return loss_list, grad_dict, memory_list
    return loss_list, grad_dict


def sync_weights(single_model, fs_model):
    """
    同步单卡模型和 fully_shard 模型的权重
    Args:
        single_model: 单卡参考模型
        fs_model: fully_shard 模型
    """
    params_single = single_model.parameters_dict()
    params_fs = fs_model.parameters_dict()
    rank = get_rank()
    for key in params_fs:
        params_fs[key].set_data(params_single[key])


# ============================================================================
# 显存监控函数
# ============================================================================

def get_memory_usage_npu_smi(rank=None):
    """
    Get current NPU memory usage in MB by parsing npu-smi info output.
    Automatically gets the current rank from the communication environment.
    Note:
        Line formula: tail -n (16 - rank * 2)
        - rank=0: tail -n 16 | head -n 1
        - rank=1: tail -n 14 | head -n 1
        - rank=2: tail -n 12 | head -n 1
        - rank=3: tail -n 10 | head -n 1
        - rank=4: tail -n 8  | head -n 1
        - rank=5: tail -n 6  | head -n 1
        - rank=6: tail -n 4  | head -n 1
        - rank=7: tail -n 2  | head -n 1
    """
    import subprocess
    try:
        if rank is None:
            rank = get_rank()
        line_num = 16 - rank * 2
        cmd = f'npu-smi info | tail -n {line_num} | head -n 1 | awk -F"|" \'{{print $5}}\''
        result = subprocess.run(
            cmd,
            shell=True,
            capture_output=True,
            text=True,
            timeout=5
        )
        output = result.stdout.strip()
        if output and output.isdigit():
            return float(output)
        return 0.0
    except Exception as e:
        logger.warning(f'Warning: Failed to get memory usage from npu-smi (rank={rank}): {e}')
        return 0.0


def get_memory_usage_runtime():
    """Get current NPU memory allocated in MB using mindspore.runtime.memory_allocated()."""
    try:
        from mindspore import runtime
        # memory_allocated() returns bytes, convert to MB
        allocated_bytes = runtime.memory_allocated()
        allocated_mb = allocated_bytes / (1024 * 1024)
        return allocated_mb
    except Exception as e:
        logger.warning(f'Warning: Failed to get memory usage from runtime: {e}')
        return 0.0


def get_memory_usage(rank=None):
    """Get memory usage from both methods and return as tuple (npu_smi, runtime)."""
    npu_smi = get_memory_usage_npu_smi(rank)
    runtime_mb = get_memory_usage_runtime()
    return npu_smi, runtime_mb


def print_memory_usage(label, mem_usage):
    """Print memory usage from both methods."""
    npu_smi, runtime = mem_usage
    logger.warning(f'  {label}: npu-smi={npu_smi:.2f} MB, runtime={runtime:.2f} MB (diff={abs(npu_smi - runtime):.2f} MB)')


def print_memory_summary(test_name, memory_list, rank=0):
    """
    打印显存使用汇总 (共用方法)
    
    Args:
        test_name: 测试用例名称
        memory_list: [(before, after), ...] 格式的显存列表
        rank: 进程 rank (仅 rank 0 打印)
    """
    if rank != 0:
        return
    if not memory_list:
        logger.warning(f'No memory data to summarize for {test_name}')
        return
    logger.warning('\n' + '='*70)
    logger.warning(f'Memory Usage Summary ({test_name})')
    logger.warning('='*70)
    # 计算显存变化统计
    mem_before_list = [m[0] for m in memory_list]
    mem_after_list = [m[1] for m in memory_list]
    # 计算平均显存使用
    avg_before_npu_smi = np.mean([m[0] for m in mem_before_list])
    avg_before_runtime = np.mean([m[1] for m in mem_before_list])
    avg_after_npu_smi = np.mean([m[0] for m in mem_after_list])
    avg_after_runtime = np.mean([m[1] for m in mem_after_list])
    logger.warning(f'\nAverage Memory Usage:')
    logger.warning(f'  Before step:  npu-smi={avg_before_npu_smi:.2f} MB, runtime={avg_before_runtime:.2f} MB')
    logger.warning(f'  After step:   npu-smi={avg_after_npu_smi:.2f} MB, runtime={avg_after_runtime:.2f} MB')
    logger.warning(f'  Avg Delta:    npu-smi={avg_after_npu_smi - avg_before_npu_smi:+.2f} MB, runtime={avg_after_runtime - avg_before_runtime:+.2f} MB')
    # 计算每 step 显存变化
    logger.warning(f'\nPer-step Memory Changes:')
    for i, ((before_npu, before_rt), (after_npu, after_rt)) in enumerate(memory_list):
        delta_npu = after_npu - before_npu
        delta_rt = after_rt - before_rt
        logger.warning(f'  Step {i+1}: Δ={delta_npu:+.2f} MB (npu-smi), {delta_rt:+.2f} MB (runtime)')
    logger.warning('='*70)


def compare_loss(single_loss_list, fs_loss_list, test_name='', rank=0, tol=0.1):
    """
    对比单卡和 fully_shard 的 loss (共用方法)
    Args:
        single_loss_list: 单卡训练 loss 列表
        fs_loss_list: fully_shard 训练 loss 列表
        test_name: 测试用例名称 (可选)
        rank: 进程 rank (仅 rank 0 打印)
        tol: 容忍误差 (默认 0.1)
    Returns:
        max_diff: 最大 loss 差异
    """
    if rank != 0:
        return 0.0
    if not single_loss_list or not fs_loss_list:
        logger.warning(f'Cannot compare loss: empty loss list(s)')
        return float('inf')
    max_len = min(len(single_loss_list), len(fs_loss_list))
    logger.warning('\n' + '='*70)
    if test_name:
        logger.warning(f'Loss Comparison ({test_name})')
    else:
        logger.warning('Loss Comparison (Single-card vs fully_shard)')
    logger.warning('='*70)
    max_diff = 0.0
    for i in range(max_len):
        single_loss = single_loss_list[i]
        fs_loss = fs_loss_list[i]
        diff = abs(single_loss - fs_loss)
        max_diff = max(max_diff, diff)
        logger.warning(f'  Step {i+1}: Single={single_loss:.6f}, FS={fs_loss:.6f}, Diff={diff:.6f}')
    logger.warning(f'\nFinal Loss:')
    logger.warning(f'  Single-card: {single_loss_list[-1]:.6f}')
    logger.warning(f'  fully_shard: {fs_loss_list[-1]:.6f}')
    logger.warning(f'  Max Diff: {max_diff:.6f}')
    logger.warning(f'  Final Diff: {abs(single_loss_list[-1] - fs_loss_list[-1]):.6f}')
    logger.warning('='*70)
    assert max_diff < tol, f"Loss difference too large: max_diff={max_diff}, tol={tol}"
    return max_diff

likedislike
MengXY107
MengXY107成员
6月12日 评论:
def test_fully_shard_ms_check_007():
    """
    只针对每个layer配置不同的fully_shard,不对整个net配置fully_shard
    ZeRO-2 基础训练 + replicate_params={module.embedding}
    
    TEST_SUMMARY: mesh=(8,), reshard_after_forward=False, replicate_params 复制 embedding 参数
    
    Design Description:
    fully_shard 单模块 + ZeRO-2 基础训练,mesh=(8,),设置 replicate_params={module.embedding}
    
    Test Steps:
    1. 构建测试网络
    2. mesh=init_device_mesh(mesh=(8,))
       fully_shard(model, mesh=mesh, reshard_after_forward=False, replicate_params={module.embedding})
    3. 设置 `set_requires_gradient_sync(True)`
    4. 执行 10 step 训练
    
    Expected Result:
    1. 精度有问题,不 hang
    """
    rank = get_rank()
    world_size = get_group_size()

    epochs = 1
    max_steps = 10
    batch_size = 2
    seq_len = 32
    vocab_size = 1000

    model_config = get_model_config(num_layers=2, vocab_size=vocab_size, hidden_size=256, seq_len=seq_len)
    
    # 创建单卡模型和 fully_shard 模型
    single_model = PanGuMoETransformerSingle(model_config)
    fs_model = PanGuMoETransformerFSInit(model_config)
    
    # 同步权重
    sync_weights(single_model, fs_model)
    
    # ✅ 获取需要复制的参数集合 (embedding 权重)
    # replicate_params 会导致这些参数在所有卡上复制,不分片
    # 注意:MindSpore 中 Embedding 层的参数名是 embedding_table 而不是 weight
    # 参考:E:\mindspore_ci\MindSporeTest\parallel\fully_shard\test_fully_shard_zero2_replicate.py
    replicate_params_set = None
    # 直接访问 fs_model.embedding (PanGuMoETransformer 结构)
    if hasattr(fs_model, 'embedding'):
        embedding_layer = fs_model.embedding
        if hasattr(embedding_layer, 'embedding_table'):
            replicate_params_set = {embedding_layer.embedding_table}
            logger.warning(f'\ntest_fully_shard_ms_check_007: Found embedding parameter: {replicate_params_set}')
        else:
            logger.warning('\ntest_fully_shard_ms_check_007: Embedding layer has no embedding_table attribute')
    else:
        logger.warning('\ntest_fully_shard_ms_check_007: Model has no embedding attribute')
    
    # 生成数据集
    all_data = list(generator_dataset(size=100, batch_size=batch_size, seq_len=seq_len, vocab_size=vocab_size))
    train_dataset_1 = GeneratorDataset(all_data[:], ["data", "label"], shuffle=False)
    train_dataset_2 = GeneratorDataset(all_data[:], ["data", "label"], shuffle=False)
    
    # 初始化 mesh: (8,) 单维 mesh (8 卡全用)
    mesh = init_device_mesh(device_type="npu", mesh_shape=(8,), mesh_dim_names=("dp",))
    
    # 执行单卡训练并监控显存
    single_loss_list, single_grad_dict, single_memory_list = run_train_single(
        single_model, train_dataset_1, epochs=epochs, max_steps=max_steps,
        rank=rank, monitor_memory=True
    )
    
    apply_fully_shard(fs_model, mesh=mesh, reshard_after_forward=False, apply_root_fs=False, apply_to_layers=True,
                      comm_fusion_zero_copy=False, replicate_params=replicate_params_set)
    # 执行 fully_shard 训练 (ZeRO-2 模式 + replicate_params 复制 embedding)
    # 预期结果:精度有问题,但不 hang
    loss_list, grad_dict, memory_list = run_train_with_fully_shard(
        fs_model, train_dataset_2, epochs=epochs, max_steps=max_steps,
        mesh=mesh,
        reshard_after_forward=False,  # ZeRO-2 模式
        apply_fs=False,
        apply_to_layers=False,
        replicate_params=replicate_params_set,  # ✅ 复制 embedding 参数,不分片
        rank=rank,
        monitor_memory=True
    )
    
    # 验证 loss 数据 (只验证不 hang,不验证精度)
    assert len(loss_list) == max_steps
    assert all(np.isfinite(l) for l in loss_list)
    assert len(single_loss_list) == max_steps
    assert all(np.isfinite(l) for l in single_loss_list)
    
    # 验证显存监控数据
    assert len(memory_list) == max_steps, f"Expected {max_steps} memory records, got {len(memory_list)}"
    assert len(single_memory_list) == max_steps, f"Expected {max_steps} single-card memory records, got {len(single_memory_list)}"
    
    # 打印显存使用汇总
    print_memory_summary('test_fully_shard_ms_check_007 - fully_shard', memory_list, rank=rank)
    print_memory_summary('test_fully_shard_ms_check_007 - single_card', single_memory_list, rank=rank)
    
    # 注意:此用例预期精度有问题,不做严格的 loss 对比
    # 只验证训练能完成,不 hang
    logger.warning('\ntest_fully_shard_ms_check_007: Expected accuracy degradation with replicate_params')

likedislike