https://zhuanlan.zhihu.com/p/405571578
這是那會的一篇文章,略顯稚嫩哈哈:
轉眼間過了這么久啦,神經網絡量化應用已經完全實現大面積落地了、相比之前成熟多了!
我工作的時候雖然也簡單接觸過量化,但感覺還遠遠不夠,趁着最近項目需要,重新再學習一下,也打算把重新學習的路線寫成一篇系列文,分享給大家。
本篇系列文的主要內容計划從頭開始梳理一遍量化的基礎知識以及代碼實踐。因為老潘對TensorRT比較熟悉,會主要以TensorRT的量化方式進行描述以及講解。不過TensorRT由於是閉源工具,內部的實現看不到,咱們也不能兩眼一抹黑。所以也打算參考Pytorch、NCNN、TVM、TFLITE的量化op的現象方式學習和實踐一下。
當然這只是學習計划,之后可能也會變動。對於量化我也是學習者,既然要用到這個技術,必須要先理解其內部原理。而且接觸了挺長時間量化,感覺這里面學問還是不少。好記性不如爛筆頭,寫點東西記錄下,也希望這系列文章在能夠幫助大家的同時,拋磚引玉,一起討論、共同進步。
參考了以下關於量化的一些優秀文章,不完全統計列了一些,推薦感興趣的同學閱讀:
當然在學習途中,也認識了很多在量化領域經驗豐富的大佬(田子宸、JermmyXu等等),嗯,這樣前進路上也就不孤單了。
OK,廢話不多說開始吧。
Why量化
我們都知道,訓練好的模型的權重一般來說都是FP32也就是單精度浮點型,在深度學習訓練和推理的過程中,最常用的精度就是FP32。當然也會有FP64、FP16、BF16、TF32等更多的精度:

FP32 是單精度浮點數,用8bit 表示指數,23bit 表示小數;FP16半精度浮點數,用5bit 表示指數,10bit 表示小數;BF16是對FP32單精度浮點數截斷數據,即用8bit 表示指數,7bit 表示小數。TF32 是一種截短的 Float32 數據格式,將 FP32 中 23 個尾數位截短為 10 bits,而指數位仍為 8 bits,總長度為 19 (=1 + 8 + 10) bits。
對於浮點數來說,指數位表示該精度可達的動態范圍,而尾數位表示精度。之前老潘的一篇文章中提到,FP16的普遍精度是~5.96e−8 (6.10e−5) … 65504
,而我們模型中的FP32權重有部分數值是1e-10
級別。這樣從FP32->FP16會導致部分精度丟失,從而模型的精度也會下降一些。

其實從FP32->FP16也是一種量化,只不過因為FP32->FP16幾乎是無損的(CUDA中使用__float2half
直接進行轉換),不需要calibrator
去校正、更不需要retrain
。
而且FP16的精度下降對於大部分任務影響不是很大,甚至有些任務會提升。NVIDIA對於FP16有專門的Tensor Cores可以進行矩陣運算,相比FP32來說吞吐量提升一倍。

實際點來說,量化就是將我們訓練好的模型,不論是權重、還是計算op,都轉換為低精度去計算。因為FP16的量化很簡單,所以實際中我們談論的量化更多的是INT8的量化,當然也有3-bit、4-bit的量化,不過目前來說比較常見比較實用的,也就是INT8量化了,之后老潘的重點也是INT8量化。
那么經過INT8量化后的模型:
- 模型容量變小了,這個很好理解,FP32的權重變成INT8,大小直接縮了4倍
- 模型運行速度可以提升,實際卷積計算的op是INT8類型,在特定硬件下可以利用INT8的指令集去實現高吞吐,不論是GPU還是INTEL、ARM等平台都有INT8的指令集優化
- 對於某些設備,使用INT8的模型耗電量更少,對於嵌入式側端設備來說提升是巨大的
所以說,隨着我們模型越來越大,需求越來越高,模型的量化自然是少不了的一項技術。
如果你擔心INT8量化對於精度的影響,我們可以看下NVIDIA量化研究的一些結論:

