第 17 讲 · 混合专家模型(MoE)的系统 · Systems for Mixture-of-Experts Models

Large Language Model Systems · Lecture 17。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

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

本篇目录

对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 17 · Wed 3/18 · Large models with Mixture-of-Expert(slides:llmsys-17-MoE.pdf,41 页)。 阅读材料:Lepikhin et al. (2021) GShard;Fedus et al. (2022) Switch Transformers;Rajbhandari et al. (2022) DeepSpeed-MoE;Dai et al. (2024) DeepSeekMoE;DeepSeek-AI (2024) DeepSeek-V3 Technical Report;Jiang et al. (2024) Mixtral of Experts。 依据说明:slides 覆盖:稠密模型难以扩展与稀疏模型的动机、Transformer MoE(用专家替换 FFN、门控网络)、Switch Transformer 与 top-k 门控、共享专家 + 路由专家(DeepSpeed-MoE、DeepSeekMoE)、不同层激活的专家不同、Mixtral 的参数(47B)、MoE 在小数据上过拟合、专家学到了什么、路由的几何解释、专家并行(GShard)与 all-to-all、token 的计算路径、隔层使用 MoE、专家级负载均衡损失、MoE 推理的性能因素与 DeepSpeed-MoE 的推理优化(专家并行 + 张量并行、内核融合、分层 all-to-all)、训练损失与吞吐、DeepSeekMoE(细粒度专家)、DeepSeek-V3 的配置与设备级均衡损失、DeepEP 与 EPLB、DeepSeek-V3 与 DeepSpeed 的代码。容量因子与丢弃的定义、Switch 的损失形式、无辅助损失的偏置均衡、按节点限制路由、V3 路由器的具体步骤取自上面的论文与 V3 的官方推理代码。本讲义的参数核算、MoE 层的实现、负载均衡的小实验、4 进程(gloo)的专家并行、专家摆放与分层 all-to-all 的模型、细粒度专家的小实验,都是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:code/lecture17/(5 个脚本 + moe.py;03、05 各约 20 秒,其余 2 秒以内) 预计阅读时间:2.5 小时(含运行代码)


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

0.1 学习目标 Learning goals

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

  1. 说清楚 MoE 怎样把”参数量”和”每个 token 的计算量”分开,并能核算 Mixtral、DeepSeek-V3 的总参数与激活参数。
  2. 从零实现一个 top-k 路由的 MoE 层,比较 Switch、GShard、Mixtral、DeepSeek-V3 的路由方式;理解容量因子与 token 丢弃。
  3. 解释为什么需要负载均衡,写出 E·Σ fᵢPᵢ 损失,说明 DeepSeek-V3 的无辅助损失偏置为什么不干扰模型。
  4. 描述专家并行的数据流(两次 all-to-all),估算它的通信量,理解按节点限制路由、专家摆放与复制、分层 all-to-all 各在解决什么。
  5. 读懂 DeepSeek-V3 的路由代码,知道细粒度专家和共享专家的动机,以及它们的代价。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
Transformer 的 FFN(第 6 讲)、SwiGLU(第 7 讲)MoE 推理服务里的调度、显存与 KV 缓存(第 22–27 讲)
集合通信与 α-β 模型(第 14 讲);张量并行(第 16 讲)FP8 训练(第 19 讲的数字格式)
访存受限与算术强度(第 4 讲)DeepSeek-V3 的 MLA 注意力细节

0.3 本讲地图 Map

§1  为什么要稀疏:参数与计算解耦;Mixtral、DeepSeek-V3 的参数账;推理的带宽          ← 代码 01
§2  MoE 层:路由器、top-k、四种路由;实现;几何意义;容量与丢弃                      ← 代码 02
§3  负载均衡:坍缩、均衡损失、无辅助损失的偏置                                       ← 代码 03
§4  专家并行:all-to-all、通信量、按节点限制、专家摆放、分层 all-to-all、推理优化      ← 代码 04
§5  DeepSeekMoE 与 DeepSeek-V3:细粒度、共享专家、路由器代码走读                     ← 代码 05
§6 小结   §7 常见误区   §8 练习   §9 延伸阅读   §10 下一讲预告

1. 为什么要稀疏 · Why sparsity

1.1 稠密模型的扩展问题

第 1、14 讲的 6ND:训练的计算量正比于参数 × token。想要更大的模型,每个 token 的计算就要跟着涨(slides 第 3 页:“算力是训练大模型的首要难题”)。可是参数多真的需要每个 token 都用上全部参数吗?一个关于化学的 token 未必需要”诗歌”的知识。

