vLLM BlogPublication de la source

Optimisations de GLM-5.3, partie 2 : PCP, DCP et Hybrid HiSparse sur 16 H200

TL;DR : Les longs contextes agentiques confrontent GLM-5.3 à deux limites sur les H200 : la mémoire du cache KV et la vitesse de préremplissage. Nous traitons les deux sur 16 H200, répartis en un nœud de préremplissage et un nœud de décodage :

TL;DR : Les longs contextes agentiques confrontent GLM-5.3 à deux limites sur les H200 : la mémoire du cache KV et la vitesse de préremplissage. Nous traitons les deux sur 16 H200, répartis en un nœud de préremplissage et un nœud de décodage :

  • Le parallélisme de contexte de préremplissage (PCP) divise chaque prompt entre les GPU par token plutôt que par tête, ce qui convient bien mieux à l'attention clairsemée de GLM-5.3 que le parallélisme de tenseurs.
  • Le parallélisme de contexte de décodage (DCP) fragmente le cache KV du moteur de préremplissage par token, afin qu'un contexte de 1M tienne sur le nœud de préremplissage.
  • Hybrid HiSparse permet au décodage de conserver le KV dans la mémoire de l'hôte, afin qu'une requête de 1M de tokens tienne.
  • Sur AgentX, un préremplissage PCP8 × DCP8 avec un décodage Hybrid HiSparse offre 1,7–3,2× la vitesse par utilisateur d'un déploiement en parallélisme de données à même débit, et c'est la seule configuration que nous ayons testée capable de servir des contextes de 1M de tokens.

Servir de longs contextes dans des déploiements limités en mémoire

Les charges de travail agentiques ont de longs contextes qui s'allongent à chaque tour, ce qui pèse à la fois sur la mémoire GPU et sur le préremplissage. Sur les H200, les poids de GLM-5.3 occupent la majeure partie de la mémoire de chaque GPU, laissant peu de place au cache KV de ces longs contextes. Et comme chaque tour ajoute davantage de sorties d'outils et de contenus de fichiers à préremplir, sous charge les moteurs de préremplissage peuvent devenir le goulot d'étranglement de tout le déploiement.

La partie 1 montrait comment Hybrid HiSparse permet à de longs contextes de tenir dans un déploiement agrégé sur un seul nœud 8× H200. La désagrégation préremplissage/décodage demande davantage. Sur le nœud de préremplissage, nous utilisons PCP pour accélérer les longs prompts en parallélisant sur les tokens du prompt. Avec MLA, cependant, chaque rang PCP se retrouve détentrice d'une copie complète du cache KV ; nous le combinons donc avec DCP pour supprimer cette réplication. Sur le nœud de décodage, Hybrid HiSparse permet une plus haute concurrence sur de longs contextes. Le nouveau noyau modulaire de vLLM (Model Runner V2) rend la composition de tout cela bien plus facile.

Pourquoi pas TP ou DP ?

Deux manières standard d'exécuter le préremplissage de GLM-5.3 sur un nœud 8× H200 sont DP8 et TP8 :

  • DP8 affecte chaque prompt à un seul GPU, si bien que son attention ne peut pas utiliser les sept autres. Cela limite le temps jusqu'au premier token (TTFT) pour les longs prompts, et le cache KV du prompt doit tenir dans la mémoire de ce GPU.
  • TP8 partitionne les têtes d'attention entre les GPU. Comme le KV latent de MLA est partagé par les 64 têtes, chaque GPU stocke le cache complet et le lit pour ne servir que 8 têtes. L'indexeur, qui note chaque token antérieur pour sélectionner les 2,048 que le MLA clairsemé consulte, n'est pas partitionné du tout, si bien que son coût quadratique est répété sur chaque GPU.

Aucun de ces schémas ne répartit le travail d'attention d'un long prompt sur les huit GPU sans aussi conserver l'intégralité de son KV sur chaque GPU qui le sert. Les deux sections suivantes traitent chaque moitié. PCP répartit les requêtes d'un prompt entre les GPU, et DCP répartit son KV.

PCP : répartir les tokens, pas les têtes

