Mechanics 机制
事物如何运作——Transformer 是什么、模型并行如何利用 GPU
CS336 · 第 1 课
语言模型从字符串开始——如何把文本切分成模型可以理解的单元,是整个流程的第一步。这一课建立对全课的宏观理解,然后深入这个看似简单却影响深远的问题。
Spring 2026 官方来源:Lecture 1 trace · 课程日程
本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。
本讲问题:本讲先回答为什么语言模型必须把文本变成有限符号序列,再比较 character、byte、word 与 BPE 分别保留什么信息、付出什么代价。
原始文本 → token 边界选择 → vocabulary 与 sequence length 权衡 → BPE merge 机制 → tokenizer 如何改变模型输入、计算量与跨语言表现。
八年前,研究者会自己实现和训练模型。六年前,研究者下载 BERT 做 fine-tune。今天,研究者直接调用 GPT-4/Claude/Gemini 的 API。
向上抽象提升了生产力,但语言模型的抽象层和编程语言或操作系统不同——它是泄漏的。不理解底层,就无法做真正的研究。
这门课的核心信念:理解源于构建 (understanding via building)。
不是读论文、不是调 API,而是从第一行代码开始,构建一个语言模型的每一个组件。
GPT-4 据称有 1.8T 参数,训练成本约 1 亿美元。xAI 用 20 万张 H100 训练 Grok。Stargate 项目计划 4 年投资 5000 亿美元。
更关键的是:没有公开细节。GPT-4 的技术报告明确说不会披露架构、训练数据或方法。
前沿模型遥不可及。但课堂上训练的小模型 (<1B 参数) 能代表大模型吗?
例 1:Attention 和 MLP 的 FLOPs 占比随规模变化。小模型训得好的超参数,大模型上不一定最优。
例 2:某些能力是"涌现"的——在小模型上不存在,到一定规模才突然出现。
事物如何运作——Transformer 是什么、模型并行如何利用 GPU
榨干硬件、认真对待规模——scaling laws 的思维方式
哪些数据和建模决策效果好——这部分不能完全教会
这门课能教的是前两者。机制(how things work)和 mindset(efficiency-first)在任何规模都适用。直觉则需要你自己在大规模实验中积累。
The Bitter Lesson(苦涩教训):错误的解读是"规模万能,算法不重要"。正确的解读是"能随规模扩展的算法才是关键"。效率在大规模时更加重要——你承受不起浪费。
公式:accuracy = efficiency × resources
| 年份 | 突破 | 意义 |
|---|---|---|
| 2003 | Bengio 神经语言模型 | 首次用神经网络建模语言 |
| 2014 | Sutskever seq2seq | 序列到序列建模 |
| 2014 | Adam 优化器 | 训练深度网络的基础设施 |
| 2015 | Bahdanau Attention | 注意力机制的诞生 |
| 2017 | Transformer | 架构范式转变 |
GPT-J、OPT、BLOOM、Llama 系列、Mistral、Qwen、DeepSeek、OLMo——开放权重和开放数据的模型生态快速发展。
开放度的三个层次:
这门课围绕一个核心问题:给定固定资源,如何训练最好的模型?
资源 = 数据 + 硬件(算力、内存、通信带宽)。设计决策围绕"榨干硬件"展开:
| 阶段 | 效率驱动的决策 |
|---|---|
| 数据处理 | 避免在低质量数据上浪费算力 |
| Tokenization | 直接用字节优雅但对当前架构计算低效 |
| 模型架构 | 为减少内存/FLOPs 而改动(共享 KV cache、滑动窗口 attention) |
| 训练 | 可以只做 single epoch |
| Scaling Laws | 用小模型做超参搜索,预测大模型最优配置 |
| Alignment | 更精准的调优 → 需要更小的基座模型 |
Tokenizer → Transformer → Training,跑通完整流程
Kernels → Parallelism → Inference,榨干硬件
小规模实验 → 预测大规模最优配置
来源 → 过滤 → 去重,数据决定模型上限
以及最后一个模块:Alignment(SFT → RLHF → DPO → GRPO),让模型真正有用。
本节灵感来自 Andrej Karpathy 的 Tokenization 讲解,强烈推荐观看。
神经网络只能接收有限范围内的整数索引,但人类文本是任意长、包含任意 Unicode 字符的字符串。我们要构造一个确定、可逆、词表大小可控的映射,同时又不让序列长得难以计算。
先修缺口:如果你还会把“字符”“字节”“token”“token ID”混为一谈,先不要记 BPE 算法。下面会从 UTF-8 的最小例子把四者分开。
原始文本通常表示为 Unicode 字符串。语言模型对 token 序列(通常用整数索引表示)放置概率分布。
所以我们需要两个过程:
词表大小 (vocabulary size) 是可能的 token 数量。
Unicode 文本
↓ UTF-8 编码
bytes(唯一的初始单位)
↓ 按已有 merge rule 逐步合并
current segmentation(这一次输入当前怎样切)
↓ 查 vocabulary
token IDs(送进模型的整数)
vocabulary: token → token_id
merge table: adjacent pair → merge rank
vocabulary ≠ current segmentation
token ID ≠ merge rank
后面每一步都只改变其中某些对象。只要能持续回答“现在改的是哪张表、哪条序列”,BPE 就不会再显得神秘。
关键指标:压缩比 (compression ratio)
compression_ratio = num_bytes / num_tokens
即每个 token 对应多少字节。压缩比越高,同样信息用更少的 token 表示,模型处理效率越高。
Unicode 字符串是字符序列。每个字符可通过 ord() 转为码点(整数):
ord("a") = 97
ord("🌍") = 127757
ord("你") = 20320
问题:
若统一使用前面定义的 bytes/token,字符级 tokenizer 的数值取决于 UTF-8:ASCII 字符约为 1 byte/token,而常见中文字符通常约为 3 bytes/token。不能把“1 字符/token”与 bytes/token 当成同一个单位;这里真正的问题是 token 数仍大致随字符数增长。
Prerequisite refresher:Unicode character 是抽象字符;UTF-8 是把字符写成 bytes 的一种编码规则;byte 是 8 个 bit,因此恰有 (2^8=256) 个可能值,即 0 到 255。两者不是同一个对象。
UTF-8 为每个合法 Unicode 字符串规定了可逆的字节表示,所以“先 UTF-8、再按 byte 处理”覆盖任意语言,不需要为中文、emoji 或未来出现的罕见字符准备 UNK:
"a" → b"a" # 1 字节
"🌍" → b"\xf0\x9f\x8c\x8d" # 4 字节
"你" → b"\xe4\xbd\xa0" # 3 字节
优势:初始词表极小且固定——只有 256 个可能的 byte。注意,“你”是一个 Unicode character,却是三个 byte;byte-level BPE 的初始序列是三个单位,不是一个“中文字 token”。
劣势:压缩比 = 1 字节/token,压缩效果极差。序列会非常长。
为什么序列长度重要? Transformer 的 attention 是 O(n²) 复杂度。序列越长,计算和内存开销越大。上下文长度受限于序列长度,所以极低的压缩比意味着模型能"看到"的有效内容更少。
Exact identityUTF-8 编码后,每个单位一定是 0–255 中的一个值;把 bytes 原样拼回并按 UTF-8 解码,可以恢复原文本。
Asymptotic complexity标准 dense self-attention 对 token 数 (n) 的时间与 attention-score 内存是 (O(n^2))。这不等于整个 Transformer 的所有计算都严格等于 (n^2),但说明过长的 byte 序列代价很高。
经典 NLP 的做法:把文本按词分割。
import regex
pattern = r"\w+|."
segments = regex.findall(pattern, "I'll say supercalifragilisticexpialidocious!")
# → ["I", "'", "ll", " ", "say", " ", "supercalifragilisticexpialidocious", "!"]
更精细的版本(GPT-2 使用的预分词正则):
GPT2_TOKENIZER_REGEX = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
问题:
UNK token——这既丑陋,又会扰乱 perplexity 计算词级 tokenization 往往能得到更高的 bytes/token,但具体数值取决于语言、UTF-8 byte 长度和语料中的词长分布;不能把“约 4 字符/token”直接塞进 bytes/token 口径。它更根本的问题仍是开放词表、UNK 与巨大 vocabulary。
BPE 最初由 Philip Gage(1994)用于数据压缩,Sennrich et al.(2016)把它引入 subword NLP。它试图同时保留 byte 的完备性与较长 token 的压缩效率:从唯一的 byte 分割出发,只学习数据中反复出现的相邻组合。
| 对象 | 它回答的问题 | 示例 |
|---|---|---|
| bytes | 不可再假设更小的初始单位是什么? | 97, 98, 99 |
| vocabulary | 哪些 token 存在、各自 ID 是什么? | ab → 256 |
| current segmentation | 当前 corpus/input 此刻怎样切? | [ab][c] |
| merge table | 哪对相邻 token 可以合并、优先级是什么? | (a,b) → rank 0 |
| token IDs | 最终 token 用什么整数交给模型? | [256, 99] |
初始 corpus segmentation:
[a][b][c]
[a][b][c]
[a][b][d]
初始 vocabulary 至少有:
a, b, c, d
执行 a + b → ab 后,corpus segmentation:
[ab][c]
[ab][c]
[ab][d]
新的 vocabulary:
a, b, c, d, ab
合并改写的是训练 corpus 的当前切分,并把 ab 加进词表;它没有删除 a 或 b。未来输入里如果没有合适的 merge,仍可能产生单独的 a、b。
训练输入不是“一个大字符串直接开合并”。常见实现先做 pre-tokenization,确定不允许跨越的边界,再把每个 piece 变成 UTF-8 bytes。然后每一轮都在当前 segmentation上重新计数。
raw text
↓ pre-tokenization(例如不跨特殊 token 或某些空白边界)
UTF-8 bytes
↓ 每个 byte 作为初始 token
current corpus segmentation
↓ 重复:计数 → 选 pair → 新 token → 记录 rank → 改 segmentation
vocabulary + ordered merge table
为便于看清对象,用字母代替相应单字节 token:
初始 segmentation:
a b c
a b c
a b d
Round 1 pair frequency:
(a,b): 3
(b,c): 2
(b,d): 1
选择 a+b → ab
vocabulary 新增 ab
merge table 记录 rank 0: (a,b) → ab
更新后的 segmentation:
ab c
ab c
ab d
Round 2 pair frequency:
(ab,c): 2
(ab,d): 1
选择 ab+c → abc
vocabulary 新增 abc
merge table 记录 rank 1: (ab,c) → abc
rank 不是凭空定义的。它就是规则在训练中被学到的次序:rank 0 最早,rank 1 次之。若最高频率并列,训练 specification 必须规定 deterministic tie-break(例如比较 pair 的 byte 序);否则同一 corpus 在不同运行中可能学到不同 tokenizer。
Training 会做的事:统计 frequency;创建新 token;扩展 vocabulary;修改训练 corpus segmentation;按学习次序产生 merge rank。
训练停止条件:达到目标词表大小、达到 merge 次数,或已没有可合并 pair。若有 256 个基础 byte 且没有额外 special tokens,做 \(M\) 次有效 merge 后词表大小是 \(256+M\)。这是 Exact identity,不是经验近似。
现在假设训练已经结束,得到:
rank 0: b + c → bc
rank 1: a + b → ab
rank 2: a + bc → abc
编码输入 abc。先转 UTF-8 bytes,初始 segmentation 唯一为 [a][b][c]。此时两对都能合并:
(a,b) 有规则,rank 1
(b,c) 有规则,rank 0
选当前可执行规则中 rank 最小者:
[a][b][c] → [a][bc]
重新查看新的相邻 pair:
(a,bc) 有规则,rank 2
[a][bc] → [abc]
重点不是“先把 rank 0 的规则在整段文字上扫完,再扫 rank 1”,而是:每一步查看当前 segmentation 中所有可执行 merge,选择 rank 最小的一项,然后更新邻接关系。高效实现会用 priority queue,但语义必须与这个过程相同。
Encoding 不会做的事:不重新统计输入 frequency;不创建 token;不改变 vocabulary;不产生新 rank。它只消费训练阶段冻结的 vocabulary 与 merge table。
假设 vocabulary 同时有 a, b, c, ab, bc, abc。若允许“从任意已有 token 开始”,同一输入一开始就有四种合法状态:
[a][b][c] [ab][c] [a][bc] [abc]
算法还没执行就已经不确定。byte 序列则由 UTF-8 唯一决定:
唯一初始 bytes + 冻结的 merge ranks + deterministic tie handling
→ 唯一最终 segmentation
所以“从 bytes 开始”不只是因为 byte 小,而是为了给编码算法一个规范、唯一的初始状态。
Vocabulary 只声明 token 存在,不声明该字符串应该怎样切。下面的反例让两种算法产生不同结果:
vocabulary:
a, b, c, d, ab, abc, bc, bcd
merge rules:
rank 0: b+c → bc
rank 1: bc+d → bcd
rank 2: a+b → ab
rank 3: ab+c → abc
输入 abcd,BPE:
[a][b][c][d]
→ [a][bc][d] # rank 0
→ [a][bcd] # rank 1
longest-match 可能得到:
[abc][d]
两者都只使用 vocabulary 中存在的 token,却得到不同 segmentation。因此 merge ranks 定义 BPE 的切分;“词表中有最长 token”不是选择规则。
vocabulary:
abc → token_id 261 # 模型 embedding lookup 使用
merge table:
(ab,c) → merge rank 1 # tokenizer 决定合并顺序使用
token ID 可以按实现任意分配;merge rank 必须保留训练顺序。某些实现恰好让后加 token 的 ID 与 merge 顺序相关,这只是编号约定,不是概念同一。
给定 token IDs,先在 vocabulary 中查到每个 token 对应的 byte string,按顺序拼接,再做 UTF-8 decode。Decoding 不需要 merge table,因为切分已经由 ID 边界给出。单个 token 的 bytes 甚至可能不是独立合法的 UTF-8;只要拼接后的完整 byte stream 合法即可。
| BPE | Huffman | |
|---|---|---|
| 贪心信号 | 最高频的相邻 pair | 最低频的两个 node |
| 每步产物 | 更长的 token 与一条 merge rule | prefix-code tree 的父节点 |
| 最终目标 | 学习 subword vocabulary 与确定切分 | 给符号分配无前缀歧义的变长 bit code |
共同点只是“由 frequency 驱动的 greedy construction”;不能据此认为 BPE 也在构造 prefix code,或 Huffman 也在合并文本中的相邻片段。
1. “merge 后旧 token 消失。”错。旧 token 留在 vocabulary;改变的是当前 segmentation。
2. “encoding 会统计当前输入的 pair frequency。”错。即使输入里 (a,b) 出现一万次,也不能越过 rank 更小的可执行规则。
3. “merge rank 就是 token ID。”错。一个映射 pair 的优先级,一个索引 token。
4. “vocabulary 越大,sequence 越长。”通常相反:更多可用 merge 往往提高压缩率、缩短序列;但 embedding/output matrix 也随 vocabulary 变大,且具体语料上的长度不保证单调改善。
5. “只要 vocab 有长 token 就直接用。”longest-match 反例已经证明错误;必须由 merge ranks 走到它。
6. “BPE 就是 Huffman。”两者的贪心方向、对象与输出结构都不同。
原始的 BPE 直接在字节上运行。GPT-2 做了两个关键改动:
[cat] [!],边界两侧的 t 与 ! 就不是合法候选 pair。<|endoftext|>,不参与 BPE 合并,始终作为一个整体。| 方法 | 词表大小 | 压缩比 | OOV 问题 | 无损 |
|---|---|---|---|---|
| Character | ~150K | ~1.0 | 无 | 是 |
| Byte | 256 | ~1.0 | 无 | 是 |
| Word | 无限 | ~4.0 | 有 (UNK) | 否 |
| BPE | 可控 | ~3-4.0 | 无 (fallback to bytes) | 是 |
原版 Transformer 论文用的是 BPE。今天的主流模型(GPT 系列、Llama、Mistral)都用 BPE 或其变体。
但 BPE 也有问题:
未来方向:Tokenizer-free
直接用字节作为输入(ByT5、MEGABYTE、BLT、TFree),让模型自己学组合。理论上更优雅,但目前尚未扩展到前沿规模。这是一个开放问题。
问题 1:UTF-8 编码中,一个中文字符"你"占多少字节?
a b c规则为 rank 0: b+c→bc,rank 1: a+b→ab,rank 2: a+bc→abc。最终 segmentation 是什么?
答案:[abc]。初始虽然 (a,b) 和 (b,c) 都可合并,但先选 rank 0 得到 [a][bc],再用 rank 2。
如果你先得到 [ab][c],你把“从左到右”当成了优先级;如果你重新数 pair 次数,你把 training 与 encoding 混在了一起。
训练时执行 a+b→ab。在 vocabulary、current segmentation、merge table 三者中,哪些改变?a、b 是否被删除?
答案:三者都改变:vocabulary 新增 ab;当前 corpus 中匹配位置变成 [ab];merge table 新增一条带 rank 的规则。a、b 不删除。
若你认为旧 token 被删除,说明你把“当前怎么切”误当成了“系统允许哪些 token 存在”。
答案:只要输入是合法 UTF-8,它总能退回对应的 1–4 个基础 byte tokens;已有 merge 可能进一步合并其中某些相邻 bytes,但编码不会创建新 token。
若你回答 UNK,说明你仍在用 word-level vocabulary 的 mental model 理解 byte-level BPE。
问题 2:为什么 byte-level tokenization 的压缩比差是个问题?
问题 3:如果 BPE 训练做了 100 次合并,最终词表大小是多少?
a/b 仍留在 vocabulary。abc 不代表 encoder 会 longest-match 到它。Tokenizer 把同一段文本变成 \(D\) 个 training tokens;序列更长会改变 attention 工作量与 activation memory。下一课会把 token 数、参数量、dtype、FLOPs 和显存逐项换成可以手算的资源账本。