ITSkillsCenter
Intelligence Artificielle

Servir YOLO v11 derrière une API FastAPI : tutoriel pas-à-pas

7 min de lecture

📍 Guide principal : Détection d’objets en 2026 : pipeline YOLO v11 + Ultralytics + Roboflow. Recommandé en amont : avoir un engine TensorRT prêt.

Servir YOLO v11 derrière une API HTTP est le pattern de déploiement le plus courant côté serveur. FastAPI s’est imposé comme le framework Python par défaut pour ce type de service grâce à trois qualités : un parsing automatique des requêtes via Pydantic, une documentation OpenAPI/Swagger générée sans code, et des performances comparables à NodeJS grâce à Starlette et Uvicorn. Combiné avec Ultralytics et un engine TensorRT, l’ensemble tient en moins de 200 lignes de Python et délivre des centaines d’inférences par seconde sur un GPU dédié. Ce tutoriel va du squelette vide à une image Docker production-ready avec batching, métriques Prometheus et tests d’intégration.

Prérequis

  • Un modèle YOLO v11 exporté en .engine (ou .onnx à défaut).
  • Python 3.11 ou 3.12 installé dans un environnement virtuel propre.
  • Docker installé pour le packaging final.
  • GPU NVIDIA + CUDA + driver à jour si vous visez la production GPU (cf. tutoriel d’installation).
  • Niveau attendu : Python intermédiaire, base d’asyncio, expérience HTTP/REST.
  • Temps estimé : 2 heures pour la première version end-to-end.

Étape 1 — Initialiser le projet FastAPI

Démarrer un projet propre en isolant les dépendances dans un environnement virtuel évite les conflits avec d’autres projets Python sur la même machine. Cela conditionne aussi la reproductibilité du build Docker plus tard. Dans un terminal :

mkdir yolo-api && cd yolo-api
python3.11 -m venv .venv
source .venv/bin/activate          # Windows : .\.venv\Scripts\Activate.ps1
pip install -U pip
pip install fastapi uvicorn[standard] python-multipart pillow ultralytics prometheus-client

L’installation prend quelques minutes (Ultralytics tire PyTorch et OpenCV en transitif). Vérifier que tout est OK :

python -c "import fastapi, uvicorn, ultralytics; print('OK')"

Si la commande imprime OK sans warning, l’environnement est prêt. Créer ensuite la structure de fichiers minimale :

yolo-api/
├── app/
│   ├── __init__.py
│   ├── main.py
│   └── inference.py
├── models/
│   └── best.engine          # à copier depuis l'export TensorRT
├── tests/
│   └── test_api.py
├── requirements.txt
├── Dockerfile
└── docker-compose.yml

Étape 2 — Charger le modèle au démarrage et exposer un endpoint

Charger le modèle à chaque requête est une erreur classique : 200 ms de cold start s’ajoutent à chaque inférence et la VRAM se fragmente. La bonne approche consiste à charger le modèle une seule fois au démarrage de l’application, dans un événement startup, et à le garder dans une variable globale ou dans le state de l’application :

# app/inference.py
from ultralytics import YOLO
from pathlib import Path

class YoloService:
    def __init__(self, model_path: str):
        self.model = YOLO(model_path)
        # Warmup : la première inférence compile le graphe et alloue VRAM
        self.model.predict("https://ultralytics.com/images/bus.jpg", verbose=False)

    def predict(self, image_bytes: bytes, conf: float = 0.25):
        from io import BytesIO
        from PIL import Image
        img = Image.open(BytesIO(image_bytes)).convert("RGB")
        results = self.model.predict(img, conf=conf, verbose=False)
        boxes = []
        for box in results[0].boxes:
            boxes.append({
                "class_id": int(box.cls[0]),
                "class_name": results[0].names[int(box.cls[0])],
                "confidence": float(box.conf[0]),
                "bbox": [float(v) for v in box.xyxy[0].tolist()]
            })
        return boxes

Côté FastAPI, instancier le service au démarrage et exposer l’endpoint d’inférence :

# app/main.py
from fastapi import FastAPI, UploadFile, File, HTTPException
from contextlib import asynccontextmanager
from app.inference import YoloService

@asynccontextmanager
async def lifespan(app: FastAPI):
    app.state.yolo = YoloService("./models/best.engine")
    yield

app = FastAPI(title="YOLO v11 API", lifespan=lifespan)

@app.post("/predict")
async def predict(file: UploadFile = File(...)):
    if file.content_type not in {"image/jpeg", "image/png", "image/webp"}:
        raise HTTPException(415, "Type d'image non supporté")
    data = await file.read()
    detections = app.state.yolo.predict(data)
    return {"detections": detections, "count": len(detections)}

@app.get("/health")
async def health():
    return {"status": "ok"}

Lancer le serveur en local :

uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 1

Le warmup au boot prend 5 à 30 secondes selon le modèle. Une fois la ligne Application startup complete affichée, tester avec curl :

curl -X POST -F "file=@bus.jpg" http://localhost:8000/predict | python -m json.tool

La sortie attendue est un JSON avec les boîtes détectées, leur classe et leur confiance. Si l’inférence renvoie une erreur 500, regarder les logs Uvicorn — le plus souvent c’est un problème de chargement de modèle (chemin incorrect ou GPU non détecté).

Étape 3 — Documentation Swagger et validation Pydantic

L’un des grands avantages de FastAPI est la doc OpenAPI générée automatiquement à /docs. Pour qu’elle soit utile, il faut formaliser les schémas de réponse via Pydantic :

from pydantic import BaseModel, Field
from typing import List

class Detection(BaseModel):
    class_id: int = Field(description="Identifiant numérique de la classe")
    class_name: str
    confidence: float = Field(ge=0, le=1)
    bbox: List[float] = Field(min_length=4, max_length=4, description="x1, y1, x2, y2 en pixels")

class PredictionResponse(BaseModel):
    detections: List[Detection]
    count: int

@app.post("/predict", response_model=PredictionResponse)
async def predict(file: UploadFile = File(...)):
    ...

Avec ces annotations, le Swagger à http://localhost:8000/docs affiche un schéma JSON complet, exploitable directement par les consommateurs de l’API. Les tests d’intégration peuvent aussi se baser sur le schéma OpenAPI exporté pour rester synchrones avec la réalité du service.

Étape 4 — Métriques Prometheus pour l’observabilité

Un endpoint d’inférence sans télémétrie est un endpoint qu’on ne peut ni dépanner ni dimensionner. Exposer des métriques Prometheus prend dix lignes avec prometheus-client :

from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST
from fastapi.responses import Response
import time

REQUESTS = Counter("yolo_requests_total", "Total des requêtes /predict", ["status"])
LATENCY = Histogram("yolo_inference_latency_seconds", "Latence d'inférence")
DETECTIONS = Counter("yolo_detections_total", "Nombre total de détections par classe", ["class_name"])

@app.middleware("http")
async def metrics_middleware(request, call_next):
    if request.url.path == "/predict":
        start = time.time()
        response = await call_next(request)
        LATENCY.observe(time.time() - start)
        REQUESTS.labels(status=response.status_code).inc()
        return response
    return await call_next(request)

@app.get("/metrics")
async def metrics():
    return Response(generate_latest(), media_type=CONTENT_TYPE_LATEST)

L’endpoint /metrics au format texte Prometheus permet à un serveur Prometheus distant de scraper toutes les 15 secondes. Couplé avec un dashboard Grafana, on visualise latence p95, taux d’erreur et débit en temps réel. Pour aller plus loin sur le suivi de la santé du modèle lui-même, voir le tutoriel monitoring drift.

Étape 5 — Tests d’intégration avec httpx

Tester l’API avec httpx.AsyncClient et le TestClient de FastAPI permet de valider l’ensemble en quelques secondes en CI. Créer tests/test_api.py :

