合并受阻
GE Python Fusion Pass能力补齐
阅读说明
本文中的“图”是计算图:算子是图中的节点,Tensor在节点之间的流向是边。“子图融合”是把一段已有计算结构替换成更高效的新结构。
下列名称在全文中使用同一含义:
| 名称 | 含义 |
|---|---|
PatternFusionPass |
按指定图结构匹配一段子图,然后决定是否替换。 |
DecomposePass |
匹配某一类算子节点,然后把该节点拆成新的计算结构。 |
meet_requirements() |
条件判断方法。图结构匹配成功后,用它判断本次是否真正执行替换。 |
replacement() |
替换结构生成方法。它负责生成并返回一张“替换图”,由GE将这张图接入原图。 |
| 替换图 | 用于替换原有节点或子图的新计算图,代码中通常命名为replacement_graph。 |
MatchResult |
一次模式匹配的结果,可用于获取本次命中的节点和已记录的Tensor。 |
SubgraphBoundary |
子图边界,记录原子图从哪里接收输入、从哪里向外输出。 |
PassContext |
当前Pass的编译期执行上下文,可读取Pass名称、编译选项和错误信息,也可设置错误信息。 |
| Shape、DataType、Format | Tensor的形状、数据类型和数据排布格式。本文统称为“Tensor描述信息”。 |
1. 当前Python Fusion Pass基础能力
Python已经具备以下基础能力:
FusionBasePass.run(graph, context)可以直接访问和修改整张图。PatternFusionPass可以按模式匹配子图,再调用条件判断方法和替换结构生成方法。DecomposePass可以按算子类型匹配单个节点,再判断是否分解并生成替换图。Pattern和MatchResult可以描述待匹配结构及本次匹配结果。SubgraphBoundary、SubgraphInput和SubgraphOutput可以描述待替换子图的输入、输出边界。SubgraphRewriter可以按照子图边界将替换图接入原图。PassContext可以在FusionBasePass.run(graph, context)中读取当前Pass名称、编译选项和错误信息。GraphBuilder可以构造模式图和替换图;简单场景也可以直接用Python算子表达式描述图结构。
当前主要缺少以下能力:
PatternFusionPass和DecomposePass的条件判断方法、替换结构生成方法无法获取PassContext。- 在全图Shape推导已经结束后,新生成的替换图缺少一个根据原图输入补齐Tensor描述信息的入口。
FusionBasePass直接改图时,缺少Python侧的融合可行性检查和融合结果上报接口。
2. 目标公开接口
2.1 PatternFusionPass获取编译期上下文
原有写法保持可用:
def meet_requirements(self, match_result) -> bool: ...
def replacement(self, match_result): ...
需要读取编译期上下文时,可以在参数末尾增加context:
def meet_requirements(self, match_result, context: PassContext) -> bool: ...
def replacement(self, match_result, context: PassContext): ...
Python还支持一种简化构图写法:用户不手动创建完整的Graph,而是通过inputs和算子表达式描述替换结构:
def replacement(self, inputs): ...
def replacement(self, inputs, match_result): ...
def replacement(self, inputs, context: PassContext): ...
def replacement(self, inputs, match_result, context: PassContext): ...
这里的inputs是PatternInputs对象,表示替换图的外部输入占位符集合:
inputs[0]表示替换图的第0个外部输入,inputs[:3]表示连续取得前3个外部输入。- 每个输入是用于构图的
TensorHolder,不是运行时Tensor数据,也不是原图中的实际Node对象。 - GE执行替换时,会按输入索引把原图外部输入连接到替换图对应的输入占位符。
- 输入数量由用户访问了哪些索引决定,因此不能直接遍历
inputs。
第三个参数存在两种历史含义,按参数名区分:
replacement(self, inputs, context)中,第三个参数严格命名为context,GE传入PassContext。replacement(self, inputs, details)中,第三个参数不是context,GE按原有行为传入MatchResult。- 四参数写法的最后一个参数必须命名为
context。 - 不支持的参数组合会在用户类定义时抛出
TypeError,不等到执行图匹配时才报错。
2.2 DecomposePass获取编译期上下文
原有写法保持可用:
def meet_requirements(self, node) -> bool: ...
def replacement(self, node): ...
需要编译期上下文时,可以在参数末尾增加context:
def meet_requirements(self, node, context: PassContext) -> bool: ...
def replacement(self, node, context: PassContext): ...
这里的node是本次被匹配、准备分解的原图节点。
2.3 补齐替换图的Tensor描述信息
公开接口为:
infer_shape(
replacement: Graph,
source: MatchResult | Node | SubgraphBoundary,
) -> None
参数含义:
replacement是用户刚构造的替换图。source提供原图的输入边界信息,使替换图知道每个输入应继承什么Shape、DataType和Format。
不同Pass使用不同的原图信息:
| 使用场景 | source应传入的对象 |
对象提供的信息 |
|---|---|---|
PatternFusionPass |
本次匹配的MatchResult |
被匹配子图的输入、输出边界。 |
DecomposePass |
本次匹配的Node |
被分解节点的输入、输出边界。 |
FusionBasePass直接按边界改图 |
SubgraphBoundary |
用户明确构造的子图输入、输出边界。 |
接口执行两步操作:
- 根据原图输入更新替换图中各输入占位节点的Tensor描述信息。
- 在替换图内部执行一次完整推导,更新后续算子的Shape、DataType和Format。
成功时直接修改传入的替换图并返回None。参数类型不正确时抛出TypeError;替换图为空、内部对象引用已经失效或底层推导失败时抛出RuntimeError。
是否需要调用取决于Pass阶段:
BEFORE_INFER_SHAPE阶段:替换图接入原图后,GE后续通常还会对整张图统一推导,因此一般不需要主动调用。AFTER_INFER_SHAPE及之后:全图推导已经完成,replacement()和SubgraphRewriter.replace()只负责替换图结构,不会自动再次推导。新引入算子的Tensor描述信息不完整时,需要在replacement()方法把替换图交给GE之前调用infer_shape()。- 如果用户构图时已经完整设置Tensor描述信息,或者没有引入需要推导的新算子,可以不调用。
2.4 融合可行性检查
公开接口为:
@dataclass(frozen=True)
class FuseCheckResult:
ok: bool
reason: str = ""
def can_fuse(nodes: Iterable[Node]) -> FuseCheckResult: ...
nodes是准备合并或共同改写的一组原图节点,可以传列表、元组或生成器等可迭代集合。
接口检查:
- 节点集合不是空集合。
- 所有节点有效并属于同一张图。
- 节点的执行流标签和核资源配置不存在冲突。
- 融合后不会形成数据环或控制环。
ok=True表示允许继续融合。ok=False表示本次业务上不适合融合,reason说明原因,此时不抛异常,Pass可以跳过本次候选。集合中包含非Node对象时抛出TypeError;节点的内部引用已经失效时抛出RuntimeError。
2.5 融合结果上报
公开接口为:
def report_fuse(
nodes_before: Iterable[Node],
nodes_after: Iterable[Node],
context: PassContext,
) -> None: ...
nodes_before是改图前被融合、替换或删除的原节点。nodes_after是改图后新生成或保留的结果节点;只删除旧节点时传空集合。context用于记录当前由哪个Pass完成了这次改图。
直接改图时必须遵循以下顺序:
检查是否允许融合
-> 修改节点、边或属性
-> 上报改图前后的节点关系
-> 删除不再需要的旧节点
上报必须发生在删除旧节点之前,因为底层还需要读取旧节点信息。
2.6 自动完成检查、替换和上报
原有接口保持不变:
SubgraphRewriter.replace(boundary, replacement_graph) -> int
新增带context的调用方式:
SubgraphRewriter.replace(
boundary,
replacement_graph,
context=context,
) -> None
boundary说明原子图的输入、输出连接位置。replacement_graph是需要接入原图的新计算结构。- 传入
context后,底层自动完成融合检查、子图替换和融合结果上报。
使用带context的自动流程后,不要再次手动调用can_fuse()或report_fuse(),否则会发生重复检查或重复上报。不带context时继续返回整数状态码;带context时成功返回None,失败抛出RuntimeError。
3. 用户使用方式
3.1 PatternFusionPass:根据编译选项决定是否替换
from ge.passes import PassContext, PatternFusionPass, infer_shape
class FuseAddRelu(PatternFusionPass):
def patterns(self):
return build_patterns()
def meet_requirements(
self, match_result, context: PassContext
) -> bool:
return context.get_option_value("ge.custom.enable_rule") == "1"
def replacement(self, match_result, context: PassContext):
replacement_graph = build_replacement_graph(match_result)
infer_shape(replacement_graph, match_result)
return replacement_graph
执行过程为:先匹配图结构,再读取编译选项决定是否执行;决定执行后构造替换图,补齐Tensor描述信息,最后通过return replacement_graph把替换图交给GE接入原图。
3.2 使用输入占位符简化替换图构造
如果替换结构可以用算子表达式直接描述,用户可以使用inputs创建替换图的外部输入:
def replacement(self, inputs, match_result, context: PassContext):
a, b, c = inputs[:3]
if context.get_option_value("ge.custom.enable_rule") != "1":
context.set_error_message("rule is disabled")
return GEMM(a, b, c, 1.0, 1.0)
上述代码表示:原图进入被匹配子图的前三个外部输入,将分别连接到新建GEMM算子的三个输入。返回的TensorHolder会由Python框架自动转换成替换图。
只需要编译期上下文、不需要匹配详情时,可以省略match_result:
def replacement(self, inputs, context: PassContext):
return inputs[0]
直接返回inputs[0]表示删除被匹配结构,并把原来的第0个外部输入直接连接到原子图的后续节点。
3.3 DecomposePass:根据节点属性和编译选项执行分解
from ge.passes import DecomposePass, PassContext, infer_shape
class SplitGroupedConv(DecomposePass):
def meet_requirements(self, node, context: PassContext) -> bool:
return (
node.get_attr("groups") > 1
and context.get_option_value("ge.custom.split_group_conv") == "1"
)
def replacement(self, node, context: PassContext):
replacement_graph = build_grouped_conv_replacement(node)
infer_shape(replacement_graph, node)
return replacement_graph
这段代码先检查节点属性和编译开关,再生成分解后的替换图,并根据原节点输入补齐替换图的Tensor描述信息。
3.4 FusionBasePass:使用自动替换流程
from ge.passes import SubgraphRewriter
def run(self, graph, context):
boundary = build_boundary(graph)
replacement_graph = build_replacement_graph(graph)
SubgraphRewriter.replace(
boundary,
replacement_graph,
context=context,
)
return True
当用户能够明确描述“需要替换哪些输入和输出”以及“用什么新图替换”时,优先使用这条路径。底层统一完成检查、替换和上报。
3.5 FusionBasePass:直接修改原图
from ge.passes import can_fuse, report_fuse
def remove_intermediate_node(graph, node, context):
nodes_before = [node]
result = can_fuse(nodes_before)
if not result.ok:
context.set_error_message(result.reason)
return False
reconnect_node_edges(graph, node)
report_fuse(nodes_before, [], context)
graph.remove_node(node)
return True
只有在改图无法用统一的子图边界和替换图表达时,才需要用户直接操作节点和边。此时必须自行保证“先检查、再改图、删除旧节点前完成上报”的顺序。
4. 总体实现方案
4.1 各层职责
| 层次 | 主要职责 | 不承担的职责 |
|---|---|---|
| Python公开接口层 | 提供稳定的用户接口、清晰的返回值,并在用户类定义时检查方法参数。 | 不重复实现C++图算法。 |
Python本地扩展加载层_native.py |
加载C++扩展模块并把接口名称导出给上层使用。 | 不放置业务判断。 |
| Python与C++绑定层(pybind) | 在Python对象和C++对象之间转换,检查内部对象引用是否有效,并把C++失败转换为Python异常。 | 不重新实现Shape推导、融合检查和上报算法。 |
| C++图处理层 | 真正执行Tensor描述推导、融合检查、子图替换和结果上报。 | 不感知Python侧的封装类型。 |
4.2 PassContext传递流程
调用链为:
GE开始执行一个Python Fusion Pass
-> C++适配层接收当前CustomPassContext
-> Python与C++绑定层生成同一上下文的PassContext视图
-> _bridge.py把context继续传给Python Pass实例
-> 用户的meet_requirements()或replacement()使用context
关键设计:
- Python不增加新的V2公开类,用户继续继承原有
PatternFusionPass或DecomposePass。 - 用户类定义时,
base.py和pattern.py检查判断、替换方法是否声明context,并把不同写法适配成统一调用形式。 _bridge.py只负责继续传递context,每次匹配时不再重复分析Python方法签名。- 子类不重写
meet_requirements()时,基类默认方法仍返回True,不会因为新增context而失败。
异常处理保持原有语义:
meet_requirements()抛出Python异常时,底层记录警告并按“不满足条件”处理。replacement()抛出Python异常时,底层记录错误并判定替换失败。- 底层不会自动把上述Python异常写入
PassContext;业务需要向上层提供错误信息时,由用户显式调用context.set_error_message()。
4.3 infer_shape实现流程
用户调用infer_shape(replacement_graph, source)
-> Python检查参数类型和替换图是否为空
-> Python与C++绑定层识别source是MatchResult、Node还是SubgraphBoundary
-> C++根据原图边界更新替换图输入
-> C++在替换图内部执行完整推导
-> 原地更新替换图的Shape、DataType和Format
三类原图信息共用同一套C++推导实现,Python层不复制算法。替换图中的第0、1、2个输入占位节点,必须分别对应原图边界的第0、1、2个输入。
4.4 can_fuse和report_fuse实现流程
can_fuse()的流程为:
接收节点集合
-> C++扩展按顺序读取一次所有节点
-> 执行融合可行性检查
-> 返回是否可融合及原因
-> Python封装成FuseCheckResult
只读取一次节点集合,保证生成器等一次性迭代对象也能正确使用。
report_fuse()不需要额外转换返回值,因此Python直接公开C++扩展接口。成功返回None,失败抛出RuntimeError。
4.5 SubgraphRewriter.replace实现流程
Python与C++绑定层保留两种调用:
- 不带
context时调用原有C++替换接口,继续返回整数状态码。 - 带
context时调用包含融合检查和结果上报的C++替换接口,成功返回None,失败转换为RuntimeError。
5. 子模块修改关系
| 子模块 | 主要修改 | 目的 |
|---|---|---|
compiler/graph/fusion/pass/ |
增加内部C++适配类和包含CustomPassContext的调用参数。 |
把当前Pass上下文送到Python。 |
api/python/ge/ge/passes/base.py |
定义公开基类方法参数,并在子类定义时启动签名适配。 | 同时支持原有写法和新增context写法。 |
api/python/ge/ge/passes/pattern.py |
检查方法参数、区分match_result与context,并把简化构图写法转换成替换图。 |
在类定义阶段统一不同用户写法。 |
api/python/ge/ge/passes/_bridge.py |
接收C++传入的context并继续传给Python实例。 |
保持桥接层职责单一。 |
api/python/ge/ge/passes/native_bindings/ |
绑定Tensor描述推导、融合检查、结果上报和带上下文的替换接口。 | 连接Python接口与C++图处理能力。 |
api/python/ge/ge/passes/fuse_inspector.py |
定义FuseCheckResult并封装can_fuse()返回值。 |
让用户明确读取ok和reason。 |
_native.py、_ge_pass_native.pyi、__init__.py |
加载本地扩展、补充类型声明并公开接口。 | 保证接口可导入且有正确类型提示。 |
| Python和C++测试 | 验证接口行为、参数适配以及Python到C++的完整调用链。 | 防止功能、兼容性和异常处理回归。 |
| API文档和示例 | 说明参数含义、使用阶段、调用顺序和完整示例。 | 让用户能够正确选择和使用接口。 |
集成时需要同时检查base.py、类型声明文件和C++集成测试,确保上下文传递与Tensor描述推导两部分修改都被保留。
6. 测试策略
| 测试类别 | 关键测试项 | 测试方法 | 用例类型 |
|---|---|---|---|
| 功能 | PatternFusionPass和DecomposePass的meet_requirements()、replacement()声明context |
执行真实Pass,检查两个方法收到当前PassContext,并验证Pass名称、编译选项以及错误信息的读取和设置 |
UT/ST |
| 功能 | 使用inputs构造替换图,覆盖inputs、inputs + match_result、inputs + context、inputs + match_result + context四种合法方法参数 |
检查替换图输入数量、输入索引、返回Tensor和原图边界连接关系 | UT/ST |
| 功能 | infer_shape()支持MatchResult、Node和SubgraphBoundary三类原图信息 |
构造带不同输入描述的替换图,检查替换图输入占位节点和后续算子的Shape、DataType、Format是否正确更新 | UT/ST |
| 功能 | can_fuse()覆盖可融合、空集合、跨图、配置冲突和成环风险 |
检查FuseCheckResult.ok和reason,并确认业务上不可融合时不抛异常、不修改原图 |
UT/ST |
| 功能 | report_fuse()覆盖普通替换和只删除旧节点 |
在旧节点有效时上报,检查改图前后节点关系、当前Pass名称和融合记录 | UT/ST |
| 功能 | 带context的SubgraphRewriter.replace()自动完成检查、替换和上报 |
检查返回值、最终图连接关系和融合记录,并确认用户不需要重复调用can_fuse()或report_fuse() |
UT/ST |
| 异常 | 判断、替换方法的参数数量、参数顺序或context参数名不符合约定 |
在定义用户Pass类时检查是否抛出TypeError,并检查错误信息是否指出允许的方法形式 |
UT |
| 异常 | infer_shape()收到错误类型、空替换图、失效对象或不匹配的输入索引 |
分别检查TypeError和RuntimeError,确认失败后不继续提交描述不完整的替换图 |
UT/ST |
| 异常 | can_fuse()、report_fuse()收到非Node对象、失效节点或错误context |
检查异常类型和错误原因,并确认图结构未被意外修改 | UT/ST |
| 可靠性 | PassContext、MatchResult和Node只在当前同步调用期间使用;多个Pass连续执行时上下文不串用 |
在当前调用内外、连续Pass和异常路径中访问对象,检查生命周期和Pass名称隔离 | UT/ST |
| 可靠性 | 节点集合使用生成器;report_fuse()发生在删除旧节点之前 |
记录生成器迭代次数并检查节点删除前后的上报结果,确认集合只读取一次且旧节点不会提前失效 | UT/ST |
| 性能 | 同一个Pass多次匹配时,方法参数分析只在用户类定义阶段执行 | 统计签名分析调用次数并对多次匹配路径做微基准,确认桥接层不在每次匹配时重复分析 | UT/Benchmark |
| 兼容性 | 原有判断、替换方法参数,默认meet_requirements(),第三个非context参数的MatchResult语义,以及不带context的replace() |
运行原有样例和回归用例,检查原有返回值、异常方式和改图结果保持不变 | UT/ST |
| 兼容性 | Python继续公开原有Pass类,不要求用户迁移到带V2后缀的新类,当前未商发阶段不修改桥接接口版本 | 检查公开导出、类型声明和桥接版本 | UT/静态检查 |
| 特性交叉 | Pattern、Decompose、FusionBasePass自动替换和直接改图流程,与前置/后置Shape推导阶段组合 | 端到端编译代表图,检查编译选项、最终图结构、Tensor描述和融合记录 | ST |
| 特性交叉 | 动态Shape、多输入多输出、控制边和连续多次匹配 | 编译并检查边界输入映射、动态描述传递、图拓扑和多次替换结果 | UT/ST |
| 质量门禁 | Python/C++相关测试、静态检查、文档检查和增量覆盖率 | 执行对应测试套和仓库门禁任务 | CI |
7. 验收标准
PatternFusionPass和DecomposePass的合法新旧方法参数均可正常运行,默认方法和输入占位符构图行为正确;不支持的方法参数会在类定义阶段失败并明确说明原因。PassContext能在判断、替换方法中读取Pass名称、编译选项和错误信息,并能设置错误信息;对象只在当前同步调用期间使用。infer_shape()能根据三类原图信息正确更新替换图的Shape、DataType和Format;输入或推导异常时会失败并给出明确错误。can_fuse()能正确区分可融合、业务上不可融合和接口调用错误,report_fuse()能在旧节点仍然有效时正确上报改图结果。- 带
context的SubgraphRewriter.replace()能自动完成检查、替换和上报;原有不带context的调用方式保持兼容。 PatternFusionPass、DecomposePass和FusionBasePass的典型组合场景执行成功,最终图结构、节点Tensor描述和融合上报结果符合预期。- 相关单元测试、系统测试、静态检查、文档检查和增量覆盖率均满足仓库门禁。


ONNX Plugin Python 化 — 软件需求规格说明(SRS)
文档定位:面向插件开发者和实现开发者的软件需求规格与串讲主阅读版本。
关联文档:
- 完整接口评审:ONNX Plugin Python 接口方案评审
- 滚动执行计划:
onnx_plugin_python_execution_plan.md- 文档差异清单:
onnx_plugin_python_doc_diff.md
阅读说明
文档目的
本文说明如何让插件开发者使用 Python 编写 ONNX 到 GE Operator 的转换逻辑,给出公开接口、用户写法、模块划分、分期计划和验收要求。
本文优先回答以下问题:
- Python ONNX Plugin 怎么写;
- 首轮可以读取哪些 ONNX 内容、修改哪些 GE Operator 内容;
- Python callback 如何进入现有 GE parser;
- 整个能力由哪些模块组成,各自承担什么职责;
- 按什么节奏分期交付,每期的完成标志是什么。
文档范围
首轮(PR1/MVP)包括:
onnx_plugin(source, domain, opsets, target)插件描述;ParseParamsFn对应的parse_nodecallback;OnnxNode只读字段和常用属性;ge.graph.Operator常用属性操作和端口注册;- Python 插件发现、注册提交和独立 bridge;
- Python UT、bridge UT 和 parser ST。
首轮不包括:
ParseParamsByOperatorFn、ParseOpToGraphFn、ParseSubgraphPostFn;- 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 只读值对象。 |
ge.graph.Operator |
callback 期间对 ge::Operator 的受控 Python 包装。 |
parse_node |
首轮 Python callback,对应 C++ ParseParamsFn。 |
| canonical path | 经过 Path.resolve() 归一化后的唯一物理路径,用于模块去重。 |
| bridge loader | 负责 descriptor 校验、creator/callback map 冲突检查和注册提交的薄组件。 |
总体概述
本特性打通一条最小生产链路:用户用 Python 编写 plugin 文件,声明 ONNX source 到 GE target 的映射;初始化阶段由 loader 发现并按 canonical path 单次 import,registry 冻结 descriptor,薄 bridge loader 预检查后提交到现有 parser registry;解析阶段由 parser 按 origin type 找到 Python callback,把 ONNX 节点扁平化为只读 OnnxNode,交给用户 callback 补充 target Operator 的属性与端口。
模块分层与调用关系
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/>冲突预检查 + 提交"]
FLAT["parser-side flattener<br/>NodeProto → 扁平值"]
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"| FLAT
FLAT -->|"扁平值"| 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["parser-side flattener<br/>NodeProto → 扁平值"]
G6 --> G7["bridge SO<br/>OnnxNode + 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 | 首轮 |
ParseParamsByOperatorFn |
7 | PR3 |
ParseOpToGraphFn |
74 | PR2 |
ParseSubgraphPostFn |
1 | PR4 |
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 侧错误语义。
2.3 首轮支持矩阵
| 能力 | 首轮状态 | 不支持时的行为 |
|---|---|---|
onnx_plugin(source, domain, opsets, target) |
支持 | 非法字段在注册阶段报错。 |
parse_node(node, target) |
支持 | callback 异常使当前节点解析失败。 |
OnnxNode name/origin_type/inputs/outputs |
支持 | 只读,转换失败则节点解析失败。 |
| ONNX FLOAT、INT、STRING 属性及同类型列表 | 支持 | 未知或不支持类型明确报错。 |
Operator.set_attr bool/int/float/str 及同类型列表 |
支持 | 非白名单值明确报错。 |
| 默认动态输入、动态输出注册 | 支持 | 非法 name/count 明确报错。 |
| fixed/optional 端口补充 | 支持 | 已有原型端口不应重复注册。 |
Tensor、Graph、Sparse 属性、ref_attr_name |
延后 | callback 前报告不支持。 |
| TensorDesc 修改、ES 子图 | 延后 | 首轮不提供入口。 |
ParseParamsByOperatorFn、ParseOpToGraphFn、ParseSubgraphPostFn |
延后 | 首轮不存在对应装饰器。 |
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) |
不公开直接构造函数和内部 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 结束后失效。 |
首轮模块导出目标:
# ge.onnx_plugin.__all__
[
"OnnxNode",
"OnnxPlugin",
"onnx_plugin",
]
# ge.graph.__all__ 增加
["Operator"]
接口边界:
OnnxPlugin.parse_node返回原始 callable,保持装饰器后的函数名称、类型注解和可测试性;OnnxNode.inputs/outputs使用只读 tuple,attrs使用只读 Mapping;- 使用 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返回值必须是None;- Python 异常由 bridge 捕获并转换为 parser failure;
- 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] |
其他规则:
- parser 侧完成 Message 类型确认和字段扁平化,bridge 不直接链接 GE 私有 protobuf 生成类型;
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; - 对象不能 copy、deepcopy、pickle;
- 属性支持 bool、int、float、str 和对应同类型 list;
- 动态端口 count 为非负整数;
- 固定端口用于确实需要在解析期补充 IR 端口的 target,已有原型端口不应重复注册。
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 后续 callback 扩展
descriptor + callback 允许未来增加独立转换阶段,callback 名称确认为 decompose(对应 C++ ParseOpToGraphFn):
# decompose 名称已确认;参数和返回值在迭代 2 编码前冻结。
@elu.decompose
def elu_decompose(source, builder):
...
首轮 OnnxPlugin 只公开 parse_node,不提前加入 decompose 占位方法。
3.7 接口评审和资料交付
所有首轮公开接口已在编码前完成接口评审。代码合入前需同步以下资料:
- API Reference:逐个公开符号说明原型、参数、返回值、异常、生命周期和线程约束;
- 用户指南:插件目录、
ASCEND_CUSTOM_OPP_PATH、加载时机、C++/Python 冲突和常见错误; - 入门 Sample:Elu 属性解析和 Sum 动态输入;
- C++ 迁移指南:
REGISTER_CUSTOM_OP、ParseParamsFn、SetAttr、动态端口到 Python 的对应关系; .pyi/类型提示:公开类型、方法签名和只读属性与接口评审结果一致。
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:属性解析 ST
Sample 面向 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
ST 检查:模块只 import 一次、origin key 生成正确、parser creator 可创建、目标节点 type 正确、属性类型和值正确、图连接正确、callback 后 Operator 引用失效。
4.4 接口使用 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.5 测试泛化矩阵
| 维度 | 基础值 | 泛化值 | 主要检查 |
|---|---|---|---|
| 插件载体 | 单个 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 返回 | None |
非 None、抛出异常 | Status 转换和 Graph 不加节点。 |
| 注册关系 | 无冲突 | C++/Python、Python/Python | C++ 优先和预检查。 |
| 生命周期 | callback 内访问 | callback 后访问、保存到全局 | handle 失效。 |
| 无插件回归 | 配置测试插件 | 环境变量为空、路径无 Python 文件 | 原 C++ parser 行为不变。 |
5. C++ 能力如何体现在 Python 中
| GE/metadef C++ 能力 | Python 表达 | bridge/框架动作 | 阶段 |
|---|---|---|---|
REGISTER_CUSTOM_OP、OpRegistrationData |
onnx_plugin(...) |
descriptor 转换为 registration data。 | 首轮 |
FrameworkType(ONNX) |
不对外暴露 | bootstrap 内部固定。 | 首轮 |
OriginOpType |
source、domain、opsets | 展开完整 origin type 集合。 | 首轮 |
ParseParamsFn |
@plugin.parse_node |
C++ wrapper 调用 Python。 | 首轮 |
NodeProto |
OnnxNode |
parser 侧扁平化。 | 首轮 |
AttributeProto |
Python 标量/list | 类型白名单转换。 | 首轮 |
Operator::GetAttr/SetAttr |
get_attr/set_attr |
callback-bound handle 调用现有接口。 | 首轮 |
DynamicInputRegister |
register_dynamic_input |
固定默认 is_push_back=true。 |
首轮 |
DynamicOutputRegister |
register_dynamic_output |
固定默认 is_push_back=true。 |
首轮 |
ParseParamsByOperatorFn |
独立窄入口 | 写入 ByOperator map。 | PR3 |
ParseOpToGraphFn |
@plugin.decompose |
写入 graph callback map。 | PR2 |
ParseSubgraphFuncV2 |
独立 subgraph 入口 | 写入 subgraph callback map。 | PR4 |
不向 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 冲突。 |
| parser-side flattener | 读取私有 NodeProto 并转换为扁平值。 | 不调用用户 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
-> parser-side flattener 生成 OnnxNode
-> bridge 创建 callback-bound Operator
-> parse_node(node, target)
-> callback 成功后 Graph::AddOp
6.3 初始化接入路径
| 场景 | 真实入口 | Python 接入要求 |
|---|---|---|
| atc | api/atc/main_impl.cc::LoadCustomOpLib |
ParseGraph 前完成 descriptor 发现和提交。 |
| session 插件加载 | TBEPluginManager::LoadCustomOpLib |
复用一次 import,延后到 C++ creator 完成的位置。 |
| session/aclgrphParseONNX | AclGraphParserUtil::AclParserInitialize |
首次 factory/registry 查询前完成注册。 |
| aclgrphParseONNX | onnx_parser.cc 中 std::call_once |
Python bootstrap 保持进程级幂等。 |
薄 bridge loader 复用现有 loader 的 mutex 和 api_ 状态模式,仅增加 registered_ 幂等标志;registered_ 在完整 descriptor batch 提交成功后置位,失败时保持未置位,同进程不重试。
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 注册冲突处理
注册前检查:target 原型是否存在、target parser creator 是否已存在、ParseParamsFn map、ByOperator map、subgraph callback 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 一次。 |
| parser-side NodeProto flattener | 读取私有 protobuf 并输出扁平值。 | 隔离 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 初始化;
- 未配置、单插件、多插件和 1/10/100 descriptor 的初始化耗时和 Host 内存需记录观测;
- Python callback 不进入模型执行路径,不影响 Device 性能或 OM 格式。
9. 测试策略
9.1 测试边界
| 层次 | 输入 | 输出检查 |
|---|---|---|
| Python UT | descriptor、OnnxNode、Operator 参数 | 冻结 descriptor、异常类型和错误信息。 |
| parser flattener UT | NodeProto | 扁平字段、属性类型和失败状态。 |
| 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 标量和列表属性 | flattener UT + bridge UT |
| 功能 | Operator 属性和端口 | Python UT + bridge UT |
| 异常 | Tensor/Graph/Sparse、未知枚举 | flattener UT |
| 异常 | callback 异常或返回非 None | bridge UT |
| 生命周期 | callback 后保存 Operator | bridge UT |
| 冲突 | Python/Python opset 重叠、C++/Python 同 key | Python UT + parser ST |
| loader | custom_op/ONNX 同文件一次 import | parser/runtime ST |
| 兼容性 | 无 Python plugin | parser ST |
9.3 测试框架
复用 pytest、现有 graph Python UT、parser gtest 和 parser ST,不新增测试框架。测试 plugin 放在测试资源目录,通过测试专用 ASCEND_CUSTOM_OPP_PATH 加载。
10. 开发计划和门禁
10.1 分期计划
整体特性通过 4 个 PR 交付;PR1/MVP 内部按 A-D 四个工作包顺序推进,只有 D 完成后才能声明 MVP 完成。
| PR | 目标任务 | 完成标志 |
|---|---|---|
| PR1:MVP 参数解析主链 | Python API、插件发现/注册、独立 Bridge、parse_node、属性、默认动态端口、直接 Parser API、打包和资料 |
最小生产垂直链通过,MVP 可独立验收 |
| PR2:一对多子图分解 | ParseOpToGraphFn 对应的 decompose callback,复用 ES 完成一对多展开 |
Python 能表达并完成原 C++ 一对多分解链路 |
| PR3:Operator 参数解析扩展 | ParseParamsByOperatorFn,扩展 Operator/TensorDesc/属性/端口能力 |
两类参数解析 callback 形成真实闭环 |
| PR4:子图与全链路收口 | ParseSubgraphPostFn/V2,补齐剩余字段与 callback,接入 atc/session |
Python 覆盖原 C++ ONNX Plugin 完整生产链路 |
PR1 工作包:
| 工作包 | 内容 | 状态 |
|---|---|---|
| A | 用户接口、registry、loader、fake backend UT(含 canonical-path) | 已完成 |
| B | 独立 Bridge SO、扁平化、native backend、Bridge UT | 未开始 |
| C | Parser 注册、冲突预检查、真实 Parser UT/ST | 未开始 |
| D | 打包、回归、性能观测、API 资料和复盘 | 未开始 |
10.2 编码前门禁
| 决策项 | 当前状态 |
|---|---|
| 公开接口和资料 | 已评审 |
| bridge SO、ABI 和安装归属 | 工作包 B 启动前冻结 |
| atc/session 初始化锚点 | 工作包 C 启动前验证 |
| GIL 和多模型并发 | 工作包 B 启动前评审 |
| 性能阈值 | 性能责任人在工作包 D 前确认 |
11. 验收标准
- 插件开发者可以使用
onnx_plugin(...)和parse_node编写 Elu 等价插件。 - Python plugin 可以从
ASCEND_CUSTOM_OPP_PATH稳定发现,同一 canonical 物理模块只 import 一次。 - atc、session 和
aclgrphParseONNX都能在首次 parser 查询前完成注册。 OnnxNode基础字段和常用属性可以正确转换,未支持类型明确失败。- callback 可以通过
ge.graph.Operator设置属性并注册动态端口。 - callback 异常后当前 Operator 不入图,保存的 Operator 引用不可继续使用。
- C++/Python 冲突均拒绝 Python 且有明确诊断。
- custom_op 与 ONNX plugin 可以一次 import 后分别进入各自 registry。
- 未配置 Python plugin 时,现有 C++ ONNX parser 行为不变。
- 首轮不修改 graph C++ ABI、AscendIR、OM、compiler 和 runtime。
- Python UT、bridge UT、parser ST 全部通过。
- 首轮全部公开接口、
__all__和类型声明完成接口评审。
附录 A:源码证据索引
| 主题 | 源码位置 |
|---|---|
| atc plugin 注册 | api/atc/main_impl.cc |
| session plugin 加载 | api/session/common/plugin/tbe_plugin_manager.cc |
| ACL parser 初始化 | parser/parser/common/acl_graph_parser_util.cc |
| ONNX parser 入口 | parser/parser/onnx/onnx_parser.cc |
| parser creator 注册 | parser/parser/common/op_registration_tbe.cc |
| OpRegistry::Register | graph_metadef/register/register.cpp |
| 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 |
附录 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
描述
统一 HostcpuEngineUpdatePass 中已有 OpTypeUtils 分类接口的算子类型判断:Data 类节点使用 IsDataNode,Const 类节点使用 IsConstNode;无对应分类接口的 strcmp 保持不变。本次仅修改代码,不涉及文档。
变更类型
关联的Issue
无。
如何测试
ut_libge_multiparts_utest。--gtest_filter=UtestHostcpuEngineUpdatePass.*,13 个用例全部通过。核对清单
其他信息
该 PR 仅用于个人仓 Mentor 预审;确认后将另行向官方
cann/ge:develop提交正式 PR。