Optimum 文件
量化
並獲得增強的文件體驗
開始使用
量化
量化是一種透過使用低精度資料型別(如8位整數 int8
)代替通常的32位浮點數(float32
)來表示權重和啟用值,從而降低執行推理的計算和記憶體成本的技術。
減少位元數意味著最終的模型需要更少的記憶體儲存,消耗更少的能源(理論上),並且像矩陣乘法這樣的操作可以用整數算術更快地執行。它還允許在嵌入式裝置上執行模型,這些裝置有時只支援整數資料型別。
理論
量化背後的基本思想很簡單:將權重和啟用值從高精度表示(通常是常規的32位浮點數)轉換為較低精度的資料型別。最常見的低精度資料型別是
float16
,累積資料型別為float16
bfloat16
,累積資料型別為float32
int16
,累積資料型別為int32
int8
,累積資料型別為int32
累積資料型別指定了對該資料型別的值進行累積(加法、乘法等)操作後結果的型別。例如,讓我們考慮兩個 int8
值 A = 127
和 B = 127
,並定義 C
為 A
和 B
的和
C = A + B
這裡的結果遠大於 int8
中可表示的最大值 127
。因此需要一個更高精度的資料型別來避免巨大的精度損失,否則整個量化過程將變得毫無用處。
量化
兩種最常見的量化情況是 float32 -> float16
和 float32 -> int8
。
量化到 float16
從 float32
量化到 float16
的過程非常直接,因為這兩種資料型別遵循相同的表示方案。在將操作量化到 float16
時,你需要問自己以下問題:
- 我的操作是否有
float16
的實現? - 我的硬體是否支援
float16
?例如,英特爾 CPU 一直支援float16
作為儲存型別,但計算是在轉換為float32
後完成的。完全支援將在 Cooper Lake 和 Sapphire Rapids 中實現。 - 我的操作對較低精度敏感嗎?例如,
LayerNorm
中 epsilon 的值通常非常小(~1e-12
),但float16
中可表示的最小正值約為6e-5
,這可能導致NaN
問題。同樣的問題也適用於非常大的值。
量化到 int8
從 float32
量化到 int8
更加棘手。int8
只能表示256個值,而 float32
可以表示非常廣泛的值。其思想是找到最佳的方式將我們的 float32
值範圍 [a, b]
對映到 int8
空間。
讓我們考慮一個在 [a, b]
範圍內的浮點數 x
,那麼我們可以寫出以下量化方案,也稱為仿射量化方案
x = S * (x_q - Z)
其中
x_q
是與x
關聯的量化後int8
值S
和Z
是量化引數S
是縮放因子,是一個正的float32
值Z
稱為零點,它是在float32
領域中對應於值0
的int8
值。這對於能夠精確表示值0
非常重要,因為它在機器學習模型中無處不在。
在 [a, b]
範圍內的 x
的量化值 x_q
可以計算如下
x_q = round(x/S + Z)
而在 [a, b]
範圍之外的 float32
值會被裁剪到最接近的可表示值,所以對於任何浮點數 x
x_q = clip(round(x/S + Z), round(a/S + Z), round(b/S + Z))
通常 round(a/S + Z)
對應於所考慮資料型別中可表示的最小值,而 round(b/S + Z)
對應於最大值。但這也可能變化,例如在使用對稱量化方案時,你將在下一段中看到。
對稱和仿射量化方案
上面的方程被稱為仿射量化方案,因為從 [a, b]
到 int8
的對映是仿射的。
這個方案的一個常見特例是對稱量化方案,我們考慮一個對稱的浮點值範圍 [-a, a]
。在這種情況下,整數空間通常是 [-127, 127]
,這意味著 -128
被從常規的 [-128, 127]
有符號 int8
範圍中排除了。原因是擁有對稱範圍可以使 Z = 0
。雖然在256個可表示的值中損失了一個,但這可以提供加速,因為可以跳過許多加法操作。
注意:要了解量化引數 S
和 Z
是如何計算的,您可以閱讀 《為高效的純整數算術推理而進行的神經網路量化與訓練》 論文,或 Lei Mao關於該主題的部落格文章。
逐張量和逐通道量化
根據您所追求的精度/延遲權衡,您可以調整量化引數的粒度。
- 量化引數可以按逐張量計算,這意味著每個張量將使用一對
(S, Z)
。 - 量化引數可以按逐通道計算,這意味著可以為張量的某個維度上的每個元素儲存一對
(S, Z)
。例如,對於一個形狀為[N, C, H, W]
的張量,為第二個維度設定逐通道量化引數將導致有C
對(S, Z)
。雖然這可以提供更好的精度,但需要更多記憶體。
校準
上一節描述了從 float32
到 int8
的量化過程,但還有一個問題:float32
值的 [a, b]
範圍是如何確定的?這就是校準發揮作用的地方。
校準是量化過程中計算 float32
範圍的步驟。對於權重來說,這很簡單,因為實際範圍在量化時是已知的。但對於啟用值來說,情況就不那麼明朗了,存在不同的方法:
- 訓練後動態量化:每個啟用值的範圍在執行時動態計算。雖然這種方法無需太多工作就能獲得很好的結果,但由於每次計算範圍會引入開銷,因此可能比靜態量化稍慢。在某些硬體上,它也不是一個可行的選項。
- 訓練後靜態量化:每個啟用值的範圍在量化時預先計算好,通常是透過將代表性資料透過模型並記錄啟用值來實現。實踐中的步驟是:
- 在啟用值上放置觀察器以記錄其值。
- 在校準資料集上進行一定數量的前向傳播(大約
200
個樣本就足夠了)。 - 根據某種校準技術計算每個計算的範圍。
- 量化感知訓練:每個啟用值的範圍在訓練時計算,其思想與訓練後靜態量化相同。但使用“偽量化”運算元代替觀察器:它們像觀察器一樣記錄值,但同時也模擬量化引起的誤差,讓模型適應它。
對於訓練後靜態量化和量化感知訓練,都需要定義校準技術,最常見的有:
- 最小值-最大值:計算的範圍是
[觀察到的最小值, 觀察到的最大值]
,這對於權重效果很好。 - 移動平均最小值-最大值:計算的範圍是
[移動平均觀察到的最小值, 移動平均觀察到的最大值]
,這對於啟用值效果很好。 - 直方圖:記錄值的直方圖以及最小值和最大值,然後根據某個標準進行選擇。
- 熵:計算的範圍是使全精度資料和量化資料之間誤差最小的範圍。
- 均方誤差:計算的範圍是使全精度資料和量化資料之間均方誤差最小的範圍。
- 百分位數:使用給定的百分位值
p
對觀察值計算範圍。其思想是嘗試使p%
的觀察值在計算的範圍內。雖然在進行仿射量化時這是可能的,但在進行對稱量化時並不總能精確匹配。您可以檢視 ONNX Runtime 中的實現方式 以瞭解更多細節。
將模型量化到 int8 的實際步驟
為了有效地將模型量化到 int8
,需要遵循以下步驟:
- 選擇要量化的運算元。適合量化的運算元是那些在計算時間上占主導地位的,例如線性投影和矩陣乘法。
- 嘗試訓練後動態量化,如果速度足夠快,則到此為止,否則繼續第3步。
- 嘗試訓練後靜態量化,這可能比動態量化更快,但通常會降低精度。在您想要量化的地方為模型應用觀察器。
- 選擇一種校準技術並執行。
- 將模型轉換為其量化形式:移除觀察器,並將
float32
運算元轉換為其int8
對應物。 - 評估量化後的模型:精度是否足夠好?如果是,則到此為止,否則從第3步重新開始,但這次使用量化感知訓練。
在 🤗 Optimum 中執行量化支援的工具
🤗 Optimum 提供了 API,可以使用不同的工具針對不同的目標執行量化:
optimum.onnxruntime
包允許使用 ONNX Runtime 工具量化和執行 ONNX 模型。optimum.intel
包能夠量化 🤗 Transformers 模型,同時滿足精度和延遲約束。optimum.fx
包提供了對 PyTorch 量化函式 的封裝,以允許在 PyTorch 中對 🤗 Transformers 模型進行圖模式量化。與上述兩種相比,這是一個更低階的 API,提供了更大的靈活性,但需要您做更多的工作。optimum.gptq
包允許使用 GPTQ 量化和執行 LLM 模型。
更進一步:機器如何表示數字?
本節對於理解其餘部分並非至關重要。它簡要解釋了數字在計算機中的表示方式。由於量化是從一種表示轉換為另一種表示,瞭解一些基礎知識可能會有所幫助,但這絕不是強制性的。
計算機最基本的表示單位是位元。計算機中的一切都表示為位元序列,包括數字。但根據所討論的數字是整數還是實數,其表示方式有所不同。
整數表示
整數通常用以下位元長度表示:8
、16
、32
、64
。在表示整數時,考慮兩種情況:
- 無符號(正)整數:它們簡單地表示為位元序列。每個位元對應於2的冪(從
0
到n-1
,其中n
是位元長度),最終的數字是這些2的冪的和。
例如:19
表示為一個無符號的 int8 是 00010011
,因為
19 = 0 x 2^7 + 0 x 2^6 + 0 x 2^5 + 1 x 2^4 + 0 x 2^3 + 0 x 2^2 + 1 x 2^1 + 1 x 2^0
- 有符號整數:表示有符號整數不那麼直接,存在多種方法,最常見的是二進位制補碼。更多資訊,您可以檢視關於該主題的 維基百科頁面。
實數表示
實數通常用以下位元長度表示:16
、32
、64
。表示實數的兩種主要方式是
- 定點數:保留固定數量的數字來表示整數部分和小數部分。
- 浮點數:用於表示整數部分和小數部分的數字數量可以變化。
浮點表示法可以表示更大範圍的值,這是我們將重點關注的,因為它最常用。浮點表示法有三個組成部分
- 符號位:這是指定數字符號的位元。
- 指數部分
float16
中有 5 位元bfloat16
中有 8 位元float32
中有 8 位元float64
中有 11 位元
- 尾數部分
float16
中有 11 位元(10 個顯式儲存)bfloat16
中有 8 位元(7 個顯式儲存)float32
中有 24 位元(23 個顯式儲存)float64
中有 53 位元(52 個顯式儲存)
有關每種資料型別的位元分配的更多資訊,請檢視維基百科上關於 bfloat16 浮點格式 的精美插圖。
對於一個實數 x
,我們有
x = sign x mantissa x (2^exponent)
參考文獻
- 論文 Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference
- 部落格文章 Basics of Quantization in Machine Learning (ML) for Beginners
- 部落格文章 How to accelerate and compress neural networks with quantization
- 維基百科關於整數表示的頁面在此和在此
- 維基百科頁面關於