
过去八年,研究人员与底层技术的联系逐渐减弱——八年前他们会自己实现并训练模型,六年前开始下载现成模型(如 BERT)并微调,如今更多只是对专有模型(如 GPT-4、Claude、Gemini)进行提示。虽然抽象层级的提升提高了生产力,但这些抽象并不完全封闭,仍存在漏洞。要进行真正的基础研究,仍需“拆开技术栈”深入理解其原理。本课程的目标就是通过从零构建语言模型来获得这种理解。
SERIES · 斯坦福CS336: Language Modeling from Scratch
2025-09-18 · 20 min read · by GUMP

过去八年,研究人员与底层技术的联系逐渐减弱——八年前他们会自己实现并训练模型,六年前开始下载现成模型(如 BERT)并微调,如今更多只是对专有模型(如 GPT-4、Claude、Gemini)进行提示。虽然抽象层级的提升提高了生产力,但这些抽象并不完全封闭,仍存在漏洞。要进行真正的基础研究,仍需“拆开技术栈”深入理解其原理。本课程的目标就是通过从零构建语言模型来获得这种理解。
大型语言模型正在快速工业化。
更多参数,表现就会不同
这说明:小模型的实验结果未必能直接推广到大模型,因为规模不仅影响性能,还会带来新的能力与计算结构分配的变化。

图1

图2
to frontier models?
在这门课中,我们能学到三类可迁移到前沿模型的知识:
课程可以完整教授机制和思维方式(可直接迁移),但直觉只能部分传授,因为它并不总是能跨规模迁移。很多设计决策目前还无法通过理论完全证明,只能依赖实验探索——例如 Noam Shazeer 在 2020 年提出的 SwiGLU 激活函数就是这样得出的。

无分词器方法(Tokenizer-free approaches)**[Xue+ 2021][Yu+ 2023][Pagnoni+ 2024][Deiseroth+ 2024]**
起点:原始Transformer模型。

主要变体:
GPU (A100)

类比
仓库(warehouse) 相当于DRAM(动态随机存取存储器),而工厂(factory) 相当于SRAM(静态随机存取存储器)。

技巧
核心原则:即使在多GPU之间,数据移动也更慢,因此最小化数据移动的原则依然成立。

主要方法:
gather、reduce、all-reduce等集体通信操作。目标:根据给定的提示(prompt)生成标记(tokens),这是实际使用模型的关键环节。

应用场景:除了生成内容,推理还用于强化学习、测试计算和模型评估。
全局成本:推理计算(每次使用)的总量超过训练计算(一次性成本)。
两个阶段:
加速解码的方法
主要用于在大规模训练前,从小规模实验中预测最优的超参数和损失。


