资讯详情

资讯详情

QK-norm、softcap与退火QK-norm:稳定Transformer训练的三种保险丝

我最近在练一个大概三亿参数的 decoder-only 模型数据量不算多满打满算几十 B token。前七万步一切正常loss 曲线漂亮得像教科书第七万零几十步loss 直接从我印象里的 3.1 附近跳到了 9 以上。等我把训练挂回去、重新 dump 完 logits 分布才发现根本不是数据或学习率的问题——QK 点积炸了。也就是从那时候起我开始认真对待“QK 保险丝”这群技术QK-norm、softcap、退火 QK-norm。这篇东西我想把这三个方案放在一起讲清楚它们各自在防什么、怎么实现的、小模型到底值不值得装。尤其最后一点网上说法很两极有人觉得小模型容量本来就紧张再加 norm 等于把注意力焊死也有人觉得小模型训练更脆不装保险丝一个 spike 就能让你前功尽弃。我两边都踩过下面用我自己的训练记录说话。1. 保险丝要挡的到底是什么attention logits 失控的完整链条1.1 一个 300M 模型在第七万步的“意外”那次事故的现场其实很典型loss 在几十步内从 3.1 爬到 9.4然后开始剧烈震荡。我第一反应是学习率出问题或者数据 pipeline 混进了脏 batch但检查下来都正常。真正的问题藏在 attention logits 里我用一个采样 hook 打印了每层 QK 点积的统计值发现最后几层的 logits max 已经冲到 60 以上而正常训练时这个数字通常在 5 到 10 之间。这个现象在 Transformer 训练里并不罕见。QK 点积本质上是两个向量的内积它没有天然的上下界。如果 Q 向量和 K 向量的模长在训练中持续被放大logits 就会被推向极端。而我们通常不直接约束模长只会做初始化、加 scale、加 LayerNorm 这类间接控制。于是当某一步参数更新恰好让一批 Q、K 的模长同时变大logits 就可能在极短时间内冲破阈值形成肉眼可见的 loss spike。1.2 logits 失控为什么会演变成梯度灾难单看 logits 大本身不一定致命。致命的是它后面紧接着 softmax。softmax 对输入尺度极其敏感当 logits 从 5 涨到 50softmax 输出会快速趋向 one-hot最大的那个位置几乎拿到全部概率其他位置直接归零。一旦 softmax 变成 one-hot注意力梯度就废了。因为被忽略的那些 token 的梯度贡献约等于零只有被选中的那个位置在反向传播里有信号。这条路径上的参数会继续加大 logits让下一次前向更加极端。这个正反馈循环持续几轮loss 就会像坐过山车一样冲上去而网络内部的权重早已被严重扰动。这也是为什么我说保险丝防的是“失控的链条”而不是单纯防“logits 数值大”。QK-norm、softcap 这类措施的价值是在链条的第一环把 logits 的幅度按住避免后面的 softmax 饱和、梯度丢失、参数震荡一环接一环地发生。1.3 小模型为什么格外经不起这种冲击大模型遇到 logits 尖峰时往往表现为损失轻微抖动因为参数量大、冗余度高某些层被扰动后还有其他路径能兜住。小模型没有这个冗余空间。三亿参数的模型某两层的 QK 模长一旦炸掉几乎等于把这一路特征全部冲乱恢复时间非常长。小模型的训练还往往伴随更少的数据和更短的训练步数。一次严重的 spike 可能直接抹掉前面几万步积累的成果尤其在训练后期学习率已经降下来模型依赖精细调整过的注意力模式被 spike 一冲很多局部结构就再也回不到原来的状态。所以我的观点是小模型不是“不需要保险丝”而是“更需要保险丝”。但小模型也确实存在表达力被压制的副作用这是下一节要展开的核心矛盾。2. 三种保险丝的机制对比QK-norm、softcap、退火 QK-norm2.1 各自在电路的哪个位置动手用电路来打比方QK-norm 是在“电源端”动手直接限制 Q、K 向量的模长softcap 是在“负载端”动手在 logits 进入 softmax 前加一道限幅退火 QK-norm 则是给 QK-norm 装了一个定时开关训练后期逐步把限制撤掉。QK-norm 的实现非常直白对每个 attention head 的 Q 向量和 K 向量分别做一次 RMSNorm让它们在参与点积前拥有固定的模长。这样一来QK 点积的天然尺度就被限制在 head_dim 附近不会再跟着训练中模长的漂移而无限放大。softcap 的做法是给注意力分数加一个非线性压限logits C * tanh(logits / C)。它不改变 Q、K 的模长而是在 logits 离开点积计算后、进入 softmax 前把绝对值大于 C 的部分平滑地压回来。这个方案保留了正常范围内的自由度只对极端值动手。退火 QK-norm 在思想上又进一步它把 QK-norm 视为一个训练阶段的工具而不是模型架构的永久组成部分。在训练前期norm 提供稳定在训练后期当模型已经学会控制自身行为就逐步把 norm 的影响退掉让注意力恢复原本的锐利度。2.2 三种方案的本质差异QK-norm 的核心思路是“让 logits 的统计学分布是可控的”。RMSNorm 会让 Q、K 各自的 RMS 变成 1所以即使输入 x 的模长跑偏再远norm 输出也被拉回固定范围。代价是它会影响所有样本的 logits而不是只影响极端值。也就是说即使那些本来很正常的注意力模式也被统一地压缩了动态范围。softcap 的核心思路是“保留正常动态只处理尾部”。当 logits 绝对值远小于 C 时tanh 函数近似线性softcap 几乎不起作用只有当 logits 明显超过 C 的时候它才发力把极端值压下来。这个性质让它对正常注意力分布的影响更小但代价是需要调一个合适的 C调太小会在正常区间引入不必要的压缩调太大又等于没装。退火 QK-norm 的核心思路是“按训练阶段切换策略”。它承认前期稳定性和后期表达力都是必要的但不能同时拥有所以用时间维度来换取空间维度。前期享受 norm 带来的稳定后期通过退火逐步释放注意力头的自由度。2.3 一张表看懂三种方案的取舍方案动手位置对大 logits 的处理对正常 logits 的影响需要调的超参数推理阶段成本QK-normQ、K 投影之后直接限制向量模长压缩整体动态范围基本无eps 默认即可两个 RMSNormsoftcap注意力分数之后非线性截断到 [-C, C]在 abs(logits)C 时近似线性一个 C 值一个 tanh退火 QK-normQ、K 投影之后 训练计划训练前期压制后期放开前期受限后期恢复退火起点、退火窗口、曲线不额外增加这三个方案也不是互斥的。很多实践中的配置是“QK-norm 打底 softcap 兜底 后期退火”三层保险各自负责一段风险窗口。不过在小模型上我不建议一上来就三重叠加因为每加一层都会引入新的超参数和实现细节出了问题反而难以定位。2.4 从恒压电源到限流电阻的生活化类比QK-norm 让我想到稳压电源。电网电压不稳设备容易烧你就在入口处加一级稳压不管进来的电压怎么波动输出总是固定值。代价是输出电压被统一限定某些设备可能得不到它想要的更高电压。softcap 更像限流电阻。它不改变正常工作电压只在电流超过阈值的时候把它限住。设备正常运行时感觉不到它的存在但短路时它能救命。缺点是阈值设多少需要经验而且它不能阻止电压本身漂移只是不让漂移造成破坏。退火 QK-norm 则像一个训练后期允许拆掉的稳压器。电路在最容易出事故的初期需要稳压但等整个系统稳定运行后你希望设备能直接吃到原始电压以获得更好的性能所以拆掉它。拆的过程不能太突然否则设备会闪断重启这就是退火窗口存在的意义。3. 小模型的独特账本head_dim、表达力损失和训练预算3.1 head_dim 越小QK-norm 的“限制感”越明显小模型通常不仅参数量小head_dim 也小。很多 300M 到 1B 的模型head_dim 用的是 64甚至还有 32 的。head_dim 直接决定了每个注意力头能表达多少种不同的匹配模式也决定了 QK-norm 之后的 logits 动态范围。QK-norm 把 Q、K 归一化到固定模长后点积的期望标准差大约在 1 附近乘上标准 attention scale 后。这时候如果 head_dim 是 128注意力头还有足够的维度去编码相对位置、句法关系、词汇匹配等不同信号如果 head_dim 只有 32这些信号被压缩到同一个 1 量级的 logits 空间里头与头之间的区分度就会明显下降。我在实践中感受到的差别是head_dim 64 的模型加 QK-norm下游任务分数通常只掉零点几个点head_dim 32 的模型加 QK-norm某些需要精确指代的评测任务能掉三到五个点。这个损失在训练曲线上不一定看得出来因为 PPL 甚至可能更好看但真实能力已经悄悄缩水了。3.2 被牺牲的注意力锐利度在哪些任务上看得见注意力锐利度指的是模型能否让某一两个 token 获得极高权重。文档检索、多跳问答、代码补全里精确的变量引用都依赖这种锐利选择能力。QK-norm 把 logits 的整体尺度压下来后softmax 的分布会变得更平注意力分数之间的差距变小锐利度自然下降。小模型本身在锐利度上就没有优势因为参数量限制了它精确建模长程依赖的能力。再加一层模长限制等于把一份本来就不丰厚的预算继续压低。我做过一个对照实验规模不大仅代表我自己的观察同一个 300M 模型训练数据完全相同有 QK-norm 的版本 PPL 略好但在一个简单的文档检索任务上准确率比无 QK-norm 版本低了约 4 个点。这让我意识到一个很重要的问题PPL 是“平均视角”注意力锐利度是“极端视角”。保险丝把极端情况稳住的同时也可能把极端能力一并抹平。小模型用户如果特别在意检索类能力必须认真权衡这笔账。3.3 数据预算与算力约束改变了性价比但这笔账的另一头是训练稳定性。小模型常见的训练场景是数据量不大、算力有限、只打算跑一次训练。在这种预算下一次中等规模的 loss spike 可能就让你损失几十上百个小时的算力。QK-norm 把 spike 的概率压到很低这种确定性带来的收益比那些“理论上存在但很难复现”的表达力损失更值钱。我的个人阈值判断是如果你的模型小于 1B、训练数据在 100B token 以内、head_dim 不超过 64、下游任务以通用生成和对话为主那么装 QK-norm 的数学期望是正的。它牺牲的那点锐利度往往可以在退火阶段拿回来。如果你是在做搜索引擎、做密集检索、做需要精确指代的任务或者你有很多次重训机会那可以优先考虑不装或者只装一个宽松的 softcap。4. 实战给 300M 模型加上 QK-norm 与 softcap 的最小改动4.1 完整参考实现QK-norm 的插入位置我用一段接近 PyTorch 风格的伪代码来说明。关键点是norm 必须作用在 head_dim 维度上而不是整个 hidden_dim 上。很多初学者把 RMSNorm 直接接到 q_proj 输出后面发现效果不对就是维度搞错了。import math import torch import torch.nn as nn class RMSNorm(nn.Module): def __init__(self, dim, eps1e-6): super().__init__() self.eps eps self.weight nn.Parameter(torch.ones(dim)) def forward(self, x): # x: [..., head_dim] x x * torch.rsqrt(x.pow(2).mean(-1, keepdimTrue) self.eps) return x * self.weight class Attention(nn.Module): def __init__(self, hidden_dim, num_heads, head_dim): super().__init__() self.num_heads num_heads self.head_dim head_dim self.q_proj nn.Linear(hidden_dim, num_heads * head_dim, biasFalse) self.k_proj nn.Linear(hidden_dim, num_heads * head_dim, biasFalse) self.v_proj nn.Linear(hidden_dim, num_heads * head_dim, biasFalse) self.o_proj nn.Linear(num_heads * head_dim, hidden_dim, biasFalse) self.q_norm RMSNorm(head_dim) self.k_norm RMSNorm(head_dim) def forward(self, x): bsz, seq_len, _ x.shape q self.q_proj(x).view(bsz, seq_len, self.num_heads, self.head_dim) k self.k_proj(x).view(bsz, seq_len, self.num_heads, self.head_dim) v self.v_proj(x).view(bsz, seq_len, self.num_heads, self.head_dim) # QK-norm 放在这里对最后一维 head_dim 做归一化 q self.q_norm(q) k self.k_norm(k) # 如果用了 RoPE建议在 norm 之后做旋转顺序更稳定 # q, k apply_rope(q, k) q q.transpose(1, 2) # [bsz, heads, seq_len, head_dim] k k.transpose(1, 2) v v.transpose(1, 2) attn torch.matmul(q, k.transpose(-1, -2)) / math.sqrt(self.head_dim) attn torch.softmax(attn, dim-1) out torch.matmul(attn, v) out out.transpose(1, 2).contiguous().view(bsz, seq_len, -1) return self.o_proj(out)这里有几个细节值得强调。第一q_proj 和 k_proj 的 bias 最好设为 False因为 norm 对偏置比较敏感加了 bias 反而会让归一化后的向量带上一个固定的偏移。第二RMSNorm 的 weight 初始化为 1 就好不需要额外去调 scale。第三如果你用的是 GQA 或 MQAK 的 head 数比 Q 少k_norm 要按 K 的实际 head 数来建不能直接复制 Q 的配置。4.2 softcap 的配置细节避免把注意力压成“一锅粥”softcap 的代码更简单通常在注意力分数计算完之后、softmax 之前插入attn torch.matmul(q, k.transpose(-1, -2)) / math.sqrt(self.head_dim) softcap 50.0 attn softcap * torch.tanh(attn / softcap) attn torch.softmax(attn, dim-1)但配置 C 值需要先了解你的 logits 分布。我的做法是在训练日志里记录 logits 的 max、p99、mean、std跑上两三千步看正常区间在哪。如果正常 logits 大多在 5 以内、极少超过 15那 C 设 30 或者 50 都行基本不干扰正常区间。如果正常 logits 就已经到 30那 C 至少要设 80否则 tanh 的非线性区会在日常计算里就生效把注意力分布压得太平。softcap 的陷阱是它只能限幅不能恢复梯度。当 logits 绝对值远超 C 时tanh 的导数趋近于零这部分梯度会被截断。所以它适合兜底不适合作为唯一的稳定性手段。真正频繁出现 logits 尖峰时我更推荐用 QK-norm 从源头控制。4.3 训练监控统计 qk logits 而不是只看 loss装保险丝之后必须配套监控。loss 是结果指标logits 分布是原因指标。你在 loss 上看到 spike 时往往已经晚了。我习惯在训练循环里每隔固定步数采样一次def log_qk_stats(step, attn): with torch.no_grad(): max_logit attn.max().item() mean_logit attn.mean().item() std_logit attn.std().item() p99_logit torch.quantile(attn.flatten(), 0.99).item() print(fstep{step} max{max_logit:.2f} p99{p99_logit:.2f} fmean{mean_logit:.2f} std{std_logit:.2f})如果 p99 持续上升比如从 8 涨到 12 再到 18别等它炸。这时候就要考虑调整初始化、降低学习率或者提前把 QK-norm 的介入权重拉高。小模型最忌讳的就是“loss 还好就不管 logits”因为 loss 的平滑很容易掩盖内部分布的缓慢漂移。5. 退火 QK-norm 的实操什么时候退、怎么退、退完发生了什么5.1 为什么要退norm 会锁住注意力的“锐化”窗口前面说过QK-norm 会把注意力分布的动态范围限制在一个较窄的区间里。这个限制在训练前期是保护在训练后期却可能变成天花板。模型在后期需要精细地区分“哪个 token 重要、哪个 token 不重要”需要注意力分数之间的差距足够大而 norm 不允许这个差距无限拉大。退火 QK-norm 的做法就是在训练后期把 norm 的影响逐步撤掉让模型重新获得调整注意力锐度的空间。它和 warmup 刚好相反warmup 是让优化器从平滑过渡到激进退火是让模型从受限过渡到自由。一个好的退火计划应该让模型在几乎没有感知的情况下完成这个切换。5.2 三种退火实现与我的推荐方案第一种是插值法也是我最常用的。定义退火系数 lambda它从 1 平滑衰减到 0def anneal_factor(step, start_step, end_step): if step start_step: return 1.0 if step end_step: return 0.0 t (step - start_step) / (end_step - start_step) # cos 从 1 到 0比线性更平滑 return 0.5 * (1.0 math.cos(math.pi * t)) # 在 attention forward 中 lam anneal_factor(step, start_step, end_step) q lam * self.q_norm(q) (1 - lam) * q k lam * self.k_norm(k) (1 - lam) * k第二种是直接切换法在某个步数直接把 norm 从计算图里拿掉。这个方案简单但极容易导致 loss spike因为 QK logits 会在一步之间从 norm 约束内跳到无约束状态。除非你的学习率已经降到非常低否则我不推荐。第三种是空间法先只退 K 侧 norm保留 Q 侧 norm 一段时间或者反过来。这样可以让模型分阶段适应但实现起来要多写不少状态逻辑。我的经验是插值法已经足够平滑不需要再去搞分侧退火增加不必要的复杂度。5.3 退火窗口、学习率与常见翻车点退火窗口的长度我一般取总训练步数的 5% 到 15%。总步数一万步时窗口至少给一千步总步数十万步时窗口五千到一万步比较稳妥。起点通常放在总步数的 85% 到 95% 之间。太早退模型还没稳定norm 的保护价值没有充分实现太晚退剩余步数不够模型充分适应无 norm 的动力学。退火期间的学习率也应该配合。如果训练计划里本来就有 cosine decay退火窗口大概率落在学习率已经比较低的区间这是天然的配合。但如果你用的是常数学习率或者分段常数那退火开始时要主动把 lr 降到峰值的 0.1 到 0.3 倍否则更容易翻车。我实际踩过的坑有两个。第一个是 lambda 下降太快。有一次我把一万步的退火窗口设成了两千步结果退火到一半 loss 开始出现低频振荡每次振荡都伴随一轮 logits 尖峰。改成 cos 计划、窗口拉长到五千步之后问题立刻消失。第二个是退火结束后马上做了一次 short warmup 再训练导致已有模式被破坏。退火结束后模型应该进入一种“静默适应”状态学习率保持很低不要给它新的剧烈扰动。退火完成后怎么验证我会看三件事loss 曲线在退火区间有没有异常抬升qk logits 的 max 和 p99 是否逐步脱离 norm 约束并稳定在一个新的区间下游任务的 benchmark 是否比不退火的版本更好或者至少不差。如果退火后 loss 短暂上升但很快回落通常说明模型正在重新校准注意力尺度可以接受如果 loss 持续不降就要回退窗口长度或者停止退火。6. 要不要装的决策清单别盲信保险丝6.1 按任务类型与训练阶段选择的决策框架我在实际项目中总结了一套比较实用的判断顺序核心是先看模型规模和数据预算再看下游任务对注意力锐利度的敏感程度。场景建议模型 1B、head_dim 64、数据 100B token、通用生成装 QK-norm并在总步数 90% 附近退火模型 1B、下游是文档检索/精确指代类任务不装 QK-norm必要时加宽松 softcap模型 1B 到 7B、数据充足、训练预算宽裕QK-norm 退火基本可以无脑装微调或 SFT 阶段保持和预训练一致的配置不要新引入 norm模型已经稳定训练很久、无任何 spike 先例不要强行装保持现状这套建议不是说没装保险丝就一定出事而是说在不确定性和预算约束下哪些选择更稳。小模型用户最常见的错误是两个极端要么完全不信保险丝等 spike 出现后才痛苦地重训要么把 norm 焊死在架构里从头到尾不退火最后发现模型检索能力差得莫名其妙。6.2 不装保险丝的例外场景有一种情况我特别想强调如果你的模型要在推理阶段做极长上下文的扩展QK-norm 需要谨慎。因为长上下文推理时新增 token 的 K 向量可能和训练时看到的有分布差异norm 会把这种差异强行拉到和训练一致的尺度短上下文看是保护长上下文看可能反而限制了模型对新位置的适应能力。另外如果你在模型上还叠加了很重的量化方案低 bit 量化对 Q、K 向量的模长很敏感。QK-norm 让 Q/K 的尺度变得很集中对量化是好事但退火之后你又把尺度放开了这会让量化误差重新变得不可控。所以退火前要评估量化方案能不能接受无 norm 的 logits 分布不能为了训练期性能牺牲部署期稳定性。6.3 我的个人取舍一次性训练与研究性调试分开对待现在再回到“小模型要不要装”这个问题我的答案已经很清楚取决于你还有没有第二次机会。如果你只有一次训练预算跑完就上线那我强烈建议装。无论是 QK-norm、softcap 还是退火组合它们的核心价值不是“让分数更高”而是“让训练过程可控”。一次训练里少一次 spike比在评测集上高零点几个点重要得多。如果你有研究性质的调试空间可以反复训练、能接受失败那我会建议先不装只加监控。等 logits 分布真的出现问题时再装上保险丝做对照。很多时候你会惊讶地发现某些模型在特定数据配比下可以一直稳定跑下去根本不需要 norm。盲装反而会让你的实验多一个变量干扰你对模型本身的判断。我还想分享一个“半自动保险丝”的思路在训练代码里写一个监控器当 qk logits 的 p99 连续 N 步超过某个阈值时自动把 QK-norm 临时插入计算图等分布稳定后再逐步移除。这个方案适合超长训练任务等于给模型装了一个可自动触发的智能保险丝。代价是实现复杂但在资源有限的项目里非常值得投入。保险丝的本质不是永远限制电流而是让电路安稳度过最容易出事故的阶段。训练前期接上它训练后期有计划地拆掉它比永远焊死在电路里聪明得多。这也是我从三亿参数的教训里最深的体会任何训练技巧都要服务于“这一阶段的真实风险”而不是服务于某种教条。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →