☰
从零构建大模型:架构选型、训练循环与稳定性调优全指南
2026/10/19 3:53:19 网站建设 项目流程

从零构建大模型这件事,听起来像是只有大厂实验室才能碰的活儿,但实际上,随着开源生态和硬件方案的成熟,个人开发者或者小团队已经有条件把一个千万到十亿参数级别的模型从空白跑到收敛。这个系列前两章,如果按常见路线走,一般会覆盖环境搭建、数据准备和预训练的基础流程。到了第三章,就进入真正决定模型“性格”和“上限”的阶段:网络结构怎么定,训练循环怎么写,稳定性怎么调,以及遇到问题怎么定位。

这一章我会尽量把这些事情讲清楚。内容主要面向已经跑通了一个小型模型的读者,也就是你知道怎么用框架开一个训练脚本、能处理基础的张量操作,但对大模型特有的那些“工业级细节”还不太熟悉。读完这一章,你应该能独立搭建一个具备完整训练流程的模型雏形,并且知道在训练出现异常时,该从哪些维度去排查。

1. 第三章的定位:从“能跑”到“可控”

在开始堆代码之前,先把这一章在整个从零构建大模型系列里的位置盘清楚。系列前两章一般是把数据集洗干净、把训练框架搭起来、能跑通一个最小的模型。但“能跑”和“可控”之间隔着一条很大的鸿沟,这也是为什么要专门写第三章的原因。

1.1 为什么“能跑”不等于“可用”

我见过不少人卡在这个阶段:模型训练起来了,loss 也在往下降,但生成出来的内容完全不可用,或者训练到一半突然崩溃。问题通常不是出在某一行代码,而是出在整个训练系统的设计上——结构选型没有依据、超参数没有配套调整、异常处理没有预案。

第三章要解决的问题,就是把一个大模型的构建从“先写个网络试试”变成“按照工程逻辑逐项落地”。这里的关键词是“可控”:loss 的下降曲线是可控的,显存占用是可预估的,训练中断后可以恢复,出现异常时能从日志和监控指标里快速定位。

1.2 这一章覆盖的核心范围

具体来说,从零构建大模型(第三章)会围绕四条主线展开:

第一,架构选型。在Transformer的大框架下,你想要什么样的模型:是标准的Decoder架构,还是带前缀的Prefix LM,或者带因果注意力的变体。这个选择直接决定后续所有训练的配置方式。

第二,核心组件实现。RMSNorm、旋转位置编码、SwiGLU激活函数、注意力计算的mask逻辑,这些不只是“照着论文抄一遍”,而是要理解每个组件为什么需要这样设计,以及在实现时有哪些容易被忽略的细节。

第三,训练循环与稳定性控制。数据怎么按批次组织,梯度怎么累积,学习率怎么调度,混合精度和梯度裁剪怎么配合,checkpoint怎么存储最安全。这一部分是最容易“看起来没毛病、跑起来问题一堆”的地方。

第四,问题排查与调试。loss 不降、loss 震荡、显存溢出、训练速度越来越慢,这些经典问题都有固定的排查路径。我会把我实际踩过的一些坑放进来,包括一些开头根本想不到的原因。

四条线走完,你手里的就不再是一个“能跑”的脚本,而是一套具备容错能力、可观测、可恢复的训练系统。这也是第三章在整个系列里最核心的价值。

2. 架构选型与核心组件拆解

大模型构建的第一步,是把“Transformer”这个大概念落到具体的设计决策。很多初学者会把时间花在调整超参数上,但架构选型一旦错了,后面怎么调都别扭。

2.1 架构选型:Decoder-only 为什么是默认选择

目前从零构建大模型,最大的概率是选择 Decoder-only 架构,也就是类似 GPT 系列的结构。这个选择背后有很实际的逻辑:Encoder-Decoder 适合序列到序列任务,需要在编码器和解码器之间维护两套注意力机制,编码复杂度更高,对于大多数“续写式”的生成场景来说反而用不上;而纯 Encoder 结构擅长理解任务,但不太擅长生成自然语言。

Decoder-only 的优势在于统一:预训练任务只用一个“预测下一个词”的目标,不管是做对话、写文章、代码补全还是分类任务的顶层适配,最终都在同一个语言模型上完成。你不需要为不同任务设计不同模型,只需要在推理时组织不同的 prompt 或接入不同的下游头。

从零构建的角度来说,Decoder-only 还有个实际好处:因果注意力只需要实现一套,训练和推理的代码路径高度一致,代码量更少、出错面更小。对于个人和小团队而言,少一个模块就少一类 bug。

架构选型这块,我的建议是:除非你有非常明确的特定任务需要(比如对标 BERT 的纯理解场景),否则优先选择 Decoder-only 的因果语言模型。这也是模型构建这个事情上,少数可以“闭眼选”的决策之一。

2.2 核心组件:RMSNorm、旋转位置编码与 SwiGLU

架构选定之后,就要实现具体的网络组件。现代大模型的标准配置通常包含三个关键组件,它们在不同论文里的叫法略有差异,但功能是稳定的。

第一个是 RMSNorm,也就是 Root Mean Square Normalization,用来替代传统的 LayerNorm。LayerNorm 需要计算均值和方差,而 RMSNorm 只计算均方根,只做缩放、不做中心化。节省的那点计算量是一方面,更重要的是训练稳定性表现更好。在深层网络里,每个子层输出前做一次 Norm,能避免激活值在反向传播时产生过大的方差漂移。

实现 RMSNorm 时要注意一个细节:参数初始化的 scale 和 epsilon。epsilon 一般取 1e-5 到 1e-6,太小可能导致除零不稳定,太大会损失归一化精度。权重初始值一般全设为 1,这个没必要用随机初始化。

第二个是旋转位置编码,也就是 RoPE。它把位置信息通过旋转矩阵注入到注意力计算中,使得向量在旋转后能天然携带相对位置关系。实现的时候有几个易错点:head_dim 必须是偶数,因为旋转是按 2 个维度一组操作的;基频 base 的设置影响位置编码的有效长度,一般用 10000,但如果你要处理超长文本,可以考虑调整到 100000 甚至更高。

RoPE 实现出来后,最好写一个简单的测试用例:拿绝对位置为 1 和 2 的两个向量,计算旋转后的注意力分数,确认分数差异与相对位置相关、与绝对位置无关。这种自检能省掉后面不少追查 bug 的时间。

第三个是激活函数 SwiGLU,也就是门控线性单元。传统 Transformer 用的是 ReLU 或 GELU,而 SwiGLU 引入了门控机制:把输入分成两路,一路经过激活函数后再与另一路逐元素相乘。这个设计的直觉是,让网络学会动态调节信息的“闸门”,只让量级相关的信息通过。

实现时要注意隐藏层维度的设定——SwiGLU 会有一个扩展因子,和标准 FFN 相比,hidden_size 需要相应放大或缩小,具体取决于你是否单独增大中间维度。很多实现里会把 FFN 中间维度设置为 hidden_size 的 8/3 倍,这个细节掉了,参数量就会出现明显偏差。

2.3 注意力机制的 mask 逻辑与 KV Cache

注意力计算是模型的核心,也是实现时最容易出问题的地方。两类 mask 尤其要搞清楚。

第一类是 padding mask。因为训练数据是分批 pad 到等长的,需要把 padding token 对应的注意力分数置为负无穷。处理不当的话,模型会去关注那些无效的填充位,影响训练质量。

第二类是因果 mask。Decoder-only 的核心约束是当前位置不能看到未来的位置,所以需要生成一个上三角为负无穷的 mask 矩阵。在 4D 注意力矩阵的场景里,这两个 mask 要做广播对齐,padding mask 和 causal mask 如果维度对不上,排在批次和头维度上的运算就会报错,或者更糟糕的是,不报错但结果内含 bug。

