Skip to content

Commit a917d53

Browse files
committed
feat: added support for conv2d using new NeutronC flow
1 parent 3459bfd commit a917d53

8 files changed

Lines changed: 1847 additions & 638 deletions

File tree

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

Lines changed: 154 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -48,78 +48,171 @@
4848
from torch.nn import Parameter
4949

5050

51+
# The arguments of the conv are:
52+
# convolution(
53+
# Tensor input, Tensor weight, Tensor? bias,
54+
# SymInt[] stride, SymInt[] padding, SymInt[] dilation,
55+
# bool transposed, SymInt[] output_padding, SymInt groups
56+
# ) -> Tensor
57+
Stride = Padding = Dilation = OutPadding = list[int]
58+
Transposed = bool
59+
Groups = int
60+
ConvolutionArgs = tuple[
61+
Node, Node, Node | None, Stride, Padding, Dilation, Transposed, OutPadding, Groups
62+
]
63+
64+
5165
class ConvolutionConverter(NodeConverter):
5266
@staticmethod
53-
def _is_supported_on_target(
67+
def _is_supported_on_target_regular_conv(
68+
node: Node,
69+
parameters_mapping: dict[str, Parameter],
70+
) -> bool:
71+
(
72+
inp_node,
73+
w_node,
74+
b_node,
75+
stride,
76+
_,
77+
dilation,
78+
_,
79+
_,
80+
_,
81+
) = ConvolutionConverter._get_convolution_arguments(node)
82+
83+
# Input must be INT8/UINT8
84+
# Output must be INT8/UINT8
85+
inp_out_supported_types = [torch.int8, torch.uint8]
86+
if not NodeConverter.uses_quantization_type_for_io(
87+
node, inp_out_supported_types, [0], [0]
88+
):
89+
return False
90+
91+
# Weights must be INT8
92+
w_supported_types = [torch.int8]
93+
if not NodeConverter.uses_quantization_type_for_io(
94+
node, w_supported_types, [1], []
95+
):
96+
return False
97+
98+
# Bias must be INT32
99+
if b_node is not None:
100+
b_supported_types = [torch.int32]
101+
if not NodeConverter.uses_quantization_type_for_io(
102+
node, b_supported_types, [2], []
103+
):
104+
return False
105+
106+
# Weights must be constant
107+
if not node_is_effectively_static_tensor(w_node, parameters_mapping):
108+
return False
109+
110+
# Bias must be constant (if present)
111+
if b_node is not None and not node_is_effectively_static_tensor(
112+
b_node, parameters_mapping
113+
):
114+
return False
115+
116+
# kernelH <= 4096, kernelW <= 4096
117+
# strideH <= 4096, strideW <= 4096
118+
# dilationH <= 4096, dilationW <= 4096
119+
w_node_shape = w_node.meta["val"].shape
120+
121+
kernel_h = w_node_shape[2]
122+
kernel_w = w_node_shape[3]
123+
stride_h = stride[0]
124+
stride_w = stride[1]
125+
dilation_h = dilation[0]
126+
dilation_w = dilation[1]
127+
128+
dim_sizes = [kernel_h, kernel_w, stride_h, stride_w, dilation_h, dilation_w]
129+
130+
if any(dim > 4096 for dim in dim_sizes):
131+
return False
132+
133+
# kernelH * kernelW * inpC <= 65535
134+
inp_node_shape = inp_node.meta["val"].shape
135+
inp_channels = (
136+
inp_node_shape[1] if len(inp_node_shape) == 4 else inp_node_shape[0]
137+
)
138+
139+
if kernel_h * kernel_w * inp_channels > 65535:
140+
return False
141+
142+
return True
143+
144+
@staticmethod
145+
def _is_supported_on_target_transp_conv(
54146
node: Node,
55147
neutron_target_spec: NeutronTargetSpec,
56148
parameters_mapping: dict[str, Parameter],
57-
custom_delegation_options: CustomDelegationOptions,
58149
) -> bool:
150+
# TODO: EIEX-894 update the requirements of delegation for new Neutron flow
151+
_, w_node, _, stride, padding, dilation, transposed, _, groups = (
152+
ConvolutionConverter._get_convolution_arguments(node)
153+
)
154+
59155
num_macs = neutron_target_spec.get_num_macs()
60156
node_t_params = get_node_tensor_params(node)
61-
weights = node.args[1]
62-
conv_params = ConvParameters(
63-
*ConvolutionConverter._get_convolution_arguments(node)
64-
)
65157

66158
if node_t_params["batch_size"] != 1:
67-
# Only batch size 1 is supported on neutron.
159+
# Only TransposeConv2d with batch size = 1 is supported on neutron.
68160
return False
69161

70-
if conv_params.transposed:
71-
# TransposeConv2d with groups > 1 is not supported
72-
# TODO: split into multiple convs with groups = 1
73-
if conv_params.groups > 1:
74-
return False
75-
if not node_is_effectively_static_tensor(weights, parameters_mapping):
76-
# Only supported if the weights are static, because TFLite `TransposeConv` uses permuted
77-
# weights. In case the weights are dynamic, a Transpose operator would have to be added, which
78-
# is not supported on Neutron.
79-
return False
80-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#876 TransposeConv2DKernelKind
81-
if (
82-
conv_params.dilation != [1, 1]
83-
or conv_params.padding[0] != 0
84-
or conv_params.padding[1] >= node_t_params["kernel_width"]
85-
or (
86-
conv_params.padding[1] != 0 and node_t_params["inp_height"] != 1
87-
) # Slice added by explicit padding
88-
or conv_params.stride[0] != 1
89-
or (
90-
(
91-
conv_params.stride[1] != node_t_params["kernel_width"] / 2
92-
or node_t_params["out_height"] != 1
93-
)
94-
and conv_params.stride[1] != node_t_params["kernel_width"]
95-
)
96-
or conv_params.stride[1] % 2 != 0
97-
or node_t_params["inp_channels"] % num_macs != 0
98-
or node_t_params["out_channels"] % num_macs != 0
99-
or node_t_params["kernel_width"] % 2 != 0
100-
or node_t_params["kernel_height"] != 1
101-
):
102-
return False
103-
elif conv_params.groups == 1: # Regular convolution.
104-
pass
105-
elif conv_utils.group_conv_convertible_as_depthwise(
106-
node, conv_params.groups
107-
): # Depthwise convolution.
108-
# Only supported if the weights are static, because TFLite `DepthwiseConv2D` uses permuted
162+
# TransposeConv2d with groups > 1 is not supported
163+
# TODO: split into multiple convs with groups = 1
164+
if groups > 1:
165+
return False
166+
if not node_is_effectively_static_tensor(w_node, parameters_mapping):
167+
# Only supported if the weights are static, because TFLite `TransposeConv` uses permuted
109168
# weights. In case the weights are dynamic, a Transpose operator would have to be added, which
110169
# is not supported on Neutron.
111-
if not node_is_effectively_static_tensor(weights, parameters_mapping):
112-
return False
113-
elif conv_utils.group_conv_convertible_into_multiple_convolutions(
114-
node, conv_params.groups
115-
): # Separable conv.
116-
# Requires addition of `Split` and `Concatenation` operators, which are not supported on Neutron.
117170
return False
118-
else: # Unexpected case (should never happen).
171+
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#876 TransposeConv2DKernelKind
172+
if (
173+
dilation != [1, 1]
174+
or padding[0] != 0
175+
or padding[1] >= node_t_params["kernel_width"]
176+
or (
177+
padding[1] != 0 and node_t_params["inp_height"] != 1
178+
) # Slice added by explicit padding
179+
or stride[0] != 1
180+
or (
181+
(
182+
stride[1] != node_t_params["kernel_width"] / 2
183+
or node_t_params["out_height"] != 1
184+
)
185+
and stride[1] != node_t_params["kernel_width"]
186+
)
187+
or stride[1] % 2 != 0
188+
or node_t_params["inp_channels"] % num_macs != 0
189+
or node_t_params["out_channels"] % num_macs != 0
190+
or node_t_params["kernel_width"] % 2 != 0
191+
or node_t_params["kernel_height"] != 1
192+
):
119193
return False
120194

121195
return True
122196

197+
@staticmethod
198+
def _is_supported_on_target(
199+
node: Node,
200+
neutron_target_spec: NeutronTargetSpec,
201+
parameters_mapping: dict[str, Parameter],
202+
custom_delegation_options: CustomDelegationOptions,
203+
) -> bool:
204+
is_transposed = (ConvolutionConverter._get_convolution_arguments(node))[6]
205+
206+
if is_transposed:
207+
return ConvolutionConverter._is_supported_on_target_transp_conv(
208+
node, neutron_target_spec, parameters_mapping
209+
)
210+
211+
else:
212+
return ConvolutionConverter._is_supported_on_target_regular_conv(
213+
node, parameters_mapping
214+
)
215+
123216
@staticmethod
124217
def _is_supported_in_IR(
125218
node: Node,
@@ -149,10 +242,6 @@ def _is_supported_in_IR(
149242

150243
return True
151244

152-
Stride = Padding = Dilation = OutPadding = list[int]
153-
Transposed = bool
154-
Groups = int
155-
156245
def _compute_slicing_params(
157246
self, output_shape, explicit_padding
158247
) -> tuple[list[int], list[int]]:
@@ -170,14 +259,14 @@ def _compute_slicing_params(
170259
@staticmethod
171260
def _get_convolution_arguments(
172261
conv_node: Node,
173-
) -> (Stride, Padding, Dilation, Transposed, OutPadding, Groups):
174-
# The arguments of the conv are:
175-
# [x, w, b, stride, padding, dilation, transposed, output padding, groups]
176-
# https://github.com/pytorch/pytorch/blob/v2.6.0/aten/src/ATen/native/Convolution.cpp#L286-L291
177-
_, _, _, stride, padding, dilation, transposed, out_padding, groups = (
262+
) -> ConvolutionArgs:
263+
x, w, b, stride, padding, dilation, transposed, out_padding, groups = (
178264
conv_node.args
179265
)
180266
return (
267+
x,
268+
w,
269+
b,
181270
list(stride),
182271
list(padding),
183272
list(dilation),
@@ -380,16 +469,8 @@ def _convert_2d_conv(
380469

381470
elif conv_utils.group_conv_convertible_into_multiple_convolutions(
382471
t_op, conv_params.groups
383-
): # Convert to separated `Conv2D`.
384-
t_op.builtin_options = conv_2d_options.Conv2D()
385-
386-
return conv_utils.create_separated_convolutions_based_on_group(
387-
t_op,
388-
conv_params,
389-
self.builder,
390-
self._convert_unpadded_2D,
391-
conv_utils.conv_op_factory,
392-
)
472+
):
473+
raise RuntimeError("NXP backend: Group convolution was not decomposed.")
393474

394475
else:
395476
# Convert to regular `Conv2D`.
@@ -419,7 +500,7 @@ def _convert_2d_conv(
419500
def convert(self, node: Node):
420501
self.assert_convertible(node)
421502

422-
stride, padding, dilation, transposed, out_padding, groups = (
503+
_, _, _, stride, padding, dilation, transposed, out_padding, groups = (
423504
self._get_convolution_arguments(node)
424505
)
425506

backends/nxp/tests/generic_tests/test_batch_norm_fusion.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,7 +112,7 @@ def test_batch_norm_conv_fusing__full_pipeline__1d(bias: bool):
112112
module, tuple(input_shape)
113113
).exported_program()
114114

115-
assert len(edge_program.graph.nodes) == 21
115+
assert len(edge_program.graph.nodes) == 7
116116
assert not graph_contains_any_of_ops(edge_program.graph, batch_norm_target_ops)
117117

118118

backends/nxp/tests/generic_tests/test_neutron_converter_manager.py

Lines changed: 1 addition & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -6,38 +6,11 @@
66
import multiprocessing
77
import pickle
88

9-
import torch
10-
from executorch import exir
11-
from executorch.backends.nxp.backend.edge_program_converter import (
12-
EdgeProgramToIRConverter,
13-
)
149
from executorch.backends.nxp.backend.neutron_converter_manager import (
1510
NeutronConverterManager,
1611
)
17-
from executorch.backends.nxp.backend.node_format_inference import NodeFormatInference
1812
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
19-
from executorch.backends.nxp.tests.models import Conv2dModule, LinearModule
20-
21-
22-
def test_conv2d_neutron_conversion():
23-
model = Conv2dModule()
24-
25-
example_input = (torch.ones(1, 4, 32, 32),)
26-
exir_program = torch.export.export(model, example_input)
27-
edge_program_manager = exir.to_edge(exir_program)
28-
29-
NodeFormatInference(edge_program_manager.exported_program()).identify_node_formats()
30-
edge_program_converter = EdgeProgramToIRConverter()
31-
tflite_model, *_ = edge_program_converter.convert_program(
32-
edge_program_manager.exported_program()
33-
)
34-
35-
neutron_converter_manager = NeutronConverterManager()
36-
neutron_model = neutron_converter_manager.convert(tflite_model, "imxrt700", False)
37-
38-
assert len(
39-
neutron_model
40-
), "Produced NeutronGraph-based TFLite model has zero length!"
13+
from executorch.backends.nxp.tests.models import LinearModule
4114

4215

4316
def test_conv2d_neutron_conversion__prefetching(mocker):

0 commit comments

Comments
 (0)