vLLM Inference Benchmarking: Prefix Caching & Batch Size Sweep

You can also read the article here on Substack and view the code on GitHub.

This documentation is fairly thorough, simply because I wanted to take the time to practice writing about the internals of transformer systems. The README for this project is much shorter and objective.

Internals of the System

Tokenization and Embedding

The input text fed into an LLM first goes through a process known as tokenization.

For example, "stemming" is a known facet of tokenization, in which a word like running gets reduced to run.

After the input text is split into tokens, each token is mapped to a specific integer ID, then looked up in an embedding table. This embedding can be thought of as the model's initial, standalone interpretation of the word, similar to looking up its definition in a dictionary, but in the model's internal vector space.

This embedding is a high-dimensional vector, in this case 3,584 dimensions.

The encoded input is then passed into the transformer architecture itself. This project uses Qwen2.5-7B: a causal, decoder-only transformer.

The 28 Decoder Layers

Each of the 28 layers performs exactly two operations, in sequence:

1. Attention

The goal of attention is to examine every token, determine which other tokens are relevant to it, and pull information from those tokens proportionally to how relevant they are.

This is done by taking each token's vector (3,584 numbers) and projecting it into three separate vectors, via three separate learned weight matrices:

  1. Query (Q) — "what am I looking for?"
  2. Key (K) — "what do I contain, that other tokens might want?"
  3. Value (V) — "what information do I actually hand over, if I'm picked?"

The concrete sequence of one token figuring out what to attend to:

  1. Compare its Query against every other token's Key. This is done via a dot product, multiplying matching numbers together and summing them. In linear algebra, a dot product encodes similarity: a high dot product signals "this Key strongly matches what I'm looking for." After this step, the token has a raw relevance score against every preceding token.

    Within this step, right after Q and K are computed and before the dot product comparison happens, each Q vector and each K vector are rotated by an angle determined by that token's position in the sequence. V is left untouched. (This is RoPE — Rotary Position Embedding.)
  2. Convert those scores into a probability distribution via softmax. The raw, unbounded scores become clean probabilities, e.g. "80% on token 3, 15% on token 7," and so on.
  3. Blend the Value vectors using those weights. Every other token's Value vector is scaled by its attention weight from step 2, then summed. The result is a new vector — the token's attention output, built as a weighted mixture of information pulled from the tokens it decided were relevant.

This whole mechanism is captured in one line:

Attention(Q, K, V) = softmax(QKᵀ / √d) · V

This doesn't happen just once — it runs in parallel across several heads. Qwen breaks from the usual 1-to-1 head matching: there are 28 query heads, but only 4 key/value heads. Every token still produces 28 separate Query projections, but those query heads are split into 4 groups of 7, and all 7 query heads within a group share the same Key and Value vectors. This is Grouped-Query Attention (GQA) — understanding this mechanism carefully matters, since it's central to this whole project.

2. Feedforward Block

Feedforward takes one token's vector and transforms it in place, without involving any other tokens. It's a two-layer network, though in Qwen's SwiGLU version there are actually three weight matrices, not two:

  1. Up-projection — expands the vector from 3,584 to a larger intermediate size.
  2. Gate projection — a second, separate weight matrix projects into that same larger size, then passes through a SiLU activation, a smooth function that lets positive values through and suppresses negative ones.
  3. Elementwise multiply — the up-projected vector and the SiLU-activated gate vector are multiplied together, elementwise. The gate vector acts like a set of dials, controlling how much of each up-projected value actually passes through.

The result is then multiplied by a third weight matrix, projecting it back down to 3,584 dimensions. Empirically, this gating mechanism lets the network learn more selective, expressive transformations for the same parameter budget.

As a token's vector passes through the 28 stacked layers, the raw numbers tend to drift, growing very large or shrinking. To counter this, Qwen uses RMSNorm as a "reset step" before real computation in each layer, rescaling the numbers back into a consistent, well-behaved range.

Finally: after the 28th layer and one last RMSNorm, a linear projection maps the 3,584-dimensional vector back up to vocabulary size, producing a probability distribution over every possible next token.

The Motivation

Consider this question:

Glucose is transported into the muscle cell:
A. via protein transporters called GLUT4.
B. only in the presence of insulin.
C. via hexokinase.
D. via monocarbylic acid transporters.
Answer: A

Chances are this triggers a minor flashback to every multiple-choice test you've ever taken. Notice the shape: the question, four labeled options, then Answer: followed by the correct letter.

Many eval datasets are built the same way. In particular, MMLU (Massive Multitask Language Understanding) is a multiple-choice benchmark comprised of 57 subjects.

MMLU, and most eval benchmarks, test the model on hundreds or thousands of separate questions. To keep the test comparable, every single question gets the same five examples in front of it — that consistency is what makes it a controlled benchmark rather than a moving target. For example:

[Question 1 + A/B/C/D + "Answer: A"]

[Question 2 + A/B/C/D + "Answer: C"]

[Question 3 + A/B/C/D + "Answer: B"]

[Question 4 + A/B/C/D + "Answer: D"]

[Question 5 + A/B/C/D + "Answer: A"]

Photosynthesis primarily occurs in which part of a plant cell?
A. Mitochondria
B. Nucleus
C. Chloroplast
D. Ribosome
Answer:

While this repetition is necessary for control and reproducibility, it's computationally wasteful in large benchmarks. The model repeatedly sees the same block of text, computes attention over it token by token, and only then moves on to the actual new information.

This project tests the usefulness of prefix caching. The logic: if two different requests happen to start with the exact same sequence of tokens (e.g. tokens 1 to 800, the fixed 5-shot block), then the K and V vectors for those first 800 tokens will come out identical both times, same input tokens, same model weights.

Prefix caching keeps the KV cache from the first request around even after that request finishes. When the next request comes in, vLLM checks: does this new prompt start with a token sequence I've already computed? If so, it reuses the cached prefix instead of recomputing it.

Experiment Configuration

With the motivation for efficiency established, here's the actual experiment. This multiple-choice format is a familiar one, our brains don't stumble over it since it's the same testing format most of us grew up with. With that context, the specific configuration being tested makes more sense.

Variable Values tested
--enable-prefix-caching on / off
--max-num-seqs 32, 128, 256
Prompt order sorted by length / shuffled
Client concurrency matched to each run's max_num_seqs

2 × 3 × 2 = 12 total runs.

max_num_seqs was tuned alongside caching because, in an initial experiment that didn't use concurrent request handling, caching showed no measurable benefit, not because caching doesn't work, but because with only one request in flight at a time, there was barely any "waiting" for the GPU to fill with useful cached work. Testing multiple max_num_seqs values was necessary to find out where caching's benefit actually shows up across different levels of concurrent load.

Results

Throughput (req/sec), averaged across sorted/shuffled prompt order, one run per configuration:

max_num_seqs caching off caching on
32 ~282 ~245
128 ~85 ~353
256 ~76 ~80

Accuracy was 0.736–0.742 in every configuration, essentially unchanged throughout, as expected, since these flags affect serving performance, not model correctness.

Findings

Known Limitations

Motivation

My motivation for beginning this benchmark is to improve my capabilities as a potential research engineer. I believe in science, research, and discovery, and want to use my skills as an engineer to support such avenues.