KV Cache 是推理阶段会频繁用到的机制,但很多人在训练阶段不怎么关注。实际上在训练起步阶段就把 KV Cache 的接口设计好,后面想做模型部署、接口对比就会省事很多。训练时不需要存 Cache,但推理的生成函数会用到,在设计模型类的时候,把 cache 参数作为 forward 的可选项,后面不用改网络结构就能支持推理加速。

3. 训练循环搭建与关键技术实现

模型结构就位了,接下来就是让它跑起来去学语言规律。这个环节,目标不只是“loss 能降”,而是要建立一套可观测、可恢复、可复现的训练系统。从零构建大模型走到这一步,才真正进入工业落地的节奏。

3.1 数据组织和迭代策略

很多从零构建模型的初学者会低估数据组织的重要性。模型结构不好,可能训练一两天就能发现;数据组织有问题,比如序列长度分配不合理、padding 浪费太多,可能整个训练周期做完,你都不知道模型效果不佳是数据问题、结构问题还是超参问题。

训练数据要按 token 而不是按字符来组织。每个样本先分词,再切分成目标长度,比如 1024 或 2048 token 作为一条训练样本。切分时我推荐给样本上下文加一个 overlap,相邻样本之间重叠一小段,避免把一个完整语义单元硬生生切断,也能增加数据利用率。

在一个数据批次里,尽量保证序列长度相近,减少 padding 浪费。常见做法是构建样本时按长度分桶,再用贪心或近似装箱算法把长度相近的样本分配到同一个批次,这样 padding 率能压到 10% 以下。批次构造函数里还要顺手做一个信息统计:批量中的平均长度、padding 比例,这些会在后面排查训练效果时派上用场。

3.2 训练循环的编写要点

训练循环本身不长,但有几个细节是“写错不报错、跑起来才知道不对”的隐形坑。

第一个容易忽视的地方是注意力分数和 logits 的关联。模型前向传播最后会输出 logits,计算 loss 时会先做 log_softmax。如果你的数据标签本身就是 token id,那用交叉熵直接算就行。这里要确认两个对齐:预测 logits 的序列长度和标签序列长度是否一致;标签里的 padding token 有没有在 loss 计算时被 mask 掉,否则会让模型去学习“预测出 padding token”。

第二个是梯度累积。显存不够时,可以用 gradient accumulation 模拟更大 batch。这个机制本身不复杂,但落实的时候要小心 loss scale 的问题——累积步数为 N,loss 要除以 N 再往回传,这一步有些人会漏。归根结底,loss 的 mean 是在微批次内做的,要让它跨微批次也保持相近量级,就需要手动平均一下。

第三个是优化器配置。语言模型训练最常用的还是 AdamW。beta 参数一般用 (0.9, 0.95),epsilon 设到 1e-8,weight decay 跟结构规模有关,一般取 0.1 到 0.01 之间。如果模型较大,我建议从 0.1 起手,在有困惑度验证集上对比再调整。这里顺便提一个容易迷惑的地方:weight decay 只需要对可训练权重生效,对一些 layer norm 参数或 embedding 是否可以施加需要单独区分,否则会出现不平衡的衰减。

第四是学习率调度。常见组合是 warmup 加余弦退火。warmup 步数一般占总训练步数的 2% 到 5%。原因是训练初期模型参数是随机的,梯度方向很不稳定,学习率过大会把参数推向奇怪的区域。余弦退火阶段让学习率平滑降低,有助于收敛到更好的局部最优点。

3.3 混合精度、梯度裁剪与检查点存储

混合精度训练基本上是从零构建中等以上模型的必然选择。核心是让大部分计算用 FP16 或 BF16 来做,减少显存和带宽占用,同时保留一份 FP32 的权重副本用于参数更新。BF16 在处理大数值范围时更稳,而当前大多数新的加速卡都支持,但如果你还在比较旧的卡上跑,要注意对 FP16 的支持和 loss scaling 的配合。

梯度裁剪不能漏。语言模型训练偶尔会出现 loss 飙升甚至直接变 NaN,一个非常有用的防线就是梯度裁剪:设定一个最大梯度范数,超过就缩放回去。常用阈值在 1.0 附近,也可以参考模型大小的经验值调。梯度裁剪是在混合精度场景里处理梯度溢出的一层保护,配合对异常 loss 的检测逻辑一起用。

