第 21 讲 · LLM:混合专家(架构、内核、并行)· Mixture-of-Experts

Advanced Topics in Machine Learning Systems (LLM Edition) · Lecture 21。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:CMU 15-779(Fall 2025,Zhihao Jia),Lecture 21 · Wed 11/19 · LLMs: Mixture-of-Experts (Architectures, Kernels, Parallelism)(slides 标题页写的是 Lecture 18,课表上是第 21 次课)。 必读论文:课表上这一讲没有列必读论文。本讲从 slides 引用的论文里选两篇做精读:W. Fedus, B. Zoph, N. Shazeer, Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity(JMLR 2022);T. Gale, D. Narayanan, C. Young, M. Zaharia, MegaBlocks: Efficient Sparse Training with Mixture-of-Experts(MLSys 2023)。 依据说明:本讲以原课 slides 18-mixture-of-experts.pdf(28 页)为主线,它分三部分:

  • MoE 的结构:Transformer 块回顾、前馈层占大部分参数、门控与 top-K、Transformer + MoE、GLaM / DeepSpeed-MoE 的数字、DeepSeek-V3 的共享专家、混合头注意力(MoH)、MoE 的好处;
  • MoE 的内核:单批与批量、批量 MoE = 块稀疏矩阵乘法(MegaBlocks)、置换 → 分组计算 → 反置换、CSR 与分组 GEMM、用前缀和求置换下标;
  • MoE 的并行:专家并行与专家容量(Switch Transformer)、负载均衡损失(GShard)、注意力用 AllReduce + MoE 用 AlltoAll 的混合并行。

另外参考了 GShard(ICLR 2021)、DeepSpeed-MoE(ICML 2022)、Mixtral(2024)、DeepSeek-V3 技术报告(2024)、MoH(2024)、GLaM(ICML 2022)的公开内容。论文数字只做转述。

moe.py(路由、负载均衡、五种专家计算写法、块稀疏格式、H100 上的代价模型、专家并行的模拟、可训练的 MoE 层)和全部实验都是本讲义自己设计的;训练实验用的是 d = 32 / 64 的小模型,GPU 上的时间来自 roofline 模型,不是测量。 配套代码 Code:code/lecture21/(5 个脚本 + 一个共享模块;需要 numpy 和 torch;最长的第一次运行约 2.5 分钟) 预计阅读时间:3 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标

学完这一讲,你应该能够:

  1. 算出一个 MoE 模型的总参数和每个 token 激活的参数,解释”参数多了 E 倍、计算只多了 k 倍”。
  2. 写出 top-k 门控的公式和负载均衡损失,说明不均衡从哪来、辅助损失和 DeepSeek-V3 的偏置项各怎样治它。
  3. 用前缀和求出置换下标和 CSR 的 indptr;比较”每个专家都算""逐专家循环""按容量补齐""分组 GEMM""块稀疏”五种写法的计算量、内核数和丢 token 的情况。
  4. 解释专家容量的取舍(丢 token vs 算补齐),以及 MegaBlocks 怎样用块稀疏矩阵乘法同时避开两者。
  5. 说明为什么单个请求时 MoE 很省、批量时却要读几乎所有专家,以及 MoE 要多大的批才变成计算受限。
  6. 画出专家并行的 dispatch / combine 两次 all-to-all,估算它的通信量;说明分层 all-to-all、限制节点数的路由、冗余专家各解决什么问题。

0.2 先修与本讲范围

默认你已经会本讲只给直觉,后面再展开
第 5 讲:Transformer 块、SwiGLU 前馈层、LikesWorld 小模型MLA、多 token 预测等 DeepSeek-V3 的其他部分
第 12 讲:AllReduce / all-to-all 的 α-β 模型DualPipe 的完整调度(第 14 讲的流水线并行是基础)
第 14 讲:张量并行、流水线并行真正的 GPU 分组 GEMM / 块稀疏内核的实现
第 1 讲:roofline、算术强度专家的剪枝、蒸馏、量化(附加讲 A2 讲一般的压缩)

0.3 本讲地图

§1  为什么要 MoE:参数在哪里,"参数多 ≠ 计算多"      ← 代码 01 演示 1、2、4
§2  MoE 的结构:门控、负载均衡、各家的变体             ← 代码 02、04 演示 1–2
§3  MoE 的内核:置换、前缀和、五种写法、块稀疏         ← 代码 01 演示 3、03、02 演示 4
§4  推理时的 MoE                                        ← 代码 04 演示 3–4、05 演示 4
§5  MoE 的并行:专家并行与混合并行                     ← 代码 05
§6  把它们放在一起
§7  论文精读:Switch Transformer;MegaBlocks
§8 小结  §9 常见误区  §10 练习  §11 延伸阅读  §12 下一讲预告

0.4 记号

记号含义
N一批里的 token 数
d、f隐藏维度;每个专家的前馈宽度
E、k专家数;每个 token 选几个专家
n_e分到专家 e 的 (token, 槽位) 数,Σ_e n_e = N·k
C专家容量(每个专家最多处理几个 token)
6·d·f一个 SwiGLU 专家处理一行(一个 token)的 FLOPs(3 个矩阵,每个 2·d·f)

0.5 关于实验

  • 记忆任务(01、02 号脚本):4096 个键 (a, b),每个对应一个随机的值(16 种)。a、b 各有一个嵌入,相加后经过前馈层再输出。值没有规律,只能靠前馈层的参数记住——用来看”前馈层能存多少”。
  • MoE 小 Transformer(04 号脚本):第 5 讲的 LikesWorld 模型(2 层、d = 64),每层的 SwiGLU 换成 MoE,每个 token 用到的前馈宽度保持 128 不变。
  • H100 上的时间(01、03 号脚本):roofline 模型 + tile 量化 + 每个内核 4 µs 的启动开销;专家并行(05 号脚本):第 12 讲的 α-β 模型,真的在模拟设备之间搬数据。

1. 为什么要 MoE · Why Mixture-of-Experts

1.1 参数都在前馈层(代码 01 演示 1)

slides 先回顾 Transformer 块:自注意力 + 前馈层(FFN),各带残差和归一化。一个关键的事实是大部分参数在前馈层。按公开配置只数矩阵:

模型总参数每个 token 激活激活 / 总前馈层占总参数
Llama-2-7B(稠密)6.74B6.74B100%64.2%(每个块里 66.8%)
Mixtral-8x7B46.70B12.88B27.6%96.6%
DeepSeek-V3671.03B37.55B5.6%98.0%

Mixtral 论文的说法是”每个 token 能用到 47B 参数、只激活 13B”,DeepSeek-V3 报告是”671B 总参数、每个 token 激活 37B”,和我们数的一致(DeepSeek-V3 的激活参数我们算出 37.6B,两个嵌入矩阵都算在内;不算输入嵌入是 36.6B)。

DeepSeek-V3 的参数是 Llama-2-7B 的 100 倍,每个 token 的计算只是它的 5.5 倍。这就是 MoE 的全部吸引力:参数(容量)和计算解耦了。

1.2 为什么不直接把前馈层加宽

slides 第 4 页的说法:稠密的前馈层里”一切都混在一起”,每个 token 都要经过所有参数;把特征维度加宽,计算量随之平方增长(d 翻倍、d_ff = 4d 也翻倍,前馈层的参数和计算都变成 4 倍)。

MoE 的想法(slides 第 6 页):放 E 个专家,每个都是一个前馈层;让每个专家负责一部分输入。一个 token 只经过 k 个专家,于是:

  • 参数 ≈ E 倍(E 份前馈层);
  • 每个 token 的计算 ≈ k 倍(k 份前馈层)+ 一个很小的路由器(d × E)。

