Dit artikel biedt een technische uiteenzetting over de werking van vLLM, startend bij de engine core en de scheduler die gebruikmaakt van paged attention voor optimaal KV-cache beheer. Er worden diverse geavanceerde optimalisaties besproken, waaronder chunked prefill, prefix caching, guided decoding via FSM's en speculative decoding. Daarnaast wordt uitgelegd hoe vLLM schaalt van enkelvoudige GPU's naar multi-GPU systemen via Tensor Parallelism en hoe een gedistribueerde serving layer met API-servers en headless nodes is ingericht. Het artikel sluit af met een analyse van performance-metrieken zoals TTFT en ITL, en het gebruik van benchmarks en het Roofline Model voor auto-tuning.
Inside vLLM: Anatomie van een LLM-inferentiesysteem met hoge doorvoer
Dit artikel is het eerste deel van een serie. Het begint breed en voegt vervolgens details toe (volgens een omgekeerde piramide-aanpak), zodat er een accuraat mentaal model van het volledige systeem kan worden gevormd zonder te verdrinken in details. Latere delen zullen dieper ingaan op specifieke subsystemen.
Het artikel is gestructureerd in vijf delen:
- LLM engine & engine core: de basisprincipes van vLLM (scheduling, paged attention, continuous batching, etc.).
- Geavanceerde functies: chunked prefill, prefix caching, guided & speculative decoding, gedisaggregeerde P/D.
- Opschalen: van single-GPU naar multi-GPU executie.
- Serving layer: gedistribueerde en concurrente web-scaffolding.
- Benchmarks en auto-tuning: het meten van latentie en doorvoer.
Notities:
- De analyse is gebaseerd op commit 42172ad (9 augustus 2025).
- De focus ligt op de V1 engine. Concepten uit de (nu deprecated) V0 engine zijn waar relevant meegenomen.
---
LLM Engine & Engine Core
De LLM-engine is het fundamentele bouwblok van vLLM. Op zichzelf maakt het inferentie met een hoge doorvoer mogelijk, maar alleen in een offline setting; het kan nog niet via het web aan klanten worden aangeboden.
Hieronder volgt een voorbeeld van offline inferentie (aangepast uit basic.py):
from vllm import LLM, SamplingParams
prompts = [
"Hello, my name is",
"The president of the United States is",
]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
def main():
llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")
outputs = llm.generate(prompts, sampling_params)
if __name__ == "__main__":
main()
Omgevingsvariabelen:
VLLMUSEV1="1" (gebruik van engine V1)
VLLMENABLEV1_MULTIPROCESSING="0" (uitvoering in een enkel proces)
Deze configuratie is:
- Offline: geen web/gedistribueerde systeemstructuur.
- Synchroon: alle executie vindt plaats in één blokkerend proces.
- Single-GPU: geen data-, model-, pipeline- of expertparallelisme (DP/TP/PP/EP = 1).
- Standaard transformer: ondersteuning voor hybride modellen zoals Jamba vereist een complexere hybride KV-cache geheugenallocator.
De LLM Engine constructor
De belangrijkste componenten van de engine zijn:
- vLLM config: bevat alle instellingen voor het model, de cache, parallellisme, etc.
- Processor: zet ruwe inputs om in
EngineCoreRequests via validatie, tokenisatie en verwerking.
- Engine core client: in het offline voorbeeld wordt
InprocClient gebruikt (equivalent aan EngineCore). Voor schaalbare serving wordt dit opgebouwd naar DPLBAsyncMPClient.
- Output processor: converteert ruwe
EngineCoreOutputs naar de RequestOutput die de gebruiker ziet.
De Engine core zelf bestaat uit verschillende subcomponenten:
- Model Executor: stuurt de forward passes op het model aan (momenteel
UniProcExecutor met één Worker-proces op één GPU; schaalbaar naar MultiProcExecutor voor meerdere GPU's).
- Structured Output Manager: gebruikt voor guided decoding.
- Scheduler: bepaalt welke requests in de volgende stap van de engine worden meegenomen. Deze bevat:
- Policy setting: kan FCFS (first come first served) of priority zijn.
- Wacht- en uitvoeringsrijen (waiting and running queues).
- KV cache manager: het hart van paged attention.
De KV-cache manager beheert een freeblockqueue — een pool van beschikbare KV-cache blokken (vaak honderdduizenden, afhankelijk van de VRAM-grootte en blokgrootte). Tijdens paged attention dienen deze blokken als indexstructuur die tokens koppelt aan hun berekende KV-cache blokken.
Voor een standaard transformerlaag (non-MLA) wordt de blokgrootte als volgt berekend: 2 (key/value) blocksize (standaard=16) numkvheads headsize dtypenumbytes (bijv. 2 voor bf16)
Tijdens de constructie van de model executor wordt een Worker-object aangemaakt en worden drie belangrijke procedures uitgevoerd:
1. Init device
- Toewijzing van een CUDA-device (bijv. "cuda:0") aan de worker en controle of het model dtype ondersteund wordt (bijv. bf16).
- Verificatie of er voldoende VRAM beschikbaar is, gegeven de gevraagde
gpumemoryutilization (bijv. 0.8 → 80% van totale VRAM).
- Opzetten van distributed settings (DP / TP / PP / EP, etc.).
- Instantiëren van een
modelrunner (bevat de sampler, KV-cache en forward-pass buffers zoals inputids, posities, etc.).
- Instantiëren van een
InputBatch-object (bevat CPU-side forward-pass buffers, block tables voor KV-cache indexering, sampling metadata, etc.).
2. Load model
- Instantiëren van de modelarchitectuur.
- Laden van de modelgewichten.
- Aanroepen van
model.eval() (PyTorch's inferentiemodus).
- Optioneel: aanroepen van
torch.compile() op het model.
3. Initialize KV cache
- Vaststellen van per-layer KV-cache specificaties. Bij hybride modellen (sliding window, Transformer/SSM zoals Jamba) is dit complexer.
- Uitvoeren van een dummy/profiling forward pass en maken van een GPU-geheugensnapshop om te berekenen hoeveel KV-cache blokken in het beschikbare VRAM passen.
- Alloceren, reshapen en binden van KV-cache tensors aan de attention layers.
- Voorbereiden van attention metadata (bijv. backend instellen op FlashAttention).
- Tenzij
--enforce-eager is opgegeven, wordt voor elke warmup batch size een dummy run gedaan om CUDA-graphs te capturen. Deze graphs reduceren kernel launch overhead en verbeteren de latentie.
---
De Generate functie
De eerste stap is het valideren en voeden van requests aan de engine. Voor elke prompt gebeurt het volgende:
- Er wordt een unieke request ID aangemaakt en de aankomsttijd wordt vastgelegd.
- Een input preprocessor tokeniseert de prompt en retourneert een dictionary met prompt,
prompttokenids en type (text, tokens, embeds, etc.).
- Deze informatie wordt verpakt in een
EngineCoreRequest, inclusief prioriteit, sampling parameters en andere metadata.
- Het request gaat naar de engine core, die het omzet in een
Request-object met status WAITING. Dit wordt toegevoegd aan de wachtrij van de scheduler (append bij FCFS, heap-push bij priority).
In een synchrone engine zijn deze initiële prompts de enige die worden verwerkt. Een asynchrone engine ondersteunt continuous batching: na elke stap worden zowel nieuwe als oude requests overwogen.
Zolang er requests zijn om te verwerken, roept de engine herhaaldelijk de step() functie aan. Elke stap bestaat uit drie fasen:
- Schedule: selecteren welke requests in deze stap worden uitgevoerd (decode en/of chunked prefill).
- Forward pass: het model uitvoeren en tokens samplen.
- Postprocess: gesamplede token ID's toevoegen aan elk Request, detokeniseren en stopcondities controleren. Als een request is voltooid, worden de KV-cache blokken teruggegeven aan de
freeblockqueue.
Stopcondities zijn:
- Het request overschrijdt de lengtelimiet (
maxmodellength of eigen max_tokens).
- Het gesamplede token is het EOS ID (tenzij
ignore_eos is ingeschakeld).
- Het gesamplede token komt overeen met een van de
stoptokenids.
- Stop-strings zijn aanwezig in de output.
---
Scheduler
De scheduler handelt twee hoofdtypen workloads af:
- Prefill requests: een forward pass over alle prompt tokens. Deze zijn meestal compute-bound. Aan het einde wordt één token gesampled uit de waarschijnlijkheidsverdeling van de laatste positie.
- Decode requests: een forward pass over alleen het meest recente token. Eerdere KV-vectoren zijn reeds gecached. Deze zijn memory-bandwidth-bound, omdat alle modelgewichten (en KV-caches) geladen moeten worden om één token te berekenen.
De V1 scheduler kan beide typen requests in dezelfde stap mengen. De scheduler prioriteert decode requests (die al in de running queue staan):
- Berekening van het aantal te genereren nieuwe tokens.
- Aanroep van de
allocate_slots functie van de KV-cache manager.
- Bijwerken van het token budget.
Daarna worden prefill requests uit de wachtrij verwerkt:
- Opvragen van het aantal berekende blokken (retourneert 0 als prefix caching is uitgeschakeld).
- Aanroep van
allocate_slots.
- Het request wordt verplaatst van de wachtrij naar de uitvoeringsrij (
RUNNING).
- Bijwerken van het token budget.
Hoe allocate_slots werkt:
- Bereken aantal blokken: bepalen hoeveel nieuwe KV-cache blokken ($n$) gealloceerd moeten worden (standaard 16 tokens per blok).
- Controleer beschikbaarheid: als er onvoldoende blokken in de pool zitten, stopt het proces vroegtijdig. De engine kan proberen lage-prioriteit requests te evicteren (recompute preemption) of de scheduling overslaan.
- Alloceren van blokken: via de coordinator worden de eerste $n$ blokken uit de
freeblockqueue gehaald en gekoppeld aan het request in de reqtoblocks dictionary.
---
Run forward pass
De model executor roept execute_model aan, wat wordt gedelegeerd aan de Worker en vervolgens aan de model runner. De stappen zijn:
- Update states: voltooide requests verwijderen uit de
input_batch; metadata bijwerken (bijv. KV-cache blokken per request).
- Prepare inputs: buffers kopiëren van CPU → GPU; posities berekenen;
slot_mapping bouwen en attention metadata construeren.
- Forward pass: het model uitvoeren met custom paged attn kernels. Alle sequenties worden afgeplat en geconcateneerd tot één lange "super sequence". Positie-indices en attention masks zorgen ervoor dat elke sequentie alleen naar zijn eigen tokens kijkt, wat continuous batching zonder right-padding mogelijk maakt.
- Gather last-token states: hidden states extraheren voor de laatste positie van elke sequentie en logits berekenen.
- Sample: tokens samplen uit de logits op basis van de sampling config (greedy, temperature, top-p, etc.).
De forward pass heeft twee modi:
- Eager mode: standaard PyTorch forward pass.
- "Captured" mode: herhalen van een vooraf gecapturde CUDA Graph om kernel launch overhead te verminderen.
---
Geavanceerde functies
Chunked prefill
Chunked prefill splitst de prefill stap van lange prompts in kleinere stukken (chunks). Zonder dit zou één zeer lang request een engine stap kunnen monopoliseren, waardoor andere requests worden vertraagd en de latentie stijgt. Door het aantal nieuwe tokens per stap te begrenzen (longprefilltoken_threshold), kunnen meerdere prefills en decodes efficiënter worden afgewisseld.
Prefix Caching
Prefix caching voorkomt het opnieuw berekenen van tokens die meerdere prompts aan het begin delen.
Werking in vLLM:
- De engine splitst de prompt in blokken van 16 tokens.
- Voor elk volledig blok wordt een hash berekend (via ingebouwde hash of SHA-256), inclusief metadata zoals MM hash, LoRA ID en cache salt.
- De engine controleert via
findlongestcachehit of deze hashes al bestaan in de cachedblockhashto_block map.
- Bij een hit worden de bestaande KV-blokken hergebruikt; bij een miss worden ze berekend en opgeslagen voor toekomstig gebruik.
Guided Decoding (FSM)
Guided decoding beperkt de logits bij elke stap via een grammar-based finite state machine (FSM). Dit garandeert dat alleen tokens die toegestaan zijn door de grammatica gesampled kunnen worden (bijv. regex patronen of programmeertalen).
Proces in vLLM:
- Een
StructuredOutputManager beheert een grammarbitmask tensor.
- De grammatica wordt asynchroon gecompileerd via een backend zoals
xgrammar.
- Na de forward pass breidt de engine de bitmask uit naar de vocabulaire-grootte en zet alle niet-toegestane logits op $-\infty$.
- Na het samplen van een token wordt de FSM naar de volgende staat gevorderd via
accept_tokens.
Speculative Decoding
Speculative decoding versnelt generatie door een kleiner "draft" model te gebruiken om $k$ tokens goedkoop voor te stellen, waarna het grote model deze in één keer verifieert.
Stappen:
- Draft: Het kleine model stelt $k$ tokens voor.
- Verify: Het grote model voert één pass uit over de context + de $k$ draft tokens. Dit levert waarschijnlijkheden op voor die $k+1$ posities.
- Accept/reject: Van links naar rechts worden tokens geaccepteerd als de kans van het grote model $\ge$ de kans vanHet draft model is (of via een probabilistische regel).
- Bij de eerste afwijzing stopt het proces en wordt er een gecorrigeerde distributie gebruikt om het laatste token te samplen.
vLLM V1 implementeert snellere, minder accurate proposal schemes in plaats van een volledig draft LM: n-gram, EAGLE en Medusa.
Gedisaggregeerde P/D (Prefill/Decoding)
Omdat prefill (compute-bound) en decode (memory-bandwidth-bound) verschillende performance profielen hebben, is het zinvol ze te scheiden. Dit geeft betere controle over TTFT (time-to-first-token) en ITL (inter-token latency).
In de praktijk worden $N$ prefill-instanties en $M$ decode-instanties gedraaid. Prefill workers schrijven KV's naar een dedicated KV-cache service; decode workers lezen deze uit via een Connector abstractie (zoals SharedStorageConnector of LMCache).
---
Opschalen: Van UniProcExecutor naar MultiProcExecutor
Wanneer modelgewichten niet meer in het VRAM van één GPU passen, is sharding nodig.
- Tensor Parallelism (TP): verdeelt het model over meerdere GPU's op dezelfde node (hoge bandbreedte).
- Pipeline Parallelism (PP): verdeelt het model over verschillende nodes.
MultiProcExecutor coördineert dit via:
- Een
rpcbroadcastmq message queue voor werkdistributie.
- Het spawnen van een
WorkerProc voor elke rank (bijv. TP=8 → 8 processen).
- Elke worker heeft een
rpcbroadcastmq om werk te ontvangen en een workerresponsemq om resultaten terug te sturen naar de parent.
---
Gedistribueerde Serving
Voor grootschalige serving wordt het model gerepliceerd over meerdere nodes (Data Parallelism / DP). Een typische setup bestaat uit API-servers en headless engine nodes.
De Headless Server Node
Op een headless node start een CoreEngineProcManager meerdere processen (DPEngineCoreProc). Elke replica:
- Voert een handshake uit met de frontend via ZMQ sockets.
- Initialiseert een DP-groep (bijv. via NCCL).
- Draait drie threads: een input thread (ontvangt requests), een main thread (voert
engine_core.step() uit) en een output thread (stuurt resultaten terug).
De API Server Node
De API server gebruikt een AsyncLLM wrapper met een DPLBAsyncMPClient voor load balancing over de beschikbare engines.
Request Lifecycle:
- Curl Request: Gebruiker stuurt een POST request naar de FastAPI endpoint.
- Tokenisatie: De API server tokeniseert de prompt en bereidt metadata voor.
- Load Balancing:
getcoreengineforrequest kiest de engine met de laagste belasting (score = $\text{waiting} \times 4 + \text{running}$).
- Transport: Het request gaat via een input socket naar de gekozen engine.
- Executie: De engine voert de stappen uit (
scheduler → executor → forward pass).
- Response: Tokens worden via de output socket teruggestuurd, verwerkt door asyncio tasks in de API server en via Uvicorn terug naar de gebruiker gestuurd.
---
Benchmarks en Auto-tuning: Latentie vs Doorvoer
Er is een fundamentele trade-off tussen latentie (tijd tot resultaat) en doorvoer (tokens per seconde).
Belangrijke Metrieken
| Metriek | Definitie |
| TTFT | Time To First Token: tijd van aanvraag tot het eerste output token. |
| ITL | Inter-Token Latency: tijd tussen twee opeenvolgende tokens. |
| TPOT | Time Per Output Token: gemiddelde ITL over alle output tokens. |
| E2E Latency | End-to-End latentie: totale tijd voor een request ($\text{TTFT} + \sum \text{ITLs}$). |
| Throughput | Totaal aantal verwerkte tokens of requests per seconde. |
| Goodput | Doorvoer die voldoet aan de Service Level Objectives (SLO's). |
Het Roofline Model
Bij kleine batch-groottes ($B$) is de ITL laag, maar is het systeem onderbenut. Naarmate $B$ stijgt richting een verzadigingspunt ($B{sat}$), blijft de latentie bijna vlak omdat de tijd gedomineerd wordt door HBM-bandbreedte (het streamen van gewichten). Boven $B{sat}$ wordt het systeem compute-bound en stijgt de latentie lineair met de batch grootte, terwijl de doorvoer optimaliseert.
Benchmarking in vLLM
vLLM biedt CLI-tools voor prestaties:
vllm bench latency: meet E2E latentie met kleine batches.
vllm bench throughput: stuurt een vaste set prompts tegelijk (QPS=Inf) om maximale doorvoer te meten.
vllm bench serve: simuleert real-world workloads met Poisson/Gamma distributies voor aankomsttijden van requests.
Er is ook een auto-tune script dat de serve benchmark gebruikt om optimale instellingen te vinden die voldoen aan specifieke SLO's (bijv. "maximaliseer doorvoer terwijl p99 E2E $< 500$ ms blijft").
Inside vLLM: Anatomie van een LLM-inferentiesysteem met hoge doorvoer
Dit artikel is het eerste deel van een serie. Het begint breed en voegt vervolgens details toe (volgens een omgekeerde piramide-aanpak), zodat er een accuraat mentaal model van het volledige systeem kan worden gevormd zonder te verdrinken in details. Latere delen zullen dieper ingaan op specifieke subsystemen.
Het artikel is gestructureerd in vijf delen:
- LLM engine & engine core: de basisprincipes van vLLM (scheduling, paged attention, continuous batching, etc.).
- Geavanceerde functies: chunked prefill, prefix caching, guided & speculative decoding, gedisaggregeerde P/D.
- Opschalen: van single-GPU naar multi-GPU executie.
- Serving layer: gedistribueerde en concurrente web-scaffolding.
- Benchmarks en auto-tuning: het meten van latentie en doorvoer.
Notities:
- De analyse is gebaseerd op commit 42172ad (9 augustus 2025).
- De focus ligt op de V1 engine. Concepten uit de (nu deprecated) V0 engine zijn waar relevant meegenomen.
---
LLM Engine & Engine Core
De LLM-engine is het fundamentele bouwblok van vLLM. Op zichzelf maakt het inferentie met een hoge doorvoer mogelijk, maar alleen in een offline setting; het kan nog niet via het web aan klanten worden aangeboden.
Hieronder volgt een voorbeeld van offline inferentie (aangepast uit basic.py):
from vllm import LLM, SamplingParams
prompts = [
"Hello, my name is",
"The president of the United States is",
]
sampling_params = SamplingParams(temperature=0.8, top_p=0.95)
def main():
llm = LLM(model="TinyLlama/TinyLlama-1.1B-Chat-v1.0")
outputs = llm.generate(prompts, sampling_params)
if __name__ == "__main__":
main()
Omgevingsvariabelen:
VLLMUSEV1="1" (gebruik van engine V1)
VLLMENABLEV1_MULTIPROCESSING="0" (uitvoering in een enkel proces)
Deze configuratie is:
- Offline: geen web/gedistribueerde systeemstructuur.
- Synchroon: alle executie vindt plaats in één blokkerend proces.
- Single-GPU: geen data-, model-, pipeline- of expertparallelisme (DP/TP/PP/EP = 1).
- Standaard transformer: ondersteuning voor hybride modellen zoals Jamba vereist een complexere hybride KV-cache geheugenallocator.
De LLM Engine constructor
De belangrijkste componenten van de engine zijn:
- vLLM config: bevat alle instellingen voor het model, de cache, parallellisme, etc.
- Processor: zet ruwe inputs om in
EngineCoreRequests via validatie, tokenisatie en verwerking.
- Engine core client: in het offline voorbeeld wordt
InprocClient gebruikt (equivalent aan EngineCore). Voor schaalbare serving wordt dit opgebouwd naar DPLBAsyncMPClient.
- Output processor: converteert ruwe
EngineCoreOutputs naar de RequestOutput die de gebruiker ziet.
De Engine core zelf bestaat uit verschillende subcomponenten:
- Model Executor: stuurt de forward passes op het model aan (momenteel
UniProcExecutor met één Worker-proces op één GPU; schaalbaar naar MultiProcExecutor voor meerdere GPU's).
- Structured Output Manager: gebruikt voor guided decoding.
- Scheduler: bepaalt welke requests in de volgende stap van de engine worden meegenomen. Deze bevat:
- Policy setting: kan FCFS (first come first served) of priority zijn.
- Wacht- en uitvoeringsrijen (waiting and running queues).
- KV cache manager: het hart van paged attention.
De KV-cache manager beheert een freeblockqueue — een pool van beschikbare KV-cache blokken (vaak honderdduizenden, afhankelijk van de VRAM-grootte en blokgrootte). Tijdens paged attention dienen deze blokken als indexstructuur die tokens koppelt aan hun berekende KV-cache blokken.
Voor een standaard transformerlaag (non-MLA) wordt de blokgrootte als volgt berekend: 2 (key/value) blocksize (standaard=16) numkvheads headsize dtypenumbytes (bijv. 2 voor bf16)
Tijdens de constructie van de model executor wordt een Worker-object aangemaakt en worden drie belangrijke procedures uitgevoerd:
1. Init device
- Toewijzing van een CUDA-device (bijv. "cuda:0") aan de worker en controle of het model dtype ondersteund wordt (bijv. bf16).
- Verificatie of er voldoende VRAM beschikbaar is, gegeven de gevraagde
gpumemoryutilization (bijv. 0.8 → 80% van totale VRAM).
- Opzetten van distributed settings (DP / TP / PP / EP, etc.).
- Instantiëren van een
modelrunner (bevat de sampler, KV-cache en forward-pass buffers zoals inputids, posities, etc.).
- Instantiëren van een
InputBatch-object (bevat CPU-side forward-pass buffers, block tables voor KV-cache indexering, sampling metadata, etc.).
2. Load model
- Instantiëren van de modelarchitectuur.
- Laden van de modelgewichten.
- Aanroepen van
model.eval() (PyTorch's inferentiemodus).
- Optioneel: aanroepen van
torch.compile() op het model.
3. Initialize KV cache
- Vaststellen van per-layer KV-cache specificaties. Bij hybride modellen (sliding window, Transformer/SSM zoals Jamba) is dit complexer.
- Uitvoeren van een dummy/profiling forward pass en maken van een GPU-geheugensnapshop om te berekenen hoeveel KV-cache blokken in het beschikbare VRAM passen.
- Alloceren, reshapen en binden van KV-cache tensors aan de attention layers.
- Voorbereiden van attention metadata (bijv. backend instellen op FlashAttention).
- Tenzij
--enforce-eager is opgegeven, wordt voor elke warmup batch size een dummy run gedaan om CUDA-graphs te capturen. Deze graphs reduceren kernel launch overhead en verbeteren de latentie.
---
De Generate functie
De eerste stap is het valideren en voeden van requests aan de engine. Voor elke prompt gebeurt het volgende:
- Er wordt een unieke request ID aangemaakt en de aankomsttijd wordt vastgelegd.
- Een input preprocessor tokeniseert de prompt en retourneert een dictionary met prompt,
prompttokenids en type (text, tokens, embeds, etc.).
- Deze informatie wordt verpakt in een
EngineCoreRequest, inclusief prioriteit, sampling parameters en andere metadata.
- Het request gaat naar de engine core, die het omzet in een
Request-object met status WAITING. Dit wordt toegevoegd aan de wachtrij van de scheduler (append bij FCFS, heap-push bij priority).
In een synchrone engine zijn deze initiële prompts de enige die worden verwerkt. Een asynchrone engine ondersteunt continuous batching: na elke stap worden zowel nieuwe als oude requests overwogen.
Zolang er requests zijn om te verwerken, roept de engine herhaaldelijk de step() functie aan. Elke stap bestaat uit drie fasen:
- Schedule: selecteren welke requests in deze stap worden uitgevoerd (decode en/of chunked prefill).
- Forward pass: het model uitvoeren en tokens samplen.
- Postprocess: gesamplede token ID's toevoegen aan elk Request, detokeniseren en stopcondities controleren. Als een request is voltooid, worden de KV-cache blokken teruggegeven aan de
freeblockqueue.
Stopcondities zijn:
- Het request overschrijdt de lengtelimiet (
maxmodellength of eigen max_tokens).
- Het gesamplede token is het EOS ID (tenzij
ignore_eos is ingeschakeld).
- Het gesamplede token komt overeen met een van de
stoptokenids.
- Stop-strings zijn aanwezig in de output.
---
Scheduler
De scheduler handelt twee hoofdtypen workloads af:
- Prefill requests: een forward pass over alle prompt tokens. Deze zijn meestal compute-bound. Aan het einde wordt één token gesampled uit de waarschijnlijkheidsverdeling van de laatste positie.
- Decode requests: een forward pass over alleen het meest recente token. Eerdere KV-vectoren zijn reeds gecached. Deze zijn memory-bandwidth-bound, omdat alle modelgewichten (en KV-caches) geladen moeten worden om één token te berekenen.
De V1 scheduler kan beide typen requests in dezelfde stap mengen. De scheduler prioriteert decode requests (die al in de running queue staan):
- Berekening van het aantal te genereren nieuwe tokens.
- Aanroep van de
allocate_slots functie van de KV-cache manager.
- Bijwerken van het token budget.
Daarna worden prefill requests uit de wachtrij verwerkt:
- Opvragen van het aantal berekende blokken (retourneert 0 als prefix caching is uitgeschakeld).
- Aanroep van
allocate_slots.
- Het request wordt verplaatst van de wachtrij naar de uitvoeringsrij (
RUNNING).
- Bijwerken van het token budget.
Hoe allocate_slots werkt:
- Bereken aantal blokken: bepalen hoeveel nieuwe KV-cache blokken ($n$) gealloceerd moeten worden (standaard 16 tokens per blok).
- Controleer beschikbaarheid: als er onvoldoende blokken in de pool zitten, stopt het proces vroegtijdig. De engine kan proberen lage-prioriteit requests te evicteren (recompute preemption) of de scheduling overslaan.
- Alloceren van blokken: via de coordinator worden de eerste $n$ blokken uit de
freeblockqueue gehaald en gekoppeld aan het request in de reqtoblocks dictionary.
---
Run forward pass
De model executor roept execute_model aan, wat wordt gedelegeerd aan de Worker en vervolgens aan de model runner. De stappen zijn:
- Update states: voltooide requests verwijderen uit de
input_batch; metadata bijwerken (bijv. KV-cache blokken per request).
- Prepare inputs: buffers kopiëren van CPU → GPU; posities berekenen;
slot_mapping bouwen en attention metadata construeren.
- Forward pass: het model uitvoeren met custom paged attn kernels. Alle sequenties worden afgeplat en geconcateneerd tot één lange "super sequence". Positie-indices en attention masks zorgen ervoor dat elke sequentie alleen naar zijn eigen tokens kijkt, wat continuous batching zonder right-padding mogelijk maakt.
- Gather last-token states: hidden states extraheren voor de laatste positie van elke sequentie en logits berekenen.
- Sample: tokens samplen uit de logits op basis van de sampling config (greedy, temperature, top-p, etc.).
De forward pass heeft twee modi:
- Eager mode: standaard PyTorch forward pass.
- "Captured" mode: herhalen van een vooraf gecapturde CUDA Graph om kernel launch overhead te verminderen.
---
Geavanceerde functies
Chunked prefill
Chunked prefill splitst de prefill stap van lange prompts in kleinere stukken (chunks). Zonder dit zou één zeer lang request een engine stap kunnen monopoliseren, waardoor andere requests worden vertraagd en de latentie stijgt. Door het aantal nieuwe tokens per stap te begrenzen (longprefilltoken_threshold), kunnen meerdere prefills en decodes efficiënter worden afgewisseld.
Prefix Caching
Prefix caching voorkomt het opnieuw berekenen van tokens die meerdere prompts aan het begin delen.
Werking in vLLM:
- De engine splitst de prompt in blokken van 16 tokens.
- Voor elk volledig blok wordt een hash berekend (via ingebouwde hash of SHA-256), inclusief metadata zoals MM hash, LoRA ID en cache salt.
- De engine controleert via
findlongestcachehit of deze hashes al bestaan in de cachedblockhashto_block map.
- Bij een hit worden de bestaande KV-blokken hergebruikt; bij een miss worden ze berekend en opgeslagen voor toekomstig gebruik.
Guided Decoding (FSM)
Guided decoding beperkt de logits bij elke stap via een grammar-based finite state machine (FSM). Dit garandeert dat alleen tokens die toegestaan zijn door de grammatica gesampled kunnen worden (bijv. regex patronen of programmeertalen).
Proces in vLLM:
- Een
StructuredOutputManager beheert een grammarbitmask tensor.
- De grammatica wordt asynchroon gecompileerd via een backend zoals
xgrammar.
- Na de forward pass breidt de engine de bitmask uit naar de vocabulaire-grootte en zet alle niet-toegestane logits op $-\infty$.
- Na het samplen van een token wordt de FSM naar de volgende staat gevorderd via
accept_tokens.
Speculative Decoding
Speculative decoding versnelt generatie door een kleiner "draft" model te gebruiken om $k$ tokens goedkoop voor te stellen, waarna het grote model deze in één keer verifieert.
Stappen:
- Draft: Het kleine model stelt $k$ tokens voor.
- Verify: Het grote model voert één pass uit over de context + de $k$ draft tokens. Dit levert waarschijnlijkheden op voor die $k+1$ posities.
- Accept/reject: Van links naar rechts worden tokens geaccepteerd als de kans van het grote model $\ge$ de kans vanHet draft model is (of via een probabilistische regel).
- Bij de eerste afwijzing stopt het proces en wordt er een gecorrigeerde distributie gebruikt om het laatste token te samplen.
vLLM V1 implementeert snellere, minder accurate proposal schemes in plaats van een volledig draft LM: n-gram, EAGLE en Medusa.
Gedisaggregeerde P/D (Prefill/Decoding)
Omdat prefill (compute-bound) en decode (memory-bandwidth-bound) verschillende performance profielen hebben, is het zinvol ze te scheiden. Dit geeft betere controle over TTFT (time-to-first-token) en ITL (inter-token latency).
In de praktijk worden $N$ prefill-instanties en $M$ decode-instanties gedraaid. Prefill workers schrijven KV's naar een dedicated KV-cache service; decode workers lezen deze uit via een Connector abstractie (zoals SharedStorageConnector of LMCache).
---
Opschalen: Van UniProcExecutor naar MultiProcExecutor
Wanneer modelgewichten niet meer in het VRAM van één GPU passen, is sharding nodig.
- Tensor Parallelism (TP): verdeelt het model over meerdere GPU's op dezelfde node (hoge bandbreedte).
- Pipeline Parallelism (PP): verdeelt het model over verschillende nodes.
MultiProcExecutor coördineert dit via:
- Een
rpcbroadcastmq message queue voor werkdistributie.
- Het spawnen van een
WorkerProc voor elke rank (bijv. TP=8 → 8 processen).
- Elke worker heeft een
rpcbroadcastmq om werk te ontvangen en een workerresponsemq om resultaten terug te sturen naar de parent.
---
Gedistribueerde Serving
Voor grootschalige serving wordt het model gerepliceerd over meerdere nodes (Data Parallelism / DP). Een typische setup bestaat uit API-servers en headless engine nodes.
De Headless Server Node
Op een headless node start een CoreEngineProcManager meerdere processen (DPEngineCoreProc). Elke replica:
- Voert een handshake uit met de frontend via ZMQ sockets.
- Initialiseert een DP-groep (bijv. via NCCL).
- Draait drie threads: een input thread (ontvangt requests), een main thread (voert
engine_core.step() uit) en een output thread (stuurt resultaten terug).
De API Server Node
De API server gebruikt een AsyncLLM wrapper met een DPLBAsyncMPClient voor load balancing over de beschikbare engines.
Request Lifecycle:
- Curl Request: Gebruiker stuurt een POST request naar de FastAPI endpoint.
- Tokenisatie: De API server tokeniseert de prompt en bereidt metadata voor.
- Load Balancing:
getcoreengineforrequest kiest de engine met de laagste belasting (score = $\text{waiting} \times 4 + \text{running}$).
- Transport: Het request gaat via een input socket naar de gekozen engine.
- Executie: De engine voert de stappen uit (
scheduler → executor → forward pass).
- Response: Tokens worden via de output socket teruggestuurd, verwerkt door asyncio tasks in de API server en via Uvicorn terug naar de gebruiker gestuurd.
---
Benchmarks en Auto-tuning: Latentie vs Doorvoer
Er is een fundamentele trade-off tussen latentie (tijd tot resultaat) en doorvoer (tokens per seconde).
Belangrijke Metrieken
| Metriek | Definitie |
| TTFT | Time To First Token: tijd van aanvraag tot het eerste output token. |
| ITL | Inter-Token Latency: tijd tussen twee opeenvolgende tokens. |
| TPOT | Time Per Output Token: gemiddelde ITL over alle output tokens. |
| E2E Latency | End-to-End latentie: totale tijd voor een request ($\text{TTFT} + \sum \text{ITLs}$). |
| Throughput | Totaal aantal verwerkte tokens of requests per seconde. |
| Goodput | Doorvoer die voldoet aan de Service Level Objectives (SLO's). |
Het Roofline Model
Bij kleine batch-groottes ($B$) is de ITL laag, maar is het systeem onderbenut. Naarmate $B$ stijgt richting een verzadigingspunt ($B{sat}$), blijft de latentie bijna vlak omdat de tijd gedomineerd wordt door HBM-bandbreedte (het streamen van gewichten). Boven $B{sat}$ wordt het systeem compute-bound en stijgt de latentie lineair met de batch grootte, terwijl de doorvoer optimaliseert.
Benchmarking in vLLM
vLLM biedt CLI-tools voor prestaties:
vllm bench latency: meet E2E latentie met kleine batches.
vllm bench throughput: stuurt een vaste set prompts tegelijk (QPS=Inf) om maximale doorvoer te meten.
vllm bench serve: simuleert real-world workloads met Poisson/Gamma distributies voor aankomsttijden van requests.
Er is ook een auto-tune script dat de serve benchmark gebruikt om optimale instellingen te vinden die voldoen aan specifieke SLO's (bijv. "maximaliseer doorvoer terwijl p99 E2E $< 500$ ms blijft").