原生推理庫Paddle Inference

原生推理庫Paddle Inference

https://www.paddlepaddle.org.cn/tutorials/projectdetail/3949123

 

深度學習一般分爲訓練和推理兩個部分,訓練是神經網絡“學習”的過程,主要關注如何搜索和求解模型參數,發現訓練數據中的規律,生成模型。有了訓練好的模型,就要在線上環境中應用模型,實現對未知數據做出推理,這個過程在AI領域叫做推理部署。用戶可以選擇如下四種部署應用方式之一:

  • 服務器端高性能部署:將模型部署在服務器上,利用服務器的高性能幫助用戶處理推理業務。
  • 模型服務化部署:將模型以線上服務的形式部署在服務器或者雲端,用戶通過客戶端請求發送需要推理的輸入內容,服務器或者雲通過響應報文將推理結果返回給用戶。
  • 移動端部署:將模型部署在移動端上,例如手機或者物聯網的嵌入式端。
  • Web端部署:將模型部署在網頁上,用戶通過網頁完成推理業務。

本節將會爲大家講解如何使有飛槳實現服務器端高性能部署。

在實際應用中,推理部署階段會面臨和訓練時完全不一樣的硬件環境,當然也對應着不一樣的計算性能要求。因此上線部署可能會遇到各種問題,例如上線部署的硬件環境和訓練時不同;推理計算耗時太長, 可能造成服務不可用;模型佔用內存過高無法上線。但是在實際業務中,我們訓練得到的模型,就是需要能在具體生產環境中正確、高效地實現推理功能,完成上線部署。 對工業級部署而言,要求的條件往往非常繁多而且苛刻,不是每個深度學習框架都對實際生產部署上能有良好的支持。一款對推理支持完善的的框架,會讓你的模型上線工作事半功倍。
飛槳作爲源於產業實踐的深度學習框架,在推理部署能力上有特別深厚的積累和打磨,提供了性能強勁、上手簡單的服務器端推理庫Paddle Inference,幫助用戶擺脫各種上線部署的煩惱。

Paddle Inference是什麼

飛槳框架的推理部署能力經過多個版本的升級迭代,形成了完善的推理庫Paddle Inference。Paddle Inference功能特性豐富,性能優異,針對不同平臺不同的應用場景進行了深度的適配優化,做到高吞吐、低時延,保證了飛槳模型在服務器端即訓即用,快速部署。

  • 主流軟硬件環境兼容適配
    支持服務器端X86 CPU、NVIDIA GPU芯片,兼容Linux/macOS/Windows系統。
  • 支持飛槳所有模型
    支持所有飛槳訓練產出的模型,真正即訓即用。
  • 多語言環境豐富接口可靈活調用
    支持C++, Python, C, Go和R語言API, 接口簡單靈活,20行代碼即可完成部署。可通過Python API,實現對性能要求不太高的場景快速支持;通過C++高性能接口,可與線上系統聯編;通過基礎的C API可擴展支持更多語言的生產環境。

【性能測一測】
通過比較ResNet50和BERT模型的訓練前向耗時和推理耗時,可以觀測到Paddle Inference有顯著的加速效果。

模型 CPU訓練前向耗時/ms CPU推理耗時/ms GPU訓練前向耗時/ms GPU推理耗時/ms
bert(21M) 54 23 43 1.5
Resnet50 236.780 49.604 8.47 4.49

說明:測試耗時的方法,使用相同的輸入數據先空跑1000次,循環運行1000次,每次記錄模型運行的耗時,最後計算出模型運行的平均耗時。

基於Paddle Inference的單機推理部署,即在一臺機器進行推理部署。相比Paddle Serving在多機多卡下進行推理部署,單機推理部署不產生多機通信與調度的時間成本,能夠最大程度地利用機器的Paddle Inference算力來提高推理部署的性能。對於擁有高算力機器,已有線上業務系統服務,期望加入模型推理作爲一個子模塊,且對性能要求較高的用戶,採用單機推理部署能夠充分利用計算資源,加速部署過程。

Paddle Inference高性能實現

內存/顯存複用提升服務吞吐量

在推理初始化階段,對模型中的OP輸出Tensor 進行依賴分析,將兩兩互不依賴的Tensor在內存/顯存空間上進行復用,進而增大計算並行量,提升服務吞吐量。

細粒度OP橫向縱向融合減少計算量

在推理初始化階段,按照已有的融合模式將模型中的多個OP融合成一個OP,減少了模型的計算量的同時,也減少了 Kernel Launch的次數,從而能提升推理性能。目前Paddle Inference支持的融合模式多達幾十個。

內置高性能的CPU/GPU Kernel

內置同Intel、Nvidia共同打造的高性能kernel,保證了模型推理高性能的執行。

子圖集成TensorRT加快GPU推理速度

Paddle Inference採用子圖的形式集成TensorRT,針對GPU推理場景,TensorRT可對一些子圖進行優化,包括OP的橫向和縱向融合,過濾冗餘的OP,併爲OP自動選擇最優的kernel,加快推理速度。

子圖集成Paddle Lite輕量化推理引擎

Paddle Lite 是飛槳深度學習框架的一款輕量級、低框架開銷的推理引擎,除了在移動端應用外,還可以使用服務器進行 Paddle Lite 推理。Paddle Inference採用子圖的形式集成 Paddle Lite,以方便用戶在服務器推理原有方式上稍加改動,即可開啓 Paddle Lite 的推理能力,得到更快的推理速度。並且,使用 Paddle Lite 可支持在百度崑崙等高性能AI芯片上執行推理計算。

