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
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -854,6 +854,7 @@ endif()

if(EXECUTORCH_BUILD_COREAI)
add_subdirectory(${CMAKE_CURRENT_SOURCE_DIR}/backends/apple/coreai)
list(APPEND _executorch_backends coreaidelegate)
endif()

if(EXECUTORCH_BUILD_MLX)
Expand Down
88 changes: 68 additions & 20 deletions backends/apple/coreai/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,26 @@ endforeach()

enable_language(OBJCXX)

# The consolidated shared runtime target is created after this directory.
if(EXECUTORCH_BUILD_SHARED)
set(_coreai_library_type SHARED)
set(_coreai_runtime executorch_shared)
else()
set(_coreai_library_type STATIC)
set(_coreai_runtime executorch)
endif()

set(_coreai_runtime_sources
runtime/coreai_backend.mm
runtime/coreai_bookmarks.mm
runtime/coreai_cache_key.mm
runtime/coreai_cache_root.mm
runtime/coreai_error.mm
runtime/coreai_fault_injection.mm
runtime/coreai_load_coordinator.mm
runtime/coreai_manifest.mm
runtime/coreai_source_bundle.mm
runtime/coreai_tensor_io.mm
runtime/coreai_validation.mm
runtime/coreai_storage.mm
)
Expand Down Expand Up @@ -70,14 +82,16 @@ if(EXECUTORCH_BUILD_TESTS AND CMAKE_SYSTEM_NAME STREQUAL "Darwin")
runtime/test/coreai_fake_loader.mm
runtime/test/coreai_acquisition_test.mm
runtime/test/coreai_acquisition_fixture.mm
runtime/test/coreai_delegate_fixture.mm
runtime/test/coreai_delegate_test.mm
runtime/ETCoreAITensor.mm
${_coreai_runtime_sources}
)
coreai_configure_objc_target(coreai_host_test)
target_compile_definitions(coreai_host_test PRIVATE COREAI_ASSETS_TESTING=1)
find_library(COREAI_FOUNDATION_FRAMEWORK Foundation REQUIRED)
target_link_libraries(
coreai_host_test PRIVATE executorch GTest::gtest
coreai_host_test PRIVATE ${_coreai_runtime} GTest::gtest
${COREAI_FOUNDATION_FRAMEWORK}
)
add_test(NAME coreai_host_test COMMAND coreai_host_test)
Expand Down Expand Up @@ -346,24 +360,21 @@ if(EXECUTORCH_BUILD_TESTS
PROPERTIES TIMEOUT 600 ENVIRONMENT
"COREAI_BRIDGE_FIXTURE_DIR=${_coreai_fixture_dir}"
)
add_executable(
coreai_sdk_source_lifetime_test
runtime/test/SDKSourceLifetimeTest.swift runtime/ETCoreAIModel.swift
runtime/ETCoreAITensor.mm
)
set_target_properties(
coreai_sdk_source_lifetime_test PROPERTIES Swift_LANGUAGE_VERSION 6
)
coreai_configure_objc_target(coreai_sdk_source_lifetime_test)
coreai_require_os27(coreai_sdk_source_lifetime_test)
target_compile_options(
coreai_sdk_source_lifetime_test
PRIVATE
"$<$<COMPILE_LANGUAGE:Swift>:-parse-as-library;-import-objc-header;${CMAKE_CURRENT_SOURCE_DIR}/runtime/ETCoreAIBridge.h>"
)
target_link_libraries(
coreai_sdk_source_lifetime_test PRIVATE executorch::coreai_dependencies
)
endif()
add_executable(
coreai_sdk_source_lifetime_test
runtime/test/SDKSourceLifetimeTest.swift runtime/ETCoreAIModel.swift
runtime/ETCoreAITensor.mm
)
coreai_configure_objc_target(coreai_sdk_source_lifetime_test)
coreai_configure_swift_target(
coreai_sdk_source_lifetime_test coreai_sdk_source_lifetime_test
)
target_compile_options(
coreai_sdk_source_lifetime_test
PRIVATE "$<$<COMPILE_LANGUAGE:Swift>:-parse-as-library>"
)
if(_coreai_run_sdk_tests)
add_test(
NAME coreai_sdk_source_lifetime_test
COMMAND
Expand All @@ -375,8 +386,45 @@ if(EXECUTORCH_BUILD_TESTS
endif()
endif()

add_library(
coreaidelegate ${_coreai_library_type} ${_coreai_runtime_sources}
$<TARGET_OBJECTS:coreai_bridge_obj>
)
coreai_configure_objc_target(coreaidelegate)
# The delegate contains no Swift sources; its archive is assembled by C++.
set_target_properties(coreaidelegate PROPERTIES LINKER_LANGUAGE CXX)
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}
executorch::coreai_dependencies
)
executorch_target_link_options_shared_lib(coreaidelegate)
if(EXECUTORCH_BUILD_SHARED)
set_target_properties(
coreaidelegate PROPERTIES OUTPUT_NAME executorch_backend_coreai
)
executorch_target_soname_policy(coreaidelegate)
executorch_target_shipped_runtime_path(coreaidelegate)
endif()

# A consumer is necessary to check Swift linkage and registration retention.
add_executable(coreai_runtime_smoke runtime/test/runtime_smoke.cpp)
if(_coreai_run_sdk_tests)
add_test(NAME coreai_runtime_smoke COMMAND coreai_runtime_smoke)
endif()
if(NOT EXECUTORCH_BUILD_TESTS)
set_target_properties(coreai_runtime_smoke PROPERTIES EXCLUDE_FROM_ALL TRUE)
endif()
coreai_require_os27(coreai_runtime_smoke)
set_target_properties(coreai_runtime_smoke PROPERTIES LINKER_LANGUAGE CXX)
target_link_libraries(
coreai_runtime_smoke PRIVATE coreaidelegate ${_coreai_runtime}
)

install(
TARGETS coreai_swift
TARGETS coreaidelegate coreai_swift
EXPORT ExecuTorchTargets
LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}
Expand Down
123 changes: 115 additions & 8 deletions backends/apple/coreai/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@ with one bundle's path, digest, and files. Neither type can be constructed
without validation; filesystem materialization and SDK host probing remain
outside the manifest parser.

The experimental `CoreAIBackend` runtime supports stateless FP32/FP16 tensor
models in both formats. ExecuTorch tensors must be contiguous. Output shapes
must fit the executor's declared resize and capacity constraints. Stateful
functions, image values, other dtypes, and interleaved layouts are not supported.
Calls on one session must be serialized by the caller.

Supported inputs are borrowed directly from ExecuTorch storage through Core AI
raw views. The bridge copies shape metadata, not tensor bytes, and does not
allocate intermediate input `NSData`, `Data`, or `NDArray` buffers. Each call
wraps `const_data_ptr()` and the current shape's `nbytes()`, not the allocation's
maximum capacity. Outputs use the SDK's actual returned shape; the backend
resizes them within ExecuTorch's declared bounds before copying only the logical
result bytes. Unused capacity is neither wrapped as input nor written as output.
The backend waits for inference and all input access to finish before returning
from `execute`, including on errors. This uses ExecuTorch's normal input-lifetime
contract; callers must not concurrently mutate, release, or rebind the input
storage. Output copiers retain the SDK arrays and copy directly into validated
ExecuTorch destinations, without an intermediate `Data` allocation or payload
copy. If any destination overlaps an input or another destination, all outputs
are staged before writing destinations in order. This preserves input/output
aliasing and overlapping-output semantics. Staging is not zero-initialized.
This does not guarantee that Core AI itself avoids device transfers or internal
copies.

After an execution error, do not use the outputs as inference results. Output
shapes may already have changed, but no result bytes are copied unless every
output passes validation and resizing. Malformed SDK outputs return `Internal`.
Output dtype/rank mismatches and incompatible shapes or capacity return
`InvalidArgument`.

### Calling from Swift

When using the Core AI backend, run ExecuTorch model loading, inference and
Expand All @@ -62,13 +92,26 @@ architectures=["h17p"])` requests that compiler architecture; omitting the list
lets the compiler emit its supported architectures for the target platform.
These are Core AI architecture names, not CPU names such as `arm64`.

Both formats can require device specialization. The acquisition coordinator
first attempts SDK bookmark restoration. A missing bookmark or SDK-confirmed
cache miss falls back to source materialization and persistent specialization.
It returns the acquired model; function binding is the caller's responsibility.
At load time, the runtime queries `AIModel.deviceArchitectureName` and selects
the exact matching entry in the manifest's `archs` map. No runtime architecture
option is required, and there is no fallback to another architecture. Platform,
deployment floor, and architecture checks happen before asset reads or storage
creation. Missing-architecture errors identify the device architecture, list
available architectures, and request a matching export. Missing selected files
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
function and ordered I/O on that same acquired model.

### Asset storage

Backend initialization reads the string runtime spec `coreai_assets_dir`. If
absent, it sets the directory to `executorch_coreai` beneath user-domain
`NSCachesDirectory`. The coordinator and storage helpers always receive that
explicit directory. It contains raw bookmarks, staged bundles and lock files.

The assets root must be an absolute path. The application chooses it, should
reserve it for this backend, and is trusted not to rename, replace or modify its
contents while the backend uses it; the backend does not defend against other
Expand Down Expand Up @@ -133,6 +176,17 @@ and enumeration use explicit policies, with a missing child distinguishable
from an error. Fault injection is a separate, thread-local test interface and
compiles away in production.

Delegate construction and registration do not create storage directories or load
models. Per-model initialization performs SDK acquisition and function loading,
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 selected bundle has a model key derived from its export digest, bundle
name, and source/AOT mode. A readable variant key combines that model key with
the platform and Core AI device architecture, plus a hash of a versioned
Expand Down Expand Up @@ -175,6 +229,28 @@ 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.

### Runtime options

Pass options through the public `Module` API before loading. In an error-returning
loader using the `executorch::runtime` namespace:

```cpp
BackendOptions<1> options;
ET_CHECK_OK_OR_RETURN_ERROR(options.set_option("coreai_assets_dir", assets_dir));
LoadBackendOptionsMap map;
ET_CHECK_OK_OR_RETURN_ERROR(map.set_options("CoreAIBackend", options.view()));
ET_CHECK_OK_OR_RETURN_ERROR(module.load(map));
```

No options are required when using the default assets root. `coreai_assets_dir`
selects an application-chosen cache directory reserved for this backend. The
supplied path is validated during preflight, but warm bookmark hits need no
source.

Use separate option maps to choose different storage roots for different models.
The path is a root directory, not the bundle itself. ExecuTorch option strings
have a 255-byte limit.

## SDK Bridge

The private Swift module `CoreAIBridge` (`runtime/ETCoreAIModel.swift`) wraps
Expand Down Expand Up @@ -241,15 +317,40 @@ 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.
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
`EXECUTORCH_BUILD_SHARED=ON`, the delegate is a shared library linked to the
single consolidated `executorch_shared` runtime. Otherwise it is static.
Consumers of the shared delegate must use that same shared runtime.
Installed CMake targets resolve framework and Swift runtime dependencies using
the consumer's selected SDK and toolchain, not the producer's Xcode paths.
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
`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).
Cross-builds and older hosts can build these tests without registering them.
Running the smoke test checks registration and availability; it does not perform
model inference. Do not execute SDK27 binaries on an older host.

## Host Tests

On macOS, `EXECUTORCH_BUILD_COREAI=ON` with `EXECUTORCH_BUILD_TESTS=ON`
registers the standalone `coreai_host_test` executable with CTest. It tests
manifest parsing, AOT asset selection, cache identity, bookmark I/O, acquisition
coordination, lock contention, and filesystem storage using Foundation, without
Swift or CoreAI SDK calls. Acquisition contention tests run two threads inside a
bounded child process; bookmark lock tests separately cover contention between
processes. The host test target is omitted for Apple cross-builds.
coordination, lock contention, filesystem storage, and delegate initialization
and execution against a fake bridge. It uses Foundation without Swift or CoreAI
SDK calls. Acquisition contention tests run two threads inside a bounded child
process; bookmark lock tests separately cover contention between processes. The
host test target is omitted for Apple cross-builds. On supported macOS hosts,
CTest also runs the Swift bridge tests, source-lifetime probes, and
`coreai_runtime_smoke`, which link Swift and the Core AI framework.

CI builds and runs these tests on macOS 27. To use the same configuration from
the repository root:
Expand All @@ -262,3 +363,9 @@ cmake -S . -B <build-dir> -G Ninja \
cmake --build <build-dir> --target backends/apple/coreai/all
ctest --test-dir <build-dir>/backends/apple/coreai --output-on-failure --no-tests=error
```

For a compile-only production smoke build, use a fresh build directory without
`EXECUTORCH_BUILD_TESTS`, set `CMAKE_OSX_ARCHITECTURES=arm64`, and build
`coreai_runtime_smoke`. For iOS also set `CMAKE_SYSTEM_NAME=iOS` and
`CMAKE_OSX_SYSROOT=iphoneos`. Do not run the iOS binary on the host or install
a simulator as part of this build.
Loading
Loading