Skip to content

Commit d163471

Browse files
Test Add Tensor with new Neutron flow
1 parent 920b493 commit d163471

4 files changed

Lines changed: 232 additions & 18 deletions

File tree

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

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
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

6+
import torch
7+
8+
from executorch.backends.nxp.backend.data_format import NXP_NODE_FORMAT
69
from executorch.backends.nxp.backend.ir.converter.node_converter import (
710
CustomDelegationOptions,
811
NodeConverter,
@@ -23,11 +26,33 @@ def _is_supported_on_target(
2326
parameters_mapping: dict[str, Parameter],
2427
custom_delegation_options: CustomDelegationOptions,
2528
) -> bool:
26-
if NodeConverter.uses_shape_broadcasting(node):
27-
# Shape broadcasting may require the addition of `Transpose` ops during conversion.
28-
return False
29+
if custom_delegation_options.use_new_flow_neutron_c:
30+
if not NodeConverter.at_least_one_input_shape_matches_the_output_shape(
31+
node
32+
):
33+
return False
2934

30-
return True
35+
# If one input is in channel first and ranks of input tensors are not equal, we need to add Transposes
36+
# Transpose is currently not supported for new flow
37+
if any(
38+
input_node.meta[NXP_NODE_FORMAT].is_channels_first()
39+
for input_node in node.all_input_nodes
40+
) and NodeConverter._node_inputs_ranks_not_equal(node):
41+
return False
42+
43+
supported_types = [torch.int8, torch.uint8]
44+
if not NodeConverter.uses_quantization_type_for_io(
45+
node, supported_types, [0, 1], [0]
46+
):
47+
return False
48+
49+
return True
50+
else:
51+
if NodeConverter.uses_shape_broadcasting(node):
52+
# Shape broadcasting may require the addition of `Transpose` ops during conversion.
53+
return False
54+
55+
return True
3156

3257
@staticmethod
3358
def _is_supported_in_IR(
@@ -43,12 +68,13 @@ def _is_supported_in_IR(
4368

4469
return True
4570

46-
# add.Tensor Node format: (Tensor self, Tensor other, *, Scalar alpha=1)
4771
def convert(self, node: Node):
48-
"""Convert 'add_tensor' operator to TFLite 'add'."""
72+
"""Convert 'add_tensor' operator to NeutronIR 'Add'.
73+
The ExecuTorch schema is:
74+
add.Tensor(Tensor self, Tensor other, Scalar alpha=1)
75+
"""
4976
self.assert_convertible(node)
50-
5177
t_op = self._create_tflite_op_with_io_tensors(node)
52-
5378
t_op.builtin_options = add_options.Add()
79+
5480
self.builder.append_operators([t_op])

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

Lines changed: 195 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,40 @@
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.
5+
56
import numpy as np
67
import pytest
78
import torch
89

910
from executorch.backends.nxp.backend.edge_program_converter import (
1011
EdgeProgramToIRConverter,
1112
)
12-
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
13+
from executorch.backends.nxp.tests.executorch_pipeline import (
14+
ModelInputSpec,
15+
to_quantized_edge_program,
16+
)
1317
from executorch.backends.nxp.tests.executors import (
1418
convert_run_compare,
19+
graph_contains_any_of_ops,
1520
ToChannelFirstPreprocess,
1621
ToChannelLastPreprocess,
1722
)
23+
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
24+
from executorch.backends.nxp.tests.model_output_comparator import (
25+
NumericalStatsOutputComparator,
26+
)
1827
from executorch.backends.nxp.tests.models import (
1928
AddTensorConvModule,
2029
AddTensorModule,
2130
AddTensorOneInputModule,
2231
)
32+
from executorch.backends.nxp.tests.nsys_testing import lower_run_compare
33+
from executorch.backends.nxp.tests.ops_aliases import (
34+
AddTensor,
35+
Convolution,
36+
ExecutorchDelegateCall,
37+
)
2338
from torch.export import ExportedProgram
2439
from executorch.backends.nxp.tests.use_qat import * # noqa F403
2540

@@ -64,7 +79,6 @@ def test_add_tensor_quant_conversion(mocker, input_shape, use_qat):
6479
@pytest.mark.parametrize(
6580
"input_shape",
6681
[
67-
pytest.param((4,), id="1D."),
6882
pytest.param((6, 6), id="2D."),
6983
pytest.param((1, 4, 8), id="3D."),
7084
pytest.param((1, 4, 8, 8), id="4D."),
@@ -92,20 +106,26 @@ def test_add_tensor_one_input_quant_conversion(mocker, input_shape, use_qat):
92106

93107

94108
@pytest.mark.parametrize(
95-
"input_shape",
109+
"x_input_shape",
96110
[
97111
pytest.param((1, 4, 8, 8), id="4D."),
98112
pytest.param((1, 4, 5, 5), id="4D, product of dims is not a multiple of 8."),
99113
],
100114
)
101-
def test_add_tensor_w_conv_quant_conversion(mocker, input_shape, use_qat):
115+
def test_add_tensor_w_conv_quant_conversion(mocker, x_input_shape, use_qat):
102116
model = AddTensorConvModule()
103117

104118
converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program")
105119

120+
n, c, h, w = x_input_shape
121+
y_input_shape = (n, 8, h, w)
122+
106123
# Run conversion
107124
_ = to_quantized_edge_program(
108-
model, input_shape, use_qat=use_qat, use_neutron_for_format_conversion=False
125+
model,
126+
[x_input_shape, y_input_shape],
127+
use_qat=use_qat,
128+
use_neutron_for_format_conversion=False,
109129
)
110130

111131
# Capture generated model
@@ -114,7 +134,13 @@ def test_add_tensor_w_conv_quant_conversion(mocker, input_shape, use_qat):
114134
# Capture converted program
115135
exported_program: ExportedProgram = converter_spy.call_args.args[1]
116136

117-
input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8)
137+
input_data_1 = (np.random.random(x_input_shape).astype(np.float32) * 50).astype(
138+
np.int8
139+
)
140+
input_data_2 = (np.random.random(y_input_shape).astype(np.float32) * 50).astype(
141+
np.int8
142+
)
143+
input_data = {0: input_data_1, 1: input_data_2}
118144

119145
convert_run_compare(
120146
exported_program,
@@ -149,7 +175,7 @@ def test_add_tensor_broadcasting_unsupported_quant_conversion(
149175
nodes = list(edge_program.graph.nodes)
150176

151177
# Broadcast is not supported, node is not converted
152-
assert nodes[6].target.__name__ == "aten.add.Tensor" # Add Tensor is not delegated.
178+
assert nodes[6].target == AddTensor # Add Tensor is not delegated.
153179

154180
# Capture converted program
155181
# exported_program: ExportedProgram = converter_spy.call_args.args[1]
@@ -159,3 +185,164 @@ def test_add_tensor_broadcasting_unsupported_quant_conversion(
159185
# input_data = {0: x_input_data, 1: y_input_data}
160186
#
161187
# convert_run_compare(exported_program, tfl_model=tflite_flatbuffers_model, input_data=input_data)
188+
189+
190+
class TestAddTensorNewNeutronFlow:
191+
@pytest.mark.skip("AIR-14602: incorrect results")
192+
@pytest.mark.parametrize(
193+
"x_input_shape",
194+
[
195+
pytest.param((1,), id="1D."),
196+
pytest.param((6, 8), id="2D."),
197+
pytest.param((1, 4, 8), id="3D."),
198+
pytest.param((1, 4, 8, 8), id="4D."),
199+
],
200+
)
201+
def test__basic_nsys_inference(self, x_input_shape, mocker):
202+
x_input_spec = ModelInputSpec(x_input_shape)
203+
model = AddTensorModule()
204+
graph_verifier = DetailedGraphVerifier(
205+
mocker, expected_delegated_ops={AddTensor: 1}, expected_non_delegated_ops={}
206+
)
207+
208+
lower_run_compare(
209+
model,
210+
[x_input_spec, x_input_spec],
211+
graph_verifier,
212+
use_new_flow_neutron_c=True,
213+
)
214+
215+
@pytest.mark.skip("AIR-14602: incorrect results")
216+
@pytest.mark.parametrize(
217+
"x_input_shape",
218+
[
219+
pytest.param((6, 8), id="2D."),
220+
pytest.param((1, 4, 8), id="3D."),
221+
pytest.param((1, 4, 8, 8), id="4D."),
222+
],
223+
)
224+
def test__basic_nsys_inference_qat(self, x_input_shape, mocker):
225+
x_input_spec = ModelInputSpec(x_input_shape)
226+
model = AddTensorModule()
227+
comparator = NumericalStatsOutputComparator()
228+
graph_verifier = DetailedGraphVerifier(
229+
mocker, expected_delegated_ops={AddTensor: 1}, expected_non_delegated_ops={}
230+
)
231+
232+
lower_run_compare(
233+
model,
234+
[x_input_spec, x_input_spec],
235+
graph_verifier,
236+
output_comparator=comparator,
237+
use_new_flow_neutron_c=True,
238+
use_qat=True,
239+
)
240+
241+
@pytest.mark.skip("AIR-14602: incorrect results")
242+
@pytest.mark.parametrize(
243+
"input_spec",
244+
[
245+
pytest.param(
246+
[ModelInputSpec((4, 6)), ModelInputSpec((1, 6))], id="2 inputs 2D."
247+
),
248+
pytest.param(
249+
[ModelInputSpec((5, 3, 4)), ModelInputSpec((1, 3, 1))],
250+
id="2 inputs 3D.",
251+
),
252+
pytest.param(
253+
[ModelInputSpec((4,)), ModelInputSpec((4, 4))], id="2 inputs 2D+3D."
254+
),
255+
],
256+
)
257+
def test__correct_broadcast(self, input_spec, mocker):
258+
model = AddTensorModule()
259+
graph_verifier = DetailedGraphVerifier(
260+
mocker, expected_delegated_ops={AddTensor: 1}, expected_non_delegated_ops={}
261+
)
262+
263+
lower_run_compare(
264+
model, input_spec, graph_verifier, use_new_flow_neutron_c=True
265+
)
266+
267+
@pytest.mark.parametrize(
268+
"input_spec",
269+
[
270+
pytest.param(
271+
[ModelInputSpec((4, 1)), ModelInputSpec((1, 6))], id="2 inputs 2D."
272+
),
273+
pytest.param(
274+
[ModelInputSpec((1, 3, 4)), ModelInputSpec((5, 3, 1))],
275+
id="2 inputs 3D.",
276+
),
277+
pytest.param(
278+
[ModelInputSpec((6, 4)), ModelInputSpec((6, 6, 1))],
279+
id="2 inputs 2D+3D.",
280+
),
281+
],
282+
)
283+
def test__incorrect_broadcast(self, input_spec):
284+
# Broadcast where at least one of the inputs is not equal to output is not supported
285+
model = AddTensorModule()
286+
287+
delegated_ep = to_quantized_edge_program(
288+
model, input_spec, use_new_flow_neutron_c=True
289+
).exported_program()
290+
291+
# Make sure the `add.Tensor` was NOT delegated.
292+
assert not graph_contains_any_of_ops(
293+
delegated_ep.graph, [ExecutorchDelegateCall]
294+
)
295+
assert graph_contains_any_of_ops(delegated_ep.graph, [AddTensor])
296+
297+
@pytest.mark.skip("AIR-14602: incorrect results")
298+
@pytest.mark.parametrize(
299+
"x_input_shape",
300+
[
301+
pytest.param(
302+
(1, 4, 5, 5), id="4D, product of dims is not a multiple of 8."
303+
),
304+
],
305+
)
306+
def test__w_conv(self, x_input_shape, mocker):
307+
model = AddTensorConvModule()
308+
309+
n, c, h, w = x_input_shape
310+
y_input_spec = ModelInputSpec((n, 8, h, w))
311+
x_input_spec = ModelInputSpec(x_input_shape)
312+
313+
graph_verifier = DetailedGraphVerifier(
314+
mocker,
315+
expected_delegated_ops={AddTensor: 1, Convolution: 1},
316+
expected_non_delegated_ops={},
317+
)
318+
319+
lower_run_compare(
320+
model,
321+
[x_input_spec, y_input_spec],
322+
graph_verifier,
323+
use_new_flow_neutron_c=True,
324+
)
325+
326+
@pytest.mark.parametrize(
327+
"input_spec",
328+
[
329+
pytest.param(
330+
[ModelInputSpec((1, 4, 5, 5)), ModelInputSpec((1, 5))],
331+
id="2 inputs 4D + 2D.",
332+
),
333+
pytest.param(
334+
[ModelInputSpec((1, 4, 4, 10)), ModelInputSpec((1, 4, 1))],
335+
id="2 inputs last + 3D.",
336+
),
337+
],
338+
)
339+
def test__w_conv_unsupported(self, input_spec):
340+
model = AddTensorConvModule()
341+
342+
delegated_ep = to_quantized_edge_program(
343+
model, input_spec, use_new_flow_neutron_c=True
344+
).exported_program()
345+
346+
# Make sure the `add.Tensor` was NOT delegated.
347+
assert graph_contains_any_of_ops(delegated_ep.graph, [ExecutorchDelegateCall])
348+
assert graph_contains_any_of_ops(delegated_ep.graph, [AddTensor])

backends/nxp/tests/models.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,9 +656,9 @@ def __init__(self):
656656
super().__init__()
657657
self.conv = Conv2dModule(padding=1, stride=1)
658658

659-
def forward(self, x):
659+
def forward(self, x, y):
660660
x = self.conv(x)
661-
return x + x
661+
return x + y
662662

663663

664664
class AddTensorOneInputModule(torch.nn.Module):

backends/nxp/tests/ops_aliases.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from executorch.exir.dialects._ops import ops as exir_ops
1313

1414
Abs = exir_ops.edge.aten.abs.default
15+
AddTensor = exir_ops.edge.aten.add.Tensor
1516
AvgPool2D = exir_ops.edge.aten.avg_pool2d.default
1617
Bmm = exir_ops.edge.aten.bmm.default
1718
Convolution = exir_ops.edge.aten.convolution.default

0 commit comments

Comments
 (0)