Améliorez les performances des modèles Falcon avec Amazon SageMaker

Optimisez les performances des modèles Falcon avec Amazon SageMaker

Quel est le framework et la configuration optimale pour l’hébergement de grands modèles de langage (LLMs) destinés aux applications d’intelligence artificielle générative de génération de texte ? Malgré l’abondance d’options pour servir les LLMs, il est difficile de répondre à cette question en raison de la taille des modèles, des architectures de modèles variables, des exigences de performance des applications, et plus encore. Le conteneur Amazon SageMaker pour l’inférence de grands modèles (LMI) facilite le service des LLMs en regroupant différents frameworks et techniques qui optimisent le déploiement des LLMs. Le conteneur LMI dispose d’un puissant ensemble de services appelé DJL serving qui est indépendant du LLM sous-jacent. Il fournit des paramètres de configuration au niveau du système qui peuvent être ajustés pour obtenir les meilleures performances pour l’infrastructure d’hébergement d’un LLM donné. Il prend également en charge des optimisations récentes telles que le traitement continu par lots, également connu sous le nom de traitement itératif par lots ou de traitement par lots en continu, ce qui permet d’améliorer considérablement le débit.

Dans un précédent article, nous avons montré comment vous pouvez utiliser le conteneur LMI pour déployer la famille de modèles Falcon sur SageMaker. Dans cet article, nous démontrons comment améliorer le débit et la latence du service Falcon-40B en utilisant des techniques telles que le traitement continu par lots. Nous fournissons également une compréhension intuitive des paramètres de configuration fournis par le conteneur SageMaker LMI qui peuvent vous aider à trouver la meilleure configuration pour votre application du monde réel.

Fondamentaux de l’inférence générative de texte pour les LLMs

Commençons par examiner quelques fondamentaux sur la façon de réaliser une inférence pour les LLMs destinés à la génération de texte.

Passage direct, activations et cache KV

Étant donné une séquence d’entrée de jetons, ils sont exécutés dans un passage direct à travers toutes les couches du LLM (comme Falcon) pour générer le jeton suivant. Un passage direct fait référence au processus par lequel les données d’entrée sont transmises à un réseau neuronal pour produire une sortie. Dans le cas de la génération de texte, le passage direct implique l’alimentation d’une graine initiale ou d’un contexte dans le modèle de langage et la génération du caractère ou du jeton suivant dans la séquence. Pour générer une séquence de texte, le processus est souvent effectué de manière itérative, c’est-à-dire qu’il est répété pour chaque étape ou position dans la séquence de sortie. À chaque itération, le modèle génère le caractère ou le jeton suivant, qui devient partie intégrante du texte généré, et ce processus continue jusqu’à ce que la longueur de texte souhaitée soit générée.

La génération de texte avec des modèles de langage comme Falcon ou GPT est auto-régressive. Cela signifie que le modèle génère un jeton à la fois en se basant sur les jetons précédemment générés. En d’autres termes, à chaque itération, le modèle prend le texte précédemment généré en entrée et prédit le jeton suivant en se basant sur ce contexte. Comme mentionné dans vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention, dans ce processus de décodage auto-régressif, tous les jetons d’entrée du LLM produisent leurs tenseurs de clés et de valeurs d’attention, et ces tenseurs sont conservés en mémoire GPU pour générer les prochains jetons. Ces tenseurs de clés et de valeurs mis en cache sont souvent appelés le cache KV.

Phases de pré-remplissage et de décodage

Dans un processus de décodage auto-régressif, comme celui utilisé dans la génération de texte avec des modèles de langage tels que Falcon, il y a généralement deux phases principales : la phase de pré-remplissage et la phase de décodage. Ces phases sont essentielles pour générer un texte cohérent et contextuellement pertinent.

La phase de pré-remplissage comprend les éléments suivants :

  • Contexte initial – La phase de pré-remplissage commence par un contexte initial ou un texte de départ fourni par l’utilisateur. Ce contexte initial peut être une phrase, une expression ou même un seul mot. Il définit le point de départ de la génération de texte et fournit un contexte pour la suite.
  • Conditionnement du modèle – Le contexte fourni est utilisé pour conditionner le modèle de langage. Le modèle utilise ce contexte en entrée et génère le jeton (mot ou caractère) suivant dans la séquence en fonction de sa compréhension du contexte.
  • Génération de jetons – Le modèle génère un jeton à la fois, prédisant ce qui devrait venir ensuite dans le texte. Ce jeton est ajouté au contexte, l’étendant ainsi de manière effective.
  • Processus itératif – Le processus de génération de jetons est répété de manière itérative. À chaque étape, le modèle génère un jeton en tenant compte du contexte mis à jour, qui inclut maintenant les jetons générés lors des étapes précédentes.

