Chengshu@skadai · 2026.09.30
3,554 字 · 624 词 · 约 12 分钟

Karpathy《Let's build GPT》第四篇:nanoGPT 走读、encoder vs decoder,以及从 1000 万参数到 GPT-3 的距离

Karpathy《Let's build GPT》精读系列第四篇(收官):把论文架构图讲完(encoder、cross-attention 与 decoder-only 的关系),走读 nanoGPT(多头合并成四维批处理、MLP 用 GELU、checkpoint 与 DDP 等工程件),对照从 1000 万参数 / 30 万 token 到 GPT-3 的 175B / 300B token——结构几乎相同,规模差约一百万倍。最后讲清 ChatGPT 的两阶段:预训练只给你一个「文档补全器」,助手化还需要 SFT → 奖励模型 → PPO 三步对齐。文末另附「从 BERT 到 GPT:同一个 Transformer,三种用法」对照表(本文补充,专门写给做过 BERT 微调、没有大模型实战经验的人)。文中 10 张配图均截自视频对应时刻,并把当时的完整观点句(英文原句+中文翻译)拼进图里。

来源:YouTube 原视频(Andrej Karpathy,2023-01-17,时长 1:56)·配套代码:nanoGPT

阅读说明

  • 这是四篇系列的最后一篇,覆盖原视频 01–01。前三篇把模型搭起来、训练出结果;这一篇回答的是「我写的这个东西,和真正的 GPT 到底差在哪」。
  • 内容分四块:① 原论文架构图里剩下没讲的部分(encoder、cross-attention);② nanoGPT 代码走读(我们的代码与它的差异);③ 从 1000 万参数到 GPT-3 的规模对照;④ ChatGPT 的两阶段(预训练 → 对齐)与本文的收束。
  • 文末我加了一节 「从 BERT 到 GPT:同一个 Transformer,三种用法」——这是本文补充,专门给做过 BERT 微调、但没有大模型实战经验的人做衔接;Karpathy 在视频里没有讲这部分。
  • 数字均来自视频(10M 参数、约 300,000 个 GPT-2 token、GPT-3 最大 175B、300B tokens、百万倍等)。

1. 把论文架构图讲完:encoder、cross-attention 与「decoder-only」

前三篇结束时,我们对论文架构图还有一处没交代清楚:为什么原图里有两块 Transformer。

Karpathy 的解释是:因为那是一篇机器翻译论文。

  • 它假设输入是一段法语 token。这些 token 进入 encoder,而 encoder 里没有三角掩码——所有 token 可以自由地互相通信,把整个法语句子「编码」成一组表示;
  • 然后在 decoder 一侧,除了我们熟悉的掩码自注意力,还多了一条从 encoder 输出接过来的连接。这就是 cross-attention:
    • query 仍然由 x(解码器当前状态)产生;
    • 但 key 和 value 来自旁边——由 encoder 的节点生成;
    • encoder 的输出被接入解码器的每一个 block。

它在做什么?Karpathy 的原话:

它在让解码过程不只以当前解码的历史为条件,还以「已经看过的、完整编码过的法语提示」为条件。

这就是 encoder-decoder 模型,也是原论文需要两块 Transformer 加一个额外 block 的原因。

图 1|1:45 原论文的 encoder-decoder:cross-attention 里 query 来自解码器,key / value 来自编码器

那我们为什么不用? Karpathy 给出了一句非常干脆的答案:

我们没有这样做,是因为我们没有东西要编码——没有任何条件输入。我们只是有一份文本文件,只是想模仿它。这就是我们使用 decoder-only Transformer 的原因,GPT 正是这么做的。

图 2|1:46 没有条件输入,就不需要 encoder——decoder-only,正是 GPT 的做法

本文补充:这一小节其实把整个 GPT 家族和原论文的关系说清了。把「编码器 / 解码器 / 交叉注意力」这三件东西组合起来,就能覆盖你现在能看到的所有主流架构:

  • 只要 encoder(去掉掩码)→ BERT 那一类;
  • 只要 decoder(保留掩码)→ GPT 那一类;
  • 两个都要 + cross-attention → 原论文的翻译模型、T5、以及各种「条件生成」模型。

2. nanoGPT 走读:我们的 200 行 vs 它的两个文件(01–01)

Karpathy 用一个「非常简短的走读」把 nanoGPT 和课上写的代码对应起来。他说 nanoGPT 的核心就是两个文件:

  • train.py:训练网络的样板代码。和课上写的东西是同一套逻辑(训练循环、取 batch、评估、更新),但因为要考虑工程现实而复杂得多:保存/加载 checkpoint 与预训练权重、学习率衰减、torch.compile、跨多节点/多 GPU 的分布式训练。
  • model.py:模型本身,几乎与课上写的完全一致。

图 3|1:46 nanoGPT 仓库;右侧那条 validation loss 曲线,就是我们刚亲手跑出来的那条

2.1 差异一:多头注意力被合并成一个批处理实现

在课上,多头是「显式地建多个 Head,然后 torch.cat 拼接」。而在 nanoGPT 里:

所有的头都被实现在一个 causal self-attention 里,用批处理的方式完成。

结果是张量多出一个维度:除了 (B, T, C),还有一个 heads 维——变成四维张量。

Karpathy 特别强调:这在数学上完全等价,只是更高效——因为所有头现在被当成 batch 维一起算。

图 4|1:47 nanoGPT 的 CausalSelfAttention:Q/K/V、点积、掩码、softmax、dropout、取出 value——只是头变成了第四个维度

2.2 差异二:MLP 用 GELU 而不是 ReLU

它用的是 GELU 非线性,而不是 ReLU。这么做只是因为 OpenAI 用了它——我想能加载他们的 checkpoint。

这是一个很真实的工程理由:激活函数的选择有时不是为了更优,而是为了权重兼容。

图 5|1:48 MLP 用 GELU;block 的「通信 + 计算」结构与课上完全一致

2.3 其余部分:几乎逐行对得上

  • 位置编码 + token 编码相加;
  • 一叠 Block(通信 + 计算);
  • 末尾的 LayerNorm;
  • 最后的线性层 lm_head;
  • generate() 函数也非常相似。

此外还有两处工程细节:把参数分成「需要 weight decay」和「不需要 weight decay」两组;以及 checkpoint 的加载逻辑。

图 6|1:48 GPT 部分完全一致:位置编码、token 编码、blocks、末尾 LayerNorm、最终线性层

本文补充:这就是「架构相同、工程不同」最直观的一次演示。你以后读任何一个开源大模型实现(LLaMA、Qwen、DeepSeek),会发现自己能认出全部结构——剩下的差异几乎都是:位置编码的实现方式(RoPE / ALiBi)、归一化放在哪里(pre-norm / RMSNorm)、激活函数(GELU / SwiGLU)、以及注意力里的各种效率优化(GQA / MQA / FlashAttention)。骨架你已经在视频里亲手写过一遍了。

3. 从 1000 万参数到 GPT-3:差的是规模,不是结构(01–01)

3.1 我们训练出来的到底是什么

Karpathy 打印了参数量:

我这个用来生成小莎士比亚的 Transformer,大约是 1000 万参数。

然后是数据这一侧,这里有一个容易忽略的换算:

  • 莎士比亚文本约 1,000,000 个字符,也就是约 100 万个 token(字符级);
  • 但 OpenAI 用的不是字符级词表,而是 subword chunk,词表约 50,000 个元素,序列因此被大幅压缩;
  • 所以同样这本文本,在 OpenAI 的词表下大约是 300,000 个 token。

结论:我们训练的是一个 1000 万参数的模型,用了大约 30 万个 token。

图 7|1:50 GPT-3 论文的参数表:最大 175B 参数——我们的 10M 与它架构相同,规模差四个数量级

3.2 GPT-3 那一侧

Karpathy 直接翻到 GPT-3 论文的模型表:

我们 GPT-3(最大)
参数量 ~10,000,000 175,000,000,000
训练 token 数 ~300,000 300,000,000,000
层数 / 头数 / 头大小 / 学习率 我们自己设的 论文里逐项列出

注意他的说法:这些超参数你应该已经「认得出来」了——n_layer、d_model、n_head、head size、batch size、学习率,和我们调的是同一套东西。

训练 token 数从 30 万到 3000 亿,大约是 100 万倍(a millionfold increase)。

他还补了一句:这个数字按今天的标准已经不算大了——现在动辄上 1 万亿 token。

但除此之外,架构与我们实现的几乎完全相同;真正的差别在于——这是一个巨大的基础设施挑战,通常需要成千上万张 GPU 互相通信。

