Aller au contenu
English

Inférence asynchrone

  • Leçon 6
  • 45 min
  • Module 2 : Choisir un mode de service

Ce que vous saurez faire

Calculez les scores d’un travail trop volumineux pour une seule requête, comme un mois de billets de soutien avec un modèle lent. L’appelant reçoit une réponse aussitôt et récupère les scores plus tard. Le service reste rapide pour les agents.

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

Ce que vous devez savoir

  • La tâche par lots et ses sorties, présentées dans Inférence par lots.
  • Le budget de latence (p95 sous 50 ms pour un score), la limite de 30 s de la passerelle et le coût fixe d’un appel, présentés dans Inférence en ligne.

Ce qu’il vous faut sur votre ordinateur

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 n’ont pas été testées.

Vous apportez un manteau chez le nettoyeur. Personne ne le nettoie pendant que vous attendez au comptoir. Vous recevez un reçu papier avec un numéro et vous revenez demain. Vous montrez le numéro et vous récupérez votre manteau. Si le portant est plein, le commerce vous le dit aussitôt : « Pas aujourd’hui, revenez demain. » Vous n’attendez pas au comptoir, et le commerce nettoie les manteaux à son rythme.

Le problème : un travail plus long que l’attente permise

Section intitulée « Le problème : un travail plus long que l’attente permise »

L’équipe qualité de Larkfield vérifie un mois de billets à la fois. Son outil envoie le mois à escalation-service et attend les scores en retour. Pour juillet, cela représente 1 400 billets.

Avec le modèle actuel, ce n’est pas un problème. Un appel pour 1 400 billets a pris 7 ms. Mais Priya teste un modèle de texte qui lit les mots de chaque billet, et ces modèles sont beaucoup plus lents. Supposons qu’il lui faut 40 ms par billet. Le mois prend alors 1 400 × 40 ms = 56 s. La passerelle de Larkfield arrête chaque requête après 30 s. La requête ne peut donc jamais se terminer. L’équipe qualité reçoit une erreur, et le service a travaillé 30 s pour rien.

Où se trouve la limite? Faites une prédiction, puis essayez. Le premier temps par billet est mesuré; les temps plus longs sont simulés.

Explorer

1 400 billets dans une requête face à la limite de 30 s de la passerelle

Durée par billet
1,76 ms (mesuré)
Billets dans la requête
1 400 billets
Une requête prend
2,46 s
Résultat
Une requête, une longue attente

1 400 billets × 1,76 ms par billet (mesuré) = 2,46 s. Une requête finit encore avant la limite de 30 s de la passerelle, mais l’appelant attend avec la connexion ouverte. (Un seul appel pour tous est beaucoup plus rapide : 1 400 billets en un appel ont pris 7 ms.)

  • Tient dans les 50 ms du centre d’assistance
  • Plus de 50 ms, moins de 30 s
  • Plus de 30 s : utilisez une tâche
  • Pâle, pointillé : durée simulée
Une requête : 2,46 s2,46 sle centre d’assistance attend 50 msla passerelle arrête à 30 s1 ms10 ms100 ms1 s10 s1 min10 min1 hDurée d’une requête (chaque pas : ×10)
Une requête : 2,46 s2,46 sassistance : 50 mspasserelle : 30 s1 ms100 ms10 s1 hDurée d’une requête (chaque pas : ×10)
Une requête avec les 1 400 billets de juillet. Faites passer le temps par billet de 1,76 ms, mesuré, à celui de modèles plus lents, simulés. Trouvez quand la requête dépasse 30 s.
Description textuelle de l’explorateur

L’explorateur montre une requête de 1 400 billets sur un axe temporel allant de 1 ms à 1 heure. Chaque graduation de l’axe vaut 10 fois la précédente. Des lignes pointillées indiquent les 50 ms du centre de soutien et les 30 s de la passerelle. Une commande règle le temps par billet : 1,76 ms (mesuré), puis 5, 10, 20, 25, 40, 100 et 400 ms (simulés). Les temps simulés ont un remplissage pâle et un contour pointillé.

L’explorateur suppose que chaque billet prend tout le temps indiqué, comme le ferait un modèle lent. À 1,76 ms, le temps mesuré, cela donne 2,46 s. C’est le temps mesuré pour 1 400 billets traités un à un; un appel pour les 1 400 a pris seulement 7 ms. À 5 ms, le travail prend 7 s, et à 10 ms, 14 s. À 20 ms, il prend 28 s : encore sous 30 s, mais près de la limite. À 25 ms, il prend 35 s; à 40 ms, 56 s; à 100 ms, 2,3 minutes; à 400 ms, 9,3 minutes. Dès 25 ms, la passerelle arrête la requête, et l’explorateur indique d’utiliser une tâche. La limite exacte est de 30 000 ms ÷ 1 400 ≈ 21,4 ms par billet.

