Skip to content

Commit 1d9dbf0

Browse files
committed
NXP backend: Test max_pool2d with new Neutron flow.
1 parent 48a8d58 commit 1d9dbf0

3 files changed

Lines changed: 160 additions & 28 deletions

File tree

backends/nxp/backend/edge_helper.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,8 +441,10 @@ def output_quantization_type(
441441
│ <returned type>
442442
"""
443443
users = list(node.users)
444-
if len(users) == 1:
444+
if output_index is None:
445+
# Basic QDQ case (without getitem nodes).
445446
if not _is_quantize(quantize_node := users[0]):
447+
# Broken QDQ schema.
446448
return None
447449

448450
else: # Multiple users

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

Lines changed: 53 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import operator
77

88
import numpy as np
9+
import torch
910

1011
from executorch.backends.nxp.backend.edge_helper import try_get_arg
1112
from executorch.backends.nxp.backend.ir.converter.conversion import (
@@ -73,32 +74,58 @@ def _is_supported_on_target(
7374
MaxPool2DWithIndicesConverter._get_node_args(node)
7475
)
7576

76-
output_shape = node.meta["val"][0].shape # Shape of the main output (index 0)
77-
if output_shape[0] != 1:
78-
# /neutron-converter/src/OperatorC/MaxPoolPlugin.cpp?at=NEUTRON_SOFTWARE_2.2.2#106
79-
return False
80-
81-
# Neutron only has a restriction on `stride_h`. `stride_w` is not restricted.
82-
stride_h = stride[0]
83-
if stride_h not in (1, 2):
84-
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#901
85-
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#923
86-
return False
87-
88-
channels = output_shape[1]
89-
if channels % neutron_target_spec.get_num_macs() != 0:
90-
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#903
91-
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#925
92-
return False
93-
94-
if any(pad > kernel_dim for pad, kernel_dim in zip(padding, kernel_size)):
95-
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#904-907
96-
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#926-929
97-
98-
# Cannot be tested as PyTorch crashes in this case. It requires the padding to be at most half of the
99-
# effective kernel size, which is an even stricter requirement than what Neutron imposes.
100-
# https://github.com/pytorch/pytorch/blob/449b1768410104d3ed79d3bcfe4ba1d65c7f22c0/torch/_meta_registrations.py#L4483-L4489
101-
return False
77+
if custom_delegation_options.use_new_flow_neutron_c:
78+
# Requirements specified by the new Neutron flow documentation.
79+
80+
supported_types = [torch.int8, torch.uint8]
81+
if not NodeConverter.uses_quantization_type_for_io(
82+
node, supported_types, [0], [0]
83+
):
84+
return False
85+
86+
maximum_supported_kernel_size = 4096
87+
# If there is no padding, Neutron allows maximum stride of 4096. Otherwise, it's 32. But the converter
88+
# always inserts a `Pad` operator to add the padding, so the `AvgPool` never pads it's input itself, so
89+
# 4096 is always the limit. And similarly, the `AvgPool` input padding limitation does not apply either.
90+
maximum_supported_stride = 4096
91+
92+
if any(k > maximum_supported_kernel_size for k in kernel_size):
93+
return False
94+
if any(s > maximum_supported_stride for s in stride):
95+
return False
96+
if any(p > k for p, k in zip(padding, kernel_size, strict=True)):
97+
# Padding must be smaller than the kernel size.
98+
return False
99+
100+
else:
101+
output_shape = node.meta["val"][
102+
0
103+
].shape # Shape of the main output (index 0)
104+
if output_shape[0] != 1:
105+
# /neutron-converter/src/OperatorC/MaxPoolPlugin.cpp?at=NEUTRON_SOFTWARE_2.2.2#106
106+
return False
107+
108+
# Neutron only has a restriction on `stride_h`. `stride_w` is not restricted.
109+
stride_h = stride[0]
110+
if stride_h not in (1, 2):
111+
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#901
112+
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#923
113+
return False
114+
115+
channels = output_shape[1]
116+
if channels % neutron_target_spec.get_num_macs() != 0:
117+
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#903
118+
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#925
119+
return False
120+
121+
if any(pad > kernel_dim for pad, kernel_dim in zip(padding, kernel_size)):
122+
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#904-907
123+
# /neutron-library/src/utils/NeutronLibraryInterrogation.cpp?at=refs%2Ftags%2FNEUTRON_SOFTWARE_2.2.2#926-929
124+
125+
# Cannot be tested as PyTorch crashes in this case. It requires the padding to be at most half of the
126+
# effective kernel size, which is an even stricter requirement than what Neutron imposes.
127+
# https://github.com/pytorch/pytorch/blob/449b1768410104d3ed79d3bcfe4ba1d65c7f22c0/torch/_meta_registrations.py#L4483-L4489
128+
return False
102129

103130
return True
104131

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

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
ToChannelFirstPreprocess,
2020
ToChannelLastPreprocess,
2121
)
22+
from executorch.backends.nxp.tests.graph_verifier import BaseGraphVerifier
23+
from executorch.backends.nxp.tests.nsys_testing import lower_run_compare
2224
from executorch.backends.nxp.tests.use_qat import * # noqa F403
2325

2426
# noinspection PyProtectedMember
@@ -47,7 +49,7 @@ def forward(self, x):
4749

4850

4951
class MaxPool2dModule(torch.nn.Module):
50-
def __init__(self, kernel_size=3, **kwargs):
52+
def __init__(self, kernel_size: int | tuple[int, ...] = 3, **kwargs):
5153
super().__init__()
5254
self.max_pool2d = torch.nn.MaxPool2d(kernel_size, **kwargs)
5355

@@ -250,3 +252,104 @@ def test_max_pool_2d__from_1d(self, mocker):
250252
tflite_input_preprocess=ToChannelLastPreprocess(),
251253
tflite_output_preprocess=ToChannelFirstPreprocess(),
252254
)
255+
256+
257+
class TestMaxPool2DNewNeutronFlow:
258+
# noinspection PyMethodMayBeStatic
259+
def assert_delegated(self, model, input_shape):
260+
graph_verifier = BaseGraphVerifier(
261+
exp_num_delegate_call_nodes=1, # Delegated MaxPool.
262+
exp_non_delegated_nodes=[],
263+
)
264+
265+
lower_run_compare(
266+
model, input_shape, graph_verifier, use_new_flow_neutron_c=True
267+
)
268+
269+
# noinspection PyMethodMayBeStatic
270+
def assert_not_delegated(self, model, input_shape):
271+
delegated_ep = to_quantized_edge_program(
272+
model, input_shape, use_new_flow_neutron_c=True
273+
).exported_program()
274+
275+
# Make sure the `max_pool2d` was NOT delegated.
276+
assert not graph_contains_any_of_ops(
277+
delegated_ep.graph, [ExecutorchDelegateCall]
278+
)
279+
assert graph_contains_any_of_ops(delegated_ep.graph, [MaxPool2D])
280+
281+
def test__basic_nsys_inference(self):
282+
input_shape = (2, 4, 6, 7) # The old flow limited the batch size to 1.
283+
model = MaxPool2dModule()
284+
self.assert_delegated(model, input_shape)
285+
286+
def test__kernel_size_limit(self):
287+
kernel_size = (1, 4096)
288+
input_shape = (1, 4) + kernel_size
289+
model = MaxPool2dModule(kernel_size)
290+
self.assert_delegated(model, input_shape)
291+
292+
def test__kernel_size_limit_exceeded(self):
293+
kernel_size = (1, 4097) # Exceeds the kernel size limit.
294+
input_shape = (1, 4) + kernel_size
295+
model = MaxPool2dModule(kernel_size)
296+
self.assert_not_delegated(model, input_shape)
297+
298+
def test__stride_limit__no_padding(self):
299+
stride = 4096
300+
input_shape = (1, 4, 1, 4096)
301+
model = MaxPool2dModule(1, stride=stride)
302+
self.assert_delegated(model, input_shape)
303+
304+
def test__stride_limit_exceeded__no_padding(self):
305+
stride = 4097 # Exceeds the stride limit.
306+
input_shape = (1, 4, 1, 4096)
307+
model = MaxPool2dModule(1, stride=stride)
308+
self.assert_not_delegated(model, input_shape)
309+
310+
def test__stride_limit__padding(self):
311+
padding = 1
312+
stride = 4096
313+
input_shape = (1, 2, 3, stride)
314+
model = MaxPool2dModule(3, stride=stride, padding=padding)
315+
self.assert_delegated(model, input_shape)
316+
317+
def test__stride_limit_exceeded__padding(self):
318+
padding = 1
319+
stride = 4097 # Exceeds the stride limit.
320+
input_shape = (1, 2, 3, stride)
321+
model = MaxPool2dModule(3, stride=stride, padding=padding)
322+
self.assert_not_delegated(model, input_shape)
323+
324+
@pytest.mark.skip(
325+
reason="Large padding requires large kernel size which results in an extremely slow test."
326+
)
327+
def test__padding_limit(self):
328+
# As the padding is added wia a `Pad` operator (not the `AvgPool` arguments), there is no limit to the padded
329+
# value. But as padding can be at most half of the kernel size (PyTorch requirement) and kernel size is limited
330+
# to 4096, padding of 2048 is the limit.
331+
padding = 2048
332+
kernel_size = padding * 2
333+
input_shape = (1, 1, 2, 3)
334+
model = MaxPool2dModule(kernel_size, padding=padding)
335+
self.assert_delegated(model, input_shape)
336+
337+
def test__padding__avg_pool_limit_exceeded(self):
338+
# NeutronIR `AvgPool` padding is limited to 32. But as it is added by the `Pad` operator instead, there is no
339+
# limit. This tests ensures the `AvgPool` padding limit is not a problem.
340+
padding = 33
341+
kernel_size = padding * 2
342+
input_shape = (1, 2, 3, 4)
343+
model = MaxPool2dModule(kernel_size, padding=padding)
344+
self.assert_delegated(model, input_shape)
345+
346+
def test__padding_to_kernel_ratio_exceeded(self):
347+
# Both PyTorch and Neutron require the padding to be at most half of the kernel size.
348+
kernel_size = 3
349+
padding = 2 # More than half of the kernel size.
350+
input_shape = (1, 2, 3, 4)
351+
model = MaxPool2dModule(kernel_size, padding=padding)
352+
with pytest.raises(
353+
RuntimeError, match="pad should be at most half of effective kernel size"
354+
):
355+
to_quantized_edge_program(model, input_shape, use_new_flow_neutron_c=True)

0 commit comments

Comments
 (0)