3.3 一句话总结这个差距

GPT-3 在架构上与我们写的基本相同,只是规模大了 10,000 到 1,000,000 倍——取决于你怎么数。

这就是为什么这个视频值得逐行看完:它把「规模」和「结构」这两件事彻底分开了。你以后再看各家模型发布,会清楚哪些是结构创新、哪些只是规模与数据工程。

图 8|1:55 收束:架构基本相同,规模大了 1 万到 100 万倍

4. 预训练之后:从「文档补全器」到「助手」(01–01)

4.1 预训练给你的不是助手,是文档补全器

Karpathy 提醒了一个极其容易被忽略的事实:

完成预训练之后,你得到的不是一个「能回答你问题的东西」。你得到的是一个文档补全器(document completer)。

它的行为是:

  • 胡说的是互联网,不是莎士比亚——它会生成随意的新闻文章和文档;
  • 你给它一个问题,它可能回你更多问题,也可能忽略你的问题,也可能开始写一篇新闻;
  • 因为它被训练的目标只是「补全序列」——所以它会做「一份训练数据里的相似文档接下来会做的事」。

他用了很重的一个词:totally undefined behavior(完全未定义的行为)。

图 9|1:52 预训练之后是「文档补全器」,不是助手——这正是 ChatGPT 还需要第二个阶段的原因

4.2 第二阶段:对齐,大致三步

Karpathy 用 OpenAI 那篇 ChatGPT 博客把流程讲了一遍:

  1. 收集助手式数据并做监督微调(SFT):数据的格式就是「上面是问题,下面是回答」。这类数据规模远小于互联网——可能是几千条量级。然后在这个格式上微调,让模型开始「期待看到问题、并补全回答」。
    • 他还点出一个反直觉的现象:这些超大模型在微调阶段非常「样本高效」——「这居然真的管用」。
  2. 训练奖励模型:让模型对同一问题生成多个回答,由人类标注者排序(哪个更好)。用这些偏好数据训练一个奖励模型,用来预测任意候选回答的「可取程度」。
  3. 用 PPO 做强化学习微调:有了奖励模型之后,用 PPO(一种策略梯度强化学习方法)去优化采样策略,让模型生成的回答在奖励模型下期望得分更高。

这三步把模型从「文档补全器」变成「问答助手」。

而且 Karpathy 明确划定了边界:

这个阶段的大部分数据并不公开,它是 OpenAI 内部的,也难复现得多。nanoGPT 覆盖的是预训练阶段。

图 10|1:54 对齐的三步:SFT → 奖励模型 → PPO,这才是 ChatGPT 的第二个阶段

4.3 视频的收尾

Karpathy 用几句话总结了整场讲座:

我们训练了一个 decoder-only 的 Transformer,遵循 2017 年那篇著名的《Attention Is All You Need》,在 tiny Shakespeare 上得到了合理的结果。全部训练代码大约 200 行——我会把这份代码库放出来,里面还带着我们一路构建过来的 git log 提交记录,另外也会放上 Colab notebook。

然后是他的临别提醒:

我们没有讲任何微调阶段。如果你想做的不是「语言建模」,而是执行任务、做特定对齐、或者情感分类——只要你不想要一个纯粹的文档补全器,你就必须走后面那些微调阶段:可能是简单的监督微调,也可能是像 ChatGPT 那样训练奖励模型、再跑若干轮 PPO。

5. 从 BERT 到 GPT:同一个 Transformer,三种用法(本文补充)

这一节不是视频内容,是写给「做过 BERT 微调、但没碰过大模型训练」的人的一页对照表。你其实已经会读 Transformer 了,只是用它的方式不一样。

原论文 BERT(encoder-only) GPT(decoder-only)
注意力掩码 编码器无掩码 / 解码器有掩码 无掩码(双向看全句) 有掩码(只能看过去)
训练目标 机器翻译 掩码语言模型 + 下一句预测 预测下一个 token
输入 源语言 → 目标语言 单句 / 句对 一段文本
输出用途 生成译文 取 [CLS] 表示做分类 生成续写
典型用法 — 加一个头做微调 预训练 + 提示 / 微调
注意力里的 Q/K/V 来源 解码器 query 来自自身,K/V 来自编码器(cross-attention) 三者都来自自身(self-attention) 三者都来自自身(self-attention,带掩码)

