Part 6 讲义:Let's build GPT
〇、本节地图与五天切法
| 时间(官方章节) | 内容 | 本讲义 | 难度 |
|---|---|---|---|
| 0:00 – 7:52 | 引言:ChatGPT、Transformer、nanoGPT | 第一节 | ★ |
| 7:52 – 22:11 | 读数据、tokenizer、data loader | 第二节 | ★★ |
| 22:11 – 42:13 | bigram baseline + 训练 | 第三节 | ★★ |
| 42:13 – 1:02:00 | 数学戏法三连 + 位置编码 | 第四节 | ★★★ |
| 1:02:00 – 1:11:38 | THE CRUX:self-attention | 第五节 | ★★★★★ |
| 1:11:38 – 1:19:11 | 六条重要笔记 | 第六节 | ★★★ |
| 1:19:11 – 1:37:49 | 多头 → FFN → 残差 → LayerNorm | 第七节 | ★★★★ |
| 1:37:49 – 1:42:39 | 扩容 + dropout | 第八、九节 | ★★ |
| 1:42:39 – 1:54:32 | 模型家族、nanoGPT、ChatGPT 三阶段 | 第十节 | ★★ |
五天切法:Day 22 → 0:00–42:13 + STEP 0–2(读本讲义一~三节)| Day 23 → 42:13–1:19:11 + STEP 3–4(读四~六节,最重要的一天)| Day 24 → 1:19:11–1:37:49 + STEP 5–6(读第七节)| Day 25 → 1:37:49–结尾 + STEP 7–8(读八~十节)| Day 26 → 复盘 + 自测 + 毕业礼。
一、全局观:我们到底要造什么
先把地图铺开,免得在细节里迷路。任务和 makemore 一模一样——给定前文,预测下一个字符——只是数据从 3 万个名字换成了 100 万字符的莎士比亚全集,模型从 MLP/WaveNet 换成 Transformer。整个系统只有三块:
文本 → [tokenizer] → 整数序列 → [Transformer] → 下一个字符的概率分布 → [采样循环] → 生成文本
而 Transformer 本体,剥掉所有吓人的名词后是这样一个流水线:
idx (B,T) 整数
│ token embedding + position embedding # "我是谁" + "我在哪"
▼
x (B,T,C)
│ Block × N: x = x + MultiHeadAttention(LN(x)) # 沟通:token 之间交换信息
│ x = x + FeedForward(LN(x)) # 思考:每个 token 独立消化
▼
LN → Linear(C→vocab)
▼
logits (B,T,vocab) → cross_entropy(与右移一位的 targets)
你会发现除了 MultiHeadAttention,每个零件都是旧的:embedding 是 Part 2 的,LayerNorm 是 Part 3/4 里 BN 的近亲,残差的原理你在 Part 4 推过(加法梯度直通),Block 堆叠的写法是 Part 5 的 Sequential。本节真正的新东西只有一个:attention。所以视频花了整整 37 分钟(42:13–1:19:11)铺垫和讲解它,本讲义也一样——第四、五、六节全是它。
二、数据、tokenizer 与批次(7:52)
2.1 字符级 tokenizer:最笨但最透明
chars = sorted(list(set(text))) # 语料中出现过的全部字符,65 个
stoi = {ch: i for i, ch in enumerate(chars)}
itos = {i: ch for i, ch in enumerate(chars)}
encode = lambda s: [stoi[c] for c in s] # "hii" → [46, 47, 47]
decode = lambda l: ''.join(itos[i] for i in l) # 反向
tokenizer 的本质是文本和整数序列之间的可逆编码,它有一个根本权衡:词表大小 vs 序列长度。字符级:词表只有 65,但一句话要编码成很长的序列。GPT-2 用的 BPE(子词级):词表 50257,同一句话的序列短得多——"hello" 可能就是 1 个 token。词表越大,模型每一步"吞掉"的信息越多,但 embedding 表和输出层也越大。本节选字符级是教学取舍:让我们把全部注意力留给 Transformer 本身(Karpathy 后来有专门一讲 BPE tokenizer,那是你 1B 路上的必修课)。
2.2 一个 chunk = block_size 个训练样本(14:27,本节第一个关键理解)
取一段长 9 的文本,比如编码后是 [18, 47, 56, 57, 58, 1, 15, 47, 58]。令 block_size=8,则 x 是前 8 个,y 是右移一位的后 8 个。这一对 (x, y) 里同时藏着 8 个独立的训练样本:
context [18] → target 47
context [18, 47] → target 56
context [18, 47, 56] → target 57
context [18, 47, 56, 57] → target 58
context [18, 47, 56, 57, 58] → target 1
context [18, 47, 56, 57, 58, 1] → target 15
context [18, 47, 56, 57, 58, 1, 15] → target 47
context [18, 47, 56, 57, 58, 1, 15, 47] → target 58
为什么要这样?两个原因。效率:一次 forward 就能计算 T 个位置的 loss(配合因果 mask,每个位置只看得见自己的前缀,互不作弊)。推理需要:生成时上下文是从 1 个字符逐渐长到 block_size 的,模型必须在"任何长度的前缀"上都会预测——这种训练方式恰好把所有长度都练了。
所以 batch 维度加上后,一个 (B=4, T=8) 的批其实是 32 个样本。data loader 只有四行:
def get_batch(split):
data = train_data if split == 'train' else val_data
ix = torch.randint(len(data) - block_size, (batch_size,)) # 随机起点
x = torch.stack([data[i:i+block_size] for i in ix])
y = torch.stack([data[i+1:i+block_size+1] for i in ix]) # 右移一位
return x, y
三、bigram baseline 逐行讲(22:11)
先用最笨的模型把整个管线(数据→loss→训练→生成)跑通,再逐步换心脏。这是 Karpathy 方法论的直接实践:永远先有一个能跑的 baseline。
class BigramLanguageModel(nn.Module):
def __init__(self):
super().__init__()
self.token_embedding_table = nn.Embedding(vocab_size, vocab_size)
def forward(self, idx, targets=None):
logits = self.token_embedding_table(idx) # (B,T,65):直接查表当 logits!
if targets is None:
loss = None
else:
B, T, C = logits.shape
loss = F.cross_entropy(logits.view(B*T, C), targets.view(B*T))
return logits, loss
def generate(self, idx, max_new_tokens):
for _ in range(max_new_tokens):
logits, _ = self(idx)
logits = logits[:, -1, :] # 只要最后一个位置的预测
probs = F.softmax(logits, dim=-1)
idx_next = torch.multinomial(probs, num_samples=1)
idx = torch.cat((idx, idx_next), dim=1) # 拼回去,滚动生成
return idx
四个值得停下来的点:
- embedding 表直接当 logits:表的第 i 行 = "看到字符 i 时,下一个字符的 65 个打分"。这就是 Part 1 的 bigram 计数表,换成了可训练参数的皮
- reshape 约定:
F.cross_entropy要求类别维在第二维,所以 (B,T,C) 要摊平成 (B*T, C)——printf 一样常用,记住它 - 初始 loss 的数学检查(Part 3 的老朋友):随机初始化时每类概率≈1/65,loss 应≈
-ln(1/65) ≈ 4.17。跑出来差太远说明初始化有问题——这是免费的 sanity check - generate 是所有 LLM 的推理循环:预测→采样→拼接→再预测。ChatGPT 逐字往外蹦的样子,就是这个 while 循环
训练 3000 步后 loss ≈ 2.5,生成的是"形似英文的乱码"。记住 2.5——接下来每加一个零件都拿它对账。
四、数学戏法:三个版本的聚合(42:13)
bigram 的病根:每个位置只看自己。要变强,位置 t 必须吸收 0..t 的信息。Karpathy 先用"最弱的聚合方式"(求平均)打样,重点不是平均本身,而是怎么算得又对又快。
v1:for 循环(写给人看的版本)
xbow[b, t] = x[b, :t+1].mean(0) # bow = bag of words:过去的词装袋子里拌匀
v2:矩阵乘(写给 GPU 看的版本)—— 用具体数字看穿它
视频里的 toy example,亲手算一遍胜过十遍解释。取 T=3:
a = tril(ones(3,3)) a = 1 0 0 行归一化后 1 0 0
每行除以行和 1 1 0 → 1/2 1/2 0
1 1 1 1/3 1/3 1/3
b = 2 7 c = a @ b = 2 7 ← 第 1 行:只有 b[0]
6 4 4 5.5 ← 第 2 行:(2+6)/2, (7+4)/2
6 5 4.67 5.33 ← 第 3 行:三行的平均
看到了吗:a 的每一行是一组权重,c 的每一行 = 用这组权重对 b 的各行加权求和。下三角形状保证第 t 行只用到前 t 个。矩阵乘一次完成所有位置的聚合,GPU 最擅长的就是它。
wei @ v、embedding 查表、线性层,全是这一件事的不同权重来源。
v3:softmax + mask(为"可学习"留门的版本)
wei = torch.zeros((T, T))
wei = wei.masked_fill(tril == 0, float('-inf')) # 未来位置 → -inf
wei = F.softmax(wei, dim=-1) # 每行归一化;-inf → 权重恰好为 0
数值上和 v2 一模一样(全 0 经 softmax 就是均匀分布),看起来是脱裤子放屁——但它换来了一个决定性的接口:那个全 0 的分数矩阵可以换成任何数据算出来的分数,softmax 自动把分数变成归一化权重,mask 自动保证因果。把"分数从哪来"这个空填上,均匀平均就升级成 attention。这就是下一节。
另外此处顺手引入位置编码(1:00:18):每个位置 0..T-1 也查一张 embedding 表,加到 token embedding 上。为什么需要它——第六节笔记 2 揭晓。
五、THE CRUX:self-attention(1:02:00)
5.1 动机:每个 token 想要的不一样
均匀平均对所有历史一视同仁,但语言不是这样的:假设当前 token 是个元音,它可能最想知道"我前面最近的辅音是谁";一个引号想找和它配对的另一半。不同的 token、在不同的语境里,想从过去提取的信息不同——所以权重必须是数据依赖的(data-dependent)。self-attention 的方案:
k = self.key(x) # (B,T,hs) 每个 token 广播:"我包含什么"
q = self.query(x) # (B,T,hs) 每个 token 提问:"我在找什么"
wei = q @ k.transpose(-2, -1) * hs**-0.5 # (B,T,T) 亲和度:我的问题 × 你的内容
wei = wei.masked_fill(tril[:T,:T] == 0, float('-inf'))
wei = F.softmax(wei, dim=-1)
v = self.value(x) # (B,T,hs) "你若看我,我给你的是这些"
out = wei @ v # 按注意力权重加权聚合 value
逐个拆:
- q @ k.T 在算什么:wei[b, i, j] = 位置 i 的 query 和位置 j 的 key 的点积。两个向量方向越一致,点积越大——"i 在找的"和"j 所包含的"对上了,i 就多看 j 几眼。整个 (T,T) 矩阵一次算完所有配对
- 为什么要第三个投影 v:"我是谁"(用于被匹配)和"我能提供什么"(用于被聚合)是两回事。类比:面试时你的简历关键词(key)决定你被谁搜到,但入职后你输出的工作内容(value)是另一份东西。分开投影给模型解耦的自由
- 读懂 wei 矩阵:训练后打印某个 wei[0],你会看到类似——第 8 行在第 2 列有个 0.6 的高权重:第 8 个 token 强烈关注第 2 个 token。这张 T×T 的表就是"谁在看谁"的完整地图,每个 batch、每一层、每个头都不同
5.2 为什么除以 √head_size(笔记 6 提前展开,配套练习已数值验证)
q、k 初始时各分量方差≈1,那么 hs 维点积的方差≈hs(独立随机变量求和,方差相加)。实测:hs=16 时 wei.var() ≈ 16.7。方差 16 意味着 wei 里常出现 ±8 这种大数,softmax(8) 之后≈one-hot——每个 token 只死盯一个 token,其他全被忽略,而且 softmax 饱和区梯度≈0,学不动。乘上 hs-0.5 后方差回到≈1.04,softmax 保持"发散",注意力有商量的余地,梯度有路可走。Part 3 的老主题(控制激活分布)在 attention 里的重演。
5.3 比形状更硬的正确性检验:因果性测试
练习 STEP 4 里有个 Part 5 方法论的直接应用——形状对不代表语义对,所以再补一刀:改动位置 5 之后的输入,位置 0–4 的输出必须一个比特都不变。mask 写错的话这个 assert 立刻爆,而 loss 曲线可能要骗你几千步。这类"语义级 assert"是你以后写任何自定义层都该配的。
六、六条笔记,展开版(1:11:38)
Karpathy 连讲六条"务必记住",每条都是面试和读论文的高频考点:
笔记 1:attention 是一种通信机制(communication)
把 T 个 token 看成有向图的 T 个节点:每个节点向"能看见它"的节点提供信息,按边权(注意力权重)聚合。这个视角的价值在于解耦——attention 本身只定义"怎么通信",而"谁能看谁"(图的连接结构)由 mask 单独决定。改 mask 就改图:全连接、因果、稀疏、滑动窗口……后来的各种高效 attention 变体,多数是在改这张图。
笔记 2:attention 没有空间概念
注意 wei = q @ k.T 里没有任何位置信息——把序列打乱重排,每对 token 的亲和度不变。attention 天生作用于"集合"而非"序列"。这就是位置 embedding 必须存在的原因:把"我在位置 3"注入到 token 表示里,位置信息才能参与 q/k 的匹配。(对比:Part 5 的卷积把位置刻在结构里——谁和谁融合是写死的——所以卷积不需要位置编码。1B 时代你会遇到 RoPE、ALiBi,都是这个问题的进阶答案。)
笔记 3:batch 维互不通信
(B,T,T) 的 wei 是 B 张独立的注意力图,样本之间零交流。这听起来理所当然,但对照 Part 4/5 的 BatchNorm——它恰恰在 batch 维搅合样本——你就明白为什么 Transformer 用 LayerNorm:整个架构里样本从头到尾互不干扰,推理时来一个算一个,天然干净。
笔记 4:encoder block vs decoder block,只差一行
删掉 masked_fill 那一行,每个 token 就能看见全文——这叫 encoder block(适合"理解"任务:情感分类、BERT)。留着 mask,只能看过去——decoder block(适合"生成")。同一份代码,一行之差,两个世界。
笔记 5:self-attention vs cross-attention
self:q、k、v 都从同一个 x 算出(自己人内部开会)。cross:q 来自一路(比如正在生成的译文),k、v 来自另一路(原文的 encoder 输出)——"翻译时每写一个词,回头查一眼原文"。GPT 是纯 self;原始 Transformer 论文(机器翻译)是 encoder + decoder + cross 三件套。
笔记 6:scaled attention
见 5.2。一句话版本:√head_size 是方差修正项,防 softmax 饱和。
七、从单头到 Transformer Block(1:19:11)
7.1 多头:多路并行的通信频道(1:21:59)
class MultiHeadAttention(nn.Module):
def __init__(self, num_heads, head_size):
super().__init__()
self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)])
self.proj = nn.Linear(head_size * num_heads, n_embd)
def forward(self, x):
out = torch.cat([h(x) for h in self.heads], dim=-1) # 各头结果拼接
return self.proj(out)
为什么要多头?一次 attention 只能表达一种"找法"——一套 q/k 只能问一种问题。多头让 4 组独立的 q/k/v 并行工作:也许一个头学会盯元音辅音搭配,一个头盯词首词尾,一个头盯远处的引号配对。会计学:head_size = n_embd / n_head(64/4=16),4 个头各输出 16 维,拼回 64 维——总计算量和单个大头相当,但表达力更强(类似 Part 5 提过的分组思想)。末尾的 proj 让各头的信息融合一次再进残差。
7.2 FeedForward:先沟通,再思考(1:24:25)
self.net = nn.Sequential(
nn.Linear(n_embd, 4 * n_embd), # 扩到 4 倍(论文原始配方)
nn.ReLU(),
nn.Linear(4 * n_embd, n_embd), # 收回来
)
attention 聚合完信息后,如果立刻进下一轮聚合,token "来不及消化"。FFN 给每个 token 一段独立的私人计算(逐位置、不跨 token):升维→非线性→降维。Karpathy 的口号:attention 负责 communicate,FFN 负责 compute。整个 Transformer = 这两拍的 N 次循环。4× 的中间维度给足"思考空间"——现代 LLM 依然大体沿用(LLaMA 用 SwiGLU,约 8/3×,思想不变)。
7.3 残差连接:深网络的救命高速路(1:26:48)
直接把 Block 堆 3 层,训练反而变差——Part 3 的老病(深了训不动)复发。第一副药是残差:
x = x + self.sa(x) # 不是 x = self.sa(x)!
x = x + self.ffwd(x)
为什么灵?用 Part 4 你亲手推过的结论:加法的 backward 把梯度原样分发给两条支路。于是从 loss 到最底层,存在一条只经过"+"号的通路——梯度无衰减直达(Karpathy 叫它 gradient superhighway)。子层(sa、ffwd)变成挂在高速路旁的"支线",各自学自己的残差修正。初始时支线的投影层输出很小,网络行为接近恒等映射——先像浅网络一样训练,再逐渐"长深"。这是 2015 年 ResNet 的思想,Transformer 全盘继承。
7.4 LayerNorm:BN 的"无耦合"表亲(1:32:51)
第二副药。和 BN 的全部区别就在归一化的维度:
| BatchNorm(Part 3/4/5) | LayerNorm(从此以后) | |
|---|---|---|
| 归一化维度 | batch 维(跨样本,每通道) | 特征维(每个 token 自己) |
| 样本耦合 | 有——每个样本的输出依赖同批其他样本 | 零 |
| running 统计量 | 需要(推理时用) | 不需要,训练推理同款 |
| batch=1 推理 | 出问题 | 毫无压力 |
| backward | Part 4 大魔王 | 简单一个量级 |
你在 Part 4 被 BN 反向折磨、在 Part 5 被 BN 的 3D bug 坑——现在明白了:那些痛苦全部源自"跨样本耦合"这一个设计,而 LN 一刀砍掉了它。另一个演进:视频用 Pre-Norm(x + sa(ln(x)),先归一化再进子层)而非原始论文的 Post-Norm——Pre-Norm 让残差主路上完全没有非线性阻碍,深模型训练更稳,是现代 LLM 的标配。
7.5 Dropout(1:37:49,扩容前最后一个零件)
训练时以概率 p 随机把一些激活置零(等效于每步训练一个随机子网络),推理时自动关闭。模型变大后防过拟合的标准手段,加在三处:attention 权重后、多头 proj 后、FFN 末尾。小模型可设 0,视频扩容后用 0.2。
八、组装完整 GPT + 参数会计(1:37:49)
class GPTLanguageModel(nn.Module):
def __init__(self):
super().__init__()
self.token_embedding_table = nn.Embedding(vocab_size, n_embd)
self.position_embedding_table = nn.Embedding(block_size, n_embd)
self.blocks = nn.Sequential(*[Block(n_embd, n_head) for _ in range(n_layer)])
self.ln_f = nn.LayerNorm(n_embd) # 末端 LN(Pre-Norm 架构的惯例)
self.lm_head = nn.Linear(n_embd, vocab_size) # 语言模型头
def forward(self, idx, targets=None):
B, T = idx.shape
tok_emb = self.token_embedding_table(idx) # (B,T,C) 我是谁
pos_emb = self.position_embedding_table(torch.arange(T)) # (T,C) 我在哪
x = tok_emb + pos_emb # broadcast 相加
x = self.blocks(x)
x = self.ln_f(x)
logits = self.lm_head(x)
...
def generate(self, idx, max_new_tokens):
for _ in range(max_new_tokens):
idx_cond = idx[:, -block_size:] # ⚠️ 只喂最后 block_size 个
...
两个易错点:位置 embedding 必须加(笔记 2:不加的话模型对"字符顺序"完全失明,你可以做实验删掉它看 loss 差多少);generate 要裁剪上下文——序列越生成越长,超过 block_size 后位置表会索引越界,且 tril mask 也只有 block_size 大。
参数会计(CPU 配置:n_embd=64, n_head=4, n_layer=4, block=64, vocab=65)
| 部件 | 算式 | 参数量 |
|---|---|---|
| token embedding | 65 × 64 | 4,160 |
| position embedding | 64 × 64 | 4,096 |
| 每个 Block 的 attention | q/k/v 三投影 64×16×3×4头 + proj 64×64(含 bias) | ≈ 16.6k |
| 每个 Block 的 FFN | 64×256 + 256×64(含 bias) | ≈ 33.1k |
| 每个 Block 的 2 个 LN | 2 × (64+64) | 256 |
| Block × 4 | ≈ 200k | |
| 末端 LN + lm_head | 128 + 64×65+65 | ≈ 4.4k |
| 合计 | ≈ 212k |
注意 FFN 占了每个 Block 的 2/3 参数——这个比例在 1B、70B 模型上依然如此。以后听到"MoE 把 FFN 换成专家混合",你就知道为什么动的是 FFN:钱都花在那儿。视频的 GPU 配置(n_embd=384, 6 头, 6 层, block=256)同样的算法得 ≈10M。
九、战绩表与实验纪律
| 改动(每次只改一个) | val loss(视频口径) |
|---|---|
| bigram baseline | ≈ 2.5 |
| + 单头 self-attention | ≈ 2.4 |
| + 多头(4 头) | ≈ 2.28 |
| + FeedForward | ≈ 2.24 |
| + 堆 Block + 残差 + LN | ≈ 2.06 |
| 扩容 10M + dropout(GPU) | ≈ 1.48 |
这张表本身就是 Part 5 第九节"单变量实验纪律"的示范:每个零件的贡献都被单独度量过。你的 CPU 配置(212k 参数)预期落在 1.8–2.0——数字不同没关系,趋势必须一致:每加一个零件 loss 都该降,不降说明那一步写错了。
十、家族谱系与 ChatGPT 三阶段(1:42:39)
10.1 三大家族(用笔记 4/5 的语言)
| 家族 | 结构 | mask | 代表 / 用途 |
|---|---|---|---|
| decoder-only | 本节写的 | 因果 mask | GPT 系、LLaMA、你的 1B——生成 |
| encoder-only | 去掉 mask | 无 | BERT——理解/分类/检索 |
| encoder–decoder | 两者 + cross-attention | encoder 无 / decoder 有 | 原始论文(翻译)、T5 |
10.2 从我们的模型到 ChatGPT,还差什么(1:48:53)
Karpathy 明确给出坐标系。我们今天完成的是预训练(pretraining)的微缩版——在海量文本上学"预测下一个 token"。GPT-3 的预训练和我们的代码原理上没有区别,差的是规模:词表 5 万(BPE)、上下文几千、参数 175B、数据 3000 亿 token、几千张卡。而从"会接话的文本补全器"到 ChatGPT 还要两步:SFT(监督微调:用"问题→理想回答"的数据把它掰成助手的样子)和 RLHF(人类反馈强化学习:人给回答排序 → 训练奖励模型 → 用它继续调)。这两步用的数据量远小于预训练,改的是"性格"不是"知识"。你的 1B 计划走的正是这条完整路线,而今天是第一段的起点。
十一、预备问题与自测
看视频前带着这些问题
- 长 9 的整数序列、block_size=8,里面藏着几个训练样本?分别是什么?
- 手算:tril 归一化的 3×3 矩阵乘 [[2,7],[6,4],[6,5]],结果是什么?
- 把 softmax 输入的某些位置填成 -inf,输出对应位置是什么?
- q 和 k 的点积大,语义上说明什么?为什么聚合的是 v 不是 x?
- x = x + f(x) 的 backward,梯度怎么分?(Part 4 原题)
- LN 和 BN 各在哪个维度归一化?哪个有样本耦合?
学完后的自我检测(答不上 3 道以上,回对应小节重看)
| # | 题目 | 要点(小节) |
|---|---|---|
| 1 | "矩阵乘 = 批量加权聚合"——用 toy example 讲给别人听 | 四 |
| 2 | 三版聚合各自的角色,v3 换来了什么接口 | 四 |
| 3 | Q/K/V 的直觉;为什么 key 和 value 要分开 | 五 5.1 |
| 4 | 不除 √head_size 会怎样(含实测数字 16.7 vs 1.0) | 五 5.2 |
| 5 | 六条笔记逐条复述(各一两句) | 六 |
| 6 | 多头的动机与"会计学"(head_size = n_embd/n_head) | 七 7.1 |
| 7 | communicate vs compute 的分工;FFN 为什么 4×、为什么它吃掉 2/3 参数 | 七 7.2 / 八 |
| 8 | 残差为什么救深网络(用加法 backward 回答);Pre-Norm 是什么 | 七 7.3/7.4 |
| 9 | 位置 embedding 为什么必需;generate 为什么要裁剪 idx | 八 |
| 10 | 预训练/SFT/RLHF 各做什么;我们完成的是哪段 | 十 |
| 🎓 | 毕业礼:合上一切,90 分钟从空文件手写完整 GPT 并跑通 | 全部 |
十二、连回 1B 主线
| 本节写的 | 1B 训练里的 |
|---|---|
| GPTLanguageModel(decoder-only + Pre-Norm) | GPT-2 / LLaMA / 你的 1B 的同款骨架,nanoGPT 即其工程化版本 |
| 字符 tokenizer | 下一课:BPE(词表 50k+)——Karpathy 有专门一讲 |
| block_size 与位置表 | context length 扩展(RoPE/ALiBi)的问题源头 |
| wei (T,T) 矩阵的 O(T²) | Flash Attention、KV cache 要优化的正是它 |
| FFN 占 2/3 参数 | MoE 动刀的位置 |
| 预训练/SFT/RLHF 路线图 | 你 1B 项目的完整蓝图 |
完成本节自测(尤其是毕业礼)之后,去读你文件夹里的《CNIT-1B-Base模型训练笔记》和《OLMo踩坑日记》——那两份文档里的每个术语,你现在都亲手写过了。