请添加图片描述

前言

昇腾 CANN(Compute Architecture for Neural Networks)是华为面向 AI 场景推出的异构计算架构,对上支持业界主流 AI 框架,对下提供统一编程接口,使开发者能够高效利用昇腾 NPU 的澎湃算力。cann-learning-hub 是 CANN 官方维护的示例学习仓库,精选了大量从入门到进阶的算子与模型开发样例,是新手开发者快速熟悉昇腾开发流程的最佳入口。本文将带领读者从零开始,在 5 分钟内完成第一个昇腾算子的编译与运行,并对核心代码进行深度解读,同时提供完整的错误排查指南,帮助读者绕过开发过程中最常见的陷阱。

1. cann-learning-hub 的项目定位

1.1 官方学习入口的定位与价值

cann-learning-hub 仓库托管在原子开源平台 atomgit.com,由华为昇腾团队持续维护和更新。它的核心定位是「可执行的学习文档」——每一个示例都是一个独立可运行的项目,配有详细的 README 说明和分层递进的目录结构。相比零散的技术文档和深奥的 API 参考手册,cann-learning-hub 提供了最直观的学习路径:开发者可以直接 clone 代码、修改参数、观察输出,从而在实践中理解抽象概念。

1.2 仓库结构总览

克隆仓库后,典型目录结构如下:

cann-learning-hub/
├── README.md
├── requirements.txt
├── samples/                        # 示例目录
│   ├── ascend_c/                   # Ascend C 算子开发示例
│   │   ├── add_kernel/             # Add 算子完整工程
│   │   │   ├── cmake/
│   │   │   ├── host/src/           # host 侧 host.cpp
│   │   │   ├── kernel/src/         # kernel 侧核心实现
│   │   │   ├── CMakeLists.txt
│   │   │   └── BUILD.gn
│   │   ├── vector_add/             # 向量加法
│   │   └── ...
│   └── PyTorch/                    # PyTorch 接入示例
└── docs/                           # 配套文档

从结构可以看出,仓库以「语言/框架 × 场景」进行分类。最核心的是 samples/ascend_c/ 目录,其中每个子目录对应一个独立算子工程,每个工程内部遵循统一的 host + kernel 双文件结构。

1.3 快速上手指引的设计理念

cann-learning-hub 在设计之初就贯彻了「最小可用」原则。每个示例都控制在极小的数据规模(通常为 8×8 或 16×16 张量),运行时间控制在毫秒级别,确保开发者无需准备真实数据集即可完整体验整个编译-运行流程。同时,每个示例都提供了「一键编译脚本」和「一键运行脚本」,将原本分散在 CMakeLists.txt、ldscripts、run.sh 中的大量配置收敛为两三个命令即可完成的操作。

2. 环境准备清单

在开始之前,需要确保开发环境的软硬件版本满足最低要求。以下是完整的版本对照表。

2.1 硬件与操作系统要求

项目 最低要求 推荐配置
昇腾 NPU 设备 昇腾 910 / 910B / 310 系列 昇腾 910B
服务器操作系统 Ubuntu 20.04 LTS / EulerOS 2.0 Ubuntu 22.04 LTS
内存 16 GB 32 GB 及以上
磁盘 100 GB 可用空间 SSD

2.2 CANN 版本要求

CANN 是昇腾计算通信库,包含驱动层、运行时层和编译工具链的完整栈。当前 cann-learning-hub 推荐的最低版本为 CANN 7.0,即 CANN 社区版的 7.x 系列。

查看已安装 CANN 版本的命令如下:

# 检查 CANN 版本
cat /usr/local/Ascend/ascend-toolbox/latest/version.info

如果系统未安装 CANN,可从华为昇腾社区下载对应版本的安装包,使用 ascend_install.sh 完成安装。

2.3 Python 环境要求

项目 最低要求 推荐配置
Python 版本 Python 3.7.x Python 3.8 / 3.9
pip 版本 ≥ 19.3 最新稳定版

Python 环境检查脚本如下:

#!/bin/bash
# check_env.sh — 运行环境检查脚本

echo "===== 环境检查开始 ====="

# Python 版本
PYTHON_VERSION=$(python3 --version 2>&1)
echo "[1/6] Python 版本: $PYTHON_VERSION"

# CANN 版本
if [ -f /usr/local/Ascend/ascend-toolbox/latest/version.info ]; then
    CANN_VERSION=$(cat /usr/local/Ascend/ascend-toolbox/latest/version.info)
    echo "[2/6] CANN 版本: $CANN_VERSION"
else
    echo "[2/6] CANN 未安装,请先安装 CANN"
fi

# NPU 驱动版本
if [ -f /usr/local/Ascend/driver/version.info ]; then
    DRIVER_VERSION=$(cat /usr/local/Ascend/driver/version.info)
    echo "[3/6] NPU 驱动版本: $DRIVER_VERSION"
else
    echo "[3/6] NPU 驱动未安装或路径异常"
fi

# 编译工具
if command -v g++ &> /dev/null; then
    GXX_VERSION=$(g++ --version | head -1)
    echo "[4/6] g++ 版本: $GXX_VERSION"
