|
4 | 4 | # This source code is licensed under the BSD-style license found in the |
5 | 5 | # LICENSE file in the root directory of this source tree. |
6 | 6 |
|
| 7 | +import copy |
| 8 | +import io |
7 | 9 | import operator |
8 | 10 | import unittest |
9 | 11 | from unittest.mock import patch |
|
12 | 14 |
|
13 | 15 | import torch |
14 | 16 | from executorch import exir |
15 | | -from executorch.exir import to_edge |
| 17 | +from executorch.exir import EdgeProgramManager, to_edge, to_edge_transform_and_lower |
| 18 | +from executorch.exir._serialize import _PTEFile, _serialize_pte_binary |
16 | 19 | from executorch.exir.backend.backend_api import to_backend |
17 | 20 | from executorch.exir.backend.compile_spec_schema import CompileSpec |
18 | 21 | from executorch.exir.backend.test.backend_with_compiler_demo import ( |
19 | 22 | BackendWithCompilerDemo, |
20 | 23 | ) |
21 | 24 | from executorch.exir.backend.test.demo_backend import DemoBackend |
22 | | -from executorch.exir.schema import DelegateCall, Program |
| 25 | +from executorch.exir.backend.test.op_partitioner_demo import AddMulPartitionerDemo |
| 26 | +from executorch.exir.schema import DelegateCall, DelegateScratch, Program |
| 27 | +from executorch.exir.tensor import make_allocation_info |
23 | 28 |
|
24 | 29 | from executorch.extension.pybindings.portable_lib import ( # @manual |
25 | 30 | _load_for_executorch_from_buffer, |
@@ -107,6 +112,206 @@ def forward(self, x): |
107 | 112 | torch.allclose(model_outputs[0], expected_res, atol=1e-03, rtol=1e-03) |
108 | 113 | ) |
109 | 114 |
|
| 115 | + def _repeated_sin_calls(self, backend_id, compile_specs, num_calls): |
| 116 | + """Lowers sin once and returns it with a module that calls it num_calls times.""" |
| 117 | + |
| 118 | + class SinModule(torch.nn.Module): |
| 119 | + def forward(self, x): |
| 120 | + return torch.sin(x) |
| 121 | + |
| 122 | + model_inputs = (torch.ones(1),) |
| 123 | + lowered_sin_module = to_backend( |
| 124 | + backend_id, |
| 125 | + to_edge(export(SinModule(), model_inputs, strict=True)).exported_program(), |
| 126 | + compile_specs, |
| 127 | + ) |
| 128 | + |
| 129 | + class RepeatedCalls(torch.nn.Module): |
| 130 | + def __init__(self): |
| 131 | + super().__init__() |
| 132 | + self.sin = lowered_sin_module |
| 133 | + |
| 134 | + def forward(self, x): |
| 135 | + for _ in range(num_calls): |
| 136 | + x = self.sin(x) |
| 137 | + return x |
| 138 | + |
| 139 | + edge = to_edge(export(RepeatedCalls(), model_inputs, strict=True)) |
| 140 | + return lowered_sin_module, edge, model_inputs |
| 141 | + |
| 142 | + def _delegate_indices(self, plan): |
| 143 | + return [ |
| 144 | + instruction.instr_args.delegate_index |
| 145 | + for instruction in plan.chains[0].instructions |
| 146 | + if isinstance(instruction.instr_args, DelegateCall) |
| 147 | + ] |
| 148 | + |
| 149 | + def test_calls_of_one_lowered_module_share_one_delegate(self): |
| 150 | + lowered_sin_module, edge, _ = self._repeated_sin_calls( |
| 151 | + BackendWithCompilerDemo.__name__, |
| 152 | + [CompileSpec("max_value", bytes([1]))], |
| 153 | + num_calls=3, |
| 154 | + ) |
| 155 | + self.assertTrue(copy.deepcopy(lowered_sin_module).share_handle_across_calls) |
| 156 | + |
| 157 | + plan = edge.to_executorch().executorch_program.execution_plan[0] |
| 158 | + self.assertEqual(len(plan.delegates), 1) |
| 159 | + self.assertEqual(self._delegate_indices(plan), [0, 0, 0]) |
| 160 | + args = [ |
| 161 | + tuple(instruction.instr_args.args) |
| 162 | + for instruction in plan.chains[0].instructions |
| 163 | + if isinstance(instruction.instr_args, DelegateCall) |
| 164 | + ] |
| 165 | + self.assertEqual(len(set(args)), 3) |
| 166 | + |
| 167 | + def test_shared_delegate_matches_separate_calls_at_runtime(self): |
| 168 | + lowered_sin_module, edge, model_inputs = self._repeated_sin_calls( |
| 169 | + BackendWithCompilerDemo.__name__, |
| 170 | + [CompileSpec("max_value", bytes([1]))], |
| 171 | + num_calls=3, |
| 172 | + ) |
| 173 | + program = edge.to_executorch() |
| 174 | + self.assertEqual(len(program.executorch_program.execution_plan[0].delegates), 1) |
| 175 | + |
| 176 | + expected = model_inputs[0] |
| 177 | + single_call = _load_for_executorch_from_buffer(lowered_sin_module.buffer()) |
| 178 | + for _ in range(3): |
| 179 | + expected = single_call.forward([expected])[0] |
| 180 | + outputs = _load_for_executorch_from_buffer(program.buffer).forward( |
| 181 | + list(model_inputs) |
| 182 | + ) |
| 183 | + self.assertTrue(torch.equal(outputs[0], expected)) |
| 184 | + |
| 185 | + def test_sharing_survives_a_serde_round_trip(self): |
| 186 | + _, edge, _ = self._repeated_sin_calls( |
| 187 | + BackendWithCompilerDemo.__name__, |
| 188 | + [CompileSpec("max_value", bytes([1]))], |
| 189 | + num_calls=2, |
| 190 | + ) |
| 191 | + buffer = io.BytesIO() |
| 192 | + exir.save(edge.exported_program(), buffer) |
| 193 | + buffer.seek(0) |
| 194 | + |
| 195 | + plan = ( |
| 196 | + EdgeProgramManager(exir.load(buffer)) |
| 197 | + .to_executorch() |
| 198 | + .executorch_program.execution_plan[0] |
| 199 | + ) |
| 200 | + self.assertEqual(self._delegate_indices(plan), [0, 0]) |
| 201 | + |
| 202 | + def test_runtime_rejects_a_shared_delegate_with_scratch(self): |
| 203 | + _, edge, model_inputs = self._repeated_sin_calls( |
| 204 | + BackendWithCompilerDemo.__name__, |
| 205 | + [CompileSpec("max_value", bytes([1]))], |
| 206 | + num_calls=2, |
| 207 | + ) |
| 208 | + |
| 209 | + def delegate_calls(program): |
| 210 | + return [ |
| 211 | + instruction.instr_args |
| 212 | + for instruction in program.execution_plan[0].chains[0].instructions |
| 213 | + if isinstance(instruction.instr_args, DelegateCall) |
| 214 | + ] |
| 215 | + |
| 216 | + program = edge.to_executorch().executorch_program |
| 217 | + scratch = [DelegateScratch(allocation=make_allocation_info(1, 0), size=4)] |
| 218 | + |
| 219 | + def run(program): |
| 220 | + return _load_for_executorch_from_buffer( |
| 221 | + bytes(_serialize_pte_binary(pte_file=_PTEFile(program=program))) |
| 222 | + ).forward(list(model_inputs)) |
| 223 | + |
| 224 | + # The same scratch on two separate delegates is valid, so the errors |
| 225 | + # below come from the sharing alone. |
| 226 | + separate = copy.deepcopy(program) |
| 227 | + delegates = separate.execution_plan[0].delegates |
| 228 | + delegates.append(copy.deepcopy(delegates[0])) |
| 229 | + delegate_calls(separate)[1].delegate_index = 1 |
| 230 | + for call in delegate_calls(separate): |
| 231 | + call.scratch = scratch |
| 232 | + run(separate) |
| 233 | + |
| 234 | + # The emitter never writes these, but another producer could. |
| 235 | + for calls_with_scratch in ([0, 1], [0], [1]): |
| 236 | + shared = copy.deepcopy(program) |
| 237 | + for index in calls_with_scratch: |
| 238 | + delegate_calls(shared)[index].scratch = scratch |
| 239 | + with self.assertRaisesRegex(RuntimeError, "error: 0x23"): |
| 240 | + run(shared) |
| 241 | + |
| 242 | + def test_partitioned_module_shares_one_delegate_across_calls(self): |
| 243 | + class AddModule(torch.nn.Module): |
| 244 | + def forward(self, x): |
| 245 | + return x + x |
| 246 | + |
| 247 | + model_inputs = (torch.ones(2, 2),) |
| 248 | + add = ( |
| 249 | + to_edge_transform_and_lower( |
| 250 | + export(AddModule(), model_inputs, strict=True), |
| 251 | + partitioner=[AddMulPartitionerDemo()], |
| 252 | + ) |
| 253 | + .exported_program() |
| 254 | + .module() |
| 255 | + ) |
| 256 | + |
| 257 | + class TwoCalls(torch.nn.Module): |
| 258 | + def __init__(self): |
| 259 | + super().__init__() |
| 260 | + self.add = add |
| 261 | + |
| 262 | + def forward(self, x): |
| 263 | + return self.add(self.add(x)) |
| 264 | + |
| 265 | + plan = ( |
| 266 | + to_edge(export(TwoCalls(), model_inputs, strict=False)) |
| 267 | + .to_executorch() |
| 268 | + .executorch_program.execution_plan[0] |
| 269 | + ) |
| 270 | + self.assertEqual(self._delegate_indices(plan), [0, 0]) |
| 271 | + |
| 272 | + def test_each_call_uses_the_delegate_of_its_own_module(self): |
| 273 | + class SinModule(torch.nn.Module): |
| 274 | + def forward(self, x): |
| 275 | + return torch.sin(x) |
| 276 | + |
| 277 | + model_inputs = (torch.ones(1),) |
| 278 | + edge_sin = to_edge(export(SinModule(), model_inputs, strict=True)) |
| 279 | + compile_specs = [CompileSpec("max_value", bytes([1]))] |
| 280 | + first, second = ( |
| 281 | + to_backend( |
| 282 | + BackendWithCompilerDemo.__name__, |
| 283 | + edge_sin.exported_program(), |
| 284 | + compile_specs, |
| 285 | + ) |
| 286 | + for _ in range(2) |
| 287 | + ) |
| 288 | + |
| 289 | + class TwoModules(torch.nn.Module): |
| 290 | + def __init__(self): |
| 291 | + super().__init__() |
| 292 | + self.first = first |
| 293 | + self.second = second |
| 294 | + |
| 295 | + def forward(self, x): |
| 296 | + return self.first(self.second(self.first(x))) |
| 297 | + |
| 298 | + program = ( |
| 299 | + to_edge(export(TwoModules(), model_inputs, strict=True)) |
| 300 | + .to_executorch() |
| 301 | + .executorch_program |
| 302 | + ) |
| 303 | + # Identical blobs are stored once, but separate modules keep separate |
| 304 | + # delegates, and a repeated call goes back to its own module's delegate. |
| 305 | + self.assertEqual(len(program.backend_delegate_data), 1) |
| 306 | + self.assertEqual(self._delegate_indices(program.execution_plan[0]), [0, 1, 0]) |
| 307 | + |
| 308 | + def test_calls_keep_their_own_delegate_unless_the_backend_allows_sharing(self): |
| 309 | + _, edge, _ = self._repeated_sin_calls(DemoBackend.__name__, [], num_calls=2) |
| 310 | + |
| 311 | + plan = edge.to_executorch().executorch_program.execution_plan[0] |
| 312 | + self.assertEqual(len(plan.delegates), 2) |
| 313 | + self.assertEqual(self._delegate_indices(plan), [0, 1]) |
| 314 | + |
110 | 315 | def test_emit_lowered_backend_module(self): |
111 | 316 | module_list = [ |
112 | 317 | models.Emformer(), |
|
0 commit comments