onnx-mlir

Logo

ONNX 模型在 MLIR 编译器基础设施中的表示和参考下推

在 GitHub 上查看项目 onnx/onnx-mlir

操作指南

使用 Python 进行推理
使用 C/C++ 进行推理
使用 Java 进行推理

参考资料

ONNX 方言
OMTensor C99 运行时 API
OMTensorList C99 运行时 API
OMTensor Java 运行时 API
OMTensorList Java 运行时 API
生成 ONNX 方言
关于文档

开发

添加操作
测试指南
错误处理
命令行选项
插桩
常量传播
添加加速器

工具

工具

RunONNXModel.py
DocCheck

此项目由 onnx 维护

托管于 GitHub Pages — 主题来自 orderedlist

测试

在 onnx-mlir 中,有三种类型的测试来确保实现的正确性

  1. ONNX 后端测试
  2. LLVM FileCheck 测试
  3. 数值测试
  4. 使用 gdb
  5. ONNX 模型库

ONNX 后端测试

后端测试是基于 ONNX 节点和模型测试的 onnx-mlir 的端到端测试。它们可用于测试 C/C++ .so 库和 JNI .jar 存档。对于每个 C/C++ 测试目标,添加 -jni 后缀即可得到相应的 JNI 测试目标。要调用测试,请使用以下命令

cmake --build . --config Release --target check-onnx-backend[-jni]

要运行后端测试,需要安装诸如 third_party/onnx 之类的包。您可以使用命令 pip install your-onnx-mlir/third_party/onnx 来安装自己的 onnx 包。JNI 测试需要 jsoniter jar,如果系统中找不到已安装的版本,则默认从其 Maven 仓库下载。如果用户在构建 ONNX-MLIR 时开启了 cmake 选项 ONNX_MLIR_BUILD_JSONITER,则 jsoniter jar 将从其 github 仓库克隆的源代码本地构建。请注意,本地构建 jsoniter jar 需要安装 Maven 构建工具。

onnx 包提供的所有测试用例都列在文件 test/backend/all_test_names.txt 中。check-onnx-backend 将选择性地运行其中一些。check-onnx-backend 将运行的 onnx 中的节点和模型测试由 test/backend/test.py 文件中的变量 test_to_enable 定义。用户可以通过环境变量 TEST_CASE_BY_USER 来测试单个测试用例。例如,

TEST_CASE_BY_USER=selected_test_name cmake --build . --config Release --target check-onnx-backend[-jni]

当指定了 TEST_CASE_BY_USER 后,中间结果、.onnx 文件和 .so 文件会保留在 build/test/backend 中用于调试。如果您想检查生成的共享库中是否包含特定指令,请将环境变量 TEST_INSTRUCTION_CHECK 设置为 true,并在测试名称后添加指令名称,例如 TEST_CASE_BY_USER=selected_test_name,instruction_name。请注意,在 onnx 测试名称后添加 _cpu 后缀。

ONNX 支持的测试用例

文件 test/backend/all_test_names.txt 包含 ONNX 包提供的所有测试用例。您可以通过将测试用例添加到 test/backend/inference_backend.py 来启用它。all_test_names.txt 是通过命令“make check-onnx-backend-case”自动生成的。只有当 ONNX 包升级时才需要更新。

将 ONNX 支持的测试用例添加到当前的后端测试集中

当添加了一个算子的 ONNX 到 Krnl 的转换后,应该将该算子相应的后端测试添加到 test.py 中。可用测试用例可在 third_party/onnx/onnx/backend/test/case/node 中找到。您可以通过在 test/backend/all_test_names.txt 中查找新算子来识别新测试。找到新测试后,您可以将其添加到 test/backend/inference_backend.py. 中。请注意,在 onnx 测试名称后添加 _cpu 后缀。与测试相关联,您可以定义如何为新算子运行测试。例如,

        "test_and2d_cpu": {STATIC_SHAPE:{}, DYNAMIC_SHAPE:{-1:{-1}}, CONSTANT_INPUT:{-1}},