else
    echo "[4/6] g++ 未安装"
fi

if command -v cmake &> /dev/null; then
    CMAKE_VERSION=$(cmake --version | head -1)
    echo "[5/6] cmake 版本: $CMAKE_VERSION"
else
    echo "[5/6] cmake 未安装"
fi

# NPU 设备可见性
echo "[6/6] NPU 设备列表:"
python3 -c "import torch; print(f'  CUDA available: {torch.cuda.is_available()}')" 2>/dev/null || \
    python3 -c "import acl; print('  ACL available')" 2>/dev/null || \
    echo "  使用 aclinfo 检查设备"

echo "===== 环境检查完成 ====="

将上述脚本保存为 check_env.sh,赋予执行权限后运行即可快速诊断环境状态:

chmod +x check_env.sh
./check_env.sh

2.4 环境准备常见问题

初次配置环境时,最容易遇到的是 CANN 版本与驱动版本不匹配的问题。两者的版本号需要在同一代际内对齐,例如 CANN 7.0 需要搭配对应版本的驱动。华为昇腾社区提供了版本配套查询页面,安装前务必核对清楚。

3. 5 分钟跑通完整步骤

下面按时间顺序给出完整操作流程。每个步骤都有明确的目标和预期输出,完成后建议对照预期结果确认无误再进入下一步。

3.1 第一步:克隆仓库

git clone https://atomgit.com/cann/cann-learning-hub.git
cd cann-learning-hub

克隆完成后,进入第一个算子示例目录:

cd samples/ascend_c/add_kernel

预期输出:目录中应包含 CMakeLists.txtBUILD.gnhost/kernel/ 等子目录和文件。

3.2 第二步:安装依赖

在仓库根目录下安装 Python 依赖:

pip install -r requirements.txt

如果使用 conda 或 venv,建议先创建独立环境以避免依赖冲突:

conda create -y -n cann_demo python=3.8
conda activate cann_demo
pip install -r requirements.txt

3.3 第三步:编译示例

以 Add 算子为例,编译过程通过 CMake 完成。进入 add_kernel 目录,执行:

mkdir -p build && cd build
cmake ..
make -j$(nproc)

如果编译成功,会在 build/out/ 目录下生成可执行文件 add_kernel

完整的一键编译脚本如下:

#!/bin/bash
# compile.sh — 一键编译脚本

set -e
SAMPLE_DIR=$(cd "$(dirname "$0")" && pwd)
BUILD_DIR="$SAMPLE_DIR/build"
OUT_DIR="$SAMPLE_DIR/build/out"

echo "===== 开始编译 ====="
echo "工作目录: $SAMPLE_DIR"

mkdir -p "$BUILD_DIR"
cd "$BUILD_DIR"

cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)

if [ -d "$OUT_DIR" ]; then
    echo "===== 编译成功 ====="
    ls -lh "$OUT_DIR/"
else
    echo "===== 编译失败:输出目录不存在 ====="
    exit 1
fi

将脚本保存到 samples/ascend_c/add_kernel/ 目录下运行即可。

3.4 第四步:运行验证

编译产物生成后,直接运行可执行文件:

cd build/out
./add_kernel

预期输出:终端打印出两个输入张量的内容、加法运算结果,以及 “Run success” 字样,确认 NPU kernel 正常执行完毕。

如果希望一键运行,可以编写以下脚本:

#!/bin/bash
# run.sh — 一键运行脚本

set -e
OUT_DIR=$(cd "$(dirname "$0")/build/out" && pwd)
BIN_NAME="add_kernel"

echo "===== 开始运行 $BIN_NAME ====="
cd "$OUT_DIR"
./"$BIN_NAME"
echo "===== 运行结束 ====="

4. 第一个算子示例深度解读

4.1 Add 算子的工程结构

Add 算子工程是 cann-learning-hub 中最简单的完整示例,仅包含以下几个核心文件:

add_kernel/
├── CMakeLists.txt          # 顶层 CMake 配置
├── BUILD.gn                # Bazel 构建配置(可选)
├── host/                   # host 侧代码
│   ├── CMakeLists.txt      # host 侧 CMake 子配置
│   └── src/
│       └── host.cpp        # host 侧 host.cpp:内存申请、数据搬运、kernel 启动
├── kernel/                 # kernel 侧代码
│   ├── CMakeLists.txt      # kernel 侧 CMake 子配置
│   └── src/
│       └── add_custom.cpp  # Ascend C 核心实现:kernel 函数
└── cmake/                  # CMake 公共模块
    └── helper.cmake

理解 Add 算子的关键,在于区分 host 侧和 kernel 侧各自的职责边界。

4.2 host 侧 host.cpp 解读

host 侧代码负责整个算子执行流程中的统筹调度工作,包括 HostTensor 内存申请、输入数据填充、kernel 函数参数打包、NPU 设备内存分配、数据搬运以及 kernel 启动。一个典型的 host 侧实现如下:

#include "acl/acl.h"
#include "kernel_operator.h"

int main(int argc, char *argv[])
{
    // 1. 初始化 ACL 运行时
    aclInit(nullptr);
    aclrtContext context;
    aclrtContextHolder holder;
    aclrtCreateContext(&holder, 0);  // device id = 0
    context = holder.context;

    // 2. 准备输入数据(Host 侧内存)
    const int32_t tensorSize = 8;
    std::vector<float> hostA(tensorSize, 1.0f);
    std::vector<float> hostB(tensorSize, 2.0f);
    std::vector<float> hostC(tensorSize, 0.0f);

    // 3. 在 NPU 设备上分配内存
    void *devA = nullptr;
    void *devB = nullptr;
    void *devC = nullptr;
    aclrtMalloc(&devA, tensorSize * sizeof(float), ACL_MEM_MALLOC_NORMAL_ONLY);
    aclrtMalloc(&devB, tensorSize * sizeof(float), ACL_MEM_MALLOC_NORMAL_ONLY);
    aclrtMalloc(&devC, tensorSize * sizeof(float), ACL_MEM_MALLOC_NORMAL_ONLY);

    // 4. 将数据从 Host 拷贝到 NPU 设备
    aclrtMemcpy(devA, tensorSize * sizeof(float),
                hostA.data(), tensorSize * sizeof(float),
                ACL_MEMCPY_HOST_TO_DEVICE);
    aclrtMemcpy(devB, tensorSize * sizeof(float),
                hostB.data(), tensorSize * sizeof(float),
                ACL_MEMCPY_HOST_TO_DEVICE);

    // 5. 准备 kernel 执行参数
    uint32_t blockDim = 1;
    uint32_t kernelType = 0;  // 0 表示矢量运算
    AddCustomKernelRun(kernelType, devA, devB, devC,
                       tensorSize, blockDim, nullptr);

    // 6. 同步等待 kernel 执行完成
    aclrtSynchronizeStream(nullptr);

    // 7. 将结果从 NPU 设备拷贝回 Host
    aclrtMemcpy(hostC.data(), tensorSize * sizeof(float),
                devC, tensorSize * sizeof(float),
                ACL_MEMCPY_DEVICE_TO_HOST);

    // 8. 打印结果验证
    printf("Result: ");
    for (int i = 0; i < tensorSize; ++i) {
        printf("%.2f ", hostC[i]);
    }
    printf("\nRun success\n");

    // 9. 释放资源
    aclrtFree(devA);
    aclrtFree(devB);
    aclrtFree(devC);
    aclrtDestroyContext(holder);

    return 0;
}

这段代码清晰地展示了 host 侧的九步标准流程:初始化 → 准备数据 → 分配设备内存 → 数据上传 → 启动 kernel → 同步等待 → 数据回传 → 打印结果 → 释放资源。开发者在自定义算子时,核心改动通常集中在第 5 步的参数打包和 kernel 实现中。

4.3 kernel 侧 add_custom.cpp 解读

kernel 侧使用 Ascend C 编程范式编写,在 NPU Vector Core 上执行实际的算术运算。Ascend C 提供了一套类 C++ 的编程接口,包含 GlobalTensor、LocalTensor 等张量抽象,以及 Vector、Matmul 等计算原语。

#include "kernel_operator.h"

namespace {
constexpr uint32_t BLOCK_DIM = 1;
}  // namespace

// 算子注册表中的 KernelType 枚举值对应此函数
__aicore__ inline void AddCustomProcess(
    GlobalTensor<float> a,
    GlobalTensor<float> b,
    GlobalTensor<float> c,
    int32_t totalLength)
{
    // 使用 LocalTensor 进行分块处理,避免一次性处理过大数据
    LocalTensor<float> localA = a.GetLocalTensor();
    LocalTensor<float> localB = b.GetLocalTensor();
    LocalTensor<float> localC = c.GetLocalTensor();

    // Ascend C 提供的 VectorAdd 原语,执行逐元素加法
    VectorAdd(localC, localA, localB, totalLength);
}

// Ascend C 算子入口函数
// 当 host 侧通过 AddCustomKernelRun(kernelType, ...) 启动 kernel 时,
// 调度器根据 kernelType 路由到此处
__aicore__ void AddCustom(
    KernelArgs *args,
    const GlobalTensor<float> &a,
    const GlobalTensor<float> &b,
    const GlobalTensor<float> &c)
{
    AddCustomProcess(a, b, c, args->totalLength);
}

// 算子注册表
REGISTER_CUSTOM_KERNEL("AddCustom", AddCustom, BLOCK_DIM);

这里的几个关键设计点值得深入理解:

第一,__aicore__ 是 Ascend C 的函数装饰符,表明该函数将编译后在 NPU Vector Core 上执行。__aicore__ 修饰的函数中只能使用 Ascend C 提供的 API,不得调用标准 C++ 的 I/O 或动态内存分配函数。

第二,GlobalTensor 表示全局地址空间中的张量视图,跨 TCB(Tensor Control Block)共享;LocalTensor 表示本地地址空间中的张量视图,属于单个 AI Core 的私有内存。通过 GetLocalTensor() 可以将 GlobalTensor 转换为 LocalTensor 进行实际计算。

