A.6 从原理到实验:训练与推理实践路径
实践的目标不是获得一次正确输出,而是能说明输出如何产生、哪些状态发生了变化,以及换一种实现后为什么仍然正确。本附录用五个脚本组织四组 CPU 实验,再给出迁移到真实训练和服务的验收标准。开源框架的运行配方见 A.7。
A.6.1 先建立可复现的基线
手算一个输出层:前向、损失、梯度和更新
↓
训练两层双头 GPT:观察完整前向与反向
↓
保存并恢复训练:验证模型与训练状态
↓
加入 KV Cache:验证 Prefill 与 Decode 等价
↓
拆解后训练目标:SFT、LoRA、DPO 与组内优势
↓
迁移到框架:在质量约束下测量性能和成本这不是所有人的唯一学习顺序。已经完成 A.2 的完整训练实验时,可直接从恢复训练开始;若对梯度含义不确定,应先做输出层实验。
以下命令从本书源码目录执行。环境使用 Python 3.11 和 PyTorch 2.8.0;它是这些教学脚本的验证环境,不代表生产部署的推荐版本。五个脚本不下载预训练权重或外部数据,运行在 CPU 上。
python3.11 -m venv /tmp/llm-practice-env
/tmp/llm-practice-env/bin/python -m pip install torch==2.8.0 numpy后文用 python 表示上述环境中的解释器。先激活环境,再运行实验:
CPU 实验中第二行是 False 也可以继续。不要用教学模型的执行时间推断 GPU 内核或生产系统的加速比。固定随机种子有助于复现,但不保证跨设备、跨版本逐位一致,边界见 PyTorch 可复现性说明。
A.6.2 实验一:手算梯度,再让自动微分核对
先只取语言模型的输出层。输入矩阵 $X\in\mathbb{R}^{N\times d}$ 表示 $N$ 个待预测位置的隐藏向量,每行 $d$ 个数;$W\in\mathbb{R}^{d\times V}$ 和 $b\in\mathbb{R}^{V}$ 是输出层参数,$V$ 为词表大小。这里的 $N$ 只是本实验的位置数,不是模型参数量。
令 $Y$ 为目标编号对应的独热矩阵,$P=\operatorname{softmax}(XW+b)$,$G$ 为损失对 logits 的梯度,则:
分母 $N$ 来自平均损失,不能在梯度中遗漏。$dX$ 的作用是把误差信号传回前面的 Transformer 层;它不是另一个目标词元。以学习率 $\eta$ 做一次 SGD 更新,就是 $W\leftarrow W-\eta dW$、$b\leftarrow b-\eta db$。
脚本取两个位置、二维隐藏向量、三个候选词元:
目标编号为 $y=[2,0]$,编号从零开始。第一行希望选第三个词元,第二行希望选第一个词元。矩阵乘法得到:
平均损失为 $-(\log0.229124+\log0.357353)/2\approx1.251260$。第一行梯度是 $[0.563555,0.207320,0.229124-1]/2$。再用 $X^TG$ 算权重梯度,例如第一行第一列为 $1\times0.281778+(-1)\times(-0.321323)=0.603101$。取 $\eta=0.1$,该权重从 $0.1$ 更新为 $0.039690$;全部权重和偏置更新后,损失约为 $1.099419$。展示数字经过舍入,脚本用未舍入值计算。
运行源码中的输出层梯度实验 examples/training_step.py:
核对三件事:打印的每个矩阵形状与公式一致;手算梯度与自动微分一致;对单个参数施加正负微小扰动所得的中心差分与梯度相近。中心差分为:
这里 $w$ 是参数矩阵中的一个数,$\delta$ 是小扰动。扰动过大有截断误差,过小有浮点消减误差;实验使用双精度核对。自动梯度检查的原理见 PyTorch gradcheck。
验收与边界。 能解释 P−Y、均值分母和三个梯度的维度,再独立换一组矩阵重算。本例只手推输出层,不冒充整个 Transformer 的手工反向传播;整个模型的反向计算在下一实验中由自动微分完成。一次固定小样本更新使损失下降,也不意味着任意学习率、任意批次都必然下降。
A.6.3 实验二:完整训练,再恢复同一条训练轨迹
先运行 examples/tiny_gpt.py。它包含两层、每层两个头、宽度 32,完整链路为嵌入、两次 Transformer block、最终归一化、词表投影、交叉熵和反向传播。
保存路径已存在时换一个新路径;脚本不会覆盖已有模型。四条算式训练用于观察记忆过程,不是算术泛化实验。A.2给出了具体批次和各层维度。
接着运行训练恢复实验 examples/training_resume.py:
默认比较连续 40 步与“17 步后保存、再恢复运行 23 步”。文件保存的是第 17 步状态,不是最终第 40 步状态;保存路径必须不存在。输出中的各项 exact resume checks 都应为 True。若要独立接着训练该文件,再保存一个新文件:
比较两条轨迹,而不只比较能否加载文件:
模型参数和配置
加载错误的结构、词表或参数
优化器状态
Adam 的一阶矩、二阶矩和计数丢失
学习率调度器与更新步数
从错误的学习率位置继续
随机数状态
后续样本顺序或随机运算不同
数据顺序、游标等读取进度
重读或跳过训练数据
只保存 model.state_dict() 足以覆盖某些推理加载场景,不足以恢复训练;参见 PyTorch 检查点教程。本实验使用 CPU、固定软件环境和有限的数据读取状态;多进程数据加载、多卡 RNG、混合精度缩放器和分布式分片检查点需要另行保存与验证,见 7.7 节。
验证不参与更新。 留出文本不进入训练批次;评估阶段关闭梯度,不执行优化器更新。本例留出集合很小且与训练数据同为算式,只能检查评估路径,不能证明一般语言能力。真实任务还要防范文档近重复、相同来源跨集合、时间穿越和测试集参与调参,见 5.5 节。
故障练习。 在实验副本中分别不恢复优化器或数据游标,再比较后续损失和参数。若仍然全部一致,检查测试是否真的继续更新、是否比较了正确的模型,以及是否只检查了输出形状。
A.6.4 实验三:缓存不能改变模型在同一前缀上的计算含义
缓存实验 examples/kv_cache.py 复用同一个 TinyGPT 的参数,比较完整前缀计算和带缓存计算。模型无需训练:等价性检验比较的是实现,不是回答质量。
设 $p$ 为已经缓存的词元数,$n$ 为本轮新增词元数,$m=p+n$。批大小为 $B$,头数为 $H$,每头宽度为 $d_h$。本例 $H=2$、$d_h=16$,每层独立保存 K/V。
嵌入与位置
词元编号 [B,n],位置 p…p+n−1
[B,n,32]
每层 Q/K/V 投影
归一化后的 [B,n,32]
新 Q/K/V 各 [B,2,n,16]
追加该层缓存
旧 K/V 各 [B,2,p,16]
完整 K/V 各 [B,2,m,16]
注意力权重
Q 与完整 K、因果掩码
[B,2,n,m]
读取 V、合头、投影
权重与完整 V
[B,n,32]
残差、MLP、下一层
[B,n,32]
[B,n,32]
最终归一化与输出层
[B,n,32]
logits [B,n,9]
Prefill 时 $p=0$,一次处理整个 Prompt;逐词元 Decode 时 $n=1$,位置不能重新从零开始。分块处理时 $n>1$,块内仍要屏蔽未来位置。对本轮第 $i$ 行,能读取的完整 K/V 位置满足 $j\le p+i$,其中 $i,j$ 从零编号。
例如旧缓存长 4,新输入长 2,则两个新查询的可见性为:
此处 1 表示可见;它是布尔选择规则,不是把分数乘以 0 来屏蔽。被屏蔽分数应在 Softmax 前置为负无穷。非方阵下不能不加区分地照搬 tril();框架的掩码对齐规则需查具体 API,见 PyTorch SDPA。
验收。 分别检查逐词元与分块 Decode 的每一行 logits,而不是只比较最终贪心词元;两个不同的分布也可能选中同一个词元。每次调用后,各层缓存长度都应增加 $n$,旧缓存不被意外修改。允许符合精度的浮点误差,不要求不同计算形状下逐位相同。
一次 CPU FP32 运行中,逐词元与分块路径的最大 logits 误差分别为 1.192e-07 和 5.960e-08;两层最终 K/V 都为 [1,2,6,16]。这些数值是本例观测结果,不是通用于任意模型的容差阈值。
实验还输出每层处理的位置数。它说明投影和 MLP 少算了多少历史位置,不是加速倍数:注意力仍读取不断增长的缓存,内存分配和内核启动也有成本。通用缓存结构可对照 Transformers 缓存文档;分页、GQA、量化缓存和跨请求调度属于后续工程扩展。
A.6.5 实验四:后训练到底改了什么
运行后训练核心实验 examples/post_training.py:
它把四个易混淆的问题分开验证。
本例运行中,SFT 五个有效目标的平均损失从 2.999133 降到 2.761865;DPO 的偏好概率从 0.5 升到 0.622459。LoRA 首步基座与 A 都未改变,B 发生更新。组内优势的均值为零,但初始 surrogate loss 为零时梯度范数仍为 0.235679:损失的数值为零不等于梯度为零。
SFT
只对选定助手目标计损失;输入和下一词元目标正确错位;分母是有效目标数
损失下降就证明指令遵循和安全性提高
LoRA
初始增量为零;基础权重冻结;反向后只有适配器参数更新
低秩更新在任意任务上等价于全量微调
DPO
策略与参考模型的回答对数概率差;初始相同策略得到 log(2);偏好梯度方向正确
几组偏好对足以对齐真实模型
组内优势
同一问题多次采样的奖励减去组均值;全相同奖励安全地产生零优势
已完成 GRPO 或整套 RLHF
SFT 的 labels 形状是 [B,T],被忽略位置不计入损失。必须把掩码与预测目标所在位置对齐,不能把用户输入掩掉后误删应被监督的第一个回答词元。真实聊天模板的角色边界和结束符规则见 8.1 节。
LoRA 对输入宽度 $d_{in}$、输出宽度 $d_{out}$ 的线性层采用 $A\in\mathbb{R}^{r\times d_{in}}$、$B\in\mathbb{R}^{d_{out}\times r}$,其中 $r$ 为秩,增量权重为 $(\alpha/r)BA$。当 $B=0$ 时初始输出不变;首步 $A$ 的梯度也可能为零,因此不能用“每个适配器张量首步都必须改变”验收。初始化与缩放约定见 PEFT LoRA。
DPO 的完整损失和参考模型含义见 8.3 节。实验输入的是已经计算好的回答对数概率;实际训练还需要从词元 logits 中选择目标概率、屏蔽 Prompt 与填充、按回答求和,不能把整段对话无差别求和。
组内优势实验覆盖奖励标准化与单词元裁剪目标的一次更新。完整在线策略优化还涉及采样策略、旧策略概率、截断、KL 项、奖励可靠性及训练与推理权重同步。不同变体采用不同的标准差、长度归一化和裁剪规则,应以所选算法定义为准,不能把一个组内标准化公式当作统一的 GRPO 实现。框架入口与边界见 A.7。
A.6.6 从会运行到会判断:三个综合项目
五个脚本通过,只说明基础机制得到验证。下面三个项目要求在真实数据与约束下形成可复查的工程结论,不设脱离任务的统一分数门槛。
项目一:可恢复的小模型训练。 使用经过许可的本地文本,按文档来源或时间拆分训练、验证、测试集,记录去重规则、分词器版本和数据摘要。建立单设备基线,报告有效词元数、验证交叉熵、梯度范数、更新量与参数范数之比、吞吐及峰值显存。随后只改变一项:混合精度、累积步数或激活重计算。比较质量与资源,再执行中断恢复演练。测试集只用于最终验收,不用来选超参数。
项目二:领域微调与退化评估。 对同一基础模型建立未微调、SFT、LoRA 三个对照;有足够偏好数据后再加入 DPO。固定测试任务、聊天模板、解码设置和回答长度预算,分别报告领域收益、通用能力退化、拒答与格式失败率。模型裁判与人工抽查交叉核验,保留坏案例,不能只报告平均分提高。若修改训练数据或配置,先在验证集选择,再完成一次冻结测试。
项目三:有服务目标的推理优化。 固定模型与真实请求长度分布,先测基础精度,再逐项切换量化、前缀缓存、投机解码和并行策略。记录首词元延迟、后续词元延迟、端到端延迟、尾延迟、成功率和满足服务目标的吞吐。冷缓存、热缓存、不同负载单独报告;一次高速离线生成不能替代在线排队下的结果。测量口径见 11.13 节和 vLLM 基准说明。
每个项目交付五件材料:可重建的环境与配置、固定的数据划分、基线和对照结果、失败案例与诊断证据、包含限制条件的结论。没有改善的实验也有价值,只要能解释哪个假设被否定。
A.6.7 专家判断的起点:先定位,再优化
训练 loss 异常低
目标错位、监督掩码、数据重复、是否只测训练集
直接宣称配方优于基线
验证 loss 升高
数据分布、有效词元分母、模型模式、过拟合与评估管线
只增加训练步数
缓存输出与基线不同
第一个不同位置、该层 K/V、位置偏移、掩码、dtype
只放宽最终文本相等条件
显存不足
权重、梯度、优化器、激活、缓存分别占多少
一律减小模型或盲目加卡
加卡后不加速
单卡基线、通信等待、数据读取、负载不均与同步
把设备数当作收益
吞吐升高但用户更慢
排队时间、首词元与尾延迟、错误率、输出长度
只按平均 tokens/s 上线
奖励上涨但回答变差
奖励漏洞、长度偏置、分布变化、独立质量评测
继续提高奖励权重
这是本书建议的排查顺序,不是各类故障的唯一成因。处理一次问题时,应先保留可复现输入,再定位最早出现偏差的阶段;只改变一个假设相关变量,确认修复后加入回归检查。
学习前沿技术也采用同一方法。读 FlashAttention、FP8、MoE、长上下文或推理时扩展的论文,先回答四个问题:改了什么计算或状态;省下哪一项资源、增加哪一项成本;比较是否固定质量和预算;论文收益能否迁移到自己的硬件与请求分布。把论文结论、框架实现和本机测量分开记录,才能判断应当采用、继续实验,还是暂不采用。
最后更新于