支持加載PaddleSlim量化壓縮後的模型

PaddleSlim是飛槳深度學習模型壓縮工具,Paddle Inference可聯動PaddleSlim,支持加載量化、裁剪和蒸餾後的模型並部署,由此減小模型存儲空間、減少計算佔用內存、加快模型推理速度。其中在模型量化方面,Paddle Inference在X86 CPU上做了深度優化,常見分類模型的單線程性能可提升近3倍,ERNIE模型的單線程性能可提升2.68倍。

推理部署實戰

場景劃分

Paddle Inference應用場景,按照API接口類型可以分C++, Python, C, Go和R。Python適合直接應用,可通過Python API實現性能要求不太高的場景的快速支持;C++接口屬於高性能接口,可與線上系統聯編;C接口是基於C++,用於支持更多語言的生產環境。
不同接口的使用流程一致,但個別操作細節存在差異。其中,比較常見的場景是C++和Python。因此本教程以這兩類接口爲例,介紹如何使用Pdddle Inference API進行單機服務器的推理預測部署。

推理部署流程

使用Paddle Inference進行推理部署的流程如下所示。詳細API文檔請參考API文檔

  1. 配置推理選項。Config是飛槳提供的配置管理器API。在使用Paddle Inference進行推理部署過程中,需要使用Config詳細地配置推理引擎參數,包括但不限於在何種設備(CPU/GPU)上部署、加載模型路徑、開啓/關閉計算圖分析優化、使用MKLDNN/TensorRT進行部署的加速等。參數的具體設置需要根據實際需求來定。
  2. 創建PredictorPredictor是飛槳提供的推理引擎API。根據設定好的推理配置Config創建推理引擎Predictor,也就是推理引擎的一個實例。創建期間會進行模型加載、分析和優化等工作。
  3. 準備輸入數據。準備好待輸入推理引擎的數據,首先獲得模型中每個輸入的名稱以及指向該數據塊(CPU或GPU上)的指針,再根據名稱將對應的數據塊拷貝進Tensor。飛槳採用Tensor作爲輸入/輸出數據結構,可以減少額外的拷貝,提升推理性能。
  4. 調用Predictor.Run()執行推理。
  5. 獲取推理輸出。與輸入數據類似,根據輸出名稱將輸出的數據(矩陣向量)由Tensor拷貝至(CPU或GPU上)以進行後續的處理。
  6. 最後,獲取輸出並不意味着預測過程的結束,在一些特別的場景中,單純的矩陣向量不能讓使用者明白它有什麼意義。進一步地,我們需要根據向量本身的意義,解析數據,獲取實際的輸出。舉個例子,transformer 翻譯模型,我們將字詞變成向量輸入到預測引擎中,而預測引擎反饋給我們的,仍然是矩陣向量。但是這些矩陣向量是有意義的,我們利用這些向量去找翻譯結果所對應的句子,就完成了使用 transformer 翻譯的過程。
    以上操作的具體使用方法和示例會在下文給出。

前提準備

  1. 安裝或源碼編譯推理庫
    使用飛槳進行推理部署,需要使用與當前部署環境一致的Paddle推理庫。
    如果使用Python API,只需本地電腦成功安裝Paddle,安裝方法請參考快速安裝
    如果使用C++/C API,需要下載或編譯推理庫。推薦先從飛槳官網下載推理庫,下載請點擊推理庫。如果官網提供的推理庫版本無法滿足需求,或想要對代碼進行自定義修改,可以採用源碼編譯的方式獲取推理庫,推理庫的編譯請參考前文“源碼編譯”。

  2. 導出模型文件
    模型部署首先要有部署的模型文件。模型部署首先要有部署的模型文件。在動態圖模型訓練過程中或者模型訓練結束後,可以通過paddle.jit.save 接口來導出用於部署的標準化模型文件。而對於靜態圖模型,則可以使用paddle.static.save_inference_model保存模型。兩種方式都可以根據推理需要的輸入和輸出, 對訓練模型進行剪枝, 去除和推理無關部分, 得到的模型相比訓練時更加精簡, 適合進一步優化和部署。
    我們用一個簡單的例子來展示下導出模型文件的這一過程。

 

  1. 我們用一個簡單的例子來展示下導出模型文件的這一過程。

import numpy as np
import paddle
import paddle.nn as nn
import paddle.optimizer as opt

BATCH_SIZE = 16
BATCH_NUM = 4
EPOCH_NUM = 4

IMAGE_SIZE = 784
CLASS_NUM = 10

# define a random dataset
class RandomDataset(paddle.io.Dataset):
    def __init__(self, num_samples):
        self.num_samples = num_samples

    def __getitem__(self, idx):
        image = np.random.random([IMAGE_SIZE]).astype('float32')
        label = np.random.randint(0, CLASS_NUM - 1, (1, )).astype('int64')
        return image, label

    def __len__(self):
        return self.num_samples

class LinearNet(nn.Layer):
    def __init__(self):
        super(LinearNet, self).__init__()
        self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)

    @paddle.jit.to_static
    def forward(self, x):
        return self._linear(x)