稀疏模型(sparse model)的想法:参数很多,但每个 token 只用其中一小部分。混合专家(Mixture-of-Experts,MoE)是最成功的一种(slides 第 4–5 页):

  • 把 Transformer 里的 FFN 换成 E 个专家(每个是一个小 FFN);
  • 加一个门控网络 / 路由器(gating network / router),根据 token 决定送给哪几个专家;
  • 输出 = 被选中专家输出的加权和。

(slides 特别提醒:这和机器学习里”学一组预测器的加权平均”的 Mixture-of-Experts 学习算法不是一回事。)

1.2 参数账:47B 而不是 56B

slides 第 10 页的问题:Mixtral 8×7B 有多少参数?01 号脚本按公开的配置核算:

部分参数占比
注意力1.34B2.9%
专家(32 层 × 8 个)45.10B96.6%
嵌入 + 输出层0.26B0.6%
合计46.70B
每个 token 激活12.88B28%

只有 FFN 被复制成 8 份,注意力、嵌入、归一化是共用的——所以是 47B 而不是 8 × 7 = 56B。每个 token 只走 2 个专家,激活 12.9B:计算相当于一个 13B 的稠密模型,参数却有 47B。

DeepSeek-V3(slides 第 32 页)更极端:

部分参数占比
注意力(MLA,61 层)11.41B1.7%
稠密 FFN(前 3 层)1.19B0.2%
路由专家(58 层 × 256 个)653.91B97.4%
共享专家(58 层 × 1 个)2.55B0.4%
嵌入 + 输出层1.85B0.3%
合计671.0B
每个 token 激活37.6B(不算嵌入查表 36.6B)5.6%

与技术报告的 671B / 37B 一致。97% 的参数是路由专家,每个专家只有 44M 参数(中间维 2048,而稠密 FFN 是 18432)——这就是”细粒度专家”(§5)。

1.3 计算省了多少

前向的 FLOPs ≈ 2 × 参与计算的参数(01 号脚本第 3 部分):

模型每个 token 前向(MoE)同样总参数的稠密模型省了
Mixtral 8×7B25 GFLOP93 GFLOP3.7×
DeepSeek-V373 GFLOP1340 GFLOP18.3×

这就是”参数涨十几倍、计算几乎不变”:训练(6ND 里的 N 按激活参数算)和推理都便宜得多。slides 第 26 页 DeepSpeed-MoE 的结果也是这个方向:达到同样的质量,MoE 的训练吞吐比稠密模型高得多。

1.4 代价:显存和带宽

但所有专家都要放在显存里:Mixtral BF16 约 93 GB,DeepSeek-V3 FP8 权重约 671 GB。更微妙的是推理解码时的带宽(slides 第 21 页”MoE 推理的性能取决于总模型大小、激活多少专家、总的显存带宽”)。一批 B 个 token 解码一步,平均要读多少个专家的权重?假设路由均匀、相互独立,每层碰到的专家数 = E·(1 − (1 − k/E)^B):

模型B = 141664256
Mixtral(8 选 2)2.0(25%)5.5(68%)7.9(99%)8.08.0
DeepSeek-V3(256 选 8)8.0(3%)30.5(12%)102.0(40%)222.4(87%)255.9(100%)

B = 1 时 MoE 只读 k 个专家,带宽和计算一起省。批一大,几乎每个专家都被至少一个 token 选中——DeepSeek-V3 在 B = 64 时要读 222 个专家的权重,每个专家却平均只处理 2 个 token。算术强度极低,又回到了第 4 讲的”访存受限”。MoE 推理省计算,不一定省带宽。所以 slides 第 22 页 DeepSpeed-MoE 推理的设计目标是”最小化每块卡上的关键数据路径、最大化聚合的显存带宽”:把专家摊到很多块卡上,每块卡只读自己那几个专家(§4)。


2. MoE 层 · The MoE layer

2.1 路由器与 top-k

slides 第 6–7 页(Switch Transformer):路由器是一个线性层 W_g(d × E),给每个专家打分 h·W_g;选分数最高的 k 个专家,输出

y=∑e∈TopKge(h)⋅Experte(h)y = \sum_{e \in \text{TopK}} g_e(h) \cdot \text{Expert}_e(h)

几种常见的路由(moe.route):

路由打分 → 选择权重代表
softmax → top-k对 E 个专家做 softmax,取最大的 k 个就是概率(不归一化)Switch(k = 1)
softmax → top-k,归一化同上这 k 个概率再归一化GShard(k = 2)
top-k → softmax先取打分最大的 k 个只在这 k 个里 softmaxMixtral(k = 2)
sigmoid,分组,偏置sigmoid 打分 + 偏置,先选组再选专家不带偏置的分数归一化 × 2.5DeepSeek-V3(k = 8)

