Vous avez des difficultés avec l'IA ou le développement full-stack ? Nos experts sont là pour vous guider : conseils sur mesure, intégration technique, et plus encore. Contactez-nous à [email protected].

Techniques d'optimisation de l'inférence LLM

L'optimisation de l'inférence est une partie essentielle des applications d'IA générative déployées en production. L'utilisation efficace des LLM à grande échelle est un défi et de nombreuses techniques ont été développées au cours des dernières années pour rendre l'inférence plus rapide et moins coûteuse. Passons en revue ces techniques dans cet article.

Techniques d'optimisation de l'inférence LLM

L'architecture des LLM en point de mire

Les grands modèles de langage (LLM) sont tous basés sur l'architecture transformatrice inventée en 2017 par Vaswani et al. L'architecture transformatrice permet d'obtenir une précision supérieure, un apprentissage en quelques étapes et des capacités quasi humaines dans diverses tâches linguistiques. Cependant, ces modèles de base, qui comprennent souvent des dizaines ou des centaines de milliards de paramètres, sont coûteux à former et gourmands en ressources lors de l'inférence. Les coûts d'inférence augmentent avec des contextes d'entrée longs, qui exigent une puissance de traitement importante en raison des données d'entrée volumineuses. L'efficacité de l'inférence est donc un défi majeur, notamment en ce qui concerne la gestion de la mémoire et des ressources informatiques.

L'architecture du transformateur
L'architecture du transformateur

Plus précisément, la plupart des LLMs bien connus sont des LLMs de décodeur uniquement, comme GPT-3, GPT-4, LLaMA, Mistral, DeepSeek, etc. Ces modèles sont pré-entraînés sur une tâche de modélisation causale, fonctionnant comme des prédicteurs du mot suivant. Ils traitent une séquence de mots en entrée et produisent les mots suivants de manière autorégressive jusqu'à ce qu'une condition d'arrêt soit atteinte.

L'inférence LLM dans les modèles à décodeur seul implique deux phases clés : la phase de pré-remplissage et la phase de décodage. Dans la phase de pré-remplissage, le modèle traite les jetons d'entrée pour calculer les états intermédiaires (clés et valeurs) afin de générer le premier nouveau jeton. Cette phase, qui ressemble à une opération matrice-matrice, est hautement parallélisée et utilise efficacement les capacités des GPU. Inversement, la phase de décodage génère des jetons, un par un, en s'appuyant sur les états des jetons précédents. Cette opération matrice-vecteur est liée à la mémoire, car le transfert de données vers le GPU, plutôt que la vitesse de calcul, dicte principalement la latence, ce qui entraîne une sous-utilisation de la puissance de calcul du GPU.

L'optimisation de la phase de décodage est un point central pour relever les défis de l'inférence. Les solutions incluent le développement de mécanismes d'attention efficaces et une meilleure gestion des clés et des valeurs afin de réduire les goulots d'étranglement de la mémoire. Ce billet met en évidence des approches pratiques pour améliorer les performances de l'inférence, en supposant que les lecteurs ont une compréhension de base de l'architecture du transformateur et des mécanismes d'attention. Ces optimisations sont cruciales pour améliorer le débit et réduire la latence dans les déploiements LLM réels.

Une autre complication provient de l'utilisation de différents tokenizers à travers les LLM, ce qui affecte la comparabilité des tokens. Les jetons, qui équivalent à peu près à quatre caractères anglais, varient en représentation selon le tokéniseur, ce qui rend les comparaisons directes du débit d'inférence (par exemple, jetons par seconde) trompeuses. Cette variabilité souligne le besoin de mesures d'évaluation standardisées pour évaluer et comparer avec précision la performance des LLM pendant l'inférence.

Mise en lots

La mise en lots est une stratégie clé pour améliorer l'utilisation du GPU et le débit dans les grands modèles de langage (LLM). En traitant plusieurs requêtes simultanément à l'aide du même modèle, la mise en lot répartit le coût de la mémoire des poids du modèle entre les requêtes, ce qui permet aux lots plus importants d'exploiter davantage la puissance de calcul du GPU. Cependant, il y a une limite à la taille des lots, car des lots trop importants peuvent entraîner un débordement de la mémoire en raison des exigences de mémoire des LLM, notamment en ce qui concerne la mise en cache clé-valeur (KV) (nous y reviendrons plus tard).

Techniques de mise en lots
Techniques de mise en lots

La mise en lots traditionnelle ou statique présente des limites car les demandes au sein d'un lot génèrent souvent des nombres différents de jetons d'achèvement, ce qui se traduit par des temps d'exécution variables. Toutes les demandes doivent donc attendre que la plus lente se termine, ce qui peut s'avérer problématique lorsque les durées d'exécution varient considérablement. Pour remédier à ce problème, des techniques avancées telles que la mise en lots en vol ont été développées afin d'optimiser les performances.

Le batching en vol, également connu sous le nom de batching continu, s'attaque aux défis posés par la nature dynamique des charges de travail LLM, qui peuvent aller de simples réponses de chatbot à des résumés de documents complexes ou à la génération de code. Ces tâches produisent des résultats de tailles très différentes, ce qui rend difficile la mise en lot et l'exécution efficace des requêtes en parallèle. Contrairement à la mise en lot statique, la mise en lot en vol permet au serveur d'expulser immédiatement les séquences terminées du lot et de commencer à traiter de nouvelles demandes alors que d'autres sont encore en cours. Cette approche améliore considérablement l'utilisation des GPU en s'adaptant aux différents temps d'exécution des requêtes dans des scénarios réels.

Déploiement multi-GPU avec parallélisation des modèles

La parallélisation des modèles est une stratégie essentielle pour gérer la mémoire et les exigences de calcul des modèles d'apprentissage automatique à grande échelle en les distribuant sur plusieurs GPU. Cette approche permet de traiter des modèles plus importants ou des lots d'entrées qui dépassent la capacité de mémoire d'un seul appareil, ce qui la rend essentielle pour la formation et l'inférence lorsque les contraintes de mémoire sont strictes. Il existe plusieurs techniques pour diviser les poids des modèles, notamment le parallélisme de pipeline, le parallélisme tensoriel et le parallélisme de séquence, chacune abordant des aspects différents de la distribution des modèles. Contrairement au parallélisme de données, qui se concentre sur la réplication des poids de modèle entre les dispositifs afin de traiter des lots d'entrée plus importants pendant la formation, ces méthodes sont plus pertinentes pour réduire les empreintes de mémoire pendant la formation et l'inférence.

Plusieurs GPU NVIDIA
Plusieurs GPU NVIDIA

Le parallélisme de pipeline divise le modèle verticalement en morceaux séquentiels, chaque morceau contenant un sous-ensemble de couches assignées à un dispositif distinct. Par exemple, dans une configuration de pipeline à quatre voies, chaque dispositif gère un quart des couches du modèle, transmettant les sorties au dispositif suivant dans l'ordre. Bien que cette méthode réduise considérablement les besoins en mémoire par périphérique, elle introduit des inefficacités connues sous le nom de "bulles de pipeline", où les périphériques peuvent rester inactifs pendant qu'ils attendent les sorties des couches précédentes. Le microbatching, qui divise les lots d'entrée en sous-lots plus petits pour un traitement séquentiel, peut réduire ces bulles mais ne les élimine pas complètement, car les temps morts persistent pendant les passages avant et arrière.

Le parallélisme tensoriel, en revanche, divise les couches individuelles horizontalement en blocs de calcul plus petits qui peuvent être exécutés indépendamment sur plusieurs appareils. Ce parallélisme est particulièrement efficace pour les composants du transformateur tels que les blocs d'attention et les perceptrons multicouches (MLP), où, par exemple, différentes têtes d'attention peuvent être affectées à des dispositifs distincts pour le calcul parallèle. Cependant, le parallélisme tensoriel est moins efficace pour les opérations telles que LayerNorm et Dropout, qui ne peuvent pas être facilement divisées et doivent être répliquées sur plusieurs appareils, ce qui entraîne une utilisation redondante de la mémoire pour le stockage des activations. Cette limitation souligne la nécessité d'approches complémentaires pour optimiser l'efficacité de la mémoire.

Le parallélisme de séquence permet de remédier à l'inefficacité de la mémoire d'opérations telles que LayerNorm et Dropout en les partitionnant le long de la dimension de la séquence d'entrée, en tirant parti de leur indépendance par rapport aux éléments de la séquence. Cette méthode réduit l'empreinte mémoire des activations redondantes, ce qui en fait un complément précieux du parallélisme tensoriel. Ces techniques de parallélisation ne s'excluent pas mutuellement et peuvent être combinées pour optimiser davantage les grands modèles de langage (LLM). En outre, des stratégies d'optimisation spécifiques pour le module d'attention peuvent améliorer l'évolutivité et réduire les demandes de mémoire par GPU, permettant une formation et une inférence plus efficaces pour les grands modèles.

Optimisation de l'attention

L'article de 2017 *Attention Is All You Need* de Vaswani et al. a présenté le modèle Transformer, dont la pierre angulaire est l'auto-attention. L'auto-attention permet au modèle d'évaluer la pertinence des différents mots d'une phrase les uns par rapport aux autres, améliorant la compréhension contextuelle pour des tâches telles que le traitement du langage naturel. L'article a formalisé l'auto-attention, en particulier par le biais du mécanisme d'attention par produit ponctuel échelonné (SDPA), qui associe des paires de requêtes et de valeurs clés à une sortie, ce qui en fait un composant essentiel des réseaux neuronaux modernes. Voici quelques-unes des techniques les plus importantes pour optimiser les calculs d'attention :

Le document d'attention
Le document d'attention

L'attention multi-têtes (MHA) s'appuie sur la SDPA en exécutant plusieurs opérations d'attention en parallèle, chacune avec des projections distinctes des matrices d'interrogation, de clé et de valeur. Ces opérations parallèles, ou "têtes", se concentrent sur différents sous-espaces de représentation, enrichissant ainsi la compréhension de l'entrée par le modèle. Les sorties de ces têtes sont concaténées et projetées linéairement, ce qui permet de maintenir une efficacité de calcul comparable à celle de l'attention à une seule tête en réduisant la dimensionnalité de chaque tête (par exemple, en divisant la dimension du modèle par le nombre de têtes, par exemple 8).

L'attention multi-requête (MQA) optimise MHA pour l'inférence en partageant les projections de clés et de valeurs entre plusieurs têtes d'attention tout en conservant les projections de requêtes multiples. Cela réduit les besoins en bande passante de la mémoire et la taille du cache clé-valeur (KV), ce qui permet d'augmenter la taille des lots et d'améliorer l'utilisation du calcul. Cependant, l'AQM peut légèrement réduire la précision, et les modèles qui l'exploitent nécessitent un entraînement ou un réglage fin avec l'AQM activé pour maintenir les performances.

L'attention portée aux requêtes groupées (GQA) équilibre MHA et MQA en regroupant les têtes de requêtes et en partageant les projections de valeurs clés au sein de chaque groupe, ce qui permet d'obtenir une qualité proche de MHA avec une efficacité de calcul plus proche de MQA. Des modèles comme le Llama 2 70B utilisent l'AQG, et ceux qui ont été formés avec MHA peuvent être adaptés à l'AQG avec un minimum de formation supplémentaire. L'AQM et l'AQG réduisent les besoins en mémoire cache de la KV, bien que d'autres optimisations de la gestion de la mémoire cache restent nécessaires.

FlashAttention améliore les mécanismes d'attention en réorganisant les calculs pour exploiter plus efficacement les hiérarchies de mémoire des GPU. Contrairement au traitement traditionnel couche par couche, FlashAttention fusionne les opérations et utilise le "pavage" pour calculer en une seule fois de petites portions de la matrice de sortie, minimisant ainsi les opérations de lecture/écriture de la mémoire. Cet algorithme d'attention exacte, sensible aux E/S, s'intègre parfaitement dans les modèles existants sans modification, offrant des accélérations significatives en optimisant le mouvement des données.

Mise en cache clé-valeur

La mise en cache des KV est une technique d'optimisation critique utilisée pendant la phase de décodage des grands modèles de langage (LLM) pour améliorer l'efficacité des calculs d'auto-attention. Dans cette phase, chaque jeton généré dépend des tenseurs de clé (K) et de valeur (V) de tous les jetons précédents, y compris ceux calculés lors de l'étape de pré-remplissage et des étapes de décodage ultérieures. Au lieu de recalculer ces tenseurs pour chaque jeton à chaque étape, la mise en cache KV les stocke dans la mémoire du GPU, en ajoutant de nouveaux tenseurs au cache au fur et à mesure qu'ils sont calculés. En règle générale, un cache KV distinct est maintenu pour chaque couche du modèle, ce qui réduit considérablement les calculs redondants et accélère le processus de décodage.

Mise en cache clé-valeur
Mise en cache clé-valeur

Les besoins en mémoire pour les LLM sur les GPU sont principalement déterminés par deux composants : les poids du modèle et le cache KV. Les poids du modèle, qui se composent des paramètres du modèle, occupent une grande partie de la mémoire ; par exemple, un modèle à 7 milliards de paramètres comme Llama 2 7B avec une précision de 16 bits nécessite environ 14 Go. Le cache KV, quant à lui, stocke les tenseurs d'auto-attention afin d'éviter les recalculs. Sa taille est déterminée par des facteurs tels que le nombre de couches, les têtes d'attention, les dimensions des têtes et la précision. Pour chaque jeton, la taille du cache est calculée comme suit : 2 * num_layers * (num_heads * dim_head) * precision_in_bytes, où le facteur 2 tient compte des matrices K et V. Pour un lot d'entrées, la taille totale du cache KV augmente avec la taille du lot et la longueur de la séquence, pouvant atteindre des tailles importantes, comme ~2 Go pour un modèle Llama 2 7B avec une longueur de séquence de 4 096 et une taille de lot de 1.

La gestion efficace du cache KV pose des problèmes en raison de sa croissance linéaire en fonction de la taille du lot et de la longueur de la séquence, ce qui peut limiter le débit et compliquer le traitement des entrées à contexte long. Une inefficacité courante provient du surprovisionnement statique, où la mémoire est réservée pour la longueur de séquence maximale prise en charge (par exemple, 2 048 jetons), quelle que soit la taille réelle de l'entrée. Cela conduit à un gaspillage ou à une fragmentation importants de la mémoire, car une grande partie de l'espace réservé reste souvent inutilisée pendant toute la durée de vie de la requête, ce qui mobilise de précieuses ressources mémoire du GPU.

Pour remédier à ces inefficacités, l'algorithme PagedAttention introduit une nouvelle approche inspirée de la pagination des systèmes d'exploitation. Il divise le cache KV en blocs de taille fixe, chacun représentant un nombre déterminé de jetons, qui peuvent être stockés de manière non contiguë dans la mémoire. Une table de blocs suit ces blocs, les récupérant au besoin pendant les calculs d'attention. Lorsque de nouveaux jetons sont générés, des blocs supplémentaires sont alloués dynamiquement. Cette méthode minimise le gaspillage de mémoire en éliminant le besoin d'allocation contiguë et de surprovisionnement, en permettant des tailles de lots plus importantes et en améliorant le débit, ce qui constitue une avancée significative dans la gestion de la mémoire cache KV pour les LLM.

Optimisation du modèle

Dans cette section, nous discutons des différentes techniques d'optimisation des grands modèles de langage (LLM) afin de réduire leur consommation de mémoire et d'améliorer leur performance sur les GPU. Les méthodes clés comprennent la quantification, la sparsité et la distillation, chacune ciblant des aspects différents de l'efficacité du modèle. Ces techniques modifient les poids des modèles, exploitent l'accélération du matériel GPU et transfèrent les connaissances vers des modèles plus petits, ce qui permet d'exécuter des modèles plus importants sur un matériel limité tout en maintenant les performances. Ces méthodes peuvent dégrader la précision du modèle et doivent donc être utilisées avec prudence.

La quantification réduit la précision des poids et des activations d'un modèle, généralement de 32 ou 16 bits à 8 bits ou moins, ce qui permet aux modèles d'occuper moins de mémoire et de transférer des données plus efficacement. Alors que la quantification des poids est simple en raison de leur nature fixe après l'entraînement, la quantification des activations est plus complexe en raison des valeurs aberrantes qui étendent leur plage dynamique. Des techniques comme LLM.int8() traitent ce problème en appliquant sélectivement une précision plus élevée à certaines activations, ou en réutilisant la plage dynamique des poids quantifiés pour les activations, bien que les GPU puissent avoir besoin de convertir les poids à nouveau à une précision plus élevée pour les opérations.

La sparité consiste à élaguer les valeurs du modèle proches de zéro, créant ainsi des matrices peu denses qui requièrent moins de mémoire. Les GPU prennent en charge la sparité structurée, par exemple en représentant deux valeurs sur quatre par des zéros, ce qui accélère les calculs. La combinaison de la rareté et de la quantification peut encore améliorer la vitesse d'exécution. Les recherches se poursuivent pour explorer les représentations claires optimales pour les LLM, ce qui indique une voie prometteuse pour l'amélioration des vitesses d'inférence.

La distillation transfère les connaissances d'un modèle "enseignant" plus grand à un modèle "étudiant" plus petit, ce qui permet de réduire la taille tout en préservant les performances. Par exemple, DistilBERT permet de réduire la taille de 40 % et d'augmenter la vitesse de 60 % par rapport à BERT, tout en conservant 97 % de ses capacités. La distillation peut impliquer l'imitation des résultats de l'enseignant ou l'utilisation de données générées par l'enseignant pour la formation, avec des méthodes telles que "Distiller pas à pas", qui intègrent des justifications pour un apprentissage efficace. Cependant, les licences restrictives de nombreux LLM avancés limitent la disponibilité de modèles d'enseignants appropriés pour la distillation.

Inférence spéculative

L'inférence spéculative, également connue sous le nom d'échantillonnage spéculatif ou de génération assistée, est une méthode permettant de paralléliser l'exécution de grands modèles de langage autorégressifs (LLM) tels que les modèles de type GPT, qui génèrent généralement du texte jeton par jeton. Dans l'exécution standard, chaque jeton dépend de tous les jetons précédents pour le contexte, ce qui rend la génération parallèle impossible puisque le nième jeton doit être généré avant le (n+1)ème. L'inférence spéculative résout ce problème en utilisant un modèle préliminaire "moins coûteux" pour prédire simultanément plusieurs jetons futurs, qui sont ensuite vérifiés ou rejetés en parallèle par le modèle principal, ce qui permet une génération de texte plus rapide.

Le processus consiste à générer un projet de suite de plusieurs jetons à l'aide d'une méthode moins gourmande en ressources, suivi d'une vérification parallèle par le modèle principal en utilisant le projet comme contexte spéculatif. Si le modèle de vérification correspond aux jetons du brouillon, ils sont acceptés ; dans le cas contraire, les jetons qui ne correspondent pas et les suivants sont rejetés, et le processus recommence avec un nouveau brouillon. Les ébauches de jetons peuvent être générées à l'aide de différentes approches, telles que l'entraînement de plusieurs modèles, le réglage fin de plusieurs têtes sur un modèle pré-entraîné pour prédire les futurs jetons, ou l'utilisation d'un modèle d'ébauche plus petit aux côtés d'un modèle de vérification plus grand et plus performant, chacun avec ses propres compromis.

Inférence désagrégée

L'inférence désagrégée est une technique dans laquelle les tâches de calcul sont réparties sur différents matériels afin d'optimiser les performances, les coûts et l'utilisation des ressources. Plus précisément, elle sépare les phases de pré-remplissage et de décodage. En désagrégeant ces phases, chacune peut être assignée au matériel le mieux adapté à ses exigences de calcul, ce qui améliore l'efficacité et l'évolutivité.

Inférence désagrégée
Inférence désagrégée

Le préremplissage est une opération à forte intensité de calcul, qui nécessite d'importantes multiplications de matrices pour traiter l'ensemble de l'invite d'entrée et produire des caches KV. Cette phase bénéficie d'un matériel à haute performance comme les GPU ou les TPU, qui excellent dans les calculs parallèles. Le préremplissage étant une tâche unique par demande d'inférence, il peut être déchargé sur un nœud de calcul centralisé et puissant, optimisé pour ce type de charge de travail. Cette configuration permet un traitement plus rapide des requêtes volumineuses et réduit la charge sur les appareils moins performants, ce qui la rend idéale pour les environnements basés sur l'informatique en nuage ou les centres de données où du matériel à haut débit est disponible.

Le décodage, en revanche, est lié à la mémoire et implique la génération itérative de jetons, en s'appuyant fortement sur l'accès aux caches KV. Il nécessite moins de puissance de calcul mais un accès rapide à la mémoire, ce qui le rend adapté à un matériel moins puissant et optimisé pour la mémoire, comme les unités centrales ou les appareils périphériques. En déplaçant le décodage vers un matériel séparé - potentiellement plus proche de l'utilisateur final, comme les serveurs sur site ou les périphériques - l'inférence désagrégée réduit la latence et les demandes de bande passante du réseau. Cette séparation permet un déploiement flexible, où le pré-remplissage s'exécute sur des serveurs cloud haut de gamme et le décodage sur des dispositifs locaux ou périphériques, optimisant ainsi l'allocation des ressources et permettant une mise à l'échelle efficace pour des applications telles que les chatbots en temps réel ou les systèmes d'IA interactifs.

Conclusion

De nombreuses techniques d'optimisation de l'inférence ont été inventées récemment afin d'améliorer les performances des LLM.

L'implémentation de ces techniques nécessite une compréhension approfondie de l'architecture LLM et du matériel que vous utilisez, il est donc généralement plus facile d'utiliser un moteur d'inférence existant qui a déjà implémenté ces techniques comme vLLM, TensorRT-LLM, LMDeploy, etc. Nous avons en fait implémenté ces techniques dans notre propre moteur d'inférence chez NLP Cloud et nous avons écrit un article de blog sur les moteurs d'inférence si vous souhaitez déployer vos propres modèles : Vous pouvez le lire ici.

Si vous ne pouvez ou ne voulez pas déployer vous-même vos propres LLM, vous pouvez utiliser NLP Cloud et exploiter des modèles d'IA génératifs rapides à l'échelle de la production. Essayez l'inférence rapide sur NLP Cloud dès maintenant !

Si vous avez des questions sur les moteurs d'inférence en général, n'hésitez pas à nous les poser, c'est toujours un plaisir de vous conseiller !

Julien
Directeur technique de NLP Cloud