Attention Masking: Controlling Information Flow

Michael BrenndoerferUpdated May 27, 202552 min read

Part of Language AI Handbook

Covers attention masking techniques including padding masks, causal masks, and sparse patterns.

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

Attention Masking

Self-attention computes pairwise interactions between all positions in a sequence, letting every token gather information from every other token. This generality is one of attention's greatest strengths, but it also raises an immediate practical problem: sometimes we need to block certain interactions entirely. A model generating text one token at a time must not be allowed to peek at the tokens that come after the current position. Sequences padded to a uniform length so they can be batched together should not let real tokens attend to meaningless padding. Controlling which positions are "visible" to which others is not a minor detail but a foundational design decision that shapes what a transformer can and cannot do.

Think of attention masking as a privacy screen on a monitor. Without the screen, anyone walking past can read what is displayed. With the screen, only people sitting in the correct position see the content, while everyone else sees darkness. In the same way, an attention mask designates which query-key pairs are "in view" (allowed to interact) and which are "blocked" (prevented from interacting). The screen does not change the underlying pixels; it simply controls visibility. The mask does not change the attention mechanism; it simply controls which scores contribute to the final weighted sum.

Masking modifies the attention computation by adding large negative values to specific positions before the softmax. Recall that attention weights are computed as:

αij=exp⁡(sij)∑k=1nexp⁡(sik)\alpha_{ij} = \frac{\exp(s_{ij})}{\sum_{k=1}^{n} \exp(s_{ik})}

where:

  • αij\alpha_{ij}: the attention weight from query position ii to key position jj, representing how much position ii "attends to" position jj
  • sijs_{ij}: the raw attention score between positions ii and jj, computed as the dot product of their query and key vectors (scaled by 1/dk1/\sqrt{d_k})
  • nn: the sequence length

When we add a large negative value (like −109-10^9) to a score sijs_{ij}, the exponential exp⁡(sij−109)\exp(s_{ij} - 10^9) becomes vanishingly small, driving αij\alpha_{ij} to near-zero. These masked positions effectively disappear from the weighted sum. This simple mechanism enables causal language modeling, efficient batch processing, and custom attention patterns.

The elegance of this approach is that it requires no changes to the attention mechanism itself. The formula remains identical. The mask is simply an extra additive term inserted at one specific point in the computation, yet that term determines which positions survive softmax. Masked positions contribute nothing to the output, so from the model's perspective, they simply do not exist during that forward pass.

Understanding attention masking deeply matters for several reasons. First, it is the difference between a model that cheats during training (by reading future tokens) and one that learns to predict. Second, it is what makes batched training of variable-length sequences possible without corrupting representations. Third, it is the foundation of efficient attention variants that scale transformers to sequences far longer than standard full attention can handle. Every serious transformer practitioner needs to understand that masks exist, how they work, when to apply them, and what happens when they are misconfigured.

Historical Context

Attention masking was introduced as part of the original Transformer architecture in "Attention Is All You Need" (Vaswani et al., 2017). The paper described the causal mask as applying "an additional mask in the scaled dot-product attention, which prevents positions from attending to subsequent positions." At the time, this was described almost as a minor implementation detail, but it proved to be one of the most consequential design choices in modern deep learning. The causal mask is what allowed GPT-style autoregressive models to train on entire sequences in parallel rather than token by token, reducing training time by orders of magnitude and making large-scale language modeling practical. The padding mask, while simpler conceptually, was equally critical for enabling batched training on heterogeneous real-world text corpora. Without these two masking mechanisms working in concert, the transformer's rise to dominance in NLP would have been far slower.

Why Masking Matters

Before building the machinery of masking, it is worth pausing to understand why the problem exists in the first place. The need for masking arises from a fundamental tension in how transformers are trained versus how they are used at inference time.

Consider training a language model to predict the next word. Given the sentence "The cat sat on the mat," the model should learn to predict "cat" given "The," "sat" given "The cat," "on" given "The cat sat," and so on. This sequential prediction task is called autoregressive modeling because each prediction is conditioned on all prior predictions. The natural way to train such a model would be to feed it one token at a time, compute a prediction, then move on to the next position. But this sequential approach is tragically slow: you can only process one token at a time, so training on millions of sentences would take an impractical amount of time.

The solution that transformers use is to process the entire sequence in parallel during training, computing predictions for all positions simultaneously. This is possible because at training time we already have the complete target sequence. We can process "The cat sat on the mat" all at once, compute predictions at every position, and compare each prediction to the correct next token. But this creates the cheating problem: standard self-attention allows position 1 ("cat") to see position 3 ("on"), which means the model can trivially predict "sat" by simply reading ahead to see what comes next. The learned "predictions" would be meaningless because they are not really predictions at all.

Attention Mask

An attention mask is a matrix of values that modifies attention scores before softmax. Positions marked for masking receive large negative values, causing the softmax to assign them near-zero weights. This effectively prevents the query from attending to those positions.

Masking solves this by blocking attention to future positions during training. The model learns to predict each token using only past context, exactly matching the autoregressive setup it will encounter during generation at inference time. The causal mask is thus a training-time simulation of the inference-time constraint: at inference, future tokens do not exist yet, and the mask enforces the same condition during training even though the tokens are already there.

Padding presents a different but equally important challenge. When batching sequences of different lengths, we pad shorter sequences to match the longest. But these padding tokens carry no meaning whatsoever. They are placeholders inserted purely for computational convenience. Allowing real tokens to attend to padding would corrupt their representations by incorporating noise into the weighted sum. A model that learns to interpret padding as meaningful signal would behave erratically on sequences of different lengths. Masking removes padding from the attention computation entirely. This ensures real tokens see only other real tokens.

The key insight is that both masking problems, causal masking and padding masking, have the same solution: add a large negative value to the scores of positions that should be invisible. The mechanism is identical; only the pattern of masked positions differs. This unification is not accidental. It reflects the deeper structure of the attention computation, where everything flows through a single softmax normalization that is highly sensitive to the relative magnitudes of its inputs.

Padding Masks

Real-world text comes in varying lengths. "Hello" has one token while "The quick brown fox jumps over the lazy dog" has nine. To process multiple sequences efficiently in a batch, we pad shorter sequences to a common length using a special padding token. Think of padding as blank lines at the bottom of a form: they are there so every form has the same number of lines, but they carry no information and should be ignored when reading the content.

The practical consequence of padding is that every batch contains some positions that should not influence any computation. If we naively apply self-attention without masking, those padding positions will accumulate information from real tokens, and real tokens will waste attention capacity attending to padding positions. Neither interaction is meaningful. A token like "[PAD]" has no semantic content, so attending to it pollutes the output representation with noise.

The severity of this problem scales with the degree of padding in the batch. In a batch where most sequences are short but one is very long, most positions across most sequences are padding. In that scenario, a large fraction of every attention matrix would be wasted on padding interactions. The learned representations would be noisy, training would be less efficient, and the model might learn spurious patterns based on the position of padding tokens rather than the content of real tokens.

In[3]:
Code
# Example batch with varying sequence lengths
sequences = [
    ["The", "cat", "sat"],  # Length 3
    ["Hello", "world"],  # Length 2
    ["A", "quick", "brown", "fox"],  # Length 4
]

# Pad to maximum length
max_len = max(len(seq) for seq in sequences)
pad_token = "[PAD]"

