For the complete documentation index, see llms.txt. Markdown versions of documentation pages are available by appending .md to the page URL.
Navigation principale

Optimisation de la latence

Réduisez la latence dans une grande variété de cas d’utilisation des LLM.

Ce guide présente les principes essentiels pour réduire la latence dans une grande variété de cas d’utilisation des LLM. Ces techniques sont issues de notre travail avec de nombreux clients et développeurs sur des applications en production. Elles devraient donc s’appliquer à vos projets, qu’il s’agisse d’un workflow ciblé ou d’une application de discussion complète.

Les techniques sont nombreuses. Ce guide les regroupe en sept principes qui correspondent aux grandes approches permettant de réduire la latence.

Pour terminer, nous examinerons un exemple afin de voir comment les appliquer.

Sept principes

  1. Traitez les tokens plus rapidement.
  2. Générez moins de tokens.
  3. Utilisez moins de tokens d’entrée.
  4. Effectuez moins de requêtes.
  5. Parallélisez.
  6. Réduisez l’attente des utilisateurs.
  7. Ne recourez pas systématiquement à un LLM.

Traitez les tokens plus rapidement

La vitesse d’inférence est probablement le premier facteur qui vous vient à l’esprit lorsqu’il est question de latence (mais, comme vous le verrez bientôt, c’est loin d’être le seul). Elle correspond au débit réel auquel le LLM traite les tokens et se mesure souvent en TPM (tokens par minute) ou en TPS (tokens par seconde).

Le principal facteur qui influence la vitesse d’inférence est la taille du modèle : les petits modèles sont généralement plus rapides (et moins coûteux) et, bien utilisés, peuvent même surpasser les grands modèles. Pour maintenir des résultats de qualité avec de petits modèles, voici quelques pistes :

Vous pouvez également optimiser l’inférence grâce à des fonctionnalités comme nos Sorties prédites. Les sorties prédites permettent de réduire considérablement la latence de génération lorsque vous connaissez à l’avance l’essentiel de la sortie, par exemple pour les tâches de modification de code. En fournissant une prédiction au modèle, vous permettez au LLM de se concentrer davantage sur les modifications à apporter et moins sur le contenu qui restera identique.

Deep dive
Capacité de calcul et optimisations supplémentaires de l’inférence

Générez moins de tokens

La génération de tokens est presque toujours l’étape qui prend le plus de temps lorsqu’on utilise un LLM. En règle générale, réduire de 50 % le nombre de tokens de sortie peut réduire la latence d’environ 50 %. La manière de réduire la taille de la sortie dépend de son type :

Si vous générez du langage naturel, demander au modèle d’être plus concis (« en moins de 20 mots » ou « soyez bref ») peut être utile. Vous pouvez aussi utiliser des exemples few-shot et/ou l’affinage pour apprendre au modèle à produire des réponses plus courtes.

Si vous générez une sortie structurée, essayez d’en réduire la syntaxe au minimum lorsque c’est possible : raccourcissez les noms de fonctions, omettez les arguments nommés, regroupez les paramètres, etc.

Enfin, bien que ce soit peu courant, vous pouvez aussi utiliser max_tokens ou stop_tokens pour interrompre la génération avant son terme.

Gardez-le à l’esprit : un token de sortie en moins, c’est une (milli)seconde de gagnée !

Utilisez moins de tokens d’entrée

Réduire le nombre de tokens d’entrée diminue bien la latence, mais l’effet est généralement limité : réduire votre prompt de 50 % peut ne diminuer la latence que de 1 à 5 %. À moins de travailler avec des contextes vraiment volumineux (documents, images), il peut être plus judicieux de concentrer vos efforts ailleurs.

Cela dit, si vous travaillez effectivement avec des contextes très volumineux (ou si vous cherchez à gagner le moindre gain de performance et avez épuisé toutes les autres possibilités), vous pouvez utiliser les techniques suivantes pour réduire le nombre de tokens d’entrée :

  • Affinez le modèle pour ne plus avoir à fournir de longues instructions ou de longs exemples.
  • Filtrez le contexte fourni en entrée, par exemple en élaguant les résultats RAG, en nettoyant le HTML, etc.
  • Maximisez la longueur du préfixe commun aux prompts en plaçant les parties dynamiques (par exemple, les résultats RAG et l’historique) plus loin dans le prompt. Votre requête tire ainsi mieux parti du cache KV, utilisé par la plupart des fournisseurs de LLM, et moins de tokens d’entrée sont traités à chaque requête.

