Skip to content

Commit 0dbaed4

Browse files
NXP backend: Resolve mean.dim format related issue. (#20218)
### Summary The existing `mean.dim` support in NXP backend failed to consider the format related implications of the operators dimension reducing ability". This PR correctly identifies these edge cases and handles conversion to Neutron IR accordingly. ### Test plan Unit tests provided. cc @robert-kalmar @JakeStevens @digantdesai @rascani
1 parent ab45eb6 commit 0dbaed4

4 files changed

Lines changed: 417 additions & 100 deletions

File tree

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

Lines changed: 120 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@
55

66
import torch
77

8+
from executorch.backends.nxp.backend.data_format import DataFormat
9+
from executorch.backends.nxp.backend.ir.converter.conversion import translator
10+
from executorch.backends.nxp.backend.ir.converter.conversion.common import OpsList
811
from executorch.backends.nxp.backend.ir.converter.conversion.translator import (
912
create_channels_last_to_channels_first_permutation,
1013
)
@@ -89,10 +92,15 @@ def _is_supported_in_IR(
8992
def _to_pos_dim(d: int, rank: int):
9093
return d + rank if d < 0 else d
9194

95+
@staticmethod
96+
def _normalize_dim(dim: list[int], rank: int) -> list[int]:
97+
# convert negative index to positive
98+
return [MeanDimConverter._to_pos_dim(d, rank) for d in dim]
99+
92100
@staticmethod
93101
def _normalize_and_to_channel_last_dim(dim: list[int], rank: int) -> list[int]:
94102
# convert negative index to positive
95-
dim = [MeanDimConverter._to_pos_dim(d, rank) for d in dim]
103+
dim = MeanDimConverter._normalize_dim(dim, rank)
96104

97105
perm = create_channels_last_to_channels_first_permutation(rank, True)
98106
dim = [perm[d] for d in dim]
@@ -106,6 +114,114 @@ def _get_attrs(node: Node) -> tuple[list[int], bool]:
106114
keepdim = node.args[2] if len(node.args) >= 3 else False
107115
return dim, keepdim
108116

117+
def _get_dim_and_handle_io_formats(
118+
self, ops: OpsList, dim: list[int], keep_dim: bool
119+
):
120+
t_op = ops.middle_op
121+
x = t_op.tmp_inputs[0]
122+
y = t_op.tmp_outputs[0]
123+
124+
channels_last_input = x.tensor_format.is_channels_last()
125+
channels_last_output = y.tensor_format.is_channels_last()
126+
formatless_input = not channels_last_input
127+
formatless_output = not channels_last_output
128+
129+
dim = self._normalize_dim(dim, x.rank)
130+
131+
if keep_dim:
132+
# The rank is preserved and the io formats should always be equal.
133+
assert (
134+
x.tensor_format == y.tensor_format
135+
), "NXP backend: There is a bug in `mean.dim` format inference."
136+
137+
# Just adjust the dim to match the input format.
138+
if channels_last_input:
139+
dim = self._normalize_and_to_channel_last_dim(dim, x.rank)
140+
141+
else:
142+
# `keep_dim = False`, so the output rank != input rank, and the operator changes the tensor format.
143+
144+
if channels_last_input and formatless_output:
145+
if 1 in dim:
146+
# If we are reducing over the channels, the channels dimension gets removed and the output ends up
147+
# exactly equal in channels last and channels first, regardless of which other dimensions are
148+
# removed. Therefore, we can just adjust the `dim` and we don't need to insert any `Transpose` ops.
149+
dim = self._normalize_and_to_channel_last_dim(dim, x.rank)
150+
elif all(spatial_dim in dim for spatial_dim in range(2, x.rank)):
151+
# All spatial dims are reduced, leaving only batch and channels (both optionally). So the result is
152+
# equal in channels first and channels last as long as we adjust the `dim` to match a channels last
153+
# input (similarly to the case above).
154+
dim = self._normalize_and_to_channel_last_dim(dim, x.rank)
155+
else:
156+
# If the channels dimension is preserved, we must transpose the input to channels first (to match
157+
# the edge model) and we must keep the `dim` unchanged (referencing channels first dimensions).
158+
# Otherwise, the output would not match the input.
159+
to_channels_first_perm = (
160+
translator.create_channels_last_to_channels_first_permutation(
161+
x.rank
162+
)
163+
)
164+
ops.add_pre(
165+
self.builder.create_transpose_operator_before(
166+
t_op, 0, to_channels_first_perm
167+
)
168+
)
169+
t_op.tmp_inputs[0].tensor_format = DataFormat.CHANNELS_FIRST
170+
171+
elif formatless_input and channels_last_output:
172+
# We need apply the `mean` with the original `dim`, which will produce a channels first output. Then,
173+
# we need to append a `Transpose` operator to make the output channels last.
174+
to_channels_last_perm = (
175+
translator.create_channels_first_to_channels_last_permutation(
176+
y.rank, True
177+
)
178+
)
179+
ops.add_post(
180+
self.builder.create_transpose_operator_after(
181+
t_op, 0, to_channels_last_perm
182+
)
183+
)
184+
t_op.tmp_outputs[0].tensor_format = DataFormat.CHANNELS_FIRST
185+
186+
elif formatless_input and formatless_output:
187+
# No action needed.
188+
pass
189+
190+
else: # channels_last_input and channels_last_output
191+
# This case cannot currently occur, as it would require the case:
192+
# channels last 4D -> mean -> channels_last 3D
193+
# which cannot currently happen as the 3D conv/pooling/... is supported by adding `view_copy` nodes in
194+
# the edge dialect and converting the node to 4D, and the `view_copy` nodes prevent the propagation of
195+
# the format to the `mean.dim` output.
196+
# Therefore, the implementation cannot be tested. But from experience with other operators, it should
197+
# work correctly. We just need to add 2 `Transpose` ops to make the IO channels first, and keep the
198+
# `dim` unchanged.
199+
to_channels_first_perm = (
200+
translator.create_channels_last_to_channels_first_permutation(
201+
x.rank
202+
)
203+
)
204+
ops.add_pre(
205+
self.builder.create_transpose_operator_before(
206+
t_op, 0, to_channels_first_perm
207+
)
208+
)
209+
t_op.tmp_inputs[0].tensor_format = DataFormat.CHANNELS_FIRST
210+
211+
to_channels_last_perm = (
212+
translator.create_channels_first_to_channels_last_permutation(
213+
y.rank, True
214+
)
215+
)
216+
ops.add_post(
217+
self.builder.create_transpose_operator_after(
218+
t_op, 0, to_channels_last_perm
219+
)
220+
)
221+
t_op.tmp_outputs[0].tensor_format = DataFormat.CHANNELS_FIRST
222+
223+
return dim
224+
109225
def convert(self, node: Node):
110226
"""Convert the 'mean.dim' operator to NeutronIR 'Mean'.
111227
The ExecuTorch schema is:
@@ -123,10 +239,9 @@ def convert(self, node: Node):
123239

124240
t_op = self._create_tflite_op_with_io_tensors(node)
125241
t_op.builtin_options = mean_options.Mean(keepdim)
126-
x = t_op.tmp_inputs[0]
127242

128-
if x.tensor_format.is_channels_last():
129-
dim = self._normalize_and_to_channel_last_dim(dim, x.rank)
243+
ops = OpsList(middle_op=t_op)
244+
dim = self._get_dim_and_handle_io_formats(ops, dim, keepdim)
130245

131246
convert_axes_from_attribute(t_op, self.builder, dim)
132-
self.builder.append_operators([t_op])
247+
self.builder.append_operators(ops.flatten())

backends/nxp/backend/node_format_inference.py

Lines changed: 53 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,27 @@
99
import torch
1010

1111
from executorch.backends.nxp.backend.data_format import DataFormat, NXP_NODE_FORMAT
12-
13-
from executorch.backends.nxp.backend.edge_helper import is_channels_last_dim_order
12+
from executorch.backends.nxp.backend.edge_helper import (
13+
is_channels_last_dim_order,
14+
try_get_arg,
15+
)
1416
from executorch.backends.nxp.backend.edge_program_converter import functions_converters
15-
from executorch.exir.dialects._ops import ops as exir_ops
17+
from executorch.backends.nxp.tests.ops_aliases import (
18+
AdaptiveAvgPool2D,
19+
AvgPool2D,
20+
Convolution,
21+
DequantizePerChannel,
22+
DequantizePerTensor,
23+
GetItem,
24+
MaxPool2D,
25+
MaxPool2DWithIndices,
26+
MeanDim,
27+
PermuteCopy,
28+
QuantizePerTensor,
29+
UpsampleBilinear2D,
30+
UpsampleNearest2D,
31+
ViewCopy,
32+
)
1633
from executorch.exir.dialects.edge._ops import EdgeOpOverload
1734
from torch.export import ExportedProgram
1835
from torch.fx import Node
@@ -25,21 +42,22 @@ class NodeFormatInference:
2542
# The op in the dictionary is mapped to a dictionary, which holds indices to input nodes
2643
# that are always channels first.
2744
ops_with_channels_first_nodes = {
28-
exir_ops.edge.aten._adaptive_avg_pool2d.default: {"inputs": [0]},
45+
AdaptiveAvgPool2D: {"inputs": [0]},
2946
torch.ops.aten.adaptive_avg_pool2d.default: {"inputs": [0]},
30-
exir_ops.edge.aten.avg_pool2d.default: {"inputs": [0]},
31-
exir_ops.edge.aten.convolution.default: {"inputs": [0, 1]},
32-
exir_ops.edge.aten.max_pool2d_with_indices.default: {"inputs": [0]},
33-
exir_ops.edge.aten.max_pool2d.default: {"inputs": [0]},
34-
exir_ops.edge.aten.upsample_bilinear2d.vec: {"inputs": [0]},
35-
exir_ops.edge.aten.upsample_nearest2d.vec: {"inputs": [0]},
47+
AvgPool2D: {"inputs": [0]},
48+
Convolution: {"inputs": [0, 1]},
49+
MaxPool2DWithIndices: {"inputs": [0]},
50+
MaxPool2D: {"inputs": [0]},
51+
UpsampleBilinear2D: {"inputs": [0]},
52+
UpsampleNearest2D: {"inputs": [0]},
3653
}
3754

3855
# A set of Edge Aten ops, which have the ability to change the format (for example - input nodes
3956
# are channels first but output is formatless).
4057
ops_that_can_change_tensor_format = {
41-
exir_ops.edge.aten.view_copy.default,
42-
exir_ops.edge.aten.permute_copy.default,
58+
ViewCopy,
59+
PermuteCopy,
60+
MeanDim,
4361
}
4462

4563
_type_changed_during_last_run: bool
@@ -71,10 +89,10 @@ def __init__(self, edge_program: ExportedProgram, only_for_op_support_check=Fals
7189
self._type_changed_during_last_run = False
7290

7391
self._known_targets = list(functions_converters) + [
74-
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
75-
exir_ops.edge.quantized_decomposed.dequantize_per_channel.default,
76-
exir_ops.edge.quantized_decomposed.quantize_per_tensor.default,
77-
operator.getitem,
92+
DequantizePerTensor,
93+
DequantizePerChannel,
94+
QuantizePerTensor,
95+
GetItem,
7896
]
7997

8098
def identify_node_formats(self):
@@ -104,10 +122,7 @@ def _infer_format_of_nodes(self, node: Node):
104122
self._handle_node_which_uses_channels_first_format(node)
105123

106124
elif op_type in self.ops_that_can_change_tensor_format:
107-
if op_type in [
108-
exir_ops.edge.aten.view_copy.default,
109-
exir_ops.edge.aten.permute_copy.default,
110-
]:
125+
if op_type in [ViewCopy, PermuteCopy]:
111126
# Try to assign the `formatless` format to the input and output. The converter will then handle the
112127
# transition.
113128
# Note: If the format for the input/output has already been assigned as channels first, it will NOT be
@@ -119,10 +134,28 @@ def _infer_format_of_nodes(self, node: Node):
119134
self._node_inputs[node][0], DataFormat.FORMATLESS
120135
)
121136

137+
elif op_type == MeanDim:
138+
# The operator schema is:
139+
# mean.dim(Tensor self, int[1]? dim, bool keepdim=False, *, ScalarType? dtype=None) -> Tensor
140+
keep_dim = try_get_arg(node, 2) or False
141+
if keep_dim:
142+
# The operator preserves the rank, so we can handle it as an operator that can use any node format.
143+
self._handle_node_which_can_use_any_node_format(node)
144+
else:
145+
# The operator removes dimensions, so the IO must be marked as `formatless` (unless overridden by
146+
# channels first of course).
147+
self._assign_format_to_node(
148+
self._node_outputs[node][0], DataFormat.FORMATLESS
149+
)
150+
self._assign_format_to_node(
151+
self._node_inputs[node][0], DataFormat.FORMATLESS
152+
)
153+
122154
else:
123155
logger.error(
124156
f"Node format inference for node type: {op_type} not found!"
125157
)
158+
126159
elif node.op != "call_function" or (
127160
hasattr(node, "target") and node.target in self._known_targets
128161
):

0 commit comments

Comments
 (0)