import copy
import torch
from torch.fx import Node
from common.graph_utils import observer_nodes
from common.models import example_inputs, get_model
from common.pt2e import (
ANNOTATION_KEY,
MovingAverageMinMaxObserver,
QuantizationSpec,
Quantizer,
SharedQuantizationSpec,
annotate_input_qspec_map,
annotate_output_qspec,
convert_pt2e,
export_module,
prepare_pt2e,
quiet,
)
from common.qspecs import act_spec
from common.viz import draw, table
quiet()
_ = torch.manual_seed(0)
ADD = torch.ops.aten.add.TensorM03-4 | 共用 qparams:SharedQuantizationSpec 與隱式共用
兩個 int8 tensor 要做整數加法(或 concat)時,必須在同一個尺度上:scale 不同的整數不能直接相加,硬體得先 rescale 其中一個。 所以有些後端要求 add、cat 的所有輸入與輸出共用同一組 (scale, zero point)。
在 PT2E 裡,「共用 qparams」的做法是讓幾個位置共用同一個 observer 實例:這個 observer 看過所有位置的數值,算出一組 scale,大家一起用。 共用有兩種來源:
- 顯式:標籤的規格寫
SharedQuantizationSpec(某個位置),意思是「和那個位置用同一個 observer」。 - 隱式:同一個 tensor 上的兩張標籤屬性相同時,
prepare_pt2e自動合併成一個 observer。M03-1 的 C 段提過。
段落:
- A. 顯式共用:讓 add 的輸入與輸出共用一組 qparams。
- B. 隱式共用:同一個 tensor 同時有輸出標籤與邊標籤時,什麼時候合併、什麼時候串接。
B. 隱式共用:輸出標籤 + 邊標籤
M03-1 的 C 段看過:同一個 tensor 可以有輸出標籤(影響所有 consumer),也可以有邊標籤(只影響一個 consumer)。 兩張都貼的時候會發生什麼?
用 cat_pool 模型(輸入 x 同時餵給 conv2d 與 conv2d_1)。TwoLabelQuantizer 在 x 的輸出貼 spec A,在邊 x → conv2d_1 貼 spec B。 它是為了這個實驗點名寫的,node 名稱只適用 cat_pool。allow_implicit_sharing 是 QuantizationAnnotation 的欄位(預設 True),設成 False 可以關掉合併。
class TwoLabelQuantizer(Quantizer):
def __init__(self, spec_a, spec_b, implicit=True):
self.spec_a, self.spec_b, self.implicit = spec_a, spec_b, implicit
def annotate(self, model):
nodes = {n.name: n for n in model.graph.nodes}
annotate_output_qspec(nodes["x"], self.spec_a)
annotate_input_qspec_map(nodes["conv2d_1"], nodes["x"], self.spec_b)
nodes["conv2d_1"].meta[ANNOTATION_KEY].allow_implicit_sharing = self.implicit
return model
def validate(self, model):
pass四種情況的 spec B:
a = act_spec() # int8 per-tensor affine [-128, 127],MinMaxObserver
different = QuantizationSpec( # qscheme 與整數範圍都和 A 不同
dtype=torch.int8, quant_min=-127, quant_max=127, qscheme=torch.per_tensor_symmetric,
observer_or_fake_quant_ctr=a.observer_or_fake_quant_ctr,
)
other_observer = QuantizationSpec( # 量化屬性和 A 完全相同,只有 observer 類別不同
dtype=a.dtype, quant_min=a.quant_min, quant_max=a.quant_max, qscheme=a.qscheme,
observer_or_fake_quant_ctr=MovingAverageMinMaxObserver,
)
cases = [
("B 與 A 屬性不同", TwoLabelQuantizer(a, different)),
("B 與 A 屬性相同", TwoLabelQuantizer(a, a)),
("屬性相同,但 allow_implicit_sharing=False", TwoLabelQuantizer(a, a, implicit=False)),
("B 與 A 只有 observer 類別不同", TwoLabelQuantizer(a, other_observer)),
]每種情況都 prepare、校準、convert,印出 x 走到 conv2d_1 的路上經過什麼(obs = observer,q/dq = quantize/dequantize):
def path_to(gm, consumer):
"""x 走到 consumer 的路上依序經過的 node。"""
steps, src = [], next(n for n in gm.graph.nodes if n.name == consumer).args[0]
while src.op != "placeholder":
target = str(src.target)
steps.append("obs" if src.op == "call_module" else "dq" if "dequantize" in target else "q")
src = src.args[0]
return " → ".join(["x", *reversed(steps), consumer])
gm_cat = export_module(get_model("cat_pool").eval(), example_inputs(4))
rows = []
for title, quantizer in cases:
prepared = prepare_pt2e(copy.deepcopy(gm_cat), quantizer)
obs = observer_nodes(prepared)
kinds = ", ".join(sorted({type(m).__name__ for _, m in obs}))
before = path_to(prepared, "conv2d_1")
with torch.no_grad():
for _ in range(4):
prepared(torch.randn(4, 3, 16, 16))
rows.append([title, len(obs), kinds, before, path_to(convert_pt2e(prepared), "conv2d_1")])
table(rows, ["情況", "observer 數", "observer 類別", "prepare 之後", "convert 之後"])| 情況 | observer 數 | observer 類別 | prepare 之後 | convert 之後 |
|---|---|---|---|---|
| B 與 A 屬性不同 | 2 | MinMaxObserver | x → obs → obs → conv2d_1 | x → q → dq → q → dq → conv2d_1 |
| B 與 A 屬性相同 | 1 | MinMaxObserver | x → obs → conv2d_1 | x → q → dq → conv2d_1 |
| 屬性相同,但 allow_implicit_sharing=False | 2 | MinMaxObserver | x → obs → obs → conv2d_1 | x → q → dq → q → dq → conv2d_1 |
| B 與 A 只有 observer 類別不同 | 1 | MinMaxObserver | x → obs → conv2d_1 | x → q → dq → conv2d_1 |
assert [r[1] for r in rows] == [2, 1, 2, 1]
assert rows[0][4] == "x → q → dq → q → dq → conv2d_1" and rows[1][4] == "x → q → dq → conv2d_1"
assert rows[3][2] == "MinMaxObserver"- 屬性不同:兩張標籤串接。
x先被 A 量化,再被 B 量化一次,convert 之後是兩層 q/dq,捨入兩次。不是誰覆蓋誰,也不會報錯。 - 屬性相同:自動合併成一個 observer,只量化一次。它們本來就是同一個 tensor,沒必要量化兩次,這就是隱式共用。
allow_implicit_sharing=False:不合併,回到兩個 observer 串接。關掉之後要自己負責同一個 tensor 不被量化兩次。- 比的是量化屬性(dtype、整數範圍、qscheme…),不比 observer 類別。只有 observer 類別不同時也會合併, 留下的是 A 的
MinMaxObserver,B 指定的MovingAverageMinMaxObserver被默默忽略(這張圖上 A 的標籤在 graph 順序上先出現)。
要讓同一個 tensor 只量化一次:讓所有標籤的屬性一致,或明確用 SharedQuantizationSpec 指向同一個位置。
小結
- 共用 qparams = 幾個位置共用同一個 observer 實例;圖上仍是好幾個 observer node。
- 顯式:
SharedQuantizationSpec(位置),位置是(producer, consumer)的邊或一個 node 的輸出。 - 隱式:同一個 tensor 上屬性相同的標籤自動合併;屬性不同則串接成兩次量化,不報錯。