02 号脚本第 1 部分验证了实现:每个 token 的权重之和,Switch 是 0.194–0.843(就是那个专家的概率),GShard、Mixtral 是 1,V3 是 2.5。Switch 的权重为什么不归一化?k = 1 时归一化后恒为 1,路由器就从任务损失里得不到任何梯度——它不知道选得好不好。同理,sigmoid + 归一化在 k = 1 时也会失效(V3 用 k = 8,没有这个问题;我们在实验时踩到了这个坑)。

2.2 实现:按专家分组

逐个 token 地算(每个 token 调用 k 个专家)当然对,但太慢。高效实现(moe.MoE.forward):

  1. 路由:得到每个 token 的 k 个专家编号 idx 和权重 w;
  2. 按专家分组:对每个专家 e,找出选了它的所有 (token, 选择),把这些 token 拼成一个矩阵;
  3. 每个专家对它的 token 做一次矩阵乘法;
  4. 按权重加回原位(index_add)。

与逐 token 的参照只差舍入(02 号脚本第 1 部分)。第 4 部分在本机测了一下(⏱,d = 512、4096 个 token):

层参数前向 GFLOP⏱ 本机
MoE(8 选 2,每个专家 FFN 1024)8.4M17.21≈ 20–23 ms
稠密 FFN 2048(同样的激活参数)2.1M17.18≈ 13 ms
稠密 FFN 8192(同样的总参数)8.4M68.72≈ 52–61 ms

FLOPs 上 MoE 只比”同样激活参数的稠密 FFN”多一个路由器(可以忽略)。实际却慢 1.6–1.7 倍:挑出 token(gather)、8 次较小的矩阵乘法、加回原位(scatter)都有开销。这就是 slides 第 24 页 DeepSpeed-MoE 的内核优化——把门控函数融合成一个内核、用稠密的 token→专家映射表,MoE 相关的内核延迟降了 6 倍以上。

2.3 路由的几何意义

top-1 路由选的是 argmax_e (x · w_e)。两个专家的分界线是 x · (w_a − w_b) = 0——一条过原点的直线。所以输入空间被切成几个扇形,每个专家负责一块(02 号脚本第 2 部分画了 2 维、3 个专家的情形):这就是 slides 第 13 页”在专家中心之间画线性边界”。专家的分工完全由训练决定,不一定对应人能理解的概念:slides 第 12 页总结,编码器的专家常对应标点、专有名词这类浅层类别,解码器的专家更不专门化,多语言时专家也不会按语言分工(因为路由按 token、还有负载均衡)。slides 第 9 页的图也说明,同一个 token 在不同层激活的专家不同。

2.4 容量与丢弃

GPU 上的计算喜欢固定的形状,可每个专家会分到多少 token 事先不知道。Switch / GShard 的办法是给每个专家一个容量(capacity):

capacity=⌈cf⋅k⋅nE⌉\text{capacity} = \left\lceil cf \cdot \frac{k \cdot n}{E} \right\rceil

cf 是容量因子(capacity factor)。完全均衡时每个专家正好分到 k·n/E 个;cf > 1 留出余量。超出容量的 token 被丢弃:它在这一层的 MoE 输出为 0,只剩残差连接把输入原样传下去(不会出错,但这个 token 少算了一层)。02 号脚本第 3 部分(4096 个 token、8 个专家):

路由器k最忙专家 ÷ 平均cf = 1.01.252.0
均衡(独立随机打分)11.05丢 2.0% / 空 2%0.0% / 20%0.0% / 50%
不均衡(偏向后面的专家)12.12丢 25.7% / 空 26%15.5% / 32%1.5% / 51%
不均衡21.86丢 21.4% / 空 21%11.6% / 29%0.0% / 50%

即使路由器完全”公平”,随机起伏也会让 cf = 1.0 丢掉 2%;路由器有偏好时丢 1/4。cf 加大能少丢,但空着的槽位也照样占显存、参与(填零的)计算和通信——这是一个取舍。真正的解决办法是让路由器自己变均衡(§3)。DeepSeek-V3 训练和推理都不丢 token,靠的也是好的负载均衡。


3. 负载均衡 · Load balancing

3.1 路由坍缩

如果什么都不管,路由器会怎样?03 号脚本在一个小任务上训练 MoE(8 个高斯簇、每簇一个不同的目标函数,8 个专家,top-1),3 个种子:

α(均衡损失系数)测试 MSE没人用的专家最忙专家 ÷ 平均
00.05272.7 个2.68
0.010.04791.3 个2.04
0.10.03700 个1.13

不加均衡损失,8 个专家里平均 2.7 个最后一个 token 也分不到——它们的参数白白占着显存。看训练过程(种子 0):第 50 步时负载还比较平均,到第 200 步就有专家只剩 1–2%,最后变成 0。这是富者愈富:稍微受欢迎的专家分到更多 token、学得更好,路由器就更偏向它;没人选的专家得不到梯度,永远翻不了身。这叫路由坍缩(routing collapse)。坍缩不只浪费参数,还伤害效果:8 个簇本来需要 8 个专家,坍缩后几个专家被迫同时照顾几个簇(MSE 0.0527 vs 0.0370)。

