CANN cann-learning-hub:5 分钟跑通第一个昇腾算子

文章目录
前言
昇腾 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.txt、BUILD.gn、host/、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 MALLOC 或 ACL_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 directory 或 file 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 提供的调试和性能分析工具,不断积累实战经验,早日成为昇腾生态中的熟练开发者。
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐


所有评论(0)