Skip to content

第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 邮箱登录、不公开,因此本章只取论证结构,不复述其幻灯片。原文已逐段重述、重新推导并换成可运行的量,引用见论文与延伸。讲师在多处明确表示"没有很简单的办法讲清楚",本章保留这些不确定标注,不把它们抹平成结论。

本章目标 ​

学完后你能做到:

  1. 用 VT 支撑集规模解释自回归分解为什么不是约定而是必然,并说出数量级。
  2. 复述结构—灵活性权衡:巨型 MLP 在能力上可行、在算力上不可行,取平均则预设过多而无参数。
  3. 区分 Transformer 的两类算子——逐位置的 MLP 与跨位置的 attention——并说明参数量为何与序列长度无关。
  4. 说明多头为什么是同一算子的多组权重而不是多份拷贝,以及它为什么不改变参数量。
  5. 把因果掩码表述为"删除 + 重归一化",并说明它与直接加 −∞ 的关系。
  6. 把温度与 top-k 表述为同一个概率向量上的两种手术,并各自说明改了什么、没改什么。
  7. 把分词粒度变化换算成 token 数、logits 显存与费用。

阶段一:VT——为什么必须分解 ​

第5章把 P(x1:T)=∏tP(xt∣x<t) 当成定义介绍。这一章问它从哪来。

先看不做分解会怎样。词表大小 V、序列长度 T,长度为 T 的 token 序列一共有

|S|=VT

种。把概率直接定义在这张表上,每个序列一个自由参数——这是最诚实的写法,也正好是走不通的写法。取工业级词表 V=50257(GPT-2 的词表大小,也是本章后面参数记账统一使用的口径)配一个 T=1000 的上下文:

text
support_log10(50257, 1000) = 4701.20   → 约 10^4701 个序列

即使把上下文缩到 32 个 token,support_log10(50257, 32) = 150.44,仍然是 10150 量级。参照物是 GPT-2 small 形状的模型,按第8章的无偏置、embedding 共享口径算是 123,532,032 个参数,约 108.09:

104701⏟逐序列一个参数≫108.09⏟真实模型

差着四千多个数量级。这个差距不是"训练慢一点",是连表示都放不下。

于是分解不是设计偏好,是唯一出路:链式法则把一张 VT 的表换成 T 张 V 的表。每张表的支撑集只有 V,而 V 就是词表大小——恰好是模型输出层天然能吐出来的宽度。输出层本来就只能给 V 个数,而分解之后它需要的恰好就是 V 个,两者严丝合缝,这是第8章输出投影被写成 (V,d) 而不是别的原因。

代价写在明面上:分解之后 P(xt∣x<t) 不再独立,t 处的分布依赖前 t−1 个 token。这就是"自回归"的全部含义,也是第8章因果掩码存在的理由。

python/llm_core/design_tradeoffs.py 的 sequence_support_log10() 与 chain_rule_nll() 把这两步都做成了可对拍的函数。

阶段二:巨型 MLP 假说——结构与灵活性的取舍 ​

支撑集论证只解释了"必须分解",没有解释"为什么用 Transformer 而不是别的东西"。补上这一步,需要先接受一个不舒服的前提。

假设算力和数据都无限。那么正确的做法是把整个序列喂给一个足够宽的 MLP,让它自己去学所有位置之间的关系。课程讲者的说法是:给定足够数据(不至于过拟合)和足够算力,存在某个 MLP 能真正解决这个任务。这不是定理,是经验判断,但它指对了一件重要的事——这个任务本身没有捷径,问题只在于代价。

代价有多大:这个 MLP 的输入层宽度是 VT,隐藏层哪怕只有 4096 宽:

text
giant_mlp_parameter_log10(50257, 1000, hidden=4096) = 4704.81

比阶段一的支撑集还大三个数量级,而且这只是第一层。拿通用逼近定理来对照会看得更清楚:定理说的是"输入维度固定时,隐层足够宽就能逼近任意连续函数",它从来没有替你解决输入维度本身是 VT 的问题。理论可行与工程可行之间隔着四千多个数量级。

于是只能往回退:人为规定一些结构,把复杂度压下来。但压过头同样坏:

  • 规定到极致:直接取所有位置的向量平均。计算量与 T 成正比,但没有任何可学参数——你把答案写进了结构里,模型无事可学。
  • 完全放开:上一段那个 VT 输入层的巨型 MLP。参数无限多,算不动。
  • 中间某处:Transformer 所在的位置。

这就是结构—灵活性权衡:你引入多少结构,就等于替模型做掉多少决定;做掉的越多,算得越动,但留给数据去发现的空间越小。Transformer 不是唯一解,是这个连续谱上的一个具体取值。

这条轴是理解新模型的第一把钥匙。MoE(第12章)往"灵活"方向挪:把 FFN 拆成专家、用路由选少数几个,参数总量涨而每个 token 的计算量不涨。线性注意力、状态空间模型往"便宜"方向挪:牺牲精确的两两交互,换 T 的线性复杂度。FlashAttention(第10章)不在这条轴上——它一个参数都没动,只改了同一次计算的访存顺序。

判断一次架构改动落在轴上的哪一端,比记住它的超参数更有用。

阶段三:逐位置与跨位置——两条算子线 ​

一旦决定规定结构,还剩一个具体问题:结构规定成什么样。Transformer 的答案是把每一层劈成两半,职责分得很干净。

MLP 是逐位置的。 它对第 t 个位置做什么,和第 t+1 个位置完全无关——位置之间没有任何依赖。这个性质带来两个直接后果:可以完美并行(所有位置同时算),而且参数量与 T 无关。

attention 是唯一跨位置的算子。 整个 Transformer 里,凡是让第 t 个位置看到第 s≠t 个位置的操作,都发生在 attention 里。MLP、LayerNorm、embedding 查表、输出投影,没有一个是跨位置的。

这两条线交替出现,构成第8章的 block:

"参数量与 T 无关"是一条可以直接验证的命题。把 d=768、L=12、FFN =3072 代进 transformer_parameter_count():

组成公式值
embedding(输入查表)V⋅d38,597,376
每层 attention4d22,359,296
每层 FFN2d⋅4d4,718,592
每层合计4d2+2d⋅4d7,077,888
输出头(embedding 共享时)00
总计(12 层)Vd+12⋅(4d2+2d⋅4d)123,532,032

表里没有一个 T。把上下文从 1000 加到 100,000,参数量一动不动——变的只是运行时激活值和 O(T2) 的注意力矩阵。这也是第10章里"训练能并行、解码只能逐步走"的结构性来源:训练时所有位置同时算,推理时第 t 步必须等第 t−1 步的 token 落地。

阶段四:多头是同一算子的多组权重 ​

"多头"这个名字容易让人误解成"并行跑了 H 份 attention"。不是。

每一份所谓"拷贝",和原版共享完全相同的输入与完全相同的输出格式,唯一区别是 WQ、WK、WV 三组权重不同。所以更准确的说法是:attention 这一个算子,用 H 组不同的权重各跑了一遍,把 H 份结果拼起来。

这个理解有一个立刻可验证的推论:头数不改变参数量。把 d=768 切成 H 份,每份宽度 d/H,attention 的总参数是

H×4⋅dH⋅d=4d2

与 H 无关。python/tests/test_design_tradeoffs.py::test_head_count_changes_no_parameter_budget 对 H∈{1,8,64} 逐个对拍,都落在 2,359,296。

那么多头买到了什么?买入的是同一组输入的不同投影方式。不同的 WQ 会问出不同的问题:有的头在找"上一个词",有的头在找"所有形容词",有的头几乎只看自己。课程里的说法是"没有人真正清楚它到底怎么运作,它最终就是有效"——这是一个诚实的经验性判断,不是定理。本章保留它的不确定性。

顺带一个第8章没细说的量级:头数本身很小。课程里给的估计是,百亿(1010)参数级别的模型,头数可以达到 100 左右,具体多少还取决于模型规模。头少,是因为每个头分到的宽度小;而头多并不会让总参数变多——只会让每个头看得更窄。

阶段五:因果掩码是删除加重归一化 ​

第8章把因果掩码实现成"未来位置加 −∞"。讲义层面还有一步没写,而这个版本能解释为什么不能省。

第 t 个位置只能看 0…t。朴素想法是把 t 之后的注意力分数设为 −∞。这确实可行,softmax 会自动把它们压成 0,然后剩下的项重新归一化——数值上是对的。

但说"掩码"和说"删除",指向的机制并不完全一样:

  • 掩码的说法:先在全部 T 个 key 位置上定义一个分布,再把不可见的那几项的概率质量转走,剩下的重新缩放。
  • 删除的说法:分布的定义域根本就是 {0,…,t} 这个子集,不可见的位置从头到尾没参与运算。

两者数值完全相同(causal_masked_attention() 与"对前缀单独做 softmax"逐元素相等,见 test_causal_mask_equals_softmax_over_the_visible_prefix),但第二种说法能让你立刻回答一个真问题:第 t 行的概率,是在 T 个位置上归一化,还是在 t+1 个位置上归一化? 答案是后者。第 0 行的输出恒等于 [1,0,0,…]——只有它自己,概率 1。这个观察在 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 [2,1,−1](与第5章同一组数),实测分布:

T分布说明
0.1[0.999955, 4.54×10−5, 9.36×10−14]尾部压到十的十三次方量级
1.0[0.705385, 0.259496, 0.035119]模型自身分布
10.0[0.377978, 0.342009, 0.280013]明显变平
1000.0[0.333778, 0.333444, 0.332778]趋近均匀 1/3

关键在于排序恒定不变。四个温度下 argsort 都是 [0, 1, 2]。温度只缩放 logits 之间的差距,不重排名次,所以它改变的是"分布有多尖",不是"谁排第一"。想改谁排第一,只有 top-k 和 top-p 能做到。

T=0.1 那一行值得停下来看一眼:第三项是 9.36×10−14,按任何实用标准都当零处理,但它是一个非零的浮点数。温度没有截断点,它只是连续地压低尾部——所以把低温当成 top-k 用是安全的做法,但它的行为和 top-k 有一处本质差别,见下一段。

top-k 改分布的支撑集。 top_k_renormalize() 把 [0.4,0.3,0.15,0.1,0.05] 取 k=2:

text
[0.571429, 0.428571, 0.0, 0.0, 0.0]

注意它是在概率向量上做的手术:先归零,再把幸存项重新缩放到和为 1。所以 top-k 之后被丢弃 token 的概率是构造出来的精确零,而温度再低也只是把概率压到 10−13 这种量级。两者在采样结果上常常难以区分,但在复盘和审计时是两回事:温度的效果随 T 连续变化,top-k 的效果由整数 k 决定。

两者的共同点是第5章已经建立的结论:它们都在 logits 进入训练损失之前起作用,都不改权重。训练目标和它们无关。

阶段七:分词粒度如何变成账单 ​

前面所有取舍都在模型内部。这一节走一个已经在系统边界上的量:分词器。

阶段一说过,词表大小 V 决定输出层宽度,也决定每个 token 要付多少 logits 内存:

logits 字节数=B×T×V×bytes

代入实测(batch 1,FP32):

配置TVlogits 显存
基线100050257191.7 MiB
上下文 +50%150050257287.6 MiB(1.500×)
词表 128k1000128256489.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"

步骤 ​

  1. 先跑支撑集论证,把数量级差距打印出来(support_log10 与 GPT-2 small 的参数量对比)。

  2. 跑参数记账,验证表里的每一格,并确认把上下文改长,参数量不变。

  3. 对同一组 logits 扫温度,记录排序不变、尖锐度变化,以及 T 过低时的下溢。

  4. 对同一个概率向量做 top-k,确认被丢弃项是精确的 0 而不是"很小"。

  5. 验证因果掩码与"前缀 softmax"逐元素相等,且每行精确和为 1。

  6. 跑判分与总览:

    bash
    python -m pytest python/tests/test_design_tradeoffs.py -q
    python -m labs.run_all
    text
    21 passed
    support_log10(50257,1000) = 4701.20
    gpt2_small_total = 123532032

    判定条件:

    • 支撑集与参数量相差 4600 个数量级以上;
    • 参数量公式与手算逐项相等,且表中无 T 项;
    • 三个温度下 argsort 相同;
    • 因果掩码每行和为 1,严格上三角为 0。

python/llm_core/design_tradeoffs.py 提供全部被测函数,evidence/23-design-tradeoffs-v1.json 记录本机实测值。

故障注入与预期信号 ​

故障预期失败信号修复后证据
估计巨型 MLP 参数量时漏掉输入层宽度 VT结论差好几个数量级,却"看起来合理"打印 giant_mlp_parameter_log10 与真实模型 log10(total) 的差
参数记账里把上下文长度算进去参数量随 T 变化,与"训练/推理成本"混淆同一套配置在两个 T 下 total 相同
把多头当成多份完整拷贝,参数按 H 倍算参数量虚高 H 倍H∈{1,8,64} 都等于 4d2
因果掩码用乘 0 而不是置 −∞整行变成 0/0,输出 NaN用 −∞;每行 softmax 后和为 1
因果掩码后不重新归一化第 t 行概率和 < 1,等价于"置信度随位置衰减"与前缀 softmax 逐元素相等
温度取 0 传给 softmax分母为 0,输出 NaNsoftmax_temperature 对 T≤0 直接抛错;greedy 是另一条路径
低温下期待长尾仍是"很小但非零"下溢把长尾变成精确 0,候选集被静默缩小记录 T=0.1 时第三项为 0.0
top-k 在 logits 上截断后忘记归一化概率和 < 1,采样分布不合法在概率向量上归零后重新缩放,sum == 1
用同一个 V 同时算序列数和上下文长度把 VT 误读成 V⋅T分别打印两者的 log10

本章验收 ​

  1. 自查清单全部能答"是":
  1. 不看资料,完成这三道闭卷解释题:
  • 给定 V=50257、T=1000,写出逐序列参数化与链式分解后的参数量级差,并解释为什么这个差距让分解成为唯一出路,而不只是更优选择。
  • 一个模型发布说明说"我们把 attention 换成了线性注意力"。说明它落在结构—灵活性轴的哪一端、牺牲了什么、换来了什么,并指出它不属于哪一类改动(例如 FlashAttention 为什么不属于这一类)。
  • 有人说"我们的模型 temperature 设成 0.05,所以候选 token 更少了"。指出这句话把两个旋钮混在了一起,并说明低温在什么条件下会产生与 top-k 相同的实际效果。
  1. 通过条件复核:八条论证都有对应的可执行断言,且断言值来自真实运行而不是文档抄写。缺少任一条时本章保持 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):阶段二那条权衡在规模轴上的定量版本——给定算力,参数与数据如何配比才最优。
  • 索引:必读论文

实验与参考 ​

前端/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 计数表就是模型"那一节,然后用本章阶段二的问题问它一次:一个参数按 Vn 增长的计数表,在"结构—灵活性"这条轴上落在哪一端,它预设了多少、留下了多少。答案是本章这条轴最早的一次实践——把 n 从 3 提到 1000 的那一天,计数表就再也走不动了,于是有了 embedding(第5章阶段四)和 Transformer(第8章)。

也可以直接进 参考页 · 课程速查表,把本章的八条命题对回你日常读到的模型发布说明。

私有学习站 · 原理从零构建 · 勿提交个人隐私或密钥