昇腾CANN cmake:CANN 项目的 CMake 构建模块实战
从 ops-nn 到 cann-recipes-*,几乎所有 CANN 开源仓库都用 CMake 做构建系统。cann-cmake 仓库提供一套标准的 CMake 模块——FindCANN.cmake(找到 CANN 安装路径)、AscendCCore.cmake(Ascend C 编译规则)、AscendKernel.cmake(kernel 编译和链接)——让开发者的 CMakeLists.txt 从 200 行缩减到 20 行。
FindCANN.cmake:自动发现 CANN 安装
传统做法是手动设ASCEND_HOME_PATH环境变量,CMake 脚本里写死路径。cann-cmake 的 FindCANN 自动发现。
# CMakeLists.txt(最少配置) cmake_minimum_required(VERSION 3.16) project(my_ascend_op LANGUAGES CXX) # 引入 CANN CMake 模块 list(APPEND CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake") include(FindCANN) # FindCANN 自动做了这些事: # 1. 搜索 $ASCEND_HOME_PATH 环境变量(用户自定义) # 2. 搜索 /usr/local/Ascend(默认安装路径) # 3. 设置 CANN_INCLUDE_DIRS(头文件路径) # 4. 设置 CANN_LIBRARIES(库文件路径) # 5. 检查 CANN 版本(通过 version.txt) # 6. 如果找不到 → 报错 + 提示安装 message(STATUS "CANN version: ${CANN_VERSION}") message(STATUS "CANN include: ${CANN_INCLUDE_DIRS}") message(STATUS "CANN libs: ${CANN_LIBRARIES}") # 版本检查(有些特性只在 8.0+ 可用) if(CANN_VERSION VERSION_LESS "8.0.0") message(FATAL_ERROR "CANN 8.0+ required (flash attention and MC2)") endif()FindCANN 内部逻辑:
# FindCANN.cmake 核心实现 # 搜索优先级:$ASCEND_HOME_PATH > /usr/local/Ascend > pkg-config find_path(CANN_ROOT_DIR NAMES version.cfg PATHS $ENV{ASCEND_HOME_PATH} /usr/local/Ascend /opt/Ascend PATH_SUFFIXES ascend-toolkit/latest ascend-toolkit/8.0.0 ) if(NOT CANN_ROOT_DIR) message(FATAL_ERROR "CANN not found. Install CANN toolkit or set ASCEND_HOME_PATH.\n" "Download: https://www.hiascend.com/software/cann") endif() # 提取版本信息 file(READ "${CANN_ROOT_DIR}/version.cfg" CANN_VER_STR) string(REGEX MATCH "[0-9]+\\.[0-9]+\\.[0-9]+" CANN_VERSION "${CANN_VER_STR}") # 自动找到子目录 set(CANN_INCLUDE_DIRS "${CANN_ROOT_DIR}/include" "${CANN_ROOT_DIR}/include/ascendc" "${CANN_ROOT_DIR}/opp/built-in/op_proto/custom" # 算子原型 ) set(CANN_LIBRARIES "${CANN_ROOT_DIR}/lib64/libascendcl.so" "${CANN_ROOT_DIR}/lib64/libge_executor.so" "${CANN_ROOT_DIR}/lib64/libascend_hal.so" ) find_package_handle_standard_args(CANN REQUIRED_VARS CANN_ROOT_DIR CANN_INCLUDE_DIRS CANN_LIBRARIES VERSION_VAR CANN_VERSION )AscendCCore.cmake:Ascend C 源代码编译
Ascend C 的 kernel 代码(.cpp 文件,且内部用 Ascend C API)需要 TIKC 编译器编译,不是普通的 C++ 编译器。AscendCCore 封装了 tikcc 的调用规则。
# 引入 Ascend C 编译规则 include(AscendCCore) # 定义 Ascend C kernel 源文件 ascendc_add_library(my_ops STATIC kernels/matmul_tiling.cpp kernels/softmax_fusion.cpp kernels/gelu_activation.cpp LINK_LIBRARIES ${CANN_LIBRARIES} COMPILE_OPTIONS -DBLOCK_DIM=32 -DMATMUL_TILE_M=16 -DMATMUL_TILE_N=16 ) # ascendc_add_library 的底层操作: # 1. 用 tikcc(TIK 编译器)把 .cpp → .o(Ascend C → 二进制) # 2. 设置正确的目标架构(--soc-version=ascend910) # 3. 链接 runtime 和 driver 库 # 4. 生成适用于动态加载的 .so # 等价的手动命令(ascendc_add_library 内部执行) # tikcc kernels/matmul_tiling.cpp \ # --target=ascend910 \ # --opt-level=3 \ # -I${CANN_INCLUDE_DIRS} \ # -c -o matmul_tiling.o关键:tikcc是 CANN 的 Ascend C 编译器。它和 GCC 是不同的工具链——GCC 编译的是给 CPU 执行的代码,tikcc 编译的是给 NPU 执行的代码。ascendc_add_library自动选择正确的编译器。
AscendKernel.cmake:多架构 kernel 编译
同一份 Ascend C 代码在不同 NPU 架构上需要不同的编译优化——Ascend 910 (达芬奇) 和 Ascend 950DT (下一代) 的 L1 缓存大小不同,tile 大小也不同。AscendKernel 支持多架构编译。
include(AscendKernel) # 为多架构编译 kernel ascend_multi_arch_kernel(my_ops_kernel SOURCES kernels/matmul.cpp ARCHITECTURES ascend910 ascend950 # 可选:下一代架构编译 COMPILE_FLAGS_ascend910 -DL1_CACHE_SIZE=32KB -DTILE_M=16 COMPILE_FLAGS_ascend950 -DL1_CACHE_SIZE=64KB # 更大的 L1 → 更大的 tile -DTILE_M=32 ) # 生成的文件结构: # build/ # ├── ascend910/ # │ └── my_ops_kernel.so (Architecture 910 optimized) # ├── ascend950/ # │ └── my_ops_kernel.so (Architecture 950 optimized) # └── generic/ # └── my_ops_kernel.so (通用实现,动态选择)运行时自动选择对应架构的 kernel:
// CANN runtime 自动选择对应硬件架构的 .so// 不需要手动判断 NPU 型号auto*kernel=AscendRuntime::LoadKernel("my_ops_kernel");// runtime 内部:// if (npu_arch == "Ascend910") → load ascend910/my_ops_kernel.so// if (npu_arch == "Ascend950") → load ascend950/my_ops_kernel.soops-proto 自动生成
CANN 的算子库都有ops-proto目录——算子的 Protobuf 定义(输入输出描述)。cmake 仓库提供了 Protobuf 编译规则。
# 自动生成算子 Proto,不需要手动编译 include(AscendOpsProto) ascend_ops_proto_generate(MY_OPS PROTO_FILES ops-proto/matmul.proto ops-proto/softmax.proto ops-proto/gelu.proto OUTPUT_DIR ${CMAKE_BINARY_DIR}/generated ) # 生成的 C++ 文件: # generated/matmul.pb.h ← 包含 MatMulOp 的 Protobuf 结构 # generated/matmul.pb.cc # generated/softmax.pb.h # ... # 链接到最终的 .so target_link_libraries(my_ops PRIVATE MY_OPS_PROTO)算子的 Proto 定义包含输入输出 tensor 的类型和形状信息——ge(图引擎)用这些信息做图优化。
完整项目的 CMakeLists.txt
组合 FindCANN + AscendCCore + AscendKernel + AscendOpsProto 四件套:
cmake_minimum_required(VERSION 3.16) project(ascend-ops-project VERSION 1.0.0 LANGUAGES CXX) # 引入 CANN CMake 模块(从 cann-cmake 仓库) list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake/cann-cmake") include(FindCANN) include(AscendCCore) include(AscendKernel) include(AscendOpsProto) # 版本检查 if(CANN_VERSION VERSION_LESS "8.0.0") message(FATAL_ERROR "CANN 8.0+ required") endif() # 算子 Proto 生成 ascend_ops_proto_generate(OPS_PROTO PROTO_FILES ${CMAKE_CURRENT_SOURCE_DIR}/ops-proto/*.proto OUTPUT_DIR ${CMAKE_BINARY_DIR}/generated/proto ) # Ascend C kernel 编译 ascendc_add_library(ops_kernels STATIC ${CMAKE_CURRENT_SOURCE_DIR}/kernels/*.cpp COMPILE_OPTIONS -DBLOCK_DIM=32 LINK_LIBRARIES OPS_PROTO ${CANN_LIBRARIES} ) # 多架构编译 ascend_multi_arch_kernel(ops_kernels_opt SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/kernels/matmul_tuned.cpp ARCHITECTURES ascend910 ascend950DT # Atlas A2 服务器 ) # 最终产物 add_executable(ops_test test/main.cpp) target_link_libraries(ops_test PRIVATE ops_kernels ops_kernels_opt)踩坑一:FindCANN 在新版安装路径下的版本文件路径
CANN 8.5 的安装路径变了:旧版/usr/local/Ascend/ascend-toolkit/8.0.0/version.cfg,新版/usr/local/Ascend/ascend-toolkit/latest/version.cfg。FindCANN.cmake 如果只搜固定路径,在新版 CANN 上会搜不到。
修复:在 FindCANN.cmake 里同时搜索*/latest/和*/8.x.x/。
# 同时搜索 latest 和带版本号的路径 find_path(CANN_ROOT_DIR NAMES version.cfg PATHS "${ASCEND_HOME}/ascend-toolkit/latest" "${ASCEND_HOME}/ascend-toolkit/8.5.0" "${ASCEND_HOME}/ascend-toolkit/8.0.0" "${ASCEND_HOME}/ascend-toolkit" DOC "Root directory of CANN toolkit" )踩坑二:Ascend C 编译的预处理器宏不展开
ascendc_add_library的COMPILE_OPTIONS用-D传递编译时参数。但-DTILE_M=16在 tikcc 里是TILE_M=16——tikcc 的预处理宏有特殊语法(不能和 GCC 混用)。
错误:
ascendc_add_library(my_ops STATIC kernels/matmul.cpp COMPILE_OPTIONS -DTILE_M=16 -DTILE_N=16 # GCC 风格的预定义宏 )tikcc 不接受空格式的-D——报unknown compiler flag。
正确:
ascendc_add_library(my_ops STATIC kernels/matmul.cpp COMPILE_DEFINITIONS TILE_M=16 TILE_N=16 # ascenc_add_library 内部会根据这些 DEFINITIONS 生成 tikcc 风格的 # --define=TILE_M=16 --define=TILE_N=16 )踩坑三:Protobuf 编译依赖遗漏
ascend_ops_proto_generate自动生成的.pb.cc文件需要链接 Protobuf 运行时库。但默认不链接——因为假设系统中已经全局安装了 libprotobuf。
修复:在 CMakeLists.txt 里显式加find_package(Protobuf)。
find_package(Protobuf REQUIRED) ascend_ops_proto_generate(OPS_PROTO PROTO_FILES ${CMAKE_CURRENT_SOURCE_DIR}/ops-proto/*.proto OUTPUT_DIR ${CMAKE_BINARY_DIR}/generated/proto ) target_link_libraries(ops_kernels PRIVATE OPS_PROTO protobuf::libprotobuf # ← 必须显式链接 ${CANN_LIBRARIES} )如果不加protobuf::libprotobuf,链接时会报undefined reference to google::protobuf::...。
cann-cmake 的价值不在复杂的构建逻辑——在于标准化。所有 CANN 开源仓库用同一套 CMake 模块(FindCANN + AscendCCore + AscendOpProto),开发者的 CMakeLists.txt 写 20 行就够。对着镜像的 CANN 版本找不同的 FindCANN 路径——标准化消除了这些重复劳动。
