← All tutorials

Positional Encoding and Masking

On this page

1 Attention has no idea about order 2 Sine and cosine waves encode each position 3 Relative positions rotate or penalize the score 3.1 RoPE rotates each query and key by its position 3.2 ALiBi subtracts a penalty that grows with distance 4 A learned table cannot extrapolate, and ALiBi extrapolates furthest 5 Masking decides who may look at whom 5.1 The padding mask hides the padding tokens 5.2 The causal mask hides the future 5.3 Loss masking removes padding from the loss 6 RoPE and the causal mask live inside each attention call 7 References

Attention alone ignores word order and lets every token see every other token. Positional encodings tell attention where each token sits, and masks block token pairs that must not attend. Positions are added once at the input or, for RoPE and ALiBi, in every attention layer; masks apply on every forward pass, in training and generation. Each token (a word or word piece) enters attention as a vector, its embedding. Attention runs in several side-by-side copies called heads.

Attention, the layer this page gives positions and masks.
Attention(Q, K, V)
= softmax(QKT√dk) V
Each token's output is a weighted average of the values V (what tokens carry), weighted by how well its query (what it looks for) matches each key (what others offer). QKT scores every query against every key (T is the transpose), dividing by √dk, the key size, stops the scores from growing with the key size, and softmax turns each row into weights that sum to 1. No term here knows where a token sits, which is the gap the next sections fill.

1 Attention has no idea about order

Nothing in the attention line looks at where a token sits. "Dog bites man" and "man bites dog" look the same to the model.

dogbitesmanattentionno positionsdogbitesmanmanbitesdogattentionno positionsmanbitesdogsame three outputs, only reordered
Figure 1. Without positions, swapping the words only swaps the output rows.

This checks that attention ignores order; five tokens go in once in order and once shuffled, and the last line prints True.

import torch

def attention(Q, K, V):
    S = Q @ K.T / Q.size(-1) ** 0.5
    return S.softmax(dim=-1) @ V

n, d = 5, 16
X = torch.randn(n, d)  # no positions added
out = attention(X, X, X)

perm = torch.randperm(n)
Xs = X[perm]
out_shuf = attention(Xs, Xs, Xs)
same = torch.allclose(out[perm], out_shuf, atol=1e-6)
print(same)  # True
  1. 1Attention with no positions anywhere, the attention eq card above Section 1, line 2.
  2. 2Five tokens through it in their original order, the top row of Figure 1.
  3. 3Shuffled tokens give the same rows shuffled, the bottom row of Figure 1.

2 Sine and cosine waves encode each position

The original Transformer (Vaswani et al., 2017) adds a fixed position vector to each token's embedding before the first layer, built from sine and cosine waves of different speeds, one pair per pair of dimensions:

The positional encoding, and where it is added.
PE(pos,2i) = sin(pos100002i/d)
The positional encoding PE gives position pos one number per dimension; the even dimension 2i is a sine wave over positions. The divisor sets the wave's speed: it grows with the pair number i, so pair 0 cycles every 2π positions and the last pair takes about 10000·2π. Fast pairs tell neighbors apart, slow pairs tell far regions apart, like the second and hour hands of a clock. d is the embedding size, dmodel.
PE(pos,2i+1) = cos(pos100002i/d)
The odd dimension next to it takes the cosine of the same angle, so each pair is a point on a circle, a clock hand at an angle. Moving a fixed offset of a few positions turns every hand by a fixed amount, which is one fixed rotation matrix whatever pos is; the paper hoped this makes "that many tokens back" easy for attention to learn.
xpos = embedding(token)
+ PE(pos)
The first layer receives one vector per token that holds both what the token is and where it sits. The position vector PE(pos) has the embedding's size, so it is simply added; the same word at two positions now gives two different vectors, and so two different queries and keys.
dims 0 and 1 (fast) dims 14 and 15 (slow) grey hand at position 0, dark at 5 pos 5 pos 0 pos 31 dim 0dim 15 −1 0 +1
Figure 2. Each pair of dimensions is a clock hand turning at its own speed; a position is the set of hand angles.

The sinusoidal encoding in NumPy for 8 positions and 16 dimensions; one row per position comes out.

import numpy as np

BASE = 10000

def positional_encoding(n, d):
    pos = np.arange(n)[:, None]  # (n, 1)
    j = np.arange(d)[None, :]    # (1, d)
    i = j // 2                   # dims 2i and 2i+1 share i
    angle = pos / BASE ** (2 * i / d)
    even = j % 2 == 0
    return np.where(even, np.sin(angle), np.cos(angle))

pe = positional_encoding(8, 16)
print(pe.shape)  # (8, 16)
  1. 1Grids of positions and dimensions; dimensions 2i and 2i+1 share i, eq card in Section 2, lines 1 and 2.
  2. 2The angle pos/100002i/d, eq card in Section 2, lines 1 and 2.
  3. 3Sine in even dimensions (line 1) and cosine in odd ones (line 2), eq card in Section 2.
  4. 4One row per position comes out, one row of the heat map in Figure 2.

Cosine similarity measures how alike two vectors point, 1 for the same direction and 0 for perpendicular.

0.40.71.0051015positions apartcosine similarity with position 010.93620.76730.693
Figure 3. Neighbors stay alike (1), and similarity falls with distance, but not smoothly: five apart (2) is more alike than three apart (3).

3 Relative positions rotate or penalize the score

Two methods, RoPE (rotary position embedding) and ALiBi (attention with linear biases), leave the embeddings alone and act inside attention at every layer, so a score can depend on how far apart two tokens are instead of where each one sits.

3.1 RoPE rotates each query and key by its position

RoPE (Su et al., 2021) turns each query and key by an angle set by its position, so "not" two words before "good" scores the same at position 5 or 500.

A turning speed for each pair, then the score after rotation.
θp = 10000−2(p−1)/d
p = 1, 2, …, d/2
Each pair of coordinates in a query or key gets its own turning speed θp, in radians per position. d is the query or key size, so there are d/2 pairs; pair 1 turns a full radian per position and later pairs turn ever slower, the same fast-to-slow spread as the sinusoidal encoding's pairs.
(Rmq) · (Rnk)
= qT Rn−m k
The score after rotation depends only on how far apart the two tokens are. Rm turns every pair p of the query q at position m by m·θp, and Rn turns the key k at n. A dot product only sees the angle between two arrows, so turning both by the same extra amount changes nothing; what survives is k turned by the gap n − m. Shift a whole sentence and every score stays the same.
query key score −10+1

Figure 4. Moving both arrows keeps the angle, so the score, the same.

Query at position 4, key at 6, gap 2.

3.2 ALiBi subtracts a penalty that grows with distance

ALiBi (Press et al., 2021) adds no position vectors; it subtracts from each score, before the softmax, a penalty that grows linearly with distance, so it costs almost nothing.

A score that drops with distance, and how steeply it drops in each head.
sij = qi · kj√dk
− ma·(i − j),  j ≤ i
The farther back key j sits from query i, the lower its score. The score is the usual scaled dot product minus head a's slope ma times the distance i − j: a neighbor loses almost nothing, a token 100 back loses 100 times the slope, so far tokens need a much better match to get weight. Nothing is learned about position, so the same penalty extends to any length; j ≤ i because ALiBi pairs with the causal mask (Section 5.2). The paper writes this without √dk for simplicity; the dot product is still scaled, and the bias is never scaled.
ma = 2−8a/h
a = 1, 2, …, h
Each head gets a different slope, so some heads look near and others far. Head a of the h heads gets 2 raised to −8a/h, so each head's slope is a fixed fraction of the last: 1/2, 1/4, down to 1/256 for 8 heads. A steep head barely sees past a few tokens; a shallow one reads most of the context.
0−8 016 penalty head 1, slope 1/2, attends near head 8, slope 1/256, attends far distance i − j between query and key
Figure 5. Each head subtracts a straight-line penalty with its own slope, from 1/2 to 1/256.

4 A learned table cannot extrapolate, and ALiBi extrapolates furthest

Every positional encoding faces one test: inputs longer than any seen in training. A learned position table has one trainable vector per position. The Transformer paper found it as good as the sinusoidal encoding and kept the sinusoidal one because it "may allow the model to extrapolate" to longer sequences.

dev BLEU, English to Germansinusoidal25.8learned table25.71
Figure 6. The two encodings score within 0.1 BLEU (1), a translation score where higher is better. From the Transformer paper's Table 3, row (E).

In a learned PyTorch table, position ids go in, one learned row per position comes out, and the last call fails.

import torch
import torch.nn as nn

MAX_LEN, d = 512, 64
table = nn.Embedding(MAX_LEN, d)  # one row per position

ids = torch.arange(MAX_LEN)
print(table(ids).shape)  # torch.Size([512, 64])
table(torch.arange(2000))  # IndexError, no row past 511
  1. 1A learned table with one row per position up to 511, the learned table compared at marker 1 in Figure 6.
  2. 2Looking up all 512 positions, 0 to 511, works, since each has its own row.
  3. 3Position 512 and beyond has no row and fails, which is why Figure 7 plots no learned table at all.

The GPT-style model on The Transformer Block uses such a table, so its longest input is fixed when it is built.

The ALiBi paper trained a 16-layer language model on WikiText-103 (about 103M tokens of Wikipedia) with 512-token inputs and measured perplexity, how surprised it is by the real next token, on longer inputs, against sine, RoPE and T5's bias (a learned score bias per distance bucket). ALiBi stays below its 512-token score past 12,000 tokens.

Perplexity against inference input length for models trained on 512 tokens: the sinusoidal, rotary and T5 bias lines rise as inputs grow, the ALiBi line stays flat and low out to about 15,000 tokens
Figure 7. From the paper's Figure 1: "… current position methods … show degraded perplexity …, but our method … does not."

5 Masking decides who may look at whom

Padding and the future are two reasons to block a pair, and both add a mask matrix M to the scores before the softmax.

The mask is added to the scores before the softmax.
A = softmax(S + M)
A blocked pair gets exactly zero weight, and the open pairs share all of it. The mask M is added to the score matrix S: 0 where a pair may attend, so its score is untouched, and negative infinity where it may not. Softmax raises e to each entry, and e to negative infinity is 0, so the blocked keys vanish from A and each row's remaining weights still sum to 1.

5.1 The padding mask hides the padding tokens

Short sequences in a batch are filled out to the same length with padding tokens. Real tokens must not attend to them, so the padding keys are masked; a padding query is harmless, since its output is left out of the loss (Section 5.3).

keys (j) queries (i) k1 k2 k3 k4 k5 q1 0 0 0 -inf -inf q2 0 0 0 -inf -inf q3 0 0 0 -inf -inf q4 0 0 0 -inf -inf q5 0 0 0 -inf -inf
Figure 8. With two padding tokens at the end, columns k4 and k5 go dark for every query.

The padding mask in PyTorch for two sequences, 5 and 3 tokens long; scores go in and weights with 0 on every padded key come out.

import torch

B, h, T = 2, 8, 5  # batch, heads, padded length
lengths = torch.tensor([5, 3])
keep = torch.arange(T) < lengths[:, None]  # (B, T)

S = torch.randn(B, h, T, T)
blocked = ~keep[:, None, None, :]          # (B, 1, 1, T)
S = S.masked_fill(blocked, float("-inf"))
A = S.softmax(dim=-1)  # padded keys get weight 0
print(A[1, 0])
  1. 1Marks which keys are real from each sequence's length, the light columns of Figure 8.
  2. 2Sets the padded keys' scores to negative infinity, the M of the eq card in Section 5, line 1.
  3. 3Softmax gives those keys weight 0, the dark columns k4 and k5 in Figure 8.

Masking keys only still leaves a real key in every row. Masking padded queries too gives rows that are all negative infinity; there the softmax divides 0 by 0 and writes NaN ("not a number"), which spreads through training.

5.2 The causal mask hides the future

A decoder, which writes left to right, must not see later tokens. The causal mask blocks every key after the query: zeros on and below the diagonal, negative infinity above it.

The snippet builds that matrix for 5 tokens; look at triu, which keeps negative infinity only above the diagonal.

import torch

T = 5
NEG_INF = float("-inf")
M = torch.full((T, T), NEG_INF).triu(diagonal=1)
print(M)  # 0 on and below the diagonal, -inf above

Because no row can see its future, training grades all T next-token predictions in one pass (drawn on The Transformer Block).

Figure 9. Play writes the sentence one word at a time; each row reads only its own word and the words before it, as generation does.

0, allowed −inf, blocked (softmax turns it into 0)
k1k2k3k4k5
q1
q2
q3
q4
q5

