Pourquoi votre LLM auto-hébergé bloque à 5 utilisateurs
Un utilisateur suffit, cinq ralentissent : découvrez comment le batching, le KV cache, le prefill et la profondeur de file fixent la capacité réelle de votre serveur LLM.
Pourquoi un LLM auto-hébergé ralentit-il lorsque davantage d’utilisateurs se connectent ?
Un LLM auto-hébergé se bloque avec 5 utilisateurs simultanés parce que le serveur génère encore une seule réponse à la fois et que les 4 autres attendent dans une file. La documentation d’Ollama est claire sur la valeur par défaut : OLLAMA_NUM_PARALLEL correspond au « nombre maximal de requêtes parallèles que chaque modèle traite simultanément, valeur par défaut : 1 ». Rien n’est défaillant. 4 personnes sur 5 attendent leur tour.
La solution consiste rarement à utiliser une machine plus puissante. Il faut un moteur d’inférence capable de faire passer de nombreuses requêtes dans le modèle lors du même forward pass, ainsi que suffisamment de mémoire disponible pour conserver le contexte de chaque conversation pendant ce traitement. Les deux éléments sont importants, mais c’est le second qui détermine réellement votre limite.
Les deux phases de chaque requête
Le prefill lit l’intégralité du prompt en une seule fois et construit le cache d’attention correspondant. Tous les tokens du prompt traversent le modèle ensemble. Le prefill consiste donc en une grande multiplication matricielle et dépend du débit de calcul. Le decode génère ensuite la réponse un token à la fois. À chaque token, l’intégralité des poids du modèle doit de nouveau être lue depuis la mémoire, alors que les calculs effectués sur ce seul token sont minimes. Le decode dépend donc de la bande passante mémoire.
Cette asymétrie explique pourquoi le batching fonctionne. Pour le decode d’un utilisateur, supposons que 5 GB de poids soient lus par token, tandis que la plupart des unités de calcul restent inactives. Ajoutez une deuxième requête : le moteur lit une seule fois les mêmes 5 GB, puis calcule deux tokens à partir de ces données. Le deuxième utilisateur ajoute presque aucun temps de traitement. Traiter les requêtes strictement l’une après l’autre fait perdre cet avantage.
Deux valeurs déterminent ce que ressent l’utilisateur. Le TTFT (time to first token) correspond au temps d’attente dans la file, ajouté au temps de prefill. L’ITL (inter-token latency) est l’intervalle entre les tokens transmis en streaming. Il dépend du decode. Un serveur lent l’est généralement sur l’une de ces deux valeurs, et les corrections ne sont pas les mêmes. Il est utile de déterminer laquelle pose problème avant de modifier un paramètre. mesurer séparément le prefill et le decode permet de le vérifier.
Le batching statique fait attendre tout le monde jusqu’à la réponse la plus lente
Le batching statique est la version naïve. C’est ce que vous obtenez lorsque vous regroupez vous-même les requêtes dans le code de l’application. Le moteur collecte N requêtes, les exécute ensemble et conserve chaque slot jusqu’à la fin de la génération la plus longue du groupe.
Un utilisateur qui demande un résumé de 1,200 tokens bloque quatre réponses d’une ligne dans le batch, car celui-ci ne libère aucun slot avant la fin de son membre le plus lent.
Cela entraîne deux coûts. Les séquences terminées continuent d’occuper des slots sans effectuer de calcul utile. Le débit effectif diminue donc lorsque la longueur des sorties varie, ce qui est fréquent pour les conversations. Une requête qui arrive juste après la formation du batch doit attendre que celui-ci soit entièrement traité avant même de commencer le prefill. Son TTFT dépend donc du long texte généré pour un autre utilisateur.
Le continuous batching admet et retire des requêtes à chaque token
Le continuous batching planifie les requêtes au niveau d’une seule étape de décodage. Après chaque étape, le scheduler retire les séquences qui viennent d’émettre leur token d’arrêt, puis admet les requêtes en attente dans les slots disponibles. Une réponse qui se termine à l’étape 40 libère son slot à l’étape 40, et non à la fin d’un batch.
Ce mécanisme n’a rien d’inhabituel. llama-server décrit -cb, --cont-batching comme « whether to enable continuous batching (a.k.a dynamic batching) (default: enabled) », et vLLM repose sur ce principe. Ollama traite également les requêtes en parallèle. La configuration par défaut limite simplement leur nombre à un, ce qui explique pourquoi tant de personnes concluent que leur matériel ne peut pas gérer la concurrence alors que c’est leur configuration qui l’interdit.
Les résultats publiés sur le continuous batching sont généralement mesurés sur des cartes de datacenter qui disposent à la fois de capacité de calcul disponible et de dizaines de gigaoctets pour le cache. La tendance de ces résultats s’applique aussi à votre machine. Leur ampleur, en revanche, ne se transpose pas, comme l’explique la section sur la mémoire ci-dessous.
Le prefill concurrence avec le décodage pour les mêmes ressources de calcul
Lorsqu’une nouvelle requête arrive alors que quatre réponses sont en cours de streaming, son prompt doit d’abord être traité en prefill, une opération qui sollicite fortement le calcul. Si le scheduler réserve une étape entière à ce prefill, les quatre utilisateurs ne reçoivent aucun token pendant cette étape. Avec un prompt long, toutes les fenêtres ouvertes marquent alors une pause visible. C’est ce que l’on appelle un ralentissement du serveur lorsqu’un autre utilisateur clique sur Envoyer.
Le chunked prefill découpe un prompt long en morceaux et intègre chaque morceau à la même étape que les décodages en cours. Le guide de réglage de vLLM décrit directement ce compromis : des budgets de chunks plus petits « offrent un meilleur ITL, car moins de prefills ralentissent les décodages », tandis que des valeurs plus élevées « offrent un meilleur time to first token (TTFT), car davantage de tokens de prefill peuvent être traités dans un batch ». Il faut choisir quelle expérience protéger : celle de la personne qui attend le début de la réponse, ou celle des utilisateurs qui regardent le texte s’afficher en streaming.
La longueur du prompt détermine l’ampleur du problème. Un prompt de 6,000 tokens suivi d’une réponse de 200 tokens représente 6,000 tokens de travail de prefill pour seulement 200 étapes de décodage. Le chat augmenté par récupération et les prompts système longs vous placent tous deux dans ce cas. Le prefill n’est alors plus négligeable : c’est lui que les utilisateurs attendent. Le prefix caching est utile lorsque la partie longue se répète : vLLM expose --enable-prefix-caching, qui réutilise le cache d’un préfixe de prompt partagé au lieu de le recalculer pour chaque requête.
La mémoire qui s’épuise en premier est le cache KV
Chaque token de chaque conversation active laisse un vecteur de clé et un vecteur de valeur dans chaque couche du modèle. C’est le cache KV (cache clé/valeur). Il évite au décodage de recalculer l’intégralité du prompt pour chaque nouveau token. Sa taille par token est déterminée par l’architecture du modèle : 2 (une clé et une valeur) fois le nombre de couches, fois le nombre de têtes clé/valeur, fois la dimension d’une tête, fois le nombre d’octets par valeur. Relevez ces valeurs dans le config.json du modèle.
Faites le calcul une fois : la limite cesse d’être mystérieuse. Un modèle 8B courant avec 36 couches, 8 têtes clé/valeur et une dimension de tête de 128, dont le cache est stocké sur 16 bits, consomme 2 36 8 128 2 octets par token. Cela représente 147,456 octets, soit environ 144 KiB. Une conversation de 8,192 tokens nécessite donc environ 1.2 GB de cache. Cinq conversations en nécessitent environ 6 GB, en plus des poids du modèle. C’est la véritable réponse à la question du nombre d’utilisateurs pris en charge.
La concurrence multiplie la taille du contexte, et les outils l’indiquent explicitement. La FAQ d’Ollama précise : « Le traitement parallèle des requêtes pour un modèle donné augmente la taille du contexte selon le nombre de requêtes parallèles. Par exemple, un contexte de 2K avec 4 requêtes parallèles donne un contexte de 8K et une allocation mémoire supplémentaire. » La RAM nécessaire évolue selon OLLAMA_NUM_PARALLEL multiplié par OLLAMA_CONTEXT_LENGTH. Dans llama-server, le contexte demandé avec -c est réparti entre les -np slots. Augmenter uniquement le nombre de slots réduit donc la quantité de contexte que chaque requête peut contenir. Lisez le contexte par slot dans le journal de démarrage au lieu de le supposer.
vLLM préalloue la mémoire. --gpu-memory-utilization (0.92 par défaut) correspond à « la fraction de la mémoire GPU à utiliser pour l’exécuteur du modèle ». La mémoire restante après le chargement des poids devient le pool KV paginé. Lorsque ce pool est presque plein, le scheduler évince une requête au lieu de l’échouer :
WARNING 05-09 00:49:33 scheduler.py:1057] Sequence group 0 is preempted by PreemptionMode.RECOMPUTE mode because there is not enough KV cache space.Dans le moteur V1 de vLLM, le mode de préemption par défaut est RECOMPUTE. Une requête évincée perd donc son cache et effectue de nouveau le prefill lorsqu’elle est admise à nouveau. Ce travail est réalisé deux fois. La documentation avertit que « la préemption et le recalcul peuvent dégrader la latence de bout en bout ». Cette ligne de journal explique mieux que toute autre pourquoi un utilisateur malchanceux a attendu beaucoup plus longtemps que les autres alors que la moyenne restait correcte. Définissez disable_log_stats=False pour journaliser le compteur cumulé, ou lisez le compteur de préemption dans les métriques Prometheus exposées par vLLM.
Ce qui change avec 2, 5 et 20 utilisateurs simultanés
Deux utilisateurs. La différence est presque invisible sur un GPU disposant de suffisamment de cache, car le deuxième flux de décodage s’exécute avec le premier pour un surcoût très faible. Sur un VPS limité au CPU avec 4 à 8 GB de RAM, ce n’est pas gratuit : les deux flux partagent les mêmes quelques vCPU et la même bande passante mémoire. Chaque utilisateur obtient donc environ la moitié des tokens par seconde, tandis que la demande de cache double avec un budget beaucoup plus faible.
Cinq utilisateurs. C’est à ce stade que les valeurs par défaut ne suffisent plus et que le problème commence par devenir un problème de file d’attente. Avec OLLAMA_NUM_PARALLEL à 1, quatre personnes attendent celle qui a demandé la réponse la plus longue. Chacune retrouve une vitesse normale dès que son tour arrive. Augmenter le nombre de traitements parallèles change la nature du problème : cinq slots avec un contexte de 8K représentent un cache de 40K tokens à placer. S’il ne tient pas dans la VRAM, le moteur décharge des couches vers la RAM du système. S’il ne tient pas dans la RAM, la machine utilise le swap et le nombre de tokens par seconde s’effondre.
Vingt utilisateurs. Vingt personnes dans une interface de chat ne génèrent généralement pas vingt requêtes simultanées. C’est le point le plus important à comprendre avant d’acheter du matériel. Une personne lit une réponse et réfléchit pendant 20 à 60 secondes entre deux tours. La majeure partie de sa session est donc inactive. Vingt agents, ou vingt tâches de synthèse de documents, représentent vingt flux réels sans aucun temps d’inactivité. Il faut alors une autre machine. Un développeur qui a dirigé un agent de programmation vers son propre serveur Ollama est plus proche du deuxième cas que du premier, car l’agent envoie des requêtes aussi longtemps que la tâche s’exécute et ne laisse pas les pauses de lecture d’une personne.
Vos utilisateurs sont-ils réellement concurrents, ou seulement connectés ?
Évaluez le nombre de requêtes en cours avant de dimensionner quoi que ce soit. Le calcul est simple : le nombre de requêtes en cours correspond au nombre d’utilisateurs, multiplié par le nombre de secondes nécessaires pour générer une réponse, puis divisé par le nombre de secondes entre deux tours.
- Mesurez d’abord votre vitesse sur un flux unique, pour le prefill comme pour le decode. Ne reprenez pas la valeur obtenue avec la carte de quelqu’un d’autre : mesurez les tokens par seconde sur votre propre machine et utilisez le résultat.
- Estimez le duty cycle. Vingt utilisateurs de chat, 12 secondes de génération par tour et un tour toutes les 90 secondes donnent 20 * 12 / 90, soit environ 2.7 requêtes en cours.
- Définissez un nombre de slots légèrement supérieur à cette valeur, puis vérifiez-le par rapport à la mémoire : le nombre de slots multiplié par le contexte par requête doit tenir dans le nombre de tokens de cache réellement disponibles.
- Gardez une file d’attente courte afin que les dépassements échouent rapidement et de manière visible.
Le nombre de tokens de cache disponibles correspond à la mémoire libre après chargement des poids, divisée par le coût par token indiqué dans la section précédente. Une carte de 24 GB exécutant un modèle 8B en 16-bit consomme environ 16 GB pour les poids et dispose d’environ 6 GB de cache utilisable avec l’utilisation par défaut, soit environ cinq conversations de 8K. Pour en faire tenir davantage, réduisez le contexte par requête ou stockez le cache en 8-bit (llama-server prend --cache-type-k q8_0). Ces deux options augmentent la concurrence en échange d’un compromis, et il vaut la peine de lire l’analyse complète de ce compromis avant d’investir dans le matériel : à partir de quand un GPU VPS devient rentable face aux tokens d’API.
Quand les valeurs par défaut d’Ollama ne suffisent plus
Augmentez le nombre d’exécutions parallèles dans l’unité de service, car une variable exportée dans un shell ne sera pas transmise à un démon géré par systemd.
sudo systemctl edit ollama.service[Service]
Environment="OLLAMA_NUM_PARALLEL=4"
Environment="OLLAMA_CONTEXT_LENGTH=8192"
Environment="OLLAMA_MAX_QUEUE=64"sudo systemctl daemon-reload
sudo systemctl restart ollama
systemctl show ollama --property=Environment
ollama pssystemctl show doit afficher les trois variables que vous venez de définir. Si ce n’est pas le cas, le drop-in n’a pas été enregistré et aucune autre action ne pourra résoudre le problème. ollama ps liste ensuite le modèle chargé avec une taille supérieure à celle de ses seuls poids, car quatre emplacements de 8,192 tokens réservent 32,768 tokens de cache en plus. Une colonne PROCESSOR indiquant qu’une partie du modèle se trouve sur le CPU alors que vous attendiez un chargement complet sur le GPU signifie que vous avez demandé plus de cache que la carte n’en avait de disponible. Réduisez l’une des deux valeurs. Réduire le contexte est généralement le choix le plus sûr, mais une fenêtre trop petite tronque silencieusement les longues requêtes au lieu de générer une erreur. Il est donc préférable de dimensionner num_ctx délibérément plutôt que de le réduire jusqu’à ce que le modèle tienne.
La valeur par défaut de la file d’attente mérite un second examen. Ollama met en file d’attente jusqu’à OLLAMA_MAX_QUEUE requêtes, et « la valeur par défaut est 512 ». Au-delà, il répond « avec une erreur 503 indiquant que le serveur est surchargé ». Une file d’attente de 512 requêtes sur une machine qui en traite quatre à la fois est une promesse impossible à tenir, car le client en position 300 expire bien avant son tour. Une file d’attente courte renvoie une erreur que votre application peut réessayer ou signaler, ce qui vaut mieux qu’un indicateur de chargement qui ne se termine jamais.
Testez réellement le comportement. Envoyez deux requêtes au même moment depuis deux terminaux et surveillez-les toutes les deux. Si la seconde ne produit rien avant la fin de la première, le paramètre d’exécution parallèle n’a pas été pris en compte.
Quand un véritable moteur de serving devient rentable
vLLM justifie sa configuration supplémentaire lorsque vous disposez d’un GPU avec de la marge et que plus d’environ quatre requêtes sont réellement en cours de traitement. Son ordonnanceur fonctionne token par token, son cache est paginé afin de réutiliser les fragments libres, et il transforme la VRAM disponible en capacité de traitement simultané au lieu de la laisser inutilisée. En août 2026, l’installation et le lancement documentés se font avec deux commandes :
uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instructcurl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-1.5B-Instruct",
"messages": [{"role": "user", "content": "Say hello."}]
}'Une réponse contenant un tableau choices signifie que le serveur est opérationnel et que le modèle est chargé. En charge, les deux paramètres importants sont --max-num-seqs, le « nombre maximal de séquences à traiter au cours d’une seule itération », et --max-num-batched-tokens, le « nombre maximal de tokens pouvant être traités au cours d’une seule itération ». Le premier limite le traitement simultané. Le second définit le budget de prefill par blocs décrit précédemment.
En dessous d’environ quatre requêtes en cours de traitement, ou sur une machine dépourvue de GPU compatible, vLLM ajoute de la complexité pour un gain limité. Il nécessite une carte de classe CUDA et réserve la majeure partie de la mémoire au démarrage, ce qui constitue un mauvais compromis sur un VPS de 4 à 8 GB. Dans ce cas, privilégiez un modèle plus petit avec un contexte plus court et une file d’attente que vous contrôlez. comment Ollama et vLLM se différencient comme moteurs de serving présente ce choix en détail, tandis que exécuter Qwen 3 8B sur un VPS montre les ressources nécessaires à un modèle de taille intermédiaire avant même d’ajouter un seul utilisateur supplémentaire.
Le compromis que la sagesse populaire passe sous silence
Le continuous batching augmente le débit total et améliore généralement la latence médiane, car une requête en attente démarre plus tôt. La latence de queue évolue dans l’autre sens, mais cette moitié du problème est rarement mentionnée.
Chaque séquence supplémentaire dans une étape ajoute un peu de travail. L’ITL augmente donc pour tout le monde à mesure que le batch se remplit. Le prefill d’une nouvelle requête consomme une partie d’une étape qui aurait sinon été disponible pour les utilisateurs en streaming. Sous pression sur le cache, le scheduler préempte des requêtes, ce qui renvoie une requête partiellement générée au début de son prefill.
Une interface de chat expose les latences de queue, pas les moyennes. Un flux qui s’interrompt pendant deux secondes au milieu d’une phrase semble défaillant, même si le temps total jusqu’à la fin est bon. Mesurez le TTFT au p95 et l’ITL au p95 avec la charge prévue, et considérez la moyenne des tokens par seconde comme une mesure de capacité, pas comme une description de l’expérience utilisateur.
Le réglage pratique en découle. Limitez légèrement la concurrence en dessous de ce que la mémoire permet, afin que le moteur n’ait jamais besoin de préempter. Une file d’attente courte et prévisible vaut mieux qu’un batch profond qui sature, car un utilisateur qui attend quatre secondes puis reçoit un flux régulier sera plus satisfait qu’un utilisateur dont la réponse démarre immédiatement mais s’interrompt deux fois.
Que vérifier lorsque le service est lent
Chaque utilisateur obtient un débit normal, mais l’attente est longue. Il s’agit d’une file d’attente, pas d’un problème de vitesse. Vérifiez d’abord le paramètre de parallélisme. Le modèle traite correctement les requêtes, une par une.
Ollama renvoie HTTP 503. La file d’attente est pleine. Soit la machine est réellement à sa capacité maximale, soit OLLAMA_MAX_QUEUE est volontairement faible pour limiter la charge, ce qui est précisément son rôle.
Le nombre de tokens par seconde s’effondre sous la charge sur une machine équipée d’un CPU. Exécutez vmstat 1 pendant le problème. Des valeurs non nulles dans les colonnes si et so indiquent que la machine utilise le swap. Les poids sont donc lus sur le disque à chaque token. Aucune modification de configuration ne peut résoudre ce problème. Utilisez un modèle plus petit ou réduisez le nombre de slots.
Un utilisateur sur dix attend beaucoup plus longtemps que les autres. Recherchez preempted dans le journal vLLM. La préemption et le recalcul associé sont généralement en cause. Cela signifie que le cache est surchargé pour la longueur de contexte autorisée.
Le TTFT est mauvais alors que le serveur est inactif. Il s’agit du prefill, pas de la concurrence. Les prompts longs nécessitent un temps de traitement réel avant l’apparition du premier token. Vérifiez donc la taille des prompts et le prefix caching avant d’examiner le matériel. Si la longue attente ne concerne que le premier utilisateur après une période d’inactivité et que tous les suivants n’ont aucun problème, il ne s’agit pas du prefill. Ollama a probablement déchargé le modèle, puis relu les poids depuis le disque. Vous pouvez écarter cette cause en conservant le modèle en mémoire entre les requêtes.
FAQ
Pourquoi mon LLM auto-hébergé ralentit-il lorsqu’une deuxième personne l’utilise ?
Le plus souvent, il ne ralentit pas du tout. La deuxième requête est mise en file d’attente. Ollama est livré avec OLLAMA_NUM_PARALLEL réglé sur 1, si bien que la deuxième requête attend que la première génère son dernier token. Pour distinguer les deux cas, mesurez le flux d’un utilisateur pendant qu’un autre attend : si le nombre de tokens par seconde redevient normal dès que la génération commence, vous avez une file d’attente et l’augmentation du nombre de requêtes parallèles corrigera le problème. Si les deux flux avancent à mi-vitesse, vous partagez réellement la bande passante mémoire. Il s’agit alors d’une limite matérielle.
Combien d’utilisateurs simultanés un petit GPU peut-il servir ?
Comptez la mémoire, pas les utilisateurs. Commencez par les poids, puis ajoutez le cache KV, qui consomme 2 fois le nombre de couches fois le nombre de têtes key/value fois la dimension des têtes fois le nombre d’octets, par token et par conversation active. Un modèle 8B courant avec 36 couches, 8 têtes key/value et une dimension de tête de 128 consomme environ 144 KiB par token en 16 bits. Une conversation de 8,192 tokens nécessite donc environ 1.2 GB. Une carte de 24 GB qui conserve ce modèle en 16 bits dispose d’environ 6 GB pour le cache, soit environ cinq conversations avec le contexte maximal, ou davantage si vous réduisez le contexte.
Le continuous batching ralentit-il la réponse de chaque utilisateur ?
La latence médiane s’améliore généralement, car les requêtes n’attendent plus la fin d’un batch complet. En revanche, la latence de queue augmente. Chaque séquence supplémentaire ajoute du travail à chaque étape de décodage. L’arrivée d’une nouvelle requête prend une partie d’une étape aux utilisateurs dont la réponse est en cours de streaming, et une requête préemptée doit effectuer le prefill deux fois. Mesurez la latence inter-tokens p95, et non la moyenne. Dans une fenêtre de chat, les pauses sont visibles alors que la moyenne les masque.
Dois-je augmenter OLLAMA_NUM_PARALLEL ou passer à vLLM ?
Augmentez d’abord le nombre de requêtes parallèles. Cela ne coûte rien et ne nécessite qu’un fichier drop-in. Cette modification corrige le cas courant où quatre personnes attendent derrière une longue réponse. La mémoire est la limite : les requêtes parallèles multiplient la quantité de contexte à conserver. Surveillez donc les couches qui basculent vers le CPU. Passez à vLLM lorsque votre GPU dispose de VRAM disponible et que plus d’environ quatre requêtes sont réellement en cours, car c’est à partir de ce point que le cache paginé et la planification par token apportent davantage qu’ils ne coûtent.
Davantage de cœurs CPU corrigeront-ils un serveur LLM lent ?
Pas pour la partie la plus visible par les utilisateurs. Le décodage lit l’intégralité du modèle en mémoire pour chaque token. Il est donc limité par la bande passante de la RAM, et les cœurs supplémentaires n’apportent plus rien une fois cette bande passante saturée. Le prefill évolue avec le nombre de cœurs. Davantage de cœurs réduit donc le délai avant le premier token pour les longs prompts. Sur un VPS de 4 à 8 GB, la contrainte principale est généralement la capacité mémoire. La solution consiste alors à utiliser un modèle plus petit ou un contexte plus court plutôt qu’à ajouter des vCPUs.