Deep Implementation

TTFT vs throughput across batch sizes

Bigger batches mean higher throughput but slower time-to-first-token. I traced this tradeoff from batch size 1 to 256 and found the point where adding more requests starts hurting everyone.

Every LLM serving system faces the same fundamental tension: throughput wants large batches, but latency wants small ones. Today I want to trace this tradeoff carefully, because understanding where the knee in the curve falls is what separates a well-tuned deployment from one that is either wasting money or frustrating users.

Why bigger batches improve throughput

During decode, the model weights are loaded from HBM once per step regardless of how many requests are in the batch. A single decode step for batch size 1 loads the full model (say, 14 GB for a 7B FP16 model) to produce one token. A decode step for batch size 64 loads the same 14 GB but produces 64 tokens. The memory bandwidth cost is amortized across the batch.

This is the roofline model in action. Decode is memory-bandwidth-bound, and batching raises the arithmetic intensity by doing more compute per byte loaded. As batch size increases, you move up the roofline diagonal toward the compute ceiling.

# Simplified throughput model
#
# H100 bandwidth: 3.35 TB/s
# Model size (7B FP16): 14 GB
# Time to load weights once: 14 GB / 3350 GB/s = 4.18 ms
#
# Batch size 1:   1 token  / 4.18 ms = 239 tok/s
# Batch size 8:   8 tokens / 4.18 ms = 1,914 tok/s
# Batch size 32:  32 tokens / 4.18 ms = 7,655 tok/s
# Batch size 128: 128 tokens / 4.18 ms = 30,622 tok/s
#
# In practice, compute cost grows with batch size too,
# but until you saturate the Tensor Cores, it is nearly free.

In practice the scaling is not perfectly linear because attention computation grows with the total number of cached tokens across the batch, and there are cache effects and other overheads. But the general shape holds: throughput scales roughly linearly with batch size until you hit a ceiling.

Why bigger batches hurt TTFT

Time-to-first-token (TTFT) measures how long a user waits before the first token appears. It has two components:

Larger batches hurt both. Queuing delay increases because the scheduler waits longer to fill a bigger batch (in static batching) or because existing requests occupy all slots (in continuous batching). Prefill time increases because the prefill computation for new requests competes with ongoing decode steps for GPU resources.

In continuous batching, the prefill of a new request interrupts the decode batch. While the GPU is running prefill for request N+1, all N existing requests in the decode batch are stalled. The larger the batch, the more requests are impacted by each prefill event.

Measuring the tradeoff

To make this concrete, let me walk through what happens at different batch sizes for a 7B model on an H100, with 512-token prompts and 256-token outputs:

# At batch size 1:
#   Prefill (512 tokens):  ~10 ms
#   Per-token decode:      ~4.2 ms
#   TTFT:                  ~10 ms
#   Total generation:      10 + 256 * 4.2 = 1,085 ms
#   Throughput:            256 / 1.085 = 236 tok/s

# At batch size 16:
#   Prefill (512 tokens):  ~12 ms (slightly slower due to batch context)
#   Per-token decode:      ~4.5 ms (weights loaded once, 16 tokens produced)
#   TTFT:                  ~12 ms + queuing delay
#   Per-request decode:    256 * 4.5 = 1,152 ms
#   Throughput:            16 * 256 / 1.164 = 3,519 tok/s

# At batch size 128:
#   Prefill (512 tokens):  ~25 ms (attention over large total context)
#   Per-token decode:      ~8 ms (nearing compute bound)
#   TTFT:                  ~25 ms + significant queuing delay
#   Throughput:            128 * 256 / 2.073 = 15,800 tok/s

# At batch size 256:
#   Prefill:               ~50 ms or more
#   Per-token decode:      ~16 ms (compute saturated)
#   TTFT:                  ~50 ms + large queuing delay
#   Throughput:            256 * 256 / 4.146 = 15,810 tok/s
#   Note: throughput stops scaling! You have hit the compute ceiling.

The saturation point

There is a batch size beyond which throughput stops improving. This is the point where decode transitions from memory-bound to compute-bound. On an H100 with a 7B FP16 model, this happens around batch size 128 to 192 for typical sequence lengths. Beyond this point, you are paying more TTFT and inter-token latency for no throughput benefit.

The exact saturation point depends on:

Operational strategies

In practice, you rarely pick a single batch size and stick with it. Instead, you set a maximum batch size and let the continuous batching scheduler fill up to that limit. The question becomes: what should that maximum be?

Here is how I think about it:

The golden rule

Never run past the throughput saturation point. You are trading latency for nothing. Find the knee in your throughput curve by benchmarking your specific model, GPU, and workload, and set your max batch size just below it.

The TTFT vs throughput tradeoff is not a problem to solve. It is a dial to set. The right setting depends on whether you are building a chatbot or a data pipeline, and the only way to find it is to measure your specific system.

Next up: moving from measurement to deployment. On day 53 I will build a production Dockerfile for vLLM that handles all the packaging details that benchmarking scripts ignore.