Skip to content

Commit 91a918e

Browse files
authored
Save a lowered program whose named data is file-backed (pytorch#23693)
## Problem A backend can give ExecuTorch its named data as `FileBackedData` instead of `bytes`. The data then waits in a file on disk until the `.pte` or `.ptd` is written, instead of sitting in memory a second time. The CUDA backend stores its weights this way, and `NamedDataStore`, `to_executorch()` and `write_tensor_data_to_file()` all handle it. Saving such a lowered program with `exir.save()` fails, though: ``` TypeError: Object of type FileBackedData is not JSON serializable ``` The serializer writes the lowered module's named data as JSON, and its encoder only knows `bytes`. ## Change The encoder for named data now reads a file-backed buffer into `bytes` when it reaches it, then encodes it like any other buffer. The lowered module keeps its file-backed buffers, and the saved file is the same as if the backend had given `bytes`. Loading did not need a change: it already decodes every buffer back to `bytes`. What this costs: like any buffer in this JSON format, the data is held in memory while it is encoded, about four times the size of all named data, the same as for `bytes`. The named data of a lowered module is written once for each call to it, so a store shared by several lowered modules is written several times, and each one loads back as its own copy. That was already so for `bytes`, and changing it needs a format change. ## Test plan A new test in `exir/tests/test_serde.py` lowers a small graph with the named-data test backend, which gives it two buffers, and makes one of them a `FileBackedData` that owns its file, as the CUDA backend's are. It checks that the saved program is the same as when both buffers are `bytes`, that the module and its file still hold the data after saving, and that the program loads back with both buffers in order. It fails without this change with the error above, and passes with it. Ran `exir/tests/test_serde.py` on Linux aarch64 with Python 3.12, using this branch's `exir` package and schema over an installed nightly: 16 passed with the change, and 15 passed on `main` (all tests except the new one). The new test also fails on versions of the change that close the file after reading it, convert only the first buffer, write the converted copy back into the module, or use another base64 alphabet.
1 parent 69eeed2 commit 91a918e

3 files changed

Lines changed: 55 additions & 1 deletion

File tree

‎exir/serde/serialize.py‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import torch
2323
import torch.export.exported_program as ep
2424
from executorch.exir import delegate
25+
from executorch.exir._serialize._cord import FileBackedData
2526
from executorch.exir._serialize._named_data_store import NamedDataStoreOutput
2627
from executorch.exir.backend.backend_details import DelegateScratchSpec
2728
from executorch.exir.backend.compile_spec_schema import (
@@ -48,6 +49,14 @@
4849
log: logging.Logger = logging.getLogger(__name__)
4950

5051

52+
class _NamedDataEncoder(export_serialize.EnumEncoder):
53+
# Read each file only when it is encoded, so its raw bytes are freed right after.
54+
def default(self, obj):
55+
if isinstance(obj, FileBackedData):
56+
obj = obj.to_bytes()
57+
return super().default(obj)
58+
59+
5160
class GraphModuleSerializer(export_serialize.GraphModuleSerializer):
5261
def __init__(
5362
self,
@@ -291,7 +300,7 @@ def serialize_bytes(b: bytes) -> str:
291300
assert isinstance(serialized_artifact.exported_program, schema.ExportedProgram)
292301

293302
serialized_processed_bytes = serialize_bytes(lowered_module.processed_bytes)
294-
named_data_store = json.dumps(export_serialize._dataclass_to_dict(lowered_module.named_data_store_output),cls=export_serialize.EnumEncoder) if lowered_module.named_data_store_output else None
303+
named_data_store = json.dumps(export_serialize._dataclass_to_dict(lowered_module.named_data_store_output),cls=_NamedDataEncoder) if lowered_module.named_data_store_output else None
295304

296305
serialized_lowered_module = SerdeLoweredBackendModule(
297306
original_module=serialized_artifact.exported_program,

‎exir/tests/targets.bzl‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ def define_common_targets(is_fbcode = False):
8080
"//executorch/exir:lib",
8181
"//executorch/exir/backend:backend_api",
8282
"//executorch/exir/backend/test:backend_with_compiler_demo",
83+
"//executorch/exir/backend/test:backend_with_named_data_map",
8384
"//executorch/exir/backend/test:op_partitioner_demo",
8485
"//executorch/exir/serde:serialize",
8586
"//executorch/backends/xnnpack/partition:xnnpack_partitioner",

‎exir/tests/test_serde.py‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
# pyre-strict
88

99
import io
10+
import os
1011
import tempfile
1112
import unittest
1213
from contextlib import contextmanager
@@ -19,12 +20,17 @@
1920
XnnpackFloatingPointPartitioner,
2021
)
2122
from executorch.exir import to_edge
23+
from executorch.exir._serialize._cord import FileBackedData
2224
from executorch.exir.backend.backend_api import CompileSpec, to_backend
2325
from executorch.exir.backend.test.backend_with_compiler_demo import (
2426
BackendWithCompilerDemo,
2527
)
28+
from executorch.exir.backend.test.backend_with_named_data_map import (
29+
BackendWithNDMPartitioner,
30+
)
2631

2732
from executorch.exir.backend.test.op_partitioner_demo import AddMulPartitionerDemo
33+
from executorch.exir.lowered_backend_module import get_lowered_submodules
2834
from executorch.exir.program._program import (
2935
EdgeProgramManager,
3036
to_edge_transform_and_lower,
@@ -246,6 +252,44 @@ def forward(self, a, x, b):
246252
edge_new = deserialize(serialize(edge.exported_program()))
247253
self.check_ep(edge.exported_program(), edge_new, inputs)
248254

255+
def test_delegate_with_file_backed_named_data(self) -> None:
256+
class Model(nn.Module):
257+
def forward(self, x):
258+
return (x + x) - x
259+
260+
edge = to_edge(export(Model(), (torch.randn(1, 2),), strict=True)).to_backend(
261+
BackendWithNDMPartitioner()
262+
)
263+
graph_module = edge.exported_program().graph_module
264+
(_, lowered, _) = get_lowered_submodules(graph_module)[0]
265+
output = lowered.named_data_store_output
266+
assert output is not None
267+
payload = bytes(range(256))
268+
output.buffers[1] = payload
269+
other = output.buffers[0]
270+
expected = serialize(edge.exported_program()).exported_program
271+
with tempfile.TemporaryDirectory() as directory:
272+
path = os.path.join(directory, "data.bin")
273+
with open(path, "wb") as f:
274+
f.write(payload)
275+
data = FileBackedData.move_from(path)
276+
output.buffers[1] = data
277+
278+
serialized = serialize(edge.exported_program()).exported_program
279+
self.assertEqual(serialized, expected)
280+
buffer = io.BytesIO()
281+
exir.save(edge.exported_program(), buffer)
282+
self.assertIs(lowered.named_data_store_output, output)
283+
self.assertIs(output.buffers[1], data)
284+
self.assertEqual(data.to_bytes(), payload)
285+
286+
buffer.seek(0)
287+
loaded = exir.load(buffer)
288+
(_, loaded_lowered, _) = get_lowered_submodules(loaded.graph_module)[0]
289+
loaded_output = loaded_lowered.named_data_store_output
290+
assert loaded_output is not None
291+
self.assertEqual(loaded_output.buffers, [other, payload])
292+
249293
def test_delegate_xnnpack(self) -> None:
250294
class SimpleConv1DModel(nn.Module):
251295
def __init__(self):

0 commit comments

Comments
 (0)