, ,

What is vLLM, and why is it fast?

vLLM explained for infrastructure teams: continuous batching, PagedAttention and the KV cache, prefix caching and other speed-ups, the settings that matter, and how llm-d adds cache-aware routing and split prefill and decode on top of vLLM.

GPU batch slots with continuous batching and paged KV cache blocks

AI Infrastructure series · Part 17 of 24

vLLM is the open-source serving engine you will meet most often when running open models on your own GPUs. It is the model server inside llm-d, inside many Kubernetes AI platforms, and behind a good share of hosted model APIs. Its speed does not come from faster maths. It comes from scheduling: it adds and removes requests from the running batch on every step, and it hands out KV cache memory in small blocks instead of reserving it up front. This part walks through both ideas with numbers, then covers the settings that matter, what the start-up log tells you, and how vLLM fits under llm-d.

Try it: Continuous Batching Lab

Race three schedulers on the same 60 requests, step a request through PagedAttention and watch blocks get allocated, shared and freed, fill one step’s token budget, and find the batch size that keeps every user fast enough.

Open the demo →

What is vLLM?

vLLM started in 2023 at UC Berkeley’s Sky Computing Lab, alongside the research paper that introduced PagedAttention (Kwon et al., SOSP 2023). It is now a PyTorch Foundation project with contributors from many companies. It loads a model from Hugging Face or local storage, runs it on one or many GPUs, and serves it through an OpenAI-compatible API on port 8000. Part 16 showed the one-line start command and what a model server does in general.

It runs most popular open models, including Llama, Qwen, Mistral, DeepSeek and gpt-oss, on NVIDIA and AMD GPUs, Google TPUs and several other accelerators. When it launched, its authors measured up to 24 times the throughput of plain Hugging Face Transformers and up to 3.5 times that of TGI, the engine most people used at the time. The GPU does the same arithmetic in every engine. vLLM wins by keeping more requests on the GPU at once and wasting less memory on each one. To see why that matters, start with what one decode step costs.

Why can one GPU step serve many users?

During decode, a model writes one token per request per step. To write that token it must run every layer, which means reading every weight from GPU memory once (part 4, part 13). The maths per weight is tiny: two operations per parameter per token. So a single-user decode step is limited by how fast the GPU can read memory, not by how fast it can calculate.

Batching changes this. If eight requests are in the batch, the GPU still reads each weight only once per step and uses it eight times. The weights are the expensive part of the read, so the step takes only a little longer, and you get eight tokens instead of one. What grows with the batch is the KV cache each request must also read: its own history of keys and values (part 14).

How much does a bigger batch buy? A worked example

Take Llama 3.1 8B in 16-bit on one H100 SXM. The numbers are ideal ceilings (illustrative), real engines reach a fraction of them, but the shape is right.

1

Weights to read each step. 8 billion parameters × 2 bytes = about 16 GB.

2

KV cache to read each step. 128 KB per token. If each request has about 1,000 tokens of history, that is about 131 MB per request.

3

Batch of 1. 16.1 GB ÷ 3.35 TB/s = 4.8 ms per step, so at most about 208 tokens per second for one user and 208 in total.

4

Batch of 64. 16 GB + 64 × 131 MB = 24.4 GB per step, 7.3 ms. Each user now gets about 137 tokens per second, and the GPU produces 64 ÷ 7.3 ms ≈ 8,800 tokens per second in total: 42 times more, for a 34% slower stream per user.

5

Is it still memory-bound? Compute for 64 tokens is 2 × 8 billion × 64 ≈ 1 trillion operations, about 1 ms at the H100’s 989 BF16 TFLOPS. The 7.3 ms read still dominates, so batching is close to free until the batch is in the hundreds.

Batch sizeRead per stepStep timePer user (tokens/s)Total (tokens/s)
116.1 GB4.8 ms208208
817.0 GB5.1 ms1961,572
3220.2 GB6.0 ms1665,308
6424.4 GB7.3 ms1378,791
12832.8 GB9.8 ms10213,082
25649.6 GB14.8 ms6817,306
Batch 1one user
208
Batch 87.6× the tokens
1,572
Batch 3225× the tokens
5,308
Batch 6442× the tokens
8,791
Batch 12863× the tokens
13,082
Batch 25683× the tokens
17,306

