diff --git a/backends/apple/coreai/.llms/skills/code-writing.md b/backends/apple/coreai/.llms/skills/code-writing.md index 59e651114f7..b643f037b65 100644 --- a/backends/apple/coreai/.llms/skills/code-writing.md +++ b/backends/apple/coreai/.llms/skills/code-writing.md @@ -41,7 +41,7 @@ repository root, in an env with ExecuTorch installed in editable mode and python -m pytest backends/apple/coreai ``` -Standard pytest options apply, e.g. `-v` or `-k sidecar`. +Standard pytest options apply, e.g. `-v` or `-k manifest`. The real-toolchain AOT test (`CoreAIAOTCompileTest`) is gated on `xcrun coreai-build`; it runs on macOS with the Metal Toolchain and skips diff --git a/backends/apple/coreai/__init__.py b/backends/apple/coreai/__init__.py index 74c8f258708..c1ccddf70b9 100644 --- a/backends/apple/coreai/__init__.py +++ b/backends/apple/coreai/__init__.py @@ -8,7 +8,6 @@ from executorch.backends.apple.coreai.compiler.preprocess import ( AOTCompileConfig, - coreai_sidecar_dir, CoreAIBackend, ) from executorch.backends.apple.coreai.partition.partitioner import CoreAIPartitioner @@ -48,7 +47,6 @@ def get_default_compile_config() -> EdgeCompileConfig: "AOTCompileConfig", "CoreAIBackend", "CoreAIPartitioner", - "coreai_sidecar_dir", "get_default_compile_config", "get_default_passes", ] diff --git a/backends/apple/coreai/compiler/preprocess.py b/backends/apple/coreai/compiler/preprocess.py index 18f7a10f04f..cf2f86224a8 100644 --- a/backends/apple/coreai/compiler/preprocess.py +++ b/backends/apple/coreai/compiler/preprocess.py @@ -7,19 +7,16 @@ # Core AI backend for delegating an EdgeProgram to Apple's Core AI framework # via the ``coreai-torch`` converter. -import contextlib import copy import hashlib import json import logging -import os -import shutil import subprocess from dataclasses import dataclass from enum import Enum from pathlib import Path from tempfile import TemporaryDirectory -from typing import Any, Dict, final, Iterator, List, Optional, Tuple +from typing import Any, Dict, final, List, Optional, Tuple from executorch.backends.apple.coreai.compiler.constants import MAIN_ENTRYPOINT from executorch.backends.apple.coreai.compiler.enumerated_shapes import ( @@ -48,18 +45,9 @@ # are mmap-friendly on device. Applied to every file flattened into the NDS. _ASSET_ALIGNMENT = 16 -# Build-time only: directory where sidecar bundles are written. Read from the -# environment (set it via :func:`coreai_sidecar_dir`) so no build-machine path -# is ever serialized into the .pte. The runtime load directory is a separate, -# runtime-provided concern (default: the .pte's dir). -SIDECAR_DIR_ENV = "COREAI_SIDECAR_DIR" - class COMPILE_SPEC_KEYS(Enum): - # Whether this delegate's asset is delivered as a sidecar bundle (vs - # embedded in the .pte via NamedDataStore). Serialized: the runtime needs - # to know how to load the asset. Carries only the delivery mode, no path. - USES_SIDECAR = "coreai_uses_sidecar" + # Minimum OS # Minimum OS / deployment version. This is a save-time property of the # .aimodel (save_asset(minimum_os=...)) that ALSO feeds coreai-build, so it @@ -91,12 +79,8 @@ class COMPILE_SPEC_KEYS(Enum): class AssetPackaging(str, Enum): # Portable .aimodel embedded in the .pte via NamedDataStore. INLINE = "inline" - # Portable .aimodel written as a sidecar next to the .pte. - SIDECAR = "sidecar" # AOT-compiled per-arch .aimodelc bundles embedded in the .pte. AOT_COMPILED_INLINE = "aot_compiled_inline" - # AOT-compiled per-arch .aimodelc bundles written as a sidecar. - AOT_COMPILED_SIDECAR = "aot_compiled_sidecar" @dataclass(frozen=True) @@ -173,96 +157,12 @@ def _get_compile_spec( return None -def _reject_existing_asset_dir(dest: Path) -> None: - """Refuse to write over an asset directory a previous build left behind. - - Each delegate owns one ``/`` directory, and the hash covers the - ``.aimodel`` only. AOT options are applied after it is computed, so a - rebuild with a different platform or OS floor lands on the same path: left - alone, the manifest would advertise the new options while the bundles on - disk stayed from the earlier build. Two delegates in one build always - differ here, so this only fires across builds. - """ - if dest.exists(): - raise RuntimeError( - f"sidecar asset directory already exists: {dest}\n" - f"A previous build wrote it, and its contents may have been " - f"compiled with different options. Remove it, or point " - f"COREAI_SIDECAR_DIR at a clean directory." - ) - - -@contextlib.contextmanager -def coreai_sidecar_dir(path: str) -> Iterator[None]: - """Set ``COREAI_SIDECAR_DIR`` for the duration of a ``with`` block. - - Sidecar delivery (``uses_sidecar=True``, including AOT+sidecar) requires this - env var to name the build-time output directory. The prior value is - restored on exit. - - The directory must not already hold asset directories: they are build - output, and stale ones are indistinguishable from this build's. Loose - files beside them (``.DS_Store``, a ``.pte``) are left alone. Setting the - env var directly skips this check, but each asset directory is still - guarded individually as it is written. - - Example:: - - with coreai_sidecar_dir("build/model"): - lowered = to_edge_transform_and_lower( - ep, partitioner=[CoreAIPartitioner(uses_sidecar=True)] - ) - """ - existing = Path(path) - if existing.exists() and not existing.is_dir(): - raise RuntimeError(f"sidecar output path is not a directory: {path}") - if existing.is_dir() and any(p.is_dir() for p in existing.iterdir()): - raise RuntimeError( - f"sidecar output directory already holds assets: {path}\n" - f"Assets from an earlier build cannot be told apart from this " - f"one's. Remove the directory or choose another." - ) - prev = os.environ.get(SIDECAR_DIR_ENV) - os.environ[SIDECAR_DIR_ENV] = path - try: - yield - finally: - if prev is None: - os.environ.pop(SIDECAR_DIR_ENV, None) - else: - os.environ[SIDECAR_DIR_ENV] = prev - - def _nds_key(model_hash: str, relative_path: str) -> str: # Namespaced by content hash so identical assets dedup to one entry and the # hash doubles as an on-device cache key. return f"coreai/{model_hash}/{relative_path}" -# Warn at most once per process that a sidecar dir was set but ignored. -_WARNED_SIDECAR_ENV_IGNORED = False - - -def _maybe_warn_sidecar_env_ignored() -> None: - """Soft guardrail: env var set but this delegate uses inline delivery. - - Likely a misconfiguration (user set the sidecar dir but forgot - ``uses_sidecar=True``). Not an error: the env var is process-wide and may - legitimately be set for a different delegate, so we only warn, once. - """ - global _WARNED_SIDECAR_ENV_IGNORED - if _WARNED_SIDECAR_ENV_IGNORED: - return - if os.environ.get(SIDECAR_DIR_ENV): - _WARNED_SIDECAR_ENV_IGNORED = True - logger.warning( - "%s is set but this Core AI delegate uses inline delivery, " - "so the sidecar directory is ignored. Did you mean to pass " - "uses_sidecar=True to CoreAIPartitioner?", - SIDECAR_DIR_ENV, - ) - - def _asset_metadata(staging: Path) -> Dict[str, Any]: """Per-file sizes and a SHA-256 digest per bundle under ``staging``. @@ -297,19 +197,12 @@ def _deliver( model_hash: str, packaging: AssetPackaging, manifest_extra: Dict[str, Any], - sidecar_dir: Optional[str], ) -> PreprocessResult: - """Ship a staging directory of bundles, embedded or on disk. + """Embed a staging directory of bundles in the .pte with its manifest. ``staging`` holds the ``.aimodel`` / ``.aimodelc`` bundles, never their - contents, so both deliveries land on the same ``//`` layout: - inline under the ``coreai/`` NamedDataStore prefix, sidecar under the build - output directory. Keeping one implementation is what stops the two from - drifting apart again. - - The sidecar write is staged and renamed into place, so a failure partway - cannot leave a half-populated ``/`` behind for - :func:`_reject_existing_asset_dir` to trip over on the next build. + contents, so assets land under ``coreai///`` in the + NamedDataStore. """ manifest = { "packaging": packaging.value, @@ -317,24 +210,7 @@ def _deliver( **manifest_extra, **_asset_metadata(staging), } - if sidecar_dir is None: - return _embed_dir_inline(staging, model_hash, manifest) - - dest = Path(sidecar_dir) / model_hash - dest.parent.mkdir(parents=True, exist_ok=True) - _reject_existing_asset_dir(dest) - pending = dest.with_name(f".{model_hash}.partial") - if pending.exists(): - shutil.rmtree(pending) - try: - pending.mkdir(parents=True) - for bundle in sorted(staging.iterdir()): - shutil.move(str(bundle), str(pending / bundle.name)) - pending.rename(dest) - except BaseException: - shutil.rmtree(pending, ignore_errors=True) - raise - return PreprocessResult(processed_bytes=json.dumps(manifest).encode("utf-8")) + return _embed_dir_inline(staging, model_hash, manifest) def _prepare_program_for_conversion(edge_program: ExportedProgram) -> ExportedProgram: @@ -384,8 +260,7 @@ def _embed_dir_inline( """Flatten every file under ``root_dir`` into the NamedDataStore. ``root_dir`` is the directory *containing* the ``.aimodel`` / ``.aimodelc`` - bundles, never a bundle itself, so the bundle name survives into the keys - and the asset reconstructs to the layout the sidecar routes write on disk. + bundles, never a bundle itself, so the bundle name survives into the keys. Keys are ``coreai/{hash}/{relpath}``; ``manifest_extra`` becomes the manifest (packaging, files, archs, ...). @@ -566,16 +441,12 @@ def _compile_aot(program, opts: Dict[str, Any], tmp_dir: Path) -> Tuple[str, Pat class CoreAIBackend(BackendDetails): """AOT lowering of an edge program to a Core AI asset. - Delivery is chosen by compile specs, along two orthogonal axes: + Delivery is chosen by compile specs: the portable ``.aimodel`` (default), + or AOT-compiled per-architecture ``.aimodelc`` bundles (``aot_compile``, via + ``xcrun coreai-build``; architecture selection is one / a list / all). The + bundles are embedded in the ``.pte`` via NamedDataStore. - * **Format**: the portable ``.aimodel`` (default), or AOT-compiled - per-architecture ``.aimodelc`` bundles (``aot_compile``, via - ``xcrun coreai-build``; architecture selection is one / a list / all). - * **Location**: embedded in the ``.pte`` via NamedDataStore (default), or a - ``sidecar`` written to ``$COREAI_SIDECAR_DIR`` (``uses_sidecar``). - - ``processed_bytes`` is always a small JSON manifest naming what/where; the - bytes live in the NamedDataStore (inline) or on disk (sidecar). + ``processed_bytes`` is always a small JSON manifest naming what/where. Runtime execution is not wired up yet. """ @@ -588,20 +459,6 @@ def preprocess( _get_compile_spec(compile_specs, COMPILE_SPEC_KEYS.AOT_COMPILE_CONFIG) is not None ) - uses_sidecar = ( - _get_compile_spec(compile_specs, COMPILE_SPEC_KEYS.USES_SIDECAR) is not None - ) - - # Sidecar delivery (portable or AOT) needs a build-time output dir. - sidecar_dir = None - if uses_sidecar: - sidecar_dir = os.environ.get(SIDECAR_DIR_ENV) - if not sidecar_dir: - raise ValueError( - "sidecar asset delivery requires the " - f"{SIDECAR_DIR_ENV} environment variable to name the " - "build-time output directory (set it via coreai_sidecar_dir)" - ) mutated_inputs = edge_program.graph_signature.user_inputs_to_mutate if mutated_inputs: @@ -643,57 +500,44 @@ def preprocess( # (via save_asset(minimum_os=...)). In the aot-compiled path the temp # .aimodel is discarded, so it keeps the default floor (no # double-specification). - if not uses_sidecar: - _maybe_warn_sidecar_env_ignored() if aot_compiled: return CoreAIBackend._preprocess_aot_compiled( - program, _aot_compile_options(compile_specs), sidecar_dir, bindings + program, _aot_compile_options(compile_specs), bindings ) return CoreAIBackend._preprocess_portable( - program, - _min_os_version(compile_specs), - sidecar_dir, - bindings, + program, _min_os_version(compile_specs), bindings ) # Portable .aimodel delivery. @staticmethod def _preprocess_portable( - program, min_os, sidecar_dir: Optional[str], bindings: Dict[str, Any] + program, min_os, bindings: Dict[str, Any] ) -> PreprocessResult: with TemporaryDirectory() as tmp: model_hash = _save_and_hash(program, Path(tmp) / "model.aimodel", min_os) return _deliver( Path(tmp), model_hash, - AssetPackaging.SIDECAR if sidecar_dir else AssetPackaging.INLINE, + AssetPackaging.INLINE, { **bindings, # relative path the runtime resolves against its base "path": f"{model_hash}/model.aimodel", "min_deployment_version": _os_version_text(min_os), }, - sidecar_dir, ) # AOT-compiled .aimodelc delivery (per architecture). @staticmethod def _preprocess_aot_compiled( - program, - opts: Dict[str, Any], - sidecar_dir: Optional[str], - bindings: Dict[str, Any], + program, opts: Dict[str, Any], bindings: Dict[str, Any] ) -> PreprocessResult: with TemporaryDirectory() as tmp: model_hash, out = _compile_aot(program, opts, Path(tmp)) return _deliver( out, model_hash, - ( - AssetPackaging.AOT_COMPILED_SIDECAR - if sidecar_dir - else AssetPackaging.AOT_COMPILED_INLINE - ), + AssetPackaging.AOT_COMPILED_INLINE, { **bindings, "platform": opts["platform"], @@ -703,5 +547,4 @@ def _preprocess_aot_compiled( for arch, bundle in _compiled_arch_bundles(out) }, }, - sidecar_dir, ) diff --git a/backends/apple/coreai/partition/partitioner.py b/backends/apple/coreai/partition/partitioner.py index d07297e0fd2..30b690d79c2 100644 --- a/backends/apple/coreai/partition/partitioner.py +++ b/backends/apple/coreai/partition/partitioner.py @@ -197,7 +197,6 @@ class CoreAIPartitioner(Partitioner): def __init__( self, *, - uses_sidecar: bool = False, aot_compile_config: Optional[AOTCompileConfig] = None, min_deployment_version: Optional[str] = None, input_enumerations: Optional[ @@ -206,13 +205,6 @@ def __init__( take_over_constant_data: bool = True, take_over_mutable_buffer: bool = True, ) -> None: - # uses_sidecar selects sidecar delivery (vs inline). It is embedded as a - # compile spec because the runtime needs to know how to load the asset, - # but it carries only the mode, no path. The build-time output directory - # comes from the COREAI_SIDECAR_DIR env var (see preprocess.py / - # coreai_sidecar_dir), never a compile spec, so no build-machine path is - # serialized. - # # aot_compile_config requests ahead-of-time ``xcrun coreai-build # compile`` in preprocess, emitting per-architecture ``.aimodelc`` # bundles instead of the portable ``.aimodel``. It is serialized as a @@ -225,8 +217,6 @@ def __init__( # symbols in :meth:`partition` and propagated to each subgraph boundary # in preprocess, so it is not built into a compile spec here. specs = [] - if uses_sidecar: - specs.append(CompileSpec(COMPILE_SPEC_KEYS.USES_SIDECAR.value, b"1")) if min_deployment_version is not None: specs.append( CompileSpec( diff --git a/backends/apple/coreai/test/test_partitioner.py b/backends/apple/coreai/test/test_partitioner.py index 0c3aa3a7679..624048c6c46 100644 --- a/backends/apple/coreai/test/test_partitioner.py +++ b/backends/apple/coreai/test/test_partitioner.py @@ -10,7 +10,6 @@ import torch.nn as nn from executorch.backends.apple.coreai import get_default_compile_config -from executorch.backends.apple.coreai.compiler.preprocess import COMPILE_SPEC_KEYS from executorch.backends.apple.coreai.partition.partitioner import ( _OperatorsSupportedForCoreAIBackend, CoreAIPartitioner, @@ -270,13 +269,6 @@ def forward(self, x, n): class PartitionerCompileSpecTest(unittest.TestCase): - def test_uses_sidecar_spec_is_embedded(self): - # The delivery mode is serialized (the runtime needs it); the build dir - # is not (it is an env var); only the mode flag rides along. - specs = CoreAIPartitioner(uses_sidecar=True).delegation_spec.compile_specs - self.assertEqual([s.key for s in specs], [COMPILE_SPEC_KEYS.USES_SIDECAR.value]) - self.assertNotIn(b"/", specs[0].value) - def test_inline_partitioner_embeds_no_specs(self): self.assertEqual(CoreAIPartitioner().delegation_spec.compile_specs, []) diff --git a/backends/apple/coreai/test/test_preprocess.py b/backends/apple/coreai/test/test_preprocess.py index 27674b27b64..6570dac278b 100644 --- a/backends/apple/coreai/test/test_preprocess.py +++ b/backends/apple/coreai/test/test_preprocess.py @@ -6,15 +6,14 @@ """Tests for ``CoreAIBackend.preprocess`` and its AOT-compile config. -Covers all four asset-delivery combinations: portable ``.aimodel`` vs -AOT-compiled ``.aimodelc``, each either inline (embedded in the .pte) or as a -sidecar, plus ``AOTCompileConfig`` parsing. The compiled-delivery cases mock +Covers both asset formats, portable ``.aimodel`` and AOT-compiled +``.aimodelc``, embedded in the .pte, plus ``AOTCompileConfig`` parsing. The +compiled-delivery cases mock ``coreai-build`` so they run without the Metal Toolchain; the real-toolchain integration is in :class:`CoreAIAOTCompileTest` (gated on ``coreai-build``). """ import json -import os import subprocess import tempfile import unittest @@ -29,7 +28,6 @@ _asset_metadata, AOTCompileConfig, COMPILE_SPEC_KEYS, - coreai_sidecar_dir, CoreAIBackend, ) from executorch.backends.apple.coreai.partition.partitioner import CoreAIPartitioner @@ -84,7 +82,6 @@ def _aot_spec(config: dict) -> CompileSpec: ) -_SIDECAR_SPEC = CompileSpec(COMPILE_SPEC_KEYS.USES_SIDECAR.value, b"1") _MOCK_BUILD = "executorch.backends.apple.coreai.compiler.preprocess._run_coreai_build" @@ -151,10 +148,9 @@ def test_inline_embeds_files_in_nds(self): self.assertEqual(out.external_data, {}) # nothing external for inline def test_inline_keys_keep_the_bundle_directory(self): - """Embedded keys must reconstruct to the shape sidecar writes on disk. + """The bundle name has to survive flattening into NamedDataStore keys. - The bundle name has to survive flattening, otherwise unpacking cannot - tell that these files belong to a ``.aimodel``. + Otherwise unpacking cannot tell that these files belong to a ``.aimodel``. """ manifest = json.loads( CoreAIBackend.preprocess(_edge_program(), []).processed_bytes @@ -164,80 +160,6 @@ def test_inline_keys_keep_the_bundle_directory(self): manifest["files"], ) - def test_sidecar_writes_bundle_and_does_not_embed(self): - with tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - result = CoreAIBackend.preprocess(_edge_program(), [_SIDECAR_SPEC]) - manifest = json.loads(result.processed_bytes) - self.assertEqual(manifest["packaging"], "sidecar") - # Only the relative, hash-keyed path is referenced (no build path). - self.assertEqual(manifest["path"], f"{manifest['hash']}/model.aimodel") - self.assertNotIn(d, result.processed_bytes.decode()) - bundle = Path(d) / manifest["path"] - self.assertTrue(bundle.is_dir()) - self.assertTrue((bundle / "main.mlirb").exists()) - self.assertIsNone(result.data_store_output) # nothing embedded - - def test_sidecar_without_env_var_raises(self): - # uses_sidecar set but COREAI_SIDECAR_DIR unset -> fail fast. - env = {k: v for k, v in os.environ.items() if k != "COREAI_SIDECAR_DIR"} - with mock.patch.dict(os.environ, env, clear=True): - with self.assertRaises(ValueError): - CoreAIBackend.preprocess(_edge_program(), [_SIDECAR_SPEC]) - - def test_inline_with_env_var_warns(self): - # Env var set but delegate is inline -> soft warning, not an error. - import executorch.backends.apple.coreai.compiler.preprocess as cp - - with tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - cp._WARNED_SIDECAR_ENV_IGNORED = False # allow the once-warning - with self.assertLogs(cp.logger, level="WARNING") as cm: - result = CoreAIBackend.preprocess(_edge_program(), []) - self.assertEqual(json.loads(result.processed_bytes)["packaging"], "inline") - self.assertTrue(any("uses_sidecar=True" in m for m in cm.output)) - - def test_sidecar_dir_rejects_a_file(self): - with tempfile.TemporaryDirectory() as d: - path = Path(d) / "notadir" - path.write_text("x") - with self.assertRaisesRegex(RuntimeError, "not a directory"): - with coreai_sidecar_dir(str(path)): - pass - - def test_sidecar_dir_ignores_loose_files(self): - """Only asset directories conflict; ``.DS_Store`` must not block a build.""" - with tempfile.TemporaryDirectory() as d: - (Path(d) / ".DS_Store").write_bytes(b"\x00") - with coreai_sidecar_dir(d): - result = CoreAIBackend.preprocess(_edge_program(), [_SIDECAR_SPEC]) - self.assertEqual(json.loads(result.processed_bytes)["packaging"], "sidecar") - - def test_sidecar_dir_rejects_existing_assets(self): - with tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - CoreAIBackend.preprocess(_edge_program(), [_SIDECAR_SPEC]) - with self.assertRaisesRegex(RuntimeError, "already holds assets"): - with coreai_sidecar_dir(d): - pass - - def test_coreai_sidecar_dir_sets_and_restores_env(self): - self.assertNotIn("COREAI_SIDECAR_DIR", os.environ) - with coreai_sidecar_dir("/tmp/scoped"): - self.assertEqual(os.environ["COREAI_SIDECAR_DIR"], "/tmp/scoped") - self.assertNotIn("COREAI_SIDECAR_DIR", os.environ) # restored on exit - - def test_end_to_end_sidecar(self): - with tempfile.TemporaryDirectory() as d: - ep = torch.export.export(nn.Linear(8, 8).eval(), (torch.randn(2, 8),)) - with coreai_sidecar_dir(d): - to_edge_transform_and_lower( - ep, partitioner=[CoreAIPartitioner(uses_sidecar=True)] - ) - bundles = list(Path(d).glob("*/model.aimodel")) - self.assertEqual(len(bundles), 1) - self.assertTrue((bundles[0] / "main.mlirb").exists()) - class ManifestBindingsTest(unittest.TestCase): def test_names_follow_boundary_order_past_single_digits(self): @@ -262,20 +184,16 @@ def test_sizes_and_digests_cover_every_delivered_file(self, _build): ([], {"model.aimodel"}), ([aot], {"model.h15g.aimodelc", "model.h16.aimodelc"}), ): - with self.subTest(aot=bool(specs)), tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - manifest = json.loads( - CoreAIBackend.preprocess( - _edge_program(), specs + [_SIDECAR_SPEC] - ).processed_bytes - ) - root = Path(d) / manifest["hash"] - on_disk = { - p.relative_to(root).as_posix(): p.stat().st_size - for p in root.rglob("*") - if p.is_file() + with self.subTest(aot=bool(specs)): + result = CoreAIBackend.preprocess(_edge_program(), specs) + manifest = json.loads(result.processed_bytes) + out = result.data_store_output + prefix = f"coreai/{manifest['hash']}/" + embedded = { + key[len(prefix) :]: len(out.buffers[entry.buffer_index]) + for key, entry in out.pte_data.items() } - self.assertEqual(manifest["files"], on_disk) + self.assertEqual(manifest["files"], embedded) self.assertEqual(set(manifest["bundle_digests"]), bundles) def test_digest_changes_with_file_contents(self): @@ -400,55 +318,14 @@ def test_inline_embeds_compiled_bundles(self, _build): self.assertIsNotNone(result.data_store_output) @mock.patch(_MOCK_BUILD, side_effect=_fake_run_coreai_build) - def test_sidecar_writes_compiled_bundles(self, _build): - with tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - result = CoreAIBackend.preprocess( - _edge_program(), - [_aot_spec({"architectures": ["h15g"]}), _SIDECAR_SPEC], - ) - manifest = json.loads(result.processed_bytes) - self.assertEqual(manifest["packaging"], "aot_compiled_sidecar") - self.assertEqual(list(manifest["archs"]), ["h15g"]) - self.assertIsNone(result.data_store_output) # bundles on disk only - self.assertNotIn(d, result.processed_bytes.decode()) - for rel in manifest["archs"].values(): - bundle = Path(d) / rel - self.assertTrue(bundle.is_dir()) - self.assertTrue(str(bundle).endswith(".aimodelc")) - - @mock.patch(_MOCK_BUILD, side_effect=_fake_run_coreai_build) - def test_archs_map_is_the_same_inline_and_sidecar(self, _build): - """One shape for ``archs``, whatever the delivery. - - Inline could derive paths from the hash, but emitting the same map means - a consumer never has to branch on ``packaging`` to read it. For inline - the value is the key under ``coreai/``; for sidecar it is relative to - the sidecar dir. - """ + def test_archs_map_points_at_embedded_bundles(self, _build): + """Each ``archs`` value is the bundle's key under ``coreai/``.""" config = {"platform": "iOS", "architectures": ["h15g", "h16"]} - inline = json.loads( - CoreAIBackend.preprocess( - _edge_program(), [_aot_spec(config)] - ).processed_bytes - ) - with tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - sidecar = json.loads( - CoreAIBackend.preprocess( - _edge_program(), [_aot_spec(config), _SIDECAR_SPEC] - ).processed_bytes - ) - self.assertIsInstance(inline["archs"], dict) - self.assertEqual(inline["archs"], sidecar["archs"]) - for arch, rel in inline["archs"].items(): - self.assertEqual(rel, f"{inline['hash']}/model.{arch}.aimodelc") - self.assertIn( - f"coreai/{rel}/model.mil", - CoreAIBackend.preprocess( - _edge_program(), [_aot_spec(config)] - ).data_store_output.pte_data, - ) + result = CoreAIBackend.preprocess(_edge_program(), [_aot_spec(config)]) + manifest = json.loads(result.processed_bytes) + for arch, rel in manifest["archs"].items(): + self.assertEqual(rel, f"{manifest['hash']}/model.{arch}.aimodelc") + self.assertIn(f"coreai/{rel}/model.mil", result.data_store_output.pte_data) @mock.patch(_MOCK_BUILD, side_effect=_fake_run_coreai_build) def test_defaults_to_all_architectures(self, _build): @@ -460,82 +337,19 @@ def test_defaults_to_all_architectures(self, _build): self.assertTrue(manifest["archs"]) @mock.patch(_MOCK_BUILD, side_effect=_fake_run_coreai_build) - def test_sidecar_rebuild_with_new_options_is_rejected(self, _build): - """A second build must not quietly inherit the first one's bundles. - - The asset directory is keyed on the .aimodel hash, which does not cover - platform, OS floor or compute preference, so a rebuild with different - options lands on the same path. - """ - with tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - CoreAIBackend.preprocess( - _edge_program(), - [ - _aot_spec({"platform": "iOS", "architectures": ["h15g"]}), - _SIDECAR_SPEC, - ], - ) - with self.assertRaisesRegex(RuntimeError, "already exists"): - CoreAIBackend.preprocess( - _edge_program(), - [ - _aot_spec({"platform": "macOS", "architectures": ["h15g"]}), - _SIDECAR_SPEC, - ], - ) - - @mock.patch(_MOCK_BUILD, side_effect=_fake_run_coreai_build) - def test_failed_write_leaves_no_asset_directory(self, _build): - """A half-written asset dir would block every later build. - - ``_reject_existing_asset_dir`` hard-fails on an existing directory, so - the write is staged and renamed; a failure partway must clean up. - """ - with tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - with mock.patch( - "executorch.backends.apple.coreai.compiler.preprocess" - ".shutil.move", - side_effect=OSError("no space left on device"), - ): - with self.assertRaises(OSError): - CoreAIBackend.preprocess( - _edge_program(), - [_aot_spec({"architectures": ["h15g"]}), _SIDECAR_SPEC], - ) - # Nothing left behind, so the retry below is not blocked. - self.assertEqual(list(Path(d).iterdir()), []) - result = CoreAIBackend.preprocess( - _edge_program(), - [_aot_spec({"architectures": ["h15g"]}), _SIDECAR_SPEC], - ) - manifest = json.loads(result.processed_bytes) - bundle = Path(d) / next(iter(manifest["archs"].values())) - self.assertTrue(bundle.is_dir()) - - @mock.patch(_MOCK_BUILD, side_effect=_fake_run_coreai_build) - def test_two_delegates_write_separate_asset_dirs(self, _build): - """A graph break gives two delegates, which must not collide. - - They share one sidecar directory but differ in hash, so the - already-exists guard has to stay quiet here. - """ - with tempfile.TemporaryDirectory() as d: - with coreai_sidecar_dir(d): - lowered = _lower_with_break( - CoreAIPartitioner( - uses_sidecar=True, - aot_compile_config=AOTCompileConfig( - platform="iOS", architectures=["h15g"] - ), - ) - ) - lbms = get_lowered_backend_modules(lowered.exported_program().graph_module) - self.assertEqual(len(lbms), 2) - hashes = {json.loads(bytes(lbm.processed_bytes))["hash"] for lbm in lbms} - self.assertEqual(len(hashes), 2) - self.assertEqual(len(list(Path(d).iterdir())), 2) + def test_two_delegates_get_separate_asset_keys(self, _build): + """A graph break gives two delegates, whose assets must not collide.""" + lowered = _lower_with_break( + CoreAIPartitioner( + aot_compile_config=AOTCompileConfig( + platform="iOS", architectures=["h15g"] + ), + ) + ) + lbms = get_lowered_backend_modules(lowered.exported_program().graph_module) + self.assertEqual(len(lbms), 2) + hashes = {json.loads(bytes(lbm.processed_bytes))["hash"] for lbm in lbms} + self.assertEqual(len(hashes), 2) def _lower_linear(partitioner): @@ -569,26 +383,6 @@ def test_aot_inline_embeds_aimodelc(self): self.assertIsNotNone(lbm.named_data_store_output) self.assertGreater(len(bytes(lowered.to_executorch().buffer)), 0) - def test_aot_sidecar_writes_aimodelc_and_keeps_pte_small(self): - with tempfile.TemporaryDirectory() as sidecar: - with coreai_sidecar_dir(sidecar): - lowered = _lower_linear( - CoreAIPartitioner( - aot_compile_config=AOTCompileConfig(platform="macOS"), - uses_sidecar=True, - ) - ) - lbm, manifest = _lowered_manifest(lowered) - - self.assertEqual(manifest["packaging"], "aot_compiled_sidecar") - self.assertGreaterEqual(len(manifest["archs"]), 1) - # Sidecar: compiled bundle *contents* must not be embedded in the .pte. - self.assertIsNone(lbm.named_data_store_output) - for rel in manifest["archs"].values(): - bundle = os.path.join(sidecar, rel) - self.assertTrue(os.path.isdir(bundle), bundle) - self.assertTrue(bundle.endswith(".aimodelc")) - def test_delegates_present(self): lowered = _lower_linear( CoreAIPartitioner(aot_compile_config=AOTCompileConfig(platform="macOS"))