Aller au contenu
English

Charger et initialiser

  • Leçon 7
  • 40 min
  • Module 3 : Créer le service de prédiction

Ce que vous saurez faire

Faites le travail lent une seule fois, au démarrage du service, pour que chaque requête fasse seulement le travail rapide. Si le modèle est incorrect ou absent, le service refuse de démarrer.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 40 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur

  • Le projet ~/projects/escalation-service avec son .venv, actif dans un terminal. Sous Windows : $HOME\projects\escalation-service.
  • escalation/contract.py, escalation/bundle.py et bundles/escalation-1.0.0/, du module 1. escalation/settings.py, escalation/scoring.py et escalation/jobs.py, du module 2.
  • python -m pytest donne 20 passed (20 tests réussis).
  • Trois fichiers importés par main.py : escalation/errors.py, escalation/routes.py et escalation/security.py. La prochaine leçon les écrit ligne par ligne. Pour cette leçon, copiez seulement ces trois fichiers de l’instantané end-of-m03 dans votre dossier escalation. Vous pourrez ainsi démarrer le service maintenant et apprendre le démarrage avant les routes.
  • Si votre projet est absent ou endommagé, copiez l’instantané end-of-m02. Utiliser un instantané présente les étapes.

Testé avec Python 3.14.6 et Docker Desktop (Docker Engine 29.6.1) sur macOS, avec une puce Apple. Les étapes pour Windows et Linux ne sont pas testées.

Une cuisine de restaurant fait le travail lent avant l’ouverture des portes. Les cuisiniers coupent les légumes, préparent les bouillons et chauffent les fours. Quand une commande arrive, un plat prend quelques minutes, pas des heures. Aucun cuisinier ne prépare un nouveau bouillon pour chaque assiette.

Un service de prédiction a les mêmes deux types de travail. Certains travaux sont lents et identiques pour chaque requête : importer les paquets, vérifier le paquet du modèle, charger le modèle. D’autres sont rapides et propres à chaque requête : vérifier un billet de soutien, lui attribuer un score, répondre. Cette leçon place chaque type de travail au bon endroit, dans escalation/main.py, le fichier qui démarre le service.

Tomás a une idée simple. « Le modèle fait seulement 5 ko. Pourquoi ne pas appeler load_bundle() dans le point de terminaison? Comme ça, pas besoin de code de démarrage. »

Amira mesure les deux méthodes dans un même processus Python. D’abord, elle calcule le score d’un billet avec un paquet déjà en mémoire. Ensuite, elle recharge le paquet avant chaque calcul, comme Tomás le suggère. Faites une prédiction : le paquet fait 5 ko. De combien chaque requête sera-t-elle plus lente?

Un billet, médiane de 50 appels, mesurée sur l'ordinateur de cette leçon. Recharger le paquet à chaque requête prend 3,6 ms, soit deux fois le temps du calcul du score.
Description textuelle du graphique

Deux barres. « Charger une seule fois » : le paquet est déjà en mémoire, et calculer le score d’un billet prend 1,7 ms. « Charger chaque fois » : la requête appelle d’abord load_bundle() (3,6 ms), puis calcule le score du billet : 5,3 ms au total. Chaque valeur est la médiane de 50 appels. Le test a été exécuté trois fois dans un même processus Python sur macOS, avec une puce Apple.

Le chargement coûte plus de temps que le calcul du score. load_bundle() lit cinq fichiers, calcule leurs empreintes SHA-256, vérifie les versions des paquets et désérialise le pipeline. Il calcule ensuite les scores des 14 cas de parité. Tout ce travail donne le même résultat chaque fois. Dans un conteneur avec 1 CPU, chaque chargement suivant prenait aussi 3,6 ms.

Pour un modèle de 5 ko, 3,6 ms semble peu. Mais ce modèle est le plus petit que vous rencontrerez. Un modèle de texte de 500 Mo prend plusieurs secondes à lire et à vérifier. Le service de soutien attendrait plusieurs secondes par billet, pour un travail qui donne toujours le même résultat.