三个能立刻用上的结论:

  1. nn.Embedding(vocab, n_embd) + lm_head 就是 BERT 的 embedding + MLM head。 你在 BERT 里见到的「embedding 层 + 输出层权重绑定(weight tying)」,和我们在第一篇里写的 token_embedding_table / lm_head 是同一件事的两种形态。
  2. BERT 的「微调」和 GPT 的「预训练」不是同一层级的概念。 同一个 Transformer,前面接不同的头、用不同的目标训练,就变成了完全不同的东西。ChatGPT 的「微调」是在预训练之上再做对齐(第三篇之外的第二阶段);而 BERT 的微调是在预训练之上接一个任务头。两者都叫 fine-tuning,但数据和目标完全不同。
  3. 你以前熟悉的 [CLS]、[SEP]、段落级表示,本质是「encoder 看全句」的产物。 GPT 不能这样工作——它的每个位置只能看到自己左边,所以它天然是「逐 token 生成」的,而不是「一次理解整句」。这就是为什么 GPT 需要「把任务写进提示里」,而 BERT 只需要换一个头。

如果你想沿着这条线继续走,Karpathy 在视频里给的路标是:nanoGPT(预训练)→ 复现 GPT-2 (124M) → 再往上就是基础设施问题和数据问题了。

本篇速查

问题 答案
原论文为什么有两个 Transformer? 机器翻译需要 condition 在源语言上 → encoder + cross-attention
cross-attention 里 Q/K/V 各来自哪? Q 来自解码器当前状态,K/V 来自编码器输出
GPT 为什么是 decoder-only? 没有条件输入,只需要自回归生成
nanoGPT 与我们的代码差在哪? 多头合并为批处理(四维张量)、MLP 用 GELU、checkpoint / DDP / lr 衰减等工程件
我们的规模? ~10M 参数,~300,000 个 GPT-2 token
GPT-3 最大规模? 175B 参数,300B tokens(差约 100 万倍)
预训练的产物是什么? 文档补全器,不是助手
对齐的三个步骤? SFT → 训练奖励模型(人类排序)→ PPO 微调采样策略
nanoGPT 覆盖哪一段? 只覆盖预训练

自测题

  1. cross-attention 与 self-attention 的唯一区别是什么?为什么编码器不需要三角掩码?
  2. 如果让你做一个「中文 → 英文」的翻译模型,你会用 encoder-only、decoder-only 还是 encoder-decoder?为什么?
  3. nanoGPT 把多头实现在同一个批处理里,张量变成了四维。请说出这四个维度分别是什么。
  4. 为什么「参数量大 100 万倍」和「架构几乎相同」这两件事可以同时成立?这对你判断模型新闻有什么帮助?
  5. 预训练模型的输出是「文档补全器」。请举一个你在日常使用中见过、能用这个性质解释的失败案例。
  6. 为什么 PPO 需要先有一个奖励模型,而不是直接用人类反馈微调?

系列小结

四篇走完,你现在手上应该有这样的东西:

  • 一篇完整可跑的代码心智模型:从 1MB 文本 → 65 个字符的词表 → (B,T) 的样本 → embedding + 位置编码 → 掩码多头自注意力 → 前馈 → 残差 + LayerNorm → lm_head → 交叉熵 → 采样生成;
  • 一条能对上数字的损失曲线:4.87 → 2.5 → 2.4 → 2.28 → 2.24 → 2.08 → 2.06 → 1.48,每一个台阶对应一个具体的零件;
  • 一个尺度坐标:我们的 10M / 300k token 与 GPT-3 的 175B / 300B token,结构相同,规模差百万倍。

如果只记一件事,我建议记 Karpathy 在视频里反复回到的那个方法论:先做一个最简单、能跑的基线,然后一次只改一个东西,并且永远知道自己在哪个数字上。 这也是这个系列四篇文章真正想传递的东西。

系列四篇

  1. 语言模型、字符级 tokenizer 与 bigram 基线(00–00)
  2. 自注意力:从「加权平均的数学技巧」到 Q / K / V(00–01)
  3. 多头注意力、前馈、残差与 LayerNorm:拼出完整的 Transformer Block(01–01)
  4. nanoGPT、encoder vs decoder,以及从 1000 万参数到 GPT-3 的距离(01–01)

讨论

这里是静态站点,没有内嵌评论区。如果这篇文章对你有用,欢迎通过 RSS 订阅后续更新。