IoT e IA

Cómo desplegar modelos de ML en dispositivos edge: empaquetado, versionado y despliegues con MQTT

Una guía práctica para llevar modelos de machine learning a flotas de dispositivos edge, usando mensajes retenidos de MQTT para anunciar nuevas versiones y recoger el estado del despliegue de cada dispositivo.

Diagrama de cómo desplegar modelos de ML en dispositivos edge con un broker MQTT que anuncia versiones del modelo y dispositivos que informan del estado del despliegue
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

Entrenar un modelo es solo la mitad del trabajo: para aportar valor tienes que desplegar modelos de ML en dispositivos edge, mantenerlos actualizados y saber en todo momento qué versión ejecuta cada dispositivo. Esta guía muestra un patrón probado para empaquetar, versionar y desplegar modelos en una flota, usando MQTT para señalar las actualizaciones y recoger el estado mientras los archivos del modelo viajan por HTTPS.

Por qué es difícil desplegar modelos en el edge

Llevar un modelo a un servidor es un único despliegue. Llevarlo a cientos de gateways, cámaras o microcontroladores es un problema de sistemas distribuidos:

  • Los dispositivos se desconectan en momentos aleatorios y deben ponerse al día al reconectarse.
  • El hardware es diverso, así que un mismo artefacto de modelo puede no servir para todos los dispositivos.
  • Un modelo defectuoso puede degradar las predicciones en silencio en lugar de fallar, por lo que necesitas señales de salud más allá de "el proceso está en marcha".
  • El ancho de banda y el almacenamiento son limitados, y algunas instalaciones están detrás de cortafuegos que solo permiten conexiones salientes.

MQTT encaja bien con estas restricciones. Los dispositivos mantienen una única conexión saliente con un broker, los mensajes retenidos entregan al instante la última instrucción a quienes se incorporan tarde y un mensaje Last Will te avisa cuando un dispositivo se desconecta. Si este reparto de responsabilidades es nuevo para ti, nuestra panorámica de qué hace un broker MQTT es una buena introducción.

Paso 1: empaquetar el modelo como artefacto versionado

Trata cada modelo como una versión inmutable, nunca como un archivo que se sobrescribe. Un paquete de modelo debería contener:

Archivo Finalidad
model.tflite (o .onnx, _edgetpu.tflite, .engine) El modelo compilado para un runtime concreto
labels.txt o classes.json Nombres de las clases de salida en el orden correcto
manifest.json Versión, runtime, forma de la entrada, preprocesamiento, hardware de destino, checksum
preprocess.json (opcional) Constantes de normalización, modo de redimensionado, orden de canales

Las discrepancias de preprocesamiento son la causa más frecuente de "el modelo funciona en mi portátil pero no en el dispositivo". Registrar en el manifiesto el tamaño de entrada esperado, el orden de color y la normalización, y hacer que el dispositivo lo compruebe al cargar, evita toda una categoría de errores silenciosos.

Elige el formato según el destino:

Hardware de destino Formato típico Notas
Microcontrolador (ESP32-S3, Cortex-M) TensorFlow Lite Micro, int8 Normalmente compilado dentro del firmware; se actualiza mediante OTA de firmware
Raspberry Pi / gateway ARM TFLite u ONNX Runtime Se puede sustituir en tiempo de ejecución como archivo
Coral Edge TPU _edgetpu.tflite Debe compilarse con el compilador de Edge TPU
NVIDIA Jetson Motor TensorRT u ONNX Los motores dependen de la GPU y de la versión de TensorRT, así que compílalos por tipo de dispositivo

En los microcontroladores, el modelo suele vivir dentro de la imagen del firmware, así que actualizar el modelo es actualizar el firmware. Nuestro tutorial de TensorFlow Lite Micro cubre ese caso; el resto de esta guía se centra en dispositivos de clase Linux que cargan los modelos desde disco.

Paso 2: versionar semánticamente y firmar cada versión

Usa una cadena de versión que diga algo a los operadores, como defect-classifier 2.3.0, donde el número mayor cambia cuando las entradas o salidas cambian de forma incompatible. Junto a la versión, calcula un checksum SHA-256 del artefacto y publícalo en el manifiesto. Los dispositivos deben rechazar cualquier archivo cuyo checksum no coincida. Para una garantía mayor, firma el manifiesto con una clave privada custodiada por tu sistema de CI y distribuye solo la clave pública a los dispositivos.

Guarda los artefactos en un almacenamiento de objetos con HTTPS o en un servidor de archivos interno. MQTT es el canal equivocado para binarios de varios megabytes: el broker tendría que mantener payloads grandes en memoria y muchos planes limitan el tamaño de los mensajes. El plan Serverless gratuito de ConnectMQ, por ejemplo, limita los mensajes a 256 KB, de sobra para un manifiesto pero no para un modelo de visión.

Paso 3: señalar las actualizaciones por MQTT

El plano de control usa tres topics por dispositivo (o por grupo de dispositivos):

fleet/<site>/<device>/model/desired    retained, written by the deployment service
fleet/<site>/<device>/model/reported   retained, written by the device
fleet/<site>/<device>/model/events     not retained, progress and errors

