From bf13b0bd353e2cac89102ba62afb16df697eb5b2 Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Sun, 5 Jul 2026 08:33:11 -0400 Subject: [PATCH 1/8] Fix XNNPACK Conv1d same padding --- backends/xnnpack/_passes/__init__.py | 2 + .../xnnpack/_passes/conv1d_unsqueeze_pass.py | 124 +++++++++++++++++- backends/xnnpack/operators/op_conv2d.py | 19 ++- .../xnnpack/partition/config/gemm_configs.py | 29 ++++ backends/xnnpack/test/ops/test_conv1d.py | 43 +++++- 5 files changed, 209 insertions(+), 8 deletions(-) diff --git a/backends/xnnpack/_passes/__init__.py b/backends/xnnpack/_passes/__init__.py index 22147fa4215..43be66bba8e 100644 --- a/backends/xnnpack/_passes/__init__.py +++ b/backends/xnnpack/_passes/__init__.py @@ -16,6 +16,7 @@ ChannelsLastTaggedReshapePass, ) from executorch.backends.xnnpack._passes.conv1d_unsqueeze_pass import ( + Conv1dFoldedPadMetaPass, Conv1dUnsqueezePass, ) from executorch.backends.xnnpack._passes.convert_to_linear import ConvertToLinearPass @@ -87,6 +88,7 @@ def __init__( PReLUReshapePass, ChannelsLastTaggedReshapePass, RemoveRedundantCopyPass, + Conv1dFoldedPadMetaPass, ] else: self.passes = passes diff --git a/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py b/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py index 7a6b031160a..0e7dd8f1ef6 100644 --- a/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py +++ b/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py @@ -159,8 +159,42 @@ def call(self, graph_module: torch.fx.GraphModule): # c. Add unsqueeze to input (3d -> 4d) and squeeze to output (4d -> 3d) # unsqueeze -> conv2d -> squeeze + input_node = node.args[0] + if ( + isinstance(input_node, torch.fx.Node) + and input_node.target + == exir_ops.edge.aten.constant_pad_nd.default + and len(input_node.users) == 1 + ): + pad_value = ( + input_node.args[2] if len(input_node.args) > 2 else 0 + ) + pad_amounts = list(input_node.args[1]) + temporal_only = len(pad_amounts) <= 2 or all( + amount == 0 for amount in pad_amounts[2:] + ) + if ( + pad_value == 0 + and len(pad_amounts) % 2 == 0 + and all(amount >= 0 for amount in pad_amounts) + and temporal_only + ): + time_before = pad_amounts[0] if len(pad_amounts) > 0 else 0 + time_after = pad_amounts[1] if len(pad_amounts) > 1 else 0 + conv_time_padding = node.args[4][0] + node.meta["xnnpack_input_padding"] = [ + conv_time_padding + time_before, + 0, + conv_time_padding + time_after, + 0, + ] + node.meta["xnnpack_conv1d_folded_pad"] = True + pad_node = input_node + input_node = pad_node.args[0] + node.replace_input_with(pad_node, input_node) + graph.erase_node(pad_node) + with graph.inserting_before(node): - input_node = node.args[0] unsqueeze_before = self.create_node( graph, exir_ops.edge.aten.unsqueeze_copy.default ) @@ -201,3 +235,91 @@ def call(self, graph_module: torch.fx.GraphModule): graph_module = super().call(graph_module).graph_module return PassResult(graph_module, True) + + +class Conv1dFoldedPadMetaPass(XNNPACKPass): + """ + Restores metadata for Conv1d nodes whose explicit pad was folded into + asymmetric XNNPACK input padding. + + Later passes retrace the graph using symmetric ATen convolution args, which + cannot represent even-kernel same padding exactly. Serialization, however, + needs tensor metadata that matches the asymmetric XNNPACK padding fields. + """ + + def _resize_meta_val(self, node: torch.fx.Node, shape: tuple): + if "val" not in node.meta: + return + + val = node.meta["val"] + if hasattr(val, "new_empty"): + node.meta["val"] = val.new_empty(shape) + else: + node.meta["val"] = torch.empty(shape) + + def _set_users_meta( + self, + node: torch.fx.Node, + shape_4d: tuple, + shape_3d: tuple, + ): + visited = set() + stack = list(node.users) + while stack: + user = stack.pop() + if user in visited or user.op != "call_function": + continue + visited.add(user) + + if user.target == exir_ops.edge.aten.squeeze_copy.dim: + self._resize_meta_val(user, shape_3d) + for squeeze_user in user.users: + if squeeze_user.op == "call_function": + self._resize_meta_val(squeeze_user, shape_3d) + continue + + self._resize_meta_val(user, shape_4d) + stack.extend(user.users) + + def call(self, graph_module: torch.fx.GraphModule): + for node in graph_module.graph.nodes: + if ( + node.op != "call_function" + or node.target != exir_ops.edge.aten.convolution.default + or "xnnpack_input_padding" not in node.meta + or "xnnpack_conv1d_folded_pad" not in node.meta + ): + continue + + input_shape = tuple(node.args[0].meta["val"].shape) + kernel_shape = tuple(node.args[1].meta["val"].shape) + stride = node.args[3] + dilation = node.args[5] + padding_top, padding_right, padding_bottom, padding_left = node.meta[ + "xnnpack_input_padding" + ] + + kernel_h = kernel_shape[2] + kernel_w = kernel_shape[3] + out_h = ( + input_shape[2] + + padding_top + + padding_bottom + - dilation[0] * (kernel_h - 1) + - 1 + ) // stride[0] + 1 + out_w = ( + input_shape[3] + + padding_left + + padding_right + - dilation[1] * (kernel_w - 1) + - 1 + ) // stride[1] + 1 + + shape_4d = (input_shape[0], kernel_shape[0], out_h, out_w) + shape_3d = (input_shape[0], kernel_shape[0], out_h) + self._resize_meta_val(node, shape_4d) + self._set_users_meta(node, shape_4d, shape_3d) + + graph_module.recompile() + return PassResult(graph_module, True) diff --git a/backends/xnnpack/operators/op_conv2d.py b/backends/xnnpack/operators/op_conv2d.py index 315bbd863cb..c095a0fc41a 100644 --- a/backends/xnnpack/operators/op_conv2d.py +++ b/backends/xnnpack/operators/op_conv2d.py @@ -149,10 +149,21 @@ def define_node( check_or_raise( len(stride) == 2, "XNNPACK currently only supports 2D convolution" ) - kwargs["padding_top"] = padding[0] - kwargs["padding_right"] = padding[1] - kwargs["padding_bottom"] = padding[0] - kwargs["padding_left"] = padding[1] + folded_padding = node.meta.get("xnnpack_input_padding") + if folded_padding is not None: + check_or_raise( + len(folded_padding) == 4, + "Expected XNNPACK padding as [top, right, bottom, left]", + ) + kwargs["padding_top"] = folded_padding[0] + kwargs["padding_right"] = folded_padding[1] + kwargs["padding_bottom"] = folded_padding[2] + kwargs["padding_left"] = folded_padding[3] + else: + kwargs["padding_top"] = padding[0] + kwargs["padding_right"] = padding[1] + kwargs["padding_bottom"] = padding[0] + kwargs["padding_left"] = padding[1] kwargs["kernel_height"] = kernel_shape[2] kwargs["kernel_width"] = kernel_shape[3] kwargs["subsampling_height"] = stride[0] diff --git a/backends/xnnpack/partition/config/gemm_configs.py b/backends/xnnpack/partition/config/gemm_configs.py index d0e9528809d..8e77545a7d1 100644 --- a/backends/xnnpack/partition/config/gemm_configs.py +++ b/backends/xnnpack/partition/config/gemm_configs.py @@ -397,6 +397,35 @@ def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool: return False return True + def _get_act_deps( + self, node: torch.fx.Node, ep: ExportedProgram, precision: ConfigPrecisionType + ) -> Tuple[bool, List[torch.fx.Node]]: + act_input = get_input_node(node, self.act_idx) + conv_stride = cast(List[int], node.args[3]) + if ( + precision != ConfigPrecisionType.FP32 + and len(conv_stride) == 1 + and is_node(act_input) + and act_input.target == exir_ops.edge.aten.constant_pad_nd.default + and len(act_input.users) == 1 + ): + pad_value = act_input.args[2] if len(act_input.args) > 2 else 0 + pad_amounts = cast(List[int], act_input.args[1]) + temporal_only = len(pad_amounts) <= 2 or all( + a == 0 for a in pad_amounts[2:] + ) + if ( + pad_value == 0 + and len(pad_amounts) % 2 == 0 + and all(a >= 0 for a in pad_amounts) + and temporal_only + ): + valid, deps = super()._get_act_deps(act_input, ep, precision) + if valid: + return (True, [act_input, *deps]) + + return super()._get_act_deps(node, ep, precision) + def supported_precision_types(self): return [ ConfigPrecisionType.FP32, diff --git a/backends/xnnpack/test/ops/test_conv1d.py b/backends/xnnpack/test/ops/test_conv1d.py index 35d9bced512..b637e1d95bf 100644 --- a/backends/xnnpack/test/ops/test_conv1d.py +++ b/backends/xnnpack/test/ops/test_conv1d.py @@ -90,6 +90,23 @@ def forward(self, x): z = torch.add(y, z) return z + class Conv1dSamePadding(torch.nn.Module): + def __init__(self, kernel_size: int): + super().__init__() + self.conv1d = torch.nn.Conv1d( + in_channels=2, + out_channels=4, + kernel_size=kernel_size, + padding="same", + bias=True, + ) + + def forward(self, x): + return self.conv1d(x) + + def _get_calibration_samples(self, inputs): + return [tuple(torch.randn_like(inputs[i]) for i in range(len(inputs)))] + def _test_conv1d( self, module, @@ -102,9 +119,7 @@ def _test_conv1d( skip_to_executorch=False, ): calibration_samples = ( - [tuple(torch.randn_like(inputs[i]) for i in range(len(inputs)))] - if quantized - else None + self._get_calibration_samples(inputs) if quantized else None ) tester = ( @@ -160,6 +175,28 @@ def test_qs8_conv1d(self): self.Conv1d(), inputs, 1, quantized=True, dynamic_shape=dynamic_shapes ) + def test_qs8_conv1d_even_kernel_same_padding(self): + inputs = (torch.randn(1, 2, 16),) + ( + Tester(self.Conv1dSamePadding(kernel_size=4), inputs) + .quantize( + Quantize(calibration_samples=self._get_calibration_samples(inputs)) + ) + .export() + .check_count({"torch.ops.aten.conv1d.padding": 1}) + .to_edge_transform_and_lower() + .check_not( + [ + "executorch_exir_dialects_edge__ops_aten_convolution_default", + "executorch_exir_dialects_edge__ops_aten_constant_pad_nd_default", + ] + ) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + .serialize() + .run_method_and_compare_outputs(num_runs=10, atol=0.04, rtol=0.02) + ) + def test_qs8_conv1d_batchnorm_seq(self): inputs = (torch.randn(2, 2, 4),) dynamic_shapes = ({0: torch.export.Dim("batch", min=2, max=10)},) From a07b3008ff1ce43ea785433758d7a4bcd45515f8 Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Sun, 5 Jul 2026 08:44:08 -0400 Subject: [PATCH 2/8] Avoid folding Conv1d input pad for transposed convs --- .../xnnpack/_passes/conv1d_unsqueeze_pass.py | 16 +++--- .../xnnpack/partition/config/gemm_configs.py | 1 + backends/xnnpack/test/ops/test_conv1d.py | 49 +++++++++++-------- 3 files changed, 37 insertions(+), 29 deletions(-) diff --git a/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py b/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py index 0e7dd8f1ef6..ec511551164 100644 --- a/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py +++ b/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py @@ -165,6 +165,7 @@ def call(self, graph_module: torch.fx.GraphModule): and input_node.target == exir_ops.edge.aten.constant_pad_nd.default and len(input_node.users) == 1 + and not node.args[6] ): pad_value = ( input_node.args[2] if len(input_node.args) > 2 else 0 @@ -264,22 +265,19 @@ def _set_users_meta( shape_3d: tuple, ): visited = set() - stack = list(node.users) + stack = [(user, shape_4d) for user in node.users] while stack: - user = stack.pop() + user, current_shape = stack.pop() if user in visited or user.op != "call_function": continue visited.add(user) if user.target == exir_ops.edge.aten.squeeze_copy.dim: self._resize_meta_val(user, shape_3d) - for squeeze_user in user.users: - if squeeze_user.op == "call_function": - self._resize_meta_val(squeeze_user, shape_3d) - continue - - self._resize_meta_val(user, shape_4d) - stack.extend(user.users) + stack.extend((u, shape_3d) for u in user.users) + else: + self._resize_meta_val(user, current_shape) + stack.extend((u, current_shape) for u in user.users) def call(self, graph_module: torch.fx.GraphModule): for node in graph_module.graph.nodes: diff --git a/backends/xnnpack/partition/config/gemm_configs.py b/backends/xnnpack/partition/config/gemm_configs.py index 8e77545a7d1..5bd9b88d1e8 100644 --- a/backends/xnnpack/partition/config/gemm_configs.py +++ b/backends/xnnpack/partition/config/gemm_configs.py @@ -408,6 +408,7 @@ def _get_act_deps( and is_node(act_input) and act_input.target == exir_ops.edge.aten.constant_pad_nd.default and len(act_input.users) == 1 + and not node.args[6] ): pad_value = act_input.args[2] if len(act_input.args) > 2 else 0 pad_amounts = cast(List[int], act_input.args[1]) diff --git a/backends/xnnpack/test/ops/test_conv1d.py b/backends/xnnpack/test/ops/test_conv1d.py index b637e1d95bf..cd6e0d531bd 100644 --- a/backends/xnnpack/test/ops/test_conv1d.py +++ b/backends/xnnpack/test/ops/test_conv1d.py @@ -91,12 +91,13 @@ def forward(self, x): return z class Conv1dSamePadding(torch.nn.Module): - def __init__(self, kernel_size: int): + def __init__(self, kernel_size: int, dilation: int = 1): super().__init__() self.conv1d = torch.nn.Conv1d( in_channels=2, out_channels=4, kernel_size=kernel_size, + dilation=dilation, padding="same", bias=True, ) @@ -177,25 +178,33 @@ def test_qs8_conv1d(self): def test_qs8_conv1d_even_kernel_same_padding(self): inputs = (torch.randn(1, 2, 16),) - ( - Tester(self.Conv1dSamePadding(kernel_size=4), inputs) - .quantize( - Quantize(calibration_samples=self._get_calibration_samples(inputs)) - ) - .export() - .check_count({"torch.ops.aten.conv1d.padding": 1}) - .to_edge_transform_and_lower() - .check_not( - [ - "executorch_exir_dialects_edge__ops_aten_convolution_default", - "executorch_exir_dialects_edge__ops_aten_constant_pad_nd_default", - ] - ) - .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) - .to_executorch() - .serialize() - .run_method_and_compare_outputs(num_runs=10, atol=0.04, rtol=0.02) - ) + configs = [ + (2, 1), + (3, 1), + (4, 1), + (4, 2), + ] + for kernel_size, dilation in configs: + with self.subTest(kernel_size=kernel_size, dilation=dilation): + ( + Tester(self.Conv1dSamePadding(kernel_size=kernel_size, dilation=dilation), inputs) + .quantize( + Quantize(calibration_samples=self._get_calibration_samples(inputs)) + ) + .export() + .check_count({"torch.ops.aten.conv1d.padding": 1}) + .to_edge_transform_and_lower() + .check_not( + [ + "executorch_exir_dialects_edge__ops_aten_convolution_default", + "executorch_exir_dialects_edge__ops_aten_constant_pad_nd_default", + ] + ) + .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) + .to_executorch() + .serialize() + .run_method_and_compare_outputs(num_runs=10, atol=0.04, rtol=0.02) + ) def test_qs8_conv1d_batchnorm_seq(self): inputs = (torch.randn(2, 2, 4),) From bf3644f3fb354c038994dc8b3f18b1ca7572908f Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Thu, 9 Jul 2026 12:00:28 -0400 Subject: [PATCH 3/8] Use explicit XNNPACK PAD op for Conv1d same-padding (adopt #20553 approach) Instead of folding constant_pad_nd into asymmetric Conv2d padding, keep it as an explicit XNNPACK pad op: - Remove pad-folding logic from Conv1dUnsqueezePass - Remove Conv1dFoldedPadMetaPass (no longer needed) - Revert xnnpack_input_padding handling in op_conv2d.py - Add InsertPadQDQPass: inserts QDQ after pad in quantized contexts so it serializes as a quantized static pad This matches the approach from #20553 and fixes the crash with conv1d -> flatten (even kernel + same padding). --- backends/xnnpack/_passes/__init__.py | 4 +- .../xnnpack/_passes/conv1d_unsqueeze_pass.py | 119 ------------------ backends/xnnpack/_passes/insert_pad_qdq.py | 102 +++++++-------- backends/xnnpack/operators/op_conv2d.py | 19 +-- 4 files changed, 53 insertions(+), 191 deletions(-) diff --git a/backends/xnnpack/_passes/__init__.py b/backends/xnnpack/_passes/__init__.py index 43be66bba8e..d29a32d6cae 100644 --- a/backends/xnnpack/_passes/__init__.py +++ b/backends/xnnpack/_passes/__init__.py @@ -16,10 +16,10 @@ ChannelsLastTaggedReshapePass, ) from executorch.backends.xnnpack._passes.conv1d_unsqueeze_pass import ( - Conv1dFoldedPadMetaPass, Conv1dUnsqueezePass, ) from executorch.backends.xnnpack._passes.convert_to_linear import ConvertToLinearPass +from executorch.backends.xnnpack._passes.insert_pad_qdq import InsertPadQDQPass from executorch.backends.xnnpack._passes.convert_to_sdpa import ConvertToSDPAPass from executorch.backends.xnnpack._passes.convert_to_upsample_bilinear2d import ( ConvertToUpsampleBilinear2d, @@ -28,7 +28,6 @@ from executorch.backends.xnnpack._passes.decompose_cat import DecomposeConcatenate from executorch.backends.xnnpack._passes.fuse_activation_pass import FuseActivationPass from executorch.backends.xnnpack._passes.fuse_batch_norm import FuseBatchNormPass -from executorch.backends.xnnpack._passes.insert_pad_qdq import InsertPadQDQPass from executorch.backends.xnnpack._passes.prelu_reshape_pass import PReLUReshapePass from executorch.backends.xnnpack._passes.propagate_custom_meta_pass import ( PropagateCustomMetaPass, @@ -88,7 +87,6 @@ def __init__( PReLUReshapePass, ChannelsLastTaggedReshapePass, RemoveRedundantCopyPass, - Conv1dFoldedPadMetaPass, ] else: self.passes = passes diff --git a/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py b/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py index ec511551164..d07ecb67e6b 100644 --- a/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py +++ b/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py @@ -160,40 +160,6 @@ def call(self, graph_module: torch.fx.GraphModule): # c. Add unsqueeze to input (3d -> 4d) and squeeze to output (4d -> 3d) # unsqueeze -> conv2d -> squeeze input_node = node.args[0] - if ( - isinstance(input_node, torch.fx.Node) - and input_node.target - == exir_ops.edge.aten.constant_pad_nd.default - and len(input_node.users) == 1 - and not node.args[6] - ): - pad_value = ( - input_node.args[2] if len(input_node.args) > 2 else 0 - ) - pad_amounts = list(input_node.args[1]) - temporal_only = len(pad_amounts) <= 2 or all( - amount == 0 for amount in pad_amounts[2:] - ) - if ( - pad_value == 0 - and len(pad_amounts) % 2 == 0 - and all(amount >= 0 for amount in pad_amounts) - and temporal_only - ): - time_before = pad_amounts[0] if len(pad_amounts) > 0 else 0 - time_after = pad_amounts[1] if len(pad_amounts) > 1 else 0 - conv_time_padding = node.args[4][0] - node.meta["xnnpack_input_padding"] = [ - conv_time_padding + time_before, - 0, - conv_time_padding + time_after, - 0, - ] - node.meta["xnnpack_conv1d_folded_pad"] = True - pad_node = input_node - input_node = pad_node.args[0] - node.replace_input_with(pad_node, input_node) - graph.erase_node(pad_node) with graph.inserting_before(node): unsqueeze_before = self.create_node( @@ -236,88 +202,3 @@ def call(self, graph_module: torch.fx.GraphModule): graph_module = super().call(graph_module).graph_module return PassResult(graph_module, True) - - -class Conv1dFoldedPadMetaPass(XNNPACKPass): - """ - Restores metadata for Conv1d nodes whose explicit pad was folded into - asymmetric XNNPACK input padding. - - Later passes retrace the graph using symmetric ATen convolution args, which - cannot represent even-kernel same padding exactly. Serialization, however, - needs tensor metadata that matches the asymmetric XNNPACK padding fields. - """ - - def _resize_meta_val(self, node: torch.fx.Node, shape: tuple): - if "val" not in node.meta: - return - - val = node.meta["val"] - if hasattr(val, "new_empty"): - node.meta["val"] = val.new_empty(shape) - else: - node.meta["val"] = torch.empty(shape) - - def _set_users_meta( - self, - node: torch.fx.Node, - shape_4d: tuple, - shape_3d: tuple, - ): - visited = set() - stack = [(user, shape_4d) for user in node.users] - while stack: - user, current_shape = stack.pop() - if user in visited or user.op != "call_function": - continue - visited.add(user) - - if user.target == exir_ops.edge.aten.squeeze_copy.dim: - self._resize_meta_val(user, shape_3d) - stack.extend((u, shape_3d) for u in user.users) - else: - self._resize_meta_val(user, current_shape) - stack.extend((u, current_shape) for u in user.users) - - def call(self, graph_module: torch.fx.GraphModule): - for node in graph_module.graph.nodes: - if ( - node.op != "call_function" - or node.target != exir_ops.edge.aten.convolution.default - or "xnnpack_input_padding" not in node.meta - or "xnnpack_conv1d_folded_pad" not in node.meta - ): - continue - - input_shape = tuple(node.args[0].meta["val"].shape) - kernel_shape = tuple(node.args[1].meta["val"].shape) - stride = node.args[3] - dilation = node.args[5] - padding_top, padding_right, padding_bottom, padding_left = node.meta[ - "xnnpack_input_padding" - ] - - kernel_h = kernel_shape[2] - kernel_w = kernel_shape[3] - out_h = ( - input_shape[2] - + padding_top - + padding_bottom - - dilation[0] * (kernel_h - 1) - - 1 - ) // stride[0] + 1 - out_w = ( - input_shape[3] - + padding_left - + padding_right - - dilation[1] * (kernel_w - 1) - - 1 - ) // stride[1] + 1 - - shape_4d = (input_shape[0], kernel_shape[0], out_h, out_w) - shape_3d = (input_shape[0], kernel_shape[0], out_h) - self._resize_meta_val(node, shape_4d) - self._set_users_meta(node, shape_4d, shape_3d) - - graph_module.recompile() - return PassResult(graph_module, True) diff --git a/backends/xnnpack/_passes/insert_pad_qdq.py b/backends/xnnpack/_passes/insert_pad_qdq.py index 744a642105e..a20f46040c8 100644 --- a/backends/xnnpack/_passes/insert_pad_qdq.py +++ b/backends/xnnpack/_passes/insert_pad_qdq.py @@ -4,81 +4,75 @@ # This source code is licensed under the BSD-style license found in the # LICENSE file in the root directory of this source tree. +from typing import cast, List + import torch from executorch.backends.xnnpack._passes.xnnpack_pass import XNNPACKPass -from executorch.backends.xnnpack.utils.quant_utils import is_quant, tag_as_implicit_q_dq +from executorch.backends.xnnpack.utils.quant_utils import ( + is_dequant, + tag_as_implicit_q_dq, +) from executorch.exir.dialects._ops import ops as exir_ops from executorch.exir.pass_base import PassResult class InsertPadQDQPass(XNNPACKPass): """ - Completes the quantization of a constant_pad_nd that sits inside a quantized - region but was left with an fp32 output. - - An even-kernel 'same'-padding conv decomposes (after quantization) into - dequant -> constant_pad_nd -> convolution. Because the pad is introduced by - to_edge decomposition -- after the quantizer has run -- it is never annotated, - so no quantize follows it and its output would serialize as fp32. The - downstream conv would then reject its (now fp32) activation. + Inserts implicit quantize/dequantize pairs after constant_pad_nd nodes + that sit in a quantized context (input is a dequantize node), so the pad + can be serialized as a quantized static pad op. - A zero-valued pad preserves quantization, so we insert an implicit - quantize -> dequantize pair after the pad, reusing the feeding dequant's - params. The pad then delegates as a normal quantized XNNStaticConstantPad and - the conv sees a proper dequantized activation. The pad node itself is left in - place, so all graph shapes stay consistent through later retracing passes. + Without this pass, a zero-valued constant_pad_nd between a dequantize and + a convolution would serialize as fp32 while the conv expects quantized + activation, causing a mismatch. """ - def _insert_qdq_after(self, graph, pad, q_params): - with graph.inserting_after(pad): - q = graph.create_node( - "call_function", - exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, - args=(), - ) - q.meta = pad.meta.copy() - tag_as_implicit_q_dq(q) - with graph.inserting_after(q): - dq = graph.create_node( - "call_function", - exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, - args=(q,) + q_params, - ) - dq.meta = q.meta.copy() - tag_as_implicit_q_dq(dq) - pad.replace_all_uses_with(dq) - # Set last so replace_all_uses_with above does not rewrite the quantize's - # own input. - q.args = (pad,) + q_params - - def call(self, graph_module: torch.fx.GraphModule): + def call(self, graph_module: torch.fx.GraphModule) -> PassResult: graph = graph_module.graph - for pad in list(graph.nodes): + for node in list(graph.nodes): if ( - pad.op != "call_function" - or pad.target != exir_ops.edge.aten.constant_pad_nd.default + node.op != "call_function" + or node.target != exir_ops.edge.aten.constant_pad_nd.default ): continue - # Only per-tensor static activations are handled: _insert_qdq_after - # builds quantize_per_tensor.default, so the feeding dequant must have - # the matching per-tensor signature (a per-channel/per-token/affine - # dequant would supply mismatched args). - dq = pad.args[0] - if ( - not isinstance(dq, torch.fx.Node) - or dq.target - != exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default + pad_input = node.args[0] + if not ( + isinstance(pad_input, torch.fx.Node) and is_dequant(pad_input) ): continue - # Skip if the pad's output is already quantized. Requiring *no* user to - # be a quantize (rather than merely "not all") avoids double-quantizing - # pre-existing quant consumers when the pad has mixed users. - if not pad.users or any(is_quant(user) for user in pad.users): + pad_value = cast(float, node.args[2]) if len(node.args) > 2 else 0.0 + if pad_value != 0.0: continue - self._insert_qdq_after(graph, pad, tuple(dq.args[1:])) + pad_amounts = cast(List[int], node.args[1]) + if any(p < 0 for p in pad_amounts): + continue + + q_params = pad_input.args[1:] + + with graph.inserting_after(node): + q = graph.create_node( + "call_function", + exir_ops.edge.quantized_decomposed.quantize_per_tensor.default, + args=(node,) + q_params, + ) + q.meta = node.meta.copy() + tag_as_implicit_q_dq(q) + + with graph.inserting_after(q): + dq = graph.create_node( + "call_function", + exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default, + args=(q,) + q_params, + ) + dq.meta = q.meta.copy() + tag_as_implicit_q_dq(dq) + + node.replace_all_uses_with(dq) + q.args = (node,) + q_params graph_module.recompile() + graph_module = super().call(graph_module).graph_module return PassResult(graph_module, True) diff --git a/backends/xnnpack/operators/op_conv2d.py b/backends/xnnpack/operators/op_conv2d.py index c095a0fc41a..315bbd863cb 100644 --- a/backends/xnnpack/operators/op_conv2d.py +++ b/backends/xnnpack/operators/op_conv2d.py @@ -149,21 +149,10 @@ def define_node( check_or_raise( len(stride) == 2, "XNNPACK currently only supports 2D convolution" ) - folded_padding = node.meta.get("xnnpack_input_padding") - if folded_padding is not None: - check_or_raise( - len(folded_padding) == 4, - "Expected XNNPACK padding as [top, right, bottom, left]", - ) - kwargs["padding_top"] = folded_padding[0] - kwargs["padding_right"] = folded_padding[1] - kwargs["padding_bottom"] = folded_padding[2] - kwargs["padding_left"] = folded_padding[3] - else: - kwargs["padding_top"] = padding[0] - kwargs["padding_right"] = padding[1] - kwargs["padding_bottom"] = padding[0] - kwargs["padding_left"] = padding[1] + kwargs["padding_top"] = padding[0] + kwargs["padding_right"] = padding[1] + kwargs["padding_bottom"] = padding[0] + kwargs["padding_left"] = padding[1] kwargs["kernel_height"] = kernel_shape[2] kwargs["kernel_width"] = kernel_shape[3] kwargs["subsampling_height"] = stride[0] From 1fd9e3b7530e8522e0cbd3a41d3de761878fa59b Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Sat, 11 Jul 2026 10:49:01 -0400 Subject: [PATCH 4/8] Fix idempotency and pad-deps for InsertPadQDQPass and ConvolutionConfig MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor InsertPadQDQPass with guard checks (pad_value, pad_amounts, negative amounts) and correct idempotency (re-add is_quant check). Use is_dequant instead of matching specific dequant target. Add super().call() retrace to update graph_module metadata. Merge duplicated _get_act_deps in ConvolutionConfig — handle both 1D and 2D convs with their respective pad constraints. Also follow QDQ chain (dequant -> pad -> q -> dq -> conv) when InsertPadQDQPass has already inserted a quantize/dequantize pair after the pad. Restore XNNPACK pass ordering in __init__.py and Conv1dUnsqueezePass variable scoping to match origin/main (no functional change). Add regression test for quantized Conv1d even-kernel same-padding covering both symmetric and asymmetric pad cases. Co-authored-by: Claude --- backends/xnnpack/_passes/__init__.py | 2 +- .../xnnpack/_passes/conv1d_unsqueeze_pass.py | 3 +- backends/xnnpack/_passes/insert_pad_qdq.py | 10 +- .../xnnpack/partition/config/gemm_configs.py | 99 +++++++++---------- backends/xnnpack/test/ops/test_conv1d.py | 11 ++- 5 files changed, 67 insertions(+), 58 deletions(-) diff --git a/backends/xnnpack/_passes/__init__.py b/backends/xnnpack/_passes/__init__.py index d29a32d6cae..22147fa4215 100644 --- a/backends/xnnpack/_passes/__init__.py +++ b/backends/xnnpack/_passes/__init__.py @@ -19,7 +19,6 @@ Conv1dUnsqueezePass, ) from executorch.backends.xnnpack._passes.convert_to_linear import ConvertToLinearPass -from executorch.backends.xnnpack._passes.insert_pad_qdq import InsertPadQDQPass from executorch.backends.xnnpack._passes.convert_to_sdpa import ConvertToSDPAPass from executorch.backends.xnnpack._passes.convert_to_upsample_bilinear2d import ( ConvertToUpsampleBilinear2d, @@ -28,6 +27,7 @@ from executorch.backends.xnnpack._passes.decompose_cat import DecomposeConcatenate from executorch.backends.xnnpack._passes.fuse_activation_pass import FuseActivationPass from executorch.backends.xnnpack._passes.fuse_batch_norm import FuseBatchNormPass +from executorch.backends.xnnpack._passes.insert_pad_qdq import InsertPadQDQPass from executorch.backends.xnnpack._passes.prelu_reshape_pass import PReLUReshapePass from executorch.backends.xnnpack._passes.propagate_custom_meta_pass import ( PropagateCustomMetaPass, diff --git a/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py b/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py index d07ecb67e6b..7a6b031160a 100644 --- a/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py +++ b/backends/xnnpack/_passes/conv1d_unsqueeze_pass.py @@ -159,9 +159,8 @@ def call(self, graph_module: torch.fx.GraphModule): # c. Add unsqueeze to input (3d -> 4d) and squeeze to output (4d -> 3d) # unsqueeze -> conv2d -> squeeze - input_node = node.args[0] - with graph.inserting_before(node): + input_node = node.args[0] unsqueeze_before = self.create_node( graph, exir_ops.edge.aten.unsqueeze_copy.default ) diff --git a/backends/xnnpack/_passes/insert_pad_qdq.py b/backends/xnnpack/_passes/insert_pad_qdq.py index a20f46040c8..34ca6cfce78 100644 --- a/backends/xnnpack/_passes/insert_pad_qdq.py +++ b/backends/xnnpack/_passes/insert_pad_qdq.py @@ -10,6 +10,7 @@ from executorch.backends.xnnpack._passes.xnnpack_pass import XNNPACKPass from executorch.backends.xnnpack.utils.quant_utils import ( is_dequant, + is_quant, tag_as_implicit_q_dq, ) from executorch.exir.dialects._ops import ops as exir_ops @@ -22,6 +23,8 @@ class InsertPadQDQPass(XNNPACKPass): that sit in a quantized context (input is a dequantize node), so the pad can be serialized as a quantized static pad op. + Skips pads whose output is already quantized (idempotent). + Without this pass, a zero-valued constant_pad_nd between a dequantize and a convolution would serialize as fp32 while the conv expects quantized activation, causing a mismatch. @@ -37,9 +40,7 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: continue pad_input = node.args[0] - if not ( - isinstance(pad_input, torch.fx.Node) and is_dequant(pad_input) - ): + if not (isinstance(pad_input, torch.fx.Node) and is_dequant(pad_input)): continue pad_value = cast(float, node.args[2]) if len(node.args) > 2 else 0.0 @@ -50,6 +51,9 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: if any(p < 0 for p in pad_amounts): continue + if any(is_quant(user) for user in node.users): + continue + q_params = pad_input.args[1:] with graph.inserting_after(node): diff --git a/backends/xnnpack/partition/config/gemm_configs.py b/backends/xnnpack/partition/config/gemm_configs.py index 5bd9b88d1e8..f093b60cf15 100644 --- a/backends/xnnpack/partition/config/gemm_configs.py +++ b/backends/xnnpack/partition/config/gemm_configs.py @@ -401,29 +401,59 @@ def _get_act_deps( self, node: torch.fx.Node, ep: ExportedProgram, precision: ConfigPrecisionType ) -> Tuple[bool, List[torch.fx.Node]]: act_input = get_input_node(node, self.act_idx) - conv_stride = cast(List[int], node.args[3]) + is_transpose = node.args[6] if ( precision != ConfigPrecisionType.FP32 - and len(conv_stride) == 1 + and not is_transpose and is_node(act_input) - and act_input.target == exir_ops.edge.aten.constant_pad_nd.default - and len(act_input.users) == 1 - and not node.args[6] ): - pad_value = act_input.args[2] if len(act_input.args) > 2 else 0 - pad_amounts = cast(List[int], act_input.args[1]) - temporal_only = len(pad_amounts) <= 2 or all( - a == 0 for a in pad_amounts[2:] - ) - if ( - pad_value == 0 - and len(pad_amounts) % 2 == 0 - and all(a >= 0 for a in pad_amounts) - and temporal_only - ): - valid, deps = super()._get_act_deps(act_input, ep, precision) - if valid: - return (True, [act_input, *deps]) + # Find a constant_pad_nd in the activation chain. It may be the direct + # input to the conv, or it may be behind the QDQ pair inserted by + # InsertPadQDQPass (dequant -> pad -> q -> dq -> conv). + pad_node = None + if act_input.target == exir_ops.edge.aten.constant_pad_nd.default: + pad_node = act_input + elif is_dequant(act_input) and len(act_input.users) == 1: + q_node = get_input_node(act_input, 0) + if isinstance(q_node, torch.fx.Node) and is_quant(q_node): + maybe_pad = get_input_node(q_node, 0) + if ( + isinstance(maybe_pad, torch.fx.Node) + and maybe_pad.target + == exir_ops.edge.aten.constant_pad_nd.default + ): + pad_node = maybe_pad + + if pad_node is not None and len(pad_node.users) == 1: + conv_padding = cast(List[int], node.args[4]) + is_1d = len(conv_padding) == 1 + is_2d = len(conv_padding) == 2 + + pad_value = pad_node.args[2] if len(pad_node.args) > 2 else 0 + if pad_value != 0.0: + return super()._get_act_deps(node, ep, precision) + + pad_amounts = cast(List[int], pad_node.args[1]) + + pad_ok = False + if is_1d: + temporal_only = len(pad_amounts) <= 2 or all( + a == 0 for a in pad_amounts[2:] + ) + pad_ok = ( + len(pad_amounts) % 2 == 0 + and all(a >= 0 for a in pad_amounts) + and temporal_only + ) + elif is_2d: + pad_ok = len(pad_amounts) <= 4 or all( + a == 0 for a in pad_amounts[4:] + ) + + if pad_ok: + valid, deps = super()._get_act_deps(pad_node, ep, precision) + if valid: + return (True, [pad_node, *deps]) return super()._get_act_deps(node, ep, precision) @@ -434,37 +464,6 @@ def supported_precision_types(self): ConfigPrecisionType.DYNAMIC_QUANT, ] - def _get_act_deps( - self, node: torch.fx.Node, ep: ExportedProgram, precision: ConfigPrecisionType - ) -> Tuple[bool, List[torch.fx.Node]]: - # An even-kernel 'same'-padding conv decomposes into - # dequant -> constant_pad_nd -> convolution. Pull the zero-valued spatial - # pad into this conv's partition so it delegates as a quantized - # XNNStaticConstantPad alongside the conv (InsertPadQDQPass completes the - # pad's quantization); otherwise the pad is orphaned and the conv is left - # with an unquantized activation. - # Only supporting 2D, non-transposed convs - is_transpose = node.args[6] - is_2d_conv = len(cast(List[int], node.args[4])) == 2 - act_input = get_input_node(node, self.act_idx) - if ( - precision != ConfigPrecisionType.FP32 - and not is_transpose - and is_2d_conv - and act_input.target == exir_ops.edge.aten.constant_pad_nd.default - and len(act_input.users) == 1 - and is_dequant(get_input_node(act_input, 0)) - ): - pad_value = act_input.args[2] if len(act_input.args) > 2 else 0 - pad_amounts = cast(List[int], act_input.args[1]) - spatial_only = len(pad_amounts) <= 4 or all(a == 0 for a in pad_amounts[4:]) - if pad_value == 0 and spatial_only: - valid, deps = super()._get_act_deps(act_input, ep, precision) - if valid: - return (True, [act_input, *deps]) - - return super()._get_act_deps(node, ep, precision) - class AddmmConfig(GEMMConfig): """ diff --git a/backends/xnnpack/test/ops/test_conv1d.py b/backends/xnnpack/test/ops/test_conv1d.py index cd6e0d531bd..20dc864bf87 100644 --- a/backends/xnnpack/test/ops/test_conv1d.py +++ b/backends/xnnpack/test/ops/test_conv1d.py @@ -187,9 +187,16 @@ def test_qs8_conv1d_even_kernel_same_padding(self): for kernel_size, dilation in configs: with self.subTest(kernel_size=kernel_size, dilation=dilation): ( - Tester(self.Conv1dSamePadding(kernel_size=kernel_size, dilation=dilation), inputs) + Tester( + self.Conv1dSamePadding( + kernel_size=kernel_size, dilation=dilation + ), + inputs, + ) .quantize( - Quantize(calibration_samples=self._get_calibration_samples(inputs)) + Quantize( + calibration_samples=self._get_calibration_samples(inputs) + ) ) .export() .check_count({"torch.ops.aten.conv1d.padding": 1}) From 4c182385abfa2eea16b9217765cade9d8729a108 Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Sun, 12 Jul 2026 13:32:27 -0400 Subject: [PATCH 5/8] Fix InsertPadQDQPass idempotency by removing redundant super().call() --- backends/xnnpack/_passes/insert_pad_qdq.py | 1 - 1 file changed, 1 deletion(-) diff --git a/backends/xnnpack/_passes/insert_pad_qdq.py b/backends/xnnpack/_passes/insert_pad_qdq.py index 34ca6cfce78..f5db7d9b620 100644 --- a/backends/xnnpack/_passes/insert_pad_qdq.py +++ b/backends/xnnpack/_passes/insert_pad_qdq.py @@ -78,5 +78,4 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: q.args = (node,) + q_params graph_module.recompile() - graph_module = super().call(graph_module).graph_module return PassResult(graph_module, True) From a4be095cfda8be07932f5249b6fcc9e4ad67d027 Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Mon, 13 Jul 2026 14:24:22 -0400 Subject: [PATCH 6/8] Address Claude review: per-tensor guard, dead QDQ-chain branch, minor cleanups - insert_pad_qdq.py: restore is_per_tensor guard dropped during rewrite; track modified flag accurately - gemm_configs.py: remove dead QDQ-chain branch; remove redundant len%2==0 and duplicate fallthrough - lint fixes applied --- backends/xnnpack/_passes/insert_pad_qdq.py | 11 ++- .../xnnpack/partition/config/gemm_configs.py | 69 ++++++------------- 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/backends/xnnpack/_passes/insert_pad_qdq.py b/backends/xnnpack/_passes/insert_pad_qdq.py index f5db7d9b620..0edd305daa7 100644 --- a/backends/xnnpack/_passes/insert_pad_qdq.py +++ b/backends/xnnpack/_passes/insert_pad_qdq.py @@ -10,6 +10,7 @@ from executorch.backends.xnnpack._passes.xnnpack_pass import XNNPACKPass from executorch.backends.xnnpack.utils.quant_utils import ( is_dequant, + is_per_tensor, is_quant, tag_as_implicit_q_dq, ) @@ -32,6 +33,7 @@ class InsertPadQDQPass(XNNPACKPass): def call(self, graph_module: torch.fx.GraphModule) -> PassResult: graph = graph_module.graph + modified = False for node in list(graph.nodes): if ( node.op != "call_function" @@ -40,7 +42,11 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: continue pad_input = node.args[0] - if not (isinstance(pad_input, torch.fx.Node) and is_dequant(pad_input)): + if not ( + isinstance(pad_input, torch.fx.Node) + and is_dequant(pad_input) + and is_per_tensor(pad_input) + ): continue pad_value = cast(float, node.args[2]) if len(node.args) > 2 else 0.0 @@ -76,6 +82,7 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: node.replace_all_uses_with(dq) q.args = (node,) + q_params + modified = True graph_module.recompile() - return PassResult(graph_module, True) + return PassResult(graph_module, modified) diff --git a/backends/xnnpack/partition/config/gemm_configs.py b/backends/xnnpack/partition/config/gemm_configs.py index f093b60cf15..0296964b730 100644 --- a/backends/xnnpack/partition/config/gemm_configs.py +++ b/backends/xnnpack/partition/config/gemm_configs.py @@ -406,54 +406,29 @@ def _get_act_deps( precision != ConfigPrecisionType.FP32 and not is_transpose and is_node(act_input) + and act_input.target == exir_ops.edge.aten.constant_pad_nd.default + and len(act_input.users) == 1 ): - # Find a constant_pad_nd in the activation chain. It may be the direct - # input to the conv, or it may be behind the QDQ pair inserted by - # InsertPadQDQPass (dequant -> pad -> q -> dq -> conv). - pad_node = None - if act_input.target == exir_ops.edge.aten.constant_pad_nd.default: - pad_node = act_input - elif is_dequant(act_input) and len(act_input.users) == 1: - q_node = get_input_node(act_input, 0) - if isinstance(q_node, torch.fx.Node) and is_quant(q_node): - maybe_pad = get_input_node(q_node, 0) - if ( - isinstance(maybe_pad, torch.fx.Node) - and maybe_pad.target - == exir_ops.edge.aten.constant_pad_nd.default - ): - pad_node = maybe_pad - - if pad_node is not None and len(pad_node.users) == 1: - conv_padding = cast(List[int], node.args[4]) - is_1d = len(conv_padding) == 1 - is_2d = len(conv_padding) == 2 - - pad_value = pad_node.args[2] if len(pad_node.args) > 2 else 0 - if pad_value != 0.0: - return super()._get_act_deps(node, ep, precision) - - pad_amounts = cast(List[int], pad_node.args[1]) - - pad_ok = False - if is_1d: - temporal_only = len(pad_amounts) <= 2 or all( - a == 0 for a in pad_amounts[2:] - ) - pad_ok = ( - len(pad_amounts) % 2 == 0 - and all(a >= 0 for a in pad_amounts) - and temporal_only - ) - elif is_2d: - pad_ok = len(pad_amounts) <= 4 or all( - a == 0 for a in pad_amounts[4:] - ) - - if pad_ok: - valid, deps = super()._get_act_deps(pad_node, ep, precision) - if valid: - return (True, [pad_node, *deps]) + conv_padding = cast(List[int], node.args[4]) + is_1d = len(conv_padding) == 1 + is_2d = len(conv_padding) == 2 + + pad_value = ( + cast(float, act_input.args[2]) if len(act_input.args) > 2 else 0.0 + ) + pad_amounts = cast(List[int], act_input.args[1]) + spatial_only = ( + is_1d + and (len(pad_amounts) <= 2 or all(a == 0 for a in pad_amounts[2:])) + ) or ( + is_2d + and (len(pad_amounts) <= 4 or all(a == 0 for a in pad_amounts[4:])) + ) + + if pad_value == 0.0 and all(a >= 0 for a in pad_amounts) and spatial_only: + valid, deps = super()._get_act_deps(act_input, ep, precision) + if valid: + return (True, [act_input, *deps]) return super()._get_act_deps(node, ep, precision) From 2d4f494e31ea00d419de595fee6911cb135d6d98 Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Tue, 14 Jul 2026 23:15:57 -0400 Subject: [PATCH 7/8] Tighten dequant check in InsertPadQDQPass: remove redundant is_dequant is_per_tensor already verifies the node is a quant/dequant with per_tensor in its target name. The broader is_dequant check was confusing and created a mismatch with the create_node targets below. Co-authored-by: Claude --- backends/xnnpack/_passes/insert_pad_qdq.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/backends/xnnpack/_passes/insert_pad_qdq.py b/backends/xnnpack/_passes/insert_pad_qdq.py index 0edd305daa7..e115be92738 100644 --- a/backends/xnnpack/_passes/insert_pad_qdq.py +++ b/backends/xnnpack/_passes/insert_pad_qdq.py @@ -9,7 +9,6 @@ import torch from executorch.backends.xnnpack._passes.xnnpack_pass import XNNPACKPass from executorch.backends.xnnpack.utils.quant_utils import ( - is_dequant, is_per_tensor, is_quant, tag_as_implicit_q_dq, @@ -44,7 +43,6 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: pad_input = node.args[0] if not ( isinstance(pad_input, torch.fx.Node) - and is_dequant(pad_input) and is_per_tensor(pad_input) ): continue From db93db712e5419272c41dcf9decd2a14cf1f838b Mon Sep 17 00:00:00 2001 From: SakshamKapoor2911 Date: Wed, 15 Jul 2026 09:24:37 -0400 Subject: [PATCH 8/8] Tighten dequant check in InsertPadQDQPass: exact target match for .default only is_per_tensor() matches both dequantize_per_tensor.default and dequantize_per_tensor.tensor, but we always insert quantize_per_tensor.default. Using .tensor q_params to build a .default node would produce a malformed node. Restore the original exact-signature guard from main (pad_input.target == .default) with an added is_dequant fast-path gate. --- backends/xnnpack/_passes/insert_pad_qdq.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backends/xnnpack/_passes/insert_pad_qdq.py b/backends/xnnpack/_passes/insert_pad_qdq.py index e115be92738..3187f272e46 100644 --- a/backends/xnnpack/_passes/insert_pad_qdq.py +++ b/backends/xnnpack/_passes/insert_pad_qdq.py @@ -9,7 +9,7 @@ import torch from executorch.backends.xnnpack._passes.xnnpack_pass import XNNPACKPass from executorch.backends.xnnpack.utils.quant_utils import ( - is_per_tensor, + is_dequant, is_quant, tag_as_implicit_q_dq, ) @@ -43,7 +43,9 @@ def call(self, graph_module: torch.fx.GraphModule) -> PassResult: pad_input = node.args[0] if not ( isinstance(pad_input, torch.fx.Node) - and is_per_tensor(pad_input) + and is_dequant(pad_input) + and pad_input.target + == exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default ): continue