Open
fix: CannBotDSL 不可用时跳过构建并隔离运行依赖 #3
zyh_gitcodeAdd on 15 days ago
fix: CannBotDSL 不可用时跳过构建并隔离运行依赖 #3
Open
zyh_gitcodeAdd on 15 days ago
Total: 12 files changed+569-18
@@ -68,6 +68,8 @@ aclnnStatus aclnnMixedQuantSparseFlashMlaGetWorkspaceSize(
68 if (!workspaceSize || !executor) {68 if (!workspaceSize || !executor) {
69 return ACLNN_ERR_PARAM_NULLPTR;69 return ACLNN_ERR_PARAM_NULLPTR;
70 }70 }
71+ *workspaceSize = 0;
72+ *executor = nullptr;
71 auto uniqueExecutor = CREATE_EXECUTOR();73 auto uniqueExecutor = CREATE_EXECUTOR();
72 if (!uniqueExecutor.get()) {74 if (!uniqueExecutor.get()) {
73 return ACLNN_ERR_INNER_CREATE_EXECUTOR;75 return ACLNN_ERR_INNER_CREATE_EXECUTOR;
@@ -34,7 +34,8 @@ add_modules_sources(
34)34)
35 35 
36if(BUILD_OPEN_PROJECT)36if(BUILD_OPEN_PROJECT)
37- enable_cannbotdsl_kernel(mixed_quant_sparse_flash_mla "${CMAKE_CURRENT_SOURCE_DIR}/../op_dsl")37+ enable_cannbotdsl_kernel(mixed_quant_sparse_flash_mla "${CMAKE_CURRENT_SOURCE_DIR}/../op_dsl"
38+ L0_NAME MixedQuantSparseFlashMlaDsl)
38endif()39endif()
39 40 
40include(${CMAKE_CURRENT_SOURCE_DIR}/../../sparse_flash_mla/op_host/checkers/checker_sources.cmake)41include(${CMAKE_CURRENT_SOURCE_DIR}/../../sparse_flash_mla/op_host/checkers/checker_sources.cmake)
@@ -812,7 +812,7 @@ function collect_rpm_deb_package() {
812 812 
813function build_package(){813function build_package(){
814 if [ -f "${BUILD_PATH}/no_ops.flag" ]; then814 if [ -f "${BUILD_PATH}/no_ops.flag" ]; then
815- echo "[INFO] No operator can be compiled (all requested ops skipped by CANN version check), skip package generation."815+ echo "[INFO] No operator can be compiled (all requested ops skipped by compatibility checks), skip package generation."
816 return 0816 return 0
817 fi817 fi
818 if [[ "${PACKAGE_TYPE}" == "all" ]]; then818 if [[ "${PACKAGE_TYPE}" == "all" ]]; then
@@ -845,7 +845,7 @@ function build_host(){
845 845 
846function build_kernel(){846function build_kernel(){
847 if [ -f "${BUILD_PATH}/no_ops.flag" ]; then847 if [ -f "${BUILD_PATH}/no_ops.flag" ]; then
848- echo "[INFO] No operator can be compiled (all requested ops skipped by CANN version check), skip kernel build."848+ echo "[INFO] No operator can be compiled (all requested ops skipped by compatibility checks), skip kernel build."
849 return 0849 return 0
850 fi850 fi
851 build ops_transformer_kernel851 build ops_transformer_kernel
@@ -568,6 +568,15 @@ else()
568 file(REMOVE "${CMAKE_BINARY_DIR}/no_ops.flag")568 file(REMOVE "${CMAKE_BINARY_DIR}/no_ops.flag")
569endif()569endif()
570 570 
571+get_property(_dsl_skipped GLOBAL PROPERTY CANNBOTDSL_SKIPPED_ONLY_OPS)
572+if(_dsl_skipped AND NOT CANNBOTDSL_PACKAGES)
573+ set(_remaining_ops ${ASCEND_OP_NAME})
574+ list(REMOVE_ITEM _remaining_ops ${_dsl_skipped})
575+ if(NOT _remaining_ops)
576+ file(WRITE "${CMAKE_BINARY_DIR}/no_ops.flag" "no operator passed compatibility checks\n")
577+ endif()
578+endif()
579+ 
571# 已生成的 DSL 包仍需构建和打包,不属于零算子。580# 已生成的 DSL 包仍需构建和打包,不属于零算子。
572if (CANNBOTDSL_PACKAGES)581if (CANNBOTDSL_PACKAGES)
573 file(REMOVE "${CMAKE_BINARY_DIR}/no_ops.flag")582 file(REMOVE "${CMAKE_BINARY_DIR}/no_ops.flag")
@@ -759,6 +759,27 @@ endfunction()
759 759 
760# Called by the operator CMakeLists after native operator selection.760# Called by the operator CMakeLists after native operator selection.
761function(enable_cannbotdsl_kernel op_name source_dir)761function(enable_cannbotdsl_kernel op_name source_dir)
762+ set(${op_name}_DSL_AVAILABLE FALSE PARENT_SCOPE)
763+ cmake_parse_arguments(DSL "" "L0_NAME" "" ${ARGN})
764+ if(DSL_L0_NAME AND TARGET ${OPHOST_NAME}_opapi_obj)
765+ string(REGEX REPLACE "([a-z0-9])([A-Z])" "\\1_\\2" _l0_stem "${DSL_L0_NAME}")
766+ string(TOLOWER "${_l0_stem}" _l0_stem)
767+ set(_unavailable "${CMAKE_BINARY_DIR}/cannbot_dsl/unavailable/${op_name}")
768+ file(MAKE_DIRECTORY "${_unavailable}")
769+ file(CONFIGURE OUTPUT "${_unavailable}/${_l0_stem}.h" CONTENT [=[
770+#pragma once
771+#include <aclnn/aclnn_base.h>
772+#include <opdev/op_log.h>
773+namespace l0op {
774+template <typename... Args> inline aclnnStatus @DSL_L0_NAME@(Args&&...)
775+{
776+ OP_LOGE(ACLNN_ERR_INNER, "CannBotDSL @op_name@ was not compiled in this Host package; check the build compatibility warning and install matching components.");
777+ return ACLNN_ERR_INNER;
778+}
779+}
780+]=] @ONLY)
781+ target_include_directories(${OPHOST_NAME}_opapi_obj PRIVATE "${_unavailable}")
782+ endif()
762 set(_dsl_compute_unit ascend950)783 set(_dsl_compute_unit ascend950)
763 if(NOT _dsl_compute_unit IN_LIST ASCEND_COMPUTE_UNIT784 if(NOT _dsl_compute_unit IN_LIST ASCEND_COMPUTE_UNIT
764 OR (NOT ENABLE_OPS_HOST AND NOT ENABLE_OPS_KERNEL))785 OR (NOT ENABLE_OPS_HOST AND NOT ENABLE_OPS_KERNEL))
@@ -787,14 +808,48 @@ function(enable_cannbotdsl_kernel op_name source_dir)
787 set(_mode kernel)808 set(_mode kernel)
788 endif()809 endif()
789 set(_stage "${_dsl_root}/${_mode}/${op_name}")810 set(_stage "${_dsl_root}/${_mode}/${op_name}")
811+ if(TARGET nnopbase)
812+ include(CheckCXXSourceCompiles)
813+ set(CMAKE_REQUIRED_INCLUDES ${NNOPBASE_OPDEV_INC_DIR} ${NNOPBASE_ACLNN_INC_DIR}
814+ ${ACL_INC_DIR} "${ASCEND_CANN_PACKAGE_PATH}/include")
815+ set(CMAKE_REQUIRED_LIBRARIES nnopbase "${ASCEND_CANN_PACKAGE_PATH}/lib64/libascendcl.so")
816+ set(CMAKE_REQUIRED_FLAGS "-std=c++17")
817+ set(CMAKE_REQUIRED_LINK_OPTIONS "-Wl,--allow-shlib-undefined")
818+ unset(_dsl_abi_ok CACHE)
819+ check_cxx_source_compiles([=[
820+#include <acl/acl.h>
821+#include <opdev/direct_invoke_task.h>
822+int main() {
823+ using Submit = aclnnStatus (*)(const char*, uint32_t, op::DirectInvokePrepareFn,
824+ op::DirectInvokeLaunchFn, op::DirectInvokeDestroyFn, aclOpExecutor*, op::OpArgContext*);
825+ Submit volatile submit = &op::AddDirectInvokeTask;
826+ auto volatile load = &aclrtBinaryLoadFromData;
827+ auto volatile get = &aclrtBinaryGetFunctionByEntry;
828+ auto volatile launch = &aclrtLaunchKernelWithArgsArray;
829+ auto volatile simt = &aclrtLaunchSIMTKernelWithArgsArray;
830+ return submit && load && get && launch && simt ? 0 : 1;
831+}
832+]=] _dsl_abi_ok)
833+ endif()
790 file(GLOB_RECURSE _dependencies CONFIGURE_DEPENDS "${_source_dir}/*.py")834 file(GLOB_RECURSE _dependencies CONFIGURE_DEPENDS "${_source_dir}/*.py")
791 set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_dependencies} "${_builder}")835 set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS ${_dependencies} "${_builder}")
792- execute_process(COMMAND "${CMAKE_COMMAND}" -E env "ASCEND_HOME_PATH=${ASCEND_CANN_PACKAGE_PATH}"836+ if(TARGET nnopbase AND NOT _dsl_abi_ok)
793- TORCH_DEVICE_BACKEND_AUTOLOAD=0 PYTHONDONTWRITEBYTECODE=1837+ set(_result 77)
794- "${HI_PYTHON}" "${_builder}"838+ set(_stderr "CANN Runtime or OpBase DirectInvoke ABI does not match; see the CMake configure log")
795- --source "${_source_dir}/${op_name}.py" --output "${_stage}"839+ else()
796- --soc "${_dsl_compute_unit}" --mode "${_mode}" --aclnn-type "${_aclnn_type}"840+ execute_process(COMMAND "${CMAKE_COMMAND}" -E env "ASCEND_HOME_PATH=${ASCEND_CANN_PACKAGE_PATH}"
797- RESULT_VARIABLE _result OUTPUT_VARIABLE _stdout ERROR_VARIABLE _stderr)841+ TORCH_DEVICE_BACKEND_AUTOLOAD=0 PYTHONDONTWRITEBYTECODE=1
842+ "${HI_PYTHON}" "${_builder}"
843+ --source "${_source_dir}/${op_name}.py" --output "${_stage}"
844+ --soc "${_dsl_compute_unit}" --mode "${_mode}" --aclnn-type "${_aclnn_type}"
845+ RESULT_VARIABLE _result OUTPUT_VARIABLE _stdout ERROR_VARIABLE _stderr)
846+ endif()
847+ if(_result EQUAL 77)
848+ file(REMOVE_RECURSE "${_stage}")
849+ file(REMOVE "${_stage}.input.json" "${_stage}.report.json")
850+ message(WARNING "[CannBotDSL] ${op_name}: DSL not compiled; other implementations remain enabled.\n${_stderr}")
851+ return()
852+ endif()
798 if(NOT _result EQUAL 0)853 if(NOT _result EQUAL 0)
799 file(MAKE_DIRECTORY "${_dsl_root}/${_mode}")854 file(MAKE_DIRECTORY "${_dsl_root}/${_mode}")
800 set(_error_log "${_stage}.error.log")855 set(_error_log "${_stage}.error.log")
@@ -812,6 +867,7 @@ function(enable_cannbotdsl_kernel op_name source_dir)
812 endif()867 endif()
813 message(FATAL_ERROR "DSL source publication failed for ${op_name}: ${_cause}\nExit result: ${_result}\nPython: ${HI_PYTHON}\nFull log: ${_error_log}")868 message(FATAL_ERROR "DSL source publication failed for ${op_name}: ${_cause}\nExit result: ${_result}\nPython: ${HI_PYTHON}\nFull log: ${_error_log}")
814 endif()869 endif()
870+ set(${op_name}_DSL_AVAILABLE TRUE PARENT_SCOPE)
815 file(READ "${_stage}.report.json" _report)871 file(READ "${_stage}.report.json" _report)
816 string(JSON _count LENGTH "${_report}" dependencies)872 string(JSON _count LENGTH "${_report}" dependencies)
817 math(EXPR _last "${_count} - 1")873 math(EXPR _last "${_count} - 1")
@@ -134,6 +134,13 @@ function(_cannbotdsl_add_runtime source)
134 endif()134 endif()
135 endforeach()135 endforeach()
136 add_subdirectory("${source}" "${CMAKE_BINARY_DIR}/cannbot_dsl/runtime")136 add_subdirectory("${source}" "${CMAKE_BINARY_DIR}/cannbot_dsl/runtime")
137+ get_target_property(_runtime_type opkit_runtime TYPE)
138+ if(NOT _runtime_type STREQUAL "INTERFACE_LIBRARY")
139+ # Guard the runtime submission once for generated and handwritten L0 users.
140+ # Scope weak declarations here so newer optional APIs do not prevent ASC library loading.
141+ set(_compat_header "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../common/include/common/cannbotdsl_compat.h")
142+ target_compile_options(opkit_runtime PRIVATE "SHELL:-include '${_compat_header}'")
143+ endif()
137 foreach(_variable CANN_ROOT OPBASE_INCLUDE_DIR OPBASE_LIBRARY)144 foreach(_variable CANN_ROOT OPBASE_INCLUDE_DIR OPBASE_LIBRARY)
138 if(_variable IN_LIST _temporary_cache)145 if(_variable IN_LIST _temporary_cache)
139 unset(${_variable} CACHE)146 unset(${_variable} CACHE)
@@ -162,8 +169,6 @@ function(cannbotdsl_add_modules out_headers)
162 _cannbotdsl_add_runtime("${_stage}/runtime")169 _cannbotdsl_add_runtime("${_stage}/runtime")
163 endif()170 endif()
164 if(TARGET ${OPHOST_NAME}_opapi_obj)171 if(TARGET ${OPHOST_NAME}_opapi_obj)
165- string(TOUPPER "${op_name}" _op_macro)
166- target_compile_definitions(${OPHOST_NAME}_opapi_obj PRIVATE "${_op_macro}_WITH_DSL=1")
167 target_link_libraries(${OPHOST_NAME}_opapi_obj PRIVATE opkit::runtime)172 target_link_libraries(${OPHOST_NAME}_opapi_obj PRIVATE opkit::runtime)
168 endif()173 endif()
169 string(JSON _last LENGTH "${_package}" operators)174 string(JSON _last LENGTH "${_package}" operators)
@@ -189,7 +194,27 @@ function(cannbotdsl_add_modules out_headers)
189 add_subdirectory("${_module_dir}" "${_dsl_root}/${_operator}/module")194 add_subdirectory("${_module_dir}" "${_dsl_root}/${_operator}/module")
190 endif()195 endif()
191 if(TARGET ${OPHOST_NAME}_opapi_obj)196 if(TARGET ${OPHOST_NAME}_opapi_obj)
192- target_include_directories(${OPHOST_NAME}_opapi_obj PRIVATE "${_module_dir}/op_api")197+ target_include_directories(${OPHOST_NAME}_opapi_obj BEFORE PRIVATE "${_module_dir}/op_api")
198+ endif()
199+ get_target_property(_runtime_type opkit_runtime TYPE)
200+ if(NOT _runtime_type STREQUAL "INTERFACE_LIBRARY" AND NOT _aclnn_type STREQUAL "aclnn_exclude")
201+ # Adapt only the generated L0 build copy. Keep the publication and its
202+ # integrity manifest intact; no OpKit function names are redefined.
203+ set(_l0 "${_module_dir}/op_api/${_stem}.cpp")
204+ file(READ "${_l0}" _l0_code)
205+ set(_submit "return ADD_TO_LAUNCHER_LIST_CANNBOTDSL(")
206+ string(FIND "${_l0_code}" "${_submit}" _submit_at)
207+ if(_submit_at LESS 0)
208+ message(FATAL_ERROR "Unsupported generated DSL L0 entry: ${_l0}")
209+ endif()
210+ set(_compat_header "${CMAKE_CURRENT_FUNCTION_LIST_DIR}/../common/include/common/cannbotdsl_compat.h")
211+ string(REPLACE "${_submit}"
212+ "auto compatibility_status = ops_transformer::CheckCannbotdslRuntime(\"${_operator}\");\n if (compatibility_status != ACLNN_SUCCESS) { return compatibility_status; }\n ${_submit}"
213+ _l0_code "${_l0_code}")
214+ set(_checked_l0 "${_dsl_root}/${_operator}/checked_l0.cpp")
215+ file(WRITE "${_checked_l0}" "#include \"${_compat_header}\"\n${_l0_code}")
216+ set_source_files_properties("${_l0}" TARGET_DIRECTORY "operator_${_operator}" PROPERTIES HEADER_FILE_ONLY TRUE)
217+ target_sources("operator_${_operator}" PRIVATE "${_checked_l0}")
193 endif()218 endif()
194 # Hide implementation symbols while retaining the public ACLNN declarations.219 # Hide implementation symbols while retaining the public ACLNN declarations.
195 set_target_properties("operator_${_operator}" PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES)220 set_target_properties("operator_${_operator}" PROPERTIES CXX_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN YES)
@@ -12,12 +12,46 @@
12import argparse12import argparse
13import fcntl13import fcntl
14import hashlib14import hashlib
15+import importlib.metadata
16+import inspect
15import json17import json
16import os18import os
17from pathlib import Path19from pathlib import Path
18import shutil20import shutil
21+import sys
19import tempfile22import tempfile
20 23 
24+class DslUnavailable(ValueError):
25+ """The optional DSL build environment does not meet its requirements."""
26+ 
27+ 
28+def check_distribution(name):
29+ from packaging.requirements import Requirement
30+ from packaging.specifiers import SpecifierSet
31+ 
32+ dist = importlib.metadata.distribution(name)
33+ python = dist.metadata.get("Requires-Python")
34+ if python and not SpecifierSet(python).contains(".".join(map(str, sys.version_info[:3]))):
35+ raise DslUnavailable(f"{name} requires Python {python}; detected {sys.version.split()[0]}")
36+ for text in dist.requires or ():
37+ requirement = Requirement(text)
38+ if requirement.marker and not requirement.marker.evaluate({"extra": ""}):
39+ continue
40+ found = importlib.metadata.version(requirement.name)
41+ if requirement.specifier and not requirement.specifier.contains(found, prereleases=True):
42+ raise DslUnavailable(f"{name} requires {requirement}; detected {requirement.name}=={found}")
43+ return {"version": dist.version}
44+ 
45+def check_dependencies():
46+ try:
47+ for name in ("cannbot-dsl", "opkit"):
48+ check_distribution(name)
49+ import opkit
50+ if not {"mode", "l0", "l2"}.issubset(inspect.signature(opkit.build).parameters):
51+ raise DslUnavailable("OpKit must support Host/Kernel/All publication and l0/l2")
52+ except (ImportError, OSError, RuntimeError, ValueError) as exc:
53+ raise DslUnavailable(str(exc)) from exc
54+ 
21 55 
22def resolve_target(soc):56def resolve_target(soc):
23 # Match the SoCs supported by OpKit device publication. Resolve the selected57 # Match the SoCs supported by OpKit device publication. Resolve the selected
@@ -107,7 +141,7 @@ def framework_files():
107 distribution("opkit").locate_file("opkit/resources/runtime/CMakeLists.txt")141 distribution("opkit").locate_file("opkit/resources/runtime/CMakeLists.txt")
108 )142 )
109 if not runtime.is_file():143 if not runtime.is_file():
110- raise ValueError(144+ raise DslUnavailable(
111 f"OpKit at {opkit.__file__} does not provide the current device/C++ runtime at {runtime}. "145 f"OpKit at {opkit.__file__} does not provide the current device/C++ runtime at {runtime}. "
112 "The build toolchain requires matching device/C++ compiler components."146 "The build toolchain requires matching device/C++ compiler components."
113 )147 )
@@ -138,8 +172,11 @@ def framework_files():
138 )172 )
139 from opkit.compiler.toolchain import optimizer_path173 from opkit.compiler.toolchain import optimizer_path
140 174 
141- paths.add(Path(optimizer_path()).resolve(strict=True))175+ try:
142- paths.add(Path(resolve_bisheng().path).resolve(strict=True))176+ paths.add(Path(optimizer_path()).resolve(strict=True))
177+ paths.add(Path(resolve_bisheng().path).resolve(strict=True))
178+ except (OSError, RuntimeError) as exc:
179+ raise DslUnavailable(str(exc)) from exc
143 version = Path(os.environ["ASCEND_HOME_PATH"]) / "version.info"180 version = Path(os.environ["ASCEND_HOME_PATH"]) / "version.info"
144 if version.is_file():181 if version.is_file():
145 paths.add(version.resolve())182 paths.add(version.resolve())
@@ -276,8 +313,13 @@ def main():
276 args.output.parent.mkdir(parents=True, exist_ok=True)313 args.output.parent.mkdir(parents=True, exist_ok=True)
277 with Path(str(args.output) + ".lock").open("w") as lock:314 with Path(str(args.output) + ".lock").open("w") as lock:
278 fcntl.flock(lock, fcntl.LOCK_EX)315 fcntl.flock(lock, fcntl.LOCK_EX)
279- build(args)316+ try:
317+ check_dependencies()
318+ build(args)
319+ except DslUnavailable as exc:
320+ print(f"CannBotDSL unavailable: {exc}", file=sys.stderr)
321+ return 77
280 322 
281 323 
282if __name__ == "__main__":324if __name__ == "__main__":
283- main()325+ raise SystemExit(main())
@@ -0,0 +1,45 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * Licensed under the CANN Open Software License Agreement Version 2.0.
4+ */
5+#ifndef OPS_TRANSFORMER_CANNBOTDSL_COMPAT_H_
6+#define OPS_TRANSFORMER_CANNBOTDSL_COMPAT_H_
7+ 
8+// Optional DSL runtime and L0 entries share these capability checks.
9+// ASC does not require these headers or symbols.
10+#include "runtime.h"
11+#include "opdev/op_log.h"
12+#include <cstring>
13+ 
14+#pragma weak aclrtBinaryLoadFromData
15+#pragma weak aclrtBinaryGetFunctionByEntry
16+#pragma weak aclrtLaunchKernelWithArgsArray
17+#pragma weak aclrtLaunchSIMTKernelWithArgsArray
18+ 
19+namespace op {
20+extern decltype(AddDirectInvokeTask) AddDirectInvokeTask __attribute__((weak));
21+ 
22+} // namespace op
23+ 
24+namespace ops_transformer {
25+inline aclnnStatus CheckCannbotdslRuntime(const char* name)
26+{
27+ const bool directInvoke = op::AddDirectInvokeTask != nullptr;
28+ const bool runtime = aclrtBinaryLoadFromData && aclrtBinaryGetFunctionByEntry &&
29+ aclrtLaunchKernelWithArgsArray && aclrtLaunchSIMTKernelWithArgsArray;
30+ if (!directInvoke || !runtime) {
31+ OP_LOGE(ACLNN_ERR_INNER, "CannBotDSL operator %s is unavailable: incompatible %s.", name,
32+ directInvoke ? "Runtime binary/launch APIs" : "OpBase DirectInvoke ABI");
33+ return ACLNN_ERR_INNER;
34+ }
35+ const char* soc = aclrtGetSocName();
36+ if (soc == nullptr || std::strncmp(soc, "Ascend950", 9) != 0) {
37+ OP_LOGE(ACLNN_ERR_INNER, "CannBotDSL operator %s requires Ascend950; detected %s.",
38+ name, soc == nullptr ? "unknown" : soc);
39+ return ACLNN_ERR_INNER;
40+ }
41+ return ACLNN_SUCCESS;
42+}
43+} // namespace ops_transformer
44+ 
45+#endif
@@ -144,3 +144,15 @@ DSL Host 模块。最终由原有安装规则汇总,不需要手动拼接中
144 144 
145扩展编译规格、接入 ASC/DSL 混合算子及了解各接口分类,请继续阅读145扩展编译规格、接入 ASC/DSL 混合算子及了解各接口分类,请继续阅读
146[DSL 开发指南](cannbotdsl_develop_guide.md)。146[DSL 开发指南](cannbotdsl_develop_guide.md)。
147+ 
148+## 可选 DSL 环境与兼容性提示
149+ 
150+DSL 是可选后端。公共构建会检查本次 Python 中的 `cannbot-dsl`、`opkit` 及其发布元数据声明的依赖范围、Host/Kernel 分模式接口和构建工具;同时检查所选 CANN 的 Runtime API 与 OpBase DirectInvoke 接口。Host 任务也需要 DSL 前端和 Host 降低工具,但不要求构建机器存在 NPU。
151+ 
152+环境不满足时输出 `[CannBotDSL] ... DSL not compiled`,不登记该 DSL 发布包。混合算子的 ASC 部分继续构建,其他算子仍按原有条件编译和安装。跳过原因直接写入构建日志,对应的历史发布包会被清理,避免误收进新安装包。仅选择 AddDsl 且环境不满足时,日志会说明未生成可执行算子并跳过打包。
153+ 
154+Transformer 在生成 L0 的编译副本中显式调用公共检查,保留发布包与校验信息不变,不使用函数名称替换宏。运行时检查实际 OpBase DirectInvoke ABI、Runtime 二进制加载/启动接口和 SoC;接口按弱引用处理,避免可选 DSL 的新符号阻止 ASC 共享库加载。检查失败只返回当前 DSL 调用的错误,日志指出不满足的组件能力。MQSMLA 的 DSL 分支未编入时也会明确提示,不自动切换到数据格式不同的 ASC 分支。AddDsl 的 Torch 调用若未找到安装接口,会提示检查构建兼容日志和 OPP 安装。
155+ 
156+兼容性检查使用组件声明的版本范围和实际接口能力,不要求 Toolkit 与构建时版本字符串完全相等。源码错误、DSL 编译错误和产物校验错误仍会报告失败,不会被作为环境不支持而隐藏。运行 C++ ACLNN 产物不要求部署机器安装 Python 的 CannBotDSL/OpKit。
157+ 
158+混合算子的公共发布登记可指定 L0 名称,例如 `enable_cannbotdsl_kernel(mixed_quant_sparse_flash_mla "${CMAKE_CURRENT_SOURCE_DIR}/../op_dsl" L0_NAME MixedQuantSparseFlashMlaDsl)`。Host 未发布时框架生成轻量不可用入口,Host 发布成功时优先使用真实生成接口;算子 C++ 不需要通过 `WITH_DSL` 条件编译分发。
@@ -6,6 +6,11 @@ if(NOT "add_dsl" IN_LIST ASCEND_OP_NAME)
6 return()6 return()
7endif()7endif()
8 8 
9-add_op_to_compiled_list("${CMAKE_CURRENT_SOURCE_DIR}")
10add_modules_sources(OPTYPE add_dsl ACLNNTYPE aclnn)9add_modules_sources(OPTYPE add_dsl ACLNNTYPE aclnn)
11enable_cannbotdsl_kernel(add_dsl "${CMAKE_CURRENT_SOURCE_DIR}/op_dsl")10enable_cannbotdsl_kernel(add_dsl "${CMAKE_CURRENT_SOURCE_DIR}/op_dsl")
11+ 
12+if(add_dsl_DSL_AVAILABLE)
13+ add_op_to_compiled_list("${CMAKE_CURRENT_SOURCE_DIR}")
14+else()
15+ set_property(GLOBAL APPEND PROPERTY CANNBOTDSL_SKIPPED_ONLY_OPS add_dsl)
16+endif()
@@ -33,6 +33,9 @@ at::Tensor add_dsl(const at::Tensor& x, const at::Tensor& y)
33 33 
34 const c10::OptionalDeviceGuard device_guard(x.device());34 const c10::OptionalDeviceGuard device_guard(x.device());
35 auto out = at::empty(x.sizes(), x.options());35 auto out = at::empty(x.sizes(), x.options());
36+ TORCH_CHECK(GetOpApiFuncAddr("aclnnAddDslGetWorkspaceSize") != nullptr,
37+ "CannBotDSL AddDsl was not compiled or installed in the selected OPP package. "
38+ "Check the build compatibility warning and install matching CANN/DSL components.");
36 ACLNN_CMD(aclnnAddDsl, x, y, out);39 ACLNN_CMD(aclnnAddDsl, x, y, out);
37 return out;40 return out;
38}41}
@@ -0,0 +1,351 @@
1+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
2+# Licensed under the CANN Open Software License Agreement Version 2.0.
3+"""Optional DSL compatibility must not prevent unrelated ASC builds or calls."""
4+ 
5+import ctypes
6+import importlib.util
7+import os
8+from pathlib import Path
9+import shutil
10+import subprocess
11+import sys
12+import types
13+ 
14+import pytest
15+ 
16+ROOT = Path(__file__).resolve().parents[3]
17+ 
18+RUNTIME_INTERFACE = '''#pragma once
19+#include <cstdint>
20+using aclnnStatus = int;
21+struct aclOpExecutor {};
22+constexpr int ACLNN_SUCCESS=0;
23+constexpr int ACLNN_ERR_INNER=7;
24+extern "C" {
25+int aclrtBinaryLoadFromData();
26+int aclrtBinaryGetFunctionByEntry();
27+int aclrtLaunchKernelWithArgsArray();
28+int aclrtLaunchSIMTKernelWithArgsArray();
29+const char* aclrtGetSocName();
30+}
31+namespace op {
32+struct OpArgContext {};
33+using DirectInvokePrepareFn = void(*)();
34+using DirectInvokeLaunchFn = void(*)();
35+using DirectInvokeDestroyFn = void(*)();
36+inline void DestroyOpArgContext(OpArgContext*) {}
37+aclnnStatus AddDirectInvokeTask(const char*, uint32_t, DirectInvokePrepareFn,
38+ DirectInvokeLaunchFn, DirectInvokeDestroyFn, aclOpExecutor*, OpArgContext*);
39+}
40+'''
41+ 
42+ 
43+def checker():
44+ spec = importlib.util.spec_from_file_location("dsl_compat", ROOT / "cmake/scripts/build_cannbotdsl.py")
45+ module = importlib.util.module_from_spec(spec)
46+ spec.loader.exec_module(module)
47+ return module
48+ 
49+ 
50+@pytest.mark.parametrize("version,expected", [("0.7.0", False), ("0.8.1", True), ("0.9.0", False)])
51+def test_declared_dependency_range(monkeypatch, version, expected):
52+ module = checker()
53+ monkeypatch.setattr(module.importlib.metadata, "distribution", lambda name: types.SimpleNamespace(
54+ version="0.8.0", requires=["cannbot-dsl>=0.8.0,<0.9.0", 'torch; extra == "test"'], metadata={}))
55+ monkeypatch.setattr(module.importlib.metadata, "version", lambda name: version)
56+ if expected:
57+ assert module.check_distribution("opkit") == {"version": "0.8.0"}
58+ else:
59+ with pytest.raises(module.DslUnavailable, match="cannbot-dsl"):
60+ module.check_distribution("opkit")
61+ 
62+ 
63+def test_requires_python_is_checked(monkeypatch):
64+ module = checker()
65+ monkeypatch.setattr(module.importlib.metadata, "distribution", lambda name: types.SimpleNamespace(
66+ version="0.8.0", requires=[], metadata={"Requires-Python": ">=99"}))
67+ with pytest.raises(module.DslUnavailable, match="Python"):
68+ module.check_distribution("opkit")
69+ 
70+ 
71+def test_missing_component_skips_publication(tmp_path, monkeypatch, capsys):
72+ module = checker()
73+ def missing():
74+ raise module.DslUnavailable("No module named 'opkit'")
75+ monkeypatch.setattr(module, "check_dependencies", missing)
76+ monkeypatch.setattr(sys, "argv", ["build", "--source", str(tmp_path / "entry.py"),
77+ "--output", str(tmp_path / "package"), "--soc", "ascend950"])
78+ assert module.main() == 77
79+ assert "opkit" in capsys.readouterr().err
80+ assert not (tmp_path / "package").exists()
81+ 
82+ 
83+def test_source_error_is_not_an_environment_skip(tmp_path, monkeypatch):
84+ module = checker()
85+ monkeypatch.setattr(module, "check_dependencies", lambda: None)
86+ def fail(args):
87+ raise ValueError("operator compile error")
88+ monkeypatch.setattr(module, "build", fail)
89+ monkeypatch.setattr(sys, "argv", ["build", "--source", str(tmp_path / "entry.py"),
90+ "--output", str(tmp_path / "package"), "--soc", "ascend950"])
91+ with pytest.raises(ValueError, match="operator compile error"):
92+ module.main()
93+ 
94+ 
95+@pytest.mark.parametrize("mode", ["all", "host", "kernel"])
96+def test_compatibility_skip_preserves_asc_and_removes_old_publication(tmp_path, mode):
97+ scripts = tmp_path / "cmake/scripts"
98+ scripts.mkdir(parents=True)
99+ (scripts / "build_cannbotdsl.py").write_text(
100+ "import sys\nprint('CannBotDSL unavailable: OpBase ABI unavailable', file=sys.stderr)\nsys.exit(77)\n")
101+ (tmp_path / "entry.py").write_text("# DSL collector")
102+ stage = tmp_path / f"build/cannbot_dsl/{mode}/entry"
103+ stage.mkdir(parents=True)
104+ (stage / "package.json").write_text("old publication")
105+ for suffix in (".input.json", ".report.json"):
106+ Path(str(stage) + suffix).write_text("old report")
107+ (tmp_path / "asc.cpp").write_text('extern "C" int AscOnly() { return 42; }\n')
108+ (tmp_path / "CMakeLists.txt").write_text(f'''
109+cmake_minimum_required(VERSION 3.19)
110+project(optional_dsl CXX)
111+include("{ROOT}/cmake/func.cmake")
112+set(OPS_ADV_DIR "{tmp_path}")
113+set(HI_PYTHON "{sys.executable}")
114+set(ASCEND_BINARY_OUT_DIR "${{CMAKE_BINARY_DIR}}/binary")
115+set(ASCEND_CANN_PACKAGE_PATH "{tmp_path}/sdk")
116+set(ASCEND_COMPUTE_UNIT ascend950)
117+set(ENABLE_OPS_HOST {"OFF" if mode == "kernel" else "ON"})
118+set(ENABLE_OPS_KERNEL {"OFF" if mode == "host" else "ON"})
119+set(MODULE_OPTYPE entry)
120+set(MODULE_ACLNNTYPE aclnn_inner)
121+enable_cannbotdsl_kernel(entry "{tmp_path}")
122+get_property(packages GLOBAL PROPERTY CANNBOTDSL_PACKAGES)
123+if(packages OR entry_DSL_AVAILABLE)
124+ message(FATAL_ERROR "Incompatible DSL publication was registered")
125+endif()
126+add_library(asc SHARED asc.cpp)
127+install(TARGETS asc DESTINATION lib)
128+''')
129+ build = tmp_path / "build"
130+ configured = subprocess.run(["cmake", "-S", str(tmp_path), "-B", str(build)], capture_output=True, text=True)
131+ assert configured.returncode == 0, configured.stdout + configured.stderr
132+ assert "DSL not compiled" in " ".join(configured.stderr.split())
133+ assert "OpBase ABI unavailable" in " ".join(configured.stderr.split())
134+ for args in (["--build", str(build)], ["--install", str(build), "--prefix", str(tmp_path / "install")]):
135+ result = subprocess.run(["cmake", *args], capture_output=True, text=True)
136+ assert result.returncode == 0, result.stdout + result.stderr
137+ library = next((tmp_path / "install/lib").glob("*asc.*"))
138+ assert ctypes.CDLL(str(library)).AscOnly() == 42
139+ assert not stage.exists()
140+ assert not Path(str(stage) + ".report.json").exists()
141+ assert not list((tmp_path / "install").rglob("package.json"))
142+ 
143+ 
144+@pytest.mark.parametrize("missing", ["opbase", "runtime", "soc", "none"])
145+def test_missing_optional_runtime_symbols_allow_asc_library_to_load(tmp_path, missing):
146+ if sys.platform != "linux":
147+ pytest.skip("ELF weak symbol and RTLD_NOW contract")
148+ compiler = shutil.which("c++")
149+ if not compiler:
150+ pytest.skip("C++ compiler is required")
151+ (tmp_path / "opdev").mkdir()
152+ (tmp_path / "opdev/op_log.h").write_text(
153+ '#include <cstdio>\n#define OP_LOGE(code, ...) do { std::fprintf(stderr, __VA_ARGS__); } while (0)\n')
154+ # Public interface stand-ins; compile the real Transformer guard and enforce
155+ # eager DSO resolution so an unresolved optional symbol cannot be hidden.
156+ (tmp_path / "runtime.h").write_text(RUNTIME_INTERFACE)
157+ component = '#include "runtime.h"\n'
158+ if missing != "opbase":
159+ component += 'namespace op { aclnnStatus AddDirectInvokeTask(const char*, uint32_t, DirectInvokePrepareFn, DirectInvokeLaunchFn, DirectInvokeDestroyFn, aclOpExecutor*, OpArgContext*) { return 0; } }\n'
160+ if missing != "runtime":
161+ component += 'extern "C" { int aclrtBinaryLoadFromData(){return 0;} int aclrtBinaryGetFunctionByEntry(){return 0;} int aclrtLaunchKernelWithArgsArray(){return 0;} int aclrtLaunchSIMTKernelWithArgsArray(){return 0;} }\n'
162+ component += 'extern "C" const char* aclrtGetSocName() { return "' + ("Ascend910B" if missing == "soc" else "Ascend950") + '"; }\n'
163+ (tmp_path / "components.cpp").write_text(component)
164+ (tmp_path / "entry.cpp").write_text('''namespace op {
165+int InvokeDsl() {
166+ auto status = ops_transformer::CheckCannbotdslRuntime("SampleDsl");
167+ if (status != ACLNN_SUCCESS) { return status; }
168+ return AddDirectInvokeTask("SampleDsl", 0, nullptr, nullptr, nullptr, nullptr, nullptr);
169+}
170+}
171+extern "C" int AscOnly() { return 42; }
172+extern "C" int DslOnly() { return op::InvokeDsl(); }
173+''')
174+ library = tmp_path / "libentry.so"
175+ object_file = tmp_path / "entry.o"
176+ subprocess.run([compiler, "-std=c++17", "-fPIC", "-c", str(tmp_path / "entry.cpp"), "-I", str(tmp_path),
177+ "-include", str(ROOT / "common/include/common/cannbotdsl_compat.h"), "-o", str(object_file)], check=True, capture_output=True)
178+ subprocess.run([compiler, "-std=c++17", "-shared", "-fPIC", str(object_file), str(tmp_path / "components.cpp"),
179+ "-I", str(tmp_path), "-Wl,-z,defs", "-o", str(library)], check=True, capture_output=True)
180+ handle = ctypes.CDLL(str(library), mode=os.RTLD_NOW | os.RTLD_LOCAL)
181+ assert handle.AscOnly() == 42
182+ assert handle.DslOnly() == (0 if missing == "none" else 7)
183+ 
184+ 
185+@pytest.mark.parametrize("has_runtime_api", [None, False, True])
186+def test_abi_link_probe_uses_the_selected_sdk(tmp_path, has_runtime_api):
187+ if sys.platform != "linux":
188+ pytest.skip("Linux SDK link probe")
189+ from test_cannbotdsl_build import compiler_fixture
190+ 
191+ fixture = compiler_fixture(tmp_path)
192+ sdk = tmp_path / "sdk"
193+ include = sdk / "include"
194+ (include / "acl").mkdir(parents=True)
195+ (include / "opdev").mkdir()
196+ (sdk / "lib64").mkdir()
197+ (include / "acl/acl.h").write_text('''#pragma once
198+#include <cstdint>
199+using aclnnStatus = int;
200+struct aclOpExecutor;
201+extern "C" {
202+int aclrtBinaryLoadFromData();
203+int aclrtBinaryGetFunctionByEntry();
204+int aclrtLaunchKernelWithArgsArray();
205+int aclrtLaunchSIMTKernelWithArgsArray();
206+}
207+''')
208+ (include / "opdev/direct_invoke_task.h").write_text('''#pragma once
209+#include <acl/acl.h>
210+namespace op {
211+struct OpArgContext;
212+using DirectInvokePrepareFn = void(*)();
213+using DirectInvokeLaunchFn = void(*)();
214+using DirectInvokeDestroyFn = void(*)();
215+aclnnStatus AddDirectInvokeTask(const char*, uint32_t, DirectInvokePrepareFn,
216+ DirectInvokeLaunchFn, DirectInvokeDestroyFn, aclOpExecutor*, OpArgContext*);
217+}
218+''')
219+ (tmp_path / "opbase.cpp").write_text('''#include <opdev/direct_invoke_task.h>
220+namespace op { aclnnStatus AddDirectInvokeTask(const char*, uint32_t, DirectInvokePrepareFn,
221+ DirectInvokeLaunchFn, DirectInvokeDestroyFn, aclOpExecutor*, OpArgContext*) { return 0; } }
222+''')
223+ (tmp_path / "acl.cpp").write_text(
224+ 'extern "C" { int aclrtBinaryLoadFromData(){return 0;} int aclrtBinaryGetFunctionByEntry(){return 0;} '
225+ 'int aclrtLaunchKernelWithArgsArray(){return 0;} '
226+ + ('int aclrtLaunchSIMTKernelWithArgsArray(){return 0;}' if has_runtime_api else '') + '}')
227+ for source, output in (("opbase.cpp", "libnnopbase.so"), ("acl.cpp", "libascendcl.so")):
228+ subprocess.run(["c++", "-shared", "-fPIC", "-I", str(include), str(tmp_path / source),
229+ "-o", str(sdk / "lib64" / output)], check=True, capture_output=True)
230+ if has_runtime_api is None:
231+ (sdk / "lib64/libascendcl.so").unlink()
232+ (tmp_path / "entry.py").write_text("# collector")
233+ (tmp_path / "CMakeLists.txt").write_text(f'''
234+cmake_minimum_required(VERSION 3.19)
235+project(sdk_abi_probe CXX)
236+include("{ROOT}/cmake/func.cmake")
237+{fixture}
238+set(ASCEND_CANN_PACKAGE_PATH "{sdk}")
239+set(NNOPBASE_OPDEV_INC_DIR "{include}")
240+set(ENABLE_OPS_HOST ON)
241+set(ENABLE_OPS_KERNEL ON)
242+set(ASCEND_COMPUTE_UNIT ascend950)
243+add_library(nnopbase SHARED IMPORTED)
244+set_target_properties(nnopbase PROPERTIES IMPORTED_LOCATION "{sdk}/lib64/libnnopbase.so")
245+enable_cannbotdsl_kernel(entry "{tmp_path}")
246+get_property(packages GLOBAL PROPERTY CANNBOTDSL_PACKAGES)
247+file(WRITE "${{CMAKE_BINARY_DIR}}/packages.txt" "${{packages}}")
248+''')
249+ result = subprocess.run(["cmake", "-S", str(tmp_path), "-B", str(tmp_path / "build")], capture_output=True, text=True)
250+ assert result.returncode == 0, result.stdout + result.stderr
251+ assert bool((tmp_path / "build/packages.txt").read_text()) is bool(has_runtime_api)
252+ if not has_runtime_api:
253+ assert "ABI does not match" in " ".join(result.stderr.split())
254+ 
255+ 
256+def test_unavailable_l0_is_generated_without_operator_ifdefs(tmp_path):
257+ scripts = tmp_path / "cmake/scripts"
258+ scripts.mkdir(parents=True)
259+ (scripts / "build_cannbotdsl.py").write_text("import sys\nprint('DSL unavailable', file=sys.stderr)\nsys.exit(77)\n")
260+ (tmp_path / "entry.py").write_text("# collector")
261+ (tmp_path / "aclnn").mkdir()
262+ (tmp_path / "opdev").mkdir()
263+ (tmp_path / "aclnn/aclnn_base.h").write_text("using aclnnStatus=int; constexpr int ACLNN_ERR_INNER=7;\n")
264+ (tmp_path / "opdev/op_log.h").write_text("#define OP_LOGE(...) ((void)0)\n")
265+ (tmp_path / "api.cpp").write_text('#include "sample_dsl.h"\nextern "C" int CallDsl(){return l0op::SampleDsl(1, nullptr);}\n')
266+ (tmp_path / "CMakeLists.txt").write_text(f'''
267+cmake_minimum_required(VERSION 3.19)
268+project(unavailable_l0 CXX)
269+include("{ROOT}/cmake/func.cmake")
270+set(OPS_ADV_DIR "{tmp_path}")
271+set(HI_PYTHON "{sys.executable}")
272+set(ASCEND_BINARY_OUT_DIR "${{CMAKE_BINARY_DIR}}/binary")
273+set(ASCEND_COMPUTE_UNIT ascend950)
274+set(ENABLE_OPS_HOST ON)
275+set(MODULE_OPTYPE entry)
276+set(MODULE_ACLNNTYPE aclnn_inner)
277+set(OPHOST_NAME host)
278+add_library(host_opapi_obj SHARED api.cpp)
279+target_include_directories(host_opapi_obj PRIVATE "{tmp_path}")
280+enable_cannbotdsl_kernel(entry "{tmp_path}" L0_NAME SampleDsl)
281+''')
282+ build = tmp_path / "build"
283+ for args in (["-S", str(tmp_path), "-B", str(build)], ["--build", str(build)]):
284+ result = subprocess.run(["cmake", *args], capture_output=True, text=True)
285+ assert result.returncode == 0, result.stdout + result.stderr
286+ library = next(build.glob("*host_opapi_obj.*"))
287+ assert ctypes.CDLL(str(library)).CallDsl() == 7
288+ 
289+ 
290+def test_generated_l0_checks_runtime_explicitly(tmp_path):
291+ if sys.platform != "linux":
292+ pytest.skip("Linux optional runtime linkage")
293+ from test_cannbotdsl_build import compiler_fixture
294+ 
295+ fixture = compiler_fixture(tmp_path)
296+ (tmp_path / "opdev").mkdir()
297+ (tmp_path / "opdev/op_log.h").write_text("#define OP_LOGE(...) ((void)0)\n")
298+ builder = tmp_path / "cmake/scripts/build_cannbotdsl.py"
299+ # Stand-ins publish a real static runtime target and generated L0 source.
300+ # The production CMake then creates and compiles its guarded build copy.
301+ with builder.open("a") as stream:
302+ stream.write("\n(root / 'runtime/runtime.h').write_text(" + repr(RUNTIME_INTERFACE) + ")\n")
303+ stream.write("(root / 'runtime/stub.cpp').write_text('extern \\\"C\\\" const char* aclrtGetSocName(){return \\\"Ascend950\\\";}\\n')\n".replace('\\\\"', '\\"'))
304+ stream.write("(root / 'runtime/CMakeLists.txt').write_text('add_library(opkit_runtime STATIC stub.cpp)\\nadd_library(opkit::runtime ALIAS opkit_runtime)\\ntarget_include_directories(opkit_runtime PUBLIC \"${CMAKE_CURRENT_SOURCE_DIR}\" \"" + str(tmp_path) + "\")\\n')\n")
305+ l0 = ('#define ADD_TO_LAUNCHER_LIST_CANNBOTDSL(...) 0\n'
306+ 'namespace l0op { int entry() { return ADD_TO_LAUNCHER_LIST_CANNBOTDSL(entry); } }\n')
307+ stream.write("(root / op / 'op_api' / (op + '.cpp')).write_text(" + repr(l0) + ")\n")
308+ stream.write("module = root / op / 'CMakeLists.txt'\nmodule.write_text(module.read_text().replace(str(root / op) + '/', './'))\n")
309+ (tmp_path / "entry.py").write_text("# collector")
310+ (tmp_path / "api.cpp").write_text('namespace l0op { int entry(); }\nextern "C" int CallDsl(){return l0op::entry();}\n')
311+ (tmp_path / "CMakeLists.txt").write_text(f'''
312+cmake_minimum_required(VERSION 3.19)
313+project(explicit_guard CXX)
314+include("{ROOT}/cmake/func.cmake")
315+{fixture}
316+set(ASCEND_COMPUTE_UNIT ascend950)
317+set(ENABLE_OPS_HOST ON)
318+set(ENABLE_OPS_KERNEL ON)
319+enable_cannbotdsl_kernel(entry "{tmp_path}")
320+get_property(CANNBOTDSL_PACKAGES GLOBAL PROPERTY CANNBOTDSL_PACKAGES)
321+include("{ROOT}/cmake/opbuild.cmake")
322+cannbotdsl_add_modules(headers)
323+add_library(api SHARED api.cpp)
324+cannbotdsl_attach_to_opapi(api)
325+''')
326+ build = tmp_path / "build"
327+ for args in (["-S", str(tmp_path), "-B", str(build)], ["--build", str(build)]):
328+ result = subprocess.run(["cmake", *args], capture_output=True, text=True)
329+ assert result.returncode == 0, result.stdout + result.stderr
330+ assert ctypes.CDLL(str(build / "libapi.so"), mode=os.RTLD_NOW).CallDsl() == 7
331+ checked = build / "cannbot_dsl/entry/checked_l0.cpp"
332+ assert "CheckCannbotdslRuntime" in checked.read_text()
333+ original = build / "cannbot_dsl/all/entry/entry/op_api/entry.cpp"
334+ assert "CheckCannbotdslRuntime" not in original.read_text()
335+ 
336+ 
337+@pytest.mark.parametrize("selected,skipped,expected", [
338+ ("regular", "", False), ("add_dsl", "add_dsl", True),
339+ ("regular;add_dsl", "add_dsl", False),
340+])
341+def test_only_unavailable_dsl_selection_skips_package(tmp_path, selected, skipped, expected):
342+ cmake = (ROOT / "cmake/custom_build.cmake").read_text()
343+ start = cmake.index("get_property(_dsl_skipped GLOBAL PROPERTY CANNBOTDSL_SKIPPED_ONLY_OPS)")
344+ end = cmake.index("# 已生成的 DSL 包", start)
345+ script = tmp_path / "skip.cmake"
346+ script.write_text(f'set(CMAKE_BINARY_DIR "{tmp_path}")\nset(ASCEND_OP_NAME "{selected}")\n'
347+ f'set_property(GLOBAL PROPERTY CANNBOTDSL_SKIPPED_ONLY_OPS "{skipped}")\n'
348+ + cmake[start:end])
349+ result = subprocess.run(["cmake", "-P", str(script)], capture_output=True, text=True)
350+ assert result.returncode == 0, result.stdout + result.stderr
351+ assert (tmp_path / "no_ops.flag").exists() is expected