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
的 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_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)上,后端测试可以为编译的模型生成 SIMD 指令。要启用 SIMD,请设置 TEST_MCPU 环境变量,例如,
TEST_MCPU=z14 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 支持的当前版本时,可以调用 onnx 版本转换器,并将环境变量 INVOKECONVERTER
设置为 true。例如,对于 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),数值测试可以为编译后的模型生成 SIMD 指令。要启用 SIMD,请设置 TEST_ARGS
环境变量,例如:
TEST_ARGS="-mcpu=z14" CTEST_PARALLEL_LEVEL=$(nproc) cmake --build . --config Release --target check-onnx-numerical
目前我们提供了对加速器 NNPA 的测试。其描述在此处。
编译 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)
请注意,检测结果表明 GDB 在 ONNX 操作级别正确地执行了步进。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";
...
});
项目中使用此支持的一些示例位于以下文件中:
同样,可以通过将 --debug-only=my_opt_name_here
选项添加到 onnx-mlir
或 onnx-mlir-opt
来激活这些调试语句。
我们提供了一个 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
选项通知编译器要发出的编译时统计信息,并通知 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.py
和 RunONNXModelZoo.py
都适用。