slides 把这叫”FLOPs 随模型大小亚线性增长”。

1.3 前馈层存知识:在玩具任务上看”容量”(代码 01 演示 2)

一个直观的理解是把前馈层看成”键值记忆”:第一层的每一列像一个”键”,第二层的对应行像”值”。我们用一个只能靠前馈层记住的任务来测它的容量:

前馈层前馈参数每个 token 用到记住的比例
稠密 f = 323,0723,07259.9%
稠密 f = 646,1446,14493.3%
稠密 f = 12812,28812,288100.0%
稠密 f = 25624,57624,576100.0%
MoE 8 × (f = 32),选 124,8323,32899.9%
MoE 8 × (f = 32),选 224,8326,400100.0%

8 个 f = 32 的专家、每个 token 只走 1 个:每个 token 的计算和”稠密 f = 32”一样(只多了路由器的 256 个参数),记住的比例从 59.9% 涨到 99.9%。选 2 个专家多花一倍计算,记住的比例几乎一样——容量主要由总参数决定。这个任务在稠密 f = 128 时已经饱和,所以更公平的对照是同样计算量的 f = 32 和两倍计算量的 f = 64(93.3%)。

1.4 MoE 的好处在论文里怎么量(代码 01 演示 4)

slides 第 10 页给了两组数字,第 13 页总结了三条好处(参数多但计算不多;给定质量下吞吐更高、延迟更低;分布式训练更高效)。几篇论文的说法:

  • DeepSpeed-MoE:1.3B+MoE-128(总参数 52B,每个 token 激活 1.3B)的验证损失和 6.7B 稠密模型相当——训练算力约少 5 倍。
  • Switch Transformer:64 个专家的 Switch-Base 和 T5-Base 每个 token 的 FLOPs 相同,达到同样的质量步数少 7.5 倍;Switch-C(1.6T 参数、2048 个专家)达到固定困惑度比 T5-XXL 快 4 倍。
  • GLaM:1.2T 参数(slides 写 1.3T)、64 个专家选 2,每个 token 激活 96.6B;29 个任务平均超过 GPT-3,推理 FLOPs 约一半、训练能耗约 1/3。

这些比较都是在”同样的每 token 计算量”或”同样的质量”下做的。代价是总参数(显存)多了几十倍:训练时要靠专家并行把参数分到很多卡上(§5),推理时要靠大批量把读参数的开销摊薄(§3.1)。


2. MoE 的结构 · How MoE works

2.1 门控与 top-k(slides 第 7 页;代码 02 演示 1)

一个 MoE 层(slides 用 Mixtral 的写法,一个 token、选 2 个专家):

g=softmax(Wg x),I={e0,e1}=Topk(g),si=geige0+ge1,y=s0 FFNe0(x)+s1 FFNe1(x).g = \mathrm{softmax}(W_g\,x),\qquad I = \{e_0, e_1\} = \mathrm{Top}_k(g),\qquad s_i = \frac{g_{e_i}}{g_{e_0} + g_{e_1}},\qquad y = s_0\,\mathrm{FFN}_{e_0}(x) + s_1\,\mathrm{FFN}_{e_1}(x).

  • 路由器(router / gating network)只是一个 d × E 的线性层加 softmax;
  • top-k 是贪心地选分数最高的 k 个;
  • 门控权重在选中的 k 个里归一化,没选中的专家不计算,也拿不到这个 token 的梯度;
  • 每个专家的结构可以不同(Mixtral 是 SwiGLU)。

演示 1 在一个 token 上手算:选出专家 {0, 5},权重 0.743 / 0.257,和”每个专家都算一遍、再按权重取”的结果完全相同。

2.2 Transformer + MoE(slides 第 8–9 页;代码 04 演示 1)

把 Transformer 块里的前馈层直接换成 MoE 层,别的都不变。具体放法各家不同:

  • Switch / GShard 每隔一层放一个 MoE 层;
  • Mixtral 每层都是 MoE;
  • DeepSeek-V3 前 3 层是稠密的,其余 58 层是 MoE。

04 号脚本把第 5 讲小模型的两层前馈都换成 MoE,每个 token 用到的前馈宽度保持 128 不变:

模型每层前馈参数每 token 用到答案准确率测试损失每层 最忙/平均
稠密 SwiGLU(f = 128)24,57624,576100.0%0.7926—
A:8 个专家选 2(f = 64)+ 辅助损失98,81624,576100.0%0.79301.78 / 1.79
B:1 共享 + 16 选 3(f = 32)+ 偏置项105,47224,57695.9%0.80241.86 / 1.83

A 和稠密模型一样学会了。LikesWorld 考的是”回头查”(注意力),前馈层的容量不是瓶颈,所以 MoE 在这里不会更好——它的好处要在需要记住很多东西的任务上才看得到(§1.3)。B 只到 95.9%。排查(见代码 README):最后 500 步冻结偏置项 95.7%,改用辅助损失 97.7%,换一个随机种子是 100%——差距主要来自这个小模型训练的随机性,不能据此说 DeepSeek 式的结构更差。

2.3 各家的变体

变体做法动机
Switch Transformertop-1;门控 y = p_i(x)·E_i(x)(不归一化)路由计算少、每个专家的容量可以减半、通信减半
GShardtop-2;第二个专家按 2·g₂ 的概率随机派发;分组派发第二个专家权重很小时不值得占容量
Mixtral8 个专家选 2,每层都是 MoE—
DeepSpeed-MoE 的 PR-MoE每个 token 固定经过一个稠密 MLP + 一个选出的专家(Residual-MoE);越深的层专家越多(Pyramid)论文发现:后半部分的层更需要专家;固定一个 + 选一个和选两个效果相当,但通信少、训练快 10% 以上
DeepSeekMoE / DeepSeek-V3(slides 第 11 页)1 个共享专家(每个 token 都经过,不路由)+ 256 个细粒度路由专家(宽 2048)选 8;sigmoid 打分共享专家放”大家都需要的知识”,细粒度专家组合更多
混合头注意力 MoH(slides 第 12 页)把注意力头当专家:一部分共享头总是激活,其余由路由器选不是所有头都同样重要;MoH-LLaMA3-8B 用 75% 的头,14 个基准平均比 LLaMA3-8B 高 2.4 个点(论文数字)

共享专家可以看成 Residual-MoE 的推广:一部分计算所有 token 都做,路由专家只负责”修正”。

2.4 负载均衡(slides 第 27 页;代码 02 演示 2)

问题:多数 token 被派给少数专家。slides 的解释是”富者更富”——受欢迎的专家拿到更多 token、训练得更好、于是更受欢迎;其他专家得不到足够的训练数据。

不均衡从哪来:在我们的记忆任务上,路由器随机初始化时负载本来比较均匀,不加均衡也不会坍缩。但只要给所有输入加一个共同的大方向(真实 LLM 的隐藏状态就有很大的公共分量),路由器的打分就被 w_e·c 主导,和 token 本身关系不大——第 0 步最忙的专家就分到了 69%。

辅助损失(GShard、Switch):

Laux=α⋅E∑e=1Efe Pe,\mathcal{L}_{\text{aux}} = \alpha \cdot E \sum_{e=1}^{E} f_e \, P_e,

f_e 是这一批里分给专家 e 的比例(数出来的,不可导),P_e 是路由器给专家 e 的平均概率(可导)。完全均匀时 f_e = P_e = 1/E,损失 = α;坍缩到一个专家时接近 α·E(演示 1:7.30)。梯度只经过 P_e,让被分得多的专家的路由概率下降。Switch 用 α = 10⁻²,在 10⁻¹ 到 10⁻⁵ 之间扫过。

