Durante seis años, Core ML fue la única respuesta a «cómo ejecuto mi modelo en un iPhone». En iOS 27 apareció una segunda: Core AI, un framework aparte con su propio formato, su propio compilador y su propio depurador. Core ML no desaparece: los árboles de decisión y el feature engineering tabular siguen allí. Core AI va de redes neuronales y de que las arquitecturas modernas corran sobre CPU, GPU y Neural Engine sin ajustes manuales para cada chip.
Repaso el stack en orden: cómo entra el modelo en el proyecto, qué ocurre en la primera carga (y por qué tarda tanto), cómo controlarlo y dónde frenaría yo antes de producción.
Versiones: Core AI llega con iOS 27, iPadOS 27, macOS 27, tvOS 27, visionOS 27 y watchOS 27. Las firmas que aparecen abajo salen de la documentación de Apple a finales de septiembre de 2026; la API está publicada, no está en beta, pero cada parte del stack soporta un conjunto distinto de plataformas y volveré sobre ello.
Qué incluye realmente Core AI#
El framework es solo la parte visible. Bajo el nombre Core AI, Apple reunió cinco cosas, y confundirlas sale caro:
- Core AI framework — la API de Swift:
AIModel,InferenceFunction,NDArray,AIModelCache. .aimodel— el formato portátil del modelo. Funciona en cualquier dispositivo Apple, pero por sí solo no se ejecuta.- coreai-torch — extensiones de PyTorch: convertir un modelo a
.aimodel, exportar varias funciones de inferencia en un único artefacto, operaciones optimizadas para atención y normalización, kernels propios de Metal 4. - coreai-optimization — cuantización y paletización con control de la técnica capa por capa.
- coreai-models — un catálogo de modelos listos para exportar más un paquete Swift con utilidades.
Aparte está el Core AI Debugger, una app de macOS que hace algo que Core ML nunca pudo: rastrear valores de tensores hasta el código Python original. Xcode, además, suma un debug gauge y un instrumento Core AI.
Lo primero que rompe la compilación#
Xcode no compila un proyecto con .aimodel tal cual. Necesita el Metal Toolchain, que no viene instalado:
% xcodebuild -downloadComponent MetalToolchainO por Xcode > Settings > Components > Other Components > Metal Toolchain. Sin él la compilación falla por un compilador de Metal ausente, y el texto del error no sugiere en absoluto que se arregle con una casilla en los ajustes.
El archivo se añade arrastrándolo al Project Navigator; después debe aparecer en la fase Compile Sources del target. Si no aparece, el modelo nunca llega al bundle.
Mirar el modelo antes de escribir código contra él#
Selecciona el .aimodel en el navegador y se abre el visor. La pestaña General trae el tamaño en parámetros y en bytes, los metadatos (descripción, autor, licencia, pares clave-valor arbitrarios, editables allí mismo y guardados solos por Xcode) y, más útil todavía, la precisión numérica separada entre cómputo y almacenamiento. También muestra la distribución de operaciones del grafo ordenada por cantidad.
La pestaña Functions guarda las firmas: nombres y tipos de cada entrada y salida, con un signo de interrogación en la dimensión de un NDArray allí donde se resuelve en tiempo de ejecución. La mayoría de modelos tiene una sola función.
Abre esa pestaña antes de la primera línea de código. La mitad de los errores de integración son un desajuste de forma o de tipo escalar, y en el visor se ven en diez segundos.
Carga: por qué await y por qué tarda#
import CoreAI
// Especializa el modelo para este dispositivo y lo carga.
let model = try await AIModel(contentsOf: urlOfModel)
// Carga una función del modelo.
guard let function = try model.loadFunction(named: "main") else {
// La función esperada no existe.
}init(contentsOf:) no es asíncrono por cortesía. Entre un archivo .aimodel y un modelo funcional está la especialización: Core AI mira las unidades de cómputo que ofrece ese dispositivo concreto y genera código ejecutable para ese hardware y esa versión del sistema. En modelos grandes eso tarda de verdad, y el usuario lo va a notar.
loadFunction(named:) tampoco es barato: prepara los recursos de una función concreta. Lanza excepción si falla la carga y devuelve nil cuando no existe ninguna función con ese nombre — dos casos distintos que se colapsan facilísimo en un try? y luego cuestan tres horas de depuración. Todos los nombres están en functionNames.
La misma función de inferencia puede llamarse desde varias tareas a la vez; la documentación lo dice explícitamente, así que no hace falta envolverla en un actor serializador propio.
Inferencia: NDArray y dos formas de acceder a la memoria#
Entradas y salidas son InferenceValue: o un NDArray o una imagen. Cuál de los dos se ve en la pestaña Functions, o mediante el descriptor en tiempo de ejecución.
La comprobación en runtime hace falta cuando el modelo llega desde un servidor y puede cambiar entre versiones sin recompilar la app:
let function: InferenceFunction = ...
let functionDescriptor = function.descriptor
guard let valueDescriptor = functionDescriptor.inputDescriptor(of: "input"),
case .ndArray(let arrayDescriptor) = valueDescriptor else {
// Entrada no encontrada o de tipo inesperado.
}
guard arrayDescriptor.shape == [3, 4] else {
// Forma inesperada.
}
guard arrayDescriptor.scalarType == .float32 else {
// Tipo escalar inesperado.
}Después, el array. Fíjate en cómo se separa el acceso: un NDArray es de solo lectura por defecto y se escribe a través de mutableView(as:). Swift lo vigila en tiempo de compilación, de modo que el código siempre enseña qué le pasa a la memoria.
// Un array con la forma y el tipo esperados.
var input = NDArray(shape: [3, 4], scalarType: .float32)
// Vista mutable para escribir.
var mutableView = input.mutableView(as: Float.self)
guard let elements = mutableView.contiguousElements else {
// Disposición de memoria no contigua.
}
writeInputData(into: elements)
// Ejecución.
var outputs = try await function.run(inputs: ["input": input])
guard let predictionValue = outputs.remove("prediction") else {
// Salida no encontrada.
}
guard let prediction = predictionValue.ndArray else {
// Salida de otro tipo.
}
processOutput(prediction.view())Las claves de inputs son los nombres fijados al convertir el modelo, no algo que uno pueda inventarse por su cuenta. contiguousElements devuelve nil con una disposición no contigua: un camino poco frecuente, pero no uno para atravesarlo con force unwrap.
Para imágenes, CVPixelBuffer sustituye a NDArray, y el descriptor informa del ancho, el alto y el formato de píxel esperados; un -1 en una dimensión significa que es dinámica. Las entradas se montan con InferenceFunction.Inputs() e insert(_:for:).
La caché de especializaciones es la parte más útil de la API#
Por defecto AIModel especializa el modelo y cachea el resultado: la primera ejecución paga el precio completo, las siguientes cargan lo ya preparado. Salvo que el sistema puede borrar los assets cacheados cuando falta espacio, y entonces una ejecución «siguiente» vuelve a ser silenciosamente la primera.
Por eso conviene comprobar la caché antes de cargar siempre — responde a si hay que dibujar progreso:
func loadModel(from modelURL: URL) async throws -> AIModel {
let cache = AIModelCache.default
// Un resultado no nulo significa que ya existe la especialización.
if let model = try cache.model(for: modelURL, options: .default) {
return model
}
// No hay caché: avisamos a la persona y especializamos ahora.
Task { @MainActor in
informUser("Preparando las funciones de IA. Esto puede tardar…")
}
return try await AIModel(contentsOf: modelURL, options: .default)
}cache.model(for:options:) no especializa nada: solo responde sí o no. Si el modelo se descarga, puedes especializarlo por adelantado en un momento cómodo con AIModel.specialize(contentsOf:options:), que guarda los assets en la caché y devuelve el modelo listo; cualquier inicialización posterior con la misma URL y las mismas opciones carga directamente de caché.
La retención la gobierna cachePolicy. La política por defecto permite al sistema reclamar espacio. .persistent lo impide, y existe para un caso concreto: borrar el .aimodel de origen para no tener dos copias de los mismos pesos en el dispositivo. En tvOS .persistent no está disponible: allí el almacenamiento local tiene que poder vaciarse.
Si tienes varias apps o extensiones que comparten modelo, crea un app group y la caché con AIModelCache(appGroup:). Una especialización para todo el grupo en lugar de una copia por target.
Otro detalle: no puedes borrar el archivo de origen y seguir llamando a AIModel(contentsOf:) — la URL de origen es la clave con la que se indexa la especialización. Para ese caso está bookmarkData: lo guardas tras especializar y en el siguiente arranque recuperas el modelo con AIModel(resolvingBookmark:), saltándote el origen. Un bookmark puede invalidarse tras una actualización del sistema, así que la rama «no encontrado» tiene que ser un camino real y no un fatalError.
Opciones de especialización#
SpecializationOptions.default deja que el sistema elija la combinación de CPU, GPU y Neural Engine que minimiza la latencia. También hay .cpuOnly e init(preferredComputeUnitKind:).
Veo exactamente un motivo sólido para salirse del valor por defecto: un modelo pequeño corriendo en segundo plano que no debería competir por la GPU con el dibujado de la interfaz. Ahí .cpuOnly se gana su sitio. En lo demás, el valor por defecto suele ganar, y eso se mide en vez de suponerse. Las unidades disponibles varían por dispositivo: consulta ComputeUnitKind.
Conviene conocer otro flag de antemano: expectFrequentReshapes. Con modelos de formas dinámicas, Core AI optimiza la función para cada forma nueva de entrada. En un modelo de lenguaje, donde la longitud de la secuencia crece un token por paso, esa optimización empieza a costar más de lo que ahorra. Poniéndolo en true se usa la versión dinámica genérica de la función.
Compilación anticipada: llevarse lo pesado a tu Mac#
Parte de la especialización puede hacerse en la máquina de compilación. coreai-build convierte un .aimodel en un conjunto de assets .aimodelc, uno por arquitectura de dispositivo:
% xcrun coreai-build compile MyModel.aimodel --platform iOS --min-deployment-version 27.0 --output compiled/La salida son archivos MyModel.<arch>.aimodelc, donde <arch> coincide con lo que devuelve AIModel.deviceArchitectureName en tiempo de ejecución. Cada asset compilado funciona en cualquier versión del sistema igual o superior a la que pasaste en --min-deployment-version.
Aquí hay una bifurcación. Meter todas las arquitecturas en el bundle significa llevar varias copias del modelo, de las que el dispositivo usa una. Apple recomienda alojar los .aimodelc por tu cuenta y descargar la variante que toca, consultando la arquitectura en runtime:
let arch = AIModel.deviceArchitectureName
let assetName = "MyModel.\(arch).aimodelc"Un .aimodelc se carga con el mismo AIModel(contentsOf:): el código de carga no cambia. Las descargas y actualizaciones encajan en Background Assets.
Y ahora lo honesto sobre los bordes de esta función. La compilación anticipada cubre solo dispositivos compatibles con Apple Intelligence: iPhone y iPad con A17 Pro o posterior, Mac con M1 o posterior, Vision Pro con M2. En tvOS y watchOS no existe en absoluto, aunque Core AI sí funcione allí. Y aun con AOT, una parte de la especialización sigue ocurriendo en el dispositivo; cuánta depende del modelo y de las unidades de cómputo que use. La formulación de Apple es cuidadosa: menos trabajo, no ningún trabajo.
Dónde frenaría yo#
Tres cosas que revisaría antes de meter Core AI en un plan de release.
La primera, la cobertura desigual de plataformas. El framework se anuncia para los seis sistemas, pero .persistent falta en tvOS, AOT falta en tvOS y watchOS, y AOT no existe en dispositivos sin Apple Intelligence. La matriz función × plataforma es más densa de lo que sugiere la página del framework, y conviene tenerla completa antes de las decisiones de arquitectura, no después.
La segunda, el coste de la primera ejecución. La especialización ocurre en el dispositivo del usuario y no es una constante: depende del modelo, del hardware y de si la especialización anterior sobrevivió en la caché. Diseñar la experiencia asumiendo «el modelo ya está listo» no funciona: el estado «preparando» hay que dibujarlo.
La tercera, el tamaño de la entrega. Un .aimodel en el bundle, los .aimodelc por arquitectura y la caché de especializaciones en disco son tres copias distintas de los mismos pesos, y cada una ocupa. La secuencia «descargar → especializar → guardar el bookmark → borrar el origen» existe justamente porque la versión ingenua infla la app.
Y si el modelo que piensas llevar por Core AI es de lenguaje, lo más probable es que no escribas inferencia con NDArray a mano: puedes entregarlo a una LanguageModelSession de Foundation Models y obtener los prompts, el streaming y la salida estructurada de siempre. Cómo se monta eso va aparte.



