Skip to content

Commit 30aaa4e

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

5 files changed

Lines changed: 183 additions & 24 deletions

File tree

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,41 @@
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.
55

66
import executorch.backends.nxp.backend.ir.lib.tflite.Padding as tflPadding
7+
import torch
78
from executorch.backends.nxp.backend.ir.converter.conversion import common
89
from executorch.backends.nxp.backend.ir.converter.node_converter import (
910
CustomDelegationOptions,
1011
NodeConverter,
1112
)
12-
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
1313
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
1414
average_pool_2d_options,
1515
)
16-
from torch import Size
16+
17+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
1718
from torch.fx import Node
1819
from torch.nn import Parameter
1920

21+
KernelSize = tuple[int, int]
22+
Stride = tuple[int, int]
23+
2024

2125
class AdaptiveAvgPool2dConverter(NodeConverter):
2226

27+
@staticmethod
28+
def _get_equivalent_avg_pool_parameters(node: Node) -> tuple[KernelSize, Stride]:
29+
input_size = node.args[0].meta["val"].shape[2:] # Spatial dims from NCHW shape.
30+
output_size = node.args[1]
31+
stride = (input_size[0] // output_size[0], input_size[1] // output_size[1])
32+
kernel_size = (
33+
input_size[0] - (output_size[0] - 1) * stride[0],
34+
input_size[1] - (output_size[1] - 1) * stride[1],
35+
)
36+
37+
return kernel_size, stride
38+
2339
@staticmethod
2440
def _is_supported_in_IR(
2541
node: Node,
@@ -39,30 +55,53 @@ def _is_supported_in_IR(
3955

4056
return True
4157

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]
58+
@staticmethod
59+
def _is_supported_on_target(
60+
node: Node,
61+
neutron_target_spec: NeutronTargetSpec,
62+
parameters_mapping: dict[str, Parameter],
63+
custom_delegation_options: CustomDelegationOptions,
64+
) -> bool:
65+
kernel_size, stride = (
66+
AdaptiveAvgPool2dConverter._get_equivalent_avg_pool_parameters(node)
5467
)
55-
t_op.builtin_options.padding = tflPadding.Padding.VALID
5668

57-
# AdaptiveAvgPool2d Node format: (Tensor self, SymInt[2] output_size)
69+
if custom_delegation_options.use_new_flow_neutron_c:
70+
# Requirements specified by the new Neutron flow documentation.
71+
72+
if not NodeConverter.uses_quantization_type_for_io(
73+
node,
74+
supported_types=[torch.int8, torch.uint8],
75+
input_indices=[0],
76+
output_indices=[0],
77+
):
78+
return False
79+
80+
if any(k > 4096 for k in kernel_size):
81+
return False
82+
83+
if any(s > 4096 for s in stride):
84+
return False
85+
86+
return True
87+
5888
def convert(self, node: Node):
59-
"""Convert '_adaptive_avg_pool2d' operator to TFLite 'AveragePool2D'."""
89+
"""Convert the '_adaptive_avg_pool2d' operator to NeutronIR 'AveragePool2D'.
90+
The ExecuTorch schema is:
91+
_adaptive_avg_pool2d(
92+
Tensor self,
93+
SymInt[2] output_size
94+
) -> Tensor
95+
"""
6096
self.assert_convertible(node)
6197

62-
input_size = node.args[0].meta["val"].shape
63-
output_size = node.args[1]
64-
6598
t_op = self._create_tflite_op_with_io_tensors(node)
99+
t_op.builtin_options = average_pool_2d_options.AveragePool2D()
100+
101+
kernel_size, stride = self._get_equivalent_avg_pool_parameters(node)
102+
103+
common.assign_2d_strides(t_op.builtin_options, stride)
104+
t_op.builtin_options.filter_h, t_op.builtin_options.filter_w = kernel_size
105+
t_op.builtin_options.padding = tflPadding.Padding.VALID
66106

67-
self._convert_adaptive_avg_pool_2d(input_size, output_size, t_op)
68107
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: 112 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.
@@ -13,12 +13,25 @@
1313
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
1414
from executorch.backends.nxp.tests.executors import (
1515
convert_run_compare,
16+
graph_contains_any_of_ops,
1617
ToChannelFirstPreprocess,
1718
ToChannelLastPreprocess,
1819
)
20+
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
21+
from executorch.backends.nxp.tests.model_output_comparator import (
22+
AllCloseOutputComparator,
23+
)
1924
from executorch.backends.nxp.tests.models import (
2025
AdaptiveAvgPool2dConvMeanDimModule,
2126
AdaptiveAvgPool2dConvModule,
27+
AdaptiveAvgPool2dModule,
28+
)
29+
30+
from executorch.backends.nxp.tests.nsys_testing import lower_run_compare
31+
32+
from executorch.backends.nxp.tests.ops_aliases import (
33+
AdaptiveAvgPool2D,
34+
ExecutorchDelegateCall,
2235
)
2336
from torch.export import ExportedProgram
2437
from executorch.backends.nxp.tests.use_qat import * # noqa F403
@@ -151,3 +164,101 @@ def test_adaptive_avg_pool_2d_mean_dim_quant_conversion(mocker, use_qat):
151164
tflite_output_preprocess=ToChannelFirstPreprocess(),
152165
input_data=input_data,
153166
)
167+
168+
169+
class TestAdaptiveAvgPool2DNewNeutronFlow:
170+
def test__basic_nsys_inference(self, mocker, use_qat):
171+
input_shape = (2, 3, 4, 6)
172+
output_size = (2, 3)
173+
model = AdaptiveAvgPool2dModule(output_size)
174+
graph_verifier = DetailedGraphVerifier(
175+
mocker,
176+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
177+
expected_non_delegated_ops={},
178+
)
179+
180+
output_comparator = AllCloseOutputComparator(
181+
3.9e-3
182+
) # Accept small error due to Neutron bug.
183+
184+
lower_run_compare(
185+
model,
186+
input_shape,
187+
graph_verifier,
188+
output_comparator=output_comparator,
189+
use_qat=use_qat,
190+
use_new_flow_neutron_c=True,
191+
)
192+
193+
@pytest.mark.xfail(
194+
strict=True,
195+
reason="Known Neutron bad compute issue. Will be fixed in Neutron SW 3.1.2.",
196+
)
197+
def test__know_neutron_issue(self, mocker):
198+
input_shape = (2, 3, 10, 15)
199+
output_size = (5, 5)
200+
model = AdaptiveAvgPool2dModule(output_size)
201+
graph_verifier = DetailedGraphVerifier(
202+
mocker,
203+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
204+
expected_non_delegated_ops={},
205+
)
206+
207+
# Use high tolerance so we notice when the issue is fixed.
208+
output_comparator = AllCloseOutputComparator(3.9e-3)
209+
210+
lower_run_compare(
211+
model,
212+
input_shape,
213+
graph_verifier,
214+
output_comparator=output_comparator,
215+
use_new_flow_neutron_c=True,
216+
)
217+
218+
def test__kernel_size_and_stride_limit(self, mocker):
219+
input_shape = (1, 3, 4, 4096) # input_size = (1, 4096)
220+
output_size = (
221+
2,
222+
1,
223+
) # If we reduced both dims to 1, ExecuTorch would replace the op with mean.
224+
# stride = input_size // output_size = 4096 / 1 = 4096
225+
# kernel_size = input_size - (output_size - 1) * stride = 4096 - 0 * 4096 = 4096
226+
227+
model = AdaptiveAvgPool2dModule(output_size)
228+
graph_verifier = DetailedGraphVerifier(
229+
mocker,
230+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
231+
expected_non_delegated_ops={},
232+
)
233+
234+
output_comparator = AllCloseOutputComparator(
235+
4e-3
236+
) # Accept small error due to Neutron bug.
237+
238+
lower_run_compare(
239+
model,
240+
input_shape,
241+
graph_verifier,
242+
output_comparator=output_comparator,
243+
use_new_flow_neutron_c=True,
244+
)
245+
246+
def test__kernel_size_and_stride_limit_exceeded(self):
247+
input_shape = (1, 3, 4, 4097) # input_size = (1, 4097)
248+
output_size = (
249+
2,
250+
1,
251+
) # If we reduced both dims to 1, ExecuTorch would replace the op with mean.
252+
# stride = input_size // output_size = 4097 / 1 = 4097
253+
# kernel_size = input_size - (output_size - 1) * stride = 4097 - 0 * 4097 = 4097
254+
255+
model = AdaptiveAvgPool2dModule(output_size)
256+
delegated_ep = to_quantized_edge_program(
257+
model, input_shape, use_new_flow_neutron_c=True
258+
).exported_program()
259+
260+
# Make sure the `adaptive_avg_pool2d` was NOT delegated.
261+
assert not graph_contains_any_of_ops(
262+
delegated_ep.graph, [ExecutorchDelegateCall]
263+
)
264+
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
@@ -13,6 +13,7 @@
1313

1414
Abs = exir_ops.edge.aten.abs.default
1515
AvgPool2D = exir_ops.edge.aten.avg_pool2d.default
16+
AdaptiveAvgPool2D = exir_ops.edge.aten._adaptive_avg_pool2d.default
1617
Bmm = exir_ops.edge.aten.bmm.default
1718
Convolution = exir_ops.edge.aten.convolution.default
1819
DequantizePerChannel = exir_ops.edge.quantized_decomposed.dequantize_per_channel.default

0 commit comments

Comments
 (0)