已合并
feat: 支持 Python 自定义算子原型与实现桥接注册 #4320
lfz2812创建于 16 天前
feat: 支持 Python 自定义算子原型与实现桥接注册 #4320
已合并
lfz2812创建于 16 天前
31 个文件变更+3045-444
Mapi/python/ge/ge/custom_op/_bridge.py+13-1
@@ -21,7 +21,11 @@ from typing import Dict, Optional
21from ._ir_types import InputType, OutputType21from ._ir_types import InputType, OutputType
22from ._signature import _get_runtime_attr_spec, _validate_args_signature22from ._signature import _get_runtime_attr_spec, _validate_args_signature
23from .base import EagerOpExecutionContext23from .base import EagerOpExecutionContext
24-from .bootstrap import get_registered_op_impls, load_custom_op_plugins24+from .bootstrap import (
25+ get_registered_op_impls,
26+ get_registered_op_protos,
27+ load_custom_op_plugins,
28+)
25from .context import _declare_launch_args_ctx_scope, _execute_ctx_scope29from .context import _declare_launch_args_ctx_scope, _execute_ctx_scope
26from .registry import (30from .registry import (
27 INTERFACE_ANNOTATED_ARGS,31 INTERFACE_ANNOTATED_ARGS,
@@ -46,6 +50,14 @@ def load_and_get_op_impl_descriptors() -> list:
46 return get_registered_op_impls()50 return get_registered_op_impls()
47 51 
48 52 
53+def load_and_get_op_descriptors() -> dict:
54+ load_custom_op_plugins()
55+ return {
56+ "protos": get_registered_op_protos(),
57+ "impls": get_registered_op_impls(),
58+ }
59+ 
60+ 
49def _get_holder(instance_id: str) -> _OpImplHolder:61def _get_holder(instance_id: str) -> _OpImplHolder:
50 with _HOLDER_LOCK:62 with _HOLDER_LOCK:
51 holder = _OP_IMPL_HOLDERS.get(instance_id)63 holder = _OP_IMPL_HOLDERS.get(instance_id)
Mapi/session/client/ge_api.cc+2-39
@@ -41,6 +41,7 @@
41#include "register/op_lib_register_impl.h"41#include "register/op_lib_register_impl.h"
42#include "graph/utils/tensor_adapter.h"42#include "graph/utils/tensor_adapter.h"
43#include "graph/utils/type_utils.h"43#include "graph/utils/type_utils.h"
44+#include "graph/utils/ir_definitions_query.h"
44#include "api/gelib/gelib.h"45#include "api/gelib/gelib.h"
45#include "api/aclgrph/option_utils.h"46#include "api/aclgrph/option_utils.h"
46#include "proto/ge_api.pb.h"47#include "proto/ge_api.pb.h"
@@ -1533,44 +1534,6 @@ ge::Status GeSessionGraphDebugJSONPrint(ge::Session &session, uint32_t graph_id,
1533ge::Status GetRegisteredIrDef(const char *op_type, std::vector<std::pair<ge::AscendString, ge::AscendString>> &inputs,1534ge::Status GetRegisteredIrDef(const char *op_type, std::vector<std::pair<ge::AscendString, ge::AscendString>> &inputs,
1534 std::vector<std::pair<ge::AscendString, ge::AscendString>> &outputs,1535 std::vector<std::pair<ge::AscendString, ge::AscendString>> &outputs,
1535 std::vector<std::pair<ge::AscendString, ge::AscendString>> &attrs) {1536 std::vector<std::pair<ge::AscendString, ge::AscendString>> &attrs) {
1536- GE_ASSERT_NOTNULL(op_type);1537+ return GetRegisteredIrDefFromGraph(op_type, inputs, outputs, attrs);
1537- const auto op = ge::OperatorFactory::CreateOperator("_", op_type);
1538- GE_WARN_ASSERT(!op.IsEmpty(), "No operator found for type: %s", op_type);
1539- const auto desc = ge::OpDescUtils::GetOpDescFromOperator(op);
1540- 
1541- static const auto kInputTypeString = []() {
1542- std::map<ge::IrInputType, ge::AscendString> typeStr;
1543- typeStr[ge::IrInputType::kIrInputRequired] = "required";
1544- typeStr[ge::IrInputType::kIrInputOptional] = "optional";
1545- typeStr[ge::IrInputType::kIrInputDynamic] = "dynamic";
1546- return typeStr;
1547- }();
1548- 
1549- static const auto kOutputTypeString = []() {
1550- std::map<ge::IrOutputType, ge::AscendString> typeStr;
1551- typeStr[ge::IrOutputType::kIrOutputRequired] = "required";
1552- typeStr[ge::IrOutputType::kIrOutputDynamic] = "dynamic";
1553- return typeStr;
1554- }();
1555- 
1556- GE_ASSERT_NOTNULL(desc, "Failed to get OpDesc from operator: %s", op_type);
1557- for (const auto &name2type : desc->GetIrInputs()) {
1558- auto iter = kInputTypeString.find(name2type.second);
1559- GE_ASSERT(iter != kInputTypeString.end(), "Unknown input type: %d for operator: %s", name2type.second, op_type);
1560- inputs.emplace_back(ConvertToAscendString(name2type.first), iter->second);
1561- }
1562- for (const auto &name2type : desc->GetIrOutputs()) {
1563- auto iter = kOutputTypeString.find(name2type.second);
1564- GE_ASSERT(iter != kOutputTypeString.end(), "Unknown output type: %d for operator: %s", name2type.second, op_type);
1565- outputs.emplace_back(ConvertToAscendString(name2type.first), iter->second);
1566- }
1567- 
1568- std::map<ge::AscendString, ge::AscendString> attrs_and_types;
1569- GE_ASSERT_GRAPH_SUCCESS(op.GetAllIrAttrNamesAndTypes(attrs_and_types),
1570- "Failed to get attr names and types for operator: %s", op_type);
1571- for (const auto &attr : desc->GetIrAttrNames()) {
1572- attrs.emplace_back(ConvertToAscendString(attr), attrs_and_types[ConvertToAscendString(attr)]);
1573- }
1574- return ge::SUCCESS;
1575}1538}
1576}1539}
Mapi/session/client/ge_api_v2.cc+2-39
@@ -38,6 +38,7 @@
38#include "graph/opsproto_manager.h"38#include "graph/opsproto_manager.h"
39#include "base/registry/opp_package_utils.h"39#include "base/registry/opp_package_utils.h"
40#include "graph/utils/type_utils.h"40#include "graph/utils/type_utils.h"
41+#include "graph/utils/ir_definitions_query.h"
41#include "api/gelib/gelib.h"42#include "api/gelib/gelib.h"
42#include "api/aclgrph/option_utils.h"43#include "api/aclgrph/option_utils.h"
43#include "graph/fusion/pass/pass_plugin_loader.h"44#include "graph/fusion/pass/pass_plugin_loader.h"
@@ -1163,44 +1164,6 @@ bool IsIrRepSupport(const char *rep) {
1163ge::Status GetRegisteredIrDef(const char *op_type, std::vector<std::pair<ge::AscendString, ge::AscendString>> &inputs,1164ge::Status GetRegisteredIrDef(const char *op_type, std::vector<std::pair<ge::AscendString, ge::AscendString>> &inputs,
1164 std::vector<std::pair<ge::AscendString, ge::AscendString>> &outputs,1165 std::vector<std::pair<ge::AscendString, ge::AscendString>> &outputs,
1165 std::vector<std::pair<ge::AscendString, ge::AscendString>> &attrs) {1166 std::vector<std::pair<ge::AscendString, ge::AscendString>> &attrs) {
1166- GE_ASSERT_NOTNULL(op_type);1167+ return GetRegisteredIrDefFromGraph(op_type, inputs, outputs, attrs);
1167- const auto op = ge::OperatorFactory::CreateOperator("_", op_type);
1168- GE_WARN_ASSERT(!op.IsEmpty(), "No operator found for type: %s", op_type);
1169- const auto desc = ge::OpDescUtils::GetOpDescFromOperator(op);
1170- 
1171- static const auto kInputTypeString = []() {
1172- std::map<ge::IrInputType, ge::AscendString> typeStr;
1173- typeStr[ge::IrInputType::kIrInputRequired] = "required";
1174- typeStr[ge::IrInputType::kIrInputOptional] = "optional";
1175- typeStr[ge::IrInputType::kIrInputDynamic] = "dynamic";
1176- return typeStr;
1177- }();
1178- 
1179- static const auto kOutputTypeString = []() {
1180- std::map<ge::IrOutputType, ge::AscendString> typeStr;
1181- typeStr[ge::IrOutputType::kIrOutputRequired] = "required";
1182- typeStr[ge::IrOutputType::kIrOutputDynamic] = "dynamic";
1183- return typeStr;
1184- }();
1185- 
1186- GE_ASSERT_NOTNULL(desc, "Failed to get OpDesc from operator: %s", op_type);
1187- for (const auto &name2type : desc->GetIrInputs()) {
1188- auto iter = kInputTypeString.find(name2type.second);
1189- GE_ASSERT(iter != kInputTypeString.end(), "Unknown input type: %d for operator: %s", name2type.second, op_type);
1190- inputs.emplace_back(ConvertToAscendString(name2type.first), iter->second);
1191- }
1192- for (const auto &name2type : desc->GetIrOutputs()) {
1193- auto iter = kOutputTypeString.find(name2type.second);
1194- GE_ASSERT(iter != kOutputTypeString.end(), "Unknown output type: %d for operator: %s", name2type.second, op_type);
1195- outputs.emplace_back(ConvertToAscendString(name2type.first), iter->second);
1196- }
1197- 
1198- std::map<ge::AscendString, ge::AscendString> attrs_and_types;
1199- GE_ASSERT_GRAPH_SUCCESS(op.GetAllIrAttrNamesAndTypes(attrs_and_types),
1200- "Failed to get attr names and types for operator: %s", op_type);
1201- for (const auto &attr : desc->GetIrAttrNames()) {
1202- attrs.emplace_back(ConvertToAscendString(attr), attrs_and_types[ConvertToAscendString(attr)]);
1203- }
1204- return ge::SUCCESS;
1205}1168}
1206}1169}
Mdocs/en/design/modules/ge_python/ge_python.md+9-1
@@ -821,7 +821,7 @@ Note: `EagerOpExecutionContext` and `AnnotatedArgsContext` are provided by `_ge_
821 821 
822#### Module Positioning822#### Module Positioning
823 823 
824-The long-term goal of the Python custom operator is to support users in describing custom operator prototypes and implementing custom operator capabilities in Python. Callable `execute` and `declare_launch_args` methods are now reflected from the implementation class to detect execution capability and declarative static-graph address-refresh capability, respectively. User classes are not required to inherit from `BaseCustomOp` or `EagerExecuteOp`, while existing inheritance-based implementations remain compatible. The execution entry supports both the legacy `execute(ctx)` form and a schema-bound form whose inputs and attributes are bound from canonical IR in declaration order.824+The long-term goal of the Python custom operator is to support users in describing custom operator prototypes and implementing custom operator capabilities in Python. Callable `execute` and `declare_launch_args` methods are now reflected from the implementation class to detect execution capability and declarative static-graph address-refresh capability, respectively. User classes are not required to inherit from `BaseCustomOp` or `EagerExecuteOp`, while existing inheritance-based implementations remain compatible. The execution entry supports both the legacy `execute(ctx)` form and a schema-bound form whose inputs and attributes are bound from canonical IR in declaration order. The current stage also registers Python prototypes with `OperatorFactory`, but does not invoke Python `infer_meta` or provide compile-time or RT2 Meta inference.
825 825 
826#### Runtime Native Artifact Selection826#### Runtime Native Artifact Selection
827 827 
@@ -965,6 +965,14 @@ Within one AnnotatedArgs task-plan lifecycle, `declare_launch_args` is invoked e
965- `bootstrap.py` scans paths and dynamically loads Python modules965- `bootstrap.py` scans paths and dynamically loads Python modules
966- Supports single `.py` files, `.py` files in plain directories, and Python packages containing `__init__.py`966- Supports single `.py` files, `.py` files in plain directories, and Python packages containing `__init__.py`
967 967 
968+**Prototype registration**:
969+ 
970+- `register_op` collects required, optional, and dynamic inputs, 12 attribute types, required and dynamic outputs, and `mutates_args`.
971+- In one loading transaction, the bridge first registers Python prototype creators and collects the effective canonical IR, then registers implementation runtime entries and Adapter creators.
972+- Python proto-only, Python proto with implementation, C++ proto with Python implementation, and prototype-free legacy implementations are supported. A schema-bound implementation without a prototype is rejected.
973+- A Python prototype may replace a built-in prototype with the same name. Registration fails when a loaded C++ or Python custom operator already owns that name.
974+- A failed batch is rolled back in reverse order: Adapter creators, implementation runtime entries, and prototype creators. Unloading removes only objects owned by the current loader.
975+ 
968**Usage sample**:976**Usage sample**:
969 977 
970```python978```python
Mdocs/en/design/modules/ge_python/ge_python_custom_op_design.md+22-14
@@ -19,7 +19,7 @@ The long-term goal of Python custom operators is to let users describe custom op
19- Python users implement the compile-time `AnnotatedArgsOp` callback through `declare_launch_args` and declare kernel launch arguments with `AnnotatedArgsContext`, `AnnotatedKernelArgs`, and `AnnotatedKernelLaunchInfo`.19- Python users implement the compile-time `AnnotatedArgsOp` callback through `declare_launch_args` and declare kernel launch arguments with `AnnotatedArgsContext`, `AnnotatedKernelArgs`, and `AnnotatedKernelLaunchInfo`.
20- `ge.runtime` provides runtime data structures required by the context for return values or input parameters, such as `Tensor`, `StorageShape`, `StorageFormat`, `Shape`, and `TensorPlacement`.20- `ge.runtime` provides runtime data structures required by the context for return values or input parameters, such as `Tensor`, `StorageShape`, `StorageFormat`, `Shape`, and `TensorPlacement`.
21 21 
22-V2 plans to add Python prototype and Meta inference capabilities on top of the V1 execution path.22+V2 extends the V1 execution path with Python prototype and Meta inference capabilities. The current stage provides Python prototype creators, Adapter registration transactions, and ownership management, but does not yet invoke Python `infer_meta`.
23 23 
24In V2, the Python function decorated with `register_op` performs Meta inference and is referred to as `infer_meta` throughout this document; the function itself does not have to be named `infer_meta`. Following the operator prototype, it receives input `TensorDesc` objects, including optional and dynamic inputs, together with attribute values, and returns one or more `TensorDesc` objects describing output shape and data type. It neither reads input Tensor data nor executes the operator kernel.24In V2, the Python function decorated with `register_op` performs Meta inference and is referred to as `infer_meta` throughout this document; the function itself does not have to be named `infer_meta`. Following the operator prototype, it receives input `TensorDesc` objects, including optional and dynamic inputs, together with attribute values, and returns one or more `TensorDesc` objects describing output shape and data type. It neither reads input Tensor data nor executes the operator kernel.
25 25 
@@ -71,6 +71,7 @@ The actual module boundaries are as follows:
71| Runtime loader | `runtime/custom_op/custom_op_loader.cc` | Unified loading of C++ custom ops and Python custom ops |71| Runtime loader | `runtime/custom_op/custom_op_loader.cc` | Unified loading of C++ custom ops and Python custom ops |
72| Bridge loader | `runtime/custom_op/python_custom_op_bridge_loader.cc` | Artifact selection, loading `libge_python_custom_op_bridge.so`, and creator registration |72| Bridge loader | `runtime/custom_op/python_custom_op_bridge_loader.cc` | Artifact selection, loading `libge_python_custom_op_bridge.so`, and creator registration |
73| Pybind bridge | `runtime/custom_op/python_custom_op_pybind_bridge.cc` | Importing the Python bridge module, creating holders, and calling back `execute` / `declare_launch_args` |73| Pybind bridge | `runtime/custom_op/python_custom_op_pybind_bridge.cc` | Importing the Python bridge module, creating holders, and calling back `execute` / `declare_launch_args` |
74+| Proto runtime | `runtime/custom_op/python_custom_op_proto.*` | Deep-copying C POD prototypes and registering `OperatorFactory` creators |
74| Adapter | `runtime/custom_op/python_custom_op_adapter.*` | Serving as a C++ `BaseCustomOp` instance to access the existing runtime |75| Adapter | `runtime/custom_op/python_custom_op_adapter.*` | Serving as a C++ `BaseCustomOp` instance to access the existing runtime |
75| Capability helper | `inc/graph_metadef/graph/custom_op/` | `CustomOpCapability` and `CustomOpCast<T>` |76| Capability helper | `inc/graph_metadef/graph/custom_op/` | `CustomOpCapability` and `CustomOpCast<T>` |
76 77 
@@ -80,6 +81,7 @@ V1 functions include:
80 81 
81- `@register_op_impl(op_type=...)` registers a Python custom operator implementation.82- `@register_op_impl(op_type=...)` registers a Python custom operator implementation.
82- `@register_op(op_type=..., mutates_args=...)` collects a custom operator prototype from a Python function signature.83- `@register_op(op_type=..., mutates_args=...)` collects a custom operator prototype from a Python function signature.
84+- The bridge registers Python prototypes as `OperatorFactory` creators and collects canonical IR from the effective creators. It does not invoke `infer_meta` at this stage.
83- `register_op_impl` reflects callable `execute` and `declare_launch_args` methods and declares the corresponding capabilities without requiring inheritance from `BaseCustomOp`, `EagerExecuteOp`, or `AnnotatedArgsOp`.85- `register_op_impl` reflects callable `execute` and `declare_launch_args` methods and declares the corresponding capabilities without requiring inheritance from `BaseCustomOp`, `EagerExecuteOp`, or `AnnotatedArgsOp`.
84- The legacy `execute(self, ctx)` form directly receives an `EagerOpExecutionContext`; the schema-bound form receives inputs and attributes assembled from canonical IR.86- The legacy `execute(self, ctx)` form directly receives an `EagerOpExecutionContext`; the schema-bound form receives inputs and attributes assembled from canonical IR.
85- `EagerOpExecutionContext` supports input and output tensor queries, dynamic input instance counts, runtime attribute access, output and workspace allocation, and stream retrieval.87- `EagerOpExecutionContext` supports input and output tensor queries, dynamic input instance counts, runtime attribute access, output and workspace allocation, and stream retrieval.
@@ -96,6 +98,8 @@ V1 functions include:
96- The return value of the Python `execute` method is not used as a status code. A normal return indicates success, and an exception indicates failure.98- The return value of the Python `execute` method is not used as a status code. A normal return indicates success, and an exception indicates failure.
97- The Python custom op currently declares `EagerExecuteOp` and `AnnotatedArgsOp` capabilities. Other C++ capability interfaces are retained as overrides in the adapter but are treated as unsupported.99- The Python custom op currently declares `EagerExecuteOp` and `AnnotatedArgsOp` capabilities. Other C++ capability interfaces are retained as overrides in the adapter but are treated as unsupported.
98- The schema-bound form depends on canonical IR from the existing operator prototype. While loading descriptors, the bridge collects canonical IR and calls `validate_op_impl_descriptor` before holder creation or any business callback to validate schema-bound signatures once. `execute` validates IR inputs and attributes, excludes IR outputs from its parameters, and does not constrain its return annotation or return value. `declare_launch_args` validates inputs, outputs, and attributes and requires a `None` return annotation. Runtime callbacks only assemble arguments and invoke business methods; they do not validate signatures, and validation state does not enter the holder lifecycle.100- The schema-bound form depends on canonical IR from the existing operator prototype. While loading descriptors, the bridge collects canonical IR and calls `validate_op_impl_descriptor` before holder creation or any business callback to validate schema-bound signatures once. `execute` validates IR inputs and attributes, excludes IR outputs from its parameters, and does not constrain its return annotation or return value. `declare_launch_args` validates inputs, outputs, and attributes and requires a `None` return annotation. Runtime callbacks only assemble arguments and invoke business methods; they do not validate signatures, and validation state does not enter the holder lifecycle.
101+- Cross-SO prototype and Adapter descriptors are synchronously borrowed C POD views. Runtime callbacks must finish validation and deep copying before returning.
102+- A Python prototype may replace a built-in prototype. If `CustomOpFactory` already contains a C++ or Python custom operator with the same name, registration reports a custom-operator conflict.
99- A schema-bound callback obtains the current context through `get_execute_ctx()`. The binding is valid only in the dynamic scope of that callback.103- A schema-bound callback obtains the current context through `get_execute_ctx()`. The binding is valid only in the dynamic scope of that callback.
100- The Python custom op native/bridge is related to the Python ABI at build time. Cross-Python minor version compatibility is not guaranteed.104- The Python custom op native/bridge is related to the Python ABI at build time. Cross-Python minor version compatibility is not guaranteed.
101- The bridge C ABI remains v1. The `execute` and `declare_launch_args` callbacks pass only the holder and corresponding context. The bridge queries canonical IR through the public run-package API instead of passing a private ABI projection.105- The bridge C ABI remains v1. The `execute` and `declare_launch_args` callbacks pass only the holder and corresponding context. The bridge queries canonical IR through the public run-package API instead of passing a private ABI projection.
@@ -325,14 +329,16 @@ Python custom op loading is managed by `runtime/custom_op` to avoid direct Pytho
325- `NeedLoadPythonCustomOps()` returns true only when Python files or packages are found under `ASCEND_CUSTOM_OPP_PATH`.329- `NeedLoadPythonCustomOps()` returns true only when Python files or packages are found under `ASCEND_CUSTOM_OPP_PATH`.
326- `LoadPythonCustomOps()` resolves the loaded Python runtime key and selects the bridge/native artifact under `custom_op/python_custom_op_artifacts/<python_tag>-<platform>`.330- `LoadPythonCustomOps()` resolves the loaded Python runtime key and selects the bridge/native artifact under `custom_op/python_custom_op_artifacts/<python_tag>-<platform>`.
327- `libge_python_custom_op_bridge.so` exposes C ABI v1 through `GeGetPythonCustomOpBridgeApi()`.331- `libge_python_custom_op_bridge.so` exposes C ABI v1 through `GeGetPythonCustomOpBridgeApi()`.
328-- The bridge imports `_ge_custom_op_native` and `ge.custom_op._bridge`, registers descriptors, and creates Python holders for each adapter.332+- The bridge imports `_ge_custom_op_native` and `ge.custom_op._bridge` and obtains one prototype/implementation snapshot. It registers all prototypes first, then validates and registers Adapters.
333+- `CustomOpLoader::LoadCustomOps()` records whether Python custom ops are loaded, so repeated lifecycle load requests return success without invoking the bridge registration entry again. The dynamic `LoadPythonCustomOpsIfNeeded()` path intentionally does not use this flag, allowing newly added Python custom op paths to be discovered during runtime. The lower-level `LoadPythonCustomOps()` function performs one bridge registration attempt; callers are responsible for invoking `UnloadPythonCustomOps()` after a failed attempt so that partial registrations are cleaned up.
334+- `UnloadPythonCustomOps()` removes registered Adapter creators, clears the Python custom-op runtime registry in one operation, and then removes registered proto creators. It does not perform per-entry runtime unregistration or maintain pending-cleanup state in the bridge loader.
329- `UnloadCustomOps()` uses an `active_users_` reference count to manage the lifecycle: each `LoadCustomOps()` increments the count by 1, each `UnloadCustomOps()` decrements it by 1, and Python custom ops are only unloaded (holders/registry cleaned up and bridge closed) when the count reaches zero. `ShutdownCustomOpsForProcess()` is retained as a compatibility wrapper that internally calls `UnloadCustomOps()`.335- `UnloadCustomOps()` uses an `active_users_` reference count to manage the lifecycle: each `LoadCustomOps()` increments the count by 1, each `UnloadCustomOps()` decrements it by 1, and Python custom ops are only unloaded (holders/registry cleaned up and bridge closed) when the count reaches zero. `ShutdownCustomOpsForProcess()` is retained as a compatibility wrapper that internally calls `UnloadCustomOps()`.
330 336 
331**Output**337**Output**
332 338 
333-- Python descriptors are registered as `CustomOpFactory` creators.339+- Python prototypes are registered as `OperatorFactory` creators, and Python implementations are registered as `CustomOpFactory` creators.
334- When the adapter is destructed, the Python holder is destroyed and the runtime registry entry is released.340- When the adapter is destructed, the Python holder is destroyed and the runtime registry entry is released.
335-- The runtime registry does not allow unregistration while active adapters exist.341+- Individual runtime registry unregistration remains guarded by active adapters; process unload removes Adapter creators first and clears the registry in bulk.
336 342 
337### 3.3 Non-Functional Requirements343### 3.3 Non-Functional Requirements
338 344 
@@ -450,21 +456,21 @@ The Python-version-sensitive bridge uses the official `GetRegisteredIrDef` signa
450 456 
451Runtime symbol resolution is used because `libge_runner.so`/`libge_runner_v2.so` already depend on `custom_op_runtime`, while the bridge is dynamically loaded by `custom_op_runtime`; directly linking the bridge back to a runner would create a reverse SO dependency. The function signature and data types are still governed by official public run-package headers, with only symbol binding deferred until after the bridge is loaded.457Runtime symbol resolution is used because `libge_runner.so`/`libge_runner_v2.so` already depend on `custom_op_runtime`, while the bridge is dynamically loaded by `custom_op_runtime`; directly linking the bridge back to a runner would create a reverse SO dependency. The function signature and data types are still governed by official public run-package headers, with only symbol binding deferred until after the bridge is loaded.
452 458 
453-#### PythonCustomOpDescriptor459+#### PythonCustomOpProto and PythonCustomOpAdapterDescriptor
454 460 
455-The C++ runtime uses `PythonCustomOpDescriptor`:461+The C++ runtime stores an owning prototype and an Adapter descriptor separately; the Adapter descriptor stores only the implementation key:
456 462 
457```cpp463```cpp
458-struct PythonCustomOpDescriptor {464+struct PythonCustomOpAdapterDescriptor {
459- std::string descriptor_key;
460 std::string op_type;465 std::string op_type;
466+ std::string impl_descriptor_key;
461 CustomOpCapabilityMask capabilities{0U};467 CustomOpCapabilityMask capabilities{0U};
462};468};
463```469```
464 470 
465-#### PythonCustomOpCallbacks471+#### PythonCustomOpAdapterCallbacks
466 472 
467-The bridge registers create/destroy/execute/declare_launch_args callbacks to the runtime. `IsValid()` accepts `kEagerExecute`, `kAnnotatedArgs`, or both; it requires create/destroy and verifies execute/declare_launch_args according to the capability mask.473+The bridge registers create/destroy/execute/declare_launch_args callbacks to the runtime. Schema-bound signature validation is not a C++ callback; the bridge calls `validate_op_impl_descriptor` internally before registering an Adapter. `IsValid()` accepts `kEagerExecute`, `kAnnotatedArgs`, or both; it requires create/destroy and verifies execute/declare_launch_args according to the capability mask.
468 474 
469#### BorrowedEagerOpExecutionContext475#### BorrowedEagerOpExecutionContext
470 476 
@@ -481,6 +487,7 @@ The native binding holds a `gert::AnnotatedArgsContext *` and an independent val
481- **Capability reflection**: The registry applies `getattr` and `callable` checks to the implementation class, maps `execute` to `eager_execute`, and maps `declare_launch_args` to `annotated_args`. The inheritance hierarchy of the Python user class does not participate in capability detection.487- **Capability reflection**: The registry applies `getattr` and `callable` checks to the implementation class, maps `execute` to `eager_execute`, and maps `declare_launch_args` to `annotated_args`. The inheritance hierarchy of the Python user class does not participate in capability detection.
482- **Capability filtering**: `CustomOpCast<T>()` first identifies `CustomOpCapabilityProvider` and then checks the bitmask to determine whether the target interface is supported.488- **Capability filtering**: `CustomOpCast<T>()` first identifies `CustomOpCapabilityProvider` and then checks the bitmask to determine whether the target interface is supported.
483- **IR argument assembly**: The bridge queries canonical IR through the public run-package API during descriptor loading for signature validation, then queries it again at holder creation and owns the resulting runtime snapshot. Callbacks read required, optional, and dynamic inputs/outputs and typed runtime attributes in that IR order, then construct positional and keyword arguments.489- **IR argument assembly**: The bridge queries canonical IR through the public run-package API during descriptor loading for signature validation, then queries it again at holder creation and owns the resulting runtime snapshot. Callbacks read required, optional, and dynamic inputs/outputs and typed runtime attributes in that IR order, then construct positional and keyword arguments.
490+- **Registration transaction**: The runtime synchronously deep-copies prototype C POD data and registers its creator, collects canonical IR, and finally registers the implementation runtime entry and Adapter creator. If any step fails, the upper-level loader invokes unload to roll the completed steps back in reverse order.
484- **Callback signature validation**: While loading a descriptor, the bridge calls `validate_op_impl_descriptor` to validate schema-bound signatures once, before holder creation or any business callback. For `execute`, it validates the total parameter count, the positional form and supplied type annotations of inputs, and the keyword-only form, names, and supplied type annotations of attributes. Outputs, the return annotation, and the return value are not validated. `declare_launch_args` validates inputs, outputs, attributes, and a `None` return annotation. Runtime callbacks do not validate signatures, and validation state does not enter the holder lifecycle.491- **Callback signature validation**: While loading a descriptor, the bridge calls `validate_op_impl_descriptor` to validate schema-bound signatures once, before holder creation or any business callback. For `execute`, it validates the total parameter count, the positional form and supplied type annotations of inputs, and the keyword-only form, names, and supplied type annotations of attributes. Outputs, the return annotation, and the return value are not validated. `declare_launch_args` validates inputs, outputs, attributes, and a `None` return annotation. Runtime callbacks do not validate signatures, and validation state does not enter the holder lifecycle.
485- **Holder lifecycle**: The C++ adapter owns `PythonCustomOpHolder`. The Python side uses `_OP_IMPL_HOLDERS` to save instances by `instance_id`. When the adapter is destructed, the Python holder is destroyed.492- **Holder lifecycle**: The C++ adapter owns `PythonCustomOpHolder`. The Python side uses `_OP_IMPL_HOLDERS` to save instances by `instance_id`. When the adapter is destructed, the Python holder is destroyed.
486- **Context binding**: Schema-bound callbacks establish dynamic scopes with `ContextVar`, and `get_execute_ctx()` / `get_declare_launch_args_ctx()` read their corresponding bindings. Resetting the token restores the outer context after nested invocation.493- **Context binding**: Schema-bound callbacks establish dynamic scopes with `ContextVar`, and `get_execute_ctx()` / `get_declare_launch_args_ctx()` read their corresponding bindings. Resetting the token restores the outer context after nested invocation.
@@ -500,10 +507,11 @@ Entry EnsureReady()
500 -> BuildPrebuiltBridgeLibraryCandidates()507 -> BuildPrebuiltBridgeLibraryCandidates()
501 -> dlopen libge_python_custom_op_bridge.so508 -> dlopen libge_python_custom_op_bridge.so
502 -> set_artifact_config(native_module_path)509 -> set_artifact_config(native_module_path)
503- -> load_and_get_op_impl_descriptors()510+ -> load_and_get_op_descriptors()
504- -> Collect canonical IR and call validate_op_impl_descriptor
505 -> register_custom_ops(registrar)511 -> register_custom_ops(registrar)
506- -> CustomOpFactory::RegisterCustomOpCreator()512+ -> Register Python prototype creators and collect canonical IR
513+ -> Call validate_op_impl_descriptor
514+ -> Register implementation runtime entries and Adapter creators
507```515```
508 516 
509#### PreRun Idempotent Reload Process517#### PreRun Idempotent Reload Process
@@ -521,7 +529,7 @@ This path ensures that when the user sets the Python custom op path only after i
521```text529```text
522CustomOpRegistry::CreateOrGetCustomOp(op_type)530CustomOpRegistry::CreateOrGetCustomOp(op_type)
523 -> PythonCustomOpAdapter(desc)531 -> PythonCustomOpAdapter(desc)
524- -> PythonCustomOpHolder(desc)532+ -> PythonCustomOpImplHolder(desc)
525 -> callbacks.create(desc)533 -> callbacks.create(desc)
526 -> The bridge resolves the public GetRegisteredIrDef symbol and queries canonical IR534 -> The bridge resolves the public GetRegisteredIrDef symbol and queries canonical IR
527 -> The bridge holder stores PythonCustomOpIrMeta without validating signatures again535 -> The bridge holder stores PythonCustomOpIrMeta without validating signatures again
Mdocs/zh/design/modules/ge_python/ge_python.md+9-1
@@ -745,7 +745,7 @@ custom_op/
745 745 
746#### 模块定位746#### 模块定位
747 747 
748-Python 自定义算子的长期目标是支持用户使用 Python 描述自定义算子原型,并实现自定义算子的各类能力。当前通过反射实现类上的可调用 `execute` 和 `declare_launch_args` 方法,分别识别执行能力和静态图声明式地址刷新能力,不要求用户类继承 `BaseCustomOp` 或 `EagerExecuteOp`;已有继承写法继续兼容。执行入口同时支持 `execute(ctx)` 兼容形式和按照 canonical IR 输入、属性顺序绑定的 schema-bound 形式。748+Python 自定义算子的长期目标是支持用户使用 Python 描述自定义算子原型,并实现自定义算子的各类能力。当前通过反射实现类上的可调用 `execute` 和 `declare_launch_args` 方法,分别识别执行能力和静态图声明式地址刷新能力,不要求用户类继承 `BaseCustomOp` 或 `EagerExecuteOp`;已有继承写法继续兼容。执行入口同时支持 `execute(ctx)` 兼容形式和按照 canonical IR 输入、属性顺序绑定的 schema-bound 形式。当前阶段还将 Python 原型注册到 `OperatorFactory`,但不调用 Python `infer_meta`,也不提供编译期或 RT2 Meta 推导。
749 749 
750#### 运行时 native artifact 选择750#### 运行时 native artifact 选择
751 751 
@@ -884,6 +884,14 @@ class AnnotatedAddCustom:
884- `bootstrap.py` 负责扫描路径并动态加载 Python 模块884- `bootstrap.py` 负责扫描路径并动态加载 Python 模块
885- 支持单个 `.py` 文件、普通目录下的 `.py` 文件和包含 `__init__.py` 的 Python 包885- 支持单个 `.py` 文件、普通目录下的 `.py` 文件和包含 `__init__.py` 的 Python 包
886 886 
887+**原型注册**:
888+ 
889+- `register_op` 收集 required/optional/dynamic input、12 类 attr、required/dynamic output 和 `mutates_args`
890+- bridge 在同一加载事务中先注册 Python proto creator、收集生效的 canonical IR,再注册 impl runtime entry 和 Adapter creator
891+- 支持 Python proto-only、Python proto + impl、C++ proto + Python impl、无 proto legacy impl;无 proto schema-bound impl 注册失败
892+- Python 原型可以覆盖同名内置原型;与已加载的同名 C++/Python 自定义算子冲突时注册失败
893+- 批次失败按 Adapter creator、impl runtime entry、proto creator 的逆序回滚;卸载只清理当前 loader 持有的对象
894+ 
887**使用示例**:895**使用示例**:
888```python896```python
889from ge.custom_op import get_execute_ctx, register_op_impl897from ge.custom_op import get_execute_ctx, register_op_impl
Mdocs/zh/design/modules/ge_python/ge_python_custom_op_design.md+22-14
@@ -19,7 +19,7 @@ Python 自定义算子的完整定位是支持用户用 Python 描述自定义
19- Python 用户通过 `declare_launch_args` 实现 `AnnotatedArgsOp` 编译期回调,使用 `AnnotatedArgsContext``AnnotatedKernelArgs``AnnotatedKernelLaunchInfo` 声明 kernel 启动参数。19- Python 用户通过 `declare_launch_args` 实现 `AnnotatedArgsOp` 编译期回调,使用 `AnnotatedArgsContext``AnnotatedKernelArgs``AnnotatedKernelLaunchInfo` 声明 kernel 启动参数。
20- `ge.runtime` 提供 context 返回或入参所需的 `Tensor``StorageShape``StorageFormat``Shape``TensorPlacement` 等运行时数据结构。20- `ge.runtime` 提供 context 返回或入参所需的 `Tensor``StorageShape``StorageFormat``Shape``TensorPlacement` 等运行时数据结构。
21 21 
22-V2 计划在 V1 执行能力的基础上扩展 Python 原型和 Meta 推导能力。22+V2 在 V1 执行能力的基础上扩展 Python 原型和 Meta 推导能力。当前阶段已经实现 Python 原型 creator、Adapter 注册事务和所有权管理,但尚不调用 Python `infer_meta`。
23 23 
24V2 中,被 `register_op` 装饰的 Python 函数负责 Meta 推导,本文统一称为 `infer_meta`,但不要求函数名必须是 `infer_meta`。该函数按照算子原型接收输入 `TensorDesc`(包括可选输入和动态输入)及属性值,返回一个或多个描述输出 shape 和 data type 的 `TensorDesc`;它不读取输入 Tensor 数据,也不执行算子 kernel。24V2 中,被 `register_op` 装饰的 Python 函数负责 Meta 推导,本文统一称为 `infer_meta`,但不要求函数名必须是 `infer_meta`。该函数按照算子原型接收输入 `TensorDesc`(包括可选输入和动态输入)及属性值,返回一个或多个描述输出 shape 和 data type 的 `TensorDesc`;它不读取输入 Tensor 数据,也不执行算子 kernel。
25 25 
@@ -71,6 +71,7 @@ Python custom op 是 GE Python 体系的一部分,与 Python pass 共享以下
71| Runtime loader | `runtime/custom_op/custom_op_loader.cc` | 统一加载 C++ custom op 和 Python custom op |71| Runtime loader | `runtime/custom_op/custom_op_loader.cc` | 统一加载 C++ custom op 和 Python custom op |
72| Bridge loader | `runtime/custom_op/python_custom_op_bridge_loader.cc` | 选择 artifact、加载 `libge_python_custom_op_bridge.so`、注册 creator |72| Bridge loader | `runtime/custom_op/python_custom_op_bridge_loader.cc` | 选择 artifact、加载 `libge_python_custom_op_bridge.so`、注册 creator |
73| Pybind bridge | `runtime/custom_op/python_custom_op_pybind_bridge.cc` | 导入 Python bridge 模块、创建 holder、回调 `execute` / `declare_launch_args` |73| Pybind bridge | `runtime/custom_op/python_custom_op_pybind_bridge.cc` | 导入 Python bridge 模块、创建 holder、回调 `execute` / `declare_launch_args` |
74+| Proto runtime | `runtime/custom_op/python_custom_op_proto.*` | 深拷贝 C POD 原型并注册 `OperatorFactory` creator |
74| Adapter | `runtime/custom_op/python_custom_op_adapter.*` | 作为 C++ `BaseCustomOp` 实例接入现有运行时 |75| Adapter | `runtime/custom_op/python_custom_op_adapter.*` | 作为 C++ `BaseCustomOp` 实例接入现有运行时 |
75| Capability helper | `inc/graph_metadef/graph/custom_op/` | `CustomOpCapability``CustomOpCast<T>` |76| Capability helper | `inc/graph_metadef/graph/custom_op/` | `CustomOpCapability``CustomOpCast<T>` |
76 77 
@@ -80,6 +81,7 @@ V1 功能包括:
80 81 
81- `@register_op_impl(op_type=...)` 注册 Python 自定义算子实现。82- `@register_op_impl(op_type=...)` 注册 Python 自定义算子实现。
82- `@register_op(op_type=..., mutates_args=...)` 根据 Python 函数签名收集自定义算子原型。83- `@register_op(op_type=..., mutates_args=...)` 根据 Python 函数签名收集自定义算子原型。
84+- bridge 将 Python 原型同步注册为 `OperatorFactory` creator,并从生效 creator 收集 canonical IR;当前不调用 `infer_meta`
83- `register_op_impl` 反射实现类上的可调用 `execute``declare_launch_args` 方法并声明对应能力,不要求继承 `BaseCustomOp``EagerExecuteOp``AnnotatedArgsOp`85- `register_op_impl` 反射实现类上的可调用 `execute``declare_launch_args` 方法并声明对应能力,不要求继承 `BaseCustomOp``EagerExecuteOp``AnnotatedArgsOp`
84- `execute(self, ctx)` 兼容形式直接接收 `EagerOpExecutionContext`;schema-bound 形式接收按 canonical IR 组装的输入和属性。86- `execute(self, ctx)` 兼容形式直接接收 `EagerOpExecutionContext`;schema-bound 形式接收按 canonical IR 组装的输入和属性。
85- `EagerOpExecutionContext` 支持输入输出 tensor 查询、动态输入实例数、运行时属性读取、输出/工作区分配和 stream 获取。87- `EagerOpExecutionContext` 支持输入输出 tensor 查询、动态输入实例数、运行时属性读取、输出/工作区分配和 stream 获取。
@@ -96,6 +98,8 @@ V1 功能包括:
96- Python `execute` 的返回值当前不作为状态码使用;正常返回表示成功,抛出异常表示失败。98- Python `execute` 的返回值当前不作为状态码使用;正常返回表示成功,抛出异常表示失败。
97- Python custom op 当前声明 `EagerExecuteOp``AnnotatedArgsOp` capability;其它 C++ 能力接口由 adapter 保留 override 但按不支持处理。99- Python custom op 当前声明 `EagerExecuteOp``AnnotatedArgsOp` capability;其它 C++ 能力接口由 adapter 保留 override 但按不支持处理。
98- schema-bound 形式依赖已有算子原型的 canonical IR。bridge 加载 descriptor 时收集 canonical IR,并在创建 holder 和调用业务 callback 之前调用 `validate_op_impl_descriptor`,一次性校验 schema-bound 签名:`execute` 校验 IR 输入和属性,不把输出参数纳入签名,也不限制返回注解或返回值;`declare_launch_args` 校验输入、输出、属性并要求 `-> None`。runtime callback 只组装实参并调用业务方法,不再校验签名;校验结果属于 descriptor 加载阶段,不进入 holder 生命周期。100- schema-bound 形式依赖已有算子原型的 canonical IR。bridge 加载 descriptor 时收集 canonical IR,并在创建 holder 和调用业务 callback 之前调用 `validate_op_impl_descriptor`,一次性校验 schema-bound 签名:`execute` 校验 IR 输入和属性,不把输出参数纳入签名,也不限制返回注解或返回值;`declare_launch_args` 校验输入、输出、属性并要求 `-> None`。runtime callback 只组装实参并调用业务方法,不再校验签名;校验结果属于 descriptor 加载阶段,不进入 holder 生命周期。
101+- 跨 SO 的 proto/Adapter descriptor 是同步借用的 C POD view,runtime callback 返回前必须完成校验和深拷贝。
102+- Python 原型允许覆盖内置原型;若 `CustomOpFactory` 已存在同名 C++ 或 Python 自定义算子,则视为自定义算子冲突。
99- schema-bound 回调通过 `get_execute_ctx()` 获取当前 context;该绑定只在当前回调动态作用域内有效。103- schema-bound 回调通过 `get_execute_ctx()` 获取当前 context;该绑定只在当前回调动态作用域内有效。
100- Python custom op native/bridge 与构建时 Python ABI 相关,不提供跨 Python minor version 兼容承诺。104- Python custom op native/bridge 与构建时 Python ABI 相关,不提供跨 Python minor version 兼容承诺。
101- bridge C ABI 保持为 v1,`execute``declare_launch_args` 回调只传 holder 与对应 context;canonical IR 由 bridge 通过 run 包公共接口查询,不通过私有 ABI 投影传递。105- bridge C ABI 保持为 v1,`execute``declare_launch_args` 回调只传 holder 与对应 context;canonical IR 由 bridge 通过 run 包公共接口查询,不通过私有 ABI 投影传递。
@@ -325,14 +329,16 @@ Python custom op 加载由 `runtime/custom_op` 管理,避免 `graph_metadef/re
325- `NeedLoadPythonCustomOps()` 仅在 `ASCEND_CUSTOM_OPP_PATH` 下发现 Python 文件或包时返回 true。329- `NeedLoadPythonCustomOps()` 仅在 `ASCEND_CUSTOM_OPP_PATH` 下发现 Python 文件或包时返回 true。
326- `LoadPythonCustomOps()` 解析已加载 Python runtime key,选择 `custom_op/python_custom_op_artifacts/<python_tag>-<platform>` 下的 bridge/native artifact。330- `LoadPythonCustomOps()` 解析已加载 Python runtime key,选择 `custom_op/python_custom_op_artifacts/<python_tag>-<platform>` 下的 bridge/native artifact。
327- `libge_python_custom_op_bridge.so` 通过 `GeGetPythonCustomOpBridgeApi()` 暴露 C ABI v1。331- `libge_python_custom_op_bridge.so` 通过 `GeGetPythonCustomOpBridgeApi()` 暴露 C ABI v1。
328-- bridge 导入 `_ge_custom_op_native` 和 `ge.custom_op._bridge`,注册 descriptor,并为每个 adapter 创建 Python holder332+- bridge 导入 `_ge_custom_op_native` 和 `ge.custom_op._bridge`,一次获取 proto/impl snapshot;先注册全部 proto再校验注册 Adapter
333+- `CustomOpLoader::LoadCustomOps()` 记录 Python custom op 是否已经加载,因此生命周期加载请求重复调用时会直接返回成功,不重复调用 bridge 注册入口。动态 `LoadPythonCustomOpsIfNeeded()` 路径不使用该状态,运行期间可以继续发现新增加的 Python custom op 路径。底层 `LoadPythonCustomOps()` 负责执行一次 bridge 注册尝试;注册失败后由调用方调用 `UnloadPythonCustomOps()` 清理本次产生的部分注册。
334+- `UnloadPythonCustomOps()` 先移除已注册的 Adapter creator,再一次性清理 Python 自定义算子 runtime registry,最后清理已注册的 proto creator。bridge loader 不再逐项注销 runtime entry,也不维护待清理状态。
329- `UnloadCustomOps()` 采用 `active_users_` 引用计数管理生命周期:每次 `LoadCustomOps()` 使计数 +1,每次 `UnloadCustomOps()` 使计数 -1,仅当计数归零时才卸载 Python custom op、清理 Python holder/registry 并关闭 bridge。`ShutdownCustomOpsForProcess()` 作为兼容 wrapper 保留,内部调用 `UnloadCustomOps()`335- `UnloadCustomOps()` 采用 `active_users_` 引用计数管理生命周期:每次 `LoadCustomOps()` 使计数 +1,每次 `UnloadCustomOps()` 使计数 -1,仅当计数归零时才卸载 Python custom op、清理 Python holder/registry 并关闭 bridge。`ShutdownCustomOpsForProcess()` 作为兼容 wrapper 保留,内部调用 `UnloadCustomOps()`
330 336 
331**输出**337**输出**
332 338 
333-- Python descriptor 注册为 `CustomOpFactory` creator。339+- Python proto 注册为 `OperatorFactory` creator,Python impl 注册为 `CustomOpFactory` creator。
334- adapter 析构时销毁 Python holder,并 release runtime registry entry。340- adapter 析构时销毁 Python holder,并 release runtime registry entry。
335-- active adapter 存在runtime registry 不允许 unregister341+- 单项 runtime registry 注销仍受 active adapter 保护;进程卸载先移除 Adapter creator,再批量清理 registry
336 342 
337### 3.3 非功能需求343### 3.3 非功能需求
338 344 
@@ -450,21 +456,21 @@ Python 版本敏感的 bridge 使用 run 包公开头文件中 `GetRegisteredIrD
450 456 
451这里采用运行时符号解析,是因为 `libge_runner.so`/`libge_runner_v2.so` 已依赖 `custom_op_runtime`,而 bridge 由 `custom_op_runtime` 动态加载;若 bridge 再直接链接 runner,会形成反向 SO 依赖。该方式仍以 run 包正式公共头文件约束函数签名和数据类型,只把符号绑定推迟到 bridge 加载后执行。457这里采用运行时符号解析,是因为 `libge_runner.so`/`libge_runner_v2.so` 已依赖 `custom_op_runtime`,而 bridge 由 `custom_op_runtime` 动态加载;若 bridge 再直接链接 runner,会形成反向 SO 依赖。该方式仍以 run 包正式公共头文件约束函数签名和数据类型,只把符号绑定推迟到 bridge 加载后执行。
452 458 
453-#### PythonCustomOpDescriptor459+#### PythonCustomOpProto 与 PythonCustomOpAdapterDescriptor
454 460 
455-C++ runtime 使用 `PythonCustomOpDescriptor`461+C++ runtime 分别保存 owning proto 和 Adapter descriptor;Adapter descriptor 只保存实现 key
456 462 
457```cpp463```cpp
458-struct PythonCustomOpDescriptor {464+struct PythonCustomOpAdapterDescriptor {
459- std::string descriptor_key;
460 std::string op_type;465 std::string op_type;
466+ std::string impl_descriptor_key;
461 CustomOpCapabilityMask capabilities{0U};467 CustomOpCapabilityMask capabilities{0U};
462};468};
463```469```
464 470 
465-#### PythonCustomOpCallbacks471+#### PythonCustomOpAdapterCallbacks
466 472 
467-bridge 向 runtime 注册 create/destroy/execute/declare_launch_args 回调。`IsValid()` 接受 `kEagerExecute`、`kAnnotatedArgs` 或两者组合,要求 create/destroy 非空,并按 capability 校验 execute/declare_launch_args 回调。473+bridge 向 runtime 注册 create/destroy/execute/declare_launch_args 回调。schema-bound 签名校验不是 C++ callback,而是 bridge 在注册 Adapter 前内部调用 `validate_op_impl_descriptor` 完成。`IsValid()` 接受 `kEagerExecute`、`kAnnotatedArgs` 或两者组合,要求 create/destroy 非空,并按 capability 校验 execute/declare_launch_args 回调。
468 474 
469#### BorrowedEagerOpExecutionContext475#### BorrowedEagerOpExecutionContext
470 476 
@@ -481,6 +487,7 @@ native binding 保存 `gert::AnnotatedArgsContext *` 和独立 validity 标记
481- **能力反射**:registry 对实现 class 执行 `getattr``callable` 检查,把 `execute` 映射为 `eager_execute`,把 `declare_launch_args` 映射为 `annotated_args`;Python 用户类的继承关系不参与能力判断。487- **能力反射**:registry 对实现 class 执行 `getattr``callable` 检查,把 `execute` 映射为 `eager_execute`,把 `declare_launch_args` 映射为 `annotated_args`;Python 用户类的继承关系不参与能力判断。
482- **capability 过滤**`CustomOpCast<T>()` 先识别 `CustomOpCapabilityProvider`,再按 bitmask 判断是否支持目标接口。488- **capability 过滤**`CustomOpCast<T>()` 先识别 `CustomOpCapabilityProvider`,再按 bitmask 判断是否支持目标接口。
483- **IR 实参组装**:bridge 在 descriptor 加载阶段通过 run 包公共接口查询 canonical IR 以校验签名,holder 创建时再次查询并持有运行期 IR 快照;runtime callback 按该快照的 IR 顺序读取 required/optional/dynamic 输入输出和 typed runtime attrs,分别构造 positional arguments 和 keyword arguments。489- **IR 实参组装**:bridge 在 descriptor 加载阶段通过 run 包公共接口查询 canonical IR 以校验签名,holder 创建时再次查询并持有运行期 IR 快照;runtime callback 按该快照的 IR 顺序读取 required/optional/dynamic 输入输出和 typed runtime attrs,分别构造 positional arguments 和 keyword arguments。
490+- **注册事务**:先同步深拷贝 proto C POD 并注册 creator,再收集 canonical IR,最后注册 impl runtime entry 和 Adapter creator;任一步失败由上层 loader 调用卸载,按相反顺序回滚已完成的步骤。
484- **回调签名校验**:bridge 加载 descriptor 时调用 `validate_op_impl_descriptor` 一次性校验 schema-bound 签名,并在创建 holder 和业务 callback 之前完成。`execute` 校验总参数数量、输入的位置形式及已提供的类型注解,以及属性的 keyword-only 形式、名称和已提供的类型注解;输出参数、返回注解和返回值不参与校验。`declare_launch_args` 校验输入、输出、属性及 `None` 返回注解。runtime callback 不再校验签名,校验状态也不进入 holder 生命周期。491- **回调签名校验**:bridge 加载 descriptor 时调用 `validate_op_impl_descriptor` 一次性校验 schema-bound 签名,并在创建 holder 和业务 callback 之前完成。`execute` 校验总参数数量、输入的位置形式及已提供的类型注解,以及属性的 keyword-only 形式、名称和已提供的类型注解;输出参数、返回注解和返回值不参与校验。`declare_launch_args` 校验输入、输出、属性及 `None` 返回注解。runtime callback 不再校验签名,校验状态也不进入 holder 生命周期。
485- **holder 生命周期**:C++ adapter 拥有 `PythonCustomOpHolder`,Python 侧 `_OP_IMPL_HOLDERS``instance_id` 保存实例;adapter 析构时销毁 Python holder。492- **holder 生命周期**:C++ adapter 拥有 `PythonCustomOpHolder`,Python 侧 `_OP_IMPL_HOLDERS``instance_id` 保存实例;adapter 析构时销毁 Python holder。
486- **上下文绑定**:schema-bound 回调使用 `ContextVar` 建立动态作用域,`get_execute_ctx()` / `get_declare_launch_args_ctx()` 读取对应绑定;token reset 支持嵌套调用后恢复外层 context。493- **上下文绑定**:schema-bound 回调使用 `ContextVar` 建立动态作用域,`get_execute_ctx()` / `get_declare_launch_args_ctx()` 读取对应绑定;token reset 支持嵌套调用后恢复外层 context。
@@ -500,10 +507,11 @@ native binding 保存 `gert::AnnotatedArgsContext *` 和独立 validity 标记
500 -> BuildPrebuiltBridgeLibraryCandidates()507 -> BuildPrebuiltBridgeLibraryCandidates()
501 -> dlopen libge_python_custom_op_bridge.so508 -> dlopen libge_python_custom_op_bridge.so
502 -> set_artifact_config(native_module_path)509 -> set_artifact_config(native_module_path)
503- -> load_and_get_op_impl_descriptors()510+ -> load_and_get_op_descriptors()
504- -> 收集 canonical IR 并调用 validate_op_impl_descriptor
505 -> register_custom_ops(registrar)511 -> register_custom_ops(registrar)
506- -> CustomOpFactory::RegisterCustomOpCreator()512+ -> 注册 Python proto creator 并收集 canonical IR
513+ -> 调用 validate_op_impl_descriptor
514+ -> 注册 impl runtime entry 和 Adapter creator
507```515```
508 516 
509#### PreRun 幂等补加载流程517#### PreRun 幂等补加载流程
@@ -521,7 +529,7 @@ GraphManager::PreRun()
521```text529```text
522CustomOpRegistry::CreateOrGetCustomOp(op_type)530CustomOpRegistry::CreateOrGetCustomOp(op_type)
523 -> PythonCustomOpAdapter(desc)531 -> PythonCustomOpAdapter(desc)
524- -> PythonCustomOpHolder(desc)532+ -> PythonCustomOpImplHolder(desc)
525 -> callbacks.create(desc)533 -> callbacks.create(desc)
526 -> bridge 解析 GetRegisteredIrDef 公共符号并查询 canonical IR534 -> bridge 解析 GetRegisteredIrDef 公共符号并查询 canonical IR
527 -> bridge holder 保存 PythonCustomOpIrMeta;不再校验签名535 -> bridge holder 保存 PythonCustomOpIrMeta;不再校验签名
Mgraph_metadef/graph/CMakeLists.txt+1-0
@@ -116,6 +116,7 @@ SET(GRAPH_SOURCE_LIST
116 "context/runtime_inference_context.cc"116 "context/runtime_inference_context.cc"
117 "refiner/shape_refiner.cc"117 "refiner/shape_refiner.cc"
118 "ir/ir_definitions_recover.cc"118 "ir/ir_definitions_recover.cc"
119+ "ir/ir_definitions_query.cc"
119 "opsproto/opsproto_manager.cc"120 "opsproto/opsproto_manager.cc"
120 "utils/op_desc_utils.cc"121 "utils/op_desc_utils.cc"
121 "utils/tuning_utils.cc"122 "utils/tuning_utils.cc"
Agraph_metadef/graph/ir/ir_definitions_query.cc+66-0
@@ -0,0 +1,66 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#include "graph/utils/ir_definitions_query.h"
12+ 
13+#include <map>
14+ 
15+#include "common/checker.h"
16+#include "graph/operator_factory.h"
17+#include "graph/utils/op_desc_utils.h"
18+ 
19+extern "C" VISIBILITY_EXPORT ge::Status GetRegisteredIrDefFromGraph(
20+ const char *op_type, std::vector<std::pair<ge::AscendString, ge::AscendString>> &inputs,
21+ std::vector<std::pair<ge::AscendString, ge::AscendString>> &outputs,
22+ std::vector<std::pair<ge::AscendString, ge::AscendString>> &attrs) {
23+ GE_ASSERT_NOTNULL(op_type);
24+ const auto op = ge::OperatorFactory::CreateOperator("_", op_type);
25+ GE_WARN_ASSERT(!op.IsEmpty(), "No operator found for type: %s", op_type);
26+ const auto desc = ge::OpDescUtils::GetOpDescFromOperator(op);
27+ 
28+ static const auto kInputTypeString = []() {
29+ std::map<ge::IrInputType, ge::AscendString> type_str;
30+ type_str[ge::IrInputType::kIrInputRequired] = "required";
31+ type_str[ge::IrInputType::kIrInputOptional] = "optional";
32+ type_str[ge::IrInputType::kIrInputDynamic] = "dynamic";
33+ return type_str;
34+ }();
35+ 
36+ static const auto kOutputTypeString = []() {
37+ std::map<ge::IrOutputType, ge::AscendString> type_str;
38+ type_str[ge::IrOutputType::kIrOutputRequired] = "required";
39+ type_str[ge::IrOutputType::kIrOutputDynamic] = "dynamic";
40+ return type_str;
41+ }();
42+ 
43+ GE_ASSERT_NOTNULL(desc, "Failed to get OpDesc from operator: %s", op_type);
44+ for (const auto &name2type : desc->GetIrInputs()) {
45+ const auto iter = kInputTypeString.find(name2type.second);
46+ GE_ASSERT(iter != kInputTypeString.end(), "Unknown input type: %d for operator: %s", name2type.second, op_type);
47+ inputs.emplace_back(ge::AscendString(name2type.first.c_str()), iter->second);
48+ }
49+ for (const auto &name2type : desc->GetIrOutputs()) {
50+ const auto iter = kOutputTypeString.find(name2type.second);
51+ GE_ASSERT(iter != kOutputTypeString.end(), "Unknown output type: %d for operator: %s", name2type.second, op_type);
52+ outputs.emplace_back(ge::AscendString(name2type.first.c_str()), iter->second);
53+ }
54+ 
55+ std::map<ge::AscendString, ge::AscendString> attrs_and_types;
56+ GE_ASSERT_GRAPH_SUCCESS(op.GetAllIrAttrNamesAndTypes(attrs_and_types),
57+ "Failed to get attr names and types for operator: %s", op_type);
58+ for (const auto &attr : desc->GetIrAttrNames()) {
59+ const auto attr_name = ge::AscendString(attr.c_str());
60+ const auto iter = attrs_and_types.find(attr_name);
61+ GE_ASSERT(iter != attrs_and_types.end(), "Failed to get attr type for operator: %s, attr: %s", op_type,
62+ attr.c_str());
63+ attrs.emplace_back(attr_name, iter->second);
64+ }
65+ return ge::SUCCESS;
66+}
Ainc/graph_metadef/graph/utils/ir_definitions_query.h+26-0
@@ -0,0 +1,26 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software; you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#ifndef INC_GRAPH_METADEF_GRAPH_UTILS_IR_DEFINITIONS_QUERY_H_
12+#define INC_GRAPH_METADEF_GRAPH_UTILS_IR_DEFINITIONS_QUERY_H_
13+ 
14+#include <string>
15+#include <utility>
16+#include <vector>
17+ 
18+#include "framework/common/ge_visibility.h"
19+#include "ge/ge_api_types.h"
20+ 
21+extern "C" VISIBILITY_EXPORT ge::Status GetRegisteredIrDefFromGraph(
22+ const char *op_type, std::vector<std::pair<ge::AscendString, ge::AscendString>> &inputs,
23+ std::vector<std::pair<ge::AscendString, ge::AscendString>> &outputs,
24+ std::vector<std::pair<ge::AscendString, ge::AscendString>> &attrs);
25+ 
26+#endif // INC_GRAPH_METADEF_GRAPH_UTILS_IR_DEFINITIONS_QUERY_H_
Mruntime/custom_op/CMakeLists.txt+2-0
@@ -12,6 +12,7 @@ add_library(custom_op_runtime SHARED
12 "custom_op_loader.cc"12 "custom_op_loader.cc"
13 "python_custom_op_adapter.cc"13 "python_custom_op_adapter.cc"
14 "python_custom_op_bridge_loader.cc"14 "python_custom_op_bridge_loader.cc"
15+ "python_custom_op_proto.cc"
15)16)
16 17 
17target_compile_definitions(custom_op_runtime PRIVATE18target_compile_definitions(custom_op_runtime PRIVATE
@@ -54,6 +55,7 @@ target_link_libraries(custom_op_runtime
54)55)
55 56 
56add_library(ge_python_custom_op_bridge SHARED57add_library(ge_python_custom_op_bridge SHARED
58+ "python_custom_op_bridge_descriptors.cc"
57 "python_custom_op_pybind_bridge.cc"59 "python_custom_op_pybind_bridge.cc"
58)60)
59 61 
Mruntime/custom_op/python_custom_op_adapter.cc+75-59
@@ -21,12 +21,12 @@
21namespace ge {21namespace ge {
22namespace custom_op {22namespace custom_op {
23namespace {23namespace {
24-struct PythonCustomOpRuntimeEntry {24+struct PythonCustomOpImplRuntimeEntry {
25- explicit PythonCustomOpRuntimeEntry(PythonCustomOpDescriptor d, PythonCustomOpCallbacks cb)25+ explicit PythonCustomOpImplRuntimeEntry(PythonCustomOpAdapterDescriptor d, PythonCustomOpAdapterCallbacks cb)
26 : desc(std::move(d)), callbacks(cb) {}26 : desc(std::move(d)), callbacks(cb) {}
27 27 
28- PythonCustomOpDescriptor desc;28+ PythonCustomOpAdapterDescriptor desc;
29- PythonCustomOpCallbacks callbacks;29+ PythonCustomOpAdapterCallbacks callbacks;
30 // A descriptor_key maps to one shared runtime entry. Multiple adapters may30 // A descriptor_key maps to one shared runtime entry. Multiple adapters may
31 // reference it, while each adapter owns one Python custom op instance.31 // reference it, while each adapter owns one Python custom op instance.
32 // active_adapter_count prevents unregister while callbacks are still in use.32 // active_adapter_count prevents unregister while callbacks are still in use.
@@ -34,27 +34,27 @@ struct PythonCustomOpRuntimeEntry {
34 std::mutex mutex;34 std::mutex mutex;
35};35};
36 36 
37-class PythonCustomOpRuntimeRegistryImpl {37+class PythonCustomOpImplRuntimeRegistryImpl {
38 public:38 public:
39- bool Register(const PythonCustomOpDescriptor &desc, const PythonCustomOpCallbacks &callbacks) {39+ bool Register(const PythonCustomOpAdapterDescriptor &desc, const PythonCustomOpAdapterCallbacks &callbacks) {
40- if (desc.descriptor_key.empty() || desc.op_type.empty() || (!callbacks.IsValid(desc.capabilities))) {40+ if (desc.impl_descriptor_key.empty() || desc.op_type.empty() || (!callbacks.IsValid(desc.capabilities))) {
41- GELOGW("Register python custom op runtime failed, descriptor key[%s], op type[%s].", desc.descriptor_key.c_str(),41+ GELOGW("Register python custom op runtime failed, descriptor key[%s], op type[%s].",
42- desc.op_type.c_str());42+ desc.impl_descriptor_key.c_str(), desc.op_type.c_str());
43 return false;43 return false;
44 }44 }
45 45 
46 std::lock_guard<std::mutex> lock(mutex_);46 std::lock_guard<std::mutex> lock(mutex_);
47- if (descriptor_key_to_runtime_entry_.find(desc.descriptor_key) != descriptor_key_to_runtime_entry_.cend()) {47+ if (descriptor_key_to_runtime_entry_.find(desc.impl_descriptor_key) != descriptor_key_to_runtime_entry_.cend()) {
48- GELOGW("Python custom op runtime descriptor key[%s] has already registered.", desc.descriptor_key.c_str());48+ GELOGW("Python custom op runtime descriptor key[%s] has already registered.", desc.impl_descriptor_key.c_str());
49 return false;49 return false;
50 }50 }
51- auto runtime_entry = ComGraphMakeShared<PythonCustomOpRuntimeEntry>(desc, callbacks);51+ auto runtime_entry = ComGraphMakeShared<PythonCustomOpImplRuntimeEntry>(desc, callbacks);
52 if (runtime_entry == nullptr) {52 if (runtime_entry == nullptr) {
53 GELOGE(GRAPH_FAILED, "Create python custom op runtime entry failed, descriptor key[%s], op type[%s].",53 GELOGE(GRAPH_FAILED, "Create python custom op runtime entry failed, descriptor key[%s], op type[%s].",
54- desc.descriptor_key.c_str(), desc.op_type.c_str());54+ desc.impl_descriptor_key.c_str(), desc.op_type.c_str());
55 return false;55 return false;
56 }56 }
57- descriptor_key_to_runtime_entry_.emplace(desc.descriptor_key, std::move(runtime_entry));57+ descriptor_key_to_runtime_entry_.emplace(desc.impl_descriptor_key, std::move(runtime_entry));
58 return true;58 return true;
59 }59 }
60 60 
@@ -74,22 +74,31 @@ class PythonCustomOpRuntimeRegistryImpl {
74 return true;74 return true;
75 }75 }
76 76 
77- bool Acquire(const PythonCustomOpDescriptor &desc, PythonCustomOpCallbacks &callbacks) {77+ bool Acquire(const PythonCustomOpAdapterDescriptor &desc, PythonCustomOpAdapterCallbacks &callbacks) {
78- auto runtime_entry = Get(desc.descriptor_key);78+ const std::lock_guard<std::mutex> map_lock(mutex_);
79- if (runtime_entry == nullptr) {79+ const auto iter = descriptor_key_to_runtime_entry_.find(desc.impl_descriptor_key);
80+ if (iter == descriptor_key_to_runtime_entry_.cend()) {
80 GELOGW("Acquire python custom op runtime failed because descriptor key[%s] is not registered.",81 GELOGW("Acquire python custom op runtime failed because descriptor key[%s] is not registered.",
81- desc.descriptor_key.c_str());82+ desc.impl_descriptor_key.c_str());
82 return false;83 return false;
83 }84 }
84 85 
85- std::lock_guard<std::mutex> lock(runtime_entry->mutex);86+ const auto &runtime_entry = iter->second;
87+ std::lock_guard<std::mutex> runtime_lock(runtime_entry->mutex);
88+ if ((runtime_entry->desc.op_type != desc.op_type) ||
89+ (runtime_entry->desc.impl_descriptor_key != desc.impl_descriptor_key) ||
90+ (runtime_entry->desc.capabilities != desc.capabilities)) {
91+ GELOGW("Acquire python custom op runtime descriptor mismatch, descriptor key[%s].",
92+ desc.impl_descriptor_key.c_str());
93+ return false;
94+ }
86 ++runtime_entry->active_adapter_count;95 ++runtime_entry->active_adapter_count;
87 callbacks = runtime_entry->callbacks;96 callbacks = runtime_entry->callbacks;
88 return true;97 return true;
89 }98 }
90 99 
91- void Release(const PythonCustomOpDescriptor &desc) {100+ void Release(const PythonCustomOpAdapterDescriptor &desc) {
92- auto runtime_entry = Get(desc.descriptor_key);101+ auto runtime_entry = Get(desc.impl_descriptor_key);
93 if (runtime_entry == nullptr) {102 if (runtime_entry == nullptr) {
94 return;103 return;
95 }104 }
@@ -107,7 +116,7 @@ class PythonCustomOpRuntimeRegistryImpl {
107 }116 }
108 117 
109 private:118 private:
110- std::shared_ptr<PythonCustomOpRuntimeEntry> Get(const std::string &descriptor_key) {119+ std::shared_ptr<PythonCustomOpImplRuntimeEntry> Get(const std::string &descriptor_key) {
111 std::lock_guard<std::mutex> lock(mutex_);120 std::lock_guard<std::mutex> lock(mutex_);
112 const auto iter = descriptor_key_to_runtime_entry_.find(descriptor_key);121 const auto iter = descriptor_key_to_runtime_entry_.find(descriptor_key);
113 if (iter == descriptor_key_to_runtime_entry_.cend()) {122 if (iter == descriptor_key_to_runtime_entry_.cend()) {
@@ -117,85 +126,95 @@ class PythonCustomOpRuntimeRegistryImpl {
117 }126 }
118 127 
119 std::mutex mutex_;128 std::mutex mutex_;
120- std::map<std::string, std::shared_ptr<PythonCustomOpRuntimeEntry>> descriptor_key_to_runtime_entry_;129+ std::map<std::string, std::shared_ptr<PythonCustomOpImplRuntimeEntry>> descriptor_key_to_runtime_entry_;
121};130};
122 131 
123-PythonCustomOpRuntimeRegistryImpl &GetPythonCustomOpRuntimeRegistryImpl() {132+PythonCustomOpImplRuntimeRegistryImpl &GetPythonCustomOpImplRuntimeRegistryImpl() {
124- static PythonCustomOpRuntimeRegistryImpl runtime_registry;133+ static PythonCustomOpImplRuntimeRegistryImpl runtime_registry;
125 return runtime_registry;134 return runtime_registry;
126}135}
136+ 
127} // namespace137} // namespace
128 138 
129-PythonCustomOpHolder::PythonCustomOpHolder(const PythonCustomOpDescriptor &desc) : desc_(desc) {139+PythonCustomOpImplHolder::PythonCustomOpImplHolder(const PythonCustomOpAdapterDescriptor &desc) : desc_(desc) {
130- if (!PythonCustomOpRuntimeRegistry::GetInstance().Acquire(desc_, callbacks_)) {140+ if (!PythonCustomOpImplRuntimeRegistry::GetInstance().Acquire(desc_, callbacks_)) {
131 return;141 return;
132 }142 }
133- if (callbacks_.create == nullptr) {143+ if (callbacks_.create_impl_holder == nullptr) {
134- PythonCustomOpRuntimeRegistry::GetInstance().Release(desc_);144+ PythonCustomOpImplRuntimeRegistry::GetInstance().Release(desc_);
135 return;145 return;
136 }146 }
137- holder_ = callbacks_.create(&desc_);147+ const PythonCustomOpAdapterDescriptorView descriptor_view = {
148+ {desc_.op_type.data(), desc_.op_type.size()},
149+ {desc_.impl_descriptor_key.data(), desc_.impl_descriptor_key.size()},
150+ desc_.capabilities};
151+ holder_ = callbacks_.create_impl_holder(&descriptor_view);
138 if (holder_ == nullptr) {152 if (holder_ == nullptr) {
139- PythonCustomOpRuntimeRegistry::GetInstance().Release(desc_);153+ PythonCustomOpImplRuntimeRegistry::GetInstance().Release(desc_);
140 return;154 return;
141 }155 }
142 valid_ = true;156 valid_ = true;
143}157}
144 158 
145-PythonCustomOpHolder::~PythonCustomOpHolder() {159+PythonCustomOpImplHolder::~PythonCustomOpImplHolder() {
146 if (valid_) {160 if (valid_) {
147- if ((holder_ != nullptr) && (callbacks_.destroy != nullptr)) {161+ if ((holder_ != nullptr) && (callbacks_.destroy_impl_holder != nullptr)) {
148- callbacks_.destroy(holder_);162+ callbacks_.destroy_impl_holder(holder_);
149 holder_ = nullptr;163 holder_ = nullptr;
150 }164 }
151- PythonCustomOpRuntimeRegistry::GetInstance().Release(desc_);165+ PythonCustomOpImplRuntimeRegistry::GetInstance().Release(desc_);
152 }166 }
153}167}
154 168 
155-bool PythonCustomOpHolder::IsValid() const {169+bool PythonCustomOpImplHolder::IsValid() const {
156 return valid_;170 return valid_;
157}171}
158 172 
159-void *PythonCustomOpHolder::GetHolder() const {173+void *PythonCustomOpImplHolder::GetHolder() const {
160 return holder_;174 return holder_;
161}175}
162 176 
163-const PythonCustomOpCallbacks &PythonCustomOpHolder::GetCallbacks() const {177+const PythonCustomOpAdapterCallbacks &PythonCustomOpImplHolder::GetCallbacks() const {
164 return callbacks_;178 return callbacks_;
165}179}
166 180 
167-const PythonCustomOpDescriptor &PythonCustomOpHolder::GetDescriptor() const {181+const PythonCustomOpAdapterDescriptor &PythonCustomOpImplHolder::GetDescriptor() const {
168 return desc_;182 return desc_;
169}183}
170 184 
171-PythonCustomOpRuntimeRegistry &PythonCustomOpRuntimeRegistry::GetInstance() {185+PythonCustomOpImplRuntimeRegistry &PythonCustomOpImplRuntimeRegistry::GetInstance() {
172- static PythonCustomOpRuntimeRegistry runtime_registry;186+ static PythonCustomOpImplRuntimeRegistry runtime_registry;
173 return runtime_registry;187 return runtime_registry;
174}188}
175 189 
176-bool PythonCustomOpRuntimeRegistry::Register(const PythonCustomOpDescriptor &desc,190+bool PythonCustomOpImplRuntimeRegistry::Register(const PythonCustomOpAdapterDescriptor &desc,
177- const PythonCustomOpCallbacks &callbacks) {191+ const PythonCustomOpAdapterCallbacks &callbacks) {
178- return GetPythonCustomOpRuntimeRegistryImpl().Register(desc, callbacks);192+ return GetPythonCustomOpImplRuntimeRegistryImpl().Register(desc, callbacks);
179}193}
180 194 
181-bool PythonCustomOpRuntimeRegistry::Unregister(const std::string &descriptor_key) {195+bool PythonCustomOpImplRuntimeRegistry::Unregister(const std::string &descriptor_key) {
182- return GetPythonCustomOpRuntimeRegistryImpl().Unregister(descriptor_key);196+ return GetPythonCustomOpImplRuntimeRegistryImpl().Unregister(descriptor_key);
183}197}
184 198 
185-bool PythonCustomOpRuntimeRegistry::Acquire(const PythonCustomOpDescriptor &desc, PythonCustomOpCallbacks &callbacks) {199+bool PythonCustomOpImplRuntimeRegistry::Acquire(const PythonCustomOpAdapterDescriptor &desc,
186- return GetPythonCustomOpRuntimeRegistryImpl().Acquire(desc, callbacks);200+ PythonCustomOpAdapterCallbacks &callbacks) {
201+ return GetPythonCustomOpImplRuntimeRegistryImpl().Acquire(desc, callbacks);
187}202}
188 203 
189-void PythonCustomOpRuntimeRegistry::Release(const PythonCustomOpDescriptor &desc) {204+void PythonCustomOpImplRuntimeRegistry::Release(const PythonCustomOpAdapterDescriptor &desc) {
190- GetPythonCustomOpRuntimeRegistryImpl().Release(desc);205+ GetPythonCustomOpImplRuntimeRegistryImpl().Release(desc);
191}206}
192 207 
193-void PythonCustomOpRuntimeRegistry::Clear() {208+void PythonCustomOpImplRuntimeRegistry::Clear() {
194- GetPythonCustomOpRuntimeRegistryImpl().Clear();209+ GetPythonCustomOpImplRuntimeRegistryImpl().Clear();
195}210}
196 211 
197-PythonCustomOpAdapter::PythonCustomOpAdapter(PythonCustomOpDescriptor desc)212+void ClearPythonCustomOpRuntimeRegistry() {
198- : desc_(std::move(desc)), holder_(new (std::nothrow) PythonCustomOpHolder(desc_)) {}213+ PythonCustomOpImplRuntimeRegistry::GetInstance().Clear();
214+}
215+ 
216+PythonCustomOpAdapter::PythonCustomOpAdapter(PythonCustomOpAdapterDescriptor desc)
217+ : desc_(std::move(desc)), holder_(new (std::nothrow) PythonCustomOpImplHolder(desc_)) {}
199 218 
200PythonCustomOpAdapter::~PythonCustomOpAdapter() = default;219PythonCustomOpAdapter::~PythonCustomOpAdapter() = default;
201 220 
@@ -214,7 +233,7 @@ graphStatus PythonCustomOpAdapter::Execute(gert::EagerOpExecutionContext *ctx) {
214 if ((holder_ == nullptr) || (!holder_->IsValid()) || (holder_->GetHolder() == nullptr) ||233 if ((holder_ == nullptr) || (!holder_->IsValid()) || (holder_->GetHolder() == nullptr) ||
215 (holder_->GetCallbacks().execute == nullptr)) {234 (holder_->GetCallbacks().execute == nullptr)) {
216 GELOGE(GRAPH_FAILED, "Python custom op adapter is invalid, descriptor key[%s], op type[%s].",235 GELOGE(GRAPH_FAILED, "Python custom op adapter is invalid, descriptor key[%s], op type[%s].",
217- desc_.descriptor_key.c_str(), desc_.op_type.c_str());236+ desc_.impl_descriptor_key.c_str(), desc_.op_type.c_str());
218 return GRAPH_FAILED;237 return GRAPH_FAILED;
219 }238 }
220 return holder_->GetCallbacks().execute(holder_->GetHolder(), ctx);239 return holder_->GetCallbacks().execute(holder_->GetHolder(), ctx);
@@ -227,7 +246,7 @@ graphStatus PythonCustomOpAdapter::DeclareLaunchArgs(gert::AnnotatedArgsContext
227 if ((holder_ == nullptr) || (!holder_->IsValid()) || (holder_->GetHolder() == nullptr) ||246 if ((holder_ == nullptr) || (!holder_->IsValid()) || (holder_->GetHolder() == nullptr) ||
228 (holder_->GetCallbacks().declare_launch_args == nullptr)) {247 (holder_->GetCallbacks().declare_launch_args == nullptr)) {
229 GELOGE(GRAPH_FAILED, "Python custom op adapter is invalid, descriptor key[%s], op type[%s].",248 GELOGE(GRAPH_FAILED, "Python custom op adapter is invalid, descriptor key[%s], op type[%s].",
230- desc_.descriptor_key.c_str(), desc_.op_type.c_str());249+ desc_.impl_descriptor_key.c_str(), desc_.op_type.c_str());
231 return GRAPH_FAILED;250 return GRAPH_FAILED;
232 }251 }
233 return holder_->GetCallbacks().declare_launch_args(holder_->GetHolder(), &ctx);252 return holder_->GetCallbacks().declare_launch_args(holder_->GetHolder(), &ctx);
@@ -269,8 +288,5 @@ graphStatus PythonCustomOpAdapter::ReportUnsupported(CustomOpCapability capabili
269 return GRAPH_FAILED;288 return GRAPH_FAILED;
270}289}
271 290 
272-void ClearPythonCustomOpRuntimeRegistry() {
273- PythonCustomOpRuntimeRegistry::GetInstance().Clear();
274-}
275} // namespace custom_op291} // namespace custom_op
276} // namespace ge292} // namespace ge
Mruntime/custom_op/python_custom_op_adapter.h+26-20
@@ -20,37 +20,43 @@
20 20 
21namespace ge {21namespace ge {
22namespace custom_op {22namespace custom_op {
23-class PythonCustomOpRuntimeRegistry {23+struct PythonCustomOpAdapterDescriptor {
24- public:24+ std::string op_type;
25- static PythonCustomOpRuntimeRegistry &GetInstance();25+ std::string impl_descriptor_key;
26+ CustomOpCapabilityMask capabilities{0U};
27+};
26 28 
27- static bool Register(const PythonCustomOpDescriptor &desc, const PythonCustomOpCallbacks &callbacks);29+class PythonCustomOpImplRuntimeRegistry {
30+ public:
31+ static PythonCustomOpImplRuntimeRegistry &GetInstance();
32+ 
33+ static bool Register(const PythonCustomOpAdapterDescriptor &desc, const PythonCustomOpAdapterCallbacks &callbacks);
28 static bool Unregister(const std::string &descriptor_key);34 static bool Unregister(const std::string &descriptor_key);
29- static bool Acquire(const PythonCustomOpDescriptor &desc, PythonCustomOpCallbacks &callbacks);35+ static bool Acquire(const PythonCustomOpAdapterDescriptor &desc, PythonCustomOpAdapterCallbacks &callbacks);
30- static void Release(const PythonCustomOpDescriptor &desc);36+ static void Release(const PythonCustomOpAdapterDescriptor &desc);
31 static void Clear();37 static void Clear();
32 38 
33 private:39 private:
34- PythonCustomOpRuntimeRegistry() = default;40+ PythonCustomOpImplRuntimeRegistry() = default;
35- ~PythonCustomOpRuntimeRegistry() = default;41+ ~PythonCustomOpImplRuntimeRegistry() = default;
36};42};
37 43 
38-class PythonCustomOpHolder {44+class PythonCustomOpImplHolder {
39 public:45 public:
40- explicit PythonCustomOpHolder(const PythonCustomOpDescriptor &desc);46+ explicit PythonCustomOpImplHolder(const PythonCustomOpAdapterDescriptor &desc);
41- ~PythonCustomOpHolder();47+ ~PythonCustomOpImplHolder();
42 48 
43- PythonCustomOpHolder(const PythonCustomOpHolder &) = delete;49+ PythonCustomOpImplHolder(const PythonCustomOpImplHolder &) = delete;
44- PythonCustomOpHolder &operator=(const PythonCustomOpHolder &) = delete;50+ PythonCustomOpImplHolder &operator=(const PythonCustomOpImplHolder &) = delete;
45 51 
46 bool IsValid() const;52 bool IsValid() const;
47 void *GetHolder() const;53 void *GetHolder() const;
48- const PythonCustomOpCallbacks &GetCallbacks() const;54+ const PythonCustomOpAdapterCallbacks &GetCallbacks() const;
49- const PythonCustomOpDescriptor &GetDescriptor() const;55+ const PythonCustomOpAdapterDescriptor &GetDescriptor() const;
50 56 
51 private:57 private:
52- PythonCustomOpDescriptor desc_;58+ PythonCustomOpAdapterDescriptor desc_;
53- PythonCustomOpCallbacks callbacks_;59+ PythonCustomOpAdapterCallbacks callbacks_;
54 void *holder_{nullptr};60 void *holder_{nullptr};
55 bool valid_{false};61 bool valid_{false};
56};62};
@@ -63,7 +69,7 @@ class PythonCustomOpAdapter final : public EagerExecuteOp,
63 public ArgsUpdater,69 public ArgsUpdater,
64 public CustomOpCapabilityProvider {70 public CustomOpCapabilityProvider {
65 public:71 public:
66- explicit PythonCustomOpAdapter(PythonCustomOpDescriptor desc);72+ explicit PythonCustomOpAdapter(PythonCustomOpAdapterDescriptor desc);
67 ~PythonCustomOpAdapter() override;73 ~PythonCustomOpAdapter() override;
68 74 
69 bool IsValid() const;75 bool IsValid() const;
@@ -81,8 +87,8 @@ class PythonCustomOpAdapter final : public EagerExecuteOp,
81 private:87 private:
82 graphStatus ReportUnsupported(CustomOpCapability capability, const char *method_name) const;88 graphStatus ReportUnsupported(CustomOpCapability capability, const char *method_name) const;
83 89 
84- PythonCustomOpDescriptor desc_;90+ PythonCustomOpAdapterDescriptor desc_;
85- std::unique_ptr<PythonCustomOpHolder> holder_;91+ std::unique_ptr<PythonCustomOpImplHolder> holder_;
86};92};
87 93 
88void ClearPythonCustomOpRuntimeRegistry();94void ClearPythonCustomOpRuntimeRegistry();
Mruntime/custom_op/python_custom_op_bridge_c_api.h+6-3
@@ -15,11 +15,14 @@
15 15 
16namespace ge {16namespace ge {
17namespace custom_op {17namespace custom_op {
18-struct PythonCustomOpDescriptor;18+struct PythonCustomOpProtoDescriptorView;
19-struct PythonCustomOpCallbacks;19+struct PythonCustomOpAdapterDescriptorView;
20+struct PythonCustomOpAdapterCallbacks;
20 21 
21struct PythonCustomOpRegistrar {22struct PythonCustomOpRegistrar {
22- bool (*register_custom_op)(const PythonCustomOpDescriptor *desc, const PythonCustomOpCallbacks *callbacks);23+ bool (*register_op_proto)(const PythonCustomOpProtoDescriptorView *desc);
24+ bool (*register_op_adapter)(const PythonCustomOpAdapterDescriptorView *desc,
25+ const PythonCustomOpAdapterCallbacks *callbacks);
23};26};
24 27 
25struct PythonCustomOpBridgeArtifactConfig {28struct PythonCustomOpBridgeArtifactConfig {
Aruntime/custom_op/python_custom_op_bridge_descriptors.cc+328-0
@@ -0,0 +1,328 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#include "Python.h"
12+#ifdef ASCEND_CI_LIMITED_PY37
13+#undef PyCFunction_NewEx
14+#endif
15+ 
16+#include <map>
17+ 
18+#include "common/ge_common/debug/ge_log.h"
19+#include "pybind11/embed.h"
20+#include "pybind11/stl.h"
21+#include "runtime/custom_op/python_custom_op_bridge_descriptors.h"
22+ 
23+namespace ge {
24+namespace custom_op {
25+namespace py = pybind11;
26+namespace {
27+constexpr const char *kInterfaceAnnotatedArgs = "annotated_args";
28+constexpr const char *kInterfaceEagerExecute = "eager_execute";
29+ 
30+PythonCustomOpStringView MakeStringView(const std::string &value) {
31+ return PythonCustomOpStringView{value.data(), value.size()};
32+}
33+ 
34+bool IsStrictInt(const py::handle &value) {
35+ return py::isinstance<py::int_>(value) && (!py::isinstance<py::bool_>(value));
36+}
37+ 
38+bool IsStrictFloat(const py::handle &value) {
39+ return py::isinstance<py::float_>(value);
40+}
41+ 
42+Status ParseAttrKind(const std::string &type, uint32_t &kind) {
43+ static const std::map<std::string, uint32_t> kAttrKinds = {
44+ {"VT_INT", kPythonAttrInt},
45+ {"VT_FLOAT", kPythonAttrFloat},
46+ {"VT_BOOL", kPythonAttrBool},
47+ {"VT_STRING", kPythonAttrString},
48+ {"VT_DATA_TYPE", kPythonAttrDataType},
49+ {"VT_TENSOR", kPythonAttrTensor},
50+ {"VT_LIST_INT", kPythonAttrListInt},
51+ {"VT_LIST_FLOAT", kPythonAttrListFloat},
52+ {"VT_LIST_BOOL", kPythonAttrListBool},
53+ {"VT_LIST_STRING", kPythonAttrListString},
54+ {"VT_LIST_DATA_TYPE", kPythonAttrListDataType},
55+ {"VT_LIST_LIST_INT", kPythonAttrListListInt},
56+ };
57+ const auto iter = kAttrKinds.find(type);
58+ if (iter == kAttrKinds.cend()) {
59+ return FAILED;
60+ }
61+ kind = iter->second;
62+ return SUCCESS;
63+}
64+ 
65+Status ParseInterfaces(const py::object &interfaces_obj, CustomOpCapabilityMask &capabilities) {
66+ capabilities = 0U;
67+ for (const auto &item : interfaces_obj.cast<py::list>()) {
68+ const std::string interface_name = py::str(item);
69+ if (interface_name == kInterfaceEagerExecute) {
70+ AddCustomOpCapability(capabilities, CustomOpCapability::kEagerExecute);
71+ } else if (interface_name == kInterfaceAnnotatedArgs) {
72+ AddCustomOpCapability(capabilities, CustomOpCapability::kAnnotatedArgs);
73+ } else {
74+ GELOGE(FAILED, "Unsupported python custom op interface[%s].", interface_name.c_str());
75+ return FAILED;
76+ }
77+ }
78+ return capabilities == 0U ? FAILED : SUCCESS;
79+}
80+} // namespace
81+ 
82+Status ProtoAttrStorage::Parse(const py::dict &item) {
83+ try {
84+ name = py::str(item["name"]);
85+ type = py::str(item["type"]);
86+ if (ParseAttrKind(type, kind) != SUCCESS) {
87+ return FAILED;
88+ }
89+ const auto required_obj = item["is_required"];
90+ if (!py::isinstance<py::bool_>(required_obj)) {
91+ return FAILED;
92+ }
93+ is_required = required_obj.cast<bool>();
94+ const auto value = item["default"];
95+ if (is_required) {
96+ return value.is_none() ? SUCCESS : FAILED;
97+ }
98+ if (value.is_none() || (kind == kPythonAttrTensor)) {
99+ return FAILED;
100+ }
101+ switch (kind) {
102+ case kPythonAttrInt:
103+ if (!IsStrictInt(value)) {
104+ return FAILED;
105+ }
106+ int_value = value.cast<int64_t>();
107+ return SUCCESS;
108+ case kPythonAttrFloat:
109+ if (!IsStrictFloat(value)) {
110+ return FAILED;
111+ }
112+ float_value = value.cast<double>();
113+ return SUCCESS;
114+ case kPythonAttrBool:
115+ if (!py::isinstance<py::bool_>(value)) {
116+ return FAILED;
117+ }
118+ bool_value = value.cast<bool>() ? 1U : 0U;
119+ return SUCCESS;
120+ case kPythonAttrString:
121+ if (!py::isinstance<py::str>(value)) {
122+ return FAILED;
123+ }
124+ string_value = py::str(value);
125+ return SUCCESS;
126+ case kPythonAttrDataType:
127+ if (!IsStrictInt(value)) {
128+ return FAILED;
129+ }
130+ data_type_value = value.cast<int32_t>();
131+ return SUCCESS;
132+ default:
133+ return ParseList(value);
134+ }
135+ } catch (const py::error_already_set &err) {
136+ GELOGE(FAILED, "Parse python custom op attr failed: %s", err.what());
137+ return FAILED;
138+ } catch (const std::exception &err) {
139+ GELOGE(FAILED, "Parse python custom op attr failed: %s", err.what());
140+ return FAILED;
141+ }
142+}
143+ 
144+Status ProtoAttrStorage::ParseList(const py::handle &value) {
145+ if (!py::isinstance<py::list>(value)) {
146+ return FAILED;
147+ }
148+ const py::list values = py::reinterpret_borrow<py::list>(value);
149+ for (const auto &element : values) {
150+ switch (kind) {
151+ case kPythonAttrListInt:
152+ if (!IsStrictInt(element)) {
153+ return FAILED;
154+ }
155+ list_int_values.emplace_back(element.cast<int64_t>());
156+ break;
157+ case kPythonAttrListFloat:
158+ if (!IsStrictFloat(element)) {
159+ return FAILED;
160+ }
161+ list_float_values.emplace_back(element.cast<double>());
162+ break;
163+ case kPythonAttrListBool:
164+ if (!py::isinstance<py::bool_>(element)) {
165+ return FAILED;
166+ }
167+ list_bool_values.emplace_back(element.cast<bool>() ? 1U : 0U);
168+ break;
169+ case kPythonAttrListString:
170+ if (!py::isinstance<py::str>(element)) {
171+ return FAILED;
172+ }
173+ list_string_values.emplace_back(py::str(element));
174+ break;
175+ case kPythonAttrListDataType:
176+ if (!IsStrictInt(element)) {
177+ return FAILED;
178+ }
179+ list_data_type_values.emplace_back(element.cast<int32_t>());
180+ break;
181+ case kPythonAttrListListInt: {
182+ if (!py::isinstance<py::list>(element)) {
183+ return FAILED;
184+ }
185+ std::vector<int64_t> row;
186+ for (const auto &row_element : element.cast<py::list>()) {
187+ if (!IsStrictInt(row_element)) {
188+ return FAILED;
189+ }
190+ row.emplace_back(row_element.cast<int64_t>());
191+ }
192+ list_list_int_values.emplace_back(std::move(row));
193+ break;
194+ }
195+ default:
196+ return FAILED;
197+ }
198+ }
199+ return SUCCESS;
200+}
201+ 
202+PythonCustomOpProtoAttrView ProtoAttrStorage::BuildView() {
203+ PythonCustomOpAttrDefaultView default_view{};
204+ default_view.has_value = is_required ? 0U : 1U;
205+ default_view.int_value = int_value;
206+ default_view.float_value = float_value;
207+ default_view.bool_value = bool_value;
208+ default_view.string_value = MakeStringView(string_value);
209+ default_view.data_type_value = data_type_value;
210+ default_view.list_int_values = list_int_values.empty() ? nullptr : list_int_values.data();
211+ default_view.list_float_values = list_float_values.empty() ? nullptr : list_float_values.data();
212+ default_view.list_bool_values = list_bool_values.empty() ? nullptr : list_bool_values.data();
213+ list_string_views.clear();
214+ list_string_views.reserve(list_string_values.size());
215+ for (const auto &value : list_string_values) {
216+ list_string_views.emplace_back(MakeStringView(value));
217+ }
218+ default_view.list_string_values = list_string_views.empty() ? nullptr : list_string_views.data();
219+ default_view.list_data_type_values = list_data_type_values.empty() ? nullptr : list_data_type_values.data();
220+ list_list_int_views.clear();
221+ list_list_int_views.reserve(list_list_int_values.size());
222+ for (const auto &row : list_list_int_values) {
223+ list_list_int_views.emplace_back(PythonCustomOpInt64ArrayView{row.empty() ? nullptr : row.data(), row.size()});
224+ }
225+ default_view.list_list_int_values = list_list_int_views.empty() ? nullptr : list_list_int_views.data();
226+ switch (kind) {
227+ case kPythonAttrListInt:
228+ default_view.count = list_int_values.size();
229+ break;
230+ case kPythonAttrListFloat:
231+ default_view.count = list_float_values.size();
232+ break;
233+ case kPythonAttrListBool:
234+ default_view.count = list_bool_values.size();
235+ break;
236+ case kPythonAttrListString:
237+ default_view.count = list_string_values.size();
238+ break;
239+ case kPythonAttrListDataType:
240+ default_view.count = list_data_type_values.size();
241+ break;
242+ case kPythonAttrListListInt:
243+ default_view.count = list_list_int_values.size();
244+ break;
245+ default:
246+ break;
247+ }
248+ return PythonCustomOpProtoAttrView{MakeStringView(name), kind, static_cast<uint8_t>(is_required ? 1U : 0U),
249+ default_view};
250+}
251+ 
252+Status ProtoDescriptorStorage::Parse(const py::dict &dict) {
253+ try {
254+ descriptor_key = py::str(dict["descriptor_key"]);
255+ op_type = py::str(dict["op_type"]);
256+ for (const auto &item : dict["inputs"].cast<py::list>()) {
257+ const auto input = item.cast<py::dict>();
258+ inputs.emplace_back(ProtoInputStorage{py::str(input["name"]), input["kind"].cast<uint32_t>()});
259+ }
260+ for (const auto &item : dict["attrs"].cast<py::list>()) {
261+ ProtoAttrStorage attr;
262+ if (attr.Parse(item.cast<py::dict>()) != SUCCESS) {
263+ return FAILED;
264+ }
265+ attrs.emplace_back(std::move(attr));
266+ }
267+ for (const auto &item : dict["outputs"].cast<py::list>()) {
268+ const auto output = item.cast<py::dict>();
269+ outputs.emplace_back(ProtoOutputStorage{py::str(output["name"]), output["kind"].cast<uint32_t>()});
270+ }
271+ } catch (const py::error_already_set &err) {
272+ GELOGE(FAILED, "Parse python custom op proto descriptor failed: %s", err.what());
273+ return FAILED;
274+ } catch (const std::exception &err) {
275+ GELOGE(FAILED, "Parse python custom op proto descriptor failed: %s", err.what());
276+ return FAILED;
277+ }
278+ return SUCCESS;
279+}
280+ 
281+PythonCustomOpProtoDescriptorView ProtoDescriptorStorage::BuildView() {
282+ input_views.clear();
283+ input_views.reserve(inputs.size());
284+ for (const auto &input : inputs) {
285+ input_views.emplace_back(PythonCustomOpProtoInputView{MakeStringView(input.name), input.kind});
286+ }
287+ attr_views.clear();
288+ attr_views.reserve(attrs.size());
289+ for (auto &attr : attrs) {
290+ attr_views.emplace_back(attr.BuildView());
291+ }
292+ output_views.clear();
293+ output_views.reserve(outputs.size());
294+ for (const auto &output : outputs) {
295+ output_views.emplace_back(PythonCustomOpProtoOutputView{MakeStringView(output.name), output.kind});
296+ }
297+ return PythonCustomOpProtoDescriptorView{
298+ MakeStringView(descriptor_key),
299+ MakeStringView(op_type),
300+ input_views.empty() ? nullptr : input_views.data(),
301+ input_views.size(),
302+ attr_views.empty() ? nullptr : attr_views.data(),
303+ attr_views.size(),
304+ output_views.empty() ? nullptr : output_views.data(),
305+ output_views.size(),
306+ };
307+}
308+ 
309+Status AdapterDescriptorStorage::Parse(const py::dict &dict) {
310+ try {
311+ op_type = py::str(dict["op_type"]);
312+ impl_descriptor_key = py::str(dict["descriptor_key"]);
313+ return ParseInterfaces(dict["interfaces"], capabilities);
314+ } catch (const py::error_already_set &err) {
315+ GELOGE(FAILED, "Parse python custom op adapter descriptor failed: %s", err.what());
316+ return FAILED;
317+ } catch (const std::exception &err) {
318+ GELOGE(FAILED, "Parse python custom op adapter descriptor failed: %s", err.what());
319+ return FAILED;
320+ }
321+}
322+ 
323+PythonCustomOpAdapterDescriptorView AdapterDescriptorStorage::BuildView() const {
324+ return PythonCustomOpAdapterDescriptorView{MakeStringView(op_type), MakeStringView(impl_descriptor_key),
325+ capabilities};
326+}
327+} // namespace custom_op
328+} // namespace ge
Aruntime/custom_op/python_custom_op_bridge_descriptors.h+82-0
@@ -0,0 +1,82 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#ifndef CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_BRIDGE_DESCRIPTORS_H_
12+#define CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_BRIDGE_DESCRIPTORS_H_
13+ 
14+#include <cstdint>
15+#include <string>
16+#include <vector>
17+ 
18+#include "pybind11/pytypes.h"
19+#include "runtime/custom_op/python_custom_op_bridge_types.h"
20+ 
21+namespace ge {
22+namespace custom_op {
23+struct ProtoInputStorage {
24+ std::string name;
25+ uint32_t kind{0U};
26+};
27+ 
28+struct ProtoOutputStorage {
29+ std::string name;
30+ uint32_t kind{0U};
31+};
32+ 
33+struct ProtoAttrStorage {
34+ std::string name;
35+ std::string type;
36+ uint32_t kind{0U};
37+ bool is_required{true};
38+ int64_t int_value{0};
39+ double float_value{0.0};
40+ uint8_t bool_value{0U};
41+ std::string string_value;
42+ int32_t data_type_value{0};
43+ std::vector<int64_t> list_int_values;
44+ std::vector<double> list_float_values;
45+ std::vector<uint8_t> list_bool_values;
46+ std::vector<std::string> list_string_values;
47+ std::vector<PythonCustomOpStringView> list_string_views;
48+ std::vector<int32_t> list_data_type_values;
49+ std::vector<std::vector<int64_t>> list_list_int_values;
50+ std::vector<PythonCustomOpInt64ArrayView> list_list_int_views;
51+ 
52+ Status Parse(const pybind11::dict &item);
53+ Status ParseList(const pybind11::handle &value);
54+ PythonCustomOpProtoAttrView BuildView();
55+};
56+ 
57+struct ProtoDescriptorStorage {
58+ std::string descriptor_key;
59+ std::string op_type;
60+ std::vector<ProtoInputStorage> inputs;
61+ std::vector<ProtoAttrStorage> attrs;
62+ std::vector<ProtoOutputStorage> outputs;
63+ std::vector<PythonCustomOpProtoInputView> input_views;
64+ std::vector<PythonCustomOpProtoAttrView> attr_views;
65+ std::vector<PythonCustomOpProtoOutputView> output_views;
66+ 
67+ Status Parse(const pybind11::dict &dict);
68+ PythonCustomOpProtoDescriptorView BuildView();
69+};
70+ 
71+struct AdapterDescriptorStorage {
72+ std::string op_type;
73+ std::string impl_descriptor_key;
74+ CustomOpCapabilityMask capabilities{0U};
75+ 
76+ Status Parse(const pybind11::dict &dict);
77+ PythonCustomOpAdapterDescriptorView BuildView() const;
78+};
79+} // namespace custom_op
80+} // namespace ge
81+ 
82+#endif // CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_BRIDGE_DESCRIPTORS_H_
Mruntime/custom_op/python_custom_op_bridge_loader.cc+109-33
@@ -16,10 +16,12 @@
16 16 
17#include <cstdlib>17#include <cstdlib>
18#include <cstring>18#include <cstring>
19+#include <exception>
19#include <map>20#include <map>
20#include <mutex>21#include <mutex>
21#include <new>22#include <new>
22#include <string>23#include <string>
24+#include <utility>
23#include <vector>25#include <vector>
24 26 
25#include "common/ge_common/string_util.h"27#include "common/ge_common/string_util.h"
@@ -32,6 +34,7 @@
32#include "mmpa/mmpa_api.h"34#include "mmpa/mmpa_api.h"
33#include "runtime/custom_op/python_custom_op_adapter.h"35#include "runtime/custom_op/python_custom_op_adapter.h"
34#include "runtime/custom_op/python_custom_op_bridge_c_api.h"36#include "runtime/custom_op/python_custom_op_bridge_c_api.h"
37+#include "runtime/custom_op/python_custom_op_proto.h"
35 38 
36namespace ge {39namespace ge {
37namespace custom_op {40namespace custom_op {
@@ -174,12 +177,11 @@ class PythonCustomOpBridgeLoader {
174 void Unload() {177 void Unload() {
duhua
duhuaduhua13 天前

是不是在原来的逻辑基础上补充原型的清理逻辑就行呀?

likedislike
lfz2812
13 天前 评论:
lfz2812
12 天前 评论:
175 std::lock_guard<std::mutex> lock(mutex_);178 std::lock_guard<std::mutex> lock(mutex_);
176 GELOGI("Unload python custom ops with bridge library[%s].", loaded_path_.c_str());179 GELOGI("Unload python custom ops with bridge library[%s].", loaded_path_.c_str());
177- CustomOpFactory::RemoveCustomOps(GetRegisteredOpTypes());180+ ClearPythonCustomOpRegistrations();
181+ ClearRegisteredState();
178 if ((api_ != nullptr) && (api_->reset_bridge_state != nullptr)) {182 if ((api_ != nullptr) && (api_->reset_bridge_state != nullptr)) {
179 api_->reset_bridge_state();183 api_->reset_bridge_state();
180 }184 }
181- ClearPythonCustomOpRuntimeRegistry();
182- ClearRegisteredState();
183 }185 }
184 186 
185 void ShutdownForProcess() {187 void ShutdownForProcess() {
@@ -200,9 +202,26 @@ class PythonCustomOpBridgeLoader {
200 }202 }
201 203 
202 private:204 private:
205+ void ClearPythonCustomOpRegistrations() {
206+ std::vector<AscendString> adapter_op_types;
207+ adapter_op_types.reserve(registered_op_type_to_impl_descriptor_key_.size());
208+ for (const auto &item : registered_op_type_to_impl_descriptor_key_) {
209+ adapter_op_types.emplace_back(item.first.c_str());
210+ }
211+ CustomOpFactory::RemoveCustomOps(adapter_op_types);
212+ ClearPythonCustomOpRuntimeRegistry();
213+ std::vector<std::string> proto_op_types;
214+ proto_op_types.reserve(registered_op_type_to_proto_key_.size());
215+ for (const auto &item : registered_op_type_to_proto_key_) {
216+ proto_op_types.emplace_back(item.first);
217+ }
218+ UnregisterPythonCustomOpProtos(proto_op_types);
219+ }
220+ 
203 Status RegisterCustomOpsFromBridge() {221 Status RegisterCustomOpsFromBridge() {
204 static constexpr PythonCustomOpRegistrar kRegistrar = {222 static constexpr PythonCustomOpRegistrar kRegistrar = {
205- &RegisterCustomOpFromBridge,223+ &RegisterOpProtoFromBridge,
224+ &RegisterOpAdapterFromBridge,
206 };225 };
207 GELOGI("Register python custom ops with bridge library[%s].", loaded_path_.c_str());226 GELOGI("Register python custom ops with bridge library[%s].", loaded_path_.c_str());
208 const auto ret = api_->register_custom_ops(&kRegistrar);227 const auto ret = api_->register_custom_ops(&kRegistrar);
@@ -213,29 +232,93 @@ class PythonCustomOpBridgeLoader {
213 return SUCCESS;232 return SUCCESS;
214 }233 }
215 234 
216- static bool RegisterCustomOpFromBridge(const PythonCustomOpDescriptor *desc,235+ static bool RegisterOpProtoFromBridge(const PythonCustomOpProtoDescriptorView *desc) {
217- const PythonCustomOpCallbacks *callbacks) {236+ if (desc == nullptr) {
237+ return false;
238+ }
239+ return GetInstance().RegisterOpProto(*desc);
240+ }
241+ 
242+ static bool RegisterOpAdapterFromBridge(const PythonCustomOpAdapterDescriptorView *desc,
243+ const PythonCustomOpAdapterCallbacks *callbacks) {
218 if ((desc == nullptr) || (callbacks == nullptr)) {244 if ((desc == nullptr) || (callbacks == nullptr)) {
219 return false;245 return false;
220 }246 }
221- return GetInstance().RegisterCustomOp(*desc, *callbacks);247+ return GetInstance().RegisterOpAdapter(*desc, *callbacks);
222 }248 }
223 249 
224- bool RegisterCustomOp(const PythonCustomOpDescriptor &desc, const PythonCustomOpCallbacks &callbacks) {250+ bool RegisterOpProto(const PythonCustomOpProtoDescriptorView &view) {
225- const auto registered_op_type_iter = registered_op_type_to_descriptor_key_.find(desc.op_type);251+ PythonCustomOpProto proto;
226- if (registered_op_type_iter != registered_op_type_to_descriptor_key_.end()) {252+ if (ParsePythonCustomOpProto(view, proto) != GRAPH_SUCCESS) {
227- if (registered_op_type_iter->second == desc.descriptor_key) {253+ GELOGE(FAILED, "[Parse][PythonCustomOpProto] failed.");
254+ return false;
255+ }
256+ const auto existing = registered_op_type_to_proto_key_.find(proto.op_type);
257+ if (existing != registered_op_type_to_proto_key_.cend()) {
258+ if (existing->second == proto.descriptor_key) {
228 return true;259 return true;
229 }260 }
230 GELOGE(FAILED,261 GELOGE(FAILED,
231- "[Check][PythonCustomOp]Op type[%s] has been registered with descriptor key[%s] by python custom op, "262+ "Python custom op proto conflict, op type[%s], existing source[descriptor key:%s], "
232- "current descriptor key is[%s].",263+ "current source[descriptor key:%s].",
233- desc.op_type.c_str(), registered_op_type_iter->second.c_str(), desc.descriptor_key.c_str());264+ proto.op_type.c_str(), existing->second.c_str(), proto.descriptor_key.c_str());
234 return false;265 return false;
235 }266 }
236- if (!PythonCustomOpRuntimeRegistry::Register(desc, callbacks)) {267+ if (RegisterPythonCustomOpProto(proto) != GRAPH_SUCCESS) {
237- GELOGE(FAILED, "[Register][PythonCustomOpRuntimeRegistry] failed, descriptor key[%s], op type[%s].",268+ GELOGE(FAILED, "[Register][PythonCustomOpProto] failed, descriptor key[%s], op type[%s].",
238- desc.descriptor_key.c_str(), desc.op_type.c_str());269+ proto.descriptor_key.c_str(), proto.op_type.c_str());
270+ return false;
271+ }
272+ (void)registered_op_type_to_proto_key_.emplace(proto.op_type, proto.descriptor_key);
273+ return true;
274+ }
275+ 
276+ static bool CopyStringView(const PythonCustomOpStringView &view, const bool allow_empty, std::string &value) {
277+ if ((view.size != 0U) && (view.data == nullptr)) {
278+ return false;
279+ }
280+ value.assign(view.data == nullptr ? "" : view.data, view.size);
281+ return (allow_empty || (!value.empty())) && (value.find('\0') == std::string::npos);
282+ }
283+ 
284+ static bool ParseAdapterDescriptor(const PythonCustomOpAdapterDescriptorView &view,
285+ PythonCustomOpAdapterDescriptor &desc) {
286+ if ((!CopyStringView(view.op_type, false, desc.op_type)) ||
287+ (!CopyStringView(view.impl_descriptor_key, false, desc.impl_descriptor_key))) {
288+ return false;
289+ }
290+ desc.capabilities = view.capabilities;
291+ return true;
292+ }
293+ 
294+ bool RegisterOpAdapter(const PythonCustomOpAdapterDescriptorView &view,
295+ const PythonCustomOpAdapterCallbacks &callbacks) {
296+ PythonCustomOpAdapterDescriptor desc;
297+ if (!ParseAdapterDescriptor(view, desc)) {
298+ GELOGE(FAILED, "[Parse][PythonCustomOpAdapter] failed.");
299+ return false;
300+ }
301+ const auto existing = registered_op_type_to_impl_descriptor_key_.find(desc.op_type);
302+ if (existing != registered_op_type_to_impl_descriptor_key_.cend()) {
303+ if (existing->second == desc.impl_descriptor_key) {
304+ return true;
305+ }
306+ GELOGE(FAILED,
307+ "Python custom op adapter conflict, op type[%s], existing source[impl key:%s], "
308+ "current source[impl key:%s].",
309+ desc.op_type.c_str(), existing->second.c_str(), desc.impl_descriptor_key.c_str());
310+ return false;
311+ }
312+ if (CustomOpFactory::IsExistOp(AscendString(desc.op_type.c_str()))) {
313+ GELOGE(FAILED,
314+ "[Check][PythonCustomOpAdapter]Op type[%s] conflicts, existing source[CustomOpFactory creator], "
315+ "current source[Python impl descriptor key:%s].",
316+ desc.op_type.c_str(), desc.impl_descriptor_key.c_str());
317+ return false;
318+ }
319+ if (!PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks)) {
320+ GELOGE(FAILED, "[Register][PythonCustomOpImplRuntimeRegistry] failed, descriptor key[%s], op type[%s].",
321+ desc.impl_descriptor_key.c_str(), desc.op_type.c_str());
239 return false;322 return false;
240 }323 }
241 324 
@@ -249,28 +332,20 @@ class PythonCustomOpBridgeLoader {
249 return std::unique_ptr<BaseCustomOp>(adapter);332 return std::unique_ptr<BaseCustomOp>(adapter);
250 });333 });
251 if (ret != GRAPH_SUCCESS) {334 if (ret != GRAPH_SUCCESS) {
252- (void)PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key);335+ (void)PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key);
253 GELOGE(FAILED, "[Register][PythonCustomOpCreator] failed, descriptor key[%s], op type[%s].",336 GELOGE(FAILED, "[Register][PythonCustomOpCreator] failed, descriptor key[%s], op type[%s].",
254- desc.descriptor_key.c_str(), desc.op_type.c_str());337+ desc.impl_descriptor_key.c_str(), desc.op_type.c_str());
255 return false;338 return false;
256 }339 }
257- registered_op_type_to_descriptor_key_[desc.op_type] = desc.descriptor_key;340+ (void)registered_op_type_to_impl_descriptor_key_.emplace(desc.op_type, desc.impl_descriptor_key);
258- GELOGI("Python custom op[%s] is registered from bridge, descriptor key[%s].", desc.op_type.c_str(),341+ GELOGI("Python custom op[%s] adapter is registered, impl key[%s].", desc.op_type.c_str(),
259- desc.descriptor_key.c_str());342+ desc.impl_descriptor_key.c_str());
260 return true;343 return true;
261 }344 }
262 345 
263 void ClearRegisteredState() {346 void ClearRegisteredState() {
264- registered_op_type_to_descriptor_key_.clear();347+ registered_op_type_to_proto_key_.clear();
265- }348+ registered_op_type_to_impl_descriptor_key_.clear();
266- 
267- std::vector<AscendString> GetRegisteredOpTypes() const {
268- std::vector<AscendString> op_types;
269- op_types.reserve(registered_op_type_to_descriptor_key_.size());
270- for (const auto &item : registered_op_type_to_descriptor_key_) {
271- op_types.emplace_back(AscendString(item.first.c_str()));
272- }
273- return op_types;
274 }349 }
275 350 
276 Status EnsureLoaded() {351 Status EnsureLoaded() {
@@ -333,7 +408,8 @@ class PythonCustomOpBridgeLoader {
333 void *handle_{nullptr};408 void *handle_{nullptr};
334 const PythonCustomOpBridgeApi *api_{nullptr};409 const PythonCustomOpBridgeApi *api_{nullptr};
335 std::string loaded_path_;410 std::string loaded_path_;
336- std::map<std::string, std::string> registered_op_type_to_descriptor_key_;411+ std::map<std::string, std::string> registered_op_type_to_proto_key_;
412+ std::map<std::string, std::string> registered_op_type_to_impl_descriptor_key_;
337};413};
338} // namespace414} // namespace
339 415 
Mruntime/custom_op/python_custom_op_bridge_types.h+94-17
@@ -11,8 +11,8 @@
11#ifndef CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_BRIDGE_TYPES_H_11#ifndef CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_BRIDGE_TYPES_H_
12#define CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_BRIDGE_TYPES_H_12#define CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_BRIDGE_TYPES_H_
13 13 
14+#include <cstddef>
14#include <cstdint>15#include <cstdint>
15-#include <string>
16 16 
17#include "graph/custom_op/capability.h"17#include "graph/custom_op/capability.h"
18#include "graph/error_codes.h"18#include "graph/error_codes.h"
@@ -24,25 +24,102 @@ class EagerOpExecutionContext;
24 24 
25namespace ge {25namespace ge {
26namespace custom_op {26namespace custom_op {
27-struct PythonCustomOpDescriptor {27+struct PythonCustomOpStringView {
28- std::string descriptor_key;28+ const char *data;
29- std::string op_type;29+ size_t size;
30- CustomOpCapabilityMask capabilities{0U};
31};30};
32 31 
33-// IR 原型由 Python bridge 通过 run 包正式公开的 GetRegisteredIrDef 接口获取并在 bridge 内部缓存,32+enum PythonCustomOpProtoInputKind : uint32_t {
34-// 不再通过 runtime/bridge callback 传递 IR 的私有 POD 投影。33+ kPythonInputRequired = 0U,
34+ kPythonInputOptional = 1U,
35+ kPythonInputDynamic = 2U,
36+};
35 37 
36-using PythonCustomOpHolderCreateFn = void *(*)(const PythonCustomOpDescriptor *desc);38+enum PythonCustomOpProtoOutputKind : uint32_t {
37-using PythonCustomOpHolderDestroyFn = void (*)(void *holder);39+ kPythonOutputRequired = 0U,
38-using PythonCustomOpExecuteFn = graphStatus (*)(const void *holder, gert::EagerOpExecutionContext *ctx);40+ kPythonOutputDynamic = 1U,
39-using PythonCustomOpDeclareLaunchArgsFn = graphStatus (*)(const void *holder, gert::AnnotatedArgsContext *ctx);41+};
40 42 
41-struct PythonCustomOpCallbacks {43+enum PythonCustomOpProtoAttrKind : uint32_t {
42- PythonCustomOpHolderCreateFn create{nullptr};44+ kPythonAttrInt = 0U,
43- PythonCustomOpHolderDestroyFn destroy{nullptr};45+ kPythonAttrFloat = 1U,
44- PythonCustomOpExecuteFn execute{nullptr};46+ kPythonAttrBool = 2U,
45- PythonCustomOpDeclareLaunchArgsFn declare_launch_args{nullptr};47+ kPythonAttrString = 3U,
48+ kPythonAttrDataType = 4U,
49+ kPythonAttrTensor = 5U,
50+ kPythonAttrListInt = 6U,
51+ kPythonAttrListFloat = 7U,
52+ kPythonAttrListBool = 8U,
53+ kPythonAttrListString = 9U,
54+ kPythonAttrListDataType = 10U,
55+ kPythonAttrListListInt = 11U,
56+};
57+ 
58+struct PythonCustomOpInt64ArrayView {
59+ const int64_t *data;
60+ size_t count;
61+};
62+ 
63+struct PythonCustomOpAttrDefaultView {
64+ uint8_t has_value;
65+ int64_t int_value;
66+ double float_value;
67+ uint8_t bool_value;
68+ PythonCustomOpStringView string_value;
69+ int32_t data_type_value;
70+ const int64_t *list_int_values;
71+ const double *list_float_values;
72+ const uint8_t *list_bool_values;
73+ const PythonCustomOpStringView *list_string_values;
74+ const int32_t *list_data_type_values;
75+ const PythonCustomOpInt64ArrayView *list_list_int_values;
76+ size_t count;
77+};
78+ 
79+struct PythonCustomOpProtoInputView {
80+ PythonCustomOpStringView name;
81+ uint32_t kind;
82+};
83+ 
84+struct PythonCustomOpProtoAttrView {
85+ PythonCustomOpStringView name;
86+ uint32_t kind;
87+ uint8_t is_required;
88+ PythonCustomOpAttrDefaultView default_value;
89+};
90+ 
91+struct PythonCustomOpProtoOutputView {
92+ PythonCustomOpStringView name;
93+ uint32_t kind;
94+};
95+ 
96+struct PythonCustomOpProtoDescriptorView {
97+ PythonCustomOpStringView descriptor_key;
98+ PythonCustomOpStringView op_type;
99+ const PythonCustomOpProtoInputView *inputs;
100+ size_t input_count;
101+ const PythonCustomOpProtoAttrView *attrs;
102+ size_t attr_count;
103+ const PythonCustomOpProtoOutputView *outputs;
104+ size_t output_count;
105+};
106+ 
107+struct PythonCustomOpAdapterDescriptorView {
108+ PythonCustomOpStringView op_type;
109+ PythonCustomOpStringView impl_descriptor_key;
110+ CustomOpCapabilityMask capabilities;
111+};
112+ 
113+using PythonCustomOpImplHolderCreateFn = void *(*)(const PythonCustomOpAdapterDescriptorView *desc);
114+using PythonCustomOpImplHolderDestroyFn = void (*)(void *holder);
115+using PythonCustomOpImplExecuteFn = graphStatus (*)(const void *holder, gert::EagerOpExecutionContext *ctx);
116+using PythonCustomOpImplDeclareLaunchArgsFn = graphStatus (*)(const void *holder, gert::AnnotatedArgsContext *ctx);
117+ 
118+struct PythonCustomOpAdapterCallbacks {
119+ PythonCustomOpImplHolderCreateFn create_impl_holder{nullptr};
120+ PythonCustomOpImplHolderDestroyFn destroy_impl_holder{nullptr};
121+ PythonCustomOpImplExecuteFn execute{nullptr};
122+ PythonCustomOpImplDeclareLaunchArgsFn declare_launch_args{nullptr};
46 123 
47 bool IsValid(CustomOpCapabilityMask capabilities) const {124 bool IsValid(CustomOpCapabilityMask capabilities) const {
48 const auto supported_capabilities = static_cast<CustomOpCapabilityMask>(CustomOpCapability::kEagerExecute) |125 const auto supported_capabilities = static_cast<CustomOpCapabilityMask>(CustomOpCapability::kEagerExecute) |
@@ -50,7 +127,7 @@ struct PythonCustomOpCallbacks {
50 if ((capabilities == 0U) || ((capabilities & (~supported_capabilities)) != 0U)) {127 if ((capabilities == 0U) || ((capabilities & (~supported_capabilities)) != 0U)) {
51 return false;128 return false;
52 }129 }
53- if ((create == nullptr) || (destroy == nullptr)) {130+ if ((create_impl_holder == nullptr) || (destroy_impl_holder == nullptr)) {
54 return false;131 return false;
55 }132 }
56 if (HasCustomOpCapability(capabilities, CustomOpCapability::kEagerExecute) && (execute == nullptr)) {133 if (HasCustomOpCapability(capabilities, CustomOpCapability::kEagerExecute) && (execute == nullptr)) {
Aruntime/custom_op/python_custom_op_proto.cc+484-0
@@ -0,0 +1,484 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#include "runtime/custom_op/python_custom_op_proto.h"
12+ 
13+#include <cmath>
14+#include <limits>
15+#include <map>
16+#include <memory>
17+#include <set>
18+#include <utility>
19+ 
20+#include "framework/common/debug/ge_log.h"
21+#include "graph/ascend_string.h"
22+#include "graph/attr_value.h"
23+#include "graph/debug/ge_util.h"
24+#include "graph/operator.h"
25+#include "graph/operator_factory.h"
26+#include "graph/operator_factory_impl.h"
27+#include "graph/custom_op_factory.h"
28+ 
29+namespace ge {
30+namespace custom_op {
31+namespace {
32+bool CopyString(const PythonCustomOpStringView &view, const bool allow_empty, std::string &value) {
33+ if ((view.size != 0U) && (view.data == nullptr)) {
34+ return false;
35+ }
36+ value.assign(view.data == nullptr ? "" : view.data, view.size);
37+ return (allow_empty || (!value.empty())) && (value.find('\0') == std::string::npos);
38+}
39+ 
40+bool IsValidArray(const void *data, const size_t count) {
41+ return (count == 0U) || (data != nullptr);
42+}
43+ 
44+bool IsValidDataType(const int32_t value) {
45+ return (value >= 0) && (value < static_cast<int32_t>(ge::DT_MAX));
46+}
47+ 
48+graphStatus ConvertInputKind(const uint32_t kind, ge::IrInputType &converted) {
49+ switch (kind) {
50+ case kPythonInputRequired:
51+ converted = ge::kIrInputRequired;
52+ return GRAPH_SUCCESS;
53+ case kPythonInputOptional:
54+ converted = ge::kIrInputOptional;
55+ return GRAPH_SUCCESS;
56+ case kPythonInputDynamic:
57+ converted = ge::kIrInputDynamic;
58+ return GRAPH_SUCCESS;
59+ default:
60+ return GRAPH_PARAM_INVALID;
61+ }
62+}
63+ 
64+graphStatus ConvertOutputKind(const uint32_t kind, ge::IrOutputType &converted) {
65+ switch (kind) {
66+ case kPythonOutputRequired:
67+ converted = ge::kIrOutputRequired;
68+ return GRAPH_SUCCESS;
69+ case kPythonOutputDynamic:
70+ converted = ge::kIrOutputDynamic;
71+ return GRAPH_SUCCESS;
72+ default:
73+ return GRAPH_PARAM_INVALID;
74+ }
75+}
76+ 
77+const char *GetRequiredAttrToken(const uint32_t kind) {
78+ static const std::map<uint32_t, const char *> kTokens = {
79+ {kPythonAttrInt, "Int"},
80+ {kPythonAttrFloat, "Float"},
81+ {kPythonAttrBool, "Bool"},
82+ {kPythonAttrString, "String"},
83+ {kPythonAttrDataType, "Type"},
84+ {kPythonAttrTensor, "Tensor"},
85+ {kPythonAttrListInt, "ListInt"},
86+ {kPythonAttrListFloat, "ListFloat"},
87+ {kPythonAttrListBool, "ListBool"},
88+ {kPythonAttrListString, "ListString"},
89+ {kPythonAttrListDataType, "ListType"},
90+ {kPythonAttrListListInt, "ListListInt"},
91+ };
92+ const auto iter = kTokens.find(kind);
93+ return (iter == kTokens.cend()) ? nullptr : iter->second;
94+}
95+ 
96+graphStatus ParseStringArray(const PythonCustomOpStringView *data, const size_t count,
97+ std::vector<std::string> &values) {
98+ if (!IsValidArray(data, count)) {
99+ return GRAPH_PARAM_INVALID;
100+ }
101+ values.reserve(count);
102+ for (size_t i = 0U; i < count; ++i) {
103+ std::string value;
104+ if (!CopyString(data[i], true, value)) {
105+ return GRAPH_PARAM_INVALID;
106+ }
107+ values.emplace_back(std::move(value));
108+ }
109+ return GRAPH_SUCCESS;
110+}
111+ 
112+graphStatus ParseOptionalAttrDefinition(const PythonCustomOpProtoAttrView &view, PythonCustomOpAttr &attr) {
113+ const auto &source = view.default_value;
114+ auto &definition = attr.default_definition;
115+ switch (view.kind) {
116+ case kPythonAttrInt:
117+ definition.int_value = source.int_value;
118+ return GRAPH_SUCCESS;
119+ case kPythonAttrFloat: {
120+ if (std::isfinite(source.float_value) &&
121+ (std::fabs(source.float_value) > static_cast<double>(std::numeric_limits<float32_t>::max()))) {
122+ return GRAPH_PARAM_INVALID;
123+ }
124+ definition.float_value = source.float_value;
125+ return GRAPH_SUCCESS;
126+ }
127+ case kPythonAttrBool:
128+ if (source.bool_value > 1U) {
129+ return GRAPH_PARAM_INVALID;
130+ }
131+ definition.bool_value = source.bool_value != 0U;
132+ return GRAPH_SUCCESS;
133+ case kPythonAttrString: {
134+ if (!CopyString(source.string_value, true, definition.string_value)) {
135+ return GRAPH_PARAM_INVALID;
136+ }
137+ return GRAPH_SUCCESS;
138+ }
139+ case kPythonAttrDataType:
140+ if (!IsValidDataType(source.data_type_value)) {
141+ return GRAPH_PARAM_INVALID;
142+ }
143+ definition.data_type_value = source.data_type_value;
144+ return GRAPH_SUCCESS;
145+ case kPythonAttrTensor:
146+ return GRAPH_PARAM_INVALID;
147+ case kPythonAttrListInt:
148+ if (!IsValidArray(source.list_int_values, source.count)) {
149+ return GRAPH_PARAM_INVALID;
150+ }
151+ if (source.count != 0U) {
152+ definition.list_int_values.assign(source.list_int_values, source.list_int_values + source.count);
153+ }
154+ return GRAPH_SUCCESS;
155+ case kPythonAttrListFloat: {
156+ if (!IsValidArray(source.list_float_values, source.count)) {
157+ return GRAPH_PARAM_INVALID;
158+ }
159+ if (source.count != 0U) {
160+ definition.list_float_values.assign(source.list_float_values, source.list_float_values + source.count);
161+ }
162+ for (const auto value : definition.list_float_values) {
163+ if (std::isfinite(value) && (std::fabs(value) > static_cast<double>(std::numeric_limits<float32_t>::max()))) {
164+ return GRAPH_PARAM_INVALID;
165+ }
166+ }
167+ return GRAPH_SUCCESS;
168+ }
169+ case kPythonAttrListBool: {
170+ if (!IsValidArray(source.list_bool_values, source.count)) {
171+ return GRAPH_PARAM_INVALID;
172+ }
173+ definition.list_bool_values.reserve(source.count);
174+ for (size_t i = 0U; i < source.count; ++i) {
175+ if (source.list_bool_values[i] > 1U) {
176+ return GRAPH_PARAM_INVALID;
177+ }
178+ definition.list_bool_values.emplace_back(source.list_bool_values[i] != 0U);
179+ }
180+ return GRAPH_SUCCESS;
181+ }
182+ case kPythonAttrListString: {
183+ return ParseStringArray(source.list_string_values, source.count, definition.list_string_values);
184+ }
185+ case kPythonAttrListDataType: {
186+ if (!IsValidArray(source.list_data_type_values, source.count)) {
187+ return GRAPH_PARAM_INVALID;
188+ }
189+ definition.list_data_type_values.reserve(source.count);
190+ for (size_t i = 0U; i < source.count; ++i) {
191+ if (!IsValidDataType(source.list_data_type_values[i])) {
192+ return GRAPH_PARAM_INVALID;
193+ }
194+ definition.list_data_type_values.emplace_back(source.list_data_type_values[i]);
195+ }
196+ return GRAPH_SUCCESS;
197+ }
198+ case kPythonAttrListListInt:
199+ if (!IsValidArray(source.list_list_int_values, source.count)) {
200+ return GRAPH_PARAM_INVALID;
201+ }
202+ definition.list_list_int_values.reserve(source.count);
203+ for (size_t i = 0U; i < source.count; ++i) {
204+ const auto &row = source.list_list_int_values[i];
205+ if (!IsValidArray(row.data, row.count)) {
206+ return GRAPH_PARAM_INVALID;
207+ }
208+ if (row.count == 0U) {
209+ definition.list_list_int_values.emplace_back();
210+ } else {
211+ definition.list_list_int_values.emplace_back(row.data, row.data + row.count);
212+ }
213+ }
214+ return GRAPH_SUCCESS;
215+ default:
216+ return GRAPH_PARAM_INVALID;
217+ }
218+}
219+ 
220+graphStatus MaterializeOptionalAttrValue(const PythonCustomOpAttr &attr, ge::AttrValue &value) {
221+ const auto &definition = attr.default_definition;
222+ switch (attr.kind) {
223+ case kPythonAttrInt:
224+ return value.SetAttrValue(definition.int_value);
225+ case kPythonAttrFloat:
226+ return value.SetAttrValue(static_cast<float32_t>(definition.float_value));
227+ case kPythonAttrBool:
228+ return value.SetAttrValue(definition.bool_value);
229+ case kPythonAttrString:
230+ return value.SetAttrValue(AscendString(definition.string_value.c_str()));
231+ case kPythonAttrDataType:
232+ return value.SetAttrValue(static_cast<ge::DataType>(definition.data_type_value));
233+ case kPythonAttrListInt:
234+ return value.SetAttrValue(definition.list_int_values);
235+ case kPythonAttrListFloat: {
236+ std::vector<float32_t> values;
237+ values.reserve(definition.list_float_values.size());
238+ for (const auto item : definition.list_float_values) {
239+ values.emplace_back(static_cast<float32_t>(item));
240+ }
241+ return value.SetAttrValue(values);
242+ }
243+ case kPythonAttrListBool:
244+ return value.SetAttrValue(definition.list_bool_values);
245+ case kPythonAttrListString: {
246+ std::vector<AscendString> values;
247+ values.reserve(definition.list_string_values.size());
248+ for (const auto &item : definition.list_string_values) {
249+ values.emplace_back(item.c_str());
250+ }
251+ return value.SetAttrValue(values);
252+ }
253+ case kPythonAttrListDataType: {
254+ std::vector<ge::DataType> values;
255+ values.reserve(definition.list_data_type_values.size());
256+ for (const auto item : definition.list_data_type_values) {
257+ values.emplace_back(static_cast<ge::DataType>(item));
258+ }
259+ return value.SetAttrValue(values);
260+ }
261+ case kPythonAttrListListInt:
262+ return value.SetAttrValue(definition.list_list_int_values);
263+ default:
264+ return GRAPH_PARAM_INVALID;
265+ }
266+}
267+ 
268+bool SameDouble(const double lhs, const double rhs) {
269+ return ((lhs <= rhs) && (lhs >= rhs)) || (std::isnan(lhs) && std::isnan(rhs));
270+}
271+ 
272+bool SameDoubleVector(const std::vector<double> &lhs, const std::vector<double> &rhs) {
273+ if (lhs.size() != rhs.size()) {
274+ return false;
275+ }
276+ for (size_t i = 0U; i < lhs.size(); ++i) {
277+ if (!SameDouble(lhs[i], rhs[i])) {
278+ return false;
279+ }
280+ }
281+ return true;
282+}
283+ 
284+bool SameDefault(const PythonCustomOpAttr &lhs, const PythonCustomOpAttr &rhs) {
285+ const auto &left = lhs.default_definition;
286+ const auto &right = rhs.default_definition;
287+ switch (lhs.kind) {
288+ case kPythonAttrInt:
289+ return left.int_value == right.int_value;
290+ case kPythonAttrFloat:
291+ return SameDouble(left.float_value, right.float_value);
292+ case kPythonAttrBool:
293+ return left.bool_value == right.bool_value;
294+ case kPythonAttrString:
295+ return left.string_value == right.string_value;
296+ case kPythonAttrDataType:
297+ return left.data_type_value == right.data_type_value;
298+ case kPythonAttrListInt:
299+ return left.list_int_values == right.list_int_values;
300+ case kPythonAttrListFloat:
301+ return SameDoubleVector(left.list_float_values, right.list_float_values);
302+ case kPythonAttrListBool:
303+ return left.list_bool_values == right.list_bool_values;
304+ case kPythonAttrListString:
305+ return left.list_string_values == right.list_string_values;
306+ case kPythonAttrListDataType:
307+ return left.list_data_type_values == right.list_data_type_values;
308+ case kPythonAttrListListInt:
309+ return left.list_list_int_values == right.list_list_int_values;
310+ case kPythonAttrTensor:
311+ return true;
312+ default:
313+ return false;
314+ }
315+}
316+ 
317+class PythonCustomOpProtoOperator final : public ge::Operator {
318+ public:
319+ PythonCustomOpProtoOperator(const AscendString &name, const AscendString &type) : Operator(name, type) {}
320+ 
321+ using Operator::InputRegister;
322+ using Operator::OptionalInputRegister;
323+ using Operator::OutputRegister;
324+ using Operator::RequiredAttrWithTypeRegister;
325+};
326+ 
327+ge::Operator CreateOperatorFromProto(const AscendString &name, const PythonCustomOpProto &proto) {
328+ PythonCustomOpProtoOperator op(name, AscendString(proto.op_type.c_str()));
329+ for (const auto &input : proto.inputs) {
330+ switch (input.kind) {
331+ case ge::kIrInputRequired:
332+ op.InputRegister(input.name.c_str());
333+ break;
334+ case ge::kIrInputOptional:
335+ op.OptionalInputRegister(input.name.c_str());
336+ break;
337+ case ge::kIrInputDynamic:
338+ op.DynamicInputRegister(input.name.c_str(), 0U);
339+ break;
340+ default:
341+ break;
342+ }
343+ }
344+ for (const auto &output : proto.outputs) {
345+ if (output.kind == ge::kIrOutputRequired) {
346+ op.OutputRegister(output.name.c_str());
347+ } else if (output.kind == ge::kIrOutputDynamic) {
348+ op.DynamicOutputRegister(output.name.c_str(), 0U);
349+ }
350+ }
351+ for (const auto &attr : proto.attrs) {
352+ if (attr.is_required) {
353+ op.RequiredAttrWithTypeRegister(attr.name.c_str(), GetRequiredAttrToken(attr.kind));
354+ } else {
355+ ge::AttrValue default_value;
356+ if (MaterializeOptionalAttrValue(attr, default_value) != GRAPH_SUCCESS) {
357+ GELOGE(GRAPH_FAILED, "Materialize python custom op attr[%s] default failed.", attr.name.c_str());
358+ return ge::Operator();
359+ }
360+ op.AttrRegister(attr.name.c_str(), default_value);
361+ }
362+ }
363+ return op;
364+}
365+ 
366+} // namespace
367+ 
368+graphStatus ParsePythonCustomOpProto(const PythonCustomOpProtoDescriptorView &view, PythonCustomOpProto &proto) {
369+ if ((!IsValidArray(view.inputs, view.input_count)) || (!IsValidArray(view.attrs, view.attr_count)) ||
370+ (!IsValidArray(view.outputs, view.output_count))) {
371+ return GRAPH_PARAM_INVALID;
372+ }
373+ PythonCustomOpProto parsed;
374+ if ((!CopyString(view.descriptor_key, false, parsed.descriptor_key)) ||
375+ (!CopyString(view.op_type, false, parsed.op_type))) {
376+ return GRAPH_PARAM_INVALID;
377+ }
378+ 
379+ std::set<std::string> input_names;
380+ parsed.inputs.reserve(view.input_count);
381+ for (size_t i = 0U; i < view.input_count; ++i) {
382+ PythonCustomOpInput input;
383+ if ((!CopyString(view.inputs[i].name, false, input.name)) || (!input_names.insert(input.name).second) ||
384+ (ConvertInputKind(view.inputs[i].kind, input.kind) != GRAPH_SUCCESS)) {
385+ return GRAPH_PARAM_INVALID;
386+ }
387+ parsed.inputs.emplace_back(std::move(input));
388+ }
389+ 
390+ std::set<std::string> attr_names;
391+ parsed.attrs.reserve(view.attr_count);
392+ for (size_t i = 0U; i < view.attr_count; ++i) {
393+ const auto &source = view.attrs[i];
394+ PythonCustomOpAttr attr;
395+ if ((!CopyString(source.name, false, attr.name)) || (!attr_names.insert(attr.name).second) ||
396+ (source.is_required > 1U) || (source.default_value.has_value > 1U)) {
397+ return GRAPH_PARAM_INVALID;
398+ }
399+ if (GetRequiredAttrToken(source.kind) == nullptr) {
400+ return GRAPH_PARAM_INVALID;
401+ }
402+ attr.kind = source.kind;
403+ attr.is_required = source.is_required != 0U;
404+ if (attr.is_required == (source.default_value.has_value != 0U)) {
405+ return GRAPH_PARAM_INVALID;
406+ }
407+ if ((!attr.is_required) && (ParseOptionalAttrDefinition(source, attr) != GRAPH_SUCCESS)) {
408+ return GRAPH_PARAM_INVALID;
409+ }
410+ parsed.attrs.emplace_back(std::move(attr));
411+ }
412+ 
413+ std::set<std::string> output_names;
414+ parsed.outputs.reserve(view.output_count);
415+ for (size_t i = 0U; i < view.output_count; ++i) {
416+ PythonCustomOpOutput output;
417+ if ((!CopyString(view.outputs[i].name, false, output.name)) || (!output_names.insert(output.name).second) ||
418+ (ConvertOutputKind(view.outputs[i].kind, output.kind) != GRAPH_SUCCESS)) {
419+ return GRAPH_PARAM_INVALID;
420+ }
421+ parsed.outputs.emplace_back(std::move(output));
422+ }
423+ proto = std::move(parsed);
424+ return GRAPH_SUCCESS;
425+}
426+ 
427+bool IsSamePythonCustomOpProto(const PythonCustomOpProto &lhs, const PythonCustomOpProto &rhs) {
428+ if ((lhs.descriptor_key != rhs.descriptor_key) || (lhs.op_type != rhs.op_type) ||
429+ (lhs.inputs.size() != rhs.inputs.size()) || (lhs.attrs.size() != rhs.attrs.size()) ||
430+ (lhs.outputs.size() != rhs.outputs.size())) {
431+ return false;
432+ }
433+ for (size_t i = 0U; i < lhs.inputs.size(); ++i) {
434+ if ((lhs.inputs[i].name != rhs.inputs[i].name) || (lhs.inputs[i].kind != rhs.inputs[i].kind)) {
435+ return false;
436+ }
437+ }
438+ for (size_t i = 0U; i < lhs.attrs.size(); ++i) {
439+ const auto &left = lhs.attrs[i];
440+ const auto &right = rhs.attrs[i];
441+ if ((left.name != right.name) || (left.kind != right.kind) || (left.is_required != right.is_required) ||
442+ ((!left.is_required) && (!SameDefault(left, right)))) {
443+ return false;
444+ }
445+ }
446+ for (size_t i = 0U; i < lhs.outputs.size(); ++i) {
447+ if ((lhs.outputs[i].name != rhs.outputs[i].name) || (lhs.outputs[i].kind != rhs.outputs[i].kind)) {
448+ return false;
449+ }
450+ }
451+ return true;
452+}
453+ 
454+graphStatus RegisterPythonCustomOpProto(const PythonCustomOpProto &proto) {
GengChao
GengChaoGengChao13 天前

调用的时机可以保证覆盖同名的内置算子原型吗?

likedislike
lfz2812
13 天前 评论:
455+ if (CustomOpFactory::IsExistOp(AscendString(proto.op_type.c_str()))) {
456+ GELOGE(GRAPH_FAILED,
457+ "Python custom op proto conflict, op type[%s], existing source[CustomOpFactory creator], "
458+ "current source[Python descriptor key:%s].",
459+ proto.op_type.c_str(), proto.descriptor_key.c_str());
460+ return GRAPH_FAILED;
461+ }
462+ const auto owned_proto = ComGraphMakeShared<const PythonCustomOpProto>(proto);
463+ if (owned_proto == nullptr) {
464+ GELOGE(GRAPH_FAILED, "Create python custom op proto failed, descriptor key[%s], op type[%s].",
465+ proto.descriptor_key.c_str(), proto.op_type.c_str());
466+ return GRAPH_FAILED;
467+ }
468+ const OpCreatorV2 creator = [owned_proto](const AscendString &name) -> Operator {
469+ return CreateOperatorFromProto(name, *owned_proto);
470+ };
471+ OperatorFactoryImpl::SetRegisterOverridable(true);
472+ const auto ret = OperatorFactoryImpl::RegisterOperatorCreator(proto.op_type, creator);
473+ OperatorFactoryImpl::SetRegisterOverridable(false);
474+ if (ret != GRAPH_SUCCESS) {
475+ return ret;
476+ }
477+ return GRAPH_SUCCESS;
478+}
479+ 
480+void UnregisterPythonCustomOpProtos(const std::vector<std::string> &op_types) {
481+ OperatorFactoryImpl::RemoveCustomOpCreators(op_types);
482+}
483+} // namespace custom_op
484+} // namespace ge
Aruntime/custom_op/python_custom_op_proto.h+70-0
@@ -0,0 +1,70 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#ifndef CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_PROTO_H_
12+#define CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_PROTO_H_
13+ 
14+#include <cstdint>
15+#include <string>
16+#include <vector>
17+ 
18+#include "graph/error_codes.h"
19+#include "graph/op_desc.h"
20+#include "runtime/custom_op/python_custom_op_bridge_types.h"
21+ 
22+namespace ge {
23+namespace custom_op {
24+struct PythonCustomOpAttrDefault {
25+ int64_t int_value{0};
26+ double float_value{0.0};
27+ bool bool_value{false};
28+ std::string string_value;
29+ int32_t data_type_value{0};
30+ std::vector<int64_t> list_int_values;
31+ std::vector<double> list_float_values;
32+ std::vector<bool> list_bool_values;
33+ std::vector<std::string> list_string_values;
34+ std::vector<int32_t> list_data_type_values;
35+ std::vector<std::vector<int64_t>> list_list_int_values;
36+};
37+ 
38+struct PythonCustomOpInput {
39+ std::string name;
40+ ge::IrInputType kind{ge::kIrInputRequired};
41+};
42+ 
43+struct PythonCustomOpAttr {
44+ std::string name;
45+ uint32_t kind{kPythonAttrInt};
46+ bool is_required{true};
47+ PythonCustomOpAttrDefault default_definition;
48+};
49+ 
50+struct PythonCustomOpOutput {
51+ std::string name;
52+ ge::IrOutputType kind{ge::kIrOutputRequired};
53+};
54+ 
55+struct PythonCustomOpProto {
56+ std::string descriptor_key;
57+ std::string op_type;
58+ std::vector<PythonCustomOpInput> inputs;
59+ std::vector<PythonCustomOpAttr> attrs;
60+ std::vector<PythonCustomOpOutput> outputs;
61+};
62+ 
63+graphStatus ParsePythonCustomOpProto(const PythonCustomOpProtoDescriptorView &view, PythonCustomOpProto &proto);
64+bool IsSamePythonCustomOpProto(const PythonCustomOpProto &lhs, const PythonCustomOpProto &rhs);
65+graphStatus RegisterPythonCustomOpProto(const PythonCustomOpProto &proto);
66+void UnregisterPythonCustomOpProtos(const std::vector<std::string> &op_types);
67+} // namespace custom_op
68+} // namespace ge
69+ 
70+#endif // CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_PROTO_H_
Mruntime/custom_op/python_custom_op_pybind_bridge.cc+174-120
@@ -30,10 +30,11 @@
30 30 
31#include "common/checker.h"31#include "common/checker.h"
32#include "common/ge_common/debug/ge_log.h"32#include "common/ge_common/debug/ge_log.h"
33-#include "ge/ge_api_v2.h"33+#include "graph/utils/ir_definitions_query.h"
34#include "graph/operator_factory.h"34#include "graph/operator_factory.h"
35#include "pybind11/embed.h"35#include "pybind11/embed.h"
36#include "pybind11/stl.h"36#include "pybind11/stl.h"
37+#include "runtime/custom_op/python_custom_op_bridge_descriptors.h"
37#include "runtime/custom_op/python_custom_op_bridge_types.h"38#include "runtime/custom_op/python_custom_op_bridge_types.h"
38 39 
39#undef PYBIND11_CHECK_PYTHON_VERSION40#undef PYBIND11_CHECK_PYTHON_VERSION
@@ -45,15 +46,11 @@ namespace py = pybind11;
45namespace {46namespace {
46constexpr const char *kBridgeModuleName = "ge.custom_op._bridge";47constexpr const char *kBridgeModuleName = "ge.custom_op._bridge";
47constexpr const char *kCustomOpModuleName = "ge.custom_op";48constexpr const char *kCustomOpModuleName = "ge.custom_op";
49+constexpr const char *kCustomOpProtoModuleName = "ge.custom_op.proto";
48constexpr const char *kCustomOpNativeModuleName = "ge.custom_op._ge_custom_op_native";50constexpr const char *kCustomOpNativeModuleName = "ge.custom_op._ge_custom_op_native";
49constexpr const char *kEnvCustomOppPath = "ASCEND_CUSTOM_OPP_PATH";51constexpr const char *kEnvCustomOppPath = "ASCEND_CUSTOM_OPP_PATH";
50-constexpr const char *kInterfaceAnnotatedArgs = "annotated_args";52+constexpr const char *kGetRegisteredIrDefFromGraphSymbol = "GetRegisteredIrDefFromGraph";
51-constexpr const char *kInterfaceEagerExecute = "eager_execute";53+constexpr const char *kGraphLibraryName = "libgraph.so";
52-constexpr const char *kGetRegisteredIrDefSymbol = "GetRegisteredIrDef";
53-constexpr const char *kRunnerLibraryNames[] = {
54- "libge_runner_v2.so",
55- "libge_runner.so",
56-};
57 54 
58struct PythonCustomOpIrInputMeta {55struct PythonCustomOpIrInputMeta {
59 std::string name;56 std::string name;
@@ -77,37 +74,33 @@ struct PythonCustomOpIrMeta {
77 std::vector<PythonCustomOpIrOutputMeta> outputs;74 std::vector<PythonCustomOpIrOutputMeta> outputs;
78};75};
79 76 
80-using GetRegisteredIrDefFn = decltype(&::GetRegisteredIrDef);77+using GetRegisteredIrDefFromGraphFn = decltype(&::GetRegisteredIrDefFromGraph);
81 78 
82-// runner 已依赖 custom_op_runtime,bridge 又由 custom_op_runtime 动态加载,因此 bridge 不能反向硬链接 runner。79+bool GetRegisteredIrDefFromLoadedGraph(const char *op_type,
83-// Python 可能以 RTLD_LOCAL 加载 runner 依赖组,这里使用正式公共头文件约束函数签名,80+ std::vector<std::pair<ge::AscendString, ge::AscendString>> &inputs,
84-// 并通过 RTLD_NOLOAD 获取已加载的 runner handle 查询公共符号,不重复加载或提升其全局可见性。81+ std::vector<std::pair<ge::AscendString, ge::AscendString>> &outputs,
85-bool GetRegisteredIrDefFromLoadedRunner(const char *op_type,82+ std::vector<std::pair<ge::AscendString, ge::AscendString>> &attrs) {
86- std::vector<std::pair<ge::AscendString, ge::AscendString>> &inputs,83+ void *handle = dlopen(kGraphLibraryName, RTLD_NOW | RTLD_NOLOAD);
87- std::vector<std::pair<ge::AscendString, ge::AscendString>> &outputs,84+ if (handle == nullptr) {
88- std::vector<std::pair<ge::AscendString, ge::AscendString>> &attrs) {85+ GELOGE(FAILED, "Graph library is not loaded when querying IR for custom op[%s].", op_type);
89- for (const auto *runner_library : kRunnerLibraryNames) {86+ return false;
90- void *handle = dlopen(runner_library, RTLD_NOW | RTLD_NOLOAD);
91- if (handle == nullptr) {
92- continue;
93- }
94- 
95- (void)dlerror();
96- void *symbol = dlsym(handle, kGetRegisteredIrDefSymbol);
97- const char *error = dlerror();
98- if ((symbol == nullptr) || (error != nullptr)) {
99- (void)dlclose(handle);
100- continue;
101- }
102- 
103- const auto get_registered_ir_def = reinterpret_cast<GetRegisteredIrDefFn>(symbol);
104- const auto ret = get_registered_ir_def(op_type, inputs, outputs, attrs);
105- (void)dlclose(handle);
106- GE_ASSERT_SUCCESS(ret, "GetRegisteredIrDef failed for custom op[%s].", op_type);
107- return true;
108 }87 }
109- 88+ (void)dlerror();
110- GE_ASSERT_TRUE(false, "No loaded run package exports %s for custom op[%s].", kGetRegisteredIrDefSymbol, op_type);89+ void *symbol = dlsym(handle, kGetRegisteredIrDefFromGraphSymbol);
90+ const char *error = dlerror();
91+ if ((symbol == nullptr) || (error != nullptr)) {
92+ (void)dlclose(handle);
93+ GELOGE(FAILED, "Failed to find graph IR query symbol[%s].", kGetRegisteredIrDefFromGraphSymbol);
94+ return false;
95+ }
96+ const auto get_registered_ir_def = reinterpret_cast<GetRegisteredIrDefFromGraphFn>(symbol);
97+ const auto ret = get_registered_ir_def(op_type, inputs, outputs, attrs);
98+ (void)dlclose(handle);
99+ if (ret != ge::SUCCESS) {
100+ GELOGE(ret, "GetRegisteredIrDefFromGraph failed for custom op[%s].", op_type);
101+ return false;
102+ }
103+ return true;
111}104}
112 105 
113bool CopyAscendString(const ge::AscendString &value, const char *field_name, std::string &result) {106bool CopyAscendString(const ge::AscendString &value, const char *field_name, std::string &result) {
@@ -157,7 +150,7 @@ std::unique_ptr<PythonCustomOpIrMeta> CollectPythonCustomOpIrMeta(const std::str
157 std::vector<std::pair<ge::AscendString, ge::AscendString>> inputs;150 std::vector<std::pair<ge::AscendString, ge::AscendString>> inputs;
158 std::vector<std::pair<ge::AscendString, ge::AscendString>> outputs;151 std::vector<std::pair<ge::AscendString, ge::AscendString>> outputs;
159 std::vector<std::pair<ge::AscendString, ge::AscendString>> attrs;152 std::vector<std::pair<ge::AscendString, ge::AscendString>> attrs;
160- GE_ASSERT_TRUE(GetRegisteredIrDefFromLoadedRunner(op_type.c_str(), inputs, outputs, attrs));153+ GE_ASSERT_TRUE(GetRegisteredIrDefFromLoadedGraph(op_type.c_str(), inputs, outputs, attrs));
161 154 
162 auto ir_meta = std::unique_ptr<PythonCustomOpIrMeta>(new (std::nothrow) PythonCustomOpIrMeta());155 auto ir_meta = std::unique_ptr<PythonCustomOpIrMeta>(new (std::nothrow) PythonCustomOpIrMeta());
163 GE_ASSERT_NOTNULL(ir_meta, "Allocate IR meta failed for custom op[%s].", op_type.c_str());156 GE_ASSERT_NOTNULL(ir_meta, "Allocate IR meta failed for custom op[%s].", op_type.c_str());
@@ -200,17 +193,12 @@ struct PythonCustomOpBridgeHolder {
200 std::unique_ptr<PythonCustomOpIrMeta> ir_meta;193 std::unique_ptr<PythonCustomOpIrMeta> ir_meta;
201};194};
202 195 
203-CustomOpCapabilityMask ParseInterfaces(const py::object &interfaces_obj) {196+bool CopyStringView(const PythonCustomOpStringView &view, std::string &value) {
204- CustomOpCapabilityMask capabilities = 0U;197+ if ((view.size != 0U) && (view.data == nullptr)) {
205- for (const auto &item : interfaces_obj.cast<py::list>()) {198+ return false;
206- const std::string interface_name = py::str(item);
207- if (interface_name == kInterfaceEagerExecute) {
208- AddCustomOpCapability(capabilities, CustomOpCapability::kEagerExecute);
209- } else if (interface_name == kInterfaceAnnotatedArgs) {
210- AddCustomOpCapability(capabilities, CustomOpCapability::kAnnotatedArgs);
211- }
212 }199 }
213- return capabilities;200+ value.assign(view.data == nullptr ? "" : view.data, view.size);
201+ return (!value.empty()) && (value.find('\0') == std::string::npos);
214}202}
215 203 
216class PythonCustomOpPybindBridge {204class PythonCustomOpPybindBridge {
@@ -244,49 +232,30 @@ class PythonCustomOpPybindBridge {
244 py::gil_scoped_acquire gil;232 py::gil_scoped_acquire gil;
245 py::object descriptors_obj;233 py::object descriptors_obj;
246 try {234 try {
247- descriptors_obj = bridge_module_.attr("load_and_get_op_impl_descriptors")();235+ descriptors_obj = bridge_module_.attr("load_and_get_op_descriptors")();
248 } catch (const py::error_already_set &err) {236 } catch (const py::error_already_set &err) {
249 GELOGE(FAILED, "Load python custom op descriptors failed: %s", err.what());237 GELOGE(FAILED, "Load python custom op descriptors failed: %s", err.what());
250 return FAILED;238 return FAILED;
251 }239 }
252 240 
253- const py::list descriptor_list = descriptors_obj.cast<py::list>();241+ py::dict descriptors;
254- std::vector<PythonCustomOpDescriptor> descriptors;242+ try {
255- descriptors.reserve(descriptor_list.size());243+ descriptors = descriptors_obj.cast<py::dict>();
256- for (const auto &item : descriptor_list) {244+ } catch (const py::error_already_set &err) {
257- PythonCustomOpDescriptor desc;245+ GELOGE(FAILED, "Parse python custom op descriptor snapshot failed: %s", err.what());
258- const auto parse_ret = ParseDescriptor(item.cast<py::dict>(), desc);246+ return FAILED;
259- if (parse_ret != SUCCESS) {247+ } catch (const std::exception &err) {
260- GELOGE(parse_ret, "Parse python custom op descriptor failed.");248+ GELOGE(FAILED, "Parse python custom op descriptor snapshot failed: %s", err.what());
261- return parse_ret;249+ return FAILED;
262- }
263- auto ir_meta = CollectPythonCustomOpIrMeta(desc.op_type);
264- try {
265- const bool validated =
266- bridge_module_.attr("validate_op_impl_descriptor")(desc.descriptor_key, BuildPythonIrMeta(ir_meta.get()))
267- .cast<bool>();
268- if (!validated) {
269- GELOGE(FAILED, "Validate python custom op[%s] descriptor[%s] failed.", desc.op_type.c_str(),
270- desc.descriptor_key.c_str());
271- return FAILED;
272- }
273- } catch (const py::error_already_set &err) {
274- GELOGE(FAILED, "Validate python custom op[%s] descriptor[%s] failed: %s", desc.op_type.c_str(),
275- desc.descriptor_key.c_str(), err.what());
276- return FAILED;
277- }
278- descriptors.emplace_back(std::move(desc));
279 }250 }
280 251 
281- const auto callbacks = GetCallbacks();252+ if ((registrar.register_op_proto == nullptr) || (registrar.register_op_adapter == nullptr)) {
282- for (const auto &desc : descriptors) {253+ return FAILED;
283- if ((registrar.register_custom_op == nullptr) || (!registrar.register_custom_op(&desc, &callbacks))) {
284- GELOGE(FAILED, "Register python custom op[%s] failed.", desc.op_type.c_str());
285- return FAILED;
286- }
287- GELOGI("Python custom op[%s] is registered from pybind bridge.", desc.op_type.c_str());
288 }254 }
289- return SUCCESS;255+ if (CollectAndRegisterProtoDescriptors(descriptors, registrar) != SUCCESS) {
256+ return FAILED;
257+ }
258+ return CollectAndRegisterImplDescriptorsWithCheck(descriptors, registrar);
290 }259 }
291 260 
292 void ResetBridgeState() {261 void ResetBridgeState() {
@@ -318,30 +287,74 @@ class PythonCustomOpPybindBridge {
318 owns_interpreter_ = false;287 owns_interpreter_ = false;
319 }288 }
320 289 
321- void *CreateHolder(const PythonCustomOpDescriptor &desc) {290+ void *CreateImplHolder(const PythonCustomOpAdapterDescriptorView *desc_view) {
291+ if (desc_view == nullptr) {
292+ return nullptr;
293+ }
294+ std::string descriptor_key;
295+ std::string op_type;
296+ if ((!CopyStringView(desc_view->impl_descriptor_key, descriptor_key)) ||
297+ (!CopyStringView(desc_view->op_type, op_type))) {
298+ GELOGW("Create python custom op holder failed because adapter descriptor view is invalid.");
299+ return nullptr;
300+ }
322 if (EnsureBridgeReady() != SUCCESS) {301 if (EnsureBridgeReady() != SUCCESS) {
323 GELOGW("Prepare python custom op bridge failed when creating holder.");302 GELOGW("Prepare python custom op bridge failed when creating holder.");
324 return nullptr;303 return nullptr;
325 }304 }
326 py::gil_scoped_acquire gil;305 py::gil_scoped_acquire gil;
327- const std::string instance_id = BuildInstanceId(desc.descriptor_key);306+ const std::string instance_id = BuildInstanceId(descriptor_key);
328- auto ir_meta = CollectPythonCustomOpIrMeta(desc.op_type);307+ auto ir_meta = CollectPythonCustomOpIrMeta(op_type);
GengChao
GengChaoGengChao13 天前

这里要修改一下CollectPythonCustomOpIrMeta的内部实现了,因为这个函数本来是服务于execute的,这意味着必然是图执行时,所以这个函数依赖的runner的so一定是被加载了的,当前扩散到图编译时,那么比如atc流程是没有人加载runner的so导致这个函数不能正常工作; 建议``` graph.so: 实现 IR 查询逻辑 导出一个内部、稳定的 extern "C" 符号

