Skip to content

Support 3-D convolution in the portable convolution kernel - #24385

Open
shoumikhin wants to merge 22 commits into
pytorch:mainfrom
shoumikhin:portable-conv3d
Open

shoumikhin wants to merge 22 commits into
pytorch:mainfrom
shoumikhin:portable-conv3d

Conversation

@shoumikhin

@shoumikhin shoumikhin commented Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

What is wrong today

A model with torch.nn.Conv3d lowers without an error but fails at run time on CPU, for example with XNNPACK:

Check failed (in.dim() == 3 || in.dim() == 4): Expect input tensor to be 3-D or 4-D, but got, 5.

XNNPACK does not take 3-D or bfloat16 convolutions, so they run on the portable convolution kernel, which only supported 1-D and 2-D. This blocks vision encoders with a Conv3d patch embedding, such as Qwen2-VL and Qwen2.5-VL. It is the failure in #11704.

What this change does

The portable convolution kernel now runs 3-D convolutions, regular and transposed. 1-D and 2-D convolutions run as 3-D ones with extra dims of size 1 and add up their terms in the same order as before, so their results stay the same, bit for bit. The input and output may now have different dim orders, because torch.export gives a Conv3d with a channels-last input a contiguous output.

It also fixes older bugs in the argument checks, which affect 1-D and 2-D too:

  • A bias of the wrong length was read past its end (missing parentheses), and a Bool bias crashed the kernel.
  • groups = 0 divided by zero, and a huge groups could overflow in the bias check.
  • A huge padding or dilation, or an output size too large for the 32-bit tensor sizes, let a call that should fail run. Stride, padding and dilation must now fit in 32 bits.
  • Like PyTorch, the checks now reject a kernel dim of 0, a regular convolution's kernel larger than the padded input, and an empty spatial dim unless the batch or channel dim is empty too.

Testing

  • New tests cover grouped regular and transposed 3-D convolutions with contiguous, channels-last and mixed layouts, and the checks above. All but one fail without this change; that one checks that an empty batch still works.
  • The portable kernel tests pass with AddressSanitizer and UndefinedBehaviorSanitizer on Linux Arm64.
  • The Cortex-M conv3d test, now with several channels, a 2x3x3 kernel, stride and padding, no longer expects a failure and passes in CI on the Arm simulator.
  • A Conv3d patch embedding lowered with XNNPACK, and random convolutions on the portable kernel, match eager PyTorch in float32 and bfloat16.
  • Speed against main on Arm64 and x86-64: regular 1-D and 2-D convolutions are 1.5 to 13 times faster with GCC and with Clang at -Os, and 18 percent faster to 11 percent slower with Clang at -O2 and -O3. Transposed ones are 5 percent faster to 17 percent slower.
  • op_convolution.o for a Cortex-M55 (Arm GNU toolchain 15.2) grows from 32,484 to 33,263 bytes at -Os, and from 48,477 to 61,457 at -O3.

Notes

check_convolution_args meant to compare the bias length with the number
of output channels, but the conditional operator binds less tightly
than ==, so

    bias.size(0) == transposed ? groups * weight.size(1) : weight.size(0)

compared the length with transposed and then used a channel count as the
condition. The check passed for any weight with channels, so a bias
shorter than the number of output channels got through, and the kernel
read past its end.

Add the parentheses. The new test gives a regular and a transposed
convolution a bias of the wrong length and expects both to fail.
check_convolution_args computed weight.size(0) % groups without
rejecting groups = 0 first, so it divided by zero, which crashes on
x86. Reject groups below 1, as PyTorch does. The new test passes
groups = 0 and expects the call to fail.
A program with an nn.Conv3d could be lowered with the XNNPACK
partitioner and saved, but then failed at run time:

    Check failed (in.dim() == 3 || in.dim() == 4): Expect input tensor
    to be 3-D or 4-D, but got, 5.

XNNPACK does not take convolutions with more than 2 spatial dims, or any
bfloat16 convolution, so they run on the portable convolution kernel,
which only handled 1-D and 2-D. Vision encoders that embed image patches
with a Conv3d, such as Qwen2.5-VL's, hit this.

The kernel now runs every convolution as a 3-D one. It views a 1-D or
2-D convolution as 3-D, with leading spatial dims of size 1, stride 1,
padding 0 and dilation 1, the same way it used to view 1-D as 2-D.

- The regular loop adds up the depth taps outside the channel loop, so a
  depth of 1 adds no work per channel. 1-D and 2-D convolutions have one
  depth tap, so they add up their terms in the same order as before and
  give the same results. Its innermost loop steps the indices by the
  strides, and it reads the bias once per output channel of each batch
  item instead of once per output value.
- The transposed loop handles one output depth at a time.
- The argument checks and the output size take the number of spatial
  dims from the weight, so 3-D arguments get the same checks as 1-D and
  2-D ones. get_unsqueezed_sizes and get_unsqueezed_dim_order are no
  longer used and are removed.
- The Cortex-M Conv3d test no longer expects a failure.

Tests: a grouped 3-D convolution and a grouped transposed 3-D
convolution match PyTorch. Depth has its own stride, padding and
dilation in both, and the input and weight values do not repeat along
any dim, so mixing up dims or indices changes the result. Both fail
without this change. The invalid input test also checks that a 3-D
convolution rejects a stride, padding or dilation list of the wrong
length, and that a 6-D input is rejected.
@shoumikhin shoumikhin added the release notes: ops & kernels Changes to the opset and any new / changed kernel implementations label Oct 31, 2026
@shoumikhin
shoumikhin requested a review from rascani as a code owner October 31, 2026 13:41
@shoumikhin shoumikhin added the release notes: ops & kernels Changes to the opset and any new / changed kernel implementations label Oct 31, 2026
Copilot AI balanced review requested due to automatic review settings October 31, 2026 13:41
@pytorch-bot

pytorch-bot Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

✅ No Failures

As of commit 4631d0d with merge base aed328d (image):
💚 Looks good so far! There are no failures yet. 💚

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 Oct 31, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 32m 34s —— View job


PR Review: #24385

Needs Discussion: the PR removes two helpers from a public util target, multiplies by groups before validating it, turns a loud XNNPACK int8 Conv3d failure into silent zeros with no tracking issue, leaves gaps in the 3-D tests, and hasn't measured its code size on an MCU toolchain.

Detailed review

Summary

The PR sends 1-D, 2-D and the new 3-D convolutions through a single 3-D implementation in the portable convolution.out kernel. Along the way it fixes the bias-length check and rejects groups < 1. The reviewers found no indexing bugs in the new loops. The problems are a public-helper removal, an overflow caused by check ordering, an untracked int8 regression, two test gaps, and an MCU size cost nobody has measured.

Code Quality

  • Known int8 regression has no tracking. Accepting 5-D inputs (kernels/portable/cpu/util/kernel_ops_util.cpp:421-424) means a static int8 Conv3d lowered with XNNPACK no longer fails with an error. Per the Notes, the XNNPACK quantize and dequantize partitions next to the undelegated Conv3d now return zeros, and a silent wrong result is worse than the old error. Please file an issue, link it here, and mention the change in the release note (the PR is labeled release notes: ops & kernels). Alternatively, decide whether this PR should wait for the XNNPACK fix.
  • groups is used before it is validated. The corrected bias check computes groups * weight.size(1) on every transposed call with a bias (kernel_ops_util.cpp:431-436). The new groups > 0 check and the weight.size(0) >= groups bound only run later (kernel_ops_util.cpp:454-469). A malformed program with transposed=true, a bias and groups near INT64_MAX overflows int64_t at that multiply. That is undefined behavior, and UBSan flags it, even though a later check rejects the call. Moving the bias block below the groups checks bounds groups to [1, weight.size(0)] first. Fix this →
  • Input smaller than the dilated kernel (existed before this PR, now also reachable through depth). For a non-transposed convolution with stride ≥ 2, _kernel_output_size_helper (kernel_ops_util.cpp:180-182) truncates a negative numerator toward zero and returns 1. For example, input depth 2, kernel depth 3, stride 2 and padding 0 produce an output depth of 1 from two of the three taps, where PyTorch raises an error. check_avg_pool1d_args guards against this (kernel_ops_util.cpp:305-312), but check_convolution_args doesn't. A follow-up PR is fine. Fix this →

