一些题外话:这篇博客源自于实际的项目经历,项目中我负责对各类模型在Qt系统上的部署,从Libtorch到Pytorch再到TensorFlow的模型部署,都浅浅走了一遍,不透彻但能跑通了。
整体介绍:以TensorFlow训练DenseNet121分类CIFAR10的应用场景为例,讲模型在C++环境下的TensorRT加速部署。
零. 环境配置
名称 | 版本号 |
---|---|
TensorRT | TensorRT-7.2.3.4.Windows10.x86_64.cuda-11.1.cudnn8.1 |
tensorflow-gpu | 2.9.1 |
C++ Compiler | MSVC/14.29.30133 |
CUDA | 11.1 |
cuDNN | 8.4.1 |
libtorch | libtorch-1.8.2+cu111 |
pytorch | torch1.12.0+cu113 |
tf2onnx | 1.11.1 |
opencv | opencv-3.4.13 |
keras | 2.9.0 |
h5py | 3.9.0 |
Windows | Windows 10 家庭中文版 19044.1889 |
OpenCV | 3.4.13 |
模型部署整体的流程如下图所示:
可以参考链接:使用 TensorFlow、ONNX 和 TensorRT 加速深度学习推理
一、模型训练及保存
参考这篇博客,训练一个基于keras.application中的DenseNet网络的、处理Cifar10的模型,保存为.hdf5
格式。
我们在经典的DenNet121网络前加了resize层,使得网络能接收CIFAR10数据集中32x32x3
的数据。代码如下:
1 | import matplotlib.pyplot as plt |
事实上,如果使用这个训练得到的cifar10.h5
模型来做下面的转换,在转到trt
引擎文件的时候会报错:
1 | [07/28/2022-12:54:39] [W] [TRT] onnx2trt_utils.cpp:220: Your ONNX model has been generated with INT64 weights, while TensorRT does not natively support INT64. Attempting to cast down to INT32. |
这是因为目前TensorRt的BUG:#974 (comment),不支持模型中的resize_image
操作。不支持的还有NonZero (op is not supported in TRT yet。)
刚才训练代码里使用的keras.backend.resize_images
这个方法使用的是 the nearest
model + half_pixel
+ round_prefer_ceil
。
一模一样的issue 。
解决方案:Lambda式子改成model.add(K.layers.Lambda(lambda x:tf.image.resize(x,[224,224])))
。
OK,使用Keras的Sequential模型,“搭”自己的网络很快,保存也方便。
二、模型冻结
hdf5模型是可以再次被训练的动态图,现将其冻结转换成pb文件,用于前向计算。
1 | import tensorflow as tf |
三、转onnx文件
使用tf2onnx.convert
命令将.pb
文件转为.onnx
文件:
1 | python -m tf2onnx.convert --input E:/cifar10.pb --inputs Input:0 --outputs Identity:0 --output E:/cifar10.onnx --opset 11 |
–inputs :模型输入层的名字 –outputs :模型输出层的名字
输入输出层的名字在冻结代码里可以输出出来。
生成的onnx文件可以在Netron网站进行可视化,查看网络结构。
此时onnx模型的输入向量维度可以通过netron看到是**float32[unk__1220,224,224,3]
**,格式是TF的NHWC.
四、生成优化引擎文件
1 | trtexec --onnx=cifar10.onnx --saveEngine=cifar10.trt --workspace=4096 --minShapes=Input:0:1x32x32x3 --optShapes=Input:0:1x32x32x3 --maxShapes=Input:0:50x32x32x3 --fp16 |
- onnx: 输入的onnx模型
- saveEngine:转换好后保存的tensorrt engine
- workspace:使用的gpu内存,有时候不够,需要手动增大点,单位是MB
- minShapes:动态尺寸时的最小尺寸,格式为NCHW,需要给定输入node的名字,
- optShapes:推理测试的尺寸,trtexec会执行推理测试,该shape就是测试时的输入shape
- maxShapes:动态尺寸时的最大尺寸,这里只有batch是动态的,其他维度都是写死的
- fp16:float16推理
五、数据预处理
我们的最终目的是使用引擎对数据进行前向推理。到第四章结束,我们就拿到了最终的“模型”即序列化的引擎文件,下面是对数据的预处理,即加载数据。(我是直接使用了这位佬根据官方MNIST数据集处理代码改写的CIFAR10代码,github链接)
为了满足动态批量的数据输入,可以利用Libtorch的DataLoader类。自定义我们的DataLoader类,只需要重写torch::data::dataset
的get和size方法。
这篇文章完全可以让你自学废对自定义数据类型的加载:Custom Data Loading using PyTorch C++ API
假设现在已经写好了CustomDataset
类,那么分批喂数据的代码大抵就可以是这样:
1 | // Make DataSet |
六、加载引擎文件
流程:
- 读取
.trt
文件到变量. - 通过
nvinfer1::createInferRuntime
创建runtime对象. - 调用runtime的
deserializeCudaEngine
方法反序列化.trt
文件得到engine对象. IExecutionContext* context = engine->createExecutionContext();
得到执行上下文对象context.
模型的推理就通过context的enqueueV2
方法实现。可以把前三步集合到一个方法中,名叫readTRTfile,方法返回一个engine
对象。
之所以不直接取到context后返回context,因为我们需要调用engine的方法查看模型的输入输出维度。
【要点】前文我们生成的模型(得到的pb亦或是pt文件)都是动态批量,得到动态输入的onnx,转为trt时指定了之后推理输入的shape范围,注意只是范围,得到的trt经过deserialize得到engine,在调用engine时需要指定维度。如果没有指定或者维度不对则报错:
1 | [E] [TRT] Parameter check failed at: engine.cpp::nvinfer1::rt::ShapeMachineContext::resolveSlots::1318, condition: allInputDimensionsSpecified(routine) |
1 | //查看engine的输入输出维度 |
以DenseNet121的trt文件为例,以上程序输出
1 | index 0, dims: (-1,224,224,3) |
所以我们得把输入的动态维度写死,在python里,在调用engine推理前做这样的设置即可:context.set_binding_shape(0, (BATCH, 3, INPUT_H, INPUT_W))
,C++代码里应该调用IExecutionContext类型的实例的setBindingDimensions(int bindingIndex, Dims dimensions)方法。
1 | //确定动态维度 |
然后再执行推理就可以了。
总体思路是:拿到一个对维度未知的模型engine文件后,首先读入文件内容并做deserialize获得engine。
然后调用getBindingDimensions()查看engine的输入输出维度(如果知道维度就不用)。
在调用context->executeV2()做推理前把维度值为-1的动态维度值替换成具体的维度并调用context->setBindingDimensions()设置具体维度,然后在数据填入input buffer准备好后调用context->executeV2()做推理即可:
为什么是V2,V1V2有什么区别:
execute/enqueue are for implicit batch networks, and executeV2/enqueueV2 are for explicit batch networks. The V2 versions don’t take a batch_size argument since it’s taken from the explicit batch dimension of the network / or from the optimization profile if used.
In TensorRT 7, the ONNX parser requires that you create an explicit batch network, so you’ll have to use V2 methods.
到这里,我们通过readTRTfile函数得到了engine对象,通过engine得到了context对象,然后确定了context输入的动态维度。
七、执行推理
写一个doinference的方法,传入输入和输出数据数组。前文写的DataLoader每批得到的数据都是torch::tensor
向量,
cudaMalloc
开辟GPU内存。cudaMemcpyAsync
将批数据传给GPU。- 调用
context.enqueueV2
执行推理。 cudaMemcpyAsync
将批数据传回CPU。
大致分为这四步。
程序运行结果:
1 | (TrtInfer::testAllSample) test_dataset_size0 |
代码之后贴出来…笔记推了好久好久,之后继续更
可能遇到的错误:
onnx转trt
1 | [W] Dynamic dimensions required for input: input_1:0, but no shapes were provided. Automatically overriding shape to: 1x224x224x3 |
1 | [E] [TRT] input_1:0: for dimension number 1 in profile 0 does not match network definition (got min=3, opt=3, max=3), expected min=opt=max=224). |
1 | ERROR: builtin_op_importers.cpp:2593 In function importResize: |
1 | [E] [TRT] C:\source\rtSafe\cuda\cudaConvolutionRunner.cpp (483) - Cudnn Error in nvinfer1::rt::cuda::CudnnConvolutionRunner::executeConv: 2 (CUDNN_STATUS_ALLOC_FAILED) |
【Could not load library cudnn_cnn_infer64_8.dll. Error code 1455.Please make sure cudnn_cnn_infer64_8.dll is in your library path! 】
or 【context null】
原因:内存不足,重启VS或者电脑就OK。(或者参考此问答)
安装chocolatey。
Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; iex ((New-Object System.Net.WebClient).DownloadString('https://chocolatey.org/install.ps1'))
https://zhuanlan.zhihu.com/p/108833705
https://techwizard.cloud/2019/04/13/powershell-tip-exception-calling-downloadstring-with-1-arguments/#:~:text=throwing%20below%20error%3A-,Exception%20calling%20%E2%80%9CDownloadString%E2%80%9D%20with%20%E2%80%9C1%E2%80%9D%20argument(s,script%20will%20resolve%20this%20issue.
添加seuic源:
1 | $ choco source add -n=seuic -s"http://choco.seuic.info/nuget/" |
然后choco install fcgiwrap
,提示下载失败,源没有这个包。。。。焯
iPadGoodNotes+GoogleDrive+WindowsNotion
需求:在iPad上学习、批注文献,电脑端使用Notion对文献集中管理。文献要实时同步,电脑端要能看到最新的批注情况。
探索了一段时间,尝试过Foxit、Notability、GoodNotes、PDFViewer、iCloud甚至百度云,但综合价格、批注习惯和生态等因素,用GoodNotes+GoogleDrive对文献进行同步是相对最适合我的(0$哈哈哈)。
文献同步
GoodNotes开启GoogleDrive备份。Notes中所有内容会被同结构地备份到GoogleDrive下自动生成的GoodNotes文件夹中,至此实现了文件的云端备份。
顺其自然的,在电脑端直接浏览GoogleDrive里的文献就是最新批注的。
使用网页版GoogleDrive体验不如桌面版的,缓冲时间很不友好,有时网络问题甚至可能打不开了,因此:
下载GoogleDrive Desktop,并将同步模式设置为镜像模式。此时你就可以指定一个目录镜像地存放Drive中所有文件。
之所以不使用Stream这种节省空间的模式是因为他会把文件挂到xx/My Drive/
下,路径中有个带空格的My Drive!路径带空格根本忍不了,甚至如果你没有把系统改成英文的(如果你是家庭版Windows还不能改成英文系统!),安装下来的GoogleDrive只会是中文版的,他会把文件挂到xx/我的云盘/
下,路径带中文!关键这个路径名不能被更改。

