Skip to content

Commit cb91593

Browse files
Pop-kornrobert-kalmar
authored andcommitted
NXP backend: Add support for depthwise and separable convolution.
The `group` attribute of the `aten.convolution` has an effect on how the weights are used in the computation of the convolution. To properly utilize Neutron, the convolution is sometimes converted to `DepthwiseConv2D`.
1 parent 77f16dc commit cb91593

File tree

5 files changed

+912
-43
lines changed

5 files changed

+912
-43
lines changed

backends/nxp/backend/edge_helper.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55

66
import torch
77
from torch.fx import Node
8+
from torch.nn import Parameter
89

910

1011
def input_tensor(node: Node, input_index: int) -> torch.Tensor:
@@ -38,3 +39,35 @@ def input_tensor_safe(node: Node, input_index: int) -> torch.Tensor | None:
3839
return None
3940

4041
return input_tensor(node, input_index)
42+
43+
44+
def node_is_static_tensor(node: Node, parameters_mapping: dict[str, Parameter]) -> bool:
45+
"""Return `True` if the given `node` has static data in the `parameters_mapping` dict.
46+
:param node: Tensor node to check for data.
47+
:param parameters_mapping: Dict mapping tensor names to their static data. Should be inferred from the
48+
`state_dict` attribute of an edge program.
49+
"""
50+
return node.name in parameters_mapping.keys()
51+
52+
53+
def node_is_effectively_static_tensor(
54+
node: Node, parameters_mapping: dict[str, Parameter]
55+
) -> bool:
56+
"""Return `True` if the given `node` has static data, or follows after a `Dequantize` node with a static input.
57+
In the IR, the `node` will be turned into a static quantized tensor.
58+
:param node: Tensor node to check for data.
59+
:param parameters_mapping: Dict mapping tensor names to their static data. Should be inferred from the
60+
`state_dict` attribute of an edge program.
61+
"""
62+
if node_is_static_tensor(node, parameters_mapping):
63+
return True
64+
65+
def _is_dequantize(node_: Node) -> bool:
66+
return node_.target.__name__ in {
67+
"quantized_decomposed.dequantize_per_tensor.default",
68+
"quantized_decomposed.dequantize_per_channel.default",
69+
}
70+
71+
return _is_dequantize(node) and node_is_static_tensor(
72+
node.args[0], parameters_mapping
73+
)

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

Lines changed: 144 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,36 @@
66
import numpy as np
77
import 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+
)
1014
from 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
1819
from 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+
)
2230
from 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
2534
from executorch.backends.nxp.backend.ir.lib.tflite.TensorType import TensorType
2635
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
2736
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
2837
conv_2d_options,
38+
depthwise_conv_2d_options,
2939
)
3040
from torch.fx import Node
3141
from torch.nn import Parameter
@@ -48,7 +58,24 @@ 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 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+
# Separable convolution. Currently not supported.
75+
return False
76+
77+
else:
78+
# All conversion options related to the `group` attribute have been checked and none of them can be used.
5279
return False
5380

5481
if input_tensor_safe(node, 2) is None:
@@ -59,69 +86,144 @@ def _is_supported_in_IR(
5986

6087
return True
6188

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-
)
89+
Stride = Padding = Dilation = OutPadding = list[int]
90+
Transposed = bool
91+
Groups = int
7292

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-
)
78-
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:
93+
@staticmethod
94+
def _get_convolution_arguments(
95+
conv_node: Node,
96+
) -> (Stride, Padding, Dilation, Transposed, OutPadding, Groups):
97+
# The arguments of the conv are:
98+
# [x, w, b, stride, padding, dilation, transposed, output padding, groups]
99+
# https://github.com/pytorch/pytorch/blob/v2.6.0/aten/src/ATen/native/Convolution.cpp#L286-L291
100+
_, _, _, stride, padding, dilation, transposed, out_padding, groups = (
101+
conv_node.args
102+
)
103+
return stride, padding, dilation, transposed, out_padding, groups
104+
105+
# noinspection PyPep8Naming
106+
def _convert_unpadded_2D(
107+
self, t_op: tflite_model.Operator, conv_params: ConvParameters
108+
) -> conv_utils.ConvConversionResult:
109+
"""Convert the `aten.convolution` into TFLite. The `padding` and `builtin_options` must be converter by the
110+
caller.
111+
"""
112+
common.assign_2d_strides(t_op.builtin_options, conv_params.stride)
113+
common.assign_2d_dilations(t_op.builtin_options, conv_params.dilation)
114+
115+
x: tflite_model.Tensor = t_op.tmp_inputs[0]
116+
w: tflite_model.Tensor = t_op.tmp_inputs[1]
117+
y: tflite_model.Tensor = t_op.tmp_outputs[0]
118+
119+
if (b := try_get_input(t_op, 2)) is None:
84120
# Operator has no bias. Convolution aten op can omit it, TFLite can't.
85-
output_channels = weight_tensor.shape.vector[0]
121+
output_channels = w.shape.vector[0]
86122

87-
if weight_tensor.type == TensorType.FLOAT32:
123+
if w.type == TensorType.FLOAT32:
88124
bias_type = np.dtype(np.float32)
89-
elif weight_tensor.type in [TensorType.INT8, TensorType.UINT8]:
125+
elif w.type in [TensorType.INT8, TensorType.UINT8]:
90126
bias_type = np.dtype(np.int32)
91127
else:
92128
# Should never happen.
93129
raise NotImplementedError(
94-
f"Convolution node with unsupported weight type: {weight_tensor.type}"
130+
f"Convolution node with unsupported weight type: {w.type}"
95131
)
96132

97-
bias_tensor = self.builder.create_zeros_tensor(
133+
b = self.builder.create_zeros_tensor(
98134
[output_channels], "zero_bias", bias_type, True
99135
)
100136

101137
# 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)
138+
input_scale = np.array(x.quantization.scale.vector)
139+
weight_scale = np.array(w.quantization.scale.vector)
104140
bias_scale = input_scale * weight_scale
105141
bias_zero_point = np.zeros(weight_scale.shape, dtype=np.int64)
106142

107143
set_quantization_parameters_to_tensor(
108-
bias_tensor, bias_scale, bias_zero_point, quantized_dimension=0
144+
b, bias_scale, bias_zero_point, quantized_dimension=0
109145
)
110146

111147
# 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]
148+
t_op.tmp_inputs = [x, w, b]
149+
t_op.tmp_outputs = [y]
150+
151+
conversion_result = ConvConversionResult(x, w, b, y)
152+
conversion_result.ops_list.middle_op = t_op
153+
154+
return conversion_result
155+
156+
def _convert_2d_conv(
157+
self, t_op: tflite_model.Operator, conv_params: ConvParameters
158+
) -> list[tflite_model.Operator]:
159+
if conv_utils.group_conv_convertible_as_depthwise(
160+
t_op, conv_params.groups
161+
): # Convert to `DepthwiseConv2D`.
162+
t_op.builtin_options = depthwise_conv_2d_options.DepthwiseConv2D()
163+
164+
conversion_result = self._convert_unpadded_2D(t_op, conv_params)
165+
t_op.builtin_options.padding, explicit_padding = (
166+
aten_translator.convert_padding(conv_params.padding)
167+
)
168+
if explicit_padding is not None:
169+
# Need to prepend a 'Pad' operator, which adds 0s.
170+
conversion_result.ops_list.add_pre(
171+
self.builder.create_pad_operator_before(t_op, 0, explicit_padding)
172+
)
173+
174+
# DepthwiseConv2D expects weights in format [kernel_channels, kernel_height, kernel_width, output_channels]
175+
perm = [3, 1, 2, 0]
176+
weight_tensor = conversion_result.conv_weight_tensor
177+
if tensor_has_data(weight_tensor):
178+
# Transpose cloned tensor statically
179+
t_op.tmp_inputs[1] = self.builder.create_transposed_tensor(
180+
weight_tensor, perm
181+
)
182+
else:
183+
raise NotImplementedError("Dynamic Depthwise Conv weights.")
184+
185+
elif conv_utils.group_conv_convertible_into_multiple_convolutions(
186+
t_op, conv_params.groups
187+
):
188+
t_op.builtin_options = conv_2d_options.Conv2D()
189+
190+
return conv_utils.create_separated_convolutions_based_on_group(
191+
t_op,
192+
conv_params,
193+
self.builder,
194+
self._convert_unpadded_2D,
195+
conv_utils.conv_op_factory,
196+
)
197+
198+
else:
199+
# Convert to regular `Conv2D`.
200+
t_op.builtin_options = conv_2d_options.Conv2D()
201+
conversion_result = self._convert_unpadded_2D(t_op, conv_params)
202+
t_op.builtin_options.padding, explicit_padding = (
203+
aten_translator.convert_padding(conv_params.padding)
204+
)
205+
if explicit_padding is not None:
206+
# Need to prepend a 'Pad' operator, which adds 0s.
207+
conversion_result.ops_list.add_pre(
208+
self.builder.create_pad_operator_before(t_op, 0, explicit_padding)
209+
)
114210

115-
return ops.flatten()
211+
return conversion_result.ops_list.flatten()
116212

117213
def convert(self, node: Node):
118214
self.assert_convertible(node)
119215

120-
stride = node.args[3]
121-
padding = node.args[4]
122-
dilation = node.args[5]
216+
stride, padding, dilation, _, _, groups = self._get_convolution_arguments(node)
123217

124218
t_op = self._create_tflite_op_with_io_tensors(node)
125-
ops_to_add = self._convert_2d_conv(stride, padding, dilation, t_op)
219+
conv_params = ConvParameters(stride, padding, dilation, groups)
220+
221+
rank = t_op.tmp_inputs[1].shape.len()
222+
if rank == 4: # Conv2D
223+
ops_to_add = self._convert_2d_conv(t_op, conv_params)
224+
else:
225+
raise NotImplementedError(
226+
f"{rank - 2}D convolution is not supported."
227+
) # Should never get here.
126228

127229
self.builder.append_operators(ops_to_add)

0 commit comments

Comments
 (0)