La règle est celle de la cuisine : faites le travail lent et constant une seule fois, au démarrage; faites seulement le travail propre au billet à chaque requête. « Une seule fois » signifie une fois par processus (worker), un processus serveur qui répond aux requêtes (plus de détails ci-dessous). Le diagramme montre les deux chemins, avec les temps mesurés dans un conteneur avec 1 CPU.

Deux chemins. Démarrage, travail lent une fois par processus : démarrer le processus avec fastapi run; importer scikit-learn, pandas et FastAPI, environ 1,0 s; lire les paramètres MODEL_BUNDLE et MODEL_SHA256; load_bundle vérifie l'empreinte globale, les empreintes et les versions, puis exécute joblib.load et vérifie les caractéristiques, 74 ms; l'autovérification calcule les scores des 14 cas de parité et sert aussi de préchauffage, dans les 74 ms; ensuite, le service ouvre son port et /ready répond 200, environ 2 s après docker run. Si une vérification échoue, le journal indique Start-up error (erreur de démarrage) et le processus se termine avec le code 3, sans servir de requêtes. Le paquet chargé reste en mémoire. Chaque requête, travail rapide : POST /v1/score avec un billet; vérifier la clé d'API et le contrat; construction du tableau, de 0,6 à 0,8 ms; préparation, de 1,2 à 1,5 ms; modèle, 0,05 ms; réponse et journalisation, environ 3 ms en tout. L'idée de Tomás, load_bundle à chaque requête, ajoute 3,6 ms chaque fois pour un modèle de 5 ko.
Le travail lent s'exécute une fois par processus, avant l'ouverture du port. Chaque requête utilise le paquet déjà en mémoire. Temps mesurés dans le conteneur avec 1 CPU, créé au module 4.
Description textuelle du diagramme

La colonne de gauche montre le chemin de démarrage. Le processus démarre (fastapi run). Il importe scikit-learn, pandas et FastAPI, ce qui prend environ 1,0 s. Il lit les paramètres MODEL_BUNDLE et MODEL_SHA256. load_bundle() vérifie l’empreinte globale, les empreintes des fichiers et les versions des paquets. Ensuite seulement, il appelle joblib.load et vérifie les caractéristiques. L’autovérification calcule les scores des 14 cas de parité. Le premier load_bundle(), avec l’autovérification, prend 74 ms. Puis le service ouvre son port, et /ready répond 200 : environ 2 s après docker run. Si une vérification échoue, le journal indique Start-up error (erreur de démarrage). Le processus se termine avec le code 3 et ne sert aucune requête.

Le paquet chargé reste en mémoire. La colonne de droite montre le chemin de chaque requête. POST /v1/score apporte un billet en JSON. Viennent ensuite les vérifications de la clé d’API et du contrat, frame (construction du tableau, de 0,6 à 0,8 ms); prepare (préparation, de 1,2 à 1,5 ms); model (modèle, 0,05 ms); la réponse et une ligne de journal. Environ 3 ms en tout. Une note indique que load_bundle() dans chaque requête ajoute 3,6 ms, pour un modèle de 5 ko.

FastAPI réserve un endroit au travail de démarrage : le lifespan (cycle de vie). Cette fonction s’exécute une fois au démarrage du serveur, avant la première requête, puis une autre fois à son arrêt. Son nom décrit ce qu’elle couvre : toute la vie de l’application.

Créez escalation/main.py. La première partie est le lifespan :

