读完某AI团队开年放出的新论文消息第一反应不是又刷了一版刷榜指标而是标题里这个mHC确实戳中了我的兴趣点——残差连接这个大家每天都在写、却很少停下细想的基础结构居然还能挖出新东西。这几年做大规模预训练和长上下文模型我最大的感觉就是当网络深到几十上百层以后决定模型能不能稳定训下去的往往不是什么花哨的注意力变体而是残差路径上那一点点信息流动的比例。这篇想借mHC这个点把残差连接的来龙去脉、这几年业界尝试过的各种改进路线以及如果我自己要在代码里试一版mHC风格的改动会怎么设计实验、怎么避开那些一眼看不到的坑完整串一遍。不管你是做底层模型训练还是只要微调开源大模型理解残差连接在DeepNet和Transformer里到底在扛什么活都算得上是一项投入产出比很高的底层能力。1. 残差连接深度学习里最不起眼的关键结构1.1 从ResNet说起恒等映射为什么能救命先说个基础背景。残差连接Residual Connection这个概念最早大规模走进视野是靠残差网络在图像任务上的惊艳表现。核心公式很简单网络的某一层输出不是直接算出来的结果F(x)而是F(x)加一个输入本身x写成y F(x) x。这个加上x的操作最直接的作用是给梯度提供了一条高速公路。没有这条捷径时信息每经过一层变换梯度就要乘一次该层的雅可比矩阵深层网络里连乘几十次以后梯度要么指数级缩小到消失要么指数级膨胀到爆炸。有了恒等映射的通道反向传播时梯度可以绕过中间的复杂变换直接从深层传回浅层。这也是为什么ResNet能做到上百层还训得动而同样深度的纯卷积堆叠早就收敛不动了。从信息论的角度理解更容易恒等映射保证了每一层至少要保留输入的部分信息网络每一层学的是在已有表征基础上的增量修正而不是从零开始重新建模。这个设计哲学后来几乎蔓延到了所有深度学习架构里——LSTM的门控、Transformer的残差、甚至如今很多状态空间模型里的跳跃连接本质都是同一件事让信息流动更可控。1.2 Transformer里的残差为什么比ResNet更微妙到了Transformer时代残差连接的位置变讲究了。一个标准的Transformer Block大致是输入先进LayerNorm再进多头注意力或FFN然后与输入相加——这就是Pre-LN结构如果先做注意力/FFN、再做LayerNorm、再相加则是Post-LN结构。早期Transformer论文用的是Post-LN训练时额外加了学习率预热和梯度裁剪来维持稳定。但实践很快发现Post-LN在深层Transformer里非常娇气网络一旦上到十几层甚至几十层梯度尖峰gradient spike会频繁出现训练曲线动不动就发散。后来大家逐渐统一到Pre-LN也就是先归一化再变换再相加因为归一化后的子层输出经过残差直连时梯度能更稳定地传回浅层本质上牺牲一点表达能力换来训练的鲁棒性。这里出现的困境值得停下来想一想Pre-LN把训练稳定住了但代价是靠近输出的层能获得更强的梯度信号浅层反而容易吃不到足够的监督信息逐层之间的信号强度存在明显的不均衡。这类问题在深层大模型里会被放大——深层就那么几层掌握着绝大部分梯度浅层慢慢变成只做模板匹配。残差连接的新设计几乎都是冲着这个梯度/信号分配不均去的。2. 从ReZero到ResiDual几年间的残差改进演进线2.1 ReZero与LayerScale给残差分支加一个水龙头顺着时间线捋残差连接的各种变体已经有了一个很清晰的谱系新出的mHC放在这条线里会更好理解。先说ReZero。它的做法非常激进把残差分支整体乘上一个可学习的标量初始化为0。网络刚开始训练时等于恒等映射相当于所有层都关闭了变换随着训练进行这个标量逐渐变大子层才慢慢打开。这套思路让一万层的全连接网络都能训起来核心原因是训练初期的梯度传播极其稳定——因为一开始就没有任何变换在扰乱信号。LayerScale的思路更克制一些给残差分支乘一个固定的初始化值比如0.1到1e-4训练中也可以学习。它和ReZero的区别在于不要完全关死子层只是让子层初期的输出被压缩到一个很小的范围。这在ViT、Swin这类视觉Transformer里特别有效尤其是深层Patch Embedding之后叠加很多Block的场景没加LayerScale的深层ViT很容易在几百步训练后loss突然飞升。这两个方案的共同点非常明显它们都意识到残差分支的权重不是越大越好训练初期尤其不能太大。子层输出方差如果过高前向传播的特征范数会指数膨胀反向传播的梯度也同步爆炸。用一个标量把分支流量掐住相当于给信息流装了个水龙头。2.2 DeepNorm与ResiDual专门为大模型深度而生的残差设计再往后走残差改进开始和模型深度直接挂钩。DeepNorm的观察是Post-LN在深层训不动的根源在于残差结构导致的层间方差累积——每经过一层输出方差都会叠加子层的贡献层数越多方差越大梯度也随之失控。它的做法是给残差分支的缩放系数根据层数动态调整简单说就是层越深残差分支的分量越小整体方差被限制在一个可控范围。有了DeepNorm之后Post-LN重新能推上几百层这个设计在后来的很多深层多语言模型中都有影子。ResiDual则是另一种思路把Pre-LN和Post-LN并行接起来让两路信息同时流过Block并各自产生残差。仔细想挺有趣——Pre-LN状态稳定Post-LN表达能力强两者互有短板干脆同时保留。实验证明这种并联结构在深层Transformer里比单独用其中任何一种都更稳不过因为结构复杂工程上一直没大规模普及。2.3 残差结构调节的不是能力而是信息流分配比例把这几个方案放在一起能看到一个底层规律残差结构的好坏从来不是看单层变换的拟合能力而是看整个网络里信息流和梯度流的交通调度。ReZero和LayerScale在调节前向信号和反向梯度的绝对大小DeepNorm在按深度控制方差ResiDual在提供多条并行路径让不同层按需选择信息源。这也是我判断mHC值得仔细拆解的出发点。残差连接这块地方每一点改动都可能在非常深的模型里产生蝴蝶效应。如果某AI团队提出的mHC真是要对这个结构做文章那大概率也是瞄着深层训练稳定性浅层梯度增强多分支信息融合这几个方向中的一个或几个去。3. mHC可能是怎么设计的命名推演与预期收益3.1 命名拆解modified Highway Connection还是momentum-based Hidden ConnectionmHC这个缩写本身圈子里的习惯通常有两种解读一种是modified Highway Connection往高速公路网络的门控思想靠另一种是momentum-based Hidden Connection强调隐藏状态的动量式累积。这两种理解指向的设计细节不太一样但大方向都能落在残差连接变体上。Highway Connection的原始思想大家可能不陌生让网络对是否通过变换有自己的门控决定类似LSTM里输入门和遗忘门的结构。如果mHC取的是这个方向那它大概率在残差连接旁边增加了一条可学习的门控路径让网络自己决定每层有多少信息走直接映射、多少走变换映射。这么做的好处是比固定缩放更灵活缺点是门控本身引入了额外的参数量和优化难度。如果往momentum方向理解那就更有意思了。动量式的隐藏状态连接很可能是在残差路径上引入一个历史的隐藏状态累积项——不只用当前层的输入做残差而是把之前若干层的隐藏状态做一个指数滑动平均再和当前层的输出混合。这种设计在视觉上有点像用了动量的跳层连接能让浅层信息更平滑地传导到深层同时不显著增加参数。3.2 设计目标对着Pre-LN的两个痛点动手结合当前大模型训练的实际痛点我猜mHC首要瞄准的是Pre-LN结构下的两个毛病。第一个痛点是深层与浅层梯度不均衡。Pre-LN虽然稳但梯度信号明显偏向靠近输出的层浅层更新缓慢这会导致模型整体表达能力向高层集中。动量式的残差路径让浅层隐藏状态能被推向更深的位置等于给了浅层信息一条平滑的升级通道梯度也能顺着历史状态累积回流到更浅的层。第二个痛点是残差分支的流量完全由当前层状态决定缺少时间维度上的平滑。训练过程中如果某一层的输出出现尖峰普通残差结构会把这个尖峰直接传导到后续所有层即便有归一化也只能压一部分。如果残差路径里加入历史动量项单步尖峰的影响会被平均掉一部分前向传播的方差包络会更平滑。当然这些都是基于命名和已有研究规律的合理推演正式论文的细节我没看到完整版不能断言mHC就是这么设计的。但按这个思路去读任何一篇新的残差连接论文通常都能迅速抓住作者的意图值得先照着这个框架预判再拿论文里的实验去验证。3.3 拿到论文后先盯这三个信息点无论mHC具体实现是什么读一篇残差改进论文我建议先抓三个信息点比从头到尾啃文章效率高得多。第一看收益曲线的形状。一篇残差改进论文如果真有用训练loss曲线应该早期就展现出差异——通常是一开始掉得更快、中后期更平滑而不是最终指标小幅领先但过程曲线几乎重合。第二看替换位置。mHC到底是替换了每个Block内部那一小段相加还是替换了跨层的大尺度跳跃路径这两者的信息流差异很大也决定了复现难度。第三看超参敏感性。残差类改进最大的问题是换一个模型规模或者换一个学习率效果就完全绷不住所以论文里关于动量系数、初始化方式、是否随深度变化的敏感性分析比最后那张指标表格重要得多。4. 在自己的模型里实验mHC风格的残差连接4.1 先搭一个最小可运行的实现纸上谈兵没用真想评估这种新结构动手改代码是必须的。这里给出一个和mHC风格动量式历史残差接近的最简PyTorch实现只改动一个Block的前向逻辑不涉及任何额外的预训练权重跑通成本很低。import torch import torch.nn as nn class MHCStyleBlock(nn.Module): def __init__(self, d_model, sublayer, momentum0.9): super().__init__() self.sublayer sublayer # 通常是attention或ffn模块 self.norm nn.LayerNorm(d_model) self.momentum momentum self.hidden_prev None # 保存上一时间步的隐藏状态 def reset_prev(self): # 每个新序列或新batch开始时重置历史状态 self.hidden_prev None def forward(self, x): # x: [batch, seq_len, d_model] if self.hidden_prev is None: self.hidden_prev x y self.sublayer(self.norm(x)) # mHC风格当前输出与历史隐藏状态做动量式混合而不是单纯x y h self.momentum * self.hidden_prev (1 - self.momentum) * y self.hidden_prev h.detach() # 阻止梯度跨时间步回溯 return h这里有两个工程细节非常关键稍微疏忽就会得到一堆NaN。第一个是hidden_prev的状态管理。训练时每个batch的序列长度可能不一致必须在新sequence开始时调用reset_prev()否则上一个batch的隐藏状态会污染当前batch。分布式训练时更麻烦序列并行场景里每个rank保存的历史状态边界要对齐不然流式处理时会出现错位。第二个是detach()的处理。动量项如果直接作为计算图的一部分参与反向传播会形成一个跨时间步的循环依赖导致梯度被反复展开轻则训练变慢重则由梯度爆炸引发NaN。我在早期实验中试过不detach效果是loss确实掉得快但10个step之内就会彻底发散。保留历史状态但切断它和当前步的梯度链路是正确的开放。4.2 替换残差连接之后实验应该从这四个维度观察残差结构改动不像换个激活函数单看最终指标一片模糊要系统性地观察训练过程。我建议在复现mHC风格改动时固定四个观察维度的记录。第一个维度是训练loss曲线的形状重点看前几百步的下降斜率有没有明显变化。残差结构改动如果有效通常会让前期收敛明显变快——这是梯度通路变顺畅的直接体现。第二个维度是梯度范数的分布尤其要看最底层和最顶层的梯度比值。普通Pre-LN模型里浅层梯度往往比深层小一两个数量级如果mHC真的改善了浅层梯度流通这个比值应该会拉近。第三个维度是前向传播的层间输出方差。残差改动的核心目标之一就是控制特征方差随深度稳定增长可以每隔一段训练步数记录各层输出的L2范数均值看包络是否平滑。第四个维度是各层表征的相关性。如果残差加动量之后浅层和深层的表征过度趋同模型深度就成了虚设这个信号需要靠PCA或中心核对齐之类的方法来检测。4.3 一个可复用的对比实验模板更具体一点我提供一个自己常用的A/B实验模板在单卡8卡规模下就能跑出有效结论。第一步固定模型尺寸。建议用8层到16层的Transformer隐藏维度512到768。别一上来就在7B模型上测残差类改动在大模型上的效果可能和小模型完全相反——小模型里能加速训练的设计大模型里可能直接训练崩塌需要单独验证。第二步固定优化器超参数用一致的AdamW配置、学习率、预热步数、权重衰减只改动残差结构避免引入混淆变量。第三步固定训练步数和数据顺序最好用同一个随机种子这样可以观察逐层梯度范数的精确变化。第四步先在小规模上跑通再逐步增大depth观察收益是始终存在还是仅仅在小模型上有效。这套流程跑下来一般来说2000到5000步的短训练就能看出残差改动的有效性趋势没必要一开始就开一个大训练。另一个注意点是如果模型里已经有LayerScale或者DeepNorm这类残差缩放模块先关掉它们再测试mHC多种残差改进叠加时效果往往不是增强而是互相抵消信息流会被反复压扁。5. 实验mHC过程中可能踩到的坑5.1 前200步很稳2000步后突然发散的假稳定这是我在做各种残差改动时遇到最频繁也最坑的现象。前几百步曲线下降得漂亮梯度范数平稳正当你以为新结构有效时训练突然在数千步后开始震荡loss弹上去就下不来。事后分析原因基本都指向动量式结构和AdamW之间的耦合动量为梯度估计引入了额外的惯性而AdamW本身就依赖一阶矩和二阶矩的平滑估计两者叠加会让学习率实际上被放大了。应对策略有两个。一是把动量系数调低从0.9降到0.5乃至0.3尽量让残差路径接近常规结构。二是观察AdamW的二阶矩variance统计量如果发现某些层的v值异常偏小说明这些层几乎没有梯度变化动量结构可能正在把浅层梯度吸干。另外值得强调的是这种假稳定和具体随机种子高度相关同一个结构换个种子可能300步就炸所以在小规模上至少要跑三个不同种子再做结论。5.2 初始化方式和数值精度会在深层被大幅放大残差类改动里初始化方式的影响比任何超参数都更大。动量式残差路径的初始状态默认是当前输入这等于第一次前向时网络的行为和普通残差完全一致一旦进入第二层起历史状态和当前输入就出现偏差这个偏差在深层网络里会被逐层积累。如果初始化时各层参数的方差偏大累积偏差会在前向传播中指数级放大直接表现为loss曲线在前几步出现一个尖峰。另一个精度问题是混合精度训练里的hidden_prev长期累积误差。在float16下做指数滑动平均一个极小的舍入误差经过数百步累积后会变得不再可忽略最终可能改变有效表示。bf16的指数范围更大问题要好一些但尾数精度低意味着累积操作的相对误差依然不小。我建议在做这类实验时对历史状态hidden_prev采用float32存储即使参与计算的张量是bf16历史累积值单独用高精度保存能显著减少无声的Bug。5.3 训练有效、推理却用不上的模型前后不一致残差连接改动里还有一个容易被忽视的问题训练时引入的历史状态在推理时能不能稳定复现。如果是自回归生成任务推理时每一步的输入是上一步的输出和训练时的分布天然不同动量残差会让分布偏移被历史状态平滑掉还是放大完全取决于具体实现。如果推理时仍然需要保留整条序列的历史状态显存占用会比普通残差结构高出一截——尤其在做长上下文推理时这个额外开销可能直接让吞吐量不可接受。实际问题很现实要么把历史状态设计成可丢弃的推理时只用当前步的隐藏状态要么接受额外的状态缓存开销在长序列推理时用分段截断不让历史状态无限增长。我在实验里用过窗口式动量残差即只保存最近K步的隐藏状态做平均效果上和全量动量差别不大工程开销却小了很多。如果mHC在后续复现中出现显存瓶颈这算是一个现成的缓解方案。最后说一点个人体会。残差连接这块地方看起来是一行代码的事实际牵动的是整个网络的梯度流、方差包络、信息传递效率牵一发动全身。某AI团队拿它单独发论文本身就是对基础结构研究价值的一次提醒——在大模型参数规模趋同的背景下真正能拉开训练效率和效果差距的往往就是这些没人愿意细看、却在每一步前向和反向中都起作用的角落。如果你也想在自己的模型里试试这类改动我的建议就一句别急着上大模型先拿小模型把梯度范数、层间方差、浅层梯度占比这三个指标摸清楚再谈放大规模的事。这套方法比盲目追新结构有用得多。
阅读完成 · 觉得有帮助?