def train(layer, loader, loss_fn, opt):
    for epoch_id in range(EPOCH_NUM):
        for batch_id, (image, label) in enumerate(loader()):
            out = layer(image)
            loss = loss_fn(out, label)
            loss.backward()
            opt.step()
            opt.clear_grad()
            print("Epoch {} batch {}: loss = {}".format(
                epoch_id, batch_id, np.mean(loss.numpy())))

# create network
layer = LinearNet()
loss_fn = nn.CrossEntropyLoss()
adam = opt.Adam(learning_rate=0.001, parameters=layer.parameters())

# create data loader
dataset = RandomDataset(BATCH_NUM * BATCH_SIZE)
loader = paddle.io.DataLoader(dataset,
    batch_size=BATCH_SIZE,
    shuffle=True,
    drop_last=True,
    num_workers=2)

# train
train(layer, loader, loss_fn, adam)

# save
path = "example.model/linear"
paddle.jit.save(layer, path)
W0511 10:17:34.233616   117 gpu_context.cc:244] Please NOTE: device: 0, GPU Compute Capability: 7.0, Driver API Version: 10.1, Runtime API Version: 10.1
W0511 10:17:34.238706   117 gpu_context.cc:272] device: 0, cuDNN Version: 7.6.
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/layers/utils.py:77: DeprecationWarning: Using or importing the ABCs from 'collections' instead of from 'collections.abc' is deprecated, and in 3.8 it will stop working
  return (isinstance(seq, collections.Sequence) and
Epoch 0 batch 0: loss = 2.361412286758423
Epoch 0 batch 1: loss = 2.555819511413574
Epoch 0 batch 2: loss = 2.3603224754333496
Epoch 0 batch 3: loss = 2.3046326637268066
Epoch 1 batch 0: loss = 2.0856692790985107
Epoch 1 batch 1: loss = 2.229004144668579
Epoch 1 batch 2: loss = 2.0080854892730713
Epoch 1 batch 3: loss = 2.091014862060547
Epoch 2 batch 0: loss = 2.35357666015625
Epoch 2 batch 1: loss = 2.4300479888916016
Epoch 2 batch 2: loss = 2.3404417037963867
Epoch 2 batch 3: loss = 2.212562084197998
Epoch 3 batch 0: loss = 2.0857276916503906
Epoch 3 batch 1: loss = 2.3606109619140625
Epoch 3 batch 2: loss = 2.3114867210388184
Epoch 3 batch 3: loss = 2.0755224227905273

該程序運行結束後,會在本目錄中生成一個example.model目錄,目錄中包含linear.pdmodel, linear.pdiparams 兩個文件,linear.pdmodel文件表示模型的結構文件,linear.pdiparams表示所有參數的融合文件。

 

基於C++ API的推理部署

爲了簡單方便地進行推理部署,飛槳提供了一套高度優化的C++ API推理接口。下面對各主要API使用方法進行詳細介紹。

API詳細介紹

在上述的推理部署流程中,我們瞭解到Paddle Inference預測包含了以下幾個方面:

  • 配置推理選項
  • 創建predictor
  • 準備模型輸入
  • 模型推理
  • 獲取模型輸出

那我們先用一個簡單的程序介紹這一過程:

std::unique_ptr<paddle_infer::Predictor> CreatePredictor() {
  // 通過Config配置推理選項
  paddle_infer::Config config;
  config.SetModel("./model/resnet50.pdmodel",
                     "./model/resnet50.pdiparams");
  config.EnableUseGpu(100, 0);
  config.EnableMKLDNN();
  config.EnableMemoryOptim();
  // 創建Predictor
  return paddle_infer::CreatePredictor(config);
}

void Run(paddle_infer::Predictor *predictor,
         const std::vector<float>& input,
         const std::vector<int>& input_shape, 
         std::vector<float> *out_data) {
  // 準備模型的輸入
  int input_num = std::accumulate(input_shape.begin(), input_shape.end(), 1, std::multiplies<int>());

  auto input_names = predictor->GetInputNames();
  auto input_t = predictor->GetInputHandle(input_names[0]);
  input_t->Reshape(input_shape);
  input_t->CopyFromCpu(input.data());
  
  // 模型推理
  CHECK(predictor->Run());
  
  // 獲取模型的輸出
  auto output_names = predictor->GetOutputNames();
  
  // there is only one output of Resnet50
  auto output_t = predictor->GetOutputHandle(output_names[0]);
  std::vector<int> output_shape = output_t->shape();
  int out_num = std::accumulate(output_shape.begin(), output_shape.end(), 1, std::multiplies<int>());
  out_data->resize(out_num);
  output_t->CopyToCpu(out_data->data());
}

以上的程序中CreatePredictor函數對推理過程進行了配置以及創建了Predictor。 Run函數進行了輸入數據的準備,模型推理以及輸出數據的獲取過程。

接下來我們依次對程序中出現的Config、模型輸入輸出和Predictor做一個詳細的介紹。

 

使用Config管理推理配置

Config管理Predictor的推理配置,提供了模型路徑設置、推理引擎運行設備選擇以及多種優化推理流程的選項。配置中包括了必選配置以及可選配置。

1. 必選配置

1-1 設置模型和參數路徑

  • 從文件加載:模型文件夾model_dir下有一個模型文件my.pdmodel和一個參數文件my.pdiparams時,傳入模型文件和參數文件路徑。使用方式爲:config->SetModel("./model_dir/my.pdmodel", "./model_dir/my.pdiparams");
  • 內存加載模式:如果模型是從內存加載,可以使用config->SetModelBuffer(model.data(), model.size(), params.data(), params.size())

2. 可選配置

2-1 加速CPU推理

// 開啓MKLDNN,可加速CPU推理,要求預測庫帶MKLDNN功能。
config->EnableMKLDNN();	  	  		
// 可以設置CPU數學庫線程數math_threads,可加速推理。
// 注意:math_threads * 外部線程數 需要小於總的CPU的核心數目,否則會影響預測性能。
config->SetCpuMathLibraryNumThreads(10); 

2-2 使用GPU推理

// EnableUseGpu後,模型將運行在GPU上。
// 第一個參數表示預先分配顯存數目,第二個參數表示設備的ID。
config->EnableUseGpu(100, 0); 

如果使用的預測lib帶Paddle-TRT子圖功能,可以打開TRT選項進行加速:

// 開啓TensorRT推理,可提升GPU推理性能,需要使用帶TensorRT的推理庫
config->EnableTensorRtEngine(1 << 30      	   /*workspace_size*/,   
                        	 batch_size        /*max_batch_size*/,     
                        	 3                 /*min_subgraph_size*/, 
                       		 paddle_infer::PrecisionType::kFloat32 /*precision*/, 
                        	 false             /*use_static*/, 
                        	 false             /*use_calib_mode*/);

通過計算圖分析,Paddle可以自動將計算圖中部分子圖融合,並調用NVIDIA的 TensorRT 來進行加速。

2-3 內存/顯存優化

config->EnableMemoryOptim();  // 開啓內存/顯存複用

該配置設置後,在模型圖分析階段會對圖中的變量進行依賴分類,兩兩互不依賴的變量會使用同一塊內存/顯存空間,縮減了運行時的內存/顯存佔用(模型較大或batch較大時效果顯著)。

2-4 debug開關

// 該配置設置後,會關閉模型圖分析階段的任何圖優化,預測期間運行同訓練前向代碼一致。
config->SwitchIrOptim(false);
// 該配置設置後,會在模型圖分析的每個階段後保存圖的拓撲信息到.dot文件中,該文件可用graphviz可視化。
config->SwitchIrDebug();

使用Tensor管理輸入/輸出

1. 準備輸入

1-1 獲取模型所有輸入的tensor名字

std::vector<std::string> input_names = predictor->GetInputNames();

1-2 獲取對應名字下的tensor

// 獲取第0個輸入
auto input_t = predictor->GetInputHandle(input_names[0]);

1-3 將數據copy到tensor中

// 在copy前需要設置tensor的shape
input_t->Reshape({batch_size, channels, height, width});
// tensor會根據上述設置的shape從input_data中拷貝對應數目的數據到tensor中。
input_t->CopyFromCpu<float>(input_data /*數據指針*/);

當然我們也可以用mutable_data獲取tensor的數據指針:

// 參數可爲paddle_infer::PlaceType::kGPU, paddle_infer::PlaceType::kCPU
float *input_d = input_t->mutable_data<float>(paddle_infer::PlaceType::kGPU);

2. 獲取輸出
2-1 獲取模型所有輸出的tensor名字

std::vector<std::string> out_names = predictor->GetOutputNames();

2-2 獲取對應名字下的tensor

// 獲取第0個輸出
auto output_t = predictor->GetOutputHandle(out_names[0]);

2-3 將數據copy到tensor中

std::vector<float> out_data;
// 獲取輸出的shpae
std::vector<int> output_shape = output_t->shape();
int out_num = std::accumulate(output_shape.begin(), output_shape.end(), 1, std::multiplies<int>());
out_data->resize(out_num);
output_t->CopyToCpu(out_data->data());

我們可以用data接口獲取tensor的數據指針:

// 參數可爲paddle_infer::PlaceType::kGPU, paddle_infer::PlaceType::kCPU
int output_size;
float *output_d = output_t->data<float>(paddle_infer::PlaceType::kGPU, &output_size);

使用Predictor進行高性能推理

Predictor是在模型上執行推理的預測器,根據Config中的配置進行創建。

auto predictor = paddle_infer::CreatePredictor(config);

paddle_infer::CreatePredictor期間首先對模型進行加載,並且將模型轉換爲由變量和運算節點組成的計算圖。接下來將進行一系列的圖優化,包括OP的橫向縱向融合,刪除無用節點,內存/顯存優化,以及子圖(Paddle-TRT)的分析,加速推理性能,提高吞吐。

 

C++ API使用示例

本節提供一個使用飛槳 C++ 預測庫和ResNet50模型進行圖像分類預測的代碼示例,展示預測庫使用的完整流程。示例代碼地址

一:獲取Resnet50模型

點擊鏈接下載模型, 該模型在imagenet 數據集訓練得到的,如果你想獲取更多的模型訓練信息,請訪問這裏

二:樣例編譯

文件resnet50_test.cc 爲預測的樣例程序(程序中的輸入爲固定值,如果您有opencv或其他方式進行數據讀取的需求,需要對程序進行一定的修改)。
腳本compile.sh 包含了第三方庫、預編譯庫的信息配置。

編譯Resnet50樣例,我們首先需要對腳本compile.sh 文件中的配置進行修改。

1)修改compile.sh

