Skip to content

Commit 2aebdf6

Browse files
authored
Add CoreAI CI on GitHub's experimental macOS27 runner (#23218)
This PR creates a CoreAI CI job on macOS27 runner. It also pins the CoreAI dependencies in a new requirements.txt.
1 parent a51094c commit 2aebdf6

9 files changed

Lines changed: 133 additions & 50 deletions

File tree

‎.github/pytorch-probot.yml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ tracking_issue: 7679
33
ciflow_push_tags:
44
- ciflow/android
55
- ciflow/apple
6+
- ciflow/coreai
67
- ciflow/cuda
78
- ciflow/cuda-perf
89
- ciflow/docker

‎.github/workflows/coreai.yml‎

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: Core AI
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
- release/*
8+
tags:
9+
- ciflow/coreai/*
10+
pull_request:
11+
paths:
12+
- .github/workflows/coreai.yml
13+
- backends/apple/coreai/**
14+
workflow_dispatch:
15+
16+
concurrency:
17+
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref_name }}-${{ github.ref_type == 'branch' && github.sha }}-${{ github.event_name == 'workflow_dispatch' }}
18+
cancel-in-progress: true
19+
20+
permissions:
21+
contents: read
22+
23+
jobs:
24+
run-decision:
25+
name: CI run decision
26+
uses: ./.github/workflows/_ci-run-decision.yml
27+
28+
test-coreai:
29+
needs: run-decision
30+
if: |
31+
github.event_name == 'pull_request' ||
32+
needs.run-decision.outputs.is-full-run == 'true'
33+
uses: pytorch/test-infra/.github/workflows/macos_job.yml@main
34+
with:
35+
default-packages: ""
36+
job-name: test-coreai
37+
# GitHub-hosted macOS 27 with Xcode 27 (public preview).
38+
runner: xcode-27
39+
# coreai-opt supports Python < 3.14.
40+
python-version: "3.12"
41+
submodules: recursive
42+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
43+
timeout: 90
44+
script: |
45+
set -eux
46+
47+
echo "::group::Toolchain"
48+
sw_vers
49+
xcodebuild -version
50+
xcrun --show-sdk-version
51+
echo "::endgroup::"
52+
53+
echo "::group::Install the Metal Toolchain"
54+
# coreai-build ships in the Metal Toolchain; without it the AOT compile
55+
# tests skip.
56+
xcodebuild -downloadComponent MetalToolchain
57+
xcrun --find coreai-build
58+
echo "::endgroup::"
59+
60+
echo "::group::Install ExecuTorch and Core AI requirements"
61+
${CONDA_RUN} python install_executorch.py --editable > /dev/null
62+
${CONDA_RUN} pip install -r backends/apple/coreai/requirements.txt
63+
${CONDA_RUN} pip list
64+
echo "::endgroup::"
65+
66+
echo "::group::Run Core AI export tests"
67+
${CONDA_RUN} python -m pytest backends/apple/coreai
68+
echo "::endgroup::"

‎backends/apple/__init__.py‎

Whitespace-only changes.

‎backends/apple/coreai/.llms/skills/code-writing.md‎

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -33,21 +33,15 @@ Legitimate non-prose dashes are fine: CLI flags (`--platform`,
3333

3434
## Running the tests
3535

36-
Run the whole backend suite (both `test/` and `passes/test/`) with the helper
37-
script, from inside the Core AI conda env:
36+
Run the whole backend suite (both `test/` and `passes/test/`) from the
37+
repository root, in an env with ExecuTorch installed in editable mode and
38+
`backends/apple/coreai/requirements.txt` installed:
3839

3940
```bash
40-
conda run -n coreai backends/apple/coreai/run_all_tests.sh
41+
python -m pytest backends/apple/coreai
4142
```
4243

43-
Extra args are forwarded to `unittest`, e.g. `... run_all_tests.sh -v` or
44-
`... run_all_tests.sh -k sidecar`.
45-
46-
**Use this script (unittest), not `pytest` directly.** pytest's default
47-
discovery puts `backends/apple/` on `sys.path`, so `import coreai` resolves to
48-
this backend directory and shadows the real Apple `coreai` SDK (`coreai_torch`
49-
then fails importing `coreai.authoring`). unittest imports via the full
50-
`executorch.backends.apple.coreai.*` path and avoids the shadowing.
44+
Standard pytest options apply, e.g. `-v` or `-k sidecar`.
5145

5246
The real-toolchain AOT test (`CoreAIAOTCompileTest`) is gated on
5347
`xcrun coreai-build`; it runs on macOS with the Metal Toolchain and skips

‎backends/apple/coreai/README.md‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,21 @@
55
> general use, and provides **no backward- or forward-compatibility guarantees**
66
> (no BC/FC). APIs, compile specs, serialized manifests, and file layouts may
77
> change or break at any time without notice. Do not depend on it.
8+
9+
## Running the tests
10+
11+
Install ExecuTorch in editable mode, then the pinned Core AI SDK packages:
12+
13+
```bash
14+
python install_executorch.py --editable
15+
pip install -r backends/apple/coreai/requirements.txt
16+
```
17+
18+
Run the backend's Python tests from the repository root:
19+
20+
```bash
21+
python -m pytest backends/apple/coreai
22+
```
23+
24+
`CoreAIAOTCompileTest` needs `xcrun coreai-build` from the Metal Toolchain
25+
(`xcodebuild -downloadComponent MetalToolchain`) and skips without it.

‎backends/apple/coreai/compiler/enumerated_shapes.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def _shape_symbols(shape) -> Set[str]:
138138

139139
def graph_input_names(program, entrypoint: str = MAIN_ENTRYPOINT) -> List[str]:
140140
"""Names of the converted coreai graph's inputs (in argument order)."""
141-
graph = program.get_graph(entrypoint)
141+
graph = program._get_graph(entrypoint)
142142
names = []
143143
for attr in graph.arg_attrs:
144144
if "coreai.name" in attr:

‎backends/apple/coreai/compiler/io_compat.py‎

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import torch
1919
from executorch.backends.apple.coreai.compiler.constants import MAIN_ENTRYPOINT
2020
from torch.export.exported_program import ExportedProgram
21-
from torch.export.graph_signature import InputKind
21+
from torch.export.graph_signature import InputKind, OutputKind
2222

2323
logger = logging.getLogger(__name__)
2424

@@ -49,18 +49,40 @@ def _coreai_io(program, entrypoint: str = MAIN_ENTRYPOINT):
4949
Ranked tensors -> (element_type_str, shape_tuple). Any non-tensor coreai
5050
type is represented as (str(type), None) so the comparison can flag it,
5151
rather than assuming a tensor and crashing on ``.element_type`` / ``.shape``.
52+
53+
Mutated buffers the delegate owns are skipped: coreai-torch marks the state
54+
argument with ``MutableBuffers.buffer_mutation`` naming its result, and
55+
neither crosses the ExecuTorch boundary.
5256
"""
5357
from coreai._compiler.ir import RankedTensorType
5458

55-
func_type = program.get_graph(entrypoint).function_type.value
59+
graph = program._get_graph(entrypoint)
60+
func_type = graph.function_type.value
5661

5762
def _describe(t):
5863
if RankedTensorType.isinstance(t):
5964
return (str(t.element_type), tuple(t.shape))
6065
return (str(t), None)
6166

62-
inputs = [_describe(t) for t in func_type.inputs]
63-
outputs = [_describe(t) for t in func_type.results]
67+
arg_attrs = list(graph.arg_attrs or [])
68+
res_attrs = list(graph.res_attrs or [])
69+
state_results = {
70+
attrs["MutableBuffers.buffer_mutation"].value
71+
for attrs in arg_attrs
72+
if "MutableBuffers.buffer_mutation" in attrs
73+
}
74+
inputs = [
75+
_describe(t)
76+
for i, t in enumerate(func_type.inputs)
77+
if i >= len(arg_attrs) or "MutableBuffers.buffer_mutation" not in arg_attrs[i]
78+
]
79+
outputs = [
80+
_describe(t)
81+
for i, t in enumerate(func_type.results)
82+
if i >= len(res_attrs)
83+
or "coreai.name" not in res_attrs[i]
84+
or res_attrs[i]["coreai.name"].value not in state_results
85+
]
6486
return inputs, outputs
6587

6688

@@ -73,20 +95,20 @@ def _edge_io(edge_program: ExportedProgram):
7395
entries).
7496
7597
Inputs walk ``graph_signature.input_specs``, which is ordered like the
76-
placeholders. Mutated buffers are included alongside the user inputs,
77-
mirroring ``TorchConverter._register_io``: a mutation is passed in and
78-
handed back, so coreai gives it a graph argument.
98+
placeholders, and outputs walk ``output_specs``, ordered like the output
99+
node. Mutated buffers the delegate owns (``BUFFER`` inputs in
100+
``buffers_to_mutate`` and their ``BUFFER_MUTATION`` outputs) are skipped:
101+
ExecuTorch never hands that state to the delegate. Mutated user inputs
102+
stay, because ExecuTorch does pass those across.
79103
"""
80104
placeholders = {
81105
n.name: n for n in edge_program.graph.nodes if n.op == "placeholder"
82106
}
83107
signature = edge_program.graph_signature
84-
mutated = set(signature.buffers_to_mutate.values())
85108

86109
inputs = []
87110
for spec in signature.input_specs:
88-
is_mutated_buffer = spec.kind == InputKind.BUFFER and spec.target in mutated
89-
if spec.kind != InputKind.USER_INPUT and not is_mutated_buffer:
111+
if spec.kind != InputKind.USER_INPUT:
90112
continue
91113
node = placeholders.get(getattr(spec.arg, "name", None))
92114
# A non-tensor input carries its literal value on the spec instead.
@@ -96,7 +118,11 @@ def _edge_io(edge_program: ExportedProgram):
96118
else:
97119
inputs.append((type(val).__name__, None))
98120
outputs = []
99-
for arg in edge_program.graph.output_node().args[0]:
121+
for spec, arg in zip(
122+
signature.output_specs, edge_program.graph.output_node().args[0]
123+
):
124+
if spec.kind == OutputKind.BUFFER_MUTATION:
125+
continue
100126
val = arg.meta.get("val") if hasattr(arg, "meta") else arg
101127
if hasattr(val, "dtype"):
102128
outputs.append((val.dtype, tuple(val.shape)))
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
coreai-core==1.0.0b3
2+
coreai-torch==0.4.3
3+
coreai-opt==0.3.0

‎backends/apple/coreai/run_all_tests.sh‎

Lines changed: 0 additions & 27 deletions
This file was deleted.

0 commit comments

Comments
 (0)