EPLB
EPLB 解决的问题场景
在 MoE Load Balance 中介绍了各种负载均衡算法,目标是在 MoE 模型训练阶段,尽量保证模型尽可能均衡的选择专家,实现真正的 MoE「术业有专攻」的设计初衷。
以 DeepSeek-V3 为例,训练时已经有一套 auxiliary-loss-free 的均衡机制:给每个 expert 的路由分数加一个可学习的 bias,谁太热就把 bias 调低。得益于 auxiliary-loss-free 算法的有效性,他们在整个训练期间都没有丢弃任何 token。
No Token-Dropping. Due to the effective load balancing strategy, DeepSeek-V 3 keeps a good load balance during its full training. Therefore, DeepSeek-V 3 does not drop any tokens during training.
但是,除了训练期间的负载均衡,DeepSeek V3 论文同时提到,他们也采取了特别的部署策略,来保证推理期间也不丢弃 token1。
In addition, we also implement specific deployment strategies to ensure inference load balance, so DeepSeek-V 3 also does not drop tokens during inference.
也就是说,即使在训练时专家的负载均衡做的很好,**在推理阶段,线上数据分布改变后仍可能出现热门专家:
- 线上请求领域变化、不同 batch 的 token 分布和 GPU 拓扑仍然会带来物理失衡。
- 领域漂移 domain-shift。线上流量的分布和 14.8 T 预训练语料不一样,而 bias 是在训练分布上学出来的,冻结之后不会再动。
- batch 规模。训练时一个 micro-batch 足够大,大数定律会帮你抹平方差;decode 阶段每个 expert 的 batch 常常在 256 token 以内,方差本身就是一等公民。
- 拓扑不一样。训练时 routed expert 摊在 8 个节点的 64 张卡上,部署时 prefill 是 EP32、decode 是 EP320。训练期均衡的是「每个 expert 的负载」,而部署时要均衡的是「每张卡的负载」,后者还取决于哪些 expert 恰好落在同一张卡上。
在推理阶段,已经不能继续在线修改模型的 $W_g$ 或 $b_i$,因此引入了 EPLB23。EPLB 不会去改变模型 weight,而是在物理部署层复制并重排这些专家,从而均衡物理 GPU 的负载。
| 机制 | 解决的问题 | 是否改变逻辑路由 |
|---|---|---|
| Router balance / auxiliary loss | 避免模型长期只选择少数专家 | 会影响专家选择 |
| EPLB | 复制、放置已有专家,平衡物理 GPU 负载 | 不改变逻辑专家 |
| DP load balancer | 把请求、序列分配给不同 DP 实例 | 不负责专家布局 |
| DeepEP | 执行 token 的 dispatch/combine 通信 | 不负责负载规划 |
EPLB 设计原则:Replication / Placement
对于 MoE Layer,一层的耗时由负载最重那张卡决定,不由平均负载决定。负载不均衡指标如下,其中 $L_g$ 是第 $g$ 张卡上所有 expert 的 token 数之和。
$$ \text{imbalance} = \frac{\max_g L_g}{\frac{1}{G}\sum_g L_g} $$要压低 $\max_g L_g$,能动的只有两样东西:
- 复制(replication):把热 expert 复制成 $r_i$ 份,每份承担 $w_i / r_i$ 的负载。
- 摆放(placement):决定哪个 expert replica 放在哪张卡。
对应于这篇讨论4,要想解决 EP 负载不均衡,经典的两条思路:
- 全局重排序方案:对专家重新排序,采用“高低搭配”的策略来平衡负载。这即是对应着 Placement
- 冗余副本方案:也就是在算力闲置的 GPU 上部署热门专家的副本,并将部分输入分流到副本上,从而实现负载的均衡。这即是对应着 Replication
只有第一个 Replication 能拆开单个热 expert。假设不复制,那么最热那个 expert 的负载 $\max_i w_i$ 会完整地压在某一张卡上,于是
$$ \frac{\max_g L_g}{\overline{L}} \;\ge\; \frac{\max_i w_i}{\overline{L}} $$EPLB 方案结合了全局重排序方案和冗余副本方案。
rebalance_experts 接口
GitHub 上给出 EPLB 最核心的接口 rebalance_experts :给定每个 logical expert 的负载估计,算出一份「每个 expert 复制几份、每份放在哪张 GPU」的部署方案。
|
|
关键输入参数:
| Notation | 对应变量 | 含义 |
|---|---|---|
| $L$ | - | layer 层数 |
| $E$ | - | 专家数 |
| $M$ | num_replicas | 副本总数(= physical experts 槽位数) |
| $G$ | num_groups | expert group 数(node-limited routing 用的那个分组) |
| $N$ | num_nodes | 节点数,节点内是 NVLink |
| $P$ | num_gpus | GPU 总数,必须是 num_nodes 的整数倍 |
rebalance_experts 的输出为以下 3 个变量:
| 名字 | shape | 含义 |
|---|---|---|
phy2log |
[L, M] |
第 $s$ 个物理槽位上装的是哪个 logical expert。加载权重时用它 |
log2phy |
[L, E, maxlogcnt] |
logical expert $e$ 的所有副本槽位号,不足的位置填 -1。dispatch 时用它 |
logcnt |
[L, E] |
logical expert $e$ 有几个副本 |
下图展示了经过 rebalance_experts 之后,如何基于这 3 个输出对 expert 排布进行调整。其中 P2P 搬运权重是最重的部分,而且期间那些层不能服务请求。SGLang 的解法是按层分块(--eplb-rebalance-layers-per-chunk),EPLBManager 那个生成器在块与块之间 yield 回去继续跑推理,把一次全量重排摊成很多小步5。
生产级 EPLB 实现
以 SGLang 为例,在生产环境中,基于 EPLB 搭建的动态负载均衡系统大致如下图所示:
参考 SGLang 的 python/sglang/srt/eplb/ 对应代码6:
- 统计负载。
ExpertDistributionRecorder在每次 forward 里累计每个 logical expert 收到的 token 数,dump_record()出来的logical_count就是喂给rebalance_experts的weight。 - 决定何时重排。
EPLBManager是个生成器,每eplb_rebalance_num_iterations次 forward 醒一次,还会先看average_utilization_rate_over_window判断值不值得动——重排本身有代价,不均衡不严重时最好别碰。 - 算方案。对应于 DeepSeek 那份代码,
eplb_algorithms/deepseek.py是eplb.py的逐行搬运,唯一的改动是把自动的策略选择换成显式的enable_hierarchical - 搬权重。
ExpertLocationUpdater按新的phy2log把 expert 权重在 GPU 之间 P 2 P 搬过去。
Q: 专家动态调整是发生在什么时候?
取决于预测算法的设计,频率可高可低。因为调整是有开销的,一般不会在每轮迭代中进行一次调整。
Q: 调整过程需要重新加载权重吗 ?
需要。如果是小 EP,只涉及小 EP 范围内的设备,如果是大 EP 调整范围更大。
EPLB 的实现
原语:replicate_experts
前 num_log 个槽位是原件,之后每多一个槽位,就给「当前每副本负载最高」的那个 expert 再复制一份。 weight / logcnt 是每个 logical expert 摊到每个副本上的负载,取 argmax 就是当前的瓶颈。
|
|
它在优化的目标是
$$ \min_{r} \; \max_i \frac{w_i}{r_i} \quad \text{s.t.} \quad \sum_i r_i = P,\; r_i \ge 1 $$值得强调的是,这个贪心不是启发式,它就是精确最优解。把它写成「反复给当前最高平均值追加一个席位」,就会发现这正是选举里的 D’Hondt(Jefferson)最高平均数法,而最高平均数法对 min-max 目标是最优的。
代价也小:循环 num_redundant 次,每次是一个 [n, num_log] 的向量化 max,所有层、所有节点并行推进。真正贵的是下一个原语。
原语:balanced_packing
balanced_packing 不搬 tensor,也不直接操作 GPU;它只计算一张摆放坐标表。这个函数会在分层策略里调用两次,同一个“物品/包”在两次调用中指代不同东西:
| 调用位置 | 待装的物品 | 包 | weight 表示什么 |
|---|---|---|---|
| Step 1 | expert group | node | group 收到的 token 总数 |
| Step 3 | physical expert replica | GPU | 该副本预计承担的 token 数 |
先把输入抽象成一个普通装箱问题。设一层里有 $n$ 个物品、$m$ 个包:
weight[l, j]是第 $l$ 行中物品 $j$ 的重量;每一行独立装箱。num_packs = m是包的数量。groups_per_pack = n / m是每个包必须容纳的物品数,记为 $k$。
目标是让各包的总重量尽量接近,但有一个不能违反的硬约束:每个包必须恰好装 $k$ 个物品。
每次只做一件事:在还没有装满的包里,找到当前总重量最小的那个,把下一个物品放进去。
|
|
核心循环直接读成四步:
indices = ...sort(...).indices:得到从重到轻的原始物品 ID。pack_items[i] < groups_per_pack:过滤掉已经装满的包。min(..., key=pack_weights.__getitem__):在剩余包中选择当前总负载最小的包。- 先记录
pack_index和rank_in_pack,再更新这个包的累计重量和物品数。因为rank_in_pack写在pack_items[pack] += 1之前,所以槽位从 0 开始编号。
这就是带固定基数约束的 LPT(Longest Processing Time first):先处理最重的物品,避免把大物品拖到最后;再用“高低搭配”压低最重包的总负载。
分层策略:三步走
两个原语拼起来就是 rebalance_experts_hierarchical,三步:
flowchart LR
W["weight<br/>L x E"] --> S1
S1["Step 1<br/>group 装进 node<br/>balanced_packing"] --> S2
S2["Step 2<br/>node 内复制<br/>replicate_experts"] --> S3
S3["Step 3<br/>副本装进 GPU<br/>balanced_packing"] --> O["phy2log<br/>log2phy<br/>logcnt"]
为什么要分层?因为 DeepSeek-V3 用的是 node-limited routing:256 个 routed expert 分成 8 个 group,每个 token 先选 top-4 个 group、再在里面选 8 个 expert,从而保证一个 token 最多只需要访问 4 个节点。如果把同一个 group 的 expert 全部放在同一个节点内,那么 group 内部的流量就走 NVLink 而不是 IB。分层策略就是为了保住这个性质:先决定哪些 group 归哪个节点,之后所有操作都锁在节点内部,绝不跨节点搬 expert。
分层策略与全局策略
rebalance_experts 支持两种策略:
- 分层策略
- 全局策略
在具体实现上,全局策略是分层策略在 $N=1$ 和 $G=1$ 的特殊实现
|
|
相当于告诉算法:
把所有 expert 看成一个 group,把全部 GPU 看成一个大节点,不考虑真实节点边界。
因此原来的三层分层约束自然退化成全局策略。
策略的选择是自动的:num_groups % num_nodes == 0 就分层,否则全局。
DeepSeek-V3 有 8 个 group,于是 prefill 的 4 节点(8 % 4 = 0)走分层、decode 的 40 节点(8 % 40 = 8)走全局。
效果与代价
下面这组数是我在 DeepSeek-V3 的形状上跑出来的:61 层里前 3 层是 dense,所以 58 个 MoE 层,每层 256 个 routed expert、8 个 group。部署按 prefill 的配置:EP32、4 个节点、288 个副本(也就是每卡 8 + 1 个 expert,正是 paper 里的 32 个冗余 expert)。
负载是人造的,因为真实的线上 expert 负载分布没有公开数据。我用了两种:对数正态($w_i = e^{\sigma z_i}$,模拟训练期均衡之后残留的温和偏斜)和 Zipf($w_i \propto \text{rank}^{-a}$,模拟病态的热点)。所有绝对数值都只在这两个人造分布下成立,但它们揭示的机制是真的。
温和偏斜下,EPLB 基本把问题解决了
| 偏斜(对数正态 $\sigma$) | 不用 EPLB | 分层 288 | 全局 288 | 分层 320 | 全局 320 |
|---|---|---|---|---|---|
| 0.1 | 1.073 | 1.007 | 1.002 | 1.006 | 1.001 |
| 0.2 | 1.151 | 1.011 | 1.002 | 1.010 | 1.001 |
| 0.3 | 1.237 | 1.020 | 1.008 | 1.018 | 1.004 |
| 0.5 | 1.444 | 1.031 | 1.008 | 1.031 | 1.007 |
训练已经把 expert 负载拉得比较平时,EPLB 能把 7%~44% 的单卡偏差压到 3% 以内。增加 32 个冗余副本已经拿到了大部分收益,继续增加到 64 个帮助不大。
全局策略更均衡,但分层策略只差一两个百分点,同时能保证 expert group 不跨节点。对跨节点带宽敏感的 Prefill 来说,这个交换是划算的。
偏斜严重时,分层策略会撞上节点级上限
| 偏斜(Zipf $a$) | 不用 EPLB | 分层 288 | 全局 288 | 分层 320 | 全局 320 |
|---|---|---|---|---|---|
| 0.0(完全均匀) | 1.000 | 1.000 | 1.000 | 1.000 | 1.000 |
| 0.3 | 1.399 | 1.024 | 1.001 | 1.025 | 1.002 |
| 0.6 | 2.430 | 1.084 | 1.001 | 1.083 | 1.001 |
| 1.0 | 6.064 | 1.415 | 1.006 | 1.341 | 1.001 |
| 1.5 | 13.448 | 2.900 | 1.178 | 2.364 | 1.033 |
在 $a=1.0$ 时,全局策略仍能做到 1.006,分层策略却停在 1.415。问题不在节点内部,而在节点之间:
| 层级 | max/mean |
|---|---|
| 节点之间(4 个节点的总负载) | 1.328 |
| 节点内部(每个节点的 8 张卡) | 1.024 |
| 整体(32 张卡) | 1.415 |
分层策略先把 expert group 分给节点,之后的复制和装箱都只发生在节点内部。设节点 $n$ 的总负载为 $L_n$,每个节点有 $P/N$ 张 GPU,那么:
$$ \underbrace{\frac{\max_g L_g}{\;\sum_n L_n / P\;}}_{\text{整体不均衡}} \;\ge\; \underbrace{\frac{\max_n L_n}{\;\sum_n L_n / N\;}}_{\text{节点级不均衡}} $$右边就是节点级不均衡,也是分层策略的硬下限。增加冗余副本只能改善节点内部的分配,不能把负载搬到其他节点:
| 副本数 | 冗余数 | 全局策略 | 分层策略 |
|---|---|---|---|
| 256 | 0 | 5.370 | 5.377 |
| 288 | 32 | 1.006 | 1.416 |
| 384 | 128 | 1.001 | 1.330 |
| 640 | 384 | 1.001 | 1.329 |
全局策略逐渐接近 1.00,分层策略则停在 1.33 左右。
这个下限和 expert group 的粒度有关。DeepSeek-V3 只有 8 个 group,放进 4 个节点后,每个节点恰好两个 group,可调整空间很小。实验中把 group 数从 8 增加到 64,节点级 max/mean 从 1.0225 降到 1.0023,整体也从 1.0305 降到 1.0118。
Prefill 分层策略,Decode 全局策略
但 group 数属于模型结构,部署时不能随便修改。
- 分层策略真正换来的,是通信局部性:每个 group 只在一个节点内
- 全局策略则可能把同一 group 铺到所有节点,削弱 node-limited routing 对跨节点通信的约束
因此 DeepSeek-V3 在 Prefill 使用分层策略,在节点更多、主要依赖 IB 点对点通信的 Decode 使用全局策略。
| Prefill 分层策略 | Decode 全局策略 | |
|---|---|---|
| 规模 | 4 节点 / EP 32 | 40 节点 / EP 320 |
| Group 与节点 | 8 groups 可均分到 4 节点 | 8 groups 无法均分到 40 节点 |
| 每 GPU expert 数 | 9 个 | 1 个 |
| 通信 | IB 到节点,再用 NVLink 转发 | Direct P2P over IB |
| 副本范围 | 限制在所属节点内 | 可以放到任意 GPU |
| 主要收益 | 保持 locality、控制跨节点通信 | 更充分地消除全局热点 |
| 主要代价 | 存在节点级负载不均衡地板 | 可能牺牲 group/node locality |
分层策略用于 EP size 较小的 prefill,全局策略用于 EP size 较大的 decode
DeepSeek V3 论文提到,Prefill 不增加跨节点 token All-to-All,是因为冗余副本仍放在原 expert 所属的节点内。Dispatcher 只在该节点内部选择不同 GPU,不会把同一个 token 同时发给多个副本。
在 Decode 阶段,DeepSeek-V3 Decode 正是每张 GPU 一个 physical expert,因此 只要决定热点 expert 复制几份,单卡负载就基本确定了。副本放在哪张 GPU,对计算装箱来说没有区别,因此可以把 320 张 GPU 当成一个扁平的 physical slot 池,不存在一个重排布的过程。
推理与部署实践
DeepSeek 在 2025 年介绍了他们 DeepSeek V3 的推理部署系统7。
计算通信 Overlap
对于 prefill 阶段,两个 batch 的计算和通信交错进行,一个 batch 在进行计算的时候可以去掩盖另一个 batch 的通信开销
对于 decode 阶段,不同阶段的执行时间有所差别,所以我们把 attention 部分拆成了两个 stage,共计 5 个 stage 的流水线来实现计算和通信的重叠。
更好的 Load Balancer
对于 MoE 模型的 Serving,结合 DeepEP 和 EPLB 可以比较好的实现高性能部署,然而在某些情况下仍然存在 imbalance 的情况,为此社区又进一步有一些新的方案89。
| Waterfill | LPLB | |
|---|---|---|
| 管哪部分负载 | shared expert(每个 token 都要过,稠密) | routed expert 里被 EPLB 复制过的那些(稀疏) |
| 决定什么 | 每个 token 的 shared 槽位由哪张卡执行 | 一个 logical expert 的 token 怎么在它的多个副本之间分配 |
| 手段 | 按剩余空隙灌水的轻量启发式 | 每层解一个 min-max 线性规划(GPU 上) |
| 前提 | shared expert fusion | EPLB 得真的放了冗余副本 |
| 开销 | 接近零 | 一次 all-reduce + 一次 LP 求解 |
参考资料
-
DeepSeek-V3 Technical Report, https://arxiv.org/abs/2412.19437 ↩︎
-
deepseek-ai/EPLB, https://github.com/deepseek-ai/EPLB ↩︎
-
DeepSeek Open Source Week Day 4 - Optimized Parallelism Strategies(DualPipe / EPLB / profile-data,2025-02-27), https://github.com/deepseek-ai/open-infra-index ↩︎
-
MoE 并行负载均衡:EPLB 的深度解析与可视化, https://zhuanlan.zhihu.com/p/29963005584 ↩︎
-
Support layerwise rebalancing experts, sgl-project/sglang#6851, https://github.com/sgl-project/sglang/pull/6851 ↩︎
-
sglang/python/sglang/srt/eplb/,其中
eplb_algorithms/deepseek.py是eplb.py的移植, https://github.com/sgl-project/sglang/tree/main/python/sglang/srt/eplb ↩︎ -
DeepSeek-V3 / R1 推理系统概览, https://zhuanlan.zhihu.com/p/27181462601 ↩︎
-
https://docs.sglang.io/docs/advanced_features/expert_parallelism#workload-balancer ↩︎
Author Houmin Wei
Publish January 1, 0001
LastMod August 27, 2026
License 本作品采用 CC BY-NC-ND 4.0 许可协议进行许可,转载时请注明原文链接
如果你在浏览博客的过程中发现了任何问题,欢迎在对应文章下评论。如果你有其他事情想要咨询,可以通过邮件联系我。