Vol. 14 · LLM Foundations

LLM Internals: KV Cache & Generation

An in-depth analysis of the mechanics of inference: how exactly KV-cache works and why O(n²) without it, prefill vs decode phases, GPU memory arithmetic, GQA/MQA/PagedAttention, prompt caching at the API level (Anthropic + OpenAI), sampling from logits to token, speculative decoding, and the main case - “mother washed the frame” through LLM step by step.

01

KV-Cache: mechanics and why without it O(n²)

4 cards
01KV-CacheWhy is autoregressive generation without KV-cache catastrophically slow?

Decoder models generate one token at a time. Without optimization, each step requires a complete recalculation of attention for ALL previous tokens. 200 response tokens = 200 × N² operations. KV-cache is a solution that turns this into linear cost.

Decode workbenchReuse the past instead of projecting it again.

Add output tokens, then compare cached and uncached decode.

Decode mode
Sequence
K projection
V projection
Current queryq₄Attention read
This step
Cumulative K/V projections
Reused slots

KV cache keeps prior keys and values. The new query still reads the growing sequence, but old K/V projections are not recomputed.
Diagramtext
WITHOUT KV-CACHE: quadratic hell

Generating the t₅ token requires attention throughout the entire sequence:
  Q = W_Q · [t₀, t₁, t₂, t₃, t₄, t₅] ← 6 tokens
  K = W_K · [t₀, t₁, t₂, t₃, t₄, t₅] ← 6 tokens
  V = W_V · [t₀, t₁, t₂, t₃, t₄, t₅] ← 6 tokens
                                           ↑ We are counting everything!

Total for 200 tokens: 1+2+3+...+200 = 20100 matmul steps
Complexity: O(n² × d_model) ← disaster on long prompts

───────────────────────────────── ─────────────────────────────────
With KV-CACHE: linear decode

Key observation: K and V for t₀..t₄ do not change!
  Why recount what has already been counted?

kv_cache = {
  K: [k₀, k₁, k₂, k₃, k₄], ← calculated once, stored
  V: [v₀, v₁, v₂, v₃, v₄], ← calculated once, stored
}

Generation t₅:
  q₅ = W_Q · t₅ ← new token only
  k₅ = W_K · t₅ ← new token only
  v₅ = W_V · t₅ ← new token only
  kv_cache.K.append(k₅) ← update the cache
  kv_cache.V.append(v₅)
  out = attention(q₅, kv_cache.K, kv_cache.V) ← O(seq) per step

Total for 200 tokens: 200 matmul steps ← O(n) at the decode phase
Prefill (prompt processing) is still O(n²) - but only once

practical effect

  • 10-100× decode phase acceleration
  • price: GPU memory - cache grows with length
  • cache is stored per-layer, per-head
02KV-CacheWhat is stored in KV-cache: anatomy

KV-cache is not a “query cache”, it is a specific data structure: two tensors K and V for each layer, each head, each token. Q is NOT put into the cache - only Q of the new token is needed for the next step.

cache structuretext
# Cache form for one model:
kv_cache.shape = [
  2, #K and V (two tensors)
  num_layers, #32 for Llama-3 8B
  num_kv_heads, #8 (GQA) or 32 (MHA)
  seq_len, # current length (growing!)
  d_head #128 = 4096 / 32
]

# Q is NOT cached because:
# Q is only needed to calculate the current step
# q_i = W_Q t_i - only for new token i
# K and V are ALWAYS needed with each new token

# At each decode step:
kv_cache.K[layer][head].append(k_new)
kv_cache.V[layer][head].append(v_new)
# seq_len increases by 1 → cache grows
03KV-CacheCausal Mask and KV-cache: why Query should not be stored

Causal mask in the decoder-only model provides a key property: when generating token t_i, attention weights to t_i do not affect t₀..t_{i-1} - they have already been generated. This makes caching K and V correct.

Diagramtext
WHY IS IT MATHEMATICALLY CORRECT TO CACH K,V?

Causal attention matrix (lower triangle):

       t₀ t₁ t₂ t₃
  t₀ [α₀₀ 0 0 0 ]
  t₁ [α₁₀ α₁₁ 0 0 ]
  t₂ [α₂₀ α₂₁ α₂₂ 0 ]
  t₃ [α₃₀ α₃₁ α₃₂ α₃₃]

