Skip to main content
  1. Posts/

MoE 结构学习笔记:Expert 与 Gate 的几种关系

Table of Contents
Note: This article is available in Chinese only. 本文暂无英文版本。 View original

起因
#

机缘使然,最近在工作中接触到了 MoE(Mixture of Experts)。

有意思的是,我接触到的用法,和现在大模型里的 MoE 长得完全不一样。

我这边是:所有 expert 都算一遍,gate 只负责最后加权融合。

而一说 MoE,大家脑子里冒出来的多半是 DeepSeek / Mixtral 那种:几十个 expert,每个 token 只挑两三个来算。

差异大到,我一开始都怀疑是不是自己理解错了 MoE 这个词。

后来顺着这个反差梳理了一下,发现 MoE 压根就不是一种结构,而是一族结构。

把它们区分开的那把钥匙,就是 gate 和 expert 的关系。

这篇笔记就是我的梳理过程。

背景:MoE 从哪来
#

MoE 最早能追溯到 1991 年。当时的结构就已经是现在这个样子了:多个 expert,外加一个 gating network。

动机是"分而治之":把输入空间划分成若干区域,每个 expert 只负责其中一片,gate 学会这个划分。这样每个 expert 只需要拟合一个较简单的局部函数,比一个复杂的全局模型好训。

注意这和 bagging/boosting 那种"一堆独立模型投票"不是一回事——MoE 的专家有分工,而且 gate 是按输入来加权,是动态的。这个后面还会再提。

核心公式长这样:

$$y = \sum_{i=1}^{N} g(x)_i \cdot E_i(x)$$

其中 \(E_i(x)\) 是第 \(i\) 个 expert 的输出,\(g(x)\) 是 gate 输出的权重,\(N\) 是 expert 个数。输出就是所有 expert 输出的加权和。

注意,这个最原始的版本里,所有 expert 都要算一遍,gate 只是决定最后的加权比例。也就是说,最早的 MoE 是稠密的。

那它为什么沉寂了二十年?原因和当时整个神经网络的情况差不多:训练太难、算力不够、没有合适的场景。expert 学不出清晰分工,gate 也不稳定,看起来就是个更复杂的 MLP。

转机在 2017 年,一个关键改动:gate 不再给所有 expert 加权,而是只选出 top-k 个,只有被选中的 expert 才参与计算

这个改动的意义在于,它把 MoE 从"加权融合结构"变成了"条件计算":参数总量可以疯狂涨,但每次 forward 只激活一小部分。

graph LR
    subgraph 萌芽期[1991–2016 萌芽期]
        A[1991
稠密 MoE
全部专家都算] --> B[1994
分层 MoE] end subgraph 复兴期[2017–2021 复兴期] C[2017
稀疏 top-k
只激活部分] --> D[2020 GShard
大规模 Transformer] --> E[2021 Switch
top-1 更简化] end subgraph 爆发期[2024 爆发期] F[Mixtral 8x7B] --> G[DeepSeek-V2/V3
细粒度 + shared expert] end B --> C E --> F

先插一句名词辨析,不然读论文容易乱:gate、gating network、router,说的是同一样东西——那个决定"谁参与、权重多少"的组件。下文统一叫 gate。

中间还有一条常被忽略的支线:多任务学习里的 MMoE,用的恰恰是 1991 年那种稠密形态——所有 expert 都算,gate 只做加权。它在推荐系统里用得非常多,影响力不比 LLM 里的 MoE 小。

到 2024 年,Mixtral 和 DeepSeek-V2 让 MoE 真正走进工业界,从"论文里的结构"变成了"默认选项"。

所以到今天,“MoE” 这个词下面其实压着两种完全不同的用法:稀疏的和稠密的。这也是我写这篇笔记的初衷。

为什么要用 MoE
#

先说 LLM 里的稀疏 MoE,它的动机一句话就能说清楚:在算力约束下换容量

稠密模型的参数和 FLOPs 是严格绑定的:参数翻倍,算力基本也要翻倍。

而稀疏 MoE 把这两者解耦了:总参数可以很大,但每个 token 只经过 top-k 个 expert,FLOPs 只由激活的那部分参数决定。

