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

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