Skip to content

Commit 69ae8c5

Browse files
authored
Support output_padding in XNNPACK transposed convolution (pytorch#18185) (pytorch#18185)
Summary: XNNPACK got support for transposed convolutions with output padding at some point. Wire it up through ET. Differential Revision: D96603677
1 parent f9aca01 commit 69ae8c5

3 files changed

Lines changed: 181 additions & 38 deletions

File tree

backends/xnnpack/operators/op_conv2d.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -140,14 +140,11 @@ def define_node(
140140
stride = cast(List[int], node.args[3])
141141
padding = cast(List[int], node.args[4])
142142
dilation = cast(List[int], node.args[5])
143+
output_padding = cast(List[int], node.args[7])
143144
if len(padding) == 1:
144145
padding = padding + padding
145-
146-
# args[7] = output padding
147-
check_or_raise(
148-
all(out_pad == 0 for out_pad in cast(List[int], node.args[7])),
149-
"XNNPACK does not support output padding",
150-
)
146+
if len(output_padding) == 1:
147+
output_padding = output_padding + output_padding
151148

152149
check_or_raise(
153150
len(stride) == 2, "XNNPACK currently only supports 2D convolution"
@@ -165,8 +162,8 @@ def define_node(
165162
kwargs["group_input_channels"] = group_input_channels
166163
kwargs["group_output_channels"] = group_output_channels
167164
kwargs["groups"] = groups
168-
kwargs["adjustment_height"] = 0
169-
kwargs["adjustment_width"] = 0
165+
kwargs["adjustment_height"] = output_padding[0]
166+
kwargs["adjustment_width"] = output_padding[1]
170167
kwargs["flags"] = 0
171168

172169
if is_depthwise_conv:

backends/xnnpack/partition/config/gemm_configs.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -382,18 +382,6 @@ def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
382382
)
383383
return False
384384

385-
# XNNPACK does not support non-zero output padding in transposed
386-
# convolutions.
387-
if is_transpose and any(
388-
out_pad != 0 for out_pad in cast(List[int], node.args[7])
389-
):
390-
why(
391-
node,
392-
"XNNPACK does not support transposed convolutions with"
393-
"non-zero output padding",
394-
)
395-
return False
396-
397385
if (
398386
is_transpose
399387
and weight_quant_params is not None

backends/xnnpack/test/ops/test_conv2d.py

Lines changed: 176 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -656,17 +656,20 @@ def get_inputs(self):
656656
conv_count=1,
657657
)
658658

659-
def test_padded_output_tconv(self):
660-
class TConv2d(torch.nn.Module):
661-
def __init__(self):
659+
def test_fp32_tconv_output_padding(self):
660+
"""Test transposed convolution with non-zero output padding."""
661+
662+
class TConv2dOutputPadding(torch.nn.Module):
663+
def __init__(self, output_padding):
662664
super().__init__()
665+
self.transpose = True
663666
self.conv = torch.nn.ConvTranspose2d(
664667
in_channels=2,
665668
out_channels=1,
666669
kernel_size=(3, 3),
667670
stride=(2, 2),
668671
padding=(1, 1),
669-
output_padding=(0, 1),
672+
output_padding=output_padding,
670673
dilation=(1, 1),
671674
groups=1,
672675
bias=True,
@@ -675,26 +678,181 @@ def __init__(self):
675678
def forward(self, x):
676679
return self.conv(x)
677680

678-
m = TConv2d()
679-
inputs = (torch.randn(1, 2, 8, 8),)
680-
tester = Tester(m.eval(), inputs)
681+
def get_inputs(self):
682+
return (torch.randn(1, 2, 8, 8),)
681683

682-
conv_count: int = 1
683-
op = "torch.ops.aten.conv_transpose2d"
684+
# Test asymmetric output padding (0, 1)
685+
self._test(TConv2dOutputPadding(output_padding=(0, 1)))
684686

685-
(tester.export().check_count({op: conv_count}).to_edge_transform_and_lower())
687+
# Test symmetric output padding (1, 1)
688+
self._test(TConv2dOutputPadding(output_padding=(1, 1)))
686689

687-
# tconv should not be offloaded to XNNPack, since output padding is not supported
688-
(
689-
tester.check(
690-
["executorch_exir_dialects_edge__ops_aten_convolution_default"]
690+
def test_qs8_tconv_output_padding(self):
691+
"""Test quantized transposed convolution with non-zero output padding."""
692+
693+
class TConv2dOutputPadding(torch.nn.Module):
694+
def __init__(self, output_padding):
695+
super().__init__()
696+
self.transpose = True
697+
self.conv = torch.nn.ConvTranspose2d(
698+
in_channels=2,
699+
out_channels=1,
700+
kernel_size=(3, 3),
701+
stride=(2, 2),
702+
padding=(1, 1),
703+
output_padding=output_padding,
704+
dilation=(1, 1),
705+
groups=1,
706+
bias=True,
707+
).to(torch.float)
708+
709+
def forward(self, x):
710+
return self.conv(x)
711+
712+
def get_inputs(self):
713+
return (torch.randn(1, 2, 8, 8),)
714+
715+
# Test asymmetric output padding (0, 1) with quantization
716+
self._test(
717+
TConv2dOutputPadding(output_padding=(0, 1)),
718+
quant_config=get_symmetric_quantization_config(),
719+
)
720+
721+
# Test symmetric output padding (1, 1) with quantization
722+
self._test(
723+
TConv2dOutputPadding(output_padding=(1, 1)),
724+
quant_config=get_symmetric_quantization_config(),
725+
)
726+
727+
def test_fp32_tconv_output_padding_large_stride(self):
728+
"""Test transposed convolution with larger output padding and stride values."""
729+
730+
class TConv2dLargeOutputPadding(torch.nn.Module):
731+
def __init__(self, stride, output_padding):
732+
super().__init__()
733+
self.transpose = True
734+
self.conv = torch.nn.ConvTranspose2d(
735+
in_channels=8,
736+
out_channels=16,
737+
kernel_size=(5, 5),
738+
stride=stride,
739+
padding=(2, 2),
740+
output_padding=output_padding,
741+
dilation=(1, 1),
742+
groups=1,
743+
bias=True,
744+
).to(torch.float)
745+
746+
def forward(self, x):
747+
return self.conv(x)
748+
749+
def get_inputs(self):
750+
return (torch.randn(2, 8, 16, 16),)
751+
752+
# Test with stride=4 and output_padding=(3, 3) - maximum valid for stride 4
753+
self._test(TConv2dLargeOutputPadding(stride=(4, 4), output_padding=(3, 3)))
754+
755+
# Test with stride=3 and asymmetric output_padding=(2, 1)
756+
self._test(TConv2dLargeOutputPadding(stride=(3, 3), output_padding=(2, 1)))
757+
758+
# Test with asymmetric stride and output_padding
759+
self._test(TConv2dLargeOutputPadding(stride=(4, 3), output_padding=(3, 2)))
760+
761+
def test_fp32_tconv_output_padding_various_shapes(self):
762+
"""Test transposed convolution with output padding on various input shapes."""
763+
764+
class TConv2dVariousShapes(torch.nn.Module):
765+
def __init__(
766+
self,
767+
in_channels,
768+
out_channels,
769+
kernel_size,
770+
stride,
771+
padding,
772+
output_padding,
773+
height,
774+
width,
775+
):
776+
super().__init__()
777+
self.transpose = True
778+
self.height = height
779+
self.width = width
780+
self.in_channels = in_channels
781+
self.conv = torch.nn.ConvTranspose2d(
782+
in_channels=in_channels,
783+
out_channels=out_channels,
784+
kernel_size=kernel_size,
785+
stride=stride,
786+
padding=padding,
787+
output_padding=output_padding,
788+
dilation=(1, 1),
789+
groups=1,
790+
bias=True,
791+
).to(torch.float)
792+
793+
def forward(self, x):
794+
return self.conv(x)
795+
796+
def get_inputs(self):
797+
return (torch.randn(1, self.in_channels, self.height, self.width),)
798+
799+
# Test with larger kernel (7x7), stride=2, output_padding=1
800+
self._test(
801+
TConv2dVariousShapes(
802+
in_channels=3,
803+
out_channels=32,
804+
kernel_size=(7, 7),
805+
stride=(2, 2),
806+
padding=(3, 3),
807+
output_padding=(1, 1),
808+
height=32,
809+
width=32,
691810
)
692-
.check_not(["torch.ops.higher_order.executorch_call_delegate"])
693-
.to_executorch()
694-
.serialize()
695-
.run_method_and_compare_outputs(qtol=1)
696811
)
697812

813+
# Test with rectangular kernel and asymmetric output padding
814+
self._test(
815+
TConv2dVariousShapes(
816+
in_channels=16,
817+
out_channels=8,
818+
kernel_size=(3, 5),
819+
stride=(2, 3),
820+
padding=(1, 2),
821+
output_padding=(1, 2),
822+
height=24,
823+
width=32,
824+
)
825+
)
826+
827+
# Test with small spatial dimensions but larger output padding
828+
self._test(
829+
TConv2dVariousShapes(
830+
in_channels=4,
831+
out_channels=4,
832+
kernel_size=(4, 4),
833+
stride=(4, 4),
834+
padding=(0, 0),
835+
output_padding=(3, 3),
836+
height=4,
837+
width=4,
838+
)
839+
)
840+
841+
# Test with batch size > 1 and asymmetric everything
842+
model = TConv2dVariousShapes(
843+
in_channels=6,
844+
out_channels=12,
845+
kernel_size=(5, 3),
846+
stride=(3, 2),
847+
padding=(2, 1),
848+
output_padding=(2, 1),
849+
height=20,
850+
width=15,
851+
)
852+
# Override get_inputs to use larger batch
853+
model.get_inputs = lambda: (torch.randn(4, 6, 20, 15),)
854+
self._test(model)
855+
698856
def test_dq_conv2d(self) -> None:
699857
model = Conv2d(
700858
in_channels=3,

0 commit comments

Comments
 (0)