Lecture 1:概览与分词
Stanford CS336 Language Modeling from Scratch - Lecture 1: Overview, Tokenization,时长 01:19:21。
为什么要从零构建语言模型#
在 coding agent 已经能零样本写出语言模型的今天,一个自然的问题是:亲手从零搭建语言模型还有必要吗?
答案是肯定的,但需要先理解一个正在发生的断层。回溯过去十年,AI 研究者与底层技术之间的距离不断拉大。2016 年前后,研究者自己实现并训练模型是常态;2018 年 BERT 出现后,主流范式变成下载预训练模型再 fine-tune;而到了今天,大多数人的工作方式是直接 prompting 一个 API 模型(GPT、Claude、Gemini)。
向高层抽象迁移本身是好事——它提高了生产力。但与编程语言或操作系统不同,语言模型的抽象层是泄漏的(leaky)。当你 prompting 一个模型时遇到它做不到的事情,你几乎没有任何补救手段。而如果你想做根本性的研究(fundamental research),仅仅在 prompting 层面操作,实际上极大地约束了你的设计空间。
Full understanding of this technology is necessary for fundamental research. Philosophy of this course: understanding via building.
这门课的理念很明确:通过构建来理解。但这里有一个现实问题——前沿模型的工业化已经发生。GPT-4 的训练成本据称在 1 亿美元量级,当前前沿模型的训练成本可能已接近 10 亿美元。更关键的是,这些模型的技术细节完全不公开——GPT-4 的论文明确声明因”竞争格局和安全考量”不分享任何构建细节。
那在一门大学课程中训练的”小”模型(约 1B 参数规模),真的能学到可迁移到前沿模型的知识吗?这里需要清醒认识两个现实:
-
计算分布随规模变化。在小模型中,MLP 层和 Attention 层的 FLOPs 占比大约各占一半(MLP ~44%);但在 175B 参数的模型中,MLP 层的 FLOPs 占比飙升到 ~80%。这意味着在小规模上优化 Attention 的效果,到大规模未必同样显著。
-
涌现能力(emergent abilities)的存在。在许多任务上,小模型的表现几乎是随机水平,只有当模型规模跨越某个临界点后,性能才会突然跃升。

