GPUaillminferencebandwidthbatching

LLM Inference Is a Memory Bandwidth Problem

The path from prompt to token runs through matrix operations on an accelerator, and the surprise is which resource binds. Generating tokens one at a time reads the entire model from memory per token, so inference is usually bandwidth-bound rather than compute-bound — which is why model size and memory bandwidth dominate the conversation.

▶ Run the labFollow the mechanism

Software view, hardware view

The gap between what you wrote and what the machine does is where this whole domain lives.

The question
When an agent sends a prompt to a model, what does the hardware actually do — and which resource runs out first?
What you wrote
An API call that returns tokens, with latency attributed vaguely to "the model being large".
What the hardware does
Two phases with different bottlenecks. Processing the prompt is a wide matrix multiplication over all tokens at once and is compute-bound. Generating each subsequent token is a narrow operation that must still read every model weight from memory, and is bandwidth-bound.
The two phases respond to completely different fixes, and conflating them produces the wrong optimisation. Compute-bound prefill is helped by more arithmetic; bandwidth-bound decode is helped by moving fewer bytes — a smaller model, lower precision, or batching so that one weight read serves many sequences.
SourceCompilerInstructionsFront EndExecutionRegistersCachesMemoryI/OBehavior

The path, end to end

A request arrives over the network, is tokenized, and becomes a sequence of vectors. Those pass through the model's layers as a series of matrix operations executed on an accelerator, with the weights living in the device's high-bandwidth memory. The output is a distribution over the vocabulary, one token is chosen, appended, and the process repeats for the next token.

The repetition is the crucial structural fact. Generating a hundred tokens is not one pass through the model; it is a hundred passes, each of which needs the weights again. Since the weights do not fit in any on-chip memory, each pass reads them from device memory afresh.

That makes the arithmetic stark. For a single sequence, generating one token performs a modest amount of arithmetic against every weight in the model — which is an extremely low arithmetic intensity, exactly the profile When the Memory Bus Is the Bottleneck describes. The accelerator has enormous arithmetic capacity and spends most of it waiting for weights to arrive.

prompttoken idsmatrix opsread weights — every tokenappend, repeatAgent / clientNetworkInference serverTokenizerModel runtimeAcceleratorDevice memory (weights + KV cache)Token
UserLLMAgentToolDataDecisionHumanGuardrail

Prefill and decode are different problems

Processing the prompt — prefill — handles all input tokens simultaneously. That is a wide matrix multiplication with high arithmetic intensity, the shape accelerators are built for, and it is typically compute-bound. It is also why time to first token scales with prompt length in a way that feels like real work, because it is.

Generating each output token — decode — processes a single position. The matrix operations become narrow, arithmetic intensity collapses, and the same full sweep of weights is required. Decode is therefore bandwidth-bound for a single sequence, and per-token latency is governed roughly by how long it takes to stream the model's weights out of memory.

This explains a set of otherwise puzzling observations directly: a long prompt costs a lot up front but subsequent tokens arrive at a steady rate; a model twice the size roughly halves the token rate even on hardware with plenty of spare arithmetic; and running one request at a time wastes most of the accelerator. It also explains why Inside One Model Call: Queue, First Token, Generation separates time-to-first-token from inter-token latency — they are two different hardware regimes, not two parts of one.

Two phases, two bottlenecks, two sets of fixes
Prefill (prompt)Decode (each output token)
Work shapeAll prompt tokens at once — wideOne position at a time — narrow
Arithmetic intensityHighVery low: full weight sweep for little arithmetic
Usual bottleneckComputeMemory bandwidth
Scales withPrompt lengthOutput length × model size
User-visible asTime to first tokenInter-token latency / tokens per second
What helpsMore arithmetic throughput; better parallelismFewer bytes: smaller model, lower precision, batching
What does not helpBatching, mostly — it is already wideMore arithmetic capacity; it is already idle

Batching: why serving is cheaper than chatting

If decode is bandwidth-bound because one weight read serves one sequence, the fix follows immediately: make one weight read serve many sequences. Batching several requests so their decode steps proceed together reads the weights once and applies them to every sequence in the batch, so arithmetic intensity rises with batch size and the accelerator moves back towards being compute-bound.

This is why the economics of serving many concurrent users differ so sharply from running a model for one user. The marginal cost of an additional sequence in an existing batch is small, because the dominant cost — streaming the weights — was already paid. It is also why a locally-run model on a single machine gets a small fraction of the tokens per second that a served deployment achieves with the same hardware.

The trade is latency against throughput, the same shape as Throughput Improved, Latency Did Not. Larger batches use the hardware better and increase total tokens per second, while any individual request may wait to be batched and shares the device with others. Continuous batching — adding and removing sequences as they arrive and finish rather than waiting for a fixed batch — is the standard way of getting most of the throughput without most of the latency penalty.

Relative bytes read from device memory per generated token, as batch size grows — 1 unit ≈ one full sweep of model weightsSIMPLIFIED
Batch of 1×1
Batch of 8×0.125
Batch of 32×0.031
Batch of 128×0.008
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
Batch of 1One full weight sweep serves one token; the accelerator is mostly idle
Batch of 8The same sweep serves eight sequences — cost per token divided by eight
Batch of 32Approaching compute-bound; arithmetic starts to be the constraint
Batch of 128Weight traffic is no longer dominant; KV cache and memory capacity now bind

