Skip to content

Commit a636282

Browse files
committed
NXP backend: Enable new Neutron C flow support for Clamp operator
1 parent f662ba6 commit a636282

2 files changed

Lines changed: 123 additions & 15 deletions

File tree

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

Lines changed: 83 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,24 @@
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 copy import copy
7+
8+
import numpy as np
69
from executorch.backends.nxp.backend.edge_helper import try_get_arg
710
from executorch.backends.nxp.backend.ir.converter.node_converter import (
11+
_is_dequant_node,
812
CustomDelegationOptions,
913
is_not_qdq_node,
1014
NodeConverter,
1115
)
1216
from executorch.backends.nxp.backend.ir.lib.tflite.BuiltinOperator import (
1317
BuiltinOperator,
1418
)
19+
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
20+
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
21+
maximum_options,
22+
minimum_options,
23+
)
1524
from executorch.backends.nxp.backend.neutron_operator_support import (
1625
activation_supported_on_target,
1726
)
@@ -21,6 +30,16 @@
2130
from torch.nn import Parameter
2231

2332

33+
def _is_convertible_to_relu(node):
34+
bounds = ClampConverter._get_clamp_bounds(node)
35+
36+
# Only some specific bounds are supported on the target hardware.
37+
if bounds not in ClampConverter.SUPPORTED_BOUNDS.values():
38+
return False
39+
40+
return True
41+
42+
2443
class ClampConverter(NodeConverter):
2544
SUPPORTED_BOUNDS = {
2645
"ReluN1To1": (-1, 1),
@@ -48,7 +67,7 @@ def _get_clamp_bounds(clamp_node: Node) -> tuple[float | None, float | None]:
4867
def _is_supported_in_IR(
4968
node: Node,
5069
parameters_mapping: dict[str, Parameter],
51-
custom_delegation_options: CustomDelegationOptions,
70+
_: CustomDelegationOptions,
5271
) -> bool:
5372
# No NeutronIR-specific restrictions.
5473
return True
@@ -58,22 +77,19 @@ def _is_supported_on_target(
5877
node: Node,
5978
neutron_target_spec: NeutronTargetSpec,
6079
parameters_mapping: dict[str, Parameter],
61-
custom_delegation_options: CustomDelegationOptions,
80+
_: CustomDelegationOptions,
6281
) -> bool:
63-
bounds = ClampConverter._get_clamp_bounds(node)
64-
65-
# Only some specific bounds are supported on the target hardware.
66-
if bounds not in ClampConverter.SUPPORTED_BOUNDS.values():
67-
return False
82+
if neutron_target_spec.use_new_flow_neutron_c:
83+
return True
6884

69-
return True
85+
return _is_convertible_to_relu(node)
7086

7187
@classmethod
7288
def supports_partitioning_result(
7389
cls,
7490
node: Node,
7591
partition_list: list[Partition],
76-
custom_delegation_options: CustomDelegationOptions,
92+
_: CustomDelegationOptions,
7793
neutron_target_spec: NeutronTargetSpec,
7894
parameters_mapping: dict[str, Parameter],
7995
) -> bool:
@@ -91,6 +107,15 @@ def supports_partitioning_result(
91107

92108
return True
93109

110+
@staticmethod
111+
def propagate_quantization(from_node, to_node):
112+
to_node.quantization = copy(from_node.quantization)
113+
114+
@staticmethod
115+
def _quantize_value(value, zp, scale, quant_min, quant_max):
116+
rescaled_value = round(value / scale) + zp
117+
return np.clip(rescaled_value, quant_min, quant_max)
118+
94119
def convert(self, node: Node):
95120
"""Convert the `aten.clamp.default` operator to Neutron IR `Relu*` operators.
96121
The schema is:
@@ -101,13 +126,57 @@ def convert(self, node: Node):
101126
) -> Tensor
102127
"""
103128
self.assert_convertible(node)
129+
to_relu = _is_convertible_to_relu(node)
104130

105131
bounds = self._get_clamp_bounds(node)
106-
107132
t_op = self._create_tflite_op_with_io_tensors(node)
108133

109-
# noinspection PyTypeChecker,PyUnboundLocalVariable
110-
t_op.opcode_index = self.builder.op_code_index_for_op_type(
111-
self.BOUNDS_TO_NEUTRON_IR_OP[bounds]
134+
if not self.neutron_target_spec.use_new_flow_neutron_c or to_relu:
135+
# noinspection PyTypeChecker,PyUnboundLocalVariable
136+
t_op.opcode_index = self.builder.op_code_index_for_op_type(
137+
self.BOUNDS_TO_NEUTRON_IR_OP[bounds]
138+
)
139+
self.builder.append_operators([t_op])
140+
return
141+
142+
q_node = node.args[0]
143+
assert _is_dequant_node(q_node)
144+
_, scale, zp, quant_min, quant_max, _ = q_node.args
145+
146+
x = t_op.tmp_inputs[0]
147+
y = t_op.tmp_outputs[0]
148+
149+
if x.quantization is not None and y.quantization is None:
150+
self.propagate_quantization(x, y)
151+
152+
if x.quantization != y.quantization:
153+
raise AssertionError(
154+
"Input and output quantization should be same in order to convert to max/min."
155+
)
156+
157+
max_y = self.builder.duplicate_tensor(x)
158+
159+
min_value, max_value = bounds
160+
min_value = self._quantize_value(min_value, zp, scale, quant_min, quant_max)
161+
max_value = self._quantize_value(max_value, zp, scale, quant_min, quant_max)
162+
163+
min_tensor = self.builder.create_tensor_for_data(
164+
np.array([min_value], np.int8), "min"
165+
)
166+
self.propagate_quantization(x, min_tensor)
167+
max_tensor = self.builder.create_tensor_for_data(
168+
np.array([max_value], np.int8), "max"
112169
)
113-
self.builder.append_operators([t_op])
170+
self.propagate_quantization(x, max_tensor)
171+
172+
max_op = tflite_model.Operator(builtin_options=maximum_options.Maximum())
173+
max_op.tmp_inputs = [x, max_tensor]
174+
max_op.tmp_outputs = [max_y]
175+
176+
min_op = tflite_model.Operator(builtin_options=minimum_options.Minimum())
177+
min_op.tmp_inputs = [max_y, min_tensor]
178+
min_op.tmp_outputs = [y]
179+
180+
self.propagate_quantization(x, max_y)
181+
182+
self.builder.append_operators([max_op, min_op])

backends/nxp/quantizer/patterns.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
from functools import partial
1111

1212
import torch
13+
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters.clamp_converter import (
14+
_is_convertible_to_relu,
15+
)
1316
from executorch.backends.nxp.quantizer.utils import (
1417
get_bias_qparams,
1518
get_bias_qparams_transp_conv,
@@ -408,12 +411,48 @@ def get_anchors(
408411
)
409412

410413

411-
class ClampPattern(SingleInputBasicPattern):
414+
class ClampPattern(QuantizationPattern):
412415
"""Quantizer for the `aten.clamp.default` operator."""
413416

414417
def partition_types(self):
415418
return [torch.ops.aten.clamp.default]
416419

420+
def get_anchors(
421+
self, gm: fx.GraphModule, fused_partition: list[fx.GraphModule]
422+
) -> PartitionAnchors | None:
423+
node = fused_partition[0].nodes[-1]
424+
425+
if (
426+
self.neutron_quantizer.neutron_target_spec.use_new_flow_neutron_c
427+
and not _is_convertible_to_relu(node)
428+
):
429+
# Shared spec pattern
430+
assert len(fused_partition[0].input_nodes) == 1
431+
prev_node = fused_partition[0].input_nodes[0]
432+
433+
# Previous node was not quantized => we are not able to share q-params
434+
if Q_ANNOTATION_KEY not in prev_node.meta:
435+
return None
436+
437+
qspec = SharedQuantizationSpec(prev_node)
438+
439+
return PartitionAnchors(
440+
inputs=[(node, NodeArgsIdx(0))],
441+
weights=[],
442+
biases=[],
443+
output=[
444+
(node, qspec),
445+
],
446+
)
447+
else:
448+
# Single input pattern
449+
return PartitionAnchors(
450+
inputs=[(node, NodeArgsIdx(0))],
451+
weights=[],
452+
biases=[],
453+
output=[(node,)],
454+
)
455+
417456

418457
def _is_batch_norm(node_: Node) -> bool:
419458
return node_.op == "call_function" and node_.target in [

0 commit comments

Comments
 (0)