我选择自定义的文件路径为:E:/Google/Drive/
,那我GoodNotes里的论文就在本地的E:/Google/Drive/GoodNotes/
文件夹下同步存在着。GoodNotes修改论文后自动同步到Drive里,电脑的Drive自动同步后,点击E:/Google/Drive/GoodNotes/xxx.pdf
看到的就是最新的批注论文。
但是在电脑端对GoodNotes文件夹下的pdf修改后,GoodNotes是看不到修改的,并且GoodNotes对其修改后会覆盖掉。这个问题我这个方案是无解的,没办法,这受限于GoodNotes操作的文件本质是GoodNotes File而非PDF,鱼和熊掌不可兼得。
文献管理
使用Notion管理文献,需要在使用Notion时跳转到最新的批注文献(本地),比如上文中的E:/Google/Drive/GoodNotes/xxx.pdf
,但是非会员限制单个文件<5MB,会员好贵的!!!而且就真的是上传上去了,不能同步更新了。所以通过在Notion中嵌入本地文件链接,直接通过链接打开文件。使用Ngnix。
下载并配置Nginx
下载地址,我选择的是Stable的nginx/Windows-1.22.1。下载并解压。
双击执行文件夹中nginx.exe,浏览器中输入并转到localhost,正常会显示Nginx的欢迎界面。
下面配置安装目录下的conf/nginx.conf
,主要是配置location。打开后在原server的location字段下添加新的location字段:
1 | location /goodnotes/ { |
这里alias和root是有区别的,我用了alias,root与alias主要区别在于nginx如何解释location后面的uri,这使两者分别以不同的方式将请求映射到服务器文件上。参考nginx配置静态资源访问
然后终端中输入nginx -s stop
停止服务后start nginx
开启服务。
按说
nginx -s reload
就可以起到更新conf文件后重新加载nginx服务的作用,但是我这边实践证明reload多少有问题,conf没有得到更新还是老内容,所以还是先stop再start了。参考了解决nginx退出后却依然能访问页面的问题
tasklist /fi "IMAGENAME eq nginx.exe"
查看所有运行了的Nginx进程taskkill /f /pid 16708
杀死PID16708的进程
此时浏览器中输入localhost/goodnotes/Interpretable/xxx.pdf
就可以打开本机的E:/Google/Drive/GoodNotes/Interpretable/xxx.pdf
了。
设置开机自启动nginx服务:右键nginx.exe生成快捷方式,将快捷方式剪切到系统的启动文件夹下。(Win+R输入shell:startup跳转过去)


至此,只实现了对pdf类型的跳转打开,期间尝试了Nginx - Shell Script CGI,但是没成功,网上大部分都是Linux或者苹果的博客。后续我会再试的。
参考文章:
在 Notion 中插入本地文件和目录链接
Nginx支持web界面执行bash|python等系统命令和脚本
nginx配置静态资源访问