El servicio de despliegue publica un mensaje de estado deseado, retenido para que un dispositivo desconectado lo reciba en cuanto se conecte:

{
  "model": "defect-classifier",
  "version": "2.3.0",
  "url": "https://models.example.com/defect-classifier/2.3.0/model.tflite",
  "sha256": "9f2c4c1d5e8b0a7f3d6e1b2c4a5f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b2a3f",
  "runtime": "tflite",
  "rollout": "canary",
  "issuedAt": "2026-10-16T08:00:00Z"
}

El dispositivo compara el estado deseado con lo que ejecuta, descarga, verifica, sustituye e informa:

{
  "model": "defect-classifier",
  "version": "2.3.0",
  "state": "active",
  "previous": "2.2.1",
  "loadedAt": "2026-10-16T08:03:12Z",
  "selfTest": "passed"
}

Este patrón de estado deseado/informado es la misma idea que usan las sombras de dispositivo (device shadows) y funciona con cualquier broker que cumpla el estándar.

Paso 4: un agente de actualización mínimo para el dispositivo

El agente siguiente se ejecuta en un dispositivo edge con Linux. Se suscribe a su topic de estado deseado, descarga el modelo, verifica el checksum, ejecuta una prueba rápida, sustituye el modelo activo de forma atómica y conserva la versión anterior para poder revertir.

import hashlib
import json
import os
import shutil
import time
import urllib.request
from pathlib import Path

import paho.mqtt.client as mqtt

DEVICE = "gw-07"
BASE = f"connectmq/demo/a91e4d/fleet/plant1/{DEVICE}/model"
MODELS = Path("/opt/models")
ACTIVE = MODELS / "active"           # enlace simbólico al directorio de la versión activa
STATE_FILE = MODELS / "state.json"


def current_version():
    try:
        return json.loads(STATE_FILE.read_text())["version"]
    except (FileNotFoundError, KeyError, json.JSONDecodeError):
        return None


def report(client, state, **extra):
    body = {"model": extra.pop("model", None), "state": state,
            "version": extra.pop("version", current_version()),
            "ts": int(time.time() * 1000), **extra}
    client.publish(f"{BASE}/reported", json.dumps(body), qos=1, retain=True)


def event(client, msg):
    client.publish(f"{BASE}/events", json.dumps({"msg": msg, "ts": int(time.time() * 1000)}), qos=0)


def sha256_of(path):
    h = hashlib.sha256()
    with open(path, "rb") as f:
        for chunk in iter(lambda: f.read(1 << 20), b""):
            h.update(chunk)
    return h.hexdigest()


def smoke_test(model_path):
    # Carga el modelo y ejecuta una inferencia con una entrada de muestra guardada.
    # Sustitúyelo por tu runtime, p. ej. tflite_runtime.interpreter.Interpreter.
    return model_path.stat().st_size > 0


def apply_update(client, desired):
    version = desired["version"]
    if version == current_version():
        report(client, "active", model=desired["model"], version=version)
        return
    target = MODELS / version
    target.mkdir(parents=True, exist_ok=True)
    tmp = target / "model.tmp"
    event(client, f"downloading {version}")
    urllib.request.urlretrieve(desired["url"], tmp)
    if sha256_of(tmp) != desired["sha256"]:
        tmp.unlink(missing_ok=True)
        report(client, "failed", model=desired["model"], reason="checksum mismatch")
        return
    final = target / "model.tflite"
    tmp.replace(final)
    if not smoke_test(final):
        report(client, "failed", model=desired["model"], reason="smoke test failed")
        return
    previous = current_version()
    link_tmp = MODELS / "active.tmp"
    if link_tmp.exists() or link_tmp.is_symlink():
        link_tmp.unlink()
    link_tmp.symlink_to(target)
    os.replace(link_tmp, ACTIVE)  # sustitución atómica del enlace simbólico
    STATE_FILE.write_text(json.dumps({"version": version, "previous": previous}))
    report(client, "active", model=desired["model"], version=version, previous=previous)
    # Conserva en disco solo la versión actual y la anterior
    for d in MODELS.iterdir():
        if d.is_dir() and not d.is_symlink() and d.name not in (version, previous):
            shutil.rmtree(d, ignore_errors=True)


def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe(f"{BASE}/desired", qos=1)
    report(client, "online")


def on_message(client, userdata, msg):
    try:
        apply_update(client, json.loads(msg.payload))
    except Exception as exc:  # informa de cada fallo en lugar de morir en silencio
        report(client, "failed", reason=str(exc))


MODELS.mkdir(parents=True, exist_ok=True)
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"{DEVICE}-a91e4d")
client.on_connect = on_connect
client.on_message = on_message
client.will_set(f"{BASE}/reported", json.dumps({"state": "offline"}), qos=1, retain=True)
client.connect("broker.connectmq.com", 1883, keepalive=60)
client.loop_forever()

