深度學習演算法優化系列十七 | TensorRT介紹,安裝及如何使用?

1. 前言

由於前期OpenVINO的分享已經基本做完了,筆者也可以成功的在CPU和Intel神經棒上完整的部署一些工作了,因此開始來學習TensorRT啦。先聲明一下我使用的TensorRT版本是TensorRT-6.0.1.5

2. 介紹

TensorRT是NVIDIA推出的一個高性能的深度學習推理框架,可以讓深度學習模型在NVIDIA GPU上實現低延遲,高吞吐量的部署。TensorRT支援Caffe,TensorFlow,Mxnet,Pytorch等主流深度學習框架。TensorRT是一個C++庫,並且提供了C++ API和Python API,主要在NVIDIA GPU進行高性能的推理(Inference)加速。

Figure1. TensorRT是一個高性能的神經網路推理優化器和運行時引擎

當前,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。—維基百科

TensorRT是一個可編程的推理加速器

ONNX/TensorFlow/Custom Framework等模型的工作方式如下:

ONNX Workflow V1

3. TensorRT支援的Layer

3.1 Caffe

這些是Caffe框架中支援的OP。

  • BatchNormalization。
  • BNLL。
  • Clip。
  • Concatenation。
  • Convolution。
  • Crop。
  • Deconvolution。
  • Dropout。
  • ElementWise。
  • ELU。
  • InnerProduct。
  • Input。
  • LeakyReLU。
  • LRN。
  • Permute。
  • Pooling。
  • Power。
  • Reduction。
  • ReLU,TanH,和Sigmoid。
  • Reshape。
  • SoftMax。
  • Scale。

3.2 TensorFlow

這些是TensorFlow中支援的OP。

  • Add, Sub, Mul, Div, Minimum and Maximum。
  • ArgMax。
  • ArgMin。
  • AvgPool。
  • BiasAdd。
  • Clip。
  • ConcatV2。
  • Const。
  • Conv2d。
  • ConvTranspose2D。
  • DepthwiseConv2dNative。
  • Elu。
  • ExpandDims。
  • FusedBatchNorm。
  • Identity。
  • LeakyReLU。
  • MaxPool。
  • Mean。
  • Negative, Abs, Sqrt, Recip, Rsqrt, Pow, Exp and Log。
  • Pad is supported if followed by one of these TensorFlow layers: Conv2D, DepthwiseConv2dNative, MaxPool, and AvgPool.
  • Placeholder
  • ReLU, TanH, and Sigmoid。
  • Relu6。
  • Reshape。
  • ResizeBilinear, ResizeNearestNeighbor。
  • Sin, Cos, Tan, Asin, Acos, Atan, Sinh, Cosh, Asinh, Acosh, Atanh, Ceil and Floor。
  • Selu。
  • Slice。
  • SoftMax。
  • Softplus。
  • Softsign。
  • Transpose。

3.3 ONNX

因為篇幅有限,就不列舉了,可以自己去看官方文檔。

除了上面列舉的層,如果我們的網路中有自定義的Layer,這個時候咋辦呢?TensorRT中有一個 Plugin 層,這個層提供了 API 可以由用戶自己定義TensorRT不支援的層。 如下圖所示:

TensorRT自定義層

4. 為什麼TensorRT能讓模型跑的快?

這一問題的答案就隱藏下面這張圖中:

TensorRT優化訓練好的神經網路模型以產生可部署的運行時推理引擎

從圖上可以看到,TensorRT主要做了下面幾件事,來提升模型的運行速度。

  • TensorRT支援FP16和INT8的計算。我們知道深度學習在訓練的時候一般是應用32位或者16位數據,TensorRT在推理的時候可以降低模型參數的位寬來進行低精度推理,以達到加速推斷的目的。這在後面的文章中是重點內容,筆者經過一周的研究,大概明白了TensorRT INT8量化的一些細節,後面會逐漸和大家一起分享討論。
  • TensorRT對於網路結構進行重構,把一些能夠合併的運算合併在了一起,針對GPU的特性做了優化。大家如果了解GPU的話會知道,在GPU上跑的函數叫Kernel,TensorRT是存在Kernel的調用的。在絕大部分框架中,比如一個卷積層、一個偏置層和一個reload層,這三層是需要調用三次cuDNN對應的API,但實際上這三層的實現完全是可以合併到一起的,TensorRT會對一些可以合併網路進行合併;再比如說,目前的網路一方面越來越深,另一方面越來越寬,可能並行做若干個相同大小的卷積,這些卷積計算其實也是可以合併到一起來做的。(加粗的話轉載自參考鏈接1)。
  • 然後Concat層是可以去掉的,因為TensorRT完全可以實現直接接到需要的地方。
  • Kernel Auto-Tuning:網路模型在推理計算時,是調用GPU的CUDA核進行計算的。TensorRT可以針對不同的演算法,不同的網路模型,不同的GPU平台,進行 CUDA核的調整,以保證當前模型在特定平台上以最優性能計算。
  • Dynamic Tensor Memory 在每個tensor的使用期間,TensorRT會為其指定顯示記憶體,避免顯示記憶體重複申請,減少記憶體佔用和提高重複使用效率。
  • 不同的硬體如P4卡還是V100卡甚至是嵌入式設備的卡,TensorRT都會做優化,得到優化後的engine。

下面是一個原始的Inception Block,首先input後會有多個卷積,卷積完後有BiasReLU,結束後將結果concat到一起,得到下一個input。我們一起來看一下使用TensorRT後,這個原始的計算圖會被優化成了什麼樣子。

首先,在沒有經過優化的時候Inception Block如Figure1所示:

Figure1,原始的Inception Block

