DeepSeek-V3 有 671B 参数,每个 token 却只用到其中 37B:每层 256 个专家,路由器只挑 8 个。混合专家(Mixture of Experts, MoE)就这样把参数量和计算量拆开了。账单落在两处:路由必须均衡,否则大部分专家闲着、最忙的那张卡拖慢所有人;专家分到不同的卡上之后,每层前向要做两次 all-to-all,把 token 送过去再取回来。流水线并行里的 DualPipe 费那么大劲去重叠的,正是这两次 all-to-all。
从稠密 FFN 到 MoE
先回顾一下 Transformer 块里的前馈网络(FFN,也叫 MLP)。对每个 token 的隐状态 \(u \in \mathbb{R}^h\),现代 LLM 常用 SwiGLU 形式:
$$\mathrm{FFN}(u) = W_{down}\,\big(\mathrm{SiLU}(W_{gate}\,u) \odot W_{up}\,u\big),\quad W_{gate}, W_{up} \in \mathbb{R}^{f\times h},\ W_{down}\in\mathbb{R}^{h\times f}$$三块矩阵共 \(3hf\) 个参数,前向每个 token 约 \(2\cdot 3hf = 6hf\) 次浮点运算(一次乘加记 2 FLOPs)。FFN 有个关键性质:它逐 token 独立,不同 token 之间没有交互(交互都在注意力里)。这就给了「不同 token 走不同参数」的空间。
MoE 把一个 FFN 换成 \(E\) 个结构相同的 FFN(称为专家,expert),再加一个小小的路由器(router,也叫 gating network)。每个 token 只被送到其中 \(k\) 个专家,输出是这 \(k\) 个专家输出的加权和:
$$h'_t = u_t + \sum_{i=1}^{E} g_{i,t}\,\mathrm{FFN}_i(u_t),\qquad g_{i,t}=0\ \text{对未选中的专家}$$于是参数量涨了约 \(E\) 倍,而每个 token 的计算只涨了 \(k\) 倍。\(E/k\) 常被称为稀疏度(sparsity),一般在 8 到 64 之间;DeepSeek-V3 是 \(E=256\)、\(k=8\),稀疏度 32。这种「参数多、但每个输入只用一小部分」的思路叫条件计算(conditional computation):模型很大,但每个 token 只「走过」其中一条窄路。它在大规模神经网络里的现代版本来自 Shazeer 等人 2017 年的 Sparsely-Gated MoE。
为什么值得这么做?因为在训练 FLOPs 固定时,更多参数通常意味着更低的 loss:参数可以看作模型能存下的「知识容量」,而 FLOPs 是每次查询的成本,MoE 把这两者解耦了。MoE 让我们用同样的每 token 计算量撑起大得多的参数量。代价全在系统侧:参数显存变大、每个专家拿到的 batch 变小(Shazeer 等人称为 shrinking batch problem)、路由会不均衡、专家分布在不同卡上时要搬运 token。后面基本都在处理这些代价。
shrinking batch 值得算一下。一张卡上 \(T\) 个 token、\(E\) 个专家、top-\(k\),均匀路由时每个专家只拿到 \(Tk/E\) 个 token。专家的矩阵乘形状是 \([Tk/E,\ h]\times[h,\ d_e]\)(\(d_e\) 是专家的中间维度)。它的算术强度(每从显存读一字节权重能做多少 FLOPs)正比于这个行数:权重 \(h\times d_e\) 读一次,被多少行 token 复用,就做多少倍的计算。稠密模型里每个 FFN 吃满 \(T\) 行,MoE 里只有 \(Tk/E\) 行:以 \(E/k=32\) 为例,同样的 \(T\) 下专家 GEMM 的行数只有稠密的 1/32,很容易掉进带宽受限区。解法只有一个:让每个专家看到更多 token——把很多数据并行副本的 token 汇聚到同一个专家上,这正是后面专家并行的出发点。
路由器:打分、top-k、加权求和
路由器本身就是一个线性层。设一批里有 \(T\) 个 token,隐状态堆成 \(U \in \mathbb{R}^{T\times h}\),路由器参数 \(W_r \in \mathbb{R}^{h\times E}\)(第 \(i\) 列 \(e_i\) 可以看成专家 \(i\) 的「质心」向量),得到路由 logits:
$$H = U W_r \in \mathbb{R}^{T\times E},\qquad H_{t,i} = u_t^\top e_i$$然后三步:
- 打分:把 logits 变成亲和度(affinity)\(s_{i,t}\),用 softmax 或 sigmoid。
- 选 top-k:对每个 token 取分数最高的 \(k\) 个专家,记为集合 \(\mathcal{T}_t\)。
- 加权:门控值 \(g_{i,t}\) 由选中专家的分数得到(可能再归一化),未选中的为 0;输出 \(\sum_{i\in\mathcal{T}_t} g_{i,t}\mathrm{FFN}_i(u_t)\)。
路由器怎么学?top-k 选择本身不可导,但门控值 \(g_{i,t}\) 是可导的:输出对 \(g_{i,t}\) 的梯度是 \(\mathrm{FFN}_i(u_t)\) 与上游梯度的内积,再经 softmax/sigmoid 传回 \(W_r\)。所以路由器学的是「被选中的专家里,谁该多给权重」,而谁被选中这件事只通过分数的相对大小间接地被改变。这也是为什么输出要乘门控值,而不是直接把选中专家的输出相加:没有这个乘法,路由器就收不到任何梯度。
写成式子:设上游传回的梯度为 \(\delta_t = \partial \mathcal{L}/\partial h'_t\),则
$$\frac{\partial \mathcal{L}}{\partial g_{i,t}} = \delta_t^\top\,\mathrm{FFN}_i(u_t)\quad (i\in\mathcal{T}_t),$$再乘上 \(\partial g/\partial H\) 传到 logits、传到 \(W_r\) 和 \(u_t\)。直观地说:如果专家 \(i\) 的输出方向与「让 loss 下降的方向」一致(\(\delta^\top \mathrm{FFN}_i\lt 0\)),它的门控值就被调大,进而它在这类 token 上的 logit 被调高,下次更可能被选中。未被选中的专家完全拿不到这种信号,这也是坍缩的根源之一:没被选中就没有机会证明自己。
Shazeer 2017 的版本是 noisy top-k:在 logits 上加一个可学习幅度的高斯噪声再取 top-k,未选中的位置置为 \(-\infty\) 后再 softmax:
$$G(x)=\mathrm{Softmax}(\mathrm{KeepTopK}(H(x),k)),\quad H(x)_i = (xW_g)_i + \mathcal{N}(0,1)\cdot\mathrm{Softplus}((xW_{noise})_i)$$噪声的作用是帮助负载均衡(让分数接近的专家都有机会被选到)。他们当时猜测 \(k>1\) 是必要的,否则路由器没有「比较两个专家」的梯度信号;Switch Transformer 反过来证明 \(k=1\) 也能工作,而且路由计算更省、每个专家的容量可以减半、通信更少。GShard 用 top-2,并对第二个专家做随机路由:以正比于其(归一化后)门控值 \(g_2\) 的概率才真正派发,门控值很小时干脆省掉,节省容量。
从张量形状的角度看一次 MoE 前向(单卡、不考虑并行):
U : [T, h] # 本批 token
logits = U @ W_r # [T, E]
s = score(logits) # [T, E] softmax / sigmoid
idx, w = topk(s, k) # [T, k] 专家编号, [T, k] 门控值(可能归一化)
# 按专家分组:把 T*k 个 (token, 专家) 对按专家编号排序
perm = argsort(idx.flatten())
X = U[perm // k] # [T*k, h] 每个专家的输入连续存放
Y = grouped_ffn(X, counts_per_expert) # [T*k, h]
out = zeros(T, h).index_add(0, perm // k, Y * w.flatten()[perm, None])
\(T\cdot k\) 个 (token, 专家) 对展平后,第 \(j\) 个对属于 token \(\lfloor j/k \rfloor\),所以 perm // k 就是排序后每一行对应的 token 编号。这个「排序—按专家分组算—按原顺序加回去」的结构,到了专家并行只是把中间那步换成跨卡的 all-to-all。
softmax 门控还是 sigmoid 门控
打分函数有几种常见组合,差别在在哪一步归一化:
| 做法 | 公式 | 代表 |
|---|---|---|
| 先 softmax 再 top-k(不再归一化) | \(s=\mathrm{softmax}(H)\),\(g_i = s_i\ (i\in\mathcal{T})\) | Switch(k=1)、DeepSeekMoE、DeepSeek-V2 |
| 先 softmax 再 top-k,再在选中者间归一化 | \(g_i = s_i/\sum_{j\in\mathcal{T}}s_j\) | GShard 的 top-2 |
| 先 top-k 再 softmax | \(g=\mathrm{softmax}(\mathrm{TopK}(H))\) | Shazeer 2017、Mixtral |
| sigmoid,再在选中者间归一化 | \(s_i=\sigma(u^\top e_i)\),\(g_i = s_i/\sum_{j\in\mathcal{T}}s_j\) | DeepSeek-V3 |
后两种「选中者之间归一化」在数学上是一回事吗?先 top-k 再 softmax 等价于对选中的 logits 做 softmax,即 \(g_i = e^{H_i}/\sum_{j\in\mathcal{T}}e^{H_j}\);先 softmax 再在选中者间归一化,\(e^{H_i}/Z\) 除以 \(\sum_{j\in\mathcal T} e^{H_j}/Z\),结果完全相同。区别只在不归一化的那一类:门控值之和小于 1,且受未选中专家分数的影响。
softmax 和 sigmoid 的本质区别是专家之间是否竞争。softmax 的分数和为 1,一个专家分数上升必然压低其他专家;sigmoid 对每个专家独立打分,\(s_i \in (0,1)\) 只取决于 \(u^\top e_i\)。DeepSeek-V3 从 V2 的 softmax 换成了 sigmoid,再在选中的分数之间归一化得到门控值;报告没有解释原因,也没有给出这一项的消融。从结构上看,sigmoid 至少有两点方便:专家数到 256 时,softmax 的分数平均只有 \(1/256\),彼此强耦合,而 sigmoid 让每个专家的分数有自己的尺度;后面那个只用于选择的偏置项(见「无辅助损失的负载均衡」)加在一个与其他专家解耦的分数上,也更容易控制。
还有一个容易混的概念:Expert Choice 路由(Zhou et al., 2022)反过来让每个专家挑 top-C 个 token,天然均衡;但一个 token 被谁选中取决于同一批里其他 token(包括序列中后面的 token),对自回归语言模型来说等于泄露了未来信息。这也是 DeepSeek 不采用 Expert Choice 的原因。
专家容量与丢 token
路由是数据相关的:同一批里,有的专家可能被几百个 token 选中,有的一个都没有。但加速器(尤其 TPU/XLA 这种静态形状的编译器)希望每个专家的输入是固定形状 \([C, h]\)。于是有了专家容量(expert capacity):
$$C = \left\lceil \mathrm{CF}\cdot\frac{T\cdot k}{E} \right\rceil$$\(T k/E\) 是完全均衡时每个专家应收到的分配数(\(T\) 个 token,每个分给 \(k\) 个专家,共 \(Tk\) 个分配,平摊到 \(E\) 个专家),\(\mathrm{CF}\)(capacity factor)是留出的余量,比如 1.25 表示允许比平均多 25%。Switch Transformer(\(k=1\))和 GShard(\(k=2\))用的都是这个式子。
- 某个专家收到超过 \(C\) 个分配时,多出来的被丢弃(dropped tokens):这个 token 在该专家处不计算,若它所有选中的专家都满了,就只通过残差连接原样进入下一层。GShard 的实现是维护一个计数器,按 token 顺序依次填入专家缓冲区。
- 收到不足 \(C\) 个时,剩下的位置是padding,照样算、白算。
所以 CF 是一个直接的权衡:CF 大,丢得少,但计算和通信(固定形状的 buffer 要整块传)都变多;CF 小则反之。具体地,所有专家的槽位总数是 \(E\cdot C \approx \mathrm{CF}\cdot Tk\),即专家计算量和 all-to-all 的数据量都按 CF 倍放大。举个数:\(T=4096\)、\(E=64\)、\(k=8\)、\(\mathrm{CF}=1.25\),均衡负载是 \(4096\times8/64=512\),容量 \(C=640\);若最热门的专家收到 900 个分配,就要丢 260 个,而冷门专家可能只用了 300 个槽、白算 340 个。Switch Transformer 的经验是丢 token 的比例要压低(他们的实验里通常 <1%),而只要辅助损失系数足够,负载就足够均衡。
后来的工作逐渐走向不丢 token(dropless):MegaBlocks 把 MoE 计算重写成 block-sparse 矩阵乘,GPU 上处理变长的专家输入,「既不丢 token 也不浪费在 padding 上」;DeepSeek-V2 训练时用的是设备级丢 token:先算出每个设备的平均计算预算(相当于设备级 CF = 1.0),在超预算的设备上丢掉亲和度最低的 token,并保证约 10% 的训练序列的 token 永远不丢;DeepSeek-V3 则说由于负载均衡做得好,训练和推理都不丢 token。
还有一个细节:top-k 时丢谁?GShard 的算法先为所有 token 的第一选择占位,再处理第二选择,这样容量紧张时优先保住每个 token 的首选专家。下面的模拟器也按这个顺序(先所有 token 的第 1 选择,再第 2 选择……)。
负载均衡辅助损失:推导与梯度
不加约束地训练,路由器很容易坍缩(routing collapse):某几个专家早期碰巧分数高、被选得多、于是被训练得更好、于是分数更高……最后大部分专家几乎没有 token。坍缩有两重坏处:参数浪费,以及专家分布在多张卡上时,最忙的那张卡决定整步的时间。
从「想要什么」开始
设 \(f_i\) 是这批 \(T\) 个 token 中被分配给专家 \(i\) 的比例。完全均衡时 \(f_i = 1/E\)。一个自然的目标是最小化 \(\sum_i f_i^2\):在 \(\sum_i f_i = 1\) 的约束下,由柯西–施瓦茨不等式
$$\Big(\sum_i f_i\Big)^2 \le E\sum_i f_i^2 \;\Rightarrow\; \sum_i f_i^2 \ge \frac{1}{E},$$等号当且仅当所有 \(f_i\) 相等。问题是 \(f_i\) 由 top-k 计数得来,对参数不可导。GShard 的做法(Switch 继承并简化)是把平方中的一个 \(f_i\) 换成可导的近似——路由器分给专家 \(i\) 的平均概率 \(P_i\):
$$P_i = \frac{1}{T}\sum_{t} s_{i,t},\qquad \mathcal{L}_{aux} = \alpha\cdot E\cdot \sum_{i=1}^{E} f_i\, P_i$$为什么不干脆用 \(\sum_i P_i^2\),它本身就可导?因为 \(P\) 是「平均概率」,而真正决定负载的是 argmax/top-k 的计数。路由器完全可以让每个 token 的概率分布都接近均匀(\(P_i\approx 1/E\)),但在 top-k 时总是同一个专家以微弱优势胜出——\(\sum P_i^2\) 很小,负载却完全坍缩。这正是 Shazeer 2017 区分 importance(概率和)和 load(实际分到的样本数)的原因。\(f_iP_i\) 的写法把真实负载 \(f\) 作为「权重」,梯度只推 \(P\),两者兼顾。
这就是 Switch Transformer 的负载均衡损失(GShard 里写作 \(\frac1E\sum_e \frac{c_e}{S} m_e\),是同一个东西差一个常数)。乘 \(E\) 是为了让完全均衡时损失恒为 \(\alpha\),不随专家数变化:\(E\cdot\sum_i \frac1E\cdot\frac1E = 1\)。Switch 用 \(\alpha = 10^{-2}\),并说在 \(10^{-1}\) 到 \(10^{-5}\) 之间扫过,\(10^{-2}\) 既能很快均衡又不干扰主损失。
梯度往哪推
\(f\) 当作常数(它没有梯度),只有 \(P\) 传梯度。对 softmax 打分 \(s_{\cdot,t}=\mathrm{softmax}(H_{t,\cdot})\),利用 \(\partial s_i/\partial H_k = s_i(\delta_{ik}-s_k)\):
$$\frac{\partial}{\partial H_{t,k}}\sum_i f_i P_i = \frac{1}{T}\sum_i f_i\, s_{i,t}(\delta_{ik} - s_{k,t}) = \frac{1}{T}\, s_{k,t}\Big(f_k - \sum_i f_i s_{i,t}\Big)$$读法:对每个 token,括号里是「专家 \(k\) 的负载」减去「这个 token 的分数加权的平均负载」。负载高于平均的专家,其 logit 被往下推;低于平均的被往上推,推的力度与该 token 给它的分数 \(s_{k,t}\) 成正比。这就是一个平滑版的「谁忙就压谁」。
举个小例子:\(E=4\),这一批负载 \(f=(0.55, 0.25, 0.10, 0.10)\),某个 token 的分数 \(s=(0.4, 0.3, 0.2, 0.1)\)。加权平均负载 \(\sum_i f_i s_i = 0.22+0.075+0.02+0.01=0.325\)。对四个 logit 的梯度(忽略 \(1/T\))分别是 \(0.4\times0.225=0.090\)、\(0.3\times(-0.075)=-0.0225\)、\(0.2\times(-0.225)=-0.045\)、\(0.1\times(-0.225)=-0.0225\)。梯度下降减去它们:专家 1 的 logit 被压低,其余三个被抬高,而且专家 3 比专家 4 抬得多——因为这个 token 本来就更偏向专家 3,把它「让」给专家 3 的代价最小。
DeepSeek-V3 用 sigmoid 打分、\(P_i\) 用归一化后的 \(s'_{i,t}=s_{i,t}/\sum_j s_{j,t}\)。同样推一下:\(\partial \log s'_i/\partial H_k = \delta_{ik}(1-s_i) - s'_k(1-s_k)\),于是
$$\frac{\partial}{\partial H_{t,k}}\sum_i f_i P_i = \frac{1}{T}\, s'_{k,t}\,(1-s_{k,t})\Big(f_k - \sum_i f_i s'_{i,t}\Big),$$结构和 softmax 版本一样,只是多了 sigmoid 的饱和因子 \(1-s_{k,t}\):分数已经接近 1 的专家,logit 的梯度很小。下面的模拟器两种都按这两个式子实现。
注意副作用:这个梯度直接改的是 logits,也就改了门控值 \(g\),而 \(g\) 是乘在专家输出上的——均衡的压力和语言建模的目标在同一组参数上拉扯。\(\alpha\) 太大,模型质量下降;太小,均衡不住。这就是负载均衡与模型性能之间的两难(dilemma),也是下面无辅助损失方法要解决的问题。
各家的变体
- Shazeer 2017:两个损失,importance loss \(w\cdot \mathrm{CV}(\mathrm{Importance}(X))^2\)(Importance 是每个专家的门控值之和,CV 是变异系数),外加一个基于噪声的平滑 load 估计的 \(\mathrm{CV}^2\) 损失;因为只均衡 importance 时,可能「一个专家收到少量大权重的样本、另一个收到大量小权重的样本」。
- DeepSeekMoE / V2:专家级 \(\mathcal{L}_{ExpBal}=\alpha_1\sum_i f_iP_i\),其中 \(f_i = \frac{N'}{K'T}\sum_t \mathbb{1}(t\ \text{选}\ i)\)(\(N'\) 路由专家数、\(K'\) 激活的路由专家数),这样均衡时 \(f_i=1\)。另加设备级损失 \(\alpha_2\sum_d f'_dP'_d\):\(f'_d\) 是设备 \(d\) 上专家 \(f\) 的平均,\(P'_d\) 是设备上专家 \(P\) 的和。他们的思路是:为了算力均衡,只需要设备之间均衡,不必逐专家严格均衡;所以专家级系数设小(防坍缩),设备级系数设大。V2 又加了通信均衡损失 \(\alpha_3\sum_d f''_dP''_d\),其中 \(f''_d = \frac{D}{MT}\sum_t \mathbb{1}(t\ \text{发往设备}\ d)\),让每个设备接收的 token 数也接近 \(MT\)。
- DeepSeek-V3:主力换成下面的无辅助损失偏置,只保留一个极小(\(\alpha=0.0001\))的序列级平衡损失,在单条序列内防止极端不均衡:\(f_i\)、\(P_i\) 都只在一条序列的 \(T\) 个 token 上统计,且 \(P_i\) 用归一化后的 sigmoid 分数 \(s'_{i,t}=s_{i,t}/\sum_j s_{j,t}\)。
Router z-loss 与数值稳定
ST-MoE(Zoph et al., 2022)发现,在最大规模上,即使按 Switch 的建议把路由器放在 float32 里计算,训练仍然会不稳定。他们提出 router z-loss:
$$\mathcal{L}_z = \frac{1}{T}\sum_{t=1}^{T}\Big(\log\sum_{j=1}^{E} e^{H_{t,j}}\Big)^2$$它惩罚的是每个 token 路由 logits 的 log-sum-exp(softmax 的归一化常数的对数)的平方。梯度是
$$\frac{\partial \mathcal{L}_z}{\partial H_{t,k}} = \frac{2}{T}\,\mathrm{LSE}(H_{t,\cdot})\cdot \mathrm{softmax}(H_{t,\cdot})_k,$$只要 LSE 为正,所有 logits 都被往下压,且分数越大压得越多——效果就是把 logits 的整体尺度控制在小范围内。为什么这有用?因为 bfloat16 只有 7 位尾数,数越大舍入误差越大,而指数函数会把小的输入误差放大。ST-MoE 给的例子:10 个 logit 等于 128、一个等于 128.5,在 bfloat16 里 128.5 可能被舍入成 128,softmax 输出就从 \(\frac{1}{1+10e^{-0.5}}\approx 0.142\) 变成 \(\frac{1}{11}\approx 0.091\)。他们也解释了为什么不直接裁剪 logits:裁剪发生在舍入之后,本身就是一种更大的不连续。
总损失写成 \(\mathcal{L} = \mathcal{L}_{CE} + c_B\mathcal{L}_B + c_z\mathcal{L}_z\),他们扫参后取 \(c_z = 0.001\),在他们的实验里稳定了全部 3 次运行且质量没有下降(而 Adafactor 的 update clipping 虽然也稳定,但质量掉得很厉害)。
细粒度专家与共享专家
前面的均衡讨论都默认「专家」是一个完整大小的 FFN。DeepSeekMoE(Dai et al., 2024)换了个角度:每个专家到底学得有多「专」?它提了两点改动,后来贯穿 DeepSeek-V2、V3。
细粒度切分(fine-grained expert segmentation)
如果专家数少,一个专家要接收各种各样的 token,被迫学很杂的知识。于是把每个专家的 FFN 中间维度切成 \(1/m\),专家数变成 \(mN\),同时激活数变成 \(mK\)。每个小专家有 \(3hf/m\) 个参数,激活 \(mK\) 个就是 \(3Khf\),和原来一样;总参数 \(mN\cdot 3hf/m = 3Nhf\) 也不变。变的是组合空间:一个 token 可以从更多种「专家组合」里挑出最适合自己的那一种,每个小专家因此可以只负责更窄的一类知识。例如\(N=16\)、top-2 只有 \(\binom{16}{2}=120\) 种组合;每个专家切成 4 份后 top-8,有 \(\binom{64}{8}=4{,}426{,}165{,}368\) 种。
共享专家隔离(shared expert isolation)
不同专家往往都需要一些通用知识(语法、常见模式),于是每个都学一份,造成冗余。DeepSeekMoE 单独拿出 \(K_s\) 个共享专家,每个 token 必经、不经路由器;为保持计算量不变,路由专家的激活数相应减少 \(K_s\)。V3 的形式:
$$h'_t = u_t + \sum_{i=1}^{N_s}\mathrm{FFN}^{(s)}_i(u_t) + \sum_{i=1}^{N_r} g_{i,t}\,\mathrm{FFN}^{(r)}_i(u_t)$$类似的结构更早在 DeepSpeed-MoE(Rajbhandari 等人)里出现过,那里主要出于工程考虑;DeepSeekMoE 是从「让路由专家只学差异化的知识」这个角度引入的。
几个真实模型的配置:
| 模型 | 共享 | 路由专家 | 每 token 激活 | 专家中间维度 | 总参 / 激活 |
|---|---|---|---|---|---|
| Mixtral 8x7B | 0 | 8 | 2 | 14336 | 47B / 13B |
| DeepSeek-V2 | 2 | 160 | 6 | 1536 | 236B / 21B |
| DeepSeek-V3 | 1 | 256 | 8 | 2048 | 671B / 37B |
Mixtral 每层都是 MoE;DeepSeek-V2 的第 1 层、V3 的前 3 层仍是稠密 FFN,其余层为 MoE。
无辅助损失的负载均衡
辅助损失的毛病在于:均衡的压力通过梯度作用在门控值上,和语言建模目标互相拉扯。DeepSeek 的 Wang et al.(2024)提出 Loss-Free Balancing,V3 把它作为主要的均衡手段。思路是:既然辅助损失的问题在于它通过梯度改动了门控值,那就不走梯度,直接给每个专家一个偏置 \(b_i\),只用于选择:
$$g'_{i,t} = \begin{cases} s_{i,t}, & s_{i,t} + b_i \in \mathrm{Topk}\big(\{s_{j,t}+b_j\}_{j},\,K\big) \\ 0, & \text{otherwise}\end{cases}$$关键是:门控值仍然用原始的 \(s_{i,t}\),偏置只影响谁进 top-k。偏置的更新规则:每个 batch 训练完,统计每个专家分到的 token 数 \(c_i\) 及其平均 \(\bar c\),
$$e_i = \bar c - c_i,\qquad b_i \leftarrow b_i + u\cdot\mathrm{sign}(e_i)$$超载的专家偏置减小、欠载的增大,每步固定走 \(u\)。Loss-Free 的实验对比了 \(u=0.0001\)(早期收敛慢)、\(0.001\)、\(0.01\)(后期偏置抖动、均衡变差),选 \(0.001\);也试过 \(b_i \leftarrow b_i + u\,e_i\) 和乘性偏置,均衡略好或相近,但困惑度没有更好,所以保留 sign 版本。V3 中把这个步长叫 bias update speed \(\gamma\),前 14.3T token 用 \(0.001\),最后 500B token 设为 0。
可以把它看成一个简单的反馈控制器:目标是「每个专家的负载等于平均」,\(b_i\) 是控制量,sign 更新相当于固定步长的积分控制——负载持续偏高,偏置就持续下降,直到稳定在一个能抵消该专家「天然受欢迎程度」的值附近,然后在附近以 \(\pm u\) 抖动。也可以把 \(b_i\) 看成约束 \(c_i \le \bar c\) 的对偶变量(拉格朗日乘子),sign 更新就是对偶上升的一种粗糙版本。这个视角能解释上面调步长的实验:\(u\) 太小,收敛慢;\(u\) 太大,稳态抖动大。还有一点:偏置是加在分数上的,它的合适量级取决于分数的尺度。sigmoid 分数在 \((0,1)\) 上分布较开,0.001 量级的步长足够;如果是 256 个专家的 softmax,分数本身只有 \(10^{-3}\) 量级,同样的步长意义完全不同(在下面的模拟器里切到 softmax 也能看到偏置法收敛变慢)。
两个细节值得注意:
- 用的是上一个 batch 的负载统计,而不是当前序列的——用当前序列会让一个 token 的路由依赖同序列后面的 token,泄露未来信息(这正是 Expert Choice 的问题)。
- 它是 batch 级的均衡,比序列级约束更宽松:允许某个领域的序列集中用某些专家。V3 报告观察到无辅助损失的模型专家在不同领域上的专精更明显;他们还验证了一个 batch 级的辅助损失在达到相近的 batch 级均衡时,验证 loss 和无辅助损失方法相同(1B 模型 2.253 对 2.253,序列级辅助损失为 2.258)。batch 级均衡的风险是单条序列或小 batch 内不均衡、以及推理时的领域偏移,前者靠大规模 EP+DP 带来的大 micro-batch 缓解,后者靠推理时部署冗余专家。
衡量均衡程度可以用 MaxVio:\(\mathrm{MaxVio} = \frac{\max_i \mathrm{Load}_i - \overline{\mathrm{Load}}}{\overline{\mathrm{Load}}}\),即最忙的专家比平均超出多少。实验里 1B / 3B 模型上 Loss-Free 的全局 MaxVio 为 0.04,辅助损失方法分别为 0.72 / 0.52,同时困惑度更低。
演示:路由器模拟器
这个路由器很小:\(T\) 个 token 的隐状态是随机向量,\(E\) 个专家各有一个随机质心,亲和度 logits 是两者的内积,再加上一个「专家受欢迎程度」偏斜项(拖 skew 让前面的专家天然更受欢迎,模拟坍缩的早期)。上半部分是这一批的路由结果:每个专家的负载柱、容量线和被丢掉的分配。下半部分可以选一种均衡方法跑若干「训练步」(每步换一批新 token):
- 辅助损失:给每个专家一个可学习的 logit 偏移 \(\theta_i\),按上面推出的梯度 \(\frac{1}{T}\sum_t s_{k,t}(f_k-\sum_i f_i s_{i,t})\)(sigmoid 时用对应的式子)做梯度下降。它会改变门控值。
- 无辅助损失偏置:\(b_i \leftarrow b_i + \gamma\,\mathrm{sign}(\bar c - c_i)\),只影响选择,门控值不变。
token 和专家质心是随机生成的(示意);容量、丢弃数、\(\sum f_iP_i\)、z-loss、MaxVio、梯度和偏置更新都按上文的公式计算。演示里只跑几百步,远少于真实训练,所以 \(\gamma\) 默认取得比 V3 的 0.001 大。
左:每步 batch 的 MaxVio(紫)与「门控值偏移」(橙,\(\frac1T\sum_t\sum_i |s_{i,t}(\theta)-s_{i,t}(0)|\),衡量均衡手段改了多少门控分数)。右:每个专家当前的 \(b_i\)(偏置法)或 \(\theta_i\)(辅助损失法)。
可以试试:把 skew 拉到 2 以上、选「不均衡」看 MaxVio 很高、丢弃很多;切到「无辅助损失偏置」跑 200 步,MaxVio 降下来而橙线一直是 0;切到「辅助损失」也能降下来,但橙线随之上升——它是靠改门控分数实现均衡的。再把 CF 调到 1.0 附近,即使平均均衡,随机波动也会造成丢弃;选 dropless 则不丢,但最忙专家的负载(决定这一步的时间)就是 MaxVio 所描述的那部分开销。
专家并行:dispatch 与 combine
专家多了以后,所有专家的参数放不进一张卡,而且前面算过,单卡上每个专家分到的 token 太少。两个问题有同一个解法:把专家分到不同的卡上,再把各卡的 token 汇集到专家所在的卡,这就是专家并行(Expert Parallelism, EP)。因为各专家的 FFN 完全独立,EP 比 TP 轻得多:不需要切矩阵乘,只需要把 token 的隐状态送到对的专家那里,算完再送回来。
设 EP 组有 \(P\) 张卡,每卡放 \(E/P\) 个专家。注意力等非 MoE 部分在这 \(P\) 张卡上是数据并行的:每张卡处理自己那份 token。于是一个 MoE 层的流程是:
- 路由:每张卡对本地 \(T\) 个 token 算路由,得到 \(T\cdot k\) 个 (token, 专家) 分配。
- permute:按目标专家(从而按目标卡)排序,同一目标卡的数据在内存里连续。
- dispatch(all-to-all):每张卡把发往卡 \(j\) 的那段发给卡 \(j\)。之后每张卡持有所有卡发来的、属于自己专家的 token。
- 专家计算:本地 \(E/P\) 个专家各自处理收到的变长批次(grouped GEMM)。
- combine(all-to-all):把结果按原路发回 token 所在的卡。
- unpermute + 加权求和:恢复原顺序,乘门控值,把 \(k\) 个专家的输出加起来,再加残差。
all-to-all 是这里唯一的新通信原语。\(P\) 张卡,每张卡把自己的数据切成 \(P\) 块,第 \(j\) 块发给卡 \(j\);同时从每张卡各收一块。把「卡 \(i\) 要发给卡 \(j\) 的那块」记作 \(X_{ij}\),排成一个 \(P\times P\) 的方阵:发之前卡 \(i\) 持有第 \(i\) 行,发之后卡 \(j\) 持有第 \(j\) 列——all-to-all 就是一次分块矩阵转置。用分片的记号写,就是把一个下标的分片轴挪到另一个下标上:\(A[I_X, J]\to A[I, J_X]\)。在 MoE 里,\(I\) 是 token、\(J\) 是专家:dispatch 之前按 token 分片(每卡一份 token),之后按专家分片(每卡一组专家)。
带容量的实现(GShard、Switch)写得更「张量化」:路由先产生一个形状为 \([\text{token}, E, C]\) 的 0/1 派发张量(token \(t\) 去专家 \(e\) 的第 \(c\) 个槽位),和输入做一次 einsum 得到 \([E, C, h]\) 的专家输入,然后用 all-to-all 把分片维从 token 换成专家;combine 是反过来乘一个带门控值的 \([\text{token}, E, C]\) 张量。每块的大小因此是固定的,这正是容量存在的工程原因。
4 张卡、每卡 2 个专家、top-1,点「下一步」看一个 MoE 层的数据怎么走:
写成代码是什么样
在 PyTorch 里,一个最朴素的 EP 前向大致如下(省略共享专家和数值细节)。注意 dispatch 之前多了一次小的 all-to-all:每张卡发往每个目标卡的 token 数是动态的,接收方必须先知道每段有多长才能分配缓冲区。
# 每张卡:U [T, h],本卡持有专家 [r*E_loc, (r+1)*E_loc)
idx, w = router(U) # [T, k], [T, k]
flat = idx.flatten() # T*k 个分配
order = flat.argsort() # 按专家(也就按目标卡)排序
X = U[order // k] # permute: [T*k, h]
send_cnt = bincount(flat // E_loc, P) # 发往每张卡的数量 [P]
recv_cnt = all_to_all(send_cnt) # 先交换计数(小)
Xr = all_to_all_v(X, send_cnt, recv_cnt) # dispatch: [sum(recv_cnt), h]
Yr = grouped_ffn(Xr, tokens_per_local_expert) # 本卡 E_loc 个专家
Y = all_to_all_v(Yr, recv_cnt, send_cnt) # combine: 原路返回 [T*k, h]
out = zeros_like(U).index_add_(0, order // k, Y * w.flatten()[order, None])
反向时自动微分会把两次 all_to_all_v 反过来各做一次:combine 的反向是把输出梯度按 dispatch 的方向发出去,dispatch 的反向是把输入梯度按 combine 的方向发回来。所以每个 MoE 层每个训练步共 4 次大的 all-to-all,它们都在关键路径上:下一层的计算要等这一层的 combine 完成。这就是为什么 DeepSeek 要把一个 micro-batch 的 all-to-all 和另一个 micro-batch 的计算重叠(DualPipe 里一对前向/反向块,推理里两个 micro-batch 交叠),单个 micro-batch 内部很难藏。
还有一个容易忽略的点:同一张卡上,路由到本卡专家的那部分分配其实不用经过网络,all_to_all_v 会把它当成本地拷贝;而一个 token 选了同一张远端卡上的两个专家时,朴素实现会发两份,更好的实现只发一份、在接收端复制——V3 的「每个目标节点只过一次 IB」是同一思路在节点层面的版本。
通信量与节点受限路由
每层传多少字节
设每卡本地 \(T\) 个 token、隐维 \(h\)、每元素 \(b\) 字节。dispatch 时每张卡要发出 \(T\cdot k\) 份 \(h\) 维向量(一个 token 选了 \(k\) 个专家就要发 \(k\) 份,除非落在同一张卡上可以去重)。路由均匀时,目标在其他卡上的比例是 \((P-1)/P\):
$$V_{dispatch} \approx T\cdot k\cdot h\cdot b\cdot\frac{P-1}{P}\ \text{bytes / GPU},\qquad V_{combine}\ \text{同量级}$$前向一次 dispatch 一次 combine,反向再各一次(梯度沿相反方向走),所以每个 MoE 层每步共 4 次 all-to-all。拿 DeepSeek-V3 的形状感受一下:\(h=7168\)、\(k=8\),每个 token 每层 dispatch 约 \(8\times7168 = 57{,}344\) 个数——这比 PP 在级边界上每 token 传一份 \(h\) 维向量多了一个量级,而且每一个 MoE 层都要传。
和计算比呢?专家计算每个分配 \(6\,h\,d_e\) FLOPs(SwiGLU,\(d_e\) 是专家中间维度)。在节点间带宽 \(BW\) 上传 \(k h b\) 字节需要 \(khb/BW\),计算 \(k\cdot 6hd_e/F\),比值
$$\frac{t_{comm}}{t_{comp}} \approx \frac{b\cdot F}{6\,d_e\cdot BW}\quad(\text{每份副本都跨节点时})$$与 \(h\)、\(k\)、\(T\) 都无关,只看专家有多「细」(\(d_e\) 越小越吃亏)和算力/带宽比。细粒度专家(\(d_e=2048\))让这个比值变差,这就是 DeepSeek-V3 报告说跨节点 EP 让计算:通信接近 1:1 的原因。
放在环形网络(比如 TPU 的环面)上看,all-to-all 比同样数据量的 all-gather 便宜得多:all-gather 里每一份数据都要送到所有卡,绕环一整圈;all-to-all 里每一小块只去一个目的地,在双向环上平均只走 \(N/4\) 跳,总代价约是 all-gather 的 1/4。还要注意,这两次 all-to-all 只在 token 的分片轴和专家的分片轴是同一组卡时才出现,这正是下文「EP 与 DP 共用卡」的情形。
设备受限与节点受限路由
细粒度专家让 \(k\) 变大,一个 token 的 \(k\) 个专家可能散落在很多卡、很多节点上。DeepSeek-V2 的设备受限路由:对每个 token,先选出包含最高亲和度专家的 \(M\) 个设备,再只在这些设备的专家里做 top-k;他们发现 \(M\ge 3\) 时效果与不受限的 top-k 大致相当,V2 用 \(M=3\)。
DeepSeek-V3 换成节点受限路由:每个 token 最多发往 \(M=4\) 个节点,节点的选择依据是「该节点上专家的最高 \(K_r/M\) 个亲和度之和」。配合的通信实现是:
- 一个 token 先经 IB 发到每个目标节点上与自己同号的那张卡(每个目标节点只发一次,不管该节点上选了几个专家),到达后再经 NVLink 转发给真正持有专家的卡;
- NVLink 160 GB/s 约是 IB 50 GB/s 的 3.2 倍。一份数据过一次 IB 的时间里,NVLink 可以转发约 3.2 份,所以只要每个 token 在每个目标节点上平均选不超过 3.2 个专家,NVLink 转发就不会成为瓶颈,IB 和 NVLink 的传输可以完全重叠。于是在同样的通信成本下,每个 token 最多能选 \(4\times3.2\approx 13\) 个专家(V3 实际用 8 个);
- dispatch/combine 内核用 warp specialization 把 20 个 SM 分成 10 个通信通道,分别处理 IB 发送、IB→NVLink 转发、NVLink 接收;只用 20 个 SM 就能吃满 IB 和 NVLink 带宽。DeepSeek 后来开源的 DeepEP 就是这类 EP all-to-all 内核的库。
这样跨节点流量的上界从「每 token \(k\) 份」变成「每 token 至多 \(M\) 份」。下面的计算器把这两种情况都算出来。
演示:EP all-to-all 成本计算器
估算一个 MoE 层前向的 dispatch + combine 时间和专家计算时间。模型:路由均匀随机;「节点受限 + 每节点去重」时,每个 token 跨 IB 的份数取「\(k\) 个专家落在的不同远端节点数」的期望(并截断到 \(M\));NVLink 上按每份分配都转发一次计。时间取 IB 与 NVLink 中较慢者(假设二者重叠,如 V3 的做法),计算只算路由专家与共享专家的矩阵乘。结果只用来看量级。
每节点 8 卡。「朴素」指不做节点限制、也不按节点去重(每份分配单独过 IB)。有效算力默认 800 TFLOPs,是一个假设的 FP8 矩阵乘可达算力;改它看比值怎么变。
默认参数下(V3 形状、64 路 EP、M=4、IB 50 GB/s)可以看到:朴素方案的 IB 时间约是专家计算的 3 倍;节点受限 + 去重把每个 token 的 IB 份数从 7 份压到 3.5 份,通信时间仍与计算同量级。这就是 DeepSeek-V3 说的「计算:通信约 1:1」,也是 DualPipe 要专门安排计算和通信重叠的原因。把 \(d_e\) 拉大(粗专家),比值迅速下降;切到 Mixtral 形状(单节点 8 路 EP)则完全没有 IB 流量。
EP 与 DP、TP、PP 的组合
只用 EP 有个问题:EP 只作用在 MoE 层,注意力等其他层没被切分。如果 \(P\) 张卡只是 EP 而在非 MoE 部分处理同一批 token,那就是重复计算。所以 EP 几乎总是和数据并行共用一组卡:非 MoE 部分这组卡是 DP(各自处理不同 token),MoE 部分同一组卡变成 EP。这也是 MoE 最早的分布式做法(Shazeer 等人,2017):普通层和路由器按常规数据并行,每个专家只保留一份,收到的是所有数据并行副本里路由到它的 token 的合集。这样顺带解决了开头的 shrinking batch:每个专家的有效 batch 从单卡的 \(kT/E\) 变成 \(kTP/E\),随 EP 规模增长。举个数:\(T=4096\)、\(k=8\)、\(E=256\),单卡上每个专家只有 128 行;64 路 EP 后是 8192 行,矩阵乘终于「喂饱」了。也正因为 EP 在处理输入的方式上和 DP 一样,有些框架干脆把 EP 当成 DP 的一个子维度来实现。
于是梯度同步也要分开看:
- 非专家参数(注意力、路由器、共享专家、embedding)在整个 DP 组上 all-reduce;
- 专家参数只有在持有同一专家的副本之间才需要同步。如果 DP 度是 \(D\)、EP 度是 \(P\)(\(P\mid D\)),每个专家有 \(D/P\) 个副本,梯度只在这 \(D/P\) 张卡之间 all-reduce。例如 \(D=128\)、\(P=64\):注意力的梯度在 128 张卡上 all-reduce,而每个专家的梯度只在 2 张卡之间同步。
和其他维度:
- TP:可以在专家内部再做 TP(expert TP,把每个专家的 \(d_e\) 切开),代价是每个专家的 GEMM 变得更小更碎;细粒度专家本来就小,所以 DeepSeek-V3 干脆不用 TP。一般而言,EP 只管 MoE 层、TP 和 CP 主要管注意力等其他部分,它们互补,可以叠加使用。
- PP:MoE 层按层分到不同流水级,每级内部再做 EP。DeepSeek-V3:16 路 PP × 64 路 EP(跨 8 个节点)× ZeRO-1 DP,2048 张 H800;V2 是 16 路 zero-bubble PP × 8 路 EP × ZeRO-1 DP。
- 拓扑:all-to-all 对带宽敏感,EP 组优先放在高带宽域内;放不下时(V3 的 64 路跨 8 节点)就需要节点受限路由和精心写的 kernel,把跨节点流量压到最少。
负载不均在 EP 下的真实代价
单卡时负载不均只是浪费 padding;EP 下它直接变成等待。一层 MoE 的专家计算时间由最忙的那张卡决定,其他卡算完就空等下一次 all-to-all。若某张卡的负载是平均的 \(1+\mathrm{MaxVio}\) 倍,这一层专家计算的效率上限就是
$$\eta = \frac{\overline{\text{load}}}{\max \text{load}} = \frac{1}{1+\mathrm{MaxVio}}.$$MaxVio = 0.5 意味着专家部分只有 67% 的效率;all-to-all 也一样,接收最多的卡决定传输完成的时间(这就是 V2 那个「通信均衡损失」要管接收端的原因)。另外,接收缓冲区的大小也由最坏情况决定:dropless 时一张卡可能收到远超平均的 token,显存必须按峰值预留,或者用动态分配。卡越多,batch 越大,统计上越容易均衡;这也是 V3 说 batch 级均衡的缺点「被大规模 EP + DP 带来的大 micro-batch 天然缓解」的意思。
低精度通信
dispatch 的数据量和精度成正比。DeepSeek-V3 把 MoE up-projection 之前的激活量化为 FP8 再 dispatch(与 up-projection 的 FP8 前向计算正好衔接,缩放因子取 2 的整数次幂),反向时 down-projection 之前的激活梯度也同样处理;而前向和反向的 combine 都保留 BF16,以保护训练精度的关键部分。所以 dispatch 每元素 1 字节、combine 2 字节——计算器里默认就是这个设置。
专家计算:padding、block-sparse 与 grouped GEMM
dispatch 之后,每张卡上有 \(E/P\) 个专家,每个专家拿到的 token 数不同(比如 37、0、210、96……)。怎么算?
- 逐专家循环:对每个专家调一次矩阵乘。简单,但专家多、每个专家的 token 少时,kernel 启动开销和小 GEMM 的低利用率很要命。
- padding 到容量:每个专家固定 \(C\) 个槽,堆成 \([E_{local}, C, h]\) 的批量矩阵乘(bmm)。形状静态、很快,但溢出要丢、空槽白算——这就是 CF 那套逻辑的由来。
- block-sparse(MegaBlocks):把「所有专家的 FFN」看成一个块对角稀疏矩阵乘,token 数是 block 大小的倍数即可,不丢 token,也几乎不 padding。端到端比基于 Tutel 的 MoE 训练最多快 40%。
- grouped GEMM:一次 kernel 调用里做一组形状各不相同的 GEMM(每个专家一组 \([n_e, h]\times[h, d_e]\)),由 kernel 内部按 tile 调度。只需要知道每个专家的 token 数和偏移,这正是 permute 之后天然得到的。现在主流框架的 MoE 层大多走这条路。
SwiGLU 专家的一次前向在 grouped GEMM 下是三次分组矩阵乘:gate 和 up(可以合并为一个 \([h, 2d_e]\) 的分组 GEMM),逐元素 SiLU·乘,再一次 down。反向同样是分组 GEMM,对输入和对权重各一组——和流水线那篇讲的 B/W 拆分对得上。
推理部署中的 EP(DeepSeek-V3)
推理部署把「均衡」和「每个专家的 batch」这两个问题放大得更明显:推理时 batch 由线上流量决定,没法靠调超参解决。DeepSeek-V3 的部署方案是个好例子:
- prefill:最小部署单元 4 节点 32 卡。注意力用 TP4 + SP,配合 DP8;MoE 部分用 EP32,保证每个专家处理的 batch 足够大。all-to-all 沿用训练时的做法(先 IB 跨节点,再 NVLink 节点内转发)。
- 冗余专家:为了每张卡处理的 token 数大致相同,把高负载专家复制部署。高负载专家根据线上统计周期性(例如每 10 分钟)调整,然后在节点内重新排布专家,尽量在不增加跨节点 all-to-all 的前提下均衡各卡负载。prefill 阶段设 32 个冗余专家,每卡除原有 8 个专家外再放 1 个冗余专家。
- decode:把共享专家当作一个总被选中的「重负载」路由专家,于是每个 token 选 9 个专家。最小单元 40 节点 320 卡:注意力 TP4 + SP、DP80,MoE 用 EP320,每卡只放一个专家,其中 64 张卡负责冗余专家和共享专家;dispatch/combine 用 IB 上的直接点对点传输以降低延迟。
- 两个 micro-batch 交叠:prefill 时同时处理两个计算量相近的 micro-batch,一个的注意力和 MoE 与另一个的 dispatch/combine 重叠——和训练里 DualPipe 的块内重叠是同一个想法。
报告还提到在探索「动态冗余」:每卡放更多专家(例如 16 个),每步只激活其中 9 个,在每层 all-to-all 之前即时计算全局最优的路由方案。这是一种在部署侧而不是训练侧解决均衡问题的思路。
总参数、激活参数与 FLOPs
训练的 FLOPs 只和激活参数有关:每 token 约 \(6N_{active}\)(前向 2、反向 4),再加注意力的序列相关项。显存则和总参数有关。用 V3 的形状算一下,看看 671B / 37B 是怎么来的:
- 每个专家:\(3\times7168\times2048 \approx 44.0\text{M}\) 参数。
- 每个 MoE 层的专家参数:\((256+1)\times 44.0\text{M}\approx 11.3\text{B}\);58 个 MoE 层(61 层减去前 3 个稠密层)共约 \(656\text{B}\)。
- 每 token 激活的专家参数:\((8+1)\times44.0\text{M}\times58 \approx 23\text{B}\)。
- 其余部分每个 token 都要用:MLA 注意力每层约 0.19B、61 层约 11.4B;3 个稠密 FFN(中间维 18432)约 1.2B;embedding 和输出层各约 0.93B(词表 129280)。合计约 14.5B。
- 总参数 \(656+14.5\approx 671\text{B}\);激活参数 \(23+14.5\approx 37\text{B}\)。专家参数占了总量的 98%,却只占激活量的 60%。
Mixtral 同理:\(32\times 8\times 3\times4096\times14336\approx 45\text{B}\) 专家参数,激活 2 个约 11.3B,加注意力约 1.3B 和 embedding 约 0.26B,总 47B、激活 13B。
推理(decode)时 MoE 的账反过来更难看。设一步有 \(B\) 个 token,看一层专家的一个权重矩阵:\(E\) 个专家各一份 \(h\times d_e\),int8 下要读 \(E h d_e\) 字节;而每个 token 只用 \(k\) 个专家,计算量是 \(2kBhd_e\) FLOPs。想要算力受限,算术强度得超过硬件的算力/带宽比,以 TPU v5e(约 240 FLOPs/字节)为例:
$$\frac{2kBhd_e}{Ehd_e} > 240 \;\Rightarrow\; B > 120\cdot\frac{E}{k}$$对 DeepSeek-V3 的 \(E=256, k=8\),要 \(B > 3840\) 个 token。直观地说:每一步都要把所有专家的权重读一遍,但每个专家只服务 \(k/E\) 的 token,所以 batch 要比稠密模型大 \(E/k\) 倍才能摊平读权重的成本。这就是 MoE 推理特别依赖大 batch 和大规模 EP 的原因。
其他稳定训练的技巧
- 选择性精度(Switch):只把路由器的输入和计算放到 float32,算完的 dispatch/combine 张量再转回 bfloat16,这样 all-to-all 不必传 float32。Switch 报告纯 bfloat16 的模型发散了,而选择性精度在速度几乎不变的情况下恢复了 float32 的稳定性。
- 更小的初始化(Switch):把权重初始化尺度 \(s\) 从 1.0 降到 0.1(截断正态 \(\sigma=\sqrt{s/n}\)),质量更好、方差更小。
- 专家 dropout(Switch 微调):参数远多于等 FLOPs 的稠密模型,在小的下游任务上更易过拟合,可以在专家内部用更高的 dropout。
- router z-loss(ST-MoE),以及它建议的把所有被指数化的张量放到 float32。
- 噪声(Shazeer 2017)与 随机路由(GShard):早期的均衡手段,现在较少用。
小结
- MoE 用路由器把每个 token 送到 \(k\) 个专家,参数涨 \(E\) 倍、计算只涨 \(k\) 倍;路由器的梯度只通过门控值流动。
- 容量 \(C=\mathrm{CF}\cdot Tk/E\) 换来静态形状,代价是丢 token 和 padding;block-sparse / grouped GEMM 让不丢 token 成为常态。
- 辅助损失 \(\alpha E\sum f_iP_i\) 用 \(P\) 代替不可导的 \(f\),梯度把高负载专家的 logit 往下推,但也扰动了门控值;z-loss 管 logits 的尺度。
- DeepSeek 路线:细粒度 + 共享专家提高专精度;只用于选择的偏置 \(b_i\) 做 batch 级均衡,只留极小的序列级损失兜底。
- EP 与 DP 共用卡,两次 all-to-all(前向)+ 两次(反向);通信/计算比约 \(bF/(6d_eBW)\),细专家跨节点时与计算同量级,靠节点受限路由、按节点去重、专用内核和 DualPipe 式重叠压下去。
参考与延伸阅读
想系统了解大规模训练里的各种并行,推荐 Ultra-Scale Playbook;想从 TPU 和分片的角度看 MoE 的通信与推理成本,推荐 Scaling Book 的相关章节。
- Ultra-Scale Playbook:Expert Parallelism 与 5D Parallelism in a Nutshell 两节
- Scaling Book:All the Transformer Math(Sparsity and Mixture-of-Experts,及第 8 题);Sharded Matrices(AllToAll);Training
- Shazeer et al., 2017. Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer
- Lepikhin et al., 2020. GShard(§2.2 Algorithm 1、§3.1 Algorithm 2)
- Fedus et al., 2021. Switch Transformers(§2.1–2.4、§5)
- Zoph et al., 2022. ST-MoE(router z-loss,§3)
- Gale et al., 2022. MegaBlocks
- Dai et al., 2024. DeepSeekMoE(§3)
- DeepSeek-AI, 2024. DeepSeek-V2(§2.2:设备受限路由、三种均衡损失、设备级丢 token)
- Wang et al., 2024. Auxiliary-Loss-Free Load Balancing Strategy for Mixture-of-Experts
- DeepSeek-AI, 2024. DeepSeek-V3 Technical Report(§2.1.2、§3.2、§4.2、§4.5.3)
- Jiang et al., 2024. Mixtral of Experts
- deepseek-ai/DeepEP