Treinar um modelo é só metade do trabalho: para gerar valor, você precisa implantar modelos de ML em dispositivos de borda, mantê-los atualizados e saber a qualquer momento qual versão cada dispositivo está executando. Este guia mostra um padrão comprovado para empacotar, versionar e distribuir modelos em uma frota, usando o MQTT para sinalizar atualizações e coletar status, enquanto os próprios arquivos de modelo trafegam via HTTPS.
Por que a implantação de modelos na borda é difícil
Entregar um modelo a um servidor é uma única implantação. Entregá-lo a centenas de gateways, câmeras ou microcontroladores, espalhados por fazendas, frigoríficos ou plantas em Manaus e Joinville, é um problema de sistemas distribuídos:
- Os dispositivos ficam offline em momentos aleatórios e precisam se atualizar quando se reconectam.
- O hardware varia, então um único artefato de modelo pode não servir para todos os dispositivos.
- Um modelo ruim pode degradar as previsões silenciosamente em vez de travar, então você precisa de sinais de saúde além de "o processo está rodando".
- A largura de banda e o armazenamento são limitados, e alguns locais ficam atrás de firewalls que só permitem conexões de saída.
O MQTT se encaixa bem nessas restrições. Os dispositivos mantêm uma única conexão de saída com um broker, as mensagens retidas entregam imediatamente a instrução mais recente a quem se conecta depois e uma mensagem de Last Will avisa quando um dispositivo cai. Se você é novo nessa divisão de responsabilidades, nossa visão geral sobre o que faz um broker MQTT é uma boa introdução.
Passo 1: empacote o modelo como um artefato versionado
Trate cada modelo como uma release imutável, nunca como um arquivo que é sobrescrito. Um pacote de modelo deve conter:
| Arquivo | Finalidade |
|---|---|
model.tflite (ou .onnx, _edgetpu.tflite, .engine) |
O modelo compilado para um runtime específico |
labels.txt ou classes.json |
Nomes das classes de saída na ordem correta |
manifest.json |
Versão, runtime, formato da entrada, pré-processamento, hardware-alvo, checksum |
preprocess.json (opcional) |
Constantes de normalização, modo de redimensionamento, ordem dos canais |
Incompatibilidades de pré-processamento são a causa mais frequente de "o modelo funciona no meu notebook, mas não no dispositivo". Registrar no manifesto o tamanho de entrada esperado, a ordem das cores e a normalização, e fazer o dispositivo verificá-los no carregamento, evita toda uma classe de erros silenciosos.
Escolha o formato de acordo com o alvo:
| Hardware-alvo | Formato típico | Observações |
|---|---|---|
| Microcontrolador (ESP32-S3, Cortex-M) | TensorFlow Lite Micro, int8 | Geralmente compilado no firmware; atualizado via OTA de firmware |
| Raspberry Pi / gateway ARM | TFLite ou ONNX Runtime | Pode ser trocado em tempo de execução como arquivo |
| Coral Edge TPU | _edgetpu.tflite |
Precisa ser compilado com o Edge TPU compiler |
| NVIDIA Jetson | Engine TensorRT ou ONNX | Engines ficam atreladas à GPU e à versão do TensorRT, então gere uma por tipo de dispositivo |
Em microcontroladores, o modelo muitas vezes fica dentro da imagem do firmware, então uma atualização de modelo é uma atualização de firmware. Nosso tutorial de TensorFlow Lite Micro cobre esse caso; o restante deste guia se concentra em dispositivos da classe Linux que carregam modelos a partir do disco.
Passo 2: versione semanticamente e assine cada release
Use uma string de versão que diga algo aos operadores, como defect-classifier 2.3.0, em que o número principal (major) muda quando entradas ou saídas mudam de forma incompatível. Junto com a versão, calcule um checksum SHA-256 do artefato e publique-o no manifesto. Os dispositivos devem recusar um arquivo cujo checksum não confira. Para maior garantia, assine o manifesto com uma chave privada mantida pelo seu sistema de CI e distribua aos dispositivos apenas a chave pública.
Armazene os artefatos em um object storage via HTTPS ou em um servidor de arquivos interno. O MQTT é o canal errado para binários de vários megabytes: o broker teria de manter payloads grandes na memória, e muitos planos limitam o tamanho das mensagens. O plano gratuito Serverless do ConnectMQ, por exemplo, limita as mensagens a 256 KB, o que é mais do que suficiente para um manifesto, mas não para um modelo de visão.
Passo 3: sinalize as atualizações via MQTT
O plano de controle usa três topics por dispositivo (ou por grupo de dispositivos):
fleet/<site>/<device>/model/desired retido, escrito pelo serviço de implantação
fleet/<site>/<device>/model/reported retido, escrito pelo dispositivo
fleet/<site>/<device>/model/events não retido, progresso e erros
O serviço de implantação publica uma mensagem de estado desejado, retida para que um dispositivo offline a receba assim que se conectar:
{
"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"
}
O dispositivo compara o estado desejado com o que está executando, baixa, verifica, troca e informa de volta:
{
"model": "defect-classifier",
"version": "2.3.0",
"state": "active",
"previous": "2.2.1",
"loadedAt": "2026-10-16T08:03:12Z",
"selfTest": "passed"
}
Esse padrão desired/reported é a mesma ideia usada pelos device shadows e funciona com qualquer broker compatível com o padrão.
Passo 4: um agente de atualização mínimo para o dispositivo
O agente abaixo roda em um dispositivo de borda Linux. Ele assina o seu topic de estado desejado, baixa o modelo, verifica o checksum, executa um smoke test, troca o modelo ativo de forma atômica e mantém a versão anterior para rollback.
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" # symlink para o diretório da versão ativa
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):
# Carrega o modelo e executa uma inferência com uma entrada de exemplo armazenada.
# Substitua pelo seu runtime, p. ex. 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) # troca atômica do symlink
STATE_FILE.write_text(json.dumps({"version": version, "previous": previous}))
report(client, "active", model=desired["model"], version=version, previous=previous)
# Mantém em disco apenas a versão atual e a 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 cada falha em vez de morrer em silêncio
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()
Observe que o download roda dentro do callback de mensagem, o que bloqueia o loop de rede. Isso é aceitável para modelos pequenos e para um ambiente de testes; em produção, passe o trabalho para uma thread de worker para que os keepalives continuem durante downloads longos. O seu processo de inferência deve abrir o modelo pelo symlink active e recarregá-lo quando ele mudar, por exemplo observando o arquivo de estado.
Para disparar um rollout durante os testes, publique uma mensagem de estado desejado retida:
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
O servidor MQTT público serve para experimentar o fluxo, mas lembre-se de que qualquer pessoa pode lê-lo; portanto, nunca coloque ali URLs reais de modelos nem tokens assinados.
Passo 5: faça o rollout em etapas
Nunca envie um modelo novo para a frota inteira de uma só vez. Um rollout em etapas fica assim:
- Modo sombra. O novo modelo roda ao lado do antigo em alguns dispositivos e as duas saídas são publicadas, mas apenas o antigo aciona ações. Compare-as offline.
- Canary. Publique o estado desejado para um pequeno grupo de dispositivos. Acompanhe o estado informado, a latência de inferência e a distribuição das previsões.
- Ondas progressivas. Expanda para grupos maiores, fazendo pausas entre as ondas. Um script de implantação simplesmente escreve o topic de estado desejado para o próximo lote de dispositivos.
- Rollout completo. Quando a última onda estiver saudável, torne a nova versão o padrão para os dispositivos provisionados daí em diante.
- Rollback. Se as métricas piorarem, publique a versão anterior como estado desejado. Como o dispositivo mantém a última versão em disco, o rollback nem precisa de download.
A associação aos grupos pode ficar no seu serviço de implantação, ou você pode codificá-la nos topics, por exemplo fleet/plant1/canary/model/desired, que um dispositivo do grupo canary assina além do seu próprio topic.
Monitorando o que cada dispositivo executa
Como cada dispositivo publica uma mensagem reported retida, uma única assinatura com wildcard em fleet/+/+/model/reported fornece um inventário completo da frota no momento em que se conecta. Combine isso com a telemetria de previsões para detectar desvios: se a distribuição de classes ou os índices de confiança mudarem bruscamente após uma atualização, provavelmente o modelo está vendo entradas com as quais não foi treinado. Nosso artigo sobre MLOps para IoT aprofunda o monitoramento e os ciclos de retreinamento.
Checklist de produção
- Os artefatos são imutáveis, versionados e armazenados via HTTPS.
- Todo manifesto traz um checksum SHA-256 e, de preferência, uma assinatura.
- Os dispositivos verificam, executam smoke test e fazem a troca de forma atômica, mantendo uma versão anterior.
- O estado desejado e o informado ficam em topics MQTT retidos, com Last Will para detecção de dispositivos offline.
- Os rollouts seguem a sequência sombra, canary, ondas e completo, com rollback por uma única mensagem.
- Os brokers usam TLS na porta 8883 e credenciais por dispositivo.
Para frotas de produção, um broker MQTT gerenciado cuida de certificados, escalabilidade e autenticação, para que a sua equipe possa se concentrar nos modelos. No ConnectMQ, cada dispositivo recebe credenciais com o client ID vinculado a ele, e o explorador de topics em tempo real do portal mostra as mensagens desired e reported à medida que um rollout avança. Para o contexto mais amplo de como combinar modelos e dispositivos conectados, veja nosso guia completo de IA e IoT.
Comece a usar o ConnectMQ
O ConnectMQ é um broker MQTT gerenciado com suporte a MQTT 3.1.1 e 5.0 sobre TCP, TLS e WebSockets, pronto para transportar os sinais de atualização dos seus modelos. Cadastre-se no plano gratuito Serverless, sem cartão de crédito, e configure hoje mesmo o seu primeiro rollout.
Perguntas frequentes
Posso enviar o próprio arquivo do modelo via MQTT?
Você pode dividir arquivos pequenos em pedaços, mas raramente vale a pena. Use o MQTT para o sinal de atualização e para o status, e baixe o artefato via HTTPS, onde downloads retomáveis e cache são padrão.
Como um dispositivo que estava offline recebe a atualização?
Como a mensagem de estado desejado é retida, o broker a entrega imediatamente quando o dispositivo se reconecta e faz a assinatura. O dispositivo então compara as versões e se atualiza, se necessário.
Como faço o rollback de um modelo ruim?
Publique a versão anterior como estado desejado. Um dispositivo que mantém sua última versão em disco pode voltar a ela instantaneamente, sem baixar nada.
Microcontroladores usam a mesma abordagem?
A sinalização é a mesma, mas o modelo geralmente é compilado no firmware, então o dispositivo executa uma atualização OTA de firmware em vez de trocar um arquivo de modelo.



