Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions backends/nxp/backend/custom_delegation_options.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,3 @@ class CustomDelegationOptions:
# not create any NeutronGraph that can be called. This is done by the partitioner itself, and is not handled by
# the individual node converters.
allow_no_op_partitions: bool = False

# The new neutron converter flow has different constraints for supported operators. These need to be addressed when
# deciding is operator is delegated or not in _is_supported_on_target().
use_new_flow_neutron_c: bool = False
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@


import torch

from executorch.backends.nxp.backend.ir.converter.node_converter import (
CustomDelegationOptions,
NeutronTargetSpec,
Expand Down Expand Up @@ -36,7 +35,7 @@ def _is_supported_on_target(
custom_delegation_options: CustomDelegationOptions,
) -> bool:

if custom_delegation_options.use_new_flow_neutron_c:
if neutron_target_spec.use_new_flow_neutron_c:
# Requirements specified by the new Neutron flow documentation.

supported_types = [torch.int8, torch.uint8]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import numpy as np
import torch

from executorch.backends.nxp.backend.ir.converter.conversion import (
aten_translator,
common,
Expand All @@ -22,7 +21,6 @@
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
average_pool_2d_options,
)

from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
from torch.fx import Node
from torch.nn import Parameter
Expand Down Expand Up @@ -66,7 +64,7 @@ def _is_supported_on_target(
kernel = node.args[1]
stride = node.args[2]

if custom_delegation_options.use_new_flow_neutron_c:
if neutron_target_spec.use_new_flow_neutron_c:
# Requirements specified by the new Neutron flow documentation.

supported_types = [torch.int8, torch.uint8]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,32 @@
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

import math

import numpy as np
import torch
from executorch.backends.nxp.backend.edge_helper import try_get_arg
from executorch.backends.nxp.backend.ir.converter.conversion.translator import (
torch_type_to_numpy_type,
)
from executorch.backends.nxp.backend.ir.converter.node_converter import (
_is_dequant_node,
_is_quant_node,
CustomDelegationOptions,
is_not_qdq_node,
NodeConverter,
)
from executorch.backends.nxp.backend.ir.converter.quantization_utils import (
propagate_quantization,
)
from executorch.backends.nxp.backend.ir.lib.tflite.BuiltinOperator import (
BuiltinOperator,
)
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
maximum_options,
minimum_options,
)
from executorch.backends.nxp.backend.neutron_operator_support import (
activation_supported_on_target,
)
Expand All @@ -21,15 +38,26 @@
from torch.nn import Parameter


def _is_convertible_to_relu(node):
bounds = ClampConverter._get_clamp_bounds(node)
bounds = tuple(v if v is not None and math.isfinite(v) else None for v in bounds)

# Some specific bounds can be replaced with single op ReLU.
if bounds not in ClampConverter.RELU_COMPATIBLE_BOUNDS.values():
return False

return True


class ClampConverter(NodeConverter):
SUPPORTED_BOUNDS = {
RELU_COMPATIBLE_BOUNDS = {
"ReluN1To1": (-1, 1),
"Relu0To1": (0, 1),
"Relu6": (0, 6),
"Relu": (0, None),
}

BOUNDS_TO_NEUTRON_IR_OP = {
BOUNDS_TO_RELU_NEUTRON_IR_OP = {
(-1, 1): BuiltinOperator.RELU_N1_TO_1,
(0, 1): BuiltinOperator.RELU_0_TO_1,
(0, 6): BuiltinOperator.RELU6,
Expand All @@ -53,27 +81,52 @@ def _is_supported_in_IR(
# No NeutronIR-specific restrictions.
return True

@staticmethod
def _io_quant_is_same(node: Node):
quant = next(iter(node.users.keys()))
dequant = node.args[0]

if not _is_dequant_node(dequant):
return False

if not _is_quant_node(quant):
return False

q_params = quant.args[1:]
dq_params = dequant.args[1:]
return all([q == dq for q, dq in zip(q_params, dq_params)])
Comment thread
StrycekSimon marked this conversation as resolved.

@staticmethod
def _is_supported_on_target(
node: Node,
neutron_target_spec: NeutronTargetSpec,
parameters_mapping: dict[str, Parameter],
custom_delegation_options: CustomDelegationOptions,
) -> bool:
bounds = ClampConverter._get_clamp_bounds(node)
relu_compatible = _is_convertible_to_relu(node)

if neutron_target_spec.use_new_flow_neutron_c:
io_quant_consistent = ClampConverter._io_quant_is_same(node)
quant_supported = NodeConverter.uses_quantization_type_for_io(
node,
supported_types=[torch.int8, torch.uint8],
input_indices=[0],
output_indices=[0],
)

# Only some specific bounds are supported on the target hardware.
if bounds not in ClampConverter.SUPPORTED_BOUNDS.values():
return False
# We either convert to ReLU -> SingleInputQuantization pattern
# or we convert to Min/Max, which requires same quantization on
# both input and output.
return (relu_compatible | io_quant_consistent) and quant_supported

return True
return relu_compatible

@classmethod
def supports_partitioning_result(
cls,
node: Node,
partition_list: list[Partition],
custom_delegation_options: CustomDelegationOptions,
_: CustomDelegationOptions,
neutron_target_spec: NeutronTargetSpec,
parameters_mapping: dict[str, Parameter],
) -> bool:
Expand All @@ -82,7 +135,10 @@ def supports_partitioning_result(
# Neutron cannot delegate a partition where ReLU or ReLU6 is the only operator
# and at the same time the node does not satisfy delegation requirements.
# In contrast, ReLUN1To1 and ReLU0To1 are supported and delegated successfuly.
if bounds in [cls.SUPPORTED_BOUNDS["Relu"], cls.SUPPORTED_BOUNDS["Relu6"]]:
if bounds in [
cls.RELU_COMPATIBLE_BOUNDS["Relu"],
cls.RELU_COMPATIBLE_BOUNDS["Relu6"],
]:
is_alone_in_partition = cls.is_node_alone_in_partition(
node, partition_list, filter_fn=is_not_qdq_node
)
Expand All @@ -91,8 +147,21 @@ def supports_partitioning_result(

return True

@staticmethod
def _quantize_value(
value: int,
zp: int,
scale: float,
quant_min: int,
quant_max: int,
dtype: type = np.int8,
) -> np.integer:
rescaled_value = round(value / scale) + zp
return dtype(np.clip(rescaled_value, quant_min, quant_max))
Comment thread
StrycekSimon marked this conversation as resolved.

def convert(self, node: Node):
"""Convert the `aten.clamp.default` operator to Neutron IR `Relu*` operators.
"""Convert the `aten.clamp.default` operator to either
Neutron IR `Relu*` operator or combination of `Min` and `Max`.
The schema is:
aten::clamp(
Tensor self,
Expand All @@ -101,13 +170,83 @@ def convert(self, node: Node):
) -> Tensor
"""
self.assert_convertible(node)
to_relu = _is_convertible_to_relu(node)

bounds = self._get_clamp_bounds(node)

bounds = tuple(
v if v is not None and math.isfinite(v) else None for v in bounds
)
t_op = self._create_tflite_op_with_io_tensors(node)

# noinspection PyTypeChecker,PyUnboundLocalVariable
t_op.opcode_index = self.builder.op_code_index_for_op_type(
self.BOUNDS_TO_NEUTRON_IR_OP[bounds]
)
self.builder.append_operators([t_op])
if not self.neutron_target_spec.use_new_flow_neutron_c or to_relu:
# noinspection PyTypeChecker,PyUnboundLocalVariable
t_op.opcode_index = self.builder.op_code_index_for_op_type(
self.BOUNDS_TO_RELU_NEUTRON_IR_OP[bounds]
)
self.builder.append_operators([t_op])
return

q_node = node.args[0]
assert _is_dequant_node(q_node)
_, scale, zp, quant_min, quant_max, q_type = q_node.args
q_type = torch_type_to_numpy_type(q_type).type

x = t_op.tmp_inputs[0]
y = t_op.tmp_outputs[0]

if x.quantization is not None and y.quantization is None:
propagate_quantization(x, y)

min_value, max_value = bounds

if min_value is not None:
min_value = self._quantize_value(
value=min_value,
zp=zp,
scale=scale,
quant_min=quant_min,
quant_max=quant_max,
dtype=q_type,
)
min_tensor = self.builder.create_tensor_for_data(
np.array([min_value], q_type), "min"
)
propagate_quantization(x, min_tensor)

if max_value is not None:
max_value = self._quantize_value(
value=max_value,
zp=zp,
scale=scale,
quant_min=quant_min,
quant_max=quant_max,
dtype=q_type,
)
max_tensor = self.builder.create_tensor_for_data(
np.array([max_value], q_type), "max"
)
propagate_quantization(x, max_tensor)

if None not in bounds:
tmp_y = self.builder.duplicate_tensor(x)
tmp_x = tmp_y
propagate_quantization(x, tmp_y)
else:
tmp_y = y
tmp_x = x

ops_to_add = []
if max_value is not None:
min_op = tflite_model.Operator(builtin_options=minimum_options.Minimum())
min_op.tmp_inputs = [x, max_tensor]
min_op.tmp_outputs = [tmp_y]
ops_to_add.append(min_op)

if min_value is not None:
max_op = tflite_model.Operator(builtin_options=maximum_options.Maximum())
max_op.tmp_inputs = [tmp_x, min_tensor]
max_op.tmp_outputs = [y]
ops_to_add.append(max_op)

ops_to_add = ops_to_add if len(ops_to_add) >= 1 else [x]
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are you assigning a list with the tensor x to a list of operators ops_to_add?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry, should not have trusted Copilot... 😄

self.builder.append_operators(ops_to_add)
Comment thread
StrycekSimon marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@

import numpy as np
import torch

from executorch.backends.nxp.backend.edge_helper import try_get_arg
from executorch.backends.nxp.backend.ir.converter.conversion import (
aten_translator,
Expand Down Expand Up @@ -74,7 +73,7 @@ def _is_supported_on_target(
MaxPool2DWithIndicesConverter._get_node_args(node)
)

if custom_delegation_options.use_new_flow_neutron_c:
if neutron_target_spec.use_new_flow_neutron_c:
# Requirements specified by the new Neutron flow documentation.

supported_types = [torch.int8, torch.uint8]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# LICENSE file in the root directory of this source tree.

import torch

from executorch.backends.nxp.backend.data_format import NXP_NODE_FORMAT
from executorch.backends.nxp.backend.ir.converter.node_converter import (
CustomDelegationOptions,
Expand All @@ -26,7 +25,7 @@ def _is_supported_on_target(
parameters_mapping: dict[str, Parameter],
custom_delegation_options: CustomDelegationOptions,
) -> bool:
if custom_delegation_options.use_new_flow_neutron_c:
if neutron_target_spec.use_new_flow_neutron_c:
if not NodeConverter.at_least_one_input_shape_matches_the_output_shape(
node
):
Expand Down
10 changes: 6 additions & 4 deletions backends/nxp/backend/neutron_target_spec.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright 2025 NXP
# Copyright 2026 NXP
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
Expand All @@ -8,12 +8,10 @@
from enum import Enum

import torch

from executorch.backends.nxp.backend.neutron_converter_manager import (
NeutronConverterManager,
)
from executorch.exir.dialects._ops import ops as exir_ops

from torch.fx import Node


Expand Down Expand Up @@ -98,13 +96,17 @@ class NeutronTargetSpec:
The functionality for probing the properties of Neutron Target.
"""

def __init__(self, target: str):
def __init__(self, target: str, use_new_flow_neutron_c: bool = False):

converter_manager = NeutronConverterManager()
converter_manager.verify_target(target)
neutron_converter = converter_manager.get_converter()
self.neutron_target = neutron_converter.getNeutronTarget(target)

# The new neutron converter flow has different constraints for supported operators. These need to be addressed when
# deciding is operator is delegated or not in _is_supported_on_target().
self.use_new_flow_neutron_c = use_new_flow_neutron_c

if self.is_subsystem():
raise ValueError(
f"Target `{target}` is not a neutron-C target. Only MCU targets are supported at the moment."
Expand Down
Loading
Loading