3.2 负载均衡损失

slides 第 19 页的专家级均衡损失(Switch Transformer;DeepSeekMoE 的写法只差常数):

Lbalance=α⋅E∑i=1Efi⋅Pi,fi=分给专家 i 的 (token, 选择) 数n⋅k,Pi=1n∑tpt,iL_{\text{balance}} = \alpha \cdot E \sum_{i=1}^{E} f_i \cdot P_i,\qquad f_i = \frac{\text{分给专家 } i \text{ 的 (token, 选择) 数}}{n \cdot k},\qquad P_i = \frac{1}{n}\sum_t p_{t,i}

  • 完全均衡时 fᵢ = Pᵢ = 1/E,L = α;越不均衡越大。
  • fᵢ 是计数,不可导;梯度只经过 Pᵢ。某个专家分到的 token 越多(fᵢ 大),损失对 Pᵢ 的梯度越大,于是把这个专家的概率往下压——恰好是我们想要的反馈。

α 太小(0.01)在这个任务上压不住坍缩;α = 0.1 时 8 个专家都用上了。

3.3 top-2 与系统上的代价

top-2 时这个任务没有坍缩(第二选择给了冷门专家梯度),但负载仍然不均(03 号脚本第 2 部分):

设置测试 MSE最忙专家 ÷ 平均cf = 1.0 丢弃cf = 1.25 丢弃
softmax,不加均衡0.04021.7322.1%12.1%
softmax,α = 0.10.04041.154.7%0.0%
sigmoid,不加均衡0.04281.5012.0%4.6%
sigmoid + 偏置(γ = 0.001)0.04101.051.5%0.0%

效果上差不多,系统上差很多:不均衡时 cf = 1.0 要丢 22% 的 (token, 选择);在专家并行里,一层的时间由最忙的那块卡决定(§4.4)。均衡首先是一个系统问题。

3.4 DeepSeek-V3:无辅助损失的偏置

均衡损失有一个副作用:它的梯度和任务无关,系数太大会损害模型质量。DeepSeek-V3 换了个办法:给每个专家一个偏置 bᵢ,只在”选谁”时加到打分上,不进入输出的权重,也不产生梯度;每步训练后按负载调整——负载高于平均的专家 bᵢ −= γ,低于平均的 bᵢ += γ。上表最后一行:最忙专家只比平均多 5%,cf = 1.25 时不再丢弃。种子 0 结束时的偏置在 −0.147 到 +0.108 之间——冷门专家被”抬”了一把,热门的被”压”了一下。

V3 并没有完全去掉辅助损失:它保留了一个系数极小的序列级均衡损失,防止单条序列内的极端不均衡。slides 第 33 页还有一个设备级均衡损失:按设备(专家组)统计负载,直接约束卡与卡之间的不均衡(DeepSeek-V2 用过)。


4. 专家并行 · Expert parallelism

4.1 GShard 的做法

专家太多,一块卡放不下。GShard(Lepikhin et al. 2021,slides 第 15–16 页)的专家并行:

  • 每块卡放一部分专家(slides 的图是一卡一个专家);
  • 其余部分(注意力、嵌入、路由器)每块卡复制一份,像数据并行一样各自处理自己的 token;
  • MoE 层里:路由 → all-to-all 派发(dispatch,把 token 发到它选中的专家所在的卡)→ 各卡用本地专家计算 → all-to-all 收回(combine,把结果发回 token 原来的卡)→ 加权求和。

GShard 还隔一层才用 MoE(slides 第 18 页),减少 all-to-all 的次数。slides 第 17 页的”token 的计算路径”:一个 token 在第 1 层被送到 GPU 2 上的专家 2,第 2 层可能又去了 GPU 1——每一层都可能换卡。

4.2 真正跑一遍

04 号脚本第 1 部分在本机起 4 个进程(gloo),每块卡 2 个专家、24 个 token、top-2。每块卡的步骤:

  1. 路由,得到每个 (token, 选择) 的专家编号;
  2. 按目的卡稳定排序;
  3. 先用一次 all-to-all 交换计数(“我要发给你几行”),接收方才能分配缓冲区;
  4. all-to-all 派发 token(和它们的专家编号);
  5. 本地专家计算;
  6. all-to-all 收回(切分方向相反);
  7. 恢复原始顺序,按权重加起来。