Total tokens per second from one H100, Llama 3.1 8B in 16-bit, 1,000 tokens of history per request. Ideal bandwidth ceilings, illustrative.

Two lessons fall out of this table, and every vLLM design choice follows from them. First, a serving engine should keep the batch as full as possible on every step, because an empty slot is throughput thrown away. Second, the batch is limited by how many requests’ KV caches fit in memory at once. Continuous batching solves the first problem. PagedAttention solves the second.

How do static, dynamic and continuous batching differ?

All three engines batch. The difference is when a request may join or leave the batch.

Static batching collects a fixed number of requests, runs them together until the last one finishes, then takes the next group. Answers vary in length, from a yes or no to a 2,000-word report, so the short answer finishes early and its slot sits idle until the long one is done, and new arrivals wait for the whole group.

Dynamic batching, common in general inference servers, closes a batch when it is full or when the oldest request has waited a set time. That cuts waiting when traffic is light, but once a batch starts it still runs to completion, so the idle-slot problem stays.

Continuous batching (iteration-level scheduling, from the Orca paper, Yu et al., OSDI 2022) makes the decision on every step. When a request writes its last token, it leaves, and a waiting request takes its slot on the very next step. Nobody waits for a slow neighbour and the batch stays full. The timelines below run the same 16 requests, arriving over time, through all three.

Static batchingwaits for 4 requests, then runs until the longest one ends 9 finished, wait 4.8 steps slot 1 slot 2 slot 3 slot 4 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 R12 Dynamic batchingalso starts when the oldest has waited 1 step; still runs to the end 9 finished, wait 4.2 steps slot 1 slot 2 slot 3 slot 4 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 Continuous batchingrefills any free slot on the very next step 15 finished, wait 0.1 steps slot 1 slot 2 slot 3 slot 4 R1 R2 R3 R4 R5 R6 R7 R8 R9 R10 R11 R12 R13 R14 R15 R16 step 0 step 5 step 10 step 15 step 20

Four batch slots over 20 steps, same requests and arrival times in each row (illustrative). Grey hatching is an idle slot. A red line sweeps across to show time passing. Continuous batching finishes more requests and almost nobody waits.

MethodWhen a request joinsWhen its slot freesResult in the timeline
StaticWhen a full batch has gathered and the GPU is freeWhen the longest request in the batch ends9 finished, average wait 4.8 steps, slots busy 55% of the time
DynamicWhen the batch is full or the oldest has waited 1 stepWhen the longest request in the batch ends9 finished, average wait 4.2 steps, slots busy 58%
ContinuousOn the next step after any slot freesAs soon as that request ends15 finished, average wait 0.1 steps, slots busy 81%

The cost is bookkeeping: every step is a different mix of requests, rebuilt every few milliseconds. That is the scheduler’s job.

What does the scheduler do on every step?

The scheduler keeps two lists. The running list holds requests that already have KV cache on the GPU. The waiting list holds new requests and any that were pushed out earlier for lack of memory (preempted), which go back to the front. Each step has a token budget, set by --max-num-batched-tokens, and a cap on requests, set by --max-num-seqs.

Recent vLLM versions treat prompt tokens and answer tokens the same way: the scheduler’s output for a step is simply a list of requests and how many tokens each will process. A decoding request needs 1. A new request needs its whole prompt, or a slice of it. This is what lets one forward pass mix prefill and decode, called chunked prefill, which is on by default.

Waiting list R7, newprompt 3,000 tokens R8, newprompt 12,000 tokens R9, newwaits for next step preempted requestsrejoin at the front One step, in order 1 Running first: 48 requests × 1 token = 48 2 Budget left: 8,192 − 48 = 8,144 tokens 3 Admit R7 whole: 3,000 tokens, 188 blocks 4 R8 gets a 5,144-token chunk, rest later 5 One forward pass, sample, free finished Running list 48 decoding1 new token each R7, prefill donedecodes from next step R8, partial5,144 of 12,000 read at most –max-num-seqsrequests at once Token budget for this step: 8,192 (–max-num-batched-tokens) R7 prompt 3,000 R8 chunk 5,144 48 decode tokens (thin dark sliver at the left) take almost none of the budget but are always served first, so a long prompt arriving never freezes the answers already streaming.

One scheduler step, highlighted in order (illustrative numbers). Decode tokens are scheduled first, then prompts fill whatever budget remains, and a prompt that does not fit is cut into chunks across steps.

What happens, step by step, every few milliseconds:

  1. Serve the running requests. Each decoding request gets its next token. If its last KV block is full, the cache manager must find it one new block first.
  2. Admit from the waiting list, in order. For each request the manager looks up prompt blocks already in the prefix cache, then allocates blocks for the rest. If free blocks run short, admission stops for this step.
  3. Chunk what does not fit. A prompt bigger than the remaining budget gets a slice now and the rest later.
  4. Run and clean up. One forward pass covers all of it. Tokens stream back, finished requests leave and their blocks return to the free pool.

The budget is the main dial between two latency numbers from part 13. A small budget (the vLLM docs use 2,048 as an example) keeps time between tokens steady because each step carries little prefill. A larger one, above 8,192, gets prompts through faster and raises throughput, at the cost of slightly longer steps for everyone decoding.

How does PagedAttention store the KV cache?

Continuous batching keeps slots full, but each running request needs its KV cache in GPU memory, and that is where older engines lost most of their capacity. They gave each request one contiguous region sized for the longest answer it might give, because they could not know in advance how long it would be. A request that wrote 200 tokens into a region reserved for 2,048 left 90% of it empty, and nobody else could use that space. Memory also fragmented as regions of different sizes came and went. The vLLM paper measured 60 to 80% of KV memory wasted this way.

PagedAttention borrows the operating system’s answer to the same problem, virtual memory. The KV cache is split into fixed-size blocks, 16 tokens each by default. A request sees its cache as a list of logical blocks, 0, 1, 2 and so on. A per-request block table maps each logical block to any free physical block in GPU memory. The attention kernel follows the table to find each token’s keys and values, so the physical blocks never need to be next to each other.

Logical blocks Block tables Physical blocks in GPU memory Request A, 55 tokens block 0 · 16 of 16 block 1 · 16 of 16 block 2 · 16 of 16 block 3 · 7/16 Request B, 19 tokens block 0 · 16 of 16 block 1 · 3 of 16 0 → physical 71 → physical 12 → physical 123 → physical 4 0 → physical 31 → physical 9 0 free 1 · A1 2 free 3 · B0 4 · A3 5 free 6 free 7 · A0 8 free 9 · B1 10 free 11 free 12 · A2 13 free 14 free Blocks sit anywhere; order does not matter. Only the last block of a request can be part full, so waste is at most 15 tokens per request. A uses 4 blocks, B uses 2, 9 stay free.

Block table mapping, the same idea as page tables in an operating system. Each request only holds blocks it has filled, plus one part-full block at the end.

What happens as a request grows and ends?

Blocks are handed out one at a time, as the request needs them. Follow request A from the diagram above:

After prefill55 tokens, 4 blocks last block 7 of 16 9 tokens later64 tokens, 4 blocks no allocation needed Token 65needs a 5th block new, from free Request endsblocks go back 5 blocks back in the free pool; full ones keep their contents for prefix-cache hits until evicted

Lifetime of one request’s blocks, highlighted stage by stage. A new block is taken only when the previous one is full; at the end every block goes back to the pool.

Because a request only ever holds blocks it has filled plus one partial block, the paper measured waste under 4%. The memory that older engines left empty now holds more requests, which, from the bandwidth table, means more tokens per step from the same GPU.

How do prefix caching and copy-on-write share blocks?

A block table is just a list of block numbers, so two requests can point at the same physical block. vLLM uses this in two ways.

