|
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 | 5 |
|
| 6 | +import unittest |
| 7 | + |
| 8 | +import kgb |
6 | 9 | import numpy as np |
7 | | -import pytest |
8 | 10 | import torch |
9 | 11 |
|
10 | 12 | from executorch.backends.nxp.backend.edge_program_converter import ( |
|
13 | 15 | from executorch.backends.nxp.tests.executorch_pipeline import to_quantized_edge_program |
14 | 16 | from executorch.backends.nxp.tests.executors import ( |
15 | 17 | convert_run_compare, |
16 | | - ToNCHWPreprocess, |
17 | | - ToNHWCPreprocess, |
| 18 | + graph_contains_any_of_ops, |
| 19 | + ToChannelFirstPreprocess, |
| 20 | + ToChannelLastPreprocess, |
18 | 21 | ) |
19 | 22 | from executorch.backends.nxp.tests.models import Conv2dModule |
| 23 | +from executorch.exir.dialects._ops import ops as exir_ops |
| 24 | +from parameterized import parameterized |
20 | 25 | from torch.export import ExportedProgram |
21 | 26 |
|
22 | 27 |
|
23 | | -@pytest.fixture(autouse=True) |
24 | | -def reseed_model_per_test_run(): |
25 | | - torch.manual_seed(23) |
26 | | - np.random.seed(23) |
| 28 | +class Conv2dTransposeModule(torch.nn.Module): |
| 29 | + def __init__(self, in_channels: int, dim0: int, dim1: int): |
| 30 | + super().__init__() |
| 31 | + self.dim0 = dim0 |
| 32 | + self.dim1 = dim1 |
| 33 | + self.conv = Conv2dModule( |
| 34 | + in_channels=in_channels, out_channels=in_channels, kernel_size=(1, 1) |
| 35 | + ) |
27 | 36 |
|
| 37 | + def forward(self, x): |
| 38 | + x = self.conv(x) |
| 39 | + return torch.transpose(x, self.dim0, self.dim1) |
28 | 40 |
|
29 | | -class Conv2dPermuteCopyModule(torch.nn.Module): |
30 | | - def __init__(self, new_dims: tuple[int, ...]): |
| 41 | + |
| 42 | +class Conv2dPermuteModule(torch.nn.Module): |
| 43 | + def __init__(self, in_channels: int, new_dims: tuple[int, ...]): |
31 | 44 | super().__init__() |
32 | 45 | self.new_dims = new_dims |
33 | | - self.conv = Conv2dModule() |
| 46 | + self.conv = Conv2dModule( |
| 47 | + in_channels=in_channels, |
| 48 | + out_channels=in_channels, |
| 49 | + stride=1, |
| 50 | + kernel_size=3, |
| 51 | + padding=1, |
| 52 | + ) |
34 | 53 |
|
35 | 54 | def forward(self, x): |
36 | 55 | x = self.conv(x) |
37 | 56 | return torch.permute(x, self.new_dims) |
38 | 57 |
|
39 | 58 |
|
40 | | -def test_permute_copy_quant_conversion__with_bias(mocker): |
41 | | - input_shape = (1, 4, 8, 8) |
42 | | - new_dims = (0, 2, 3, 1) |
| 59 | +class LinearPermuteModule(torch.nn.Module): |
| 60 | + def __init__(self, in_features: int, new_dims: tuple[int, ...]): |
| 61 | + super().__init__() |
| 62 | + self.new_dims = new_dims |
| 63 | + self.fc = torch.nn.Linear(in_features, in_features) |
| 64 | + |
| 65 | + def forward(self, x): |
| 66 | + x = self.fc(x) |
| 67 | + return torch.permute(x, self.new_dims) |
| 68 | + |
| 69 | + |
| 70 | +class TestPermuteCopyConversion(kgb.SpyAgency, unittest.TestCase): |
| 71 | + @classmethod |
| 72 | + def setUpClass(cls): |
| 73 | + torch.manual_seed(23) |
| 74 | + np.random.seed(42) |
| 75 | + |
| 76 | + @parameterized.expand( |
| 77 | + [ |
| 78 | + ["To channel first permutation", (1, 16, 8, 8), (0, 3, 1, 2)], |
| 79 | + ["To channel last permutation", (1, 16, 8, 8), (0, 2, 3, 1)], |
| 80 | + ] |
| 81 | + ) |
| 82 | + def test_permute_copy_conversion__from_permute_4D__quantized( |
| 83 | + self, _: str, input_shape, new_dims |
| 84 | + ): |
| 85 | + with kgb.spy_on( |
| 86 | + EdgeProgramToIRConverter.convert_program, call_original=True |
| 87 | + ) as converter_spy: |
| 88 | + model = Conv2dPermuteModule(input_shape[1], new_dims) |
| 89 | + |
| 90 | + # Run conversion |
| 91 | + edge_program = to_quantized_edge_program( |
| 92 | + model, input_shape |
| 93 | + ).exported_program() |
43 | 94 |
|
44 | | - converter_spy = mocker.spy(EdgeProgramToIRConverter, "convert_program") |
| 95 | + # Make sure the `Permute_copy` was delegated. |
| 96 | + assert not graph_contains_any_of_ops( |
| 97 | + graph=edge_program.graph, ops=[exir_ops.edge.aten.permute_copy.default] |
| 98 | + ) |
| 99 | + assert any( |
| 100 | + "lowered_module" in node.name for node in edge_program.graph.nodes |
| 101 | + ) |
45 | 102 |
|
46 | | - # Run conversion |
47 | | - _ = to_quantized_edge_program(Conv2dPermuteCopyModule(new_dims), input_shape) |
| 103 | + # Capture generated model |
| 104 | + tflite_flatbuffers_model, io_formats = converter_spy.calls[-1].return_value |
48 | 105 |
|
49 | | - # Capture generated model |
50 | | - tflite_flatbuffers_model, io_formats = converter_spy.spy_return |
| 106 | + # Capture converted program |
| 107 | + exported_program: ExportedProgram = converter_spy.calls[-1].args[0] |
51 | 108 |
|
52 | | - # Capture converted program |
53 | | - edge_program: ExportedProgram = converter_spy.call_args.args[1] |
| 109 | + input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype( |
| 110 | + np.int8 |
| 111 | + ) |
54 | 112 |
|
55 | | - input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype(np.int8) |
| 113 | + convert_run_compare( |
| 114 | + exported_program, |
| 115 | + input_data, |
| 116 | + tfl_model=tflite_flatbuffers_model, |
| 117 | + atol=1.0, |
| 118 | + tflite_input_preprocess=ToChannelLastPreprocess(), |
| 119 | + tflite_output_preprocess=ToChannelFirstPreprocess(), |
| 120 | + ) |
56 | 121 |
|
57 | | - convert_run_compare( |
58 | | - edge_program, |
59 | | - input_data, |
60 | | - tfl_model=tflite_flatbuffers_model, |
61 | | - atol=1.0, |
62 | | - tflite_input_preprocess=ToNHWCPreprocess(), |
63 | | - tflite_output_preprocess=ToNCHWPreprocess(), |
| 122 | + @parameterized.expand( |
| 123 | + [ |
| 124 | + ["Permutation can be replaced by reshapes", (10, 1, 8), (0, 2, 1)], |
| 125 | + ["Permutation can be replaced by reshapes", (10, 1, 1), (2, 1, 0)], |
| 126 | + ["Permutation is identical and can be removed", (10, 1, 8), (0, 1, 2)], |
| 127 | + ] |
64 | 128 | ) |
| 129 | + def test_permute_copy_conversion__from_permute_3D__quantized( |
| 130 | + self, _: str, input_shape, new_dims |
| 131 | + ): |
| 132 | + with kgb.spy_on( |
| 133 | + EdgeProgramToIRConverter.convert_program, call_original=True |
| 134 | + ) as converter_spy: |
| 135 | + # Run conversion |
| 136 | + edge_program = to_quantized_edge_program( |
| 137 | + LinearPermuteModule(input_shape[2], new_dims), input_shape |
| 138 | + ).exported_program() |
| 139 | + |
| 140 | + # Make sure the `Permute_copy` was delegated. |
| 141 | + assert not graph_contains_any_of_ops( |
| 142 | + graph=edge_program.graph, ops=[exir_ops.edge.aten.permute_copy.default] |
| 143 | + ) |
| 144 | + assert any( |
| 145 | + "lowered_module" in node.name for node in edge_program.graph.nodes |
| 146 | + ) |
| 147 | + |
| 148 | + # Capture generated model |
| 149 | + tflite_flatbuffers_model, io_formats = converter_spy.calls[-1].return_value |
| 150 | + |
| 151 | + # Capture converted program |
| 152 | + exported_program: ExportedProgram = converter_spy.calls[-1].args[0] |
| 153 | + |
| 154 | + input_data = (np.random.random(input_shape).astype(np.float32) * 50).astype( |
| 155 | + np.int8 |
| 156 | + ) |
| 157 | + |
| 158 | + convert_run_compare( |
| 159 | + exported_program, |
| 160 | + input_data, |
| 161 | + tfl_model=tflite_flatbuffers_model, |
| 162 | + atol=1.0, |
| 163 | + ) |
| 164 | + |
| 165 | + @parameterized.expand( |
| 166 | + [ |
| 167 | + ["Transpose dims 1 and 2", (1, 16, 8, 8), (0, 2, 1, 3)], |
| 168 | + ["To (2, 0, 1, 3) permutation", (1, 16, 8, 8), (2, 0, 1, 3)], |
| 169 | + ["To (3, 1, 2, 0) permutation", (1, 16, 8, 8), (3, 1, 2, 0)], |
| 170 | + ["To (3, 1, 0, 2) permutation", (1, 16, 8, 8), (3, 1, 0, 2)], |
| 171 | + ] |
| 172 | + ) |
| 173 | + def test_permute_copy_non_delegated_conversion__from_permute_4D__quantized( |
| 174 | + self, _: str, input_shape, new_dims |
| 175 | + ): |
| 176 | + model = Conv2dPermuteModule(input_shape[1], new_dims) |
| 177 | + edge_program = to_quantized_edge_program(model, input_shape).exported_program() |
| 178 | + |
| 179 | + nodes = list(edge_program.graph.nodes) |
| 180 | + assert len(nodes) == 10 |
| 181 | + assert ( |
| 182 | + nodes[6].target == exir_ops.edge.aten.permute_copy.default |
| 183 | + ) # PermuteCopy not delegated. |
| 184 | + |
| 185 | + @parameterized.expand( |
| 186 | + [ |
| 187 | + ["Transpose dims 1 and 2", (1, 16, 8, 8), 1, 2], |
| 188 | + ["Transpose dims 2 and 3", (1, 16, 8, 8), 2, 3], |
| 189 | + ] |
| 190 | + ) |
| 191 | + def test_permute_copy_non_delegated_conversion__from_transpose_4D__quantized( |
| 192 | + self, _: str, input_shape, dim0, dim1 |
| 193 | + ): |
| 194 | + model = Conv2dTransposeModule(input_shape[1], dim0, dim1) |
| 195 | + edge_program = to_quantized_edge_program(model, input_shape).exported_program() |
| 196 | + |
| 197 | + nodes = list(edge_program.graph.nodes) |
| 198 | + assert len(nodes) == 10 |
| 199 | + assert ( |
| 200 | + nodes[6].target == exir_ops.edge.aten.permute_copy.default |
| 201 | + ) # PermuteCopy not delegated. |
0 commit comments