六年来,"怎么把自己的模型跑在 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 月下旬;API 已正式发布,不是 beta。但这套栈的各个部分支持的平台并不一致,这点后面会单独讲。
Core AI 到底包含什么#
框架只是露出水面的部分。苹果把五样东西放进了 Core AI 这个名字下,搞混它们代价不小:
- Core AI framework —— Swift API:
AIModel、InferenceFunction、NDArray、AIModelCache。 .aimodel—— 可移植的模型格式。在所有苹果设备上通用,但它本身并不执行。- coreai-torch —— PyTorch 扩展:把模型转成
.aimodel,把多个推理函数导出进同一个产物,使用针对 attention 和归一化的硬件优化算子,以及自定义的 Metal 4 kernel。 - coreai-optimization —— 量化与调色板化,可以逐层选择压缩手法。
- coreai-models —— 一份可直接导出的模型目录,外加一个装着辅助工具的 Swift 包。
另外还有 Core AI Debugger,一个 macOS 应用,做到了 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 即可;之后它应该出现在目标 target 的 Compile Sources 阶段里。没出现,模型就进不了 bundle。
写代码之前,先用眼睛看模型#
在导航器里选中 .aimodel,查看器就会打开。General 标签页里有以参数量和字节计的体积、元数据(描述、作者、许可证、任意键值对 —— 可以就地编辑,Xcode 自动保存),更有用的是把数值精度拆成计算精度和存储精度两栏。同一页还会按数量排序展示图里的算子分布。
Functions 标签页放的是签名:每个输入输出的名字和类型,凡是运行时才确定的维度,NDArray 上会显示一个问号。大多数模型只有一个函数。
这个标签页值得在写第一行代码之前就打开。集成阶段一半的 bug 都是形状或标量类型对不上,而在查看器里十秒就能看出来。
加载:为什么是 await,为什么慢#
import CoreAI
// 为当前设备特化模型并加载。
let model = try await AIModel(contentsOf: urlOfModel)
// 从模型里加载一个函数。
guard let function = try model.loadFunction(named: "main") else {
// 处理找不到预期函数的情况。
}init(contentsOf:) 是异步的,并不是出于客气。.aimodel 文件和一个可用模型之间隔着特化:Core AI 会看这台设备具体有哪些计算单元,然后为这套硬件和这个系统版本生成可执行代码。模型一大,这段时间就很实在,用户会看见。
loadFunction(named:) 也不便宜,它要为某个具体函数准备资源。加载失败会抛异常,而模型里没有同名函数时返回 nil —— 这是两种完全不同的情况,却非常容易被一个 try? 揉在一起,然后换来三小时排查。所有函数名可以从 functionNames 拿到。
同一个推理函数可以从不同的 task 里并发调用,这点文档写得很明确,所以不需要再自己套一层串行化的 actor。
推理:NDArray 与两种内存访问#
输入输出是 InferenceValue,要么是 NDArray,要么是图像。具体是哪种,看 Functions 标签页,或者运行时看描述符。
运行时检查在这种场景下是必要的:模型来自服务端,可能在不同版本之间变化,而 App 并不重新构建。
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 里的 key 是模型转换时定下的名字,不是这边可以自己发挥的东西。布局不连续时 contiguousElements 会返回 nil —— 这条路不常走,但也不该用 force unwrap 硬闯过去。
图像场景下 CVPixelBuffer 取代 NDArray,描述符会给出期望的宽、高和像素格式;维度里的 -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 和同样的 options 初始化都会直接走缓存。
保留策略由 cachePolicy 决定。默认策略允许系统在存储吃紧时回收。.persistent 禁止这件事,它存在只为一个具体场景:你删掉了源 .aimodel,不想让设备上留着同一份权重的两个副本。tvOS 上没有 .persistent —— 那边的本地存储必须是可清理的。
如果有多个 App 或扩展共用一个模型,就建一个 app group,用 AIModelCache(appGroup:) 创建缓存。整组共用一次特化,而不是每个 target 各存一份。
还有一处细节:不能删掉源文件之后继续调 AIModel(contentsOf:) —— 源 URL 本身就是检索这份特化的键。为此有 bookmarkData:特化之后把它存下来,下次启动用 AIModel(resolvingBookmark:) 绕过源文件直接恢复模型。书签可能因系统升级而失效,所以"没找到"那条分支必须是能走通的路径,而不是 fatalError。
特化选项#
SpecializationOptions.default 让系统去挑延迟最低的 CPU / GPU / Neural Engine 组合。另有 .cpuOnly 和 init(preferredComputeUnitKind:)。
在我看来,偏离默认值只有一个扎实的理由:一个小模型在后台跑,不该跟界面渲染抢 GPU —— 这时 .cpuOnly 站得住。其余情况默认值通常更优,而这件事该测量而不是猜测。各设备可用的计算单元并不相同,用 ComputeUnitKind 确认。
还有一个值得提前知道的标志:expectFrequentReshapes。对动态形状的模型,Core AI 默认会为每一种新的输入形状做一次优化。而在语言模型里,序列长度每步涨一个 token,这项优化很快就比它省下的更贵。把它设成 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 的任意系统版本上。
这里有个岔路。把所有架构都塞进 bundle,意味着随包携带好几份模型,而设备只用其中一份。苹果的建议是自己托管这些 .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,仍有一部分特化留在设备上完成;具体留多少取决于模型和它用到的计算单元。苹果的措辞也很谨慎:是更少的工作,不是没有工作。
我会在哪里停一下#
把 Core AI 放进发版计划之前,有三件事值得先确认。
第一,平台覆盖并不整齐。框架标称支持六个系统,但 .persistent 在 tvOS 上没有,AOT 在 tvOS 和 watchOS 上没有,在不支持 Apple Intelligence 的设备上 AOT 干脆不存在。这张"功能 × 平台"的表比框架页面给人的印象要密,最好在做架构决策之前就填好,而不是之后。
第二,首次运行的成本。特化发生在用户设备上,而且不是个常数:取决于模型、取决于硬件,也取决于上一次的特化有没有在缓存里活下来。按"模型已经就绪"去设计体验是行不通的 —— "正在准备"这个状态总得画出来。
第三,交付体积。bundle 里的 .aimodel、按架构的 .aimodelc、磁盘上的特化缓存 —— 这是同一份权重的三种副本,每一份都占地方。"下载 → 特化 → 保存书签 → 删掉源文件"这套流程之所以存在,正是因为朴素做法会把 App 撑大。
另外,如果你打算用 Core AI 承载的是语言模型,那多半根本不用手写 NDArray 推理:可以把它交给 Foundation Models 的 LanguageModelSession,照常使用提示词、流式输出和结构化输出。这一套怎么搭,我单开一篇讲。