出自《INTEGER QUANTIZATION FOR DEEP LEARNING INFERENCE: PRINCIPLES AND EMPIRICAL EVALUATION》,文末有下載鏈接。
量化現狀
量化技術已經廣泛應用於實際生產環境了,也有很多大廠開源了其量化方法。不過比較遺憾的是目前這些方法比較瑣碎,沒有一套比較成熟比較完善的量化方案,使用起來稍微有點難度。不過我們仍可以從這些框架中學習到很多。
谷歌是比較早進行量化嘗試的大廠了,感興趣的可以看下Google的白皮書Quantizing deep convolutional networks for efficient inference: A whitepaper
以及Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference
。
TensorFlow很早就支持了量化訓練,而TFLite也很早就支持了后訓練量化,感興趣的可以看下TFLite的量化規范,目前TensorRT支持TensorFlow訓練后量化的導出的模型。
TensorRT
TensorRT在2017年公布了自己的后訓練量化方法,不過沒有開源,NCNN按照這個思想實現了一個,也特別好用。不過目前TensorRT8也支持直接導入通過ONNX導出的QTA好的模型,使用上方便了不少,之后老潘會重點講下。

NVIDIA自家也推出了針對Pytorch的量化工具(為什么沒有TensorFlow,因為TF已經有挺好用的官方工具了),支持PTQ以及QTA,稱為Pytorch Quantization,之后也會提到。
TVM
TVM有自己的INT8量化操作,可以跑量化,我們也可以添加自己的算子。不過TVM目前只支持PTQ,可以通過交叉熵或者percentile的方式進行校准。不過如果動手能力強的話,應該可以拿自己計算出來的scale值傳入TVM去跑,應該也有人這樣做過了。
比較有參考意義的一篇:
...
當然還有很多優秀的量化框架,想看詳細的可以看這篇,后續如果涉及到具體知識點老潘也會再提到。
量化基本知識
進入主題前需要提兩個概念,也就是量化的兩個重要過程,一個是量化(Quantize),另一個是反量化(Dequantize):
- 量化就是將浮點型實數量化為整型數(FP32->INT8)
- 反量化就是將整型數轉換為浮點型實數(INT8->FP32)

量化和反量化操作在最終的模型推理中都會用到,接下來我們就具體說下。
之后實數就代表我們的FP32浮點數,而整數就代表INT8整型數。
量化操作
比如有一個FP32的浮點型數字,然后我們需要把這個數變為整型,也就是要量化它,怎么搞。我們可以把這個數字乘上一個量化系數
,比如
,那么量化后的值
,然后我們對這個數字進行四舍五入(也就是round操作)最終為
這樣就行了嗎,523有點大啊,我們的整型INT8的范圍是[-128,127],無符號INT8的范圍也才[0-255],這個量化后的值有點放不下呀。
怎么辦,當然是要截斷了,假設我們的INT8范圍是,因為我們使用的是INT8,所以這里的
,那么上述的式子又可以變為:
這樣就結束了么?
當然沒有,剛才的這個數字,被映射到了127,那么如果是
呢?貌似直接帶入算出來也是0,但是這樣做對么?
基於線性量化的對稱量化和非對稱量化
對不對的關鍵在於我們是否是采用對稱量化,什么是對稱量化呢?這里的對稱指的是以0為中心進行量化(還有另一種說法,這里老潘先略過),然后0兩邊的動態范圍都是一樣的。

可以看上圖,左邊是非對稱量化,右邊是對稱量化(也稱為Affine quantization和Scale quantization)。可以觀察到:
- 對稱量化的實數0也對應着整數的0,而非對稱量化的實數0不一定對應着整數0,而是z。
- 對稱量化實數的范圍是對稱的(
),而非對稱量化的則不對稱(
)
- 對稱量化整數的范圍是對稱的([-127,127]),而非對稱量化的則不對稱([-128,127])
所以上述的非對稱量化過程可以簡述為,其中
是
zero-point
,這個數字就代表實數0映射到整數是多少,而對稱量化則是。
這樣就明白了剛才的問題:如果是呢?貌似直接帶入算出來也是0,如果我們采用的是對稱量化,那就沒問題!
需要說明一點,不論是非對稱還是對稱量化,是基於線性量化(也可以稱作均勻量化)的一種。線性量化將FP32映射到INT8數據類型,每個間隔是相等的,而不相等的就稱為非線性量化。非線性量化因為對部署並不是很友好,雖然能夠更好地捕捉到權重分布的密集點,但感覺用的並不多,這里也就先不多說了。
關於詳細的非對稱量化,對稱量化對比
可以參考這篇文章:
對稱量化
接下來的重點是對稱量化,也就是TensorRT中使用的量化方式,這里的范圍也就是[-127,127],因為只比[-128,127]少了一個范圍,所以實際量化中並沒有太大的影響。
話說回來,上文量化操作中,量化系數隨便說了個,這個當然是不對的,這個
需要根據我們的實際數據分布來計算。

