Dispatch/Combine 在 DeepGEMM Mega MoE 内核中的实现与融合
概述
"Every MoE Layer must perform a dynamic data layout transformation."
—— 《Understanding GPU Microarchitecture from a Workload Perspective (3): DeepEP's First Principles》§1
DeepEP 将 MoE 的数据运动分解为 Dispatch(Token-major → Destination-major → Expert-major)与 Combine(Expert-major → Destination-major → Token-major)两个显式阶段。DeepGEMM 的 Mega MoE 内核在 Blackwell SM100 上将这两者全部融合进单个 GPU Kernel,借助 symmetric memory(NVLink)消除显式通信缓冲区,将"动态布局变换"转化为"直接远程读取 + 本地池化写入"。
本文逐行追踪 Mega MoE 内核中 Dispatch/Combine 的实现位置、数据流拓扑、以及博客所述 3-step 变换被"坍缩"的方式。
1. 博客概念回顾:Dispatch / Combine 三步变换
博客 §1.1 给出的抽象模型:
Router 输出: Token → Expert (topk_idx, topk_weights)
Dispatch: Token-major → Destination-major → Expert-major
Combine: Expert-major → Destination-major → Token-major
其中三个角色的 layout 需求:
| 角色 | 期望 Layout |
|------|-------------|
| Communication | Destination-major(按目标 GPU 组织) |
| Expert GEMM | Expert-major(连续的 [M, K] 矩阵) |
| Next Layer | Token-major(按 Token 组织) |
博客强调:"This is not simple communication — it is a dynamic data layout transformation."
2. Mega MoE 内核整体架构:三组 Warp Role
Mega MoE 将线程分为三组截然不同的 Warp 角色,在一个 kernel 内完成全部工作:
// sm100_fp8_fp4_mega_moe.cuh:356-360
if (warp_idx < kNumDispatchWarps) {
// Dispatch warps: 负责 Token 拉取与布局变换
} else if (warp_idx == kNumDispatchWarps) {
// GEMM TMA load warp (tokens + SFA)
} else if (warp_idx == kNumDispatchWarps + 1) {
// GEMM TMA load warp (weights + SFB)
} else if (warp_idx == kNumDispatchWarps + 2) {
// GEMM MMA issue warp
} else if (warp_idx >= kNumDispatchWarps + kNumMMANonEpilogueWarps) {
// Epilogue warps: 负责 GEMM 后处理 + Combine
}
graph TB
subgraph "Mega MoE Single Kernel"
D["Dispatch Warps<br/>(Token → Expert Pool)"]
TMA1["TMA Load Warp<br/>(Tokens + SFA)"]
TMA2["TMA Load Warp<br/>(Weights + SFB)"]
MMA["MMA Issue Warp<br/>(Tensor Core 计算)"]
EPI["Epilogue Warps<br/>(SwiGLU / NVLink 回写 / Combine)"]
end
D -->|"l1_token_buffer<br/>(Expert Pool)"| TMA1
TMA1 -->|"smem_a"| MMA
TMA2 -->|"smem_b"| MMA
MMA -->|"TMEM accum"| EPI
EPI -->|"combine_token_buffer"| EPI
EPI -->|"y (Token-major)"| OUT["Output"]
3. Dispatch 在哪里?——融合进 Dispatch Warps
3.1 结论:Dispatch 不是独立步骤,而是 kernel 内的前半段
在 DeepEP 中,Dispatch 是一个独立的通信 kernel,显式产出 Dispatch Buffer(Destination-major)和 Expert Buffer(Expert-major)。
在 Mega MoE 中,Dispatch 被坍缩为 kernel 内 Dispatch Warps 的一段协作逻辑:
// sm100_fp8_fp4_mega_moe.cuh:356-649
if (warp_idx < kNumDispatchWarps) {
cutlass::arch::warpgroup_reg_dealloc<kNumDispatchRegisters>();
// 1. 读取 topk_idx,按 expert 计数
read_topk_idx([&](const uint32_t& token_topk_idx, const int& expert_idx) {
atomicAdd_block(smem_expert_count + expert_idx, 1);
});
// 2. 全局 offset 分配 (atomicAdd on workspace)
// ... ptx::atomic_add(workspace.get_expert_send_count_ptr(i), send_value)
// 3. 将 src_token_topk_idx 写入远端 rank 的 workspace (NVLink)
read_topk_idx([&](const uint32_t& token_topk_idx, const int& expert_idx) {
const auto dst_rank_idx = expert_idx / kNumExpertsPerRank;
const auto dst_slot_idx = atomicAdd_block(smem_expert_count + expert_idx, 1);
const auto dst_ptr = workspace.get_src_token_topk_idx_ptr(
expert_idx % kNumExpertsPerRank, sym_buffer.rank_idx, dst_slot_idx);
*sym_buffer.map(dst_ptr, dst_rank_idx) = token_topk_idx; // 远端写入!
});
// 4. Grid sync + NVLink barrier (跨 rank 同步)
// 5. Pull token data: 从远端 rank 的 input_token_buffer 读到本地 l1_token_buffer
ptx::tma_load_1d(
pull_buffer.get_base_ptr(),
sym_buffer.map(input_token_buffer.get_data_buffer(src_token_idx).get_base_ptr(),
current_rank_in_expert_idx), // 远端地址
pull_mbarrier, kHidden);
// 6. 存储 topk_weights 与 src_metadata (供 Combine 使用)
*workspace.get_token_src_metadata_ptr(pool_token_idx) =
{current_rank_in_expert_idx, src_token_idx, src_topk_idx};
}
3.2 Dispatch 的数据流(坍缩版本)
博客的 3-step 变换在 Mega MoE 中被坍缩为 1 步:
flowchart LR
subgraph "DeepEP (3步显式)"
A1["Token Buffer<br/>(Token-major)"]
A2["Dispatch Buffer<br/>(Destination-major)"]
A3["Expert Buffer<br/>(Expert-major)"]
A1 --> A2 --> A3
end
subgraph "Mega MoE (1步坍缩)"
B1["远端 input_token_buffer<br/>(Token-major)"]
B2["本地 l1_token_buffer<br/>(Expert Pool / Expert-major)"]
B1 -->|"TMA Load + symmetric memory"| B2
end
关键洞察:Mega MoE 完全跳过了 Destination-major 中间缓冲区。Dispatch Warps 直接从远端 rank 的 Token-major buffer(input_token_buffer)通过 NVLink TMA load 写入本地按 Expert 分块的 pool buffer(l1_token_buffer)。"Destination" 信息被编码在:
- 远端 rank 选择:sym_buffer.map(ptr, dst_rank_idx)
- 本地 Expert 池偏移:expert_pool_block_offset * BLOCK_M + token_idx_in_expert
4. Token → Expert → Destination 变换的具体实现
4.1 topk_idx 的双重作用
topk_idx 是 Router 输出的 Token→Expert 映射,在 Mega MoE 中被两个阶段消费:
Dispatch 阶段(决定 Token 从哪来):
// sm100_fp8_fp4_mega_moe.cuh:372-376
expert_idx = static_cast<int>(
__ldg(input_topk_idx_buffer.get_base_ptr<int64_t>() + i * kNumTopk + lane_idx));
if (expert_idx >= 0)
process(i * kNumTopk + lane_idx, expert_idx);
Combine 阶段(决定结果写回哪):
// sm100_fp8_fp4_mega_moe.cuh:1183-1187
const auto src_metadata = *workspace.get_token_src_metadata_ptr(m_idx + m_idx_in_block);
const uint32_t dst_rank_idx = src_metadata.rank_idx;
const uint32_t dst_token_idx = src_metadata.token_idx;
const uint32_t dst_topk_idx = src_metadata.topk_idx;
4.2 topk_weights 的作用
topk_weights 在 L1 epilogue 中被用于加权 SwiGLU 输出:
// sm100_fp8_fp4_mega_moe.cuh:984-1011
// Apply SwiGLU: silu(gate) * up
gate = __fmul2_rn(gate, {math::fast_rcp(denom.x), math::fast_rcp(denom.y)});
const auto up = __bfloat1622float2(bf16_up);
swiglu_values[i * 2 + k] = __fmul2_rn(__fmul2_rn(gate, up), weights);
// ^^^^^^
// 来自 topk_weights 的 gate weight
这对应博客 §1.2 中的语义恢复:
"Output(T0) = 0.73 × Expert2(T0) + 0.27 × Expert7(T0)"
在 Mega MoE 中,这个权重被提前应用到 L1 输出(per-expert 加权),而 L2 输出到 combine buffer 时是 BF16 无权重,最终 Combine 只做 topk 累加。
5. Combine 在哪里?——融合进 Epilogue Warps
5.1 Combine 的两阶段实现
阶段 A:L2 Epilogue 将结果写入远端 combine buffer
// sm100_fp8_fp4_mega_moe.cuh:1196-1202
const auto dst_token = combine_token_buffer.get_rank_buffer(dst_topk_idx)
.get_data_buffer(token_idx);
const auto dst_ptr = math::advance_ptr<float4>(...);
*sym_buffer.map(dst_ptr, dst_rank_idx) = packed; // NVLink 写入远端
阶段 B:本地 Combine 累加 + 写回 Token-major 输出
// sm100_fp8_fp4_mega_moe.cuh:1226-1356
// Combine: reduce top-k results and write back
for (uint32_t token_idx = sm_idx * kNumEpilogueWarps + epilogue_warp_idx;
token_idx < num_tokens;
token_idx += kNumSMs * kNumEpilogueWarps) {
// 读取 topk_idx 确定该 token 有几个 expert 贡献
const int stored_topk_slot_idx = lane_idx < kNumTopk ?
static_cast<int>(__ldg(input_topk_idx_buffer.get_base_ptr<int64_t>()
+ token_idx * kNumTopk + lane_idx)) : -1;
// 遍历 topk,从 combine_token_buffer[slot_idx] 加载并累加
for (uint32_t chunk = 0; chunk < kNumChunks; ++ chunk) {
float2 reduced[kNumUint4PerLane * kNumElemsPerUint4] = {};
while (do_reduce) {
// Prefetch next top-k
do_reduce = move_mask_and_load(load_stage_idx ^ 1);
// Accumulate
for (uint32_t j = 0; j < kNumUint4PerLane; ++ j)
ptx::accumulate(reduced[j * kNumElemsPerUint4 + l], bf16_values[l]);
}
// Cast to BF16, TMA store 到 y[token_idx] (Token-major)
ptx::tma_store_1d(
math::advance_ptr(y, static_cast<uint64_t>(token_idx) * kNumHiddenBytes + ...),
combine_store_buffer, kNumChunkBytes);
}
}
5.2 Combine 数据流
flowchart TB
subgraph "Combine Phase A: L2 Epilogue → Remote Combine Buffer"
L2["L2 GEMM Output<br/>(本地 Expert Pool)"]
L2 -->|"STSM + TMA Store"| CB["combine_token_buffer<br/>[topk, num_tokens] BF16<br/>via NVLink to remote rank"]
end
subgraph "Combine Phase B: Local Reduction → Token-major Output"
CB -->|"TMA Load"| ACC["Accumulate topk<br/>in float registers"]
ACC -->|"Cast BF16 + TMA Store"| Y["y[token_idx]<br/>(Token-major)"]
end
6. Symmetric Memory 如何替代显式通信
6.1 核心机制
// sym_buffer.cuh:34-37
template <typename ptr_t>
CUTLASS_DEVICE ptr_t map(const ptr_t& ptr, const uint32_t& dst_rank_idx) const {
int64_t mapped_ptr = offsets[dst_rank_idx] + reinterpret_cast<int64_t>(ptr);
return *reinterpret_cast<ptr_t*>(&mapped_ptr);
}
每个 GPU 持有一份 symmetric buffer,通过 sym_buffer.map(ptr, dst_rank_idx) 将本地指针映射到远端 rank 的 NVLink 可访问地址。
6.2 对比 DeepEP 的通信模型
| 维度 | DeepEP | Mega MoE |
|---|---|---|
| 通信缓冲区 | Dispatch Buffer / Receive Buffer / Expert Buffer | 无独立缓冲区 |
| 通信原语 | 显式 all-to-All / RDMA | sym_buffer.map() + TMA load/store |
| 同步 | FIFO + Barrier | NVLink barrier (nvlink_barrier) |
| 数据粒度 | Chunk(聚合后发送) | 单 Token TMA load |
博客 §4.3 描述的 "GPU-centric communication fabric: NVLink + RDMA + GPU SM together form the data path" 在 Mega MoE 中被推向极致:SM 直接执行 TMA 指令访问远端显存,无需独立通信 kernel。
6.3 三次 NVLink 同步点
// sm100_fp8_fp4_mega_moe.cuh:430-436
comm::nvlink_barrier<kNumRanks, kNumSMs, kNumDispatchThreads,
kDispatchGridSyncIndex, kBeforeDispatchPullBarrierTag>(...);
// sm100_fp8_fp4_mega_moe.cuh:643-649
comm::nvlink_barrier<kNumRanks, kNumSMs, kNumDispatchThreads,
kDispatchGridSyncIndex, kAfterWorkspaceCleanBarrierTag>(...);
// sm100_fp8_fp4_mega_moe.cuh:1217-1221
comm::nvlink_barrier<kNumRanks, kNumSMs, kNumEpilogueThreads,
kEpilogueGridSyncIndex, kBeforeCombineReduceBarrierTag>(...);
对应三个阶段的跨 rank 同步:
1. kBeforeDispatchPullBarrierTag:确保所有 rank 写完 src_token_topk_idx 后再开始 pull
2. kAfterWorkspaceCleanBarrierTag:确保 workspace 清理完成后再进入 epilogue
3. kBeforeCombineReduceBarrierTag:确保所有 rank 写完 combine buffer 后再开始 reduce
7. 博客 3-step 变换的坍缩分析
7.1 变换路径对比
flowchart TB
subgraph "DeepEP: 显式 3-step"
direction TB
T1["Token Buffer<br/>Token-major"]
D1["Dispatch Buffer<br/>Destination-major"]
E1["Expert Buffer<br/>Expert-major"]
T1 -- "Step 1: 按目标 GPU 分组" --> D1
D1 -- "Step 2: 按 Expert 分组" --> E1
end
subgraph "Mega MoE: 坍缩 1-step"
direction TB
T2["远端 input_token_buffer<br/>Token-major"]
E2["本地 l1_token_buffer<br/>Expert Pool<br/>(Expert-major)"]
T2 -- "直接 TMA Load<br/>+ sym_buffer.map" --> E2
end
7.2 为什么可以坍缩?
- Symmetric memory 消除了"Destination"的物理边界:远端 Token buffer 可通过 NVLink 直接访问,无需先搬到本地
- TMA 硬件支持 2D 跨步加载:可以直接从远端 Token-major buffer 按
src_token_idx索引加载,无需中间转置 - Expert Pool 布局天然是 Expert-major:
l1_token_buffer按[num_max_pool_tokens, hidden]组织,每个 Expert 占据连续的ceil_div(num_tokens, BLOCK_M) * BLOCK_M个 slot
7.3 坍缩后的代价转移
| 被消除的 | 新增的 |
|---|---|
| Dispatch Buffer 分配 | Workspace 元数据(expert count, src_token_topk_idx) |
| Destination-major 中间布局 | sym_buffer.map() 地址计算 |
| 显式通信 kernel 启动开销 | NVLink barrier 同步 |
| 多次 HBM 读写 | TMA 直传(远端 HBM → 本地 SMEM) |
8. 什么被融合了?Mega MoE vs DeepEP 对比
8.1 融合全景图
flowchart LR
subgraph "DeepEP (多 kernel)"
D1["Dispatch<br/>Kernel"]
C1["Communicate<br/>(NVLink/RDMA)"]
G1["Expert GEMM<br/>Kernel"]
CO1["Combine<br/>Kernel"]
D1 --> C1 --> G1 --> CO1
end
subgraph "Mega MoE (单 kernel)"
D2["Dispatch Warps"]
G2["MMA Warps<br/>(L1 GEMM + SwiGLU + L2 GEMM)"]
CO2["Epilogue Warps<br/>(Combine)"]
D2 --> G2 --> CO2
end
8.2 详细功能对照
| 功能 | DeepEP | Mega MoE |
|---|---|---|
| Token → Expert 映射 | Dispatch kernel 显式排序 | Dispatch Warps 直接 pull |
| 跨节点通信 | RDMA all-to-All | sym_buffer.map() TMA |
| Expert GEMM | 独立 GEMM kernel | MMA Warps 内联 |
| 激活函数 | 独立 SwiGLU kernel | L1 Epilogue 内联 |
| Expert → Token 恢复 | Combine kernel 显式 reduce | Epilogue Warps 内联 |
| 通信-计算重叠 | Chunk 流水线 | Warp 角色间 barrier 同步 |
8.3 关键代码位置索引
| 博客概念 | Mega MoE 代码位置 | 说明 |
|---|---|---|
| Token-major input | input_token_buffer (line 107-110) |
FP8, [num_max_tokens_per_rank, hidden] |
| topk_idx 读取 | read_topk_idx lambda (line 363-380) |
Dispatch Warps 消费 |
| Expert 计数 | atomicAdd_block(smem_expert_count + expert_idx, 1) (line 384) |
Counting sort 的 count 阶段 |
| 全局 offset | ptx::atomic_add(workspace.get_expert_send_count_ptr(i), ...) (line 393) |
Prefix sum + scatter |
| 远端写入 | *sym_buffer.map(dst_ptr, dst_rank_idx) = token_topk_idx (line 403) |
NVLink 直接写入 |
| Token pull | ptx::tma_load_1d(pull_buffer, sym_buffer.map(...), ...) (line 546-550) |
远端 Token → 本地 SMEM |
| Expert Pool 存储 | ptx::tma_store_1d(l1_token_buffer.get_data_buffer(pool_token_idx), ...) (line 585-587) |
SMEM → 本地 Expert Pool |
| L1 GEMM | ptx::SM100_MMA_MXF8F6F4_2x1SM_SS::fma(...) (line 853-856) |
Tensor Core |
| SwiGLU | swiglu_values[i * 2 + k] = __fmul2_rn(__fmul2_rn(gate, up), weights) (line 1011) |
L1 Epilogue |
| L2 GEMM | 同上 MMA issue (line 820-864) | 第二层 GEMM |
| Combine buffer 写入 | *sym_buffer.map(dst_ptr, dst_rank_idx) = packed (line 1202) |
NVLink 回写 |
| Combine reduce | ptx::accumulate(reduced[...], bf16_values[l]) (line 1321) |
topk 累加 |
| Token-major 输出 | ptx::tma_store_1d(math::advance_ptr(y, token_idx * kNumHiddenBytes), ...) (line 1349-1350) |
最终输出 |
9. 核心结论
9.1 Dispatch/Combine 的命运
"Dispatch performs: Token-major → Destination-major → Expert-major"
在 Mega MoE 中,这个 3-step 变换被坍缩为 1 步:
- Token-major → Expert-major:通过 sym_buffer.map() 直接远端 Token 读取 + 本地 Expert Pool 写入
- Destination-major 被完全消除:不再存在按 GPU 分组的中间缓冲区,"Destination" 被编码在 NVLink 地址映射中
9.2 融合的本质
Mega MoE 的融合不是简单的"把多个 kernel 拼在一起",而是利用 symmetric memory 重新定义了数据所有权:
- 数据不再"移动",而是"被访问":Token 留在原 rank 的 buffer 中,远端 SM 通过 TMA 直接读取
- 布局变换不再"分步执行",而是"即时计算":从 Token-major 到 Expert-major 的映射在 Dispatch Warps 的寄存器中完成
- 通信不再"独立阶段",而是"计算 kernel 的内存操作":NVLink barrier 替代了显式通信 kernel 的启动/同步
9.3 与博客论点的呼应
"DeepEP represents MoE systems' evolution from 'communication optimization' to 'dataflow execution.'" (§10)
Mega MoE 延续并深化了这一演化:
- DeepEP:将通信优化为 dataflow pipeline(Chunk → FIFO → Warp Specialization)
- Mega MoE:将 dataflow 进一步融合为 single-kernel dataflow execution(symmetric memory + TMA + Warp Role)
博客 §9 的总结精准预见了这一方向:
"System trends are further fusing Communication + Compute. In DeepGEMM, Mega MoE Kernel targets Blackwell SM100, fusing: Token Routing → Dispatch → Linear1 → Activation → Linear2 → Combine"
附录:关键数据结构布局
A.1 Symmetric Buffer 布局(每个 rank 一份)
┌─────────────────────────────────────────────────────────────┐
│ Workspace (barrier, expert count, src_token_topk_idx, ...) │
├─────────────────────────────────────────────────────────────┤
│ input_token_buffer [num_max_tokens_per_rank, hidden] │ ← Token-major (FP8)
├─────────────────────────────────────────────────────────────┤
│ input_sf_buffer [num_max_tokens_per_rank, hidden/128] │
├─────────────────────────────────────────────────────────────┤
│ input_topk_idx [num_max_tokens_per_rank, num_topk] │
├─────────────────────────────────────────────────────────────┤
│ input_topk_weights [num_max_tokens_per_rank, num_topk] │
├─────────────────────────────────────────────────────────────┤
│ l1_token_buffer [num_max_pool_tokens, hidden] │ ← Expert Pool (FP8)
├─────────────────────────────────────────────────────────────┤
│ l1_sf_buffer [num_padded_sf_pool_tokens, hidden/128│
├─────────────────────────────────────────────────────────────┤
│ l1_topk_weights [num_max_pool_tokens, 1] │
├─────────────────────────────────────────────────────────────┤
│ l2_token_buffer [num_max_pool_tokens, intermediate] │ ← L1 输出 / L2 输入
├─────────────────────────────────────────────────────────────┤
│ l2_sf_buffer [num_padded_sf_pool_tokens, ...] │
├─────────────────────────────────────────────────────────────┤
│ combine_token_buffer [num_topk, num_max_tokens_per_rank] │ ← Combine 中间结果 (BF16)
└─────────────────────────────────────────────────────────────┘
A.2 Expert Pool 内部布局
l1_token_buffer (Expert-major pool):
┌──────────────────────────────────────────┐
│ Expert 0: token_0, token_1, ..., token_n │ ← pool_block_offset = 0
│ Expert 1: token_0, token_1, ..., token_m │ ← pool_block_offset = ceil_div(n, BLOCK_M)
│ Expert 2: ... │
│ ... │
└──────────────────────────────────────────┘
每个 Expert 的 slot 数对齐到 BLOCK_M(padding 用于 TMA 对齐)
分析基于 DeepGEMM 源码(commit 待确认)与博客《Understanding GPU Microarchitecture from a Workload Perspective (3): DeepEP's First Principles》