Dans notre moteur de préremplissage 8× H200, PCP divise chaque prompt entre les huit GPU par token : chaque GPU calcule les requêtes pour un huitième du prompt, avec les 64 têtes. Une répartition naïve serait déséquilibrée. Sous attention causale, un token consulte chaque token qui le précède, donc le GPU détenant la fin du prompt ferait bien plus de travail que celui détenant le début. PCP coupe à la place le prompt en 16 morceaux et les associe : le GPU prend un morceau au début et un morceau à la fin, de sorte que chaque GPU reçoit le même mélange de tokens bon marché et coûteux (la figure 3 montre cela comme un motif en V sur les huit GPU). C'est le sharding équilibré en charge issu de Context Parallelism for Scalable Million-Token Inference. Enfin, le nouveau KV de chaque GPU est all-gathered, afin que chaque requête puisse toujours consulter le contexte complet.

Figure 1. Un préremplissage sur 8 GPU sous TP8 et PCP8, ombré selon le GPU qui détient chaque partie. Les barres montrent l'intensité arithmétique du MLA clairsemé ; la ligne marque le point où le H200 devient limité par le calcul en BF16.

La figure 1 compare les deux répartitions. PCP résout les deux problèmes. Chaque GPU n'indexe que ses propres tokens, si bien que l'indexeur n'est plus répliqué sur chaque GPU et que chaque GPU en fait un huitième. Et chaque ligne de KV lue en mémoire sert désormais les 64 têtes au lieu de 8, faisant passer le MLA clairsemé d'environ 26 à environ 191 FLOPs par octet déplacé,1 proche du point où le H200 devient limité par le calcul en BF16.2

Ne pas répliquer l'indexeur compte surtout pour les longs prefills. Le coût de l'indexeur croît quadratiquement avec la longueur du prompt, tandis que celui du MLA sparse ne croît que linéairement, car chaque requête (query) porte son attention sur un nombre fixe de 2,048 tokens. En comptant les FLOPs pour un prefill depuis zéro, l'indexeur dépasse le MLA sparse sous TP8 dès que le prompt atteint environ 32K tokens ; sous PCP8, où aucun GPU ne le réplique, le point de bascule se déplace à environ 256K.

Ensemble, ces caractéristiques font du PCP un excellent choix pour le prefill de longs prompts. Mais le PCP a tout de même deux coûts. Chaque GPU conserve tous les poids d'attention, tandis que les experts MoE restent répartis sur les huit GPU avec EP8. Et parce que le nouveau KV est all-gathered à chaque couche, chaque GPU finit par stocker l'intégralité du cache KV. C'est ce cache répliqué, et non le calcul, qui limite la longueur du contexte, et c'est ce que le DCP adresse.

DCP : sharder aussi le cache KV

Une requête GLM-5.3 de 1M de tokens nécessite 53,4 Gio de cache KV en FP8,3 mais chaque GPU ne dispose que d'environ 15 Gio d'espace. Le parallélisme de contexte de décodage (DCP) résout ce problème en fractionnant le cache lui-même : les tokens sont distribués en round-robin sur les huit GPU, si bien que chaque GPU stocke un token sur huit, et la même requête ne nécessite plus que 6,7 Gio (=53,4/8) par GPU. La figure 2 compare cette approche à DP8 et TP8.

Figure 2. TP réplique le KV latent de MLA, donc huit GPU n'apportent aucune capacité supplémentaire. DCP le divise par huit.

Remarque : PCP fractionne les requêtes ; DCP fractionne le KV. Ces noms sont historiques. DCP a été conçu pour le décodage, où chaque requête n'a qu'un seul token de requête, si bien que le cache KV est la seule chose qu'il vaut la peine de fractionner. PCP a été conçu pour le préremplissage (prefill), où les nombreux tokens de requête du prompt constituent la charge de travail. Notre configuration exécute les deux dans le moteur de prefill.

À la suite de ce sharding, la majeure partie du KV dont les requêtes de chaque GPU ont besoin réside désormais sur d'autres GPU. En décodage, DCP envoie les requêtes vers le KV : chaque GPU effectue l'attention sur son propre shard, et les résultats partiels sont fusionnés avec leur log-sum-exp. Dans ce moteur de prefill, on fait l'inverse : à chaque couche, chaque GPU regroupe (all-gather) les shards dans une copie temporaire du contexte complet, effectue l'attention pour ses propres requêtes, puis libère la copie. Seul le cache persistant est divisé par huit.

La figure 3 réunit le tout. Parcourez une couche de PCP8 × DCP8 pour voir quel KV chaque GPU stocke, quelles requêtes il calcule, et ce qui traverse l'interconnexion. Passez à DP8, TP8 ou PCP8 pour comparer. Notez que les deux dispositions ne coïncident pas : le GPU stocke un token sur huit mais calcule les requêtes des chunks et , c'est pourquoi chaque couche nécessite l'all-gather.

