课程 13 · 专家并行
MoE 的并行不是「把权重切开」那么简单:token 会往少数热门专家涌,热门专家所在的卡就变成瓶颈。EPLB 就是给这个问题做的动态再平衡——140 行代码,一个可优化目标。
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 的区别很关键:
| TP | EP | |
|---|---|---|
| 切什么 | 每层权重矩阵 | 专家(一整个子网络) |
| 通信 | all-reduce(每次前向、每层) | all-to-all(每个 MoE 层、按 token 路由) |
| 通信量取决于 | 激活值大小 | 路由结果——热门专家会让某几张卡收到远超平均的 token |
假设 64 个专家分布在 8 张卡上,每卡 8 个。如果 router 在某批请求里把 40% 的 token 都路由到专家 7,那么:
训练时的经典解法是 auxiliary loss(惩罚路由不均),但推理时你改不了权重。推理侧的解法是:给热门专家多放几个副本,再把副本均匀摊到各卡上——这就是 EPLB(Expert Parallel Load Balancer)。
policy.py 开头就把术语定义清楚了[policy.py L1–7]:
logical expert:模型定义的专家,共
num_logical个。
physical expert:GPU 上实际放置的副本,共num_logical + num_redundant个。
于是有两张映射表:
physical_to_logical_map:每个物理槽位放的是哪个逻辑专家。logical_to_physical_map:每个逻辑专家有哪些副本(无效位填 -1)。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,最小化历史峰值和。贪心不保证全局最优。
EPLB运行时按rearrangement interval和prefill相关配置触发;此提交的触发函数没有负载偏斜阈值判断。触发策略与实际weight shuffle见触发源码。
EP 把专家分散到卡上,但路由是数据决定的、天然不均;EPLB 用「冗余副本 + 峰值最小化贪心」把这个不均抹平,代价是周期性的权重搬运。
4 个逻辑专家、2 个冗余副本,3 个历史 step 的 token 数(每个专家):
| E0 | E1 | E2 | E3 | |
|---|---|---|---|---|
| step 1 | 100 | 10 | 10 | 10 |
| step 2 | 100 | 10 | 10 | 10 |
| step 3 | 100 | 10 | 10 | 10 |
第一个冗余副本应该给谁?第二个呢?
初始每专家 1 个副本:每副本负载 E0=100、其余=10。
给 E0 加副本后:E0 每副本 50,全局峰值从 100 降到 50,改善最大 → 第一份给 E0。
此时 E0 两个副本各 50,仍是最热。再给 E0 加第三个副本会变成 33,但也许该给 E1?取决于配额——若只剩一个冗余且目标是峰值最小,仍然给 E0(100/3≈33 < 50)。所以第二个还是 E0。
这说明一个反直觉的结论:冗余不是平均分配的,热门专家可以占多个副本,直到它的每副本负载接近次热门。
「MoE 推理的负载不均怎么解?」
「推理时改不了路由权重,所以用专家并行的冗余副本:统计每个专家在一段时间内的 token 数,给最热的专家多放几个副本,再把所有副本按『让最忙的卡尽量不忙』的目标贪心摊到各卡上。代价是再平衡要搬权重,所以按interval与prefill配置触发。DeepInfer 的 EPLB 就是这套,策略含逐物理专家循环,内部采用张量计算。」
可以问:「如果 router 本身有 bug,导致所有 token 都去同一个专家呢?」——EPLB 能缓解但不能根治,这类问题要回训练侧看负载均衡 loss。