第三,VectorAdd 是 Ascend C 内置的矢量加法原语,它在底层会自动进行数据切分、流水排布和向量指令发射,是最高效的加法实现路径。

4.4 kernel 启动参数详解

kernel 启动参数通过 AddCustomKernelRun 函数从 host 侧传递给 NPU 运行时。在实际的 cann-learning-hub 示例中,kernel_operator.h 会生成对应的启动函数,参数列表通常包含以下字段:

// 典型 kernel 启动参数结构体(由 kernel_operator.h 展开)
struct AddCustomKernelParams {
    int32_t totalLength;    // 总数据长度(元素个数)
    uint32_t blockDim;       // 并行 block 数量
    void *stream;            // ACL 异步流(nullptr 表示使用默认流)
};
  • totalLength:标量参与运算的元素总数,用于计算切块策略和数据分布。kernel 内部根据此值决定每个 AI Core 处理多少数据。
  • blockDim:指定启动的 AI Core 并行度。对于矢量算子通常为 1(由 Vector Core 内部自动并行),对于矩阵乘法可能设置为 8 或 16 以充分利用多个 Core。
  • stream:ACL 异步执行流。如果传入 nullptr,则使用当前 context 的默认流,此时 host 侧的同步等待调用 aclrtSynchronizeStream(nullptr) 才能正确生效。

5. 常见运行错误排查

5.1 CANNOT MALLOC 错误

错误特征:终端输出包含 CANNOT MALLOCACL_ERROR_NO_MEM

完整排查路径

第一步,检查 NPU 设备内存使用情况:

# 查看当前进程的 NPU 内存分配状态
python3 -c "import acl; print(acl.rt.get_mem_info())"

# 清理所有 ACL 资源后重试
python3 -c "import acl; acl.reset(); print('ACL 已重置')"

第二步,确认设备未被他占用。昇腾设备默认 device id 为 0,如果其他进程正在使用:

# 列出当前占用 NPU 设备的进程
npu-smi info -l
npu-smi info -t process -i 0

如果有陌生进程占用,需要先终止或等待其释放资源。

第三步,检查申请内存的大小是否超出设备容量。对于 910 系列 NPU,单次分配不建议超过设备可用内存的 80%,可使用以下脚本探测可用内存:

#!/bin/bash
# check_npu_mem.sh — NPU 内存检查脚本

echo "===== NPU 内存状态 ====="
npu-smi info -t memory -i 0

echo ""
echo "===== 当前 NPU 进程 ====="
npu-smi info -t process -i 0

echo ""
echo "===== 检查驱动版本与 CANN 匹配性 ====="
DRIVER_VER=$(cat /usr/local/Ascend/driver/version.info 2>/dev/null | grep -i "version" | head -1)
CANN_VER=$(cat /usr/local/Ascend/ascend-toolbox/latest/version.info 2>/dev/null | grep -i "version" | head -1)
echo "驱动版本: $DRIVER_VER"
echo "CANN 版本: $CANN_VER"

5.2 INVALID DEVICE ID 错误

错误特征:运行时提示 ACL_ERROR_INVALID_DEVICE 或设备编号超出范围。

完整排查路径

第一步,确认系统可见的 NPU 设备数量:

# Python 方式
python3 -c "import torch; print('CUDA devices:', torch.cuda.device_count())" 2>/dev/null

# ACL 方式
python3 -c "import acl; print('Available devices:', acl.rt.get_device_count())"

# 命令行方式
npu-smi info -l

第二步,如果 host 侧代码中硬编码了 device id,需要改为实际存在的设备编号:

// 错误写法(硬编码 device id = 0)
aclrtCreateContext(&holder, 0);

// 正确写法(获取实际可用设备数后动态选择)
int32_t deviceCount = 0;
aclrtGetDeviceCount(&deviceCount);
int32_t deviceId = 0;  // 或从命令行参数传入
if (deviceId >= deviceCount) {
    printf("Invalid device id: %d, available: %d\n", deviceId, deviceCount);
    return -1;
}
aclrtCreateContext(&holder, deviceId);

第三步,检查环境变量 ASCEND_VISIBLE_DEVICES 是否限制了可见设备列表:

echo $ASCEND_VISIBLE_DEVICES
# 如果输出为空或为其他值,可能导致 device id 映射异常
# 可尝试临时清空后运行
unset ASCEND_VISIBLE_DEVICES
./add_kernel

5.3 KERNEL NOT FOUND 错误

错误特征:运行时出现 ACL_ERROR_KERNEL_NOT_FOUND 或类似提示,kernel 符号未注册。

完整排查路径

第一步,确认 kernel 侧代码编译成功、生成了正确的 .o 目标文件:

