Dans cet article de blog, l’équipe d’ingénierie de MosaicML partage les bonnes pratiques pour tirer parti des modèles de langage volumineux (LLM) open source populaires pour une utilisation en production. Nous fournissons également des instructions pour le déploiement de services d’inférence construits autour de ces modèles afin d’aider les utilisateurs dans leur sélection de modèles et de matériel de déploiement. Nous avons travaillé avec plusieurs backends basés sur PyTorch en production ; ces instructions sont tirées de notre expérience avec FasterTransformers, vLLM, TensorRT-LLM de NVIDIA, qui sera bientôt disponible, et d’autres.
Comprendre la génération de texte LLM
Les modèles de langage volumineux (LLM) génèrent du texte en deux étapes : le « préremplissage », où les jetons dans l’invite d’entrée sont traités en parallèle, et le « décodage », où le texte est généré un « jeton » à la fois de manière autorégressive. Chaque jeton généré est ajouté à l’entrée et réinjecté dans le modèle pour générer le jeton suivant. La génération s’arrête lorsque le LLM génère un jeton d’arrêt spécial ou lorsqu’une condition définie par l’utilisateur est remplie (par exemple, un nombre maximal de jetons a été généré). Si vous souhaitez en savoir plus sur la façon dont les LLM utilisent les blocs de décodeur, consultez cet article de blog.
Les jetons peuvent être des mots ou des sous-mots ; les règles exactes de division du texte en jetons varient d’un modèle à l’autre. Par exemple, vous pouvez comparer la façon dont les modèles Llama tokenisent le texte à la façon dont les modèles OpenAI tokenisent le texte. Bien que les fournisseurs d’inférence LLM parlent souvent de performances en termes de mesures basées sur les jetons (par exemple, jetons/seconde), ces chiffres ne sont pas toujours comparables entre les types de modèles compte tenu de ces variations. Pour un exemple concret, l’équipe d’Anyscale a constaté que la tokenisation de Llama 2 est 19 % plus longue que la tokenisation de ChatGPT (mais a toujours un coût global beaucoup plus faible). Et les chercheurs de HuggingFace ont également constaté que Llama 2 nécessitait environ 20 % de jetons supplémentaires pour s’entraîner sur la même quantité de texte que GPT-4.
Métriques importantes pour le service LLM
Alors, comment devrions-nous envisager la vitesse d’inférence ?
Notre équipe utilise quatre métriques clés pour le service LLM :
- Temps jusqu’au premier jeton (TTFT) : vitesse à laquelle les utilisateurs commencent à voir la sortie du modèle après avoir entré leur requête. De faibles temps d’attente pour une réponse sont essentiels dans les interactions en temps réel, mais moins importants dans les charges de travail hors ligne. Cette métrique est déterminée par le temps nécessaire pour traiter l’invite, puis générer le premier jeton de sortie.
- Temps par jeton de sortie (TPOT) : temps nécessaire pour générer un jeton de sortie pour chaque utilisateur qui interroge notre système. Cette métrique correspond à la façon dont chaque utilisateur percevra la « vitesse » du modèle. Par exemple, un TPOT de 100 millisecondes/jeton serait de 10 jetons par seconde par utilisateur, soit environ 450 mots par minute, ce qui est plus rapide qu’une personne typique ne peut lire.
- Latence : temps total nécessaire au modèle pour générer la réponse complète pour un utilisateur. La latence globale de la réponse peut être calculée à l’aide des deux métriques précédentes : latence = (TTFT) + (TPOT) * (le nombre de jetons à générer).
- Débit : nombre de jetons de sortie par seconde qu’un serveur d’inférence peut générer pour tous les utilisateurs et toutes les requêtes.
Notre objectif ? Le temps le plus rapide jusqu’au premier jeton, le débit le plus élevé et le temps le plus rapide par jeton de sortie. En d’autres termes, nous voulons que nos modèles génèrent du texte aussi rapidement que possible pour autant d’utilisateurs que nous pouvons prendre en charge.
Il existe notamment un compromis entre le débit et le temps par jeton de sortie : si nous traitons 16 requêtes d’utilisateur simultanément, nous aurons un débit plus élevé par rapport à l’exécution séquentielle des requêtes, mais nous mettrons plus de temps à générer des jetons de sortie pour chaque utilisateur.
Si vous avez des objectifs de latence d’inférence globale, voici quelques heuristiques utiles pour évaluer les modèles :
- La longueur de la sortie domine la latence globale de la réponse : Pour la latence moyenne, vous pouvez généralement simplement prendre la longueur de jeton de sortie maximale/attendue et la multiplier par un temps moyen global par jeton de sortie pour le modèle.
- La longueur de l’entrée n’est pas significative pour les performances, mais importante pour les exigences matérielles : L’ajout de 512 jetons d’entrée augmente la latence moins que la production de 8 jetons de sortie supplémentaires dans les modèles MPT. Toutefois, la nécessité de prendre en charge les entrées longues peut rendre les modèles plus difficiles à servir. Par exemple, nous vous recommandons d’utiliser l’A100-80 Go (ou plus récent) pour servir MPT-7B avec sa longueur de contexte maximale de 2 048 jetons.
- La latence globale évolue de manière sous-linéaire avec la taille du modèle : Sur le même matériel, les modèles plus volumineux sont plus lents, mais le rapport de vitesse ne correspond pas nécessairement au rapport de nombre de paramètres. La latence de MPT-30B est environ 2,5 fois supérieure à celle de MPT-7B. La latence de Llama2-70B est environ 2 fois supérieure à celle de Llama2-13B.
Les clients potentiels nous demandent souvent de fournir une latence d’inférence moyenne. Nous vous recommandons, avant de vous fixer sur des objectifs de latence spécifiques (« nous avons besoin de moins de 20 ms par jeton »), de passer un certain temps à caractériser vos longueurs d’entrée attendues et de sortie souhaitées.
Défis liés à l’inférence LLM
L’optimisation de l’inférence LLM bénéficie de techniques générales telles que :
- Fusion d’opérateurs : La combinaison de différents opérateurs adjacents entraîne souvent une meilleure latence.
- Quantification : Les activations et les pondérations sont compressées pour utiliser un plus petit nombre de bits.
- Compression : Parcimonie ou Distillation.
- Parallélisation : Parallélisme tensoriel sur plusieurs appareils ou parallélisme de pipeline pour les modèles plus volumineux.
Au-delà de ces méthodes, il existe de nombreuses optimisations importantes spécifiques à Transformer. Un excellent exemple est la mise en cache KV (clé-valeur). Le mécanisme d’attention dans les modèles basés sur Transformer avec décodeur uniquement est inefficace sur le plan du calcul. Chaque jeton est attentif à tous les jetons précédemment vus, et recalcule donc bon nombre des mêmes valeurs à chaque nouveau jeton généré. Par exemple, lors de la génération du Nième jeton, le (N-1)ème jeton est attentif aux (N-2)ème, (N-3)ème… 1er jetons. De même, lors de la génération du (N+1)ème jeton, l’attention pour le Nième jeton doit à nouveau examiner les (N-1)ème, (N-2)ème, (N-3)ème… 1er jetons. La mise en cache KV, c’est-à-dire l’enregistrement des clés/valeurs intermédiaires pour les couches d’attention, est utilisée pour conserver ces résultats pour une réutilisation ultérieure, évitant ainsi les calculs répétés.
La bande passante mémoire est essentielle
Les calculs dans les LLM sont principalement dominés par les opérations de multiplication matrice-matrice ; ces opérations avec de petites dimensions sont généralement liées à la bande passante mémoire sur la plupart des matériels. Lors de la génération de jetons de manière autorégressive, l’une des dimensions de la matrice d’activation (définie par la taille du lot et le nombre de jetons dans la séquence) est petite à de petites tailles de lot. Par conséquent, la vitesse dépend de la rapidité avec laquelle nous pouvons charger les paramètres du modèle de la mémoire GPU vers les caches/registres locaux, plutôt que de la rapidité avec laquelle nous pouvons calculer sur les données chargées. La bande passante mémoire disponible et atteinte dans le matériel d’inférence est un meilleur prédicteur de la vitesse de génération de jetons que leurs performances de calcul de pointe.
L’utilisation du matériel d’inférence est très importante en termes de coûts de service. Les GPU sont coûteux et nous avons besoin qu’ils fassent autant de travail que possible. Les services d’inférence partagés promettent de maintenir les coûts bas en combinant les charges de travail de nombreux utilisateurs, en comblant les lacunes individuelles et en regroupant les requêtes qui se chevauchent. Pour les modèles volumineux comme Llama2-70B, nous n’obtenons un bon rapport coût/performance qu’à de grandes tailles de lot. Le fait d’avoir un système de service d’inférence qui peut fonctionner à de grandes tailles de lot est essentiel pour l’efficacité des coûts. Toutefois, un grand lot signifie une plus grande taille de cache KV, ce qui augmente à son tour le nombre de GPU nécessaires pour servir le modèle. Il y a ici un tiraillement et les opérateurs de services partagés doivent faire des compromis sur les coûts et mettre en œuvre des optimisations de système.
Utilisation de la bande passante du modèle (MBU)
Dans quelle mesure un serveur d’inférence LLM est-il optimisé ?
Comme expliqué brièvement précédemment, l’inférence pour les LLM à de plus petites tailles de lot, en particulier au moment du décodage, est limitée par la vitesse à laquelle nous pouvons charger les paramètres du modèle de la mémoire de l’appareil vers les unités de calcul. La bande passante mémoire dicte la vitesse à laquelle le déplacement des données se produit. Pour mesurer l’utilisation du matériel sous-jacent, nous introduisons une nouvelle métrique appelée Utilisation de la bande passante du modèle (MBU). La MBU est définie comme (bande passante mémoire atteinte) / (bande passante mémoire de pointe) où la bande passante mémoire atteinte est ((taille totale des paramètres du modèle + taille du cache KV) / TPOT).
Par exemple, si un paramètre de 7B s’exécutant avec une précision de 16 bits a un TPOT égal à 14 ms, il déplace 14 Go de paramètres en 14 ms, ce qui se traduit par une utilisation de la bande passante de 1 To/s. Si la bande passante de pointe de la machine est de 2 To/s, nous fonctionnons à une MBU de 50 %. Par souci de simplicité, cet exemple ignore la taille du cache KV, qui est petite pour les petites tailles de lot et les longueurs de séquence plus courtes. Les valeurs MBU proches de 100 % impliquent que le système d’inférence utilise efficacement la bande passante mémoire disponible. La MBU est également utile pour comparer différents systèmes d’inférence (matériel + logiciel) de manière normalisée. La MBU est complémentaire à la métrique d’utilisation des FLOPS du modèle (MFU ; introduite dans l’article PaLM), qui est importante dans les paramètres liés au calcul.
La figure 1 montre une représentation picturale de la MBU dans un tracé similaire à un tracé de ligne de toit. La ligne inclinée continue de la région ombrée en orange montre le débit maximal possible si la bande passante mémoire est entièrement saturée à 100 %. Toutefois, en réalité, pour les petites tailles de lot (point blanc), les performances observées sont inférieures au maximum : la mesure dans laquelle elles sont inférieures est une mesure de la MBU. Pour les grandes tailles de lot (région jaune), le système est lié au calcul, et le débit atteint en tant que fraction du débit maximal possible est mesuré comme l’utilisation des FLOPS du modèle (MFU).
La MBU et la MFU déterminent la marge de manœuvre disponible pour améliorer davantage la vitesse d’inférence sur une configuration matérielle donnée. La figure 2 montre la MBU mesurée pour différents degrés de parallélisme tensoriel avec notre serveur d’inférence basé sur TensorRT-LLM. L’utilisation maximale de la bande passante mémoire est atteinte lors du transfert de grands blocs de mémoire contigus. Lorsque des modèles plus petits comme MPT-7B sont distribués sur plusieurs GPU, nous observons une MBU inférieure, car nous déplaçons des blocs de mémoire plus petits dans chaque GPU.
La figure 3 montre la MBU observée empiriquement pour différents degrés de parallélisme tensoriel et de tailles de lot sur les GPU NVIDIA H100. La MBU diminue à mesure que la taille du lot augmente. Toutefois, à mesure que nous mettons à l’échelle les GPU, la diminution relative de la MBU est moins significative. Il convient également de noter que le choix d’un matériel avec une plus grande bande passante mémoire peut améliorer les performances avec moins de GPU. À la taille de lot 1, nous pouvons atteindre une MBU plus élevée de 60 % sur 2xH100-80 Go par rapport à 55 % sur les GPU 4xA100-40 Go (figure 2).
Résultats d’analyse comparative
Latence
Nous avons mesuré le temps jusqu’au premier jeton (TTFT) et le temps par jeton de sortie (TPOT) pour différents degrés de parallélisme tensoriel pour les modèles MPT-7B et Llama2-70B. À mesure que les invites d’entrée s’allongent, le temps nécessaire pour générer le premier jeton commence à consommer une partie importante de la latence totale. Le parallélisme tensoriel sur plusieurs GPU permet de réduire cette latence.
Contrairement à l’apprentissage du modèle, la mise à l’échelle à davantage de GPU offre des rendements décroissants importants pour la latence d’inférence. Par exemple, pour Llama2-70B, le passage de 4x à 8x GPU ne diminue la latence que de 0,7x à de petites tailles de lot. L’une des raisons en est qu’un parallélisme plus élevé a une MBU inférieure (comme indiqué précédemment). Une autre raison est que le parallélisme tensoriel introduit une surcharge de communication sur un nœud GPU.
| Temps jusqu’au premier jeton (ms) | ||||
|---|---|---|---|---|
| Modèle | 1xA100-40 Go | 2xA100-40 Go | 4xA100-40 Go | 8xA100-40 Go |
| MPT-7B | 46 (1x) | 34 (0,73x) | 26 (0,56x) | - |
| Llama2-70B | Ne convient pas | 154 (1x) | 114 (0,74x) | |
Tableau 1 : Temps jusqu’au premier jeton étant donné que les requêtes d’entrée ont une longueur de 512 jetons avec une taille de lot de 1. Les modèles plus volumineux comme Llama2 70B ont besoin d’au moins 4 GPU A100-40B pour tenir dans la mémoire
À des tailles de lot plus importantes, un parallélisme tensoriel plus élevé entraîne une diminution relative plus importante de la latence des jetons. La figure 4 montre comment le temps par jeton de sortie varie pour MPT-7B. À la taille de lot 1, le passage de 2x à 4x ne réduit la latence des jetons que d’environ 12 %. À la taille de lot 16, la latence avec 4x est inférieure de 33 % à celle avec 2x. Cela correspond à notre observation précédente selon laquelle la diminution relative de la MBU est plus faible à des degrés plus élevés de parallélisme tensoriel pour la taille de lot 16 par rapport à la taille de lot 1.
La figure 5 montre des résultats similaires pour Llama2-70B, sauf que l’amélioration relative entre 4x et 8x est moins prononcée. Nous comparons également la mise à l’échelle du GPU sur deux matériels différents. Étant donn é que H100-80 Go a une bande passante mémoire GPU 2,15 fois supérieure à celle d’A100-40 Go, nous pouvons constater que la latence est inférieure de 36 % à la taille de lot 1 et de 52 % à la taille de lot 16 pour les systèmes 4x.
Débit
Nous pouvons échanger le débit et le temps par jeton en regroupant les requêtes. Le regroupement des requêtes lors de l’évaluation du GPU augmente le débit par rapport au traitement séquentiel des requêtes, mais chaque requête prendra plus de temps (sans tenir compte des effets de mise en file d’attente).
Il existe quelques techniques courantes pour le traitement par lots des requêtes d’inférence :
- Traitement par lots statique : Le client regroupe plusieurs invites dans des requêtes et une réponse est renvoyée une fois que toutes les séquences du lot sont terminées. Nos serveurs d’inférence prennent en charge cela, mais ne l’exigent pas.
- Traitement par lots dynamique : Les invites sont regroupées à la volée à l’intérieur du serveur. En règle générale, cette méthode est moins performante que le traitement par lots statique, mais peut se rapprocher de l’optimal si les réponses sont courtes ou de longueur uniforme. Ne fonctionne pas bien lorsque les requêtes ont des paramètres différents.
- Traitement par lots continu : L’idée de regrouper les requêtes à mesure qu’elles arrivent a été introduite dans cet excellent article et est actuellement la méthode SOTA. Au lieu d’attendre que toutes les séquences d’un lot soient terminées, il regroupe les séquences au niveau de l’itération. Il peut atteindre un débit 10 à 20 fois supérieur à celui du traitement par lots dynamique.
Le traitement par lots continu est généralement la meilleure approche pour les services partagés, mais il existe des situations où les deux autres pourraient être meilleurs. Dans les environnements à faible QPS, le traitement par lots dynamique peut surpasser le traitement par lots continu. Il est parfois plus facile d’implémenter des optimisations GPU de bas niveau dans un framework de traitement par lots plus simple. Pour les charges de travail d’inférence par lots hors ligne, le traitement par lots statique peut éviter une surcharge importante et atteindre un meilleur débit.
Taille du lot
L’efficacité du traitement par lots dépend fortement du flux de requêtes. Mais nous pouvons obtenir une limite supérieure sur ses performances en effectuant une analyse comparative du traitement par lots statique avec des requêtes uniformes.
| Taille du lot | |||||||
|---|---|---|---|---|---|---|---|
| Matériel | 1 | 4 | 8 | 16 | 32 | 64 | 128 |
| 1 x A10 | 0,4 (1x) | 1,4 (3,5x) | 2,3 (6x) | 3,5 (9x) | Erreur OOM (mémoire insuffisante) | ||
| 2 x A10 | 0,8 | 2,5 | 4,0 | 7,0 | 8,0 | ||
| 1 x A100 | 0,9 (1x) | 3,2 (3,5x) | 5,3 (6x) | 8,0 (9x) | 10,5 (12x) | 12,5 (14x) | |
| 2 x A100 | 1,3 | 3,0 | 5,5 | 9,5 | 14,5 | 17,0 | 22,0 |
| 4 x A100 | 1,7 | 6,2 | 11,5 | 18,0 | 25,0 | 33,0 | |
