1. DeepSeek DeepEP框架与两阶段All-to-All通信

在分布式机器学习训练中,All-to-All通信是一个关键的性能瓶颈。DeepSeek团队开发的DeepEP框架针对这一挑战提出了创新的两阶段All-to-All算法,特别适用于追求高带宽的Mixture-of-Experts(MoE)模型训练场景。

传统All-to-All算子在发送数据到同一台机器的不同rank时,会存在重复token发送的问题。DeepSeek的解决方案充分利用了现代GPU集群的硬件特性:机内NVLink带宽远高于机间RDMA带宽。基于这一观察,他们设计了两阶段通信模式:

  1. 跨节点阶段:将发往同一机器的所有token先发送给目标机器的同号卡
  2. 机内转发阶段:通过目标机器的同号卡进行机内数据转发

这种设计显著减少了跨节点通信量,充分利用了机内高带宽优势。在实际测试中,这种策略可以将端到端通信时间降低30-50%,具体效果取决于集群规模和模型配置。

2. Notify Dispatch阶段的核心作用

Notify Dispatch是整个两阶段通信算法的"大脑",负责预先计算所有必要的元数据。这些元数据包括:

  • 每台机器将接收多少token(num_tokens_per_rank)
  • 每个专家(expert)将接收多少token(num_tokens_per_expert)
  • 每个RDMA节点将接收多少token(num_tokens_per_rdma_rank)

这些信息看似简单,但在大规模分布式训练中,它们的计算和同步效率直接影响整体性能。Notify Dispatch阶段的高效实现需要考虑以下几个关键点:

  1. 计算与通信重叠:在计算元数据的同时,尽可能提前发起必要的通信
  2. 内存访问模式优化:确保对topk_idx等输入张量的访问是合并的(coalesced)
  3. 同步开销最小化:设计高效的机内和跨节点同步机制

3. Buffer管理与内存布局设计

DeepEP框架引入了多种Buffer类型来管理通信过程中的内存使用:

3.1 基础Buffer结构

基础Buffer管理一段连续的内存区域,主要功能包括:

  • 内存分配与释放(通过cudaMalloc/cudaFree)
  • IPC(Inter-Process Communication)句柄管理
  • 设备间内存地址映射
class Buffer {
public:
    Buffer(int rank, int num_ranks, int64_t num_nvl_bytes, 
           int64_t num_rdma_bytes, bool low_latency_mode);
    
    // 获取IPC内存句柄
    cudaIpcMemHandle_t get_ipc_handle() const;
    
    // 同步各rank的Buffer状态
    void sync(const std::vector<std::optional<std::string>>& all_gathered_handles,
              const std::string& root_unique_id);
    
private:
    void* buffer_ptrs[NUM_MAX_NVL_PEERS];  // 各GPU的buffer指针
    cudaIpcMemHandle_t ipc_handles[NUM_MAX_NVL_PEERS];  // IPC句柄
    // ... 其他成员变量
};

3.2 SymBuffer与AsymBuffer

这两种特殊的Buffer类型用于解决多SM(Streaming Multiprocessor)并发访问的同步问题:

  • SymBuffer:对称缓冲区,所有SM以相同方式访问
  • AsymBuffer:非对称缓冲区,不同SM访问不同偏移量

它们的典型使用场景包括:

  • 跨节点通信数据的暂存(使用SymBuffer)
  • 机内数据转发的中间存储(使用AsymBuffer)

4. Notify Dispatch的GPU内核实现

Notify Dispatch的核心逻辑实现在CUDA内核notify_dispatch中,采用多SM协作的执行模式:

4.1 SM0的特殊职责

SM0负责全局的元数据收集和同步工作:

  1. 跨节点同步
if (sm_id == 0) {
    // 全局barrier:第一个warp执行机内同步,第二个warp执行机间同步
    if (thread_id == 32) 
        nvshmem_barrier_with_same_gpu_idx<kLowLatencyMode>(rdma_team);
    barrier_device<NUM_MAX_NVL_PEERS>(task_fifo_ptrs, head, nvl_rank);
    __syncthreads();
}
  1. 元数据收集与分发
  • 通过NVLink收集本机各GPU的统计信息
  • 通过RDMA将汇总信息发送给其他节点
  • 计算各专家的token分配前缀和

