我最早试过把 LLaMA 的上下文窗口从 2048 扩到 8192。当时社区里流行的做法是线性缩放:位置索引除以 4,然后直接推理。结果模型开始输出乱码——不是质量下降那种,是彻底脱离语义的胡言乱语。

那个深夜让我意识到一个被忽略的事实:位置编码不是一张静态的坐标表,而是一组带频率的旋转轴。你对位置做的任何缩放,都会同时改变这些轴的转速。直到后来我看到 NTK-Aware 插值,它只改了一个超参数,就把这个问题解决了大半,模型权重一个没动。
位置编码为什么是一组“旋转的指针”
NTK-Aware 是为 RoPE 这种位置编码量身定做的。RoPE(旋转位置编码)不把位置数字加到词向量上,而是让每个 token 的 query 和 key 向量自己旋转一个角度,第 m 个 token 旋转 mθ 这么多。
这个设计妙在,旋转不改变向量之间的夹角。query 和 key 旋转后做点积,结果只跟它们的角度差相关,而角度差就是位置差。注意力天然只看相对位置,这是 RoPE 原论文 的核心结论。
但 RoPE 不是一根指针,是很多根。每个维度有自己的转速:θ_i = base^(-2i/d),其中 i 是维度下标,d 是注意力头维度,base 默认取 10000。
对 LLaMA 来说 d=128,i 从 0 到 63。i=0 的维度转得最快,每移动一个 token 转过约 1 弧度,走完一整圈只需要 6 个 token;越往后转速越慢,最后一个维度走完一整圈需要几万个 token。
也就是说,位置信息被分解成两种角色:快轴负责局部——我旁边的 token 是谁;慢轴负责全局——我在整篇文档的哪个位置。两者对上下文长度的态度完全不同。
线性插值把所有频率一视同仁,结果糊掉了
线性插值的思路很朴素:原来最多 2048 个位置,现在要支持 8192 个,那就把位置 m 除以 4,让编码落在模型熟悉的区域。Meta 的位置插值论文 就是这个思路。
但除以 4 等价于把每个维度的转速同时除以 4。快轴原本一个 token 的移动就能转出明显的角度差,现在要走 4 个 token,局部顺序的分辨率被压扁;慢轴又被不必要地压得更慢。模型训练时见过的“相邻夹角模式”被整体扭曲。打个不太严谨的比方:照片缩小之后,最先糊掉的一定是最细的纹理。所以不做微调直接线性插值,模型质量断崖式下降。
就算愿意微调,也是在用信息损失换跨度——没有新能力被创造出来,只是把旧能力摊到了更长的距离上。
“NTK”这个名字,学术分量其实大于实际
我第一次看到 NTK-Aware 这个名字,以为背后有一整套核方法推导。NTK(神经正切核)确实是深度学习理论里的概念,研究无限宽神经网络的训练动力学,理论细节可以看 原始论文。
在 NTK 视角下,网络的训练对不同频率成分的适应速度不一样:低频结构往往最先被拟合,高频细节要花更久。这给了一个直觉——模型对低频的变化更耐受,对高频的变化更敏感。
但如果你去读 YaRN 论文,会发现 NTK-Aware 并没有严格使用 NTK 的数学:它只是把这个“频率不平等”的直觉搬到了 RoPE 上,看效果,然后就成了。方法提出时是启发式。名字里的 NTK 更像一种致敬,不是推导依据。
这种“不严格但有效”的启发式在深度学习中并不罕见。它真正的价值,是指出了线性插值的病根:你用一把尺子量了所有频段,而模型对不同频段的耐受度根本不是一回事。
真实的做法:改一个超参数,让快轴和慢轴各得其所
NTK-Aware 的落地方式简单到让你怀疑是不是漏了什么:把 RoPE 的 base 从 10000 放大,仅此而已。
社区常用的公式是:
base' = base × s^(d/(d-2))
s = 目标长度 / 原始长度
d = 注意力头维度
对 LLaMA 7B/13B 来说 d=128,指数 128/126 ≈ 1.016。想把上下文从 2048 扩到 8192,s=4,base 大约乘 4.1——从 1 万变成 4 万出头。
为什么有效?回到转速公式 θ_i = base^(-2i/d)。base 变大后所有转速都会下降,但降幅不一样:快轴几乎不动,慢轴明显变慢。这正是我们要的分配——高频外推、低频内插。
快轴为什么可以外推?想象一块钟表。秒针转得飞快,窗口内转了几百圈,模型早就见过它每一种姿态;你把窗口拉长,秒针看到的还是同一个表盘。慢轴则像时针,在 2048 的窗口内连一整圈都没走完。你要是把它外推,模型就进入从未见过的相位区间。所以慢轴需要的是被拉长——也就是被插值,让更长距离的相位能落回模型见过的范围。
顺带说一句,base 的 10000 从哪来的?它沿用了 Transformer 原始正弦位置编码里的频率基数,本来就是个拍脑袋的默认值。在 RoPE 里,它决定的是整个频谱的展开范围,而模型学到的权重会适应这个频谱。没人规定它必须固定——NTK-Aware 第一次把“这个超参数可以随上下文长度缩放”这件事摆上了台面。
实际用起来,在 Hugging Face 的 transformers 里你甚至不需要定制代码:
config = LlamaConfig.from_pretrained("meta-llama/Llama-2-7b-hf")
config.rope_theta = 40000
model = LlamaForCausalLM.from_pretrained(..., config=config)
把位置编码扩展的主要方法放一起看,会很直观:
| 方法 | 做法 | 不微调的效果 | 主要问题 |
|---|---|---|---|
| 直接外推 | 什么都不做 | 很快崩坏 | 新距离的相位从未出现过 |
| 线性插值 | 位置除以缩放因子 | 输出质量急剧下降 | 高频局部信息被压缩 |
| NTK-Aware | 放大 base | 2–4 倍窗口内相对可用 | 中间频段处理粗糙 |
| YaRN | 按波长分组逐段缩放 | 更接近微调效果 | 公式复杂,需要调参 |
几个容易卡住的问题
模型权重一个没变,为什么它就能“处理”更长上下文?
因为它没有真正学会长距离依赖。NTK-Aware 做的事,是把原本距离 6000 的 token 的相位差,映射回模型训练时见过的等价相位差。模型还是那个模型,只是新距离在编码空间里“看似”旧距离。它没有注入新能力,只是给旧能力换了套坐标系。
把 base 设成无穷大,是不是就能无限扩展?
不行。base 无穷大时所有转速趋于 0,位置信息被稀释到几乎消失,RoPE 退化为无效编码。即便在实用范围内,base 过于激进也会让局部位置分辨能力下降。找到“局部清晰”和“全局可及”的平衡点,才是 NTK-Aware 真正在做的取舍。
动态 NTK 和静态 NTK-Aware 有什么不同?
静态 NTK 从第一个 token 开始就用新 base,哪怕你的文本只有 500 个 token,编码也跟原来不完全一致。动态 NTK 的思路是:在原始窗口长度内完全保持原来的 base,只有超出原始窗口时才逐步放大。这样短文本不受任何影响,长文本继续获得扩展红利。所以它在实践中常被视为更稳妥的变体。
它不是免费的午餐:能力是映射出来的,不是长出来的
NTK-Aware 在 2023 年夏天的本地模型圈里几乎一夜风行,因为它让没有算力微调的人也能把 7B 模型撑到更长的上下文。但它的流行不能掩盖两个真实的局限。
第一,中间频段的处理太粗。放大 base 确实让高频和低频各得其所,但中间那些维度怎么办?严格的做法是像 YaRN 那样,按每个维度的波长与上下文窗口的关系分组:波长远小于窗口的不动,波长远大于窗口的拉长,中间区域用混合系数平滑过渡。NTK-Aware 相当于给所有维度发了一把统一的量角器,然后指望频谱的连续性自动修正。
第二,它没有扩大模型对长文本的“理解”,只是让它在长文本上看起来不那么糊涂。在需要真正跨段落检索、多跳推理的长上下文评测里,NTK-Aware 往往会在细节上露馅——它能读到远处的内容,但“利用远处内容”的能力并没有随窗口一起增长。
所以现在的生态也很清楚:零成本快速验证用 NTK-Aware 或动态 NTK;要稳定效果,要么配合微调,要么直接切换别的位置编码路线。对 RoPE 这类周期性位置编码,NTK-Aware 会一直是上下文扩展工具箱里性价比最高的一件——但你得知道它修的是“尺子”,不是“脑子”。
原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/704.html