跳至主要内容

M05 概覽 | PTQ 的精度除錯:先量、再縮小範圍、最後用實驗確認

Open In Colab

M01 走過 PTQ 的四個步驟。實際用在模型上,最常遇到的問題是:量化之後精度掉了,該往哪裡查? 這一章用同一個訓練好的模型,建立一套查問題的順序。後面五本 notebook(M05-1~M05-5,M05-5 是選讀)逐一動手驗證。

為什麼需要一套查問題的順序

量化誤差可能來自很多地方:校準資料太少或不具代表性、某一層的數值範圍特別難量化、最後的輸出對誤差特別敏感…… 直接猜、直接改設定,常常改了半天沒有進步。這一章回答四個問題:

問題 一句話的答案 動手驗證
怎麼衡量量化掉了多少? 用輸出 SQNR,accuracy 常常不夠敏感 M05-1
校準資料夠不夠? 換幾份校準資料重跑,看結果的波動 M05-2
誤差從哪裡來? numeric debugger 給每個位置一個 SQNR,用來縮小範圍 M05-3
能不能把一部分留在 fp32? 可以;但要用實驗確認排除哪裡真的有用 M05-4

選讀的 M05-5 是另一個題目:weight 不是直接存的參數,而是「由別的參數算出來」時,PTQ 怎麼處理(見後面的〈選讀〉一節)。

先看一個例子:accuracy 幾乎沒變,SQNR 卻只有 27 dB

本章的模型是訓練好的 TinyConvNet(conv + BN + relu、殘差 block、pooling、最後的 linear),由本專案的 trained_tiny_net()(common/data.py)用固定 seed 訓練。 用 XNNPACKQuantizer 做一次 PTQ,比較量化前後:

模型 accuracy(1024 筆) 輸出 SQNR (dB)
fp32 0.7109 —
int8(PTQ) 0.7051 27.35

accuracy 只差零點幾個百分點,看起來量化幾乎無損;但輸出 SQNR 告訴我們,量化後的輸出和 fp32 之間有一定的誤差。 這個合成任務對量化很不敏感,accuracy 會被雜訊蓋過,所以本章用 SQNR 當主指標。

定義

輸出 SQNR

對同一批輸入,fp32 模型的輸出是 \(y\),量化後的輸出是 \(\hat{y}\)。SQNR(signal-to-quantization-noise ratio)是訊號功率與誤差功率的比,換成 dB:

\[ \mathrm{SQNR} = 10 \log_{10} \frac{\operatorname{mean}(y^2)}{\operatorname{mean}\big((y - \hat{y})^2\big)} \quad \text{(dB)} \tag{1} \]

越大越接近 fp32。換個角度看:SQNR 每高 20 dB,誤差的均方根就小 10 倍。27 dB 大約是誤差的均方根有訊號的 4.5%。 它只比兩個模型的輸出,不需要 label。本專案用 common.graph_utils.sqnr(ref, test) 計算,和式 (1) 比對:

y = torch.randn(1000)
y_hat = y + 0.05 * torch.randn(1000)
by_formula = float(10 * torch.log10(y.pow(2).mean() / (y - y_hat).pow(2).mean()))  # 式 (1)
print(f"式 (1):{by_formula:.3f} dB;sqnr():{sqnr(y, y_hat):.3f} dB")
assert abs(by_formula - sqnr(y, y_hat)) < 1e-4
式 (1):26.178 dB;sqnr():26.178 dB

逐層 SQNR

式 (1) 也可以算在圖中間每個 node 的輸出上:參考圖與量化圖跑同一批輸入,在同一個 node 比較。torchao 的 numeric debugger 做這件事(M05-3)。 參考圖要用「prepare_pt2e 之後、校準之前」的複本:BN 已經折掉,運算和量化圖一一對應,差別只有量化。

Mixed precision

讓模型的一部分留在 fp32、其餘量化。做法是讓 Quantizer 不在那一部分貼標籤(M03-6),M05-4 寫一個包住 XNNPACKQuantizer 的 SkipQuantizer 來做。

查問題的順序

整體輸出 SQNR          有沒有問題?(M05-1)
  → 換幾份校準資料     結果穩不穩?波動大就先加校準資料(M05-2)
  → 逐層 SQNR          誤差在哪附近變大?找出候選(M05-3)
  → 排除候選、重量     排除它,整體 SQNR 真的回升嗎?(M05-4)
  → 選動得最少的做法   例如只拿掉一個 module 的輸出量化(M05-4 C)

