← All courses ← Course home

Lesson 02 of 05

Specialize before the user waits

Check the cache first, specialize at a calm moment, keep the cache from being purged, and move most of the work to build time with coreai-build and per-architecture .aimodelc files.

Agent brief (llms.md)

Environment setup

Same as lesson 01: Xcode 27 with the Metal Toolchain, an iOS 27 target that links CoreAI.framework, a real device, and the coreai-models checkout with uv sync (Python 3.11+, PyTorch on MPS or CPU).

Check the cache, then decide what to show

AIModelCache.default.model(for:options:) returns a model when a specialization for that URL and options is already cached, and nil otherwise. It never specializes. Use it to decide if the user should see a "Preparing" state. This is the pattern from Apple's Managing model specialization and caching:

import CoreAI

@MainActor
final class ModelPreparer: ObservableObject {
    @Published var preparing = false

    func load(_ url: URL) async throws -> AIModel {
        if let cached = try AIModelCache.default.model(for: url, options: .default) {
            return cached
        }
        preparing = true
        defer { preparing = false }
        return try await AIModel(contentsOf: url, options: .default)
    }
}

Specialize at a calm moment

AIModel.specialize(contentsOf:options:cache:cachePolicy:) runs specialization and stores the result, without you needing to run inference. Call it right after a model download finishes, or when the user turns on the AI feature, not when they press Send. Apple notes that calling it again with the same URL and options returns the cached result.

import CoreAI

func prepareAfterDownload(_ url: URL) async {
    do {
        try await AIModel.specialize(contentsOf: url, options: .default, cachePolicy: .persistent)
    } catch {
        print("specialize failed:", error)
    }
}

Use the same SpecializationOptions here and at load time. The cache key is the model URL plus the options. If you specialize with .default and load with .cpuOnly, you pay again.

Keep the cache

Move the heavy part to build time

Apple's ahead-of-time guide describes coreai-build. It moves model compilation, the most expensive part of specialization, to your Mac. The output is one .aimodelc per device architecture.

xcrun coreai-build compile MyModel.aimodel \
  --platform iOS --min-deployment-version 27.0 --output compiled/
ls compiled/   # MyModel.<arch>.aimodelc, one per architecture
xcrun coreai-build compile --help   # see --preferred-compute and other options

Each device needs only its own file. Apple suggests hosting them and downloading the match, for example with the Background Assets framework. At runtime, pick the file with AIModel.deviceArchitectureName:

import CoreAI

func compiledAssetName(base: String) -> String {
    "\(base).\(AIModel.deviceArchitectureName).aimodelc"
}

func loadCompiled(base: String, from folder: URL, fallbackSource: URL) async throws -> AIModel {
    let compiled = folder.appendingPathComponent(compiledAssetName(base: base))
    if FileManager.default.fileExists(atPath: compiled.path) {
        return try await AIModel(contentsOf: compiled, options: .default)
    }
    // No match for this architecture (or not downloaded yet): use the portable model.
    return try await AIModel(contentsOf: fallbackSource, options: .default)
}

Two limits from Apple's page: ahead-of-time compilation only covers devices that support Apple Intelligence, and the compiled file still needs some on-device specialization. It is faster, not zero. Keep the cache check from above even with .aimodelc files.

What we could not verify

We looked for Apple documentation on how specialization behaves on chips with more than one Neural Engine (for example, extra cold-load time per Neural Engine). We did not find any on developer.apple.com or in apple/coreai-models. Measure cold load on each device class you ship, with the timers from lesson 01, instead of assuming.

Sources

Key concepts

  • AIModelCache.default.model(for:options:) tells you if a specialization is cached and never specializes.
  • AIModel.specialize(...) specializes without inference. Run it after download or on feature enable.
  • The cache key is the model URL plus SpecializationOptions. OS updates and storage pressure can remove entries.
  • coreai-build compile makes one .aimodelc per architecture. Pick it with AIModel.deviceArchitectureName.

Takeaways

  • Show a Preparing state only on a cache miss.
  • Specialize before the user presses Send, with the same options you load with.
  • Ship per-architecture .aimodelc files for Apple Intelligence devices and keep the portable model as a fallback.