Skip to content

Commit ab4271f

Browse files
authored
NXP backend: added support for aten.conv_transpose2d using new MLIR flow (#20807)
### Summary Added support for `aten.conv_transpose2d` using new Neutron flow. ### Test plan tests can be manually run using `pytest -c /dev/null backends/nxp/tests/` cc @robert-kalmar @JakeStevens @digantdesai @rascani @MartinPavella
1 parent 2ecfc10 commit ab4271f

5 files changed

Lines changed: 815 additions & 974 deletions

File tree

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

Lines changed: 109 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -58,29 +58,15 @@
5858
Stride = Padding = Dilation = OutPadding = list[int]
5959
Transposed = bool
6060
Groups = int
61-
ConvolutionArgs = tuple[
62-
Node, Node, Node | None, Stride, Padding, Dilation, Transposed, OutPadding, Groups
63-
]
6461

6562

6663
@requires_channels_first_format
6764
class ConvolutionConverter(NodeConverter):
6865
@staticmethod
69-
def _is_supported_on_target_regular_conv(
70-
node: Node,
71-
parameters_mapping: dict[str, Parameter],
66+
def _is_conv_quant_supported(
67+
node: Node, parameters_mapping: dict[str, Parameter]
7268
) -> bool:
73-
(
74-
inp_node,
75-
w_node,
76-
b_node,
77-
stride,
78-
_,
79-
dilation,
80-
_,
81-
_,
82-
_,
83-
) = ConvolutionConverter._get_convolution_arguments(node)
69+
conv_params = ConvolutionConverter._get_conv_params(node)
8470

8571
# Input must be INT8/UINT8
8672
# Output must be INT8/UINT8
@@ -98,47 +84,76 @@ def _is_supported_on_target_regular_conv(
9884
return False
9985

10086
# Bias must be INT32
101-
if b_node is not None:
87+
if conv_params.bias_node is not None:
10288
b_supported_types = [torch.int32]
10389
if not NodeConverter.uses_quantization_type_for_io(
10490
node, b_supported_types, [2], []
10591
):
10692
return False
10793

10894
# Weights must be constant
109-
if not node_is_effectively_static_tensor(w_node, parameters_mapping):
95+
if not node_is_effectively_static_tensor(
96+
conv_params.weight_node, parameters_mapping
97+
):
11098
return False
11199

112100
# Bias must be constant (if present)
113-
if b_node is not None and not node_is_effectively_static_tensor(
114-
b_node, parameters_mapping
101+
if conv_params.bias_node is not None and not node_is_effectively_static_tensor(
102+
conv_params.bias_node, parameters_mapping
115103
):
116104
return False
117105

106+
return True
107+
108+
@staticmethod
109+
def _is_supported_on_target_regular_conv(
110+
node: Node,
111+
neutron_target_spec: NeutronTargetSpec,
112+
parameters_mapping: dict[str, Parameter],
113+
) -> bool:
114+
# Check the quantization of inputs is supported
115+
if not ConvolutionConverter._is_conv_quant_supported(node, parameters_mapping):
116+
return False
117+
118+
op_args = ConvolutionConverter._get_conv_params(node)
119+
node_params = get_node_tensor_params(node)
120+
118121
# kernelH <= 4096, kernelW <= 4096
119122
# strideH <= 4096, strideW <= 4096
120123
# dilationH <= 4096, dilationW <= 4096
121-
w_node_shape = w_node.meta["val"].shape
122-
123-
kernel_h = w_node_shape[2]
124-
kernel_w = w_node_shape[3]
125-
stride_h = stride[0]
126-
stride_w = stride[1]
127-
dilation_h = dilation[0]
128-
dilation_w = dilation[1]
124+
kernel_h = node_params["kernel_height"]
125+
kernel_w = node_params["kernel_width"]
126+
stride_h = op_args.stride[0]
127+
stride_w = op_args.stride[1]
128+
dilation_h = op_args.dilation[0]
129+
dilation_w = op_args.dilation[1]
129130

130131
dim_sizes = [kernel_h, kernel_w, stride_h, stride_w, dilation_h, dilation_w]
131132

132133
if any(dim > 4096 for dim in dim_sizes):
133134
return False
134135

135-
# kernelH * kernelW * inpC <= 65535
136-
inp_node_shape = inp_node.meta["val"].shape
137-
inp_channels = (
138-
inp_node_shape[1] if len(inp_node_shape) == 4 else inp_node_shape[0]
139-
)
136+
# The following checks are mentioned in Neutron docs, however they cause some models
137+
# to be non-delegable. Discussion with Neutron team is pending, and because the convolutions seem
138+
# to work even without this constraint, this code is commented out for now
139+
# to boost the models' performance.
140+
# padT < kernelH, padB < kernelH, padL < kernelW, padR < kernelW
141+
142+
# padding_h = op_args.padding[0]
143+
# padding_w = op_args.padding[1]
144+
# if padding_h >= kernel_h or padding_w >= kernel_w:
145+
# return False
140146

141-
if kernel_h * kernel_w * inp_channels > 65535:
147+
# kernelH * kernelW * ROUND_CEIL(inpC, NUM_MACS) <= 65535
148+
inp_channels = node_params["inp_channels"]
149+
num_macs = neutron_target_spec.get_num_macs()
150+
151+
if (
152+
kernel_h
153+
* kernel_w
154+
* ConvolutionConverter._round_ceil(inp_channels, num_macs)
155+
> 65535
156+
):
142157
return False
143158

144159
return True
@@ -149,48 +164,51 @@ def _is_supported_on_target_transp_conv(
149164
neutron_target_spec: NeutronTargetSpec,
150165
parameters_mapping: dict[str, Parameter],
151166
) -> bool:
152-
# TODO: EIEX-894 update the requirements of delegation for new Neutron flow
153-
_, w_node, _, stride, padding, dilation, transposed, _, groups = (
154-
ConvolutionConverter._get_convolution_arguments(node)
155-
)
156-
157-
num_macs = neutron_target_spec.get_num_macs()
158-
node_t_params = get_node_tensor_params(node)
159-
160-
if node_t_params["batch_size"] != 1:
161-
# Only TransposeConv2d with batch size = 1 is supported on neutron.
167+
# Check the quantization of inputs is supported
168+
if not ConvolutionConverter._is_conv_quant_supported(node, parameters_mapping):
162169
return False
163170

171+
op_args = ConvolutionConverter._get_conv_params(node)
172+
node_params = get_node_tensor_params(node)
173+
164174
# TransposeConv2d with groups > 1 is not supported
165175
# TODO: split into multiple convs with groups = 1
166-
if groups > 1:
176+
if op_args.groups > 1:
167177
return False
168-
if not node_is_effectively_static_tensor(w_node, parameters_mapping):
169-
# Only supported if the weights are static, because TFLite `TransposeConv` uses permuted
170-
# weights. In case the weights are dynamic, a Transpose operator would have to be added, which
171-
# is not supported on Neutron.
178+
179+
# kernelH <= 4096, kernelW <= 4096
180+
kernel_h = node_params["kernel_height"]
181+
kernel_w = node_params["kernel_width"]
182+
183+
dim_sizes = [kernel_h, kernel_w]
184+
if any(dim > 4096 for dim in dim_sizes):
172185
return False
173-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#876 TransposeConv2DKernelKind
186+
187+
# strideH <= kernelH, strideW <= kernelW
188+
stride_h = op_args.stride[0]
189+
stride_w = op_args.stride[1]
190+
if stride_h > kernel_h or stride_w > kernel_w:
191+
return False
192+
193+
# strideH <= 2, strideW <= 2
194+
if stride_h > 2 or stride_w > 2:
195+
return False
196+
197+
# padT < kernelH, padB < kernelH, padL < kernelW, padR < kernelW
198+
padding_h = op_args.padding[0]
199+
padding_w = op_args.padding[1]
200+
if padding_h >= kernel_h or padding_w >= kernel_w:
201+
return False
202+
203+
# kernelH * kernelW * ceil(inpC, NUM_MACS) <= 65535
204+
inp_channels = node_params["inp_channels"]
205+
num_macs = neutron_target_spec.get_num_macs()
206+
174207
if (
175-
dilation != [1, 1]
176-
or padding[0] != 0
177-
or padding[1] >= node_t_params["kernel_width"]
178-
or (
179-
padding[1] != 0 and node_t_params["inp_height"] != 1
180-
) # Slice added by explicit padding
181-
or stride[0] != 1
182-
or (
183-
(
184-
stride[1] != node_t_params["kernel_width"] / 2
185-
or node_t_params["out_height"] != 1
186-
)
187-
and stride[1] != node_t_params["kernel_width"]
188-
)
189-
or stride[1] % 2 != 0
190-
or node_t_params["inp_channels"] % num_macs != 0
191-
or node_t_params["out_channels"] % num_macs != 0
192-
or node_t_params["kernel_width"] % 2 != 0
193-
or node_t_params["kernel_height"] != 1
208+
kernel_h
209+
* kernel_w
210+
* ConvolutionConverter._round_ceil(inp_channels, num_macs)
211+
> 65535
194212
):
195213
return False
196214

@@ -203,16 +221,16 @@ def _is_supported_on_target(
203221
parameters_mapping: dict[str, Parameter],
204222
custom_delegation_options: CustomDelegationOptions,
205223
) -> bool:
206-
is_transposed = (ConvolutionConverter._get_convolution_arguments(node))[6]
224+
conv_params = ConvolutionConverter._get_conv_params(node)
207225

208-
if is_transposed:
226+
if conv_params.transposed:
209227
return ConvolutionConverter._is_supported_on_target_transp_conv(
210228
node, neutron_target_spec, parameters_mapping
211229
)
212230

213231
else:
214232
return ConvolutionConverter._is_supported_on_target_regular_conv(
215-
node, parameters_mapping
233+
node, neutron_target_spec, parameters_mapping
216234
)
217235

218236
@staticmethod
@@ -244,6 +262,10 @@ def _is_supported_in_IR(
244262

245263
return True
246264

265+
@staticmethod
266+
def _round_ceil(x, n):
267+
return ((x + n - 1) // n) * n
268+
247269
def _compute_slicing_params(
248270
self, output_shape, explicit_padding
249271
) -> tuple[list[int], list[int]]:
@@ -259,22 +281,20 @@ def _compute_slicing_params(
259281
return begins, sizes
260282

261283
@staticmethod
262-
def _get_convolution_arguments(
284+
def _get_conv_params(
263285
conv_node: Node,
264-
) -> ConvolutionArgs:
286+
) -> ConvParameters:
265287
x, w, b, stride, padding, dilation, transposed, out_padding, groups = (
266288
conv_node.args
267289
)
268-
return (
269-
x,
270-
w,
271-
b,
272-
list(stride),
273-
list(padding),
274-
list(dilation),
275-
transposed,
276-
list(out_padding),
277-
groups,
290+
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)
295+
296+
return ConvParameters(
297+
x, w, b, stride, padding, dilation, transposed, out_padding, groups
278298
)
279299

280300
# noinspection PyPep8Naming
@@ -415,8 +435,10 @@ def _convert_transpose_conv(
415435
return conversion_result
416436

417437
def _convert_2d_conv(
418-
self, t_op: tflite_model.Operator, conv_params: ConvParameters
438+
self, torch_node: Node, t_op: tflite_model.Operator
419439
) -> list[tflite_model.Operator]:
440+
conv_params = self._get_conv_params(torch_node)
441+
420442
if conv_params.transposed:
421443
t_op.builtin_options = transpose_conv_options.TransposeConv()
422444
if conv_utils.group_conv_convertible_into_multiple_convolutions(
@@ -502,18 +524,11 @@ def _convert_2d_conv(
502524
def convert(self, node: Node):
503525
self.assert_convertible(node)
504526

505-
_, _, _, stride, padding, dilation, transposed, out_padding, groups = (
506-
self._get_convolution_arguments(node)
507-
)
508-
509527
t_op = self._create_tflite_op_with_io_tensors(node)
510-
conv_params = ConvParameters(
511-
stride, padding, dilation, transposed, out_padding, groups
512-
)
513528

514529
rank = t_op.tmp_inputs[1].shape.len()
515530
if rank == 4: # Conv2D
516-
ops_to_add = self._convert_2d_conv(t_op, conv_params)
531+
ops_to_add = self._convert_2d_conv(node, t_op)
517532
else:
518533
raise NotImplementedError(
519534
f"{rank - 2}D convolution is not supported."

backends/nxp/backend/ir/converter/node_converters/shared/conv_utils.py

Lines changed: 32 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2023-2025 NXP
1+
# Copyright 2023-2026 NXP
22
#
33
# This source code is licensed under the BSD-style license found in the
44
# LICENSE file in the root directory of this source tree.
@@ -13,11 +13,14 @@
1313

1414
@dataclass
1515
class ConvParameters:
16+
input_node: Node
17+
weight_node: Node
18+
bias_node: Node | None
1619
stride: list[int]
1720
padding: list[int]
1821
dilation: list[int]
1922
transposed: bool
20-
out_padding: list[int]
23+
out_padding: list[int] | None # only meaningful for transposed conv
2124
groups: int
2225

2326

@@ -40,22 +43,37 @@ def _get_IO_channels(node: Node | tflite_model.Operator) -> (int, int):
4043
def get_node_tensor_params(node: Node) -> dict:
4144
node_tensor_params = {}
4245

46+
# handle input tensor parameters
4347
input_tensor = node.args[0]
44-
assert len(input_tensor.meta["val"].shape) in [3, 4], "Supports only Conv 1D, 2D."
45-
node_tensor_params["batch_size"] = input_tensor.meta["val"].shape[0]
46-
node_tensor_params["inp_channels"] = input_tensor.meta["val"].shape[1]
47-
node_tensor_params["inp_height"] = input_tensor.meta["val"].shape[2]
48-
if len(input_tensor.meta["val"].shape) == 4:
49-
node_tensor_params["inp_width"] = input_tensor.meta["val"].shape[3]
48+
inp_shape = input_tensor.meta["val"].shape
49+
assert len(inp_shape) in [
50+
3,
51+
4,
52+
], "Supports only forward Conv 2D with possible implicit batch."
53+
54+
node_tensor_params["inp_height"] = inp_shape[2]
55+
node_tensor_params["inp_width"] = inp_shape[3]
56+
if len(inp_shape) == 4:
57+
node_tensor_params["inp_channels"] = inp_shape[1]
58+
node_tensor_params["batch_size"] = inp_shape[0]
59+
else:
60+
node_tensor_params["inp_channels"] = inp_shape[0]
61+
node_tensor_params["batch_size"] = 1
5062

63+
# handle weight tensor parameters
5164
weights = node.args[1]
52-
node_tensor_params["out_channels"] = node.meta["val"].shape[1]
53-
node_tensor_params["out_height"] = node.meta["val"].shape[2]
54-
if len(node.meta["val"].shape) == 4:
55-
node_tensor_params["out_width"] = node.meta["val"].shape[3]
5665
node_tensor_params["kernel_height"] = weights.meta["val"].shape[2]
57-
if len(weights.meta["val"].shape) == 4:
58-
node_tensor_params["kernel_width"] = weights.meta["val"].shape[3]
66+
node_tensor_params["kernel_width"] = weights.meta["val"].shape[3]
67+
68+
# handle output tensor parameters
69+
out_shape = node.meta["val"].shape
70+
71+
node_tensor_params["out_height"] = out_shape[2]
72+
node_tensor_params["out_width"] = out_shape[3]
73+
if len(out_shape) == 4:
74+
node_tensor_params["out_channels"] = out_shape[1]
75+
else:
76+
node_tensor_params["out_channels"] = out_shape[0]
5977

6078
return node_tensor_params
6179

0 commit comments

Comments
 (0)