第一次在昇腾 NPU 上跑推理,很多人卡在第一步:环境装好了,ATC 模型转换也成功了,一跑推理程序就报 aclInit failed 或者 load model failed

我当年第一次跑 ACL 推理,环境装了 3 遍,模型转了 5 遍,推理程序编译通过但运行就 core dump。最后发现是环境变量没配全——LD_LIBRARY_PATH 少了 /usr/local/Ascend/nnrt/latest/acllib/lib64,导致运行时找不到 libascendcl.so

这篇文章把我踩过的坑全部列出来,你照着做,10 分钟内必能跑通。

第一步:CANN 环境安装

1.1 确认硬件和环境

先确认你有昇腾 NPU(910/910B/310 都行),且系统是 Ubuntu 18.04/20.04 或 CentOS 7.x。

# 看有没有 NPU 设备
ls /dev/davinci*
# 有输出(比如 /dev/davinci0)说明驱动装好了

没输出?先装驱动。去昇腾社区下载对应版本的驱动([https://www.hiascend.com/hardware/firmware-drivers]),按文档装完重启。

1.2 安装 CANN Toolkit 和 NNRt

CANN 有两个包:Toolkit(开发用,含编译工具链)和 NNRt(运行时,含 ACL 库)。

# 下载 CANN 8.0.RC1 版本(示例,具体版本看你 NPU 驱动版本)
# 去 https://www.hiascend.com/software/cann/community-history 下载

# 安装 Toolkit(开发机装)
./Ascend-cann-toolkit_8.0.RC1_linux-x86_64.run --full

# 安装 NNRt(运行机装,如果开发运行同一台机器,两个都装)
./Ascend-cann-nnrt_8.0.RC1_linux-x86_64.run --full

90% 新手都会踩的坑 No.1:装完不配环境变量。 装完 CANN 一定要配环境变量,否则编译时找不到头文件,运行时找不到库。

第二步:环境变量配置

这是最容易出问题的地方。很多人以为 /etc/profile 里配一次就完事了,其实每次开新终端都要 source。

创建环境变量配置文件 ~/.cannrc

# ~/.cannrc
export ASCEND_HOME=/usr/local/Ascend
export CANN_HOME=$ASCEND_HOME/nnrt/latest
export PATH=$ASCEND_HOME/toolkit/latest/bin:$PATH
export LD_LIBRARY_PATH=$CANN_HOME/acllib/lib64:$ASCEND_HOME/driver/lib64:$LD_LIBRARY_PATH
export ASCEND_OPP_PATH=$ASCEND_HOME/nnrt/latest/opp

每次开新终端都要 source:

source ~/.cannrc

或者写进 ~/.bashrc(一劳永逸):

echo "source ~/.cannrc" >> ~/.bashrc

验证环境变量是否配好:

# 看能不能找到 ATC 工具
which atc
# 输出应该是 /usr/local/Ascend/toolkit/latest/bin/atc

# 看能不能找到 ACL 库
ldconfig -p | grep ascendcl
# 输出应该有一行 libascendcl.so

90% 新手都会踩的坑 No.2LD_LIBRARY_PATH 配了但没生效。 用 ldconfig -p | grep ascendcl 验证。如果没输出,说明库路径没配进去。检查 ~/.cannrc 里的路径是否真实存在(比如 nnrt/latest 是不是软链接,有时候装完叫 nnrt/8.0.RC1)。

第三步:模型转换(ATC)

ACL 推理需要 OM 格式的模型(离线模型)。你手里的 ONNX/PyTorch/TensorFlow 模型需要先转成 OM。

3.1 准备 ONNX 模型

如果手头没有 ONNX 模型,用 PyTorch 导出一个 ResNet-50:

# export_resnet50.py
import torch
import torchvision

model = torchvision.models.resnet50(pretrained=False)
model.eval()
dummy_input = torch.randn(1, 3, 224, 224)
torch.onnx.export(model, dummy_input, "resnet50.onnx", 
                  input_names=["input"], output_names=["output"])

3.2 用 ATC 转 OM

atc --model=resnet50.onnx \
    --framework=5 \
    --output=resnet50 \
    --input_format=NCHW \
    --input_shape="input:1,3,224,224" \
    --log=info

参数解释(解释 WHY 而非 WHAT):

  • --framework=5:ONNX 的格式编号是 5(1=Caffe, 3=TensorFlow, 5=ONNX, 6=PyTorch)
  • --input_format=NCHW:NPU 要求输入是 NCHW 格式(跟 PyTorch 一致)
  • --output=resnet50:输出的 OM 模型叫 resnet50.om(自动加 .om 后缀)

转换成功会看到 ATC run success,生成 resnet50.om 文件。

90% 新手都会踩的坑 No.3:模型转换成功,但推理时报 load model failed。 原因:ATC 转换时用的 CANN 版本,跟推理程序编译时链接的 CANN 版本不一致。解决:保证转换和运行用同一个 CANN 版本(比如都是 8.0.RC1)。

第四步:写 ACL 推理代码(C++)

这是核心。ACL 推理分 5 步:初始化 → 加载模型 → 准备输入 → 执行推理 → 解析输出。

4.1 目录结构

acl_inference/
├── CMakeLists.txt          # 编译配置
├── main.cpp               # 主程序
├── resnet50.om            # 模型文件(ATC 转换生成)
└── test_image.bin         # 输入数据(二进制文件)

4.2 完整 C++ 代码

// main.cpp
#include <acl/acl.h>
#include <acl/ops/acl_dvpp.h>
#include <iostream>
#include <fstream>
#include <vector>

// 检查 ACL 返回值的宏(不写 try-catch,直接判错)
#define CHECK_RET(ret, msg) \
    if ((ret) != ACL_SUCCESS) { \
        std::cerr << msg << ", ret = " << ret << std::endl; \
        return -1; \
    }

int main() {
    // === 第 1 步:初始化 ACL ===
    aclError ret = aclInit(nullptr);
    CHECK_RET(ret, "aclInit failed");

    // 指定要用的 NPU 设备(0 号设备)
    ret = aclrtSetDevice(0);
    CHECK_RET(ret, "aclrtSetDevice failed");

    // === 第 2 步:加载 OM 模型 ===
    uint32_t model_id = 0;
    ret = aclmdlLoadFromFile("resnet50.om", &model_id);
    CHECK_RET(ret, "aclmdlLoadFromFile failed");

    // 获取模型描述信息(输入/输出的 shape、数据类型)
    aclmdlDesc *model_desc = aclmdlCreateDesc(model_id);
    ret = aclmdlGetDesc(model_desc, model_id);
    CHECK_RET(ret, "aclmdlGetDesc failed");

    // === 第 3 步:准备输入数据 ===
    // 读二进制输入文件(假设已经把图片预处理成了 1×3×224×224 的 float32 数组)
    std::ifstream infile("test_image.bin", std::ios::binary);
    std::vector<float> input_data(1 * 3 * 224 * 224);
    infile.read(reinterpret_cast<char*>(input_data.data()), 
                input_data.size() * sizeof(float));
    infile.close();

    // 申请 NPU 显存(输入数据要从 Host 拷到 NPU)
    size_t input_size = aclmdlGetInputSizeByIndex(model_desc, 0);
    void *input_dev = nullptr;
    ret = aclrtMalloc(&input_dev, input_size, ACL_MEM_MALLOC_HUGE_FIRST);
    CHECK_RET(ret, "aclrtMalloc failed");

    // 把 Host 数据拷到 NPU
    ret = aclrtMemcpy(input_dev, input_size, 
                       input_data.data(), input_size, 
                       ACL_MEMCPY_HOST_TO_DEVICE);
    CHECK_RET(ret, "aclrtMemcpy failed");

    // 创建输入 dataset(ACL 要求用 dataset 封装输入输出)
    aclmdlDataset *input_dataset = aclmdlCreateDataset();
    aclDataBuffer *input_buffer = aclCreateDataBuffer(input_dev, input_size);
    ret = aclmdlAddDatasetBuffer(input_dataset, input_buffer);
    CHECK_RET(ret, "aclmdlAddDatasetBuffer failed");

    // === 第 4 步:执行推理 ===
    // 创建输出 dataset
    aclmdlDataset *output_dataset = aclmdlCreateDataset();
    for (size_t i = 0; i < aclmdlGetNumOutputs(model_desc); i++) {
        size_t output_size = aclmdlGetOutputSizeByIndex(model_desc, i);
        void *output_dev = nullptr;
        ret = aclrtMalloc(&output_dev, output_size, ACL_MEM_MALLOC_HUGE_FIRST);
        CHECK_RET(ret, "aclrtMalloc output failed");
        aclDataBuffer *output_buffer = aclCreateDataBuffer(output_dev, output_size);
        ret = aclmdlAddDatasetBuffer(output_dataset, output_buffer);
        CHECK_RET(ret, "aclmdlAddDatasetBuffer output failed");
    }

    // 执行模型推理
    ret = aclmdlExecute(model_id, input_dataset, output_dataset);
    CHECK_RET(ret, "aclmdlExecute failed");

    // === 第 5 步:解析输出 ===
    // 把输出从 NPU 拷回 Host
    for (size_t i = 0; i < aclmdlGetNumOutputs(model_desc); i++) {
        aclDataBuffer *output_buffer = aclmdlGetDatasetBuffer(output_dataset, i);
        void *output_dev = aclGetDataBufferAddr(output_buffer);
        size_t output_size = aclGetDataBufferSize(output_buffer);

        std::vector<float> output_data(output_size / sizeof(float));
        ret = aclrtMemcpy(output_data.data(), output_size,
                           output_dev, output_size,
                           ACL_MEMCPY_DEVICE_TO_HOST);
        CHECK_RET(ret, "aclrtMemcpy output failed");

        // 打印前 10 个输出值(真实场景要接后处理,比如 argmax 取分类结果)
        std::cout << "Output " << i << " (first 10 values): ";
        for (int j = 0; j < 10 && j < output_data.size(); j++) {
            std::cout << output_data[j] << " ";
        }
        std::cout << std::endl;
    }

    // === 清理资源 ===
    ret = aclmdlUnload(model_id);
    CHECK_RET(ret, "aclmdlUnload failed");
    ret = aclrtResetDevice(0);
    CHECK_RET(ret, "aclrtResetDevice failed");
    ret = aclFinalize();
    CHECK_RET(ret, "aclFinalize failed");

    std::cout << "Inference success!" << std::endl;
    return 0;
}

4.3 代码关键解释

为什么用 aclmdlDataset 封装输入输出? ACL 的接口设计是"面向 Dataset"的——一个模型可能有多个输入(比如 GPT 的 input_ids 和 attention_mask),用 Dataset 封装可以一次性传多个输入。

为什么输入数据要从 Host 拷到 NPU? NPU 只能直接访问自己的显存(HBM)。Host 内存的数据必须显式拷贝(用 aclrtMemcpy)。

为什么 aclrtMalloc 要用 ACL_MEM_MALLOC_HUGE_FIRST NPU 的 HBM 支持大页内存(Huge Page),用这个标志申请内存会优先用大页,性能好 10-15%。

第五步:编译

写 CMakeLists.txt

cmake_minimum_required(VERSION 3.10)
project(acl_inference)

set(CMAKE_CXX_STANDARD 14)

# 找 CANN 包(装在 /usr/local/Ascend)
find_package(Ascend REQUIRED)

# 包含 ACL 头文件路径
include_directories(${ASCEND_INCLUDE_DIRS})

# 编可执行文件
add_executable(acl_inference main.cpp)

# 链 ACL 库
target_link_libraries(acl_inference ${ASCEND_LIBRARIES})

编译:

mkdir build && cd build
cmake ..
make -j

90% 新手都会踩的坑 No.4:编译通过,但运行时报 error while loading shared libraries: libascendcl.so。 原因:LD_LIBRARY_PATH 没配全。运行时的库路径要在 ~/.cannrc 里配好(见第二步)。

第六步:运行

# 确保环境变量已 source
source ~/.cannrc

# 把 resnet50.om 拷到运行目录
cp ../resnet50.om .

# 运行(需要有 NPU 权限,加 sudo 或把用户加入 HwAiUser 组)
./acl_inference

成功输出:

Output 0 (first 10 values): 0.0023 -0.0156 0.0089 ...
Inference success!

为什么模型能转换成功但运行失败?

这是新手问的最多的问题。我总结了 4 个原因:

原因 1:CANN 版本不匹配

ATC 转换时用的 CANN 版本,跟推理程序编译/运行时用的 CANN 版本不一致。OM 模型格式可能变了,导致 aclmdlLoadFromFile 失败。

排查

# 看 ATC 版本
atc --version
# 看推理程序链接的 ACL 库版本
ldd acl_inference | grep ascendcl

解决:统一版本,重新转换模型、重新编译程序。

原因 2:NPU 驱动版本跟 CANN 不匹配

CANN 8.0 要求驱动版本 >= 24.1.0。如果驱动太老,ACL 初始化就失败(aclInit failed)。

排查

# 看驱动版本
npu-smi info

解决:升级驱动到 CANN 要求的版本。

原因 3:输入 Shape 跟模型要求的不一致

ATC 转换时指定了 input_shape="input:1,3,224,224",但推理时输入数据的 shape 不对(比如你传了 1,3,256,256 的数据),导致 aclmdlExecute 失败。

排查:打印输入数据的尺寸,跟 ATC 转换时指定的 shape 对比。

解决:推理前把输入数据 resize/crop 到模型要求的 shape。

原因 4:权限问题

运行推理程序需要访问 /dev/davinci0 设备文件,普通用户没权限。

排查

ls -l /dev/davinci0
# 如果 owner 是 root,你需要 sudo 或加入 HwAiUser 组

解决

sudo usermod -aG HwAiUser $USER
# 注销重新登录,就有权限了

90% 新手都会踩的坑(完整版)

坑编号 问题描述 原因 解决方法
1 装完 CANN 找不到头文件/库 环境变量没配 写 ~/.cannrc,每次开终端 source
2 编译通过,运行时 libascendcl.so 找不到 LD_LIBRARY_PATH 没配全 `ldconfig -p
3 模型转换成功,推理时 load model failed CANN 版本不匹配 统一转换和运行用的 CANN 版本
4 aclInit failed 驱动版本太老 升级驱动到 CANN 要求的版本
5 推理输出全是 0 或 NaN 输入数据没归一化 图片预处理要跟训练时一致(比如 ImageNet 的 mean/std)

排错方法总结

遇到问题,按这个顺序排查:

  1. 看返回值:所有 ACL 接口都返回 aclError,用 CHECK_RET 宏检查
  2. 看环境变量echo $LD_LIBRARY_PATH 确认库路径
  3. 看设备状态npu-smi info 确认 NPU 在线
  4. 看模型信息atc --mode=display_model_info --om=resnet50.om 确认模型输入/输出 shape
  5. 简化复现:先跑 CANN 自带的样例(比如 /usr/local/Ascend/nnrt/latest/samples/inference/modelInference/

工程经验:第一次跑不通别慌。ACL 的错误码很详细(比如 ACL_ERROR_INVALID_RESOURCE = 107002),去昇腾社区搜错误码,90% 的问题都有现成答案。

https://atomgit.com/cann/runtime https://atomgit.com/cann/asc-devkit https://atomgit.com/cann/cann-samples

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