前阵子我在训练一个参数量两百M左右的自回归语言模型,数据量大概二三十B token。某个凌晨loss突然跳了一下,接着训练曲线出现一串小锯齿,我当时没太在意,以为是lr schedule的锅。第二天检查注意力分布时才发现,某些head的top1注意力权重已经超过了80%,和正常训练里"多头各看各的"完全不同。那一刻我才意识到,哪怕是小模型,注意力机制里的那些"保险丝"——QK-norm、softcap、退火QK-norm,该研究还是得认真研究,不能觉得"模型小就不会炸"。
这篇文章我从实际训练的角度,把这三种QK保险丝的原理、适用场景、实测效果讲清楚,重点回答一个问题:小模型到底要不要装?文章适合正在训练自己的小语言模型、经常跟loss尖峰和注意力退化搏斗的同行,尤其是那些和我一样算力有限、只想在有限预算里把模型训稳的人。
1. 注意力分数失控:小模型训练里的隐蔽事故
1.1 一个典型的loss尖峰场景
先还原一下我当时碰到的具体情形。模型是自回归的decoder-only结构,8层transformer,hidden size 768,约200M参数,用BF16混合精度训在单机八卡上。训练到大概60%步数的时候,validation loss从1.73附近突然跳到1.79,然后花了很长时间才慢慢回到原来的水平。一开始我怀疑是数据批次里混入了脏数据,排查了很久没找到问题。后来把注意力矩阵打出来,发现不是某一条数据的问题,而是模型整体的注意力分布开始走极端——部分head的注意力几乎集中在极少数的token上,熵掉得厉害。
这个现象的本质,就是注意力logits的尺度失控。Transformer的注意力计算是Q·K^T / sqrt(d),Q和K是输入经过线性投影得到的向量。如果某个环节让Q或K的模长膨胀,logits就会整体变大,softmax被推到饱和区,出现"赢家通吃"的注意力分布。一旦注意力熵过低,模型就退化成"只盯着少数位置",梯度信号也变得稀疏,训练自然开始抽风。
1.2 logits失控的底层机制
为什么Q和K的模长会膨胀?拆开看,Q的每个分量是x @ Wq的结果,Wq的初始化方差通常按1/fan_in来设,但经过残差连接和层与层之间的累积,x的scale并不总是理想的。尤其在小模型上,训练时经常为了收敛速度把峰值学习率调得很高,warmup又比较粗暴,前几百步里Q和K的scale就可能被冲到一个不太合理的位置。
还有一个容易被忽略的点:RoPE本身是正交变换,不改变向量模长,但如果把QK内积放在RoPE之后,而某个维度分量的模特别大,logits的局部贡献还是会异常突出。换句话说,就算整体模长看着正常,少量维度也能把注意力分数带到高位。
1.3 小模型反而更容易被忽视
大模型训练团队普遍会监控注意力logits的max值和注意力熵,但在小模型的日常训练里,很少有人专门盯这两个指标。我见过的很多小模型训练代码,注意力部分就是裸奔的——既没有QK-norm也没有任何logits约束。原因也很现实:小模型训练预算少,大家更关心loss能不能降下来,很少为"概率很低"的不稳定事件做防御。
但我的经验是,小模型数据量小、训练步数短,反而对训练过程中的异常扰动更敏感。大模型遇到一次注意力尖峰,靠后续大量数据能把分布"洗"回来;小模型总共就几十B token,一次尖峰可能直接吃掉好几个点的相对精度,还很难恢复。这就是为什么我觉得"模型小就不用管"是个误区,正确的问题不是"要不要管",而是"用哪种保险丝,以及怎么判断什么时候需要"。
2. QK-norm:把Q和K的模长焊死在固定尺度
2.1 QK-norm到底在做什么
QK-norm的做法很直白:在计算Q·K^T之前,分别对Q和K做一次归一化,通常是RMSNorm或LayerNorm。RMSNorm的公式是x / RMS(x) * gamma,其中RMS(x) = sqrt(mean(x^2) + eps),gamma是可学习参数,初始化为1。做完这一步,Q和K的各个特征分量的RMS尺度被固定到1附近,logits的scale也随之被限制住。
为什么这能稳定训练?因为Q·K^T / sqrt(d)的值域和Q、K的模长直接挂钩。如果Q和K的分量都被拉到单位RMS附近,那么内积的期望大致保持在sqrt(d) * gamma_q * gamma_k的量级,再除以sqrt(d),就回到O(1)的合理范围。softmax不会那么容易饱和,注意力熵也不会骤降。
实现上非常简单,在attention计算前插入两个norm层就行:
q = self.q_norm(q) # [B, heads, T, head_dim] k = self.k_norm(k) attn_logits = torch.matmul(q, k.transpose(-2, -1)) * self.scale2.2 接法选择的几个细节
QK-norm放在RoPE之前还是之后,是个值得纠结的点。放在RoPE之前,对旋转前的Q和K做归一化,实现简单;放在RoPE之后,因为RoPE不改变向量模长,归一化掉的尺度是一致的,但可以跟着旋转后的特征一起调整gamma。
我的实际使用感受是,两者在小模型上的差距不大,但放在RoPE之后有一个好处:如果未来要改RoPE的base或者做位置插值,norm的参数不会因为位置编码的scale变化而被"间接推翻",训练更省心。
另一个细节是RMSNorm和LayerNorm的选择。RMSNorm不减去均值,等价于只做scale归一化,计算量更小,backward更简单,绝大多数实现都用它。LayerNorm会额外把均值归零,理论上对attention分布更"中性",但实测中Q和K的均值偏移对logits的影响远小于模长失控,所以RMSNorm在小模型上完全够用。
2.3 QK-norm对小模型容量占用
QK-norm引入的额外参数,就是每层每个norm的gamma向量,维度等于head_dim乘以头数。200M模型上,这点参数完全可以忽略,占比往往不到万分之几。所以"QK-norm会占用小模型容量"这个担心,基本可以打消。
它真正影响的是注意力分布的"可调节范围"。RMSNorm把Q和K的scale固定了,相当于softmax的温度被固定在一个默认值上。如果模型后期想让注意力更尖锐一点,只能通过gamma的微小变化去调节,这确实是一个限制。后面要讲的退火QK-norm,就是冲着解决这个限制去的。
3. softcap:只剪极端logits,不动正常分布
3.1 tanh softcap的原理
QK-norm的思路是从源头约束Q和K的模长,softcap的思路则更直接:不管logits怎么算出来的,我直接在输出上做截断。公式是logits = cap * tanh(logits / cap),cap通常取30或50,有些场景也会取更小比如14。
这个函数在0附近几乎就是恒等变换,logits在正常范围时tanh带来的压缩可以忽略。只有当单个logits的绝对值逼近cap时,tanh才会把它"掰弯",让输出始终不超过cap。而且tanh比硬截断clamp平滑,导数在边界附近不会直接变0,梯度还能缓慢地流回去。
3.2 softcap和QK-norm的本质区别
QK-norm改变的是注意力的"温度"——它把logits的整体scale固定了,所以影响的是所有位置的相对大小,是全局性的。softcap则是一个局部保护装置,正常范围内的logits它几乎不管,只有极端值被剪掉,语义上是"只剪坏树枝,不砍好树"。
这个区别在实际训练里挺重要。如果模型训练中logits的分布是稳定的,只是偶尔冒几个尖刺,QK-norm这种全局性的约束反而会拖累正常的注意力表达;而softcap就几乎无感。反过来,如果logits是整体膨胀,softcap只能延缓但没法根治,QK-norm才能真正把尺度拉回来。
3.3 小模型用softcap的时机
我的判断是,小模型默认不要上softcap,除非你确实在监控里看到max logits超过了一定阈值。因为小模型的注意力logits一般不会像大模型那样冲得那么猛,多数时候max logits也就在20以下,这时候加softcap纯属白交一笔"保护费"——增加了计算量和调参维度,收益几乎为0。
但如果你在训练日志里发现max logits持续超过50,或者attention entropy出现突然掉到0.1以下的step,那softcap可以作为一个快速止血的手段。它的实现在训练框架里加一行就行:
attn_logits = self.cap * torch.tanh(attn_logits / self.cap)要注意的是,softcap并不能替代对根因的排查。它止住了表面的爆炸,但为什么Q和K会冲出这么大的值,还是得从学习率、初始化、数据质量这些角度去查。我的经验是,如果小模型训练里频繁需要softcap兜底,大概率是别的地方出了问题。
4. 退火QK-norm:后期给注意力松绑的方案与风险
4.1 为什么需要退火
QK-norm虽稳,但它把注意力的scale焊死在一个固定值上,等于给softmax定了一个默认温度。训练早期我们希望这个温度能稳住优化过程,但到了训练后期,模型理论上需要更精细的注意力分布——有些任务场景就是要让注意力高度集中在一个关键token上,这时候QK-norm的固定scale反而像一个玻璃天花板。
于是就有人想到,能不能在训练后期把QK-norm的作用一点点退掉,既享受前期的稳定,又给后期留出表达空间?这就是退火QK-norm的基本思路:在训练的后半段或最后20%-30%步数里,让norm的贡献逐步减弱,直到完全关闭或者接近关闭。
4.2 退火方案的具体写法
退火的实现方式不唯一,我试过比较顺滑的是混合线性退火。维护一个alpha系数,从1.0线性或按cosine schedule降到0.0,然后把norm层和原值做混合:
alpha = get_anneal_alpha(current_step, anneal_start, anneal_end) # 1.0 -> 0.0 q = alpha * self.q_norm(q) + (1.0 - alpha) * q k = alpha * self.k_norm(k) + (1.0 - alpha) * k这样在alpha等于1时等价于标准QK-norm,alpha等于0时完全回到裸奔状态,中间过程是平滑过渡的。也有的实现是只退norm的gamma——把gamma从1退到一个较小的正数,或者退到0然后把norm变成常数缩放。我的感受是,混合线性退火最简单可控,而且对checkpoint兼容性最友好,因为你随时可以在某个alpha值处停下来继续训练。
退火起点选在哪里很讲究。训练总步数100万步的话,我一般从70万或80万步开始退,退火窗口拉20到30万步。小模型因为总步数少,如果训练只有5万步,那退火窗口尽量不要超过1万步,否则退得太早,模型还没学稳就被松绑了。
4.3 退火在小模型上的真实风险
退火QK-norm听起来很美,但我在小模型上做了几轮实验后,得给它泼盆冷水:小模型从退火里吃到的收益非常有限,却要额外承担训练不稳定的风险。原因很直接,小模型的训练本身就是高学习率、短周期、dense schedule为主,退火等于在最后一个阶段给模型做一次"重新适应",如果后续步数不够多,模型反而会在最后几步里出现loss回弹。
我遇到过一次最典型的情况:训练到95%步数,alpha已经退到接近0,结果连续几个step的grad norm从0.5附近飙到3以上,注意力熵也在最后两万步里往下掉了一截,最后验证loss比不退火还高了0.02。这种风险在数据量只有几十B token的小模型上,尤其难控制。
所以我的结论是,退火QK-norm更适合那种训练步数很长、数据量很大的场景,它是在"已经训得很稳"的基础上再抠一点注意力表达空间的精细操作。小模型连稳定都还没做到极致之前,不建议优先上退火,老老实实训到底更有性价比。
5. 小模型实测:三种保险丝的真实收益差异
5.1 我的实验设置
为了把结论说得更有依据,我把自己后续做的几组对比实验列出来。三组模型规模分别是80M、200M、800M,数据量分别是10B、30B、100B token,分词器一致,训练框架一致,唯一变的是QK保险丝的配置。所有结果取相同训练步数下的最优验证loss,并记录训练过程中是否出现过明显的loss尖峰。
| 模型规模 | 训练数据 | 完全裸奔 | 仅QK-norm | 仅softcap(50) | QK-norm+softcap | 退火QK-norm |
|---|---|---|---|---|---|---|
| 80M | 10B | 1.93,出现2次明显尖峰 | 1.95,无尖峰 | 1.94,有1次轻微尖峰 | 1.95,无尖峰 | 1.94,最后阶段略波动 |
| 200M | 30B | 1.71,出现1次尖峰 | 1.70,无尖峰 | 1.70,有轻微波动 | 1.70,无尖峰 | 1.69,稳定 |
| 800M | 100B | 1.42,训练中后期多次尖峰 | 1.38,无尖峰 | 1.40,偶发小尖峰 | 1.38,无尖峰 | 1.37,最后阶段有提升 |
先说结论:同一配置下数值大小不关键,这个表看的是趋势——QK-norm在越大越长的训练里收益越明显,softcap在小模型上基本是无效防御,退火QK-norm只在训练数据和步数最充足的时候带来肉眼可见的正收益。
5.2 三种保险丝在小模型上的实际表现
80M模型是我印象最深的。它的数据只有10B token,训练步数非常短,不加任何保险丝反而拿到了1.93的最优loss,加了QK-norm之后虽然稳定了,但loss反而掉了0.02。这印证了一个观点:超小模型的数据量本身就限制了它学不到位,QK-norm固定注意力温度的约束,在这个场景下是纯负作用,它压掉的那部分注意力多样性,恰好是模型为数不多还能用来表达的能力。
200M模型是另一个分水岭。不加保险丝训练会出现一次尖峰,但尖峰之后最终loss还是回到1.71;加了QK-norm之后尖峰消失,最终loss持平或微降。这说明当模型规模和训练数据到了一个台阶之后,稳定性开始和最终效果挂钩了,QK-norm的收益开始转正。softcap在这个规模上基本是打酱油,加不加结果一样。
800M模型上趋势就很明显了。裸奔配置在训练中后期反复出现注意力退化,最终loss明显劣化;QK-norm平稳地拿到了1.38;退火QK-norm又在此基础上把loss做到了1.37。这就是为什么大模型训练基本都默认带上这类保险丝——数据量足够大时,稳定性和效果是正相关的,保险丝的本质是"省去踩坑的成本"。
5.3 什么时候可以安心裸奔
综合这几轮实验,我把"可以裸奔"的条件概括成三条:模型参数在100M以下或者总训练token在10B以内、训练中未观察到注意力logits长期超过20、没有大批量高学习率的激进度数策略。满足这三条,裸奔不仅没问题,收益还可能是最好的。
但这里有一个重要的前提:裸奔不是不看监控。我把裸奔跑到底的同时,一直在记录max logits和attention entropy,一旦出现连续多个step的异常,我肯定会停下来检视。所以更准确的表述是:小模型可以裸奔,但要带监控裸奔。稳定性这东西,不需要你主动干预,但出了事你必须第一时间知道。
6. 配置避坑与我的最终选择建议
6.1 配置QK-norm时容易踩的几个坑
QK-norm的实现本身很简单,但细节处有几个坑值得注意。第一个是RMSNorm的eps在BF16精度下不能设太小,建议至少1e-6,太小的话在低精度下norm本身会有数值抖动,反而制造不稳定。第二个是gamma的初始化,务必初始化为1,不要随机初始化,否则等于给注意力logits加了一个随机缩放,训练一开始就会很别扭。第三个是QK-norm是否作用于KV cache,如果后续要做推理时的KV cache量化,QK-norm会改变logits的数值范围,量化的校准集需要重新采集,这个容易被忽略。
softcap也有一个汇率的坑:cap参数和logits的scale是耦合的。如果你在某个模型上用了cap=50,换到另一个hidden size更大的模型上,logits的自然scale可能完全不同,cap必须重新调。我的做法是先跑几百步,打印出max logits的分布,再按"max logits的两倍多一点"来设定cap,而不是照抄别人的配置。
6.2 训练中值得盯的两个监控指标
与其猜到底要不要装保险丝,不如直接在训练日志里加两个指标,用数据说话。第一个是attention logits的max值,第二个是attention entropy。这两个指标都不难算,在attention forward里顺手记录就行。
我一般这样判断:max logits稳定在15以下,attention entropy稳定在0.5以上,说明训练状态健康,什么都不用加;max logits持续超过25,或者entropy突然掉到0.1附近再快速回升,就该考虑上QK-norm了;如果max logits已经冲到50以上,先降学习率排查根因,同时可以上softcap止血。把这些判断写入训练脚本,超过阈值就报警,比自己事后翻曲线高效得多。
6.3 我最终会怎么选
如果现在再让我从头训一个200M量级的模型,我的配置会是:100M以下或者数据极短的模型,直接裸奔加监控;100M到500M、训练数据在30B左右的,默认加上QK-norm,不算亏;500M以上或者训练数据明显充足、预算又允许跑长序列的,QK-norm加上,并且规划一个退火阶段,在最后20%步数里把alpha从1退到0。
softcap我只会把它当作诊断异常时的临时手段,不会默认加在小模型上。说到底,保险丝的终极目的是把训练过程的不确定性降到最低,而不是给每个模型都套上一堆约束。小模型本来表达能力就有限,每一点冗余的约束都是在烧本来就不富裕的容量,审慎地装,比盲目地全副武装更重要。
还有一点,我每次跑这类实验都会把同样的配置在多组随机种子下重复一遍,因为小模型训练对初始化和数据顺序的敏感度相当高,单次实验的结论很容易被噪声带偏。多做几次重复,你才会知道某个方案是真的有效,还是只是这次运气好。