多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

昇腾 AI 处理器向量加法算子开发实战:基于 Ascend C 的 Add 样例双实现深度解析(静态 Tensor 与 TPipe/TQue)

昇腾 AI 处理器向量加法算子开发实战:基于 Ascend C 的 Add 样例双实现深度解析(静态 Tensor 与 TPipe/TQue) 人工智能深度学习算子库CANNAscend【免费下载链接】asc-devkit本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言原生支持C和C标准规范主要由类库和语言扩展层构成提供多层级API满足多维场景算子开发诉求。项目地址https://gitcode.com/cann/asc-devkit点击查看免费下载导读本文以 CANN asc-devkit 仓库中的向量加法入门样例为蓝本完整剖析 Ascend C 算子开发的两大核心编程范式基于LocalMemAllocator的静态 Tensor 实现add与基于 TPipe/TQue 队列机制的实现add_tpipe_tque。通过阅读本文你将掌握昇腾 AI Core 上加载—计算—存储三段式流水结构、GM/UB 两级存储体系、多核按block_idx切分数据的并行方法以及从编译运行到 printf/DumpTensor 调试、msOpProf 性能分析的完整实战链路。样例总览一个加法两种内存与同步管理机制向量计算样例基于 Ascend C 实现用于演示两种不同的内存与同步管理机制。它们在功能上完全等价——都是完成两个同形状向量的逐元素相加但在如何管理片上内存、如何保证流水线阶段间同步上给出了两种典型答案。目录名称说明支持的型号add静态 Tensor 编程实现LocalMemAllocator直接分配 UB 内存Ascend 950PR/Ascend 950DTAtlas A3 训练系列产品/Atlas A3 推理系列产品Atlas A2 训练系列产品/Atlas A2 推理系列产品add_tpipe_tqueTQue 与 TPipe 队列式编程实现同上两个样例均在仓库 examples/01_simd_cpp_api/00_introduction/01_add 目录下都通过 8 个 AI Core 并行完成计算每个核处理 2048 个元素blockLength 2048输入输出均为float类型、形状[8, 2048]、ND 数据排布元素总数为 8×2048 16384 个。样例一静态 Tensor 实现add算子功能与运行参数Add 算子实现两个向量的逐元素相加计算公式为$$ z_i x_i y_i $$x输入形状 [8, 2048]数据类型 float数据排布 NDy输入形状 [8, 2048]数据类型 float数据排布 NDz输出形状 [8, 2048]数据类型 float数据排布 ND。该样例使用 8 个核完成计算每个核处理 2048 个元素合计 16384 个 float 元素。目录结构如下├── add │ ├── CMakeLists.txt // 编译工程文件 │ ├── add.asc // Ascend C 样例实现与调用示例 │ └── README.md // 样例文档三阶段流水结构加载—计算—存储Add 算子的计算逻辑遵循昇腾 AI Core 上经典的load-compute-store三段式流水结构通过 DataCopy 将输入数据 x、y 从 GMGlobal Memory芯片外部全局内存搬入 UBUnified Buffer向量计算专用片上缓存在 UB 上对 xLocal、yLocal 执行向量加法结果写入 zLocal将计算结果从 UB 搬回 GM。前置知识GMGlobal MemoryAI Core 外部的全局存储通过 GlobalTensor 访问容量大但访问速度慢UBUnified BufferAI Core 内部向量计算专用片上缓存通过 LocalTensor 访问容量有限但访问速度快DataCopyGM 与 UB 之间数据搬运的接口搬运方向由参数顺序决定PipeBarrier流水线同步屏障确保数据搬运完成后再进行后续操作避免读写冲突block_idx内建变量表示当前核的索引等价于 GetBlockIdx()用于多核并行计算中的数据切分。核心代码逐段剖析完整实现位于 add.asc核心 Kernel 代码如下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(); // 确保搬出完成 }这段代码透露出几个值得注意的实现细节多核数据切分每个核通过x block_idx * blockLength计算自己在 GM 中的起始地址配合 SetGlobalBuffer 绑定长度为blockLength的连续数据段实现互不重叠的并行切分UB 内存管理这里采用LocalMemAllocatorAscendC::Hardware::UB直接为三个 LocalTensor 分配 UB 空间这是与 TPipe/TQue 方案最大的区别——内存由分配器统一管理同步则全部依赖PipeBarrier同步时机三次PipeBarrierPIPE_ALL()分别保证了搬入完成 → 计算、计算完成 → 搬出、搬出完成三个依赖关系。调用方式与 Host 侧流程调用方式为使用 Kernel 调用操作符numBlocks, 0, stream调用核函数其中numBlocks 8指定 8 个核并行执行第二个参数0表示 blockDim 场景由系统推导。Host 侧的完整调用链见 add.asc遵循标准 ACL 流程aclInit(nullptr); aclrtSetDevice(deviceId); aclrtCreateStream(stream); // aclrtMalloc 申请设备内存 aclrtMallocHost 申请主机内存 // aclrtMemcpy 将 x、y 从 Host 拷贝到 DeviceHOST_TO_DEVICE add_customblockLengthnumBlocks, 0, stream(xDevice, yDevice, zDevice); aclrtSynchronizeStream(stream); // aclrtMemcpy 将 z 从 Device 拷回 HostDEVICE_TO_HOST // VerifyResult 对比输出与 golden 数据输出 test pass! aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize();Host 侧通过std::equal将 Kernel 输出与逐元素相加的 golden 结果逐位比对一致则打印test pass!。main函数中测试数据构造为x[i] i * 0.1f、y[i] i * 0.2fgolden 为x[i] y[i]。实现流程分析阶段数据流/行为目的/原因初始化InitSocState()初始化 AI Core 硬件状态为后续操作做准备GM 地址分配SetGlobalBuffer(x block_idx * blockLength, blockLength)各核基于block_idx计算偏移处理不同数据段实现多核并行UB 空间分配ubAllocator.Allocfloat, blockLength()在 UB 上为 x、y、z 分配连续内存块供向量计算使用加载阶段 1GM → UBDataCopy(xLocal, xGm)、DataCopy(yLocal, yGm)将输入从 GM 搬入 UB因为向量计算单元只能访问 UB 上的数据流水线同步PipeBarrierPIPE_ALL()确保搬入完成后再开始计算防止计算单元读取未就绪的数据计算阶段 2UB 内计算Add(zLocal, xLocal, yLocal)在 UB 上执行向量加法利用向量单元并行处理多个元素流水线同步PipeBarrierPIPE_ALL()确保计算完成后再开始搬出防止搬出未完成的结果存储阶段 3UB → GMDataCopy(zGm, zLocal)将计算结果从 UB 搬回 GM供后续使用或输出流水线同步PipeBarrierPIPE_ALL()确保搬出完成保证数据一致性样例二TPipe/TQue 队列式实现add_tpipe_tque目录结构与规格├── add_tpipe_tque │ ├── scripts │ │ ├── gen_data.py // 输入数据与 golden 数据生成脚本 │ │ └── verify_result.py // 输出数据与 golden 数据一致性校验脚本 │ ├── CMakeLists.txt // 编译工程文件 │ ├── data_utils.h // 数据读写函数 │ ├── add_tpipe_tque.asc // Ascend C 样例实现TPipe 与 TQue 管理内存和同步及样例调用 │ └── README.md // 样例文档样例类型OpTypeAdd样例输入xshape [8, 2048]floatNDyshape [8, 2048]floatND样例输出zshape [8, 2048]floatNDKernel 函数名add_custom处理流程add_custom作为 Kernel 入口接收totalLength在add_custom内通过GetBlockNum()计算当前 block 的数据长度通过GetBlockIdx()计算当前核在 GM 中的数据起始点在 Kernel 函数内使用DataCopy将输入数据从 GM 搬入 UB并通过EnQue将输入LocalTensor放入输入队列通过DeQue从输入队列取出输入张量在 UB 内执行Add再将结果LocalTensor通过EnQue放入输出队列通过DeQue从输出队列取出结果使用DataCopy写回当前核负责的 GM 分片。队列说明该样例使用TPipe和TQue演示基本的队列式编程方法。EnQue用于将已搬到 UB 的LocalTensor入队DeQue用于在后续阶段从队列中取出张量继续处理。其中 TPipe 负责统一管理 Device 端内存等资源——一个核函数必须且只能初始化一个 TPipe 对象它通过InitBuffer为 TQue 和 TBuf 分配内存TQue 则用于执行队列相关操作、管理相关资源。核心代码逐段剖析完整实现位于 add_tpipe_tque.asc__global__ __vector__ void add_custom(__gm__ uint8_t* x, __gm__ uint8_t* y, __gm__ uint8_t* z, uint32_t totalLength) { AscendC::TPipe pipe; AscendC::TQueAscendC::TPosition::VECIN, 1 inQueueX; AscendC::TQueAscendC::TPosition::VECIN, 1 inQueueY; AscendC::TQueAscendC::TPosition::VECOUT, 1 outQueueZ; AscendC::GlobalTensorfloat xGm; AscendC::GlobalTensorfloat yGm; AscendC::GlobalTensorfloat zGm; // totalLength 表示全局输入长度GetBlockNum() 返回本次启动的核数 // 这里按核数均分每个 block 的处理区间。 uint32_t blockLength totalLength / AscendC::GetBlockNum(); // 根据 block_idx 计算当前核在 GM 中的起始地址确保各核访问互不重叠的数据分片。 xGm.SetGlobalBuffer((__gm__ float*)x blockLength * AscendC::GetBlockIdx(), blockLength); yGm.SetGlobalBuffer((__gm__ float*)y blockLength * AscendC::GetBlockIdx(), blockLength); zGm.SetGlobalBuffer((__gm__ float*)z blockLength * AscendC::GetBlockIdx(), blockLength); // 为输入和输出队列申请与 blockLength 对应的 UB 缓冲保持 TPipe/TQue 的编程范式。 pipe.InitBuffer(inQueueX, 1, blockLength * sizeof(float)); pipe.InitBuffer(inQueueY, 1, blockLength * sizeof(float)); pipe.InitBuffer(outQueueZ, 1, blockLength * sizeof(float)); // 使用 DataCopy 将输入从 GM 搬运到 UB并通过 EnQue 将 LocalTensor 入队供后续计算阶段 DeQue 取用。 AscendC::LocalTensorfloat xLocal inQueueX.AllocTensorfloat(); AscendC::LocalTensorfloat yLocal inQueueY.AllocTensorfloat(); AscendC::DataCopy(xLocal, xGm, blockLength); AscendC::DataCopy(yLocal, yGm, blockLength); inQueueX.EnQue(xLocal); inQueueY.EnQue(yLocal); // DeQue 取出输入张量在 UB 内执行 Add并将结果 EnQue 到输出队列供后续写回 GM。 xLocal inQueueX.DeQuefloat(); yLocal inQueueY.DeQuefloat(); AscendC::LocalTensorfloat zLocal outQueueZ.AllocTensorfloat(); AscendC::Add(zLocal, xLocal, yLocal, blockLength); outQueueZ.EnQuefloat(zLocal); inQueueX.FreeTensor(xLocal); inQueueY.FreeTensor(yLocal); // 从输出队列取出结果并写回当前核负责的 GM 分片。 zLocal outQueueZ.DeQuefloat(); AscendC::DataCopy(zGm, zLocal, blockLength); outQueueZ.FreeTensor(zLocal); }队列模板参数与深度选择的工程要点TQueTPosition::VECIN, 1中第一个模板参数pos表示队列的逻辑位置此处 VECIN 表示向量搬入流水、VECOUT 表示向量搬出流水第二个参数depth表示队列深度。从 TQue 官方文档 可以提炼出以下工程要点深度语义队列深度表示该队列可以连续入队/出队的次数。若对同一队列连续 n 次EnQue中间没有DeQue深度需设为 n深度与 double buffer 无关队列机制用于实现流水线并行double buffer 是在此基础上进一步提高流水线利用率。即使队列深度为 1仍可开启 double buffer推荐深度为 1非 Tensor 原地操作场景下深度设为 1 时编译器对该场景做了特殊优化性能通常更好推荐设置为 1Tensor 原地操作场景下需设为 0。样例中每个队列每次仅入队一次、出队一次因此深度取 1与文档推荐一致。两种实现的对比与选型参考维度静态 Tensor 实现addTPipe/TQue 实现add_tpipe_tque内存管理LocalMemAllocatorHardware::UB直接分配pipe.InitBuffer统一为队列分配同步控制显式PipeBarrierPIPE_ALL()队列EnQue/DeQue隐式管理阶段间依赖数据切分模板参数blockLength固定2048运行时由totalLength / GetBlockNum()动态均分可扩展性适合理解底层流水与同步原语更接近生产算子常用的队列流水范式便于引入多缓冲/循环体两种实现的计算结果完全一致选型取决于场景学习流水线与同步机制选静态实现工程化算子开发建议从 TPipe/TQue 范式起步它与昇腾 算子开发编程指南 中的流水设计思路一脉相承。编译与运行环境变量配置根据当前环境上 CANN 开发套件包的安装方式配置环境变量source ${install_path}/cann/set_env.sh说明${install_path}为 CANN 包安装目录未指定安装目录时默认为/usr/local/Ascend。静态实现add的编译执行在样例目录下依次执行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。队列实现add_tpipe_tque的编译执行该样例额外包含数据生成与结果校验环节需要 Python3 与 numpy 环境mkdir -p build cd build; # 创建并进入 build 目录 cmake -DCMAKE_ASC_ARCHITECTURESdav-2201 ..;make -j; # 编译工程默认 npu 模式 python3 ../scripts/gen_data.py # 生成测试输入数据 ./demo # 执行编译产物运行样例 python3 ../scripts/verify_result.py output/output.bin output/golden.bin # 校验输出结果是否正确其中 gen_data.py 使用np.random.uniform(1, 10, [8, 2048])生成随机输入并将逐元素相加的 golden 写入output/golden.binverify_result.py 采用np.isclose做容差比对相对容差 1e-4、绝对容差 1e-5错误元素占比不超过 1e-4 即判定通过并打印test pass!。输入/输出数据通过 data_utils.h 中的ReadFile/WriteFile以二进制格式读写。编译选项说明选项可选值说明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 REQUIRED)引入 Ascend C 工具链以project(kernel_samples LANGUAGES ASC CXX)声明 ASC 语言add_executable(demo add.asc)将.asc源文件直接编译为可执行文件并通过--npu-arch${CMAKE_ASC_ARCHITECTURES}指定目标 NPU 架构。运行结果执行完成后输出如下表示精度比对成功test pass!功能调试printf 与 DumpTensorprintf 格式化输出printf 接口提供 CPU/NPU 域调试场景的格式化输出功能在算子核侧实现代码中需要输出日志信息的位置调用即可AscendC::printf(add blockIdx%d\n, AscendC::GetBlockIdx());注意printfPRINTF接口的打印功能会对算子实际运行性能产生一定影响通常在调试阶段使用。开发者可通过设置ASCENDC_DUMP0按需关闭打印功能。DumpTensor 张量内容导出对于基于算子工程开发的算子DumpTensor 接口可用于导出指定 LocalTensor 的内容并支持打印自定义附加信息仅支持 uint32_t 类型信息如打印当前行号。在算子核侧实现代码中需要打印 Tensor 数据的位置调用例如// 向量计算: z x y AscendC::Add(zLocal, xLocal, yLocal, blockLength); AscendC::DumpTensor(zLocal, 1, 32);注意DumpTensor 接口的打印功能会对算子实际运行性能产生一定影响通常在调试阶段使用。开发者可通过设置ASCENDC_DUMP0按需关闭打印功能。值得一提的是add.asc 中预置了一段被#if 0屏蔽的调试代码开启后可在计算完成后依次导出 xLocal、yLocal、zLocal 的前 32 个元素是学习该接口用法的现成范例。性能调试msOpProf 工具msOpProf 是单算子性能分析工具提供msopprof与msopprof simulator两种使用模式可帮助用户识别算子在内存、代码和指令方面的异常用于全面的算子调优。它支持对不同运行模式真机或仿真和文件类型可执行程序或算子二进制.o文件进行性能数据采集与自动解析。真机性能采集直接测量算子在昇腾 AI 处理器上的执行时间适合在真机环境快速定位算子性能问题。对demo可执行程序执行算子调优msopprof ./demo命令完成后默认目录下会生成名为OPPROF_{timestamp}_XXX的文件夹性能数据目录结构如下├──dump # 原始性能数据用户无需查看 ├──ArithmeticUtilization.csv # Cube/Vector 指令周期占比 ├──L2Cache.csv # L2 Cache 命中率影响 MTE2合理规划数据搬运逻辑可提高命中率 ├──Memory.csv # UB、L1、主存读写带宽利用率 ├──MemoryL0.csv # L0A、L0B、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.csvmsOpProf 的完整使用说明含仿真模式、.o文件分析等进阶用法收录在算子调优msOpProf对应的官方用户指南中可结合 CANN 工具链文档查阅。可优化方向与进阶路径序号可优化方向当前实现的问题预期优化收益1多核动态分配固定使用 8 个核未根据实际可用核数动态分配动态获取可用核数充分利用多核并行降低端到端时延2增大搬运粒度每次搬运 2048 个 float 元素8KB搬运粒度相对较小增大单次搬运数据量、减少搬运次数摊销启动开销提升带宽利用率3双缓冲流水并行加载、计算、存储三个阶段严格串行硬件单元MTE2/V/MTE3无法同时工作采用 Ping-Pong 双缓冲机制使加载、计算、存储并行执行隐藏搬运时延4L2 Cache 旁路Add 输入数据只读取一次却默认经过 L2 Cache增加 Cache 污染对流式访问数据设置 L2 Cache 旁路减少不必要的 Cache 开销提升搬运效率上述方向中搬运粒度还与 DataCopy 的底层约束直接相关count * sizeof(T)需要 32 字节对齐若未对齐搬运量会向下取整到 32 字节对齐。样例中每次搬运 2048 个 float8192 字节恰好满足 32 字节对齐要求这也是切分粒度设计时需要始终牢记的硬性约束。完整的性能调优过程可参考 Add 高性能调优样例该样例是入门版本地到双缓冲流水、多核动态分配等优化手法的进阶续篇。赞分享人工智能深度学习算子库CANNAscend【免费下载链接】asc-devkit本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言原生支持C和C标准规范主要由类库和语言扩展层构成提供多层级API满足多维场景算子开发诉求。项目地址https://gitcode.com/cann/asc-devkit点击查看免费下载相关推荐CANN Ascend C 入门实战基于 TPipe 与 TQue 的 Add 向量加法样例深度解析CANN Ascend C 入门实战基于 TPipe 与 TQue 的 Add 向量加法样例深度解析 本篇技术指南以 cann samples 仓库中 add示例工程CANNCANN cann-samples 实战基于 Ascend C 的 Add 向量加法算子——静态 Tensor 与 TPipe/TQue 两种编程范式解析CANN cann samples 实战基于 Ascend C 的 Add 向量加法算子——静态 Tensor 与 TPipe/TQue 两种编程范式解析 导示例工程CANN基于静态 Tensor 的 Ascend C 向量加法算子实战cann-samples 中 Add 样例全流程解析基于静态 Tensor 的 Ascend C 向量加法算子实战cann samples 中 Add 样例全流程解析 本篇技术指南以 CANN cann sam示例工程CANN上一篇英雄联盟界面定制终极指南3分钟掌握LeaguePrank段位伪装技巧 下一篇cann/asc-devkit SIMT线程块size方法文档创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表