Skip to content

Commit 488d761

Browse files
NXP backend: Improve view_copy delegation (#15270)
### Summary The conversion of `aten.view_copy` to NeutronIR may require the insertion of extra `Transpose` operations, which may not be supported. This PR makes sure the `view_copy` is only delegated if the extra operations are supported too. ### Test plan Unit tests provided. cc @robert-kalmar
1 parent 5e60898 commit 488d761

File tree

6 files changed

+403
-17
lines changed

6 files changed

+403
-17
lines changed

backends/nxp/backend/edge_helper.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,51 @@ def get_quantization_parameters_for(node: Node) -> tuple[Scale, ZeroPoint] | Non
136136
return None
137137

138138
return node.args[1], node.args[2] # Scale and zero_point
139+
140+
141+
def get_non_qdq_users(node: Node) -> list[Node]:
142+
"""Return a list of nodes which consume the output of `node`, but Quantize/Dequantize nodes from QDQ clusters are
143+
ignored. Meaning, the list of nodes [<user_1>, ..., <user_N>] from the illustration below is returned.
144+
145+
If the graph does not follow the QDQ pattern, an empty list is returned.
146+
147+
148+
┌───▼────┐
149+
│ `node` │
150+
└───┬────┘
151+
┌────▼─────┐
152+
│ Quantize │
153+
└────┬─────┘
154+
├─────── ... ──────┐
155+
┌─────▼──────┐ ┌─────▼──────┐
156+
│ Dequantize │ ... │ Dequantize │
157+
└─────┬──────┘ └─────┬──────┘
158+
┌────▼─────┐ ┌────▼─────┐
159+
│ <user_1> │ ... │ <user_N> │
160+
└────┬─────┘ └────┬─────┘
161+
162+
"""
163+
164+
quant_nodes = list(node.users)
165+
if len(quant_nodes) != 1 or quant_nodes[0].target not in [
166+
exir_ops.edge.quantized_decomposed.quantize_per_tensor.default,
167+
exir_ops.edge.quantized_decomposed.quantize_per_channel.default,
168+
]:
169+
return []
170+
171+
dequant_nodes = list(quant_nodes[0].users)
172+
if any(
173+
dequant_node.target
174+
not in [
175+
exir_ops.edge.quantized_decomposed.dequantize_per_tensor.default,
176+
exir_ops.edge.quantized_decomposed.dequantize_per_channel.default,
177+
]
178+
for dequant_node in dequant_nodes
179+
):
180+
return []
181+
182+
res = []
183+
for dequant_node in dequant_nodes:
184+
res.extend(list(dequant_node.users))
185+
186+
return res

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,14 +125,19 @@ def supports_partitioning_result(
125125
node: Node,
126126
partition_list: list[Partition],
127127
custom_delegation_options: CustomDelegationOptions,
128-
):
128+
neutron_target_spec: NeutronTargetSpec,
129+
parameters_mapping: dict[str, Parameter],
130+
) -> bool:
129131
"""Check if the given `node` supports the assigned partitioning, which is stored the `partition_list`. Child
130132
classes can overwrite this method in case they have delegation restrictions based on the context defined by
131133
the partitioning result.
132134
133135
:param node: torch.Node to check.
134136
:param partition_list: List of proposed partitions.
135137
:param custom_delegation_options: Custom user options which affect node delegation.
138+
:param neutron_target_spec: NeutronTargetSpec instance.
139+
:param parameters_mapping: Dictionary mapping tensor names to their static data.
140+
:return: Boolean indicating whether the node supports the current partitioning.
136141
"""
137142
return True
138143

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,9 @@ def supports_partitioning_result(
156156
node: Node,
157157
partition_list: list[Partition],
158158
custom_delegation_options: CustomDelegationOptions,
159-
):
159+
neutron_target_spec: NeutronTargetSpec,
160+
parameters_mapping: dict[str, Parameter],
161+
) -> bool:
160162
# There is a bug in the NeutronConverter, where if none of the input dimensions before the one referenced by
161163
# `dim` are `!= 1`, the `Concat` is not delegated.
162164
# This only happens when the inputs to the `Concat` are model inputs, and not outputs of other

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

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,18 @@
66
import numpy as np
77

