天天看点

c++builder 运行网站的api_用TensorRT C++ API加速TensorFlow模型实例

c++builder 运行网站的api_用TensorRT C++ API加速TensorFlow模型实例

使用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++builder 运行网站的api_用TensorRT C++ API加速TensorFlow模型实例

用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)
           
c++builder 运行网站的api_用TensorRT C++ API加速TensorFlow模型实例

存在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.
           

则输出变为如下结果

c++builder 运行网站的api_用TensorRT C++ API加速TensorFlow模型实例

将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

继续阅读