第 14 课:DeepSeek DeepEP 核心架构与 MoE 专家并行通信算子拆解

本课目标与完成标准

学完后你应能:

  • 深刻理解大模型 MoE(Mixture of Experts)在集群规模扩展下的通信瓶颈(All-to-All Dispatch & Combine);
  • 掌握 DeepSeek 开源 MoE 通信库 DeepEP 的核心架构与设计哲学,说清它与通用 NCCL All-to-All 的主要区别;
  • 剖析 DeepEP 的两大核心引擎:高吞吐普通模式(High-Throughput Normal Mode) 与 低延迟推理模式(Low-Latency Mode);
  • 掌握 SM 算力分区(SM Resource Partitioning)与“计算-通信完全重叠”(Mega-MoE Overlapping)的实现机理;
  • 理解跨机分层调度(Hierarchical All-to-All:NVLink 节点内 + RDMA 节点间)与兼容 CUDA Graph 的静态缓冲设计。

通过必须同时满足:

  • 独立补齐本课唯一的代码填空题(MoE 专家路由分发 Dispatch 与加权合并 Combine 核心算子模拟),并通过测试断言;
  • 三个问答题均说明因果链与硬件交互逻辑,拒绝空洞术语;
  • 能讲清至少一个通信正确性边界与性能权衡(如 SM 占用量与 GEMM 算力竞争);
  • 总分不低于 8/10,且无一票否决级概念错误。

前置关系

核心心智模型

1. 它是什么,解决什么问题(如图 14-1 所示)

DeepEP 的两种通信模式

普通模式(训练 / Prefill)

  • 先经 RDMA 发到目标节点上同号 GPU,再经 NVLink 转发
  • 每个 token 对每个目标节点只跨机一次
  • 只占约 20 个 SM,其余 SM 继续计算

低延迟模式(Decode)

  • 纯 RDMA(IBGDA),GPU 直接发起,无 CPU 参与
  • 固定形状缓冲区,可被 CUDA Graph 捕获
  • hook 接口:传输期间不占 SM

通信可被掩盖的条件:Tgemm≥Tcomm=V/BWT_{\text{gemm}} \ge T_{\text{comm}} = V / \text{BW};每卡发送量 VV 约为 token 数 × 跨节点副本数 × dd × 每元素字节数(FP8 dispatch 为 1 字节)

图 14-1:普通模式走两跳路由,低延迟模式走纯 RDMA

DeepEP 是 DeepSeek 专门为超大规模混合专家模型(MoE,如 DeepSeek-V3 / R1 / V2)打造的首个开源专用 EP(Expert Parallelism)通信库。

(1) 传统 MoE 分布式训练与推理的通信瓶颈

DeepSeek-V3 拥有 671B 参数,采用了极度细粒度的路由机制:总共 256 个路由专家 + 1 个共享专家,每个 Token 动态激活 Top-8 个专家。当集群规模达到数百乃至数千张 GPU 时,专家被切分在不同物理节点上:

  1. NCCL all_to_allv 的巨大延迟开销:
    • NCCL 作为通用通信原语,依赖 Host 端 CPU 发起,每次 Launch 开销高达 10∼20μs10 \sim 20\,\mu\text{s};
    • 在推理 Decoding 阶段(Batch 较小,每个 Token 仅几十微秒的 GEMM 计算),通信 Launch 开销甚至超过了计算本身,导致 GPU 发生大量同步空转。
  2. 动态 Token 数量破坏 CUDA Graph 捕获:
    • 每个 Expert 接收到的 Token 数因数据分布动态变化。传统做法需要 CPU 介入统计偏移行数并重新下发变长张量,破坏了推理引擎的 CUDA Graph 静态执行图。
  3. 节点内 NVLink 与跨机 RDMA 无法高效分层流水:
    • 扁平的 All-to-All 使得跨机网络带宽与卡间 NVLink 带宽发生拥塞短板效应,无法跑满硬件物理上限。

(2) DeepEP 的主要设计

  • 纯 RDMA 低延迟内核(Low-Latency Kernel):不经过 CPU 与 NCCL。GPU 线程通过 IBGDA 直接向网卡提交 RDMA 请求;配合基于 hook 的接口,数据在网络上传输期间不占用 SM。官方在 H800 + CX7 400 Gb/s IB 上测得 dispatch 约 77~194 µs(EP 8~256),combine 延迟同为百微秒量级(具体数值见 README 性能表)。
  • 兼容 CUDA Graph(无 CPU 介入):解码阶段使用预分配的固定形状缓冲区,并返回每个本地专家实际收到的 Token 数,路由寻址与同步都在 GPU 上完成,因此可以被 CUDA Graph 捕获;
  • SM 分区与计算通信重叠(如图 14-3** 所示)**:普通模式内核只占用约 20~24 个 SM 执行通信(DeepSeek-V3 训练中为 20 个),其余 SM 继续计算,使通信时间可以被计算掩盖。

图 14-3:DeepEP 专用通信 SM 与主计算流在时间轴上的无气泡重叠机理


2. DeepEP 核心微架构与算法剖析

(1) 双模引擎设计(Dual-Engine Architecture)

DeepEP 针对训练与推理的不同吞吐特征,实现了两套独立的算子流水线:

维度 普通高吞吐模式(Normal Mode) 低延迟模式(Low-Latency Mode)
主要应用场景 离线训练(Training)与推理首字填充(Prefill) 推理逐字生成(Decoding)与低延迟服务
数据吞吐特征 大 Batch、长序列、Token 吞吐量巨大 小 Batch(1~128 tokens)、对时延极端敏感
底层硬件通道 显存 Buffer + NVLink / 跨机 RDMA 大包多通道并发 纯 RDMA(IBGDA),GPU 直接发起;基于 hook 的通信-计算重叠不占用 SM
CPU 介入度 有:CPU 需等待 GPU 上报各卡接收 Token 数后分配输出张量 无 CPU 介入,GPU 直接发起 RDMA
CUDA Graph 支持 不支持(接收数量在 CPU 端动态确定) 支持(缓冲区形状固定,无动态分配)

(2) 低延迟引擎(Low-Latency Engine)微架构细节

在 Low-Latency 模式下,所有 Token 默认都通过 RDMA 发送(即使目标卡在同一节点),以减少跳数和中间转发:

  • IBGDA 直接发起 RDMA:源 GPU 的线程直接向网卡提交 RDMA 写请求,把 Token 写入目标卡预分配的接收缓冲区,再用 RDMA 原子操作更新目标卡上的计数/标志位,不经过 CPU 代理线程;
  • 接收端轮询:目标卡的接收内核轮询这些计数/标志位,确认数据到齐后再把 Token 交给专家计算;
  • 基于 hook 的重叠:设置 return_recv_hook=True 时,dispatch/combine 只发出 RDMA 请求就返回,网络传输期间不占用 SM,计算流可以继续执行其他工作,之后调用 hook() 完成接收。官方测得 dispatch 约 77~194 µs(H800 + CX7 400 Gb/s IB,EP 8~256),combine 也是百微秒量级,而不是几个微秒。

(3) 分层 All-to-All(Hierarchical All-to-All)与两跳路由

对于跨节点集群(例如 8 卡/节点):

  • 节点内(Intra-node):NVLink(H100 为 900GB/s900\,\text{GB/s} 双向;DeepSeek 使用的 H800 为 400GB/s400\,\text{GB/s} 双向,实测约 160GB/s160\,\text{GB/s});
  • 节点间(Inter-node):受限于网卡带宽(如 400Gbps≈50GB/s400\,\text{Gbps} \approx 50\,\text{GB/s})。
  • 两跳路由机制(普通模式):发往远程节点的 Token 先经 RDMA 发送到目标节点上与本卡局部编号相同的 GPU(例如本节点 3 号卡发往目标节点 3 号卡),再由这张 GPU 经 NVLink 转发到目标专家所在的卡。一个 Token 即使在目标节点上选中了多个专家,也只跨机传输一次,节点内的多份副本由 NVLink 分发;同时每张卡只和其他节点的同号卡通信,各卡的网卡负载比较均衡。

3. 核心代码架构解析:Dispatch 与 Combine 原语

以下为低延迟模式调用流程的简化示意:

# 简化示意:参数名参考 DeepEP README 中的低延迟示例,完整参数与返回值以源码为准
import torch
import deep_ep
import deep_gemm

# 1. 初始化低延迟模式缓冲区(只需要 RDMA 缓冲区)
num_rdma_bytes = deep_ep.Buffer.get_low_latency_rdma_size_hint(
    num_max_dispatch_tokens_per_rank, hidden, group.size(), num_experts)
buffer = deep_ep.Buffer(
    group, num_nvl_bytes=0, num_rdma_bytes=num_rdma_bytes,
    low_latency_mode=True, num_qps_per_rank=num_experts // group.size())

# 2. 低延迟 Token 分发(Dispatch)
# x:        [num_tokens, hidden] 输入激活值(BF16)
# topk_idx: [num_tokens, num_topk] 各 Token 选中的全局专家编号
recv_x, recv_count, handle, event, hook = buffer.low_latency_dispatch(
    x, topk_idx, num_max_dispatch_tokens_per_rank, num_experts,
    async_finish=False, return_recv_hook=True)   # 只发出 RDMA 请求,不等待接收完成
# ... 这里可以执行其他计算;RDMA 在网络上传输期间不占用 SM ...
hook()   # 完成接收
# recv_x:默认是 (FP8 数据, 缩放系数) 元组,数据形状 [num_local_experts, num_max_dispatch_tokens_per_rank * num_ranks, hidden]
# recv_count:[num_local_experts],每个本地专家实际收到的 Token 数

# 3. 本地专家计算(DeepGEMM Masked Grouped GEMM,masked_m 即 recv_count)
deep_gemm.m_grouped_fp8_gemm_nt_masked(
    recv_x, expert_weights_fp8, expert_out,
    masked_m=recv_count, expected_m=expected_m)

# 4. 专家输出合并(Combine):送回源卡并按 Top-K 权重加权求和
combined_x, event, hook = buffer.low_latency_combine(
    expert_out, topk_idx, topk_weights, handle,
    async_finish=False, return_recv_hook=True)
hook()

实践任务:唯一代码填空题

练习说明

请实现一个自包含的 MoE 专家并行路由与加权合并算子模拟器(MoE Token Dispatch & Combine Simulator):

  • 输入 NN 个 Token,每个 Token 拥有隐藏维度 H=4H=4;
  • 每个 Token 路由到 Top-KK(K=2K=2)个专家,共有 E=4E=4 个专家,每个专家有归属的虚拟 Rank;
  • Dispatch 阶段:按照专家索引将 Token 数据打包,分发到对应专家的待处理队列中;
  • Expert Compute 阶段:专家对接收到的 Token 执行简单的前向映射(仿 GEMM 运算:y=x⋅Wey = x \cdot W_e);
  • Combine 阶段:将计算结果拉回原 Token 位置,并严格按照 Gating 权重执行归一化加权求和。

只能修改 TODO/______ 位置,不得删除断言或放宽通过条件。

Code
import numpy as np

def moe_dispatch(tokens, topk_idx, num_experts):
    """
    模拟 DeepEP Dispatch 阶段:将 Token 打包分配给各 Expert
    tokens:   [N, H]
    topk_idx: [N, K] (每个 token 选中的 expert 编号)
    返回:
      expert_inputs: dict {expert_id: list of (token_idx, token_vector)}
    """
    N, H = tokens.shape
    N_idx, K = topk_idx.shape
    assert N == N_idx

    expert_inputs = {e: [] for e in range(num_experts)}

    # TODO: 遍历所有 Token 与其 Top-K 路由目标,填入对应专家的输入队列
    for i in range(N):
        for k in range(K):
            e_id = topk_idx[i, k]
            # 存入 (原序列索引 i, 对应的 token 特征向量)
            ______

    return expert_inputs

def moe_combine(expert_outputs, topk_idx, topk_weights, N, H):
    """
    模拟 DeepEP Combine 阶段:接收各专家输出,按 Gating Weights 加权汇聚
    expert_outputs: dict {expert_id: list of (token_idx, output_vector)}
    topk_idx:       [N, K]
    topk_weights:   [N, K]
    返回:
      combined_out:   [N, H]
    """
    combined_out = np.zeros((N, H), dtype=np.float32)

    # 建立从 (token_idx, expert_id) 到输出结果的快速查找表
    result_map = {}
    for e_id, item_list in expert_outputs.items():
        for t_idx, out_vec in item_list:
            result_map[(t_idx, e_id)] = out_vec

    # TODO: 按照 topk_idx 与 topk_weights 完成原始 Token 的加权和
    for i in range(N):
        for k in range(topk_idx.shape[1]):
            e_id = topk_idx[i, k]
            weight = topk_weights[i, k]
            out_vec = result_map[(i, e_id)]
            combined_out[i] += ______

    return combined_out

# ----------------- 测试用例与端到端验证 -----------------
np.random.seed(42)
N, H, K, E = 4, 4, 2, 4

# 生成虚拟输入
tokens = np.random.randn(N, H).astype(np.float32)
# 每个 token 随机选 2 个不重复的专家
topk_idx = np.array([
    [0, 2],
    [1, 3],
    [0, 3],
    [1, 2]
], dtype=np.int32)

# 归一化 Softmax 门控权重
raw_weights = np.random.uniform(0.1, 1.0, size=(N, K))
topk_weights = (raw_weights / raw_weights.sum(axis=1, keepdims=True)).astype(np.float32)

# 专家权重矩阵(每个专家一个 H x H 线性变换)
expert_weights = {e: np.random.randn(H, H).astype(np.float32) for e in range(E)}

# 1. 运行 Dispatch
dispatched = moe_dispatch(tokens, topk_idx, num_experts=E)
assert sum(len(v) for v in dispatched.values()) == N * K, "总分发量必须等于 N * K"

# 2. 执行专家计算
expert_outputs = {}
for e, items in dispatched.items():
    expert_outputs[e] = []
    for t_idx, vec in items:
        out_vec = np.matmul(vec, expert_weights[e])
        expert_outputs[e].append((t_idx, out_vec))

# 3. 运行 Combine
combined = moe_combine(expert_outputs, topk_idx, topk_weights, N, H)

# 4. 黄金参考标杆 (单机稠密基准对比)
ref_out = np.zeros((N, H), dtype=np.float32)
for i in range(N):
    for k in range(K):
        e = topk_idx[i, k]
        w = topk_weights[i, k]
        ref_out[i] += w * np.matmul(tokens[i], expert_weights[e])

assert np.allclose(combined, ref_out, atol=1e-5), "MoE Dispatch & Combine 数学等价性校验失败!"
print("DeepEP 核心调度流数学等价验证通过。")

检查方法

运行脚本验证断言通过;核对各专家处理 Token 总数与 N * K 守恒,确认加权汇聚结果与单机数学真值严格一致。

Q1

在 MoE 推理的 Decoding 阶段,为什么传统基于 CPU 发起的 NCCL all_to_allv 会造成严重的显卡算力空转(Bubbles)?DeepEP 的“纯 RDMA(IBGDA)低延迟内核”是如何消除这一瓶颈的?

你的答案:

Q2

DeepEP 在架构设计上为何采用非对称 SM 分区(固定分配 20~24 个 SM 专职通信),而不是让全部 SM(如 H800 的 132 个 SM)并发参与传输?这与 Mega-MoE 的计算重叠有什么直接因果联系?

你的答案:

Q3

面对跨多物理机节点的大规模集群,为什么扁平的 All-to-All 性能远逊于 DeepEP 的“分层两跳路由(Hierarchical 2-Hop)”?在面临不同专家负载偏斜(Load Imbalance / Straggler)时,系统是如何防范尾部延迟爆炸的?

你的答案:

评分与通过规则

  • 代码 4 分:正确完成 Dispatch 索引映射与 Combine 加权和逻辑并跑通断言;
  • Q1~Q3 各 2 分:必须结合微架构原语(CPU Launch 延迟、NVLink 硬件带宽、GPUDirect RDMA、SM 占用与计算并发)深入说明;
  • 一票否决:混淆 Dispatch 与 Combine 的通信方向,或认为通讯必须占用全量 SM。

参考答案(仅 answer 分支)

Code
def moe_dispatch(tokens, topk_idx, num_experts):
    N, H = tokens.shape
    expert_inputs = {e: [] for e in range(num_experts)}

    for i in range(N):
        for k in range(topk_idx.shape[1]):
            e_id = int(topk_idx[i, k])
            expert_inputs[e_id].append((i, tokens[i]))

    return expert_inputs

def moe_combine(expert_outputs, topk_idx, topk_weights, N, H):
    combined_out = np.zeros((N, H), dtype=np.float32)

    result_map = {}
    for e_id, item_list in expert_outputs.items():
        for t_idx, out_vec in item_list:
            result_map[(t_idx, e_id)] = out_vec

    for i in range(N):
        for k in range(topk_idx.shape[1]):
            e_id = int(topk_idx[i, k])
            weight = topk_weights[i, k]
            out_vec = result_map[(i, e_id)]
            combined_out[i] += weight * out_vec

    return combined_out

# 执行自测
dispatched = moe_dispatch(tokens, topk_idx, num_experts=E)
expert_outputs = {e: [(t_idx, np.matmul(vec, expert_weights[e])) for t_idx, vec in items] for e, items in dispatched.items()}
combined = moe_combine(expert_outputs, topk_idx, topk_weights, N, H)
assert np.allclose(combined, ref_out, atol=1e-5)
print("参考答案验证执行通过。")

Q1 参考答案

  1. NCCL 的 CPU 发起瓶颈与时延占比: 在通用 NCCL 中,每次通信必须由 CPU 运行通信运行时代码,解析通信拓扑并向 GPU 发射通信 Kernel。这一开销固定在 10∼25μs10 \sim 25\,\mu\text{s}。在训练大 Batch 时该开销可忽略,但在推理 Decoding 阶段,每个 Step 的 GEMM 计算耗时可能仅有 30μs30\,\mu\text{s}。CPU Launch 开销直接吃掉整个执行周期的 40% 以上,造成严重的“CPU 喂不饱 GPU”的饥饿气泡。

  2. 纯 RDMA(IBGDA)低延迟内核: DeepEP 在 GPU 显存中预分配固定形状的接收缓冲区和计数/标志位。GPU 线程通过 IBGDA 直接向网卡提交 RDMA 写请求,把 Token 写到目标卡,再用 RDMA 原子操作通知对方,不需要 CPU 代理参与,也不需要按步重新分配张量,因此可以被 CUDA Graph 捕获。使用 return_recv_hook=True 时,内核只发出 RDMA 请求就返回,传输期间不占用 SM,计算可以与通信重叠,之后调用 hook() 完成接收。延迟方面,官方在 H800 + CX7 400 Gb/s IB 上测得 dispatch 约 77~194 µs(EP 8~256),combine 同为百微秒量级;它消除的是 CPU 发起与同步的开销,以及通信对 SM 的占用。

Q2 参考答案

  1. SM 资源饱和度阈值: NVLink 的双向物理带宽(如 H100 的 900 GB/s)由 GPU 专用的 NVLink 传输引擎和片上 Crossbar 提供支持。DeepSeek-V3 报告和 DeepEP 的测试表明,约 20~24 个 SM 发出的并发访存与网络请求就足以用满 NVLink/RDMA 带宽。继续增加通信 SM,带宽基本不再提升,却会挤占计算可用的 SM。

  2. Mega-MoE 计算-通信双流重叠的前提: 如果通信占满了所有 SM,GPU 就无法同时执行其他计算 Kernel。通过将 SM 显式分区:20 个 SM 锁定在独立 CUDA Stream 跑通信,其余 110 个 SM 锁定在主计算 Stream 执行共享专家(Shared Expert)或前序 Attention 算子。两者在 GPU 上并行执行,只要计算时间足够长,通信时间就能被计算掩盖。

Q3 参考答案

  1. 分层两跳(Hierarchical 2-Hop)消灭网络木桶短板: DeepSeek 使用的 H800 节点内 NVLink 实测约 160 GB/s,节点间 RDMA 约 50 GB/s,相差约 3 倍。若采用扁平 All-to-All,一个 Token 在同一远程节点上选中几个专家,就要跨机发送几次,占用稀缺的 IB 带宽。DeepEP 普通模式先经 RDMA 把 Token 发到目标节点上与本卡局部编号相同的 GPU,每个 Token 对每个目标节点只跨机发送一次,再由该 GPU 经 NVLink 转发给节点内的各专家卡,把重复副本放到带宽更高的 NVLink 上。DeepSeek-V3 还限制每个 Token 最多路由到 4 个节点,进一步控制 IB 流量。

  2. 缓解长尾专家(Straggler)机制: 热门专家被选中的频率远高于冷门专家。负载均衡主要靠模型侧:DeepSeek-V3 的无辅助损失负载均衡(按专家负载动态调整路由偏置),以及推理侧的冗余专家部署(把高负载专家复制到多张卡)。通信层面,低延迟模式按 num_max_dispatch_tokens_per_rank 预留固定上限的缓冲区,不会因为单个专家过载而动态扩容;上限需按实际负载分布留足余量。

面试高频追问与优化扩展

  • 追问 1:DeepEP 的 Combine 算子是如何在通信搬运的同时完成加权求和(In-Flight Reduction)的?
    • 回答要点:传统做法是先把各专家的原始输出通过 All-to-All 全量收集回显存,再调用一个单独的 PyTorch/Triton Kernel 算加权和,增加了额外的显存读写往返。DeepEP 的 Combine Kernel 在将远端数据通过 NVLink/RDMA 读取到片上共享内存或寄存器后,直接在片上完成乘 Top-K 权重并原地累加到输出张量,实现通信与归约融合(Fused Communication & Reduction),省去单独归约 Kernel 的一次显存读写往返。
  • 追问 2:为什么 DeepEP 可以做到完全免除辅助损失函数(Aux-loss-free)的负载均衡鲁棒性?
    • 回答要点:过去 MoE 必须依赖高权重的 Aux-loss 强行让各专家 Token 数量均等,但严重损害了模型推理能力。DeepSeek-V3 在算法端采用动态偏置调整(Bias Adjustment),在系统端配合 DeepEP 的高动态范围缓冲切分,DeepSeek-V3 报告训练和推理都不丢弃 Token;短时负载偏斜由偏置调整和冗余专家吸收,DeepEP 负责把变长的 Token 分发高效完成。

源码参考与延伸阅读