Back to blog
    OTA updatesmodel managementdeploymentmobile AIinfrastructure

    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.

    Edward Xi Yang

    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.

    Pregunta a la IA sobre este artículo

    Ship AI that runs on your users' devices.

    Free plan with 30 credits/mo, no card required. Paid plans from $10/mo USD.

    Keep reading