Relative position embeddings
Instead of learning what slot 47 means, learn what "three tokens back" means. T5 buckets distances so one small table covers any sequence length.
- 13 min read
- 3 reading levels
- Updated
Read these first
On this page 7
One lesson, three depths. Pick the one that fits you today — you can switch any time.
Beginner — No maths. Plain English.
A relative position embedding learns what distances mean, not what slots mean. "Two words back" is one thing to learn, wherever it happens.
Think about giving someone directions in a market. You do not say "go to stall number 47". You say "two stalls down, then turn left at the tea shop".
Those instructions work from anywhere in the market. Stall numbers only work if the person already knows the numbering.
Relative positions are the directions version. The model learns "the word right before this one" as a single idea, and reuses it at word 5 and at word 5000.
Why this beats numbering the slots
A slot-numbered table has to learn "the word before this one matters" separately for every slot. Slot 3 learns it. Slot 4 learns it again. Slot 900 has to learn it from the handful of examples that reached slot 900.
That is enormously wasteful. The relationship is the same everywhere, and the model is forced to relearn it position by position.
Learning distances instead means every example in your data teaches every position at once. A short sentence teaches "one back" as effectively as a long one does.
The problem this creates
Distances have a range problem. In a 4,000-token document there are about 8,000 possible distances, counting both directions.
Learning a separate number for each is as wasteful as the slot table you were escaping. And distance 3,001 versus distance 3,002 is a difference nobody needs.
T5's answer: buckets
T5 groups distances into buckets, and the grouping is deliberately uneven.
distance: 0 1 2 3 ... 15 16-19 20-25 26-33 ... 128+
bucket : 0 1 2 3 ... 15 16 17 18 ... 31
<-- exact, one each --> <-- ranges that keep widening -->Nearby distances each get their own bucket, because the difference between "one word back" and "two words back" is enormous in grammar.
Far distances share buckets, in ranges that grow steadily. The difference between 400 back and 500 back barely matters, so they land together.
Every distance beyond the largest range falls into the final bucket. There is no maximum length and no crash.
What the buckets are used for
Each bucket holds one learned number per attention head. That number is added to the attention score, exactly the way ALiBi's penalty is.
The difference from ALiBi is that these numbers are learned rather than fixed. A head can learn to boost "two words back" and suppress "one word back", if the data supports it.
The whole table is tiny. Thirty-two buckets times twelve heads is 384 numbers, and it covers a document of any length.
Where you have seen this
- T5 and Flan-T5, still widely used for summarisation and question answering.
- Transformer-XL, which introduced relative attention for long documents.
- Swin Transformer, using a two-dimensional version for image windows.
- Music transformers, where "four beats ago" is the useful unit.
Remember this
- Learn what distances mean, not what slot numbers mean.
- Nearby distances get their own bucket; far ones share widening ranges.
- The table is tiny and works at any length, because everything past the last range shares a bucket.
What to learn next
- Models with no positional encoding at all — the case for removing the mechanism entirely.
- ALiBi — the same bias idea with a fixed function instead of a table.
- Attention — where the bias lands.
Developer — Code and libraries.
Setup
pip install torch "transformers==5.6.2"Transformers is used only to check our implementation against T5's own.
The bucketing function, and the bias it feeds
import math
import torch
import torch.nn as nn
def relative_position_bucket(rel_pos, bidirectional=True, num_buckets=32, max_distance=128):
"""Map a raw distance to a bucket id. Near distances get their own bucket."""
buckets = 0
if bidirectional:
num_buckets //= 2
buckets += (rel_pos > 0).long() * num_buckets # left and right kept separate
rel_pos = rel_pos.abs()
else:
rel_pos = -torch.min(rel_pos, torch.zeros_like(rel_pos))
max_exact = num_buckets // 2 # half the buckets are exact
is_small = rel_pos < max_exact
large = max_exact + (
torch.log(rel_pos.float() / max_exact)
/ math.log(max_distance / max_exact)
* (num_buckets - max_exact)
).long()
large = torch.min(large, torch.full_like(large, num_buckets - 1))
return buckets + torch.where(is_small, rel_pos, large)
d = torch.tensor([0, 1, 2, 7, 8, 15, 16, 40, 100, 127, 128, 500, 5000])
print("distance -> bucket (one-directional, 32 buckets, max_distance 128)")
print("distance:", d.tolist())
print("bucket :", relative_position_bucket(-d, bidirectional=False).tolist())
from transformers.models.t5.modeling_t5 import T5Attention
ref = T5Attention._relative_position_bucket(-d, bidirectional=False,
num_buckets=32, max_distance=128)
print("matches transformers' own T5 implementation:",
torch.equal(relative_position_bucket(-d, bidirectional=False), ref))
# The bias itself: one learned number per (head, bucket).
N_HEADS, N_BUCKETS = 4, 32
torch.manual_seed(0)
bias_table = nn.Embedding(N_BUCKETS, N_HEADS)
print(f"\nlearned table: {tuple(bias_table.weight.shape)} = "
f"{bias_table.weight.numel()} numbers, for ANY sequence length")
seq = 6
pos = torch.arange(seq)
rel = pos[None, :] - pos[:, None] # key position minus query position
bucket = relative_position_bucket(rel, bidirectional=False)
print("\nrelative positions for 6 tokens:\n", rel)
print("\nbucket ids:\n", bucket)
bias = bias_table(bucket).permute(2, 0, 1) # (heads, query, key)
print("\nbias added to head 0's attention scores:\n", bias[0].round(decimals=3))
for n in [6, 512, 4096]:
p = torch.arange(n)
b = relative_position_bucket(p[None, :] - p[:, None], bidirectional=False)
print(f"seq_len {n:>5}: highest bucket used = {b.max().item()}, table size unchanged")distance -> bucket (one-directional, 32 buckets, max_distance 128)
distance: [0, 1, 2, 7, 8, 15, 16, 40, 100, 127, 128, 500, 5000]
bucket : [0, 1, 2, 7, 8, 15, 16, 23, 30, 31, 31, 31, 31]
matches transformers' own T5 implementation: True
learned table: (32, 4) = 128 numbers, for ANY sequence length
relative positions for 6 tokens:
tensor([[ 0, 1, 2, 3, 4, 5],
[-1, 0, 1, 2, 3, 4],
[-2, -1, 0, 1, 2, 3],
[-3, -2, -1, 0, 1, 2],
[-4, -3, -2, -1, 0, 1],
[-5, -4, -3, -2, -1, 0]])
bucket ids:
tensor([[0, 0, 0, 0, 0, 0],
[1, 0, 0, 0, 0, 0],
[2, 1, 0, 0, 0, 0],
[3, 2, 1, 0, 0, 0],
[4, 3, 2, 1, 0, 0],
[5, 4, 3, 2, 1, 0]])
bias added to head 0's attention scores:
tensor([[-1.1260, -1.1260, -1.1260, -1.1260, -1.1260, -1.1260],
[ 0.8490, -1.1260, -1.1260, -1.1260, -1.1260, -1.1260],
[ 0.3220, 0.8490, -1.1260, -1.1260, -1.1260, -1.1260],
[ 0.1200, 0.3220, 0.8490, -1.1260, -1.1260, -1.1260],
[-1.3530, 0.1200, 0.3220, 0.8490, -1.1260, -1.1260],
[ 0.5990, -1.3530, 0.1200, 0.3220, 0.8490, -1.1260]],
grad_fn=<RoundBackward1>)
seq_len 6: highest bucket used = 5, table size unchanged
seq_len 512: highest bucket used = 31, table size unchanged
seq_len 4096: highest bucket used = 31, table size unchangedReading that output
Distances 0 to 15 map to buckets 0 to 15, one for one. Exact resolution where grammar lives. Then distance 40 lands in bucket 23 and distance 100 in bucket 30. The compression accelerates.
Distances 127, 128, 500 and 5000 all share bucket 31. Everything past max_distance collapses into one. That is what makes the scheme length-agnostic, and it is also its ceiling: a T5 model genuinely cannot distinguish 200 tokens back from 5000 tokens back.
The check against transformers prints True. This is not a paraphrase of T5's bucketing, it is T5's bucketing.
The bucket matrix is constant along its diagonals. Every cell on the main diagonal is bucket 0, every cell one below is bucket 1. That structure — constant along diagonals — is what "relative" means concretely.
Look at the upper triangle of the bias matrix: every entry is -1.1260. All those cells are bucket 0, because the one-directional bucketing clamps future positions to distance 0. Those cells are then masked out by the causal mask before softmax, so the value never matters. It looks like a bug and is not.
128 numbers cover length 4096. Compare that with a learned absolute table at width 768, which would need over three million.
Where the bias is applied in T5
T5 computes the bias once, in the first layer of the encoder and once in the decoder, then shares it across every other layer:
scores = torch.matmul(q, k.transpose(-1, -2)) # note: no division by sqrt(d_k)
scores = scores + position_bias # shared across layers
weights = scores.softmax(dim=-1)Two details people miss. T5 omits the 1/sqrt(d_k) scaling, folding it into the initialisation instead. And the bias is computed by layer zero and passed forward, so layers 1 to N do not have their own tables.
Shaw-style relative embeddings, the earlier design
Before T5's scalar bias, Shaw et al. (2018) added a learned vector per relative position to the keys:
import torch
import torch.nn as nn
torch.manual_seed(0)
SEQ, HEAD_DIM, CLIP = 5, 8, 2
rel_k = nn.Embedding(2 * CLIP + 1, HEAD_DIM) # one vector per clipped distance
pos = torch.arange(SEQ)
rel = (pos[None, :] - pos[:, None]).clamp(-CLIP, CLIP) + CLIP
print("clipped relative index matrix:\n", rel)
q = torch.randn(SEQ, HEAD_DIM)
a_rel = torch.einsum("id,ijd->ij", q, rel_k(rel)) # extra term in the attention logit
print("\nextra logit term shape:", tuple(a_rel.shape))
print("parameters:", rel_k.weight.numel(), "vs a scalar-bias table of 32 x heads")clipped relative index matrix:
tensor([[2, 3, 4, 4, 4],
[1, 2, 3, 4, 4],
[0, 1, 2, 3, 4],
[0, 0, 1, 2, 3],
[0, 0, 0, 1, 2]])
extra logit term shape: (5, 5)
parameters: 40The clipping at distance 2 is visible: every position four or more to the right shares index 4. Shaw's version is more expressive than a scalar bias and considerably more expensive, since it needs a vector per distance and an extra einsum per layer. T5's scalar bias won on cost.
Common mistakes
Using bidirectional bucketing in a decoder. With bidirectional=True the table splits in half, one side for left and one for right. In a causal model the right side is never used, so you have halved your resolution for nothing.
Recomputing the bias in every layer. T5 computes it once and shares it. Recomputing wastes time, and giving each layer its own table changes the architecture and breaks checkpoint loading.
Forgetting max_distance is a real ceiling. Beyond it, all distances are identical to the model. If your task needs to distinguish 300 tokens back from 3000, raise max_distance and num_buckets before training, not after.
Adding the bias after the causal mask. Add the bias, then mask, then softmax. Masking first and adding afterwards puts finite numbers back into cells that were supposed to be -inf.
Try it yourself
Change max_distance from 128 to 2048 and reprint the bucket for distances 100, 500 and 5000. Then work out the smallest num_buckets that still gives distances 0 to 15 their own bucket. That tradeoff, exact resolution near versus reach far, is the only real design decision in this method.
What to learn next
- Models with no positional encoding at all — the case for removing the mechanism entirely.
- ALiBi — the same bias idea with a fixed function instead of a table.
- Attention — where the bias lands.
Researcher — Mathematics and papers.
Shaw et al. (2018)
The first relative formulation adds learned vectors to keys and values, indexed by clipped relative distance:
$$ e_{ij} = \frac{(x_i W^Q)\big(x_j W^K + a^K_{ij}\big)^\top}{\sqrt{d_z}}, \qquad z_i = \sum_j \alpha_{ij}\big(x_j W^V + a^V_{ij}\big) $$
with $a^K_{ij} = w^K_{\mathrm{clip}(j-i,\,k)}$ and $\mathrm{clip}(x,k) = \max(-k, \min(k, x))$. The clip at $k$ (typically 8 to 16) bounds the table at $2k+1$ vectors of width $d_z$ per head.
Cost: $O(n^2 d)$ extra multiplies per head per layer for the key term, plus $(2k+1)d$ parameters. The value term $a^V$ was later found to contribute little and is usually dropped.
Transformer-XL
Dai et al. (2019) expand the logit with sinusoidal relative encodings and two learned global bias vectors:
$$ A_{ij} = \underbrace{q_i^\top k_j}_{(a)} + \underbrace{q_i^\top W_{k,R}\, r_{i-j}}_{(b)} + \underbrace{u^\top k_j}{(c)} + \underbrace{v^\top W{k,R}\, r_{i-j}}_{(d)} $$
Term (a) is content-content. Term (b) is content-position. Term (c) is a learned global key bias, and (d) a learned global position bias; $u, v \in \mathbb{R}^{d}$ replace the query-dependent absolute terms of the naive expansion. Here $r_{i-j}$ is a fixed sinusoidal vector of the relative offset, so no table grows with length.
The implementation uses a shift trick to compute all $r_{i-j}$ terms in one matmul, reducing the cost from $O(n^2 d)$ to a single $O(n^2 d)$ product plus an $O(n^2)$ reindex.
T5
Raffel et al. (2020) reduce the whole idea to a scalar added to the logit:
$$ A_{ij} = q_i^\top k_j + b^{(h)}_{\,\beta(j - i)} $$
with $\beta$ the bucketing function and $b \in \mathbb{R}^{B \times H}$ for $B$ buckets and $H$ heads. Default $B = 32$, $\max_\text{distance} = 128$. The bucketing is exact for the first $B/2$ (or $B/4$ bidirectional) offsets and logarithmic thereafter:
$$ \beta(\delta) = \begin{cases} |\delta| & |\delta| < B_{\text{exact}} \[4pt] B_{\text{exact}} + \left\lfloor \dfrac{\ln!\big(|\delta| / B_{\text{exact}}\big)}{\ln!\big(D_{\max} / B_{\text{exact}}\big)} \big(B - B_{\text{exact}}\big) \right\rfloor & \text{otherwise} \end{cases} $$
clamped to $B - 1$. Parameters: $BH$ per model, since the table is computed in layer zero and shared. For T5-base that is $32 \times 12 = 384$ numbers, against $512 \times 768 = 393{,}216$ for a BERT-style absolute table.
T5 also omits the $1/\sqrt{d_k}$ scaling and compensates in initialisation.
Placement in the taxonomy
Setting $b^{(h)}_{\beta(\delta)} = -m_h |\delta|$ with fixed $m_h$ recovers ALiBi exactly. T5's bias is therefore the learned, bucketed generalisation of ALiBi, and ALiBi is T5's bias with the table replaced by a hand-chosen linear function. Their behaviour differs because a learned table can be non-monotone and head-specific in ways a single slope cannot.
Two-dimensional variants
Liu et al. (2021), Swin Transformer, use a relative position bias inside each attention window. For a $M \times M$ window the offsets span $(2M-1)^2$ values, and the bias table is indexed by the flattened 2-D offset. This is a direct port of T5's scalar bias to a grid, and it is the standard positional mechanism in windowed vision transformers.
Empirical standing
Kazemnejad et al. (2023) rank T5's relative bias first among explicit positional encodings for downstream length generalisation in decoder-only models, ahead of ALiBi and RoPE, with NoPE ahead of all of them on their benchmark. Press et al. (2022) report T5 bias extrapolates well but is markedly slower than ALiBi in wall-clock terms, because the bucket gather and the extra tensor are not free.
Speed is the reason relative bias lost to RoPE in large-scale decoders. RoPE folds into the query and key projections with elementwise operations and composes with fused attention kernels. A gathered bias tensor of shape $(H, n, n)$ does not, without kernel support.
Papers
- Shaw, Uszkoreit and Vaswani, Self-Attention with Relative Position Representations, NAACL 2018 — arxiv.org/abs/1803.02155
- Dai et al., Transformer-XL: Attentive Language Models Beyond a Fixed-Length Context, ACL 2019 — arxiv.org/abs/1901.02860
- Raffel et al., Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer (T5), JMLR 2020 — arxiv.org/abs/1910.10683
- Liu et al., Swin Transformer, ICCV 2021 — arxiv.org/abs/2103.14030
What to learn next
- Models with no positional encoding at all — the case for removing the mechanism entirely.
- ALiBi — the same bias idea with a fixed function instead of a table.
- Attention — where the bias lands.