Découvrez le fonctionnement de la mise en cache des prompts dans notre documentation.

Effectuez moins de requêtes

Chaque requête entraîne une certaine latence aller-retour. Ces délais peuvent vite s’accumuler.

Si le LLM doit exécuter plusieurs étapes séquentielles, envisagez de les regrouper dans un seul prompt pour obtenir tous les résultats dans une seule réponse, au lieu d’envoyer une requête par étape. Vous éviterez la latence des allers-retours supplémentaires et pourrez aussi simplifier le traitement des réponses.

Pour cela, vous pouvez regrouper les étapes dans une liste numérotée au sein du prompt, puis demander au modèle de renvoyer les résultats dans les champs nommés d’un objet JSON. Vous pourrez ainsi extraire chaque résultat et y accéder individuellement.

Parallélisez

Le traitement parallèle peut être très efficace lorsque vous exécutez plusieurs étapes avec un LLM.

Si les étapes ne sont pas strictement séquentielles, vous pouvez les répartir entre des appels parallèles. Deux chemises mettent autant de temps à sécher qu’une seule.

Si les étapes sont strictement séquentielles, vous pouvez toutefois envisager de recourir à l’exécution spéculative. Cette approche est particulièrement efficace pour les étapes de classification où un résultat est plus probable que les autres (par exemple, la modération).

  1. Lancez simultanément les étapes 1 et 2 (par exemple, la modération de l’entrée et la génération d’une histoire)
  2. Vérifiez le résultat de l’étape 1
  3. Si le résultat n’est pas celui attendu, annulez l’étape 2 (et réessayez si nécessaire)

Si votre hypothèse sur le résultat de l’étape 1 est correcte, vous l’aurez, en pratique, exécutée sans ajouter de latence !

Réduisez l’attente des utilisateurs

Il y a une grande différence entre attendre et voir les choses avancer. Faites en sorte que vos utilisateurs vivent la seconde expérience. Voici quelques techniques :

  • Streaming : c’est l’approche la plus efficace, car elle réduit le temps d’ attente à une seconde ou moins. (L’expérience de ChatGPT serait bien différente si rien ne s’affichait avant la fin de chaque réponse.)
  • Découpage en blocs : si votre sortie nécessite un traitement supplémentaire avant d’être affichée à l’utilisateur (modération, traduction), envisagez de la traiter par blocs plutôt qu’en une seule fois. Pour cela, transmettez-la en streaming à votre backend, puis envoyez les blocs traités à votre frontend.
  • Affichez les étapes : si vous exécutez plusieurs étapes ou utilisez des outils, montrez-le à l’utilisateur. Plus vous rendez l’avancement réel visible, mieux c’est.
  • États de chargement : les indicateurs animés et les barres de progression font déjà une grande différence.

Si l’affichage des étapes et des états de chargement a surtout un effet psychologique, le streaming et le découpage en blocs réduisent réellement la latence globale lorsqu’on considère l’ensemble application et utilisateur : l’utilisateur finit de lire la réponse plus tôt.

Ne recourez pas systématiquement à un LLM

Les modèles de langage sont puissants et polyvalents. Ils sont donc parfois utilisés là où une méthode classique plus rapide conviendrait mieux. Repérer ces situations peut vous permettre de réduire considérablement la latence. Voici quelques exemples :

  • Codage en dur : si votre sortie est soumise à des contraintes très strictes, vous n’avez peut-être pas besoin d’un LLM pour la générer. Les confirmations d’action, les messages de refus et les demandes de saisie courantes se prêtent très bien au codage en dur. (Vous pouvez même reprendre la bonne vieille méthode qui consiste à prévoir quelques variantes pour chacun.)
  • Précalcul : si les possibilités d’ entrée sont limitées (par exemple, la sélection d’une catégorie), vous pouvez générer plusieurs réponses à l’avance et simplement veiller à ne jamais montrer deux fois la même réponse à un utilisateur.
  • Utilisation de l’interface : des composants d’interface classiques, conçus sur mesure, présentent parfois mieux une synthèse de métriques, des rapports ou des résultats de recherche qu’un texte généré par un LLM.
  • Techniques d’optimisation traditionnelles : une application fondée sur un LLM reste une application. La recherche dichotomique, la mise en cache, les tables de hachage et la complexité temporelle restent tout aussi utiles dans un monde de modèles de langage.

Exemple

Examinons maintenant un exemple d’application, repérons les possibilités d’optimisation de la latence et proposons des solutions !

Nous allons analyser l’architecture et les prompts d’un bot de service client fictif, inspiré d’applications réelles en production. La section Architecture et prompts présente le contexte, puis la section Analyse et optimisations détaille la démarche d’optimisation de la latence.

Vous remarquerez que cet exemple ne couvre pas tous les principes, tout comme les cas d’utilisation réels ne nécessitent pas d’appliquer toutes les techniques.

Architecture et prompts

Voici l’ architecture initiale d’un bot de service client fictif. C’est cette architecture que nous allons modifier.

Schéma d’architecture de l’objet Assistants

Dans les grandes lignes, le schéma décrit le processus suivant :

  1. Un utilisateur envoie un message dans une conversation en cours.
  2. Le dernier message est transformé en une requête autonome (voir les exemples dans le prompt).
  3. Nous déterminons si des informations supplémentaires (récupérées) sont nécessaires pour répondre à cette requête.
  4. Une récupération est effectuée et produit des résultats de recherche.
  5. L’assistant raisonne à partir de la requête de l’utilisateur et des résultats de recherche, puis produit une réponse.
  6. La réponse est renvoyée à l’utilisateur.

Voici les prompts utilisés dans chaque partie du schéma. Bien qu’ils soient fictifs et simplifiés, leur structure et leur formulation correspondent à celles que l’on trouve dans une application en production.

Les espaces réservés comme « [user input here] » correspondent à des parties dynamiques, qui seraient remplacées par des données réelles à l’exécution.

Analyse et optimisations

Partie 1 : examen des prompts de récupération

En examinant l’architecture, on remarque d’abord les appels consécutifs à GPT-4 . Ils suggèrent une possible inefficacité et peuvent souvent être remplacés par un appel unique ou des appels parallèles.

Schéma d’architecture de l’objet Assistants

Dans ce cas, puisque la vérification du besoin de récupération nécessite la requête contextualisée, regroupons ces deux étapes dans un seul prompt pour effectuer moins de requêtes.

Schéma d’architecture de l’objet Assistants


En fait, ajouter du contexte et déterminer si une récupération est nécessaire sont des tâches simples et bien définies. Nous pouvons donc probablement utiliser un modèle plus petit et affiné . Passer à GPT-3.5 nous permettra de traiter les tokens plus rapidement.

Schéma d’architecture de l’objet Assistants

Partie 2 : analyse du prompt de l’assistant

Examinons maintenant le prompt de l’assistant. Le remplissage des champs JSON semble comporter de nombreuses étapes distinctes, ce qui pourrait offrir une possibilité de parallélisation.

Schéma d’architecture de l’objet Assistants

Supposons toutefois que nous ayons effectué des tests et constaté que séparer les étapes de raisonnement du JSON dégrade les réponses. Nous devons donc explorer d’autres solutions.

Pourrions-nous utiliser une version affinée de GPT-3.5 à la place de GPT-4 ? Peut-être, mais il vaut généralement mieux confier les réponses ouvertes des assistants à GPT-4, qui gère mieux un plus large éventail de cas. Cela dit, les étapes de raisonnement elles-mêmes ne nécessitent peut-être pas toutes les capacités de raisonnement de GPT-4. Leur périmètre limité et bien défini en fait de bonnes candidates potentielles pour l’affinage.

{
  message_is_conversation_continuation: "True", // <-
  number_of_messages_in_conversation_so_far: "1", // <-
  user_sentiment: "Aggravated", // <-
  query_type: "Hardware Issue", // <-
  response_tone: "Validating and solution-oriented", // <-
  response_requirements: "Propose options for repair or replacement.", // <-
  user_requesting_to_talk_to_human: "False", // <-
  enough_information_in_context: "True", // <-
  response: "...", // X -- benefits from GPT-4
}

