Skip to content

Commit 900b9fd

Browse files
authored
[CoreAI] Remove sidecar asset delivery from the exporter (pytorch#23318)
The Core AI runtime only loads assets embedded in the `.pte`, so sidecar delivery is removed from the exporter. * Removed: * `CoreAIPartitioner(uses_sidecar=...)` and the `coreai_uses_sidecar` compile spec. * `coreai_sidecar_dir` (also from `__init__` exports) and `COREAI_SIDECAR_DIR`. * The `sidecar` / `aot_compiled_sidecar` packaging values. * The sidecar write path in `_deliver`, along with its existing-directory guard and the "env var set but inline" warning. * Source `.aimodel` and AOT-compiled `.aimodelc` bundles are always embedded via NamedDataStore. * Tests: * The sidecar-only tests are deleted. * Three tests are kept and rewritten for inline delivery: the `archs` map points at embedded bundle keys, two delegates get distinct asset keys, and sizes are checked against embedded payloads. Test plan: `python -m pytest backends/apple/coreai` gives 113 passed.
1 parent d612ca4 commit 900b9fd

6 files changed

Lines changed: 53 additions & 436 deletions

File tree

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ repository root, in an env with ExecuTorch installed in editable mode and
4141
python -m pytest backends/apple/coreai
4242
```
4343

44-
Standard pytest options apply, e.g. `-v` or `-k sidecar`.
44+
Standard pytest options apply, e.g. `-v` or `-k manifest`.
4545

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

‎backends/apple/coreai/__init__.py‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88

99
from executorch.backends.apple.coreai.compiler.preprocess import (
1010
AOTCompileConfig,
11-
coreai_sidecar_dir,
1211
CoreAIBackend,
1312
)
1413
from executorch.backends.apple.coreai.partition.partitioner import CoreAIPartitioner
@@ -48,7 +47,6 @@ def get_default_compile_config() -> EdgeCompileConfig:
4847
"AOTCompileConfig",
4948
"CoreAIBackend",
5049
"CoreAIPartitioner",
51-
"coreai_sidecar_dir",
5250
"get_default_compile_config",
5351
"get_default_passes",
5452
]

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

Lines changed: 18 additions & 175 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,16 @@
77
# Core AI backend for delegating an EdgeProgram to Apple's Core AI framework
88
# via the ``coreai-torch`` converter.
99

10-
import contextlib
1110
import copy
1211
import hashlib
1312
import json
1413
import logging
15-
import os
16-
import shutil
1714
import subprocess
1815
from dataclasses import dataclass
1916
from enum import Enum
2017
from pathlib import Path
2118
from tempfile import TemporaryDirectory
22-
from typing import Any, Dict, final, Iterator, List, Optional, Tuple
19+
from typing import Any, Dict, final, List, Optional, Tuple
2320

2421
from executorch.backends.apple.coreai.compiler.constants import MAIN_ENTRYPOINT
2522
from executorch.backends.apple.coreai.compiler.enumerated_shapes import (
@@ -48,18 +45,9 @@
4845
# are mmap-friendly on device. Applied to every file flattened into the NDS.
4946
_ASSET_ALIGNMENT = 16
5047

51-
# Build-time only: directory where sidecar bundles are written. Read from the
52-
# environment (set it via :func:`coreai_sidecar_dir`) so no build-machine path
53-
# is ever serialized into the .pte. The runtime load directory is a separate,
54-
# runtime-provided concern (default: the .pte's dir).
55-
SIDECAR_DIR_ENV = "COREAI_SIDECAR_DIR"
56-
5748

5849
class COMPILE_SPEC_KEYS(Enum):
59-
# Whether this delegate's asset is delivered as a sidecar bundle (vs
60-
# embedded in the .pte via NamedDataStore). Serialized: the runtime needs
61-
# to know how to load the asset. Carries only the delivery mode, no path.
62-
USES_SIDECAR = "coreai_uses_sidecar"
50+
# Minimum OS
6351

6452
# Minimum OS / deployment version. This is a save-time property of the
6553
# .aimodel (save_asset(minimum_os=...)) that ALSO feeds coreai-build, so it
@@ -91,12 +79,8 @@ class COMPILE_SPEC_KEYS(Enum):
9179
class AssetPackaging(str, Enum):
9280
# Portable .aimodel embedded in the .pte via NamedDataStore.
9381
INLINE = "inline"
94-
# Portable .aimodel written as a sidecar next to the .pte.
95-
SIDECAR = "sidecar"
9682
# AOT-compiled per-arch .aimodelc bundles embedded in the .pte.
9783
AOT_COMPILED_INLINE = "aot_compiled_inline"
98-
# AOT-compiled per-arch .aimodelc bundles written as a sidecar.
99-
AOT_COMPILED_SIDECAR = "aot_compiled_sidecar"
10084

10185

10286
@dataclass(frozen=True)
@@ -173,96 +157,12 @@ def _get_compile_spec(
173157
return None
174158

175159

176-
def _reject_existing_asset_dir(dest: Path) -> None:
177-
"""Refuse to write over an asset directory a previous build left behind.
178-
179-
Each delegate owns one ``<hash>/`` directory, and the hash covers the
180-
``.aimodel`` only. AOT options are applied after it is computed, so a
181-
rebuild with a different platform or OS floor lands on the same path: left
182-
alone, the manifest would advertise the new options while the bundles on
183-
disk stayed from the earlier build. Two delegates in one build always
184-
differ here, so this only fires across builds.
185-
"""
186-
if dest.exists():
187-
raise RuntimeError(
188-
f"sidecar asset directory already exists: {dest}\n"
189-
f"A previous build wrote it, and its contents may have been "
190-
f"compiled with different options. Remove it, or point "
191-
f"COREAI_SIDECAR_DIR at a clean directory."
192-
)
193-
194-
195-
@contextlib.contextmanager
196-
def coreai_sidecar_dir(path: str) -> Iterator[None]:
197-
"""Set ``COREAI_SIDECAR_DIR`` for the duration of a ``with`` block.
198-
199-
Sidecar delivery (``uses_sidecar=True``, including AOT+sidecar) requires this
200-
env var to name the build-time output directory. The prior value is
201-
restored on exit.
202-
203-
The directory must not already hold asset directories: they are build
204-
output, and stale ones are indistinguishable from this build's. Loose
205-
files beside them (``.DS_Store``, a ``.pte``) are left alone. Setting the
206-
env var directly skips this check, but each asset directory is still
207-
guarded individually as it is written.
208-
209-
Example::
210-
211-
with coreai_sidecar_dir("build/model"):
212-
lowered = to_edge_transform_and_lower(
213-
ep, partitioner=[CoreAIPartitioner(uses_sidecar=True)]
214-
)
215-
"""
216-
existing = Path(path)
217-
if existing.exists() and not existing.is_dir():
218-
raise RuntimeError(f"sidecar output path is not a directory: {path}")
219-
if existing.is_dir() and any(p.is_dir() for p in existing.iterdir()):
220-
raise RuntimeError(
221-
f"sidecar output directory already holds assets: {path}\n"
222-
f"Assets from an earlier build cannot be told apart from this "
223-
f"one's. Remove the directory or choose another."
224-
)
225-
prev = os.environ.get(SIDECAR_DIR_ENV)
226-
os.environ[SIDECAR_DIR_ENV] = path
227-
try:
228-
yield
229-
finally:
230-
if prev is None:
231-
os.environ.pop(SIDECAR_DIR_ENV, None)
232-
else:
233-
os.environ[SIDECAR_DIR_ENV] = prev
234-
235-
236160
def _nds_key(model_hash: str, relative_path: str) -> str:
237161
# Namespaced by content hash so identical assets dedup to one entry and the
238162
# hash doubles as an on-device cache key.
239163
return f"coreai/{model_hash}/{relative_path}"
240164

241165

242-
# Warn at most once per process that a sidecar dir was set but ignored.
243-
_WARNED_SIDECAR_ENV_IGNORED = False
244-
245-
246-
def _maybe_warn_sidecar_env_ignored() -> None:
247-
"""Soft guardrail: env var set but this delegate uses inline delivery.
248-
249-
Likely a misconfiguration (user set the sidecar dir but forgot
250-
``uses_sidecar=True``). Not an error: the env var is process-wide and may
251-
legitimately be set for a different delegate, so we only warn, once.
252-
"""
253-
global _WARNED_SIDECAR_ENV_IGNORED
254-
if _WARNED_SIDECAR_ENV_IGNORED:
255-
return
256-
if os.environ.get(SIDECAR_DIR_ENV):
257-
_WARNED_SIDECAR_ENV_IGNORED = True
258-
logger.warning(
259-
"%s is set but this Core AI delegate uses inline delivery, "
260-
"so the sidecar directory is ignored. Did you mean to pass "
261-
"uses_sidecar=True to CoreAIPartitioner?",
262-
SIDECAR_DIR_ENV,
263-
)
264-
265-
266166
def _asset_metadata(staging: Path) -> Dict[str, Any]:
267167
"""Per-file sizes and a SHA-256 digest per bundle under ``staging``.
268168
@@ -297,44 +197,20 @@ def _deliver(
297197
model_hash: str,
298198
packaging: AssetPackaging,
299199
manifest_extra: Dict[str, Any],
300-
sidecar_dir: Optional[str],
301200
) -> PreprocessResult:
302-
"""Ship a staging directory of bundles, embedded or on disk.
201+
"""Embed a staging directory of bundles in the .pte with its manifest.
303202
304203
``staging`` holds the ``.aimodel`` / ``.aimodelc`` bundles, never their
305-
contents, so both deliveries land on the same ``<hash>/<bundle>/`` layout:
306-
inline under the ``coreai/`` NamedDataStore prefix, sidecar under the build
307-
output directory. Keeping one implementation is what stops the two from
308-
drifting apart again.
309-
310-
The sidecar write is staged and renamed into place, so a failure partway
311-
cannot leave a half-populated ``<hash>/`` behind for
312-
:func:`_reject_existing_asset_dir` to trip over on the next build.
204+
contents, so assets land under ``coreai/<hash>/<bundle>/`` in the
205+
NamedDataStore.
313206
"""
314207
manifest = {
315208
"packaging": packaging.value,
316209
"hash": model_hash,
317210
**manifest_extra,
318211
**_asset_metadata(staging),
319212
}
320-
if sidecar_dir is None:
321-
return _embed_dir_inline(staging, model_hash, manifest)
322-
323-
dest = Path(sidecar_dir) / model_hash
324-
dest.parent.mkdir(parents=True, exist_ok=True)
325-
_reject_existing_asset_dir(dest)
326-
pending = dest.with_name(f".{model_hash}.partial")
327-
if pending.exists():
328-
shutil.rmtree(pending)
329-
try:
330-
pending.mkdir(parents=True)
331-
for bundle in sorted(staging.iterdir()):
332-
shutil.move(str(bundle), str(pending / bundle.name))
333-
pending.rename(dest)
334-
except BaseException:
335-
shutil.rmtree(pending, ignore_errors=True)
336-
raise
337-
return PreprocessResult(processed_bytes=json.dumps(manifest).encode("utf-8"))
213+
return _embed_dir_inline(staging, model_hash, manifest)
338214

339215

340216
def _prepare_program_for_conversion(edge_program: ExportedProgram) -> ExportedProgram:
@@ -384,8 +260,7 @@ def _embed_dir_inline(
384260
"""Flatten every file under ``root_dir`` into the NamedDataStore.
385261
386262
``root_dir`` is the directory *containing* the ``.aimodel`` / ``.aimodelc``
387-
bundles, never a bundle itself, so the bundle name survives into the keys
388-
and the asset reconstructs to the layout the sidecar routes write on disk.
263+
bundles, never a bundle itself, so the bundle name survives into the keys.
389264
390265
Keys are ``coreai/{hash}/{relpath}``; ``manifest_extra`` becomes the
391266
manifest (packaging, files, archs, ...).
@@ -566,16 +441,12 @@ def _compile_aot(program, opts: Dict[str, Any], tmp_dir: Path) -> Tuple[str, Pat
566441
class CoreAIBackend(BackendDetails):
567442
"""AOT lowering of an edge program to a Core AI asset.
568443
569-
Delivery is chosen by compile specs, along two orthogonal axes:
444+
Delivery is chosen by compile specs: the portable ``.aimodel`` (default),
445+
or AOT-compiled per-architecture ``.aimodelc`` bundles (``aot_compile``, via
446+
``xcrun coreai-build``; architecture selection is one / a list / all). The
447+
bundles are embedded in the ``.pte`` via NamedDataStore.
570448
571-
* **Format**: the portable ``.aimodel`` (default), or AOT-compiled
572-
per-architecture ``.aimodelc`` bundles (``aot_compile``, via
573-
``xcrun coreai-build``; architecture selection is one / a list / all).
574-
* **Location**: embedded in the ``.pte`` via NamedDataStore (default), or a
575-
``sidecar`` written to ``$COREAI_SIDECAR_DIR`` (``uses_sidecar``).
576-
577-
``processed_bytes`` is always a small JSON manifest naming what/where; the
578-
bytes live in the NamedDataStore (inline) or on disk (sidecar).
449+
``processed_bytes`` is always a small JSON manifest naming what/where.
579450
Runtime execution is not wired up yet.
580451
"""
581452

@@ -588,20 +459,6 @@ def preprocess(
588459
_get_compile_spec(compile_specs, COMPILE_SPEC_KEYS.AOT_COMPILE_CONFIG)
589460
is not None
590461
)
591-
uses_sidecar = (
592-
_get_compile_spec(compile_specs, COMPILE_SPEC_KEYS.USES_SIDECAR) is not None
593-
)
594-
595-
# Sidecar delivery (portable or AOT) needs a build-time output dir.
596-
sidecar_dir = None
597-
if uses_sidecar:
598-
sidecar_dir = os.environ.get(SIDECAR_DIR_ENV)
599-
if not sidecar_dir:
600-
raise ValueError(
601-
"sidecar asset delivery requires the "
602-
f"{SIDECAR_DIR_ENV} environment variable to name the "
603-
"build-time output directory (set it via coreai_sidecar_dir)"
604-
)
605462

606463
mutated_inputs = edge_program.graph_signature.user_inputs_to_mutate
607464
if mutated_inputs:
@@ -643,57 +500,44 @@ def preprocess(
643500
# (via save_asset(minimum_os=...)). In the aot-compiled path the temp
644501
# .aimodel is discarded, so it keeps the default floor (no
645502
# double-specification).
646-
if not uses_sidecar:
647-
_maybe_warn_sidecar_env_ignored()
648503
if aot_compiled:
649504
return CoreAIBackend._preprocess_aot_compiled(
650-
program, _aot_compile_options(compile_specs), sidecar_dir, bindings
505+
program, _aot_compile_options(compile_specs), bindings
651506
)
652507
return CoreAIBackend._preprocess_portable(
653-
program,
654-
_min_os_version(compile_specs),
655-
sidecar_dir,
656-
bindings,
508+
program, _min_os_version(compile_specs), bindings
657509
)
658510

659511
# Portable .aimodel delivery.
660512
@staticmethod
661513
def _preprocess_portable(
662-
program, min_os, sidecar_dir: Optional[str], bindings: Dict[str, Any]
514+
program, min_os, bindings: Dict[str, Any]
663515
) -> PreprocessResult:
664516
with TemporaryDirectory() as tmp:
665517
model_hash = _save_and_hash(program, Path(tmp) / "model.aimodel", min_os)
666518
return _deliver(
667519
Path(tmp),
668520
model_hash,
669-
AssetPackaging.SIDECAR if sidecar_dir else AssetPackaging.INLINE,
521+
AssetPackaging.INLINE,
670522
{
671523
**bindings,
672524
# relative path the runtime resolves against its base
673525
"path": f"{model_hash}/model.aimodel",
674526
"min_deployment_version": _os_version_text(min_os),
675527
},
676-
sidecar_dir,
677528
)
678529

679530
# AOT-compiled .aimodelc delivery (per architecture).
680531
@staticmethod
681532
def _preprocess_aot_compiled(
682-
program,
683-
opts: Dict[str, Any],
684-
sidecar_dir: Optional[str],
685-
bindings: Dict[str, Any],
533+
program, opts: Dict[str, Any], bindings: Dict[str, Any]
686534
) -> PreprocessResult:
687535
with TemporaryDirectory() as tmp:
688536
model_hash, out = _compile_aot(program, opts, Path(tmp))
689537
return _deliver(
690538
out,
691539
model_hash,
692-
(
693-
AssetPackaging.AOT_COMPILED_SIDECAR
694-
if sidecar_dir
695-
else AssetPackaging.AOT_COMPILED_INLINE
696-
),
540+
AssetPackaging.AOT_COMPILED_INLINE,
697541
{
698542
**bindings,
699543
"platform": opts["platform"],
@@ -703,5 +547,4 @@ def _preprocess_aot_compiled(
703547
for arch, bundle in _compiled_arch_bundles(out)
704548
},
705549
},
706-
sidecar_dir,
707550
)

‎backends/apple/coreai/partition/partitioner.py‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,6 @@ class CoreAIPartitioner(Partitioner):
197197
def __init__(
198198
self,
199199
*,
200-
uses_sidecar: bool = False,
201200
aot_compile_config: Optional[AOTCompileConfig] = None,
202201
min_deployment_version: Optional[str] = None,
203202
input_enumerations: Optional[
@@ -206,13 +205,6 @@ def __init__(
206205
take_over_constant_data: bool = True,
207206
take_over_mutable_buffer: bool = True,
208207
) -> None:
209-
# uses_sidecar selects sidecar delivery (vs inline). It is embedded as a
210-
# compile spec because the runtime needs to know how to load the asset,
211-
# but it carries only the mode, no path. The build-time output directory
212-
# comes from the COREAI_SIDECAR_DIR env var (see preprocess.py /
213-
# coreai_sidecar_dir), never a compile spec, so no build-machine path is
214-
# serialized.
215-
#
216208
# aot_compile_config requests ahead-of-time ``xcrun coreai-build
217209
# compile`` in preprocess, emitting per-architecture ``.aimodelc``
218210
# bundles instead of the portable ``.aimodel``. It is serialized as a
@@ -225,8 +217,6 @@ def __init__(
225217
# symbols in :meth:`partition` and propagated to each subgraph boundary
226218
# in preprocess, so it is not built into a compile spec here.
227219
specs = []
228-
if uses_sidecar:
229-
specs.append(CompileSpec(COMPILE_SPEC_KEYS.USES_SIDECAR.value, b"1"))
230220
if min_deployment_version is not None:
231221
specs.append(
232222
CompileSpec(

0 commit comments

Comments
 (0)