Prefix caching. When a block is full, vLLM computes a hash from the hash of the block before it, the block’s own tokens, and extras such as a LoRA adapter ID. Because each hash includes its parent, a hash identifies the whole prompt up to that point, not just 16 tokens. When a new request arrives, the scheduler hashes its prompt block by block and looks each hash up. Every hit is a block whose keys and values are already in memory, so prefill skips those tokens. Only full blocks are cached. Each block carries a reference count; when it reaches zero the block goes to the free queue but keeps its contents and hash, so a later request can still hit it. Blocks are evicted least recently used first. Prefix caching has been on by default since vLLM’s V1 engine, because its overhead is close to zero.

Copy-on-write. When you ask for several answers to one prompt (parallel sampling, n greater than 1, or beam search), all answers share the prompt’s blocks. The paper’s design lets them share even the last part-full block, and copies it only when one answer writes a different token into it. Current vLLM versions get the same saving by running each extra sample as its own request that shares the prompt’s full blocks through the prefix cache.

Prefix caching: two chats, same 48-token system prompt system 0-15refs 2 system 16-31refs 2 system 32-47refs 2 h1 = hash(none, tokens) h2 = hash(h1, tokens) h3 = hash(h2, tokens) chat A question, own block chat B question, own block Chat B arrives after chat A: its first three hashes match, so 48 of its prompt tokens skip prefill. Copy-on-write: two samples of one prompt (n = 2) prompt 0-15 · refs 2 prompt 16-31 · refs 2 32-37 part full sample 1 writes in place sample 2 gets a copy first Full blocks stay shared for both samples. Only the block that is about to differ is copied.

Sharing blocks by reference count. Hash chaining means a hit on block 3 also proves blocks 1 and 2 match, so lookups stop at the first miss.

Prefix caching is what makes chat and agents cheap to serve. A chat resends its whole history every turn and an agent resends the same instructions and tool list every step; with those already in blocks, only the new text needs prefill. The cache only helps if the follow-up lands on the server that holds it, which is where llm-d comes in.

How much KV cache does an H100 give Llama 3.1 8B?

vLLM decides the size of the block pool at start-up. It loads the weights, runs a test forward pass at the largest batch it will schedule to measure peak activation memory, captures CUDA graphs, and gives everything that is left, within --gpu-memory-utilization, to KV blocks. Worked through for Llama 3.1 8B in 16-bit on one 80 GB H100 (overhead figure illustrative):

1

Budget. 80 GB × 0.9 (the default) = 72 GB that vLLM may use. The other 8 GB is headroom.

2

Weights. About 16 GB, leaving 56 GB.

3

Activations, CUDA graphs and runtime. Say 4 GB here; the real figure depends on the token budget and version. That leaves about 52 GB.

4

Tokens that fit. 52 GB ÷ 128 KB per token ≈ 396,000 tokens, which is about 24,800 blocks of 16.

5

Concurrency. At 8,192 tokens per request that is 48 requests of full length. At Llama 3.1’s full 128K context it is only 3. If real conversations average 1,500 tokens, about 260 fit at once.

weights 164KV cache blocks 52 GB8 free
weightsactivations and graphsKV block poolheadroom outside 0.9
Setup on one H100KV poolTokens of KVRequests at 8K eachRequests at 1,500 each
Reserved per request at 8K (old style)52 GB396,0004848, most space empty
Paged, 16-bit KV52 GB396,00048about 260
Paged, FP8 KV (--kv-cache-dtype fp8)52 GBabout 790,00096about 520
Paged, 16-bit, utilisation 0.9556 GBabout 427,00052about 280

Illustrative: 4 GB overhead assumed. Request counts above --max-num-seqs are capped by that setting, and long prompts need budget room too.

The first two rows show the whole PagedAttention story in numbers. The reserved engine and the paged engine have the same memory and the same worst case. They differ when real requests are shorter than the worst case, which is almost always: paged memory lets five times as many conversations run. The same arithmetic for a bigger model is in part 14.

What happens when the blocks run out?

vLLM admits requests based on what they need now, not what they might need later, so the pool can fill while answers are still growing. When a running request needs a new block and none is free, the scheduler preempts: it takes the most recently admitted request off the GPU, frees its blocks, and puts it back at the front of the waiting list. The log shows a warning that names the preemption mode.