表示测试 test_and2d_cpu 可以通过以下方式运行:(1) 静态形状,(2) 其所有输入强制为动态形状,或 (3) 其所有输入强制定义为常量。这是大多数算子的推荐设置。但是,有些算子对某些参数不兼容动态形状;对于这些情况,可以显式决定函数的可变参数。这通过 {-1:{-1}} 表达式指定。test/backend/inference_backend.py. 文件包含有关如何指定哪个参数和/或参数维度可以设置为动态的明确说明。

未知维度的测试

使用以下命令可以最容易地对动态张量大小进行测试,该命令也由我们的检查器使用。

cmake --build . --config Release --target check-onnx-backend-dynamic[-jni]

ONNX 节点测试通常具有已知的输入张量维度大小。因此,为了测试未知维度的张量,模型导入器(Build/FrontendONNXTransformer.cpp)提供了一个功能来生成此类用例。当环境变量 IMPORTER_FORCE_DYNAMIC 设置时,前端导入会将模型所有输入张量的所有维度(默认)更改为 -1。例如,

IMPORTER_FORCE_DYNAMIC='-1:-1' all dimensions of all the inputs will be changed
IMPORTER_FORCE_DYNAMIC='0:-1' all dimensions of the first input will be changed
IMPORTER_FORCE_DYNAMIC='0:-1|1:0,1' all dimensions of the first input and the 1st and 2nd dimensions of the second input will be changed

IMPORTER_FORCE_DYNAMIC 的 Backus-Naur 范式 (BNF) 如下。

<ImportForceDynamicExpr> :== `'` <expr> `'`
                  <expr> ::= <inputString> | <inputString> `|` <expr>
            <inputString ::= <inputIndex> `:` <dimString>
             <dimString> ::= <dimIndex> | <dimIndex> `,` <dimString>
            <inputIndex> ::= <index>
              <dimIndex> ::= <index>
                 <index> ::= -1 | <number>
                <number> ::= <digit> | <digit><number>
                 <digit> ::= 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

-1 在语义上表示所有输入或所有维度,并且具有最高优先级。例如,'0: -1, 0' 表示第一个输入的将更改所有维度。输入和维度索引从 0 开始。

例如,test_add_cpu 的默认模型是

func @main_graph(%arg0: tensor<3x4x5xf32>, %arg1: tensor<3x4x5xf32>) -> tensor<3x4x5xf32>

IMPORTER_FORCE_DYNAMIC='-1:-1' 时,结果是

func @main_graph(%arg0: tensor<?x?x?xf32>, %arg1: tensor<?x?x?xf32>) -> tensor<?x?x?xf32>

IMPORTER_FORCE_DYNAMIC='0:-1' 时,结果是

func @main_graph(%arg0: tensor<?x?x?xf32>, %arg1: tensor<3x4x5xf32>) -> tensor<3x4x5xf32>

IMPORTER_FORCE_DYNAMIC='0:0,2|1:1' 时,结果是

func @main_graph(%arg0: tensor<?x4x?xf32>, %arg1: tensor<3x?x5xf32>) -> tensor<3x4x5xf32>

这是使用现有节点测试进行动态张量测试的一种方法。由于并非所有测试用例都能在动态张量下通过,因此在 test/backend/test.py 中有一个名为 test_not_for_dynamic 的列表,用于指定哪些测试不能在 IMPORTER_FORCE_DYNAMIC 定义时通过。

带常量输入的测试

由于 onnx 节点测试在运行时接受输入张量,因此在编译 onnx 模型时输入不是常量。但是,实际上,输入可以是常量,我们希望测试这种情况。

使用以下命令可以最容易地对带常量输入的测试进行测试,该命令也由我们的检查器使用。

cmake --build . --config Release --target check-onnx-backend-constant[-jni]

要测试单个 onnx 节点,例如 test_add_cpu,请使用两个环境变量 TEST_CONSTANTIMPORTER_FORCE_CONSTANT,例如:

TEST_CONSTANT=true IMPORTER_FORCE_CONSTANT="0" TEST_CASE_BY_USER=test_add_cpu make check-onnx-backend[-jni]

这将使第一个输入(索引 0)变为常量,因此模型现在只有一个输入而不是两个。

环境变量 IMPORTER_FORCE_CONSTANT 是一个由 , 分隔的索引列表(从 0 开始,或 -1 表示所有输入索引),例如 0, 2, 3-1

输入签名测试

使用以下命令测试 onnx 模型具有各种数据类型的输入签名,该命令也由我们的检查器使用。

cmake --build . --config Release --target check-onnx-backend-signature

启用 SIMD 指令

在支持的平台上(目前是 s390x z14 及以上、x86 和 arm),后端测试可以为编译的模型生成 SIMD 指令。要启用 SIMD,请设置 TEST_MARCH 环境变量,例如:

TEST_MARCH=z16 cmake --build . --config Release --target check-onnx-backend[-jni]

后端测试的执行

utils/RunONNXLib.cpp 中定义的工具可以用来轻松执行 .so 模型文件,例如使用 TEST_CASE_BY_USER=selected_test_name make check-onnx-backend 命令生成的模型。通过在 onnx-mlir/src/Compiler/CompilerUtils.cpp 文件中将 overridePreserveFiles 值设置为 KeepFilesOfType::All,可以在以其他方式构建模型时保留模型。

当 onnx 模型比 onnx-mlir 支持的当前版本旧时,可以通过将环境变量 INVOKECONVERTER 设置为 true 来调用 onnx 版本转换器。例如,对于所有测试用例,将调用转换器:INVOKECONVERTER=true make check-onnx-backend。在 test.py 中,有一个名为 test_need_converter 的列表,用于对单个用例调用转换器。

该工具直接扫描模型提供的签名,用随机值初始化所需的输入,然后调用模型函数。然后,该程序可以与其他工具(如 gdblldbvalgrind)结合使用。要列出实用程序的选项,只需在运行时使用 -h--help 标志。

我们首先需要编译该工具,这可以通过两种模式之一完成。在第一种模式中,该工具是与静态链接的模型一起编译的。此模式在编译时需要 -D LOAD_MODEL_STATICALLY=0 选项,此外还要包含 .so 文件。最好使用 onnx-mlir/utils 目录中的 build-run-onnx-lib.sh 脚本来编译该工具及其模型,模型作为参数传递给脚本。为了避免 Mac 上的库路径问题,请在模型构建目录中运行编译后的工具。

# Compile tool with model.
cd onnx-mlir/build
sh ../utils/build-run-onnx-lib.sh test/backend/test_add/test_add.so
# Run the tool to run the model (substitute `Release` for `Debug` for the release version).
Debug/bin/run-onnx-lib
# or, on Mac, run the tool in the directory where the model was built
(cd test/backend; ../../Debug/bin/run-onnx-lib)
# if test_add.so was built in `test/backend`:
cd test/backend; ../../Debug/bin/onnx-mlir --EmitLib test_add/test_add.onnx

(在 Mac 上,您可以使用 otool -L test_add.so 查看库的路径。)

在第二种模式中,该工具在没有模型的情况下编译,模型将在运行时传递。要启用此选项,只需使用 -D LOAD_MODEL_STATICALLY=1 选项编译该工具。您可以使用与上面相同的脚本,但没有参数。然后,只要在运行时将 .so 模型文件传递给该工具,就可以从任何目录运行该工具。

# Compile tool without a model.
cd onnx-mlir/build
sh ../utils/build-run-onnx-lib.sh
# Run the tool with an argument pointing to the model.
Debug/bin/run-onnx-lib test/backend/test_add/test_add.so

LLVM FileCheck 测试

通过将中间表示作为输入并使用 LLVM FileCheck 实用程序检查输出 IR,我们可以测试一个 Pass 的功能。例如,我们有一个用于形状推断的测试用例 test.mlir。

