用 AscendCL 跑第一个昇腾推理:一个部署工程师的实操记录
刚接触 CANN 推理时我在想一件事:AI 模型编译完送进昇腾NPU,这中间谁在管 Device 初始化、模型加载、内存搬运这些事情?答案是 AscendCL——CANN 最上层的应用编程接口。
这篇文章记录我从零开始在昇腾 NPU 上跑通第一个推理 Demo 的全部步骤和踩坑点,少理论,多实操。
AscendCL 到底是什么
AscendCL 是 CANN 五层架构中的最顶层——异构计算语言层。它封装了底层的 Runtime 调用、GE 图执行和算子调度,对外暴露一套 C API。
开发者调用 AscendCL 时不需要关心 Runtime Stream 怎么管理、GE 怎么执行、算子怎么分发——这些全部被封装了。你需要做的就几步:LoadModel、CreateInput、Execute、GetOutput。
AscendCL 的身份很明确:它是 CANN 推理的"面向开发者"的接口。如果你只想做推理部署而不是写算子,AscendCL 就是你需要面对的唯一一套 API。
环境安装
硬件检查:
npu-smi info
能看到 NPU 型号和驱动版本。推理最低要求驱动版本 22.0.0+,CANN Toolkit 7.0+。
CANN 安装(Ubuntu 环境):
wget https://www.hiascend.com/software/CANN/community
chmod +x Ascend-cann-toolkit_8.0_x86_64.run
./Ascend-cann-toolkit_8.0_x86_64.run --install
# 设置环境变量
source /usr/local/Ascend/ascend-toolkit/set_env.sh
装完后 npu-smi info 能正常显示 NPU 状态,find /usr/local/Ascend -name "acl.h" 能找到头文件路径,环境就绪了。
模型准备: 用 ATC 工具把 ONNX 或 PyTorch 模型转换成 OM 格式。
atc --model=model.onnx --framework=5 --output=model --soc_version=Ascend910
--soc_version 根据实际 NPU 型号填写,比如 910B 也用 Ascend910(昇腾 NPU 型号命名不做 910A/910B 区分)。转换成功后会生成 model.om。
第一个推理 Demo
AscendCL 推理的完整流程:
Device初始化 → Context创建 → 模型加载 → 创建输入输出Buffer → 推理执行 → 获取结果 → 释放资源
下面是完整代码,模型以 OM 格式输入,输入是预处理好的 float 数组。
#include <iostream>
#include "acl/acl.h"
int main(int argc, char *argv[]) {
// 1. Device 初始化
int32_t deviceId = 0;
aclError ret = aclInit(nullptr);
ret = aclrtSetDevice(deviceId);
// 2. 创建 Context——当前线程的执行上下文
aclrtContext context;
ret = aclrtCreateContext(&context, deviceId);
// 3. 加载 OM 模型到 Device
uint32_t modelId;
ret = aclmdlLoadFromFile("./model.om", &modelId);
// 4. 创建输入输出 Buffer——必须在 Device 上分配
aclmdlDesc *modelDesc = aclmdlCreateDesc();
ret = aclmdlGetDesc(modelDesc, modelId);
size_t inputSize = aclmdlGetInputSizeByIndex(modelDesc, 0);
size_t outputSize = aclmdlGetOutputSizeByIndex(modelDesc, 0);
void *inputBuffer = nullptr;
void *outputBuffer = nullptr;
// 用 HUGE_FIRST 策略让 Runtime 优先从大块池分配
ret = aclrtMalloc(&inputBuffer, inputSize, ACL_MEM_MALLOC_HUGE_FIRST);
ret = aclrtMalloc(&outputBuffer, outputSize, ACL_MEM_MALLOC_HUGE_FIRST);
// 把输入数据从 Host 拷到 Device Buffer
float inputData[INPUT_ELEM_COUNT] = { /* 填充预处理后的数据 */ };
ret = aclrtMemcpy(inputBuffer, inputSize, inputData, inputSize,
ACL_MEMCPY_HOST_TO_DEVICE);
// 5. 创建数据集——把 Buffer 包装成 Dataset
aclmdlDataset *inputDataSet = aclmdlCreateDataset();
aclDataBuffer *inputDataBuf = aclCreateDataBuffer(inputBuffer, inputSize);
aclmdlAddDatasetBuffer(inputDataSet, inputDataBuf);
aclmdlDataset *outputDataSet = aclmdlCreateDataset();
aclDataBuffer *outputDataBuf = aclCreateDataBuffer(outputBuffer, outputSize);
aclmdlAddDatasetBuffer(outputDataSet, outputDataBuf);
// 6. 推理执行——同步模式
ret = aclmdlExecute(modelId, inputDataSet, outputDataSet);
// 7. 结果从 Device 拷回 Host
float resultData[OUTPUT_ELEM_COUNT];
ret = aclrtMemcpy(resultData, outputSize, outputBuffer, outputSize,
ACL_MEMCPY_DEVICE_TO_HOST);
std::cout << "推理完成" << std::endl;
// 处理 resultData...
// 8. 释放资源——释放顺序不要乱
aclDestroyDataBuffer(inputDataBuf);
aclDestroyDataBuffer(outputDataBuf);
aclmdlDestroyDataset(inputDataSet);
aclmdlDestroyDataset(outputDataSet);
aclrtFree(inputBuffer);
aclrtFree(outputBuffer);
aclmdlDestroyDesc(modelDesc);
aclmdlUnload(modelId);
aclrtDestroyContext(context);
aclrtResetDevice(deviceId);
aclFinalize();
return 0;
}
编译命令:
g++ -o infer_demo infer_demo.cpp -I/usr/local/Ascend/ascend-toolkit/latest/include \
-L/usr/local/Ascend/ascend-toolkit/latest/lib64 -lascendcl -std=c++11
LD_LIBRARY_PATH 记得包含 Ascend 的 lib64 目录,否则运行时找不到 libascendcl.so。
Buffer 管理:最常见的四个踩坑点
坑 1:Input Buffer 大小不匹配。 aclmdlGetInputSizeByIndex 返回模型要求的精确尺寸。如果输入做完预处理后尺寸不一致,aclrtMemcpy 不会报错,但推理结果完全错乱。必须在 CreateDataBuffer 之前先比对大小。
坑 2:Device 内存泄漏。 推理框架中反复 aclrtMalloc / aclrtFree 的代码很容易漏掉某个异常分支的 Free。CANN Runtime 在进程退出时不会自动回收泄露的 Device 显存,长时间推理服务的显存会持续增长。建议开发阶段每个 aclrtMalloc 都配对写 aclrtFree,用对称缩进规范化写法。
坑 3:多线程下 Context 未绑定。 每个线程必须有自己的 Context。非主线程调用推理前做了 aclrtSetDevice 但没做 aclrtCreateContext,推理接口会直接返回 ACL_ERROR_INVALID_CONTEXT。排查时先看错误码是不是 107005。
坑 4:Host 内存对齐。 CANN Device 要求部分 Buffer 按 64 字节对齐。用 aclrtMallocHost 代替普通 malloc 可以自动满足对齐要求。自己 malloc 再传给推理接口可能出现 ACL_ERROR_INVALID_MALLOC_TYPE。
异步执行:进阶选项
上面的 Demo 用 aclmdlExecute——同步模式,NPU 执行期间 Host 线程阻塞。对吞吐敏感的场景应该用异步版本:
aclrtStream stream;
aclrtCreateStream(&stream);
// 提交推理到 Stream,立即返回
aclmdlExecuteAsync(modelId, inputDataSet, outputDataSet, stream);
// Host 侧不空等,可以准备下一批
prepare_next_batch();
// 在需要结果时同步
aclrtSynchronizeStream(stream);
异步模式的核心收益是 Host 侧不空等。预处理、后处理、多组输入的 pipeline 编排都可以通过 Stream 来管理。一个 Stream 提交推理,另一个 Stream 搬运数据,两条流水线同时推进。
为什么 ACL 是 CANN 推理的核心
AscendCL 是大部分部署开发者接触 CANN 的第一个入口,也是最后一层抽象——它下面是 Runtime 的 Stream 调度和 GE 的图优化,上面是你自己的工程代码。懂 AscendCL 的 API 语义不代表懂它的机制,但不懂 AscendCL 连推理都跑不起来。
入门后值得深入 ACL Runtime 层——理解 aclrtSetDevice 底下为什么需要一个 Context,推理时 Stream 是怎么创建的,多流场景下如何手动编排流水线——这些知识决定了你的推理服务能跑到多少吞吐。
Model Load 的更多细节
aclmdlLoadFromFile 做的事不止是读文件。GE 在背后解析 OM 模型的计算图,对图中的算子做融合优化,然后分配执行所需的全部 Tensor 内存。这意味着模型加载阶段已经完成了图优化的大部分工作。
一个常见的疑问是:aclmdlLoadFromFile 后能不能换模型?答案是模型运行时模型描述是只读的。想换模型必须 Unload 再 Load。如果是多模型服务,建议在初始化阶段全部 Load 完,推理时通过 modelId 切换,省去 reload 开销。
uint32_t modelA, modelB;
aclmdlLoadFromFile("./modelA.om", &modelA);
aclmdlLoadFromFile("./modelB.om", &modelB);
// 推理时直接切换 modelId 就行
aclmdlExecute(modelA, input, output);
aclmdlExecute(modelB, input, output);
内存占用方面,每个 Load 的模型都会在 NPU 显存中占用一份常驻空间。模型太多时需要注意显存总量是否够用。
结语
跑通第一个推理 Demo 通常半天到一天,但把推理代码写得稳定、高效、没有资源泄漏,需要更深的 Buffer 管理理解和多线程 Context 认知。先跑起来,再优化。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)