对齐的两个阶段
sft_data: list[ChatExample] = [
ChatExample(
turns=[
Turn(role="system", content="You are a helpful assistant."),
Turn(role="user", content="What is 1 + 1?"),
Turn(role="assistant", content="The answer is 2."),
],
),
]preference_data: list[PreferenceExample] = [
PreferenceExample(
history=[
Turn(role="system", content="You are a helpful assistant."),
Turn(role="user", content="What is the best way to train a language model?"),
],
response_a="You should use a large dataset and train for a long time.",
response_b="You should use a small dataset and train for a short time.",
chosen="a",
)
]数据生成方式:
[A, B]。A优于B或B优于A。数据示例:一个偏好数据示例可能包含:
response_a和response_b。a作为更优的回应。GPT2_TOKENIZER_REGEX = r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}+| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+"""
Tokenizer 将 字符串 ↔ token(索引)
常见方法:
Character-based
Byte-based
Word-based
这些方法通常效果不理想。
BPE(Byte Pair Encoding) 是基于语料统计的有效启发式方法。
Tokenization 是必要的步骤,将来可能直接从 bytes 处理。
from dataclasses import dataclass
@dataclass(frozen=True)
class BPETokenizerParams:
"""BPETokenizer 所需的全部参数"""
vocab: dict[int, bytes] # index -> bytes
merges: dict[tuple[int, int], int] # index1, index2 -> new_index
class CharacterTokenizer(Tokenizer):
"""将字符串表示为 Unicode code points 序列"""
def encode(self, string: str) -> list[int]:
return list(map(ord, string))
def decode(self, indices: list[int]) -> str:
return "".join(map(chr, indices))
class ByteTokenizer(Tokenizer):
"""将字符串表示为字节序列"""
def encode(self, string: str) -> list[int]:
string_bytes = string.encode("utf-8")
indices = list(map(int, string_bytes))
return indices
def decode(self, indices: list[int]) -> str:
string_bytes = bytes(indices)
string = string_bytes.decode("utf-8")
return string
def merge(indices: list[int], pair: tuple[int, int], new_index: int) -> list[int]:
"""返回合并后的 indices,将所有 pair 替换为 new_index"""
new_indices = []
i = 0
while i < len(indices):
if i + 1 < len(indices) and indices[i] == pair[0] and indices[i + 1] == pair[1]:
new_indices.append(new_index)
i += 2
else:
new_indices.append(indices[i])
i += 1
return new_indices
class BPETokenizer(Tokenizer):
"""基于 merges 和 vocab 的 BPE tokenizer"""
def __init__(self, params: BPETokenizerParams):
self.params = params
def encode(self, string: str) -> list[int]:
indices = list(map(int, string.encode("utf-8")))
# 注意:实现非常慢
for pair, new_index in self.params.merges.items():
indices = merge(indices, pair, new_index)
return indices
def decode(self, indices: list[int]) -> str:
bytes_list = list(map(self.params.vocab.get, indices))
string = b"".join(bytes_list).decode("utf-8")
return string
def get_compression_ratio(string: str, indices: list[int]) -> float:
"""计算 tokenization 后的压缩率"""
num_bytes = len(bytes(string, encoding="utf-8"))
num_tokens = len(indices)
return num_bytes / num_tokens
def get_gpt2_tokenizer():
# 使用 tiktoken
# 对应 GPT-3.5-turbo 或 GPT-4 可使用 cl100k_base
return tiktoken.get_encoding("gpt2")
原始文本通常表示为 Unicode 字符串
语言模型对 token 序列(整数索引)建立概率分布
Tokenizer 负责将字符串编码为 token,并能解码回字符串
Vocabulary size = 可用 token 数量
string = "Hello, 🌍! 你好!"
tokenizer = get_gpt2_tokenizer()
# 编码 & 解码检查
indices = tokenizer.encode(string)
reconstructed_string = tokenizer.decode(indices)
assert string == reconstructed_string
# 压缩率
compression_ratio = get_compression_ratio(string, indices)
" world")"hello hello")ord() 转换为整数 code pointchr() 还原为字符assert ord("a") == 97
assert ord("🌍") == 127757
assert chr(97) == "a"
assert chr(127757) == "🌍"
tokenizer = CharacterTokenizer()
string = "Hello, 🌍! 你好!"
indices = tokenizer.encode(string)
reconstructed_string = tokenizer.decode(indices)
assert string == reconstructed_string
compression_ratio = get_compression_ratio(string, indices)
# 单字节字符
assert bytes("a", encoding="utf-8") == b"a"
# 多字节字符
assert bytes("🌍", encoding="utf-8") == b"\xf0\x9f\x8c\x8d"
tokenizer = ByteTokenizer()
string = "Hello, 🌍! 你好!"
indices = tokenizer.encode(string)
reconstructed_string = tokenizer.decode(indices)
assert string == reconstructed_string
compression_ratio == 1),序列过长 → Transformer 注意力开销大vocabulary_size = 256
compression_ratio = get_compression_ratio(string, indices)
import regex
string = "I'll say supercalifragilisticexpialidocious!"
segments = regex.findall(r"\w+|.", string) # 保留单词和标点
pattern = GPT2_TOKENIZER_REGEX
segments = regex.findall(pattern, string)
vocabulary_size = "Number of distinct segments in the training data"
compression_ratio = get_compression_ratio(string, segments)
def train_bpe(string: str, num_merges: int) -> BPETokenizerParams:
indices = list(map(int, string.encode("utf-8")))
merges: dict[tuple[int, int], int] = {}
vocab: dict[int, bytes] = {x: bytes([x]) for x in range(256)}
for i in range(num_merges):
# 统计相邻 token 对出现次数
counts = defaultdict(int)
for index1, index2 in zip(indices, indices[1:]):
counts[(index1, index2)] += 1
# 找出最常见的 pair
pair = max(counts, key=counts.get)
index1, index2 = pair
# 合并 pair
new_index = 256 + i
merges[pair] = new_index
vocab[new_index] = vocab[index1] + vocab[index2]
indices = merge(indices, pair, new_index)
return BPETokenizerParams(vocab=vocab, merges=merges)
params = train_bpe("the cat in the hat", num_merges=3)
tokenizer = BPETokenizer(params)
string = "the quick brown fox"
indices = tokenizer.encode(string)
reconstructed_string = tokenizer.decode(indices)
assert string == reconstructed_string