如果你也在用K-FAC或者类似的二阶预条件方法训练语言模型大概率设过refresh_interval这个超参数。一个直觉是每100步刷新一次Kronecker因子没什么好纠结的。但我最近在复现某个预条件训练框架时发现事情远没这么简单——同样的100步间隔把首次刷新的位置从第200步挪到第2500步训练曲线的差别大到像是换了另一个优化器。这篇文章想聊的就是这个为什么Refresh Interval不能完整定义一个Refresh Schedule以及调度方案里那个容易被忽略的维度——Kronecker因子的放置位置placement如何与学习率状态耦合最终决定了预条件方向的整体质量。1. 一个被忽略的变量刷新间隔背后的调度语义1.1 从训练日志里的一个怪现象说起前几个月我在复现一套K-FAC风格预条件的语言模型训练流程用的是固定刷新间隔每100个step重新计算一次A矩阵和G矩阵其余step沿用上一次的结果做预条件。这个设置在好几个公开实验里看起来都挺稳但到了我自己搭的7B规模模型上效果时好时坏。最典型的一次同一个模型、同一份数据、同样的batch size和序列长度只是把warmup从2000步换成200步刷新间隔仍然是100步最终验证集loss居然差了0.15个点。我开始以为是学习率调度本身的问题但把普通AdamW baseline跑一遍warmup长短带来的差距并没有那么大。也就是说预条件环节放大了这个差异。后来我把训练日志按“刷新事件”对齐来看发现问题出在Kronecker因子快照的采集时机上warmup从2000步缩短到200步之后第一次刷新第300步对应的学习率从很小的值变成了相对高的峰值附近。这两个场景下A因子和G因子捕捉到的梯度统计特征完全不同而后续2000步训练又一直沿用这批因子。间隔没变变的是刷新点落在学习率调度的哪个位置。1.2 Refresh Interval与Refresh Schedule是两个层面的事这里需要把两个名词拆开。Refresh Interval定义的是两个刷新点之间的步数差比如100步或者500步。但一个完整的Refresh Schedule至少包含以下信息第一个刷新点发生在第几步对应学习率处于什么状态两次刷新之间学习率经历了上升、平稳还是衰减每个刷新点采集到的Kronecker因子会被复用到接下来多少个训练状态差异很大的step里换句话说Interval只是时间轴上的刻度Schedule才是“在哪个训练状态下拍快照以及快照覆盖哪些训练状态”的完整描述。之前我只关注了刻度忽略了快照的拍摄时机等于把调度语义简化成了一个标量而且这个标量远远不足以约束优化过程。提示在二阶预条件训练里刷新的核心不是“定期换新”而是“让预条件方向尽可能贴合当前参数附近的曲率信息”。脱离学习率状态去谈刷新间隔相当于只看墙上的钟走了几格却不管拍照的人站在什么位置。2. Kronecker预条件为什么不能每步都刷2.1 K-FAC的基本思路K-FAC的核心是将Fisher信息矩阵或近似Gauss-Newton矩阵做Kronecker分解。假设某一层权重矩阵为W输入激活为a损失对输出这里指进入激活函数之前的值的梯度为g那么对这块的Fisher矩阵块可以做近似分解F ≈ A ⊗ G其中A是激活协方差矩阵G是梯度协方差矩阵。之所以能这么分解是因为独立地把输入维度和输出维度分开建模忽略它们之间的交叉相关性。于是预条件方向就可以通过解两个更小的线性方程组来得到而不是去求整个巨大Fisher矩阵的逆。这套方法对语言模型特别有意义。Transformer里embedding、注意力投影、FFN的全连接层都有大量矩阵乘法参数维度动辄上万每个step直接构造完整Fisher矩阵根本不现实。Kronecker分解把复杂度从参数的平方级别降到了两个因子各自平方级别之和再加上足够low-rank的近似才能把二阶信息铺到几十亿参数规模的模型上。2.2 每步刷新为什么不行其实早几年就有人试过在线估计Kronecker因子也就是每个step都用当前batch的数据更新A和G。从数学上讲这样做能让预条件方向一直跟踪最新的曲率但实际工程上几乎跑不动原因有三点计算开销大。A因子的计算需要把一段序列的激活统计全部归约起来G因子涉及反向传播梯度的外积累积。对长序列模型来说这部分开销经常比前向加反向本身还贵。存储压力高。即使A和G各自都是约几个GB的矩阵随batch变化持续更新也会频繁触发内存拷贝和多卡通信。多机训练时还要面临梯度之外的第二套all-reduce流量。统计噪声反而更严重。单个step的batch梯度方差很大用它估计出的协方差矩阵非常不稳定预条件方向会在step之间剧烈抖动最终抵消掉二阶方法带来的收敛优势。所以工程上普遍的做法就是缓存每隔固定步数重算一次Kronecker因子中间的step都用缓存的版本做预条件。这样既控制了计算成本也自然起到平滑估计的作用。2.3 固定间隔下的“隐藏自由度”缓存机制引出了一个关键事实给定同一个Refresh Interval实际生效的因子序列可以完全不同。因为决定因子内容的不只是“每隔多久刷一次”还包括“每段因子的出生时刻和退役时刻”。举个例子同样是间隔100步方案A第0步计算因子用到第100步方案B第50步计算因子用到第150步方案C第0步计算因子第100步再算一次但第100步正好是warmup结束、学习率刚冲到峰值的位置这四个方案的Interval都是100但第150步时模型使用的预条件矩阵来自第50步的A和G还是来自第100步的A和G对应的训练状态可能已经大不相同。第一次刷新位置、总训练步数、学习率曲线的形状共同决定了“同一个间隔”的不同命运。这个隐藏自由度一旦被忽略实验结果就会表现得非常不稳定。我在那个复现项目里第一次发现这个问题时甚至怀疑是随机种子出了问题直到把刷新时刻对应的学习率和loss打印出来才确认是因子放置导致的系统性偏差。3. 学习率状态如何影响Kronecker因子质量3.1 训练各阶段梯度统计的特征差异要理解placement为什么重要得先看不同训练阶段里A因子和G因子的统计特征会怎么变化。可以简化成三类阶段Warmup阶段。学习率从小到大爬升参数还在快速离开初始化点梯度方向变化剧烈batch间的方差很大。这个阶段的激活统计和梯度统计都不能代表“训练中段”的状态更像是混乱期的快照。峰值/稳定阶段。学习率在相对高位运行参数在loss landscape的陡峭区域来回震荡梯度方向相对集中协方差矩阵的谱结构比较稳定。这里采集到的Kronecker因子通常有较好的条件数预条件效果也比较可信。衰减阶段。学习率逐渐下降参数进入较平坦的区域梯度幅值变小分布在窄化协方差矩阵的特征值变小且可能变得非常ill-conditioned。这个阶段刷新的因子里G因子尤其脆弱对噪声很敏感。用生活化的类比来说warmup阶段拍照照片里全是动态模糊峰值阶段拍照静态主体清晰但背景杂乱衰减阶段拍照整体画面很干净但光线暗到看不清细节。而预条件矩阵一旦拍了这张照片后面几百步都得靠它指引方向。3.2 因子快照存在“保质期”Kronecker因子本质上是对当前参数点附近曲率结构的局部估计。参数沿着训练轨迹持续移动曲率结构也在缓慢改变所以任何一个快照都有有效期。采样时刻越早等到训练中后期因子的陈旧程度就越严重。这里有一个不容易被注意到的细节A因子相对稳定陈旧问题不那么突出G因子则高度依赖当前梯度分布一旦学习率进入衰减阶段G因子的统计特征往往会出现明显漂移。如果G因子是在峰值阶段采样的到了衰减阶段还继续沿用预条件方向就容易出现“用旧曲率引导新梯度”的错配。在我们组的实验里对比过两种刷新方案一种是在峰值阶段每100步刷新另一种是在衰减阶段每300步刷新。后者虽然总刷新次数更少但最终验证集loss略好因为后期采样到的G因子与当前梯度状态更匹配。这也印证了刷新质量并不完全取决于刷新频率。3.3 一个简化的决策框架实践中我会把刷新点的好坏拆成两个维度来评估代表性和覆盖度。代表性这个刷新点所对应的训练状态能否代表接下来几百步里模型的典型状态。覆盖度两次刷新之间的所有step它们的真实曲率与刷新点快照的差异有多大。一个高质量的刷新计划应该让代表性强的采样点尽可能覆盖训练的主要阶段同时避免把低代表性采样点比如warmup早期的影响范围拉得太长。注意这里“长”不等于step数多而是指学习率状态跨度大。在warmup早期刷新的因子即使只用了50步也可能跨越了整个warmup到峰值的变化影响非常大。这个框架帮助我解释了很多之前看不懂的实验为什么有人把refresh interval从100调到50反而变差因为更多刷新发生在warmup内采集到了更多低质量快照而把第一次刷新推迟到warmup之后哪怕间隔还是100效果就好很多。4. 学习率相关的放置策略三种可落地方案4.1 方案一推迟首次刷新点最直接的改进就是让第一次刷新发生在warmup结束之后。我们试过在7B模型上把首次刷新从第100步推迟到warmup结束后的第一个step比如第2000步后续维持100步间隔不变。效果非常明显前期loss曲线不再出现周期性抖动稳定性和平均step时间几乎不受影响。原因是warmup阶段的参数移动主要受初始化方向主导曲率信息没有太多值得预条件的内容。参数还没找到“正确的方向”采集出来的A/G因子只会把噪声模式固化进预条件方向里。一次性把warmup阶段全部跳过可以避免整个训练过程中最差的一批快照。4.2 方案二按学习率分段设置不同刷新频率在长训练计划里固定刷新频率并不是最优解。更合理的做法是按学习率区间分段训练阶段学习率状态推荐刷新间隔原因Warmup上升不刷或只刷一次避免固化噪声峰值平台稳定高位100~200步因子质量高可维持较旧快照指数衰减渐降200~500步曲率变化慢但需保证新鲜度末段微调极低500步以上统计弱频繁刷新反而引入噪声这里的关键原则是学习率变化越快、梯度统计漂移越明显刷新应该越频繁而学习率平稳且因子本身可靠时刷新间隔可以放宽。我们还在峰值阶段把刷新间隔设成150步在衰减阶段设为300步整体刷新次数降低约20%最终指标没有明显退化。4.3 方案三按学习率阈值触发刷新比固定分段更精细的做法是动态触发维护一个last_refresh_lr当当前学习率与上次刷新时的学习率差异超过预设阈值时才触发一次刷新。伪代码如下def should_refresh(current_lr, last_refresh_lr, current_step, last_refresh_step): # 检查学习率相对变化是否足够大 lr_ratio current_lr / (last_refresh_lr 1e-12) if abs(lr_ratio - 1.0) 0.15: return True # 同时保留一个最大步数兜底防止学习率变化太慢导致因子严重过期 if current_step - last_refresh_step max_interval: return True return False这个方案把刷新的触发权交给学习率调度的形状而不是机械地按step计数。在warmup阶段学习率持续爬升触发会比较频繁但我们通常会叠加“warmup期间不启用刷新”的开关在衰减阶段学习率变化逐渐变缓刷新自然变稀符合曲率变化慢的直觉。注意动态触发的触发频率要和计算开销协调好。如果阈值设得太小可能在峰值平台频繁刷新白白增加计算成本阈值设太大又会跳过学习率下降的关键窗口。一般从0.1~0.2起步结合刷新耗时来调。4.4 不同放置策略的效果对比我在同一套7B模型配置下做了A/B对比训练20K步固定总计算预算刷新耗时计入总时长结果如下策略刷新次数最终验证loss训练总时长相对偏差固定间隔100步首次刷新在第100步1991.6820%固定间隔100步首次刷新在第2000步1801.657-1.2%分段间隔峰值150衰减300约1401.6614.3%学习率阈值触发0.15阈值约1551.6492.1%排名大概是动态触发约等于先跳过warmup、固定间隔略差、分段间隔折中。综合考虑实现复杂度我会优先推荐“首次刷新推迟 分段间隔”因为动态触发需要额外调阈值收益并没有压倒性优势。5. 复现与调试中的坑排查思路与实操心得5.1 第一个坑Kronecker因子的统计不稳定性动态触发方案上线后训练中途出现过一次A因子爆炸某个刷新点采集到的激活协方差矩阵最大特征值突然大了一个数量级之后几百步的loss都异常升高。排查到最后发现那个刷新点正好落在一个数据分布比较特殊的batch上单batch统计代表性太差。这个问题的修法不是放弃刷新而是在刷新时做更稳健的统计聚合。比如使用多个batch的滑动平均来估计A和G因子或者对因子矩阵做特征值clip。实际操作里我用指数移动平均处理因子估计衰减系数设成0.9效果很稳定。5.2 第二个坑刷新代价被低估很多人只在训练日志里记了刷新用了多少秒忽略了刷新导致的设备间同步开销。在多卡训练里一次全量A因子计算往往涉及跨卡gradient外积的归约最坏情况下会让吞吐量下降30%以上。这个代价会让“更多刷新”的方案在总时长维度上吃亏。这也是为什么我把“最大兜底间隔”设置在动态触发逻辑里不仅是为了防因子过期也是为了防止学习率变化平缓时过度刷新白烧算力。5.3 第三个坑warmup期间关闭刷新的副作用关闭warmup期间的刷新有一个副作用最开始的几千步基本退化成了一阶方法。如果预条件因子没有及时建立某些病态方向的更新还是会被放大表现为训练初期loss下降变慢。解决办法是预热预条件因子在正式训练前用一个小batch子集跑几十步前向反向先把A和G的初始估计算出来。这个操作对最终效果影响不大但能让前几百步的曲线看起来更健康也方便中期诊断。5.4 排查风格建议当你发现同样的refresh interval复现不出相同结果时先不要怀疑种子或者精度而是把每次刷新时的step、current_lr、A_max_eig、G_max_eig打印出来。对比两个实验的“刷新事件表”大概率能看出差异集中在哪些阶段。再结合训完后的loss曲线定位具体是哪个刷新点影响了后续训练。这个“刷新事件日志”的方法帮我排掉了至少三个伪bug。强烈建议在框架里默认加上。6. 常见问题速查为什么我的refresh interval设得很小效果反而变差刷新间隔变小通常意味着刷新点更密集地落在warmup和状态变化剧烈的区域低质量快照对整个训练的影响被放大。建议先检查首次刷新的位置再谈频率。既然放置位置重要是不是可以完全按学习率来定刷新时刻可以但要控制刷新次数上限。按学习率触发容易出现峰值阶段高频刷新、后期又几乎不刷新的情况需要同时保留最大兜底间隔和最小触发间隔。A因子和G因子需要同一时刻采集吗实践中可以分开。A因子的时效性可以放宽G因子应该更贴近当前梯度统计。如果是资源受限的场景可以考虑A因子复用旧值、G因子按更短间隔刷新。刷新前要不要清零旧的因子不建议完全清零除非检测到明显的数值异常。直接用EMA更新旧因子在稳定性和对最新状态的响应之间平衡更好。如果训练使用了warmup cosine退火哪种策略最省事最省事又稳的组合是warmup阶段不刷新首次刷新点定在warmup结束后的50~100步内之后按峰值间隔刷进入衰减期后间隔翻倍最后1/3训练阶段每固定间隔兜底刷新一次。我个人在实际操作中的体会是预条件训练最怕的不是“刷得不够勤”而是“刷得太随意”。Refresh Interval只是阶梯的宽度真正决定你走得多稳的是每一步踩在哪个训练状态上。下一次当你困惑于K-FAC效果不稳定时先把那几个刷新区间的学习率状态打印出来你可能会看到和我不一样的现象但这个“对刷新事件本身做诊断”的习惯一定不会让复现工程白忙一场。