由于前期OpenVINO的分享已经基本做完了,笔者也可以成功的在CPU和Intel神经棒上完整的部署一些工作了,因此开始来学习TensorRT啦。先声明一下我使用的TensorRT版本是TensorRT-6.0.1.5 。
TensorRT是NVIDIA推出的一个高性能的深度学习推理框架,可以让深度学习模型在NVIDIA GPU上实现低延迟,高吞吐量的部署。TensorRT支持Caffe,TensorFlow,Mxnet,Pytorch等主流深度学习框架。TensorRT是一个C++库,并且提供了C++ API和Python API,主要在NVIDIA GPU进行高性能的推理(Inference)加速。
当前,TensorRT6.0已经支持了很多深度学习框架如Caffe/TensorFlow/Pytorch/MxNet。对于Caffe和TensorFlow的网络模型解析,然后与TensorRT中对应的层进行一一映射,然后TensorRT可以针对NVIDIA的GPU进行优化并进行部署加速。不过,对于Caffe2,Pytorch,MxNet,Chainer,CNTK等深度学习框架训练的模型都必须先转为ONNX的通用深度学习模型,然后对ONNX模型做解析。另外TensorFlow/MxNet/MATLAB都已经将TensorRT集成到框架中去了。
ONNX是一种针对机器学习所设计的开放式的文件格式,用于存储训练好的模型。它使得不同的人工智能框架(如Pytorch, MXNet)可以采用相同格式存储模型数据并交互。 ONNX的规范及代码主要由微软,亚马逊 ,Facebook 和 IBM 等公司共同开发,以开放源代码的方式托管在Github上。目前官方支持加载ONNX模型并进行推理的深度学习框架有: Caffe2, PyTorch, MXNet,ML.NET,TensorRT 和 Microsoft CNTK,并且 TensorFlow 也非官方的支持ONNX。—维基百科
ONNX/TensorFlow/Custom Framework等模型的工作方式如下:
这些是Caffe框架中支持的OP。
这些是TensorFlow中支持的OP。
因为篇幅有限,就不列举了,可以自己去看官方文档。
除了上面列举的层,如果我们的网络中有自定义的Layer,这个时候咋办呢?TensorRT中有一个 Plugin 层,这个层提供了 API 可以由用户自己定义TensorRT不支持的层。 如下图所示:
这一问题的答案就隐藏下面这张图中:
从图上可以看到,TensorRT主要做了下面几件事,来提升模型的运行速度。
下面是一个原始的Inception Block,首先input
后会有多个卷积,卷积完后有Bias
和ReLU
,结束后将结果concat
到一起,得到下一个input
。我们一起来看一下使用TensorRT后,这个原始的计算图会被优化成了什么样子。
首先,在没有经过优化的时候Inception Block如Figure1所示:
第二步,对于网络结构进行垂直整合,即将目前主流神经网络的conv、BN、Relu三个层融合为了一个层,所谓CBR,合并后就成了Figure2中的结构。
第三步,TensorRT还可以对网络做水平组合,水平组合是指将输入为相同张量和执行相同操作的层融合一起,下面的Figure3即是将三个相连的 1 × 1 1\times 1 1×1的CBR为一个大的 1 × 1 1\times 1 1×1的CBR。
最后,对于concat层,将contact层的输入直接送入下面的操作中,不用单独进行concat后在输入计算,相当于减少了一次传输吞吐,然后就获得了如Figure4所示的最终计算图。
除了计算图和底层优化,最重要的就是低精度推理了,这个后面会细讲的,我们先来看一下使用了INT8低精度模式进行推理的结果展示:包括精度和速度。来自NIVIDA提供的PPT。
我是用的是TensorRT-6.0.1.5,由于我在Windows10上使用的,所以就去TensorRT官网https://developer.nvidia.com/tensorrt
下载TensorRT6的Windows10安装包,这里我选择了Cuda9.0的包,这也就意味着我必须要安装Cudnn7.5及其以上,我这里选择了Cudnn7.6进行了安装。关于Cuda和Cudnn的安装就不说了,非常简单。安装TensorRT具体有以下步骤:
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v9.0\bin
。sample_mnist.sln
解决法案,我的目录是:F:\TensorRT-6.0.1.5\samples\sampleMNIST
。F:\TensorRT-6.0.1.5\bin
下面生成一个sample_mnist.exe
了。F:\TensorRT-6.0.1.5\data\mnist
文件夹,打开里面的README.md
,下载MNIST数据集到这个文件夹下并解压,实际上只用下载train-images-idx3-ubyte.gz
和train-labels-idx1-ubyte.gz
就可以了。然后执行generate_pgms.py
这个python文件,就会在这个文件夹下获得0-9.pgm
10张图片,数字分别是0-9
。>F:\TensorRT-6.0.1.5\bin\sample_mnist.exe --datadir=F:\TensorRT-6.0.1.5\data\mnist
获得的结果如下:
这里先看一下TensorRT最简单的使用流程,后面复杂的应用部署也是以这个为基础的,在使用TensorRT的过程中需要提供以下文件(以Caffe为例):
TensorRT的使用包括两个阶段,Build
和Deployment
。
Build阶段主要完成模型转换(从Caffe/TensorFlow/Onnx->TensorRT),在转换阶段会完成前面所述优化过程中的计算图融合,精度校准。这一步的输出是一个针对特定GPU平台和网络模型的优化过的TensorRT模型。这个TensorRT模型可以序列化的存储到磁盘或者内存中。存储到磁盘中的文件叫plan file
,这个过程可以用下图来表示:
下面的代码展示了一个简单的Build过程。注意这里的代码注释是附录的第2个链接提供的,TensorRT版本是2.0,。然后我观察了下TensorRT6.0的代码,虽然接口有一些变化但Build->Deployment这个流程是通用,所以就转载它的代码解释来说明问题了。
//创建一个builder
IBuilder* builder = createInferBuilder(gLogger);
// parse the caffe model to populate the network, then set the outputs
// 创建一个network对象,不过这时network对象只是一个空架子
INetworkDefinition* network = builder->createNetwork();
//tensorRT提供一个高级别的API:CaffeParser,用于解析Caffe模型
//parser.parse函数接受的参数就是上面提到的文件,和network对象
//这一步之后network对象里面的参数才被填充,才具有实际的意义
CaffeParser parser;
auto blob_name_to_tensor = parser.parse(“deploy.prototxt”,
trained_file.c_str(),
*network,
DataType::kFLOAT);
// 标记输出 tensors
// specify which tensors are outputs
network->markOutput(*blob_name_to_tensor->find("prob"));
// Build the engine
// 设置batchsize和工作空间,然后创建inference engine
builder->setMaxBatchSize(1);
builder->setMaxWorkspaceSize(1 << 30);
//调用buildCudaEngine时才会进行前述的层间融合或精度校准优化方式
ICudaEngine* engine = builder->buildCudaEngine(*network);
在上面的代码中使用了一个高级API:CaffeParser,直接读取 caffe的模型文件,就可以解析,也就是填充network对象。解析的过程也可以直接使用一些低级别的C++API,例如:
ITensor* in = network->addInput(“input”, DataType::kFloat, Dims3{…});
IPoolingLayer* pool = network->addPooling(in, PoolingType::kMAX, …);
解析了Caffe的模型之后,必须要指定输出Tensor,设置batch大小和设置工作空间。其中设置工作空间是进行上面所述的计算图融合优化的必须步骤。
Deploy阶段就是完成前向推理过程了,上面提到的Kernel Auto-Tuning 和 Dynamic Tensor Memory应该是也是在这个步骤中完成的。这里将Build过程中获得的plan文件首先反序列化,并创建一个 runtime engine,然后就可以输入数据,然后输出分类向量结果或检测结果。这个过程可以用下图来表示:
下面的代码展示了一个简单的Deploy过程,这里没有包含反序列化和测试时的batch流的获取。可以看到代码还是相当复杂的,特别是包含了一些CUDA编程的知识。
// The execution context is responsible for launching the
// compute kernels 创建上下文环境 context,用于启动kernel
IExecutionContext *context = engine->createExecutionContext();
// In order to bind the buffers, we need to know the names of the
// input and output tensors. //获取输入,输出tensor索引
int inputIndex = engine->getBindingIndex(INPUT_LAYER_NAME),
int outputIndex = engine->getBindingIndex(OUTPUT_LAYER_NAME);
//申请GPU显存
// Allocate GPU memory for Input / Output data
void* buffers = malloc(engine->getNbBindings() * sizeof(void*));
cudaMalloc(&buffers[inputIndex], batchSize * size_of_single_input);
cudaMalloc(&buffers[outputIndex], batchSize * size_of_single_output);
//使用cuda 流来管理并行计算
// Use CUDA streams to manage the concurrency of copying and executing
cudaStream_t stream;
cudaStreamCreate(&stream);
//从内存到显存,input是读入内存中的数据;buffers[inputIndex]是显存上的存储区域,用于存放输入数据
// Copy Input Data to the GPU
cudaMemcpyAsync(buffers[inputIndex], input,
batchSize * size_of_single_input,
cudaMemcpyHostToDevice, stream);
//启动cuda核计算
// Launch an instance of the GIE compute kernel
context.enqueue(batchSize, buffers, stream, nullptr);
//从显存到内存,buffers[outputIndex]是显存中的存储区,存放模型输出;output是内存中的数据
// Copy Output Data to the Host
cudaMemcpyAsync(output, buffers[outputIndex],
batchSize * size_of_single_output,
cudaMemcpyDeviceToHost, stream));
//如果使用了多个cuda流,需要同步
// It is possible to have multiple instances of the code above
// in flight on the GPU in different streams.
// The host can then sync on a given stream and use the results
cudaStreamSynchronize(stream);
随着TensorRT的版本迭代,前向推理的代码变成越来越简单,基本上不需要我们操心了,我们来感受一下Mnist数字识别网络的推理代码。
bool SampleMNIST::infer()
{
// Create RAII buffer manager object
samplesCommon::BufferManager buffers(mEngine, mParams.batchSize);
auto context = SampleUniquePtr(mEngine->createExecutionContext());
if (!context)
{
return false;
}
// Pick a random digit to try to infer
srand(time(NULL));
const int digit = rand() % 10;
// Read the input data into the managed buffers
// There should be just 1 input tensor
assert(mParams.inputTensorNames.size() == 1);
if (!processInput(buffers, mParams.inputTensorNames[0], digit))
{
return false;
}
// Create CUDA stream for the execution of this inference.
cudaStream_t stream;
CHECK(cudaStreamCreate(&stream));
// Asynchronously copy data from host input buffers to device input buffers
buffers.copyInputToDeviceAsync(stream);
// Asynchronously enqueue the inference work
if (!context->enqueue(mParams.batchSize, buffers.getDeviceBindings().data(), stream, nullptr))
{
return false;
}
// Asynchronously copy data from device output buffers to host output buffers
buffers.copyOutputToHostAsync(stream);
// Wait for the work in the stream to complete
cudaStreamSynchronize(stream);
// Release stream
cudaStreamDestroy(stream);
// Check and print the output of the inference
// There should be just one output tensor
assert(mParams.outputTensorNames.size() == 1);
bool outputCorrect = verifyOutput(buffers, mParams.outputTensorNames[0], digit);
return outputCorrect;
}
这篇是我的第一篇讲解TensorRT的文章,主要介绍了TensorRT,以及如何安装和使用TensorRT,之后会用例子来更详细的讲解。谢谢大家阅读到这里啦。
欢迎关注GiantPandaCV, 在这里你将看到独家的深度学习分享,坚持原创,每天分享我们学习到的新鲜知识。( • ̀ω•́ )✧
有对文章相关的问题,或者想要加入交流群,欢迎添加BBuf微信: