Appearance
第23章 · 取舍的骨架:LLM 结构是怎么被逼出来的
前置要求:完成 第5章 · 字符语言模型与概率目标 的链式分解与交叉熵、第6章 · UTF-8 与 byte BPE 的分词器、第8章 · Attention 与 Transformer Block 的因果掩码与多头,以及 第9章 · TinyGPT 的完整训练循环。本章不引入任何新组件。
前 22 章里,Transformer 的形状基本是给定的:论文这么写,教程这么实现,实现跑通了。这一章反过来做一次——把每一处设计放回它被逼出来的那一步,问一句"不这么写会怎样"。
这不是补充知识,而是一套读法。目的有两个:读新的模型发布说明时,能指出它动的是哪一环;复盘自己掌握的部分时,能区分"我能从头推一遍"和"我记住了名字"。
本章的论证骨架取自斯坦福大学公开课 CS229: Machine Learning(Spring 2026,马腾宇 Tengyu Ma 授课)的两讲剪辑合集——Lecture 14: Transformers, In-Context Learning 与 Lecture 7: Neural Networks 1 (Architecture)。视频经 Bilibili 转载(BV1SUbZ6dECT,UP主「莫大爷的家」,内嵌中英双语硬字幕,时长 2 小时 34 分)。官方课件需 Stanford 邮箱登录、不公开,因此本章只取论证结构,不复述其幻灯片。原文已逐段重述、重新推导并换成可运行的量,引用见论文与延伸。讲师在多处明确表示"没有很简单的办法讲清楚",本章保留这些不确定标注,不把它们抹平成结论。
本章目标
学完后你能做到:
- 用
支撑集规模解释自回归分解为什么不是约定而是必然,并说出数量级。 - 复述结构—灵活性权衡:巨型 MLP 在能力上可行、在算力上不可行,取平均则预设过多而无参数。
- 区分 Transformer 的两类算子——逐位置的 MLP 与跨位置的 attention——并说明参数量为何与序列长度无关。
- 说明多头为什么是同一算子的多组权重而不是多份拷贝,以及它为什么不改变参数量。
- 把因果掩码表述为"删除 + 重归一化",并说明它与直接加
的关系。 - 把温度与 top-k 表述为同一个概率向量上的两种手术,并各自说明改了什么、没改什么。
- 把分词粒度变化换算成 token 数、logits 显存与费用。
阶段一: ——为什么必须分解
第5章把
先看不做分解会怎样。词表大小
种。把概率直接定义在这张表上,每个序列一个自由参数——这是最诚实的写法,也正好是走不通的写法。取工业级词表
text
support_log10(50257, 1000) = 4701.20 → 约 10^4701 个序列即使把上下文缩到 32 个 token,support_log10(50257, 32) = 150.44,仍然是
差着四千多个数量级。这个差距不是"训练慢一点",是连表示都放不下。
于是分解不是设计偏好,是唯一出路:链式法则把一张
代价写在明面上:分解之后
python/llm_core/design_tradeoffs.py 的 sequence_support_log10() 与 chain_rule_nll() 把这两步都做成了可对拍的函数。
阶段二:巨型 MLP 假说——结构与灵活性的取舍
支撑集论证只解释了"必须分解",没有解释"为什么用 Transformer 而不是别的东西"。补上这一步,需要先接受一个不舒服的前提。
假设算力和数据都无限。那么正确的做法是把整个序列喂给一个足够宽的 MLP,让它自己去学所有位置之间的关系。课程讲者的说法是:给定足够数据(不至于过拟合)和足够算力,存在某个 MLP 能真正解决这个任务。这不是定理,是经验判断,但它指对了一件重要的事——这个任务本身没有捷径,问题只在于代价。
代价有多大:这个 MLP 的输入层宽度是
text
giant_mlp_parameter_log10(50257, 1000, hidden=4096) = 4704.81比阶段一的支撑集还大三个数量级,而且这只是第一层。拿通用逼近定理来对照会看得更清楚:定理说的是"输入维度固定时,隐层足够宽就能逼近任意连续函数",它从来没有替你解决输入维度本身是
于是只能往回退:人为规定一些结构,把复杂度压下来。但压过头同样坏:
- 规定到极致:直接取所有位置的向量平均。计算量与
成正比,但没有任何可学参数——你把答案写进了结构里,模型无事可学。 - 完全放开:上一段那个
输入层的巨型 MLP。参数无限多,算不动。 - 中间某处:Transformer 所在的位置。
这就是结构—灵活性权衡:你引入多少结构,就等于替模型做掉多少决定;做掉的越多,算得越动,但留给数据去发现的空间越小。Transformer 不是唯一解,是这个连续谱上的一个具体取值。
这条轴是理解新模型的第一把钥匙。MoE(第12章)往"灵活"方向挪:把 FFN 拆成专家、用路由选少数几个,参数总量涨而每个 token 的计算量不涨。线性注意力、状态空间模型往"便宜"方向挪:牺牲精确的两两交互,换
判断一次架构改动落在轴上的哪一端,比记住它的超参数更有用。
阶段三:逐位置与跨位置——两条算子线
一旦决定规定结构,还剩一个具体问题:结构规定成什么样。Transformer 的答案是把每一层劈成两半,职责分得很干净。
MLP 是逐位置的。 它对第
attention 是唯一跨位置的算子。 整个 Transformer 里,凡是让第
这两条线交替出现,构成第8章的 block:
"参数量与 transformer_parameter_count():
| 组成 | 公式 | 值 |
|---|---|---|
| embedding(输入查表) | 38,597,376 | |
| 每层 attention | 2,359,296 | |
| 每层 FFN | 4,718,592 | |
| 每层合计 | 7,077,888 | |
| 输出头(embedding 共享时) | 0 | |
| 总计(12 层) | 123,532,032 |
表里没有一个
阶段四:多头是同一算子的多组权重
"多头"这个名字容易让人误解成"并行跑了
每一份所谓"拷贝",和原版共享完全相同的输入与完全相同的输出格式,唯一区别是
这个理解有一个立刻可验证的推论:头数不改变参数量。把
与 python/tests/test_design_tradeoffs.py::test_head_count_changes_no_parameter_budget 对
那么多头买到了什么?买入的是同一组输入的不同投影方式。不同的
顺带一个第8章没细说的量级:头数本身很小。课程里给的估计是,百亿(
阶段五:因果掩码是删除加重归一化
第8章把因果掩码实现成"未来位置加
第
但说"掩码"和说"删除",指向的机制并不完全一样:
- 掩码的说法:先在全部
个 key 位置上定义一个分布,再把不可见的那几项的概率质量转走,剩下的重新缩放。 - 删除的说法:分布的定义域根本就是
这个子集,不可见的位置从头到尾没参与运算。
两者数值完全相同(causal_masked_attention() 与"对前缀单独做 softmax"逐元素相等,见 test_causal_mask_equals_softmax_over_the_visible_prefix),但第二种说法能让你立刻回答一个真问题:第 test_causal_rows_sum_to_one_over_the_visible_prefix_only 里被钉死:三行之和都精确等于 1.0,严格上三角恒等于 0。
因果掩码还有一条常被忽略的性质:它不增加任何参数,也不改变 attention 的形式,只是把可见范围限制在前缀。所以在阶段二那条轴上,因果掩码既没往灵活方向挪,也没往便宜方向挪——它是纯粹的约束。
阶段六:温度与 top-k 是同一个向量上的两种手术
第5章把 temperature、top-k、top-p 并列成三个旋钮。这里换一个角度:它们作用在同一个对象上,区别只在改哪一步。
温度改 logits 的间距。 取 logits
| 分布 | 说明 | |
|---|---|---|
| 0.1 | 尾部压到十的十三次方量级 | |
| 1.0 | 模型自身分布 | |
| 10.0 | 明显变平 | |
| 1000.0 | 趋近均匀 |
关键在于排序恒定不变。四个温度下 argsort 都是 [0, 1, 2]。温度只缩放 logits 之间的差距,不重排名次,所以它改变的是"分布有多尖",不是"谁排第一"。想改谁排第一,只有 top-k 和 top-p 能做到。
top-k 改分布的支撑集。 top_k_renormalize() 把
text
[0.571429, 0.428571, 0.0, 0.0, 0.0]注意它是在概率向量上做的手术:先归零,再把幸存项重新缩放到和为 1。所以 top-k 之后被丢弃 token 的概率是构造出来的精确零,而温度再低也只是把概率压到
两者的共同点是第5章已经建立的结论:它们都在 logits 进入训练损失之前起作用,都不改权重。训练目标和它们无关。
阶段七:分词粒度如何变成账单
前面所有取舍都在模型内部。这一节走一个已经在系统边界上的量:分词器。
阶段一说过,词表大小
代入实测(batch 1,FP32):
| 配置 | logits 显存 | ||
|---|---|---|---|
| 基线 | 1000 | 50257 | 191.7 MiB |
| 上下文 +50% | 1500 | 50257 | 287.6 MiB(1.500×) |
| 词表 128k | 1000 | 128256 | 489.3 MiB(2.552×) |
这张表就是"词表粒度"变成"账单"的完整传导链。厂商把分词器改细(拆出更多、更短的子词),同样一段文本会被切成更多 token,于是:
- 单次请求的 token 计费上涨;
- 训练时每步的 logits 显存与计算上涨(约 2.55 倍);
- KV cache 的长度按 token 数线性增长(第10章)。
课程视频里给了一个当场的例子:某家编码工具更新分词器后,同一段文本从约 1000 个 token 变成约 1500 个,费用相应上涨——讲者当场补了一句"我没有自己核对过,是读到的新闻,如果属实"。本章把这个例子保留成"待核实传闻",不当作事实引用。 但传导链本身是可以自己验证的:上表两行倍率就是 logits_memory_bytes() 的两次调用。
还有一层来自 BPE 本身的(第6章):一个常见词被拆成三个 token、一个罕见生物医学词被逐字符切开,都不是 bug,是字节对编码在有限词表下的必然代价。词表越小、token 越粗,长尾词被切得越碎。
动手实验
目标:把本章每一条论证都落到一个能跑的断言上。
环境准备
bash
cd <仓库根>
export PYTHONPATH="$PWD/python"步骤
先跑支撑集论证,把数量级差距打印出来(
support_log10与 GPT-2 small 的参数量对比)。跑参数记账,验证表里的每一格,并确认把上下文改长,参数量不变。
对同一组 logits 扫温度,记录排序不变、尖锐度变化,以及
过低时的下溢。 对同一个概率向量做 top-k,确认被丢弃项是精确的 0 而不是"很小"。
验证因果掩码与"前缀 softmax"逐元素相等,且每行精确和为 1。
跑判分与总览:
bashpython -m pytest python/tests/test_design_tradeoffs.py -q python -m labs.run_alltext21 passed support_log10(50257,1000) = 4701.20 gpt2_small_total = 123532032判定条件:
- 支撑集与参数量相差 4600 个数量级以上;
- 参数量公式与手算逐项相等,且表中无
项; - 三个温度下
argsort相同; - 因果掩码每行和为 1,严格上三角为 0。
python/llm_core/design_tradeoffs.py 提供全部被测函数,evidence/23-design-tradeoffs-v1.json 记录本机实测值。
故障注入与预期信号
| 故障 | 预期失败信号 | 修复后证据 |
|---|---|---|
| 估计巨型 MLP 参数量时漏掉输入层宽度 | 结论差好几个数量级,却"看起来合理" | 打印 giant_mlp_parameter_log10 与真实模型 log10(total) 的差 |
| 参数记账里把上下文长度算进去 | 参数量随 | 同一套配置在两个 total 相同 |
| 把多头当成多份完整拷贝,参数按 | 参数量虚高 | |
| 因果掩码用乘 0 而不是置 | 整行变成 0/0,输出 NaN | 用 |
| 因果掩码后不重新归一化 | 第 | 与前缀 softmax 逐元素相等 |
| 温度取 0 传给 softmax | 分母为 0,输出 NaN | softmax_temperature 对 |
| 低温下期待长尾仍是"很小但非零" | 下溢把长尾变成精确 0,候选集被静默缩小 | 记录 |
| top-k 在 logits 上截断后忘记归一化 | 概率和 < 1,采样分布不合法 | 在概率向量上归零后重新缩放,sum == 1 |
| 用同一个 | 把 | 分别打印两者的 log10 |
本章验收
- 自查清单全部能答"是":
- 不看资料,完成这三道闭卷解释题:
- 给定
、 ,写出逐序列参数化与链式分解后的参数量级差,并解释为什么这个差距让分解成为唯一出路,而不只是更优选择。 - 一个模型发布说明说"我们把 attention 换成了线性注意力"。说明它落在结构—灵活性轴的哪一端、牺牲了什么、换来了什么,并指出它不属于哪一类改动(例如 FlashAttention 为什么不属于这一类)。
- 有人说"我们的模型 temperature 设成 0.05,所以候选 token 更少了"。指出这句话把两个旋钮混在了一起,并说明低温在什么条件下会产生与 top-k 相同的实际效果。
- 通过条件复核:八条论证都有对应的可执行断言,且断言值来自真实运行而不是文档抄写。缺少任一条时本章保持
gate。
论文与延伸
- CS229: Machine Learning (Spring 2026)(Instructors: Tengyu Ma, Chris Ré):本章推导骨架的来源,取自 Lecture 14: Transformers, In-Context Learning 与 Lecture 7: Neural Networks 1 (Architecture) 两讲的剪辑合集(Bilibili 转载 BV1SUbZ6dECT,UP主「莫大爷的家」,中英双语硬字幕,时长 9292 秒)。课程课件需 Stanford 邮箱登录、不公开;本章只取论证结构,例子与数值全部重算。
- Attention Is All You Need(Vaswani 等,2017):阶段三与阶段四的原始定义。
- Training Compute-Optimal Large Language Models(Hoffmann 等,2022):阶段二那条权衡在规模轴上的定量版本——给定算力,参数与数据如何配比才最优。
- 索引:必读论文
实验与参考
- 关联概念:术语表 - 归纳偏置 / 因果掩码 / 温度采样
- 代码定位:
python/llm_core/design_tradeoffs.py、python/tests/test_design_tradeoffs.py
前端/Agent 迁移
这条轴在 Agent 系统里同样成立,而且更好用,因为它便宜得多。限制工具集就是往"结构"方向挪,放开重试与多路径搜索就是往"灵活性"方向挪。两端都走到头的失败模式是同一个:预设太少时 Agent 反复绕同一个弯,预设太多时 Agent 根本无事可做、只是在填空。第15章的 typed tools 与 policy 层是这一轴在工程上的落点。
资源 / 成本 / 隐私
本地 CPU/NumPy 即可,无网络、无 GPU。课程视频仅作公开课引用,不随仓库分发;本章所有数值均为本机实测或由实测公式推导。固定 seed 的 toy 例子不含个人数据,预计 gross cost 为 0。
Evidence
仓库当前机器证据(只读快照)
evidence/module-manifest-v1.json 中 23.evidence 指向当前文件:evidence/23-design-tradeoffs-v1.json。这是当前 checkout 的脱敏机器运行记录,只覆盖该 JSON 记录的命令、指标、产物和已知失败;它不是学习者提交,也不能推出学习者已完成本章。
学习者提交模板(待填写,不是当前机器证据)
复制下面模板并填写自己的真实运行结果。所有 <...> 都是未填写状态;actual 和 artifacts 尤其不能被当作已运行或已通过。artifacts 必须替换为本次提交中真实存在的仓库相对路径。
yaml
schema: learn-llm.evidence.v1
module: 23-design-tradeoffs
commit: <learner-commit-sha>
verified_at: <iso-date>
environment: <sanitized-python-device>
seed: 0
commands:
- PYTHONPATH=python python -m pytest python/tests/test_design_tradeoffs.py -q
- PYTHONPATH=python python <learner-controlled-tradeoff-run>
metrics:
- name: support_log10_gpt2_vocab_1000_ctx
expected: <versioned-threshold>
actual: <recorded-value>
- name: gpt2_small_parameter_total
expected: <versioned-threshold>
actual: <recorded-value>
artifacts:
- <learner-repo-relative-artifact-path>
cost:
gross_usd: 0
credit_usd: 0
licenses:
- source: <source>
version: <version>
license: <license>
attribution: <attribution>
redistribution: <redistribution>
known_failures:
- <sanitized-failure-or-none>下一步
回到 第5章 · 字符语言模型与概率目标 重读"阶段二:Bigram 计数表就是模型"那一节,然后用本章阶段二的问题问它一次:一个参数按
也可以直接进 参考页 · 课程速查表,把本章的八条命题对回你日常读到的模型发布说明。