88
from executorch.backends.nxp.backend.edge_helper import (
9+
get_non_qdq_users,
910
input_tensor,
1011
output_tensor,
1112
tensor_rank,
1213
)
1314
from executorch.backends.nxp.backend.ir.converter import quantization_utils
1415
from executorch.backends.nxp.backend.ir.converter.conversion.common import OpsList
16+
from executorch.backends.nxp.backend.ir.converter.conversion.translator import (
17+
apply_permutation_to,
18+
create_channels_first_to_channels_last_permutation,
19+
create_channels_last_to_channels_first_permutation,
20+
)
1521
from executorch.backends.nxp.backend.ir.converter.node_converter import (
1622
CustomDelegationOptions,
1723
is_not_qdq_node,
@@ -23,6 +29,12 @@
2329
from executorch.backends.nxp.backend.ir.tflite_generator.builtin_options import (
2430
reshape_options,
2531
)
32+
from executorch.backends.nxp.backend.neutron_operator_support import (
33+
transposition_is_supported_on_neutron,
34+
)
35+
from executorch.backends.nxp.backend.neutron_target_spec import NeutronTargetSpec
36+
from executorch.backends.nxp.backend.node_format import NXP_NODE_FORMAT
37+
from executorch.exir.dialects._ops import ops as exir_ops
2638
from torch.fx import Node
2739
from torch.fx.passes.infra.partitioner import Partition
2840
from torch.nn import Parameter
@@ -53,6 +65,8 @@ def supports_partitioning_result(
5365
node: Node,
5466
partition_list: list[Partition],
5567
custom_delegation_options: CustomDelegationOptions,
68+
neutron_target_spec: NeutronTargetSpec,
69+
parameters_mapping: dict[str, Parameter],
5670
):
5771
view_copy_partitions = [
5872
partition for partition in partition_list if node in partition.nodes
@@ -66,6 +80,76 @@ def supports_partitioning_result(
6680
# The `view_copy` cannot be the only node in a partition.
6781
return False
6882

83+
input_format = node.args[0].meta[NXP_NODE_FORMAT]
84+
output_format = node.meta[NXP_NODE_FORMAT]
85+
input_shape = list(node.args[0].meta["val"].shape)
86+
output_shape = list(node.meta["val"].shape)
87+
to_nchw_perm = create_channels_last_to_channels_first_permutation(
88+
len(input_shape), True
89+
)
90+
to_nhwc_perm = create_channels_first_to_channels_last_permutation(
91+
len(output_shape), True
92+
)
93+
channels_last_input_shape = apply_permutation_to(
94+
input_shape,
95+
create_channels_first_to_channels_last_permutation(len(input_shape), True),
96+
)
97+
98+
if input_format.is_channels_first() and (not output_format.is_channels_first()):
99+
# The `view_copy` removes node format. Conversion will require the addition of a `Transpose` operator.
100+
# Make sure the `Transpose` will be supported.
101+
102+
if not transposition_is_supported_on_neutron(
103+
channels_last_input_shape, to_nchw_perm, neutron_target_spec
104+
):
105+
# The `Transpose` would have to be removed by the `PermuteFullyConnectedWeightsAfterReshape` pass.
106+
# Make sure it will be applied.
107+
users = get_non_qdq_users(node)
108+
if len(users) != 1 or (linear_node := users[0]).target not in [
109+
exir_ops.edge.aten.addmm.default,
110+
exir_ops.edge.aten.mm.default,
111+
]:
112+
return False
113+
114+
if linear_node not in view_copy_partitions[0].nodes:
115+
# The `mm` / `addmm` node will not be delegated within this partition.
116+
return False
117+
118+
# Make sure the specific requirements of the `PermuteFullyConnectedWeightsAfterReshape` are satisfied.
119+
weights_index = (
120+
2 if linear_node.target == exir_ops.edge.aten.addmm.default else 1
121+
)
122+
if not (
123+
input_shape[0] == output_shape[0] # Preserve batch.
124+
and len(output_shape) == 2
125+
and output_shape[1]
126+
== linear_node.args[weights_index].meta["val"].shape[0]
127+
):
128+
return False
129+
130+
elif (
131+
not input_format.is_channels_first()
132+
) and output_format.is_channels_first():
133+
# The `view_copy` introduces node format. Conversion will require the addition of a `Transpose` operator.
134+
# Make sure the `Transpose` will be supported.
135+
if not transposition_is_supported_on_neutron(
136+
output_shape, to_nhwc_perm, neutron_target_spec
137+
):
138+
return False
139+
140+
elif input_format.is_channels_first() and output_format.is_channels_first():
141+
# The `view_copy` works with the channels first format, so both tensors will end up being transposed.
142+
# Make sure these transpositions are supported.
143+
if not (
144+
transposition_is_supported_on_neutron(
145+
channels_last_input_shape, to_nchw_perm, neutron_target_spec
146+
)
147+
and transposition_is_supported_on_neutron(
148+
output_shape, to_nhwc_perm, neutron_target_spec
149+
)
150+
):
151+
return False
152+
69153
return True
70154

71155
@staticmethod

backends/nxp/neutron_partitioner.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -317,11 +317,12 @@ def __init__(
317317
)
318318
self.neutron_target_spec = neutron_target_spec
319319

320-
@staticmethod
321320
def validate_partitioning_result(
321+
self,
322322
graph: Graph,
323323
partition_list: list[Partition],
324324
custom_delegation_options: CustomDelegationOptions,
325+
parameters_mapping: dict[str, Parameter],
325326
) -> bool:
326327
all_delegated_nodes = {
327328
node for partition in partition_list for node in partition.nodes
@@ -334,7 +335,11 @@ def validate_partitioning_result(
334335
and node.target in supported_ops
335336
):
336337
if not supported_ops[node.target].supports_partitioning_result(
337-
node, partition_list, custom_delegation_options
338+
node,
339+
partition_list,
340+
custom_delegation_options,
341+
self.neutron_target_spec,
342+
parameters_mapping,
338343
):
339344
# This node is not supported within its partition. Exclude it from delegation in the future.
340345
partitioning_valid = False
@@ -379,14 +384,21 @@ def partition(self, exported_program: ExportedProgram) -> PartitionResult:
379384
# This format will be used by the `CapabilityBasedPartitioner` to determine which nodes will be delegated.
380385
NodeFormatInference(exported_program).identify_node_formats()
381386

387+
parameters_mapping = EdgeProgramToIRConverter.map_inputs_to_parameters(
388+
exported_program
389+
)
390+
382391
iteration_limit = len(exported_program.graph.nodes)
383392
for _ in range(iteration_limit):
384393
# Run the partitioning.
385394
partition_list = capability_partitioner.propose_partitions()
386395

387396
# Check if the nodes support the partitioning result. Mark the problematic nodes with `NXP_DO_NOT_DELEGATE`.
388397
partitioning_valid = self.validate_partitioning_result(
389-
exported_program.graph, partition_list, self.custom_delegation_options
398+
exported_program.graph,
399+
partition_list,
400+
self.custom_delegation_options,
401+
parameters_mapping,
390402
)
391403
if partitioning_valid:
392404
# The result of the partitioning is fine

0 commit comments

Comments
 (0)