libge_runner*.so: 保持公开 GetRegisteredIrDef API 内部调用 graph 层实现

Python bridge: dlopen("libgraph.so", RTLD_NOW | RTLD_NOLOAD) dlsym("内部 IR 查询符号") 通过函数指针调用

likedislike
lfz2812
12 天前 评论:
329 try {308 try {
330- const bool created = bridge_module_.attr("create_op_impl_holder")(instance_id, desc.descriptor_key).cast<bool>();309+ const bool created = bridge_module_.attr("create_op_impl_holder")(instance_id, descriptor_key).cast<bool>();
331 if (!created) {310 if (!created) {
332- GELOGW("Create python custom op holder failed, descriptor key[%s], instance id[%s].",311+ GELOGW("Create python custom op holder failed, descriptor key[%s], instance id[%s].", descriptor_key.c_str(),
333- desc.descriptor_key.c_str(), instance_id.c_str());312+ instance_id.c_str());
334 return nullptr;313 return nullptr;
335 }314 }
336 } catch (const py::error_already_set &err) {315 } catch (const py::error_already_set &err) {
337- GELOGW("Create python custom op holder failed, descriptor key[%s], instance id[%s]: %s",316+ GELOGW("Create python custom op holder failed, descriptor key[%s], instance id[%s]: %s", descriptor_key.c_str(),
338- desc.descriptor_key.c_str(), instance_id.c_str(), err.what());317+ instance_id.c_str(), err.what());
318+ return nullptr;
319+ } catch (const std::exception &err) {
320+ GELOGW("Create python custom op holder failed, descriptor key[%s], instance id[%s]: %s", descriptor_key.c_str(),
321+ instance_id.c_str(), err.what());
322+ return nullptr;
323+ } catch (...) {
324+ GELOGW("Create python custom op holder failed with unknown exception, descriptor key[%s], instance id[%s].",
325+ descriptor_key.c_str(), instance_id.c_str());
339 return nullptr;326 return nullptr;
340 }327 }
341- return new (std::nothrow) PythonCustomOpBridgeHolder{desc.descriptor_key, instance_id, std::move(ir_meta)};328+ return new (std::nothrow) PythonCustomOpBridgeHolder{descriptor_key, instance_id, std::move(ir_meta)};
342 }329 }
343 330 
344- void DestroyHolder(PythonCustomOpBridgeHolder *holder) {331+ bool ValidateImpl(const PythonCustomOpAdapterDescriptorView *desc_view) {
332+ if (desc_view == nullptr) {
333+ return false;
334+ }
335+ std::string descriptor_key;
336+ std::string op_type;
337+ if ((!CopyStringView(desc_view->impl_descriptor_key, descriptor_key)) ||
338+ (!CopyStringView(desc_view->op_type, op_type))) {
339+ return false;
340+ }
341+ py::gil_scoped_acquire gil;
342+ try {
343+ const auto ir_meta = CollectPythonCustomOpIrMeta(op_type);
344+ return bridge_module_.attr("validate_op_impl_descriptor")(descriptor_key, BuildPythonIrMeta(ir_meta.get()))
duhua
duhuaduhua14 天前

之前是在注册的时候进行校验的,为什么现在放到了callback校验?注册的时候一次性校验比较友好,放callback每次回调都需要校验

likedislike
lfz2812
14 天前 评论:
345+ .cast<bool>();
346+ } catch (const py::error_already_set &err) {
347+ GELOGE(FAILED, "Validate python custom op impl failed, descriptor key[%s]: %s", descriptor_key.c_str(),
348+ err.what());
349+ return false;
350+ } catch (const std::exception &err) {
351+ GELOGE(FAILED, "Validate python custom op impl failed, descriptor key[%s]: %s", descriptor_key.c_str(),
352+ err.what());
353+ return false;
354+ }
355+ }
356+ 
357+ void DestroyImplHolder(PythonCustomOpBridgeHolder *holder) {
345 if (holder == nullptr) {358 if (holder == nullptr) {
346 return;359 return;
347 }360 }
@@ -353,6 +366,12 @@ class PythonCustomOpPybindBridge {
353 } catch (const py::error_already_set &err) {366 } catch (const py::error_already_set &err) {
354 GELOGW("Destroy python custom op holder failed, descriptor key[%s], instance id[%s]: %s",367 GELOGW("Destroy python custom op holder failed, descriptor key[%s], instance id[%s]: %s",
355 holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());368 holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());
369+ } catch (const std::exception &err) {
370+ GELOGW("Destroy python custom op holder failed, descriptor key[%s], instance id[%s]: %s",
371+ holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());
372+ } catch (...) {
373+ GELOGW("Destroy python custom op holder failed with unknown exception, descriptor key[%s], instance id[%s].",
374+ holder->descriptor_key.c_str(), holder->instance_id.c_str());
356 }375 }
357 }376 }
358 delete holder;377 delete holder;
@@ -416,15 +435,68 @@ class PythonCustomOpPybindBridge {
416 } catch (const py::error_already_set &err) {435 } catch (const py::error_already_set &err) {
417 GELOGE(GRAPH_FAILED, "DeclareLaunchArgs python custom op failed, descriptor key[%s], instance id[%s]: %s",436 GELOGE(GRAPH_FAILED, "DeclareLaunchArgs python custom op failed, descriptor key[%s], instance id[%s]: %s",
418 holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());437 holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());
419- return GRAPH_FAILED;
420 } catch (const std::exception &err) {438 } catch (const std::exception &err) {
421 GELOGE(GRAPH_FAILED, "DeclareLaunchArgs python custom op failed, descriptor key[%s], instance id[%s]: %s",439 GELOGE(GRAPH_FAILED, "DeclareLaunchArgs python custom op failed, descriptor key[%s], instance id[%s]: %s",
422 holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());440 holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());
423- return GRAPH_FAILED;
424 }441 }
442+ return GRAPH_FAILED;
425 }443 }
426 444 
427 private:445 private:
446+ Status CollectAndRegisterProtoDescriptors(const py::dict &descriptors, const PythonCustomOpRegistrar &registrar) {
447+ try {
448+ for (const auto &item : descriptors["protos"].cast<py::list>()) {
449+ ProtoDescriptorStorage proto;
450+ if (proto.Parse(item.cast<py::dict>()) != SUCCESS) {
451+ return FAILED;
452+ }
453+ const auto view = proto.BuildView();
454+ if (!registrar.register_op_proto(&view)) {
455+ GELOGE(FAILED, "Register python custom op proto[%s] failed.", proto.op_type.c_str());
456+ return FAILED;
457+ }
458+ GELOGI("Python custom op proto[%s] is registered from pybind bridge.", proto.op_type.c_str());
459+ }
460+ } catch (const py::error_already_set &err) {
461+ GELOGE(FAILED, "Collect python custom op proto descriptors failed: %s", err.what());
462+ return FAILED;
463+ } catch (const std::exception &err) {
464+ GELOGE(FAILED, "Collect python custom op proto descriptors failed: %s", err.what());
465+ return FAILED;
466+ }
467+ return SUCCESS;
468+ }
469+ 
470+ Status CollectAndRegisterImplDescriptorsWithCheck(const py::dict &descriptors,
471+ const PythonCustomOpRegistrar &registrar) {
472+ const auto callbacks = GetCallbacks();
473+ try {
474+ for (const auto &item : descriptors["impls"].cast<py::list>()) {
475+ AdapterDescriptorStorage adapter;
476+ if (adapter.Parse(item.cast<py::dict>()) != SUCCESS) {
477+ return FAILED;
478+ }
479+ const auto view = adapter.BuildView();
480+ if (!ValidateImpl(&view)) {
481+ GELOGE(FAILED, "Validate python custom op adapter[%s] failed.", adapter.op_type.c_str());
482+ return FAILED;
483+ }
484+ if (!registrar.register_op_adapter(&view, &callbacks)) {
485+ GELOGE(FAILED, "Register python custom op adapter[%s] failed.", adapter.op_type.c_str());
486+ return FAILED;
487+ }
488+ GELOGI("Python custom op adapter[%s] is registered from pybind bridge.", adapter.op_type.c_str());
489+ }
490+ } catch (const py::error_already_set &err) {
491+ GELOGE(FAILED, "Collect python custom op impl descriptors failed: %s", err.what());
492+ return FAILED;
493+ } catch (const std::exception &err) {
494+ GELOGE(FAILED, "Collect python custom op impl descriptors failed: %s", err.what());
495+ return FAILED;
496+ }
497+ return SUCCESS;
498+ }
499+ 
428 Status EnsureBridgeReady() {500 Status EnsureBridgeReady() {
429 std::lock_guard<std::mutex> lock(mutex_);501 std::lock_guard<std::mutex> lock(mutex_);
430 if (Py_IsInitialized() == 0) {502 if (Py_IsInitialized() == 0) {
@@ -498,6 +570,7 @@ class PythonCustomOpPybindBridge {
498 (void)bridge_module_.attr("clear_op_impl_holders")();570 (void)bridge_module_.attr("clear_op_impl_holders")();
499 (void)bridge_module_.attr("clear_loaded_op_impl_modules")();571 (void)bridge_module_.attr("clear_loaded_op_impl_modules")();
500 (void)py::module_::import(kCustomOpModuleName).attr("clear_registered_op_impls")();572 (void)py::module_::import(kCustomOpModuleName).attr("clear_registered_op_impls")();
573+ (void)py::module_::import(kCustomOpProtoModuleName).attr("clear_registered_op_protos")();
501 } catch (const py::error_already_set &err) {574 } catch (const py::error_already_set &err) {
502 GELOGW("Reset python custom op bridge state failed: %s", err.what());575 GELOGW("Reset python custom op bridge state failed: %s", err.what());
503 }576 }
@@ -535,22 +608,6 @@ class PythonCustomOpPybindBridge {
535 return oss.str();608 return oss.str();
536 }609 }
537 610 
538- static Status ParseDescriptor(const py::dict &descriptor_dict, PythonCustomOpDescriptor &desc) {
539- try {
540- desc.descriptor_key = py::str(descriptor_dict["descriptor_key"]);
541- desc.op_type = py::str(descriptor_dict["op_type"]);
542- desc.capabilities = ParseInterfaces(descriptor_dict["interfaces"]);
543- if (desc.capabilities == 0U) {
544- GELOGE(FAILED, "Python custom op[%s] has no supported interface.", desc.op_type.c_str());
545- return FAILED;
546- }
547- } catch (const py::error_already_set &err) {
548- GELOGE(FAILED, "Parse python custom op descriptor failed: %s", err.what());
549- return FAILED;
550- }
551- return (!desc.descriptor_key.empty() && !desc.op_type.empty()) ? SUCCESS : FAILED;
552- }
553- 
554 static py::object BuildPythonIrMeta(const PythonCustomOpIrMeta *ir_meta) {611 static py::object BuildPythonIrMeta(const PythonCustomOpIrMeta *ir_meta) {
555 if (ir_meta == nullptr) {612 if (ir_meta == nullptr) {
556 return py::none();613 return py::none();
@@ -613,16 +670,13 @@ class PythonCustomOpPybindBridge {
613 return GRAPH_FAILED;670 return GRAPH_FAILED;
614 }671 }
615 672 
616- static PythonCustomOpCallbacks GetCallbacks() {673+ static PythonCustomOpAdapterCallbacks GetCallbacks() {
617- PythonCustomOpCallbacks callbacks;674+ PythonCustomOpAdapterCallbacks callbacks;
618- callbacks.create = [](const PythonCustomOpDescriptor *desc) -> void * {675+ callbacks.create_impl_holder = [](const PythonCustomOpAdapterDescriptorView *desc) -> void * {
619- if (desc == nullptr) {676+ return PythonCustomOpPybindBridge::GetInstance().CreateImplHolder(desc);
620- return nullptr;
621- }
622- return PythonCustomOpPybindBridge::GetInstance().CreateHolder(*desc);
623 };677 };
624- callbacks.destroy = [](void *holder) {678+ callbacks.destroy_impl_holder = [](void *holder) {
625- PythonCustomOpPybindBridge::GetInstance().DestroyHolder(static_cast<PythonCustomOpBridgeHolder *>(holder));679+ PythonCustomOpPybindBridge::GetInstance().DestroyImplHolder(static_cast<PythonCustomOpBridgeHolder *>(holder));
626 };680 };
627 callbacks.execute = [](const void *holder, gert::EagerOpExecutionContext *ctx) -> graphStatus {681 callbacks.execute = [](const void *holder, gert::EagerOpExecutionContext *ctx) -> graphStatus {
628 return PythonCustomOpPybindBridge::GetInstance().Execute(static_cast<const PythonCustomOpBridgeHolder *>(holder),682 return PythonCustomOpPybindBridge::GetInstance().Execute(static_cast<const PythonCustomOpBridgeHolder *>(holder),
Mtests/ge/st/testcase/test_custom_op.cc+25-13
@@ -11,7 +11,6 @@
11#include <gtest/gtest.h>11#include <gtest/gtest.h>
12#include <cstdio>12#include <cstdio>
13#include <cstdlib>13#include <cstdlib>
14-#include <iostream>
15#include <dlfcn.h>14#include <dlfcn.h>
16#include <fstream>15#include <fstream>
17#include <map>16#include <map>
@@ -59,6 +58,7 @@
59#include "graph/custom_op/cast.h"58#include "graph/custom_op/cast.h"
60#include "graph/custom_op_factory.h"59#include "graph/custom_op_factory.h"
61#include "graph/custom_op.h"60#include "graph/custom_op.h"
61+#include "graph/operator_factory.h"
62#include "graph/ge_tensor.h"62#include "graph/ge_tensor.h"
63#include "graph/operator_reg.h"63#include "graph/operator_reg.h"
64#include "graph/op_desc.h"64#include "graph/op_desc.h"
@@ -183,13 +183,19 @@ from ge.custom_op import (
183 AnnotatedKernelLaunchInfo,183 AnnotatedKernelLaunchInfo,
184 EagerExecuteOp,184 EagerExecuteOp,
185 get_declare_launch_args_ctx,185 get_declare_launch_args_ctx,
186+ register_op,
186 register_op_impl,187 register_op_impl,
187)188)
188-from ge.runtime import Tensor189+from ge.runtime import Tensor, TensorDesc
189 190 
190MARKER_FILE = r')PY";191MARKER_FILE = r')PY";
191constexpr char kSharedPybindEagerCustomOpForSt[] = R"PY('192constexpr char kSharedPybindEagerCustomOpForSt[] = R"PY('
192 193 
194+@register_op(op_type=')PY";
195+constexpr char kSharedPybindEagerCustomOpImplForSt[] = R"PY(')
196+def infer_meta(x: TensorDesc, *, axis: int = 0) -> TensorDesc:
197+ return x
198+ 
193@register_op_impl(op_type=')PY";199@register_op_impl(op_type=')PY";
194constexpr char kSharedPybindAnnotatedArgsPrefixForSt[] = R"PY(')200constexpr char kSharedPybindAnnotatedArgsPrefixForSt[] = R"PY(')
195class StPythonPybindRemoveCoverageCustomOp(EagerExecuteOp):201class StPythonPybindRemoveCoverageCustomOp(EagerExecuteOp):
@@ -450,8 +456,9 @@ void EnsureSharedPybindCustomOpFileForSt() {
450 std::call_once(once, []() {456 std::call_once(once, []() {
451 const auto python_file = std::string(kSharedPybindCustomOpPreambleForSt) +457 const auto python_file = std::string(kSharedPybindCustomOpPreambleForSt) +
452 GetSharedPybindCustomOpMarkerFilePathForSt() + kSharedPybindEagerCustomOpForSt +458 GetSharedPybindCustomOpMarkerFilePathForSt() + kSharedPybindEagerCustomOpForSt +
453- kPythonCustomOpTypeForSt + kSharedPybindAnnotatedArgsPrefixForSt +459+ kPythonCustomOpTypeForSt + kSharedPybindEagerCustomOpImplForSt + kPythonCustomOpTypeForSt +
454- kPythonAnnotatedArgsOpTypeForSt + kSharedPybindAnnotatedArgsBodyForSt;460+ kSharedPybindAnnotatedArgsPrefixForSt + kPythonAnnotatedArgsOpTypeForSt +
461+ kSharedPybindAnnotatedArgsBodyForSt;
455 WriteTextFileForCustomOpSt(GetSharedPybindCustomOpFilePathForSt(), python_file);462 WriteTextFileForCustomOpSt(GetSharedPybindCustomOpFilePathForSt(), python_file);
456 });463 });
457}464}
@@ -1759,10 +1766,11 @@ TEST_F(CustomOpRefreshTest, eager_only_op_with_malloc_read_only_dev_args) {
1759 * 1. 构造一个合法legacy实现和一个属性名与REG_OP定义不一致的Python实现。1766 * 1. 构造一个合法legacy实现和一个属性名与REG_OP定义不一致的Python实现。
1760 * 测试步骤:1767 * 测试步骤:
1761 * 1. 通过LoadPythonCustomOps加载Python实现。1768 * 1. 通过LoadPythonCustomOps加载Python实现。
1762- * 2. 查询CustomOpFactory中是否存在该Python自定义算子creator1769+ * 2. 调用UnloadPythonCustomOps清理失败注册产生的部分状态
1770+ * 3. 查询CustomOpFactory中是否存在该Python自定义算子creator。
1763 * 预期结果:1771 * 预期结果:
1764 * 1. 注册阶段签名校验失败,LoadPythonCustomOps返回FAILED。1772 * 1. 注册阶段签名校验失败,LoadPythonCustomOps返回FAILED。
1765- * 2. CustomOpFactory中不存在合法或非法Python自定义算子creator。1773+ * 2. 卸载后CustomOpFactory中不存在合法或非法Python自定义算子creator。
1766 */1774 */
1767TEST_F(CustomOpFactoryStTest, PythonCustomOpLoaderRejectsInvalidSignatureDuringRegistration) {1775TEST_F(CustomOpFactoryStTest, PythonCustomOpLoaderRejectsInvalidSignatureDuringRegistration) {
1768 EnsureInvalidSignaturePybindCustomOpFileForSt();1776 EnsureInvalidSignaturePybindCustomOpFileForSt();
@@ -1771,25 +1779,25 @@ TEST_F(CustomOpFactoryStTest, PythonCustomOpLoaderRejectsInvalidSignatureDuringR
1771 1779 
1772 ASSERT_EQ(GePythonRuntimeManager::Instance().EnsureReady(), SUCCESS);1780 ASSERT_EQ(GePythonRuntimeManager::Instance().EnsureReady(), SUCCESS);
1773 EXPECT_EQ(custom_op::LoadPythonCustomOps(), FAILED);1781 EXPECT_EQ(custom_op::LoadPythonCustomOps(), FAILED);
1782+ custom_op::UnloadPythonCustomOps();
1774 EXPECT_EQ(CustomOpFactory::CreateOrGetCustomOp(AscendString(kPythonCustomOpTypeForSt)), nullptr);1783 EXPECT_EQ(CustomOpFactory::CreateOrGetCustomOp(AscendString(kPythonCustomOpTypeForSt)), nullptr);
1775 EXPECT_EQ(CustomOpFactory::CreateOrGetCustomOp(AscendString(kPythonAnnotatedArgsBadAttrOpTypeForSt)), nullptr);1784 EXPECT_EQ(CustomOpFactory::CreateOrGetCustomOp(AscendString(kPythonAnnotatedArgsBadAttrOpTypeForSt)), nullptr);
1776- custom_op::UnloadPythonCustomOps();
1777}1785}
1778 1786 
1779/**1787/**
1780- * 用例描述:测试Python自定义算子通过loader注册、执行后,可以按类型移除注册信息和已创建实例1788+ * 用例描述:测试Python自定义算子原型和实现通过loader注册、执行后,可以按类型移除注册信息。
1781 * 预置条件:1789 * 预置条件:
1782- * 1. 构造Python自定义算子实现文件,并配置到ASCEND_CUSTOM_OPP_PATH。1790+ * 1. 构造Python自定义算子原型和实现文件,并配置到ASCEND_CUSTOM_OPP_PATH。
1783 * 测试步骤:1791 * 测试步骤:
1784- * 1. 通过LoadPythonCustomOps加载Python实现注册到CustomOpFactory。1792+ * 1. 通过LoadPythonCustomOps将原型和实现分别注册到OperatorFactory和CustomOpFactory。
1785- * 2. 通过CustomOpFactory创建Python自定义算子实例执行。1793+ * 2. 校验原型creator生效,并通过CustomOpFactory创建Python自定义算子实例执行。
1786 * 3. 校验Python execute写入的标记文件。1794 * 3. 校验Python execute写入的标记文件。
1787 * 4. 调用UnloadPythonCustomOps移除该算子并清理runtime descriptor。1795 * 4. 调用UnloadPythonCustomOps移除该算子并清理runtime descriptor。
1788 * 预期结果:1796 * 预期结果:
1789 * 1. Python自定义算子execute被成功调用。1797 * 1. Python自定义算子execute被成功调用。
1790- * 2. creator被移除,后续查询不存在,再次创建返回空指针,runtime descriptor可成功注销1798+ * 2. 原型和实现creator被移除,后续查询不存在,再次创建返回空指针。
1791 */1799 */
1792-TEST_F(CustomOpFactoryStTest, remove_python_custom_ops_clears_creator_and_created_instance) {1800+TEST_F(CustomOpFactoryStTest, register_and_remove_python_custom_op_proto_and_impl) {
1793 EnsureSharedPybindCustomOpFileForSt();1801 EnsureSharedPybindCustomOpFileForSt();
1794 const auto &marker_file = GetSharedPybindCustomOpMarkerFilePathForSt();1802 const auto &marker_file = GetSharedPybindCustomOpMarkerFilePathForSt();
1795 (void)remove(marker_file.c_str());1803 (void)remove(marker_file.c_str());
@@ -1799,6 +1807,9 @@ TEST_F(CustomOpFactoryStTest, remove_python_custom_ops_clears_creator_and_create
1799 ASSERT_EQ(custom_op::LoadPythonCustomOps(), SUCCESS);1807 ASSERT_EQ(custom_op::LoadPythonCustomOps(), SUCCESS);
1800 ScopedLoadedPythonCustomOpsForSt loaded_python_custom_ops;1808 ScopedLoadedPythonCustomOpsForSt loaded_python_custom_ops;
1801 1809 
1810+ ASSERT_TRUE(OperatorFactory::IsExistOp(kPythonCustomOpTypeForSt));
1811+ EXPECT_FALSE(OperatorFactory::CreateOperator("st_python_proto", kPythonCustomOpTypeForSt).IsEmpty());
1812+ 
1802 const AscendString op_type(kPythonCustomOpTypeForSt);1813 const AscendString op_type(kPythonCustomOpTypeForSt);
1803 ASSERT_TRUE(CustomOpFactory::IsExistOp(op_type));1814 ASSERT_TRUE(CustomOpFactory::IsExistOp(op_type));
1804 auto *op = CustomOpFactory::CreateOrGetCustomOp(op_type);1815 auto *op = CustomOpFactory::CreateOrGetCustomOp(op_type);
@@ -1813,6 +1824,7 @@ TEST_F(CustomOpFactoryStTest, remove_python_custom_ops_clears_creator_and_create
1813 custom_op::UnloadPythonCustomOps();1824 custom_op::UnloadPythonCustomOps();
1814 loaded_python_custom_ops.Dismiss();1825 loaded_python_custom_ops.Dismiss();
1815 1826 
1827+ EXPECT_FALSE(OperatorFactory::IsExistOp(kPythonCustomOpTypeForSt));
1816 EXPECT_FALSE(CustomOpFactory::IsExistOp(op_type));1828 EXPECT_FALSE(CustomOpFactory::IsExistOp(op_type));
1817 EXPECT_EQ(CustomOpFactory::CreateOrGetCustomOp(op_type), nullptr);1829 EXPECT_EQ(CustomOpFactory::CreateOrGetCustomOp(op_type), nullptr);
1818}1830}
Mtests/ge/ut/ge/CMakeLists.txt+31-7
@@ -401,13 +401,6 @@ set(BASE_COMMON_TEST_FILES
401 "common/ge_root_model_unittest.cc"401 "common/ge_root_model_unittest.cc"
402 "common/om2_utils_unittest.cc"402 "common/om2_utils_unittest.cc"
403 "common/om2_model_data_unittest.cc"403 "common/om2_model_data_unittest.cc"
404- "common/om2_task_node_map_unittest.cc"
405- "common/om2_task_args_refresh_type_classifier_unittest.cc"
406- "common/om2_model_args_utils_unittest.cc"
407- "common/om2_task_args_io_addrs_updater_unittest.cc"
408- "common/om2_memory_app_type_classifier_unittest.cc"
409- "common/om2_model_args_layout_planner_unittest.cc"
410- "common/om2_model_args_manager_unittest.cc"
411 "common/thread_pool_unittest.cc"404 "common/thread_pool_unittest.cc"
412 "common/nano_dbg_data_unittest.cc"405 "common/nano_dbg_data_unittest.cc"
413 "common/nano_model_save_helper_unittest.cc"406 "common/nano_model_save_helper_unittest.cc"
@@ -481,6 +474,8 @@ set(MULTI_PARTS_TEST_FILES
481 "common/helper/custom_op_registry_builder_unittest.cc"474 "common/helper/custom_op_registry_builder_unittest.cc"
482 "runtime/custom_op/python_custom_op_ir_meta_unittest.cc"475 "runtime/custom_op/python_custom_op_ir_meta_unittest.cc"
483 "runtime/custom_op/custom_op_loader_unittest.cc"476 "runtime/custom_op/custom_op_loader_unittest.cc"
477+ "runtime/custom_op/python_custom_op_bridge_loader_unittest.cc"
478+ "runtime/custom_op/python_custom_op_proto_unittest.cc"
484 "graph_ir/ge_custom_op_factory_unittest.cc"479 "graph_ir/ge_custom_op_factory_unittest.cc"
485 "graph_ir/ge_custom_op_pull_registry_unittest.cc"480 "graph_ir/ge_custom_op_pull_registry_unittest.cc"
486 "graph_ir/ge_operator_factory_unittest.cc"481 "graph_ir/ge_operator_factory_unittest.cc"
@@ -699,6 +694,30 @@ target_link_libraries(ge_ut_common PUBLIC
699 694 
700# ut binary695# ut binary
701file(GLOB_RECURSE JIT_EXECUTION_SRC_FILES CONFIGURE_DEPENDS "${AIR_CODE_DIR}/api/session/jit_execution/*.cc")696file(GLOB_RECURSE JIT_EXECUTION_SRC_FILES CONFIGURE_DEPENDS "${AIR_CODE_DIR}/api/session/jit_execution/*.cc")
697+add_library(python_custom_op_bridge_loader_ut_fake SHARED
698+ "runtime/custom_op/python_custom_op_bridge_loader_fake_bridge.cc"
699+)
700+ 
701+target_compile_definitions(python_custom_op_bridge_loader_ut_fake PRIVATE
702+ google=ascend_private
703+)
704+ 
705+target_compile_options(python_custom_op_bridge_loader_ut_fake PRIVATE
706+ ${AIR_COMMON_DYNAMIC_COMPILE_OPTION}
707+ -Werror=format
708+ -Wall -Wfloat-equal -Werror
709+)
710+ 
711+target_include_directories(python_custom_op_bridge_loader_ut_fake PRIVATE
712+ ${AIR_CODE_DIR}
713+ ${AIR_CODE_DIR}/inc/graph_metadef
714+)
715+ 
716+target_link_libraries(python_custom_op_bridge_loader_ut_fake PRIVATE
717+ ge_intf_pub
718+ ge_metadef_headers
719+)
720+ 
702# libge_mutiparts_utest721# libge_mutiparts_utest
703add_executable(ut_libge_multiparts_utest722add_executable(ut_libge_multiparts_utest
704 ${COMMON_TEST_FILES}723 ${COMMON_TEST_FILES}
@@ -717,6 +736,11 @@ target_compile_options(ut_libge_multiparts_utest PRIVATE
717target_compile_definitions(ut_libge_multiparts_utest PRIVATE736target_compile_definitions(ut_libge_multiparts_utest PRIVATE
718 google=ascend_private737 google=ascend_private
719 FUNC_VISIBILITY738 FUNC_VISIBILITY
739+ PYTHON_CUSTOM_OP_LOADER_UT_FAKE_BRIDGE_PATH="$<TARGET_FILE:python_custom_op_bridge_loader_ut_fake>"
740+)
741+ 
742+add_dependencies(ut_libge_multiparts_utest
743+ python_custom_op_bridge_loader_ut_fake
720)744)
721 745 
722target_include_directories(ut_libge_multiparts_utest PRIVATE746target_include_directories(ut_libge_multiparts_utest PRIVATE
Mtests/ge/ut/ge/common/custom_ops_kernel_info_store_unittest.cc+15-9
@@ -107,8 +107,14 @@ std::atomic_uint32_t g_python_annotated_args_declare_count{0U};
107 107 
108struct MockPythonAnnotatedArgsHolder {};108struct MockPythonAnnotatedArgsHolder {};
109 109 
110-void *CreateMockPythonAnnotatedArgsHolder(const custom_op::PythonCustomOpDescriptor *desc) {110+bool ValidateMockPythonAnnotatedArgs(const custom_op::PythonCustomOpAdapterDescriptorView *desc) {
111- return (desc == nullptr) ? nullptr : new (std::nothrow) MockPythonAnnotatedArgsHolder();111+ return (desc != nullptr) && (desc->impl_descriptor_key.data != nullptr) && (desc->impl_descriptor_key.size != 0U);
112+}
113+ 
114+void *CreateMockPythonAnnotatedArgsHolder(const custom_op::PythonCustomOpAdapterDescriptorView *desc) {
115+ return ((desc == nullptr) || (desc->impl_descriptor_key.data == nullptr)) ? nullptr
116+ : new (std::nothrow)
117+ MockPythonAnnotatedArgsHolder();
112}118}
113 119 
114void DestroyMockPythonAnnotatedArgsHolder(void *holder) {120void DestroyMockPythonAnnotatedArgsHolder(void *holder) {
@@ -1091,16 +1097,16 @@ TEST_F(UtestCustomOpsKernelInfoStore, GenerateTaskDeclaresAnnotatedArgsAndFillsK
1091TEST_F(UtestCustomOpsKernelInfoStore, GenerateTaskUsesPythonAnnotatedArgsAdapter) {1097TEST_F(UtestCustomOpsKernelInfoStore, GenerateTaskUsesPythonAnnotatedArgsAdapter) {
1092 GetThreadLocalContext().SetGraphOption({{ge::SOC_VERSION, "Ascend910B"}});1098 GetThreadLocalContext().SetGraphOption({{ge::SOC_VERSION, "Ascend910B"}});
1093 const std::string kTestOpType = "TestPythonAnnotatedArgsCustomOp_BuilderTest";1099 const std::string kTestOpType = "TestPythonAnnotatedArgsCustomOp_BuilderTest";
1094- custom_op::PythonCustomOpDescriptor desc;1100+ custom_op::PythonCustomOpAdapterDescriptor desc;
1095- desc.descriptor_key = "python_annotated_args_custom_engine";1101+ desc.impl_descriptor_key = "python_annotated_args_custom_engine";
1096 desc.op_type = kTestOpType;1102 desc.op_type = kTestOpType;
1097 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kAnnotatedArgs);1103 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kAnnotatedArgs);
1098 1104 
1099- custom_op::PythonCustomOpCallbacks callbacks;1105+ custom_op::PythonCustomOpAdapterCallbacks callbacks;
1100- callbacks.create = CreateMockPythonAnnotatedArgsHolder;1106+ callbacks.create_impl_holder = CreateMockPythonAnnotatedArgsHolder;
1101- callbacks.destroy = DestroyMockPythonAnnotatedArgsHolder;1107+ callbacks.destroy_impl_holder = DestroyMockPythonAnnotatedArgsHolder;
1102 callbacks.declare_launch_args = DeclareMockPythonAnnotatedArgs;1108 callbacks.declare_launch_args = DeclareMockPythonAnnotatedArgs;
1103- ASSERT_TRUE(custom_op::PythonCustomOpRuntimeRegistry::Register(desc, callbacks));1109+ ASSERT_TRUE(custom_op::PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks));
1104 const auto creator = [desc]() -> std::unique_ptr<BaseCustomOp> {1110 const auto creator = [desc]() -> std::unique_ptr<BaseCustomOp> {
1105 auto adapter = std::make_unique<custom_op::PythonCustomOpAdapter>(desc);1111 auto adapter = std::make_unique<custom_op::PythonCustomOpAdapter>(desc);
1106 if (!adapter->IsValid()) {1112 if (!adapter->IsValid()) {
@@ -1125,7 +1131,7 @@ TEST_F(UtestCustomOpsKernelInfoStore, GenerateTaskUsesPythonAnnotatedArgsAdapter
1125 ExpectPythonAnnotatedArgsKernel(tasks[0].kernel());1131 ExpectPythonAnnotatedArgsKernel(tasks[0].kernel());
1126 1132 
1127 CustomOpFactory::RemoveCustomOps({AscendString(kTestOpType.c_str())});1133 CustomOpFactory::RemoveCustomOps({AscendString(kTestOpType.c_str())});
1128- EXPECT_TRUE(custom_op::PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));1134+ EXPECT_TRUE(custom_op::PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key));
1129}1135}
1130 1136 
1131TEST_F(UtestCustomOpsKernelInfoStore, GenerateTaskOnNonMobileSocDeclaresAnnotatedArgsOp) {1137TEST_F(UtestCustomOpsKernelInfoStore, GenerateTaskOnNonMobileSocDeclaresAnnotatedArgsOp) {
Atests/ge/ut/ge/graph/pyge_tests/python_custom_op_artifact_test.py+67-0
@@ -0,0 +1,67 @@
1+#!/usr/bin/env python3
2+# -*- coding: utf-8 -*-
3+# -----------------------------------------------------------------------------------------------------------
4+# Copyright (c) 2026 Huawei Technologies Co., Ltd.
5+# This program is free software, you can redistribute it and/or modify it under the terms and conditions of
6+# CANN Open Software License Agreement Version 2.0 (the "License").
7+# Please refer to the License for details. You may not use this file except in compliance with the License.
8+# THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
9+# INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
10+# See LICENSE in the root of the software repository for the full text of the License.
11+# -----------------------------------------------------------------------------------------------------------
12+ 
13+"""Pytest coverage for Python custom op artifact-set discovery."""
14+ 
15+import json
16+ 
17+from ge._internal.artifact_utils import current_platform_tag, current_python_tag
18+from ge.custom_op import _artifact_utils as artifact_utils
19+ 
20+ 
21+def _write_artifact_set(root, *, bridge_abi=None):
22+ artifact_dir = root / f"{current_python_tag()}-{current_platform_tag()}"
23+ artifact_dir.mkdir(parents=True)
24+ (artifact_dir / "libge_python_custom_op_bridge.so").touch()
25+ (artifact_dir / "_ge_custom_op_native.so").touch()
26+ manifest = {
27+ "python_tag": current_python_tag(),
28+ "platform": current_platform_tag(),
29+ "bridge_abi": (
30+ artifact_utils.BRIDGE_ABI_VERSION if bridge_abi is None else bridge_abi
31+ ),
32+ "artifacts": {
33+ "bridge": "libge_python_custom_op_bridge.so",
34+ "native": "_ge_custom_op_native.so",
35+ },
36+ }
37+ (artifact_dir / "manifest.json").write_text(json.dumps(manifest), encoding="utf-8")
38+ return artifact_dir
39+ 
40+ 
41+def test_find_prebuilt_custom_op_artifact_requires_complete_set(tmp_path, monkeypatch):
42+ root = tmp_path / "python_custom_op_artifacts"
43+ monkeypatch.setattr(artifact_utils, "artifacts_root", lambda: root)
44+ artifact_dir = _write_artifact_set(root)
45+ 
46+ artifact = artifact_utils.find_prebuilt_artifact()
47+ 
48+ assert artifact is not None
49+ assert artifact.bridge_path == artifact_dir / "libge_python_custom_op_bridge.so"
50+ assert artifact.native_path == artifact_dir / "_ge_custom_op_native.so"
51+ 
52+ 
53+def test_find_prebuilt_custom_op_artifact_rejects_incomplete_set(tmp_path, monkeypatch):
54+ root = tmp_path / "python_custom_op_artifacts"
55+ monkeypatch.setattr(artifact_utils, "artifacts_root", lambda: root)
56+ artifact_dir = _write_artifact_set(root)
57+ (artifact_dir / "_ge_custom_op_native.so").unlink()
58+ 
59+ assert artifact_utils.find_prebuilt_artifact() is None
60+ 
61+ 
62+def test_find_prebuilt_custom_op_artifact_rejects_abi_mismatch(tmp_path, monkeypatch):
63+ root = tmp_path / "python_custom_op_artifacts"
64+ monkeypatch.setattr(artifact_utils, "artifacts_root", lambda: root)
65+ _write_artifact_set(root, bridge_abi=artifact_utils.BRIDGE_ABI_VERSION + 1)
66+ 
67+ assert artifact_utils.find_prebuilt_artifact() is None
Mtests/ge/ut/ge/graph/pyge_tests/python_custom_op_test.py+31-0
@@ -24,6 +24,8 @@ try:
24 bridge = importlib.import_module("ge.custom_op._bridge")24 bridge = importlib.import_module("ge.custom_op._bridge")
25 custom_op = importlib.import_module("ge.custom_op")25 custom_op = importlib.import_module("ge.custom_op")
26 ir_types = importlib.import_module("ge.custom_op._ir_types")26 ir_types = importlib.import_module("ge.custom_op._ir_types")
27+ proto = importlib.import_module("ge.custom_op.proto")
28+ runtime = importlib.import_module("ge.runtime")
27 from ge.runtime import Tensor29 from ge.runtime import Tensor
28except ImportError as exc:30except ImportError as exc:
29 pytest.skip(f"无法导入 Python custom op 相关模块: {exc}", allow_module_level=True)31 pytest.skip(f"无法导入 Python custom op 相关模块: {exc}", allow_module_level=True)
@@ -33,12 +35,14 @@ except ImportError as exc:
33def clear_python_custom_op_runtime(monkeypatch):35def clear_python_custom_op_runtime(monkeypatch):
34 monkeypatch.delenv(bootstrap.ENV_PY_CUSTOM_OP_PATH, raising=False)36 monkeypatch.delenv(bootstrap.ENV_PY_CUSTOM_OP_PATH, raising=False)
35 custom_op.clear_registered_op_impls()37 custom_op.clear_registered_op_impls()
38+ proto.clear_registered_op_protos()
36 bridge.clear_op_impl_holders()39 bridge.clear_op_impl_holders()
37 bridge.clear_loaded_op_impl_modules()40 bridge.clear_loaded_op_impl_modules()
38 yield41 yield
39 bridge.clear_op_impl_holders()42 bridge.clear_op_impl_holders()
40 bridge.clear_loaded_op_impl_modules()43 bridge.clear_loaded_op_impl_modules()
41 custom_op.clear_registered_op_impls()44 custom_op.clear_registered_op_impls()
45+ proto.clear_registered_op_protos()
42 46 
43 47 
44def _write_custom_op_module(48def _write_custom_op_module(
@@ -138,6 +142,33 @@ def test_bridge_validate_op_impl_descriptor_rejects_unknown_descriptor():
138 bridge.validate_op_impl_descriptor("missing-descriptor", None)142 bridge.validate_op_impl_descriptor("missing-descriptor", None)
139 143 
140 144 
145+def test_bridge_descriptor_snapshot_contains_proto_and_impl():
146+ @proto.register_op(op_type="SnapshotCustom")
147+ def infer_meta(x: runtime.TensorDesc, *, alpha: float = 1.0) -> runtime.TensorDesc:
148+ raise AssertionError("prototype registration must not execute infer_meta")
149+ 
150+ @custom_op.register_op_impl(op_type="SnapshotCustom")
151+ class SnapshotCustom(custom_op.EagerExecuteOp):
152+ def execute(self, x: Tensor, *, alpha: float) -> None:
153+ pass
154+ 
155+ snapshot = bridge.load_and_get_op_descriptors()
156+ 
157+ assert [item["op_type"] for item in snapshot["protos"]] == ["SnapshotCustom"]
158+ assert [item["op_type"] for item in snapshot["impls"]] == ["SnapshotCustom"]
159+ 
160+ 
161+def test_bridge_descriptor_snapshot_supports_proto_only():
162+ @proto.register_op(op_type="ProtoOnlyCustom")
163+ def infer_meta(x: runtime.TensorDesc) -> runtime.TensorDesc:
164+ raise AssertionError("prototype registration must not execute infer_meta")
165+ 
166+ snapshot = bridge.load_and_get_op_descriptors()
167+ 
168+ assert [item["op_type"] for item in snapshot["protos"]] == ["ProtoOnlyCustom"]
169+ assert snapshot["impls"] == []
170+ 
171+ 
141def test_register_op_impl_exports_descriptor_dict():172def test_register_op_impl_exports_descriptor_dict():
142 @custom_op.register_op_impl(op_type="AddCustom")173 @custom_op.register_op_impl(op_type="AddCustom")
143 class AddCustom(custom_op.EagerExecuteOp):174 class AddCustom(custom_op.EagerExecuteOp):
Mtests/ge/ut/ge/graph_ir/ge_custom_op_factory_unittest.cc+92-31
@@ -85,8 +85,8 @@ struct MockPythonCustomOpHolder {
85 bool executed{false};85 bool executed{false};
86};86};
87 87 
88-void *CreateMockPythonCustomOpHolder(const PythonCustomOpDescriptor *desc) {88+void *CreateMockPythonCustomOpHolder(const PythonCustomOpAdapterDescriptorView *desc) {
89- if (desc == nullptr) {89+ if ((desc == nullptr) || (desc->impl_descriptor_key.data == nullptr)) {
90 return nullptr;90 return nullptr;
91 }91 }
92 return new (std::nothrow) MockPythonCustomOpHolder();92 return new (std::nothrow) MockPythonCustomOpHolder();
@@ -111,6 +111,14 @@ graphStatus DeclareMockPythonCustomOp(const void *holder, gert::AnnotatedArgsCon
111 return (holder == nullptr) ? GRAPH_FAILED : GRAPH_SUCCESS;111 return (holder == nullptr) ? GRAPH_FAILED : GRAPH_SUCCESS;
112}112}
113 113 
114+void *FailCreatePythonCustomOpHolder(const PythonCustomOpAdapterDescriptorView *) {
115+ return nullptr;
116+}
117+ 
118+graphStatus FailExecutePythonCustomOp(const void *, gert::EagerOpExecutionContext *) {
119+ return GRAPH_FAILED;
120+}
121+ 
114std::vector<uint8_t> BuildCustomOpPartition(const std::string &name, const std::vector<uint8_t> &bin) {122std::vector<uint8_t> BuildCustomOpPartition(const std::string &name, const std::vector<uint8_t> &bin) {
115 ge::CustomKernelItemHeader header{ge::kCustomKernelItemMagic, static_cast<uint32_t>(name.size()),123 ge::CustomKernelItemHeader header{ge::kCustomKernelItemMagic, static_cast<uint32_t>(name.size()),
116 static_cast<uint32_t>(bin.size())};124 static_cast<uint32_t>(bin.size())};
@@ -225,17 +233,17 @@ TEST(UtestCustomOpCast, falls_back_to_dynamic_cast_for_cpp_custom_op) {
225}233}
226 234 
227TEST(UtestCustomOpCast, filters_python_adapter_by_capability) {235TEST(UtestCustomOpCast, filters_python_adapter_by_capability) {
228- PythonCustomOpDescriptor desc;236+ PythonCustomOpAdapterDescriptor desc;
229- desc.descriptor_key = "python_adapter_eager_only";237+ desc.impl_descriptor_key = "python_adapter_eager_only";
230 desc.op_type = "PythonAdapterEagerOnly";238 desc.op_type = "PythonAdapterEagerOnly";
231 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);239 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);
232 240 
233- PythonCustomOpCallbacks callbacks;241+ PythonCustomOpAdapterCallbacks callbacks;
234- callbacks.create = CreateMockPythonCustomOpHolder;242+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
235- callbacks.destroy = DestroyMockPythonCustomOpHolder;243+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
236 callbacks.execute = ExecuteMockPythonCustomOp;244 callbacks.execute = ExecuteMockPythonCustomOp;
237 245 
238- ASSERT_TRUE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));246+ ASSERT_TRUE(PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks));
239 {247 {
240 PythonCustomOpAdapter adapter(desc);248 PythonCustomOpAdapter adapter(desc);
241 EXPECT_TRUE(adapter.IsValid());249 EXPECT_TRUE(adapter.IsValid());
@@ -250,24 +258,23 @@ TEST(UtestCustomOpCast, filters_python_adapter_by_capability) {
250 258 
251 EXPECT_EQ(GRAPH_SUCCESS, CustomOpCast<EagerExecuteOp>(base)->Execute(nullptr));259 EXPECT_EQ(GRAPH_SUCCESS, CustomOpCast<EagerExecuteOp>(base)->Execute(nullptr));
252 EXPECT_EQ(GRAPH_FAILED, adapter.Compile(nullptr));260 EXPECT_EQ(GRAPH_FAILED, adapter.Compile(nullptr));
253- EXPECT_FALSE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));261+ EXPECT_FALSE(PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key));
254- PythonCustomOpRuntimeRegistry::Clear();
255 }262 }
256- EXPECT_FALSE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));263+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key));
257}264}
258 265 
259TEST(UtestCustomOpCast, filters_python_adapter_annotated_args_by_capability) {266TEST(UtestCustomOpCast, filters_python_adapter_annotated_args_by_capability) {
260- PythonCustomOpDescriptor desc;267+ PythonCustomOpAdapterDescriptor desc;
261- desc.descriptor_key = "python_adapter_annotated_args_only";268+ desc.impl_descriptor_key = "python_adapter_annotated_args_only";
262 desc.op_type = "PythonAdapterAnnotatedArgsOnly";269 desc.op_type = "PythonAdapterAnnotatedArgsOnly";
263 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kAnnotatedArgs);270 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kAnnotatedArgs);
264 271 
265- PythonCustomOpCallbacks callbacks;272+ PythonCustomOpAdapterCallbacks callbacks;
266- callbacks.create = CreateMockPythonCustomOpHolder;273+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
267- callbacks.destroy = DestroyMockPythonCustomOpHolder;274+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
268 callbacks.declare_launch_args = DeclareMockPythonCustomOp;275 callbacks.declare_launch_args = DeclareMockPythonCustomOp;
269 276 
270- ASSERT_TRUE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));277+ ASSERT_TRUE(PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks));
271 {278 {
272 PythonCustomOpAdapter adapter(desc);279 PythonCustomOpAdapter adapter(desc);
273 EXPECT_TRUE(adapter.IsValid());280 EXPECT_TRUE(adapter.IsValid());
@@ -277,23 +284,23 @@ TEST(UtestCustomOpCast, filters_python_adapter_annotated_args_by_capability) {
277 EXPECT_NE(nullptr, CustomOpCast<AnnotatedArgsOp>(base));284 EXPECT_NE(nullptr, CustomOpCast<AnnotatedArgsOp>(base));
278 EXPECT_EQ(nullptr, CustomOpCast<CompilableOp>(base));285 EXPECT_EQ(nullptr, CustomOpCast<CompilableOp>(base));
279 }286 }
280- EXPECT_TRUE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));287+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key));
281}288}
282 289 
283TEST(UtestCustomOpCast, exposes_each_python_adapter_capability_in_dual_mode) {290TEST(UtestCustomOpCast, exposes_each_python_adapter_capability_in_dual_mode) {
284- PythonCustomOpDescriptor desc;291+ PythonCustomOpAdapterDescriptor desc;
285- desc.descriptor_key = "python_adapter_dual_capability";292+ desc.impl_descriptor_key = "python_adapter_dual_capability";
286 desc.op_type = "PythonAdapterDualCapability";293 desc.op_type = "PythonAdapterDualCapability";
287 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);294 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);
288 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kAnnotatedArgs);295 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kAnnotatedArgs);
289 296 
290- PythonCustomOpCallbacks callbacks;297+ PythonCustomOpAdapterCallbacks callbacks;
291- callbacks.create = CreateMockPythonCustomOpHolder;298+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
292- callbacks.destroy = DestroyMockPythonCustomOpHolder;299+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
293 callbacks.execute = ExecuteMockPythonCustomOp;300 callbacks.execute = ExecuteMockPythonCustomOp;
294 callbacks.declare_launch_args = DeclareMockPythonCustomOp;301 callbacks.declare_launch_args = DeclareMockPythonCustomOp;
295 302 
296- ASSERT_TRUE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));303+ ASSERT_TRUE(PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks));
297 {304 {
298 PythonCustomOpAdapter adapter(desc);305 PythonCustomOpAdapter adapter(desc);
299 EXPECT_TRUE(adapter.IsValid());306 EXPECT_TRUE(adapter.IsValid());
@@ -301,20 +308,74 @@ TEST(UtestCustomOpCast, exposes_each_python_adapter_capability_in_dual_mode) {
301 EXPECT_NE(nullptr, CustomOpCast<EagerExecuteOp>(base));308 EXPECT_NE(nullptr, CustomOpCast<EagerExecuteOp>(base));
302 EXPECT_NE(nullptr, CustomOpCast<AnnotatedArgsOp>(base));309 EXPECT_NE(nullptr, CustomOpCast<AnnotatedArgsOp>(base));
303 }310 }
304- EXPECT_TRUE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));311+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key));
305}312}
306 313 
307TEST(UtestCustomOpCast, rejects_unsupported_python_adapter_capability) {314TEST(UtestCustomOpCast, rejects_unsupported_python_adapter_capability) {
308- PythonCustomOpDescriptor desc;315+ PythonCustomOpAdapterDescriptor desc;
309- desc.descriptor_key = "python_adapter_shape_unsupported";316+ desc.impl_descriptor_key = "python_adapter_shape_unsupported";
310 desc.op_type = "PythonAdapterShapeUnsupported";317 desc.op_type = "PythonAdapterShapeUnsupported";
311 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kShapeInfer);318 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kShapeInfer);
312 319 
313- PythonCustomOpCallbacks callbacks;320+ PythonCustomOpAdapterCallbacks callbacks;
314- callbacks.create = CreateMockPythonCustomOpHolder;321+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
315- callbacks.destroy = DestroyMockPythonCustomOpHolder;322+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
316 323 
317- EXPECT_FALSE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));324+ EXPECT_FALSE(PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks));
325+}
326+ 
327+TEST(UtestCustomOpCast, rejects_python_adapter_with_missing_impl_key_or_callback) {
328+ PythonCustomOpAdapterDescriptor desc;
329+ desc.impl_descriptor_key = "python_adapter_required_fields";
330+ desc.op_type = "PythonAdapterRequiredFields";
331+ AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);
332+ 
333+ PythonCustomOpAdapterCallbacks callbacks;
334+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
335+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
336+ callbacks.execute = ExecuteMockPythonCustomOp;
337+ auto invalid_callbacks = callbacks;
338+ invalid_callbacks.create_impl_holder = nullptr;
339+ EXPECT_FALSE(invalid_callbacks.IsValid(desc.capabilities));
340+ invalid_callbacks = callbacks;
341+ invalid_callbacks.destroy_impl_holder = nullptr;
342+ EXPECT_FALSE(invalid_callbacks.IsValid(desc.capabilities));
343+ invalid_callbacks = callbacks;
344+ invalid_callbacks.execute = nullptr;
345+ EXPECT_FALSE(invalid_callbacks.IsValid(desc.capabilities));
346+}
347+ 
348+TEST(UtestCustomOpCast, releases_runtime_lease_when_holder_creation_fails) {
349+ PythonCustomOpAdapterCallbacks callbacks;
350+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
351+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
352+ callbacks.execute = ExecuteMockPythonCustomOp;
353+ PythonCustomOpAdapterDescriptor create_desc;
354+ create_desc.impl_descriptor_key = "python_adapter_fail_create";
355+ create_desc.op_type = "PythonAdapterFailCreate";
356+ AddCustomOpCapability(create_desc.capabilities, CustomOpCapability::kEagerExecute);
357+ callbacks.create_impl_holder = FailCreatePythonCustomOpHolder;
358+ ASSERT_TRUE(PythonCustomOpImplRuntimeRegistry::Register(create_desc, callbacks));
359+ {
360+ PythonCustomOpAdapter adapter(create_desc);
361+ EXPECT_FALSE(adapter.IsValid());
362+ }
363+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Unregister(create_desc.impl_descriptor_key));
364+ 
365+ PythonCustomOpAdapterDescriptor execute_desc;
366+ execute_desc.impl_descriptor_key = "python_adapter_fail_execute";
367+ execute_desc.op_type = "PythonAdapterFailExecute";
368+ AddCustomOpCapability(execute_desc.capabilities, CustomOpCapability::kEagerExecute);
369+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
370+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
371+ callbacks.execute = FailExecutePythonCustomOp;
372+ ASSERT_TRUE(PythonCustomOpImplRuntimeRegistry::Register(execute_desc, callbacks));
373+ {
374+ PythonCustomOpAdapter adapter(execute_desc);
375+ ASSERT_TRUE(adapter.IsValid());
376+ EXPECT_EQ(adapter.Execute(nullptr), GRAPH_FAILED);
377+ }
378+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Unregister(execute_desc.impl_descriptor_key));
318}379}
319 380 
320TEST(UtestCustomOpRegistry, load_custom_ops_partition_deserializes_registered_portable_op) {381TEST(UtestCustomOpRegistry, load_custom_ops_partition_deserializes_registered_portable_op) {
Atests/ge/ut/ge/runtime/custom_op/python_custom_op_bridge_loader_fake_bridge.cc+193-0
@@ -0,0 +1,193 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#include <atomic>
12+#include <cstdint>
13+#include <cstdlib>
14+#include <cstring>
15+ 
16+#include "runtime/custom_op/python_custom_op_bridge_c_api.h"
17+#include "runtime/custom_op/python_custom_op_bridge_types.h"
18+ 
19+namespace ge {
20+namespace custom_op {
21+namespace {
22+constexpr const char *kScenarioEnvName = "GE_PYTHON_CUSTOM_OP_LOADER_UT_SCENARIO";
23+constexpr const char *kMultiProtoFailure = "multi_proto_failure";
24+constexpr const char *kAdapterFailure = "adapter_failure";
25+constexpr const char *kSuccess = "success";
26+constexpr const char *kCppProtoImpl = "cpp_proto_impl";
27+ 
28+constexpr const char *kMultiProtoOp = "PythonLoaderMultiProtoRollbackUt";
29+constexpr const char *kAdapterOpA = "PythonLoaderAdapterRollbackAUt";
30+constexpr const char *kAdapterOpB = "PythonLoaderAdapterRollbackBUt";
31+constexpr const char *kSuccessOp = "PythonLoaderSuccessUt";
32+constexpr const char *kCppProtoOp = "PythonLoaderCppProtoOwnershipUt";
33+ 
34+constexpr const char *kMultiProtoKey = "loader_ut:multi_proto";
35+constexpr const char *kAdapterProtoKeyA = "loader_ut:adapter_proto_a";
36+constexpr const char *kAdapterProtoKeyB = "loader_ut:adapter_proto_b";
37+constexpr const char *kAdapterImplKeyA = "loader_ut:adapter_impl_a";
38+constexpr const char *kAdapterImplKeyB = "loader_ut:adapter_impl_b";
39+constexpr const char *kSuccessProtoKey = "loader_ut:success_proto";
40+constexpr const char *kSuccessImplKey = "loader_ut:success_impl";
41+constexpr const char *kCppProtoImplKey = "loader_ut:cpp_proto_impl";
42+ 
43+std::atomic<uint32_t> g_register_count{0U};
44+std::atomic<uint32_t> g_reset_count{0U};
45+int g_holder = 0;
46+ 
47+PythonCustomOpStringView StringView(const char *value) {
48+ return PythonCustomOpStringView{value, (value == nullptr) ? 0U : std::strlen(value)};
49+}
50+ 
51+PythonCustomOpProtoDescriptorView MakeProto(const char *descriptor_key, const char *op_type) {
52+ return PythonCustomOpProtoDescriptorView{
53+ StringView(descriptor_key), StringView(op_type), nullptr, 0U, nullptr, 0U, nullptr, 0U};
54+}
55+ 
56+PythonCustomOpAdapterDescriptorView MakeAdapter(const char *op_type, const char *impl_key) {
57+ return PythonCustomOpAdapterDescriptorView{StringView(op_type), StringView(impl_key),
58+ static_cast<CustomOpCapabilityMask>(CustomOpCapability::kEagerExecute)};
59+}
60+ 
61+void *CreateImplHolder(const PythonCustomOpAdapterDescriptorView *desc) {
62+ return (desc == nullptr) ? nullptr : &g_holder;
63+}
64+ 
65+void DestroyImplHolder(void *holder) {
66+ (void)holder;
67+}
68+ 
69+graphStatus Execute(const void *holder, gert::EagerOpExecutionContext *ctx) {
70+ (void)ctx;
71+ return (holder == nullptr) ? GRAPH_FAILED : GRAPH_SUCCESS;
72+}
73+ 
74+PythonCustomOpAdapterCallbacks MakeCallbacks(const bool reject) {
75+ PythonCustomOpAdapterCallbacks callbacks;
76+ callbacks.create_impl_holder = &CreateImplHolder;
77+ callbacks.destroy_impl_holder = &DestroyImplHolder;
78+ callbacks.execute = &Execute;
79+ if (reject) {
80+ callbacks.create_impl_holder = nullptr;
81+ }
82+ (void)reject;
83+ return callbacks;
84+}
85+ 
86+Status SetArtifactConfig(const PythonCustomOpBridgeArtifactConfig *config) {
87+ if ((config == nullptr) || (config->artifact_root == nullptr) || (config->native_module_path == nullptr)) {
88+ return static_cast<Status>(GRAPH_FAILED);
89+ }
90+ return static_cast<Status>(GRAPH_SUCCESS);
91+}
92+ 
93+Status RegisterMultiProtoFailure(const PythonCustomOpRegistrar &registrar) {
94+ const auto first = MakeProto(kMultiProtoKey, kMultiProtoOp);
95+ if (!registrar.register_op_proto(&first)) {
96+ return static_cast<Status>(GRAPH_FAILED);
97+ }
98+ const auto invalid = MakeProto("loader_ut:invalid_proto", "");
99+ return static_cast<Status>(registrar.register_op_proto(&invalid) ? GRAPH_SUCCESS : GRAPH_FAILED);
100+}
101+ 
102+Status RegisterAdapterFailure(const PythonCustomOpRegistrar &registrar) {
103+ const auto proto_a = MakeProto(kAdapterProtoKeyA, kAdapterOpA);
104+ const auto proto_b = MakeProto(kAdapterProtoKeyB, kAdapterOpB);
105+ if ((!registrar.register_op_proto(&proto_a)) || (!registrar.register_op_proto(&proto_b))) {
106+ return static_cast<Status>(GRAPH_FAILED);
107+ }
108+ 
109+ const auto callbacks = MakeCallbacks(false);
110+ const auto adapter_a = MakeAdapter(kAdapterOpA, kAdapterImplKeyA);
111+ if (!registrar.register_op_adapter(&adapter_a, &callbacks)) {
112+ return static_cast<Status>(GRAPH_FAILED);
113+ }
114+ const auto rejecting_callbacks = MakeCallbacks(true);
115+ const auto adapter_b = MakeAdapter(kAdapterOpB, kAdapterImplKeyB);
116+ return static_cast<Status>(registrar.register_op_adapter(&adapter_b, &rejecting_callbacks) ? GRAPH_SUCCESS
117+ : GRAPH_FAILED);
118+}
119+ 
120+Status RegisterSuccess(const PythonCustomOpRegistrar &registrar) {
121+ const auto proto = MakeProto(kSuccessProtoKey, kSuccessOp);
122+ if (!registrar.register_op_proto(&proto)) {
123+ return static_cast<Status>(GRAPH_FAILED);
124+ }
125+ const auto callbacks = MakeCallbacks(false);
126+ const auto adapter = MakeAdapter(kSuccessOp, kSuccessImplKey);
127+ return static_cast<Status>(registrar.register_op_adapter(&adapter, &callbacks) ? GRAPH_SUCCESS : GRAPH_FAILED);
128+}
129+ 
130+Status RegisterCppProtoImpl(const PythonCustomOpRegistrar &registrar) {
131+ const auto callbacks = MakeCallbacks(false);
132+ const auto adapter = MakeAdapter(kCppProtoOp, kCppProtoImplKey);
133+ return static_cast<Status>(registrar.register_op_adapter(&adapter, &callbacks) ? GRAPH_SUCCESS : GRAPH_FAILED);
134+}
135+ 
136+Status RegisterCustomOps(const PythonCustomOpRegistrar *registrar) {
137+ ++g_register_count;
138+ if ((registrar == nullptr) || (registrar->register_op_proto == nullptr) ||
139+ (registrar->register_op_adapter == nullptr)) {
140+ return static_cast<Status>(GRAPH_FAILED);
141+ }
142+ const char *scenario = std::getenv(kScenarioEnvName);
143+ if (scenario == nullptr) {
144+ return static_cast<Status>(GRAPH_FAILED);
145+ }
146+ if (std::strcmp(scenario, kMultiProtoFailure) == 0) {
147+ return RegisterMultiProtoFailure(*registrar);
148+ }
149+ if (std::strcmp(scenario, kAdapterFailure) == 0) {
150+ return RegisterAdapterFailure(*registrar);
151+ }
152+ if (std::strcmp(scenario, kSuccess) == 0) {
153+ return RegisterSuccess(*registrar);
154+ }
155+ if (std::strcmp(scenario, kCppProtoImpl) == 0) {
156+ return RegisterCppProtoImpl(*registrar);
157+ }
158+ return static_cast<Status>(GRAPH_FAILED);
159+}
160+ 
161+void ResetBridgeState() {
162+ ++g_reset_count;
163+}
164+ 
165+void ShutdownBridge() {}
166+} // namespace
167+} // namespace custom_op
168+} // namespace ge
169+ 
170+extern "C" __attribute__((visibility("default"))) uint32_t GePythonCustomOpLoaderUtGetRegisterCount() {
171+ return ge::custom_op::g_register_count.load();
172+}
173+ 
174+extern "C" __attribute__((visibility("default"))) uint32_t GePythonCustomOpLoaderUtGetResetCount() {
175+ return ge::custom_op::g_reset_count.load();
176+}
177+ 
178+extern "C" __attribute__((visibility("default"))) void GePythonCustomOpLoaderUtResetCounters() {
179+ ge::custom_op::g_register_count.store(0U);
180+ ge::custom_op::g_reset_count.store(0U);
181+}
182+ 
183+extern "C" __attribute__((visibility("default"))) const ge::custom_op::PythonCustomOpBridgeApi *
184+GeGetPythonCustomOpBridgeApi() {
185+ static const ge::custom_op::PythonCustomOpBridgeApi kBridgeApi = {
186+ ge::custom_op::kPythonCustomOpBridgeAbiVersion,
187+ &ge::custom_op::SetArtifactConfig,
188+ &ge::custom_op::RegisterCustomOps,
189+ &ge::custom_op::ResetBridgeState,
190+ &ge::custom_op::ShutdownBridge,
191+ };
192+ return &kBridgeApi;
193+}
Atests/ge/ut/ge/runtime/custom_op/python_custom_op_bridge_loader_unittest.cc+463-0
@@ -0,0 +1,463 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#include <gtest/gtest.h>
12+ 
13+#include <dlfcn.h>
14+#include <sys/stat.h>
15+#include <unistd.h>
16+ 
17+#include <cerrno>
18+#include <cstdint>
19+#include <cstdlib>
20+#include <fstream>
21+#include <memory>
22+#include <string>
23+#include <utility>
24+#include <vector>
25+ 
26+#include "common/python_runtime/python_artifact_utils.h"
27+#include "graph/ascend_string.h"
28+#include "graph/custom_op_factory.h"
29+#include "graph/operator.h"
30+#include "graph/operator_factory.h"
31+#include "graph/operator_factory_impl.h"
32+#include "runtime/custom_op/python_custom_op_adapter.h"
33+#include "runtime/custom_op/python_custom_op_bridge_loader.h"
34+ 
35+#ifndef PYTHON_CUSTOM_OP_LOADER_UT_FAKE_BRIDGE_PATH
36+#define PYTHON_CUSTOM_OP_LOADER_UT_FAKE_BRIDGE_PATH ""
37+#endif
38+ 
39+namespace {
40+constexpr const char *kScenarioEnvName = "GE_PYTHON_CUSTOM_OP_LOADER_UT_SCENARIO";
41+constexpr const char *kPythonPathEnvName = "PYTHONPATH";
42+bool g_fake_python_initialized = false;
43+bool g_fake_python_threads_initialized = false;
44+} // namespace
45+ 
46+extern "C" int Py_IsInitialized() {
47+ return g_fake_python_initialized ? 1 : 0;
48+}
49+ 
50+extern "C" const char *Py_GetVersion() {
51+ return "3.11.0";
52+}
53+ 
54+extern "C" void Py_Initialize() {
55+ g_fake_python_initialized = true;
56+}
57+ 
58+extern "C" void Py_Finalize() {
59+ g_fake_python_initialized = false;
60+ g_fake_python_threads_initialized = false;
61+}
62+ 
63+extern "C" void PyEval_InitThreads() {
64+ g_fake_python_threads_initialized = true;
65+}
66+ 
67+extern "C" int PyEval_ThreadsInitialized() {
68+ return g_fake_python_threads_initialized ? 1 : 0;
69+}
70+ 
71+extern "C" void *PyEval_SaveThread() {
72+ return reinterpret_cast<void *>(0x100);
73+}
74+ 
75+extern "C" void PyEval_RestoreThread(void *thread_state) {
76+ (void)thread_state;
77+}
78+ 
79+extern "C" int PyGILState_Check() {
80+ return 1;
81+}
82+ 
83+namespace ge {
84+namespace custom_op {
85+namespace {
86+constexpr const char *kMultiProtoFailure = "multi_proto_failure";
87+constexpr const char *kAdapterFailure = "adapter_failure";
88+constexpr const char *kSuccess = "success";
89+constexpr const char *kCppProtoImpl = "cpp_proto_impl";
90+ 
91+constexpr const char *kMultiProtoOp = "PythonLoaderMultiProtoRollbackUt";
92+constexpr const char *kAdapterOpA = "PythonLoaderAdapterRollbackAUt";
93+constexpr const char *kAdapterOpB = "PythonLoaderAdapterRollbackBUt";
94+constexpr const char *kSuccessOp = "PythonLoaderSuccessUt";
95+constexpr const char *kCppProtoOp = "PythonLoaderCppProtoOwnershipUt";
96+ 
97+constexpr const char *kAdapterImplKeyA = "loader_ut:adapter_impl_a";
98+constexpr const char *kAdapterImplKeyB = "loader_ut:adapter_impl_b";
99+constexpr const char *kSuccessImplKey = "loader_ut:success_impl";
100+constexpr const char *kCppProtoImplKey = "loader_ut:cpp_proto_impl";
101+ 
102+constexpr const char *kFakeBridgeArtifactName = "libge_python_custom_op_loader_ut_fake.so";
103+constexpr const char *kFakeNativeArtifactName = "_ge_custom_op_loader_ut_fake.so";
104+ 
105+class PreexistingSuccessCustomOp final : public BaseCustomOp {};
106+ 
107+class ScopedEnvVar {
108+ public:
109+ ScopedEnvVar(const char *name, const std::string &value) : name_(name) {
110+ const char *old_value = std::getenv(name);
111+ if (old_value != nullptr) {
112+ old_value_ = old_value;
113+ has_old_value_ = true;
114+ }
115+ (void)setenv(name_.c_str(), value.c_str(), 1);
116+ }
117+ 
118+ ~ScopedEnvVar() {
119+ if (has_old_value_) {
120+ (void)setenv(name_.c_str(), old_value_.c_str(), 1);
121+ } else {
122+ (void)unsetenv(name_.c_str());
123+ }
124+ }
125+ 
126+ ScopedEnvVar(const ScopedEnvVar &) = delete;
127+ ScopedEnvVar &operator=(const ScopedEnvVar &) = delete;
128+ 
129+ private:
130+ std::string name_;
131+ std::string old_value_;
132+ bool has_old_value_{false};
133+};
134+ 
135+class ScopedArtifactTree {
136+ public:
137+ bool Prepare(const std::string &bridge_target, const std::string &python_tag) {
138+ if (bridge_target.empty() || python_tag.empty() || (access(bridge_target.c_str(), R_OK) != 0)) {
139+ return false;
140+ }
141+ char dir_template[] = "/tmp/ge_python_custom_op_loader_ut_XXXXXX";
142+ const char *created_dir = mkdtemp(dir_template);
143+ if (created_dir == nullptr) {
144+ return false;
145+ }
146+ root_ = created_dir;
147+ const std::vector<std::string> dirs = {
148+ Path("site-packages"),
149+ Path("site-packages/ge"),
150+ Path("site-packages/ge/custom_op"),
151+ Path("site-packages/ge/custom_op/python_custom_op_artifacts"),
152+ ArtifactRoot(),
153+ };
154+ for (const auto &dir : dirs) {
155+ if ((mkdir(dir.c_str(), 0700) != 0) && (errno != EEXIST)) {
156+ return false;
157+ }
158+ dirs_.emplace_back(dir);
159+ }
160+ 
161+ const auto bridge_path = ArtifactRoot() + "/" + kFakeBridgeArtifactName;
162+ if (symlink(bridge_target.c_str(), bridge_path.c_str()) != 0) {
163+ return false;
164+ }
165+ files_.emplace_back(bridge_path);
166+ if (!WriteFile(ArtifactRoot() + "/" + kFakeNativeArtifactName, "fake native artifact")) {
167+ return false;
168+ }
169+ const std::string manifest =
170+ "{\n"
171+ " \"python_tag\": \"" +
172+ python_tag +
173+ "\",\n"
174+ " \"platform\": \"" +
175+ python_artifact::CurrentPlatformTag() +
176+ "\",\n"
177+ " \"bridge_abi\": 1,\n"
178+ " \"artifacts\": {\n"
179+ " \"bridge\": \"" +
180+ kFakeBridgeArtifactName +
181+ "\",\n"
182+ " \"native\": \"" +
183+ kFakeNativeArtifactName +
184+ "\"\n"
185+ " }\n"
186+ "}\n";
187+ return WriteFile(ArtifactRoot() + "/manifest.json", manifest);
188+ }
189+ 
190+ ~ScopedArtifactTree() {
191+ for (auto iter = files_.rbegin(); iter != files_.rend(); ++iter) {
192+ (void)remove(iter->c_str());
193+ }
194+ for (auto iter = dirs_.rbegin(); iter != dirs_.rend(); ++iter) {
195+ (void)rmdir(iter->c_str());
196+ }
197+ if (!root_.empty()) {
198+ (void)rmdir(root_.c_str());
199+ }
200+ }
201+ 
202+ std::string PythonPath() const {
203+ return Path("site-packages");
204+ }
205+ 
206+ ScopedArtifactTree(const ScopedArtifactTree &) = delete;
207+ ScopedArtifactTree &operator=(const ScopedArtifactTree &) = delete;
208+ ScopedArtifactTree() = default;
209+ 
210+ private:
211+ std::string Path(const std::string &relative_path) const {
212+ return root_ + "/" + relative_path;
213+ }
214+ 
215+ std::string ArtifactRoot() const {
216+ return Path("site-packages/ge/custom_op/python_custom_op_artifacts/fake");
217+ }
218+ 
219+ bool WriteFile(const std::string &path, const std::string &content) {
220+ std::ofstream output(path, std::ios::out | std::ios::trunc);
221+ if (!output.is_open()) {
222+ return false;
223+ }
224+ files_.emplace_back(path);
225+ output << content;
226+ output.close();
227+ return output.good();
228+ }
229+ 
230+ std::string root_;
231+ std::vector<std::string> files_;
232+ std::vector<std::string> dirs_;
233+};
234+ 
235+PythonCustomOpAdapterDescriptor MakeAdapterDescriptor(const char *op_type, const char *impl_key) {
236+ PythonCustomOpAdapterDescriptor desc;
237+ desc.op_type = op_type;
238+ desc.impl_descriptor_key = impl_key;
239+ AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);
240+ return desc;
241+}
242+ 
243+class ScopedRuntimeLease {
244+ public:
245+ explicit ScopedRuntimeLease(PythonCustomOpAdapterDescriptor desc) : desc_(std::move(desc)) {}
246+ 
247+ bool Acquire() {
248+ active_ = PythonCustomOpImplRuntimeRegistry::Acquire(desc_, callbacks_);
249+ return active_;
250+ }
251+ 
252+ void Release() {
253+ if (active_) {
254+ PythonCustomOpImplRuntimeRegistry::Release(desc_);
255+ active_ = false;
256+ }
257+ }
258+ 
259+ ~ScopedRuntimeLease() {
260+ Release();
261+ }
262+ 
263+ private:
264+ PythonCustomOpAdapterDescriptor desc_;
265+ PythonCustomOpAdapterCallbacks callbacks_;
266+ bool active_{false};
267+};
268+ 
269+uint32_t ReadFakeBridgeCounter(const char *symbol) {
270+ using GetCounterFn = uint32_t (*)();
271+ auto *get_counter = reinterpret_cast<GetCounterFn>(dlsym(RTLD_DEFAULT, symbol));
272+ if (get_counter == nullptr) {
273+ ADD_FAILURE() << "fake bridge counter symbol is missing: " << symbol;
274+ return 0U;
275+ }
276+ return get_counter();
277+}
278+ 
279+uint32_t GetRegisterCount() {
280+ return ReadFakeBridgeCounter("GePythonCustomOpLoaderUtGetRegisterCount");
281+}
282+ 
283+uint32_t GetResetCount() {
284+ return ReadFakeBridgeCounter("GePythonCustomOpLoaderUtGetResetCount");
285+}
286+ 
287+void ResetFakeBridgeCountersIfLoaded() {
288+ using ResetCountersFn = void (*)();
289+ auto *reset_counters =
290+ reinterpret_cast<ResetCountersFn>(dlsym(RTLD_DEFAULT, "GePythonCustomOpLoaderUtResetCounters"));
291+ if (reset_counters != nullptr) {
292+ reset_counters();
293+ }
294+}
295+ 
296+std::vector<AscendString> AllAdapterOpTypes() {
297+ return {AscendString(kAdapterOpA), AscendString(kAdapterOpB), AscendString(kSuccessOp), AscendString(kCppProtoOp)};
298+}
299+ 
300+std::vector<std::string> AllProtoOpTypes() {
301+ return {kMultiProtoOp, kAdapterOpA, kAdapterOpB, kSuccessOp, kCppProtoOp};
302+}
303+ 
304+void ClearKnownRegistrationState() {
305+ CustomOpFactory::RemoveCustomOps(AllAdapterOpTypes());
306+ ClearPythonCustomOpRuntimeRegistry();
307+ OperatorFactoryImpl::RemoveCustomOpCreators(AllProtoOpTypes());
308+}
309+ 
310+class PythonCustomOpBridgeLoaderTest : public testing::Test {
311+ protected:
312+ void SetUp() override {
313+ ShutdownPythonCustomOpsForProcess();
314+ ClearKnownRegistrationState();
315+ ResetFakeBridgeCountersIfLoaded();
316+ Py_Initialize();
317+ const auto runtime_key = python_artifact::ResolveLoadedPythonRuntimeKey();
318+ ASSERT_TRUE(runtime_key.has_python_symbols);
319+ ASSERT_TRUE(runtime_key.is_initialized);
320+ ASSERT_FALSE(runtime_key.python_tag.empty());
321+ ASSERT_TRUE(artifact_tree_.Prepare(PYTHON_CUSTOM_OP_LOADER_UT_FAKE_BRIDGE_PATH, runtime_key.python_tag));
322+ python_path_env_ = std::make_unique<ScopedEnvVar>(kPythonPathEnvName, artifact_tree_.PythonPath());
323+ }
324+ 
325+ void TearDown() override {
326+ ShutdownPythonCustomOpsForProcess();
327+ ClearKnownRegistrationState();
328+ scenario_env_.reset();
329+ python_path_env_.reset();
330+ Py_Finalize();
331+ }
332+ 
333+ void SetScenario(const char *scenario) {
334+ scenario_env_ = std::make_unique<ScopedEnvVar>(kScenarioEnvName, scenario);
335+ }
336+ 
337+ private:
338+ ScopedArtifactTree artifact_tree_;
339+ std::unique_ptr<ScopedEnvVar> python_path_env_;
340+ std::unique_ptr<ScopedEnvVar> scenario_env_;
341+};
342+ 
343+TEST_F(PythonCustomOpBridgeLoaderTest, keeps_partial_proto_registration_until_unload_after_proto_failure) {
344+ SetScenario(kMultiProtoFailure);
345+ 
346+ EXPECT_EQ(LoadPythonCustomOps(), FAILED);
347+ EXPECT_TRUE(OperatorFactory::IsExistOp(kMultiProtoOp));
348+ UnloadPythonCustomOps();
349+ EXPECT_FALSE(OperatorFactory::IsExistOp(kMultiProtoOp));
350+ EXPECT_EQ(GetRegisterCount(), 1U);
351+ EXPECT_EQ(GetResetCount(), 1U);
352+}
353+ 
354+TEST_F(PythonCustomOpBridgeLoaderTest, rolls_back_proto_adapter_and_runtime_entry_after_adapter_failure) {
355+ SetScenario(kAdapterFailure);
356+ 
357+ EXPECT_EQ(LoadPythonCustomOps(), FAILED);
358+ EXPECT_TRUE(OperatorFactory::IsExistOp(kAdapterOpA));
359+ EXPECT_TRUE(OperatorFactory::IsExistOp(kAdapterOpB));
360+ EXPECT_TRUE(CustomOpFactory::IsExistOp(AscendString(kAdapterOpA)));
361+ UnloadPythonCustomOps();
362+ EXPECT_FALSE(OperatorFactory::IsExistOp(kAdapterOpA));
363+ EXPECT_FALSE(OperatorFactory::IsExistOp(kAdapterOpB));
364+ EXPECT_FALSE(CustomOpFactory::IsExistOp(AscendString(kAdapterOpA)));
365+ 
366+ const auto desc = MakeAdapterDescriptor(kAdapterOpA, kAdapterImplKeyA);
367+ PythonCustomOpAdapterCallbacks callbacks;
368+ const bool acquired = PythonCustomOpImplRuntimeRegistry::Acquire(desc, callbacks);
369+ if (acquired) {
370+ PythonCustomOpImplRuntimeRegistry::Release(desc);
371+ }
372+ EXPECT_FALSE(acquired);
373+ EXPECT_EQ(GetRegisterCount(), 1U);
374+ EXPECT_EQ(GetResetCount(), 1U);
375+}
376+ 
377+TEST_F(PythonCustomOpBridgeLoaderTest, adapter_conflict_preserves_preexisting_cpp_custom_op_creator) {
378+ ASSERT_EQ(CustomOpFactory::RegisterCustomOpCreator(
379+ AscendString(kSuccessOp),
380+ []() -> std::unique_ptr<BaseCustomOp> { return std::make_unique<PreexistingSuccessCustomOp>(); }),
381+ GRAPH_SUCCESS);
382+ SetScenario(kSuccess);
383+ 
384+ EXPECT_EQ(LoadPythonCustomOps(), FAILED);
385+ UnloadPythonCustomOps();
386+ EXPECT_FALSE(OperatorFactory::IsExistOp(kSuccessOp));
387+ EXPECT_TRUE(CustomOpFactory::IsExistOp(AscendString(kSuccessOp)));
388+ EXPECT_NE(dynamic_cast<PreexistingSuccessCustomOp *>(CustomOpFactory::CreateOrGetCustomOp(AscendString(kSuccessOp))),
389+ nullptr);
390+ 
391+ PythonCustomOpAdapterCallbacks callbacks;
392+ const auto desc = MakeAdapterDescriptor(kSuccessOp, kSuccessImplKey);
393+ EXPECT_FALSE(PythonCustomOpImplRuntimeRegistry::Acquire(desc, callbacks));
394+ EXPECT_EQ(GetRegisterCount(), 1U);
395+ EXPECT_EQ(GetResetCount(), 1U);
396+}
397+ 
398+TEST_F(PythonCustomOpBridgeLoaderTest, direct_load_registers_each_call_and_unload_allows_reload) {
399+ SetScenario(kSuccess);
400+ 
401+ ASSERT_EQ(LoadPythonCustomOps(), SUCCESS);
402+ EXPECT_EQ(LoadPythonCustomOps(), SUCCESS);
403+ EXPECT_EQ(GetRegisterCount(), 2U);
404+ EXPECT_TRUE(OperatorFactory::IsExistOp(kSuccessOp));
405+ EXPECT_TRUE(CustomOpFactory::IsExistOp(AscendString(kSuccessOp)));
406+ EXPECT_NE(CustomOpFactory::CreateOrGetCustomOp(AscendString(kSuccessOp)), nullptr);
407+ 
408+ UnloadPythonCustomOps();
409+ EXPECT_FALSE(OperatorFactory::IsExistOp(kSuccessOp));
410+ EXPECT_FALSE(CustomOpFactory::IsExistOp(AscendString(kSuccessOp)));
411+ EXPECT_EQ(GetResetCount(), 1U);
412+ 
413+ ASSERT_EQ(LoadPythonCustomOps(), SUCCESS);
414+ EXPECT_EQ(GetRegisterCount(), 3U);
415+ EXPECT_TRUE(OperatorFactory::IsExistOp(kSuccessOp));
416+ UnloadPythonCustomOps();
417+ EXPECT_FALSE(OperatorFactory::IsExistOp(kSuccessOp));
418+ EXPECT_EQ(GetResetCount(), 2U);
419+ 
420+ PythonCustomOpAdapterCallbacks callbacks;
421+ const auto desc = MakeAdapterDescriptor(kSuccessOp, kSuccessImplKey);
422+ EXPECT_FALSE(PythonCustomOpImplRuntimeRegistry::Acquire(desc, callbacks));
423+}
424+ 
425+TEST_F(PythonCustomOpBridgeLoaderTest, unload_clears_runtime_registry_with_active_runtime_lease) {
426+ SetScenario(kSuccess);
427+ ASSERT_EQ(LoadPythonCustomOps(), SUCCESS);
428+ 
429+ ScopedRuntimeLease lease(MakeAdapterDescriptor(kSuccessOp, kSuccessImplKey));
430+ ASSERT_TRUE(lease.Acquire());
431+ UnloadPythonCustomOps();
432+ EXPECT_FALSE(CustomOpFactory::IsExistOp(AscendString(kSuccessOp)));
433+ EXPECT_FALSE(OperatorFactory::IsExistOp(kSuccessOp));
434+ EXPECT_EQ(GetResetCount(), 1U);
435+ lease.Release();
436+ 
437+ PythonCustomOpAdapterCallbacks callbacks;
438+ const auto desc = MakeAdapterDescriptor(kSuccessOp, kSuccessImplKey);
439+ EXPECT_FALSE(PythonCustomOpImplRuntimeRegistry::Acquire(desc, callbacks));
440+}
441+ 
442+TEST_F(PythonCustomOpBridgeLoaderTest, unload_preserves_preexisting_cpp_proto_used_by_python_impl) {
443+ const auto creator = [](const AscendString &name) -> Operator { return Operator(name, AscendString(kCppProtoOp)); };
444+ ASSERT_EQ(OperatorFactoryImpl::RegisterOperatorCreator(kCppProtoOp, creator), GRAPH_SUCCESS);
445+ SetScenario(kCppProtoImpl);
446+ 
447+ ASSERT_EQ(LoadPythonCustomOps(), SUCCESS);
448+ EXPECT_TRUE(OperatorFactory::IsExistOp(kCppProtoOp));
449+ EXPECT_TRUE(CustomOpFactory::IsExistOp(AscendString(kCppProtoOp)));
450+ 
451+ UnloadPythonCustomOps();
452+ EXPECT_TRUE(OperatorFactory::IsExistOp(kCppProtoOp));
453+ EXPECT_FALSE(OperatorFactory::CreateOperator("cpp_proto_instance", kCppProtoOp).IsEmpty());
454+ EXPECT_FALSE(CustomOpFactory::IsExistOp(AscendString(kCppProtoOp)));
455+ EXPECT_EQ(GetResetCount(), 1U);
456+ 
457+ PythonCustomOpAdapterCallbacks callbacks;
458+ const auto desc = MakeAdapterDescriptor(kCppProtoOp, kCppProtoImplKey);
459+ EXPECT_FALSE(PythonCustomOpImplRuntimeRegistry::Acquire(desc, callbacks));
460+}
461+} // namespace
462+} // namespace custom_op
463+} // namespace ge
Mtests/ge/ut/ge/runtime/custom_op/python_custom_op_ir_meta_unittest.cc+26-23
@@ -26,8 +26,10 @@ namespace bridge_loader = ::ge::python_bridge_loader;
26 26 
27struct MockPythonCustomOpHolder {};27struct MockPythonCustomOpHolder {};
28 28 
29-void *CreateMockPythonCustomOpHolder(const PythonCustomOpDescriptor *desc) {29+void *CreateMockPythonCustomOpHolder(const PythonCustomOpAdapterDescriptorView *desc) {
30- return (desc == nullptr) ? nullptr : new (std::nothrow) MockPythonCustomOpHolder();30+ return ((desc == nullptr) || (desc->impl_descriptor_key.data == nullptr)) ? nullptr
31+ : new (std::nothrow)
32+ MockPythonCustomOpHolder();
31}33}
32 34 
33void DestroyMockPythonCustomOpHolder(void *holder) {35void DestroyMockPythonCustomOpHolder(void *holder) {
@@ -145,60 +147,60 @@ bridge_loader::BridgeLoadStatus LoadMockPythonCustomOpBridge(
145} // namespace147} // namespace
146 148 
147TEST(PythonCustomOpAdapter, forwards_execute_without_ir_meta_pod) {149TEST(PythonCustomOpAdapter, forwards_execute_without_ir_meta_pod) {
148- PythonCustomOpDescriptor desc;150+ PythonCustomOpAdapterDescriptor desc;
149- desc.descriptor_key = "python_adapter_without_ir_meta";151+ desc.impl_descriptor_key = "python_adapter_without_ir_meta";
150 desc.op_type = "PythonCustomOpAdapterUt";152 desc.op_type = "PythonCustomOpAdapterUt";
151 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);153 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);
152 154 
153- PythonCustomOpCallbacks callbacks;155+ PythonCustomOpAdapterCallbacks callbacks;
154- callbacks.create = CreateMockPythonCustomOpHolder;156+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
155- callbacks.destroy = DestroyMockPythonCustomOpHolder;157+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
156 callbacks.execute = ExecuteMockPythonCustomOp;158 callbacks.execute = ExecuteMockPythonCustomOp;
157 159 
158- ASSERT_TRUE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));160+ ASSERT_TRUE(PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks));
159 {161 {
160 PythonCustomOpAdapter adapter(desc);162 PythonCustomOpAdapter adapter(desc);
161 ASSERT_TRUE(adapter.IsValid());163 ASSERT_TRUE(adapter.IsValid());
162 EXPECT_EQ(adapter.Execute(nullptr), GRAPH_SUCCESS);164 EXPECT_EQ(adapter.Execute(nullptr), GRAPH_SUCCESS);
163 }165 }
164- EXPECT_TRUE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));166+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key));
165}167}
166 168 
167TEST(PythonCustomOpAdapter, keeps_legacy_execute_without_registered_ir) {169TEST(PythonCustomOpAdapter, keeps_legacy_execute_without_registered_ir) {
168- PythonCustomOpDescriptor desc;170+ PythonCustomOpAdapterDescriptor desc;
169- desc.descriptor_key = "python_adapter_legacy_without_ir";171+ desc.impl_descriptor_key = "python_adapter_legacy_without_ir";
170 desc.op_type = "PythonCustomOpLegacyWithoutIrUt";172 desc.op_type = "PythonCustomOpLegacyWithoutIrUt";
171 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);173 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);
172 174 
173- PythonCustomOpCallbacks callbacks;175+ PythonCustomOpAdapterCallbacks callbacks;
174- callbacks.create = CreateMockPythonCustomOpHolder;176+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
175- callbacks.destroy = DestroyMockPythonCustomOpHolder;177+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
176 callbacks.execute = ExecuteMockPythonCustomOp;178 callbacks.execute = ExecuteMockPythonCustomOp;
177 179 
178- ASSERT_TRUE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));180+ ASSERT_TRUE(PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks));
179 {181 {
180 PythonCustomOpAdapter adapter(desc);182 PythonCustomOpAdapter adapter(desc);
181 ASSERT_TRUE(adapter.IsValid());183 ASSERT_TRUE(adapter.IsValid());
182 EXPECT_EQ(adapter.Execute(nullptr), GRAPH_SUCCESS);184 EXPECT_EQ(adapter.Execute(nullptr), GRAPH_SUCCESS);
183 }185 }
184- EXPECT_TRUE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));186+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key));
185}187}
186 188 
187TEST(PythonCustomOpAdapter, validates_annotated_args_callback_by_capability) {189TEST(PythonCustomOpAdapter, validates_annotated_args_callback_by_capability) {
188- PythonCustomOpDescriptor desc;190+ PythonCustomOpAdapterDescriptor desc;
189- desc.descriptor_key = "python_adapter_annotated_args_callback";191+ desc.impl_descriptor_key = "python_adapter_annotated_args_callback";
190 desc.op_type = "PythonCustomOpIrMetaUt";192 desc.op_type = "PythonCustomOpIrMetaUt";
191 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kAnnotatedArgs);193 AddCustomOpCapability(desc.capabilities, CustomOpCapability::kAnnotatedArgs);
192 194 
193- PythonCustomOpCallbacks callbacks;195+ PythonCustomOpAdapterCallbacks callbacks;
194- callbacks.create = CreateMockPythonCustomOpHolder;196+ callbacks.create_impl_holder = CreateMockPythonCustomOpHolder;
195- callbacks.destroy = DestroyMockPythonCustomOpHolder;197+ callbacks.destroy_impl_holder = DestroyMockPythonCustomOpHolder;
196 EXPECT_FALSE(callbacks.IsValid(desc.capabilities));198 EXPECT_FALSE(callbacks.IsValid(desc.capabilities));
197 199 
198 callbacks.declare_launch_args = DeclareMockPythonCustomOp;200 callbacks.declare_launch_args = DeclareMockPythonCustomOp;
199 EXPECT_TRUE(callbacks.IsValid(desc.capabilities));201 EXPECT_TRUE(callbacks.IsValid(desc.capabilities));
200- EXPECT_TRUE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));202+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Register(desc, callbacks));
201- EXPECT_TRUE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));203+ EXPECT_TRUE(PythonCustomOpImplRuntimeRegistry::Unregister(desc.impl_descriptor_key));
202}204}
203 205 
204TEST(PythonCustomOpBridgeAbi, rejects_mismatched_abi_before_registration_and_accepts_current) {206TEST(PythonCustomOpBridgeAbi, rejects_mismatched_abi_before_registration_and_accepts_current) {
@@ -218,5 +220,6 @@ TEST(PythonCustomOpBridgeAbi, rejects_mismatched_abi_before_registration_and_acc
218 EXPECT_EQ(loaded_bridge.api->register_custom_ops(nullptr), SUCCESS);220 EXPECT_EQ(loaded_bridge.api->register_custom_ops(nullptr), SUCCESS);
219 EXPECT_EQ(g_mock_python_custom_op_bridge_load_state.register_count, 1U);221 EXPECT_EQ(g_mock_python_custom_op_bridge_load_state.register_count, 1U);
220}222}
223+ 
221} // namespace custom_op224} // namespace custom_op
222} // namespace ge225} // namespace ge
Atests/ge/ut/ge/runtime/custom_op/python_custom_op_proto_unittest.cc+480-0
@@ -0,0 +1,480 @@
1+/**
2+ * Copyright (c) 2026 Huawei Technologies Co., Ltd.
3+ * This program is free software, you can redistribute it and/or modify it under the terms and conditions of
4+ * CANN Open Software License Agreement Version 2.0 (the "License").
5+ * Please refer to the License for details. You may not use this file except in compliance with the License.
6+ * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR IMPLIED,
7+ * INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY, OR FITNESS FOR A PARTICULAR PURPOSE.
8+ * See LICENSE in the root of the software repository for the full text of the License.
9+ */
10+ 
11+#include <gtest/gtest.h>
12+ 
13+#include <cstring>
14+#include <limits>
15+#include <map>
16+#include <memory>
17+#include <string>
18+#include <vector>
19+ 
20+#include "graph/custom_op_factory.h"
21+#include "graph/operator_factory.h"
22+#include "graph/utils/op_desc_utils.h"
23+#include "runtime/custom_op/python_custom_op_proto.h"
24+ 
25+namespace ge {
26+namespace custom_op {
27+namespace {
28+class PythonProtoCustomOpCollision : public BaseCustomOp {};
29+ 
30+PythonCustomOpStringView StringView(const char *value) {
31+ return PythonCustomOpStringView{value, strlen(value)};
32+}
33+ 
34+PythonCustomOpProtoDescriptorView MakeProtoView(const char *descriptor_key, const char *op_type,
35+ const PythonCustomOpProtoInputView *inputs, const size_t input_count,
36+ const PythonCustomOpProtoAttrView *attrs, const size_t attr_count,
37+ const PythonCustomOpProtoOutputView *outputs,
38+ const size_t output_count) {
39+ return PythonCustomOpProtoDescriptorView{
40+ StringView(descriptor_key), StringView(op_type), inputs, input_count, attrs, attr_count, outputs, output_count,
41+ };
42+}
43+ 
44+PythonCustomOpProtoAttrView RequiredAttr(const char *name, const uint32_t kind) {
45+ PythonCustomOpAttrDefaultView default_value{};
46+ return PythonCustomOpProtoAttrView{StringView(name), kind, 1U, default_value};
47+}
48+ 
49+PythonCustomOpProtoAttrView OptionalAttr(const char *name, const uint32_t kind,
50+ const PythonCustomOpAttrDefaultView &default_value) {
51+ return PythonCustomOpProtoAttrView{StringView(name), kind, 0U, default_value};
52+}
53+ 
54+struct AttrViewStorage {
55+ std::vector<int64_t> list_int{1, 2};
56+ std::vector<double> list_float{1.5, 2.5};
57+ std::vector<uint8_t> list_bool{1U, 0U};
58+ std::vector<PythonCustomOpStringView> list_string{StringView("a"), StringView("b")};
59+ std::vector<int32_t> list_data_type{static_cast<int32_t>(DT_FLOAT), static_cast<int32_t>(DT_INT32)};
60+ std::vector<std::vector<int64_t>> list_list_int_storage{{1, 2}, {3}};
61+ std::vector<PythonCustomOpInt64ArrayView> list_list_int;
62+ std::vector<PythonCustomOpProtoAttrView> attrs;
63+ 
64+ AttrViewStorage() {
65+ for (const auto &row : list_list_int_storage) {
66+ list_list_int.emplace_back(PythonCustomOpInt64ArrayView{row.data(), row.size()});
67+ }
68+ AddScalarAttrs();
69+ AddListAttrs();
70+ }
71+ 
72+ void AddScalarAttrs() {
73+ attrs.emplace_back(RequiredAttr("tensor_attr", kPythonAttrTensor));
74+ PythonCustomOpAttrDefaultView value{};
75+ value.has_value = 1U;
76+ value.int_value = 9;
77+ attrs.emplace_back(OptionalAttr("int_attr", kPythonAttrInt, value));
78+ value = {};
79+ value.has_value = 1U;
80+ value.float_value = 1.25;
81+ attrs.emplace_back(OptionalAttr("float_attr", kPythonAttrFloat, value));
82+ value = {};
83+ value.has_value = 1U;
84+ value.bool_value = 1U;
85+ attrs.emplace_back(OptionalAttr("bool_attr", kPythonAttrBool, value));
86+ value = {};
87+ value.has_value = 1U;
88+ value.string_value = StringView("value");
89+ attrs.emplace_back(OptionalAttr("string_attr", kPythonAttrString, value));
90+ value = {};
91+ value.has_value = 1U;
92+ value.data_type_value = static_cast<int32_t>(DT_FLOAT16);
93+ attrs.emplace_back(OptionalAttr("data_type_attr", kPythonAttrDataType, value));
94+ }
95+ 
96+ void AddListAttrs() {
97+ AddListAttr("list_int_attr", kPythonAttrListInt, list_int.data());
98+ AddListAttr("list_float_attr", kPythonAttrListFloat, list_float.data());
99+ AddListAttr("list_bool_attr", kPythonAttrListBool, list_bool.data());
100+ AddListAttr("list_string_attr", kPythonAttrListString, list_string.data());
101+ AddListAttr("list_data_type_attr", kPythonAttrListDataType, list_data_type.data());
102+ AddListAttr("list_list_int_attr", kPythonAttrListListInt, list_list_int.data());
103+ }
104+ 
105+ template <typename T>
106+ void AddListAttr(const char *name, const uint32_t kind, const T *data) {
107+ PythonCustomOpAttrDefaultView value{};
108+ value.has_value = 1U;
109+ value.count = 2U;
110+ SetListData(value, data);
111+ attrs.emplace_back(OptionalAttr(name, kind, value));
112+ }
113+ 
114+ static void SetListData(PythonCustomOpAttrDefaultView &value, const int64_t *data) {
115+ value.list_int_values = data;
116+ }
117+ static void SetListData(PythonCustomOpAttrDefaultView &value, const double *data) {
118+ value.list_float_values = data;
119+ }
120+ static void SetListData(PythonCustomOpAttrDefaultView &value, const uint8_t *data) {
121+ value.list_bool_values = data;
122+ }
123+ static void SetListData(PythonCustomOpAttrDefaultView &value, const PythonCustomOpStringView *data) {
124+ value.list_string_values = data;
125+ }
126+ static void SetListData(PythonCustomOpAttrDefaultView &value, const int32_t *data) {
127+ value.list_data_type_values = data;
128+ }
129+ static void SetListData(PythonCustomOpAttrDefaultView &value, const PythonCustomOpInt64ArrayView *data) {
130+ value.list_list_int_values = data;
131+ }
132+ 
133+ void MutateSource() {
134+ attrs[1].default_value.int_value = 99;
135+ attrs[2].default_value.float_value = 9.0;
136+ attrs[3].default_value.bool_value = 0U;
137+ attrs[4].default_value.string_value = StringView("changed");
138+ attrs[5].default_value.data_type_value = static_cast<int32_t>(DT_INT64);
139+ list_int[0] = 99;
140+ list_float[0] = 9.0;
141+ list_bool[0] = 0U;
142+ list_string[0] = StringView("changed");
143+ list_data_type[0] = static_cast<int32_t>(DT_INT64);
144+ list_list_int_storage[0][0] = 99;
145+ }
146+};
147+ 
148+void ExpectAllIrAttrTypes(const Operator &op) {
149+ std::map<AscendString, AscendString> ir_attr_types;
150+ ASSERT_EQ(op.GetAllIrAttrNamesAndTypes(ir_attr_types), GRAPH_SUCCESS);
151+ std::map<std::string, std::string> actual_ir_attr_types;
152+ for (const auto &item : ir_attr_types) {
153+ ASSERT_NE(item.first.GetString(), nullptr);
154+ ASSERT_NE(item.second.GetString(), nullptr);
155+ actual_ir_attr_types.emplace(item.first.GetString(), item.second.GetString());
156+ }
157+ EXPECT_EQ(actual_ir_attr_types, (std::map<std::string, std::string>{{"tensor_attr", "VT_TENSOR"},
158+ {"int_attr", "VT_INT"},
159+ {"float_attr", "VT_FLOAT"},
160+ {"bool_attr", "VT_BOOL"},
161+ {"string_attr", "VT_STRING"},
162+ {"data_type_attr", "VT_DATA_TYPE"},
163+ {"list_int_attr", "VT_LIST_INT"},
164+ {"list_float_attr", "VT_LIST_FLOAT"},
165+ {"list_bool_attr", "VT_LIST_BOOL"},
166+ {"list_string_attr", "VT_LIST_STRING"},
167+ {"list_data_type_attr", "VT_LIST_DATA_TYPE"},
168+ {"list_list_int_attr", "VT_LIST_LIST_INT"}}));
169+}
170+ 
171+void ExpectScalarDefaultAttrs(const Operator &op) {
172+ AttrValue tensor_default;
173+ EXPECT_NE(op.GetAttr("tensor_attr", tensor_default), GRAPH_SUCCESS);
174+ int64_t int_value = 0;
175+ EXPECT_EQ(op.GetAttr("int_attr", int_value), GRAPH_SUCCESS);
176+ EXPECT_EQ(int_value, 9);
177+ float32_t float_value = 0.0F;
178+ EXPECT_EQ(op.GetAttr("float_attr", float_value), GRAPH_SUCCESS);
179+ EXPECT_FLOAT_EQ(float_value, 1.25F);
180+ bool bool_value = false;
181+ EXPECT_EQ(op.GetAttr("bool_attr", bool_value), GRAPH_SUCCESS);
182+ EXPECT_TRUE(bool_value);
183+ std::string string_value;
184+ EXPECT_EQ(op.GetAttr("string_attr", string_value), GRAPH_SUCCESS);
185+ EXPECT_EQ(string_value, "value");
186+ DataType data_type_value = DT_UNDEFINED;
187+ EXPECT_EQ(op.GetAttr("data_type_attr", data_type_value), GRAPH_SUCCESS);
188+ EXPECT_EQ(data_type_value, DT_FLOAT16);
189+}
190+ 
191+void ExpectListDefaultAttrs(const Operator &op) {
192+ std::vector<int64_t> list_int_value;
193+ EXPECT_EQ(op.GetAttr("list_int_attr", list_int_value), GRAPH_SUCCESS);
194+ EXPECT_EQ(list_int_value, std::vector<int64_t>({1, 2}));
195+ std::vector<float32_t> list_float_value;
196+ EXPECT_EQ(op.GetAttr("list_float_attr", list_float_value), GRAPH_SUCCESS);
197+ EXPECT_EQ(list_float_value, std::vector<float32_t>({1.5F, 2.5F}));
198+ std::vector<bool> list_bool_value;
199+ EXPECT_EQ(op.GetAttr("list_bool_attr", list_bool_value), GRAPH_SUCCESS);
200+ EXPECT_EQ(list_bool_value, std::vector<bool>({true, false}));
201+ std::vector<std::string> list_string_value;
202+ EXPECT_EQ(op.GetAttr("list_string_attr", list_string_value), GRAPH_SUCCESS);
203+ EXPECT_EQ(list_string_value, std::vector<std::string>({"a", "b"}));
204+ std::vector<DataType> list_data_type_value;
205+ EXPECT_EQ(op.GetAttr("list_data_type_attr", list_data_type_value), GRAPH_SUCCESS);
206+ EXPECT_EQ(list_data_type_value, std::vector<DataType>({DT_FLOAT, DT_INT32}));
207+ std::vector<std::vector<int64_t>> list_list_int_value;
208+ EXPECT_EQ(op.GetAttr("list_list_int_attr", list_list_int_value), GRAPH_SUCCESS);
209+ EXPECT_EQ(list_list_int_value, std::vector<std::vector<int64_t>>({{1, 2}, {3}}));
210+}
211+} // namespace
212+ 
213+TEST(PythonCustomOpProto, registers_creator_with_owned_definition) {
214+ std::string required_name = "required_input";
215+ const PythonCustomOpProtoInputView inputs[] = {
216+ {PythonCustomOpStringView{required_name.data(), required_name.size()}, kPythonInputRequired},
217+ {StringView("optional_input"), kPythonInputOptional},
218+ {StringView("dynamic_input"), kPythonInputDynamic},
219+ };
220+ PythonCustomOpAttrDefaultView int_default{};
221+ int_default.has_value = 1U;
222+ int_default.int_value = 7;
223+ const PythonCustomOpProtoAttrView attrs[] = {
224+ OptionalAttr("axis", kPythonAttrInt, int_default),
225+ RequiredAttr("scale", kPythonAttrFloat),
226+ };
227+ const PythonCustomOpProtoOutputView outputs[] = {
228+ {StringView("required_input"), kPythonOutputRequired},
229+ {StringView("dynamic_output"), kPythonOutputDynamic},
230+ };
231+ const auto view = MakeProtoView("test_module:infer_meta:PythonProtoRegisterUt", "PythonProtoRegisterUt", inputs, 3U,
232+ attrs, 2U, outputs, 2U);
233+ 
234+ PythonCustomOpProto proto;
235+ ASSERT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_SUCCESS);
236+ required_name.assign("mutated_source");
237+ ASSERT_EQ(proto.inputs[0].name, "required_input");
238+ ASSERT_EQ(RegisterPythonCustomOpProto(proto), GRAPH_SUCCESS);
239+ ASSERT_TRUE(OperatorFactory::IsExistOp("PythonProtoRegisterUt"));
240+ proto.inputs[0].name = "mutated_proto";
241+ proto.attrs[0].default_definition.int_value = 99;
242+ 
243+ const auto op = OperatorFactory::CreateOperator("instance", "PythonProtoRegisterUt");
244+ const auto op_desc = OpDescUtils::GetOpDescFromOperator(op);
245+ ASSERT_NE(op_desc, nullptr);
246+ const auto &ir_inputs = op_desc->GetIrInputs();
247+ ASSERT_EQ(ir_inputs.size(), 3U);
248+ EXPECT_EQ(ir_inputs[0], std::make_pair(std::string("required_input"), kIrInputRequired));
249+ EXPECT_EQ(ir_inputs[1], std::make_pair(std::string("optional_input"), kIrInputOptional));
250+ EXPECT_EQ(ir_inputs[2], std::make_pair(std::string("dynamic_input"), kIrInputDynamic));
251+ const auto &ir_outputs = op_desc->GetIrOutputs();
252+ ASSERT_EQ(ir_outputs.size(), 2U);
253+ EXPECT_EQ(ir_outputs[0], std::make_pair(std::string("required_input"), kIrOutputRequired));
254+ EXPECT_EQ(ir_outputs[1], std::make_pair(std::string("dynamic_output"), kIrOutputDynamic));
255+ EXPECT_EQ(op_desc->GetIrAttrNames(), std::vector<std::string>({"axis", "scale"}));
256+ int64_t axis = 0;
257+ EXPECT_EQ(op.GetAttr("axis", axis), GRAPH_SUCCESS);
258+ EXPECT_EQ(axis, 7);
259+ UnregisterPythonCustomOpProtos({"PythonProtoRegisterUt"});
260+ EXPECT_FALSE(OperatorFactory::IsExistOp("PythonProtoRegisterUt"));
261+}
262+ 
263+TEST(PythonCustomOpProto, materializes_all_supported_attr_kinds_and_defaults) {
264+ AttrViewStorage storage;
265+ const auto view = MakeProtoView("test_module:infer_meta:PythonProtoAttrsUt", "PythonProtoAttrsUt", nullptr, 0U,
266+ storage.attrs.data(), storage.attrs.size(), nullptr, 0U);
267+ PythonCustomOpProto proto;
268+ ASSERT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_SUCCESS);
269+ ASSERT_EQ(proto.attrs.size(), 12U);
270+ EXPECT_TRUE(proto.attrs[0].is_required);
271+ EXPECT_EQ(proto.attrs[0].kind, kPythonAttrTensor);
272+ EXPECT_EQ(proto.attrs[1].default_definition.int_value, 9);
273+ EXPECT_EQ(proto.attrs[4].default_definition.string_value, "value");
274+ EXPECT_EQ(proto.attrs[6].default_definition.list_int_values, std::vector<int64_t>({1, 2}));
275+ EXPECT_EQ(proto.attrs[9].default_definition.list_string_values, std::vector<std::string>({"a", "b"}));
276+ EXPECT_EQ(proto.attrs[11].default_definition.list_list_int_values, std::vector<std::vector<int64_t>>({{1, 2}, {3}}));
277+ storage.MutateSource();
278+ ASSERT_EQ(RegisterPythonCustomOpProto(proto), GRAPH_SUCCESS);
279+ const auto op = OperatorFactory::CreateOperator("instance", "PythonProtoAttrsUt");
280+ const auto op_desc = OpDescUtils::GetOpDescFromOperator(op);
281+ ASSERT_NE(op_desc, nullptr);
282+ EXPECT_EQ(op_desc->GetIrAttrNames(),
283+ std::vector<std::string>({"tensor_attr", "int_attr", "float_attr", "bool_attr", "string_attr",
284+ "data_type_attr", "list_int_attr", "list_float_attr", "list_bool_attr",
285+ "list_string_attr", "list_data_type_attr", "list_list_int_attr"}));
286+ ExpectAllIrAttrTypes(op);
287+ ExpectScalarDefaultAttrs(op);
288+ ExpectListDefaultAttrs(op);
289+ UnregisterPythonCustomOpProtos({"PythonProtoAttrsUt"});
290+ EXPECT_FALSE(OperatorFactory::IsExistOp("PythonProtoAttrsUt"));
291+}
292+ 
293+TEST(PythonCustomOpProto, rejects_optional_attr_without_default) {
294+ PythonCustomOpAttrDefaultView default_value{};
295+ const auto attr = OptionalAttr("axis", kPythonAttrInt, default_value);
296+ const auto view = MakeProtoView("test_module:infer_meta:PythonProtoInvalidUt", "PythonProtoInvalidUt", nullptr, 0U,
297+ &attr, 1U, nullptr, 0U);
298+ PythonCustomOpProto proto;
299+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
300+}
301+ 
302+TEST(PythonCustomOpProto, rejects_invalid_descriptor_and_array_views) {
303+ PythonCustomOpProto proto;
304+ auto view = MakeProtoView("descriptor", "PythonProtoInvalidPodUt", nullptr, 0U, nullptr, 0U, nullptr, 0U);
305+ view.descriptor_key = StringView("");
306+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
307+ 
308+ view = MakeProtoView("descriptor", "PythonProtoInvalidPodUt", nullptr, 0U, nullptr, 0U, nullptr, 0U);
309+ view.op_type = PythonCustomOpStringView{nullptr, 1U};
310+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
311+ 
312+ const char embedded_null[] = {'b', 'a', 'd', '\0', 't', 'y', 'p', 'e'};
313+ view = MakeProtoView("descriptor", "PythonProtoInvalidPodUt", nullptr, 0U, nullptr, 0U, nullptr, 0U);
314+ view.op_type = PythonCustomOpStringView{embedded_null, sizeof(embedded_null)};
315+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
316+ 
317+ view = MakeProtoView("descriptor", "PythonProtoInvalidPodUt", nullptr, 1U, nullptr, 0U, nullptr, 0U);
318+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
319+ view = MakeProtoView("descriptor", "PythonProtoInvalidPodUt", nullptr, 0U, nullptr, 1U, nullptr, 0U);
320+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
321+ view = MakeProtoView("descriptor", "PythonProtoInvalidPodUt", nullptr, 0U, nullptr, 0U, nullptr, 1U);
322+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
323+ 
324+ const PythonCustomOpProtoInputView invalid_input_name = {PythonCustomOpStringView{nullptr, 1U}, kPythonInputRequired};
325+ view = MakeProtoView("descriptor", "PythonProtoInvalidPodUt", &invalid_input_name, 1U, nullptr, 0U, nullptr, 0U);
326+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
327+ 
328+ PythonCustomOpAttrDefaultView invalid_list_default{};
329+ invalid_list_default.has_value = 1U;
330+ invalid_list_default.list_int_values = nullptr;
331+ invalid_list_default.count = 1U;
332+ const auto invalid_list_attr = OptionalAttr("axes", kPythonAttrListInt, invalid_list_default);
333+ view = MakeProtoView("descriptor", "PythonProtoInvalidPodUt", nullptr, 0U, &invalid_list_attr, 1U, nullptr, 0U);
334+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
335+ 
336+ const PythonCustomOpInt64ArrayView invalid_row[] = {{nullptr, 1U}};
337+ PythonCustomOpAttrDefaultView invalid_nested_list_default{};
338+ invalid_nested_list_default.has_value = 1U;
339+ invalid_nested_list_default.list_list_int_values = invalid_row;
340+ invalid_nested_list_default.count = 1U;
341+ const auto invalid_nested_list_attr = OptionalAttr("axes", kPythonAttrListListInt, invalid_nested_list_default);
342+ view =
343+ MakeProtoView("descriptor", "PythonProtoInvalidPodUt", nullptr, 0U, &invalid_nested_list_attr, 1U, nullptr, 0U);
344+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
345+}
346+ 
347+TEST(PythonCustomOpProto, rejects_invalid_kinds_and_attr_defaults) {
348+ PythonCustomOpProto proto;
349+ const PythonCustomOpProtoInputView invalid_input = {StringView("x"), kPythonInputDynamic + 1U};
350+ auto view = MakeProtoView("descriptor", "PythonProtoInvalidKindUt", &invalid_input, 1U, nullptr, 0U, nullptr, 0U);
351+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
352+ 
353+ const PythonCustomOpProtoOutputView invalid_output = {StringView("y"), kPythonOutputDynamic + 1U};
354+ view = MakeProtoView("descriptor", "PythonProtoInvalidKindUt", nullptr, 0U, nullptr, 0U, &invalid_output, 1U);
355+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
356+ 
357+ const auto invalid_attr_kind = RequiredAttr("axis", kPythonAttrListListInt + 1U);
358+ view = MakeProtoView("descriptor", "PythonProtoInvalidKindUt", nullptr, 0U, &invalid_attr_kind, 1U, nullptr, 0U);
359+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
360+ 
361+ PythonCustomOpAttrDefaultView default_value{};
362+ default_value.has_value = 1U;
363+ default_value.bool_value = 2U;
364+ auto invalid_attr = OptionalAttr("flag", kPythonAttrBool, default_value);
365+ view = MakeProtoView("descriptor", "PythonProtoInvalidKindUt", nullptr, 0U, &invalid_attr, 1U, nullptr, 0U);
366+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
367+ 
368+ const uint8_t invalid_bool_list[] = {0U, 2U};
369+ default_value = {};
370+ default_value.has_value = 1U;
371+ default_value.list_bool_values = invalid_bool_list;
372+ default_value.count = 2U;
373+ invalid_attr = OptionalAttr("flags", kPythonAttrListBool, default_value);
374+ view = MakeProtoView("descriptor", "PythonProtoInvalidKindUt", nullptr, 0U, &invalid_attr, 1U, nullptr, 0U);
375+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
376+ 
377+ default_value = {};
378+ default_value.has_value = 1U;
379+ default_value.data_type_value = static_cast<int32_t>(DT_MAX);
380+ invalid_attr = OptionalAttr("data_type", kPythonAttrDataType, default_value);
381+ view = MakeProtoView("descriptor", "PythonProtoInvalidKindUt", nullptr, 0U, &invalid_attr, 1U, nullptr, 0U);
382+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
383+ 
384+ const int32_t invalid_data_type_list[] = {static_cast<int32_t>(DT_FLOAT), -1};
385+ default_value = {};
386+ default_value.has_value = 1U;
387+ default_value.list_data_type_values = invalid_data_type_list;
388+ default_value.count = 2U;
389+ invalid_attr = OptionalAttr("data_types", kPythonAttrListDataType, default_value);
390+ view = MakeProtoView("descriptor", "PythonProtoInvalidKindUt", nullptr, 0U, &invalid_attr, 1U, nullptr, 0U);
391+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
392+ 
393+ default_value = {};
394+ default_value.has_value = 1U;
395+ invalid_attr = OptionalAttr("tensor", kPythonAttrTensor, default_value);
396+ view = MakeProtoView("descriptor", "PythonProtoInvalidKindUt", nullptr, 0U, &invalid_attr, 1U, nullptr, 0U);
397+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
398+}
399+ 
400+TEST(PythonCustomOpProto, rejects_duplicate_ir_names) {
401+ PythonCustomOpProto proto;
402+ const PythonCustomOpProtoInputView duplicate_inputs[] = {{StringView("x"), kPythonInputRequired},
403+ {StringView("x"), kPythonInputOptional}};
404+ auto view = MakeProtoView("descriptor", "PythonProtoDuplicateUt", duplicate_inputs, 2U, nullptr, 0U, nullptr, 0U);
405+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
406+ 
407+ const PythonCustomOpProtoAttrView duplicate_attrs[] = {RequiredAttr("axis", kPythonAttrInt),
408+ RequiredAttr("axis", kPythonAttrFloat)};
409+ view = MakeProtoView("descriptor", "PythonProtoDuplicateUt", nullptr, 0U, duplicate_attrs, 2U, nullptr, 0U);
410+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
411+ 
412+ const PythonCustomOpProtoOutputView duplicate_outputs[] = {{StringView("y"), kPythonOutputRequired},
413+ {StringView("y"), kPythonOutputDynamic}};
414+ view = MakeProtoView("descriptor", "PythonProtoDuplicateUt", nullptr, 0U, nullptr, 0U, duplicate_outputs, 2U);
415+ EXPECT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_PARAM_INVALID);
416+}
417+ 
418+TEST(PythonCustomOpProto, compares_definitions_idempotently_including_nan) {
419+ const PythonCustomOpProtoInputView inputs[] = {{StringView("x"), kPythonInputRequired}};
420+ const PythonCustomOpProtoOutputView outputs[] = {{StringView("y"), kPythonOutputRequired}};
421+ const double nan = std::numeric_limits<double>::quiet_NaN();
422+ const double list_float[] = {1.0, nan};
423+ PythonCustomOpAttrDefaultView scalar_default{};
424+ scalar_default.has_value = 1U;
425+ scalar_default.float_value = nan;
426+ PythonCustomOpAttrDefaultView list_default{};
427+ list_default.has_value = 1U;
428+ list_default.list_float_values = list_float;
429+ list_default.count = 2U;
430+ const PythonCustomOpProtoAttrView attrs[] = {
431+ OptionalAttr("scale", kPythonAttrFloat, scalar_default),
432+ OptionalAttr("scales", kPythonAttrListFloat, list_default),
433+ };
434+ const auto view = MakeProtoView("descriptor", "PythonProtoIdempotentUt", inputs, 1U, attrs, 2U, outputs, 1U);
435+ PythonCustomOpProto lhs;
436+ PythonCustomOpProto rhs;
437+ ASSERT_EQ(ParsePythonCustomOpProto(view, lhs), GRAPH_SUCCESS);
438+ ASSERT_EQ(ParsePythonCustomOpProto(view, rhs), GRAPH_SUCCESS);
439+ EXPECT_TRUE(IsSamePythonCustomOpProto(lhs, rhs));
440+ 
441+ auto changed = rhs;
442+ changed.descriptor_key = "other_descriptor";
443+ EXPECT_FALSE(IsSamePythonCustomOpProto(lhs, changed));
444+ changed = rhs;
445+ changed.op_type = "OtherOpType";
446+ EXPECT_FALSE(IsSamePythonCustomOpProto(lhs, changed));
447+ changed = rhs;
448+ changed.inputs[0].kind = kIrInputOptional;
449+ EXPECT_FALSE(IsSamePythonCustomOpProto(lhs, changed));
450+ changed = rhs;
451+ changed.attrs[0].default_definition.float_value = 1.0;
452+ EXPECT_FALSE(IsSamePythonCustomOpProto(lhs, changed));
453+ changed = rhs;
454+ changed.attrs[1].default_definition.list_float_values[1] = 2.0;
455+ EXPECT_FALSE(IsSamePythonCustomOpProto(lhs, changed));
456+ changed = rhs;
457+ changed.outputs[0].name = "other_output";
458+ EXPECT_FALSE(IsSamePythonCustomOpProto(lhs, changed));
459+}
460+ 
461+TEST(PythonCustomOpProto, rejects_existing_custom_op_creator) {
462+ constexpr const char *kOpType = "PythonProtoCustomOpCollisionUt";
463+ ASSERT_EQ(CustomOpFactory::RegisterCustomOpCreator(AscendString(kOpType),
464+ []() { return std::make_unique<PythonProtoCustomOpCollision>(); }),
465+ GRAPH_SUCCESS);
466+ const PythonCustomOpProtoInputView inputs[] = {{StringView("replacement"), kPythonInputRequired}};
467+ const PythonCustomOpProtoOutputView outputs[] = {{StringView("replacement_output"), kPythonOutputRequired}};
468+ const auto view = MakeProtoView("test_module:infer_meta:PythonProtoCustomOpCollisionUt", kOpType, inputs, 1U, nullptr,
469+ 0U, outputs, 1U);
470+ PythonCustomOpProto proto;
471+ ASSERT_EQ(ParsePythonCustomOpProto(view, proto), GRAPH_SUCCESS);
472+ 
473+ EXPECT_EQ(RegisterPythonCustomOpProto(proto), GRAPH_FAILED);
474+ EXPECT_FALSE(OperatorFactory::IsExistOp(kOpType));
475+ EXPECT_TRUE(CustomOpFactory::IsExistOp(AscendString(kOpType)));
476+ CustomOpFactory::RemoveCustomOps({AscendString(kOpType)});
477+}
478+ 
479+} // namespace custom_op
480+} // namespace ge