Merged
feat: 添加自定义算子IR元信息收集能力 #4172
feat: 添加自定义算子IR元信息收集能力 #4172
Merged
GengChaoAdd on Jul 28
Total: 12 files changed+493-12
@@ -15,7 +15,7 @@
15import sys15import sys
16import threading16import threading
17from dataclasses import dataclass17from dataclasses import dataclass
18-from typing import Dict18+from typing import Dict, Optional
19 19 
20from .base import BaseCustomOp, EagerExecuteOp, EagerOpExecutionContext20from .base import BaseCustomOp, EagerExecuteOp, EagerOpExecutionContext
21from .bootstrap import get_registered_op_impls, load_custom_op_plugins21from .bootstrap import get_registered_op_impls, load_custom_op_plugins
@@ -75,7 +75,12 @@ def destroy_op_impl_holder(instance_id: str) -> bool:
75 return _OP_IMPL_HOLDERS.pop(instance_id, None) is not None75 return _OP_IMPL_HOLDERS.pop(instance_id, None) is not None
76 76 
77 77 
78-def call_execute(instance_id: str, ctx: EagerOpExecutionContext) -> None:78+def call_execute(
79+ instance_id: str,
80+ ir_meta: Optional[dict],
81+ ctx: EagerOpExecutionContext,
82+) -> None:
83+ del ir_meta
79 try:84 try:
80 custom_op = _get_eager_execute_op(instance_id)85 custom_op = _get_eager_execute_op(instance_id)
81 custom_op.execute(ctx)86 custom_op.execute(ctx)
@@ -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_ir_meta.cc"
15)16)
16 17 
17target_compile_definitions(custom_op_runtime PRIVATE18target_compile_definitions(custom_op_runtime PRIVATE
@@ -15,12 +15,106 @@
15#include <new>15#include <new>
16#include <utility>16#include <utility>
17 17 
18+#include "common/checker.h"
18#include "framework/common/debug/ge_log.h"19#include "framework/common/debug/ge_log.h"
20+#include "graph/operator_factory.h"
19#include "graph_metadef/graph/debug/ge_util.h"21#include "graph_metadef/graph/debug/ge_util.h"
22+#include "runtime/custom_op/python_custom_op_ir_meta.h"
20 23 
21namespace ge {24namespace ge {
22namespace custom_op {25namespace custom_op {
26+class PythonCustomOpIrMetaViewStorage {
27+ public:
28+ graphStatus Initialize(const std::string &op_type) {
29+ GE_ASSERT_GRAPH_SUCCESS(CollectCustomOpIrMeta(op_type, ir_meta_));
30+ 
31+ input_views_.reserve(ir_meta_.inputs.size());
32+ for (const auto &input : ir_meta_.inputs) {
33+ uint32_t kind = 0U;
34+ GE_ASSERT_GRAPH_SUCCESS(ConvertInputKind(input.kind, kind));
35+ input_views_.emplace_back(PythonCustomOpIrInputView{input.name.c_str(), kind});
36+ }
37+ 
38+ attr_views_.reserve(ir_meta_.attrs.size());
39+ for (const auto &attr : ir_meta_.attrs) {
40+ attr_views_.emplace_back(PythonCustomOpIrAttrView{attr.name.c_str(), attr.type.c_str()});
41+ }
42+ 
43+ output_views_.reserve(ir_meta_.outputs.size());
44+ for (const auto &output : ir_meta_.outputs) {
45+ uint32_t kind = 0U;
46+ GE_ASSERT_GRAPH_SUCCESS(ConvertOutputKind(output.kind, kind));
47+ output_views_.emplace_back(PythonCustomOpIrOutputView{output.name.c_str(), kind});
48+ }
49+ 
50+ view_ = PythonCustomOpIrMetaView{
51+ ir_meta_.op_type.c_str(), input_views_.empty() ? nullptr : input_views_.data(),
52+ input_views_.size(), attr_views_.empty() ? nullptr : attr_views_.data(),
53+ attr_views_.size(), output_views_.empty() ? nullptr : output_views_.data(),
54+ output_views_.size(),
55+ };
56+ return GRAPH_SUCCESS;
57+ }
58+ 
59+ const PythonCustomOpIrMetaView &GetView() const {
60+ return view_;
61+ }
62+ 
63+ private:
64+ static graphStatus ConvertInputKind(const ge::IrInputType kind, uint32_t &value) {
65+ switch (kind) {
66+ case ge::kIrInputRequired:
67+ value = 0U;
68+ return GRAPH_SUCCESS;
69+ case ge::kIrInputOptional:
70+ value = 1U;
71+ return GRAPH_SUCCESS;
72+ case ge::kIrInputDynamic:
73+ value = 2U;
74+ return GRAPH_SUCCESS;
75+ default:
76+ GE_ASSERT_TRUE(false, "Unsupported custom op IR input kind[%u].", static_cast<uint32_t>(kind));
77+ }
78+ }
79+ 
80+ static graphStatus ConvertOutputKind(const ge::IrOutputType kind, uint32_t &value) {
81+ switch (kind) {
82+ case ge::kIrOutputRequired:
83+ value = 0U;
84+ return GRAPH_SUCCESS;
85+ case ge::kIrOutputDynamic:
86+ value = 1U;
87+ return GRAPH_SUCCESS;
88+ default:
89+ GE_ASSERT_TRUE(false, "Unsupported custom op IR output kind[%u].", static_cast<uint32_t>(kind));
90+ }
91+ }
92+ 
93+ CustomOpIrMeta ir_meta_;
94+ std::vector<PythonCustomOpIrInputView> input_views_;
95+ std::vector<PythonCustomOpIrAttrView> attr_views_;
96+ std::vector<PythonCustomOpIrOutputView> output_views_;
97+ PythonCustomOpIrMetaView view_{};
98+};
99+ 
23namespace {100namespace {
101+std::unique_ptr<PythonCustomOpIrMetaViewStorage> CreateIrMetaViewStorage(const std::string &op_type) {
102+ if (!OperatorFactory::IsExistOp(op_type.c_str())) {
103+ return nullptr;
104+ }
105+ auto storage = std::unique_ptr<PythonCustomOpIrMetaViewStorage>(new (std::nothrow) PythonCustomOpIrMetaViewStorage());
106+ if (storage == nullptr) {
107+ GELOGE(GRAPH_FAILED, "Create custom op IR meta view storage failed, op type[%s].", op_type.c_str());
108+ return nullptr;
109+ }
110+ const auto ret = storage->Initialize(op_type);
111+ if (ret != GRAPH_SUCCESS) {
112+ GELOGE(ret, "Initialize custom op IR meta view storage failed, op type[%s].", op_type.c_str());
113+ return nullptr;
114+ }
115+ return storage;
116+}
117+ 
24struct PythonCustomOpRuntimeEntry {118struct PythonCustomOpRuntimeEntry {
25 explicit PythonCustomOpRuntimeEntry(PythonCustomOpDescriptor d, PythonCustomOpCallbacks cb)119 explicit PythonCustomOpRuntimeEntry(PythonCustomOpDescriptor d, PythonCustomOpCallbacks cb)
26 : desc(std::move(d)), callbacks(cb) {}120 : desc(std::move(d)), callbacks(cb) {}
@@ -195,7 +289,9 @@ void PythonCustomOpRuntimeRegistry::Clear() {
195}289}
196 290 
197PythonCustomOpAdapter::PythonCustomOpAdapter(PythonCustomOpDescriptor desc)291PythonCustomOpAdapter::PythonCustomOpAdapter(PythonCustomOpDescriptor desc)
198- : desc_(std::move(desc)), holder_(new (std::nothrow) PythonCustomOpHolder(desc_)) {}292+ : desc_(std::move(desc)),
293+ ir_meta_(CreateIrMetaViewStorage(desc_.op_type)),
294+ holder_(new (std::nothrow) PythonCustomOpHolder(desc_)) {}
199 295 
200PythonCustomOpAdapter::~PythonCustomOpAdapter() = default;296PythonCustomOpAdapter::~PythonCustomOpAdapter() = default;
201 297 
@@ -217,7 +313,8 @@ graphStatus PythonCustomOpAdapter::Execute(gert::EagerOpExecutionContext *ctx) {
217 desc_.descriptor_key.c_str(), desc_.op_type.c_str());313 desc_.descriptor_key.c_str(), desc_.op_type.c_str());
218 return GRAPH_FAILED;314 return GRAPH_FAILED;
219 }315 }
220- return holder_->GetCallbacks().execute(holder_->GetHolder(), ctx);316+ const auto *ir_meta = (ir_meta_ == nullptr) ? nullptr : &ir_meta_->GetView();
317+ return holder_->GetCallbacks().execute(holder_->GetHolder(), ctx, ir_meta);
221}318}
222 319 
223graphStatus PythonCustomOpAdapter::Compile(gert::OpCompileContext *ctx) {320graphStatus PythonCustomOpAdapter::Compile(gert::OpCompileContext *ctx) {
@@ -20,6 +20,8 @@
20 20 
21namespace ge {21namespace ge {
22namespace custom_op {22namespace custom_op {
23+class PythonCustomOpIrMetaViewStorage;
24+ 
23class PythonCustomOpRuntimeRegistry {25class PythonCustomOpRuntimeRegistry {
24 public:26 public:
25 static PythonCustomOpRuntimeRegistry &GetInstance();27 static PythonCustomOpRuntimeRegistry &GetInstance();
@@ -80,6 +82,7 @@ class PythonCustomOpAdapter final : public EagerExecuteOp,
80 graphStatus ReportUnsupported(CustomOpCapability capability, const char *method_name) const;82 graphStatus ReportUnsupported(CustomOpCapability capability, const char *method_name) const;
81 83 
82 PythonCustomOpDescriptor desc_;84 PythonCustomOpDescriptor desc_;
85+ std::unique_ptr<PythonCustomOpIrMetaViewStorage> ir_meta_;
83 std::unique_ptr<PythonCustomOpHolder> holder_;86 std::unique_ptr<PythonCustomOpHolder> holder_;
84};87};
85 88 
@@ -11,6 +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>
15+#include <cstdint>
14#include <string>16#include <string>
15 17 
16#include "graph/custom_op/capability.h"18#include "graph/custom_op/capability.h"
@@ -28,9 +30,39 @@ struct PythonCustomOpDescriptor {
28 CustomOpCapabilityMask capabilities{0U};30 CustomOpCapabilityMask capabilities{0U};
29};31};
30 32 
33+// 该借用式 POD view 是临时的 bridge 边界。CollectCustomOpIrMeta 当前依赖 runtime 私有接口,
34+// Python 版本敏感的 bridge/codegen 只能依赖 run 包的公开头文件,因此由 custom_op_runtime 持有 IR 快照并
35+// 通过该 view 暴露。后续 collector 所需接口全部成为 run 包稳定公开 API 后,应将 collector 迁移到
36+// bridge/codegen 侧并删除 POD 投影,改为通过正式公共头文件和符号表达依赖,不再维护私有 callback/POD 协议。
37+struct PythonCustomOpIrInputView {
38+ const char *name;
39+ uint32_t kind;
40+};
41+ 
42+struct PythonCustomOpIrAttrView {
43+ const char *name;
44+ const char *type;
45+};
46+ 
47+struct PythonCustomOpIrOutputView {
48+ const char *name;
49+ uint32_t kind;
50+};
51+ 
52+struct PythonCustomOpIrMetaView {
53+ const char *op_type;
54+ const PythonCustomOpIrInputView *inputs;
55+ size_t input_count;
56+ const PythonCustomOpIrAttrView *attrs;
57+ size_t attr_count;
58+ const PythonCustomOpIrOutputView *outputs;
59+ size_t output_count;
60+};
61+ 
31using PythonCustomOpHolderCreateFn = void *(*)(const PythonCustomOpDescriptor *desc);62using PythonCustomOpHolderCreateFn = void *(*)(const PythonCustomOpDescriptor *desc);
32using PythonCustomOpHolderDestroyFn = void (*)(void *holder);63using PythonCustomOpHolderDestroyFn = void (*)(void *holder);
33-using PythonCustomOpExecuteFn = graphStatus (*)(const void *holder, gert::EagerOpExecutionContext *ctx);64+using PythonCustomOpExecuteFn = graphStatus (*)(const void *holder, gert::EagerOpExecutionContext *ctx,
65+ const PythonCustomOpIrMetaView *ir_meta);
34 66 
35struct PythonCustomOpCallbacks {67struct PythonCustomOpCallbacks {
36 PythonCustomOpHolderCreateFn create{nullptr};68 PythonCustomOpHolderCreateFn create{nullptr};
@@ -0,0 +1,64 @@
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_ir_meta.h"
12+ 
13+#include <map>
14+#include <utility>
15+ 
16+#include "common/checker.h"
17+#include "graph/ascend_string.h"
18+#include "graph/operator_factory.h"
19+#include "graph/utils/op_desc_utils.h"
20+ 
21+namespace ge {
22+namespace custom_op {
23+graphStatus CollectCustomOpIrMeta(const std::string &op_type, CustomOpIrMeta &ir_meta) {
24+ GE_ASSERT_TRUE(!op_type.empty(), "Collect custom op IR meta failed because op type is empty.");
25+ GE_ASSERT_TRUE(OperatorFactory::IsExistOp(op_type.c_str()),
26+ "Collect custom op IR meta failed because op type[%s] is not registered.", op_type.c_str());
27+ 
28+ const auto op = OperatorFactory::CreateOperator(op_type.c_str(), op_type.c_str());
29+ const auto op_desc = OpDescUtils::GetOpDescFromOperator(op);
30+ GE_ASSERT_NOTNULL(op_desc, "Collect custom op IR meta failed to get op desc, op type[%s].", op_type.c_str());
31+ 
32+ std::map<AscendString, AscendString> attr_types;
33+ GE_ASSERT_GRAPH_SUCCESS(op.GetAllIrAttrNamesAndTypes(attr_types),
34+ "Collect custom op IR meta failed to get attr types, op type[%s].", op_type.c_str());
35+ 
36+ CustomOpIrMeta collected;
37+ collected.op_type = op_type;
38+ const auto &ir_inputs = op_desc->GetIrInputs();
39+ collected.inputs.reserve(ir_inputs.size());
40+ for (const auto &input : ir_inputs) {
41+ collected.inputs.emplace_back(CustomOpIrInputMeta{input.first, input.second});
42+ }
43+ 
44+ const auto &ir_attr_names = op_desc->GetIrAttrNames();
45+ collected.attrs.reserve(ir_attr_names.size());
46+ for (const auto &attr_name : ir_attr_names) {
47+ const auto attr_iter = attr_types.find(AscendString(attr_name.c_str()));
48+ GE_ASSERT_TRUE((attr_iter != attr_types.cend()) && (attr_iter->second.GetString() != nullptr),
49+ "Collect custom op IR meta failed to get type of attr[%s], op type[%s].", attr_name.c_str(),
50+ op_type.c_str());
51+ collected.attrs.emplace_back(CustomOpIrAttrMeta{attr_name, attr_iter->second.GetString()});
52+ }
53+ 
54+ const auto &ir_outputs = op_desc->GetIrOutputs();
55+ collected.outputs.reserve(ir_outputs.size());
56+ for (const auto &output : ir_outputs) {
57+ collected.outputs.emplace_back(CustomOpIrOutputMeta{output.first, output.second});
58+ }
59+ 
60+ ir_meta = std::move(collected);
61+ return GRAPH_SUCCESS;
62+}
63+} // namespace custom_op
64+} // namespace ge
@@ -0,0 +1,48 @@
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_IR_META_H_
12+#define CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_IR_META_H_
13+ 
14+#include <string>
15+#include <vector>
16+ 
17+#include "graph/error_codes.h"
18+#include "graph/op_desc.h"
19+ 
20+namespace ge {
21+namespace custom_op {
22+struct CustomOpIrInputMeta {
23+ std::string name;
24+ ge::IrInputType kind;
25+};
26+ 
27+struct CustomOpIrAttrMeta {
28+ std::string name;
29+ std::string type;
30+};
31+ 
32+struct CustomOpIrOutputMeta {
33+ std::string name;
34+ ge::IrOutputType kind;
35+};
36+ 
37+struct CustomOpIrMeta {
38+ std::string op_type;
39+ std::vector<CustomOpIrInputMeta> inputs;
40+ std::vector<CustomOpIrAttrMeta> attrs;
41+ std::vector<CustomOpIrOutputMeta> outputs;
42+};
43+ 
44+graphStatus CollectCustomOpIrMeta(const std::string &op_type, CustomOpIrMeta &ir_meta);
45+} // namespace custom_op
46+} // namespace ge
47+ 
48+#endif // CANN_GRAPH_ENGINE_RUNTIME_CUSTOM_OP_PYTHON_CUSTOM_OP_IR_META_H_
@@ -22,6 +22,7 @@
22#include <sstream>22#include <sstream>
23#include <stdexcept>23#include <stdexcept>
24#include <string>24#include <string>
25+#include <utility>
25 26 
26#include "common/ge_common/debug/ge_log.h"27#include "common/ge_common/debug/ge_log.h"
27#include "pybind11/embed.h"28#include "pybind11/embed.h"
@@ -180,7 +181,8 @@ class PythonCustomOpPybindBridge {
180 delete holder;181 delete holder;
181 }182 }
182 183 
183- graphStatus Execute(const PythonCustomOpBridgeHolder *holder, gert::EagerOpExecutionContext *ctx) {184+ graphStatus Execute(const PythonCustomOpBridgeHolder *holder, gert::EagerOpExecutionContext *ctx,
185+ const PythonCustomOpIrMetaView *ir_meta) {
184 if ((holder == nullptr) || (ctx == nullptr)) {186 if ((holder == nullptr) || (ctx == nullptr)) {
185 GELOGE(GRAPH_FAILED, "Python custom op bridge holder or context is null.");187 GELOGE(GRAPH_FAILED, "Python custom op bridge holder or context is null.");
186 return GRAPH_FAILED;188 return GRAPH_FAILED;
@@ -199,12 +201,17 @@ class PythonCustomOpPybindBridge {
199 holder->descriptor_key.c_str(), holder->instance_id.c_str());201 holder->descriptor_key.c_str(), holder->instance_id.c_str());
200 return GRAPH_FAILED;202 return GRAPH_FAILED;
201 }203 }
202- py::object result = bridge_module_.attr("call_execute")(holder->instance_id, BuildPythonContext(ctx));204+ py::object result =
205+ bridge_module_.attr("call_execute")(holder->instance_id, BuildPythonIrMeta(ir_meta), BuildPythonContext(ctx));
203 return TranslateStatusLike(result);206 return TranslateStatusLike(result);
204 } catch (const py::error_already_set &err) {207 } catch (const py::error_already_set &err) {
205 GELOGE(GRAPH_FAILED, "Execute python custom op failed, descriptor key[%s], instance id[%s]: %s",208 GELOGE(GRAPH_FAILED, "Execute python custom op failed, descriptor key[%s], instance id[%s]: %s",
206 holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());209 holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());
207 return GRAPH_FAILED;210 return GRAPH_FAILED;
211+ } catch (const std::exception &err) {
212+ GELOGE(GRAPH_FAILED, "Execute python custom op failed, descriptor key[%s], instance id[%s]: %s",
213+ holder->descriptor_key.c_str(), holder->instance_id.c_str(), err.what());
214+ return GRAPH_FAILED;
208 }215 }
209 }216 }
210 217 
@@ -335,6 +342,60 @@ class PythonCustomOpPybindBridge {
335 return (!desc.descriptor_key.empty() && !desc.op_type.empty()) ? SUCCESS : FAILED;342 return (!desc.descriptor_key.empty() && !desc.op_type.empty()) ? SUCCESS : FAILED;
336 }343 }
337 344 
345+ static py::object BuildPythonIrMeta(const PythonCustomOpIrMetaView *ir_meta) {
346+ if (ir_meta == nullptr) {
347+ return py::none();
348+ }
349+ if ((ir_meta->op_type == nullptr) || ((ir_meta->input_count != 0U) && (ir_meta->inputs == nullptr)) ||
350+ ((ir_meta->attr_count != 0U) && (ir_meta->attrs == nullptr)) ||
351+ ((ir_meta->output_count != 0U) && (ir_meta->outputs == nullptr))) {
352+ throw std::runtime_error("invalid custom op IR meta view");
353+ }
354+ 
355+ py::list inputs;
356+ for (size_t i = 0U; i < ir_meta->input_count; ++i) {
357+ const auto &input = ir_meta->inputs[i];
358+ if (input.name == nullptr) {
359+ throw std::runtime_error("invalid custom op IR input view");
360+ }
361+ py::dict item;
362+ item["name"] = py::str(input.name);
363+ item["kind"] = py::int_(input.kind);
364+ inputs.append(std::move(item));
365+ }
366+ 
367+ py::list attrs;
368+ for (size_t i = 0U; i < ir_meta->attr_count; ++i) {
369+ const auto &attr = ir_meta->attrs[i];
370+ if ((attr.name == nullptr) || (attr.type == nullptr)) {
371+ throw std::runtime_error("invalid custom op IR attr view");
372+ }
373+ py::dict item;
374+ item["name"] = py::str(attr.name);
375+ item["type"] = py::str(attr.type);
376+ attrs.append(std::move(item));
377+ }
378+ 
379+ py::list outputs;
380+ for (size_t i = 0U; i < ir_meta->output_count; ++i) {
381+ const auto &output = ir_meta->outputs[i];
382+ if (output.name == nullptr) {
383+ throw std::runtime_error("invalid custom op IR output view");
384+ }
385+ py::dict item;
386+ item["name"] = py::str(output.name);
387+ item["kind"] = py::int_(output.kind);
388+ outputs.append(std::move(item));
389+ }
390+ 
391+ py::dict result;
392+ result["op_type"] = py::str(ir_meta->op_type);
393+ result["inputs"] = std::move(inputs);
394+ result["attrs"] = std::move(attrs);
395+ result["outputs"] = std::move(outputs);
396+ return result;
397+ }
398+ 
338 static py::object BuildPythonContext(gert::EagerOpExecutionContext *ctx) {399 static py::object BuildPythonContext(gert::EagerOpExecutionContext *ctx) {
339 py::module_ native_module = py::module_::import(kCustomOpNativeModuleName);400 py::module_ native_module = py::module_::import(kCustomOpNativeModuleName);
340 return native_module.attr("_borrow_eager_op_execution_context")(py::int_(reinterpret_cast<uintptr_t>(ctx)));401 return native_module.attr("_borrow_eager_op_execution_context")(py::int_(reinterpret_cast<uintptr_t>(ctx)));
@@ -366,9 +427,10 @@ class PythonCustomOpPybindBridge {
366 callbacks.destroy = [](void *holder) {427 callbacks.destroy = [](void *holder) {
367 PythonCustomOpPybindBridge::GetInstance().DestroyHolder(static_cast<PythonCustomOpBridgeHolder *>(holder));428 PythonCustomOpPybindBridge::GetInstance().DestroyHolder(static_cast<PythonCustomOpBridgeHolder *>(holder));
368 };429 };
369- callbacks.execute = [](const void *holder, gert::EagerOpExecutionContext *ctx) -> graphStatus {430+ callbacks.execute = [](const void *holder, gert::EagerOpExecutionContext *ctx,
431+ const PythonCustomOpIrMetaView *ir_meta) -> graphStatus {
370 return PythonCustomOpPybindBridge::GetInstance().Execute(static_cast<const PythonCustomOpBridgeHolder *>(holder),432 return PythonCustomOpPybindBridge::GetInstance().Execute(static_cast<const PythonCustomOpBridgeHolder *>(holder),
371- ctx);433+ ctx, ir_meta);
372 };434 };
373 return callbacks;435 return callbacks;
374 }436 }
@@ -468,6 +468,7 @@ set(MULTI_PARTS_TEST_FILES
468 "common/dnnengines_unittest.cc"468 "common/dnnengines_unittest.cc"
469 "common/engine_manage_unittest.cc"469 "common/engine_manage_unittest.cc"
470 "common/helper/custom_op_registry_builder_unittest.cc"470 "common/helper/custom_op_registry_builder_unittest.cc"
471+ "runtime/custom_op/python_custom_op_ir_meta_unittest.cc"
471 "graph_ir/ge_custom_op_factory_unittest.cc"472 "graph_ir/ge_custom_op_factory_unittest.cc"
472 "graph_ir/ge_custom_op_pull_registry_unittest.cc"473 "graph_ir/ge_custom_op_pull_registry_unittest.cc"
473 "graph_ir/ge_operator_factory_unittest.cc"474 "graph_ir/ge_operator_factory_unittest.cc"
@@ -237,7 +237,13 @@ def test_bridge_custom_op_holder_and_execute():
237 descriptor_key = descriptors[0]["descriptor_key"]237 descriptor_key = descriptors[0]["descriptor_key"]
238 ctx = _FakeEagerContext()238 ctx = _FakeEagerContext()
239 assert bridge.create_op_impl_holder(instance_id, descriptor_key) is True239 assert bridge.create_op_impl_holder(instance_id, descriptor_key) is True
240- assert bridge.call_execute(instance_id, ctx) is None240+ ir_meta = {
241+ "op_type": "AddCustom",
242+ "inputs": [{"name": "x", "kind": 0}],
243+ "attrs": [{"name": "alpha", "type": "VT_FLOAT"}],
244+ "outputs": [{"name": "y", "kind": 0}],
245+ }
246+ assert bridge.call_execute(instance_id, ir_meta, ctx) is None
241 assert ctx.invalidated is True247 assert ctx.invalidated is True
242 assert bridge.destroy_op_impl_holder(instance_id) is True248 assert bridge.destroy_op_impl_holder(instance_id) is True
243 249 
@@ -252,7 +258,7 @@ def test_bridge_call_execute_ignores_return_value():
252 instance_id = "ReturnCustom#1"258 instance_id = "ReturnCustom#1"
253 ctx = _FakeEagerContext()259 ctx = _FakeEagerContext()
254 assert bridge.create_op_impl_holder(instance_id, descriptor_key) is True260 assert bridge.create_op_impl_holder(instance_id, descriptor_key) is True
255- assert bridge.call_execute(instance_id, ctx) is None261+ assert bridge.call_execute(instance_id, None, ctx) is None
256 assert ctx.invalidated is True262 assert ctx.invalidated is True
257 assert bridge.destroy_op_impl_holder(instance_id) is True263 assert bridge.destroy_op_impl_holder(instance_id) is True
258 264 
@@ -96,8 +96,10 @@ void DestroyMockPythonCustomOpHolder(void *holder) {
96 delete static_cast<MockPythonCustomOpHolder *>(holder);96 delete static_cast<MockPythonCustomOpHolder *>(holder);
97}97}
98 98 
99-graphStatus ExecuteMockPythonCustomOp(const void *holder, gert::EagerOpExecutionContext *ctx) {99+graphStatus ExecuteMockPythonCustomOp(const void *holder, gert::EagerOpExecutionContext *ctx,
100+ const PythonCustomOpIrMetaView *ir_meta) {
100 (void)ctx;101 (void)ctx;
102+ (void)ir_meta;
101 auto *mock_holder = const_cast<MockPythonCustomOpHolder *>(static_cast<const MockPythonCustomOpHolder *>(holder));103 auto *mock_holder = const_cast<MockPythonCustomOpHolder *>(static_cast<const MockPythonCustomOpHolder *>(holder));
102 if (mock_holder == nullptr) {104 if (mock_holder == nullptr) {
103 return GRAPH_FAILED;105 return GRAPH_FAILED;
@@ -0,0 +1,160 @@
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 <new>
14+ 
15+#include "graph/operator_reg.h"
16+#include "runtime/custom_op/python_custom_op_adapter.h"
17+#include "runtime/custom_op/python_custom_op_ir_meta.h"
18+ 
19+namespace ge {
20+REG_OP(PythonCustomOpIrMetaUt)
21+ .INPUT(required_input, TensorType::ALL())
22+ .OPTIONAL_INPUT(optional_input, TensorType::ALL())
23+ .DYNAMIC_INPUT(dynamic_input, TensorType::ALL())
24+ .OUTPUT(required_output, TensorType::ALL())
25+ .DYNAMIC_OUTPUT(dynamic_output, TensorType::ALL())
26+ .ATTR(z_attr, Int, 0)
27+ .REQUIRED_ATTR(a_attr, Float)
28+ .OP_END_FACTORY_REG(PythonCustomOpIrMetaUt);
29+} // namespace ge
30+ 
31+namespace ge {
32+namespace custom_op {
33+namespace {
34+struct MockPythonCustomOpHolder {};
35+ 
36+void *CreateMockPythonCustomOpHolder(const PythonCustomOpDescriptor *desc) {
37+ return (desc == nullptr) ? nullptr : new (std::nothrow) MockPythonCustomOpHolder();
38+}
39+ 
40+void DestroyMockPythonCustomOpHolder(void *holder) {
41+ delete static_cast<MockPythonCustomOpHolder *>(holder);
42+}
43+ 
44+graphStatus ExecuteMockPythonCustomOpWithIrMeta(const void *holder, gert::EagerOpExecutionContext *ctx,
45+ const PythonCustomOpIrMetaView *ir_meta) {
46+ (void)ctx;
47+ if ((holder == nullptr) || (ir_meta == nullptr)) {
48+ ADD_FAILURE() << "Python custom op holder or IR meta view is null";
49+ return GRAPH_FAILED;
50+ }
51+ 
52+ EXPECT_STREQ(ir_meta->op_type, "PythonCustomOpIrMetaUt");
53+ EXPECT_EQ(ir_meta->input_count, 3U);
54+ EXPECT_EQ(ir_meta->attr_count, 2U);
55+ EXPECT_EQ(ir_meta->output_count, 2U);
56+ if ((ir_meta->input_count != 3U) || (ir_meta->attr_count != 2U) || (ir_meta->output_count != 2U)) {
57+ return GRAPH_FAILED;
58+ }
59+ 
60+ EXPECT_STREQ(ir_meta->inputs[0].name, "required_input");
61+ EXPECT_EQ(ir_meta->inputs[0].kind, 0U);
62+ EXPECT_STREQ(ir_meta->inputs[1].name, "optional_input");
63+ EXPECT_EQ(ir_meta->inputs[1].kind, 1U);
64+ EXPECT_STREQ(ir_meta->inputs[2].name, "dynamic_input");
65+ EXPECT_EQ(ir_meta->inputs[2].kind, 2U);
66+ EXPECT_STREQ(ir_meta->attrs[0].name, "z_attr");
67+ EXPECT_STREQ(ir_meta->attrs[0].type, "VT_INT");
68+ EXPECT_STREQ(ir_meta->attrs[1].name, "a_attr");
69+ EXPECT_STREQ(ir_meta->attrs[1].type, "VT_FLOAT");
70+ EXPECT_STREQ(ir_meta->outputs[0].name, "required_output");
71+ EXPECT_EQ(ir_meta->outputs[0].kind, 0U);
72+ EXPECT_STREQ(ir_meta->outputs[1].name, "dynamic_output");
73+ EXPECT_EQ(ir_meta->outputs[1].kind, 1U);
74+ return GRAPH_SUCCESS;
75+}
76+ 
77+graphStatus ExecuteMockLegacyPythonCustomOp(const void *holder, gert::EagerOpExecutionContext *ctx,
78+ const PythonCustomOpIrMetaView *ir_meta) {
79+ (void)ctx;
80+ EXPECT_NE(holder, nullptr);
81+ EXPECT_EQ(ir_meta, nullptr);
82+ return ((holder != nullptr) && (ir_meta == nullptr)) ? GRAPH_SUCCESS : GRAPH_FAILED;
83+}
84+} // namespace
85+ 
86+TEST(PythonCustomOpIrMeta, collects_ir_in_definition_order) {
87+ CustomOpIrMeta ir_meta;
88+ ASSERT_EQ(CollectCustomOpIrMeta("PythonCustomOpIrMetaUt", ir_meta), GRAPH_SUCCESS);
89+ 
90+ EXPECT_EQ(ir_meta.op_type, "PythonCustomOpIrMetaUt");
91+ ASSERT_EQ(ir_meta.inputs.size(), 3U);
92+ EXPECT_EQ(ir_meta.inputs[0].name, "required_input");
93+ EXPECT_EQ(ir_meta.inputs[0].kind, kIrInputRequired);
94+ EXPECT_EQ(ir_meta.inputs[1].name, "optional_input");
95+ EXPECT_EQ(ir_meta.inputs[1].kind, kIrInputOptional);
96+ EXPECT_EQ(ir_meta.inputs[2].name, "dynamic_input");
97+ EXPECT_EQ(ir_meta.inputs[2].kind, kIrInputDynamic);
98+ 
99+ ASSERT_EQ(ir_meta.attrs.size(), 2U);
100+ EXPECT_EQ(ir_meta.attrs[0].name, "z_attr");
101+ EXPECT_EQ(ir_meta.attrs[0].type, "VT_INT");
102+ EXPECT_EQ(ir_meta.attrs[1].name, "a_attr");
103+ EXPECT_EQ(ir_meta.attrs[1].type, "VT_FLOAT");
104+ 
105+ ASSERT_EQ(ir_meta.outputs.size(), 2U);
106+ EXPECT_EQ(ir_meta.outputs[0].name, "required_output");
107+ EXPECT_EQ(ir_meta.outputs[0].kind, kIrOutputRequired);
108+ EXPECT_EQ(ir_meta.outputs[1].name, "dynamic_output");
109+ EXPECT_EQ(ir_meta.outputs[1].kind, kIrOutputDynamic);
110+}
111+ 
112+TEST(PythonCustomOpIrMeta, rejects_unregistered_op_without_overwriting_output) {
113+ CustomOpIrMeta ir_meta;
114+ ir_meta.op_type = "keep_me";
115+ 
116+ EXPECT_NE(CollectCustomOpIrMeta("PythonCustomOpIrMetaMissingUt", ir_meta), GRAPH_SUCCESS);
117+ EXPECT_EQ(ir_meta.op_type, "keep_me");
118+}
119+ 
120+TEST(PythonCustomOpIrMeta, forwards_pod_view_to_python_callback) {
121+ PythonCustomOpDescriptor desc;
122+ desc.descriptor_key = "python_adapter_ir_meta";
123+ desc.op_type = "PythonCustomOpIrMetaUt";
124+ AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);
125+ 
126+ PythonCustomOpCallbacks callbacks;
127+ callbacks.create = CreateMockPythonCustomOpHolder;
128+ callbacks.destroy = DestroyMockPythonCustomOpHolder;
129+ callbacks.execute = ExecuteMockPythonCustomOpWithIrMeta;
130+ 
131+ ASSERT_TRUE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));
132+ {
133+ PythonCustomOpAdapter adapter(desc);
134+ ASSERT_TRUE(adapter.IsValid());
135+ EXPECT_EQ(adapter.Execute(nullptr), GRAPH_SUCCESS);
136+ }
137+ EXPECT_TRUE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));
138+}
139+ 
140+TEST(PythonCustomOpIrMeta, keeps_legacy_execute_without_registered_ir) {
141+ PythonCustomOpDescriptor desc;
142+ desc.descriptor_key = "python_adapter_legacy_without_ir";
143+ desc.op_type = "PythonCustomOpLegacyWithoutIrUt";
144+ AddCustomOpCapability(desc.capabilities, CustomOpCapability::kEagerExecute);
145+ 
146+ PythonCustomOpCallbacks callbacks;
147+ callbacks.create = CreateMockPythonCustomOpHolder;
148+ callbacks.destroy = DestroyMockPythonCustomOpHolder;
149+ callbacks.execute = ExecuteMockLegacyPythonCustomOp;
150+ 
151+ ASSERT_TRUE(PythonCustomOpRuntimeRegistry::Register(desc, callbacks));
152+ {
153+ PythonCustomOpAdapter adapter(desc);
154+ ASSERT_TRUE(adapter.IsValid());
155+ EXPECT_EQ(adapter.Execute(nullptr), GRAPH_SUCCESS);
156+ }
157+ EXPECT_TRUE(PythonCustomOpRuntimeRegistry::Unregister(desc.descriptor_key));
158+}
159+} // namespace custom_op
160+} // namespace ge