Accueil / Articles

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.

Éditeur : REACTAPP.TOP5 min de lecture1194 mots

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 :

  1. Étape d’avancement du token. Une passe en avant à travers chaque séquence active génère exactement un nouveau token par séquence.
  2. Élimination instantanée. Les séquences qui ont généré <eos> sont renvoyées et libérées de la VRAM.
  3. 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.
  • Décodage est limité par la bande passante mémoire : les étapes de traitement d’un seul token, prenant des dizaines de millisecondes, influencent fortement l’expérience interactive.
  • 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
    Grouper statique Moyen (souvent inférieur à 40 % en raison des espaces inutiles) Haut — bloqué par de longues séquences Bas jusqu’au remplacement du groupe Grouper continu uniquement Haut (souvent supérieur à 85 % avec des emplacements dynamiques) Moyen Augmente fortement lors de l’arrivée de grandes quantités de données préremplissées Grouper continu + préremplissage en blocs Optimal (souvent supérieur à 90 %, saturation atteinte) Lisse et stable Durée par token minimale et prévisible

    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.pipeline est 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-prefill limitent le nombre de tokens par itération afin que des prompts longs ou des contextes RAG ne ralentissent pas les utilisateurs en streaming.
  • Préférez les valeurs p95/p99 plutôt que les moyennes. Le nombre moyen de tokens par seconde peut sembler correct, tandis que le planification préalable perturbe encore les sessions individuelles. Mesurez séparément le temps nécessaire pour obtenir le premier token et la latence entre tokens, afin que le temps d’attente dans la file d’attente et l’état de décodage ne soient pas fusionnés en un seul chiffre trompeur.
  • Gérez la mémoire KV en tenant compte du planificateur. Le lotage continu n’est utile que si les séquences inachevées peuvent être supprimées proprement et que de nouvelles peuvent être ajoutées ; une VRAM surchargée transforme l’admission en situation de chaos.
  • 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.