Transformers documentation
Architecture de lot continu
Architecture de lot continu
Le batch traditionnel traite un groupe fixe de requêtes ensemble et attend que la plus lente soit terminée avant de démarrer le groupe suivant. Cela entraîne une période d'inactivité du GPU entre les batches.
Avec le lotissement continu, à chaque étape de génération, le planificateur vérifie les requêtes terminées et les remplace immédiatement par celles en attente. Les requêtes courtes sont éjectées dès qu’elles sont achevées, et le GPU reste occupé en permanence. Le résultat est un débit nettement supérieur et une latence moyenne plus faible.
Cycle de vie d'une requête
Une requête traverse quatre états, de la soumission à l'achèvement.
WAIT IN QUEUE LOAD PROMPT INTO KV STREAM OUTPUT DONE
┌────────────────┐ ┌─────────────────────┐ ┌─────────────────┐ ┌────────┐
│ PENDING │───▶│ PREFILLING │───▶│ DECODING │───▶│FINISHED│
│ ○ ○ ○ · · · │ │ ████████░░░░░░░░░░ │ │ → → → → · · │ │ ✓ │
└────────────────┘ └─────────────────────┘ └─────────────────┘ └────────┘
others ahead prompt chunks +1 token/step- En attente — la demande est en file d'attente et en cours de planification.
- Préremplissage — les jetons du prompt sont traités dans un passage avant.
- Décodage — les jetons de sortie sont générés un par un.
- Terminé — la génération est complète et le résultat est disponible.
Une requête passe de l'état « en attente » à l'état « préremplissage » lorsque le planificateur détecte un budget de jetons et un espace de cache suffisants pour l'admettre. Si le prompt est trop long pour tenir dans une seule étape, le planificateur le répartit sur plusieurs passes avant (chunk prefill).
Préremplissage par blocs
Le traitement d'un long prompt en une seule étape est coûteux car il bloque la génération des autres requêtes. Le préremplissage par morceaux (chunked prefill) résout ce problème en répartissant les grands préremplissages sur plusieurs étapes.
Blocking (long prefill blocks the batch) Step: 1 2 3 4 Req A [▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓] ← one huge prefill Req B ················ wait ················ Req C ················ wait ················ Chunked (same prompt split and interleave with decode) Step: 1 2 3 4 Req A [▓▓ pre] [▓▓ pre] [▓▓ pre] [→ dec] Req B [→ dec] [→ dec] [▓▓ pre] [→ dec] Req C [→ dec] · idle · [→ dec] · idle ·
Lorsque le prompt d’une nouvelle requête dépasse le budget de jetons disponible (défini par max_batch_tokens dans ContinuousBatchingConfig), le planificateur traite autant de jetons que possible et met le reste en attente. Aux étapes suivantes, il reprend là où il s’était arrêté, en alternant le travail de préremplissage avec les étapes de décodage en cours. Cela maintient le lot productif et réduit le délai avant le premier jeton pour les autres requêtes.
Planificateur
Le planificateur détermine quelles requêtes rejoignent chaque passe avant en fonction de deux budgets et d'une limite de requêtes.
- Budget de jetons — nombre maximal de jetons de requête traités lors d'un seul passage avant, défini par
max_batch_tokensdans ContinuousBatchingConfig. - Budget de cache — le nombre total de pages KV qui peuvent être lues en un seul passage, limité par la taille totale du cache.
- Limite de requêtes — nombre maximal de requêtes dans un seul passage avant, défini par
max_requests_per_batch. Le tenseur des logits est proportionnel à la taille du vocabulaire, cette limite borne donc la mémoire de pointe pour les modèles à grand vocabulaire.
FIFO
FIFOScheduler est le planificateur par défaut. Il remplit le lot dans l'ordre de priorité. Les requêtes de décodage actives sont traitées en premier, suivies des requêtes de préremplissage actives, puis des nouvelles requêtes en attente dans l'ordre d'arrivée. Cela maximise le débit en traitant les requêtes existantes avant d'en accepter de nouvelles.
PrefillFirst
PrefillFirstScheduler termine les opérations de préremplissage par morceaux avant de reprendre le décodage. Cela réduit la fragmentation pour les charges de travail dominées par de longs prompts.
Définissez le planificateur dans ContinuousBatchingConfig.
cb_config = ContinuousBatchingConfig(scheduler_type="prefill_first")Gestion de la mémoire
Le cache KV est paginé afin que des requêtes de longueurs différentes puissent partager un pool de mémoire fixe sans fragmentation. Un pool de mémoire partagé, implémenté sous forme de CachePool, fournit ces blocs à chaque allocateur de type d'attention, qui les divise en blocs et en pages.
Shared pool (first two chunks reserved for padding / never allocated)
chunk N [ ======================================== ]
| |
full attn: [ FULL BLOCK 0 | FULL BLOCK 1 | … ]
[ page L0 | page L0 | … ] (one page per full layer)
sliding: [ ----------- SLIDING BLOCK 0 ----------- ]
[ page L1 | page L2 | page L3 | … ] (one page per sliding layer)
page = page_size tokens of KV for one layer
block = one page per layer of that attention type
chunk = pool unit sized so full and sliding blocks tile the same bytesLe batchage continu ne prend en charge que deux types d'attention : full_attention (FullAttentionCacheAllocator) et sliding_attention (SlidingAttentionCacheAllocator). Les modèles hybrides reçoivent un allocateur de chaque type. Le type d'attention provient du modèle (layer_types / sliding_window), et non de ContinuousBatchingConfig.
Les couches glissantes ne conservent qu'une fenêtre fixe de jetons récents en recyclant un petit ensemble de pages. Elles ne prennent pas en charge les tables de blocs de noyau ni le partage de blocs.
Le cache réserve deux blocs en avance par rapport aux blocs de données pour les lectures et écritures de bourrage que les requêtes n'utilisent jamais. Ces blocs ne sont jamais alloués aux requêtes.
Dimensionnement du cache
Le gestionnaire déduit au démarrage le nombre de blocs qui tiennent dans la mémoire GPU libre. Il tient compte des tenseurs KV, des masques d’attention, des activations et des indices de gestion, puis dimensionne le pool pour qu’il tienne dans max_memory_percent de la mémoire disponible. S’il n’est pas défini, il prend par défaut une valeur en fonction du matériel et des paramètres de lotissement continu. MPS réserve moins d’espace pour le cache, où le CPU et le GPU partagent la mémoire, afin d’éviter l’échange sur disque. num_blocks exprime la capacité en blocs entiers du modèle de page_size jetons lorsque vous le définissez explicitement.
Vous pouvez épingler les valeurs dans ContinuousBatchingConfig.
cb_config = ContinuousBatchingConfig(
page_size=256,
num_blocks=4096,
max_batch_tokens=512,
max_memory_percent=0.8,
)Admission
Avant qu'une requête ne rejoigne le lot, le planificateur vérifie qu'une capacité libre suffisante existe pour chaque allouateur de type d'attention. Si un allouateur ne dispose pas de ressources suffisantes, la requête est rejetée et aucune allocation n'est effectuée.
Pour éviter que le cache ne se remplisse au point que le déchargement soit la seule option, le planificateur applique une safety_margin. Dès que la capacité libre du cache passe sous le seuil de safety_margin, les nouveaux préremplissages sont mis en attente et seuls les décodages actifs se poursuivent. Le planificateur FIFO utilise par défaut 0.15 (15 % de la capacité maintenue en réserve), tandis que prefill_first utilise par défaut 0.0. Définissez safety_margin dans ContinuousBatchingConfig pour l’ajuster, où 0 désactive la marge.
Mise en cache des préfixes
Lorsque deux requêtes partagent un préfixe de prompt, elles peuvent partager les blocs qui contiennent la KV pour ce préfixe. Chaque bloc terminé est haché en fonction de son contenu. Une requête ultérieure avec un préfixe correspondant réutilise le bloc et saute la préremplissage pour ces jetons. Les blocs partagés sont comptés par référence et ne retournent au pool libre que lorsque toutes les requêtes qui les utilisent ont terminé.
allow_block_sharing est défini par défaut sur True. Le partage de préfixes n’est actif que si tous les allocateurs prennent en charge le partage, si bien que seuls les modèles utilisant exclusivement l’attention complète en bénéficient. Les modèles purement glissants et hybrides le désactivent. Définissez allow_block_sharing=False pour les charges de travail impliquant des prompts courts et des générations longues, où le suivi des allocations pèse plus lourd que les gains obtenus.
Éviction
Les blocs libérés ne sont pas effacés et restent « initialisés » avec leur contenu et leur hachage intacts, afin qu’une correspondance de préfixe ultérieure puisse les réutiliser. Lorsque le pool non initialisé s’épuise, le gestionnaire rétrograde paresseusement le bloc initialisé le plus récent vers l’état non initialisé, en supprimant son hachage. Le cache de préfixe est une couche de type « best-effort » que l’allocateur abandonne pour continuer à traiter les nouvelles requêtes.
Réinitialisation douce
Un réinitialisation douce est le chemin de repli pour le offloading. Lorsque le gestionnaire ne peut pas copier le cache KV d’une requête vers le pool d’échange CPU, car l’offloading CPU est désactivé ou le pool est plein, il effectue une réinitialisation douce de la requête à la place. Il ajoute les tokens générés par la requête à son prompt d’origine, libère ses blocs de cache et la réintègre dans la file d’attente. La section Offloading détaille quelles requêtes sont sélectionnées.
Lorsque le cache dispose à nouveau d’espace, la requête reprend là où elle s’était arrêtée, son historique de génération étant encodé dans le prompt.
Graphes CUDA
Chaque étape de génération implique une surcharge CPU, de l’assemblage du lot à la distribution des noyaux GPU et à la lecture des résultats. Les graphes CUDA éliminent cette surcharge en enregistrant une seule fois la séquence d’exécution GPU complète, puis en la rejouant pour des lots aux formes correspondantes.
Comme les formes des lots changent à chaque étape, le système de groupement continu gère cela par bourrage et mise en cache.
- Les longueurs de requête sont remplies jusqu'au multiple le plus proche de
q_padding_interval_size. - Les longueurs de KV sont remplies jusqu'au multiple le plus proche de
kv_padding_interval_size. - Les graphes enregistrés sont stockés dans un cache.
Lorsque la forme rembourrée d’un lot correspond à un graphe mis en cache, le graphe est rejoué sans aucune dispatch CPU. Les nouvelles formes déclenchent une nouvelle capture de graphe.
Les graphes CUDA nécessitent un flux de contrôle statique et sont incompatibles avec les masques d’attention. Ils sont détectés automatiquement par défaut et désactivés lorsque les conditions ne sont pas remplies.
Batching asynchrone
Le lot asynchrone utilise deux paires de tampons E/S et deux flux CUDA pour superposer le travail CPU et GPU.
Sequential CPU ── [prep N] ·········idle········· [prep N+1] ·········idle········· GPU ·········idle········· [compute N] ·········idle········· [compute N+1] Async CPU ── [prep N] [prep N+1] [prep N+2] ── GPU ── ········ [compute N] [compute N+1] ──
Pendant que le GPU calcule le lot N, le CPU prépare le lot N+1. Toutefois, superposer les deux nécessite environ le double de VRAM.
Le lot asynchrone nécessite que les graphes CUDA soient actifs, car la lecture des graphes fournit les adresses de tenseurs stables nécessaires pour que le chevauchement des flux soit correct.
Offloading
Les requêtes qui génèrent des sorties longues peuvent croître jusqu’à ce que le cache KV n’ait plus d’espace pour allouer le bloc suivant. Plutôt que de planter, le gestionnaire renvoie les requêtes actives vers la file d’attente afin de libérer des blocs pour les requêtes restantes.
L’offloading est piloté par la demande. Le planificateur signale les requêtes stalled, c’est-à-dire les requêtes actives qui n’ont pas réussi à allouer les blocs dont elles ont besoin, ainsi que le nombre de blocs requis par chacune. Le gestionnaire décharge le nombre minimal de requêtes nécessaire pour libérer suffisamment de blocs afin de couvrir cette demande. Il décharge d’abord les plus récentes, de sorte que les requêtes déjà planifiées dans le lot courant conservent leur cache, et la dernière requête active restante n’est jamais déchargée.
Deux situations déclenchent le déchargement.
- Un lot est planifié, mais certaines requêtes actives sont bloquées. Le gestionnaire décharge suffisamment de requêtes bloquées pour que le lot suivant puisse allouer des blocs pour le reste.
- Le cache est plein et aucun lot ne peut être planifié. Le gestionnaire décharge les requêtes et réessaie la planification dans une boucle au sein de la même étape, jusqu'à ce qu'un lot tienne ou qu'aucune requête ne puisse être déchargée. Il n'attend plus le prochain passage avant pour se rétablir.
Une requête déchargée suit l’un des deux chemins suivants. Lorsque cpu_offload_space est supérieur à 0.0, le gestionnaire copie les blocs de cache KV de la requête vers un tampon CPU épinglé préalloué, en regroupant toutes les requêtes qui tiennent dans le pool d’échange en un seul transfert. Une fois que de l’espace de cache GPU est disponible, le gestionnaire recopie les blocs sur le GPU et reprend la requête sans recalculer son prompt et les jetons générés. Les requêtes qui ne tiennent pas dans le pool, ou toutes les requêtes déchargées lorsque le déchargement CPU est désactivé, basculent sur un soft reset.
Étapes suivantes
- L’article de blog Continuous batching aborde la mise en cache KV, le préremplissage par morceaux et la planification dynamique, avec des chiffres de référence de performance.
- Pour des exemples d'utilisation, consultez la documentation Continuous batching.