平常建 session 時 ORT 也做同樣的改寫,只是不存檔。本專案把這幾行包成 optimized_model(onnx_model, level)(common/onnx_utils.py)。五個等級各做一次,比較 op 的個數:
graphs = {"匯出的 QDQ ONNX": onnx_model} | {lv: optimized_model(onnx_model, lv) for lv in LEVELS}counts = {name: onnx_op_counts(g) for name, g in graphs.items()}ops =sorted(set().union(*counts.values()), key=lambda o: (-counts["匯出的 QDQ ONNX"].get(o, 0), o))table([[op] + [counts[name].get(op, "") for name in graphs] for op in ops], ["op"] + [name.replace("ORT_", "") for name in graphs])
def consts(model):return {i.name: numpy_helper.to_array(i) for i in model.graph.initializer}def conv_node(model):returnnext(n for n in model.graph.node if n.name =="node_conv2d")src, by_out = consts(onnx_model), {n.output[0]: n for n in onnx_model.graph.node}conv = conv_node(onnx_model)s_x =float(src[by_out[conv.input[0]].input[1]]) # 輸入 DequantizeLinear 的 scales_w = src[by_out[conv.input[1]].input[1]] # weight DequantizeLinear 的 per-channel scaleb_fp32 = src[conv.input[2]]b_by_out = {n.output[0]: n for n in basic.graph.node}b_int32 = consts(basic)[b_by_out[conv_node(basic).input[2]].input[0]]same = np.array_equal(b_int32, np.round(b_fp32 / (s_x * s_w)).astype(np.int32))print(f"int32 bias == round(fp32 bias / (s_x · s_w)):{same}")assert b_int32.dtype == np.int32 and same
nodes =list(onnx_model.graph.node)relu =next(n for n in nodes if n.op_type =="Relu")q =next(n for n in nodes if n.op_type =="QuantizeLinear"and n.input[0] == relu.output[0])zp = src[q.input[2]]print(f"Relu 後面的 QuantizeLinear:zero point = {int(zp)},{zp.dtype} 的下限 = {np.iinfo(zp.dtype).min}")assertint(zp) == np.iinfo(zp.dtype).min
Relu 後面的 QuantizeLinear:zero point = -128,int8 的下限 = -128
zero point 就是 int8 的下限,實數 0 對應到 -128,負數在 QuantizeLinear 裡本來就會被 clamp 到 -128。Relu 在量化之後不改變任何值,ORT 就把它拿掉(原始碼的 ReluQuantFusion,只在 zero point 等於下限時做)。zero point 會是下限,是因為 observer 看到的 relu 輸出最小值就是 0。
dq_out = {n.output[0] for n in ext.graph.node if n.op_type =="DequantizeLinear"}fp32_ops =sorted({n.op_type for n in ext.graph.node if n.op_type !="QuantizeLinear"andset(n.input) & dq_out})print("EXTENDED 之後仍吃 DequantizeLinear 輸出的 op:", fp32_ops)assert fp32_ops == ["ReduceMean"]
same = [n.op_type for n in graphs["ORT_ENABLE_ALL"].graph.node] == [n.op_type for n in graphs["ORT_ENABLE_LAYOUT"].graph.node]print("ALL 與 LAYOUT 改寫出的圖相同:", same)assert same
ALL 與 LAYOUT 改寫出的圖相同: True
ALL 只比 LAYOUT 多「fp16 常數轉 fp32」,這個模型沒有 fp16 常數,所以兩者一樣。
小結
QDQ ONNX 是後端中立的:檔案裡只有標準的 QuantizeLinear/DequantizeLinear,要不要換成 int8 kernel,由執行時的 EP 與最佳化等級決定。