Skip to content

Commit 29720ef

Browse files
committed
NXP backend: Enable cat with new Neutron flow.
1 parent 0881b22 commit 29720ef

5 files changed

Lines changed: 149 additions & 560 deletions

File tree

backends/nxp/backend/custom_delegation_options.py

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,6 @@
1111
class CustomDelegationOptions:
1212
"""The class allows the user to specify details which affect which nodes will be delegated."""
1313

14-
# Neutron requires the channel dimension to be multiple of `num_macs` for concatenation (cat op).
15-
# Due to different dim ordering in torch (channel_first) and Neutron IR (channel last), dim of the channel is
16-
# ambiguous. Cat converter will defensively require both possible dimension index for the channels to be multiple
17-
# of `num_macs`. The `force_delegate_cat` allows the user to turn off the defensive check if from the model design
18-
# it is known this constraint will be satisfied.
19-
force_delegate_cat: bool = False
20-
2114
# Proposed partitions which only contain Neutron no-ops are normally not delegated, as the NeutronConverter would
2215
# not create any NeutronGraph that can be called. This is done by the partitioner itself, and is not handled by
2316
# the individual node converters.

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

Lines changed: 14 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,7 @@
88
from executorch.backends.nxp.backend.custom_delegation_options import (
99
CustomDelegationOptions,
1010
)
11-
from executorch.backends.nxp.backend.data_format import NXP_NODE_FORMAT
12-
from executorch.backends.nxp.backend.edge_helper import previous_non_qdq_node
1311
from executorch.backends.nxp.backend.ir.converter.conversion import translator
14-
from executorch.backends.nxp.backend.ir.converter.conversion.translator import (
15-
apply_permutation_to,
16-
create_channels_first_to_channels_last_permutation,
17-
)
1812
from executorch.backends.nxp.backend.ir.converter.node_converter import (
1913
_is_dequant_node,
2014
_is_quant_node,
@@ -25,7 +19,6 @@
2519
)
2620
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
2721
from torch.fx import Node
28-
from torch.fx.passes.infra.partitioner import Partition
2922
from torch.nn import Parameter
3023

3124

@@ -83,56 +76,12 @@ def _is_supported_on_target(
8376
parameters_mapping: dict[str, Parameter],
8477
custom_delegation_options: CustomDelegationOptions,
8578
) -> bool:
86-
if custom_delegation_options.force_delegate_cat:
87-
return True
88-
89-
dim = CatConverter._get_normalized_dim(node)
90-
91-
# Neutron requires the channels to be a multiple of `num_macs`. The channels could either be the second or the
92-
# last dimension, depending on the formats of the node.
93-
if node.meta[NXP_NODE_FORMAT].is_channels_first():
94-
# During conversion to IR, the shape will be permuted to channels last, and the dimension on index
95-
# `1` will end up being the channels (last dim in NHWC).
96-
channels_index = 1
97-
to_nhwc_perm = create_channels_first_to_channels_last_permutation(
98-
len(node.meta["val"].shape), True
99-
)
100-
dim = to_nhwc_perm.index(
101-
dim
102-
) # Make sure the dim points to the NHWC dimension.
103-
else:
104-
# The shape will not be permuted during conversion, so the channels will remain the last dimension.
105-
channels_index = -1
106-
107-
input_channels = [
108-
_get_shape(input_)[channels_index] for input_ in node.all_input_nodes
109-
]
110-
output_channels = _get_shape(node)[channels_index]
111-
112-
num_macs = neutron_target_spec.get_num_macs()
113-
input_shapes = [_get_shape(input_) for input_ in node.all_input_nodes]
114-
if any((input_channel % num_macs) != 0 for input_channel in input_channels):
115-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#1492
116-
117-
# If all input shapes are equal, the neutron is able to pad the last dimension of the inputs.
118-
if not (
119-
input_shapes.count(input_shapes[0]) == len(input_shapes)
120-
and dim == len(input_shapes[0]) - 1
121-
):
122-
return False
123-
124-
if (output_channels % num_macs) != 0:
125-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#1493
126-
127-
# If all input shapes are equal, the neutron is able to pad the last dimension of the output.
128-
if not (
129-
input_shapes.count(input_shapes[0]) == len(input_shapes)
130-
and dim == len(input_shapes[0]) - 1
131-
):
132-
return False
133-
134-
if len(node.all_input_nodes) < 2: # Not supported on Neutron
135-
# TODO Try to skip the operator if this case is realistic.
79+
# `cat` uses a list of inputs as its first argument, so the indices are tuples of (0, i).
80+
input_indices = [(0, i) for i in range(len(node.args[0]))]
81+
supported_types = [torch.int8, torch.uint8]
82+
if not NodeConverter.uses_quantization_type_for_io(
83+
node, supported_types, input_indices=input_indices, output_indices=[0]
84+
):
13685
return False
13786