ModeWhat happens to the preempted requestCostWhere you see it
RecomputeIts blocks are dropped. When it is readmitted, its prompt plus the tokens it already wrote are run again as one prefill.Extra GPU compute; with prefix caching some blocks may still be cached. Default in the V1 engine.That user’s stream pauses, then resumes; time between tokens spikes for them.
SwapIts blocks are copied to CPU memory over PCIe and copied back later.PCIe traffic, about 64 GB/s each way on PCIe 5.0, and CPU memory. Used by older versions.Same pause, plus PCIe load that can slow other work.

Nothing fails, but the user whose request was preempted sees their answer stall for a while. Occasional preemption under a burst is fine. Constant preemption means you are trying to run more concurrent work than the pool holds. The vLLM docs suggest raising --gpu-memory-utilization, lowering --max-num-seqs or --max-num-batched-tokens, or adding GPUs with tensor parallelism.

What is inside a running vLLM server?

A vLLM server is several processes, split so that CPU work (HTTP, tokenising, streaming) never makes the GPU wait.

ClientsOpenAI API API server process HTTP on port 8000/v1/chat/completionstokenise the promptdetokenise new tokensstream them back CPU only ZMQ Engine core process Schedulerwaiting and running liststoken budget, preemptionone decision per step KV cache managerblock pool and free queueblock tables, ref countsprefix hash lookup sends each step’s plan to workers Worker, GPU 0model runnerattention kernelsCUDA graphsKV blocks in HBM Worker, GPU 1same, other halfof every layer(tensor parallel) NCCL one worker per GPU in use

Process layout of a vLLM server with two-way tensor parallelism. Arrows show the request path; the dashes move in the direction work flows.

  • API server. Accepts OpenAI-style requests, applies the chat template, turns text into token IDs and streams tokens back. It runs in its own process and talks to the engine over ZeroMQ, so heavy tokenising does not hold up the GPU loop.
  • Engine core. A tight loop around the scheduler and the KV cache manager described above. Each step it decides who runs and how many tokens each gets, and sends only the changes since the last step to the workers.
  • Workers. One process per GPU. Each holds its share of the weights and of every KV block, and runs the forward pass. With tensor parallelism (part 5) every layer is split across GPUs, which exchange partial results with NCCL over NVLink.
  • CUDA graphs and compilation. A decode step for an 8B model is hundreds of small GPU kernels taking a few milliseconds in total, so launching each one from Python would waste a large share of the step. vLLM compiles the model with torch.compile and records the kernel sequence as CUDA graphs for common batch sizes, then replays a whole graph with one launch. --enforce-eager turns this off: start-up gets faster, decode gets slower.

Quantization (part 18), multi-LoRA (part 19), speculative decoding and expert parallelism for models like those in part 12 all plug into this same loop.

Which settings should you start with?

Most of vLLM’s tuning comes down to a handful of flags. These are starting points, not answers; measure on your own traffic.

FlagWhat it controlsGood starting value
--max-model-lenLongest prompt plus answer allowed. Sets the concurrency vLLM reports and whether it starts at all.What your app needs, e.g. 8192 or 16384, not the model’s 128K
--gpu-memory-utilizationShare of GPU memory vLLM may use; the rest after weights becomes KV blocks.0.9 (default); 0.92 to 0.95 on a GPU used only by vLLM
--max-num-seqsMost requests in the running batch.Start around 128 to 256, then tune against per-user speed (below)
--max-num-batched-tokensToken budget per step, shared by decode and prefill chunks.8192 for throughput; 2048 to 4096 for the steadiest stream
--tensor-parallel-sizeGPUs sharing one copy of the model.1 for 8B on an 80 GB GPU; 2 to 4 for 70B (part 20)
--kv-cache-dtypePrecision of the KV cache.fp8 on Hopper or newer to double the pool; check quality
--enable-prefix-cachingReuse of full prompt blocks across requests.Leave on (default in recent versions)
--enforce-eagerDisables CUDA graphs.Off; only for debugging or very fast restarts

