Continuous Batching: Optimizing LLM Inference Throughput › Throughput Analysis

Michael BrenndoerferJanuary 18, 20269 min read
Language AI HandbookAll chapters
Part 42 of 67Inference Optimization
Chapter 325Continuous Batching: Optimizing LLM Inference Throughput › Throughput Analysis
Reading section 7 of 14 · Throughput Analysis · 9 minSection 7 of 14 · 9 min

Book contents

Section 7 of 14 · 9 min read

Continuous Batching: Optimizing LLM Inference Throughput › Throughput AnalysisOne section · Sections in this chapter7 / 14 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Let's compare the throughput of static and continuous batching quantitatively. Understanding the mathematical relationship between these approaches helps explain why continuous batching delivers such significant improvements and under what conditions those improvements are largest.

The analysis that follows formalizes the intuition we built earlier: static batching allocates computational capacity based on the worst case in each batch, while continuous batching adapts to the actual workload token by token. The efficiency gap between these strategies grows with the variance of output lengths, which is exactly what characterizes real LLM workloads.

Theoretical Analysis

To understand the efficiency difference mathematically, we need to formalize what happens in each batching strategy. The key insight is that static batching allocates computational capacity based on the worst case, while continuous batching adapts to the actual workload.

For static batching with a fixed batch size BB containing requests with output lengths L1,L2,…,LBL_1, L_2, \ldots, L_B, the total computational time is determined by the longest request. Every request in the batch must wait for this slowest member, regardless of whether it has long since completed. The total computational capacity allocated, measured in token-slots (one slot processing one token), is:

Cstatic=B×max⁡(L1,L2,…,LB)C_{\text{static}} = B \times \max(L_1, L_2, \ldots, L_B)

where:

  • CstaticC_{\text{static}}: the total number of processing slots consumed (iterations ×\times batch size)
  • BB: the fixed batch size
  • LiL_i: the output length of the ii-th request
  • max⁡(L1,…,LB)\max(L_1, \ldots, L_B): the length of the longest sequence in the batch

This formula captures the basic problem: we allocate capacity based on the maximum, not the sum. Every slot runs for as many iterations as the longest sequence demands, even if that slot has been creating padding for 90% of those iterations.

We can calculate the compute efficiency ηstatic\eta_{\text{static}} as the ratio of useful work to total capacity. Useful work is the actual tokens generated, while total capacity is the processing slots allocated:

ηstatic=∑i=1BLiB×max⁡jLj\eta_{\text{static}} = \frac{\sum_{i=1}^{B} L_i}{B \times \max_{j} L_j}

where:

  • ηstatic\eta_{\text{static}}: the computational efficiency (between 0 and 1)
  • ∑i=1BLi\sum_{i=1}^{B} L_i: the total number of generated tokens (useful work)
  • B×max⁡jLjB \times \max_{j} L_j: the total capacity allocated during the batch's lifetime

Notice that efficiency depends heavily on the distribution of output lengths. If all requests have the same length, the numerator equals the denominator and efficiency is 100%. But if one request is much longer than the others, efficiency drops sharply.

To make this concrete, consider a batch of 8 requests where 7 need 20 tokens and 1 needs 200 tokens. We can work through the efficiency calculation step by step:

∑i=18Li=7×20+200=340 tokensB×max⁡jLj=8×200=1,600 token-slotsηstatic=3401,600≈0.21\begin{aligned} \sum_{i=1}^{8} L_i &= 7 \times 20 + 200 = 340 \text{ tokens} \\ B \times \max_j L_j &= 8 \times 200 = 1{,}600 \text{ token-slots} \\ \eta_{\text{static}} &= \frac{340}{1{,}600} \approx 0.21 \end{aligned}

The system performs 1600 computational operations but only 340 of them produce useful output. That is 79% wasted compute, or a 21% efficiency rating. The single outlier request forces seven other requests to continue burning resources for 180 extra iterations each.

For continuous batching, the situation is fundamentally different. Assuming the request queue remains populated (perfect slot filling), the system avoids waiting for long requests. When a request completes, its slot immediately fills with a new request. The total iterations required approach the theoretical minimum:

Tcontinuous≈∑i=1NLiBT_{\text{continuous}} \approx \frac{\sum_{i=1}^{N} L_i}{B}

where:

  • TcontinuousT_{\text{continuous}}: the approximate total iterations needed
  • NN: the total number of requests in the workload
  • LiL_i: the output length of the ii-th request
  • BB: the maximum batch size (concurrency limit)
  • ∑i=1NLi\sum_{i=1}^{N} L_i: the total number of tokens to be generated across all requests

This formula shows that continuous batching distributes work evenly across the available batch slots. The total iterations is simply the total tokens divided by the parallelism factor. Doubling BB halves the time, and adding more requests to a non-empty queue barely changes the total time because they fill slots freed by completions.

We can derive the efficiency ηcontinuous\eta_{\text{continuous}} by comparing useful work to allocated capacity:

ηcontinuous=Useful WorkTotal Capacity=∑i=1NLiTcontinuous×B≈∑i=1NLi(∑i=1NLiB)×B=1\begin{aligned} \eta_{\text{continuous}} &= \frac{\text{Useful Work}}{\text{Total Capacity}} \\ &= \frac{\sum_{i=1}^{N} L_i}{T_{\text{continuous}} \times B} \\ &\approx \frac{\sum_{i=1}^{N} L_i}{\left( \frac{\sum_{i=1}^{N} L_i}{B} \right) \times B} \\ &= 1 \end{aligned}