第二步,對於網路結構進行垂直整合,即將目前主流神經網路的conv、BN、Relu三個層融合為了一個層,所謂CBR,合併後就成了Figure2中的結構。

Figure2,垂直Fuse

第三步,TensorRT還可以對網路做水平組合,水平組合是指將輸入為相同張量和執行相同操作的層融合一起,下面的Figure3即是將三個相連的的CBR為一個大的的CBR。

Figure3,水平Fuse

最後,對於concat層,將contact層的輸入直接送入下面的操作中,不用單獨進行concat後在輸入計算,相當於減少了一次傳輸吞吐,然後就獲得了如Figure4所示的最終計算圖。

Figure4,最終計算圖

除了計算圖和底層優化,最重要的就是低精度推理了,這個後面會細講的,我們先來看一下使用了INT8低精度模式進行推理的結果展示:包括精度和速度。來自NIVIDA提供的PPT。

TensorRT INT8量化在主流網路上的精度對比

TensorRT INT8量化在主流網路上的速度對比

5. TensorRT的安裝

我是用的是TensorRT-6.0.1.5,由於我在Windows10上使用的,所以就去TensorRT官網https://developer.nvidia.com/tensorrt下載TensorRT6的Windows10安裝包,這裡我選擇了Cuda9.0的包,這也就意味著我必須要安裝Cudnn7.5及其以上,我這裡選擇了Cudnn7.6進行了安裝。關於Cuda和Cudnn的安裝就不說了,非常簡單。安裝TensorRT具體有以下步驟:

  1. 下載TensorRT-6.0.1.5安裝包並解壓。
  2. 將lib文件夾下面的dll(如下圖所示,)都拷貝到cuda文件夾下的bin目錄下,我是默認安裝了cuda9.0,所以我的cuda下的bin目錄的路徑是:C:Program FilesNVIDIA GPU Computing ToolkitCUDAv9.0bin

TensorRT lib目錄

  1. 然後這就安裝成功了,為了驗證你有沒有成功,請使用VSCode 2015/2017打開sample_mnist.sln解決法案,我的目錄是:F:TensorRT-6.0.1.5samplessampleMNIST
  2. 打開VS工程屬性,將目標平台版本改成8.1以及平台工具及改成Visual Studio 2015(v140)。然後用Release編譯,這樣你就會在F:TensorRT-6.0.1.5bin下面生成一個sample_mnist.exe了。

sampleMNIST工程屬性

  1. 進入F:TensorRT-6.0.1.5datamnist文件夾,打開裡面的README.md,下載MNIST數據集到這個文件夾下並解壓,實際上只用下載train-images-idx3-ubyte.gztrain-labels-idx1-ubyte.gz就可以了。然後執行generate_pgms.py這個python文件,就會在這個文件夾下獲得0-9.pgm10張圖片,數字分別是0-9
  2. 打開命令行測試一下上面的demo。命令如下:
>F:TensorRT-6.0.1.5binsample_mnist.exe --datadir=F:TensorRT-6.0.1.5datamnist  

獲得的結果如下:

2.pnm得到的預測結果位3,預測正確

6. TensorRT使用流程

這裡先看一下TensorRT最簡單的使用流程,後面複雜的應用部署也是以這個為基礎的,在使用TensorRT的過程中需要提供以下文件(以Caffe為例):

  • 模型文件(*.prototxt)
  • 權重文件(*.caffemodel)
  • 標籤文件(把數據映射成name字元串)。

TensorRT的使用包括兩個階段,BuildDeployment

6.1 Build

Build階段主要完成模型轉換(從Caffe/TensorFlow/Onnx->TensorRT),在轉換階段會完成前面所述優化過程中的計算圖融合,精度校準。這一步的輸出是一個針對特定GPU平台和網路模型的優化過的TensorRT模型。這個TensorRT模型可以序列化的存儲到磁碟或者記憶體中。存儲到磁碟中的文件叫plan file,這個過程可以用下圖來表示:

Build

下面的程式碼展示了一個簡單的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大小和設置工作空間。其中設置工作空間是進行上面所述的計算圖融合優化的必須步驟。

6.2 Deployment

Deploy階段就是完成前向推理過程了,上面提到的Kernel Auto-Tuning 和 Dynamic Tensor Memory應該是也是在這個步驟中完成的。這裡將Build過程中獲得的plan文件首先反序列化,並創建一個 runtime engine,然後就可以輸入數據,然後輸出分類向量結果或檢測結果。這個過程可以用下圖來表示:

Deployment

下面的程式碼展示了一個簡單的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);  

6.3 TensorRT 6.0 的Deployment

隨著TensorRT的版本迭代,前向推理的程式碼變成越來越簡單,基本上不需要我們操心了,我們來感受一下Mnist數字識別網路的推理程式碼。

bool SampleMNIST::infer()  {      // Create RAII buffer manager object      samplesCommon::BufferManager buffers(mEngine, mParams.batchSize);        auto context = SampleUniquePtr<nvinfer1::IExecutionContext>(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;  }  

7. 使用了TensorRT的優化方式效果

使用tensorRT與使用CPU相比,獲得了40倍的加速,與使用TensorFlow在GPU上推理相比,獲得了18倍的加速。

8. 總結

這篇是我的第一篇講解TensorRT的文章,主要介紹了TensorRT,以及如何安裝和使用TensorRT,之後會用例子來更詳細的講解。謝謝大家閱讀到這裡啦。

9. 參考

  • https://blog.csdn.net/qq_33869371/article/details/87929419
  • https://arleyzhang.github.io/articles/7f4b25ce/
  • http://on-demand.gputechconf.com/gtc/2017/presentation/s7310-8-bit-inference-with-tensorrt.pdf