Cela nous amène à envisager un compromis. Faut-il conserver une requête unique dont toute la sortie est générée par GPT-4, ou la diviser en deux requêtes séquentielles et utiliser GPT-3.5 pour tout sauf la réponse finale ? Deux principes entrent ici en conflit : la première option permet d’effectuer moins de requêtes, tandis que la seconde pourrait permettre de traiter les tokens plus rapidement.

Comme pour de nombreux compromis en matière d’optimisation, la réponse dépendra des détails. Par exemple :

  • La proportion de tokens dans le champ response par rapport aux autres champs.
  • La réduction moyenne de la latence obtenue en traitant plus rapidement la plupart des champs.
  • L’ augmentation moyenne de la latence liée à l’exécution de deux requêtes au lieu d’une.

La conclusion variera selon les cas, et le meilleur moyen de trancher est de tester cette approche sur des exemples issus de la production. Ici, supposons que les tests aient montré qu’il est avantageux de diviser le prompt en deux pour traiter les tokens plus rapidement.

Schéma d’architecture de l’objet Assistants

Remarque : Nous regrouperons response et enough_information_in_context dans le second prompt pour éviter de transmettre le contexte récupéré aux deux nouveaux prompts.


En fait, puisque le prompt de raisonnement ne dépend plus du contexte récupéré, nous pouvons paralléliser son exécution et le lancer en même temps que les prompts de récupération.

Schéma d’architecture de l’objet Assistants

Partie 3 : optimisation de la sortie structurée

Revenons sur le prompt de raisonnement.

Schéma d’architecture de l’objet Assistants

En examinant de plus près le JSON du raisonnement, vous remarquerez peut-être que les noms des champs eux-mêmes sont assez longs.

{
  message_is_conversation_continuation: "True", // <-
  number_of_messages_in_conversation_so_far: "1", // <-
  user_sentiment: "Aggravated", // <-
  query_type: "Hardware Issue", // <-
  response_tone: "Validating and solution-oriented", // <-
  response_requirements: "Propose options for repair or replacement.", // <-
  user_requesting_to_talk_to_human: "False", // <-
}

En les raccourcissant et en déplaçant les explications dans les commentaires, nous pouvons générer moins de tokens.

{
  cont: "True", // whether last message is a continuation
  n_msg: "1", // number of messages in the continued conversation
  tone_in: "Aggravated", // sentiment of user query
  type: "Hardware Issue", // type of the user query
  tone_out: "Validating and solution-oriented", // desired tone for response
  reqs: "Propose options for repair or replacement.", // response requirements
  human: "False", // whether user is expressing want to talk to human
}

Schéma d’architecture de l’objet Assistants

Cette petite modification a supprimé 19 tokens de sortie. Avec GPT-3.5, le gain peut se limiter à quelques millisecondes, mais avec GPT-4, il pourrait atteindre une seconde.

Schéma d’architecture de l’objet Assistants

Vous pouvez toutefois imaginer à quel point cette optimisation peut avoir un effet significatif lorsque le modèle génère des sorties plus longues.

Nous pourrions aller plus loin et utiliser un seul caractère pour chaque nom de champ JSON, ou tout placer dans un tableau, mais cela pourrait commencer à dégrader la qualité des réponses. Encore une fois, le meilleur moyen de le savoir est de faire des tests.

Bilan de l’exemple

Passons en revue les optimisations que nous avons mises en œuvre dans l’exemple du bot de service client :

Schéma d’architecture de l’objet Assistants

  1. Nous avons regroupé les étapes de contextualisation de la requête et de vérification du besoin de récupération pour réduire le nombre de requêtes.
  2. Pour le nouveau prompt, nous sommes passés à GPT-3.5, un modèle plus petit, que nous avons affiné pour traiter les tokens plus rapidement.
  3. Nous avons scindé le prompt de l’assistant en deux, en passant à GPT-3.5, un modèle plus petit, que nous avons affiné pour le raisonnement, là encore pour traiter les tokens plus rapidement.
  4. Nous avons parallélisé les vérifications du besoin de récupération et les étapes de raisonnement.
  5. Nous avons raccourci les noms des champs de raisonnement et déplacé les commentaires dans le prompt pour générer moins de tokens.