Skip to content

Commit c46dc27

Browse files
Allow transposed convolution weights with a non-default dim order (#21035)
### Summary Fixes #20804. A transposed convolution whose weight has `out_channels == 1` fails in the portable kernel with `Check failed (tensor_is_default_or_channels_last_dim_order(weight))` (status 18). `check_convolution_args` runs `tensor_is_default_or_channels_last_dim_order(weight)` unconditionally, but a transposed conv weight is laid out as `(in_channels, out_channels / groups, kH, kW)`, so when `out_channels == 1` the weight comes through with a dim order such as `[1, 0, 2, 3]`, which is neither contiguous (`[0,1,2,3]`) nor channels-last. For `out_channels > 1` the dim order is `[0,1,2,3]` and the check passes, which matches the report. The transposed path in `op_convolution.cpp` indexes the weight through strides derived from its dim order (`dim_order_to_stride_nocheck` then `calculate_linear_index`), so it reads the correct values regardless of whether the weight is in `[0,1,2,3]` or `[1,0,2,3]`. I checked this with the exact stride/index logic: every weight lookup matches between the two layouts, so the kernel already handles this weight and only the up-front check needs to change. This skips the weight dim-order check when `transposed` is true, since the kernel does not rely on that layout, rather than reordering the weight upstream. Happy to switch to normalizing the weight instead if that's preferred. ### Test plan Added `TransposedWeightNonDefaultDimOrder` in `kernels/test/op_convolution_test.cpp`: a transposed conv with a `(1, 1, 2, 2)` weight built in dim order `[1, 0, 2, 3]`, checked against a reference output computed with `torch.nn.ConvTranspose2d`. The test fails before this change (the weight is rejected) and passes after it. cc @larryliu0820 @manuelcandales
1 parent 9eb7b65 commit c46dc27

2 files changed

Lines changed: 49 additions & 2 deletions

File tree

kernels/portable/cpu/util/kernel_ops_util.cpp

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -382,8 +382,13 @@ bool check_convolution_args(
382382
ET_LOG_AND_RETURN_IF_FALSE(tensors_have_same_dtype(in, weight, out));
383383

384384
ET_LOG_AND_RETURN_IF_FALSE(tensor_is_default_or_channels_last_dim_order(in));
385-
ET_LOG_AND_RETURN_IF_FALSE(
386-
tensor_is_default_or_channels_last_dim_order(weight));
385+
// Transposed weights can have a non-default dim order (e.g. out_channels == 1
386+
// gives [1, 0, 2, 3]); the kernel indexes them by their strides, so any order
387+
// is valid here.
388+
if (!transposed) {
389+
ET_LOG_AND_RETURN_IF_FALSE(
390+
tensor_is_default_or_channels_last_dim_order(weight));
391+
}
387392
ET_LOG_AND_RETURN_IF_FALSE(tensor_is_default_or_channels_last_dim_order(out));
388393

389394
ET_CHECK_OR_RETURN_FALSE(

kernels/test/op_convolution_test.cpp

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -785,3 +785,45 @@ TEST_F(OpConvCorrectnessTest, BFloat16TypeSmokeTest) {
785785
out);
786786
EXPECT_TENSOR_CLOSE(out, expected);
787787
}
788+
789+
// Regression test for #20804: a transposed convolution weight with
790+
// out_channels == 1 arrives with a non-default dim order (e.g. [1, 0, 2, 3],
791+
// since the weight is laid out as (in_channels, out_channels / groups, kH,
792+
// kW)). The kernel indexes the weight through its strides, so it must run and
793+
// produce the same result as a default-order weight.
794+
TEST_F(OpConvCorrectnessTest, TransposedWeightNonDefaultDimOrder) {
795+
TensorFactory<ScalarType::Float> tf;
796+
797+
Tensor input = tf.make({1, 2, 2, 2}, {1, 2, 3, 4, 5, 6, 7, 8});
798+
799+
// Weight (in_channels=2, out_channels=1, kH=2, kW=2)
800+
// in dim order [1, 0, 2, 3].
801+
Tensor weight = tf.make_with_dimorder(
802+
{2, 1, 2, 2}, {1, 2, 3, 4, 5, 6, 7, 8}, /*dim_order=*/{1, 0, 2, 3});
803+
804+
optional<Tensor> bias;
805+
Tensor out = tf.zeros({1, 1, 3, 3});
806+
Tensor expected =
807+
tf.make({1, 1, 3, 3}, {26, 64, 40, 76, 184, 112, 58, 136, 80});
808+
809+
int64_t stride[2] = {1, 1};
810+
int64_t padding[2] = {0, 0};
811+
int64_t dilation[2] = {1, 1};
812+
bool transposed = true;
813+
int64_t output_padding[2] = {0, 0};
814+
int64_t groups = 1;
815+
816+
op_convolution_out(
817+
input,
818+
weight,
819+
std::optional<Tensor>(bias),
820+
executorch::aten::ArrayRef<int64_t>{stride, 2},
821+
executorch::aten::ArrayRef<int64_t>{padding, 2},
822+
executorch::aten::ArrayRef<int64_t>{dilation, 2},
823+
transposed,
824+
executorch::aten::ArrayRef<int64_t>{output_padding, 2},
825+
groups,
826+
out);
827+
828+
EXPECT_TENSOR_CLOSE(out, expected);
829+
}

0 commit comments

Comments
 (0)