Skip to content

Commit 16f088e

Browse files
mcr229facebook-github-bot
authored andcommitted
Serialize constant Data outside of flatbuffer (#1542)
Summary: We introduce the `serialize_xnnpack_binary` method which serializees the constant data outside of the flatbuffer. It leverages the xnnheader introduced in the previous diff to store offsets and sizes for both the flatbuffer payload as well as the constant data payload. Note here we have not yet switched the delegate to use the new `serialize_xnnpack_binary` function as this new serialization also requires changes on the runtime side. This will be tested in the diff which follows. Differential Revision: D52498367
1 parent 694e067 commit 16f088e

File tree

5 files changed

+262
-3
lines changed

5 files changed

+262
-3
lines changed

backends/xnnpack/serialization/schema.fbs

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,17 @@ table XNNLeakyReLU {
281281
flags: uint;
282282
}
283283

284+
// Describes data offsets for constant data
285+
table ConstantDataOffset {
286+
// Constant data offsets are relative to the constant data base offset provided
287+
// in the XNNPACKHeader.
288+
offset: uint64;
289+
290+
// The size in bytes of valid data starting at the offset. The constant data
291+
// may be followed by padding before the next piece of constant data
292+
size: uint64;
293+
}
294+
284295
table XNNGraph {
285296
// Schema version.
286297
version:string;
@@ -299,11 +310,16 @@ table XNNGraph {
299310
// Tables of constant data, used for constant Values (e.g.
300311
// data field of weight tensors). Each constant is assigned an index into the table
301312
// which are each individually aligned. 0 index is reserved to be pointed to by non-constant
302-
// Tensors
313+
// Tensors. Exactly one of constant_buffer and constant_data must be non-empty
303314
constant_buffer:[Buffer];
304315

305316
// the list index is memory buffer id, the value is the memory buffer size.
306317
mem_buffer_sizes: [uint];
318+
319+
// List of the constant data that follows the XNNGraph in this file. Each constant data is assigned an index into
320+
// the table. 0 index is reserved to be pointed to by non-constant Tensor. Exactly one of constant_buffer and
321+
// constant_data must be non-empty
322+
constant_data:[ConstantDataOffset];
307323
}
308324

309325
root_type XNNGraph;

backends/xnnpack/serialization/xnnpack_graph_schema.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -417,6 +417,12 @@ class Buffer:
417417
storage: bytes
418418

419419

420+
@dataclass
421+
class ConstantDataOffset:
422+
offset: int
423+
size: int
424+
425+
420426
@dataclass
421427
class XNNGraph:
422428
version: str
@@ -429,3 +435,5 @@ class XNNGraph:
429435

430436
constant_buffer: List[Buffer]
431437
mem_buffer_sizes: List[int]
438+
439+
constant_data: List[ConstantDataOffset]

backends/xnnpack/serialization/xnnpack_graph_serialize.py

Lines changed: 138 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@
99
import tempfile
1010

1111
from dataclasses import dataclass, fields, is_dataclass
12-
from typing import ClassVar, Literal
12+
from typing import ClassVar, List, Literal, Tuple
1313

1414
import pkg_resources
15-
from executorch.backends.xnnpack.serialization.xnnpack_graph_schema import XNNGraph
15+
from executorch.backends.xnnpack.serialization.xnnpack_graph_schema import (
16+
Buffer,
17+
ConstantDataOffset,
18+
XNNGraph,
19+
)
1620
from executorch.exir._serialize._dataclass import _DataclassEncoder
1721

1822
from executorch.exir._serialize._flatbuffer import _flatc_compile
@@ -236,6 +240,74 @@ def to_bytes(self) -> bytes:
236240
return data
237241

238242

243+
def _padding_required(offset: int, alignment: int) -> int:
244+
"""Returns the padding required to align `offset` to `alignment`."""
245+
remainder: int = offset % alignment
246+
if remainder != 0:
247+
return alignment - remainder
248+
return 0
249+
250+
251+
def _aligned_size(input_size: int, alignment: int) -> int:
252+
"""Returns input_size padded up to the next whole multiple of alignment."""
253+
aligned_size = input_size + _padding_required(input_size, alignment)
254+
assert aligned_size % alignment == 0
255+
return aligned_size
256+
257+
258+
def _pad_to(data: bytes, length: int) -> bytes:
259+
"""Returns the input followed by enough zero bytes to become the requested length.
260+
261+
Args:
262+
data: The data to pad.
263+
length: The length of the returned data.
264+
Returns:
265+
The padded data.
266+
Raises:
267+
ValueError: If the requested length is less than the input length.
268+
"""
269+
if length < len(data):
270+
raise ValueError(f"Data length {len(data)} > padded length {length}")
271+
if length > len(data):
272+
data = data + b"\x00" * (length - len(data))
273+
assert len(data) == length
274+
return data
275+
276+
277+
def _extract_constant_data(
278+
constant_buffer: List[Buffer],
279+
tensor_alignment: int = 16,
280+
) -> Tuple[bytes, List[int]]:
281+
"""Copies the tensors from the provided list into a single buffer and tracks the offsets
282+
of each tensor.
283+
284+
constant_buffer: list of Buffers from which to extract constants from. Not modified.
285+
tensor_alignment: Alignment in bytes. The starting offset of each tensor in the
286+
constant segment will be aligned to this value. Default to 16.
287+
288+
Returns:
289+
A tuple of (constant segment, list of offsets for each tensor in the segment)
290+
"""
291+
constant_segment_data: bytearray = bytearray()
292+
constant_segment_offsets: List[int] = []
293+
current_offset: int = 0
294+
for i in range(len(constant_buffer)):
295+
buffer = constant_buffer[i]
296+
buffer_length = len(buffer.storage)
297+
pad_length = _padding_required(buffer_length, tensor_alignment)
298+
299+
# Append each constant buffer to the constant segment.
300+
constant_segment_data += buffer.storage
301+
# Add padding for all but the last tensor.
302+
if i < len(constant_buffer) - 1:
303+
constant_segment_data += b"\x00" * pad_length
304+
305+
# Append constant data offset.
306+
constant_segment_offsets.append(current_offset)
307+
current_offset += buffer_length + pad_length
308+
return bytes(constant_segment_data), constant_segment_offsets
309+
310+
239311
def convert_to_flatbuffer(xnnpack_graph: XNNGraph) -> bytes:
240312
sanity_check_xnngraph_dataclass(xnnpack_graph)
241313
xnnpack_graph_json = json.dumps(xnnpack_graph, cls=_DataclassEncoder)
@@ -251,3 +323,67 @@ def convert_to_flatbuffer(xnnpack_graph: XNNGraph) -> bytes:
251323
output_path = os.path.join(d, "schema.bin")
252324
with open(output_path, "rb") as output_file:
253325
return output_file.read()
326+
327+
328+
def serialize_xnnpack_binary(xnnpack_graph: XNNGraph) -> bytes:
329+
"""Returns the runtime binary representation of the given XNNGraph.
330+
331+
Args:
332+
xnnpack_graph: XNNGraph object to serialize.
333+
334+
Returns:
335+
The serialized form of the XNNGraph, ready for execution by XNNPACK Backend
336+
"""
337+
constant_tensor_alignment = 16
338+
339+
# Extract constant data from the graph
340+
constant_data, constant_data_offsets = _extract_constant_data(
341+
xnnpack_graph.constant_buffer, constant_tensor_alignment
342+
)
343+
344+
assert len(constant_data_offsets) == len(xnnpack_graph.mem_buffer_sizes)
345+
346+
for offset_idx in range(len(constant_data_offsets)):
347+
constant_data_offset = constant_data_offsets[offset_idx]
348+
constant_data_size = xnnpack_graph.mem_buffer_sizes[offset_idx]
349+
xnnpack_graph.constant_data.append(
350+
ConstantDataOffset(constant_data_offset, constant_data_size)
351+
)
352+
353+
# We are moving all constant data from the graph to the constant data section.
354+
# So we remove all constant buffers
355+
xnnpack_graph.constant_buffer = []
356+
xnnpack_graph.mem_buffer_sizes = []
357+
358+
# Convert the XNNGraph to a flatbuffer
359+
flatbuffer_payload = convert_to_flatbuffer(xnnpack_graph)
360+
361+
# size of flatbuffer data, padded to be `constant_tensor_alignment` byte aligned
362+
padded_flatbuffer_length: int = _aligned_size(
363+
input_size=len(flatbuffer_payload),
364+
alignment=constant_tensor_alignment,
365+
)
366+
# size of header to insert, padded to be `constant_tensor_alignment` byte aligned
367+
padded_header_length: int = _aligned_size(
368+
input_size=XNNHeader.EXPECTED_LENGTH,
369+
alignment=constant_tensor_alignment,
370+
)
371+
372+
# Create the XNNPACK Header
373+
header: bytes = XNNHeader(
374+
flatbuffer_offset=padded_header_length,
375+
flatbuffer_size=len(flatbuffer_payload),
376+
constant_data_offset=padded_header_length + padded_flatbuffer_length,
377+
constant_data_size=len(constant_data),
378+
).to_bytes()
379+
380+
# Concatenate the header, flatbuffer data, and constant data
381+
# Constant data does not need to be padded to alignment because nothing follows it
382+
383+
return b"".join(
384+
[
385+
_pad_to(header, padded_header_length),
386+
_pad_to(flatbuffer_payload, padded_flatbuffer_length),
387+
constant_data,
388+
]
389+
)
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
import os
8+
import random
9+
import unittest
10+
from typing import List, Tuple
11+
12+
from executorch.backends.xnnpack.serialization.xnnpack_graph_schema import (
13+
Buffer,
14+
XNNGraph,
15+
)
16+
17+
from executorch.backends.xnnpack.serialization.xnnpack_graph_serialize import (
18+
_HEADER_BYTEORDER,
19+
serialize_xnnpack_binary,
20+
XNNHeader,
21+
)
22+
23+
24+
class TestSerialization(unittest.TestCase):
25+
def _generate_random_const_buffers(
26+
self, num_tensors: int
27+
) -> Tuple[List[Buffer], List[int]]:
28+
"""
29+
Helper function to generate `num_tensor` buffers of random sizes and random contents,
30+
we return a tuple of (list_of_buffers, list_of_mem_sizes),
31+
"""
32+
buffers = []
33+
mem_sizes = []
34+
for _ in range(num_tensors):
35+
buffer_size = random.randint(1, 1000)
36+
buffer = bytearray(os.urandom(buffer_size))
37+
buffers.append(Buffer(storage=bytes(buffer)))
38+
mem_sizes.append(buffer_size)
39+
40+
return buffers, mem_sizes
41+
42+
def test_serialize_xnnpack_binary(self):
43+
xnn_graph = XNNGraph(
44+
version="0",
45+
xnodes=[],
46+
xvalues=[],
47+
num_externs=0,
48+
input_ids=[],
49+
output_ids=[],
50+
constant_buffer=[Buffer(storage=b"")],
51+
mem_buffer_sizes=[0],
52+
constant_data=[],
53+
)
54+
buffers, sizes = self._generate_random_const_buffers(5)
55+
xnn_graph.constant_buffer.extend(buffers)
56+
xnn_graph.mem_buffer_sizes.extend(sizes)
57+
buffers = xnn_graph.constant_buffer
58+
59+
serialized_binary = serialize_xnnpack_binary(xnn_graph)
60+
offsets = xnn_graph.constant_data
61+
62+
# Check header
63+
self.assertEqual(serialized_binary[0:4], b"\x00\x00\x00\x00")
64+
self.assertEqual(serialized_binary[XNNHeader.MAGIC_OFFSET], b"XH00")
65+
flatbuffer_offset_bytes = serialized_binary[XNNHeader.FLATBUFFER_OFFSET_OFFSET]
66+
constant_data_offset_bytes = serialized_binary[
67+
XNNHeader.CONSTANT_DATA_OFFSET_OFFSET
68+
]
69+
70+
# Check flatbuffer is at flatbuffer offset
71+
flatbuffer_offset = int.from_bytes(
72+
flatbuffer_offset_bytes, byteorder=_HEADER_BYTEORDER
73+
)
74+
# Flatbuffer magic should be in the same spot as the Header's magic
75+
self.assertEqual(
76+
serialized_binary[flatbuffer_offset:][XNNHeader.MAGIC_OFFSET], b"XN00"
77+
)
78+
79+
# Check constant data
80+
# Check that constant buffers have been moved to constant data
81+
self.assertEqual(len(offsets), len(buffers))
82+
self.assertEqual(len(xnn_graph.constant_buffer), 0)
83+
84+
constant_data_offset = int.from_bytes(
85+
constant_data_offset_bytes, byteorder=_HEADER_BYTEORDER
86+
)
87+
constant_data_payload = serialized_binary[constant_data_offset:]
88+
89+
# We check that constant data indexes stored in the xnn_graph correctly index
90+
# into the correct buffer in the constant data section
91+
for idx in range(1, len(offsets)):
92+
offset = offsets[idx].offset
93+
size = offsets[idx].size
94+
95+
constant_data_bytes = constant_data_payload[offset : offset + size]
96+
constant_buffer_bytes = buffers[idx].storage
97+
98+
self.assertEqual(constant_data_bytes, constant_buffer_bytes)

backends/xnnpack/xnnpack_preprocess.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ def preprocess(
231231
output_ids=[],
232232
constant_buffer=[Buffer(storage=b"")],
233233
mem_buffer_sizes=[0],
234+
constant_data=[],
234235
)
235236

236237
node_visitors = get_node_visitors(ep, node_to_external_map)

0 commit comments

Comments
 (0)