多核协同的艺术:CANN 规约算子开发中的同步与共享内存技术
在中声明一个数组,用于存储所有 Core 的局部结果。数组大小需不小于参与并行的 Core 数量(block_num// __gm__ 关键字标识该数组位于Global Memory如果说 SPMD 模型解决了 "如何让多个核同时干活" 的问题,那么共享内存与同步机制则解决了 "如何让多个核协同干好复杂活" 的问题。从功能上,能够实现依赖全局信息的复杂算子,突破基础并行的功能边界;从性能上,能够通
在 CANN 算子开发的进阶之路中,多核并行是释放昇腾 NPU 算力的核心手段。对于ReLU、Add等逐元素算子,仅需通过 SPMD 模型将任务分片到多个 AI Core 独立执行即可满足需求。但当面对ReduceSum、ReduceMean、Softmax等依赖全局信息的规约类算子时,单纯的 "各自为战" 已无法完成任务 —— 我们需要让分散在不同 AI Core 上的局部计算结果,通过协同汇总形成最终的全局结果。本文将深入解析 CANN 多核协同的核心技术:共享内存与同步机制,完整呈现规约算子的工程实现逻辑。
一、规约算子的核心挑战:从局部计算到全局汇总
规约算子的本质是对张量进行 "聚合运算",将多维数据压缩为标量或低维数据(如ReduceSum计算全局总和、ReduceMax寻找全局最大值)。在多核并行架构下,这一过程面临两大核心挑战:
1.1 计算结果的分散性
采用 SPMD 模型并行执行时,每个 AI Core 仅处理张量的一个分片:
- 假设使用 64 个 AI Core 处理 1024 维向量的
ReduceSum,每个 Core 处理 16 个元素; - 每个 Core 独立计算出自己分片的 "局部和"(共 64 个局部结果);
- 最终需要将这 64 个局部和进一步求和,才能得到全局总和。
这种 "局部结果分散在不同 Core" 的特性,是规约算子与逐元素算子的本质区别。
1.2 核间通信的必要性
AI Core 的Local Memory(片上私有缓存)是相互隔离的,Core 之间无法直接访问对方的私有内存。要实现局部结果的汇总,必须解决两个关键问题:
- 数据共享:如何让所有 Core 的局部结果被汇总 Core 访问到?
- 时序同步:如何确保汇总 Core 开始读取时,所有 Core 都已完成局部计算并写入结果?
这两个问题的解决方案,正是 CANN 多核协同的核心:共享内存用于数据共享,同步原语用于时序控制。
二、多核协同的基础:共享内存与同步机制
CANN 通过Global Memory(全局内存)实现核间数据共享,通过Sync()同步原语保障计算时序,二者共同构成多核协同的技术基石。
2.1 共享内存:核间通信的 "公共广场"
在 CANN 架构中,Global Memory(显存)是所有 AI Core 都能访问的公共存储区域,也是核间数据共享的唯一载体。对于规约算子,我们需要在Global Memory中预先开辟一块 "共享区域",用于存放各 Core 的局部计算结果:
- 共享区域的大小由参与并行的 Core 数量决定(如 64 个 Core 需分配 64 个元素的数组);
- 每个 Core 根据自身的
block_idx(核索引),将局部结果写入共享区域的对应位置,确保数据不冲突; - 汇总 Core 从该共享区域读取所有局部结果,进行最终聚合。
共享内存的核心作用是打破 AI Core 的私有内存隔离,为核间数据交换提供 "中转平台"。
2.2 同步原语:保障时序的 "栅栏机制"
没有同步控制的核间协同会导致严重的数据一致性问题:如果汇总 Core 提前读取共享区域,可能会读取到其他 Core 尚未写入的脏数据(初始值或旧数据),导致计算结果错误。
CANN 提供的Sync()函数(同步屏障)完美解决了这一问题:
- 当某个 AI Core 执行到
Sync()时,会暂停后续操作,进入等待状态; - 只有当所有参与并行的 AI Core都执行到该
Sync()时,栅栏才会 "放行",所有 Core 同时恢复执行; - 类比:团队任务中,所有成员必须到齐集合点才能召开总结会,
Sync()就是这个 "集合点"。
同步机制的核心价值,是确保核间操作的时序一致性,避免因执行速度差异导致的数据错误。
三、规约算子的实现范式:两阶段执行流程
一个标准的多核规约算子,遵循 "核内局部规约 + 核间全局规约" 的两阶段执行模式,结合共享内存与同步机制,实现高效的全局汇总。
3.1 阶段一:核内局部规约(Intra-Core Reduction)
该阶段的目标是在单个 AI Core 内部,高效完成自身分片数据的局部聚合计算。核心优化点是充分利用Local Memory的高速访问特性,避免频繁访问Global Memory。
关键实现要点:
- 数据预加载:通过 DMA 指令将
Global Memory中的分片数据搬运至Local Memory; - 向量加速:使用 Ascend C 的向量指令(如
vadd)替代标量循环,提升局部计算效率; - 局部聚合:在
Local Memory中完成分片数据的聚合运算,得到局部结果。
代码示例(ReduceSum局部规约):
__aicore__ inline half LocalReduction(half* local_buf, int32_t slice_len) {
half local_sum = 0.0_h;
// 向量计算加速:一次处理8个half类型数据
const int32_t vec_len = 8;
int32_t vec_loop = slice_len / vec_len;
int32_t remain = slice_len % vec_len;
// 向量循环:批量处理数据
for (int32_t i = 0; i < vec_loop; ++i) {
int32_t offset = i * vec_len;
// 加载8个元素到向量寄存器
vhalf8 vec_data = vload8(local_buf + offset);
// 向量求和:将8个元素的和累加到local_sum
local_sum = vaddv(vec_data) + local_sum;
}
// 处理剩余元素(不足一个向量长度)
for (int32_t i = vec_loop * vec_len; i < slice_len; ++i) {
local_sum += local_buf[i];
}
return local_sum;
}
3.2 阶段二:核间全局规约(Inter-Core Reduction)
该阶段是多核协同的核心,通过共享内存与同步机制,将所有 Core 的局部结果汇总为全局结果。完整流程分为 5 个步骤:
步骤 1:定义共享内存区域
在Global Memory中声明一个数组,用于存储所有 Core 的局部结果。数组大小需不小于参与并行的 Core 数量(block_num):
// __gm__ 关键字标识该数组位于Global Memory
__gm__ half shared_partial_results[MAX_BLOCK_NUM];
步骤 2:写入局部结果到共享内存
每个 Core 根据自身的block_idx(核索引),将局部结果写入共享内存的对应位置,确保每个 Core 的写入地址唯一:
// 获取当前核的索引和总核数
int32_t block_idx = GetBlockIdx();
int32_t block_num = GetBlockNum();
// 步骤1:核内局部规约(已实现)
half local_sum = LocalReduction(local_buf, slice_len);
// 步骤2:将局部结果写入共享内存的指定位置
shared_partial_results[block_idx] = local_sum;
步骤 3:同步屏障确保数据就绪
所有 Core 完成局部结果写入后,必须通过Sync()同步,确保汇总 Core 开始读取时,所有局部结果都已写入完成:
// 关键同步点:等待所有核完成共享内存写入
Sync();
步骤 4:主核执行全局汇总
指定block_idx == 0的 Core 作为 "主核"(Master Core),负责读取共享内存中的所有局部结果,执行最终的聚合运算:
if (block_idx == 0) { // 仅主核执行全局汇总
half global_sum = 0.0_h;
// 遍历共享内存,累加所有局部结果
for (int32_t i = 0; i < block_num; ++i) {
global_sum += shared_partial_results[i];
}
// 将全局结果写入输出张量
*output = global_sum;
}
步骤 5:(可选)二次同步释放资源
若后续还有其他操作,可添加二次Sync()确保主核完成汇总后,其他核再继续执行(规约算子通常无需此步骤)。
3.3 完整代码框架(ReduceSum算子核心逻辑)
#include "kernel_operator.h"
using namespace AscendC;
// 共享内存:存储所有核的局部和(Global Memory)
__gm__ half shared_partial_sums[MAX_BLOCK_NUM];
class KernelReduceSum : public BaseKernel {
public:
__aicore__ inline void Init() {
// 初始化Tiling参数:分片大小、核数等
total_len_ = input_->GetShape(0);
block_num_ = GetBlockNum();
slice_len_ = total_len_ / block_num_;
// 处理余数:最后一个核多处理剩余元素
if (GetBlockIdx() == block_num_ - 1) {
slice_len_ += total_len_ % block_num_;
}
// 分配Local Memory缓冲区
local_buf_ = (half*)lm_alloc(slice_len_ * sizeof(half));
}
__aicore__ inline void Process() {
// 1. 数据入:从Global Memory搬运到Local Memory
int32_t start_idx = GetBlockIdx() * (total_len_ / block_num_);
dma_copy_async(local_buf_, input_->GetPtr() + start_idx, slice_len_ * sizeof(half));
lm_wait(); // 等待数据搬运完成
// 2. 核内局部规约:计算分片局部和
half local_sum = LocalReduction(local_buf_, slice_len_);
// 3. 写入共享内存:局部结果存入Global Memory
shared_partial_sums[GetBlockIdx()] = local_sum;
// 4. 同步屏障:等待所有核完成写入
Sync();
// 5. 主核全局汇总:计算最终结果
if (GetBlockIdx() == 0) {
half global_sum = 0.0_h;
for (int32_t i = 0; i < block_num_; ++i) {
global_sum += shared_partial_sums[i];
}
// 结果出:将全局和写入输出
dma_copy_async(output_->GetPtr(), &global_sum, sizeof(half));
gm_wait();
}
}
__aicore__ inline void Destroy() {
// 释放Local Memory资源
lm_free(local_buf_);
}
private:
// 局部规约实现(向量加速)
__aicore__ inline half LocalReduction(half* buf, int32_t len) {
half sum = 0.0_h;
int32_t vec_loop = len / 8;
int32_t remain = len % 8;
// 向量批量计算
for (int32_t i = 0; i < vec_loop; ++i) {
vhalf8 vec = vload8(buf + i * 8);
sum += vaddv(vec);
}
// 处理剩余元素
for (int32_t i = vec_loop * 8; i < len; ++i) {
sum += buf[i];
}
return sum;
}
Tensor<input_> input_; // 输入张量(Global Memory)
Tensor<output_> output_; // 输出张量(Global Memory)
half* local_buf_; // Local Memory缓冲区
int32_t total_len_; // 输入张量总长度
int32_t slice_len_; // 当前核处理的分片长度
int32_t block_num_; // 并行核数
};
// 算子注册
REGISTER_KERNEL(ReduceSum, KernelReduceSum);
四、进阶优化:分层规约与性能调优
基础实现可以满足功能需求,但在核数较多(如 256 核、512 核)时,主核的串行汇总会成为性能瓶颈。此时需要引入分层规约优化,进一步提升并行效率。
4.1 分层规约的核心思想
将全局规约分为多个层级,每一层都通过多核并行完成部分汇总,最终仅需少量层级即可得到全局结果:
- 以 256 核为例,第一层:256 核分为 32 组,每组 8 核并行汇总,得到 32 个中间结果;
- 第二层:32 核分为 4 组,每组 8 核并行汇总,得到 4 个中间结果;
- 第三层:4 核并行汇总,得到最终全局结果。
分层规约通过多轮并行汇总,避免了单主核串行处理大量局部结果的瓶颈,尤其适用于核数多、局部结果量大的场景。
4.2 其他性能优化要点
- 共享内存对齐:共享内存数组的起始地址需按 32 字节或 64 字节对齐,避免非对齐访问导致的性能损耗;
- 局部内存复用:在
Local Memory中合理分配缓冲区,避免重复分配释放; - 同步粒度控制:仅在必要时使用
Sync(),避免过度同步导致的性能开销; - 向量指令充分利用:局部规约阶段尽量使用向量指令,最大化 AI Core 的计算并行度。
五、技术升华:从规约算子到复杂协同场景
掌握共享内存与同步机制后,不仅能实现ReduceSum等基础规约算子,更能应对神经网络中依赖全局信息的复杂算子:
5.1 Softmax 算子
Softmax 的计算需要先求每行的全局最大值(用于数值稳定性)和全局指数和,这两个步骤都需要规约操作。通过多核协同,可以高效完成全局统计信息的计算,再结合逐元素运算得到最终结果。
5.2 BatchNorm 算子
BatchNorm 的训练过程需要计算批次的全局均值和方差,这两个统计量的计算本质是ReduceMean和ReduceVariance(基于ReduceSum实现)。多核协同确保了批次统计信息的高效计算。
5.3 LayerNorm 算子
LayerNorm 需要计算每个样本的全局均值和方差,同样依赖规约操作。通过合理的任务分片和核间协同,可以在保持并行效率的同时,确保统计信息的准确性。
这些复杂算子的实现,本质上都是 "局部计算 + 核间协同 + 全局汇总" 的组合,共享内存与同步机制是其共同的技术核心。
六、总结:多核协同是并行计算的核心竞争力
如果说 SPMD 模型解决了 "如何让多个核同时干活" 的问题,那么共享内存与同步机制则解决了 "如何让多个核协同干好复杂活" 的问题。掌握多核协同技术,标志着 CANN 开发者从 "会用并行" 升级为 "善用并行":
- 从功能上,能够实现依赖全局信息的复杂算子,突破基础并行的功能边界;
- 从性能上,能够通过分层规约、向量加速等优化,最大化释放 NPU 的多核算力;
- 从能力上,能够理解并行计算的核心矛盾(数据共享与时序同步),具备设计高性能异构计算程序的工程思维。
昇腾 CANN 训练营第二季为开发者提供了系统化的多核并行与算子开发课程,从基础的 SPMD 模型到高阶的多核协同,从简单算子到复杂规约算子,通过实操案例帮助开发者快速掌握核心技术。无论你是 AI 框架开发者、性能优化工程师,还是想突破技术瓶颈的应用层开发者,这里都能为你提供构建底层技术护城河的关键能力。
昇腾 CANN 训练营第二季,火热报名中!
从零到一,精通算子开发!🚀
2025 昇腾 CANN 训练营第二季重磅回归,无论你是 AI 新手还是进阶开发者,这里都有为你量身打造的课程:
- 零基础入门:轻松掌握算子开发基础。
- 进阶实战特辑:挑战高阶技巧,码力全开。
- 开发者案例分享:借鉴实战经验,少走弯路。
【专属福利】✅ 官方权威认证:通过考核,赢取 Ascend C 算子中级认证 证书!🎁 社区惊喜好礼:完成任务,解锁精美社区周边!
名额有限,立即锁定席位!🔗 报名链接:[https://www.hiascend.com/developer/activities/cann20252]
昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链
更多推荐


所有评论(0)