When calculating t₃, k₀,k₁,k₂ and v₀,v₁,v₂ are used
These values will not change in subsequent steps:
  k₄ will not be added to k₀, k₀ will remain k₀

→ It is safe to cache K,V: they are immutable after writing
→ Only K and V of the new step are added to the end of the cache
→ Q of the new step is calculated on the fly and is not needed later
04KV-CacheKV-cache and context length: what breaks at 128K

As the context window grows, KV-cache grows linearly. At 128K tokens, the cache becomes huge - more than the weights of the model itself. This is what makes long-context inference so expensive.

cache growth at different lengthstext
# KV-cache size formula (fp16):
kv_bytes = 2 × L × H_kv × S × d_h × 2

# Llama-3 8B: L=32, H_kv=8 (GQA), d_h=128
S= 4,096: 2 × 32 × 8 × 4096 × 128 × 2 = 537 MB
S= 32,768: 2 × 32 × 8 × 32768 × 128 × 2 = 4.3 GB
S=128,000: 2 × 32 × 8 × 128,000 × 128 × 2= 16.8 GB
                                     ↑ MORE models (8GB)

# GPT-4 levels (96 layers, 128 heads, d_h=128, MHA):
S=128,000: 2 × 96 × 128 × 128,000 × 128 × 2= ~640 GB
          ← requires several A100 only for KV-cache!

why GQA saves the day

  • GQA: H_kv=8 instead of 32 → 4× smaller cache
  • MQA: H_kv=1 → 32× smaller cache
  • compromise: slightly worse quality vs MHA
02

Prefill vs Decode: two different worlds

3 cards
05PhasesPrefill and Decode: fundamentally different bottlenecks

LLM inference - two stages with opposite load characteristics. Prefill - matmul-bound: processes all prompt tokens in parallel, GPU is busy. Decode - memory-bound: each step reads the entire KV-cache from memory, the GPU is almost idle. Understanding the difference means optimizing correctly.

Diagramsql
PREFILL PHASE

  Input: entire prompt [t₀, t₁, ..., t_N] at once
  What it does: simultaneous forward pass for N tokens
              builds KV-cache for all positions
  Bottleneck: COMPUTE (matmul) ← GPU is 100% busy
  Metric: TTFT (Time-To-First-Token)
  Complexity: O(N²) — quadratic of prompt length

  "mom washed the frame" (4 tokens) → prefill 1 forward pass
  long RAG prompt (2000 tokens) → 500000× more expensive

───────────────────────────────── ─────────────────────────────────
DECODE PHASE

  Input: ONE token per step (last generated)
  What it does: forward for 1 token + reads the entire KV-cache
              adds k_new, v_new to cache
  Bottleneck: MEMORY BANDWIDTH ← reading KV-cache from HBM
  Metric: TPS / TPOT (Tokens Per Second, Time Per Output Token)
  Complexity: O(S) per step, O(N×S) total

  GPU utilization during decode: 520% ← GPU is empty!
  All the time is busy reading KV-cache from memory

───────────────────────────────── ─────────────────────────────────
LATENCY METRICS

  TTFT = Time To First Token ← prefill latency
  TPOT = Time Per Output Token ← decode speed
  TPS = Tokens Per Second1/TPOT
  E2E = TTFT + N_output × TPOT ← total latency

  Streaming UX: TTFT is more important than E2E
  Batch offline: more important than TPS

Bottleneck-type optimizations

  • TTFT → shorten prompt / prompt caching
  • TPS → reduce KV-cache (GQA/MQA)
  • continuous batching → decode GPU utilization increases
06PhasesChunked Prefill: don't freeze decode

Problem: a long prefill (2K+ tokens) blocks the GPU and delays TTFT for other users in the batch. Chunked prefill splits the prefill into parts, interspersed with decode - improves the uniformity of latency.

mechanicstext
# Without chunked prefill:
User A prefill: [────────────────── 2000 tokens ──]
User B decode: [waiting]
                  ← B is blocked for the entire time prefill A

# With chunked prefill (chunk=256):
User A chunk 1: [──256──]
User B token 1: [─1─]
User A chunk 2: [──256──]
User B token 2: [─1─]
...
← A finishes a little later, but B doesn't freeze
← latency tail (p99) improves significantly

# Implementation: vLLM 0.4+, TensorRT-LLM
07PhasesContinuous Batching vs Static Batching

Static batching: requests are grouped by N and wait for the entire batch to complete. The problem is that the longest request in the batch makes everyone wait. Continuous batching: requests enter and exit the batch dynamically at each decode step.

StaticContinuous
Batch formationtimes in N requestseach decode step
New request includedwaiting for the next batchright away
GPU utilizationlow at different lengthshigh
Throughput2–3× lessmaximum
Implementationjustdifficult (KV scheduling)
Who usesold systemsvLLM, TGI, TRT-LLM
03

Memory Math: GQA, MQA, PagedAttention

3 cards
08MemoryMHA → GQA → MQA: evolution of heads to save cache

Multi-Head Attention keeps separate K and V for each head - maximum quality, maximum KV-cache. GQA and MQA - progressive reduction in the number of K/V goals while maintaining the number of Q goals. Llama-3, Mistral, Gemma use GQA.

Diagramtext
MHA: Multi-Head Attention (GPT-3, original transformer)

  num_heads_Q = 32
  num_heads_KV = 32 ← K,V for each Q-head

  Q-heads: [Q₀ Q₁ Q₂ ... Q₃₁]
  K-heads: [K₀ K₁ K₂ ... K₃₁] ← 32 separate Ks
  V-heads: [V₀ V₁ V₂ ... V₃₁] ← 32 individual Vs

  KV-cache size: 2 × 32 × S × d_h × 2 bytes ← basic

───────────────────────────────── ─────────────────────────────────
GQA: Grouped Query Attention (Llama-3, Mistral, Gemma)

  num_heads_Q = 32 ← same number of Q-heads
  num_heads_KV = 8 ← a group of 4 Q-heads shares one KV-head

  Q-groups: [Q₀ Q₁ Q₂ Q₃] [Q₄ Q₅ Q₆ Q₇] ... [Q₂₈..Q₃₁]
  K-heads: [K₀] [K₁] ... [K₇]
  V-heads: [V₀] [V₁] ... [V₇]

  KV-cache size: 2 × 8 × S × d_h × 2 bytes ← 4× smaller!
  Quality drop: minimal (GQA ≈ MHA on most benchmarks)

───────────────────────────────── ─────────────────────────────────
MQA: Multi-Query Attention (PaLM, Falcon)

  num_heads_Q = 32 ← same number of Q-heads
  num_heads_KV = 1 ← ALL Q-heads share ONE K,V pair

  KV-cache size: 2 × 1 × S × d_h × 2 bytes ← 32× less!
  Quality drop: noticeable - GQA is usually a better compromise

in production

  • Llama-3 8B: GQA (32Q / 8KV) → 2024 standard
  • Mistral 7B: GQA (32Q / 8KV)
  • MQA → niche, GQA displaces
09MemoryPagedAttention (vLLM): virtual memory for KV

The problem with traditional KV-cache: it is allocated as a contiguous block under max_seq_len. With a response of 512 tokens, a prompt of 100 tokens - 80% of the memory is idle. PagedAttention stores KV in continuous pages of ~16 tokens, allocating on demand.

mechanics pagestext
# Traditional KV-cache:
kv[req_1] = allocate(max_seq_len=2048) ← reserve everything at once
kv[req_2] = allocate(max_seq_len=2048) ← 2048 more, although 100 are needed
← 90% of GPU memory is fragmented with empty blocks

# PagedAttention:
page_size = 16 # tokens per page

req_1 ← [page_4, page_7, page_12] ← not physically adjacent
req_2 ← [page_1, page_9]
req_3 ← [page_2, page_5, page_11] ← allocated as they grow

# block_table stores the mapping of logical → physical pages
# GPU core vLLM can perform attention on non-adjacent blocks

Result:
  memory waste: 30–40% (naive) → < 4% (PagedAttention)
  throughput: +2–4× with the same GPU

bonus: sharing pages

  • common system prompt → same page for all requests
  • copy-on-write with branching (beam search)
  • vLLM, TGI, SGLang use PagedAttention
10MemorySliding Window Attention: limit cache growth

An alternative approach: do not store KV for all past tokens - only for the last W. Each token sees only a window from the W previous ones. Mistral 7B uses SWA with W=4096. The cache stops growing after W tokens.

tradeofftext
Full KV-cache:
  attention(q_i, K[0..i], V[0..i]) ← all tokens
  maximum quality
  memory: O(seq_len) → grows without limit

Sliding Window (W=4096):
  attention(q_i, K[i-W..i], V[i-W..i]) ← window only
  quality: good for local dependencies
  memory: O(W) → constant after W tokens!

SWA problem:
  token outside window = "forgotten"
  distant dependencies (beginning → end of text) are lost

Hybrid: some layers full attention,
        part of the SWA (Mistral Mixtral) layers

applies

  • Mistral 7B: W=4096 + full for multiple layers
  • bad for tasks with long dependencies
  • Longformer: SWA + global tokens (hybrid)
04

Prompt Caching: KV-cache at the API level

3 cards
11Prompt CachePrompt Caching: reuse prefill between requests

A regular KV-cache lives inside a single request. Prompt Caching is a KV-cache between requests at the API level. If many requests have the same prefix (system prompt + RAG documents), the API provider stores them in KV-cache and does not recalculate them. Savings: up to 90% prefill cost and 85% TTFT latency.

Diagramsql
WITHOUT PROMPT CACHING: every time again

Request 1: [system_prompt=2000 tok] + [query_1=20 tok]
  → prefill 2020 tokens completely $$$

Request 2: [system_prompt=2000 tok] + [query_2=15 tok]
  → prefill 2015 tokens completely $$$
2000 tok is being recalculated!

───────────────────────────────── ─────────────────────────────────
WITH PROMPT CACHING: KV-cache on the server

Request 1: [system_prompt=2000 tok] + [query_1=20 tok]
  → prefill 2020 tokens cache_write
  → KV-cache for system_prompt is saved on the server

Request 2: [system_prompt=2000 tok] + [query_2=15 tok]
  → cache_hit: 2000 tokens from cache 10% cost
  → prefill only 15 new tokens → TTFT is 10x faster

───────────────────────────────── ─────────────────────────────────
KEY RULE OF USE

  Staticstart of prompt (cached)
  Dynamicend of prompt (not cached)

  ✓ Correct:
    [system_prompt] ← static → cached
    [rag_documents] ← static for TTL period → cached
    ────────────────
    [user_query] ← changes every time

  ✗ Incorrect:
    [user_query] ← first
    [system_prompt] ← afteris not cached!

when caching is especially effective

  • long system prompt (>1024 tokens)
  • RAG with fixed documents
  • few-shot examples in the prompt
  • unique documents in each request - the cache will not help
12Prompt CacheAnthropic: cache_control API

Anthropic requires explicit marking via cache_control. You decide which blocks to cache. Minimum 1024 tokens per cached block. TTL - 5 minutes (extends with each cache hit).

request structuresql
messages = client.messages.create(
  model = "cloud-sonnet-4-6",
  system = [
    {
      "type": "text",
      "text": system_prompt, ← static prompt
      "cache_control": {"type": "ephemeral"}
    }
  ],
  messages = [
    {"role": "user", "content": [
      {"type": "text", "text": rag_context,
       "cache_control": {"type": "ephemeral"}},
      {"type": "text", "text": user_query} ← not cacheable
    ]}
  ]
)

# Check the result:
usage = messages.usage
usage.cache_read_input_tokens # how many from cache
usage.cache_creation_input_tokens # how many are written to the cache
usage.input_tokens # regular (not cached)

Pricing (Claude 3.5 Sonnet)

  • cache_write: 125% of input price
  • cache_read: 10% of input price (90% discount)
  • pays off for >2 requests with one prefix
13Prompt CacheOpenAI: automatic prefix caching

OpenAI (gpt-4o, gpt-4o-mini from Oct 2024) enables caching automatically - no markup is needed. The cache is activated at a prefix of 1024 tokens and a granularity of 128 tokens. TTL - 5-10 minutes.

differences from Anthropictext
OpenAI - automatically:
  ✓ No cache_control markup needed
  ✓ The API itself determines matching prefixes
  ✓ 50% discount on cached tokens (input)
  ? There is no explicit control - you don’t know what is cached

# Check in response:
response.usage.prompt_tokens_details
  → {"cached_tokens": 1920, "audio_tokens": 0}

Anthropic - explicit markup:
  ✓ Full control over which block to cache
  ✓ 90% discount (vs 50% for OpenAI) → more aggressive
  ! We need to rebuild the prompt structure
  ! Minimum 1024 tokens per block

Advice: both providers - install static first,
dynamic (query) to the end. It works for both.

practice

  • OpenAI - easier to get started, no code changes
  • Anthropic - more savings with correct markings
  • both: block order is critical - static first
05

Sampling: from logits to final token