checkpoint 存储上我只强调一点:不要只存模型权重,要把 optimizer 的状态、当前 epoch 步数、学习率调度器的位置一起存下来。如果只存权重,训练中断后即使恢复模型,优化器状态却要从零开始重建,等于前面一大段 momentum 和 variance 积累全部作废,训练效果大概率会出现抖动。所以推荐存“全量 checkpoint”,并且定期保存、至少保留最近一两份。

一个完整的训练循环里,日志记录也是不可缺的组件。我一般会在每步或者每固定步数记录:当前 loss、梯度范数、学习率、吞吐量(token/s)、显存占用。这些指标不是给别人看的,是给将来的自己排查问题留后手的。从零构建大模型第一步就要把日志系统写到位,后面调参会无数次回头来看这些记录。

4. 训练稳定性的关键配置与调优

网络结构对了、训练循环能跑了,这只是开始。真正让人头疼的工作,是如何让训练从头到尾保持稳定。

模型训练初期的 loss 波动是比较正常的,但你得能区分“正常波动”和“异常发散”。正常波动一般会在一个范围内起伏并整体呈下降趋势;异常发散通常表现为 loss 突然翻倍、出现 NaN/Inf、或者困惑度在某一步后直线上升。前者不用管,后者需要立刻介入。

4.1 warmup 和 learning rate 的关系

warmup 不是“别人都在用所以我也用”,它解决的是一个非常具体的数值稳定性问题。模型刚初始化时,参数的状态离目标函数的最优区域很远,梯度值可能偏大而且方向杂乱。如果直接上很高的学习率,第一波更新就可能把参数踢到非常坏的位置,后面很难拉回来。

warmup 的具体步数取决于总训练步数。你可以把总步数算出来,然后定 warmup 比例。如果总共训练 20000 步,按 3% 算就是 600 步;如果只有 4000 步,可能 warmup 200 步就足够了,比例过大会挤压有效学习时间。

我建议使用 linear warmup 加 cosine decay 的组合,前 3% 线性爬升,剩余阶段余弦退火。代码实现上可以直接用一个组合函数,也可以先 warmup 到峰值再走余弦。峰值学习率怎么定?经验值大致是 3e-4 到 5e-4 之间,具体取决于 batch size 和模型深度。batch size 翻倍时,学习率可以相应往上调整一些,但不一定需要完全线性缩放。

4.2 关键超参数速查与配置组合

这里给出一份我在搭建训练脚本时常用的基础配置表,方便对照做初始值的设置。你可以根据自己的基础环境和数据量去微调。

参数名推荐初始区间说明
学习率3e-4 ~ 5e-4配合 warmup 使用,batch 较大时可适当调高
批次大小(按 token 计)50万 ~ 100万数据量少时取偏高,数据量大时取偏低
梯度累积步数依据显存反推,4~32 常见注意 loss 缩放
warmup 比例2%~5% 总步数训练步数短时用 5%,长时用 2%
最大梯度范数1.0遇到不稳定可降到 0.5
weight decay0.1关注 embedding 和 Norm 参数的差异化处理
dropout0.0 ~ 0.1预训练通常很低甚至为 0,数据量小可加到 0.1

这是一个相对稳妥的起点组合。等模型跑起来有了第一个 loss 曲线,再针对性地调。核心思路是:每次只改一个变量,不要同时动学习率和 batch size,否则出了问题你很难判断是谁引起的。

4.3 显存优化与吞吐量考量

显存不足是个人构建模型时几乎绕不过去的坎。这个问题的核心是:显存不够不是“能不能跑”的问题,而是“怎么跑得有效率”的问题。

几种有效的显存优化手段,按见效程度排序:

第一,梯度累积(效果直接)。总 batch 是 64,但单卡只能一次吃 16,那就分 4 步累积梯度后更新一次参数。要注意,梯度累积只影响 batch 大小,不影响吞吐量,该卡还是卡,只是让“更大的 batch”变得可用。

