66import numpy as np
77import torch
88
9- from executorch .backends .nxp .backend .edge_helper import input_tensor , input_tensor_safe
9+ from executorch .backends .nxp .backend .edge_helper import (
10+ input_tensor ,
11+ input_tensor_safe ,
12+ node_is_effectively_static_tensor ,
13+ )
1014from executorch .backends .nxp .backend .ir .converter .conversion import (
1115 aten_translator ,
1216 common ,
1317)
14- from executorch .backends .nxp .backend .ir .converter .conversion .common import (
15- OpsList ,
16- try_get_input ,
17- )
18+ from executorch .backends .nxp .backend .ir .converter .conversion .common import try_get_input
1819from executorch .backends .nxp .backend .ir .converter .node_converter import (
1920 NodeConverter ,
2021 Target ,
2122)
23+ from executorch .backends .nxp .backend .ir .converter .node_converters .shared import (
24+ conv_utils ,
25+ )
26+ from executorch .backends .nxp .backend .ir .converter .node_converters .shared .conv_utils import (
27+ ConvConversionResult ,
28+ ConvParameters ,
29+ )
2230from executorch .backends .nxp .backend .ir .converter .quantization_utils import (
2331 set_quantization_parameters_to_tensor ,
2432)
33+ from executorch .backends .nxp .backend .ir .converter .tensor_utils import tensor_has_data
2534from executorch .backends .nxp .backend .ir .lib .tflite .TensorType import TensorType
2635from executorch .backends .nxp .backend .ir .tflite_generator import tflite_model
2736from executorch .backends .nxp .backend .ir .tflite_generator .builtin_options import (
2837 conv_2d_options ,
38+ depthwise_conv_2d_options ,
2939)
3040from torch .fx import Node
3141from torch .nn import Parameter
@@ -48,7 +58,29 @@ def _is_supported_in_IR(
4858 if output_padding != [0 , 0 ]:
4959 return False
5060
51- if groups != 1 :
61+ if groups == 1 :
62+ # Regular (pointwise) convolution.
63+ pass
64+
65+ elif conv_utils .group_conv_convertible_as_depthwise (
66+ node , groups
67+ ) and node_is_effectively_static_tensor (node .args [1 ], parameters_mapping ):
68+ # Depthwise convolution.
69+ # Only supported if the weights are static, because TFLite `DepthwiseConv2D` uses permuted weights. In case
70+ # the weights are dynamic, a Transpose operator would have to be added, which is not supported on Neutron.
71+ pass
72+
73+ elif conv_utils .group_conv_convertible_into_multiple_convolutions (node , groups ):
74+ # Group Separable convolution.
75+ # Not supported natively by the eIQ Neutron so Group Separable Convolution.
76+ # In practice it can be computed by splitting the Group Separable Convolution into multiple Pointwise
77+ # Convo it will use the Split and Concat operation. The Concat operation in Neutron Converter
78+ # SDK 25.03 requires the # of channels to be multipy of # of MAC units in the eIQ Neutron.
79+ # For this reason Group Separable Convolution is not delegated by default at this moment.
80+ return False
81+
82+ else :
83+ # All conversion options related to the `group` attribute have been checked and none of them can be used.
5284 return False
5385
5486 if input_tensor_safe (node , 2 ) is None :
@@ -59,69 +91,146 @@ def _is_supported_in_IR(
5991
6092 return True
6193
62- def _convert_2d_conv (
63- self , stride , padding , dilation , t_op : tflite_model .Operator
64- ) -> list [tflite_model .Operator ]:
65- ops = OpsList (middle_op = t_op )
66- t_op .builtin_options = conv_2d_options .Conv2D ()
67- common .assign_2d_strides (t_op .builtin_options , stride )
68- common .assign_2d_dilations (t_op .builtin_options , dilation )
69- t_op .builtin_options .padding , explicit_padding = (
70- aten_translator .convert_padding (padding )
71- )
72-
73- if explicit_padding is not None :
74- # Need to prepend a 'Pad' operator, which adds 0s. But these will be included in the computation!
75- ops .add_pre (
76- self .builder .create_pad_operator_before (t_op , 0 , explicit_padding )
77- )
94+ Stride = Padding = Dilation = OutPadding = list [int ]
95+ Transposed = bool
96+ Groups = int
7897
79- input_tensor : tflite_model .Tensor = t_op .tmp_inputs [0 ]
80- weight_tensor : tflite_model .Tensor = t_op .tmp_inputs [1 ]
81- output_tensor : tflite_model .Tensor = t_op .tmp_outputs [0 ]
82-
83- if (bias_tensor := try_get_input (t_op , 2 )) is None :
98+ @staticmethod
99+ def _get_convolution_arguments (
100+ conv_node : Node ,
101+ ) -> (Stride , Padding , Dilation , Transposed , OutPadding , Groups ):
102+ # The arguments of the conv are:
103+ # [x, w, b, stride, padding, dilation, transposed, output padding, groups]
104+ # https://github.com/pytorch/pytorch/blob/v2.6.0/aten/src/ATen/native/Convolution.cpp#L286-L291
105+ _ , _ , _ , stride , padding , dilation , transposed , out_padding , groups = (
106+ conv_node .args
107+ )
108+ return stride , padding , dilation , transposed , out_padding , groups
109+
110+ # noinspection PyPep8Naming
111+ def _convert_unpadded_2D (
112+ self , t_op : tflite_model .Operator , conv_params : ConvParameters
113+ ) -> conv_utils .ConvConversionResult :
114+ """Convert the `aten.convolution` into TFLite. The `padding` and `builtin_options` must be converter by the
115+ caller.
116+ """
117+ common .assign_2d_strides (t_op .builtin_options , conv_params .stride )
118+ common .assign_2d_dilations (t_op .builtin_options , conv_params .dilation )
119+
120+ x : tflite_model .Tensor = t_op .tmp_inputs [0 ]
121+ w : tflite_model .Tensor = t_op .tmp_inputs [1 ]
122+ y : tflite_model .Tensor = t_op .tmp_outputs [0 ]
123+
124+ if (b := try_get_input (t_op , 2 )) is None :
84125 # Operator has no bias. Convolution aten op can omit it, TFLite can't.
85- output_channels = weight_tensor .shape .vector [0 ]
126+ output_channels = w .shape .vector [0 ]
86127
87- if weight_tensor .type == TensorType .FLOAT32 :
128+ if w .type == TensorType .FLOAT32 :
88129 bias_type = np .dtype (np .float32 )
89- elif weight_tensor .type in [TensorType .INT8 , TensorType .UINT8 ]:
130+ elif w .type in [TensorType .INT8 , TensorType .UINT8 ]:
90131 bias_type = np .dtype (np .int32 )
91132 else :
92133 # Should never happen.
93134 raise NotImplementedError (
94- f"Convolution node with unsupported weight type: { weight_tensor .type } "
135+ f"Convolution node with unsupported weight type: { w .type } "
95136 )
96137
97- bias_tensor = self .builder .create_zeros_tensor (
138+ b = self .builder .create_zeros_tensor (
98139 [output_channels ], "zero_bias" , bias_type , True
99140 )
100141
101142 # Compute scale and zero point for bias tensor
102- input_scale = np .array (input_tensor .quantization .scale .vector )
103- weight_scale = np .array (weight_tensor .quantization .scale .vector )
143+ input_scale = np .array (x .quantization .scale .vector )
144+ weight_scale = np .array (w .quantization .scale .vector )
104145 bias_scale = input_scale * weight_scale
105146 bias_zero_point = np .zeros (weight_scale .shape , dtype = np .int64 )
106147
107148 set_quantization_parameters_to_tensor (
108- bias_tensor , bias_scale , bias_zero_point , quantized_dimension = 0
149+ b , bias_scale , bias_zero_point , quantized_dimension = 0
109150 )
110151
111152 # Assign the operator its TFLite inputs and outputs
112- t_op .tmp_inputs = [input_tensor , weight_tensor , bias_tensor ]
113- t_op .tmp_outputs = [output_tensor ]
153+ t_op .tmp_inputs = [x , w , b ]
154+ t_op .tmp_outputs = [y ]
155+
156+ conversion_result = ConvConversionResult (x , w , b , y )
157+ conversion_result .ops_list .middle_op = t_op
158+
159+ return conversion_result
160+
161+ def _convert_2d_conv (
162+ self , t_op : tflite_model .Operator , conv_params : ConvParameters
163+ ) -> list [tflite_model .Operator ]:
164+ if conv_utils .group_conv_convertible_as_depthwise (
165+ t_op , conv_params .groups
166+ ): # Convert to `DepthwiseConv2D`.
167+ t_op .builtin_options = depthwise_conv_2d_options .DepthwiseConv2D ()
168+
169+ conversion_result = self ._convert_unpadded_2D (t_op , conv_params )
170+ t_op .builtin_options .padding , explicit_padding = (
171+ aten_translator .convert_padding (conv_params .padding )
172+ )
173+ if explicit_padding is not None :
174+ # Need to prepend a 'Pad' operator, which adds 0s.
175+ conversion_result .ops_list .add_pre (
176+ self .builder .create_pad_operator_before (t_op , 0 , explicit_padding )
177+ )
178+
179+ # DepthwiseConv2D expects weights in format [kernel_channels, kernel_height, kernel_width, output_channels]
180+ perm = [3 , 1 , 2 , 0 ]
181+ weight_tensor = conversion_result .conv_weight_tensor
182+ if tensor_has_data (weight_tensor ):
183+ # Transpose cloned tensor statically
184+ t_op .tmp_inputs [1 ] = self .builder .create_transposed_tensor (
185+ weight_tensor , perm
186+ )
187+ else :
188+ raise NotImplementedError ("Dynamic Depthwise Conv weights." )
189+
190+ elif conv_utils .group_conv_convertible_into_multiple_convolutions (
191+ t_op , conv_params .groups
192+ ):
193+ # Note: by default the Group Separable Convolution is rejected by the Neutron Partitioner, see the
194+ # ConvolutionConveter._is_supported_in_IR()
195+ t_op .builtin_options = conv_2d_options .Conv2D ()
196+
197+ return conv_utils .create_separated_convolutions_based_on_group (
198+ t_op ,
199+ conv_params ,
200+ self .builder ,
201+ self ._convert_unpadded_2D ,
202+ conv_utils .conv_op_factory ,
203+ )
204+
205+ else :
206+ # Convert to regular `Conv2D`.
207+ t_op .builtin_options = conv_2d_options .Conv2D ()
208+ conversion_result = self ._convert_unpadded_2D (t_op , conv_params )
209+ t_op .builtin_options .padding , explicit_padding = (
210+ aten_translator .convert_padding (conv_params .padding )
211+ )
212+ if explicit_padding is not None :
213+ # Need to prepend a 'Pad' operator, which adds 0s.
214+ conversion_result .ops_list .add_pre (
215+ self .builder .create_pad_operator_before (t_op , 0 , explicit_padding )
216+ )
114217
115- return ops .flatten ()
218+ return conversion_result . ops_list .flatten ()
116219
117220 def convert (self , node : Node ):
118221 self .assert_convertible (node )
119222
120- stride = node .args [3 ]
121- padding = node .args [4 ]
122- dilation = node .args [5 ]
223+ stride , padding , dilation , _ , _ , groups = self ._get_convolution_arguments (node )
123224
124225 t_op = self ._create_tflite_op_with_io_tensors (node )
125- ops_to_add = self ._convert_2d_conv (stride , padding , dilation , t_op )
226+ conv_params = ConvParameters (stride , padding , dilation , groups )
227+
228+ rank = t_op .tmp_inputs [1 ].shape .len ()
229+ if rank == 4 : # Conv2D
230+ ops_to_add = self ._convert_2d_conv (t_op , conv_params )
231+ else :
232+ raise NotImplementedError (
233+ f"{ rank - 2 } D convolution is not supported."
234+ ) # Should never get here.
126235
127236 self .builder .append_operators (ops_to_add )
0 commit comments