4 cards
14SamplingFull pipeline sampling: logits → temperature → top-k → top-p → sample

After forward pass, the model returns a vector of logits: one number for each token in the dictionary (128K for GPT-4). From these raw scores, you need to select one token. The chain of operations is deterministic, except for the final sample.

Distribution workbenchTemperature reshapes. Top-p trims.

Change both controls and inspect which tokens remain eligible.

This view stops before the random draw. It isolates the two decisions that shape the candidate distribution.
Diagramtex
STEP 1: RAW LOGITS (after LM Head)

  logits = W_lm_head hidden_state[-1]
  shape: [128 256] ← vocab_size Llama-3

  Top 5 example:
  "cat" : 8.41
  "dog" : 6.23
  "mouse" : 5.87
  "bird" : 4.12
  ...128251 other tokens: -12 to +3

───────────────────────────────── ─────────────────────────────────
STEP 2: TEMPERATURE (scaling)

  logits = logits / T

  T=0.0: logits/→∞ → argmax (greedy, deterministic)
  T=0.7: “cat”: 12.01, “dog”: 8.90 ← the gap has increased
  T=1.0: "cat": 8.41, "dog": 6.23 ← raw logits
  T=1.5: “cat”: 5.61, “dog”: 4.15 ← the gap has shrunk, chaos

───────────────────────────────── ─────────────────────────────────
STEP 3: TOP-K (tail trimming by number)

  k=50: keep only 50 tokens with the highest logit
  the rest 128206 → logit = −∞

───────────────────────────────── ─────────────────────────────────
STEP 4: TOP-P / NUCLEUS SAMPLING (trimming by total probability)

  After softmax we accumulate probabilities:
  "cat": 0.62 → cumsum = 0.62
  "dog": 0.18 → cumsum = 0.80
  "mouse": 0.09 → cumsum = 0.89
  "bird": 0.04 → cumsum = 0.93 ← if p=0.9, cut off here
  ...the rest → −∞

  p=0.9: keep tokens until Σprob ≤ 0.9

───────────────────────────────── ─────────────────────────────────
STEP 5: SOFTMAX + SAMPLE

  probes = softmax(filtered_logits)
  next_token = random.choice(tokens, p=probs)
  ← the only non-deterministic step!

typical settings

  • code/factual: T=0, greedy (reproducible)
  • chat: T=0.7, top_p=0.9
  • creative: T=1.0–1.2, top_p=0.95
  • T>1.5 → incoherent text
15SamplingTemperature: physical intuition

The name "temperature" comes from thermodynamics. At high temperatures, molecules move randomly (high entropy). When low, it is orderly (low entropy). Similarly, temperature controls the entropy of the tokens' probability distribution.

distributional effecttext
# Initial logits: [8.41, 6.23, 5.87, 4.12]

T=0.3 (cold, “confident”):
  scaled: [28.0, 20.8, 19.6, 13.7]
  softmax: [0.993, 0.004, 0.002, 0.001]
  ← almost always "cat"

T=1.0 (neutral, “raw distribution”):
  scaled: [8.41, 6.23, 5.87, 4.12]
  softmax: [0.617, 0.083, 0.058, 0.011]
  ← sometimes other options

T=2.0 (hot, “chaotic”):
  scaled: [4.21, 3.12, 2.93, 2.06]
  softmax: [0.342, 0.189, 0.158, 0.071]
  ← often unexpected tokens

# T→0: argmax (deterministic)
# T→∞: uniform (everything is equally likely)
16SamplingGreedy vs Beam Search vs Sampling

Three fundamentally different strategies for choosing the next token. Greedy is fast, Beam is high-quality for NMT, Sampling is diverse for generative tasks.

StrategyLogicPlusMinus
Greedyargmax at each stepfast, deterministiclocal optima
Beam (B=4)store B best waysglobally better greedy4x more expensive, repeats
Samplingrandom from distributionvariety, creativeunstable
Top-p samplingnucleus + chancebalance of quality and diff.no

when what

  • translation/summarization: Beam B=4
  • chat/creative: top-p sampling
  • structured output: greedy (T=0)
17SamplingRepetition Penalty & Logit Biases

LLM without additional mer tend to repeat tokens - especially at high temperatures. Repetition penalty reduces the probability of tokens that have already been encountered in the context. Logit bias allows you to force specific tokens to be strengthened or banned.

mechanicspython
Repetition penalty (θ = 1.3):
  if token_id in context:
    logit[token_id] = logit[token_id] / 1.3if > 0
    logit[token_id] = logit[token_id] * 1.3if < 0
  # 1.0 = no penalty, 1.3 = moderate, 2.0 = aggressive

Logit bias:
  # force deny token
  logit_bias = {token_id_of_swear: -100} ← = -

  # force the token to be strengthened
  logit_bias = {token_id_of_yes: +10} ← ≈ 20000× more likely

# OpenAI API: logit_bias parameter
# Usage: guardrail for banned words
# or force structured output
06

Generation Optimizations: speculative, quantization

3 cards
18OptimizationSpeculative Decoding: a small model generates, a large one verifies

The main bottleneck of the decode phase is memory-bound: the GPU is idle waiting for data. Speculative decoding uses this time: a small “draft” model generates K tokens, a large model verifies them in ONE forward pass. Accepted tokens are free. Acceleration 2–3×.

Diagramsql
MECHANICS

Draft model (haiku/Llama-3-1B):
  Generates K=5 tokens quickly:
  ["neural", "network", "it", "mathematical", "function"]
  Cost: 5 × 1B-forward-pass ← cheap

Target model (cloud-sonnet / Llama-3-70B):
  One forward pass by context + 5 draft tokens:
  Receives 5 probability distributions simultaneously
  Cost: 1 × 70B-forward-pass ← but validates 5 tokens!

VERIFICATION (rejection sampling):

  Draft: ["neural", "network", "it", "mathematical", "function"]
  Target: P(neural)=0.71 P(network|...)=0.83 P(this|...)=0.04

  Token 1 "neural": draft P=0.7, target P=0.71 → ACCEPT
  Token 2 "network": draft P=0.8, target P=0.83 → ACCEPT
  Token 3 "this": draft P=0.6, target P=0.04 → REJECT
  → regenerate from position 3, discard the rest

RESULT

  Traditional decode: 5 steps × 1 forward = 5 × 70B passes
  Speculative decode: 5 draft + 1 verify = 5 × 1B + 1 × 70B
23× faster with high acceptance rate

  Acceptance rate ≈ 7080% with draft~target in the family
  Self-speculation: the model itself generates a draft based on early layers

when to use

  • latency is critical, draft and target are one family
  • self-hosting: vLLM supports speculative decoding
  • low temperature → higher acceptance rate
  • when T>1.2: draft is often rejected → no profit
19OptimizationQuantization: fewer bits - less memory

Model parameters are stored in fp16 (2 bytes) or fp32 (4 bytes). Quantization reduces to int8 (1 byte) or int4 (0.5 byte). Llama-3 70B in fp16 = 140GB, int4 = 35GB - fits on two A100 40GB.

formats and tradeofftext
# Size of Llama-3 70B in different formats:
fp32: 280 GB ← unrealistic
fp16: 140 GB ← training
bf16: 140 GB ← training (better fp16 for large numbers)
int8: 70 GB ← almost no quality loss (LLM.int8)
int4: 35 GB ← GPTQ/AWQ: slight loss of quality
int3: 27 GB ← noticeable loss
int2: 18 GB ← significant loss

# KV-cache can also be quantized:
fp16 → int8: KV-cache 2x smaller
  ← saves GPU memory for larger batches
  ← slight drift on long sequences

practice

  • int4 (GPTQ/AWQ): deploy 70B on 2×A40
  • int8 (bitsandbytes): safe for production
  • int4 KV-cache: carefully eval before deployment
20OptimizationFlash Attention: O(n) memory instead of O(n²)

The standard implementation of attention materializes a matrix S = QKᵀ of size n×n in HBM - with n=8192 this is 512MB for only one matrix. Flash Attention calculates attention by tile, never materializing the full matrix.

key ideatex
# Standard attention (slow, lots of memory):
S = Q @ K.T / sqrt(d_k) # [n × n] in HBM - expensive!
A = softmax(S) # [n × n] in HBM
O=A@V#[n×d] in HBM
Memory: O(n²)

# Flash Attention (Dao et al. 2022):
# Tile algorithm: Q, K, V blocks are loaded into SRAM
# (fast GPU memory), softmax is calculated incrementally
#S never fully materializes
Memory: O(n) ← linear! only SRAM tiles

Result:
  Speed: 2–4x faster on A100
  Memory: 5–20× less
  Numerically identical to standard attention

