Business Digital

Servir un modèle ML avec BentoML : packaging et déploiement

18 min de lecture

Vue d’ensemble : MLOps moderne : du modèle entraîné à la production scalable

Le problème : un pickle ne se déploie pas seul

Quand un data scientist remet un fichier model.pkl à l’équipe d’ingénierie, le travail réel commence. Le fichier sérialisé contient les poids du modèle mais aucune des briques nécessaires pour le mettre en production : il n’y a pas d’endpoint HTTP, pas de validation d’entrée, pas de gestion de la concurrence, et surtout pas de garantie que la version de scikit-learn utilisée pour l’entraînement sera la même qu’au moment de la prédiction. Un écart mineur de version peut faire basculer la sortie d’un classifieur de 0.94 à 0.61 sur le même vecteur d’entrée.

Le besoin réel se résume à quatre points : exposer une API JSON propre, geler les dépendances Python à l’octet près, produire une image Docker reproductible et déployer cette image quelque part. Beaucoup d’équipes tentent d’assembler ces briques à la main avec FastAPI, un requirements.txt, un Dockerfile écrit à la main et une CI maison. Ça marche pendant six mois, puis quelqu’un oublie d’épingler une version mineure et la prédiction part en vrille un vendredi soir.

BentoML adresse ce problème en imposant un format unifié, le Bento, qui regroupe le code du service, les modèles, les dépendances et la configuration runtime dans un artefact unique versionné. C’est l’équivalent pour un modèle ML de ce qu’un fichier .jar est pour une application Java : une unité de déploiement déterministe.

Pourquoi BentoML plutôt que FastAPI nu

FastAPI est un excellent framework HTTP et reste pertinent pour les architectures de microservices génériques. Le tutoriel FastAPI pour servir un modèle ML couvre cette approche en détail. Mais dès qu’on parle spécifiquement de servir des modèles, FastAPI laisse plusieurs problèmes ouverts que BentoML résout sans effort.

Premier point : la gestion des modèles. Avec FastAPI il faut écrire soi-même la logique de chargement, de versioning et de cache des poids. BentoML propose un Model Store local qui versionne automatiquement chaque sauvegarde et permet de référencer un modèle par tag (par exemple iris_clf:latest). Deuxième point : la containerisation. Avec FastAPI il faut écrire un Dockerfile, choisir une image de base, gérer les dépendances système ; BentoML génère tout cela à partir d’un fichier YAML déclaratif et garantit la cohérence entre l’environnement de build et l’image finale.

Troisième point : l’optimisation runtime. BentoML inclut nativement la gestion des workers, le batching adaptatif des requêtes, l’accélération GPU et le téléchargement de modèles au build plutôt qu’au démarrage du conteneur. Ce dernier point réduit drastiquement les temps de démarrage à froid, ce qui compte pour les déploiements serverless ou auto-scalés. Le choix entre les deux outils dépend donc du contexte : FastAPI pour une API métier classique, BentoML quand le cœur du service est un modèle ML.

Prérequis techniques

Avant de commencer, il faut un environnement Python 3.9 ou supérieur (3.11 recommandé pour ce tutoriel), pip à jour, et Docker installé localement pour la phase de containerisation. Une connaissance basique de scikit-learn aide à comprendre la partie entraînement, mais le code reste copiable tel quel. Une connexion internet est nécessaire pour télécharger BentoML et ses dépendances.

Côté ressources, un poste avec 8 Go de RAM suffit largement pour ce tutoriel. Le modèle utilisé est minuscule (quelques kilo-octets) et l’image Docker finale fait moins de 800 Mo. Si vous voulez tester la partie BentoCloud à la fin, créez un compte gratuit sur la plateforme : un crédit de découverte est offert à l’inscription, suffisant pour quelques heures d’inférence sur instance CPU de test.

Étape 1 — Installer BentoML et initialiser un projet

Créez un dossier de travail vide et un environnement virtuel isolé. L’isolation évite les conflits avec d’autres projets Python et garantit que les versions installées seront exactement celles déclarées dans le bentofile.yaml à l’étape suivante.

mkdir iris-bentoml && cd iris-bentoml
python -m venv .venv
source .venv/bin/activate  # Linux/macOS
# .venv\Scripts\activate    # Windows PowerShell

pip install --upgrade pip
pip install "bentoml>=1.4,<2.0" scikit-learn numpy

Vérifiez que BentoML est correctement installé en affichant sa version. La sortie doit indiquer une 1.4.x (la dernière stable au printemps 2026 est la 1.4.38). Si vous obtenez une version 1.1 ou 1.2, supprimez-la car la syntaxe des décorateurs présentée plus bas ne fonctionnera pas.

bentoml --version
# Output attendu : bentoml, version 1.4.38

Créez ensuite la structure de fichiers minimale du projet. Le fichier train.py entraîne le modèle, service.py définit l’API, et bentofile.yaml décrit comment builder le Bento.

touch train.py service.py bentofile.yaml

Étape 2 — Entraîner et sauvegarder un modèle dans le Model Store

Le Model Store est un répertoire local (par défaut ~/bentoml/models/) qui versionne chaque sauvegarde de modèle. Chaque appel à bentoml.sklearn.save_model crée un nouveau tag avec un hash unique, ce qui permet de garder un historique complet sans écraser les anciennes versions.

Ouvrez train.py et collez le code suivant. Il entraîne un classifieur logistique sur le jeu Iris, atteint une précision proche de 97 % sur le set de test, et sauvegarde le modèle dans le Model Store.

# train.py
import bentoml
from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split

# Chargement et split du jeu de données
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# Entraînement
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
print(f"Précision test : {model.score(X_test, y_test):.3f}")

# Sauvegarde dans le Model Store BentoML
saved = bentoml.sklearn.save_model("iris_clf", model)
print(f"Modèle sauvegardé : {saved.tag}")

Exécutez le script. Le tag affiché contient le nom logique suivi d’un identifiant unique, par exemple iris_clf:abc123xyz. Notez ce tag, il sera référencé dans le service.

python train.py
# Précision test : 0.967
# Modèle sauvegardé : iris_clf:abc123xyz

Pour vérifier que le modèle est bien enregistré, listez les entrées du Model Store. Vous devez voir une ligne iris_clf avec le tag correspondant et la taille du modèle.

bentoml models list
# Tag                    Module           Size    Creation Time
# iris_clf:abc123xyz     bentoml.sklearn  3.84KB  ...

Étape 3 — Définir un service avec @bentoml.service et @bentoml.api

L’API de BentoML 1.4 a changé par rapport aux versions 1.0 et 1.1, qui reposaient sur des objets bentoml.io.NumpyNdarray passés en arguments à un décorateur impératif. À partir de 1.2 et jusqu’à 1.4 actuellement, le pattern recommandé est déclaratif : on définit une classe Python décorée par @bentoml.service, et chaque méthode exposée comme endpoint HTTP est décorée par @bentoml.api. Les types Python standard (numpy, listes, dictionnaires, pydantic) suffisent à décrire les entrées et sorties.

Ouvrez service.py et écrivez le service complet. La classe IrisClassifier charge le modèle au démarrage via bentoml.models.BentoModel, puis expose une méthode classify qui accepte un tableau numpy et renvoie les prédictions.

# service.py
import bentoml
import numpy as np
from bentoml.models import BentoModel

# Référence au modèle dans le Model Store
IRIS_MODEL = BentoModel("iris_clf:latest")

@bentoml.service(
    resources={"cpu": "1"},
    traffic={"timeout": 10},
)
class IrisClassifier:
    # Le modèle est chargé une seule fois à l'init
    model_ref = IRIS_MODEL

    def __init__(self) -> None:
        self.model = bentoml.sklearn.load_model(self.model_ref)

    @bentoml.api
    def classify(self, input_data: np.ndarray) -> np.ndarray:
        return self.model.predict(input_data)

Trois points méritent une explication. D’abord, resources={"cpu": "1"} indique à BentoML de réserver un CPU par worker ; cette information sera utilisée à la fois en local pour dimensionner le pool de workers et en déploiement cloud pour le scheduling. Ensuite, traffic={"timeout": 10} coupe toute requête qui dépasse dix secondes, ce qui évite qu’une prédiction bloquée n’épuise les workers. Enfin, le constructeur __init__ charge le modèle une seule fois au démarrage du worker ; les appels suivants à classify réutilisent l’objet en mémoire.

Le typage input_data: np.ndarray -> np.ndarray est exploité par BentoML pour générer automatiquement la documentation OpenAPI et valider les requêtes entrantes. Si un client envoie un payload mal formé, BentoML répond avec un 422 explicite avant même d’appeler la méthode.

Étape 4 — Tester le service en local

Avant de construire le Bento, vérifiez que le service fonctionne en mode développement. La commande bentoml serve démarre un serveur Uvicorn avec auto-reload activé, ce qui permet d’itérer rapidement sur le code.

bentoml serve service:IrisClassifier --reload
# [INFO] Starting development HTTP BentoServer
# [INFO] Listening on http://localhost:3000

Le serveur écoute sur le port 3000. Ouvrez un second terminal et envoyez une requête de test avec curl. Le payload est un tableau JSON à deux dimensions : chaque ligne correspond à une fleur et chaque colonne à une mesure (sépale longueur, sépale largeur, pétale longueur, pétale largeur).

curl -X POST http://localhost:3000/classify \
  -H "Content-Type: application/json" \
  -d '{"input_data": [[5.1, 3.5, 1.4, 0.2], [6.7, 3.0, 5.2, 2.3]]}'
# Output : [0, 2]

La sortie [0, 2] signifie que la première fleur est classée comme Setosa (classe 0) et la seconde comme Virginica (classe 2). Si vous obtenez ce résultat, le service fonctionne. Vous pouvez aussi ouvrir http://localhost:3000 dans un navigateur pour voir l’interface Swagger générée automatiquement.

Étape 5 — Définir le bentofile.yaml

Le fichier bentofile.yaml est le manifeste de build du Bento. Il déclare quel service inclure, quelles versions Python utiliser, quels paquets installer et quels fichiers du projet copier dans l’artefact final. C’est ce fichier qui garantit la reproductibilité : tant qu’il est versionné dans git, n’importe qui peut rebuilder un Bento bit-à-bit identique.

# bentofile.yaml
service: "service:IrisClassifier"
labels:
  owner: data-team
  project: iris-demo
include:
  - "service.py"
python:
  requirements_txt: "./requirements.txt"
  lock_packages: true
docker:
  python_version: "3.11"
  distro: "debian"

Créez le fichier requirements.txt à côté du bentofile.yaml avec les dépendances exactes. L’option lock_packages: true demande à BentoML de geler les versions transitives lors du build, ce qui évite les surprises lors d’un rebuild ultérieur.

# requirements.txt
bentoml>=1.4,<2.0
scikit-learn==1.5.2
numpy==1.26.4

Le champ docker.python_version contrôle la version de Python dans l’image finale. Choisir explicitement 3.11 évite les écarts entre la machine de développement et le serveur de production. Le champ docker.distro peut prendre debian (par défaut, image complète) ou alpine (image minimaliste, mais incompatible avec certaines roues binaires de scientifique).

Étape 6 — Builder le Bento

Avec service.py, bentofile.yaml et requirements.txt en place, le build est une commande unique. BentoML collecte le modèle référencé, copie le code, génère un Dockerfile, calcule un hash et empaquette le tout dans le répertoire ~/bentoml/bentos/.

bentoml build
# Locking PyPI package versions.
# ...
# Successfully built Bento(tag="iris_classifier:xyz789abc")

Le tag du Bento contient le nom du service en kebab-case et un identifiant unique. Vous pouvez lister tous les Bentos disponibles pour vérifier qu’il est bien présent et inspecter son contenu.

bentoml list
# Tag                              Size      Creation Time
# iris_classifier:xyz789abc        15.42 MB  ...

bentoml get iris_classifier:latest

La sortie de bentoml get affiche la structure interne du Bento : chemin du modèle, hash des dépendances, version de Python, métadonnées. Cet artefact est immuable : toute modification du code ou des dépendances impose un nouveau build avec un nouveau tag, ce qui garantit la traçabilité.

Étape 7 — Containeriser en image Docker

La commande bentoml containerize appelle Docker en arrière-plan, copie le Bento dans l’image, installe les dépendances exactes du requirements.txt verrouillé et expose le port 3000. Le tag de l’image Docker reprend par défaut le tag du Bento.

bentoml containerize iris_classifier:latest
# Building OCI-compliant image for iris_classifier:xyz789abc with docker
# Successfully built docker image "iris_classifier:xyz789abc"

Vérifiez que l’image est bien présente dans le registre Docker local et notez sa taille. Une image typique pour un modèle scikit-learn pèse entre 700 Mo et 1 Go selon les dépendances scientifiques embarquées.

docker images iris_classifier
# REPOSITORY        TAG          IMAGE ID       CREATED         SIZE
# iris_classifier   xyz789abc    8a3f9c1e2b4d   1 minute ago    782MB

Sur un Mac à puce Apple Silicon, ajoutez l’option --platform=linux/amd64 si l’image cible un cluster x86. Sans cela, certains paquets scientifiques compilés (numpy, scipy) peuvent échouer au démarrage à cause d’instructions CPU non supportées.

