NTK-aware 插值:比朴素位置插值更好的长上下文扩展方案

有一次我打开 llama.py 的源码,想找社区里传得神乎其神的 NTK-aware 插值到底是怎么实现的。搜了半天,真正的核心就一行代码:

AI technology illustration
base = base * (scale_factor) ** (dim / (dim - 2))

说实话,第一反应是失望:把一个数字从 10000 改大一点,就能让 LLaMA 的上下文从 2048 延伸到 8192,短上下文能力还几乎不掉?这听起来像智商税。

但顺着这行代码往下挖,把 RoPE 原始论文、NTK 理论的讨论、以及后来的 YaRN 论文读完,我才意识到:这行代码是整个思考链条的浓缩。它看着便宜,是因为真正的功夫全花在"理解为什么能这么做"上了。

先搞清楚 RoPE 在干什么

Transformer 的注意力机制天生没有顺序感。把 "我打你" 打乱成 "你我打",注意力计算的结果完全不变。所以模型必须靠外部机制给每个 token 标位置。

早期方案是绝对位置编码,直接往词向量上加一串位置编号。RoPE(旋转位置编码,出自RoFormer 论文)换了一种更优雅的思路:把词向量想象成一根指针,长度是语义,角度是位置。位置 m 的词,指针整体旋转 mθ 度。

两个 token 的相对距离,就变成了两根指针的夹角。注意力只需要比较夹角,就能感知"这两个词隔了几位",不需要学习额外的相对位置参数。

RoPE 还有一个容易被忽略的关键性质:不同维度的旋转速度不一样。第 i 个维度的旋转角速度是 base^(-2i/d),base 默认 10000。i 越小转得越快(高频),i 越大转得越慢(低频)。

高频维度的周期只有大约 6 个 token,负责捕捉相邻词的精细位置;低频维度的周期长达数万个 token,负责长距离的整体关系。这个分工决定了后面所有故事的走向。

朴素插值:把尺子拉宽,刻度就废了

预训练只见过 2048 个位置,要支持 8192,最直觉的做法是把位置编号按比例缩小——位置 1 变 0.25,位置 2048 变 512。这篇论文把这种方法命名为 Positional Interpolation(PI)。

这个位置域的操作,转化到频率域,相当于把所有旋转速度都除以缩放因子。换句话说,这根橡皮筋不仅拉了,而且拉得很均匀——高频被放慢,低频也被放慢。

放慢低频没有问题(NTK-aware 后面也这么做),但放慢高频是个灾难。高频是模型花了几百亿 token 才练出来的"微观位置分辨力",现在它被减速 4 倍,模型对相邻 token 的细微差异突然变得迟钝。就像一句语速很快的话被放慢 4 倍,你每个字都听得清,却抓不住那种微妙的连续感。

社区实测也验证了这一点:直接对预训练模型做 PI 而不微调,短文本的困惑度会明显上升,模型仿佛失去了对相邻词的敏感度。

外推为什么死得那么快

那换个思路:位置编号不改,直接让模型看 8192 以内的位置。这就是外推。结果更惨,模型很快就语无伦次。

问题出在低频维度。

算一下就明白:以常见的 head_dim=128 为例,RoPE 最低频维度的旋转周期约为 5.3 万个 token。预训练只提供了 2048 个位置,连周期的 4% 都不到。模型根本没见过低频相位在远处长什么样。你直接让它处理 8192 的位置,低频相位瞬间进入一个从未采样过的区域——这不是没学好,是压根没见过。

NTK 理论(神经正切核)恰好有个启发:神经网络对外推的耐受度,跟频率高度相关。对于训练中已经被充分采样的高频分量,多推几个周期只是熟悉模式的延续;真正让模型崩溃的,是那些训练里只采到一小段的低频分量。

所以直接外推崩掉,恰恰因为低频太少见,而不是高频太脆弱。

我一开始搞反了

NTK-aware 的公式,我最初读反了。看到 base 被放大,我以为它是想压缩高频——毕竟直觉告诉我转得越快的东西越容易出事。

但把数学仔细列出来,结论正好相反。base 放大后,高频维度(i 小)的变化几乎可以忽略不计;真正被明显压缩的是低频维度。

这个反转让我困惑了一整天,直到我重新理解了频率与外推的关系:高频转得快,模型在预训练中已经通过几百次全周期采样把它学透了,外推几个新圈只是"熟悉的延续";低频转得慢,模型根本没见过它的远端相位,这才是需要被拉回训练范围的部分。

换句话说:NTK-aware 让高频外推、让低频插值。跟朴素插值"所有频率一视同仁地插值"相比,它敢于保留高频的原状。

NTK-aware 的实现:一行代码里的精密设计

具体做法是把 base 从 10000 放大为 base × α^(d/(d-2)),α 是上下文扩展倍数。由于各维度对 base 变化的敏感度不同,这一行代码产生了非均匀的效果:

  • 最高频维度几乎不变——保留模型在预训练阶段建立的微观分辨力;
  • 最低频维度大约被压缩 1/α——把远端低频相位拉回模型见过的区间;
  • 中间维度平滑过渡,不产生频谱断裂。

一张表看懂它的优势

对比维度 朴素位置插值 (PI) NTK-aware 插值
高频维度 被除以 α,微观分辨力受损 基本不变,保留外推能力
低频维度 被除以 α,虽安全但无差别压缩 被压缩约 1/α,把外推变成插值
短上下文性能 明显退化 基本保持
长上下文扩展 可行但有损 可行且损失更小
实现成本 一行代码 一行代码

2023 年发布后,llama.cpp、KoboldCpp 等推理工具很快就集成了这个方案。社区反馈表明,在 2~8 倍扩展范围内,NTK-aware 的困惑度显著优于朴素插值,长文本生成稳定性也远好于直接外推。

工业界的验证来得更直接:Meta 在 CodeLlama(论文)里把 RoPE 的 base 从 10000 提高到了 1000000,以支持 16k 长代码上下文。这本质上就是 NTK-aware 思路的一种变体——压低低频、保留高频,只是缩放因子被固定成了一个常数。

它也不是银弹:YaRN 为什么更强

NTK-aware 有一个结构性弱点:缩放曲线由单一的幂函数决定,没法对不同维度做精细的按需处理。

它把最低频压缩了 1/α,但如果某个维度波长远超上下文,压缩后低频相位可能仍在训练范围之外;而对某些中频维度,它又压缩过头了。

YaRN 论文(2023 年 9 月)正是从这点切入的。它提出 "NTK-by-parts":先按波长把维度分成高频区、中频区、低频区,高频完全不动、低频完全插值、中频平滑过渡,再对长距离注意力加一个温度系数做窗口校正。在 8~32 倍扩展范围内,YaRN 比 NTK-aware 更平稳。

同时,针对生成场景还出现了动态 NTK:根据当前序列长度实时调整缩放因子,边生成边修正,解决了静态缩放因子在超长对话中"总是对不上"的问题。

一个值得记住的数字

在 head_dim=128、base=10000 的配置下,RoPE 最低频维度的波长约为 5.3 万 token。2048 的预训练上下文,意味着模型见过的低频相位只有整个周期的不到 4%。你让一个只见过扇形 4% 的智能体去推测剩下 96% 的样子——外推不崩才怪。

我的选择:什么时候用 NTK-aware

如果你今天要为模型扩展长上下文,我的建议是:

  • 纯推理、不微调:用动态 NTK 缩放,它在短上下文性能、长上下文稳定性和实现成本之间取得了最实用的平衡。
  • 有微调预算:首选 YaRN,它的频率分区设计更适合训练过程中的多阶段缩放。
  • 理解 RoPE:NTK-aware 依然是极好的教学范本,一行代码就让你看见频率分工的全部逻辑。

几年之后再回头看,NTK-aware 也许会被更精细的方案覆盖,但它确立的视角不会过时:模型的频率结构不是一张均匀的白纸,不同频段有不同的生命周期、不同的外推承受力。你不能用一把剪子剪所有的线。

现在再看那行代码,它依然只有一行。但你已经知道它背后那个完整的世界了。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/962.html

赞 (0)
上一篇 2026年9月21日
下一篇 2026年9月21日

相关推荐