graph LR
    subgraph Dense[稠密模型]
        D1[总参数 = 激活参数
671B 激活 671B] end subgraph Sparse[稀疏 MoE] S1[总参数 671B
DeepSeek-V3] --> S2[每个 token 只激活 37B
top-k 专家] end Dense -->|容量相当| Sparse S2 --> Cost[推理成本 ≈ 正比于激活参数
知识容量 ≈ 正比于总参数]

数字上感受一下:DeepSeek-V3 总参数 671B,每个 token 激活 37B;Mixtral 8x7B 总参数 46.7B,激活 12.9B。

参数涨了十几倍,单 token 算力基本没涨。

说白了就是:推理成本大致正比于激活参数,知识容量正比于总参数。花一样的钱,装更多的知识。

顺带一提,这也正是 MoE 和 bagging/boosting 这类集成的根本区别:集成对任何输入都走同一条路(所有模型都算再平均),是静态的;MoE 是 gate 根据当前输入动态决定走哪条路,不同输入走不同路径。

第二个顺带的好处是专家分工(expert specialization):路由会让不同的 expert 自然地学会处理不同类型的输入,比如有的对数学类 token 敏感,有的对代码类敏感。不少工作都在琢磨怎么让分工更彻底。

分工本身也带来一点可解释性的可能——可以看看哪些 token 被路由到了哪个 expert。不过这个属于锦上添花,不是 MoE 被大规模采用的核心理由。

至于 gate 在后的稠密 MoE(比如 MMoE),动机完全不同:不是省算力,而是让多个任务之间做软共享

每个任务通过自己的 gate,从同一批 expert 里各取所需,而不是把所有任务硬塞进一个共享底层。这个后面展开讲。

分类学:gate 在哪,MoE 就是哪种
#

前面那个公式 \(y = \sum_{i=1}^{N} g(x)_i \cdot E_i(x)\) 其实没有规定计算顺序。

而计算顺序,恰恰是最本质的分野。

  • gate 在 expert 之前:先算 gate,根据 gate 的结果决定哪些 expert 参与计算。只有被选中的 expert 才动起来,这就是稀疏 MoE。
  • gate 在 expert 之后:所有 expert 都先算完,gate 再对结果做加权。这就是稠密 MoE。

一句话:gate 在前,是"选人干活";gate 在后,是"都干完再汇总"。

这个位置差异决定了算力、训练方式、适用场景全都不一样。下面分别展开。

gate 在前:稀疏 MoE
#

稀疏 MoE 的 forward 逻辑用伪代码表示大概是这样:

 1def forward(self, x):
 2    # x: [batch, dim]
 3    logits = self.gate(x)                     # [batch, n_experts]
 4    probs = F.softmax(logits, dim=-1)
 5    topk_probs, topk_idx = probs.topk(self.top_k, dim=-1)
 6
 7    out = 0
 8    for prob, idx in zip(topk_probs.unbind(1), topk_idx.unbind(1)):
 9        out += prob.unsqueeze(1) * self.experts[idx](x)
10    return out

三个关键点:

  1. gate 本身通常就是一个线性层加 softmax,输出 \(N\) 个权重。
  2. top-k 这一步是不可导的离散选择,所以梯度只能通过被选中的那几个 expert 传下去,这也是后面各种训练难点的根源。
  3. 没被选中的 expert 压根不 forward,这是省算力的来源,但也是负载均衡问题的来源。

顺带说一个实现细节:top-k 之后要不要把被选中 expert 的权重重新归一化(除以它们之和)?不同实现选择不同,有的保留原权重,有的会 renormalize。细节不影响理解,但看源码时别被这个差异绕晕。

