Skip to content

Commit 9ffbe36

Browse files
authored
Fix XNNPACK rejecting avg_pool2d with default arguments (pytorch#21862)
## Problem `AvgPoolingConfig.check_constraints` rejects every node with `count_include_pad=True`: ```python if count_include_pad: why( node, reason="zero-padding in the averaging calculation is not supported", ) return False ``` But `count_include_pad=True` is the aten default, so a plain `nn.AvgPool2d(2)` — the most common way to write the layer — never delegates to XNNPACK. The delegate splits around it and the node falls back to the portable op, and the `WhyNoPartition` log tells the user their model has zero-padding it does not have: ``` WhyNoPartition: Node aten_avg_pool2d_default was not partitioned because zero-padding in the averaging calculation is not supported. ``` The flag only has semantics when padded elements exist: it controls whether zeros from padding count toward the averaging divisor. With the default `padding=0` there is nothing to include or exclude, so the rejection excludes nodes XNNPACK handles correctly. Verified numerically: with `padding=0`, eager outputs for `count_include_pad=True` and `False` are bit-identical, and the delegated result matches eager within ordinary float tolerance; with `padding=1` they genuinely differ, which is the case the backend does not implement. ## Fix Reject `count_include_pad=True` only when the node has non-zero padding, reading padding from the existing `normalize_pool2d_args` call: ```python kernel_size, _, padding, _ = normalize_pool2d_args(node, has_dilation=False) ... if count_include_pad and any(p != 0 for p in padding): ``` A no-op for every node the config currently accepts (those all have `count_include_pad=False`), and every currently-rejected padded node is still rejected, so the only behavior change is crash-to-delegate for the default-argument case. The visitor needs no change: it serializes the padding values themselves and never reads `count_include_pad`, and XNNPACK's divisor convention is irrelevant when no padded elements exist. ## Effect Same model, `nn.AvgPool2d(2)` with all-default arguments, through `to_edge_transform_and_lower` with the XNNPACK partitioner: ``` before: delegates=0 remaining aten.avg_pool2d.default in graph, WhyNoPartition blames zero-padding after: delegates=1 no avg_pool2d left in graph, output matches eager guard: padding=(1,1) with count_include_pad=True still not delegated ``` ## Testing New `test_fp32_avgpool2d_default_args` lowers `nn.AvgPool2d(2)` end to end through serialize and compares outputs against eager. Verified failing-first: with the tests kept and only the source reverted, exactly that test fails and the other seven pass, including `test_fp32_avgpool2d_count_include_pad_unsupported` (padding=(1,1)), which still rejects under the fix. backends/xnnpack/test/ops/test_avgpool2d.py: 7 -> 8 passed. Full backends/xnnpack/test/ops/ suite: 229 passed on main, 230 passed with this change, no regressions. The suite runs exclude test_prelu.py, whose test_fp32_prelu_constant_weight_empty_decompositions_file_load segfaults identically on unmodified main in my environment (prebuilt runtime, macOS arm64), so it cannot observe this change either way. lintrunner reports no issues on both changed files. cc @GregoryComer @digantdesai @cbilgin @JakeStevens
1 parent 3dfa644 commit 9ffbe36

2 files changed

Lines changed: 32 additions & 4 deletions

File tree

‎backends/xnnpack/partition/config/generic_node_configs.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,8 @@ class AvgPoolingConfig(GenericNodePartitionerConfig):
168168

169169
def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
170170
"""
171-
XNNPACK does not support ceil_mode = True and count_include_pad = True
171+
XNNPACK does not support ceil_mode = True, or count_include_pad = True
172+
when the node has non-zero padding.
172173
Additionally, we only support divisor_override if divisor_override = pooling region
173174
"""
174175
if not self.check_common_constraints(node, ep):
@@ -184,7 +185,7 @@ def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
184185
if len(args) >= 6:
185186
count_include_pad = cast(bool, args[5])
186187

187-
kernel_size, _, _, _ = normalize_pool2d_args(node, has_dilation=False)
188+
kernel_size, _, padding, _ = normalize_pool2d_args(node, has_dilation=False)
188189
pooling_region = kernel_size[0] * kernel_size[1]
189190
divisor_override = pooling_region # Default divisor is pooling_region
190191
if len(args) >= 7:
@@ -194,7 +195,7 @@ def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
194195
why(node, reason="ceil mode is not supported")
195196
return False
196197

197-
if count_include_pad:
198+
if count_include_pad and any(p != 0 for p in padding):
198199
why(
199200
node,
200201
reason="zero-padding in the averaging calculation is not supported",

‎backends/xnnpack/test/ops/test_avgpool2d.py‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ def test_fp32_avgpool2d_ceil_mode_unsupported(self):
6767

6868
def test_fp32_avgpool2d_count_include_pad_unsupported(self):
6969
"""
70-
The XNNPACK backend does not support count_include_pad=True.
70+
The XNNPACK backend does not support count_include_pad=True with
71+
non-zero padding.
7172
"""
7273
inputs = (torch.randn(1, 1, 10, 10),)
7374
(
@@ -78,6 +79,32 @@ def test_fp32_avgpool2d_count_include_pad_unsupported(self):
7879
.check_not(["torch.ops.higher_order.executorch_call_delegate"])
7980
)
8081

82+
class AvgPool2dAllDefaults(torch.nn.Module):
83+
def __init__(self):
84+
super().__init__()
85+
self.avgPool = torch.nn.AvgPool2d(2)
86+
87+
def forward(self, x):
88+
return self.avgPool(x)
89+
90+
def test_fp32_avgpool2d_default_args(self):
91+
"""
92+
count_include_pad defaults to True, but with the default zero padding
93+
there are no padded elements to count, so the node must still delegate.
94+
"""
95+
inputs = (torch.randn(1, 1, 10, 10),)
96+
(
97+
Tester(self.AvgPool2dAllDefaults(), inputs)
98+
.export()
99+
.check_count({"torch.ops.aten.avg_pool2d.default": 1})
100+
.to_edge_transform_and_lower()
101+
.check_count({"torch.ops.higher_order.executorch_call_delegate": 1})
102+
.check_not(["executorch_exir_dialects_edge__ops_aten_avg_pool2d_default"])
103+
.to_executorch()
104+
.serialize()
105+
.run_method_and_compare_outputs()
106+
)
107+
81108
class AvgPool2dSingleElementKernel(torch.nn.Module):
82109
def __init__(self, divisor_override=None):
83110
super().__init__()

0 commit comments

Comments
 (0)