Skip to content

Commit 71de34d

Browse files
Arm backend: Add running manifest to track public API (#18528)
Add a manifest file that tracks the state of the public API. This will acts as a current snapshot of what the public API looks like. This manifest file can then be used in future scripts and tests to make sure that a commit does not break the API without proper deprecation. For each release of Executorch there will be a static manifest file generated from the "running" manifest. Those manifest files will be immutable. Signed-off-by: Sebastian Larsson <sebastian.larsson@arm.com>
1 parent d31d4be commit 71de34d

4 files changed

Lines changed: 461 additions & 0 deletions

File tree

backends/arm/__init__.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
"""Public entry points for the Arm backend.
6+
7+
Public API is defined by explicit module exports (e.g., ``.vgf``, ``.ethosu``,
8+
``.quantizer``). Selected symbols are re-exported here for convenience.
9+
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import importlib
15+
from typing import Any
16+
17+
# Public for tooling (manifest generation and API validation).
18+
LAZY_IMPORTS = {
19+
"EthosUBackend": ("executorch.backends.arm.ethosu", "EthosUBackend"),
20+
"EthosUCompileSpec": ("executorch.backends.arm.ethosu", "EthosUCompileSpec"),
21+
"EthosUPartitioner": ("executorch.backends.arm.ethosu", "EthosUPartitioner"),
22+
"VgfBackend": ("executorch.backends.arm.vgf", "VgfBackend"),
23+
"VgfCompileSpec": ("executorch.backends.arm.vgf", "VgfCompileSpec"),
24+
"VgfPartitioner": ("executorch.backends.arm.vgf", "VgfPartitioner"),
25+
"EthosUQuantizer": ("executorch.backends.arm.quantizer", "EthosUQuantizer"),
26+
"VgfQuantizer": ("executorch.backends.arm.quantizer", "VgfQuantizer"),
27+
("get_symmetric_quantization_config"): (
28+
"executorch.backends.arm.quantizer",
29+
"get_symmetric_quantization_config",
30+
),
31+
("get_symmetric_a16w8_quantization_config"): (
32+
"executorch.backends.arm.quantizer",
33+
"get_symmetric_a16w8_quantization_config",
34+
),
35+
}
36+
37+
38+
def __getattr__(name: str) -> Any:
39+
if name in LAZY_IMPORTS:
40+
module_name, attr = LAZY_IMPORTS[name]
41+
module = importlib.import_module(module_name)
42+
value = getattr(module, attr)
43+
globals()[name] = value
44+
return value
45+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
46+
47+
48+
def __dir__() -> list[str]:
49+
return sorted(list(globals()) + list(LAZY_IMPORTS))
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
#
6+
# This file is generated by
7+
# backends/arm/scripts/generate_public_api_manifest.py
8+
9+
[python]
10+
11+
[python.EthosUBackend]
12+
kind = "class"
13+
signature = "EthosUBackend()"
14+
15+
[python.EthosUBackend.preprocess]
16+
kind = "function"
17+
signature = "EthosUBackend.preprocess(edge_program: torch.export.exported_program.ExportedProgram, compile_specs: List[executorch.exir.backend.compile_spec_schema.CompileSpec]) -> executorch.exir.backend.backend_details.PreprocessResult"
18+
19+
[python.EthosUCompileSpec]
20+
kind = "class"
21+
signature = "EthosUCompileSpec(target: str, system_config: str | None = None, memory_mode: str | None = None, extra_flags: list[str] | None = None, config_ini: str | None = 'Arm/vela.ini')"
22+
23+
[python.EthosUCompileSpec.DebugMode]
24+
kind = "enum"
25+
signature = "EthosUCompileSpec.DebugMode(value, names=None, *, module=None, qualname=None, type=None, start=1)"
26+
27+
[python.EthosUCompileSpec.__eq__]
28+
kind = "function"
29+
signature = "EthosUCompileSpec.__eq__(self, other)"
30+
31+
[python.EthosUCompileSpec.__repr__]
32+
kind = "function"
33+
signature = "EthosUCompileSpec.__repr__(self)"
34+
35+
[python.EthosUCompileSpec.dump_debug_info]
36+
kind = "function"
37+
signature = "EthosUCompileSpec.dump_debug_info(self, debug_mode: executorch.backends.arm.common.arm_compile_spec.ArmCompileSpec.DebugMode | None)"
38+
39+
[python.EthosUCompileSpec.dump_intermediate_artifacts_to]
40+
kind = "function"
41+
signature = "EthosUCompileSpec.dump_intermediate_artifacts_to(self, output_path: str | None)"
42+
43+
[python.EthosUCompileSpec.set_pass_pipeline_config]
44+
kind = "function"
45+
signature = "EthosUCompileSpec.set_pass_pipeline_config(self, config: executorch.backends.arm.common.pipeline_config.ArmPassPipelineConfig) -> None"
46+
47+
[python.EthosUPartitioner]
48+
kind = "class"
49+
signature = "EthosUPartitioner(compile_spec: executorch.backends.arm.ethosu.compile_spec.EthosUCompileSpec, additional_checks: Optional[Sequence[torch.fx.passes.operator_support.OperatorSupportBase]] = None) -> None"
50+
51+
[python.EthosUPartitioner.ops_to_not_decompose]
52+
kind = "function"
53+
signature = "EthosUPartitioner.ops_to_not_decompose(self, ep: torch.export.exported_program.ExportedProgram) -> Tuple[List[torch._ops.OpOverload], Optional[Callable[[torch.fx.node.Node], bool]]]"
54+
55+
[python.EthosUPartitioner.partition]
56+
kind = "function"
57+
signature = "EthosUPartitioner.partition(self, exported_program: torch.export.exported_program.ExportedProgram) -> executorch.exir.backend.partitioner.PartitionResult"
58+
59+
[python.EthosUQuantizer]
60+
kind = "class"
61+
signature = "EthosUQuantizer(compile_spec: 'EthosUCompileSpec', use_composable_quantizer: 'bool' = False) -> 'None'"
62+
63+
[python.EthosUQuantizer.annotate]
64+
kind = "function"
65+
signature = "EthosUQuantizer.annotate(self, model: 'GraphModule') -> 'GraphModule'"
66+
67+
[python.EthosUQuantizer.set_global]
68+
kind = "function"
69+
signature = "EthosUQuantizer.set_global(self, quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'"
70+
71+
[python.EthosUQuantizer.set_io]
72+
kind = "function"
73+
signature = "EthosUQuantizer.set_io(self, quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'"
74+
75+
[python.EthosUQuantizer.set_module_name]
76+
kind = "function"
77+
signature = "EthosUQuantizer.set_module_name(self, module_name: 'str', quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'"
78+
79+
[python.EthosUQuantizer.set_module_type]
80+
kind = "function"
81+
signature = "EthosUQuantizer.set_module_type(self, module_type: 'Callable', quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'"
82+
83+
[python.EthosUQuantizer.transform_for_annotation]
84+
kind = "function"
85+
signature = "EthosUQuantizer.transform_for_annotation(self, model: 'GraphModule') -> 'GraphModule'"
86+
87+
[python.EthosUQuantizer.validate]
88+
kind = "function"
89+
signature = "EthosUQuantizer.validate(self, model: 'GraphModule') -> 'None'"
90+
91+
[python.VgfBackend]
92+
kind = "class"
93+
signature = "VgfBackend()"
94+
95+
[python.VgfBackend.preprocess]
96+
kind = "function"
97+
signature = "VgfBackend.preprocess(edge_program: torch.export.exported_program.ExportedProgram, compile_specs: List[executorch.exir.backend.compile_spec_schema.CompileSpec]) -> executorch.exir.backend.backend_details.PreprocessResult"
98+
99+
[python.VgfCompileSpec]
100+
kind = "class"
101+
signature = "VgfCompileSpec(tosa_spec: executorch.backends.arm.tosa.specification.TosaSpecification | str | None = None, compiler_flags: list[str] | None = None)"
102+
103+
[python.VgfCompileSpec.DebugMode]
104+
kind = "enum"
105+
signature = "VgfCompileSpec.DebugMode(value, names=None, *, module=None, qualname=None, type=None, start=1)"
106+
107+
[python.VgfCompileSpec.__eq__]
108+
kind = "function"
109+
signature = "VgfCompileSpec.__eq__(self, other)"
110+
111+
[python.VgfCompileSpec.__repr__]
112+
kind = "function"
113+
signature = "VgfCompileSpec.__repr__(self)"
114+
115+
[python.VgfCompileSpec.dump_debug_info]
116+
kind = "function"
117+
signature = "VgfCompileSpec.dump_debug_info(self, debug_mode: executorch.backends.arm.common.arm_compile_spec.ArmCompileSpec.DebugMode | None)"
118+
119+
[python.VgfCompileSpec.dump_intermediate_artifacts_to]
120+
kind = "function"
121+
signature = "VgfCompileSpec.dump_intermediate_artifacts_to(self, output_path: str | None)"
122+
123+
[python.VgfCompileSpec.set_pass_pipeline_config]
124+
kind = "function"
125+
signature = "VgfCompileSpec.set_pass_pipeline_config(self, config: executorch.backends.arm.common.pipeline_config.ArmPassPipelineConfig) -> None"
126+
127+
[python.VgfPartitioner]
128+
kind = "class"
129+
signature = "VgfPartitioner(compile_spec: executorch.backends.arm.vgf.compile_spec.VgfCompileSpec, additional_checks: Optional[Sequence[torch.fx.passes.operator_support.OperatorSupportBase]] = None) -> None"
130+
131+
[python.VgfPartitioner.ops_to_not_decompose]
132+
kind = "function"
133+
signature = "VgfPartitioner.ops_to_not_decompose(self, ep: torch.export.exported_program.ExportedProgram) -> Tuple[List[torch._ops.OpOverload], Optional[Callable[[torch.fx.node.Node], bool]]]"
134+
135+
[python.VgfPartitioner.partition]
136+
kind = "function"
137+
signature = "VgfPartitioner.partition(self, exported_program: torch.export.exported_program.ExportedProgram) -> executorch.exir.backend.partitioner.PartitionResult"
138+
139+
[python.VgfQuantizer]
140+
kind = "class"
141+
signature = "VgfQuantizer(compile_spec: 'VgfCompileSpec', use_composable_quantizer: 'bool' = False) -> 'None'"
142+
143+
[python.VgfQuantizer.annotate]
144+
kind = "function"
145+
signature = "VgfQuantizer.annotate(self, model: 'GraphModule') -> 'GraphModule'"
146+
147+
[python.VgfQuantizer.set_global]
148+
kind = "function"
149+
signature = "VgfQuantizer.set_global(self, quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'"
150+
151+
[python.VgfQuantizer.set_io]
152+
kind = "function"
153+
signature = "VgfQuantizer.set_io(self, quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'"
154+
155+
[python.VgfQuantizer.set_module_name]
156+
kind = "function"
157+
signature = "VgfQuantizer.set_module_name(self, module_name: 'str', quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'"
158+
159+
[python.VgfQuantizer.set_module_type]
160+
kind = "function"
161+
signature = "VgfQuantizer.set_module_type(self, module_type: 'Callable', quantization_config: 'Optional[QuantizationConfig]') -> 'TOSAQuantizer'"
162+
163+
[python.VgfQuantizer.transform_for_annotation]
164+
kind = "function"
165+
signature = "VgfQuantizer.transform_for_annotation(self, model: 'GraphModule') -> 'GraphModule'"
166+
167+
[python.VgfQuantizer.validate]
168+
kind = "function"
169+
signature = "VgfQuantizer.validate(self, model: 'GraphModule') -> 'None'"
170+
171+
[python.get_symmetric_a16w8_quantization_config]
172+
kind = "function"
173+
signature = "get_symmetric_a16w8_quantization_config(is_per_channel: 'bool' = True, is_qat: 'bool' = False, is_dynamic: 'bool' = False, weight_qmin: 'int' = -127, weight_qmax: 'int' = 127, epsilon: 'float' = 0.000244140625) -> 'QuantizationConfig'"
174+
175+
[python.get_symmetric_quantization_config]
176+
kind = "function"
177+
signature = "get_symmetric_quantization_config(is_per_channel: 'bool' = True, is_qat: 'bool' = False, is_dynamic: 'bool' = False, act_qmin: 'int' = -128, act_qmax: 'int' = 127, weight_qmin: 'int' = -127, weight_qmax: 'int' = 127, eps: 'float' = 1.52587890625e-05) -> 'QuantizationConfig'"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
# Copyright 2026 Arm Limited and/or its affiliates.
2+
#
3+
# This source code is licensed under the BSD-style license found in the
4+
# LICENSE file in the root directory of this source tree.
5+
"""Generate the Arm backend public API manifest."""
6+
7+
from __future__ import annotations
8+
9+
import enum
10+
import inspect
11+
from datetime import datetime
12+
from pathlib import Path
13+
from typing import Any, cast
14+
15+
import executorch.backends.arm as arm
16+
from executorch.backends.arm import LAZY_IMPORTS
17+
18+
ARM_PREFIX = "executorch.backends.arm"
19+
COPYRIGHT_START_YEAR = 2026
20+
HEADER = """# Copyright {years} Arm Limited and/or its affiliates.
21+
#
22+
# This source code is licensed under the BSD-style license found in the
23+
# LICENSE file in the root directory of this source tree.
24+
#
25+
# This file is generated by
26+
# backends/arm/scripts/generate_public_api_manifest.py
27+
"""
28+
MANIFEST_PATH = (
29+
Path(__file__).resolve().parents[1]
30+
/ "public_api_manifests"
31+
/ "api_manifest_running.toml"
32+
)
33+
34+
35+
def _copyright_year_range() -> str:
36+
current_year = datetime.now().year
37+
if current_year <= COPYRIGHT_START_YEAR:
38+
return str(COPYRIGHT_START_YEAR)
39+
return f"{COPYRIGHT_START_YEAR}-{current_year}"
40+
41+
42+
def _is_public(name: str) -> bool:
43+
return not name.startswith("_") or (name.startswith("__") and name.endswith("__"))
44+
45+
46+
def _is_init(name: str) -> bool:
47+
# Skip constructors so semantically equivalent classes with an implicit
48+
# default __init__ do not produce different manifest entries.
49+
return name != "__init__"
50+
51+
52+
def _is_backend(obj: object) -> bool:
53+
return getattr(obj, "__module__", "").startswith(ARM_PREFIX)
54+
55+
56+
def _api_kind(obj: object) -> str:
57+
if inspect.isclass(obj):
58+
return "enum" if issubclass(obj, enum.Enum) else "class"
59+
return "function"
60+
61+
62+
def _api_signature(path: str, obj: object) -> str:
63+
try:
64+
return f"{path}{inspect.signature(cast(Any, obj))}"
65+
except (TypeError, ValueError):
66+
return f"{path}()"
67+
68+
69+
def _owner(cls: type, name: str) -> type | None:
70+
for base in cls.__mro__:
71+
if name in vars(base):
72+
return base
73+
return None
74+
75+
76+
def _is_unstable_api(obj: object) -> bool:
77+
return getattr(obj, "__deprecated__", None) is not None
78+
79+
80+
def _collect_entry(
81+
path: str,
82+
obj: object,
83+
entries: dict[str, dict[str, str]],
84+
) -> None:
85+
if _is_unstable_api(obj):
86+
return
87+
entries[path] = {"kind": _api_kind(obj), "signature": _api_signature(path, obj)}
88+
if not inspect.isclass(obj):
89+
return
90+
91+
for name in sorted(dir(obj)):
92+
if not _is_public(name) or not _is_init(name):
93+
continue
94+
base = _owner(obj, name)
95+
if base is None or not _is_backend(base):
96+
continue
97+
member = getattr(obj, name)
98+
if inspect.isclass(member) or callable(member):
99+
_collect_entry(f"{path}.{name}", member, entries)
100+
101+
102+
def _collect_public_api() -> dict[str, dict[str, str]]:
103+
entries: dict[str, dict[str, str]] = {}
104+
for name in sorted(LAZY_IMPORTS):
105+
_collect_entry(name, getattr(arm, name), entries)
106+
return entries
107+
108+
109+
def _render_manifest(entries: dict[str, dict[str, str]]) -> str:
110+
lines = [HEADER.format(years=_copyright_year_range()).rstrip(), "", "[python]", ""]
111+
for path in sorted(entries):
112+
entry = entries[path]
113+
lines.extend(
114+
[
115+
f"[python.{path}]",
116+
f'kind = "{entry["kind"]}"',
117+
f'signature = "{entry["signature"]}"',
118+
"",
119+
]
120+
)
121+
return "\n".join(lines)
122+
123+
124+
def generate_manifest_from_init(
125+
*,
126+
repo_path: Path | None = None,
127+
) -> str:
128+
del repo_path
129+
return _render_manifest(_collect_public_api())
130+
131+
132+
def main() -> None:
133+
MANIFEST_PATH.write_text(generate_manifest_from_init(), encoding="utf-8")
134+
135+
136+
if __name__ == "__main__":
137+
main()

0 commit comments

Comments
 (0)