/ DeepSeek
[DeepSeek-4] DeepEP:MoE 专家并行中的 Token 交通系统
从 MoE Dispatch 与 Combine 的路由语义出发,系统拆解 DeepEP V2 的 Rank 级去重、ElasticBuffer、EPHandle、Direct/Hybrid 通信、FP8 传输、SM/QP 资源模型与通信计算重叠。
截至 2026 年 7 月,DeepEP 主仓库已经切换到 DeepEP V2。它不是完整的训练或推理框架,而是一个面向 Mixture of Experts(MoE)和 Expert Parallelism(EP)的高性能通信库,核心只围绕两个动作展开:
dispatch:把 token 发送到其选中专家所在的 GPU;combine:把专家输出送回 token 原来的 GPU,并按照路由权重合并。
这两个名字看起来像普通的 All-to-All,真正困难的地方却在于:每个 token 的目标都不同,每个专家接收的 token 数也不同,通信前后还必须完成动态布局、局部复制、专家对齐和反向路由恢复。
一句话概括:
DeepEP 是一个将 MoE 的动态稀疏 token 路由映射成高效 NVLink/RDMA 数据流的 GPU 通信运行时。
如果说 DeepGEMM 负责“专家怎样算”,那么 DeepEP 负责的就是“token 怎样送过去,算完以后又怎样送回来”。本文从一次 MoE 层的数据流出发,逐步拆解 DeepEP V2 的对象、通信路径与性能取舍。DeepEP 仓库
1. DeepEP 位于 MoE 层的什么位置#
一个典型 MoE 层可以简化为:
Hidden States
|
v
Router / Gate
计算 top-k experts
|
| topk_idx + topk_weights
v
+--------------------+
| DeepEP Dispatch | 将 token 发到专家所在 GPU
+--------------------+
|
v
Grouped GEMM / Expert MLP
|
v
+--------------------+
| DeepEP Combine | 将结果送回原 GPU 并加权求和
+--------------------+
|
v
MoE Layer Output因此,DeepEP 不负责 Router 的 Top-K 选择、专家放置与负载均衡,也不负责 Expert GEMM、Attention 或 DP/TP 的梯度同步。它只接管 Router 与 Expert GEMM 之间,以及 Expert GEMM 与 MoE 输出之间的通信和数据重排。
这个边界非常重要。DeepEP 可以缩短 token 搬运时间,却无法修复一个已经严重失衡的路由结果;它可以把数据排成 Grouped GEMM 需要的布局,却不会替框架决定专家应该放在哪张卡上。
2. 为什么普通 All-to-All 还不够#
假设一个 MoE 层采用如下配置:
EP ranks = 16
Experts = 256
Experts per rank = 16
Top-K = 8某个 token 可能选中专家 3、19、44、68、121、155、201、243。这些专家可能分布在 6 到 8 张 GPU 上;另一个 token 的目标集合又会完全不同。于是每对 rank 之间的数据量并不相等,每张 GPU 上各个专家收到的 token 数也在动态变化。
若直接使用通用的 all_to_all_single,框架通常需要在通信前后自行完成:
- 统计发往每个目标 rank 的 token 数并计算 split size;
- 按目标 rank 排序,将 token pack 到连续发送缓冲区;
- 执行变长 All-to-All,再对接收数据 unpack;
- 按本地 expert 再次排序,准备 Expert GEMM;
- 计算结束后反向重排,再执行一次 All-to-All;
- 根据原路由恢复 token 顺序并进行 Top-K 加权归约。
通信原语只解决“字节从 rank A 到 rank B”的问题,却不知道某段数据属于哪个 token、哪个 expert,也不知道回程时怎样将 K 份结果合并。DeepEP 的价值正是把布局计算、pack/unpack、通信、元数据传递、专家对齐和路由恢复融合进专用 GPU kernel。
因此更准确的关系是:
NCCL 提供底层 GPU 通信能力,DeepEP 在其上实现具有 MoE 路由语义的 Dispatch/Combine。
3. Dispatch:把稀疏路由变成连续数据流#
Dispatch 的典型输入是:
x: [num_tokens, hidden]
topk_idx: [num_tokens, num_topk]
topk_weights: [num_tokens, num_topk]其中 topk_idx[t, k] 表示第 t 个 token 选择的第 k 个专家。围绕这些索引,Dispatch 大体执行:
expert_id -> EP rank
-> 统计每个 rank / expert 的接收量
-> 计算发送与接收 buffer 的位置
-> pack token 和路由元数据
-> 通过 NVLink 或 RDMA 传输
-> 在目标 GPU 上按本地 expert 排列
-> 生成每个 expert 的 token 计数
-> 保存 Combine 所需的反向映射输出不仅是专家输入张量,还包括 Expert GEMM 分组所需的计数或前缀和。换句话说,Dispatch 的输出布局应当直接服务后续 Grouped GEMM,而不是先产生一份通用布局,再交给另一组算子重复排序。
3.1 按目标 rank 去重 token#
一个很关键、也很有 MoE 语义的优化,是在 rank 层面对 token 去重。假设 token A 的两个 Top-K 专家都位于 GPU 5:
Token A
|-- Expert 81 -> GPU 5
`-- Expert 86 -> GPU 5最直接的方法会把完整 hidden state 向 GPU 5 发送两次。DeepEP 可以只跨 rank 传输一次,再在目标 GPU 上展开给两个本地专家:
GPU 0 ---- Token A ----> GPU 5
|-- local copy -> Expert 81
`-- local copy -> Expert 86当前实现记录的 rank 接收 token 数正是去重后的数量:同一个 token 即使选择了同一 rank 上的多个专家,也只计数一次。ElasticBuffer 源码
若专家均匀分布、Top-K 选择近似独立,令 EP rank 数为 、Top-K 为 ,则每个 token 访问的不同 rank 数期望为:
当 EP=16、Top-K=8 时:
也就是说,一个 token 虽然激活 8 个专家,平均只需要将 hidden state 发往约 6.45 个 rank。若路由策略进一步限制专家组或节点范围,目标 rank 的重合度会更高,这项优化也会更有价值。
4. Combine:沿原路径返回并完成归约#
专家计算完成后,一个 token 对应 K 份输出 ,最终 MoE 输出为:
Combine 必须完成四类工作:
- 找到每份 expert 输出对应的源 token 和源 rank;
- 将结果从 expert 所在 GPU 发回原始 GPU;
- 对同一 token 的多个 expert 输出进行归约并乘以
topk_weights; - 恢复原始 token 顺序。
所以 Combine 不是简单的反向 All-to-All。它依赖 Dispatch 保存的映射,才能把动态展开后的专家输出重新折叠回原 token。
训练中的前后向也呈现出一种对称性:
Dispatch forward 的 backward ~= Combine
Combine forward 的 backward ~= Dispatch官方示例正是利用这种对应关系组织自动求导路径。README 示例
5. EPHandle:一次 Dispatch 的“回程票”#
EPHandle 是连接 Dispatch 和 Combine 的核心对象。它描述的不只是一个通信请求,而是一次动态路由布局:
token 从哪里来
-> 被发往哪些 rank
-> 在目标 buffer 的哪个 slot
-> 对应哪个 local expert
-> Combine 时怎样返回和归约当前 Handle 中包含原始 topk_idx、rank 与本地 expert 的接收前缀和、源 token 到目标 slot 的映射,以及 Hybrid 模式所需的转发元数据。Combine 因而不必重新推导整个路由。
在 Decode 场景中,如果连续步骤的路由布局确实一致,还可以复用缓存 Handle,跳过一部分 layout 计算与 CPU 同步。但这里的前提是“布局一致”,而不是“shape 一致”:新的 token 一旦选中了不同专家,盲目复用旧 Handle 就会产生错误映射。README 示例
6. 从 V1 的两条路径到 V2 的统一接口#
理解已有的 DeepEP 文章和推理框架配置时,仍然需要认识 V1 的两种通信模式。
6.1 Normal:为训练和 Prefill 追求带宽#
Normal,也称 High-throughput 模式,适用于训练、Prefill、大 batch 和 token 数较多的场景。它采用分层通信:节点内走 NVLink/NVSwitch,节点间走 RDMA。
一条典型跨节点路径是:
source GPU
| NVLink
v
local forwarding GPU
| RDMA
v
remote forwarding GPU
| NVLink
v
destination expert GPU这类设计利用了节点内与节点间带宽的不对称性,在节点内聚合或转发跨节点流量,使多张 GPU 和多张 NIC 更充分地并行工作。V1 文档将其称为 asymmetric-domain bandwidth forwarding。V1 文档
6.2 Low Latency:为 Decode 压缩固定开销#
Decode 中,每轮每个请求通常只产生一个 token。此时消息更小,kernel launch、CPU 同步、RDMA atomic 和 pack/unpack 的固定开销会比峰值带宽更重要。
V1 Low-latency kernel 因此主要采用更直接的 RDMA 路径,减少节点内转发阶段与主机控制。它还提供 receiving hook,使网络接收可以在后台推进,并与另一个 micro-batch 的 Attention 或 Expert GEMM 重叠;官方文档称这部分后台网络传输不占用计算侧 SM。V1 文档
V1 的场景划分因而十分清楚:
| 场景 | 主要目标 | V1 模式 |
|---|---|---|
| Training | 吞吐与带宽利用率 | Normal |
| Prefill | 大消息带宽 | Normal |
| Decode | 延迟与 TPOT | Low Latency |
6.3 V2:ElasticBuffer 统一两套 API#
V1 对外暴露 Buffer.dispatch()、Buffer.combine() 以及独立的 low_latency_dispatch()、low_latency_combine()。V2 将其统一为:
ElasticBuffer.dispatch()
ElasticBuffer.combine()训练、Prefill 与 Decode 使用同一套接口;内部再根据拓扑、消息规模、通信资源和 Handle 缓存选择执行方式。初始化时需要声明最坏情况下的容量:
ElasticBuffer(
group,
num_max_tokens_per_rank=...,
hidden=...,
num_topk=...,
use_fp8_dispatch=...,
)这些参数共同决定通信 buffer 与 workspace 的预留规模。统一 API 并不表示所有场景使用同一个固定 kernel,而是将策略选择下沉到了运行时。README 示例
7. V2 的底层变化:NCCL Gin 与设备侧通信#
V1 的跨节点能力主要依赖 NVSHMEM。V2 主路径改为 NCCL Gin Backend,并能够复用已有 NCCL communicator。NCCL Device API 允许 CUDA kernel 直接发起通信操作,减少 host 控制路径,也更便于将索引、搬运与通信推进组合在同一执行流程中。DeepEP 仓库
调用栈可以粗略表示为:
PyTorch
|
v
DeepEP ElasticBuffer
|
v
NCCL Gin Device Backend
|
+-- NVLink / NVSwitch
`-- RDMADeepEP 并没有取代 NCCL:NCCL 负责底层 peer 间的数据传输能力,DeepEP 负责把动态 MoE 路由翻译成可高效执行的通信计划。
8. Direct 与 Hybrid:拓扑决定数据怎样走#
V2 保留了两种内部通信形态。
8.1 Direct#
当 EP group 位于单个 scale-up domain 内、不需要独立 scale-out 层级时,rank 直接与目标 peer 通信:
GPU A ----------> GPU B典型场景是单个 NVLink/NVSwitch domain 内的 EP。Direct 模式倾向于使用更少的 Queue Pair(QP),避免细粒度消息中 doorbell ringing 本身成为显著开销。
8.2 Hybrid#
当通信同时包含节点内 scale-up 和节点间 scale-out 时,路径被拆成:
Local NVLink
|
v
Cross-node RDMA
|
v
Remote NVLink当前源码把 num_scaleout_ranks > 1 视为 Hybrid 模式,并为跨节点转发维护 channel linked list、token forward metadata,以及不同的发送与接收 buffer。ElasticBuffer Runtime
Hybrid 模式则更倾向于为各个 channel 和通知路径准备独立 QP,以推动大量细粒度 token 流并发前进。这说明 DeepEP 优化的并不是少量整齐的大包,而是目的地持续变化、大小不均的稀疏数据流。
9. ElasticBuffer:不只是预分配 Tensor#
ElasticBuffer 是 DeepEP V2 的通信运行时。它管理的不只是数据内存,还包括:
- send/recv buffer 与 Dispatch/Combine workspace;
- 路由元数据和 Handle 生命周期;
- NCCL Gin context 与 RDMA QP;
- 独立 communication stream;
- Direct/Hybrid 模式状态与通信资源配置。
“Elastic”目前更多体现为统一接口和可扩展内存布局。官方代码说明当前物理存储仍主要是 GPU memory,CPU/GPU 混合物理内存属于后续扩展方向。ElasticBuffer 源码
由于 MoE 的接收量动态变化,buffer 必须按 num_max_tokens_per_rank、hidden size、Top-K、EP size 和数据类型为最坏情况留出空间。V2 官方也明确指出,它的 buffer 占用高于 V1。DeepEP 仓库
10. EventOverlap:让通信只在真正依赖处阻塞#
DeepEP 通常在独立 communication stream 上运行。例如:
recv_x, *rest, event = buffer.dispatch(
x,
topk_idx=topk_idx,
topk_weights=topk_weights,
async_with_compute_stream=True,
)
# 在 compute stream 上执行与 recv_x 无关的工作
event.current_stream_wait()
# 从这里开始才能安全使用 recv_x依赖关系因此变为:
Communication stream: Dispatch ------------------+
v
Compute stream: Independent work -------- Wait -> Expert GEMM而不是从 Dispatch 发起时就阻塞整个默认 stream。真正的 overlap 仍要求框架找到可并行的计算,并正确管理 buffer 生命周期;设置 async_with_compute_stream=True 只建立了异步执行的可能性,不会凭空创造重叠窗口。README 示例
11. SM 与 QP:通信也要做资源预算#
通信 kernel 并非“免费”运行。它会占用 SM、寄存器、shared memory 和调度槽位;分配过多计算资源虽然更容易打满 NVLink/RDMA,却可能挤压 Expert GEMM。
更多通信 SM
-> 更容易达到网络峰值
-> 与 Expert GEMM 竞争更强
更少通信 SM
-> 更适合通信计算重叠
-> 可能无法打满链路V1 常通过 benchmark auto-tuning,再手工设置类似 Buffer.set_num_sms(24) 的参数。V2 根据 RDMA/NVLink 带宽、专家数、Top-K、单 SM 读写能力、Direct/Hybrid 模式,以及是否希望与计算重叠建立理论模型,并通过:
get_theoretical_num_sms()
get_theoretical_num_qps()选择通信 kernel 的 SM 数和 RDMA QP 数。ElasticBuffer 源码
模型隐含的前提是 Router 负载大体均衡;当少数专家过热时,理论带宽模型无法消除等待最慢 expert/rank 的尾部延迟。因此,自动资源选择仍需与真实路由分布一起验证。
12. 为什么 FP8 Dispatch 特别有效#
Dispatch 传输的主体是 hidden state。若每个 token 访问的不同目标 rank 数为 ,通信量可以近似写成:
其中 是 token 数, 是 hidden size, 是每个元素的字节数。BF16 的 ,FP8 的 ,因此 FP8 Dispatch 理论上可以将 hidden state 主体流量降低接近一半。
这并不意味着整个 MoE 层都必须使用 FP8。Expert 输出和 Combine 可以继续采用 BF16,以降低多专家加权归约中的精度风险。官方 V2 benchmark 使用的正是“FP8 Dispatch + BF16 Combine”配置。README 示例
13. 怎样阅读官方 V2 性能数字#
官方 V2 benchmark 的代表性配置是:
8K tokens/rank
Hidden = 7168
Top-K = 8
FP8 Dispatch
BF16 Combine部分结果如下:
| 拓扑 | Dispatch | Combine | 通信 SM |
|---|---|---|---|
| SM90,CX7,EP 8x2 | 90 GB/s | 81 GB/s | 12 |
| SM90,CX7,EP 8x4 | 61 GB/s | 61 GB/s | 6 |
| SM100,单节点 EP8 | 726 GB/s | 740 GB/s | 64 |
| SM100,单节点 EP8,少 SM | 643 GB/s | 675 GB/s | 24 |
官方总结称,V2 相比 V1 的峰值性能最高提升约 1.3 倍,通信 SM 用量最高减少约 4 倍,并将规模扩展到 EP2048。DeepEP 仓库
这里的 GB/s 是逻辑瓶颈带宽,可能将本地 rank 流量与经过不同层级的有效数据量计入指标,不能直接等同于一张 NIC 的物理线速。更有意义的阅读方式是同时观察:
- 相同拓扑、shape 和数据类型下的端到端延迟;
- 每 rank 的 token 数与路由分布;
- 通信使用的 SM/QP 数及其对 GEMM 的影响;
- Dispatch 和 Combine 是否真正与计算重叠;
- 最慢 rank 的尾延迟,而不只是平均逻辑带宽。
14. DeepEP 解决不了哪些问题#
14.1 Expert 负载不均衡#
如果 Expert 7 收到 5000 个 token,而 Expert 8 只收到 200 个,即使通信无限快,Expert 7 的 GEMM 仍会成为 straggler。DeepEP 不能替代 auxiliary loss、expert replication、dynamic placement 或 Expert Parallel Load Balancer(EPLB)。
14.2 错误或低效的网络拓扑#
DeepEP 高度依赖 GPU peer access、NVLink/NVSwitch、RDMA,以及 NIC-GPU 的 PCIe 亲和性。GID、MTU、QP、service level、adaptive routing 与流量隔离等配置也会直接影响表现。官方说明 V2 主要在 InfiniBand 上完成测试,理论上兼容 RoCE,并建议通过 virtual lane 隔离 EP 与其他通信流量。README 示例
14.3 不足的 buffer 容量#
MoE 接收 token 数不固定。若 num_max_tokens_per_rank 只按平均负载设置,一次路由波动就可能越过容量上限;若完全按极端上界预留,显存成本又可能过高。容量规划必须结合 batch、Top-K、路由约束、专家热度和并发策略共同完成。
14.4 框架层缺少可重叠工作#
DeepEP 可以暴露异步 event、控制通信 SM,却不能保证调度器一定有独立计算可插入。如果 Attention、Dispatch、Expert GEMM 和 Combine 之间全是严格串行依赖,再好的异步 API 也不会自动转化为端到端收益。
15. DeepEP 与 DP、TP、EP 怎样配合#
假设系统使用 DP16、TP1、EP16:
Attention: 每个 DP rank 处理自己的 token
|
Router: 为每个 token 选择 Top-K experts
|
DeepEP: 在 EP16 group 内执行 Dispatch
|
Experts: 每张卡计算本地 experts
|
DeepEP: 在 EP16 group 内执行 Combine这里:
TP1表示 expert 内部没有 Tensor Parallel 通信;EP16表示每个 MoE 层产生一次 Dispatch 和一次 Combine;DP16是否需要 AllReduce,取决于训练/推理阶段以及 DP、EP group 的组织方式;- DeepEP 只优化 EP group 内的 token 交换,不处理 DP AllReduce。
两机 16 卡的 EP16 同时涉及节点内 NVLink/NVSwitch 与节点间 RDMA,是 Hybrid 路径的典型目标。若一个 EP4 group 完全位于单机内部,则 EP 通信只经过节点内互联,不会使用服务器网卡。
16. 从源码理解 DeepEP V2#
V2 kernel 通过轻量 JIT 模块在运行时编译,安装阶段不需要预编译所有 CUDA kernel。仓库同时提供 PTX/SASS dump、PTXAS 检查和 LineInfo 等调试选项。源码目录
阅读时可以沿着一条完整数据流前进:
README ElasticBuffer example
|
v
deep_ep/buffers/elastic.py Python API、参数检查、Handle 管理
|
v
tests/elastic/test_ep.py shape、正确性、性能与调用顺序
|
v
csrc/elastic/buffer.hpp C++ runtime、Direct/Hybrid 状态
|
+--> csrc/indexing/ token 索引与布局计算
|
+--> csrc/kernels/ Dispatch/Combine 通信 kernel
|
`--> csrc/jit/ kernel 编译与缓存其中 tests/elastic/test_ep.py 是最佳入口:它把 Python API、数据 shape、Handle 生命周期、Dispatch/Combine 对称性和性能指标放在同一上下文中。直接从最底层 kernel 开始读,反而容易看见大量地址与同步细节,却不知道它们在维护哪一种上层路由语义。
结语:优化的不是 All-to-All,而是动态路由全流程#
DeepEP 的核心价值并非单独提高某次 All-to-All 的峰值带宽,而是将一整条动态路由链路作为一个系统问题处理:
Dynamic routing
+ rank-level token deduplication
+ layout and pack/unpack
+ expert alignment
+ FP8 transport
+ hierarchical NVLink/RDMA
+ communication-compute overlap
+ reverse-route recovery
+ SM/QP resource control对 MoE 系统而言,最值得借鉴的也正是这些边界清晰的原则:Dispatch/Combine 应当理解路由语义;节点内和节点间通信需要分层设计;传输布局要与 Grouped GEMM 联合设计;通信 kernel 必须接受计算资源预算;Prefill 与 Decode 需要不同的优化目标;可以缓存的路由元数据应尽量留在设备侧,避免重复计算与 CPU 同步。
最终,DeepEP 做的事情可以落回一个非常具体的比喻:Router 决定每个 token 要拜访哪些专家,Expert GEMM 完成专家计算,而 DeepEP 负责修建道路、合并同路乘客、安排换乘,并确保每份结果拿着正确的回程票回到原位。