news 2026/10/3 1:57:18

昇腾 C_API 编写 Add 算子实战:同步、异步与精细异步三种调用范式解析(基于 CANN asc-devkit)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
昇腾 C_API 编写 Add 算子实战:同步、异步与精细异步三种调用范式解析(基于 CANN asc-devkit)
  • 人工智能
  • 深度学习
  • 算子库
  • CANN
  • Ascend

【免费下载链接】asc-devkit

本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C++标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。

项目地址:https://gitcode.com/cann/asc-devkit
点击查看免费下载

导读

本文围绕 CANN asc-devkit 仓库中examples/02_simd_c_api/00_introduction/01_add下的三个 Add 算子样例展开,系统讲解如何使用昇腾 SIMD C_API(c_api/asc_simd.h)在单个.asc文件中同时实现 kernel 函数与 main 函数,并通过<<<>>>直调方式在主机侧拉起核函数。读完本文,你将掌握 C_API 的asc_copy_gm2ub/asc_copy_ub2gm数据搬运、asc_add向量计算、asc_sync全量同步以及asc_sync_notify/asc_sync_wait事件级流水同步三种编程范式,并能够独立完成编译、运行与精度校验。

样例总体介绍

仓库在 examples/02_simd_c_api/00_introduction/01_add 目录下提供了一个"基于 Ascend C 的 Add 算子<<<>>>直调方法"样例集,其核心特点是main 函数与 kernel 函数写在同一个 cpp(.asc)文件中,无需拆分 host/device 工程即可完成算子实现、调用与验证。样例集共包含三个子目录,分别演示三种不同的接口组合方式:

目录名称功能描述
c_api_async_add采用 C_API 接口实现 Add 算子,基于异步数据搬运与计算接口
c_api_delicacy_async_add采用 C_API 接口实现 Add 算子,基于异步搬运、计算接口,并手动添加同步指令控制流水线依赖
c_api_sync_add采用 C_API 接口实现 Add 算子,基于同步数据搬运与计算接口

三者计算逻辑完全一致,差异集中在核内流水线的同步策略上,非常适合作为理解昇腾 SIMD C_API 编程模型的入门阶梯。

算子规格与支持产品

三个样例的算子规格完全一致,如下表所示:

项目内容
算子类型(OpType)Add
输入 xshape2048*8(同步样例)/8*2048(两个异步样例),数据类型 float,格式 ND
输入 y与 x 同 shape、同类型、同格式
输出 z与输入同 shape、同类型、同格式
核函数名称add_custom

其中 shape 的书写顺序(2048*8与8*2048)不影响实际计算语义,因为核内均按"总长度 / block 数"进行分块;c_api_async_add中TOTAL_LENGTH = 8 * 2048 = 16384,与另外两个样例的元素总数完全一致。

算子功能即数学表达式z = x + y:将两个输入张量逐元素相加并返回结果。

支持产品范围:

  • Atlas A3 训练系列产品 / Atlas A3 推理系列产品
  • Atlas A2 训练系列产品 / Atlas A2 推理系列产品

构建配置方面,三个样例的 CMakeLists.txt 均要求 CMake 版本不低于 3.16,通过find_package(ASC REQUIRED)引入昇腾 C 语言扩展编译器,并将--npu-arch=dav-2201作为默认 NPU 架构编译选项;c_api_async_add的 CMakeLists 额外提供了CMAKE_ASC_ARCHITECTURES缓存变量,便于按实际部署的 NPU 硬件架构调整编译目标。

核内实现流程:三步式 Add 计算

C_API 编程模型中,设备侧数据不能直接被向量计算指令访问,输入必须先搬入片上存储(Local Memory / UB),计算完成后再搬回外部存储(Global Memory)。三个样例的核内实现均遵循以下三步:

  1. 第一步(搬入):将输入x、y从 Global Memory 搬运到 Local Memory,分别存入xLocal、yLocal;
  2. 第二步(计算):对xLocal、yLocal执行逐元素加法,结果存入zLocal;
  3. 第三步(搬出):将zLocal中的输出数据搬运回 Global Memory 的输出z。

