已开启
refactor: HostCPU pass统一使用OpTypeUtils判断类型 #1
gentle-knight创建于 7月30日
refactor: HostCPU pass统一使用OpTypeUtils判断类型 #1
已开启
gentle-knight创建于 7月30日
gentle-knight
gentle-knight成员
7月30日

Pull Request

描述

统一 HostcpuEngineUpdatePass 中已有 OpTypeUtils 分类接口的算子类型判断:Data 类节点使用 IsDataNode,Const 类节点使用 IsConstNode;无对应分类接口的 strcmp 保持不变。本次仅修改代码,不涉及文档。

变更类型

关联的Issue

无。

如何测试

  1. 编译 ut_libge_multiparts_utest
  2. 执行 --gtest_filter=UtestHostcpuEngineUpdatePass.*,13 个用例全部通过。
  3. 对全部变更文件执行 pre-commit,所有检查通过。

核对清单

其他信息

该 PR 仅用于个人仓 Mentor 预审;确认后将另行向官方 cann/ge:develop 提交正式 PR。

likedislike
合并受阻
gentle-knightgentle-knight成员
7月30日 修改了pull request 的描述
gentle-knight
gentle-knight成员
7月30日 评论:

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可以按算子类型匹配单个节点,再判断是否分解并生成替换图。
  • PatternMatchResult可以描述待匹配结构及本次匹配结果。
  • SubgraphBoundarySubgraphInputSubgraphOutput可以描述待替换子图的输入、输出边界。
  • SubgraphRewriter可以按照子图边界将替换图接入原图。
  • PassContext可以在FusionBasePass.run(graph, context)中读取当前Pass名称、编译选项和错误信息。
  • GraphBuilder可以构造模式图和替换图;简单场景也可以直接用Python算子表达式描述图结构。

当前主要缺少以下能力:

  • PatternFusionPassDecomposePass的条件判断方法、替换结构生成方法无法获取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): ...

这里的inputsPatternInputs对象,表示替换图的外部输入占位符集合:

  • 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 用户明确构造的子图输入、输出边界。

接口执行两步操作:

  1. 根据原图输入更新替换图中各输入占位节点的Tensor描述信息。
  2. 在替换图内部执行一次完整推导,更新后续算子的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公开类,用户继续继承原有PatternFusionPassDecomposePass
  • 用户类定义时,base.pypattern.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_resultcontext,并把简化构图写法转换成替换图。 在类定义阶段统一不同用户写法。
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()返回值。 让用户明确读取okreason
_native.py_ge_pass_native.pyi__init__.py 加载本地扩展、补充类型声明并公开接口。 保证接口可导入且有正确类型提示。
Python和C++测试 验证接口行为、参数适配以及Python到C++的完整调用链。 防止功能、兼容性和异常处理回归。
API文档和示例 说明参数含义、使用阶段、调用顺序和完整示例。 让用户能够正确选择和使用接口。

集成时需要同时检查base.py、类型声明文件和C++集成测试,确保上下文传递与Tensor描述推导两部分修改都被保留。

6. 测试策略

测试类别 关键测试项 测试方法 用例类型
功能 PatternFusionPassDecomposePassmeet_requirements()replacement()声明context 执行真实Pass,检查两个方法收到当前PassContext,并验证Pass名称、编译选项以及错误信息的读取和设置 UT/ST
功能 使用inputs构造替换图,覆盖inputsinputs + match_resultinputs + contextinputs + match_result + context四种合法方法参数 检查替换图输入数量、输入索引、返回Tensor和原图边界连接关系 UT/ST
功能 infer_shape()支持MatchResultNodeSubgraphBoundary三类原图信息 构造带不同输入描述的替换图,检查替换图输入占位节点和后续算子的Shape、DataType、Format是否正确更新 UT/ST
功能 can_fuse()覆盖可融合、空集合、跨图、配置冲突和成环风险 检查FuseCheckResult.okreason,并确认业务上不可融合时不抛异常、不修改原图 UT/ST
功能 report_fuse()覆盖普通替换和只删除旧节点 在旧节点有效时上报,检查改图前后节点关系、当前Pass名称和融合记录 UT/ST
功能 contextSubgraphRewriter.replace()自动完成检查、替换和上报 检查返回值、最终图连接关系和融合记录,并确认用户不需要重复调用can_fuse()report_fuse() UT/ST
异常 判断、替换方法的参数数量、参数顺序或context参数名不符合约定 在定义用户Pass类时检查是否抛出TypeError,并检查错误信息是否指出允许的方法形式 UT
异常 infer_shape()收到错误类型、空替换图、失效对象或不匹配的输入索引 分别检查TypeErrorRuntimeError,确认失败后不继续提交描述不完整的替换图 UT/ST
异常 can_fuse()report_fuse()收到非Node对象、失效节点或错误context 检查异常类型和错误原因,并确认图结构未被意外修改 UT/ST
可靠性 PassContextMatchResultNode只在当前同步调用期间使用;多个Pass连续执行时上下文不串用 在当前调用内外、连续Pass和异常路径中访问对象,检查生命周期和Pass名称隔离 UT/ST
可靠性 节点集合使用生成器;report_fuse()发生在删除旧节点之前 记录生成器迭代次数并检查节点删除前后的上报结果,确认集合只读取一次且旧节点不会提前失效 UT/ST
性能 同一个Pass多次匹配时,方法参数分析只在用户类定义阶段执行 统计签名分析调用次数并对多次匹配路径做微基准,确认桥接层不在每次匹配时重复分析 UT/Benchmark
兼容性 原有判断、替换方法参数,默认meet_requirements(),第三个非context参数的MatchResult语义,以及不带contextreplace() 运行原有样例和回归用例,检查原有返回值、异常方式和改图结果保持不变 UT/ST
兼容性 Python继续公开原有Pass类,不要求用户迁移到带V2后缀的新类,当前未商发阶段不修改桥接接口版本 检查公开导出、类型声明和桥接版本 UT/静态检查
特性交叉 Pattern、Decompose、FusionBasePass自动替换和直接改图流程,与前置/后置Shape推导阶段组合 端到端编译代表图,检查编译选项、最终图结构、Tensor描述和融合记录 ST
特性交叉 动态Shape、多输入多输出、控制边和连续多次匹配 编译并检查边界输入映射、动态描述传递、图拓扑和多次替换结果 UT/ST
质量门禁 Python/C++相关测试、静态检查、文档检查和增量覆盖率 执行对应测试套和仓库门禁任务 CI

7. 验收标准

  1. PatternFusionPassDecomposePass的合法新旧方法参数均可正常运行,默认方法和输入占位符构图行为正确;不支持的方法参数会在类定义阶段失败并明确说明原因。
  2. PassContext能在判断、替换方法中读取Pass名称、编译选项和错误信息,并能设置错误信息;对象只在当前同步调用期间使用。
  3. infer_shape()能根据三类原图信息正确更新替换图的Shape、DataType和Format;输入或推导异常时会失败并给出明确错误。
  4. can_fuse()能正确区分可融合、业务上不可融合和接口调用错误,report_fuse()能在旧节点仍然有效时正确上报改图结果。
  5. contextSubgraphRewriter.replace()能自动完成检查、替换和上报;原有不带context的调用方式保持兼容。
  6. PatternFusionPassDecomposePassFusionBasePass的典型组合场景执行成功,最终图结构、节点Tensor描述和融合上报结果符合预期。
  7. 相关单元测试、系统测试、静态检查、文档检查和增量覆盖率均满足仓库门禁。
likedislike
gentle-knightgentle-knight成员
7月30日 推送  1 个提交:2486d481-docs: 撤销HostCPU类型判断文档改动
gentle-knightgentle-knight成员
7月30日 修改了pull request 的描述
gentle-knight
gentle-knight成员
25 天前 评论:

ONNX Plugin Python 化 — 软件需求规格说明(SRS)

文档定位:面向插件开发者和实现开发者的软件需求规格与串讲主阅读版本。

关联文档:

阅读说明

文档目的

本文说明如何让插件开发者使用 Python 编写 ONNX 到 GE Operator 的转换逻辑,给出公开接口、用户写法、模块划分、分期计划和验收要求。

本文优先回答以下问题:

  • Python ONNX Plugin 怎么写;
  • 首轮可以读取哪些 ONNX 内容、修改哪些 GE Operator 内容;
  • Python callback 如何进入现有 GE parser;
  • 整个能力由哪些模块组成,各自承担什么职责;
  • 按什么节奏分期交付,每期的完成标志是什么。

文档范围

首轮(PR1/MVP)包括:

  • onnx_plugin(source, domain, opsets, target) 插件描述;
  • ParseParamsFn 对应的 parse_node callback;
  • OnnxNode 只读字段和常用属性;
  • ge.graph.Operator 常用属性操作和端口注册;
  • Python 插件发现、注册提交和独立 bridge;
  • Python UT、bridge UT 和 parser ST。

首轮不包括:

  • ParseParamsByOperatorFnParseOpToGraphFnParseSubgraphPostFn
  • 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_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 首轮
ParseParamsByOperatorFn 7 PR3
ParseOpToGraphFn 74 PR2
ParseSubgraphPostFn 1 PR4

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 侧错误语义。

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 子图 延后 首轮不提供入口。
ParseParamsByOperatorFnParseOpToGraphFnParseSubgraphPostFn 延后 首轮不存在对应装饰器。

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) 不公开直接构造函数和内部 registry 状态。
ge.graph.Operator callback-bound 类 由 GE 作为 callback 参数传入 nametypeget_attrset_attrregister_inputregister_optional_inputregister_outputregister_dynamic_inputregister_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 内置 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 返回值必须是 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.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 后续 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_OPParseParamsFnSetAttr、动态端口到 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_OPOpRegistrationData 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_pluginOnnxNodeOnnxPluginge.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.ccstd::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_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 初始化;
  • 未配置、单插件、多插件和 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. 验收标准

  1. 插件开发者可以使用 onnx_plugin(...)parse_node 编写 Elu 等价插件。
  2. Python plugin 可以从 ASCEND_CUSTOM_OPP_PATH 稳定发现,同一 canonical 物理模块只 import 一次。
  3. atc、session 和 aclgrphParseONNX 都能在首次 parser 查询前完成注册。
  4. OnnxNode 基础字段和常用属性可以正确转换,未支持类型明确失败。
  5. callback 可以通过 ge.graph.Operator 设置属性并注册动态端口。
  6. callback 异常后当前 Operator 不入图,保存的 Operator 引用不可继续使用。
  7. C++/Python 冲突均拒绝 Python 且有明确诊断。
  8. custom_op 与 ONNX plugin 可以一次 import 后分别进入各自 registry。
  9. 未配置 Python plugin 时,现有 C++ ONNX parser 行为不变。
  10. 首轮不修改 graph C++ ABI、AscendIR、OM、compiler 和 runtime。
  11. Python UT、bridge UT、parser ST 全部通过。
  12. 首轮全部公开接口、__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_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