Author: João Felipe De Souza
Empirical profiling of decode-path batching in autoregressive transformers.
Measures how batch size affects throughput, per-step latency, GPU memory, and batching efficiency during the decode (token generation) phase — the dominant cost in LLM serving.
For detailed architecture and design rationale, see DESIGN.md.
In LLM serving, the decode phase generates tokens one at a time per request. But multiple requests can share a single forward pass. The key tradeoffs:
- Small batch: GPU underutilized, low throughput, low latency
- Large batch: high throughput, but latency per step increases
- Too large: memory-bandwidth bound, throughput saturates or drops
Finding the sweet spot is critical for production serving systems.
| Model | Params | Layers | Heads |
|---|---|---|---|
| gpt2 | 117M | 12 | 12 |
| gpt2-medium | 345M | 24 | 16 |
| Spec | Value |
|---|---|
| GPU | NVIDIA RTX 2070 |
| VRAM | 8.6 GB |
| Framework | PyTorch 2.13 |
| Library | Transformers 5.13 |
For each (model, context_length, batch_size):
- Prefill: process the full prompt in one forward pass, producing KV-cache
- Decode: generate 64 tokens autoregressively using cached KV
- Measure: prefill time, per-step decode latency, total throughput, peak memory
- Context lengths: 128, 512, 960
- Batch sizes: 1, 2, 4, 8, 16, 32, 64
- Total: 42 benchmark runs (21 per model)
- Key points repeated 3x for statistical confidence
- CUDA events for precise GPU timing
- 16 warmup steps before measurement
- 64 measured decode steps per configuration
See DESIGN.md for detailed rationale on timing, warmup, position limits, and OOM handling.
GPT-2, ctx=128:
| Batch | Throughput (tok/s) | Step latency (ms) | Gain vs bs=1 |
|---|---|---|---|
| 1 | 200 | 5.0 | 1.0x |
| 8 | 1332 | 6.0 | 6.7x |
| 32 | 5816 ± 248 | 5.5 ± 0.2 | 29.1x |
| 64 | 7649 ± 63 | 8.4 ± 0.1 | 38.2x |
Step latency stays nearly flat from bs=1 to bs=32. The GPU has spare compute capacity that batching fills for free.
| ctx | Sweet spot batch | Peak throughput |
|---|---|---|
| 128 | 64 | 7649 ± 63 tok/s |
| 512 | 32 | 2657 ± 17 tok/s |
| 960 | 16 | 1446 ± 7 tok/s |
Longer context means larger KV-cache per request, which fills memory bandwidth earlier and forces the sweet spot to a smaller batch.
GPT-2-medium, ctx=960:
| Batch | Throughput (tok/s) | Step latency (ms) | Peak mem (GB) |
|---|---|---|---|
| 1 | 99 | 10.1 | 0.20 |
| 16 | 548 ± 2 | 29.2 ± 0.1 | 3.19 |
| 32 | 362 ± 6 | 88.4 ± 1.4 | 6.38 |
| 64 | 90 | 709.8 | 12.76 |
At bs=64, throughput drops below bs=1 because peak memory (12.76 GB) exceeds physical VRAM (8.6 GB), causing implicit swapping.
The crossover point where step latency begins rising sharply:
| Model | ctx=128 | ctx=512 | ctx=960 |
|---|---|---|---|
| gpt2 | bs=4 | bs=16 | bs=8 |
| gpt2-medium | bs=32 | bs=16 | bs=8 |
This crossover is reproducible — repeated measurements show coefficient of variation below 5% at all key points.
Efficiency = actual throughput / (batch_size x bs=1 throughput):
| Model | ctx | bs=4 | bs=16 | bs=32 | bs=64 |
|---|---|---|---|---|---|
| gpt2 | 128 | 85% | 87% | 85% | 57% |
| gpt2 | 960 | 94% | 44% | 23% | 11% |
| gpt2-medium | 512 | 97% | 58% | 32% | 11% |
| gpt2-medium | 960 | 97% | 35% | 12% | 1.4% |
At gpt2-medium ctx=960 bs=64, each request gets only 1.4% of its solo throughput — the GPU is completely memory-bound.
Measured peak memory vs analytical KV-cache estimate:
| Model | Measured / Estimated ratio |
|---|---|
| gpt2 | 3.7 - 3.9x |
| gpt2-medium | 2.0 - 2.1x |
The analytical estimate underpredicts by 2-4x because it does not account for activation memory, framework overhead, and temporary buffers.
Latency-aware sweet spots deliver 90-97% of peak throughput at roughly half the per-step latency:
| Model | ctx | Peak batch | Recommended batch | % of peak | Latency reduction |
|---|---|---|---|---|---|
| gpt2 | 512 | 64 | 32 | 96.8% | 48% less |
| gpt2 | 960 | 32 | 16 | 95.2% | 47% less |
| gpt2-medium | 512 | 32 | 16 | 90.0% | 44% less |
| gpt2-medium | 960 | 16 | 16 | 100% | — |
Key benchmark points were repeated 3x. Run-to-run variance:
| Metric | Typical CV | Worst CV |
|---|---|---|
| Step latency | 0.2 - 1.6% | 4.9% |
| Throughput | 0.2 - 2.2% | 4.8% |
| Peak memory | 0.0% | 0.0% |
Single-run vs repeated mean difference: within ±5% for all points, majority within ±2%.
| File | Description |
|---|---|
| results/decode_batching_results.csv | Full benchmark (42 runs) |
| results/decode_batching_summary.csv | Sweet spot per model/ctx |
| results/repeated_key_points_raw.csv | 3x repeated raw measurements (63 rows) |
| results/repeated_key_points_agg.csv | Aggregated mean/std/min/max (21 configs) |
| results/pareto_points.csv | Pareto-optimal configurations |
| results/regime_transitions.csv | Latency growth between batch sizes |
| results/memory_model_validation.csv | Estimated vs measured memory |
| results/metadata.json | Run configuration |
| File | Description |
|---|---|
| plots/gpt2_decode_batching_dashboard.png | 4-panel GPT-2 dashboard |
| plots/gpt2-medium_decode_batching_dashboard.png | 4-panel GPT-2-medium dashboard |
| plots/cross_model_ctx*.png | Model comparison at each ctx |
| plots/pareto_throughput_vs_latency.png | Throughput-latency Pareto frontier |
| plots/regime_detection.png | Compute vs memory-bandwidth crossover |
| plots/memory_model_validation.png | Estimated vs measured KV memory |
| plots/roofline_throughput.png | Roofline-style throughput analysis |
decode-batching-profiler/
├── decode_benchmark.py # Main benchmark sweep
├── plot_decode_benchmark.py # Basic plots and summary
├── advanced_analysis.py # Pareto, regime, memory, roofline
├── repeat_key_points.py # Statistical confidence (3x repeats)
├── README.md
├── DESIGN.md
├── LICENSE
├── requirements.txt
├── results/
└── plots/
python3 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python3 decode_benchmark.py \
--models gpt2 \
--ctx-lens 128 512 960 \
--batch-sizes 1 2 4 8 16 32 64
python3 decode_benchmark.py \
--models gpt2-medium \
--ctx-lens 128 512 960 \
--batch-sizes 1 2 4 8 16 32 64
python3 plot_decode_benchmark.py
python3 advanced_analysis.py
python3 repeat_key_points.py
- GPT-2 max_positions=1024; context capped at 960 to fit 64 decode steps
- Single-prompt workload; real serving has variable prompt lengths
- Results specific to RTX 2070; different GPUs have different crossover points
- No continuous batching; all requests start and end together
- Same prompt replicated across batch; real workloads have heterogeneous lengths
- Yu et al., Orca: A Distributed Serving System for Transformer-Based Generative Models (2022)
- Kwon et al., Efficient Memory Management for Large Language Model Serving with PagedAttention (2023)