Skip to content

Commit e82daa2

Browse files
JacobSzwejbkafacebook-github-bot
authored andcommitted
Add manifest extension AoT
Summary: Add some infra for us to optionally add some key structured data to the end of a pte. This diff is around enabling users to easily tag their model with a cryptographic signature. Has room to expand later. A key design motivation is it would be ideal if this is transparent to the rest of the extensions we have today. Im claiming the prime footer real estate for this which is unused today by anything in tree. This should let it be composable with other formats like bundledProgram too. Differential Revision: D82052721
1 parent dbac09c commit e82daa2

File tree

5 files changed

+504
-0
lines changed

5 files changed

+504
-0
lines changed

extension/manifest/TARGETS

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
load("@fbcode_macros//build_defs:python_library.bzl", "python_library")
2+
3+
oncall("executorch")
4+
5+
python_library(
6+
name = "_manifest",
7+
srcs = [
8+
"_manifest.py",
9+
],
10+
deps = [
11+
"//executorch/exir/_serialize:lib",
12+
"//executorch/exir:_warnings",
13+
],
14+
visibility = ["PUBLIC"],
15+
)

extension/manifest/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
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+
from executorch.extension.manifest._manifest import Manifest, append_manifest
8+
9+
__all__ = [
10+
"Manifest",
11+
"append_manifest",
12+
]

extension/manifest/_manifest.py

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from dataclasses import dataclass
2+
from typing import ClassVar, Literal
3+
4+
from executorch.exir._serialize._cord import Cord
5+
from executorch.exir._serialize.padding import padding_required
6+
from executorch.exir._warnings import experimental
7+
8+
# Byte order of numbers written to the manifest. Always little-endian
9+
# regardless of the host system, since all commonly-used modern CPUs are little
10+
# endian.
11+
_MANIFEST_BYTEORDER: Literal["little"] = "little"
12+
13+
@dataclass
14+
class _ManifestLayout:
15+
"""Python class mirroring the binary layout of the manifest.
16+
separate from the Manifest class, which is the user facing
17+
representation.
18+
"""
19+
EXPECTED_MAGIC: ClassVar[bytes] = b"em00"
20+
21+
EXPECTED_LENGTH: ClassVar[int] = (
22+
# Header magic
23+
4
24+
# Header length
25+
+ 4
26+
# Padding size
27+
+ 4
28+
# program offset
29+
+ 8
30+
# signature
31+
+ 8
32+
)
33+
34+
signature: int
35+
36+
# Size in bytes between the top of the manifest and the end of the data it was appended to
37+
padding_size: int = 0
38+
39+
# Size in bytes between the top of the manifest and the start of the data it was appended to.
40+
program_offset: int = 0
41+
42+
# The manifest length, in bytes, read from or to be written to the binary
43+
# footer.
44+
length: int = EXPECTED_LENGTH
45+
46+
# The magic bytes read from or to be written to the binary footer.
47+
magic: bytes = EXPECTED_MAGIC
48+
49+
def is_valid(self) -> bool:
50+
"""Returns true if the manifest appears to be well-formed."""
51+
return (
52+
self.magic == _ManifestLayout.EXPECTED_MAGIC
53+
and self.length >= _ManifestLayout.EXPECTED_LENGTH
54+
)
55+
56+
def to_bytes(self) -> bytes:
57+
"""Returns the binary representation of the Manifest. Written
58+
bottom up.
59+
60+
Note that this will ignore self.magic and self.length and will always
61+
write the proper magic/length.
62+
"""
63+
data: bytes = (
64+
65+
# uint64_t: Signature unique ID for the data the manifest was appended to.
66+
self.signature.to_bytes(8, byteorder=_MANIFEST_BYTEORDER)
67+
# uint64_t: Size in bytes between the manifest and the data it was appended to.
68+
+self.program_offset.to_bytes(8, byteorder=_MANIFEST_BYTEORDER)
69+
# uint32_t: Size in bytes between the manifest and the data it was appended to.
70+
+self.padding_size.to_bytes(4, byteorder=_MANIFEST_BYTEORDER)
71+
# uint32_t: Size of this manifest. This makes it easier to add new
72+
# fields to this header in the future. Always use the proper size
73+
# (i.e., ignore self.length) since there's no reason to create an
74+
# invalid footer.
75+
+ self.EXPECTED_LENGTH.to_bytes(4, byteorder=_MANIFEST_BYTEORDER)
76+
# Manifest magic. This lets consumers detect whether the
77+
# manifest was inserted or not. Always use the proper magic value
78+
# (i.e., ignore self.magic) since there's no reason to create an
79+
# invalid manifest.
80+
+ self.EXPECTED_MAGIC
81+
)
82+
return data
83+
84+
@staticmethod
85+
def from_bytes(data: bytes) -> "_ManifestLayout":
86+
"""Tries to read a manifest from the provided data.
87+
88+
Does not validate that the header is well-formed. Callers should
89+
use is_valid().
90+
91+
Args:
92+
data: The data to read from.
93+
Returns:
94+
The contents of the serialized manifest.
95+
Raises:
96+
ValueError: If not enough data is provided.
97+
"""
98+
if len(data) < _ManifestLayout.EXPECTED_LENGTH:
99+
raise ValueError(
100+
f"Not enough data for the manifest: {len(data)} "
101+
+ f"< {_ManifestLayout.EXPECTED_LENGTH}"
102+
)
103+
104+
return _ManifestLayout(
105+
magic=data[-4:],
106+
length=int.from_bytes(data[-8:-4], byteorder=_MANIFEST_BYTEORDER),
107+
padding_size=int.from_bytes(data[-12:-8], byteorder=_MANIFEST_BYTEORDER),
108+
program_offset=int.from_bytes(data[-20:-12], byteorder=_MANIFEST_BYTEORDER),
109+
signature=int.from_bytes(
110+
data[-28:-20], byteorder=_MANIFEST_BYTEORDER
111+
),
112+
)
113+
114+
@staticmethod
115+
def from_manifest(manifest: "Manifest") -> "_ManifestLayout":
116+
return _ManifestLayout(
117+
signature = manifest.signature,
118+
#padding_size will be properly initialized in the append_manifest function
119+
)
120+
121+
@experimental("This API is experimental and subject to change without notice.")
122+
@dataclass
123+
class Manifest:
124+
"""A manifest that can be appended to a binary blob. The manifest contains
125+
meta information about the binary blob. You must know who created the manifest
126+
to be able to interpret the data in the manifest."""
127+
128+
# Unique ID for the data the manifest was appended to. Often this might contain
129+
# a crytographic signature for the data.
130+
signature: int
131+
132+
@staticmethod
133+
def _from_manifest_layout(layout: _ManifestLayout) -> "Manifest":
134+
return Manifest(
135+
signature=layout.signature,
136+
)
137+
138+
@staticmethod
139+
def from_bytes(data: bytes) -> "Manifest":
140+
"""Tries to read a manifest from the provided data."""
141+
layout = _ManifestLayout.from_bytes(data)
142+
if not layout.is_valid():
143+
raise ValueError("Cannot parse manifest from bytes")
144+
return Manifest._from_manifest_layout(layout)
145+
146+
@experimental("This API is experimental and subject to change without notice.")
147+
def append_manifest(pte_data: Cord, manifest: Manifest, alignment:int=16):
148+
"""Appends a manifest to the provided data."""
149+
manifest_layout = _ManifestLayout.from_manifest(manifest)
150+
manifest_layout.padding_size = padding_required(len(pte_data), alignment)
151+
manifest_layout.program_offset = len(pte_data) + manifest_layout.padding_size
152+
pte_data.append((b"\x00" * manifest_layout.padding_size))
153+
pte_data.append(manifest_layout.to_bytes())
154+

extension/manifest/test/TARGETS

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
load("@fbcode_macros//build_defs:python_unittest.bzl", "python_unittest")
2+
3+
oncall("executorch")
4+
5+
python_unittest(
6+
name = "test_manifest",
7+
srcs = [
8+
"test_manifest.py",
9+
],
10+
deps = [
11+
"//executorch/extension/manifest:_manifest",
12+
"//executorch/extension/pybindings:portable_lib",
13+
"//executorch/exir:lib",
14+
"//caffe2:torch",
15+
],
16+
)

0 commit comments

Comments
 (0)