La phase de pré-remplissage se poursuit jusqu’à ce qu’une condition d’arrêt prédéterminée soit atteinte. Cette condition peut être une longueur maximale pour le texte généré, un jeton spécifique indiquant la fin du texte, ou tout autre critère défini par l’utilisateur ou l’application.

La phase de décodage comprend les étapes suivantes:

  • Complétion – Après la phase de pré-remplissage, vous disposez d’un texte partiellement généré qui peut être incomplet ou coupé à un certain moment. La phase de décodage est responsable de la complétion du texte pour le rendre cohérent et grammaticalement correct.
  • Continuation depuis le dernier jeton – Dans la phase de décodage, le modèle part du dernier jeton généré lors de la phase de pré-remplissage. Il utilise ce jeton comme contexte initial et génère le jeton suivant pour poursuivre le texte.
  • Complétion itérative – Tout comme dans la phase de pré-remplissage, le processus de génération de jetons est à nouveau itératif. Le modèle génère un jeton à la fois, en se basant sur les jetons précédents dans la séquence.
  • Condition d’arrêt – La phase de décodage a également une condition d’arrêt, qui peut être la même que dans la phase de pré-remplissage, telle que l’atteinte d’une longueur maximale ou la rencontre d’un jeton de fin de texte. Lorsque cette condition est atteinte, le processus de génération s’arrête.

La combinaison des phases de pré-remplissage et de décodage permet aux modèles autorégressifs de générer du texte qui repose sur un contexte initial et produit des séquences de texte cohérentes, pertinentes et contextuellement cohérentes.

Consultez Un système de service distribué pour les modèles génératifs basés sur les “Transformer” pour une explication détaillée du processus.

Optimisation du débit à l’aide du regroupement dynamique

Jusqu’à présent, nous n’avons parlé que d’une seule entrée. En pratique, nous nous attendons à traiter plusieurs demandes simultanées ou de manière échelonnée provenant des clients d’application pour des inférences. De manière traditionnelle, la mise en lots de base peut être utilisée pour augmenter le débit et l’utilisation des ressources de calcul du GPU. Le regroupement dynamique consiste à combiner de manière efficace les représentations numériques de plus d’une demande dans un lot et à effectuer des exécutions parallèles des passes avant autorégressives. Ce regroupement intelligent est effectué côté serveur. Le serveur DJLServing de SageMaker peut être configuré pour regrouper plusieurs demandes afin de les traiter en parallèle en définissant les paramètres suivants dans serving.properties:

  • max_batch_delay = 100 – Le délai maximum pour l’agrégation par lot en millisecondes. La valeur par défaut est de 100 millisecondes.
  • batch_size = 32 – La taille dynamique du lot. La valeur par défaut est de 1.

Cela montre essentiellement que DJLServing mettra en file d’attente les demandes pendant 100 millisecondes à la fois ou si le nombre de demandes mises en file d’attente atteint la taille de lot spécifiée, le lot sera planifié pour s’exécuter à l’arrière-plan pour l’inférence. Cela s’appelle le regroupement dynamique. Il est dynamique car la taille du lot peut varier d’un lot à l’autre en fonction du nombre de demandes ajoutées pendant cette période de temps. Cependant, parce que les demandes peuvent avoir des caractéristiques différentes, (par exemple, certaines demandes peuvent être de forme 20 jetons en entrée et 500 jetons en sortie, tandis que d’autres peuvent être inversées, avec 500 jetons en entrée mais seulement 20 en sortie), certaines demandes peuvent terminer le traitement plus rapidement que d’autres dans le même lot. Cela pourrait conduire à une sous-utilisation du GPU en attendant que toutes les demandes en cours d’exécution dans le lot terminent leur phase de décodage, même s’il y a d’autres demandes en attente d’être traitées dans la file d’attente. Le diagramme suivant illustre ce processus.

Simple Dynamic Batching Visual

Dynamic Batching Visual – remarquez les fenêtres inactives à la fin des demandes 2 et 3

Optimiser le débit en utilisant le batching continu

