【Bug已解决】[Performance] DropQDQ optimization no longer drops QDQ around MaxPool for opset >= 22 解决方案

📅 2026/8/15 4:58:12
【Bug已解决】[Performance] DropQDQ optimization no longer drops QDQ around MaxPool for opset >= 22 解决方案
【Bug已解决】[Performance] DropQDQ optimization no longer drops QDQ around MaxPool for opset 22 解决方案一、现象长什么样升级到opset 22之后发现 ONNX Runtime 的图优化DropQDQ把 Quantize-Add-DeQuantize 这种“量化—反量化”相邻对消去因为它们相互抵消不再对MaxPool周围的 QDQ 生效导致性能下降# opset 21MaxPool 两侧 QDQ 被消除图更短更快 Q - MaxPool - D MaxPool直接跑省两次量化转换 # opset 22QDQ 没被消除保留 Q - MaxPool - D 仍然做量化/反量化白白耗时具体表现只在opset 22的MaxPool上出现opset 21 及以下的MaxPool、以及其它算子Conv、Add 等的 QDQ 仍正常消除。性能回归INT8 模型里MaxPool周围多了两次量化转换延迟上升尤其MaxPool多的检测类模型更明显。优化日志里看不到MaxPool对应的 QDQ 被 drop 的记录以前有。模型数值没错只是慢——是纯性能问题。关键特征DropQDQ 这个图优化对MaxPool的模式匹配在 opset 22 下失效——因为 opset 22 改变了MaxPool的某些属性/签名匹配器没跟上于是“本来能消的 QDQ 没消”。二、背景量化模型里常出现这种结构... - QuantizeLinear - MaxPool - DeQuantizeLinear - ...如果QuantizeLinear和DeQuantizeLinear用的是相同的量化参数scale/zero_point而且中间的MaxPool在量化域和浮点域数学上等价MaxPool 是“逐元素取最大”对仿射量化是保序的所以量化域取最大 先反量化再取最大再量化那么这整段Q - MaxPool - D就可以被直接消去只留一个MaxPool在合适域里跑省掉两次量化/反量化转换。这就是DropQDQ优化的目的。图优化器用一个**模式匹配器pattern matcher**识别“QuantizeLinear→ 某 op →DeQuantizeLinear且量化参数一致”这种子图匹配到了就重写。匹配器对中间那个 op 会有属性/签名的假设。MaxPool在opset 22有变动比如新增/调整了属性像dilations、ceil_mode、以及 opset 22 对MaxPool的输出形状/可选输出indices的处理变化。匹配器里“识别 MaxPool”的那段代码可能写死了对旧属性集合的检查或要求特定属性存在opset 22 的MaxPool属性签名变了匹配器的检查过严或漏判于是模式匹配失败QDQ 没被 drop。三、根因根因是DropQDQ 优化器里针对MaxPool的模式匹配没有覆盖 opset 22 的新属性/签名导致匹配失败、本可消除的 QDQ 被保留匹配器假设旧属性集识别MaxPool时检查了某些旧版属性或要求某些属性不存在opset 22 加了新属性后检查不通过 → 不匹配。opset 版本分支缺失优化器可能只对opset 21的MaxPool启用了 DropQDQ没有为 22注册同样的规则。MaxPool 在量化域保序的性质没变但规则没覆盖新版本。Quantize/DeQuantize 参数一致性判断没变这部分的匹配其实没问题问题卡在“中间 op 是不是可消除的 MaxPool”这一步——opset 22 的 MaxPool 没被认作“可消除 op”。性能回归被掩盖模型能跑、结果对只是慢所以不容易第一时间联想到是图优化失效。一句话MaxPool 在 opset 22 的属性签名变了DropQDQ 的匹配器没跟进导致 QDQ 没被消除INT8 模型在 MaxPool 处多了无谓的量化转换性能下降。四、最小可运行复现下面用 Python 模拟“DropQDQ 模式匹配”复现 opset 22 下 MaxPool 因签名变化而未被识别from dataclasses import dataclass from typing import Optional dataclass class Node: op: str opset: int attrs: set def can_drop_qdq_buggy(mid: Node, same_qparams: bool) - bool: 错误只认 opset21 的 MaxPool且要求旧属性集。 if not same_qparams: return False if mid.op MaxPool: if mid.opset 21: return False # opset 22 没覆盖 - 不消除 # 旧属性检查 return kernel_shape in mid.attrs return False def can_drop_qdq_fixed(mid: Node, same_qparams: bool) - bool: 修复MaxPool 在所有 opset 下都保序可消除按版本取属性检查。 if not same_qparams: return False if mid.op MaxPool: # opset 22 新增属性也要纳入“保序可消除”的判定 required {kernel_shape} | ( {ceil_mode, dilations} if mid.opset 22 else set()) return required.issubset(mid.attrs) return False m21 Node(MaxPool, 21, {kernel_shape}) m22 Node(MaxPool, 22, {kernel_shape, ceil_mode, dilations}) print(buggy 21:, can_drop_qdq_buggy(m21, True)) # True消除 print(buggy 22:, can_drop_qdq_buggy(m22, True)) # False漏了 print(fixed 22:, can_drop_qdq_fixed(m22, True)) # True消除buggy在 opset 22 返回FalseQDQ 没消fixed正确地在 opset 22 也返回True——正是需要补的匹配规则。五、解决方案第一层最小直接修复最小修复是在 DropQDQ 优化器里把 opset 22 的MaxPool也纳入“可消除 op”集合并按版本正确检查其属性// optimizer_drop_qdq.cpp修复片段 bool IsMaxPoolQdqDroppable(const Node n) { if (n.OpType() ! MaxPool) return false; const int opset n.OpSet(); // MaxPool 在量化域保序所有 opset 都可消除 QDQ参数一致前提下 // 按版本取所需属性集 std::setstd::string required {kernel_shape}; if (opset 22) { required.insert({ceil_mode, dilations}); // opset 22 新增 } for (const auto a : required) { if (!n.HasAttribute(a)) return false; } return true; // 可消除 } // 在 DropQDQ 注册时对 MaxPool 不再限 opset21 void RegisterDropQdqPatterns(GraphOptimizer opt) { opt.AddPattern({QuantizeLinear, MaxPool, DeQuantizeLinear}, /*opset_agnostic*/true); // 覆盖所有 opset }这一层让 opset 22 的MaxPool周围 QDQ 重新被消除性能回归消失。六、解决方案第二层结构性改进把“哪些 op 在 QDQ 下可安全消除、各 opset 的属性检查”收口成唯一的配置对象OrtDropQdqPolicy优化器读它from dataclasses import dataclass from typing import Tuple, Dict dataclass(frozenTrue) class OrtDropQdqPolicy: DropQDQ 可消除 op 与属性检查的单一事实来源。 # MaxPool 在所有 opset 下保序可消除参数一致时 maxpool_droppable_all_opset: bool True # 各 op 在不同 opset 下需要的属性集 required_attrs: Dict[str, Dict[int, Tuple[str, ...]]] None # 量化参数必须一致才消除scale/zero_point 相同 require_same_qparams: bool True # 禁止按 opset 上限卡掉可消除 op forbid_opset_ceiling: bool True # 代码评审卡点 forbidden_patterns: Tuple[str, ...] ( MaxPool drop only if opset 21, skip QDQ for opset 22 MaxPool, ) def is_droppable(self, op: str, opset: int, attrs: tuple, same_qparams: bool) - bool: if not same_qparams: return False if op MaxPool and self.maxpool_droppable_all_opset: need {kernel_shape} if opset 22: need | {ceil_mode, dilations} return need.issubset(set(attrs)) return False def describe(self) - str: return MaxPool 各 opset 均可消除 QDQ参数一致按版本检查属性 POLICY OrtDropQdqPolicy() def plan_drop_qdq(op: str, opset: int, attrs: tuple, same_qparams: bool, policy: OrtDropQdqPolicy POLICY) - bool: return policy.is_droppable(op, opset, attrs, same_qparams)所有 DropQDQ 规则都读POLICY可消除 op 与属性检查被固化不会再因 opset 升级漏掉 MaxPool。七、解决方案第三层断言 / CI 守护把“MaxPool 各 opset 可消除、参数一致、不卡 opset”做成断言。下面用 pytest 守护import pytest def test_maxpool_droppable_all_opset(policy): assert policy.maxpool_droppable_all_opset is True assert policy.is_droppable(MaxPool, 22, (kernel_shape, ceil_mode, dilations), same_qparamsTrue) is True assert policy.is_droppable(MaxPool, 21, (kernel_shape,), same_qparamsTrue) is True def test_different_qparams_not_dropped(policy): assert policy.is_droppable(MaxPool, 22, (kernel_shape,), same_qparamsFalse) is False def test_no_opset_ceiling(policy): assert policy.forbid_opset_ceiling is True assert skip QDQ for opset 22 MaxPool in policy.forbidden_patterns def test_opset22_attrs_required(policy): # opset 22 缺新属性则不应消除 assert policy.is_droppable(MaxPool, 22, (kernel_shape,), same_qparamsTrue) is False def test_require_same_qparams(policy): assert policy.require_same_qparams is True这五组断言锁住(1) MaxPool 各 opset 可消除(2) 参数不一致不消除(3) 禁止卡 opset(4) opset22 需新属性(5) 要求量化参数一致。CI 跑通即代表 DropQDQ 不会再因 opset 22 漏掉 MaxPool。八、排查清单遇到 opset 22 下 MaxPool 周围 QDQ 没被消除、性能下降确认是性能回归结果对、只是慢且 opset 21 正常 → 锁定 DropQDQ。看是否只 MaxPool opset22Conv/Add 正常 → 匹配器对 MaxPool 签名没跟上。查优化器匹配规则是不是只对opset 21的 MaxPool 注册了 DropQDQ。查 MaxPool 属性opset 22 新增了ceil_mode/dilations等匹配检查要覆盖。改覆盖全 opsetMaxPool 保序可消除按版本取属性集去掉 opset 上限。统一到OrtDropQdqPolicyCI 断言禁止卡 opset。端到端opset 22 INT8 模型里 MaxPool 周围 QDQ 被消除、延迟恢复。九、小结[Performance] DropQDQ optimization no longer drops QDQ around MaxPool for opset 22的根因是DropQDQ图优化通过模式匹配识别“QuantizeLinear→ op →DeQuantizeLinear参数一致”并消去其中MaxPool在量化域保序本应所有 opset 都可消除但匹配器只覆盖了opset 21的MaxPool、且对 opset 22 新增的ceil_mode/dilations等属性没纳入检查于是 opset 22 的MaxPool没被认作“可消除 op”QDQ 保留INT8 模型在 MaxPool 处多了无谓的量化/反量化转换性能下降。最小修复是把 opset 22 的MaxPool也纳入可消除集合并按版本正确检查属性结构性改进是用唯一的OrtDropQdqPolicy固化可消除 op 与属性检查CI 用五组断言守护“MaxPool 各 opset 可消除、参数一致、不卡 opset”。记住图优化的模式匹配必须随 opset 演进同步更新属性签名否则“本可消除的 QDQ 没消”会以性能回归的形式悄悄出现。