怎么读 英文术语第一次出现都写成 English(中文)。没标的中文词默认按正文定义。这不是 Stanford 官方译本,不含作业答案。
一条链:文本 → token(标记)→ embedding(嵌入)→ Transformer 层(注意力混合 + MLP 变换)→ 预测下一个 token → 用数据/算力把损失压下去 → 再用 SFT/RL 把「网页上常出现的」改成「你要的」。下面按这个链展开,不跳定义。
语言模型不读「句子」,它只认一串整数,再把每个整数查成向量,最后在这些向量上做一层又一层的混合与非线性变换。整条链是:
原始文本 → tokenizer(分词器)切成 token(标记/词元)→ embedding(嵌入向量)→ 若干层 Transformer → logits(未归一化分数)→ softmax(归一化指数函数)→ 下一个 token 的概率。
本节只把这条链的前半截和「一层里到底在算什么」讲清楚。每一个名词都会回答三件事:它是什么、为什么必须存在、它接在整条链的哪一步。评测怎么做、核函数怎么写,都不在这里。
token 是语言模型的原子符号:一段被切出来的文本片段,训练和推理时都用同一个整数编号来指代它。一段自然语言先被切成 token 序列,再被映射成 ID(整数编号)序列。模型从头到尾看见的都是这些编号,从不直接看见 Unicode 字符。
tokenizer 是执行「切分 + 编号」的那套规则和数据文件。它通常包含三样东西:vocabulary(词表,即「片段 → ID」的对照表)、BPE(字节对编码)的 merge(合并)列表(哪两个相邻符号允许合成一个新符号、按什么顺序合成)、以及下面要讲的预分词规则和特殊符号。它存在,是因为神经网络的矩阵乘法只能吃固定维度的向量;要先把变长、开放的文本世界,收成一个有限词表上的离散序列。它在整条链的最前端:换了它,后面所有权重的「每一行代表什么」全部错位。
切分粒度决定一件事:一个意思平均要拆成几个 token。太碎,序列变长,注意力的计算量和推理时要保留的中间状态都按序列长度涨;太粗,词表膨胀,罕见词学不饱,还会碰到「没见过的词怎么办」。
byte(字节)级:把文本先编成 UTF-8(一种把 Unicode 码点写成字节的变长编码)字节,每个字节是 0–255 中的一个符号,作为最细的起点。它存在是为了「任何字符串都能表示」——表情、冷僻字、二进制垃圾都不会变成未知。代价是英文一个字母往往一个字节,中文一个字三个字节,序列偏长。纯字节模型(词表只有 256)能跑,但浪费上下文窗口。
character(字符)级:按 Unicode 码点切,英文是字母,中文常常是「一个字一个 token」。对中文看起来自然,对英文很亏:algorithm 要占 9 个位置,而 play 和 playing 在词表里也几乎不共享。词表大小随文字系统膨胀,且仍会遇到未登录字符。
subword(子词)级:既不是整词,也不是单字节,而是「高频的多字节块合成一个 token,低频的继续拆」。playing 可能是 play + ing;中文高频词可能整词一个 ID,生僻词退回字或字节。它存在,是为了在「序列长度」和「词表大小」之间取中间解。当代主流(GPT-2 路线之后)几乎都是 byte-level BPE:先落到字节,再在字节上做子词合并。这样既没有 unknown token(未知标记)的死角,又能把常见英文词压成 1 个 token。
三种粒度不是三种模型,是同一条链上「词表从多细开始长」的选择。byte 保证覆盖;character 按人的书写单位切;subword 用统计合并把常用块变成单列 embedding。BPE 是实现 subword 最常见的一种算法,不是「另一种粒度」。
BPE 要解决的问题:在覆盖全部字节的前提下,把语料里反复一起出现的片段收成更短的序列。它不理解语义,只看相邻 pair 的频次。
训练时的循环是确定的:
推理时不再重新统计。对一段新文本:同样预分词,块内从字节出发,按 merge 表的学习顺序(或等价的贪心最长匹配,取决于实现)把能合并的相邻 pair 合掉。最终每个块变成一串词表里有的 ID。训练和推理必须用同一份词表和同一份 merge 顺序,否则同一个词会变成不同 ID,权重对不上。
一个极小的直觉例子。假设某个块反复出现 e l 这个相邻 pair,它会很早被 merge 成 el;随后 el l 可能再被 merge 成 ell。高频英文词根会被收成短序列, hapax(只出现一次的怪串)会停在更碎的字节。BPE 不会问「这是不是一个英语单词」,它只问「这两个邻居是不是总黏在一起」。
pretokenization 是 BPE 之前的那刀:用空白、标点、数字规则等,把原始字符串切成互不跨越的块。它存在,是因为如果从整句字节上直接做 merge,空格两边的词可能被合成一个跨词 token(例如把 hello world 黏成某种奇怪的整体),词表会被「词与词的偶然搭配」污染,泛化变差。预分词把「哪些位置永远不允许 merge」写进规则,BPE 只在块内生长。
常见后果你日常已经见过:很多英文 tokenizer 会把「前置空格 + 单词」收成一个 token(GPT-2 用 Ġ 这类标记表示空格)。所以 Hello 和 Hello(前面有空格)往往是两个不同 ID。这不是 bug,是预分词规则进了词表。中文没有空格当边界,预分词规则更依赖标点和脚本类型;不同家族的 tokenizer 在中文上的 fertility(生育率:一段文本平均切成几个 token)可以差很多,直接改变「同样 4K 上下文能装多少中文」。
special tokens 是人预先写进词表、并预留 ID 的符号,不参与 BPE 的频次竞争。它们存在,是因为训练和对话需要一些「文本里本来没有的控制开关」。常见几类:
关键约束:这些 ID 必须在训练 BPE 之前就占住,绝不能让 merge 循环自己「长出」一个看起来像 <|endoftext|> 的普通子词。否则控制符和普通文本会撞号,整条链的停止条件会坏。
vocabulary 是一张有限表:每一行对应一个 token ID,后面 embedding 矩阵的每一行、输出层的每一列,都按这张表对齐。目标大小是超参数,不是越大越聪明。
实践落在一个窄带里:早期 GPT-2 约 5 万;不少 Llama 2 系约 3.2 万;Llama 3 一类把多语言和代码考虑进去后冲到 10 万以上。没有普适最优值,要同时看语言配比、代码占比、以及你愿意把多少参数预算交给「词表那两块矩阵」。
换 tokenizer 等于换模型。embedding 的第 17 行是什么意思,完全由「ID 17 在词表里是哪一段文本」定义。merge 表变了、预分词规则变了、特殊符号错位了,同一段中文会变成另一串 ID。旧权重不是「还能凑合用」,而是在用另一套字典读同一本书。训练和推理必须锁定同一份 tokenizer 文件;微调、合并模型、做投机解码时,词表不一致是一类很常见的静默事故。
上图是入口合同:文本永远先变成 ID,再变成向量。下一节的训练目标,就是在这些 ID 上「猜下一个」;再下一节的一层 Transformer,就是在这些向量上做混合。
next-token prediction(下一标记预测)是自回归语言模型的学习合同:给定前面已经出现的 token,给词表里每一个候选打一个概率,真值是「实际上紧跟着的那一个」。它存在,是因为自然语言可以看成一条从左到右的链,联合概率可以按链式法则拆成一串条件概率。模型不必另学一个「理解模块」再学一个「生成模块」——把每个位置的下一枚预测做准,生成时按这个分布取样即可。它在整条链的最末端监督信号上:前面所有层都是为了让这个条件分布变准。
P(x1, x2, ..., xT) = P(x1) * P(x2|x1) * P(x3|x1,x2) * ... * P(xT|x1..xT-1)
训练语料里的每一段文本,既是输入也是答案:位置 t 的上下文是 x1..xt,标签是 x_{t+1}。
shift by one 指:用来当标签的序列,是输入序列向左丢掉第一个、向右看下一个。设输入 ID 为 [x1, x2, x3, x4],则位置 1、2、3 的模型输出分别去对 x2, x3, x4。最后一个输入没有「再下一个」,通常不计入损失;第一个标签没有「更前面的上下文」,对应序列开头(若词表里有 beginning-of-sequence 这类起始符,就从它算起)。
它存在,是因为若用位置 t 的输出去预测 x_t 本身,模型最省事的办法是抄当前 token,损失可以很低,但完全没有「往前看、往后猜」的能力。错开一位,才逼它用已经看见的前缀去猜未见过的下一枚。
logits 对应输入 [x1, x2, ..., xT]
标签 对应 [x2, x3, ..., xT] # 实现里常写成 pred[:-1] vs target[1:]
还要配 causal mask(因果掩码):注意力里,位置 t 不能读到 t 之后的 token。否则「预测下一个」会变成「偷看答案再装懂」。训练时用掩码;生成时则是真的还没有后面那些 token。两者必须一致,这才叫自回归。
模型最后一层线性变换吐出的是一组实数,长度等于词表大小,叫做 logits:还没有变成概率的分数,可正可负,也不要求和为 1。它存在,是因为线性层的自然输出就是无约束实数;而我们需要的是「词表上的一个分布」。
softmax 把一组 logits z 变成一组非负且求和为 1 的数:
softmax(z)_i = exp(z_i) / sum_j exp(z_j)
它存在,是因为指数保证正数,除以总和保证是概率单纯形上的一点,而且对 z 可导,才能反传。谁的 logit 更大,谁的概率更大;差距被指数放大,所以训练后期模型会越来越「尖」。数值上要先减掉 max(z) 再 exp,否则容易溢出——这是实现细节,不改定义。
位置 t 上,softmax 之后得到 P(· | x1..xt)。真正被拿去算损失的,通常只是这个分布在「正确答案那一枚 token」上的那个值,而不是把词表每一维都当多标签。
cross-entropy 衡量「模型给出的分布」和「数据给出的分布」有多不齐。下一枚 token 在数据里是 one-hot(独热:正确答案那一维为 1,其余为 0)时,交叉熵塌成一句话:
L_t = - log P(x_{t+1} | x1..xt)
平均损失 = (1/T) * sum_t L_t # 只对有效位置平均,padding 要排除
它存在,是因为我们要一个可导的标量,既惩罚「把正确答案打得很低」,又在概率语言里有意义:最小化交叉熵,等价于让模型分布去贴数据的下一枚条件分布(在标签是 one-hot 时,也等价于最小化 KL 散度(Kullback–Leibler divergence,两个分布之间的差异)里模型能控制的那一项)。对错不是 0/1 准确率;就算概率最大的那一维已经是正确答案,把该概率从 0.51 推到 0.9,损失仍会下降。这就是「越来越自信地猜对」。
实现上,成熟框架的 cross_entropy(logits, target_id) 内部就是 softmax + 取目标维 + 负对数,并且用 log-sum-exp 做稳定计算。概念上请把它记成「看对下一枚的负对数概率」,不要记成「框架里随便一个分类函数」。
perplexity 是把平均负对数似然从「对数概率」还原成「有效分支数」的一个指数:
PPL = exp(平均 L_t) = exp( - (1/T) * sum_t log P(x_{t+1}|x1..xt) )
它是什么:同一套 next-token 损失的另一种读法,常记作 PPL。PPL 越接近 1,模型对语料越不「意外」;PPL = 10 的粗直觉是「每一步像在大约 10 个同等嫌疑的 token 里做选择」(这不是严格定理,是数量级直觉)。它为什么存在:人看「每个 token 多少奈特(自然对数下的信息量)」不直观,指数以后可以和词表大小、随机乱猜的基线对比。它接在训练目标之后,是损失的单调变换——在同一 tokenizer、同一套文本上,PPL 低就是损失低。
同一段话,换了 tokenizer,token 数变了,PPL 不能横比。困惑度是「每个 token 有多意外」,不是「每个汉字有多意外」。怎么用它评模型、它和下游任务准不准,留给评测分册。这里只需记住:它不是另一种训练目标,只是交叉熵的 exp。
前面已经有了向量序列 X,形状可以想成「长度 L × 宽度 d」。一层 Transformer 做两件事:先用 attention(注意力)让每个位置按需读取其它位置,再用 MLP(多层感知机,这里指位置内的前馈网络)做非线性变换。两段各包一条 residual connection(残差连接)和一次归一化。N 层就是把这一层重复 N 次。生成时还是为了把最后的隐向量变成 logits;训练时梯度从交叉熵一路传回来。
token embedding 是尺寸为「词表大小 × d」的查找表。ID 为 k 的 token,取第 k 行,得到一个 d 维向量。它是什么:离散符号的可学习坐标。它为什么存在:注意力和 MLP 都是对实数向量做线性代数,ID 本身不能乘。它在链里紧接 tokenizer:整数 → 向量。这一步没有混合不同位置,每个 token 独立查表。输出层那块「隐向量 → 词表 logits」的矩阵,有时和这张表共享(weight tying,权重绑定),有时分开——共享是为了少一套词表那么大的参数,并让「读」和「写」用同一套几何。
没有位置信息时,注意力对序列顺序是置换等变的:把 token 打乱,只要 Q、K、V 一起打乱,混合结果对得上,模型分不清「谁在谁前面」。绝对位置 embedding 给每个下标加一个可学习向量,能打破对称,但外推到训练时没见过的长度时很脆。
RoPE(rotary position embedding,旋转位置编码)不往向量上「加」一个位置向量,而是把 query(查询)和 key(键)按二维一组做旋转,旋转角与该 token 的位置下标成正比。位置 m 的 q 转 mθ,位置 n 的 k 转 nθ 之后,点积 q·k 只依赖于相对距离 (m−n) 以及内容本身。这就是「相对」:模型直接看见「隔了多远」,而不是「我在绝对第 137 位」。
它存在,是因为自回归要顺序,长上下文还希望长度外推别立刻崩。它接在 Q、K 从隐状态线性投影出来之后、点积之前;V 通常不转。细节公式属于实现,机制上记住三句即可:只转 Q/K;点积自然带相对位置;绝对下标没有单独加进残差流。
每个位置的隐向量,经三组不同的线性投影,得到 Q(query,查询)、K(key,键)、V(value,值)。
三者必须分开,是因为「用来匹配的特征」和「匹配成功后要传递的特征」不是一回事。若用同一向量既当目录又当正文,模型很难同时把「我像不像你要找的」和「我该给你什么信息」学好。它们接在归一化之后的隐状态上,是注意力的输入;RoPE 加在 Q/K 上。
注意力是一套可导的「软查找」:每个位置用自己的 Q,去和所有允许看见的位置的 K 比相似度,得到一组权重,再用这组权重去平均那些位置的 V。
scores = (Q K^T) / sqrt(d_k) # d_k 是每个头里 Q/K 的维度
weights = softmax(scores + causal_mask)
output = weights V
点积衡量对齐程度;除以 sqrt(d_k) 是因为 d_k 维独立坐标加总后,点积的方差大约随 d_k 线性涨。若不缩放,softmax 会进饱和区(一边接近 1、一边接近 0),梯度变小,深层不好训。causal_mask 把「未来」位置的 score 设成负无穷,softmax 后那些权重为 0。
它存在,是因为我们需要一种长度可变、且能按内容决定「看谁」的混合,而不是固定卷积核。它在一层里是前半段:混合上下文。没有它,每个 token 的 MLP 只能看见自己,做不成语言模型。
multi-head attention(多头注意力,简称 MHA)把 Q/K/V 的通道切成 h 组,每组独立做一次缩放点积,再把 h 个头的输出拼回去,乘一个输出投影。每个头的宽度 d_k = d / h(常见设法;d 是模型隐向量宽度,h 是头数)。
它存在,是因为单次注意力在每个位置只能给出「一种」加权模式。多头让不同头可以同时学不同的读写:有的盯上一个括号,有的抄前面的实体,有的做短距离句法。拼回去之后,后面的残差流拿到的是几种读法的总和。它不是「好几层注意力叠在一起」,是一层里的并行分路,参数仍在这一层。
推理时,已经生成的每一枚 token 都要留下自己的 K 和 V,供后面的 Q 来点积,这就是 KV cache(键值缓存)。缓存大小正比于:层数 × 序列长度 × KV 头数 × 每头维度 × 2(K 和 V)。Q 不必缓存——下一位置会重新算 Q。
GQA(grouped-query attention,分组查询注意力):Q 头仍有 h 个,K/V 头只有 g 个(g 能整除 h)。每 g_group = h/g 个 Q 头共用同一份 K/V。它是 MHA 和下面 MQA 之间的折中:质量接近多头,缓存按 g/h 缩小。
MQA(multi-query attention,多查询注意力):所有 Q 头共享唯一一组 K/V(g = 1)。缓存最小,质量通常比 GQA 再掉一点。GQA 是「分组的 MQA」;MHA 是「每组只有一个 Q 头的 GQA」。
三者都是同一套缩放点积,只改「K/V 有几份拷贝」。它们存在的理由几乎全在推理显存和带宽,不在训练公式更漂亮。训练仍可以算完整注意力;部署时 KV 头数才变成瓶颈。
layer normalization(层归一化)在一个 token 的特征维上做标准化,减轻内部协变量偏移、让不同尺度的向量能进同一套权重。经典 LayerNorm 会减均值、除标准差,再乘 γ、加 β。
RMSNorm(root mean square normalization,均方根归一化)更瘦:不减均值,只除以均方根(RMS),再乘 γ(常常没有 β)。
RMS(x) = sqrt( mean(x^2) + eps )
RMSNorm(x) = (x / RMS(x)) * γ
它存在,是因为实验里去掉均值中心化,质量差不多,算得更少。它接在每个子层(注意力或 MLP)的入口。
Pre-Norm(前置归一化)指残差的写法是:
x = x + Attention(RMSNorm(x))
x = x + MLP(RMSNorm(x))
对照 2017 年原文的 Post-Norm(后置归一化):先子层再 Norm,残差路径也被归一化挡住。Pre-Norm 让残差这条「高速公路」保持近似恒等,深层(几十层)才训得动。当代 GPT/Llama 系默认 Pre-Norm + RMSNorm,不是因为论文更老,是因为这样堆层更稳。
一层里注意力之后的 MLP,对每个位置独立做同样的非线性(不看其它位置)。老式做法是:升维 → ReLU(rectified linear unit,修正线性单元)或 GELU(Gaussian error linear unit,高斯误差线性单元)这类逐元激活 → 降维。SwiGLU(Swish-gated linear unit,Swish 门控线性单元)把升维拆成两路,一路经 Swish(也叫 SiLU:x · sigmoid(x))当门,一路当内容,逐元相乘后再降维:
SwiGLU(x) = ( Swish(x W_gate) ⊙ (x W_up) ) W_down
Swish(z) = z * sigmoid(z)
门的意义:网络可以按输入把某些通道关掉,而不是对所有通道套同一个激活形状。它存在,是因为同样参数预算下,门控前馈往往比普通 GELU MLP 更强,Llama / PaLM 一类把它收成标准件。宽度上常把中间维调到约 (8/3)d 再取整到硬件友好倍数,使总参数和「两层宽 4d 的旧 MLP」大致相当。它接在第二次 RMSNorm 之后,仍包在残差里。
residual connection 把子层输出加回输入:y = x + F(x)。F 是注意力或 MLP。没有残差时,L 层就是 L 个映射复合,反向传播是一长串雅可比相乘,容易指数变小或变大,梯度到不了底层,也难学「什么都不改、只把信息原样传下去」。加上残差,恒等映射永远在;F 只需学「相对 x 要添加的增量」。梯度可以沿 x 那条路直达浅层,再叠加各层 F 的贡献。
这就是能堆 32 层、80 层的机制条件。归一化让 F 的尺度可控,残差让梯度有路,两者一起,才谈得上「很深的 Transformer」。它在一层里出现两次:注意力后一次,MLP 后一次。整网的信息主路是这条加号链,不是每一次注意力矩阵。
把一层读成两条总线:竖着走的是残差主路,信息可以原样往下传;横着进子层的是「先 RMSNorm,再改一改」。注意力改的是位置之间的混合,MLP 改的是每个位置内部的特征。训练目标不在层内,它在堆完所有层、投影成 logits、softmax 之后,用交叉熵看下一枚 token。
| 英文 | 中文 | 一句话定义 |
|---|---|---|
| token | 标记 / 词元 | 文本被切出的原子片段,模型里用一个整数 ID 指代。 |
| tokenizer | 分词器 | 把文本切成 token 并编号的规则与词表、merge 表。 |
| vocabulary | 词表 | token 片段到 ID 的有限对照表,embedding 按行对齐它。 |
| BPE | 字节对编码 | 反复合并语料中最常见相邻 pair、直到词表够大的算法。 |
| pretokenization | 预分词 | BPE 之前按空白/标点划块,禁止跨块 merge。 |
| special tokens | 特殊标记 | 人预留的控制符(起止、填充、角色),不由 BPE 长出。 |
| embedding | 嵌入向量 | 每个 token ID 查表得到的 d 维可学习向量。 |
| next-token prediction | 下一标记预测 | 用前缀预测词表上下一枚 token 的条件分布。 |
| shift by one | 右移一位 | 用位置 t 的输出去对位置 t+1 的标签,防止抄当前词。 |
| logits | 未归一化分数 | softmax 之前、词表那么长的一组实数。 |
| softmax | 归一化指数函数 | 把 logits 变成非负且求和为 1 的概率。 |
| cross-entropy | 交叉熵 | 损失:正确答案那一枚的负对数概率。 |
| perplexity | 困惑度 | 平均交叉熵的指数,同一 tokenizer 下与损失单调等价。 |
| causal mask | 因果掩码 | 禁止位置 t 读到 t 之后的 token,保证自回归。 |
| attention | 注意力 | 用 Q 对 K 打分、softmax 后加权混合 V 的软查找。 |
| RoPE | 旋转位置编码 | 旋转 Q/K 使点积依赖相对距离的位置方案。 |
| MHA | 多头注意力 | 若干组 Q/K/V 并行做注意力再拼接。 |
| GQA | 分组查询注意力 | 多组 Q 头共享较少的 KV 头,用来缩小推理缓存。 |
| MQA | 多查询注意力 | 全部 Q 头共享一组 KV,GQA 在 g=1 时的极端。 |
| residual connection | 残差连接 | y=x+F(x),给深层一条恒等梯度通路。 |
| layer normalization | 层归一化 | 在特征维上标准化,稳定各层输入尺度。 |
| RMSNorm | 均方根归一化 | 不减均值、只按 RMS 缩放的 LayerNorm 变体。 |
| Pre-Norm | 前置归一化 | 先 Norm 再进子层,残差主路保持近恒等。 |
| MLP | 多层感知机 | 一层里按位置独立做的前馈非线性(升维–激活–降维)。 |
| SwiGLU | Swish 门控线性单元 | 两路升维、Swish 门控相乘再降维的 MLP。 |
| KV cache | 键值缓存 | 推理时存放已生成位置的 K/V,避免重复计算。 |
系统层只做一件事:把「模型在算数学」翻译成三本必须分开的账——FLOPs(浮点运算次数)、搬运字节、显存占用。慢,多半是第二本;爆,多半是第三本;贵,才是第一本。下面每个概念都按同一格式写:它是什么、为什么必须存在、接到训练或推理的哪一步。
FLOPs(浮点运算次数)的定义:一次乘或加都算一次浮点运算;矩阵乘 C = A B 里每个输出是一次乘加,习惯按 2 次 FLOPs 计。它存在,是因为芯片厂商报的是「每秒能做多少次浮点」,你必须用同一单位才能判断一次训练要跑多久、值不值。它接到的是前向 + 反向里每一次 matmul,不是优化器那几行更新。
parameter(参数)的定义:模型里要被学习、会写进 checkpoint 的数字,主要是各层权重矩阵(以及可选的 bias、norm 的 scale)。它存在,是因为网络必须用固定一组数字把 token 变成 logits;没有参数就没有函数。它接到前向读权重、反向写梯度、优化器改权重三步。
gradient(梯度)的定义:损失函数对每个参数的偏导,形状与参数相同。它存在,是因为训练的唯一更新信号就是「这个参数往哪边挪、挪多少」。它接到反向传播结束 → 优化器 step 之前;step 一做完,梯度占用的显存理论上可以释放,但实现里常常还留着等 all-reduce。
activation(激活值)的定义:前向每一层算完、还要留给反向用的中间张量(残差流、注意力里的 Q/K/V、MLP 的门控中间量等)。它存在,是因为反向传播的链式法则需要「当时前向看到的那个值」;丢掉就无法精确反传。它接到前向保存、反向立刻消费;序列一长、batch 一加大,最先爆的往往是它,不是参数本身。
Dense Transformer 训练的经验式来自「几乎所有计算都是线性层的 matmul」:每个 token 都要和几乎全部参数做一次乘加。于是有:
训练 FLOPs ≈ 6 N D
N = 参数个数
D = 训练过的 token 总数
拆开:
前向 ≈ 2 N D # 每个参数、每个 token:一次乘加
反向 ≈ 4 N D # 对激活反传 ≈ 2ND,对权重反传 ≈ 2ND
「前向约 2ND、反向约两倍」不是口诀游戏:前向每个参数被每个 token 用一次(乘加计 2);反向既要算出 dX 给上一层,又要算出 dW 给优化器,两份都和前向同量级,所以反向约 4ND,合计 6ND。注意力的 QK^T 是额外项,短序列时可忽略,长上下文会冒头——那是后面 FlashAttention(融合注意力)要管的账,不是 6ND 公式已经算进去了。每个头上 QK^T 和 PV 大约是 4 N² d_h 量级(再乘头数)。相对投影和 MLP 的 2ND,当序列长到和 hidden 可比甚至更大时,平方项就从「可忽略」变成主项。6ND 是短中序列训练的规划式,不是长上下文的真理;长序列要把注意力平方项单独记账。
6ND 接到哪。规划「这张卡要训几天」时用它:用芯片峰值 FLOPs 去除,再除以真实利用率(常见 30%–50%)。它不回答显存够不够。显存是另一本账,下面这张图。
AdamW 为什么带着两份状态。定义:AdamW 是带解耦 weight decay 的自适应优化器。一阶矩 m 是梯度的指数滑动平均(方向稳不稳),二阶矩 v 是梯度平方的指数滑动平均(这个参数历史上晃得厉不厉害)。它们存在,是因为朴素 SGD 对所有参数用同一个学习率,embedding 和输出层的尺度差几个数量级会炸;自适应步长让大梯度的参数自动迈小步。接到每个 micro-batch 的 optimizer step,不参与前向数值。
m_t = β1 * m_{t-1} + (1-β1) * g_t
v_t = β2 * v_{t-1} + (1-β2) * g_t²
θ ← θ - η * m̂ / (√v̂ + ε) - η * λ * θ
最后一项是解耦 decay,不走进 m/v
显存后果:m、v 几乎总是 fp32,每参数 4+4 字节。再加 bf16 参数 2 字节、bf16 梯度 2 字节,就是约 12 字节/参数;若还留一份 fp32 master weight,到 16 字节/参数。7B 满血 AdamW:仅 16×7e9 ≈ 112 GB,一张卡装不下——这才有后面的 ZeRO / FSDP,不是「7B 的权重文件有 112 GB」。
activation checkpointing(激活检查点,也称 gradient checkpointing)的定义:前向不保存每一层激活,只在若干层边界留检查点;反向走到那段时把中间前向重算一遍,再立刻反传。它存在,是因为激活显存随 batch × seq × layers × hidden 涨,长上下文时可以比参数+优化器还大。它接到前向的「存什么」和反向的「重算什么」:用一次额外前向(大约再加约三成 FLOPs,经验上常说用算力换显存)换掉那一大块虚线框。检查点切太稀则显存仍爆,切太密则每层都重算、墙钟变慢;它和后面按 tile 重算注意力中间量是同一哲学:HBM 装不下的,能重算就重算。
三本账不要混。6ND 告诉你「算多久」;12–16 字节/参数告诉你「优化器状态有多胖」;激活告诉你「batch 和序列还能不能加大」。融合注意力、并行、检查点,都是在这三本里挪,不是第三种魔法。
GPU 不是一块匀速黑板。离计算单元越近,容量越小、带宽越高。必须先记住两层名字。
SRAM(片上静态内存)的定义:SM(流式多处理器)上的共享内存 / L1,容量大约一百多 KB 量级,延迟极低。它存在,是因为 Tensor Core 吃数据的速度远快于从大显存里捞;必须有一块「工作台」把正在算的 tile 留下。接到单个 kernel(核函数)内部的循环:你能放进 SRAM 的,才是这一拍真正在算的东西。
HBM(高带宽显存)的定义:芯片旁边那一大块显存(H100 约 80 GB,带宽约 3 TB/s 量级),用来放参数、激活、KV、优化器状态。它存在,是因为模型状态以十亿计,SRAM 只是工作台不是仓库。接到kernel 的每次全局读写:一次 HBM round-trip 的时间,往往够在 SRAM 里把同一批数字再乘好几遍。
kernel(核函数)的定义:一次 GPU 启动、由网格上成千上万线程一起执行的函数(一次 GEMM、一次 LayerNorm、一次注意力)。它存在,是因为 GPU 不会「跑 Python for 循环」;主机只负责 launch,真正的时间在 kernel 里。接到前向/反向的每一个算子。naive 注意力慢,常常是它拆成了「算 S、写 HBM、再读回来 softmax、再写、再读乘 V」好几个 kernel,中间结果在 HBM 里来回搬。
arithmetic intensity(算术强度)的定义:这次计算里,FLOPs 除以从 HBM 搬走的字节数。
arithmetic intensity = FLOPs / bytes_moved (单位:FLOPs/byte)
它存在,是因为芯片有两道墙:算力墙和带宽墙。强度低,说明你每从 HBM 捞一个字节,只做了很少的乘加——瓶颈在搬,不在算。接到给每个 kernel 定性:改算法(融合、tiling)是在抬强度;改数学公式减少 FLOPs,对低强度 kernel 几乎不涨速。
roofline(屋顶线模型)的定义:把 kernel 画在「横轴算术强度、纵轴可达 FLOPs/s」上,性能被两条直线卡住:
可达 FLOPs/s = min( 峰值算力 , 算术强度 × 峰值 HBM 带宽 )
拐点(ridge point)= 峰值算力 / 峰值带宽
例:~1000 TFLOPS / ~3 TB/s ≈ 300 FLOPs/byte
它存在,是因为「再优化 FLOPs」和「再减少访问」是两种完全不同的活,必须先知道自己在拐点哪一侧。接到profile 之后、改代码之前。
一个具体数量级。H100 一类芯片的拐点大约三百 FLOPs/byte。形状很大的 GEMM 里,从 HBM 读来的同一块权重会被一整块激活反复乘,强度可以上到几百,落在屋顶横边附近。naive 注意力一旦写出 S:每个元素随后只做很少的逐元素运算就被写走,强度常常只有个位数到几十,落在斜边。所以同一层里,MLP 的 matmul 可能已经 compute-bound,注意力却还在等 HBM——这就是为什么只盯总 FLOPs 看不到「注意力为什么慢」。FlashAttention 抬的是强度,不是把 6ND 变小。
所以「为什么慢」的第一问不是「FLOPs 是不是 6ND」,而是:这个 kernel 在屋顶的斜边上,还是横边上? 斜边 → 少搬、融合、别写出中间矩阵。横边 → 才轮到算法减少运算量、或换更强的 Tensor Core。
接到训练的哪一步。训练里大 GEMM 往往靠近 compute-bound,所以 6ND 有意义;同一套模型一进 decode,强度掉到个位数 FLOPs/byte,6ND 立刻失效。系统课把这两段拆开,就是怕你用训练的账去解释推理的慢。
FlashAttention(融合注意力)的定义:一种 IO 感知的注意力算法——在 SRAM 里按 tile 完成 softmax(QK^T)V,不把完整的注意力矩阵写回 HBM。它存在,不是因为数学上 softmax 有新公式,而是因为 naive 实现把那张 N×N 的 S 物化以后,算术强度掉到带宽墙上。接到每一层注意力的前向(以及配套的反向),替换「三个分离 kernel」。
标准公式人人会写:
S = Q K^T # 形状:N × N,N = 序列长度
P = softmax(S) # 仍是 N × N
O = P V # N × d
为什么不能物化 S = QK^T。定义:物化(materialize)就是把这张完整矩阵当作一个张量分配在 HBM 里、写出去再读回来。N = 8192 时,单头一张 fp16 的 S 已是 8192² × 2 ≈ 128 MB;多头、多一层、再乘 batch,激活账直接爆炸,而且这 128 MB 几乎不在 SRAM 里做任何再利用就被写走——典型的低强度、memory-bound。它存在于 naive 实现里,只是因为 PyTorch 按算子构图:有一个 op 就有一块中间值。接到的就是注意力前向的中间激活;长上下文训练爆显存,第一嫌疑往往是它。
tile(分块)的定义:把 Q 按行切成 SRAM 装得下的块 Q_i,把 K/V 按行切成 K_j, V_j,一次只把一对块从 HBM 搬进 SRAM 算局部 S_ij = Q_i K_j^T。它存在,是因为 SRAM 只有一百多 KB,装不下整张 S,但装得下一对小块。接到kernel 内部的两重循环(外循环 Q 的行块,内循环 K/V 的列块)。
online softmax(在线 softmax)的定义:softmax 需要整行的 max 和 exp 求和才能归一化;在线算法边看新的一块 S_ij,边维护运行中的行最大值 m_i 和归一化分母 l_i,最大值一更新,就把已经累进输出里的那部分按 exp(m_old - m_new) 缩放。它存在,是因为你拒绝物化整行 S 之后,就不能先扫一遍再扫一遍;必须「看一块、改一块统计、改一块输出」。接到每个 Q tile 扫完所有 K tile 的过程,数学上等价于普通 softmax,数值上用 max 做稳定。
对每个 Q tile i:
m = -inf; l = 0; O = 0
对每个 K/V tile j:
S_ij = Q_i K_j^T
m' = max(m, rowmax(S_ij))
O = diag(exp(m - m')) O + exp(S_ij - m') V_j
l = exp(m - m') l + rowsum(exp(S_ij - m'))
m = m'
O_i = O / l
读图的顺序:HBM 里始终只有 Q、K、V 和正在写的 O;SRAM 每次只请一对 tile 上来,当场做局部乘加和在线 softmax,统计量 m_i, l_i 不出片。工程意义是两本账一起动:显存不再为 N×N 买单;带宽从「写读一张巨大 S」变成「Q/K/V 各流过有限次数」。FLOPs 几乎没减(还是那些乘加),屋顶线上的点却从斜边被推近拐点——这就是「系统优化可以不改模型数学」。反向同样按 tile 重算局部 S,而不是把 S 存下来,和激活检查点是同一哲学:能重算的,就别占 HBM。
一张卡装不下之后,不是「开个 DeepSpeed 就好了」,是你决定切哪一维。切错维,通信会把屋顶线重新按到带宽墙上。
data parallel(数据并行,DP)的定义:每张卡保存完整一份模型,各吃不同的 batch 切片,反向后对梯度做 all-reduce,再各自用同一份梯度更新。它存在,是因为最便宜的扩吞吐方式是「同样的层,多看几条样本」。接到每个 step 的梯度同步。它不减小单卡上的参数/优化器/激活(激活还随每卡 batch 变);只扩全局样本数。口诀里「DP 扩样本」:加卡 = 全球 batch 变大,不是模型变大。
FSDP / ZeRO 是 DP 的显存改进,不是另一种切法。定义:ZeRO(Zero Redundancy Optimizer)按阶段拿掉 DP 里的冗余副本——ZeRO-1 分片优化器状态 m/v,ZeRO-2 再分片梯度,ZeRO-3(PyTorch 里的 FSDP 接近这一档)再分片参数。需要算某一层时 all-gather 出完整权重,算完丢掉。它存在,是因为前面算过:AdamW 的 m/v 往往比权重还胖,DP 让每张卡重复存这 12–16 字节/参数是浪费。接到每一层前后的参数集合 / 梯度 reduce-scatter。代价:通信次数变多,要用计算与通信重叠来藏。
tensor parallel(张量并行,TP)的定义:把同一层里的一张大矩阵按行或按列切到多张卡上,每张卡只做一块 GEMM,然后用 all-reduce 或 all-gather 把结果拼回下一层需要的形状(Megatron 风格:列切再行切,一对通信)。它存在,是因为单层的激活和权重在宽模型里已经超单卡,DP/FSDP 解决的是「副本冗余」,解决不了「这一层的矩阵本身太大」。接到层内的 matmul 前后。口诀「TP 扩层宽」:隐藏维、头数可以随卡数加,但层内互联必须快(NVLink 级);跨机器做纯 TP 通常很痛。
pipeline parallel(流水线并行,PP)的定义:按层号把网络切成若干 stage,卡 0 算前几层,把激活传给卡 1 算后面几层,像工厂流水线。它存在,是因为深度(层数)增加时,TP 的层内通信已经够烦,不如让一张卡专职一段层。接到相邻 stage 之间的点对点传激活(正向)和传梯度(反向)。代价是 pipeline bubble:流水线排空和填满时有卡在闲。微批次(micro-batch)把一个全球 batch 切细,用来填泡。口诀「PP 扩深度」:层可以堆得更深,但气泡和 stage 间的激活传输是税。
context parallel(上下文并行,也称长序列上的 sequence/context parallel)的定义:把序列维切开,每张卡只持有一段 token 的激活和对应 KV,注意力用环状通信或类似 all-to-all 凑齐所需的 K/V。它存在,是因为激活和注意力都随 N 涨;DP 扩的是样本数,TP 扩的是宽,都救不了「一条 128K 的样本自己就把激活打爆」。接到长上下文训练/推理的注意力通信。口诀「CP 扩上下文」:上下文长度可以随卡数加,通信的是 K/V 片段,不是梯度均值。
| 方式 | 切的维 | 口诀在说什么 | 主要税 | 接到哪一步 |
|---|---|---|---|---|
| DP | batch | 同样的模型多看样本 | 梯度 all-reduce | step 前同步梯度 |
| FSDP / ZeRO | 参数/状态分片 | DP 去掉冗余副本 | 层前 all-gather | 每层进出 |
| TP | 层内矩阵的行/列 | 一层可以更宽 | 层内 all-reduce | 单层 matmul |
| PP | 层号 | 网络可以更深 | bubble + 传激活 | stage 边界 |
| CP | 序列长度 | 一条样本可以更长 | K/V 环通信 | 注意力 |
口诀不要只背。「DP 扩样本,TP 扩层宽,PP 扩深度,CP 扩上下文」四句对应四个独立自由度。真实大模型是 3D/4D 混合:FSDP/DP 拉吞吐,TP 在节点内切宽,PP 切深,长上下文再加 CP。名字在换,这四刀没换。
四种通信原语也要对应到步,不是框架黑话。all-reduce:每张卡都要同一份完整结果,DP 的梯度均值走这条。all-gather:把分片拼回完整张量,FSDP 在层前把参数凑齐走这条。reduce-scatter:先求和再按分片散回去,FSDP 在层后把梯度写成各卡自己那一片走这条。all-to-all:每张卡既发给大家也从大家收,MoE 的 token 路由和某些上下文并行走这条。接到的都是「切完之后、下一刀计算开始前,缺的那一块怎么回来」。口诀管切哪一维,原语管切完怎么圆。
训练优化的是「每 GPU 每秒多少 token」;产品优化的是首字延迟和每个用户占的显存。同一套权重,两套屋顶线。
prefill(预填充)的定义:把整段 prompt 一次送进模型,算出每一个 prompt token 的隐藏态,并把它们的 K、V 写入缓存,最后才得到第一个要生成的 token 的分布。它存在,是因为用户的问题必须先被「读完」,后面的生成才有上下文。接到一次请求的第一段计算(首字之前)。这一段高度并行:Q/K/V 都是 N × d,大 GEMM,算术强度高,常常靠近 compute-bound,有点像训练的前向。
decode(逐词生成)的定义:之后每一步只新进 1 个 token,算出这一个 token 的 Q,去和已经缓存的全部 K 做注意力,写出一个新的 logits,再把这个 token 自己的 K/V 追加进缓存。它存在,是因为自回归合同就是「下一个 token 依赖于已经写出的所有 token」,不能像 prefill 那样一次并行出整段回答(除非投机解码那种近似)。接到首字之后的每一个生成步。这一段几乎总是 memory-bound:每步 FLOPs 不多,但要把历史 KV 从 HBM 全部读一遍。
KV cache(键值缓存)的定义:推理时把已经算过的各层 K、V 存在 HBM 里,避免 decode 每步对 prompt 和已生成 token 重算一遍注意力前半。它存在,是用显存换 FLOPs:重算历史 K/V 在数学上可行,但会把每步变成又一次 prefill,延迟不可接受。接到prefill 结束时的写入,以及每一个 decode 步的读取 + 追加。
KV cache 字节 ≈ 2 × L × H_kv × d_h × S × B × e
2 K 和 V 各一份
L 层数(每一层都有自己的 K/V)
H_kv KV 头数(GQA/MQA 让它小于查询头数)
d_h 每个头的维度
S 当前序列长度(prompt + 已生成)
B batch(并发请求数,或每请求的 beam)
e 每个元素字节数(fp16/bf16 = 2,fp8 = 1)
公式里每一项都在收一笔不同的税:L 是深度税,切层(PP)或少层才能降;H_kv × d_h 是「每层每 token 的宽」,GQA/MQA/MLA 砍的是这里,不是砍查询头;S 随对话变长线性涨,所以长上下文推理是显存问题先于算力问题;B 是并发税,吞吐和每用户显存互打;e 是量化税,KV 量化直接乘在这本账上。缺任何一项,数量级会错。
代入一个量级。设 L=32、H_kv=8(查询头更多、KV 头已用 GQA 缩小)、d_h=128、S=8192、B=1、e=2:
2 × 32 × 8 × 128 × 8192 × 1 × 2 ≈ 1.07 GB
单请求看起来不大。但并发 B=64 立刻到约 68 GB,还没算权重本身(7B 的 bf16 权重大约 14 GB)。「推理显存爆」通常爆在 KV × 并发 × 上下文,不是爆在权重文件。这和训练爆在「Adam 的 m/v + 激活」是两本不同的账,不能互相套用。
两段为什么不能共用同一套「我在优化 FLOPs」的直觉。decode 一步的注意力:Q 只有一行,对长度为 S 的 K 做点积,FLOPs 约 2 S d,同时要从 HBM 读入约 2 S d 个 KV 元素;强度几乎是常数,S 变长则算的和搬的一起涨,屋顶线上一直贴着斜边。prefill 是 N 行 Q 对 N 行 K,FLOPs 按 N²、读入按 N,强度随 N 上升,长 prompt 更像训练前向。所以 prefill 值得优化大 GEMM、打满 Tensor Core;decode 的时间是 步数 × 读完整 KV 的时间,手段变成减小 KV(GQA、量化、MLA)、别把 KV 读散、必要时用投机解码减少大模型步数。并行也换:训练那套 PP 气泡在单 token 步里更难看;推理常用张量并行切宽,再加上按请求的数据并行。
若把 decode 的慢当成「模型 FLOPs 太大」,会做出加卡却不降延迟的决策——多卡若没切到 KV 读取路径上,每步仍要读完属于自己的那份历史。先问:这一步是在算,还是在搬 KV。
| 术语 | 定义 | 为什么存在 | 接到哪一步 |
|---|---|---|---|
| FLOPs(浮点运算次数) | 乘加次数,matmul 按 2 计 | 和芯片峰值同一单位 | 前向/反向的 matmul |
| parameter(参数) | 可学习权重 | 网络是这些数字定义的函数 | 前向读、step 写 |
| gradient(梯度) | 损失对参数的偏导 | 唯一的学习信号 | 反向结束 → step |
| activation(激活值) | 前向中间张量 | 反向链式法则要用 | 前向存、反向消费 |
| arithmetic intensity(算术强度) | FLOPs / 搬运字节 | 区分算力墙和带宽墙 | 给 kernel 定性 |
| HBM(高带宽显存) | 片外大显存 | 状态太大,SRAM 装不下 | kernel 全局读写 |
| SRAM(片上静态内存) | SM 上的小工作台 | 喂 Tensor Core | kernel 内 tile 循环 |
| kernel(核函数) | 一次 GPU launch 的函数 | 计算只发生在这里 | 每个算子 |
| roofline(屋顶线模型) | min(算力, 强度×带宽) | 决定优化搬运还是优化运算 | profile 之后 |
| FlashAttention | SRAM 内 tiling 的注意力 | 禁止物化 N×N 的 S | 每层注意力 |
| data parallel(数据并行) | 切 batch,模型复制或分片 | 扩样本吞吐 | 梯度同步 |
| tensor parallel(张量并行) | 切单层矩阵 | 扩层宽 | 层内 matmul |
| pipeline parallel(流水线并行) | 切层号 | 扩深度 | stage 边界 |
| KV cache(键值缓存) | 已算过的 K/V | decode 避免重算历史 | prefill 写、decode 读 |
| prefill(预填充) | 一次吃完 prompt | 先读懂上下文 | 首字之前 |
| decode(逐词生成) | 一步一个新 token | 自回归合同 | 首字之后每步 |
这一部分把语言模型从「网页」接到「能用的回答」。前面的 tokenizer、Transformer、FLOPs 账只回答「怎么算」;这里回答三件更贵的事:算力该砸在参数还是数据、训练语料怎么从垃圾堆里捞出来、预训练之后到底在优化什么信号。整条链可以先看成一张图,后面每一节都是图上某一段的放大。
读这一部分时抓住一句:预训练改的是无条件的「网上常见什么」;对齐改的是有用户意图时「什么算好回答」。评测如果被训练数据见过,数字会撒谎。 下面每个名词都按「定义 / 为什么存在 / 接到流水线哪一步」写,不压缩成一句话口诀。
scaling law(缩放律)的定义是:在足够宽的范围内,语言模型的交叉熵损失随参数量、数据量、训练算力近似呈幂律下降,而不是突然饱和成一条平线。它不是一条物理定律,是一组在日志坐标里看起来很直的经验拟合。它存在的理由很功利——训练太贵,你不能把「70B 还是 280B、喂 300B 还是 1.4T token」交给感觉。它接到流水线的训练预算分配:在动工以前,先决定 N(参数)和 D(token 数)怎么配,而不是先把模型图纸画胖再找数据填。
训练算力的常用近似是 C ≈ 6ND(前向约 2ND,反向大约两倍)。于是固定算力意味着 N × D ≈ 常数:参数加一倍,每个参数能看见的 token 就少一半。缩放律要回答的,正是这条双曲线上哪一点损失最低。
Kaplan 等人(2020)的拟合给出一个工业界跟了很久的处方:算力变大时,优先把模型做大,数据可以涨得慢一些。GPT-3 一类的选择(上千亿参数、相对较少的 token)就是这条处方的后代。Hoffmann 等人 2022 年的 Chinchilla(「小豹猫」,一篇用计算最优训练重新画缩放图的工作)说:在同样算力下,以前那些旗舰模型往往参数太大、数据太少。Chinchilla 自己用约 70B 参数、约 1.4T token,打过同算力量级但大得多、数据更饿的模型。这句话不是品味之争,是 isoFLOP(等算力)实验的机制结论。
isoFLOP(等算力曲线)的定义是:把训练 FLOPs 钉死在某一个预算上,只改变「参数多少 / 数据多少」的组合,把得到的损失连成一条曲线。它存在,是因为你若同时改 N、D 和训练步数,三个旋钮搅在一起,根本分不清是模型大了在涨,还是数据多了在涨。接到的步骤是缩放实验设计:先画等算力切片,再在切片上找最低点,那个最低点才叫计算最优(compute-optimal)。
「参数太大、数据太少」的机制,不要记成「大模型不好」。在 C ≈ 6ND 钉死时,N 取太大,D 必然变小。容量被买来了,但每个参数只看过很少的独特文本,梯度信号不够把容量填满——这是数据饥饿造成的欠拟合,不是「参数多了会诅咒」。反过来,N 取太小,模型表达力不够,再多 token 也抬不起损失的地板。所以等算力曲线对 N 往往是 U 形:两边都亏,中间才是 Chinchilla 点。Kaplan 路径相当于停在 U 的右侧(过大的 N、过小的 D)。
为什么 Kaplan 会画偏?常见解释不是「他们算错了 6ND」,而是实验没有真正停在等算力最优上:学习率日程往往和训练长度绑死,小模型没有被训到与其算力相称的 token 数;再把这些未训饱的点拿去拟合「该把算力分给 N 还是 D」,拟合就会偏向「再加大模型」。Chinchilla 的修正在方法上就是:老老实实做 isoFLOP,让每个点都用完那份算力,再读最低点。经验比例大约是每个参数配几十个 token(常被转述成约 20 token/参数),重点不是背 20,而是N 和 D 必须一起涨。
还要分清两条最优,否则后面会对着产品做错误裁剪。训练计算最优问的是:这份预训练预算怎样花,验证损失最低。推理最优问的是:训完以后,服务一个 token 的成本和延迟。小模型训得更饱,有时比没训饱的大模型更划算——这是同一套缩放思想接到推理那一步,不是推翻 Chinchilla。过了计算最优点继续喂数据(over-training)也常见:训练更贵,推理更便宜。缩放律存在的价值,就是让你能说出自己站在哪一种最优上。
缩放律外推有边界。数据质量一变、重复率一变、长上下文一变,幂律的常数会漂。它能告诉你「下一步 10× 算力大概不该只加参数」,不能替你保证「按公式放大一定赢」。接到评测时:损失按缩放律降了,下游基准仍可能不动——那是下一节的事。
模型的上限经常不在层数,在它吃过什么。数据这一步接到流水线最左端:没有可用文本,后面的 token 合同和 6ND 都是空转。
Common Crawl(公共网页爬取档案)的定义是:一个持续抓取公开网页的非营利爬虫档案,体量以 PB 计,是现代预训练语料最常见的原料,不是已经洗好的「训练好的互联网精选」。它存在,是因为没有任何实验室付得起自己把整个网爬一遍的成本,而公开网页又是覆盖面最广的自然语言来源。接到的步骤是原料入库:你拿到的不是句子,是压缩的网页快照。
这些快照的常见容器是 WARC(Web ARChive,网络归档格式):一条记录里塞着 URL、响应头、原始 HTML 或其它 payload。定义上它是归档格式,不是语料格式。它存在是为了可复现——过几年仍能从同一条 WARC 重放当时的页面。接到的步骤是抽文本:去导航、广告、脚本、重复模板,把 DOM 变成大致连续的自然语言。抽坏了,后面所有过滤都在给模板和按钮文字做语言建模。
抽完文本要做语言识别:给每一页(或每一段)打语种标签。它存在,因为算力预算是按 token 花的,你若想要一个以中文或英文为主的模型,却把预算泄给你读不懂、也不打算服务的语种,等于在 isoFLOP 曲线上主动往差的一侧走。接到的步骤是语种门:决定哪些文档进入主混合,哪些进低权重复桶或直接丢掉。
然后是过滤。定义:用启发式和分类器丢掉明显不能学的文档——太短、乱码、色情与暴力(按产品政策)、样板、机器翻译腔、超高或超低的模型打分。它存在,因为互联网的众数不是「有信息量的散文」,是导航条、发帖列表、SEO 堆砌。不过滤,缩放律拟合的是垃圾上的幂律。接到的步骤是质量门。过滤常用 perplexity(困惑度)当粗筛:用一个小模型看这段话有多「意外」;两端都可能是垃圾(乱码极意外,样板极无聊)。注意:这里的困惑度是数据筛子,和后面当训练体温计的困惑度是同一个公式、两种用途。
deduplication(去重)的定义是:把完全相同或几乎相同的文档(或段落)从训练集里删到只留一份或少数几份。它存在有两个互不替代的理由。第一,重复浪费 FLOPs——你在 6ND 里把 D 虚报了,实际独特信息更少,等于在 Chinchilla 权衡上偷偷减数据。第二,重复会把模型推向背诵:同一段话出现几千次,生成时就变成复印机,也更容易把带许可证或带隐私的字符串记死。接到的步骤是独特信息门,紧挨在混合之前。
精确去重用哈希(同一字节序列同一指纹)。近重复要另一套:MinHash(最小哈希)的定义是,把文档切成 shingle(短词组窗口),用一组哈希的最小值当指纹,使「集合越像、指纹越像」,再靠 LSH(局部敏感哈希)在海量文档里近似找邻居。它存在,因为网页转载、镜像、只改标题的 SEO 复制,精确哈希看不见。接到的步骤仍是去重,只是从「字节级」升到「内容近似级」。去重粒度要选:整页去重保多样性,段级去重更狠、也更容易误伤合法引用。
混合(mixture)的定义是:给网页、代码、书、论文、对话、多语种分配采样权重,而不是按原始磁盘占比抽样。它存在,因为 Common Crawl 的自然分布是「网上什么多」,不是「你希望模型擅长什么」;代码和高质量书籍在网上是少数派。接到的步骤是能力塑形:同样 6ND,权重大的域,下游就更可能涨。混合不是一次定终身,预训练中途改配比等于改数据的课程学习。
| 步骤 | 定义(在干什么) | 为什么存在 | 接到哪一步 |
|---|---|---|---|
| WARC / Common Crawl | 原始网页归档 | 公开网页是最广的原料 | 流水线最左,原料 |
| 抽文本 | HTML → 大致连续文字 | 模型不能直接吃 DOM | 语言识别之前 |
| 语言识别 | 打语种标签 | 算力按 token 花,不能泄给无关语种 | 质量过滤之前 |
| 过滤 | 丢掉低信息 / 违规 / 乱码 | 众数网页不是可学散文 | 去重之前 |
| 去重 / MinHash | 删精确与近重复 | 省 FLOPs,降低背诵 | 混合之前 |
| 混合 | 按域重新加权采样 | 自然分布 ≠ 目标能力 | tokenizer / 预训练 |
合成数据的定义是:用已有模型(或模板、求解器)生成文本,再当作训练或微调的监督,而不是人类写的原始网页。它存在,因为高质量示范——尤其是带推理链的数学、带单元测试的代码——人类产量跟不上缩放律要的 D。接到的步骤可以是预训练的补量,更常接到 fine-tuning(微调)和对齐。用处真实:可以把「答案」展开成中间步骤,让小模型模仿大模型的格式,补长尾语种。
模式崩塌(mode collapse)在这里的定义是:反复在模型自己的输出上训练,分布越来越窄,多样性下降,错误被当成风格加固。它存在,是因为合成数据的采样不是世界,是当前模型的尖峰;尖峰再当标签,尖峰更尖。接到的警示是:合成数据能抬某个可验证任务,也能让模型在开放生成里只会几种腔调。合成数据要当配料,要混新鲜人类文本,要有去重和过滤,不能当新的 Common Crawl。
损失下降只说明模型更会预测「下一块训练数据」。产品要的是另一件事。评测接到流水线的训练之后、对齐前后都要做:它告诉你缩放和数据清洗有没有变成能力,还是只变成了背诵。
perplexity(困惑度)的定义是平均负对数似然的指数,PPL = exp(mean NLL)。直观上:模型对这段文本越不意外,困惑度越低。它存在,因为预训练目标就是 next-token 交叉熵,你需要一个和损失单调相关、又比「loss=2.31」好讲的标量。接到的步骤是训练监控和前面说过的数据过滤。它不是产品指标:困惑度低可以是语言流畅,也可以是对训练域过拟合;换词表、换 tokenizer 之后数字不能横比。
下游基准的定义是:把模型接到一组与训练损失不同的任务上打分——选择题、阅读理解、数学、代码生成、指令遵循。它存在,因为用户不买「对网页的预测误差」,买的是任务成功率。接到的步骤是能力验收。基准的陷阱是:题太旧会饱和,题太像训练数据会虚高,题太窄会把研发吸进刷榜。
LLM-as-judge(用语言模型当法官)的定义是:让另一个(通常更强或同级的)模型比较两段回答、或按量规打分,代替部分人工评估。它存在,是因为开放式生成没法自动对答案,人类标注又贵又慢。接到的步骤是对齐阶段的离线评估,以及产品迭代里的回归测试。它便宜,但有自偏好(偏爱和自己风格像的文本)、位置偏差、对华丽腔调给分偏高。它可以当筛子,不能当唯一真理。
contamination(数据污染)的定义是:评测集的题目、答案或高度相似改写,已经出现在预训练或微调语料里,导致分数衡量的是记忆而不是泛化。它存在,因为基准一旦公开就会被爬进 Common Crawl,也会被好心人做成「学习资料」再混进混合。接到的步骤是评测可信度:发现污染,这个数字就不能拿来比较缩放或对齐是否成功。常见对策是 n-gram 重叠检查、把基准从训练里剔除、用更新更隐蔽的题、看「题面微扰后分数是否崩」——没有任何一招完美。
三套尺子怎么一起用。困惑度:训练是否在学、数据是否太脏。下游基准:某种能力是否出现。LLM-as-judge:开放生成是否像人要的。污染:这三套尺子有没有在测记忆。先定义「好」,再改数据或对齐;否则你只是在过拟合 leaderboard。
pretraining(预训练)的定义是:在大规模无标注(或弱标注)文本上做 next-token 预测,得到一个通用的条件语言模型。它优化的信号是「网页、书、代码里接下来哪个 token 常见」。它存在,是因为世界知识、语法、初级推理,用标注劳动堆不出来,只能从统计里长。接到流水线的训练主段。后果要说清楚:预训练改的是「网上常出现什么」。网上常出现的包括正确答案、也包括自信的胡说、广告腔、偏见和作弊攻略。所以一个预训练好的模型,默认不是助手,是互联网模拟器。
fine-tuning(微调)的定义是:在预训练权重上,用更小、更定向的数据继续梯度更新,把分布扳向某个域或某种交互格式。它存在,因为从零为每个产品再做一次 6ND 不划算,而预训练的表示已经可迁移。接到预训练之后。微调是总称:下面的监督微调、偏好优化、可验证奖励训练,都是微调的不同信号,不是并列的第四种魔法。
SFT(监督微调)的定义是:用「提示 + 示范回答」做普通的监督 next-token 损失,让模型模仿示范的格式、语气和步骤。信号来自人写的(或教师模型写的)示范。它存在,因为预训练几乎没见过「User / Assistant」这种合同,也不会自动拒答或按产品口吻组织答案。接到预训练之后的第一段对齐。上限也很硬:SFT 不会稳定地超出示范分布;示范里没有的拒答策略、没有的解题风格,模型只是在插值。把 SFT 做满,你得到的是一个会聊天格式的互联网模拟器,仍不是「对齐完成」。
RLHF(基于人类反馈的强化学习)的定义是:人比较或打分模型回答 → 训练一个 reward model(奖励模型)来预测这种偏好 → 再用 PPO 一类强化学习,让策略模型多说奖励模型打分高的话。它存在,是因为「什么算好回答」很难写成示范全集,却比较容易写成两两比较(这篇更无害、更有用、更老实)。接到 SFT 之后的偏好塑形。奖励模型的定义:一个(通常从 SFT 模型初始化的)打分器,输入提示和回答,输出标量。它存在,是为了把稀疏、昂贵的人类比较,变成训练循环里每个 token 都能用的稠密代理。风险:奖励模型会过拟合标注员口味,策略会钻空子(reward hacking),优化不稳、贵。
DPO(直接偏好优化)的定义是:仍使用成对偏好 (更好回答 y_w, 更差回答 y_l),但不单独训奖励模型、也不跑 PPO;把偏好直接写进对策略的分类式损失,相对一个参考模型提高 y_w、压低 y_l。它存在,是因为 RLHF 的工程链太长(采样、奖励模型、价值模型、PPO 超参),许多团队需要一条更短的偏好拟合路径。接到的步骤与 RLHF 相同——偏好数据上的对齐——只是实现更短。它不是「更安全的 RLHF」:偏好数据有毒,DPO 一样会学进去;没有探索过程,也更难主动发现示范之外的好策略。
RLVR(可验证奖励的强化学习)的定义是:奖励不来自人的口味,而来自可自动检验的对错——数学最终答案、代码单元测试、受限格式校验。它存在,是因为推理链是否正确,人类比较又贵又不一致,但校验器便宜且可重复;2020 年代中后期的「模型开始会做竞赛数学」多半走这条信号,而不是更多聊天标注。接到的步骤是可打分任务上的强化学习,与聊天助手的品味对齐平行,不是替代。
| 阶段 | 优化信号从哪来 | 为什么需要它 | 接到哪一步 |
|---|---|---|---|
| 预训练 | 网上常出现的下一个 token | 世界知识和语言只能从统计里长 | 流水线主训练 |
| SFT | 示范回答 | 学会助手合同与格式 | 预训练之后第一段 |
| RLHF | 人类比较 → 奖励模型 → RL | 「好」难写全,但能比较 | SFT 之后的偏好 |
| DPO | 成对偏好直接进损失 | 缩短 RLHF 工程链 | 同样是偏好对齐 |
| RLVR | 校验器可打分的对错 | 推理需要可重复的对错信号 | 可验证任务上的 RL |
不要把会刷 MATH 当成对齐完成。MATH 类分数上升,说明在可验证奖励上,策略找到了能过校验器的轨迹。它不自动包含:用户意图理解、拒答、不谄媚、不泄露、不在没把握时装有把握。RLVR 可以把模型变成更好的计算器,同时让聊天偏好或安全性变差(优化一个代理指标,其它行为被挤走)。对齐完成与否,要看你声明的那组行为;没声明、没测的,就不算对齐过。
把四段对齐和前面的数据、评测接回去:SFT 示范若来自被污染的题解,下游数学分是记忆;RLHF 奖励模型若用 LLM-as-judge 自产偏好,会把法官的自偏好写进策略;RLVR 若校验器只看最终数字,模型会学「写出像对的格式」。信号从哪来,模型就往哪钻。这是对齐部分真正要建立的心智模型,而不是背四个缩写。
这一节不当主线,只为读论文和系统图时不卡壳。稠密模型里,每个 token 都经过全部 FFN。Mixture of Experts(MoE,专家混合)的定义是:把 FFN 换成一组专家网络,每个 token 只激活其中少数几个,总参数可以很大,但每次前向的 FLOPs 接近一个小得多的稠密模型。它存在,是因为缩放律说「容量有用」,但稠密地加宽每一层,训练和推理的 FLOPs 一起涨;稀疏激活试图把「参数」和「每次用到的计算」解开。接到的步骤是架构与并行,不是数据清洗,也不是对齐信号;它改变的是 6ND 里 N 怎么数(总参数 vs 激活参数)。
Router(路由器)的定义是:一个小模块(常见是对 token 隐状态做线性打分)为每个 token 选择 Top-k 专家。它存在,因为专家不会自己知道该接哪些 token;必须有人做离散选择。接到 MoE 层内部的分发。选择一偏,所有 token 挤进同一个专家,稀疏就名存实亡,所以还有负载均衡(辅助损失、capacity factor 等)。
稀疏激活的定义是:一次前向只计算被选中的专家,未被选中的专家权重在这次计算里不参与。它存在,才有「总参数 100B+、每次只走两三个专家」这种经济账。接到训练与推理的 FLOPs 账:宣传口径若只报总参数,不报激活参数,你会把 MoE 错当成同 FLOPs 的稠密巨兽。
Expert parallel(专家并行)的通信税的定义是:专家往往切到不同卡上,token 要按路由结果 all-to-all 送到对应卡,算完再送回。它存在,是因为一张卡装不下所有专家权重。税就在这里:稠密 FFN 几乎是本地矩阵乘;MoE 多了一次不规则的令牌搬运,网络比 Tensor Core 更容易成为墙。接到系统实现,不是算法故事。读 MoE 论文时先问三句:总参数还是激活参数?Top-k 是几?专家并行的 all-to-all 有没有把等算力优势吃掉?问完这三句,就可以回到数据和对齐主线。
| 术语 | 定义 | 为什么存在 | 接到哪一步 |
|---|---|---|---|
| scaling law(缩放律) | 损失随 N、D、算力近似幂律下降的经验拟合 | 训练太贵,必须先决定预算怎么分 | 预训练的 N–D 分配 |
| Chinchilla | Hoffmann 等提出的计算最优缩放:N 与 D 一起涨 | 纠正「同样算力下模型过大、数据过少」 | isoFLOP 上的最优点 |
| isoFLOP | 固定训练 FLOPs,只扫 N 与 D 的配比 | 把三个旋钮拆开,才能谈最优 | 缩放实验 |
| pretraining(预训练) | 大规模 next-token 预测 | 世界知识只能从统计里长 | 流水线主训练 |
| fine-tuning(微调) | 在预训练权重上做定向继续训练 | 避免为每个产品从零付 6ND | 预训练之后 |
| SFT(监督微调) | 用示范回答做监督学习 | 学会助手格式与语气 | 对齐第一段 |
| RLHF(基于人类反馈的强化学习) | 人偏好 → 奖励模型 → RL | 「好」难写全,但能比较 | SFT 之后的偏好 |
| reward model(奖励模型) | 预测人类偏好的打分器 | 把昂贵比较变成可优化标量 | RLHF 中间件 |
| DPO(直接偏好优化) | 成对偏好直接进损失,不训独立 RM | 缩短 RLHF 工程链 | 偏好对齐 |
| RLVR(可验证奖励的强化学习) | 用校验器对错当奖励做 RL | 推理需要可重复的对错信号 | 可验证任务 |
| Common Crawl | 公开网页爬虫档案 | 最广的预训练原料 | 数据最左端 |
| deduplication(去重) | 删除精确或近重复文本 | 避免虚报 D 和背诵 | 混合之前 |
| MinHash | 用哈希指纹近似文档集合相似度 | 精确哈希看不见转载改写 | 近重复去重 |
| contamination(数据污染) | 评测材料泄漏进训练数据 | 否则分数衡量记忆而非泛化 | 评测可信度 |
| perplexity(困惑度) | 平均 NLL 的指数 | 和训练损失对齐的体温计 / 数据筛 | 训练监控与过滤 |