Cet all-gather n'est pas gratuit : à chaque couche, il renvoie l'intégralité du contexte en cache, si bien que son coût croît avec la longueur du contexte. La figure 4 l'isole en exécutant le même chunk de prefill de 2K tokens sur un nœud PCP8 avec et sans DCP8, à contexte en cache égal. DCP8 ajoute environ 6 % par étape jusqu'à 64K tokens de contexte, montant à 15 % à 248K. C'est le prix d'un pool KV 8× plus grand. Cette surcharge pourrait être réduite en chevauchant l'all-gather de chaque couche avec le calcul de la couche précédente, une optimisation sur laquelle nous travaillons actuellement.

Figure 4. Surcharge par étape de DCP8 pour un chunk de 2K tokens sur un moteur PCP8 de 8× H200. La bande couvre les chunks de 1K, 4K et 8K.

Au-delà d'environ 250K tokens, PCP8 seul ne peut plus contenir le cache KV ; là, DCP n'est pas une surcharge mais ce qui rend les longs prompts possibles. Ensemble, PCP8 × DCP8 donne au moteur de prefill à la fois de la vitesse et de la capacité.

Le déploiement

Nous le servons en tant que déploiement désagrégé prefill/decode (P/D) sur deux nœuds 8× H200. Les deux moteurs utilisent un cache KV FP8 et un décodage spéculatif MTP avec trois draft tokens (MTP3) :

  • Prefill : un moteur PCP8 × DCP8 × EP8, avec 512 Gio de déchargement KV sur CPU afin que les tours ultérieurs réutilisent leur préfixe.
  • Decode : huit rangs DP avec EP8 et Hybrid HiSparse issus de Part 1 (pool hôte de 48 Gio + 16 Gio de déchargement par rang), si bien que la concurrence du décodage est limitée par la mémoire hôte plutôt que par la HBM.

Un routeur llm-d sensible aux préfixes envoie chaque requête au moteur de prefill, le connecteur NIXL déplace son cache KV vers un rang de décodage, et le rang de décodage renvoie les tokens en streaming.

Nous comparons le prefill PCP8 × DCP8 au prefill DP8 (huit rangs DP avec EP8 et 64 Gio de déchargement KV CPU chacun, soit les mêmes 512 Gio au total), et Hybrid HiSparse au simple déchargement KV CPU sur le nœud de décodage, avec les mêmes 512 Gio de mémoire hôte dans les deux cas (8 × 48 Gio de pool HiSparse + 8 × 16 Gio de déchargement pour HiSparse). Chaque configuration utilise les mêmes 16 GPU, la même mémoire hôte et la même version de vLLM.

Résultats

Nous avons exécuté le benchmark AgentX de SemiAnalysis : des traces de Claude Code rejouées avec réutilisation de préfixe, un serveur neuf par niveau de concurrence, des exécutions d'une heure, et MTP3 avec une longueur d'acceptation synthétique fixée à 2.99, afin que chaque configuration bénéficie du même accélération spéculative. Le débit est le débit logique tok/s par GPU (y compris les tokens en cache) : tokens d'entrée plus sortie, en comptant l'entrée en cache de préfixe, sur l'ensemble des 16 GPU. L'interactivité est le P90 d'AgentX des tokens de sortie E2E normalisés par utilisateur en tokens/s, l'axe x de la figure 5.

Figure 5. Un point par niveau de concurrence, chacun sur un déploiement neuf, à deux limites de contexte : c8–c64 à 142K et c8–c32 à 1M. Les étiquettes indiquent la concurrence pour le prefill PCP8 × DCP8 avec décodage HiSparse ; les autres configurations exécutent les mêmes concurrences, de droite à gauche. Les points creux sont hors de la frontière de Pareto de leur propre configuration.

Jusqu'à c32, le déploiement est principalement limité par le prefill, si bien que le préremplisseur fait la plus grande différence. Avec un décodage HiSparse dans les deux configurations, le prefill PCP8 × DCP8 offre 1,7–3,2× l'interactivité de DP8 à débit égal et réduit le TTFT médian de 1,5–5,2 s à 0,8–0,9 s. Le décodeur fait peu de différence à cette charge : à c8 et c16, les deux configurations PCP8 × DCP8 sont à quelques pour cent l'une de l'autre, et le décodage par simple déchargement est légèrement en tête à c8.