4.2 其他SM的工作负载

其余SM(SM1-SM7)专注于计算每个通信channel的详细元数据:

int dst_rdma_rank = sm_id - 1;
for (int channel_id = warp_id; channel_id < num_channels; channel_id += num_warps) {
    // 计算该channel处理的token范围
    get_channel_task_range(num_tokens, num_channels, channel_id, 
                          token_start_idx, token_end_idx);
    
    // 统计该channel向目标节点各GPU发送的token数
    int total_count = 0, per_nvl_rank_count[NUM_MAX_NVL_PEERS] = {0};
    for (int64_t i = token_start_idx + lane_id; i < token_end_idx; i += 32) {
        auto is_token_in_rank_values = /* 获取token分发信息 */;
        #pragma unroll
        for (int j = 0; j < NUM_MAX_NVL_PEERS; ++j)
            per_nvl_rank_count[j] += is_token_in_rank_values[j];
        total_count += (is_token_in_rank_uint64 != 0);
    }
    
    // 写入全局统计矩阵
    if (lane_id == 0) {
        for (int i = 0; i < NUM_MAX_NVL_PEERS; ++i)
            gbl_channel_prefix_matrix[(dst_rdma_rank * NUM_MAX_NVL_PEERS + i) * num_channels + channel_id] 
                = per_nvl_rank_count[i];
        rdma_channel_prefix_matrix[dst_rdma_rank * num_channels + channel_id] = total_count;
    }
}

5. 性能优化关键技巧

在实现Notify Dispatch时,DeepSeek团队应用了多项性能优化技术:

5.1 内存访问优化

  1. 合并访问:确保对topk_idx等输入张量的访问是合并的
  2. 共享内存利用:频繁访问的中间结果缓存在共享内存
  3. 寄存器压力控制:通过#pragma unroll控制循环展开程度

5.2 计算优化

  1. Warp级原语:使用warp_reduce_sum等warp级操作减少同步开销
  2. 异步执行:将计算与通信流水线化
  3. 负载均衡:动态调整各SM/channel的工作量

5.3 同步优化

  1. 轻量级Barrier:基于原子操作的机内barrier
  2. 分层同步:区分机内和机间同步需求
  3. 同步与计算重叠:在等待同步时执行其他有用工作

6. 实际应用中的注意事项

在实际部署Notify Dispatch时,需要注意以下几点:

  1. CUDA Graph兼容性:由于使用了CPU-GPU同步,当前实现与CUDA Graph不兼容
  2. 网络配置:必须关闭网络的adaptive routing功能
  3. 内存对齐:专家分配需要考虑内存对齐(expert_alignment)
  4. 错误处理:完善的错误检查机制确保分布式执行的可靠性

一个典型的使用示例如下:

def dispatch_forward(...):
    # 先计算通信布局
    num_tokens_per_rank, num_tokens_per_rdma_rank, num_tokens_per_expert, is_token_in_rank, previous_event = \
        _buffer.get_dispatch_layout(...)
    
    # 执行实际的MoE分发
    recv_x, recv_topk_idx, recv_topk_weights, num_recv_tokens_per_expert_list, handle, event = \
        _buffer.dispatch(...)
    
    return recv_x, recv_topk_idx, recv_topk_weights, num_recv_tokens_per_expert_list, handle, event

7. 扩展与未来方向

当前实现已经展示了卓越的性能,但仍有一些潜在的优化方向:

  1. 动态负载均衡:根据实时网络状况调整通信策略
  2. 拓扑感知:考虑实际的网络拓扑结构优化通信路径
  3. 混合精度支持:探索FP16/BF16等低精度通信
  4. 故障恢复:增强在节点故障情况下的恢复能力

在实际的MoE模型训练中,我们观察到Notify Dispatch阶段通常只占整个通信时间的10-15%,但它为后续的高效数据分发奠定了基础。通过精细调整元数据计算和同步的各个环节,可以在大规模分布式训练中获得显著的端到端性能提升。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