Back to blog
    Actualizaciones OTA de modelos: mantener tu IA en el dispositivo actualizada
    OTA updatesmodel managementdeploymentmobile AIinfrastructuresegment:mobile-builder

    Actualizaciones OTA de modelos: mantener tu IA en el dispositivo actualizada

    Como enviar actualizaciones de modelos a usuarios sin una release en la app store. Verificacion de versiones, descargas en segundo plano, estrategias de rollback y la infraestructura para entrega over-the-air de modelos.

    EErtas Team·

    Los modelos de IA en el dispositivo no son estaticos. Tus datos de entrenamiento mejoran, tu fine-tuning mejora y se lanzan nuevos modelos base. Actualizar el modelo no deberia requerir una actualizacion completa de la app a traves del App Store.

    Las actualizaciones over-the-air (OTA) de modelos te permiten enviar nuevos archivos GGUF a los usuarios independientemente del binario de la app. La app verifica actualizaciones, descarga el nuevo modelo en segundo plano y lo intercambia sin problemas.

    Arquitectura

    El manifiesto del modelo

    Aloja un manifiesto JSON en tu CDN junto al archivo del modelo:

    {
      "current_version": "2.1.0",
      "models": {
        "1b": {
          "url": "https://cdn.example.com/models/v2.1.0/model-1b-q4.gguf",
          "size_bytes": 612000000,
          "sha256": "a1b2c3d4e5f6...",
          "min_app_version": "3.0.0",
          "release_notes": "Precision de clasificacion mejorada"
        },
        "3b": {
          "url": "https://cdn.example.com/models/v2.1.0/model-3b-q4.gguf",
          "size_bytes": 1740000000,
          "sha256": "f6e5d4c3b2a1...",
          "min_app_version": "3.0.0",
          "release_notes": "Mejor calidad de conversacion"
        }
      },
      "rollback_version": "2.0.0",
      "rollback_url_1b": "https://cdn.example.com/models/v2.0.0/model-1b-q4.gguf",
      "rollback_url_3b": "https://cdn.example.com/models/v2.0.0/model-3b-q4.gguf"
    }
    

    El manifiesto le dice a la app: cual es la ultima version, donde descargarla, como verificarla, y a que recurrir si algo sale mal.

    Flujo de verificacion de actualizacion

    [Lanzamiento de app] -> [Obtener manifiesto del CDN]
      -> [Comparar version local con version del manifiesto]
      -> [Si hay version mas nueva disponible]:
          -> [Verificar WiFi + almacenamiento suficiente]
          -> [Descargar nuevo modelo en segundo plano]
          -> [Verificar SHA256]
          -> [Intercambiar modelo en siguiente inicio de sesion]
      -> [Si la version actual coincide]: [Sin accion]
    

    Implementacion

    // iOS: Verificar actualizaciones de modelo
    class ModelUpdater {
        private let manifestURL = URL(string: "https://cdn.example.com/manifest.json")!
    
        func checkForUpdate() async -> ModelUpdate? {
            guard let data = try? await URLSession.shared.data(from: manifestURL).0,
                  let manifest = try? JSONDecoder().decode(ModelManifest.self, from: data)
            else { return nil }
    
            let currentVersion = UserDefaults.standard.string(forKey: "model_version") ?? "0.0.0"
    
            if manifest.currentVersion > currentVersion {
                return ModelUpdate(
                    version: manifest.currentVersion,
                    url: manifest.models[selectedTier]!.url,
                    size: manifest.models[selectedTier]!.sizeBytes,
                    hash: manifest.models[selectedTier]!.sha256
                )
            }
            return nil
        }
    }
    
    // Android: Verificar actualizaciones al lanzar la app
    class ModelUpdater(private val context: Context) {
        suspend fun checkForUpdate(): ModelUpdate? = withContext(Dispatchers.IO) {
            val manifest = fetchManifest() ?: return@withContext null
            val currentVersion = prefs.getString("model_version", "0.0.0")
    
            if (manifest.currentVersion > currentVersion) {
                val model = manifest.models[selectedTier]
                ModelUpdate(
                    version = manifest.currentVersion,
                    url = model.url,
                    sizeBytes = model.sizeBytes,
                    sha256 = model.sha256
                )
            } else null
        }
    }
    

    Descarga en segundo plano

    Las descargas de modelos deben ocurrir en segundo plano sin bloquear al usuario:

    iOS: URLSession en segundo plano

    func downloadUpdate(_ update: ModelUpdate) {
        let config = URLSessionConfiguration.background(
            withIdentifier: "com.app.model-download"
        )
        let session = URLSession(configuration: config, delegate: self, delegateQueue: nil)
        let task = session.downloadTask(with: update.url)
        task.resume()
    }
    
    // El delegate maneja la completacion incluso si la app esta suspendida
    func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask,
                    didFinishDownloadingTo location: URL) {
        let destination = modelDirectory.appendingPathComponent("model-new.gguf")
        try? FileManager.default.moveItem(at: location, to: destination)
    
        if verifyHash(destination, expected: pendingUpdate.sha256) {
            // El intercambio ocurrira en el siguiente inicio de sesion
            UserDefaults.standard.set(pendingUpdate.version, forKey: "pending_model_version")
        } else {
            try? FileManager.default.removeItem(at: destination)
        }
    }
    

    Android: WorkManager

    class ModelDownloadWorker(
        context: Context, params: WorkerParameters
    ) : CoroutineWorker(context, params) {
    
        override suspend fun doWork(): Result {
            val url = inputData.getString("url") ?: return Result.failure()
            val expectedHash = inputData.getString("hash") ?: return Result.failure()
    
            val tempFile = File(applicationContext.cacheDir, "model-new.gguf")
    
            // Descargar
            downloadFile(url, tempFile) { progress ->
                setProgress(workDataOf("progress" to progress))
            }
    
            // Verificar
            if (tempFile.sha256() != expectedHash) {
                tempFile.delete()
                return Result.failure()
            }
    
            // Preparar para intercambio
            val destination = File(applicationContext.filesDir, "model-pending.gguf")
            tempFile.renameTo(destination)
    
            return Result.success()
        }
    }
    
    // Programar la descarga
    fun scheduleModelDownload(url: String, hash: String) {
        val request = OneTimeWorkRequestBuilder<ModelDownloadWorker>()
            .setConstraints(
                Constraints.Builder()
                    .setRequiredNetworkType(NetworkType.UNMETERED) // Solo WiFi
                    .setRequiresStorageNotLow(true)
                    .build()
            )
            .setInputData(workDataOf("url" to url, "hash" to hash))
            .build()
    
        WorkManager.getInstance(context).enqueue(request)
    }
    

    Intercambio de modelo

    No intercambies el modelo mientras esta cargado. Intercambia en un punto seguro:

    Estrategia de intercambio seguro

    1. La descarga se completa: Nuevo modelo guardado como model-pending.gguf
    2. En el siguiente lanzamiento de la app (o inicio de siguiente sesion de chat): a. Descargar modelo actual b. Renombrar model-current.gguf a model-previous.gguf c. Renombrar model-pending.gguf a model-current.gguf d. Cargar nuevo modelo e. Actualizar numero de version almacenado
    3. Si el nuevo modelo falla al cargar: Revertir a model-previous.gguf
    func swapModelIfPending() throws {
        let pendingPath = modelDirectory.appendingPathComponent("model-pending.gguf")
        let currentPath = modelDirectory.appendingPathComponent("model-current.gguf")
        let previousPath = modelDirectory.appendingPathComponent("model-previous.gguf")
    
        guard FileManager.default.fileExists(atPath: pendingPath.path) else { return }
    
        // Descargar modelo actual
        engine.unload()
    
        // Rotar archivos
        try? FileManager.default.removeItem(at: previousPath) // Eliminar respaldo viejo
        try? FileManager.default.moveItem(at: currentPath, to: previousPath) // Respaldar actual
        try FileManager.default.moveItem(at: pendingPath, to: currentPath) // Promover pendiente
    
        // Intentar cargar nuevo modelo
        do {
            try engine.load(at: currentPath.path)
            // Exito: actualizar version
            UserDefaults.standard.set(pendingVersion, forKey: "model_version")
        } catch {
            // Rollback
            try? FileManager.default.removeItem(at: currentPath)
            try? FileManager.default.moveItem(at: previousPath, to: currentPath)
            try engine.load(at: currentPath.path)
        }
    }
    

    Estrategia de rollback

    Siempre mantiene la version anterior del modelo disponible:

    • Rollback local: Mantiene model-previous.gguf en el dispositivo. Si el nuevo modelo falla al cargar o produce calidad pobre, revierte inmediatamente.
    • Rollback remoto: Incluye URLs de rollback en el manifiesto. Si descubres un problema de calidad del modelo, actualiza el manifiesto para apuntar a la version anterior. Todas las apps se "actualizaran" al modelo antiguo que funciona.
    • Rollback automatico: Si la app detecta fallos de inferencia o crashes despues de un intercambio de modelo, revierte automaticamente a la version anterior.

    Frecuencia de actualizacion

    EscenarioFrecuencia de actualizacionNotas
    Producto temprano (iterando rapido)Semanal-bisemanalMejoras rapidas de calidad
    Producto estableMensual-trimestralMejoras incrementales
    Nuevo modelo base disponibleSegun necesidadSaltos importantes de calidad
    Datos de entrenamiento cambian significativamenteSegun necesidadCambios de dominio

    Cada actualizacion es una ejecucion de fine-tuning ($5-50) mas distribucion CDN. El costo es minimo comparado con la mejora de calidad.

    Costos de infraestructura

    UsuariosDescargas/mesTamano del modeloCosto CDN (Cloudflare R2)
    1,000~200 (actualizaciones + nuevos usuarios)1.7GB~$0.01/mes
    10,000~2,0001.7GB~$0.05/mes
    100,000~20,0001.7GB~$0.51/mes

    Con los precios sin egreso de Cloudflare R2, la entrega OTA de modelos es esencialmente gratuita. Incluso con 100K usuarios, el costo CDN es menor a $1/mes.

    El paso de fine-tuning y exportacion GGUF es donde plataformas como Ertas agilizan el flujo de trabajo. Re-entrena con datos actualizados, exporta GGUF, sube al CDN, actualiza el manifiesto. Tus usuarios obtienen el modelo mejorado automaticamente.

    Ship AI that runs on your users' devices.

    Early bird pricing starts at $14.50/mo — locked in for life. Plans for builders and agencies.

    Keep reading