跳过正文
  1. 从模型结构到真实系统的项目沉淀。/

CodeLab-LLaMA2 / 星语 MoE:大模型训练实践

目录

CodeLab-LLaMA2 / 星语 MoE 以“把大模型核心链路跑通”为目标,围绕 LLaMA2 的结构、训练和应用做系统化拆解。

  • 从零实现 Attention、FFN、RMSNorm、Rotary Embedding 等核心模块。
  • 支持预训练、SFT、LoRA 微调和推理流程,覆盖从结构实现到训练调试的完整链路。
  • 延伸到 RAG、Agent 和 MoE-FFN 替换策略,帮助理解大模型系统如何从理论进入应用。

项目链接:GitHub,在线体验:ModelScope

项目详解
#

很高兴,前面的章节已经让我们完整的学习到了如何一步一步构建大模型,如何预训练,如何高效微调。

这章,打算以最近特别火的MoE架构,来重新设计模型,我打算取名为星语

什么是MoE架构?
#

MoE 是一种“稀疏激活”的神经网络架构:把模型拆成许多专门的子网络(称为 experts),然后用一个 gating(门/路由)网络根据输入选择其中一小部分专家来处理该输入。这样模型总参数可以非常大,但每次前向只激活少数专家,计算成本/延迟接近传统模型,从而实现“用常量计算成本得到更多参数规模”的目标。

我们可以看看论文中的架构图:

Image

可以很明显的看到,就是把原来的MLP(FFN)替换为图中的MoE组件。

对于MoE组件来说,可以简单的看做是原来大的FFN划分为多个小的并行的FFN。那么这些小的FFN就是所谓的专家。下面拆解一下MoE核心的组件

  • 专家(experts):并行的子网络(通常是 FFN/前馈层的重复体)。

  • 门控/路由(gating):对输入 x 输出权重或直接选择 top-k 个专家(如 top-1/top-2)。

  • 稀疏激活:每个 token/样本只经过少数专家(比如 1 或 2 个),保证计算效率。

  • 辅助损失(auxiliary loss):常加入负载均衡损失,避免所有请求集中到少数专家造成“拥塞”

对于这个架构来说是非常简单的。但是为什么要这样设计的,为什么可以设计?

“为什么能用多个小 FFN(专家)去替换一个大 FFN,还能变得更强、更有效?”

为什么MoE更加高效?
#

“为什么能用多个小 FFN(专家)去替换一个大 FFN,还能变得更强、更有效?”

原始 Transformer 里的 FFN 是什么
#

在标准 Transformer(如 GPT、BERT)中,每一层都有两部分:

  1. 自注意力(Self-Attention)

  2. 前馈网络(Feed Forward Network, FFN)

FFN 的形式通常是:

$ \text{FFN}(x) = W_2 , \sigma(W_1 x + b_1) + b_2$

其中 ($\sigma$) 是非线性激活(ReLU / GELU)。 这个 FFN 是 对每个 token 独立应用 的,即没有跨 token 的交互,只是逐 token 的变换。

维度上一般是:

  • 输入维度 ( $d_\text{model}$ )

  • 隐藏维度 ( $d_\text{ff} = 4d_\text{model}$ )(比如 4096 → 16384 → 4096)

所以它的参数主要是 ( $2 \times d_\text{model} \times d_\text{ff}$ ),这是 Transformer 参数的主要来源之一。


MoE 是怎么“替换” FFN 的
#

MoE 层做的,就是:

把原来唯一的 FFN 换成 N 个并行的专家(FFN),并用一个 gating 网络 决定每个 token 应该送到哪些专家。

即:

$ \text{MoE}(x) = \sum_{i=1}^{N} g_i(x) \cdot \text{FFN}_i(x)$

其中:

  • ( $\text{FFN}_i$ ) 是第 i 个专家(结构和原 FFN 一样,但参数独立)

  • ( $g_i(x)$ ) 是门控权重(由一个小网络根据 x 计算,常是 softmax 输出的 top-k 稀疏选择)

