Grouper les données de manière continue et préremplir par blocs dans vLLM et SGLang
Les lots statiques de LLM gaspillent les GPU en raison du remplissage inutile. Un planification au niveau des itérations et un préremplissage par blocs permettent de maintenir des flux de décodage fluides tout en assurant que les nouveaux prompts progressent également.
Sources: Continuous Batching & Chunked Prefill: How vLLM and SGLang Beat Static LLM Inference
Le lotage classique au niveau des requêtes laisse une grande partie des cycles GPU consacrée à l’ajout de données inutiles lorsque la longueur des sorties des modèles de langage varie considérablement. Le lotage continu au niveau des itérations — popularisé en production par vLLM et SGLang à la suite de la ligne de recherche Orca — permet d’ajouter ou de supprimer des séquences à chaque étape de traitement des tokens. Le préremplissage en blocs empêche alors que l’ingestion de prompts longs ne bloque les flux déjà en cours de décodage. Les équipes de plateforme qui continuent d’utiliser des pipelines statiques Hugging Face rencontrent ces deux problèmes dès que le trafic mélange des conversations courtes avec des générations longues.
1. Le piège du lotage statique
Les serveurs de vision et ceux traitant des données tabulaires effectuent un lotage de tenseurs fixes — seize images de la forme [16, 3, 224, 224], une passe en avant, seize réponses ensemble. En appliquant ce modèle aux grands modèles de langage, le débit s’effondre pour deux raisons structurelles :
- Longueurs de sortie imprévisibles. Une requête peut être traitée en quelques centaines de millisecondes ; une autre, dans le même lot, peut générer des centaines de tokens pendant de nombreuses secondes.
- Blocage en tête de file. Les lots statiques ne peuvent pas renvoyer rapidement la réponse courte. Leur emplacement reste occupé par des tokens de remplissage tandis que la GPU attend le processus le plus lent.
Dans le lotage statique, la requête la plus lente et la plus longue dicte le rythme pour toutes les autres. Le lotage continu traite le lot comme une piste d’envol : les séquences terminées quittent immédiatement la file et celles qui attendent prennent leur place. La charge liée aux tokens de remplissage n’est pas une simple inefficacité — elle gaspille régulièrement la majeure partie des FLOPs théoriques pour des tâches inutiles lorsque les distributions de longueur présentent une queue lourde, ce qui est le cas par défaut pour les assistants de conversation et de codage.
2. Comment fonctionne le lotage continu (planification au niveau itératif)
Au lieu de planifier toute une demande du début à la fin, le moteur planifie une itération de token :
- Étape d’avancement du token. Une passe en avant à travers chaque séquence active génère exactement un nouveau token par séquence.
- Élimination instantanée. Les séquences qui ont généré
<eos>sont renvoyées et libérées de la VRAM. - Admission instantanée. La capacité ainsi libérée est attribuée à une demande en file d’attente sans attendre le reste du lot précédent.
Travailler à l’échelle d’itération permet aux cœurs de se concentrer sur des tâches réelles plutôt que sur le remplissage par zéros, ce qui offre généralement des gains de productivité importants avec une latence médiane comparable. L’unité de travail du planificateur passe d’une « complétion totale » à « une étape pour chaque séquence en cours », c’est pourquoi des gestionnaires de mémoire tels que paged attention s’associent naturellement au lotage continu : les emplacements apparaissent et disparaissent sans devoir restructurer un tenseur statique rigide.
3. Le problème du préremplissage et le préremplissage par blocs
La décodage continu soulève un nouveau conflit entre préremplissage et décodage :
- Prefill est limité par les calculs : le modèle traite l’intégralité du prompt pour construire la mémoire cache KV. Un prompt de plusieurs milliers de tokens peut nécessiter des centaines de millisecondes de calculs intensifs sur des matrices.
Un planificateur naïf qui pause trente décodages en cours pour traiter une seule demande volumineuse provoque des interruptions dans le flux — le classique problème de latence lié au préremplissage. Le préremplissage en blocs divise cette demande en parties (par exemple en segments de 512 tokens) et mélange ces blocs avec les étapes de décodage, permettant ainsi au traitement de se répartir sur plusieurs itérations sans pics brutaux de latence. Les utilisateurs déjà en train de recevoir des données continuent d’obtenir des tokens ; les nouveaux arrivants progressent également dans l’ingestion de la demande. La même logique s’applique lorsque RAG insère des contextes longs au cours d’une session : sans segmentation en blocs, un seul chargement de données peut paralyser des dizaines d’autres processus.
4. Comparaison des performances et du débit
| Architecture de service | Utilisation du GPU | Latence finale (p99) | Jitter entre tokens |
|---|
Ces fourchettes sont indicatives ; les valeurs exactes dépendent de la taille du modèle, du matériel et de la composition des prompts. Ce classement qualitatif se retrouve dans la plupart des descriptions publiques : le grouper statique est moins efficace en termes d’utilisation, le grouper continu seul réduit les gaspillages de décodage mais pas les interférences dues au préremplissage, tandis que le préremplissage en blocs permet un flux continu même sous charge mixte.
5. Simulation d’un planificateur au niveau des itérations en Python
Le schéma suivant montre l’admission des requêtes, les préremplissages par blocs et l’éviction à chaque étape. Il s’agit d’un modèle pédagogique, non d’un environnement de production, mais il rend la boucle de contrôle explicite :
from dataclasses import dataclass
from typing import List
@dataclass
class LLMRequest:
request_id: str
remaining_prefill: int
tokens_to_generate: int
tokens_generated: int = 0
@property
def is_finished(self) -> bool:
return self.remaining_prefill == 0 and self.tokens_generated >= self.tokens_to_generate
class ContinuousBatchEngine:
"""
Simulates iteration-level scheduling with dynamic slot allocation
and chunked prefill budgeting.
"""
def __init__(self, max_batch_slots: int = 3, prefill_chunk_size: int = 256):
self.max_slots = max_batch_slots
self.chunk_size = prefill_chunk_size
self.waiting_queue: List[LLMRequest] = []
self.running_slots: List[LLMRequest] = []
def submit_request(self, request: LLMRequest):
self.waiting_queue.append(request)
def step_iteration(self, step_number: int):
print(f"\n--- [ITERATION STEP {step_number}] ---")
# 1. Admit waiting requests if open slots exist
while len(self.running_slots) < self.max_slots and self.waiting_queue:
new_req = self.waiting_queue.pop(0)
self.running_slots.append(new_req)
print(f" [ADMITTED] {new_req.request_id} entered running batch.")
# 2. Execute one token step across running batch
for req in self.running_slots:
if req.remaining_prefill > 0:
# Process a chunk of prefill
chunk = min(req.remaining_prefill, self.chunk_size)
req.remaining_prefill -= chunk
print(f" [PREFILL CHUNK] {req.request_id}: Processed {chunk} prompt tokens (Remaining: {req.remaining_prefill})")
else:
# Process single decode token
req.tokens_generated += 1
print(f" [DECODE TOKEN] {req.request_id}: Emitted token #{req.tokens_generated}/{req.tokens_to_generate}")
# 3. Evict finished requests immediately
finished = [r for r in self.running_slots if r.is_finished]
for req in finished:
self.running_slots.remove(req)
print(f" [COMPLETED & EVICTED] {req.request_id} returned to client.")
if __name__ == "__main__":
engine = ContinuousBatchEngine(max_batch_slots=2, prefill_chunk_size=200)
# Simulate 3 incoming requests of varying prompt & response sizes
engine.submit_request(LLMRequest("Req_A (Short)", remaining_prefill=0, tokens_to_generate=2))
engine.submit_request(LLMRequest("Req_B (Long)", remaining_prefill=0, tokens_to_generate=4))
engine.submit_request(LLMRequest("Req_C (Large Prompt)", remaining_prefill=400, tokens_to_generate=2))
# Run 5 continuous iterations
for step in range(1, 6):
engine.step_iteration(step)
6. Leçons pratiques pour les ingénieurs de plateformes d’IA
- Ne déployez pas de pipelines naïfs de Hugging Face en tant que serveurs de production. Le lotage statique par défaut de
transformers.pipelineest un modèle adapté aux laboratoires. Enveloppez les modèles open-source dans des environnements de lotage continu tels que vLLM ou SGLang. - Activez le préremplissage par blocs pour des charges de chat mixtes. Dans vLLM, des paramètres tels que
--enable-chunked-prefilllimitent le nombre de tokens par itération afin que des prompts longs ou des contextes RAG ne ralentissent pas les utilisateurs en streaming.
Ensemble, le lotage continu et la précharge par blocs constituent l’approche par défaut pour un service moderne à poids ouvert — et non des paramètres de réglage optionnels après échec d’un design statique en production.