Skip to content

Commit 6cca6e6

Browse files
authored
NXP backend: added aten.slice support (#15889)
### Summary adds support for aten.slice operator ### Test plan tests can be manually run using `pytest -c /dev/null backends/nxp/tests/` cc @robert-kalmar @JakeStevens @digantdesai
1 parent 2441917 commit 6cca6e6

File tree

9 files changed

+459
-3
lines changed

9 files changed

+459
-3
lines changed

backends/nxp/backend/edge_program_converter.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
exir_ops.edge.aten.mul.Tensor: MulTensorConverter, # noqa F405
4242
exir_ops.edge.aten.permute_copy.default: PermuteCopyConverter, # noqa F405
4343
exir_ops.edge.aten.relu.default: ReLUConverter, # noqa F405
44+
exir_ops.edge.aten.slice_copy.Tensor: SliceTensorConverter, # noqa F405
4445
exir_ops.edge.aten._softmax.default: SoftmaxConverter, # noqa F405
4546
exir_ops.edge.aten.sub.Tensor: SubTensorConverter, # noqa F405
4647
exir_ops.edge.aten.tanh.default: TanhConverter, # noqa F405

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@
5656
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.sigmoid_converter import (
5757
SigmoidConverter,
5858
)
59+
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.slice_tensor_converter import (
60+
SliceTensorConverter,
61+
)
5962
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.softmax_converter import (
6063
SoftmaxConverter,
6164
)
@@ -90,6 +93,7 @@
9093
"QDQQuantizeConverter",
9194
"ReLUConverter",
9295
"SigmoidConverter",
96+
"SliceTensorConverter",
9397
"SoftmaxConverter",
9498
"SubTensorConverter",
9599
"TanhConverter",
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
# Copyright 2025 NXP
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
6+
import numpy as np
7+
from executorch.backends.nxp.backend.edge_helper import input_tensor
8+
from executorch.backends.nxp.backend.ir.converter.conversion import translator
9+
from executorch.backends.nxp.backend.ir.converter.conversion.common import OpsList
10+
from executorch.backends.nxp.backend.ir.converter.node_converter import (
11+
CustomDelegationOptions,
12+
NodeConverter,
13+
)
14+
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
15+
slice_options,
16+
)
17+
from executorch.backends.nxp.backend.neutron_operator_support import (
18+
transposition_is_supported_on_neutron,
19+
)
20+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
21+
from executorch.backends.nxp.backend.node_format import NXP_NODE_FORMAT
22+
from torch.fx import Node
23+
from torch.nn import Parameter
24+
25+
26+
class SliceTensorConverter(NodeConverter):
27+
@staticmethod
28+
def _is_supported_on_target(
29+
node: Node,
30+
neutron_target_spec: NeutronTargetSpec,
31+
parameters_mapping: dict[str, Parameter],
32+
custom_delegation_options: CustomDelegationOptions,
33+
) -> bool:
34+
# Provisional solution - slice conversion works for neutron software 2.2.1+
35+
neutron_flavor = neutron_target_spec.neutron_target.__module__.split(".")[0]
36+
if neutron_flavor != "neutron_converter_SDK_25_12":
37+
return False
38+
39+
input_shape = input_tensor(node, 0).shape
40+
dim = node.args[1]
41+
if node.args[0].meta[NXP_NODE_FORMAT].is_channels_first():
42+
dim = translator.create_channels_last_to_channels_first_permutation(
43+
len(input_shape)
44+
)[dim]
45+
input_shape = translator.apply_permutation_to(
46+
input_shape,
47+
translator.create_channels_first_to_channels_last_permutation(
48+
len(input_shape)
49+
),
50+
)
51+
input_rank = len(input_shape)
52+
53+
# Slicing is only allowed along the channel dimension.
54+
# Therefore, we must verify that Neutron supports swapping the channel dimension
55+
# with the dimension intended for slicing.
56+
if dim != -1 and dim != input_rank - 1:
57+
perm = list(range(0, input_rank))
58+
perm[dim], perm[-1] = perm[-1], perm[dim]
59+
60+
if not transposition_is_supported_on_neutron(
61+
list(input_shape), perm, neutron_target_spec
62+
):
63+
return False
64+
65+
# The shape of dimension that we want to slice must be divisible by num_macs
66+
num_macs = neutron_target_spec.get_num_macs()
67+
return input_shape[dim] % num_macs == 0
68+
69+
@staticmethod
70+
def _is_supported_in_IR(
71+
node: Node,
72+
parameters_mapping: dict[str, Parameter],
73+
custom_delegation_options: CustomDelegationOptions,
74+
) -> bool:
75+
args = node.args
76+
if len(args) != 4:
77+
return False
78+
79+
dim, start, end = SliceTensorConverter._get_clipped_slice_args(node)
80+
input_rank = len(input_tensor(node, 0).shape)
81+
82+
# Check "dim" out of bounds
83+
if dim >= input_rank or abs(dim) > input_rank:
84+
return False
85+
86+
# Check invalid combination of "start" and "end" parameters
87+
if start >= end:
88+
return False
89+
90+
return True
91+
92+
def _convert_to_slice(self, t_op, main_input, input_rank, dim, start, end) -> None:
93+
# Prepare the TFLite parameters 'begin' and 'size' tensors
94+
begin = [0] * input_rank # By default, start the slice at 0
95+
size = (
96+
main_input.shape.vector.copy()
97+
) # By default, end the slice at the end of the dimension
98+
99+
size[dim] = max(end - start, 0)
100+
begin[dim] = start
101+
102+
# We can slice only the channels dimension
103+
# So we swap the sliced dimension with the channels dimension
104+
begin[-1], begin[dim] = begin[dim], begin[-1]
105+
size[-1], size[dim] = size[dim], size[-1]
106+
107+
begin_tensor = self.builder.create_tensor_for_data(
108+
np.asarray(begin, np.int32), "begin"
109+
)
110+
size_tensor = self.builder.create_tensor_for_data(
111+
np.asarray(size, np.int32), "size"
112+
)
113+
114+
t_op.tmp_inputs = [main_input, begin_tensor, size_tensor]
115+
t_op.builtin_options = slice_options.Slice()
116+
ops = OpsList(middle_op=t_op)
117+
118+
# If slicing along non-channels dimension, we need to swap it with channels dimension.
119+
# Otherwise Neutron will not convert it.
120+
if dim != -1 and dim != input_rank - 1:
121+
# Create permutation for swapping
122+
perm = list(range(0, input_rank))
123+
perm[dim], perm[-1] = perm[-1], perm[dim]
124+
125+
# Insert forward and backward transpose
126+
ops.add_pre(self.builder.create_transpose_operator_before(t_op, 0, perm))
127+
ops.add_post(self.builder.create_transpose_operator_after(t_op, 0, perm))
128+
129+
self.builder.append_operators(ops.flatten())
130+
131+
Dim = Start = End = int
132+
133+
@staticmethod
134+
def _get_clipped_slice_args(node: Node) -> tuple[Dim, Start, End]:
135+
input_shape = input_tensor(node, 0).shape
136+
_, dim, start, end = node.args
137+
sliced_tensor_rank = input_shape[dim]
138+
139+
end = int(np.clip(end, 0, sliced_tensor_rank))
140+
start = int(np.clip(start, 0, sliced_tensor_rank))
141+
142+
return dim, start, end
143+
144+
def convert(self, node: Node):
145+
"""Convert 'slice_tensor' operator to NeutronIR 'Slice'."""
146+
self.assert_convertible(node)
147+
t_op = self._create_tflite_op_with_io_tensors(node)
148+
inputs = t_op.tmp_inputs[0]
149+
rank = inputs.rank
150+
151+
dim, start, end = self._get_clipped_slice_args(node)
152+
153+
if t_op.tmp_inputs[0].tensor_format.is_channels_last():
154+
dim = translator.create_channels_last_to_channels_first_permutation(
155+
t_op.tmp_inputs[0].rank
156+
)[dim]
157+
158+
self._convert_to_slice(t_op, inputs, rank, dim, start, end)

backends/nxp/backend/neutron_converter_manager.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@ class NeutronConverterManager:
2626
contains NeutronGraph nodes.
2727
"""
2828

29-
def __init__(self, neutron_converter_flavor: str = "SDK_25_09"):
29+
def __init__(
30+
self,
31+
neutron_converter_flavor: str = "SDK_25_09",
32+
):
3033

3134
neutron_converter_modules = [
3235
module.name
@@ -76,7 +79,9 @@ def convert(self, tflite_model: bytes, target: str) -> bytes:
7679
cctx = self.neutron_converter.CompilationContext()
7780
cctx.targetOpts = self.neutron_converter.getNeutronTarget(target)
7881
cctx.compilationOpts.minNumOpsPerGraph = 1
79-
cctx.compilationOpts.excludeGraphPasses = "MergeTranspose"
82+
cctx.compilationOpts.excludeGraphPasses = (
83+
"HoistSliceAboveTranspose,MergeTranspose"
84+
)
8085

8186
# Try to use multiprocessing for isolation, but fall back to direct execution
8287
# if the environment doesn't support it (e.g., in sandcastle/build environments)

backends/nxp/neutron_partitioner.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,9 @@ def tag_qdq_clusters(self, nodes: list[torch.fx.Node]):
211211
exir_ops.edge.aten.mul.Tensor: MulTensorConverter, # noqa F405
212212
exir_ops.edge.aten.permute_copy.default: PermuteCopyConverter, # noqa F405
213213
exir_ops.edge.aten.relu.default: ReLUConverter, # noqa F405
214-
exir_ops.edge.aten._softmax.default: SoftmaxConverter, # noqa F405
215214
exir_ops.edge.aten.sigmoid.default: SigmoidConverter, # noqa F405
215+
exir_ops.edge.aten.slice_copy.Tensor: SliceTensorConverter, # noqa F405
216+
exir_ops.edge.aten._softmax.default: SoftmaxConverter, # noqa F405
216217
exir_ops.edge.aten.sub.Tensor: SubTensorConverter, # noqa F405
217218
exir_ops.edge.aten.tanh.default: TanhConverter, # noqa F405
218219
exir_ops.edge.aten.view_copy.default: ViewCopyConverter, # noqa F405

backends/nxp/quantizer/neutron_quantizer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
ReshapePattern,
4040
SharedSpecPattern,
4141
SigmoidPattern,
42+
SliceTensorPattern,
4243
SoftMaxPattern,
4344
SubTensorPattern,
4445
TanhInPlacePattern,
@@ -216,6 +217,7 @@ def __init__(self, neutron_target_spec: NeutronTargetSpec):
216217
NeutronAtenQuantizer(ReluInPlacePattern(), static_qconfig),
217218
NeutronAtenQuantizer(ReshapePattern(), static_qconfig),
218219
NeutronAtenQuantizer(SigmoidPattern(), static_qconfig),
220+
NeutronAtenQuantizer(SliceTensorPattern(), static_qconfig),
219221
NeutronAtenQuantizer(SoftMaxPattern(), static_qconfig),
220222
NeutronAtenQuantizer(SubTensorPattern(), static_qconfig),
221223
NeutronAtenQuantizer(TanhPattern(), static_qconfig),

backends/nxp/quantizer/patterns.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,15 @@ def partition_types(self):
779779
return [torch.ops.aten.view.default]
780780

781781

782+
class SliceTensorPattern(SharedSpecPattern):
783+
"""
784+
Quantizer for Slice operator.
785+
"""
786+
787+
def partition_types(self):
788+
return [torch.ops.aten.slice.Tensor]
789+
790+
782791
class SoftMaxPattern(QuantizationPattern):
783792
"""
784793
Quantizer for Softmax operator.

0 commit comments

Comments
 (0)