Skip to content

Commit 6a6b94c

Browse files
committed
NXP backend: Test adaptive_avg_pool2d with new Neutron flow.
1 parent b04cc65 commit 6a6b94c

5 files changed

Lines changed: 199 additions & 24 deletions

File tree

Lines changed: 73 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,59 @@
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+
import logging
56

67
import executorch.backends.nxp.backend.ir.lib.tflite.Padding as tflPadding
8+
import torch
9+
10+
from executorch.backends.nxp.backend.data_format import NXP_NODE_FORMAT
711
from executorch.backends.nxp.backend.ir.converter.conversion import common
812
from executorch.backends.nxp.backend.ir.converter.node_converter import (
913
CustomDelegationOptions,
1014
NodeConverter,
1115
)
12-
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
1316
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
1417
average_pool_2d_options,
1518
)
16-
from torch import Size
19+
20+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
1721
from torch.fx import Node
1822
from torch.nn import Parameter
1923

24+
KernelSize = tuple[int, int]
25+
Stride = tuple[int, int]
26+
2027

2128
class AdaptiveAvgPool2dConverter(NodeConverter):
2229

30+
@staticmethod
31+
def _get_equivalent_avg_pool_parameters(node: Node) -> tuple[KernelSize, Stride]:
32+
input_size = node.args[0].meta["val"].shape[2:] # Spatial dims from NCHW shape.
33+
output_size = node.args[1]
34+
stride = (input_size[0] // output_size[0], input_size[1] // output_size[1])
35+
kernel_size = (
36+
input_size[0] - (output_size[0] - 1) * stride[0],
37+
input_size[1] - (output_size[1] - 1) * stride[1],
38+
)
39+
40+
return kernel_size, stride
41+
2342
@staticmethod
2443
def _is_supported_in_IR(
2544
node: Node,
2645
parameters_mapping: dict[str, Parameter],
2746
custom_delegation_options: CustomDelegationOptions,
2847
) -> bool:
48+
if (
49+
format_ := node.meta.get(NXP_NODE_FORMAT)
50+
) is None or not format_.is_channels_first():
51+
logging.warning(
52+
"NXP backend: `adaptive_avg_pool_2d` doesn't have the required input format for delegation. "
53+
"Please run `NodeFormatInference.identify_node_formats()` during lowering or report this issue."
54+
)
55+
return False
56+
2957
input_size = node.args[0].meta["val"].shape
3058
output_size = node.args[1]
3159

@@ -39,30 +67,53 @@ def _is_supported_in_IR(
3967

4068
return True
4169

42-
# noinspection PyMethodMayBeStatic
43-
def _convert_adaptive_avg_pool_2d(
44-
self, input_size: Size, output_size: list[int], t_op: tflite_model.Operator
45-
):
46-
t_op.builtin_options = average_pool_2d_options.AveragePool2D()
47-
stride = [input_size[-2] // output_size[-2], input_size[-1] // output_size[-1]]
48-
common.assign_2d_strides(t_op.builtin_options, stride)
49-
t_op.builtin_options.filter_h = (
50-
input_size[-2] - (output_size[-2] - 1) * stride[-2]
51-
)
52-
t_op.builtin_options.filter_w = (
53-
input_size[-1] - (output_size[-1] - 1) * stride[-1]
70+
@staticmethod
71+
def _is_supported_on_target(
72+
node: Node,
73+
neutron_target_spec: NeutronTargetSpec,
74+
parameters_mapping: dict[str, Parameter],
75+
custom_delegation_options: CustomDelegationOptions,
76+
) -> bool:
77+
kernel_size, stride = (
78+
AdaptiveAvgPool2dConverter._get_equivalent_avg_pool_parameters(node)
5479
)
55-
t_op.builtin_options.padding = tflPadding.Padding.VALID
5680

57-
# AdaptiveAvgPool2d Node format: (Tensor self, SymInt[2] output_size)
81+
if custom_delegation_options.use_new_flow_neutron_c:
82+
# Requirements specified by the new Neutron flow documentation.
83+
84+
if not NodeConverter.uses_quantization_type_for_io(
85+
node,
86+
supported_types=[torch.int8, torch.uint8],
87+
input_indices=[0],
88+
output_indices=[0],
89+
):
90+
return False
91+
92+
if any(k > 4096 for k in kernel_size):
93+
return False
94+
95+
if any(s > 4096 for s in stride):
96+
return False
97+
98+
return True
99+
58100
def convert(self, node: Node):
59-
"""Convert '_adaptive_avg_pool2d' operator to TFLite 'AveragePool2D'."""
101+
"""Convert the '_adaptive_avg_pool2d' operator to NeutronIR 'AveragePool2D'.
102+
The ExecuTorch schema is:
103+
_adaptive_avg_pool2d(
104+
Tensor self,
105+
SymInt[2] output_size
106+
) -> Tensor
107+
"""
60108
self.assert_convertible(node)
61109

62-
input_size = node.args[0].meta["val"].shape
63-
output_size = node.args[1]
64-
65110
t_op = self._create_tflite_op_with_io_tensors(node)
111+
t_op.builtin_options = average_pool_2d_options.AveragePool2D()
112+
113+
kernel_size, stride = self._get_equivalent_avg_pool_parameters(node)
114+
115+
common.assign_2d_strides(t_op.builtin_options, stride)
116+
t_op.builtin_options.filter_h, t_op.builtin_options.filter_w = kernel_size
117+
t_op.builtin_options.padding = tflPadding.Padding.VALID
66118

67-
self._convert_adaptive_avg_pool_2d(input_size, output_size, t_op)
68119
self.builder.append_operators([t_op])

backends/nxp/backend/node_format_inference.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ class NodeFormatInference:
2525
# The op in the dictionary is mapped to a dictionary, which holds indices to input nodes
2626
# that are always channels first.
2727
ops_with_channels_first_nodes = {
28+
exir_ops.edge.aten._adaptive_avg_pool2d.default: {"inputs": [0]},
29+
torch.ops.aten.adaptive_avg_pool2d.default: {"inputs": [0]},
2830
exir_ops.edge.aten.avg_pool2d.default: {"inputs": [0]},
2931
exir_ops.edge.aten.convolution.default: {"inputs": [0, 1]},
3032
exir_ops.edge.aten.max_pool2d_with_indices.default: {"inputs": [0]},

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

Lines changed: 116 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
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.
@@ -10,15 +10,29 @@
1010
from executorch.backends.nxp.backend.edge_program_converter import (
1111
EdgeProgramToIRConverter,
1212
)
13+
from executorch.backends.nxp.tests.dataset_creator import RandomDatasetCreator
1314
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
1415
from executorch.backends.nxp.tests.executors import (
1516
convert_run_compare,
17+
graph_contains_any_of_ops,
1618
ToChannelFirstPreprocess,
1719
ToChannelLastPreprocess,
1820
)
21+
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
22+
from executorch.backends.nxp.tests.model_output_comparator import (
23+
AllCloseOutputComparator,
24+
)
1925
from executorch.backends.nxp.tests.models import (
2026
AdaptiveAvgPool2dConvMeanDimModule,
2127
AdaptiveAvgPool2dConvModule,
28+
AdaptiveAvgPool2dModule,
29+
)
30+
31+
from executorch.backends.nxp.tests.nsys_testing import lower_run_compare
32+
33+
from executorch.backends.nxp.tests.ops_aliases import (
34+
AdaptiveAvgPool2D,
35+
ExecutorchDelegateCall,
2236
)
2337
from torch.export import ExportedProgram
2438
from executorch.backends.nxp.tests.use_qat import * # noqa F403
@@ -151,3 +165,104 @@ def test_adaptive_avg_pool_2d_mean_dim_quant_conversion(mocker, use_qat):
151165
tflite_output_preprocess=ToChannelFirstPreprocess(),
152166
input_data=input_data,
153167
)
168+
169+
170+
class TestAdaptiveAvgPool2DNewNeutronFlow:
171+
def test__basic_nsys_inference(self, mocker, use_qat):
172+
input_shape = (2, 3, 4, 6)
173+
output_size = (2, 3)
174+
model = AdaptiveAvgPool2dModule(output_size)
175+
graph_verifier = DetailedGraphVerifier(
176+
mocker,
177+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
178+
expected_non_delegated_ops={},
179+
)
180+
181+
output_comparator = AllCloseOutputComparator(
182+
7.8e-3
183+
) # Accept small error due to Neutron bug (AIR-14585).
184+
185+
lower_run_compare(
186+
model,
187+
input_shape,
188+
graph_verifier,
189+
RandomDatasetCreator(low=-1, high=1),
190+
output_comparator=output_comparator,
191+
use_qat=use_qat,
192+
use_new_flow_neutron_c=True,
193+
)
194+
195+
@pytest.mark.xfail(
196+
strict=True,
197+
reason="Known Neutron bad compute issue. Will be fixed in Neutron SW 3.1.2.",
198+
)
199+
def test__know_neutron_issue(self, mocker):
200+
input_shape = (2, 3, 10, 15)
201+
output_size = (5, 5)
202+
model = AdaptiveAvgPool2dModule(output_size)
203+
graph_verifier = DetailedGraphVerifier(
204+
mocker,
205+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
206+
expected_non_delegated_ops={},
207+
)
208+
209+
# Use high tolerance so we notice when the issue is fixed.
210+
output_comparator = AllCloseOutputComparator(7.8e-3)
211+
212+
lower_run_compare(
213+
model,
214+
input_shape,
215+
graph_verifier,
216+
RandomDatasetCreator(low=-1, high=1),
217+
output_comparator=output_comparator,
218+
use_new_flow_neutron_c=True,
219+
)
220+
221+
def test__kernel_size_and_stride_limit(self, mocker):
222+
input_shape = (1, 3, 4, 4096) # input_size = (1, 4096)
223+
output_size = (
224+
2,
225+
1,
226+
) # If we reduced both dims to 1, ExecuTorch would replace the op with mean.
227+
# stride = input_size // output_size = 4096 / 1 = 4096
228+
# kernel_size = input_size - (output_size - 1) * stride = 4096 - 0 * 4096 = 4096
229+
230+
model = AdaptiveAvgPool2dModule(output_size)
231+
graph_verifier = DetailedGraphVerifier(
232+
mocker,
233+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
234+
expected_non_delegated_ops={},
235+
)
236+
237+
output_comparator = AllCloseOutputComparator(
238+
7.9e-3
239+
) # Accept small error due to Neutron bug (AIR-14585).
240+
241+
lower_run_compare(
242+
model,
243+
input_shape,
244+
graph_verifier,
245+
RandomDatasetCreator(low=-1, high=1),
246+
output_comparator=output_comparator,
247+
use_new_flow_neutron_c=True,
248+
)
249+
250+
def test__kernel_size_and_stride_limit_exceeded(self):
251+
input_shape = (1, 3, 4, 4097) # input_size = (1, 4097)
252+
output_size = (
253+
2,
254+
1,
255+
) # If we reduced both dims to 1, ExecuTorch would replace the op with mean.
256+
# stride = input_size // output_size = 4097 / 1 = 4097
257+
# kernel_size = input_size - (output_size - 1) * stride = 4097 - 0 * 4097 = 4097
258+
259+
model = AdaptiveAvgPool2dModule(output_size)
260+
delegated_ep = to_quantized_edge_program(
261+
model, input_shape, use_new_flow_neutron_c=True
262+
).exported_program()
263+
264+
# Make sure the `adaptive_avg_pool2d` was NOT delegated.
265+
assert not graph_contains_any_of_ops(
266+
delegated_ep.graph, [ExecutorchDelegateCall]
267+
)
268+
assert graph_contains_any_of_ops(delegated_ep.graph, [AdaptiveAvgPool2D])

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
@@ -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+
AdaptiveAvgPool2D = exir_ops.edge.aten._adaptive_avg_pool2d.default
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)