Testing

  • No NDHWC kernel test. check_convolution_args now accepts 5-D channels-last tensors (runtime/core/exec_aten/util/dim_order_util.h:78), and the kernel indexes them by t.strides(). Both 3-D tests (kernels/test/op_convolution_test.cpp:1185-1237) use contiguous tensors only.
    • The Cortex-M conv3d case (backends/cortex_m/test/ops/test_conv.py:226-233) is NDHWC, but it has one input and one output channel, so its strides address memory exactly like contiguous ones.
    • The 2-D tests run both layouts (op_convolution_test.cpp:156, 1157).
    • tf.channels_last_like (runtime/core/exec_aten/testing_util/tensor_factory.h:894) and get_channels_last_data (op_convolution_test.cpp:801) only handle 4-D. tf.make_channels_last takes 5-D sizes (tensor_factory.h:867-873), so the 3-D tests only need their data in NDHWC order. Fix this →
  • The 3-D rejection checks have no positive control. The three ET_EXPECT_KERNEL_FAILURE(context_, conv_3d(...)) checks (op_convolution_test.cpp:670-672) also pass on main, which rejects every 5-D input at the rank check. So they don't show that the four-element arrays are what gets rejected. No 3-D test passes a single stride, padding or dilation value either, which leaves the broadcast described in the comment at lines 645-646 untested. A passing call conv_3d(one, zero, one), checked against tf.full({1, 4, 2, 2, 2}, 16), covers both. Fix this →

Backward Compatibility

  • Helpers removed from a public target. The PR deletes get_unsqueezed_sizes and get_unsqueezed_dim_order (main kernels/portable/cpu/util/kernel_ops_util.h:53-63 and kernel_ops_util.cpp:167-199).
    • The kernel_ops_util target has visibility = ["PUBLIC"] (kernels/portable/cpu/util/targets.bzl:210, 223) and is already used outside the portable kernels (Cadence, Cortex-M, optimized).
    • No in-tree callers remain, but internal and out-of-tree callers can't be ruled out, and the Notes say add forward portable Conv3d support for contiguous 5D inputs in aten:convolution.out #19977 was reverted for breaking internal jobs.
    • 3-D support doesn't need this removal. Keep the helpers in this PR and remove them in a separate one, so reverting one change doesn't undo the other. Fix this →

Performance

  • 1-D and 2-D users pay for the 3-D loops, and nobody has measured the cost on an MCU. Every 1-D and 2-D convolution now runs the 3-D loop nest.
    • The description's own numbers, all from 64-bit hosts:
      • Regular convolutions are up to 10% slower with Clang at -O2/-O3.
      • Transposed convolutions are up to 12% slower with GCC at -Os.
      • Kernel code is up to 14% larger at -Os and 55% larger at -O2/-O3.
    • MCU builds link this kernel (backends/cortex_m/test/build_test_runner.sh:48).
    • CI won't catch the growth: test-arm-cortex-m-size-test (.github/workflows/pull.yml:705) measures size_test, which links no ops (test/CMakeLists.txt:48-56).
    • Please post the -Os size change of op_convolution.o when built with arm-none-eabi, or say that the trade-off is accepted.

Recommendation

Needs Discussion

The reviewers found no blocking bug in the kernel. Three decisions are needed before merge:

The groups ordering fix and the two tests are small changes.

  • Gather context (diff, commits, CI results)
  • Run three independent reviewers
  • Consolidate findings
  • Fact-check the consolidated findings
  • Write and post the review

The bias length check of a transposed convolution multiplies groups by
weight.size(1). It ran before groups was checked, so a program with a
huge groups value overflowed int64_t there, which is undefined behavior,
even though a later check rejects the call.

Run the bias check after groups is known to be between 1 and
weight.size(0). The InvalidGroups test now also passes INT64_MAX as
groups to a transposed convolution with a bias. Without this change,
UndefinedBehaviorSanitizer reports the overflow.
The 3-D convolution change stopped using these two helpers and deleted
them. They belong to kernel_ops_util, a public library that other kernels
and backends link, so code outside this repository may still call them.
Deleting them is a separate cleanup, so restore them unchanged.
The 3-D convolution tests used contiguous tensors only, so nothing
checked that a channels-last (NDHWC) tensor with more than one channel
is read through its strides. Both 3-D tests now also run with
channels-last input, weight and output, like the 2-D tests. For this,
TensorFactory::channels_last_like accepts 5-D tensors: depth and height
are next to each other in both layouts, so a 5-D tensor is permuted like
a 4-D one whose height covers both.

