Skip to content

Infer XNNPACK conv dimensionality from the weight rank - #21588

Open
adityasingh2400 wants to merge 1 commit into
pytorch:mainfrom
adityasingh2400:fix-xnn-conv-single-spatial-param
Open

Infer XNNPACK conv dimensionality from the weight rank#21588
adityasingh2400 wants to merge 1 commit into
pytorch:mainfrom
adityasingh2400:fix-xnn-conv-single-spatial-param

Conversation

@adityasingh2400

@adityasingh2400 adityasingh2400 commented Aug 5, 2026

Copy link
Copy Markdown

Summary

Fixes #10965.

ATen lets stride, padding, dilation and output_padding be a single value that is broadcast over every spatial dim, so len(stride) is not the convolution's dimensionality. torch.nn.Conv2d(3, 3, 5, [2]) keeps stride=(2,) because _pair passes an iterable through unchanged, and that reaches the edge graph as aten.convolution.default with a length-1 stride on a rank-4 weight.

The XNNPACK backend infers dimensionality from that length in three places, so a 2d conv with a length-1 stride is mistaken for a 1d conv:

Conv1dUnsqueezePass skips a node unless len(stride) == 1, so it picked up the 2d conv, unsqueezed the already-4d weight to 5d and then failed inside the fake tensor convolution rule. That is the traceback in the issue.

ConvolutionConfig.check_constraints rejects 3d convs with len(conv_stride) > 2. A Conv3d built with stride=[2] passes that guard, gets partitioned, and then hits the same unsqueeze path. The dynamic-quant guard right below it has the same problem in reverse: a genuine 2d conv with a length-1 stride is refused for no reason.

Conv2d.define_node already normalizes padding and output_padding from length 1 to length 2, but not stride or dilation, so check_or_raise(len(stride) == 2) fires and dilation[1] would be out of range. That is the second failure the issue describes, the one reached by adding dilation=[2].

The fix takes the dimensionality from the weight rank, which is unambiguous, and normalizes stride and dilation in the node visitor the same way the two neighbouring params already are. _get_act_deps moves to the same weight-rank derivation, which also stops a length-1 padding from making a 2d conv look 1d and silently blocking the constant_pad_nd fusion.

Review order: _passes/conv1d_unsqueeze_pass.py, then partition/config/gemm_configs.py, then operators/op_conv2d.py, then the tests.

Test plan

Added to backends/xnnpack/test/ops/test_conv2d.py:

  • test_fp32_conv2d_single_element_spatial_params, transposed and not, covering stride=(2,) with padding=(1,)
  • test_fp32_conv2d_single_element_dilation, covering dilation=(2,)
  • test_fp32_conv3d_single_element_stride_doesnt_partition, asserting a 3d conv with stride=(2,) still produces zero delegates

I do not have an ExecuTorch C++ runtime built on this machine, so I could not run run_method_and_compare_outputs, and the three tests above were not executed as unit tests. I verified the export and lowering half of them directly against this branch and against upstream/main at efd6b55, driving the same modules through torch.export.export plus to_edge_transform_and_lower([XnnpackPartitioner()]) and counting delegate and leftover convolution nodes.

On upstream/main:

conv2d stride=[2]              -> Exception: An error occurred when running the 'Conv1dUnsqueezePass' pass
conv2d stride=[2] dilation=[2] -> Exception: An error occurred when running the 'Conv1dUnsqueezePass' pass
conv3d stride=[2]              -> Exception: An error occurred when running the 'Conv1dUnsqueezePass' pass

The conv3d row is the partitioner half of the bug. Today that conv is accepted by ConvolutionConfig and then dies in the same pass, so the "Only support 1D + 2D Conv" guard is not actually holding.

On this branch:

conv2d stride=(2,) padding=(1,) transpose=True   delegates=1 undelegated_conv=0
conv2d stride=(2,) padding=(1,) transpose=False  delegates=1 undelegated_conv=0
conv2d stride=(1,) padding=(1,) dilation=(2,)    delegates=1 undelegated_conv=0
conv2d stride=2  (unchanged baseline)            delegates=1 undelegated_conv=0
conv1d stride=2  (unchanged baseline)            delegates=1 undelegated_conv=0
conv3d stride=(2,)                               delegates=0 undelegated_conv=1

The conv1d and plain conv2d rows are there to show the unsqueeze path and the ordinary 2d path are untouched.

Because Conv1dUnsqueezePass now resolves the weight node before deciding, I also ran the same check through XNNPACKQuantizer plus prepare_pt2e and convert_pt2e, to exercise the dequant-unwrap branch. All four still fully delegate on this branch:

q conv1d baseline        delegates=1 undelegated_conv=0
q conv2d stride=[2]      delegates=1 undelegated_conv=0
q conv2d baseline        delegates=1 undelegated_conv=0
q conv1d per-channel     delegates=1 undelegated_conv=0

And because _get_act_deps changed how it decides 1d versus 2d, I re-ran the even-kernel same-padding cases that exercise the constant_pad_nd fusion. The pad and the conv are still both absorbed into one delegate:

qs8 conv2d even-kernel same-padding      delegates=1 conv=0 pad=0
qs8 conv1d even-kernel same-padding k=4  delegates=1 conv=0 pad=0
qs8 conv1d same-padding k=4 dilation=2   delegates=1 conv=0 pad=0

This PR was authored with AI assistance using Claude Code.

cc @GregoryComer @digantdesai @cbilgin @JakeStevens

ATen lets stride, padding and dilation be a single value that is broadcast
over every spatial dim, so len(stride) is not the conv dimensionality. A
Conv2d built with stride=[2] was routed into Conv1dUnsqueezePass, which
unsqueezed a 4d weight to 5d and failed in the fake tensor conv rule, and a
Conv3d built with stride=[2] slipped past the partitioner guard that only
supports 1d and 2d.

Derive the dimensionality from the weight rank instead, and normalize stride
and dilation in the conv2d node visitor the way padding and output_padding
already are.

Fixes pytorch#10965

Signed-off-by: Aditya Singh <adisin650@gmail.com>
@pytorch-bot

pytorch-bot Bot commented Aug 5, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21588

Note: Links to docs will display an error until the docs builds have been completed.

❌ 1 Cancelled Job

As of commit 775b070 with merge base efd6b55 (image):

CANCELLED JOB - The following job was cancelled. Please retry:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 5, 2026
@nil-is-all nil-is-all added the module: xnnpack Issues related to xnnpack delegation and the code under backends/xnnpack/ label Aug 5, 2026
@nil-is-all nil-is-all added the release notes: xnnpack Changes to the XNNPack backend delegate label Aug 5, 2026
@nil-is-all

Copy link
Copy Markdown
Contributor

Thanks for putting up the PR! Running CI now.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. module: xnnpack Issues related to xnnpack delegation and the code under backends/xnnpack/ release notes: xnnpack Changes to the XNNPack backend delegate

Projects

None yet

Development

Successfully merging this pull request may close these issues.

XNN Conv2d won't lower with single stride/dilation/etc.

3 participants