La limite est d’environ 21 ms par billet : 30 000 ms ÷ 1 400. Et 20 ms est déjà risqué. Les 28 s ne laissent aucune marge pour une période chargée ou un mois légèrement plus volumineux.

L’explorateur multiplie le nombre de billets par le temps par billet. Pour le modèle actuel, c’est trop pessimiste. La majeure partie des 1,76 ms correspond au coût fixe d’un appel. Un appel pour les 1 400 billets a pris 7 ms. Un modèle de texte lent consacre son temps à chaque billet. La multiplication est donc juste pour les temps simulés.

Tomás demande : « Alors, faites attendre la passerelle 10 minutes. » Cela déplace le problème. L’outil qualité garde une connexion ouverte pendant 10 minutes. Une interruption réseau à la neuvième minute fait perdre tout le travail. Pendant ce temps, le service consacre son CPU à une énorme requête. Les agents attendent leurs scores prévus en 50 ms.

L’inférence asynchrone sépare la requête du travail. L’appelant envoie le travail et reçoit aussitôt un reçu. Le service fait le travail plus tard, en arrière-plan. L’appelant revient chercher le résultat. Asynchrone signifie « pas en même temps » : la réponse n’arrive pas dans le même échange que la question.

Le travail que l’appelant transmet est une tâche. Les tâches qui attendent leur tour forment une file d’attente, où la première entrée est la première sortie. Trois éléments HTTP jouent le rôle du reçu du nettoyeur :

  • 202 Accepted : « J’ai votre travail, mais il n’est pas terminé. » Le corps contient l’identifiant de tâche, le numéro sur le reçu papier.
  • Location: /v1/jobs/<id> : où demander des nouvelles de cette tâche.
  • Retry-After: 2 : combien de secondes attendre avant de demander à nouveau. Demander à répétition jusqu’à la fin de la tâche s’appelle l’interrogation périodique.
Un diagramme de séquence avec les trois mêmes acteurs : le système du centre de soutien, escalation-service avec des tâches et l'équipe senior. Le système du centre de soutien envoie POST /v1/jobs avec 1 400 billets. Le service répond 202 Accepted (accepté) avec un identifiant de tâche, l'état queued (en attente), un en-tête Location et Retry-After : 2. La tâche attend dans la file, puis le processus de traitement calcule ses scores. Dans une boucle, toutes les Retry-After secondes, le système du centre de soutien envoie GET /v1/jobs/{id}. Le service répond queued ou running (en cours). Puis un GET reçoit done (terminé) avec 1 400 scores, et le système transmet la liste à examiner à l'équipe senior. Une note : si la file est pleine, la réponse est 429; réessayez plus tard.
Asynchrone : la réponse à la requête est un reçu, pas les scores. L'appelant interroge le service jusqu'à la fin de la tâche. Une file pleine refuse aussitôt, avec 429.
Description textuelle du diagramme

Les trois mêmes acteurs : le système du centre de soutien, escalation-service (tâches) et l’équipe senior. Le système du centre de soutien envoie POST /v1/jobs avec 1 400 billets. Le service répond aussitôt : 202 Accepted (accepté), avec un identifiant de tâche, l’état queued (en attente), un en-tête Location (emplacement) et Retry-After: 2 (réessayez après 2 secondes). La tâche attend dans la file, puis le processus de traitement calcule ses scores. Dans une boucle, toutes les Retry-After secondes, le système du centre de soutien envoie GET /v1/jobs/{id}. Le service répond queued ou running (en cours). Puis un GET reçoit done (terminé), avec les 1 400 scores. Le système du centre de soutien transmet la liste à examiner à l’équipe senior. Une note au bas : si la file est pleine, le service répond 429; réessayez plus tard.

Créez escalation/jobs.py. Il contient les tâches, la file d’attente et un fil d’exécution qui calcule les scores des tâches l’une après l’autre :

