学完一门语言模型课程之后,最容易产生的错觉是:只要把 Transformer、优化器和训练循环写出来,就已经完成了一个语言模型项目。实际上,代码能跑只是起点。真正的工程问题是:在固定的人力、数据、GPU、时间和服务预算下,如何让模型稳定地学到目标能力,并且能够被可信地测量和使用。
下面这套框架把 CS336 的全部内容整理成一条可执行路线。它不假设你要训练最大的模型,而是强调每一个决策都必须能被测量、解释和回滚。
第一步:先写清楚目标函数之外的目标
训练目标通常是 next-token prediction,但项目目标不应该只写成“把 loss 降下来”。开始之前至少明确四个约束:
| 约束 | 要回答的问题 | 会影响的决定 |
|---|---|---|
| 能力 | 模型要生成、分类、补全、推理还是调用工具? | 数据、tokenizer、评估集 |
| 规模 | 参数量、上下文、并发和部署设备是什么? | 架构、精度、并行方式 |
| 预算 | 可用 GPU 小时、数据量和训练周期是多少? | scaling、checkpoint、实验数量 |
| 风险 | 哪些数据、输出和失败模式不可接受? | 过滤、对齐、监控、回滚 |
这一步的产物不是一段宣传语,而是一份可以验收的实验协议。例如,“代码补全”至少要拆成语法正确率、测试通过率、长文件上下文能力、首 token 延迟和每请求成本;“通用聊天”也不能只用一个人工印象分。
第二步:建立最小但可解释的基线
不要一上来同时更换 tokenizer、架构、优化器和数据。第一版应该尽量朴素:稳定的 tokenizer、标准 Transformer、清晰的训练循环、固定的数据切分和可复现实验配置。基线的目的不是追求最好分数,而是建立因果关系。
一个合格的基线至少记录:
- 参数量、激活量、每 step token 数和理论 FLOPs;
- 实际 tokens/s、GPU 利用率、显存峰值和通信时间;
- train loss、validation loss、梯度范数、学习率和数值溢出次数;
- checkpoint、随机种子、数据版本、代码提交和硬件信息;
- 固定的 smoke test、短跑实验和完整训练实验。
没有这些记录,后续看到 loss 变化时,你无法判断是模型真的学得更好,还是 batch、数据顺序、精度或硬件发生了变化。
第三步:先把 token 和数据管线测明白
数据管线应该在训练模型之前单独验收。对同一批样本,比较原始字符数、token 数、不同语言和不同任务的长度分布;检查特殊 token、文档边界、截断策略和 packed sequence 是否按预期工作。
重点观察三个量:
- token 预算:总 token 数是否足够支撑目标参数量和训练步数?
- 有效信息密度:重复、模板、低质量网页和无意义代码占了多少比例?
- 混配稳定性:不同数据源的采样权重是否会在训练中漂移,是否有某一类数据压制其他能力?
数据去重和过滤必须保留可追溯结果。对每条规则记录删除数量、保留数量、抽样样本和误删风险;否则当评估下降时,你无法知道问题来自数据质量还是模型配置。训练集、验证集和测试集还要进行近似去重,避免把答案泄漏误认为泛化能力。
第四步:用预算选择模型,而不是凭直觉堆参数
在固定计算预算下,模型大小、数据量和训练步数要一起决定。一个简单的估算顺序是:
- 先由任务和部署设备确定上下文长度、最大 batch、目标延迟和可接受模型大小;
- 再由 GPU 小时和吞吐估算总训练 FLOPs 与 token 预算;
- 使用小模型实验拟合 loss 随参数、数据和训练步数的趋势;
- 最后决定是增加参数、增加数据,还是延长训练。
如果模型每一步都在等待通信,理论 FLOPs 就没有意义;如果数据重复严重,额外 token 也不等于额外信息;如果服务阶段只能使用低精度和小 batch,训练阶段的最优配置也可能不是部署阶段的最优配置。
第五步:把训练失败分类,而不是只调学习率
训练出现异常时,先根据症状分类。下面的表格比“再试几个超参数”更有用:
| 症状 | 优先检查 | 可能的根因 |
|---|---|---|
| loss 从第一步就变成 NaN | 输入、mask、logits、精度 | 数据异常、softmax 溢出、loss scaling 错误 |
| loss 下降后周期性尖峰 | 梯度范数、学习率、激活范数 | 更新过大、循环状态不稳定、通信不同步 |
| train 降而 validation 不动 | 数据切分、重复率、评估实现 | 记忆训练样本、污染、指标错位 |
| GPU 利用率低但显存满 | profile、kernel、通信时间 | 内存带宽受限、碎片、同步或 all-reduce 等待 |
| tokens/s 随卡数增加很少 | 并行切分、网络拓扑、bubble | 通信比例过高、负载不均、流水线空泡 |
| 训练正常但推理很慢 | prefill/decode、KV Cache、量化 | decode 内存带宽、cache 过大、batch 调度不佳 |
数值稳定性要同时看三个层面:初始化与归一化是否让激活处于可控范围,混合精度是否在关键归约处保留足够精度,kernel 是否正确处理边界、mask 和累加顺序。只改变学习率,往往只能掩盖根因。
第六步:优化系统时遵循“测量—假设—验证”
GPU 优化不能从“重写一个更底层的 kernel”开始。正确顺序是:先用 profile 确认热点,再写出关于瓶颈的假设,最后用独立 benchmark 验证。
例如,一个 attention kernel 很慢,可能是:
- HBM 读写量过大;
- tile 尺寸导致 occupancy 下降;
- 中间张量物化造成额外写回;
- warp 间同步过多;
- 实际瓶颈来自上游 layout 转换,而不是 attention 本身。
只有在基准覆盖真实形状、batch、序列长度和精度时,优化结果才有工程意义。单个理想尺寸上的微秒级胜利,不一定能改善训练总时间,更不一定能改善线上尾延迟。
第七步:建立分层评估,而不是等待最终分数
评估应该像测试金字塔一样分层:
- 算子层:数值误差、边界条件、梯度检查和不同精度的一致性;
- 训练层:小数据过拟合、loss 曲线、checkpoint 恢复和随机种子稳定性;
- 能力层:困惑度、知识、代码、数学、长上下文和多语言任务;
- 行为层:指令遵循、拒答边界、多轮一致性、工具调用和安全性;
- 系统层:TTFT、每 token 延迟、吞吐、尾延迟、显存和成本。
这样做的好处是,失败可以被定位。如果能力层退化而算子和训练层正常,优先检查数据和后训练;如果能力分数没变但系统层变差,问题可能在量化、batch 调度或 KV Cache;如果小数据过拟合都做不到,继续跑大规模实验只是在浪费 GPU。
第八步:后训练要保留基座对照组
SFT、偏好优化和 RLVR 的每一次改动,都应该与基座模型和上一个版本做对照。除了目标任务,还要测试通用能力、事实性、拒答、长度分布和过度思考。
后训练最危险的地方是局部指标变好、整体行为变坏。一个模型可能更愿意输出符合格式的答案,却更容易自信地编造;也可能数学 benchmark 上升,却因为思考 token 增多而让产品成本失控。评估必须同时包含质量和代价。
第九步:把推理部署当成另一种工作负载
部署前先测量真实请求分布,而不是只测一个固定 prompt。至少区分:
- 短输入短输出的交互式请求;
- 长上下文的文档问答;
- 长输出或 agent 循环;
- 大 batch 的离线生成。
prefill 更偏计算,decode 更偏内存和调度。对应的工程手段也不同:prefill 关注大矩阵吞吐和批处理,decode 关注权重驻留、KV Cache、continuous batching、speculative decoding 和尾延迟。对外暴露的服务指标应该包括 TTFT、TPOT、P50/P95/P99 延迟、有效吞吐和每百万 token 成本。
第十步:让每次实验都能回答一个问题
一套成熟的 CS336 工程流程,最终应该形成这样的实验记录:
问题:把上下文长度从 8K 提到 32K,能力和成本如何变化?
假设:token 数增加会提高 prefill 成本,KV Cache 会成为 decode 的主要限制。
控制变量:模型权重、数据、优化器、硬件和评估集保持不变。
观测:loss、长上下文任务、TTFT、TPOT、显存峰值、吞吐和尾延迟。
结论:接受、拒绝,或者提出下一轮更具体的假设。这比“这组参数看起来更好”严格得多,也更容易把研究结果迁移到别的模型、硬件和业务场景。
一份可以落地的最终检查表
在宣布一个语言模型项目完成前,至少确认:
- tokenizer、数据版本和切分结果可复现;
- 小数据过拟合、断点恢复和多精度训练都通过;
- 训练曲线、梯度、激活、通信和 kernel profile 有记录;
- 评估集没有明显污染,并且覆盖能力、行为和系统指标;
- 后训练保留基座对照,能解释每次行为变化;
- 真实请求分布下,prefill、decode、KV Cache 和尾延迟都测过;
- checkpoint、配置、代码提交和部署镜像可以回滚。
这套流程的核心不是让每个项目都变得庞大,而是让每个结论都能被解释。CS336 真正训练的能力,也不是背下某一代 Transformer 的结构,而是学会在模型、数据、硬件和产品约束之间做出可验证的选择。