单算子编译出包的疑惑
在昇腾CANN的cann-ops
代码仓库中,quickstart.md
文档为我们提供了算子编译执行的入门指引。核心操作是通过运行build.sh
脚本来完成。如图1所示,脚本提供了编译构建的基本方法。
图1
初次尝试时,如果直接运行 bash build.sh
命令进行所有自定义算子的完整编译,往往会面临一个现实问题:编译过程耗时非常长。笔者亲测时,就曾因等待时间远超预期而不得不中途放弃。对于日常专注于单个算子开发与调试的场景,这种全量编译的方式显然效率不高。
那么,更优的选择是什么呢?答案是进行单算子编译。build.sh
脚本提供了 -n
参数来实现这一目的。如图2所示,使用此参数可以指定仅编译特定的算子。
图2
让我们实际操作一下:执行命令 sh build.sh -n "whole_reduce_sum"
。如图3所示,命令执行后,目标算子whole_reduce_sum
成功完成了编译并生成了对应的算子部署包。这证明单算子编译的方法是可行且有效的。
图3
然而,细心的开发者可能会发现一个看似不太寻常的现象:当我们向上查看编译过程的输出日志时(图4),会发现除了我们指定的whole_reduce_sum
算子外,编译信息中还夹杂着其他众多算子的相关内容。这不禁让人产生疑问:明明只指定了一个算子进行编译,为什么编译过程会涉及这么多“无关”信息呢?
图4
要解开这个疑惑,更清晰地理解整个编译流程究竟是如何运作的,我们需要从头梳理一下build.sh
脚本执行时的内在逻辑和关键步骤。
build.sh的执行流程
当我们执行 sh build.sh -n "whole_reduce_sum"
这条命令时,究竟发生了什么呢?让我们一步步拆解其执行脉络:
参数识别: 脚本捕捉到命令行中的 -n "whole_reduce_sum"
参数信息,将其提取并存入 ascend_op_name
变量中。这意味着它已经明确知晓:本次任务的核心是 whole_reduce_sum
这个特定算子。
环境准备: 紧接着,脚本调用 set_env
函数,布置所需的运行环境。这包括加载关键的 CANN 软件包的环境脚本,并确认 bisheng
编译工具已就绪可用。
清理场地: 为了确保构建过程不受旧数据干扰,clean
函数被调用。它移除之前可能遗留的构建目录 (build
) 和输出目录 (output
),然后为本次编译创建出干净、崭新的工作空间。
依赖安装: install_json_pkg
函数启动,它的职责是检查并确保 nlohmann_json
第三方库已妥善安装到项目指定的 third_party
目录下,为后续步骤扫清依赖障碍。
配置核心(关键步骤): 进入构建目录后,流程迎来了关键节点——调用 cmake_config
函数进行 CMake 配置。此时,脚本将我们指定的算子名称指令:-DASCEND_OP_NAME=whole_reduce_sum
,作为CUSTOM_OPTION的一部分加入到 CMake 的配置选项中。这一步的意图非常明确:告诉构建系统,本次只需编译名为 whole_reduce_sum
的算子。 (图5)
图5
执行构建: 一切准备就绪后,build_package
函数接过接力棒,它驱动 cmake --build
命令,启动针对 whole_reduce_sum
算子的编译过程。
从上述流程看,特别是第5步配置和第6步构建,脚本确实传达了“仅编译whole_reduce_sum
”的指令。那么,为什么我们在编译输出日志中(图4)会看到其他算子的信息呢?这可能需要更深入地探查 CMake 内部的运作细节。
从脚本的输出信息中(图6),我们可以清晰地看到最终传递给 cmake
命令的所有选项:
图6
- CUSTOM_OPTION / extra_option 的内容:(以.结束)
-DBUILD_OPEN_PROJECT=ON -DASCEND_OP_NAME=whole_reduce_sum -DASCEND_THIRD_LIB_PATH=/home/mindspore/work/cann-ops/third_party -DCUSTOM_ASCEND_CANN_PACKAGE_PATH=/usr/local/Ascend/ascend-toolkit/latest -DCHECK_COMPATIBLE=false .
从 CMakePresets.json 解析到的 opts 内容:-DCMAKE_BUILD_TYPE=Release -DENABLE_SOURCE_PACKAGE=True -DENABLE_BINARY_PACKAGE=True -DASCEND_COMPUTE_UNIT=ascend910b -DENABLE_TEST=True -Dvendor_name=customize -DASCEND_CANN_PACKAGE_PATH=/home/mindspore/Ascend/ascend-toolkit/latest -DASCEND_PYTHON_EXECUTABLE=python3 -DCMAKE_INSTALL_PREFIX=/home/mindspore/work/cann-ops/build_out -DENABLE_CROSS_COMPILE=False -DCMAKE_CROSS_PLATFORM_COMPILER=/usr/bin/aarch64-linux-gnu-g++
最终,这些选项被合并,形成了完整的 CMake 构建配置命令:
cmake .. \ -DCMAKE_BUILD_TYPE=Release \ -DENABLE_SOURCE_PACKAGE=True \ -DENABLE_BINARY_PACKAGE=True \ -DASCEND_COMPUTE_UNIT=ascend910b \ -DENABLE_TEST=True \ -Dvendor_name=customize \ -DASCEND_CANN_PACKAGE_PATH=/home/mindspore/Ascend/ascend-toolkit/latest \ -DASCEND_PYTHON_EXECUTABLE=python3 \ -DCMAKE_INSTALL_PREFIX=/home/mindspore/work/cann-ops/build_out \ -DENABLE_CROSS_COMPILE=False \ -DCMAKE_CROSS_PLATFORM_COMPILER=/usr/bin/aarch64-linux-gnu-g++ \ -DBUILD_OPEN_PROJECT=ON \ -DASCEND_OP_NAME=whole_reduce_sum \ #算子名称 -DASCEND_THIRD_LIB_PATH=/home/mindspore/work/cann-ops/third_party \ -DCUSTOM_ASCEND_CANN_PACKAGE_PATH=/usr/local/Ascend/ascend-toolkit/latest \ -DCHECK_COMPATIBLE=false
在这条冗长的命令中,我们清晰地看到了 -DASCEND_OP_NAME=whole_reduce_sum
这个选项,它确实被准确地传递给了 CMake。**这至少证明,脚本在意图传达“单算子编译”信息这个环节上,是没有问题的。**那么,问题的根源可能藏在 CMake 处理 ASCEND_OP_NAME
这个参数的方式,或者 CMake本身的构建逻辑之中。
CMake构建解析:疑点初现
现在我们需要深入项目的CMake构建描述体系。这套体系由主控文件 CMakeLists.txt
和三个关键支撑文件(config.cmake
、func.cmake
、intf.cmake
)共同驱动。让我们理清它们的工作脉络:
1. 构建规则总指挥:CMakeLists.txt此文件主导全局构建流程:
项目初始化: 确立项目名称为 cann_ops_adv
,定义 BUILD_OPEN_PROJECT
、ENABLE_CCACHE
等开关。
环境配置: 设置默认参数,包括NPU计算单元(ascend910b
)、默认算子名称(ALL
)、厂商名称(customize
)。这里有个关键细节:ASCEND_OP_NAME
变量默认值虽是 ALL
,但会被我们传入的 -DASCEND_OP_NAME=whole_reduce_sum
覆盖(图7)。
图7
加载配置文件: 引入三个配置文件(config.cmake
、func.cmake
、intf.cmake
)。
第三方支持: 配置 nlohmann_json
库的集成。
目标定义: 定义核心库目标(如 op_host_aclnn
、opapi、opsproto、optiling 、ops_kernel
、generate_ops_info
等)。
算子扫描(关键环节): 调用 op_add_subdirectory
函数扫描算子目录——此处将决定哪些算子被处理。
ACLNN处理: 生成ACLNN相关源文件和头文件。
构建目标: 自定义构建目标(如 prepare_build
、generate_compile_cmd、generate_ops_info等)。
部署与打包: 设置安装路径和打包参数。
2. 环境配置:config.cmake此文件专注基础环境检查和配置:
- 环境检查: 验证 Python3 和 CANN 工具包的有效性。全局开关: 设置影响全流程的构建开关(
PREPARE_BUILD
、ENABLE_OPS_HOST
等)。路径配置: 定义源码和构建的关键路径。参数调优: 根据构建类型调整编译参数。编译加速: 启用 CCACHE
缓存加速。版本适配: 检查与基础 CANN 的兼容性。预处理(关键疑点): 执行 prepare.sh
脚本——这个步骤可能成为突破口。3. 功能函数:func.cmake定义多个CMake函数用于算子构建:
- 目录扫描:通过
op_add_subdirectory
函数扫描算子目录依赖处理:通过op_add_depend_directory
处理算子依赖编译命令生成:通过add_compile_cmd_target
生成编译命令算子信息处理:通过add_ops_info_target
生成算子信息编译选项处理:通过add_ops_compile_options
、add_ops_tiling_keys
、add_opc_config
处理编译选项源文件复制:通过add_ops_src_copy
处理源文件复制二进制编译:通过add_bin_compile_target
处理二进制文件编译静态算子处理:通过add_static_ops
处理静态算子NPU支持:通过add_npu_support_target
生成NPU支持配置4. 接口文件:intf.cmake
- 条件加载: 根据
BUILD_OPEN_PROJECT
开关动态加载接口。公共接口: 引入 intf_pub.cmake
定义基础配置。测试接口: 按需加载测试相关接口。5. 公共接口:intf_pub.cmake定义所有目标共享的编译属性:
- 接口目标: 定义
intf_pub
接口库。头文件路径: 设置 CANN 头文件包含目录。链接配置: 指定库搜索路径和安全加固选项。编译设置: 统一编译选项。梳理完复杂的构建规则网络,一个核心矛盾愈发清晰:CMakeLists.txt
开头明确定义 ASCEND_OP_NAME
变量已被我们的 -DASCEND_OP_NAME=whole_reduce_sum
覆盖(图7),理论上后续流程应仅处理 whole_reduce_sum
算子,但编译日志中的多算子信息证明事实并非如此。经过反复验证,疑点指向了 config.cmake
中的预处理环节—— prepare.sh
脚本的执行。这个在环境配置期间立即运行的脚本,可能正是导致 ASCEND_OP_NAME
设置被干扰的关键所在。它是否在CMake变量生效前就锁定了算子范围?(这个不太可能,ASCEND_OP_NAME在CMakeLists.txt的开头就进行了定义。)还是绕过了我们传入的算子名称直接进行操作?
prepare.sh:缺失的算子参数
当我们仔细审视执行 prepare.sh
脚本的完整命令时,发现一个关键问题:
bash /home/mindspore/work/cann-ops/cmake/scripts/prepare.sh \ -s /home/mindspore/work/cann-ops \ -b /home/mindspore/work/cann-ops/build/prepare_build \ -p /usr/local/Ascend/ascend-toolkit/latest \ --autogen-dir /home/mindspore/work/cann-ops/build/autogen \ --build-open-project ON \ --binary-out-dir /home/mindspore/work/cann-ops/build/binary \ --impl-out-dir /home/mindspore/work/cann-ops/build/impl \ --op-build-tool /usr/local/Ascend/ascend-toolkit/latest/tools/opbuild/op_build \ --ascend-cmake-dir /usr/local/Ascend/ascend-toolkit/latest/tools/op_project_templates/ascendc/customize/cmake \ --tiling-key FALSE \ --ops-compile-options FALSE \ --check-compatible false \ --ascend-compute_unit ascend910b \ --op_debug_config false \ --third_lib_path /home/mindspore/work/cann-ops/third_party \ RESULT_VARIABLE result \ OUTPUT_STRIP_TRAILING_WHITESPACE \ OUTPUT_VARIABLE PREPARE_BUILD_OUTPUT_VARIABLE
在 prepare.sh
的众多调用参数中,唯独缺少了我们最关心的 ASCEND_OP_NAME
参数! 用户调用时指定的 whole_reduce_sum
算子名称在这里完全消失了(图8)。
图8
那么,prepare.sh 究竟做了什么?原来,在 prepare.sh
脚本内部,它执行了一个嵌套的 CMake 配置和构建过程(图9):
图9
- 在
build/prepare_build
目录下重新进行 CMake 配置接着执行 make prepare_build
完成特定构建目标嵌套 CMake 配置的完整命令:
cmake /home/mindspore/work/cann-ops \ -DBUILD_OPEN_PROJECT=ON \ -DPREPARE_BUILD=ON \ # 关键标记 -DCUSTOM_ASCEND_CANN_PACKAGE_PATH=/usr/local/Ascend/ascend-toolkit/latest \ -DASCEND_AUTOGEN_DIR=/home/mindspore/work/cann-ops/build/autogen \ -DASCEND_BINARY_OUT_DIR=/home/mindspore/work/cann-ops/build/binary \ -DASCEND_IMPL_OUT_DIR=/home/mindspore/work/cann-ops/build/impl \ -DOP_BUILD_TOOL=/usr/local/Ascend/ascend-toolkit/latest/tools/opbuild/op_build \ -DASCEND_CMAKE_DIR=/usr/local/Ascend/ascend-toolkit/latest/tools/op_project_templates/ascendc/customize/cmake \ -DCHECK_COMPATIBLE=false \ -DTILING_KEY=FALSE \ -DOPS_COMPILE_OPTIONS=FALSE \ -DASCEND_COMPUTE_UNIT=ascend910b \ -DOP_DEBUG_CONFIG=false \ -DASCEND_THIRD_LIB_PATH=/home/mindspore/work/cann-ops/third_party
作为下游,在这组配置参数中,自然也无从获取和传递 -DASCEND_OP_NAME=whole_reduce_sum
。这意味着在 prepare.sh 触发的这次嵌套构建中,ASCEND_OP_NAME
变量将使用其默认值—— ALL
(图10)。
图10
问题全貌串联:
- 用户调用
sh build.sh -n "whole_reduce_sum"
,主构建流程中 CMake 正确接收了 -DASCEND_OP_NAME=whole_reduce_sum
主 CMakeLists.txt 引入 config.cmakeconfig.cmake 调用 prepare.sh 脚本prepare.sh 未携带 ASCEND_OP_NAME 参数,启动嵌套 CMake 配置嵌套配置中 ASCEND_OP_NAME
缺失,回退到默认值 ALL嵌套构建在 ASCEND_OP_NAME=ALL
状态下执行,处理所有算子主构建的输出日志流中,用户看到单算子编译却有多算子信息的"异常"现象细心的读者可能担心:prepare.sh 内部又调用 CMake,而 CMake 会再次加载 config.cmake,是否会导致 prepare.sh 被无限递归调用?
答案是不会的(图11):
图11
- 主构建中调用 prepare.sh 的条件是
(NOT PREPARE_BUILD AND ENABLE_OPS_KERNEL)
嵌套构建通过 -DPREPARE_BUILD=ON
明确标记了自身性质当嵌套构建的 CMake 加载到 config.cmake 时,因 PREPARE_BUILD=ON
,跳过了执行 prepare.sh 的条件分支这种设计避免了无限递归,但是通过prepare.sh进行嵌套构建的机制,使得嵌套构建未能继承主构建的算子参数解决办法
"单算子编译出现多算子信息"的根本原因,在于 prepare.sh 脚本在发起嵌套构建时,未能将用户指定的 ASCEND_OP_NAME 参数传递给内部 CMake 配置。要解决这个问题,需要改造 config.cmake 和 prepare.sh,使其能够传递并接收 ASCEND_OP_NAME 参数。
一个更简单的临时办法是,直接修改CMakeLists.txt里面 ASCEND_OP_NAME 的值为用户传入的值,可以看到执行时间缩短到只有原来的1/3(图12)。
图12