
使用TensorRT能够有效加速tensorflow模型的推理,同时C++相比于其他语言要更加的高效,因此在追求模型推断速度时,用TensorRT的C++APi来部署tensorflow模型是一种不错的方式。本文旨在提供一个用TensorRT的C++APi来部署tensorflow模型的实例,讲解部署过程中可能遇到的问题和处理方法,以提取inception_resnet_v2最后全局平均层输出特征为例。TensorRT版本5.1.5
准备pb模型
images
首先构建模型,然后load权重,之后调用tf.graph_util.convert_variables_to_constants函数将模型持久化,其中outputs是需要作为输出的tensor的列表,最后用pb_graph.SerializeToString()将图序列化并写入到pb文件当中,这样就生成了pb模型。
生成uff文件
有了pb模型,需要将其转换为tensorRT可用的uff模型,只需调用uff自带的convert脚本即可
python /usr/lib/python2.7/site-packages/uff/bin/convert_to_uff.py pbmodel_name.pb
如转换成功会输出如下信息
用C++API调用模型
调用uff模型的代码主要参考TensorRT官方的samples中的sampleUffMNIST代码,下面我会主要讲解其中几个重要的部分
TensorRT要进行推断首先需要定义网络结构,然后生成Cuda engine,从uff生成engine的代码如下,输入uff文件名和输入输出op的名字就能生成engine
ICudaEngine
首先要定义一个IBuilder* builder,以及一个用来解析uff文件的parser,之后通过builder创建network, parser会将uff中定义的网络结构和权值解析出来放到network当中,需要注意的是uffparser解析时需要先给定网络输入输出输出的节点。之后就能够用builder根据network中的结构生成 ICudaEngine* engine。生成engine前我们需要定义最大的batchsize数,在之后使用engine时输入的batchsize不能超过这个数值否则就会出错,这个值最好设置的和实际使用时的batch size数一致,这时候tensorRT的效率最高,输入batchsize小于最大batchsize时不能达到最高的效率。另外还需指定一个工作空间的最大大小。
生成engine之后就可以开始进行推断,在推断前首先需要生成一个执行上下文IExecutionContext* context,直接用engine->createExecutionContext()获得。执行推断的代码如下
std
总体来说有三个步骤,首先在设备(显卡)上开辟内存用来装网络的输入和输出,地址存在buffer当中。输出部分直接按照输出大小开辟空间,输入部分通过createCudaBuffer开辟内存并且将输入的数据拷贝到设备上去。之后可以调用context->execute(batchSize,&buffers[0])来执行推断的运算,最后通过verifyOutput函数将设备上的输出结果拷贝出来到。createCudabuffer代码如下
void* createCudaBuffer(int64_t eltCount, DataType dtype,const std::vector<cv::Mat>& srcs)
{
/* in that specific case, eltCount == INPUT_H * INPUT_W */
int batchSize = srcs.size();
assert(eltCount ==batchSize * INPUT_H * INPUT_W * INPUT_C);
assert(elementSize(dtype) == sizeof(float));
std::cerr<<"start to generate input"<<std::endl;
size_t memSize = eltCount * elementSize(dtype);
float* inputs = new float[eltCount];
int threadNum=batchSize;
std::thread threads[threadNum];
//uint8_t fileData[batchSize * INPUT_H * INPUT_W * INPUT_C];
int part = batchSize/threadNum;
if(batchSize%threadNum!=0) part=part+1;
for(int i =0;i<threadNum;i++){
threads[i]=std::thread(preprocess,i*part,min(i*part+part,batchSize),std::ref(srcs),inputs);
}
for(int i =0;i<threadNum;i++)
threads[i].join();
void* deviceMem = safeCudaMalloc(memSize);
CHECK(cudaMemcpy(deviceMem, inputs, memSize, cudaMemcpyHostToDevice));
//std::cerr<<"finish to generate input"<<std::endl;
delete[] inputs;
return deviceMem;
}
将图片进行预处理后存到数组inputs中然后将inputs中内容拷贝到设备上(预处理部分采用多线程加速)。verifyOutput代码和sampleUffMNIST示例中类似,将设备内存上网络的输出结果拷贝出来,这样就是执行了一次完整推断。加速效果在不同GPU上有所不同,在Tesla M40上相比于tensorflow中推断可以稳定获得3倍的速度提升
简单的插件层示例
如果tensorflow模型中有一些TensorRT不支持的操作,那么要成功的在TensorRT中调用模型就需要自行编写这些操作的实现作为TensorRT的插件,我以Sign取符号函数为例讲解一些TensorRT插件的编写
class
首先要定义Sign插件从IPluginV2继承,代码中列出来继承后需要实现的类方法,核心函数enqueue中调用了实现sign功能的cuda核函数进行计算。
为了能让TensorRT的uffparser能认出我们编写的层,我们还需要定义一个Creator用来创建插件层示例代码如下
class
完成这些之后我们就可以在TensorRt中使用Sign操作了,在网络定义中直接调用Sign可以通过如下代码实现,先定义一个Sign操作,然后通过addPluginV2添加到网络中
auto sl = Sign();
auto sign_layer=network->addPluginV2(input,1,sl);
如果想直接让uffparser直接解析含有Sign操作的网络则需要在转换uff文件是进行一些步骤。
out, _ = inception_resnet_v2(images, is_training=False)
out = tf.sign(out)
存在warning说不能识别Sign操作,自动转换为名为Sign的插件(tensorRT默认起的名字实际没有对应op),我们需要告诉转换器在转换时将tensorflow的Sign操作转换为我们编写的Sign插件(Sign_TRT),因此需要编写一个config.py文件给uff转换器这部分知识可参考TensorRT samples sampleUffSSD
本例子中config.py如下
import graphsurgeon as gs
import tensorflow as tf
sign_node = gs.create_plugin_node("Sign", op="Sign_TRT", dtype=tf.float32)
namespace_plugin_map = {
"Sign": sign_node
}
def preprocess(dynamic_graph):
dynamic_graph.collapse_namespaces(namespace_plugin_map)
首先注册一个sign node然后给出一个映射将tensorflow图中的sign op对应到sign node上,告诉转换器按照映射将op转换为对应的tensorRT 层。按照如下方式调用
python /usr/lib/python2.7/site-packages/uff/bin/convert-to-uff pbmodel_name.
则输出变为如下结果
将Sign操作转化为了我们之前编写的插件Sign,之后按照之前步骤就能够正常用C++API调用uff文件进行推断。
心得体会
1.虽然用tensorRT能够缩短很多的GPU推断时间,但是部署的服务最终响应时间并不仅仅由GPU推断时间决定,整个流程中的数据预处理、数据的编解码都会存在影响,在其他部分也应该注意效率。一个例子就是前文中提到的多线程预处理batch图片,如果单线程操作就会在预处理的部分直接耗时很长,成为整个服务响应时间的瓶颈。
2. 生成pb模型、转换uff模型以及调用uffparser时register Input,output,这三个过程中输入输出节点的名字一定要注意保持一致,否则最终在parser进行解析时会出现错误。
3.运行程序出现cuda failure一般情况下是由于将内存数据拷贝到磁盘时出现了非法内存访问,注意检查buffer开辟的空间大小和拷贝过去数据的大小是否一致
4.tensorRT中插件其实有比较多种IPlugin,IPluginExt,IPluginV2,注意在添加自己写的层到网络时要用对应的函数addPlugin,addPluginExt,addPluginV2,否则有些默认调用的类方法时不会调用的,比如用addPlugin添加的层是不会调用configureWithFormat方法的。
参考资料
Nvidia TensorRT Samples
tensorrt-developer-guide
TensorRT API Docs