打開compile.sh,我們對以下的幾處信息進行修改:

# 根據預編譯庫中的version.txt信息判斷是否將以下三個標記打開
WITH_MKL=ON       
WITH_GPU=ON         
USE_TENSORRT=OFF

# 配置預測庫的根目錄
LIB_DIR=${YOUR_LIB_DIR}/paddle_inference_install_dir

# 如果上述的WITH_GPU 或 USE_TENSORRT設爲ON,請設置對應的CUDA, CUDNN, TENSORRT的路徑。
CUDNN_LIB=/paddle/nvidia-downloads/cudnn_v7.5_cuda10.1/lib64
CUDA_LIB=/paddle/nvidia-downloads/cuda-10.1/lib64
# TENSORRT_ROOT=/paddle/nvidia-downloads/TensorRT-6.0.1.5

運行 sh compile.sh, 會在目錄下產生build目錄。

2) 運行樣例

bash run.sh
# 或者
bash compile.sh
./build/resnet50_test -model_file resnet50/inference.pdmodel --params_file resnet50/inference.pdiparams

運行結束後,程序會將模型結果打印到屏幕,說明運行成功。

 

C++ API性能調優

在前面預測接口的介紹中,我們瞭解到,通過使用Config可以對Predictor進行配置模型運行的信息。
在本節中,我們會對Config中的優化配置進行詳細的介紹。

