Skip to content

Commit 76dfb19

Browse files
NXP backend: Add support for 1D MaxPool and remove dependency on the non-edge dialect aten.max_pool2d.default. (pytorch#17821)
### Summary Add MaxPool1D support and remove dependency on non-edge dialect `aten.max_pool2d.default`. ### Test plan Unit-tests provided.
1 parent e79bd63 commit 76dfb19

28 files changed

Lines changed: 658 additions & 462 deletions

‎backends/nxp/_passes/remove_getitem_pass.py‎

Lines changed: 0 additions & 105 deletions
This file was deleted.

‎backends/nxp/backend/edge_program_converter.py‎

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
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.
55

6+
import operator
7+
68
import executorch.backends.nxp.backend.ir.logger as logger
79
import flatbuffers
810
from executorch.backends.nxp.backend.ir.conversion_config import ConversionConfig
@@ -38,7 +40,7 @@
3840
exir_ops.edge.aten.convolution.default: ConvolutionConverter, # noqa F405
3941
exir_ops.edge.aten.hardtanh.default: HardTanhConverter, # noqa F405
4042
exir_ops.edge.aten.leaky_relu.default: LeakyReluConverter, # noqa F405
41-
exir_ops.edge.aten.max_pool2d.default: MaxPool2dConverter, # noqa F405
43+
exir_ops.edge.aten.max_pool2d_with_indices.default: MaxPool2DWithIndicesConverter, # noqa F405
4244
exir_ops.edge.aten.mean.dim: MeanDimConverter, # noqa F405
4345
exir_ops.edge.aten.mm.default: MMConverter, # noqa F405
4446
exir_ops.edge.aten.mul.Tensor: MulTensorConverter, # noqa F405
@@ -57,6 +59,9 @@
5759
}
5860

5961

62+
NXP_PROCESSED_TAG = "NXP_PROCESSED_TAG"
63+
64+
6065
class EdgeProgramToIRConverter:
6166
"""
6267
Converter from convertion of ExportedProgram in Edge dialect to IR (TFLite Flatbuffers).
@@ -159,6 +164,11 @@ def _process_nodes(self, nodes: list[Node], conversion_context: ConversionContex
159164
if node.target in qdq_related_functions and "cluster" in node.meta:
160165
# Skip (De)Quantize nodes that were already processed
161166
pass
167+
elif node.target == operator.getitem and node.meta.get(
168+
NXP_PROCESSED_TAG, False
169+
):
170+
# The node was already processed alongside the Q/DQ ops.
171+
pass
162172
elif node.target in functions_converters:
163173
functions_converters[node.target](conversion_context).convert(node)
164174
else:
@@ -264,9 +274,8 @@ def build_conversion_context(
264274
def _convert_qdq_cluster_q_dq_nodes(
265275
self, nodes: list[Node], conversion_context: ConversionContext
266276
):
267-
"""
268-
Go through program and convert De(Quantize) nodes that are part of the QDQ cluster into
269-
tensors.
277+
"""Go through the program and convert [De]Quantize nodes that are part of a QDQ cluster into tensors.
278+
Also convert related `GetItem` nodes to NO-OPs, which just propagate the quantization.
270279
271280
:param nodes: Program's nodes.
272281
:param conversion_context: ConversionContext instance.
@@ -285,3 +294,14 @@ def _convert_qdq_cluster_q_dq_nodes(
285294
and part_of_qdq_cluster
286295
):
287296
qdq_q_ops_converters[node.target](conversion_context).convert(node)
297+
298+
# Usually, `getitem` nodes are a part of a "foreign" QDQ cluster. They consume the output of the main compute
299+
# operator, and they are followed by a `Quantize` operator, which specifies the output quantization parameters
300+
# of the cluster. So the input of the `GetItem` is float32, and the output is quantized. Due to how the Neutron
301+
# IR represents quantization, the quantization parameters must be propagated from the output to the input.
302+
for node in nodes:
303+
if node.target == operator.getitem:
304+
# Convert the builtin function into a "NO-OP" in the IR, and propagate the quantization parameters in
305+
# reverse.
306+
GetItemConverter(conversion_context).convert(node) # noqa: F405
307+
node.meta[NXP_PROCESSED_TAG] = True

‎backends/nxp/backend/ir/converter/builder/aten_model_builder_director.py‎

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@
88
ModelBuilder,
99
)
1010
from executorch.backends.nxp.backend.ir.converter.conversion import translator
11+
12+
from executorch.backends.nxp.backend.ir.converter.tensor_utils import (
13+
get_name_of_node_output,
14+
)
1115
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
1216
from torch.fx import Node
1317
from torch.nn import Parameter
@@ -30,19 +34,26 @@ def append_as_fake_tensor(self, node: Node, node_format: DataFormat):
3034
if self.tensor_exists(node.name):
3135
return
3236

33-
tensor = node.meta["val"]
34-
if isinstance(tensor, tuple):
35-
tensor = tensor[0] # Fake tensor
36-
_type = translator.convert_data_type(tensor.dtype)
37-
shape = list(tensor.shape)
37+
def _append_tensor(tensor_, name=None):
38+
type_ = translator.convert_data_type(tensor_.dtype)
39+
shape = list(tensor_.shape)
3840

39-
if node_format.is_channels_first():
40-
shape = translator.dims_to_channels_last(shape)
41+
if node_format.is_channels_first():
42+
shape = translator.dims_to_channels_last(shape)
4143

42-
tensor = self.create_empty_tensor(node.name, _type, shape)
43-
tensor.tensor_format = DataFormat.convert_executorch_format_to_neutron(
44-
node_format
45-
)
44+
tensor = self.create_empty_tensor(name or node.name, type_, shape)
45+
tensor.tensor_format = DataFormat.convert_executorch_format_to_neutron(
46+
node_format
47+
)
48+
49+
tensor_or_tuple = node.meta["val"]
50+
if isinstance(tensor_or_tuple, tuple):
51+
# The `node` can produce multiple output tensors, which are represented using this tuple.
52+
for i, t in enumerate(tensor_or_tuple):
53+
_append_tensor(t, get_name_of_node_output(node, i))
54+
55+
else:
56+
_append_tensor(tensor_or_tuple)
4657

4758
def append_as_static_tensor(
4859
self, node: Node, node_format: DataFormat, tensor: Parameter

‎backends/nxp/backend/ir/converter/builder/quantization_verification.py‎

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -86,13 +86,23 @@ def valid(self, op: tflite_model.Operator) -> bool:
8686
first_quantization = shared_tensors[0].quantization
8787

8888
# Check quantization values (scales & zero-points)
89-
scales_same = all(
90-
first_quantization.scale == t.quantization.scale for t in shared_tensors[1:]
91-
)
92-
zp_same = all(
93-
first_quantization.zero_point == t.quantization.zero_point
94-
for t in shared_tensors[1:]
95-
)
89+
try:
90+
scales_same = all(
91+
first_quantization.scale == t.quantization.scale
92+
for t in shared_tensors[1:]
93+
)
94+
zp_same = all(
95+
first_quantization.zero_point == t.quantization.zero_point
96+
for t in shared_tensors[1:]
97+
)
98+
except AttributeError:
99+
# Common error when one of the tensors is not quantized.
100+
logger.w(
101+
f"NXP backend: The Neutron IR operator {op.builtin_options} is not quantized correctly. "
102+
"Please report this."
103+
)
104+
return False
105+
96106
return scales_same and zp_same
97107

98108
def __str__(self):

‎backends/nxp/backend/ir/converter/conversion/common.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#
22
# Copyright 2023 Martin Pavella
3-
# Copyright 2023-2025 NXP
3+
# Copyright 2023-2026 NXP
44
#
55
# License: MIT
66
# See the LICENSE_MIT for more details.
@@ -12,7 +12,7 @@
1212
'conversion/builtin/' directory.
1313
"""
1414

15-
from typing import List, MutableSequence, Optional
15+
from typing import List, MutableSequence, Optional, Sequence
1616

1717
import executorch.backends.nxp.backend.ir.logger as logger
1818
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
@@ -71,7 +71,7 @@ def extend_1d_dilation_to_2d(tflite_1d_dilation: MutableSequence):
7171
)
7272

7373