padded_sequences = []
for seq in sequences:
    padded = seq + [pad_token] * (max_len - len(seq))
    padded_sequences.append(padded)
Out[4]:
Console
Padded sequences (max length = 4):
  Sequence 0: ['The', 'cat', 'sat', '[PAD]']
  Sequence 1: ['Hello', 'world', '[PAD]', '[PAD]']
  Sequence 2: ['A', 'quick', 'brown', 'fox']

Padding ensures uniform tensor shapes, but it introduces tokens that should be invisible to the attention mechanism. A padding mask marks which positions contain real tokens versus padding. The mask is a binary indicator: each position either belongs to the real content (marked as "keep") or to the padding area (marked as "block").

In[5]:
Code
import numpy as np


def create_padding_mask(sequences, pad_token="[PAD]"):
    """
    Create a padding mask for a batch of sequences.

    Returns a mask where True indicates positions to KEEP (real tokens)
    and False indicates positions to MASK (padding tokens).
    """
    batch_size = len(sequences)
    seq_len = len(sequences[0])

    # True where we have real tokens, False for padding
    mask = np.array(
        [[token != pad_token for token in seq] for seq in sequences]
    )

    return mask


padding_mask = create_padding_mask(padded_sequences)
Out[6]:
Console
Padding mask (True = real token, False = padding):
  Sequence 0: [np.True_, np.True_, np.True_, np.False_]
             ['The', 'cat', 'sat', '[PAD]']
  Sequence 1: [np.True_, np.True_, np.False_, np.False_]
             ['Hello', 'world', '[PAD]', '[PAD]']
  Sequence 2: [np.True_, np.True_, np.True_, np.True_]
             ['A', 'quick', 'brown', 'fox']

The padding mask is a boolean array where True marks real tokens and False marks padding. To apply this mask to attention scores, we need to convert it into an additive mask. This conversion is the bridge between the "which positions are real" question and the "how much should each position contribute" answer.

Converting to Attention Mask

We have a boolean padding mask, but attention needs numerical scores. How do we bridge this gap? The answer lies in understanding how softmax behaves with extreme values.

Softmax converts a vector of arbitrary real numbers into a probability distribution. Every element becomes positive, and all elements sum to one. The function achieves this through exponentiation followed by normalization. The key insight is that softmax is sensitive to relative differences between values, not their absolute magnitudes. When computing softmax, each score is exponentiated, then divided by the sum of all exponentials:

softmax(z)i=ezi∑jezj\text{softmax}(z)_i = \frac{e^{z_i}}{\sum_j e^{z_j}}

where:

  • ziz_i: the ii-th element of the input vector, which is the attention score for one query-key pair
  • ezie^{z_i}: the exponential of that score, which converts any real number to a positive value
  • ∑jezj\sum_j e^{z_j}: the sum of exponentials across all positions, which is the normalizing constant ensuring the outputs sum to one

If one score is extremely negative while others are moderate, its exponential becomes vanishingly small. To illustrate, consider applying softmax to a vector with one large negative value:

softmax([2.0,1.5,−10000])≈[0.62,0.38,0.00]\text{softmax}([2.0, 1.5, -10000]) \approx [0.62, 0.38, 0.00]

The third element receives essentially zero weight. This happens because e−10000e^{-10000} is astronomically small compared to e2.0e^{2.0} and e1.5e^{1.5}. In practice, e−10000e^{-10000} is so close to zero that floating-point arithmetic rounds it to exactly zero, so the masked position truly disappears from the computation.

Why does this formula make sense? Notice that the exponential function is monotone: larger inputs produce larger outputs. By making one input dramatically smaller than all the others, we guarantee its contribution to the normalization sum is negligible, and its share of the normalized output is negligible too. We are essentially creating a value so small that, in the competition of exponentials, it loses completely.

This gives us our masking strategy: add a large negative value to positions we want to block. We typically use −109-10^9 rather than true infinity to avoid numerical edge cases, though both work in practice. True negative infinity can cause NaN values in edge cases where entire rows are masked (since 0/00/0 becomes NaN), whereas −109-10^9 keeps the arithmetic well-defined.

In[7]:
Code
# Demonstrate how mask value affects attention weight

base_scores = np.array([2.0, 1.5, 1.0])  # Three positions with similar scores
mask_values = np.linspace(0, -20, 100)

# For each mask value, compute softmax and track the masked position's weight
masked_weights = []
other_weights_0 = []
other_weights_1 = []

for mv in mask_values:
    scores_with_mask = base_scores.copy()
    scores_with_mask[2] = base_scores[2] + mv  # Apply mask to position 2
    exp_scores = np.exp(scores_with_mask - scores_with_mask.max())
    weights = exp_scores / exp_scores.sum()
    masked_weights.append(weights[2])
    other_weights_0.append(weights[0])
    other_weights_1.append(weights[1])
Out[8]:
Visualization
Line plot showing attention weight vs mask value, with masked position weight dropping to zero as mask becomes negative.
Effect of mask value on attention weight distribution. As the mask value becomes more negative, attention to the masked position drops rapidly toward zero while unmasked positions absorb the redistributed attention. By a mask value of -15, the masked position is effectively invisible.

The plot demonstrates why we use large negative values like −109-10^9. Even a mask value of −10-10 reduces the masked position's weight to near zero, while −15-15 makes it essentially invisible. The unmasked positions automatically absorb the redistributed attention: when one position drops out of the softmax competition, the remaining positions share that "freed" probability mass. The model does not need to relearn anything; the redistribution happens automatically through the normalization in softmax.

In[9]:
Code
def padding_mask_to_attention_mask(padding_mask):
    """
    Convert padding mask to additive attention mask.

    For self-attention, we need a (batch, seq_len, seq_len) mask
    that masks out attention TO padding positions.

    Args:
        padding_mask: shape (batch, seq_len), True for real tokens

    Returns:
        attention_mask: shape (batch, seq_len, seq_len)
        0.0 for allowed attention, -inf for masked positions
    """
    batch_size, seq_len = padding_mask.shape

    # Expand mask to (batch, 1, seq_len) for broadcasting
    # We want to mask attention TO padding positions (keys/values)
    mask_expanded = padding_mask[:, np.newaxis, :]

    # Broadcast to (batch, seq_len, seq_len)
    attention_mask = np.where(
        mask_expanded,
        0.0,  # Real token: no modification
        -1e9,  # Padding: large negative value
    )

    return attention_mask


attention_mask = padding_mask_to_attention_mask(padding_mask)
Out[10]:
Console
Attention mask shape: (3, 1, 4)

Sequence 0 attention mask (length 3, 1 pad token):
[[ 0.e+00  0.e+00  0.e+00 -1.e+09]]

Sequence 1 attention mask (length 2, 2 pad tokens):
[[ 0.e+00  0.e+00 -1.e+09 -1.e+09]]

The attention mask has shape (batch, seq_len, seq_len). For sequence 0, the last column contains large negative values because position 3 is padding. Every query position will receive near-zero attention weight for position 3 after softmax. Notice that we mask along the key dimension, not the query dimension. The reason is that we want to prevent any query from attending to a padding key, but we do not need to prevent a padding position from being a query: in practice the output at padding positions is simply discarded, so it does not matter what the padding query "attends to."

Visualizing Padding Mask Effects

Let's see how padding masks affect attention weights in practice. We'll create random attention scores and compare the distributions with and without masking.