与单进程比较:输出、对输入的梯度、专家权重的梯度逐位相同(稳定排序让每个专家收到的 token 顺序和单进程一样,连求和顺序都没变);路由器的梯度差 4.4e-15(它是复制的参数,要像数据并行那样 all-reduce,求和顺序不同)。反向时,all-to-all 的反向就是把发送和接收对调的另一次 all-to-all——moe.AllToAll 只有十几行。

发送矩阵并不均匀:4 块卡收到的行数是 [57, 44, 44, 47](平均 48)。训练时常用固定容量,正是为了形状固定、不用先交换计数。

4.3 通信量与按节点限制路由

每个 token、每个 MoE 层前向要传(04 号脚本第 2 部分):Mixtral 33 KB(k = 2、d = 4096、BF16 派发和收回);DeepSeek-V3 172 KB(k = 8、d = 7168,派发用 FP8、收回用 BF16),58 层约 10 MB / token——比张量并行(t = 8、前向每层两次 all-reduce,约 50 KB)还多,而且要跨节点。专家并行需要很快的 all-to-all(slides 第 15 页)。

DeepSeek-V3 的应对之一是按节点限制路由:256 个专家分成 8 组(一组对应一个节点),先选出得分最高的 4 组,只在这 4 组里选 8 个专家。随机打分时,不加限制每个 token 平均发往 5.30 个节点,加了限制最多 4 个(平均 3.96)。同一节点内的多个专家只跨 IB 发一次,再由 NVLink 转发——跨节点的流量有了上限。这就是 slides 第 32 页配置里的 “num expert group = 8、num limited group = 4”。DeepSeek 还开源了专门的通信库 DeepEP(slides 第 34 页),针对 MoE 的派发和收回优化 NVLink + RDMA 的 all-to-all。

4.4 专家摆放:卡与卡之间的均衡

专家之间均衡还不够,卡与卡之间也要均衡。04 号脚本第 3 部分把 64 个专家放到 8 块卡上,专家热度服从 Zipf 分布:

专家热度按编号分块按编号轮流贪心(按负载)贪心 + 复制 8 个最热的
均匀1.001.001.001.00
不均(s = 0.6)2.791.431.011.00
很不均(s = 1.0)4.582.201.691.01

(最忙的卡 ÷ 平均。)热度很不均时,最热的那个专家一个人的负载就可能超过平均——怎么摆都不行,只能复制它,让两块卡分担。这就是 DeepSeek 开源的 EPLB(Expert Parallelism Load Balancer,slides 第 34 页)的思路:根据统计的负载,复制热门专家(“冗余专家”)并重新摆放。

4.5 分层 all-to-all 与推理优化

直接的 all-to-all 里,每块卡要给其余 P − 1 块卡各发一条消息,延迟随卡数线性增长(slides 第 25 页)。分层 all-to-all(DeepSpeed-MoE):先在机器内部按目的机器重新整理(一次机器内的 all-to-all),再让每块卡只和其他机器上”对应位置”的卡交换——跨机器的消息从 P − 8 条降到 P/8 − 1 条,每条大 8 倍。04 号脚本第 4 部分(α-β 模型,每块卡派发 64 KB,假设的参数):

卡数 P直接分层快了
1695 μs26 μs3.7×
2562497 μs327 μs7.6×
102410177 μs1287 μs7.9×

消息小(推理、小批量)时延迟占主导,分层的好处最大。DeepSpeed-MoE 推理的整套优化(slides 第 22–25 页):

  • 专家并行 + 专家切片:把走同一条关键数据路径的 token 分组,不同组在不同的卡上并行;
  • 张量并行切非专家部分(注意力),一般在机器内部;再加数据并行;
  • 并行策略协调的通信:张量并行组内的卡数据相同,只需一块卡参与跨机器的 all-to-all;
  • 内核优化:门控融合成一个内核、稠密的 token→专家映射表(§2.2)。

5. DeepSeekMoE 与 DeepSeek-V3

5.1 细粒度专家与共享专家

DeepSeekMoE(Dai et al. 2024,slides 第 8、30 页)对标准 MoE 做了两个改动:

  • 细粒度专家(fine-grained experts):把每个专家切成 m 个更小的专家(中间维除以 m),每个 token 选 m·k 个。参数和计算都不变,但组合数暴增(01 号脚本第 5 部分):16 个专家选 2 个只有 120 种组合;切成 64 个选 8 个有 4,426,165,368 种;V3 的 256 选 8 更多。动机是”知识的混杂”:一个 token 往往需要好几种知识,粗粒度的专家只好每个都学一个混合体。
  • 共享专家(shared experts):几个专家每个 token 都经过,负责通用知识;路由专家负责 token 特有的知识(slides 第 8 页的图,最早来自 DeepSpeed-MoE 的 Residual-MoE)。动机是”知识的冗余”:没有共享专家时,好几个路由专家都要各学一遍通用知识。

论文在 2B 到 145B 的语言模型上、在同样的总参数和计算下,细粒度 + 共享一致地更好。05 号脚本第 3 部分在一个专门按这两个动机设计的小任务上检验(每个 token 需要 4 种”技能”的组合 + 一种公共技能;三种配置总参数和计算都相同):

配置测试 MSE(2 个种子平均)
粗粒度:8 个专家 × 16,选 20.4692
细粒度:32 个专家 × 4,选 80.4919
细粒度 + 共享:1 + 31 个专家 × 4,选 70.5015

在小任务上没有更好(我们在另一个任务上也试过,结论相同)。这不说明论文错了:小任务里每个专家只有几个隐藏单元,切得更细之后单个专家太弱、路由的噪声也更大;组合数的好处要在模型和数据都足够大时才显现。系统上的代价倒是确定的:专家越小越多,矩阵乘法越碎(§2.2),all-to-all 的目的地也越多。

slides 第 11 页还提醒了 MoE 的另一个风险:在小数据上过拟合——参数多、每个专家看到的数据少,微调时尤其明显。

5.2 DeepSeek-V3 的配置

slides 第 32 页:词表 129280、d = 7168、61 层(最下面 3 层是稠密 FFN,中间维 18432)、128 个头(MLA 注意力),MoE 层:1 个共享专家 + 256 个路由专家(每个中间维 2048)、每个 token 激活 8 个,专家分 8 组、每个 token 限 4 组。FFN 是 SwiGLU:FFN(x) = (SiLU(x·W₁) ⊙ x·W₃)·W₂。MoE 层的输出:

y=Shared(x)+∑e∈Top8ge⋅Experte(x)y = \text{Shared}(x) + \sum_{e \in \text{Top8}} g_e \cdot \text{Expert}_e(x)

为什么最下面 3 层用稠密 FFN?DeepSeekMoE 论文的观察是:最前面几层的负载均衡收敛得特别慢,干脆不用 MoE。

5.3 路由器代码走读

DeepSeek-V3 官方推理代码(inference/model.py 的 Gate.forward,sigmoid 分支)的步骤,05 号脚本照着写了一遍(v3_gate),与我们的 moe.route(kind="sigmoid") 选中的专家完全相同、权重差 0:

scores = linear(x, self.weight).sigmoid()          # 每个专家一个 0–1 的亲和度
original_scores = scores
scores = scores + self.bias                         # 均衡偏置:只用来选
scores = scores.view(n, n_groups, -1)
group_scores = scores.topk(2, dim=-1)[0].sum(-1)    # 每组的分数 = 组内最大的两个之和
indices = group_scores.topk(topk_groups, dim=-1)[1] # 保留 4 组
mask = ones(n, n_groups).scatter_(1, indices, False)
scores = scores.masked_fill_(mask.unsqueeze(-1), -inf).flatten(1)
indices = torch.topk(scores, self.topk, dim=-1)[1]  # 在 4 组里选 8 个专家
weights = original_scores.gather(1, indices)        # 权重用不带偏置的分数
weights /= weights.sum(-1, keepdim=True)
weights *= self.route_scale                         # 2.5

三个性质(05 号脚本第 2 部分,2000 个随机 token):

  1. 每个 token 的 8 个专家最多来自 4 组(实际 1942 个来自 4 组、55 个 3 组、3 个 2 组)。
  2. 权重之和恒为 2.5:路由专家的输出总以同样的”总量”加到共享专家的输出上。
  3. 把一个专家的偏置加 0.3,选中它的 token 从 96 个变成 950 个;而选中集合没变的 token,权重只差舍入(1.7e-16)——偏置改变”选谁”,不改变函数本身。读代码时最容易漏掉的就是 original_scores 这一行。

slides 第 36–39 页还走读了 DeepSpeed 的 MoE 代码(deepspeed/moe/layer.py、experts.py):用 deepspeed.moe.layer.MoE(hidden_size, expert, num_experts, ep_size, k) 把一个 FFN 包成 MoE 层,ep_size 指定专家并行的卡数;门控、容量、all-to-all 都在库里完成。