escalation/jobs.py
"""Asynchronous scoring: accept a large job now, score it later, keep the result.
POST /v1/jobs 202 Accepted, with the job's ID and status URL
GET /v1/jobs/{job_id} queued → running → done (or failed), and the scores
One worker thread takes jobs from a queue, one at a time. The queue has a
limit: when it is full, a new job gets 429, so a burst cannot use all memory.
The jobs live in this process's memory. That is enough to learn the pattern,
but a restart loses every job, and a second worker process cannot see them.
A real service keeps jobs in a queue service and results in storage (see the course *Scaling APIs and AI Workloads*).
"""
import logging
import queue
import threading
import time
import uuid
from dataclasses import dataclass, field
from datetime import datetime, timezone
from escalation.contract import TicketIn
from escalation.scoring import score_tickets
logger = logging.getLogger("escalation")
class QueueFull(Exception):
pass
@dataclass
class Job:
id: str
tickets: list[TicketIn]
status: str = "queued"
created_at: str = field(default_factory=lambda: datetime.now(timezone.utc).isoformat(timespec="seconds"))
finished_at: str | None = None
seconds: float | None = None
model_version: str | None = None
results: list[dict] | None = None
error: str | None = None
class JobRunner:
def __init__(self, get_bundle, max_queued: int, extra_ms_per_ticket: float = 0.0):
self.get_bundle = get_bundle
self.extra_ms = extra_ms_per_ticket
self.queue: queue.Queue[Job] = queue.Queue(maxsize=max_queued)
self.jobs: dict[str, Job] = {}
self.thread = threading.Thread(target=self.work, daemon=True, name="job-worker")
self.thread.start()
def submit(self, tickets: list[TicketIn]) -> Job:
job = Job(id=uuid.uuid4().hex[:12], tickets=tickets)
try:
self.queue.put_nowait(job)
except queue.Full:
raise QueueFull from None
self.jobs[job.id] = job
return job
def work(self) -> None:
while True:
job = self.queue.get()
job.status = "running"
started = time.perf_counter()
try:
bundle = self.get_bundle()
results, _ = score_tickets(bundle, job.tickets)
if self.extra_ms: # a stand-in for a slow model, such as a large text model
time.sleep(self.extra_ms * len(job.tickets) / 1000)
job.results = [
{"ticket_id": t.ticket_id, "score": r.score, "flag": r.flag}
for t, r in zip(job.tickets, results)
]
job.model_version = bundle.version
job.status = "done"
except Exception as error: # a failed job must not stop the worker
logger.exception("job %s failed", job.id)
job.status, job.error = "failed", str(error)
job.seconds = round(time.perf_counter() - started, 3)
job.finished_at = datetime.now(timezone.utc).isoformat(timespec="seconds")
job.tickets = [] # the input is not needed any more
self.queue.task_done()

Lisez-le en trois parties. submit donne un identifiant à la tâche et la place dans la file sans attendre. Si la file est pleine, put_nowait échoue aussitôt, et submit lève QueueFull. work s’exécute sans arrêt dans son propre fil d’exécution. Il prend la tâche suivante et calcule les scores de tous ses billets en un seul appel à score_tickets. Il stocke ensuite les résultats dans la tâche. Si quelque chose échoue, la tâche reçoit l’état failed (échec) avec l’erreur. Le fil d’exécution passe à la tâche suivante.

extra_ms_per_ticket sert seulement à l’apprentissage. Il ajoute une pause de ce nombre de millisecondes par billet, pour simuler un modèle lent. Son paramètre, EXTRA_MS_PER_TICKET, vaut 0 sauf si vous le modifiez.

Les routes HTTP POST /v1/jobs et GET /v1/jobs/{id} sont présentées dans Connecter l’API. Ici, vous essayez l’exécuteur directement, dans une session Python.

Exemple détaillé : deux tâches dans une session Python

Section intitulée « Exemple détaillé : deux tâches dans une session Python »

Amira démarre Python dans le dossier du projet. Elle charge le paquet et les billets de juillet, comme dans la leçon précédente :

Python session, in ~/projects/escalation-service
import time
import pandas as pd
from escalation.settings import load_settings
from escalation.bundle import load_bundle
from escalation.batch import COLUMNS, check_rows
from escalation.jobs import JobRunner, QueueFull
settings = load_settings()
bundle = load_bundle(settings.model_bundle, settings.model_sha256)
rows = pd.read_csv("data/july_tickets.csv", usecols=COLUMNS, dtype=str, keep_default_na=False, na_values=[""])
tickets, bad = check_rows(rows, first_row=2)

Une petite tâche. Elle crée un exécuteur avec une file de 4 places, la valeur par défaut de MAX_QUEUED_JOBS, et soumet 3 billets :

Python session (continued)
runner = JobRunner(lambda: bundle, max_queued=4)
job = runner.submit(tickets[:3])
job.id, job.status
time.sleep(1)
job.status, job.seconds, job.model_version
job.results[0]

Les sorties :

('68e0ec7a36af', 'running')
('done', 0.004, '1.0.0')
{'ticket_id': 'T-119233', 'score': 0.037624397312637836, 'flag': False}

submit a répondu aussitôt avec un identifiant. Le fil d’exécution avait déjà pris la tâche; son état était donc running, et non queued. Une seconde plus tard, la tâche était done. Elle a pris 4 ms, et son premier résultat est le score de T-119233 obtenu par la tâche par lots. Le même modèle et le même contrat donnent le même score dans chaque mode.

Une tâche lente. Amira simule maintenant le modèle de texte lent de Priya avec 40 ms par billet (simulées) et soumet le mois entier. Puis, pendant que la grosse tâche s’exécute, elle soumet cinq petites tâches de 10 billets :

Python session (continued)
slow = JobRunner(lambda: bundle, max_queued=4, extra_ms_per_ticket=40)
start = time.perf_counter()
big = slow.submit(tickets)
print(f"accepted in {(time.perf_counter() - start) * 1000:.1f} ms: job {big.id}, {big.status}")
time.sleep(0.5)
for n in range(1, 6):
try:
small = slow.submit(tickets[:10])
print(f"job {n} of 10 tickets: accepted")
except QueueFull:
print(f"job {n} of 10 tickets: refused, the queue is full")
while big.status in ("queued", "running"):
print(f"after {time.perf_counter() - start:2.0f} s: {big.status}")
time.sleep(10)
big.status, big.seconds, len(big.results)

Les sorties :

accepted in 2.9 ms: job 19733f90f765, running
job 1 of 10 tickets: accepted
job 2 of 10 tickets: accepted
job 3 of 10 tickets: accepted
job 4 of 10 tickets: accepted
job 5 of 10 tickets: refused, the queue is full
after 1 s: running
after 11 s: running
after 21 s: running
after 31 s: running
after 41 s: running
after 51 s: running
('done', 56.014, 1400)

Le mois a été accepté en 2,9 ms, même si le travail a pris 56 s. Dans une seule requête, il aurait échoué à 30 s. La pause d’une demi-seconde a laissé le temps au fil d’exécution de prendre la grosse tâche. La file était donc vide. Quatre petites tâches l’ont ensuite remplie, et la cinquième a été refusée aussitôt. La boucle fait de l’interrogation périodique : elle demande des nouvelles toutes les 10 secondes, comme un client avec Retry-After. Après 56 s, la tâche était done avec 1 400 résultats : 1 400 × 40 ms = 56 s, comme prévu.

Les quatre petites tâches ont attendu derrière la grosse. Cette attente est le prix à payer pour un seul fil d’exécution. Chaque tâche échappe à la limite de la passerelle, mais elle peut attendre plusieurs minutes dans une file chargée.

Chaque tâche passe par quelques états :

Un diagramme des états d'une tâche. Au départ, POST /v1/jobs avec la réponse 202 crée une tâche dans l'état queued. Si la file est pleine, la réponse est 429, et la requête se termine par un refus, sans tâche. Depuis queued, le processus de traitement prend la tâche, qui passe à running. Depuis running, elle passe à done, avec les scores conservés dans la tâche, ou à failed, avec l'erreur conservée dans la tâche. Une note sur running : les tâches sont dans la mémoire de ce processus; un redémarrage les perd donc toutes.
Les états d'une tâche. Une file pleine refuse aussitôt tout nouveau travail. Les scores et les erreurs restent dans la tâche, mais seulement dans la mémoire d'un processus.
Description textuelle du diagramme

Une tâche commence dans l’état queued, après que POST /v1/jobs a répondu 202. Si la file est pleine, la requête est refusée avec 429, et aucune tâche n’existe. Le processus de traitement prend une tâche en attente, et elle passe à running. Une tâche en cours se termine dans l’état done, avec ses scores conservés dans la tâche. Elle peut aussi se terminer dans l’état failed, avec son erreur conservée dans la tâche. Une note sur running : les tâches sont dans la mémoire de ce processus; un redémarrage les perd donc toutes.

Trois limites protègent la file. Elle contient au plus MAX_QUEUED_JOBS (4) tâches en attente. Un afflux de tâches ne peut donc pas utiliser toute la mémoire : la cinquième reçoit 429 too_many_jobs (trop de tâches). Une tâche contient au plus MAX_JOB_TICKETS (5 000) billets. Elle ne peut donc pas occuper le processus de traitement pendant des heures. Au-delà, la réponse est 413 job_too_large (tâche trop volumineuse). Enfin, les résultats restent dans la mémoire d’un processus jusqu’à son redémarrage. C’est le point faible, comme le montre le cas d’échec à la fin de cette leçon. Un vrai service conserve les résultats pendant une durée fixe, par exemple un jour, et l’indique dans sa documentation.

Vous connaissez maintenant trois modes. Le même modèle et la même fonction score_tickets se trouvent derrière chacun. Ce qui change, c’est qui lance le travail, qui attend et comment les résultats arrivent. Trois questions permettent de choisir :

  1. Qui attend le résultat, et combien de temps peut-on attendre? Personne, jusqu’à une heure connue : par lots. Une personne, moins d’une seconde : en ligne. Un programme, plusieurs minutes : asynchrone.
  2. Quelle est la taille d’un travail? Un cas : en ligne. Plusieurs cas qui arrivent ensemble à une heure prévue : par lots. Plusieurs cas envoyés par quelqu’un à la demande : asynchrone.
  3. Que se passe-t-il en cas d’échec? La réponse donne le plan de reprise dont le mode a besoin.

Le tableau résume le module.

Par lots En ligne Asynchrone
Qui lance le travail Un planificateur, à une heure fixe L’appelant, une requête par cas L’appelant, une tâche pour plusieurs cas
Combien de temps l’appelant attend Personne n’attend; les résultats sont prêts à une heure connue Toute la latence : budget de 50 ms chez Larkfield Quelques millisecondes pour le reçu (202), puis des minutes pour le résultat
Comment les résultats arrivent Fichiers : scores.csv, summary.json Dans la réponse : score, indicateur, version du modèle Interrogation périodique avec GET /v1/jobs/{id} jusqu’à done
Nombre clé Débit : 1 400 billets en 0,75 s Latence : environ 3 à 4 ms par requête Temps dans la file plus le travail
Limites 5 % de lignes incorrectes; colonnes du contrat Un billet, taille du corps, délais d’expiration File de 4 places (429), 5 000 billets (413)
Reprise après un échec Relancer dans le même dossier : mêmes entrées et même paquet, mêmes sorties Plan de l’appelant sans réponse : ouvrir la page sans score; le traitement par lots complète plus tard Soumettre à nouveau après 429, failed ou une tâche perdue; conserver les tâches dans un service de file d’attente et un stockage
Exemple chez Larkfield Le fichier nocturne de Mei pour l’examen du matin L’indicateur quand un agent ouvre un billet Un mois de billets pour l’équipe qualité, avec un modèle lent

Utilisez l’inférence asynchrone quand un appelant demande un travail à la demande, trop long pour l’attente permise dans une requête. Les cas courants comprennent les gros fichiers, les modèles lents et les travaux dont vous ne pouvez pas prévoir la durée.

Ne l’utilisez pas quand le travail respecte le budget de latence ou se fait seulement à des heures prévues. Les modes en ligne et par lots sont plus simples.

Elle coûte plus cher que les deux autres. Le service doit stocker les tâches et les résultats, limiter la file et supprimer les anciens résultats. Chaque appelant a aussi besoin d’une boucle d’interrogation périodique. Un vrai système a également besoin d’un service de file d’attente et d’un stockage. Ce sont d’autres éléments à exploiter et à payer.

  1. Créez escalation/jobs.py, comme dans L’exécuteur de tâches : jobs.py.

  2. Ouvrez un terminal dans ~/projects/escalation-service, activez .venv et définissez MODEL_SHA256.

  3. Démarrez Python avec python et saisissez le premier bloc de Exemple détaillé : deux tâches dans une session Python.

  4. Saisissez le bloc de la petite tâche. Vérifiez que l’état devient done et que job.results[0] contient le score 0.037624397312637836 pour T-119233.

  5. Saisissez le bloc de la tâche lente. Attendez-vous à ce que la boucle s’exécute pendant environ une minute.

  6. Vérifiez la dernière ligne : 'done', environ 56 secondes et 1 400 résultats. Vérifiez qu’exactement une des cinq petites tâches a été refusée.

  7. Quittez Python avec exit().

  8. Exécutez python -m pytest. Il indique encore 20 passed (20 tests réussis) : jobs.py reçoit ses tests avec les routes HTTP dans le module 3.

Afficher ce qui peut différer sur votre ordinateur

Vos identifiants de tâche sont différents : ils sont aléatoires. L’état juste après submit peut être queued ou running. Cela dépend du fait que le fil d’exécution a déjà pris la tâche ou non. Deux petites tâches peuvent être refusées au lieu d’une. Dans ce cas, le fil d’exécution n’avait pas pris la grosse tâche après une demi-seconde. La grosse tâche occupait donc aussi une place dans la file. Votre valeur seconds pour la tâche lente dépasse légèrement 56. La pause de 56 s est simulée, et le calcul réel des scores ajoute quelques millisecondes.

À vous de jouer : la plus grande requête qui respecte la limite

Section intitulée « À vous de jouer : la plus grande requête qui respecte la limite »

Le modèle de texte de Priya prend 40 ms par billet de soutien (simulation). Combien de billets une requête peut-elle contenir au maximum sans dépasser la limite de 30 secondes de la passerelle? Notez votre réponse, puis vérifiez-la avec l’explorateur. Sa deuxième commande règle le nombre de billets.

Explorer

Temps par billet et billets par requête

Durée par billet
40 ms (simulé)
Billets dans la requête
1 400 billets
Une requête prend
56 s
Résultat
Utilisez une tâche

1 400 billets × 40 ms par billet (simulé) = 56 s. La passerelle arrête toute requête après 30 s, donc cette requête ne peut jamais finir. Envoyez-la comme tâche : le service répond 202 tout de suite, et les scores sont prêts après environ 56 s.

  • Tient dans les 50 ms du centre d’assistance
  • Plus de 50 ms, moins de 30 s
  • Plus de 30 s : utilisez une tâche
  • Pâle, pointillé : durée simulée
Une requête : 56 s56 s (simulé)le centre d’assistance attend 50 msla passerelle arrête à 30 s1 ms10 ms100 ms1 s10 s1 min10 min1 hDurée d’une requête (chaque pas : ×10)
Une requête : 56 s56 s (simulé)assistance : 50 mspasserelle : 30 s1 ms100 ms10 s1 hDurée d’une requête (chaque pas : ×10)
Deux commandes : le temps par billet (1,76 ms mesuré, ou une simulation) et le nombre de billets dans une requête. L'affichage indique si la requête respecte 50 ms, respecte 30 secondes ou nécessite une tâche.
Description textuelle de l’explorateur

L’explorateur commence à 40 ms par billet (simulation), avec 1 400 billets. Cela donne 56 secondes, au-delà de la limite de 30 secondes. Il indique donc d’utiliser une tâche. Une deuxième commande règle le nombre de billets dans une requête : 1, 20, 200, 1 400 ou 5 000.

À 40 ms, un billet respecte les 50 ms du service de soutien. Avec 20 billets, une requête prend 800 ms. Avec 200 billets, elle prend 8 secondes : une seule requête, mais une longue attente. Avec 1 400 billets, elle prend 56 secondes; avec 5 000 billets, 3,3 minutes. Les deux nécessitent une tâche. Au temps mesuré de 1,76 ms, même 5 000 billets traités un par un prennent seulement 8,8 secondes.

Afficher la réponse

30 000 ms ÷ 40 ms = 750 billets. Dans l’explorateur, 200 billets prennent 8 secondes et respectent la limite; 1 400 prennent 56 secondes et la dépassent. Mais 750 billets prendraient les 30 secondes au complet, sans marge pour une période chargée. Une requête sûre est beaucoup plus petite, par exemple 200 billets. Pour un mois de billets, utilisez une tâche.

Erreur courante

GET pour une tâche répond 404 après un redémarrage

Symptôme : l’équipe qualité soumet la tâche de juillet et reçoit 202 avec un identifiant de tâche. Quelques minutes plus tard, les vérifications périodiques reçoivent soudainement 404, même si l’identifiant est correct. Amira a fait l’essai avec le service terminé du module 3. Elle a soumis 3 billets et reçu l’identifiant de tâche e6cd444d099c. Elle a vu l’état done (terminé), redémarré le service, puis demandé à nouveau :

HTTP/1.1 404 Not Found
...
{"error":{"code":"job_not_found","message":"There is no job with this ID here.","request_id":"ec357afe6eb3","fields":[]}}

Cause : les tâches résident dans la mémoire d’un seul processus. Un redémarrage, après une mise en production ou une panne, lance un nouveau processus avec un dictionnaire jobs vide. Toutes les tâches en attente, en cours et terminées disparaissent. Avec deux processus, chacun possède son propre jobs. Une requête d’état peut donc aussi atteindre le processus qui n’a jamais eu la tâche.

Solution : dans ce cours, un appelant qui reçoit 404 pour une tâche qu’il a soumise doit la soumettre à nouveau. Dans un système réel, conservez les tâches dans un service de file d’attente et les résultats dans un stockage externe au processus. Ainsi, n’importe quel processus peut répondre, et un redémarrage ne perd rien. Le cours Mise à l’échelle des API et des charges de travail d’IA traite des services de messagerie et de file d’attente.

Pour un vrai service d’IA : une tâche acceptée est une promesse. Avant d’accepter un travail qui prend des minutes, décidez où la tâche réside quand le processus s’arrête. Dites aux appelants combien de temps les résultats sont conservés.

Vérifiez votre compréhension

Pourquoi POST /v1/jobs répond-il 202 Accepted plutôt que 200 OK avec les scores?

Afficher la réponse

202 Accepted (accepté) signifie « le travail est accepté, mais il n’est pas encore terminé ». Les scores n’existent pas quand le service répond; la tâche est seulement dans la file d’attente. La réponse fournit l’identifiant de tâche et Location (emplacement) pour demander son état. Retry-After (réessayer après) indique quand demander à nouveau. L’appelant obtient les scores plus tard avec GET /v1/jobs/{id}, quand l’état est done.