偏置项(无辅助损失,DeepSeek-V3):给每个专家一个偏置 b_e,只在选专家时加到分数上(门控权重仍用原来的分数)。每一步结束后,负载高于平均的专家 b_e 减 γ,低于平均的加 γ(γ = 0.001,最后 500B token 设成 0)。它不往主损失里加任何项。DeepSeek-V3 还加了一个系数极小(10⁻⁴)的序列级辅助损失,防止单条序列内极端不均衡。

两个种子平均的结果:

做法第 0 步最忙训练后最忙最忙/平均空专家记住的比例
没有共同方向,不做均衡18.6%17.9%1.43×099.9% / 99.8%
不做均衡69.1%46.5%3.72×1.599.3% / 89.2%
辅助损失 α = 0.0169.1%22.1%1.77×0100.0% / 99.3%
辅助损失 α = 0.169.1%13.8%1.11×099.2% / 100.0%
偏置项 γ = 0.00169.1%13.0%1.04×099.0% / 93.7%

几点观察:

  • 不做均衡时平均有 1.5 个专家一个 token 都没分到,参数白放着;记住的比例一个种子几乎没掉,另一个掉了 10 个百分点(这个任务的总容量有富余)。
  • 不均衡更直接的代价是效率:专家并行时最忙的那块卡要多算 3.7 倍,其他卡等着它(§5.5)。
  • 偏置项几乎做到完全均匀,代价是它每一步都在改路由,专家记住的东西会被”换走”一部分(一个种子 93.7%)。

2.5 一个坑:top-1 时不能把门控归一化(代码 02 演示 3)

Switch 的 top-1 用 y = p_i(x)·E_i(x):门控值就是路由器输出的概率,路由器因此可导。如果照搬 top-k 的写法在选中的专家里归一化,k = 1 时权重恒为 1,路由器的梯度恰好是 0(演示 3:1.6×10⁻⁹,Switch 写法是 2.2×10⁻²)。

  • 只归一化、不加辅助损失:路由器停在随机初始化,相当于一个固定的随机划分,在这个任务上大体还能记住(99.5% / 92.8%);
  • 归一化 + 辅助损失:最糟(36.4% / 35.1%)。路由器只受”要均匀”这一个信号驱动,不知道哪个专家擅长什么,路由在训练中被反复改写,专家刚记住的东西就被换走。

2.6 小 Transformer 学到了什么样的路由(代码 04 演示 2)

按 token 的”角色”统计第一选择的专家(模型 A):

  • 同一个词大多去同一个专家(两层分别 82% / 74%);
  • 两层都把”问题里的人名”几乎全部交给同一个专家(第 1 层 100% 去专家 3,第 2 层 99% 去专家 1),和”事实里的人名”分开——第 1 层的前馈层在注意力之后,已经知道”前一个词是 q”;
  • “答案”和”事实里的物品”去的专家分布很像,它们是同一类词。Mixtral 论文 §5 也发现路由更多和语法 / 词的类型对齐,而不是和领域(数学、生物……)对齐;
  • 相邻 token 第一选择相同的比例低于随机(第 1 层 3% vs 17%):LikesWorld 里相邻的词几乎总是不同角色。Mixtral 在真实文本上看到的是高于随机——这个统计取决于数据的结构。

3. MoE 的内核 · Optimizing MoE kernels

3.1 单个请求 vs 一批请求(slides 第 15–16 页;代码 01 演示 3)

单个请求:只有 k 个专家被用到,只需要读这几个专家的权重——解码本来就是访存受限的,读得少就快(slides 第 15 页)。

一批请求:批量矩阵乘法 Y = WX 的批越大,权重的复用越多、越能用上 Tensor Core(slides 第 16 页)。但 MoE 的批里,不同 token 去不同专家:一批 B 个 token(各自独立、均匀地选 k 个)平均碰到 E·(1 − (1 − k/E)^B) 个专家。

BMixtral(8 选 2)DeepSeek-V3(256 选 8)
12.08.0
45.530.5
167.9102.0
648.0222.4
2568.0255.9

B = 16 时 Mixtral 已经平均碰到 7.9 个专家,要读的字节和”同总参数的稠密层”差不多。Mixtral 一层前馈在 H100 上(BF16,roofline):

BMoE:时间 / token稠密·同激活参数稠密·同总参数
1210.34 µs210.34 µs841.36 µs
1652.06 µs13.15 µs52.59 µs
2563.29 µs0.82 µs3.29 µs
40960.71 µs0.71 µs2.85 µs

每读一个参数只做 B·k/E 次乘加。所以稠密层在 B ≈ 295(H100 的拐点)时变成计算受限,Mixtral 要 B ≈ 1181,DeepSeek-V3 要 B ≈ 9451——专家越多越”稀”,需要的批越大。这解释了 DeepSeek-V3 的解码部署:320 块 GPU 做专家并行、每块 GPU 只放 1 个专家,把整个集群的 token 汇集到每个专家上(报告 §3.4.2)。批足够大时,MoE 每个 token 的时间回到”同激活参数的稠密层”,比同总参数的稠密层快 4 倍:MoE 省的是计算,前提是批要大。

3.2 批量 MoE 的四步(slides 第 18 页)

一批 token 进入 MoE 层:

  1. 路由:每个 token 选出 k 个专家和权重;
  2. 置换(permute):把 token 按专家分组,同一个专家的 token 排在一起;
  3. 计算:每个专家处理自己那一组;
  4. 反置换(un-permute)+ 混合:把结果放回原来的顺序,按门控权重加和。

slides 第 17 页的关键视角:批量 MoE 就是一个块稀疏矩阵乘法——把所有专家的计算融合成一个内核以最大化并行度(MegaBlocks)。

3.3 用前缀和求置换下标(slides 第 20–22 页;代码 03 演示 1)

置换要知道”每个 token 的这一份放到缓冲区的第几行”。slides 的方法:把 (N × E) 的 0/1 选择矩阵转置后按行展开(专家优先),做一次前缀和(scan),再变回来:

选择:x0→{0,2}  x1→{0,1}  x2→{0,2}  x3→{1,2}
mask        = [[1,0,1],[1,1,0],[1,0,1],[0,1,1]]
cumsum(flatten(mask.T)).reshape(3,4).T = [[1,3,6],[2,4,6],[3,4,7],[3,5,8]]
置换下标(mask 为 1 处的数 − 1)= [[0,5],[1,3],[2,6],[4,7]]
indptr = [0, 3, 5, 8]

代码逐个数字复现了 slides。前缀和在 GPU 上可以高效并行,一个内核就能求出所有目标位置,不需要原子操作排队。

slides 第 22 页的讨论题”indptr 怎样从已有的数据里得到”:indptr 就是每个专家那一列前缀和的最后一个数 [3, 5, 8] 前面补一个 0——同一次扫描顺便就得到了。有了 indptr,专家计算就是 slides 第 19 页的 Z = GroupGemm(data, indptr, weights):第 e 组行([indptr[e], indptr[e+1]))乘第 e 个专家的权重,这正是 CSR(压缩稀疏行)格式。

3.4 五种写法(代码 03 演示 2)

同一层 MoE(N = 2048,d = 128,f = 256,8 个专家选 2,最忙/平均 = 1.65):

写法与写法 0 的最大差执行 / 有用 FLOPs矩阵乘内核丢弃
0 每个专家都算一遍04.00×30%
1 逐专家循环2.4e-71.00×240%
2 容量补齐(因子 1.0)2.4e-71.00×317.5%
2 容量补齐(因子 1.25)2.4e-71.25×37.7%
2 容量补齐(动态 = 最忙)2.4e-71.65×30%
3 置换 + 分组 GEMM01.00×30%
4 块稀疏(块 128)1.3e-61.09×30%

