Skip to content

Commit c215c38

Browse files
author
ssjia
committed
Update (base update)
[ghstack-poisoned]
2 parents c65459a + 68bb668 commit c215c38

33 files changed

Lines changed: 1858 additions & 286 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/ao_ext/ops/mxfp_conv2d_op.py

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -206,11 +206,12 @@ def __init__(
206206
padding: tuple[int, int],
207207
dilation: tuple[int, int],
208208
groups: int,
209-
config: MXFPOpConfig,
209+
weight_dtype: MXFPDType,
210+
block_size: int,
210211
) -> None:
211212
super().__init__()
212-
self.config = config
213-
self.weight_dtype = mxfp_dtype_to_str(config.weight_dtype)
213+
self.weight_dtype = mxfp_dtype_to_str(weight_dtype)
214+
self.block_size = block_size
214215

215216
self.register_buffer("weight_qdata", weight_qdata, persistent=True)
216217
self.register_buffer("weight_scale", weight_scale, persistent=True)
@@ -241,7 +242,7 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
241242
list(self.padding),
242243
list(self.dilation),
243244
self.groups,
244-
self.config.block_size,
245+
self.block_size,
245246
self.weight_dtype,
246247
)
247248

@@ -283,5 +284,6 @@ def transform_conv2d_to_mxfp(
283284
padding,
284285
dilation,
285286
module.groups,
286-
config,
287+
config.weight_dtype,
288+
config.block_size,
287289
)

backends/arm/ao_ext/ops/mxfp_linear_op.py

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
)
3434

3535

36+
_SUPPORTED_OUTPUT_DTYPES: set[torch.dtype] = {
37+
torch.float32,
38+
torch.bfloat16,
39+
}
40+
41+
3642
def _get_mx_elem_dtype(
3743
weight_qdata: torch.Tensor,
3844
weight_payload_dtype: str = "",
@@ -137,11 +143,14 @@ def __init__(
137143
weight_qdata: torch.Tensor,
138144
weight_scale: torch.Tensor,
139145
bias: torch.Tensor | None,
140-
config: MXFPOpConfig,
146+
weight_dtype: MXFPDType,
147+
block_size: int,
148+
output_dtype: torch.dtype = torch.float32,
141149
) -> None:
142150
super().__init__()
143-
self.config = config
144-
self.weight_dtype = mxfp_dtype_to_str(config.weight_dtype)
151+
self.weight_dtype = mxfp_dtype_to_str(weight_dtype)
152+
self.block_size = block_size
153+
self.output_dtype = output_dtype
145154

146155
self.register_buffer("weight_qdata", weight_qdata, persistent=True)
147156
self.register_buffer("weight_scale", weight_scale, persistent=True)
@@ -158,14 +167,17 @@ def __init__(
158167
)
159168

160169
def forward(self, x: torch.Tensor) -> torch.Tensor:
161-
return torch.ops.tosa_mxfp.linear.default(
170+
output = torch.ops.tosa_mxfp.linear.default(
162171
x,
163172
self.weight_qdata,
164173
self.weight_scale,
165174
self.bias,
166-
self.config.block_size,
175+
self.block_size,
167176
self.weight_dtype,
168177
)
178+
if self.output_dtype != torch.float32:
179+
output = output.to(self.output_dtype)
180+
return output
169181

170182

171183
def transform_linear_to_mxfp(
@@ -195,4 +207,14 @@ def transform_linear_to_mxfp(
195207
weight_scale = weight_scale.unsqueeze(0)
196208

197209
bias = module.bias.detach().to(torch.float32) if module.bias is not None else None
198-
return MXFPLinearOp(weight_qdata, weight_scale, bias, config)
210+
output_dtype = weight.dtype
211+
if output_dtype not in _SUPPORTED_OUTPUT_DTYPES:
212+
raise ValueError(f"Unsupported output_dtype: {output_dtype}")
213+
return MXFPLinearOp(
214+
weight_qdata,
215+
weight_scale,
216+
bias,
217+
config.weight_dtype,
218+
config.block_size,
219+
output_dtype,
220+
)

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:

0 commit comments

Comments
 (0)