applies

  • all modern libraries: vLLM, HuggingFace
  • critical for long-context (>8K tokens)
  • FlashAttention-3 (2024): optimized for H100
07

Token Embeddings vs Text Embeddings: Don't be confused!

2 cards
21EmbeddingsToken Embeddings (within LLM) vs Text Embeddings (for RAG) - different things

One of the most common conceptual failures: mixing token embeddings (a table inside GPT, transforms ID→vector) and text embeddings (output of an encoder model of type text-embedding-3, representing all text as one vector for search). These are fundamentally different objects with different properties.

Diagramtext
TOKEN EMBEDDINGS (within LLM)

  What: matrix E ∈ ℝ^[vocab_size × d_model]
          lookup table: token_id → vector

  Input: integer token-ID (15496)
  Output: row vector of matrix E, dim = d_model (4096)
  Meaning: initial representation of a token in model space
          changes (enriches) on each transformer block

  Example:
  "mother" → id=77341 → E[77341] → [0.12, -0.34, 0.89, ...] ∈ ℝ^4096

  Usage: only inside the model, first layer
  NOT SUITABLE for search: not trained for retrieval task,
  context-sensitive (contextualized through attention)

───────────────────────────────── ─────────────────────────────────
TEXT EMBEDDINGS (for RAG/search)

  What: output of a specialized encoder model
          entire text → one vector representation

  Input: text of arbitrary length (up to max_seq_len)
  Output: one vector, dim = 768–3072 (depending on model)
  Meaning: semantic representation of the ENTIRE text
          specially trained: close in meaning → close vectors

  Example:
  "mom washed the frame" → encoder → [0.41, 0.23, -0.18, ...] ∈ ℝ^768
  cosine("mom washed the frame", "mom washed the frame") → 0.97 ← close!

  Usage: indexing + search in RAG
  Models: text-embedding-3-large, bge-m3, e5-large

───────────────────────────────── ─────────────────────────────────
KEY DIFFERENCE

  Token embedding: "bank" → always the same vector ← static
  Text embedding: "bank on the river" → one vector ← context included
                    "bank account" → another vector
  LLM hidden state: "bank" → different vector in different contexts ← contextualized

selection rule

  • RAG/search → text-embedding-3 / bge-m3
  • fine-tuning models → token embeddings (inside)
  • never use token embeddings from LLM for retrieval
22EmbeddingsNiche case: hidden states LLM as embedding

Sometimes LLM hidden states are used as text embeddings - through prompt engineering. It works (LLM-Embedder, E5-mistral), but is niche: expensive (high inference), slow, justified only when maximum semantics is needed.

when it makes sensetext
# E5-mistral: prompt engineering for retrieval
prompt = f"Instruct: Given a query, find relevant documents.\n
Query: {query}"
embedding = llm_model.last_hidden_state(prompt)[-1]
← take the last hidden state (pool)

# When justified:
✓ the task requires deep understanding (code, science)
✓ has a GPU, latency < 1s is acceptable
✓ bi-encoder embedding is not enough

# Why standard text-embedding is better in 99% of cases:
✗ LLM inference: 100–500ms vs 5–20ms for bi-encoder
✗ cost 10–50× higher
✗ bge-m3 on MTEB is often no worse than LLM-embedded
08

End-to-End: “mom washed the frame” via GPT

2 cards
23End-to-EndEach step: from “continue the text: mom washed the frame” to the first response token

End-to-end parsing of one inference call. Let's take Llama-3 8B - specific numbers, specific forms of tensors. Task: continue the sentence.

Diagramtex
INPUT: "Continue the text: mom washed the frame"
────────────────────────────── ───────────────────────────────

① TOKENIZATION (tokenizer: tiktoken / sentencepiece)
  "Continue" → [3643, 70878] ← 2 tokens (Cyrillic is expensive)
  "text" → [50078]
  "mom" → [86392]
  "soap" → [42341, 261] ← the word broke
  "frame" → [100342]

  input_ids = [3643, 70878, 50078, 86392, 42341, 261, 100342]
  seq_len = 7

────────────────────────────── ───────────────────────────────
② TOKEN EMBEDDINGS (lookup in matrix E ∈ ℝ^[128256 × 4096])
  E[3643] → x₀ ∈ ℝ^4096 ← vector for "Continue"
  E[70878] → x₁ ∈ ℝ^4096
  ...
  E[100342]→ x₆ ∈ ℝ^4096
  shape: [7 × 4096]

