补1 · 补1 · LLMs 101:预训练的四个组成部分与默认训练栈

Stanford CS312 深度学习炼金术(2026)官方讲义 · 中文重构

原始标题 LLMs 101
材料来源 llms_101_slides.pdf(39 页幻灯片)、llms_101_handout.pdf(19 页讲义 handout)
课程主页 https://deep-learning-alchemy.github.io/
材料版权 课程材料版权归原课程方及授课教师所有,本书仅供学习

对应讲次:补1(Stanford CS312 深度学习炼金术)

制作模型:deepseek-flash(vb compose)

一、LLMs 101:一次预训练的四个组成部分

这一讲要讲的东西不新,但很基础:深度学习不是一堆零散的技巧,而是一份可以逐项说清楚的配方。讲义给出的框架是——指定一个深度学习算法,本质上就是把四件事讲明白:

这是把四个要素叠在同一张“预训练卡通图”上的总览页,读者一眼就能看到数据→模型→预测→损失→更新的完整回路,是后面所有小节的骨架

🖼️ 这是把四个要素叠在同一张“预训练卡通图”上的总览页,读者一眼就能看到数据→模型→预测→损失→更新的完整回路,是后面所有小节的骨架

  1. 数据(Data):规定输入到模型的分布,例如互联网文本。
  2. 架构(Architecture):规定参数 $\theta$ 如何映射成模型的一次预测,例如 transformer 输出对数似然。
  3. 目标(Objective):用损失函数度量 $\theta$ 的误差,例如对预测似然取交叉熵。
  4. 优化(Optimization):规定模型应该如何根据损失更新自己,例如对交叉熵损失做梯度下降。

这四件事不是并列的清单,而是一条闭合回路:数据喂进模型,模型给出预测,目标把预测和真实答案之间的差距量化成损失,优化再根据损失反过来修改 $\theta$。讲义用一张「预训练卡通图」把这条回路一层层叠出来,从最初只有一个「带参数 $\theta$ 的模型」,逐步加上 Input、Prediction、Loss,最后形成完整回路。

后面所有内容——分词、交叉熵、Adam、残差、RoPE、SwiGLU——都挂在这张图的某一个位置上。读的时候不妨一直问:这一节在补哪一格?

本讲面向谁

讲义对读者定位说得很直白:

默认训练栈的定位

课程使用的这套训练栈不是完美的,更不是 state-of-the-art。但讲义强调它「确实反映了深度学习实践的许多重要方面」,最低限度上,你必须熟悉其中相当一部分组件。

同时它也明确说了:不必理解、更不必背下每一个组件(讲者自称也不是每个都记得)。课程会在作业里把注意力引到相关组件上。所以这一讲的正确读法是建立索引,而不是背诵。

编者说明:这一讲的定位类似「工具箱开箱」。它不推导为什么 transformer 好用,只告诉你一个能跑起来的训练配方由哪些旋钮组成、每个旋钮默认拧在哪。后续单元会逐个把旋钮拆开做实验。

二、数据:分词、拼接与定长序列

这张图画出了“多篇文档被拼接、再切成 Seq 1/2/3,每段以 bos 开头、以 eos 结尾”的结构,是理解定长打包与 1023 个预测目标最直接的图

🖼️ 这张图画出了“多篇文档被拼接、再切成 Seq 1/2/3,每段以 bos 开头、以 eos 结尾”的结构,是理解定长打包与 1023 个预测目标最直接的图

分词器与词表

分词器(tokenizer) 负责把文本映射成整数 ID 序列,并提供反向解码。一个 token 可以是一个词、词的一部分、标点、空白,或者其它文本片段。

这里有一个初学者最容易带错直觉的点:token ID 是类别标签,不是数值。ID 为 100 的 token 和 ID 为 101 的 token 之间没有任何「更接近」的关系,它们只是词表里的两个编号。词表大小 $V$ 决定模型每次要打分多少个可能的下一 token。

课程准备的数据使用 SentencePiece 分词器,$V = 4096$(含特殊 token)。分词在训练之前一次性完成,训练器读到的直接就是整数 ID。换分词器意味着必须重建数据,并同步修改模型的 vocab_size。

拼接、切块与 1023 个预测目标

文档长度各不相同,而稠密的训练 batch 希望每一行长度一致。讲义给出的准备流程是:

  1. 对每篇文档分词,插入边界符号(BOS:beginning-of-sequence;EOS:end-of-sequence);
  2. 把所有 token 流拼接起来;
  3. 按固定长度 $T = 1024$ 切块成行。

结果是:一行里可能装着多篇文档的碎片,而一篇长文档会横跨好几行。行边界和文档边界并不重合——这一点很重要,后面讲注意力时会再遇到。

每一行 1024 个 token,但第一个 BOS 不参与预测:它是上下文,没有「前一个 token」可以拿它当目标。所以一行的预测目标数是 $1023$:模型在位置 $1,\dots,1023$ 上的输出,分别去对齐位置 $2,\dots,1024$ 上的真实 token。同理,行末最后一个 token 在行内没有对应的预测目标。讲义明确:不训练单独的首 token 预测器,训练时也不采样 token。

数据规模的几个口径

讲义特意区分了几个容易混的量。设有 $N$ 行、行长 $T$:

两者相关但不相等。再加上「训练跑几遍数据」,才是完整的训练预算。

课程默认设置:

项 值
训练池(DCLM) 9,600,000 行
实际抽取 600,000 行(num_train_sequences)
每行长度 1024
输入 token 总数 $600{,}000 \times 1024 = 614{,}400{,}000$
预测目标总数 $600{,}000 \times 1023 = 613{,}800{,}000$

DCLM(DataComp-LM)是这里的互联网文档来源。训练与验证 token 都存成二进制数组,配套的元数据记录形状与整数类型;程序用内存映射(memory-mapped) 的方式按片访问,不必先把整个数据集读进内存。讲义强调一句:存储格式不应该改变哪些样本、哪些目标定义了损失。

顺序与随机种子

data_seed=42 先对训练池做一次全局打乱,再取前 $N$ 行。每个 epoch 复用这个顺序,验证集不受影响。改动种子会同时改变「抽到哪些样本」和「它们的顺序」——这是一个容易被忽略的耦合。

BOS 不是注意力屏障

讲义在这里敲了一记警钟:特殊 token 是数据准备阶段约定出来的符号,不是 transformer 的数学要求。BOS 出现在打包序列内部时,它得到的仍然只是一个普通的可学习嵌入。一个边界标记不会自动阻断注意力跨越这个边界——也就是说,模型完全可以读到上一篇文档的尾部内容。

默认配置里,训练不会在运行时临时插入 BOS/EOS,也不会因为某一行里出现边界标记就重置注意力。

编者拓展|打包序列的「文档隔离」问题:因为拼接后的行里可能混着多篇文档,实践中常见的补救办法是在注意力矩阵上加块对角掩码(只允许同一篇文档内部互相注意)。这份讲义采用的是「不做隔离」的朴素做法,你需要知道这是一个刻意选择的简化,而不是 transformer 的限制。相关讨论可参考对打包与注意力掩码的工程实践综述(如 On Efficient Training of Large-Scale Deep Learning Models 一类文献),此处不展开。

三、目标函数:自回归建模与交叉熵损失

这页画出了 BOS→Token1→Token2→Token3 的逐 token 预测与对应的概率输出,是把“连乘分解”落到具体张量上的关键图

🖼️ 这页画出了 BOS→Token1→Token2→Token3 的逐 token 预测与对应的概率输出,是把“连乘分解”落到具体张量上的关键图

