CodeLab-LLaMA2 / 星语 MoE 以“把大模型核心链路跑通”为目标,围绕 LLaMA2 的结构、训练和应用做系统化拆解。
- 从零实现 Attention、FFN、RMSNorm、Rotary Embedding 等核心模块。
- 支持预训练、SFT、LoRA 微调和推理流程,覆盖从结构实现到训练调试的完整链路。
- 延伸到 RAG、Agent 和 MoE-FFN 替换策略,帮助理解大模型系统如何从理论进入应用。
项目链接:GitHub,在线体验:ModelScope。
项目详解#
很高兴,前面的章节已经让我们完整的学习到了如何一步一步构建大模型,如何预训练,如何高效微调。
这章,打算以最近特别火的MoE架构,来重新设计模型,我打算取名为星语。
什么是MoE架构?#
MoE 是一种“稀疏激活”的神经网络架构:把模型拆成许多专门的子网络(称为 experts),然后用一个 gating(门/路由)网络根据输入选择其中一小部分专家来处理该输入。这样模型总参数可以非常大,但每次前向只激活少数专家,计算成本/延迟接近传统模型,从而实现“用常量计算成本得到更多参数规模”的目标。
我们可以看看论文中的架构图:
可以很明显的看到,就是把原来的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)中,每一层都有两部分:
自注意力(Self-Attention)
前馈网络(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架构是花费最少的。这个通过实验可以证明。
存在问题#
负载不均衡(Load Imbalance)#
在训练时,gating 网络可能倾向于选择某几个专家,导致:
某些专家负载过高(被频繁调用);
其他专家几乎没被用(梯度更新太少)。
这会带来:
训练不稳定;
专家崩塌(collapse);
GPU 之间工作量差异大,效率下降。
缓解负载不均衡的主要机制是 auxiliary loss(辅助负载平衡损失)
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