優化原理
預測主要存在兩個方面的優化,一是預測期間內存/顯存的佔用,二是預測花費的時間。

  • 預測期間內存/顯存的佔用決定了一個模型在機器上的並行的數量,如果一個任務是包含了多個模型串行執行的過程,內存/顯存的佔用也會決定任務是否能夠正常執行(尤其對GPU的顯存來說)。內存/顯存優化增大了模型的並行量,提升了服務吞吐量,同時也保證了任務的正常執行,因此顯的極爲重要。
  • 預測的一個重要的指標是模型預測的時間,通過對 kernel 的優化,以及加速庫的使用,能夠充份利用機器資源,使得預測任務高性能運行。

1. 內存/顯存優化
在預測初始化階段,飛槳預測引擎會對模型中的 OP 輸出 Tensor 進行依賴分析,將兩兩互不依賴的 Tensor 在內存/顯存空間上進行復用。

可以通過調用以下接口方式打開內存/顯存優化。

Config config;
config.EnableMemoryOptim();

運行過推理之後,如果想回收推理引擎使用的臨時內存/顯存,降低內存/顯存的使用,可以通過以下接口。

config.TryShrinkMemory();

內存/顯存優化效果

模型 關閉優化 打開優化
MobileNetv1(batch_size=128 ) 3915MB 1820MB
ResNet50(batch_size=128) 6794MB 2204MB

2. 性能優化

在模型預測期間,飛將預測引擎會對模型中進行一系列的 OP 融合,比如 Conv 和 BN 的融合,Conv 和 Bias、Relu 的融合等。OP 融合不僅能夠減少模型的計算量,同時可以減少 Kernel Launch 的次數,從而能提升模型的性能。

可以通過調用以下接口方式打開 OP 融合優化:

Config config;
config.SwitchIrOptim(true);  // 默認打開

除了通用的 OP 融合優化外,飛槳預測引擎有針對性的對 CPU 以及 GPU 進行了性能優化。

CPU 性能優化

1.對矩陣庫設置多線程

模型在CPU預測期間,大量的運算依託於矩陣庫,如 OpenBlas,MKL。 通過設置矩陣庫內部多線程,能夠充分利用 CPU 的計算資源,加速運算性能。

可以通過調用以下接口方式設置矩陣庫內部多線程。

Config config;
// 通常情況下,矩陣內部多線程(num) * 外部線程數量 <= CPU核心數
config->SetCpuMathLibraryNumThreads(num);  

2.使用 MKLDNN 加速

MKLDNN是Intel發佈的開源的深度學習軟件包。目前飛槳預測引擎中已經有大量的OP使用MKLDNN加速,包括:Conv,Batch Norm,Activation,Elementwise,Mul,Transpose,Pool2d,Softmax 等。

可以通過調用以下接口方式打開MKLDNN優化。

Config config;
config.EnableMKLDNN();  

開關打開後,飛槳預測引擎會使用 MKLDNN 加速的 Kernel 運行,從而加速 CPU 的運行性能。

GPU 性能優化

使用 TensorRT 子圖性能優化

TensorRT 是 NVIDIA 發佈的一個高性能的深度學習預測庫,飛槳預測引擎採用子圖的形式對 TensorRT 進行了集成。在預測初始化階段,通過對模型分析,將模型中可以使用 TensorRT 運行的 OP 進行標註,同時把這些標記過的且互相連接的 OP 融合成子圖並轉換成一個 TRT OP 。在預測期間,如果遇到 TRT OP ,則調用 TensorRT 庫對該 OP 進行優化加速。

可以通過調用以下接口的方式打開 TensorRT 子圖性能優化:

config->EnableTensorRtEngine(1 << 30      /* workspace_size*/,   
                        batch_size        /* max_batch_size*/,     
                        3                 /* min_subgraph_size*/, 
                        paddle_infer::PrecisionType::kFloat32 /* precision*/, 
                        false             /* use_static*/, 
                        false             /* use_calib_mode*/);

該接口中的參數的詳細介紹如下:

  • workspace_size,類型:int,默認值爲1 << 20。指定TensorRT使用的工作空間大小,TensorRT會在該大小限制下篩選合適的kernel執行預測運算,一般可以設置爲幾百兆(如1 << 29, 512M)。
  • max_batch_size,類型:int,默認值爲1。需要提前設置最大的batch大小,運行時batch大小不得超過此限定值。
  • min_subgraph_size,類型:int,默認值爲3。Paddle-TRT 是以子圖的形式運行,爲了避免性能損失,當子圖內部節點個數大於min_subgraph_size的時候,纔會使用Paddle-TRT運行。
  • precision,類型:enum class Precision {kFloat32 = 0, kHalf, kInt8,};, 默認值爲PrecisionType::kFloat32。指定使用TRT的精度,支持FP32(kFloat32),FP16(kHalf),Int8(kInt8)。若需要使用Paddle-TRT int8離線量化校準,需設定precision爲 - PrecisionType::kInt8, 且設置use_calib_mode 爲true。
  • use_static,類型:bool, 默認值爲 false 。如果指定爲 true ,在初次運行程序的時候會將 TRT 的優化信息進行序列化到磁盤上,下次運行時直接加載優化的序列化信息而不需要重新生成。
  • use_calib_mode,類型:bool, 默認值爲false。若要運行 Paddle-TRT int8 離線量化校準,需要將此選項設置爲 true 。

目前 TensorRT 子圖對圖像模型有很好的支持,支持的模型如下

  • 分類:Mobilenetv1/2, ResNet, NasNet, VGG, ResNext, Inception, DPN,ShuffleNet
  • 檢測:SSD,YOLOv3,FasterRCNN,RetinaNet
  • 分割:ICNET,UNET,DeepLabV3,MaskRCNN
 

基於Python API的推理部署

飛槳提供了高度優化的C++測庫,爲了方便使用,我們也提供了與C++預測庫對應的Python接口。使用Python預測API與C++預測API相似,主要包括Tensor, ConfigPredictor,分別對應於C++ API中同名的數據類型。接下來給出更爲詳細的介紹。

使用Config管理推理配置

paddle.inference.Config是創建預測引擎的配置,提供了模型路徑設置、預測引擎運行設備選擇以及多種優化預測流程的選項。通過Config,可以指定,預測引擎執行的方式。舉幾個例子,如果我們希望預測引擎在 CPU 上運行,那麼,我們可以設置disable_gpu()的選項配置,那麼在實際執行的時候,預測引擎會在 CPU 上執行。同樣,如果設置switch_ir_optim()True或是False,則決定了預測引擎是否會自動進行優化。

使用之前,我們需要創建一個Config的實例,用於完成這些選項的設置。

config = paddle.inference.Config()

Config可以設置的選項,具體如下:

  • set_model(): 設置模型的路徑,model_filename:模型文件名,params_filename:參數文件名。

    config.set_model(model_filename, params_filename)

既然模型的文件已經配置好了,那麼我們還需要指定我們的預測引擎是在什麼設備上執行的,就是說,需要指定在 CPU 上或是在 GPU 的哪一張卡上預測。

  • enable_use_gpu(): 啓用使用 GPU 的預測方式,並且設置 GPU 初始分配的顯存(單位M)和 Device ID,即是在哪一張 GPU 的卡上執行預測。 需要另外注意的是,Device ID,假設,我們現在有一臺8張顯卡的機器,我們設定環境變量如下:

    export CUDA_VISIBLE_DEVICES=1,3,5

    那麼,在使用enable_use_gpu()設置 Device ID 的時候,可以設置的卡的編號是:0,1,2。0號卡實際代表的是機器上的編號爲1的顯卡,而1號卡實際代表的是機器上編號爲3的顯卡,同理,2號卡實際代表的是機器上的編號爲5的顯卡。

  • gpu_device_id(): 返回使用的 GPU 的 Device ID。

  • disable_gpu(): 該方法從字面上理解是禁用 GPU,即,是使用 CPU 進行預測。

完成了模型的配置,執行預測引擎的設備的設定,我們還可以進行一些其他的配置。比如:

  • switch_ir_optim(): 打開或是關閉預測引擎的優化,默認是開啓的。設置的方式如下:

    config.switch_ir_optim(True)
  • enable_tensorrt_engine(): 啓用TensorRT的引擎進行預測優化。具體的參數信息和上文使用TensorRT子圖性能優化是一樣的。這裏提供一個簡單的設置示例。

    config.enable_tensorrt_engine(precision_mode=paddle.inference.PrecisionType.Float32,
                                use_calib_mode=True)
  • enable_mkldnn(): 開啓 MKLDNN 加速選項,一般是使用 CPU 進行預測的時候,如果機器支持 MKLDNN 可以開啓。

    config.enable_mkldnn()
  • disable_glog_info(): 禁用預測中所有的日誌信息。

在完成了Config的設置之後,我們可以通過配置的Config,創建一個用於執行預測引擎的實例,這個實例是基於數據結構Predictor,創建的方式是直接調用paddle.inference.create_predictor()方法。如下:

predictor = paddle.inference.create_predictor(config)

Config示例

首先,如前文所說,設置模型和參數路徑:

config = paddle.inference.Config("./model/resnet50.pdmodel", "./model/resnet50.pdiparams")

使用set_model()方法設置模型和參數路徑方式同上。

其他預測引擎配置選項示例如下:

config.enable_use_gpu(100, 0) # 初始化100M顯存,使用gpu id爲0
config.gpu_device_id()        # 返回正在使用的gpu device id
config.disable_gpu()		  # 禁用gpu,使用cpu進行預測
config.switch_ir_optim(True)  # 開啓IR優化
config.enable_tensorrt_engine(precision_mode=paddle.inference.PrecisionType.Float32,
                              use_calib_mode=True) # 開啓TensorRT預測,精度爲fp32,開啓int8離線量化
config.enable_mkldnn()		  # 開啓MKLDNN

最後,根據config得到預測引擎的實例predictor:

predictor = paddle.inference.create_predictor(config)

使用Tensor管理輸入/輸出

TensorPredictor的一種輸入/輸出數據結構,下面我們將用詳細的例子加以講解。

首先,我們配置好了Config的一個實例config,接下來,需要使用這個config創建一個predictor

# 創建predictor
predictor = paddle.inference.create_predictor(config)

因爲config裏面包含了模型的信息,在這裏,我們創建好了predictor之後,實際上已經可以獲取模型的輸入的名稱了。因此,可以通過

input_names = predictor.get_input_names()

獲取模型輸入的名稱。需要注意的是,這裏的input_names是一個List[str],存儲着模型所有輸入的名稱。進而,可以通過每一個輸入的名稱,得到Tensor的一個實例,此時,輸入數據的名稱已經和對應的Tensor關聯起來了,無需再另外設置數據的名稱。

input_tensor = predictor.get_input_handle(input_names[0])

得到數據之後,就可以完成對數據的設置了。

fake_input = numpy.random.randn(1, 3, 224, 224).astype("float32")
input_tensor.copy_from_cpu(fake_input)

使用一個copy_from_cpu()方法即可完成設置,數據類型,通過numpy來保證。 在實際的使用過程中,有的用戶會問到,”如果我想將預測引擎在 GPU 上執行怎麼辦呢?是否有一個copy_from_gpu()的方法?“ 回答也很簡單,沒有copy_from_gpu()的方法,無論是在 CPU 上執行預測引擎,還是在 GPU 上執行預測引擎,copy_from_cpu()就夠了。

接着,執行預測引擎:

# 運行predictor
predictor.run()

完成預測引擎的執行之後,我們需要獲得預測的輸出。與設置輸入類似 首先,我們獲取輸出的名稱 其次,根據輸出的名稱得到Tensor的實例,用來關聯輸出的Tensor。 最後,使用copy_to_cpu()得到輸出的矩陣向量。

output_names = predictor.get_output_names()
output_tensor = predictor.get_output_handle(output_names[0])
output_data = output_tensor.copy_to_cpu() # numpy.ndarray類型

使用Predictor進行高性能推理

class paddle.inference.Predictor

Predictor是運行預測的引擎,由paddle.inference.create_predictor(config)創建。

Predictor示例

import numpy

# 引用 paddle inference 預測庫
import paddle.inference as paddle_infer

# 創建 config
config = paddle_infer.Config("./model/resnet50.pdmodel", "./model/resnet50.pdiparams")

# 根據 config 創建 predictor
predictor = paddle_infer.create_predictor(config)

# 獲取輸入 Tensor
input_names = predictor.get_input_names()
input_tensor = predictor.get_input_handle(input_names[0])

# 從 CPU 獲取數據,設置到 Tensor 內部
fake_input = numpy.random.randn(1, 3, 224, 224).astype("float32")
input_tensor.copy_from_cpu(fake_input)

# 執行預測
predictor.run()

支持方法列表

在這裏,我們先做一個總結。總結下前文介紹的使用預測庫的 Python API 都有哪些方法。

  • Tensor
    • copy_from_cpu(input: numpy.ndarray) -> None
    • copy_to_cpu() -> numpy.ndarray
    • reshape(input: numpy.ndarray|List[int]) -> None
    • shape() -> List[int]
    • set_lod(input: numpy.ndarray|List[List[int]]) -> None
    • lod() -> List[List[int]]
    • type() -> PaddleDType
  • Config
    • set_model(prog_file: str, params_file: str) -> None
    • prog_file() -> str
    • params_file() -> str
    • enable_use_gpu(memory_pool_init_size_mb: int, device_id: int) -> None
    • gpu_device_id() -> int
    • switch_ir_optim(x: bool = True) -> None
    • enable_tensorrt_engine(workspace_size: int = 1 << 20, max_batch_size: int, min_subgraph_size: int, precision_mode: PrecisionType, use_static: bool, use_calib_mode: bool) -> None
    • enable_mkldnn() -> None
    • disable_glog_info() -> None
    • delete_pass(pass_name: str) -> None
  • Predictor
    • run() -> None
    • get_input_names() -> List[str]
    • get_input_handle(input_name: str) -> Tensor
    • get_output_names() -> List[str]
    • get_output_handle(output_name: str) -> Tensor

Python API使用示例

下面是使用Python API進行推理的一個完整示例。

使用前文提前準備生成的模型example.model,在該路徑下即可找到預測需要的模型文件linear.pdmodel和參數文件linear.pdiparams

我們可以ls查看一下對應的路徑下面內容:

ls example.model 
linear.pdiparams  linear.pdiparams.info  linear.pdmodel

模型文件準備好了,接下來,可以直接運行下面的代碼得到預測的結果,預測的結果。