This shows that efficiency approaches 100% because the total capacity used matches the useful work. Every slot-iteration produces a real output token rather than padding. The approximation becomes exact when the queue is always full and requests are perfectly divisible across slots.

The practical implication is that continuous batching eliminates the sensitivity to output length distribution. Whether requests generate 10 tokens or 1000, the throughput stays near the theoretical maximum. Static batching is at the mercy of whoever happens to be in the same batch as you, while continuous batching processes everyone at their natural speed.

Empirical Comparison

Let's run a more complete simulation comparing the two approaches across a realistic workload. Theory tells us continuous batching should win, but simulation reveals the magnitude of improvement and how it varies with workload characteristics.

In[25]:
Code
def simulate_workload(
    num_requests: int, batch_size: int, output_lengths: List[int], method: str
) -> dict:
    """Simulate a workload with either static or continuous batching."""

    if method == "static":
        # Process in fixed batches
        total_iterations = 0
        total_latency = 0

        for batch_start in range(0, num_requests, batch_size):
            batch_end = min(batch_start + batch_size, num_requests)
            batch_lengths = output_lengths[batch_start:batch_end]
            max_len = max(batch_lengths)

            total_iterations += max_len
            # All requests in batch have same completion time
            for length in batch_lengths:
                total_latency += max_len  # Wait for slowest

        return {
            "total_iterations": total_iterations,
            "average_latency": total_latency / num_requests,
            "throughput": num_requests / total_iterations,
        }

    else:  # continuous
        # Simulate continuous batching
        remaining = list(output_lengths)
        active = []
        waiting_idx = 0
        iteration = 0
        completion_times = []

        while active or waiting_idx < len(remaining):
            # Fill batch with waiting requests
            while len(active) < batch_size and waiting_idx < len(remaining):
                active.append(
                    {
                        "idx": waiting_idx,
                        "remaining": remaining[waiting_idx],
                        "start": iteration,
                    }
                )
                waiting_idx += 1

            if not active:
                break

            iteration += 1

            # Decrement all active and check completions
            still_active = []
            for req in active:
                req["remaining"] -= 1
                if req["remaining"] <= 0:
                    completion_times.append(iteration - req["start"])
                else:
                    still_active.append(req)
            active = still_active

        return {
            "total_iterations": iteration,
            "average_latency": np.mean(completion_times),
            "throughput": num_requests / iteration,
        }
In[26]:
Code
# Generate a realistic workload with variable output lengths
num_requests = 100
batch_size = 8

# Output lengths following a log-normal distribution (common in real workloads)
output_lengths = np.random.lognormal(
    mean=4.0, sigma=0.8, size=num_requests
).astype(int)
output_lengths = np.clip(output_lengths, 10, 500).tolist()

static_results = simulate_workload(
    num_requests, batch_size, output_lengths, "static"
)
continuous_results = simulate_workload(
    num_requests, batch_size, output_lengths, "continuous"
)
Out[27]:
Console
Workload: 100 requests, batch size 8
Output length range: 10 - 448 tokens
Mean output length: 77.0 tokens

Static Batching:
  Total iterations: 2585
  Average latency: 202.5 iterations
  Throughput: 0.039 requests/iteration

Continuous Batching:
  Total iterations: 1107
  Average latency: 77.0 iterations
  Throughput: 0.090 requests/iteration

Improvement:
  Throughput gain: 2.34x
  Latency reduction: 62.0%

Continuous batching reaches materially higher throughput and lower average latency. While the static batch is held back by the longest sequence in each group, continuous batching processes requests at their natural speed, returning short responses quickly while long ones continue processing.

In[28]:
Code
# Test with different variance levels
variances = [0.2, 0.5, 0.8, 1.2]
static_throughputs = []
continuous_throughputs = []
static_latencies = []
continuous_latencies = []

for var in variances:
    lengths = np.random.lognormal(mean=4.0, sigma=var, size=100).astype(int)
    lengths = np.clip(lengths, 10, 500).tolist()

    static = simulate_workload(100, 8, lengths, "static")
    continuous = simulate_workload(100, 8, lengths, "continuous")

    static_throughputs.append(static["throughput"])
    continuous_throughputs.append(continuous["throughput"])
    static_latencies.append(static["average_latency"])
    continuous_latencies.append(continuous["average_latency"])
Out[29]:
Visualization
Grouped bar chart comparing throughput of static versus continuous batching at four variance levels. Continuous batching bars are taller at all variance levels.
Throughput comparison across output length variances. Continuous batching maintains consistently high throughput as variance increases, while static batching degrades because higher variance means more extreme outlier lengths that force the entire batch to wait.
Grouped bar chart comparing latency of static versus continuous batching at four variance levels. Static batching latency grows with variance while continuous remains low.
Average latency comparison across output length variances. Continuous batching latency stays low and stable, while static batching latency grows sharply with variance because short requests must wait progressively longer for their batch's outlier to finish.

The improvement from continuous batching grows with output length variance. When all requests generate similar lengths, static batching loses less to waiting because the maximum is close to the average. But real workloads have high variance: some users ask for single-sentence summaries while others request long document analyses. This variance makes continuous batching useful in practical deployments.