import pytest
from fastapi.testclient import TestClient
from app.main import app

@pytest.fixture(scope="module")
def client():
    with TestClient(app) as c:
        yield c

def test_health(client):
    r = client.get("/health")
    assert r.status_code == 200
    assert r.json() == {"status": "ok"}

def test_predict_invalid_type(client):
    r = client.post("/predict", files={"file": ("x.txt", b"hello", "text/plain")})
    assert r.status_code == 415

def test_predict_valid(client):
    with open("tests/fixtures/bus.jpg", "rb") as f:
        r = client.post("/predict", files={"file": ("bus.jpg", f, "image/jpeg")})
    assert r.status_code == 200
    body = r.json()
    assert body["count"] >= 1
    assert all(0 <= d["confidence"] <= 1 for d in body["detections"])

Lancer pytest -v. La suite doit passer en moins de 30 secondes. Si test_predict_valid échoue parce qu'aucune détection n'est trouvée, c'est probablement un problème de chargement de modèle dans le contexte de test — vérifier que le chemin ./models/best.engine est résolu correctement depuis le répertoire de pytest.

Étape 6 — Packaging Docker pour la production

Le packaging Docker stabilise la dépendance CUDA + driver + Python. La base recommandée pour un service GPU est l'image officielle nvidia/cuda avec le runtime Python superposé. Créer un Dockerfile multi-stage qui sépare l'installation des deps du copy du code source pour optimiser le cache :

# Dockerfile
FROM nvidia/cuda:12.4.1-runtime-ubuntu22.04 AS base

RUN apt-get update && apt-get install -y python3.11 python3.11-venv python3-pip libgl1 libglib2.0-0 \
    && rm -rf /var/lib/apt/lists/*

WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app/ ./app/
COPY models/ ./models/

EXPOSE 8000
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "1"]

Construire et lancer avec accès GPU :

docker build -t yolo-api:latest .
docker run --rm -p 8000:8000 --gpus all yolo-api:latest

L'image finale fait environ 5 Go (CUDA runtime + PyTorch + Ultralytics) ; on peut la réduire à 2-3 Go en utilisant nvidia/cuda en variante slim et en retirant les dépendances inutiles. Pour une orchestration Kubernetes, ajouter un readiness probe sur /health et configurer les ressources GPU via nvidia.com/gpu: 1 dans le spec du pod. Les workers Uvicorn restent à 1 par pod (chaque worker charge le modèle dans sa propre VRAM ; mieux vaut multiplier les pods que les workers).

Échecs récurrents au passage en production

Symptôme Cause probable Action
Latence p99 à 500 ms alors que p50 à 30 ms Cold start sur worker non warmé, ou GC Python qui kick Warmup au démarrage, fixer le nombre de workers à 1 par pod.
Erreur 500 au premier appel après déploiement Modèle non chargé pendant le warmup Vérifier le chemin dans YoloService(...), monter le volume models dans le pod.
Throughput plafonné à 30 req/s sur GPU Inférence non batchée, GIL Python qui sérialise Implémenter un batcher avec asyncio.Queue qui groupe 4-8 images par appel.
OOM GPU au bout de quelques heures Fuite mémoire dans le pré-traitement (PIL non fermée) Utiliser with Image.open(...) as img:, monitorer la VRAM via nvidia-smi.
Image Docker énorme et build lent requirements.txt non figé, copy du modèle dans une couche modifiée souvent Pip install puis COPY app/ pour conserver la couche pip en cache.
API publique sans authentification Oubli classique en passage à la prod Ajouter un middleware FastAPI qui vérifie une clé API ou un JWT, mettre derrière un reverse proxy (Traefik/Nginx).

Articles recommandés ensuite

Liens externes

Avec une API FastAPI testée et packagée en Docker, le modèle YOLO v11 est exploitable par n'importe quel client HTTP — front web, mobile, autre microservice. La couche de monitoring drift se branche ensuite directement sur les métriques exposées par cet endpoint.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité