已合并
feat: 支持schema-bound自定义算子execute接口 #4188
feat: 支持schema-bound自定义算子execute接口 #4188
已合并
GengChao创建于 7月30日
共 8 个文件变更+720-161
@@ -17,6 +17,7 @@ __all__ = [
17 "EagerExecuteOp",17 "EagerExecuteOp",
18 "EagerOpExecutionContext",18 "EagerOpExecutionContext",
19 "clear_registered_op_impls",19 "clear_registered_op_impls",
20+ "get_execute_ctx",
20 "get_registered_op_impl_by_descriptor_key",21 "get_registered_op_impl_by_descriptor_key",
21 "get_registered_op_impl_dicts",22 "get_registered_op_impl_dicts",
22 "get_registered_op_impls",23 "get_registered_op_impls",
@@ -28,6 +29,7 @@ _LAZY_EXPORTS = {
28 "EagerExecuteOp": ".base",29 "EagerExecuteOp": ".base",
29 "EagerOpExecutionContext": ".base",30 "EagerOpExecutionContext": ".base",
30 "clear_registered_op_impls": ".registry",31 "clear_registered_op_impls": ".registry",
32+ "get_execute_ctx": ".context",
31 "get_registered_op_impl_by_descriptor_key": ".registry",33 "get_registered_op_impl_by_descriptor_key": ".registry",
32 "get_registered_op_impl_dicts": ".registry",34 "get_registered_op_impl_dicts": ".registry",
33 "get_registered_op_impls": ".registry",35 "get_registered_op_impls": ".registry",
@@ -12,6 +12,7 @@
12 12 
13"""Bridge-facing Python runtime helpers for GE custom op implementations."""13"""Bridge-facing Python runtime helpers for GE custom op implementations."""
14 14 
15+import inspect
15import sys16import sys
16import threading17import threading
17from dataclasses import dataclass18from dataclasses import dataclass
@@ -19,6 +20,7 @@ from typing import Dict, Optional
19 20 
20from .base import BaseCustomOp, EagerExecuteOp, EagerOpExecutionContext21from .base import BaseCustomOp, EagerExecuteOp, EagerOpExecutionContext
21from .bootstrap import get_registered_op_impls, load_custom_op_plugins22from .bootstrap import get_registered_op_impls, load_custom_op_plugins
23+from .context import _execute_ctx_scope
22from .registry import get_registered_op_impl_by_descriptor_key24from .registry import get_registered_op_impl_by_descriptor_key
23 25 
24 26 
@@ -32,6 +34,25 @@ class _OpImplHolder:
32_HOLDER_LOCK = threading.RLock()34_HOLDER_LOCK = threading.RLock()
33_OP_IMPL_HOLDERS: Dict[str, _OpImplHolder] = {}35_OP_IMPL_HOLDERS: Dict[str, _OpImplHolder] = {}
34 36 
37+_IR_INPUT_REQUIRED = 0
38+_IR_INPUT_OPTIONAL = 1
39+_IR_INPUT_DYNAMIC = 2
40+ 
41+_RUNTIME_ATTR_GETTERS = {
42+ "VT_INT": "get_int",
43+ "VT_FLOAT": "get_float",
44+ "VT_BOOL": "get_bool",
45+ "VT_STRING": "get_str",
46+ "VT_DATA_TYPE": "get_data_type",
47+ "VT_TENSOR": "get_tensor",
48+ "VT_LIST_INT": "get_list_int",
49+ "VT_LIST_FLOAT": "get_list_float",
50+ "VT_LIST_BOOL": "get_list_bool",
51+ "VT_LIST_STRING": "get_list_str",
52+ "VT_LIST_DATA_TYPE": "get_list_data_type",
53+ "VT_LIST_LIST_INT": "get_list_list_int",
54+}
55+ 
35 56 
36def load_and_get_op_impl_descriptors() -> list:57def load_and_get_op_impl_descriptors() -> list:
37 load_custom_op_plugins()58 load_custom_op_plugins()
@@ -75,15 +96,81 @@ def destroy_op_impl_holder(instance_id: str) -> bool:
75 return _OP_IMPL_HOLDERS.pop(instance_id, None) is not None96 return _OP_IMPL_HOLDERS.pop(instance_id, None) is not None
76 97 
77 98 
99+def _is_legacy_execute(method) -> bool:
100+ params = list(inspect.signature(method).parameters.values())
101+ return (
102+ len(params) == 1
103+ and params[0].name == "ctx"
104+ and params[0].kind
105+ in (
106+ inspect.Parameter.POSITIONAL_ONLY,
107+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
108+ )
109+ )
110+ 
111+ 
112+def _build_execute_inputs(ctx: EagerOpExecutionContext, ir_inputs: list) -> list:
113+ args = []
114+ for ir_index, item in enumerate(ir_inputs):
115+ kind = item["kind"]
116+ if kind == _IR_INPUT_REQUIRED:
117+ args.append(ctx.get_required_input_tensor(ir_index))
118+ elif kind == _IR_INPUT_OPTIONAL:
119+ args.append(ctx.get_optional_input_tensor(ir_index))
120+ elif kind == _IR_INPUT_DYNAMIC:
121+ instance_num = ctx.get_dynamic_input_num(ir_index)
122+ args.append(
123+ [
124+ ctx.get_dynamic_input_tensor(ir_index, relative_index)
125+ for relative_index in range(instance_num)
126+ ]
127+ )
128+ else:
129+ raise ValueError(
130+ f"unsupported custom op IR input kind: {kind}, ir index: {ir_index}"
131+ )
132+ return args
133+ 
134+ 
135+def _read_runtime_attr(attrs, index: int, ir_type: str):
136+ getter_name = _RUNTIME_ATTR_GETTERS.get(ir_type)
137+ if getter_name is None:
138+ raise ValueError(
139+ f"unsupported custom op runtime attr type: {ir_type}, attr index: {index}"
140+ )
141+ return getattr(attrs, getter_name)(index)
142+ 
143+ 
144+def _build_execute_attrs(ctx: EagerOpExecutionContext, ir_attrs: list) -> dict:
145+ if not ir_attrs:
146+ return {}
147+ attrs = ctx.get_attrs()
148+ return {
149+ item["name"]: _read_runtime_attr(attrs, index, item["type"])
150+ for index, item in enumerate(ir_attrs)
151+ }
152+ 
153+ 
78def call_execute(154def call_execute(
79 instance_id: str,155 instance_id: str,
80 ir_meta: Optional[dict],156 ir_meta: Optional[dict],
81 ctx: EagerOpExecutionContext,157 ctx: EagerOpExecutionContext,
82) -> None:158) -> None:
83- del ir_meta
84 try:159 try:
85 custom_op = _get_eager_execute_op(instance_id)160 custom_op = _get_eager_execute_op(instance_id)
86- custom_op.execute(ctx)161+ method = custom_op.execute
162+ if _is_legacy_execute(method):
163+ method(ctx)
164+ return
165+ if ir_meta is None:
166+ descriptor = custom_op.__ge_op_impl_descriptor__
167+ raise RuntimeError(
168+ f"canonical IR not found for schema-bound execute: {descriptor.op_type}"
169+ )
atomgit-bot
atomgit-botatomgit-bot7月30日

🟡 Medium Priority

此 diff 删除了 base.py 中的 __init_subclass__ 钩子、_adapt_inputs_execute 装饰器以及全部相关辅助函数(_MISSING、_build_input_tensor_list、_inject_ctx_methods、_restore_ctx_methods、_check_execute_arg_count、_get_execute_arg_name 等),导致 execute(self, inputs) 模式不再被自动适配。

受影响路径:call_execute → _is_legacy_execute(method) → len(params)==1 且 params[0].name=="inputs"(非 "ctx"),返回 False → 走入 schema-bound 分支 → ir_meta is None 时抛出 RuntimeError("canonical IR not found for schema-bound execute: ...")。

该错误信息对旧 execute(self, inputs) 用户具有误导性——用户并未使用 schema-bound 模式,却收到"canonical IR not found for schema-bound execute"的错误。旧的 __init_subclass__ 在类定义时就会通过 _build_execute_signature_error 给出清晰的错误提示,现在变为运行时才暴露且错误信息不准确。

虽然 PR 描述声明"保留 legacy execute(ctx) 调用方式",但 execute(self, inputs) 模式的静默移除构成对外部 API 使用者的不兼容变更,缺少弃用警告或迁移指引。

建议:在 call_execute 的 _is_legacy_execute 返回 False 且 ir_meta is None 时,先检查方法签名是否恰为单参数且参数名非 "ctx"(如 "inputs"),给出明确的迁移提示:"execute(self, inputs) is no longer supported; use execute(self, ctx) for legacy or execute(self, *args, **kwargs) for schema-bound"。或者保留 __init_subclass__ 钩子对旧签名的检测并在类定义时发出 DeprecationWarning。

likedislike
不准确?
GengChao
GengChao
7月30日 评论:
170+ args = _build_execute_inputs(ctx, ir_meta["inputs"])
171+ kwargs = _build_execute_attrs(ctx, ir_meta["attrs"])
172+ with _execute_ctx_scope(ctx):
173+ method(*args, **kwargs)
87 finally:174 finally:
88 ctx._invalidate()175 ctx._invalidate()
89 176 
@@ -20,6 +20,36 @@ __all__: List[str] = [
20]20]
21 21 
22 22 
23+class RuntimeAttrs:
24+ """Borrowed view of runtime attributes for the current execute callback."""
25+ 
26+ def get_int(self, index: int) -> int: ...
27+ 
28+ def get_float(self, index: int) -> float: ...
29+ 
30+ def get_bool(self, index: int) -> bool: ...
31+ 
32+ def get_str(self, index: int) -> str: ...
33+ 
34+ def get_data_type(self, index: int) -> DataType: ...
35+ 
36+ def get_tensor(self, index: int) -> Tensor: ...
37+ 
38+ def get_list_int(self, index: int) -> List[int]: ...
39+ 
40+ def get_list_float(self, index: int) -> List[float]: ...
41+ 
42+ def get_list_bool(self, index: int) -> List[bool]: ...
43+ 
44+ def get_list_str(self, index: int) -> List[str]: ...
45+ 
46+ def get_list_data_type(self, index: int) -> List[DataType]: ...
47+ 
48+ def get_list_list_int(self, index: int) -> List[List[int]]: ...
49+ 
50+ def get_attr_num(self) -> int: ...
51+ 
52+ 
23class EagerOpExecutionContext:53class EagerOpExecutionContext:
24 """Eager op execution context (Python view of ``gert::EagerOpExecutionContext``).54 """Eager op execution context (Python view of ``gert::EagerOpExecutionContext``).
25 55 
@@ -53,6 +83,14 @@ class EagerOpExecutionContext:
53 """Return the number of runtime input tensors for current compute node."""83 """Return the number of runtime input tensors for current compute node."""
54 ...84 ...
55 85 
86+ def get_dynamic_input_num(self, ir_index: int) -> int:
87+ """Return the number of instantiated tensors for a dynamic input IR slot."""
88+ ...
89+ 
90+ def get_attrs(self) -> RuntimeAttrs:
91+ """Return the borrowed runtime attributes for the current compute node."""
92+ ...
93+ 
56 def get_required_input_tensor(self, ir_index: int) -> Tensor:94 def get_required_input_tensor(self, ir_index: int) -> Tensor:
57 """Return input tensor by ir index.95 """Return input tensor by ir index.
58 96 
@@ -13,96 +13,9 @@
13"""Base definitions for Python GE custom ops."""13"""Base definitions for Python GE custom ops."""
14 14 
15from __future__ import annotations15from __future__ import annotations
16-import inspect
17from abc import ABC, abstractmethod16from abc import ABC, abstractmethod
18-from functools import wraps
19-from typing import Callable, List
20 17 
21-from ._native import EagerOpExecutionContext18+from ._native import EagerOpExecutionContext as EagerOpExecutionContext
22- 
23- 
24-_MISSING = object()
25-_POSITIONAL_PARAMETER_KINDS = (
26- inspect.Parameter.POSITIONAL_ONLY,
27- inspect.Parameter.POSITIONAL_OR_KEYWORD,
28-)
29-_EXECUTE_CONTEXT_ARG_NAME = "ctx"
30-_EXECUTE_INPUTS_ARG_NAME = "inputs"
31-_SUPPORTED_EXECUTE_ARG_NAMES = (_EXECUTE_CONTEXT_ARG_NAME, _EXECUTE_INPUTS_ARG_NAME)
32- 
33- 
34-def _build_input_tensor_list(ctx: EagerOpExecutionContext) -> List[object]:
35- return [ctx.get_input_tensor(index) for index in range(ctx.get_input_num())]
36- 
37- 
38-def _inject_ctx_methods(instance, ctx: EagerOpExecutionContext) -> dict:
39- original_attrs = {}
40- for name in dir(ctx):
41- if name.startswith("_"):
42- continue
43- ctx_method = getattr(ctx, name)
44- if not callable(ctx_method):
45- continue
46- original_attrs[name] = instance.__dict__.get(name, _MISSING)
47- setattr(instance, name, ctx_method)
48- return original_attrs
49- 
50- 
51-def _restore_ctx_methods(instance, original_attrs: dict) -> None:
52- for name, value in original_attrs.items():
53- if value is _MISSING:
54- instance.__dict__.pop(name, None)
55- else:
56- setattr(instance, name, value)
57- 
58- 
59-def _adapt_inputs_execute(method: Callable) -> Callable:
60- @wraps(method)
61- def wrapper(self, ctx: EagerOpExecutionContext) -> None:
62- inputs = _build_input_tensor_list(ctx)
63- original_attrs = _inject_ctx_methods(self, ctx)
64- try:
65- method(self, inputs)
66- finally:
67- _restore_ctx_methods(self, original_attrs)
68- 
69- return wrapper
70- 
71- 
72-def _build_execute_signature_error(method: Callable) -> str:
73- return (
74- "EagerExecuteOp.execute only supports execute(self, ctx) or "
75- f"execute(self, inputs); got execute{inspect.signature(method)}. "
76- "Use 'ctx' for EagerOpExecutionContext or 'inputs' for the input tensor list."
77- )
78- 
79- 
80-def _positional_params(method: Callable) -> List[inspect.Parameter]:
81- return [
82- param
83- for param in inspect.signature(method).parameters.values()
84- if param.kind in _POSITIONAL_PARAMETER_KINDS
85- ]
86- 
87- 
88-def _has_non_positional_params(method: Callable) -> bool:
89- return any(
90- param.kind not in _POSITIONAL_PARAMETER_KINDS
91- for param in inspect.signature(method).parameters.values()
92- )
93- 
94- 
95-def _check_execute_arg_count(method: Callable) -> None:
96- if len(_positional_params(method)) != 2 or _has_non_positional_params(method):
97- raise TypeError(_build_execute_signature_error(method))
98- 
99- 
100-def _get_execute_arg_name(method: Callable) -> str:
101- _check_execute_arg_count(method)
102- arg_name = _positional_params(method)[1].name
103- if arg_name not in _SUPPORTED_EXECUTE_ARG_NAMES:
104- raise TypeError(_build_execute_signature_error(method))
105- return arg_name
106 19 
107 20 
108class BaseCustomOp(ABC):21class BaseCustomOp(ABC):
@@ -112,14 +25,7 @@ class BaseCustomOp(ABC):
112class EagerExecuteOp(BaseCustomOp):25class EagerExecuteOp(BaseCustomOp):
113 """Base class for Python eager execute custom ops."""26 """Base class for Python eager execute custom ops."""
114 27 
115- def __init_subclass__(cls, **kwargs):
116- super().__init_subclass__(**kwargs)
117- method = cls.__dict__.get("execute")
118- if callable(method):
119- arg_name = _get_execute_arg_name(method)
120- if arg_name == _EXECUTE_INPUTS_ARG_NAME:
121- cls.execute = _adapt_inputs_execute(method)
122- 
123 @abstractmethod28 @abstractmethod
124- def execute(self, ctx: EagerOpExecutionContext) -> None:29+ def execute(self, *args, **kwargs) -> None:
30+ """Execute with either the legacy context or schema-bound arguments."""
125 raise NotImplementedError31 raise NotImplementedError
@@ -0,0 +1,53 @@
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+"""Execution context access for schema-bound Python custom ops."""
14+ 
15+from contextlib import contextmanager
16+from contextvars import ContextVar
17+from dataclasses import dataclass
18+from typing import Iterator, Optional
19+ 
20+from ._native import EagerOpExecutionContext
21+ 
22+ 
23+@dataclass
24+class _ExecuteContextBinding:
25+ ctx: EagerOpExecutionContext
26+ active: bool = True
27+ 
28+ 
29+_CURRENT_EXECUTE_CONTEXT: ContextVar[Optional[_ExecuteContextBinding]] = ContextVar(
30+ "ge_custom_op_execute_context", default=None
31+)
32+ 
33+ 
34+def get_execute_ctx() -> EagerOpExecutionContext:
35+ """Return the borrowed context of the active schema-bound execute callback."""
36+ 
37+ binding = _CURRENT_EXECUTE_CONTEXT.get()
38+ if binding is None or not binding.active:
39+ raise RuntimeError(
40+ "get_execute_ctx() is only available inside schema-bound execute"
41+ )
42+ return binding.ctx
43+ 
44+ 
45+@contextmanager
46+def _execute_ctx_scope(ctx: EagerOpExecutionContext) -> Iterator[None]:
47+ binding = _ExecuteContextBinding(ctx=ctx)
48+ token = _CURRENT_EXECUTE_CONTEXT.set(binding)
49+ try:
50+ yield
51+ finally:
52+ binding.active = False
53+ _CURRENT_EXECUTE_CONTEXT.reset(token)
@@ -9,17 +9,175 @@
9 */9 */
10 10 
11#include "bindings.h"11#include "bindings.h"
12+#include "exe_graph/runtime/continuous_vector.h"
12#include "exe_graph/runtime/eager_op_execution_context.h"13#include "exe_graph/runtime/eager_op_execution_context.h"
14+#include "exe_graph/runtime/runtime_attrs.h"
13#include "runtime/native_bindings/runtime_type_wrappers.h"15#include "runtime/native_bindings/runtime_type_wrappers.h"
14 16 
17+#include <cstddef>
18+#include <cstdint>
19+#include <cstring>
15#include <memory>20#include <memory>
16#include <stdexcept>21#include <stdexcept>
22+#include <string>
23+#include <utility>
17 24 
18namespace ge {25namespace ge {
19namespace python_custom_op_native {26namespace python_custom_op_native {
20namespace {27namespace {
21namespace runtime_native = ::ge::python_runtime_native;28namespace runtime_native = ::ge::python_runtime_native;
22 29 
30+template <typename T>
31+const T *GetRequiredAttr(const gert::RuntimeAttrs *attrs, const size_t index, const char *type_name) {
32+ const auto *value = attrs->GetAttrPointer<T>(index);
33+ if (value == nullptr) {
34+ throw std::runtime_error(std::string("Failed to get runtime attr type[") + type_name + "] at index[" +
35+ std::to_string(index) + "]");
36+ }
37+ return value;
38+}
39+ 
40+template <typename T>
41+py::list BuildTypedList(const gert::TypedContinuousVector<T> *value) {
42+ if (value == nullptr) {
43+ throw std::runtime_error("Runtime attr list is null");
44+ }
45+ py::list result;
46+ const auto *data = value->GetData();
47+ for (size_t index = 0U; index < value->GetSize(); ++index) {
48+ result.append(data[index]);
49+ }
50+ return result;
51+}
52+ 
53+py::list BuildDataTypeList(const gert::ContinuousVector *value) {
54+ if (value == nullptr) {
55+ throw std::runtime_error("Runtime attr data type list is null");
56+ }
57+ py::list result;
58+ const auto *data = static_cast<const ge::DataType *>(value->GetData());
59+ for (size_t index = 0U; index < value->GetSize(); ++index) {
60+ result.append(runtime_native::MakeGraphTypeEnum("DataType", static_cast<int32_t>(data[index])));
61+ }
62+ return result;
63+}
64+ 
65+py::list BuildBoolList(const gert::ContinuousVector *value) {
66+ if (value == nullptr) {
67+ throw std::runtime_error("Runtime attr bool list is null");
68+ }
69+ py::list result;
70+ const auto *data = static_cast<const uint8_t *>(value->GetData());
71+ for (size_t index = 0U; index < value->GetSize(); ++index) {
72+ result.append(data[index] != 0U);
73+ }
74+ return result;
75+}
76+ 
77+py::list BuildStringList(const gert::ContinuousVector *value) {
78+ if (value == nullptr) {
79+ throw std::runtime_error("Runtime attr string list is null");
80+ }
81+ py::list result;
82+ const auto *data = static_cast<const char *>(value->GetData());
83+ for (size_t index = 0U; index < value->GetSize(); ++index) {
84+ result.append(py::str(data));
85+ data += std::strlen(data) + 1U;
86+ }
87+ return result;
88+}
89+ 
90+py::list BuildIntListList(const gert::ContinuousVectorVector *value) {
91+ if (value == nullptr) {
92+ throw std::runtime_error("Runtime attr nested int list is null");
93+ }
94+ py::list result;
95+ for (size_t index = 0U; index < value->GetSize(); ++index) {
96+ const auto *inner = value->Get(index);
97+ if (inner == nullptr) {
98+ throw std::runtime_error("Runtime attr nested int list element is null");
99+ }
100+ const auto *data = static_cast<const int64_t *>(inner->GetData());
101+ py::list inner_result;
102+ for (size_t inner_index = 0U; inner_index < inner->GetSize(); ++inner_index) {
103+ inner_result.append(data[inner_index]);
104+ }
105+ result.append(std::move(inner_result));
106+ }
107+ return result;
108+}
109+ 
110+class BorrowedRuntimeAttrs {
111+ public:
112+ BorrowedRuntimeAttrs(const gert::RuntimeAttrs *attrs, std::shared_ptr<bool> valid)
113+ : attrs_(attrs), valid_(std::move(valid)) {}
114+ 
115+ int64_t GetInt(size_t index) const {
116+ return *GetRequiredAttr<int64_t>(Get(), index, "VT_INT");
117+ }
118+ 
119+ float GetFloat(size_t index) const {
120+ return *GetRequiredAttr<float>(Get(), index, "VT_FLOAT");
121+ }
122+ 
123+ bool GetBool(size_t index) const {
124+ return *GetRequiredAttr<bool>(Get(), index, "VT_BOOL");
125+ }
126+ 
127+ std::string GetStr(size_t index) const {
128+ return GetRequiredAttr<char>(Get(), index, "VT_STRING");
129+ }
130+ 
131+ py::object GetDataType(size_t index) const {
132+ const auto value = *GetRequiredAttr<ge::DataType>(Get(), index, "VT_DATA_TYPE");
133+ return runtime_native::MakeGraphTypeEnum("DataType", static_cast<int32_t>(value));
134+ }
135+ 
136+ py::object GetTensor(size_t index) const {
137+ const auto *tensor = GetRequiredAttr<gert::Tensor>(Get(), index, "VT_TENSOR");
138+ return py::cast(runtime_native::NativeTensor::Borrow(const_cast<gert::Tensor *>(tensor), valid_));
139+ }
140+ 
141+ py::list GetListInt(size_t index) const {
142+ return BuildTypedList(Get()->GetListInt(index));
143+ }
144+ 
145+ py::list GetListFloat(size_t index) const {
146+ return BuildTypedList(Get()->GetListFloat(index));
147+ }
148+ 
149+ py::list GetListBool(size_t index) const {
150+ return BuildBoolList(GetRequiredAttr<gert::ContinuousVector>(Get(), index, "VT_LIST_BOOL"));
151+ }
152+ 
153+ py::list GetListStr(size_t index) const {
154+ return BuildStringList(GetRequiredAttr<gert::ContinuousVector>(Get(), index, "VT_LIST_STRING"));
155+ }
156+ 
157+ py::list GetListDataType(size_t index) const {
158+ return BuildDataTypeList(GetRequiredAttr<gert::ContinuousVector>(Get(), index, "VT_LIST_DATA_TYPE"));
159+ }
160+ 
161+ py::list GetListListInt(size_t index) const {
162+ return BuildIntListList(Get()->GetListListInt(index));
163+ }
164+ 
165+ size_t GetAttrNum() const {
166+ return Get()->GetAttrNum();
167+ }
168+ 
169+ private:
170+ const gert::RuntimeAttrs *Get() const {
171+ if ((valid_ == nullptr) || (!(*valid_)) || (attrs_ == nullptr)) {
172+ throw std::runtime_error("Borrowed runtime attrs have expired");
173+ }
174+ return attrs_;
175+ }
176+ 
177+ const gert::RuntimeAttrs *attrs_{nullptr};
178+ std::shared_ptr<bool> valid_;
179+};
180+ 
23class BorrowedEagerOpExecutionContext {181class BorrowedEagerOpExecutionContext {
24 public:182 public:
25 explicit BorrowedEagerOpExecutionContext(gert::EagerOpExecutionContext *ctx)183 explicit BorrowedEagerOpExecutionContext(gert::EagerOpExecutionContext *ctx)
@@ -34,6 +192,22 @@ class BorrowedEagerOpExecutionContext {
34 return Get()->GetComputeNodeInputNum();192 return Get()->GetComputeNodeInputNum();
35 }193 }
36 194 
195+ size_t GetDynamicInputNum(size_t ir_index) const {
196+ const auto *instance_info = Get()->GetIrInputInstanceInfo(ir_index);
197+ if (instance_info == nullptr) {
198+ throw std::runtime_error("Failed to get dynamic input instance info");
199+ }
200+ return instance_info->GetInstanceNum();
201+ }
202+ 
203+ py::object GetAttrs() const {
204+ const auto *attrs = Get()->GetAttrs();
205+ if (attrs == nullptr) {
206+ throw std::runtime_error("Failed to get runtime attrs");
207+ }
208+ return py::cast(BorrowedRuntimeAttrs(attrs, valid_));
209+ }
210+ 
37 py::object GetRequiredInputTensor(size_t ir_index) const {211 py::object GetRequiredInputTensor(size_t ir_index) const {
38 const auto *tensor = Get()->GetRequiredInputTensor(ir_index);212 const auto *tensor = Get()->GetRequiredInputTensor(ir_index);
39 return CastRequiredTensor(tensor, "Failed to get required input tensor");213 return CastRequiredTensor(tensor, "Failed to get required input tensor");
@@ -128,10 +302,27 @@ BorrowedEagerOpExecutionContext BorrowEagerOpExecutionContext(uintptr_t ctx_hand
128} // namespace302} // namespace
129 303 
130void BindEagerOpExecutionContext(py::module_ &m) {304void BindEagerOpExecutionContext(py::module_ &m) {
305+ py::class_<BorrowedRuntimeAttrs>(m, "RuntimeAttrs", "Borrowed view of gert::RuntimeAttrs")
306+ .def("get_int", &BorrowedRuntimeAttrs::GetInt, py::arg("index"))
307+ .def("get_float", &BorrowedRuntimeAttrs::GetFloat, py::arg("index"))
308+ .def("get_bool", &BorrowedRuntimeAttrs::GetBool, py::arg("index"))
309+ .def("get_str", &BorrowedRuntimeAttrs::GetStr, py::arg("index"))
310+ .def("get_data_type", &BorrowedRuntimeAttrs::GetDataType, py::arg("index"))
311+ .def("get_tensor", &BorrowedRuntimeAttrs::GetTensor, py::arg("index"))
312+ .def("get_list_int", &BorrowedRuntimeAttrs::GetListInt, py::arg("index"))
313+ .def("get_list_float", &BorrowedRuntimeAttrs::GetListFloat, py::arg("index"))
314+ .def("get_list_bool", &BorrowedRuntimeAttrs::GetListBool, py::arg("index"))
315+ .def("get_list_str", &BorrowedRuntimeAttrs::GetListStr, py::arg("index"))
316+ .def("get_list_data_type", &BorrowedRuntimeAttrs::GetListDataType, py::arg("index"))
317+ .def("get_list_list_int", &BorrowedRuntimeAttrs::GetListListInt, py::arg("index"))
318+ .def("get_attr_num", &BorrowedRuntimeAttrs::GetAttrNum);
319+ 
131 py::class_<BorrowedEagerOpExecutionContext>(m, "EagerOpExecutionContext",320 py::class_<BorrowedEagerOpExecutionContext>(m, "EagerOpExecutionContext",
132 "Borrowed view of gert::EagerOpExecutionContext")321 "Borrowed view of gert::EagerOpExecutionContext")
133 .def("get_input_tensor", &BorrowedEagerOpExecutionContext::GetInputTensor, py::arg("index"))322 .def("get_input_tensor", &BorrowedEagerOpExecutionContext::GetInputTensor, py::arg("index"))
134 .def("get_input_num", &BorrowedEagerOpExecutionContext::GetInputNum)323 .def("get_input_num", &BorrowedEagerOpExecutionContext::GetInputNum)
324+ .def("get_dynamic_input_num", &BorrowedEagerOpExecutionContext::GetDynamicInputNum, py::arg("ir_index"))
325+ .def("get_attrs", &BorrowedEagerOpExecutionContext::GetAttrs)
135 .def("get_required_input_tensor", &BorrowedEagerOpExecutionContext::GetRequiredInputTensor, py::arg("ir_index"))326 .def("get_required_input_tensor", &BorrowedEagerOpExecutionContext::GetRequiredInputTensor, py::arg("ir_index"))
136 .def("get_optional_input_tensor", &BorrowedEagerOpExecutionContext::GetOptionalInputTensor, py::arg("ir_index"))327 .def("get_optional_input_tensor", &BorrowedEagerOpExecutionContext::GetOptionalInputTensor, py::arg("ir_index"))
137 .def("get_dynamic_input_tensor", &BorrowedEagerOpExecutionContext::GetDynamicInputTensor, py::arg("ir_index"),328 .def("get_dynamic_input_tensor", &BorrowedEagerOpExecutionContext::GetDynamicInputTensor, py::arg("ir_index"),
@@ -12,6 +12,7 @@
12 12 
13"""Pytest coverage for Python custom op API and bridge descriptor protocol."""13"""Pytest coverage for Python custom op API and bridge descriptor protocol."""
14 14 
15+import contextvars
15import importlib16import importlib
16import textwrap17import textwrap
17from pathlib import Path18from pathlib import Path
@@ -62,11 +63,30 @@ class _FakeEagerContext:
62 self.inputs = list(inputs or [])63 self.inputs = list(inputs or [])
63 self.invalidated = False64 self.invalidated = False
64 self.stream = "fake_stream"65 self.stream = "fake_stream"
66+ self.input_calls = []
67+ self.runtime_attrs = _FakeRuntimeAttrs()
68+ self.attrs_requested = False
65 69 
66- @staticmethod70+ def get_required_input_tensor(self, ir_index):
67- def get_optional_input_tensor(ir_index):71+ self.input_calls.append(("required", ir_index))
72+ return ("required", ir_index)
73+ 
74+ def get_optional_input_tensor(self, ir_index):
75+ self.input_calls.append(("optional", ir_index))
68 return ("optional", ir_index)76 return ("optional", ir_index)
69 77 
78+ def get_dynamic_input_num(self, ir_index):
79+ self.input_calls.append(("dynamic_num", ir_index))
80+ return 2
81+ 
82+ def get_dynamic_input_tensor(self, ir_index, relative_index):
83+ self.input_calls.append(("dynamic", ir_index, relative_index))
84+ return ("dynamic", ir_index, relative_index)
85+ 
86+ def get_attrs(self):
87+ self.attrs_requested = True
88+ return self.runtime_attrs
89+ 
70 def get_input_num(self):90 def get_input_num(self):
71 return len(self.inputs)91 return len(self.inputs)
72 92 
@@ -80,6 +100,30 @@ class _FakeEagerContext:
80 self.invalidated = True100 self.invalidated = True
81 101 
82 102 
103+class _FakeRuntimeAttrs:
104+ def __init__(self):
105+ self.calls = []
106+ 
107+ def __getattr__(self, name):
108+ if not name.startswith("get_"):
109+ raise AttributeError(name)
110+ 
111+ def getter(index):
112+ self.calls.append((name, index))
113+ return (name, index)
114+ 
115+ return getter
116+ 
117+ 
118+def _create_holder(instance_id: str, op_type: str) -> None:
119+ descriptors = {
120+ item["op_type"]: item for item in bridge.load_and_get_op_impl_descriptors()
121+ }
122+ assert bridge.create_op_impl_holder(
123+ instance_id, descriptors[op_type]["descriptor_key"]
124+ )
125+ 
126+ 
83def test_register_op_impl_exports_descriptor_dict():127def test_register_op_impl_exports_descriptor_dict():
84 @custom_op.register_op_impl(op_type="AddCustom")128 @custom_op.register_op_impl(op_type="AddCustom")
85 class AddCustom(custom_op.EagerExecuteOp):129 class AddCustom(custom_op.EagerExecuteOp):
@@ -95,57 +139,27 @@ def test_register_op_impl_exports_descriptor_dict():
95 assert AddCustom.__ge_op_impl_descriptor__.to_bridge_dict() == descriptors[0]139 assert AddCustom.__ge_op_impl_descriptor__.to_bridge_dict() == descriptors[0]
96 140 
97 141 
98-def test_inputs_execute_adapts_inputs_and_injects_context_methods():142+def test_schema_bound_execute_class_definition_is_supported():
99 @custom_op.register_op_impl(op_type="AutoExecuteCustom")143 @custom_op.register_op_impl(op_type="AutoExecuteCustom")
100 class AutoExecuteCustom(custom_op.EagerExecuteOp):144 class AutoExecuteCustom(custom_op.EagerExecuteOp):
101 def __init__(self):145 def __init__(self):
102- self.seen_inputs = None146+ self.seen_args = None
103- self.seen_stream = None
104- self.seen_optional = None
105- self.seen_input_num = None
106 147 
107- def execute(self, inputs) -> None:148+ def execute(self, x, optional_y, dynamic_z, *, alpha) -> None:
108- self.seen_inputs = inputs149+ self.seen_args = (x, optional_y, dynamic_z, alpha)
109- self.seen_stream = self.get_stream()
110- self.seen_optional = self.get_optional_input_tensor(1)
111- self.seen_input_num = self.get_input_num()
112 150 
113- ctx = _FakeEagerContext(inputs=["x", "y"])
114 instance = AutoExecuteCustom()151 instance = AutoExecuteCustom()
152+ instance.execute("x", None, ["z0", "z1"], alpha=1.0)
115 153 
116- assert instance.execute(ctx) is None154+ assert instance.seen_args == ("x", None, ["z0", "z1"], 1.0)
117- assert instance.seen_inputs == ["x", "y"]
118- assert instance.seen_stream == "fake_stream"
119- assert instance.seen_optional == ("optional", 1)
120- assert instance.seen_input_num == 2
121- assert "get_stream" not in instance.__dict__
122- assert "get_optional_input_tensor" not in instance.__dict__
123- assert "get_input_num" not in instance.__dict__
124 155 
125 156 
126-def test_inputs_execute_restores_existing_instance_attribute():157+def test_get_execute_ctx_is_unavailable_outside_schema_execute():
127- @custom_op.register_op_impl(op_type="AutoExecuteRestoreCustom")158+ with pytest.raises(
128- class AutoExecuteRestoreCustom(custom_op.EagerExecuteOp):159+ RuntimeError,
129- def __init__(self):160+ match="only available inside schema-bound execute",
130- self.get_stream = "user_value"161+ ):
131- 162+ custom_op.get_execute_ctx()
132- def execute(self, inputs) -> None:
133- assert self.get_stream() == "fake_stream"
134- 
135- ctx = _FakeEagerContext(inputs=["x"])
136- instance = AutoExecuteRestoreCustom()
137- 
138- assert instance.execute(ctx) is None
139- assert instance.get_stream == "user_value"
140- 
141- 
142-def test_inputs_execute_ignores_return_value():
143- @custom_op.register_op_impl(op_type="AutoExecuteReturnCustom")
144- class AutoExecuteReturnCustom(custom_op.EagerExecuteOp):
145- def execute(self, inputs) -> None:
146- return True
147- 
148- assert AutoExecuteReturnCustom().execute(_FakeEagerContext(inputs=["x"])) is None
149 163 
150 164 
151def test_ctx_execute_keeps_context_argument():165def test_ctx_execute_keeps_context_argument():
@@ -164,24 +178,6 @@ def test_ctx_execute_keeps_context_argument():
164 assert instance.seen_ctx is ctx178 assert instance.seen_ctx is ctx
165 179 
166 180 
167-def test_execute_rejects_unknown_single_argument_name():
168- with pytest.raises(TypeError) as exc_info:
169- exec(
170- textwrap.dedent("""
171- class UnknownExecuteArgCustom(custom_op.EagerExecuteOp):
172- def execute(self, input) -> None:
173- pass
174- """),
175- {"custom_op": custom_op},
176- )
177- 
178- message = str(exc_info.value)
179- assert (
180- "Use 'ctx' for EagerOpExecutionContext or 'inputs' for the input tensor list."
181- in message
182- )
183- 
184- 
185def test_register_op_impl_rejects_duplicate_op_type():181def test_register_op_impl_rejects_duplicate_op_type():
186 @custom_op.register_op_impl(op_type="AddCustom")182 @custom_op.register_op_impl(op_type="AddCustom")
187 class AddCustom(custom_op.EagerExecuteOp):183 class AddCustom(custom_op.EagerExecuteOp):
@@ -263,6 +259,290 @@ def test_bridge_call_execute_ignores_return_value():
263 assert bridge.destroy_op_impl_holder(instance_id) is True259 assert bridge.destroy_op_impl_holder(instance_id) is True
264 260 
265 261 
262+def test_bridge_call_execute_binds_schema_inputs_and_attrs():
263+ called = []
264+ 
265+ @custom_op.register_op_impl(op_type="SchemaBoundCustom")
266+ class SchemaBoundCustom(custom_op.EagerExecuteOp):
267+ def execute(self, x, optional_y, dynamic_z, *, alpha, axes):
268+ called.append((x, optional_y, dynamic_z, alpha, axes))
269+ return True
270+ 
271+ descriptor_key = bridge.load_and_get_op_impl_descriptors()[0]["descriptor_key"]
272+ instance_id = "SchemaBoundCustom#1"
273+ ctx = _FakeEagerContext()
274+ ir_meta = {
275+ "op_type": "SchemaBoundCustom",
276+ "inputs": [
277+ {"name": "x", "kind": 0},
278+ {"name": "optional_y", "kind": 1},
279+ {"name": "dynamic_z", "kind": 2},
280+ ],
281+ "attrs": [
282+ {"name": "alpha", "type": "VT_FLOAT"},
283+ {"name": "axes", "type": "VT_LIST_INT"},
284+ ],
285+ "outputs": [],
286+ }
287+ 
288+ assert bridge.create_op_impl_holder(instance_id, descriptor_key) is True
289+ assert bridge.call_execute(instance_id, ir_meta, ctx) is None
290+ assert called == [
291+ (
292+ ("required", 0),
293+ ("optional", 1),
294+ [("dynamic", 2, 0), ("dynamic", 2, 1)],
295+ ("get_float", 0),
296+ ("get_list_int", 1),
297+ )
298+ ]
299+ assert ctx.input_calls == [
300+ ("required", 0),
301+ ("optional", 1),
302+ ("dynamic_num", 2),
303+ ("dynamic", 2, 0),
304+ ("dynamic", 2, 1),
305+ ]
306+ assert ctx.runtime_attrs.calls == [
307+ ("get_float", 0),
308+ ("get_list_int", 1),
309+ ]
310+ assert ctx.attrs_requested is True
311+ assert ctx.invalidated is True
312+ 
313+ 
314+def test_schema_bound_execute_can_get_current_context():
315+ called = []
316+ 
317+ @custom_op.register_op_impl(op_type="SchemaContextCustom")
318+ class SchemaContextCustom(custom_op.EagerExecuteOp):
319+ def execute(self, x):
320+ current_ctx = custom_op.get_execute_ctx()
321+ called.append((x, current_ctx, current_ctx.get_stream()))
322+ 
323+ instance_id = "SchemaContextCustom#1"
324+ ctx = _FakeEagerContext()
325+ _create_holder(instance_id, "SchemaContextCustom")
326+ 
327+ bridge.call_execute(
328+ instance_id,
329+ {
330+ "op_type": "SchemaContextCustom",
331+ "inputs": [{"name": "x", "kind": 0}],
332+ "attrs": [],
333+ "outputs": [],
334+ },
335+ ctx,
336+ )
337+ 
338+ assert called == [(("required", 0), ctx, "fake_stream")]
339+ assert ctx.invalidated is True
340+ with pytest.raises(
341+ RuntimeError,
342+ match="only available inside schema-bound execute",
343+ ):
344+ custom_op.get_execute_ctx()
345+ 
346+ 
347+def test_get_execute_ctx_is_not_bound_for_legacy_execute():
348+ errors = []
349+ 
350+ @custom_op.register_op_impl(op_type="LegacyContextCustom")
351+ class LegacyContextCustom(custom_op.EagerExecuteOp):
352+ def execute(self, ctx):
353+ try:
354+ custom_op.get_execute_ctx()
355+ except RuntimeError as exc:
356+ errors.append(str(exc))
357+ 
358+ instance_id = "LegacyContextCustom#1"
359+ ctx = _FakeEagerContext()
360+ _create_holder(instance_id, "LegacyContextCustom")
361+ 
362+ bridge.call_execute(instance_id, None, ctx)
363+ 
364+ assert errors == ["get_execute_ctx() is only available inside schema-bound execute"]
365+ assert ctx.invalidated is True
366+ 
367+ 
368+def test_schema_execute_context_is_deactivated_after_exception():
369+ copied_contexts = []
370+ 
371+ @custom_op.register_op_impl(op_type="FailingSchemaContextCustom")
372+ class FailingSchemaContextCustom(custom_op.EagerExecuteOp):
373+ def execute(self, x):
374+ assert custom_op.get_execute_ctx() is not None
375+ copied_contexts.append(contextvars.copy_context())
376+ raise ValueError("schema execute failed")
377+ 
378+ instance_id = "FailingSchemaContextCustom#1"
379+ ctx = _FakeEagerContext()
380+ _create_holder(instance_id, "FailingSchemaContextCustom")
381+ 
382+ with pytest.raises(ValueError, match="schema execute failed"):
383+ bridge.call_execute(
384+ instance_id,
385+ {
386+ "op_type": "FailingSchemaContextCustom",
387+ "inputs": [{"name": "x", "kind": 0}],
388+ "attrs": [],
389+ "outputs": [],
390+ },
391+ ctx,
392+ )
393+ 
394+ assert ctx.invalidated is True
395+ with pytest.raises(
396+ RuntimeError,
397+ match="only available inside schema-bound execute",
398+ ):
399+ copied_contexts[0].run(custom_op.get_execute_ctx)
400+ 
401+ 
402+def test_schema_execute_context_restores_outer_binding_after_nested_call():
403+ called = []
404+ outer_ctx = _FakeEagerContext()
405+ inner_ctx = _FakeEagerContext()
406+ 
407+ @custom_op.register_op_impl(op_type="InnerSchemaContextCustom")
408+ class InnerSchemaContextCustom(custom_op.EagerExecuteOp):
409+ def execute(self, x):
410+ called.append(("inner", custom_op.get_execute_ctx()))
411+ 
412+ @custom_op.register_op_impl(op_type="OuterSchemaContextCustom")
413+ class OuterSchemaContextCustom(custom_op.EagerExecuteOp):
414+ def execute(self, x):
415+ called.append(("outer_before", custom_op.get_execute_ctx()))
416+ bridge.call_execute(
417+ "InnerSchemaContextCustom#1",
418+ {
419+ "op_type": "InnerSchemaContextCustom",
420+ "inputs": [{"name": "x", "kind": 0}],
421+ "attrs": [],
422+ "outputs": [],
423+ },
424+ inner_ctx,
425+ )
426+ called.append(("outer_after", custom_op.get_execute_ctx()))
427+ 
428+ _create_holder("InnerSchemaContextCustom#1", "InnerSchemaContextCustom")
429+ _create_holder("OuterSchemaContextCustom#1", "OuterSchemaContextCustom")
430+ 
431+ bridge.call_execute(
432+ "OuterSchemaContextCustom#1",
433+ {
434+ "op_type": "OuterSchemaContextCustom",
435+ "inputs": [{"name": "x", "kind": 0}],
436+ "attrs": [],
437+ "outputs": [],
438+ },
439+ outer_ctx,
440+ )
441+ 
442+ assert called == [
443+ ("outer_before", outer_ctx),
444+ ("inner", inner_ctx),
445+ ("outer_after", outer_ctx),
446+ ]
447+ assert inner_ctx.invalidated is True
448+ assert outer_ctx.invalidated is True
449+ 
450+ 
451+def test_bridge_call_execute_supports_zero_input_and_zero_attr_schema():
452+ called = []
453+ 
454+ @custom_op.register_op_impl(op_type="NoArgCustom")
455+ class NoArgCustom(custom_op.EagerExecuteOp):
456+ def execute(self):
457+ called.append(True)
458+ 
459+ descriptor_key = bridge.load_and_get_op_impl_descriptors()[0]["descriptor_key"]
460+ instance_id = "NoArgCustom#1"
461+ ctx = _FakeEagerContext()
462+ assert bridge.create_op_impl_holder(instance_id, descriptor_key) is True
463+ 
464+ assert (
465+ bridge.call_execute(
466+ instance_id,
467+ {"op_type": "NoArgCustom", "inputs": [], "attrs": [], "outputs": []},
468+ ctx,
469+ )
470+ is None
471+ )
472+ assert called == [True]
473+ assert ctx.input_calls == []
474+ assert ctx.runtime_attrs.calls == []
475+ assert ctx.attrs_requested is False
476+ assert ctx.invalidated is True
477+ 
478+ 
479+@pytest.mark.parametrize(
480+ ("ir_type", "getter_name"),
481+ [
482+ ("VT_INT", "get_int"),
483+ ("VT_FLOAT", "get_float"),
484+ ("VT_BOOL", "get_bool"),
485+ ("VT_STRING", "get_str"),
486+ ("VT_DATA_TYPE", "get_data_type"),
487+ ("VT_TENSOR", "get_tensor"),
488+ ("VT_LIST_INT", "get_list_int"),
489+ ("VT_LIST_FLOAT", "get_list_float"),
490+ ("VT_LIST_BOOL", "get_list_bool"),
491+ ("VT_LIST_STRING", "get_list_str"),
492+ ("VT_LIST_DATA_TYPE", "get_list_data_type"),
493+ ("VT_LIST_LIST_INT", "get_list_list_int"),
494+ ],
495+)
496+def test_bridge_reads_runtime_attr_by_canonical_type(ir_type, getter_name):
497+ attrs = _FakeRuntimeAttrs()
498+ 
499+ assert bridge._read_runtime_attr(attrs, 3, ir_type) == (getter_name, 3)
500+ assert attrs.calls == [(getter_name, 3)]
501+ 
502+ 
503+def test_bridge_rejects_schema_bound_execute_without_ir_meta():
504+ @custom_op.register_op_impl(op_type="MissingSchemaCustom")
505+ class MissingSchemaCustom(custom_op.EagerExecuteOp):
506+ def execute(self, inputs):
507+ pass
508+ 
509+ descriptor_key = bridge.load_and_get_op_impl_descriptors()[0]["descriptor_key"]
510+ instance_id = "MissingSchemaCustom#1"
511+ ctx = _FakeEagerContext()
512+ assert bridge.create_op_impl_holder(instance_id, descriptor_key) is True
513+ 
514+ with pytest.raises(
515+ RuntimeError,
516+ match="canonical IR not found for schema-bound execute: MissingSchemaCustom",
517+ ):
518+ bridge.call_execute(instance_id, None, ctx)
519+ 
520+ assert ctx.invalidated is True
521+ 
522+ 
523+def test_native_context_exposes_execute_binding_views():
524+ native_module = importlib.import_module("ge.custom_op._native")._native
525+ assert hasattr(native_module, "RuntimeAttrs")
526+ for method_name in (
527+ "get_int",
528+ "get_float",
529+ "get_bool",
530+ "get_str",
531+ "get_data_type",
532+ "get_tensor",
533+ "get_list_int",
534+ "get_list_float",
535+ "get_list_bool",
536+ "get_list_str",
537+ "get_list_data_type",
538+ "get_list_list_int",
539+ "get_attr_num",
540+ ):
541+ assert hasattr(native_module.RuntimeAttrs, method_name)
542+ assert hasattr(native_module.EagerOpExecutionContext, "get_dynamic_input_num")
543+ assert hasattr(native_module.EagerOpExecutionContext, "get_attrs")
544+ 
545+ 
266def test_bridge_rejects_unknown_descriptor_key():546def test_bridge_rejects_unknown_descriptor_key():
267 with pytest.raises(KeyError, match="descriptor_key not found"):547 with pytest.raises(KeyError, match="descriptor_key not found"):
268 bridge.create_op_impl_holder("unknown#1", "not-found")548 bridge.create_op_impl_holder("unknown#1", "not-found")
@@ -214,6 +214,8 @@ TEST_F(EagerOpExecutionContextUT, GetDynamicInputTensor) {
214 auto context = dynamic_input_case_.context_holder.GetContext<EagerOpExecutionContext>();214 auto context = dynamic_input_case_.context_holder.GetContext<EagerOpExecutionContext>();
215 215 
216 ASSERT_NE(context, nullptr);216 ASSERT_NE(context, nullptr);
217+ ASSERT_NE(context->GetIrInputInstanceInfo(1), nullptr);
218+ EXPECT_EQ(context->GetIrInputInstanceInfo(1)->GetInstanceNum(), 2U);
217 auto tensor_1_in_context = context->GetDynamicInputTensor(1, 1);219 auto tensor_1_in_context = context->GetDynamicInputTensor(1, 1);
218 ASSERT_NE(tensor_1_in_context, nullptr);220 ASSERT_NE(tensor_1_in_context, nullptr);
219 EXPECT_EQ(tensor_1_in_context->GetOriginShape(), dynamic_input_case_.input_tensors[1].GetOriginShape());221 EXPECT_EQ(tensor_1_in_context->GetOriginShape(), dynamic_input_case_.input_tensors[1].GetOriginShape());