Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions backends/apple/coreai/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,14 @@ endforeach()
enable_language(OBJCXX)

set(_coreai_runtime_sources
runtime/coreai_backend.mm runtime/coreai_assets.mm
runtime/coreai_storage.mm runtime/coreai_bookmarks.mm
runtime/coreai_load_coordinator.mm runtime/coreai_pte.cpp
runtime/coreai_backend.mm
runtime/coreai_assets.mm
runtime/coreai_storage.mm
runtime/coreai_bookmarks.mm
runtime/coreai_cache_key.mm
runtime/coreai_load_coordinator.mm
runtime/coreai_pte.cpp
runtime/coreai_cache.mm
)

# Keep ARC and C++ exception settings away from the private Swift module.
Expand All @@ -56,6 +61,7 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
runtime/test/coreai_filesystem_fixture.mm
runtime/test/coreai_source_test.mm
runtime/test/coreai_bookmark_test.mm
runtime/test/coreai_key_format_test.mm
runtime/test/coreai_source_fixture.mm
runtime/test/coreai_bookmark_fixture.mm
runtime/test/coreai_fake_loader.mm
Expand All @@ -64,6 +70,8 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
runtime/test/coreai_delegate_test.mm
runtime/test/coreai_pte_test.mm
runtime/test/coreai_pte_fixture.mm
runtime/test/coreai_cache_test.mm
runtime/test/coreai_eviction_test.mm
runtime/ETCoreAITensor.mm
${_coreai_runtime_sources}
)
Expand All @@ -73,12 +81,15 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
# The consolidated runtime target is created after this directory.
if(EXECUTORCH_BUILD_SHARED)
set(_coreai_host_runtime executorch_shared)
set(_coreai_host_data_loader "")
else()
set(_coreai_host_runtime executorch)
set(_coreai_host_data_loader extension_data_loader)
endif()
target_link_libraries(
coreai_host_test PRIVATE ${_coreai_host_runtime} program_schema
GTest::gtest ${COREAI_FOUNDATION_FRAMEWORK}
coreai_host_test
PRIVATE ${_coreai_host_runtime} ${_coreai_host_data_loader} program_schema
GTest::gtest ${COREAI_FOUNDATION_FRAMEWORK}
)
add_test(NAME coreai_host_test COMMAND coreai_host_test)
set_tests_properties(coreai_host_test PROPERTIES TIMEOUT 120)
Expand Down Expand Up @@ -393,9 +404,11 @@ endif()
if(EXECUTORCH_BUILD_SHARED)
set(_coreai_library_type SHARED)
set(_coreai_runtime executorch_shared)
set(_coreai_data_loader "")
else()
set(_coreai_library_type STATIC)
set(_coreai_runtime executorch)
set(_coreai_data_loader extension_data_loader)
endif()
add_library(
coreaidelegate ${_coreai_library_type} ${_coreai_runtime_sources}
Expand All @@ -408,8 +421,8 @@ coreai_require_os27(coreaidelegate)
target_include_directories(coreaidelegate PUBLIC ${_common_include_directories})
add_dependencies(coreaidelegate coreai_swift)
target_link_libraries(
coreaidelegate PRIVATE coreai_swift ${_coreai_runtime} program_schema
executorch::coreai_dependencies
coreaidelegate PRIVATE coreai_swift ${_coreai_runtime} ${_coreai_data_loader}
program_schema executorch::coreai_dependencies
)
executorch_target_link_options_shared_lib(coreaidelegate)
if(EXECUTORCH_BUILD_SHARED)
Expand All @@ -434,6 +447,12 @@ target_link_libraries(
coreai_runtime_smoke PRIVATE coreaidelegate ${_coreai_runtime}
)

install(
FILES runtime/coreai_cache.h
DESTINATION
${CMAKE_INSTALL_INCLUDEDIR}/executorch/backends/apple/coreai/runtime
)

install(
TARGETS coreaidelegate coreai_swift
EXPORT ExecuTorchTargets
Expand Down
184 changes: 139 additions & 45 deletions backends/apple/coreai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ are reported separately from an absent architecture entry.

Both formats can require device specialization. Every load first attempts SDK
bookmark restoration. A missing bookmark or SDK-confirmed cache miss falls back
to source materialization and persistent specialization, then binds the requested
to source materialization and cached specialization, then binds the requested
function and ordered I/O on that same acquired model.

### Asset storage
Expand Down Expand Up @@ -171,24 +171,49 @@ validating source assets only when recovery requires them. There is no synthetic
zero-input inference at initialization. SDK specialization and function resource
loading can still be substantial.

The runtime always uses `AIModelCache.default` with persistent retention and
stores an opaque bookmark for direct restoration. `coreai_assets_dir` does not
choose the SDK artifact directory. The backend does not inspect SDK-private
files, alter their backup flags, or use an App Group cache.

Each partition derives a key from its selected export digest/bundle, source/AOT
mode, platform, Core AI device architecture, default SDK cache namespace,
versioned default options and persistent policy. Function bindings and delivery
location are excluded. No model bytes are hashed at runtime. Each key determines
a raw bookmark in `bookmarks`, a bundle directory in `staging`, and one of
256 shared coordination files in `locks`. There is no PTE-wide bookmark list or
serialized lifecycle record.
The runtime always uses `AIModelCache.default` with storage-pressure-only purging
and stores an opaque bookmark for direct restoration. Removing staged source does
not trigger SDK purging, but the SDK may reclaim artifacts when storage is low.
A later bookmark miss rematerializes source and specializes again, so packaged
named data must remain available. This policy applies when specializing; restoring
an existing bookmark does not update that entry's retention policy.
`coreai_assets_dir` does not choose the SDK artifact directory. The backend does
not inspect SDK-private files, alter their backup flags, or use an App Group cache.

Each partition uses the shared key builder to produce
`<model-hash>-<device>-<specialization-hash>`. The model component identifies
the format version, selected export digest/bundle, and source/AOT mode. The device
component contains filename-safe platform and Core AI architecture names,
separated by an underscore, not an individual machine identifier. The
specialization component hashes the loader's stable specialization descriptor.
Both hashes are lowercase SHA-256 values. Function bindings and delivery location
are excluded, and no model bytes are hashed at runtime.

The Swift loader owns an immutable snapshot of the actual SDK specialization
options. Its descriptor is derived from that snapshot, and specialization uses
those same options. Options equal to SDK `.default` produce exactly the UTF-8
string `default`. Other snapshots encode a versioned JSON descriptor with sorted
allowed compute units, the preferred compute unit, and frequent-reshape behavior.
The coordinator reads the descriptor once before lookup; it cannot independently
select an options label. Future configurable SDK fields must be represented in
the descriptor schema and tested before use. No public specialization option is
exposed yet. SDK cache selection and the storage-pressure-only purge policy
remain fixed and are separate from specialization identity.

The cache-key module owns hashing and filename interpretation, bookmark storage
owns locked I/O and bookmark/staging inventory, and the coordinator owns SDK
acquisition and eviction ordering. PTE clearing selects model prefixes rather
than reconstructing keys from today's loader options.

Each composite key determines a raw bookmark in `bookmarks`, a bundle directory
in `staging`, and one of 256 shared coordination files in `locks`. No metadata
sidecars or PTE-wide bookmark lists are written.

A healthy bookmark hit does not consult NDS and attempts to clean leftover
staging. On a missing bookmark or confirmed SDK miss, the coordinator prepares
the source, specializes it and atomically writes the returned bookmark bytes.
After durable publication, it attempts to remove the entire keyed staging
directory while holding the key's stripe lock. Persistent SDK caching supports
directory while holding the key's stripe lock. The SDK cache policy supports
inference and bookmark restoration without that extracted copy.
Keep the packaged named data available: a later SDK cache miss requires source
materialization again. There is no secondary source-URL cache lookup.
Expand All @@ -204,12 +229,11 @@ The backend does not track historical or unrecorded SDK entries.
Raw bookmarks have an 8 MiB backend allocation limit, not an asserted SDK format
maximum, and are published atomically because losing one orphans an SDK entry.
A fixed pool of at most 256 disk locks coordinates loading and eviction across
threads and processes. The first byte of the validated cache key selects
`stripe-v1-00.lock` through `stripe-v1-ff.lock` within the lock directory. Files
are created on demand, so new model versions do not grow the pool beyond 256.
The mapping is part of the on-disk format and is not configurable. Different
keys on the same stripe serialize; different stripes remain independent.
Bookmarks and staging remain separate for every key. Runtime
threads and processes. The first byte of a SHA-256 digest of the validated full
cache key selects its stripe. Files are created on demand, so new model versions
do not grow the pool beyond 256. The mapping is part of the on-disk format and is
not configurable. Different keys on the same stripe serialize; different stripes
remain independent. Bookmarks and staging remain separate for every key. Runtime
operations hold only one stripe at a time.

Lock ownership is maintained by the OS, and acquiring a lock does not flush its
Expand All @@ -218,12 +242,95 @@ clearing or after process exit, so waiters and future openers keep using the
same inode. Removing the assets root externally requires all loads, sessions
and maintenance to stop.

Upgrading from per-key lock files requires stopping all processes using the
root and upgrading them together. Old and new locking schemes must not run
concurrently against the same root. Legacy per-key lock files are ignored and
left untouched; they can be removed separately during a fully quiescent offline
cleanup. The 256-file bound applies to the new stripe pool, not pre-existing
legacy files. Bookmarks and staged bundles need no migration.
Changing cache-key formats requires stopping all processes using the root and
upgrading them together. Different key formats and lock mappings must not run
concurrently against the same root. Older single-hash bookmarks, metadata, and
staged bundles are not migrated or discovered by these maintenance APIs. Retire
old entries using their version's maintenance API while quiescent. Pre-existing
per-key lock files are ignored and are outside the 256-file stripe-pool bound.

### Lifecycle logging

Build with `EXECUTORCH_ENABLE_LOGGING=ON` and `EXECUTORCH_LOG_LEVEL=Info` to
observe cache lookup, bookmark restoration hits, missing-bookmark versus
SDK-confirmed misses, specialization start/success, durable bookmark saves, and
cache-clearing milestones. Each message carries the cache key so events from
multiple partitions can be correlated. These messages do not include bookmark
contents, model data, or per-inference logs.

A cache hit means that the SDK successfully restored a model from its bookmark.
Specialization start means a request to the SDK's default cache; the
SDK does not report whether that request compiled afresh or reused an internal
artifact. The durable-save message confirms local bookmark publication,
including its final directory flush, not independent verification of SDK-private
artifact storage. During clearing, SDK entry absence and successful local
cleanup are reported separately. Failed operations do not emit their success
messages, and clearing an absent entry remains quiet.

### Clearing cached assets

The public C++ maintenance APIs are in `executorch::backends::coreai`:

```cpp
runtime::Error clear_cache(const char* coreai_assets_dir = nullptr);
runtime::Error clear_cache_for_pte(
runtime::DataLoader& pte, const char* coreai_assets_dir = nullptr);
runtime::Error clear_cache_for_pte(
const char* pte_path, const char* coreai_assets_dir = nullptr);
```

All three APIs use the same default assets directory as backend initialization
when the directory is omitted or null. A supplied path must be absolute; invalid
paths fail without falling back to the default. If loading used a custom root,
pass that same root when clearing; the PTE does not store runtime overrides.

Root-wide clearing visits keyed bookmarks and staged directories. PTE clearing
collects model hashes for every declared bundle in every Core AI delegate across
all methods. It inventories the cache filenames once and matches the complete
model component of each valid composite key, finding every recorded device and
specialization variant without knowing which options were previously used.
Manifests must be structurally valid before any entry is cleared; their minimum
OS and runtime compatibility do not restrict maintenance. Discovery requires
neither metadata reads, SDK availability, nor an architecture query, and does not
load models or materialize assets. Actual SDK eviction still requires a usable
bookmark and an available loader.

These synchronous calls require callers to unload affected models and prevent
concurrent loads, inference and maintenance across processes. Stripe locks are
not a whole-root barrier. Clearing first deletes the recorded SDK entry, then
staging, then its bookmark. Busy or unknown SDK errors preserve the files;
SDK-confirmed absence permits cleanup. Filesystem errors may leave partial
cleanup, but the bookmark is retained until staging removal succeeds so cleanup
can be retried. Independent keys are still attempted in sorted order, and the
first key error is returned. Each removal syncs the directory it changed. A
missing root or absent entry is a noncreating no-op.

Everything under a keyed `staging/<key>` directory belongs to the backend and is
removed with that key. Outside keyed entries, clearing does not delete PTEs,
other files or the stable lock files. Unkeyed interrupted-staging directories
also remain untouched. Bookmarks are SDK eviction handles, not weights. For an
existing entry with a missing or structurally invalid bookmark, clearing logs an
Info message with the cache key, attempts local cleanup, and continues to other keys.
The message states that SDK eviction could not be confirmed; bookmark contents
are not logged. Absent entries remain quiet no-ops. I/O failures and SDK eviction
errors still preserve bookmarks for retry. Lost bookmarks can leave SDK entries
that these APIs cannot target. No history of removed entries is retained.
Copied or reused exported artifacts can share keys; clearing them also removes
the other copy's warm-cache benefit. Subsequent loads can specialize again.

### Persistent SDK Rejection

SDK errors are not classified as transient or permanent.
Busy errors and all unclassified eviction/restore errors retain the local
bookmark for retry; only successful eviction or a confirmed SDK miss permits
normal removal. If retries keep failing, first stop every process using the
assets root. Back up or quarantine only the affected key's bookmark and keyed
staging outside that root, leaving the stable lock files in place. A later load
can attempt cold acquisition and publish a new bookmark.
This manual recovery does not confirm SDK eviction or guarantee disk-space
reclamation: keep the backup because it may contain the only remaining eviction
handle. Do not perform manual recovery while loading, inference, or maintenance
is running.

### Runtime options

Expand Down Expand Up @@ -252,7 +359,8 @@ have a 255-byte limit.
The private Swift module `CoreAIBridge` (`runtime/ETCoreAIModel.swift`) wraps
the `CoreAI` framework behind the Objective-C protocols in
`runtime/ETCoreAIBridge.h`. It restores models from bookmarks, specializes source
bundles with the persistent default SDK cache and binds named functions with
bundles with the default SDK cache and storage-pressure-only purging, and binds
named functions with
ordered inputs and outputs. Asynchronous SDK calls run in detached tasks and
report through completion blocks; function binding completes synchronously.

Expand Down Expand Up @@ -294,23 +402,8 @@ Ninja builds use one architecture per build directory. Current runtime support
covers arm64 macOS and iOS device builds. x86_64 builds are blocked by Swift
`Float16` availability, and the tested iOS simulator SDKs do not contain Core AI.

### Lifecycle logging

Build with `EXECUTORCH_ENABLE_LOGGING=ON` and `EXECUTORCH_LOG_LEVEL=Info` to
observe cache lookup, bookmark restoration hits, missing-bookmark versus
SDK-confirmed misses, specialization start/success, and durable bookmark saves. Each message carries the cache key so events from
multiple partitions can be correlated. These messages do not include bookmark
contents, model data, or per-inference logs.

A cache hit means that the SDK successfully restored a model from its bookmark.
Specialization start means a request to the SDK's persistent default cache; the
SDK does not report whether that request compiled afresh or reused an internal
artifact. The durable-save message confirms local bookmark publication,
including its final directory flush, not independent verification of SDK-private
artifact storage. Failed operations do not emit their success messages.

The delegate does not require `EXECUTORCH_BUILD_EXTENSION_DATA_LOADER`.
Enable it for consumers such as `Module` that load program data.
Enable `EXECUTORCH_BUILD_EXTENSION_DATA_LOADER` for the public PTE-file
maintenance API and consumers such as `Module`.
Link the CMake target `coreaidelegate`, not just its archive filename. Its
transitive dependencies supply Swift and Foundation/CoreAI linkage, and its
link interface retains static backend registration. With
Expand All @@ -323,7 +416,8 @@ Select the Apple SDK/toolchain when configuring the consumer project.
SwiftPM and XCFramework distribution are not integrated yet.

The `coreai_runtime_smoke` target checks final linkage of a C++ consumer,
including Swift dependencies and backend registration retention. With
including Swift dependencies, backend registration retention, and the public
cache-clearing overloads. With
`EXECUTORCH_BUILD_TESTS=ON` it is built; otherwise it is excluded from the
default build. SDK-linked tests are registered with CTest only for native macOS
builds when the host meets the target's deployment minimum (at least 27.0).
Expand Down
6 changes: 5 additions & 1 deletion backends/apple/coreai/runtime/ETCoreAIBridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,13 @@ typedef void (^ETCoreAIAcquisitionCompletion)(
NSError* _Nullable error);

@protocol ETCoreAIModelLoading<NSObject>
// Stable for this loader's lifetime; derived from its actual specialization
// options.
@property(nonatomic, readonly, copy) NSString* specializationIdentifier;
- (void)restoreModelFromBookmark:(NSData*)bookmark
completion:(ETCoreAIAcquisitionCompletion)completion;
// Uses only the SDK default cache and persistent policy. Never returns Miss.
// Uses the SDK default cache with storage-pressure-only purging. Never returns
// Miss.
- (void)specializeModelAtURL:(NSURL*)url
completion:(ETCoreAIAcquisitionCompletion)completion;
// Nil error means SDK-confirmed deletion. No busy/absent classification is
Expand Down
Loading
Loading