In[11]:
Code
def softmax(x, axis=-1):
    """Numerically stable softmax."""
    exp_x = np.exp(x - x.max(axis=axis, keepdims=True))
    return exp_x / exp_x.sum(axis=axis, keepdims=True)


# Simulate attention scores for sequence 1 (2 real tokens, 2 padding)
seq_len = 4
scores = np.random.randn(seq_len, seq_len)

# Attention without masking
weights_unmasked = softmax(scores)

# Attention with padding mask for sequence 1
mask = attention_mask[1]  # Sequence 1 has 2 padding tokens
weights_masked = softmax(scores + mask)
Out[12]:
Visualization
Heatmap showing attention weights distributed across all 4 positions.
Attention weights without masking. All four positions receive non-trivial attention, including the two padding tokens in columns 2 and 3, which dilute the signal from real content.
Heatmap showing attention weights concentrated in first 2 columns with zeros in last 2 columns.
Attention weights with padding mask applied. Columns 2 and 3 (padding positions) receive exactly zero weight, and the freed probability mass is redistributed entirely to the real tokens in columns 0 and 1.

The contrast is stark. Without masking, attention flows to all positions including padding. The padding tokens absorb 10-30% of the attention weight at each query position. With the mask applied, padding positions receive exactly 0.00 weight, and the remaining attention redistributes entirely to real tokens. This ensures padding never contaminates token representations.

The redistribution effect is particularly important: the two real tokens now share 100% of the attention weight instead of roughly 50-70%. The model has more focused, meaningful signal to work with. Over millions of training examples, this difference accumulates into substantially cleaner representations.

Causal Masks

Causal masking, also called look-ahead masking, prevents positions from attending to future positions. This is essential for autoregressive language models that generate text one token at a time. The name "causal" comes from causality: in a causal system, the past influences the present, but the future does not. A causal attention mask enforces the same constraint.

Think of causal masking as the rule that applies when you are doing a fill-in-the-blanks exercise: you can read the words before the blank to figure out what goes there, but you cannot look ahead to the words after the blank. The causal mask enforces this rule systematically across every position in the sequence simultaneously.

During training, we process entire sequences in parallel for efficiency. But the model must learn to predict each position using only past context. Causal masking enforces this constraint: position ii can only attend to positions 0,1,…,i0, 1, \ldots, i, where ii is the current position index (0-indexed). This creates an asymmetric attention pattern where earlier positions see less context and later positions see more. Position 0 sees only itself. Position 1 sees itself and position 0. Position n−1n-1 sees the entire sequence. This graduated context is exactly what an autoregressive model needs.

Causal Attention

Causal attention restricts each position to attend only to itself and previous positions. This creates a left-to-right information flow where future tokens cannot influence past representations. Causal masking is what allows transformers to train on full sequences in parallel while simulating the sequential left-to-right prediction they perform at inference time.

The practical benefit of causal masking extends beyond preventing cheating. It also allows the transformer to serve as its own training data generator: by masking the sequence causally and computing cross-entropy loss between each position's prediction and the true next token, we get nn training signal pairs from a single forward pass on a sequence of length nn. This is called "teacher forcing" and it is one of the reasons large language models can train efficiently on massive datasets.

The Causal Mask Formula

How do we formalize "only attend to past positions"? We need a mask that blocks any attention where the key position comes after the query position. Mathematically, the causal mask MM is defined as:

Mij={0if j≤i−∞if j>iM_{ij} = \begin{cases} 0 & \text{if } j \leq i \\ -\infty & \text{if } j > i \end{cases}

where:

  • ii: the query position (row index), representing the token that is currently "attending"
  • jj: the key position (column index), representing the token being attended to
  • MijM_{ij}: the mask value added to the attention score at position (i,j)(i, j), which is either zero (no effect) or −∞-\infty (complete suppression)

The condition j≤ij \leq i means "key position is at or before query position." When this holds, we add 0 (no masking). When j>ij > i, the key is in the future relative to the query, so we add −∞-\infty to block attention completely.

This creates a lower triangular pattern: the diagonal and everything below it are 0, and everything above the diagonal is −∞-\infty. If you visualize the n×nn \times n mask matrix, the allowed region forms a triangle whose tip is at the top-left corner and whose base runs along the bottom row. Row ii has exactly i+1i+1 allowed positions (positions 0 through ii), growing from 1 at the top to nn at the bottom.

Why does this formula make sense? Notice that the lower triangular structure directly encodes the "past only" constraint. The diagonal (j=ij = i) represents attending to the current token itself, which is always allowed. Below the diagonal (j<ij < i) represents attending to past tokens, also always allowed. Above the diagonal (j>ij > i) represents attending to future tokens, which is always blocked. The matrix structure is a direct geometric representation of the "past is visible, future is hidden" rule.

In[13]:
Code
def create_causal_mask(seq_len):
    """
    Create a causal (look-ahead) mask.

    Returns a mask where:
    - 0.0 for positions that CAN be attended (current and past)
    - -inf for positions that CANNOT be attended (future)

    Shape: (seq_len, seq_len)
    """
    # Create lower triangular matrix of ones
    # mask[i, j] = 1 if j <= i (can attend), else 0
    causal = np.tril(np.ones((seq_len, seq_len)))

    # Convert to additive mask
    mask = np.where(causal, 0.0, -1e9)

    return mask


seq_len = 5
causal_mask = create_causal_mask(seq_len)
Out[14]:
Console
Causal mask (0 = can attend, -1e9 = cannot attend):
[[ 0.e+00 -1.e+09 -1.e+09 -1.e+09 -1.e+09]
 [ 0.e+00  0.e+00 -1.e+09 -1.e+09 -1.e+09]
 [ 0.e+00  0.e+00  0.e+00 -1.e+09 -1.e+09]
 [ 0.e+00  0.e+00  0.e+00  0.e+00 -1.e+09]
 [ 0.e+00  0.e+00  0.e+00  0.e+00  0.e+00]]

Position 0 can only attend to itself (only position 0 has value 0 in its row). Position 1 can attend to positions 0 and 1. Position 4 can attend to all positions. The upper triangle is filled with large negative values, blocking all look-ahead. The resulting pattern is exactly the lower triangular structure predicted by the formula.

Visualizing Causal Attention

Let's trace through how causal masking affects attention during sequence processing. We will use a concrete five-token sequence and visualize the resulting attention weight matrix.

In[15]:
Code
# Example sequence
tokens = ["The", "cat", "sat", "on", "mat"]
seq_len = len(tokens)

# Random attention scores (before softmax)
scores = np.random.randn(seq_len, seq_len) * 0.5

# Apply causal mask
causal_mask = create_causal_mask(seq_len)
masked_scores = scores + causal_mask

# Compute attention weights
causal_weights = softmax(masked_scores)
Out[16]:
Visualization
Lower triangular heatmap showing causal attention weights with zeros in upper triangle.
Causal attention pattern for a five-token sequence. The lower triangular structure shows allowed attention, while the upper triangle is completely zeroed out. Position 0 ('The') attends only to itself, while position 4 ('mat') distributes attention across all five tokens based on relevance.

The triangular structure is clear. "The" at position 0 attends entirely to itself (weight 1.00) because it has no past context. "cat" can attend to "The" or itself, distributing its attention between the two based on similarity scores. By position 4, "mat" distributes attention across all five positions based on learned relevance patterns. This graduated context mirrors the information available to an autoregressive model during generation: as more of the sequence has been generated, more context is available.

This structure ensures that when training on a sequence like "The cat sat on mat," each position learns to predict the next token using only information from positions to its left. The training procedure and the inference procedure are now perfectly aligned, which is why causal masking is sometimes described as making training and inference "consistent" with each other.

Why Causal Masking Enables Parallel Training

Without causal masking, training autoregressive models would be painfully slow. We would need to generate one token at a time, feeding each output back as input for the next step. On a sequence of length 512, this means 512 sequential forward passes. Causal masking eliminates this bottleneck entirely.

In[17]:
Code
# During training, we have the full sequence
training_sequence = ["<s>", "The", "cat", "sat", "</s>"]
target_sequence = ["The", "cat", "sat", "</s>", "<pad>"]

# The model predicts each position simultaneously
# Causal masking ensures each prediction only sees past context

# Position 0: sees only "<s>" -> predicts "The"
# Position 1: sees "<s> The" -> predicts "cat"
# Position 2: sees "<s> The cat" -> predicts "sat"
# Position 3: sees "<s> The cat sat" -> predicts "</s>"

# All predictions happen in one forward pass!

Each row of the attention matrix corresponds to one prediction task. Row 0 uses context [x0][x_0], row 1 uses context [x0,x1][x_0, x_1], and so on. The causal mask automatically provides the correct context for each prediction position without needing separate forward passes. A single matrix multiplication and one softmax computation delivers all nn predictions simultaneously. This parallelism is what makes training large language models on trillions of tokens computationally feasible.

The efficiency gain is not merely constant-factor. Without causal masking, training on sequences of length nn requires nn sequential steps, each involving a full forward pass. With causal masking, the same work reduces to a single forward pass. On modern hardware where parallelism is the primary source of speed, this is the difference between a technique that is practical and one that is not. GPT-2, GPT-3, and virtually every other large autoregressive language model owes its training efficiency to this one masking technique.

Combining Multiple Masks

Real models often need both padding and causal masks simultaneously. A decoder processing batched sequences must handle variable lengths (requiring padding masks) while maintaining autoregressive constraints (requiring causal masks). These two requirements are independent but must be enforced together in every forward pass.

The solution is mathematically elegant: combine masks by addition. If we have a causal mask McausalM_{\text{causal}} and a padding mask MpadM_{\text{pad}}, the combined mask is:

Mcombined=Mcausal+MpadM_{\text{combined}} = M_{\text{causal}} + M_{\text{pad}}

where:

  • McausalM_{\text{causal}}: the causal mask with 0 for past/current positions and −∞-\infty for future positions
  • MpadM_{\text{pad}}: the padding mask with 0 for real token positions and −∞-\infty for padding positions
  • McombinedM_{\text{combined}}: the result, which blocks a position if either mask blocks it

Since both masks use −∞-\infty for blocked positions and 0 for allowed positions, the sum gives −∞-\infty wherever either mask blocks attention. A position is only allowed (value 0) when both masks allow it: the position must be in the past (causal constraint) and must be a real token (padding constraint). This is the logical AND of the two constraints, implemented through simple arithmetic addition.

Why does addition work here? Because adding any finite number to −∞-\infty still gives −∞-\infty, and adding zero to any value leaves it unchanged. The mask values act like boolean logic: 0 is "true" (allow) and −∞-\infty is "false" (block), and addition implements the OR operation on the "block" signal.

In[18]:
Code
def combine_masks(*masks):
    """
    Combine multiple attention masks by addition.

    Each mask should use 0 for allowed positions and -inf for blocked.
    The result blocks any position blocked by ANY mask.
    """
    combined = np.zeros_like(masks[0])
    for mask in masks:
        combined = combined + mask
    return combined


# Example: Batch of 2 sequences with different lengths
# Sequence 0: "Hello world" (2 tokens) + 2 padding
# Sequence 1: "The cat sat" (3 tokens) + 1 padding
batch_sequences = [
    ["Hello", "world", "[PAD]", "[PAD]"],
    ["The", "cat", "sat", "[PAD]"],
]
seq_len = 4

# Create padding mask for this batch
padding_mask = create_padding_mask(batch_sequences)
padding_attn_mask = padding_mask_to_attention_mask(padding_mask)

# Create causal mask (same for all sequences)
causal = create_causal_mask(seq_len)
# Expand to batch dimension: (1, seq_len, seq_len) for broadcasting
causal_expanded = causal[np.newaxis, :, :]

# Combine masks
combined_mask = combine_masks(padding_attn_mask, causal_expanded)
Out[19]:
Console
Sequence 0: ['Hello', 'world', '[PAD]', '[PAD]']
Combined mask (0=attend, large negative=block):
[[          0 -1000000000 -2000000000 -2000000000]
 [          0           0 -2000000000 -2000000000]
 [          0           0 -1000000000 -2000000000]
 [          0           0 -1000000000 -1000000000]]

Sequence 1: ['The', 'cat', 'sat', '[PAD]']
Combined mask:
[[          0 -1000000000 -1000000000 -2000000000]
 [          0           0 -1000000000 -2000000000]
 [          0           0           0 -2000000000]
 [          0           0           0 -1000000000]]

For sequence 0 with 2 real tokens, the combined mask blocks:

  • Upper triangle (causal constraint): future positions are hidden
  • Columns 2 and 3 (padding positions): padding keys are hidden

The resulting mask allows each position to attend only to past, non-padding positions. This is exactly the correct behavior for a causal language model trained on batched variable-length sequences.

Out[20]:
Visualization
Mask visualization showing allowed attention only in 2x2 lower-left corner.
Combined mask for sequence 0 (2 real tokens followed by 2 padding). Only the lower-left 2x2 block shows allowed attention (marked 'Y'). Both the causal constraint (upper triangle) and padding constraint (columns 2-3) contribute to the blocked regions.
Mask visualization showing allowed attention only in 3x3 lower-left region.
Combined mask for sequence 1 (3 real tokens followed by 1 padding). The lower-left 3x3 triangular block allows attention. Column 3 is entirely blocked due to the padding token.

The green cells marked "Y" show where attention is allowed. For sequence 0, only the 2x2 lower-left block is active. For sequence 1, the lower-left triangular 3x3 region allows attention. Both patterns combine causal and padding constraints in a single mask. The visual pattern makes clear how the two masks compose: the causal mask "cuts" along the diagonal, and the padding mask "cuts" along the rightmost columns.

Worked Example: Tracing a Masked Forward Pass

To make the mechanics concrete, let's trace through a complete masked attention computation step by step with small numerical values you can verify by hand.

We will use a toy sequence of three tokens: "I", "love", "AI". We want to apply causal masking so that during training, each position predicts the next token using only past context.

Step 1: Set up the scores. Suppose we have the following raw attention scores (these would normally come from the dot product of queries and keys, but we will use fixed values for clarity):

S=[1.00.50.20.81.20.30.60.91.5]S = \begin{bmatrix} 1.0 & 0.5 & 0.2 \\ 0.8 & 1.2 & 0.3 \\ 0.6 & 0.9 & 1.5 \end{bmatrix}

where SijS_{ij} is the score from query position ii to key position jj.

Step 2: Construct the causal mask. For a 3-token sequence, the causal mask is:

M=[0−∞−∞00−∞000]M = \begin{bmatrix} 0 & -\infty & -\infty \\ 0 & 0 & -\infty \\ 0 & 0 & 0 \end{bmatrix}

In practice we use −109-10^9 instead of −∞-\infty.

Step 3: Add the mask to the scores. The masked scores are S+MS + M:

S+M=[1.0−∞−∞0.81.2−∞0.60.91.5]S + M = \begin{bmatrix} 1.0 & -\infty & -\infty \\ 0.8 & 1.2 & -\infty \\ 0.6 & 0.9 & 1.5 \end{bmatrix}

The upper triangle has been driven to −∞-\infty, making those positions invisible.

Step 4: Apply softmax row by row. Recall that softmax divides each exponential by the sum of exponentials in the row. For row 0 (position "I"):

softmax([1.0,−∞,−∞])=[e1.0e1.0+e−∞+e−∞,e−∞…,e−∞…]≈[1.00,0.00,0.00]\begin{aligned} \text{softmax}([1.0, -\infty, -\infty]) &= \left[\frac{e^{1.0}}{e^{1.0} + e^{-\infty} + e^{-\infty}}, \frac{e^{-\infty}}{\ldots}, \frac{e^{-\infty}}{\ldots}\right] \\ &\approx [1.00, 0.00, 0.00] \end{aligned}

Position "I" attends only to itself: weight 1.0. For row 1 (position "love"):

softmax([0.8,1.2,−∞])=[e0.8e0.8+e1.2,e1.2e0.8+e1.2,0]=[2.2252.225+3.320,3.3202.225+3.320,0]≈[0.40,0.60,0.00]\begin{aligned} \text{softmax}([0.8, 1.2, -\infty]) &= \left[\frac{e^{0.8}}{e^{0.8} + e^{1.2}}, \frac{e^{1.2}}{e^{0.8} + e^{1.2}}, 0\right] \\ &= \left[\frac{2.225}{2.225 + 3.320}, \frac{3.320}{2.225 + 3.320}, 0\right] \\ &\approx [0.40, 0.60, 0.00] \end{aligned}

Position "love" splits its attention between "I" (40%) and itself (60%). For row 2 (position "AI"):

softmax([0.6,0.9,1.5])≈[0.17,0.23,0.60]\text{softmax}([0.6, 0.9, 1.5]) \approx [0.17, 0.23, 0.60]

Position "AI" can attend to all three positions and distributes attention based on score magnitude, attending most strongly to itself (1.5 is the highest score).

Step 5: Interpret the result. The final attention weight matrix is:

A=[1.000.000.000.400.600.000.170.230.60]A = \begin{bmatrix} 1.00 & 0.00 & 0.00 \\ 0.40 & 0.60 & 0.00 \\ 0.17 & 0.23 & 0.60 \end{bmatrix}

Each row is a valid probability distribution summing to 1.0. The upper triangle is exactly zero. The model is now constrained to make predictions using only the causal context it should have. When predicting the word that follows "love," the model sees only "I" and "love"; when predicting the word after "AI," it sees all three. This is precisely the progressive left-to-right context structure that makes autoregressive language modeling work.

Mask Shapes and Broadcasting

Attention masks can have different shapes depending on the use case. Understanding these shapes and how they broadcast determines whether the implementation works as intended.

The attention score matrix has shape (batch, num_heads, seq_len, seq_len) in a full transformer with multi-head attention. Masks can be provided in more compact forms and rely on NumPy or PyTorch's broadcasting rules to expand to the full shape automatically. The four common mask shapes are:

  • Full shape (batch, num_heads, seq_len, seq_len): Fully specified mask for every batch item and head, giving maximum control but consuming the most memory
  • Per-batch (batch, 1, seq_len, seq_len): Same mask across all heads for each batch item; the head dimension broadcasts to num_heads
  • Per-batch compact (batch, 1, 1, seq_len): Key-only masking where the query dimension also broadcasts; this is the natural shape for padding masks, since padding depends only on which keys are padding, not on which query is asking
  • Global (1, 1, seq_len, seq_len): Same mask for the entire batch; this is the natural shape for causal masks, since the causal constraint is the same for every sequence

Broadcasting allows us to store the mask in its most natural compact form and let the framework handle expansion. This is both memory-efficient and computationally clean: you define the mask at the level of abstraction that matches its semantics, and broadcasting handles the rest.

In[21]:
Code
# Different mask shapes and their purposes
batch_size = 2
num_heads = 4
seq_len = 8

# Causal mask: same for all batches and heads
# Shape: (1, 1, seq_len, seq_len)
causal_mask = create_causal_mask(seq_len)[np.newaxis, np.newaxis, :, :]

# Padding mask: varies per batch, same across heads
# Original shape: (batch, seq_len)
# Expanded shape: (batch, 1, 1, seq_len)
padding_indicator = np.array(
    [
        [True, True, True, True, True, False, False, False],  # 5 real tokens
        [True, True, True, False, False, False, False, False],  # 3 real tokens
    ]
)
padding_mask_expanded = padding_indicator[:, np.newaxis, np.newaxis, :]
attention_padding = np.where(padding_mask_expanded, 0.0, -1e9)

# Simulated attention scores
scores = np.random.randn(batch_size, num_heads, seq_len, seq_len)

# Broadcasting handles dimension matching automatically
masked_scores = scores + causal_mask + attention_padding
Out[22]:
Console
Causal mask shape: (1, 1, 8, 8)
Padding mask shape: (2, 1, 1, 8)
Scores shape: (2, 4, 8, 8)
Masked scores shape: (2, 4, 8, 8)

Broadcasting expands smaller dimensions automatically:
  Causal (1,1,8,8) + Padding (2,1,1,8) + Scores (2,4,8,8) = (2,4,8,8)

Broadcasting rules make mask combination elegant. The causal mask (1, 1, 8, 8) applies identically to all batch items and heads. The padding mask (2, 1, 1, 8) applies per batch item across all heads, masking keys based on which positions are padding. The final result (2, 4, 8, 8) has the correct combined mask for each batch item and attention head. Neither mask needs to be explicitly replicated to match the full score tensor shape; broadcasting handles that automatically and without allocating additional memory.

Memory-Efficient Masking

For very long sequences, storing full (seq_len, seq_len) masks becomes expensive. A 10,000-token sequence requires 100 million entries per mask. For a batch of 32 sequences with 4 attention heads, the full mask tensor would require 32 * 4 * 10,000 * 10,000 * 4 bytes = 51.2 GB, which is completely infeasible. Several strategies reduce this cost.

Lazy mask generation: Instead of precomputing the full mask, generate it during the attention computation.

In[23]:
Code
def apply_causal_mask_lazy(scores):
    """
    Apply causal mask without pre-allocating a full mask tensor.
    Uses numpy's triu to directly set upper triangle to -inf.
    """
    seq_len = scores.shape[-1]
    # Create mask in-place using views
    masked = scores.copy()
    # Get upper triangle indices
    row_indices, col_indices = np.triu_indices(seq_len, k=1)
    # Set upper triangle to -inf
    masked[..., row_indices, col_indices] = -1e9
    return masked


# Compare memory: full mask vs lazy
seq_len = 1000
full_mask_memory = seq_len * seq_len * 4  # 4 bytes per float32
Out[24]:
Console
Full mask memory for seq_len=1000: 4,000,000 bytes (4.0 MB)
Lazy approach: generates mask on-the-fly, no pre-allocation needed

A 1000-token sequence requires 4 MB just for the mask tensor. The lazy approach avoids this allocation by setting mask values directly on the score matrix during computation. For very long sequences, this savings becomes critical.

Boolean masks: Store masks as boolean arrays (1 byte per element) rather than float32 (4 bytes), converting to float only when needed.

In[25]:
Code
# Boolean mask storage
seq_len = 1000
bool_mask = np.tril(np.ones((seq_len, seq_len), dtype=bool))
float_mask = np.tril(np.ones((seq_len, seq_len), dtype=np.float32))

bool_memory = bool_mask.nbytes
float_memory = float_mask.nbytes
Out[26]:
Console
Boolean mask: 1,000,000 bytes (1.0 MB)
Float32 mask: 4,000,000 bytes (4.0 MB)
Memory savings: 75%

For sequences of length 1000, boolean masks use 75% less memory. This adds up when processing large batches or very long documents. Modern deep learning frameworks like PyTorch support boolean masks natively and perform the float conversion internally only when needed, so you get the memory savings without manual conversion code.

Efficient Masking Implementation

With our understanding of padding and causal masks in place, we can now build a complete masked attention function. The key insight is that masking fits directly into the standard attention formula through a single addition operation before the softmax. No additional branches, no special-casing, no changes to the backpropagation graph: just one extra tensor addition that changes everything about which positions contribute.

The Masked Attention Formula

Recall that standard scaled dot-product attention computes:

Attention(Q,K,V)=softmax(QKTdk)V\text{Attention}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}}\right)V

