6年のあいだ、「自分のモデルをiPhoneで動かすには」という問いへの答えはCore MLひとつでした。iOS 27で二つ目の答えが出ました。Core AI — 独自のフォーマット、独自のコンパイラ、独自のデバッガを持つ別フレームワークです。Core MLがなくなるわけではありません。決定木や表形式の特徴量エンジニアリングは引き続きそちらの担当です。Core AIはニューラルネットワーク向けで、最新のアーキテクチャをチップごとの手作業チューニングなしにCPU・GPU・Neural Engineへ載せるためのものです。
スタックを順に見ていきます。モデルがプロジェクトに入るまで、初回ロードで何が起きるのか(なぜあれほど待たされるのか)、それをどう制御するか、そして本番投入前に私ならどこで一度立ち止まるか。
バージョン。Core AIはiOS 27、iPadOS 27、macOS 27、tvOS 27、visionOS 27、watchOS 27から利用できます。以下のシグネチャは2026年9月下旬時点のAppleのドキュメントによるもので、APIはベータではなく正式版です。ただしスタックの部分ごとに対応プラットフォームが異なり、その点は後ほど改めて触れます。
Core AIに含まれるもの#
フレームワークは表に見えている部分にすぎません。AppleはCore AIという名前のもとに五つのものをまとめており、これを取り違えると高くつきます。
- Core AI framework — Swift API。
AIModel、InferenceFunction、NDArray、AIModelCache。 .aimodel— 可搬なモデル形式。Appleのどのデバイスでも通用しますが、それ自体は実行されません。- coreai-torch — PyTorch拡張。モデルを
.aimodelへ変換し、複数の推論関数を単一のアーティファクトへエクスポートし、attentionや正規化向けの最適化済み演算やMetal 4のカスタムカーネルを使えます。 - coreai-optimization — 量子化とパレット化。手法をレイヤ単位で選べます。
- coreai-models — エクスポート可能なモデルのカタログと、ヘルパーを収めたSwiftパッケージ。
これらとは別に Core AI Debugger があります。Core MLには最後までなかったこと — テンソルの値を元のPythonソースまで遡って追跡できます。Xcode側にもCore AIのdebug gaugeとCore AI instrumentが加わりました。
最初にビルドを壊すもの#
Xcodeは.aimodelを含むプロジェクトをそのままではビルドできません。デフォルトでは入っていないMetal Toolchainが必要です。
% xcodebuild -downloadComponent MetalToolchainあるいはXcode > Settings > Components > Other Components > Metal Toolchainから。入っていないとMetalコンパイラが見つからないというエラーでビルドが落ちますが、そのエラー文から「設定のチェックひとつで直る」とは到底読み取れません。
ファイル自体はProject Navigatorへドラッグして追加します。その後、対象ターゲットのCompile Sourcesに現れているはずです。現れていなければ、モデルはバンドルに入りません。
コードを書く前にモデルを眺める#
ナビゲータで.aimodelを選ぶとビューアが開きます。Generalタブにはパラメータ数とバイト数でのサイズ、メタデータ(説明、作者、ライセンス、任意のキーと値のペア — その場で編集でき、Xcodeが自動保存します)、そしてより有用な、計算用と格納用に分けた数値精度が並びます。グラフ内の演算分布も件数順で表示されます。
Functionsタブにはシグネチャがあります。入力と出力それぞれの名前と型、そして実行時に決まる次元にはNDArrayのところに疑問符が入ります。ほとんどのモデルは関数をひとつだけ持ちます。
このタブはコードの一行目より先に開いてください。統合時のバグの半分は形状かスカラー型の不一致で、ビューアなら10秒で見えます。
ロード — なぜawaitで、なぜ遅いのか#
import CoreAI
// このデバイス向けにモデルを特殊化してロードする。
let model = try await AIModel(contentsOf: urlOfModel)
// モデルから関数をロードする。
guard let function = try model.loadFunction(named: "main") else {
// 期待した関数が見つからない場合の処理。
}init(contentsOf:)が非同期なのは礼儀のためではありません。.aimodelファイルと動くモデルのあいだには特殊化が挟まります。Core AIはそのデバイスが持つ計算ユニットを見て、そのハードウェアとOSバージョン向けの実行コードを生成します。大きなモデルでは相応の時間がかかり、ユーザーはそれを目にします。
loadFunction(named:)も安くはありません。特定の関数のためのリソースを準備します。ロード失敗時は例外を投げ、その名前の関数が存在しない場合はnilを返します — この二つはまったく別の事態なのに、try?ひとつに潰れやすく、その後のデバッグに3時間を持っていきます。名前の一覧はfunctionNamesから取れます。
同じ推論関数を複数のタスクから同時に呼んでも安全です。ドキュメントに明記されているので、自前の直列化アクターで包む必要はありません。
推論 — NDArrayと二種類のメモリアクセス#
入出力はInferenceValueで、中身はNDArrayか画像のどちらかです。どちらかはFunctionsタブか、実行時のディスクリプタで分かります。
実行時のチェックが要るのは、モデルがサーバーから届き、アプリを再ビルドせずにリリース間で変わりうる場合です。
let function: InferenceFunction = ...
let functionDescriptor = function.descriptor
guard let valueDescriptor = functionDescriptor.inputDescriptor(of: "input"),
case .ndArray(let arrayDescriptor) = valueDescriptor else {
// 入力が見つからない、または想定外の型。
}
guard arrayDescriptor.shape == [3, 4] else {
// 想定外の形状。
}
guard arrayDescriptor.scalarType == .float32 else {
// 想定外のスカラー型。
}続いて配列そのもの。アクセスの分け方に注目してください。NDArrayはデフォルトで読み取り専用で、書き込みはmutableView(as:)を通します。Swiftがコンパイル時にこれを見張るので、メモリに何が起きているかは常にコードから読み取れます。
// 期待される形状と型に合わせた配列。
var input = NDArray(shape: [3, 4], scalarType: .float32)
// 書き込み用のミュータブルビュー。
var mutableView = input.mutableView(as: Float.self)
guard let elements = mutableView.contiguousElements else {
// メモリ配置が連続していない場合の処理。
}
writeInputData(into: elements)
// 実行。
var outputs = try await function.run(inputs: ["input": input])
guard let predictionValue = outputs.remove("prediction") else {
// 出力が見つからない。
}
guard let prediction = predictionValue.ndArray else {
// 想定と異なる型の出力。
}
processOutput(prediction.view())inputsのキーはモデル変換時に決まった名前であって、こちら側で考えてよいものではありません。contiguousElementsは非連続配置のときnilを返します。頻度の低い経路ですが、force unwrapで踏み抜いてよい場所ではありません。
画像の場合はNDArrayの代わりにCVPixelBufferを使い、ディスクリプタが期待される幅・高さ・ピクセルフォーマットを返します。次元の-1は動的という意味です。入力はInferenceFunction.Inputs()とinsert(_:for:)で組み立てます。
特殊化キャッシュがこのAPIで一番役に立つ#
デフォルトではAIModelがモデルを特殊化して結果をキャッシュします。初回は全額を払い、以降は出来合いを読み込む — はずですが、システムはストレージ逼迫時にキャッシュ済みアセットを削除できます。そうなると「以降」の起動が静かにまた初回に戻ります。
だからロード前のキャッシュ確認は毎回やる価値があります。進捗表示が必要かどうかに答えてくれるからです。
func loadModel(from modelURL: URL) async throws -> AIModel {
let cache = AIModelCache.default
// nil以外なら、すでに特殊化されキャッシュされている。
if let model = try cache.model(for: modelURL, options: .default) {
return model
}
// キャッシュがない。利用者に伝えてから特殊化する。
Task { @MainActor in
informUser("AI機能を準備しています。しばらくお待ちください…")
}
return try await AIModel(contentsOf: modelURL, options: .default)
}cache.model(for:options:)は特殊化をしません。あるかないかを答えるだけです。モデルをダウンロードする設計なら、都合のよいタイミングでAIModel.specialize(contentsOf:options:)を使って先に特殊化できます。アセットをキャッシュへ保存して準備済みモデルを返すので、以降は同じURLと同じオプションでの初期化がすべてキャッシュから読み込まれます。
保持のしかたはcachePolicyが決めます。デフォルトではストレージ逼迫時にシステムが回収できます。.persistentはそれを禁じるもので、用途はひとつ — 同じ重みのコピーを端末に二つ置かないよう、元の.aimodelを削除する場合です。tvOSでは.persistentは使えません。 あちらのローカルストレージは消去可能でなければならないからです。
同じモデルを使うアプリや拡張が複数あるなら、app groupを作ってAIModelCache(appGroup:)でキャッシュを作ります。ターゲットごとにコピーを持つのではなく、グループにひとつの特殊化で済みます。
もう一点。元ファイルを削除したうえでAIModel(contentsOf:)を呼び続けることはできません。元のURLこそが特殊化を引くキーだからです。そのためのbookmarkDataがあります。特殊化後に保存しておき、次回起動時はAIModel(resolvingBookmark:)で元ファイルを経由せずモデルを復元します。ブックマークはOSアップデートで無効化されうるので、「見つからない」分岐はfatalErrorではなく実際に動く経路である必要があります。
特殊化オプション#
SpecializationOptions.defaultは、レイテンシが最小になるCPU・GPU・Neural Engineの組み合わせをシステムに選ばせます。.cpuOnlyとinit(preferredComputeUnitKind:)もあります。
デフォルトから外れる確かな理由は、私の見るかぎりひとつだけです。小さなモデルをバックグラウンドで回していて、UIの描画とGPUを取り合わせたくない場合 — ここでは.cpuOnlyが効きます。それ以外ではたいていデフォルトが勝つので、想像ではなく計測してください。利用できるユニットはデバイスごとに違います。ComputeUnitKindで確認を。
もうひとつ先に知っておきたいフラグがexpectFrequentReshapesです。動的形状のモデルに対して、Core AIはデフォルトでは入力形状が変わるたびに関数を最適化します。系列長が1トークンずつ伸びる言語モデルでは、この最適化が節約分より高くつき始めます。trueにすると関数の汎用的な動的版に切り替わります。
事前コンパイル — 重い部分を自分のMacへ#
特殊化の一部はビルドマシンで済ませられます。coreai-buildは.aimodelをデバイスアーキテクチャごとの.aimodelcアセット群へコンパイルします。
% xcrun coreai-build compile MyModel.aimodel --platform iOS --min-deployment-version 27.0 --output compiled/出力はMyModel.<arch>.aimodelcという名前で、<arch>は実行時にAIModel.deviceArchitectureNameが返す値と一致します。コンパイル済みアセットは、--min-deployment-versionで指定した以上のOSバージョンならどれでも動きます。
ここで道が分かれます。全アーキテクチャをバンドルへ入れるということは、デバイスがひとつしか使わないモデルのコピーを何個も配ることです。Appleの推奨は、.aimodelcを自前でホストし、実行時にアーキテクチャを調べて該当するものだけを取得するやり方です。
let arch = AIModel.deviceArchitectureName
let assetName = "MyModel.\(arch).aimodelc".aimodelcのロードは同じAIModel(contentsOf:)です。ロード側のコードは変えなくて済みます。ダウンロードと更新はBackground Assetsの担当にするのが筋です。
そして、この機能の縁について正直なところを。事前コンパイルが対象にするのはApple Intelligence対応デバイスだけです。A17 Pro以降のiPhoneとiPad、M1以降のMac、M2のVision Pro。tvOSとwatchOSには存在すらしません — Core AI本体はそこで動くにもかかわらず。さらにAOTを使っても特殊化の一部はデバイス上に残ります。どれだけ残るかはモデルと使う計算ユニット次第です。Appleの書きぶりも慎重で、作業が減るであってなくなるとは言っていません。
私ならどこで立ち止まるか#
Core AIをリリース計画へ入れる前に確認したい三点です。
ひとつめはプラットフォーム対応の不揃いさ。フレームワークは6つのOS向けとされていますが、.persistentはtvOSにないし、AOTはtvOSとwatchOSにないし、Apple Intelligence非対応デバイスにはAOTがそもそもありません。機能×プラットフォームの表はフレームワークのページが示すより密で、アーキテクチャを決める前に埋めておきたいものです。
ふたつめは初回起動のコスト。特殊化はユーザーの端末で走り、定数ではありません。モデルにも、ハードウェアにも、前回の特殊化がキャッシュに生き残っていたかにも左右されます。「モデルは準備済み」を前提にしたUXは成立しません。準備中の状態は必ず描くことになります。
みっつめは配布サイズ。バンドルの.aimodel、アーキテクチャごとの.aimodelc、ディスク上の特殊化キャッシュ — 同じ重みの別々のコピーが三つあり、それぞれ場所を食います。「ダウンロード → 特殊化 → ブックマーク保存 → 元ファイル削除」という手順が存在するのは、素朴なやり方だとアプリが膨れるからです。
なお、Core AIで運ぼうとしているモデルが言語モデルなら、NDArrayで推論を手書きすることはおそらくありません。Foundation ModelsのLanguageModelSessionに渡せば、いつものプロンプト・ストリーミング・構造化出力がそのまま使えます。その組み立て方は別稿で扱います。