A sensible first command for Llama 3.1 8B on one H100:

vllm serve meta-llama/Llama-3.1-8B-Instruct \
  --max-model-len 16384 \
  --gpu-memory-utilization 0.92 \
  --max-num-seqs 256 \
  --max-num-batched-tokens 8192

What does the start-up log tell you?

Three lines in the start-up log answer most sizing questions: how much memory the weights took, how many tokens of KV cache fit, and how many full-length requests that is. An example of their shape (illustrative numbers, and exact wording varies between versions):

# illustrative excerpt, not a real capture
INFO  Model loading took 14.99 GiB memory
INFO  GPU KV cache size: 396,272 tokens
INFO  Maximum concurrency for 16,384 tokens per request: 24.19x

Read the last line with care. “24x” does not mean vLLM will run only 24 requests; it means 24 requests could each use the full 16,384 tokens at once. Real requests are shorter, so more run, up to --max-num-seqs. If the number is below 1, vLLM refuses to start, because not even one maximum-length request would fit. Lower --max-model-len, raise the utilisation, quantize the KV cache or add a GPU.

How do you choose max-num-seqs?

From the bandwidth table, a bigger batch always gives more total tokens and always gives each user fewer. The right batch size is the largest one where every user is still fast enough. Plot both curves and draw your target on the left one:

Speed each user sees (tokens/s) Total from the GPU (tokens/s) 22014773018k12k6k0 example target: 100 tokens/s per user 1816326412825618163264128256 batch size (requests running)batch size (requests running) At batch 128 each user still gets about 102 tokens/s, and the GPU makes about 13,100: 63 times batch 1.

Same model and GPU as the bandwidth table, ideal ceilings (illustrative). Longer conversations pull both curves down faster because every request reads more KV cache per step.

In practice you run a load test at rising concurrency, watch time per output token at the 90th or 99th percentile, and stop raising --max-num-seqs just before it crosses your target. The demo’s tuning tab does this arithmetic for different GPUs, context lengths and targets.

How do llm-d and vLLM work together?

Everything so far happens inside one vLLM server. vLLM does not decide which of twenty copies of a model a request should go to, and the default Kubernetes answer, a Service that spreads requests in turn, throws away much of what vLLM built. A follow-up message in a chat has a one-in-eight chance of reaching the pod with its history cached when there are eight replicas. The other seven times a GPU prefills the whole conversation again. llm-d is the layer that fixes this on Kubernetes, and it uses vLLM as its default engine.

Clientchat, RAG, agent GatewayEnvoy, Gateway API ask Endpoint picker (llm-d inference scheduler) Queuewaiting per pod KV cachefree blocks per pod Prefixcached blocks hit filter unsuitable pods, add weighted scores, pick the best one to chosen pod InferencePool (Kubernetes pods, each one vLLM) Prefill pod 1vLLM, reads prompts Prefill pod 2vLLM, reads prompts KV blocks via NIXLover RDMA Decode pod 1writes answers Decode pod 2writes answers Decode pod 3writes answers each pod: continuous batching,PagedAttention, prefix cache metrics and KV events Tiered KV cache GPU HBM (fastest) CPU RAM local SSD or shared storage blocks spill down

llm-d with prefill and decode split. Without the split, the same vLLM pods do both stages and the NIXL transfer disappears; the router and its scoring stay.

1

vLLM in every pod. Each llm-d model server is an ordinary vLLM instance. Batching, paged memory and the prefix cache from this post all happen inside it.

2

A router that reads vLLM’s state. The gateway asks the endpoint picker where to send each request. The picker scores every pod on its waiting queue, its free KV blocks and how many of the request’s prompt blocks it already caches. It learns the last one from cache events that vLLM pods publish as blocks are stored and evicted, which only works because vLLM keeps its cache as hashed blocks.

3

Prefill and decode on separate pods. Prompt reading is compute-heavy and answer writing is bandwidth-heavy (part 13). llm-d can run them in different vLLM pools and move the finished KV blocks from the prefill pod to a decode pod using NVIDIA’s NIXL library over RDMA. This needs a fast network between nodes and helps most with long prompts at large scale.

