Skip to content

Commit f07ddec

Browse files
authored
NXP backend: New Neutron-C flow support for ReLU (#19275)
### Summary Removes unnecessary checks for ReLU conversion when using new Neutron-C flow. ### Test plan New unit test cases were added. cc @digantdesai @robert-kalmar @JakeStevens
1 parent 03d1818 commit f07ddec

11 files changed

Lines changed: 292 additions & 208 deletions

File tree

backends/nxp/backend/graph_utils.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,13 @@
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+
import numpy as np
67
import torch
8+
from executorch.backends.nxp.backend.ir.converter.conversion.translator import (
9+
torch_type_to_numpy_type,
10+
)
11+
from executorch.backends.nxp.backend.ir.converter.node_converter import _is_dequant_node
12+
from executorch.backends.nxp.backend.ir.converter.quantization_utils import quantize
713
from executorch.exir.dialects._ops import ops as exir_ops
814
from torch.fx import Node
915

@@ -47,3 +53,77 @@ def get_output_shape(node: Node) -> tuple[torch.Size] | torch.Size | None:
4753
return tuple([v.shape for v in val])
4854

4955
return None
56+
57+
58+
def is_clamp_preserved_under_quantization(
59+
node: Node, min_val: int = 0, max_val: int | None = None
60+
) -> bool:
61+
"""
62+
Checks if Clamp/ReLU/HardTanh is preserved under quantization and did
63+
not collapse into either identity or constant.
64+
65+
Valid quant. bounds - Quant. bounds -
66+
one hinge is preserved Collapse to identity
67+
│ │ │ │
68+
│ ▼/¯¯¯¯¯ ReLU6(x) │ ▼/¯¯¯¯¯ ReLU6(x)
69+
│ / │ /
70+
│ / ▼/
71+
▼ / /
72+
¯¯¯¯¯ Hinge ¯¯¯¯¯ Hinge
73+
74+
Args:
75+
node: Node to check whether is preserved
76+
min_val: Lower bound (hinge) of the operator (eg. 0 for ReLU)
77+
max_val: Upper bound of the operator (eg. 6 for ReLU6 or None for ReLU)
78+
"""
79+
80+
q_node = node.args[0]
81+
82+
if not _is_dequant_node(q_node):
83+
return False
84+
85+
if len(q_node.args) == 6:
86+
# per-tensor
87+
_, scale, zp, quant_min, quant_max, q_type = q_node.args
88+
else:
89+
# per-channel
90+
_, scale, zp, quant_min, quant_max, _, q_type = q_node.args
91+
92+
quant_min = np.iinfo(q_type).min if quant_min is None else quant_min
93+
quant_max = np.iinfo(q_type).max if quant_max is None else quant_max
94+
95+
q_type = torch_type_to_numpy_type(q_type).type
96+
quantized_min_val = quantize(
97+
value=min_val,
98+
zero_point=zp,
99+
scale=scale,
100+
quant_min=quant_min,
101+
quant_max=quant_max,
102+
dtype=q_type,
103+
)
104+
105+
if max_val is not None:
106+
quantized_max_val = quantize(
107+
value=max_val,
108+
zero_point=zp,
109+
scale=scale,
110+
quant_min=quant_min,
111+
quant_max=quant_max,
112+
dtype=q_type,
113+
)
114+
return (
115+
# If at least one bound is inside the quantization range
116+
# the hinge of the ReLU/HardTanh is preserved and therefore does not
117+
# collapse to identity or constant.
118+
(
119+
np.all(quant_min < quantized_min_val)
120+
or np.all(quantized_max_val < quant_max)
121+
)
122+
# When both operator bounds are outside the quantization range
123+
# the operator collapses into constant value (eg. 0 or 6 for ReLU6).
124+
and not np.all(quant_max < quantized_min_val)
125+
and not np.all(quant_min > quantized_max_val)
126+
)
127+
128+
# Ensure ReLU/HardTanh hinge is preserved.
129+
return quant_min < quantized_min_val < quant_max

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

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@
88
import numpy as np
99
import torch
1010
from executorch.backends.nxp.backend.edge_helper import try_get_arg
11+
from executorch.backends.nxp.backend.graph_utils import (
12+
is_clamp_preserved_under_quantization,
13+
)
1114
from executorch.backends.nxp.backend.ir.converter.conversion.translator import (
1215
torch_type_to_numpy_type,
1316
)
@@ -20,6 +23,7 @@
2023
)
2124
from executorch.backends.nxp.backend.ir.converter.quantization_utils import (
2225
propagate_quantization,
26+
quantize,
2327
)
2428
from executorch.backends.nxp.backend.ir.lib.tflite.BuiltinOperator import (
2529
BuiltinOperator,
@@ -117,17 +121,20 @@ def _is_supported_on_target(
117121
output_indices=[0],
118122
)
119123

120-
# We either convert to ReLU -> SingleInputQuantization pattern
121-
# or we convert to Min/Max, which requires same quantization on
122-
# both input and output.
123-
return (relu_compatible | io_quant_consistent) and quant_supported
124+
if relu_compatible and activation_supported_on_target(
125+
node,
126+
):
127+
return True
128+
129+
# We convert to Min/Max, which requires same quantization for both input and output.
130+
return io_quant_consistent and quant_supported
124131

125132
@classmethod
126133
def supports_partitioning_result(
127134
cls,
128135
node: Node,
129136
partition_list: list[Partition],
130-
_: CustomDelegationOptions,
137+
custom_delegation_options: CustomDelegationOptions,
131138
neutron_target_spec: NeutronTargetSpec,
132139
parameters_mapping: dict[str, Parameter],
133140
) -> bool:
@@ -136,30 +143,19 @@ def supports_partitioning_result(
136143
# Neutron cannot delegate a partition where ReLU or ReLU6 is the only operator
137144
# and at the same time the node does not satisfy delegation requirements.
138145
# In contrast, ReLUN1To1 and ReLU0To1 are supported and delegated successfuly.
139-
if bounds in [
140-
cls.RELU_COMPATIBLE_BOUNDS["Relu"],
141-
cls.RELU_COMPATIBLE_BOUNDS["Relu6"],
142-
]:
146+
if bounds in cls.RELU_COMPATIBLE_BOUNDS.values():
143147
is_alone_in_partition = cls.is_node_alone_in_partition(
144148
node, partition_list, filter_fn=is_not_qdq_node
145149
)
146150
if is_alone_in_partition:
147-
return activation_supported_on_target(node, neutron_target_spec)
151+
return is_clamp_preserved_under_quantization(
152+
node,
153+
min_val=bounds[0],
154+
max_val=bounds[1],
155+
)
148156

149157
return True
150158

151-
@staticmethod
152-
def _quantize_value(
153-
value: int,
154-
zp: int,
155-
scale: float,
156-
quant_min: int,
157-
quant_max: int,
158-
dtype: type = np.int8,
159-
) -> np.integer:
160-
rescaled_value = round(value / scale) + zp
161-
return dtype(np.clip(rescaled_value, quant_min, quant_max))
162-
163159
def convert(self, node: Node):
164160
"""Convert the `aten.clamp.default` operator to either
165161
Neutron IR `Relu*` operator or combination of `Min` and `Max`.
@@ -202,9 +198,9 @@ def convert(self, node: Node):
202198
min_value, max_value = bounds
203199

204200
if min_value is not None:
205-
min_value = self._quantize_value(
201+
min_value = quantize(
206202
value=min_value,
207-
zp=zp,
203+
zero_point=zp,
208204
scale=scale,
209205
quant_min=quant_min,
210206
quant_max=quant_max,
@@ -216,9 +212,9 @@ def convert(self, node: Node):
216212
propagate_quantization(x, min_tensor)
217213

218214
if max_value is not None:
219-
max_value = self._quantize_value(
215+
max_value = quantize(
220216
value=max_value,
221-
zp=zp,
217+
zero_point=zp,
222218
scale=scale,
223219
quant_min=quant_min,
224220
quant_max=quant_max,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ def supports_partitioning_result(
9494
node, partition_list, filter_fn=is_not_qdq_node
9595
)
9696
if is_alone_in_partition:
97-
return activation_supported_on_target(node, neutron_target_spec)
97+
return activation_supported_on_target(node)
9898

9999
return True
100100

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
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+
7+
from executorch.backends.nxp.backend.graph_utils import (
8+
is_clamp_preserved_under_quantization,
9+
)
610
from executorch.backends.nxp.backend.ir.converter.node_converter import (
711
CustomDelegationOptions,
812
is_not_qdq_node,
@@ -30,6 +34,15 @@ def _is_supported_in_IR(
3034
) -> bool:
3135
return True
3236

37+
@staticmethod
38+
def _is_supported_on_target(
39+
node: Node,
40+
neutron_target_spec: NeutronTargetSpec,
41+
parameters_mapping: dict[str, Parameter],
42+
custom_delegation_options: CustomDelegationOptions,
43+
) -> bool:
44+
return activation_supported_on_target(node)
45+
3346
@classmethod
3447
def supports_partitioning_result(
3548
cls,
@@ -43,7 +56,7 @@ def supports_partitioning_result(
4356
node, partition_list, filter_fn=is_not_qdq_node
4457
)
4558
if is_alone_in_partition:
46-
return activation_supported_on_target(node, neutron_target_spec)
59+
return is_clamp_preserved_under_quantization(node)
4760

4861
return True
4962

backends/nxp/backend/ir/converter/quantization_utils.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,19 @@ def set_quantization_parameters_to_tensor(
135135
def quantize_int8(
136136
data: np.ndarray, scale: List[float], zero_point: List[int]
137137
) -> np.ndarray:
138-
new_data = np.add(np.round(np.divide(data, scale)), zero_point)
139-
return np.clip(new_data, -128, 127).astype(np.int8)
138+
return quantize(data, zero_point=zero_point, scale=scale)
139+
140+
141+
def quantize(
142+
value: np.ndarray | int,
143+
zero_point: List[int] | int,
144+
scale: List[float] | float,
145+
quant_min: int = -128,
146+
quant_max: int = 127,
147+
dtype: type = np.int8,
148+
) -> np.ndarray | np.integer:
149+
rescaled_value = np.add(np.round(np.divide(value, scale)), zero_point)
150+
return dtype(np.clip(rescaled_value, quant_min, quant_max))
140151

141152

142153
def dequantize(

backends/nxp/backend/neutron_operator_support.py

Lines changed: 13 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@
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
7-
from executorch.backends.nxp.backend.edge_helper import input_tensor
8-
from executorch.backends.nxp.backend.ir.converter.conversion.translator import (
9-
dims_to_channels_last,
10-
)
6+
import torch
117
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
128
from torch.fx import Node
139

@@ -42,20 +38,20 @@ def transposition_is_supported_on_neutron(
4238

4339

4440
def activation_supported_on_target(
45-
node: Node, neutron_target_spec: NeutronTargetSpec
41+
node: Node,
4642
) -> bool:
4743
"""This function determines if the current NeutronSoftware properly supports an activation operator represented by the given node.
4844
4945
:param node: The node representing the activation operator.
50-
:param neutron_target_spec: Object for querying the target platform to retrieve its properties.
5146
"""
52-
input_shape = list(input_tensor(node, 0).shape)
53-
if node.args[0].meta[NXP_NODE_FORMAT].is_channels_first():
54-
input_shape = dims_to_channels_last(input_shape)
55-
56-
c = input_shape[-1]
57-
num_macs = neutron_target_spec.get_num_macs()
58-
59-
# activations in Neutron are delegable only
60-
# if `num_channels` % `num_macs` == 0
61-
return c % num_macs == 0
47+
# Prevent circular import
48+
from executorch.backends.nxp.backend.ir.converter.node_converter import (
49+
NodeConverter,
50+
)
51+
52+
return NodeConverter.uses_quantization_type_for_io(
53+
node,
54+
supported_types=[torch.int8, torch.uint8],
55+
input_indices=[0],
56+
output_indices=[0],
57+
)

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

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,12 @@
88
# noinspection PyUnusedImports
99
import pytest
1010
import torch
11-
1211
from executorch.backends.nxp.tests.graph_verifier import DetailedGraphVerifier
1312
from executorch.backends.nxp.tests.nsys_testing import (
1413
lower_run_compare,
1514
RandomDatasetCreator,
1615
)
17-
from executorch.backends.nxp.tests.ops_aliases import Abs, Convolution, Relu
16+
from executorch.backends.nxp.tests.ops_aliases import Abs
1817
from executorch.backends.nxp.tests.use_qat import * # noqa F403
1918

2019

@@ -99,23 +98,3 @@ def test__basic_nsys_inference__big(self, mocker):
9998
graph_verifier,
10099
dataset_creator,
101100
)
102-
103-
def test_basic_nsys_inference__with_conv(self, mocker):
104-
input_shape = (2, 3, 6, 7)
105-
in_channels = input_shape[1]
106-
model = ConvBlocksWithAbsModule(conv_in_channels=in_channels)
107-
108-
# one `relu` ends up in the same delegated partition as `abs`
109-
graph_verifier = DetailedGraphVerifier(
110-
mocker,
111-
expected_delegated_ops={Abs: 1, Relu: 1},
112-
expected_non_delegated_ops={Relu: 1, Convolution: 2},
113-
)
114-
115-
dataset_creator = self._get_dataset_creator()
116-
lower_run_compare(
117-
model,
118-
input_shape,
119-
graph_verifier,
120-
dataset_creator,
121-
)

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

Lines changed: 1 addition & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
ToChannelFirstPreprocess,
1818
ToChannelLastPreprocess,
1919
)
20-
from executorch.backends.nxp.tests.models import Conv2dWithActivation, HardTanhModule
20+
from executorch.backends.nxp.tests.models import Conv2dWithActivation
2121
from executorch.exir.dialects._ops import ops as exir_ops
2222
from torch.export import ExportedProgram
2323
from executorch.backends.nxp.tests.use_qat import * # noqa F403
@@ -117,34 +117,3 @@ def test_custom_hardtanh_quant(
117117
input_data=input_data,
118118
atol=2.0,
119119
)
120-
121-
122-
@pytest.mark.parametrize(
123-
"input_shape, activation_range",
124-
[
125-
pytest.param(
126-
(3, 7, 15, 7),
127-
(0, float("inf")),
128-
id="activation range: Relu, num_channels not divisible by NUM_MACS, alone in partition",
129-
),
130-
pytest.param(
131-
(3, 7, 15, 7),
132-
(0, 6),
133-
id="activation range: Relu6, num_channels not divisible by NUM_MACS, alone in partition",
134-
),
135-
],
136-
)
137-
def test_hardtanh__unsupported(
138-
input_shape: tuple[int],
139-
activation_range: tuple[float, float],
140-
use_qat: bool,
141-
):
142-
min_val, max_val = activation_range
143-
model = HardTanhModule(min_val, max_val)
144-
delegated_ep = to_quantized_edge_program(
145-
model, input_shape, use_qat=use_qat
146-
).exported_program()
147-
148-
# Make sure the `hardtanh` was NOT delegated.
149-
assert not graph_contains_any_of_ops(delegated_ep.graph, [ExecutorchDelegateCall])
150-
assert graph_contains_any_of_ops(delegated_ep.graph, [HardTanh, HardTanh_])

0 commit comments

Comments
 (0)