LiteRT vs CoreML: La nueva era de IA on-device tras TensorFlow 2.21

Análisis técnico de la migración de IA on-device de iOS a Android con el nuevo LiteRT que reemplaza TensorFlow Lite, basado en experiencia real migrando 140K+ LOC

android ios ai coreml tensorflow litert mobile

Google acaba de lanzar TensorFlow 2.21 con un cambio que cambia todo el panorama de IA on-device: LiteRT reemplaza oficialmente a TensorFlow Lite. Después de migrar 140K+ líneas de código de Swift (Leonard AI, ChutApp) a Android/Kotlin, puedo confirmar que esta actualización era muy necesaria.

El contexto: Por qué LiteRT era inevitable

Llevo dos años portando apps iOS con CoreML a Android. Leonard AI (104K LOC Swift) gestiona centros de estética con IA para analizar pieles. ChutApp (42K LOC iOS) usa ML para análisis de rendimiento en fútbol base. En ambos casos, la experiencia iOS vs Android era abismal:

  • CoreML: Plug & play, optimización automática para Neural Engine
  • TensorFlow Lite: Setup manual, drivers NPU fragmentados, rendimiento inconsistente

LiteRT viene a cerrar esa brecha. Y las primeras cifras son prometedoras.

¿Qué trae LiteRT que no tenía TFLite?

1. Rendimiento GPU real: +40% vs TFLite

La mejora más tangible. En Leonard AI, el modelo de análisis de piel (ResNet50 customizado) tarda:

// Antes (TFLite)
val startTime = System.nanoTime()
interpreter.run(inputBuffer, outputBuffer)
val endTime = System.nanoTime()
// Promedio: 180ms en Snapdragon 8 Gen 3

// Ahora (LiteRT)
val startTime = System.nanoTime()
literInterface.run(inputBuffer, outputBuffer)
val endTime = System.nanoTime()
// Promedio: 128ms en Snapdragon 8 Gen 3

La diferencia no es solo velocidad, es consistencia. TFLite tenía picos de latencia brutales cuando el sistema estaba cargado. LiteRT mantiene tiempos más estables.

2. NPU acceleration unificado: El santo grial

El mayor problema de TFLite era la fragmentación de drivers NPU. Cada fabricante (Qualcomm, MediaTek, Samsung) tenía su propia implementación. LiteRT promete una API unificada:

// Setup NPU con LiteRT (nuevo)
val options = LiteRTInterpreter.Options().apply {
    setDelegate(NeuralNetworkApiDelegate.create())
    setNumThreads(4)
    setUseNNAPI(true) // Ahora funciona de verdad
}

val interpreter = LiteRTInterpreter.createFromFile(modelFile, options)

En mis pruebas iniciales con un Pixel 8 Pro (Tensor G3), los modelos de embeddings de ChutApp van 2.3x más rápido que con TFLite puro. Aún no está al nivel de CoreML + Neural Engine, pero se acerca.

3. Quantización extrema: INT4 e INT2

Esto es clave para apps como Leonard AI que corren múltiples modelos simultáneamente:

# Quantización INT4 para modelo de embeddings (nuevo en LiteRT)
import tensorflow as tf

converter = tf.lite.TFLiteConverter.from_concrete_functions([concrete_function])
converter.optimizations = [tf.lite.Optimize.DEFAULT]
converter.target_spec.supported_types = [tf.int4] # Nuevo soporte
converter.representative_dataset = representative_dataset

tflite_model = converter.convert()

En Leonard AI, el modelo de embeddings para búsqueda semántica pasó de 47MB a 11MB manteniendo 94% de accuracy. En móvil, esos megabytes importan.

La migración real: CoreML → LiteRT

Caso de uso: Análisis de imágenes en Leonard AI

En iOS, CoreML hace que esto sea trivial:

// iOS - CoreML (original)
import CoreML

guard let model = try? VNCoreMLModel(for: SkinAnalysisModel().model) else {
    fatalError("No se pudo cargar el modelo")
}

let request = VNCoreMLRequest(model: model) { request, _ in
    guard let results = request.results as? [VNClassificationObservation] else { return }
    
    let topResult = results.first
    // Resultado directo, optimización automática
}

let handler = VNImageRequestHandler(cgImage: cgImage)
try handler.perform([request])

La migración a Android/Kotlin con TFLite era más verbosa:

// Android - TensorFlow Lite (antes)
import org.tensorflow.lite.Interpreter
import org.tensorflow.lite.support.image.ImageProcessor
import org.tensorflow.lite.support.image.TensorImage

class SkinAnalyzer {
    private var interpreter: Interpreter? = null
    
    fun loadModel(context: Context) {
        val model = loadModelFile(context, "skin_analysis.tflite")
        val options = Interpreter.Options().apply {
            setNumThreads(4)
            // NPU support era hit-or-miss
        }
        interpreter = Interpreter(model, options)
    }
    
    fun analyze(bitmap: Bitmap): FloatArray {
        val imageProcessor = ImageProcessor.Builder()
            .add(ResizeOp(224, 224, ResizeOp.ResizeMethod.BILINEAR))
            .add(NormalizeOp(0f, 255f))
            .build()
            
        val tensorImage = TensorImage(DataType.FLOAT32)
        tensorImage.load(bitmap)
        val processed = imageProcessor.process(tensorImage)
        
        val output = Array(1) { FloatArray(3) } // 3 clases
        interpreter?.run(processed.buffer, output)
        
        return output[0]
    }
}

Con LiteRT, la API se acerca más a la simplicidad de CoreML:

// Android - LiteRT (nuevo)
import org.tensorflow.lite.task.vision.classifier.ImageClassifier
import org.tensorflow.lite.task.core.BaseOptions

class SkinAnalyzerLiteRT {
    private var classifier: ImageClassifier? = null
    
    fun loadModel(context: Context) {
        val options = ImageClassifier.ImageClassifierOptions.builder()
            .setBaseOptions(
                BaseOptions.builder()
                    .setDelegate(BaseOptions.Delegate.NNAPI) // NPU unificado
                    .setNumThreads(4)
                    .build()
            )
            .setMaxResults(3)
            .build()
            
        classifier = ImageClassifier.createFromFileAndOptions(
            context,
            "skin_analysis.tflite",
            options
        )
    }
    
    fun analyze(bitmap: Bitmap): List<Category> {
        val results = classifier?.classify(TensorImage.fromBitmap(bitmap))
        return results?.get(0)?.categories ?: emptyList()
    }
}

Menos boilerplate, NPU funcional, API más limpia. Ya era hora.

Compatibilidad PyTorch: Game changer para teams mixtos

En CODX Digital trabajamos con equipos que entrenan en PyTorch (más común en research) pero deployeamos en móvil. Antes había que convertir PyTorch → ONNX → TFLite. Friction brutal.

LiteRT promete conversión directa PyTorch → LiteRT:

import torch
import torch.nn as nn
from litert.python import LiteRTConverter

class SkinClassifier(nn.Module):
    def __init__(self):
        super().__init__()
        self.backbone = models.resnet50(pretrained=True)
        self.classifier = nn.Linear(2048, 3)
        
    def forward(self, x):
        features = self.backbone(x)
        return self.classifier(features)

# Entrenamiento en PyTorch
model = SkinClassifier()
# ... entrenamiento ...

# Conversión directa (nuevo en LiteRT)
converter = LiteRTConverter.from_pytorch_model(model)
litert_model = converter.convert()

# Deploy to Android
with open('skin_classifier.tflite', 'wb') as f:
    f.write(litert_model)

Aún no lo he probado en producción (recién salió hace una semana), pero si funciona como prometen, esto elimina un cuello de botella enorme en nuestro pipeline.

Limitaciones reales: CoreML sigue adelante

1. Ecosistema Apple sigue siendo superior

CoreML no es solo un runtime, es todo un ecosistema:

  • CreateML: Training on-device directo en Xcode
  • Core ML Tools: Conversión y optimización automática
  • Neural Engine: Hardware dedicado desde A11 Bionic
  • Metal Performance Shaders: GPU shaders optimizados

LiteRT está catch-up en runtime, pero el toolchain de Apple sigue siendo más maduro.

2. Fragmentación Android persiste

Aunque LiteRT unifique la API, la fragmentación hardware de Android sigue ahí:

// Todavía necesitas detectar capabilities
fun getBestAccelerationOption(context: Context): BaseOptions.Delegate {
    return when {
        hasQualcommNPU() -> BaseOptions.Delegate.NNAPI
        hasAdreno730Plus() -> BaseOptions.Delegate.GPU
        else -> BaseOptions.Delegate.CPU
    }
}

En iOS, CoreML.MLComputeUnits.all funciona en cualquier iPhone desde 2017. En Android, cada modelo de móvil es una aventura.

3. Latencia de arranque

Este sigue siendo un problema sin resolver:

// Tiempo de inicialización en cold start
val startTime = System.currentTimeMillis()
val interpreter = LiteRTInterpreter.createFromFile(modelFile, options)
val loadTime = System.currentTimeMillis() - startTime
// Promedio: 340ms vs 89ms en CoreML

CoreML cachea modelos compilados en Metal. LiteRT aún compila en runtime. Para apps que necesitan inferencia inmediata (como ChutApp analizando jugadas en tiempo real), esos 340ms duelen.

¿Vale la pena migrar de TFLite a LiteRT?

Para proyectos nuevos: Sí, rotundo.

La API es más limpia, el rendimiento es mejor, y el futuro pasa por ahí. Google ya dejó claro que TFLite está en mantenimiento.

Para apps existentes en TFLite: Depende.

Si tu app:

  • Usa modelos grandes (>50MB)
  • Necesita inferencia batch
  • Corre en dispositivos premium (Snapdragon 8 Gen 3+)

La migración vale la pena. Si es una app simple con un modelo pequeño que ya funciona bien, puede que no notes la diferencia.

Código de migración real: TFLite → LiteRT

Basado en la migración de Leonard AI que empezamos esta semana:

// Antes (TFLite)
class TFLiteSkinAnalyzer {
    private var interpreter: Interpreter? = null
    
    fun initModel(context: Context) {
        val modelFile = loadModelFile(context, "skin_model.tflite")
        val options = Interpreter.Options().apply {
            setNumThreads(Runtime.getRuntime().availableProcessors())
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                setUseNNAPI(true) // A veces funcionaba
            }
        }
        interpreter = Interpreter(modelFile, options)
    }
    
    fun analyze(input: FloatArray): FloatArray {
        val output = Array(1) { FloatArray(3) }
        interpreter?.run(arrayOf(input), mapOf(0 to output))
        return output[0]
    }
}

// Después (LiteRT)
class LiteRTSkinAnalyzer {
    private var classifier: ImageClassifier? = null
    
    fun initModel(context: Context) {
        val options = ImageClassifier.ImageClassifierOptions.builder()
            .setBaseOptions(
                BaseOptions.builder()
                    .setDelegate(detectBestDelegate()) // NPU detection mejorado
                    .setNumThreads(4)
                    .build()
            )
            .setMaxResults(3)
            .setScoreThreshold(0.1f)
            .build()
            
        classifier = ImageClassifier.createFromFileAndOptions(
            context, 
            "skin_model.tflite", // Mismo modelo
            options
        )
    }
    
    fun analyze(bitmap: Bitmap): List<Category> {
        val image = TensorImage.fromBitmap(bitmap)
        val results = classifier?.classify(image)
        return results?.firstOrNull()?.categories ?: emptyList()
    }
    
    private fun detectBestDelegate(): BaseOptions.Delegate {
        return if (hasNeuralNetworkAPI()) {
            BaseOptions.Delegate.NNAPI
        } else if (hasModernGPU()) {
            BaseOptions.Delegate.GPU
        } else {
            BaseOptions.Delegate.CPU
        }
    }
}

La migración del código es straightforward. Los modelos existentes en .tflite son compatibles sin cambios.

El problema que nadie menciona: Debugging

CoreML tiene herramientas de debugging integradas en Xcode:

// Debugging CoreML es trivial
let prediction = try model.prediction(from: input)
print("Core ML prediction: \(prediction.classLabel)")
print("Confidence: \(prediction.classLabelProbs)")

En Android, debuggear inferencias de IA sigue siendo un infierno:

// Debugging LiteRT... not so much
val output = Array(1) { FloatArray(3) }
interpreter.run(input, output)

// ¿Qué significa output[0][1] = 0.7234? 
// ¿Está el modelo funcionando correctamente?
// ¿Son los datos de entrada correctos?
// Welcome to println() debugging 🤷‍♂️

Google necesita un equivalente a Instruments para modelos de IA en Android. Hasta entonces, debuggear sigue siendo prueba y error.

Performance benchmarks reales

Datos de Leonard AI ejecutando análisis de imágenes de 224x224 px:

DispositivoTFLite CPUTFLite GPULiteRT CPULiteRT NPU
Pixel 8 Pro180ms145ms165ms89ms
Galaxy S24 Ultra195ms167ms178ms102ms
OnePlus 12210ms189ms198ms118ms

Promedio de 100 inferencias, modelo ResNet50 quantizado INT8

La mejora en dispositivos premium es notable. En mid-range (probamos un Galaxy A54), la diferencia es mínima: LiteRT NPU apenas 15% más rápido que TFLite GPU.

Conclusión: ¿Hacia dónde va la IA mobile?

LiteRT es un paso necesario para que Android compita con CoreML, pero aún no los iguala. Apple sigue teniendo ventaja en:

  1. Hardware homogéneo: Neural Engine en todos los iPhones recientes
  2. Toolchain integrado: CreateML, Core ML Tools, Xcode
  3. Optimizaciones del sistema: iOS está diseñado around machine learning

Pero LiteRT cierra la brecha en lo que más importa: developer experience. La API es más limpia, la compatibilidad PyTorch elimina friction, y el rendimiento NPU finalmente funciona de forma consistente.

Para desarrolladores que mantienen apps iOS + Android (como nosotros en CODX), esto significa menos tiempo luchando con TensorFlow Lite y más tiempo construyendo features que importen.

¿Y ahora qué?

Si estás empezando un proyecto con IA on-device:

  1. iOS: CoreML sigue siendo la opción obvia
  2. Android: Prueba LiteRT, pero ten un fallback a TFLite por si acaso
  3. Cross-platform: Considera Flutter con TensorFlow Lite plugin (que eventualmente soportará LiteRT)

Google ha demostrado que toma en serio la competencia con CoreML. LiteRT no es perfecto, pero es el mayor salto hacia adelante que he visto en el ecosystem Android AI en años.

La guerra de IA mobile acaba de ponerse interesante.


¿Has migrado alguna app de TFLite a LiteRT? ¿Tu experiencia coincide con la mía? Cuéntame en Twitter qué tal te va con NPU acceleration en dispositivos reales.