loke.dev
Header image for What Nobody Tells You About the LLM Prefill Phase: Why Your First Token Is the Most Expensive 500ms in Your Stack

What Nobody Tells You About the LLM Prefill Phase: Why Your First Token Is the Most Expensive 500ms in Your Stack

A deep dive into the asymmetrical compute costs of the prefill vs. decoding phases and why your Time to First Token (TTFT) is the real bottleneck in modern AI UX.

· 8 min read

If you’ve ever sat staring at a blinking cursor for two seconds before an LLM finally starts vomiting text, you haven’t encountered a "slow connection." You’ve encountered the Prefill Phase—the most computationally violent moment in an LLM's lifecycle.

Most developers treat LLM latency as a single, flat metric: "tokens per second." But that's a lie. Your model has two completely different personalities. There is the Decoding Phase, where it lazily generates one word at a time, and then there is the Prefill Phase, where it tries to digest your entire 10,000-word system prompt in a single, massive parallel gulp.

If you want to build AI applications that actually feel snappy, you have to stop optimizing for throughput and start understanding why that first token is the most expensive 500ms in your entire stack.

The Two-Stroke Engine of an LLM

To understand why the first token is different, we have to look at how Transformers actually process data. When you send a prompt to a model like Llama 3 or GPT-4, the GPU goes through two distinct stages:

1. Prefill: The model takes your entire input (the prompt) and processes it all at once. It calculates the "hidden states" for every single token and populates the KV Cache (Key-Value Cache). This is highly parallel and compute-bound.
2. Decoding: The model generates the next token. Then it takes *that* token, adds it to the prompt, and generates the *next* one. This is sequential and memory-bound.

The irony? The GPU is actually *better* at the Prefill phase from a mathematical standpoint because it can use all its cores simultaneously. But because the Prefill phase has to deal with $N$ tokens at once, the computational cost scales. If your prompt is long enough, the prefill "wall" becomes the dominant factor in your user experience.

Why the First Token is "Special" (and Heavy)

In the decoding phase, we only care about the last token generated. But in the prefill phase, we care about the relationship between *every* token and *every other* token.

This is the $O(N^2)$ problem you heard about in college, but with a hardware twist. During prefill, the GPU performs a massive Matrix-Matrix multiplication ($GEMM$). It’s trying to saturate the Tensor Cores.

Let's look at a quick Python snippet to illustrate how we measure this discrepancy using the transformers library. This isn't production code; it’s a diagnostic tool to see the "Prefill Tax" in action.

import torch
import time
from transformers import AutoModelForCausalLM, AutoTokenizer

model_id = "meta-llama/Llama-3.2-1B" # Small for demonstration
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(model_id, torch_dtype=torch.float16).to("cuda")

prompt = "Explain the concept of quantum entanglement in the style of a pirate. " * 50 # Long prompt
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")

# Measure Prefill + First Token
start_prefill = time.perf_counter()
with torch.no_grad():
    outputs = model.generate(
        **inputs, 
        max_new_tokens=1, 
        use_cache=True, 
        return_dict_in_generate=True
    )
end_prefill = time.perf_counter()

# Measure Subsequent Decoding
input_ids = outputs.sequences
start_decode = time.perf_counter()
with torch.no_grad():
    model.generate(
        input_ids, 
        max_new_tokens=10, 
        use_cache=True
    )
end_decode = time.perf_counter()

prefill_time = end_prefill - start_prefill
decode_time_per_token = (end_decode - start_decode) / 10

print(f"Time to First Token (Prefill): {prefill_time:.4f}s")
print(f"Time per subsequent token: {decode_time_per_token:.4f}s")
print(f"Ratio: {prefill_time / decode_time_per_token:.2f}x")

When you run this, you’ll often find that the first token takes 10x to 50x longer than the second token. That gap is the time spent calculating the KV Cache for your prompt.

The KV Cache: The Memory Tax You're Paying

The KV Cache is the reason LLMs don't have to re-process the entire prompt for every single word they generate. Once we’ve calculated the "Keys" and "Values" for a token during the prefill phase, we store them in GPU memory (VRAM).

But here’s what nobody tells you: The KV Cache is huge.

For a Llama-3-8B model using 16-bit precision, the KV Cache consumes roughly:
$2 \times layers \times heads \times head\_dim$ bytes per token.

For a context of 8,000 tokens, you’re looking at roughly 1-2GB of VRAM just to "remember" the prompt. If you have 50 users hitting the server at once, your VRAM is gone before you even start generating text.

This leads to KV Cache fragmentation. If the GPU doesn't have a contiguous block of memory to store the prefill results, it has to swap data or stall, spiking your Time to First Token (TTFT) even further.

The "Context Window" Lie