Ten en cuenta que la descarga se ejecuta dentro del callback del mensaje, lo que bloquea el bucle de red. Es aceptable para modelos pequeños y un entorno de pruebas; en producción, delega el trabajo en un hilo worker para que los keepalives continúen durante las descargas largas. Tu proceso de inferencia debe abrir el modelo a través del enlace simbólico active y recargarlo cuando cambie, por ejemplo vigilando el archivo de estado.

Para disparar un despliegue durante las pruebas, publica un mensaje de estado deseado retenido:

mosquitto_pub -h broker.connectmq.com -p 1883 -i deployer-a91e4d -r -q 1 \
  -t "connectmq/demo/a91e4d/fleet/plant1/gw-07/model/desired" \
  -f desired.json

El servidor MQTT público gratuito sirve para probar el flujo y para prototipos, no para producción, pero recuerda que cualquiera puede leerlo, así que nunca pongas allí URL reales de modelos ni tokens firmados.

Paso 5: desplegar por fases

Nunca envíes un modelo nuevo a toda la flota de golpe. Un despliegue por fases tiene este aspecto:

  1. Modo sombra. El nuevo modelo se ejecuta junto al antiguo en unos pocos dispositivos y se publican ambas salidas, pero solo el antiguo dispara acciones. Compáralas fuera de línea.
  2. Canary. Publica el estado deseado para un grupo reducido de dispositivos. Vigila su estado informado, la latencia de inferencia y la distribución de las predicciones.
  3. Oleadas progresivas. Amplía a grupos más grandes, con pausas entre oleadas. Un script de despliegue simplemente escribe el topic de estado deseado para el siguiente lote de dispositivos.
  4. Despliegue completo. Cuando la última oleada esté sana, convierte la nueva versión en la predeterminada para los dispositivos que se aprovisionen a partir de ese momento.
  5. Reversión. Si las métricas empeoran, publica la versión anterior como estado deseado. Como el dispositivo conserva la última versión en disco, la reversión ni siquiera necesita una descarga.

La pertenencia a los grupos puede residir en tu servicio de despliegue, o puedes codificarla en los topics, por ejemplo fleet/plant1/canary/model/desired, al que un dispositivo del grupo canary se suscribe además de a su propio topic.

Monitorizar qué ejecuta cada dispositivo

Como cada dispositivo publica un mensaje reported retenido, una sola suscripción con comodín a fleet/+/+/model/reported proporciona un inventario completo de la flota en el momento de conectarse. Combínalo con la telemetría de predicciones para vigilar la deriva: si la distribución de clases o las puntuaciones de confianza cambian bruscamente tras una actualización, probablemente el modelo esté viendo entradas con las que no se entrenó. Nuestro artículo sobre MLOps para IoT profundiza en la monitorización y los ciclos de reentrenamiento.

Lista de comprobación para producción

  1. Los artefactos son inmutables, están versionados y se almacenan en HTTPS.
  2. Cada manifiesto incluye un checksum SHA-256 e, idealmente, una firma.
  3. Los dispositivos verifican, hacen una prueba rápida y sustituyen de forma atómica, conservando una versión anterior.
  4. Los estados deseado e informado viven en topics MQTT retenidos, con un Last Will para detectar desconexiones.
  5. Los despliegues pasan por modo sombra, canary, oleadas y despliegue completo, con reversión mediante un solo mensaje.
  6. Los brokers usan TLS en el puerto 8883 y credenciales por dispositivo.

Para flotas en producción, un broker MQTT gestionado se encarga de los certificados, el escalado y la autenticación para que tu equipo pueda centrarse en los modelos. Con ConnectMQ, cada dispositivo recibe credenciales cuyo client ID está vinculado a él, y el explorador de topics en directo del portal muestra los mensajes de estado deseado e informado a medida que avanza un despliegue. Para el contexto más amplio de cómo combinar modelos y dispositivos conectados, consulta nuestra guía completa de IA e IoT.

Empieza con ConnectMQ

ConnectMQ es un broker MQTT gestionado compatible con MQTT 3.1.1 y 5.0 sobre TCP, TLS y WebSockets, listo para transportar las señales de actualización de tus modelos. Regístrate en el plan Serverless gratuito, sin tarjeta de crédito, y pon en marcha hoy tu primer despliegue.

Preguntas frecuentes

¿Puedo enviar el propio archivo del modelo por MQTT?

Puedes dividir en fragmentos los archivos pequeños, pero rara vez merece la pena. Usa MQTT para la señal de actualización y el estado, y descarga el artefacto por HTTPS, donde las descargas reanudables y la caché son estándar.

¿Cómo recibe la actualización un dispositivo que estaba desconectado?

Como el mensaje de estado deseado es retenido, el broker lo entrega inmediatamente cuando el dispositivo se reconecta y se suscribe. Después, el dispositivo compara las versiones y se actualiza si es necesario.

¿Cómo revierto un modelo defectuoso?

Publica la versión anterior como estado deseado. Un dispositivo que conserva su última versión en disco puede volver a ella al instante sin descargar nada.

¿Los microcontroladores usan el mismo enfoque?

La señalización es la misma, pero el modelo suele estar compilado dentro del firmware, así que el dispositivo realiza una actualización OTA del firmware en lugar de sustituir un archivo de modelo.

Artículos relacionados

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.