DeepEP Buffer 系统深度分析:代码实证 vs 博客"第一性原理"模型
分析日期:2026-07-30
代码版本:DeepEP main branch
博客来源:Understanding GPU Microarchitecture from a Workload Perspective (3): DeepEP's First Principles
0. 摘要
博客中提出的 5 层 Buffer 模型(Token Buffer → Dispatch Buffer → Chunk Buffer → NVLink/RDMA → Receive Buffer → Expert Buffer)是一个教学简化模型。在实际的 DeepEP 代码实现中,Buffer 的组织方式与该模型存在显著差异。本文通过直接引用 csrc/elastic/buffer.hpp、deep_ep/include/deep_ep/common/layout.cuh、deep_ep/include/deep_ep/impls/dispatch.cuh 等核心代码,逐层还原真实的 Buffer 架构,并评估博客模型的准确性。
核心结论:
- 博客的 5 层模型捕获了数据流的逻辑阶段,但混淆了"用户输入/输出 Buffer"与"EP 内部通信 Buffer"的边界
- 实际实现中不存在独立的"Chunk Buffer"——chunk 是通信 kernel 的调度粒度而非独立 Buffer 段
- ElasticBuffer 的真实布局由 BufferLayout<kWithMBarrier> 模板参数化,分为 Direct 模式(3 段)和 Hybrid 模式(4 段)
- Legacy Buffer(NVSHMEM)与 Elastic Buffer(NCCL Gin)在组织哲学上根本不同:前者是"通道环形队列",后者是"rank-token 二维数组"
1. 博客原文引用:5 层 Buffer 模型
博客 section 2.1 对 Normal Kernel 数据路径的描述:
Data path:
Token Buffer → Dispatch Buffer → Chunk Buffer → NVLink / RDMA Pipeline → Receive Buffer → Expert BufferToken Buffer: Stores Router output. Layout: Token-major.
Dispatch Buffer: First layout transformation: Token-major → Destination-major.
Chunk Buffer: Critically important. The network is unsuitable for single-Token sends — produces small packets, high startup overhead, low bandwidth utilization. Tokens are aggregated:
Token Stream → Chunk → Network Transfer. Token is the scheduling granularity; Chunk is the communication granularity.Receive Buffer: Destination GPU receives Chunks from multiple GPUs.
Expert Buffer: Final transformation: Destination-major → Expert-major, forming Expert GEMM input.
博客 section 2.2 对 Low-Latency Kernel 的描述:
Low-Latency Kernel reduces intermediate layers:
Token Buffer → Direct RDMA → Receive Buffer → Expert BufferGoal: minimize end-to-end Token latency.
2. ElasticBuffer 内部结构:真实内存布局
2.1 整体内存排布
代码位置:csrc/elastic/buffer.hpp:19-34
class ElasticBuffer {
// Buffer bytes = GPU buffer + CPU buffer (excludes workspace)
// Memory layout: [[[Workspace] GPU buffer] CPU buffer]
int64_t num_buffer_bytes;
int64_t num_gpu_buffer_bytes;
int64_t num_cpu_buffer_bytes;
void* buffer;
void *workspace;
void *host_workspace, *mapped_host_workspace;
// ...
};
关键洞察:整体内存排布是 [[Workspace] GPU buffer] CPU buffer],其中:
- Workspace:位于最前端,用于 barrier、notify reduction、rank/expert 计数器等元数据
- GPU buffer:实际通信数据区(dispatch/combine 交替复用同一段)
- CPU buffer:仅用于 Engram(远程 KV cache 存储)
与博客对比:博客完全没有提及 Workspace。实际上 Workspace 占据独立空间,且被所有 kernel 共享使用。
2.2 构造函数中的分配逻辑
代码位置:csrc/elastic/buffer.hpp:81-140
ElasticBuffer(...) {
// Workspace is aligned to 2 MB so that it sits cleanly at the front of the GPU segment
const auto num_workspace_bytes = math::align<int64_t>(
layout::WorkspaceLayout::get_num_bytes(), symmetric::kNumAlignmentBytes);
// Create NCCL symmetric memory context
// Symmetric memory layout: [[[Workspace] GPU buffer] CPU buffer]
const auto num_sym_bytes = num_workspace_bytes + num_buffer_bytes;
this->nccl_context = std::make_shared<nccl::NCCLSymmetricMemoryContext>(
nccl_comm, cpu_comm, num_ranks, rank_idx,
num_sym_bytes, num_cpu_buffer_bytes, ...);
// Assign workspaces and buffers
workspace = this->nccl_context->mapped_window_ptr;
workspace_layout_wo_expert = std::make_shared<layout::WorkspaceLayout>(
workspace, nccl_context->num_scaleout_ranks, nccl_context->num_scaleup_ranks, 0);
buffer = static_cast<uint8_t*>(workspace) + num_workspace_bytes;
CUDA_RUNTIME_CHECK(cudaMemset(workspace, 0, num_workspace_bytes));
// Allocate host workspaces
CUDA_RUNTIME_CHECK(cudaMallocHost(&host_workspace, layout::WorkspaceLayout::get_num_bytes(), cudaHostAllocMapped));
CUDA_RUNTIME_CHECK(cudaHostGetDevicePointer(&mapped_host_workspace, host_workspace, 0));
std::memset(host_workspace, 0, layout::WorkspaceLayout::get_num_bytes());
}
关键分配规则:
1. workspace 从 mapped_window_ptr(NCCL 对称内存窗口起始)开始
2. buffer = workspace + num_workspace_bytes(2 MB 对齐)
3. host_workspace 是额外分配的 CUDA host-mapped 内存,用于 CPU 侧同步读取计数器
4. 整个 num_sym_bytes = num_workspace_bytes + num_buffer_bytes 注册到 NCCL 对称内存
3. Buffer 段映射:博客模型 vs 实际实现
3.1 BufferLayout 模板:核心组织单元
代码位置:deep_ep/include/deep_ep/common/layout.cuh:251-311
template <bool kWithMBarrier>
struct BufferLayout {
TokenLayout token_layout;
int num_ranks;
int num_max_tokens_per_rank;
void* base;
__forceinline__ __device__ __host__
BufferLayout(const TokenLayout& token_layout,
const int& num_ranks,
const int& max_num_tokens_per_rank,
void* base = nullptr) :
token_layout(token_layout),
num_ranks(num_ranks), num_max_tokens_per_rank(max_num_tokens_per_rank),
base(base) {}
int64_t get_num_bytes_per_rank() const {
return num_max_tokens_per_rank * get_num_bytes_per_token();
}
int64_t get_num_bytes() const {
return get_num_bytes_per_rank() * num_ranks;
}
BufferLayout get_rank_buffer(const int& rank_idx) const {
return BufferLayout(token_layout,
1, num_max_tokens_per_rank,
static_cast<int8_t*>(base) + get_num_bytes_per_rank() * rank_idx);
}
TokenLayout get_token_buffer(const int& token_idx, const bool& global = false) const {
EP_UNIFIED_ASSERT(num_ranks == 1 or global);
return TokenLayout(token_layout.num_hidden_bytes, token_layout.num_sf_bytes,
token_layout.num_topk, token_layout.with_metadata,
static_cast<int8_t*>(base) + token_layout.get_num_bytes<kWithMBarrier, int64_t>() * token_idx);
}
};
BufferLayout 的维度含义:
BufferLayout = [num_ranks] × [num_max_tokens_per_rank] × [TokenLayout]
- 第一维
num_ranks:每个 rank 一个"逻辑通道" - 第二维
num_max_tokens_per_rank:每个 rank 最多接收的 token 数 - 第三维
TokenLayout:每个 token 的字节布局
3.2 TokenLayout:单个 token 的内部结构
代码位置:deep_ep/include/deep_ep/common/layout.cuh:179-249
struct TokenLayout {
int num_hidden_bytes, num_sf_bytes;
bool with_metadata;
int num_topk, num_metadata_bytes;
void* base;
__forceinline__ __device__ __host__
TokenLayout(const int& num_hidden_bytes, const int& num_sf_bytes,
const int& num_topk, const bool& with_metadata, void* base = nullptr) :
num_hidden_bytes(num_hidden_bytes),
num_sf_bytes(num_sf_bytes),
with_metadata(with_metadata),
num_topk(num_topk),
num_metadata_bytes(num_topk * (sizeof(int) + sizeof(float)) +
(with_metadata ? (1 + num_topk) * sizeof(int) : 0)),
base(base) {}
template <bool kWithMBarrier, typename dtype_t = int>
__forceinline__ __device__ __host__ dtype_t get_num_bytes() const {
const auto num_bytes = math::align(num_hidden_bytes, ptx::kNumTMAAlignBytes) +
math::align(num_sf_bytes, ptx::kNumTMAAlignBytes) +
math::align(num_metadata_bytes, ptx::kNumTMAAlignBytes) +
math::align<int>(kWithMBarrier ? sizeof(ptx::mbarrier) : 0, ptx::kNumTMAAlignBytes);
return static_cast<dtype_t>(num_bytes);
}
void* get_hidden_ptr() const { return get_base_ptr(); }
sf_pack_t* get_sf_ptr() const {
return math::advance_ptr<sf_pack_t>(base, math::align(num_hidden_bytes, ptx::kNumTMAAlignBytes));
}
int* get_topk_idx_ptr() const { return get_metadata_ptr(); }
float* get_topk_weights_ptr() const {
return math::advance_ptr<float>(get_metadata_ptr(), num_topk * sizeof(int));
}
int* get_src_token_global_idx_ptr() const {
return math::advance_ptr<int>(get_topk_weights_ptr(), num_topk * sizeof(float));
}
int* get_linked_list_idx_ptr() const {
return get_src_token_global_idx_ptr() + 1;
}
ptx::mbarrier* get_mbarrier_ptr() const {
return math::advance_ptr<ptx::mbarrier>(get_metadata_ptr(), math::align(num_metadata_bytes, ptx::kNumTMAAlignBytes));
}
};
TokenLayout 内存排布:
[hidden_bytes | SF_bytes | metadata_bytes | mbarrier?]
↓ ↓ ↓
hidden scale factors topk_idx[num_topk]
topk_weights[num_topk]
src_token_global_idx
linked_list_idx[num_topk]
关键发现:每个 token 自带 topk_idx、topk_weights 等 metadata。这意味着 Buffer 不是纯数据区,而是"数据+元数据"交错的打包结构。博客中的"Layout Transformation"实际上是在 kernel 中通过 TMA store 到不同 slot 完成的。
3.3 Direct 模式的 Buffer 段分配(单节点 / scaleout_ranks == 1)
代码位置:csrc/elastic/buffer.hpp:586-613
static int64_t get_dispatch_buffer_size(const int& num_max_tokens_per_rank,
const int& hidden, const int& num_sf_packs, const int& num_topk,
const int& elem_size,
const int& num_scaleout_ranks, const int& num_scaleup_ranks,
const bool& is_scaleup_nvlink) {
const auto num_ranks = num_scaleup_ranks * num_scaleout_ranks;
const auto token_layout = get_dispatch_token_layout(hidden, elem_size, num_sf_packs, num_topk);
if (num_scaleout_ranks == 1) {
// Direct dispatch
const auto send_buffer_layout = layout::BufferLayout<false>(
token_layout, is_scaleup_nvlink ? 0 : 1, num_max_tokens_per_rank);
const auto recv_buffer_layout = layout::BufferLayout<false>(
token_layout, num_ranks, num_max_tokens_per_rank);
return send_buffer_layout.get_num_bytes() + recv_buffer_layout.get_num_bytes();
}
// ...
}
Direct 模式 Buffer 结构:
GPU buffer = [recv_buffer (num_ranks × max_tokens) | send_buffer (1 × max_tokens)]
代码位置:deep_ep/include/deep_ep/impls/dispatch.cuh:264-268(实际 kernel 中的使用)
// Buffer layouts
const auto token_layout = layout::TokenLayout(kNumHiddenBytes, kNumSFPacks * sizeof(sf_pack_t), kNumTopk, true);
const auto tma_buffer = layout::BufferLayout<true>(token_layout, kNumDispatchWarps, 1,
math::advance_ptr<int>(smem, kNumSmemBytesForNotify)).get_rank_buffer(dispatch_warp_idx).get_token_buffer(0);
auto recv_buffer = layout::BufferLayout<false>(token_layout, kNumRanks, kNumMaxTokensPerRank, buffer);
auto send_buffer = layout::BufferLayout<false>(token_layout, 1, kNumMaxTokensPerRank, recv_buffer.get_buffer_end_ptr());
recv_buffer = recv_buffer.get_rank_buffer(rank_idx);
Direct 模式 kernel 数据流:
输入 x (用户 Tensor)
↓ TMA load
smem tma_buffer (warp 本地 staging)
↓ TMA store
recv_buffer[dst_rank][dst_slot] (NVLink 直达目标 rank 的对称内存)
↓ gin.put (RDMA)
recv_buffer[...] (对于 NVLink 不可达的 rank,先写入 send_buffer 再 RDMA)
3.4 Hybrid 模式的 Buffer 段分配(多节点 / scaleout_ranks > 1)
代码位置:csrc/elastic/buffer.hpp:601-613
else {
// Hybrid dispatch
const auto scaleup_recv_buffer = layout::BufferLayout<false>(
token_layout, num_scaleup_ranks, num_scaleout_ranks * num_max_tokens_per_rank);
const auto scaleout_send_buffer = layout::BufferLayout<false>(
token_layout, 1, num_max_tokens_per_rank);
const auto scaleout_recv_buffer = layout::BufferLayout<false>(
token_layout, num_scaleout_ranks,
/* kNumChannels * kNumMaxTokensPerChannel */ num_max_tokens_per_rank + kNumMaxChannels);
return scaleup_recv_buffer.get_num_bytes() +
scaleout_send_buffer.get_num_bytes() +
scaleout_recv_buffer.get_num_bytes();
}
Hybrid 模式 Buffer 结构:
GPU buffer = [scaleup_recv_buffer | scaleout_send_buffer | scaleout_recv_buffer]
代码位置:deep_ep/include/deep_ep/impls/hybrid_dispatch.cuh:92-97(kernel 实际使用)
// All the buffers
auto scaleup_buffer = layout::BufferLayout<false>(
token_layout, kNumScaleupRanks, kNumScaleoutRanks * kNumMaxTokensPerRank, buffer);
auto scaleout_send_buffer = layout::BufferLayout<false>(
token_layout, 1, kNumMaxTokensPerRank, scaleup_buffer.get_buffer_end_ptr());
auto scaleout_recv_buffer = layout::BufferLayout<false>(
token_layout, kNumScaleoutRanks, kNumChannels * kNumMaxTokensPerChannel, scaleout_send_buffer.get_buffer_end_ptr());
Hybrid 模式 kernel 数据流(三阶段流水线):
输入 x
↓ (scaleout warps)
scaleout_send_buffer → gin.put(RDMA) → 远端 scaleout_recv_buffer
↓ (forward warps 轮询各 scaleout peer)
scaleout_recv_buffer → TMA store → scaleup_buffer[dst_scaleup_rank][slot]
↓ (copy epilogue)
scaleup_buffer → recv_x (用户 Tensor)
4. V1 (Legacy) vs V2 (Elastic) Buffer 组织对比
4.1 Legacy Buffer:NVSHMEM 通道环形队列
代码位置:csrc/legacy/buffer.hpp:22-81
struct Buffer {
// Low-latency mode buffer
int low_latency_buffer_idx = 0;
bool low_latency_mode = false;
// NVLink Buffer
int64_t num_nvl_bytes;
void* buffer_ptrs[LEGACY_NUM_MAX_NVL_PEERS] = {nullptr};
void** buffer_ptrs_gpu = nullptr;
// NVSHMEM Buffer
int64_t num_rdma_bytes;
void* rdma_buffer_ptr = nullptr;
// Workspace
void* workspace = nullptr;
// Host-side MoE info
volatile int* moe_recv_counter = nullptr;
int* moe_recv_counter_mapped = nullptr;
volatile int* moe_recv_expert_counter = nullptr;
volatile int* moe_recv_rdma_counter = nullptr;
// ...
};
代码位置:csrc/legacy/config.hpp:53-73(NVL buffer size hint)
size_t get_nvl_buffer_size_hint(size_t hidden_bytes, int num_ranks) const {
const int num_channels = num_sms / 2;
size_t num_bytes = 0;
num_bytes += num_channels * num_nvl_ranks * (2 * num_rdma_ranks + 3) * sizeof(int); // metadata
num_bytes += num_channels * num_nvl_ranks * num_max_nvl_chunked_recv_tokens * hidden_bytes; // data
num_bytes += num_channels * num_nvl_ranks * num_max_nvl_chunked_recv_tokens * internode::get_source_meta_bytes();
num_bytes += num_channels * num_nvl_ranks * num_max_nvl_chunked_recv_tokens * kNumMaxTopK * sizeof(topk_idx_t);
num_bytes += num_channels * num_nvl_ranks * num_max_nvl_chunked_recv_tokens * kNumMaxTopK * sizeof(float);
num_bytes += num_channels * num_nvl_ranks * num_max_nvl_chunked_recv_tokens * kNumMaxScales * sizeof(float);
return num_bytes;
}
Legacy NVLink Buffer 布局:
num_nvl_bytes = Σ_channels Σ_ranks [
metadata (start/end/head/tail)
+ data[num_max_nvl_chunked_recv_tokens]
+ source_meta[...]
+ topk_idx[...]
+ topk_weights[...]
+ scales[...]
]
Legacy 的关键特征:
1. 通道分离:每个 channel 有独立的 queue head/tail,形成环形缓冲
2. chunk 显式化:num_max_nvl_chunked_recv_tokens 是实际的 chunk 大小参数
3. IPC 共享:通过 buffer_ptrs[LEGACY_NUM_MAX_NVL_PEERS] 直接打开对端内存
4. NVSHMEM 分配:RDMA buffer 通过 nvshmem::alloc() 分配
4.2 Elastic Buffer:NCCL Gin 对称内存
Elastic 的关键特征:
1. 模板化布局:BufferLayout<kWithMBarrier> 编译期确定排布
2. 对称内存:通过 NCCL Symmetric Memory (ncclWinAllocate) 注册
3. rank × token 二维索引:buffer[rank_idx][token_idx] 直接寻址,无环形队列
4. TMA 硬件加速:利用 Tensor Memory Accelerator 做全局内存拷贝
5. mbarrier 同步:每个 token slot 自带 mbarrier,支持异步等待
4.3 对比表
| 维度 | Legacy (V1) | Elastic (V2) |
|---|---|---|
| 通信后端 | NVSHMEM (nvshmem_*) |
NCCL Gin (ncclDevComm + ncclWindow) |
| 内存注册 | nvshmem::alloc |
ncclWinAllocate (Symmetric) |
| Buffer 组织 | 通道环形队列 (head/tail) | rank × token 二维数组 |
| Chunk 概念 | 显式 num_max_*_chunked_*_tokens |
隐式(warp 轮询 tokens) |
| 同步机制 | nvshmem barrier + 显式 flag |
mbarrier + NCCL barrier + atomic counter |
| 元数据位置 | 与 data 交错在同一 queue | TokenLayout 内 data 后紧跟 metadata |
| 拓扑感知 | 固定 8 NVLink peers | 动态 scaleout/scaleup 逻辑域 |
| CPU 同步 | moe_recv_counter (host-mapped) |
host_workspace (mapped) + encode_decode_positive |
5. BufferLayout 模板深度解析
5.1 rank_buffer vs token_buffer
// 获取某个 rank 的子 BufferLayout(维度退化为 1 × num_max_tokens_per_rank)
BufferLayout get_rank_buffer(const int& rank_idx) const {
return BufferLayout(token_layout,
1, num_max_tokens_per_rank,
static_cast<int8_t*>(base) + get_num_bytes_per_rank() * rank_idx);
}
// 获取某个 token 的 TokenLayout(维度退化为单个 token)
TokenLayout get_token_buffer(const int& token_idx, const bool& global = false) const {
EP_UNIFIED_ASSERT(num_ranks == 1 or global);
return TokenLayout(token_layout.num_hidden_bytes, token_layout.num_sf_bytes,
token_layout.num_topk, token_layout.with_metadata,
static_cast<int8_t*>(base) + token_layout.get_num_bytes<kWithMBarrier, int64_t>() * token_idx);
}
维度链:
BufferLayout(num_ranks, max_tokens)
├── .get_rank_buffer(i) → BufferLayout(1, max_tokens)
│ └── .get_token_buffer(j) → TokenLayout(single token)
└── .get_channel_buffer<kTokensPerChannel>(c) → BufferLayout(num_ranks, max_tokens)
// 注意:channel 不是降维,而是 stride 访问
5.2 kWithMBarrier 模板参数
kWithMBarrier 控制是否在 TokenLayout 末尾预留 mbarrier 空间:
- BufferLayout<true>(smem 中的 TMA staging buffer):需要 mbarrier 做 load/store 同步
- BufferLayout<false>(global memory 中的 recv/send buffer):不需要 mbarrier
代码位置:deep_ep/include/deep_ep/common/layout.cuh:201-208
template <bool kWithMBarrier, typename dtype_t = int>
__forceinline__ __device__ __host__ dtype_t get_num_bytes() const {
const auto num_bytes = math::align(num_hidden_bytes, ptx::kNumTMAAlignBytes) +
math::align(num_sf_bytes, ptx::kNumTMAAlignBytes) +
math::align(num_metadata_bytes, ptx::kNumTMAAlignBytes) +
math::align<int>(kWithMBarrier ? sizeof(ptx::mbarrier) : 0, ptx::kNumTMAAlignBytes);
return static_cast<dtype_t>(num_bytes);
}
5.3 Channel Buffer 的 stride 访问
template <int kNumTokensPerChannel>
__forceinline__ __device__ __host__
BufferLayout get_channel_buffer(const int& channel_idx) const {
EP_UNIFIED_ASSERT(num_max_tokens_per_rank % kNumTokensPerChannel == 0);
return BufferLayout(token_layout,
num_ranks, num_max_tokens_per_rank,
static_cast<int8_t*>(base) + get_num_bytes_per_token() * kNumTokensPerChannel * channel_idx);
}
重要:
get_channel_buffer返回的 BufferLayout 仍然保持num_ranks × num_max_tokens_per_rank维度,但 base 指针偏移了kNumTokensPerChannel * channel_idx。这意味着 channel 是交错排布而非连续分块的——第kNumTokensPerChannel * c个 token 属于 channel c。
6. WorkspaceLayout:元数据专用空间
6.1 整体结构
代码位置:deep_ep/include/deep_ep/common/layout.cuh:10-177
struct WorkspaceLayout {
void* workspace;
int num_ranks;
int num_scaleout_ranks, num_scaleup_ranks;
int num_experts, num_experts_per_rank;
static constexpr int kNumMaxRanks = 1024;
static constexpr int kNumMaxExperts = 2048;
static constexpr int kNumMaxExpertsPerRank = 256;
static constexpr int kNumMaxInflightAGRS = 32;
static constexpr int64_t kNumBarrierSignalBytes = 16;
static int64_t get_num_bytes() {
int64_t num_bytes = 0;
num_bytes += kNumBarrierSignalBytes; // NVLink barrier
num_bytes += (kNumMaxRanks + kNumMaxExperts) * sizeof(int64_t); // Notify reduction
num_bytes += kNumMaxRanks * sizeof(int64_t) * 2; // Scaleup rank count
num_bytes += kNumMaxExperts * sizeof(int64_t) * 2; // Scaleup expert count
num_bytes += kNumMaxRanks * sizeof(int); // Scaleup atomic sender
num_bytes += kNumMaxRanks * sizeof(int) * 2; // Scaleout rank count
num_bytes += kNumMaxExperts * sizeof(int) * 2; // Scaleout expert count
num_bytes += kNumMaxRanks * kNumMaxChannels * sizeof(int64_t); // Scaleout channel tails
num_bytes += kNumMaxRanks * kNumMaxChannels * sizeof(int); // Channel scaleup tails
num_bytes += 2 * 2 * sizeof(int64_t); // PP counts
num_bytes += (kNumMaxInflightAGRS + 1) * kNumMaxRanks * sizeof(int); // AGRS signals
return num_bytes;
}
// ...
};
6.2 Workspace 各段用途
| 偏移段 | 用途 | 消费者 |
|---|---|---|
nvl_barrier_counter (16B) |
NVLink 全局 barrier | gpu_barrier |
notify_reduction_workspace |
SM 间原子加归约 | notify warps |
scaleup_rank_expert_count |
scaleup 域 rank/expert 接收计数 | notify + copy epilogue |
scaleup_atomic_sender_counter |
发送端 slot 原子分配 | dispatch warps |
scaleout_rank_expert_count |
scaleout 域 rank/expert 接收计数 | hybrid notify warps |
scaleout_channel_signaled_tail |
scaleout channel 完成通知 | forward warps |
channel_scaleup_tail |
channel → scaleup 链表尾 | copy epilogue |
pp_send/recv_count |
Pipeline Parallel 计数 | PP kernel |
agrs_recv/session_signal |
All-Gather Reduce-Scatter 会话 | AGRS kernel |
6.3 host_workspace 的双重映射
// Allocate host workspaces
CUDA_RUNTIME_CHECK(cudaMallocHost(&host_workspace, layout::WorkspaceLayout::get_num_bytes(), cudaHostAllocMapped));
CUDA_RUNTIME_CHECK(cudaHostGetDevicePointer(&mapped_host_workspace, host_workspace, 0));
设计意图:
- host_workspace:CPU 端虚拟地址,用于 cudaDeviceSynchronize() 后读取计数器
- mapped_host_workspace:GPU 端虚拟地址,用于 GPU kernel 写入
- 两者指向同一物理内存(zero-copy),实现 CPU 无锁轮询
7. 代码实证:Buffer 分配与使用的完整链路
7.1 dispatch 调用链
Python: buffer.dispatch(x, topk_idx, topk_weights, ...)
↓
C++: ElasticBuffer::dispatch (csrc/elastic/buffer.hpp:702)
├── 计算 num_sms, num_qps
├── 分配 psum_num_recv_tokens_per_scaleup_rank
├── 分配 psum_num_recv_tokens_per_expert
├── 分配 dst_buffer_slot_idx (non-hybrid) 或 [num_channels][num_scaleout][max_tokens_per_channel][topk] (hybrid)
├── 分配 token_metadata_at_forward (hybrid only)
├── 分配 channel_linked_list (hybrid only)
├── 清理 host_workspace 计数器
├── launch_dispatch(...) → dispatch_impl kernel
│ ├── notify warps: 统计 expert/rank 接收数 → 写 workspace
│ ├── dispatch warps: TMA load x → TMA store recv_buffer/send_buffer → gin.put (RDMA)
│ └── cudaTriggerProgrammaticLaunchCompletion (触发 epilogue)
├── CPU 等待 host_workspace 计数器 (do_cpu_sync)
├── 分配 recv_x, recv_sf, recv_topk_idx, recv_topk_weights, recv_src_metadata
└── launch_dispatch_copy_epilogue → dispatch_copy_epilogue_impl
├── 从 scaleup_buffer TMA load → smem
├── expand 模式: atomicAdd psum expert 计数器 → 分配 slot
└── TMA store → recv_x
7.2 combine 调用链
Python: buffer.combine(x, handle, topk_weights, ...)
↓
C++: ElasticBuffer::combine (csrc/elastic/buffer.hpp:1180)
├── launch_combine(...) → combine_impl kernel
│ ├── 读取 src_metadata 获取 (src_token_idx, src_rank_idx, src_topk_idx)
│ ├── NVLink bypass: TMA store 到对端 scaleup_buffer
│ ├── RDMA: TMA store 到 send_buffer → gin.put
│ └── barrier
├── 分配 combined_x, combined_topk_weights
└── launch_combine_reduce_epilogue → combine_reduce_epilogue_impl
├── 从 comm_buffer TMA load 多个 rank 的数据
├── combine_reduce: 本地归约 (allow_multiple_reduction)
├── 加 bias_0, bias_1
└── TMA store → combined_x
8. 博客模型准确性评估
8.1 映射关系
| 博客术语 | 实际代码对应 | 准确性 |
|---|---|---|
| Token Buffer | 用户输入 Tensor x(不在 EP Buffer 内) |
部分准确——逻辑上是,但物理上不属于 EP Buffer |
| Dispatch Buffer | recv_buffer / scaleup_buffer(按目标 rank 索引) |
准确——Destination-major 布局 |
| Chunk Buffer | 不存在独立 Buffer;chunk 是 warp 调度策略 | 不准确——chunk 是通信粒度,不是独立存储段 |
| NVLink/RDMA Pipeline | gin.put<team_t>() / tma_store_1d() + NCCL 对称内存 |
准确——但"Pipeline"是 gin 硬件抽象 |
| Receive Buffer | recv_buffer(与 Dispatch Buffer 同段) |
混淆——Dispatch/Receive 是同一 Buffer 的两面 |
| Expert Buffer | 用户输出 Tensor recv_x(不在 EP Buffer 内) |
部分准确——逻辑上是,但物理上是 copy epilogue 的输出 |
8.2 博客简化了什么
-
合并了 Dispatch Buffer 与 Receive Buffer:
- 博客描述为两个独立段
- 实际是recv_buffer同一块内存,发送端 TMA store 到对端recv_buffer的某个 slot,接收端从同一recv_buffer读取 -
虚构了 "Chunk Buffer":
- 博客暗示存在一个独立的 chunk 聚合区
- 实际 chunk 是 warp 的调度单位(每次处理kNumTokensPerChannel个 token),数据始终驻留在 rank × token 的二维数组中 -
未区分 user Tensor 与 EP internal Buffer:
- Token Buffer (x) 和 Expert Buffer (recv_x) 是用户分配的 Tensor
- EP Buffer 只包含 workspace + GPU buffer + CPU buffer -
忽略了 Workspace:
- Workspace 是独立的元数据区,承担 barrier、counter、notify 等关键功能
- 博客模型完全没有体现这个"控制平面" -
未区分 Direct 与 Hybrid 模式:
- Hybrid 模式下存在 3 个独立 Buffer 段(scaleup_recv / scaleout_send / scaleout_recv)
- 博客的线性流水线描述无法体现三阶段流水
8.3 博客捕捉到了什么
- Token 是调度粒度:kernel 中
for (int token_idx = token_start; token_idx < num_tokens; token_stride)确实以 token 为单位迭代 - Destination-major 布局:
recv_buffer.get_rank_buffer(dst_rank).get_token_buffer(slot)确实是按目标 rank 索引 - 通信粒度 vs 调度粒度分离:chunk(warp 批量)确实是通信硬件高效利用的关键
9. Mermaid 图:真实 Buffer 层级结构
9.1 ElasticBuffer 物理布局
graph TB
subgraph "NCCL Symmetric Memory (num_sym_bytes)"
W["Workspace<br/>(barrier + counters + tails)<br/>2MB aligned"]
GPU["GPU Buffer<br/>(dispatch/combine 复用)"]
CPU["CPU Buffer<br/>(Engram storage)"]
end
subgraph "GPU Buffer: Direct Mode"
RBUF["recv_buffer<br/>[num_ranks × max_tokens]<br/>TokenLayout"]
SBUF["send_buffer<br/>[1 × max_tokens]<br/>RDMA staging"]
end
subgraph "GPU Buffer: Hybrid Mode"
SRBUF["scaleup_recv_buffer<br/>[num_scaleup_ranks × scaleout×max_tokens]"]
SSOUT["scaleout_send_buffer<br/>[1 × max_tokens]<br/>RDMA staging"]
SRECV["scaleout_recv_buffer<br/>[num_scaleout_ranks × channels×tokens_per_channel]"]
end
subgraph "TokenLayout (per token)"
HID["hidden_bytes"]
SF["sf_bytes<br/>(FP8 scale factors)"]
META["metadata<br/>topk_idx + topk_weights<br/>+ src_token_global_idx<br/>+ linked_list_idx"]
MBAR["mbarrier<br/>(仅 smem staging)"]
end
W --> GPU --> CPU
GPU --> RBUF & SBUF
GPU --> SRBUF --> SSOUT --> SRECV
RBUF --> HID --> SF --> META --> MBAR
9.2 dispatch 数据流
flowchart LR
subgraph "User Space"
X["x<br/>[num_tokens, hidden]"]
RX["recv_x<br/>[num_recv, hidden]"]
end
subgraph "EP Buffer (Direct)"
RB["recv_buffer<br/>[rank][slot]"]
SB["send_buffer<br/>[0][token]"]
end
subgraph "Hardware"
NVL["NVLink<br/>(对称内存直通)"]
RDMA["RDMA<br/>gin.put"]
end
X -->|"TMA load → smem"| RB
X -->|"TMA load → smem"| SB
RB -->|"TMA store → gin.get_sym_ptr"| NVL
SB -->|"gin.put"| RDMA
NVL -->|"远端 RB"| RB
RDMA -->|"远端 RB"| RB
RB -->|"copy epilogue"| RX
9.3 Hybrid dispatch 三阶段流水
flowchart TB
subgraph "Stage 1: Scaleout Send (RDMA)"
X["x<br/>输入"]
SSOUT["scaleout_send_buffer<br/>RDMA staging"]
X -->|"TMA store"| SSOUT
SSOUT -->|"gin.put(ncclTeamTagRail)"| NET["RDMA Network"]
end
subgraph "Stage 2: Forward (NVLink)"
SRECV["scaleup_recv_buffer<br/>[scaleout_rank][slot]"]
SRBUF["scaleup_buffer<br/>[scaleup_rank][slot]"]
NET -->|"远端写入"| SRECV
SRECV -->|"TMA store<br/>轮询各 scaleout peer"| SRBUF
end
subgraph "Stage 3: Copy Epilogue"
RX["recv_x"]
SRBUF -->|"TMA load → expand/sort<br/>TMA store"| RX
end
10. 总结
10.1 博客模型的教学价值
博客的 5 层模型作为第一性原理抽象具有以下价值:
- 清晰分离了"调度粒度 (Token)"与"通信粒度 (Chunk)"
- 突出了 Layout Transformation 的核心地位
- 为理解 Normal vs Low-Latency 的 trade-off 提供了框架
10.2 实际实现的复杂性
实际代码中的 Buffer 系统远比博客复杂:
1. 模板元编程:BufferLayout<kWithMBarrier> 编译期多态
2. 拓扑自适应:Direct vs Hybrid 两种完全不同的分段策略
3. 数据元数据打包:TokenLayout 内 data + metadata 交错
4. 双层同步:Workspace 控制面 + Buffer 数据面
5. SM 间协作:notify warps / dispatch warps / forward warps 各司其职
10.3 核心洞察
DeepEP Buffer 的本质不是"多层转换",而是一个 rank × token 的二维对称内存数组,通过 TMA + NCCL Gin 硬件原语实现零拷贝的布局变换。
博客描述的"Token → Dispatch → Chunk → Network → Receive → Expert"流水线实际上是以下代码路径的逻辑抽象:
用户 x → smem staging → recv_buffer[rank][slot] (NVLink/RDMA) → smem staging → 用户 recv_x
↑ TMA load ↑ TMA store / gin.put ↑ TMA load ↑ TMA store
中间两步(smem staging)是同一个 recv_buffer 的两面,而非两个独立 Buffer。
附录:关键文件索引
| 文件 | 核心内容 |
|---|---|
csrc/elastic/buffer.hpp |
ElasticBuffer C++ 类,分配逻辑,buffer size 计算 |
deep_ep/include/deep_ep/common/layout.cuh |
BufferLayout, TokenLayout, WorkspaceLayout |
deep_ep/include/deep_ep/impls/dispatch.cuh |
Direct dispatch kernel,Buffer 使用 |
deep_ep/include/deep_ep/impls/hybrid_dispatch.cuh |
Hybrid dispatch kernel,三阶段流水 |
deep_ep/include/deep_ep/impls/dispatch_copy_epilogue.cuh |
Copy epilogue,Buffer → 用户 Tensor |
deep_ep/include/deep_ep/impls/combine.cuh |
Combine kernel,数据回传 |
deep_ep/include/deep_ep/impls/hybrid_combine.cuh |
Hybrid combine kernel |
deep_ep/include/deep_ep/impls/combine_reduce_epilogue.cuh |
Reduce epilogue,归约 + bias |
csrc/legacy/buffer.hpp |
Legacy Buffer (NVSHMEM) |
csrc/legacy/config.hpp |
Legacy Config + LowLatencyLayout |
deep_ep/buffers/elastic.py |
Python 封装 |
tests/elastic/test_ep.py |
测试用例,Buffer 使用模式 |