Transformers documentation

Inférence sur serveur Web

You are viewing main version, which requires installation from source. If you'd like regular pip install, checkout the latest stable version (v5.22.2).
Hugging Face's logo
Join the Hugging Face community

and get access to the augmented documentation experience

to get started

Inférence sur serveur Web

Un serveur web est un système qui attend des requêtes et les traite au fur et à mesure de leur arrivée. Cela signifie que vous pouvez utiliser Pipeline comme moteur d'inférence sur un serveur web, car vous pouvez utiliser un itérateur (de la même manière que vous itéreriez sur un jeu de données) pour traiter chaque requête entrante.

Concevoir un serveur web avec Pipeline est toutefois particulier, car les deux sont fondamentalement différents. Les serveurs web sont multiplexés (multithreading, asynchrone, etc.) afin de traiter plusieurs requêtes de manière concurrente. Pipeline et le modèle sous-jacent, en revanche, ne sont pas conçus pour le parallélisme, car ils consomment beaucoup de mémoire. Il est préférable d’attribuer à un Pipeline toutes les ressources disponibles lorsqu’il est en cours d’exécution ou pour une tâche intensivement calculatoire.

Ce guide montre comment contourner cette différence en utilisant un serveur web pour gérer la charge plus légère de la réception et de l’envoi des requêtes, et un seul thread pour gérer la charge plus lourde de l’exécution de Pipeline.

Créer un serveur

Starlette est un framework léger pour la création de serveurs web. Vous pouvez utiliser tout autre framework de votre choix, mais il est possible que vous deviez apporter quelques modifications au code ci-dessous.

Avant de commencer, assurez-vous que Starlette et uvicorn sont installés.

!pip install starlette uvicorn

Vous pouvez maintenant créer un simple serveur web dans un fichier server.py. L’essentiel est de charger le modèle une seule fois afin d’éviter que des copies inutiles n’occupent de la mémoire.

Créez un pipeline pour compléter le jeton masqué, [MASK].

from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
from transformers import pipeline
import asyncio

async def homepage(request):
    payload = await request.body()
    string = payload.decode("utf-8")
    response_q = asyncio.Queue()
    await request.app.model_queue.put((string, response_q))
    output = await response_q.get()
    return JSONResponse(output)

async def server_loop(q):
    pipe = pipeline(task="fill-mask",model="google-bert/bert-base-uncased")
    while True:
        (string, response_q) = await q.get()
        out = pipe(string)
        await response_q.put(out)

app = Starlette(
    routes=[
        Route("/", homepage, methods=["POST"]),
    ],
)

@app.on_event("startup")
async def startup_event():
    q = asyncio.Queue()
    app.model_queue = q
    asyncio.create_task(server_loop(q))

Démarrer le serveur avec la commande suivante.

uvicorn server:app

Interroger le serveur avec une requête POST.

curl -X POST -d "Paris is the [MASK] of France." http://localhost:8000/

Cela doit renvoyer la sortie ci-dessous.

[{'score': 0.9969332218170166,
  'token': 3007,
  'token_str': 'capital',
  'sequence': 'paris is the capital of france.'},
 {'score': 0.0005914849461987615,
  'token': 2540,
  'token_str': 'heart',
  'sequence': 'paris is the heart of france.'},
 {'score': 0.00043787318281829357,
  'token': 2415,
  'token_str': 'center',
  'sequence': 'paris is the center of france.'},
 {'score': 0.0003378340043127537,
  'token': 2803,
  'token_str': 'centre',
  'sequence': 'paris is the centre of france.'},
 {'score': 0.00026995912776328623,
  'token': 2103,
  'token_str': 'city',
  'sequence': 'paris is the city of france.'}]

Mise en file d'attente des requêtes

Le mécanisme de file d’attente du serveur peut être utilisé pour des applications intéressantes telles que le lotissement dynamique. Le lotissement dynamique accumule d’abord plusieurs requêtes avant de les traiter avec Pipeline.

L'exemple ci-dessous est écrit en pseudocode pour la lisibilité plutôt que pour la performance. En particulier, vous remarquerez que :

  1. Il n'y a pas de limite de taille de lot.

  2. Le délai d'attente est réinitialisé à chaque récupération de file, ce qui peut entraîner une attente bien supérieure à la valeur de timeout avant le traitement d'une requête. Cela retarderait également la première requête d'inférence de cette même durée. Le serveur web attend toujours 1 ms, même si la file est vide, ce qui est inefficace, car ce temps pourrait être utilisé pour démarrer l'inférence. Cela pourrait toutefois se justifier si le groupement (batching) est essentiel à votre cas d'utilisation.

    Il serait préférable d’avoir une seule échéance de 1 ms, plutôt que de la réinitialiser à chaque récupération, comme indiqué ci-dessous.

async def server_loop(q):
    pipe = pipeline(task="fill-mask", model="google-bert/bert-base-uncased")
    while True:
        (string, rq) = await q.get()
        strings = []
        queues = []
        strings.append(string)
        queues.append(rq)
        while True:
            try:
                (string, rq) = await asyncio.wait_for(q.get(), timeout=1)
            except asyncio.exceptions.TimeoutError:
                break
            strings.append(string)
            queues.append(rq)
        outs = pipe(strings, batch_size=len(strings))
        for rq, out in zip(queues, outs):
            await rq.put(out)

Vérification des erreurs

De nombreux problèmes peuvent survenir en production. Vous pouvez rencontrer un dépassement de mémoire, un manque d’espace disque, un échec du chargement d’un modèle, une configuration de modèle incorrecte, une requête erronée, et bien d’autres encore.

L’ajout de blocs try...except est utile pour renvoyer ces erreurs à l’utilisateur afin de faciliter le débogage. Gardez à l’esprit que cela peut représenter un risque de sécurité si vous ne devriez pas divulguer certaines informations.

Circuit breaking

Essayez de renvoyer une erreur 503 ou 504 lorsque le serveur est surchargé, plutôt que de forcer l'utilisateur à attendre indéfiniment.

Il est relativement simple de mettre en œuvre ces types d’erreurs, car il ne s’agit que d’une seule file d’attente. Examinez la taille de la file pour déterminer à quel moment commencer à renvoyer des erreurs avant que votre serveur ne tombe sous la charge.

Bloquer le thread principal

PyTorch n'est pas conscient de l'asynchronisme, de sorte que le calcul bloquera l'exécution du thread principal.

Pour cette raison, il est préférable d’exécuter PyTorch dans un thread ou un processus séparé. Lorsque l’inférence d’une seule requête est particulièrement longue (plus de 1 s), c’est encore plus important, car cela signifie que chaque requête pendant l’inférence doit attendre 1 s avant même de recevoir une erreur.

Batching dynamique

Le batch dynamique peut s’avérer très efficace lorsqu’il est utilisé dans le bon contexte, mais il n’est pas nécessaire lorsque vous ne transmettez qu’une seule requête à la fois (voir inférence par lot pour plus de détails).

Update on GitHub