ORT 模型部署

Deploy ORT model

onnxruntime-inference-examples/mobilenet.ipynb at main · microsoft/onnxruntime-inference-examples

详细记录YOLACT实例分割ncnn实现

真正部署模型,不应该把后处理包括在模型推理中,这会影响模型在GPU上的部署,性能也不一定会好。这里的后处理,不仅仅是model(input) 之后的,也可以是作者放在模型推理过程中,但是实际上可以归为后处理的部分。

判断函数是否在ONNX trace的过程中:torch.onnx.is_in_onnx_export()

ONNX to TF

Torch to ONNX

  1. torch不支持F.grid_sample 算子。从ONNX支持的算子列表来看,opset=16 时,可以直接使用grid_sampler 而不需要手动设置符号函数。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# https://github.com/pytorch/pytorch/issues/27212#issuecomment-1059773074
# https://gist.github.com/daigo0927/8c8b3005cffb61983e80ceab6c1f2274
# https://github.com/onnx/onnx/pull/3557

from torch.onnx import register_custom_op_symbolic
import torch.onnx.symbolic_helper as sym_help

def grid_sampler(g, input, grid, mode, padding_mode, align_corners):
# mode
# 'bilinear' : onnx::Constant[value={0}]
# 'nearest' : onnx::Constant[value={1}]
# 'bicubic' : onnx::Constant[value={2}]
# padding_mode
# 'zeros' : onnx::Constant[value={0}]
# 'border' : onnx::Constant[value={1}]
# 'reflection' : onnx::Constant[value={2}]
mode = sym_help._maybe_get_const(mode, "i")
padding_mode = sym_help._maybe_get_const(padding_mode, "i")
mode_str = ['bilinear', 'nearest', 'bicubic'][mode]
padding_mode_str = ['zeros', 'border', 'reflection'][padding_mode]
align_corners = int(sym_help._maybe_get_const(align_corners, "b"))

return g.op("com.microsoft::GridSample", input, grid,
mode_s=mode_str,
padding_mode_s=padding_mode_str,
align_corners_i=align_corners)

register_custom_op_symbolic('::grid_sampler', grid_sampler, 1)
  1. _DCNv2 完全不受支持。它是用CUDA编译的,无论是自带的DCNv2还是mmcv的DCNv2。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@staticmethod
def symbolic(
g, input, offset, mask, weight, bias, stride, padding, dilation, deformable_groups
):
from torch.nn.modules.utils import _pair

stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
# as of trt 7, the dcn operation will be translated again by modifying the onnx file
# so the exporting code is kept to resemble the forward()
return g.op(
"custom_domain::_DCNv2",
input,
offset,
mask,
weight,
bias,
stride_i=stride,
padding_i=padding,
dilation_i=dilation,
deformable_groups_i=deformable_groups,
)

在这里可以注册一个符号函数(symbolic_function)来自己手动通过cpp实现DCNv2,但是我其实并不确定它能不能运行到mobile端。最上面导出NCNN的方法其实说过,可以跳过它,就是不用它,换到一个Conv层就行。在实现层面上,参考下面的模型变量key:

1
2
3
4
5:'dla.ida_up.proj_1.conv.weight'
6:'dla.ida_up.proj_1.conv.bias'
7:'dla.ida_up.proj_1.conv.conv_offset_mask.weight'
8:'dla.ida_up.proj_1.conv.conv_offset_mask.bias'

它自带了一个conv,同时还有一个conv_offset_mask来学习stride的微小偏移量,这使得我们可以不使用后面的conv_offset_mask,只使用conv本身的权重来推理。不过这样会丢掉一些性能。下面这个用的是TensorRT推理,所以可以支持CUDA。DCNv2的实现中,其中一个就是参考了这个repo的。它这里面实现了自定义的算子,并且用符号函数把算子和onnx计算图联系到了一起。

TensorRT-CenterNet/ctdet2onnx.md at master · CaoWGG/TensorRT-CenterNet

  1. Floating point exception (core dumped) 。这个报错出现地非常地无厘头,因为它没有任何报错栈,只是单纯地显示它出错了。如果用torch1.9,会有更多的报错信息,虽然基本上也看不了。截图如下:

raw_error.png

Dealing with floating point exceptions

