Skip to content

Commit 348f0e9

Browse files
Test Add Tensor with new Neutron flow
1 parent 90a7cdb commit 348f0e9

4 files changed

Lines changed: 293 additions & 17 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: 256 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
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.dataset_creator import RandomDatasetCreator
14+
from executorch.backends.nxp.tests.executorch_pipeline import (
15+
ModelInputSpec,
16+
to_quantized_edge_program,
17+
)
1318
from executorch.backends.nxp.tests.executors import (
1419
convert_run_compare,
20+
graph_contains_any_of_ops,
1521
ToChannelFirstPreprocess,
1622
ToChannelLastPreprocess,
1723
)
24+
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
1825
from executorch.backends.nxp.tests.models import (
1926
AddTensorConvModule,
2027
AddTensorModule,
2128
AddTensorOneInputModule,
2229
)
30+
from executorch.backends.nxp.tests.nsys_testing import lower_run_compare
31+
from executorch.backends.nxp.tests.ops_aliases import (
32+
AddTensor,
33+
Convolution,
34+
ExecutorchDelegateCall,
35+
)
2336
from torch.export import ExportedProgram
2437
from executorch.backends.nxp.tests.use_qat import * # noqa F403
2538

@@ -92,20 +105,26 @@ def test_add_tensor_one_input_quant_conversion(mocker, input_shape, use_qat):
92105

93106

94107
@pytest.mark.parametrize(
95-
"input_shape",
108+
"x_input_shape",
96109
[
97110
pytest.param((1, 4, 8, 8), id="4D."),
98111
pytest.param((1, 4, 5, 5), id="4D, product of dims is not a multiple of 8."),
99112
],
100113
)
101-
def test_add_tensor_w_conv_quant_conversion(mocker, input_shape, use_qat):
114+
def test_add_tensor_w_conv_quant_conversion(mocker, x_input_shape, use_qat):
102115
model = AddTensorConvModule()
103116

104117
converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program")
105118

119+
n, c, h, w = x_input_shape
120+
y_input_shape = (n, 8, h, w)
121+
106122
# Run conversion
107123
_ = to_quantized_edge_program(
108-
model, input_shape, use_qat=use_qat, use_neutron_for_format_conversion=False
124+
model,
125+
[x_input_shape, y_input_shape],
126+
use_qat=use_qat,
127+
use_neutron_for_format_conversion=False,
109128
)
110129

111130
# Capture generated model
@@ -114,7 +133,13 @@ def test_add_tensor_w_conv_quant_conversion(mocker, input_shape, use_qat):
114133
# Capture converted program
115134
exported_program: ExportedProgram = converter_spy.call_args.args[1]
116135

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

119144
convert_run_compare(
120145
exported_program,
@@ -149,7 +174,7 @@ def test_add_tensor_broadcasting_unsupported_quant_conversion(
149174
nodes = list(edge_program.graph.nodes)
150175

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

154179
# Capture converted program
155180
# exported_program: ExportedProgram = converter_spy.call_args.args[1]
@@ -159,3 +184,227 @@ def test_add_tensor_broadcasting_unsupported_quant_conversion(
159184
# input_data = {0: x_input_data, 1: y_input_data}
160185
#
161186
# convert_run_compare(exported_program, tfl_model=tflite_flatbuffers_model, input_data=input_data)
187+
188+
189+
class TestAddTensorNewNeutronFlow:
190+
@pytest.mark.parametrize(
191+
"x_input_shape",
192+
[
193+
pytest.param((1,), id="1D."),
194+
pytest.param((6, 5), id="2D."),
195+
pytest.param((1, 4, 7), id="3D."),
196+
pytest.param((2, 4, 3, 15), id="4D."),
197+
pytest.param(
198+
(6, 82),
199+
id="2D incorrect.",
200+
marks=pytest.mark.xfail(reason="AIR-14602: incorrect results"),
201+
),
202+
pytest.param(
203+
(1, 68, 7),
204+
id="3D incorrect.",
205+
marks=pytest.mark.xfail(reason="AIR-14602: incorrect results"),
206+
),
207+
pytest.param(
208+
(1, 4, 9, 11, 4),
209+
id="5D incorrect.",
210+
marks=pytest.mark.xfail(reason="AIR-14602: incorrect results"),
211+
),
212+
],
213+
)
214+
def test__basic_nsys_inference(self, x_input_shape, mocker):
215+
x_input_spec = ModelInputSpec(x_input_shape)
216+
model = AddTensorModule()
217+
graph_verifier = DetailedGraphVerifier(
218+
mocker, expected_delegated_ops={AddTensor: 1}, expected_non_delegated_ops={}
219+
)
220+
dataset_creator = RandomDatasetCreator(low=-1.0, high=1.0)
221+
222+
lower_run_compare(
223+
model,
224+
[x_input_spec, x_input_spec],
225+
graph_verifier,
226+
dataset_creator,
227+
use_new_flow_neutron_c=True,
228+
)
229+
230+
@pytest.mark.parametrize(
231+
"x_input_shape",
232+
[
233+
pytest.param((1,), id="1D."),
234+
pytest.param((6, 5), id="2D."),
235+
pytest.param((1, 4, 7), id="3D."),
236+
pytest.param((2, 4, 3, 15), id="4D."),
237+
pytest.param(
238+
(1, 4, 9, 11, 4),
239+
id="5D.",
240+
marks=pytest.mark.xfail(reason="AIR-14602: incorrect results"),
241+
),
242+
],
243+
)
244+
def test__basic_nsys_inference_qat(self, x_input_shape, mocker):
245+
x_input_spec = ModelInputSpec(x_input_shape)
246+
model = AddTensorModule()
247+
graph_verifier = DetailedGraphVerifier(
248+
mocker, expected_delegated_ops={AddTensor: 1}, expected_non_delegated_ops={}
249+
)
250+
dataset_creator = RandomDatasetCreator(low=-1.0, high=1.0)
251+
252+
lower_run_compare(
253+
model,
254+
[x_input_spec, x_input_spec],
255+
graph_verifier,
256+
dataset_creator,
257+
use_new_flow_neutron_c=True,
258+
use_qat=True,
259+
)
260+
261+
@pytest.mark.parametrize(
262+
"input_spec",
263+
[
264+
pytest.param(
265+
[ModelInputSpec((4, 6)), ModelInputSpec((1, 6))], id="2 inputs 2D."
266+
),
267+
pytest.param(
268+
[ModelInputSpec((5, 3, 4)), ModelInputSpec((1, 3, 1))],
269+
id="2 inputs 3D.",
270+
),
271+
pytest.param(
272+
[ModelInputSpec((4,)), ModelInputSpec((4, 4))], id="2 inputs 1D + 2D."
273+
),
274+
pytest.param(
275+
[ModelInputSpec((69, 73)), ModelInputSpec((1, 73))],
276+
id="2 inputs 2D incorrect.",
277+
marks=pytest.mark.xfail(reason="AIR-14602: incorrect results"),
278+
),
279+
],
280+
)
281+
def test__broadcast(self, input_spec, mocker):
282+
model = AddTensorModule()
283+
graph_verifier = DetailedGraphVerifier(
284+
mocker, expected_delegated_ops={AddTensor: 1}, expected_non_delegated_ops={}
285+
)
286+
dataset_creator = RandomDatasetCreator(low=-1.0, high=1.0)
287+
288+
lower_run_compare(
289+
model,
290+
input_spec,
291+
graph_verifier,
292+
dataset_creator,
293+
use_new_flow_neutron_c=True,
294+
)
295+
296+
@pytest.mark.parametrize(
297+
"input_spec",
298+
[
299+
pytest.param(
300+
[ModelInputSpec((4, 1)), ModelInputSpec((1, 6))], id="2 inputs 2D."
301+
),
302+
pytest.param(
303+
[ModelInputSpec((1, 3, 4)), ModelInputSpec((5, 3, 1))],
304+
id="2 inputs 3D.",
305+
),
306+
pytest.param(
307+
[ModelInputSpec((6, 4)), ModelInputSpec((6, 6, 1))],
308+
id="2 inputs 2D + 3D.",
309+
),
310+
],
311+
)
312+
def test__broadcast_unsupported(self, input_spec):
313+
# Broadcast where at least one of the inputs is not equal to output is not supported
314+
model = AddTensorModule()
315+
316+
delegated_ep = to_quantized_edge_program(
317+
model, input_spec, use_new_flow_neutron_c=True
318+
).exported_program()
319+
320+
# Make sure the `add.Tensor` was NOT delegated.
321+
assert not graph_contains_any_of_ops(
322+
delegated_ep.graph, [ExecutorchDelegateCall]
323+
)
324+
assert graph_contains_any_of_ops(delegated_ep.graph, [AddTensor])
325+
326+
@pytest.mark.parametrize(
327+
"x_input_shape",
328+
[
329+
pytest.param(
330+
(1, 4, 5, 5), id="4D, product of dims is not a multiple of 8."
331+
),
332+
],
333+
)
334+
def test__w_conv(self, x_input_shape, mocker):
335+
model = AddTensorConvModule()
336+
337+
n, c, h, w = x_input_shape
338+
y_input_spec = ModelInputSpec((n, 8, h, w))
339+
x_input_spec = ModelInputSpec(x_input_shape)
340+
341+
graph_verifier = DetailedGraphVerifier(
342+
mocker,
343+
expected_delegated_ops={AddTensor: 1, Convolution: 1},
344+
expected_non_delegated_ops={},
345+
)
346+
dataset_creator = RandomDatasetCreator(low=-1.0, high=1.0)
347+
348+
lower_run_compare(
349+
model,
350+
[x_input_spec, y_input_spec],
351+
graph_verifier,
352+
dataset_creator,
353+
use_new_flow_neutron_c=True,
354+
)
355+
356+
@pytest.mark.parametrize(
357+
"input_spec",
358+
[
359+
pytest.param(
360+
[ModelInputSpec((1, 4, 5, 5)), ModelInputSpec((1, 8, 5, 1))],
361+
id="2 inputs 4D + 4D.",
362+
),
363+
pytest.param(
364+
[ModelInputSpec((1, 4, 5, 67)), ModelInputSpec((1, 8, 5, 1))],
365+
id="2 inputs 4D + 4D incorrect.",
366+
marks=pytest.mark.xfail(reason="AIR-14602: incorrect results"),
367+
),
368+
],
369+
)
370+
def test__w_conv_broadcast(self, input_spec, mocker):
371+
model = AddTensorConvModule()
372+
373+
graph_verifier = DetailedGraphVerifier(
374+
mocker,
375+
expected_delegated_ops={AddTensor: 1, Convolution: 1},
376+
expected_non_delegated_ops={},
377+
)
378+
dataset_creator = RandomDatasetCreator(low=-1.0, high=1.0)
379+
380+
lower_run_compare(
381+
model,
382+
input_spec,
383+
graph_verifier,
384+
dataset_creator,
385+
use_new_flow_neutron_c=True,
386+
)
387+
388+
@pytest.mark.parametrize(
389+
"input_spec",
390+
[
391+
pytest.param(
392+
[ModelInputSpec((1, 4, 5, 5)), ModelInputSpec((1, 5))],
393+
id="2 inputs 4D + 2D.",
394+
),
395+
pytest.param(
396+
[ModelInputSpec((1, 4, 4, 10)), ModelInputSpec((1, 4, 1))],
397+
id="2 inputs 4D + 3D.",
398+
),
399+
],
400+
)
401+
def test__w_conv_unsupported(self, input_spec):
402+
model = AddTensorConvModule()
403+
404+
delegated_ep = to_quantized_edge_program(
405+
model, input_spec, use_new_flow_neutron_c=True
406+
).exported_program()
407+
408+
# Make sure the `add.Tensor` was NOT delegated.
409+
assert graph_contains_any_of_ops(delegated_ep.graph, [ExecutorchDelegateCall])
410+
assert graph_contains_any_of_ops(delegated_ep.graph, [AddTensor])

0 commit comments

Comments
 (0)