Skip to content

Commit f6cef83

Browse files
authored
NXP backend: added support for same/valid padding for conv2d (pytorch#20957)
### Summary Added support for `same`/`valid/` padding for `conv2d`. ### Test plan tests can be manually run using `pytest -c /dev/null backends/nxp/tests/` cc @robert-kalmar @JakeStevens @digantdesai @rascani @MartinPavella
1 parent de5f619 commit f6cef83

6 files changed

Lines changed: 130 additions & 84 deletions

File tree

backends/nxp/backend/edge_program_converter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
exir_ops.edge.dim_order_ops._clone_dim_order.default: CloneConverter, # noqa F405
4242
exir_ops.edge.aten.constant_pad_nd.default: ConstantPadNDConverter, # noqa F405
4343
exir_ops.edge.aten.convolution.default: ConvolutionConverter, # noqa F405
44+
exir_ops.edge.aten.convolution.out: ConvolutionConverter, # noqa F405
4445
exir_ops.edge.aten.exp.default: ExpConverter, # noqa F405
4546
exir_ops.edge.aten.hardtanh.default: HardTanhConverter, # noqa F405
4647
exir_ops.edge.aten.leaky_relu.default: LeakyReluConverter, # noqa F405

backends/nxp/backend/ir/converter/node_converters/ops_converters/convolution_converter.py

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -241,17 +241,15 @@ def _is_supported_in_IR(
241241
) -> bool:
242242
input_tensor_rank = len(node.meta["val"].shape)
243243
dimensions = input_tensor_rank - 2
244-
is_transposed = node.args[6]
245-
output_padding = node.args[7]
246-
groups = node.args[8]
244+
conv_params = ConvolutionConverter._get_conv_params(node)
247245

248-
if is_transposed and conv_utils.group_conv_convertible_as_depthwise(
249-
node, groups
246+
if conv_params.transposed and conv_utils.group_conv_convertible_as_depthwise(
247+
node, conv_params.groups
250248
):
251249
# TFLite does not support transposed depthwise convolution
252250
return False
253251

254-
if not is_transposed and output_padding != [0] * dimensions:
252+
if not conv_params.transposed and conv_params.out_padding != [0] * dimensions:
255253
return False
256254

257255
if input_tensor_safe(node, 2) is None:
@@ -284,14 +282,21 @@ def _compute_slicing_params(
284282
def _get_conv_params(
285283
conv_node: Node,
286284
) -> ConvParameters:
285+
def _normalize_ls_arg(ls):
286+
# sometimes, `conv2d` args can be a list of one element. In such case, convert it to 2d arg
287+
# example: padding = [0] => [0, 0]
288+
return [ls[0], ls[0]] if len(ls) == 1 else ls
289+
287290
x, w, b, stride, padding, dilation, transposed, out_padding, groups = (
288291
conv_node.args
289292
)
290293

291-
stride = list(stride)
292-
padding = list(padding)
293-
dilation = list(dilation)
294-
out_padding = None if out_padding is None else list(out_padding)
294+
stride = _normalize_ls_arg(list(stride))
295+
padding = _normalize_ls_arg(list(padding))
296+
dilation = _normalize_ls_arg(list(dilation))
297+
out_padding = (
298+
None if out_padding is None else _normalize_ls_arg(list(out_padding))
299+
)
295300

296301
return ConvParameters(
297302
x, w, b, stride, padding, dilation, transposed, out_padding, groups

backends/nxp/neutron_partitioner.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,7 @@ def tag_qdq_clusters(self, nodes: list[torch.fx.Node]):
214214
exir_ops.edge.dim_order_ops._clone_dim_order.default: CloneConverter, # noqa F405
215215
exir_ops.edge.aten.constant_pad_nd.default: ConstantPadNDConverter, # noqa F405
216216
exir_ops.edge.aten.convolution.default: ConvolutionConverter, # noqa F405
217+
exir_ops.edge.aten.convolution.out: ConvolutionConverter, # noqa F405
217218
exir_ops.edge.aten.exp.default: ExpConverter, # noqa F405
218219
exir_ops.edge.aten.hardtanh.default: HardTanhConverter, # noqa F405
219220
exir_ops.edge.aten.leaky_relu.default: LeakyReluConverter, # noqa F405

backends/nxp/quantizer/neutron_quantizer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
CatPattern,
2626
ClampPattern,
2727
Conv2dPattern,
28+
Conv2dPatternPadding,
2829
ConvTranspose2dPattern,
2930
DropoutPattern,
3031
ExpPattern,
@@ -275,6 +276,7 @@ def __init__(self, neutron_target_spec: NeutronTargetSpec, is_qat: bool = False)
275276
OpQuantizer(CatPattern(is_qat=is_qat), static_qconfig),
276277
OpQuantizer(ClampPattern(self, is_qat=is_qat), static_qconfig),
277278
OpQuantizer(Conv2dPattern(self, is_qat=is_qat), static_qconfig),
279+
OpQuantizer(Conv2dPatternPadding(self, is_qat=is_qat), static_qconfig),
278280
OpQuantizer(
279281
ConvTranspose2dPattern(self, is_qat=is_qat), static_qconfig
280282
),

backends/nxp/quantizer/patterns.py

Lines changed: 12 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -537,65 +537,6 @@ def _is_batch_norm(node_: Node) -> bool:
537537

538538

539539
class ConvPattern(QuantizationPattern):
540-
@abstractmethod
541-
def partition_types(self) -> list[OpOverload]:
542-
pass
543-
544-
def get_anchors(
545-
self, gm: fx.GraphModule, fused_partition: list[fx.GraphModule]
546-
) -> PartitionAnchors:
547-
conv_node = fused_partition[0].nodes[-1]
548-
549-
bias_quantization_qspec = DerivedQuantizationSpec(
550-
derived_from=[
551-
(conv_node.args[0], conv_node),
552-
(conv_node.args[1], conv_node),
553-
],
554-
derive_qparams_fn=get_bias_qparams,
555-
dtype=torch.int32,
556-
quant_min=-(2**31) + 1,
557-
quant_max=2**31 - 1,
558-
qscheme=torch.per_channel_symmetric,
559-
ch_axis=0,
560-
)
561-
562-
weight_observer_or_fake_quant_ctr = (
563-
FakeQuantize.with_args(observer=MovingAveragePerChannelMinMaxObserver)
564-
if self.is_qat
565-
else PerChannelMinMaxObserver
566-
)
567-
weight_quantization_spec = QuantizationSpec(
568-
dtype=torch.int8,
569-
observer_or_fake_quant_ctr=weight_observer_or_fake_quant_ctr,
570-
quant_min=-127,
571-
quant_max=127,
572-
qscheme=torch.per_channel_symmetric,
573-
ch_axis=0,
574-
)
575-
576-
# Keep bias empty if not supplied
577-
bias = []
578-
if len(conv_node.args) > 2 and conv_node.args[2] is not None:
579-
bias = [(conv_node, NodeArgsIdx(2), bias_quantization_qspec)]
580-
581-
output_specs = [(conv_node,)]
582-
# In order for QAT to be numerically correct, there should be no quantization between
583-
# convolution node and batch norm node.
584-
if self.is_qat:
585-
conv_users = conv_node.users
586-
possibly_bn = list(conv_users.keys())[0] if len(conv_users) == 1 else None
587-
if possibly_bn and _is_batch_norm(possibly_bn):
588-
output_specs = []
589-
590-
return PartitionAnchors(
591-
inputs=[(conv_node, NodeArgsIdx(0))],
592-
weights=[(conv_node, NodeArgsIdx(1), weight_quantization_spec)],
593-
biases=bias,
594-
output=output_specs,
595-
)
596-
597-
598-
class Conv2dPattern(ConvPattern):
599540
def __init__(self, neutron_quantizer, is_qat: bool = False):
600541
super().__init__(is_qat=is_qat)
601542

@@ -604,8 +545,9 @@ def __init__(self, neutron_quantizer, is_qat: bool = False):
604545
self.neutron_quantizer.neutron_target_spec.neutron_target_info
605546
)
606547

548+
@abstractmethod
607549
def partition_types(self) -> list[OpOverload]:
608-
return [torch.ops.aten.conv2d.default]
550+
pass
609551

610552
def get_anchors(
611553
self, gm: fx.GraphModule, fused_partition: list[fx.GraphModule]
@@ -689,6 +631,16 @@ def get_anchors(
689631
)
690632

691633

634+
class Conv2dPattern(ConvPattern):
635+
def partition_types(self) -> list[OpOverload]:
636+
return [torch.ops.aten.conv2d.default]
637+
638+
639+
class Conv2dPatternPadding(ConvPattern):
640+
def partition_types(self) -> list[OpOverload]:
641+
return [torch.ops.aten.conv2d.padding]
642+
643+
692644
class ConvTranspose2dPattern(QuantizationPattern):
693645
def __init__(self, neutron_quantizer, is_qat: bool = False):
694646
super().__init__(is_qat=is_qat)

backends/nxp/tests/ir/converter/node_converter/test_conv_converter.py

Lines changed: 99 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ def assert_delegated_and_correct(
3535
input_shape,
3636
mocker,
3737
request,
38-
use_qat,
38+
use_qat=False,
3939
exp_delegated_ops=None,
4040
exp_non_delegated_ops=None,
4141
et_ref_model=ReferenceModel.QUANTIZED_EXECUTORCH_CPP,
@@ -558,19 +558,6 @@ def test__tr_no_deleg(
558558

559559

560560
class TestConv:
561-
@staticmethod
562-
def _conv_id(ins, oc, ks=3, s=2, d=1, p=0, b=True, g=1):
563-
return (
564-
f"ic={ins}, "
565-
f"oc={oc}, "
566-
f"ks={ks}, "
567-
f"s={s}, "
568-
f"d={d}, "
569-
f"p={p}, "
570-
f"b={b}, "
571-
f"g={g}"
572-
)
573-
574561
@pytest.mark.parametrize(
575562
"input_shape, out_channels",
576563
[
@@ -1105,6 +1092,104 @@ def test__d_fwd_misc_arg(
11051092

11061093
assert_delegated_and_correct(model, input_shape, mocker, request, use_qat)
11071094

1095+
@pytest.mark.parametrize(
1096+
"input_shape, kernel_size, stride, dilation, padding, bias",
1097+
[
1098+
pytest.param(
1099+
ins := (2, 5, 13, 11),
1100+
ks := 3,
1101+
s := 2,
1102+
d := 2,
1103+
p := "valid",
1104+
b := True,
1105+
id=f"string padding arg: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b)}",
1106+
),
1107+
pytest.param(
1108+
ins := (2, 5, 13, 11),
1109+
ks := 3,
1110+
s := 1,
1111+
d := 2,
1112+
p := "same",
1113+
b := True,
1114+
id=f"string padding arg: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b)}",
1115+
),
1116+
],
1117+
)
1118+
def test__fwd_str_arg(
1119+
self,
1120+
input_shape,
1121+
kernel_size,
1122+
stride,
1123+
dilation,
1124+
padding,
1125+
bias,
1126+
request,
1127+
mocker,
1128+
):
1129+
out_channels = input_shape[1]
1130+
1131+
model = Conv2dModule(
1132+
in_channels=input_shape[1],
1133+
out_channels=out_channels,
1134+
kernel_size=kernel_size,
1135+
stride=stride,
1136+
dilation=dilation,
1137+
padding=padding,
1138+
bias=bias,
1139+
)
1140+
1141+
assert_delegated_and_correct(model, input_shape, mocker, request)
1142+
1143+
@pytest.mark.parametrize(
1144+
"input_shape, kernel_size, stride, dilation, padding, bias",
1145+
[
1146+
pytest.param(
1147+
ins := (2, 5, 13, 11),
1148+
ks := 3,
1149+
s := 2,
1150+
d := 2,
1151+
p := "valid",
1152+
b := True,
1153+
id=f"string padding arg, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}",
1154+
),
1155+
pytest.param(
1156+
ins := (2, 5, 13, 11),
1157+
ks := 3,
1158+
s := 1,
1159+
d := 2,
1160+
p := "same",
1161+
b := True,
1162+
id=f"string padding arg, depthwise: {_conv_id(ins, ins[1], ks=ks, s=s, d=d, p=p, b=b, g=ins[1])}",
1163+
),
1164+
],
1165+
)
1166+
def test__d_fwd_str_arg(
1167+
self,
1168+
input_shape,
1169+
kernel_size,
1170+
stride,
1171+
dilation,
1172+
padding,
1173+
bias,
1174+
request,
1175+
mocker,
1176+
):
1177+
out_channels = input_shape[1]
1178+
group = input_shape[1]
1179+
1180+
model = Conv2dModule(
1181+
in_channels=input_shape[1],
1182+
out_channels=out_channels,
1183+
kernel_size=kernel_size,
1184+
stride=stride,
1185+
dilation=dilation,
1186+
padding=padding,
1187+
bias=bias,
1188+
group=group,
1189+
)
1190+
1191+
assert_delegated_and_correct(model, input_shape, mocker, request)
1192+
11081193
@pytest.mark.parametrize(
11091194
"input_shape, out_channels, kernel_size, stride, dilation, padding",
11101195
[

0 commit comments

Comments
 (0)