SSD Nodes Learn 🎉 VPS vanaf $5.50/mnd
Gidsen Matt ConnorDoor Matt Connor · Bijgewerkt 2026-08-13

Waarom vertraagt uw zelfgehost LLM bij 5 gebruikers?

Uw LLM hapert omdat de standaardinstelling voor parallelle verzoeken op 1 staat. Ontdek hoe batching, KV cache en queue depth bepalen hoeveel gebruikers uw server echt aankan.

Waarom vertraagt een zelfgehost LLM wanneer er meer gebruikers bijkomen?

Een zelfgehost LLM loopt vast bij 5 gelijktijdige gebruikers omdat de server nog steeds één antwoord tegelijk genereert, terwijl de andere vier in een wachtrij staan. De documentatie van Ollama is duidelijk over de standaardinstelling: OLLAMA_NUM_PARALLEL is "het maximale aantal parallelle verzoeken dat elk model tegelijkertijd verwerkt, standaard 1." Er is niets defect. Vier van uw vijf gebruikers wachten op hun beurt.

De oplossing is zelden een krachtigere server. Het is een serving engine die veel verzoeken in één forward pass door het model stuurt, gecombineerd met voldoende extra geheugen om de conversatie van iedereen vast te houden terwijl dit gebeurt. Beide onderdelen zijn van belang, en het tweede onderdeel bepaalt in de praktijk uw limiet.

De twee fasen die elk verzoek doorloopt

Prefill leest de volledige prompt in één keer en bouwt de attention-cache hiervoor op. Elke prompt-token doorloopt het model tegelijkertijd; prefill is daarom één grote matrixvermenigvuldiging die wordt beperkt door de rekenkracht (arithmetic throughput). Decode schrijft vervolgens het antwoord token voor token. Voor elk token moeten de volledige gewichten van het model opnieuw uit het geheugen worden gelezen, terwijl de rekenkundige bewerkingen voor dat ene token minimaal zijn. Decode wordt daarom beperkt door de geheugenbandbreedte.

Deze asymmetrie is de reden waarom batching werkt. Bij het decoderen voor één gebruiker wordt bijvoorbeeld 5 GB aan gewichten per token gelezen, terwijl het merendeel van de rekeneenheden onbenut blijft. Voeg een tweede verzoek toe en de engine leest dezelfde 5 GB één keer, om vervolgens twee tokens te berekenen. De tweede gebruiker kost vrijwel geen extra tijd. Het strikt na elkaar afhandelen van verzoeken verspilt dit voordeel.

Twee getallen bepalen de gebruikerservaring. TTFT (time to first token) is de wachttijd in de wachtrij plus de prefill-tijd. ITL (inter-token latency) is de pauze tussen gestreamde tokens en wordt bepaald door de decode-fase. Een trage server is meestal traag in een van deze twee, en de oplossingen hiervoor verschillen.

Statische batching laat iedereen wachten op het traagste antwoord

Statische batching is de naïeve methode; dit is wat u krijgt als u verzoeken zelf groepeert in uw applicatiecode. De engine verzamelt N verzoeken, voert deze tegelijkertijd uit en houdt elke slot bezet totdat de langste generatie in de groep is voltooid.

Eén gebruiker die om een samenvatting van 1.200 tokens vraagt, houdt vier antwoorden van één regel vast in de batch, omdat de batch geen enkele slot vrijgeeft totdat het traagste lid klaar is.

Dit brengt twee kosten met zich mee. Voltooide sequenties blijven slots bezetten die niets nuttigs berekenen, waardoor de effectieve doorvoer daalt naarmate de uitvoerlengtes variëren, en de lengtes van chat-uitvoer variëren sterk. Een verzoek dat één stap nadat de batch is gevormd binnenkomt, wacht totdat de volledige batch is verwerkt voordat het zelfs met de prefill kan beginnen. Dit betekent dat de TTFT wordt bepaald door het essay van iemand anders.

Continuous batching accepteert en beëindigt verzoeken per token

Continuous batching plant op het niveau van een enkele decoding-stap. Na elke stap verwijdert de scheduler sequenties die zojuist hun stop-token hebben gegenereerd en accepteert vervolgens wachtende verzoeken in de vrijgekomen slots. Een antwoord dat bij stap 40 eindigt, maakt zijn slot vrij bij stap 40, niet pas aan het einde van een batch.

Dit is niet ongebruikelijk. llama-server documenteert -cb, --cont-batching als "of continuous batching (ook wel dynamic batching genoemd) moet worden ingeschakeld (standaard: ingeschakeld)", en vLLM is rond dit principe gebouwd. Ollama bedient eveneens parallelle verzoeken. De standaardinstelling beperkt het aantal simpelweg tot één, wat de reden is dat veel mensen concluderen dat hun hardware geen concurrency ondersteunt, terwijl hun configuratie dit expliciet uitschakelde.

Gepubliceerde resultaten van continuous batching worden doorgaans gemeten op datacenter-kaarten die zowel over extra rekenkracht als over tientallen gigabytes voor de cache beschikken. De vorm van die resultaten is toepasbaar op uw eigen systeem. De omvang ervan echter niet, en de onderstaande sectie over geheugen legt uit waarom.

Prefill concurreert met decode om dezelfde rekenkracht

Wanneer er een nieuw verzoek binnenkomt terwijl er vier antwoorden worden gestreamd, moet de prompt eerst worden geprefilled, en prefill is rekenintensief. Als de scheduler die prefill een eigen stap geeft, ontvangen de vier streamende gebruikers tijdens die stap geen tokens. Bij een lange prompt is dit een zichtbare pauze in elk open venster. Dit is de hapering die mensen bedoelen wanneer ze zeggen dat de server stottert zodra iemand anders op verzenden drukt.

Chunked prefill breekt een lange prompt op in stukken en mengt elk stuk in dezelfde stap als de lopende decodes. De tuning-handleiding van vLLM stelt de afweging direct vast: kleinere chunk-budgetten "bereiken een betere ITL omdat er minder prefills zijn die de decodes vertragen", terwijl hogere waarden "een betere time to first token (TTFT) bereiken omdat u meer prefill-tokens in een batch kunt verwerken". U kiest wiens ervaring u beschermt: de persoon die wacht tot een antwoord begint, of de mensen die de tekst zien streamen.

De lengte van de prompt bepaalt hoe erg dit effect is. Een prompt van 6.000 tokens met een antwoord van 200 tokens betekent 6.000 tokens aan prefill-werk tegenover 200 decode-stappen. Retrieval-augmented chat en lange systeemprompts dwingen u beide in dat regime, waardoor prefill niet langer een verwaarloosbare factor is, maar de bewerking waar gebruikers op wachten. Prefix caching helpt wanneer het lange gedeelte zich herhaalt: vLLM stelt --enable-prefix-caching beschikbaar, wat de cache hergebruikt voor een gedeeld prompt-prefix in plaats van deze voor elk verzoek opnieuw te berekenen.

Het geheugen dat als eerste uitgeput raakt, is de KV cache

Elke token in elke actieve conversatie laat een key-vector en een value-vector achter in elke laag van het model. Dit is de KV cache (key/value cache), en deze zorgt ervoor dat het decoderingsproces niet voor elke nieuwe token de volledige prompt opnieuw hoeft te berekenen. De grootte per token staat vast op basis van de vorm van het model: 2 (één key, één value) vermenigvuldigd met het aantal lagen, het aantal key/value-heads, de dimensie van de head en het aantal bytes per waarde. Lees deze getallen uit de config.json van het model.

Bereken het één keer en het plafond is geen mysterie meer. Een typisch 8B-model met 36 lagen, 8 key/value-heads en een head-dimensie van 128, waarbij de cache in 16-bit wordt opgeslagen, kost 2 36 8 128 2 bytes per token. Dat is 147.456 bytes, ongeveer 144 KiB. Eén conversatie van 8.192 tokens vereist daarom ongeveer 1,2 GB aan cache. Vijf van dergelijke conversaties vereisen ongeveer 6 GB, bovenop de gewichten, en dat is het werkelijke antwoord op hoeveel gebruikers er passen.