(容量写法和丢弃的 token 比的是”同样丢弃”的标准答案。)

五种写法的数学结果一致,区别全在”多算了多少、启动了几次、丢了多少”:

  • 写法 0 多算 E/k = 4 倍;
  • 逐专家循环刚好,但要启动 3E 个小矩阵乘法;
  • 容量写法形状是静态的(TPU / XLA 需要),代价是丢 token 或者算补齐的 0;
  • 分组 GEMM 刚好只算需要的行;
  • 块稀疏每个专家补齐到 128 的倍数,多算 9.4%,专家分到几千个 token 时可以忽略。

3.5 专家容量与丢 token(slides 第 25–26 页;代码 02 演示 4)

GShard / Switch 为了让形状静态,给每个专家一个容量:

C=⌈k⋅NE×capacity_factor⌉.C = \left\lceil \frac{k \cdot N}{E} \times \text{capacity\_factor} \right\rceil.

超出容量的 token 被丢弃,只走残差连接(这一层等于没算);不满的补 0。slides 第 26 页的取舍:容量因子太小,溢出的 token 直接传到下一层;太大,GPU 利用率低(算补齐的 0)。GShard 的算法 1 先处理所有 token 的第一选择、再处理第二选择,“在专家里排第几”又是一个前缀和。

记忆任务上(共同方向 10,两个种子):

做法丢弃算的行数 / 需要的记住的比例
不均衡 + 容量因子 1.042.7%1.00×70.5% / 50.1%
不均衡 + 容量因子 2.013.9%2.00×96.5% / 72.2%
辅助损失 + 容量因子 1.08.6%1.00×92.0% / 92.6%
辅助损失 + 容量因子 1.253.3%1.25×97.7% / 95.1%
辅助损失 + 容量因子 1.50.6%1.50×99.1% / 99.5%
辅助损失 + 不丢0%动态100.0% / 99.3%

均衡和容量是连在一起的:不均衡时容量因子 1.0 丢掉 43% 的 token;辅助损失让负载接近均匀,同样的容量只丢 9%。容量因子 1.5 已经和完全不丢差不多,代价是多算 50%。MegaBlocks 图 2 在 The Pile 上看到同样的趋势:容量因子 1 时验证损失降 0.15,不丢时降 0.26(1.73 倍),并且引用 Tutel 的发现——有的模型容量因子要到 11 才不丢,而且需要的容量会在训练中不可预测地突增。

3.6 MegaBlocks:把 MoE 写成块稀疏矩阵乘法(slides 第 17 页;代码 03 演示 3)

MegaBlocks 图 3 的三步推理:

  • (A) 现有实现用批量矩阵乘法:要求每个专家的 token 数相同、形状相同;
  • (B) 它等价于一个块对角矩阵乘法,对角块大小相同;
  • (C) 把每个对角块拆成很多固定大小的小块(128 × 128),对角块的行数就可以不同——块稀疏矩阵乘法。

具体地,把所有专家的 W1 横着拼成 d × (E·f) 的矩阵,“每个 token 只和自己专家的那几列相乘”等价于输出是块稀疏的(SDD:稀疏 = 稠密 × 稠密);第二层 W2 竖着拼成 (E·f) × d,用 DSD(稠密 = 稀疏 × 稠密)。前向是 SDD → DSD,反向还要 SDDᵀ、DSᵀD、DSDᵀ、DDᵀS。

格式(MegaBlocks 图 5)——3 个专家分到 3 / 1 / 6 行、块大小 2、每个专家宽 4:

    块行 0  ■ ■ · · · ·
    块行 1  ■ ■ · · · ·
    块行 2  · · ■ ■ · ·
    块行 3  · · · · ■ ■
    块行 4  · · · · ■ ■
    块行 5  · · · · ■ ■
  BCSR 行偏移   = [0, 2, 4, 6, 8, 10, 12]
  列下标        = [0, 1, 0, 1, 2, 3, 4, 5, 4, 5, 4, 5]
  行下标(BCOO)= [0, 0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5]
  转置下标      = [0, 2, 1, 3, 4, 5, 6, 8, 10, 7, 9, 11]

三个设计:

  • 块大小 128:作者在 A100 上测了 CUTLASS 各种 tile 的稠密矩阵乘法,128 × 128 一直和最好的一样快;块足够大才能跑出接近峰值的吞吐,元数据也可以忽略(每 16,384 个数才一个下标)。
  • BCSR + 行下标(混合 BCSR-COO):SDD 的每个线程块要知道自己算哪一块;只有 BCSR 时要在行偏移里搜索。也可以多启动线程块让空闲的直接返回,但专家很多时(非常稀疏)这个开销很明显。存行下标后,线程块直接查表。
  • 转置下标:反向传播要按转置顺序遍历稀疏矩阵(例如 dW2 = Hᵀ·dY)。不复制、不转置非零块本身,只存一个”按列优先”的块编号数组——像数据库的二级索引。演示 3 用它算的 DSᵀD 和稠密计算的最大差 2.4×10⁻⁷。

另外,每个专家的 token 数补齐到 128 的倍数,这一步融合进置换内核。

3.7 GPU 上谁快(代码 03 演示 4)

H100 的 roofline 模型(tile 128 行、算力效率 75%、带宽效率 80%、每个内核启动 4 µs,都是假设)。Mixtral 式一层、4096 个 token(预填充 / 训练):

写法负载均匀:时间负载不均(最忙/平均 2.92):时间不均时丢弃不均时激活显存
逐专家循环4.289 ms4.229 ms0%570 MB
容量 1.03.902 ms3.902 ms30.2%537 MB
容量 2.07.791 ms7.791 ms11.5%1074 MB
动态容量(不丢)4.388 ms11.681 ms0%1611 MB
分组 / 块稀疏4.205 ms4.145 ms0%570 MB
  • 负载均匀时容量 1.0 反而最快:每个专家正好 1024 行,没有 tile 量化;分组 GEMM 每个专家各自补到 128 的倍数,多算 8%。差别都在负载不均的时候。
  • 负载不均时动态容量(不丢)比分组 GEMM 慢 2.8 倍,激活显存是 3 倍:所有专家都按最忙的那个补齐。这就是 MegaBlocks 的出发点——它报告 Tutel 因此只能用小 2–8 倍的 micro-batch(表 3),硬件效率下降,这是端到端加速比(最多 4.35 倍)的主要来源。
  • 同样的偏斜落在 64 个细粒度专家上,最忙的是平均的 13.5 倍:动态容量要 52.5 ms,分组 GEMM 仍是 4.1 ms。细粒度专家时逐专家循环要启动 192 个内核,解码一步 1.83 ms vs 分组 GEMM 1.08 ms——专家越细,“一个内核算完所有专家”越重要。
  • 解码一步(64 个 token)时几种写法都在读权重,时间几乎一样(1.07 ms),补齐的行几乎不花时间:访存受限时浪费的是带宽,不是算力。

4. 推理时的 MoE · MoE at inference

4.1 推理时也设容量会怎样(代码 04 演示 3)

两个小模型都是不丢 token 训练的。推理时每篇文档单独前向(N ≈ 30):

容量因子A:丢弃A:答案准确率B:丢弃B:答案准确率
不限0.0%100.0%0.0%95.9%
2.00.6%100.0%1.6%96.2%
1.54.4%100.0%8.4%95.9%
1.2510.1%100.0%14.9%95.7%
1.019.1%99.5%27.0%80.8%

小批量时容量最容易不够:一篇文档里同一种词反复出现(”.”、“likes”),挤在同一个专家上。B 有 16 个更小的专家,每个专家的容量只有约 6 个,丢得更多;共享专家只占它前馈计算的 1/4,兜不住。DeepSeek-V3 报告说训练和推理都不丢 token,推理时用冗余专家来平衡负载(§5.5),而不是靠容量。