escalation/main.py
"""The escalation service: Larkfield's C05 model behind an HTTP API.
Start-up does the slow work once: read the settings, check and load the
bundle, score the parity cases. Only then does the service accept requests.
Each request does only the fast work: check the ticket, score it, answer.
"""
import logging
import time
import uuid
from contextlib import asynccontextmanager
from fastapi import FastAPI, Request
from escalation.bundle import BundleError, load_bundle
from escalation.errors import add_error_handlers, error_response
from escalation.jobs import JobRunner
from escalation.routes import router
from escalation.settings import Settings, SettingsError, load_settings
MAX_BODY_BYTES = 2_000_000 # about 5,000 tickets for a job; one ticket is about 300 bytes
logger = logging.getLogger("escalation")
def create_app(settings: Settings | None = None) -> FastAPI:
settings = settings or load_settings()
logging.basicConfig(level=settings.log_level, format="%(levelname)s %(name)s: %(message)s")
@asynccontextmanager
async def lifespan(app: FastAPI):
started = time.perf_counter()
try:
app.state.bundle = load_bundle(settings.model_bundle, settings.model_sha256)
except BundleError as error:
logger.error("Start-up error: %s", error)
raise
app.state.jobs = JobRunner(
lambda: app.state.bundle, settings.max_queued_jobs, settings.extra_ms_per_ticket
)
logger.info(
"model %s loaded (bundle %s...) in %.0f ms",
app.state.bundle.version,
app.state.bundle.digest[:12],
(time.perf_counter() - started) * 1000,
)
yield

Tout ce qui précède yield est du travail de démarrage. Le lifespan charge le paquet et le garde dans app.state.bundle, où chaque requête peut le trouver. Il crée aussi le JobRunner du module 2, qui calcule les scores des tâches avec le même paquet. Le code après yield s’exécuterait à l’arrêt du serveur; ce service n’a rien à nettoyer. Si load_bundle() lève une BundleError, le lifespan écrit une ligne de journal claire et lève de nouveau l’erreur. Le serveur s’arrête alors et n’accepte jamais de requête avec un modèle incorrect.

La deuxième partie crée l’application. Elle ajoute un identifiant de requête à chaque réponse, rejette les corps trop volumineux et ajoute les deux points de terminaison de santé :

escalation/main.py (continued)
app = FastAPI(
title="Escalation Service",
version="1.0.0",
description="Scores new support tickets for the risk of escalation within 72 hours.",
lifespan=lifespan,
docs_url="/docs" if settings.show_docs else None,
redoc_url=None,
openapi_url="/openapi.json" if settings.show_docs else None,
)
app.state.settings = settings
@app.middleware("http")
async def request_id_and_limits(request: Request, call_next):
request.state.request_id = uuid.uuid4().hex[:12]
started = time.perf_counter()
length = request.headers.get("content-length")
if length and int(length) > MAX_BODY_BYTES:
response = error_response(
request, 413, "body_too_large", f"The body is larger than {MAX_BODY_BYTES} bytes."
)
else:
response = await call_next(request)
response.headers["X-Request-ID"] = request.state.request_id
logger.info(
"%s %s %d %.1fms id=%s",
request.method,
request.url.path,
response.status_code,
(time.perf_counter() - started) * 1000,
request.state.request_id,
)
return response
add_error_handlers(app)
app.include_router(router)
@app.get("/health", tags=["health"])
def health() -> dict:
return {"status": "ok"}
@app.get("/ready", tags=["health"])
def ready(request: Request) -> dict:
bundle = getattr(request.app.state, "bundle", None)
if bundle is None:
return error_response(request, 503, "not_ready", "The model is not loaded.")
return {"status": "ready", "model_version": bundle.version}
return app
try:
app = create_app()
except (SettingsError, BundleError) as error:
raise SystemExit(f"Start-up error: {error}") from None

L’intergiciel et l’identifiant de requête fonctionnent comme dans ticket-api, dans le cours sur les API. Les quatre dernières lignes gèrent une erreur qui survient plus tôt : un paramètre incorrect. Par exemple, un MODEL_SHA256 qui ne fait pas 64 caractères. Le processus s’arrête alors immédiatement, avec le code de sortie 1.

main.py importe escalation.errors et escalation.routes, et routes.py importe escalation/security.py. Ce sont les trois fichiers copiés de l’instantané end-of-m03 (voir Avant de commencer). Sans eux, fastapi run s’arrête avec ModuleNotFoundError: No module named 'escalation.errors' (module introuvable). La prochaine leçon, Connecter l’API, les écrit ligne par ligne.

Enfin, indiquez à la commande fastapi où se trouve l’application. Créez pyproject.toml dans le dossier du projet :

pyproject.toml
[project]
name = "escalation-service"
version = "1.0.0"
description = "Scores new support tickets for the risk of escalation within 72 hours."
requires-python = ">=3.12"
dependencies = [
"fastapi[standard]==0.142.2",
"scikit-learn==1.9.1",
"pandas==3.0.6",
"numpy==2.5.3",
"joblib==1.6.0",
]
[tool.fastapi]
entrypoint = "escalation.main:app"
[tool.pytest.ini_options]
testpaths = ["tests"]

[tool.fastapi] entrypoint nomme le module et la variable : app dans escalation/main.py. Maintenant, fastapi run et fastapi dev n’ont plus besoin d’un nom de fichier.

Le service a besoin de l’empreinte globale du paquet dans MODEL_SHA256. Définissez-la à partir du fichier SHA256SUMS, puis démarrez le service :

Terminal, in ~/projects/escalation-service
export MODEL_SHA256=$(shasum -a 256 bundles/escalation-1.0.0/SHA256SUMS | cut -c1-64)
fastapi run

Dans une vraie mise en production, l’empreinte globale vient d’un endroit fiable, extérieur au paquet. C’est expliqué dans Confiance et intégrité des artefacts : provenance. Ici, vous la calculez à partir d’un paquet que vous avez créé vous-même.

La sortie (abrégée) :

⚡️ Starting FastAPI in production mode
...
🌐 Server started at http://0.0.0.0:8000
Documentation at http://0.0.0.0:8000/docs
...
INFO: Started server process [62654]
INFO: Waiting for application startup.
INFO escalation: model 1.0.0 loaded (bundle 8f233029aa4c...) in 881 ms
INFO: Application startup complete.
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

Lisez les lignes dans l’ordre. La bannière indique « Server started » (serveur démarré) avant tout chargement : cela signifie seulement que la commande a démarré. Ensuite, le lifespan s’exécute. Le service journalise la version du modèle, les 12 premiers caractères de l’empreinte globale et le temps de chargement. Uvicorn ouvre le port seulement après « Application startup complete » (démarrage de l’application terminé). Votre empreinte globale est différente, car votre metadata.json contient votre propre heure d’entraînement. Votre temps est différent aussi. Ici, il variait de 881 à 1 416 ms sur quatre exécutions, car le premier load_bundle() importe aussi scikit-learn.

Dans un deuxième terminal, appelez les deux points de terminaison de santé :

Terminal 2
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/ready

La sortie :

{"status":"ok"}
{"status":"ready","model_version":"1.0.0"}

Omar demande : « Qu’est-ce que le service de soutien reçoit s’il envoie un billet pendant le chargement du modèle? » Amira a fait l’essai. Elle a appelé /health toutes les 50 ms dès le démarrage. Les 37 premiers appels n’ont obtenu aucune connexion, et le 38e a reçu 200. Le port reste fermé jusqu’à la fin du lifespan. Aucune requête ne peut donc atteindre un service à moitié chargé.

Le temps entre « démarrer le processus » et « prêt pour la première requête » est le démarrage à froid. Ici, il vient surtout des importations de paquets (environ 1,0 s), pas du modèle (74 ms). Dans le conteneur avec 1 CPU, le délai entre docker run et /ready était d’environ 2 s (médiane de 8 démarrages). Avec un quart de CPU, il était de 6,7 s. Dans le nuage, la plateforme doit aussi télécharger l’image, et le délai était d’environ 25 s. Paramètres de ressources du conteneur le mesure.

La première requête après un démarrage est souvent plus lente que les autres. Python et scikit-learn préparent certaines choses seulement à la première exécution du code. Exécuter du vrai travail avant l’arrivée des utilisateurs est un préchauffage. Dans ce service, l’autovérification sert de préchauffage : les 14 cas de parité traversent le même pipeline avant l’ouverture du port. Dans le conteneur avec 1 CPU, le premier /v1/score après l’état prêt prenait de 4,5 à 5,7 ms. Les requêtes suivantes avaient une médiane de 3,1 à 5,5 ms. Le premier utilisateur n’attend donc presque pas plus longtemps. Le préchauffage ne demande aucun code supplémentaire, car la vérification doit s’exécuter de toute façon.

Un processus serveur répond aux requêtes. fastapi run --workers 2 en démarre deux. Chaque processus est distinct et possède sa propre mémoire. Chacun exécute donc son propre lifespan et charge sa propre copie du paquet. Essayez :

Terminal
fastapi run --workers 2

La sortie (abrégée) :

INFO: Started parent process [63495]
INFO: Started server process [63499]
INFO: Started server process [63498]
INFO escalation: model 1.0.0 loaded (bundle 8f233029aa4c...) in 799 ms
INFO escalation: model 1.0.0 loaded (bundle 8f233029aa4c...) in 799 ms

Deux lignes « loaded » (chargé) : deux copies. C’est pourquoi la règle dit « une fois par processus », pas « une fois ». Cela détermine aussi le coût en mémoire. Dans le conteneur du module 4, le service utilisait environ 150 Mio avec un processus et environ 340 Mio avec deux. Chaque processus serveur occupe environ 190 Mio, surtout pour les paquets Python, pas pour le modèle de 5 ko. Paramètres de ressources du conteneur montre ce qui arrive quand les processus dépassent la limite de mémoire. Arrêtez le service avec Ctrl+C.

/health et /ready répondent à des questions différentes, comme dans le cours sur les API et dans L’essentiel de Docker et du nuage pour les développeurs en IA. /health demande « le processus est-il vivant? ». Une plateforme redémarre un processus qui cesse d’y répondre. /ready demande « peut-il faire son travail maintenant? » et indique la version du modèle qui répond. Une plateforme n’envoie aucun trafic à une réplique qui n’est pas prête.

Dans ce service, le lifespan décide en premier. Si le paquet échoue à une vérification, le processus se termine et ne répond à aucun des deux points de terminaison. /ready vérifie quand même la présence du paquet. Il reste donc correct si une modification ultérieure charge le modèle en arrière-plan.

Tomás demande : « Dans l’API de prix des maisons du cours sur les API, le service démarrait sans modèle et répondait 503. Pourquoi quitter ici? » Les deux choix sont valides, avec des coûts différents :

  • Quitter au démarrage (ce service) : un modèle incorrect ne peut jamais répondre, et le journal indique pourquoi. Une plateforme avec des révisions garde l’ancienne révision en service jusqu’à ce que la nouvelle soit prête. C’est expliqué dans Configuration opérationnelle : trois sondes, deux points de terminaison du cours sur Docker. Le coût : avec un seul conteneur sans ancienne révision, rien ne répond avant votre correction.
  • Démarrer et répondre 503 (l’API de prix des maisons du cours sur les API) : le processus reste actif. Vous pouvez donc lire /ready et les journaux par HTTP. Le coût : un processus qui semble vivant, mais qui ne peut pas faire son travail. Une plateforme qui vérifie seulement /health le garde actif.

Pour un service de modèle, un modèle incorrect est pire qu’une absence de réponse, car un mauvais score semble valide. C’est pourquoi escalation-service refuse de démarrer.

Exercice guidé : démarrer, vérifier et arrêter le service

