Skip to content

Commit 443ba3b

Browse files
pytorchbotlucylq
andauthored
[executorch][serialization] Data serialization interface (#7487)
* [executorch][serialization] Data serialization interface Pull Request resolved: #7194 Introduce data serialization interface. ghstack-source-id: 260014193 @exported-using-ghexport Differential Revision: [D65947145](https://our.internmc.facebook.com/intern/diff/D65947145/) * [executorch][serialization] Refactor flatbuffer utils into separate file (#7488) Pull Request resolved: #7254 Todo: let xnnpack and vulkan serialization use these utils instead of redefining the same functions. For usage in extension/flat_tensor/serialize. ghstack-source-id: 260036856 @exported-using-ghexport Differential Revision: [D66854756](https://our.internmc.facebook.com/intern/diff/D66854756/) Co-authored-by: lucylq <[email protected]> --------- Co-authored-by: lucylq <[email protected]>
1 parent fab1463 commit 443ba3b

File tree

4 files changed

+140
-39
lines changed

4 files changed

+140
-39
lines changed

exir/_serialize/TARGETS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ runtime.python_library(
3333
"_dataclass.py",
3434
"_flatbuffer.py",
3535
"_program.py",
36+
"padding.py",
3637
],
3738
resources = {
3839
"//executorch/schema:program.fbs": "program.fbs",

exir/_serialize/_program.py

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@
2121
_program_json_to_flatbuffer,
2222
)
2323

24+
from executorch.exir._serialize.padding import aligned_size, pad_to, padding_required
25+
2426
from executorch.exir.schema import (
2527
BackendDelegateDataReference,
2628
BackendDelegateInlineData,
@@ -50,19 +52,6 @@ def _json_to_program(program_json: bytes) -> Program:
5052
return _json_to_dataclass(json.loads(program_json), cls=Program)
5153

5254

53-
def _padding_required(offset: int, alignment: int) -> int:
54-
"""Returns the padding required to align `offset` to `alignment`."""
55-
remainder: int = offset % alignment
56-
if remainder != 0:
57-
return alignment - remainder
58-
return 0
59-
60-
61-
def _aligned_size(input_size: int, alignment: int) -> int:
62-
"""Returns input_size padded up to the next whole multiple of alignment."""
63-
return input_size + _padding_required(input_size, alignment)
64-
65-
6655
def _insert_flatbuffer_header(
6756
flatbuffer_data: bytes, magic_regex: str, header_data: bytes
6857
) -> bytes:
@@ -211,25 +200,6 @@ def to_bytes(self) -> bytes:
211200
return data
212201

213202

214-
def _pad_to(data: bytes, length: int) -> bytes:
215-
"""Returns the input followed by enough zero bytes to become the requested length.
216-
217-
Args:
218-
data: The data to pad.
219-
length: The length of the returned data.
220-
Returns:
221-
The padded data.
222-
Raises:
223-
ValueError: If the requested length is less than the input length.
224-
"""
225-
if length < len(data):
226-
raise ValueError(f"Data length {len(data)} > padded length {length}")
227-
if length > len(data):
228-
data = data + b"\x00" * (length - len(data))
229-
assert len(data) == length
230-
return data
231-
232-
233203
def _get_extended_header(program_data: bytes) -> Optional[_ExtendedHeader]:
234204
"""Returns the extended header of the program data, if present and valid."""
235205
try:
@@ -330,7 +300,7 @@ def _extract_constant_segment(
330300
constant_segment_data.append(buffer.storage)
331301
buffer_length = len(buffer.storage)
332302
pad_length = (
333-
_padding_required(buffer_length, tensor_alignment)
303+
padding_required(buffer_length, tensor_alignment)
334304
if tensor_alignment is not None
335305
else 0
336306
)
@@ -432,11 +402,11 @@ def serialize_pte_binary(
432402
)
433403
program.segments.append(
434404
DataSegment(
435-
offset=_aligned_size(prev_end, segment_alignment), size=len(data)
405+
offset=aligned_size(prev_end, segment_alignment), size=len(data)
436406
)
437407
)
438408
# Add to aggregate segments cord with padding.
439-
padding_length = _padding_required(len(segments_data), segment_alignment)
409+
padding_length = padding_required(len(segments_data), segment_alignment)
440410
if padding_length > 0:
441411
segments_data.append(b"\x00" * padding_length)
442412
segments_data.append(data)
@@ -454,15 +424,15 @@ def serialize_pte_binary(
454424

455425
# Size of the header to insert. Its size is padded to the largest
456426
# force_align value present in the schema.
457-
padded_header_length: int = _aligned_size(
427+
padded_header_length: int = aligned_size(
458428
input_size=_ExtendedHeader.EXPECTED_LENGTH,
459429
alignment=result.max_alignment,
460430
)
461431
# Size of the program with the header inserted.
462432
program_size: int = padded_header_length + len(result.data)
463433
# Offset to the first segment, or zero if there are no segments.
464434
segment_base_offset: int = (
465-
_aligned_size(input_size=program_size, alignment=segment_alignment)
435+
aligned_size(input_size=program_size, alignment=segment_alignment)
466436
if len(segments_data) > 0
467437
else 0
468438
)
@@ -471,7 +441,7 @@ def serialize_pte_binary(
471441
header_data: bytes = _ExtendedHeader(
472442
program_size=program_size, segment_base_offset=segment_base_offset
473443
).to_bytes()
474-
header_data = _pad_to(header_data, padded_header_length)
444+
header_data = pad_to(header_data, padded_header_length)
475445

476446
# Insert the header into the flatbuffer data.
477447
program_data: bytes = _insert_flatbuffer_header(
@@ -496,7 +466,7 @@ def serialize_pte_binary(
496466
# - segments data (optional); aligned to segment_alignment.
497467
pte_data = Cord(program_data)
498468
if len(segments_data) > 0:
499-
padding_length = _padding_required(len(pte_data), segment_alignment)
469+
padding_length = padding_required(len(pte_data), segment_alignment)
500470
pte_data.append(b"\x00" * padding_length)
501471
# The first segment after program data should start at the segment base offset.
502472
assert (

exir/_serialize/data_serializer.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
from abc import ABC, abstractmethod
2+
from dataclasses import dataclass
3+
from typing import Dict, List, Sequence
4+
5+
from executorch.exir._serialize._cord import Cord
6+
7+
from executorch.exir.schema import ScalarType
8+
9+
10+
@dataclass
11+
class TensorLayout:
12+
"""Tensor layout information for externally-serialized tensors.
13+
14+
Attributes:
15+
scalar_type: type of the elements in the tensor.
16+
sizes: size of each dim in the tensor.
17+
dim_order: specifies the order the dimensions are laid out in memory,
18+
from outer to inner.
19+
"""
20+
21+
scalar_type: ScalarType
22+
sizes: List[int]
23+
dim_order: List[int]
24+
25+
26+
@dataclass
27+
class TensorEntry:
28+
"""Represents a single tensor in `DataPayload`, specifying its location
29+
and metadata.
30+
31+
Attributes:
32+
buffer_index: The index inside `DataPayload.buffers` that this
33+
TensorEntry refers to.
34+
layout: Metadata about the tensor.
35+
"""
36+
37+
buffer_index: int
38+
layout: TensorLayout
39+
40+
41+
@dataclass
42+
class DataPayload:
43+
"""Contains the data and metadata required for serialization.
44+
45+
Having an index-based arrangement instead of embedding the buffers in
46+
TensorEntry allows the caller to deduplicate buffers and point multiple
47+
fully qualified names (FQNs) to the same entry.
48+
49+
Attributes:
50+
buffers: a sequence of tensor buffers.
51+
fqn_to_tensor: a map from fully qualified names to serializable tensors.
52+
"""
53+
54+
buffers: Sequence[bytes]
55+
fqn_to_tensor: Dict[str, TensorEntry]
56+
57+
58+
class DataSerializer(ABC):
59+
"""Serializes and deserializes FQN-tagged tensor data.
60+
61+
This base class enables serialization into different formats. See
62+
executorch/extension/flat_tensor/ for an example.
63+
"""
64+
65+
@abstractmethod
66+
def serialize(
67+
self,
68+
data: DataPayload,
69+
) -> Cord:
70+
"""
71+
Serializes a list of tensors emitted by ExecuTorch into a binary blob.
72+
73+
Args:
74+
data: the tensor buffers and tensor layout information required for
75+
serialization.
76+
77+
Returns:
78+
A binary blob that contains the serialized data.
79+
"""
80+
raise NotImplementedError("serialize_data")
81+
82+
@abstractmethod
83+
def deserialize(self, blob: Cord) -> DataPayload:
84+
"""
85+
Deserializes a blob into a list of tensors. Reverses the effect of
86+
serialize.
87+
88+
Args:
89+
blob: A binary blob that contains the serialized data.
90+
91+
Returns:
92+
DataPayload: tensor buffers and tensor layout information
93+
deserialized from `blob`.
94+
"""
95+
raise NotImplementedError("deserialize_data")

exir/_serialize/padding.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# (c) Meta Platforms, Inc. and affiliates. Confidential and proprietary.
2+
3+
# pyre-strict
4+
5+
6+
def pad_to(data: bytes, length: int) -> bytes:
7+
"""Returns the input followed by enough zero bytes to become the requested length.
8+
9+
Args:
10+
data: The data to pad.
11+
length: The length of the returned data.
12+
Returns:
13+
The padded data.
14+
Raises:
15+
ValueError: If the requested length is less than the input length.
16+
"""
17+
if length < len(data):
18+
raise ValueError(f"Data length {len(data)} > padded length {length}")
19+
if length > len(data):
20+
data = data + b"\x00" * (length - len(data))
21+
assert len(data) == length
22+
return data
23+
24+
25+
def padding_required(offset: int, alignment: int) -> int:
26+
"""Returns the padding required to align `offset` to `alignment`."""
27+
remainder: int = offset % alignment
28+
if remainder != 0:
29+
return alignment - remainder
30+
return 0
31+
32+
33+
def aligned_size(input_size: int, alignment: int) -> int:
34+
"""Returns input_size padded up to the next whole multiple of alignment."""
35+
return input_size + padding_required(input_size, alignment)

0 commit comments

Comments
 (0)