find build/ -name "*.o" | head -20
ls -lh build/out/*.o 2>/dev/null || echo "未找到 .o 文件"

第二步,检查算子注册表是否正确匹配。kernel 启动时根据注册的算子名称路由,如果 host 侧调用的名称与 kernel 侧注册的名称不一致,就会报 KERNEL NOT FOUND:

// host 侧调用名称必须与 kernel 侧注册名称严格一致
AddCustomKernelRun("AddCustom", ...);  // 名称为 "AddCustom"

// kernel 侧注册
REGISTER_CUSTOM_KERNEL("AddCustom", AddCustom, BLOCK_DIM);  // 名称必须相同

第三步,检查 kernel 函数是否正确编译进了最终的可执行文件:

# 使用 nm 或 readelf 查看导出的 kernel 符号
nm -C build/out/add_kernel | grep -i "add\|kernel\|custom" | head -20
readelf -s build/out/add_kernel | grep -i "add\|custom" | head -10

如果注册表相关的符号没有出现在输出中,说明 kernel 侧 CMakeLists.txt 可能未正确配置编译源文件。

5.4 一键错误诊断脚本

将以上三个排查路径整合为一个一键诊断工具:

#!/bin/bash
# diagnose.sh — 一键错误诊断脚本

ERROR_MSG="$1"

if [ -z "$ERROR_MSG" ]; then
    echo "用法: ./diagnose.sh '<错误信息>'"
    echo "示例: ./diagnose.sh 'CANNOT MALLOC'"
    exit 1
fi

echo "===== 错误诊断开始 ====="
echo "错误信息: $ERROR_MSG"
echo ""

if echo "$ERROR_MSG" | grep -qi "malloc\|no_mem\|mem"; then
    echo "[诊断方向] CANNOT MALLOC"
    echo "--- 检查 NPU 内存状态 ---"
    npu-smi info -t memory -i 0 2>/dev/null || echo "npu-smi 不可用,跳过"
    echo "--- 检查 NPU 进程 ---"
    npu-smi info -t process -i 0 2>/dev/null || echo "npu-smi 不可用,跳过"
    echo "建议: 降低单次分配大小或清理其他 NPU 进程"
elif echo "$ERROR_MSG" | grep -qi "device\|invalid_device"; then
    echo "[诊断方向] INVALID DEVICE ID"
    echo "--- 检查可用设备数 ---"
    python3 -c "import acl; print('Available devices:', acl.rt.get_device_count())" 2>/dev/null || \
        echo "ACL 不可用"
    echo "--- 检查 ASCEND_VISIBLE_DEVICES ---"
    echo "当前值: ${ASCEND_VISIBLE_DEVICES:-<未设置>}"
    echo "建议: 确认 device id 在有效范围内(0 ~ device_count-1)"
elif echo "$ERROR_MSG" | grep -qi "kernel.*not.*found\|kernel_not_found\|kernel not found"; then
    echo "[诊断方向] KERNEL NOT FOUND"
    echo "--- 检查 kernel 符号 ---"
    nm -C build/out/add_kernel 2>/dev/null | grep -i "custom\|kernel\|register" | head -10 || \
        echo "nm 不可用或 build/out 不存在"
    echo "--- 检查编译产物 ---"
    find build/ -name "*.o" 2>/dev/null | head -5 || echo "未找到 .o 文件"
    echo "建议: 确认 kernel 侧编译成功且注册名称与 host 侧一致"
else
    echo "[诊断方向] 未知错误类型"
    echo "--- 基本环境检查 ---"
    bash check_env.sh 2>/dev/null || echo "check_env.sh 不存在,请手动检查环境"
    echo "建议: 查看官方文档的错误码章节定位具体原因"
fi

echo ""
echo "===== 诊断结束 ====="

保存为 diagnose.sh 后,通过传入错误信息关键字即可获得针对性的排查指引:

chmod +x diagnose.sh
./diagnose.sh "CANNOT MALLOC"
./diagnose.sh "INVALID DEVICE ID"
./diagnose.sh "KERNEL NOT FOUND"

6. 两个关键陷阱与解决方案

陷阱一:NPU 驱动版本与 CANN 不匹配

问题描述:CANN 是用户态的软件栈,而 NPU 驱动运行在内核态。两者的版本号必须属于同一适配周期,否则会导致运行时检测到版本不兼容后主动拒绝执行。常见的报错形式是运行时报出 ACL_ERROR_INVALID_VERSION 或直接段错误(segmentation fault)。

排查思路:首先确认当前安装的驱动版本和 CANN 版本,然后去华为昇腾社区下载页面查询两者的配套关系。

#!/bin/bash
# check_version_match.sh — 版本匹配性检查脚本

echo "===== 版本配套检查 ====="

DRIVER_VER=""
CANN_VER=""
DRIVER_PATH="/usr/local/Ascend/driver/version.info"
CANN_PATH="/usr/local/Ascend/ascend-toolbox/latest/version.info"

if [ -f "$DRIVER_PATH" ]; then
    DRIVER_VER=$(cat "$DRIVER_PATH")
    echo "[驱动] $DRIVER_VER"
else
    echo "[驱动] 未找到 version.info ($DRIVER_PATH)"
fi

if [ -f "$CANN_PATH" ]; then
    CANN_VER=$(cat "$CANN_PATH")
    echo "[CANN] $CANN_VER"
else
    echo "[CANN] 未找到 version.info ($CANN_PATH)"
fi

echo ""
echo "===== 参考配套规则 ====="
echo "CANN 7.0 需要驱动 23.0.x 系列"
echo "CANN 7.1 需要驱动 23.0.x 或 24.0.x 系列"
echo "建议前往华为昇腾社区查询完整配套表:"
echo "https://www.huaweicloud.com/ascend/resources"

解决方案:卸载旧版本后,按配套表同时安装匹配版本的驱动和 CANN。切忌只更新其中之一。安装完成后重新运行 check_env.sh 验证版本是否匹配。

陷阱二:示例路径含中文导致编译失败

问题描述:如果 cann-learning-hub 仓库被克隆到了包含中文字符的路径下(例如 /home/用户名/桌面/cann-learning-hub),CMake 在处理路径时会因为编码问题导致编译器无法找到源文件,编译报错通常是 No such file or directoryfile not found

问题根源:Ascend C 编译器(基于 GCC 定制)在处理非 ASCII 字符路径时存在编码兼容性问题。Windows 用户特别容易遇到此问题,因为 Windows 默认使用 GBK 或 GB2312 编码处理文件系统路径。

解决方案一(推荐):将仓库克隆到纯 ASCII 路径下:

# 创建纯 ASCII 路径
sudo mkdir -p /opt/cann-workspace
sudo chmod 777 /opt/cann-workspace
cd /opt/cann-workspace

# 重新克隆仓库
git clone https://atomgit.com/cann/cann-learning-hub.git

然后修改环境变量 PROJECT_ROOT 指向新路径:

export PROJECT_ROOT=/opt/cann-workspace/cann-learning-hub
export PATH=$PROJECT_ROOT/samples/ascend_c/add_kernel/build/out:$PATH

解决方案二:如果必须使用中文路径,可以在 CMakeLists.txt 中显式指定源文件路径的绝对路径来规避编码问题:

# 在 CMakeLists.txt 中添加以下配置
set(CMAKE_IGNORE_PATH "/home/用户名/桌面")

# 强制使用 UTF-8 编码处理源文件路径
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -finput-charset=utf-8 -fexec-charset=utf-8")

不过最稳妥的方案仍然是切换到纯 ASCII 路径进行开发,从根本上消除编码隐患。

7. 实战代码总动员

以下汇总了全文所有实战脚本,共 13 个代码块,覆盖了从环境检查、编译构建、运行验证到错误诊断的完整开发闭环。

#!/bin/bash
# check_env.sh — 运行环境检查脚本
echo "===== 环境检查开始 ====="
PYTHON_VERSION=$(python3 --version 2>&1)
echo "[1/6] Python 版本: $PYTHON_VERSION"
if [ -f /usr/local/Ascend/ascend-toolbox/latest/version.info ]; then
    CANN_VERSION=$(cat /usr/local/Ascend/ascend-toolbox/latest/version.info)
    echo "[2/6] CANN 版本: $CANN_VERSION"
else
    echo "[2/6] CANN 未安装"
fi
if [ -f /usr/local/Ascend/driver/version.info ]; then
    DRIVER_VERSION=$(cat /usr/local/Ascend/driver/version.info)
    echo "[3/6] NPU 驱动版本: $DRIVER_VERSION"
else
    echo "[3/6] NPU 驱动未安装"
fi
if command -v g++ &> /dev/null; then
    GXX_VERSION=$(g++ --version | head -1)
    echo "[4/6] g++ 版本: $GXX_VERSION"
else
    echo "[4/6] g++ 未安装"
fi
if command -v cmake &> /dev/null; then
    CMAKE_VERSION=$(cmake --version | head -1)
    echo "[5/6] cmake 版本: $CMAKE_VERSION"
else
    echo "[5/6] cmake 未安装"
fi
echo "[6/6] NPU 设备列表:"
npu-smi info -i 0 2>/dev/null || echo "  npu-smi 不可用"
echo "===== 环境检查完成 ====="
#!/bin/bash
# compile.sh — 一键编译脚本
set -e
SAMPLE_DIR=$(cd "$(dirname "$0")" && pwd)
BUILD_DIR="$SAMPLE_DIR/build"
OUT_DIR="$SAMPLE_DIR/build/out"
echo "===== 开始编译 ====="
mkdir -p "$BUILD_DIR" && cd "$BUILD_DIR"
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
if [ -d "$OUT_DIR" ]; then
    echo "===== 编译成功 ====="
    ls -lh "$OUT_DIR/"
else
    echo "===== 编译失败 =====" && exit 1
fi
#!/bin/bash
# run.sh — 一键运行脚本
set -e
OUT_DIR=$(cd "$(dirname "$0")/build/out" && pwd)
BIN_NAME="add_kernel"
echo "===== 开始运行 $BIN_NAME ====="
cd "$OUT_DIR" && ./"$BIN_NAME" && echo "===== 运行结束 ====="
#!/bin/bash
# check_npu_mem.sh — NPU 内存检查脚本
echo "===== NPU 内存状态 ====="
npu-smi info -t memory -i 0 2>/dev/null
echo ""
echo "===== 当前 NPU 进程 ====="
npu-smi info -t process -i 0 2>/dev/null
echo ""
echo "===== 驱动版本 ====="
cat /usr/local/Ascend/driver/version.info 2>/dev/null || echo "未知"
echo "===== CANN 版本 ====="
cat /usr/local/Ascend/ascend-toolbox/latest/version.info 2>/dev/null || echo "未知"
#!/bin/bash
# check_version_match.sh — 版本匹配性检查脚本
echo "===== 版本配套检查 ====="
DRIVER_PATH="/usr/local/Ascend/driver/version.info"
CANN_PATH="/usr/local/Ascend/ascend-toolbox/latest/version.info"
[ -f "$DRIVER_PATH" ] && echo "[驱动] $(cat $DRIVER_PATH)" || echo "[驱动] 未找到"
[ -f "$CANN_PATH" ] && echo "[CANN] $(cat $CANN_PATH)" || echo "[CANN] 未找到"
echo ""
echo "CANN 7.0 需要驱动 23.0.x 系列"
echo "CANN 7.1 需要驱动 23.0.x 或 24.0.x 系列"
echo "参考: https://www.huaweicloud.com/ascend/resources"
#!/bin/bash
# diagnose.sh — 一键错误诊断脚本
ERROR_MSG="$1"
if [ -z "$ERROR_MSG" ]; then
    echo "用法: ./diagnose.sh '<错误信息>'" && exit 1
fi
echo "===== 错误诊断 ====="
echo "错误信息: $ERROR_MSG"
echo ""
if echo "$ERROR_MSG" | grep -qi "malloc\|no_mem"; then
    echo "[方向] CANNOT MALLOC"
    npu-smi info -t memory -i 0 2>/dev/null
elif echo "$ERROR_MSG" | grep -qi "device\|invalid_device"; then
    echo "[方向] INVALID DEVICE ID"
    python3 -c "import acl; print('可用设备:', acl.rt.get_device_count())" 2>/dev/null || echo "ACL 不可用"
elif echo "$ERROR_MSG" | grep -qi "kernel.*not.*found"; then
    echo "[方向] KERNEL NOT FOUND"
    nm -C build/out/add_kernel 2>/dev/null | grep -i "custom\|register" | head -10 || echo "符号未找到"
else
    echo "[方向] 未知错误,请手动检查"
fi
// host.cpp — 典型 host 侧实现(核心部分)
#include "acl/acl.h"
#include "kernel_operator.h"

int main(int argc, char *argv[])
{
    aclInit(nullptr);
    aclrtContextHolder holder;
    aclrtCreateContext(&holder, 0);  // device id = 0

    const int32_t tensorSize = 8;
    std::vector<float> hostA(tensorSize, 1.0f);
    std::vector<float> hostB(tensorSize, 2.0f);
    std::vector<float> hostC(tensorSize, 0.0f);

    void *devA = nullptr, *devB = nullptr, *devC = nullptr;
    aclrtMalloc(&devA, tensorSize * sizeof(float), ACL_MEM_MALLOC_NORMAL_ONLY);
    aclrtMalloc(&devB, tensorSize * sizeof(float), ACL_MEM_MALLOC_NORMAL_ONLY);
    aclrtMalloc(&devC, tensorSize * sizeof(float), ACL_MEM_MALLOC_NORMAL_ONLY);

    aclrtMemcpy(devA, tensorSize * sizeof(float), hostA.data(), tensorSize * sizeof(float), ACL_MEMCPY_HOST_TO_DEVICE);
    aclrtMemcpy(devB, tensorSize * sizeof(float), hostB.data(), tensorSize * sizeof(float), ACL_MEMCPY_HOST_TO_DEVICE);

    uint32_t blockDim = 1;
    AddCustomKernelRun(0, devA, devB, devC, tensorSize, blockDim, nullptr);

    aclrtSynchronizeStream(nullptr);
    aclrtMemcpy(hostC.data(), tensorSize * sizeof(float), devC, tensorSize * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST);

    printf("Result: ");
    for (int i = 0; i < tensorSize; ++i) printf("%.2f ", hostC[i]);
    printf("\nRun success\n");

    aclrtFree(devA); aclrtFree(devB); aclrtFree(devC);
    aclrtDestroyContext(holder);
    return 0;
}
// add_custom.cpp — Ascend C kernel 侧实现
#include "kernel_operator.h"

namespace { constexpr uint32_t BLOCK_DIM = 1; }

__aicore__ inline void AddCustomProcess(
    GlobalTensor<float> a,
    GlobalTensor<float> b,
    GlobalTensor<float> c,
    int32_t totalLength)
{
    LocalTensor<float> localA = a.GetLocalTensor();
    LocalTensor<float> localB = b.GetLocalTensor();
    LocalTensor<float> localC = c.GetLocalTensor();
    VectorAdd(localC, localA, localB, totalLength);
}

__aicore__ void AddCustom(KernelArgs *args,
    const GlobalTensor<float> &a,
    const GlobalTensor<float> &b,
    const GlobalTensor<float> &c)
{
    AddCustomProcess(a, b, c, args->totalLength);
}

REGISTER_CUSTOM_KERNEL("AddCustom", AddCustom, BLOCK_DIM);
# CMakeLists.txt — 顶层 CMake 配置示例
cmake_minimum_required(VERSION 3.16)
project(add_kernel)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 指向 CANN 安装路径(根据实际安装位置调整)
set(CANN_ROOT "/usr/local/Ascend/ascend-toolbox/latest")
include(${CANN_ROOT}/cmake/ascend.cmake)

add_subdirectory(host)
add_subdirectory(kernel)
# kernel/CMakeLists.txt — kernel 侧编译配置
project(add_kernel_kernel)

set(KERNEL_SOURCES
    ${CMAKE_CURRENT_SOURCE_DIR}/src/add_custom.cpp
)

add_library(add_kernel_kernel SHARED ${KERNEL_SOURCES})

target_include_directories(add_kernel_kernel PRIVATE
    ${CMAKE_SOURCE_DIR}/kernel/include
    ${CANN_ROOT}/include
)

target_link_libraries(add_kernel_kernel PRIVATE
    ascendc
)
// device_id 动态选择示例
int32_t deviceCount = 0;
aclrtGetDeviceCount(&deviceCount);
int32_t deviceId = 0;
if (argc > 1) {
    deviceId = atoi(argv[1]);
}
if (deviceId >= deviceCount) {
    printf("Invalid device id: %d, available: %d\n", deviceId, deviceCount);
    return -1;
}
aclrtCreateContext(&holder, deviceId);
#!/bin/bash
# safe_build.sh — 安全构建脚本(含路径检查)
set -e

CURRENT_DIR=$(pwd)
PROJECT_NAME=$(basename "$CURRENT_DIR")

echo "===== 安全构建检查 ====="

# 检查路径是否包含非 ASCII 字符
if echo "$CURRENT_DIR" | grep -P '[^\x00-\x7F]' > /dev/null 2>&1; then
    echo "[警告] 当前路径包含非 ASCII 字符,可能导致编译失败"
    echo "当前路径: $CURRENT_DIR"
    echo "建议移动到纯 ASCII 路径,如 /opt/cann-workspace/"
    read -p "是否继续? (y/N): " confirm
    [ "$confirm" != "y" ] && [ "$confirm" != "Y" ] && exit 1
fi

echo "[路径检查] 通过"
echo "[编译] 启动 CMake + Make"
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Release
make -j$(nproc)
echo "===== 构建完成 ====="
#!/bin/bash
# quickstart.sh — 5 分钟速通一键脚本
set -e
echo "===== 5 分钟速通:克隆 → 编译 → 运行 ====="
echo ""
echo "[Step 1/4] 检查环境..."
bash check_env.sh
echo ""
echo "[Step 2/4] 编译示例..."
bash compile.sh
echo ""
echo "[Step 3/4] 运行验证..."
bash run.sh
echo ""
echo "[Step 4/4] 全部完成!"

8. 进阶学习路径推荐

完成第一个 Add 算子之后,建议读者继续深入以下几个方向。

首先,推荐使用 asc-devkit(昇腾开发套件)搭建完整的本地开发环境。asc-devkit 集成了 CANN 编译器、调试工具和性能分析器,可以显著提升开发效率。安装方式如下:

# 下载 asc-devkit 安装包(从昇腾社区获取具体链接)
wget https://ascend-repo.obs.cn-south-1.myhuaweicloud.com/ascend-devkit/asc-devkit_latest_linux-aarch64.run
chmod +x asc-devkit_latest_linux-aarch64.run
sudo ./asc-devkit_latest_linux-aarch64.run

安装完成后,通过以下命令验证开发环境:

ascend-devkit info
ascend-cann-compiler --version

其次,可以继续探索 cann-learning-hub 中的其他示例,例如向量乘法(VectorMul)、矩阵乘(Matmul)、卷积(Conv2d)等,逐步掌握不同类型算子在 Ascend C 中的实现范式。仓库地址为:

https://atomgit.com/cann/cann-learning-hub

该仓库持续更新,涵盖从基础矢量运算到复杂模型算子的完整学习路径。每个示例都保持了「最小可用」的设计理念,确保开发者能够在最短时间内完成从理解到验证的完整闭环。

结语

cann-learning-hub 作为昇腾 CANN 官方维护的学习仓库,用极低的上手门槛为开发者打开了昇腾算子开发的大门。本文从环境准备出发,完整走过了克隆、编译、运行的全流程,深度解读了 Add 算子的 host 侧调度逻辑和 kernel 侧 Ascend C 实现,并给出了针对三大常见错误和一众陷阱的完整解决方案。13 个实战脚本覆盖了开发全链路,读者可直接将其保存复用。

昇腾生态的成熟度正在快速提升,Ascend C 作为面向 AI Core 的底层开发框架,兼具易用性和高性能。建议读者在跑通本文示例后,进一步阅读 cann-learning-hub 中进阶算子的源码,结合 asc-devkit 提供的调试和性能分析工具,不断积累实战经验,早日成为昇腾生态中的熟练开发者。

Logo

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

更多推荐