跳到主要内容
昇腾AI芯片与算子开发

2.3 AscendCL 算子调用与 aclnn 标准算子库

掌握 aclnn 标准算子库的两段式调用范式(GetWorkspaceSize + 执行),通过 aclnnAdd 示例走通从数据准备到算子下发的完整流程,理解昇腾单算子调用与 CUDA kernel launch 的区别

aclnn AscendCL 算子调用 昇腾

上一章我们搭建了 AscendCL 的资源骨架。现在补上最关键的一块:怎么真的让昇腾算起来。这一章介绍 aclnn——昇腾的标准算子库接口,你会看到「调用一个算子」比 CUDA 里写一个 kernel 简单得多,但背后的异步执行、workspace 管理逻辑值得仔细品味。

📑 目录


1. aclnn 是什么

aclnn(Ascend Neural Network,昇腾神经网络算子库接口) 是 CANN 提供的一组 C API,把昇腾的高性能算子(Add、MatMul、Softmax、LayerNorm、Attention 相关算子等)包装成统一可调用的形式。

  • 命名规范:aclnn<算子名>(如 aclnnAdd、aclnnMatmul、aclnnSoftmax)。
  • 功能定位 ≈ cuBLAS + cuDNN 的合体:通用矩阵乘、卷积、归一化、激活等常用算子都有现成实现,开发者不需要自己写 kernel。
  • 是「应用层」与「算子层」之间的桥梁:torch_npu 派发 PyTorch 算子的终点,往往就是 aclnn。

对 AI Infra 工程师来说,aclnn 的意义是:先会用现成的高性能算子,再决定要不要手写 Ascend C 算子。

2. aclnn 算子的两段式调用范式

几乎所有 aclnn* 算子都遵循同样的两段式调用:

第一步:aclnnXxxGetWorkspaceSize(输入..., 输出..., &workspaceSize, &executor)
     —— 查一下这个算子这次执行需要多大的临时工作区(workspace),并创建执行器
第二步:aclnnXxx(executor, workspace, workspaceSize, stream)
     —— 把工作区交给执行器,在指定 Stream 上异步下发算子

示例(aclnnAdd,向量加法 dst = src1 + src2):

// 假设已定义输入张量描述与数据(见第3节)
size_t workspaceSize = 0;
aclnnAddExecutor executor = nullptr;

// 第一步:获取 workspace 大小并创建执行器
aclnnAddGetWorkspaceSize(
    src1Desc, src1Data, src2Desc, src2Data,
    dstDesc, dstData,
    &workspaceSize, &executor);

// 第二步:分配 workspace 并执行算子(异步)
void *workspace = nullptr;
if (workspaceSize > 0) {
    aclrtMalloc(&workspace, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);
}
aclnnAdd(executor, workspace, workspaceSize, stream);

// 同步并释放
aclrtSynchronizeStream(stream);
aclDestroyAclnnAddExecutor(executor);   // 释放执行器(如需)
if (workspace) aclrtFree(workspace);

为什么分两步?因为 不同算子、不同形状需要的临时工作区大小不同。第一步查询、第二步执行,让调用方可以统一管理 workspace 内存,避免算子内部「偷偷 malloc」。

3. 数据准备:Tensor 与 aclTensorDesc

调用 aclnn 前,需要先描述数据的「形状与类型」——用 aclTensorDesc:

// 构造一个 shape=[1024], dtype=float32 的输入张量描述
const int64_t shape[] = {1024};
aclDataType dataType = ACL_FLOAT;
aclFormat format = ACL_FORMAT_ND;
int64_t ndim = 1;

aclTensorDesc *src1Desc = aclCreateTensorDesc(dataType, ndim, shape, format);

再加上实际的数据指针(来自 aclrtMalloc 分配的设备内存),就构成一次 aclnn 调用的完整输入。Desc 描述「形状与类型」,指针指向「真实数据」——这个二分法贯穿所有 aclnn 接口。

4. 完整示例:aclnnAdd 跑通向量加法

把前几节的碎片拼起来,就是一个完整可编译的昇腾程序:

#include "acl/acl.h"
#include "acl/acl_op_compiler.h"
#include <cstdio>

int main() {
    aclInit(nullptr);
    int32_t deviceId = 0;
    aclrtSetDevice(deviceId);

    aclrtStream stream;
    aclrtCreateStream(&stream);

    const int64_t num = 1024;
    const int64_t shape[] = {num};
    int64_t ndim = 1;

    // 1. 分配设备内存
    void *src1 = nullptr, *src2 = nullptr, *dst = nullptr;
    size_t bytes = num * sizeof(float);
    aclrtMalloc(&src1, bytes, ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMalloc(&src2, bytes, ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMalloc(&dst,  bytes, ACL_MEM_MALLOC_HUGE_FIRST);

    // 2. 构造张量描述
    aclTensorDesc *src1Desc = aclCreateTensorDesc(ACL_FLOAT, ndim, shape, ACL_FORMAT_ND);
    aclTensorDesc *src2Desc = aclCreateTensorDesc(ACL_FLOAT, ndim, shape, ACL_FORMAT_ND);
    aclTensorDesc *dstDesc  = aclCreateTensorDesc(ACL_FLOAT, ndim, shape, ACL_FORMAT_ND);

    // 3. 两段式调用 aclnnAdd
    size_t workspaceSize = 0;
    aclnnAddExecutor executor = nullptr;
    aclnnAddGetWorkspaceSize(src1Desc, src1, src2Desc, src2,
                             dstDesc, dst, &workspaceSize, &executor);
    void *workspace = nullptr;
    if (workspaceSize > 0)
        aclrtMalloc(&workspace, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);
    aclnnAdd(executor, workspace, workspaceSize, stream);

    // 4. 同步并清理
    aclrtSynchronizeStream(stream);
    aclDestroyTensorDesc(src1Desc);
    aclDestroyTensorDesc(src2Desc);
    aclDestroyTensorDesc(dstDesc);
    aclrtFree(src1); aclrtFree(src2); aclrtFree(dst);
    if (workspace) aclrtFree(workspace);
    aclrtDestroyStream(stream);
    aclrtResetDevice(deviceId);
    aclFinalize();
    return 0;
}

这段代码就是昇腾「单算子编程」的模板:资源 → 数据 → 描述 → 两段式调用 → 同步清理。把 aclnnAdd 换成 aclnnMatmul、aclnnSoftmax,套路完全一样。

5. workspace:算子的临时工作区

workspace 是昇腾算子的一个特色概念,值得单独说:

  • 部分算子在执行时需要临时空间(例如 MatMul 的转置缓冲、Softmax 的中间结果)。
  • aclnn 把「需要多大空间」的查询(第一步)和「真正执行」(第二步)分开,让调用方统一分配。
  • 好处:内存申请次数可控、算子内部不隐藏大 malloc,对长期运行的服务(推理引擎)尤其友好。

对比 CUDA:cuBLAS 也有类似机制(cublasGetWorkspaceSize + 用户传入 workspace buffer),所以有经验的读者会发现这是个熟悉的模式。

6. 单算子调用与 CUDA kernel launch 的对比

维度CUDA(手写 kernel)aclnn(单算子调用)
编程成本要写 kernel、配 grid/block直接调用现成算子接口
性能可控到极致通常已是高性能实现(类似 cuBLAS)
灵活性完全自由受限(只能调库里有的算子)
调参thread/block、共享内存Tiling 由算子内部决定(也可配置属性)
适用场景算子开发、极致性能应用开发、快速部署

结论:aclnn 适合「会用就行」的应用层;想要「超出库的性能」或「库外的新算子」,才需要走向 Ascend C(本模块第 3 章)。

7. 异步执行与同步策略

  • aclnnXxx 是异步接口:调用后立即返回,实际计算在 Stream 上排队。
  • 结果可用的前提是 aclrtSynchronizeStream(stream) 或等价的同步点。
  • 多算子连续调用时,只要在同一个 Stream 上,就是天然串行、无需逐个同步;需要并行时再拆多条 Stream。

性能提示:把「数据搬移 + 一连串算子」放进同一个 Stream,最后统一同步一次,能最大化重叠与吞吐——这正是推理引擎(如 MindIE/vLLM-Ascend)的内部做法。

总结

  • aclnn 是昇腾标准算子库接口,命名 aclnn<算子名>,功能对标 cuBLAS/cuDNN。
  • 调用范式固定:aclnnXxxGetWorkspaceSize(查空间 + 建执行器)→ aclnnXxx(下发执行)。
  • Tensor 描述(aclTensorDesc)+ 设备内存指针 = 一次调用的全部输入。
  • workspace 让内存管理透明可控,长期运行的服务尤其受益。
  • aclnn 是应用层的「快速通道」;追求极致性能再走 Ascend C。

延伸阅读