Avec le batching continu, également connu sous le nom de batching itératif ou roulant, nous tirons parti des différences entre les étapes de pré-remplissage et de décodage. Pour activer le batching continu, DJServing fournit les configurations supplémentaires suivantes selon le serving.properties:

  • engine=MPI – Nous vous encourageons à utiliser le moteur MPI pour le batching continu.
  • option.rolling_batch=auto ou lmi-dist – Nous recommandons d’utiliser auto car il choisira automatiquement l’algorithme de batch roulant le plus approprié ainsi que d’autres optimisations à l’avenir.
  • option.max_rolling_batch_size=32 – Cela limite le nombre de demandes concurrentes. La valeur par défaut est 32.

Avec le batching continu, la pile de services (DJLServing) n’attend pas que toutes les demandes en cours dans un lot complètent leur étape de décodage. Au lieu de cela, aux points de rupture logiques (à la fin d’une itération de l’étape de décodage), il récupère les demandes supplémentaires qui attendent dans la file d’attente tandis que le lot actuel est encore en cours de traitement (d’où le nom de batch roulant). Il effectue cette vérification pour les demandes en attente à la fin de chaque itération de l’étape de décodage. Rappelez-vous, pour chaque demande, nous devons exécuter l’étape de pré-remplissage suivie de l’étape de décodage séquentielle. Étant donné que nous pouvons traiter tous les jetons issus de la demande initiale en parallèle pour son étape de pré-remplissage, chaque fois qu’une nouvelle demande est ajoutée, nous mettons temporairement en pause l’étape de décodage des demandes en cours du lot – nous sauvegardons temporairement son cache KV et ses activations en mémoire et exécutons l’étape de pré-remplissage des nouvelles demandes.

La taille de ce cache peut être configurée avec l’option suivante:

  • option.max_rolling_batch_prefill_tokens=1024 – Limite le nombre de jetons de pré-remplissage simultanés enregistrés dans le cache pour le lot roulant (entre les étapes de décodage et de pré-remplissage)

Lorsque le pré-remplissage est terminé, nous combinons les nouvelles demandes et les anciennes demandes en pause dans un nouveau lot roulant, qui peut procéder à leur étape de décodage en parallèle. Notez que les anciennes demandes en pause peuvent reprendre leur étape de décodage là où elles se sont arrêtées et que les nouvelles demandes commenceront par leur premier nouveau jeton.

Batching continu ou itératif

Vous avez peut-être déjà réalisé que le batching continu est une approche presque similaire à celle avec laquelle nous parallélisons naturellement les tâches dans notre vie quotidienne. Nous avons des messages, des courriels, des notifications téléphoniques (potentiellement de nouvelles demandes) qui arrivent à des moments aléatoires (analogues à des demandes multiples arrivant de manière aléatoire pour les GPU). Tout cela se produit pendant que nous accomplissons nos tâches en cours – composer des courriels, coder, participer à des réunions (analogues aux tâches en cours de traitement dans les GPU). À des points de rupture logiques, nous mettons en pause nos tâches en cours et vérifions nos notifications pour décider s’il y a une action requise de notre part, et s’il y en a, nous l’ajoutons à nos tâches en cours (lot roulant de la vie réelle), ou nous le mettons sur une liste de tâches à faire (la file d’attente).

Mettre tout ensemble: Comment penser à l’utilisation de la mémoire des GPUs

Il est recommandé de tester vos modèles pour voir quelle configuration est la plus rentable pour votre cas d’utilisation professionnelle. Pour comprendre, visualisons l’empreinte mémoire des GPUs lorsque le modèle est chargé et que les requêtes successives sont traitées par lots. Pour cet article, supposons que nous chargeons le modèle Falcon-40B sur l’un des types d’instance G5, qui sont équipées de GPUs NVIDIA A10G, avec une mémoire de 24 Go chacun. Notez qu’une compréhension similaire s’applique aux types d’instance p3, p4 et p5, qui sont livrés avec les séries de GPU V100, A100 et H100.

Voici un aperçu de l’obtention d’une valeur approximative de la mémoire totale requise pour servir Falcon-40B:

  • Taille du modèle = Nombre de paramètres du modèle (40 milliards pour Falcon-40B) x 4 octets par paramètre (pour FP32) = 160 Go
  • Mémoire totale approximative requise pour charger Falcon-40B pour l’inférence = Taille du modèle (160 Go) + Cache KV (Attention Cache) (=*20 Go) + Mémoire supplémentaire ajoutée par les frameworks d’IA (environ 2 Go)
Visualisation de la mémoire

Visualisation de la mémoire – Comprendre l’empreinte mémoire d’un modèle Falcon-40B chargé

Pour Falcon-40B, si nous compressons le modèle en quantifiant le modèle en utilisant le type de données bfloat16 (2 octets), la taille du modèle devient d’environ 80 Go. Comme vous pouvez le voir, cela est toujours supérieur à la mémoire prise en charge par un seul dispositif d’accélération, nous devons donc adopter une technique de partitionnement (fragmentation) du modèle avec une approche spéciale de parallélisme tensoriel (TP) et fragmenter le modèle sur plusieurs dispositifs d’accélération. Supposons que nous ayons choisi g5.24xlarge, qui dispose de 4 GPU A10G. Si nous configurons DJLServing (serving.properties) avec les éléments suivants, nous pouvons nous attendre à ce que les 80 Go de poids du modèle soient répartis également sur les 4 GPUs:

  • option.tensor_parallel_degree%0Aoption.-,tensor_parallel_degree,-%3D2%0A%23%20specify%20per) = 4 ou 8, ou utilisez max (maximum de GPUs détectés sur l’instance)

Avec tensor_parallel_degree réglé sur 4, environ 20 Go des 24 Go de mémoire GPU (environ 84%) sont déjà utilisés avant même qu’une seule requête ne soit traitée. Les 16% restants du GPU seront utilisés pour le cache KV des requêtes entrantes. Il est possible que pour votre scénario d’entreprise, avec ses besoins de latence et de débit, 2-3 Go de mémoire restante soit largement suffisant. Sinon, vous pouvez augmenter la taille de l’instance à g5.48xlarge, qui dispose de 8 GPUs et utilise tensor_parallel_degree réglé sur 8. Dans ce cas, environ 10 Go des 24 Go disponibles de chaque GPU sont utilisés pour les poids du modèle et nous avons environ 60% du GPU restant pour les activations et le cache KV. Intuitivement, nous pouvons voir que cette configuration peut nous permettre d’obtenir un débit plus élevé. De plus, parce que nous avons un tampon plus important maintenant, nous pouvons augmenter les paramètres max_rolling_batch_prefill_tokens et max_rolling_batch_size pour optimiser davantage le débit. Ensemble, ces deux paramètres contrôleront les préallocations des remplissages d’activation et du cache KV pour le modèle. Un plus grand nombre pour ces deux paramètres se traduira par un débit plus élevé, à condition que vous disposiez d’un tampon suffisant pour le cache KV dans la mémoire GPU.

Le traitement continu par lots avec PagedAttention

PagedAttention est un nouvel algorithme d’optimisation développé par l’UC Berkeley qui améliore le processus de traitement continu par lots en permettant à la mémoire cache d’attention (mémoire clef-valeur) d’être non contiguë en allouant la mémoire en pages ou en blocs de taille fixe. Cela est inspiré par les concepts de mémoire virtuelle et de pagination utilisés par les systèmes d’exploitation.

Comme indiqué dans l’article vLLM, la mémoire cache d’attention de chaque séquence de jetons est partitionnée en blocs et est associée à des blocs physiques grâce à une table de blocs. Lors du calcul de l’attention, un noyau PagedAttention peut utiliser la table de blocs pour extraire efficacement les blocs de la mémoire physique. Cela permet une réduction significative du gaspillage de mémoire, ainsi qu’une possibilité d’utiliser une taille de lot plus élevée, une utilisation accrue du GPU et un débit plus élevé.

Comparaison des performances

Pour assurer un test de charge efficace de votre configuration de déploiement, il est recommandé de commencer par prendre en compte le scénario commercial et définir clairement les caractéristiques des entrées et des sorties pour l’application basée sur LLM. Par exemple, si vous travaillez sur un cas d’utilisation de résumé de centre d’appels, l’entrée peut consister en un texte plus long, comme une transcription de chat de 500 jetons entre un agent du service client et un client, mais la sortie peut être relativement plus petite, d’environ 100 jetons, représentant un résumé de la transcription. D’autre part, si vous travaillez sur un scénario de génération de code, l’entrée peut être aussi courte que 15 jetons, comme “écrivez une implémentation efficace en Python pour décrire toutes les ressources EC2, y compris la pagination”, mais la sortie peut être beaucoup plus grande, atteignant 500 jetons. Il est également important de déterminer si l’obtention d’une latence réduite ou la maximisation du débit est la priorité absolue pour votre scénario spécifique.

Après avoir acquis une compréhension approfondie du scénario commercial, vous pouvez analyser et déterminer la configuration optimale pour votre environnement d’hébergement. Dans ce contexte, l’environnement d’hébergement comprend plusieurs éléments clés, tels que le type d’instance et d’autres paramètres de configuration tels que tensor_parallel_degree, max_rolling_batch_size, max_rolling_batch_prefill_tokens, et plus encore. Notre objectif est d’identifier la configuration la plus efficace pour répondre aux exigences de temps de réponse, de débit et de qualité de sortie du modèle.

Dans notre analyse, nous avons mesuré les performances pour illustrer les avantages du traitement continu par lots par rapport au traitement par lots dynamique traditionnel. Nous avons utilisé les configurations détaillées dans le tableau suivant dans serving.properties pour le traitement par lots dynamique et le traitement par lots itératif, en utilisant un conteneur LMI sur SageMaker.

Traitement par lots dynamique Traitement continu par lots Traitement continu par lots avec PagedAttention

moteur=Python

option.model_id=tiiuae/falcon-40b

option.tensor_parallel_degree=8

option.dtype=fp16

batch_size=4

max_batch_delay=100

option.trust_remote_code = true

moteur=MPI

option.model_id = {{s3_url}}

option.trust_remote_code = true

option.tensor_parallel_degree = 8

option.max_rolling_batch_size = 32

option.rolling_batch = auto

option.dtype = fp16

option.max_rolling_batch_prefill_tokens = 1024

option.paged_attention = False

moteur=MPI

option.model_id = {{s3_url}}

option.trust_remote_code = true

option.tensor_parallel_degree = 8

option.max_rolling_batch_size = 32

option.rolling_batch = auto

option.dtype = fp16

option.max_rolling_batch_prefill_tokens = 1024

option.paged_attention = True

Les deux configurations ont été évaluées pour le Falcon-40B avec le type de données FP16 déployé sur ml.g5.48xlarge dans plusieurs scénarios différents représentant des applications réelles :

  • Un petit nombre de jetons d’entrée avec un grand nombre de jetons générés – Dans ce scénario, le nombre de jetons d’entrée était fixé à 32 et 128 nouveaux jetons ont été générés
Stratégie de mise en lots Débit (jetons/sec) Latence p90 (secs)
Mise en lots dynamique 5,53 58,34
Mise en lots continue 56,04 4,74
Mise en lots continue avec PagedAttention 59,18 4,76
  • Une grande entrée avec un petit nombre de jetons générés – Ici, nous fixons le nombre de jetons d’entrée à 256 et demandons au LLM de résumer l’entrée en 32 jetons
Stratégie de mise en lots Débit (jetons/sec) Latence p90 (secs)
Mise en lots dynamique 19,96 59,31
Mise en lots continue 46,69 3,88
Mise en lots continue avec PagedAttention 44,75 2,67

Nous pouvons voir que la mise en lots continue avec PagedAttention offre une amélioration du débit 10 fois supérieure dans le scénario 1 et 2,3 fois supérieure dans le scénario 2 par rapport à l’utilisation de la mise en lots dynamique sur SageMaker avec le conteneur LMI.

Conclusion

Dans cet article, nous avons examiné comment les LLM utilisent la mémoire et expliqué comment la mise en lots continue améliore le débit en utilisant un conteneur LMI sur SageMaker. Nous avons démontré les avantages de la mise en lots continue pour le Falcon-40B en utilisant un conteneur LMI sur SageMaker en montrant les résultats des tests. Vous pouvez trouver le code sur le repo GitHub.

We will continue to update IPGirl; if you have any questions or suggestions, please contact us!

Share:

Was this article helpful?

93 out of 132 found this helpful

Discover more

AI

5 millionaires utilisant ChatGPT

Voici comment certaines des personnes les plus réussies de la planète utilisent ChatGPT.

AI

Découvrez MovieChat un système novateur de compréhension vidéo qui intègre des modèles de base vidéo et de grands modèles linguistiques.

Les grands modèles de langage (LLM) ont récemment réalisé d’énormes progrès dans le secteur du traitement du la...

AI

Découvrez Prompt Diffusion un cadre d'IA pour permettre l'apprentissage en contexte dans les modèles génératifs basés sur la diffusion.

Les modèles de langage de grande envergure de pointe (LLMs), tels que BERT, GPT-2, BART, T5, GPT-3 et GPT-4, ont été ...

AI

Machine de rêve de l'IA DejaVu réduit les coûts des conversations de l'IA sans perdre son esprit.

Former un modèle de langage de grande taille nécessite des ressources informatiques importantes, notamment des GPU et...