这页直接把概率表换算成 log(1/0.05)=4.605,用一个数字把“负对数似然”从公式变成可感知的量,初学者最需要这一页

🖼️ 这页直接把概率表换算成 log(1/0.05)=4.605,用一个数字把“负对数似然”从公式变成可感知的量,初学者最需要这一页

自回归分解

语言模型给 token 序列分配概率。把序列写成 $x = (x_1, \dots, x_T)$,每个 token ID 落在大小为 $V$ 的词表里。模型有可训练参数 $\theta$。

自回归模型只用前面的 token 预测下一个 token。由概率链式法则,整个序列的似然可以分解为

$$p_\theta(x_1) \prod_{t=1}^{T-1} p_\theta(x_{t+1} \mid x_{\le t}), \qquad x_{\le t} = (x_1, \dots, x_t).$$

如果第一个 token 是作为上下文给定的,被建模的似然就变成

$$p_\theta(x_{2:T} \mid x_1) = \prod_{t=1}^{T-1} p_\theta(x_{t+1} \mid x_{\le t}).$$

在下文里,讲义把这个条件似然简记为 $p_\theta(x)$。这个约定正好避免了给「前面没有任何上下文」的那个 token 分配损失——对应上一节说的 1023 个目标。

讲义把幻灯片里的记号 $Q_\theta$ 和 handout 里的 $p_\theta$ 当作同一个东西:模型 $Q_\theta$ 表示序列 $x$ 在真实分布 $P$ 下的似然,写成

