对应 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
学完这一讲,你应该能够:
- 说清楚 MoE 怎样把”参数量”和”每个 token 的计算量”分开,并能核算 Mixtral、DeepSeek-V3 的总参数与激活参数。
- 从零实现一个 top-k 路由的 MoE 层,比较 Switch、GShard、Mixtral、DeepSeek-V3 的路由方式;理解容量因子与 token 丢弃。
- 解释为什么需要负载均衡,写出 E·Σ fᵢPᵢ 损失,说明 DeepSeek-V3 的无辅助损失偏置为什么不干扰模型。
- 描述专家并行的数据流(两次 all-to-all),估算它的通信量,理解按节点限制路由、专家摆放与复制、分层 all-to-all 各在解决什么。
- 读懂 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.34B | 2.9% |
| 专家(32 层 × 8 个) | 45.10B | 96.6% |
| 嵌入 + 输出层 | 0.26B | 0.6% |
| 合计 | 46.70B | |
| 每个 token 激活 | 12.88B | 28% |
只有 FFN 被复制成 8 份,注意力、嵌入、归一化是共用的——所以是 47B 而不是 8 × 7 = 56B。每个 token 只走 2 个专家,激活 12.9B:计算相当于一个 13B 的稠密模型,参数却有 47B。
DeepSeek-V3(slides 第 32 页)更极端:
| 部分 | 参数 | 占比 |
|---|---|---|
| 注意力(MLA,61 层) | 11.41B | 1.7% |
| 稠密 FFN(前 3 层) | 1.19B | 0.2% |
| 路由专家(58 层 × 256 个) | 653.91B | 97.4% |
| 共享专家(58 层 × 1 个) | 2.55B | 0.4% |
| 嵌入 + 输出层 | 1.85B | 0.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×7B | 25 GFLOP | 93 GFLOP | 3.7× |
| DeepSeek-V3 | 73 GFLOP | 1340 GFLOP | 18.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 = 1 | 4 | 16 | 64 | 256 |
|---|---|---|---|---|---|
| Mixtral(8 选 2) | 2.0(25%) | 5.5(68%) | 7.9(99%) | 8.0 | 8.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 个专家,输出
几种常见的路由(moe.route):
| 路由 | 打分 → 选择 | 权重 | 代表 |
|---|---|---|---|
| softmax → top-k | 对 E 个专家做 softmax,取最大的 k 个 | 就是概率(不归一化) | Switch(k = 1) |
| softmax → top-k,归一化 | 同上 | 这 k 个概率再归一化 | GShard(k = 2) |
| top-k → softmax | 先取打分最大的 k 个 | 只在这 k 个里 softmax | Mixtral(k = 2) |
| sigmoid,分组,偏置 | sigmoid 打分 + 偏置,先选组再选专家 | 不带偏置的分数归一化 × 2.5 | DeepSeek-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):
- 路由:得到每个 token 的 k 个专家编号 idx 和权重 w;
- 按专家分组:对每个专家 e,找出选了它的所有 (token, 选择),把这些 token 拼成一个矩阵;
- 每个专家对它的 token 做一次矩阵乘法;
- 按权重加回原位(index_add)。
与逐 token 的参照只差舍入(02 号脚本第 1 部分)。第 4 部分在本机测了一下(⏱,d = 512、4096 个 token):
| 层 | 参数 | 前向 GFLOP | ⏱ 本机 |
|---|---|---|---|
| MoE(8 选 2,每个专家 FFN 1024) | 8.4M | 17.21 | ≈ 20–23 ms |
| 稠密 FFN 2048(同样的激活参数) | 2.1M | 17.18 | ≈ 13 ms |
| 稠密 FFN 8192(同样的总参数) | 8.4M | 68.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):
cf 是容量因子(capacity factor)。完全均衡时每个专家正好分到 k·n/E 个;cf > 1 留出余量。超出容量的 token 被丢弃:它在这一层的 MoE 输出为 0,只剩残差连接把输入原样传下去(不会出错,但这个 token 少算了一层)。02 号脚本第 3 部分(4096 个 token、8 个专家):
| 路由器 | k | 最忙专家 ÷ 平均 | cf = 1.0 | 1.25 | 2.0 |
|---|---|---|---|---|---|
| 均衡(独立随机打分) | 1 | 1.05 | 丢 2.0% / 空 2% | 0.0% / 20% | 0.0% / 50% |
| 不均衡(偏向后面的专家) | 1 | 2.12 | 丢 25.7% / 空 26% | 15.5% / 32% | 1.5% / 51% |
| 不均衡 | 2 | 1.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 | 没人用的专家 | 最忙专家 ÷ 平均 |
|---|---|---|---|
| 0 | 0.0527 | 2.7 个 | 2.68 |
| 0.01 | 0.0479 | 1.3 个 | 2.04 |
| 0.1 | 0.0370 | 0 个 | 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 的写法只差常数):
- 完全均衡时 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.0402 | 1.73 | 22.1% | 12.1% |
| softmax,α = 0.1 | 0.0404 | 1.15 | 4.7% | 0.0% |
| sigmoid,不加均衡 | 0.0428 | 1.50 | 12.0% | 4.6% |
| sigmoid + 偏置(γ = 0.001) | 0.0410 | 1.05 | 1.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。每块卡的步骤:
- 路由,得到每个 (token, 选择) 的专家编号;
- 按目的卡稳定排序;
- 先用一次 all-to-all 交换计数(“我要发给你几行”),接收方才能分配缓冲区;
- all-to-all 派发 token(和它们的专家编号);
- 本地专家计算;
- all-to-all 收回(切分方向相反);
- 恢复原始顺序,按权重加起来。
与单进程比较:输出、对输入的梯度、专家权重的梯度逐位相同(稳定排序让每个专家收到的 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.00 | 1.00 | 1.00 | 1.00 |
| 不均(s = 0.6) | 2.79 | 1.43 | 1.01 | 1.00 |
| 很不均(s = 1.0) | 4.58 | 2.20 | 1.69 | 1.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 | 直接 | 分层 | 快了 |
|---|---|---|---|
| 16 | 95 μs | 26 μs | 3.7× |
| 256 | 2497 μs | 327 μs | 7.6× |
| 1024 | 10177 μs | 1287 μs | 7.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,选 2 | 0.4692 |
| 细粒度:32 个专家 × 4,选 8 | 0.4919 |
| 细粒度 + 共享:1 + 31 个专家 × 4,选 7 | 0.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 层的输出:
为什么最下面 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):
- 每个 token 的 8 个专家最多来自 4 组(实际 1942 个来自 4 组、55 个 3 组、3 个 2 组)。
- 权重之和恒为 2.5:路由专家的输出总以同样的”总量”加到共享专家的输出上。
- 把一个专家的偏置加 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
- MoE 用 E 个专家替换 FFN,每个 token 只走 top-k 个:参数和计算解耦。Mixtral 47B / 13B,DeepSeek-V3 671B / 37B;每个 token 的计算是同样总参数稠密模型的 1/3.7 和 1/18。
- 代价一:显存放下全部专家;批大时几乎所有专家都要读,推理是访存受限的。
- 路由有几种:softmax-top-k(Switch / GShard)、top-k-softmax(Mixtral)、sigmoid + 分组 + 偏置(V3)。高效实现按专家分组做矩阵乘法;容量因子决定每个专家的缓冲区,超出的 token 被丢弃(只剩残差)。
- 不加约束时路由会坍缩(富者愈富)。均衡损失 α·E·Σ fᵢPᵢ 的梯度经 Pᵢ 压低热门专家;V3 用只影响选择的偏置做均衡,不给模型添加无关的梯度。
- 专家并行:专家分到各卡、其余复制;每层前向两次 all-to-all(派发、收回),反向是对调的两次。V3 每个 token 每层约 172 KB;按节点限制路由、专家复制(EPLB)、分层 all-to-all 分别控制跨节点流量、卡间不均衡和延迟。
- 细粒度 + 共享专家(DeepSeekMoE)在大模型上更好,但在小任务上未必;它让矩阵更碎、通信目的地更多。
7. 常见误区 · Common pitfalls
- “Mixtral 8×7B 有 56B 参数。” 只有 FFN 被复制,47B(§1.2)。
- “MoE 推理又省计算又省带宽。” 批大时几乎每个专家都被选中,带宽并不省(§1.4)。
- “FLOPs 一样,MoE 就和稠密 FFN 一样快。” 分组、gather / scatter、小矩阵都有开销(§2.2)。
- “Switch 的门控权重应该归一化。” k = 1 时归一化后恒为 1,路由器得不到梯度(§2.1)。
- “被丢弃的 token 会让模型出错。” 它只是这一层少算了 MoE,残差照常传下去;代价是质量,不是崩溃(§2.4)。
- “负载均衡只是为了效率。” 坍缩也伤害效果(§3.1);不过它首先是系统问题(§3.3)。
- “V3 的偏置也加到了权重上。” 偏置只用来选,权重用 original_scores(§5.3)。
- “专家并行只需要一次通信。” 前向派发、收回两次 all-to-all(加计数),反向再两次;复制的路由器还要 all-reduce(§4.2)。
- “专家之间均衡了,卡之间就均衡了。” 热门专家要复制,摆放要按负载(§4.4)。
- “细粒度专家一定更好。” 在大模型上是;小模型上未必,而且系统代价更高(§5.1)。
8. 练习 · Exercises
- 一个 MoE 模型:32 层、d = 4096、注意力每层 4d² 参数、每个专家是 SwiGLU(中间维 11008)、16 个专家选 2 个、词表 32000(输入输出不共享)。算总参数和每个 token 的激活参数。
- E = 64、k = 2,一批 B = 32 个 token,平均每层碰到几个专家?如果每个专家 50 MB(FP8),解码这一步每层要读多少字节?
- 写出 Switch 的均衡损失。如果 4 个专家的 f = (0.7, 0.1, 0.1, 0.1)、P = (0.4, 0.2, 0.2, 0.2),损失是多少(不乘 α)?完全均衡时呢?
- n = 2048 个 token、E = 16、k = 2、cf = 1.25:每个专家的容量是多少?如果某个专家被选了 400 次,丢掉几次?
- 解释为什么 Switch(k = 1)用”概率”而不是 1 作为门控权重。DeepSeek-V3 的”sigmoid + 归一化”在 k = 1 时会怎样?
- DeepSeek-V3 的偏置更新规则是什么?它为什么不会改变模型在”同样的专家选择”下的输出?
- 4 块卡的专家并行,卡 0 要发给 4 块卡的 (token, 选择) 数是 [10, 5, 20, 5]。前向一共有哪几次集合通信?反向呢?AllToAll 的反向里 send 和 recv 怎么变?
- d = 4096、k = 2、BF16,每个 token 每个 MoE 层前向要派发和收回多少字节?32 层、每步 4M 个 token,一步一共多少?
- 256 个专家分 8 组,每个 token 选 8 个。如果不限制,最坏情况发往几个节点?限制 4 组时呢?同一节点的多个专家为什么只需跨节点发一次?
- 有 8 个专家的负载 (8, 1, 1, 1, 1, 1, 1, 2),放到 4 块卡上(每块卡至少一个)。最好的摆法最忙的卡是多少?复制最热的专家之后呢?
- 直接 all-to-all 与分层 all-to-all(每机 8 卡),P = 128 时每块卡分别发几条跨机器的消息?
- 细粒度专家:把 8 个专家(中间维 4096、选 2)切成 32 个(中间维 1024、选 8),总参数、每 token 计算、组合数、all-to-all 每个 token 的派发份数各怎么变?
参考答案
- 每层注意力 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(忽略路由器和归一化)。
- 64 × (1 − (1 − 2/64)^32) = 64 × (1 − 0.3623) ≈ 40.8 个;约 40.8 × 50 MB ≈ 2.0 GB 每层(B = 1 时只读 2 个,100 MB)。
- 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。
- ⌈1.25 × 2 × 2048 / 16⌉ = 320;400 − 320 = 80 次。
- k = 1 时如果权重恒为 1,输出与路由器的分数无关,任务损失对路由器没有梯度,路由器学不到”选得好不好”;用概率 p 作权重,输出依赖 p,梯度就能告诉路由器调高好专家的概率。V3 的 sigmoid + 归一化在 k = 1 时权重恒为 2.5,同样没有梯度(V3 用 k = 8)。
- 每步之后,负载高于平均的专家 bᵢ −= γ,低于平均的 bᵢ += γ。偏置只加在用来选专家的分数上,权重来自不带偏置的 sigmoid 分数,所以选择不变时输出完全不变。
- 前向: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]。
- 2 × 4096 × 2 字节 × 2(派发 + 收回)= 32 KB;32 层 × 4 × 10⁶ token × 32 KB ≈ 4.2 TB(前向;反向再一倍)。
- 最坏 8 个专家分在 8 组,发往 8 个节点;限制后最多 4 个。同一节点的多个专家:跨节点只需把 token 的激活送到那个节点一次,节点内部再用 NVLink 复制给各个专家所在的卡。
- 总负载 16,平均 4。最热的专家 8 单独占一块卡,最忙就是 8(= 2 × 平均);其余 8 分给 3 块卡(如 3、3、2)。把专家 0 复制一份(各 4):两块卡各 4,剩下的 8 分给另两块卡(4、4),最忙 4。
- 直接:P − 8 = 120 条(另 7 条在机器内);分层:P/8 − 1 = 15 条(每条大 8 倍)。
- 总参数不变、每 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 真正跑一遍。