又是新的一周,机器学习周报也写到了第三十九期。熟悉我这种更新节奏的朋友应该知道,这份周报不是论文翻译,也不是课程笔记,而是我这一周在真实项目、实验、读paper过程中沉淀下来的技术记录。有些东西很细,细到只有踩过坑的人才觉得有用;有些东西很大,大到会影响后续一系列选型。这一期我打算把最近折腾的几个方向一起整理出来:大模型微调时的学习率调度问题、分布式训练里一次让人头疼的显存不均衡排查、两篇值得记录的研究工作,以及我一直在迭代的实验管理方式。希望这份记录能给正在做类似事情的人一些参考,哪怕只是少踩一个坑,也值了。
1. 模型微调里的学习率调度:参数背后的真实影响
1.1 为什么微调时学习率调度比预训练阶段更敏感
很多人对学习率调度的理解停留在“训练时把学习率降下来”这个层面,但到了模型微调场景,这个问题的性质其实变了。预训练阶段模型要从随机初始化状态学出一个通用表示,学习率的主要矛盾是“能不能收敛”;而微调阶段模型已经具备较强的通用能力,我们要做的是让它在特定分布的数据上继续调整,此时的主要矛盾变成了“学多少新知识、忘多少旧能力”的平衡。
二范数下的权重更新量可以简单理解为“学习率乘以梯度”的累积。因为在微调阶段,骨干网络的梯度往往比随机初始化时要小一到两个数量级,如果沿用预训练时期的大学习率,很容易把原本已经稳定的特征表示冲坏。相反,如果一上来就给一个很小的学习率,适配新数据的效率又太低。所以学习率调度本质上是给“遗忘速度”和“学到速度”之间装了一个阀门:前期阀门开大一点,让模型快速适应新分布;后期阀门关小,确保权重在最优区域附近稳定收敛。
这也是为什么在很多微调实践中,“warmup + 衰减”几乎成了标配。warmup阶段不只是在稳定优化器状态,它更重要的作用是让模型从预训练分布平稳过渡到新分布,避免在一开始就产生过大的梯度冲击。
1.2 我在小规模数据集上做的三组对比实验
这周我在一个文本分类任务上做了一组小规模对比实验,数据集量级大概一万多条,预训练模型是一个中等尺寸的生成式模型。我刻意控制了其他变量,只改变学习率调度方式,结果挺有意思。
第一组用的是固定学习率加一个短warmup:学习率固定在2e-5,前200步线性升温到目标值,之后全程不变。第二组用线性衰减:warmup之后按线性方式把学习率从2e-5降到0。第三组用余弦衰减:同样warmup到2e-5,再按余弦曲线降到接近0。
最终结果对比如下:
| 调度方式 | 验证集指标(相对提升) | 收敛表现 | 训练后期稳定性 |
|---|---|---|---|
| 固定学习率 + warmup | baseline | 收敛较快,但在最后几百步仍有轻微波动 | 一般 |
| 线性衰减 | +1.2% | 收敛平稳,后期略有震荡 | 较好 |
| 余弦衰减 | +1.8% | 收敛平稳,后期几乎无波动 | 最好 |
从绝对值上看,指标差异不算巨大,但训练后期的稳定性差异非常明显。固定学习率那一组在接近收敛时仍然会有小的性能抖动,说明学习率一直在让权重跨越最优区域;而余弦衰减组因为后期学习率极小,权重的随机游走范围被压缩,整体输出更稳定。
我的结论是,在中小规模数据集上做微调时,余弦衰减普遍比线性衰减更“从容”,尤其当训练步数不太多的时候,它天然自带“先快后慢”的节奏,不需要额外操心衰减率设置。如果你的训练资源紧张,只能跑固定步数,那余弦衰减基本是开箱即用的选择。
1.3 实际操作中的参数选择与两个常见误区
学习率调度里最容易被忽略的是warmup步数的设定。我见过不少同学把warmup设成固定步数,比如200步,但总训练步数只有600步,结果warmup占了三分之一,模型刚把优化器状态稳住就进入衰减阶段,反而打乱了节奏。合理做法是让warmup占总步数的3%-10%,训练步数越短,这个比例越要保守。
另一个误区是直接照搬别人论文里的学习率。论文里的最佳学习率跟模型尺寸、batch size、数据分布强相关,换了场景直接套用,很可能差一个数量级。我这边比较稳的启动方式是:先固定调度策略,把峰值学习率按对数尺度扫一遍,从5e-6、1e-5、2e-5、5e-5里选表现最好的,再去精调warmup比例和衰减曲线。
还有一个值得提的细节:如果你的微调任务用了比较大的batch size,比如大到需要梯度累积,那学习率的设定要适当调大一些。因为实际生效的更新步数变少了,同样的总更新量需要靠更大的单步更新来补。这也是很多人把batch size翻倍之后模型反而变差的原因之一。
2. 分布式训练下显存不均衡:一次完整排查复盘
2.1 现象:某个训练节点反复OOM
前阵子我在跑一个多节点数据并行训练任务,模型不算大,但数据维度比较杂。任务启动后不久,其中一个节点就开始报显存溢出(OOM),而其他几个节点的显存占用看起来还在安全水位。起初我以为是某个节点上的其他进程占了显存,没太在意,重启之后继续跑。
结果问题复现了,而且总是同一个节点出问题,其他节点稳定得很。这时候才算引起我的重视,开始正经排查。这类“只有某个节点OOM”的现象,在分布式训练里其实比全局OOM更让人头疼,因为全局OOM通常改改batch size就好了,单点OOM则意味着数据分布、计算图、甚至通信逻辑里可能存在不对称的地方。
2.2 排查链路:从显存统计到数据加载逻辑
排查过程我按四条线来走:
第一,确认硬件层没问题。用系统自带的显存查询命令逐卡看占用,确认没有历史残留进程,排除了其他任务抢占显存的可能性。
第二,检查框架侧的显存统计。这一步能区分是“模型本身占用”还是“激活值/临时张量”导致的峰值。从统计结果看,OOM节点的显存峰值比其他节点高出约20%-25%,而且峰值出现在每个step的后半段。这说明问题不是模型权重或梯度的静态占用,而是计算过程中产生的临时张量大小不一致。
第三,审查数据加载与预处理逻辑。这一步终于发现了关键线索:我们当时用了动态padding策略,按每个batch内的最大长度做对齐,用来省显存、省算力。想法本身没错,但问题是数据没有做任何长度排序或分桶,两个相邻样本的长度可能差异极大,某些batch被一条长文本直接拉高到接近2000 token,其他节点却平均只有800多 token。每个step的峰值自然完全不同。
第四,排查是否有loss缩放类的机制。数据并行下不同节点处理的batch不同,如果存在跟loss scale相关的全局同步,也会加剧某些节点的临时显存压力。检查后发现我们的场景没有这个问题,但如果你用的是混合精度训练,这一步值得仔细看。
2.3 根因确认与修复方案
根因就是动态padding在没有分桶策略的时候,会让batch内最大长度频繁出现极端值,导致部分节点的计算图峰值显存远超预期。解决方向有两个:
第一步,把“纯动态padding”改成“分桶动态padding”。按照样本长度分成几个区间,同一个桶内的样本长度接近,再在桶内做动态padding。这样既能保留动态padding省显存的优点,又不会出现batch长度突刺。实测改动后,各节点显存峰值差异从20%以上降到了5%以内。
第二步,对仍然偶尔出现的长样本做长度截断和分桶边界保护。具体说就是设定一个硬上限,超过上限的样本要么截断,要么单独成桶。这一步是为了防止个别极端样本把某个batch撑满。
顺带我也开了梯度检查点(gradient checkpointing)来降低激活值占用,虽然会让计算时间略涨,但整体显存压力小了很多。如果你遇到类似问题,建议按“数据分布 -> 计算图峰值 -> 全局同步机制”的顺序排查,大概率能在前三步内找到根因。
3. 本周读到的两个值得记录的研究方向
3.1 长文本外推:从位置编码到注意力机制的工程取舍
这一周我花了些时间看长文本建模相关的工作,其中一篇的思路挺有意思。它没有刻意改注意力结构,而是把重心放在位置编码和长序列训练策略上,用比较小的代价让模型在推理时支持更长的上下文。
这类做法的核心在于“外推”而不是“插值”。位置编码如果只在训练长度内见过有限位置,推理时遇到更长序列就会出现分布外的情况。当前很多方案试图让模型把训练阶段学到的位置信息“外推”到未见过的范围,常见做法包括调整位置编码的频率参数,或者在训练过程中随机截取不同长度的序列段来模拟长文本场景。
我比较关注的是它在工程上的可落地性。因为它基本不改变模型主干结构,对现有推理框架很友好,不需要为了长文本专门重写算子。这点在实际部署中价值很大:很多边缘场景的算力是固定的,结构改动越大,适配成本越高。
不过这类方法的代价也显而易见:外推超出一定比例后,模型在长文本上的注意力会变得很“散”,相关性判断的准确率明显下降。如果你要做的是需要精确推理的长文档任务,单纯靠位置编码改进是不够的,还是得回到注意力机制的稀疏化或压缩上想办法。
3.2 量化感知训练在边缘设备上的工程权衡
另一个让我认真做了笔记的方向是量化感知训练。这个方向不算新,但最近看到一些工作开始把“量化粒度”和“训练稳定性”放在一起考虑,我觉得值得记录。
传统的训练后量化(PTQ)要求模型权重和激活值的分布比较规整,一旦遇到离散特征或者离群点,量化误差就会放大。量化感知训练的做法是在训练阶段就模拟量化噪声,让模型参数适应低比特表示,从而减小量化后的精度损失。
这周看到的这篇工作主要是讲“分通道量化”的粒度选择。它对比了按张量量化、按通道量化、按token量化几种方式下的精度表现和硬件效率。结论不算意外:量化粒度越细,精度损失越小,但硬件利用率下降得越快,因为很多底层算子是为整块张量并行设计的,拆得越碎,访存开销越高。
我自己的经验是:如果你部署的目标设备支持比较成熟的量化算子,优先用逐通道量化,这个粒度在大多数场景下精度效率都足够好;只有当逐通道量化仍然损失过大时,再考虑往更细粒度走。另外,校准集的选择会影响最终量化效果的长期稳定性,最好覆盖真实部署时的输入分布,而不是随手找一批看起来差不多的数据。
4. “周报式学习”附带迭代出来的实验管理习惯
4.1 一套足够简单但完整的实验记录模板
写周报写到第三十九期,最大的副产品其实是实验管理习惯的沉淀。我以前也是那种“跑完就忘”的状态,过了两周想回头看当时的设置,只能从命令行历史里翻蛛丝马迹。后来我被迫形成了一套简单的实验记录模板,不需要复杂的平台,就是一个固定格式的文本块,放在每个实验目录里。
这个模板包含几类信息:实验目的(一句话说清楚要验证什么)、环境信息(框架版本、硬件型号、随机种子)、模型和数据配置(模型规模、数据来源、预处理方式)、训练配置(学习率、调度方式、训练步数、batch size、梯度累积步数)、实验结果(最优指标、对应step、训练时长)、以及备注(踩了哪些坑、下一步想怎么调)。
别小看这个模板,它把“能不能复现”的周期从几天缩短到了几分钟。特别是随机种子和框架版本这两项,我遇到过好几次因为库版本升级,结果指标莫名其妙变差的情况,没有记录根本定位不到。
4.2 复现和性能波动:如何判断结果是“真变好”还是“噪声”
这一周还有一个迭代点与复现相关。在做对比实验的时候,我习惯所有配置下至少跑三遍,取中间值而非平均值。原因是偶然因素导致的波动在少数几次实验里会严重影响判断。
举个直观的例子:一个改动如果让指标涨了0.3%,但三遍实验的方差就是0.5%,这个涨很可能是噪声。我现在的操作方法是:把每次实验的结果单独记录,先看三次之间的浮动范围,再看中位数变化,只有中位数和最大最小值都一致变好,我才认为这个改动真正有效。
另外,为了减少跨周实验的可比性问题,我会为每个系列实验固定一个“基准配置”,每次新实验都从基准配置出发,以基准的指标作为相对零点。这样一来,即使过了一个月再看“这一版相对当时涨了多少”,也有据可查。周报本身也承担了这个职责,它让我能够回看某一周对某个问题的判断,跟后来的结论做对比,这比单次实验总结有价值得多。
4.3 给同样坚持记录周报的人一点建议
如果你也在坚持做周报式的学习记录,我的建议是不要追求每天都写很多。定一个“最低更新量”,比如每周至少记录一个真正搞懂了的问题,哪怕是“某参数设错了”这种小问题。关键是每周都有一个让自己拍大腿的瞬间被留下来。
这种记录习惯的价值,会在三个月、半年后体现出来。很多当时觉得绕弯路的过程,回头看恰好是理解某个机制最完整的素材。我写周报的初衷本来是监督自己别偷懒,现在反而成了我做技术判断时的资料库,这倒是意外的收获了。
再分享一个这周刚体会到的小技巧:跑任何分布式训练之前,先小规模跑一遍“数据分布统计”,把每个batch的最大长度、batch内长度方差打出来看一眼。很多显存OOM和训练不稳定的问题,在这一步就能提前暴露。第三十九期周报就写到这里,下期见。