各章节里出现的关键词,都在这里有一条简短、面向直觉的解释。正文中带虚线下划线的词, 鼠标悬停会弹出气泡,点"更多"就跳到这里对应词条。
多维数组,深度学习里数据的基本容器。标量是 0 维、向量 1 维、矩阵 2 维,再高维就统称张量。
形如 (B, T, C) 的数据就是一个 3 维张量:B 个序列 × 每序列 T 个位置 × 每位置 C 维向量。
一次喂给模型并行处理的一组样本。batch_size=32 表示一次处理 32 条序列。
批越大,梯度估计越稳,但越吃显存。注意它和"向量维度"是两码事 —— 一个是"几条样本",一个是"每个 token 几个数字"。
把离散的 token(整数 id)映射成一个可训练的稠密向量。nn.Embedding 本质是一张查找表:
第 i 行就是第 i 个 token 的向量。训练让语义相近的 token 拥有相近的向量。
把文本和数字互相转换的工具:encode 把字符串变成整数 id 序列,decode 反过来。
第 1–4 章用最简单的"字符级"切分 —— 每个字符一个 id;第 6 章起用 GPT-2 的 BPE(tiktoken);第 18 章从零训了一个字节级 BPE。
函数定义时给参数设的默认值,调用时不传就用它。例如 forward(self, idx, targets=None)
里 targets 缺省为 None —— 只想前向出 logits、不算 loss 时,就不传 targets。
模型一次能看到的最大 token 数。block_size=8 意味着每个位置最多回看前 8 个字符;
更早的历史会被截断(generate 时要 idx[:, -block_size:])。这是注意力的"视野上限"。
每个 token 用多长的向量来表示,本项目 n_embd=32。它决定了 token_embedding、
位置表、Wq/Wk/Wv 的形状。⚠️ 本章里它和 batch_size、
head_size 数值都撞成 32,但含义完全不同:一个是"每个 token 几个数字(维度)",
一个是"一次几条句子",一个是"注意力头的输出宽度"。看到 32 先问"这是谁的 32"。
模型最后一层输出的、未经 softmax 的原始打分。可正可负,数值大小代表"倾向";
过 softmax 后才变成概率。形状常见为 (B, T, vocab_size) —— 每个位置对词表里每个字符都打一个分。
把一组任意实数(打分)变成一组和为 1 的正数(概率分布):每个数取 exp 再除以总和。
分数越大,概率越大。它天生保证"每行加起来 = 1"。
把一组数按比例缩放,使其满足某种总量约束。本项目里特指"每行除以该行的和", 让每行加起来 = 1,从而变成一组权重(概率分布)。softmax 内部做的也是这件事。 它管的是"尺度",和"哪些位置为 0"(因果)是两件正交的事。
分类任务的损失函数。先对 logits 做 softmax 得概率,取正确类别的概率 p,
loss = -ln(p)。模型越确信正确答案(p→1),loss→0;越瞎猜,loss 越大。
随机初始时 loss ≈ ln(vocab_size)。
loss 对每个参数的偏导数,指出"参数往哪个方向调、loss 下降最快"。
loss.backward() 负责把它算出来,优化器再据此更新参数。
梯度在传播中变得极小,参数几乎不更新、模型学不动。本项目会在两处碰到它:
① 层一深就连乘衰减:梯度从顶层一路乘回底层,层数越多越小 ——
这是它最经典的成因,靠残差连接开一条直通车来治。
② softmax 被推到极端:某一项概率接近 1 时,它对输入的导数趋近 0,梯度也就传不回去 ——
这正是注意力要对分数做缩放的原因。
PyTorch 的梯度默认累加(backward 把新梯度加到旧的上)。所以每轮训练开头必须先
optimizer.zero_grad() 清零,否则会带着上一轮的陈旧方向越走越偏。
顺序:zero_grad → backward → step。
拿到梯度后,真正决定"每个参数走多大一步"的算法。AdamW 是 Adam 的改良版(带权重衰减),
是当下训练 Transformer 的常用默认选择。lr(学习率)控制步子大小。
序列内部每个位置,通过 query/key/value 机制, 按相关性从其它位置(因果设定下只看过去)聚合信息,得到融合了上下文的新表示。 "self"指 q、k、v 都来自同一个序列。
同一个输入经三个不同的线性投影得到的三种角色。query=我在找什么; key=我宣传自己是什么(被检索);value=你看我我给你什么(被取走的信息)。 用 query 比对所有 key 算出权重,再用权重聚合 value。
两个等长向量对应元素相乘再求和,得到一个标量。结果越大,表示两向量方向越一致。
注意力用 q · k 来衡量"query 和 key 有多相关",作为关注分数。
一个方阵,对角线及其左下方保留,右上方全部为 0。torch.tril 生成。
注意力用它做因果掩码:让第 i 个位置只能看到 0..i,看不到未来。
它管的是"哪些位置必须为 0"(结构),和归一化管的"尺度"互相独立。
在注意力分数里,把"未来位置"设成 -∞,经 softmax 后它们权重变 0。
保证预测下一个字符时不能偷看答案。靠下三角实现:
masked_fill(tril==0, -inf)。
注意力把 q·k 的分数除以 √head_size。因为点积的方差随维度增大,
不缩放会让 softmax 过于尖锐、把权重几乎全压给一个位置,导致梯度消失。
除以 √d 把分数尺度拉回正常。
因为注意力本身看不出顺序(打乱输入,输出只跟着打乱,权重不变),
所以额外给"第几个位置"也学一个向量,加到 token 嵌入上,模型才知道谁先谁后。
代码:x = tok_emb + pos_emb。
把注意力拆成若干个并行的头:每个头在 n_embd 的一个子空间里独立
打分聚合(每头 head_size = n_embd / n_head 维),最后把各头输出拼接(cat)再过一个 proj 线性层融合,回到 n_embd。
要点:不是把模型加宽,而是把同一块 n_embd 切成几份 —— 切几份总料不变,显存/算力基本持平。
每个头会在训练中自发分化出不同专长(盯前一词、回看句首、配对括号…),没人指定;
这与 Q/K/V 那种"公式钦定"的分工不同。本项目第 3 关开始用。
预训练之后的一步:用「问 → 答」成对数据继续训练同一批参数(包括 Wq/Wk/Wv),
把模型调成"以助手口吻回答"的姿态。要点:"抓相关上下文"的能力是预训练就建好的,SFT 只是在上面拧风格 ——
它不会"激活" QKV 里某个为问答预留的用途(QKV 2017 年为翻译而生,SFT 2022 年才出现,不存在"提前挖坑")。
它属于后训练的第一段(注意:后训练 = SFT + 偏好对齐,
SFT 本身还不是"对齐")。本项目在第 8 章 / 06_sft.py 手搓了一遍。
最基础的神经网络:几层全连接(Linear)中间夹非线性(如 ReLU)。
"多层"指它不止一层线性变换。Transformer 里每个 Block 的 FeedForward 就是一个很小的 MLP
(Linear → ReLU → Linear),负责对每个位置的向量单独"加工"。
Transformer 层里的小 MLP:Linear(n_embd, 4·n_embd) → ReLU → Linear(4·n_embd, n_embd),中间放大 4 倍再压回。
它是逐位置(position-wise)的 —— 对每个 token 的向量单独加工,位置之间不交换信息。
分工:注意力让 token 互相"沟通",FFN 让每个 token 自己"思考"。
最常用的非线性激活函数:ReLU(x) = max(0, x),把负数清零、正数保留。
作用是给网络引入非线性 —— 没有它,多层 Linear 叠起来在数学上还是一个线性变换,层数白加。
把子层的输出加回它的输入:x = x + 子层(x)。那个 + 是一条梯度高速公路,
反向传播时梯度能绕过子层直达底层,避免 梯度消失,深网络才堆得动。
子层只需学"在原信号上加什么修正"。出自 2015 年 ResNet,Transformer 全靠它。
对每个位置那一根向量(沿特征维)做 归一化:减均值、除标准差,把数值拉回稳定范围,深网络训练才不发散。
它不跨位置、不跨 batch,与序列长度无关。pre-norm(先 norm 再进子层,x + 子层(ln(x)))比原始 Transformer 的 post-norm 更好训。
Transformer 的一层 = 注意力子层 + FFN 子层,各自带 残差 和 LayerNorm:
x = x + sa(ln1(x)) 然后 x = x + ffwd(ln2(x))(沟通 + 思考)。
完整 GPT 就是把 n_layer 个 Block 摞起来。
训练时随机丢弃一部分连接(如 10%),逼网络别过度依赖某几条路径,防过拟合。 只在训练时生效,推理时关闭。模型变大后(第 3 关)容易死记硬背,所以加了它。
把输入从头到尾过一遍网络、算出输出的过程 —— 这里就是 idx → 嵌入 → Block×N → ln_f → lm_head → logits。
训练和推理都要做前向;训练还会接着反向传播更新权重,推理则到 logits 就停、拿去采样。
按模型输出的概率分布随机抽一个 token 当下一个字符(代码 torch.multinomial(probs, 1)),
不是永远取最高分(argmax)。取最高分会陷入重复死循环;随机采样保多样,所以同一个开头每次生成都可能不同。
把自己刚生成的字符接回输入,再预测下一个,如此循环。GPT 生成文本就是这样一个字一个字滚出来的: 每多一个字,就把整条序列重新做一次前向传播。
每步只保留概率最高的 k 个 token,其余打分置 −∞(概率 0)再归一化采样,把长尾低概率 token 一刀切掉,避免偶尔抽到离谱的词。默认 k=50。
把过去每个 token 算出的 K/V 向量缓存下来,生成新 token 时只算新一列、复用历史,避免每步把整段注意力重算一遍。计算量从平方级降到线性,本项目实测推理提速约 2.5–3.1×。
把文本切成子词单元的分词法,介于字符级和整词级之间。GPT-2 的 BPE 词表有 50257 个 token —— 本项目 124M 复现就用它(代码里向上取整到 50304,凑 128 的倍数更省 GPU)。
训练方法:从单个字符(或字节)出发,反复统计相邻两个 token 的出现次数,把最多的那一对合并成新 token,直到词表够大。第 18 章从零实现了一遍,见 字节级 BPE。
经过教育质量筛选的大规模英文网页语料,常用于预训练。本项目用其子集(300M / 10B token)做 GPT-2 124M 复现 —— 全英文,所以模型不会中文。
把超大数据集切成的一块块文件(这里是 .npy),训练时一片片顺序读入,不必把整个 10B token 数据集一次塞进内存。
优化器的事实标准:Adam 的改良版,把权重衰减从梯度里拆出来单独做。本项目用 betas=(0.9, 0.95),并对参数分组(权重给衰减、bias/LayerNorm 不给)。
16 位浮点。前向用它算更快、省一半显存,数值范围和 fp32 一样大、精度几乎无损 —— 大模型训练的常规操作。需要 N 卡支持。
把注意力算得又快又省显存的实现(F.scaled_dot_product_attention):不显式构造 T×T 注意力矩阵,边算边融合。和普通注意力数学等价,只是更高效。
做法:把 Q、K、V 切成块,一块 Q 在片上 SRAM 里依次和每块 K/V 算分数、用 online softmax 累加输出,T×T 的分数矩阵从不写回显存。 第 19 章用 Triton 手写了前向 kernel,换进第 6 章的 124M 后 val loss 与 SDPA 一致。出处 Dao et al., arXiv 2205.14135;FlashAttention-2 见 arXiv 2307.08691。
预训练:在海量无标注文本上做"预测下一个 token",产出只会续写的基座模型(base model) —— Phase 1 做的就是这步。 后训练:在 base 之上继续调,让它"会听话":包括 SFT 和 偏好对齐。 现代大模型 = 预训练 → SFT → 偏好对齐 一条流水线,后一段都站在前一段权重的肩膀上。
后训练的一段,目标是让回答更合人类口味、更有用更安全。数据是"同一问题的两个回答 + 人类判断 A 比 B 好"。 它是目标,不是某个具体算法:实现它有两条路 —— RLHF(走强化学习)和 DPO(不走强化学习)。 所以"偏好对齐 = 强化学习"是把目标和其中一种手段画了等号。
微调时更新模型的全部参数。它是一种"手段"(怎么改权重),不是一种"任务" —— 既能用来做 SFT,也能用来做 DPO。 所以"全量微调 = SFT"是把手段误当成任务。它和 LoRA 是二选一的两种手段,不是先后步骤。效果直接,但每个任务都要存一份完整模型,显存/硬盘开销大。
一类"只更新极少量参数"的微调手段的统称,目的是省显存、省存储。LoRA 是其中最流行的一种(还有 QLoRA = LoRA+量化 等)。 与 全量微调 相对,二者是同一件事(微调)的两种改权重方式。
一种 PEFT 手段:冻结原模型,在某些 Linear 旁边挂两个低秩小矩阵 A、B,只训练它俩。
可训练参数常只占 ~1%,却能逼近全量微调的效果;一个底座还能挂多个适配器随时切换。它是"怎么省钱地微调"的手段,
可叠加在 SFT 或 DPO 上。本项目在第 9 章 / 07_lora.py 手搓了一遍。
SFT 的关键一步:把整条"指令+回答"喂进去做前向,但只对回答段算 loss,
指令/模板段的标签设成 -100(PyTorch 的 ignore_index)被忽略。这样模型只学"给定指令该怎么答",
而不会去学"生成用户的指令"。SFT 和预训练用同一个 loss,差别主要就在这一处掩码。
把"指令"和"回答"拼成一条序列时套的固定格式(用 <|user|> / <|assistant|> 等特殊标记标出角色边界),
让模型分清谁说的、该自己答哪段。结尾的 EOS(结束标记)尤其重要:base 模型的毛病是不会停,
SFT 在每条回答末尾放 EOS 并算进 loss,就是教模型"答完就停"。
一种不走强化学习的 偏好对齐方法:把 RLHF 的目标推成闭式解,
直接在"chosen 好 / rejected 差"的偏好对上做一个类监督损失,拉高 chosen、压低 rejected。
不需要奖励模型、不需要在线采样、没有 RL 循环,单机就能跑。它不是 PPO 的变体,
而是和 RLHF 平行的另一条路;卖点是"不用 RL 那套笨重机器也能拿到 RLHF 的效果"。本项目偏好对齐只做 DPO(08_dpo.py);第 21 章把离线 DPO 放进和 SFT、GRPO 的同台对照。
用强化学习做 偏好对齐:先用偏好数据训一个奖励模型给回答打分, 再用 PPO(一种 RL 算法)让模型最大化奖励。要在线采样 rollout、要价值网络,工程量比 DPO 大数倍。 它和 DPO 是实现同一目标的两条路(RLHF 走 RL,DPO 不走)。本项目没有实现 RLHF / PPO;第 21 章实现了同属强化学习的 GRPO,奖励来自判分器而不是奖励模型。
把 Transformer 的 FFN 拆成多个"专家"(结构相同、各自更窄的小 FFN),
每个 token 由路由器派给其中 top-k 个处理。
效果是总参数(容量)变多、每 token 计算量不变 —— Mixtral 8×7B(总 46.7B / 激活 12.9B)、
DeepSeek-V3(总 671B / 激活 37B)都是这个机关。专家的分工不是人设计的,是训练中自己涌现的,
而且多为词形/标点这类低层统计分工,不是"数学专家/法律专家"。本项目在 09_moe.py 手搓(4 专家 · top-2)。
MoE 的派单员:一个 n_embd → n_expert 的线性层,给每个 token 对每个专家打分,
softmax 后只保留分数最高的 top_k 个,输出 = 选中专家的加权和(k>1 时权重在选中的几个里重新归一化)。
派单单位是 token(同一句话里相邻字符可以走不同专家)。top-k 挑选本身不可导,
但选中专家的权重来自 softmax、可导 —— 梯度顺着权重传回路由器,它就学会了"该派给谁"。
⚠️ 所以那个权重是路由器唯一的学习通道,top-1 时千万别归一化:只剩一个专家,归一化后权重恒等于 1, 梯度随之归零、路由器等于被冻住。Switch Transformer 的 top-1 就是直接拿未归一化的概率当权重。
治路由器偏心的辅助损失(Switch Transformer 式):aux = n_expert · Σ f_e·P_e,
其中 f_e 是实际派给专家 e 的 token 占比(不可导)、P_e 是路由器给 e 的平均概率(可导)。
负载均匀时取最小值 1;谁过热,梯度就顺着可导的 P 压谁的分数。以小系数(如 0.01)加进总 loss,
防止专家塌缩。
MoE 不加管束时的失败模式:"接单多 → 练得好 → 分数高 → 接单更多"的正反馈滚雪球,
路由器把绝大多数 token 塞给一两个专家,其余专家拿不到梯度、彻底闲置 ——
参数还在,容量已废,MoE 退化成小 dense 模型。解法就是负载均衡 loss。
top_k 越小、专家越多,越容易塌;09_moe.py 里 --top-k 1 --aux 0(Switch 同款 top-1)可以亲眼看偏载滚雪球,
而默认的 top-2 + 4 专家配置天生抗塌(每个 token 同时养 2 个专家,谁都饿不死)。
处理一个 token 实际参与计算的参数量。dense 模型里激活参数 = 总参数; MoE 里每 token 只走 top-k 个专家,激活参数远小于总参数 —— 所以模型简介里会写两个数字 (如"总 671B / 激活 37B")。注意 MoE 省的是算力(FLOPs),不是显存:没被激活的专家参数也得放在显存里待命。
DeepSeek-V3 的做法(arXiv 2412.19437 §2.1.2;方法出自 arXiv 2408.15664):每个专家配一个偏置 b,只在挑选 top-k 时加到分数上,
门控权重仍用原始分数,偏置也不进 loss。每步训练后,接单高于平均的专家 b 减 γ,低于平均的加 γ(V3 前 14.3T token 用 γ=0.001)。
和 aux loss 相比,它不往语言模型的梯度里掺别的目标。
V3 另外保留一个系数 0.0001 的序列级均衡 loss。本项目 09_moe.py --balance bias 是它的教学版。
把"第几个位置"编码成旋转角度,直接转进 q 和 k 里,而不是像 位置嵌入那样查一张表再相加。 向量按两两一对当成平面上的点,位置越靠后转得越多;每对配一个不同的转速,快的管近处、慢的管远处。
它最重要的性质是:转完之后 q·k 只依赖两个 token 的相对距离,与它们的绝对位置无关 ——
这正是语言里真正要紧的信息。而且没有"表只有 block_size 行"的限制,所以后来把上下文从 4K 拉到 128K 的做法
(NTK / YaRN 等)动的都是 RoPE 的频率。只作用在 q、k 上,v 不转(位置只该影响"谁看谁",不影响搬运的内容)。
本项目在第 12 章 / 10_llama.py 手搓了一遍。
🥚 它在真实训练里错过一次(整段跑了几万亿 token 才发现用的是上一代设置)→ 学习札记。
LayerNorm 的简化版。LayerNorm 做两件事:①减均值 ②除以标准差; RMSNorm 只做第 ②,直接除以均方根,而且不带 bias。
省掉的那一步在实测里贡献很小,但归一化每层要做两次,几十上百层累起来就是实打实的算力与参数。 效果基本持平,所以 LLaMA 之后的模型基本都换成了它。代价是输出不再以 0 为中心 —— 归一化真正要的是把尺度拉回来,这一点它照做。
FFN 的门控版本。输入走两路:一路过
SiLU(x·sigmoid(x),也叫 Swish)当闸门,一路当内容,两路逐元素相乘,再压回主干。
和 ReLU 的差别在于谁来决定关不关:ReLU 由通道自己说了算,只有"全开"和"关死"; 门控的开度来自另一路独立的线性变换,可以做到"这个通道只放三成过去"。 因为多了一个矩阵(gate / up / down 三个),隐层要收窄到 8/3·n_embd 才与 ReLU 版参数量相当 —— 这就是那个奇怪系数的来历。
让多个 query 头共享一份 K/V。query 头数不变(各问各的),但 K/V 只存 n_kv_head 份,
每组内的 query 头合用一份。因为 KV-cache 是按 K/V 头数算的,
显存直接按比例省:4 个 query 头配 2 份 K/V,cache 就少一半。
两个极端:n_kv_head = n_head 就是普通多头注意力(MHA);
n_kv_head = 1 是 MQA,省得最狠但表达力损失明显。
GQA 正是被当作两者之间的折中提出来的,现在主流模型多用它(比如 64 个 query 头配 8 份 K/V)。
给隐藏层 2D 权重用的优化器(Keller Jordan 2024)。先做动量,再用 Newton-Schulz 迭代把更新矩阵近似正交化
(五次多项式系数 3.4445, −4.7750, 2.0315,迭代 5 步),让更新在各个方向上的步长接近;嵌入、输出头、归一化等参数仍用 AdamW。
Moonshot(arXiv 2502.16982)在大模型上加了 weight decay 和按矩阵形状缩放更新,报告达到同样效果约需 AdamW 52% 的算力。
Kimi K2、DeepSeek-V4、Qwen3.8-Flash-Next 都公开使用 Muon(后两者的 Newton-Schulz 分别是 10 次和 8 步)。
本项目 10_llama.py --optim muon 可以直接对照(第 12 章 STEP 6 的 Q&A)。
去掉 softmax 之后,注意力输出可以写成 o_t = (Σ_i v_i k_iᵀ) q_t:不必保留每个历史 k_i、v_i,只维护每个头一块 head_size × head_size 的矩阵 S。
来一个 token 写一次 S ← S + v kᵀ,读的时候算 o = S q。推理状态的大小与上下文长度无关,模型等价于一个 RNN,
Katharopoulos et al.(arXiv 2006.16236,ICML 2020)据此把复杂度从 O(N²) 降到 O(N)。
代价是只加不减:key 之间不正交时读出会串扰,同一个 key 写两次新旧值会叠加。
本项目 第 13 章 / phase4-efficiency/11_linear_attn.py --preset linear:键值检索 N=32 答对 16.6%(全注意力 100%);
语言模型测试长度从 64 拉到 512,val loss 从 1.576 涨到 1.673。改进写法见 delta rule 与 Gated DeltaNet。
往记忆板写入时先读出 key 当前存的值,只补上与目标的差额:S ← S + β (v − S k) kᵀ,β ∈ (0,1) 是写入强度,由输入算出。
Yang et al.(arXiv 2406.06484)把它解释为对在线回归损失 ½‖S k − v‖² 做一步 SGD;k 做 L2 归一化且 β=1 时,旧值被整个换成新值。
Schlag et al.(arXiv 2102.11174)最早把它用在线性 Transformer 上,用来缓解纯加法写入的容量限制。
第 13 章 --preset delta:训练长度 64 上 val loss 1.5651,测试长度 512 时 1.629。它能改写单个 key,但没有遗忘门,整块状态不会衰减。
在 delta rule 上加一个遗忘门 α:S_t = S_{t−1}(α_t(I − β_t k_t k_tᵀ)) + β_t v_t k_tᵀ,o_t = S_t q_t
(Yang, Kautz, Hatamizadeh,arXiv 2412.06464 §3.1 式 (10),ICLR 2025)。α_t ∈ (0,1) 让整块记忆按比例衰减,β_t 负责精确改写单个 key。
论文 §2.1 对照:Mamba2 是 S_t = α_t S_{t−1} + v_t k_tᵀ,有遗忘门没有 delta rule。
Qwen3-Next-80B-A3B 的 48 层里 36 层是 Gated DeltaNet。第 13 章 --preset gdn(4 层,每头 32×32 记忆板):
val loss 1.5646,测试长度 512 时 1.532(全注意力 3.517);键值检索 N=32 答对 99.9%。第一层 α 开训时约 0.95,训完 4 个头的平均值为 0.58–0.66。
大部分层用固定状态的线性注意力,每隔几层留一层保留全部 K/V 的注意力层,用后者做精确检索、前者省推理状态。
Qwen3-Next-80B-A3B:12 × (3 × Gated DeltaNet + 1 × Gated Attention),48 层;Qwen3.5-397B-A17B:15 × 同样的 4 层周期,60 层;
Qwen3.8-Flash-Next:12 × (3 × Gated DeltaNet + 1 × QSA),注意力层在持续预训练阶段换成稀疏注意力;
Kimi Linear(arXiv 2510.26692)3 层 KDA 配 1 层 MLA,KV cache 最多省 75%。
Zoology(arXiv 2312.04927)的分析里,高效架构与注意力的质量差距有 82% 由上下文召回能力解释。
第 13 章 --preset hybrid(3 GDN + 1 注意力):val loss 1.5446,测试长度 512 时 1.536;键值检索 N=32 答对 93.3%,
这组训练 6000 步时 loss 仍在下降、未收敛,不能据此和纯 GDN 排先后。本实验最多记 32 对,纯 GDN 的 32×32 记忆板装得下;需要从很长的上下文里精确找回内容时,注意力层才是必需的。
每个 query 只和一部分 key 算注意力分数,其余位置不参与。K/V 仍可全部保存,省的是每一步要读和要算的量; 和线性注意力对比,后者把历史压进固定大小的状态,省的是存储。常见做法是先用便宜的方式给历史打分,再只对高分部分做完整注意力。
本项目在第 14 章 / phase4-efficiency/12_sparse_attn.py 手写了 NSA 式三支路(用 mask 模拟):256 上下文里平均每个 query 读 76.8 个 key(全注意力 128.5);
远处找钥匙任务上准确率 99.85%(全注意力 100%,只滑窗 3.1%)。
DeepSeek 2025 提出(Yuan et al., arXiv 2502.11089)。三条支路并行:压缩把每块 token 压成一个摘要 key/value;挑选复用 query 对摘要的注意力分数,取 top-n 块逐个细看;
滑窗看最近 w 个 token。输出 o = Σ gc · Attn(q, Kc, Vc),门 g ∈ [0,1] 由 MLP + sigmoid 得到,三条支路各有独立的 K/V。
论文默认:压缩块 l=32、步长 d=16,挑选块 l′=64、挑 n=16 块,滑窗 w=512;64K 上下文解码每步读 5,632 个 token(表 4)。在 27B 总参 / 3B 激活的模型上从预训练开始使用。 第 14 章教学版取 block_len 8、top_n 4、window 32。
每个 query 只看最近 w 个 token,读取量固定为 w,KV cache 也只需保留最近 w 个。缺点是窗口以外的内容完全看不到,只能靠多层叠加间接传递。
第 14 章实测:tiny shakespeare 字符级语言建模上 w=32 的 val loss 1.5052,全注意力 1.5038;远处找钥匙任务里钥匙在窗口之外,准确率 3.1%,等于在 32 个值里乱猜。 NSA 用它做三条支路之一,DeepSeek-V4 的 CSA/HCA 也带一条 128 的滑窗支路。
DeepSeek-V3.2 的 DSA(arXiv 2512.02556 §2.1)里给 (query, 历史 token) 打分的小模块:It,s = Σj wt,j · ReLU(qt,j · ks)。
头数少、可用 FP8 实现;每个 query 按分数选 2048 个 token 做完整注意力。
后续变体:DeepSeek-V4 的 CSA 先把每 4 个 token 的 KV 压成一条,再用 indexer 挑 top-k 条(Flash 512,Pro 1024),indexer 注意力用 FP4; Qwen3.8-Flash-Next 的 QSA 用 4 个 query 头 + 1 个 key 头的 MQA indexer,给每 4 个 token 池化成的块打分,预算 2048 token。
挂在 Transformer 旁边的一张大参数表,按局部 n-gram 寻址。每个 token 用以它结尾的 2-gram、3-gram 经 哈希算出几个行号, 每个头取 1 行拼成记忆向量,再经 门控、投影和短卷积加回残差流。 表有多少行,每 token 都只读固定几行,所以总参数可以涨、每 token 算力几乎不变;和 MoE 一样把容量与算力拆开,区别是按字面地址取向量,不做路由。 行号只依赖输入 token,forward 之前就能算出,表可以放在主机内存。
出处:DeepSeek Engram(Cheng et al., arXiv 2601.07372;Engram-27B 与 MoE-27B 同为 26.7B 总参、3.8B 激活,Engram 表 5.7B);Qwen3.8-Flash-Next 的 N-gram Embedding(125B 总参、6B 激活,另有 51B 表参数放在加速器之外)。
本项目在第 15 章 / phase4-efficiency/13_engram.py 手写:8 个头 × 262,144 行 × 16 维,表 33.6M 参数,是主干的 4.63 倍;FineWeb-Edu 上 5000 步 val loss 从 5.0561 降到 4.9945(单种子)。
把以当前位置结尾的 n 个 token id 各乘一个随机奇数,异或到一起,对表长取模,得到行号;开头不足 n 个 token 时用 0 号 token 补齐。 不同的 n-gram 可能落到同一行,叫撞行。每种 n-gram 用多个头、每个头一组不同的乘子,两个 n-gram 在一个头上撞了,在别的头上多半撞不上,拼起来的向量仍能区分。
第 15 章在 FineWeb-Edu 训练集前 20 万 token(111,302 种 2-gram)上统计:每头 16,384 行时 2-gram 撞行 85.3%,行被全部用满;262,144 行时降到 18.4%,与均匀随机撒点的理论值一致。 Engram(arXiv 2601.07372 §2.2)用的也是 multiplicative-XOR 哈希,并先做 NFKC 规范化和小写的 tokenizer 压缩。
决定取回的 n-gram 记忆在当前位置加多少。当前隐状态 h 当 query,记忆 e 经投影后当 key:
α = σ(RMSNorm(h) · RMSNorm(W_k e) / √d),α 是 0~1 的标量,再乘上 W_v e 加回残差流。
行号只看字面上的最近几个 token,同一个 n-gram 在不同上下文里取回同一个向量,撞行时还混着别的 n-gram,门负责在不搭的位置把它关小。
公式出自 Engram(arXiv 2601.07372 §2.3,式 (3)(4)),之后接 kernel 4、dilation 3 的因果卷积;第 15 章的 NgramMemory.forward 用同样的门、kernel 4 的因果卷积。
训练集上的 loss 继续下降,没见过的数据上的 loss 却上升:参数记住了训练集的具体内容,没有学到能推广的规律。数据少、参数多、同一批数据被反复看很多遍时容易出现。 常见应对:加数据、提前停止、dropout、减少参数。
第 15 章的反例:tiny shakespeare 训练集约 100 万字符,5000 步看了约 20 遍;挂上 32,768 行(4.19M 参数)的 n-gram 表后,train loss 降到 1.0813,val 从不挂表的 1.5334 涨到 1.6536。 同样的表放到 FineWeb-Edu(多数文本只见一次)上没有出现这种情况。
把大参数表留在 CPU 内存(或更慢的存储)里,只把本步要用的行取出来送进加速器。n-gram 表适合这样做:行号只依赖输入 token,数据一读进来就能算出, 取行和拷贝可以和上一步的 GPU 计算并行;MoE 的专家要等路由器算完才知道选谁。
Qwen3.8-Flash-Next 技术报告:"additional 51B parameters of n-gram embedding tables held off the accelerator",表放主机内存、异步预取(§2.3)。 第 15 章在单卡 RTX 4090 上测了 32,768 行的表:不挂表 276,231 token/s,表在显存 262,066,表在主机内存 148,229(各 400 步测一次;脚本同步查表、同步拷贝,没有做预取)。
Gemma 3n 的逐层嵌入:按单个 token id 给每一层查一段向量(HF transformers 实现里 vocab_size_per_layer_input 262,144,每层 256 维),经门控乘入该层。 官方说明这部分参数可以放在 CPU 上加载和计算,E2B / E4B 总参 5B / 8B,加速器上约 2B / 4B。
它和 N-gram 查表记忆都是地址确定、可放主机内存的稀疏表,区别在键:PLE 用单个 token,n-gram 表用最近几个 token。 Qwen3.8 报告 §2.3 原句:"N-gram embeddings further generalize unigram lookup by conditioning memory retrieval on local context rather than token identity alone"。
让便宜的草稿员(小模型,或主模型自带的 MTP 头)先猜 γ 个 token,主模型把"上下文 + γ 个草稿"一次 forward,
因果掩码保证每个位置的输出只依赖前文,于是一次拿到 γ+1 个位置的分布 p。草稿 x 以 min(1, p(x)/q(x)) 的概率接受,
拒绝时从 max(0, p−q) 归一化后的分布重抽,后面的草稿作废。输出分布与主模型逐个采样相同,省的是主模型 forward 次数
(Leviathan et al.,arXiv 2211.17192 §2.3,ICML 2023;Chen et al.,arXiv 2302.01318 同期提出,p/q 字母含义相反)。
接受率 α 下每次主 forward 期望吐 (1 − α^(γ+1)) / (1 − α) 个 token(式 (1))。
本项目 第 16 章 / phase4-efficiency/14_mtp.py(RTX 4090,600 token,temperature 1.0):1 层 64 维草稿模型接受率 0.667–0.745,
γ=6 时每次主 forward 吐 3.093 个 token,但草稿单次 forward 耗时是主模型的约 27%,墙钟反而为 0.944×;γ=2 墙钟 1.238×。
同一位置 20 万次投机采样与 p 的总变差距离 0.0005(草稿 q 与 p 为 0.053);贪心下 20 段提示逐字一致。
训练时让主模型顺带预测更远的 token。Gloeckle et al.(arXiv 2404.19737,ICML 2024)在共享主干上接 n 个独立输出头并行预测;
DeepSeek-V3(arXiv 2412.19437 §2.2)改成顺序模块:M_k [RMSNorm(h_i); RMSNorm(Emb(t_{i+k}))] 过一个 Transformer Block,
嵌入表和输出头与主模型共用,保持完整因果链。DeepSeek-V3 取深度 D=1,MTP loss 权重 λ 前 10T token 为 0.3、之后 0.1;
推理时 MTP 头当投机解码的草稿员,第二个 token 接受率 85%–90%,TPS 为 1.8 倍(§5.4.3)。
第 16 章 14_mtp.py:主模型 742,849 参数,MTP 模块 214,528(两个 RMSNorm + 256→128 投影 + 1 个 Block)。
MTP 头 val loss 1.5332,自草稿接受率 0.899,每次主 forward 吐 1.893 个 token,墙钟 1.27×。带 MTP loss(λ=0.3)的主模型 val loss 1.5276,
不带为 1.5357,单种子下差距在噪声量级。
用更少的位数存模型权重。三步:① 给一段权重定一个 scale;② 权重除以 scale 后四舍五入到格点,只存格点编号;③ 用时乘回 scale。 4 位整数只有 16 个编码,对称用法取 −7 … 7 共 15 个格点。
精度损失主要取决于一个 scale 管多少个权重,以及 scale 所管范围里有没有离群值。
本项目在第 17 章 / phase4-efficiency/15_quant.py 把第 6 章的 124M 量化成 15 种格式:4.5 位的 NVFP4 从 497.9 MB 压到 126.9 MB,val loss 从 3.0213 到 3.0638;整矩阵一个 scale 的 int4 崩到 9.9342。
把一段权重映射到格点上的缩放系数。对称整数量化取 scale = 这段权重的最大绝对值 / qmax,qmax = 2bits−1 − 1。
按一个 scale 管多少权重分三档:per-tensor 整个矩阵一个;per-row(per-channel)每行一个;group-g 每 g 个一个。
管得越细,格子越贴合局部的数值范围,误差越小;但 scale 本身也占位数,摊到每个权重上的开销是 scale 位数 / g(见 bpw)。 第 17 章实测 int4:整矩阵 val loss 9.9342,每行 3.2456,每 128 个 3.1360,每 32 个 3.0949(fp32 为 3.0213)。
量化格式里每个权重平均占的位数 = 格点位数 + scale 等元数据的位数摊到它管的权重上。 int4 每 32 个一个 16 位 scale:4 + 16/32 = 4.5;NVFP4 每 16 个一个 8 位 scale:4 + 8/16 = 4.5;MXFP4 每 32 个一个 8 位 scale:4 + 8/32 = 4.25。 模型存储大小 ≈ 被量化参数数 × bpw / 8,再加上没量化的那部分参数。
同一个矩阵(或同一组激活)里,比绝大多数数值大很多倍的少数数。整矩阵共用一个 scale 时,scale 由最大值决定, 一个离群值会把所有格子撑大,正常大小的数被大量舍成 0。
第 17 章里 124M 的 48 个 Linear 矩阵,最大绝对值是标准差的 5.6 到 30.4 倍;int4 整矩阵量化 val loss 崩到 9.9342,只把每个矩阵最大的 0.1% 权重留在 16 位,回到 3.2633。 LLM.int8()(Dettmers et al., arXiv 2208.07339)针对的是激活里的离群特征维:把这些维拆出来用 FP16 算,其余走 int8。
把权重量化成格点后立刻反量化回浮点,放回模型里算 loss 或生成。得到的数值与真 4 位存储在计算时解码出来的相同,所以能用来比较格式的精度;
但权重在内存里仍是浮点,不省显存、也不提速。真正省显存需要把格点编号按位打包,再配专门的反量化 / 矩阵乘算子。
第 17 章的 15_quant.py 用的就是假量化,页面上的 MB 是按位数算出来的存储大小。
4 位浮点:1 位符号、2 位指数、1 位尾数。非负值只有 0, 0.5, 1, 1.5, 2, 3, 4, 6 八个(指数为 0 时是次正规数 0 与 0.5),最大 6。 和均匀的 int4 格点比,它在 0 附近更密、远处更稀,贴合权重扎堆在 0 附近的分布。单独用它表示不了多少数,要配块级 scale 使用, NVFP4 和 MXFP4 的元素都是 E2M1。
llama.cpp 的 4 位权重格式(PR ggml-org/llama.cpp#1684,Iwan Kawrakow,2023-06)。每 256 个权重一个超块,切成 8 个 32 的子块;
子块存 4 位非对称格点(反量化 x = a·q + b)和各 6 位的 scale、min,超块存两个 FP16(d、dmin)把 6 位值还原。
合计 4 + 12/32 + 32/256 = 4.5 位 / 权重。
常见的 Q4_K_M 是混合方案:PR 原文 "uses Q6_K for half of the attention.wv and feed_forward.w2 tensors, else Q4_K"(之后版本的规则可能有调整)。
第 17 章的仿写版 q4_k_like 在 124M 上 val loss 3.0656(fp32 为 3.0213)。
NVIDIA 的 4 位格式:元素是 E2M1,每 16 个值共享一个 8 位 FP8(E4M3)scale,另有整个张量一个 FP32 scale, 每权重 4 + 8/16 = 4.5 位。E4M3 scale 能取小数,块内最大值基本正好对到 E2M1 的 6 上;块也比 MXFP4 小一半,一个离群值牵连的值更少。 出处:NVIDIA 技术博客 "Introducing NVFP4 for Efficient and Accurate Low-Precision Inference"(2025-06-24)。 第 17 章在 124M 上实测 val loss 3.0638。
OCP Microscaling Formats (MX) v1.0 标准里的 4 位格式(论文 Rouhani et al., arXiv 2310.10537):元素是 E2M1, 每 32 个值共享一个 8 位 E8M0 scale,E8M0 只能表示 2 的整数次幂。每权重 4 + 8/32 = 4.25 位。 因为 scale 只能是 2 的幂,块内最大值除以 scale 后落在 4 到 8 之间,超过 6 的部分被截断。 第 17 章在 124M 上实测 val loss 3.1030,比 NVFP4 多 0.039。
初始词表是 256 个字节的 BPE:文本先按 UTF-8 编成字节,再反复把出现最多的相邻一对合并成新 token。 任何字符串都能编码,不需要"未知字符";代价是中文等每字 3 字节的文字初始序列长,要靠合并压回来。GPT-2 论文 §2.2 提出这一做法。
第 18 章 phase5-fullstack/16_bpe.py 在 8 MB FineWeb-Edu 上训到 16,384 词表(16,128 次合并,纯 Python 约 2 分钟),
1 MB 测试语料每 token 4.252 字节,GPT-2 的 50,257 词表是 4.622。
BPE 训练的全部产物:按先后顺序记下的每一次合并,第 i 条合并产生 id 为 256 + i 的新 token。 编码新文本时,在每一段里反复找"合并表里排得最靠前"的相邻对合掉,直到没有能合的;解码是把每个 token 查回它代表的字节拼起来。 只用合并表的前 k 条,就得到一个 256 + k 的小词表(第 18 章 STEP 4 的扫描就是这样做的)。
BPE 合并前先用规则把文本切成小段,合并不跨段。GPT-2 的正则按缩写('s 't …)、字母串、数字串、其他符号、空白分类切,
所以 dog. dog! 里的 dog 是同一个 token。
第 18 章对照:同样 16,128 次合并,只按空格切时词表里有 2,279 个字母和标点粘在一起的 token(如 s, ation.),
GPT-2 正则下只有 7 个(全是缩写);只按空格切的压缩率高 3.8%。
一段文本的 UTF-8 字节数除以编码后的 token 数。越大,一个 token 平均代表的文本越多,同样 1024 的上下文能装更多内容,训练同样多文本需要的步数也越少。 它依赖训练语料:第 18 章用英文语料训出的分词器,中文例句每 token 只有 1 个字节(一个汉字都没合并)。
把一行数分成几段依次读,只记到目前为止的最大值 m 和按 m 算的指数和 l;新的一段带来更大的最大值时,之前的 l(以及累加中的输出)乘 exp(m_旧 − m_新) 缩回去。 读完一整行,结果与一次算完的 softmax 完全相同,不是近似。 出处 Milakov & Gimelshein, arXiv 1805.02867;它是 FlashAttention 能分块的前提。
OpenAI 开源的 GPU 编程语言,写在 Python 里、按"块"描述计算:tl.load 读一块数据到片上,tl.dot 做块间矩阵乘,
线程分配、共享内存和流水由编译器决定。PyTorch 的 torch.compile 在 GPU 上生成的也是 Triton kernel。
第 19 章 17_flash_attn.py 的 _flash_fwd 用约 30 行 Triton 写了 FlashAttention 前向。
模型最终 loss 随参数量 N 和训练 token 数 D 按幂律下降的经验规律。Chinchilla(Hoffmann et al., arXiv 2203.15556)用的形式是
L(N, D) = E + A/N^α + B/D^β:E 是数据本身降不下去的部分,另两项分别是模型太小、数据太少欠下的账。
做法是训一批小模型拟合系数,再外推到大模型。Kaplan et al.(arXiv 2001.08361)只数非嵌入参数,Chinchilla 数全部参数,两者的系数不能直接互换。
第 20 章 18_scaling.py 用 21 个 0.6M–25M 小模型的训练终点拟合,外推第 6 章 124M 训 10B token 的 loss:预测 3.36,实测 3.03。拟合点的 D 最多 16 亿,外推越远,预测偏高越多。
训练算力约 C ≈ 6·N·D(每个 token 前向 2N、反向 4N 次浮点运算)。C 固定时,让预测 loss 最小的那组 (N, D) 就是算力最优配比。 由 L(N, D) 求导可得 N_opt ∝ Cβ/(α+β)、D_opt ∝ Cα/(α+β)。Chinchilla 的结论是 N 与 D 大致同比例增长,约每个参数 20 个 token。 只看训练算力最省;推理成本也要算的话,常会刻意用更小的模型训更多 token。
从 n 个数据点里有放回地随机抽 n 个(有的点被抽到多次,有的没被抽到),用这组点重新拟合,重复几百次,看结果的分布。 数据点少时,它能说明一个拟合值有多不稳定。第 20 章对 21 个拟合点做了 200 次,给出外推值的 90% 范围。 它只反映「换几个点结果会晃多少」;如果全部拟合点都偏在同一侧(比如都只覆盖训练前段),区间会很窄,却盖不住真实值,第 20 章主拟合对 124M 的外推就是这样。
算注意力分数之前,先对每个头的 q、k 各做一次 LayerNorm。不加时,训练中 q、k 的量级可能越长越大,注意力分数(q·k/√d)随之失控, softmax 塌到只看一个位置,梯度暴涨,loss 回升,这叫注意力 logit 增长(Wortsman et al., arXiv 2309.14322)。 第 20 章里 1.8M 参数的模型训 4 亿 token 时,第 2 层注意力分数涨到约 1000 万;加 qk-norm 后同配置 val loss 从 4.95 降到 4.19。
奖励来自能自动判对错的程序(对最终答案、跑单元测试)的强化学习。和 RLHF 的区别在奖励从哪来:RLHF 用人类偏好训出的奖励模型打分, RLVR 用判分器,不需要标注也不会被"讨好"奖励模型。DeepSeek-R1 的推理能力主要靠这类训练得到。 第 21 章用它教 124M 做两位数加法。
DeepSeekMath(Shao et al., arXiv 2402.03300)提出的 RL 算法:同一道题采样 G 个回答,用组内平均奖励当基线、组内标准差归一化,
得到每个回答的优势;loss = −A · log π(回答) + β · KL(π ‖ π_ref)。
和 PPO 比省掉了价值网络(critic)。论文每题采 64 个,KL 系数 0.04。第 21 章 19_grpo.py 的 run_grpo 是它的最小实现。
检查模型回答对不对的程序,只输出分数,不给出正确答案。第 21 章的判分器是一行比较:回答去掉空格后是否等于正确的和。 判分器能判的东西决定了 RLVR 能教什么:判分器看不到的行为(比如格式之外的胡言乱语),奖励也管不到。
一个回答比基线好多少。策略梯度按优势加权:A > 0 的回答概率被抬高,A < 0 的被压低。 GRPO 里 A = (r − 组内均值) / 组内标准差,同一组的优势加起来为 0;组内奖励全相同(全对或全错)时 A 全为 0,这道题不产生梯度。
每道题采样 k 次,至少一次答对的题所占的比例。pass@1(常用贪心解码)衡量模型最常给出的答案对不对,pass@k 衡量它能不能采样出正确答案。 做 RL 之前,起点的 pass@k(k 取组大小)大致决定一组里能不能凑出对的回答,也就是 RL 有没有信号。
Google 2017 年的论文,提出 Transformer 架构:用自注意力彻底取代 RNN / CNN 来建模序列依赖, 并引入多头注意力与位置编码。它是 GPT、BERT 等几乎所有现代大模型的共同基石。 本项目第 2 关搭的单头自注意力是它的核心零件,第 3 关会完整复现它的架构(多头 + 残差 + LayerNorm + 前馈网络)。 🥚 它的"前世今生"(8 位作者、翻译起源、家谱)有个小故事 → 学习札记。