4.2 批量解码碰到几个专家(代码 04 演示 4)

同一步里 B 个请求的 token 一共碰到几个专家:

BA 第 1 层A 第 2 层公式(8, 2)B 第 1 层B 第 2 层公式(16, 3)
12.02.02.03.03.03.0
43.83.65.56.25.99.0
165.44.87.98.88.415.4
646.05.28.010.09.716.0

真实路由明显少于”均匀独立”的公式:这里的批来自同一种文档、同一个位置,token 高度相关(大家都在写 “likes” 或 ”.”)。真实服务里的请求更杂,会更接近公式。无论哪种情况,批一大,“只读被选中的专家”的好处就迅速消失(§3.1)。


5. MoE 的并行 · Parallelizing MoE

5.1 专家并行(slides 第 25 页;代码 05 演示 1)

专家并行(expert parallelism):每个专家放在一块 GPU 上(类似模型并行)。注意力层仍然是数据并行的,每块卡有自己的一批 token。一层 MoE:

  1. dispatch:按”选中的专家在哪块卡”分组,第一次 all-to-all 把 token 的隐藏向量发过去;
  2. 每块卡只算本地专家;
  3. combine:第二次 all-to-all 把结果发回原卡,按门控权重加和。

05 号脚本在 8 个模拟设备上真的这样做了一遍(16 个专家、每卡 2 个、每卡 512 个 token、选 2 个):

  • 结果和单卡分组 GEMM 的最大差为 0;
  • 发得最多的设备发了 926 KB,公式 N·k·(1 − 1/n)·d·4 字节 = 918 KB;
  • α-β 时间:dispatch 37 µs + combine 37 µs;
  • 各卡算的行数 998–1053(平均 1024)。

两点结论:

  • 每次 all-to-all 每卡发出的量 ≈ 自己的 token 数 × k × d × 字节数,和专家有多少个无关,只和 k 成正比。这也是 Switch 选 k = 1 的理由之一。
  • 即使路由是随机的,最忙的卡也要多算 3%,其他卡在下一次 all-to-all 前等它。

Switch 论文 §5.4 用 einsum 写了同一件事:路由器给出 [n, B/n, E, C] 的 0/1 派发张量,和输入 [n, B/n, d] 相乘得到 [n, E, C, d],再用 all-to-all 把切分的维度从 n 换成 E。

5.2 混合并行(slides 第 28 页;代码 05 演示 5)

slides 第 28 页:注意力层用数据并行 + 模型并行(AllReduce),MoE 层用专家并行(AlltoAll)。Mixtral 式一层、8 块 GPU、每块 GPU 4096 个 token(BF16):

做法集合通信每卡发出(前向)NVLink 时间每卡参数
注意力:张量并行AllReduce470 MB1.04 ms5M
注意力:数据并行—0042M
MoE:专家并行AlltoAll117 MB0.26 ms176M
MoE:张量并行(每个专家切 8 份)AllReduce470 MB1.04 ms176M
MoE:数据并行—001,409M

(张量并行时 8 块卡共同处理同一批 8 × 4096 个 token。)

  • 专家并行只发”要去别处的 token”,每个 token 只去 k 个专家;张量并行每个 token 的完整激活都要参与 AllReduce。
  • 显存上专家并行和张量并行一样省;数据并行每卡要放全部 1.4B 个专家参数——放不下。所以常见的组合是:注意力用数据并行(或张量并行),MoE 用专家并行。
  • Switch 论文 §5.5 讨论了三者都用时的取舍:加专家只加参数不加 FLOPs;要加 FLOPs 就得加宽 d_ff,显存不够就要加模型并行 m,而总卡数 N = n·m 固定,于是数据并行 n 变小、批变小。最好的映射靠实验确定。

5.3 分层 all-to-all(DeepSpeed-MoE;代码 05 演示 2)

all-to-all 的轮数随设备数线性增长(成对交换要 p − 1 轮),每轮付一次固定开销 α。DeepSpeed-MoE §5.3 的分层做法:先在节点内交换,把”发往同一个节点内编号 h 的 GPU”的数据集中到本节点的 h 号 GPU,再由各节点的 h 号 GPU 之间做节点间交换。轮数从 p − 1 降到 (G − 1) + (p/G − 1)。

2 个节点 × 8 块 GPU(节点内 NVLink 450 GB/s、α 5 µs;节点间 50 GB/s、α 10 µs):

每对设备之间的消息扁平分层带宽下限
256 B150 µs45 µs0
1 MB465 µs245 µs168 µs
16 MB5,183 µs3,251 µs2,684 µs
  • 小消息(解码、小批量推理)时间几乎全是 α,分层把 15 轮降到 8 轮,快 3 倍多。
  • 大消息时瓶颈是网卡带宽,两种做法经网卡发的字节数一样多。我们的扁平写法慢,是因为”成对交换”的 15 轮里每一轮都有设备在走网卡、整轮按网卡速度算——这是调度的问题,不是分层省了带宽。
  • 所以分层的好处是:小消息时少付 α;大消息时把网卡流量集中成几条大消息,不让慢链路拖住每一轮。

5.4 限制节点数的路由(DeepSeek-V3;代码 05 演示 3)

DeepSeek-V3 的集群里节点内是 NVLink(160 GB/s),节点间是 InfiniBand(50 GB/s),差 3.2 倍。两个设计:

  • 同一节点只经网卡发一次:一个 token 要去同一个远端节点上的几个专家时,经网卡只发一次,到了再用 NVLink 转发;
  • 每个 token 最多发到 M = 4 个节点:先按”每个节点上最高的 k/M 个亲和度之和”选出 4 个节点,再在这 4 个节点的专家里选 top-8。

256 个专家放在 8 个节点(随机亲和度,是最不利于”限制节点”的情况):

做法每 token 涉及节点经网卡 / token亲和度保留
不限节点,每个 (token, 专家) 单独发5.30100.6 KB100%
不限节点,同一节点只发一次5.3066.5 KB100%
最多 4 个节点,同一节点只发一次3.9750.0 KB99.42%

网卡流量降了一半,选中专家的亲和度之和只少了 0.6%。真实模型是带着这个限制从头训练的,亲和度会适应它。报告的另一个算账:每个 token 在每个目标节点上平均可以用 3.2 个专家而不让 NVLink 成为瓶颈,所以同样的通信代价最多能选 4 × 3.2 ≈ 13 个专家(实际用 8 个)。DeepSeek-V3 只用 20 个 SM 就跑满了 IB 和 NVLink 的带宽,用的正是第 6 讲的 warp 特化:不同的 warp 分别负责 IB 发送、IB 到 NVLink 的转发、NVLink 接收。

5.5 负载不均与冗余专家(代码 05 演示 4)

专家并行时,MoE 层的时间由最忙的那块 GPU 决定。256 个专家放到 32 块 GPU(每块 8 个),专家负载服从 Zipf 分布:

负载偏斜专家 最忙/平均按编号顺序放贪心放(LPT)+32 个冗余专家
s = 01.111.041.001.00
s = 0.58.142.191.461.13
s = 1.042.085.885.392.79

(后三列是 GPU 的最忙/平均。)

  • 偏斜大时,单个专家本身就超过了一块 GPU 的平均负载,光靠摆放解决不了;
  • 复制最热的 32 个专家、把它们的 token 分给两个副本,最忙/平均从 5.39 降到 2.79。这是 DeepSeek-V3 预填充阶段的做法:按线上统计的负载每 10 分钟调整一次冗余专家,每块 GPU 除了自己的 8 个专家再放 1 个冗余专家;解码阶段 320 块 GPU 里有 64 块专门放冗余专家和共享专家。
  • 训练时不能这样随便复制(每个副本都要同步梯度和优化器状态),所以训练靠负载均衡损失 / 偏置项让负载本身均匀(§2.4)。

