Skip to content

Commit 0b0880b

Browse files
JacobSzwejbkafacebook-github-bot
authored andcommitted
Add manifest extension AoT (#14128)
Summary: Pull Request resolved: #14128 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 66639e4 commit 0b0880b

File tree

5 files changed

+566
-0
lines changed

5 files changed

+566
-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 append_manifest, Manifest
8+
9+
__all__ = [
10+
"Manifest",
11+
"append_manifest",
12+
]

extension/manifest/_manifest.py

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
from dataclasses import dataclass
2+
from typing import ClassVar, Literal
3+
4+
from executorch.exir._serialize.padding import padding_required
5+
from executorch.exir._warnings import experimental
6+
7+
# Byte order of numbers written to the manifest. Always little-endian
8+
# regardless of the host system, since all commonly-used modern CPUs are little
9+
# endian.
10+
_MANIFEST_BYTEORDER: Literal["little"] = "little"
11+
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+
20+
EXPECTED_MAGIC: ClassVar[bytes] = b"em00"
21+
22+
MAX_SIGNATURE_SIZE: ClassVar[int] = 512
23+
24+
EXPECTED_MIN_LENGTH: ClassVar[int] = (
25+
# Header magic
26+
4
27+
# Header length
28+
+ 4
29+
# program offset
30+
+ 8
31+
# Padding
32+
+ 4
33+
# signature size
34+
+ 4
35+
)
36+
37+
EXPECTED_MAX_LENGTH: ClassVar[int] = EXPECTED_MIN_LENGTH + MAX_SIGNATURE_SIZE
38+
39+
signature: bytes
40+
41+
# The actual size of the signature
42+
signature_size: int = 0
43+
44+
# The size of any padding required
45+
padding_size: int = 0
46+
47+
# Size in bytes between the top of the manifest and the start of the data it was appended to.
48+
program_offset: int = 0
49+
50+
# The manifest length, in bytes, read from or to be written to the binary
51+
# footer.
52+
length: int = 0
53+
54+
# The magic bytes read from or to be written to the binary footer.
55+
magic: bytes = EXPECTED_MAGIC
56+
57+
def __post_init__(self):
58+
"""Post init hook to validate the manifest."""
59+
if self.signature_size == 0:
60+
self.signature_size = len(self.signature)
61+
if self.length == 0:
62+
self.length = _ManifestLayout.EXPECTED_MIN_LENGTH + self.signature_size
63+
64+
# Not using self.is_valid() here to deliver better error messages.
65+
if len(self.signature) > _ManifestLayout.MAX_SIGNATURE_SIZE:
66+
raise ValueError(
67+
f"Signature is too large. {self.signature_size}. Manifest only supports signatures up to {_ManifestLayout.MAX_SIGNATURE_SIZE} bytes."
68+
)
69+
if self.magic != _ManifestLayout.EXPECTED_MAGIC:
70+
raise ValueError(
71+
f"Invalid magic. Expected {_ManifestLayout.EXPECTED_MAGIC}. Got {self.magic}"
72+
)
73+
if self.length < _ManifestLayout.EXPECTED_MIN_LENGTH:
74+
raise ValueError(
75+
f"Invalid length. Expected at least {_ManifestLayout.EXPECTED_MIN_LENGTH}. Got {self.length}"
76+
)
77+
if self.length > _ManifestLayout.EXPECTED_MAX_LENGTH:
78+
raise ValueError(
79+
f"Invalid length. Expected at most {_ManifestLayout.EXPECTED_MAX_LENGTH}. Got {self.length}"
80+
)
81+
if self.signature_size != len(self.signature):
82+
raise ValueError(
83+
f"Invalid signature size must match len(self.signature). Expected {len(self.signature)}. Got {self.signature_size}"
84+
)
85+
86+
def is_valid(self) -> bool:
87+
"""Returns true if the manifest appears to be well-formed."""
88+
return (
89+
self.magic == _ManifestLayout.EXPECTED_MAGIC
90+
and self.length >= _ManifestLayout.EXPECTED_MIN_LENGTH
91+
and self.length <= _ManifestLayout.EXPECTED_MAX_LENGTH
92+
and self.signature_size >= 0
93+
and self.signature_size <= _ManifestLayout.MAX_SIGNATURE_SIZE
94+
and self.program_offset >= 0
95+
and len(self.signature) == self.signature_size
96+
)
97+
98+
def to_bytes(self) -> bytes:
99+
""""Returns the binary representation of the Manifest. Written bottom up
100+
to allow for BC considerations. The compatibility-preserving way to make
101+
changes is to increase the header's length field and add new fields at
102+
the top. This means we can always check the last 8 bytes for the magic
103+
and size, and then load the full footer."
104+
"""
105+
if not self.is_valid():
106+
raise ValueError("Cannot serialize an invalid manifest")
107+
108+
data: bytes = (
109+
# bytes: Signature unique ID for the data the manifest was appended to.
110+
self.signature
111+
# actual size of the signature
112+
+ self.signature_size.to_bytes(4, byteorder=_MANIFEST_BYTEORDER)
113+
# uint32_t: Any padding required to align the manifest.
114+
+ self.padding_size.to_bytes(4, byteorder=_MANIFEST_BYTEORDER)
115+
# uint64_t: Size in bytes between the manifest and the data it was appended to.
116+
+ self.program_offset.to_bytes(8, byteorder=_MANIFEST_BYTEORDER)
117+
# uint32_t: Actual size of this manifest.
118+
+ self.length.to_bytes(4, byteorder=_MANIFEST_BYTEORDER)
119+
# Manifest magic. This lets consumers detect whether the
120+
# manifest was inserted or not. Always use the proper magic value
121+
# (i.e., ignore self.magic) since there's no reason to create an
122+
# invalid manifest.
123+
+ self.EXPECTED_MAGIC
124+
)
125+
return data
126+
127+
@staticmethod
128+
def from_bytes(data: bytes) -> "_ManifestLayout":
129+
"""Tries to read a manifest from the provided data.
130+
131+
Does not validate that the header is well-formed. Callers should
132+
use is_valid().
133+
134+
Args:
135+
data: The data to read from.
136+
Returns:
137+
The contents of the serialized manifest.
138+
Raises:
139+
ValueError: If not enough data is provided.
140+
"""
141+
if len(data) <= _ManifestLayout.EXPECTED_MIN_LENGTH:
142+
raise ValueError(
143+
f"Not enough data for the manifest: {len(data)} "
144+
+ f"< {_ManifestLayout.EXPECTED_MIN_LENGTH}"
145+
)
146+
magic = data[-4:]
147+
length = int.from_bytes(data[-8:-4], byteorder=_MANIFEST_BYTEORDER)
148+
program_offset = int.from_bytes(data[-16:-8], byteorder=_MANIFEST_BYTEORDER)
149+
padding_size = int.from_bytes(data[-20:-16], byteorder=_MANIFEST_BYTEORDER)
150+
signature_size = int.from_bytes(data[-24:-20], byteorder=_MANIFEST_BYTEORDER)
151+
signature = data[-(signature_size + 24) : -24]
152+
return _ManifestLayout(
153+
signature=signature,
154+
signature_size=signature_size,
155+
padding_size=padding_size,
156+
program_offset=program_offset,
157+
length=length,
158+
magic=magic,
159+
)
160+
161+
@staticmethod
162+
def from_manifest(manifest: "Manifest") -> "_ManifestLayout":
163+
return _ManifestLayout(
164+
signature=manifest.signature,
165+
signature_size=len(manifest.signature),
166+
length=_ManifestLayout.EXPECTED_MIN_LENGTH + len(manifest.signature),
167+
# program_offset and padding_size are set at append time.
168+
)
169+
170+
171+
@experimental("This API is experimental and subject to change without notice.")
172+
@dataclass
173+
class Manifest:
174+
"""A manifest that can be appended to a binary blob. The manifest contains
175+
meta information about the binary blob. You must know who created the manifest
176+
to be able to interpret the data in the manifest."""
177+
178+
# Unique ID for the data the manifest was appended to. Often this might contain
179+
# a crytographic signature for the data.
180+
signature: bytes
181+
182+
@staticmethod
183+
def _from_manifest_layout(layout: _ManifestLayout) -> "Manifest":
184+
return Manifest(
185+
signature=layout.signature,
186+
)
187+
188+
@staticmethod
189+
def from_bytes(data: bytes) -> "Manifest":
190+
"""Tries to read a manifest from the provided data."""
191+
layout = _ManifestLayout.from_bytes(data)
192+
if not layout.is_valid():
193+
raise ValueError("Cannot parse manifest from bytes")
194+
return Manifest._from_manifest_layout(layout)
195+
196+
197+
@experimental("This API is experimental and subject to change without notice.")
198+
def append_manifest(pte_data: bytes, manifest: Manifest, alignment: int = 16):
199+
"""Appends a manifest to the provided data."""
200+
padding = padding_required(len(pte_data), alignment)
201+
202+
manifest_layout = _ManifestLayout.from_manifest(manifest)
203+
manifest_layout.program_offset = len(pte_data) + manifest_layout.padding_size
204+
manifest_layout.padding_size = padding
205+
206+
return pte_data + (b"\x00" * padding) + manifest_layout.to_bytes()

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)