最後的output_data就是預測程序返回的結果,output_data是一個numpy.ndarray類型的數組,可以直接獲取其數據的值。output_data作爲一個numpy.ndarray,大家需要自定義不同的後處理方式也更爲方便。以下的代碼就是使用Python預測API全部的部分。

如下的實例中,我們打印出了前10個數據。輸出的數據的形狀是[1, 1000],其中1代表的是batch_size的大小,1000是這唯一一個樣本的輸出,有1000個數據。

import numpy as np
# 引用 paddle inference 預測庫
import paddle.inference as paddle_infer

def main():
    # 設置Config
    config = set_config()

    # 創建Predictor
    predictor = paddle_infer.create_predictor(config)

    # 獲取輸入的名稱
    input_names = predictor.get_input_names()
    input_tensor = predictor.get_input_handle(input_names[0])

    # 設置輸入
    fake_input = np.random.randn(1,784).astype("float32")
    input_tensor.copy_from_cpu(fake_input)

    # 運行predictor
    predictor.run()

    # 獲取輸出
    output_names = predictor.get_output_names()
    output_tensor = predictor.get_output_handle(output_names[0])
    output_data = output_tensor.copy_to_cpu() # numpy.ndarray類型
    print("輸出的形狀如下: ")
    print(output_data.shape)
    print("輸出前10個的數據如下: ")
    print(output_data[:10])

def set_config():
    config = paddle_infer.Config("./example.model/linear.pdmodel", "./example.model/linear.pdiparams")
    config.disable_gpu()
    return config

if __name__ == "__main__":
    main()  
輸出的形狀如下: 
(1, 10)
輸出前10個的數據如下: 
[[ 1.392835   -2.1492589  -1.244189   -0.5736715   1.6074656   2.109792
  -0.04553057  1.1988349   1.3468434  -0.6586081 ]]
--- Running analysis [ir_graph_build_pass]
--- Running analysis [ir_graph_clean_pass]
--- Running analysis [ir_analysis_pass]
--- Running IR pass [simplify_with_basic_ops_pass]
--- Running IR pass [layer_norm_fuse_pass]
---    Fused 0 subgraphs into layer_norm op.
--- Running IR pass [attention_lstm_fuse_pass]
--- Running IR pass [seqconv_eltadd_relu_fuse_pass]
--- Running IR pass [seqpool_cvm_concat_fuse_pass]
--- Running IR pass [mul_lstm_fuse_pass]
--- Running IR pass [fc_gru_fuse_pass]
---    fused 0 pairs of fc gru patterns
--- Running IR pass [mul_gru_fuse_pass]
--- Running IR pass [seq_concat_fc_fuse_pass]
--- Running IR pass [gpu_cpu_squeeze2_matmul_fuse_pass]
--- Running IR pass [gpu_cpu_reshape2_matmul_fuse_pass]
--- Running IR pass [gpu_cpu_flatten2_matmul_fuse_pass]
--- Running IR pass [matmul_v2_scale_fuse_pass]
--- Running IR pass [gpu_cpu_map_matmul_v2_to_mul_pass]
I0511 10:18:07.135977   117 fuse_pass_base.cc:57] ---  detected 1 subgraphs
--- Running IR pass [gpu_cpu_map_matmul_v2_to_matmul_pass]
--- Running IR pass [matmul_scale_fuse_pass]
--- Running IR pass [gpu_cpu_map_matmul_to_mul_pass]
--- Running IR pass [fc_fuse_pass]
I0511 10:18:07.136333   117 fuse_pass_base.cc:57] ---  detected 1 subgraphs
--- Running IR pass [repeated_fc_relu_fuse_pass]
--- Running IR pass [squared_mat_sub_fuse_pass]
--- Running IR pass [conv_bn_fuse_pass]
--- Running IR pass [conv_eltwiseadd_bn_fuse_pass]
--- Running IR pass [conv_transpose_bn_fuse_pass]
--- Running IR pass [conv_transpose_eltwiseadd_bn_fuse_pass]
--- Running IR pass [is_test_pass]
--- Running IR pass [runtime_context_cache_pass]
--- Running analysis [ir_params_sync_among_devices_pass]
--- Running analysis [adjust_cudnn_workspace_size_pass]
--- Running analysis [inference_op_replace_pass]
--- Running analysis [ir_graph_to_program_pass]
I0511 10:18:07.138321   117 analysis_predictor.cc:1006] ======= optimize end =======
I0511 10:18:07.138358   117 naive_executor.cc:102] ---  skip [feed], feed -> generated_var_0
I0511 10:18:07.138401   117 naive_executor.cc:102] ---  skip [linear_1.tmp_1], fetch -> fetch
 

Python API性能調優

Python 預測 API 的優化與 C++ 預測 API 的優化方法完全一樣。大家在使用的時候,可以參照 C++ 預測 API 的優化說明。唯一存在不同的是,調用的方法的名稱,在這裏,我們做了一個對應的表格供大家查閱。

C++預測API函數名稱 Python預測API函數名稱
EnableMemoryOptim() enable_memory_optim()
SwitchIrOptim(true) switch_ir_optim(True)
EnableMKLDNN() enable_mkldnn()
SetCpuMathLibraryNumThreads(num) set_cpu_math_library_num_threads(num)
EnableTensorRtEngine() enable_tensorrt_engine()

以上方法在使用 Python 預測 API 的時候,都可以直接使用

config.methods_name()

完成調用與配置。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章