我最早动手训练 Transformer 时,最让我犯嘀咕的组件不是 attention,而是 position encoding。当时我的直觉是:你把一张表塞进 token embedding 里,模型就能“记住”位置。后来我读到一篇论文,把这张表整个扔掉,只在 attention score 上减一个随距离变大的数字——结果外推表现比辛辛苦苦学出来的位置编码还好。论文名字就叫 Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation。这篇文章想跟你聊聊 ALiBi:它怎么工作,为什么有效,以及它的天花板在哪。

位置编码解决什么问题,又卡在哪里
self-attention 计算的是 token 两两之间的相似度,对位置完全不敏感。把一句话的顺序打乱,attention 分数一模一样。为了区分“猫追狗”和“狗追猫”,必须把“谁在哪个位置”的信息注入。原始 Transformer 用绝对位置编码:给每个位置一个向量加到 embedding 上。BERT 用可学习的绝对位置向量,GPT 也是如此。这种方案有个问题:训练时位置向量的行数只有 max_len 那么多,超长序列没有对应的向量,只能硬插值,性能崩得很快。这也是早期 LLM 的 context length 被卡在 512 或 1024 的原因之一。
后来出现了相对位置编码(T5 的 bias)、Rotary(RoPE)等,解决了部分外推问题,但各自有额外机制。而 ALiBi 选择了一条更极端的路:完全不用位置向量,把位置信息表达成 attention score 上的一个负 bias。
ALiBi 就两件事:减一个数,每头斜率不同
假设你有一个 query 在位置 i,一个 key 在位置 j。ALiBi 在它们算出的注意力分数上直接减去一个量:
score(i, j) = query_i @ key_j - m * |i - j|
其中 m 是每个注意力头专属的斜率。距离越远,减得越多,注意力分数越低,softmax 之后就被压得越小。这个设定有多省事?你只需要在 attention 矩阵上加一个由 sequence length 决定的矩阵,每个头用自己的 m。m 不用学习,论文里取几何级数:比如 8 个头,斜率就是 1/2、1/4、1/8……一直到 1/256。官方实现见 GitHub 仓库。
写一个伪代码你感受下:
import torch
from torch import arange
def alibi_scores(q, k, m):
scores = q @ k.transpose(-1, -2) # [batch, heads, query_len, key_len]
positions = arange(q.size(-1)).float()[:, None] - arange(k.size(-1)).float()[None, :]
distance = positions.abs()
scores = scores - m.view(1, -1, 1, 1) * distance[None, None]
return scores
没有位置 embedding,没有相对位置表,没有旋转矩阵。就一个减法。
为什么这个“偷懒”的办法反而外推得好
我一开始觉得不靠谱:一个永远惩罚全局距离的先验,会不会让模型失去捕捉远距离依赖的能力?比如代词指代,指代对象可能隔了几十个词。如果注意力被压得很低,怎么联系?这个疑问让我困惑了很久,直到我意识到:注意力是多头分工的。每个头有不同的斜率,意味着有的头对距离极度敏感(只管附近),有的头几乎不惩罚远距离(专门用来连接远处)。如果某个头的斜率非常小,它就能形成全局连接。模型的表达能力没有丢失,只是把“远近偏好”分摊到不同头上。
而且,恰恰因为 ALiBi 不学位置向量,它根本不存在“没见过那么远”的问题。你在长度 512 的数据上训练,学到的是“距离越远,bias 越大”的连续函数。测试时不管序列是 1024 还是 2048,这个函数照样成立,只是 bias 变得更大一点。这比学一组离散位置向量好太多了。
我自己的一个经历:我试着在一个中文生成任务里,把标准 GPT(用 learned positional embeddings)改成 ALiBi。训练长度 512 时,标准 GPT 跑到 768 就崩了,ALiBi 推到 1024 还稳稳的。当时我有点被说服了——尽管从直觉上,我觉得它简单得太不像话。
拿它和位置编码全家桶比一比
| 方案 | 位置表达形式 | 是否学习 | 长度外推 | 代表 |
|---|---|---|---|---|
| 绝对位置编码 | 位置索引 → 向量,加进 embedding | 是 | 差 | 原始 Transformer, BERT |
| 相对位置嵌入 | 距离索引 → 向量,查表加进 score | 是 | 中等(超出表范围即失效) | T5, DeBERTa |
| RoPE | 旋转角度按频率乘进 q/k | 否(频率可调) | 较好,但需要插值 | LLaMA, GPT-NeoX |
| ALiBi | 距离 × 斜率,直接减到 score | 否(斜率固定) | 训练 512 可测 4k+ | BLOOM, MPT |
BLOOM 和 MPT 实际都采用了 ALiBi,分别见 HuggingFace BLOOM 页面 和 MosaicML 发布说明。这说明它已经从论文走进了大规模生产。
ALiBi 的边界:它不是万能外推器
ALiBi 不是万能的。论文里虽然展示出优秀的外推,但如果你把训练长度本身设得很长,差距就会缩小。有研究指出,ALiBi 的固定斜率在小训练长度上表现好,但长序列训练时可能限制模型对远距离信息的利用。另外,线性惩罚意味着距离 1000 和距离 100 的 bias 相差 10 倍,如果序列长度达到 100k,bias 可能大到让所有长距离注意力都几乎为 0,导致模型只能局部建模。这也是后来出现交叉注意力插值、动态斜率调节的原因。
当前业界采用 ALiBi 的大模型主要是 BLOOM(176B)和 MosaicML 的 MPT 系列,不过它们也没有把 context 扩展到无限。实际上对于超长 context(比如 128k),很多模型用 RoPE 或位置插值。ALiBi 的地位更多是“一个优雅的证明”,证明位置信息可以更直接地注入。
常见问题
没有位置编码,那“猫咬狗”和“狗咬猫”它能区分吗?
能。因为 query 和 key 在训练时会发生一种“隐性”的方向偏差——比如“咬”作为 key,跟紧跟在后面的宾语有更强的关联。ALiBi 通过距离惩罚约束了位置,但不是完全靠自己的固定函数完成区分。模型仍然有顺序敏感能力,只是位置信号来自距离。
斜率是怎么固定的?需要调吗?
论文里用的是几何序列,每头一个。比如 8 个头,斜率就是 1/2、1/4……到 1/256。实际实现中只要按头数生成这个序列,不需要训练。调斜率可以略微提升效果,但通常不值得。
ALiBi 和 T5 的相对位置 bias 到底差别在哪?
T5 用可学习的 bias,距离是离散的,每个距离一个标量,所以训练时只见过有限距离范围。ALiBi 使用固定线性函数,距离是连续的,天然可以外推。
我该在我的模型里用 ALiBi 吗?
如果你只需要处理数倍于训练长度的序列,而且想要零成本提升外推,ALiBi 是一个好选择。但若需要超长上下文,优先考虑 RoPE + 动态缩放或位置插值,再结合 ALiBi 的变体。
我的判断
从绝对位置编码到 RoPE,再到 ALiBi,一个核心趋势是:位置信息从“没法解释的向量”逐渐变成“可以直接计算的先验”。ALiBi 用最简单的数学做了这件事,它的局限同样来自这份简单。就像一把没有刻度的尺子,它能量出距离,但量不出你遇到的是不是直线上的点。我的看法是:在长上下文引擎日益复杂的今天,ALiBi 作为底层距离惩罚仍然有价值,尤其适合与动态调整结合。论文里的实验细节很值得一读,原始论文在这里。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/621.html