Fix XNNPACK pooling crashing on default stride and single-element kernel_size - #21489
Conversation
…nel_size The aten pool2d schemas declare stride with a default of the empty list, meaning kernel_size, and declare kernel_size, stride, padding and dilation as int[2], which accepts a scalar or a 1-element list that broadcasts to both dimensions. The XNNPACK partitioner configs and visitors indexed node.args positionally and assumed a fully populated 2-element list, so F.max_pool2d(x, 2) raised IndexError from inside the partitioner's own constraint check, and a 1-element kernel_size raised while computing the pooling region. normalize_pool2d_args applies the schema defaults in one place, next to the existing normalize_mean_dims which exists for the same reason on mean.dim. Fixes pytorch#10968 Fixes pytorch#10969
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21489
Note: Links to docs will display an error until the docs builds have been completed. ✅ No FailuresAs of commit 75962fe with merge base a1d4d35 ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
|
|
Thanks for filing the issue and the PR, @slipstr34m. Running CI tests now. |
JakeStevens
left a comment
There was a problem hiding this comment.
lgtm, waiting on CI results
|
|
||
| def normalize_pool2d_args( | ||
| node: torch.fx.Node, has_dilation: bool | ||
| ) -> Tuple[List[int], List[int], List[int], List[int]]: |
There was a problem hiding this comment.
nit: these are all fixed sizes lists for the aten schema so we could consider immutable tuple instead
There was a problem hiding this comment.
On the tuple nit, agreed, and I have it ready locally. I held off pushing since a new commit re-triggers the CI approval, and that seemed like a poor trade for a nit on an already-green PR. Happy to push it if you'd rather have it in before merge.
|
@JakeStevens CI finished green after your approval. |
Fixes #21488
Fixes #10968
Fixes #10969
Problem
The aten schemas are
Two schema facts the XNNPACK pooling code does not account for.
stridedefaults to the empty list, meaning "use kernel_size", and may be absent fromnode.argsentirely. Andint[2]accepts a scalar or a 1-element list, which broadcasts to both dimensions.MaxPool2dConfig.check_constraintsindexed positionally:and
AvgPoolingConfig.check_constraintsdidBoth assume a fully populated 2-element list, so three plain uses of the documented API raise
IndexErrorfrom inside the partitioner's own constraint check, before any support decision is reached:The visitors index the same way, so even a node that passed the config would fail during serialization.
Declaring
target_name = "max_pool2d.default"promises the whole schema. The identical models with an explicit 2-element stride lower and run correctly today.Fix
One helper,
normalize_pool2d_args, placed next to the existingnormalize_mean_dimsinbackends/xnnpack/utils/utils.py, which exists for exactly this reason onmean.dimand is already called from both that config and that visitor:It is called from
AvgPoolingConfig.check_constraints,MaxPool2dConfig.check_constraints,MaxPooling2d.define_nodeandAveragePooling2d.define_nodein place of the raw indexing.This is the same normalization five other backends here already do, so "the partitioner may assume canonical args" is contradicted by standing practice in the repo.
backends/arm/_passes/rewrite_max_pool2d_pass.py:82-87is structurally the same helper:backends/samsung/builders/op_max_pool2d.py:51-55does the same, includingstride = cast(List[int], node.args[2]) if len(node.args) > 2 else kernel_size.backends/mlx/ops.pycarries the commentif not stride: # empty list means default to kernel_size.backends/qualcomm/builders/op_max_pool2d.py:63-76andbackends/apple/mps/operators/convolution_ops.py:53-57both broadcast 1-element kernel, stride, padding and dilation.AvgPoolingConfigalready conceded the point internally. It guardslen(args) >= 5,>= 6and>= 7forceil_mode,count_include_padanddivisor_overrideon the lines around the crash. It knows arg tuples arrive short, it just started guarding at index 5 instead of index 2.Two incidental cleanups caused by the change, called out so they are not a surprise in review: the
# pyre-ignore[16]on the stride comparison is dropped becausestrideis now typed, andcast/Listimports that became unused are removed.Effect
Same script run on either side of the change.
delegatedis whether the node reached XNNPACK,maxdiffis deviation from eager.The negative control is the important one. Normalization could have turned "correctly decline" into "delegate something XNNPACK cannot do", and it does not.
Declining these nodes instead of normalizing them would also have fixed the crash, but it is strictly worse. All four cases match eager once they delegate, so XNNPACK does support them, and silently dropping
F.max_pool2d(x, 2)out of the delegate would be a performance regression on one of the most common CNN patterns there is.Blast radius
Only nodes whose pooling args are currently missing or scalar, which is exactly the set that raises today. For fully specified 2-element args the helper returns the same lists, so existing behaviour is unchanged. No serialization format or runtime change; the
.pteschema already carries separate height and width fields.Testing
Four cases added, two per file.
test_maxpool2d.pygets default stride and a 1-elementkernel_size.test_avgpool2d.pygets a 1-elementkernel_size, and a 1-elementkernel_sizewithdivisor_override=5. The second matters because normalizing turns thedivisor_override != pooling_regioncomparison from a crash into a live code path, and it must still decline, since the region is 3 * 3 rather than 5.Verified failing-first by reverting only the four source files and keeping the tests: all four fail with
IndexErroratgeneric_node_configs.py:352,:360and:184.Which spellings are affected, for reference:
nn.MaxPool2dsuppliesstridefromkernel_sizein its own__init__, and a bare int is expanded to[n, n]before the op is called, so the common spelling is safe on both counts and existing coverage never reaches these paths.lintrunnerreports no issues on all six changed files.cc @GregoryComer @digantdesai @cbilgin @JakeStevens