À partir de c32, le déploiement devient davantage limité par le décodage, et le décodage HiSparse prend l'avantage. À c64, il double presque le débit, atteignant 8,340 tok/s logiques par GPU à 29.6 tok/s/utilisateur, tandis que toutes les autres configurations chutent à 1–2 tok/s/utilisateur.

Avec un contexte de 1M, le déploiement a besoin des deux. Un rang de préfill DP8 ne peut pas contenir une seule requête de 1M tokens, et un rang de decode en offload seul ne le peut pas non plus.

Faire en sorte que tout se compose

Une grande partie du travail difficile dans vLLM consiste à faire en sorte que des fonctionnalités comme celles-ci se composent entre elles. Chacune modifie la manière dont le KV cache est disposé, ordonnancé ou déplacé, et chaque combinaison doit rester correcte et rapide.

Pour le P/D, le KV doit changer de mise en page entre les deux moteurs. Le moteur de prefill sharde le KV de chaque requête par token sur huit GPU. Chaque rang de décode a besoin de tout le KV, y compris celui de la couche MTP, dans sa propre mise en page paginée. Le connecteur NIXL effectue ce réassemblage entre les deux nœuds, et la réutilisation de préfixe depuis le déchargement CPU doit continuer de fonctionner en parallèle.

Chaque configuration ici s'exécute sur Model Runner V2. Il garde l'état spécifique au modèle hors du chemin d'exécution commun et exécute le décodage spéculatif sans synchronisations CPU–GPU. Hybrid HiSparse est construit dessus.

Essayez-le

Tout s'exécute sur vLLM v0.32.0 ou version ultérieure (l' vllm/vllm-openai:v0.32.0 image). Les résultats ci-dessus ont été mesurés sur vLLM main au 9e38b4111a. Le gist de reproduction épingle 2537629eb2. Les indicateurs clés du moteur de prefill :

--prefill-context-parallel-size 8 \
--decode-context-parallel-size 8 \
--dcp-comm-backend ag_rs \
--enable-expert-parallel \
--attention-config '{"sparse_mla_force_mqa":true}'

Sans sparse_mla_force_mqa, les lots de prefill dont les invites tiennent dans le top-k de 2,048 tokens empruntent le chemin MHA dense. L'indicateur les maintient aussi sur le noyau sparse-MLA (MQA), de sorte que chaque ligne de KV sert toutes les têtes.

Remerciements

Ce travail a été réalisé par Roberto L. Castro (Red Hat), Simon Veitner (Red Hat), Matthew Bonanni (Red Hat), Lucas Wilkinson (Red Hat) et Nicolò Lucchesi (Mistral). Nous remercions Maroon Ayoub (Red Hat) et Robert Shaw (Red Hat) d'avoir soumis le déploiement à des tests de charge, signalé des erreurs et corrigé des bugs.

Notes

Notes de bas de page

  1. Par requête, le sparse MLA lit 2,048 lignes de KV sélectionnées de 660 octets chacune (656 octets de KV FP8 dans le format fp8_ds_mla de FlashMLA, plus un index de 4 octets), soit environ 1.35 Mo. Chaque tête ajoute 2,176 octets pour sa requête et sa sortie en BF16, et effectue 2,176 FLOPs par ligne (QK sur 576 dimensions, PV sur 512). Ainsi, 8 têtes effectuent 35.7 MFLOPs sur 1.37 Mo, soit environ 26 FLOPs par octet, et 64 têtes effectuent 285 MFLOPs sur 1.49 Mo, soit environ 191. ↩

  2. Pour le H200 SXM : 989 TFLOP/s BF16 dense (les 1,979 de la fiche technique supposent une parcimonie 2:4) 4.8 To/s 206 FLOPs par octet (fiche technique NVIDIA H200). ↩

  3. Par token : 78 couches × 656 o de KV MLA, 21 couches d'indexeur × 132 o de clés d'indexeur (128 valeurs FP8 plus une échelle de 4 octets), et 788 o pour la couche MTP, soit 54,728 o au total. Pour = 1,048,576 tokens, cela représente 54,728 Mio 53.4 Gio. ↩

Source originale

vLLM Blog

À propos du contenu

La publication originale et les droits appartiennent à la source.

Traduction automatique · Consultez l’original