ZB (Zero-Buffer) Combine 算子深度分析
项目: UMDK/CAM — Communication Acceleration for Matrix on Ascend NPU
分支:cam_feature(remotes/origin/cam_feature)
分析日期: 2026-07-21
分析范围:moe_combine_normal_zb+moe_combine_lowlatency_zb两个 Zero-Buffer Combine 算子
目录
- ZB Combine 设计哲学
- moe_combine_normal_zb 完整分析
- moe_combine_lowlatency_zb 完整分析
- Combine 阶段的 SHMEM 通信
- 与 master 版 moe_combine_normal 的对比
- Tiling 策略
- 关键数据结构与内存布局
1. ZB Combine 设计哲学
1.1 Zero-Buffer 的核心思想
Zero-Buffer 是 CAM 库在 cam_feature 分支上引入的全新 MoE 通信范式。其核心洞察是:
传统 MoE Combine 需要两次数据搬运:
1. 每个 rank 将本地 GMM 输出 CopyBufferToShare 写入远端 rank 的 HCCL Window
2. 通过 polling WaitBuffCopy 等待所有源 rank 的数据到位
3. 最后执行加权求和
Zero-Buffer 将其简化为一次直接读取:
- 数据在 GMM 完成后直接写入 SHMEM window(由 dispatch 阶段完成)
- Combine 阶段通过 shmem_ptr() 直接读取远端 SHMEM 中的数据
- 通过 ZeroBufferSyncFlag 的 magic + phase 协议保证数据可见性
1.2 设计原则
| 原则 | 说明 |
|---|---|
| 零拷贝 | Combine 不需要额外的 buffer 中转,直接从 SHMEM window 读取 |
| Magic 单调递增 | 每个 kernel 入口调用 IncrementMagic(),保证 flag 唯一性 |
| Ping-Pong 双缓冲 | magic % 2 选择 buffer,避免连续 kernel 调用间的 flag 冲突 |
| MTE3 Fence 保序 | SetFlag 前强制 MTE3 fence,确保数据写入先于 flag 可见 |
| 全 AIV 并行 | 所有 AIV 核同时参与计算,SplitCoreCal 均匀分配 token |
1.3 文件结构
moe_combine_normal_zb/
├── op_kernel/ # 设备端 kernel 代码
│ ├── moe_combine_normal_zero_buffer.cpp # kernel 入口 + 实现
│ ├── moe_combine_normal_zero_buffer.h # 类定义
│ └── moe_combine_normal_zero_buffer_tiling.h # tiling 数据结构
├── op_host/ # 主机端 tiling 代码
│ ├── moe_combine_normal_zero_buffer.cpp # tiling 入口
│ └── moe_combine_normal_zero_buffer_tiling.cc # tiling 实现
└── op_api/ # aclnn API 注册
├── aclnn_moe_combine_normal_zero_buffer.cpp
└── aclnn_moe_combine_normal_zero_buffer.h
moe_combine_lowlatency_zb/
├── op_kernel/
│ ├── moe_combine_lowlatency_zero_buffer.cpp
│ ├── moe_combine_lowlatency_zero_buffer.h
│ └── moe_combine_lowlatency_zero_buffer_tiling.h
├── op_host/
│ ├── moe_combine_lowlatency_zero_buffer.cpp
│ └── moe_combine_lowlatency_zero_buffer_tiling.cpp
└── op_api/
├── aclnn_moe_combine_lowlatency_zero_buffer.cpp
└── aclnn_moe_combine_lowlatency_zero_buffer.h
utils/op_kernel/ # 公共工具
├── zero_buffer_api.h # SHMEM API 封装 (include shmem.h)
├── zero_buffer_sync_flag.h # 跨卡 flag 同步机制
├── comm_args_zb.h # ZB 版通信参数
├── sync_collectives_zb.h # 同步原语
├── moe_lowlatency_zero_buffer_base.h # lowlatency 共享工具
└── data_copy_zb.h # 数据拷贝辅助
2. moe_combine_normal_zb 完整分析
2.1 算子签名与 I/O
Kernel 入口 (moe_combine_normal_zero_buffer.cpp):
extern "C" __global__ __aicore__ void moe_combine_normal_zero_buffer(
GM_ADDR recvX, // [bs*k, h] 本 rank 收到的所有 token(各 expert 输出)
GM_ADDR epRecvCount, // [moeExpertNum, epWorldSize] 每个 expert 从各 rank 收到的 token 数
GM_ADDR topkWeights, // [bs, k] top-k 权重
GM_ADDR topkIdx, // [bs, k] top-k expert ID
GM_ADDR sendTokenIdx, // [bs, k] 在远端 recvX 中的偏移索引
GM_ADDR probGrad, // [bs, h] 概率梯度输入(可选)
GM_ADDR XOut, // [bs, h] 输出
GM_ADDR sendCostStatsOut,// [epWorldSize] 诊断统计(可选)
GM_ADDR gradOut, // [bs, k] 梯度输出(可选)
GM_ADDR workspaceGM,
GM_ADDR tilingGM)
关键输入语义:
- recvX: 本 rank 上所有 expert 的 GMM 输出,按 [token_idx, topk_idx] 排列
- epRecvCount: 全局接收计数,epRecvCount[expertId][rankId] 表示该 expert 从 rankId 收到的 token 数
- sendTokenIdx: 每个 (token, topk) 对在其目标 rank 的 recvX 中的位置索引
2.2 执行流程
Process():
│
├── syncFlag_.IncrementMagic() // Step 1: 获取 magic M,递增
├── syncFlag_.BarrierAll() // Step 2: 全 rank 屏障,确保所有 GMM 完成
├── ReadBufferFromRemote() // Step 3: 从远端 SHMEM 读取并加权求和
└── SyncAll<true>() // Step 4: 全核同步
Step 1: IncrementMagic
__aicore__ inline void IncrementMagic() {
GM_ADDR magicAddr = flagBaseGM_ + coreIdx_ * MAGIC_SLOT_ALIGN; // 512B 对齐
// DataCacheCleanAndInvalid → 读 magic
magic_ = static_cast<int32_t>(magicGT(0));
magicGT(0) = magic_ + 1; // 递增
// DataCacheCleanAndInvalid → 写回
barrierSeq_ = 0;
}
每个 core 有独立的 magic slot(512B 对齐),避免 false sharing。
Step 2: BarrierAll
__aicore__ inline void BarrierAll() {
SyncAll<true>(); // 核内同步
SplitCoreCal(epWorldSize_, ...); // 分配远端 rank
// Write: 向所有分配的远端 rank 写 barrier flag
for (r = startRank; r < endRank; r++) {
DataCopy(remoteSlot, ubFlag, ...); // 写 flag 到远端 SHMEM
}
// Wait: 轮询本地 slot 等待所有远端 rank 的 flag
for (r = startRank; r < endRank; r++) {
do { observed = localSlot; } while (observed != barrierFlag);
}
SyncAll<true>();
}
关键保证: BarrierAll 返回时,所有 rank 的 GMM 输出已完整写入 SHMEM。
Step 3: ReadBufferFromRemote
__aicore__ inline void ReadBufferFromRemote() {
SplitCoreCal(axisBS_, ...); // 将 bs 个 token 均匀分配到各 AIV core
// 初始化所有 buffer
tpipe_->InitBuffer(xOutBuf_, h32AlignRecvXLen_); // 14KB (h=7168, bf16)
tpipe_->InitBuffer(tokenFloatBuf_, h32AlignFloatLen_); // 28KB (fp32)
tpipe_->InitBuffer(weightedMulBuf_, h256AlignFloatLen_); // 28KB
tpipe_->InitBuffer(sumFloatBuf_, h32AlignFloatLen_); // 28KB
tpipe_->InitBuffer(weightedSumQueue_, DOUBLE_BUFFER, h32AlignRecvXLen_); // 28KB
// ... topkWeightsBuf_, sendTokenIdxBuf_, topkIdxBuf_, allRecvCountBuf_
// 读取全局 recvCount 到本地
DataCopyPad(allRecvCountLocal, epRecvCountGT_, ...);
for (tokenIndex = startTokenIndex; tokenIndex < endTokenIndex; tokenIndex++) {
// 读取当前 token 的 topkWeights, topkIdx, sendTokenIdx
DataCopyPad(topkWeightsLocal, topkWeightsGT_[tokenIndex * axisK_], ...);
DataCopyPad(topkIdxLocal, topkIdxGT_[tokenIndex * axisK_], ...);
DataCopyPad(sendTokenIdxLocal, sendTokenIdxGT_[tokenIndex * axisK_], ...);
ReadBufferAndWeightedSum(tokenIndex, startTokenIndex);
}
}
2.3 加权求和核心逻辑
__aicore__ inline void ReadBufferAndWeightedSum(uint32_t tokenIndex, uint32_t startTokenIndex) {
Duplicate(sumFloatBufLocal, 0.0f, axisH_); // 清零累加器
for (uint32_t topkId = 0U; topkId < axisK_; topkId++) {
float scale = topkWeightsLocal.GetValue(topkId);
int32_t expertId = topkIdxLocal.GetValue(topkId);
int32_t remoteReadOffset = sendTokenIdxLocal(topkId);
int32_t remoteReadBase = allRecvCountLocal(expertId * epWorldSize_ + epRankId_);
uint64_t remoteReadAddr = (remoteReadBase + remoteReadOffset) * hRecvXTypeLen_;
int32_t dstRankId = expertId / moeExpertPerRankNum_;
auto ptr = reinterpret_cast<__gm__ uint8_t *>(shmem_ptr(recvXGM_, dstRankId));
dstGT.SetGlobalBuffer((__gm__ XType *)(ptr + remoteReadAddr));
// 从远端 SHMEM 读取 token
LocalTensor<XType> tmpToken = weightedSumQueue_.AllocTensor<XType>();
DataCopyPad(tmpToken, dstGT, xOutCopyParams, ...);
weightedSumQueue_.EnQue(tmpToken);
tmpToken = weightedSumQueue_.DeQue<XType>();
// bf16 → fp32 → 乘权重 → 累加
Cast(tokenFloatLocal, tmpToken, CAST_NONE, axisH_); // bf16 → fp32
Muls(weightedMulBufLocal, tokenFloatLocal, scale, axisH_); // × weight
Add(sumFloatBufLocal, sumFloatBufLocal, weightedMulBufLocal, axisH_); // 累加
weightedSumQueue_.FreeTensor<XType>(tmpToken);
}
// fp32 → bf16 → 写输出
Cast(xOutLocal, sumFloatBufLocal, CAST_RINT, axisH_);
SyncFunc<V_MTE3>();
DataCopyPad(xOutGlobal_[tokenIndex * axisH_], xOutLocal, ...);
}
加权求和公式:
$$X_{out}[i] = \text{Cast}{fp32 \to bf16}\left(\sum{j=0}^{K-1} w_j \cdot \text{Cast}{bf16 \to fp32}(X{recv}[expert_j, token_j])\right)$$
2.4 地址计算详解
对于 token i 的第 j 个 top-k 选择:
expertId = topkIdx[i][j] // 目标 expert
dstRankId = expertId / moeExpertPerRankNum_ // expert 所在的 rank
remoteReadBase = epRecvCount[expertId][epRankId_] // 本 rank 在该 expert 上的基址偏移
remoteReadOffset = sendTokenIdx[i][j] // 在该 expert 接收列表中的位置
remoteReadAddr = (remoteReadBase + remoteReadOffset) * hRecvXTypeLen_
// 最终地址 = 远端 rank 的 SHMEM window + remoteReadAddr
finalAddr = shmem_ptr(recvXGM_, dstRankId) + remoteReadAddr
2.5 UB 内存预算
| Buffer | 大小 | 说明 |
|---|---|---|
xOutBuf_ |
14KB | 输出 token (h=7168 × bf16) |
tokenFloatBuf_ |
28KB | fp32 中间结果 |
weightedMulBuf_ |
28KB | 256 对齐的乘法结果 |
sumFloatBuf_ |
28KB | 累加器 |
weightedSumQueue_ |
28KB | 双缓冲 DMA queue |
topkWeightsBuf_ |
32B | k 个 float 权重 |
sendTokenIdxBuf_ |
32B | k 个 int32 索引 |
topkIdxBuf_ |
32B | k 个 int32 expert ID |
allRecvCountBuf_ |
128KB | epWorldSize × moeExpertNum × 4B (max 512×64×4=128KB) |
| 总计 | ~254KB |
3. moe_combine_lowlatency_zb 完整分析
3.1 算子签名与 I/O
Kernel 入口 (moe_combine_lowlatency_zero_buffer.cpp):
extern "C" __global__ __aicore__ void moe_combine_lowlatency_zero_buffer(
GM_ADDR expandX, // [A*tpWorldSize, h] 展开的 expert 输出
GM_ADDR expertIds, // [bs, k] top-k expert ID
GM_ADDR assistInfoForCombine, // [A*128] 辅助信息
GM_ADDR epSendCount, // [moeExpertNum*epWorldSize] EP 发送计数
GM_ADDR scales, // [bs, k] top-k 权重
GM_ADDR tpSendCount, // [tpWorldSize] TP 发送计数
GM_ADDR xActiveMask, // [bs] 或 [bs, k] 活跃 mask(可选)
GM_ADDR activationScale, // 预留
GM_ADDR weightScale, // 预留
GM_ADDR groupList, // 预留
GM_ADDR expandScales, // 预留
GM_ADDR sharedExpertX, // [bs, h] 共享专家输出(可选)
GM_ADDR elasticInfo, // 弹性信息(可选)
GM_ADDR oriX, // [bs, h] 原始输入(copy expert 用)
GM_ADDR constExpertAlpha1, // [constExpertNum] 常量专家参数
GM_ADDR constExpertAlpha2, // [constExpertNum]
GM_ADDR constExpertV, // [constExpertNum, h]
GM_ADDR XOut, // [bs, h] 输出
GM_ADDR workspaceGM, GM_ADDR tilingGM)
TilingKey 编码:
10000: tp=1, IsInt8Quant=0
10100: tp=2, IsInt8Quant=0
10020: tp=1, IsInt8Quant=1
10120: tp=2, IsInt8Quant=1
3.2 执行流程
Process():
│
├── CopyValidExpandXToRemote() // Step 1: 拷贝本地 expert 数据到 SHMEM
├── SyncAll<true>() // Step 2: 全核同步
├── tpipe_->Reset() // 重置 pipe
├── SetSyncFlag() // Step 3: 向远端 rank 写 flag
├── WaitSyncFlag() // Step 4: 轮询等待远端 flag
├── SyncAll<true>() // Step 5: 全核同步
└── InputToDstOutput() // Step 6: 从 SHMEM 读取并加权求和
Step 1: CopyValidExpandXToRemote
__aicore__ inline void CopyValidExpandXToRemote() {
if (!needCopyExpandX_) return; // oriX==0 或 validTokenNum==0 时跳过
SplitToCore(validTokenNum_, aivNum_, ...); // 分配 token
tpipe_->InitBuffer(copyInBuf_, COPY_BUFFER_NUM * GM2GM_COPY_BLOCK_SIZE); // 2×64KB
// 双缓冲 GM→GM 拷贝:oriX → expandX (SHMEM)
for (i = 0; dataSizeRemain > 0; ++i) {
copySize = min(dataSizeRemain, 64KB);
WaitFlag<MTE3_MTE2>(eventId); // 等待 DMA 通道
DataCopyPad(copyLocal[bufferOffset], oriXByteGT[srcOffset], ...); // GM→UB
SetFlag<MTE2_MTE3>(eventId);
WaitFlag<MTE2_MTE3>(eventId);
DataCopyPad(expandXByteGT[dstOffset], copyLocal[bufferOffset], ...); // UB→GM(SHMEM)
SetFlag<MTE3_MTE2>(eventId);
}
}
注意: 这是 lowlatency 特有的步骤。因为 lowlatency 模式下,本地 expert 的输出需要从 oriX(原始位置)拷贝到 expandX(SHMEM window)以供远端 rank 读取。normal_zb 不需要此步骤,因为数据已在 dispatch 阶段直接写入 SHMEM。
Step 3-4: SetSyncFlag / WaitSyncFlag
__aicore__ inline void SetSyncFlag() {
// 构造 flag 值:0x3F800000 = 1.0f (IEEE 754)
Duplicate(statusTensor, 0, rankNumPerBlock_ * 8);
Duplicate(statusTensor, 0x3F800000, mask, ...); // 按 mask 设置 1.0
for (targetRankId = curBlockStartRankId_; targetRankId < curBlockEndRankId_; ++targetRankId) {
auto ptr = GetSyncFlagAddrByRankId(targetRankId) + epRankId_ * COMBINE_SYNC_STATE_OFFSET;
gmRemoteStatusGT.SetGlobalBuffer(ptr);
DataCopy(gmRemoteStatusGT, statusTensor[...], 8UL);
}
}
__aicore__ inline void WaitSyncFlag() {
// 轮询本地 SHMEM 中的 flag 区域
compareTarget = 1.0f * rankNumPerBlock_;
while ((sumOfFlag < minTarget) || (sumOfFlag > maxTarget)) {
DataCopy(statusFp32Tensor, statusFp32TensorGT[curBlockStartRankId_ * ...], ...);
ReduceSum(statusSumOutTensor, statusFp32Tensor, ...);
sumOfFlag = statusSumOutTensor.GetValue(0);
}
// 清理 flag
Duplicate(cleanStateTensor, 0, ...);
DataCopy(statusFp32TensorGT[...], cleanStateTensor.ReinterpretCast<float>(), ...);
}
同步协议: 每个 rank 向远端 rank 的 SHMEM flag 区域写入 1.0f。当本 rank 收到所有远端 rank 的 flag 后(sum == rankNumPerBlock),表示所有远端数据已就绪。
Step 6: InputToDstOutput
__aicore__ inline void InputToDstOutput() {
SplitToCore(axisBS_, aivNum_, ...); // 分配 token
for (tokenIndex = startTokenId; tokenIndex < endTokenId; tokenIndex++) {
DataCopyPad(expertIdsTensor, expertIdsGM_[tokenIndex * axisK_], ...);
Duplicate(sumTokenTensor_, 0.0f, axisH_);
for (topkId = 0U; topkId < axisK_; topkId++) {
int32_t dstExpertId = expertIdsTensor.GetValue(topkId);
int32_t srcRankId = dstExpertId / moeExpertPerRankNum_;
uint32_t offsetIdx = dstExpertId * epWorldSize_ + epRankId_;
uint32_t col = offsetIdx % moeExpertNum_;
int32_t remoteBase = (col == 0) ? 0 : epSendCountGM_(offsetIdx - 1);
int32_t remoteOffset = expandIdxLocal(localTokenId * axisK_ + topkId);
float scale = expertScalesLocal_.GetValue(localTokenId * axisK_ + topkId);
// 从远端 SHMEM 读取
auto srcPtr = shmem_ptr(expandXGM_, srcRankId);
srcWinGMTensor.SetGlobalBuffer(srcPtr + hExpandXTypeSize_ * (remoteBase + remoteOffset));
// 加权求和(同 normal_zb)
DataCopyPad(tmpToken, srcWinGMTensor, ...);
Cast(tokenReduceTensor_, tmpToken, CAST_NONE, axisH_);
Muls(weightedSumTensor_, tokenReduceTensor_, scale, axisH_);
Add(sumTokenTensor_, sumTokenTensor_, weightedSumTensor_, axisH_);
}
Cast(xOutLocal, sumTokenTensor_, CAST_RINT, axisH_);
DataCopyPad(XOutGT_[tokenIndex * axisH_], xOutLocal, ...);
}
}
3.3 与 normal_zb 的关键差异
| 维度 | normal_zb | lowlatency_zb |
|---|---|---|
| 同步机制 | ZeroBufferSyncFlag (通用框架) |
自实现 SetSyncFlag/WaitSyncFlag |
| Magic 管理 | IncrementMagic() 自动管理 |
无 magic,使用固定 flag 值 1.0f |
| 本地数据拷贝 | 不需要(dispatch 已写入 SHMEM) | 需要 CopyValidExpandXToRemote |
| 地址计算 | epRecvCount 全局表 + sendTokenIdx |
epSendCount 前缀和 + expandIdx |
| TP 支持 | 仅 EP | EP + TP (tpWorldSize=1 or 2) |
| 共享专家 | 不支持 | 支持 (sharedExpertNum, sharedExpertRankNum) |
| 弹性缩容 | 不支持 | 支持 (elasticInfo) |
| 常量专家 | 不支持 | 支持 (constExpertAlpha1/2/V) |
| Copy Expert | 不支持 | 支持 (oriX → expandX 拷贝) |
| 量化通信 | 不支持 | INT8 量化 (IsInt8Quant) |
| TilingKey | 无 | 4 种组合 (tp × quant) |
| 输入复杂度 | 6 个输入 | 17 个输入 |
3.4 lowlatency 的地址计算
// 前缀和计算远端基址
offsetIdx = dstExpertId * epWorldSize_ + epRankId_
col = offsetIdx % moeExpertNum_
remoteBase = (col == 0) ? 0 : epSendCountGM_(offsetIdx - 1)
remoteOffset = expandIdxLocal(localTokenId * axisK_ + topkId)
// 最终地址
srcAddr = shmem_ptr(expandXGM_, srcRankId) + hExpandXTypeSize_ * (remoteBase + remoteOffset)
epSendCount 存储的是前缀和(prefix sum),epSendCount[i-1] 即为第 i 个 expert 在本 rank 上的数据基址。
4. Combine 阶段的 SHMEM 通信
4.1 SHMEM API 层次
shmem.h (CANN 提供)
├── shmem_ptr(baseGM, rankId) → 获取远端 rank 的 SHMEM window 基址
├── aclshmem_half_put_nbi(...) → 非阻塞 half 写入远端
├── aclshmem_bfloat16_put_nbi(...) → 非阻塞 bf16 写入远端
├── aclshmem_float_put_nbi(...) → 非阻塞 float 写入远端
├── aclshmem_half_get_nbi(...) → 非阻塞 half 读取远端
└── aclshmem_bfloat16_get_nbi(...) → 非阻塞 bf16 读取远端
zero_buffer_api.h (CAM 封装)
└── #include "shmem.h"
zero_buffer_sync_flag.h (同步框架)
└── ZeroBufferSyncFlag 类
4.2 shmem_ptr 的使用模式
两个 combine 算子都使用 shmem_ptr 进行远端 SHMEM 读取:
// normal_zb
auto ptr = reinterpret_cast<__gm__ uint8_t *>(shmem_ptr(recvXGM_, dstRankId));
dstGT.SetGlobalBuffer((__gm__ XType *)(ptr + remoteReadAddr));
// lowlatency_zb
auto srcPtr = reinterpret_cast<__gm__ uint8_t *>(shmem_ptr(expandXGM_, srcRankId));
srcWinGMTensor.SetGlobalBuffer((__gm__ ExpandXType *)(srcPtr + hExpandXTypeSize_ * (remoteBase + remoteOffset)));
shmem_ptr(baseGM, rankId) 语义: 返回 rankId 的 SHMEM window 虚拟地址,该地址在本 rank 的地址空间中映射到远端 rank 的 SHMEM 物理内存(通过 HCCL 的 window 机制)。
4.3 ZeroBufferSyncFlag 内存布局
gva_gm + FLAG_AREA_BASE (100KB):
├── Magic Area (24KB = 48 cores × 512B)
│ ├── slot[0] @ 0×512B ← core 0 的 magic
│ ├── slot[1] @ 1×512B ← core 1 的 magic
│ └── ...
├── Barrier PingPong Buffer 0 (epWorldSize × 32B)
│ ├── slot[srcRank=0] ← rank 0 写入的 barrier flag
│ ├── slot[srcRank=1]
│ └── ...
├── Barrier PingPong Buffer 1 (同上)
├── Flag PingPong Buffer 0 (epWorldSize × slotsPerRank × 32B)
│ ├── slot[srcRank=0][eventID=0]
│ ├── slot[srcRank=0][eventID=1]
│ └── ...
└── Flag PingPong Buffer 1 (同上)
4.4 同步协议详解
Writer (rank A, core i):
1. 完成数据写入 (DMA 到远端 SHMEM)
2. SetFlag(destRank=B, eventID=i, phase)
→ MTE3 fence (flush 所有先前的写入)
→ 写 flag 到 B 的本地 SHMEM 的 slot [A][i]
Reader (rank B, core j):
1. WaitFlag(srcRank=A, eventID=i, phase)
→ 轮询本地 SHMEM 直到 flag 匹配 (magic, phase)
2. 返回后,rank A 的数据保证可见
DAG 保证(无环依赖):
DataWrite(A→B) ── MTE3 fence ──► SetFlag(A→B)
│
▼
WaitFlag(B←A) ──► DataRead(B←A)
4.5 lowlatency 的自定义同步
lowlatency_zb 没有使用 ZeroBufferSyncFlag 框架,而是实现了自己的同步:
// 同步区域: metaDataGvaGM_ + COMBINE_SYNC_FLAG_BASE_OFFSET (100KB)
// 每个 rank 占 COMBINE_SYNC_STATE_OFFSET (32B)
// SetSyncFlag: 向远端 rank 的 flag 区域写入 1.0f
GetSyncFlagAddrByRankId(rankId) = shmem_ptr(metaDataGvaGM_, rankId) + 100KB + epRankId_ * 32B
// WaitSyncFlag: 轮询本 rank 的 flag 区域,直到所有远端 rank 都写入
// 使用 ReduceSum 求和,期望值 = rankNumPerBlock_
与 normal_zb 的区别:
- normal_zb 使用 magic + phase 的 64-bit flag,支持跨 kernel 同步
- lowlatency_zb 使用固定值 1.0f,仅支持单次 kernel 内同步
- lowlatency_zb 在等待完成后会清理 flag(写回 0),为下次同步做准备
5. 与 master 版 moe_combine_normal 的对比
5.1 架构差异总览
| 维度 | master (HCCL Window) | cam_feature (Zero-Buffer) |
|---|---|---|
| 数据搬运 | CopyBufferToShare → WaitBuffCopy | 直接 SHMEM 读取 |
| 同步机制 | polling WaitBuffCopy (per-token per-topk) |
ZeroBufferSyncFlag (per-rank) |
| Magic 管理 | GetHcclContext + localWindowsExp |
IncrementMagic() + per-core slot |
| Window 访问 | epWinContext_->localWindowsIn / remoteRes |
shmem_ptr(baseGM, rankId) |
| 状态管理 | per-token per-topk 的 32B status slot | per-rank 的 64B flag slot |
| 数据源 | tokenSrcInfo (3-tuple: rankId, tokenIdx, topkIdx) |
sendTokenIdx + epRecvCount |
| Copy 方向 | 本地 → 远端 (CopyBufferToShare) | 远端 → 本地 (DataCopyPad from SHMEM) |
5.2 master 版执行流程
// master: moe_combine_normal.cpp
Process():
├── CopyBufferToShareAndSetStatus() // 将本地 token 拷贝到远端 window
│ └── for each token:
│ ├── CopyBufferToShare() // DataCopyPad 到远端 SHMEM
│ └── SetStatusBySrcInfo() // 写 status flag
└── ReadBufferFromRemote() // 读取远端数据
└── for each token:
├── WaitBuffCopy() // 轮询等待所有源 rank 数据到位
└── ReadBufferAndWeightedSum() // 加权求和
5.3 ZB 版执行流程
// cam_feature: moe_combine_normal_zero_buffer.cpp
Process():
├── syncFlag_.IncrementMagic() // 获取 magic
├── syncFlag_.BarrierAll() // 全 rank 屏障
├── ReadBufferFromRemote() // 直接读取远端 SHMEM
│ └── for each token:
│ └── ReadBufferAndWeightedSum() // 直接从远端 SHMEM 读取 + 加权
└── SyncAll<true>()
5.4 关键差异分析
5.4.1 数据流方向
master: 推模式 (Push)
Rank A: CopyBufferToShare → 写入 Rank B 的 window
Rank B: WaitBuffCopy → 轮询等待 → 从本地 window 读取
ZB: 拉模式 (Pull)
Rank A: (dispatch 阶段已写入 SHMEM)
Rank B: shmem_ptr(recvXGM_, rankA) → 直接读取 Rank A 的 SHMEM
优势: ZB 消除了 CopyBufferToShare 阶段,Combine 不需要先将本地数据推送到远端。
5.4.2 同步粒度
master: per-token per-topk 粒度
// 每个 (token, topk) 对有一个 32B status slot
GM_ADDR stateGM = GetStateAddrByRankId(srcRankId) + (srcTokenId * axisK_ + srcTopkId) * UB_32_ALIGN;
// 轮询直到所有 K 个 source 都到位
while (current != target) {
DataCopy(stateTensorLocal, stateGMTensor, calCount);
Sum(tempStateTensorLocal, stateTensorLocal, sumPerKParams);
current = tempStateTensorLocal(0);
}
ZB: per-rank 粒度
// 每个 rank 有一个 64B flag slot
// BarrierAll 一次等待所有 rank
syncFlag_.BarrierAll();
优势: ZB 的同步粒度更粗,减少了 polling 次数和 flag 写入次数。
5.4.3 Magic 管理
master: 使用 HCCL context 中的 magic
auto contextGM0 = AscendC::GetHcclContext<HCCL_GROUP_ID_0>();
epWinContext_ = (__gm__ HcclOpResParam *)contextGM0;
selfMagicTensor.SetGlobalBuffer(epWinContext_->localWindowsExp + MAGIC_WIN_OFFSET + coreIdx_ * WIN_512_ALIGN);
magic_ = selfMagicTensor(0); // 0 或 1 翻转
selfMagicTensor(0) = ((magic_ == 0) ? 1 : 0);
ZB: 使用 ZeroBufferSyncFlag 的递增 magic
magic_ = static_cast<int32_t>(magicGT(0));
magicGT(0) = magic_ + 1; // 单调递增
优势: ZB 的 magic 单调递增,可以区分任意历史 kernel 的 flag,不会产生 ABA 问题。
5.4.4 内存占用
master:
- 需要 tokenSrcInfo 数组 (bs × k × 12B)
- 需要 per-token per-topk 的 status slot (bs × k × 32B)
- 需要 ping-pong 双缓冲
ZB:
- 需要 sendTokenIdx 数组 (bs × k × 4B)
- 需要 epRecvCount 表 (moeExpertNum × epWorldSize × 4B)
- 只需要 per-rank 的 flag slot
5.5 性能提升预估
| 优化点 | 预估收益 |
|---|---|
| 消除 CopyBufferToShare | 减少一次 GM→GM DMA,约节省 bs × k × h × sizeof(bf16) 的写入延迟 |
| 粗粒度同步 | 从 O(bs × K) 次 polling 降至 O(1) 次 BarrierAll |
| 消除 WaitBuffCopy | 消除 per-token 的 polling 循环(可能涉及数百次循环迭代) |
| 减少 flag 写入 | 从 bs×K 次 flag 写入降至 epWorldSize 次 |
6. Tiling 策略
6.1 normal_zb Tiling
核心流程 (MoeCombineNormalZeroBufferA3TilingFuncImpl):
// 1. 获取属性
GetAttrAndSetTilingData(context, *tilingData, nodeName);
// → 读取 meta_data_ptr, ep_world_size, ep_rank_id, moe_expert_num, global_bs
// 2. 校验输入输出
TilingCheckMoeCombineNormalZeroBuffer(context, nodeName, isEnableDiagnose);
// → 检查 dim/format/dataType
// 3. 校验属性
CheckAttrs(context, *tilingData, nodeName, localMoeExpertNum);
// → moeExpertNum % epWorldSize == 0
// → tp=2 时 localMoeExpertNum == 1
// → globalBs == 0 || globalBs >= bs * epWorldSize
// 4. 校验 shape
CheckTensorShape(context, *tilingData, nodeName, localMoeExpertNum);
// → k ∈ (0, min(16, moeExpertNum)]
// → h ∈ [1024, 7168]
// 5. 校验 win 区大小
tokenNeedSizeCombine = Ceil(h * 2 + 511, 512) * 512; // 512 对齐
actualSize = (maxBs * k * tokenNeedSizeCombine + COMBINE_STATE_WIN_OFFSET + NOTIFY_DISPATCH_WIN_OFFSET) * 2;
// 6. 设置 blockDim 和 schedule mode
blockDim = ascendcPlatform.CalcTschBlockDim(aivNum, 0, aivNum);
context->SetScheduleMode(1); // batch mode,所有核同时启动
约束条件:
- epWorldSize ∈ [2, 384]
- tpWorldSize ∈ [0, 2]
- moeExpertNum ∈ (0, 512]
- k ∈ (0, min(16, moeExpertNum)]
- h ∈ [1024, 7168]
- (moeExpertNum / epWorldSize) × epWorldSize ≤ 1280
6.2 lowlatency_zb Tiling
核心流程 (MoeCombineLowlatencyA3TilingFuncImpl):
// 1. 获取属性(更复杂)
GetAttrAndSetTilingData(context, *tilingData, nodeName, groupEp, groupTp, commQuantMode);
// → 额外读取 sharedExpertNum, sharedExpertRankNum, zeroExpertNum, copyExpertNum, constExpertNum
// 2. 计算 A(expandX 的 dim0)
if (isShared) {
A = maxBs * maxSharedGroupNum; // 共享专家卡
} else {
A = globalBs * min(localMoeExpertNum, expertIdsDim1); // MoE 专家卡
}
// 3. 校验 win 区大小
tokenNeedSizeCombine = Ceil(h * 2 + 511, 512) * 512;
tokenActualLen = Ceil(h * 2 + 31, 32) * 32 + 44; // + scale(32B) + expandIdx(3*4B)
tokenNeedSizeDispatch = Ceil(tokenActualLen + 511, 512) * 512;
actualSize = ((maxBs * tokenNeedSizeDispatch * epWorldSize * localMoeExpertNum) +
(maxBs * tokenNeedSizeCombine * (k + sharedExpertNum))) * 2;
// 4. 计算 TilingKey
tilingKey = 10000;
if (tpWorldSize == 2) tilingKey += 100;
if (commQuantMode == 2) tilingKey += 20;
// 5. 设置 Mc2CcTiling(HCCL 通信配置)
SetHCommCfg(context, tilingData, groupEp, groupTp);
// → AlltoAll: level0:fullmesh;level1:pairwise
// → ReduceScatter: level0:ring
额外约束:
- epWorldSize ∈ [2, 768](支持更大规模)
- moeExpertNum ∈ (0, 1024]
- h ∈ [1024, 8192]
- bs ∈ [1, 512]
- sharedExpertNum ∈ [0, 4]
- moeExpertNum % (epWorldSize - sharedExpertRankNum) == 0
- zeroBufferPtr != 0
6.3 Win 区大小计算对比
normal_zb:
actualSize = (maxBs × k × tokenNeedSizeCombine + COMBINE_STATE_WIN_OFFSET + NOTIFY_DISPATCH_WIN_OFFSET) × 2
= (maxBs × k × Ceil(h×2, 512) + 5MB + 204KB) × 2
lowlatency_zb:
actualSize = ((maxBs × tokenNeedSizeDispatch × epWorldSize × localMoeExpertNum) +
(maxBs × tokenNeedSizeCombine × (k + sharedExpertNum))) × 2
差异: lowlatency 需要同时容纳 dispatch 数据区和 combine 数据区,而 normal 只需要 combine 数据区(dispatch 数据区在 notify 算子中管理)。
6.4 Schedule Mode
两个算子都使用 SetScheduleMode(1) (batch mode):
- 所有 AIV 核同时启动
- 核间通过 SyncAll<true>() 同步
- 不需要核间通信,完全并行
7. 关键数据结构与内存布局
7.1 Tiling 数据结构
normal_zb:
struct MoeCombineNormalZeroBufferInfo {
uint32_t epWorldSize; // EP 通信域大小
uint32_t tpWorldSize; // TP 通信域大小 (0 or 1 or 2)
uint32_t epRankId; // 本 rank ID
uint32_t tpRankId; // 本 TP rank ID
uint32_t expertShardType; // 专家分片类型
uint32_t moeExpertNum; // MoE 专家总数
uint32_t moeExpertPerRankNum; // 每 rank 专家数
uint32_t globalBs; // 全局 batch size
uint32_t bs; // 本 rank batch size
uint32_t k; // top-K
uint32_t h; // hidden size
uint32_t aivNum; // AIV 核数
uint64_t totalUbSize; // UB 总大小
uint64_t totalWinSize; // Window 总大小
float armAvgFactor; // ARM 平均因子
float epsilon; // 数值精度
bool isEnableDiagnose; // 是否启用诊断
bool isGetProb; // 是否获取概率梯度
};
struct MoeCombineNormalZeroBufferTilingData {
MoeCombineNormalZeroBufferInfo moeCombineNormalInfo;
uint64_t zeroBufferPtr; // Zero-Buffer 对称地址
};
lowlatency_zb:
struct MoeCombineLowlatencyZeroBufferInfo {
// ... (同上,额外字段)
uint32_t sharedExpertNum; // 共享专家数
uint32_t sharedExpertRankNum; // 共享专家所在 rank 数
uint32_t zeroExpertNum; // 零专家数
uint32_t copyExpertNum; // 拷贝专家数
uint32_t constExpertNum; // 常量专家数
bool isTokenMask; // token 级 mask
bool isExpertMask; // expert 级 mask
bool hasSharedExpertX; // 是否有共享专家输入
bool hasElasticInfo; // 是否有弹性信息
uint64_t zeroBufferPtr; // Zero-Buffer 地址
};
struct MoeCombineLowlatencyZeroBufferTilingData {
Mc2InitTiling mc2InitTiling; // MC2 初始化 tiling
Mc2CcTiling mc2CcTiling1; // MC2 CC tiling 1 (AlltoAll)
Mc2CcTiling mc2CcTiling2; // MC2 CC tiling 2 (ReduceScatter)
MoeCombineLowlatencyZeroBufferInfo moeCombineLowlatencyZeroBufferInfo;
};
7.2 常量定义
// normal_zb 内存布局常量
constexpr uint64_t NOTIFY_MAGIC_OFFSET = 50UL * 1024UL; // 50KB
constexpr uint64_t WIN_MAGIC_OFFSET = 100UL * 1024UL; // 100KB
constexpr uint64_t HALF_WIN_STATE_OFFSET = 8UL * 1024UL * 1024UL; // 8MB
constexpr uint64_t COMBINE_WIN_STATE_OFFSET = 5UL * 1024UL * 1024UL; // 5MB
constexpr uint64_t WIN_ADDR_ALIGN = 512UL;
// lowlatency_zb 同步常量
constexpr uint64_t COMBINE_SYNC_FLAG_BASE_OFFSET = 100UL * 1024UL; // 100KB
constexpr uint64_t COMBINE_SYNC_STATE_OFFSET = 32U; // 32B per rank
// 通用常量
constexpr uint32_t UB_32_ALIGN = 32U;
constexpr uint32_t MUL_256_ALIGN = 256U;
constexpr uint8_t DOUBLE_BUFFER = 2;
7.3 类继承/依赖关系
MoeCombineNormalZeroBuffer<RecvXType, XType, SrcInfoType>
├── 使用 ZeroBufferSyncFlag (通过 TBuf)
├── 使用 shmem_ptr() 访问远端 SHMEM
└── 使用 TQue/TBuf 管理 UB
MoeCombineLowlatencyZeroBuffer<ExpandXType, XType, ExpandIdxType, IsNeedReduceScatter, IsInt8Quant>
├── 自实现 SetSyncFlag/WaitSyncFlag
├── 使用 shmem_ptr() 访问远端 SHMEM
├── 使用 moe_lowlatency_zero_buffer_base.h 工具
└── 支持 TP=2, 共享专家, 弹性缩容, 常量专家
附录:关键函数索引
normal_zb
| 函数 | 文件 | 行号 | 说明 |
|---|---|---|---|
moe_combine_normal_zero_buffer |
cpp:18 | kernel 入口 | |
Init() |
h:108 | 初始化所有 buffer 和地址 | |
Process() |
h:175 | 主执行流程 | |
IncrementMagic() |
h:117 | 获取并递增 magic | |
ReadBufferFromRemote() |
h:143 | 从远端 SHMEM 读取数据 | |
ReadBufferAndWeightedSum() |
h:124 | 加权求和核心逻辑 | |
SplitCoreCal() |
h:82 | 均匀分配 token 到各 core | |
InitMagic() |
h:113 | 读取并翻转 magic |
lowlatency_zb
| 函数 | 文件 | 行号 | 说明 |
|---|---|---|---|
moe_combine_lowlatency_zero_buffer |
cpp:37 | kernel 入口 | |
Init() |
h:135 | 初始化 | |
Process() |
h:286 | 主执行流程 | |
CopyValidExpandXToRemote() |
h:214 | 拷贝本地数据到 SHMEM | |
SetSyncFlag() |
h:172 | 写同步 flag | |
WaitSyncFlag() |
h:193 | 轮询等待 flag | |
InputToDstOutput() |
h:241 | 从 SHMEM 读取并加权求和 | |
SplitToCore() |
h:155 | 分配 token 到各 core |
工具类
| 类/函数 | 文件 | 说明 |
|---|---|---|
ZeroBufferSyncFlag |
zero_buffer_sync_flag.h | 通用跨卡 flag 同步 |
IncrementMagic() |
zero_buffer_sync_flag.h:103 | magic 管理 |
SetFlag() |
zero_buffer_sync_flag.h:127 | 写 flag |
WaitFlag() |
zero_buffer_sync_flag.h:153 | 轮询等待 flag |
BarrierAll() |
zero_buffer_sync_flag.h:238 | 全 rank 屏障 |
SyncCollectives |
sync_collectives_zb.h | 底层同步原语 |
shmem_ptr() |
shmem.h (CANN) | 获取远端 SHMEM 地址 |
总结
CAM 库的 ZB (Zero-Buffer) Combine 算子代表了 MoE 通信的一次范式转变:
- 从推模式到拉模式: 消除了 Combine 阶段的
CopyBufferToShare,改为直接从远端 SHMEM 读取 - 从细粒度到粗粒度同步: 从 per-token per-topk 的 polling 升级为 per-rank 的 BarrierAll
- 从固定 magic 到递增 magic: 解决了 ABA 问题,支持跨 kernel 同步
- 从单一场景到多样化: lowlatency 版支持 TP=2、共享专家、弹性缩容、常量专家等高级特性
这些优化使得 Combine 阶段的延迟显著降低,尤其在大规模 EP 场景下(epWorldSize 达到 384/768),粗粒度同步的优势更加明显。