LLM serving has two distinct phases with different hardware profiles: prefill (computing KV cache for the input prompt) and decode (autoregressive token generation). Treating them as one workload is the most common cause of latency hot-spots in production. The RTX 4090 24GB is well balanced for both: 660 TFLOPS dense FP8 keeps prefill fast, and 1008 GB/s GDDR6X bandwidth keeps decode reasonable. This benchmark reports isolated numbers on a stock UK 4090 host across all the quantisation variants we ship in production, so you can reason about latency budgets, set max-num-batched-tokens correctly, and decide whether disaggregated serving is worth the operational complexity.
Contents
- Why split prefill and decode
- Methodology
- Prefill benchmarks
- Decode benchmarks
- Mixed workloads and chunked prefill
- Prefix caching effect
- Disaggregated serving
- Production gotchas
Why split prefill and decode
Prefill is compute-bound. It runs a single large GEMM per layer over the whole prompt, so you read each weight matrix once and amortise that read across all input tokens. Doubling prompt length roughly doubles latency but also doubles work; throughput in tokens-per-second stays close to the GPU’s tensor-core ceiling. On a 4090 with FP8 weights this lands around 12,000 t/s for an 8B model.
Decode is memory-bound. Each step generates one token but still reads the entire weight matrix to do so. Doubling batch size barely changes per-step latency because the work is dominated by VRAM reads, not by FLOPs; instead you double aggregate throughput at constant per-user latency. On the 4090, decode throughput is gated by the 1008 GB/s memory bandwidth and lands around 195 t/s single-stream for Llama 3 8B FP8.
A long-prompt request (think RAG with 4000 tokens of retrieved context) spends most of its TTFT in prefill; once decoding starts, throughput is gated by VRAM bandwidth. Designing SLAs and batches without separating these is a common cause of latency hot-spots, especially when a long prompt arrives while many short ones are decoding.
Methodology
All numbers from a stock 4090 (450 W TDP, no power cap), Ryzen 9 7950X, 64 GB DDR5-5600, Ubuntu 24.04, driver 560.x, CUDA 12.6. Server is vLLM 0.6.4 with PyTorch 2.5 and FlashAttention 2.6. Prefill and decode are isolated by separate runs: for prefill we issue a single request of the target prompt length and 1 output token, measure end-to-end minus a 5 ms scheduler floor, and report tokens/second as prompt_len / latency. For decode we use a 32-token prompt and 256-token output, measure inter-token latency over the last 200 tokens, and report 1 / median_step_latency. Each measurement is the median of 20 runs after a 60-second warm-up.
# Decode microbenchmark
import time, requests
prompt = "Write a short reply: hi" # 32 tokens
r = requests.post("http://127.0.0.1:8000/v1/completions",
json={"model": "llama-3-8b-fp8", "prompt": prompt,
"max_tokens": 256, "stream": True}, stream=True)
times = []
last = time.time()
for line in r.iter_lines():
if line and line.startswith(b"data:"):
now = time.time()
times.append(now - last)
last = now
print(f"median inter-token: {sorted(times)[len(times)//2]*1000:.1f} ms")
Prefill benchmarks
Single request, FP8 weights with FP16 activations where applicable, varying prompt length. Numbers below are tokens/second of input processed.
| Model | Quant | Prompt 512 | Prompt 2048 | Prompt 8192 | Latency 2048 |
|---|---|---|---|---|---|
| Llama 3.1 8B | FP8 | 11,800 t/s | 12,200 t/s | 11,400 t/s | 168 ms |
| Llama 3.1 8B | AWQ INT4 | 6,900 t/s | 7,200 t/s | 6,800 t/s | 284 ms |
| Mistral 7B | FP8 | 13,200 t/s | 13,500 t/s | 12,800 t/s | 152 ms |
| Qwen 2.5 14B | AWQ | 3,900 t/s | 4,000 t/s | 3,800 t/s | 512 ms |
| Qwen 2.5 32B | AWQ | 2,150 t/s | 2,200 t/s | 2,050 t/s | 930 ms |
| Llama 3.1 70B | AWQ INT4 | 1,750 t/s | 1,800 t/s | 1,700 t/s | 1,138 ms |
| Phi-3 mini | FP8 | 22,400 t/s | 23,100 t/s | 21,700 t/s | 89 ms |
Llama 3 8B FP8 prefill at 12,200 t/s means a 2048-token RAG prompt takes ~170 ms. That dominates TTFT in retrieval workloads, so latency-sensitive UX must either trim context or use prefix caching aggressively. AWQ is roughly 40% slower than FP8 at prefill on the same model because the dequant kernel adds work to every GEMM; the trade-off is acceptable for 14B+ models where AWQ unlocks fit-on-card.
Prefill scaling with prompt length
Prefill throughput is roughly flat across prompt lengths for models up to 14B because the kernel is launch-bound at very short prompts and bandwidth-bound at long ones. For 32B+ models, prefill drops by 5-8% at 8k tokens because L2 starts thrashing.
Decode benchmarks
Decode-only steady-state, batch 1, no contention. Decoder reads the full weight matrix per step, so throughput tracks model size and quantisation overhead inversely.
| Model | Quant | Decode t/s | VRAM bandwidth used | Per-token energy |
|---|---|---|---|---|
| Llama 3.1 8B | FP8 | 198 | ~85% of 1008 GB/s | 1.4 J |
| Llama 3.1 8B | AWQ INT4 | 168 | ~70% | 1.6 J |
| Mistral 7B | FP8 | 215 | ~83% | 1.3 J |
| Qwen 2.5 14B | AWQ | 135 | ~80% | 2.3 J |
| Qwen 2.5 32B | AWQ | 65 | ~78% | 5.0 J |
| Llama 3.1 70B | AWQ INT4 | 23 | ~75% | 14.8 J |
| Phi-3 mini | FP8 | 480 | ~70% | 0.56 J |
Decode is bandwidth-bound on the 4090; the percentages above are the fraction of theoretical 1008 GB/s actually consumed during inference. The 70B at 75% of peak with INT4 weights is excellent and explains why the 4090 holds its own against the 5080 on this workload despite the latter’s newer architecture: 70B fits more cleanly in 24 GB than in 16 GB, so the larger card is not bandwidth-starved on speculative decode.
Aggregate decode at batch
Aggregate decode throughput rises substantially with batch because weight reads amortise. For Llama 3 8B FP8: batch 8 hits 880 t/s aggregate, batch 16 hits 1,020, batch 32 hits 1,100, batch 64 saturates at 1,140. The per-user rate falls correspondingly: 110 t/s at batch 8, 64 at batch 16, 34 at batch 32. Match batch to your SLA on per-user t/s, not to your aggregate target.
Mixed workloads and chunked prefill
vLLM continuous batching interleaves prefill and decode. When a long prompt arrives, decode for in-flight requests pauses for one or two iterations. We measured the worst-case decode jitter on Llama 3 8B FP8 at 70 ms when a single 8192-token prompt is admitted alongside 16 ongoing decodes. Setting --max-num-batched-tokens 4096 bounds this pause to under 30 ms because the 8k prompt is split across two iterations.
| Setting | Long-prompt decode jitter | Steady throughput hit |
|---|---|---|
| No chunked prefill | 70 ms p99 | baseline |
| –max-num-batched-tokens 8192 | 50 ms p99 | -2% |
| –max-num-batched-tokens 4096 | 30 ms p99 | -4% |
| –max-num-batched-tokens 2048 | 18 ms p99 | -9% |
Prefix caching effect
For chatbots with stable system prompts, vLLM’s prefix cache eliminates 60-90% of prefill cost. Effective prefill rate climbs above 40,000 t/s because the GPU only needs to compute the unique tail. A 1500-token system prompt + 50-token user message goes from 140 ms cold prefill to 15 ms hot. See the vLLM setup guide for the flag combination we recommend and the concurrent users benchmark for the downstream effect on capacity.
Disaggregated serving
For RAG and document Q&A workloads where prompts are long but replies are short, consider disaggregated serving: run prefill on a dedicated 4090 (compute-rich) and decode on cheaper memory-rich GPUs (or the same card with a separate scheduler instance). This is operationally non-trivial and most teams should defer it until they hit a clear bottleneck. The break-even is around 4,000+ requests per minute with average input 2,000+ tokens. For most teams a single 4090 per replica is the simpler choice; operational notes live in the FP8 Llama deployment and 70B INT4 deployment guides.
Production gotchas
- Long prompts starve decode if chunked prefill is off. Always run with
--enable-chunked-prefillin production. The 4% throughput cost is recovered by lower jitter and fewer SLA violations. - AWQ prefill is materially slower than FP8. If you can fit the model in FP8 (i.e., 7-13B on a 24 GB card), prefer FP8 for prefill-heavy workloads.
- FP8 KV cache halves decode bandwidth pressure. Always pair
--quantization fp8with--kv-cache-dtype fp8; without it, decode throughput drops 25%. - Prefill power spikes. Prefill briefly hits 410 W; if your power budget is tight, cap with
nvidia-smi -pl 380as documented in the power draw post. - Prefix cache invalidates on max_model_len changes. Restart the worker after any config change touching context length.
- First-token p99 is dominated by long-tail prompts. If 1% of your prompts are 16k tokens, your p99 TTFT will follow them, not your steady-state. Bucket and reject pathological prompts at the gateway.
- Speculative decoding helps only at low concurrency. Draft-model speculative decoding can boost single-stream decode by 30-50% but breaks down at batch above 4 because draft validation costs dominate. Use it for voice and coding inline only.
Verdict
The 4090’s prefill and decode profile is what makes it the right card for general-purpose LLM serving below the 70B tier. Prefill at 12k t/s lets you serve RAG with sub-second TTFT on most prompts; decode at 195 t/s single-stream and 1,100 t/s aggregate batch 32 is comfortable for chat-grade SLAs. For longer-context or vision workloads where prefill dominates, the larger 5090 or 6000 Pro pulls ahead; see the 4090 vs 5090 decision. For broader benchmarks see the Llama 3 8B benchmark, Qwen 14B benchmark, Qwen 32B benchmark, and Llama 70B INT4 benchmark.
Tune prefill and decode independently
Lower TTFT, smoother token streams. UK dedicated GPU hosting.
Order the RTX 4090 24GBSee also: Llama 3 8B benchmark, concurrent users, vLLM setup, FP8 Llama deployment, AWQ guide, tokens per watt, FP8 tensor cores, 5060 Ti chunked prefill.