Section intitulée « Exercice guidé : démarrer, vérifier et arrêter le service »
  1. Écrivez escalation/main.py et pyproject.toml comme ci-dessus.
  2. Copiez escalation/errors.py, escalation/routes.py et escalation/security.py depuis l’instantané end-of-m03.
  3. Définissez MODEL_SHA256 comme dans Démarrer le service.
  4. Exécutez fastapi run.
  5. Vérifiez que le journal contient une ligne model 1.0.0 loaded (bundle …) in … ms (modèle 1.0.0 chargé en … ms).
  6. Dans un deuxième terminal, exécutez curl http://127.0.0.1:8000/ready. La commande donne {"status":"ready","model_version":"1.0.0"}.
  7. Arrêtez le service avec Ctrl+C.
  8. Démarrez-le avec fastapi run --workers 2. Comptez les lignes « loaded ».
  9. Arrêtez le service avec Ctrl+C.
Afficher le résultat attendu à l’étape 8

Deux lignes INFO escalation: model 1.0.0 loaded (bundle …) in … ms, une par processus. Chaque processus a exécuté son propre lifespan et garde sa propre copie du paquet.

À vous de jouer : modifier un fichier de l’ensemble

Section intitulée « À vous de jouer : modifier un fichier de l’ensemble »

Copiez l’ensemble dans un nouveau dossier à l’extérieur du projet, puis ajoutez une espace à la fin de son contract.json. Ensuite, démarrez le service avec ce dossier. MODEL_SHA256 reste identique.

Terminal
cp -R bundles/escalation-1.0.0 ../edited-bundle
echo " " >> ../edited-bundle/contract.json
MODEL_BUNDLE=../edited-bundle fastapi run

Sous Windows, copiez le dossier dans l’Explorateur de fichiers et ajoutez l’espace dans VS Code. Exécutez ensuite $env:MODEL_BUNDLE = "..\edited-bundle" avant fastapi run. Faites d’abord une prédiction : le service démarre-t-il? Sinon, laquelle des six vérifications de load_bundle() l’arrête, et quel est le code de sortie? Supprimez ensuite ../edited-bundle.

Afficher la réponse

Le service ne démarre pas. Le journal indique :

ERROR escalation: Start-up error: ../edited-bundle/contract.json: the file changed after the bundle was made
ERROR: Application startup failed. Exiting.

Le code de sortie est 3. La vérification 1 réussit : personne n’a modifié SHA256SUMS, donc son empreinte correspond encore à MODEL_SHA256. La vérification 2 échoue : l’empreinte de contract.json ne correspond plus à celle dans SHA256SUMS. Une seule espace ajoutée suffit, et joblib.load n’a jamais été exécuté.

Erreur courante

Le service s'arrête au démarrage avec le code de sortie 3

Symptôme : après une nouvelle mise en production, le service ne démarre pas. Le journal se termine par :

INFO: Waiting for application startup.
ERROR escalation: Start-up error: bundles/escalation-1.0.0: the bundle digest is 8f233029aa4c..., but MODEL_SHA256 expects 382077148b6c...
ERROR: Traceback (most recent call last):
...
escalation.bundle.BundleError: bundles/escalation-1.0.0: the bundle digest is 8f233029aa4c..., but MODEL_SHA256 expects 382077148b6c...
ERROR: Application startup failed. Exiting.

echo $? (PowerShell : $LASTEXITCODE) donne 3. Amira a reproduit la même erreur avec l’image de conteneur du module Choisir l’infrastructure et mesurer : docker inspect --format '{{.State.ExitCode}}' donnait aussi 3.

Cause : MODEL_SHA256 contient l’empreinte d’un autre ensemble. Quelqu’un a collé l’empreinte de escalation-1.1.0 (382077148b6c…), mais MODEL_BUNDLE désigne encore la version 1.0.0. La première vérification compare les deux empreintes, qui sont différentes. Uvicorn se termine avec le code 3 lorsque le cycle de vie échoue. Un paramètre incorrect arrête le processus plus tôt, avec le code 1.

Solution : définissez MODEL_BUNDLE et MODEL_SHA256 ensemble, à partir du même registre de mise en production. Lisez les deux empreintes abrégées dans la ligne du journal. La première correspond à l’ensemble sur disque, la seconde à celui que vous avez demandé.

