向 ONNX 添加新算子或函数

或者将现有算子更新到新的 Opset 版本。

目录

提议并提交新的 ONNX 算子或函数

算子是定义 ONNX 模型所使用的基本构建块。凭借丰富的算子集,ONNX 可以描述来自各种框架的大多数 DNN 和 ML 模型。函数允许使用更基础的算子来表达复杂的算子。ONNX 规范包含一组核心算子,可以支持许多模型。添加所有可能的算子不是目标,但会根据不断发展的需求添加更多算子。

本文档描述了接受新提议算子的流程以及如何将新算子正确提交为 ONNX 标准的一部分。目标是根据我们的经验、学习以及从社区收集的反馈,改进我们目前的工作。

添加算子的 4 个步骤

  1. 决定提议什么

  2. 为新的算子/函数提交 PR

  3. Operators SIG 对 PR 进行评审

  4. 合并 PR 并纳入下一个 ONNX 版本

步骤 1: 提议新的算子/函数

为了提议新的算子/函数,需要满足以下条件

  1. 如果算子可以用其他 ONNX 算子表达,那么它应该是一个函数而不是一个算子(ONNX 中有一个函数:MeanVarianceNormalization)。

  2. 如果算子可以分解成新的基本元素,则应提议这些基本元素,并将该算子设为一个函数。

  3. 基于一个模型。这将有助于我们理解其用法以及它解决了实际问题。如果模型是私有的或知识产权(IP)且无法共享,则该算子不属于标准,应作为自定义算子(custom OP)实现。

  4. 该算子需要至少由一个(知名)框架实现。这有助于我们了解该算子的实际行为及其用法。

  5. 算子签名和行为

    1. 如果该算子在 numpy 中可用,优先采用 numpy 语义。

    2. 如果该算子在多个框架中可用,请确保您的设计是通用的并涵盖这些框架。

  6. 优先使用属性而不是输入。

  7. 算子不应比用例所需的更复杂。但是,只要不使实现更复杂,算子就应该尽可能通用。这需要在通用性和复杂性之间仔细权衡。例如,对于某些算子来说,从 3-D 张量推广到 N-D 张量(从实现角度来看)是直接的,但对于其他算子则很复杂。在这种情况下,将根据泛化的复杂性做出选择。

步骤 2: 提交 PR

一旦满足了提议新的算子/函数的条件,您需要为新的算子/函数提交一个 PR。以下是 PR 应包含的内容。评审者应在签署(signoff)前验证 PR 的完整性。

  1. 描述

    1. 编写关于算子及其预期行为的详细描述。基本上,描述应该足够清晰,以避免实现者之间产生混淆。

    2. 在描述中添加示例以说明用法。

    3. 在描述中添加对相应框架中该算子来源的引用(如果可能)。

    4. 在描述中编写数学公式或伪代码。核心算法需要非常清晰。

  2. 编写一个 Python 参考实现,该参考实现应涵盖算子的所有预期行为。只有在极其罕见的情况下,我们才会免除此要求。

  3. 算子版本:查看我们的版本控制文档

  4. 编写单元测试,覆盖主要用法和边界情况。

    1. 测试示例将提取到文档中。

    2. 我们也会为其生成二进制数据。

    3. 示例:onnx/backend/test/case/node/abs.py

  5. 编写升级和降级测试

    1. onnx/test/automatic_upgrade_test.py 中使用 _test_op_upgrade 为您的算子添加至少一个自动升级测试。这些测试会在给定的 opset 版本(通常是引入该算子的版本)创建给定的算子,并测试版本转换器是否能够将其转换到可用的最高版本。因此,对于一个新算子,_test_op_upgrade 不会测试任何内容,但一旦该算子在未来的 opset 中更新,该测试将自动变得非平凡。

    2. 同样,在 onnx/test/automatic_downgrade_test.py 中使用 _test_op_downgrade 为您的算子添加至少一个自动降级测试。指定当前版本,以便一旦该算子在更高的 opset 版本中更新,该测试将确保向下转换得到验证。

  6. 更新文档并生成测试数据。

    1. 运行脚本。如果您在 onnx/backend/test/data/node 下有无法由 onnx/backend/test/case/node 中的脚本生成的文件,请进一步使用 python onnx/backend/test/cmd_tools.py generate-data --clean 来清理目录并仅保留所需的测试数据,以更新文档并生成测试数据。

  7. Shape 推断函数

    1. 在有意义和适用情况下,请提供一个 shape 推断函数。

    2. 在无法进行 shape 推断的情况下,至少必须有进行秩推断的逻辑(为输出 shape 添加正确数量的维度)

    3. Shape 推断函数必须附带单元测试(onnx/test/shape_inference_test.py)。

    4. 在实现您自己的函数时,您可以参考 TopK 算子的 shape 推断函数(onnx/defs/math/defs.cc

示例参考

PR 1959 是一个很好的参考示例。

步骤 3: Operators SIG 对 PR 进行评审

Operators SIG 负责 ONNX 规范中的算子/函数。SIG 定期开会并评审 PR。

签署(Sign-off)

至少得到两名 Operators SIG 贡献者的签署(sign-off)。

步骤 4: ONNX 发布

一旦 PR 经过 Operators SIG 评审并签署,它将被合并。您的新算子/函数将成为主分支的一部分,并可供任何从源代码构建的人使用。这些不是正式发布版本。ONNX 定期发布正式新版本,这些版本是主分支的快照。您的新算子/函数将包含在该发布版本中。

更新现有算子

当需要支持新的场景或输入类型等情况时,可能需要更新现有算子的定义。该过程与创建新算子的过程大体相似。

检查清单

更新现有算子时请使用此检查清单:https://github.com/onnx/onnx/wiki/Checklist-for-updating-an-existing-operator

移除算子或函数

移除现有 ONNX 算子或函数的原因有很多,例如被其他算子替换,或可以分解为一组其他算子。本文档描述了从标准中移除现有 ONNX 算子的标准。

移除算子

ONNX 中的任何算子都是由于模型和/或框架需要而添加的。为了弃用此类算子,我们需要执行以下操作。

  • 除非有替代算子,否则算子不能被弃用。

    • 替代算子可以是更通用的、取代旧算子的算子。

    • 或者是一组基本算子,它们可以共同实现被弃用算子的相同功能和行为(函数)。

  • 如果被弃用算子可以由现有算子分解,则必须将其转换为函数。

  • 如果替代算子尚未包含在 ONNX 标准中,则首先添加替代算子或算子集。

  • 添加一个版本适配器,将该算子转换为版本转换器使用的替代算子。示例:onnx/version_converter/adapters/upsample_9_10.h

  • 弃用算子不需要宽限期。

移除函数

根据定义,函数由 ONNX 基本元素组成;但是,函数可能已被支持 ONNX 的框架或运行时加速。因此,不建议移除函数,例外情况是添加另一个单一函数来取代其功能。

记录移除算子或函数

为了确保所有人都知道弃用信息,需要执行以下操作

  • 任何从 ONNX 移除的算子或函数都需要在发布说明中提及。

  • 需要更新其旧文档以显示新的替代方案以及旧方案到新方案的映射关系。

    • 只需移除 def.ccold.cc 将保留。

    • old.cc 需要更新与替代方案的映射关系。

  • 需要更新 ONNX checker,以便在出错时显示适当的消息。

  • 所有移除的算子都需要附加到 operator.md 文件末尾。