Optimización de IA On-Device en iOS: De CoreML a Core AI en 2026

Guía práctica para optimizar modelos de IA on-device en iOS, desde CoreML hasta el futuro Core AI, con código real y lecciones de proyectos de producción

iOS CoreML AI SwiftUI performance

Durante los últimos dos años, he integrado IA on-device en tres proyectos de producción: Leonard AI (104K LOC Swift), ChutApp y Kunoa. Lo que empezó como experimentos con CoreML se ha convertido en una parte fundamental de la arquitectura mobile. Con Apple anunciando Core AI como reemplazo de CoreML para iOS 27, es el momento perfecto para hacer balance de lo aprendido y prepararse para el futuro.

La realidad de CoreML en 2026

CoreML está lejos de ser perfecto. En Leonard AI, una SaaS para gestión de centros de estética con 300 clientes activos, implementé reconocimiento de imágenes para análisis automático de tratamientos. El primer prototipo funcionaba, pero tenía problemas serios de rendimiento.

Problema #1: Latencia impredecible

// Primer intento - problemático
class TreatmentAnalyzer: ObservableObject {
    private var model: MLModel?
    
    init() {
        // 😱 Carga síncrona en init - bloquea la UI
        guard let modelURL = Bundle.main.url(forResource: "SkinAnalysis", withExtension: "mlmodelc"),
              let model = try? MLModel(contentsOf: modelURL) else {
            fatalError("No se pudo cargar el modelo")
        }
        self.model = model
    }
    
    func analyze(image: UIImage) -> TreatmentPrediction? {
        // Latencia: 800ms-2.5s en iPhone 12
        // Inconsistente entre dispositivos
        guard let prediction = try? model?.prediction(from: imageToMLMultiArray(image)) else {
            return nil
        }
        return parsePrediction(prediction)
    }
}

En dispositivos más antiguos (iPhone 11 y anteriores), la latencia era inaceptable para una experiencia fluida. El problema no era solo el modelo, sino cómo lo gestionaba.

Solución: Carga asíncrona y warmup predictivo

actor ModelManager {
    private var loadedModels: [String: MLModel] = [:]
    private var loadingTasks: [String: Task<MLModel, Error>] = [:]
    
    func getModel(named name: String) async throws -> MLModel {
        // Evitar cargas duplicadas
        if let model = loadedModels[name] {
            return model
        }
        
        if let existingTask = loadingTasks[name] {
            return try await existingTask.value
        }
        
        let task = Task {
            guard let modelURL = Bundle.main.url(forResource: name, withExtension: "mlmodelc") else {
                throw ModelError.notFound
            }
            
            // Configuración específica para Neural Engine
            let config = MLModelConfiguration()
            config.computeUnits = .cpuAndNeuralEngine
            config.allowLowPrecisionAccumulationOnGPU = true
            
            let model = try MLModel(contentsOf: modelURL, configuration: config)
            
            // Warmup con tensor dummy
            _ = try? model.prediction(from: createDummyInput())
            
            return model
        }
        
        loadingTasks[name] = task
        
        defer {
            loadingTasks.removeValue(forKey: name)
        }
        
        let model = try await task.value
        loadedModels[name] = model
        return model
    }
}

class TreatmentAnalyzer: ObservableObject {
    private let modelManager = ModelManager()
    @Published var isReady = false
    
    init() {
        Task {
            // Precarga en background
            _ = try await modelManager.getModel(named: "SkinAnalysis")
            await MainActor.run {
                isReady = true
            }
        }
    }
    
    @MainActor
    func analyze(image: UIImage) async -> TreatmentPrediction? {
        do {
            let model = try await modelManager.getModel(named: "SkinAnalysis")
            
            // Procesamiento en background queue
            let prediction = await Task.detached(priority: .userInitiated) {
                try? model.prediction(from: self.imageToMLMultiArray(image))
            }.value
            
            return prediction.map(parsePrediction)
        } catch {
            return nil
        }
    }
}

Resultado: Latencia reducida de ~2s a ~400ms en promedio, y experiencia consistente entre dispositivos.

Problema #2: Gestión de memoria con modelos grandes

En Kunoa, una app de salud con IA conversacional, el modelo de embeddings para RAG ocupaba 180MB. Con múltiples instancias y el modelo principal, llegábamos a memory pressure.

Estrategia de memoria inteligente

actor MLModelCache {
    private var cache: [String: (model: MLModel, lastAccess: Date)] = [:]
    private let maxCacheSize = 3
    private let cacheTimeout: TimeInterval = 300 // 5 minutos
    
    func getCachedModel(named name: String) async throws -> MLModel {
        // Cleanup automático
        await cleanupExpiredModels()
        
        if let entry = cache[name] {
            // Actualizar último acceso
            cache[name] = (entry.model, Date())
            return entry.model
        }
        
        // Evict modelo menos usado si estamos al límite
        if cache.count >= maxCacheSize {
            await evictLeastRecentlyUsed()
        }
        
        let model = try await loadModel(named: name)
        cache[name] = (model, Date())
        
        return model
    }
    
    private func cleanupExpiredModels() {
        let now = Date()
        cache = cache.filter { _, entry in
            now.timeIntervalSince(entry.lastAccess) < cacheTimeout
        }
    }
    
    private func evictLeastRecentlyUsed() {
        guard let oldestKey = cache.min(by: { $0.value.lastAccess < $1.value.lastAccess })?.key else {
            return
        }
        cache.removeValue(forKey: oldestKey)
    }
    
    func preload(models: [String]) async {
        for model in models {
            _ = try? await getCachedModel(named: model)
        }
    }
}

// Uso en SwiftUI con observabilidad
@MainActor
class HealthAnalysisViewModel: ObservableObject {
    @Published var analysis: HealthAnalysis?
    @Published var isProcessing = false
    
    private let modelCache = MLModelCache()
    
    func analyzeSymptoms(_ symptoms: [String]) async {
        isProcessing = true
        defer { isProcessing = false }
        
        do {
            // Cargar solo el modelo necesario según el contexto
            let modelName = selectOptimalModel(for: symptoms)
            let model = try await modelCache.getCachedModel(named: modelName)
            
            let input = createMLInput(from: symptoms)
            let prediction = try model.prediction(from: input)
            
            analysis = parseHealthAnalysis(prediction)
        } catch {
            // Error handling
            analysis = HealthAnalysis.error(error.localizedDescription)
        }
    }
    
    private func selectOptimalModel(for symptoms: [String]) -> String {
        // Lógica para elegir el modelo más apropiado
        // Modelo ligero para síntomas simples, completo para casos complejos
        return symptoms.count > 5 ? "HealthAnalysisComplete" : "HealthAnalysisLite"
    }
}

Problema #3: Batching y pipeline de procesamiento

Con múltiples usuarios simultáneos en Leonard AI, procesar imágenes una a una era ineficiente. La solución fue implementar batching inteligente.

actor BatchProcessor<Input, Output> {
    typealias ProcessingFunction = ([Input]) async throws -> [Output]
    
    private var pendingItems: [(Input, CheckedContinuation<Output?, Error>)] = []
    private let maxBatchSize: Int
    private let processingFunction: ProcessingFunction
    private var processingTask: Task<Void, Never>?
    
    init(
        maxBatchSize: Int = 8,
        processingFunction: @escaping ProcessingFunction
    ) {
        self.maxBatchSize = maxBatchSize
        self.processingFunction = processingFunction
    }
    
    func process(_ input: Input) async throws -> Output? {
        return try await withCheckedThrowingContinuation { continuation in
            Task {
                await self.addToBatch(input, continuation)
            }
        }
    }
    
    private func addToBatch(
        _ input: Input,
        _ continuation: CheckedContinuation<Output?, Error>
    ) {
        pendingItems.append((input, continuation))
        
        if pendingItems.count >= maxBatchSize {
            triggerProcessing()
        } else {
            scheduleProcessing()
        }
    }
    
    private func triggerProcessing() {
        processingTask?.cancel()
        processingTask = Task {
            await processBatch()
        }
    }
    
    private func scheduleProcessing() {
        guard processingTask == nil else { return }
        
        processingTask = Task {
            // Esperar un poco para agrupar más elementos
            try? await Task.sleep(nanoseconds: 50_000_000) // 50ms
            await processBatch()
        }
    }
    
    private func processBatch() {
        let currentBatch = pendingItems
        pendingItems.removeAll()
        
        guard !currentBatch.isEmpty else {
            processingTask = nil
            return
        }
        
        Task {
            do {
                let inputs = currentBatch.map { $0.0 }
                let outputs = try await processingFunction(inputs)
                
                for (index, item) in currentBatch.enumerated() {
                    let output = outputs.indices.contains(index) ? outputs[index] : nil
                    item.1.resume(returning: output)
                }
            } catch {
                for item in currentBatch {
                    item.1.resume(throwing: error)
                }
            }
            
            processingTask = nil
            
            // Si hay más elementos pendientes, procesarlos
            if !pendingItems.isEmpty {
                await processBatch()
            }
        }
    }
}

// Uso específico para análisis de imágenes
class ImageAnalysisService {
    private let batchProcessor: BatchProcessor<UIImage, AnalysisResult>
    private let modelManager: ModelManager
    
    init() {
        self.modelManager = ModelManager()
        self.batchProcessor = BatchProcessor { [weak self] images in
            await self?.processBatch(images) ?? []
        }
    }
    
    func analyze(_ image: UIImage) async throws -> AnalysisResult? {
        return try await batchProcessor.process(image)
    }
    
    private func processBatch(_ images: [UIImage]) async -> [AnalysisResult] {
        do {
            let model = try await modelManager.getModel(named: "ImageAnalysis")
            
            // Procesamiento en paralelo con límite de concurrencia
            let results = await withTaskGroup(of: (Int, AnalysisResult?).self) { group in
                for (index, image) in images.enumerated() {
                    group.addTask {
                        let prediction = try? model.prediction(from: self.imageToMLMultiArray(image))
                        return (index, prediction.map(self.parseAnalysis))
                    }
                }
                
                var indexedResults: [(Int, AnalysisResult?)] = []
                for await result in group {
                    indexedResults.append(result)
                }
                return indexedResults.sorted(by: { $0.0 < $1.0 }).map { $0.1 }
            }
            
            return results.compactMap { $0 }
        } catch {
            return Array(repeating: AnalysisResult.error, count: images.count)
        }
    }
}

Core AI: ¿Qué está por venir?

Según los reportes de Bloomberg, Apple reemplazará CoreML con Core AI en iOS 27. Esta no es solo una nomenclatura nueva, sino una evolución de la arquitectura.

Cambios esperados

1. Integración más profunda con Apple Silicon

// Especulación basada en patrones actuales de Apple
import CoreAI

class ModernAIProcessor {
    func configureOptimization() async {
        let config = AIModelConfiguration()
        
        // Mayor control sobre la ejecución
        config.preferredComputeUnits = .neuralEngineWithGPUFallback
        config.memoryOptimization = .aggressive
        config.powerEfficiency = .balanced
        
        // Integración con Metal para computación custom
        config.metalDevice = MTLCreateSystemDefaultDevice()
        
        await AIRuntime.configure(config)
    }
}

2. Mejor soporte para modelos generativos

Uno de los puntos débiles actuales de CoreML es el soporte limitado para modelos generativos grandes. Core AI debería solucionar esto:

// Posible API futura
class GenerativeModelManager {
    func loadLLM(named: String) async throws -> LLMModel {
        let config = LLMConfiguration()
        config.contextLength = 8192
        config.streamingEnabled = true
        config.quantization = .int4 // Soporte nativo para cuantización
        
        return try await LLMModel.load(named: named, configuration: config)
    }
    
    func generateText(
        from prompt: String, 
        using model: LLMModel
    ) -> AsyncThrowingStream<String, Error> {
        AsyncThrowingStream { continuation in
            Task {
                do {
                    for try await token in model.generate(prompt: prompt) {
                        continuation.yield(token)
                    }
                    continuation.finish()
                } catch {
                    continuation.finish(throwing: error)
                }
            }
        }
    }
}

3. Framework unificado

En lugar de manejar CoreML, Vision, Speech y NLP por separado:

// Visión futura - API unificada
class UnifiedAIService {
    func process(_ input: AIInput) async throws -> AIOutput {
        let pipeline = AIPipeline {
            // Composición declarativa de procesamiento
            VisionStage()
                .detectObjects()
                .extractText()
            
            NLPStage()
                .extractEntities()
                .analyzeSentiment()
            
            GenerationStage()
                .generateSummary()
        }
        
        return try await pipeline.execute(input)
    }
}

Preparándose para la migración

Mientras esperamos Core AI, hay cosas que podemos hacer para facilitar la futura migración:

1. Abstraer la capa de ML

protocol MLInferenceProtocol {
    associatedtype Input
    associatedtype Output
    
    func predict(_ input: Input) async throws -> Output
    func warmup() async throws
    func cleanup() async
}

// Implementación actual con CoreML
class CoreMLInference<Input, Output>: MLInferenceProtocol {
    private let model: MLModel
    private let inputTransformer: (Input) -> MLFeatureProvider
    private let outputTransformer: (MLFeatureProvider) -> Output
    
    init(
        modelName: String,
        inputTransformer: @escaping (Input) -> MLFeatureProvider,
        outputTransformer: @escaping (MLFeatureProvider) -> Output
    ) async throws {
        let config = MLModelConfiguration()
        config.computeUnits = .cpuAndNeuralEngine
        
        guard let url = Bundle.main.url(forResource: modelName, withExtension: "mlmodelc") else {
            throw MLError.modelNotFound
        }
        
        self.model = try MLModel(contentsOf: url, configuration: config)
        self.inputTransformer = inputTransformer
        self.outputTransformer = outputTransformer
    }
    
    func predict(_ input: Input) async throws -> Output {
        let featureProvider = inputTransformer(input)
        let prediction = try model.prediction(from: featureProvider)
        return outputTransformer(prediction)
    }
    
    func warmup() async throws {
        // Implementación específica de warmup
    }
    
    func cleanup() async {
        // Cleanup si es necesario
    }
}

// Futura implementación con Core AI
class CoreAIInference<Input, Output>: MLInferenceProtocol {
    // La API cambiaría, pero la interfaz pública se mantiene
    func predict(_ input: Input) async throws -> Output {
        // Nueva implementación con Core AI
        fatalError("Implementar cuando Core AI esté disponible")
    }
    
    func warmup() async throws {
        // Nueva implementación
    }
    
    func cleanup() async {
        // Nueva implementación
    }
}

2. Configuración flexible

enum MLBackend {
    case coreML
    case coreAI // Futuro
    case hybrid // Migración gradual
}

@MainActor
class AIServiceConfiguration: ObservableObject {
    @Published var backend: MLBackend = {
        if #available(iOS 27.0, *) {
            return .coreAI
        } else {
            return .coreML
        }
    }()
    
    @Published var optimizationLevel: OptimizationLevel = .balanced
    @Published var memoryLimit: Int = 512 // MB
}

class AIServiceFactory {
    static func createImageAnalysisService(
        config: AIServiceConfiguration
    ) -> any MLInferenceProtocol<UIImage, AnalysisResult> {
        switch config.backend {
        case .coreML:
            return try! CoreMLInference(
                modelName: "ImageAnalysis",
                inputTransformer: imageToMLInput,
                outputTransformer: mlOutputToAnalysis
            )
        case .coreAI:
            // return CoreAIInference(...) cuando esté disponible
            fatalError("Core AI no disponible aún")
        case .hybrid:
            // Lógica de fallback
            return try! CoreMLInference(/* ... */)
        }
    }
}

3. Monitoreo y métricas

Para evaluar el impacto de la migración:

actor PerformanceMonitor {
    private var metrics: [String: [Double]] = [:]
    
    func record(operation: String, duration: TimeInterval) {
        if metrics[operation] == nil {
            metrics[operation] = []
        }
        metrics[operation]?.append(duration)
        
        // Mantener solo las últimas 100 mediciones
        if let array = metrics[operation], array.count > 100 {
            metrics[operation] = Array(array.suffix(100))
        }
    }
    
    func getAverageLatency(for operation: String) -> Double? {
        guard let durations = metrics[operation], !durations.isEmpty else {
            return nil
        }
        return durations.reduce(0, +) / Double(durations.count)
    }
    
    func exportMetrics() -> [String: Any] {
        var result: [String: Any] = [:]
        for (operation, durations) in metrics {
            result[operation] = [
                "average": durations.reduce(0, +) / Double(durations.count),
                "min": durations.min() ?? 0,
                "max": durations.max() ?? 0,
                "count": durations.count
            ]
        }
        return result
    }
}

// Uso en el servicio
extension MLInferenceProtocol {
    func timedPredict(_ input: Input) async throws -> Output {
        let startTime = CFAbsoluteTimeGetCurrent()
        
        defer {
            let duration = CFAbsoluteTimeGetCurrent() - startTime
            Task {
                await PerformanceMonitor.shared.record(
                    operation: "\(Self.self).predict",
                    duration: duration
                )
            }
        }
        
        return try await predict(input)
    }
}

Lecciones aprendidas en producción

Después de tres proyectos con IA on-device, estas son las lecciones más importantes:

1. La experiencia de usuario es más importante que la precisión del modelo Un modelo 95% preciso que tarda 3 segundos es peor que uno 90% preciso que responde en 400ms.

2. Gestión de memoria proactiva En dispositivos con 4GB RAM o menos, la gestión agresiva de memoria es crítica. Implementa eviction policies desde el principio.

3. Fallback siempre Ten un plan B para cuando la IA on-device falle. En Leonard AI, tenemos fallback a procesamiento en cloud para casos edge.

4. Testing exhaustivo en dispositivos reales Los simuladores no reflejan el rendimiento real. Testa en iPhone 11, 12, 13 y 14 mínimo.

5. Batching inteligente Para aplicaciones con múltiples usuarios, el batching puede reducir la latencia promedio significativamente.

Conclusión

La IA on-device en iOS está en un momento de transición. CoreML ha sido un primer paso sólido, pero Core AI promete solucionar muchas de sus limitaciones actuales. La clave está en preparar el código para esta migración mientras se optimiza la implementación actual.

Los frameworks van y vienen, pero los principios de optimización se mantienen: gestión inteligente de memoria, procesamiento asíncrono, monitoreo de performance y experiencia de usuario primera. Con Core AI en el horizonte, es el momento perfecto para revisar nuestras implementaciones actuales y prepararnos para el siguiente salto.

La IA on-device no es el futuro; es el presente. Y con las mejoras que vienen en iOS 27, será aún más fundamental en el desarrollo mobile moderno.