# Lecture 1 · 概览与分词

> **CS336: Language Modeling from Scratch** · Stanford · Spring 2025
> 📅 Apr 1 · 💻 [可执行讲义](https://github.com/stanford-cs336/spring2025-lectures/blob/main/lecture_01.py) · 讲者 Percy Liang

---

## 承上启下

- **与 CS221 的衔接**：CS221 的 [[Lecture 17 · 语言模型]] 从建模视角回答了「语言模型是什么」——对 token 序列的概率分布 $p(x_1, \dots, x_T)$，以及从 n-gram 到神经网络的演化。CS336 则回答工程问题：这个分布在工业规模上究竟是怎么被造出来的——数据、分词、架构、系统、对齐，每一层都亲手实现。
- **本讲**：课程全景（为什么要 from scratch、效率这条主线、五个单元与五次作业），然后进入整条流水线的第一块积木——**分词（tokenization）**，重点是 BPE 算法的训练、编码与解码。
- **下一讲** [[Lecture 2 · PyTorch 与资源核算]]：从 tensor 开始搭建训练语言模型的最小积木，并学会用 napkin math 估算内存与算力。

---

## 课程定位

**这门课是什么**：

- 一门**从零构建语言模型**的课程——不调 API、不用 Hugging Face 的现成组件，而是亲手写数据处理、分词器、Transformer、训练循环、分布式并行，直到对齐。
- 目标是**理解**而非生产力——生产环境当然应该用现成的库，但只有把每个细节实现一遍，才能弄懂整套技术栈为什么长成这样。

**为什么要 from scratch**：

- 研究者正在与底层技术**脱节**：8 年前研究者自己写模型训练代码，6 年前下载 BERT 再微调，今天很多研究只是 prompt GPT-4——离底层越来越远。
- 抽象层能提升生产力，但语言模型的抽象是**漏的（leaky abstraction）**：分词方式会影响算术能力，数据配比会影响下游行为，学习率会和 batch size 纠缠——这些底层细节会穿透 API 边界影响上层结果。这和编程语言、操作系统那种「几乎不漏」的抽象不同，所以要做基础研究，必须撕开这堆抽象。
- **理解 = 动手建造**（"What I cannot create, I do not understand" —— Feynman）。

> [!tip] 直觉
> 为什么「漏的抽象」是选修这门课的核心理由？如果抽象不漏（像 TCP/IP 之于网页开发），上层研究者确实不需要懂底层。但语言模型的每个「实现细节」——分词粒度、数据去重、优化器数值稳定性——都会以意想不到的方式改变模型行为。你无法只靠调 API 定位这些问题，正如无法只靠开车学会修发动机。

**知识的三个层次（哪些能从小模型迁移到大模型）**：

| 层次 | 例子 | 可迁移性 |
| :--- | :--- | :--- |
| **机制（mechanics）** | Transformer 怎么算、并行怎么切 | 完全可迁移 |
| **心态（mindset）** | 榨干硬件、认真对待规模 | 完全可迁移 |
| **直觉（intuitions）** | 哪种数据配比好、SwiGLU 是否有用 | 只能部分迁移 |

这个三分法解释了「课上只训 <1B 小模型」为什么仍然有价值：机制和心态在任何规模下都成立；但直觉类结论（比如某个架构 trick 在 1B 有效）**不保证**在 100B 下仍然成立——这正是「涌现行为随规模变化」带来的根本限制。

**和其他课程的区别**：

- **CS224n**（自然语言处理）：覆盖 NLP 各种任务（翻译、QA、摘要），模型训练只是其中一部分。
- **CS336**：专注预训练语言模型本身，深入数据 / 系统 / 缩放 / 对齐全栈——**一次作业的工作量约等于 CS224n 五次作业加 final project 的总和**（Percy 原话，针对作业 1）。

**规模与现实的落差**：

- GPT-4 据传有 1.8T 参数、训练成本约 \$100M；xAI 用 20 万张 H100 的集群训练 Grok；OpenAI/NVIDIA/Oracle 的 Stargate 项目计划 4 年投入 \$500B。
- 这门课训练的是 <1B 参数的小模型——但如上所述，**机制和心态可迁移**，直觉只能部分迁移。

**效率是主旋律**：

$$\text{accuracy} = \text{efficiency} \times \text{resources}$$

符号含义：模型最终的能力（accuracy）由两个因子决定——资源（算力、数据，通常是给定的预算）和效率（单位资源能换来多少能力）。资源无法轻易改变，所以这门课的核心问题被定义为：**给定固定的算力与数据预算，如何训出最好的模型**。

- 对 bitter lesson 的正确解读**不是**「规模就是一切、算法不重要」，而是「**能随规模扩展的算法**才是一切」——scale 与算法效率相乘才是最终能力。
- 佐证：OpenAI 的研究显示，达到 AlexNet 水平的 ImageNet 精度所需算力在 2012–2019 年间下降了 **44 倍**——算法效率的进步与摩尔定律同样惊人。

**效率如何驱动全课的设计决策**（今天我们是 compute-constrained 的，所以每个环节都围绕效率展开）：

- **数据处理**：不要把宝贵的梯度更新浪费在低质量、重复的数据上——这是 [[Lecture 13 · 数据 I]] 的主题。
- **分词**：直接吃原始字节更优雅，但目前计算上不划算——这正是本讲 BPE 存在的理由。
- **架构**：大量改动都是为了省内存或省 FLOPs（KV cache 共享、滑动窗口注意力等），见 [[Lecture 3 · 架构与超参数]]。
- **训练**：单个 epoch 就够——多看新数据比反复看旧数据更划算。
- **缩放定律**：在小模型上调超参数，外推到大模型，避免在大模型上试错，见 [[Lecture 9 · 缩放定律 I：基础]]。
- **对齐**：如果模型可以事后对齐到具体用途，基础模型本身可以更小更通用，见 [[Lecture 15 · 对齐 I：SFT 与 RLHF]]。

**课程结构（五个单元 + 五次作业）**：

| 单元 | 主题 | 作业 |
| :---: | :--- | :--- |
| **1. Basics** | Transformer、优化器、训练循环 | A1：BPE 分词器 + Transformer + AdamW |
| **2. Systems** | GPU、kernel、并行 | A2：Triton kernel + 数据并行 + 优化器状态分片 |
| **3. Scaling** | 缩放定律、推理优化 | A3：拟合缩放定律、预测最优超参数 |
| **4. Data** | 数据源、去重、过滤 | A4：从 Common Crawl 构建预训练数据 |
| **5. Alignment** | SFT、DPO、RL | A5：SFT + DPO + GRPO |

**程序语言的类比**：

```mermaid
graph LR
    L["自然语言<br/>(Language)"] --> T["分词<br/>(Tokenization)"]
    T --> TT["token 序列"]
    TT --> LM["语言模型<br/>(Language Model)"]
    LM --> D["分布<br/>P(next token)"]
    
    P["程序<br/>(Program)"] --> LEX["词法分析<br/>(Lexing)"]
    LEX --> TOK["token 序列"]
    TOK --> PAR["解析<br/>(Parsing)"]
    PAR --> AST["抽象语法树<br/>(AST)"]
    
    style L fill:#e1f5fe,stroke:#0277bd
    style P fill:#e1f5fe,stroke:#0277bd
    style T fill:#ffe0b2,stroke:#e65100
    style LEX fill:#ffe0b2,stroke:#e65100
    style LM fill:#c8e6c9,stroke:#2e7d32
    style PAR fill:#c8e6c9,stroke:#2e7d32
```

自然语言与程序语言都是结构化的字符序列——都需要先**分词**，再交给模型 / 解析器处理。但语言模型只学到分布，不像解析器那样显式构建语法树。

---

## 1. 分词：把文本变成模型能吃的东西

### 1.1 为什么需要分词

语言模型的输入和输出都是**离散的 token 序列**——模型不直接吃字符串，必须先把文本切成一个个「单位」，再映射为整数 ID。分词器（tokenizer）就是这个双向映射：`encode: str → list[int]`，`decode: list[int] → str`。

衡量分词器好坏的核心指标是**压缩率（compression ratio）**：

$$\text{compression ratio} = \frac{\text{UTF-8 字节数}}{\text{token 数}}$$

即平均每个 token 「吃掉」多少字节。压缩率越高，同一段文本的序列越短——训练和推理的每一步都在为序列长度付费，所以压缩率直接就是钱。

**四种粒度的选择**（按讲义顺序：字符 → 字节 → 词 → 子词）：

| 粒度 | 词表大小 | 压缩率（字节/token） | 致命缺陷 |
| :--- | :---: | :---: | :--- |
| **字符级**（Unicode 码点） | ~15 万 | ≈1（英文） | 词表巨大且稀有字符（如生僻 emoji）的 token 训练严重不足 |
| **字节级** | 256 | 恒为 1 | 序列太长——上下文窗口和注意力的开销都按 token 数计费 |
| **词级** | 无上界（数十万起） | 高 | OOV 问题：没见过的词只能映射到 `UNK`，污染困惑度 |
| **子词级（BPE）** | 数万–十几万（可控） | 英文约 4 | 需要额外训练一个分词器；对数字、拼写类任务不友好 |

> [!warning] 易错点
> 字符级和字节级不是一回事。字符级以 Unicode 码点为单位，词表约 15 万（Unicode 收录的字符数）；字节级以 UTF-8 字节为单位，词表恰好 256。前者的问题是词表大且长尾稀疏，后者的问题是压缩率恒为 1、序列过长。BPE 之所以从**字节**（而非字符）出发，正是要同时避开这两个坑。

> [!note]
> 现代语言模型几乎都用子词分词——BPE（Byte-Pair Encoding）是最流行的选择，GPT / LLaMA / Qwen 都用它（LLaMA 1/2 用的是 SentencePiece 实现的 BPE 变体）。

> [!tip] 直觉
> 子词分词的妙处在于「频率决定粒度」：高频词（"the"、"ing"）合并成单个 token 享受高压缩率，低频词（人名、错别字）自动退化为更细的子词甚至字节——**常见的东西粗粒度处理，罕见的东西细粒度兜底**。这让词表大小、序列长度、开放词表三者同时可控，是一种数据驱动的自适应折中。

### 1.2 从 UTF-8 字节到 token

**UTF-8 编码**是一个通用的「字符 → 字节序列」映射——任何 Unicode 字符都能表示成 1–4 个字节：

```python
# 英文字母 1 字节，中文 3 字节，emoji 4 字节
"hello".encode("utf-8")   # b'hello'        (5 字节)
"你好".encode("utf-8")     # b'\xe4\xbd\xa0\xe5\xa5\xbd'  (6 字节)
"🌍".encode("utf-8")       # b'\xf0\x9f\x8c\x8d'  (4 字节)
```

BPE 的起点是**字节序列**——任何文本先转成 UTF-8 字节，保证词表是**字节安全的（byte-level fallback）**：即使遇到训练时没见过的字符，也能退化到单字节 token 表示。

> [!note]
> 字节级 BPE 保证了**真正的开放词表**——不需要 `UNK` token，任何文本都能被编码；只要 token 序列合法，也总能解码回原文本。

> [!warning] 易错点
> 解码方向有个陷阱：**任意**的 token ID 序列拼出的字节串不一定是合法 UTF-8（比如恰好截断在一个多字节字符中间）。模型自由生成时可能产生这种序列，所以实际实现中 `decode` 要用 `bytes.decode("utf-8", errors="replace")`，把非法片段替换成 U+FFFD（�），而不是直接抛异常。

### 1.3 BPE 训练算法：贪心合并高频 pair

BPE（Byte-Pair Encoding）1994 年由 Philip Gage 提出，本来是个数据压缩算法；Sennrich et al.（2016）把它引入 NLP 做机器翻译的子词切分；GPT-2 把它扩展到字节级并推广开来。核心思想：从 256 个单字节 token 开始，**迭代地把语料中最常见的相邻 token 对（pair）合并成一个新 token**——分词器不是人工设计的，而是从数据里「训练」出来的。

**算法伪代码**：

```python
# 输入：训练语料 string，合并次数 num_merges
# 输出：词表 vocab（id → bytes）、合并规则 merges（pair → new_id）

indices = list(string.encode("utf-8"))  # 初始：每个字节是一个 token (0–255)
vocab = {i: bytes([i]) for i in range(256)}  # 256 个单字节 token
merges = {}

for i in range(num_merges):
    # 1. 统计所有相邻 pair 的出现次数
    counts = {}
    for (a, b) in zip(indices, indices[1:]):
        counts[(a, b)] = counts.get((a, b), 0) + 1
    
    # 2. 找出频率最高的 pair
    best_pair = max(counts, key=counts.get)
    
    # 3. 分配新 token ID，更新词表和合并规则
    new_id = 256 + i
    merges[best_pair] = new_id
    vocab[new_id] = vocab[best_pair[0]] + vocab[best_pair[1]]
    
    # 4. 在当前序列中把这个 pair 全部替换成新 token
    indices = merge(indices, best_pair, new_id)
```

> [!example] 手算一遍：对 `aaabdaaabac` 做 3 次合并
> 初始字节序列（a=97, b=98, c=99, d=100）：
> `[97, 97, 97, 98, 100, 97, 97, 97, 98, 97, 99]`（11 个 token）
>
> **迭代 1**：pair 计数中 `(97,97)` 出现 4 次最高 → 合并为 256（`b'aa'`）。从左到右贪心替换（`aaa` 变成 `[256, 97]`）：
> `[256, 97, 98, 100, 256, 97, 98, 97, 99]`
>
> **迭代 2**：`(256,97)` 和 `(97,98)` 各出现 2 次打平，本实现取先出现者 `(256,97)` → 合并为 257（`b'aaa'`）：
> `[257, 98, 100, 257, 98, 97, 99]`
>
> **迭代 3**：`(257,98)` 出现 2 次最高 → 合并为 258（`b'aaab'`）：
> `[258, 100, 258, 97, 99]`（5 个 token）
>
> 压缩率从 1.0（11 字节 11 token）提升到 2.2（11 字节 5 token）——高频模式 `aaab` 被「压」成了一个 token。

> [!warning] 易错点
> - **贪心 ≠ 最优**：BPE 每一步只看当前最高频 pair，不保证全局最优的词表。但它实现简单、训练快，实践效果好。
> - **平局处理是实现细节**：多个 pair 频率相同时选哪个（插入顺序、字典序）会影响最终词表——想复现 GPT-2 的分词器，必须匹配它的平局规则。

**训练复杂度与加速**：

- 朴素实现每次迭代都要扫一遍全序列统计 pair 再替换，各是 $O(N)$；共 $M$ 次合并（$M$ = 词表大小 − 256，通常数万），总计 $O(MN)$——在 GB 级语料上不可接受。
- 作业 1 的关键优化：合并 `(a,b)` 只会改变**它两侧相邻的 pair** 的计数，所以可以增量更新计数而不必全量重扫；配合预分词（见 1.5），还能把计数从「逐 token」变成「逐词型 × 词频」，语料再大也只需处理去重后的词型集合。

### 1.4 BPE 编码与解码

训练完成后，我们得到两张表：

1. **vocab**：`{token_id: bytes}` — 每个 token 对应的字节序列
2. **merges**：`{(id1, id2): merged_id}` — 哪些 pair 可以合并、合并成什么

**编码（text → token IDs）**：

```python
def encode(string: str) -> list[int]:
    indices = list(string.encode("utf-8"))  # 初始：字节序列
    
    # 按训练时的顺序，依次应用所有合并规则
    for (pair, new_id) in merges.items():
        indices = merge(indices, pair, new_id)
    
    return indices
```

> [!warning] 易错点
> **合并顺序必须与训练时一致**（从早到晚）。因为后期的合并规则引用的是前期合并产生的新 token ID——例如规则 `(256, 97) → 257` 只有在 `(97, 97) → 256` 先被应用后才可能匹配上。乱序应用会得到与训练分布不一致的切分。

**解码（token IDs → text）**：

```python
def decode(indices: list[int]) -> str:
    byte_seq = b''.join(vocab[i] for i in indices)  # 拼回字节序列
    return byte_seq.decode("utf-8", errors="replace")  # 非法字节替换为 U+FFFD
```

**作业 1 的优化方向**：

- 上面的 `encode()` 会遍历**全部**几万条合并规则，即使输入只有几个词——只考察输入中实际出现的 pair、用优先队列按规则序取最早可用的合并，可以快几个数量级。
- 支持**特殊 token**（如 `<|endoftext|>`）：训练语料按文档边界插入，编码时整体识别、绝不被 BPE 切开。
- 使用**预分词正则**（GPT-2 的 regex，见 1.5）：先把文本按语言边界切块，再对每块内部做 BPE，避免产生跨词合并。
- **性能**：预分词与计数可以并行化（多进程 map-reduce），这是在 OpenWebText 规模语料上训练分词器的前提。

### 1.5 真实分词器的复杂度

**GPT-2 的预分词正则**（出自讲义）：

```python
pattern = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
```

这条正则把文本先切成几类片段，BPE 只在片段内部做合并：

- `'(?:[sdmt]|ll|ve|re)`：英文缩写后缀（`'s`、`'t`、`'ll`、`'ve`…），单独成段以保持一致切分；
- `?\p{L}+`：连续字母（可带一个前导空格）——空格被吸附到后面的词上；
- `?\p{N}+`：连续数字（可带前导空格）——注意字母和数字被强制分开；
- `?[^\s\p{L}\p{N}]+`：连续标点 / 符号；
- `\s+(?!\S)` 与 `\s+`：处理尾部与连续空白。

先切块、再对每块内部做 BPE——**防止合并跨越词边界**（否则可能出现 `e t` 这种横跨两个词的高频 token），保留一定语言结构。

**特殊 token**：

- `<|endoftext|>`：文档分隔符（GPT 系列）——它让模型知道「上文到此为止」，也让训练数据能把多篇文档拼接进同一个 batch。
- `<|begin_of_text|>`、`<|start_header_id|>` 等（LLaMA 3）：对话格式的结构标记。
- 这些 token 在词表中占位、编码时整体识别、不参与 BPE 合并。

**词表大小的选择**：

| 分词器 | 词表大小 | 备注 |
| :--- | ---: | :--- |
| GPT-2 / GPT-3 | 50,257 | = 256 字节 + 50,000 次合并 + 1 个 `<\|endoftext\|>` |
| LLaMA 1/2 | 32,000 | SentencePiece BPE |
| LLaMA 3 | 128,256 | 词表扩大以改善多语言压缩率 |
| Qwen 2.5 | 151,643 | 多语言词表更大 |

> [!tip] 直觉
> 词表大小是一场三方拉锯：词表越大 → 压缩率越高、序列越短（同样上下文窗口装下更多内容、每个文本训练/推理更便宜）；但嵌入表和输出 softmax 也随 $V$ 线性变大，且长尾 token 出现次数太少、学不充分。5 万–15 万是当前的经验甜点区，多语言模型偏向更大词表——因为要对多种文字都维持可接受的压缩率。

### 1.6 观察真实分词器的行为（tiktokenizer 演示）

用 [Tiktokenizer](https://tiktokenizer.vercel.app/) 观察 GPT-2 分词器，有几个值得记住的现象：

- **空格属于后面的词**：`" world"`（带前导空格）是一个 token，和 `"world"` 是**不同**的 token——这是预分词正则 `?\p{L}+` 的直接后果。
- **同一个词在不同位置切分不同**：句首的 `"Hello"` 和句中的 `" Hello"` ID 不同；大小写变体也各自独立。模型必须分别学习它们的语义，这是子词方案的隐性代价。
- **长数字被切成不规则片段**：GPT-2 的 BPE 会把 `1234567` 切成任意学到的数字片段；GPT-4 的 cl100k 则强制数字最多三位一组。数字切分的不一致是 LLM 算术能力差的原因之一。
- **压缩率因语言而异**：英文约 4 字节/token，而 GPT-2 时代的中文常常一个汉字要 2–3 个 token——对非拉丁语言既贵又低效，这推动了后来多语言词表的扩张。

**tokenization-free 方法**：ByT5、MegaByte、BLT（Byte Latent Transformer）等工作尝试直接建模字节、彻底绕开分词器——更优雅，也消除了上述所有怪癖。但截至目前，**没有任何 frontier 模型采用**：字节序列比 BPE 序列长约 4 倍，而注意力的代价是 $O(T^2)$，压缩率的损失在大规模下太昂贵。这是「效率驱动设计决策」的又一例证。

---

## 2. 课程工作量与期望

### 2.1 五次作业在做什么

**作业 1（Basics：分词 + Transformer）**：

- 实现完整的 BPE 分词器（训练 / 编码 / 解码 / 特殊 token / 正则预分词），并在真实语料上高效训练；
- 从零实现 Transformer（多头注意力、RMSNorm、SwiGLU、RoPE）、交叉熵损失与 AdamW 优化器——只允许用 PyTorch 的张量原语，不允许用 `nn.Transformer` 之类的现成模块；
- 排行榜：在 **90 分钟 H100 预算内**在 OpenWebText 上把困惑度压到最低——从第一次作业开始就在训练「效率心态」。

**作业 2（Systems：kernel 与并行）**：

- 用 Triton 手写 RMSNorm 等 kernel，对比 PyTorch 原生实现的性能；
- 实现分布式数据并行训练与**优化器状态分片**（ZeRO 思想的核心）；
- 系统性地做 benchmark 与 profiling。流水线 / 张量并行等更复杂的策略在 [[Lecture 7 · 并行 I：基础]] 与 [[Lecture 8 · 并行 II：分布式训练实战]] 中讲授。

**作业 3（Scaling：缩放定律）**：

- 通过课程提供的「训练 API」提交小模型训练任务（隔离了真实算力消耗）；
- 在固定 FLOPs 预算内设计实验、拟合缩放定律，**预测**更大规模下的最优超参数与损失——体验「用小模型的实验外推大模型的决策」这一工业界核心工作流。

**作业 4（Data：数据）**：

- 把 Common Crawl 的原始 HTML 转成干净文本；
- 训练质量分类器与有害内容分类器做过滤；
- 用 MinHash 做模糊去重；排行榜：固定 token 预算下的困惑度——数据质量直接决定模型质量，见 [[Lecture 13 · 数据 I]]。

**作业 5（Alignment：对齐）**：

- 监督微调（SFT）：少量高质量示例即可激活基座模型已有的能力（LIMA 的核心论点）；
- 实现 DPO（直接偏好优化）与 GRPO——对齐算法的两条主线分别在 [[Lecture 15 · 对齐 I：SFT 与 RLHF]] 与 [[Lecture 16 · 对齐 II：RLVR]] 展开。

> [!warning] 易错点
> 这门课很硬核——Percy 在第一讲反复警告：作业 1 的工作量就抵得上 CS224n 全部五次作业加 final project。不要因为「分词器听起来简单」而低估它：在大语料上高效训练 BPE 需要认真的算法与工程优化。

### 2.2 心态：效率优先，全栈思维

```mermaid
graph TD
    A["效率<br/>(Efficiency)"] --> B["算法效率<br/>FLOPs per token"]
    A --> C["系统效率<br/>MFU / memory bandwidth"]
    A --> D["数据效率<br/>token 质量 / 去重"]
    
    B --> E["更好的架构<br/>(Transformer变体)"]
    B --> F["更好的优化器<br/>(AdamW / Muon)"]
    
    C --> G["kernel 融合<br/>(Flash Attention)"]
    C --> H["并行策略<br/>(3D parallelism)"]
    
    D --> I["数据过滤<br/>(质量筛选)"]
    D --> J["合成数据<br/>(蒸馏 / 自生成)"]
    
    style A fill:#ffe0b2,stroke:#e65100
    style B fill:#e1f5fe,stroke:#0277bd
    style C fill:#e1f5fe,stroke:#0277bd
    style D fill:#e1f5fe,stroke:#0277bd
```

**全栈 = 不只写模型代码**：

- 懂硬件：GPU 架构、内存层次、tensor core——见 [[Lecture 5 · GPU]]；
- 懂系统：CUDA kernel、通信原语、分布式协议；
- 懂算法：缩放定律、优化器、架构设计；
- 懂数据：去重、过滤、质量评估。

> [!tip] 直觉
> 这门课的目标不是让你训出 SOTA 模型，而是让你在看到 DeepSeek / LLaMA 的技术报告时，能读懂每一行、知道每个决策背后的资源账。「懂技术报告」这个能力本身，就是通过亲手实现每一层换来的。

---

## 3. 当前格局：开放模型崛起

### 3.1 工业化的语言模型

讲义开头展示了一组令人震撼的数字：

| 模型 | 参数量 | 训练数据 | 训练成本 / 规模 |
| :--- | :---: | :---: | :--- |
| GPT-4 | ~1.8T（传闻） | 未公开 | ~\$100M（传闻） |
| LLaMA 3.1 | 405B | 15.6T tokens | 3.8×10²⁵ FLOPs，约 1.6 万张 H100 |
| Qwen 2.5 | 72B | 18T tokens | 未公开 |
| DeepSeek-V3 | 671B（37B 激活） | 14.8T tokens | 278.8 万 H100·小时 ≈ \$560 万 |

> [!note] 6ND 法则
> 训练一个 $N$ 参数、$D$ token 的稠密 Transformer，总浮点运算量约为
> $$C \approx 6ND$$
> 其中 $C$ 是训练 FLOPs，$N$ 是参数量，$D$ 是训练 token 数；系数 6 = 前向 2 + 反向 4（推导见 [[Lecture 2 · PyTorch 与资源核算]]）。代入 LLaMA 3.1：$6 \times 405\times10^9 \times 15.6\times10^{12} \approx 3.8\times10^{25}$，与 Meta 实报吻合。
>
> 顺带一提 Chinchilla 的**计算最优**配比 $D^\ast \approx 20N$：LLaMA 3.1 用了约 $38N$ 的 token，是刻意「过训练」——多花训练算力换更小的模型，摊薄推理成本。缩放定律的完整推导见 [[Lecture 9 · 缩放定律 I：基础]]。

**frontier models 的不透明**：GPT-4 技术报告明确说「考虑到竞争格局和安全影响，本报告不包含架构、硬件、训练算力、数据集构建、训练方法的进一步细节」。

### 3.2 开放模型的价值

开放程度是一个光谱，不是非黑即白：

- **OLMo**（AI2 / Allen Institute）：**完全开放**——权重、训练代码、数据、中间 checkpoint 全公开，是唯一能端到端复现的选项；
- **LLaMA**（Meta）：权重开放、技术报告详细，但训练数据不公开；
- **Qwen**（阿里巴巴）：权重开放、报告详细；
- **DeepSeek**：权重开放，MoE 架构与训练基础设施的细节透明度在开放模型中数一数二（架构详见 [[Lecture 4 · 混合专家模型 MoE]]）。

> [!note]
> 开放模型是研究的基石——只有看到完整的训练过程、数据配比、超参数选择，才能做**可复现的科学研究**。这门课大量引用 OLMo / LLaMA / DeepSeek 的公开细节作为教学素材，正是因为 frontier 闭源模型「无细节可讲」。

---

## 4. 从分词到语言模型：下一步是什么

分词只是第一步——把文本变成 `[token_id₁, token_id₂, ...]` 的整数序列。接下来：

```mermaid
flowchart LR
    TEXT["文本<br/>'the quick brown fox'"] --> TOK["分词器"]
    TOK --> IDS["token IDs<br/>[1820, 4062, 14198, 39935]"]
    IDS --> EMB["嵌入层<br/>(V, D)"]
    EMB --> VEC["嵌入向量<br/>(T, D)"]
    VEC --> TF["Transformer<br/>注意力 + MLP"]
    TF --> LOGITS["logits<br/>(T, V)"]
    LOGITS --> NEXT["预测下一个 token"]
    
    style TOK fill:#ffe0b2,stroke:#e65100
    style EMB fill:#e1f5fe,stroke:#0277bd
    style TF fill:#c8e6c9,stroke:#2e7d32
    style LOGITS fill:#f3e5f5,stroke:#7b1fa2
```

**下一讲（[[Lecture 2 · PyTorch 与资源核算]]）预告重点**：

- **PyTorch 张量与内存布局**：浮点格式（fp32 / fp16 / bf16 / fp8）、storage 与 stride、einops 风格的张量操作；
- **FLOPs 核算**：$6ND$ 公式的拆解（前向 $2ND$、反向 $4ND$）、MFU（模型 FLOPs 利用率）；
- **内存估算**：参数 / 梯度 / 优化器状态 / 激活各占多少；
- **训练循环全件**：初始化、数据加载（memmap、pinned memory）、优化器（SGD → AdamW）、checkpointing、混合精度。

> [!tip] 直觉
> napkin math（餐巾纸计算）是下一讲的主旋律——训任何东西之前，先用几次乘法回答「放得下吗、要多久、瓶颈在哪」。这个习惯是全课所有作业的第一道工序。

---

## 总结

```mermaid
mindmap
  root((Lecture 1<br/>概览与分词))
    课程定位
      从零构建 vs 调 API
      漏的抽象
      机制与心态可迁移
      五单元：Basics / Systems / Scaling / Data / Alignment
    效率是主线
      accuracy = efficiency x resources
      算法效率 44 倍提升
      全栈思维：算法 + 系统 + 数据
    分词
      字符 / 字节 / 词 / 子词权衡
      压缩率 = 字节数 / token 数
      BPE：贪心合并高频 pair
      字节安全：UTF-8 → 开放词表
      预分词正则与特殊 token
    工业化现实
      GPT-4: 1.8T 参数 / $100M
      6ND 法则
      开放模型：OLMo / LLaMA / Qwen / DeepSeek
```

**关键要点**：

- **从零构建 = 理解全栈**：这门课不教怎么调 Hugging Face API，而是让你亲手写分词器、Transformer、训练循环、分布式并行——因为语言模型的抽象是漏的，只有实现过才能真正理解。
- **效率是贯穿全课的主线**：$\text{accuracy} = \text{efficiency} \times \text{resources}$——资源是给定的，能优化的只有效率；数据处理、分词、架构、训练策略的几乎所有设计决策都由效率驱动。
- **BPE 是现代语言模型的标配分词方案**：从字节出发、贪心合并高频 pair、保证字节安全（无 `UNK`）。训练算法简单但有效；压缩率（字节/token）是衡量它的核心指标。
- **分词不是孤立的**：预分词正则（避免跨词合并）、特殊 token（文档边界）、词表大小（压缩率 vs 嵌入表大小 vs 长尾学习）都是权衡后的工程决策；空格吸附、数字切分等怪癖会直接影响模型行为。
- **开放模型是研究的基石**：GPT-4 不公开细节、OLMo / DeepSeek 高度透明——只有后者能支撑可复现的科学研究。

**下一讲预告**：

[[Lecture 2 · PyTorch 与资源核算]]——深入 PyTorch 的张量、内存布局、浮点格式（fp32 / bf16 / fp8），掌握 $6ND$ 公式的前向 / 反向拆解，学会估算参数 / 梯度 / 优化器 / 激活的内存占用，理解混合精度训练、随机性控制、数据加载器与 checkpointing。napkin math 贯穿全场——用简单乘法快速判断训练配置是否可行。这是后续所有作业的基础工具集。

---

## 复习自测

> [!question]- Q1：字节级分词词表只有 256、天然没有 OOV，为什么没有任何 frontier 模型直接用它？
> 因为压缩率恒为 1（每字节一个 token），序列长度会比 BPE 长约 4 倍。而注意力的计算量是 $O(T^2)$、上下文窗口按 token 计费、生成时每个 token 都要一次完整前向——序列变长 4 倍意味着训练与推理成本全面上涨。ByT5 / MegaByte / BLT 等 tokenization-free 方案试图解决这个问题，但尚未在 frontier 规模上被验证。

> [!question]- Q2：BPE 编码时为什么必须按训练时的顺序应用合并规则？
> 因为后期规则引用的是前期合并产生的新 token ID。例如规则 `(256,97)→257` 中的 256 是由更早的 `(97,97)→256` 产生的——如果先尝试应用 `(256,97)`，序列里根本还没有 256，规则匹配不上，最终切分会与训练时的分布不一致，模型看到的 token 统计特性就变了。

> [!question]- Q3：手算：对 `abababab`（a=97, b=98）训练 BPE，做 2 次合并，写出每步的序列。
> 初始：`[97,98,97,98,97,98,97,98]`。
> 迭代 1：`(97,98)` 出现 4 次最高 → 合并为 256（`b'ab'`），得 `[256,256,256,256]`。
> 迭代 2：`(256,256)` 最高（朴素计数会数出 3 次相邻对，但从左到右贪心替换是不重叠的）→ 合并为 257（`b'abab'`），得 `[257,257]`。
> 8 字节 → 2 个 token，压缩率 4.0。

> [!question]- Q4：把词表从 5 万扩到 15 万，哪些量变好、哪些变差？
> 变好：压缩率提高、序列变短——同样的上下文窗口装下更多文本，每段文本的训练与推理成本下降；多语言（尤其非拉丁文字）的压缩率显著改善。
> 变差：嵌入表与输出 softmax 的参数量、显存、计算都随 $V$ 线性增长；长尾 token 出现频次更低、嵌入学习不充分；小模型上词表参数占比可能过大。

> [!question]- Q5：用 $C \approx 6ND$ 估算 Qwen 2.5 72B（18T tokens）的训练 FLOPs。
> $C \approx 6 \times 72\times10^9 \times 18\times10^{12} \approx 7.8\times10^{24}$ FLOPs——约为 LLaMA 3.1 405B（$3.8\times10^{25}$）的五分之一。这类两秒钟的估算正是「资源核算心态」的日常应用。

---

## 参考资料

- 💻 [lecture_01.py（本讲可执行讲义）](https://github.com/stanford-cs336/spring2025-lectures/blob/main/lecture_01.py)
- 📄 [Neural Machine Translation of Rare Words with Subword Units（Sennrich et al., 2016）](https://arxiv.org/abs/1508.07909) — BPE 在 NLP 中的开创性应用
- 📄 [Language Models are Unsupervised Multitask Learners / GPT-2（Radford et al., 2019）](https://cdn.openai.com/better-language-models/language_models_are_unsupervised_multitask_learners.pdf) — 字节级 BPE 分词器 + 预分词正则
- 📄 [GPT-4 Technical Report（OpenAI, 2023）](https://arxiv.org/abs/2303.08774) — frontier model 的不透明现实
- 📄 [LLaMA: Open and Efficient Foundation Language Models（Touvron et al., 2023）](https://arxiv.org/abs/2302.13971)
- 📄 [LLaMA 3 Model Card](https://github.com/meta-llama/llama3/blob/main/MODEL_CARD.md) — 405B 参数 / 15.6T tokens / 3.8×10²⁵ FLOPs
- 📄 [DeepSeek-V3 Technical Report（2024）](https://arxiv.org/abs/2412.19437) — 开放的 MoE 架构与训练细节
- 📄 [Training Compute-Optimal Large Language Models / Chinchilla（Hoffmann et al., 2022）](https://arxiv.org/abs/2203.15556) — 计算最优配比 D ≈ 20N
- 📄 [AI and Efficiency（Hernandez & Brown, OpenAI, 2020）](https://arxiv.org/abs/2005.04305) — ImageNet 算法效率 7 年 44 倍
- 📄 [ByT5（Xue et al., 2021）](https://arxiv.org/abs/2105.13626) · [MegaByte（Yu et al., 2023）](https://arxiv.org/abs/2305.07185) · [Byte Latent Transformer（Pagnoni et al., 2024）](https://arxiv.org/abs/2412.09871) — tokenization-free 路线
- 🌐 [CS336 课程主页（Spring 2025）](https://stanford-cs336.github.io/spring2025/)
- ▶️ [Lecture 1 视频（YouTube）](https://www.youtube.com/watch?v=SQ3fZ1sAqXI)
- 🔗 [Tiktokenizer 在线体验](https://tiktokenizer.vercel.app/) — 可视化 BPE 分词过程
