Sparse Attention Patterns › Implementing Sparse Attention

Michael BrenndoerferUpdated June 25, 202510 min read
Language AI HandbookAll chapters
Part 17 of 67Efficient Attention
Chapter 118Sparse Attention Patterns › Implementing Sparse Attention
Reading section 8 of 13 · Implementing Sparse Attention · 10 minSection 8 of 13 · 10 min

Book contents

Section 8 of 13 · 10 min read

Sparse Attention Patterns › Implementing Sparse AttentionOne section · Sections in this chapter8 / 13 ↓
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

Having established the theory behind sparse patterns, let's implement a complete sparse attention module. This implementation prioritizes clarity over performance. This shows how the mask-based approach works in practice. Understanding this foundation will help you work with optimized libraries like xformers or Flash Attention later.

The mask-based implementation we build here is conceptually the simplest possible approach. You compute the full attention score matrix, apply the mask to zero out disallowed pairs, and run softmax as usual. This approach does not save computation in terms of floating-point operations: you still compute all n2n^2 scores before masking. What it does save is the cost of reasoning about which positions to compute and how to reconstruct a dense output from sparse inputs. For small sequences on a laptop, this simplification is fine. For large sequences in production, you need a kernel that avoids computing the masked entries altogether, which is what xformers and Flash Attention provide.

The value of implementing the mask-based version first is that it gives you a correct reference implementation you can use to validate more complex sparse kernels. Any optimized implementation should produce exactly the same output as the mask-based version for the same mask. If they differ, something is wrong with the optimization. This test-driven approach to building sparse attention is how most research teams develop and validate new patterns.

The Core Algorithm

Sparse attention follows the same computational structure as standard attention, with one addition: we apply a mask before the softmax to block certain attention connections. The algorithm proceeds in four steps:

  1. Compute raw attention scores: Multiply queries by keys to get similarity scores
  2. Apply the sparse mask: Set blocked positions to −∞-\infty (we use −109-10^9 numerically)
  3. Softmax normalization: Convert scores to probability weights (blocked positions become zero)
  4. Weighted sum: Combine values according to the attention weights

Let's implement each step:

In[27]:
Code
def sparse_attention(query, key, value, mask, scale=None):
    """
    Compute sparse attention given a boolean mask.

    Args:
        query: (seq_len, d_k) query vectors
        key: (seq_len, d_k) key vectors
        value: (seq_len, d_v) value vectors
        mask: (seq_len, seq_len) boolean mask (True = attend, False = block)
        scale: Optional scaling factor (default: 1/sqrt(d_k))

    Returns:
        output: (seq_len, d_v) attention output
        weights: (seq_len, seq_len) attention weights (masked)
    """
    seq_len, d_k = query.shape

    # Step 0: Determine scaling factor
    # The 1/sqrt(d_k) scaling prevents attention scores from growing
    # too large as dimension increases, which would push softmax into
    # saturation regions with near-zero gradients
    if scale is None:
        scale = 1.0 / np.sqrt(d_k)

    # Step 1: Compute raw attention scores
    # Each score[i,j] measures similarity between query[i] and key[j]
    scores = query @ key.T * scale  # (seq_len, seq_len)

    # Step 2: Apply the sparse mask
    # Where mask is True, keep the score unchanged
    # Where mask is False, replace with a very large negative number
    # This ensures exp(-1e9) ≈ 0 after softmax
    masked_scores = np.where(mask, scores, -1e9)

    # Step 3: Softmax normalization
    # We subtract the max for numerical stability (prevents overflow)
    # This doesn't change the result since softmax is shift-invariant
    exp_scores = np.exp(
        masked_scores - masked_scores.max(axis=1, keepdims=True)
    )
    weights = exp_scores / exp_scores.sum(axis=1, keepdims=True)

    # Explicitly zero blocked positions
    # They're already near-zero from softmax, but this ensures exactness
    weights = np.where(mask, weights, 0.0)

    # Step 4: Weighted sum of values
    # Each output[i] is a weighted combination of all value vectors,
    # with weights determined by attention
    output = weights @ value

    return output, weights

Comparing Full and Sparse Attention

To validate our implementation and demonstrate the effectiveness of sparse patterns, let's compare the outputs of full attention versus a combined local + strided pattern:

In[28]:
Code
# Test with random inputs
seq_len = 16
d_model = 32

query = np.random.randn(seq_len, d_model)
key = np.random.randn(seq_len, d_model)
value = np.random.randn(seq_len, d_model)

# Compare full vs sparse attention
full_mask = np.ones((seq_len, seq_len), dtype=bool)
sparse_mask = create_combined_sparse_mask(seq_len, local_window=2, stride=4)[0]

full_output, full_weights = sparse_attention(query, key, value, full_mask)
sparse_output, sparse_weights = sparse_attention(query, key, value, sparse_mask)
Out[29]:
Console
Sparse vs Full Attention Comparison:

Output shape: (16, 32)
Mean absolute difference: 0.1956
Max absolute difference: 1.8285
Positions with difference > 0.1: 283 / 512

Sparse mask: 154 / 256 pairs attended (60.2%)

This random, untrained example shows the mechanics of the approximation rather than establishing its quality. Ordinary query rows differ because the sparse mask removes keys, while the bidirectional hub rows match full attention exactly because those queries remain dense. Whether the remaining differences are acceptable must be measured with end-task evaluation after training.