本章在這個模型上實際走過一遍:逐層 SQNR 掉最多的是 head_conv,把它留在 fp32 卻沒有幫助; 真正有用的是最後的 fc,而且只要拿掉它的輸出量化就夠了。逐層分數是線索,不是結論。

選讀:weight 由別的參數算出來時(M05-5)

torch.nn.utils.parametrize 可以讓一個參數變成「由別的參數算出來」:註冊之後,每次存取 conv.weight 都會重新計算一次。 用法見 PyTorch 教學 Parametrizations 與 register_parametrization 的文件。

常見的例子是 weight_norm。它把 weight 拆成方向 \(v\) 與長度 \(g\),每個輸出 channel 各自算:

\[ W = g \cdot \frac{v}{\lVert v \rVert} \tag{2} \]

訓練時更新的參數是 \(g\) 與 \(v\),\(W\) 只是算出來的值。

observer 插在哪裡

export 追蹤 forward 時,會把式 (2) 一起記進圖:conv 的 weight 不再是讀參數的 get_attr node,而是 aten._weight_norm 這個運算的輸出。 Quantizer 標的是 conv 的 weight 輸入,所以 observer 接在 _weight_norm 的輸出上。量化的是算出來的 \(W\),\(g\)、\(v\) 本身不量化。

這正是該量化的位置:推論時 conv 只用到 \(W\)。沒有 BN 時 prepare、convert 都正常;convert 預設 fold_quantize=True,會把 _weight_norm 連同 weight 的 quantize 一起預先算成 int8 常數,圖上就看不到 \(g\)、\(v\) 了。

有 BN 時,prepare_pt2e 會報錯

prepare_pt2e 一開始就把 BN 折進 conv(M04-1):要把 conv 的 weight 當常數讀出來、乘上 BN 的係數再寫回去。 weight 是 _weight_norm 的輸出、不是常數,所以直接報錯,不會默默跳過:

ValueError: Expected get_attr node, got call_function

怎麼解

PTQ 不訓練,用不到 \(g\)、\(v\)。把 \(W\) 算好、存成一般的參數,BN 就能照常折。有兩個做法:

時機 做法 要注意的
export 之前 remove_parametrizations(..., leave_parametrized=True):把當下算出的 weight 存成一般參數 只對 deepcopy 出的複本呼叫,原模型也會壞掉
export 之後 strip_weight_norm(gm):把圖上的 _weight_norm 換成常數 只處理 weight_norm

strip_weight_norm 是本專案的工具(common/weight_norm.py),不是 torch 或 torchao 的 API。讀者只要知道它做的事;它怎麼改圖,以及 deepcopy 的陷阱為什麼會發生,都在 M05-5 D 段。

QAT 要在訓練時更新 \(g\)、\(v\),不能在 prepare 之前拿掉,做法不同,見 M06-6。

小結與本章地圖

  • 先決定怎麼量:輸出 SQNR 連續、不需要 label,比這個任務的 accuracy 敏感得多。
  • 校準資料要看波動;逐層 SQNR 用來縮小範圍;最後用實驗確認,選動得最少的改法。
  • (選讀)weight 由 parametrize 算出來時,observer 量化的是算出來的 weight;有 BN 時要先把它換成常數。
notebook 回答的問題
M05-1 完整 PTQ 與衡量方式 accuracy 與 SQNR 哪個看得出量化誤差;per-channel 與 per-tensor
M05-2 校準 校準樣本數與結果的波動;校準時 BN 會不會影響結果
M05-3 逐層診斷 numeric debugger 怎麼用、怎麼讀、參考圖用哪一張
M05-4 Mixed precision 排除哪個 module 真的有用、誤差怎麼累積
M05-5(選讀)weight_norm 由別的參數算出來的 weight:observer 插在哪、有 BN 時為什麼報錯、兩個解法

這一章刻意跳過的:PTQ 真的不夠時用訓練補回來(QAT,M06);後端實際的整數運算和 Q/DQ 圖差多少(M07 起);改進 PTQ 本身的演算法(例如 AdaRound、OCTAV),見部落格〈量化技術路線〉的 PTQ 一節。

參考資料