Marketing materials love to brag about 128k or 1M token context windows. What they don't tell you is that the Prefill Phase for a 128k context can take several seconds, even on an H100.

If you are building a RAG (Retrieval-Augmented Generation) system and you're stuffing 20 PDF chunks into the prompt, you aren't just making the model smarter—you're making the user wait.

Why the math gets ugly:

In the attention mechanism, every token looks at every other token.
- If you have 1,000 tokens, that's 1,000,000 relationships.
- If you have 2,000 tokens, that's 4,000,000 relationships.

Even with optimizations like FlashAttention-2, which keeps these calculations on the fast SRAM of the GPU, the raw compute required for the prefill phase grows quadratically with prompt length.

How to Cheat the Prefill Phase

Since we can't break the laws of physics, we have to use engineering tricks to hide or reduce the prefill latency.

1. Prompt Caching (The Holy Grail)

If you have a massive system prompt (the "identity" of your bot) that doesn't change, why are you recalculating it every time? Modern inference engines like vLLM or providers like Anthropic and DeepSeek now support Prompt Caching.

They hash your prompt. If they see the same prefix twice, they load the KV Cache from a previous run directly from memory/disk.

2. Prefill Chunking

If you send a 10,000-token prompt, some engines will try to prefill all 10,000 tokens in one burst. This "busy-waits" the GPU and blocks other users. Chunked Prefill breaks the prompt into smaller pieces (e.g., 512 tokens at a time), allowing the engine to interleave the prefill of one request with the decoding of another.

3. Streaming is not just for show

Streaming isn't just about "looking cool." It's a UX necessity to mask the TTFT. However, if your prefill takes 800ms, streaming doesn't start until 800ms has passed.

Here is how you should handle streaming in a FastAPI context to ensure you're not adding overhead on top of the prefill latency:

import asyncio
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from langchain_openai import ChatOpenAI

app = FastAPI()
llm = ChatOpenAI(streaming=True)

async def generate_words(prompt: str):
    # The 'astream' call handles the prefill handover internally
    async for chunk in llm.astream(prompt):
        # We yield immediately to get that first token to the client
        yield f"data: {chunk.content}\n\n"

@app.get("/chat")
async def chat(prompt: str):
    return StreamingResponse(generate_words(prompt), media_type="text/event-stream")

The Hardware Bottleneck: Compute vs. Bandwidth

To really understand why prefill is the "expensive" part, you have to understand the hardware limit.

* Prefill is Compute-Bound: The GPU is waiting on its Tensor Cores to finish math. It’s limited by TFLOPS (Teraflops).
* Decoding is Memory-Bound: The GPU is waiting on its VRAM to send the next piece of data. It’s limited by Memory Bandwidth (GB/s).

When you have a short prompt, your TTFT is almost instant because the GPU has massive TFLOPS. But as the prompt grows, the $O(N^2)$ math eventually overwhelms the TFLOPS.

This is why an A100 GPU might feel as fast as a 4090 for small prompts, but will absolutely crush it on long-context prompts. The enterprise cards have the memory architecture to handle the massive KV Caches and the compute density to chew through the prefill phase without choking.

Practical Advice for Developers

If you're building an LLM-backed product today, here's how you deal with the Prefill tax:

1. Monitor TTFT vs. ITL: Don't just measure "latency." Track Time to First Token (TTFT) and Inter-Token Latency (ITL) separately. If TTFT is high, your prompt is too long or you're not using prompt caching. If ITL is high, your GPU is saturated or your batch size is too large.
2. Trim your RAG context: Don't just dump the top 10 search results into the prompt. Use a re-ranker (like BGE-Reranker) to pick the top 3. Every token you remove from the prompt reduces the prefill time quadratically.
3. Use a Specialized Inference Engine: If you are self-hosting, do not use raw Hugging Face generate(). Use vLLM, TensorRT-LLM, or TGI. These engines use PagedAttention, which manages the KV Cache memory much more efficiently, preventing the "memory fragmentation" that spikes TTFT.
4. System Prompt Hygiene: Put your most static information at the *beginning* of the prompt. Prompt caching mechanisms work by matching prefixes. If you put the user's current time or name at the very top, you break the cache for everything that follows.

The "First Token" UX

We often talk about AI as if it's a conversation, but from a systems perspective, it's more like a heavy industrial engine. It takes a lot of energy to get the flywheel spinning (Prefill), but once it's moving, it can maintain speed with relatively little effort (Decoding).

Your job as an engineer isn't just to pick the best model; it's to manage the flywheel. If you ignore the prefill phase, you're building an app that feels heavy and unresponsive, no matter how many tokens per second the model can output once it finally gets going.

Stop looking at the average speed. Start looking at the 500ms tax. That’s where the real optimization happens.