CANN Ascend C Add 向量加法算子入门实战:静态 Tensor 编程范式与多核流水实现解析

发布时间:2026/9/18 18:56:55
CANN Ascend C Add 向量加法算子入门实战:静态 Tensor 编程范式与多核流水实现解析 CANN Ascend C Add 向量加法算子入门实战静态 Tensor 编程范式与多核流水实现解析【免费下载链接】cann-samplesCANN高性能实战演进样例与体系化调优知识库项目地址: https://gitcode.com/cann/cann-samples导读本文围绕 CANN 开源仓库 cann-samples 中 Add 向量加法入门样例 展开完整讲解基于 Ascend C SIMD C API 的静态 Tensor 编程方式如何用搬入—计算—搬出三段式流水结构在 AI Core 上实现两个向量的逐元素加法如何通过 8 个核并行分片提升吞吐以及如何编译、运行、调试与性能剖析该算子。读完本文你将掌握 GlobalTensor/LocalTensor/DataCopy/PipeBarrier 等核心编程要素的用法并具备将同一套模板迁移到其他逐元素Element-wise算子的能力。样例概述做什么、怎么做本样例位于 Samples/0_Introduction/01_simd_cpp_api/01_add/add它演示了 Ascend C 向量加法的基本用法。算子的数学定义是逐元素加法$$z_i x_i y_i$$x输入张量形状为[8, 2048]数据类型为 float数据排布格式为 NDy输入张量形状为[8, 2048]数据类型为 float数据排布格式为 NDz输出张量形状为[8, 2048]数据类型为 float数据排布格式为 ND。样例运行参数本样例使用 8 个核完成计算每个核处理 2048 个元素blockLength 2048数据总量为 8×2048 16384 个 float 元素。核函数启动时通过内核调用符numBlocks, 0, stream指定numBlocks 8即同时拉起 8 个 block8 个核并行执行。支持的产品与 CANN 软件版本产品CANN 软件版本Ascend 950PR/Ascend 950DT CANN 9.1.0Atlas A3 训练系列产品/Atlas A3 推理系列产品 CANN 9.0.0Atlas A2 训练系列产品/Atlas A2 推理系列产品 CANN 9.0.0目录结构Samples/0_Introduction/01_simd_cpp_api/01_add/add ├── CMakeLists.txt // 编译工程文件 ├── add.asc // Ascend C 样例实现 调用样例 └── README.md // 样例说明文档其中 add.asc 是单文件完整实现同时包含核函数kernel 侧与宿主侧host 侧的调用、数据构造和精度校验逻辑是理解核函数怎么写、怎么调、怎么验的最小闭环。三个核心存储/同步概念GM、UB、DataCopy、PipeBarrier在阅读核函数代码之前先建立四个基础概念这也是后续所有 SIMD 向量算子通用的编程要素GMGlobal MemoryAI Core 外部的全局存储容量大但访问速度慢通过GlobalTensor访问。它是算子的输入输出数据在设备侧的落脚点。UBUnified BufferAI Core 内部的向量计算专用片上缓存容量有限但访问速度快通过LocalTensor访问。向量计算单元只能读取 UB 上的数据因此输入数据必须先搬到 UB 才能参与计算。DataCopy在 GM 与 UB 之间搬运数据的 API搬运方向由参数顺序决定DataCopy(local, global, len)是 GM→UB 搬入DataCopy(global, local, len)是 UB→GM 搬出。PipeBarrier流水线同步屏障用于保证数据搬运完成后再执行后续操作避免不同硬件流水MTE 搬运单元与 Vector 计算单元之间的读写冲突。此外还有一个贯穿多核编程的内建变量block_idx它表示当前核的编号等价于GetBlockIdx()用于多核并行时的数据分片计算。核函数实现静态 Tensor 三段式流水Add 算子的计算逻辑严格遵循搬入—计算—搬出三段式流水结构将输入数据 x 和 y 从 GM 搬运到 UB在 UB 上对xLocal、yLocal执行向量加法操作结果存入zLocal将计算结果从 UB 搬运回 GM。核心代码如下与仓库中 add.asc 一致template uint32_t blockLength __vector__ __global__ void add_custom(__gm__ float* x, __gm__ float* y, __gm__ float* z) { AscendC::InitSocState(); // Global Tensor在GM上分配输入/输出缓冲区 AscendC::GlobalTensorfloat xGm, yGm, zGm; xGm.SetGlobalBuffer(x block_idx * blockLength, blockLength); // 每个核按block_idx偏移处理各自的数据段 yGm.SetGlobalBuffer(y block_idx * blockLength, blockLength); zGm.SetGlobalBuffer(z block_idx * blockLength, blockLength); // Local Tensor在UB上分配计算缓冲区 AscendC::LocalMemAllocatorAscendC::Hardware::UB ubAllocator; AscendC::LocalTensorfloat xLocal ubAllocator.Allocfloat, blockLength(); AscendC::LocalTensorfloat yLocal ubAllocator.Allocfloat, blockLength(); AscendC::LocalTensorfloat zLocal ubAllocator.Allocfloat, blockLength(); // GM - UB: 搬入输入数据 AscendC::DataCopy(xLocal, xGm, blockLength); AscendC::DataCopy(yLocal, yGm, blockLength); AscendC::PipeBarrierPIPE_ALL(); // 确保搬入完成后才进行计算 // 向量计算: z x y AscendC::Add(zLocal, xLocal, yLocal, blockLength); AscendC::PipeBarrierPIPE_ALL(); // 确保计算完成后才搬出 // UB - GM: 搬出计算结果 AscendC::DataCopy(zGm, zLocal, blockLength); AscendC::PipeBarrierPIPE_ALL(); // 确保搬出完成 }代码中的编程要素可以逐行拆解__vector__ __global__声明这是一个在向量计算单元上执行、可从宿主侧启动的核函数参数用__gm__ float*标记为 GM 地址空间。InitSocState()初始化 AI Core 硬件状态为后续操作做准备是所有核函数的第一行。SetGlobalBuffer(addr, len)将GlobalTensor绑定到 GM 上的某段连续内存。x block_idx * blockLength的写法让每个核从自己的数据段起点开始访问实现数据分片。LocalMemAllocatorAscendC::Hardware::UB静态 Tensor 编程模式下在 UB 上申请内存的分配器Allocfloat, blockLength()为 float 类型的blockLength个元素分配一块连续空间。x、y、z 各分配一块互不重叠。AscendC::Add(dst, src0, src1, len)向量加法指令在 UB 上并行计算dst[i] src0[i] src1[i]。三处PipeBarrierPIPE_ALL()分别隔离搬入→计算计算→搬出搬出→结束保证数据就绪后再被消费。宿主侧调用与精度校验源码级补充核函数本身不负责数据准备与结果验证这些逻辑都在 add.asc 的宿主侧完成整体链路为初始化运行环境aclInit(nullptr)初始化 ACL 运行时aclrtSetDevice(deviceId)选择设备样例固定使用 device 0aclrtCreateStream(stream)创建执行流。分配设备内存通过aclrtMalloc为 x、y、z 各分配totalLength * sizeof(float)的设备侧内存分配策略为ACL_MEM_MALLOC_HUGE_FIRST再通过aclrtMallocHost分配一块宿主侧内存用于回拷结果。数据上板aclrtMemcpy把宿主侧的 x、y 数据以ACL_MEMCPY_HOST_TO_DEVICE方向拷入设备内存。启动核函数add_customblockLengthnumBlocks, 0, stream(xDevice, yDevice, zDevice)其中模板参数blockLength 2048在编译期确定运行时参数为三个设备地址numBlocks 8指定 8 个核并行。同步与回拷aclrtSynchronizeStream(stream)等待核函数执行完成再用ACL_MEMCPY_DEVICE_TO_HOST将结果 z 拷回宿主。资源释放依次aclrtFree设备内存、aclrtFreeHost宿主内存、aclrtDestroyStream销毁流、aclrtResetDevice复位设备、aclFinalize结束 ACL 运行时。精度校验宿主侧用 CPU 直接计算 golden 结果golden[i] x[i] y[i]再通过VerifyResult逐元素比对std::equal。比对通过打印test pass!并返回 0否则打印test failed!返回 1。样例同时在标准输出打印输出与真值的前 20 个元素便于人工核对。main 函数中的数据生成方式为x[i] i * 0.1f、y[i] i * 0.2f共8 * 2048个元素。实现流程解析每个阶段在做什么、为什么下表把核函数的执行过程按阶段拆解明确每个动作的数据流动与设计意图阶段数据流动/行为实现目的/原因初始化InitSocState()初始化 AI Core 硬件状态为后续操作做准备GM 地址分配SetGlobalBuffer(x block_idx * blockLength, blockLength)每个核根据block_idx计算偏移量处理不同的数据段实现多核并行UB 空间分配ubAllocator.Allocfloat, blockLength()在 UB 上为 x、y、z 各分配一块连续内存供向量计算使用搬入Stage 1GM → UBDataCopy(xLocal, xGm)、DataCopy(yLocal, yGm)将输入数据从 GM 搬运到 UB因为向量计算单元只能访问 UB 上的数据流水同步PipeBarrierPIPE_ALL()确保搬入完成后再开始计算避免计算单元读取到未就绪的数据计算Stage 2UB 上计算Add(zLocal, xLocal, yLocal)在 UB 上执行向量加法利用向量单元并行处理多个元素流水同步PipeBarrierPIPE_ALL()确保计算完成后再开始搬出避免搬出未完成的结果搬出Stage 3UB → GMDataCopy(zGm, zLocal)将计算结果从 UB 搬运回 GM供后续使用或输出流水同步PipeBarrierPIPE_ALL()确保搬出完成保证数据一致性可以看到三段式结构中穿插的三次同步并不是冗余而是分别守护数据就绪结果就绪写回完成三个关键时序点这是保证多流水MTE2 搬入、Vector 计算、MTE3 搬出并发安全的最小同步骨架。可优化方向分析从能跑到跑得快本样例是教学用的基础实现刻意省略了性能优化。文档明确列出了四个可优化方向理解它们有助于建立 Ascend C 性能优化的直觉序号可优化方向当前实现的问题预期优化收益1多核动态分配固定使用 8 个核未根据实际可用核数动态分配动态获取可用核数充分利用多核并行能力减少端到端耗时2增大搬运粒度每次搬运 2048 个 float 元素8KB搬运粒度较小增大单次搬运数据量减少搬运次数摊薄启动开销提升带宽利用率3双缓冲流水线并行搬入、计算、搬出三个阶段严格串行执行各硬件单元MTE2/V/MTE3无法同时工作采用 Ping-Pong 双缓冲机制使搬入、计算、搬出可并行执行隐藏搬运延迟4L2 Cache bypassAdd 输入数据只读取一次但默认经过 L2 Cache增加了 Cache 污染对流式访问数据设置 L2 Cache bypass减少不必要的 Cache 开销提升搬运效率这四个方向分别对应核数利用率搬运效率流水并行度缓存策略四类通用优化手法。本仓库的 add_tpipe_tque 样例即给出了另一个维度的演进——用 TPipe/TQue 队列机制替代手写PipeBarrier由框架自动管理内存分配与流水同步而在 1_Features 与 2_Performance 中还能找到双缓冲、多核动态分配等优化手法的完整落地案例例如 softmax_regbase_story、rms_norm_quant_story 等演进式样例。功能调试printf 与 DumpTensorprintfprintf接口提供 CPU 域 / NPU 域调试场景下的格式化输出功能。在算子 kernel 侧需要输出日志的位置直接调用即可例如AscendC::printf(add blockIdx%d\n, AscendC::GetBlockIdx());注意printfPRINTF接口打印功能会对算子实际运行的性能带来一定影响通常在调测阶段使用。开发者可以按需通过设置ASCENDC_DUMP0的方式关闭打印功能。DumpTensorDumpTensor用于 Dump 指定LocalTensor的内容同时支持打印自定义的附加信息仅支持uint32_t数据类型的信息比如打印当前行号。调用方式// 向量计算: z x y AscendC::Add(zLocal, xLocal, yLocal, blockLength); AscendC::DumpTensor(zLocal, 1, 32);仓库的 add.asc 中预留了完整的调试代码示例默认以#if 0关闭开发者把#if 0改为#if 1即可依次 Dump xLocal、yLocal、zLocal 的内容每段打印 32 个元素并附带自定义标识#if 0 // Debug I/O. Set #if 1 to enable print. AscendC::printf(%s\n, [ DumpTensor in xLocal]); AscendC::DumpTensor(xLocal, 1, 32); AscendC::printf(%s\n, [ DumpTensor in yLocal]); AscendC::DumpTensor(yLocal, 2, 32); AscendC::printf(%s\n, [ DumpTensor in zLocal]); AscendC::DumpTensor(zLocal, 3, 32); #endif注意DumpTensor 接口打印功能会对算子实际运行的性能带来一定影响通常在调测阶段使用。开发者可以按需通过设置ASCENDC_DUMP0来关闭打印功能。性能调试msOpProf 单算子性能分析msOpProf 是单算子性能分析工具包含msopprof和msopprof simulator两种使用方式。该工具协助用户定位算子内存、算子代码以及算子指令的异常实现全方位的算子调优支持基于不同运行模式上板或仿真和不同文件形式可执行文件或算子二进制 .o 文件进行性能数据的采集和自动解析。上板性能采集上板性能采集可以直接测定算子在实际昇腾 AI 处理器上的运行时间适合在板环境中快速定位算子性能问题。基于可执行文件 demo 执行msopprof ./demo命令完成后会在默认目录下生成以OPPROF_{timestamp}_XXX命名的文件夹性能数据文件夹结构示例如下├──dump # 原始的性能数据用户无需关注 ├──ArithmeticUtilization.csv # cube/vector指令cycle占比 ├──L2Cache.csv # L2 Cache命中率影响MTE2建议合理规划数据搬运逻辑增加命中率 ├──Memory.csv # UBL1和主存储器读写带宽速率 ├──MemoryL0.csv # L0AL0B和L0C读写带宽速率 ├──MemoryUB.csv # Vector和Scalar到UB的读写带宽速率 ├──OpBasicInfo.csv # 算子基础信息 ├──PipeUtilization.csv # 采集计算单元和搬运单元耗时和占比 ├──ResourceConflictRatio.csv # UB上的bank group、bank conflict和资源冲突率在所有指令中的占比 └──visualize_data.bin # MindStudio Insight呈现文件查看具体的性能分析结果# 查看Task Duration 以及各项数据 cat ./OPPROF_*/PipeUtilization.csv对 Add 这样的向量算子而言重点通常落在PipeUtilization.csv看 Vector 计算单元与 MTE2/MTE3 搬运单元的耗时占比判断是否存在搬运瓶颈与L2Cache.csv评估上一节提到的 L2 Cache bypass 优化空间上。编译运行全流程配置环境变量请根据当前环境上 CANN 开发套件包的安装方式配置环境变量source ${install_path}/cann/set_env.sh说明${install_path}为 CANN 包安装目录未指定安装目录时默认安装至/usr/local/Ascend下。从仓库的 cmake/ascend.cmake 可以看出编译系统会优先读取ASCEND_HOME_PATH环境变量定位工具链未设置时root 用户依次探测/usr/local/Ascend/ascend-toolkit/latest、/usr/local/Ascend/latest非 root 用户探测$HOME/Ascend/ascend-toolkit/latest、$HOME/Ascend/latest均找不到则报错要求显式设置。工具链编译器为${ASCEND_DIR}/${SYSTEM_PREFIX}/ccec_compiler/bin/bisheng编译 ASC 语言工程时必须依赖该工具链。编译与执行在本样例目录下执行如下命令mkdir -p build cd build; # 创建并进入build目录 cmake -DCMAKE_ASC_ARCHITECTURESdav-2201 ..;make -j; # 编译工程默认npu模式 ./demo # 执行样例使用 CPU 调试或 NPU 仿真模式时添加-DCMAKE_ASC_RUN_MODEcpu或-DCMAKE_ASC_RUN_MODEsim参数即可示例如cmake -DCMAKE_ASC_RUN_MODEcpu -DCMAKE_ASC_ARCHITECTURESdav-2201 ..;make -j; # CPU调试模式 cmake -DCMAKE_ASC_RUN_MODEsim -DCMAKE_ASC_ARCHITECTURESdav-2201 ..;make -j; # NPU仿真模式注意切换编译模式前需清理 cmake 缓存可在 build 目录下执行rm CMakeCache.txt后重新 cmake。编译选项说明选项可选值说明CMAKE_ASC_RUN_MODEnpu默认、cpu、sim运行模式NPU 运行、CPU 调试、NPU 仿真CMAKE_ASC_ARCHITECTURESdav-2201默认、dav-3510NPU 架构dav-2201 对应 Atlas A2 训练系列产品/Atlas A2 推理系列产品和 Atlas A3 训练系列产品/Atlas A3 推理系列产品dav-3510 对应 Ascend 950PR/Ascend 950DT架构选项在样例的 CMakeLists.txt 中通过find_package(ASC)引入 ASC 语言编译支持并用target_compile_options将--npu-arch${CMAKE_ASC_ARCHITECTURES}传给 ASC 编译器仓库根目录的 CMakeLists.txt 进一步校验NPU_ARCH只能是dav-3510或dav-2201二者之一。因此编译前请先确认目标设备的架构代号选择与设备匹配的值。执行结果执行结果如下说明精度对比成功test pass!延伸与 TPipe/TQue 队列式实现的对比同目录下的兄弟样例 add_tpipe_tque 用完全相同的算法z x y、形状[8, 2048]、float、ND演示了另一种编程范式——基于TPipe和TQue的内存与同步管理机制。其核函数流程为add_custom作为核入口接收totalLength通过GetBlockNum()计算当前 block 的数据长度通过GetBlockIdx()计算当前核在 GM 中对应的数据起点用DataCopy把输入数据从 GM 搬到 UB并通过EnQue将输入LocalTensor放入输入队列通过DeQue从输入队列取出输入张量在 UB 中执行Add再通过EnQue将结果LocalTensor放入输出队列通过DeQue从输出队列取出结果并使用DataCopy写回当前核负责的 GM 分片。两种范式对比可以直观看到静态 Tensor 与队列式编程的差异本样例静态 Tensor手动调用LocalMemAllocator分配 UB 空间、手动插入PipeBarrier做同步代码路径直观、适合理解底层流水而 TPipe/TQue 版本把内存分配与同步交给框架管理为后续引入多 buffer 流水并行Ping-Pong 双缓冲铺平了道路——这也是 可优化方向分析 中双缓冲流水线并行方向在框架层面的落地基础。两个样例的工程结构也体现了从单文件演示到脚本化验证的演进本样例把数据生成、真值计算内联在 main 函数中并直接打印test pass!而 add_tpipe_tque 则拆分为scripts/gen_data.py生成输入与 golden 数据与scripts/verify_result.py比对输出更贴近算子工程的实际开发流程。小结通过 Add 入门样例可以建立起 Ascend C 向量算子开发的最小知识闭环核侧InitSocState()→GlobalTensor/LocalTensor绑定与分配 →DataCopy搬入 →Add计算 →DataCopy搬出配合PipeBarrier守护时序宿主侧ACL 初始化 → 设备内存分配与数据上板 →启动核函数 → 同步回拷 → 逐元素精度校验调试与调优printf/DumpTensor 做功能定位msOpProf 做性能剖析再沿多核动态分配、搬运粒度、双缓冲、L2 bypass 四个方向迭代优化。掌握了这套模式向 LeakyReLU、Gelu 等更复杂的逐元素算子迁移时只需替换核函数中的向量计算指令与数据搬运细节整体骨架可以完全复用。【免费下载链接】cann-samplesCANN高性能实战演进样例与体系化调优知识库项目地址: https://gitcode.com/cann/cann-samples创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考