Batch continu pour les LLM : gagner du débit sans sacrifier la latence

Prefill, decode, KV cache, PagedAttention et admission : comprendre le batch continu et construire un benchmark d’inférence reproductible.

Un serveur de génération ne traite pas des requêtes de durée prévisible. Un prompt de 200 tokens peut produire trois mots ; un autre, reçu au même instant, peut générer plusieurs centaines de tokens. Si le serveur forme un lot fixe et attend que toutes les séquences se terminent, les plus courtes occupent artificiellement leur place jusqu’à la fin de la plus longue.

Le batch continu corrige ce gaspillage : à chaque itération de décodage, le planificateur peut retirer les requêtes terminées et admettre celles qui attendent. Le GPU travaille sur un ensemble qui évolue. Cette idée augmente souvent le débit, mais elle déplace le problème vers la mémoire du KV cache et l’arbitrage entre temps d’attente, premier token et régularité du streaming.

Préfill et décodage n’ont pas le même profil

La génération autoregressive comporte deux phases. Pendant le prefill, le modèle traite les tokens du prompt et construit les clés et valeurs d’attention. L’opération peut paralléliser le prompt et sollicite fortement le calcul. Pendant le decode, le modèle produit généralement un token par séquence et par itération en relisant les poids et le KV cache ; la bande passante mémoire devient souvent déterminante.

Mélanger une longue phase de prefill avec des séquences déjà en streaming peut retarder leur prochain token. À l’inverse, toujours privilégier le decode maintient un flux régulier mais laisse les nouvelles requêtes attendre. Le planificateur ne cherche donc pas seulement à remplir le GPU : il arbitre entre deux formes de latence.

Du lot statique au lot continu

Dans un lot statique, N requêtes entrent ensemble. Les séquences sont souvent alignées par padding et le lot reste fermé jusqu’à leur achèvement. Cette approche est simple et efficace pour des entrées de taille proche, mais elle convient mal à un service interactif dont les arrivées et longueurs varient.

Avec un batch continu — aussi appelé in-flight batching ou planification au niveau de l’itération — le contenu du lot est réévalué à chaque étape :

  1. les séquences actives produisent leur prochain token ;
  2. celles qui rencontrent un token de fin ou leur limite sont retirées ;
  3. la mémoire correspondante peut être libérée ;
  4. de nouvelles requêtes sont admises si le budget de tokens et de mémoire le permet ;
  5. le lot suivant repart avec une forme différente.

La documentation Transformers de Hugging Face décrit précisément ce réordonnancement dynamique. TGI, vLLM et TensorRT-LLM proposent des mécanismes comparables, avec des algorithmes et paramètres différents. Il vaut mieux utiliser leur planificateur éprouvé que reproduire le concept avec une boucle Python : synchronisation GPU, annulations, streaming, préemption et comptabilité mémoire rendent l’implémentation réelle beaucoup plus complexe.

Le KV cache fixe la capacité réelle

À chaque couche du Transformer, les clés et valeurs déjà calculées sont conservées pour éviter de retraiter tout le contexte à chaque nouveau token. Ce KV cache croît avec le nombre de séquences actives et leur longueur. Une requête qui annonce un contexte ou une sortie très longue peut donc réserver une part importante de la mémoire, même si le nombre de requêtes paraît faible.

Le papier vLLM a popularisé PagedAttention, qui découpe le KV cache en blocs plutôt que d’exiger une zone contiguë dimensionnée au pire cas. L’objectif est de réduire fragmentation et réservation inutile, puis de rendre possibles des lots plus grands. Ce mécanisme complète le batch continu : le planificateur remplit les places, la mémoire paginée permet de les allouer plus finement.

La pagination n’élimine pas la limite physique. Lorsque les blocs manquent, le moteur doit refuser, mettre en attente, préempter, recalculer ou décharger une partie du cache. Chacune de ces stratégies modifie la latence. La métrique « GPU utilisé à 100 % » ne dit pas si le serveur accomplit un travail utile ou rejoue des calculs après préemption.

Débit et latence : mesurer les bons objets

MesureCe qu’elle révèle
Temps en fileSaturation et efficacité de l’admission
TTFT, temps jusqu’au premier tokenAttente, prefill et réactivité initiale
ITL ou TPOTRégularité entre les tokens pendant le streaming
Latence totaleExpérience complète selon la longueur de sortie
Tokens d’entrée et de sortie par secondeCapacité utile du serveur
GoodputRequêtes qui respectent réellement les objectifs de latence
Occupation et préemptions du KV cachePression mémoire cachée derrière la charge

Une moyenne masque les files d’attente et les séquences pénalisées. Il faut publier au minimum médiane, p95 et p99 par classe de requête. Le débit maximal obtenu avec une file infinie n’est pas une capacité de production si le TTFT dépasse l’objectif utilisateur.

Les réglages qui changent le compromis

  • Budget total de tokens : borne la somme des tokens présents ou potentiels dans le lot, plus utile qu’un simple nombre de requêtes.
  • Budget de prefill : évite qu’une vague de longs prompts bloque trop longtemps les décodages en cours.
  • Nombre de requêtes concurrentes : protège mémoire, file et temps de planification.
  • Longueurs maximales : empêchent une demande exceptionnelle de monopoliser le KV cache.
  • Politique de préemption : choisit quelles séquences suspendre et si leur état sera conservé ou recalculé.
  • Chunked prefill : découpe les longs prompts pour mieux les entrelacer avec le decode, au prix d’une planification plus complexe.
  • Prefix caching : réutilise le KV cache de préfixes identiques lorsque le moteur et le workload s’y prêtent.

Les noms exacts changent selon le moteur. Par exemple, TGI expose un nombre maximal de tokens par lot et un seuil d’attente avant d’insérer de nouveaux prefills. TensorRT-LLM planifie tant que le KV cache dispose de place. Ces paramètres doivent être calibrés à partir d’une distribution de requêtes, pas copiés depuis un benchmark.

Un benchmark reproductible

  1. Figer l’environnement : modèle et révision, tokenizer, quantification, moteur, conteneur, pilotes, GPU, nombre de cartes et parallélisme.
  2. Décrire le workload : distribution des longueurs de prompts et sorties, paramètres de génération, proportion de préfixes communs et requêtes annulées.
  3. Choisir le modèle de charge : concurrence fermée pour une capacité brute, puis arrivées à débit contrôlé pour reproduire un service réel.
  4. Préchauffer : séparer chargement du modèle, compilation éventuelle et régime stable.
  5. Balayer la charge : augmenter progressivement les requêtes par seconde jusqu’à ce que file, TTFT ou erreurs franchissent le SLO.
  6. Répéter : conserver seeds, corpus, commandes, configuration et résultats bruts pour comparer une version future.

La capacité à annoncer n’est pas le pic de tokens par seconde. C’est le débit soutenu auquel les percentiles de TTFT et d’ITL restent acceptables, sans préemptions excessives ni erreurs de mémoire.

Prévoir la surcharge

Le batch continu utilise mieux le GPU ; il ne crée pas de capacité infinie. Au-delà du point de saturation, la file grossit et le TTFT se dégrade avant que l’utilisation GPU ne fournisse un signal clair. Le serveur doit disposer de limites de file, délais d’expiration, annulation, classes de priorité et mécanismes de backpressure.

L’autoscaling doit combiner plusieurs signaux : file d’attente, tokens en cours, pression du KV cache et latence, avec le temps nécessaire au chargement du modèle. Monter une réplique après l’explosion du p99 arrive souvent trop tard si le poids du modèle demande plusieurs minutes à charger.

Le batch continu est donc une technique de planification, pas un bouton « performance ». Bien réglé, il remplace immédiatement les séquences terminées et augmente le travail utile par passage GPU. Mal réglé, il favorise le débit agrégé au détriment du premier token ou du streaming. La qualité de l’inférence en production se mesure précisément à cet équilibre.

Sources principales