Skip to content

Commit 85dfa44

Browse files
NXP backend: Add mean.dim support with new Neutron flow. (#19740)
### Summary Add `mean.dim` support with new Neutron flow. ### Test plan Unit tests provided. cc @robert-kalmar @JakeStevens @digantdesai @rascani
1 parent d83aa08 commit 85dfa44

5 files changed

Lines changed: 297 additions & 40 deletions

File tree

backends/nxp/backend/edge_helper.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ def is_no_op_on_neutron(node: Node, parameters_mapping: dict[str, Parameter]) ->
318318
input_data = torch.rand(val.shape, dtype=val.dtype) * 10 - 5
319319
args_with_random_data.append(input_data)
320320

321-
case list():
321+
case list() if any(isinstance(a, Node) for a in arg):
322322
# Lists of input nodes are not supported to keep the code simple. It is not crucial to support this
323323
# case as the affected operators are either not supported on Neutron, or are extremely unlikely to
324324
# be no-ops (e.g. GRU). One exception is `aten.cat`, which is explicitly supported above.

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,7 @@ def _get_node_args(
152152
:return: Tuple of (kernel_size, stride, padding, dilation, ceil_mode).
153153
"""
154154
kernel_size = node.args[1]
155-
stride = node.args[
156-
2
157-
] # The default value is equal to the kernel_size, so it is never empty here.
155+
stride = try_get_arg(node, 2) or kernel_size
158156
padding = try_get_arg(node, 3) or (0, 0)
159157
dilation = try_get_arg(node, 4) or (1, 1)
160158
ceil_mode = try_get_arg(node, 5) or False

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

Lines changed: 87 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
# LICENSE file in the root directory of this source tree.
55

66
import torch
7+
78
from executorch.backends.nxp.backend.data_format import NXP_NODE_FORMAT
89

910
from executorch.backends.nxp.backend.ir.converter.conversion.translator import (
1011
create_channels_last_to_channels_first_permutation,
1112
)
1213
from executorch.backends.nxp.backend.ir.converter.node_converter import (
1314
CustomDelegationOptions,
15+
is_not_qdq_node,
1416
NodeConverter,
1517
)
1618
from executorch.backends.nxp.backend.ir.converter.node_converters.shared.reduce_utils import (
@@ -21,45 +23,90 @@
2123
)
2224
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
2325
from torch.fx import Node
26+
from torch.fx.passes.infra.partitioner import Partition
2427
from torch.nn import Parameter
2528

2629

2730
class MeanDimConverter(NodeConverter):
31+
32+
@classmethod
33+
def supports_partitioning_result(
34+
cls,
35+
node: Node,
36+
partition_list: list[Partition],
37+
custom_delegation_options: CustomDelegationOptions,
38+
neutron_target_spec: NeutronTargetSpec,
39+
parameters_mapping: dict[str, Parameter],
40+
) -> bool:
41+
if custom_delegation_options.use_new_flow_neutron_c:
42+
dim, keepdim = MeanDimConverter._get_attrs(node)
43+
input_shape = node.args[0].meta["val"].shape
44+
45+
is_alone_in_partition = cls.is_node_alone_in_partition(
46+
node, partition_list, filter_fn=is_not_qdq_node
47+
)
48+
49+
if (
50+
is_alone_in_partition
51+
and keepdim
52+
and all(input_shape[d] == 1 for d in dim)
53+
):
54+
# The operator is a no-op, so the Neutron Converter will skip it. If it's the only node in the
55+
# partition, the graph would end up empty.
56+
return False
57+
58+
return True
59+
2860
@staticmethod
2961
def _is_supported_on_target(
3062
node: Node,
3163
neutron_target_spec: NeutronTargetSpec,
3264
parameters_mapping: dict[str, Parameter],
3365
custom_delegation_options: CustomDelegationOptions,
3466
) -> bool:
35-
keepdim = node.args[2] if len(node.args) >= 3 else False
36-
rank = len(node.args[0].meta["val"].shape)
37-
dim = [MeanDimConverter._to_pos_dim(d, rank) for d in node.args[1]]
67+
if custom_delegation_options.use_new_flow_neutron_c:
68+
# Requirements specified by the new Neutron flow documentation.
69+
70+
if not NodeConverter.uses_quantization_type_for_io(
71+
node,
72+
supported_types=[torch.int8, torch.uint8],
73+
input_indices=[0],
74+
output_indices=[0],
75+
):
76+
return False
3877

39-
if rank != 4 or not keepdim:
40-
# neutron-converter/src/OperatorC/GlobalAvgPoolPlugin.cpp#74-77
41-
return False
78+
return True
4279

43-
# The `mean.dim` gets converted to AveragePool by the NeutronConverter, so the channels must be a
44-
# multiple of `num_macs`.
45-
# neutron-converter/src/OperatorC/GlobalAvgPoolPlugin.cpp#59-85
46-
num_macs = neutron_target_spec.get_num_macs()
47-
channels_dim = 1 if node.meta[NXP_NODE_FORMAT].is_channels_first() else -1
48-
if (node.meta["val"].shape[channels_dim] % num_macs) != 0:
49-
return False
80+
else:
81+
# Requirements of the old Neutron flow.
82+
rank = len(node.args[0].meta["val"].shape)
83+
dim, keepdim = MeanDimConverter._get_attrs(node)
84+
dim = [MeanDimConverter._to_pos_dim(d, rank) for d in dim]
5085

51-
# Neutron only supports reduction over the spatial dimensions H, W.
52-
if node.meta[NXP_NODE_FORMAT].is_channels_first():
53-
# The input is NCHW. H and W are at indices 2 and 3.
54-
if dim not in [[2, 3], [3, 2]]:
86+
if rank != 4 or not keepdim:
87+
# neutron-converter/src/OperatorC/GlobalAvgPoolPlugin.cpp#74-77
5588
return False
56-
else:
57-
# The input is formatless. It can be considered as NHWC, as this is the way Neutron will look at
58-
# the dimensions. So H and W are the middle dimensions.
59-
if dim not in [[1, 2], [2, 1]]:
89+
90+
# The `mean.dim` gets converted to AveragePool by the NeutronConverter, so the channels must be a
91+
# multiple of `num_macs`.
92+
# neutron-converter/src/OperatorC/GlobalAvgPoolPlugin.cpp#59-85
93+
num_macs = neutron_target_spec.get_num_macs()
94+
channels_dim = 1 if node.meta[NXP_NODE_FORMAT].is_channels_first() else -1
95+
if (node.meta["val"].shape[channels_dim] % num_macs) != 0:
6096
return False
6197

62-
return True
98+
# Neutron only supports reduction over the spatial dimensions H, W.
99+
if node.meta[NXP_NODE_FORMAT].is_channels_first():
100+
# The input is NCHW. H and W are at indices 2 and 3.
101+
if dim not in [[2, 3], [3, 2]]:
102+
return False
103+
else:
104+
# The input is formatless. It can be considered as NHWC, as this is the way Neutron will look at
105+
# the dimensions. So H and W are the middle dimensions.
106+
if dim not in [[1, 2], [2, 1]]:
107+
return False
108+
109+
return True
63110

64111
@staticmethod
65112
def _is_supported_in_IR(
@@ -91,15 +138,29 @@ def _normalize_and_to_channel_last_dim(dim: list[int], rank: int) -> list[int]:
91138
perm = create_channels_last_to_channels_first_permutation(rank, True)
92139
dim = [perm[d] for d in dim]
93140

141+
# noinspection PyTypeChecker
94142
return dim
95143

96-
# Mean Dim Node format: (Tensor self, int[1]? dim, bool keepdim=False, *, ScalarType? dtype=None)
144+
@staticmethod
145+
def _get_attrs(node: Node) -> tuple[list[int], bool]:
146+
dim = node.args[1]
147+
keepdim = node.args[2] if len(node.args) >= 3 else False
148+
return dim, keepdim
149+
97150
def convert(self, node: Node):
98-
"""Convert 'mean.dim' operator to TFLite 'Mean'."""
151+
"""Convert the 'mean.dim' operator to NeutronIR 'Mean'.
152+
The ExecuTorch schema is:
153+
mean.dim(
154+
Tensor self,
155+
int[1]? dim,
156+
bool keepdim=False,
157+
*,
158+
ScalarType? dtype=None
159+
) -> Tensor
160+
"""
99161
self.assert_convertible(node)
100162

101-
dim = node.args[1]
102-
keepdim = node.args[2] if len(node.args) >= 3 else False
163+
dim, keepdim = self._get_attrs(node)
103164

104165
t_op = self._create_tflite_op_with_io_tensors(node)
105166
t_op.builtin_options = mean_options.Mean(keepdim)

0 commit comments

Comments
 (0)