在__vector__ __global__修饰的核函数入口处,首先调用asc_init()完成片上环境初始化,随后按block_idx计算当前核(AI Core 上运行的 block)负责的数据分片,再依次执行搬运—计算—搬运。

三种 C_API 实现范式源码解析

范式一:c_api_sync_add —— 同步搬运 + 显式asc_sync

同步样例的核函数源码位于 c_api_add.asc,核心代码段如下:

__vector__ __global__ __aicore__ void add_custom(__gm__ float* x, __gm__ float* y, __gm__ float* z) { asc_init(); __ubuf__ float xLocal[TILE_LENGTH]; __ubuf__ float yLocal[TILE_LENGTH]; __ubuf__ float zLocal[TILE_LENGTH]; uint32_t blockLength = TILE_LENGTH * NUM_BLOCKS / block_num; asc_copy_gm2ub(xLocal, (x + block_idx * blockLength), blockLength * sizeof(float)); asc_sync(); asc_copy_gm2ub(yLocal, (y + block_idx * blockLength), blockLength * sizeof(float)); asc_sync(); asc_add(zLocal, xLocal, yLocal, blockLength); asc_sync(); asc_copy_ub2gm((z + block_idx * blockLength), zLocal, blockLength * sizeof(float)); asc_sync(); }

要点解读:

  • __gm__表示 Global Memory 空间指针,__ubuf__表示片上 Unified Buffer(UB)空间数组;
  • asc_copy_gm2ub(dst, src, size)以字节数为单位将数据从全局搬入 UB,asc_copy_ub2gm反向搬出;
  • 每次搬运/计算之后都紧跟asc_sync()全量同步。asc_sync在 include/c_api/sync/sync.h 中声明,作用是等待流水线上所有已发出的任务执行完毕,保证后续指令能安全读取前序指令的产出数据;
  • asc_add(zLocal, xLocal, yLocal, blockLength)为向量加法接口,声明于 include/c_api/vector_compute/compute/vector_arith.h,第四个参数count为参与计算的元素个数;
  • blockLength = TILE_LENGTH * NUM_BLOCKS / block_num实现了数据在多核间的自适应切分:当实际启动的 block 数小于 8 时,每个 block 分到的数据量自动增大,保证总数据不遗漏。

这种"每步一同步"的写法最简单直观,代码可读性强、不易出错,但同步开销较大,流水线无法重叠执行。

范式二:c_api_async_add —— 异步搬运 + 批间同步

异步样例源码位于 c_api_add.asc,其核函数省去了__aicore__修饰,并把同步点从"每条指令之后"收敛为"阶段之间":

__vector__ __global__ void add_custom(__gm__ float* x, __gm__ float* y, __gm__ float* z) { asc_init(); constexpr uint32_t block_length = TOTAL_LENGTH / NUM_BLOCKS; __gm__ float* x_gm = x + block_idx * block_length; __gm__ float* y_gm = y + block_idx * block_length; __gm__ float* z_gm = z + block_idx * block_length; __ubuf__ float x_local[block_length]; __ubuf__ float y_local[block_length]; __ubuf__ float z_local[block_length]; asc_copy_gm2ub(x_local, x_gm, block_length * sizeof(float)); asc_copy_gm2ub(y_local, y_gm, block_length * sizeof(float)); asc_sync(); asc_add(z_local, x_local, y_local, block_length); asc_sync(); asc_copy_ub2gm(z_gm, z_local, block_length * sizeof(float)); asc_sync(); }

要点解读:

  • 两次asc_copy_gm2ub(x 与 y 的搬入)之间不再插入同步。由于二者写入的是不同的 UB 缓冲区(x_local与y_local),彼此无数据依赖,可异步并发下发,由同一流水级顺序执行即可保证正确性;
  • 同步点仅保留三处:全部搬入完成之后、向量计算完成之后、搬出完成之后,分别对应"搬运→计算""计算→搬出"两个数据依赖边界;
  • 这种写法比范式一少了一半同步指令,让数据搬运与后续指令的发射可以更早进行,属于"异步接口 + 粗粒度同步"的折中方案。

范式三:c_api_delicacy_async_add —— 异步接口 + 事件级手动同步

精细异步样例源码位于 c_api_add.asc,它不再依赖asc_sync()全量同步,而是把数据进一步切成C_API_TILE_NUM = 8个 tile,在循环中通过asc_sync_notify/asc_sync_wait精确控制MTE2(搬入)、Vector(计算)、MTE3(搬出)三条流水线之间的事件依赖,实现流水线级并行:

constexpr uint32_t C_API_ONE_BLOCK_SIZE = 32; constexpr uint32_t C_API_ONE_REPEAT_BYTE_SIZE = 256; constexpr uint32_t C_API_TOTAL_LENGTH = 16384; constexpr uint32_t C_API_TILE_NUM = 8; constexpr uint32_t C_API_TILE_LENGTH = 256; __vector__ __global__ __aicore__ void add_custom(__gm__ float* x, __gm__ float* y, __gm__ float* z) { asc_init(); uint32_t blockLength = C_API_TOTAL_LENGTH / block_num; uint32_t tileLength = blockLength / C_API_TILE_NUM; __gm__ float* xGm = x + block_idx * blockLength; __gm__ float* yGm = y + block_idx * blockLength; __gm__ float* zGm = z + block_idx * blockLength; __ubuf__ float xLocal[C_API_TILE_LENGTH]; __ubuf__ float yLocal[C_API_TILE_LENGTH]; __ubuf__ float zLocal[C_API_TILE_LENGTH]; uint16_t burst_len = tileLength; for (uint32_t i = 0; i < C_API_TILE_NUM; i++) { if (i != 0) { asc_sync_wait(PIPE_V, PIPE_MTE2, EVENT_ID0); } burst_len = tileLength * sizeof(float) / C_API_ONE_BLOCK_SIZE; asc_copy_gm2ub(xLocal, xGm + i * tileLength, 1, burst_len, 0, 0); asc_copy_gm2ub(yLocal, yGm + i * tileLength, 1, burst_len, 0, 0); asc_sync_notify(PIPE_MTE2, PIPE_V, EVENT_ID0); asc_sync_wait(PIPE_MTE2, PIPE_V, EVENT_ID0); if (i != 0) { asc_sync_wait(PIPE_MTE3, PIPE_V, EVENT_ID0); } asc_add(zLocal, xLocal, yLocal, tileLength * sizeof(float) / C_API_ONE_REPEAT_BYTE_SIZE, 1, 1, 1, 8, 8, 8); if (i != (C_API_TILE_NUM - 1)) { asc_sync_notify(PIPE_V, PIPE_MTE2, EVENT_ID0); } asc_sync_notify(PIPE_V, PIPE_MTE3, EVENT_ID0); asc_sync_wait(PIPE_V, PIPE_MTE3, EVENT_ID0); asc_copy_ub2gm(zGm + i * tileLength, zLocal, 1, burst_len, 0, 0); if (i != (C_API_TILE_NUM - 1)) { asc_sync_notify(PIPE_MTE3, PIPE_V, EVENT_ID0); } } }

