TensorRT下FP32轉INT8的過程


作者:Tiso-yan
來源:CSDN
原文:https://blog.csdn.net/qq_32043199/article/details/81119357

1. 關於TensorRT

  NVIDIA TensorRT是一種高性能神經網絡推理(Inference)引擎,用於在生產環境中部署深度學習應用程序,應用有圖像分類、分割和目標檢測等,可提供最大的推理吞吐量和效率。TensorRT是第一款可編程推理加速器,能加速現有和未來的網絡架構。TensorRT需要CUDA的支持。TensorRT包含一個爲優化生產環境中部署的深度學習模型而創建的庫,可獲取經過訓練的神經網絡(通常使用32位或16位數據),並針對降低精度的INT8運算來優化這些網絡。藉助CUDA的可編程性,TensorRT將能夠加速助推深度神經網絡日益多樣化、複雜的增長趨勢。通過TensorRT的大幅度加速,服務提供商能夠以經濟實惠的成本部署這些計算密集型人工智能工作負載。

2.關於int8 inference

對於INT8 推斷(Inference),需要生成一個校準表來量化模型。接下來主要關注INT8推斷(Inference)的幾個方面,即:如何生成校準表,如何使用校準表,和INT8推斷(Inference)實例。

1) 如何生成校準表?

校準表的生成需要輸入有代表性的數據集, 對於分類任務TensorRT建議輸入五百張到一千張有代表性的圖片,最好每個類都要包括。生成校準表分爲兩步:第一步是將輸入的數據集轉換成batch文件;第二步是將轉換好的batch文件喂到TensorRT中來生成基於數據集的校準表,可以去統計每一層的情況。

2) 如何使用校準表?

校準這個過程如果要跑一千次是很昂貴的,所以TensorRT支持將其存入文檔,後期使用可以從文檔加載,其中存儲和加載的功能通過兩個方法來支持,即writeCalibrationCache和readCalibrationCache。最簡單的實現是從write()和read()返回值,這樣就必須每次執行都做一次校準。如果想要存儲校準時間,需要實現用戶自定義的write/read方法,具體的實現可以參考TensorRT中的simpleINT8實例。

下圖是simple_int8 mnist實例

在這裏插入圖片描述
下面我將介紹一下tensorrt的int8 inference的方法

3.tensorrt的int8 inference基本介紹

  • 目標: 在沒有明顯準確度丟失的情況下將FP32的CNNs網絡轉換爲INT8
  • 理由: INT8類型的存儲方式有很高的通量和較低的內存需求
  • 挑戰: 相對於FP32, INT8有明顯較低的精度和動態範圍
  • 解決方式: 在將權值以及計算時最小化有效信息損失.
  • 結果: 上述轉換可以通過TensorRT來進行實現,同時該方法不需要額外的大量調整和重新訓練.

4. 面臨的挑戰

相對於FP32,INT8的精度和動態範圍要小很多:

在這裏插入圖片描述
從FP32到INT8需要不止一次的類型轉換

5. int8 類型的計算方式爲何具有高通量

需要sm_61+ (Pascal TitanX, GTX 1080, Tesla P4, P40 and others)
四位字節積在32位結果中
計算方法: Result += A[0] * B[0] +A[1] * B[1] +A[2] * B[2] +A[3] * B[3]

在這裏插入圖片描述

我們必須保證沒有精度損失和解決方案簡單並且計算效率高

6.計算方法

6.1 線性量化法:

表現爲: Tensor Values = FP32 scale factor * int8 array + FP32 bias

實際上我們並不需要FP32 bias

在這裏插入圖片描述
在這裏插入圖片描述

所以:Tensor Values = FP32 scale factor * int8 array,即A = scale_A * QA

其中FP32 scale factor是一個浮點型的縮減係數,它隨着優化過程改變 int8 array爲一個int8型的矩陣

6.2 如何獲得FP32 scale factor

量化:

在這裏插入圖片描述
可見:它分爲了兩種情況,飽和與不飽和

不飽和我們可以將它全部映射到int8的精度上,但是過飽和時就需要設置閾值(saturate)
在這裏插入圖片描述
那我們如何去優化閾值的選擇呢?

最小化信息丟失,因爲FP32~int8只是重新編碼信息。我們選擇閾值時應儘量保證信息少丟失就行。下圖是幾種隨着閾值變化而歸一化數據指數變化的趨勢(針對不同的模型,不同的layer,這個指數是不一樣的)

我們用兩種編碼的相對熵來表示這個映射過程的好壞。

信息損失由Kullback-Leibler divergence (AKA relative entropy or information divergence)來衡量

  • P, Q -兩個離散概率分佈
  • KL_divergence(P,Q):= SUM(P[i] * log(P[i] / Q[i] ), i)

KL_divergence(P,Q)是判斷映射的好壞,但如何進行映射呢,這裏使用的是一個Calibration(校準器)或者說一個calibration dataset(矯正數據集)

過程如下:

在校準數據集上運行FP32推斷。
對每一層

  1. 收集激活的直方圖。
  2. 基於不同的飽和度閾值,生成多個量化分佈
  3. 取最小閾值的量化

其實上面說起來基本思路是首先構建一種FP32數據向INT8數據的映射關係,該映射中的邊界並不是兩種數據類型的最大值,而是將FP32設置成一個Threshold,將這個閾值與INT8的最大值(127)構建映射關係,具體實現形式是通過一個scale來進行對應。

首先的問題是,這種映射關係的閾值如何尋找呢?不同的網路顯然這一閾值是不同的,因此我們需要一個矯正數據集(calibration dataset)來進行scale的選取,其選擇的標準爲最小化KL_divergence(: KL_divergence(P,Q):= SUM(P[i] * log(P[i] / Q[i] ), i))

tensorrt上int8的工作流程;

你需要準備:

  • 一個已經訓練好的FP32的model
  • 校準器(Calibration dataset.)

TensorRT將會:

  • 在FP32上對校準數據集進行運行推斷
  • 收集需要的數據(不同閾值下的KL量化分佈圖)
  • 運行矯正算法–> 優化scale係數
  • 量化FP32權值到INT8
  • 產生CalibrationTable和INT8 execution engine

7. int8的效果

它能更高效的完成inference,且沒有大的性能損失
在這裏插入圖片描述

  1. 補充、
    8.1 熵校準的僞碼:

Input: FP32 histogram H with 2048 bins: bin[ 0 ],, bin[ 2047 ]
For i in range( 128 , 2048 ):
reference_distribution_P = [ bin[ 0 ] , ..., bin[ i-1 ] ] // take first ‘ i ‘ bins from H
outliers_count = sum( bin[ i ] , bin[ i+1 ] ,, bin[ 2047 ] )
reference_distribution_P[ i-1 ] += outliers_count
P /= sum(P) // normalize distribution P
candidate_distribution_Q = quantize [ bin[ 0 ],, bin[ i-1 ] ] into 128 levels 
                                               // explained later
expand candidate_distribution_Q to ‘ i ’ bins // explained later
Q /= sum(Q) // normalize distribution Q
divergence[ i ] = KL_divergence( reference_distribution_P, candidate_distribution_Q)
End For
Find index ‘m’ for which divergence[ m ] is minimal
threshold = ( m + 0.5 ) * ( width of a bin )
 
 
/*● KL_divergence(P, Q) requires that len(P) == len(Q)
● Candidate distribution Q is generated after merging ‘ i ’ bins from bin[0] to
bin[i-1] into 128 bins
● Afterwards Q has to be ‘expanded’ again into ‘i’ bins
Here is a simple example: reference distribution P consisting of 8 bins, we want to quantize into 2 bins:
P = [ 1, 0, 2, 3, 5, 3, 1, 7]
we merge into 2 bins (8 / 2 = 4 consecutive bins are merged into one bin)
[1 + 0 + 2 + 3 , 5 + 3 + 1 + 7] = [6, 16]
then proportionally expand back to 8 bins, we preserve empty bins from the original distribution P:
Q = [ 6/3, 0, 6/3, 6/3, 16/4, 16/4, 16/4, 16/4] = [ 2, 0, 2, 2, 4, 4, 4, 4]
now we should normalize both distributions, after that we can compute KL_divergence
P /= sum(P) Q /= sum(Q)
result = KL_divergence(P, Q)*/

8.2 int8 卷積核的僞碼:

// I8 input tensors: I8_input, I8_weights, I8 output tensors: I8_output
// F32 bias (original bias from the F32 model)
// F32 scaling factors: input_scale, output_scale, weights_scale[K]
I32_gemm_out = I8_input * I8_weights // Compute INT8 GEMM (DP4A)
F32_gemm_out = (float)I32_gemm_out // Cast I32 GEMM output to F32 float
// At this point we have F32_gemm_out which is scaled by ( input_scale * weights_scale[K] ),
// but to store the final result in int8 we need to have scale equal to "output_scale", so we have to rescale:
// (this multiplication is done in F32, *_gemm_out arrays are in NCHW format)
For i in 0, ... K-1:
rescaled_F32_gemm_out[ :, i, :, :] = F32_gemm_out[ :, i, :, :] * [ output_scale / (input_scale * weights_scale[ i ] ) ]
// Add bias, to perform addition we have to rescale original F32 bias so that it's scaled with "output_scale"
rescaled_F32_gemm_out _with_bias = rescaled_F32_gemm_out + output_scale * bias
// Perform ReLU (in F32)
F32_result = ReLU(rescaled_F32_gemm_out _with_bias)
// Convert to INT8 and save to global
I8_output = Saturate( Round_to_nearest_integer( F32_result ) )
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章