Figure 10. Both masks add negative infinity to blocked scores.

5.3 Loss masking removes padding from the loss

Loss masking keeps padded positions out of the loss; you set their targets to -100, the ignore_index that F.cross_entropy skips by default.

import torch
import torch.nn.functional as F

B, T, VOCAB = 2, 5, 100
IGNORE = -100
logits = torch.randn(B, T, VOCAB)
targets = torch.tensor([[4, 8, 15, 16, 23], [42, 7, 9, IGNORE, IGNORE]])
pred = logits.flatten(0, 1)  # (B*T, VOCAB)
target = targets.flatten()   # (B*T,)

loss = F.cross_entropy(pred, target, ignore_index=IGNORE)
print(loss)  # mean over the 8 real targets
  1. 1Logits and targets, flattened to one row per position; -100 marks the two padded targets at positions 4 and 5, the dark columns of Figure 8.
  2. 2The mean cross-entropy over the 8 real targets only, so the dark padding positions of Figure 8 add nothing to the loss.

Attention masks choose which tokens each token reads; loss masks choose which predictions count.

In From BLIP to the Q-Former, three different masks let one set of weights train on three objectives at once.


6 RoPE and the causal mask live inside each attention call

RoPE as Hugging Face's Llama code writes it, then a causal call; shape (B, h, T, dk) goes in and comes out.

import torch
import torch.nn.functional as F

B, h, T, d_k, BASE = 1, 8, 16, 64, 10000.0
inv_freq = BASE ** -(torch.arange(0, d_k, 2) / d_k)
angles = torch.arange(T)[:, None] * inv_freq  # (T, d_k/2)
emb = torch.cat((angles, angles), dim=-1)     # (T, d_k)
cos, sin = emb.cos(), emb.sin()

def rotate_half(x):
    x1, x2 = x.chunk(2, dim=-1)
    return torch.cat((-x2, x1), dim=-1)

q, k, v = torch.randn(3, B, h, T, d_k)
q = q * cos + rotate_half(q) * sin
k = k * cos + rotate_half(k) * sin
out = F.scaled_dot_product_attention(q, k, v, is_causal=True)
  1. 1The speeds θp, one per pair, times each position, eq card in Section 3.1, line 1.
  2. 2Pairs coordinate c with c + dk/2 and turns each pair a quarter turn, part of the rotation Rm, eq card in Section 3.1, line 3.
  3. 3Rotates q and k by their positions, eq card in Section 3.1, line 3.
  4. 4Causal attention on the rotated queries and keys, eq card in Section 5, line 1, with the causal M of Section 5.2.
Sine and cosineAdded once to the token embeddings at the bottom of the stack, every forward pass; nothing learned.
Learned tableSame place, one learned row per position, 512 rows here.
RoPEEvery attention layer, rotates q and k before the scores; no weights.
ALiBiEvery attention layer, a fixed bias added to the scores; no weights.
MasksCausal mask built once per sequence length, padding mask once per batch; added in every attention layer.
Loss maskingEvery training step, inside the loss only.
RoPE baseSets pair p's turn per position, base−2(p−1)/d. Larger turns every pair after the first slower, so far tokens stay apart; smaller turns faster and wraps sooner. Infinite, every pair but the first stops turning, so only one fast pair carries position and it repeats every 2π positions; 1, every pair turns alike, one radian per position. Default 10000, the same constant that sets the slowest wave of the sinusoidal encoding in Section 2; rope_theta in Hugging Face.
ALiBi slope maMultiplies the distance i − j. Steeper makes head a look near, gentler lets it reach far; 0 ignores order, very steep sees only the neighbour. Fixed at 2−8a/h, 1/2 to 1/256 for 8 heads.
Codemodeling_llama.py (Hugging Face), F.scaled_dot_product_attention (PyTorch docs).

How do attention, positions and masks stack into a full model? The Transformer Block.


7 References

  1. Vaswani, A., et al. (2017). Attention Is All You Need. NeurIPS 2017. arXiv:1706.03762.
  2. Su, J., et al. (2021). RoFormer: Enhanced Transformer with Rotary Position Embedding. arXiv:2104.09864.
  3. Press, O., et al. (2021). Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation. ICLR 2022. arXiv:2108.12409.