Skip to content

Commit 8dac17c

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

4 files changed

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

0 commit comments

Comments
 (0)