where:

  • QQ: the query matrix, shape (n,dk)(n, d_k), where each row is a query vector for one position
  • KK: the key matrix, shape (n,dk)(n, d_k), where each row is a key vector for one position
  • VV: the value matrix, shape (n,dv)(n, d_v), where each row is a value vector for one position
  • dkd_k: the dimension of the query and key vectors, used in the scaling factor 1/dk1/\sqrt{d_k}

To add masking, we simply insert the mask matrix MM before the softmax:

Attention(Q,K,V,M)=softmax(QKTdk+M)V\text{Attention}(Q, K, V, M) = \text{softmax}\left(\frac{QK^T}{\sqrt{d_k}} + M\right)V

where:

  • QQ: the query matrix of shape (n,dk)(n, d_k), representing what each position is "looking for"
  • KK: the key matrix of shape (n,dk)(n, d_k), representing what each position "offers"
  • VV: the value matrix of shape (n,dv)(n, d_v), containing the information to aggregate
  • MM: the mask matrix of shape (n,n)(n, n), with 0 for allowed and −∞-\infty for blocked positions
  • dkd_k: the dimension of queries and keys, used for scaling to prevent vanishing gradients
  • nn: the sequence length

The formula unfolds in three steps:

  1. Compute raw scores: QKTQK^T produces an (n,n)(n, n) matrix where entry (i,j)(i, j) measures the similarity between query ii and key jj.

  2. Scale and mask: Divide by dk\sqrt{d_k} to stabilize gradients, then add the mask MM. Masked positions receive −∞-\infty, which the softmax will convert to near-zero weights.

  3. Normalize and aggregate: Softmax converts scores to weights summing to 1 (per row), then these weights select and combine values from VV.

This formulation is elegant because the mask MM does not change the computational structure. We still compute all pairwise scores, but the masking happens through simple addition before softmax. The exponential function in softmax then "erases" the masked positions. The gradient with respect to masked positions is also effectively zero, so backpropagation naturally ignores them during parameter updates.

Implementing Masked Attention

Let's translate the formula into code. The implementation follows the three-step structure exactly:

In[27]:
Code
def scaled_dot_product_attention(query, key, value, mask=None):
    """
    Compute scaled dot-product attention with optional masking.

    Args:
        query: shape (batch, seq_len, d_k) or (batch, heads, seq_len, d_k)
        key: same shape as query
        value: same shape as query
        mask: broadcastable mask, 0 for attend, -inf for block

    Returns:
        output: weighted values, same shape as value
        attention_weights: softmax weights
    """
    d_k = query.shape[-1]

    # Step 1: Compute raw similarity scores
    # (batch, ..., seq_len, d_k) @ (batch, ..., d_k, seq_len)
    # -> (batch, ..., seq_len, seq_len)
    scores = query @ key.swapaxes(-2, -1) / np.sqrt(d_k)

    # Step 2: Apply mask before softmax
    if mask is not None:
        scores = scores + mask

    # Step 3: Normalize to weights and aggregate values
    attention_weights = softmax(scores, axis=-1)
    output = attention_weights @ value

    return output, attention_weights

Notice how the mask application is just a single line: scores = scores + mask. This simplicity is intentional. The mask contains 0 for allowed positions (no effect on scores) and large negative values for blocked positions (which softmax converts to near-zero weights). No branching, no special cases, just addition. The mask is a first-class citizen of the attention formula, not a post-hoc patch.

Testing the Implementation

Let's verify that our implementation correctly handles both padding and causal masks:

In[28]:
Code
# Test setup
batch_size = 2
seq_len = 6
d_model = 8

# Random query, key, value
Q = np.random.randn(batch_size, seq_len, d_model)
K = np.random.randn(batch_size, seq_len, d_model)
V = np.random.randn(batch_size, seq_len, d_model)

# Padding: batch 0 has 6 tokens, batch 1 has 4 tokens
padding_indicator = np.array(
    [
        [True, True, True, True, True, True],  # All real
        [True, True, True, True, False, False],  # 4 real + 2 padding
    ]
)

# Create combined mask (causal + padding)
causal = create_causal_mask(seq_len)[np.newaxis, :, :]  # (1, seq_len, seq_len)
padding = np.where(
    padding_indicator[:, np.newaxis, :],  # (batch, 1, seq_len)
    0.0,
    -1e9,
)
combined_mask = causal + padding

# Run attention
output, weights = scaled_dot_product_attention(Q, K, V, mask=combined_mask)
Out[29]:
Console
Output shape: (2, 6, 8)
Attention weights shape: (2, 6, 6)

Batch 1 attention weights (4 real tokens + 2 padding):
Last 2 columns should be ~0 (padding), upper triangle should be ~0 (causal)
[[1.    0.    0.    0.    0.    0.   ]
 [0.286 0.714 0.    0.    0.    0.   ]
 [0.046 0.626 0.328 0.    0.    0.   ]
 [0.158 0.31  0.169 0.363 0.    0.   ]
 [0.552 0.061 0.124 0.262 0.    0.   ]
 [0.369 0.195 0.336 0.1   0.    0.   ]]

The attention weight matrix confirms that both masks work together correctly. Looking at the output for batch 1 (which has 4 real tokens and 2 padding tokens):

  • Columns 4 and 5 are nearly zero: The padding mask successfully blocks attention to padding positions. No real token wastes attention on meaningless padding.

  • Upper triangle is nearly zero: The causal mask blocks attention to future positions. Position 0 cannot see positions 1-5, position 1 cannot see positions 2-5, and so on.

  • Lower-left region has non-zero weights: The intersection of "past positions" and "real tokens" receives all the attention. These are exactly the positions each query should attend to.

The combined mask creates a triangular pattern truncated by the padding boundary. This is precisely what a causal language model needs when processing variable-length batched sequences. The two masks, one enforcing temporal ordering and one enforcing content validity, combine into a single constraint.

Custom Attention Patterns