要点解读:

  • asc_sync_notify(pipe, tpipe, id)与asc_sync_wait(pipe, tpipe, id)是事件级同步原语(声明于 include/c_api/sync/sync.h),其中pipe/tpipe为流水线类型(PIPE_V向量计算、PIPE_MTE2数据搬入、PIPE_MTE3数据搬出),id为事件号(样例统一复用EVENT_ID0)。语义为:notify方完成该流水级任务后发送事件,wait方在对应流水级等待该事件;
  • 循环体内形成了三阶段流水:第i次迭代搬入第i个 tile → 计算第i个 tile → 搬出第i个 tile;同时通过asc_sync_notify(PIPE_V, PIPE_MTE2, ...)告知 Vector 计算已完成、可以搬入下一个 tile,实现前一个 tile 的计算与后一个 tile 的搬入重叠;
  • asc_copy_gm2ub使用了扩展签名:asc_copy_gm2ub(dst, src, 1, burst_len, 0, 0),其中burst_len = tileLength * sizeof(float) / 32表示按 32 字节 block 为单位的搬运突发长度,这是针对连续内存块的高效搬运写法;
  • asc_add使用了扩展签名:asc_add(dst, src0, src1, repeat, 1, 1, 1, 8, 8, 8),repeat = tileLength * sizeof(float) / 256表示重复执行次数(256 字节/次),后续参数为迭代间隔与重复间隔等硬件调度参数。结合 include/c_api/vector_compute/compute/vector_arith.h 中asc_add的多个重载可见,C_API 同时提供"按元素数"与"按 repeat 次数"两种粒度的计算接口;
  • 该样例的主机侧输入使用常量初始化(x全为1.2f、y全为2.3f),golden 直接取1.2 + 2.3 = 3.5f,便于精确比对。

这是三种写法中性能上限最高的方案,代价是同步逻辑复杂、依赖关系需要开发者手工维护。

主机侧调用链:ACL Runtime 直调<<<>>>

三个样例的调用侧实现高度一致,均封装在kernel_add函数中,展示了一条完整的"host 侧数据准备 → 核函数直调 → 结果回拷 → 资源释放"链路,以同步样例为例:

aclInit(nullptr); aclrtSetDevice(deviceId); aclrtCreateStream(&stream); aclrtMallocHost((void**)(&zHost), totalByteSize); aclrtMalloc((void**)&xDevice, totalByteSize, ACL_MEM_MALLOC_HUGE_FIRST); aclrtMalloc((void**)&yDevice, totalByteSize, ACL_MEM_MALLOC_HUGE_FIRST); aclrtMalloc((void**)&zDevice, totalByteSize, ACL_MEM_MALLOC_HUGE_FIRST); aclrtMemcpy((uint8_t*)xDevice, totalByteSize, xHost, totalByteSize, ACL_MEMCPY_HOST_TO_DEVICE); aclrtMemcpy((uint8_t*)yDevice, totalByteSize, yHost, totalByteSize, ACL_MEMCPY_HOST_TO_DEVICE); add_custom<<<numBlocks, 0, stream>>>(xDevice, yDevice, zDevice); aclrtSynchronizeStream(stream); aclrtMemcpy(zHost, totalByteSize, (uint8_t*)zDevice, totalByteSize, ACL_MEMCPY_DEVICE_TO_HOST);

关键点:

  • 依次执行aclInit(初始化 ACL)、aclrtSetDevice(指定 device)、aclrtCreateStream(创建执行流);
  • 设备侧内存通过aclrtMalloc以ACL_MEM_MALLOC_HUGE_FIRST策略申请,主机侧通过aclrtMallocHost申请;
  • 核函数调用采用 CUDA 风格的<<<numBlocks, 0, stream>>>直调语法:第一个参数为 grid 维 block 数(样例取NUM_BLOCKS = 8,与核内block_num对应),第二个参数为局部 block 维大小(取 0),第三个参数为 ACL stream;
  • c_api_sync_add与c_api_delicacy_async_add在调用后使用aclrtSynchronizeStream(stream)等待流内任务完成,c_api_async_add使用aclrtSynchronizeDevice()等待设备整体同步——这是异步样例与另外两个样例在主机侧的细微差异;
  • 结果通过aclrtMemcpy以ACL_MEMCPY_DEVICE_TO_HOST模式回拷到主机,随后按逆序释放设备内存、销毁 stream、aclrtResetDevice并aclFinalize收尾。