Gelijktijdigheid vermenigvuldigt de context, en de tools geven dit expliciet aan. De FAQ van Ollama stelt: "Parallelle verwerking van verzoeken voor een bepaald model resulteert in een toename van de contextgrootte met het aantal parallelle verzoeken. Bijvoorbeeld, een context van 2K met 4 parallelle verzoeken resulteert in een context van 8K en extra geheugentoewijzing." Het vereiste RAM-geheugen schaalt met OLLAMA_NUM_PARALLEL vermenigvuldigd met OLLAMA_CONTEXT_LENGTH. In llama-server wordt de context waar u om vraagt met -c verdeeld over de -np slots, waardoor het verhogen van het aantal slots op zichzelf de capaciteit per verzoek verkleint. Lees de context per slot uit het opstartlogboek in plaats van er zomaar vanuit te gaan.

vLLM reserveert vooraf geheugen. --gpu-memory-utilization (standaard 0.92) is "de fractie van het GPU-geheugen die wordt gebruikt voor de model-executor". Wat er overblijft na het laden van de gewichten wordt de paged KV-pool, en wanneer die pool tekortschiet, stoot de scheduler een verzoek af in plaats van het te laten falen:

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.

In de V1-engine van vLLM is de standaard preemption-modus RECOMPUTE, wat betekent dat een afgestoten verzoek de cache verwijdert en opnieuw wordt ingevuld wanneer het weer wordt toegelaten. Dat werk wordt dus dubbel uitgevoerd. De documentatie waarschuwt dat "preemption en herberekening de end-to-end latentie nadelig kunnen beïnvloeden", en deze logregel is de beste verklaring voor waarom één ongelukkige gebruiker veel langer moest wachten dan de rest, terwijl uw gemiddelde er gezond uitzag. Stel disable_log_stats=False in om het cumulatieve aantal te loggen, of lees de preemption-teller uit de Prometheus-metrieken die vLLM beschikbaar stelt.

Wat verandert er bij 2, 5 en 20 gelijktijdige gebruikers

Twee gebruikers. Vrijwel onzichtbaar op een GPU met voldoende cache, omdat de tweede decoderingsstroom nauwelijks extra tijd in beslag neemt naast de eerste. Op een VPS die alleen op de CPU draait met 4 tot 8 GB RAM is dit niet gratis: beide stromen delen hetzelfde beperkte aantal vCPU's en dezelfde RAM-bandbreedte. Hierdoor ziet elke gebruiker ongeveer de helft van het aantal tokens per seconde en verdubbelt de vraag naar cache binnen een veel kleiner budget.

Vijf gebruikers. Vanaf dit punt volstaan de standaardinstellingen niet meer en ontstaat er een wachtrijprobleem. Bij OLLAMA_NUM_PARALLEL op 1 wachten vier mensen op degene die om een lang antwoord vroeg, waarna ieder van hen op normale snelheid verder kan zodra zij aan de beurt zijn. Verhoogt u het aantal parallelle taken, dan verandert de aard van het probleem: vijf slots met elk 8K context vereisen 40K aan cache. Als dit niet in het VRAM past, verplaatst de engine lagen naar het systeem-RAM. Past het daar ook niet in, dan gaat het systeem swappen en keldert het aantal tokens per seconde.

Twintig gebruikers. Twintig mensen in een chat-interface betekenen meestal niet twintig gelijktijdige verzoeken; dit is het belangrijkste inzicht voordat u hardware aanschaft. Een persoon leest een antwoord en denkt 20 tot 60 seconden na tussen de beurten door, waardoor het grootste deel van de sessie inactief is. Twintig agents of twintig document-samenvattingstaken zijn daarentegen twintig actieve stromen zonder enige inactiviteit. Dat vereist een ander type machine.

Zijn uw gebruikers gelijktijdig actief, of enkel ingelogd?