5.6 把通信藏起来

专家并行的两次 all-to-all 在关键路径上:dispatch 之前不能算专家,combine 之前不能算下一层。DeepSeek-V3 的 DualPipe 把一对前向块和反向块的四个部分(注意力、dispatch、MLP、combine)交错起来,让 all-to-all 和计算重叠,报告说通信几乎完全被藏住(§3.2.1)。推理的预填充阶段也同时处理两个计算量相近的 micro-batch,一个算注意力和 MoE 时,另一个做 dispatch 和 combine。这和第 14 讲流水线并行的”用 micro-batch 填空泡”是同一个思路。


6. 把它们放在一起 · Putting it together

问题原因办法本讲
想要更多参数,但计算不能涨稠密层每个 token 都经过所有参数MoE:E 个专家选 k 个§1
负载不均富者更富;隐藏状态的公共分量辅助损失;偏置项(无辅助损失);序列级小辅助损失§2.4
每个专家的 token 数不一样动态路由容量 + 补齐 / 丢弃(静态形状);分组 GEMM;块稀疏(MegaBlocks)§3
求置换下标要并行地知道”排第几”前缀和,顺便得到 indptr§3.3
批一大就要读所有专家B 个 token 碰到 E(1 − (1 − k/E)^B) 个专家更大的批;专家并行汇集 token§3.1、§4.2
参数放不下一块卡总参数大专家并行(AlltoAll)+ 注意力数据 / 张量并行§5.1–5.2
all-to-all 慢轮数多(α)、跨节点带宽低分层 all-to-all;同节点只发一次;限制节点数;通信与计算重叠§5.3–5.4、§5.6
最慢的卡拖慢所有卡负载不均贪心放置;冗余专家(推理)§5.5

一句话:MoE 用”稀疏的计算”换”更多的参数”,代价是一连串系统问题——动态形状、负载不均、all-to-all——而这一讲的每一项技术都在对付其中一个。


7. 论文精读 · Paper reading

7.1 Switch Transformers: Scaling to Trillion Parameter Models with Simple and Efficient Sparsity(Fedus, Zoph, Shazeer, JMLR 2022)

1. 问题:缩放定律说参数越多越好,但稠密模型的计算随参数线性增长。能不能把”参数量”当成一个独立的缩放维度,参数增加而每个 token 的 FLOPs 不变?已有的 MoE 为什么没有被广泛采用?

2. 为什么重要又困难:

  • MoE 在机器翻译上有过成功(Shazeer 2017、GShard),但复杂、通信开销大、训练不稳定;
  • 硬件和库都是为稠密矩阵乘法设计的,TPU 还要求所有形状在编译时静态确定,而路由是动态的;
  • 大的稀疏模型用 bfloat16 训练容易发散;微调时参数多、数据少,容易过拟合。

3. 以前的工作为什么不行:

  • Shazeer et al. 2017 认为至少要路由到 k ≥ 2 个专家,路由器才能学到有意义的梯度;top-2 的计算、通信、容量都翻倍;
  • GShard 用 float32 训练整个 MoE 来保证稳定,通信的张量也是 float32;
  • 原始的 MoE 有分开的负载均衡损失和重要性损失,比较复杂。

4. 新意:

  • Switch 路由(k = 1):只送给一个专家,门控值 p_i(x) 保证路由器可导。路由计算更少,每个专家的容量至少可以减半,通信更少;
  • 专家容量与容量因子(式 3),超出容量的 token 走残差;简化的负载均衡损失 α·N·Σ f_i·P_i(式 4),α = 10⁻²;
  • 选择性精度:只在路由器函数内部转成 float32(本地计算),派发 / 合并的张量转回 bfloat16,既稳定又不增加通信;
  • 更小的初始化(缩放 0.1);微调时用专家 dropout(专家内部 0.4、其他层 0.1);
  • 用 Mesh-TensorFlow 组合数据、模型、专家并行(§5,图 9),设计出 395B 的 Switch-XXL 和 1.6T 的 Switch-C;
  • 把稀疏模型蒸馏回稠密模型。

5. 结果:

  • 表 1(128 个专家、32 个 TPUv3 核):Switch-Base(容量因子 1.0)达到目标质量 62.8 小时、每秒 1000 个样本;top-2 的 MoE-Base 最好是 68.7 小时;T5-Base 在 100k 步内达不到;Switch 在较低的容量因子(1.0、1.25)下表现更好;
  • 选择性精度(表 2):float32 质量 −1.718、每秒 1160 个样本;bfloat16 发散;选择性精度 −1.716、1390 个样本;
  • 初始化缩放 0.1(表 3):质量 −2.72(标准差 0.01)vs 默认 −3.60(标准差 0.68);
  • 64 个专家达到 T5-Base 同样的质量步数少 7.5 倍;墙钟时间约快 7 倍;101 种语言全部提升,91% 的语言快 4 倍以上;
  • Switch-C(1.6T 参数、2048 个专家)比 T5-XXL 快 4 倍达到固定困惑度;蒸馏能把模型缩小 99%、保留 30% 的质量提升;
  • 被丢的 token 一般少于 1%(足够大的辅助损失系数保证了均衡)。

本讲的验证:在记忆任务上复现了”参数多但计算不变”的好处(8 个专家选 1:99.9% vs 稠密 59.9%);容量因子越小丢得越多、质量越差,均衡和容量相互影响;辅助损失 α = 0.01 足以把负载拉平。另外发现了一个和 Switch 写法相关的坑:top-1 时如果归一化门控,路由器的梯度为 0。

局限(含论文自己的讨论):

  • 大模型(Switch-XXL)仍会偶尔不稳定;
  • 预训练困惑度的提升没有完全转化为下游任务的 SOTA;
  • 容量因子要调;丢 token 有代价;
  • 最好的数据 / 模型 / 专家并行映射靠实验确定;TPU 的静态形状是”容量”这个设计的根源。

7.2 MegaBlocks: Efficient Sparse Training with Mixture-of-Experts(Gale, Narayanan, Young, Zaharia, MLSys 2023)

1. 问题:MoE 的路由是动态的、负载是不均衡的,而现有框架为了用批量矩阵乘法,把每个专家的 token 数裁剪或补齐成固定的容量。于是用户必须在”丢 token(损失质量)“和”算补齐(浪费计算和显存)“之间二选一,还多了一个要调的超参数。能不能既不丢 token、又不浪费?

2. 为什么重要又困难:

  • 丢 token 的质量损失不小:容量因子 1 的 MoE 验证损失降 0.15,不丢的降 0.26(图 2);
  • 不丢就要大的容量:Tutel 发现有的模型容量因子要到 11,而且需要的容量会在训练中突增;补齐让 MoE 层的计算翻倍以上;
  • 稀疏计算在 GPU 上通常很慢;现有的块稀疏库(cuSPARSE 的 blocked-ELL、Triton Blocksparse)要么要求每行非零块数相同,要么假设稀疏结构不变(要预计算查找表),而 MoE 的结构每一步、每一层都在变。

3. 以前的工作为什么不行:

  • GShard / Switch / Tutel:固定容量 + 批量矩阵乘法,丢 token 或补齐;Tutel 的动态容量不丢,但所有专家按最忙的那个补齐;
  • BASE 层把路由变成线性分配问题,保证均衡,但运行开销大;专家选 token 的路由(Zhou et al.)同一个 token 可能被多个专家选中,仍会丢 token。

4. 新意:

  • 把 MoE 的计算重写成块稀疏运算:块对角、每个对角块的行数可以不同,用 SDD 和 DSD 交替实现两层 MLP,“dropless MoE”(dMoE);
  • 为动态稀疏写的块稀疏 GPU 内核(基于 CUTLASS),支持转置和不转置的所有组合:
    • 块大小 128 × 128(和 cuBLAS 的稠密矩阵乘法一样快);
    • 混合 BCSR-COO:额外存每个非零块的行下标,SDD 的线程块直接定位;
    • 转置下标:按转置顺序遍历的二级索引,不复制数据;
  • 置换时把每组补齐到 128 的倍数,融合进置换内核;稀疏拓扑的元数据用一个 CUDA 内核构造,同时构造转置的元数据,在前向和反向的多次矩阵乘法里摊销。

5. 结果(8 块 A100、The Pile、64 个专家 top-1):

  • 和 Tutel 的动态容量 dMoE 比,端到端训练快 1.38 倍、2.0 倍、4.35 倍(XS / Small / Medium),优势随模型增大——Tutel 的补齐让它只能用小 2 / 4 / 8 倍的 micro-batch;
  • 和 Megatron-LM 的稠密 Transformer 比,达到同样的验证损失快 1.8–2.4 倍;
  • 和各自最好容量因子的丢 token MoE 比,快 1.38、1.37、1.18 倍,而且少了一个要调的超参数;
  • 块稀疏内核达到 cuBLAS 批量矩阵乘法吞吐的 98.6%(标准差 4%,最低 91%、最高 104%);转置访问的 DSᵀD 局部性差一些,但占时间很少。

本讲的验证:逐个数字复现了 slides 上用前缀和求置换下标的例子;实现了 BCSR + 行下标 + 转置下标的格式和 SDD / DSD / DSᵀD,与稠密计算一致;块稀疏的写法不丢 token,只多算补齐到块大小的行(9.4%);roofline 模型下,负载不均时动态容量比分组 / 块稀疏慢 2.8 倍、激活显存 3 倍。也看到它的边界:负载均匀时,固定容量(因子 1.0)没有 tile 量化,反而略快。

局限(含论文自己的讨论):

  • 每个专家补齐到 128 的倍数,专家很多、每个专家 token 很少时(细粒度专家、小批量)浪费变大;
  • MoE-Medium 时显存限制了 micro-batch,硬件效率下降;作者提到可以用更多设备或选择性重计算减少显存(附加讲 A1);
  • 论文集中在单节点 8 卡的专家并行,没有深入大规模分布式通信(Tutel、FasterMoE 的方向);
  • 可变大小的专家(块的列数也不同)留作未来工作。

7.3 两篇论文之间的联系

  • 同一个问题的两个时代:Switch 生在 TPU 上,静态形状是硬约束,所以发明了”容量”,并用 top-1 和辅助损失把丢 token 压到 1% 以下;MegaBlocks 生在 GPU 上,可以写动态的内核,于是把”容量”这个约束整个拿掉。两篇论文都承认丢 token 有代价,只是一个靠路由去避免,一个靠内核去避免。
  • 辅助损失和块稀疏是互补的:MegaBlocks 不丢 token,但负载不均时最忙的专家仍然决定专家并行的时间(§5.5),所以辅助损失(或偏置项)仍然需要——它从”保证不丢”变成了”保证不等”。
  • 都在算账:Switch 的 top-1 是为了让容量减半、通信减半;MegaBlocks 选 128 × 128 的块是为了让稀疏矩阵乘法跑出稠密矩阵乘法的速度。
  • 一个可以写成评审的新想法:把 MegaBlocks 的块稀疏内核用在 Switch 的专家并行上——每块卡收到的 token 数是动态的,本地仍然可以用块稀疏、不补齐;再结合 DeepSeek-V3 的冗余专家,让”不丢、不等、不补”同时成立。

8. 小结 · Summary

  1. 前馈层是参数的大头。MoE 把它复制成 E 份、每个 token 只走 k 份,于是参数和计算解耦:DeepSeek-V3 的参数是 Llama-2-7B 的 100 倍,每个 token 的计算只是 5.5 倍。
  2. 门控 g = softmax(W_g x)、top-k、在选中的里归一化(top-1 时不要归一化)。负载均衡靠辅助损失 α·E·Σ f_e·P_e 或 DeepSeek-V3 的偏置项;不均衡在第 0 步就可能出现。
  3. 批量 MoE = 路由 → 置换 → 分组计算 → 反置换。置换下标和 indptr 都来自同一次前缀和;专家计算是一个分组 GEMM(CSR)。
  4. 容量让形状静态,代价是丢 token 或算补齐;MegaBlocks 把 MoE 写成块稀疏矩阵乘法(BCSR + 行下标 + 转置下标、128 × 128 的块),不丢也不补,负载不均时优势最大。
  5. 单个请求很省,批量时要读几乎所有专家:每读一个参数只做 B·k/E 次乘加,MoE 要大 E/k 倍的批才计算受限。
  6. 专家并行每层两次 all-to-all,量和 k 成正比、和专家数无关;混合并行是注意力用数据 / 张量并行、MoE 用专家并行。
  7. 大规模时的工程:分层 all-to-all(少付 α)、同节点只发一次 + 限制节点数(少用网卡)、冗余专家(少等最慢的卡)、通信与计算重叠。

9. 常见误区 · Common misconceptions

  1. “MoE 的计算量是稠密模型的 1/E。” 和同总参数的稠密模型比,计算是 k/E;和同激活参数的稠密模型比,计算差不多一样。MoE 的好处是”同样的计算、更多的参数”,不是”同样的参数、更少的计算”(虽然两种说法在数学上等价,比较对象不同,结论的含义完全不同)。
  2. “MoE 推理一定比同总参数的稠密模型快。” 只在批足够大时成立。批小时读权重是瓶颈;批中等时几乎所有专家都要读,和同总参数的稠密模型读的字节差不多(§3.1)。
  3. “负载均衡损失是为了让模型更准。” 它首先是为了效率和不丢 token:均衡的负载才能让专家并行不等最慢的卡、让固定容量少丢 token。系数太大反而会损害质量(DeepSeek-V3 因此改用偏置项)。
  4. “top-1 和 top-2 只是 k 不同,写法一样。” top-1 时如果在选中的专家里归一化门控,权重恒为 1,路由器拿不到梯度(§2.5)。
  5. “容量因子越大越好。” 大了不丢 token,但每个专家都按容量算,补齐的 0 也算;动态容量在负载不均时要算 3 倍、存 3 倍的激活(§3.7)。
  6. “块稀疏 = 非结构化稀疏,GPU 上会很慢。” MoE 的稀疏是很粗的块(128 × 128),每个块就是一个稠密的 tile,MegaBlocks 的内核跑到 cuBLAS 的 98.6%。
  7. “专家并行的通信量随专家数增长。” 每卡发出的量 ≈ token 数 × k × d,和专家数无关;专家数影响的是每块卡上放几个专家、负载能否均衡。
  8. “专家会按领域(数学、代码、生物……)分工。” Mixtral 的分析发现路由更多和语法、词的类型对齐;我们的小模型上,路由也主要按”词 + 角色”分。

10. 练习 · Exercises(含参考答案)

练习 1(参数账):一个 MoE 模型 d = 4096,32 层,每层 64 个专家(SwiGLU,f = 1408)选 6 个,另有 2 个共享专家(同样宽),注意力用 GQA(32 个 Q 头、8 个 KV 头、头维 128),词表 32000。算总参数和激活参数。

