27. Sliding-Window Attention
Problem
Mistral 7B's headline architectural trick is that no query ever attends to the full history: each query position i is restricted to a local window of the W most recent key positions (including itself), rather than every position from 0 to i. Stacking L layers of window W still gives a theoretical receptive field of roughly L * W tokens, but each layer's attention matmul only ever needs to touch an O(seq_len * W) band instead of the full O(seq_len2) matrix.
This problem asks for the windowed attention output itself. A full O(seq_len2) scores matrix with the band masked out is an acceptable, interview-reasonable solution here — a truly O(seq_len * W) rolling-buffer version is the natural follow-up once this is working. Given q, k, v already split into heads, compute attention where position i may only attend to positions j with j <= i (causal) and i - j < window_size (local).
Source: src/27_sliding_window_attention.py
def sliding_window_attention(
q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, window_size: int,
) -> torch.Tensor: ...
>>> q = k = v = torch.randn(1, 2, 6, 4)
>>> sliding_window_attention(q, k, v, window_size=3).shape
torch.Size([1, 2, 6, 4])
Step-by-Step Approach
- Compute the ordinary scaled dot-product scores matrix
q @ k.transpose(-1, -2) / sqrt(head_dim), shape(batch, heads, seq, seq)— the windowing happens entirely in the mask, not the matmul itself. - Build a
(seq, seq)boolean band mask withtorch.arange:i = arange(seq).view(seq, 1),j = arange(seq).view(1, seq), mask =(j <= i) & ((i - j) < window_size). - Apply the mask with
scores.masked_fill(~mask, float("-inf"))so masked-out positions get exactly zero probability after softmax, without special-casing them. - Note the mask always keeps at least the diagonal
j == ifor every row, so no row is ever all-inf— softmax is safe from NaNs by construction. torch.softmax(scores, dim=-1)thenweights @ vto get the windowed output, exactly as in ordinary attention.- Sanity-check with
window_size >= seq_len: the band mask degenerates to plain causal masking, so the output must match ordinary causal self-attention exactly.
The key insight is that sliding-window attention is not a different attention mechanism, it's the same softmax(QKT)V with a stricter mask: standard causal attention only removes future keys, this removes future keys and keys further than window_size in the past.
Reference solution
import math
import torch
def sliding_window_attention(
q: torch.Tensor, k: torch.Tensor, v: torch.Tensor, window_size: int,
) -> torch.Tensor:
batch, heads, seq, hd = q.shape
# full scores matrix, then mask out everything outside the causal + local
# window band -- masked_fill with -inf before softmax is what keeps the
# excluded positions at exactly 0 probability without special-casing them
scores = q @ k.transpose(-1, -2) / math.sqrt(hd) # (batch, heads, seq, seq)
idx = torch.arange(seq, device=q.device)
i, j = idx.view(seq, 1), idx.view(1, seq)
# every row i always keeps at least the diagonal (j == i), so no row of
# the mask is ever all-False -- softmax never sees an all -inf row (no NaNs)
band_mask = (j <= i) & ((i - j) < window_size) # (seq, seq)
scores = scores.masked_fill(~band_mask, float("-inf"))
weights = torch.softmax(scores, dim=-1)
return weights @ v # (batch, heads, seq, head_dim)
Key Functions & Tricks
torch.arange(seq).view(seq, 1)/.view(1, seq)— broadcasts to an(seq, seq)pairwise index-difference grid without any Python loop.tensor.masked_fill(~mask, float("-inf"))— sets disallowed logits to-infsosoftmaxdrives their probability to exactly 0.torch.softmax(scores, dim=-1)— normalizes each query's kept-key scores into a probability distribution over just its window.- Boolean
&between two broadcastable masks — composes the causal condition and the window condition into a single band mask in one expression. q @ k.transpose(-1, -2)— batched matmul that broadcasts over both thebatchandheadsleading dimensions automatically.
How to Recognize This Pattern
Recognize this pattern whenever a problem restricts attention to a fixed-size local neighborhood instead of the full causal history — the phrase "window size" or "local attention" next to an otherwise-ordinary attention signature is the signal. The core work is entirely in the mask construction, not in a new attention formula. A common variation asks for an explicit O(seq_len * window_size) version using a rolling buffer of the last W keys/values instead of a full masked (seq, seq) matrix — useful when seq_len is large enough that materializing the full scores matrix is wasteful. The most common pitfall is getting the window boundary off by one (using i - j <= window_size instead of <, which silently includes one extra key per query) or forgetting the causal half of the mask entirely and only checking the window distance.