AI 工程基础体系 · 第 72/100 篇。内容覆盖机器学习、深度学习与生成式 AI;模型、数据、评测、权限和成本会作为同一生产系统处理。
Transformer 位置编码:绝对位置、RoPE、ALiBi 与长度外推
Transformer 的自注意力机制本身不理解“第几个 token”。如果把输入序列中的 token 任意打乱,只要 token 集合和注意力计算方式不变,标准自注意力并不会自动知道原始顺序。
位置编码(positional encoding)解决的正是这个问题:向模型提供 token 在序列中的位置信息,使模型能够区分:
“猫 追 老鼠”
“老鼠 追 猫”
这两个序列包含相同的 token,但位置关系不同,语义也不同。
本文重点分析四类相关机制:
- 绝对位置编码:每个位置拥有独立的位置表示;
- RoPE(Rotary Position Embedding,旋转位置编码):把位置信息注入 Query 和 Key 的旋转;
- ALiBi(Attention with Linear Biases):直接对注意力分数施加与距离相关的线性偏置;
- 长度外推:模型在训练长度之外处理更长序列时,位置机制为什么会失败,以及如何诊断和改进。
1. 自注意力为什么需要位置编码
1.1 标准自注意力的输入与输出
设输入序列有 个 token,每个 token 的隐藏向量维度为 :
自注意力首先通过三个线性变换得到:
其中:
- :Query,表示当前位置要查询什么;
- :Key,表示每个位置具有什么可被匹配的特征;
- :Value,表示真正被聚合的内容;
- :可训练参数。
缩放点积注意力为:
其中 是 Key 的维度。
第 个 Query 对第 个 Key 的原始注意力分数为:
这个分数只取决于 和 的内容,不包含 和 的位置。
1.2 自注意力的置换等变性
设 是一个对序列行进行置换的矩阵。将输入打乱为:
则:
因此:
经过 softmax 后,输出也只是按相同方式重新排列:
这说明标准自注意力对输入顺序是“置换等变”的:它可以处理内容,但不会自行产生顺序概念。
位置编码需要破坏这种完全的置换对称性,使模型能够区分绝对位置或相对位置。
2. 位置编码注入的两个基本位置
位置机制通常在两处之一发挥作用。
2.1 加到 token 表示上
绝对位置编码通常使用:
其中:
- 是第 个 token 的嵌入;
- 是第 个位置的位置向量;
- 是 Transformer 第一层接收的输入。
之后:
由于位置已经进入 ,它会间接影响 Query、Key 和 Value。
2.2 直接修改注意力分数
相对位置方法通常写成:
其中 是位置相关的偏置。
ALiBi 就属于这一类;某些相对位置偏置方法也采用类似形式。
两种方式的差异是:
- 加法位置编码修改 token 的表示,位置影响会经过线性层传播;
- 注意力偏置直接改变“当前位置关注另一个位置的倾向”。
3. 绝对位置编码
绝对位置编码为每个位置 生成一个向量 。它回答的是:
当前 token 位于整个序列的第几个位置?
绝对位置编码主要有两种形式:
- 可学习的绝对位置嵌入;
- 固定的正弦—余弦位置编码。
3.1 可学习的绝对位置嵌入
设最大支持长度为 ,隐藏维度为 ,可以定义一个位置嵌入矩阵:
第 个位置直接取:
然后:
位置嵌入矩阵和 token embedding 一样,通过训练学习。
示例
假设:
token embedding:
e_0 = [0.2, 0.1]
e_1 = [0.7, 0.3]
position embedding:
p_0 = [0.0, 0.4]
p_1 = [0.5, 0.1]
那么输入 Transformer 的表示为:
h_0 = e_0 + p_0 = [0.2, 0.5]
h_1 = e_1 + p_1 = [1.2, 0.4]
位置 和位置 即使出现相同 token,也会因为 与 不同而产生不同表示。
优点
可学习绝对位置的优点是表达能力直接:
- 模型可以为不同位置学习不同模式;
- 不需要人为规定位置向量的函数形式;
- 在训练长度范围内通常容易优化。
边界
最大问题是位置表有固定大小。
如果训练时:
而推理时出现:
则 根本不存在。工程实现通常会直接报错,或者必须扩展位置表;但新增位置没有经过训练,不能视为可靠的长度外推。
需要区分两种情况:
- 扩展位置表:只是增加参数存储空间;
- 学会更长位置上的行为:需要额外训练或微调。
前者不自动带来后者。
3.2 固定正弦—余弦位置编码
《Attention Is All You Need》中使用了固定的正弦—余弦位置编码。对位置 和维度索引 ,定义:
其中:
- 是位置;
- 是隐藏维度;
- 是偶数维;
- 是奇数维。
每一对维度使用一个不同频率的正弦和余弦。
具体算例
令 ,位置 。
对于第 维:
对于第 维:
所以近似为:
PE(1) = [0.8415, 0.5403, 0.0100, 0.99995]
位置 则为:
PE(0) = [0, 1, 0, 1]
模型输入仍然是:
为什么使用多种频率
低维度变化快,高维度变化慢:
- 高频维度能表示相邻位置的细粒度差异;
- 低频维度能表示较长距离上的位置趋势;
- 多个频率组合后,不同位置通常具有不同编码。
这种构造还具有一个重要性质:位置平移可以通过线性变换表达。
对任意频率 ,有:
因此,给定位置 的正弦—余弦向量,理论上可以通过一个与 有关的线性变换得到位置 的向量。
这解释了它为什么比固定大小的学习位置表更容易生成训练长度之外的位置编码。
但这并不等价于“模型一定能很好地外推”。位置编码可计算,只代表输入可构造;模型是否学会在更长距离上使用这些模式,仍取决于训练分布、注意力结构和数值稳定性。
3.3 绝对位置编码的相对位置信息问题
虽然正弦编码包含位置 ,但 Transformer 接收的是:
注意力分数展开后为:
其中只有最后一项明确只由位置决定。中间两项混合了内容和位置。
这意味着,绝对位置编码需要模型自己学习如何从绝对位置表示中推导:
- 相对距离;
- 前后关系;
- 是否相邻;
- 是否位于局部窗口;
- 某种固定结构是否重复出现。
这不是不可能,但学习路径不如直接把相对位置关系放进注意力分数那么明确。
4. RoPE:旋转位置编码
RoPE 的核心思想是:
不把位置向量简单加到 token 表示上,而是按照位置对 Query 和 Key 的每个二维子空间进行旋转。
它仍然使用绝对位置 生成旋转角度,但 Query 与 Key 的内积会自然呈现相对位置关系。
4.1 二维旋转
二维向量:
旋转角度 后:
RoPE 将 维向量分成 个二维子空间,每个子空间使用不同角频率:
常见频率为:
对位置 的 Query 和 Key 分别进行旋转:
这里 是两个 token 的位置。
4.2 RoPE 为什么产生相对位置关系
考虑同一个二维子空间:
由于旋转矩阵是正交矩阵:
因此:
关键结果是:
只通过旋转差 关联两个位置,而不是独立依赖 和 。
因此,RoPE 中 Query 在位置 与 Key 在位置 的内积具有相对位置结构。
这并不表示 RoPE 完全没有绝对位置信息。旋转角度本身仍然由绝对位置计算;更准确的说法是:
RoPE 以绝对位置为输入,但通过 Query/Key 内积把位置关系转化为相对位移相关的形式。
4.3 RoPE 的数值算例
先只看一个二维子空间,令:
q = [1, 0]
k = [1, 0]
令角频率 。
当 Query 位于位置 ,Key 位于位置 时:
两者内积为:
如果把两者同时向后移动 10 个位置,变为 ,则:
结果保持不变。
这正是相对位置特性的最小示例:只要位置差相同,旋转后的内积在这个二维子空间中保持相同。
4.4 RoPE 的实际实现
以下代码展示一个简化版 RoPE。它假设:
- 输入形状为
[batch, heads, seq_len, head_dim]; head_dim必须是偶数;- 使用偶数维和奇数维组成二维旋转平面;
position_ids从 0 开始。
import torch
def apply_rope(x, position_ids, base=10000.0):
"""
x: [batch, heads, seq_len, head_dim]
position_ids: [batch, seq_len]
"""
batch, heads, seq_len, head_dim = x.shape
if head_dim % 2 != 0:
raise ValueError("head_dim 必须是偶数")
if position_ids.shape != (batch, seq_len):
raise ValueError("position_ids 形状必须为 [batch, seq_len]")
half_dim = head_dim // 2
device = x.device
dtype = x.dtype
# inv_freq[i] = base^(-2i / head_dim)
inv_freq = 1.0 / (
base ** (torch.arange(0, half_dim, device=device).float() * 2 / head_dim)
)
# [batch, seq_len, half_dim]
angles = position_ids.to(torch.float32).unsqueeze(-1) * inv_freq
cos = torch.cos(angles).to(dtype)
sin = torch.sin(angles).to(dtype)
# 将最后一维拆成偶数维与奇数维
x_even = x[..., 0::2]
x_odd = x[..., 1::2]
# [batch, 1, seq_len, half_dim]
cos = cos.unsqueeze(1)
sin = sin.unsqueeze(1)
rotated_even = x_even * cos - x_odd * sin
rotated_odd = x_even * sin + x_odd * cos
out = torch.empty_like(x)
out[..., 0::2] = rotated_even
out[..., 1::2] = rotated_odd
return out
# 可运行示例
x = torch.tensor(
[[[
[1.0, 0.0, 0.0, 1.0],
[0.0, 1.0, 1.0, 0.0],
[1.0, 1.0, 0.5, -0.5],
]]]
)
position_ids = torch.tensor([[0, 1, 2]])
y = apply_rope(x, position_ids)
print(y.shape)
print(y)
预期输出的形状为:
torch.Size([1, 1, 3, 4])
这段代码只完成旋转,不包含注意力、因果 Mask 或 KV Cache。真实模型还需要保证以下数据流一致:
hidden states
├──> Q projection ──> RoPE ──> attention score
├──> K projection ──> RoPE ──> attention score
└──> V projection ────────────> weighted sum
RoPE 通常只应用于 Query 和 Key,不应用于 Value。因为位置关系应当影响“如何匹配和聚合”,而不是直接旋转被传递的内容表示。
4.5 RoPE 与因果注意力
自回归语言模型在位置 只能查看位置 的 token。因果 Mask 定义为:
注意力分数变为:
RoPE 解决位置关系,因果 Mask 解决信息泄漏;两者是不同机制,不能相互替代。
一个常见错误是认为“使用 RoPE 后模型自然知道不能看未来”。这是错误的。RoPE 不限制可见范围,若没有因果 Mask,当前位置仍可能读取未来 token。
4.6 RoPE 与 KV Cache
在增量解码中,模型每次只处理新 token:
- 计算当前 token 的 Query;
- 计算当前 token 的 Key 和 Value;
- 将新的 Key、Value 追加到缓存;
- 当前 Query 与全部历史 Key 做注意力。
若当前 token 的位置为 ,则:
历史 Key 在生成时已经按各自位置旋转:
因此缓存中的 Key 不能简单地重复使用同一个位置旋转。实现必须确保:
- 首次预填充阶段的
position_ids正确; - 增量阶段新 token 的位置从历史长度继续增长;
- padding、左填充和批处理时每个样本的位置编号没有混淆;
- 重新排序 beam 或 batch 时,缓存与位置状态同步移动。
如果位置编号错误,模型通常仍能生成文本,但会出现长文本逻辑错乱、重复、突然退化等不容易立即定位的故障。
5. ALiBi:线性注意力偏置
ALiBi 不向 token embedding 中加入位置向量,也不旋转 Query 和 Key,而是在注意力分数上直接加入距离惩罚。
对第 个注意力头:
在因果注意力中通常只考虑 ,因此 。
其中:
- :Query 的位置;
- :Key 的位置;
- :Key 相对当前 Query 的距离;
- :第 个头的斜率;
- 距离越远,偏置越负。
它表达了一个先验:
在内容匹配分数相同的情况下,注意力头更倾向于关注距离当前更近的位置。
5.1 ALiBi 的数值算例
假设某个头的内容分数为:
content score = [1.0, 0.9, 0.8]
当前 Query 位于位置 ,三个 Key 位于 。
令斜率:
距离分别为:
i - j = [2, 1, 0]
线性偏置为:
[-0.4, -0.2, 0.0]
加上偏置后的分数:
[0.6, 0.7, 0.8]
因此,即使位置 0 的内容分数最高,它因为距离更远,最终分数反而低于位置 2。
softmax 后,注意力权重约为:
[0.269, 0.299, 0.332]
如果没有 ALiBi,原始分数 [1.0, 0.9, 0.8] 的 softmax 约为:
[0.367, 0.332, 0.301]
ALiBi 将权重向近邻位置移动,但不会阻止模型关注远处位置。远处内容分数足够高时,仍然可以胜过距离惩罚。
5.2 多头斜率
ALiBi 通常为不同注意力头设置不同斜率:
有的头使用较大斜率,更强地偏好局部上下文;有的头使用较小斜率,更容易关注长距离上下文。
原始 ALiBi 论文使用一种按头数生成斜率的规则。工程实现中不应只根据“看起来相似”自行替换斜率公式,因为斜率分布会改变模型的注意力归纳偏置。
5.3 ALiBi 的实现
import torch
def make_alibi_bias(
num_heads,
seq_len,
slopes,
device=None,
dtype=torch.float32,
):
"""
返回 [1, num_heads, seq_len, seq_len] 的 ALiBi 偏置。
这里只构造距离偏置,不包含因果 Mask。
"""
if len(slopes) != num_heads:
raise ValueError("slopes 的数量必须等于 num_heads")
positions = torch.arange(seq_len, device=device)
# distance[i, j] = i - j
distance = positions[:, None] - positions[None, :]
slopes = torch.tensor(slopes, device=device, dtype=dtype)
bias = -slopes[None, :, None, None] * distance[None, None, :, :]
return bias
num_heads = 2
seq_len = 4
bias = make_alibi_bias(
num_heads=num_heads,
seq_len=seq_len,
slopes=[0.1, 0.4],
)
# 因果 Mask:未来位置不可见
causal_mask = torch.triu(
torch.ones(seq_len, seq_len, dtype=torch.bool),
diagonal=1,
)
scores = torch.zeros(1, num_heads, seq_len, seq_len)
scores = scores + bias
scores = scores.masked_fill(causal_mask[None, None], float("-inf"))
print(scores[0, 0])
print(scores[0, 1])
第一个头和第二个头的距离惩罚不同。第二个头的斜率为 0.4,因此对远距离位置的惩罚更强。
这里有一个实现细节:如果直接对未来位置使用 -m(i-j),由于 ,未来位置会得到正偏置。因此必须同时使用因果 Mask;ALiBi 本身不是访问权限控制机制。
6. 四种机制如何比较
6.1 绝对位置嵌入
token embedding + position embedding
特点:
- 位置直接进入隐藏状态;
- 可以是可学习表,也可以是固定函数;
- 可学习表通常受最大训练长度限制;
- 相对距离需要模型间接推导;
- 在训练长度范围内通常行为直观。
6.2 正弦—余弦位置编码
token embedding + deterministic position function
特点:
- 不需要为每个位置保存可学习参数;
- 可以计算更大的位置编号;
- 具有周期结构和多频率结构;
- 可计算不等于一定能可靠外推;
- 现代大语言模型中并非最常见的主流方案,但它是理解位置编码的基础。
6.3 RoPE
Q/K projection -> rotate by position -> attention
特点:
- 不增加独立的位置 embedding 加法;
- Query-Key 内积自然包含相对位置差;
- 适合自回归 Transformer 和 KV Cache;
- 长度外推会受到旋转相位、频率和训练分布影响;
- 现代 decoder-only 模型中使用非常广泛。
6.4 ALiBi
attention score + distance-dependent linear bias
特点:
- 不需要位置 embedding 表;
- 直接对距离施加线性先验;
- 长度外推结构简单;
- 近邻偏好可能影响需要精确长距离检索的任务;
- 斜率设计与训练过程是模型行为的一部分,不能随意改动。
7. 什么是长度外推
长度外推指:
模型训练时只见过长度不超过 的序列,但推理时处理长度 的序列。
必须区分三个概念:
7.1 最大可接受长度
模型代码是否允许输入更长的 input_ids。这通常由以下因素限制:
- 位置 embedding 表大小;
- RoPE 的缓存角度表;
- 模型配置中的最大位置字段;
- CUDA kernel 或显存限制;
- KV Cache 容量。
7.2 可计算长度
系统能否在数学和内存层面算出结果。例如正弦位置编码可以为新位置计算数值,RoPE 也可以生成更大的角度。
7.3 可靠长度
模型在更长输入上是否仍然保持可接受的:
- 语言质量;
- 长距离检索能力;
- 位置敏感任务准确率;
- 格式遵循能力;
- 重复率和幻觉率。
可计算长度不等于可靠长度。
8. 为什么绝对位置容易在长序列上失败
8.1 学习位置表的硬边界
对可学习绝对位置嵌入:
当推理长度超过训练长度时,位置 没有对应向量。
直接将表扩展为:
并不会让新增行具有正确语义。随机初始化的新位置可能导致:
- 注意力分布异常;
- 新增区域输出退化;
- 位置边界处突然出现质量断层;
- 模型过度依赖训练过的位置模式。
8.2 固定正弦编码的边界更软但并非没有边界
固定正弦编码可以计算任意 ,但模型训练时只观察了有限范围内的相位组合。
更长位置可能产生:
- 未训练过的相位组合;
- 高频维度的快速振荡;
- 不同远位置在部分频率上出现相似相位;
- 模型对距离的判断与训练分布不一致。
因此,固定函数解决的是“位置向量如何生成”,不是“神经网络如何泛化到新长度”。
9. RoPE 的长度外推边界
RoPE 的每个二维子空间都有角度:
位置差为 时,相关项依赖:
当 变大时,相位会持续旋转。由于正弦和余弦是周期函数,某些远距离位置可能在部分频率上呈现相似相位。
9.1 相位增长
若某个频率为 ,当位置从 增加到 时,角度变化为:
训练长度内模型学习的是某个相位范围。推理长度扩大后,模型可能进入训练中未充分覆盖的区域。
9.2 高频与低频的作用不同
- 高频维度对局部位置差敏感,但相位变化快;
- 低频维度变化慢,能承载更长距离趋势;
- 长度外推需要在局部精度和长距离范围之间平衡。
这也是许多 RoPE 长度扩展方法要调整频率、缩放位置或重新训练的原因。
9.3 RoPE scaling 不是统一 API
不同模型和不同版本的 Transformers 可能支持不同的 RoPE 扩展配置,例如线性缩放、动态缩放或其他变体。但这些能力具有版本和模型架构依赖:
- 配置字段名称可能变化;
- 支持的
rope_type可能不同; - 某种缩放只适用于特定模型实现;
- 修改配置后可能需要重新校准或微调;
- 不能把任意模型的 RoPE 参数直接复制到另一个模型。
因此,使用 Hugging Face Transformers 时,应以具体模型的配置、模型类实现和当前版本文档为准,而不是假设所有模型都接受相同参数。
10. ALiBi 为什么通常更容易做长度外推
ALiBi 的位置偏置是:
它不需要查表,也不需要把位置映射到训练过的 embedding。只要 和 是整数,偏置就能继续计算。
从函数形式看,ALiBi 对距离的定义没有固定最大长度:
因此其长度外推边界比可学习绝对位置表更自然。
但这仍然不是无条件保证。
10.1 线性偏置可能过度惩罚远距离
当距离很大时:
会变得非常负。若内容分数没有足够大的优势,远距离 token 的注意力权重会接近零。
这对以下任务可能不利:
- 长距离指代;
- 文档前部定义在后部被引用;
- 跨段落约束;
- 长上下文代码依赖;
- 需要精确检索远处证据的任务。
10.2 训练分布仍然重要
即使偏置函数可以继续计算,模型参数仍然是在有限长度、有限距离分布上训练的。模型可能没有学会:
- 如何组合多个远距离证据;
- 如何在很长序列中定位相关片段;
- 如何保持跨越大量 token 的状态;
- 如何在多个候选长距离位置之间选择。
因此,ALiBi 的“可外推结构”比绝对位置表更好,并不等价于所有任务上的长上下文性能都更好。
11. 位置机制与注意力模式的关系
位置编码不是单独存在的,它会改变注意力矩阵:
其中:
- 是内容相关分数;
- 可以包含因果 Mask 和位置偏置;
- 是最终注意力权重。
不同位置机制改变的是 或额外偏置的构造方式:
| 机制 | 位置进入哪里 | 主要归纳偏置 |
|---|---|---|
| 可学习绝对位置 | 输入隐藏状态 | 每个绝对位置拥有独立表示 |
| 正弦—余弦 | 输入隐藏状态 | 多频率、可计算的位置函数 |
| RoPE | Query 和 Key | 内积中体现相对旋转差 |
| ALiBi | 注意力分数 | 距离越远,分数线性下降 |
这张表不能替代理论差异。真正影响模型行为的是位置机制与内容分数、Mask、层数、头数和训练任务的共同作用。
12. 一个最小的注意力实现
下面的代码将内容分数、RoPE 和因果 Mask 放在同一个流程中,展示位置机制在数据流中的真实位置。
import math
import torch
import torch.nn.functional as F
def causal_attention_with_rope(q, k, v, position_ids):
"""
q, k, v: [batch, heads, seq_len, head_dim]
position_ids: [batch, seq_len]
"""
q = apply_rope(q, position_ids)
k = apply_rope(k, position_ids)
head_dim = q.shape[-1]
scores = torch.matmul(q, k.transpose(-1, -2)) / math.sqrt(head_dim)
seq_len = q.shape[-2]
future = torch.triu(
torch.ones(seq_len, seq_len, device=q.device, dtype=torch.bool),
diagonal=1,
)
scores = scores.masked_fill(future[None, None], float("-inf"))
weights = F.softmax(scores, dim=-1)
output = torch.matmul(weights, v)
return output, weights
batch = 1
heads = 2
seq_len = 4
head_dim = 4
torch.manual_seed(0)
q = torch.randn(batch, heads, seq_len, head_dim)
k = torch.randn(batch, heads, seq_len, head_dim)
v = torch.randn(batch, heads, seq_len, head_dim)
position_ids = torch.arange(seq_len).unsqueeze(0)
output, weights = causal_attention_with_rope(q, k, v, position_ids)
print("output shape:", output.shape)
print("weights shape:", weights.shape)
print("future attention weights:",
weights[0, 0][torch.triu(torch.ones(seq_len, seq_len, dtype=torch.bool), diagonal=1)])
预期输出中:
output shape: torch.Size([1, 2, 4, 4])
weights shape: torch.Size([1, 2, 4, 4])
未来位置的注意力权重应为零。由于 softmax 输入中未来位置被设置为负无穷,它们不会参与归一化。
这个例子同时说明:
- RoPE 只修改 ;
- Value 不旋转;
- 因果 Mask 仍然必须存在;
- 注意力输出形状由 batch、head、序列长度和 head dimension 决定。
13. Padding、位置编号与批处理陷阱
位置编码的正确性不仅取决于公式,还取决于 position_ids 的构造。
13.1 右填充
对于右填充:
真实 token: A B C
padding: <pad> <pad>
通常真实 token 的位置仍是:
0 1 2
padding 位置应通过 attention mask 排除。
13.2 左填充
对于左填充:
<pad> <pad> A B C
有两种常见策略:
- 让真实 token 使用位置
0,1,2; - 让整个张量列位置使用
0,1,2,3,4,再依赖 Mask。
对使用 RoPE 的自回归模型,具体策略必须与模型训练和实现保持一致。不能因为“张量第几列”而随意决定逻辑位置。
13.3 增量解码
假设历史 KV Cache 中已有长度 ,新 token 的位置通常应从 开始:
历史位置: 0, 1, ..., T-1
新位置: T
如果错误地把新 token 的位置重新设置为 0,RoPE 会把它当作序列开头旋转,导致 Query/Key 的相对关系错误。
13.4 诊断方法
出现位置相关故障时,可以记录:
input_ids.shape
attention_mask
position_ids
past_key_values 中每层 K/V 的序列长度
当前 decode position
重点检查:
position_ids是否单调增长;- padding 部分是否被屏蔽;
- batch 内不同样本的位置是否被错误共享;
- KV Cache 重排后位置状态是否匹配;
- 训练和推理是否使用了相同的起始位置约定。
14. 长度外推的失败表现
长度外推失败不一定表现为程序报错。更常见的是“能运行但质量下降”。
14.1 位置边界断层
在可学习绝对位置表扩展后,接近原训练长度的位置开始出现明显退化:
长度 1k:正常
长度 2k:正常
长度 4k:在后半段重复或失去格式
这通常说明新增位置没有得到充分训练,而不是 tokenizer 突然失效。
14.2 长文本重复
模型可能在较长输出中反复生成:
同一段落
同一个列表项
相同的函数体
可能原因包括:
- 位置相位进入未训练区域;
- 远距离信息无法稳定利用;
- KV Cache 的位置编号错误;
- 生成长度超过模型真实上下文窗口;
- 注意力或采样实现存在数值问题。
14.3 “中间信息丢失”
长上下文任务中,模型能够利用开头和结尾,却忽略中间信息。这种现象未必只由位置编码导致,还可能与:
- 注意力竞争;
- 训练样本结构;
- 特殊 token 分布;
- 上下文压缩能力;
- 检索和推理任务难度
有关。因此不能仅凭一个长上下文失败案例断言“RoPE 外推失败”。
14.4 因果 Mask 错误
如果未来位置没有被屏蔽,离线评测可能看起来异常优秀,因为模型读取了答案或未来上下文。生产生成时则会表现不同。
检查注意力矩阵时,应确认:
attention[i, j] = 0 for j > i
而不是只检查输出文本。
15. 长度外推的评测方法
只测试“能否输入更长 token”是不够的。至少需要区分以下测试。
15.1 语言建模损失
在长度分别为:
L_train / 2
L_train
2 * L_train
4 * L_train
的样本上测量 loss,观察是否出现随位置增长的明显退化。
但平均 loss 可能掩盖局部位置问题,因此还应按 token 位置分桶:
如果后半段 loss 明显升高,说明存在位置或上下文利用问题。
15.2 针对性长距离检索
构造一个文档,在不同位置放入唯一事实:
用户编号:A17
...
大量干扰文本
...
请回答用户编号。
改变事实所在位置,比较模型准确率。
这可以区分:
- 模型是否能看到远处内容;
- 模型是否能在远处内容与当前问题之间建立关联;
- 位置偏置是否过度偏好局部 token。
15.3 相对距离测试
保持 token 内容不变,只改变目标与查询之间的距离:
距离 32
距离 128
距离 512
距离 2048
这比只扩大总序列长度更能直接测量位置机制对距离的处理能力。
15.4 位置置换反例
构造两个只改变顺序的输入:
A B C
C B A
如果模型输出完全不区分,位置注入可能没有正确生效;如果使用了错误的 position_ids,也可能出现异常相似结果。
16. 常见误解
16.1 “有了位置编码,模型就理解语法顺序”
位置编码只提供位置信号。模型还需要通过训练学习:
- 位置关系如何对应语法;
- 哪些距离重要;
- 哪些 token 之间存在依赖;
- 如何把位置与内容结合。
位置编码不是语法规则本身。
16.2 “RoPE 就是相对位置编码”
严格说,RoPE 通过绝对位置旋转 Query 和 Key,但内积具有相对位置结构。称其为“具有相对位置性质的旋转编码”比简单说“纯相对位置编码”更准确。
16.3 “ALiBi 只允许模型看附近内容”
不正确。ALiBi 只是降低远距离位置的分数:
如果远处内容的语义匹配分数足够高,它仍然可以获得较大的注意力权重。真正禁止访问未来的是因果 Mask。
16.4 “把最大长度配置改大就完成了长上下文支持”
配置修改可能只解决:
- 输入检查;
- 缓存表大小;
- 张量形状限制。
它不保证:
- 位置机制适配;
- 模型已经训练过新位置;
- 显存足够;
- 长距离能力保持;
- 评测指标不下降。
16.5 “上下文窗口越长,模型就越能使用全部上下文”
注意力计算允许访问不等于模型有效利用。长文本还受到:
- 注意力复杂度;
- KV Cache 显存;
- 训练数据长度;
- 信息密度;
- 位置偏置;
- 任务结构
的共同限制。
17. 复杂度与生产成本
位置编码通常不是 Transformer 长上下文成本的主要来源,真正昂贵的是注意力矩阵。
对序列长度 ,标准全注意力需要构造:
因此时间和中间显存通常随 增长。
自回归生成时,使用 KV Cache 后,每一步的新 Query 仍需要与历史 Key 比较。单步计算大致随历史长度线性增长,完整生成过程的累计注意力计算仍会随着上下文变长而明显增加。
位置机制还会影响额外成本:
- 可学习绝对位置:需要位置 embedding 表;
- 正弦位置:需要生成或缓存正余弦值;
- RoPE:需要生成每个位置、每个旋转维度的
cos/sin; - ALiBi:需要构造或融合距离偏置。
这些成本通常低于 ,但在高吞吐服务中仍要注意:
- 是否重复生成相同位置的旋转缓存;
- 是否为 padding 位置浪费计算;
- 是否把完整长度的 ALiBi bias 物化到显存;
- 是否与 FlashAttention 等 kernel 的 bias 接口兼容;
- 是否在 KV Cache 扩展时产生额外内存拷贝。
生产系统中的“支持长度”应同时记录:
模型训练长度
模型配置长度
推理框架允许长度
单请求可用长度
batch 条件下可用长度
经过评测的可靠长度
这些值可能不同。
18. 在 Hugging Face Transformers 中如何确认位置机制
Hugging Face Transformers 为大量模型提供统一加载接口,但“位置编码类型”和“长度配置”仍然是模型架构相关的。
一个基本检查示例:
from transformers import AutoConfig
model_id = "your-model-id"
config = AutoConfig.from_pretrained(model_id)
print("model_type:", getattr(config, "model_type", None))
print("max_position_embeddings:",
getattr(config, "max_position_embeddings", None))
print("rope_theta:", getattr(config, "rope_theta", None))
print("rope_scaling:", getattr(config, "rope_scaling", None))
这里的 getattr 是为了避免不同模型配置缺少某些字段。输出只用于检查,不能据此断言所有模型都支持相同的扩展方式。
进一步确认时应检查:
- 模型类是否实现 RoPE;
- 配置是否含有对应的 RoPE 参数;
- tokenizer 是否引入额外特殊 token;
- attention mask 和 position ids 如何在模型中生成;
- 当前 Transformers 版本是否支持该模型的长度扩展配置;
- 模型卡或训练说明中的上下文长度是多少。
如果使用自定义模型代码或 trust_remote_code=True,还需要把远程代码作为生产依赖审查,因为位置实现可能不完全遵循标准模型类。
19. 如何选择位置机制
不能脱离训练过程单独选择位置编码。一个位置机制至少要与以下内容一起确定:
- 模型架构;
- 训练最大长度;
- 目标推理长度;
- 是否使用 KV Cache;
- 是否需要长距离检索;
- 是否计划进行长上下文微调;
- 推理 kernel 是否支持对应的 bias 或旋转;
- 显存和吞吐预算。
一般可以这样理解:
可学习绝对位置
适合:
- 明确固定长度;
- 训练和部署长度接近;
- 希望位置参数直接学习。
风险:
- 超过位置表边界;
- 新增位置未训练;
- 对长度外推不友好。
正弦—余弦位置编码
适合:
- 需要固定、可计算的位置函数;
- 教学和基础 Transformer 实现;
- 不希望为每个位置保存独立参数。
风险:
- 可生成新位置不代表模型能泛化;
- 周期和频率设计仍会影响长距离行为。
RoPE
适合:
- decoder-only 自回归模型;
- 希望注意力内积包含相对位置信息;
- 使用 KV Cache;
- 采用现有主流 LLM 架构。
风险:
- 长度外推需要验证;
- 旋转频率和 scaling 配置不能随意修改;
- position ids 错误会直接破坏缓存推理。
ALiBi
适合:
- 希望用简单的距离先验;
- 需要避免固定位置 embedding 表;
- 任务对局部性有合理需求。
风险:
- 远距离内容会受到线性惩罚;
- 斜率设计影响不同头的注意力范围;
- 不能仅凭“可计算任意长度”推断长距离能力。
20. 一个可靠的排查顺序
当模型在更长上下文上退化时,可以按以下因果链排查:
第一步:确认是否超过硬限制
检查:
输入长度
模型配置长度
tokenizer 截断设置
推理框架限制
KV Cache 容量
如果输入在 tokenizer 阶段已经被截断,后续位置分析没有意义。
第二步:确认位置编号
打印或断言:
assert position_ids.shape == attention_mask.shape
对于无 padding 的单序列,可以检查:
expected = torch.arange(position_ids.shape[-1], device=position_ids.device)
assert torch.equal(position_ids[0], expected)
对于 padding 和 batch,需要按照模型约定检查真实 token 的逻辑位置。
第三步:确认因果 Mask
检查未来区域是否为负无穷,softmax 后是否为零:
future_weights = weights[..., torch.triu(
torch.ones(seq_len, seq_len, dtype=torch.bool),
diagonal=1
)]
assert torch.allclose(
future_weights,
torch.zeros_like(future_weights),
atol=1e-6,
)
第四步:比较位置机制本身
在相同输入、相同模型权重和相同长度下,对比:
- 训练长度内;
- 刚超过训练长度;
- 大幅超过训练长度。
如果只在边界后出现突变,优先怀疑位置表或位置缩放;如果所有长度都退化,可能是 Mask、缓存或模型加载问题。
第五步:分离长度问题和注意力问题
使用短文本重复填充,观察:
- 仅扩大长度但不增加信息时是否重复;
- 将关键信息放在不同位置时准确率如何;
- 去掉 KV Cache 后结果是否变化;
- 单 batch 与多 batch 结果是否一致。
如果关闭 KV Cache 后正常、开启后异常,通常应优先检查增量位置和缓存拼接。
结语
绝对位置编码、RoPE 和 ALiBi 解决的是同一个基础问题,但注入位置和归纳偏置不同:
- 绝对位置编码把“我在第几个位置”加入 token 表示;
- 正弦—余弦位置编码用固定多频率函数表达位置;
- RoPE 旋转 Query 和 Key,使内积自然携带相对位移信息;
- ALiBi 直接根据距离调整注意力分数。
长度外推的关键也不只是“能不能计算位置向量”。真正需要验证的是:
可学习绝对位置通常有明确的长度边界;正弦位置可以继续生成但不保证泛化;RoPE 的外推受到相位和频率分布影响;ALiBi 的函数形式更容易延伸,但线性距离惩罚可能削弱远距离依赖。
在生产系统中,位置编码应和注意力 Mask、position_ids、KV Cache、模型配置、推理框架以及长上下文评测一起审查。只修改最大长度配置,而不验证这些状态和数据流,通常只能得到“程序接受了更长输入”,不能得到真正可靠的长度外推。
系列导航与关联阅读
- 系列入口:AI 工程完整学习路线:从机器学习与 Transformer 到 RAG、Agent 和生产治理
- 上一篇:Attention 数学细解:QKV、缩放点积、Mask、多头与复杂度
- 下一篇:Tokenizer 训练与诊断:BPE、Unigram、特殊符号和多语言覆盖
官方资料
本文依据研究论文、标准组织与主流框架官方文档重新梳理;正文、示例与工程清单由 WR BLOG 编写。

评论
0 条讨论