第二,激活值重计算。这个对显存节省非常明显,代价是会增加训练时间(约 30% 到 50%)。如果时间允许但显存紧张,值得优先考虑。有些框架里它叫 activation checkpointing 或 gradient checkpointing。

第三,准确计算每一层参数量带来的激活显存开销。从零构建时很容易犯一个错误:关注参数总量,却忽略激活值在长序列下的显存开销。Transformer 的激活显存主要消耗在注意力矩阵和 FFN 中间态上,序列长度每增加一倍,激活显存近似线性增长,长序列任务里尤其明显。

吞吐量也是很值得记录的数据。通过日志记录每秒处理的 token 数,你就能判断优化措施的到位还是不到位。比如原本 5000 token/s,加了个随机化数据访问的机制降到 4500,就要去看是不是每个 epoch 都做了重复的 shuffle;如果数据加载占了瓶颈,就该改成预取和异步加载。

4.4 数据配比与采样策略

很多人以为数据配比是“预处理阶段”的事,实际上在整个训练过程中,数据配比会影响每一个 batch 的构造,从而极大影响模型效果,这一环属于训练稳定性的重要组成部分。

数据配比的核心是:通用文本、代码、数学、多语种,这些数据不应该按原始数量比例混入。如果按原始量混,代码和数学会被淹没在通用文本里。一般做法是,给不同来源的数据设置采样权重,让稀缺但重要的数据(比如数学、代码)获得相对更高的被采样概率。常见的配比从“通用、代码、数学” 7:2:1 起步。

采样策略上,还需要注意不去大规模重复使用同一批数据。小数据集要训练很多 epoch,但重复采样会让模型记住数据而不是学会规律,困惑度看似在降、泛化却在变差。这里有一个组合策略:面对小数据集时,可以适当加大 dropout,同时配合数据增强或调整配比来缓解重复采样带来的问题。

5. 常见问题与排查技巧实录

训练跑起来之后,你就会遇到各种奇怪的现象。从零构建大模型这个阶段,大部分时间都会花在调试上。我把几个高频问题整理成一份排查笔记,这些场景我基本都亲手经历过,也见过其他开发者踩进同样的坑。

5.1 问题一:loss 降得很慢,或者卡住不降了

这是最常被问到的问题之一。loss 还在降但幅度特别小,或者干脆卡住。如果是卡住一点都不动,优先检查哪里呢?

先看学习率。如果用的学习率过低,比如 1e-5 以下,模型基本是“原地踏步”。再看数据:检查你喂进去的输入输出对齐没有。我之前遇到过输入往左偏移了一个 token,模型其实在学习预测“自己前面刚看到的词”,loss 虽然能降,但模型根本没学到可泛化的语言规律,最后生成结果一塌糊涂。

区分 loss 下降慢和下降真卡住的方法是看梯度范数。我一般会在日志里打印平均梯度范数,如果梯度范数整体在缩水、而且已经缩到非常小,那大概率是学得差不多了;如果梯度范数一直很小但 loss 仍然很大,那就要怀疑数据对齐或者标签质量了。

另外,如果你的数据来自混合配比,某一类数据的占比一不小心写得非常高,模型会被这个明显占优的类别带走,loss 下降也容易变慢。清晰把握数据分布对这个问题极为关键。

5.2 问题二:loss 突然飙升或者变成 NaN/Inf

loss 飙升,最常见的直接原因是优化器状态里的梯度爆炸了。找原因的路径一般是这样的:先看学习率调度器位置——是不是进入了余弦退火的某个阶段,学习率突然掉到太低,反而让模型在某一步上产生了奇怪的小范围震荡?再看梯度裁剪参数——如果阈值设得太大,等于没剪。

另外一个被忽视的原因是基础浮点精度不足。在混合精度下,如果模型里的 logits 数值范围本身就很大,FP16 下容易出现溢出。常见的解法是把模型权重换成 BF16,或者更精细地把 logits 计算放到 FP32 精度下。排查时要看一下精度转换的位置,别在整个前向传播上大范围切精度,容易引入新的奇异行为。

