77# Core AI backend for delegating an EdgeProgram to Apple's Core AI framework
88# via the ``coreai-torch`` converter.
99
10- import contextlib
1110import copy
1211import hashlib
1312import json
1413import logging
15- import os
16- import shutil
1714import subprocess
1815from dataclasses import dataclass
1916from enum import Enum
2017from pathlib import Path
2118from 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
2421from executorch .backends .apple .coreai .compiler .constants import MAIN_ENTRYPOINT
2522from executorch .backends .apple .coreai .compiler .enumerated_shapes import (
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
5849class 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):
9179class 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-
236160def _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-
266166def _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
340216def _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
566441class 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 )
0 commit comments