大模型训练底层逻辑:递推、递归与三明治结构
我经常遇到底下有人问“我用某个预训练模型跑了几个任务loss也降了但让我自己改个结构就蒙了到底该从哪入手”这个问题很典型。很多人不是不聪明而是把大模型训练当成黑盒了只会调包调用从来没在底层把“数据如何流动、梯度如何传播、网络为什么稳定”这三件事揉碎了看。我在这个行业里折腾了好几年踩过的坑比调过的参还多最后发现不管模型多大、多花哨训练底层的逻辑都可以压缩成三个词递推、递归、三明治。今天这篇不讲虚的就围绕这三个词把大模型训练的底层逻辑讲透顺便回答一个更实在的问题你手里的显存、时间和头发到底都消耗在哪里。不管你是在读的学生、刚转行的算法工程师还是已经开始接触微调、训练部署的从业者只要你想从“调包侠”变成“能拆包、能修包、能造包”的人这篇文章都值得看完。我会把前向传播怎么一层层递推下去、反向传播怎么递归式地回传梯度、以及深度网络为什么必须做成“三明治”结构这件事掰开揉碎讲清楚。最后还会给出排查训练异常的实操速查表保证你能带着干货走。1. 先厘清递推、递归、三明治到底在说什么先说结论方便你建立地图。递推对应的是前向传播输入数据进入网络后每一层的输出都会变成下一层的输入这样一层接一层地传下去本质上是复合函数的逐步求值。递归对应的是反向传播要算每个参数的梯度就得从最终的损失出发利用链式法则一层层往回调这个调用自身来求上游梯度的过程就是递归。三明治对应的是网络的结构设计为了让前向递推不失控、反向递归不消失人们把残差连接、归一化、激活函数这些“夹层”组合成重复的稳定块像两片面包夹着一块肉一样让信号可以在深层网络里平稳流动。这三个词不是三种独立的技术而是从三个角度观察同一个训练过程。你训练一个模型前向是在递推计算反向是在递归求导框架里堆叠的那些 Block则是在用三明治结构保证你干得动这件事。接下来我分别展开。1.1 调包侠的玻璃墙先说个观察。很多人调包跑模型的时候看着训练日志里那个不断下降的loss会误以为自己掌握了大模型训练。但只要稍微出点异常这层滤镜就碎了loss突然变成NaN你只能去论坛上抄各种玄学方案显存爆了不知道是激活占用还是梯度占用只能把batch调小再试试模型层数一加深训练直接不收敛你又只能安慰自己是“数据不好”。这些困惑的背后其实是三层知识缺口第一不清楚前向传播时每一层到底产生了什么中间状态第二不清楚反向传播时梯度是按照什么路径、以什么幅度流回来的第三不清楚网络结构里的某个连接、某次归一化到底是用来干什么的。这三层缺口恰好就对应递推、递归和三明治。所以我说这不仅是术语是理解训练过程的思维脚手架。1.2 一张思维地图为了避免让你在后面的细节里迷路我先把三重逻辑和训练流程的对应关系用一张表放出来。这张表不复杂但非常实用你后面排查问题时可以直接回看。训练阶段底层逻辑核心问题关键资源消耗前向传播递推如何逐层计算特征表示中间激活值显存反向传播递归如何逐层计算参数梯度梯度和计算图显存网络结构三明治如何让深层信号稳定流动残差、归一化、激活组合优化更新三明治协同如何用梯度更新参数优化器状态显存有了这张地图你再看模型训练就不会觉得是一团乱麻了。下面我按顺序一层一层讲透。2. 递推逻辑前向传播就是状态的一层套一层2.1 递推式的前向传播你可以把前向传播理解成一条流水线原料从一头进去经历若干道工序每一道工序产出一个半成品再传给下一道。数学上这就是递推关系。拿最简单的多层感知机举例假设第l层的权重是W_l、偏置是b_l、激活函数是σ那么第l1层的输出就是h_{l1} σ(W_l h_l b_l)这个式子看着简单但它是大模型前向传播的老祖宗。你把l从0一路推到L最后得到的就是网络的输出。哪怕换成Transformer逻辑也一模一样h_{l1} h_l Attention(LayerNorm(h_l))每一层的输出都是下一层的输入整体还是一个递推过程。所谓“深层网络”就是把这条流水线拉得很长。递推有两个很有意思的性质。第一它是一种顺序依赖必须等前一层算完后一层才能开始所以你很难把一个超深模型的所有层完全并行化。第二每一层的递推都不只是算了个输出还产生了一个“中间状态”也就是该层的激活值。这个状态后面会被反向传播引用所以它不能随便丢掉。这俩性质会在后面直接决定你的显存和速度。2.2 递推决定了你的显存峰值你以为显存主要花在模型权重上其实在大规模训练里中间激活值的显存占用往往比权重还猛。原因很简单递推过程要求你把每一层的激活都保留下来这样反向传播时才有办法算局部梯度。权重只有一份但激活值有“层数 × batch大小 × 序列长度 × 特征维度”这么多份。我举一个具体例子。假设一个7B规模的Transformer模型权重在FP16下大约是14GB这还在可以接受的范围内。但如果你在训练时用到了4K序列长度、较大的batch中间激活值轻轻松松就能吃掉超过20GB显存。而且很多人在写自定义结构时还会不自觉地创建大量临时张量进一步加剧递推过程中的显存开销。所以每次别人问我“为什么显存总是不够”我第一反应就是先看你的中间状态而不是先去砍模型参数量。2.3 实操心得用Shape推演递推信息流递推过程能不能跑通最容易暴露问题的地方是形状不匹配。我在初学阶段经常犯一个低级的错自己拼一个模块前向一跑就报维度错误。后来我养成一个习惯先做一次单样本的“形状推演”再写业务逻辑。具体方法是拿一个很小的输入跑一次前向然后逐个打印每个关键算子的输出张量形状。你不需要多复杂的工具直接在核心模块后面加一行断言就行。x torch.randn(2, 32, 768) # batch2, seq32, dim768 for block in model.layers: x block(x) assert x.shape[-1] 768, f维度错啦: {x.shape}这个习惯看起来笨但极其有效。它逼着你把递推关系在心里完整走一遍。每经过一个线性层、一个注意力层、一个归一化你都要问自己形状变没变通道数符合预期吗序列维度和特征维度有没有被搞混很多线上训练报错的根源其实在写代码的那一刻就已经埋下了只是大部分人懒得在模型跑大任务前先做一遍这种低成本验证。2.4 递推也有隐患状态坍缩与分布漂移递推不是无代价的。普通线性变换反复嵌套时信号的方差和均值会不断漂移如果激活函数还是Sigmoid这种有饱和区的那么多层递推之后特征会慢慢挤到饱和区域附近梯度接近于零。这种现象你可以叫它“状态坍缩”也可以叫“激活退化”。反正结果就是网络越深输出变得越不敏感。有人可能会问那为什么大模型好像没有这个困扰因为人家不是裸奔的递推而是带了“三明治结构”的递推。残差连接和归一化就是专门来治状态坍缩的。这个我们放到后面第4节细说你先记住一个结论纯递推是理想模型现实中递推必须加约束才稳定。3. 递归逻辑反向传播是计算图上的递归求导3.1 从损失到每一层链式法则有多大威力前向递推只是把数据算了一遍真正让模型“学习”的是反向传播。反向传播的本质是递归求梯度。你定义了一个损失函数L它依赖于最后一层的输出而最后一层的输出又依赖倒数第二层倒数第二层又依赖更前面的层。这种依赖关系天然是一棵“计算树”树的根是损失叶子是参数。为了求某个参数的梯度你要从根出发沿着依赖关系一层层往回推。写成递归形式就是这样∂L / ∂W_l (∂L / ∂h_L) × (∂h_L / ∂h_{L-1}) × ... × (∂h_{l1} / ∂h_l) × (∂h_l / ∂W_l)你看这里面有多个因子的连乘。如果你想在代码里实现这个连乘最自然的方式就是定义一个递归函数算第l层的梯度时先递归地拿第l1层的梯度再乘以本地的雅可比矩阵。递归逻辑就这样出现了。3.2 为什么偏偏选择反向递归来一场正向遍历的代价对比理论上你也能用正向模式算梯度也就是从输入出发一碰到参数就算偏导。但那样的话每对输入和参数组合都要单独跑一遍传播复杂度正比于参数数量代价高到无法接受。反向模式就不一样它先从根到叶子做一次前向递推保存好所有中间状态然后从根开始往回递归一次就能把全部参数的梯度同时算出来。一句话总结反向模式用相对较小的代价换来所有叶子的梯度。我举个生活化的类比。想象你要调查一个巨大的家族谱系正向模式就像每个家族成员都挨个跑去找祖先规模大到爆炸反向模式则像先把家谱画出来然后从最老的祖先开始给每个后代派发一条消息让他们一次性知道自己的来历。画家谱对应前向递推派消息对应反向递归。后者明显优雅得多。3.3 把递归写清楚局部梯度乘以上游梯度很多人背过链式法则但真正自己写反向传播时还是会迷糊。其实你就记七字诀上游梯度乘局部梯度。当前节点的上游梯度是它之后所有节点的梯度汇总当前节点的局部梯度是当前节点的输出对其输入的导数。两者相乘就得到更前一层需要用的梯度。我从实践中推荐一个写递归反向的通用模板def backward(node): if node is loss: return 1 total_grad 0 for dependent in node.dependents: upstream backward(dependent) local node.local_grad(dependent) total_grad upstream * local node.grad total_grad return total_grad这是一个递归思路的示意真实框架不会用Python递归实现因为图深了会爆栈。它们会用拓扑序迭代来做用栈显式管理。但你想理解逻辑用递归写最清晰。这也提醒你递归是一种思维模型迭代才是工程实现。两者不矛盾。3.4 梯度消失与梯度爆炸递归的天然脆弱性递归求梯度有一个躲不开的问题梯度是连乘出来的。如果每一层的局部梯度都小于1那么连乘多次后梯度会指数级缩小这就是梯度消失如果局部梯度大于1梯度会指数级放大这就是梯度爆炸。你在训练深层模型时经常看到loss突然变NaN大概率就是递归路径上有某个地方梯度爆炸了。我用一个极端例子帮你想清楚假设每一层局部梯度都是0.550层连乘后梯度大约是0.5^50这个数小到浮点数都难以精确表示。反过来如果每一层局部梯度都是1.550层连乘后大约是1.5^50直接爆掉。所以你看模型深到一定程度能不能稳定训练关键不在前向递推的精度而在递归这条路径上的尺度控制。这也是为什么几乎每个框架都会提供梯度裁剪因为工程上确实会遇到这种失控场景。3.5 工程优化用“递归重算”换取显存既然反向递归很吃中间状态那能不能不存那么多中间状态可以这就是“激活重计算”也叫梯度检查点。做法很直接正常前向时把某一段的激活值丢掉反向递归回到这个区域时再临时把前向重新跑一遍把需要的中间状态算回来。听起来有点浪费算力但它确实能把超大模型塞进更小的显存里。我实际用下来的感受是开启梯度检查点之后显存占用可能下降30%到50%代价是训练时间增加20%到30%。对很多卡着显存线跑训练的团队来说这个时间换空间的交易非常划算。更精髓的是它其实是在“递归计算梯度”的过程中额外加了一层递归为了求前向状态又递归式地求了一次前向。你把这两层逻辑都搞通以后再看框架里那些checkpoint、recompute相关的开关就不会觉得它们是什么黑魔法了。4. 三明治逻辑深层网络的稳定结构与“夹心”设计4.1 残差连接给梯度开一条“直达高速路”前面提到纯递推会让状态坍缩递归会让梯度消失。怎么破答案就是三明治结构。你可能听过残差连接y x F(x)这个公式短得不能再短但它背后是一个深刻的三明治思想把F(x)夹在输入x和输出y之间同时保留一条从x直接通往y的捷径。相当于两片面包中间夹着一块肉而“面包片”本身就是恒等映射。为什么这个结构能缓解深层训练的退化问题因为前向递推时就算F(x)接近零x还是能原封不动地传到后面这就保证了信息不会坍缩反向递归时就算F(x)这一路的梯度接近零x这一路的直通梯度依然存在这就保证了梯度不会消失。你可以把“直通梯度”理解为高速公路的应急车道主路堵死时它还能把车送回来。Transformer的每个子层几乎都默认带残差原因就在这里。4.2 归一化的“位置学”Pre-Norm跟Post-Norm到底差在哪三明治里的“肉馅”通常不止是线性变换还包括归一化。这时候就出现了一个经典问题归一化放在哪里放在残差块之后叫Post-Norm放在残差块之前叫Pre-Norm。两者都是三明治但夹层顺序不同效果差异很大。Post-Norm的典型结构是LayerNorm(x F(x))也就是先融合再归一化。早期Transformer常用这种结构但它对深度很敏感深层训练时需要非常小心地调学习率和初始化。Pre-Norm的典型结构是x F(LayerNorm(x))也就是先归一化再变换。这个设计现在被大量大模型采用因为它在主干路径上直接传递了原始输入归一化只是为F内部提供稳定分布主干路径不受归一化缩放影响训练稳定性明显更好。我做过对比实验同样一个深模型用Post-Norm可能训到一定深度就开始发散换到Pre-Norm以后能稳定收敛。但Post-Norm也不是一无是处它的表达能力在某些任务上可以更强。所以不要死记结论要理解这背后的逻辑归一化是夹层夹的位置决定了“主干捷径”受到多大的干扰。你把“位置学”想通了以后换结构就不会两眼一抹黑。4.3 更多三明治组合激活函数、初始化与优化器配合除了残差和归一化激活函数和权重初始化也可以看出三明治的影子。最经典的组合是“线性层 - 激活函数 - 线性层”这本身就是一种三明治两个线性变换之间夹着一个非线性激活。如果没有这个夹层两个线性层等效于一个线性层你叠再多都没用。所以激活函数不是可有可无的装饰它是让递推从“线性复制”变成“非线性组合”的关键。权重初始化也遵循类似的稳态度量。它要保证前向递推时各层激活方差保持在合理范围同时反向递归时各层梯度方差也不要膨胀或收缩。Xavier初始化、He初始化本质上都是在根据层的大小去调这个方差尺度。从我的经验看很多自定义模型的训练不顺不是学习率问题而是初始化压根没匹配好激活函数。比如你用ReLU家族却按Sigmoid的推荐策略初始化梯度递归的尺度一开始就偏了后面怎么调都别扭。4.4 架构设计里反复出现的“块加跳跃”模式理解了残差和归一化你会发现一个现象深度学习里的有效架构几乎都是“块加跳跃”的重复组合。卷积网络有残差块Transformer有自注意力加前馈网络加残差MoE在专家之间也加门控和残差。你甚至可以把一次训练循环看成一个大三明治前向递推做一次夹心计算反向递归做一次夹心求导优化器更新再夹一次。这个模式之所以如此普遍并不是因为大家抄来抄去而是因为它同时解决了递推失稳、递归梯度和非线性表达三个问题。你以后设计任何新模块先问自己一句这个模块的主路径上有没有保留一条干净的恒等捷径如果没有那它大概率在深层场景下会出事。5. 三重逻辑协同训练全景图与异常排查手册5.1 三者如何咬合训练一个模型本质上是把三重逻辑串成一条流水线。前向递推负责把输入变成预测反向递归负责把预测误差变成每个参数的梯度三明治结构负责让这个过程在几十层、上百层之间依然稳定。我习惯把前向递推想象成水沿着管道流动反向递归想象成水流返回源头去探测每一处管壁的受力而三明治结构则是管道里加装的稳压阀和旁路。没有稳压阀水流到后面早就不成样了没有旁路一旦某处堵住整个循环就断了。理解了这三者的咬合关系你再去看训练曲线就会有一种“透视感”。loss下降说明前向递推在逐步逼近目标任务梯度范数稳定说明反向递归的尺度没失控你改个结构后模型瞬间不收敛了多半是三明治结构里某个夹层的位置被破坏了。这种能力不是天赋是反复推敲底层逻辑后自然获得的。5.2 用显存优化案例反推三重逻辑显存异常是最常见的训练问题之一我用它来演示一下三重逻辑怎么协同排查。前面说了中间激活值来自前向递推计算图来自反向递归的需求而结构里的不少临时张量来自三明治块的内部实现。当你显存告急时可以按这个顺序逐个开刀先开梯度检查点用“递归重算”换显存再检查是否在循环里创建了不必要的临时变量减少递推过程的峰值占用最后审视三明治块里有没有多余的拼接、切片操作能合并的算子尽量融合。我印象很深的一次经历是给一个长序列训练任务做显存优化。一开始我没开重计算4K序列长度直接让激活值爆满。开了梯度检查点之后显存立刻松动再通过算子融合把几个逐元素操作合并掉总算把训练batch提了上去。整个过程看下来前向递推决定了你会产生多少激活反向递归决定了你需要保留多少计算图三明治结构决定了你在每个块内额外制造多少中间量。三者都摸透了你才能系统性地优化而不是靠“调小batch”这种一刀切的土办法。5.3 常见训练异常排查速查表我把实操中踩过的高频坑整理成一张速查表希望能帮你快速定位问题。这张表不一定能覆盖所有情况但能覆盖80%的入门级事故。异常现象常见原因主要底层逻辑优先排查方向loss不下降前向递推时特征分布坍缩梯度信息不足递推检查激活统计确认初始化是否匹配激活函数loss突然变成NaN反向递归时梯度爆炸或FP16精度溢出递归开梯度裁剪检查损失缩放减小学习率深层网络不收敛三明治结构中缺少残差或归一化位置不当三明治加残差连接对比Pre-Norm与Post-Norm激活值全部趋同递推状态坍缩非线性表达失效递推三明治换激活函数加入归一化和残差显存OOM中间激活值过多或计算图保留过多递推递归开梯度检查点合并算子减小batch训练速度极慢反向递归的递归链太长或重计算比例过高递归调整重计算位置检查是否有不必要的多次反传排查的时候我的建议是一次只改一个变量改完跑一小段训练看曲线。最怕的是同时改学习率、改结构、开重计算那样一旦出了好或坏的结果你都说不清是哪个操作贡献的。工程上这叫控制变量数学上这叫保持实验可解释性。别嫌慢排查慢才是真的快。5.4 从“调包”到“拆包”的三步进阶法很多人问我怎么才能摆脱只会调包的状态我的答案不是让你远离框架而是让你学会“拆包”。我把它拆成三步你可以按顺序练习。第一步用纯Python或NumPy手写一个两层网络的前向递推和反向递归不要用自动微分。这一关过了你就真正理解了链式法则是怎么在每一层乘上梯度的。第二步在你的真实模型上给每一层挂forward hook和backward hook跑一个batch打印每一层的激活均值和梯度范数。看着这些数字你会直观感受到递推和递归过程中哪些层是瓶颈。for name, layer in model.named_modules(): layer.register_forward_hook(lambda m, inp, out: print(name, out.mean().item()))第三步主动做结构对比实验把残差去掉训练一个深模型看它怎么退化把Post-Norm换成Pre-Norm看训练曲线怎么变化。这个过程会让你把三明治逻辑焊死在脑子里。做完这三步你再去看任何大模型结构源码都会觉得它们不再是天书而是一层一层可以拆开检查的积木。6. 写在最后我踩过最值的坑如果要我说一个个人体会那就是训练样例跑出来的那一刻你只完成了20%另外80%是你能不能解释整个过程中的每一个关键现象。我最初也经历过“模型跑通了但不知道为什么跑通”的状态后来逼着自己做了大量前向、反向的打印和结构消融实验才慢慢建立起直觉。你会发现以前遇到loss不降只会去调学习率现在你第一时间会想“前向递推有没有坍缩”以前遇到NaN只会重开训练现在你会想“反向递归的梯度在哪个环节爆掉了”。最后再分享一个我特别习惯的动作不要只在训练结束后看曲线要在训练过程中每隔几百步把各层梯度范数打出来。一旦发现某几层的梯度范数比其他层小数个数量级就说明递归路径上这个地方是“卡点”。你可以考虑在那附近加一条捷径、换一种归一化位置或者给那层单独调一下初始化。这些小操作听起来朴素但实测下来它们比你在网上搜各种玄学参数管用得多。大模型训练不是魔法它是一套可以用逻辑推演到底的工程体系。递推让你知道信号怎么走递归让你知道梯度怎么来三明治让你知道结构怎么撑住深层网络。把这三重逻辑吃透以后你再看那些动不动几十层上百层的大模型就不会再觉得它们是遥不可及的黑盒而更像一个你能读懂图纸、随时能修一修的复杂机器。希望这篇长文能帮你把底层的那几根骨头摸清楚也欢迎你在实操中带着自己的新体会回来交流。