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
- Traitez les tokens plus rapidement.
- Générez moins de tokens.
- Utilisez moins de tokens d’entrée.
- Effectuez moins de requêtes.
- Parallélisez.
- Réduisez l’attente des utilisateurs.
- 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 :
- l’utilisation d’un prompt plus long et plus détaillé,
- l’ajout d’exemples few-shot, ou l’augmentation de leur nombre,
- l’affinage / la distillation.
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.
La vitesse d’inférence dépend également des ressources de
calcul disponibles et des éventuelles
optimisations supplémentaires de l’inférence que vous utilisez.
La plupart des utilisateurs ne peuvent pas agir directement sur ces facteurs. Mais si le sujet vous intéresse et que
vous avez une certaine maîtrise de votre infrastructure, du matériel plus rapide ou
des moteurs fonctionnant à un niveau de saturation plus faible peuvent légèrement augmenter
votre débit en TPM. Et si vous travaillez au plus près de l’infrastructure, il existe une multitude d’autres
optimisations de l’inférence
qui dépassent quelque peu le cadre de ce guide.
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).
- 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)
- Vérifiez le résultat de l’étape 1
- 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.

Dans les grandes lignes, le schéma décrit le processus suivant :
- Un utilisateur envoie un message dans une conversation en cours.
- Le dernier message est transformé en une requête autonome (voir les exemples dans le prompt).
- Nous déterminons si des informations supplémentaires (récupérées) sont nécessaires pour répondre à cette requête.
- Une récupération est effectuée et produit des résultats de recherche.
- L’assistant raisonne à partir de la requête de l’utilisateur et des résultats de recherche, puis produit une réponse.
- 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.

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.

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.

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.

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
responsepar 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.

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.

Partie 3 : optimisation de la sortie structurée
Revenons sur le prompt de raisonnement.

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
}
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.

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 :

- 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.
- 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.
- 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.
- Nous avons parallélisé les vérifications du besoin de récupération et les étapes de raisonnement.
- Nous avons raccourci les noms des champs de raisonnement et déplacé les commentaires dans le prompt pour générer moins de tokens.