6. 小结 · Summary

  1. MoE 用 E 个专家替换 FFN,每个 token 只走 top-k 个:参数和计算解耦。Mixtral 47B / 13B,DeepSeek-V3 671B / 37B;每个 token 的计算是同样总参数稠密模型的 1/3.7 和 1/18。
  2. 代价一:显存放下全部专家;批大时几乎所有专家都要读,推理是访存受限的。
  3. 路由有几种:softmax-top-k(Switch / GShard)、top-k-softmax(Mixtral)、sigmoid + 分组 + 偏置(V3)。高效实现按专家分组做矩阵乘法;容量因子决定每个专家的缓冲区,超出的 token 被丢弃(只剩残差)。
  4. 不加约束时路由会坍缩(富者愈富)。均衡损失 α·E·Σ fᵢPᵢ 的梯度经 Pᵢ 压低热门专家;V3 用只影响选择的偏置做均衡,不给模型添加无关的梯度。
  5. 专家并行:专家分到各卡、其余复制;每层前向两次 all-to-all(派发、收回),反向是对调的两次。V3 每个 token 每层约 172 KB;按节点限制路由、专家复制(EPLB)、分层 all-to-all 分别控制跨节点流量、卡间不均衡和延迟。
  6. 细粒度 + 共享专家(DeepSeekMoE)在大模型上更好,但在小任务上未必;它让矩阵更碎、通信目的地更多。

7. 常见误区 · Common pitfalls

  1. “Mixtral 8×7B 有 56B 参数。” 只有 FFN 被复制,47B(§1.2)。
  2. “MoE 推理又省计算又省带宽。” 批大时几乎每个专家都被选中,带宽并不省(§1.4)。
  3. “FLOPs 一样,MoE 就和稠密 FFN 一样快。” 分组、gather / scatter、小矩阵都有开销(§2.2)。
  4. “Switch 的门控权重应该归一化。” k = 1 时归一化后恒为 1,路由器得不到梯度(§2.1)。
  5. “被丢弃的 token 会让模型出错。” 它只是这一层少算了 MoE,残差照常传下去;代价是质量,不是崩溃(§2.4)。
  6. “负载均衡只是为了效率。” 坍缩也伤害效果(§3.1);不过它首先是系统问题(§3.3)。
  7. “V3 的偏置也加到了权重上。” 偏置只用来选,权重用 original_scores(§5.3)。
  8. “专家并行只需要一次通信。” 前向派发、收回两次 all-to-all(加计数),反向再两次;复制的路由器还要 all-reduce(§4.2)。
  9. “专家之间均衡了,卡之间就均衡了。” 热门专家要复制,摆放要按负载(§4.4)。
  10. “细粒度专家一定更好。” 在大模型上是;小模型上未必,而且系统代价更高(§5.1)。

8. 练习 · Exercises

  1. 一个 MoE 模型:32 层、d = 4096、注意力每层 4d² 参数、每个专家是 SwiGLU(中间维 11008)、16 个专家选 2 个、词表 32000(输入输出不共享)。算总参数和每个 token 的激活参数。
  2. E = 64、k = 2,一批 B = 32 个 token,平均每层碰到几个专家?如果每个专家 50 MB(FP8),解码这一步每层要读多少字节?
  3. 写出 Switch 的均衡损失。如果 4 个专家的 f = (0.7, 0.1, 0.1, 0.1)、P = (0.4, 0.2, 0.2, 0.2),损失是多少(不乘 α)?完全均衡时呢?
  4. n = 2048 个 token、E = 16、k = 2、cf = 1.25:每个专家的容量是多少?如果某个专家被选了 400 次,丢掉几次?
  5. 解释为什么 Switch(k = 1)用”概率”而不是 1 作为门控权重。DeepSeek-V3 的”sigmoid + 归一化”在 k = 1 时会怎样?
  6. DeepSeek-V3 的偏置更新规则是什么?它为什么不会改变模型在”同样的专家选择”下的输出?
  7. 4 块卡的专家并行,卡 0 要发给 4 块卡的 (token, 选择) 数是 [10, 5, 20, 5]。前向一共有哪几次集合通信?反向呢?AllToAll 的反向里 send 和 recv 怎么变?
  8. d = 4096、k = 2、BF16,每个 token 每个 MoE 层前向要派发和收回多少字节?32 层、每步 4M 个 token,一步一共多少?
  9. 256 个专家分 8 组,每个 token 选 8 个。如果不限制,最坏情况发往几个节点?限制 4 组时呢?同一节点的多个专家为什么只需跨节点发一次?
  10. 有 8 个专家的负载 (8, 1, 1, 1, 1, 1, 1, 2),放到 4 块卡上(每块卡至少一个)。最好的摆法最忙的卡是多少?复制最热的专家之后呢?
  11. 直接 all-to-all 与分层 all-to-all(每机 8 卡),P = 128 时每块卡分别发几条跨机器的消息?
  12. 细粒度专家:把 8 个专家(中间维 4096、选 2)切成 32 个(中间维 1024、选 8),总参数、每 token 计算、组合数、all-to-all 每个 token 的派发份数各怎么变?