通过gdb --args python [finetune.py](http://finetune.py) ${params} ,可以查看到更多的报错信息,截图如下:

gdb_callstack.png

ONNX to ORT

  1. 除法算子导致张量除法的datatype不对应。onnxruntime.capi.onnxruntime_pybind11_state.Fail: [ONNXRuntimeError] : 1 : FAIL : Load model from /media/tclab/980Pro/users/bangwhe/e2ec/gcn.onnx failed:Type Error: Type parameter (T) of Optype (Div) bound to different types (tensor(double) and tensor(float) in node (Div_337).
1
2
3
4
5
6
7
# 原代码
img_poly[..., 0] = img_poly[..., 0] / (w / 2.) - 1
img_poly[..., 1] = img_poly[..., 1] / (h / 2.) - 1

# 修改后的代码,w先做除法
img_poly[..., 0] = img_poly[..., 0] / w * 2. - 1
img_poly[..., 1] = img_poly[..., 1] / h * 2. - 1
  1. 循环中的== 生成了bool,可能不受支持。onnxruntime.capi.onnxruntime_pybind11_state.InvalidGraph: [ONNXRuntimeError] : 10 : INVALID_GRAPH : Load model from /media/tclab/980Pro/users/bangwhe/e2ec/gcn.onnx failed:This is an invalid model. Type Error: Type 'tensor(bool)' of input parameter (763) of operator (ScatterND) in node (ScatterND_543) is invalid.
1
2
3
4
5
6
7
8
9
10
# 原代码
batch_size = cnn_feature.size(0)
gcn_feature = torch.zeros([img_poly.size(0), cnn_feature.size(1), img_poly.size(1)]).to(img_poly.device)
for i in range(batch_size):
poly = img_poly[ind == i].unsqueeze(0)
feature = torch.nn.functional.grid_sample(cnn_feature[i:i+1], poly)[0].permute(1, 0, 2)
gcn_feature[ind == i] = feature

# 修改后的代码
gcn_feature[0] = torch.nn.functional.grid_sample(cnn_feature[0:1], img_poly[0:1].unsqueeze(0))[0].permute(1, 0, 2)

ORT NNAPI

https://github.com/BangwenHe/ORTSegDemo

onnxruntime/ScoreMNIST.java at master · microsoft/onnxruntime

onnxruntime-inference-examples/ImageUtil.kt at main · microsoft/onnxruntime-inference-examples

导出成onnx或者ort模型后,部署到手机上肯定是会支持CPU的,但是是否支持GPU还需要看模型的支持或者NNAPI的支持。增加CPU的线程数可以直接使用mSessionOptions.setIntraOpNumThreads(4); ,肯定是支持的。

  1. shape错误,应该是slice(切片)算子不受到NNAPI的支持。
1
2
W/System.err: ai.onnxruntime.OrtException: Error code - ORT_FAIL - 
message: model_builder.cc:374 RegisterModelOutputs shape_proto cannot be null for output: 217
  1. 我认为我导出的这个e2ec或者是snake不能运行到mobile端的GPU上应该是因为添加了太多的逻辑运算,而不仅仅是Conv、BN、ReLU这些算子,例如NonZero,还有一些多维取值算子,例如ScatterND、Gather等。这些逻辑算子和取值算子对于逻辑运算单元少的GPU来说,并行起来是非常困难的。所以优化的角度应该是把类似后处理的逻辑运算给提取出来,只保留大部分的backbone,手动从heatmap中提取关键点的位置信息,posenet输出的就是heatmap,需要手动做提取。

Build ONNXRuntime From Source

Build for Android

要求cmake 3.18+,android SDK需要手动通过sdkmanager下载。加上--build_java 会很慢,所以先删掉。

1
2
3
4
5
export PATH=/mnt/tbdisk/bangwhe/experiments/cmake-3.20.6-linux-x86_64/bin/:$PATH

./build.sh --android --build_nnapi\
--android_sdk_path /mnt/tbdisk/bangwhe/env/Android/platforms/android-29/ \
--android_ndk_path /mnt/tbdisk/bangwhe/env/android-ndk-r20b/

编译得到的so库都保存在build文件夹中,下面有子文件夹,分别保存安卓端和Linux桌面端。

OnnxRuntime C++ Impl

Linux

onnxruntime-inference-examples/model-explorer.cpp at main · microsoft/onnxruntime-inference-examples

error: use of deleted function ‘Ort::Experimental::Session::Session(const Ort::Experimental::Session&) :函数被删除了,后面有debug信息。这个session不能通过函数传到其它的函数里,从而被调用。Ort::Value 也是一样的,但是通过std::move 将其变成右值,就可以传到vector 中。(Why?)

vscode添加索引路径:command palette → C/C++: Edit Configurations (JSON)

2022-08-15 10:50:20.544504982 [W:onnxruntime:, graph.cc:1220 Graph] Initializer 1894 appears in graph inputs and will not be treated as constant value/weight. This may prevent some of the graph optimizations, like const folding. Move it out of graph inputs if there is no need to override it, by either re-generating the model with latest exporter/converter or with the tool onnxruntime/tools/python/remove_initializer_from_input.py. initializer 1894 出现在图形输入中,不会被视为常量值/权重,可能会阻止一些类似常量折叠的优化,可以使用onnxruntime/tools/python/remove_initializer_from_input.py 来进行移除操作。移除前的推理时间是45ms,移除后是19ms,提升还是比较大的。

cv:dnn:blobFromImage 不太好用,经常把320x320的Mat变成3x1的Mat。(Why?)

OpenCV的Mat 类和ORT的Ort::Value 类都可以得到它们的数据指针,方法分别是:

  • [Mat.data](http://Mat.data) 可以直接得到数据的指针,但是更常用的是Mat.at<DataType>()
  • Ort::Value::GetTensorMutableData<DataType>() ,得到Ort::Value 保存的张量的数据指针
  • 通过这两个指针,我们可以得到预处理得到的结果和输入网络前的张量的大小。如果需要对比预处理是否相同,可以自己读指针,然后打印数据出来跟python版的数据做对比。

terminate called after throwing an instance of 'Ort::Exception' what(): not enough space: expected 1228800, got 409600 :明显的报错,空间不够。这是因为传入的p_data_element_count*shape 的乘积大小不相同,所以导致了报错,传入的参数为102400307200 ,再乘上float类型占用的4byte,有4096001228800 ,所以商(3)代表少了三个通道,给p_data_element_count 乘上3即可。

incomplete type is not allowed :需要先定义一个type,才能在后续使用这个标识符的时候知道它是啥。这里报错是因为OrtSession这个类(标识符)不存在,需要变成指针类型才可以,即OrtSession*类型。

02

error: use of deleted function ‘Ort::Session::Session(const Ort::Session&)’ :使用了被删除的拷贝构造函数,所以不能把它当作变量传到某一个函数中,编译器就会报错(为什么会删除呢?参考我的02)。那我们总不可能不用函数吧?在3501行,注释中说:

Prevent users from accidentally copying the API structure, it should always be passed as a pointer.

所以我们应该传入一个指针。从类名往上溯源,发现一个Base 类,它有一个函数:operator T*() { return p_; } ,这个是类型转换的函数,所以我们只需要把Session转换成它的包装类即可,即

1
getOutputInfo((OrtSession*) session);

a nonstatic member reference must be relative to a specific object :函数不是静态函数,所以必须通过方法访问。

Android

安卓平台和Linux平台的实现基本上都是一样的,参考我的两个demo:https://github.com/BangwenHe/ORTSegDemo和https://github.com/BangwenHe/ort-snake-cpp。主要的步骤就是编写cmake,通过cmake把预编译的so库链接到项目中。安卓上更需要考虑怎么处理JNI,以及编译的问题。

JNI的影响是jstring到string,long到jlong。

编译:由于java版本和c++版本都有libonnxruntime.so 这个文件,所以如果打包到一起,会导致文件重复而报错。

我写了一个 OnnxRuntime 手机端推理的 Demo,OrtSegDemo。只要能够导出 ONNX 文件,就可以用它在 CPU 上实现推理,GPU 不一定能够成功。下面是用法:

1280X1280.PNG

需要修改的地方就是上面三个箭头指向的地方:

  1. 把导出的 ONNX 模型放到 raw 文件夹中,注意文件名只能由字母、数字和下划线组成
  2. 添加一个新的 ResId 变量,例如导出的文件名叫做 e2ec_sim.onnx ,那么可以加上一条新的语句:int e2ecId = R.raw.e2ec_sim;
  3. 修改第 54 行的传入参数,将 tinyposePath 修改成 e2ecId
  4. 编译运行整个项目

下面是我的运行结果:

7f108cf0-91dd-43ca-943c-b1ef133b04b7.png

可以看到,最后会显示一个平均延时。


ORT 模型部署
http://hebangwen.github.io/2024/03/12/Deploy-ORT-model/
作者
何榜文
发布于
2024年3月12日
许可协议