import copy
import dataclasses
from collections import OrderedDict
import torch
from common.data import calibration_batches
from common.graph_utils import observer_nodes, qdq_nodes
from common.models import ConvBnReLU, example_inputs
from common.pt2e import (
X86InductorQuantizer,
convert_pt2e,
export_module,
get_default_x86_inductor_quantization_config,
get_module_name_filter,
get_xnnpack_api,
prepare_pt2e,
quiet,
)
from common.viz import table
quiet()
_ = torch.manual_seed(0)
CONV2D = torch.ops.aten.conv2d.defaultM03-7 | 後端的設定 API:global、op 類型、module 名稱
M03-6 自己寫規則(node.target + module 名稱篩選)決定哪些 node 要量化。真實的後端把這類規則包成設定 API,讓使用者呼叫。 以 X86InductorQuantizer(torchao 給 x86 CPU + Inductor 用的 Quantizer)為例:
| 呼叫 | 套用到 |
|---|---|
set_global(cfg) |
沒被其他規則命中的 node |
set_module_type_qconfig(nn.Conv2d, cfg) |
某種 module 類別產生的 op |
set_module_name_qconfig("stem", cfg) |
某個名字的子模組產生的 op |
cfg 給 None 表示這一段不量化。這些 set_* 不是 Quantizer 基底類別的 API,是各後端自己定義的,名稱與行為因後端而異:
- A.
X86InductorQuantizer:三種設定的優先順序,以及用None排除一段。 - B. 同樣的想法放在 torchao 測試版 XNNPACKQuantizer 上:
set_module_name對 conv 默默不生效。
模型是三段串接的 conv + bn + relu,子模組名稱分別是 stem、block、head_conv:
chain = torch.nn.Sequential(OrderedDict(stem=ConvBnReLU(3, 8), block=ConvBnReLU(8, 8), head_conv=ConvBnReLU(8, 8)))
gm = export_module(chain.eval(), example_inputs(4))
convs = [n.name for n in gm.graph.nodes if n.target == CONV2D]
print("圖上的 conv:", convs)圖上的 conv: ['conv2d', 'conv2d_1', 'conv2d_2']
A. X86InductorQuantizer:誰的優先權高
要知道每個 conv 實際套用了哪一條設定,替每條設定做一個指紋:複製預設 config,只把 activation 的 quant_max 改成不同的值 (global 255、op 類型 200、module 名稱 100)。prepare 之後讀 conv 輸入 observer 的 quant_max,就知道是哪一條生效。 這些 config 只是用來辨識,所以這裡不比較精度。
g = get_default_x86_inductor_quantization_config()
def with_qmax(qmax):
"""複製預設 config,只改 activation 的 quant_max。"""
act = g.input_activation
assert act is not None
return dataclasses.replace(g, input_activation=dataclasses.replace(act, quant_max=qmax))
by_type, by_name = with_qmax(200), with_qmax(100)
fingerprint = {255: "global", 200: "op 類型", 100: "module 名稱"}
def make(use_global=True, type_cfg=None, names=None):
q = X86InductorQuantizer()
if use_global:
q.set_global(g)
if type_cfg:
q.set_module_type_qconfig(torch.nn.Conv2d, type_cfg)
for name, cfg in (names or {}).items():
q.set_module_name_qconfig(name, cfg)
return q
def applied(prepared):
"""每個 conv 套用的設定:看它輸入 observer 的 quant_max;沒有 observer 就是 fp32。"""
result = []
for conv in (n for n in prepared.graph.nodes if n.target == CONV2D):
src = conv.args[0]
result.append(fingerprint[getattr(prepared.get_submodule(src.target), "quant_max")]
if src.op == "call_module" else "fp32")
return result
settings = [
("只有 set_global", make()),
("global + op 類型(nn.Conv2d)", make(type_cfg=by_type)),
("global + op 類型 + module 名稱(stem)", make(type_cfg=by_type, names={"stem": by_name})),
("global + module 名稱(block)= None", make(names={"block": None})),
("不設 global,只有 module 名稱(stem)", make(use_global=False, names={"stem": by_name})),
]
rows = []
for title, quantizer in settings:
prepared = prepare_pt2e(copy.deepcopy(gm), quantizer)
used = applied(prepared)
with torch.no_grad():
for batch in calibration_batches(n=16, batch_size=16):
prepared(*batch)
rows.append([title, *used, len(qdq_nodes(convert_pt2e(prepared)))])
table(rows, ["設定", "stem", "block", "head_conv", "q/dq 數"])| 設定 | stem | block | head_conv | q/dq 數 |
|---|---|---|---|---|
| 只有 set_global | global | global | global | 9 |
| global + op 類型(nn.Conv2d) | op 類型 | op 類型 | op 類型 | 9 |
| global + op 類型 + module 名稱(stem) | module 名稱 | op 類型 | op 類型 | 9 |
| global + module 名稱(block)= None | global | fp32 | global | 6 |
| 不設 global,只有 module 名稱(stem) | module 名稱 | fp32 | fp32 | 3 |
assert [r[1:4] for r in rows] == [
["global"] * 3,
["op 類型"] * 3,
["module 名稱", "op 類型", "op 類型"],
["global", "fp32", "global"],
["module 名稱", "fp32", "fp32"],
]- 優先順序:module 名稱 > op 類型 > global(第 2、3 個 row)。同一個 node 只被最先命中的規則標一次,之後的階段看到它已經標過就跳過(
X86InductorQuantizer.annotate的說明),和 M03-6 的_annotated是同一個機制。 - 設成
None就是明確不量化這一段(第 4 個 row),這是最直接的局部量化寫法。 - 不設 global 時,沒被任何規則命中的 node 不量化(第 5 個 row)。
各後端的方法名稱不同,例如 X86/Arm Inductor 用 set_module_name_qconfig,XNNPACK 用 set_module_name。 M03-6 自己寫的規則(node.target + module 名稱篩選),就是這些 API 背後的做法。
B. XNNPACKQuantizer 的 set_module_name 對 conv 不生效
對 torchao 測試版 XNNPACKQuantizer 做同樣的事:set_module_name("stem", cfg),預期 stem 被量化。
xnnpack_cls, config_fn, _ = get_xnnpack_api()
prepared = prepare_pt2e(copy.deepcopy(gm), xnnpack_cls().set_module_name("stem", config_fn(is_per_channel=True)))
print("prepare 之後的 observer 數:", len(observer_nodes(prepared)))
assert len(observer_nodes(prepared)) == 0prepare 之後的 observer 數: 0
一個 observer 都沒有,stem 默默留在 fp32,也沒有報錯。原因在篩選函式:它要求 pattern 裡每個 node 都屬於 stem。 conv 本身有 nn_module_stack,但 conv 的 weight 是 get_attr node,沒有這個欄位:
in_stem = get_module_name_filter("stem")
conv = next(n for n in gm.graph.nodes if n.target == CONV2D)
print(f"conv 屬於 stem? {in_stem(conv)}")
print(f"conv 的 weight({conv.args[1].op})屬於 stem? {in_stem(conv.args[1])}")
assert in_stem(conv) and not in_stem(conv.args[1])conv 屬於 stem? True
conv 的 weight(get_attr)屬於 stem? False
所以在 torch 2.14 + torchao 0.18 上,這份 XNNPACKQuantizer 的 module 層級設定對 conv 不生效(對 linear 正常,executorch 版也一樣,M08-1);set_operator_type 也沒被讀取。 用任何設定 API 之後,都要把標籤或 observer 印出來確認。
小結
- 後端的
set_*設定 API 是把「找 node 的規則」包給使用者用,不是Quantizer基底類別的一部分,各後端不同。 X86InductorQuantizer:module 名稱 > op 類型 > global,None表示不量化。- 設定可能默默不生效:設完之後檢查 observer 或標籤。