Beyond standard padding and causal masks, researchers have explored various attention patterns for efficiency and modeling goals. Masking is a design tool as well as a correctness constraint: by choosing which positions can attend to which others, you shape what information the model can use and at what computational cost.

Think of the full attention matrix as a complete graph: every token is connected to every other token. Masking is the process of removing edges from this graph. The causal mask removes all "forward" edges. The padding mask removes all "noise" edges. Custom patterns remove different subsets of edges to achieve specific computational or modeling goals.

Local Attention

Restrict attention to a fixed window around each position. Instead of attending globally, each token only looks at its ww nearest neighbors on each side. This reduces complexity from O(n2)O(n^2) to O(n⋅w)O(n \cdot w), where:

  • nn: the sequence length (total number of tokens)
  • ww: the window size (number of positions each token can attend to on each side)

The trade-off is expressiveness for efficiency. Local attention cannot capture dependencies between positions that are more than ww steps apart. For tasks where most relevant context is nearby, this is not a significant limitation. For tasks requiring long-range reasoning, it can be a serious bottleneck.

In[30]:
Code
def create_local_attention_mask(seq_len, window_size):
    """
    Create a local attention mask with a fixed window.
    Each position attends to window_size positions before and after.

    Args:
        seq_len: sequence length
        window_size: number of positions to attend on each side

    Returns:
        mask: (seq_len, seq_len) attention mask
    """
    mask = np.full((seq_len, seq_len), -1e9)

    for i in range(seq_len):
        start = max(0, i - window_size)
        end = min(seq_len, i + window_size + 1)
        mask[i, start:end] = 0.0

    return mask


seq_len = 10
window_size = 2
local_mask = create_local_attention_mask(seq_len, window_size)
Out[31]:
Visualization
Diagonal band pattern showing local attention where each position attends to nearby positions only.
Local attention mask with window size 2. Each position attends only to the 2 positions before and 2 after itself, creating a band-diagonal pattern. Positions outside the window receive zero attention weight, concentrating each token's representation on its immediate neighbors.

Local attention is the core mechanism in models like Longformer and BigBird designed to handle very long sequences. The window captures local context efficiently, and special "global" tokens (like [CLS]) can still attend to all positions for tasks that require long-range information. The combination of local and global tokens gives these models both efficiency and expressiveness.

Strided Attention

Attend to every kk-th position, spreading attention across the sequence with fixed intervals. Here, kk is the stride parameter that controls how far apart attended positions are. Where local attention captures fine-grained local context, strided attention captures coarse global patterns by sampling the sequence at regular intervals.

In[32]:
Code
def create_strided_attention_mask(seq_len, stride):
    """
    Create a strided attention mask.
    Each position attends to every stride-th position.

    Args:
        seq_len: sequence length
        stride: attend to every stride-th position

    Returns:
        mask: (seq_len, seq_len) attention mask
    """
    mask = np.full((seq_len, seq_len), -1e9)

    for i in range(seq_len):
        # Attend to positions at stride intervals, aligned to position
        for j in range(0, seq_len, stride):
            mask[i, j] = 0.0
        # Also attend to local neighborhood for current position
        mask[i, i] = 0.0

    return mask


stride = 3
strided_mask = create_strided_attention_mask(seq_len, stride)

Strided attention is most useful when combined with local attention. By itself, strided attention misses the fine-grained context between strided positions. But paired with local attention, it provides the long-range "summary" connections that local windows cannot reach. This is the insight behind Sparse Transformer.

Combined Patterns

Real efficient attention mechanisms often combine multiple patterns. The Sparse Transformer uses local and strided attention together to cover both nearby and distant positions at a fraction of the cost of full attention.

In[33]:
Code
def create_sparse_attention_mask(seq_len, local_window, stride):
    """
    Create a sparse attention mask combining local and strided patterns.

    Args:
        seq_len: sequence length
        local_window: size of local attention window
        stride: stride for global attention

    Returns:
        mask: (seq_len, seq_len) attention mask
    """
    # Start with all blocked
    mask = np.full((seq_len, seq_len), -1e9)

    # Add local attention
    for i in range(seq_len):
        start = max(0, i - local_window)
        end = min(seq_len, i + local_window + 1)
        mask[i, start:end] = 0.0

    # Add strided attention
    for i in range(seq_len):
        for j in range(0, seq_len, stride):
            mask[i, j] = 0.0

    return mask


sparse_mask = create_sparse_attention_mask(seq_len, local_window=1, stride=4)
Out[34]:
Visualization
Fully filled attention matrix showing all positions can attend to all positions.
Full attention: every position attends to every position, giving maximum expressiveness at O(n^2) cost.
Sparse pattern with diagonal band and periodic vertical stripes.
Sparse attention combining local window and stride: the diagonal band captures local context while periodic columns provide global connectivity, used in Sparse Transformer.
Lower triangular band pattern combining causal and local constraints.
Causal plus local attention: the lower triangular structure enforces left-to-right ordering while the local window limits each position to nearby past context, useful for efficient autoregressive models.

Each pattern represents a different trade-off. Full attention has maximum expressiveness but O(n2)O(n^2) cost. Sparse patterns reduce complexity at the cost of some long-range interactions. The choice depends on sequence length, available compute, and task requirements.

In[35]:
Code
# Compare sparsity levels across different mask patterns
def compute_sparsity(mask):
    """Compute the fraction of allowed (non-masked) positions."""
    return (mask == 0).sum() / mask.size


seq_lengths = [64, 128, 256, 512, 1024]
patterns = {
    "Full": [],
    "Causal": [],
    "Local (w=32)": [],
    "Sparse (w=16, s=64)": [],
}

for n in seq_lengths:
    # Full attention: all n^2 pairs
    patterns["Full"].append(1.0)

    # Causal: lower triangle = n(n+1)/2 pairs
    causal = np.tril(np.ones((n, n)))
    patterns["Causal"].append(causal.sum() / causal.size)

    # Local with window 32
    local = create_local_attention_mask(n, window_size=32)
    patterns["Local (w=32)"].append((local == 0).sum() / local.size)

    # Sparse: local window 16 + stride 64
    sparse = create_sparse_attention_mask(n, local_window=16, stride=64)
    patterns["Sparse (w=16, s=64)"].append((sparse == 0).sum() / sparse.size)
Out[36]:
Visualization
Line plot showing attention density decreasing for local and sparse patterns as sequence length increases.
Density of attention patterns across increasing sequence lengths. Full and causal attention maintain roughly constant density as sequences grow, while local and sparse patterns become dramatically more efficient. At 1024 tokens, sparse attention requires less than 10 percent of the connections that full attention uses, enabling much longer context windows on the same hardware.

The efficiency gains are dramatic. Full attention always uses 100% of possible pairs. Causal attention uses approximately 50% (the lower triangle). But local and sparse patterns become increasingly efficient as sequences grow longer: at 1024 tokens, local attention with window 32 uses only a few percent of pairs, and sparse patterns even less. This is why efficient attention variants are essential for processing long documents, where full attention would be computationally prohibitive.

Limitations and Impact

Attention masking is essential infrastructure for modern transformers, but it also introduces implementation constraints and failure modes that practitioners need to understand.

The most significant computational limitation is that standard masking does not reduce the fundamental O(n2)O(n^2) cost of computing all pairwise attention scores. Even with half the positions masked, we still allocate and populate the full n×nn \times n score matrix before applying the mask. For causal attention, approximately half the matrix is immediately set to near-zero by softmax, but the memory for those entries and the computation to produce them has already been spent. Truly sparse attention requires specialized implementations that physically skip the masked computations, such as block-sparse matrix operations or the custom CUDA kernels used in FlashAttention. Writing and optimizing such kernels is significantly harder than dense matrix multiplication, which benefits from decades of hardware and software optimization. This gap between the theoretical efficiency of sparse attention and its practical implementation complexity is one of the active challenges in efficient transformer research.

Masking also fundamentally constrains what the model can learn, and the mask choice becomes an architectural decision with lasting consequences. Causal masking prevents bidirectional context: when processing the word "bank," the model cannot look ahead to "river" or "money" to resolve the ambiguity. This is by design for generative models, but it makes causal models weaker for discriminative tasks like sentiment classification or named entity recognition, where the full context is available and relevant. This is precisely why BERT was designed without causal masking, using bidirectional attention instead. The choice between causal and bidirectional attention is not a technical detail but an architectural philosophy that shapes the model's capabilities and the tasks it excels at.

A subtler limitation involves the interaction between masking and gradient flow during training. When entire rows of the attention matrix are masked (for instance, padding positions in the query dimension), the model has no gradient signal at those positions. This is correct behavior, but it means that models trained on heavily padded batches receive weaker gradient signals. In extreme cases, if most of a batch is padding, training can become unstable or slow. Practitioners typically bucket sequences by length before batching to minimize padding, but this adds preprocessing complexity and can introduce bias if done carelessly.

Despite these limitations, masking made several capabilities of modern NLP practical. Causal masking enabled efficient parallel training of autoregressive models, collapsing what would be nn sequential forward passes into a single parallel one. This made training on trillions of tokens computationally feasible and gave us the GPT family of models. Padding masks allowed practical batching of diverse real-world text, which is essential for any realistic training pipeline since natural language is inherently variable in length. Custom patterns like local and strided attention extended transformers to sequence lengths that would otherwise require prohibitive amounts of memory and compute, enabling applications in genomics, document understanding, and audio processing where sequences can be thousands or millions of tokens long.

The mask is also a safety mechanism. The causal mask is what prevents the model from developing a fundamentally incorrect inductive bias during training, one that exploits future tokens. A model trained without the causal mask on language modeling data would appear to perform well during training but would fail catastrophically at inference time, since future tokens are unavailable during generation. The mask enforces consistency between training and inference, and that consistency is foundational to reliable language modeling.

Key Parameters

When implementing attention masking, several parameters control the mask behavior:

  • mask_value: The large negative value used for masked positions (typically −109-10^9 or -float('inf')). Using −109-10^9 rather than true infinity avoids numerical issues with some operations while still producing near-zero attention weights after softmax. If every position in a row is masked, using −∞-\infty produces NaN via 0/00/0 in softmax, while −109-10^9 produces a uniform distribution that is mathematically wrong but numerically stable.

  • window_size (local attention): Controls how many positions on each side a token can attend to. Smaller windows reduce computation but may miss important long-range dependencies. Common values range from 128 to 512 tokens in production systems.

  • stride (strided attention): Determines the spacing between attended positions in sparse patterns. A stride of kk means attending to every kk-th position, reducing complexity while maintaining some global connectivity.

  • pad_token_id: The token ID used for padding in tokenized sequences. This must match the padding token used during tokenization to correctly identify positions to mask.

Summary

Attention masking controls which positions can attend to which others, enabling autoregressive generation, efficient batch processing, and scalable attention over long sequences. The mechanism is remarkably simple: add large negative values to the scores of positions that should be blocked, and let softmax do the rest. The complexity lies not in the mechanism but in understanding when and why each type of mask is needed.

The key concepts from this chapter are:

  • Additive masking: Adding large negative values before softmax drives attention weights to near-zero, effectively blocking those positions. No special logic is required in the attention function itself.
  • Padding masks: Prevent attention to padding tokens when batching sequences of different lengths. Real tokens should not be influenced by meaningless padding values, and allowing such influence would corrupt representations and waste model capacity.
  • Causal masks: Block attention to future positions, enforcing left-to-right information flow for autoregressive language models. This enables parallel training on full sequences while maintaining the correct sequential context constraint, collapsing nn sequential predictions into a single parallel forward pass.
  • Mask combination: Multiple masks combine by addition. Any position blocked by any mask receives near-zero attention, implementing logical OR over the block conditions.
  • Broadcasting: Masks can have shapes like (1, 1, seq_len, seq_len) for global patterns or (batch, 1, 1, seq_len) for per-sequence patterns, with broadcasting handling dimension expansion automatically and without extra memory allocation.
  • Custom patterns: Local and strided attention, along with sparse patterns, reduce complexity for long sequences by limiting which positions can interact. These patterns trade some expressiveness for dramatic efficiency gains that make long-context modeling practical.
  • Architectural choice: The choice of mask shapes what the model can learn. Causal masking suits generative models; bidirectional (no causal mask) attention suits discriminative models. This decision propagates through all downstream fine-tuning and deployment choices.

In the next chapter, we'll explore multi-head attention, which runs multiple attention operations in parallel with different learned projections. This allows the model to attend to information from different representation subspaces at different positions simultaneously, a capability that masking makes possible to apply selectively and efficiently.

Quiz

Ready to test your understanding? Take this quick quiz to reinforce what you've learned about attention masking.

Attention Masking Quiz

Question 1 of 100 of 10 completed
Why do we add large negative values (like -10⁹) to attention scores for masked positions?

Comments

1 comment

  1. AvadorMember

    Another great chapter, really easy to understand explanations.

    The coding blocks are a really nice touch. Gives perspective of the actual implementation. On to multi head attention. Thanks Michael.

    1. Michael BrenndoerferMember

      Thank you, Avador - Glad you find it useful!

Reference

Citation details

Cite or share this article.

BIBTEXAcademic
@misc{brenndoerfer2025attentionmasking, author = {Michael Brenndoerfer}, title = {Attention Masking: Controlling Information Flow}, year = {2025}, url = {https://mbrenndoerfer.com/writing/attention-masking-transformers}, organization = {mbrenndoerfer.com}, note = {Accessed: 2026-09-27} }
APAAcademic
Michael Brenndoerfer (2025). Attention Masking: Controlling Information Flow. Retrieved from https://mbrenndoerfer.com/writing/attention-masking-transformers
MLAAcademic
Michael Brenndoerfer. "Attention Masking: Controlling Information Flow." 2026. Web. September 27, 2026. <https://mbrenndoerfer.com/writing/attention-masking-transformers>.
CHICAGOAcademic
Michael Brenndoerfer. "Attention Masking: Controlling Information Flow." Accessed September 27, 2026. https://mbrenndoerfer.com/writing/attention-masking-transformers.
HARVARDAcademic
Michael Brenndoerfer (2025) 'Attention Masking: Controlling Information Flow'. Available at: https://mbrenndoerfer.com/writing/attention-masking-transformers (Accessed: September 27, 2026).
SimpleBasic
Michael Brenndoerfer (2025). Attention Masking: Controlling Information Flow. https://mbrenndoerfer.com/writing/attention-masking-transformers

About the author

Continue with the full handbook

This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.

Explore Language AI Handbook
Newsletter

Stay up to date

Get articles, book updates, and news delivered to your inbox.

No spam, unsubscribe anytime.

or

Join the community

Sign in to remove popups, track your reading progress, and join the discussion.