import copy
import torch
from common.data import calibration_batches, evaluate, make_loader, trained_tiny_net
from common.graph_utils import sqnr
from common.models import example_inputs
from common.pt2e import convert_pt2e, export_module, get_xnnpack_quantizer, prepare_pt2e, quiet
from common.viz import table
quiet()
_ = torch.manual_seed(0)M05-1 | 完整 PTQ:先決定怎麼衡量量化掉了多少
PTQ 的四個步驟(export、prepare、校準、convert)在 M01-1 走過了。這一章要回答的是:量化之後結果不理想,往哪裡查。 查之前得先有一把夠敏感的尺。這本 notebook 用同一個訓練好的模型比較兩把尺:
- A. 準備:訓練好的模型與一個
ptq()函式,後面幾本 notebook 都用同一套。 - B. accuracy 與輸出 SQNR:哪一個看得出量化誤差。
- C. 用 SQNR 比一個設定:per-channel 與 per-tensor weight。
A. 準備
trained_tiny_net() 是本專案的工具(common/data.py):用固定 seed,在合成的 4 類圖片上把 TinyConvNet 訓練 20 個 epoch, 回傳 eval 模式的模型。TinyConvNet 有 conv + BN + relu、一個殘差 block、pooling 與最後的 linear(fc),本章所有數字都來自它。
ptq() 把 M01-1 的四步驟包成一個函式。校準資料由 calibration_batches(n, seed) 產生:只有輸入、沒有 label 的合成資料,seed 不同就是不同的一份。
def ptq(model, per_channel=True, calib_samples=128, seed=123):
"""export → prepare_pt2e → 校準 → convert_pt2e,回傳 convert 之後的圖。"""
quantizer, _ = get_xnnpack_quantizer(is_per_channel=per_channel)
prepared = prepare_pt2e(export_module(model, example_inputs()), quantizer)
with torch.no_grad():
for batch in calibration_batches(calib_samples, seed=seed):
prepared(*batch)
return convert_pt2e(copy.deepcopy(prepared))
model = trained_tiny_net()
converted = ptq(model)
test_loader = make_loader(n=1024, seed=2, shuffle=False) # 測試資料:1024 筆,有 label
(x_eval,) = next(calibration_batches(64, seed=9)) # 算 SQNR 用的另一批輸入,不是校準資料B. accuracy 與輸出 SQNR
輸出 SQNR(signal-to-quantization-noise ratio)把量化後的輸出和 fp32 輸出之間的差,換算成 dB(定義見本章概覽)。 它只比兩個模型的輸出,不需要 label。common.graph_utils.sqnr(ref, test) 是本專案算它的函式。
acc_fp32, acc_int8 = evaluate(model, test_loader), evaluate(converted, test_loader)
with torch.no_grad():
out_sqnr = sqnr(model(x_eval), converted(x_eval))
table([["fp32", f"{acc_fp32:.4f}", "—"], ["int8(PTQ)", f"{acc_int8:.4f}", f"{out_sqnr:.2f}"]],
["模型", "accuracy(1024 筆)", "輸出 SQNR (dB)"])| 模型 | accuracy(1024 筆) | 輸出 SQNR (dB) |
|---|---|---|
| fp32 | 0.7109 | — |
| int8(PTQ) | 0.7051 | 27.35 |
print(f"accuracy 差 {abs(acc_fp32 - acc_int8):.4f}")
assert abs(acc_fp32 - acc_int8) < 0.02 and out_sqnr < 35accuracy 差 0.0059
- accuracy 幾乎沒動:這個合成任務對量化很不敏感,1024 筆裡量化前後只差幾筆答對。拿它比較不同設定,差別會被雜訊蓋過。
- 輸出 SQNR 是連續的數字,量化誤差變大變小都看得出來,而且不需要 label,診斷時一樣能用。
所以後面幾本 notebook 以輸出 SQNR 為主指標,accuracy 為輔。真實任務的 accuracy 會不會比較敏感,這裡沒有驗證。
C. 用 SQNR 比一個設定:per-channel 與 per-tensor
同一個模型、同一份校準資料,只換 weight 的量化粒度:
rows, scores = [], {}
for name, per_channel in (("per-channel weight", True), ("per-tensor weight", False)):
c = ptq(model, per_channel=per_channel)
with torch.no_grad():
scores[name] = sqnr(model(x_eval), c(x_eval))
rows.append([name, f"{evaluate(c, test_loader):.4f}", f"{scores[name]:.2f}"])
table(rows, ["設定", "accuracy", "輸出 SQNR (dB)"])| 設定 | accuracy | 輸出 SQNR (dB) |
|---|---|---|
| per-channel weight | 0.7051 | 27.35 |
| per-tensor weight | 0.7090 | 25.65 |
gap = scores["per-channel weight"] - scores["per-tensor weight"]
print(f"per-channel 比 per-tensor 高 {gap:.2f} dB")
assert gap > 0per-channel 比 per-tensor 高 1.70 dB
- SQNR 上 per-channel 比較好;accuracy 的差別在雜訊量級,看不出誰好。
- per-channel 的預期好處:BN 折進 conv 之後,各 output channel 的值域可能差很多(M04-1 C),per-tensor 會被值域最大的 channel 拖累。
- 這只是一個模型、一份校準資料的結果,不能推廣成「per-channel 一定比較好幾 dB」。設定的影響要用資料量出來。
小結
- 這個任務的 accuracy 對量化不敏感;輸出 SQNR 連續、不需要 label,是本章的主指標。
- 有了一把尺,就能比較設定:per-channel 在這個模型上的 SQNR 比較高。
- 下一本 M05-2 用同一把尺看校準資料的影響。