Bereken het aantal gelijktijdige verzoeken voordat u de capaciteit bepaalt. De berekening is eenvoudig: het aantal gelijktijdige verzoeken is gelijk aan het aantal gebruikers, vermenigvuldigd met het aantal seconden dat nodig is voor het genereren per beurt, gedeeld door het aantal seconden tussen beurten.

  1. Meet eerst uw eigen snelheid bij een enkele stream, inclusief prefill en decodering. Neem geen getallen over van andermans hardware: meet het aantal tokens per seconde op uw eigen systeem en gebruik de resultaten die u zelf behaalt.
  2. Schat de duty cycle in. Twintig chatgebruikers, 12 seconden generatietijd per beurt, één beurt per 90 seconden, resulteert in 20 * 12 / 90, oftewel ongeveer 2,7 gelijktijdige verzoeken.
  3. Stel het aantal slots iets hoger in dan dit getal en controleer of dit past binnen het geheugen: het aantal slots vermenigvuldigd met de context per verzoek moet passen binnen de cache-tokens die u daadwerkelijk beschikbaar heeft.
  4. Houd de wachtrij kort, zodat een overflow snel en zichtbaar tot een foutmelding leidt.

Het aantal beschikbare cache-tokens is het vrije geheugen na het laden van de gewichten, gedeeld door de kosten per token zoals beschreven in de bovenstaande sectie. Een kaart van 24 GB die een 8B-model in 16-bit draait, verbruikt ongeveer 16 GB voor de gewichten en heeft bij standaardgebruik ongeveer 6 GB aan bruikbare cache over; dit komt overeen met ongeveer vijf 8K-gesprekken. Om meer te kunnen verwerken, kunt u de context per verzoek verkorten of de cache opslaan in 8-bit (llama-server gebruikt --cache-type-k q8_0). Beide opties vergroten de gelijktijdigheid ten koste van iets anders. Het is raadzaam om de eerlijke afweging van deze compromissen te lezen voordat u investeert in hardware: wanneer een GPU VPS rendabeler is dan API-tokens.

Waar de standaardinstellingen van Ollama niet langer volstaan

Verhoog het aantal parallelle verzoeken via de service-unit, aangezien een shell-export niet wordt doorgegeven aan een door systemd beheerde daemon.

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 ps

systemctl show hoort de drie variabelen die u zojuist heeft ingesteld weer te geven. Als dit niet het geval is, is het drop-in bestand niet opgeslagen en heeft verdere actie geen zin. ollama ps toont vervolgens het geladen model met een omvang die groter is dan de gewichten alleen, omdat vier slots van 8,192 tokens daarnaast 32,768 tokens aan cache reserveren. Een PROCESSOR-kolom die aangeeft dat een deel van het model op de CPU staat terwijl u verwachtte dat alles op de GPU zou draaien, betekent dat u om meer cache heeft gevraagd dan de kaart beschikbaar had. Verlaag een van de twee getallen.

De standaardwachtrij verdient extra aandacht. Ollama plaatst maximaal OLLAMA_MAX_QUEUE verzoeken in de wachtrij, waarbij "de standaardwaarde 512 is". Daarboven reageert het "met een 503-foutmelding die aangeeft dat de server overbelast is". Een wachtrij van 512 op een systeem dat er vier tegelijk verwerkt, is een belofte die u niet kunt waarmaken, omdat de client op positie 300 al een time-out krijgt lang voordat deze aan de beurt is. Een korte wachtrij geeft een foutmelding terug waarop uw applicatie kan reageren met een nieuwe poging of een melding, wat beter is dan een laadscherm dat nooit verdwijnt.

Test het in de praktijk. Verstuur twee verzoeken op exact hetzelfde moment vanuit twee terminals en observeer beide. Als de tweede pas resultaat geeft nadat de eerste is voltooid, is de parallelle instelling niet effectief geworden.

Wanneer een echte serving engine zichzelf begint terug te verdienen

vLLM verdient de extra configuratie wanneer u beschikt over een GPU met reservecapaciteit en meer dan ongeveer vier verzoeken die daadwerkelijk in behandeling zijn. De scheduler werkt per token, het cachegeheugen is gepagineerd zodat vrije fragmenten worden hergebruikt, en het zet onbenut VRAM om in gelijktijdigheid in plaats van dit ongebruikt te laten. Sinds augustus 2026 zijn de gedocumenteerde installatie en start twee commando's:

uv pip install vllm --torch-backend=auto
vllm serve Qwen/Qwen2.5-1.5B-Instruct
curl 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."}]
    }'

Een antwoord dat een choices-array bevat, betekent dat de server actief is en het model is geladen. Onder belasting zijn de twee instellingen die ertoe doen --max-num-seqs, het "maximum aantal sequenties dat in één iteratie wordt verwerkt", en --max-num-batched-tokens, het "maximum aantal tokens dat in één iteratie kan worden verwerkt". De eerste begrenst de gelijktijdigheid. De tweede is het budget voor chunked prefill zoals eerder beschreven.

Bij minder dan ongeveer vier gelijktijdige verzoeken, of op een systeem zonder ondersteunde GPU, kost vLLM complexiteit zonder veel op te leveren. Het vereist een CUDA-geschikte kaart en claimt bij het opstarten het grootste deel van het geheugen; dit is een ongunstige afweging op een VPS met 4 tot 8 GB RAM. In dat geval is de oplossing een kleiner model met een kortere context en een wachtrij die u zelf beheert. hoe Ollama en vLLM verschillen als serving engines behandelt deze keuze volledig, en het draaien van Qwen 3 8B op een VPS laat zien wat een middelgroot model vereist voordat u ook maar één extra gebruiker toevoegt.

De afweging die in de volksmond wordt genegeerd

Continuous batching verhoogt de totale doorvoer en verbetert doorgaans ook de mediane latentie, omdat een verzoek in de wachtrij eerder wordt verwerkt. De staartlatentie (tail latency) beweegt echter de andere kant op, en dat aspect wordt zelden benoemd.

Elke extra sequentie in een stap voegt een kleine hoeveelheid werk toe, waardoor de ITL voor iedereen stijgt naarmate de batch zich vult. De prefill van een nieuw binnengekomen verzoek neemt een deel van de stap in beslag die anders voor de streamende gebruikers beschikbaar was geweest. Onder cache-druk voert de scheduler een preemption uit, waardoor een half gegenereerd verzoek teruggaat naar het begin van de prefill.

Een chat-UI toont staarten, geen gemiddelden. Een stream die halverwege een zin twee seconden pauzeert, wordt als defect ervaren, zelfs als de totale tijd tot voltooiing goed is. Meet de p95 TTFT en p95 ITL onder de verwachte belasting en beschouw het gemiddeld aantal tokens per seconde als een capaciteitsgetal in plaats van een beschrijving van de gebruikerservaring.

De praktische instelling volgt hieruit. Beperk de concurrency tot net onder wat het geheugen toelaat, zodat de engine nooit hoeft te preempten. Een korte, voorspelbare wachtrij is beter dan een grote batch die zorgt voor thrashing, omdat een gebruiker die vier seconden wacht en daarna vloeiend streamt, tevredener is dan een gebruiker die direct begint en twee keer stottert.

Wat te controleren bij traagheid

Elke gebruiker is normaal, maar het wachten duurt lang. Dit is een wachtrijprobleem, geen snelheidsprobleem. Controleer eerst de instelling voor parallellisme. Het model functioneert correct, maar verwerkt verzoeken één voor één.

HTTP 503 van Ollama. De wachtrij is vol. Of de server zit daadwerkelijk op de maximale capaciteit, of OLLAMA_MAX_QUEUE is bewust laag ingesteld om de belasting te beperken; dit is het gewenste gedrag.

Tokens per seconde storten in onder belasting op een CPU-server. Voer vmstat 1 uit terwijl dit gebeurt. Niet-nul waarden in de kolommen si en so betekenen dat de machine aan het swappen is, waardoor gewichten bij elk token van de schijf worden gelezen. Geen enkele configuratiewijziging lost dit op. Verklein het model of verminder het aantal slots.