这看起来似乎令人沮丧,但实际上可迁移的知识可以分为三个层次:
- Mechanics(机械层面):Transformer 是什么、Model Parallelism 如何工作——这些是确定性的知识,完全可以从小规模学习后迁移到大规模
- Mindset(思维方式):如何从硬件中压榨最大性能、如何严肃对待 scaling——这是一种工程思维,同样跨规模适用
- Intuitions(直觉):哪些数据和建模决策能带来好的 accuracy——这一层不一定能跨规模迁移,有些在小规模上有效的直觉在大规模上可能完全不成立
前两类知识是这门课能可靠传授的;第三类需要保持警惕——有些设计决策至今没有很好的理论解释,纯粹来自实验。例如 Noam Shazeer 引入 SwiGLU 激活函数的论文,其结论部分坦承这些架构变体”simply not (yet) justifiable and just come from experimentation”。
关于效率的核心定位:这门课的核心问题可以表述为——给定一定的计算和数据预算,能构建出的最好模型是什么? 换言之,最大化效率。效率在大规模下尤为重要,因为浪费一次训练的代价极其高昂。[Hernandez+ 2020] 的研究显示,2012 到 2019 年间,ImageNet 上的算法效率提升了 44 倍——这说明算法层面的进步与硬件进步同样重要。
语言模型发展简史#
语言模型的历史可以清晰地划分为四个阶段,每一阶段都在效率和能力上实现了质的飞跃。
Pre-neural 时代(2010 年以前)#
语言模型的思想最早可追溯到 Shannon [1950],他提出用语言模型来测量英语的信息熵。此后数十年的主流方案是 n-gram 语言模型,被广泛应用于机器翻译和语音识别系统 [Brants+ 2007]。
Neural ingredients 阶段(2010s)#
这个阶段涌现了一系列关键技术组件,它们后来成为大语言模型的基础设施:
- LSTM (Long-Short Term Memory) [Hochreiter+ 1997]——解决了 RNN 的长距离依赖问题
- 第一个神经语言模型 [Bengio+ 2003]——用神经网络直接建模词序列概率
- Sequence-to-Sequence [Sutskever+ 2014]——将编码-解码框架引入机器翻译
- Adam 优化器 [Kingma+ 2014]——至今仍是训练深度模型的默认优化器之一
- Attention 机制 [Bahdanau+ 2014]——让模型能”关注”输入序列的特定位置
- Transformer 架构 [Vaswani+ 2017]——完全基于注意力机制,抛弃了递归结构
- Mixture of Experts [Shazeer+ 2017]——通过稀疏激活实现模型容量的扩展
- 模型并行 [Huang+ 2018][Rajbhandari+ 2019][Shoeybi+ 2019]——使训练超大模型成为可能
Early foundation models 阶段(2010 年代末)#
这一阶段的核心转变是 pretraining + fine-tuning 范式的确立:
- ELMo [Peters+ 2018]:用 LSTM 做预训练,证明预训练特征可以提升下游任务
- BERT [Devlin+ 2018]:用 Transformer 做预训练,fine-tuning 在 NLU 任务上取得突破
- T5 (11B 参数) [Raffel+ 2019]:将所有任务统一为 text-to-text 格式
Embracing scaling 阶段(2020 年至今)#
从 GPT-3 [Brown+ 2020] 开始,领域进入了”拥抱规模”的时代。关键的认知转变是:如果你想让模型在某个任务上表现好,与其设计特定的架构或训练策略,不如直接把模型做大、数据做多。
这个阶段诞生了两类重要的开放模型生态:
Open-weight models(公开权重但不公开训练细节):
- Meta 的 Llama 系列 [Meta 2024, 2025]
- DeepSeek 系列 [DeepSeek-AI+ 2025]
- Alibaba 的 Qwen [Qwen+ 2024]
- Moonshot 的 Kimi [Kimi Team 2025]
- 智谱的 GLM [GLM-4.5 Team 2025]
Open-source models(同时公开权重、论文、代码和数据):
- AI2 的 OLMo [Groeneveld+ 2024][Team OLMo 2025]
- NVIDIA 的 Nemotron [Parmar+ 2024]
- Marin 的开放开发模型 [Marin 8B/32B retro]
开放性对于信任和创新至关重要 [Kapoor+ 2024]。正是来自开放模型的思想和实践,使得像 CS336 这样的课程成为可能。
课程路线图:构建 LM 的六大模块#
这门课的整体设计围绕一个明确的目标展开:从零训练一个约 1B 参数的语言模型。为达成这个目标,课程分解为六个模块,每个模块对应一个核心工程问题。值得注意的是,课程采用了一种独特的 executable lecture(可执行讲座)形式——所有讲义本身就是一个可以运行的 Python 程序,代码和教学内容融为一体。
模块一:模型架构(Architecture)#
起点是原始 Transformer [Vaswani+ 2017],但现代大语言模型在此基础上做了大量修改。需要理解的核心组件包括:
Attention 机制的变体:从标准 Multi-Head Attention 到 Multi-Query Attention [Shazeer 2019]、Grouped-Query Attention [Ainslie+ 2023],以及 DeepSeek-AI [2024] 的变体。目标是在减少 KV cache 内存的同时尽量保持模型质量。
非 Transformer 架构:Recurrence/State-Space Models 如 Mamba [Katharopoulos+]、Gated DeltaNet [Dao+ 2024],以及线性 Attention 的变体 [Yang+ 2024][Lahoti+ 2026]。
MLP 层:Dense MLP 与 Mixture of Experts (MoE) [Shazeer+ 2017][Fedus+ 2021] 的选择。MoE 通过稀疏激活在不增加推理计算量的前提下扩大模型容量。
模型形状参数:hidden dimension、depth、head 数量、expert 数量等超参数的选择。
模块二:训练(Training)#
训练模块回答的核心问题是:如何设定模型的参数?
- 损失函数:除了标准的 next-token prediction,还有 multi-token prediction [Gloeckle+ 2024][DeepSeek-AI+ 2024]
- 优化器:从经典的 AdamW [Kingma+ 2014] 到更新的 SOAP [Vyas+ 2024]、Muon [Keller+ 2024] 和 Loshchilov+ 2017 的变体
- 初始化策略:Xavier init [Glorot+ 2010]、muP (Maximal Update Parameterization) [Yang+ 2022]——后者尤为重要,因为它实现了超参数迁移,即在小规模上调好的超参数可以直接用于大规模
- 学习率调度:从 cosine schedule [Loshchilov+ 2016] 到 WSD (Warmup-Stable-Decay) [Hu+ 2024]
- 正则化:dropout、weight decay 等
- Batch size:critical batch size 的概念 [McCandlish+ 2018]
- MoE 的负载均衡:如 aux-free 的方案 [Wang+ 2024][DeepSeek-AI+ 2024]
模块三:系统优化(Kernels & Parallelism)#
这个模块的核心问题是:如何让 GPU 真正快起来?
GPU Kernel 是运行在 GPU 上的函数。PyTorch 中每个原语操作都会启动一个标准 kernel,但通过编写自定义 kernel 可以获得巨大的性能提升。核心原则是最小化数据搬运:
- Naive 方式:读 HBM → 计算 A → 写 HBM → 读 HBM → 计算 B → 写 HBM
- Fused 方式:读 HBM → 计算 A 和 B → 写 HBM
相关优化策略包括 operator fusion(如 matmul + activation 融合)、tiling(FlashAttention 的核心思想)、warp divergence、memory coalescing、bank conflicts 等。实现工具有 CUDA、Triton、CUTLASS、ThunderKittens。
并行训练 解决”如果有 1024 块 GPU 怎么用?“的问题。GPU 之间的数据搬运比 GPU 内部更慢,但”最小化数据搬运”的原则不变。核心概念包括:
- 经典 collective operations:gather、reduce、all-reduce
- Shard memory:将参数、激活值、梯度、优化器状态分片到多块 GPU
- 计算拆分策略:data / tensor / pipeline / sequence / expert parallelism
推理加速 也是课程覆盖的内容。加速解码的方法包括:
- 使用更便宜的模型(pruning、quantization、distillation)
- Speculative decoding:用一个轻量 “draft” 模型并行生成多个 token,然后用完整模型打分验证(这是 exact decoding!)
- 系统优化:fused kernels、continuous batching
模块四:Scaling Laws#
当你已经有了模型、能让它跑得快、能并行训练之后,下一个问题是:如何科学地扩大规模?
设想你有 1025 FLOPs 的计算预算(相当于数千万美元),你只能训练一个模型——如果搞砸了,这些钱就白费了。你不可能在这个规模做 hyperparameter tuning,因为每次训练都太贵了。
核心思维转变:不要把注意力放在”一个模型”上,而是关注一个 scaling recipe——它是从 FLOPs 预算到一组超参数(配置文件)的映射。
具体做法是:
- 在多个较小的规模(如 1024 FLOPs)上训练,记录 loss
- 拟合一个 scaling law
- 用 scaling law 预测目标规模(如 1025 FLOPs)上的 loss
这使得两件事成为可能:
- 用小规模实验优化大规模的 scaling recipe
- 在真正训练之前预测目标规模的性能
经典的 compute-optimal scaling laws 来自 [Kaplan+ 2020] 和 [Hoffmann+ 2022](Chinchilla 论文)。实践中使用 ISOFLOP curves:在多个小 FLOPs 预算下找到最优模型大小 N,然后拟合 scaling law 外推到大 FLOPs 预算。
一个关键要求是 scaling laws 依赖于精心构造的 scaling recipe。[Yang+ 2022] 的 muP 提供了 hyperparameter transfer 的理论基础,使得在小规模调好的超参数可以迁移到大规模。而 predictability 至少和 optimality 一样重要——一个次优但可预测的 recipe 比一个偶尔最优但不可预测的 recipe 更有用。
模块五:数据与评估(Data & Evaluation)#
有了模型和训练方法后,下一个问题是:用什么数据训练?
数据质量在根本上决定了模型的能力边界。对数据的选择实际上是在回答”你希望模型能做什么?“——多语言?擅长对话?还是 agentic coding?
评估(Evaluation) 分为两个层次,它们服务于不同目的,不应混淆:
- 内部评估(for development):关键要求是跨规模的平滑性和相对性能。Perplexity 是最典型的内部指标——一个 perplexity 的绝对值没有太大意义,但两个模型的 perplexity 比较非常有价值
- 外部评估(for reporting):关注生态有效性(ecological validity)。典型 benchmark 包括 GPQA、HLE、SWE-Bench、Terminal-Bench
数据类型 分为三个阶段:
- Pretraining data:大规模、多样化
- Mid-training data:高质量,包括 long-context 数据
- Post-training data:supervised fine-tuning 数据(对话、agentic traces with tool calling)
数据治理 的关键环节:
- Filtering:用分类器保留高质量内容、过滤有害内容
- Deduplication:节省计算、避免记忆化;工具包括 Bloom filters 和 MinHash
- Data mixing:不同数据源的权重分配 [Liu+ 2024][Chen+ 2026]
- Rewriting / synthetic data:用 LM 改写或生成数据,使其更贴近下游任务 [Maini+ 2024]
效率作为统一主题#
回到一个核心认知:当前阶段是 compute-constrained 的,所有设计决策都围绕”在固定资源下最大化性能”展开。效率不是某个模块独有的话题,而是渗透在每一个决策中:
- Systems:显然是关于效率的
- Tokenization:直接操作 raw bytes 在理论上更优雅,但以当前模型架构的计算效率来看是不现实的
- Model architecture:许多架构变化的动机是减少内存或 FLOPs(如 sharing KV cache、window attention)
- Data filtering:避免在低质量数据上浪费宝贵的计算
- Scaling laws:用小模型的计算来替代大模型的超参数调优
明天我们将变成 data-constrained 的……但今天,效率仍是王道。
Tokenization:从字符串到 token 序列#
语言模型的输入和输出都是 token 序列(以整数索引表示),但人类书写的文本是 Unicode 字符串。Tokenizer 的任务就是完成两者之间的双向转换:
encode: string → token indicesdecode: token indices → string
一个合格的 tokenizer 必须满足 round-trip 一致性:decode(encode(s)) == s。如果这个不成立,说明实现有 bug。
评价 tokenizer 的核心指标是 compression ratio(压缩比):
compression ratio=token数bytes数
例如字符串 "the quick brown fox" 有 20 个 bytes,GPT-5 的 tokenizer 将其编码为 8 个 token,压缩比为 20/8=2.5 bytes/token。
压缩比越高越好——因为 Attention 的计算复杂度是序列长度的二次方,更短的 token 序列意味着更低的计算成本。但提高压缩比的直接手段(增大词表)会带来稀疏性问题:词表中越来越多的 token 出现频率极低,导致 embedding 矩阵的参数利用率下降。
现代多语言 tokenizer 的词表大小通常在 100K-200K。
几种基本 tokenizer 的对比#
在引入 BPE 之前,先看看三种朴素方案为何不够好:
Character Tokenizer:将每个 Unicode 字符映射为一个 token。Unicode 共有约 150K 个字符,所以词表大小可达 150K。但大量 Unicode 字符极为罕见,导致词表中充斥着几乎不被使用的条目。更关键的是,压缩比很低——每个字符都是独立 token,没有任何压缩。
Byte Tokenizer:先将字符串用 UTF-8 编码为字节序列,每个字节(0-255)作为一个 token。词表大小仅 256,非常紧凑。但压缩比恒为 1——序列长度等于字节数,对于 Attention 来说太长了。ASCII 字符(如英文字母)每个只占 1 个字节,但中文、emoji 等字符可能占 3-4 个字节,进一步拉长序列。
Word Tokenizer:用空格或正则表达式将文本切分成词,每个词作为一个 token。优点是每个 token 都有明确的语义(因为词是人类设计的语义单元),压缩比也不错。但有两个致命问题:
- 词表大小等于训练数据中不同词的数量,理论上无界
- 测试时遇到训练数据中未出现过的词,只能用
UNKtoken 替代,这不仅丑陋,还会破坏 perplexity 的计算
Character tokenizer 和 Byte tokenizer 都”不够好”——前者词表冗余、后者序列太长。Word tokenizer 看似理想,却面临开放词表和 UNK 问题。需要一种能在压缩比和词表大小之间取得平衡的方案。
Byte-Pair Encoding 的训练与实现#
Byte-Pair Encoding (BPE) 最初由 Philip Gage 在 1994 年为数据压缩提出,后被 [Sennrich+ 2015] 引入 NLP(用于神经机器翻译),并在 GPT-2 [Radford+ 2019] 中首次应用于大语言模型。
BPE 的核心直觉是:在原始字节序列上,频繁出现的相邻 byte 对应该被合并为一个新 token。高频序列(如英文中的 “th”、“he”、“in”)获得单独的 token,低频序列则被保留为更小的单元(极端情况下退化为单个字节)。这保证了:
- 无论输入什么文本,都能被 tokenize(不存在 UNK)
- 常见模式获得高效的压缩
- 词表大小是可控的(= 256 + num_merges)
BPE 训练算法#
训练过程概念上非常简洁。给定一个文本语料,算法如下:
- 将整个语料转换为 UTF-8 字节序列,每个字节就是一个初始 token(索引 0-255)
- 统计所有相邻 token 对的出现次数
- 找到出现次数最多的那一对,为其分配一个新的 token 索引(从 256 开始递增)
- 在整个序列中,将所有该 pair 的出现替换为新 token
- 重复步骤 2-4,直到达到指定的合并次数
num_merges
以字符串 "the cat in the hat" 为例,逐步执行:
初始状态:将字符串编码为字节序列
1indices = [116, 104, 101, 32, 99, 97, 116, 32, 105, 110, 32, 116, 104, 101, 32, 104, 97, 116](其中 116=‘t’, 104=‘h’, 101=‘e’, 32=’ ’, 99=‘c’, 97=‘a’, 105=‘i’, 110=‘n’)
第一轮合并:统计相邻对的频率。(116, 104) 即 “th” 出现 2 次(最高频)。创建新 token 256 代表 “th”,替换所有出现位置:
1merges[(116, 104)] = 2562vocab[256] = b"th"3indices → [256, 101, 32, 99, 97, 116, 32, 105, 110, 32, 256, 101, 32, 104, 97, 116]序列从 18 个 token 缩短到 16 个。
第二轮合并:此时 (256, 101) 即 “the” 出现 2 次。创建 token 257 代表 “the”:
1merges[(256, 101)] = 2572vocab[257] = b"the"3indices → [257, 32, 99, 97, 116, 32, 105, 110, 32, 257, 32, 104, 97, 116]第三轮合并:继续寻找最高频 pair 并合并……
随着合并的进行,序列不断缩短,词表不断增长。最终的 compression ratio 取决于合并次数——合并越多,压缩比越高,但词表也越大。对于这个玩具例子,3 次合并后 compression ratio 为 1.5。
Encode 与 Decode#
训练完成后得到两样东西:一个 merges 字典(记录每次合并的 pair → new_index 映射)和一个 vocab 字典(记录每个 index → bytes 映射)。
Encode(对新文本 tokenize):
- 将输入字符串转为 UTF-8 字节序列
- 按照训练时的合并顺序,依次检查序列中是否存在可合并的 pair
- 如果存在,执行合并
- 重复直到没有更多可合并的 pair
Decode(从 token indices 恢复字符串):
- 对每个 token index,查找 vocab 字典得到对应的 bytes
- 拼接所有 bytes
- 用 UTF-8 解码得到字符串
工程优化与 Assignment 1#
上述实现虽然正确,但极其缓慢。encode() 函数需要遍历所有 merges(数量等于 vocab_size - 256),对于 100K+ 词表这意味着每次编码都要做数万次全序列扫描。
Assignment 1 的核心任务就是优化 BPE 的性能:
encode()目前遍历所有 merges,应该只遍历与当前序列相关的 merges,需要构建索引结构- 处理 special tokens(如
<|endoftext|>)——概念上不复杂但对构建现代 tokenizer 很重要 - 使用 pre-tokenization:先用正则表达式(如 GPT-2 的 tokenizer regex)将文本切分为 chunks,再对每个 chunk 独立做 BPE,大幅提升速度
- 追求极致性能时,Python 本身可能成为瓶颈——欢迎用 Rust 或 C 重写
Tokenization 的未来#
当前 tokenizer 的存在本质上是一个工程妥协——直接在 raw bytes 上建模更优雅、更通用(不需要为不同语言和模态设计不同的 tokenizer),但以当前的模型架构来说计算上不可行(byte 序列太长)。
Tokenizer-free 的方案 [Xue+ 2021][Yu+ 2024][Pagnoni+ 2024][Deiseroth+ 2024][Hwang+ 2025] 正在被积极探索,但尚未在前沿规模上验证。
即便 tokenizer 最终被取代,替代方案也必须满足两个核心要求:
- 某种形式的抽象是必要的。Transformer 需要操作的是对原始序列的某种 abstraction——这在文本之外(如视频、DNA 序列)更为明显,原始的 byte 或 unit 信噪比太低,需要先提升到一个有意义的表示层级
- 可变长度的 chunk(自适应计算)是必要的。不是所有的 byte 都同等重要——信息密度高的区域应该获得更多的计算资源。任何 end-to-end 的方案如果不具备这两个性质,大概率是次优的
部分内容可能已过时