如果 loss 直接跳成 NaN,那就要走一套固定动作了:先减少 batch size 看是否复现;然后把混合精度关掉跑一小段看是否消失;再把学习率降到很低看是否还出现 NaN;最后怀疑数据本身有问题,比如标签构造出现了一个整数映射越界,普通检查不太看得出来。按这个顺序排查,能快速缩小范围。

5.3 问题三:显存溢出(OOM)

显存溢出在不同阶段出现,解法不太一样。

如果你在训练刚开始就溢出,那大概率是单个 batch 的显存就超了。先把 batch size 调小,如果调小后没问题,就正常了;如果调得非常小仍然溢出,那要检查序列长度、attention mask 矩阵的维度是不是比预期大。

如果你发现训练到中途才溢出,一种比较隐蔽的情况是:前面很多步是在“边界试探”,随着 token 数增长,显存碎片不断增加。这就要做显存碎片整理,或者考虑周期性释放缓存。我遇到更多的情况其实是“总 batch size 太大,梯度累积步数又没有调低”,导致中间状态积压。

防止 OOM 的兜底方案是设置一个“安全模式”:在启动训练前自动计算 batch size 的上限,根据当前模型参数量、序列长度和可用显存推算出一个保守值,跑起来再逐步往上试探。从零构建阶段,把这一步做成脚本里的自动检测,能省下大量人工排查时间。

5.4 问题四:训练速度不符合预期

训练速度慢,很多情况下并不是模型结构有问题,而是数据加载卡住了。

我这里说的是最容易被掩盖的一种情况:显式 shuffle 或数据处理在每次 epoch 重新执行。传统小模型训练时,每次把数据随机重排的开销可以忽略,但在大模型场景下,数据量动辄几十 GB 甚至上 TB,每次 shuffle 都意味着海量 IO,速度自然起不来。

解决方案是:把数据预切分成多个预排序的二进制文件,每个文件内部数据保持稳定顺序,在文件级别做随机读取和 shuffle。用内存映射方式加载特征数据,减少反复读盘。这样 shuffle 是“文件层面”的,IO 压力大幅降低。

还有一个很常被忽略的问题:日志打印本身也会拖慢训练速度。如果每个 batch 都打印一次完整的 loss 和显存统计数值,频繁的同步输出会干扰 GPU 计算流。把日志频率降到“每 N 步”或“每 N 秒”,吞吐量会有惊喜的提升。

6. 实操心得与后续扩展方向

模型构建进行到这一步,你再回头看最初“从零构建大模型”这个标题,会发现真正的难点其实不在于把代码写出来,而在于建立一套完整的、经得起异常冲击的训练机制。模型结构只占整个工作量的三成,剩下七成都在“稳定”和“排查”这两个词上。

我个人在实际操作中的体会是:从零构建大模型,一定要带着“最小可用系统”的心态去推进。第一版不要追求高级技巧,不用一开始就上复杂的并行策略,先把一个单机、单卡或双卡能跑完的小模型完整跑起来,把 logging、checkpoint、异常检测这些基础设施补齐,然后再逐步扩大规模、引入高级技巧。很多人第一次上手就试图复现一个很大的模型,结果在环境的未知因素里浪费了大量时间,反而不如先把小规模跑通、把整个链路打通更有收获。

最后再分享一个小技巧。在训练脚本里预留一个“快速自检模式”:加载极小数据量,比如 10 个 batch,用 1 到 2 步训练跑通全流程。这个模式的作用是,在正式训练前先发现代码层面的低级错误,比如维度对齐问题、mask 广播问题、数据类型问题。我基本每一次改动网络结构后都会执行一次快速自检,花不了几分钟,却能省掉半天起步调试时间。

这一章的内容到这里,算是给“从零构建大模型”加上了完整的训练层。后续如果有机会,还可以延伸的方向包括:序列并行策略、大规模分布式训练、以及模型推理阶段的量化与部署。但从构建的角度来说,这一章你已经把核心功底打牢了。带着这份功底去应对更大的模型和更复杂的数据场景,你会稳得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询