The checks that reject 3-D stride, padding and dilation lists of the
wrong length also passed before 3-D support, because the rank check
rejected every 5-D input. A call with single values now runs first and
must succeed, so those checks fail only because of the list length. It
also covers single values that apply to every spatial dim.
For a regular convolution, PyTorch raises an error when the dilated
kernel is larger than the padded input in any spatial dim. The portable
kernel did not check this. Its output size math rounds a negative
quotient toward zero, so with a stride above 1 the output got size 1 and
was computed from only part of the kernel. For example, an input depth
of 2, a kernel depth of 3 and a stride of 2 gave an output depth of 1.
1-D and 2-D convolutions had the same problem.

check_convolution_args now applies the PyTorch rule. Transposed
convolutions are not affected.
GCC at -Os inlines transposed_kernel_range and transposed_input_range at
every call site, once per spatial dim in every dtype instantiation of the
transposed convolution. With the depth loop, that made op_convolution.o
14 percent larger than before 3-D support when built for a Cortex-M55
with the Arm GNU toolchain at -Os. The helpers do not depend on the
dtype, so one out-of-line copy can serve them all, and the object is
now 3 percent larger than before.

The helpers run once per output tile or kernel tap, outside the
multiply-add loops.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 19:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

…istakes

The call that checks single stride, padding and dilation values used 1,
0 and 1, which the kernel also uses for a dim that gets no value, so it
could not catch a single value that is not applied to the depth. It now
uses stride 2, padding 1 and dilation 2, and the calls that must fail use
four-element lists of the same values.

The transposed 3-D test had one output channel per group, so its weight
had the same layout in contiguous and channels-last order, and a wrong
weight stride went unnoticed. It now has two output channels per group,
with new PyTorch reference values.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 20:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Pushed:

  • groups is now checked before the bias length check uses it.
  • get_unsqueezed_sizes and get_unsqueezed_dim_order are back, unchanged.
  • The 3-D tests also run with channels-last tensors. The 3-D rejection checks now start with a call that must succeed, with single values that are not the defaults.
  • A kernel larger than the padded input is now rejected, like in PyTorch, with a test.
  • Built for a Cortex-M55 with the Arm GNU toolchain at -Os, op_convolution.o grows from 32,484 to 33,392 bytes. Keeping the transposed range helpers out of line brought the growth down from 14 to 3 percent. Speed was measured only on Arm64 and x86-64 hosts, not on an MCU. The numbers are in the description.
  • The silent wrong values from a static int8 Conv3d with XNNPACK are fixed in Keep ops above 6 dims out of XNNPACK and fix quantized ops it leaves out #24380. The description now says this change depends on it.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Oct 31, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 35m 37s —— View job


PR Review: #24385

Approve once #24380 lands: I found no bugs in the new 3-D loops, and the remaining items don't block: an overflow that gets past the new kernel-size check, an unchecked bias dtype, and two tests that check less than they appear to.

Detailed review

Summary

This PR sends 1-D, 2-D and new 3-D convolutions in portable convolution.out through one 3-D implementation and tightens the argument checks. I found no bugs in the new loops or in the 1-D/2-D bit-for-bit claim. The problems:

  • an overflow lets a huge dilation past the new kernel-size check;
  • a bias dtype check is missing, which predates this PR;
  • two tests check less than they appear to.

Code Quality

  • A huge dilation gets past the new kernel-size check (kernels/portable/cpu/util/kernel_ops_util.cpp:479-482).
    • Stride, padding and dilation have only lower bounds: param_array_is_valid (line 46), and padding_is_valid defaults to enforce_half_kernel = false (kernel_ops_util.h:45).
    • Example: a 1-D convolution with an input of 3, a kernel of 3 and dilation 2^62. In practice dilation * (kernel_size - 1) + 1 wraps to a negative number, so the check passes.
    • _kernel_output_size_helper (line 213) also wraps. Line 246 then narrows the negative size into the 32-bit SizesType as 3, and the call succeeds when it should be rejected.
    • The PR fixes this same kind of signed overflow for groups. Since the check is new, fix it in this PR: bound the parameters in check_convolution_args, for example to INT32_MAX. That covers this check and the output-size math. Fix this →
  • The bias dtype is never checked. This predates the PR, so a follow-up PR is fine.
    • check_convolution_args compares dtypes only for in, weight and out (line 442).
    • For a bias outside REALHBF16, such as Bool, get_load_to_compute_fn in convolution_out (kernels/portable/cpu/op_convolution.cpp:497-500) fails the context and returns nullptr.
    • convolution_wrapper runs anyway, and conv3d_impl calls the null load_bias (op_convolution.cpp:276-278; 183-185 when transposed). A malformed program crashes instead of getting InvalidArgument.
    • Fix: an isRealHBF16Type check in the bias block at kernel_ops_util.cpp:511-520. Fix this →