如上式,代表當前輸入數據分布中的實數最大值,因為是對稱,因此實際范圍是
。而
代表INT8量化,那么上述的量化公式就是之前提到的對稱量化公式。
可以對比下非對稱和對稱的量化公式,對稱量化因為,所以公式簡化了很多。

對於對稱量化,假設當前根據權重分布,選取的為4,那么
。
如下式子,在反量化的時候我們需要將反向操作一番,將量化后的結果乘以重新變為浮點型。這里其實也就相當於乘以
,因為有
。

那么實際操作過程中,scale系數是怎么用呢?或者說這個量化系數是怎么作用於所有的輸入、所有的權重呢?
一般量化過程中,有pre-tensor
和pre-channel
兩種方式,pre-tensor
顯而易見,就是對於同一塊輸入(比如某個卷積前的輸入tensor)我們采用一個scale,該層所有的輸入數據共享一個scale值;而pre-channel
呢一般是作用於權重,比如一個卷積的權重維度是[64,3,3,3](輸入通道為3輸出通道為64,卷積核為3x3),pre-channel
就是會產生64個scale值,分別作用於該卷積權重參數的64個通道。
為什么權重不能是pre-tensor
呢?這個對精度的影響太大了,所以一般不用。輸入就可以pre-tensor
?當然可以,也經過測試了,對精度的影響不是很大,完全可以用。
那為什么權重必須是pre-channel
呢?不能是每個權重值都有自己的scale么?呃,這個問題嘛,首先可以想到,這個計算量,應該挺大,其次嘛,讓我們分析一下。
卷積操作量化
鋪墊了這么多,那么接下來說下量化最核心的操作吧,量化過程中最核心的操作當然是卷積量化。
我們都知道卷積操作可以拆分為im2col+sgemm,而大部分的計算都在矩陣運算也就是sgemm中,我們量化的重點也就是這個操作。以前是FP32計算,而現在變成INT8去計算,這是怎么轉換的呢?
接下來重點分析一下量化公式!注意!這個很重要!
首先,矩陣相乘可以表示為,X為輸入W為權重,Y為輸出。偏置bias一般可以去掉,對精度影響也不大,所以就先不考慮了。

注意看上圖輸入X的維度為[m,p]而W的維度為[p,n],因此i的范圍為[0,m),k的范圍為[0,p)。W和Y同理。這里的輸入和權重都是FP32精度,也就是實數。
而對應的INT8精度的輸入和權重為,q下標就代表quantize也就是量化:

接下來,我們把矩陣公式細粒度拆成一個一個計算,也就是行和列每個元素相乘然后求和:

首先是最左邊,和
分別代表浮點型的輸入和權重,
代表第
行,
代表第
列,因此
代表第
行,第
列的元素,
同理。兩者相乘求和就可以得到
,可以看到這里求和的范圍是
,
從1到
變化。
進一步,兩個浮點型的運算可以被近似為INT8反量化后的運算,進一步等於量化后的運算:

可以看到上式每個元素都有自己的scale值,也就是,而我們也必須把x和w的scale值提取到前面才能讓x和w實現INT8類型的矩陣運算:

這里可以發現,如果想要把這兩個scale元素,也就是和
提出來,那么這個
必須干掉,這里可以暫停一下想下為什么?

當把k去除將s取出來之后,我們發現和
分別代表輸入的第
行的scale和權重的第
列的scale值,這樣輸入的每一行必須共享scale,而權重的每一列也必須共享scale!

那么pre-channel
又是怎么來的呢?
還記得之前說過的im2col+sgemm操作嗎(如果不記得強烈建議去看看),其中的sgemm
是這樣的,需要注意,下圖左邊的kernel矩陣,每一行代表一個輸出通道的kernel集合(這里因為輸入圖像是三通道的,因此kernel有三個,不同顏色代表一個kernel):

這就是pre-channel
或者詳細點就是per-output-channel
也就是卷積輸出通道,我們對每一個卷積權重的輸出通道那一維進行量化,然后共享一個scale,這也就呼應了上述的公式!
后記
到此我們已經講述了量化的基本概念以及卷積量化的實際操作是什么樣的,當然想說的還有很多...就是現在實在寫不動了,關於非對稱量化的公式以及為什么非對稱量化計算量比較大,就放到第二期再說吧。文中提到的一些資料,號內回復”量化“即可獲取。
后續文章會繼續說明其他量化的操作細節以及實際部署中的代碼細節,涉及到TensorRT以及Pytorch和TVM,感興趣的不妨持續關注老潘~
也歡迎大家一起討論,如有錯誤也歡迎指正。