Skip to content

Commit da01860

Browse files
NXP backend: Enable adaptive_avg_pool2d with new Neutron flow. (pytorch#19540)
### Summary This PR reflects the new Neutron requirements for the enablement of `adaptive_avg_pool_2d` in NXP backend, ### Test plan Unit tests provided. cc @robert-kalmar @JakeStevens @digantdesai @rascani
1 parent 01ef73b commit da01860

4 files changed

Lines changed: 202 additions & 23 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: 126 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,114 @@ 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+
@pytest.mark.parametrize(
172+
"input_shape, output_size",
173+
[
174+
pytest.param((1, 3, 16, 16), (8, 8), id="H == W."),
175+
pytest.param((1, 3, 16, 8), (8, 2), id="H != W."),
176+
pytest.param(
177+
(2, 3, 4, 6),
178+
(2, 3),
179+
id="H != W, non multiples of num_macs, batch != 1.",
180+
),
181+
],
182+
)
183+
def test__basic_nsys_inference(self, mocker, use_qat, input_shape, output_size):
184+
model = AdaptiveAvgPool2dModule(output_size)
185+
graph_verifier = DetailedGraphVerifier(
186+
mocker,
187+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
188+
expected_non_delegated_ops={},
189+
)
190+
191+
output_comparator = AllCloseOutputComparator(
192+
7.84e-3
193+
) # Accept small error due to Neutron bug (AIR-14585).
194+
195+
lower_run_compare(
196+
model,
197+
input_shape,
198+
graph_verifier,
199+
RandomDatasetCreator(low=-1, high=1),
200+
output_comparator=output_comparator,
201+
use_qat=use_qat,
202+
use_new_flow_neutron_c=True,
203+
)
204+
205+
@pytest.mark.xfail(
206+
strict=True,
207+
reason="Known Neutron bad compute issue. Will be fixed in Neutron SW 3.1.2.",
208+
)
209+
def test__know_neutron_issue(self, mocker):
210+
input_shape = (2, 3, 10, 15)
211+
output_size = (5, 5)
212+
model = AdaptiveAvgPool2dModule(output_size)
213+
graph_verifier = DetailedGraphVerifier(
214+
mocker,
215+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
216+
expected_non_delegated_ops={},
217+
)
218+
219+
# Use high tolerance so we notice when the issue is fixed.
220+
output_comparator = AllCloseOutputComparator(7.8e-3)
221+
222+
lower_run_compare(
223+
model,
224+
input_shape,
225+
graph_verifier,
226+
RandomDatasetCreator(low=-1, high=1),
227+
output_comparator=output_comparator,
228+
use_new_flow_neutron_c=True,
229+
)
230+
231+
def test__kernel_size_and_stride_limit(self, mocker):
232+
input_shape = (1, 3, 4, 4096) # input_size = (1, 4096)
233+
output_size = (
234+
2,
235+
1,
236+
) # If we reduced both dims to 1, ExecuTorch would replace the op with mean.
237+
# stride = input_size // output_size = 4096 / 1 = 4096
238+
# kernel_size = input_size - (output_size - 1) * stride = 4096 - 0 * 4096 = 4096
239+
240+
model = AdaptiveAvgPool2dModule(output_size)
241+
graph_verifier = DetailedGraphVerifier(
242+
mocker,
243+
expected_delegated_ops={AdaptiveAvgPool2D: 1},
244+
expected_non_delegated_ops={},
245+
)
246+
247+
output_comparator = AllCloseOutputComparator(
248+
7.9e-3
249+
) # Accept small error due to Neutron bug (AIR-14585).
250+
251+
lower_run_compare(
252+
model,
253+
input_shape,
254+
graph_verifier,
255+
RandomDatasetCreator(low=-1, high=1),
256+
output_comparator=output_comparator,
257+
use_new_flow_neutron_c=True,
258+
)
259+
260+
def test__kernel_size_and_stride_limit_exceeded(self):
261+
input_shape = (1, 3, 4, 4097) # input_size = (1, 4097)
262+
output_size = (
263+
2,
264+
1,
265+
) # If we reduced both dims to 1, ExecuTorch would replace the op with mean.
266+
# stride = input_size // output_size = 4097 / 1 = 4097
267+
# kernel_size = input_size - (output_size - 1) * stride = 4097 - 0 * 4097 = 4097
268+
269+
model = AdaptiveAvgPool2dModule(output_size)
270+
delegated_ep = to_quantized_edge_program(
271+
model, input_shape, use_new_flow_neutron_c=True
272+
).exported_program()
273+
274+
# Make sure the `adaptive_avg_pool2d` was NOT delegated.
275+
assert not graph_contains_any_of_ops(
276+
delegated_ep.graph, [ExecutorchDelegateCall]
277+
)
278+
assert graph_contains_any_of_ops(delegated_ep.graph, [AdaptiveAvgPool2D])

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
ConstantPadND = exir_ops.edge.aten.constant_pad_nd.default

0 commit comments

Comments
 (0)