|
2 | 2 | # |
3 | 3 | # This source code is licensed under the BSD-style license found in the |
4 | 4 | # LICENSE file in the root directory of this source tree. |
5 | | - |
6 | | - |
7 | 5 | import itertools |
8 | 6 | import unittest |
9 | 7 |
|
|
14 | 12 | from executorch.backends.nxp.backend.edge_program_converter import ( |
15 | 13 | EdgeProgramToIRConverter, |
16 | 14 | ) |
| 15 | +from executorch.backends.nxp.backend.ir.converter.node_converters.ops_converters import ( |
| 16 | + PermuteCopyConverter, |
| 17 | +) |
| 18 | +from executorch.backends.nxp.backend.node_format_inference import NodeFormatInference |
| 19 | +from executorch.backends.nxp.edge_passes.move_auxiliary_operator_into_separate_qdq_cluster_pass import ( |
| 20 | + MoveLeadingAuxiliaryOperatorIntoSeparateQDQClusterPass, |
| 21 | +) |
| 22 | +from executorch.backends.nxp.edge_passes.neutron_edge_pass_manager import ( |
| 23 | + NeutronEdgePassManager, |
| 24 | +) |
| 25 | +from executorch.backends.nxp.edge_passes.remove_io_quant_ops_pass import ( |
| 26 | + RemoveIOQuantOpsPass, |
| 27 | +) |
| 28 | +from executorch.backends.nxp.neutron_partitioner import QDQClusterRecognizer |
| 29 | +from executorch.backends.nxp.quantizer.neutron_quantizer import NeutronQuantizer |
| 30 | +from executorch.backends.nxp.tests import executors |
17 | 31 | from executorch.backends.nxp.tests.executorch_pipeline import ( |
| 32 | + _quantize_model, |
| 33 | + get_random_calibration_inputs, |
18 | 34 | to_edge_program, |
| 35 | + to_model_input_spec, |
19 | 36 | to_quantized_edge_program, |
20 | 37 | ) |
21 | 38 | from executorch.backends.nxp.tests.executors import ( |
22 | 39 | convert_run_compare, |
23 | 40 | graph_contains_any, |
24 | 41 | graph_contains_any_of_ops, |
| 42 | + OverrideTargetSupportCheck, |
25 | 43 | ToChannelFirstPreprocess, |
26 | 44 | ToChannelLastPreprocess, |
27 | 45 | ) |
| 46 | +from executorch.exir import EdgeCompileConfig |
28 | 47 | from executorch.exir.dialects._ops import ops as exir_ops |
| 48 | +from executorch.extension.export_util.utils import export_to_edge |
29 | 49 | from parameterized import parameterized |
30 | 50 | from torch import nn |
31 | 51 | from torch.export import ExportedProgram |
@@ -76,6 +96,42 @@ def forward(self, x): |
76 | 96 | return self.block(x) |
77 | 97 |
|
78 | 98 |
|
| 99 | +class TransposeReshapeModel(nn.Module): |
| 100 | + |
| 101 | + def __init__(self, new_shape: list[int]): |
| 102 | + super().__init__() |
| 103 | + self.new_shape = new_shape |
| 104 | + |
| 105 | + def forward(self, x): |
| 106 | + # `x` should be 4D. |
| 107 | + |
| 108 | + x = torch.add(x, x) |
| 109 | + x = torch.permute(x, [0, 3, 1, 2]) |
| 110 | + # A `clone(memory_format=contiguous)` will be added here during the lowering to edge dialect. |
| 111 | + x = torch.reshape(x, self.new_shape) |
| 112 | + |
| 113 | + return x |
| 114 | + |
| 115 | + |
| 116 | +class PermuteCopyReshapeModel(nn.Module): |
| 117 | + |
| 118 | + def __init__(self, new_shape: list[int], permutation: list[int]): |
| 119 | + super().__init__() |
| 120 | + self.new_shape = new_shape |
| 121 | + self.permutation = permutation |
| 122 | + |
| 123 | + def forward(self, x): |
| 124 | + # `x` should be 4D. |
| 125 | + |
| 126 | + x = torch.add(x, x) |
| 127 | + x = torch.permute(x, self.permutation) |
| 128 | + # A `clone(memory_format=contiguous)` will be added here during the lowering to edge dialect. |
| 129 | + x = torch.reshape(x, self.new_shape) |
| 130 | + x = torch.add(x, x) |
| 131 | + |
| 132 | + return x |
| 133 | + |
| 134 | + |
79 | 135 | class TestCloneConverter(unittest.TestCase): |
80 | 136 | __test__ = False # Prevent interfering with PyTest tests |
81 | 137 |
|
@@ -185,3 +241,87 @@ def test_clone_pool_view_copy_quant(self, input_shape: tuple[int] = (1, 64, 25, |
185 | 241 | input_data=input_data, |
186 | 242 | atol=1.0, |
187 | 243 | ) |
| 244 | + |
| 245 | + def test_clone__to_contiguous_format(self): |
| 246 | + input_shape = (1, 8, 8, 8) |
| 247 | + new_shape = [1, 32, 2, 8] |
| 248 | + |
| 249 | + model = TransposeReshapeModel(new_shape).eval() |
| 250 | + |
| 251 | + calibration_inputs = get_random_calibration_inputs( |
| 252 | + to_model_input_spec(input_shape) |
| 253 | + ) |
| 254 | + |
| 255 | + example_input = calibration_inputs[0] |
| 256 | + |
| 257 | + exir_program_aten = torch.export.export(model, example_input, strict=True) |
| 258 | + |
| 259 | + exir_program_aten__module_quant = _quantize_model( |
| 260 | + exir_program_aten.module(), NeutronQuantizer(), calibration_inputs |
| 261 | + ) |
| 262 | + |
| 263 | + edge_compile_config = EdgeCompileConfig(_check_ir_validity=False) |
| 264 | + edge_program_manager = export_to_edge( |
| 265 | + exir_program_aten__module_quant, |
| 266 | + example_input, |
| 267 | + edge_compile_config=edge_compile_config, |
| 268 | + ) |
| 269 | + # Make sure the `aten.clone` was inserted as expected. |
| 270 | + nodes = list(edge_program_manager.exported_program().graph.nodes) |
| 271 | + assert nodes[9].target == exir_ops.edge.dim_order_ops._clone_dim_order.default |
| 272 | + assert nodes[9].kwargs["dim_order"] == [0, 1, 2, 3] |
| 273 | + |
| 274 | + # Move the `clone` out of the cluster with the `view_copy`. |
| 275 | + edge_program_manager = NeutronEdgePassManager( |
| 276 | + [MoveLeadingAuxiliaryOperatorIntoSeparateQDQClusterPass()] |
| 277 | + )(edge_program_manager) |
| 278 | + |
| 279 | + # Tag QDQ clusters, so the conversion works correctly. |
| 280 | + QDQClusterRecognizer().tag_qdq_clusters( |
| 281 | + list(edge_program_manager.exported_program().graph.nodes) |
| 282 | + ) |
| 283 | + edge_program_manager.exported_program().graph_module.recompile() |
| 284 | + edge_program_manager = edge_program_manager.transform( |
| 285 | + [RemoveIOQuantOpsPass(edge_program_manager=edge_program_manager)] |
| 286 | + ) |
| 287 | + |
| 288 | + # Identify the node formats. |
| 289 | + NodeFormatInference( |
| 290 | + edge_program_manager.exported_program() |
| 291 | + ).identify_node_formats() |
| 292 | + |
| 293 | + # Convert to the IR. |
| 294 | + converted_model, _ = EdgeProgramToIRConverter().convert_program( |
| 295 | + edge_program_manager.exported_program() |
| 296 | + ) |
| 297 | + |
| 298 | + # Make sure the IR version produces the same outputs. |
| 299 | + executors.convert_run_compare( |
| 300 | + edge_program_manager.exported_program(), |
| 301 | + np.random.random_integers(0, 255, input_shape).astype("int8"), |
| 302 | + tfl_model=converted_model, |
| 303 | + ) |
| 304 | + |
| 305 | + def test_clone__to_contiguous_format__non_delegated_permute_copy(self): |
| 306 | + input_shape = (2, 4, 6, 8) |
| 307 | + new_shape = [3, 4, 16, 2] |
| 308 | + permutation = [3, 2, 1, 0] # Unsupported by default. |
| 309 | + |
| 310 | + model = PermuteCopyReshapeModel(new_shape, permutation).eval() |
| 311 | + |
| 312 | + # Prohibit `permute_copy` delegation in case support for the permutation is added in the future. |
| 313 | + def _unsupported_target(*_): |
| 314 | + return False |
| 315 | + |
| 316 | + with OverrideTargetSupportCheck( |
| 317 | + PermuteCopyConverter, new_target_support_check=_unsupported_target |
| 318 | + ): |
| 319 | + ep = to_quantized_edge_program(model, input_shape).exported_program() |
| 320 | + |
| 321 | + nodes = list(ep.graph.nodes) |
| 322 | + assert not graph_contains_any_of_ops( |
| 323 | + ep.graph, [exir_ops.edge.aten.clone.default] |
| 324 | + ) |
| 325 | + assert nodes[3].name == "executorch_call_delegate" |
| 326 | + assert nodes[6].target == exir_ops.edge.aten.permute_copy.default |
| 327 | + assert nodes[9].name == "executorch_call_delegate_1" |
0 commit comments