Skip to content

Commit c72bc87

Browse files
NXP backend: Enable aten.upsample_nearest2d with new Neutron flow. (pytorch#19796)
### Summary NXP backend: Enable `aten.upsample_nearest2d` with new Neutron flow. ### Test plan Unit tests provided. cc @robert-kalmar @JakeStevens @digantdesai @rascani
1 parent 007570a commit c72bc87

2 files changed

Lines changed: 220 additions & 31 deletions

File tree

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

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

66
import numpy as np
7+
import torch
78

89
from executorch.backends.nxp.backend.data_format import DataFormat, NXP_NODE_FORMAT
910
from executorch.backends.nxp.backend.edge_helper import node_has_well_defined_shape
1011
from executorch.backends.nxp.backend.ir.converter.node_converter import (
1112
CustomDelegationOptions,
13+
is_not_qdq_node,
1214
NodeConverter,
1315
)
1416
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options.resize_nearest_neighbor_options import (
1517
ResizeNearestNeighbor,
1618
)
1719
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
1820
from torch.fx import Node
21+
from torch.fx.passes.infra.partitioner import Partition
1922
from torch.nn import Parameter
2023

24+
HeightScale = float
25+
WidthScale = float
26+
2127

2228
# noinspection SpellCheckingInspection
2329
class UpsampleNearest2DConverter(NodeConverter):
2430

31+
@classmethod
32+
def supports_partitioning_result(
33+
cls,
34+
node: Node,
35+
partition_list: list[Partition],
36+
custom_delegation_options: CustomDelegationOptions,
37+
neutron_target_spec: NeutronTargetSpec,
38+
parameters_mapping: dict[str, Parameter],
39+
) -> bool:
40+
h_scale, w_scale = cls._get_effective_scales(node)
41+
is_alone_in_partition = cls.is_node_alone_in_partition(
42+
node, partition_list, filter_fn=is_not_qdq_node
43+
)
44+
45+
if is_alone_in_partition and h_scale == w_scale == 1:
46+
# The operator is a no-op, so the Neutron Converter will skip it. If it's the only node in the
47+
# partition, the graph would end up empty.
48+
return False
49+
50+
return True
51+
2552
@staticmethod
2653
def _is_supported_in_IR(
2754
node: Node,
@@ -36,6 +63,14 @@ def _is_supported_in_IR(
3663
" format. Please report this."
3764
)
3865

66+
# The conversion requires the output shape to be known and static.
67+
if not node_has_well_defined_shape(node):
68+
return False
69+
70+
if len(node.meta["val"].shape) != 4:
71+
# Unexpected case. The input should always be 4D.
72+
return False
73+
3974
return True
4075

4176
@staticmethod
@@ -45,39 +80,62 @@ def _is_supported_on_target(
4580
parameters_mapping: dict[str, Parameter],
4681
custom_delegation_options: CustomDelegationOptions,
4782
) -> bool:
48-
# Neutron requires static shapes.
49-
# neutron-converter/src/OperatorC/UpsamplePlugin.cpp?at=NEUTRON_SOFTWARE_2.2.3#74
50-
if not node_has_well_defined_shape(node):
51-
return False
52-
53-
if len(node.meta["val"].shape) != 4:
54-
# Unexpected case. The input should always be 4D.
55-
return False
56-
57-
# The tensors here use the channels first format (NCHW).
83+
# The tensors are always 4D and use the channels first format (NCHW).
5884
_, in_c, in_h, in_w = node.all_input_nodes[0].meta["val"].shape
5985
_, _, out_h, out_w = node.meta["val"].shape
6086

61-
# Neutron supports only the doubling and quadrupleing of both height and width at the same time.
62-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.3#768
63-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.3#778
64-
supported_scales = [2, 4]
65-
if not any(
66-
in_h * scale == out_h and in_w * scale == out_w
67-
for scale in supported_scales
68-
):
69-
return False
70-
71-
# Neutron requires the input channels to be a multiple of `num_macs`.
72-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.3#767
73-
if in_c % neutron_target_spec.get_num_macs() != 0:
74-
return False
87+
if custom_delegation_options.use_new_flow_neutron_c:
88+
# Requirements specified by the new Neutron flow documentation.
89+
90+
if not NodeConverter.uses_quantization_type_for_io(
91+
node,
92+
supported_types=[torch.int8, torch.uint8],
93+
input_indices=[0],
94+
output_indices=[0],
95+
):
96+
return False
97+
98+
supported_scales = [1, 2, 4, 8]
99+
h_scale, w_scale = UpsampleNearest2DConverter._get_effective_scales(node)
100+
# The H and W scales don't need to be equal but both must be supported.
101+
if (h_scale not in supported_scales) or (w_scale not in supported_scales):
102+
return False
103+
104+
else:
105+
# Requirements of the old Neutron flow.
106+
107+
# Neutron supports only the doubling and quadrupleing of both height and width at the same time.
108+
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.3#768
109+
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.3#778
110+
supported_scales = [2, 4]
111+
if not any(
112+
in_h * scale == out_h and in_w * scale == out_w
113+
for scale in supported_scales
114+
):
115+
return False
116+
117+
# Neutron requires the input channels to be a multiple of `num_macs`.
118+
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.3#767
119+
if in_c % neutron_target_spec.get_num_macs() != 0:
120+
return False
75121

76122
return True
77123

124+
@staticmethod
125+
def _get_effective_scales(node: Node) -> tuple[HeightScale, WidthScale]:
126+
# Neutron supports variants where `align_corners=False` and `align_corners=True`. ExecuTorch doesn't have this
127+
# parameter. Its behavior is equivalent to `align_corners=False`. Hence, the scale calculation corresponds to
128+
# the `align_corners=False` case in the Neutron documentation.
129+
_, _, in_h, in_w = node.all_input_nodes[0].meta["val"].shape
130+
_, _, out_h, out_w = node.meta["val"].shape
131+
h_scale = out_h / in_h
132+
w_scale = out_w / in_w
133+
134+
return h_scale, w_scale
135+
78136
def convert(self, node: Node):
79137
"""Convert the `aten.upsample_nearest2d.vec` operator to Neutron IR `ResizeNearestNeighbor`.
80-
The schema is:
138+
The ExecuTorch schema is:
81139
aten::upsample_nearest2d.vec(
82140
Tensor input,
83141
SymInt[]? output_size,
@@ -90,6 +148,8 @@ def convert(self, node: Node):
90148
x = t_op.tmp_inputs[0]
91149
y = t_op.tmp_outputs[0]
92150

151+
# Neutron supports variants where `align_corners=False` and `align_corners=True`. ExecuTorch doesn't have this
152+
# parameter. Its behavior is equivalent to `align_corners=False` and `half_pixel_centers=False`.
93153
t_op.builtin_options = ResizeNearestNeighbor(False, False)
94154

95155
# The `aten.upsample_nearest2d` can use either the `size` attribute or the `scale_factor` to define the output

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

Lines changed: 135 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,30 @@
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
911

1012
from executorch.backends.nxp.backend.edge_program_converter import (
1113
EdgeProgramToIRConverter,
1214
)
15+
from executorch.backends.nxp.tests.dataset_creator import RandomDatasetCreator
1316
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
1417
from executorch.backends.nxp.tests.executors import (
1518
convert_run_compare,
1619
graph_contains_any_of_ops,
1720
ToChannelFirstPreprocess,
1821
ToChannelLastPreprocess,
1922
)
20-
from executorch.exir.dialects._ops import ops as exir_ops
23+
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
24+
from executorch.backends.nxp.tests.nsys_testing import lower_run_compare
25+
from executorch.backends.nxp.tests.ops_aliases import (
26+
AddTensor,
27+
ExecutorchDelegateCall,
28+
UpsampleNearest2D,
29+
)
30+
from executorch.backends.nxp.tests.use_qat import * # noqa F403
2131

2232

2333
@pytest.fixture(autouse=True)
@@ -26,11 +36,6 @@ def reseed_model_per_test_run():
2636
np.random.seed(23)
2737

2838

29-
# noinspection PyProtectedMember
30-
ExecutorchDelegateCall = torch.ops.higher_order.executorch_call_delegate
31-
UpsampleNearest2D = exir_ops.edge.aten.upsample_nearest2d.vec
32-
33-
3439
class UpsampleNearestModule(torch.nn.Module):
3540

3641
def __init__(self, size=None, scale=None):
@@ -41,6 +46,13 @@ def forward(self, x):
4146
return self.upsample(x)
4247

4348

49+
class UpsampleNearestAddModule(UpsampleNearestModule):
50+
51+
def forward(self, x):
52+
x = super().forward(x)
53+
return x + x
54+
55+
4456
@pytest.mark.parametrize(
4557
"input_shape, size",
4658
[
@@ -181,3 +193,120 @@ def test_convert_upsample_nearest2d__no_delegation__unsupported_size(input_shape
181193
# Make sure the `upsample` was NOT delegated (size != double of input).
182194
assert not graph_contains_any_of_ops(delegated_ep.graph, [ExecutorchDelegateCall])
183195
assert graph_contains_any_of_ops(delegated_ep.graph, [UpsampleNearest2D])
196+
197+
198+
class TestUpsampleNearest2DNewNeutronFlow:
199+
200+
# noinspection PyMethodMayBeStatic
201+
def assert_delegated(
202+
self,
203+
model,
204+
input_shape,
205+
mocker,
206+
use_qat=False,
207+
expected_delegated_ops=None,
208+
):
209+
if expected_delegated_ops is None:
210+
expected_delegated_ops = {UpsampleNearest2D: 1}
211+
212+
graph_verifier = DetailedGraphVerifier(
213+
mocker,
214+
expected_delegated_ops=expected_delegated_ops,
215+
expected_non_delegated_ops={},
216+
)
217+
218+
# Cover also negative values to thoroughly test the operator.
219+
dataset_creator = RandomDatasetCreator(low=-2, high=2)
220+
221+
lower_run_compare(
222+
model,
223+
input_shape,
224+
graph_verifier,
225+
dataset_creator,
226+
use_qat=use_qat,
227+
use_new_flow_neutron_c=True, # Use the new flow.
228+
)
229+
230+
# noinspection PyMethodMayBeStatic
231+
def assert_not_delegated(self, model, input_shape):
232+
delegated_ep = to_quantized_edge_program(
233+
model, input_shape, use_new_flow_neutron_c=True
234+
).exported_program()
235+
236+
assert not graph_contains_any_of_ops(
237+
delegated_ep.graph, [ExecutorchDelegateCall]
238+
)
239+
assert graph_contains_any_of_ops(delegated_ep.graph, [UpsampleNearest2D])
240+
241+
def test__qat(self, mocker, use_qat):
242+
input_shape = (1, 2, 3, 4)
243+
output_size = (6, 8)
244+
model = UpsampleNearestModule(size=output_size)
245+
self.assert_delegated(model, input_shape, mocker, use_qat=use_qat)
246+
247+
@pytest.mark.parametrize(
248+
"input_shape, output_size",
249+
[
250+
pytest.param((1, 2, 3, 4), (6, 8), id="batch=1, scale_h=scale_w=2"),
251+
pytest.param((1, 2, 3, 3), 6, id="batch=1, scale_h=scale_w=2, scalar size"),
252+
pytest.param(
253+
(3, 3, 3, 5),
254+
(6, 5),
255+
id="batch=3, scale_h=2, scale_w=1 (no num_macs multiples)",
256+
),
257+
pytest.param((2, 2, 3, 4), (3, 16), id="batch=2, scale_h=1, scale_w=4"),
258+
pytest.param((2, 2, 3, 4), (24, 8), id="batch=2, scale_h=8, scale_w=2"),
259+
],
260+
)
261+
def test__output_size(self, mocker, input_shape, output_size):
262+
model = UpsampleNearestModule(size=output_size)
263+
self.assert_delegated(model, input_shape, mocker)
264+
265+
def test__output_size__unsupported(self):
266+
input_shape = (1, 2, 3, 4)
267+
output_size = (9, 12) # scale = (3, 3)
268+
model = UpsampleNearestModule(size=output_size)
269+
self.assert_not_delegated(model, input_shape)
270+
271+
@pytest.mark.parametrize(
272+
"input_shape, scale",
273+
[
274+
pytest.param((1, 2, 3, 4), (2, 2), id="batch=1, scale_h=scale_w=2"),
275+
pytest.param(
276+
(1, 2, 3, 4), 4, id="batch=1, scale_h=scale_w=4, scalar scale"
277+
),
278+
pytest.param(
279+
(3, 3, 3, 5),
280+
(2, 1),
281+
id="batch=3, scale_h=2, scale_w=1 (no num_macs multiples)",
282+
),
283+
pytest.param((2, 2, 3, 4), (4, 1), id="batch=2, scale_h=4, scale_w=1"),
284+
pytest.param((2, 2, 3, 4), (2, 8), id="batch=2, scale_h=2, scale_w=8"),
285+
],
286+
)
287+
def test__scales(self, mocker, input_shape, scale):
288+
model = UpsampleNearestModule(scale=scale)
289+
self.assert_delegated(model, input_shape, mocker)
290+
291+
def test__scales__unsupported(self):
292+
input_shape = (1, 2, 3, 4)
293+
scale = (3, 3)
294+
model = UpsampleNearestModule(scale=scale)
295+
self.assert_not_delegated(model, input_shape)
296+
297+
def test__noop__alone_in_partition__not_delegated(self):
298+
input_shape = (1, 2, 3, 4)
299+
scale = 1
300+
model = UpsampleNearestModule(scale=scale)
301+
self.assert_not_delegated(model, input_shape)
302+
303+
def test__noop__not_alone_in_partition__delegated(self, mocker):
304+
input_shape = (1, 2, 3, 4)
305+
scale = 1
306+
model = UpsampleNearestAddModule(scale=scale)
307+
self.assert_delegated(
308+
model,
309+
input_shape,
310+
mocker,
311+
expected_delegated_ops={UpsampleNearest2D: 1, AddTensor: 1},
312+
)

0 commit comments

Comments
 (0)