Skip to content

Commit 1d598ab

Browse files
Arm backend: Add manifest validation and add Backwards compabillety testing (#18813)
1 parent 51d30d4 commit 1d598ab

17 files changed

Lines changed: 1296 additions & 10 deletions

.github/workflows/pull.yml

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -816,6 +816,33 @@ jobs:
816816
# Test test_arm_backend.sh with test
817817
backends/arm/test/test_arm_backend.sh "${ARM_TEST}"
818818
819+
test-arm-backend-public-api-backward-compatibility:
820+
name: test-arm-backend-public-api-backward-compatibility
821+
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main
822+
permissions:
823+
id-token: write
824+
contents: read
825+
with:
826+
runner: linux.2xlarge.memory
827+
docker-image: ci-image:executorch-ubuntu-22.04-arm-sdk
828+
submodules: 'recursive'
829+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
830+
timeout: 120
831+
script: |
832+
# The generic Linux job chooses to use base env, not the one setup by the image
833+
CONDA_ENV=$(conda env list --json | jq -r ".envs | .[-1]")
834+
conda activate "${CONDA_ENV}"
835+
836+
source .ci/scripts/utils.sh
837+
install_executorch "--use-pt-pinned-commit"
838+
839+
.ci/scripts/setup-arm-baremetal-tools.sh --enable-mlsdk-deps --install-mlsdk-deps-with-pip
840+
source examples/arm/arm-scratch/setup_path.sh
841+
842+
backends/arm/scripts/public_api_manifest/validate_all_public_api_manifests.sh
843+
844+
python backends/arm/test/public_api_bc/run_public_api_bc_scenarios.py
845+
819846
test-llama-runner-qnn-linux:
820847
name: test-llama-runner-qnn-linux
821848
uses: pytorch/test-infra/.github/workflows/linux_job_v2.yml@main

backends/arm/TARGETS

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,15 @@ runtime.python_library(
119119
"//executorch/exir:lib",
120120
],
121121
)
122+
runtime.python_library(
123+
name = "public_api",
124+
srcs = ["__init__.py"],
125+
deps = [
126+
":ethosu",
127+
":vgf",
128+
"//executorch/backends/arm/quantizer:lib",
129+
],
130+
)
122131