func @test_default_transpose(%arg0 : tensor<5x5x1x32xf32>) -> tensor<*xf32> {
  %0 = "onnx.Transpose"(%arg0) : (tensor<5x5x1x32xf32>) -> tensor<*xf32>
  "std.return"(%0) : (tensor<*xf32>) -> ()

您可以在此测试用例上运行形状推断 Pass,并获得以下输出:

module  {
  func @test_default_transpose(%arg0: tensor<5x5x1x32xf32>) -> tensor<32x1x5x5xf32> {
    %0 = "onnx.Transpose"(%arg0) {perm = [3, 2, 1, 0]} : (tensor<5x5x1x32xf32>) -> tensor<32x1x5x5xf32>
    return %0 : tensor<32x1x5x5xf32>
  }
}

手动检查输出是否正确。如果输出正确,则将其转换为以后可以自动检查的内容。使用命令

Debug/bin/onnx-mlir-opt --shape-inference test.mlir | python ../utils/mlir2FileCheck.py 

您将获得以下内容:

// mlir2FileCheck.py
// CHECK-LABEL:  func @test_default_transpose
// CHECK-SAME:   ([[PARAM_0_:%.+]]: tensor<5x5x1x32xf32>) -> tensor<32x1x5x5xf32> {
// CHECK:           [[VAR_0_:%.+]] = "onnx.Transpose"([[PARAM_0_]]) {perm = [3, 2, 1, 0]} : (tensor<5x5x1x32xf32>) -> tensor<32x1x5x5xf32>
// CHECK:           return [[VAR_0_]] : tensor<32x1x5x5xf32>
// CHECK:         }

将源代码和检查代码合并,并添加到适当的测试用例中。onnx dialect 的所有测试用例都收集在 test/mlir/onnx 目录下。可以使用 make check-onnx-lit 调用这些测试用例。此目标是构建的基本要求。

数值测试

数值测试用于测试 ONNX 包提供的测试之外的数值正确性。目标是提供广泛的基于数值的单元测试;这对于确保优化转换是有效且正确的非常重要:当我们针对特定架构参数(如向量宽度)进行专业化时,会出现更多边界情况。数值测试基于被测试操作的简单、朴素(且极其缓慢)的实现生成大量的数值单元测试,用于验证我们的操作下推和优化的正确性。

数值测试的结构应使以下两个组件独立且分开:

动机是,我们希望通过两种方式生成测试用例参数:

  // RapidCheck test case generation.
  bool success = rc::check("convolution implementation correctness", []() {
    const auto N = *rc::gen::inRange(1, 10);
    const auto C = *rc::gen::inRange(1, 20);
    const auto H = *rc::gen::inRange(5, 20);
    const auto W = *rc::gen::inRange(5, 20);

    const auto kH = *rc::gen::inRange(1, 15);
    const auto kW = *rc::gen::inRange(1, 15);

    // We don't want an entire window of padding.
    const auto pHBegin = *rc::gen::inRange(0, kH - 1);
    const auto pHEnd = *rc::gen::inRange(0, kH - 1);
    const auto pWBegin = *rc::gen::inRange(0, kW - 1);
    const auto pWEnd = *rc::gen::inRange(0, kW - 1);

    // Make sure we have at least 1 output per dimension.
    RC_PRE((H >= kH) && (W > kW));

    RC_ASSERT(isOMConvTheSameAsNaiveImplFor(
        N, C, H, W, kH, kW, pHBegin, pHEnd, pWBegin, pWEnd));
  });
  assert(success && "error while performing RapidCheck tests");

有时能够看到与数值测试相关的 mlir 文件会很方便。为此,最简单的方法是将 src/Compiler/CompilerUtils.cpp 中的 overridePreserveFiles 变量设置为您要保留的文件类型(例如 KeepFilesOfType::All)。然后,无论您如何编译模型,输入和输出的 mlir 文件都将得以保留,以及未优化和已优化的字节码文件以及一些额外的二进制文件。

在发生故障时,RapidCheck(用于数值测试的基础设施)和 onnx 模型都允许用户使用相同的值重新运行测试。运行测试时,您可能会看到以下输出:

Model will use the random number generator seed provided by "TEST_SEED=1440995966"
RapidCheck Matrix-Vector test case generation.
Using configuration: seed=4778673019411245358

通过记录以下两个环境变量中的种子值:

export RC_PARAMS="seed=4778673019411245358"
export TEST_SEED=1440995966

您可以分别强制 RapidCheck 中使用的随机种子以及用于填充 ONNX 输入向量的随机种子相同。仅设置第一个(RC_PARAMS),您将看到相同的测试配置被运行,但输入值不同。同时设置两者,您将看到相同的配置和相同的输入用于完全相同的运行。

如果您需要更改 ATOL 和 RTOL 以进行精度检查,请将环境变量 TEST_ATOLTEST_RTOL 设置为新的值。

启用 SIMD 指令

在支持的平台上(目前是 s390x z14 及以上、x86 和 arm),数值测试可以为编译的模型生成 SIMD 指令。要启用 SIMD,请设置 TEST_ARGS 环境变量,例如:

TEST_ARGS="-march=z16" CTEST_PARALLEL_LEVEL=$(nproc) cmake --build . --config Release --target check-onnx-numerical

特定加速器的测试

目前我们提供 NNPA 加速器的测试。它在此处描述:这里

使用 gdb

获取 ONNX 模型源代码

编译 ONNX 模型时,添加选项 --preserveMLIR。将创建一个 MLIR 格式的模型源代码,名为 your_model_name.input.mlir。操作的行信息将被附加并一直传播到二进制文件。当您在 gdb 中运行编译后的库时,您可以在模型中停止并相对于 ONNX 操作进行单步调试。下面是一个模型 test_add.onnx 的示例:

$Debug/bin/onnx-mlir --preserveMLIR test_add.onnx
$. ../utils/build-run-onnx-lib.sh
$gdb Debug/bin/run-onnx-lib
(gdb) b run_main_graph
(gdb) run ./test_add.so
(gdb) list
1	builtin.module  {
2	  builtin.func @main_graph(%arg0: tensor<3x4x5xf32>, %arg1: tensor<3x4x5xf32>) -> tensor<3x4x5xf32> {
3	    %0 = "onnx.Add"(%arg0, %arg1) : (tensor<3x4x5xf32>, tensor<3x4x5xf32>) -> tensor<3x4x5xf32>
4	    return %0 : tensor<3x4x5xf32>
5	  }
(gdb) b 3
Breakpoint 2 at 0x3fffdf01778: file /home/chentong/onnx-mlir/build/test_add.input.mlir, line 3.
(gdb) c
Continuing.

Breakpoint 2, main_graph () at /home/chentong/onnx-mlir/build/test_add.input.mlir:3
3	    %0 = "onnx.Add"(%arg0, %arg1) : (tensor<3x4x5xf32>, tensor<3x4x5xf32>) -> tensor<3x4x5xf32>
(gdb) n
[Detaching after vfork from child process 2333437]
#  0) before op=     Add VMem:  6804
[Detaching after vfork from child process 2333470]
#  1) after  op=     Add VMem:  6804
4	    return %0 : tensor<3x4x5xf32>
(gdb)

请注意,插桩的输出表明在 onnx 操作级别的 gdb 单步调试是正确的。您需要为 onnx-mlir 添加额外的标志才能进行插桩,而 gdb 则不需要。源文件是 test_add.input.mlir。gdb 中支持 onnx 级别符号的进一步工作之一。如果可以在 gdb 中打印张量,那将非常有用。

使用 LLVM 调试支持

在 LLVM 和 MLIR 项目中添加跟踪代码的标准方法是使用 LLVM_DEBUG 宏。LLVM 的官方文档在此:这里

要插入单个“打印输出”在调试控制下,可以使用以下模板:

#include "llvm/Support/Debug.h"

#define DEBUG_TYPE "my_opt_name_here"
...
LLVM_DEBUG(llvm::dbgs() << "debug msg here" <<  obj_to_print << "\n");

要触发调试跟踪,只需调用编译器并带上 --debug-only=my_opt_name_here 选项。

另一个名为 DEBUG_WITH_TYPE 的宏可以在源文件只有一个跟踪消息的情况下使用。在这种情况下,您可以省略 DEBUG_TYPE 定义,而是使用以下内容:

DEBUG_WITH_TYPE("my_debug_msg", llvm::dbgs() << "my trace msg here\n");

为了保护更大 portions 的代码,可以使用此模板。

LLVM_DEBUG({
  for(i...) {
    llvm::dbgs() << "emit trace for a: " << a << "\n";
    compute b;  // should be side effects free
    llvm::dbgs() << "emit trace for 'b':" << b << "\n";
    ...
});

项目中一些使用此支持的示例位于这些文件中:

同样,这些调试语句可以通过将 --debug-only=my_opt_name_here 选项添加到 onnx-mlironnx-mlir-opt 来激活。

ONNX 模型库

我们提供了一个 Python 脚本 RunONNXModelZoo.py,用于使用 ONNX 模型库 中的模型检查推理准确性。RunONNXModelZoo.py 需要 RunONNXModel.py 在同一文件夹中。例如,使用 mnist-8 检查推理准确性:

$ mkdir test && cd test
$ ln -s /onnx-mlir/utils/RunONNXModel.py
$ ln -s /onnx-mlir/utils/RunONNXModelZoo.py
$ ONNX_MLIR_HOME=/onnx-mlir/build/Release/ python RunONNXModelZoo.py -m mnist-8 -c="-O3"

使用 -h 运行脚本以查看所有选项。除了使用 -m 标志指定模型和 -c 标志指定编译选项外,有用的选项是 -k 标志将 onnx 模型保留在当前目录中作为 .tgz 文件,以及 -l debug 标志以打印大量调试信息。

要找出可用的模型,请使用 -p 运行脚本以打印可用模型列表;或者使用 -m 后面跟一个不完整的名称,脚本将建议确切的名称。

如果不使用 -m 指定模型,脚本将检查 ONNX 模型库中的所有模型。

ONNX 模型库性能分析

如果您想收集模型库(或任何模型)的性能信息,最简单的方法是在编译时请求所需的统计信息(使用 -profile-ir 标志),将统计输出重定向到文件,然后使用 make-report.py 进行分析。例如:

> ONNX_MLIR_INSTRUMENT_FILE=run.log RunONNXModelZoo.py -c "-O3 --march=arm64 --profile-ir=Onnx" -m bertsquad-10
...
> make-report.py -r run.log
...
Statistics start (all ops).
  onnx.Add, 112, 0.0130570
  onnx.Cast, 105, 0.0001860
  onnx.Concat, 55, 0.0001290
  onnx.Constant, 473, 0.0008220

运行时性能信息也可以与特定的编译时统计信息结合使用。假设我们对 SIMD 统计信息感兴趣。我们通过 -opt-report 选项通知编译器要发出的编译时统计信息,并通知 RunONNXModelZoo.py 我们希望使用 --log-to-file 选项保留编译器输出。例如:

> ONNX_MLIR_INSTRUMENT_FILE=run.log RunONNXModelZoo.py -c "-O3 --march=arm64 -opt-report=Simd --profile-ir=Onnx" -m bertsquad-10 --log-to-file compile.log
...
> make-report.py -c compile.log -r run.log
...
Statistics start (all ops).
  onnx.Add-simd, 112, 0.0130570
  onnx.Cast, 23, 0.0000650
  onnx.Gemm, 1, 0.0003570
  onnx.Gemm-simd, 72, 0.8109330

在上面的列表中,被向量化的操作会单独汇总,并在其各自的操作名称后面附加 -simd 后缀。

相同的选项和环境变量对于 RunONNXModel.pyRunONNXModelZoo.py 同样有效。