The mean absolute difference gives you a useful quality signal, but the interpretation depends on context. A difference of 0.1 in the raw attention output might correspond to a small or large change in the model's predictions depending on what operations follow. Typically, the attention output is passed through a feed-forward layer and layer normalization, both of which can amplify or dampen small errors. In practice, researchers evaluate sparse attention not by measuring raw output differences but by comparing end-task metrics (perplexity, BLEU score, accuracy) between full and sparse variants trained from scratch. When trained end-to-end, the model adapts its weights to compensate for the constraints imposed by the sparse mask, often matching or approaching full attention performance.

The output difference heatmap also tells you something about which positions are most affected by sparsity. Positions at the boundary of the local window, where tokens just outside the window boundary might have been relevant, tend to show slightly larger differences than positions comfortably inside a dense cluster of similar tokens. This motivates the design choice of overlapping windows or combining local and global patterns: the positions most hurt by sparsity are the ones at the edges of the allowed region, and you can reduce this boundary effect by ensuring that boundary regions are covered by a second complementary pattern.

Out[30]:
Visualization
Heatmap of absolute full-versus-sparse output differences, with zero-valued horizontal rows at bidirectional hub positions and nonzero differences elsewhere.
Element-wise difference between full and sparse attention outputs for random untrained inputs. Dense hub-query rows have zero difference, while ordinary sparse-query rows show the approximation error introduced by masking keys.

The heatmap makes the structural effect of the mask visible. Rows 0, 4, 8, and 12 are hub queries, so their dense attention produces exactly the same outputs in both runs. The other rows show nonzero differences whose magnitude depends on which keys the sparse mask removed. This synthetic comparison is a useful implementation check, not evidence that the sparse model will preserve downstream quality.

Out[31]:
Visualization
Dense heatmap showing attention weights between all position pairs.
Full attention weights: All positions attend to all positions. Dense matrix shows no structural constraints.
Sparse heatmap with a diagonal band and horizontal and vertical hub stripes where attention is allowed.
Sparse attention weights: Only allowed positions have non-zero weights. The combined local+strided pattern is visible.

Complexity Analysis

Let's verify the computational savings from sparse attention empirically. The theoretical analysis we've done with Big-O notation tells us what the scaling should be, but empirical measurements validate whether our implementation achieves that scaling and quantify the constant factors that Big-O notation abstracts away.

In[32]:
Code
import time


def benchmark_attention(seq_lens, patterns, num_trials=5):
    """
    Benchmark different attention patterns across sequence lengths.
    """
    results = []
    d_model = 64

    for n in seq_lens:
        query = np.random.randn(n, d_model)
        key = np.random.randn(n, d_model)
        value = np.random.randn(n, d_model)

        for pattern_name, create_mask_fn in patterns.items():
            mask = create_mask_fn(n)

            # Warm up
            _ = sparse_attention(query, key, value, mask)

            # Time multiple trials
            times = []
            for _ in range(num_trials):
                start = time.perf_counter()
                _ = sparse_attention(query, key, value, mask)
                times.append(time.perf_counter() - start)

            avg_time = np.mean(times)
            pairs = mask.sum()

            results.append(
                {
                    "seq_len": n,
                    "pattern": pattern_name,
                    "time_ms": avg_time * 1000,
                    "pairs": pairs,
                    "sparsity": 1 - pairs / (n * n),
                }
            )

    return results


# Define patterns
patterns = {
    "full": lambda n: np.ones((n, n), dtype=bool),
    "local_16": lambda n: create_local_attention_mask(n, 8),
    "sparse": lambda n: create_combined_sparse_mask(n, 4, 8)[0],
}

# Benchmark
seq_lens = [64, 128, 256, 512]
benchmark_results = benchmark_attention(seq_lens, patterns)
Out[33]:
Console
Attention Pattern Benchmark:

 Seq Len Pattern       Time (ms)      Pairs   Sparsity
-------------------------------------------------------
      64 full              0.049      4,096       0.0%
      64 local_16          0.035      1,016      75.2%
      64 sparse            0.051      1,388      66.1%
     128 full              0.106     16,384       0.0%
     128 local_16          0.094      2,104      87.2%
     128 sparse            0.098      4,708      71.3%
     256 full              0.368     65,536       0.0%
     256 local_16          0.325      4,280      93.5%
     256 sparse            0.369     17,108      73.9%
     512 full              1.599    262,144       0.0%
     512 local_16          1.303      8,632      96.7%
     512 sparse            1.346     64,948      75.2%
Out[34]:
Visualization
Log-log line plot comparing the number of attention pairs computed by full, local, and sparse attention patterns as sequence length increases.
Attention pairs vs sequence length for different patterns. Full attention grows quadratically while the sparse patterns use fewer pairs, with the gap widening at longer sequences.

The benchmark confirms the theoretical complexity analysis: sparse patterns scale better than full attention, with the advantage growing as sequence length increases. Note that our NumPy implementation doesn't fully exploit sparsity since it still computes the full matrix before masking. Production implementations using optimized sparse kernels would show even larger speedups.

The gap between "pairs computed" and actual execution time matters. In our mask-based implementation, the number of pairs plotted corresponds to the non-masked entries, but the actual execution time includes computing the full score matrix and then applying the mask. This is why execution time does not scale as favorably as the pair count alone would suggest. A true sparse implementation that skips computing masked entries would show a much closer alignment between pair counts and execution times.

This distinction between theoretical sparsity and practical speedup is one of the central engineering challenges in sparse attention research. The theoretical analysis shows what should be possible. Achieving it requires specialized kernels, careful memory layouts, and hardware-specific optimizations. Libraries like xformers and Flash Attention have invested significant engineering effort into closing this gap, and they represent the practical tools you would use when deploying sparse attention in production systems.