观文听傑

返回

上一篇说明了位置函数“怎样算”:RoPE 把每层每头的 Query、Key 按全局位置旋转,使点积显式依赖相对位移。但一个危险的推论是:既然 sincos 能为任意整数位置返回数值,把模型配置里的最大长度从 4K 改成 32K,模型就自然拥有 32K 上下文。

这混淆了名义上下文长度(Claimed Context Length)和有效上下文长度(Effective Context Length)。前者只说明接口与显存允许输入多长;后者才说明模型在该长度上仍能定位、提取并组合信息。本文聚焦一件事:RoPE 模型如何把训练外的位置映回较熟悉的相位范围,以及怎样证明扩窗真的有效。

01 “能放进去”为什么不等于“能用起来”?#

设预训练最大长度为 L0L_0,目标长度为 L1>L0L_1>L_0,每头宽度为偶数 dd。原始 RoPE 的第 ii 个二维频率为:

ωi=b2i/d,i=0,1,,d/21\omega_i=b^{-2i/d},\qquad i=0,1,\ldots,d/2-1

位置 pp 对应角度 pωip\omega_i。训练期间,模型只在相对位移 Δ=sp[(L01),L01]\Delta=s-p\in[-(L_0-1),L_0-1] 内见过这些相位怎样影响 Q–K 分数。直接输入更长序列时,代码仍能算出:

qpR(Δ)ksq_p^\top R(\Delta)k_s

ΔL0|\Delta|\ge L_0 的相位组合位于训练分布之外。神经网络在已见区间内拟合良好,并不约束它在区间外继续平滑。

预训练:位置 0 ───────────────────── L₀-1
                   模型见过的相位与距离

直接扩窗:位置 0 ───────────────────── L₀-1 ───────────── L₁-1
                                               未见相位区间

结果可能是:张量形状合法、无越界、无 OOM,但长距离注意力已失真。
text

所以至少要区分四层能力:

层级要回答的问题仅修改最大长度能否证明
接口容量tokenizer 与模型是否接受 L1L_1 个 token?
系统容量显存、prefill 延迟与吞吐是否可承受?不能
语言建模长度增加时损失/困惑度是否稳定?不能
信息利用能否在不同位置检索、推理与聚合?不能

02 直接外推究竟把什么送出了训练区间?#

把每对 Q/K 维度写成二维向量,RoPE 分数可以看成许多三角基函数的加权组合。内容投影学到的系数只在训练位移区间内受到损失约束。

content q_p [N,H,L,d] ─► R(p; ω) ─┐
                                    ├─ q̃k̃ᵀ / √d ─► logits [N,H,L,S]
content k_s [N,H,S,d] ─► R(s; ω) ─┘

位置部分:R(p)ᵀR(s) = R(s-p) = R(Δ)
训练只约束 |Δ| < L₀;直接扩窗要求模型在 |Δ| < L₁ 上外推。
text

高频维的角度变化快,善于区分局部位置,却会在长距离上经历很多次绕圈;低频维变化慢,覆盖更大的距离尺度。扩窗不是简单“多生成几行 sin/cos 表”,而是让所有频率共同进入未受训练约束的新组合。

03 位置插值怎样把长窗口压回旧范围?#

位置插值(Position Interpolation, PI)不再直接使用目标位置 pp,而是令:

α=L0L1=1s,p=αp\alpha=\frac{L_0}{L_1}=\frac{1}{s},\qquad p'=\alpha p

其中 s=L1/L0s=L_1/L_0 是扩展因子。于是目标位置区间 [0,L1)[0,L_1) 被压到 [0,L0)[0,L_0);相对位移也变成:

Δ=α(s2p2)=Δs\Delta' = \alpha(s_2-p_2)=\frac{\Delta}{s}
目标窗口 L₁: 0──1──2──3──4──5──6──7
                 │  除以扩展因子 s=2

旧相位范围 L₀:0─0.5─1─1.5─2─2.5─3─3.5
text

这里产生的是连续位置,不要求落在整数网格上。PI 的关键假设是:在旧区间内做插值,通常比把模型推向整个训练区间之外更稳定。它没有改变 Q/K 的形状,也没有增加可训练位置表。

04 用 4→8 token 手算“稳定”与“代价”#

L0=4L_0=4L1=8L_1=8,所以 s=2s=2。只看一个角频率 ω=1\omega=1 rad/token。

长序列中,位置 1 的 Key 与位置 7 的 Query 相距 6:

方案有效位移旋转角度
直接外推6666 rad
位置插值6/2=36/2=333 rad

插值把长距离重新放回训练见过的 Δ3|\Delta|\le3 范围。但相邻 token 的位移也从 1 压成了 0.5:

cos(1)0.540,qquadcos(0.5)0.878\cos(1)\approx0.540,qquad \cos(0.5)\approx0.878

若内容向量恰好使这一对维度的点积退化为 cos(Δω)\cos(\Delta\omega),相邻位置会显得更相似。PI 解决了“远距离外推”,却同时牺牲了部分“局部分辨率”。这正是后续频率缩放不应一刀切的原因。

05 缩位置与缩频率为什么等价?#

RoPE 角度只由位置与频率的乘积决定:

ps缩位置ωi=pωis缩频率\underbrace{\frac{p}{s}}_{\text{缩位置}}\omega_i =p\underbrace{\frac{\omega_i}{s}}_{\text{缩频率}}

所以线性 RoPE 缩放(Linear RoPE Scaling)可以保留整数 position_ids,改用:

ωi=ωis\omega_i'=\frac{\omega_i}{s}

这对 KV Cache 很方便:缓存逻辑位置仍是 0,1,2,...,只在生成 cos/sin 时使用缩放后的频率。

position_ids [L] ───────────────┐
                                ├─ outer product ─► angles [L,d/2]
base b ─► inv_freq [d/2] ─► / s ┘

                                      ├─ cos/sin ─► rotate Q [N,H,L,d]
                                      └─ cos/sin ─► rotate K [N,H,L,d]
text

注意:这里的 ss 是扩展因子,不是前文用作 Key 位置的下标。实现中应使用 factorkey_position 等清晰名称,避免同一个字母承担两种语义。

06 为什么非均匀频率缩放能保住更多局部信息?#

线性 PI 把每个 ωi\omega_i 都除以相同的 ss。更细的思路是:

  • 高频、短波维主要解析近邻,少缩放或不缩放;
  • 低频、长波维负责远距离,接近完整缩放;
  • 中间频率平滑过渡,避免频谱断层。

一种有代表性的基底缩放把 RoPE base 从 bb 改成:

b=bsd/(d2),d>2b'=b\cdot s^{d/(d-2)},\qquad d>2

于是:

ωi=(b)2i/d\omega_i'=(b')^{-2i/d}

i=0i=0 时,最高频 ω0=1\omega_0'=1 不变;到最低频附近,缩放量逐渐接近 1/s1/s。这常被称为 NTK-aware 一类缩放。它是一种频率分配策略,不是“神经切线核已给出质量证明”。

d=8d=8b=10000b=10000s=2s=2,原频率恰为 [1, 0.1, 0.01, 0.001]

维度对原频率线性 PI基底缩放(约)
010.51
10.10.050.079
20.010.0050.0063
30.0010.00050.0005

YaRN 在此方向上进一步按波长区间混合插值与外推,并调整注意力尺度。工程上应把 lineardynamicyarn 等视为不同的频率契约;不能只看到相同 factor 就认为生成的角度相同。

07 用 PyTorch 写出两种可检查的频率#

下面只生成 RoPE 的逆频率 inv_freq [d/2],不隐藏缩放发生在哪里:

这里使用的 torch.arangetorch.exp 与张量广播都是 PyTorch 2.13 当前稳定 API。频率用 float32 生成;长位置若直接用低精度计算角度,舍入误差会随位置放大。

08 把缩放频率接回 Q/K 旋转#

函数输入是 Q 或 K,而不是 token ids;标准 RoPE 仍发生在每层每头的 Q/K 投影之后。输出形状与输入相同,所以后续仍可交给 torch.nn.functional.scaled_dot_product_attention

09 当前 Transformers 配置表达了什么?#

Hugging Face Transformers 当前官方文档用 rope_parameters 声明 RoPE 变体。线性缩放示例为:

from transformers import LlamaConfig

config = LlamaConfig()
config.rope_parameters = {
    "rope_type": "linear",
    "rope_theta": 10000.0,
    "factor": 8.0,
}
python

官方当前列出的 rope_type 包含 defaultlineardynamicyarnlongropellama3;不同类型要求的键并不相同,缺失参数会报错。部分混合注意力模型还可按 layer type 分别配置。

10 扩窗微调时,数据流怎样变化?#

PI 类方法通常从已有 checkpoint 继续训练,而不是只在推理时改配置:

长文档 ─► tokenize / pack ─► input_ids [N,L₁]

                              ├─ position_ids [N,L₁]
                              │       │
                              │       └─ RoPE scaling ─► Q̃,K̃

                              └─ causal LM labels [N,L₁]


                                next-token loss [N,L₁]


                                   更新原 checkpoint
text

核心训练契约包括:

  1. 训练和部署使用完全相同的 rope_typefactorrope_theta 与原始最大长度。
  2. 长样本必须覆盖所需距离;只把许多短样本拼成一批,不会自动产生跨片段监督。
  3. 若 packed sequence 不允许片段互相注意,attention mask 与 position reset 必须一致。
  4. 除长序列外保留一部分短序列,监测扩窗是否损害原窗口质量。
  5. 自注意力的计算和 logits 存储通常随 L2L^2 增长;位置缩放只处理位置分布,不消除计算代价。

