Sliding Window Attention › Mistral-Style Windowed Attention

Michael BrenndoerferUpdated June 26, 20256 min read
Language AI HandbookAll chapters
Part 17 of 67Efficient Attention
Chapter 119Sliding Window Attention › Mistral-Style Windowed Attention
Reading section 8 of 12 · Mistral-Style Windowed Attention · 6 minSection 8 of 12 · 6 min

Book contents

Section 8 of 12 · 6 min read

Sliding Window Attention › Mistral-Style Windowed AttentionOne section · Sections in this chapter8 / 12 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Mistral 7B popularized sliding window attention in modern large language models. Their approach uses a 4,096-token window size, which covers most practical context lengths while enabling efficient inference.

Key Design Choices

Mistral's implementation incorporates several practical considerations:

  • Large window size (4096): Covers entire documents in many use cases, reducing the need for global tokens
  • Rolling buffer KV cache: During generation, only the most recent ww key-value pairs are stored
  • Pre-fill and chunking: Long prompts are processed in chunks to manage memory

The rolling buffer approach is particularly elegant. Traditional transformers store all past key-value pairs, requiring memory that grows with sequence length:

Mfull=O(n⋅L⋅d)M_{\text{full}} = O(n \cdot L \cdot d)

where:

  • MfullM_{\text{full}}: memory required for the full KV cache
  • nn: current sequence length (grows during generation)
  • LL: number of transformer layers
  • dd: model dimension

With sliding window attention, we only store the most recent ww key-value pairs, giving constant memory usage:

Mwindow=O(w⋅L⋅d)M_{\text{window}} = O(w \cdot L \cdot d)

where ww is the window size. Since ww, LL, and dd are all constants for a given model, memory usage becomes independent of sequence length. This is the key insight that enables generating arbitrarily long sequences without running out of memory.

In[26]:
Code
class RollingKVCache:
    """
    Rolling key-value cache for efficient autoregressive generation.

    Maintains only the most recent 'window_size' key-value pairs,
    enabling constant memory usage regardless of sequence length.
    """

    def __init__(self, window_size, num_heads, d_k, d_v):
        self.window_size = window_size
        self.num_heads = num_heads

        # Pre-allocate buffers
        self.k_cache = np.zeros((window_size, num_heads, d_k))
        self.v_cache = np.zeros((window_size, num_heads, d_v))

        # Current position in the circular buffer
        self.position = 0
        self.length = 0

    def update(self, k, v):
        """
        Add new key-value pair to the cache.

        Args:
            k: New key of shape (num_heads, d_k)
            v: New value of shape (num_heads, d_v)
        """
        # Write to current position (wraps around)
        idx = self.position % self.window_size
        self.k_cache[idx] = k
        self.v_cache[idx] = v

        self.position += 1
        self.length = min(self.length + 1, self.window_size)

    def get_kv(self):
        """Get current keys and values in correct order."""
        if self.length < self.window_size:
            # Haven't filled the buffer yet
            return self.k_cache[: self.length], self.v_cache[: self.length]
        else:
            # Buffer is full, need to reorder
            start = self.position % self.window_size
            indices = np.concatenate(
                [np.arange(start, self.window_size), np.arange(0, start)]
            )
            return self.k_cache[indices], self.v_cache[indices]
In[27]:
Code
# Demonstrate rolling cache behavior
cache = RollingKVCache(window_size=4, num_heads=2, d_k=8, d_v=8)

# Simulate adding tokens
for i in range(6):
    k = np.full((2, 8), i)  # Simple keys to track order
    v = np.full((2, 8), i)
    cache.update(k, v)

    K, V = cache.get_kv()
    first_values = K[:, 0, 0]  # First element of each cached key
Out[28]:
Console
Rolling KV Cache Demo (window=4):
After 6 updates, cache contains: 4 entries
Cached positions (by value): [2. 3. 4. 5.]
Only most recent 4 tokens are stored

After adding 6 tokens to a cache with window size 4, tokens 0 and 1 have been evicted. The cache now contains only tokens 2, 3, 4, and 5. This shows how the circular buffer automatically discards the oldest entries when it reaches capacity. This behavior mirrors what happens during autoregressive generation in models like Mistral.

Memory Comparison

The rolling buffer approach provides substantial memory savings for long sequences:

In[29]:
Code
def kv_cache_memory_mb(
    seq_len, num_layers, num_heads, d_head, window_size=None, dtype_bytes=2
):
    """
    Calculate KV cache memory usage.

    Args:
        seq_len: Current sequence length
        num_layers: Number of transformer layers
        num_heads: Number of attention heads
        d_head: Dimension per head
        window_size: If provided, use rolling buffer; otherwise full cache
        dtype_bytes: 2 for fp16, 4 for fp32
    """
    # Full cache: 2 (K+V) * layers * heads * seq_len * d_head * dtype
    if window_size is None:
        elements = 2 * num_layers * num_heads * seq_len * d_head
    else:
        # Rolling buffer: only window_size positions
        effective_len = min(seq_len, window_size)
        elements = 2 * num_layers * num_heads * effective_len * d_head

    return elements * dtype_bytes / (1024**2)  # Convert to MB


# Mistral 7B config (approximate)
config = {"num_layers": 32, "num_heads": 32, "d_head": 128, "window_size": 4096}

seq_lengths = [1024, 4096, 16384, 65536]
Out[30]:
Console
KV Cache Memory (fp16, Mistral 7B config):

   Seq Len      Full Cache   Rolling (w=4096)    Savings
--------------------------------------------------------
     1,024          512.0 MB             512.0 MB       0.0%
     4,096         2048.0 MB            2048.0 MB       0.0%
    16,384         8192.0 MB            2048.0 MB      75.0%
    65,536        32768.0 MB            2048.0 MB      93.8%

The memory comparison reveals the scaling advantage of the rolling buffer. At 1,024 tokens (below the window size), both approaches use the same memory. At 4,096 tokens (exactly the window size), they remain equivalent. Beyond that point, the savings become dramatic: at 16K tokens, the rolling buffer uses 75% less memory, and at 64K tokens, savings reach over 93%.

Out[31]:
Visualization
Line plot comparing full cache linear memory growth versus rolling buffer constant memory usage.
KV cache memory comparison between full cache and rolling buffer (window=4096) for Mistral 7B configuration. The rolling buffer provides constant memory usage regardless of sequence length, enabling processing of arbitrarily long sequences.

This enables processing long documents on hardware that could not otherwise accommodate them.