Étape 8 — Lancer le container et tester

Lancez le conteneur en mappant le port 3000 sur l’hôte. Le drapeau --rm supprime automatiquement le conteneur à l’arrêt, ce qui est pratique pour les tests.

docker run --rm -p 3000:3000 iris_classifier:latest serve
# [INFO] Starting production HTTP BentoServer
# [INFO] Listening on http://0.0.0.0:3000

Dans un second terminal, refaites le test curl de l’étape 4. La réponse doit être identique à celle du serveur de développement, ce qui confirme que la containerisation n’a rien cassé.

curl -X POST http://localhost:3000/classify \
  -H "Content-Type: application/json" \
  -d '{"input_data": [[5.1, 3.5, 1.4, 0.2]]}'
# [0]

Si le test passe, vous avez un artefact Docker prêt pour la production : reproductible, versionné, autonome. Le même tag d’image peut être déployé sur n’importe quel orchestrateur compatible OCI (Kubernetes, ECS, Cloud Run, Nomad).

Étape 9 — Déployer sur BentoCloud ou pousser vers un registry privé

Deux chemins se présentent à ce stade. Le premier est BentoCloud, la plateforme managée de l’éditeur. La création de compte donne droit à un crédit de découverte qui couvre quelques heures d’inférence CPU pour tester. Le déploiement se fait en une commande après authentification.

bentoml cloud login
# Suit le lien affiché, valide dans le navigateur

bentoml deploy iris_classifier:latest -n iris-prod
# Pushing Bento to BentoCloud...
# Deployment 'iris-prod' created.
# URL : https://iris-prod-xxx.bentoml.ai

Le second chemin est un registry Docker privé : Amazon ECR, Google Artifact Registry, Azure Container Registry ou un Harbor auto-hébergé. La méthode est identique à n’importe quelle image OCI : on tague, on s’authentifie au registry, on pousse.

docker tag iris_classifier:xyz789abc \
  registry.example.com/ml/iris_classifier:xyz789abc

docker login registry.example.com
docker push registry.example.com/ml/iris_classifier:xyz789abc

À partir de là, n’importe quel manifeste Kubernetes ou fichier Compose peut tirer l’image et la lancer. Le port à exposer est 3000, et la commande à passer au conteneur est serve (déjà déclarée comme CMD dans le Dockerfile généré).

Étape 10 — Observer les logs et monitorer les requêtes

En production, l’observation passe par trois canaux. Les logs stdout/stderr du conteneur contiennent les traces de démarrage, les requêtes reçues et les exceptions. Avec Docker, on les consulte avec docker logs <container_id> ; avec Kubernetes, kubectl logs <pod>.

docker logs -f $(docker ps -q --filter ancestor=iris_classifier:latest)
# [INFO] [api_server:1] 192.168.1.10 - POST /classify 200 OK 12ms

Le second canal est l’endpoint /metrics, exposé automatiquement par BentoML au format Prometheus. Il publie le nombre de requêtes, la latence par percentile, le nombre de workers actifs et les erreurs.

curl http://localhost:3000/metrics | head -20
# bentoml_service_request_total{...} 1284
# bentoml_service_request_duration_seconds_bucket{le="0.05"} 1198

Le troisième canal est l’endpoint /healthz, utilisé par Kubernetes pour les probes de liveness et de readiness. Une réponse 200 indique que le service est prêt à recevoir du trafic. En cas d’échec du chargement du modèle, l’endpoint répond 503 et Kubernetes arrête de router des requêtes vers le pod.

Pour pousser ces métriques vers une stack Prometheus + Grafana, déclarez un ServiceMonitor dans Kubernetes pointant sur /metrics. Pour la détection de dérive du modèle (data drift) au-delà des métriques système, voir le guide détection de dérive avec Evidently.

Erreurs fréquentes et résolutions

Symptôme Cause Résolution
ModuleNotFoundError: No module named 'bentoml.io' Code écrit pour l’API 1.0/1.1, incompatible avec 1.4 Migrer vers le pattern @bentoml.service + @bentoml.api avec type hints
RuntimeError: Model 'iris_clf:latest' not found Le Model Store n’a aucune entrée pour ce nom Lancer python train.py puis vérifier avec bentoml models list
docker: Cannot connect to the Docker daemon Docker Desktop ou daemon arrêté Démarrer Docker, vérifier avec docker ps
Image Docker trop volumineuse (> 2 Go) Dépendances scientifiques non épinglées, build inclut le cache pip Activer lock_packages: true et épingler torch/scipy à des versions précises
Timeout sur grosse requête batch traffic.timeout trop bas Augmenter dans @bentoml.service(traffic={"timeout": 60}) et rebuilder
Conteneur ne démarre pas sur ARM/Apple Silicon Image buildée pour amd64, hôte arm64 sans Rosetta Rebuilder avec --platform linux/arm64 ou activer l’émulation

Ressources officielles

La documentation BentoML est exhaustive et activement maintenue. Trois pages méritent une lecture complète quand vous passerez à un cas plus complexe que le tutoriel Iris : la page Services qui détaille les options avancées des décorateurs, la page Bento build options qui couvre toutes les clés du bentofile.yaml, et la page Scale with BentoCloud pour le déploiement managé.

Le dépôt GitHub bentoml/BentoML contient des dizaines d’exemples prêts à l’emploi : XGBoost, PyTorch, transformers Hugging Face, vLLM pour les LLM. Le changelog des versions 1.4.x liste précisément les évolutions d’API à surveiller entre deux mises à jour mineures.

Tutoriels associés

Ce tutoriel s’inscrit dans la série MLOps moderne pour la production, qui couvre l’ensemble du cycle de vie d’un modèle. Trois lectures complémentaires forment un parcours cohérent.

Pour suivre les expériences d’entraînement en amont du packaging, le guide tracking d’expériences avec MLflow montre comment versionner les hyperparamètres et les métriques avant même de produire un Bento. Pour orchestrer plusieurs étapes (préparation, entraînement, évaluation, déploiement) en pipeline reproductible, le guide Kubeflow Pipelines explique comment chaîner les étapes sur Kubernetes. Pour surveiller la qualité du modèle après mise en production, le guide détection de dérive avec Evidently détaille la mise en place des alertes sur les distributions d’entrée et de sortie.

FAQ

Quelle version minimale de Python est requise par BentoML 1.4 ?
Python 3.9 minimum. Les versions 3.10, 3.11 et 3.12 sont supportées et 3.11 reste le compromis le plus stable pour les dépendances scientifiques classiques (numpy, scipy, scikit-learn).

Peut-on servir un modèle PyTorch ou Hugging Face avec le même pattern ?
Oui, le pattern @bentoml.service + @bentoml.api est identique. Seul l’appel de sauvegarde change : bentoml.pytorch.save_model pour PyTorch, bentoml.transformers.save_model pour les modèles Hugging Face, bentoml.xgboost.save_model pour XGBoost.

Le Model Store BentoML peut-il être partagé entre plusieurs développeurs ?
Localement non, le Model Store est un dossier sur le disque utilisateur. Pour partager, on pousse les modèles soit vers BentoCloud (commande bentoml models push), soit dans un Bento qui sera versionné dans un registry Docker.

Faut-il toujours containeriser, ou peut-on déployer directement un Bento ?
BentoCloud accepte directement les Bentos sans étape containerize intermédiaire (la plateforme construit l’image côté serveur). Pour tout déploiement hors BentoCloud (Kubernetes maison, Cloud Run, ECS), il faut passer par bentoml containerize puis pousser l’image dans un registry.

Comment gérer plusieurs versions d’un modèle en parallèle ?
Chaque appel à bentoml.sklearn.save_model crée un nouveau tag. Référencez un tag précis dans le service (iris_clf:abc123xyz) au lieu de :latest pour figer la version. Pour faire de l’A/B test, deux Bentos avec deux modèles différents derrière un même reverse proxy fonctionnent.

BentoML supporte-t-il le batching automatique des requêtes ?
Oui, on l’active sur une méthode via @bentoml.api(batchable=True, batch_dim=0). BentoML accumule alors les requêtes concurrentes pendant une fenêtre courte et les exécute en une seule passe sur le modèle, ce qui améliore le débit sur GPU.

Que se passe-t-il si je change le modèle sans rebuilder le Bento ?
Si le service référence iris_clf:latest, un nouveau save_model pointera latest sur la nouvelle version mais le Bento déjà buildé reste figé sur la version embarquée au moment du build. C’est voulu : le Bento est immuable. Pour propager le nouveau modèle, rebuildez et redéployez.

Tutoriels associés (complément)

Service ITSkillsCenter

Site ou application web sur mesure

Conception Pro + Nom de domaine 1 an + Hébergement 1 an + Formation + Support 6 mois. Accès et code livrés. À partir de 350 000 FCFA.

Demander un devis
Publicité