答案:注意力每层 2·4096·4096 + 2·4096·1024 = 41.9M;一个专家 3·4096·1408 = 17.3M;路由器 4096·64 = 0.26M。每层总前馈 = 66 × 17.3M + 0.26M = 1142M,激活前馈 = 8 × 17.3M + 0.26M = 138.7M。嵌入 2 × 32000 × 4096 = 262M。总参数 ≈ 32 × (41.9 + 1142) + 262 ≈ 38.1B;激活 ≈ 32 × (41.9 + 138.7) + 262 ≈ 6.04B。可以用 moe.ModelSpec 验证。

练习 2(前缀和):5 个 token、4 个专家,每个选 2 个:x0→{1,3}、x1→{0,1}、x2→{1,2}、x3→{0,3}、x4→{2,3}。写出置换下标和 indptr。

答案:每个专家分到的个数:专家 0 两个(x1、x3),专家 1 三个(x0、x1、x2),专家 2 两个(x2、x4),专家 3 三个(x0、x3、x4)。indptr = [0, 2, 5, 7, 10]。按专家优先、同一专家内按 token 顺序编号:专家 0:x1→0、x3→1;专家 1:x0→2、x1→3、x2→4;专家 2:x2→5、x4→6;专家 3:x0→7、x3→8、x4→9。置换下标:x0 [2, 7],x1 [0, 3],x2 [4, 5],x3 [1, 8],x4 [6, 9]。可以用 moe.permutation_indices 检查。

练习 3(容量):N = 4096 个 token、E = 16、k = 2、容量因子 1.25。容量是多少?如果最忙的专家分到 900 个,它丢多少?动态容量时要算多少行(相对需要的)?

答案:C = ⌈2·4096/16 × 1.25⌉ = 640;最忙的专家丢 260 个。动态容量 C = 900,算 16 × 900 = 14,400 行,需要的是 8192 行,1.76 倍。

练习 4(批量解码):DeepSeek-V3 每层 256 个路由专家选 8 个,一层有多少个专家被 B = 32 个 token 碰到(均匀独立)?如果每块 GPU 只放这一层的 8 个专家(专家并行 32 路),解码时每块 GPU 每步平均处理多少个 (token, 专家)?要让每个专家的算术强度达到 H100 的拐点(约 295),全集群一步要有多少个 token?

答案:256·(1 − (1 − 8/256)^32) ≈ 163 个。每块 GPU 平均处理 32·8/32 = 8 个,每个专家平均 1 个 token——严重访存受限。每个专家要约 295 个 token,全集群要 295·256/8 ≈ 9440 个 token 同时解码。这就是 DeepSeek-V3 解码阶段要用 320 块 GPU、每块放一个专家的原因。

练习 5(辅助损失):E = 4,一批里分给 4 个专家的比例 f = (0.7, 0.1, 0.1, 0.1),平均概率 P = (0.6, 0.15, 0.15, 0.1)。算 E·Σ f·P。对 P₁ 的梯度是多少?它让 P₁ 往哪个方向变?

答案:4 × (0.42 + 0.015 + 0.015 + 0.01) = 4 × 0.46 = 1.84(均匀时是 1)。∂/∂P₁ = E·f₁ = 2.8,比其他专家的 0.4 大得多:梯度下降让 P₁ 下降最多,也就是降低被分得最多的专家的路由概率。

练习 6(专家并行通信):8 块 GPU、每块 8192 个 token、d = 7168、k = 8、BF16,专家均匀地放在 8 块卡上。每块卡一次 dispatch 发出多少字节?如果用节点间 50 GB/s 的网卡(假设全部跨节点),要多久?换成 k = 2 呢?

答案:8192 × 8 × (1 − 1/8) × 7168 × 2 ≈ 822 MB,约 16.4 ms。k = 2 时 205 MB,约 4.1 ms——和 k 成正比。实际系统还要加 combine 一次、元数据,并且会用 DeepSeek-V3 式的”同节点只发一次”减少网卡流量。

练习 7(块稀疏):4 个专家分到 130、0、260、10 个 token,块大小 128,每个专家宽 f = 256。补齐后多少行?非零块多少个?多算了多少?如果用动态容量(按最忙的补齐)呢?

答案:补齐后 256 + 0 + 384 + 128 = 768 行(需要 400 行),多算 92%;非零块 = (2 + 0 + 3 + 1) × 2 = 12 个。动态容量 C = 260,4 × 260 = 1040 行,多算 160%。专家的 token 很少时块稀疏的补齐也不可忽略——这是 MegaBlocks 自己承认的局限(小 micro-batch 时可以用更小的块)。

练习 8(设计):你要在 64 块 GPU(8 个节点)上服务一个 256 专家、选 8 的 MoE,线上负载偏斜很大(几个专家特别热)。列出你会用的三种手段,并说明各自对准什么瓶颈。

答案(一种合理的组合):

  • 冗余专家 + 按负载贪心放置:对准”最慢的那块卡”;
  • 限制每个 token 最多去 M 个节点、同节点只经网卡发一次:对准跨节点带宽;
  • 预填充用两个 micro-batch 交错(一个算、一个通信),解码用大批量 + 专家并行把 token 汇集到每个专家:对准 all-to-all 延迟和访存受限。

另外不要在推理时用容量丢 token(§4.1)。


11. 延伸阅读 · Further reading

  • N. Shazeer et al., Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer(ICLR 2017)——稀疏门控 MoE 的起点。
  • D. Lepikhin et al., GShard: Scaling Giant Models with Conditional Computation and Automatic Sharding(ICLR 2021)——top-2、专家容量、分组派发、辅助损失、随机路由;600B 参数的翻译模型。
  • S. Rajbhandari et al., DeepSpeed-MoE: Advancing Mixture-of-Experts Inference and Training to Power Next-Generation AI Scale(ICML 2022)——PR-MoE、MoS 蒸馏、专家切片、分层 all-to-all。
  • N. Du et al., GLaM: Efficient Scaling of Language Models with Mixture-of-Experts(ICML 2022)。
  • A. Jiang et al., Mixtral of Experts(2024)——8 选 2 的开源 MoE;§5 的路由分析。
  • DeepSeek-AI, DeepSeek-V3 Technical Report(2024)——共享 + 细粒度专家、无辅助损失的均衡、限制节点数的路由、DualPipe、冗余专家的部署。
  • P. Jin et al., MoH: Multi-Head Attention as Mixture-of-Head Attention(2024)——把注意力头当专家。
  • C. Hwang et al., Tutel: Adaptive Mixture-of-Experts at Scale(MLSys 2023)——动态容量、自适应并行。
  • 本仓库 CMU11-868 第 17 讲(MoE 的实现)、CMU10-423 第 16 讲(MoE 的建模)。

12. 下一讲预告 · Next lecture

原课的讲课到这里就结束了(之后是项目工作日、感恩节假期和期末项目展示)。本讲义接下来是三篇附加讲,整理原课第 1 讲课程地图里列出、但课表没有单独排课的主题。

附加讲 A1 · 内存优化:重计算与卸载——这一讲反复遇到”显存不够”:MegaBlocks 的 MoE-Medium 因为显存只能用小的 micro-batch,DeepSeek-V3 要把 671B 参数分到几百块卡上。A1 回到训练显存的另一大块——激活:

  • Chen et al. 2016 的 √n 检查点;
  • 一般计算图上的最优重计算(Checkmate);
  • Megatron 的选择性重计算(为什么只重算注意力的 softmax 那一段就能省 70% 的激活);
  • 把优化器状态、参数卸载到 CPU / NVMe(ZeRO-Offload、ZeRO-Infinity)时,PCIe 带宽怎样决定能不能跑满 GPU。
← 返回课程目录