Pour un vrai service d’IA : cet échec montre que la protection fait son travail. Le service refuse un modèle que personne n’a approuvé, avant d’exécuter le moindre code du fichier joblib. Si aucun autre service ne répond, cela entraîne une interruption. Planifiez donc les mises en production pour que l’ancienne version réponde jusqu’à ce que la nouvelle soit prête. Retour en arrière et retrait vous permet de pratiquer cette approche.

Vérifiez votre compréhension

Tomás veut appeler load_bundle() dans le point de terminaison de calcul du score, car le modèle fait seulement 5 ko. Qu'a montré la mesure, et pourquoi ce coût augmente-t-il pour d'autres modèles?

Afficher la réponse

Calculer le score d’un billet de soutien avec l’ensemble en mémoire prenait 1,7 ms. Recharger d’abord l’ensemble ajoutait 3,6 ms, pour une requête de 5,3 ms au total. C’est trois fois plus de travail, pour un résultat qui ne change jamais. Le chargement lit les fichiers et calcule leurs empreintes. Il vérifie les versions, désérialise le pipeline et calcule les scores des cas de parité. Ce travail augmente avec la taille du modèle : un modèle de 500 Mo prend plusieurs secondes, à chaque requête.

Vérifiez votre compréhension

Vous démarrez le service avec quatre processus. Combien de fois l'ensemble est-il chargé, et qu'est-ce que cela implique pour la mémoire?

Afficher la réponse

Quatre fois. Chaque processus est distinct et exécute son propre cycle de vie. Le journal contient donc quatre lignes « loaded » (chargé). La mémoire utilisée augmente avec le nombre de processus. Dans le conteneur du module Choisir l’infrastructure et mesurer, un processus utilisait environ 150 Mio, deux environ 340 Mio et quatre, 631 Mio. Quatre processus dépassaient la limite de 512 Mio. Choisissez le nombre de processus en tenant compte de la limite de mémoire.

Vérifiez votre compréhension

Pourquoi escalation-service se termine-t-il quand la vérification de l'ensemble échoue, plutôt que de démarrer et de répondre 503? C'est ce que faisait l'API des prix des maisons du cours Créer et déployer votre première API.

Afficher la réponse

Un mauvais modèle donne des scores qui semblent corrects, ce qui est pire qu’une absence de réponse. L’arrêt rend l’échec évident : une ligne dans le journal et le code de sortie 3. Une plateforme qui gère des révisions laisse alors l’ancienne révision répondre. Répondre 503 maintient le processus actif, ce qui vous aide à consulter le problème par HTTP. Mais le processus semble fonctionner alors qu’il ne peut pas faire son travail. Une plateforme qui vérifie seulement /health le conserve.

Récapitulatif

  • Le travail au démarrage est lent et toujours identique : importations, paramètres, load_bundle(), autovérification. Il s’exécute une fois par processus, dans le cycle de vie de FastAPI, avant l’ouverture du port.
  • Le travail par requête est rapide et différent chaque fois : vérifier un billet, calculer son score avec l’ensemble en mémoire, répondre. Environ 3 ms ici.
  • Charger l’ensemble à chaque requête ajoutait 3,6 ms par requête pour un modèle de 5 ko; pour un grand modèle, plusieurs secondes.
  • Le démarrage à froid est le temps nécessaire avant de pouvoir traiter la première requête. L’autovérification sert aussi de préchauffage.
  • Chaque processus conserve sa propre copie de l’ensemble, donc la mémoire utilisée augmente avec le nombre de processus.
  • Un ensemble incorrect arrête le service avec une Start-up error (erreur de démarrage) et le code de sortie 3 : rien n’est servi.

Ensuite, vous connecterez les routes : le point de terminaison de calcul du score, les informations du modèle, les tâches et la clé d’API.

Leçon suivante : Connecter l'API