编译与运行

在任意一个样例目录的根目录下,按以下步骤即可完成编译与运行。

1. 配置环境变量:根据当前环境上 CANN 开发套件包的安装方式,选择对应的命令:

# 默认安装路径,root 用户安装 source /usr/local/Ascend/cann/set_env.sh # 默认安装路径,非 root 用户安装 source $HOME/Ascend/cann/set_env.sh # 自定义安装路径 install_path source ${install_path}/cann/set_env.sh

2. 编译并执行:

mkdir -p build && cd build # 创建并进入 build 目录 cmake ..; make -j # 编译工程 ./c_api_add_example # 执行样例

编译过程中,find_package(ASC REQUIRED)会通过 CANN 提供的 ASC 语言扩展编译工具链将.asc文件中的__vector__ __global__核函数与主机代码编译为可执行文件;--npu-arch=dav-2201指定目标 NPU 架构,实际部署时应按硬件修改该参数(详见各样例的 CMakeLists.txt)。

3. 预期输出:执行成功后打印精度比对结果,表示算子计算结果与 golden 完全一致:

[Success] Case accuracy is verification passed.

精度校验逻辑

三个样例均内置了VerifyResult/verify_result函数完成端到端校验,流程为:

  1. 打印输出张量与 golden 张量的前 20 个元素(多于 20 个时以...截断),便于人工观察;
  2. 通过std::equal逐元素比对设备计算结果与主机 golden;
  3. 一致则打印[Success] Case accuracy is verification passed.并返回 0,否则打印[Failed] Case accuracy is verification failed!并返回 1。

golden 的构造方式:同步与异步样例在主机侧初始化x[i] = i * 0.1f; y[i] = i * 0.1f;,golden 取x[i] + y[i];精细异步样例则用常量初始化并直接以valueX + valueY构造 golden。整个 main 函数采用"构造数据 → 调用kernel_add上板计算 → 构造 golden → 精度比对 → 返回退出码"的结构,可直接作为回归用例复用。

三种范式的选型建议

实现范式同步策略代码复杂度流水线重叠适用场景
c_api_sync_add每条指令后asc_sync()低无入门学习、验证算法正确性
c_api_async_add阶段间asc_sync()中搬运指令间部分重叠常规算子开发,兼顾可读性与性能
c_api_delicacy_async_addasc_sync_notify/asc_sync_wait事件同步高搬入—计算—搬出三流水线深度重叠追求极致性能、UB 复用受限的高阶场景

建议初学者先对照同步样例理解"搬运—计算—搬出"三步模型,再依次进阶到异步与精细异步写法;当算子存在多组独立数据分块时,事件级同步能带来最明显的性能收益,但务必核对每个notify/wait的流水线方向与事件号,避免引入数据竞争。更多 C_API 接口与算子开发规范可参考 docs/en/quick_start.md 及 include/c_api/asc_simd.h 接口声明。

  • 人工智能
  • 深度学习
  • 算子库
  • CANN
  • Ascend

【免费下载链接】asc-devkit

本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C++标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。

项目地址:https://gitcode.com/cann/asc-devkit
点击查看免费下载

相关推荐

上一篇:aws-amplify数据迁移策略:从传统数据库到云原生存储的无缝过渡
下一篇:VSCode-GitLens访问错误:AccessDeniedError与权限处理

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/3 1:55:27

IM未读数与红点方案选型:从服务端一致性到状态机设计

IM会话未读数和红点方案选型&#xff0c;这个话题我琢磨了挺久。凡是做过即时通讯&#xff08;IM&#xff09;客户端或服务端的同学&#xff0c;基本都绕不过这一关。未读数看起来是个简单的东西——无非就是数字加减、红点显隐&#xff0c;但真正落地的时候&#xff0c;你会发…

作者头像 李华
网站建设 2026/10/3 1:55:13

光伏制造企业SAP与金蝶云星空ERP集成方案与接口实现详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华