伪代码可以写成:

for batch in mixture(short_sequences, long_sequences):
    ids, valid, segment = pack(batch, target_length=L₁)
    pos = build_positions(valid, segment_policy)
    logits = model(ids, position_ids=pos, attention_mask=valid)
    loss = next_token_loss(logits, ids, valid, segment)
    backward(loss)
    update_parameters()
text

11 KV Cache 为什么也属于缩放契约?#

prefill 与逐 token decode 必须对同一全局位置生成同一角度:

# cache.key 已保存按各自位置旋转后的历史 Key。
past_length = 0 if cache is None else cache.key.size(-2)
new_positions = torch.arange(
    past_length,
    past_length + new_q.size(-2),
    device=new_q.device,
)

rotated_q = apply_scaled_rope(
    new_q, new_positions, factor=8.0, scaling="linear"
)
rotated_k = apply_scaled_rope(
    new_k, new_positions, factor=8.0, scaling="linear"
)
python

若运行中更改 factorbase 或缩放类型,历史 Key 与新 Query 会处在不同坐标系,旧 cache 必须失效。动态缩放还可能让频率依赖当前或目标序列长度;手写实现若在长度跨阈值后改变频率,就要保证历史 Key 被一致重算,不能只旋转新 Key。

正确:整个请求固定一份 RoPE contract
prefill K(0..P-1) ─► cache ─► decode Q/K(P), Q/K(P+1), ...

错误:prefill factor=4,decode 中途改成 factor=8
旧 K 坐标系 A ────────────────┐
                               ├─ 点积没有统一位置语义
新 Q 坐标系 B ────────────────┘
text

12 怎样定义“有效上下文长度”?#

不要把单个最大长度准确率当成答案。一个可执行的评估立方体至少有三条轴:

                         任务复杂度
                    单证据 / 多证据 / 多跳 / 聚合


证据位置  开头 / 中间 / 末尾 ◄─┼─► 上下文长度  L₀ / 2L₀ / 4L₀ / L₁
text

对每个格子重复不同随机种子并报告置信区间。建议同时保留四类指标:

指标族例子能发现什么
语言建模滑窗 loss、perplexity长度增加后整体分布是否崩坏
定位检索passkey / key-value exact match是否能在远处找到单条证据
组合推理多 needle、多跳追踪、计数与聚合是否真正联合使用多处信息
原窗口回归原任务分数、短上下文 perplexity扩窗是否破坏已有能力

“在 128K 的一个末尾 passkey 上成功”最多证明一个格子通过。RULER 的动机正是:简单单针检索接近满分时,增加 needle 数量、追踪和聚合后仍可能明显下降。

13 用分桶程序避免只报一个平均数#

下面的骨架把模型调用留成显式接口,输出记录可直接聚合为长度×位置表:

evidence_fraction 要按 token 位置而不是字符位置计算,否则中英文、代码和空格会让证据落点偏移。生成式 exact match 还应固定解码策略、最大新 token 数与答案规范化规则。

14 系统指标为什么必须与质量一起测?#

即使质量稳定,长上下文也可能因成本失去工程价值:

  • 完整自注意力的关系矩阵规模从 L02L_0^2 增到 (sL0)2=s2L02(sL_0)^2=s^2L_0^2
  • KV Cache 容量随层数、KV 头数、序列长度与 head width 近似线性增长。
  • prefill 处理整个输入,通常比单步 decode 更受长序列影响。
  • 更长输入可能降低 batch size,引发吞吐骤降和排队延迟上升。

因此每个长度桶还应记录:

输入长度 L
   ├─► 质量:loss / retrieval / reasoning accuracy
   ├─► 延迟:time-to-first-token、inter-token latency
   ├─► 吞吐:tokens/s、requests/s
   └─► 资源:峰值显存、KV bytes/request、可用 batch size
text

滑动窗口注意力、稀疏注意力、检索增强生成(Retrieval-Augmented Generation, RAG)与压缩记忆可以减少成本或筛掉无关文本,但它们改变了信息可见范围,不属于单纯 RoPE 缩放。

15 一条可执行的调试路径#

  1. 先冻结随机 Q/K 做角度测试。 比较原始、线性与基底缩放的 inv_freq,确认最高频和最低频是否按设计变化。
  2. 验证旋转保范数。 每个位置的 Q/K 在旋转前后 L2 范数应保持接近;否则偶奇配对或广播轴有误。
  3. 验证共同平移。 固定相同缩放参数,同时给 Query/Key 位置加偏移,RoPE 点积应保持不变。
  4. 验证完整前向与 cache。 关闭 dropout,逐位置比较 full causal forward 与 prefill+decode,而不是只看最终 token。
  5. 打印实际 token 长度。 tokenizer 截断可能让所谓 32K 样本仍只有 4K;检查进入模型后的 input_ids.shape
  6. 按长度画 loss。L0L_0 逐步扫到 L1L_1,寻找突然断崖,而不是只测两个端点。
  7. 按证据位置画热图。 开头、中央、末尾分别测,避免平均数掩盖 Lost-in-the-Middle。
  8. 回归短上下文。 用未改 checkpoint 的同一批短样本作基线,确认扩窗没有用明显短程退化换取长程分数。
  9. 记录系统曲线。 在固定硬件、batch 与解码参数下测 TTFT、吞吐和峰值显存。

16 最常见的长上下文误判#

  • 只改 max_position_embeddings 接口接受更长输入,不代表权重适配了新频率。
  • 把无 OOM 当作成功。 这只验证系统容量,没验证语言建模或信息利用。
  • 只测一个末尾 passkey。 模型可能在中央证据、多针或聚合任务上失败。
  • 只报全局平均准确率。 U 形位置偏差会被平均数掩盖。
  • 扩窗后不测短任务。 均匀插值压缩局部距离,原窗口能力可能回退。
  • 训练与推理 factor 不同。 Q/K 进入不同频谱,checkpoint 语义被破坏。
  • 运行中更改缩放却复用 KV Cache。 历史 Key 与新 Query 坐标系不一致。
  • 混淆 RoPE base 与扩展因子。 rope_theta=10000 不是窗口长度,factor=8 也不是角频率。
  • 低精度生成超长位置角度。 float16/bfloat16 的位置舍入可能让相邻大整数无法可靠区分。
  • 宣称插值消除了 O(L2)O(L^2) 它只改位置相位,不改变标准注意力的二次计算。

17 与相近方案的边界#

方案改变什么主要收益主要边界
直接 RoPE 外推只允许更大的 position id零结构改动未见相位可能失效
线性 Position Interpolation所有位置/频率统一除以 factor把长距离压回旧范围局部距离也被压缩
NTK-aware / YaRN 类按频率非均匀缩放,可能调注意力尺度更好平衡局部与长程方法与 checkpoint 参数必须匹配
LongRoPE 类搜索分维、分位置的非均匀插值支持更激进扩展搜索、训练与验证更复杂
滑动窗口注意力限制每个 Query 可见的 Key控制计算与 cache 上界窗口外信息不能直接访问
RAG先检索再把相关片段放入上下文减少无关 token 与成本受检索召回、切块和排序影响

PI、YaRN 和 LongRoPE 仍属于“让 RoPE checkpoint 适配更长距离”;滑动窗口与 RAG 则改变了模型实际读取哪些 token。它们可以组合,但实验必须分别归因。

18 今天真正需要记住什么?#

  1. RoPE 能计算任意位置,只说明位置函数有定义;有效上下文还取决于权重是否学会使用训练外距离。
  2. Position Interpolation 用 p=p/sp'=p/s 把目标窗口映回旧相位范围,等价于把所有频率除以 ss
  3. 均匀缩放会压缩局部距离;非均匀频率方案尝试保留高频局部分辨率,同时拉伸低频长程尺度。
  4. 缩放参数是训练、完整前向、KV Cache 与部署共同遵守的坐标契约,不能在请求中途切换。
  5. 有效窗口必须按长度、证据位置和任务复杂度分桶,并同时回归短上下文质量与系统成本。

19 思考题与小练习#

  1. 一个模型从 L0=2048L_0=2048 扩到 L1=8192L_1=8192。分别计算原位置 0, 1024, 4096, 8191 在线性 PI 下的位置;再计算原本相隔 8 token 的局部距离被压成多少。
  2. 修改 rope_inv_freq,打印 head_dim=64factor=8 时原始、线性与基底缩放的前 3/后 3 个频率。解释为什么两种缩放不能只比较最后一个频率。
  3. evaluate_context_grid 扩成两针任务:两条证据分别放在 (0.1,0.9)(0.3,0.7)(0.45,0.55)。除了 exact match,还应记录哪些失败类型?

相关工作#

20 下一篇预告#

位置缩放解决的是“远距离坐标怎样进入注意力”,没有解决标准注意力随序列长度二次增长的问题。下一篇将从一个 token 到底能读哪些 Key 出发,比较滑动窗口、块稀疏与全局 token 如何改变可见性图、复杂度和 KV Cache。

模型能接收 32K 就真的会用 32K 吗?RoPE 位置插值、频率缩放与长上下文验证
https://zwjcode.cn/blog/rope-context-extension-position-interpolation-evaluation
作者
发布于 2026年9月9日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。