Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 26

KV-Cache: Accelerating Autoregressive Inference

Recommended resources

Introduction

Welcome back! In our last lesson, we explored Rotary Positional Embeddings (RoPE), a sophisticated architectural method for encoding positional information by rotating query and key vectors. We saw how this preserves relative positioning, a crucial inductive bias for language.

Today, we shift from an architectural optimization to a critical inference optimization. While training an LLM is a monumental task, making it generate text quickly and efficiently for millions ofusers is an entirely different engineering challenge. This is where you, as an applied AI engineer, will often focus your efforts.

This lesson addresses the learning outcome: Explain the function of the KV-cache in accelerating autoregressive inference. We'll uncover a major source of inefficiency in the standard text generation process and see how a simple, elegant caching strategy slashes the computational cost, making real-time LLM applications possible.


The Inefficiency of Stateless Autoregressive Generation

As you know, autoregressive models like GPT generate text one token at a time. To predict the next token, the model needs to consider the entire sequence generated so far.

Let's break down the "naive" or stateless generation process:

  1. Input: The model receives the entire sequence of tokens generated so far (e.g., "The cat sat on").
  2. Process: It runs a full forward pass, computing the attention scores for every token in the sequence.
  3. Output: It uses the prediction for the very last token to choose the next one (e.g., "the").
  4. Append & Repeat: The new token is appended to the sequence ("The cat sat on the"), and the entire process repeats from step 1.

The inefficiency lies in step 2. To generate just one new token, the model re-computes the Key (K) and Value (V) vectors for all the previous tokens, even though it just calculated them in the previous step. This is a massive amount of redundant work.

The following video introduces the LLM inference workload and highlights this one-token-at-a-time process, setting the stage for understanding the inefficiency.

Mastering LLM Inference Optimization From Theory to Cost Effective Deployment: Mark Moyou

To start, let's watch a brief clip from 'Mastering LLM Inference Optimization' by Mark Moyou. This will frame the inference process from a practical, GPU-level perspective.

Watch from 02:46 to 05:39. Pay close attention to two key ideas: The one-token-at-a-time generation process. The concept that every generated token is stored on the GPU because the model needs to 'remember' what it said before. This stored memory is the foundation of the KV-cache.


Quantifying the Waste: The "Triangle of Waste"

The video you just watched mentions storing tokens on the GPU. The core of the problem is how that stored information is used. In a stateless model, the K and V vectors are recalculated from scratch every time. This computational load grows quadratically with the sequence length, leading to O(T²) complexity for generating T tokens.

Let's visualize this waste.

KV Cache in 15 min

The video 'KV Cache in 15 min' by Zachary Huang provides an exceptionally clear visual explanation of this redundant computation, which he calls the 'triangle of waste'.

Watch from 00:15 to 06:30. Focus on understanding the grid visualization that shows new computation versus wasteful re-computation. This will make the quadratic complexity problem crystal clear.

As you saw, for each new token, we only truly need to compute the K and V vectors for that single new token. All the previous K and V vectors remain unchanged. This insight is the key to the solution.


The Solution: Caching Keys and Values

The KV-cache is a simple but powerful optimization that eliminates this redundant work. The idea is to store, or "cache," the Key and Value tensors for each token after they are computed.

The generation process is now split into two phases:

  1. Prefill (or Prompt Processing): When the model first receives the prompt (e.g., "The cat sat"), it performs one initial forward pass. It computes the K and V vectors for all tokens in the prompt and stores them in the cache.
  2. Decode (or Generation): For every subsequent token generation step:
    • The model only takes the single, most recent token as input.
    • It computes the Query (Q), Key (K), and Value (V) vectors for only this one token.
    • It retrieves the previously computed K and V vectors from the cache.
    • It concatenates the new K and V vectors with the cached ones.
    • The new Q vector attends to this full history of K and V vectors to predict the next token.
    • The newly computed K and V are appended to the cache for the next step.
KV-Cache Computation Process with Caching
This diagram shows the two-phase KV-cache process. In the **Prefill** step, K and V are computed for the entire prompt and stored. In each **Decode** step, only the new token's Q, K, and V are computed, and the past K and V are restored from the cache, drastically reducing computation.

By doing this, the amount of new work at each step becomes constant (just processing one token), transforming the overall complexity from quadratic O(T²) to linear O(T).

Let's see how this efficient workflow looks in practice.

KV Cache in 15 min

The same video now explains the new, efficient workflow with the KV-cache enabled.