4

Cache beyond one GPU. KV blocks evicted from HBM can be kept in CPU memory or on storage and brought back when a matching prompt arrives, so a hot system prompt does not have to be recomputed after a burst pushes it out.

The link between the two is the block. vLLM stores the KV cache as fixed-size hashed blocks; llm-d uses the same hashes to know where every prefix lives across the fleet, and moves and tiers the same blocks. A cache-aware router can only be as good as the cache it routes to.

When is vLLM alone enough?

Your situationWhat to runWhy
One model on one to four GPUs, one replicaPlain vLLMNothing to route between; vLLM already batches and caches
Two or three replicas, short unrelated promptsvLLM behind a normal load balancerLittle repeated text, so cache-aware routing saves little
Several replicas, chat, RAG or agents with long shared promptsvLLM plus the llm-d routerFollow-ups land where their prefix is cached; busy pods get less
Many teams sharing a GPU clustervLLM plus llm-d with prioritiesFair queueing and load shedding across tenants
Very large or long-prompt workloads on an RDMA networkllm-d with prefill and decode splitEach pool sized for its own bottleneck

What do people get wrong about vLLM?

  • Leaving max-model-len at the model’s maximum. Llama 3.1 offers 128K tokens of context. If your app never sends more than 8K, the default makes vLLM report tiny concurrency, and on smaller GPUs refuse to start at all.
  • Reading “maximum concurrency” as a hard limit. It counts full-length requests. Real ones are shorter, so far more run at once.
  • Expecting gpu-memory-utilization to cap the KV cache. It sets how much vLLM takes in total. Whatever the weights and activations leave inside that share all becomes KV blocks, so vLLM always looks as if it has filled the GPU. That is by design, not a leak.
  • Raising max-num-seqs until throughput stops growing. Throughput keeps rising long after users notice slow streams. Tune against time per output token, not total tokens.
  • Round-robin across replicas with long shared prompts. Each replica’s prefix cache only helps requests that reach it. Use session affinity at minimum, or a cache-aware router.

Short answers to common questions

Does vLLM make a single request faster?

Not much. One request is limited by memory bandwidth in any engine. vLLM’s gains are in how many requests one GPU can serve at a good speed. Speculative decoding and quantization are the features that help a single stream.

Why does vLLM take 90% of GPU memory as soon as it starts?

It allocates the whole KV block pool up front so it never has to ask the GPU for memory mid-request. Lower --gpu-memory-utilization if something else must share the GPU.

Does prefix caching change the answers?

No. A hit reuses keys and values that would have been computed the same way. It changes how fast the first token arrives, not what is written.

Do I need llm-d to use vLLM on Kubernetes?

No. A vLLM Deployment behind a Service works. llm-d becomes useful when you run several replicas of a model and your prompts share long beginnings.

Running vLLM well
  • Choose the smallest tensor-parallel size where weights fit and enough KV cache is left for your users.
  • Set max model length to what your application needs, not the model’s maximum.
  • Read the start-up log for KV cache tokens and maximum concurrency.
  • Try an 8-bit KV cache and keep prefix caching on when prompts share a beginning.
  • Watch time per output token as you raise batch size; stop before users notice.
  • Watch for preemption warnings; constant preemption means the pool is too small for your concurrency.
  • Add a cache-aware router such as llm-d when you run many replicas of the same model.

PagedAttention, block size and memory waste figures are from the vLLM paper (Kwon et al., SOSP 2023); launch throughput figures are from the vLLM project’s announcement. Continuous batching follows Orca (Yu et al., OSDI 2022). Scheduler behaviour, chunked prefill, preemption modes, prefix-cache hashing and eviction, CUDA graph modes and process layout are from the vLLM documentation (optimization and prefix caching design pages) and the vLLM V1 engine announcement; setting names and defaults can change between versions. llm-d details are from the llm-d project documentation. Bandwidth, memory and step figures are worked ideal ceilings, and the log excerpt and timelines are illustrative.

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading