Skip to content

Commit 4d3b46d

Browse files
authored
[ExecuTorch][Core AI][2/x] Core backend: partitioner, preprocess, IO compat, copy-op pass (pytorch#21510)
Introduces the Core AI ExecuTorch backend. Partitioner: op support derived from coreai-torch tables, `ops_to_not_decompose`, and a guard requiring `to_edge_transform_and_lower`. Preprocess: converts the edge program to a Core AI program and delivers it as a portable `.aimodel` (inline via NamedDataStore or sidecar) or AOT compiled per architecture `.aimodelc`. Also adds the delegate boundary IO compatibility check and the copy op to functional rewrite pass. Includes partitioner, preprocess, and IO compat tests.
1 parent 9a2d135 commit 4d3b46d

14 files changed

Lines changed: 2447 additions & 0 deletions

‎backends/apple/coreai/README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Core AI backend (ExecuTorch)
2+
3+
> ⚠️ **Under construction — not for use.**
4+
> This backend is a work in progress. It is **not** ready for production or
5+
> general use, and provides **no backward- or forward-compatibility guarantees**
6+
> (no BC/FC). APIs, compile specs, serialized manifests, and file layouts may
7+
> change or break at any time without notice. Do not depend on it.

‎backends/apple/coreai/__init__.py‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 typing import List
8+
9+
from executorch.backends.apple.coreai.compiler.preprocess import (
10+
AOTCompileConfig,
11+
coreai_sidecar_dir,
12+
CoreAIBackend,
13+
)
14+
from executorch.backends.apple.coreai.partition.partitioner import CoreAIPartitioner
15+
from executorch.exir import EdgeCompileConfig
16+
from executorch.exir.pass_base import PassType
17+
18+
19+
def get_default_passes() -> List[PassType]:
20+
"""Default edge transform passes for Core AI lowering.
21+
22+
``NarrowToCoreAIDtypesPass`` casts int64/float64 graph inputs to 32-bit at
23+
the boundary (preserving the model's external I/O dtype) so index-style
24+
inputs can be delegated. Core AI supports only up-to-32-bit dtypes.
25+
"""
26+
from executorch.backends.apple.coreai.passes import NarrowToCoreAIDtypesPass
27+
28+
return [NarrowToCoreAIDtypesPass()]
29+
30+
31+
def get_default_compile_config() -> EdgeCompileConfig:
32+
"""Default ``EdgeCompileConfig`` for Core AI lowering.
33+
34+
``_skip_dim_order=True`` keeps ExecuTorch on ``aten._to_copy`` instead of
35+
emitting ``dim_order_ops._to_dim_order_copy``, which coreai-torch cannot
36+
lower (its validator requires dim-order ops be decomposed).
37+
"""
38+
return EdgeCompileConfig(_skip_dim_order=True)
39+
40+
41+
__all__ = [
42+
"AOTCompileConfig",
43+
"CoreAIBackend",
44+
"CoreAIPartitioner",
45+
"coreai_sidecar_dir",
46+
"get_default_compile_config",
47+
"get_default_passes",
48+
]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
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.
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+
"""Shared constants for the Core AI backend compiler."""
8+
9+
# The single entrypoint / graph name of a per-delegate coreai AIProgram. Each
10+
# ExecuTorch delegate is converted to its own coreai program whose only graph is
11+
# named "main" (``save_asset`` emits ``main.hash`` / ``main.mlirb`` accordingly).
12+
MAIN_ENTRYPOINT = "main"
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
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+
"""Delegate-boundary compatibility checks for the Core AI backend.
8+
9+
ExecuTorch calls a delegate with plain tensors whose dtypes/shapes come from the
10+
edge subgraph boundary. The converted ``.aimodel`` must declare I/O that matches
11+
what ExecuTorch will feed/read, or the runtime will mismatch. :func:`
12+
assert_io_compatible` compares the two *before* compilation and fails loudly.
13+
"""
14+
15+
import logging
16+
from typing import Any, List, Sequence, Tuple
17+
18+
import torch
19+
from executorch.backends.apple.coreai.compiler.constants import MAIN_ENTRYPOINT
20+
from torch.export.exported_program import ExportedProgram
21+
from torch.export.graph_signature import InputKind
22+
23+
logger = logging.getLogger(__name__)
24+
25+
# coreai's element-type spelling for each torch dtype (from coreai_torch's
26+
# _type_mapping: signed ints -> siN, unsigned -> uiN, bool -> i1, floats -> fN).
27+
# These are the un-narrowed spellings, asserted exactly. coreai narrows
28+
# int64->si32 and float64->f32 in get_tensor_type, so those two dtypes surface
29+
# here as mismatches by design (that is the signal).
30+
_TORCH_TO_COREAI_ELT = {
31+
torch.float16: "f16",
32+
torch.float32: "f32",
33+
torch.float64: "f64",
34+
torch.bfloat16: "bf16",
35+
torch.bool: "i1",
36+
torch.int8: "si8",
37+
torch.uint8: "ui8",
38+
torch.int16: "si16",
39+
torch.uint16: "ui16",
40+
torch.int32: "si32",
41+
torch.uint32: "ui32",
42+
torch.int64: "si64",
43+
}
44+
45+
46+
def _coreai_io(program, entrypoint: str = MAIN_ENTRYPOINT):
47+
"""Core AI graph I/O as lists of (element_type_str, shape_or_None).
48+
49+
Ranked tensors -> (element_type_str, shape_tuple). Any non-tensor coreai
50+
type is represented as (str(type), None) so the comparison can flag it,
51+
rather than assuming a tensor and crashing on ``.element_type`` / ``.shape``.
52+
"""
53+
from coreai._compiler.ir import RankedTensorType
54+
55+
func_type = program.get_graph(entrypoint).function_type.value
56+
57+
def _describe(t):
58+
if RankedTensorType.isinstance(t):
59+
return (str(t.element_type), tuple(t.shape))
60+
return (str(t), None)
61+
62+
inputs = [_describe(t) for t in func_type.inputs]
63+
outputs = [_describe(t) for t in func_type.results]
64+
return inputs, outputs
65+
66+
67+
def _edge_io(edge_program: ExportedProgram):
68+
"""Edge subgraph I/O as lists of (dtype_or_typename, shape_or_None).
69+
70+
Tensors -> (torch_dtype, shape_tuple). Non-tensor I/O (symint / const
71+
int/float) -> (type_name_str, None), kept as entries so both sides stay
72+
positionally aligned with :func:`_coreai_io` (which also emits non-tensor
73+
entries).
74+
75+
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.
79+
"""
80+
placeholders = {
81+
n.name: n for n in edge_program.graph.nodes if n.op == "placeholder"
82+
}
83+
signature = edge_program.graph_signature
84+
mutated = set(signature.buffers_to_mutate.values())
85+
86+
inputs = []
87+
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:
90+
continue
91+
node = placeholders.get(getattr(spec.arg, "name", None))
92+
# A non-tensor input carries its literal value on the spec instead.
93+
val = node.meta.get("val") if node is not None else spec.arg.value
94+
if hasattr(val, "dtype"):
95+
inputs.append((val.dtype, tuple(val.shape)))
96+
else:
97+
inputs.append((type(val).__name__, None))
98+
outputs = []
99+
for arg in edge_program.graph.output_node().args[0]:
100+
val = arg.meta.get("val") if hasattr(arg, "meta") else arg
101+
if hasattr(val, "dtype"):
102+
outputs.append((val.dtype, tuple(val.shape)))
103+
else:
104+
outputs.append((type(val).__name__, None))
105+
return inputs, outputs
106+
107+
108+
def io_mismatches(
109+
coreai_io: Sequence[Tuple[str, Tuple[Any, ...]]],
110+
edge_io: Sequence[Tuple[Any, Tuple[Any, ...]]],
111+
kind: str,
112+
) -> List[str]:
113+
"""Return human-readable mismatch messages between coreai and edge I/O.
114+
115+
Compares count, per-tensor dtype (all mapped types: floats and ints), rank,
116+
and static (concrete) dims. Symbolic (dynamic) dims are skipped: coreai is
117+
specialized to a concrete size while the edge dim stays symbolic.
118+
"""
119+
if len(coreai_io) != len(edge_io):
120+
return [
121+
f"{kind} count mismatch: .aimodel has {len(coreai_io)}, "
122+
f"ExecuTorch feeds {len(edge_io)}"
123+
]
124+
errors: List[str] = []
125+
for i, ((elt, cshape), (dtype, eshape)) in enumerate(zip(coreai_io, edge_io)):
126+
coreai_nontensor = cshape is None
127+
edge_nontensor = eshape is None
128+
if coreai_nontensor or edge_nontensor:
129+
# At least one side is non-tensor; only a class mismatch matters.
130+
if coreai_nontensor != edge_nontensor:
131+
errors.append(
132+
f"{kind} {i}: tensor/non-tensor mismatch "
133+
f"(.aimodel={elt}, ExecuTorch feeds {dtype})"
134+
)
135+
continue
136+
expected = _TORCH_TO_COREAI_ELT.get(dtype)
137+
if expected is not None and elt != expected:
138+
errors.append(
139+
f"{kind} {i}: dtype mismatch (.aimodel={elt}, ExecuTorch feeds "
140+
f"{dtype}, expected {expected})"
141+
)
142+
if len(cshape) != len(eshape):
143+
errors.append(
144+
f"{kind} {i}: rank mismatch (.aimodel={cshape}, "
145+
f"ExecuTorch={eshape})"
146+
)
147+
continue
148+
for d, (cdim, edim) in enumerate(zip(cshape, eshape)):
149+
if isinstance(edim, int) and cdim != edim:
150+
errors.append(
151+
f"{kind} {i} dim {d}: static shape mismatch "
152+
f"(.aimodel={cdim}, ExecuTorch={edim})"
153+
)
154+
return errors
155+
156+
157+
def assert_io_compatible(program, edge_program: ExportedProgram) -> None:
158+
"""Raise if the ``.aimodel`` boundary I/O is incompatible with ExecuTorch."""
159+
coreai_in, coreai_out = _coreai_io(program)
160+
edge_in, edge_out = _edge_io(edge_program)
161+
logger.info(
162+
"Core AI delegate boundary I/O:\n"
163+
" inputs: .aimodel=%s\n"
164+
" ExecuTorch=%s\n"
165+
" outputs: .aimodel=%s\n"
166+
" ExecuTorch=%s",
167+
coreai_in,
168+
edge_in,
169+
coreai_out,
170+
edge_out,
171+
)
172+
errors = io_mismatches(coreai_in, edge_in, "input") + io_mismatches(
173+
coreai_out, edge_out, "output"
174+
)
175+
if errors:
176+
raise ValueError(
177+
"Core AI delegate boundary is incompatible with ExecuTorch:\n "
178+
+ "\n ".join(errors)
179+
)

0 commit comments

Comments
 (0)