Vérifiez votre compréhension

La limite de la file d'attente est de 4. Une grosse tâche s'exécute, et l'équipe qualité soumet 6 tâches à la fois. Prédisez ce qui arrive à chacune des 6 tâches. Expliquez pourquoi une limite vaut mieux qu'une file d'attente illimitée.

Afficher la réponse

La tâche en cours n’est plus dans la file d’attente. Les 4 premières nouvelles tâches sont acceptées et attendent dans la file (202). Les tâches 5 et 6 sont refusées immédiatement avec 429 too_many_jobs (trop de tâches). L’équipe doit les soumettre à nouveau plus tard. Sans limite, une arrivée massive de tâches pourrait remplir la mémoire du service. Toutes les tâches, y compris les requêtes des agents, en souffriraient. Une limite refuse tôt et clairement, tout en gardant le service en santé.

Exercice du module : choisir un mode pour trois charges de travail

Section intitulée « Exercice du module : choisir un mode pour trois charges de travail »

D’abord, un exemple détaillé. Les réponses d’Amira aux trois besoins de Larkfield forment les colonnes du tableau dans Choisir un mode de service. Lisez chaque colonne à partir du haut : qui lance le travail, qui attend et combien de temps, comment arrivent les résultats, puis la reprise après échec.

Votre tâche. Choisissez un mode pour chacune des trois charges de travail fictives ci-dessous. Pour chacune, répondez aux trois questions et indiquez le mode, la réception des résultats et la reprise après échec nécessaire.

  1. La liste des clients à risque de départ. L’équipe marketing d’une petite boutique en ligne envoie un courriel de rabais chaque lundi matin. Elle cible les clients susceptibles de ne plus acheter. Un modèle de départ des clients attribue un score aux 80 000 clients. Les courriels partent à 9 h.
  2. Le paiement par carte. Une entreprise de paiement vérifie chaque paiement par carte pour détecter la fraude pendant que le client attend à la caisse. Le terminal de la boutique attend au maximum 2 secondes pour l’ensemble du paiement. Le modèle de fraude prend environ 15 ms par paiement.
  3. Le recalcul des scores des polices. Une compagnie d’assurance recalcule les scores de ses 2 millions de polices d’assurance habitation une fois par mois. Elle le fait quand elle met à jour son modèle de risque. Le modèle est lent : environ 200 ms par police. Les actuaires demandent un recalcul depuis leur propre outil quand le nouveau modèle est prêt. Ils ont besoin des résultats dans les deux jours.
Afficher une réponse modèle

1. La liste des clients à risque de départ : par lots. Personne n’attend un seul score; l’équipe marketing a besoin de la liste avant 9 h le lundi. Les 80 000 clients reçoivent leur score ensemble. Exécutez la tâche le dimanche soir, écrivez la liste et un résumé dans un dossier daté, puis envoyez les courriels. Utilisez la liste pour les destinataires. Les utilisateurs reçoivent les résultats dans un fichier. Reprise : mettez les lignes clients incorrectes de côté, avec une limite d’échecs, et écrivez le fichier de façon atomique. Si la tâche échoue, avertissez quelqu’un et relancez-la avant 9 h. Une liste manquante signifie aucun courriel cette semaine, pas des courriels envoyés aux mauvaises personnes.

2. Le paiement par carte : en ligne. Un client attend, et un paiement représente un cas. La vérification de fraude reçoit un budget de latence, une partie des 2 secondes, par exemple 100 ms. Les 15 ms respectent ce budget. Les utilisateurs reçoivent le résultat dans la réponse : approuver, refuser ou vérifier manuellement. Reprise : le terminal a besoin d’un plan si aucune réponse n’arrive dans le budget prévu. L’entreprise doit le décider d’avance, par exemple « approuver les petits paiements, vérifier les gros manuellement ». Le service a besoin d’une haute disponibilité, car les paiements arrivent toute la journée.

3. Le recalcul des scores des polices : asynchrone (ou par lots, s’il s’exécute à date fixe). 2 millions × 200 ms = 400 000 secondes, soit environ 4,6 jours avec un seul processus. Aucune requête ne peut donc contenir ce travail. L’outil des actuaires soumet une tâche et reçoit immédiatement un identifiant de tâche. Le service découpe le travail en plusieurs petites parties et les exécute sur plusieurs processus pour terminer dans les deux jours. Les utilisateurs vérifient périodiquement la tâche, puis téléchargent un fichier de résultats depuis le stockage. Reprise : une exécution de plusieurs jours connaîtra un redémarrage. Les tâches et leur progression résident donc dans un service de file d’attente et du stockage, pas dans la mémoire d’un processus. Une partie qui échoue est relancée, pas la tâche entière.