Key points

  • Inference is two phases: prefill is wide and compute-bound, decode is narrow and bandwidth-bound.
  • Decode reads every model weight from memory for every generated token, giving very low arithmetic intensity.
  • Per-token latency for a single sequence is governed by how fast the weights can be streamed, not by arithmetic.
  • Batching makes one weight read serve many sequences, which is why serving many users is far cheaper per token.
  • The two phases need opposite fixes; optimising the wrong one produces no improvement at all.

Where the Data Is

Change an input and watch which number moves — and which one refuses to.

Where a value can be, and roughly what each costs relative to a register — 1 unit ≈ one register accessSIMPLIFIED
Register×1
L1 cache×4
L2 cache×14
L3 cache×45
DRAM×200
NVMe storage×100000
Network round trip×10000000
Ratios, not times. Absolute latencies depend on the processor, its clock, the memory it is attached to and what else is running — publishing them would be wrong everywhere except one machine. The bars are log-scaled, so each step is larger than it looks.
RegisterAlready in the core. Effectively free.
L3 cacheUsually shared between cores, so other work affects your hit rate.
DRAMTwo orders of magnitude past L1. This is the cliff.
NVMe storageAnother three orders of magnitude, and the OS gets involved.
Network round tripDifferent universe. Included to keep the earlier rows in perspective.

The exact ratios vary by machine and the absolute times vary far more, which is why none are shown. What is stable enough to build intuition on is the shape: each level is several times the one above, and the gap between the last cache level and memory is the one that decides most program performance.

Follow the mechanism

The path through the machine, hop by hop — and the conclusions it invites that are wrong.

  1. 1
    Prompt → tokenizer: text becomes token ids, then vectors the model can operate on.
  2. 2
    Runtime → accelerator: layers become matrix operations dispatched to the device.
  3. 3
    Accelerator → device memory: weights are read for the current step; for decode this is the full model per token.
  4. 4
    Device memory → accelerator: the bandwidth of this path sets the token rate whenever the batch is small.
  5. 5
    Output token → runtime: the token is appended to the sequence and the whole sweep repeats for the next one.
What people conclude from this — wrongly
  • "The accelerator shows low utilisation, so it is oversized" — during decode it is bandwidth-starved; the arithmetic units genuinely have nothing to do.
  • "A faster accelerator will speed up generation" — only if it also has more memory bandwidth, which is the binding resource.
  • "Batching adds latency, so avoid it" — it adds queueing latency and multiplies throughput; for serving, that is usually the correct trade.
  • "Prompt processing and generation are the same work" — they are different shapes with different bottlenecks and different fixes.

Consequences, controls and cost

What it causes
  • • Token rate falls roughly in proportion to model size, even when arithmetic capacity is plentiful.
  • • Single-user local inference achieves a small fraction of the throughput the same hardware reaches when serving.
  • • Time to first token and inter-token latency respond to different optimisations and must be measured separately.
  • • Memory capacity, not arithmetic, is frequently what decides whether a model can be served at all.
What you can do
  • • Batch concurrent requests — continuous batching where possible — so one weight read serves many sequences.
  • • Reduce bytes per weight through quantization, which directly raises the token rate in the decode phase.
  • • Keep the model resident on the device and avoid any per-request weight movement.
  • • Measure prefill and decode separately so effort goes to whichever phase actually dominates your traffic.
How to see it
  • • Report time to first token and inter-token latency separately; a single average conflates the two regimes.
  • • Compare achieved memory bandwidth against device peak during decode — near peak confirms bandwidth-bound.
  • • Sweep batch size and plot tokens per second against per-request latency to find the operating point you want.
  • • Track device memory occupancy of weights versus KV cache, since the latter grows with context and concurrency.
What it costs
  • • Batching raises throughput and per-request latency at the same time; the balance is a product decision.
  • • Quantization reduces bytes moved at some cost in output quality, and the cost is workload-dependent.
  • • Keeping large models resident consumes device memory that could otherwise hold KV cache for more concurrent sequences.

Scope

§224 — what these claims are specific to.

What these claims are specific to
  • GPU-SPECIFICDescribes transformer inference on accelerators with separate high-bandwidth device memory; architectures with different memory topologies, and non-transformer models, shift the balance.
  • SIMPLIFIEDOmits attention's own scaling with context length, KV cache growth, speculative decoding, mixture-of-experts routing and multi-device sharding — each of which changes where the bottleneck sits.

Misconceptions

Claim
“LLM inference is compute-bound because models are enormous.”
Reality
Model size is why it is *bandwidth*-bound: every weight must be read for every generated token, so the size determines bytes moved rather than arithmetic performed. Prefill is compute-bound; decode, which dominates most responses, is not.
Claim
“The GPU is at 20% utilisation, so we are wasting it.”
Reality
During single-sequence decode the arithmetic units genuinely have nothing to do while weights stream in. The fix is batching to raise arithmetic intensity, not a smaller device.
Claim
“Running the model locally should be as fast per token as the hosted service.”
Reality
The hosted service batches many users into each weight sweep. A single local sequence pays the full sweep for one token, which is why per-token rates differ so much on comparable hardware.

Where the rest of this lives

Programming Languages & Runtime Internals
Kernel fusion in model runtimes

Runtimes fuse adjacent operations to avoid writing intermediate activations back to device memory, which is a compiler technique applied directly to the bandwidth problem described here.