graph TD
    x["输入 x: [batch, dim]"] --> W["Gate 网络
linear + softmax
shape: [batch, n_experts]"] W --> S[Softmax
row-wise] S --> T[Top-k 选择
k=2] T --> E1["Expert 1: E₁(x)"] T --> E2["Expert 2: E₂(x)"] E1 & E2 --> P["概率加权
p₁, p₂ = softmax 输出"] P --> y["输出 y = p₁·E₁(x) + p₂·E₂(x)"]

这条线的主要区别,看 gate 怎么选就行:

  • Shazeer 2017:noisy top-k。在打分上加一点噪声再取 top-k,让专家之间的选择有点随机性,不至于一上来就固定死。
  • Switch Transformer:更激进,k 直接设成 1,只挑得分最高的那个。最简、最好训。
  • Mixtral 8x7B:8 个 expert、top-2。
  • DeepSeekMoE:把 expert 拆细,再加一批每个 token 都必过的 shared expert。这个放到后面"共享"那个维度细说。
graph TD
    x["输入 x: [batch, dim]"] --> E["所有 Expert
并行计算 E₁(x)...Eₙ(x)"] E --> W["Gate 网络
linear + softmax
shape: [batch, n_experts]"] W --> S[Softmax
row-wise] E & W --> F["加权求和
y = Σ gᵢ·Eᵢ(x)"]

gate 在后:稠密 MoE
#

稠密 MoE 的 forward 逻辑是另一种写法:

1def forward(self, x):
2    # x: [batch, dim]
3    g = self.gate(x)                          # [batch, n_experts]
4    outputs = [expert(x) for expert in self.experts]  # 每个 expert 都算
5    stacked = torch.stack(outputs, dim=-1)    # [batch, dim, n_experts]
6    out = (stacked * g.unsqueeze(1)).sum(dim=-1)
7    return out

对比上面那段代码,差别一目了然:这里没有 top-k,没有离散选择,所有 expert 都 forward 了,gate 只出现在最后一步的加权里。

所以它本质上是完全可微的,训练上没有稀疏 MoE 那些路由问题。

这也是它的代价:expert 的数量不能大。每个样本都要过全部 N 个 expert,N 一上来算力就线性爆炸,所以稠密 MoE 的 N 通常是个位数。

这决定了它的适用边界:任务是"融合几路视角",而不是"从一大堆专家里挑人"。

这条线的代表是 MMoE 和 PLE,场景基本都在多任务学习。

稠密 MoE 的代表:MMoE 和 PLE
#

MMoE(Multi-gate Mixture-of-Experts)解决的是这样一个问题:多个任务共享一个底层网络时,任务之间会互相拉扯(比如 CTR 任务学到的表征对 CVR 任务有害),完全分开又浪费数据和参数。

它的做法是:底层换成一组 expert,每个任务 k 配一个自己的 gate \(g^k\),得到自己的融合结果,再走各自的任务塔:

$$y_k = h^k\left(\sum_{i=1}^{N} g^k(x)_i \cdot E_i(x)\right)$$

其中 \(E_i\) 是共享的那批 expert,\(g^k\) 是任务 k 自己的 gate,\(h^k\) 是任务 k 自己的塔。

每个任务用不同的权重组合同一批 expert 的输出,任务之间既有共享(同一批 expert)又有隔离(各自的 gate 和塔),这比 hard sharing 灵活,比完全独立省参数。

注意这里的计算形态:所有 \(E_i\) 都要算(N 通常就几个),gate 在 expert 之后,只负责加权。

PLE(Progressive Layered Extraction)在 MMoE 基础上往前走了一步:它观察到 MMoE 里所有 expert 都是所有任务共用的,任务之间还是可能互相污染。

于是它把 expert 显式分成 shared expert 和 task-specific expert 两类——shared 部分每个样本都过,task-specific 部分只有对应任务用。gate 只负责 shared 部分的加权。

多任务里"哪些参数共享、哪些隔离"这个老问题,PLE 给了一个很工程化的答案。

这两个结构在推荐系统的 CTR/CVR 多目标建模里用得非常多。

它们和 LLM 稀疏 MoE 的差别,不在于"高级/低级",而在于目的:前者要的是任务间的软共享,后者要的是省算力。

一个特例:Soft MoE
#

前面说 gate 在前的稀疏 MoE 会丢掉没被选中的 expert 的信息,gate 在后的稠密 MoE 又省不了算力。

有没有折中?有,Soft MoE 给出了一个很漂亮的答案。

它的核心是"slot":引入一个可学习矩阵 \(\Phi \in \mathbb{R}^{d \times m}\),可以理解成 m 个可学习的"位置"。

对输入 \(X\)(比如一帧图像的 n 个 token),先算每个 slot 对每个 token 的软权重:

$$\phi = \text{softmax}(X \cdot \Phi)$$

然后用这些权重把 token 揉成 m 个混合 token:

$$\tilde{X} = \phi^{\top} \cdot X$$

最后每个混合 token 过一个 expert,再按同样的权重散回原位置。

整个过程没有 top-k,没有离散选择,完全可导;但 expert 的输入从 n 个 token 变成了 m 个混合 token,算力还是省下来了(n 远大于 m 时)。

值得留意的是,Soft MoE 里的 \(\Phi\) 是可学习的,但它不依赖输入 \(x\)——也就是说,这里没有传统意义上"针对每个输入做决策"的 gate,而是一组学好的静态混合比例。

这和前面两种 MoE 的 gate 都不一样。严格来说它更接近"可学习的输入降采样",但大家都把它归进 MoE 家族,因为它保留了"多个 expert + 软分配"的内核。

Soft MoE 出来主要是为了视觉场景(ViT),它对稀疏 MoE 的几个训练问题(负载均衡 loss、专家坍塌)免疫,是个很有意思的观察点。

graph TD
    X["输入 Token序列 X: [n_tok, dim]"] --> Φ["Slot矩阵 Φ:
可学习 [dim, m]"] Φ --> W["权重计算
φ = softmax(X·Φ)
shape: [n_tok, m]"] W --> Xh["混合 Token
X̃ = φᵀ·X
shape: [m, dim]"] Xh --> E["每个 Slot 过一个 Expert
E₁(X̃₁), …, Eₘ(X̃ₘ)"] E --> y["输出加权散布
y = Σ φᵢ · Eᵢ(X̃ᵢ)"]

其余三个次级维度
#

gate 的位置是主刀,但看论文时还有三个维度值得一起问。

graph TD
    subgraph D1[维度二:gate 的输入]
        A1[只看 x
条件路由
LLM 常见] A2[只看任务 id
静态路由
多任务常见] A3[x + 任务 id
混合] end subgraph D2[维度三:expert 的粒度与共享] B1[大 expert
一整个 FFN] B2[细粒度 expert
拆小 + shared expert 必过
DeepSeekMoE] end subgraph D3[维度四:gate 结构] C1[linear + softmax
基准] C2[noisy top-k
Shazeer] C3[top-1 argmax
Switch] C4[aux-loss-free bias
DeepSeek] C5[hash 路由
无参数] end
  • gate 的输入:只看 \(x\)(条件路由,每个样本路径不同);拼任务 id(静态路由,同任务内路径相同)。
  • expert 粒度与共享:可以是一整个 FFN,也可以拆细(细粒度 expert + shared expert 承接公共知识)。
  • gate 结构:从 linear+softmax 基准 → 加噪声 → top-1 → 用 bias 修正负载 → hash 路由(完全没有可学习参数)。

这四个维度组合起来,基本覆盖了主流 MoE 论文的设计空间。读论文的时候拿这四个问题套一遍,架构图很快就通了。

已知的坑
#

这部分我没有在生产环境里实际踩过,属于"读论文读来的坑",简单记一笔。

稀疏 MoE 最大的两个坑是同一枚硬币的两面:

  • 负载不均:gate 会逐渐"偏爱"少数 expert,大量 token 挤向同一批 expert,其他 expert 闲着。工程上每个 expert 的显存要按最忙的那个来分配,闲着的就是浪费。
  • 专家坍塌:训练早期 gate 一旦形成赢家通吃,收不到 token 的 expert 就没有梯度,慢慢死掉,模型退化成一个小的稠密网络。
graph LR
    T[Token 流] --> G[Gate
赢家通吃] G -->|80% token| E1[Expert 1
过载] G -->|15% token| E2[Expert 2
一般] G -->|5% token| E3[Expert 3
饿死 → 梯度为 0 → 死掉] G -->|0% token| E4[Expert 4
完全闲置]

主流的解法有几条路线:

  • 加负载均衡 aux loss:让每个 expert 分到的 token 比例,和它拿到的平均 gate 权重都尽量均匀。缺点是这个 loss 会干扰主任务梯度,强度怎么调是个手艺活。
  • 限制每个 expert 的 capacity,超出部分直接丢弃 token。简单粗暴,但要丢数据。
  • DeepSeek 的 aux-loss-free 路线:给每个 expert 的 gate 打分加一个可调 bias,训练中按负载动态修正——超载的减一点,饿着的加一点。不干扰梯度,也不用丢 token。

工程侧还有 all-to-all 通信、expert 并行这些分布式训练的坑,和路由本身关系不大,这里不展开。

另外提一句:稀疏 MoE 对微调也不友好,负载均衡在继续训练时容易再次失衡,这也是社区里 MoE 微调经验帖多的原因。

常见用途速览
#

graph TD
    subgraph MoE[MoE 家族]
        Sparse[稀疏 MoE
gate 在前
省算力换容量] Dense[稠密 MoE
gate 在后
多任务软共享] Soft[Soft MoE
slot 混合
视觉省算力] end Sparse -->|LLM| LLM[DeepSeek / Mixtral / Qwen] Sparse -->|语音| ASR[MoE ASR] Sparse -->|视觉| ViT[V-MoE] Dense -->|推荐系统| Rec[MMoE / PLE
CTR/CVR] Dense -->|金融领域| Fin[多任务加权融合] Soft -->|视觉| SoftViT[Soft MoE / ViT]
  • LLM:DeepSeek-V3/R1、Mixtral、Qwen。省推理算力,稀疏路由。
  • 推荐系统多任务:MMoE/PLE 系。任务软共享,稠密加权。工业界存量应用可能比想象的多。
  • 视觉:V-MoE(稀疏)、Soft MoE(软 slot)。给大 ViT 省算力。
  • 语音:MoE ASR,结构上走 LLM 稀疏那套。
  • 金融领域:我自己接触到的用法更接近 gate 在后的加权融合形态(场景就不展开了),和 LLM 那套完全不同。这也是我写这篇文章的最初动机。

几个容易混淆的概念
#

顺带澄清几个名字里带 expert 或者和 MoE 长得很像、但本质不同的东西,免得看文章时对不上号:

  • ensemble(集成):和 MoE 最本质的区别是静态 vs 动态。bagging/boosting 里不管输入是什么,所有基学习器都要算一遍再投票/平均,路径是静态的;MoE 的 gate 会根据当前输入决定走哪条路(稀疏的直接只激活部分 expert),不同输入走不同路径,是动态的。另外集成也没有"专家分工"这回事。
  • Mixture-of-Agents (MoA):2024 年出现在 LLM 推理侧的一类方法,让多个 agent/LLM 轮流产出、互相参考再汇总。名字像 MoE,但它是推理时的多模型协作,不是模型内部的一个层,不共享参数也不做门控路由。
  • 知识蒸馏/专家混合(routing in compression):有一些蒸馏工作把大模型的层"打包"成 expert 再路由,属于 MoE 的衍生用法,但目的不是省算力而是压缩/迁移。

这些都不影响本文的主线,只是"名字里带 MoE 的字"不等于就是 MoE。

总结
#

三类用法放在一张表里对比:

稀疏 MoE稠密 MoESoft MoE
gate 位置expert 之前expert 之后静态可学习 slot
计算形态只算 top-k全算再加权全算但输入是混合 token
可导性离散选择,不可导完全可导完全可导
主要动机省算力换容量多任务软共享视觉大模型省算力
代表Switch / Mixtral / DeepSeekMMoE / PLESoft MoE

拿到任何一篇 MoE 论文,先问三个问题:

graph TD
    Q1{gate 在哪?}
    Q1 -->|expert 之前| S[稀疏 MoE
省算力换容量] Q1 -->|expert 之后| D[稠密 MoE
多任务软共享] S --> Q2{gate 输入?} D --> Q3{expert 怎么组织?} Q2 -->|只看 x| S1[条件路由
LLM 常见] Q2 -->|拼任务 id| S2[任务路由
多任务常见] Q3 -->|全共享| D1[MMoE 形态] Q3 -->|shared + specific| D2[PLE 形态]
  1. gate 在哪:expert 之前还是之后?这决定了它是稀疏还是稠密。
  2. gate 的输入是什么:只有 x,还是带任务信息?
  3. expert 怎么组织:有没有共享,粒度多细?

问完这三个问题,任何 MoE 的架构图都能对号入座。剩下的都是这三个问题的具体答案。

参考资料
#

Related

【施工中】torch2trt 学习笔记

前言 # 偶然发现了 torch2trt 的模型转换方案,思路是直接将pytorch op映射到TensorRT的python api. 在pytorch进行每个op forward的时候,tensorrt也相应往network上添加op. 这里会先涉及torch2trt的使用,后面会补充这个转换工具的代码学习