Skip to content

Commit 278b617

Browse files
NXP backend: Enable prelu with new Neutron flow
1 parent c1ff55a commit 278b617

4 files changed

Lines changed: 159 additions & 146 deletions

File tree

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

Lines changed: 7 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
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

6-
from executorch.backends.nxp.backend.data_format import NXP_NODE_FORMAT
6+
import torch
7+
78
from executorch.backends.nxp.backend.ir.converter.node_converter import (
89
CustomDelegationOptions,
910
NodeConverter,
@@ -24,38 +25,17 @@ def _is_supported_on_target(
2425
parameters_mapping: dict[str, Parameter],
2526
custom_delegation_options: CustomDelegationOptions,
2627
) -> bool:
27-
node_shape = node.meta["val"].shape
28-
rank = len(node_shape)
29-
30-
# According to Neutron spec., PReLU can be done only on 4D tensors
31-
if rank != 4:
32-
return False
33-
34-
ch_idx, h_idx, w_idx = PReLUConverter._get_channel_spatial_indices(node)
35-
# According to Neutron spec., size of channels must be divisible by num_macs.
36-
num_macs = neutron_target_spec.get_num_macs()
37-
if node_shape[ch_idx] % num_macs != 0:
28+
if not NodeConverter.inputs_satisfy_broadcast_condition(node):
3829
return False
3930

40-
# According to Neutron spec., height * width cannot be greater than a given constant.
41-
if node_shape[w_idx] * node_shape[h_idx] > 4096:
31+
supported_types = [torch.int8, torch.uint8]
32+
if not NodeConverter.uses_quantization_type_for_io(
33+
node, supported_types, [0, 1], [0]
34+
):
4235
return False
4336

4437
return True
4538

46-
@staticmethod
47-
def _get_channel_spatial_indices(node: Node):
48-
if node.meta[NXP_NODE_FORMAT].is_channels_first():
49-
ch_idx = 1
50-
h_idx = 2
51-
w_idx = 3
52-
else:
53-
ch_idx = 3
54-
h_idx = 1
55-
w_idx = 2
56-
57-
return ch_idx, h_idx, w_idx
58-
5939
@staticmethod
6040
def _is_supported_in_IR(
6141
node: Node,

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

Lines changed: 135 additions & 118 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,32 @@
99
from executorch.backends.nxp.backend.edge_program_converter import (
1010
EdgeProgramToIRConverter,
1111
)
12-
from executorch.backends.nxp.tests.executors import (
13-
convert_run_compare,
14-
graph_contains_any_of_ops,
12+
from executorch.backends.nxp.tests.executors import graph_contains_any_of_ops
13+
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
14+
from executorch.backends.nxp.tests.model_output_comparator import (
15+
AllCloseOutputComparator,
1516
)
1617
from executorch.backends.nxp.tests.models import (
18+
ConvPReLUModule,
1719
LinearPReLUModule,
1820
TwoPartitionPReLUModel,
1921
)
22+
23+
from executorch.backends.nxp.tests.nsys_testing import lower_run_compare
24+
from executorch.backends.nxp.tests.ops_aliases import (
25+
AddMm,
26+
Convolution,
27+
ExecutorchDelegateCall,
28+
GtScalar,
29+
MulTensor,
30+
PermuteCopy,
31+
Prelu,
32+
ViewCopy,
33+
WhereSelf,
34+
)
2035
from torch.export import ExportedProgram
2136
from executorch.backends.nxp.tests.use_qat import * # noqa F403
2237
from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program
23-
from executorch.exir.dialects._ops import ops as exir_ops
2438

2539

2640
@pytest.fixture(autouse=True)
@@ -29,123 +43,126 @@ def reseed_model_per_test_run():
2943
np.random.seed(23)
3044

3145

32-
# noinspection PyProtectedMember
33-
ExecutorchDelegateCall = torch.ops.higher_order.executorch_call_delegate
34-
35-
36-
@pytest.mark.parametrize(
37-
"input_shape",
38-
[
39-
pytest.param((1, 8, 24, 32), id="4D."),
40-
],
41-
)
42-
def test_prelu_with_linear_quant_conversion(mocker, input_shape):
43-
converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program")
44-
45-
# Run conversion
46-
channels = input_shape[-1]
47-
edge_program = to_quantized_edge_program(
48-
LinearPReLUModule(in_features=channels, out_features=channels),
49-
input_shape,
50-
).exported_program()
51-
52-
# Capture generated entities
53-
neutron_ir_model, _ = converter_spy.spy_return
54-
exported_program: ExportedProgram = converter_spy.call_args.args[1]
55-
56-
# Check `prelu` was not decomposed into simpler edge operators
57-
assert not graph_contains_any_of_ops(
58-
exported_program.graph,
46+
class TestPreluConverter:
47+
@pytest.mark.parametrize(
48+
"input_shape",
5949
[
60-
exir_ops.edge.aten.gt.Scalar,
61-
exir_ops.edge.aten.mul.Tensor,
62-
exir_ops.edge.aten.where.self,
50+
pytest.param((1,), id="1D."),
51+
pytest.param(
52+
(36, 487),
53+
id="2D incorrect.",
54+
marks=pytest.mark.xfail(
55+
reason="AIR-14737: incorrect results", strict=True
56+
),
57+
),
58+
pytest.param(
59+
(87, 842),
60+
id="2D incorrect alt.",
61+
marks=pytest.mark.xfail(
62+
reason="AIR-14737: incorrect results", strict=True
63+
),
64+
),
65+
pytest.param((7, 83), id="2D."),
66+
pytest.param(
67+
(1, 43, 183),
68+
id="3D incorrect alt.",
69+
marks=pytest.mark.xfail(
70+
reason="AIR-14737: incorrect results", strict=True
71+
),
72+
),
73+
pytest.param((1, 43, 93), id="3D."),
74+
pytest.param((1, 4, 7, 8), id="4D."),
75+
pytest.param((1, 4, 3, 4, 14), id="5D."),
6376
],
6477
)
65-
66-
assert graph_contains_any_of_ops(
67-
exported_program.graph,
68-
[exir_ops.edge.aten.prelu.default],
69-
)
70-
71-
# Check `prelu` was delegated
72-
assert not graph_contains_any_of_ops(
73-
edge_program.graph,
74-
[exir_ops.edge.aten.prelu.default],
75-
)
76-
77-
input_data = (
78-
(2 * np.random.random(input_shape).astype(np.float32) - 1) * 50
79-
).astype(np.int8)
80-
81-
convert_run_compare(exported_program, input_data, tfl_model=neutron_ir_model)
82-
83-
84-
@pytest.mark.parametrize(
85-
"input_shape",
86-
[
87-
pytest.param((1, 8, 24, 32), id="4D."),
88-
],
89-
)
90-
def test_prelu_2_partitions(mocker, input_shape):
91-
# TODO (Martin) Add a channels last dim order variant of this test to verify correct partitioning.
92-
# Run conversion
93-
edge_program = to_quantized_edge_program(
94-
TwoPartitionPReLUModel(), [input_shape, input_shape]
95-
).exported_program()
96-
97-
# Check `prelu` was delegated
98-
assert not graph_contains_any_of_ops(
99-
edge_program.graph,
100-
[exir_ops.edge.aten.prelu.default],
101-
)
102-
103-
# Check there are two partitions
104-
edge_nodes = list(edge_program.graph.nodes)
105-
assert sum(n.target == ExecutorchDelegateCall for n in edge_nodes) == 2
106-
107-
108-
@pytest.mark.parametrize(
109-
"input_shape",
110-
[
111-
pytest.param((1,), id="1D not supported."),
112-
pytest.param((1, 8), id="2D not supported."),
113-
pytest.param((1, 8, 16), id="3D not supported."),
114-
pytest.param((1, 8, 16, 32, 64), id="5D not supported."),
115-
pytest.param((1, 8, 16, 31), id="channels must be divisible by NUM_MACS"),
116-
pytest.param((1, 8, 1024, 8), id="width*height is too big (limit 4096)"),
117-
],
118-
)
119-
def test_prelu__no_delegation__unsupported_conversion(mocker, input_shape):
120-
# Run conversion
121-
channels = input_shape[-1]
122-
edge_program = to_quantized_edge_program(
123-
LinearPReLUModule(in_features=channels, out_features=channels),
124-
input_shape,
125-
).exported_program()
126-
127-
# Check `prelu` was not delegated (only `linear` was)
128-
edge_nodes = list(edge_program.graph.nodes)
129-
assert sum(n.target == ExecutorchDelegateCall for n in edge_nodes) == 1
130-
131-
# Check `prelu` was decomposed into simpler edge operators
132-
assert graph_contains_any_of_ops(
133-
edge_program.graph,
134-
[
135-
exir_ops.edge.aten.gt.Scalar,
136-
],
137-
)
138-
139-
assert graph_contains_any_of_ops(
140-
edge_program.graph,
141-
[
142-
exir_ops.edge.aten.mul.Tensor,
143-
],
144-
)
145-
146-
assert graph_contains_any_of_ops(
147-
edge_program.graph,
78+
def test__basic_nsys_inference(self, mocker, request, input_shape):
79+
channels = input_shape[-1]
80+
rank = len(input_shape)
81+
model = LinearPReLUModule(in_features=channels, out_features=channels)
82+
graph_verifier = DetailedGraphVerifier(
83+
mocker,
84+
expected_delegated_ops={
85+
Prelu: 1,
86+
AddMm: 1,
87+
PermuteCopy: 1,
88+
ViewCopy: 0 if rank == 2 else 2,
89+
},
90+
expected_non_delegated_ops={},
91+
)
92+
comparator = AllCloseOutputComparator(atol=1)
93+
converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program")
94+
95+
lower_run_compare(
96+
model,
97+
input_shape,
98+
graph_verifier,
99+
request,
100+
output_comparator=comparator,
101+
remove_quant_io_ops=True,
102+
)
103+
104+
# Capture generated entities
105+
neutron_ir_model, _ = converter_spy.spy_return
106+
exported_program: ExportedProgram = converter_spy.call_args.args[1]
107+
108+
# Check `prelu` was not decomposed into simpler edge operators
109+
assert not graph_contains_any_of_ops(
110+
exported_program.graph,
111+
[
112+
GtScalar,
113+
MulTensor,
114+
WhereSelf,
115+
],
116+
)
117+
118+
def test_prelu_2_partitions(self):
119+
input_shape = (1, 8, 24, 32)
120+
# Run conversion
121+
edge_program = to_quantized_edge_program(
122+
TwoPartitionPReLUModel(), [input_shape, input_shape]
123+
).exported_program()
124+
125+
# Check `prelu` was delegated
126+
assert not graph_contains_any_of_ops(
127+
edge_program.graph,
128+
[Prelu],
129+
)
130+
131+
# Check there are two partitions
132+
edge_nodes = list(edge_program.graph.nodes)
133+
assert sum(n.target == ExecutorchDelegateCall for n in edge_nodes) == 2
134+
135+
@pytest.mark.parametrize(
136+
"input_shape",
148137
[
149-
exir_ops.edge.aten.where.self,
138+
pytest.param((1, 8, 42, 24), id="4D."),
139+
pytest.param(
140+
(1, 8, 42, 21),
141+
id="4D.",
142+
marks=pytest.mark.xfail(
143+
reason="AIR-14737: incorrect results", strict=True
144+
),
145+
),
150146
],
151147
)
148+
def test__w_conv(self, mocker, request, input_shape):
149+
channels = input_shape[1]
150+
model = ConvPReLUModule(in_channels=channels)
151+
graph_verifier = DetailedGraphVerifier(
152+
mocker,
153+
expected_delegated_ops={
154+
Prelu: 1,
155+
Convolution: 1,
156+
},
157+
expected_non_delegated_ops={},
158+
)
159+
comparator = AllCloseOutputComparator(atol=1)
160+
161+
lower_run_compare(
162+
model,
163+
input_shape,
164+
graph_verifier,
165+
request,
166+
output_comparator=comparator,
167+
remove_quant_io_ops=True,
168+
)

backends/nxp/tests/models.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -887,6 +887,20 @@ def forward(self, x):
887887
return self.prelu(x)
888888

889889

890+
class ConvPReLUModule(torch.nn.Module):
891+
def __init__(self, in_channels, num_parameters=1):
892+
super().__init__()
893+
894+
self.conv = Conv2dModule(
895+
in_channels=in_channels, out_channels=in_channels, stride=1, padding=1
896+
)
897+
self.prelu = torch.nn.PReLU(num_parameters)
898+
899+
def forward(self, x):
900+
x = self.conv(x)
901+
return self.prelu(x)
902+
903+
890904
class TwoPartitionPReLUModel(torch.nn.Module):
891905
def __init__(self):
892906
super().__init__()

backends/nxp/tests/ops_aliases.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
DequantizePerTensor = exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default
2828
ExecutorchDelegateCall = torch.ops.higher_order.executorch_call_delegate
2929
GetItem = operator.getitem
30+
GtScalar = exir_ops.edge.aten.gt.Scalar
3031
HardTanh = exir_ops.edge.aten.hardtanh.default
3132
HardTanh_ = exir_ops.edge.aten.hardtanh_.default
3233
LeakyRelu = exir_ops.edge.aten.leaky_relu.default
@@ -35,9 +36,9 @@
3536
MeanDim = exir_ops.edge.aten.mean.dim
3637
MulTensor = exir_ops.edge.aten.mul.Tensor
3738
PermuteCopy = exir_ops.edge.aten.permute_copy.default
39+
Prelu = exir_ops.edge.aten.prelu.default
3840
QuantizePerChannel = exir_ops.edge.quantized_decomposed.quantize_per_channel.default
3941
QuantizePerTensor = exir_ops.edge.quantized_decomposed.quantize_per_tensor.default
40-
PermuteCopy = exir_ops.edge.aten.permute_copy.default
4142
Relu = exir_ops.edge.aten.relu.default
4243
Sigmoid = exir_ops.edge.aten.sigmoid.default
4344
Slice = exir_ops.edge.aten.slice.Tensor
@@ -53,3 +54,4 @@
5354
UpsampleBilinear2D = exir_ops.edge.aten.upsample_bilinear2d.vec
5455
UpsampleNearest2D = exir_ops.edge.aten.upsample_nearest2d.vec
5556
ViewCopy = exir_ops.edge.aten.view_copy.default
57+
WhereSelf = exir_ops.edge.aten.where.self

0 commit comments

Comments
 (0)