工作台课程

CS336 · 从零构建语言模型

Lecture 4 · 混合专家模型 MoE

Lecture 4 · 混合专家模型 MoE

CS336: Language Modeling from Scratch · Stanford · Spring 2025
📅 Apr 10 · 💻 不可执行讲义(PDF) · 讲者 Tatsunori Hashimoto


承上启下

上一讲(Lecture 3 · 架构与超参数

  • 现代 Transformer 已收敛到一套稳定配方:Pre-Norm + RMSNorm + SwiGLU + RoPE + 无 bias,这些是「稠密模型内部」的共识。
  • 超参数大多落在平坦盆地里:FFN 倍数(ReLU/GeLU 用 $4\times$,GLU 系用 $\frac{8}{3}\times$)、head 维度 64–128、深宽比约 100–200、词表 30k–250k。
  • 训练稳定性技巧(z-loss、QK-norm、soft-capping)都在管 softmax 的输入尺度——这个思路本讲还会在 router 上再遇到一次。
  • GQA/MQA 通过共享 K/V head 压缩 KV cache,是「推理成本反过来塑造架构」的例子。

本讲(混合专家模型 MoE)

  • 上一讲优化的是单个稠密模型的架构细节——无论怎么改,每个 token 都要走遍所有参数。
  • 本讲换一个思路:同样的计算预算,能不能装下更多参数?
  • 答案是 MoE(Mixture of Experts,混合专家模型):把 FFN 层拆成多个「专家」,每个 token 只激活其中几个——每 token 计算量不变,总参数量成倍增长。

说明

稠密模型是「小参数 × 全激活」,MoE 是「大参数 × 稀疏激活」——在相同 FLOPs 下,MoE 的总参数量可以是稠密模型的 4–8 倍甚至更多,性能通常更好;代价是路由设计负载均衡All-to-All 通信这三座新大山。


1. 为什么需要 MoE?

1.1 缩放定律的困境

Chinchilla 缩放定律(Hoffmann et al., 2022,详见 Lecture 9 · 缩放定律 I:基础)告诉我们:想训练算力最优的模型,参数量 $N$ 与数据量 $D$ 要等比例同增(约 20 tokens/参数)。而 Lecture 2 · PyTorch 与资源核算 的训练计算量公式是

$$C \approx 6ND$$

$N$、$D$ 各翻一倍,FLOPs 就涨四倍——按这条路线继续堆稠密模型,成本增长快于收益。MoE 的问题意识由此而来:能不能只增加 $N$(容量),不等比例增加每个 token 实际消耗的计算?

graph TD
    A["想要更好的模型"] --> B["缩放定律:需要更大 N 和更多 D"]
    B --> C["计算成本 ∝ N·D"]
    C --> D["训练成本爆炸"]
    D --> E{"有没有办法<br/>只增加 N<br/>但不增加 FLOPs?"}
    E -->|稀疏激活| F["MoE"]

    style A fill:#e1f5fe,stroke:#0277bd
    style D fill:#ffcdd2,stroke:#c62828
    style F fill:#c8e6c9,stroke:#2e7d32

    classDef default fill:#fff,stroke:#666

现实案例(推理 FLOPs 近似正比于激活参数量,以 Llama 3.1 405B 为基准 1×):

模型 类型 总参数 激活参数 FLOPs(推理时) 性能
Llama 3.1 405B 稠密 405B 405B 基准 1× 基准
DeepSeek-V3 MoE 671B 37B ≈ 0.09× 更优
Mixtral 8×7B MoE 47B 13B ≈ 0.03× 接近 Llama 2 70B

提示

DeepSeek-V3 的总参数量是 Llama 3.1 405B 的 1.66 倍,但每个 token 只激活 37B 参数——推理 FLOPs 只有稠密 405B 的约 9%,性能却更好。要习惯 MoE 时代的「双参数量」记法:总参数决定容量与显存占用,激活参数决定每 token 的 FLOPs。MoE 省的是「算」,不省「存」——671B 参数无论激活多少,都得放在显存/内存里。

1.2 MoE 的核心思想

把 Transformer 的 FFN 层拆成 $N$ 个「专家(expert)」,每次只用其中 $K$ 个:

flowchart TB
    subgraph Dense["稠密 FFN(传统)"]
        D1["输入 x"] --> D2["一个大 FFN<br/>所有参数都激活"]
        D2 --> D3["输出"]
    end

    subgraph MoE["MoE FFN(稀疏)"]
        M1["输入 x"] --> Router["路由函数 g(x)<br/>选出 top-K 专家"]
        Router --> E1["专家 1"]
        Router --> E2["专家 2"]
        Router -.不激活.-> E3["专家 3"]
        Router -.不激活.-> EN["专家 N"]
        E1 --> Combine["加权组合"]
        E2 --> Combine
        Combine --> M2["输出"]
    end

    style D2 fill:#ffcdd2,stroke:#c62828
    style Router fill:#fff9c4,stroke:#f57f17
    style E1 fill:#c8e6c9,stroke:#2e7d32
    style E2 fill:#c8e6c9,stroke:#2e7d32
    style E3 fill:#e0e0e0,stroke:#757575
    style EN fill:#e0e0e0,stroke:#757575
    style Combine fill:#e1f5fe,stroke:#0277bd

前向传播对比——稠密 FFN 激活全部参数,MoE 只对路由选中的专家求值:

$$\text{稠密:}\; y = \mathrm{FFN}(x) \qquad\qquad \text{MoE:}\; y = \sum_{i \in \mathrm{TopK}(x)} g_i(x)\, E_i(x)$$

符号说明:$E_i(\cdot)$ 是第 $i$ 个专家(一个独立的小 FFN),$g_i(x) \ge 0$ 是路由函数给专家 $i$ 的门控权重(未选中的专家 $g_i = 0$,对应的 $E_i(x)$ 根本不计算),$\mathrm{TopK}(x)$ 是被选中的 $K$ 个专家的下标集合。

注意

  • 优势:相同 FLOPs 下参数量可达稠密模型的数倍,同算力预算下 loss 更低(Switch Transformer、OLMoE 等系统实验反复验证)。
  • 代价:需要设计路由函数、解决负载均衡、处理通信开销(并行训练时专家分布在不同设备上);训练稳定性与微调也更棘手。

2. MoE 架构细节

2.1 专家的粒度

问题:应该在 Transformer 的哪个部件用 MoE?每个专家应该多大、多少个?

业界共识(2025):

graph LR
    A["Transformer Block"] --> B["Multi-Head Attention<br/>(保持稠密)"]
    B --> C["Add & Norm"]
    C --> D["MoE FFN Layer<br/>(替换原 FFN)"]
    D --> E["Add & Norm"]

    style B fill:#e0e0e0,stroke:#757575
    style D fill:#c8e6c9,stroke:#2e7d32
设计选择 说明
只替换 FFN FFN 占 Transformer 参数的约 2/3,是稀疏化收益最大的部位;attention 的 MoE 变体(如 MoA)存在但不主流,且 attention 稀疏化对稳定性影响更大
哪些层用 MoE 早期 GShard 隔层替换;现代模型(Mixtral、DeepSeek 系)基本每层都用,但 DeepSeek-V3 保留前 3 层为稠密 FFN(浅层功能通用,稀疏化收益小且伤稳定性)
专家大小 每个专家 = 一个完整的小 SwiGLU FFN(gate_proj + up_proj + down_proj)

细粒度专家(fine-grained experts)是 DeepSeekMoE 引入的重要维度:把每个专家切小为原来的 $\frac{1}{m}$,同时把专家数与激活数都乘 $m$($N \to mN$,$K \to mK$)。激活参数量不变,但可选的专家组合数从 $\binom{N}{K}$ 暴增到 $\binom{mN}{mK}$,专家分工可以更细、冗余更少。

例子

$N=16, K=2$ 时组合数 $\binom{16}{2} = 120$;切细 4 倍($m=4$)后 $N=64, K=8$,组合数 $\binom{64}{8} \approx 4.4 \times 10^9$。同样的激活计算量,表达「专家组合」的自由度完全不在一个量级——这是 DeepSeek 系列坚持用大量小专家(V3 达 256 个)的核心理由。

代码示例(简化的 MoE FFN):

import torch
import torch.nn as nn

class ExpertFFN(nn.Module):
    """单个专家 = 一个标准的 SwiGLU FFN"""
    def __init__(self, d_model: int, d_ff: int):
        super().__init__()
        self.up_proj = nn.Linear(d_model, d_ff, bias=False)
        self.gate_proj = nn.Linear(d_model, d_ff, bias=False)
        self.down_proj = nn.Linear(d_ff, d_model, bias=False)

    def forward(self, x):
        # SwiGLU: (SiLU(W_gate(x)) ⊙ W_up(x)) W_down —— 激活函数作用在门控分支上
        return self.down_proj(torch.nn.functional.silu(self.gate_proj(x)) * self.up_proj(x))

class MoELayer(nn.Module):
    """MoE FFN 层:N 个专家 + 路由函数"""
    def __init__(self, d_model: int, d_ff: int, num_experts: int, top_k: int):
        super().__init__()
        self.num_experts = num_experts
        self.top_k = top_k

        # N 个独立的专家
        self.experts = nn.ModuleList([
            ExpertFFN(d_model, d_ff) for _ in range(num_experts)
        ])

        # 路由网络:输入 x -> N 个专家的分数
        self.router = nn.Linear(d_model, num_experts, bias=False)

    def forward(self, x):
        # x: (batch, seq_len, d_model)
        batch_size, seq_len, d_model = x.shape
        x_flat = x.view(-1, d_model)  # (batch * seq_len, d_model)

        # 1. 路由:计算每个 token 对每个专家的分数
        router_logits = self.router(x_flat)  # (batch * seq_len, num_experts)
        router_weights = torch.softmax(router_logits, dim=-1)

        # 2. Top-K 选择:每个 token 只选 top_k 个专家
        top_k_weights, top_k_indices = torch.topk(router_weights, self.top_k, dim=-1)
        top_k_weights = top_k_weights / top_k_weights.sum(dim=-1, keepdim=True)  # 重归一化

        # 3. 专家计算(简化版:逐 token 循环,实际实现会批量化)
        output = torch.zeros_like(x_flat)
        for i in range(x_flat.shape[0]):
            for k in range(self.top_k):
                expert_idx = top_k_indices[i, k]
                expert_output = self.experts[expert_idx](x_flat[i:i+1])
                output[i] += top_k_weights[i, k] * expert_output.squeeze(0)

        return output.view(batch_size, seq_len, d_model)

说明

真实实现(如 Megatron-LM、MegaBlocks)会先按专家把 token 分桶,再对每个专家做一次批量矩阵乘,彻底避免逐 token 循环;token 数不齐导致的「不规则矩阵乘」正是 Lecture 6 · Kernel 与 Triton 里 MegaBlocks 这类块稀疏 kernel 要解决的问题。多 GPU 上还要做 expert parallelism(见 §6)。

2.2 共享专家(Shared Experts)

问题:某些功能(基础语法、高频词处理)是所有 token 都需要的。如果只有路由专家,每个专家都得各自学一份这种通用功能——参数冗余,还挤占了专业化的容量。

解决方案(DeepSeekMoE / Qwen-MoE):除了 $N$ 个「路由专家」,额外加 $M$ 个「共享专家」,每个 token 都无条件经过

$$y = \underbrace{\sum_{j=1}^{M} E_j^{\text{shared}}(x)}_{\text{稠密激活,所有 token 都走}} + \underbrace{\sum_{i \in \mathrm{TopK}(x)} g_i(x)\, E_i^{\text{routed}}(x)}_{\text{稀疏激活,top-}K}$$

DeepSeek-V3 配置:256 个路由专家(每 token 激活 8 个)+ 1 个共享专家。

提示

共享专家负责「通识」,路由专家负责「专科」。DeepSeekMoE 的消融显示:去掉共享专家后,路由专家之间的参数冗余明显上升(很多专家被迫重复学习通用功能)。用公司打比方:共享专家是所有项目共用的行政与 IT 部门,路由专家是各业务线——没有前者,每条业务线都得自建一套后勤。


3. 路由函数设计

路由函数 $g(x)$ 决定每个 token 去哪些专家——这是 MoE 的核心设计点,也是所有麻烦(均衡、稳定性、通信)的源头。

3.1 Top-K 路由(主流方案)

思路:用一个可学习的线性层给 $N$ 个专家打分,softmax 后取分数最高的 $K$ 个,并在选中集合内重新归一化权重:

$$s = \mathrm{softmax}(W_r\, x) \in \mathbb{R}^N, \qquad \mathcal{K} = \mathrm{TopK}(s, K)$$

$$g_i(x) = \begin{cases} \dfrac{s_i}{\sum_{j \in \mathcal{K}} s_j} & i \in \mathcal{K} \\[2mm] 0 & \text{否则} \end{cases} \qquad\Longrightarrow\qquad y = \sum_{i \in \mathcal{K}} g_i(x)\, E_i(x)$$

符号说明:$W_r \in \mathbb{R}^{N \times d_{\text{model}}}$ 是路由权重矩阵(整个 MoE 层里唯一的「决策者」,参数量小得可以忽略),$s_i$ 是 token 对专家 $i$ 的归一化分数,$\mathcal{K}$ 是选中的 $K$ 个专家下标。重归一化保证选中专家的权重和为 1。

超参数

参数 典型值 说明
K(top-k) 2–8 DeepSeek-V3 = 8,Mixtral = 2
N(专家总数) 8–256 DeepSeek-V3 = 256,Mixtral = 8
激活比例 K/N DeepSeek-V3 = 8/256 ≈ 3.1%
flowchart LR
    X["输入 token x"] --> R["W_router · x"]
    R --> S["Softmax"]
    S --> T["Top-K 选择"]
    T --> E1["专家 1<br/>权重 0.6"]
    T --> E2["专家 2<br/>权重 0.4"]
    T -.丢弃.-> E3["专家 3<br/>权重 0.05"]
    E1 --> O["0.6·E₁(x) + 0.4·E₂(x)"]
    E2 --> O

    style R fill:#fff9c4,stroke:#f57f17
    style S fill:#e1f5fe,stroke:#0277bd
    style T fill:#ffccbc,stroke:#d84315
    style E1 fill:#c8e6c9,stroke:#2e7d32
    style E2 fill:#c8e6c9,stroke:#2e7d32
    style E3 fill:#e0e0e0,stroke:#757575

说明

$\mathrm{TopK}$ 本身不可导,但被选中专家的门控权重 $g_i(x)$ 对路由参数 $W_r$ 可导:如果专家 $i$ 的输出对降低 loss 有贡献,梯度会推高 $s_i$,让它下次更容易被选中。未选中的专家收不到任何梯度——这是一种有偏但实践中极其有效的启发式。理论上更「干净」的做法(REINFORCE 等随机梯度估计)反而因方差大、不稳定而被淘汰。

注意

路由是可学习的,梯度会让模型「钻空子」——训练早期表现稍好的专家会被更多选中、得到更多训练、变得更好……正反馈之下,少数专家过载、其余闲置。这就是专家崩塌,需要显式的负载均衡机制(见 §4)。

3.2 其他路由策略(对比)

路由方式 原理 优势 劣势 代表工作
Top-K 可学习打分 + topk 灵活,性能好 需要负载均衡 Switch, Mixtral, DeepSeek
Hash 路由 固定哈希函数按 token id 分配 负载天然均衡、无需学习 无法按语义分工,性能上限低 Hash Layers (Roller et al., 2021)
线性分配 把路由建模为最优分配问题求解 均衡有保证 求解开销大,推理时不好用 BASE Layers (2021)
随机路由 随机采样 K 个专家 简单,天然均衡 无法学习专业化 早期实验
RL 路由 把离散选择当动作,用策略梯度训练 理论上无偏 方差大、训练不稳定 Bengio et al. 早期条件计算

RL 路由的失败很有启发性:路由本质上是一个离散决策问题(可类比 CS221 Lecture 8 · 强化学习(未找到对应页面) 里的动作选择),但工业界最终选择了「假装可导」的 top-k 启发式——简单、有偏、稳定胜过优雅、无偏、高方差

业界现状(2025):Top-K + 均衡机制是唯一被大规模反复验证的方案。

3.3 路由的「专业化」现象

训练后统计各专家实际接收的 token,会发现专家自发专业化:某些专家集中处理特定语言、代码或数字/标点等 token 类型(下图为示意,编号虚构):

graph TD
    T1["中文 token<br/>你好"] --> E42["专家 42<br/>中文倾向"]
    T2["代码 token<br/>def func"] --> E137["专家 137<br/>代码倾向"]
    T3["数学 token<br/>∫ x dx"] --> E201["专家 201<br/>数学符号倾向"]
    T4["通用 token<br/>the cat"] --> Shared["共享专家<br/>通识"]

    style E42 fill:#ffccbc,stroke:#d84315
    style E137 fill:#c8e6c9,stroke:#2e7d32
    style E201 fill:#b3e5fc,stroke:#0277bd
    style Shared fill:#ffe0b2,stroke:#e65100

提示

为什么会专业化?梯度优化形成正反馈:擅长某类 token 的专家 → 路由分数高 → 更常接到那类 token → 在那类 token 上练得更好。共享专家的存在让路由专家不必浪费容量在通识上,进一步促进分化。

注意

「专业化」不等于人类直觉的「学科分工」。Mixtral 论文专门分析过:其专家并没有按主题(数学、生物……)分化,分化更多体现在 token 级、语法级的浅层模式上(如缩进、标点、高频词)。OLMoE 与 DeepSeek 的分析则在更细粒度的设置下观察到更清晰的领域倾向。结论:专业化真实存在,但它是优化「顺手」形成的统计分工,别把专家过度拟人化成「数学家」「程序员」。


4. 负载均衡

4.1 问题:专家崩塌(Expert Collapse)

Top-K 路由是可学习的,放任梯度自由更新会导致少数专家承担绝大多数 token

$$\text{理想(8 专家):每个专家 } 12.5\% \qquad \text{崩塌:专家 1 占 } 78\%,\ \text{专家 2 占 } 15\%,\ \text{其余各} \sim 1\%$$

后果

  • 参数利用率崩坏——闲置专家学不到东西,MoE 退化成一个昂贵的小稠密模型;
  • 性能退化——过载专家被迫身兼百职,失去专业化收益;
  • 系统灾难——专家并行时,某几块 GPU 过载而其余空转,吞吐由最慢者决定。

4.2 解决方案:辅助损失(Auxiliary Loss)

在语言建模交叉熵之外,额外加一个负载均衡损失惩罚不均衡的路由。Switch Transformer (2021) 的经典形式:

$$\mathcal{L}_{\text{aux}} = \alpha \cdot N \sum_{i=1}^{N} f_i \, P_i$$

符号说明:$f_i$ 是实际被路由到专家 $i$ 的 token 占比(batch 内统计量,不可导),$P_i$ 是专家 $i$ 在所有 token 上的平均路由概率(softmax 输出的均值,可导),$N$ 是专家数,$\alpha$ 是平衡系数(典型值 0.01)。完全均衡时 $f_i = P_i = \frac{1}{N}$,损失取最小值 $\alpha$。

def load_balancing_loss(router_probs, expert_indices, num_experts):
    """
    router_probs: (batch * seq_len, num_experts) - 路由概率
    expert_indices: (batch * seq_len, top_k) - 被选中的专家
    """
    # 1. 每个专家被选中的频率(fraction of tokens)
    expert_mask = F.one_hot(expert_indices, num_experts).sum(dim=1)  # (B*T, num_experts)
    f_i = expert_mask.float().mean(dim=0)  # (num_experts,) - 每个专家的负载比例

    # 2. 每个专家的平均路由概率
    P_i = router_probs.mean(dim=0)  # (num_experts,)

    # 3. 辅助损失 = num_experts * Σᵢ (f_i · P_i)
    #    直觉:f_i 高(被选多)且 P_i 高(分数高)的专家会被惩罚
    aux_loss = num_experts * (f_i * P_i).sum()

    return aux_loss

# 总损失
total_loss = ce_loss + alpha * load_balancing_loss(...)
#                       ↑ 平衡系数,通常 0.01

为什么是 $f_i \cdot P_i$ 这个形式? 两个巧妙之处:

  • $f_i$ 是「谁真的过载」的准确统计,但它来自不可导的计数;$P_i$ 可导但只是「意图」。乘积形式让梯度通过 $P_i$ 流动、方向由 $f_i$ 加权——过载专家($f_i$ 大)的路由概率被压得最狠;
  • 在 $\sum_i f_i$ 与 $\sum_i P_i$ 固定、且 $f$ 与 $P$ 正相关的条件下,$\sum_i f_i P_i$ 在均匀分布处取最小——所以最小化它就是在推动均衡。

提示

辅助损失本质是给主目标加一个「反垄断税」:谁接的活越多、要价越高,谁被课的税越重。$\alpha$ 控制税率——太小压不住崩塌,太大则强行均分、牺牲专业化。它是 MoE 训练里最重要的超参数之一。

4.3 其他均衡策略

方法 原理 优劣
Expert capacity 每个专家设硬上限 $C = \frac{T}{N} \times c_f$($T$ 为 token 总数,$c_f$ 为容量因子),超出的 token 被丢弃(走残差直通)或溢出到次选专家 强制均衡、显存可预算,但丢 token 损失信息
噪声注入 路由 logits 加高斯噪声,鼓励探索 简单,但噪声尺度难调
Sinkhorn 归一化 迭代算法把路由矩阵行列和都拉平 理论优雅,计算开销大

现代共识(2025):辅助损失为主,capacity 限制为辅(训练时容量因子常取 1.0–1.5);DeepSeek-V3 则进一步走向无辅助损失的 bias 调节(见 §8.3)。


5. 训练稳定性

MoE 比稠密模型更难训——router 引入了新的 softmax、新的离散性、新的对称性问题。

5.1 Router Z-Loss

问题:路由 logits 可能数值爆炸(某个专家的分数持续走高),softmax 饱和后梯度异常,甚至 NaN。这与 Lecture 3 · 架构与超参数 中输出层 z-loss 面对的是同一类问题——softmax 对 logits 整体尺度不敏感,交叉熵不会自动约束它。

解决方案(ST-MoE, 2022):惩罚路由 logits 的 log-partition 函数的平方:

$$\mathcal{L}_z = \beta \cdot \frac{1}{B} \sum_{b=1}^{B} \left( \log \sum_{j=1}^{N} e^{\ell_j^{(b)}} \right)^2$$

符号说明:$\ell_j^{(b)}$ 是第 $b$ 个 token 对专家 $j$ 的路由 logit(softmax 前),$B$ 是 batch 内 token 数,$\beta$ 是系数(典型值 0.001)。$\mathrm{logsumexp}$ 是 logits 的光滑最大值,压住它就间接压住了最大 logit。

def router_z_loss(router_logits):
    """
    router_logits: (batch * seq_len, num_experts) - 路由打分(softmax 前)
    """
    # Z-loss = log(Σⱼ exp(logitⱼ))²
    # 等价于惩罚 logsumexp 的平方
    log_z = torch.logsumexp(router_logits, dim=-1)  # (B*T,)
    z_loss = (log_z ** 2).mean()
    return z_loss

# 总损失
total_loss = ce_loss + alpha * load_balancing_loss + beta * router_z_loss
#                                                      ↑ 通常 beta = 0.001

说明

于是完整的 MoE 训练目标是三项之和:$\mathcal{L} = \mathcal{L}_{\text{CE}} + \alpha\,\mathcal{L}_{\text{aux}} + \beta\,\mathcal{L}_z$——主目标学语言,$\mathcal{L}_{\text{aux}}$ 管「分得匀」,$\mathcal{L}_z$ 管「分得稳」。工程上还会叠加 QK-norm 与梯度裁剪(clip_grad_norm = 1.0)等稠密模型的常规手段。

5.2 专家参数初始化

MoE 对初始化更敏感:如果 $N$ 个专家初始化完全相同且都收到相同梯度(例如稠密门控下),对称性会让它们难以分化——分化只能依赖路由打分的微小差异与选择的随机性,速度很慢。

标准做法是让每个专家独立随机初始化,从一开始就打破对称:

# 每个专家独立初始化,用不同的随机种子
for i, expert in enumerate(self.experts):
    torch.manual_seed(base_seed + i)
    expert.reset_parameters()

(Upcycling 场景刻意反其道而行——所有专家从同一份稠密 FFN 复制而来,靠路由的随机性慢慢分化,这正是它的主要风险,见 §7。)

5.3 FP32 路由权重

经验规则:即使模型主体用 BF16/FP16 训练,路由网络的权重与 logits 保持 FP32。路由要在 $N$ 个接近的分数里排序选 top-k,低精度的舍入误差足以翻转选择结果,造成训练轨迹抖动。

class MoELayer(nn.Module):
    def __init__(self, ...):
        self.router = nn.Linear(d_model, num_experts, bias=False)
        self.router.to(torch.float32)  # 强制 FP32

    def forward(self, x):
        router_logits = self.router(x.float())  # 转 FP32 计算
        # ... 后续用 FP32 的 router_logits

5.4 微调阶段的过拟合

ST-MoE 的另一个重要发现:稀疏模型在小数据微调时比同激活量的稠密模型更容易过拟合——参数多、有效数据少,路由还可能在分布偏移下漂移。缓解手段包括:微调时只更新部分参数(如冻结路由或只调非专家参数)、加大微调阶段的 dropout、或用更大的微调数据。这是 MoE「训练便宜、用起来娇贵」的一面,在做对齐(Lecture 15 · 对齐 I:SFT 与 RLHF)时需要记住。

提示

本节四个技巧对应 MoE 的四个新脆弱点:router 的 softmax(z-loss)、专家的对称性(独立初始化)、离散选择对噪声的敏感(FP32 路由)、参数量与微调数据的失衡(过拟合)。共同主线:稀疏性把「连续优化」变成了「连续优化 + 离散分配」的混合问题,所有额外的护栏都是在替离散那一半擦屁股。


6. 并行训练策略

MoE 的稀疏性带来新的并行挑战——传统的数据并行 + 张量并行不够用(并行策略的系统性介绍见 Lecture 7 · 并行 I:基础Lecture 8 · 并行 II:分布式训练实战)。

6.1 Expert Parallelism(专家并行)

思路:总参数量太大,单卡装不下所有专家——把 $N$ 个专家分布到不同设备,每个设备只持有一部分专家;token 则「走出去」找专家。

graph TB
    subgraph "GPU 0"
        E1["专家 1-64"]
    end
    subgraph "GPU 1"
        E2["专家 65-128"]
    end
    subgraph "GPU 2"
        E3["专家 129-192"]
    end
    subgraph "GPU 3"
        E4["专家 193-256"]
    end

    T["Token batch"] --> R["路由决策"]
    R -->|All-to-All| E1
    R -->|All-to-All| E2
    R -->|All-to-All| E3
    R -->|All-to-All| E4
    E1 -->|All-to-All| O["输出"]
    E2 -->|All-to-All| O
    E3 -->|All-to-All| O
    E4 -->|All-to-All| O

    style T fill:#e1f5fe,stroke:#0277bd
    style R fill:#fff9c4,stroke:#f57f17
    style O fill:#c8e6c9,stroke:#2e7d32

通信模式——每个 MoE 层前向要做两次 All-to-All(反向再来两次):

  1. All-to-All(dispatch):按路由结果把每个 token 的激活发到其专家所在的 GPU;
  2. 本地专家计算:各 GPU 对收到的 token 批量跑自己的专家;
  3. All-to-All(combine):把专家输出送回 token 原属的 GPU 做加权求和。

单向通信量约为 $B \times L \times K \times d_{\text{model}}$ 个激活元素(每个 token 要发给 $K$ 个专家)——它随 batch 与序列长度线性增长,且无法像梯度 All-Reduce 那样容易与计算重叠。

注意

All-to-All 是带宽密集型操作,且跨节点带宽(InfiniBand)比节点内 NVLink 低一个量级以上。token dispatch 一旦跨节点,通信时间可能超过专家计算本身——这是 MoE 扩展的主要系统限制,也是 §8 里 DeepSeek 各种「设备级路由」设计的动机。

6.2 混合并行(Hybrid Parallelism)

大规模 MoE 训练把多种并行叠起来用:

并行方式 分割维度 用于 通信原语
Data Parallelism batch 所有层 All-Reduce(梯度)
Tensor Parallelism 矩阵列/行 注意力、FFN 内部 All-Reduce(前向/反向)
Expert Parallelism 专家数 MoE 层 All-to-All
Pipeline Parallelism 层深度 整个模型 P2P(send/recv)

DeepSeek-V3 实际配置(技术报告):

  • 2048 块 H800 GPU;
  • 16 路流水线并行(自研 DualPipe 调度,用计算-通信重叠掩盖 All-to-All);
  • 64 路专家并行,跨 8 个节点——256 个路由专家分摊后每块 GPU 持有 4 个专家
  • 数据并行用 ZeRO-1(切分 optimizer state);
  • 不使用张量并行——TP 的 All-Reduce 太频繁,在 H800 的带宽条件下不划算。
graph LR
    subgraph "DeepSeek-V3 并行策略堆叠"
        A["2048 块 H800"] -->|"PP = 16"| B["流水线 16 段<br/>DualPipe 调度"]
        B -->|"EP = 64"| C["每 EP 组 64 卡<br/>每卡 4 个专家"]
        C -->|"ZeRO-1 DP"| D["切分 optimizer state<br/>的数据并行"]
    end

    style A fill:#e1f5fe,stroke:#0277bd
    style B fill:#c8e6c9,stroke:#2e7d32
    style C fill:#fff9c4,stroke:#f57f17
    style D fill:#ffccbc,stroke:#d84315

提示

专家并行度不必等于专家数——每卡放多个专家反而让本地矩阵乘更「胖」、效率更高(V3 是 256 专家 / EP64 = 每卡 4 个)。真正的关键是把 All-to-All 尽量按在节点内:路由层面用设备/节点受限路由(§8.2、§8.3),调度层面用 DualPipe 这类重叠技术把通信藏进计算里。GPU 带宽层级的量化直觉见 Lecture 5 · GPU


7. 从稠密模型到 MoE:Upcycling

问题:从零训练 MoE 需要海量计算——能否复用已有的稠密模型

Upcycling(稀疏再利用,Komatsuzaki et al., 2022):把训练好的稠密模型改造成 MoE,再继续训练:

  1. 把原稠密 FFN 复制 $N$ 份,作为 $N$ 个专家的初始化;
  2. 插入随机初始化的 router,通常从保守的 top-k 设置起步;
  3. 用足够长的继续预训练让专家分化;
  4. 再进入指令微调 / 对齐阶段。
flowchart LR
    A["Dense LM"] --> B["复制 FFN 权重"]
    B --> C["插入 Router"]
    C --> D["MoE LM"]
    D --> E["继续预训练"]
    E --> F["SFT / 对齐"]

    style A fill:#e1f5fe,stroke:#0277bd
    style D fill:#fff9c4,stroke:#f57f17
    style F fill:#c8e6c9,stroke:#2e7d32

提示

Upcycling 不是「免费变强」,而是把稠密模型已学到的通用表示当作初始化,避免 MoE 从随机专家开始摸索。它的先天矛盾是:初始化时所有专家完全相同(对称),而 MoE 的价值恰恰来自专家不同——所以成败取决于继续训练能否用足够的数据、合适的路由正则与负载均衡把对称性打破(对照 §5.2:从零训练时会刻意独立初始化来避免这个问题)。

7.1 例子:MiniCPM 与 Qwen MoE

官方讲义给了两个典型例子:

模型 初始化 MoE 设置 讲义中的结论
MiniCPM MoE MiniCPM dense model top-k=2,8 experts,约 4B active params 用约 520B tokens 继续训练后,相比 base model 有收益
Qwen MoE Qwen 1.8B top-k=4,60 experts,4 shared experts 与 DeepSeekMoE 类似,是较早被确认成功的 upcycling 案例之一

注意

Upcycling 的难点不在「把 FFN 复制 N 份」,而在于避免所有专家学成同一个函数。如果 router 过早塌缩,或继续训练的数据量不足,得到的只是一个参数更多、利用率更差的模型。MiniCPM 的 520B token 继续训练量说明:打破对称是要付出真金白银的算力的。


8. DeepSeek MoE:从 v1 到 v3

Lecture 4 最后用 DeepSeek 系列把前面的概念串起来:共享专家、细粒度专家、设备级路由、通信均衡、aux-loss-free balancing 都不是孤立技巧,而是在回答同一个问题:如何让稀疏激活既有效(数学上均衡、分工细)又跑得动(系统上通信可控)

8.1 DeepSeek MoE v1

维度 设计
参数规模 16B total,约 2.8B active
专家形态 2 个 shared experts + 64 个 routed experts(每个专家为标准 FFN 的 1/4 大小,激活 6 个)
路由 标准 top-k routing
均衡 expert-level + device-level auxiliary loss

理解方式

  • shared experts 承接所有 token 共需的通用能力,消除路由专家间的功能冗余;
  • 细粒度 routed experts(1/4 尺寸 × 4 倍数量)用组合数优势换更细的分工;
  • device-level balancing 说明 DeepSeek 从 v1 起就把系统侧的均衡(每台设备的负载)当作与数学目标同级的约束,而不只是优化一个漂亮的 loss。

8.2 DeepSeek MoE v2

维度 设计
参数规模 236B total,约 21B active
专家形态 2 个 shared experts + 160 个 routed experts,细粒度比例约 1/10
激活 6 个 routed experts active
新增机制 top-M device routing(每个 token 最多访问 M=3 台设备);communication balancing loss

Top-M device routing 的动机很朴素:如果一个 token 的 top-k 专家散落在太多设备上,All-to-All 的扇出会非常难看。先把 token 可访问的设备集合限制在 M 台(按设备上专家的最高分选设备),再在这些设备内部选专家——把模型质量和通信成本放进同一个优化框架。

Communication balancing loss 进一步平衡系统侧指标:每台设备收到多少 token、发出多少输出、每个 MoE 层是否出现局部热点——本质是把 §4 的均衡思想从「专家维度」推广到「设备维度」。

提示

MoE 的扩展瓶颈经常不是 FLOPs,而是跨设备 token dispatch。路由策略如果只追求 loss、不管通信拓扑,训练吞吐会被 All-to-All 直接打穿。DeepSeek 的演进路线就是不断把「通信」显式写进路由的约束里——算法为系统让路,这门课反复出现的主题。

8.3 DeepSeek MoE v3

维度 设计
参数规模 671B total,约 37B active
专家形态 1 个 shared expert + 256 个细粒度 routed experts(前 3 层保持稠密)
激活 8 个 routed experts active
路由 sigmoid 打分;按「分数 + per-expert bias」选 top-k;门控权重在选中集合内归一化
均衡 per-expert bias 的 auxiliary-loss-free balancing + 很轻的 sequence-wise auxiliary loss;node-limited routing(每 token 至多 4 个节点)

v3 路由的两个细节值得展开:

  • sigmoid 替代 softmax 打分:softmax 让专家分数互相竞争(一个升、其余必降),sigmoid 让每个专家的分数独立——细粒度大 $N$ 下更稳定,也让 bias 调节不会牵一发动全身;
  • auxiliary-loss-free balancing:给每个专家维护一个只影响 top-k 选择、不参与门控权重的 bias $b_i$,按负载在线调整。

$$s_i = \sigma(w_i^\top x), \qquad \mathcal{K} = \mathrm{TopK}(s_i + b_i,\ K), \qquad g_i = \frac{s_i}{\sum_{j \in \mathcal{K}} s_j}$$

注意 $b_i$ 只出现在选择步骤里,输出加权仍用原始分数 $s_i$——均衡干预不污染模型的函数值。

# 伪代码:per-expert bias 的在线更新直觉
if expert_load[i] < target_load:
    bias[i] += step_size   # 专家吃得太少,下次更容易被选中
else:
    bias[i] -= step_size   # 专家吃得太多,下次降低优先级

最值得记住的一点:v3 没有把负载均衡问题「取消掉」,只是把它从显式 auxiliary loss(会给主目标注入干扰梯度)迁移到不产生梯度的在线 bias 调节里,再留一个很轻的序列级辅助项兜底。均衡压力还在,只是换了个不伤主目标的施力点。


9. 补充:DeepSeek-V3 还用了什么?

严格说,下面两点已经超出 MoE 本身,但官方 Lecture 4 在结尾把它们放进 DeepSeek-V3 的整体架构里讲,所以值得放在这里作索引。

9.1 MLA:Multi-head Latent Attention

核心想法:把 K/V 表示成一个低维潜变量(latent)的函数,缓存潜变量而非完整 K/V,从而压缩 KV cache——与 Lecture 3 · 架构与超参数 里 GQA 的目标相同,但走「低秩压缩」而非「共享 head」的路线。

$$c_t^{KV} = W^{DKV} h_t \quad (\text{降维,只缓存这个}), \qquad k_t^C = W^{UK} c_t^{KV}, \quad v_t^C = W^{UV} c_t^{KV} \quad (\text{用时再升维})$$

符号说明:$h_t$ 是第 $t$ 个 token 的隐状态,$W^{DKV}$ 把它压到低维 $c_t^{KV}$(DeepSeek-V2 中维度仅为 $4 d_{\text{head}}$),$W^{UK}, W^{UV}$ 在计算注意力时再把潜变量投影回各 head 的 K/V。

收益

  • DeepSeek-V2 报告 KV cache 缩减约 93%,同时质量优于 GQA 同级配置——因为压缩矩阵是学出来的,而不是像 GQA 那样硬性共享;
  • query 侧也可做类似压缩,降低训练期激活内存;
  • 对长上下文推理尤其重要(KV cache 是长上下文的主要内存瓶颈,见 Lecture 10 · 推理)。

复杂之处:RoPE 的旋转依赖各 head 的完整 key,与「只缓存低维潜变量」不兼容。解法是解耦:留出一小部分维度作为不经压缩、可正常旋转的 RoPE key,与潜变量恢复出的内容 key 拼接使用。

9.2 MTP:Multi-Token Prediction

MTP 的想法是在主模型之外挂一个轻量模块,训练时额外预测更远的未来 token:

$$\text{普通 LM:} x_t \to \hat{x}_{t+1} \qquad\qquad \text{MTP:} x_t \to \hat{x}_{t+1}, \hat{x}_{t+2}, \dots$$

DeepSeek-V3 的实现相当保守——讲义特别指出它实际只做 one-token-ahead 的 MTP 变体(额外预测一个 token)。它的价值主要在于:

  • 每个位置多一个预测目标,训练信号更密
  • MTP 模块天然可以充当 speculative decoding 的草稿模型(draft model),加速推理;
  • 推理时可以直接丢弃 MTP 模块,不增加主模型的部署成本。

总结

mindmap
  root((MoE))
    动机
      同 FLOPs 装更多参数
      总参数与激活参数解耦
    架构
      只替换 FFN
      细粒度专家
      共享专家
    路由
      Top-K 打分
      离散选择的梯度启发式
      专家专业化
    负载均衡
      辅助损失
      expert capacity
      per-expert bias
    训练稳定
      router z-loss
      FP32 路由
      独立初始化
      微调易过拟合
    系统
      专家并行
      两次 All-to-All
      设备受限路由
    DeepSeek 演进
      v1 共享加细粒度
      v2 通信均衡
      v3 aux-loss-free
      MLA 与 MTP

本讲核心

问题 MoE 的回答
同样 FLOPs 下能否扩大参数量? 用稀疏激活:每个 token 只走 top-k 专家
路由不可导怎么办? 工业界主要用 top-k heuristic + softmax/sigmoid + 辅助均衡
专家负载不均怎么办? auxiliary loss、per-device balancing、per-expert bias
为什么 MoE 难训? router 不稳定、专家塌缩、fine-tuning 易过拟合、All-to-All 通信复杂
为什么 MoE 仍然流行? 在相同推理 FLOPs 下,更多参数和更强模型质量通常是值得的

一句话

MoE 的本质是把「模型容量」与「每个 token 的计算量」解耦;真正难的不是多放几个专家,而是让路由、负载均衡、通信和训练稳定性同时成立。

和后续课程的连接


复习自测

题目

MoE 省算不省存。每 token 的训练/推理 FLOPs 正比于激活参数(如 DeepSeek-V3 的 37B),所以计算成本像个小模型;但总参数(671B)无论激活与否都要放进显存/内存,部署门槛、通信拓扑都由它决定。这也是为什么 MoE 必须配专家并行——不是为了算得快,而是单卡根本装不下。

题目

因为被选中专家的门控权重 $g_i(x) = s_i / \sum_{j \in \mathcal{K}} s_j$ 对路由参数可导:某专家的输出对降 loss 有贡献时,梯度会推高其分数,使其更容易再次被选中。未选中专家收不到梯度——这是有偏的启发式,但比 REINFORCE 等无偏随机估计稳定得多,被工业界普遍接受。

题目

$f_i$(实际负载占比)是准确的均衡度量但来自计数、不可导;$P_i$(平均路由概率)可导但只是意图。乘积让梯度经 $P_i$ 流动、力度由 $f_i$ 加权——过载专家的路由概率被压得最狠。完全均衡时 $f_i = P_i = 1/N$,损失取最小值 $\alpha \cdot N \cdot N \cdot \frac{1}{N^2} = \alpha$。

题目

第一次(dispatch)把每个 token 的激活按路由结果发到专家所在 GPU,第二次(combine)把专家输出送回 token 原属 GPU 做加权求和——token 的「出差」必须有去有回。单向通信量约 $B \times L \times K \times d_{\text{model}}$,随 batch 和序列长度线性增长;而跨节点带宽比节点内 NVLink 低一个量级以上,dispatch 一旦大量跨节点,通信就会盖过计算。这正是 top-M device routing / node-limited routing 的动机。

题目

共享专家承接所有 token 共需的通用功能,去掉它时每个路由专家都得重复学一份通识,参数冗余、专业化受挤压(DeepSeekMoE 消融验证)。per-expert bias 只作用于 top-k 的选择步骤且不参与梯度,负载失衡时在线加减 bias 即可调节流量——均衡压力不再以辅助损失梯度的形式干扰主目标的优化,门控权重也不被污染。


参考资料