Pourquoi le premier token est lent : prefill ou decode ?
Le prefill, limité par le calcul, détermine le time to first token. Le decode, limité par la mémoire, détermine les tokens/s. Mesurez-les séparément.
Prefill et decode, en un paragraphe
La distinction entre prefill et decode explique la plupart des questions de latence concernant un LLM (large language model) auto-hébergé. Le prefill lit l’intégralité du prompt en un seul passage et dépend des capacités de calcul. Le decode génère la réponse un token à la fois et dépend de la bande passante mémoire. Le time to first token est une mesure du prefill. Le nombre de tokens par seconde est une mesure du decode.
Les deux phases s’exécutent sur le même GPU (graphics processing unit), avec les mêmes poids, au sein du même processus. Il est donc naturel de les considérer comme une seule charge de travail. En pratique, elles se comportent comme deux programmes différents qui partagent le même périphérique. Si vous les séparez, une longue liste de résultats difficiles à interpréter devient beaucoup plus claire.
Pourquoi le prefill est-il limité par le calcul ?
Le prefill fait passer l’intégralité du prompt dans chaque couche, une fois. Avec un prompt de 2,000 tokens, chaque multiplication matricielle doit traiter 2,000 lignes de calcul. Le GPU effectue donc beaucoup d’opérations arithmétiques pour chaque octet de poids chargé. Ce rapport entre les opérations arithmétiques et les octets transférés s’appelle l’intensité arithmétique. Elle est élevée pendant le prefill. Le GPU fonctionne alors près de sa limite de calcul, tandis que le bus mémoire reste disponible.
Le prefill produit deux éléments : le cache KV (les tenseurs key et value) pour chaque token du prompt, ainsi que le premier token de sortie. Rien n’est transmis au lecteur avant la fin de cette passe. C’est pourquoi le temps du prefill et le time to first token (TTFT) correspondent presque à la même mesure.
Le coût du prefill augmente avec la longueur du prompt. La partie linéaire correspond aux calculs matriciels effectués dans chaque couche. La partie quadratique correspond à l’attention : chaque token porte sur tous les tokens précédents. Elle devient importante avec les contextes longs. Doubler le prompt double donc au minimum le TTFT.
Vous pouvez l’observer en une minute. Envoyez à votre serveur un prompt de 200 tokens, puis un prompt de 2,000 tokens, en demandant chaque fois le même nombre de tokens de sortie. Le TTFT augmente fortement. La vitesse de streaming après le premier token varie à peine.
Pourquoi le decode est-il limité par la bande passante mémoire ?
Le decode produit un token par étape. Pour produire ce token, le GPU doit lire tous les poids du modèle depuis la mémoire, utiliser chaque poids pour quelques opérations, puis le supprimer. L’intensité arithmétique est proche de 1. Les unités de calcul passent donc la plupart de leur temps à attendre.
Le decode est lent, car chaque token nécessite de lire tout le modèle depuis la mémoire. Le bus mémoire impose donc le rythme et les unités de calcul restent inactives.
La limite théorique de la vitesse de decode sur un flux unique se calcule donc simplement. Divisez la bande passante mémoire par l’espace occupé par les poids.
The data behind this chart
[
{
"device": "CPU, dual channel DDR5-5600",
"mem_bandwidth_gb_s": 90,
"decode_ceiling_tok_s": 6
},
{
"device": "NVIDIA A10G",
"mem_bandwidth_gb_s": 600,
"decode_ceiling_tok_s": 38
},
{
"device": "NVIDIA L40S",
"mem_bandwidth_gb_s": 864,
"decode_ceiling_tok_s": 54
},
{
"device": "NVIDIA RTX 4090",
"mem_bandwidth_gb_s": 1008,
"decode_ceiling_tok_s": 63
},
{
"device": "NVIDIA A100 80GB SXM",
"mem_bandwidth_gb_s": 2039,
"decode_ceiling_tok_s": 127
},
{
"device": "NVIDIA H100 SXM",
"mem_bandwidth_gb_s": 3350,
"decode_ceiling_tok_s": 209
}
]La colonne de bande passante contient la valeur publiée par chaque constructeur. La colonne de plafond correspond à cette valeur divisée par 16 GB, soit la taille d’un modèle de 8 milliards de paramètres stocké en précision 16 bits. Il s’agit d’un calcul, pas d’un résultat de benchmark. Votre débit mesuré sera inférieur. Il est utile de savoir dans quelle mesure, car cela indique si vous devez corriger votre stack de serving ou votre matériel.
Lisez les 6 lignes dans l’ordre : la tendance est claire. Un CPU équipé de DDR5 en dual channel atteint environ 90 GB/s, ce qui plafonne le decode à environ 6 tokens par seconde pour ce modèle. Une L40S atteint environ 54. Une H100 SXM, avec une bande passante publiée de 3350 GB/s, se situe autour de 209.
C’est aussi pourquoi la quantification est le levier individuel le plus efficace pour accélérer le decode. Stockez le même modèle en 8 bits au lieu de 16 bits : vous divisez par deux le nombre d’octets lus par token, et le plafond double environ. Vous n’avez ajouté aucun calcul. Vous avez déplacé moins de données en mémoire.
Comment mesurer chaque phase sur votre propre serveur ?
Ollama renvoie la séparation dans le corps de la réponse. Demandez une complétion sans streaming et lisez les compteurs.
curl -s http://localhost:11434/api/generate -d '{
"model": "llama3.2",
"prompt": "Explain memory bandwidth in two sentences.",
"stream": false
}' | jq '{prompt_eval_count, prompt_eval_duration, eval_count, eval_duration}'Utilisez un model tag que vous avez réellement téléchargé, ce que ollama list vous indiquera. prompt_eval_count et prompt_eval_duration concernent le prefill : le nombre de tokens du prompt et le temps qui lui est consacré. eval_count et eval_duration concernent le decode. Les durées sont exprimées en nanosecondes ; la vitesse de decode est donc eval_count / eval_duration * 1e9 et la vitesse de prefill prompt_eval_count / prompt_eval_duration * 1e9. La vitesse de prefill doit être nettement supérieure à celle du decode pour une même requête. Tout le reste de cette section explique cet écart.
Pour un serveur compatible avec l’API OpenAI, comme vLLM, curl peut mesurer le délai avant le premier octet.
curl -N -s -o /dev/null \
-w 'pretransfer %{time_pretransfer}s first_byte %{time_starttransfer}s\n' \
http://localhost:8000/v1/completions \
-H 'Content-Type: application/json' \
-d '{"model": "meta-llama/Llama-3.1-8B-Instruct", "prompt": "Explain memory bandwidth.", "max_tokens": 128, "stream": true}'time_starttransfer correspond à l’instant où le premier octet du corps est arrivé. Avec "stream": true, il inclut donc le TTFT et l’établissement de la connexion. Soustrayez time_pretransfer pour supprimer le coût de l’établissement de la connexion. Exécutez la commande deux fois et conservez le deuxième résultat, car le premier appel peut inclure le chargement à froid du modèle.
vLLM publie également cette séparation sous forme de métriques Prometheus sur /metrics. Exécutez curl -s http://localhost:8000/metrics | grep -E 'time_to_first_token|inter_token_latency' pour obtenir les histogrammes vllm:time_to_first_token_seconds et vllm:inter_token_latency_seconds. Ajoutez vllm:num_requests_running et vllm:num_requests_waiting pour la profondeur de la file d’attente, ainsi que vllm:kv_cache_usage_perc pour la pression exercée sur le cache. Ces cinq noms suffisent pour constituer le dashboard.
Sous charge, vllm bench serve --model <name> --num-prompts 200 --request-rate 4 sollicite le serveur en fonctionnement et indique le délai avant le premier token ainsi que la latence par token de sortie avec des percentiles. C’est la seule façon d’observer les deux phases se concurrencer. Avant toute optimisation, établissez une baseline propre : la méthode présentée dans mesurer les tokens par seconde sur un LLM local vous en fournit une qui reste valable après un redémarrage.
Pourquoi un long system prompt retarde-t-il le premier token, mais pas la vitesse de streaming ?
Parce que le system prompt correspond au travail de prefill, et à rien d’autre. Il est traité une seule fois, lors du même passage que le reste du prompt, avant l’apparition du premier token. Après ce passage, il n’existe plus que sous forme d’entrées du KV cache, que le décodage lit avec le reste. Ainsi, un system prompt de 3,000 tokens augmente le TTFT à chaque requête, tout en laissant le nombre de tokens par seconde presque inchangé.
Presque, mais pas exactement. Ces entrées supplémentaires du KV cache sont relues à chaque étape du décodage. Un prompt très long ralentit donc légèrement le décodage. La section suivante explique ce point.
La solution consiste à éviter de recalculer le même préfixe. Un serveur avec prefix caching conserve le KV cache d’un préfixe partagé et le réutilise. La deuxième requête contenant le même system prompt ignore donc entièrement cette partie du prefill. vLLM appelle cela automatic prefix caching ; vérifiez vllm serve --help pour votre version, car la valeur par défaut a changé selon les releases. Ce KV cache en GPU est différent du prompt cache facturé par un fournisseur d’API. La différence entre un KV cache et un prompt cache mérite d’être lue avant de régler l’un ou l’autre.
Pourquoi le décodage ralentit-il à mesure que le contexte se remplit ?
Deux raisons, toutes deux liées au cache KV.
La première est la bande passante. À chaque étape du décodage, le mécanisme d’attention lit les clés et les valeurs de tous les tokens précédents. Les poids représentent un coût fixe par token. Le cache KV, lui, augmente au fil du contexte. Vous pouvez calculer sa taille à partir de config.json du modèle : le nombre d’octets par token est égal à 2 multiplié par num_hidden_layers, par num_key_value_heads, par la dimension des têtes (hidden_size divisé par num_attention_heads), puis par le nombre d’octets par élément. Le premier 2 correspond à une clé et une valeur.
Pour une configuration courante de 8 milliards de paramètres, avec 32 couches, 8 têtes de clés et de valeurs sous GQA (grouped query attention), une dimension de tête de 128 et une précision de 16 bits, on obtient 2 x 32 x 8 x 128 x 2 = 131,072 octets, soit environ 128 KiB par token. Une conversation de 8,000 tokens utilise donc environ 1 GB de cache KV par requête.
La seconde est la capacité. Ce 1 GB correspond à de la mémoire qui ne peut pas contenir les poids ni le contexte d’un autre utilisateur. Le serveur dimensionne son pool KV une seule fois au démarrage, avec vLLM via --gpu-memory-utilization, et les nouvelles requêtes attendent lorsque le pool est plein. La hausse de vllm:num_requests_waiting alors que vllm:kv_cache_usage_perc reste proche de 1 est la signature exacte de cet état. Certaines stacks préemptent une requête en cours et recalculent ensuite son cache au lieu de la placer en file d’attente. Pour l’utilisateur, cela se traduit par un blocage au milieu du flux.
Un contexte long vous coûte deux fois : davantage de travail de prefill au démarrage, puis davantage de lectures en mémoire par token pendant le reste de la réponse.
Pourquoi le batching améliore-t-il le débit tout en dégradant la latence de queue ?
Comme le décodage est limité par la bande passante, les requêtes supplémentaires coûtent presque rien du côté du calcul. Une seule lecture des poids peut produire un token pour chaque séquence du batch. Le débit total augmente donc presque linéairement avec la taille du batch, jusqu’à ce que le pool KV soit épuisé ou que le batch devienne suffisamment grand pour être de nouveau limité par le calcul. Le continuous batching reconstruit le batch à chaque étape. Une requête terminée quitte donc le batch et une requête en attente le rejoint sans attendre les autres séquences.
La facture apparaît dans les percentiles. Le prochain token de chaque utilisateur attend désormais la partie la plus lente d’une étape partagée. La p50, c’est-à-dire la médiane, reste acceptable, tandis que la p99, qui correspond à la requête la plus lente sur 100, s’allonge. C’est la p99 que les utilisateurs remarquent, car elle correspond à la pause au milieu d’une phrase.
Le prefill accentue le phénomène. Lorsqu’un prompt volumineux arrive au milieu du flux, il occupe le device pendant une étape complète. Tous les utilisateurs dont la génération est en cours voient alors un temps d’arrêt. Le chunked prefill réduit fortement ce problème en découpant un prompt volumineux en morceaux et en mélangeant chaque morceau aux batches de décodage. En août 2026, le moteur vLLM V1 applique ce comportement par défaut et expose cet arbitrage via --max-num-batched-tokens. La documentation de réglage de vLLM décrit clairement le compromis : des valeurs plus faibles, autour de 2048, offrent une meilleure latence inter-token (ITL), car moins de prefills interrompent les décodages. Des valeurs plus élevées offrent un meilleur TTFT, car davantage de tokens de prefill tiennent dans un même batch. Ce flag unique règle le compromis entre prefill et décodage sous la forme d’une valeur numérique. Le seuil auquel la p99 devient inacceptable dépend de la capacité. combien d’utilisateurs simultanés un LLM auto-hébergé peut servir permet de l’évaluer avec les mêmes métriques.
Pourquoi un GPU plus puissant ne change-t-il parfois rien ?
Parce que « plus puissant » signifie généralement plus de capacité de calcul, alors que le décodage n’est pas limité par le calcul.
Comparez deux lignes du tableau ci-dessus. L’A100 80GB offre une bande passante publiée de 2039 GB/s, contre 864 GB/s pour la L40S. Le plafond de décodage suit exactement cette différence : 127 tokens par seconde contre 54. La RTX 4090 est une carte très rapide selon la plupart des critères, mais ses 1008 GB/s limitent son plafond à 63. Quelles que soient les autres différences entre deux cartes, le décodage d’un flux unique suit la bande passante indiquée dans la fiche technique.
Il existe donc deux façons d’accélérer le décodage : lire moins d’octets par token, en quantifiant les poids ou en utilisant un modèle plus petit, ou acheter davantage de bande passante. Le prefill est le cas inverse. Il dépend de la capacité de calcul. Une carte plus rapide réduit donc réellement le TTFT avec les prompts longs. Si le problème est que le premier token met quatre secondes à apparaître, un meilleur matériel peut le résoudre. Si le problème est que le texte s’affiche lentement, ce ne sera probablement pas le cas.
Faut-il exécuter le prefill et le decode sur des workers distincts ?
Les grandes stacks de serving font exactement cela. Cette technique s’appelle la désagrégation du prefill et du decode. Un pool de workers exécute uniquement le prefill. Un second pool exécute uniquement le decode. Le KV cache construit par le premier est transféré au second via une interconnexion rapide. Cela fonctionne parce que ces phases nécessitent du matériel et une planification différents. Le prefill nécessite de la puissance de calcul et de gros lots de tokens. Le decode nécessite de la bande passante et de nombreuses séquences concurrentes. Leur séparation permet à chaque pool d’évoluer indépendamment. Elle évite aussi qu’un prompt très volumineux ne bloque tous les flux actifs.
Sur un seul VPS (virtual private server) équipé d’un seul GPU, cela ne vaut presque jamais le coût. Vous répartiriez un même périphérique contre lui-même et transformeriez un pointeur en transfert réseau de plusieurs gigaoctets de cache. Cette technique devient utile lorsque vous disposez de suffisamment d’accélérateurs pour consacrer des machines entières à chaque phase, et d’un trafic suffisamment régulier pour maintenir les deux pools actifs. En dessous de ce seuil, le prefill par blocs offre presque la même isolation avec un seul flag.
Que modifier lorsque les chiffres sont mauvais
Lorsque le TTFT est trop élevé :
- Raccourcissez le prompt. Le coût du prefill dépend du nombre de tokens du prompt, et le system prompt est pris en compte à chaque requête.
- Activez le prefix caching afin qu’un préfixe répété soit calculé une seule fois au lieu de l’être à chaque requête.
- Augmentez
--max-num-batched-tokenspour traiter davantage de prefill à chaque étape. - Vérifiez la file d’attente avant d’accuser le modèle. Une valeur de
vllm:num_requests_waitingsupérieure à zéro signifie que la requête n’avait pas encore démarré : le problème vient de la capacité.
Lorsque le nombre de tokens par seconde est trop faible :
- Quantifiez les poids. Moins d’octets par poids signifie moins d’octets lus pour chaque token.
- Comparez la bande passante mémoire publiée de votre carte avec le graphique ci-dessus et vérifiez dans quelle mesure vous approchez de la limite.
- Réduisez
--max-num-batched-tokensafin que les prefill interrompent moins souvent le decode. - Vérifiez la longueur du contexte. Une conversation qui atteint plusieurs milliers de tokens lit un cache KV beaucoup plus volumineux à chaque étape.
Le runtime joue également un rôle, car Ollama et vLLM planifient différemment le prefill et le decode, et un réglage utile pour l’un peut être sans effet sur l’autre. Mesurez d’abord les deux phases, puis ne modifiez qu’un seul paramètre à la fois.
FAQ
Pourquoi le premier token prend-il plusieurs secondes alors que les suivants sont générés rapidement ?
L’attente correspond au prefill, puis la génération en streaming correspond au decode. Le prefill traite l’intégralité du prompt en une seule passe limitée par la puissance de calcul, avant qu’une sortie soit disponible. Son coût augmente donc avec la longueur du prompt. Le decode émet ensuite un token par étape, à un débit principalement déterminé par la bande passante mémoire. Ce débit dépend presque pas de la longueur du prompt. Un long system prompt envoyé à chaque requête est la cause la plus courante. Le prefix caching supprime le coût de la partie répétée.
Un prompt plus long réduit-il le nombre de tokens par seconde ?
Un peu, mais pour une raison différente du TTFT. À chaque étape de decode, le système lit les keys et les values de tous les tokens précédents. Un KV cache plus volumineux implique donc davantage d’octets lus par token. Avec une configuration courante de 8 milliards de paramètres, le cache occupe environ 128 KiB par token. Un contexte de 8,000 tokens représente donc environ 1 GB relu à chaque étape. L’effet principal d’un prompt long concerne toujours le TTFT, et non la vitesse de génération en streaming.
Quelle caractéristique du GPU permet de prévoir la vitesse de decode ?
La bande passante mémoire. Divisez la bande passante annoncée par la taille des weights en mémoire pour obtenir la limite théorique d’un flux. Une carte qui dispose de davantage de puissance de calcul, mais de la même bande passante, ne générera pas les tokens plus rapidement. C’est également pourquoi la quantification en 8 bits double approximativement la vitesse de decode : elle divise par deux le nombre d’octets lus par token, sans modifier les calculs.
Pourquoi le throughput augmente-t-il lorsque j’ajoute des utilisateurs, alors que chaque utilisateur a l’impression que le service ralentit ?
Une seule lecture des weights permet de traiter un token pour chaque séquence du batch. Le nombre total de tokens par seconde augmente donc avec la taille du batch. Chaque token individuel doit alors attendre une étape partagée, ce qui augmente simultanément la latence par utilisateur. Surveillez la latence inter-token p99, et non le débit agrégé, puis vérifiez vllm:num_requests_waiting pour déterminer si les requêtes sont en file d’attente plutôt qu’en cours d’exécution.