Skip to content

Commit 37f871f

Browse files
committed
NXP backend: Enable constant_pad_nd with new Neutron flow.
1 parent 5971a4b commit 37f871f

4 files changed

Lines changed: 174 additions & 28 deletions

File tree

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

Lines changed: 34 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from typing import Collection
88

99
import numpy as np
10+
import torch
1011

1112
from executorch.backends.nxp.backend.data_format import NXP_NODE_FORMAT
1213

@@ -41,19 +42,33 @@ def _is_supported_on_target(
4142
parameters_mapping: dict[str, Parameter],
4243
custom_delegation_options: CustomDelegationOptions,
4344
) -> bool:
44-
paddings = node.args[1]
45-
if node.meta[NXP_NODE_FORMAT].is_channels_first():
46-
# Dim `1` will end up being the channels. It is padded by paddings[4:6].
47-
if len(paddings) > 4 and paddings[4:6] != [0, 0]:
48-
# Attempt to Pad channels dimension -> currently not supported
49-
return False
50-
else:
51-
# Dim `-1` will end up being the channels. It is padded by paddings[:2].
52-
if len(paddings) > 0 and paddings[:2] != [0, 0]:
53-
# Attempt to Pad channels dimension -> currently not supported
45+
if custom_delegation_options.use_new_flow_neutron_c:
46+
# Requirements specified by the new Neutron flow documentation.
47+
48+
if not NodeConverter.uses_quantization_type_for_io(
49+
node,
50+
supported_types=[torch.int8, torch.uint8],
51+
input_indices=[0],
52+
output_indices=[0],
53+
):
5454
return False
5555

56-
return True
56+
return True
57+
58+
else:
59+
paddings = node.args[1]
60+
if node.meta[NXP_NODE_FORMAT].is_channels_first():
61+
# Dim `1` will end up being the channels. It is padded by paddings[4:6].
62+
if len(paddings) > 4 and paddings[4:6] != [0, 0]:
63+
# Attempt to Pad channels dimension -> currently not supported
64+
return False
65+
else:
66+
# Dim `-1` will end up being the channels. It is padded by paddings[:2].
67+
if len(paddings) > 0 and paddings[:2] != [0, 0]:
68+
# Attempt to Pad channels dimension -> currently not supported
69+
return False
70+
71+
return True
5772

5873
@staticmethod
5974
def _is_supported_in_IR(
@@ -110,7 +125,14 @@ def _convert_paddings_to_tflite(
110125
return paddings
111126

112127
def convert(self, node: Node):
113-
"""Convert the `aten.constant_pad_nd` operator to TFLite `PadV2`."""
128+
"""Convert the `aten.constant_pad_nd` operator to NeutronIR `PadV2`.
129+
The ExecuTorch schema is:
130+
constant_pad_nd(
131+
Tensor self,
132+
SymInt[] pad,
133+
Scalar value=0
134+
) -> Tensor
135+
"""
114136
self.assert_convertible(node)
115137

116138
t_op = self._create_tflite_op_with_io_tensors(node)

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

Lines changed: 132 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,15 @@
44
# LICENSE file in the root directory of this source tree.
55

66
import numpy as np
7+
8+
# noinspection PyUnusedImports
79
import pytest
810
import torch
11+
912
from executorch.backends.nxp.backend.ir.conversion_config import ConversionConfig
13+
from executorch.backends.nxp.backend.ir.converter.builder.model_builder import (
14+
ModelBuilder,
15+
)
1016
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.constant_pad_nd_converter import (
1117
ConstantPadNDConverter,
1218
)
@@ -17,16 +23,18 @@
1723
from executorch.backends.nxp.tests.executors import (
1824
convert_run_compare,
1925
graph_contains_any_of_ops,
26+
OverrideTargetSupportCheck,
2027
ToNCHWPreprocess,
2128
ToNHWCPreprocess,
2229
)
30+
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
2331
from executorch.backends.nxp.tests.models import (
2432
ConstantPadNDConvModule,
2533
ConstantPadNDModule,
2634
)
35+
from executorch.backends.nxp.tests.nsys_testing import lower_run_compare
36+
from executorch.backends.nxp.tests.ops_aliases import ConstantPadND, Convolution
2737
from executorch.backends.nxp.tests.use_qat import * # noqa F403
28-
from executorch.backends.nxp.tests.executors import OverrideTargetSupportCheck
29-
from executorch.exir.dialects._ops import ops as exir_ops
3038

3139

3240
@pytest.fixture(autouse=True)
@@ -158,9 +166,8 @@ def test_constant_pad_nd__unsupported_paddings(input_shape, paddings, use_qat):
158166
model, input_shape, use_qat=use_qat
159167
).exported_program()
160168

161-
nodes = list(exec_program.graph.nodes)
162169
# There is at least one non-delegated Pad node
163-
assert any(node.name == "aten_constant_pad_nd_default" for node in nodes)
170+
assert graph_contains_any_of_ops(exec_program.graph, [ConstantPadND])
164171

165172

166173
@pytest.mark.xfail(reason="EIEX=855")
@@ -173,9 +180,7 @@ def test_constant_pad_nd__delegation__formatless__supported_padding(use_qat):
173180
).exported_program()
174181

175182
# Make sure the `pad` was delegated.
176-
assert not graph_contains_any_of_ops(
177-
exec_program.graph, [exir_ops.edge.aten.constant_pad_nd.default]
178-
)
183+
assert not graph_contains_any_of_ops(exec_program.graph, [ConstantPadND])
179184

180185

181186
def test_constant_pad_nd__delegation__formatless__unsupported_padding(use_qat):
@@ -187,9 +192,7 @@ def test_constant_pad_nd__delegation__formatless__unsupported_padding(use_qat):
187192
).exported_program()
188193

189194
# Make sure the `pad` was NOT delegated.
190-
assert graph_contains_any_of_ops(
191-
exec_program.graph, [exir_ops.edge.aten.constant_pad_nd.default]
192-
)
195+
assert graph_contains_any_of_ops(exec_program.graph, [ConstantPadND])
193196

194197

195198
@pytest.mark.xfail(reason="Regression in Neutron SW 3.0.1 (AIR-14264)", strict=True)
@@ -202,9 +205,7 @@ def test_constant_pad_nd__delegation__channels_first__supported_padding(use_qat)
202205
).exported_program()
203206

204207
# Make sure the `pad` was delegated.
205-
assert not graph_contains_any_of_ops(
206-
exec_program.graph, [exir_ops.edge.aten.constant_pad_nd.default]
207-
)
208+
assert not graph_contains_any_of_ops(exec_program.graph, [ConstantPadND])
208209

209210

210211
def test_constant_pad_nd__delegation__channels_first__unsupported_padding(use_qat):
@@ -216,6 +217,122 @@ def test_constant_pad_nd__delegation__channels_first__unsupported_padding(use_qa
216217
).exported_program()
217218

218219
# Make sure the `pad` was NOT delegated.
219-
assert graph_contains_any_of_ops(
220-
exec_program.graph, [exir_ops.edge.aten.constant_pad_nd.default]
220+
assert graph_contains_any_of_ops(exec_program.graph, [ConstantPadND])
221+
222+
223+
class TestConstantPadNDNewNeutronFlow:
224+
"""The PyTorch padding is added to the individual dimensions from the back (slightly confusing), see:
225+
https://pytorch.org/docs/stable/generated/torch.nn.functional.pad.html#torch.nn.functional.pad
226+
"""
227+
228+
# noinspection PyMethodMayBeStatic
229+
def assert_delegated(self, model, input_shape, mocker, use_qat=False):
230+
graph_verifier = DetailedGraphVerifier(
231+
mocker,
232+
expected_delegated_ops={ConstantPadND: 1},
233+
expected_non_delegated_ops={},
234+
)
235+
236+
lower_run_compare(
237+
model,
238+
input_shape,
239+
graph_verifier,
240+
use_qat=use_qat,
241+
use_new_flow_neutron_c=True,
242+
)
243+
244+
def assert_delegated_and_output_shape_equals(
245+
self, model, input_shape, expected_output_shape, mocker
246+
):
247+
model_builder_spy = mocker.spy(ModelBuilder, "finish")
248+
249+
self.assert_delegated(model, input_shape, mocker)
250+
251+
neutron_ir_subgraph = model_builder_spy.call_args[0][0].get_sub_graph()
252+
assert neutron_ir_subgraph.outputs.tmp_outputs[0].shape.vector == list(
253+
expected_output_shape
254+
)
255+
256+
@pytest.mark.parametrize(
257+
"input_shape, paddings",
258+
[
259+
pytest.param((2,), tuple(range(2)), id="1D, padding H"),
260+
pytest.param((2, 4), tuple(range(2)), id="2D, padding H"),
261+
pytest.param((2, 4), tuple(range(4)), id="2D, padding N, H"),
262+
pytest.param((2, 4, 6), tuple(range(2)), id="3D, padding H"),
263+
pytest.param((2, 4, 6), tuple(range(4)), id="3D, padding C, H"),
264+
pytest.param((2, 4, 6, 8), tuple(range(2)), id="4D, padding W"),
265+
pytest.param((2, 4, 6, 8), tuple(range(4)), id="4D, padding H, W"),
266+
pytest.param((1, 2, 3, 4, 5), tuple(range(2)), id="5D, padding D"),
267+
pytest.param((1, 2, 3, 4, 5), tuple(range(4)), id="5D, padding W, D"),
268+
],
269+
)
270+
def test__basic_nsys_inference(self, mocker, input_shape, paddings, use_qat):
271+
# These test cases are also supported by the old flow.
272+
model = ConstantPadNDModule(paddings)
273+
self.assert_delegated(model, input_shape, mocker, use_qat)
274+
275+
def test__channels_padding(self, mocker):
276+
input_shape = (2, 4, 6)
277+
# These paddings will be applied to the last dimension, which is the channels as the input is formatless.
278+
paddings = (1, 1)
279+
expected_output_shape = (2, 4, 8) # Padded channels.
280+
model = ConstantPadNDModule(paddings)
281+
282+
self.assert_delegated_and_output_shape_equals(
283+
model, input_shape, expected_output_shape, mocker
284+
)
285+
286+
def test__batch_padding(self, mocker):
287+
input_shape = (2, 4, 6)
288+
paddings = (0, 0, 0, 0, 1, 1) # Padding applied to the batch dimension.
289+
expected_output_shape = (4, 4, 6) # Padded batch.
290+
model = ConstantPadNDModule(paddings)
291+
292+
self.assert_delegated_and_output_shape_equals(
293+
model, input_shape, expected_output_shape, mocker
294+
)
295+
296+
@pytest.mark.parametrize("constant", [0.0, -13.37])
297+
def test__specific_constant(self, mocker, constant):
298+
input_shape = (2, 4, 6)
299+
paddings = (1, 1)
300+
model = ConstantPadNDModule(paddings, constant)
301+
self.assert_delegated(model, input_shape, mocker)
302+
303+
@pytest.mark.parametrize(
304+
"input_shape, paddings",
305+
[
306+
pytest.param((1, 4, 6, 8), tuple(range(2)), id="4D, padding W"),
307+
pytest.param((1, 4, 6, 8), tuple(range(4)), id="4D, padding H, W"),
308+
],
221309
)
310+
def test__channels_first(self, mocker, input_shape, paddings):
311+
model = ConstantPadNDConvModule(paddings)
312+
graph_verifier = DetailedGraphVerifier(
313+
mocker,
314+
expected_delegated_ops={ConstantPadND: 1, Convolution: 1},
315+
expected_non_delegated_ops={},
316+
)
317+
318+
lower_run_compare(
319+
model, input_shape, graph_verifier, use_new_flow_neutron_c=True
320+
)
321+
322+
@pytest.mark.xfail(
323+
strict=True,
324+
raises=RuntimeError,
325+
reason="Known issue in Neutron: https://jira.sw.nxp.com/browse/AIR-14624", # @lint-ignore
326+
)
327+
def test__bugged_channels_first_case(self, mocker):
328+
input_shape, paddings = (1, 2, 6, 8), (0, 1, 2, 3, 1, 1)
329+
model = ConstantPadNDConvModule(paddings)
330+
graph_verifier = DetailedGraphVerifier(
331+
mocker,
332+
expected_delegated_ops={ConstantPadND: 1, Convolution: 1},
333+
expected_non_delegated_ops={},
334+
)
335+
336+
lower_run_compare(
337+
model, input_shape, graph_verifier, use_new_flow_neutron_c=True
338+
)

backends/nxp/tests/model_output_comparator.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,13 @@ def compare_sample(self, sample_dir, cpu_output_tensors, npu_output_tensors):
9191
assert np.any(
9292
cpu_tensor
9393
), "Output tensor contains only zeros. This is suspicious."
94-
assert np.allclose(cpu_tensor, npu_tensor, atol=self.atol)
94+
all_close = np.allclose(cpu_tensor, npu_tensor, atol=self.atol)
95+
if not all_close:
96+
max_diff = np.abs(cpu_tensor - npu_tensor).max()
97+
print(
98+
f"NPU output doesn't match reference. Maximum absolute difference: {max_diff}"
99+
)
100+
assert all_close
95101

96102

97103
def _default_postprocess_fn(outputs: np.ndarray, _: str):

backends/nxp/tests/ops_aliases.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Abs = exir_ops.edge.aten.abs.default
1515
AvgPool2D = exir_ops.edge.aten.avg_pool2d.default
1616
Bmm = exir_ops.edge.aten.bmm.default
17+
ConstantPadND = exir_ops.edge.aten.constant_pad_nd.default
1718
Convolution = exir_ops.edge.aten.convolution.default
1819
DequantizePerChannel = exir_ops.edge.quantized_decomposed.dequantize_per_channel.default
1920
DequantizePerTensor = exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default

0 commit comments

Comments
 (0)