13887
return True
@@ -150,50 +99,14 @@ def _is_supported_in_IR(
15099

151100
return True
152101

153-
@classmethod
154-
def supports_partitioning_result(
155-
cls,
156-
node: Node,
157-
partition_list: list[Partition],
158-
custom_delegation_options: CustomDelegationOptions,
159-
neutron_target_spec: NeutronTargetSpec,
160-
parameters_mapping: dict[str, Parameter],
161-
) -> bool:
162-
# There is a bug in the NeutronConverter, where if none of the input dimensions before the one referenced by
163-
# `dim` are `!= 1`, the `Concat` is not delegated.
164-
# This only happens when the inputs to the `Concat` are model inputs, and not outputs of other
165-
# operators.
166-
cat_partition = [p for p in partition_list if node in p.nodes][0]
167-
cat_inputs = map(previous_non_qdq_node, node.args[0])
168-
169-
if not all(
170-
input_.op == "call_function" and input_ in cat_partition.nodes
171-
for input_ in cat_inputs
172-
):
173-
# Some inputs of the `cat` are NOT in the same partition as `cat`.
174-
dim = CatConverter._get_normalized_dim(node)
175-
input_shapes = [list(n.meta["val"].shape) for n in node.args[0]]
176-
if node.meta[NXP_NODE_FORMAT].is_channels_first():
177-
# Transform the shapes to channels last.
178-
to_nhwc_perm = create_channels_first_to_channels_last_permutation(
179-
len(node.meta["val"].shape), True
180-
)
181-
input_shapes = [
182-
apply_permutation_to(shape, to_nhwc_perm) for shape in input_shapes
183-
]
184-
185-
# Transform the `dim` to refer to a channels last dimension.
186-
dim = to_nhwc_perm.index(dim)
187-
188-
for input_shape in input_shapes:
189-
if not any(d != 1 for d in input_shape[:dim]):
190-
# Do not delegate if there are no "non-1" dimensions in the shape before the `dim` dimension.
191-
return False
192-
193-
return True
194-
195102
def convert(self, node: Node):
196-
"""Convert the 'aten.cat' operator to TFLite 'Concatenation'."""
103+
"""Convert the 'aten.cat' operator to NeutronIR 'Concatenation'.
104+
The ExecuTorch schema is:
105+
cat(
106+
Tensor[] tensors,
107+
int dim=0
108+
) -> Tensor
109+
"""
197110
self.assert_convertible(node)
198111

199112
t_op = self._create_tflite_op_with_io_tensors(node)
@@ -205,5 +118,5 @@ def convert(self, node: Node):
205118
t_op.tmp_inputs[0].rank
206119
)[dim]
207120

208-
t_op.builtin_options = Concatenation(dim)
121+
t_op.builtin_options = Concatenation(int(dim))
209122
self.builder.append_operators([t_op])

backends/nxp/tests/generic_tests/test_context_sensitive_delegation.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,15 @@ def test_noop_partitions__concatenate_one_tensor_and_add_zeros():
120120
)
121121

122122

123+
@pytest.mark.xfail(
124+
strict=True,
125+
reason="Neutron Converter currently supports these 2 noops in sequence.",
126+
)
123127
def test_noop_partitions__concatenate_one_tensor_and_add_zeros__forced_delegation():
128+
# When the noop `Concatenate` and noop `Add` are in sequence, Neutron Converter supports them. This edge case is
129+
# not reflected in our logic. But as this edge case is extremely rare (and even if it ever happened in a real
130+
# model, the consequences would be minimal), fixing it is not a priority.
131+
124132
input_shape = (1, 2, 3, 4)
125133
module = ConcatAddNoOpModel(input_shape)
126134

0 commit comments

Comments
 (0)