已合并
feat: 支持schema-bound自定义算子execute接口 #4188
GengChao创建于 7月30日
feat: 支持schema-bound自定义算子execute接口 #4188
已合并
共 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 | ||
| 15 | import sys | 16 | import sys |
| 16 | import threading | 17 | import threading |
| 17 | from dataclasses import dataclass | 18 | from dataclasses import dataclass |
| @@ -19,6 +20,7 @@ from typing import Dict, Optional | |||
| 19 | 20 | ||
| 20 | from .base import BaseCustomOp, EagerExecuteOp, EagerOpExecutionContext | 21 | from .base import BaseCustomOp, EagerExecuteOp, EagerOpExecutionContext |
| 21 | from .bootstrap import get_registered_op_impls, load_custom_op_plugins | 22 | from .bootstrap import get_registered_op_impls, load_custom_op_plugins |
| 23 | +from .context import _execute_ctx_scope | ||
| 22 | from .registry import get_registered_op_impl_by_descriptor_key | 24 | from .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 | ||
| 36 | def load_and_get_op_impl_descriptors() -> list: | 57 | def 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 None | 96 | 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 | + | ||
| 78 | def call_execute( | 154 | def 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 | + ) | ||
| 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 | + | ||
| 23 | class EagerOpExecutionContext: | 53 | class 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 | ||
| 15 | from __future__ import annotations | 15 | from __future__ import annotations |
| 16 | -import inspect | ||
| 17 | from abc import ABC, abstractmethod | 16 | from abc import ABC, abstractmethod |
| 18 | -from functools import wraps | ||
| 19 | -from typing import Callable, List | ||
| 20 | 17 | ||
| 21 | -from ._native import EagerOpExecutionContext | 18 | +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 | ||
| 108 | class BaseCustomOp(ABC): | 21 | class BaseCustomOp(ABC): |
| @@ -112,14 +25,7 @@ class BaseCustomOp(ABC): | |||
| 112 | class EagerExecuteOp(BaseCustomOp): | 25 | class 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 | @abstractmethod | 28 | @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 NotImplementedError | 31 | 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 | ||
| 18 | namespace ge { | 25 | namespace ge { |
| 19 | namespace python_custom_op_native { | 26 | namespace python_custom_op_native { |
| 20 | namespace { | 27 | namespace { |
| 21 | namespace runtime_native = ::ge::python_runtime_native; | 28 | namespace 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 | + | ||
| 23 | class BorrowedEagerOpExecutionContext { | 181 | class 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 | } // namespace | 302 | } // namespace |
| 129 | 303 | ||
| 130 | void BindEagerOpExecutionContext(py::module_ &m) { | 304 | void 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 | ||
| 15 | import importlib | 16 | import importlib |
| 16 | import textwrap | 17 | import textwrap |
| 17 | from pathlib import Path | 18 | from pathlib import Path |
| @@ -62,11 +63,30 @@ class _FakeEagerContext: | |||
| 62 | self.inputs = list(inputs or []) | 63 | self.inputs = list(inputs or []) |
| 63 | self.invalidated = False | 64 | 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 | - @staticmethod | 70 | + 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 = True | 100 | 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 | + | ||
| 83 | def test_register_op_impl_exports_descriptor_dict(): | 127 | def 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 = None | 146 | + 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 = inputs | 149 | + 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 None | 154 | + 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 | ||
| 151 | def test_ctx_execute_keeps_context_argument(): | 165 | def test_ctx_execute_keeps_context_argument(): |
| @@ -164,24 +178,6 @@ def test_ctx_execute_keeps_context_argument(): | |||
| 164 | assert instance.seen_ctx is ctx | 178 | 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 | - | ||
| 185 | def test_register_op_impl_rejects_duplicate_op_type(): | 181 | def 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 True | 259 | 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 | + | ||
| 266 | def test_bridge_rejects_unknown_descriptor_key(): | 546 | def 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()); |
🟡 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。