跳至主要内容

M05-1 | 完整 PTQ:先決定怎麼衡量量化掉了多少

Open In Colab

PTQ 的四個步驟(export、prepare、校準、convert)在 M01-1 走過了。這一章要回答的是:量化之後結果不理想,往哪裡查。 查之前得先有一把夠敏感的尺。這本 notebook 用同一個訓練好的模型比較兩把尺:

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)

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 < 35
accuracy 差 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 > 0
per-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 用同一把尺看校準資料的影響。