74-
def assign_2d_strides(options: StridedOptions, strides: Optional[List[int]]):
74+
def assign_2d_strides(options: StridedOptions, strides: Sequence[int] | None):
7575
"""Assign to 'obj' the attributes 'stride_h' and 'stride_w' from 'strides'.
7676
If 'strides' is None, assign 1s.
7777

‎backends/nxp/backend/ir/converter/node_converter.py‎

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
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.
55

6+
import operator
67
from abc import ABC, abstractmethod
78

89
import torch
@@ -14,6 +15,10 @@
1415
from executorch.backends.nxp.backend.ir.converter.builder.aten_model_builder_director import (
1516
AtenModelBuilderDirector,
1617
)
18+
19+
from executorch.backends.nxp.backend.ir.converter.tensor_utils import (
20+
get_name_of_node_output,
21+
)
1722
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
1823
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
1924
from executorch.exir.dialects._ops import ops as exir_ops
@@ -231,25 +236,45 @@ def _create_tflite_op_with_io_tensors(self, node: Node) -> tflite_model.Operator
231236
# Initialize node's inputs
232237
t_operator.inputs = tflite_model.OperatorInputs()
233238

234-
input_nodes = []
235-
for arg in node.args:
236-
match arg:
237-
case Node():
238-
input_nodes.append(arg)
239-
case list() if all(isinstance(node_, Node) for node_ in arg):
240-
input_nodes.extend(arg)
241-
242-
for ancestor_node in input_nodes:
243-
assert self.context.tflite_builder.tensor_exists(ancestor_node.name)
244-
t_operator.tmp_inputs.append(
245-
self.context.tflite_builder.tensor_for_name(ancestor_node.name)
246-
)
247-
248-
# Add node's output as a new tensor
249-
assert self.context.tflite_builder.tensor_exists(node.name)
250-
t_operator.outputs = tflite_model.OperatorOutputs()
251-
t_operator.tmp_outputs.append(
252-
self.context.tflite_builder.tensor_for_name(node.name)
239+
if node.target == operator.getitem:
240+
# Special case of a builtin function, which can extract a specific output tensor from the previous node.
241+
previous_node = node.args[0]
242+
output_index = node.args[1]
243+
input_name = get_name_of_node_output(previous_node, output_index)
244+
assert self.builder.tensor_exists(input_name)
245+
t_operator.tmp_inputs.append(self.builder.tensor_for_name(input_name))
246+
247+
else:
248+
# Regular operator.
249+
input_nodes = []
250+
for arg in node.args:
251+
match arg:
252+
case Node():
253+
input_nodes.append(arg)
254+
case list() if all(isinstance(node_, Node) for node_ in arg):
255+
input_nodes.extend(arg)
256+
257+
for ancestor_node in input_nodes:
258+
assert self.builder.tensor_exists(ancestor_node.name)
259+
t_operator.tmp_inputs.append(
260+
self.builder.tensor_for_name(ancestor_node.name)
261+
)
262+
263+
# Add node's outputs as a new tensors
264+
num_outputs = (
265+
len(node.meta["val"]) if isinstance(node.meta["val"], tuple) else 1
253266
)
267+
if num_outputs == 1:
268+
# Single output node.
269+
assert self.builder.tensor_exists(node.name)
270+
t_operator.outputs = tflite_model.OperatorOutputs()
271+
t_operator.tmp_outputs.append(self.builder.tensor_for_name(node.name))
272+
else:
273+
# The node has multiple outputs.
274+
t_operator.outputs = tflite_model.OperatorOutputs()
275+
for output_index in range(num_outputs):
276+
tensor_name = get_name_of_node_output(node, output_index)
277+
assert self.builder.tensor_exists(tensor_name)
278+
t_operator.tmp_outputs.append(self.builder.tensor_for_name(tensor_name))
254279

255280
return t_operator

‎backends/nxp/backend/ir/converter/node_converters/ops_converters/__init__.py‎

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,17 @@
2828
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.convolution_converter import (
2929
ConvolutionConverter,
3030
)
31+
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.getitem_converter import (
32+
GetItemConverter,
33+
)
3134
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.hardtanh_converter import (
3235
HardTanhConverter,
3336
)
3437
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.leaky_relu_converter import (
3538
LeakyReluConverter,
3639
)
37-
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.max_pool_2d_converter import (
38-
MaxPool2dConverter,
40+
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.max_pool2d_with_indices_converter import (
41+
MaxPool2DWithIndicesConverter,
3942
)
4043
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.mean_dim_converter import (
4144
MeanDimConverter,
@@ -101,9 +104,10 @@
101104
"CloneConverter",
102105
"ConstantPadNDConverter",
103106
"ConvolutionConverter",
107+
"GetItemConverter",
104108
"HardTanhConverter",
105109
"LeakyReluConverter",
106-
"MaxPool2dConverter",
110+
"MaxPool2DWithIndicesConverter",
107111
"MeanDimConverter",
108112
"MMConverter",
109113
"MulTensorConverter",

0 commit comments

Comments
 (0)