Skip to content

Commit edf6927

Browse files
StrycekSimonjirioc
andauthored
NXP backend: Improve Neutron targets handling (#14718)
### Summary Adds NeutronTargetSpec class containing metadata about the desired target for better handling of Neutron target support. ### Test plan This feature modifies handling of individual operators target support and therefore should be covered by already existing unit tests. cc @digantdesai @JakeStevens @robert-kalmar Co-authored-by: Jiri Ocenasek <[email protected]>
1 parent 499ce50 commit edf6927

17 files changed

+289
-254
lines changed

backends/nxp/backend/edge_program_converter.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
from torch.fx import Node
1919
from torch.nn.parameter import Parameter
2020
from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters import * # noqa F403
21+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
2122
from executorch.backends.nxp.backend.node_format_inference import (
2223
NodeFormat,
2324
NodeFormatInference,
@@ -54,19 +55,22 @@ class EdgeProgramToIRConverter:
5455
"""
5556

5657
_default_conversion_config = ConversionConfig()
58+
_default_target_spec = NeutronTargetSpec("imxrt700", "SDK_25_09")
5759
_default_delegation_options = CustomDelegationOptions()
5860

5961
def convert_program(
6062
self,
6163
edge_program: ExportedProgram,
62-
conversion_config=_default_conversion_config,
64+
conversion_config: ConversionConfig = _default_conversion_config,
65+
neutron_target_spec: NeutronTargetSpec = _default_target_spec,
6366
custom_delegation_options: CustomDelegationOptions = _default_delegation_options,
6467
) -> (bytes, dict):
6568
"""
6669
Convert ExportedProgram in Edge dialect to IR (TFLite flatbuffers) as bytes.
6770
6871
:param edge_program: Converter ExportedProgram.
6972
:param conversion_config: ConversionConfig instance.
73+
:param neutron_target_spec: Object for querying the target platform to retrieve its properties.
7074
:param custom_delegation_options: Custom user options which affect node delegation.
7175
:return: TFLite flatbuffers as bytes.
7276
"""
@@ -76,6 +80,7 @@ def convert_program(
7680
cc = self.build_conversion_context(
7781
parameters_mapping,
7882
node_formats,
83+
neutron_target_spec,
7984
conversion_config,
8085
custom_delegation_options,
8186
)
@@ -173,11 +178,12 @@ def map_inputs_to_parameters(edge_program: ExportedProgram) -> dict[str, Paramet
173178
def build_conversion_context(
174179
parameters_mapping: dict,
175180
node_formats: dict[Node, NodeFormat],
181+
neutron_target_spec: NeutronTargetSpec,
176182
conversion_config: ConversionConfig = _default_conversion_config,
177183
custom_delegation_options: CustomDelegationOptions = _default_delegation_options,
178184
) -> ConversionContext:
179185
tflite_builder = AtenModelBuilderDirector(
180-
3, "TFLite from EdgeProgram", conversion_config
186+
3, "TFLite from EdgeProgram", neutron_target_spec, conversion_config
181187
)
182188

183189
# Add "sentinel" buffer (defined in schema.fbs)

backends/nxp/backend/ir/converter/builder/model_builder.py

Lines changed: 41 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
FlexTranspose,
4949
)
5050
from executorch.backends.nxp.backend.ir.tflite_optimizer import optimizer
51+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
5152

5253

5354
class ModelBuilder:
@@ -74,17 +75,21 @@ class ModelBuilder:
7475

7576
_zeros_tensor_map: Dict # Mapping 'string' shapes to 'tflT.Tensor' objects
7677

77-
_default_conversion_config = ConversionConfig()
78+
neutron_target_spec: NeutronTargetSpec
7879

7980
conversion_config: ConversionConfig
8081

82+
_default_conversion_config = ConversionConfig()
83+
8184
def __init__(
8285
self,
8386
model_version: int,
8487
model_description: str,
88+
neutron_target_spec: NeutronTargetSpec,
8589
conversion_config: ConversionConfig = _default_conversion_config,
8690
) -> None:
8791
self._tfl_model = tflite_model.Model(model_version, model_description)
92+
self.neutron_target_spec = neutron_target_spec
8893
self.conversion_config = conversion_config
8994

9095
self.op_code_type_index_map = {}
@@ -471,31 +476,7 @@ def finish(self) -> tflite_model.Model:
471476

472477
return self._tfl_model
473478

474-
def _assign_tensor_and_buffer_indices( # noqa C901
475-
self, allow_inputs_stripping: bool
476-
):
477-
"""Correctly initialize all references via indices in all tensors and buffers."""
478-
479-
# Assign each buffer its index
480-
for i, buffer in enumerate(self.get_buffers().vector):
481-
buffer.tmp_index = i
482-
483-
# Assign each tensor its index and its buffer index
484-
for i, tensor in enumerate(self.get_tensors().vector):
485-
if tensor.tmp_null_tensor:
486-
# Using -1 as the index to the 'tensors' vector is way of telling the TFLite inference engine, that
487-
# this tensor should not be used.
488-
# https://github.com/tensorflow/tensorflow/blob/05404d959119d41a8ffb8a75c6f232cfd8540d45/tensorflow/lite/kernels/kernel_util.cc#L79-L98
489-
tensor.tmp_index = -1
490-
else:
491-
tensor.tmp_index = i
492-
493-
tensor.buffer = tensor.tmp_buffer.tmp_index
494-
495-
# TODO Remove inputs and outputs that are not in the tensors collection
496-
497-
# Assign 'Outputs' and 'Inputs' their tensor indices
498-
outputs = self.get_sub_graph().outputs
479+
def _assign_io_tensor_indices(self, inputs, outputs, allow_inputs_stripping: bool):
499480
for tensor in outputs.tmp_outputs:
500481
try:
501482
outputs.append(tensor.tmp_index)
@@ -505,7 +486,6 @@ def _assign_tensor_and_buffer_indices( # noqa C901
505486
f"The tensor '{tensor.name}' is among the model outputs, but does NOT appear in the graph!",
506487
)
507488

508-
inputs = self.get_sub_graph().inputs
509489
for tensor in inputs.tmp_inputs:
510490
try:
511491
inputs.append(tensor.tmp_index)
@@ -520,14 +500,46 @@ def _assign_tensor_and_buffer_indices( # noqa C901
520500
f"The tensor '{tensor.name}' is among the model inputs, but does NOT appear in the graph!",
521501
)
522502

523-
# Assign each operator its inputs and outputs indices
524-
for operator in self.get_sub_graph().operators.vector:
503+
def _assign_operators_io_tensor_indices(self, operators):
504+
for operator in operators.vector:
525505
for inputTensor in operator.tmp_inputs:
526506
operator.inputs.append(inputTensor.tmp_index)
527507

528508
for outputTensor in operator.tmp_outputs:
529509
operator.outputs.append(outputTensor.tmp_index)
530510

511+
def _assign_tensor_and_buffer_indices(self, allow_inputs_stripping: bool):
512+
"""Correctly initialize all references via indices in all tensors and buffers."""
513+
514+
# Assign each buffer its index
515+
for i, buffer in enumerate(self.get_buffers().vector):
516+
buffer.tmp_index = i
517+
518+
# Assign each tensor its index and its buffer index
519+
for i, tensor in enumerate(self.get_tensors().vector):
520+
if tensor.tmp_null_tensor:
521+
# Using -1 as the index to the 'tensors' vector is way of telling the TFLite inference engine, that
522+
# this tensor should not be used.
523+
# https://github.com/tensorflow/tensorflow/blob/05404d959119d41a8ffb8a75c6f232cfd8540d45/tensorflow/lite/kernels/kernel_util.cc#L79-L98
524+
tensor.tmp_index = -1
525+
else:
526+
tensor.tmp_index = i
527+
528+
tensor.buffer = tensor.tmp_buffer.tmp_index
529+
530+
# TODO Remove inputs and outputs that are not in the tensors collection
531+
532+
subgraph = self.get_sub_graph()
533+
534+
# Assign 'Outputs' and 'Inputs' their tensor indices
535+
self._assign_io_tensor_indices(
536+
inputs=subgraph.inputs,
537+
outputs=subgraph.outputs,
538+
allow_inputs_stripping=allow_inputs_stripping,
539+
)
540+
# Assign each operator its inputs and outputs indices
541+
self._assign_operators_io_tensor_indices(operators=subgraph.operators)
542+
531543
def _build_operator_code(
532544
self, op_type: BuiltinOperator, version, custom_code: str = None
533545
):

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

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
# LICENSE file in the root directory of this source tree.
55

66
from abc import ABC, abstractmethod
7-
from enum import Enum
87

98
import torch
109

@@ -16,6 +15,7 @@
1615
AtenModelBuilderDirector,
1716
)
1817
from executorch.backends.nxp.backend.ir.tflite_generator import tflite_model
18+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
1919
from executorch.exir.dialects._ops import ops as exir_ops
2020
from torch.fx import Node
2121
from torch.fx.passes.infra.partitioner import Partition
@@ -42,17 +42,6 @@ def is_not_qdq_node(node: torch.fx.Node) -> bool:
4242
return not (_is_quant_node(node) or _is_dequant_node(node))
4343

4444

45-
class Target(Enum):
46-
IGNORE = "ignore" # No target platform. Any target specific restrictions will be ignored.
47-
48-
RT700 = "imxrt700"
49-
IMX95 = "imx95"
50-
51-
@classmethod
52-
def values(cls) -> list[str]:
53-
return [elt.value for elt in cls]
54-
55-
5645
class NodeConverter(ABC):
5746
"""
5847
Classes which implement conversion of torch.Node to TFLite should inherit from this class and overwrite the
@@ -94,7 +83,7 @@ def _is_supported_in_IR(
9483
@staticmethod
9584
def _is_supported_on_target(
9685
node: Node,
97-
target: Target,
86+
neutron_target_spec: NeutronTargetSpec,
9887
parameters_mapping: dict[str, Parameter],
9988
custom_delegation_options: CustomDelegationOptions,
10089
) -> bool:
@@ -103,31 +92,31 @@ def _is_supported_on_target(
10392
can be used by operators with no target specific requirements.
10493
10594
:param node: The node (edge operator) to check.
106-
:param target: Value of the `Target` enum representing the target platform to check for.
95+
:param neutron_target_spec: Object for querying the target platform to retrieve its properties.
10796
:param parameters_mapping: Dictionary mapping tensor names to their static data (if they have it).
10897
:param custom_delegation_options: Custom options which affect delegation.
10998
"""
110-
return target == Target.RT700
99+
return True
111100

112101
@classmethod
113102
def is_supported(
114103
cls,
115104
node: Node,
116-
target: Target,
105+
neutron_target_spec: NeutronTargetSpec,
117106
parameters_mapping: dict[str, Parameter],
118107
custom_delegation_options: CustomDelegationOptions,
119108
) -> bool:
120109
"""Check if the given `node` is supported in the IR and on the given `target` platform.
121110
122111
:param node: torch.Node to check.
123-
:param target: Value of the `Target` enum representing the target platform to check for.
112+
:param neutron_target_spec: Object for querying the target platform to retrieve its properties.
124113
:param parameters_mapping: Dict mapping tensor names to their data.
125114
:param custom_delegation_options: Custom user options which affect node delegation.
126115
"""
127116
return cls._is_supported_in_IR(
128117
node, parameters_mapping, custom_delegation_options
129118
) and cls._is_supported_on_target(
130-
node, target, parameters_mapping, custom_delegation_options
119+
node, neutron_target_spec, parameters_mapping, custom_delegation_options
131120
)
132121

133122
@classmethod

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

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,11 @@
99
from executorch.backends.nxp.backend.ir.converter.node_converter import (
1010
CustomDelegationOptions,
1111
NodeConverter,
12-
Target,
1312
)
1413
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
1514
add_options,
1615
)
16+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
1717
from torch.fx import Node
1818
from torch.nn import Parameter
1919

@@ -22,20 +22,15 @@ class AddTensorConverter(NodeConverter):
2222
@staticmethod
2323
def _is_supported_on_target(
2424
node: Node,
25-
target: Target,
25+
neutron_target_spec: NeutronTargetSpec,
2626
parameters_mapping: dict[str, Parameter],
2727
custom_delegation_options: CustomDelegationOptions,
2828
) -> bool:
29-
match target:
30-
case Target.RT700:
31-
if node_uses_shape_broadcasting(node):
32-
# Shape broadcasting may require the addition of `Transpose` ops during conversion.
33-
return False
34-
35-
return True
29+
if node_uses_shape_broadcasting(node):
30+
# Shape broadcasting may require the addition of `Transpose` ops during conversion.
31+
return False
3632

37-
case _:
38-
return False
33+
return True
3934

4035
@staticmethod
4136
def _is_supported_in_IR(

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

Lines changed: 41 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@
1313
_is_dequant_node,
1414
_is_quant_node,
1515
NodeConverter,
16-
Target,
1716
)
1817
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options.concatenation_options import (
1918
Concatenation,
2019
)
20+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
2121
from torch.fx import Node
2222
from torch.nn import Parameter
2323

@@ -72,51 +72,52 @@ def _all_io_shares_quantization_parameters(node: Node) -> bool:
7272
@staticmethod
7373
def _is_supported_on_target(
7474
node: Node,
75-
target: Target,
75+
neutron_target_spec: NeutronTargetSpec,
7676
parameters_mapping: dict[str, Parameter],
7777
custom_delegation_options: CustomDelegationOptions,
7878
) -> bool:
7979
if custom_delegation_options.force_delegate_cat:
8080
return True
8181

82-
match target:
83-
case Target.RT700:
84-
dim = CatConverter._get_normalized_dim(node)
85-
86-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#1491
87-
if dim == 0:
88-
return False
89-
90-
# Neutron requires the channels to be a multiple of `8`. The channels could either be the second or the
91-
# last dimension, depending on the formats of the node. The format, however, cannot be determined
92-
# during conversion, as it depends on what other nodes are delegated.
93-
input_channels = [
94-
# The second dimension is the channels in PyTorch. If the inputs/output are not channels first, it
95-
# will still be the channels in the IR.
96-
_get_shape(input_)[1]
97-
for input_ in node.all_input_nodes
98-
] + [
99-
# If the inputs/outputs are channels first, the last dimension will be the channels.
100-
_get_shape(input_)[-1]
101-
for input_ in node.all_input_nodes
102-
]
103-
if any((input_channel % 8) != 0 for input_channel in input_channels):
104-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#1492
105-
return False
106-
107-
output_channels = [_get_shape(node)[1], _get_shape(node)[-1]]
108-
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#1493
109-
if any((out_c % 8) != 0 for out_c in output_channels):
110-
return False
111-
112-
if len(node.all_input_nodes) < 2: # Not supported on Neutron
113-
# TODO Try to skip the operator if this case is realistic.
114-
return False
115-
116-
return True
117-
118-
case _:
119-
return False
82+
dim = CatConverter._get_normalized_dim(node)
83+
84+
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#1491
85+
if dim == 0:
86+
return False
87+
88+
# Neutron requires the channels to be a multiple of numMacs. The channels could either be the second or the
89+
# last dimension, depending on the formats of the node. The format, however, cannot be determined
90+
# during conversion, as it depends on what other nodes are delegated.
91+
input_channels = [
92+
# The second dimension is the channels in PyTorch. If the inputs/output are not channels first, it
93+
# will still be the channels in the IR.
94+
_get_shape(input_)[1]
95+
for input_ in node.all_input_nodes
96+
] + [
97+
# If the inputs/outputs are channels first, the last dimension will be the channels.
98+
_get_shape(input_)[-1]
99+
for input_ in node.all_input_nodes
100+
]
101+
if any(
102+
(input_channel % neutron_target_spec.get_num_macs()) != 0
103+
for input_channel in input_channels
104+
):
105+
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#1492
106+
return False
107+
108+
output_channels = [_get_shape(node)[1], _get_shape(node)[-1]]
109+
# neutron-library/src/utils/NeutronLibraryInterrogation.cpp#1493
110+
if any(
111+
(out_c % neutron_target_spec.get_num_macs()) != 0
112+
for out_c in output_channels
113+
):
114+
return False
115+
116+
if len(node.all_input_nodes) < 2: # Not supported on Neutron
117+
# TODO Try to skip the operator if this case is realistic.
118+
return False
119+
120+
return True
120121

121122
@staticmethod
122123
def _is_supported_in_IR(

0 commit comments

Comments
 (0)