草稿
[WIP]【PR】: 迁移 ONNX Plugin 文档至 develop #2
gentle-knight创建于 29 天前
[WIP]【PR】: 迁移 ONNX Plugin 文档至 develop #2
草稿
gentle-knight创建于 29 天前
gentle-knight
gentle-knight成员
29 天前

Pull Request

描述

将 cann/ge PR 4151 的有效文档内容迁移到个人 fork gentle-knight/ge 的 develop 分支,并纳入已完成复核的 ONNX Plugin C++ 链路调研与 Python 化方案修订。

迁移保留 12 个有效 Markdown 文件;明确排除框架理解 walkthrough 文档 hfqx/analysis/ge_project_framework_understanding.md,并同步删除 README 中两处索引;同时排除无法通过 JSON/OAT 校验的零字节 hfqx/dev-log/learning_record.ipynb

变更类型

关联的Issue

无。迁移自 cann/ge PR 4151。

如何测试

  1. 校验相对 develop 仅包含 12 个预期文档文件,且无 walkthrough 与空 notebook。
  2. 校验 README 本地链接、5 张 Mermaid 图、Python 示例 AST,并执行全量 pre-commit/OAT。

测试结果:所有本地链接有效;Mermaid 5/5 解析通过;可执行 Python 示例 AST 通过;pre-commit、codespell、detect-private-key 与 OAT 全部通过;五路迁移复核无阻塞项。

核对清单

其他信息

目标仓库:gentle-knight/ge
源分支:docs/onnx-plugin-pr-4151-to-develop
目标分支:develop
旧 PR 4151 与误建 PR 4204 将在本 PR 的远端文件范围验证通过后关闭。

likedislike
合并受阻
GengChao
GengChao29 天前进行代码检视1
hfqx/analysis/onnx_plugin_python_modification_plan.md
已过期
@@ -0,0 +188,4 @@
188+ 
189+| C++ 接口/属性 | 当前插件用途 | Python 对应 | 动作与状态 |
190+|---|---|---|---|
191+| `Operator::GetName/GetOpType` | 读取节点名和 target 类型 | `OperatorView.name/type` | 新增只读视图 |
GengChao
GengChao29 天前评论:

可以就叫Operator,放在ge的graph模块下,这样后续可以按需扩展其他场景需要的能力到operator.py,直到跟ge::Operator能力基本一致

likedislike
GengChao
GengChao29 天前进行代码检视1
hfqx/analysis/onnx_plugin_python_modification_plan.md
已过期
@@ -0,0 +531,4 @@
531+ )
532+ output_desc = target.output_desc(0)
533+ output_desc.set_data_type(DataType(dst_dtype))
534+ return OperatorUpdate(
GengChao
GengChao29 天前评论:

Operator不要定位为只读+Update才能更改了,这样比较别扭; TensorDescUpdate一样的道理,就提供set接口让用户自己调用,然后更新后的tensordesc和持有它的operator对象

likedislike
GengChao
GengChao29 天前进行代码检视1
hfqx/analysis/onnx_plugin_python_modification_plan.md
已过期
@@ -0,0 +488,4 @@
488+ "alpha": node.attrs.get("alpha", 1.0),
489+ "original_type": "ai.onnx::11::ThresholdedRelu",
490+ },
491+ dynamic_inputs=(DynamicPort("x", 1),),
GengChao
GengChao29 天前评论:

DynamicPort的封装没有必要,提供一个用例 当ir既有可选,必选,动态输入的时候 这个函数应该怎么写

likedislike
GengChao
GengChao29 天前进行代码检视1
hfqx/analysis/onnx_plugin_python_modification_plan.md
已过期
@@ -0,0 +647,4 @@
647+ 
648+### 基于调研结果对原计划的修订
649+ 
650+| 原计划问题 | 本次修订 |
GengChao
GengChao29 天前评论:

事情列的挺多,一个迭代做不完,建议你列一个表单,我们当前迭代支持哪些parsr接口,接口里面支持哪些类型的属性设置 ; 其他的暂不支持; 第一个迭代打通流程和涵盖基本的插件注册的写法即可

likedislike
gentle-knightgentle-knight成员
28 天前 推送  1 个提交:ae4a7f1f-docs: 完善 ONNX Plugin Python 化设计文档
gentle-knightgentle-knight成员
28 天前 推送  1 个提交:71872be6-docs: 从 PR 中移除串讲稿
gentle-knightgentle-knight成员
28 天前 强制推送  1 个提交:331aa1ff-docs: 完善 ONNX Plugin Python 化设计文档
gentle-knightgentle-knight成员
28 天前 推送  1 个提交:81849177-docs: 补充公开接口评审与 ST Sample
gentle-knightgentle-knight成员
28 天前 推送  1 个提交:7a649bf7-docs: 完善 ImplyType 和端口注册设计
gentle-knightgentle-knight成员
25 天前 推送  1 个提交:caa645a9-docs: 补充 ONNX Plugin 竞品调研
gentle-knightgentle-knight成员
25 天前 推送  1 个提交:20558035-docs: 增加环境变量复用评审门禁
gentle-knightgentle-knight成员
16 天前 设置为草稿状态
gentle-knight
gentle-knight成员
25 分钟前 评论:

ONNX Plugin Python 化 SRS

阅读说明

文档目的

本文说明如何让插件开发者使用 Python 编写 ONNX 到 GE Operator 的转换逻辑,给出公开接口、用户写法、模块划分和验收要求。文档描述当前代码仓库已交付的现状(截至 PR #4583,commit 9e7628ea7),作为测试设计的输入。

当前 C++ 参考实现(threshold_relu_onnx_plugin.cpp)完成了三件事:

  1. 注册 ONNX 原始算子到 GE 目标算子:
    ThresholdedRelu 注册为 GE 的 PartitionedCall
  2. 解析 ONNX NodeProto(一对一):
    读取 node.name()alpha 属性,设置到 ge::Operator,并注册动态输入输出。
  3. 构造 GE 子图(一对多):
    Data -> Identity -> Threshold -> Mul 表达 ThresholdedRelu,再设置 Graph 输入输出。

文档范围

当前已交付(PR1~PR4 合入内容 + PR #4583):

  • onnx_plugin(source, domain, opsets, target) 插件描述;
  • ParseParamsFn 对应的 parse_node callback;
  • ParseParamsByOperatorFn 对应的 parse_operator callback;
  • ParseOpToGraphFn 对应的 decompose callback(复用 ES GraphBuilder 构图);
  • OnnxNode 只读字段和常用属性;
  • ge.graph.Operator 常用属性操作和端口注册(含 source 只读语义);
  • Python 插件发现、注册提交和独立 bridge(atc/session/aclgrphParseONNX 全入口接入);
  • Python UT、bridge UT、parser ST 和 examples/onnx_plugin/ 端到端样例。

当前不包括:

  • ParseSubgraphPostFn/ParseSubgraphFuncV2 对应的 Python callback——经评审裁决不提供:控制流算子(If/While/Case)由内置 C++ 实现处理,不存在用户自定义控制算子的真实场景,相关代码已回退(PR #4583,2026-08-28);
  • TensorDesc 修改、Tensor/Graph/Sparse 属性和属性引用(后续迭代评估);
  • Operator 原型、Kernel、compiler、runtime、AscendIR 或 OM 格式修改。

名词说明

名称 含义
ONNX plugin 将 ONNX 原始节点转换为 GE 目标 Operator 的 parser 插件。
source ONNX 原始 op type,例如 Elu
target GE 中已经注册的目标 Operator type,例如 AccumulateNV2
origin type 由 domain、opset 和 source 构成的完整类型,例如 ai.onnx::11::Elu
OnnxNode parser 从 NodeProto 提取出的 Python 只读值对象(native pybind,由 C++ bridge 构造)。
ge.graph.Operator callback 期间对 ge::Operator 的受控 Python 包装(ctypes 借用句柄)。
parse_node 对应 C++ ParseParamsFn 的 Python callback,接收 (node, target)
parse_operator 对应 C++ ParseParamsByOperatorFn 的 Python callback,接收只读 source 和可写 target
decompose 对应 C++ ParseOpToGraphFn 的 Python callback,接收只读 source,返回替代子图 ge.graph.Graph
canonical path 经过 Path.resolve() 归一化后的唯一物理路径,用于模块去重。
bridge loader 按运行时 Python ABI(cpXY)发现并 dlopen bridge SO 的组件(onnx_plugin_bridge_loader.cc)。
bridge 独立 libge_python_onnx_plugin_bridge.so,负责 GIL/handle/异常转换和注册协调(RegisterDescriptor 冲突预检查 + Finalize/Register 提交)。

总体概述

本特性打通一条生产链路:用户用 Python 编写 plugin 文件,声明 ONNX source 到 GE target 的映射;初始化阶段由 loader 发现并按 canonical path 单次 import,registry 冻结 descriptor,bridge loader 按 Python ABI(cpXY)加载 bridge SO,bridge 预检查后提交到现有 parser registry;解析阶段由 parser 按 origin type 找到 Python callback,bridge 确认 protobuf 消息类型为 ge::onnx::NodeProto 后以引用方式借用构造只读 native OnnxNode,交给用户 callback 补充 target Operator 的属性与端口(或按 callback 类型传入只读 source/返回替代子图)。

模块分层与调用关系

flowchart TB
    subgraph Py["Python 侧 api/python/ge/ge/"]
        API["公开接口层<br/>onnx_plugin / OnnxNode /<br/>OnnxPlugin / Operator"]
        REG["descriptor registry<br/>校验 / 冻结 / origin 索引"]
        LOADER["plugin loader<br/>canonical-path 单次 import"]
    end

    subgraph Cpp["C++ 侧 parser/"]
        BL["薄 bridge loader<br/>冲突预检查 + 提交"]
        NATIVE["bridge 内构造<br/>NodeProto → native OnnxNode"]
        SO["独立 bridge SO<br/>GIL / handle / 异常转换"]
        REGP["现有 parser/registry<br/>OpParserFactory / OpRegistry"]
    end

    LOADER -->|"模块只 import 一次"| REG
    API -->|"decorator 写入"| REG
    REG -->|"descriptor batch"| BL
    BL -->|"Finalize / Register"| REGP
    REGP -->|"NodeProto 引用"| NATIVE
    NATIVE -->|"OnnxNode"| SO
    SO -->|"dispatch parse_node"| API

注册与解析两阶段链路

端到端链路分为注册阶段和解析阶段,虚线表示注册阶段产物被解析阶段消费:

flowchart TD
    subgraph Reg["注册阶段(初始化时执行一次)"]
        P1["plugin.py(ASCEND_CUSTOM_OPP_PATH)"] -->|"canonical path 单次 import"| L1["plugin loader"]
        L1 -->|"decorator 注册"| R1["Python registry<br/>冻结 descriptor"]
        R1 -->|"descriptor batch"| B1["薄 bridge loader<br/>冲突预检查"]
        B1 -->|"Finalize"| F1["OpParserFactory<br/>注册 target creator"]
        B1 -->|"Register"| F2["OpRegistry<br/>origin 映射 + callback map"]
    end

    subgraph Parse["解析阶段(每个 ONNX 节点)"]
        G1["ONNX GraphProto"] --> G2["构造完整 origin type"]
        G2 --> G3["AdapterOpType 查映射"]
        G3 --> G4["创建 target Operator"]
        G4 --> G5["OnnxCustomParserAdapter"]
        G5 --> G6["bridge SO<br/>确认 NodeProto 类型并借用构造"]
        G6 --> G7["native OnnxNode +<br/>callback-bound Operator"]
        G7 --> G8["parse_node(node, target)"]
        G8 -->|"成功"| G9["Graph::AddOp + 连边"]
    end

    F1 -. "提供 target creator" .-> G4
    F2 -. "提供 origin 映射" .-> G3
    F2 -. "提供 ParseParamsFn callback" .-> G5

1. 竞品用户界面

1.1 横向对比

在确定用户界面之前,先对照业界同类能力的用户写法,说明 GE Python ONNX Plugin 的定位差异。

# onnx2torch:converter 开发者入口
@add_converter(operation_type="Elu", version=6)
def convert_elu(node: OnnxNode, graph: OnnxGraph) -> OperationConverterResult:
    return OperationConverterResult(
        torch_module=nn.ELU(alpha=node.attributes.get("alpha", 1.0)),
        onnx_mapping=onnx_mapping_from_node(node),
    )
# TensorRT Python Plugin:定义自定义层的 shape/dtype 和 Kernel
@trtp.register("example::identity")
def identity_desc(inp: trtp.TensorDesc) -> trtp.TensorDesc:
    return inp.like()


@trtp.impl("example::identity")
def identity_impl(inp, outputs, stream) -> None:
    launch_identity(inp, outputs[0], stream)
对象 转换方向 用户 callback 的主要职责 对本方案的参考
PyTorch ONNX Exporter PyTorch/FX → ONNX 创建 ONNX 表达 callable 和诊断方式
TensorRT ONNX Parser ONNX → TensorRT Network Python 不可扩展节点映射 Parser/Network 分层
TensorRT Python Plugin Custom Node → Plugin Layer 定义 shape/dtype/Kernel 装饰器形式
onnx2torch ONNX → PyTorch FX/Module 从零创建 Module 和 mapping 只读节点 + 属性默认值写法
GE Python ONNX Plugin ONNX → 已有 GE target Operator 补充属性和实例端口 本次设计主体

1.2 定位结论

GE 的 ONNX parser 已经创建 target Operator 并负责入图和连边,因此 GE Python ONNX Plugin 的 callback 不需要:

  • 从零创建算子实现(区别于 TensorRT Python Plugin);
  • 创建 Module、Graph 或 mapping 对象(区别于 onnx2torch);
  • 声明完整的 shape/dtype/Kernel(区别于 TensorRT Python Plugin)。

它只需要做一件事:读取 ONNX 源节点,补充已经创建好的 target Operator 的属性和实例端口。因此用户界面采用「descriptor + callback」形式,callback 直接接收只读 OnnxNode 和 callback-bound 的 ge.graph.Operator

2. 当前能力和已闭合缺口

2.1 当前已有能力

GE 和现有 Python 模块已经具备以下基础能力:

  • C++ ONNX plugin 可以通过 REGISTER_CUSTOM_OPOpRegistrationData 描述映射;
  • OpRegistrationTbe::FinalizeOpParserFactory 可以建立 target parser creator;
  • OpRegistry 可以保存 ParseParams、ParseOpToGraph 等 callback;
  • OnnxCustomParserAdapter 可以调用 ParseParamsFn
  • ge::Operator 已提供属性、动态输入输出和 TensorDesc 接口;
  • ge._internal.plugin_loader 已能按环境变量扫描 Python 文件或 package;
  • ge.es.GraphBuilder 已能创建输入并构建 Graph。

现有 ops-nn 和 ops-math 注册调用扫描结果如下:

C++ callback 注册调用数 当前分期
ParseParamsFn 175 已交付(parse_node)
ParseParamsByOperatorFn 7 已交付(parse_operator)
ParseOpToGraphFn 74 已交付(decompose)
ParseSubgraphPostFn 1 不提供(已回退)

2.2 当前缺口(已闭合)

以下为特性开发前的缺口,当前全部闭合:

  • 插件开发者不能使用 Python 声明 ONNX source 到 GE target 的映射;
  • Python callback 不能直接接收 ONNX 节点值和 callback 期 ge::Operator
  • Python descriptor 不能进入 OpParserFactoryOpRegistry
  • atc、session 和 aclgrphParseONNX 没有统一的 Python ONNX plugin bootstrap;
  • custom_op 与 ONNX plugin 共用路径时,没有统一的一次 import 约束;
  • callback 异常、Operator handle 失效和注册冲突还没有 Python 侧错误语义。

已知设计限制(当前不提供):控制流算子的 Python 子图后处理 callback(见文档范围);TensorDesc 读写。

2.3 当前支持矩阵

能力 状态 不支持时的行为
onnx_plugin(source, domain, opsets, target) 支持 非法字段在注册阶段报错。
parse_node(node, target) 支持 callback 异常使当前节点解析失败。
parse_operator(source, target) 支持 source 只读,setter 拒绝修改。
decompose(source) -> Graph 支持 返回非 Graph 使当前节点解析失败。
同一 descriptor 绑定多个 callback 支持 bridge 按实际存在的 callback 分别注册。
OnnxNode name/origin_type/inputs/outputs 支持 只读,转换失败则节点解析失败。
ONNX FLOAT、INT、STRING 属性及同类型列表 支持 未知或不支持类型明确报错。
Operator.set_attr bool/int/float/str 及同类型列表 支持 非白名单值明确报错。
Operator.get_attr 支持 仅 source 侧使用,值经 _AttrValue 转换。
默认动态输入、动态输出注册 支持 非法 name/count 明确报错。
fixed/optional 端口补充 支持 已有原型端口不应重复注册。
Tensor、Graph、Sparse 属性、ref_attr_name 不支持 callback 前报告不支持。
TensorDesc 修改 不支持 未提供入口。
ParseSubgraphPostFn/ParseSubgraphFuncV2 不提供 评审裁决回退;控制算子由内置 C++ 处理。

3. 目标公开接口

3.1 完整公开接口清单

当前对外接口限定为以下 4 个符号。未出现在本表中的 registry、bootstrap、bridge loader、bridge、C API 和异常转换 helper 都是内部实现,不允许加入 __all__

公开路径 类型 用户获得方式 公开成员 构造限制
ge.onnx_plugin.onnx_plugin 函数 直接导入调用 source/domain/opsets/target 关键字参数 用户直接调用。
ge.onnx_plugin.OnnxNode 只读类 由 GE 作为 callback 参数传入 nameorigin_typeinputsoutputsattrs 不承诺用户直接构造;不提供 setter。
ge.onnx_plugin.OnnxPlugin descriptor 类 onnx_plugin() 返回 parse_node(fn)parse_operator(fn)decompose(fn) 不公开直接构造函数和内部 registry 状态。
ge.graph.Operator callback-bound 类 由 GE 作为 callback 参数传入 nametypeget_attrset_attrregister_inputregister_optional_inputregister_outputregister_dynamic_inputregister_dynamic_output 不允许用户直接构造,callback 结束后失效;source 场景只读。

当前模块导出:

# ge.onnx_plugin.__all__
[
    "OnnxNode",
    "OnnxPlugin",
    "onnx_plugin",
]

# ge.graph.__all__ 增加
["Operator"]

接口边界:

  • 每个 callback 装饰器返回原始 callable,保持装饰器后的函数名称、类型注解和可测试性;
  • 同一 descriptor 可按需绑定多个 callback(如同时绑定 parse_nodedecompose),bridge 只向 C++ 透传实际存在的 callback;
  • OnnxNode.inputs/outputs 使用只读 tuple,attrs 使用只读视图;
  • 使用 Python 内置 TypeErrorValueErrorRuntimeError,不新增公开异常类;
  • ImplyType、registry、bootstrap、bridge、protobuf、C++ Status 和裸 handle 不作为公开接口。

3.2 插件注册和 parse_node

公开写法为「描述对象 + callback 装饰器」:

from ge.graph import Operator
from ge.onnx_plugin import OnnxNode, onnx_plugin


elu = onnx_plugin(
    source="Elu",
    domain="ai.onnx",
    opsets=range(8, 19),
    target="Elu",
)


@elu.parse_node
def parse_elu(node: OnnxNode, target: Operator) -> None:
    alpha = node.attrs.get("alpha", 1.0)
    if not isinstance(alpha, float):
        raise TypeError("Elu alpha must be float")
    target.set_attr("alpha", alpha)

参数含义:

参数 含义 约束
source ONNX 原始 op type。 非空,不包含 domain 和 opset。
domain ONNX domain。 descriptor 中非空;标准 ONNX 使用 ai.onnx
opsets 支持的 ONNX opset 集合。 非空、正整数、升序去重。
target GE 目标 Operator type。 原型必须已经安装并注册。

处理规则:

  • descriptor 将 source、domain 和每个 opset 展开为完整 origin type;
  • FrameworkType 在内部固定为 ONNX,不由用户填写;
  • ImplyType 不对外暴露,内部保持默认值;
  • 同一个完整 origin key 只能由一个 C++ 或 Python plugin 提供;
  • parse_node/parse_operator 返回值必须是 Nonedecompose 返回值必须是 ge.graph.Graph
  • bridge 会在目标 Operator 上自动设置框架内部属性 ATTR_NAME_FRAMEWORK_ORIGINAL_TYPE(用户不需要也不应该手工设置 original type);
  • Python 异常由 bridge 捕获并转换为 parser failure(内部异常标记映射 PARAM_INVALID,其余映射 FAILED);
  • callback 失败后当前 target 不执行 Graph::AddOp

3.3 OnnxNode

当前公开对象:

class OnnxNode:
    name: str
    origin_type: str
    inputs: tuple[str, ...]
    outputs: tuple[str, ...]
    attrs: Mapping[str, object]

字段含义:

字段 来源 说明
name NodeProto::name() ONNX 节点名称。
origin_type parser 构造的完整 origin type 例如 ai.onnx::11::Elu
inputs NodeProto::input() 只读输入 tensor 名称序列,保持顺序和空占位。
outputs NodeProto::output() 只读输出 tensor 名称序列。
attrs AttributeProto 使用 Python 标量或同类型列表表达。

属性转换规则:

ONNX AttributeProto Python OnnxNode.attrs
FLOAT float
INT int
STRING str
FLOATS list[float]
INTS list[int]
STRINGS list[str]

其他规则:

  • bridge 在进入 Python 前确认 protobuf 消息类型为 ge::onnx::NodeProto,并以 py::cast(NodeProto*, reference) 借用构造 native OnnxNode;parser 侧不做独立的值扁平化层。
  • OnnxNode 不保存 protobuf 指针,由 GE 创建,不允许用户直接构造,不提供 setter;
  • 非空 ref_attr_name 作为未解析属性引用拒绝。

3.4 ge.graph.Operator

当前接口:

class Operator:
    @property
    def name(self) -> str: ...

    @property
    def type(self) -> str: ...

    def get_attr(self, name: str) -> object: ...
    def set_attr(self, name: str, value: object) -> None: ...
    def register_input(self, name: str) -> None: ...
    def register_optional_input(self, name: str) -> None: ...
    def register_output(self, name: str) -> None: ...
    def register_dynamic_input(self, name: str, count: int) -> None: ...
    def register_dynamic_output(self, name: str, count: int) -> None: ...

对象约束:

  • Operator 包装 callback 期的 ge::Operator,用户不能直接构造;
  • handle 只在 callback 期间有效,callback 返回或抛出异常后失效,之后调用其方法抛 RuntimeError("Operator is only valid inside parse_node");
  • parse_operator/decompose 的 source 对象为只读(内部 read_only 标记):get_attr/name/type 可用,set_attr 和端口注册抛 RuntimeError("Source Operator is read-only");
  • 对象不能 copy、deepcopy、pickle;
  • 属性支持 bool、int、float、str 和对应同类型 list;
  • 动态端口 count 为非负整数;
  • 固定端口用于确实需要在解析期补充 IR 端口的 target,已有原型端口不应重复注册;
  • 动态 IO target(如 PartitionedCall)必须由 callback 注册端口(如 register_input("x")/register_output("y")),否则 parser 连线阶段按索引名找不到端口而失败(SetOperatorInputs 报 IO name 为空);静态 IR target(如 Elu,原型已声明 INPUT(x)/OUTPUT(y))不需要端口注册。

ge.graph.Nodege.graph.Operator 不相同:

对象 C++ 对象 使用阶段
ge.graph.Node ge::GNode 节点已经加入 Graph 后。
ge.graph.Operator ge::Operator parser callback 期间,尚未加入 Graph。

两者不共享 handle,但可以复用属性转换、TensorDesc 和错误处理代码。

3.5 动态输入输出

Sum 到 AccumulateNV2 的写法如下:

sum_plugin = onnx_plugin(
    source="Sum",
    domain="ai.onnx",
    opsets=range(8, 19),
    target="AccumulateNV2",
)


@sum_plugin.parse_node
def parse_sum(node: OnnxNode, target: Operator) -> None:
    count = len(node.inputs)
    if count == 0:
        raise ValueError("Sum requires at least one input")
    target.register_dynamic_input("x", count)
    target.set_attr("N", count)

required、optional 和 dynamic 是 target 原型的 IR 元数据。parser callback 不重新定义 required/optional 端口,只创建当前节点需要的动态实例。

3.6 parse_operator callback

parse_operator(source, target) 对应 C++ ParseParamsByOperatorFn,source 是 parser 自动映射生成的只读 Operator,target 是已创建的目标 Operator:

operator_plugin = onnx_plugin(
    source="GroupNormRelu",
    domain="ai.onnx",
    opsets=range(8, 17),
    target="GroupNormRelu",
)


@operator_plugin.parse_operator
def parse_group_norm(source: Operator, target: Operator) -> None:
    attrs = source.get_attr("attribute")
    target.set_attr("attribute_json", attrs)

source 属性的读取约定(与 C++ by-operator 插件一致):parser 的 Message2Operator::ParseOperatorAttrs 把 ONNX 节点的 repeated attribute 整体序列化为一个 JSON 字符串,存入 source Operator 的 attribute 键;不会按属性名逐个展开。Python callback 需要 json.loads(source.get_attr("attribute")) 后按 name 字段查找。属性值通过 get_attr_AttrValue/C API 转换为 Python 值。

同一 origin 下 parser 优先调用 ParseParamsByOperatorFn(GE 既有规则,Python 透传保留该优先级)。

3.7 decompose callback

decompose(source) -> Graph 对应 C++ ParseOpToGraphFn,把一个 ONNX 节点展开为多个 GE 算子。source 是 parse_node 产出的同一个目标 Operator(只读视图);返回值必须是 ge.graph.Graph,由 bridge Graph::CopyFrom 后交给现有 ParserUtils::ExpandOneToManyGraph 展开入图:

from ge.es import GraphBuilder
from ge.es.math import Mul
from ge.es.nn import Threshold


@thresholded_relu.decompose
def decompose_thresholded_relu(source):
    alpha = float(source.get_attr("alpha"))
    builder = GraphBuilder("thresholded_relu_decomposition")
    x = builder.create_input(0)
    mask = Threshold(x, threshold=alpha)
    output = Mul(x, mask)
    return builder.build_and_reset([output])

约束:

  • builder 在 callback 内部创建,不作为参数传入(历史草案中的 (source, builder) 形式未采用);
  • decompose 依赖 parse_node 的产出:source.get_attr 读到的属性、source 上的端口注册均来自同一 descriptor 的 parse_node callback(对动态 IO target 是必需的);
  • 返回非 Graph(含 None)使当前节点解析失败(PARAM_INVALID);
  • 构图优先使用 ge.es.math/ge.es.nn 等 ES 生成算子包,不提供字符串型万能 graph.op 工厂。

3.8 后续 callback 扩展

ParseSubgraphPostFn/ParseSubgraphFuncV2 经评审裁决不提供 Python callback(控制算子内置,无需外部自定义;相关代码已在 PR #4583 回退)。后续如需扩展新 callback(如 TensorDesc 修改),沿用 descriptor + 独立窄入口的模式另行评审。

3.9 接口评审和资料交付

所有公开接口已在编码前完成接口评审。已交付资料:

  • 入门 Sample:examples/onnx_plugin/(PyTorch 自定义算子导出 ONNX → Python 插件 → atc 编译 → ACL 执行 → 结果比对,已在 Ascend910_9362 端到端验证);
  • .pyi/类型提示:公开类型、方法签名和只读属性与接口评审结果一致;
  • API Reference、用户指南和 C++ 迁移指南为后续文档交付项。

4. 用户使用方式

4.1 Elu:读取可选属性

@elu.parse_node
def parse_elu(node: OnnxNode, target: Operator) -> None:
    alpha = node.attrs.get("alpha", 1.0)
    if not isinstance(alpha, float):
        raise TypeError("Elu alpha must be float")
    target.set_attr("alpha", alpha)

执行过程为:读取 alpha → 属性不存在时使用默认值 → 属性不是 FLOAT 时明确失败 → 写入目标 Operator。

生产环境已存在 Elu C++ plugin,为避免注册冲突,parser ST 使用测试专用 origin type 执行等价逻辑。

4.2 Sum:注册动态输入

@sum_plugin.parse_node
def parse_sum(node: OnnxNode, target: Operator) -> None:
    count = len(node.inputs)
    if count == 0:
        raise ValueError("Sum requires at least one input")
    target.register_dynamic_input("x", count)
    target.set_attr("N", count)

这条用例验证:ONNX inputs 转换、动态输入数量、Operator 属性设置、零输入异常,以及不引入中间端口对象。

4.3 完整 Sample:examples/onnx_plugin/

仓库已交付可运行的用户级样例(非测试夹具),目录结构:

examples/onnx_plugin/
├── plugin/thresholded_relu_plugin.py  # GE ONNX Python 插件
├── export_onnx.py                     # PyTorch 自定义算子 -> ONNX
├── run_model.py                       # ACL 加载并执行 OM
└── run.sh                             # 导出、编译、执行一键脚本

运行方式(已验证环境:Ascend910_9362 / Atlas A3,CANN 9.2.0):

source /path/to/cann/set_env.sh
SOC_VERSION=Ascend910_9362 ./run.sh

样例覆盖:descriptor 注册、parse_node 属性中转与端口注册(动态 target PartitionedCall)、decompose 用 ES GraphBuilder 构建 Threshold + Mul 替代图、atc 编译、ACL 执行、输出与 PyTorch 参考比对。parse_nodedecompose 的配合关系(属性中转 + 端口注册)见该目录 README。

4.4 接口使用 Sample:属性解析 ST

ST 从内存插件和 ONNX 模型开始,经过真实 loader、bridge 和 parser,最后检查解析后的 GE Graph:

from ge.graph import Operator
from ge.onnx_plugin import OnnxNode, onnx_plugin


test_elu = onnx_plugin(
    source="GePythonPluginTestElu",
    domain="ge.test",
    opsets=(1,),
    target="GePythonPluginTestOp",
)


@test_elu.parse_node
def parse_test_elu(node: OnnxNode, target: Operator) -> None:
    target.set_attr("alpha", node.attrs.get("alpha", 1.0))
export ASCEND_CUSTOM_OPP_PATH="$(pwd)/plugin"
atc --framework=5 --model=model.onnx --output=model --soc_version=<SOC>

ST 检查:模块只 import 一次、origin key 生成正确、parser creator 可创建、目标节点 type 正确、属性类型和值正确、图连接正确、callback 后 Operator 引用失效。

4.5 接口使用 Sample:动态输入 ST

test_sum = onnx_plugin(
    source="GePythonPluginTestSum",
    domain="ge.test",
    opsets=(1,),
    target="GePythonPluginTestDynamicOp",
)


@test_sum.parse_node
def parse_test_sum(node: OnnxNode, target: Operator) -> None:
    count = len(node.inputs)
    if count == 0:
        raise ValueError("GePythonPluginTestSum requires at least one input")
    target.register_dynamic_input("x", count)
    target.set_attr("N", count)
输入数 预期结果
0 parser 失败,错误包含 callback、origin type 和 "requires at least one input"。
1 解析成功,动态输入 x 数量为 1,属性 N=1
3 解析成功,动态输入 x 数量为 3,属性 N=3

4.6 测试泛化矩阵

维度 基础值 泛化值 主要检查
插件载体 单个 plugin.py package、多个文件、同文件声明 custom_op 导入次数、排序和 registry 分流。
domain ge.test 空模型 domain、其他 custom domain origin type 构造和 opset 匹配。
opset 单个 1 多个、不连续、重叠、非法值 descriptor 展开和冲突诊断。
属性 FLOAT alpha 缺省、INT、STRING、列表、不支持类型 转换、默认值和负向错误。
输入数量 1 0、3、较大数量 动态端口和边界值。
callback 类型 parse_node parse_operatordecompose、多 callback 同 descriptor 分发正确、透传注册、by-operator 优先级。
callback 返回 NoneGraph for decompose) 非 None/非 Graph、抛出异常 Status 转换和 Graph 不加节点。
target 形态 静态 IR(Elu) 动态 IO(PartitionedCall) 端口注册必要性、连线成败。
source 只读 parse_operator 内 get_attr source 上 set_attr/注册端口 read-only 拒绝并报错。
注册关系 无冲突 C++/Python、Python/Python C++ 优先和预检查。
生命周期 callback 内访问 callback 后访问、保存到全局 handle 失效。
无插件回归 配置测试插件 环境变量为空、路径无 Python 文件 原 C++ parser 行为不变。
端到端 example 全流程 atc 编译 + ACL 执行 + 结果比对 真实工具链可用性。

5. C++ 能力如何体现在 Python 中

GE/metadef C++ 能力 Python 表达 bridge/框架动作 交付阶段
REGISTER_CUSTOM_OPOpRegistrationData onnx_plugin(...) descriptor 转换为 registration data。 PR1/PR2
FrameworkType(ONNX) 不对外暴露 bootstrap 内部固定。 PR1/PR2
OriginOpType source、domain、opsets 展开完整 origin type 集合。 PR1/PR2
ParseParamsFn @plugin.parse_node C++ wrapper 调用 Python。 PR2
NodeProto OnnxNode native pybind 借用构造(C++ bridge 侧完成类型确认)。 PR1/PR2
AttributeProto Python 标量/list 类型白名单转换。 PR1/PR2
Operator::GetAttr/SetAttr get_attr/set_attr callback-bound handle 调用现有接口。 PR1~PR3
DynamicInputRegister register_dynamic_input 固定默认 is_push_back=true PR1
DynamicOutputRegister register_dynamic_output 固定默认 is_push_back=true PR1
ParseParamsByOperatorFn @plugin.parse_operator 写入 ByOperator map;source 以只读标记创建。 PR3
ParseOpToGraphFn @plugin.decompose 写入 graph callback map;返回 Graph 经 CopyFrom 交接。 PR4
ParseSubgraphFuncV2 不提供 评审裁决回退;控制算子由内置 C++ 处理。 已回退

不向 Python 用户暴露:

  • google::protobuf::Message *
  • ge::Operator *
  • C++ Status
  • OpRegistrationData
  • OpParserFactory
  • OpRegistry
  • GIL 和 Python C API。

6. 总体实现方案

6.1 各层职责

层次 主要职责 不承担的职责
Python 公开接口层 提供 onnx_pluginOnnxNodeOnnxPluginge.graph.Operator 不感知 C++ registry、protobuf 和 Status。
Python descriptor registry 收集、校验、冻结 Python descriptors。 不直接修改 C++ parser map。
Python plugin loader 按 canonical path 只 import 一次模块。 不处理 creator/callback 冲突。
bridge NodeProto 构造 确认消息类型为 ge::onnx::NodeProto 并借用构造 native OnnxNode 不调用用户 Python 业务代码。
独立 C++ bridge SO 管理 GIL、Operator handle、Python callback 和异常转换。 不编入 graph_metadef,不实现 ONNX 业务规则。
薄 bridge loader 预检查 creator/callback map 冲突,协调 Finalize/Register 提交。 不新增独立 coordinator 子系统或状态机。
现有 parser/registry 创建 adapter、查找 callback、执行 Graph::AddOp。 不增加 Python 专用查询旁路。

模块之间的调用关系见「总体概述」的模块分层与调用关系图。

6.2 用户代码到 parser 的主链

注册与解析两阶段的端到端链路图见「总体概述」,此处补充关键步骤语义:

ASCEND_CUSTOM_OPP_PATH/plugin.py
  -> plugin_loader 按 canonical path 导入一次
  -> Python registry 冻结 descriptors
  -> bridge loader 预检查并提交
  -> OpRegistrationTbe::Finalize
  -> OpParserFactory target creator
  -> OpRegistry::Register callback maps
  -> OnnxCustomParserAdapter 调用 ParseParamsFn
  -> bridge 确认 NodeProto 类型并借用构造 OnnxNode
  -> bridge 创建 callback-bound Operator
  -> parse_node(node, target)
  -> callback 成功后 Graph::AddOp

6.3 初始化接入路径

场景 真实入口 Python 接入方式
aclgrphParseONNX / IR build onnx_parser.cc PrepareBeforeParse AclParserInitialize 成功后 LoadOnnxPythonPluginBridge()
atc 及其他 ModelParserFactory 直连路径 OnnxModelParser::ModelParseToGraph 入口 同一 LoadOnnxPythonPluginBridge()(幂等;ASCEND_CUSTOM_OPP_PATH 未设置时零行为变化)。该入口为实测修复补齐——atc 的 ParseGraph 不经过 PrepareBeforeParse
session ge_api_v2.cc / ge_ir_build.cc 注册后由 bridge shutdown(UnloadOnnxPythonPluginBridge)与 GePythonRuntimeManager 统一收尾。

bridge loader 按运行时 Python ABI(cpXY)与平台在 python_onnx_plugin_artifacts 中选择兼容 artifact 并 dlopen bridge SO;bridge SO 句柄保持存活,避免 OpRegistry 中已注册回调悬空。Initialize()initialized_ 幂等,重复加载安全。

6.4 custom_op 与 ONNX plugin 共存

两类能力共用 Python interpreter、ASCEND_CUSTOM_OPP_PATH、路径扫描规则和 canonical-path module cache,但不共用 C++ registry:

ge.custom_op
  -> PythonCustomOpBridge
  -> CustomOpRegistry / OpLibRegistry

ONNX plugin
  -> bridge loader
  -> OpParserFactory / OpRegistry

同一个 Python 文件可以声明两类 descriptor,但模块只能 import 一次,两类 descriptor 分别提交各自 registry。

6.5 注册冲突处理

注册前检查:origin 是否已有映射(GetOmTypeByOriOpType)、target parser creator 是否已存在、ParseParamsFn/ByOperator map,以及 Python descriptors 之间的 origin type 重叠。

注册冲突策略:

  • C++/Python 任一冲突时保留 C++,拒绝 Python;
  • Python/Python 冲突使初始化失败;
  • 已有 target creator 时拒绝 Python target;
  • 全部 descriptor 预检查通过后统一提交,不允许部分生效。

6.6 callback 调用和生命周期

规则:

  • callback 期间 bridge 持有目标 Operator;
  • Python 不能获得裸指针;
  • callback 结束立即失效,保存 Python 引用不会延长 C++ 对象生命周期;
  • 所有公开方法先检查有效性;
  • callback 失败后当前 Operator 不加入 Graph。

7. 子模块修改关系

子模块 主要修改 目的
api/python/ge/ge/onnx_plugin/ 新增 descriptor、公开入口、内部 registry/bootstrap。 提供 Python ONNX plugin 用户界面。
api/python/ge/ge/graph/operator.py 新增 callback-bound Operator。 包装 callback 期 ge::Operator
api/python/ge/ge/graph/__init__.py 导出 Operator。 形成稳定公开入口。
api/python/ge/ge/_internal/plugin_loader.py 增加 canonical-path module cache。 同一文件只 import 一次。
bridge NodeProto 借用构造 确认 protobuf 类型并以 reference 借用构造 OnnxNode。 隔离 protobuf ABI。
薄 bridge loader creator/callback 预检查和 Finalize/Register 提交。 闭合真实注册链。
parser/atc/session 初始化入口 接入统一 bootstrap。 覆盖所有解析入口。
独立 bridge SO callback wrapper、GIL、Operator handle、异常转换。 连接 Python 与现有 parser callback。
build/package 编译和安装 bridge 与 Python package。 保证组件版本匹配。
tests Python UT、bridge UT、parser ST。 验证用户接口和真实链路。

8. 非功能、错误和兼容性设计

8.1 可维护性和可测试性

  • 公开 API 只导出 onnx_pluginOnnxNodeOnnxPluginge.graph.Operator
  • registry、bootstrap、bridge loader 和 bridge helper 保持内部可见;
  • 注册字段只声明一次;
  • 新 callback 使用独立窄入口,不修改既有 callback 签名;
  • descriptor、OnnxNode 和参数校验可以独立 Python UT;
  • protobuf 提取、GIL、Operator handle 和异常可以独立 C++ UT。

8.2 可靠性和并发

  • descriptor 在写入任何 C++ registry 前完整校验;
  • 注册完成后 registry 只读;
  • bridge 不新建线程,callback 在 parser 调用线程内获取 GIL;
  • 同一 Operator 不允许并发 setter;
  • callback 异常不跨越 C++ ABI;
  • 失败后不继续提交剩余 descriptor。

8.3 错误处理

错误 行为
非法 descriptor 字段 Python 注册阶段抛 ValueErrorTypeError
Python/Python origin 重叠 初始化失败,报告两个模块。
C++/Python creator 或 callback 冲突 保留 C++,拒绝 Python。
插件模块导入失败 初始化失败,不提交不完整集合。
NodeProto 属性类型不支持 当前节点解析失败,报告属性名和类型。
callback 返回非 None 当前节点解析失败。
callback 抛出异常 bridge 记录模块、origin type 和 traceback 摘要,返回失败。
Operator handle 已失效 Python 抛 RuntimeError

8.4 安全检查

  • 不暴露裸指针、handle 数值或 Python 对象地址;
  • 不递归扫描插件目录;
  • 不执行模型属性中的代码;
  • 日志不打印 Tensor 内容或完整模型数据;
  • traceback 过滤内部地址和敏感路径;
  • 所有跨 ABI 异常在 bridge 内转换。

8.5 兼容性

  • 未配置 Python plugin 时不导入用户模块,现有 C++ parser 行为不变;
  • 不修改现有 C++ plugin API、registry key 和 callback 签名;
  • 不修改 graph C++ ABI、AscendIR 和 OM 格式;
  • 不新增 Python 专用环境变量;
  • atc、session 和 online 入口分别验证,不能假设共用同一初始化函数。

8.6 特性交叉分析

场景 适用性 分析说明
静态 Shape 适用 parser 生成标准 Operator,后续静态编译、内存和执行流程不变。
动态 Shape 适用 不修改 TensorDesc,动态维按现有流程进入推导。
动态 Shape 静态子图 适用 特性发生在 parser 前端,不修改图拆分或 DavinciModel。
离线场景 适用 atc 是主要入口,OM 格式不变。
在线场景 适用 仅在线入口调用 ONNX parser 时生效。

8.7 性能

  • 插件扫描只在初始化阶段执行,不在每个节点重复扫描;
  • 未配置 Python plugin 时,不因直接 ONNX Parser API 新增 Python runtime 初始化;
  • 未配置、单插件、多插件的初始化耗时和 Host 内存观测为可选测试项(无既定阈值门禁);
  • Python callback 不进入模型执行路径,不影响 Device 性能或 OM 格式。

9. 测试策略

9.1 测试边界

层次 输入 输出检查
Python UT descriptor、OnnxNode、Operator 参数 冻结 descriptor、异常类型和错误信息。
bridge UT(NodeProto 构造) NodeProto OnnxNode 字段、属性类型和失败状态。
bridge UT 扁平 Node 值、Operator、Python callback C++ Operator 状态、Status、handle 失效。
parser ST ONNX 模型和 Python plugin 最终 Graph 中 Operator 属性、端口和失败诊断。

9.2 测试用例

测试类别 关键测试项 用例类型
功能 descriptor 合法字段 Python UT
功能 Elu alpha 默认值和覆盖 parser ST
功能 Sum 动态输入(0/1/多个) bridge UT + parser ST
功能 NodeProto 标量和列表属性 bridge UT
功能 Operator 属性和端口 Python UT + bridge UT
功能 parse_operator source attrs JSON 约定(json.loads(source.get_attr("attribute")) bridge UT + parser ST
功能 parse_operator source 只读(set_attr/注册端口拒绝) Python UT + bridge UT
功能 decompose 返回 Graph、ES GraphBuilder 构图、常量节点 bridge UT + parser ST
功能 decompose 依赖 parse_node 属性中转与端口注册(动态 target) parser ST + 端到端
功能 同一 descriptor 多 callback 绑定与透传注册 Python UT + bridge UT
功能 静态 IR target(免端口注册)与动态 IO target(必须注册) parser ST
异常 Tensor/Graph/Sparse、未知枚举 bridge UT
异常 callback 异常或返回非 None/非 Graph bridge UT
异常 atc 端 callback 异常可定位(plog 含 Python traceback) 端到端
生命周期 callback 后保存 Operator/Graph 引用 bridge UT
冲突 Python/Python opset 重叠、C++/Python 同 key Python UT + parser ST
loader custom_op/ONNX 同文件一次 import parser/runtime ST
兼容性 无 Python plugin parser ST
端到端 examples/onnx_plugin 全流程(导出→atc→ACL→比对) E2E

已验证环境参考:Ascend910_9362(Atlas A3)、CANN 9.2.0(weekly.20260826)、Python 3.11(cp311 artifact)。soc_version 需使用完整芯片名(如 Ascend910_9362,短名会被 rtSetSocVersion 拒绝)。

9.3 测试框架

复用 pytest、现有 graph Python UT、parser gtest 和 parser ST,不新增测试框架。测试 plugin 放在测试资源目录,通过测试专用 ASCEND_CUSTOM_OPP_PATH 加载。

10. 开发计划和门禁

10.1 交付记录(历史)

特性通过 4 个 PR 交付,全部完成:

PR 目标任务 状态
PR1:MVP 参数解析主链 Python API、插件发现/注册、独立 Bridge、parse_node、属性、默认动态端口、直接 Parser API、打包和资料 已合入 develop
PR2:bridge/parser 接线 独立 bridge SO、native OnnxNode 借用构造、ParseParamsFn 真实调用、注册协调器、parser UT/ST 已合入 develop
PR3:Operator 参数解析扩展 ParseParamsByOperatorFn 对应的 parse_operatorOperator.get_attr、source 只读语义 已合入 develop
PR4:一对多构图 ParseOpToGraphFn 对应的 decompose(ES 构图)、atc 路径 bridge 加载修复、端到端 example PR #4583(open,9e7628ea7,CI 通过)

说明:原计划中 PR4 同时包含 ParseSubgraphPostFn/V2 接入,经 MDE 评审裁决为不需要(控制算子内置、无用户场景),相关代码已回退;TensorDesc 完整能力维持后置。PR1 工作包 A~D(接口/registry/loader、Bridge SO、Parser 注册、打包回归)已全部完成。

10.2 编码前门禁(历史,已全部通过)

决策项 状态
公开接口和资料 已评审
bridge SO、ABI 和安装归属 已冻结并交付(独立 SO,cpXY artifact 按 ABI 选择)
atc/session 初始化锚点 已验证(含 atc 直连路径修复)
GIL 和多模型并发 已评审
性能阈值 无门禁要求

11. 验收标准

  1. 插件开发者可以使用 onnx_plugin(...)parse_node 编写 Elu 等价插件。
  2. 插件开发者可以使用 parse_operator(source, target) 编写 by-operator 插件,source attrs 按 attribute JSON 约定读取,source 只读。
  3. 插件开发者可以使用 decompose(source) 用 ES GraphBuilder 完成一对多展开,返回 ge.graph.Graph
  4. Python plugin 可以从 ASCEND_CUSTOM_OPP_PATH 稳定发现,同一 canonical 物理模块只 import 一次。
  5. atc、session 和 aclgrphParseONNX 都能在首次 parser 查询前完成注册(atc 直连 ModelParserFactory 路径同样加载 bridge)。
  6. OnnxNode 基础字段和常用属性可以正确转换,未支持类型明确失败。
  7. callback 可以通过 ge.graph.Operator 设置属性并注册动态端口;动态 IO target 不注册端口时连线明确失败。
  8. callback 异常后当前 Operator 不入图,保存的 Operator 引用不可继续使用;atc 失败信息可定位(plog 含 Python 异常)。
  9. C++/Python 冲突均拒绝 Python 且有明确诊断;同一 origin 下 by-operator callback 优先(GE 既有规则)。
  10. custom_op 与 ONNX plugin 可以一次 import 后分别进入各自 registry。
  11. 未配置 Python plugin 时,现有 C++ ONNX parser 行为不变。
  12. 不修改 graph C++ ABI、AscendIR、OM、compiler 和 runtime;不提供控制流算子的 Python 子图 callback(已裁决回退)。
  13. Python UT、bridge UT、parser ST 全部通过;examples/onnx_plugin/ 端到端流程(导出→atc→ACL 执行→比对)在真实设备验证通过(Ascend910_9362)。
  14. 全部公开接口、__all__ 和类型声明完成接口评审。

附录 A:源码证据索引

主题 源码位置
ACL parser 初始化 / bridge 加载 parser/parser/onnx/onnx_parser.ccPrepareBeforeParseModelParseToGraph
ONNX parser 入口 parser/parser/onnx/onnx_parser.cc
parser creator 注册 parser/parser/common/op_registration_tbe.cc
OpRegistry::Register graph_metadef/register/register.cpp
bridge SO(注册协调/回调/GIL/异常) parser/parser/onnx/python_onnx_plugin_bridge/onnx_plugin_bridge.cc
bridge loader(cpXY artifact 发现/dlopen) parser/parser/onnx/python_onnx_plugin_bridge/onnx_plugin_bridge_loader.cc
Python loader api/python/ge/ge/_internal/plugin_loader.py
ONNX Plugin 公开接口 api/python/ge/ge/onnx_plugin/
Operator 包装 api/python/ge/ge/graph/operator.py
端到端样例 examples/onnx_plugin/

附录 B:接口检查结果

检查项 是否涉及 结论
公开接口评审 onnx_pluginOnnxNodeOnnxPluginOperator 及其全部公开成员已评审。
接口资料 API Reference、用户指南、迁移指南、类型提示和 Elu/Sum/ST Sample。
现有接口行为 内部 parser/session 初始化增加 bootstrap;无 Python plugin 时行为不变。
调用时序 注册早于首次 factory/registry 查询,Operator 只在 callback 期有效。
非法调用报错 descriptor、属性、handle 和 callback 都有明确错误。
独立测试 Python UT、bridge UT、parser ST 和接口使用 Sample。
likedislike