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


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


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


ONNX Plugin Python 化 SRS
阅读说明
文档目的
本文说明如何让插件开发者使用 Python 编写 ONNX 到 GE Operator 的转换逻辑,给出公开接口、用户写法、模块划分和验收要求。文档描述当前代码仓库已交付的现状(截至 PR #4583,commit 9e7628ea7),作为测试设计的输入。
当前 C++ 参考实现(threshold_relu_onnx_plugin.cpp)完成了三件事:
- 注册 ONNX 原始算子到 GE 目标算子:
ThresholdedRelu注册为 GE 的PartitionedCall。 - 解析 ONNX
NodeProto(一对一):
读取node.name()和alpha属性,设置到ge::Operator,并注册动态输入输出。 - 构造 GE 子图(一对多):
用Data -> Identity -> Threshold -> Mul表达ThresholdedRelu,再设置Graph输入输出。
文档范围
当前已交付(PR1~PR4 合入内容 + PR #4583):
onnx_plugin(source, domain, opsets, target)插件描述;ParseParamsFn对应的parse_nodecallback;ParseParamsByOperatorFn对应的parse_operatorcallback;ParseOpToGraphFn对应的decomposecallback(复用 ESGraphBuilder构图);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_OP和OpRegistrationData描述映射; OpRegistrationTbe::Finalize和OpParserFactory可以建立 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 不能进入
OpParserFactory和OpRegistry; - 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 参数传入 | name、origin_type、inputs、outputs、attrs |
不承诺用户直接构造;不提供 setter。 |
ge.onnx_plugin.OnnxPlugin |
descriptor 类 | 由 onnx_plugin() 返回 |
parse_node(fn)、parse_operator(fn)、decompose(fn) |
不公开直接构造函数和内部 registry 状态。 |
ge.graph.Operator |
callback-bound 类 | 由 GE 作为 callback 参数传入 | name、type、get_attr、set_attr、register_input、register_optional_input、register_output、register_dynamic_input、register_dynamic_output |
不允许用户直接构造,callback 结束后失效;source 场景只读。 |
当前模块导出:
# ge.onnx_plugin.__all__
[
"OnnxNode",
"OnnxPlugin",
"onnx_plugin",
]
# ge.graph.__all__ 增加
["Operator"]
接口边界:
- 每个 callback 装饰器返回原始 callable,保持装饰器后的函数名称、类型注解和可测试性;
- 同一 descriptor 可按需绑定多个 callback(如同时绑定
parse_node和decompose),bridge 只向 C++ 透传实际存在的 callback; OnnxNode.inputs/outputs使用只读 tuple,attrs使用只读视图;- 使用 Python 内置
TypeError、ValueError、RuntimeError,不新增公开异常类; 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返回值必须是None;decompose返回值必须是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)借用构造 nativeOnnxNode;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.Node 与 ge.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_nodecallback(对动态 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_node 与 decompose 的配合关系(属性中转 + 端口注册)见该目录 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_operator、decompose、多 callback 同 descriptor |
分发正确、透传注册、by-operator 优先级。 |
| callback 返回 | None(Graph 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_OP、OpRegistrationData |
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_plugin、OnnxNode、OnnxPlugin 和 ge.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_plugin、OnnxNode、OnnxPlugin和ge.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 注册阶段抛 ValueError 或 TypeError。 |
| 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_operator、Operator.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. 验收标准
- 插件开发者可以使用
onnx_plugin(...)和parse_node编写 Elu 等价插件。 - 插件开发者可以使用
parse_operator(source, target)编写 by-operator 插件,source attrs 按attributeJSON 约定读取,source 只读。 - 插件开发者可以使用
decompose(source)用 ESGraphBuilder完成一对多展开,返回ge.graph.Graph。 - Python plugin 可以从
ASCEND_CUSTOM_OPP_PATH稳定发现,同一 canonical 物理模块只 import 一次。 - atc、session 和
aclgrphParseONNX都能在首次 parser 查询前完成注册(atc 直连ModelParserFactory路径同样加载 bridge)。 OnnxNode基础字段和常用属性可以正确转换,未支持类型明确失败。- callback 可以通过
ge.graph.Operator设置属性并注册动态端口;动态 IO target 不注册端口时连线明确失败。 - callback 异常后当前 Operator 不入图,保存的 Operator 引用不可继续使用;atc 失败信息可定位(plog 含 Python 异常)。
- C++/Python 冲突均拒绝 Python 且有明确诊断;同一 origin 下 by-operator callback 优先(GE 既有规则)。
- custom_op 与 ONNX plugin 可以一次 import 后分别进入各自 registry。
- 未配置 Python plugin 时,现有 C++ ONNX parser 行为不变。
- 不修改 graph C++ ABI、AscendIR、OM、compiler 和 runtime;不提供控制流算子的 Python 子图 callback(已裁决回退)。
- Python UT、bridge UT、parser ST 全部通过;
examples/onnx_plugin/端到端流程(导出→atc→ACL 执行→比对)在真实设备验证通过(Ascend910_9362)。 - 全部公开接口、
__all__和类型声明完成接口评审。
附录 A:源码证据索引
| 主题 | 源码位置 |
|---|---|
| ACL parser 初始化 / bridge 加载 | parser/parser/onnx/onnx_parser.cc(PrepareBeforeParse、ModelParseToGraph) |
| 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_plugin、OnnxNode、OnnxPlugin、Operator 及其全部公开成员已评审。 |
| 接口资料 | 是 | 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。 |


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。
如何测试
测试结果:所有本地链接有效;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 的远端文件范围验证通过后关闭。