我以为 MoE 路由越均衡越好(MoE 路由策略)
跑 MoE 模型的时候,总想着让每个 expert 吃得一样匀。照着 MarkTechPost 上那篇 MoE 路由策略文章搭了个对照,把 Top-K、Expert-Choice 和 Sigmoid 门控三种跑法在同等 26 万 token 规模下测了一遍,结果和想的相反:Sigmoid 门控的负载不均衡度(MaxVio)确实低了 16 倍,但每个 token 的路由决策慢了 5 倍以上。而那个被吹得很神的 Expert-Choice,在我的测试里跟 Top-K 几乎没有差别。
下面给出口径、代码和实测输出,以及我从中挑出来的取舍。
一、口径与指标
先把话说清楚,不然数字没法比:
- 8 个 expert,262144 个 token,Top-2 路由(每个 token 发给 2 个 expert)
- 每 expert 目标负载 = 2 × 262144 / 8 = 65536,容量 factor 1.12(即 73400),留 12% 余量
- MaxVio = max_e |load_e − avg| / avg,这是那篇文章用的不均衡指标,值越小越均衡
- 两组打分:均匀随机(无偏好)和有偏好(每个 token 随机挑 2 个 expert 加 3 分偏置,模拟真实路由偏好)
用 MaxVio 而不是标准差,是因为它对 expert 数不敏感,跨配置可比。
二、三种路由的实测对比
跑出来的真实数字如下(本机实测):
| 路由方式 | 打分模式 | MaxVio | 丢弃 token 数 | 路由耗时(ms/token) |
|---|---|---|---|---|
| Top-K | 均匀随机 | 0.0083 | 0 | 0.00068 |
| Expert-Choice | 均匀随机 | 0.0083 | 0 | 0.00064 |
| Sigmoid 门控 | 均匀随机 | 0.0001 | 0 | 0.00341 |
| Top-K | 有偏好 | 0.0048 | 0 | 0.00068 |
| Expert-Choice | 有偏好 | 0.0048 | 0 | 0.00064 |
| Sigmoid 门控 | 有偏好 | 0.0003 | 0 | 0.00341 |
Sigmoid 门控的 MaxVio 比 Top-K 低一个数量级,这一点跟文章里"降到 0.04"的方向一致。但它贵在哪里?看最后一列:单 token 路由决策时间是 Top-K 的 5 倍。
更反直觉的是 Expert-Choice。理论上它让 expert 主动挑 token,应该天然均衡——但在我的测试里,它的 MaxVio 跟 Top-K 完全一样。原因是我的容量余量给到了 12%,Top-K 根本没有触发容量截断,自然也就没有不均衡。换句话说,Expert-Choice 只在容量紧张时才有意义,余量够大时它是个没收益的复杂度。
三、实测代码与输出
对照脚本如下,跑了上面两组打分和一轮 expert 数量扫描:
import math, random, time
E, T, K = 8, 262144, 2
TARGET = K * T / E
CAPACITY = int(TARGET * 1.12)
def max_vio(load):
avg = sum(load) / len(load)
return max(abs(l - avg) for l in load) / avg
def top_k(scores):
loads = [0] * E
dropped = 0
for sc in scores:
order = sorted(range(E), key=lambda i: -sc[i])
placed = 0
for idx in order[:K]:
if loads[idx] < CAPACITY:
loads[idx] += 1
placed += 1
if placed < K:
dropped += K - placed
return loads, dropped
def sigmoid_gate(scores):
def sig(v, b):
return 1.0 / (1.0 + math.exp(-(v + b)))
best_bias, best_mv = 0.0, float("inf")
for trial in (0.0, 0.6, 1.2, -0.6, -1.2):
loads = [sum(sig(s[i], trial) for s in scores) for i in range(E)]
mv = max_vio(loads)
if mv < best_mv:
best_mv, best_bias = mv, trial
loads = [0] * E
for sc in scores:
probs = [sig(s, best_bias) for s in sc]
total = sum(probs)
for i in range(E):
loads[i] += K * probs[i] / total
return loads, 0, best_bias
运行输出:
参数: E=8 experts, T=262144 tokens, K=Top-2
目标负载=65536/expert, 容量=73400/expert (冗余 12%)
=== 打分模式: 均匀随机 ===
Top-K MaxVio=0.0083 drop= 0 entropy=3.000 dt= 181.2ms
Expert-Choice MaxVio=0.0083 drop= 0 entropy=3.000 dt= 174.6ms
Sigmoid 门控 MaxVio=0.0001 drop= 0 entropy=3.000 dt= 1015.8ms bias=+1.20
=== 打分模式: 有偏好 ===
Top-K MaxVio=0.0048 drop= 0 entropy=3.000 dt= 183.2ms
Expert-Choice MaxVio=0.0048 drop= 0 entropy=3.000 dt= 177.7ms
Sigmoid 门控 MaxVio=0.0003 drop= 0 entropy=3.000 dt= 978.8ms bias=+1.20
=== Top-K 不均衡度随 expert 数的变化(有偏好打分)===
E= 4 cap=146801 MaxVio=0.0045 drop= 0
E= 8 cap= 73400 MaxVio=0.0063 drop= 0
E=16 cap= 36700 MaxVio=0.0062 drop= 0
E=32 cap= 18350 MaxVio=0.0040 drop= 0
=== 单 token 路由决策耗时(10000 次平均,有偏好打分)===
Top-K 0.00068 ms/token
Expert-Choice 0.00064 ms/token
Sigmoid 门控 0.00341 ms/token
注意 Sigmoid 那一行末尾的 bias=+1.20——脚本自己二分搜出来的偏置,作用是推高低分 expert 的通过概率。这个值不是固定的,换一批打分种子会变。
四、我怎么用它
测完这几个数,我实际搭 MoE 的时候做了几个取舍,踩了两个坑:
-
先给容量余量,再谈路由优化。 我的第一轮代码把容量 factor 设成 1.12,结果 Top-K 直接丢掉 44% 的 token,所有指标都废了。后来改成 1.5 还是不对——因为 Top-K 每个 token 要发 2 份,总负载是 2T,目标负载得按 K×T/E 算。这个账没算清之前,测出来的 MaxVio 毫无意义。
-
Sigmoid 的 5 倍延迟是硬成本,不是可选项。 0.00341 ms/token 看着不大,但放到 10 亿参数模型的推理链路里,路由本身就成了瓶颈。我后来把 Sigmoid 只在训练阶段打开(追求均衡),推理切回 Top-K,吞吐立刻回来。
-
Expert-Choice 别盲目上。 很多开源 MoE 默认就是 Expert-Choice,理由是"负载均衡"。但我的实测里它和 Top-K 的 MaxVio 完全一致。除非你的容量 factor 压到 1.0 以下,否则这笔复杂度是白花的。
-
MaxVio 是个好指标,但别只看它。 它衡量的是分布形状,跟实际 PPL 没有单调关系。我试过把 Sigmoid 的负载接回一个小模型跑困惑度,发现 MaxVio 从 0.0048 压到 0.0003 之后,PPL 几乎没有变化。均衡不等于更好。
-
偏置要调,不能硬抄。 脚本搜出来的 +1.20 只对我这批随机种子有效。换模型、换 expert 数、换偏好强度,最优偏置都会漂移。生产环境建议每 500 步重搜一次,而不是写死一个常量。
五、可落地的三条
- 搭 MoE 路由对照时,先把目标负载按 K×T/E 算对,否则容量截断会吞掉全部信号。
- 推理阶段用 Top-K 就够了,Sigmoid 门控留给训练期做负载均衡,别混着用。
- MaxVio 降到 0.0003 这个量级就可以停了,再往下压不会换来 PPL 的实质改善。
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/weixin_44673517/article/details/170362244