────────────────────────────── ───────────────────────────────
③ POSITIONAL EMBEDDINGS (RoPE)
  # RoPE is not applied as addition, but through rotation Q,K
  # when calculating attention inside each block
  # position is encoded into sine/cosine frequencies
  position_ids = [0, 1, 2, 3, 4, 5, 6]

────────────────────────────── ───────────────────────────────
④ 32 TRANSFORMER BLOCKS (repeat 32 times)

  ─ Block 0: ──────────────────────────────────────
  RMSNorm(x) → normalization: x / rms(x) γ

  GQA (32Q/8KV heads):
    Q = x W_Q shape: [7 × 32 × 128]
    K = x · W_K shape: [7 × 8 × 128] ← GQA: only 8!
    V = x W_V shape: [7 × 8 × 128]

    # K and V are placed in kv_cache[block_0]:
    kv_cache[0].K = K ← 7 tokens × 8 heads × 128 = 7168 float16
    kv_cache[0].V = V

    # Causal attention (lower triangle only):
    scores = Q Kᵀ / √128 shape: [7 × 32 × 7]
    scores += causal_mask ← +0 or −∞
    α = softmax(scores)
    attn_out = α V shape: [7 × 32 × 128]
    attn_out = reshape + W_O → [7 × 4096]

  Residual: x = x + attn_out

  RMSNorm(x)

  SwiGLU FFN: 4096 → 14336 → 4096
    gate = x W_gate [7 × 14336]
    up = x W_up [7 × 14336]
    act = SiLU(gate) * up
    down = act W_down [7 × 4096]

  Residual: x = x + down
  ─ ... repeated 31 times ───────────────────────

  Block output: x₃₂ ∈ ℝ^[7 × 4096]

────────────────────────────── ───────────────────────────────
⑤ LM HEAD (projection onto dictionary)
  x₃₂[-1] ← take only the last position “frame”
  logits = x₃₂[-1] · W_lm_headᵀ shape: [128256]

  Top 5 logits:
  "erased": 9.21 ← likely successor!
  "rinsed": 7.84
  "washed": 6.91
  "cleaned": 5.43
  "rubs": 4.12

────────────────────────────── ───────────────────────────────
⑥ SAMPLING (T=0.7, top_p=0.9)
  logits / 0.7 → [13.16, 11.20, 9.87, 7.76, 5.89, ...]
  softmax → [0.71, 0.12, 0.05, 0.02, 0.01, ...]
  top_p=0.9 → leave Σprob ≤ 0.9 for now
  sample() → "erased" (id: 91234)

────────────────────────────── ───────────────────────────────
⑦ NEXT STEP (DECODE)
  input_ids = [...7 tokens..., 91234] ← add a new one
  kv_cache contains K,V for the first 7 positions
  Compute only one new token: q₇, k₇, v₇
  kv_cache.append(k₇, v₇)
  ← decode step: O(1) mathmul, O(seq) cache read
24End-to-EndPerformance Numbers: Llama-3 8B on A100 80GB

Specific numbers help calibrate expectations and explain in interviews why this or that optimization is important. Latency and throughput depend on batch size, length and accuracy.

indicative metricstext
# Llama-3 8B, fp16, A100 80GB SXM, vLLM

Prefill (compute-bound):
  1024 prompt tokens: TTFT ≈ 30 ms
  4096 prompt tokens: TTFT ≈ 120 ms
  16K prompt token: TTFT ≈ 900 ms ← O(n²) hits

Decode (memory-bound):
  batch=1: TPS ≈ 80 tok/s
  batch=8: TPS ≈ 220 tok/s ← batching helps
  batch=32: TPS ≈ 400 tok/s ← saturates HBM bandwidth

Memory:
  Model weights: ~16 GB (fp16)
  KV-cache @ 4K × batch=8: ~4.3 GB
  Activations: ~1–2 GB
  Total A100 80GB: fits with a margin

With optimizations:
  int4 (AWQ): 4 GB model, batch×2
  Speculative: latency 2–3× lower
  Prompt cache: TTFT 5–10× lower on hit

GPU memory budget

  • A100 80GB: 70B fp16 + KV-cache (batch=4)
  • A100 40GB: 8B fp16 + KV-cache (batch=16)
  • 4×A100: 70B + large batch or long context

No dead end

Keep moving through the map.

Continue in sequence, switch to a related guide, or return to the seven-track learning map.