Skip to content

Commit 420ab94

Browse files
committed
feat: added support for abs using new Neutron flow
1 parent a58fcf1 commit 420ab94

4 files changed

Lines changed: 136 additions & 29 deletions

File tree

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

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
1-
# Copyright 2025 NXP
1+
# Copyright 2025-2026 NXP
22
#
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

66

7+
import torch
8+
79
from executorch.backends.nxp.backend.ir.converter.node_converter import (
810
CustomDelegationOptions,
11+
NeutronTargetSpec,
912
NodeConverter,
1013
)
1114
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
@@ -25,6 +28,25 @@ def _is_supported_in_IR(
2528
) -> bool:
2629
return True
2730

31+
@staticmethod
32+
def _is_supported_on_target(
33+
node: Node,
34+
neutron_target_spec: NeutronTargetSpec,
35+
parameters_mapping: dict[str, Parameter],
36+
custom_delegation_options: CustomDelegationOptions,
37+
) -> bool:
38+
39+
if custom_delegation_options.use_new_flow_neutron_c:
40+
# Requirements specified by the new Neutron flow documentation.
41+
42+
supported_types = [torch.int8, torch.uint8]
43+
if not NodeConverter.uses_quantization_type_for_io(
44+
node, supported_types, [0], None
45+
):
46+
return False
47+
48+
return True
49+
2850
def convert(self, node: Node):
2951
"""Convert 'aten::abs' operator to TFLite 'Abs'."""
3052
self.assert_convertible(node)

backends/nxp/tests/dataset_creator.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,8 +45,10 @@ def generate_samples(
4545
class RandomDatasetCreator(DatasetCreator):
4646
"""Dataset creator that generates random input samples."""
4747

48-
def __init__(self, num_samples=2):
48+
def __init__(self, num_samples=2, low=0.0, high=1.0):
4949
self._num_samples = num_samples
50+
self.low = low
51+
self.high = high
5052

5153
def generate_samples(
5254
self, dataset_dir: str, input_spec: list[ModelInputSpec]
@@ -103,9 +105,11 @@ def _gen_samples(
103105
case _:
104106
raise ValueError(f"Unsupported dim_order: {spec.dim_order}")
105107

106-
sample_vector = rng.random(
107-
np.prod(shape), torch_type_to_numpy_type(spec.dtype)
108-
).reshape(shape)
108+
sample_vector = (
109+
rng.uniform(self.low, self.high, size=np.prod(shape))
110+
.astype(torch_type_to_numpy_type(spec.dtype))
111+
.reshape(shape)
112+
)
109113
file_name = (
110114
f"{str(spec_idx).zfill(2)}.bin"
111115
if len(input_spec) > 1

backends/nxp/tests/ir/converter/node_converter/test_abs_converter.py

Lines changed: 102 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
# Copyright 2025 NXP
1+
# Copyright 2025-2026 NXP
22
#
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

66
import numpy as np
77
import pytest
88
import torch
9-
109
from executorch.backends.nxp.backend.edge_program_converter import (
1110
EdgeProgramToIRConverter,
1211
)
@@ -17,6 +16,13 @@
1716
ToChannelFirstPreprocess,
1817
ToChannelLastPreprocess,
1918
)
19+
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
20+
21+
from executorch.backends.nxp.tests.nsys_testing import (
22+
lower_run_compare,
23+
RandomDatasetCreator,
24+
)
25+
from executorch.backends.nxp.tests.ops_aliases import Abs, Convolution, Relu
2026

2127
from executorch.exir.dialects._ops import ops as exir_ops
2228
from torch.export import ExportedProgram
@@ -29,7 +35,7 @@ def reseed_model_per_test_run():
2935
np.random.seed(23)
3036

3137

32-
class ConvBlocksWithAbs(torch.nn.Module):
38+
class ConvBlocksWithAbsModule(torch.nn.Module):
3339
def __init__(self, conv_in_channels: int = 3):
3440
super().__init__()
3541
self.block1 = torch.nn.Sequential(
@@ -56,36 +62,108 @@ def forward(self, x):
5662
return self.block2(x)
5763

5864

59-
class Abs(torch.nn.Module):
65+
class AbsModule(torch.nn.Module):
6066
def __init__(self):
6167
super().__init__()
6268

6369
def forward(self, x):
6470
return x.abs()
6571

6672

67-
def test_conv_abs(mocker, use_qat, input_shape: tuple[int] = (1, 3, 112, 112)):
68-
model = ConvBlocksWithAbs(conv_in_channels=input_shape[1])
73+
class TestAbsLegacyNeutronFlow:
74+
def test_conv_abs(
75+
self, mocker, use_qat, input_shape: tuple[int, ...] = (1, 3, 112, 112)
76+
):
77+
model = ConvBlocksWithAbsModule(conv_in_channels=input_shape[1])
6978

70-
converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program")
79+
converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program")
7180

72-
quantized_program = to_quantized_edge_program(
73-
model, input_shape, use_qat=use_qat, use_neutron_for_format_conversion=False
74-
).exported_program()
81+
quantized_program = to_quantized_edge_program(
82+
model,
83+
input_shape,
84+
use_qat=use_qat,
85+
use_neutron_for_format_conversion=False,
86+
use_new_flow_neutron_c=False,
87+
).exported_program()
7588

76-
tflite_flatbuffers_model, io_formats = converter_spy.spy_return
77-
exported_program: ExportedProgram = converter_spy.call_args.args[1]
89+
tflite_flatbuffers_model, io_formats = converter_spy.spy_return
90+
exported_program: ExportedProgram = converter_spy.call_args.args[1]
7891

79-
assert not graph_contains_any_of_ops(
80-
graph=quantized_program.graph, ops=[exir_ops.edge.aten.abs.default]
81-
)
92+
assert not graph_contains_any_of_ops(
93+
graph=quantized_program.graph, ops=[exir_ops.edge.aten.abs.default]
94+
)
95+
96+
input_data = (np.random.random(input_shape) * 50).astype(np.int8)
97+
convert_run_compare(
98+
exported_program,
99+
tfl_model=tflite_flatbuffers_model,
100+
tflite_input_preprocess=ToChannelLastPreprocess(),
101+
tflite_output_preprocess=ToChannelFirstPreprocess(),
102+
input_data=input_data,
103+
atol=1.0,
104+
)
105+
106+
107+
class TestAbsNewNeutronFlow:
108+
@staticmethod
109+
def _get_dataset_creator():
110+
# to test `abs` reliably, we need to include negative values
111+
low = -255.0
112+
high = 255.0
113+
114+
dataset = RandomDatasetCreator(low=low, high=high)
115+
return dataset
116+
117+
def test__basic_nsys_inference(self, mocker):
118+
input_shape = (2, 3, 6, 7)
119+
model = AbsModule()
120+
graph_verifier = DetailedGraphVerifier(
121+
mocker, expected_delegated_ops={Abs: 1}, expected_non_delegated_ops={}
122+
)
82123

83-
input_data = (np.random.random(input_shape) * 50).astype(np.int8)
84-
convert_run_compare(
85-
exported_program,
86-
tfl_model=tflite_flatbuffers_model,
87-
tflite_input_preprocess=ToChannelLastPreprocess(),
88-
tflite_output_preprocess=ToChannelFirstPreprocess(),
89-
input_data=input_data,
90-
atol=1.0,
91-
)
124+
dataset_creator = self._get_dataset_creator()
125+
lower_run_compare(
126+
model,
127+
input_shape,
128+
graph_verifier,
129+
dataset_creator,
130+
use_new_flow_neutron_c=True,
131+
)
132+
133+
def test__basic_nsys_inference__big(self, mocker):
134+
# some operators have delegation requirement that size must be < 4096
135+
input_shape = (4097, 1)
136+
model = AbsModule()
137+
graph_verifier = DetailedGraphVerifier(
138+
mocker, expected_delegated_ops={Abs: 1}, expected_non_delegated_ops={}
139+
)
140+
141+
dataset_creator = self._get_dataset_creator()
142+
lower_run_compare(
143+
model,
144+
input_shape,
145+
graph_verifier,
146+
dataset_creator,
147+
use_new_flow_neutron_c=True,
148+
)
149+
150+
def test_basic_nsys_inference__with_conv(self, mocker):
151+
input_shape = (2, 3, 6, 7)
152+
in_channels = input_shape[1]
153+
model = ConvBlocksWithAbsModule(conv_in_channels=in_channels)
154+
155+
# one `relu` ends up in the same delegated partition as `abs`
156+
graph_verifier = DetailedGraphVerifier(
157+
mocker,
158+
expected_delegated_ops={Abs: 1, Relu: 1},
159+
expected_non_delegated_ops={Relu: 1, Convolution: 2},
160+
)
161+
162+
dataset_creator = self._get_dataset_creator()
163+
lower_run_compare(
164+
model,
165+
input_shape,
166+
graph_verifier,
167+
dataset_creator,
168+
use_new_flow_neutron_c=True,
169+
)

backends/nxp/tests/ops_aliases.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@
1111
import torch
1212
from executorch.exir.dialects._ops import ops as exir_ops
1313

14+
Abs = exir_ops.edge.aten.abs.default
1415
AvgPool2D = exir_ops.edge.aten.avg_pool2d.default
1516
Bmm = exir_ops.edge.aten.bmm.default
17+
Convolution = exir_ops.edge.aten.convolution.default
1618
DequantizePerChannel = exir_ops.edge.quantized_decomposed.dequantize_per_channel.default
1719
DequantizePerTensor = exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default
1820
ExecutorchDelegateCall = torch.ops.higher_order.executorch_call_delegate
@@ -28,6 +30,7 @@
2830
Squeeze = exir_ops.edge.aten.squeeze.default
2931
SqueezeDim = exir_ops.edge.aten.squeeze.dim
3032
SqueezeDims = exir_ops.edge.aten.squeeze.dims
33+
Relu = exir_ops.edge.aten.relu.default
3134
Unsqueeze = exir_ops.edge.aten.unsqueeze.default
3235
UpsampleBilinear2D = exir_ops.edge.aten.upsample_bilinear2d.vec
3336
UpsampleNearest2D = exir_ops.edge.aten.upsample_nearest2d.vec

0 commit comments

Comments
 (0)