Watch from 06:30 to 11:14. Notice how the input sequence length (T) is always 1 in the decode phase. Contrast the 'grid of waste' with the new grid showing constant work at each step. This visually demonstrates the shift from O(T²) to O(T) complexity.


The Trade-Off: Computation vs. Memory

The KV-cache offers a dramatic speedup, but it comes at a cost: memory. The cache needs to store the Key and Value tensors for every token in the sequence, across every attention head and every transformer layer.

The memory footprint of the KV-cache can be calculated as:

Where:

  • 2: For storing both Keys and Values.
  • : Number of decoder layers.
  • : Number of attention heads.
  • : Sequence length (context window).
  • : Dimension of each attention head.
  • : Bytes per parameter (e.g., 2 for float16, 4 for float32).

As you can see, the memory usage grows linearly with the sequence length. This is a primary reason why models with very long context windows require GPUs with massive amounts of VRAM.

This article provides a great summary of the advantages and disadvantages.

Understanding and Coding the KV Cache in LLMs

To reinforce the trade-offs, let's read a concise summary from Sebastian Raschka's excellent article on the KV-cache.

Read the section titled 'KV cache Advantages and Disadvantages'. It clearly summarizes the trade-off between computational efficiency and memory usage.

Test your understanding!

Consider a Llama-3-8B-like model with the following configuration:

  • Number of layers (L): 32
  • Number of heads (N_h): 32
  • Head dimension (d_h): 128
  • Sequence length (T_s): 8192 tokens
  • Precision (P): bfloat16 (2 bytes per value)

Calculate the approximate size of the KV-cache in Gigabytes (GB). (1 GB = 1024³ bytes).

Show answer

Let's plug the numbers into the formula:

Cache Size = 2 * 32 * 32 * 8192 * 128 * 2 bytes
Cache Size = 134,217,728 * 32 bytes
Cache Size = 4,294,967,296 bytes

Now, convert to GB:
Cache Size (GB) = 4,294,967,296 / (1024³)
Cache Size (GB) = 4,294,967,296 / 1,073,741,824
Cache Size (GB) = 4 GB

So, for an 8K context window, the KV-cache alone consumes 4 GB of VRAM, on top of the model weights and other activations. This highlights the significant memory cost of long contexts.


From Concept to Code

While you don't need to implement it now, your background in engineering makes it valuable to see how the KV-cache translates to code. The core logic involves modifying the forward pass of the attention module to accept an optional cache argument.

  • If the cache exists, concatenate the newly computed K and V vectors to it.
  • If not (i.e., the prefill step), initialize the cache with the new K and V.
  • Return the model's output and the updated cache.

Most modern libraries like Hugging Face's transformers handle this automatically. When you call the .generate() method, the use_cache=True parameter is enabled by default, transparently providing this massive speedup.

The video below briefly shows the code diffs and data flow, which should be intuitive given your PyTorch experience.

KV Cache in 15 min

Let's revisit the 'KV Cache in 15 min' video one last time to see how the data flows through the PyTorch module.

Watch from 11:14 to 15:50. You don't need to memorize the code, but trace the tensor shapes. See how the input X has a sequence length of 1, while the past_kv cache has the previous sequence length. Observe how torch.cat is used to combine them to form the full context.


Conclusion

You have now mastered a fundamental optimization for LLM inference. The KV-cache is not a complex architectural change but a clever engineering solution that makes interactive, low-latency generation feasible.

Key Takeaways:

  • The Problem: Stateless autoregressive inference is inefficient, with O(T²) complexity due to redundant re-computation of Key and Value vectors for the entire sequence at every step.
  • The Function: The KV-cache stores previously computed Key and Value tensors, avoiding this re-computation.
  • The Process: Inference is split into a Prefill step (processing the prompt) and subsequent Decode steps (processing one token at a time).
  • The Result: The computational complexity of generation is reduced from quadratic to linear, O(T), enabling significant speedups.
  • The Trade-Off: This speed comes at the cost of increased memory usage, as the cache size grows linearly with the sequence length.

What's Next?

The memory cost of the KV-cache is a significant bottleneck. So, the natural next question is: can we reduce it? In our next lesson, we will explore Grouped-Query Attention (GQA) and Multi-Query Attention (MQA), two architectural modifications designed specifically to shrink the size of the Key and Value tensors, and thus the KV-cache, without a major loss in performance. This is a perfect example of how architectural design and inference optimization go hand-in-hand.