课程 13 · 专家并行

当 8 个专家里 3 个被挤爆

MoE 的并行不是「把权重切开」那么简单:token 会往少数热门专家涌,热门专家所在的卡就变成瓶颈。EPLB 就是给这个问题做的动态再平衡——140 行代码,一个可优化目标。

约 55 分钟 源码:model_executor/eplb/policy.py(140 行) 前置:05、12

2026-11-08 源码校正

本页已按本地固定commit修正相关描述。三框架对照 · 带行号的离线证据 · 核查与勘误详情。

1MoE 的并行方式:EP

MoE 层里有一组「专家」(expert),每个 token 经 router 选出 top-k 个专家,只经过这几个专家的计算。专家并行(EP)就是把专家分到不同的卡上:

rank 0: experts 0,1,2,3      rank 1: experts 4,5,6,7
        ↑                            ↑
   本地 token 直接算          需要把 token 发过去(all-to-all)

实际每个rank有capacity个物理专家槽位,可能容纳多个专家:
  dispatch(发出去)→ 专家本地计算 → combine(收回来)

和 TP 的区别很关键:

TPEP
切什么每层权重矩阵专家(一整个子网络)
通信all-reduce(每次前向、每层)all-to-all(每个 MoE 层、按 token 路由)
通信量取决于激活值大小路由结果——热门专家会让某几张卡收到远超平均的 token

2问题:负载不均

假设 64 个专家分布在 8 张卡上,每卡 8 个。如果 router 在某批请求里把 40% 的 token 都路由到专家 7,那么:

训练时的经典解法是 auxiliary loss(惩罚路由不均),但推理时你改不了权重。推理侧的解法是:给热门专家多放几个副本,再把副本均匀摊到各卡上——这就是 EPLB(Expert Parallel Load Balancer)。

3三个术语

policy.py 开头就把术语定义清楚了[policy.py L1–7]:

logical expert:模型定义的专家,共 num_logical 个。
physical expert:GPU 上实际放置的副本,共 num_logical + num_redundant 个。

于是有两张映射表:

4算法:两步贪心

rebalance_experts_global()[L73] 做两件事。

第一步:决定给谁加副本

policy.py · L109–123for i in range(num_redundant_experts):
    score = tokens_per_expert_all_diff / logical_count       # 每个专家当前的「每副本负载」
    score_added = tokens_per_expert / (logical_count + 1)    # 加一个副本后的负载
    values, indices = score.max(-1, keepdim=True)            # 当前最热的专家
    ...
    redundancy_indices = values.sum(0).argmin(-1)            # 谁加副本后,峰值负载降得最多
    physical_index = num_logical_experts + i
    physical_to_logical_map[:, physical_index] = redundancy_indices

直觉:每次挑「加上一个副本后,全局最坏情况改善最多」的那个专家。注意 values.sum(0)——它对多个历史 step 求和,而不是只看当前,避免为一次突发流量做长期错误的决定。

第二步:把副本摊到各张卡

policy.py · L43–45# 目标:使 sum_{step i} max_{rank j} (sum_{expert e 分到 rank j} e 在 step i 的负载) 尽量小
# 贪心:每次把当前 expert 放到「让历史各 step 的最大 rank 负载之和最小」的 rank 上

这是一个最小化峰值的目标:不是让平均负载均衡,而是让最忙的那张卡尽量不忙。因为整层的完成时间由最忙的卡决定。

policy.py · L57–66torch.add(loads, vector.unsqueeze(1), out=candidates)
torch.maximum(candidates, peaks.unsqueeze(1), out=candidates)   # 各历史 step 的 rank 最大负载
torch.sum(candidates, dim=-1, out=scores)
scores.masked_fill_(occupancy == capacity, float("inf"))        # 已满的 rank 排除
rank = scores.argmin(dim=-1)
indices[layers, rank * capacity + occupied] = physical_order[:, job_index]

实现有遍历num_physical的Python循环,循环内对layers/ranks/history steps做张量计算;每次尝试未满rank,最小化历史峰值和。贪心不保证全局最优。

5代价:再平衡不是免费的

EPLB运行时按rearrangement interval和prefill相关配置触发;此提交的触发函数没有负载偏斜阈值判断。触发策略与实际weight shuffle见触发源码。

一句话总结

EP 把专家分散到卡上,但路由是数据决定的、天然不均;EPLB 用「冗余副本 + 峰值最小化贪心」把这个不均抹平,代价是周期性的权重搬运。

6练习 A · 手算一次副本分配

4 个逻辑专家、2 个冗余副本,3 个历史 step 的 token 数(每个专家):

E0E1E2E3
step 1100101010
step 2100101010
step 3100101010

第一个冗余副本应该给谁?第二个呢?

展开:答案

初始每专家 1 个副本:每副本负载 E0=100、其余=10。

给 E0 加副本后:E0 每副本 50,全局峰值从 100 降到 50,改善最大 → 第一份给 E0。

此时 E0 两个副本各 50,仍是最热。再给 E0 加第三个副本会变成 33,但也许该给 E1?取决于配额——若只剩一个冗余且目标是峰值最小,仍然给 E0(100/3≈33 < 50)。所以第二个还是 E0。

这说明一个反直觉的结论:冗余不是平均分配的,热门专家可以占多个副本,直到它的每副本负载接近次热门。

7练习 B · 闭卷测验

8面试复述卡

「MoE 推理的负载不均怎么解?」

展开:30 秒版本

「推理时改不了路由权重,所以用专家并行的冗余副本:统计每个专家在一段时间内的 token 数,给最热的专家多放几个副本,再把所有副本按『让最忙的卡尽量不忙』的目标贪心摊到各卡上。代价是再平衡要搬权重,所以按interval与prefill配置触发。DeepInfer 的 EPLB 就是这套,策略含逐物理专家循环,内部采用张量计算。」

可以问:「如果 router 本身有 bug,导致所有 token 都去同一个专家呢?」——EPLB 能缓解但不能根治,这类问题要回训练侧看负载均衡 loss。