$$Q_\theta(x) = \prod_i Q_\theta(x_i \mid x_{

Teacher forcing 与并行计算

训练时,模型拿到的是真实前缀,而不是自己采样出来的 token——这叫 teacher forcing(教师强制)。这一点常被误解为「训练和推理不一致的缺陷」,但它恰恰是让训练可并行的关键。

一个因果 transformer 能在一个前向里并行算出所有位置的预测,同时用掩码屏蔽掉未来位置的信息。并行计算不改变自回归分解——分解描述的是概率如何拆开,并行描述的是这些条件概率怎么一次算完,两件事互不干扰。到了生成阶段才不一样:新 token 必须一个一个顺序产生,再追加到前缀上。

交叉熵损失

最大似然训练要找的是让观测数据概率最高的参数。最小化负对数似然是等价的:对数保序,并把概率的连乘变成求和。

按目标 token 数归一化后的总体目标为

$$ L(\theta) = \mathbb{E}_{x \sim p_{\text{data}}}\left[-\frac{1}{T-1}\log p_\theta(x)\right] = \mathbb{E}_{x \sim p_{\text{data}}}\left[-\frac{1}{T-1}\sum_{t=1}^{T-1}\log p_\theta(x_{t+1} \mid x_{\le t})\right]. $$

这就是交叉熵:对每一次预测,真实分布把全部质量压在那一个实际出现的 token 上,所以它与模型分布之间的交叉熵,就退化成「该 token 的负对数概率」。幻灯片写作

$$L(\theta) = \mathbb{E}_{x \sim P}\left[-\log Q_\theta(x)\right] = \mathbb{E}_{x \sim P}\left[-\sum_i \log Q_\theta(x_i \mid x_{

再按序列长度归一化,得到每 token 损失。损失越低越好。

这里必须澄清一个高频误解:

编者说明:交叉熵不是概率本身。它惩罚的是「给实际出现的事件分配了极小的概率」。模型给正确答案 0.99 的概率,损失约 0.01;给 0.05,损失就跳到 3.0 左右。损失可以是任意非负数,不是 $[0,1]$ 区间里的东西。

用自然对数时,损失的单位是 nats per token。$\exp(L)$ 称为困惑度(perplexity)。讲义提醒:当分词器或数据分布不同时,直接比较每 token 损失要非常小心——词表不同,每个 token 承载的信息量就不同。

用幻灯片上的具体数字感受一下:

$$\log\frac{1}{0.05} = \log 20 \approx 4.605.$$

一个 token 上 4.6 nats 的损失,说明模型在那个位置上基本是在瞎猜(若词表均匀分布,损失就是 $\log V = \log 4096 \approx 8.3$)。

默认设置下的细节

Held-out 评估

验证集是从梯度更新中扣下来的样本。在固定参数值下,它的平均损失估计总体目标:

$$\hat{L}_{\text{val}}(\theta) = \frac{1}{N_{\text{val}}}\sum_{i=1}^{N_{\text{val}}} \ell(\theta; x^{(i)}).$$

这个式子假设各行等长;更一般地应按预测目标数加权。评估期间不更新参数、不更新优化器状态,dropout 等随机机制关闭。

为什么要固定验证集?因为训练 batch 的损失本身噪声很大——每一步用的是不同样本。固定一批验证数据能让不同 checkpoint 之间可比。但讲义也补了一句很关键的警告:反复用同一个验证集挑选设计,会让这个验证集变成设计过程的一部分;要做独立的最终评估,需要另留一个测试集。

编者说明:训练/验证/测试三分、以及「用验证集反复调参会导致乐观偏差」这件事,sp26 春季版的泛化与数据划分一讲已经完整展开,这里只保留与本讲相关的结论:本讲的默认配置是验证集固定 1000 行 DCLM,num_evals=100 表示整个训练大约评估 100 次,对 $S$ 步的训练,间隔是 $\lfloor S/\text{num\_evals}\rfloor + 1$ 步,最后一次评估在训练结束时进行。改变评估频率不改变梯度目标,只改变监控分辨率和运行时间。

四、优化基础:小批量梯度下降与反向传播

这页把损失曲面、θt→θt+1 的一步移动和 minibatch 估计画在一起,比纯公式更能说明“梯度下降到底在做什么”

🖼️ 这页把损失曲面、θt→θt+1 的一步移动和 minibatch 估计画在一起,比纯公式更能说明“梯度下降到底在做什么”

为什么需要 minibatch

我们想用梯度更新把损失函数往下推。但损失梯度的精确估计需要对全部数据求期望——这在互联网文本上当然做不到。

$$\theta_{t+1} = \theta_t - \alpha \nabla_\theta L(\theta)$$

于是用一个指定大小的 minibatch 来估计。讲义给出这套记号:batch $B_s$ 在更新步 $s$ 上估计总体损失

$$ \hat{L}_s(\theta) = \frac{1}{|B_s|}\sum_{x \in B_s}\ell(\theta; x), \qquad g_s = \nabla_\theta \hat{L}_s(\theta_s), \qquad \theta_{s+1} = \theta_s - \alpha_s g_s . $$

$\alpha_s$ 是学习率。用采样出来的小批量,就得到随机梯度下降(Stochastic Gradient Descent, SGD)。

默认 batch size = 64,含义是每次优化器更新跨所有可用 GPU 共处理 64 条序列,因此一次完整的默认更新消费 $64 \times 1024 = 65{,}536$ 个输入 token。默认优化器是 AdamW 而不是上面那个朴素更新,但它吃的正是同一个 minibatch 梯度。

反向传播

一层计算 $h^{(\ell)} = f_\ell(h^{(\ell-1)}; \theta^{(\ell)})$,其中 $\theta^{(\ell)}$ 是该层的可训练权重。已知损失对本层输出的导数,链式法则给出

$$ \frac{\partial L}{\partial \theta^{(\ell)}_i} = \sum_j \frac{\partial L}{\partial h^{(\ell)}_j}\frac{\partial h^{(\ell)}_j}{\partial \theta^{(\ell)}_i}, \qquad \frac{\partial L}{\partial h^{(\ell-1)}_i} = \sum_j \frac{\partial L}{\partial h^{(\ell)}_j}\frac{\partial h^{(\ell)}_j}{\partial h^{(\ell-1)}_i}. $$

第一式给出参数梯度,第二式把梯度传给前一层。反向传播从损失出发,反向重复这两步。幻灯片把它概括为一句话:先算最后一层的梯度,再算它前面每一层的梯度。

PyTorch 的 autograd 在前向传播时记录计算图,loss.backward() 自动计算并累加这些参数梯度。典型工作流就是讲义给出的五行:

input_ids = train_batches[batch_idx]
loss = training_loss(input_ids)
loss = loss / config.num_micro_batches
loss.backward()
optimizer.step()

梯度累积与 micro-batch

一个大的逻辑 batch 可以拆成 $M$ 个 micro-batch。等大拆分时,全 batch 的平均梯度是

$$g_s = M^{-1}\sum_{j=1}^{M}\nabla_\theta \hat{L}_{s,j}(\theta_s).$$

可以逐项计算并把参数梯度累加起来,每算完一项就丢掉它的中间激活,再进入下一次前向。关键约束:所有 micro-batch 都贡献完之前,参数不能变。裁剪和优化器更新只做一次,不是每个 micro-batch 做一次;下一次逻辑更新前要清空梯度。

这也解释上面代码里那行 loss / config.num_micro_batches:每个 micro-batch 的损失先除以数量,累加起来才等于大 batch 的均值。

讲义给出的默认与注意事项:

编者拓展|为什么累加梯度要除以 $M$:loss.backward() 默认是累加梯度而不是覆盖。若每个 micro-batch 都直接 backward,得到的和是 $\sum_j \nabla \hat{L}_{s,j}$,比均值大了 $M$ 倍,等于把学习率悄悄乘上了 $M$。先除以 $M$ 再 backward,累加结果才等于上式中的均值。这是实现梯度累积时最常见的 bug。

Epoch 与训练预算

一个 epoch 是对选定训练集的一遍遍历。 $N$ 个样本、每批 $B$ 个、且 $B$ 整除 $N$ 时,一个 epoch 用 $N/B$ 次更新。多个 epoch 会重复看到样本;分数 epoch 会在走完一遍前停下。

讲义强调:「多看几遍同样的数据」和「多看一些不同的数据」是两种不同的干预,即使 token 总数相同。

训练预算决定了学习率调度在多长的尺度上展开。因此改 epoch 或 batch size 会同时改变「数据被看多少遍」和「在某个 token 数处学习率是多大」。比较两次运行时,必须讲清楚是等更新步数、等 token 数、还是等墙钟时间。

默认配置:num_epochs=1.0,600,000 行,batch size 64,正好

$$\frac{600{,}000}{64} = 9375 \text{ 次优化器更新}.$$

代码里用 $S = \lfloor \text{num\_epochs} \cdot \lfloor K/M \rfloor \rfloor$,$K$ 是 micro-batch 数(可能含一个不完整的),$M$ = num_micro_batches。默认尺寸整除,所以不完整的 batch 约定不影响默认运行。训练到这个预算就停,不会因为验证损失不再下降而自动早停。

训练循环可以画成下面这条回路——注意裁剪与 step() 都发生在「所有 micro-batch 都贡献完」之后:

flowchart TD
    A[取出一个逻辑 batch<br/>batch_size = 64] --> B[切成 M 个 micro-batch]
    B --> C[前向:loss_j = training_loss]
    C --> D[loss_j / M]
    D --> E[loss_j.backward<br/>梯度累加]
    E -->|还有 micro-batch| C
    E -->|全部贡献完| F[梯度裁剪<br/>全局范数不超过 1]
    F --> G[optimizer.step<br/>AdamW 更新]
    G --> H[清零梯度<br/>scheduler 前进一格]
    H -->|s < S| A
    H -->|s = S| I[停止:按预算而非早停]

五、优化技巧:学习率调度、动量、Adam、权重衰减与梯度裁剪

这一节是整份配方里旋钮最密集的地方。逐个看。

学习率曲线(warmup 抬升 + 线性衰减 + 峰值 αmax)用一张图就说清了调度形状,纯文字很难让初学者建立“横轴是训练进度”的直觉

🖼️ 学习率曲线(warmup 抬升 + 线性衰减 + 峰值 αmax)用一张图就说清了调度形状,纯文字很难让初学者建立“横轴是训练进度”的直觉

学习率与调度

学习率决定每一步走多远。太小参数动得慢,太大足以让更新不稳定。调度(schedule) 规定这个尺度如何随训练进度变化:warmup 在训练早期从小步逐渐放大,decay 在后期把它收小。

给定计划更新数 $S$、warmup 步数 $W$、峰值学习率 $\alpha_{\max}$ 和调度索引 $s$,线性 warmup + 线性衰减为

$$ \alpha_s = \alpha_{\max}\cdot \begin{cases} s / W, & 0 \le s < W,\\[4pt] (S - s)/(S - W), & W \le s \le S. \end{cases} $$

$W = 0$ 时省略 warmup 分支。warmup 之后令 $u = (s - W)/(S - W)$,余弦衰减改用 $\alpha_s = \alpha_{\max}\bigl(1 + \cos(\pi u)\bigr)/2$;常数调度保持学习率不变;warmup–stable–decay(WSD) 在中间阶段保持峰值,只在最后选定的一段比例内线性衰减。

默认设置:learning_rate=0.003、lr_schedule="linear"、warmup_percent=0.01。默认运行时 warmup 持续

$$\lfloor 0.01 \times 9375 \rfloor = 93 \text{ 步}.$$

调度器从索引 0 开始,因此启用 warmup 时,第一次更新用的学习率是 0;之后每次优化器更新推进一格。其它可选值为 "cos"、"constant"(无 warmup),以及形如 "wsd0.1"(在最后 10% 的步数上线性衰减)。

编者说明:第一次更新学习率为 0 听起来像 bug,其实是 warmup 的下标约定导致的正常现象——那一步的梯度仍会被算出、优化器状态仍会更新,只是步长乘了 0。想避免可以在实现里用 $s+1$ 作为 warmup 计数。

动量

动量维护一个来自过去梯度的「方向」。常见写法(buffer 记作 $b_s$,更新从 $s=1$ 起编号,$b_0 = 0$):

$$ b_s = \mu b_{s-1} + g_s, \qquad \theta_{s+1} = \theta_s - \alpha_s b_s . $$

$\mu$ 控制早期梯度能贡献多久。若过去梯度都指向同一方向,动量会加速这个方向上的进展;若它们彼此冲突,动量会把冲突的部分抵消掉。$\mu = 0$ 时退化为朴素 SGD。

讲义特别提醒一个实现层面的坑:这个 buffer 用的是求和,而不是 Adam 里那种带 $(1-\mu)$ 的滑动平均。两套约定的「学习率该怎么解释」并不相同,跨实现比较时不能直接对号入座。

默认设置:optimizer_name="sgd" 选取带动量的 SGD,$\mu$ 就是 beta1,默认 0.9,beta1=0 即朴素 SGD。没有 Nesterov 动量,也没有 dampening,SGD 下 beta2 不使用。对需要衰减的参数,SGD 会把 $\lambda\theta_s$ 加到梯度上再更新动量 buffer——这是耦合的 L2 正则,与 AdamW 独立的衰减步骤不是一回事。

Adam 与自适应更新

Adam 对每个参数维护两个滑动平均:梯度的一阶矩 $m_s$ 和梯度平方的二阶矩 $v_s$。本小节从 $s=1$ 编号,$m_0 = v_0 = 0$,$g_s$ 是交给优化器的梯度(若启用裁剪,则是裁剪之后的):

$$ \begin{aligned} m_s &= \beta_1 m_{s-1} + (1-\beta_1) g_s, &&\text{梯度平均}\\ v_s &= \beta_2 v_{s-1} + (1-\beta_2)(g_s \odot g_s), &&\text{梯度平方平均}\\ \hat{m}_s &= m_s / (1 - \beta_1^s), \quad \hat{v}_s = v_s / (1 - \beta_2^s), &&\text{初始化偏差修正}\\ \theta_{s+1} &= \theta_s - \alpha_s \frac{\hat{m}_s}{\sqrt{\hat{v}_s} + 10^{-8}}, &&\text{衰减前的更新} \end{aligned} $$

乘积、平方、开方、除法都是逐坐标进行的。

默认:optimizer_name="adamw",用 Adam 的自适应更新,beta1=0.9、beta2=0.95,外加解耦的权重衰减;数值稳定项固定为 $10^{-8}$,不是可配置项。CUDA 上使用 PyTorch 的 fused AdamW 实现。

权重衰减与参数分组

权重衰减把选定参数向零收缩。在 AdamW 里,它与梯度的滑动平均是解耦的。对被衰减的参数,合并后的更新是

$$ \theta_{s+1} = (1 - \alpha_s\lambda)\,\theta_s - \alpha_s\frac{\hat{m}_s}{\sqrt{\hat{v}_s} + 10^{-8}} . $$

其中 $\lambda$ 是衰减系数。注意这里有两个直接后果:

  1. 有效衰减强度同时取决于 $\lambda$ 和学习率调度——学习率变小,衰减也跟着变弱。
  2. 在自适应优化器里,把 $\lambda\theta$ 塞进梯度会连矩估计一起改掉,那与上式不是同一条规则。

参数可以分组、给不同衰减设置:让大的投影矩阵被衰减,同时让嵌入或归一化尺度不被衰减。讲义在这里专门澄清一句:

编者说明:被排除在衰减之外,不等于被冻结。它仍然接收梯度更新,只是不接受朝向原点的那一份收缩。

默认设置:weight_decay=0.1,且衰减永远是带掩码的——嵌入参数、归一化参数和 bias 被排除。未绑定的输出投影是一个线性层权重,会被衰减;当它与输入嵌入绑定时,那个共享参数则被排除。注意力和 MLP 的投影权重会被衰减。这套掩码策略是固定的,没有额外的开关。

梯度裁剪

偶尔某个 batch 会产生异常大的梯度。全局范数裁剪把所有参数梯度在概念上拼成一个大向量,限制它的联合欧氏范数。阈值 $c$ 下,

$$ g_s^{\text{clip}} = g_s \cdot \min\left(1, \frac{c}{\|g_s\|_2}\right), $$

零梯度时取因子为 1。裁剪保持梯度方向不变,只在超过阈值时缩小其幅度——这与对每个分量分别裁剪是不同的操作。

裁剪之后,优化器处理的是被裁剪的梯度。但要注意两点:对 Adam 而言,限制梯度范数并不直接限制参数更新的范数,因为矩估计和逐坐标重缩放也在起作用;权重衰减是另外一项贡献,不受这个操作约束。

默认设置:grad_norm=1.0,在所有 micro-batch 累积完毕之后、优化器 step 之前裁剪全局参数梯度范数;grad_norm=None 关闭。还有一个诊断上的细节:在 backward 之后立刻收集的度量看到的是裁剪前的梯度,可能超过 1.0。

数值精度与执行

浮点运算存的是实数的近似。fp32 用 32 位;bfloat16(bf16) 用 16 位,指数范围相近但有效数字更少。低精度省内存、在合适硬件上能加速矩阵乘法,但会改变舍入误差。

参数、中间计算、梯度存储、优化器状态的 dtype 不必都一样。混合精度让部分状态留在 fp32,只把选定运算放到低精度。autocast 是按运算选择 dtype,不是简单地把整个模型和所有归约都转成 bf16。参数与矩留在 fp32 有助于保留微小更新,但它无法挽回更早的低精度计算已经引入的误差。

编译与融合核把多个运算合并或减少调度开销。它们旨在保持数学计算不变,但可能改变浮点求值顺序;把 batch 拆到多 GPU 上也一样。这两种手段都没有定义新的损失函数。

默认设置:precision="mp" 把参数、其梯度和 Adam 状态存在 fp32,CUDA 上对部分运算用 bf16 autocast。"fp32" 关闭 autocast;"bf16" 连参数也存 bf16,属于另一套数值配方。所有模式下损失都在 fp32 中求值;RMSNorm 的均方计算和 RoPE 的三角函数也用 fp32。默认的 bf16 混合精度路径不使用梯度 scaler。torch_compile_mode="reduce-overhead" 选定单 GPU、单 micro-batch 下训练损失计算的编译模式;多 GPU 走数据并行,不使用这条编译路径。做速度对比时,应把启动与评估开销也算进去,而不只看优化器步的耗时。

随机种子与确定性

伪随机生成器从种子产生可复现的随机流。随机初始化、dropout、数据置换都会影响实验。控制种子让比较更容易解释,但只设种子并不保证 GPU 计算逐位一致——某些并行算法有不确定的归约顺序。

确定性执行会限制算法、避开已知的非确定来源,可能更慢,也可能直接拒绝某个没有确定性实现的操作。跨设备、跨库版本、跨精度仍然需要小心。

默认:model_seed=42 管初始化与模型侧随机性;data_seed=42 独立地选择训练行及其顺序;model_seed=None 表示跳过显式的模型播种。deterministic=False(默认)不强制确定性算法;开启它会请求 PyTorch 的确定性行为、配置确定性 CUDA 矩阵乘法,并关闭 TF32 与 cuDNN benchmarking——但仍不承诺跨硬件、跨软件版本结果一致。

六、优化诊断与实验记录

只看 loss 不足以描述优化的内部状态。讲义列出几类可以追踪的量:

RMS:一个尺度统计量

对一个有 $n$ 个元素的张量 $a$,常用的尺度统计量是均方根(Root Mean Square, RMS):

$$\mathrm{RMS}(a) = \sqrt{n^{-1}\sum_i a_i^2}.$$

为什么默认用它而不是欧氏范数?因为 RMS 除以了元素个数,所以不像欧氏范数那样直接随张量大小增长——跨不同形状的模块比较时更有意义。

讲义同时提醒:RMS 不是标准差,除非均值恰为 0。

测量的时机就是测量的含义

这一条值得单独强调:指标在什么时刻被测量,决定了它描述的是什么。

这些是不同的状态,混着比会得出错误结论。另外,度量本身也可能带来额外的前向或反向开销——记录不是免费的。还有一条纪律:观测性的度量不应该更新模型,也不应该意外改变后续训练的随机性。

默认的 logger 配置

metric_loggers 把 Python 诊断挂到三个时刻:backward 之后、优化器 step 之后、评估之后。它们能拿到当前模型、优化器、调度器、batch、loss、step、配置以及验证 batch;返回的标量记在 logging/ 下。

当前默认包含每个模块与全局的激活、参数、裁剪前梯度 RMS,记录在从 0 开始的第 0、100、200…… 步。这里有一个容易误读的点:全局激活统计汇总的是被记录的可参数化叶子模块的输出,而不是某一个隐状态。

wandb_online=True 启用 Weights & Biases 记录和这些自定义 logger;wandb_entity 与 wandb_project 选择写入目标;wandb_tags、run_name_suffix 只用于组织实验,不改变机器学习的计算。

还有一处时间差值得记住:记录的训练损失是在该步更新之前测的,而同一轮的验证损失用的是更新之后的模型。

从 checkpoint 初始化与续训

加载模型权重只是给新实验一个起点;续训还会恢复优化器的历史(例如 Adam 的矩估计)。所以即使起始权重完全相同,两者的后续更新也不同。

七、架构总览:从 token ID 到 logits 的完整通路

这是全讲唯一的整体架构图,把嵌入、若干块、unembedding 与 softmax 串成一条链并标出每步的形状,是后面所有细节小节的定位地图

🖼️ 这是全讲唯一的整体架构图,把嵌入、若干块、unembedding 与 softmax 串成一条链并标出每步的形状,是后面所有细节小节的定位地图

维度符号约定

先统一记号,后面所有公式都用这一套:

符号 含义
$B$ 一次前向的 batch 内序列数
$T$ 每条序列的 token 位置数
$V$ 词表大小
$L$ transformer 块数(深度)
$d$ 隐层 / 残差流宽度
$H, d_h$ 注意力头数与每头维度,满足 $d = H d_h$
$d_{ff}$ MLP 中间宽度

batch 轴索引的是互相独立的序列。一个 token 的表示有 $d$ 个坐标,这些是学出来的特征,不是词表上的概率。深度与宽度改变的是可表示函数的族与计算代价。

默认模型 d8:$L = 8$、$d = 512$、$H = 8$、$d_h = 64$、$d_{ff} = 1792$。因此 d8 有 8 个注意力模块和 8 个 MLP 模块。

前向通路

模型先为每个输入 token 查出一个向量,然后一叠 transformer 块不断更新这些向量,最后经一次归一化和输出投影变成下一个 token 的分数。每个块由两部分组成:注意力在位置之间混合信息,MLP 在每个位置上独立做非线性计算。这是一个 decoder-only 模型:只从前面 token 的上下文预测,没有独立编码器,也没有对编码器输出的 cross-attention。

形式化地,输入 ID $X \in \{0,\dots,V-1\}^{B\times T}$,嵌入表 $E \in \mathbb{R}^{V\times d}$,输出矩阵 $W_U \in \mathbb{R}^{d\times V}$:

$$ \begin{aligned} H^{(0)} &= E[X] \in \mathbb{R}^{B\times T\times d}, &&\text{token 向量}\\ H^{(\ell)} &= \mathrm{Block}_\ell\bigl(H^{(\ell-1)}\bigr) \in \mathbb{R}^{B\times T\times d}, &&\ell = 1,\dots,L\\ \tilde{H} &= \mathrm{RMSNorm}\bigl(H^{(L)}\bigr) \in \mathbb{R}^{B\times T\times d}, &&\text{最终归一化}\\ Z &= \tilde{H}W_U \in \mathbb{R}^{B\times T\times V}, &&\text{词表分数}\\ P &= \mathrm{softmax}_V(Z) \in \mathbb{R}^{B\times T\times V}, &&\text{下一 token 概率} \end{aligned} $$

默认实现是 decoder-only、pre-norm 的 transformer,块之间各自独立参数化。它返回的是 logits;对数概率由损失计算那一步从 logits 形成。 model_builder=None 就用这个模型;自定义的 model_builder 与 model_builder_kwargs 可以接入实验性架构,只要它遵守训练与保存接口——这些都不会自动改变默认模型。

八、嵌入层与 Transformer 块:形状保持、残差与 Pre-Norm

这页给出单个块的内部结构与输入输出形状,配合“形状不变所以能堆叠”这句话,初学者才看得懂 8 层是怎么搭起来的

🖼️ 这页给出单个块的内部结构与输入输出形状,配合“形状不变所以能堆叠”这句话,初学者才看得懂 8 层是怎么搭起来的

嵌入查表

嵌入表里每个词表项对应一个学出来的向量。对 token ID $v$,查表返回第 $v$ 行 $E_v \in \mathbb{R}^d$。同一张表在每个位置都用。

查表等价于用一个 one-hot 向量去乘这张表,但实现上不需要真的构造那个几乎全是零的 one-hot 数组。学习可以让这些 token 向量对预测有用,而不必给每个单独坐标赋予人可读的含义。

默认设置:嵌入表形状 $4096 \times 512$;不对它的输出额外乘 $\sqrt{d}$ 因子,也不加可学习的绝对位置向量。

形状保持是能堆叠的原因

每个块吃 $B \times T \times d$、吐出 $B \times T \times d$。正因为形状不变,才能想堆多少层就堆多少层(默认 8 层)。

残差连接

残差连接不是 $x \mapsto f(x)$,而是

$$x \mapsto x + f(x).$$

讲义称这条不断流动的隐表示为 residual stream(残差流)。分支学到的是对这条流的改动,而不是整体替换它。

加法带来一个直接后果:梯度除了能走分支那条路,还能走恒等通路直通回去。需要说清楚的是——残差连接并不保证优化稳定,它改变的是信息与梯度传播的方式。

幻灯片在这里有一句提醒:如第一讲所示,把这个细节写对,重要得有点离奇。

编者说明:这句话指向的是「看起来无关紧要的实现细节其实决定训练成败」这一类现象。讲义没有在本讲给出具体的失败数值,所以我们也不虚构——只需要知道:残差路径的正负号、是否被归一化、是否被缩放,都属于会改变结论的实现细节。

Pre-Norm 与 Post-Norm

在 pre-norm 块里,每个分支拿到的是残差流的归一化版本;归一化在分支内部,跳连路径保持未归一化的流。这与「先相加、再归一化相加结果」(post-norm)不同。

对一个块,记 $H \in \mathbb{R}^{B\times T\times d}$:

$$ \begin{aligned} Y &= \mathrm{RMSNorm}_{\text{attn}}(H), &&A = \mathrm{Attention}(Y),\\ H' &= H + \mathrm{Dropout}(A), &&\text{第一次残差相加}\\ U &= \mathrm{RMSNorm}_{\text{mlp}}(H'), &&F = \mathrm{MLP}(U),\\ H^{\text{out}} &= H' + \mathrm{Dropout}(F), &&\text{第二次残差相加} \end{aligned} $$

块内顺序:RMSNorm → 注意力 → 残差相加 → RMSNorm → MLP → 残差相加。整个堆叠之后,再接一个独立的最终 RMSNorm,然后才进 unembedding。

默认设置:每个块有两次残差相加(一次围注意力、一次围 MLP),都用 pre-norm,各自有独立学习的 RMSNorm 尺度;跳连路径上没有随深度变化乘子。

RMSNorm

归一化控制后续运算看到的尺度。对一个 token 向量 $h \in \mathbb{R}^d$:

$$\mathrm{RMSNorm}(h)_i = \gamma_i \cdot \frac{h_i}{\sqrt{d^{-1}\sum_{j=1}^{d} h_j^2 + 10^{-5}}}, \qquad i = 1,\dots,d.$$

一个常见误解值得点明:归一化并不意味着之后所有激活的 RMS 都是 1。可学习的尺度、后续投影、残差相加都会改变幅值。在 pre-norm 下,被归一化的是分支的输入,而不是每次运算之后的整条残差流。

默认设置:每个块有 2 个宽度为 $d$ 的 RMSNorm,模型另有一个最终 RMSNorm;它们的尺度从 1 开始,不参与权重衰减;稳定项固定为 $10^{-5}$。query/key 归一化用的是同一操作,只是作用在 $d_h$ 个坐标上。

九、注意力:Q/K/V、因果掩码与 T×T 注意力矩阵

这页把 T×T 的注意力矩阵、因果掩码的 0/-∞ 模式与“某一行只读前面几个 value”的加权过程画在一起,是理解因果注意力的核心图

🖼️ 这页把 T×T 的注意力矩阵、因果掩码的 0/-∞ 模式与“某一行只读前面几个 value”的加权过程画在一起,是理解因果注意力的核心图

Q、K、V 在做什么

自注意力让每个位置能够从同一序列中更早的位置读取信息。三个角色:

这三者都是当前 token 表示的可学习投影,不是外部提供的标签。

考虑单条序列、略去 batch 轴,其表示构成 $Y \in \mathbb{R}^{T\times d}$。单个注意力头用三个可学习矩阵 $W_Q, W_K, W_V \in \mathbb{R}^{d\times d_h}$,另一个输出投影 $W_O \in \mathbb{R}^{d_h\times d}$ 把混合结果映射回残差流宽度:

$$ \begin{aligned} Q &= YW_Q \in \mathbb{R}^{T\times d_h}, &&\text{queries}\\ K &= YW_K \in \mathbb{R}^{T\times d_h}, &&\text{keys}\\ V_a &= YW_V \in \mathbb{R}^{T\times d_h}, &&\text{values}\\ S &= QK^\top/\sqrt{d_h} + C \in \mathbb{R}^{T\times T}, &&\text{带掩码的相似度分数}\\ P_a &= \mathrm{softmax}_{\text{keys}}(S), &&\text{对源位置的权重}\\ O &= P_aV_a \in \mathbb{R}^{T\times d_h}, &&\text{value 的加权混合}\\ \mathrm{Attention}(Y) &= OW_O \in \mathbb{R}^{T\times d}, &&\text{投影回残差流宽度} \end{aligned} $$

因果掩码

分数 $S_{t,u}$ 比较位置 $t$ 的 query 与位置 $u$ 的 key。因果掩码定义为

$$ C_{t,u} = \begin{cases} 0, & u \le t,\\ -\infty, & u > t. \end{cases} $$

softmax 把每一行分数变成非负、和为 1 的权重。于是位置 $t$ 的中间混合是它看得见的那些 value 向量的加权平均:

$$O_t = \sum_{u \le t} P_{a,t,u}\, V_{a,u},$$

该位置的注意力输出是 $O_tW_O$。

两点必须讲清楚:

  1. 位置 $t$ 可以看见自己。 这不是信息泄露——位置 $t$ 的输出是用来预测 token $t+1$ 的,看见自己并不暴露预测目标。
  2. 未来位置($u > t$)在 softmax 后权重为 0,因为 $-\infty$ 送进 softmax 就变成 0。

除以 $\sqrt{d_h}$ 是为了在头宽变化时控制点积的尺度:点积的方差随 $d_h$ 增长,不除会让分数过大、softmax 过度集中到近乎 one-hot;反过来若权重几乎均匀,混合出的 value 又会缺乏区分度。这两种极端给出的是不同的混合结果,而 $\sqrt{d_h}$ 让尺度落在可用区间。

默认设置:每个头都用这套因果计算,$d_h = 64$。在形成分数之前,先施加 RoPE,再对 query 和 key 施加可选的 QK 归一化(下一节)。注意力 dropout 若启用,作用在 $P_a$ 上、乘以 value 之前,默认为 0。

十、多头注意力、RoPE 与 QK Norm

RoPE 只靠文字很难想象,这页用“旋转成对维度”的图示说明位置信息是怎么被塞进 query/key 的,是这一节最值得看的图

🖼️ RoPE 只靠文字很难想象,这页用“旋转成对维度”的图示说明位置信息是怎么被塞进 query/key 的,是这一节最值得看的图

多头注意力

多头注意力在同一个输入 $Y$ 上并行跑 $H$ 个头,每个头有自己的 query/key/value 投影。每个头可以形成不同的位置关注模式。 各头的混合结果 $O^{(h)}$(在输出投影之前)沿特征轴拼接,再用一个可学习输出投影 $W_O \in \mathbb{R}^{Hd_h \times d}$ 混回宽度 $d$:

$$ \begin{aligned} O^{(h)} &= \mathrm{Head}_h(Y) \in \mathbb{R}^{T\times d_h}, &&h = 1,\dots,H\\ O_{\text{concat}} &= \bigl[\,O^{(1)} \cdots O^{(H)}\,\bigr] \in \mathbb{R}^{T\times Hd_h}, &&\text{拼接各头输出}\\ \mathrm{Attention}(Y) &= O_{\text{concat}}W_O \in \mathbb{R}^{T\times d}, &&\text{回到残差流宽度} \end{aligned} $$

各头是并行计算的,不是依次计算的。 有 $B$ 条序列时,query/key/value 可以排成 $B \times H \times T \times d_h$,注意力权重形状是 $B \times H \times T \times T$,合并输出形状是 $B \times T \times d$。

这里要区分两个矩阵:$W_O$ 混合的是不同头的特征,它与预测词表 ID 的 unembedding 矩阵完全是两回事。

默认设置:d8 用 $H = 8$ 个头、每头 $d_h = 64$,$H d_h = d = 512$。每头的 query/key/value 矩阵被合起来存成三个 $512\times512$ 的投影;$W_O$ 也是 $512\times512$。计算使用 scaled dot-product attention(SDPA),配合合适的 GPU kernel 可以不显式materialize 整个注意力矩阵——上面的方程描述的是运算语义,不是「必须把 $T\times T$ 的分数都存下来」的要求。

讲义另有一条脚注值得记下:grouped-query attention(GQA) 让若干 query 头共享更少的 key/value 头,在默认模型里是关闭的——d8 用 8 个 query 头和 8 个 key/value 头。自定义配置可以减小 num_key_value_heads,前提是它能整除 num_attention_heads。

RoPE:用旋转编码位置

token 向量本身并不显式编码位置之间的距离。旋转位置编码(Rotary Position Embeddings, RoPE) 通过旋转 query 与 key 的坐标对来加入这个信息,而不是往残差流里加一个位置向量。

对位置 $t$,一对坐标被变换为

$$ R(t\omega_i) = \begin{pmatrix} \cos(t\omega_i) & -\sin(t\omega_i)\\ \sin(t\omega_i) & \cos(t\omega_i) \end{pmatrix}, \qquad \omega_i = \theta_{\text{RoPE}}^{-2i/d_h}, \quad i = 0,\dots,d_h/2 - 1. $$

设 $q_t = (YW_Q)_t$、$k_t = (YW_K)_t$ 是位置 $t$ 上投影出的 query 与 key,各有 $d_h$ 个坐标。用从 0 开始的坐标下标、把向量的前后两半配对,旋转为

$$ \begin{pmatrix}\tilde{q}_{t,i}\\ \tilde{q}_{t,i+d_h/2}\end{pmatrix} = R(t\omega_i)\begin{pmatrix}q_{t,i}\\ q_{t,i+d_h/2}\end{pmatrix}, \qquad \begin{pmatrix}\tilde{k}_{t,i}\\ \tilde{k}_{t,i+d_h/2}\end{pmatrix} = R(t\omega_i)\begin{pmatrix}k_{t,i}\\ k_{t,i+d_h/2}\end{pmatrix}. $$

对所有配对重复,得到旋转后的 $\tilde{q}_t, \tilde{k}_t \in \mathbb{R}^{d_h}$。它们替换注意力中的原始 query 和 key(若启用 QK 归一化,接下来就作用在它们身上)。

关键性质:不同配对以不同频率旋转。由于

$$R(t\omega_i)^\top R(u\omega_i) = R\bigl((u - t)\omega_i\bigr),$$

旋转后 query 与 key 的内积只依赖相对偏移 $u - t$。这就是「相对位置表示」的来源——内容和位置都影响注意力。value 不旋转。

编者说明:这个恒等式其实就是旋转矩阵的正交性——$R(\alpha)^\top R(\beta) = R(\beta - \alpha)$。写成复数更直观:把坐标对看作复数 $q e^{it\omega}$,内积取实部后剩下 $\cos((u-t)\omega)$,位置差自然浮现。讲义没展开这一步,但它是理解「为什么 RoPE 是相对的」最省力的路径。

频率基底决定频率的范围。讲义特别区分两件事:架构支持的最大位置索引与训练上下文长度是两回事——在代码里允许更长的输入,并不意味着模型学会了可靠地使用这种长度。

默认设置:RoPE 基底固定 $\theta_{\text{RoPE}} = 500{,}000$,三角函数在 fp32 中计算、再转换类型参与注意力计算;配对方式是每个头向量的前后两半;每个行内位置从 0 重新开始。max_position_embeddings=131_072 是架构的长度上限,而实际默认训练上下文仍是 1024。没有可训练的位置嵌入参数。

QK Norm

query 和 key 的模长会影响注意力分数,不只是方向。QK 归一化(QK norm) 在缩放点积之前,对每个头的 query 向量和 key 向量分别做一次 RMSNorm。

它削弱的是分数对输入向量模长的直接依赖;可学习的尺度参数仍然允许分数尺度在训练中变化。value 不参与这个归一化。

默认设置:qk_norm=True,在 RoPE 之后对 query 与 key 各自施加独立学习的 RMSNorm 尺度,作用在每个头的 64 个坐标上。块内这些尺度跨头共享,但 query 与 key 两个模块之间不共享;稳定项 $10^{-5}$。qk_norm=False 则省去这两处归一化。

十一、SwiGLU MLP、Unembedding 与 Dropout

SwiGLU 的“上投影—门控非线性—逐元素相乘—下投影”结构用图看比读公式快得多,这页是本节唯一值得插入的结构图

🖼️ SwiGLU 的“上投影—门控非线性—逐元素相乘—下投影”结构用图看比读公式快得多,这页是本节唯一值得插入的结构图

MLP:逐 token 的非线性

MLP 用在所有位置上共享的权重,对每个 token 独立做变换。它改变单个 token 内部的特征;与注意力不同,它不混合位置。先扩张到中间宽度,可以在回到残差流宽度之前形成特征的组合。

课程的带门控 MLP 有三个矩阵:$W_g, W_u \in \mathbb{R}^{d\times d_{ff}}$ 和 $W_d \in \mathbb{R}^{d_{ff}\times d}$:

$$ \begin{aligned} G &= YW_g, \quad U = YW_u, &&G, U \in \mathbb{R}^{B\times T\times d_{ff}}\\ A &= \mathrm{SiLU}(G) \odot U, &&A \in \mathbb{R}^{B\times T\times d_{ff}}\\ \mathrm{MLP}(Y) &= AW_d, &&\mathrm{MLP}(Y) \in \mathbb{R}^{B\times T\times d} \end{aligned} $$

其中 $\mathrm{SiLU}(z) = z\,\sigma(z)$,$\sigma(z) = 1/(1+e^{-z})$,逐元素施加。非线性门控通过逐元素乘积 $\odot$ 去调制另一条投影表示,这个组合就叫 SwiGLU。

为什么必须要非线性?讲义给了一句干脆的话:没有非线性、没有门控的话,一串线性映射会坍缩成单个线性映射——堆多少层都等于一层。

默认设置:d8 用 $d = 512$、$d_{ff} = 1792$;三个投影都不带 bias;预设系列里扩张比是 3.5,自定义配置可以改 intermediate_size。门控内部没有额外 dropout;残差分支的 dropout 施加在下投影之后。

编者拓展|SwiGLU 的来历:门控线性单元出自 GLU Variants Improve Transformer(Noam Shazeer, arXiv:2002.05202)。该文在相同参数量预算下比较了若干 GLU 变体,SwiGLU(SiLU 门控)表现较好,因而被后续多个开源 LLM 采用。注意「相同参数量」这个前提:因为多了第三条矩阵,采用 SwiGLU 时通常会调小 $d_{ff}$ 来补偿参数预算——本讲的 3.5 倍就是这个背景下的一条经验比例(常见做法是把 $4d$ 按 2/3 折算,$4 \times \frac{2}{3} \approx 2.67$,本课程取 3.5 是它自己的配方选择,不是论文结论)。

Unembedding、Softmax 与权重绑定

最终 RMSNorm 之后,unembedding 把每个隐向量映射成 $V$ 个 logits,每个词表项一个。对 $\tilde{h}_t \in \mathbb{R}^d$、$W_U \in \mathbb{R}^{d\times V}$,记 $z_t = \tilde{h}_tW_U \in \mathbb{R}^V$。softmax 把这些无约束的实分数变成概率:

$$p_\theta(x_{t+1} = v \mid x_{\le t}) = \frac{e^{z_{t,v}}}{\sum_{u=0}^{V-1} e^{z_{t,u}}}.$$

权重绑定(weight tying) 把输出矩阵约束为 $W_U = E^\top$,其中 $E$ 是输入嵌入表。此时只有一个共享的可训练张量,而不是两个张量定期互相复制。它作为输入和输出两种角色收到的梯度会累加到同一个参数上。绑定减少了独立参数数量,也约束了输入表示与输出分数之间的关系。

默认不绑定:tie_word_embeddings=False 让输入和输出各用一套参数;设成 True 则共享。未绑定的 d8 有一张 $4096\times512$ 输入表和一张独立的 $512\times4096$ 数学形状输出矩阵,输出投影没有 bias。

这里有一个必须讲清的区分:

编者说明:两个独立参数也可以从相关的初值出发——下一节会看到,未绑定时输入表和输出表都由同一个基底 $A$ 缩放而来,初始方向是对齐的。那不是权重绑定。 绑定是一个结构约束(始终共享同一个张量),相关初始化只是一个起点(之后各自独立更新)。

Dropout

Dropout 在训练时随机去掉激活中的一部分元素。对概率 $p < 1$,独立的掩码 $m_i \sim \mathrm{Bernoulli}(1-p)$ 给出

$$\mathrm{Dropout}(a)_i = \frac{m_i a_i}{1-p}.$$

这个 $1/(1-p)$ 的缩放保持每个元素的条件期望不变。

评估时 dropout 是恒等映射:不施加随机掩码,也不缩放。

不过讲义提醒一句:在某一层保持期望,并不能让非线性网络的训练输出等于其评估输出。这是个容易被过度乐观的直觉。

三种 dropout 位置不同,即使概率相同也不是一回事:

默认设置:dropout=0.0,默认关闭 dropout。非零时统一支配块内三处用法:注意力权重、注意力残差输出、MLP 残差输出。评估会关掉全部,因此非零 dropout 下训练损失与验证损失同时在数据和随机计算两方面不同。

编者说明:注意力 dropout 不重新归一化这一点值得留意。它意味着幸存权重之和小于 1,等价于在软注意力的输出上叠了一层整体收缩。某些实现会做 renorm,两种约定给出的语义不同,跨实现比较时要确认。

十二、初始化与模型缩放配方

这页把 L、d=64L、H=L、dff=3.5d 这几条配方并排列出,并点出参数按 d³ 增长,是理解“模型变大意味着什么”的关键一页

🖼️ 这页把 L、d=64L、H=L、dff=3.5d 这几条配方并排列出,并点出参数按 d³ 增长,是理解“模型变大意味着什么”的关键一页

为什么尺度要按维度定

初始化为优化选起点。随机权重打破特征之间的对称性,而它们的尺度影响初始激活与梯度。

对求和了约 $d_{\text{in}}$ 项独立贡献的投影,把权重幅度按 $1/\sqrt{d_{\text{in}}}$ 缩放,可以避免仅仅因为输入宽度变大就让输出尺度膨胀。讲义强调这是一个尺度论证,不是对整个网络动力学的保证。

截断正态是从正态分布里条件采样到给定区间内。先指定正态的标准差、再截掉尾部,会让最终的实际标准差略小于名义值——这个细节在对比不同初始化方案时要注意。

同时,归一化尺度和嵌入矩阵不必遵循与其它投影相同的初始化规则。

默认初始化

对注意力与 MLP 矩阵:元素从零均值正态中抽取,名义标准差 $1/\sqrt{d_{\text{in}}}$,在三个名义标准差处截断。所有 RMSNorm 尺度从 1 开始。

对嵌入:先从标准正态(截断到 $[-3,3]$)抽一个基底表 $A \in \mathbb{R}^{V\times d}$。默认的未绑定情形下

$$E = A/d, \qquad W_U = A^\top/\sqrt{d}.$$

于是两张表起始方向对齐,但之后各自独立更新。绑定情形下,共享表从 $E = A/\sqrt{d}$、$W_U = E^\top$ 起步。

另外:默认没有任何投影被初始化为零,也没有对残差分支权重施加随深度变化的额外缩放。model_seed 控制这些抽样;初始化尺度由配方固定,不作为另一个 TrainConfig 字段暴露。

编者说明:这里有个容易困惑的点——未绑定时 $E$ 和 $W_U$ 的尺度因子不同($1/d$ 与 $1/\sqrt{d}$),但方向来自同一个 $A$。你可以理解为「起点指向同一个方向,但两边各自的量级不同」;一旦训练开始,两者的更新历史完全独立,相关性会迅速消失。

深度缩放配方

深度是依次施加的 transformer 块数。只加深,会在每个块维度固定的前提下追加更多注意力与 MLP 计算;新增的块有各自的可训练参数,不是同一套权重重复施加。

一个模型族也可以让深度与宽度一起增长。更宽的残差流要求更宽的嵌入、注意力投影、MLP 与归一化尺度。注意力可以通过加头数、加大每头维度,或两者同时来进行。

讲义也澄清一句:更多的块并不增加可用的上下文长度——因果注意力本来就已经能读到一行内所有前面的位置。

课程给的是一个默认的放大配方:给定深度 $L$,

model_name 预设覆盖从 d4 到 d18 的每一个整数深度。d8 是一整套尺寸配置的简称,不只是块数。 对预设 dL:

组件 规则 d4 d8 d12
Transformer 块数 $L$ 4 8 12
残差宽度 $d$ $64L$ 256 512 768
Query 头数 $H$ $L$ 4 8 12
Key/value 头数 $L$ 4 8 12
每头维度 $d_h$ 64 64 64 64
MLP 宽度 $d_{ff}$ $3.5d = 224L$ 896 1792 2688

这些选择会让模型的多个部分一起变化:

词表大小(4096)、上下文长度(1024)、位置上限、RoPE 频率都保持不变;RMSNorm、SwiGLU、pre-norm、QK 归一化、权重绑定、dropout 的选择也不变,除非显式修改。

参数按 $d^3$ 增长意味着什么

因为沿着这条阶梯深度和宽度一起长,参数中注意力与 MLP 的部分大致按 $d^3$(等价地,$L^3$)增长。如果更大的模型还要用更多 token 来训练,「那是一大堆算力」。

还有两条使用上的提醒:

编者拓展|$d^3$ 从哪来:注意力四个投影各是 $d\times d$,即每块约 $4d^2$;MLP 是三个 $d\times d_{ff}$ 且 $d_{ff} = 3.5d$,即每块约 $10.5d^2$。每块合计约 $14.5d^2$,乘以 $L$ 块得 $\sim 14.5\,Ld^2$。沿配方 $d = 64L$,于是 $Ld^2 = 64^2 L^3$,参数总量随 $L^3$(等价地 $d^3$)增长。这个推导是编者补的,讲义只给了结论。注意它不含嵌入与 unembedding 的 $2Vd$ 项——当 $d$ 很大时这部分占比会下降。

附:参考与延伸阅读

编者补充

已知疑点

  1. 「4096 tokens」的表述。幻灯片第 9 页写 "tokenize them with 4096 tokens",字面像是每条序列 4096 个 token,但同页又写 "600,000 sequences of 1024 tokens",handout 则明确 $V = 4096$ 是词表大小。判断幻灯片这里指的是词表大小,正文按此翻译。依据:llms_101_handout.pdf §2.1「uses a SentencePiece tokenizer with V = 4096」。

  2. 训练损失被谁除。幻灯片第 15 页代码写 loss = loss / config.num_micro_batches,而 num_micro_batches 默认是 1,所以默认运行时这一行等价于不变。正文照原样保留了这行代码,并在「四、优化基础」的编者拓展里解释了它存在的原因(backward() 是累加而非覆盖)。讲义未直接说明这一点。

  3. d_ff 与「3.5d」的一致性。$3.5 \times 512 = 1792$ ✓,$3.5 \times 256 = 896$ ✓,$3.5 \times 768 = 2688$ ✓,与 handout 表格一致。但 handout 表格里的规则写作 $3.5d = 224L$,只有在 $d = 64L$ 时才成立;若自定义配置打破 $d = 64L$,这条规则不再成立——讲义没有强调这个前提。

  4. 注意力 dropout 后不重新归一化。handout §4.12 明确写 "The surviving weights are scaled, not renormalized to sum to one."。这在数学上会让注意力的输出整体缩小,与常见框架(PyTorch nn.MultiheadAttention 的 dropout 作用在 softmax 之后、不 renorm)一致,但与另一些实现不同。已在正文用编者说明标出。

  5. d 与 H·dh 的等式约束。handout 在维度表里写 $d = H d_h$,同时默认配置 $d = 64L$、$H = L$、$d_h = 64$,两者自洽。但自定义 model_config 时讲义只说「num_attention_heads 和 head_dim 设置 $H$ 与 $d_h$,满足 $d = H d_h$」,未说明冲突时以谁为准——推测是配置校验会拒绝不一致的组合,但材料未写明。

  6. 「as you saw in the first lecture」。幻灯片第 26 页提到残差连接细节「可以重要得有点离奇」,指向第一讲的内容。本讲材料中没有给出那个具体案例或数值,因此正文只保留提醒,未虚构失败现象。

  7. metric_loggers 的「全局激活统计」口径。handout §3.11 写 "The global activation statistic pools recorded parameterized leaf-module outputs, not a single hidden state."。「pooled」的具体方式(拼接后统一算 RMS?还是对各模块 RMS 再平均?)讲义未展开,正文按原文表述保留,未做进一步解释。

  8. 术语翻译。「residual stream」译为残差流,「teacher forcing」译为教师强制(正文保留英文原词),「nats per token」译为 nats/token 未意译。这些译法在中文社区尚不统一。

  9. 本讲未涉及的部分。材料中没有任何实验数据、损失曲线或对比结果——这一讲是概念与配置的导览,讲者明确说 phenomenology 与 motivation 留给其它讲次。因此本讲正文没有「失败案例」段落:讲义本身没有给出失败实验,我也没有为了凑格式而编造。若与 sp26 老书中关于梯度下降/反向传播的推导存在讲法差异,属正常,本讲按 CS312 的记号体系($s$ 索引更新步、$M$ 个 micro-batch)独立表述。