通常只选 top-1 或 top-2 专家,也就是说:

  • 每个 token 只会经过少量(1~2个)专家;

  • 其余专家不参与该 token 的计算(稀疏激活)。


为什么这样做“有效果”(即更强、更省算力)
#

参数量大,但每次计算量几乎不变

  • 假设原 FFN 有 100M 参数;

  • 用 16 个专家,每个专家参数量也相同 → 总参数 = 1.6B;

  • 但每个 token 只用其中 1~2 个专家;

  • 所以推理计算量 ≈ 100M × (1 or 2) / 16 = 原来的 1×~2×,几乎没变。

实现了“稀疏扩展”:能增加模型容量但不增加计算量。


每个专家可以“专门化”
#

在训练中,gating 会学会根据输入特征(或语义)把不同 token 路由到不同专家。

结果是:

  • 不同专家负责不同的任务分布;

  • 有的专家擅长数学、有的擅长对话、有的擅长编程、有的擅长多语言等。

这种“自动分工”让模型整体更强,像一个协作团队。

这其实让模型具备了**条件计算(Conditional Computation)**的能力:

不同输入触发不同参数子集,而不是“所有参数都做相同的事”。


保留了 Transformer 的结构优势
#

MoE 只是替换 FFN 部分,而注意力部分不变。

这意味着:

  • 全局上下文建模(attention)依旧;

  • FFN 部分获得了“多专家并行”的灵活性;

  • 在训练时,MoE 层可以并行分布在不同设备上(称为 专家并行 expert parallelism),非常适合大规模训练。

在达到相同的训练损失的情况下,MoE架构是花费最少的。这个通过实验可以证明。

Image
Image
Image

存在问题
#

Image

负载不均衡(Load Imbalance)
#

在训练时,gating 网络可能倾向于选择某几个专家,导致:

  • 某些专家负载过高(被频繁调用);

  • 其他专家几乎没被用(梯度更新太少)。

这会带来:

  • 训练不稳定;

  • 专家崩塌(collapse);

  • GPU 之间工作量差异大,效率下降。

缓解负载不均衡的主要机制是 auxiliary loss(辅助负载平衡损失)

Image

MoE代码
#

代码地址:Code/MoEBlock.py

import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from transformers import PretrainedConfig


# ============================================================
# 模型配置类
# ============================================================
class ModelConfig(PretrainedConfig):
    *"""*
*    模型的全局配置参数类,用于统一管理 Transformer + MoE 层的结构参数。*
*    继承自 HuggingFace 的 PretrainedConfig,方便保存与加载。*
*    """*
*    *model_type = "XY-MoE"

    def __init__(self,
                 dim: int = 1024,           # 模型主维度(token embedding 的维度)
                 n_layers: int = 16,        # Transformer 层数
                 n_heads: int = 16,         # 多头注意力的头数
                 n_kv_heads: int = 8,       # key/value 头数
                 vocab_size: int = 6144,    # 词表大小
                 hidden_dim: int = None,    # FFN 隐藏层维度(若为 None 会自动计算)
                 multiple_of: int = 64,     # FFN 隐藏层对齐倍数
                 norm_eps: float = 1e-5,    # LayerNorm 的 epsilon
                 max_seq_len: int = 256,    # 最大序列长度
                 dropout: float = 0.0,      # Dropout 概率
                 flash_attn: bool = True,   # 是否启用 Flash Attention

                 ########################################################
                 # 下方为 MoE(Mixture of Experts)配置参数
                 ########################################################
                 num_experts_per_tok: int = 1,   # 每个 token 选择的专家数(top-k)
                 n_routed_experts: int = 4,      # MoE 专家总数
                 n_shared_experts: int = 1,      # “共享”专家数(dense FFN,不经过 gating)
                 gate_hidden_size: int = None,   # gate 输入维度(若 None 默认等于 dim)
                 aux_loss_alpha: float = 1e-2,   # MoE 辅助损失权重,用于均衡专家负载
                 seq_aux: bool = False,          # 辅助损失是否按序列统计
                 noise_std: float = 1.0,         # gating logits 噪声强度
                 norm_topk_prob: bool = True,    # 是否归一化 top-k 概率
                 capacity_factor: float = 1.25,  # 每个专家的 token 容量上限因子
                 **kwargs,
                 ):
        *"""*
*        初始化模型参数。这里主要保存为实例属性,供下游模块使用。*
*        """*
*        *# Transformer 主体参数
        self.dim = dim
        self.n_layers = n_layers
        self.n_heads = n_heads
        self.n_kv_heads = n_kv_heads
        self.vocab_size = vocab_size
        self.hidden_dim = hidden_dim
        self.multiple_of = multiple_of
        self.norm_eps = norm_eps
        self.max_seq_len = max_seq_len
        self.dropout = dropout
        self.flash_attn = flash_attn

        # MoE 参数
        self.num_experts_per_tok = int(num_experts_per_tok)
        self.n_routed_experts = int(n_routed_experts)
        self.n_shared_experts = int(n_shared_experts)
        self.gate_hidden_size = gate_hidden_size if gate_hidden_size is not None else dim
        self.aux_loss_alpha = aux_loss_alpha
        self.seq_aux = seq_aux
        self.noise_std = noise_std if noise_std is not None else 0.0
        self.norm_topk_prob = bool(norm_topk_prob)
        self.capacity_factor = float(capacity_factor)

        super().__init__(**kwargs)


# ============================================================
# SwiGLU 前馈网络(Feed-Forward Network)
# ============================================================
class FFN(nn.Module):
    *"""*
*    SwiGLU 变体的前馈网络层。*
*    结构:*
*        x -> fc_gate -> SiLU() 激活*
*        x -> fc_up -> 线性映射*
*        逐元素相乘 -> fc_down -> Dropout*
*    输出维度与输入相同(残差连接用)。*
*    """*

*    *def __init__(self, dim: int, hidden_dim: int = None, multiple_of: int = 256, dropout: float = 0.0):
        super().__init__()

        # 若未显式指定 hidden_dim,则自动计算(与 LLaMA 一致)
        if hidden_dim is None:
            hidden_dim = 4 * dim
            hidden_dim = int(2 * hidden_dim / 3)  # 约 2.67x dim
            hidden_dim = multiple_of * ((hidden_dim + multiple_of - 1) // multiple_of)

        # 线性层定义
        self.fc_gate = nn.Linear(dim, hidden_dim, bias=False)  # 激活分支
        self.fc_up = nn.Linear(dim, hidden_dim, bias=False)    # 门控分支
        self.fc_down = nn.Linear(hidden_dim, dim, bias=False)  # 输出投影
        self.dropout = nn.Dropout(dropout)

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        *"""*
*        x: [batch, seq_len, dim]*
*        return: [batch, seq_len, dim]*
*        """*
*        *gated = F.silu(self.fc_gate(x))  # 激活通道
        up = self.fc_up(x)               # 线性通道
        hidden = gated * up              # SwiGLU 核心操作
        out = self.fc_down(hidden)       # 映射回原维度
        out = self.dropout(out)
        return out


# ============================================================
# MoE 的 Gate 模块
# ============================================================
class MoEGate(nn.Module):
    *"""*
*    Gating 网络:负责为每个 token 计算其对应的专家分配概率。*

*    输入:*
*        x: [B, L, D] (token embedding)*

*    输出:*
*        topk_idx:    [B*L, K]  每个 token 对应的 top-k 专家索引*
*        topk_weight: [B*L, K]  对应的 top-k 概率权重*
*        aux_loss:    标量,用于负载均衡的辅助损失*
*    """*

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

        # ---------------- 基本参数 ----------------
        self.top_k = config.num_experts_per_tok         # 每个 token 选择几个专家
        self.n_experts = config.n_routed_experts        # 专家总数
        self.gating_dim = config.gate_hidden_size       # gate 的输入维度
        self.alpha = config.aux_loss_alpha              # 辅助损失权重
        self.seq_aux = config.seq_aux
        self.noise_std = config.noise_std
        self.norm_topk_prob = config.norm_topk_prob

        # ---------------- 权重定义 ----------------
        # 线性层:输入 gating_dim → 输出 n_experts
        # 输出即每个专家的 logits(未归一化分数)
        self.linear = nn.Linear(self.gating_dim, self.n_experts, bias=False)

        # 初始化参数
        self.reset_parameters()

    def reset_parameters(self):
        *"""使用 Kaiming 初始化线性层权重。"""*
*        *nn.init.kaiming_uniform_(self.linear.weight, a=math.sqrt(5))

    def forward(self, x: torch.Tensor):
        *"""*
*        Args:*
*            x: [B, L, D] token 表示*
*        Returns:*
*            topk_idx, topk_weight, aux_loss*
*        """*
*        *B, L, D = x.shape

        # 若输入维度与 gate_hidden_size 不一致,可以添加线性投影
        if D != self.gating_dim:
            # 临时线性投影(非参数化版,为演示方便)
            proj = nn.Linear(D, self.gating_dim, bias=False).to(x.device)
            nn.init.kaiming_uniform_(proj.weight, a=math.sqrt(5))
            x_flat = proj(x.view(-1, D))  # [B*L, gating_dim]
        else:
            x_flat = x.view(-1, D)

        # 线性层得到 logits
        logits = self.linear(x_flat)  # [B*L, n_experts]

        # 训练时加噪声(Switch Transformer trick)
        if self.training and self.noise_std > 0:
            logits = logits + torch.randn_like(logits) * self.noise_std

        # softmax 得到每个专家的选择概率
        scores = F.softmax(logits, dim=-1)  # [B*L, n_experts]

        # 取 top-k 专家
        topk_vals, topk_idx = torch.topk(scores, k=self.top_k, dim=-1, sorted=False)  # [B*L, K]

        # 对 top-k 权重归一化(保持总和=1)
        if self.top_k > 1 and self.norm_topk_prob:
            denom = topk_vals.sum(dim=-1, keepdim=True) + 1e-20
            topk_vals = topk_vals / denom

        # ---------------- 辅助损失(aux loss) ----------------
        # 用于防止部分专家被频繁选中,保持负载均衡
        if self.training and (self.alpha is not None) and (self.alpha > 0):
            # 各专家平均被选中的概率
            expert_prob = scores.mean(dim=0)  # 每个专家的平均 softmax 概率
            # 每个专家实际被选中的比例
            flat_topk_idx = topk_idx.view(-1)
            mask = F.one_hot(flat_topk_idx, num_classes=self.n_experts).float()
            expert_usage = mask.mean(dim=0)
            # 损失:鼓励 expert_prob 与 expert_usage 均匀
            aux_loss = self.alpha * torch.sum(expert_prob * expert_usage * float(self.n_experts))
        else:
            aux_loss = torch.tensor(0.0, device=x.device, dtype=x.dtype)

        return topk_idx, topk_vals, aux_loss


# ============================================================
# MoE FeedForward(核心层)
# ============================================================
class MOEFeedForward(nn.Module):
    *"""*
*    MoE 前馈层,将多个专家的 FFN 与 Gate 结合。*
*    流程:*
*      1. gate 计算每个 token 的 top-k 专家及权重;*
*      2. 将 token 分派到各自专家(dispatch);*
*      3. 专家独立计算;*
*      4. 按权重加权求和回原序列;*
*      5. 若存在 shared expert,则叠加输出。*
*    """*

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

        # 1创建专家列表(每个专家一个 FFN)
        self.experts = nn.ModuleList([
            FFN(config.dim, config.hidden_dim, config.multiple_of, config.dropout)
            for _ in range(n_experts)
        ])

        # 2创建 gate 模块
        self.gate = MoEGate(config)

        # 3可选的 shared experts(dense 路径)
        if getattr(config, 'n_shared_experts', 0) > 0:
            self.shared_experts = nn.ModuleList([
                FFN(config.dim, config.hidden_dim, config.multiple_of, config.dropout)
                for _ in range(config.n_shared_experts)
            ])
        else:
            self.shared_experts = None

        # 4capacity 系数
        self.capacity_factor = getattr(config, 'capacity_factor', 1.25)
        self.aux_loss = None  # 存储 aux loss

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        *"""*
*        Args:*
*            x: [B, L, D]*
*        Returns:*
*            y_out: [B, L, D]*
*        """*
*        *B, L, D = x.shape
        T0 = B * L  # flatten token 数量
        K = self.config.num_experts_per_tok

        # ========== Step 1: gating ==========
        topk_idx, topk_weight, aux_loss = self.gate(x)
        self.aux_loss = aux_loss  # 暴露给外部使用(训练时可累加)

        # 展平成 (T0, D)
        x_flat = x.view(-1, D)

        # ========== Step 2: 复制 token ==========
        # 每个 token 对应 K 条路径(用于 top-k 专家)
        x_expanded = x_flat.repeat_interleave(K, dim=0)   # (T0*K, D)
        topk_idx_flat = topk_idx.view(-1)                 # (T0*K,)
        topk_weight_flat = topk_weight.view(-1)           # (T0*K,)

        device = x.device

        # ========== Step 3: capacity 控制 ==========
        # 每个专家最多处理的 token 数
        tokens_per_expert = float(T0) / float(self.config.n_routed_experts)
        capacity = max(1, int(math.ceil(self.capacity_factor * tokens_per_expert)))

        # 记录每个专家当前分配的 token 数
        counts = torch.zeros(self.config.n_routed_experts, dtype=torch.long, device=device)
        keep_mask = torch.zeros_like(topk_idx_flat, dtype=torch.bool, device=device)

        # 简单实现:顺序遍历 token,若专家未满则保留
        for i in range(topk_idx_flat.size(0)):
            e = int(topk_idx_flat[i].item())
            if counts[e] < capacity:
                keep_mask[i] = True
                counts[e] += 1
            else:
                keep_mask[i] = False  # 超出容量的 token 被丢弃

        # 仅保留有效 token
        selected_idx = topk_idx_flat[keep_mask]
        selected_x = x_expanded[keep_mask]
        selected_weight = topk_weight_flat[keep_mask]

        # ========== Step 4: dispatch 到专家 ==========
        y_expanded = torch.zeros_like(x_expanded, device=device)
        keep_indices = torch.nonzero(keep_mask, as_tuple=False).view(-1)  # 对应位置

        # 按专家分组计算(易读实现)
        for expert_id in range(self.config.n_routed_experts):
            mask_expert = (selected_idx == expert_id)
            if mask_expert.any():
                expert_in = selected_x[mask_expert]          # 输入 token
                expert_out = self.experts[expert_id](expert_in)  # 专家输出
                expert_positions = keep_indices[mask_expert]  # 写回位置
                y_expanded[expert_positions] = expert_out

        # ========== Step 5: 合并 top-k 输出 ==========
        # 将 K 路专家输出按权重加权求和
        y_expanded_view = y_expanded.view(T0, K, D)
        topk_weight_view = topk_weight.view(T0, K, 1)
        y_combined = torch.sum(y_expanded_view * topk_weight_view.to(y_expanded_view.dtype), dim=1)
        y_out = y_combined.view(B, L, D)

        # ========== Step 6: 加上 shared experts ==========
        if self.shared_experts is not None:
            for expert in self.shared_experts:
                y_out = y_out + expert(x)

        return y_out


if __name__ == "__main__":
    cfg = ModelConfig(dim=64, n_routed_experts=4, num_experts_per_tok=2)
    moe = MOEFeedForward(cfg)

    x = torch.randn(2, 8, 64)  # batch=2, seq=8
    y = moe(x)
    print("输入形状:", x.shape)
    print("输出形状:", y.shape)
    print("辅助损失:", moe.aux_loss.item())

星语-MoE大模型
#

下面就到了我们自研的星语大模型了,我们将所有模块代码都整合到一个文件里面。方便我们优化。

预训练数据集
#

高质量的数据集对模型表现来说至关重要。这里找到一个作者开源的开源的高质量数据集。

https://www.modelscope.cn/datasets/gongjy/minimind_dataset/files

Image

微调数据集
#

预训练效果
#

微调效果
#

相关文章