ONNX 模型在 MLIR 编译器基础设施中的表示和参考下推
此项目由 onnx 维护
托管于 GitHub Pages — 主题来自 orderedlist
在 onnx-mlir 中,有三种类型的测试来确保实现的正确性
后端测试是基于 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。
文件 test/backend/all_test_names.txt 包含 ONNX 包提供的所有测试用例。您可以通过将其添加到 test/backend/inference_backend.py 中来启用测试用例。all_test_names.txt 是通过命令“make check-onnx-backend-case”自动生成的。仅在 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 的巴克斯范式 (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_CONSTANT 和 IMPORTER_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
在支持的平台(目前是 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 的列表,供您在单个情况下调用转换器。
该工具直接扫描模型提供的签名,用随机值初始化所需的输入,然后对模型进行函数调用。然后,该程序可以与其他工具(例如 gdb、lldb 或 valgrind)结合使用。要列出实用程序选项,只需在运行时使用 -h 或 --help 标志。
我们首先需要编译该工具,这可以通过两种模式之一完成。在第一种模式下,该工具与静态链接的模型一起编译。此模式在编译期间除了包含 .so 文件外,还需要 -D LOAD_MODEL_STATICALLY=0 选项。最好使用 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 实用程序检查输出 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 包提供的测试之外,数值测试还用于测试数值正确性。目标是提供广泛的基于数值的单元测试;这对于确保优化转换的有效性和正确性非常重要:随着我们针对特定架构参数(如向量宽度)进行专业化,将会出现更多的极端情况。数值测试基于对被测操作的简单、朴素(且极其缓慢)的实现,生成大量基于数值的单元测试,用于验证我们操作降低和优化的正确性。
数值测试应按以下方式构建,以使以下两个组件相互独立和分离
其动机是,我们有两种方式生成测试用例参数
isOMConvTheSameAsNaiveImplFor。 // 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_ATOL 和 TEST_RTOL 设置为新值。
在支持的平台(目前是 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 的测试。其描述见此处。
编译 ONNX 模型时,添加选项 --preserveMLIR。将创建一个名为 your_model_name.input.mlir 的 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)
请注意,仪器化输出显示 gdb 在 onnx op 级别正确单步执行。您需要 onnx-mlir 的额外标志才能在仪器化上运行,这对于 gdb 不是必需的。源文件是 test_add.input.mlir。未来的工作之一是在 gdb 中支持 onnx 级别的符号。如果张量可以在 gdb 中打印出来,那将非常有用。
在 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");
为了保护更大范围的代码,可以使用此模板。
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";
...
});
该项目中使用此支持的一些示例在这些文件中。
同样,这些调试语句可以通过向 onnx-mlir 或 onnx-mlir-opt 添加 --debug-only=my_opt_name_here 选项来激活。
我们提供了一个 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 模型库中的所有模型。
如果您想收集关于模型库(或任何模型)的性能信息,最简单的方法是在编译时请求所需的统计数据(使用 -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 选项通知编译器发出编译时统计数据,并使用 --log-to-file 选项通知 RunONNXModelZoo.py 我们要保留编译器输出。例如
> 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.py 和 RunONNXModelZoo.py 同样有效。