原生推理庫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文檔
- 配置推理選項。
Config
是飛槳提供的配置管理器API。在使用Paddle Inference進行推理部署過程中,需要使用Config
詳細地配置推理引擎參數,包括但不限於在何種設備(CPU/GPU)上部署、加載模型路徑、開啓/關閉計算圖分析優化、使用MKLDNN/TensorRT進行部署的加速等。參數的具體設置需要根據實際需求來定。 - 創建
Predictor
。Predictor
是飛槳提供的推理引擎API。根據設定好的推理配置Config
創建推理引擎Predictor
,也就是推理引擎的一個實例。創建期間會進行模型加載、分析和優化等工作。 - 準備輸入數據。準備好待輸入推理引擎的數據,首先獲得模型中每個輸入的名稱以及指向該數據塊(CPU或GPU上)的指針,再根據名稱將對應的數據塊拷貝進
Tensor
。飛槳採用Tensor
作爲輸入/輸出數據結構,可以減少額外的拷貝,提升推理性能。 - 調用Predictor.Run()執行推理。
- 獲取推理輸出。與輸入數據類似,根據輸出名稱將輸出的數據(矩陣向量)由
Tensor
拷貝至(CPU或GPU上)以進行後續的處理。 - 最後,獲取輸出並不意味着預測過程的結束,在一些特別的場景中,單純的矩陣向量不能讓使用者明白它有什麼意義。進一步地,我們需要根據向量本身的意義,解析數據,獲取實際的輸出。舉個例子,transformer 翻譯模型,我們將字詞變成向量輸入到預測引擎中,而預測引擎反饋給我們的,仍然是矩陣向量。但是這些矩陣向量是有意義的,我們利用這些向量去找翻譯結果所對應的句子,就完成了使用 transformer 翻譯的過程。
以上操作的具體使用方法和示例會在下文給出。
前提準備
-
安裝或源碼編譯推理庫
使用飛槳進行推理部署,需要使用與當前部署環境一致的Paddle推理庫。
如果使用Python API,只需本地電腦成功安裝Paddle,安裝方法請參考快速安裝。
如果使用C++/C API,需要下載或編譯推理庫。推薦先從飛槳官網下載推理庫,下載請點擊推理庫。如果官網提供的推理庫版本無法滿足需求,或想要對代碼進行自定義修改,可以採用源碼編譯的方式獲取推理庫,推理庫的編譯請參考前文“源碼編譯”。 -
導出模型文件
模型部署首先要有部署的模型文件。模型部署首先要有部署的模型文件。在動態圖模型訓練過程中或者模型訓練結束後,可以通過paddle.jit.save 接口來導出用於部署的標準化模型文件。而對於靜態圖模型,則可以使用paddle.static.save_inference_model保存模型。兩種方式都可以根據推理需要的輸入和輸出, 對訓練模型進行剪枝, 去除和推理無關部分, 得到的模型相比訓練時更加精簡, 適合進一步優化和部署。
我們用一個簡單的例子來展示下導出模型文件的這一過程。
-
我們用一個簡單的例子來展示下導出模型文件的這一過程。
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
, Config
和Predictor
,分別對應於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管理輸入/輸出
Tensor
是Predictor
的一種輸入/輸出數據結構,下面我們將用詳細的例子加以講解。
首先,我們配置好了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()
完成調用與配置。