Vérifiez votre compréhension

Nommez les trois modes de service de ce module. Pour chacun, dites comment les utilisateurs reçoivent les résultats.

Afficher la réponse

Par lots : les utilisateurs lisent les résultats dans des fichiers écrits par une exécution planifiée, comme scores.csv avec summary.json. En ligne : l’appelant reçoit le score dans la réponse à sa propre requête, pendant qu’il attend. Asynchrone : l’appelant reçoit immédiatement un identifiant de tâche (202). Il interroge ensuite périodiquement GET /v1/jobs/{id} jusqu’à l’état done, puis obtient les scores dans cette réponse.

Vérifiez votre compréhension

Le modèle de Larkfield devient 30 fois plus lent par billet. Prédisez les effets sur chacun des trois besoins de Larkfield, avec les nombres de ce module.

Afficher la réponse

30 × 1,76 ms ≈ 53 ms par billet. En ligne : un billet dépasse le budget de 50 ms. La page s’ouvre donc sans score la plupart du temps; le mode en ligne ne convient plus. Par lots : environ 45 billets par nuit prennent environ 2,4 secondes, et même 1 400 prennent environ 74 secondes. Cela termine bien avant 8 h, donc le mode par lots fonctionne encore. Asynchrone : un mois de 1 400 billets prend environ 74 secondes, au-delà des 30 secondes de la passerelle. Il faut donc une tâche, et une tâche fonctionne encore. Le modèle plus lent compromet seulement le mode où une personne attend.

Vérifiez votre compréhension

Vérification de fin : pour les modes par lots, en ligne et asynchrone, expliquez la réception des résultats et la reprise après échec nécessaire.

Afficher la réponse

Par lots : les utilisateurs lisent un fichier, comme scores.csv, à une heure connue. Reprise : les lignes incorrectes vont dans rejected.csv. Trop de lignes incorrectes, ou une colonne manquante, font échouer l’exécution sans produire de sortie. La tâche écrit ses fichiers de façon atomique. summary.json consigne les empreintes de l’entrée et de l’ensemble empaqueté. La tâche peut ainsi être relancée avant que quelqu’un ait besoin des résultats.

En ligne : l’appelant reçoit le score dans la réponse, dans le budget de latence (50 ms chez Larkfield). La reprise relève de l’appelant : un délai maximal et un plan en cas d’absence de réponse. Chez Larkfield, la page s’ouvre sans score, et le traitement par lots de nuit rattrape le travail. Le service a besoin de disponibilité et d’un signal clair « pas prêt ».

Asynchrone : l’appelant reçoit un identifiant de tâche (202) et vérifie périodiquement jusqu’à l’état done, puis lit les scores. Reprise : 429 signifie réessayer plus tard. Une tâche failed (échouée) conserve son erreur et est soumise à nouveau. Une tâche perdue après un redémarrage est soumise à nouveau. En production, les tâches et les résultats résident dans un service de file d’attente et du stockage. Un redémarrage ne perd donc rien.

Récapitulatif

  • L’inférence asynchrone accepte une tâche immédiatement (202 Accepted, un identifiant de tâche, Location, Retry-After). Elle calcule les scores en arrière-plan, et l’appelant vérifie périodiquement le résultat. Une file d’attente contient les tâches qui attendent.
  • Une requête de 1 400 billets dépasse la limite de 30 secondes d’une passerelle à environ 21 ms par billet. À 40 ms par billet (simulation), le mois a pris 56 secondes comme tâche, acceptée en 2,9 ms.
  • Les limites gardent le service en santé : 4 tâches en attente (429), 5 000 billets par tâche (413). Les tâches en mémoire sont perdues au redémarrage. Les systèmes réels utilisent un service de file d’attente et du stockage (voir Mise à l’échelle des API et des charges de travail d’IA).
  • Module 2 : choisissez le mode selon qui attend, la taille d’une unité de travail et la reprise après échec. Par lots pour les listes planifiées; en ligne pour un cas pendant que quelqu’un attend; asynchrone pour les gros travaux sur demande. Le même score_tickets sert aux trois.

Revenir à un état connu. Le module 2 n’a créé ni conteneur ni image. Si votre projet ne fonctionne plus, copiez l’instantané end-of-m02 dans ~/projects/escalation-service, comme dans Utiliser un instantané. Exécutez ensuite python -m pytest : 20 passed (20 tests réussis). Vous pouvez supprimer le dossier out/; il figure dans .gitignore.

Ensuite, vous placerez le calcul des scores derrière un service HTTP. Le service charge l’ensemble empaqueté une seule fois, au démarrage. Chaque requête ne fait donc que le travail rapide.

Leçon suivante : Charger et initialiser