Eén op de tien gebruikers wacht veel langer dan de rest. Zoek in het vLLM-logboek naar preempted. Preemption en de bijbehorende herberekening zijn de gebruikelijke oorzaak; dit betekent dat de cache overbezet is voor de toegestane contextlengte.

TTFT is slecht, zelfs als de server inactief is. Dit is prefill, geen gelijktijdigheid. Lange prompts kosten daadwerkelijke tijd voordat het eerste token verschijnt. Kijk daarom naar de promptgrootte en prefix caching voordat u naar de hardware kijkt.

FAQ

Waarom wordt mijn zelfgehoste LLM trager wanneer een tweede persoon deze gebruikt?

Meestal wordt deze niet trager, maar komt de aanvraag in een wachtrij. Ollama wordt geleverd met OLLAMA_NUM_PARALLEL ingesteld op 1, waardoor de tweede aanvraag wacht tot de eerste het laatste token heeft gegenereerd. U onderscheidt deze twee situaties door de stream van één gebruiker te timen terwijl een ander wacht: als het aantal tokens per seconde normaal is zodra de stream start, is er sprake van een wachtrij en lost het verhogen van het aantal parallelle processen dit op. Als beide streams op halve snelheid draaien, deelt u daadwerkelijk de geheugenbandbreedte en is dit een hardwarebeperking.

Hoeveel gelijktijdige gebruikers kan één kleine GPU bedienen?

Kijk naar het geheugen, niet naar het aantal gebruikers. Eerst de gewichten, daarna de KV-cache. Deze kost 2 keer het aantal lagen keer het aantal key/value-heads keer de head-dimensie keer het aantal bytes, per token, per actieve conversatie. Een typisch 8B-model met 36 lagen, 8 key/value-heads en een head-dimensie van 128 kost ongeveer 144 KiB per token in 16-bit. Een conversatie van 8,192 tokens vereist dus ongeveer 1.2 GB. Een kaart van 24 GB die dit model in 16-bit vasthoudt, heeft ongeveer 6 GB over voor de cache, wat neerkomt op ongeveer vijf conversaties bij volledige context, of meer als u de context verkort.

Maakt continuous batching het antwoord voor elke gebruiker trager?

De mediane latentie verbetert meestal, omdat aanvragen niet langer hoeven te wachten tot een hele batch is voltooid. De tail-latentie verslechtert echter. Elke extra sequentie voegt werk toe aan elke decodeerstap, de prefill van een nieuwe aanvraag neemt een deel van de tijd in beslag van de streamende gebruikers, en een onderbroken aanvraag moet twee keer prefillen. Meet de p95 inter-token latentie in plaats van het gemiddelde, omdat pauzes in een chatvenster duidelijk merkbaar zijn op een manier die gemiddelden maskeren.

Moet ik OLLAMA_NUM_PARALLEL verhogen of overstappen naar vLLM?

Verhoog eerst het aantal parallelle processen. Dit kost niets, vereist slechts één configuratiebestand en lost het veelvoorkomende scenario op waarbij vier mensen in de wachtrij staan achter één lang antwoord. Geheugen is de beperkende factor: parallelle aanvragen vermenigvuldigen de context die u moet vasthouden, dus let op of lagen naar de CPU worden verplaatst. Stap over naar vLLM wanneer u een GPU met vrije VRAM heeft en meer dan ongeveer vier aanvragen daadwerkelijk tegelijkertijd verwerkt; vanaf dat punt leveren paged cache en per-token scheduling meer op dan ze kosten.

Zorgen meer CPU-cores voor een snellere LLM-server?

Niet voor het deel dat gebruikers het meest merken. Bij het decoderen wordt het volledige model voor elk token uit het geheugen gelezen, waardoor het proces gebonden is aan de RAM-bandbreedte. Extra cores helpen niet meer zodra de bandbreedte verzadigd is. Prefill schaalt wel met cores, dus meer cores verkorten de tijd tot het eerste token bij lange prompts. Op een VPS met 4 tot 8 GB is de beperkende factor meestal de geheugencapaciteit; de effectieve oplossing is dan een kleiner model of een kortere context in plaats van meer vCPU's.