123132
runtime.python_library(
124133
name = "process_node",

backends/arm/public_api_manifests/api_manifest_running.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# LICENSE file in the root directory of this source tree.
55
#
66
# This file is generated by
7-
# backends/arm/scripts/generate_public_api_manifest.py
7+
# backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py
88

99
[python]
1010

backends/arm/scripts/pre-push

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,13 @@ run_docgen_check() {
7070
fi
7171
}
7272

73+
run_public_api_validator() {
74+
if ! backends/arm/scripts/public_api_manifest/validate_all_public_api_manifests.sh; then
75+
echo -e "${ERROR} Arm public API manifest validation failed"
76+
FAILED=1
77+
fi
78+
}
79+
7380
# This list of imperative verbs was compiled from the entire list of Executorch
7481
# commits. It should be fairly exhaustive, but add more verbs if you find one
7582
# that's missing.
@@ -149,7 +156,6 @@ for COMMIT in ${COMMITS}; do
149156
fi
150157
done
151158
fi
152-
153159
# Check license headers
154160
# We do a simple check of if all committed headers contain
155161
# "$current_year Arm". This does not guarantee OK in ci but should be ok
@@ -312,6 +318,8 @@ else
312318
echo -e "${INFO} Skipping Arm docgen (no public API inputs changed)"
313319
fi
314320

321+
run_public_api_validator
322+
315323
if [[ $FAILED ]]; then
316324
echo -e "${INFO} Fix your commit message errors with"\
317325
"'git commit --amend' or 'git commit --fixup=<SHA>'"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
load("@fbcode_macros//build_defs:build_file_migration.bzl", "fbcode_target", "non_fbcode_target")
7+
load("@fbsource//xplat/executorch/build:runtime_wrapper.bzl", "runtime")
8+
9+
oncall("executorch")
10+
11+
fbcode_target(_kind = runtime.python_library,
12+
name = "public_api_manifest",
13+
srcs = [
14+
"__init__.py",
15+
"generate_public_api_manifest.py",
16+
"validate_public_api_manifest.py",
17+
],
18+
deps = [
19+
"//executorch/backends/arm:public_api",
20+
"fbsource//third-party/pypi/tomli:tomli",
21+
],
22+
)
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# Manifests
2+
3+
Manifests are used to track the current public API of the Arm backend. They are
4+
generated with
5+
`python backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py`.
6+
7+
## Running manifest
8+
9+
There is always one running manifest which has the main purpose of tracking the
10+
API surface inbetween releases.
11+
12+
## Static manifests
13+
14+
At any given time there may be up to two static manifests. These are generated
15+
in conjunction with a release and are used to track the API surface of that
16+
release. The main purpose of these is to make sure backwards compatibility.
17+
18+
A static manifest may never be changed. It belongs to a release and must be kept
19+
as is.
20+
21+
A static manifest should not live longer than 2 releases. It may then be
22+
removed.
23+
24+
# On release
25+
26+
With each release, check that the running manifest is up to date and reflects
27+
the API surface of the release. Then, copy the running manifest to a new static
28+
manifest for the release. This can be done by running
29+
`cp <running_manifest> <static_manifest>`. The new static manifest should be
30+
named according to the release, e.g. `api_manifest_1_3.toml` for release 1.3 and
31+
so on. If there are now more than two static manifests, remove the oldest one in
32+
the same commit.
33+
34+
# API changes
35+
36+
When introducing an API change, the running manifest must be updated to reflect
37+
the change. This is done by running the manifest generation script,
38+
`python backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py`.
39+
This updates the running manifest.
40+
41+
To validate the running manifest directly, run
42+
`python backends/arm/scripts/public_api_manifest/validate_public_api_manifest.py`.
43+
44+
To validate all manifests, use `backends/arm/scripts/pre-push`. This is the
45+
check that must pass before the change is ready to merge.
46+
47+
Manifest validation only checks the API surface and signatures. Workflow-level
48+
backward compatibility is covered separately by the scenario runner described
49+
below.
50+
51+
Running-manifest validation uses exact signature matching. Any intentional API
52+
change must update `api_manifest_running.toml`.
53+
54+
Static-manifest validation uses backward-compatibility matching. The old
55+
release signature must still be callable against the current API. For example,
56+
adding a trailing optional parameter is accepted for static manifests, while
57+
removing a parameter, reordering parameters, or adding a new required
58+
parameter still fails validation.
59+
60+
## Backward-compatibility scenarios
61+
62+
Workflow-level backward compatibility is checked by
63+
`python backends/arm/test/public_api_bc/run_public_api_bc_scenarios.py`.
64+
65+
The runner hardcodes the current canonical public API workflow scripts:
66+
67+
- `backends/arm/test/public_api_bc/test_ethosu_flow.py`
68+
- `backends/arm/test/public_api_bc/test_vgf_fp_flow.py`
69+
- `backends/arm/test/public_api_bc/test_vgf_int_flow.py`
70+
71+
These scripts should be updated continuously to reflect the current public API.
72+
The runner materializes those same paths into a temporary harness and executes
73+
them there with pytest so they import the latest installed
74+
`executorch.backends.arm` package instead of the repository source tree.
75+
76+
The rolling support window is controlled by the `OLDEST_SUPPORTED_REF` constant
77+
in `backends/arm/test/public_api_bc/run_public_api_bc_scenarios.py`:
78+
79+
- If `OLDEST_SUPPORTED_REF` is empty, the runner uses the current workspace.
80+
This is the bootstrap mode until a release contains the scenario scripts.
81+
- Once a release contains the scripts, the release epic should update
82+
`OLDEST_SUPPORTED_REF` to the oldest still-supported release ref.
83+
- At that point the runner uses `git show <ref>:<path>` to fetch the old
84+
release's scripts and run them against the latest code.
85+
86+
When an old release falls out of the support window, update
87+
`OLDEST_SUPPORTED_REF` to the next newer supported release. That is how the
88+
backward-compatibility window rolls forward.
89+
Reasons for passing validation may include:
90+
- Adding a new API symbol and adding it to the running manifest.
91+
- Removing an API that was marked as deprecated and no longer exists in any
92+
manifest.
93+
- Deprecated symbols do not break backward compatibility with static
94+
manifests.
95+
- Deprecating a symbol removes it from the running manifest, but it can only be
96+
removed fully once it no longer appears in any static manifest.
97+
- Extending a static-manifest signature in a backward-compatible way, such as
98+
adding a trailing optional parameter.
99+
100+
Reasons for failing validation may include:
101+
- Removing an API symbol without deprecation.
102+
- Changing a running-manifest signature without regenerating the running
103+
manifest.
104+
- Changing a static-manifest signature in a non-backward-compatible way.
105+
- New API symbol added but not added to the running manifest.
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
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.

backends/arm/scripts/generate_public_api_manifest.py renamed to backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py

Lines changed: 22 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323
# LICENSE file in the root directory of this source tree.
2424
#
2525
# This file is generated by
26-
# backends/arm/scripts/generate_public_api_manifest.py
26+
# backends/arm/scripts/public_api_manifest/generate_public_api_manifest.py
2727
"""
2828
MANIFEST_PATH = (
29-
Path(__file__).resolve().parents[1]
29+
Path(__file__).resolve().parents[2]
3030
/ "public_api_manifests"
3131
/ "api_manifest_running.toml"
3232
)
@@ -84,8 +84,10 @@ def _collect_entry(
8484
path: str,
8585
obj: object,
8686
entries: dict[str, dict[str, str]],
87+
*,
88+
include_deprecated: bool = False,
8789
) -> None:
88-
if _is_unstable_api(obj):
90+
if _is_unstable_api(obj) and not include_deprecated:
8991
return
9092
entries[path] = {"kind": _api_kind(obj), "signature": _api_signature(path, obj)}
9193
if not inspect.isclass(obj):
@@ -99,13 +101,25 @@ def _collect_entry(
99101
continue
100102
member = getattr(obj, name)
101103
if inspect.isclass(member) or callable(member):
102-
_collect_entry(f"{path}.{name}", member, entries)
104+
_collect_entry(
105+
f"{path}.{name}",
106+
member,
107+
entries,
108+
include_deprecated=include_deprecated,
109+
)
103110

104111

105-
def _collect_public_api() -> dict[str, dict[str, str]]:
112+
def _collect_public_api(
113+
*, include_deprecated: bool = False
114+
) -> dict[str, dict[str, str]]:
106115
entries: dict[str, dict[str, str]] = {}
107116
for name in sorted(LAZY_IMPORTS):
108-
_collect_entry(name, getattr(arm, name), entries)
117+
_collect_entry(
118+
name,
119+
getattr(arm, name),
120+
entries,
121+
include_deprecated=include_deprecated,
122+
)
109123
return entries
110124

111125

@@ -127,9 +141,10 @@ def _render_manifest(entries: dict[str, dict[str, str]]) -> str:
127141
def generate_manifest_from_init(
128142
*,
129143
repo_path: Path | None = None,
144+
include_deprecated: bool = False,
130145
) -> str:
131146
del repo_path
132-
return _render_manifest(_collect_public_api())
147+
return _render_manifest(_collect_public_api(include_deprecated=include_deprecated))
133148

134149

135150
def main() -> None:
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2026 Arm Limited and/or its affiliates.
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+
set -u
8+
9+
echo "Validating Arm public API manifests"
10+
11+
manifest_failures=0
12+
for manifest_path in backends/arm/public_api_manifests/api_manifest_*.toml; do
13+
if [[ ! -f "${manifest_path}" ]]; then
14+
continue
15+
fi
16+
manifest_name="${manifest_path##*/}"
17+
echo
18+
echo "=== ${manifest_name} ==="
19+
validator_output=$(
20+
python backends/arm/scripts/public_api_manifest/validate_public_api_manifest.py \
21+
--manifest "${manifest_path}" 2>&1
22+
)
23+
validator_status=$?
24+
printf '%s\n' "${validator_output}"
25+
if [[ ${validator_status} -ne 0 ]]; then
26+
manifest_failures=$((manifest_failures + 1))
27+
fi
28+
done
29+
30+
echo
31+
if [[ ${manifest_failures} -eq 0 ]]; then
32+
echo "Arm public API manifests OK"
33+
else
34+
echo "${manifest_failures} manifest(s) failed validation"
35+
exit 1
36+
fi

0 commit comments

Comments
 (0)