参考答案

  1. 每层注意力 4 × 4096² = 67.1M;每个专家 3 × 4096 × 11008 = 135.3M。总:32 × (67.1M + 16 × 135.3M) + 2 × 32000 × 4096 = 32 × 2231.4M + 262.1M ≈ 71.7B。激活:32 × (67.1M + 2 × 135.3M) + 262.1M ≈ 11.1B(忽略路由器和归一化)。
  2. 64 × (1 − (1 − 2/64)^32) = 64 × (1 − 0.3623) ≈ 40.8 个;约 40.8 × 50 MB ≈ 2.0 GB 每层(B = 1 时只读 2 个,100 MB)。
  3. L = E·Σ fᵢPᵢ = 4 × (0.7 × 0.4 + 3 × 0.1 × 0.2) = 4 × 0.34 = 1.36;均衡时 4 × 4 × (1/4 × 1/4) = 1。
  4. ⌈1.25 × 2 × 2048 / 16⌉ = 320;400 − 320 = 80 次。
  5. k = 1 时如果权重恒为 1,输出与路由器的分数无关,任务损失对路由器没有梯度,路由器学不到”选得好不好”;用概率 p 作权重,输出依赖 p,梯度就能告诉路由器调高好专家的概率。V3 的 sigmoid + 归一化在 k = 1 时权重恒为 2.5,同样没有梯度(V3 用 k = 8)。
  6. 每步之后,负载高于平均的专家 bᵢ −= γ,低于平均的 bᵢ += γ。偏置只加在用来选专家的分数上,权重来自不带偏置的 sigmoid 分数,所以选择不变时输出完全不变。
  7. 前向:all-to-all 交换计数、all-to-all 派发(token,以及专家编号)、all-to-all 收回;反向:收回的反向(一次 all-to-all)、派发的反向(一次 all-to-all),复制参数(路由器等)的梯度 all-reduce。AllToAll 的反向把 send 和 recv 对调:卡 0 前向发 [10, 5, 20, 5],反向就收 [10, 5, 20, 5]。
  8. 2 × 4096 × 2 字节 × 2(派发 + 收回)= 32 KB;32 层 × 4 × 10⁶ token × 32 KB ≈ 4.2 TB(前向;反向再一倍)。
  9. 最坏 8 个专家分在 8 组,发往 8 个节点;限制后最多 4 个。同一节点的多个专家:跨节点只需把 token 的激活送到那个节点一次,节点内部再用 NVLink 复制给各个专家所在的卡。
  10. 总负载 16,平均 4。最热的专家 8 单独占一块卡,最忙就是 8(= 2 × 平均);其余 8 分给 3 块卡(如 3、3、2)。把专家 0 复制一份(各 4):两块卡各 4,剩下的 8 分给另两块卡(4、4),最忙 4。
  11. 直接:P − 8 = 120 条(另 7 条在机器内);分层:P/8 − 1 = 15 条(每条大 8 倍)。
  12. 总参数不变、每 token 计算不变;组合数从 C(8, 2) = 28 变成 C(32, 8) = 10,518,300;每个 token 的派发份数从 2 变成 8(每份大小都是 d,所以派发的字节数变成 4 倍,除非像 V3 那样按节点去重)。

9. 延伸阅读 · Further reading

  • Shazeer et al. (2017) Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer:现代 MoE 的起点(噪声 top-k 门控)。
  • Lepikhin et al. (2021) GShard:专家并行、all-to-all、top-2、容量。
  • Fedus et al. (2022) Switch Transformers:top-1 路由、容量因子、均衡损失、训练稳定性。
  • Rajbhandari et al. (2022) DeepSpeed-MoE:Residual-MoE(共享专家)、推理系统(专家并行 + 张量并行、分层 all-to-all、内核)。
  • Dai et al. (2024) DeepSeekMoE:细粒度专家与共享专家。
  • DeepSeek-AI (2024) DeepSeek-V3 Technical Report:无辅助损失的均衡、按节点限制路由、FP8 派发;DeepEP、EPLB 两个开源库。
  • Jiang et al. (2024) Mixtral of Experts。
  • Wang et al. (2024) Auxiliary-Loss-Free Load Balancing Strategy for Mixture-of-Experts:偏置均衡的原始论文。

10. 下一讲预告 · Next lecture

第 14–17 讲讲了四种并行:数据、流水线、张量、专家。它们解决的都是”怎样把计算分到很多卡上”。第 18 讲回到一个更朴素的问题:显存。数据并行时每块卡都存一份完整的参数、梯度和 Adam 状态——16 字节 / 参数,大部分是冗余的。ZeRO 把这三样依次切开(ZeRO-1、2、3),让显存随卡数线性下降,而通信几乎不增加。我们会算清楚每一级省多少、多传多少,在本机模拟 ZeRO 的三个阶段并与普通数据并行逐位比较,再用 PyTorch 自带的 ZeroRedundancyOptimizer 和 FSDP 真正跑一遍。

← 返回课程目录