介紹
TensorRT
是一個高性能的深度學習推理優化器,可以為深度學習應用提供低延遲、高吞吐率的部署推理。TensorRT
可用于對超大規模數據中心、嵌入式平臺或自動駕駛平臺進行推理加速。TensorRT
現在已能支持TensorFlow
、Caffe
、Mxnet
、Pytorch
等幾乎所有的深度學習框架,將TensorRT
和NVIDIA
的GPU
結合起來,能在幾乎所有的框架中進行快速和高效的部署推理。目前的TensorRT
庫支持C ++
或者Python API
部署。
為什么對推理過程進行優化
深度學習模型一般分為訓練(training
)和 推理(inference
)兩個部分:
- 訓練: 訓練包含了前向傳播和后向傳播兩個階段,針對的是訓練集。訓練時通過誤差反向傳播來不斷修改網絡權值。
- 推理: 推理只包含前向傳播一個階段,針對的是除了訓練集之外的新數據。可以是測試集,但不完全是,更多的是整個數據集之外的數據。其實就是針對新數據進行預測,預測時,速度是一個很重要的因素。
一般的深度學習項目,訓練時為了加快速度,會使用多GPU
分布式訓練。但在部署推理時,為了降低成本,往往使用單個GPU
機器甚至嵌入式平臺,比如NVIDIA Jetson
,Tesla T4
等。由于訓練的網絡模型可能會很大,參數很多,而且部署端的機器性能存在差異,就會導致推理速度慢,延遲高。這對于那些高實時性的應用場合是致命的,比如自動駕駛要求實時目標檢測,目標追蹤等。
使用訓練框架執行推理很容易,但是與使用TensorRT
之類的優化解決方案相比,相同GPU
上的性能往往低得多。訓練框架傾向于實施強調通用性的通用代碼,而當優化它們時,優化往往集中于有效的訓練。TensorRT
通過結合抽象出特定硬件細節的高級API
和優化推理的實現來解決這些問題,以實現高吞吐量,低延遲和低設備內存占用。TensorRT
就是對訓練好的模型進行優化,由于TensorRT
就只包含推理優化器,優化完的網絡不再需要依賴深度學習框架,可以直接通過TensorRT
部署在NVIDIA
的各種硬件中。
TensorRT實踐
github:https://github.com/xiaochus/DeepModelDeploy/tree/main/TensorRT/
TensorRT加速原理
對于深度學習推理,有五個用于衡量軟件的關鍵因素:
吞吐量: 給定時間段內的產出量。 每臺服務器的吞吐量通常以推斷/秒或樣本/秒來衡量,對于數據中心的經濟高效擴展至關重要。
效率:每單位功率交付的吞吐量,通常表示為性能/瓦特。效率是經濟高效地擴展數據中心的另一個關鍵因素,因為服務器,服務器機架和整個數據中心必須在固定的功率預算內運行。
延遲:執行推理的時間,通常以毫秒為單位。低延遲對于提供快速增長的基于實時推理的服務至關重要。
準確性:訓練好的神經網絡能夠提供正確答案。 對于圖像分類算法,關鍵指標為top-5
或top-1
。
內存使用情況:需要保留以在網絡上進行推理的主機和設備內存取大小決于所使用的算法。這限制了哪些網絡以及網絡的哪些組合可以在給定的推理平臺上運行。這對于需要多個網絡且內存資源有限的系統尤其重要,例如,在智能視頻分析和多攝像機,多網絡自動駕駛系統中使用的級聯多級檢測網絡。
TensorRT
的解決方案是:
權重與激活精度校準:通過將模型量化為 FP16
或INT8
來更大限度地提高吞吐量,同時保持高準確度。
層與張量融合 :通過融合內核中的節點,優化GPU
顯存和帶寬的使用。
內核自動調整:基于目標 GPU
平臺選擇最佳數據層和算法。
動態張量顯存:更大限度減少顯存占用,并高效地為張量重復利用內存。
多流執行:用于并行處理多個輸入流的可擴展設計。
通過這些方法,TensorRT
能夠在不損失精度的情況下顯著的提升模型的推理性能。
Batch推理
在GPU
上使用較大的batch
幾乎總是更有效,batch
的作用在于能盡可能多地并行計算。模型的輸入只有單個batch
的時候,單個batch
的計算量并不能充分的利用CUDA
核心的計算資源,有相當一部分的核心在閑置等待中;當輸入有多個batch
的時候,由于GPU
的并行計算的特性,不同的batch
會同步到不同的CUDA
核心中進行并行計算,提高了單位時間GPU
的利用率。
例如:FullyConnected
層有V
個輸入和K
個輸出,對于1
個batch
的實例,可以實現為1xV
的input
矩陣乘以VxK
的weight
矩陣。如果是N
個batch
的實例,這就可以實現為NxV
乘以VxK
矩陣。將向量-矩陣乘法變為矩陣-矩陣乘法,效率更高。此外,當網絡包含MatrixMultiply
層或FullyConnected
層時,如果硬件支持Tensor Core
,對于FP16
和INT8
的推理模式,將batch
大小設置為32
的倍數往往具有最佳性能。
計算圖合并與優化
TensorRT
對于網絡結構進行了重構,把一些能夠合并的運算合并在了一起,針對GPU
的特性做了優化。一個深度學習模型,在沒有優化的情況下,比如一個卷積層、一個偏置層和一個激活層,這三層是需要調用三次cuDNN
對應的API
,但實際上這三層的實現完全是可以合并到一起的,TensorRT
會對一些可以合并網絡進行合并。
TensorRT
對計算圖主要執行以下優化:
- 消除輸出不被使用的層。
- 消除等同于無操作的操作。
- 卷積,偏置和
ReLU
操作的融合。 - 匯總具有足夠相似的參數和相同的源張量的操作(例如,
GoogleNet v5
的初始模塊中的1x1
卷積)。 - 通過將層輸出定向到正確的最終目的地來合并串聯圖層。
- 在構建階段還會在虛擬數據上運行各層,以從其核目錄中選擇最快的核,并在適當的情況下執行權重預格式化和內存優化。
以一個典型的inception block
為例,優化過程如下:
首先對網絡結構進行垂直整合,即將目前主流神經網絡的
conv
、BN
、Relu
三個層融合為了一個層,稱之為CBR
。然后對網絡結構進行水平組合,水平組合是指將輸入為相同張量和執行相同操作的層融合一起。
inception block
中將三個相連的1×1
的CBR
組合為一個大的1×1
的CBR
。最后處理
concat
層,將contact
層的輸入直接送入下面的操作中,不用單獨進行concat
后在輸入計算,相當于減少了一次傳輸吞吐。
量化
量化是將數值x
映射到 y
的過程,其中 x
的定義域是一個大集合(通常是連續的),而 y
的定義域是一個小集合(通常是可數的)。大部分深度學習框架在訓練神經網絡時網絡中的張量都是32
位浮點數的精度(Full 32-bit precision,FP32
)。一旦網絡訓練完成,在部署推理的過程中由于不需要反向傳播,完全可以適當降低數據精度,比如降為FP16
或INT8
的精度。量化后模型的體積更小,將帶來以下的優勢:
減少內存帶寬和存儲空間
深度學習模型主要是記錄每個layer
(比如卷積層/全連接層)的weights
和bias
。在FP32
的模型中,每個 weight
數值原本需要32-bit
的存儲空間,通過INT8
量化之后只需要8-bit
即可。因此,模型的大小將直接降為將近 1/4
。不僅模型大小明顯降低,activation
采用INT8
之后也將明顯減少對內存的使用,這也意味著低精度推理過程將明顯減少內存的訪問帶寬需求,提高高速緩存命中率,尤其對于像batch-norm
,relu
,elmentwise-sum
這種內存約束(memory bound
)的element-wise
算子來說效果更為明顯。
提高系統吞吐量(throughput),降低系統延時(latency)
直觀的來講,對于一個專用寄存器寬度為512
位的SIMD
指令,當數據類型為FP32
而言一條指令能一次處理 16
個數值,但是當我們采用INT8
表示數據時,一條指令一次可以處理64
個數值。因此,在這種情況下可以讓芯片的理論計算峰值增加4
倍。
Tesla T4 GPU
引入了 Turing Tensor Core
技術,涵蓋所有的精度范圍,從 FP32
到FP16
到 INT8
。在 Tesla T4 GPU
上,Tensor Cores
可以進行30萬億次浮點計算(TOPS
)。通過TensorRT
我們可以將一個原本為FP32
的weight/activation
浮點數張量轉化成一個fp16/int8/uint8
的張量來處理。使用 INT8
和混合精度可以降低內存消耗,這樣就跑的模型就可以更大,用于推理的mini-batch size
可以更大,模型的單位推理速度就越快。
為什么模型量化在提升速度的同時,也并不會造成太大的精度損失?在Why are Eight Bits Enough for Deep Neural Networks? 以及Low Precision Inference with TensorRT 這兩篇博文中作者提到:
網絡在訓練的過程中學習到了數據樣本的模式可分性,同時由于數據中存在的噪聲,使得網絡具有較強的魯棒性,也就是說在輸入樣本中做輕微的變動并不會過多的影響結果性能。與圖像上目標間的位置,姿態,角度等的變化程度相比,這些噪聲引進的變動只是很少的一部分,但實際上這些噪聲引進的變動同樣會使各個層的激活值輸出發生變動,然而卻對結果影響不大,也就是說訓練好的網絡對這些噪聲具有一定的容忍度。
正是由于在訓練過程中使用高精度(FP32
)的數值表示,才使得網絡具有一定的容忍度。訓練時使用高精度的數值表示,可以使得網絡以很小的計算量修正參數,這在網絡最后收斂的時候是很重要的,因為收斂的時候要求修正量很小很小(一般訓練初始 階段學習率稍大,越往后學習率越小)。那么如果使用低精度的數據來表示網絡參數以及中間值的話,勢必會存在誤差,這個誤差某種程度上可以認為是一種噪聲。那也就是說,使用低精度數據引進的差異是在網絡的容忍度之內的,所以對結果不會產生太大影響。
FP16
TensorRT
支持高度自動化的FP16
推斷(Inference
)。與FP32
或FP64
相比,使用半精度(FP16
)可以降低神經網絡的內存使用。FP16
支持部署更大的網絡,同時比FP32
或FP64
花費更少的時間。
NVIDIA
從Volta
硬件架構開始采用了稱為Tensor Cores
的矩陣數學加速器。Tensor Cores
提供4x4x4
矩陣處理陣列,用于執行操作D = A * B + C
,其中A, B, C and D
為4×4
的矩陣。矩陣乘法輸入A
和B
是FP16
矩陣,而累加矩陣C
和D
可以是FP16
或FP32
矩陣。
當使用FP16
檢測到推理時,TensorRT
會自動使用硬件Tensor Cores
。 Tensor Cores
在NVIDIA Tesla V100
上的峰值性能比雙精度(FP64
)快一個數量級,而吞吐量比單精度(FP32
)提高了4倍。
INT8
TensorRT
可以將以單精度(FP32
)或者半精度(FP16
)訓練的模型轉化為以INT8
量化部署的模型,同時可以最小化準確率損失。由于INT8
的表達范圍遠遠小于FP32
,生成8位整數精度的網絡時不能像FP16
一樣直接縮減精度,TensorRT
對精度為FP32
的模型進行校驗來確定中間激活的動態范圍,從而確定適當的用于量化的縮放因子。
INT8
量化的本質是一種縮放(scaling
)操作,通過縮放因子將模型的分布值從FP32
范圍縮放到 INT8
范圍之內。按照量化階段的不同,一般將量化分為quantization aware training(QAT)
和post-training quantization(PTQ)
。QAT
需要在訓練階段就對量化誤差進行建模,這種方法一般能夠獲得較低的精度損失。PTQ
直接對普通訓練后的模型進行量化,過程簡單,不需要在訓練階段考慮量化問題,因此,在實際的生產環境中對部署人員的要求也較低,但是在精度上一般要稍微遜色于QAT
。
PTQ
的量化方法分為非對稱算法和對稱算法。非對稱算法那的基本思想是通過收縮因子和零點將FP32
張量的min/max
映射分別映射到UINT8
數據的min/max
, min/max -> 0~255
。對稱算法的基本思路是通過一個收縮因子將FP32
中的最大絕對值映射到INT8
數據的最大值,將最大絕對值的負值映射到INT8
數據的最小值 ,-|max|/|max| -> -128~127
。
對稱算法中使用的映射方法又分為不飽和映射和飽和映射,兩種映射方的區別就是FP32
張量的值在映射后是否能夠大致均勻分布在0
的左右。如果分布不均勻,量化之后將不能夠充分利用INT8
的數據表示能力。
簡單的將一個tensor
中的 -|max|
和 |max|
的FP32
值映射到-127
和 127
,中間值按照線性關系進行映射。這種對稱映射關系為不飽和的(No saturation
)。
Quantize(x, max) = round(s * x) where s = 127 / max
根據tensor
的分布計算一個閾值|T|
,將范圍在 ±|T|
的FP32
值映映射到±127
的范圍中,其中|T|<|max|
。超出閾值 ±|T|
的值直接映射為 ±127
。這種不對稱的映射關系為飽和的(Saturate
)。
Quantize(x, r) = round(s * clip(x, -r, r)) where s = 127 / r
下面這張圖展示的是不同網絡結構的不同layer
的激活值分布,有卷積層,有池化層。他們之間的分布很不一樣,激活值并不是均勻的分布在[-max, max]
之間的,可以看出過大或者過小的激活值其實只占參數總體的一小部分,因此如果直接使用不飽和的映射關系不能有效利用INT8
的表達范圍,就會導致比較大的精度損失。如果我們可以找到一個范圍,使網絡中tensor
的絕大多數的值都存在于這個范圍內,我們就可以利用這個范圍來對 tensor
進行量化。只要閾值|T|
選取得當,就能將分布散亂的較大的激活值舍棄掉,也就有可能使精度損失不至于降低太多。根據實驗,weights
在這兩種方式上沒有顯著的差異,而對activation
使用飽和的量化方式會有比較顯著的性能提升。因此TensorRT
在模型上使用了這兩種方法進行了混合量化。
由于網絡的每個層輸出值的分布都是不同的,飽和映射的方法就需要對每個層都計算出一個有效的閾值|T|
。為了使INT8
的輸出結果盡可能的減少精度損失,我們就需要通過校正數據集來衡量不同的INT8
分布與原來的FP3F2
分布之間的差異程度,選擇出一個與原始的FP32
分布最為相似的INT8
分布。這個衡量分布之間差異的指標就是KL
散度(Kullback–Leibler divergence
),KL
散度通常用來衡量一個分布相比另一個分布的信息損失。
INT8
量化的過程如下:
- 準備一個
FP32
的模型。 - 從驗證集選取一個子集作為校準集,校準集應該具有代表性,多樣性,最好是驗證集的一個子集。
- 在校準數據集上進行
FP32
推理。 - 推理過程中遍歷網絡的每一層:
- 收集這一層的激活值,并做直方圖,分成幾個組別(官方給的一個說明使用的是
2048
組),分組是為了下面遍歷|T|
時,減少遍歷次數; - 對于不同的閾值
|T|
進行遍歷,因為這里|T|
的取值肯定在第128-2047
組之間,所以就選取每組的中間值進行遍歷; - 選取使得
KL
散度取得最小值的|T|
。
- 收集這一層的激活值,并做直方圖,分成幾個組別(官方給的一個說明使用的是
- 返回一系列
|T|
值,每一層都有一個|T|
,創建CalibrationTable
。
官方校準的偽代碼:
//首先分成 2048個組,每組包含多個數值(基本都是小數)
Input: FP32 histogram H with 2048 bins: bin[ 0 ], …, bin[ 2047 ]
For i in range( 128 , 2048 ): // |T|的取值肯定在 第128-2047 組之間,取每組的中點
reference_distribution_P = [ bin[ 0 ] , ..., bin[ i-1 ] ] // 選取前 i 組構成P,i>=128
outliers_count = sum( bin[ i ] , bin[ i+1 ] , … , bin[ 2047 ] ) //邊界外的組
reference_distribution_P[ i-1 ] += outliers_count //邊界外的組加到邊界P[i-1]上,沒有直接丟掉
P /= sum(P) // 歸一化
// 將前面的P(包含i個組,i>=128),映射到 0-128 上,映射后的稱為Q,Q包含128個組,
// 一個整數是一組
candidate_distribution_Q = quantize [ bin[ 0 ], …, bin[ i-1 ] ] into 128 levels
//這時的P(包含i個組,i>=128)和Q向量(包含128個組)的大小是不一樣的,無法直接計算二者的KL散度
//因此需要將Q擴展為 i 個組,以保證跟P大小一樣
expand candidate_distribution_Q to ‘ i ’ bins
Q /= sum(Q) // 歸一化
//計算P和Q的KL散度
divergence[ i ] = KL_divergence( reference_distribution_P, candidate_distribution_Q)
End For
//找出 divergence[ i ] 最小的數值,假設 divergence[m] 最小,
//那么|T|=( m + 0.5 ) * ( width of a bin )
Find index ‘m’ for which divergence[ m ] is minimal
threshold = ( m + 0.5 ) * ( width of a bin )
有了INT8
的權重和對應的激活層的縮放表,TensorRT
通過GPU
的DP4A
硬件指令進行的卷積計算,該指令能夠執行8
位整數4
元向量點積(即一個4x4
的INT8
卷積操作)來加速深度神經網絡推理。輸入的tensor
為INT8
,卷積計算完之后是INT32的
,然后將該值轉換成FP32
,通過激活層后再通過縮放因子將FP32
的激活值轉為INT8
。
通過這種混合量化的過程,TensorRT
的INT8
模式在有效提升了推理速度的同時,只帶來了些許的精度損失。