MoE环游记:4、难处应当多投入
By 苏剑林 | 2025-03-28 | 42171位读者 |前两篇文章我们都在讨论负载均衡,其中在《MoE环游记:3、换个思路来分配》介绍Loss-Free方案时,笔者留了一个悬念:它引入的Bias项有一个冗余的自由度,这个自由度可以用来做另外有趣的事情。这篇文章我们就来讨论这件事。
我们知道,MoE是为每个Token只选择最匹配的$k$个Expert来进行计算,从而在增大参数量的同时还节省了计算量。然而,当我们仔细思考就会发现,这个策略实际上有明显的可改进之处:直观来看,每个Token的难度并不一样,所以更合理的方案应该是难的Token分配更多的计算资源,简单的token分配更少的资源,这样或许能在同样有限的资源下将效果最大化。
而刚才提到的Bias的额外自由度,恰好可以用来简单地实现这个目标。
设计思想 #
首先,我们回顾一下,MoE的基本形式是
\begin{equation}\boldsymbol{y} = \sum_{i\in \mathop{\text{argtop}}_k \boldsymbol{\rho}} \rho_i \boldsymbol{e}_i\end{equation}
负载不均衡是MoE训练常见的问题,对此研究人员提出了Aux Loss,这部分工作我们介绍于《MoE环游记:2、不患寡而患不均》。此外,在《MoE环游记:3、换个思路来分配》我们介绍了DeepSeek提出的Loss-Free方案,它将MoE改为
\begin{equation}\boldsymbol{y} = \sum_{i\in \mathop{\text{argtop}}_k \boldsymbol{\rho} + \boldsymbol{b}} \rho_i \boldsymbol{e}_i\end{equation}
然后通过调节新引入的Bias项$\boldsymbol{b}$来实现负载均衡。为了实现每个Token可以选择动态数量的Expert,笔者提出的做法是将Loss-Free的形式稍微修改一下:
\begin{equation}\boldsymbol{y} = \sum_{i\in \mathop{\text{argwhere}} \boldsymbol{\rho} + \boldsymbol{b} > 0} \rho_i \boldsymbol{e}_i\end{equation}
即只要满足$\rho_i + b_i > 0$的Expert就被选中,这样每个Token选出的Expert数量自然是动态的,并且免除了排序的需求,某种程度上看还变得更简化了。
优化目标 #
$\boldsymbol{b}$的优化目标有两个:一是跟Loss-Free一样,要实现负载均匀;二是要控制每个Token被选中的平均Expert数为$k$,这我们可以称为预算控制,要不然直接$b_i = \infty$将所有Expert都选出来就行了,但这不是我们想要的。
负载均衡依然采样Loss-Free的训练方式。定义记号$\boldsymbol{f} = [f_1, f_2, \cdots, f_n]$
\begin{equation}f_i = \left\{\begin{aligned}1, \quad \rho_i + b_i > 0 \\
0, \quad \rho_i + b_i \leq 0\end{aligned}\right.\end{equation}
然后记$\tilde{\boldsymbol{F}}=\mathbb{E}[\boldsymbol{f}]$,那么$\boldsymbol{F} = \tilde{\boldsymbol{F}}/|\tilde{\boldsymbol{F}}|$就是当前Expert分布,其中$|\tilde{\boldsymbol{F}}|$是$\tilde{\boldsymbol{F}}$的各分量之和。Loss-Free提出的更新公式是:
\begin{equation}\boldsymbol{b}\leftarrow \boldsymbol{b} - \alpha \mathop{\text{sign}}(\boldsymbol{F} - \boldsymbol{Q})\label{eq:aux-loss-free}\end{equation}
其中$\boldsymbol{Q}=(1/n, 1/n, \cdots, 1/n)$是目标的均匀分布。我们提到多次,$\boldsymbol{b}$存在一个冗余的自由度,体现在对$\boldsymbol{b}$所有分量加上同一个常数,排序结果不变。这样一来,我们可以把更新规则$\eqref{eq:aux-loss-free}$改为
\begin{equation}\boldsymbol{b}\leftarrow \boldsymbol{b} - \alpha \left[\mathop{\text{sign}}(\boldsymbol{F} - \boldsymbol{Q}) - \overline{\mathop{\text{sign}}(\boldsymbol{F} - \boldsymbol{Q})}\right]\label{eq:aux-loss-free-2}\end{equation}
这里向量上面加一横代表该向量的全体分量的均值,是一个标量,向量减标量代表每个分量都减去这个标量。这样一来出来的$\boldsymbol{b}$必然满足$\overline{\boldsymbol{b}}=0$,但不改变负载均衡的效果。于是我们可以$\overline{\boldsymbol{b}}$这个自由度留给预算控制。
怎么理解呢?很明显,如果给全体$b_i$都加上同一个正数,那么满足$\rho_i + b_i > 0$的几率将会变大,从而总预算也会增大。所以做法很简单,先算出当前平均预算,不难发现正好是$|\tilde{\boldsymbol{F}}|$,如果它大于$k$,那么就调小一点$\boldsymbol{b}$,反之则增大。整合到式$\eqref{eq:aux-loss-free-2}$是
\begin{equation}\boldsymbol{b}\leftarrow \boldsymbol{b} - \alpha \left[\mathop{\text{sign}}(\boldsymbol{F} - \boldsymbol{Q}) - \overline{\mathop{\text{sign}}(\boldsymbol{F} - \boldsymbol{Q})} + \mathop{\text{sign}}(|\tilde{\boldsymbol{F}}|- k)\right]\label{eq:aux-loss-free-3}\end{equation}
如果只想保证预算不超过$k$,而不非要等于$k$,那么可以改为当$|\tilde{\boldsymbol{F}}| < k$时不作改变
\begin{equation}\boldsymbol{b}\leftarrow \boldsymbol{b} - \alpha \left[\mathop{\text{sign}}(\boldsymbol{F} - \boldsymbol{Q}) - \overline{\mathop{\text{sign}}(\boldsymbol{F} - \boldsymbol{Q})} + \mathop{\text{sign}}(\max(|\tilde{\boldsymbol{F}}|- k,0))\right]\label{eq:aux-loss-free-4}\end{equation}
尝试简化 #
细细品味式$\eqref{eq:aux-loss-free-3}$,我们会发现它做了两件事,一是让$\boldsymbol{F}=\tilde{\boldsymbol{F}}/|\tilde{\boldsymbol{F}}|$逼近$\boldsymbol{Q}$,二是让$|\tilde{\boldsymbol{F}}|$逼近$k$。这看起来可以合并成一件事:让$\tilde{\boldsymbol{F}}$逼近$\tilde{\boldsymbol{Q}}=k\boldsymbol{Q}=(k/n,k/n,\cdots,k/n)$。于是式$\eqref{eq:aux-loss-free-3}$可以简化为
\begin{equation}\boldsymbol{b}\leftarrow \boldsymbol{b} - \alpha \mathop{\text{sign}}(\tilde{\boldsymbol{F}} - \tilde{\boldsymbol{Q}})\label{eq:aux-loss-free-5}\end{equation}
笔者将式$\eqref{eq:aux-loss-free-3}$和式$\eqref{eq:aux-loss-free-5}$都做了实验,发现它们在效果上大同小异,但是式$\eqref{eq:aux-loss-free-5}$的负载均衡和预算控制两个指标在训练前期的抖动都大很多,所以追求稳定性的读者可以优先考虑式$\eqref{eq:aux-loss-free-3}$或$\eqref{eq:aux-loss-free-4}$,追求简洁的读者则可以考虑式$\eqref{eq:aux-loss-free-5}$。
考虑到$\mathop{\text{sign}}$只保留了$\tilde{F}_i - \tilde{Q}_i$的符号而忽略了绝对值的大小,笔者也尝试RMS Norm替代$\mathop{\text{sign}}$:
\begin{equation}\boldsymbol{b}\leftarrow \boldsymbol{b} - \alpha (\tilde{\boldsymbol{F}} - \tilde{\boldsymbol{Q}})/\Vert\tilde{\boldsymbol{F}} - \tilde{\boldsymbol{Q}}\Vert_{RMS}\end{equation}
其中向量的$\Vert\cdot\Vert_{RMS}$是指分量的平方和的平方根。很明显$\mathop{\text{sign}}$的RMS是1,而RMS Norm之后RMS也为1,所以两者更新的数量级相同,可以用同一个$\alpha$。由于RMS Norm保留了$\tilde{F}_i - \tilde{Q}_i$的相对大小,可以做到误差小的更新也小,所以在波动程度上比$\mathop{\text{sign}}$略小,但也好得不多。
当然,用RMS Norm替换$\mathop{\text{sign}}$来增加稳定性是一个通用技巧,式$\eqref{eq:aux-loss-free}$、$\eqref{eq:aux-loss-free-2}$、$\eqref{eq:aux-loss-free-3}$或$\eqref{eq:aux-loss-free-4}$都可以做这样的替换,这就看个人审美了,总之只是略稳但不多。
初始方式 #
解决完$\boldsymbol{b}$的更新规则,我们来考虑$\boldsymbol{b}$的初始化,这是一个有意思但不算十分关键的问题。
按照常规做法,$\boldsymbol{b}$全零初始化且$\boldsymbol{\rho}$用Sigmoid激活,那么初始阶段会把$n$个Expert都选出来,明显超出$\leq k$的预算,这将会导致非常多的Token Drop。不过,如果我们没有强迫症的话,这并不是很严重的问题,因为模型其他参数通常会加Warmup但$\boldsymbol{b}$通常不加,所以在Warmup的前几步模型就会自动把这个问题解决了。
如果我们介意这一点,那么可以通过调整$\boldsymbol{b}$初始化来控制初始预算。假设Router的输入是$d$维向量,满足零均值、单位方差(有RMSNorm在,近似成立),Router的权重初始化方差为$\sigma^2$,那么Router的Logits近似为零均值、$\sigma^2 d$方差。有了这些数据,我们可以用正态近似模拟加二分法估算一个初始$\boldsymbol{b}$:
import numpy as np
def sigmoid(x):
return 1 / (1 + np.exp(-x))
def b_init(n, k, d, sigma, eps=0.1):
b1, b2 = -1, 0
std = sigma * d**0.5
logits = np.random.randn(10000, n) * std
scores = sigmoid(logits)
while True:
b = (b1 + b2) * 0.5
c = ((scores + b) > 0).sum(1).mean()
if -eps < c - k < eps:
return b
elif c > k:
b2 = b
else:
b1 = b
b_init(32, 4, 1024, 6e-3)代码中考虑的是Sigmoid激活,所以搜索区间是$[-1, 0]$,如果是其他激活函数请自行调整。不过这里的建议跟《MoE环游记:3、换个思路来分配》一文是相同的,即加$\boldsymbol{b}$的$\boldsymbol{\rho}$可以统一用Sigmoid激活,乘上Expert的$\boldsymbol{\rho}$才考虑用别的激活函数。
相关工作 #
这篇文章之前,已经有一些工作尝试过动态选择Expert数目的MoE设计,下面简单列举一些笔者搜到的工作,并从个人的审美角度做一些简单的评析。
比较朴素的做法是AdaMoE和MoE++,它们在Expert中混入了一些低计算成本的Expert,如空白Expert、复制Expert、常数Expert,同时也鼓励负载均衡,这样当Token选中这些简单Expert时,等价于少选择了其他标准的Expert,从而间接地实现了动态数目。这样做的好处是可以复用原本Top-$k$ MoE的基建,但同时也欠缺了一些灵活性。
另外一个朴素的想法是将Top-$k$选择改为Top-$p$,出自《Harder Tasks Need More Experts: Dynamic Routing in MoE Models》。这个转换看上去很自然,但实际上有颇多问题,比如无法准确控制平均预算,因为当$\boldsymbol{\rho}$接近均匀分布时Top-$p$的比例会非常大,所以原论文又新增了一项熵损失来让$\boldsymbol{\rho}$远离均匀分布。总的来说,个人感觉它引入的问题比收益更明显。
一个比较独特的做法是Ada-K Routing,它新增一个模块来预测要激活的Expert数,然后用强化学习来训练,这样做在原理上没问题,但引入强化学习无疑会增加训练复杂性。DA-MoE则利用Attention分数来识别重要Token,为其分配更多Expert,但感觉不够本质,因为“MoE”原则上不局限于FFN层,一旦用到Attention上,不就没有Attention分数可用了?
形式上跟本文做法最相似的可能是ReMoE,它同样是基于零阈值来选择Expert,但选择了Aux Loss的方式来实现负载均匀以及预算控制,同时又混合了手搓梯度的思想来控制Aux Loss权重,总体来看多了点糅合感。本文则延续了Loss-Free的思想,利用$\boldsymbol{b}$的额外自由度来调控这个阈值,从而以最小的改动实现了动态Expert数目。
文章小结 #
本文提出了一种动态选择Expert数目的MoE设计,主要思想是对Loss-Free的MoE形式稍作修改,然后调整Bias项的更新规则,利用它的额外自由度来同时实现负载均衡和预算控制。
转载到请包括本文地址:https://www.kexue.fm/archives/10815
更详细的转载事宜请参考:《科学空间FAQ》
如果您还有什么疑惑或建议,欢迎在下方评论区继续讨论。
如果您觉得本文还不错,欢迎分享/打赏本文。打赏并非要从中获得收益,而是希望知道科学空间获得了多少读者的真心关注。当然,如果你无视它,也不会影响你的阅读。再次表示欢迎和感谢!
如果您需要引用本文,请参考:
苏剑林. (Mar. 28, 2025). 《MoE环游记:4、难处应当多投入 》[Blog post]. Retrieved from https://www.kexue.fm/archives/10815
@online{kexuefm-10815,
title={MoE环游记:4、难处应当多投入},
author={苏剑林},
year={2025},
month={Mar},
url={\url{https://www.kexue.fm/archives/10815}},
}










April 1st, 2025
苏神可以看看我们被ICLR'25接收的DynMoE (\url{https://arxiv.org/abs/2405.14297}),支持动态路由(top_k)和最大专家数(max_k)
欢迎自荐!粗扫了一下,Dynamic选择方面似乎没什么启发性(当然也没啥问题),主要是感觉会不会Dynamic过度了?我看好像会自动新增和删除expert,实现的复杂性如何呢?
April 10th, 2025
请问,为什么公式(6)不会改变“负载均衡”的效果。
我理解现在选择expert的方式是$\rho_i + b_i > 0$,不再是$\rho_i + b_i$的相对大小,而是绝对大小。那么公式(6)对所有的$b_i$加上同一个标量后,不就破坏了负载均衡的更新方式(公式(5))了吗。
那又如何?负载均衡只取决于$\boldsymbol{\rho}+\boldsymbol{b}$的各分量的序,我们又没修改它的序。
但本文提出的负载均衡不是已经从依赖排序(argtop)变成了依赖绝对值(argwhere>0)吗
绝对值或者说均值,影响的是“预算”;“均衡”还是只受相对大小影响。
August 1st, 2025
请问公式6这里为什么需要减去sign(F−Q)的均值呢
将均值这个自由度留给预算控制
有点困惑,b的优化目标只有均衡负载和预算均值为k,减去$\overline{sign(F-Q)}$(0均值约束)的操作感觉是加了一个不相关的约束条件,甚至0均值约束和预算均值约束似乎还有点冲突。请问一下苏老师试过不加0均值约束的效果吗?
你是说公式$\eqref{eq:aux-loss-free-5}$?
公式$\eqref{eq:aux-loss-free-3}$。
嗷,理解苏老师您的意思了,不加0均值约束就是公式$\eqref{eq:aux-loss-free-5}$。感谢。
August 5th, 2025
苏老师,从muon is scalable那篇论文上看kimi似乎是用的式(6)? 如果是的话,想请教下为什么没有用式(7)或者其他dynamic k式(8,9,10)呢
后面的都是动态$k$的方案啊,K2是静态$k$。
谢谢!还有一个问题想请教下,假设有一部分token在dynamics k的时候没有被选中,shared experts应该直接处理所有token,还是应该只处理dynamic k当中被选中处理的token呢
shared expert跟routed expert是独立的,shared expert处理所有的token
你好,muon is scalable论文提交的https://github.com/NVIDIA/Megatron-LM/pull/1428 上是有关于式(6)的梯度更新代码吗 扫了一下似乎没有看到,请两位赐教了
我没有看过这个PR,但式6或者其他式都是基于loss free的,并没有梯度,所以应该不会出现在muon或者其他的optimizer的PR里面
谢谢回复了,意思是关于bias的参数训练优化求出的grad 这块好像没有看到比较权威的开源实现的
“关于bias的参数训练优化求出的grad”是啥意思
抱歉说的过于不专业,意思就是想看看loss-free的moe训练代码是哪里有的,目前是大致检索到在Megatron中有 先再仔细看看能不能找到代码对上苏神在blog中那些loss函数
loss-free属于训练部分,不在K2的开源范围内。但弄懂原理后实现应该不算难。
这个PR只是优化器PR。loss-free不在这里边
September 3rd, 2025
苏神你好,阅读了https://huggingface.co/moonshotai/Kimi-K2-Base/blob/main/modeling_deepseek.py的class MoE的源码,发现这里是先对专家分组,然后根据每组的专家中前2位分数之和作为该专家组分数,由此给专家组排序选择topk(=n_groups(=1,根据config.json))的专家组,然后对这些专家组内的所有专家分数进一步排序获取到topk(=num_experts_per_tok).
我是按照苏神你MoE环游记的论述阅读这份源码
1. 关于MoE-3换个思路来分配中的loss-free操作,我是知道对应这行,其实也对应着负载均衡的实现
"""
scores_for_choice = scores.view(bsz * seq_len, -1) + self.e_score_correction_bias.unsqueeze(0)
"""
2. 关于MoE-4的难处应该多投入这节中的预算控制思想,其中的每个token分配k个专家 我理解应该是config.json中的num_experts_per_tok参数吧,对应代码为
"""
self.top_k = num_experts_per_tok
_, topk_idx = torch.topk(
tmp_scores, k=self.top_k, dim=-1, sorted=False)
"""
3. 最后想问一下,modeling_deepseek.py代码对应的是推理阶段的MoE实现,如果想看训练阶段的MoE为负责均衡和预算控制的源码,应该看哪里的代码会比较权威,我目前准备是看一下官方Megatron的MoE训练框架 去找寻下是否有对应的代码
因为我发现有些MoE的训练实现好像都是魔改megatron比如下面这份MoE代码https://github.com/thu-ml/ReMoE
读源码的主要目的是希望 除了理论学习之外更熟悉工程上是怎么实现的 为了学习的质量所以比较希望找到权威高质量的源码实现 希望不吝赐教
关于3,大概检索到了这2处,阅读之后再与苏神交流
1. https://github1s.com/NVIDIA/Megatron-LM/blob/main/megatron/core/transformer/moe/router.py#L145-L146
2. https://github1s.com/NVIDIA/Megatron-LM/blob/main/megatron/core/transformer/transformer_config.py#L468
继续阅读这2份代码后,大概结论如下
1 megatron-lm应当只实现了经典的loss-free 也就是b ← b − α sign(F − Q);具体的代码可见
- def _update_router_expert_bias -> https://github1s.com/NVIDIA/Megatron-LM/blob/main/megatron/core/distributed/finalize_model_grads.py#L280
- def get_updated_expert_bias -> https://github1s.com/NVIDIA/Megatron-LM/blob/main/megatron/core/transformer/moe/moe_utils.py#L830
2 实现loss-free with budget应当是在当前Megatron-LM基础上应当是较为直接的,但细节在于魔鬼,确实需要对于分布式细节了解清楚才能bug-free改出;下面是个最初步的修改思路
- 定义budget_control参数
```python
#[class TopKRouter](https://github1s.com/NVIDIA/Megatron-LM/blob/main/megatron/core/transformer/moe/router.py#L146-L164)
self.enable_expert_bias = self.config.moe_router_enable_expert_bias
self.enable_expert_budget_control = self.config.moe_router_enable_expert_budget_control
if self.enable_expert_bias:
if self.enable_expert_budget_control:
self.register_buffer(
'local_expert_budget',
torch.zeros(
self.config.num_moe_experts,
dtype=torch.float32,
device=torch.cuda.current_device(),
),
persistent=False,
)
```
- expert_bias更新: 遵循 b←b−α[sign(F−Q)−sign(F−Q)+sign(max( F~ −k,0))]
```python
#[class TopKRouter](https://github1s.com/NVIDIA/Megatron-LM/blob/main/megatron/core/distributed/finalize_model_grads.py#L280)
def get_updated_expert_bias_with_budget_control(tokens_per_expert, expert_bias, expert_bias_update_rate):
```
噢,本文的dynamic-k思路,之前我没看到过完全相同实现的论文,所以如无意外应该属于新的,没有公开实现是正常的。至于怎么改,megatron我也不大懂,没法给参考意见,但我自己用jax写的话,其实dynamic-k比静态选top-k,代码还稍微简单一点。