Testing

  • The INT64_MAX case in InvalidGroups can't catch the old check order in CI (kernels/test/op_convolution_test.cpp:733-751).
    • With the old order, groups * weight.size(1) = INT64_MAX * 4 wraps to −4 in practice. The call is still rejected and the test passes.
    • The Debug unittest job in .github/workflows/pull.yml does build with -fsanitize=address,undefined (CMakeLists.txt:148). Nothing sets -fno-sanitize-recover or UBSAN_OPTIONS=halt_on_error=1, so UBSan only logs the overflow.
    • Fix: say in the comment that this case needs a UBSan build that halts on errors. Fix this →
  • The Cortex-M conv3d case can't catch indexing mistakes (backends/cortex_m/test/ops/test_conv.py:226-233).
    • CortexMConv3D(1, 1, 1) is a 1×1×1 kernel on one channel, with constant weights of 2.0 (line 86). The output is just twice the input.
    • It uses no padding, stride, dilation or extra kernel taps. With one channel, channels_last_3d addresses memory the same way as contiguous.
    • The PR description cites this case passing on the Arm simulator. That is the only 32-bit run of the new 3-D code the PR reports.
    • Fix: use several channels, a kernel larger than 1 in each dim, stride, padding and non-constant weights. For example, CortexMConv3D(2, 4, (2, 3, 3), stride=(1, 2, 2), padding=1) without the fill_(2.0), on a (2, 2, 4, 6, 6) input. Fix this →

Recommendation

Approve. Merge only after #24380 lands on main, or together with it. This branch is based on aed328d and does not contain #24380. By the PR's own Notes, landing this alone turns the current error for a static int8 Conv3d under XNNPACK into wrong values. None of the findings above block.

Review steps
  • Gather context (diff, commits): 9 commits on top of aed328d, 5 files, +467/−245.
  • Run three independent reviewers.
  • Consolidate findings: 9 candidates. Six were reported by two or three reviewers, and three by one.
  • Fact-check the consolidated findings: 5 kept, 4 dropped. The merge-order finding moved into the Recommendation. Dropped:
    • the ET_NOINLINE speed trade-off, already measured and accepted;
    • 3-D test gaps, covered by the shared 1-D/2-D tests;
    • a faster Qwen2-VL path, out of scope;
    • PR-description style.
  • Write and post the review.

Not done:

Stride, padding and dilation had only lower bounds. A huge value
overflowed int64_t in the new kernel size check and in the output size
math. For example, with dilation 2^62 the dilated kernel size wrapped
around to a negative number and passed the check, and the output size
that came out was narrowed to a valid 32-bit size, so a 1-D convolution
that should fail ran.

check_convolution_args now rejects values that do not fit in 32 bits,
like the 32-bit tensor sizes, so this int64_t math cannot overflow. A new
test passes a huge dilation, padding and stride. It skips ATen, which
does not reject these values.
check_convolution_args compared the dtypes of the input, weight and
output, but never looked at the bias dtype. For a bias outside the real,
half and bfloat16 types, for example Bool, the kernel failed to get a
function that loads the bias, then ran anyway and called a null function
pointer.

Reject such a bias in the argument checks. A new test passes a Bool
bias, which crashed without this change. It skips ATen, which accepts a
Bool bias.
With the old check order, the INT64_MAX groups case still failed the
bias length check, because the overflowed product wrapped to a wrong
length. So the test only notices the overflow in an
UndefinedBehaviorSanitizer build that halts on errors. CI builds the
sanitizer without halting, so say that in the comment.
Copilot AI balanced review requested due to automatic review settings October 31, 2026 23:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

PyTorch's CPU conv3d returns a contiguous output even for a
channels_last_3d input, so the exported program gave the convolution a
channels-last input and a contiguous output. The portable kernel needs
both in the same layout, so the run on the Arm simulator failed. With
one channel the two layouts were the same, which is why the old case
passed. The C++ tests cover the kernel's channels-last 3-D path.
The 32-bit bound on stride, padding and dilation left two gaps:

- An output size above 2^31 - 1 still fit int64_t, but was then cut to
  a 32-bit tensor size. Padding 2^31 - 1 on an input of 3 with a kernel
  of 1 gives 2^32 + 1, which became 1, so a call that should fail ran.
- An empty spatial dim made the transposed output size math compute
  0 - 1 in size_t. On 32-bit targets that overflowed int64_t.

PyTorch rejects empty spatial dims, and now check_convolution_args does
too. It also computes each output size and rejects one that does not
fit in the 32-bit tensor size. The tests cover both.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 00:10

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Negative transposed-convolution output sizes can narrow to valid depths and silently produce incorrect results, while tensors without stored strides can trigger invalid memory access or crashes.

2 open issues

Review effort: Balanced

for (const auto d : c10::irange(t.dim())) {
const ssize_t view_d = d < 2 ? d : d + added_dims;
sizes[view_d] = t.size(d);
strides[view_d] = t.strides()[d];
A transposed convolution with a large padding can have a negative output
size. For example, an input of 3, a kernel of 1 and padding 2^31 - 1
give 5 - 2^32. The output size check only had an upper bound, and the
size became a positive 5 when cut to 32 bits, so the call ran. PyTorch
rejects it as too small. Now each output size must be at least 1.

The test now covers that case instead of a stride above the 32-bit
bound, which the output size check catches too, so it is renamed to
HugePaddingAndDilation.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 00:22

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Maintainers must validate the internal jobs that caused the prior Conv3d revert and coordinate landing with still-open #24380 to prevent incorrect quantized XNNPACK outputs.

1 open issue
1 resolved since last review

Review effort: Balanced

With a kernel of 3, dilation 2^62 gave a negative output size once the
bound was removed, so the new check on output sizes rejected the call
too, and the test passed without the bound. With a kernel of 5 the
dilated kernel size wraps to exactly 1 and the output size to a valid 3,
so only the bound rejects the call.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 00:27

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

Internal-job validation remains unavailable after the prior Conv3d regression-driven revert, and merge ordering with #24380 must be coordinated to prevent silent quantized XNNPACK output corruption.

1 open issue

Review effort: Balanced

PyTorch's CPU conv3d returns a contiguous output even for a
channels_last_3d input, so an exported Conv3d with a channels-last input
gives the kernel a channels-last input and a contiguous output. The
kernel rejected that pair, so such a model failed at run time.

The kernel reads the input, weight and output through their own
strides, and check_convolution_args already limits the input, the output
and a non-transposed weight to the contiguous or the channels-last dim
order, so the input and output do not need to match. Drop that check.
Both 3-D tests now also run a channels-last input and weight with a
contiguous output, and the Cortex-M conv3d test uses a channels_last_3d
input again.
check_convolution_args read the kernel size from the weight but never
checked it. With a kernel dim of 0, the dilated kernel size became
1 - dilation, so even the kernel size check passed, and an input of 3
gave an output of 4 that no kernel tap ever wrote. PyTorch rejects a
kernel size of 0. Check it with kernel_size_is_valid, like the pooling
checks do.
The new check for empty spatial dims also rejected inputs whose batch
or channel dim is empty, which main and PyTorch accept and turn into an
empty output. Allow those again, like PyTorch. The transposed output
size math now subtracts 1 from the input size in int64_t instead of
size_t, so an empty spatial dim cannot make it wrap on 32-bit targets.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 03:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

With a channels_last_3d input the case now ran on the Arm simulator but
gave wrong values. The Arm test runner writes an input in its logical
order unless its dim order is NHWC or the runner's own 5-D order, so a
channels_last_3d input reached the program in the wrong byte order. The
kernel handles a channels-last input with a contiguous output, as the
C++ 3-D tests show, so give the Cortex-M case a contiguous input, with a
comment that says why.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 04:57

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

The kernel size check fixes 1-D and 2-D convolutions as well, but its
test used only a 5-D input, which main rejected at the rank check
anyway. Add a 1-D case: an input of 2 with a kernel of 3 and stride 2,
which main ran from two of the three kernel taps.
Copilot AI balanced review requested due to automatic review settings November 1, 2026 05:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota. Try requesting a review at a lower effort level.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

Thanks. Pushed:

  • Stride, padding and dilation must now fit in 32 bits, and each output size must be between 1 and the largest 32-bit size before it is narrowed. Like PyTorch, an empty spatial dim is rejected unless the batch or channel dim is empty too, and so is a kernel dim of 0. The tests cover a huge dilation, output sizes that would wrap in both directions, a kernel dim of 0, an empty spatial dim, and an empty batch with an empty spatial dim.
  • The bias dtype is checked now. A Bool bias fails the call instead of reaching a null function pointer. New test.
  • The InvalidGroups comment now says that only an UndefinedBehaviorSanitizer build that halts on errors catches the INT64_MAX case when the checks run in the wrong order.
  • The Cortex-M conv3d case now has 2 input and 4 output channels, a 2x3x3 kernel, stride (1, 2, 2), padding 1 and random weights, and passes on the Arm simulator in CI. Its input is contiguous, because the Arm test runner writes a channels_last_3d input in the wrong byte order. The kernel itself now accepts a channels-last input with a contiguous output, which is what torch.export gives a Conv3d, and the C++ 3-D tests cover that pair.

@shoumikhin

Copy link
Copy Markdown
Contributor Author

@claude /pr-review

@claude

claude Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Claude finished @shoumikhin's task in 23m 45s —— View job


PR Review: #24385

Approve once #24380 lands: the reviewers found no bugs in the 3-D loops or the new checks. The only remaining finding is a minor test gap caused by an Arm test-runner bug that has no issue filed.

Detailed review

Summary

This PR runs 1-D, 2-D and new 3-D convolutions in portable convolution.out through one 3-D implementation and tightens the argument checks. Three reviewers found no bugs in the new loops, in the new checks, or in the claim that 1-D/2-D results stay the same bit for bit. One minor test gap remains.

Testing

  • The Arm runner bug that forced a contiguous input on the Cortex-M conv3d case has no issue (backends/cortex_m/test/ops/test_conv.py:225-231).
    • The comment is right:
      • save_bytes (backends/arm/test/runner_utils.py:761-764) gets the bytes from prep_data_for_save, which calls torch_tensor_to_numpy (runner_utils.py:253-258).
      • torch_tensor_to_numpy only permutes NHWC_ORDER and NNHWC_ORDER, which is (0, 1, 3, 4, 2) (backends/arm/constants.py:33-36).
      • So a channels_last_3d input, with dim order (0, 2, 3, 4, 1), is written in NCDHW byte order, and any Arm or Cortex-M test with such an input gets wrong values.
    • Please file an issue and link it from that comment, so the case can switch back to channels_last_3d once the runner is fixed.
    • Until then, no test covers a channels-last input with a contiguous weight. That is what export gives when only the input is channels-last, as in the old Cortex-M case. The two "Export gives..." calls in kernels/test/op_convolution_test.cpp (lines 1402-1415 and 1452-1465) pass a channels-last weight. Running them with the contiguous weight as well would cover this layout on the host. Fix this →

Recommendation

Approve. Merge only after #24380 lands on main, or together with it. The PR's Notes say that landing this change alone makes a static int8 Conv3d under XNNPACK return wrong values instead of today's error. The finding above does not block the merge.

Review steps
  • Gather context: 22 commits, 5 files, +678/−257.
  • Run three independent reviewers. Each one read all five files.
  • Consolidate findings: 11 candidates. Seven were reported by two or three reviewers, and four by one.
  • Fact-check the consolidated findings: 1 kept, which combines the runner bug with the contiguous-weight gap. The merge order moved into the Recommendation. Dropped:
    • The new output-size check in check_convolution_args repeats the math in calculate_kernel_output_sizes. That function narrows to 32 bits, so the check needs its own 64-bit copy. The two can't disagree, because convolution doesn't accept an empty stride.
    • Other mixed-layout gaps, such as 4-D. The 5-D mixed calls already fail if the dim-order check comes back, and the kernel reads each tensor through its own strides.
    • The 1-D/2-D speed and size cost. It is measured and reported in the description. The suggested cause was not measured.
    • The INT64_MAX case in InvalidGroups. Its comment now says which build catches it, as the last review asked.
    • Integer convolutions accumulate in the input dtype. This predates the PR and applies to every rank.
    • check_convolution_args now accepts 5-D for every caller. The only other in-tree caller, the backward kernel, checks weight.dim() == 4 first.
    • Shorter stride lists for 3-D are not tested. param_array_is_valid rejects every length other than 1 and the kernel rank.
    • Batch, tile-edge, stride, dtype and bias gaps in the 3-D and Cortex-M tests. The 1-D/2-D host tests run the same 3-D code.
  • Write and post the review.

Not done:

This branch has not been deployed

No deployments
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. release notes: ops & kernels Changes to the opset and any new / changed kernel implementations

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants