ort_default, _ = ort_quantize(fp32_onnx)first_conv =next(n.name for n in ort_default.graph.node if n.op_type =="Conv")draw_onnx(ort_default, focus=first_conv, hops=2, title="quantize_static 產生的 QDQ ONNX:第一個 conv 單元")
ORT 的工具只看得到 ONNX 圖,但做的決定和 PT2E 的 quantizer 同一類:哪條邊插 Q/DQ、scale 由校準資料算。
B. 精度
四個 .onnx 都用 ORT 的 CPU EP(預設等級 ORT_ENABLE_ALL,ort_session)跑同一份 1024 筆測試資料(同 M05-1):
ort_pc, _ = ort_quantize(fp32_onnx, per_channel=True)candidates = {"fp32 ONNX": fp32_onnx,"PT2E → torch.onnx.export": pt2e_onnx,"quantize_static:預設(per-tensor weight)": ort_default,"quantize_static:per_channel=True": ort_pc,}xs, ys =zip(*make_loader(n=1024, seed=2, shuffle=False))x_test, y_test = torch.cat(xs), torch.cat(ys)with torch.no_grad(): y_fp32 = model(x_test)acc, snr = {}, {}for name, m in candidates.items(): out = ort_run(ort_session(m), x_test) acc[name], snr[name] = (out.argmax(1) == y_test).float().mean().item(), sqnr(y_fp32, out)table([[name, "-"if name =="fp32 ONNX"elsef"{snr[name]:.2f}", f"{acc[name]:.4f}"] for name in candidates], ["", "對 PyTorch fp32 的 SQNR (dB)", "accuracy"])
對 PyTorch fp32 的 SQNR (dB)
accuracy
fp32 ONNX
-
0.7109
PT2E → torch.onnx.export
29.18
0.7061
quantize_static:預設(per-tensor weight)
27.81
0.7129
quantize_static:per_channel=True
29.68
0.7109
quantized =list(candidates)[1:]n_diff =round((max(acc[n] for n in quantized) -min(acc[n] for n in quantized)) *len(y_test))print(f"三個量化版本的 accuracy 最多差 {n_diff} 筆(共 {len(y_test)} 筆)")print(f"SQNR 最高:{max(quantized, key=lambda n: snr[n])};最低:{min(quantized, key=lambda n: snr[n])}")assertall(snr[n] >20for n in quantized)
qop, warnings = ort_quantize(fp32_onnx, quant_format=QuantFormat.QOperator)print("QOperator format 的 op:", dict(onnx_op_counts(qop)))for w in warnings:if"QuantFormat"in w: # 另外兩則建議先做 pre-processing,與本段無關print("quantize_static 印出的警告:", w.strip())
QOperator format 的 op: {'QuantizeLinear': 2, 'QLinearConv': 4, 'MaxPool': 2, 'com.microsoft.QLinearAdd': 1, 'DequantizeLinear': 2, 'ReduceMean': 1, 'Reshape': 1, 'com.microsoft.QGemm': 1}
quantize_static 印出的警告: Please use QuantFormat.QDQ for activation type QInt8 and weight type QInt8. Or it will lead to bad performance on x64.
first_qconv =next(n.name for n in qop.graph.node if n.op_type =="QLinearConv")draw_onnx(qop, focus=first_qconv, hops=1, title="QOperator format:檔案裡直接是 QLinearConv")
QOperator format 把融合好的 int8 op(QLinearConv、QLinearAdd…)直接寫進檔案,和 M09-2 裡 CPU EP 在 ORT_ENABLE_EXTENDED 改寫出來的圖幾乎一樣。ORT 的工具自己就印出警告,建議 int8 activation + int8 weight 用 QDQ format。
兩種格式的差別在「誰決定換成 int8 op」:
格式
誰決定換成 int8 op
哪些 EP 能跑
QDQ format
執行時的 EP
任何 EP(不認得就用 fp32 跑)
QOperator format
量化工具
只有實作了這些 int8 op 的 EP
「哪些 EP 能跑」這個 column 是 ORT 文件的說法,這裡只驗證了 CPU EP。PT2E 匯出的一定是 QDQ format。
小結
ORT 的 quantize_static 和 PT2E 做同一件事:決定哪裡插 Q/DQ,用校準資料算 scale。只是它在 ONNX 圖上做。