Aller au contenu
Développement medium

Servir un LLM : batching et débit

9 min de lecture

Faire tourner un LLM pour vous seul est une chose. Le servir à des dizaines d'utilisateurs en même temps en est une autre. C'est ce second problème que résolvent les serveurs d'inférence comme vLLM ou SGLang, grâce à une poignée de techniques : le batching, le continuous batching, le PagedAttention. Ce guide explique ces mécanismes sans jargon, pour comprendre ce qui se cache derrière les performances annoncées. Public visé : lecteur débutant à intermédiaire ayant déjà lu le guide sur l'inférence.

  • Comprendre pourquoi servir plusieurs utilisateurs demande des techniques particulières.
  • Saisir ce qu'est le batching et pourquoi il améliore le débit.
  • Distinguer le batching statique du continuous batching.
  • Comprendre à quoi servent PagedAttention et RadixAttention.
  • Savoir pourquoi le débit et la latence s'opposent sous charge.

Imaginez un serveur qui traite les requêtes une par une. L'utilisateur A pose sa question, le modèle génère sa réponse token par token, puis le serveur passe à l'utilisateur B. Simple — mais terriblement inefficace.

La raison tient à la phase de decode. Générer un token ne sature pas le GPU : l'opération déplace beaucoup de données (les poids du modèle) pour assez peu de calcul. Pendant qu'une requête isolée fait son decode, le GPU est largement sous-utilisé, comme un camion de déménagement qui ferait l'aller-retour avec un seul carton.

Le problème à résoudre est donc : comment occuper pleinement le GPU alors que chaque requête, prise seule, ne lui donne pas assez de travail ?

Le batching : traiter plusieurs requêtes ensemble

Section intitulée « Le batching : traiter plusieurs requêtes ensemble »

La réponse s'appelle le batching : regrouper plusieurs requêtes dans un même calcul. Plutôt que de générer le prochain token pour une seule requête, le GPU le génère pour dix, cinquante ou cent requêtes en même temps.

L'astuce est que ce traitement groupé coûte à peine plus cher qu'une requête seule. Les poids du modèle, déjà chargés, servent à toutes les requêtes du lot d'un coup. Le débit — le nombre total de tokens produits par seconde — grimpe donc fortement.

Tous les batchings ne se valent pas. La première approche, le batching statique, forme un lot de requêtes, lance le calcul, et attend que toutes les requêtes du lot soient terminées avant de constituer le lot suivant.

Le défaut saute aux yeux. Les réponses n'ont pas toutes la même longueur : une requête qui génère 20 tokens se retrouve coincée dans le lot avec une requête qui en génère 500. Elle a fini, mais elle attend. Le GPU traite des places vides pendant que de nouvelles requêtes patientent dehors.

Le continuous batching corrige ce gaspillage. À chaque token, le serveur retire du lot les requêtes terminées et y injecte les nouvelles en attente. Le lot est recomposé en permanence, le GPU ne traite jamais de place vide. C'est la technique qu'emploient les serveurs d'inférence modernes comme vLLM et SGLang.

PagedAttention : ranger le KV cache sans gaspiller

Section intitulée « PagedAttention : ranger le KV cache sans gaspiller »

Le batching pose un nouveau défi : le KV cache. Chaque requête du lot a le sien, et il grandit au fil de la génération. Combien de mémoire réserver pour chacun ?

L'approche naïve réserve, pour chaque requête, un bloc contigu dimensionné au pire cas — la longueur de réponse maximale possible. La plupart des réponses étant courtes, l'essentiel de cette mémoire reste inutilisé. La VRAM se gaspille, et le serveur accepte beaucoup moins de requêtes qu'il ne pourrait.

PagedAttention applique au KV cache l'idée de la mémoire virtuelle d'un système d'exploitation. Le cache est découpé en petites pages de taille fixe, allouées à la demande, au fur et à mesure que la réponse s'allonge. Plus de gros bloc réservé « au cas où » : chaque requête ne consomme que ce qu'elle utilise vraiment. Résultat, bien plus de requêtes simultanées tiennent dans la même carte.

RadixAttention : réutiliser les débuts identiques

Section intitulée « RadixAttention : réutiliser les débuts identiques »

Une autre optimisation, propre à SGLang, s'attaque à un gaspillage différent. Dans la vraie vie, beaucoup de requêtes commencent pareil. Un assistant a toujours le même system prompt. Un chatbot renvoie tout l'historique de la conversation à chaque tour. Un système RAG répète les mêmes consignes.

Sans optimisation, le serveur recalcule à chaque fois le KV cache de cette partie commune. RadixAttention l'évite : il indexe les débuts de texte déjà calculés et réutilise leur cache quand une nouvelle requête partage le même préfixe. Le prefill de la partie commune devient quasi gratuit.

L'effet est nul si les requêtes n'ont rien en commun, mais considérable sur les charges très répétitives — agents, chatbots multi-tours, RAG. C'est l'argument central du guide SGLang.

Parfois, le modèle est tout simplement trop gros pour une seule carte. Un modèle de 70 milliards de paramètres en 16 bits réclame environ 140 Go de mémoire — bien au-delà des 80 Go d'un GPU haut de gamme.

La solution est le tensor parallelism : on découpe le modèle sur plusieurs GPU, qui calculent chacun leur part puis échangent leurs résultats. Du point de vue de l'utilisateur, rien ne change : il interroge un serveur unique. En coulisses, deux ou quatre cartes se partagent le travail. Les serveurs d'inférence exposent ce réglage sous un nom comme tensor-parallel-size.

Toutes ces techniques visent le débit. Mais elles ont une contrepartie qu'il faut comprendre pour bien dimensionner un service.

Plus on remplit les lots de requêtes, meilleur est le débit total — mais chaque requête individuelle attend un peu plus, car elle partage le GPU avec davantage de voisines. À l'inverse, des lots quasi vides donnent une latence minimale par requête, mais un débit médiocre et un GPU sous-exploité.

Il n'existe pas de réglage universellement bon. Le bon point de fonctionnement dépend de votre priorité : un chatbot interactif privilégie la latence, un traitement par lots de documents privilégie le débit. Choisir et régler un serveur d'inférence, c'est avant tout placer ce curseur en connaissance de cause.

  • Une requête isolée sous-utilise le GPU ; le batching le remplit en traitant plusieurs requêtes ensemble.
  • Le batching statique fait attendre les requêtes courtes ; le continuous batching recompose le lot à chaque token.
  • PagedAttention découpe le KV cache en pages allouées à la demande : beaucoup plus de requêtes dans la même VRAM.
  • RadixAttention réutilise le cache des débuts de texte identiques : décisif pour les charges répétitives.
  • Le tensor parallelism répartit un modèle trop gros sur plusieurs GPU.
  • Débit et latence s'opposent sous charge : le bon réglage dépend de votre cas d'usage.

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn