Entraîner un modèle ne représente que la moitié du travail : pour créer de la valeur, il faut déployer des modèles ML sur des appareils edge, les tenir à jour et savoir à tout moment quelle version tourne sur chaque appareil. Ce guide présente un schéma éprouvé pour le packaging, le versionnage et le déploiement de modèles sur un parc, en utilisant MQTT pour signaler les mises à jour et collecter les états, tandis que les fichiers de modèle eux-mêmes transitent en HTTPS.
Pourquoi le déploiement de modèles en périphérie est difficile
Livrer un modèle sur un serveur, c'est un seul déploiement. Le livrer sur des centaines de passerelles, de caméras ou de microcontrôleurs, c'est un problème de systèmes distribués :
- Les appareils sont hors ligne à des moments aléatoires et doivent rattraper leur retard à la reconnexion.
- Le matériel varie, si bien qu'un même artefact de modèle peut ne pas convenir à tous les appareils.
- Un mauvais modèle peut dégrader silencieusement les prédictions plutôt que planter : il faut donc des signaux de santé au-delà de « le processus tourne ».
- La bande passante et le stockage sont limités, et certains sites sont derrière des pare-feu qui n'autorisent que les connexions sortantes.
MQTT répond bien à ces contraintes. Les appareils maintiennent une seule connexion sortante vers un broker, les retained messages transmettent immédiatement la dernière instruction aux retardataires et un message Last Will vous signale quand un appareil décroche. Si cette répartition des rôles est nouvelle pour vous, notre présentation de ce que fait un broker MQTT constitue une bonne introduction.
Étape 1 : empaqueter le modèle en artefact versionné
Traitez chaque modèle comme une release immuable, jamais comme un fichier que l'on écrase. Un paquet de modèle doit contenir :
| Fichier | Rôle |
|---|---|
model.tflite (ou .onnx, _edgetpu.tflite, .engine) |
Le modèle compilé pour un runtime précis |
labels.txt ou classes.json |
Les noms des classes de sortie dans le bon ordre |
manifest.json |
Version, runtime, forme de l'entrée, prétraitement, matériel cible, somme de contrôle |
preprocess.json (facultatif) |
Constantes de normalisation, mode de redimensionnement, ordre des canaux |
Les incohérences de prétraitement sont la cause la plus fréquente du « le modèle fonctionne sur mon portable mais pas sur l'appareil ». Consigner dans le manifeste la taille d'entrée attendue, l'ordre des couleurs et la normalisation, et faire vérifier ces éléments par l'appareil au chargement, élimine toute une catégorie d'erreurs silencieuses.
Choisissez le format selon la cible :
| Matériel cible | Format typique | Remarques |
|---|---|---|
| Microcontrôleur (ESP32-S3, Cortex-M) | TensorFlow Lite Micro, int8 | Généralement compilé dans le firmware ; mis à jour via une OTA du firmware |
| Raspberry Pi / passerelle ARM | TFLite ou ONNX Runtime | Peut être remplacé à chaud sous forme de fichier |
| Coral Edge TPU | _edgetpu.tflite |
Doit être compilé avec l'Edge TPU compiler |
| NVIDIA Jetson | Moteur TensorRT ou ONNX | Les moteurs sont liés au GPU et à la version de TensorRT : compilez par type d'appareil |
Sur les microcontrôleurs, le modèle réside souvent dans l'image du firmware : une mise à jour du modèle est donc une mise à jour du firmware. Notre tutoriel TensorFlow Lite Micro couvre ce cas ; la suite de ce guide se concentre sur les appareils de classe Linux qui chargent les modèles depuis le disque.
Étape 2 : versionner sémantiquement et signer chaque release
Utilisez une chaîne de version parlante pour les opérateurs, comme defect-classifier 2.3.0, où le numéro majeur change lorsque les entrées ou les sorties évoluent de façon incompatible. En plus de la version, calculez une somme de contrôle SHA-256 de l'artefact et publiez-la dans le manifeste. Les appareils doivent refuser tout fichier dont la somme de contrôle ne correspond pas. Pour un niveau de garantie supérieur, signez le manifeste avec une clé privée détenue par votre système de CI et ne livrez aux appareils que la clé publique.
Stockez les artefacts sur un stockage objet HTTPS ou un serveur de fichiers interne. MQTT n'est pas le bon canal pour des binaires de plusieurs méga-octets : le broker devrait conserver de gros payloads en mémoire et de nombreuses offres plafonnent la taille des messages. L'offre gratuite Serverless de ConnectMQ, par exemple, limite les messages à 256 Ko, ce qui est largement suffisant pour un manifeste mais pas pour un modèle de vision.
Étape 3 : signaler les mises à jour via MQTT
Le plan de contrôle utilise trois topics par appareil (ou par groupe d'appareils) :
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
Le service de déploiement publie un message d'état souhaité, en retained, pour qu'un appareil hors ligne le reçoive dès sa connexion :
{
"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"
}
L'appareil compare l'état souhaité avec ce qu'il exécute, télécharge, vérifie, bascule et rend compte :
{
"model": "defect-classifier",
"version": "2.3.0",
"state": "active",
"previous": "2.2.1",
"loadedAt": "2026-10-16T08:03:12Z",
"selfTest": "passed"
}
Ce schéma état souhaité / état signalé reprend l'idée des device shadows, et il fonctionne avec n'importe quel broker conforme aux standards.
Étape 4 : un agent de mise à jour minimal pour l'appareil
L'agent ci-dessous tourne sur un appareil edge sous Linux. Il s'abonne à son topic d'état souhaité, télécharge le modèle, vérifie la somme de contrôle, exécute un test de fumée, bascule le modèle actif de manière atomique et conserve la version précédente pour un éventuel retour arrière.
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 to the active version directory
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):
# Load the model and run one inference on a stored sample input.
# Replace with your runtime, e.g. 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) # atomic swap of the symlink
STATE_FILE.write_text(json.dumps({"version": version, "previous": previous}))
report(client, "active", model=desired["model"], version=version, previous=previous)
# Keep only the current and previous versions on disk
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: # report every failure instead of dying silently
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()
Notez que le téléchargement s'exécute dans le callback de message, ce qui bloque la boucle réseau. C'est acceptable pour de petits modèles et une configuration de test ; en production, confiez le travail à un thread séparé pour que les keepalives continuent pendant les longs téléchargements. Votre processus d'inférence doit ouvrir le modèle via le lien symbolique active et le recharger lorsqu'il change, par exemple en surveillant le fichier d'état.
Pour déclencher un déploiement pendant les tests, publiez un message d'état souhaité en retained :
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
Le serveur MQTT public gratuit convient pour essayer le flux, mais n'oubliez pas qu'il est lisible par tous : n'y mettez jamais de vraies URL de modèles ni de jetons signés.
Étape 5 : déployer par étapes
Ne poussez jamais un nouveau modèle sur tout le parc d'un coup. Un déploiement progressif se déroule ainsi :
- Mode fantôme. Le nouveau modèle tourne à côté de l'ancien sur quelques appareils, et les deux sorties sont publiées, mais seul l'ancien déclenche des actions. Comparez-les hors ligne.
- Canary. Publiez l'état souhaité vers un petit groupe d'appareils. Surveillez leur état signalé, leur latence d'inférence et la distribution de leurs prédictions.
- Vagues progressives. Étendez à des groupes plus larges, en marquant une pause entre chaque vague. Un script de déploiement se contente d'écrire le topic d'état souhaité pour le lot d'appareils suivant.
- Déploiement complet. Une fois la dernière vague saine, faites de la nouvelle version la version par défaut pour les appareils nouvellement provisionnés.
- Retour arrière. Si les métriques se dégradent, publiez la version précédente comme état souhaité. Comme l'appareil conserve la dernière version sur disque, le retour arrière ne nécessite même pas de téléchargement.
L'appartenance aux groupes peut être gérée dans votre service de déploiement, ou encodée dans les topics, par exemple fleet/plant1/canary/model/desired, auquel un appareil du groupe canary s'abonne en plus de son propre topic.
Savoir ce que chaque appareil exécute
Comme chaque appareil publie un message reported en retained, un unique abonnement avec wildcard à fleet/+/+/model/reported fournit un inventaire complet du parc dès la connexion. Associez-le à la télémétrie des prédictions pour surveiller la dérive : si la distribution des classes ou les scores de confiance changent brusquement après une mise à jour, le modèle voit probablement des entrées sur lesquelles il n'a pas été entraîné. Notre article sur le MLOps IoT approfondit la supervision et les boucles de réentraînement.
Checklist pour la production
- Les artefacts sont immuables, versionnés et stockés en HTTPS.
- Chaque manifeste comporte une somme de contrôle SHA-256, idéalement une signature.
- Les appareils vérifient, testent et basculent de façon atomique, en conservant une version précédente.
- Les états souhaité et signalé résident sur des topics MQTT en retained, avec un Last Will pour détecter les déconnexions.
- Les déploiements suivent les étapes fantôme, canary, vagues, complet, avec un retour arrière en un seul message.
- Les brokers utilisent TLS sur le port 8883 et des identifiants par appareil.
Pour les parcs en production, un broker MQTT managé gère les certificats, la montée en charge et l'authentification, pour que votre équipe puisse se concentrer sur les modèles. Avec ConnectMQ, chaque appareil reçoit des identifiants auxquels son client ID est lié, et l'explorateur de topics en direct du portail affiche les messages d'état souhaité et signalé au fil du déploiement. Pour le contexte plus large de l'association entre modèles et appareils connectés, consultez notre guide complet de l'IA et de l'IoT.
Démarrer avec ConnectMQ
ConnectMQ est un broker MQTT managé prenant en charge MQTT 3.1.1 et 5.0 sur TCP, TLS et WebSockets, prêt à transporter vos signaux de mise à jour de modèles. Inscrivez-vous à l'offre gratuite Serverless, sans carte bancaire, et mettez en place votre premier déploiement dès aujourd'hui.
Questions fréquentes
Puis-je envoyer le fichier du modèle lui-même via MQTT ?
Vous pouvez découper de petits fichiers, mais cela en vaut rarement la peine. Utilisez MQTT pour le signal de mise à jour et l'état, et téléchargez l'artefact en HTTPS, où la reprise des téléchargements et la mise en cache sont standard.
Comment un appareil qui était hors ligne reçoit-il la mise à jour ?
Comme le message d'état souhaité est en retained, le broker le livre immédiatement lorsque l'appareil se reconnecte et s'abonne. L'appareil compare alors les versions et se met à jour si nécessaire.
Comment revenir en arrière après un mauvais modèle ?
Publiez la version précédente comme état souhaité. Un appareil qui conserve sa dernière version sur disque peut rebasculer instantanément sans rien télécharger.
Les microcontrôleurs utilisent-ils la même approche ?
La signalisation est la même, mais le modèle est généralement compilé dans le firmware : l'appareil effectue donc une mise à jour OTA du firmware au lieu de remplacer un fichier de modèle.



