对应 Maps to:CMU 11-868 / 11-968 Large Language Model Systems(Spring 2026,Lei Li),Lecture 1 · Mon 1/12 · Introduction to LLM。 阅读材料:原课第 1 讲没有指定阅读材料。本讲涉及的公式出自:Brown et al. (2020) Language Models are Few-Shot Learners(GPT-3);Kaplan et al. (2020) Scaling Laws for Neural Language Models;Hoffmann et al. (2022) Training Compute-Optimal Large Language Models(Chinchilla);Rajbhandari et al. (2020) ZeRO(16 字节/参数的记账);Korthikanti et al. (2022) Reducing Activation Recomputation in Large Transformer Models(激活值公式)。 依据说明:本讲以原课第 1 讲公开的 slides(llmsys-01-intro.pdf,51 页)为主线:规模曲线与学习目标、LLM 的能力、数学基础(下一个 token 的概率、三类语言模型、学习框架、交叉熵与困惑度、评估指标)、LLM 系统的挑战(核心问题、抽象与权衡、底层算子、不同层次的挑战、协同设计、计算 vs 数据搬运、编程模型)和课程安排。slides 把”训练一个 100B 模型要多少资源”列为学习目标,但没有在第 1 讲里算出来;§5 的完整估算、§0.6 的”城市语言”和全部实验都是本讲义自己设计的,不代表课堂上讲了什么。 配套代码 Code:
code/lecture01/(5 个脚本 + 5 个模块;01只用标准库,其余用 numpy 和 PyTorch CPU 版;03第一次约 75 秒,其余每个脚本 1–5 秒) 预计阅读时间:4–5 小时(含运行代码)
0. 本讲导航 · How to read this lecture
0.1 学习目标 Learning goals
学完这一讲,你应该能够:
- 说出 LLM 规模增长的大致轨迹,并解释为什么”规模”让系统问题变成了 LLM 研究的核心问题之一。
- 从系统负载的角度看 LLM 的各种能力:翻译、对话、少样本提示、推理、写代码并调用工具,分别对输入长度、输出长度、调用次数和时延提出了什么要求。
- 写出语言模型的数学定义(下一个 token 的条件概率、链式法则),区分 encoder-only、encoder-decoder、decoder-only 三类模型,并能用实验说明:它们的差别只在注意力掩码,系统代价却天差地别(为什么只有因果模型能用 KV 缓存)。
- 解释交叉熵损失和困惑度,知道”理论最优困惑度”的含义;能区分 n-gram 的近似误差和估计误差,理解神经语言模型为什么能同时利用长上下文和有限数据。
- 说出 LLM 系统的核心问题,把一个 Transformer 层拆成矩阵乘法、逐元素映射、归约、数据搬运四类底层运算,并用算术强度判断一个运算是计算受限还是访存受限。
- 用几条公式(参数量 ≈ 12·L·d²、训练 FLOPs ≈ 6ND、训练显存 ≈ 16 字节 × N、KV 缓存 = 2·L·头数·头维度·字节数)估算训练和服务一个 100B 模型所需的 GPU、时间、钱、显存和带宽,并知道每条公式在什么情况下不准。
0.2 先修与本讲范围 Prerequisites & scope
原课的先修是线性代数、微积分、概率统计,会 Python 和 C / C++ / Java 之一,最好学过机器学习(不强制)。本讲是导论,需要的背景都就地补上,细节留给后面的讲次。
| 默认你已经会 Assumed | 本讲只给直觉,后面再展开 Previewed only |
|---|---|
| 条件概率、期望、对数 | GPU 的结构与 CUDA 编程(第 2–4 讲) |
| 矩阵乘法;导数与链式法则 | 自动微分与深度学习框架(第 5 讲) |
| Python 基础(不熟悉的写法代码注释里都有解释) | Transformer 的每个部件(第 6 讲);LLaMA 式结构、GQA(第 7 讲) |
| 知道”训练一个模型就是最小化损失”的大意(不熟悉的话,SYLLABUS 里规划了前置讲 P3 来补) | 分布式训练、ZeRO、量化、FlashAttention、服务系统(第 14–27 讲) |
0.3 本讲地图 Map
§1 为什么要专门学 LLM 系统:规模曲线;三个学习目标;总题目"100B 模型要多少资源"
§2 LLM 能做什么——以及每种能力给系统带来的负载
§3 数学基础:下一个 token 的概率;链式法则;三类语言模型;学习框架; ← 代码 01、02、03
交叉熵与困惑度;评估指标;规模与数据
§4 LLM 系统的挑战:核心问题与指标;抽象与权衡;底层算子;不同层次的挑战; ← 代码 05
协同设计;计算 vs 数据搬运;编程模型
§5 回答总题目:参数量、FLOPs、时间、钱、显存、通信、推理 ← 代码 04
§6 课程地图 §7 怎样学这门课
§8 小结 §9 常见误区 §10 练习 §11 延伸阅读 §12 下一讲预告
建议的阅读顺序:先读 §1–§2(不需要代码);读 §3 时依次运行 01、02、03;读 §4 时运行 05;最后读 §5 并运行 04。§5 是本讲的”总账”,前面每一节都会为它准备一个零件。
0.4 记号 Notation(全课统一)
| 记号 | 意思 |
|---|---|
| ,, | 第 个 token;长度为 的序列; 之前的所有 token(“前文”) |
| , | 词表及其大小 |
| 参数为 的模型给出的概率 | |
| 模型参数量(parameters) | |
| 训练用的 token 数(data) | |
| ,,, | 层数、隐藏维度(d_model)、注意力头数、每个头的维度() |
| 序列长度 / 上下文长度 | |
| , | 批大小(batch size) |
| FLOPs | 浮点运算次数(一次乘法或一次加法各算 1 次) |
| FLOP/s | 每秒浮点运算次数(算力) |
| 算术强度 arithmetic intensity | 一个运算的 FLOPs ÷ 它必须读写的字节数,单位 FLOP/字节 |
| MFU | Model FLOPs Utilization:实际达到的有效算力 ÷ 硬件峰值算力 |
| nat | 以 为底的信息量单位(以 2 为底时叫 bit);1 nat ≈ 1.443 bit |
| KB / MB / GB / TB | 以 1000 为进位(、、、 字节);KiB / MiB / GiB 以 1024 为进位 |
0.5 关于这门课 · About 11-868
是什么。 CMU 语言技术研究所(LTI)的研究生课,2024 年春季第一次开,2025、2026 年春季都有课程主页(Spring 2026 的 slides 里还提到了一个秋季学期的版本)。主讲 Lei Li 的背景很能说明这门课的味道:他在 CMU 读完博士后在工业界做了很多年大规模 NLP 系统,主导过字节跳动的机器翻译系统 VolcTrans 和写作机器人 Xiaomingbot(slides 上写的是服务超过十亿用户),做过 Transformer 加速库 LightSeq(第 10–11 讲的主角),2023 年回到 CMU 任教;他的 VOLT(第 8 讲)获得了 ACL 2021 最佳论文。所以这门课一半是 NLP,一半是系统,而且非常强调”能动手实现”。
学什么。 slides 列出三个学习目标:
- 理解现代 LLM 系统的关键技术——例如,训练一个 100B 模型需要多少资源?
- 工程能力:亲手实现 LLM 系统的关键部件——快速的 CUDA 内核、可扩展的训练系统、高效的推理。
- 创新:发现机会,解决 LLM 系统研究里新的关键挑战。
不适合谁。 slides 很坦率:如果你想学怎样搭深度学习模型、怎样用 PyTorch,去上 11-685/11-785(深度学习导论);如果你只想学怎样使用 LLM、对底层系统不感兴趣,去上 11-667;如果你没法来上课、完成作业和项目,也别选——这门课要求真的写系统代码。
怎么考核(Spring 2026):5 次必做 + 2 次选做的编程作业共 44%(选做另加 5%),课堂小测 10%,参与 2%,小组项目 44%(开题 2%、中期 2%、展示 20%、终期报告 20%)。作业个人完成,主线是从零扩展一个教学用深度学习框架 miniTorch——写 CUDA 内核(HW1)、自动微分(HW2)、Transformer(HW3)、融合内核(HW4),之后转向分布式训练、DeepSpeed、SGLang 和 RLHF(HW5–7);要写 Python 和 C++ / CUDA,事先不要求会 CUDA。项目 2–3 人一组,二选一:给 miniTorch 实现一篇 LLM 系统论文里的组件(不许调用外部的 PyTorch / TensorFlow 代码),或者做一个以投稿系统 / ML 会议为目标的研究项目;最后在课上做海报展示,最好带演示。算力由匹兹堡超算中心(PSC Bridges-2)提供。
原课作业 vs 本讲义代码。 原课作业在 GPU 集群上写 CUDA。本讲义假设你只有一台普通电脑:GPU 内核用 Python 写的模拟器执行并逐项计数(第 2 讲起),性能用公开规格和 roofline 模型估算,多卡用”列表里的多个设备”模拟,所有模型都小到能在 CPU 上几十秒训练完。详见 SYLLABUS.md 的 §0.4。
0.6 本讲的玩具语料 · The city language
讲语言模型时,我们总想回答一个问题:这个模型离”最好”还差多远? 对真实英语这个问题没有答案,因为没人知道英语的真实分布。所以代码 city_corpus.py 造了一门小语言,它的每一句话都由几次”掷骰子”生成,骰子的概率都写在代码里。四种句型:
| 句型 | 模板 | 例子 | 概率 |
|---|---|---|---|
| T1 | <城市> is a city of <特色> . | pittsburgh is a city of bridges . | 0.25 |
| T2 | <城市> has very nice <好东西> . | santa barbara has very nice beaches . | 0.20 |
| T3 | the <人群> from <城市> who <活动> <时间> love the <特色> . | the students from pittsburgh who swim at night love the bridges . | 0.35 |
| T4 | in <城市> it is often <天气> . | in seattle it is often rainy . | 0.20 |
10 个城市各有自己的”特色""好东西""天气”分布(例如 pittsburgh 的特色:bridges 0.60、rivers 0.20、steel 0.12、hills 0.06、corn 0.02——这正是 slides 上 “Pittsburgh is a city of ___” 的例子)。T3 是专门设计的长程依赖:句末的特色只取决于 7–8 个 token 之前的城市,中间隔着人群、活动、时间这些和答案无关的”填充物”。这些概率都是为教学编的,不是真实统计。
因为一切都是已知的,我们能精确算出:任何句子的概率、任何前缀之后的下一个 token 分布、测试集上任何模型都不可能超过的”理论最优困惑度”。运行 python3 city_corpus.py 可以看到这门语言的概况:9127 个不同的句子(其中 9000 个是 T3),词表 80 个 token,熵率 0.7506 nat/token,理论最优困惑度 2.118。
1. 为什么要专门学”LLM 系统” · Why LLM systems
1.1 一条陡峭的曲线
slides 的第二、三页是两张时间线:从 2000 年前后的神经网络语言模型,到 2018 年的 BERT,再到 2020–2024 年的一系列大模型;以及一张对数坐标的”LLM 规模”图。把图上的模型和它们公开的参数量列出来:
| 年份 | 模型 | 参数量 | 备注 |
|---|---|---|---|
| 2017 | Transformer(原始论文) | base 约 65M,big 约 213M | 机器翻译 |
| 2018 | GPT-1 | 0.117B | 第一个”预训练 + 微调”的 GPT |
| 2019 | GPT-2 | 1.5B | |
| 2020 | GPT-3 | 175B | 少样本提示、上下文学习 |
| 2021 | Gopher(DeepMind) | 280B | |
| 2022 | PaLM(Google) | 540B | |
| 2023 | GPT-4 | 未公开 | 原课第 14 讲的同一张图上标的是 1.8T,这是外界流传的估计 |
| 2024 | LLaMA-3.1 | 405B | 开放权重里最大的稠密模型之一 |
| 2024 | DeepSeek-V3 | 671B(每个 token 激活 37B) | 混合专家(MoE) |
| 2025 | Qwen3 | 235B(激活 22B) | MoE |
| 2025 | Kimi K2 | 约 1T(激活 32B) | MoE |
两个趋势一眼就能看出来:
- 七年涨了一万倍。 从 GPT-1 的 1 亿参数到万亿参数。同一时期,单块 GPU 的显存只从十几 GB 涨到一两百 GB(V100 16/32 GB、A100 80 GB、H100 80 GB、B200 约 192 GB),16 位精度的峰值算力涨了十几倍。模型增长的速度远远超过了单块硬件增长的速度——差出来的部分,只能靠系统来补:多卡并行、显存优化、压缩、更好的内核。
- 2024 年以后,最大的模型几乎都是 MoE。 “总参数”和”每个 token 实际用到的参数”分开了:DeepSeek-V3 有 671B 参数,但每个 token 只激活 37B。这本身就是一个”为了系统而改模型”的例子——在计算量几乎不变的前提下增加容量,代价是更复杂的通信和负载均衡(第 17 讲)。
1.2 本课的总题目
原课第一个学习目标后面跟着一个具体问题:训练一个 1000 亿参数的模型,需要多少资源?
这个问题看起来只要一个数字,其实要拆成一串问题:一个 token 要算多少次乘法?一块 GPU 一秒能做多少次?要多少块 GPU、多少天、多少钱?显存装得下吗?GPU 之间要传多少数据、传得过来吗?训练完了,服务一个用户要多少显存、一秒能生成多少个 token?
本讲 §5 会用几条公式把这串问题全部回答一遍(结果是:约 1.2×10²⁴ 次浮点运算、1024 块 H100 一个多月、上百万美元、1.6 TB 的训练状态、每步 200 GB 的梯度通信……)。更重要的是,每一条公式都对应着这门课后面的一个单元——这门课的结构,就是这本账的结构(§6)。
1.3 为什么系统成了 LLM 研究的核心
slides 后面有一句话可以概括这门课的立场:“Scaling is all you need”——既要往上扩(scale up),也要往下缩(scale down)。
- 往上扩:更大的模型、更多的数据、更长的上下文。每往上一步,都会撞上一堵新的墙:一块卡放不下(→ 模型并行、ZeRO)、算不完(→ 数据并行、更快的内核)、传不动(→ 通信优化)。
- 往下缩:同样的能力用更少的资源。量化(第 19–20 讲)、参数高效微调(第 23 讲)、更好的推理系统(第 22–27 讲)都属于这一类。一个模型训练一次,却要被调用几十亿次,推理的成本最终往往超过训练。
在这两个方向上,决定成败的常常不是”模型设计得多巧妙”,而是”系统能不能把硬件用满”。§5.8 会看到,同样的模型和数据,MFU 从 20% 提到 60%,训练时间能差 3 倍。
2. LLM 能做什么——以及每种能力给系统带来的负载 · Capabilities
2.1 一组例子
slides 用了一连串例子展示 LLM 的能力(下面是转述):
- 写宣传文案:让模型参考课程主页,写一条宣传这门课的”爆款推文”。
- 翻译:传统机器翻译系统 vs ChatGPT / GPT-4 的对比。
- 模仿一个人聊天:一个 CMU 学生项目(WhatsApp-Llama)用自己的聊天记录微调模型,大约 10 段聊天记录、几个小时的训练,就能生成模仿本人风格的回复。
- 按例子改写风格:给几个”把句子改成尤达大师的说话方式”的例子,模型就能改写一个新句子——这是上下文学习(in-context learning):不改参数,只靠提示里的例子学会新任务。
- 常识推理:Winograd 式的代词消解(“Steven 对 Robert 做猪肉很反感,因为 ___ 是素食者”,答案是 Steven)。
- 数学推理:一道小学应用题(网球的个数),模型一步一步写出推理过程再给出答案。
- 考试:一项研究发现 ChatGPT 在沃顿商学院的 MBA 运营管理考试里能拿到 B− 到 B。
- 写代码并执行:一道工厂流程题(七道工序各有产能,问瓶颈在哪、整条线产能多少)。模型先写出分析,再生成一段 Python 代码、交给 Python 执行器运行,用运行结果来验证答案;另一个例子是让模型写一个找”山峰”位置的函数。
- 画图:按描述画一幅”羊驼沿着 CMU 校园里的雕塑《Walking to the Sky》往上爬”的图。
这些例子在原课里是为了说明”LLM 已经很强、很通用”。但对一门系统课来说,更值得问的是:每一种用法,对系统提出了什么要求?
2.2 换一个角度:能力 → 负载
一个 LLM 请求的”形状”可以用四个量刻画:输入多长(提示词,决定预填充 prefill 的计算量)、输出多长(决定逐 token 解码 decode 的步数)、一个任务要调用模型几次、用户能等多久。
| 用法 | 输入 | 输出 | 调用次数 | 时延要求 | 主要的系统挑战 | 相关讲次 |
|---|---|---|---|---|---|---|
| 翻译、改写 | 短 | 短 | 1 | 中 | 大批量吞吐;把很多短请求凑成批 | 9、22 |
| 多轮对话 | 越聊越长 | 中 | 每轮 1 次 | 高(要”秒回”) | 历史对话每轮都要重新输入 → 复用前缀的 KV 缓存 | 22、24、27 |
| 少样本提示 | 长(例子很多) | 短 | 1 | 中 | 预填充很重;很多请求共用同一段例子 → 前缀缓存 | 21、22 |
| 一步一步推理 | 中 | 很长 | 1 | 低–中 | 解码步数多;每步都要读一遍全部权重 → 访存受限 | 9、19、24 |
| 写代码 + 调用工具 / 智能体 | 越来越长 | 中 | 很多次 | 高 | 一次任务是一串调用;中间结果和上下文要反复使用 | 22、26、27 |
| 用自己的数据微调 | — | — | — | — | 训练显存;只训练少量参数 | 18、23 |
| 画图 | 短 | 图像 | 1 | 中 | 另一类模型(扩散模型),不是本课重点 | — |
这张表不是原课 slides 上的,但它解释了这门课后半部分的选题:几乎每一讲服务系统的论文,都是在针对上表的某一行做优化。
2.3 一个贯穿全课的比喻:瓶颈
slides 里那道工厂题值得多看一眼。七道工序的产能(吨/小时)分别是 60、55、56(14 吨 ÷ 0.25 小时)、50(200 吨 ÷ 4 小时)、62.5、59、82.5(3 台 × 27.5)。整条生产线的产能等于最慢的那一道工序:50 吨/小时,瓶颈是第二个反应器。
这正是系统优化的第一原则:先找瓶颈。把一道不是瓶颈的工序加快十倍,整条线一点都不会变快。05 号脚本第 4 部分把同样的道理用在一个训练步上:如果”计算”要 6.0 秒、“梯度通信”要 8.1 秒,那么不论把计算优化得多快,只要两者不能重叠,一步至少要 8.1 秒。后面几乎每一讲都会先问:这里的瓶颈是计算、访存、通信,还是调度?
3. 数学基础 · Mathematical Foundations
3.1 语言模型 = 下一个 token 的概率模型
直觉。 给你半句话 “Pittsburgh is a city of ___“,你脑子里会冒出几个候选:bridges 很可能,rivers 也说得通,corn 就有点奇怪但不是不可能。语言模型做的就是这件事,只不过它给出的不是一个答案,而是整个词表上的一个概率分布:
为什么说 “token” 而不是 “词”? 模型的词表不是字典里的词,而是分词器(tokenizer)切出来的片段:常见的词是一个 token,罕见的词会被切成好几个子词,空格、标点、数字也各有 token。城市语言里我们偷懒,直接把一个词当一个 token(“new york” 是两个 token)。分词的方式会影响序列长度、词表大小、嵌入矩阵的参数量和 softmax 的计算量——这是第 8 讲的内容。本讲说”词”的地方,严格地说都应该是”token”。
slides 举了两个例子:“Santa Barbara has very nice ___“(beach 0.5、weather 0.4、snow 0.01)和 “Pittsburgh is a city of ___“(bridges 0.6、corn 0.02)。我们的城市语言里嵌入了同样的概率表。01 号脚本第 2 部分精确算出了几个前缀之后的真实分布:
| 前缀 | 真实的下一个 token 分布(前几项) |
|---|---|
| pittsburgh is a city of | bridges 0.600 · rivers 0.200 · steel 0.120 · hills 0.060 · corn 0.020 |
| santa barbara has very nice | beaches 0.500 · weather 0.400 · views 0.060 · food 0.030 · snow 0.010 |
| the students from pittsburgh who swim at night love the | bridges 0.600 · rivers 0.200 · steel 0.120 · hills 0.060 · corn 0.020 |
| the chefs from seattle who read on weekends love the | coffee 0.500 · rain 0.250 · music 0.150 · mountains 0.070 · bridges 0.030 |
| 只看最近两个 token “love the”(最好的 trigram) | bridges 0.150 · beaches 0.098 · music 0.083 · coffee 0.081 · museums 0.060 · parks 0.052 |
两点值得注意:
- 答案是分布,不是一个词。 corn 只有 0.02 的概率,但它不是 0。“按分布采样”时偶尔会生成它——这就是为什么同一个提示词问两次,LLM 会给出不同的回答。
- 看多长的上下文,决定了能答多好。 同样以 “love the” 结尾,pittsburgh 的人最可能爱 bridges(0.60),seattle 的人最可能爱 coffee(0.50)。如果只看最近两个 token,就只能给出所有城市的平均:bridges 降到 0.150。能利用多长的上下文,直接决定模型质量——而长上下文正是 LLM 系统最昂贵的需求之一(KV 缓存随上下文线性增长,注意力的计算随它平方增长)。
3.2 链式法则:恒等式,而不是假设
一句话的概率怎样由”下一个 token 的概率”得到?slides 写的是概率论里的链式法则:
例如 。
为什么这样做? 直接给每个完整句子一个概率是不可能的——可能的句子有无穷多个。链式法则把”给句子打分”变成了”一次只预测一个 token”,而后者可以用同一个函数(神经网络)反复调用来实现。
关键的一点:链式法则是恒等式,它不做任何假设。 只要每一步都以全部前文为条件,连乘的结果一定等于句子的真实概率。01 号脚本第 3 部分验证了这一点:对 “the students from pittsburgh who swim at night love the bridges .”,12 个真实条件概率连乘得到 ,直接查表也是 ,只差 (浮点舍入)。
近似发生在模型这一步:用什么东西去估计每一个 。最古老的办法是 n-gram 的马尔可夫假设——只看最近 个 token。把上面那句话换成”只看最近两个 token”的最好估计,前 11 个位置都和真实值一样(那些位置本来就只依赖最近的词),唯独最后的 bridges 从 0.600 变成 0.150,整句的概率被低估了 4.00 倍——它依赖的 pittsburgh 在窗口之外。slides 说得很简洁:用神经网络(Transformer、CNN、RNN)来预测每一个条件概率。各种语言模型的区别,只在于怎样近似这一串条件概率。
3.3 为什么 ChatGPT 改变了 AI 的格局
slides 给了三条原因:
- 在海量原始文本上预训练(约 300B token)+ 少量人类反馈。 预训练的数据不需要人工标注,互联网上有的是;人类反馈只需要相对很少的量,就能把一个”续写机器”变成一个”助手”。
- 能听懂指令(instruction following)。 用户用自然语言描述任务即可,不需要为每个任务训练一个模型。
- 上下文学习(in-context learning)。 在提示里给几个例子,模型就能泛化到新任务(§2.1 的尤达风格改写)。
从系统的角度看,这三条有一个共同的后果:一个模型、一种接口(续写文本)服务所有任务。这让”把这一个模型训练好、服务好”的价值变得极高,也让这门课的所有优化都有了用武之地。
3.4 三类语言模型:差别只在”谁能看谁”
slides 把语言模型分成三类:
- encoder-only(BERT):掩码语言模型(masked LM)。随机遮住一些 token,让模型根据两边的上下文猜被遮住的词,建模 。它主要用来做理解类任务;也可以用来生成(例如非自回归翻译 NAT,以及 Lei Li 组的 REDER),但不常见。
- encoder-decoder(原始 Transformer、T5、翻译模型):编码器读完整个输入 ,解码器一个一个地生成输出 ,建模 。编码器可以是双向 LSTM、Transformer 或 CNN;解码器可以是 LSTM、Transformer、非自回归 Transformer 或 CNN。
- decoder-only(GPT 系列、LLaMA 及今天几乎所有 LLM):因果语言模型(causal LM),直接建模 。可以用 Transformer 或单向 LSTM 实现。slides 的评价是:最流行的 LLM 结构。
用 Transformer 实现时,三类模型用的是同一种注意力运算,区别只在注意力掩码(attention mask)——位置 可以”看”哪些位置 。02 号脚本第 1 部分把它们画了出来(■ 允许,· 禁止):
(a) encoder-only:全部可见 (b) decoder-only:只看自己和过去 (c) encoder-decoder(拼接后的等价掩码)
1 2 3 4 5 6 1 2 3 4 5 6 源1 源2 源3 目1 目2 目3
1 ■ ■ ■ ■ ■ ■ 1 ■ · · · · · 源1 ■ ■ ■ · · ·
2 ■ ■ ■ ■ ■ ■ 2 ■ ■ · · · · 源2 ■ ■ ■ · · ·
3 ■ ■ ■ ■ ■ ■ 3 ■ ■ ■ · · · 源3 ■ ■ ■ · · ·
4 ■ ■ ■ ■ ■ ■ 4 ■ ■ ■ ■ · · 目1 ■ ■ ■ ■ · ·
5 ■ ■ ■ ■ ■ ■ 5 ■ ■ ■ ■ ■ · 目2 ■ ■ ■ ■ ■ ·
6 ■ ■ ■ ■ ■ ■ 6 ■ ■ ■ ■ ■ ■ 目3 ■ ■ ■ ■ ■ ■
(真正的 encoder-decoder 模型里编码器和解码器各有一套参数,目标语言通过”交叉注意力”看源语言;(c) 是把它画成一个拼接序列后的等价掩码。只用一套参数、按 (c) 的掩码训练的模型叫 prefix-LM。)
为什么今天的 LLM 几乎都是 decoder-only? 从建模的角度,原因之一是 §3.3:所有任务都能写成”续写”,一个因果模型就够了。但从系统的角度,还有三条更硬的理由,02 号脚本逐条做了实验:
(1) 训练可以完全并行,而且每个 token 都有用。 因果掩码意味着:改动第 个 token,只有位置 的输出会变。02 号脚本第 2 部分把 “pittsburgh” 换成 “seattle”,因果模型里前 4 个位置的隐藏状态完全不变(○○○○●●●●●●●●●),双向模型里所有位置都变了(●●●●●●●●●●●●●)。所以一个长度为 的序列,一次前向就能同时得到 个”预测下一个词”的训练样本,而且每个预测都看不到答案。BERT 式的掩码语言模型只对被遮住的约 15% 的位置计算损失(原始 BERT 的设定);encoder-decoder 只对目标语言那一半计算损失(第 5 部分):
| 模型 / 训练目标 | 处理的 token | 有监督的预测 | 每个 token 的训练信号 |
|---|---|---|---|
| decoder-only(下一个 token) | 512 | 512 | 1.00 |
| encoder-only(遮住 15%) | 512 | 76 | 0.15 |
| encoder-decoder(源、目标各一半) | 512 | 256 | 0.50 |
(2) 生成时可以缓存过去的计算(KV 缓存)。 自回归生成时,每生成一个新 token 都要再跑一次模型。因果模型里,旧位置看不到新 token,所以它们在每一层算出的键 和值 永远不会变——算一次存起来,下一步直接复用。用一层注意力画出来:
预填充 prefill:一次算完提示词的 5 个位置,把每个位置的 k、v 存进缓存
位置 0 1 2 3 4
缓存 K k0 k1 k2 k3 k4
缓存 V v0 v1 v2 v3 v4
└─ 位置 4 的输出 → 采样出第 1 个新 token x5
解码 decode 第 1 步:只算新 token x5 的 q5、k5、v5
q5 和缓存里的 k0..k5 做点积 → 权重 → 对 v0..v5 加权求和 → 输出 → 采样出 x6
缓存追加:K = [k0 … k5],V = [v0 … v5] (k0..k4、v0..v4 一个都没重算)
解码第 2 步:只算 x6 的 q6、k6、v6,和 k0..k6 做注意力……如此往复
每层都有这样一份缓存。每一步只有”新 token 的那一行”需要计算,但它要读整个缓存——所以缓存越长,每一步读的字节越多(§5.7)。02 号脚本第 3 部分验证了这一点:追加一个 token 后,因果模型旧位置的隐藏状态最多只变 (浮点舍入量级),双向模型变了 ;用 KV 缓存逐个 token 计算出的 logits 和整句一次计算的结果最多差 。双向模型每来一个新 token,所有位置都要重算——没有东西可缓存。
(3) 缓存省下的计算随长度平方增长。 02 号脚本第 4 部分用 PyTorch 的 FLOP 计数器数了一下(提示词 5 个 token,贪心生成 G 个):
| 生成 token 数 G | 位置计算次数(无缓存 / 有缓存) | MFLOPs(无缓存 / 有缓存) | 倍数 | 结果相同 |
|---|---|---|---|---|
| 8 | 68 / 12 | 14.4 / 2.5 | 5.7× | 是 |
| 32 | 656 / 36 | 144.0 / 7.8 | 18.5× | 是 |
| 59 | 2006 / 63 | 458.6 / 14.1 | 32.6× | 是 |
没有缓存时,第 步要把 个位置全部重算,总量随 平方增长;有缓存时每步只算一个新位置。但要注意两点:有缓存时每步的计算并不是常数——新 token 仍要和所有旧的 、 做注意力,这一项随上下文线性增长;而且缓存要占显存,每个 token、每一层都要存一个 和一个 。§5.7 会算出,对一个 100B 模型,一条 2048 token 的序列的 KV 缓存就有 6.7 GB。KV 缓存把”重算过去”换成了”存储过去”——第 22、24、26、27 讲整整四讲都在处理由它引出的问题。
(⏱ 同一张表里本机的实测耗时只差 1.6 倍左右:这个模型太小,时间主要花在 Python 和内核调用的固定开销上。“FLOPs 少了”不等于”同样比例地变快”——这是 §4.7 的主题。)
3.5 LLM 的学习框架:预训练 → 监督微调 → 强化学习
slides 把 LLM 的训练画成三个阶段:
- 预训练(pre-training):在海量原始文本(维基百科、书籍、Reddit 等)上做下一个 token 预测。数据越多越好、通常从网上爬取,但质量很重要(维基百科、书籍、过滤过的 Reddit 帖子)。模型结构上的选择(稀疏注意力、混合专家等)也在这一阶段决定。
- 监督微调(supervised fine-tuning, SFT):用”问题 → 好的回答”这样的成对数据继续训练,让模型学会按指令回答。
- 强化学习(RL,例如 RLHF):用偏好数据(同一个问题的两个回答,哪个更好)训练,让模型的回答更符合人的偏好。
从系统的角度看,三个阶段的负载完全不同:预训练是超大规模、长时间、吞吐优先的训练(第 14–18 讲);SFT 的数据量小得多,常常用参数高效微调(第 23 讲);RLHF 要同时运行好几个模型、在”生成”和”训练”之间来回切换(第 25 讲)。
3.6 训练目标:交叉熵
预训练的损失是下一个 token 预测的交叉熵(cross-entropy loss):
直觉。 模型在第 步给真正出现的那个 token 分配了概率 ;分配得越高, 就越小。把所有位置平均起来,就是”平均每个 token 有多惊讶”。
为什么是交叉熵? 最小化交叉熵等价于最大似然估计:让模型给训练数据分配的总概率最大。它还有一个漂亮的分解:
KL 散度永远 ,所以任何模型的交叉熵都不可能低于真实分布的熵。在城市语言里,我们能精确算出第一项(熵率 0.7506 nat/token),于是每个模型”还差多少”就是第二项——本讲后面的表格里”超出下界”的那一列就是它(单位:毫 nat/token)。
训练是怎样并行的? 因为因果掩码(§3.4),一个长度为 的窗口一次前向就给出 个位置的预测,损失是它们的平均——这叫教师强制(teacher forcing):训练时每个位置的”前文”用的是真实数据,而不是模型自己生成的。
数据怎样喂进去:打包(packing)。 句子长短不一,最直接的做法是把每句补齐(padding)到同样长度——但补齐的位置算了也白算。03 号脚本第 1 部分在 20000 句训练集上算了一下:
| 做法 | 需要计算的位置数 | 浪费在填充上 |
|---|---|---|
| 每句补齐到全局最长 | 280,000 | 30.2% |
| 随机每 32 句一批、补齐到批内最长 | 278,784 | 29.9% |
| 先按长度排序再分批(分桶) | 195,424 | 0.1% |
| 首尾相接打包成一条 token 流 | 195,308 | 0.0% |
随机分批几乎没有改善:每 32 句里几乎总有一句最长的 T3。分桶能消除填充,但同一批里几乎全是同一种句型,批次不再是整体分布的随机样本。LLM 预训练几乎都用打包:把文档首尾相接(中间放一个分隔符,这里是 <bos>),按固定长度的窗口切开,每个位置都是真实 token。
3.7 怎样衡量一个语言模型
困惑度(perplexity) 是语言模型最常用的指标:
直觉。 困惑度可以理解成”模型平均在几个候选之间犹豫”。如果模型对每个 token 都在 个词里均匀地猜,困惑度正好是 ;如果它每次都确定地猜对,困惑度是 1。城市语言的理论最优困惑度是 2.118:即使知道真实分布,平均每个位置也还有约 2 个”同样合理”的选择。
01 号脚本第 4 部分在同一个测试集(2000 句、17577 个 token)上比较了各种 n-gram,并把误差拆成两种:
| n(看 n−1 个 token) | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 14 |
|---|---|---|---|---|---|---|---|---|---|
| 无限数据(精确计算) | 44.987 | 3.031 | 2.480 | 2.412 | 2.357 | 2.260 | 2.123 | 2.123 | 2.123 |
| 2000 句训练数据 | 45.039 | 3.076 | 2.501 | 2.439 | 2.410 | 2.370 | 2.457 | 2.608 | 2.934 |
| 20000 句训练数据 | 44.995 | 3.036 | 2.482 | 2.415 | 2.362 | 2.276 | 2.170 | 2.192 | 2.312 |
(真实分布下界 2.1234;Witten–Bell 插值平滑;词表大小 80,unigram 的 44.99 已经远好于均匀猜测的 80。)
- 第一行是近似误差。 数据无限多时,窗口越长越好,n = 8(看 7 个 token)就到达下界:这门语言最远的依赖正好是 7 个 token。这一行和下界的差距完全来自”模型结构看不到”。
- 后两行加上了估计误差。 数据有限时,窗口太长反而变差:2000 句时最好的是 n = 6,n = 14 退到 2.934。长上下文的组合太多,大部分只见过一两次,估不准。极端情况是不平滑的”∞-gram”(等于背下训练集):测试集里有 238 句从没在训练集里出现过,它们的概率是 0,困惑度是无穷大。
n-gram 只能在这两种误差之间取舍,因为它把 “pittsburgh who swim at night love the” 和 “pittsburgh who sing every day love the” 当成两个毫无关系的上下文。神经语言模型的优势恰恰在这里:它能学会”中间那些词不重要”,在不同上下文之间共享统计信息,于是能同时用长上下文和有限数据(§3.8)。
生成的质量也会受影响。 01 号脚本第 5 部分从各个 n-gram 里采样了 1000 句:bigram 只有 0.117 是这门语言里真实可能出现的句子,trigram 0.381,8-gram 0.966。trigram 生成的 T3 句子有 0.799 是城市和特色对不上的,例如 “the engineers from chicago who run on weekends love the coffee .”——生成到句末时,城市早已滑出窗口,模型”忘了”自己前面说过什么。
其他指标。 困惑度适合开放式生成,但它只告诉你模型对数据有多”不惊讶”。slides 还列了两类指标:
- 基于参考答案的指标:BLEU、ROUGE(数 n-gram 的重合,老但经典);COMET、BLEURT、SEScore2(用一个训练好的模型打分);InstructScore(能给出解释的打分,Lei Li 组的工作)。
- 下游任务指标:命名实体识别看 F1,问答看准确率或匹配率,代码生成看 pass@k(生成 k 个程序至少有一个通过测试的概率),检索看 nDCG,摘要看 ROUGE,翻译看 COMET / SEScore2 / BLEU。
对系统研究来说,这些指标的用处是守住底线:量化、剪枝、推测解码、KV 缓存压缩这些优化都可能悄悄损害模型质量,必须用困惑度和下游指标证明”变快了、但没变差”。城市语言的好处是能精确地量出”变差了多少”——第 19–20 讲(量化)会直接用这把尺子。
3.8 实验:在 CPU 上预训练一个小 GPT
03 号脚本用 tiny_gpt.py 里的小 GPT(2 层、d = 64、4 个头、上下文 64,共 109,312 个参数)在 20000 句城市语言上预训练:2000 步 × 每批 32 个窗口 × 每个窗口 64 个 token = 409.6 万个 token(约 21 遍训练集),⏱ 单线程 CPU 上约 22 秒。Transformer 内部每个部件为什么这样设计,第 6 讲会细讲;这里把它当作”给定前文、输出下一个 token 分布”的函数。
训练曲线(验证集困惑度;验证集的理论下界 2.1139):第 1 步 80.59(随机初始化 ≈ 均匀猜测 = 词表大小)→ 250 步 2.399 → 500 步 2.152 → 1000 步 2.141 → 2000 步 2.119(只超出下界 2.6 毫 nat/token)。
和 n-gram 比(同一个测试集、同样的 20000 句训练数据):
| 模型 | 困惑度 | 超出下界(毫 nat/token) |
|---|---|---|
| 均匀猜测 | 80.000 | — |
| 1-gram | 44.995 | 3053.6 |
| 3-gram | 2.482 | 156.0 |
| 8-gram(n-gram 里最好的) | 2.170 | 21.8 |
| 小 GPT(109,312 参数) | 2.130 | 3.1 |
| 真实分布(理论下界) | 2.123 | 0 |
小 GPT 的可约损失只有最好的 n-gram 的 14%。
它学到的分布像不像? 03 号脚本第 3 部分把小 GPT 和 3-gram 的下一个 token 分布与真实分布比较(每格依次是 真实 / 小 GPT / 3-gram):
| 前缀 | 前两个 token | KL(真实‖小 GPT) | KL(真实‖3-gram) |
|---|---|---|---|
| pittsburgh is a city of | bridges 0.60/0.62/0.15 · rivers 0.20/0.19/0.05 | 0.0063 | 1.361 |
| santa barbara has very nice | beaches 0.50/0.47/0.09 · weather 0.40/0.42/0.11 | 0.0082 | 1.210 |
| the students from pittsburgh who swim at night love the | bridges 0.60/0.61/0.15 · rivers 0.20/0.18/0.04 | 0.0051 | 1.382 |
| the chefs from seattle who read on weekends love the | coffee 0.50/0.48/0.09 · rain 0.25/0.30/0.04 | 0.0169 | 1.405 |
| the doctors from miami who sing in winter love the | beaches 0.60/0.62/0.10 · music 0.20/0.16/0.08 | 0.0235 | 1.465 |
小 GPT 几乎把真实的条件概率表”背”了下来,包括长程的 T3。一个我起初没想到的结果:3-gram 在 T1、T2 上也很差(KL 1.36、1.21)——“a city of ___” 和 “very nice ___” 里都没有城市,城市在 3 个 token 之外。按句型拆开看(第 4 部分),3-gram 在四种句型上都超出至少 98 毫 nat/token(T1 最多,236.3);8-gram 在 T1、T2、T4 上几乎没有差距,但在 T3 上还差 44.8,而小 GPT 只差 2.2:9000 种 T3 组合里大部分在训练集里只出现过零到一两次,n-gram 只能靠平滑”猜”,小 GPT 却学会了”填充词与答案无关”。
采样。 03 号脚本第 5 部分从小 GPT 采样 1000 句:
| 温度 | 合法句子 | T3 中不合法 | T3 的比例(真实 0.35) | 含 pittsburgh(真实 0.20) | 不同句子个数 |
|---|---|---|---|---|---|
| 1.0 | 0.989 | 0.019 | 0.371 | 0.197 | 470 |
| 0.5 | 1.000 | 0.000 | 0.653 | 0.311 | 601 |
温度 1.0 时 98.9% 是合法句子(8-gram 是 96.6%)。降低温度(把概率分布”变尖”)后,每一次”掷骰子”都更偏向最可能的选项:T3 的比例从 0.371 升到 0.653,含 pittsburgh 的句子从 0.197 升到 0.311。一个反直觉的结果:不同句子的个数反而从 470 个增加到 601 个。我原本以为”低温 = 更单调”,但最可能的句型 T3 恰好是组合最多的(9000 种),而 T3 里的人群、活动、时间是均匀分布——温度改变不了均匀分布。“低温更单调”只在每一步的分布都有明显高峰时成立;衡量多样性要看具体用什么指标。第 9 讲(解码)会系统地讨论这类取舍。
3.9 规模与数据都重要
slides 引用 GPT-3 的结果(在 300B token 上训练,不同大小模型的困惑度曲线)得出结论:模型规模和数据规模都重要。03 号脚本第 6 部分在城市语言上做了一个迷你版:4 种宽度(d = 8、16、32、64,2 层)× 3 种数据量,每个模型都训练 1000 步(步数相同,所以计算量只随模型大小变化)。表中是测试集超出下界的损失(括号里是训练集上的),单位毫 nat/token:
| 训练数据 | d=8(1,760 参数) | d=16(6,592) | d=32(25,472) | d=64(100,096) |
|---|---|---|---|---|
| 500 句 | 137.6(112.4) | 101.8(−5.9) | 85.6(−13.0) | 178.3(−56.9) |
| 2000 句 | 78.3(73.9) | 23.6(12.4) | 32.5(20.3) | 16.5(−0.3) |
| 20000 句 | 93.4(92.7) | 12.7(12.2) | 14.1(14.4) | 6.1(6.4) |
(参数数不含嵌入。)
- 数据少时,大模型会过拟合。 500 句时最好的是 d = 32;d = 64 在训练集上比真实分布还”好”(−56.9,意思是它把训练句背了下来、给它们的概率比真实概率还高),测试集却最差(178.3)。
- 数据多时,大模型最好。 20000 句时 d = 64 最好(6.1)。
- 模型太小,数据再多也没用。 d = 8 无论给多少数据都停在 78 以上——它的容量装不下这门语言的规律。
所以只加一样,迟早会碰到另一样的天花板。(这张表每格只训练了一次,十几个毫 nat 以内的差异——比如 d = 16 和 d = 32 谁更好、d = 8 在 2000 句和 20000 句之间的起伏——可能只是随机波动或优化没收敛;上面的结论看的是几十到上百毫 nat 的大趋势。)
规模定律(scaling laws)把这个观察变成了定量的公式:Kaplan 等人(2020)发现损失随参数量、数据量、计算量都呈幂律下降;Hoffmann 等人(2022,Chinchilla)进一步指出,在固定的计算预算下,参数量和训练 token 数应该同比例增长,经验上大约是”每个参数 20 个 token”。§5 会用这个经验法则来确定 100B 模型的训练数据量。
4. LLM 系统的挑战 · Challenges in LLM Systems
4.1 现代 LLM 是什么样的系统
slides 用三个词概括现代 LLM:
- 通才(generalist):把任何 AI 任务都表述成”基于 token 的序列生成”。
- 可指令(instructibility):一个交互式系统,能接受自然语言的、几乎无穷多样的命令。
- 能行动(agentic):调用外部工具、执行动作、接收反馈。
对系统来说,这意味着负载既大又杂:同一个模型要同时服务短的翻译请求、长的推理请求、成串的智能体调用(§2.2 的表)。
4.2 核心系统问题与衡量指标
slides 把关键的系统问题写成一句话:用更少的资源(GPU、显存、电),更快地在更大的数据集上计算(训练 / 推理)更大的 LLM。 这句话里每个词都对应一类指标:
| 维度 | 训练时关心 | 推理时关心 |
|---|---|---|
| 快 | 吞吐量(token/秒)、训练总时长 | 首 token 时延(TTFT)、token 间时延(TPOT)、吞吐量 |
| 少用资源 | GPU 数、显存峰值、MFU(有效算力 / 峰值算力) | 每 GPU 能服务的并发数、显存占用 |
| 便宜 | GPU 小时、美元 | 每百万 token 的成本 |
| 省电 | 总耗电(MWh) | 每 token 的能耗 |
| 质量不变 | 收敛后的损失 / 下游指标 | 困惑度 / 下游指标不下降(§3.7) |
这些指标经常互相冲突:把更多请求凑成一批能提高吞吐,却会增加每个请求的时延;量化能省显存,却可能损害质量。系统设计的大部分工作,就是在这些冲突之间做取舍。
4.3 正确的抽象与权衡
slides 给出两条系统设计的原则:
- 正确的抽象(right abstraction):设计有用的”积木”,把复杂性藏起来,让上层的开发者不用操心。例如写模型的人调用
torch.softmax,不用知道它在 GPU 上怎样实现;用 DDP 的人只写单卡的训练代码,不用自己写梯度同步。 - 权衡(tradeoffs):先搞清楚什么是根本的约束、什么是主要的成功指标。例如推理服务里,显存容量是硬约束,时延是指标,吞吐是成本。
一个好的抽象值多少?05 号脚本第 3 部分对一个 256×256 的矩阵按行做 softmax,三种写法算的是同一个函数(结果只差 1.5×10⁻⁸),速度却差了一个多数量级(⏱ 本机):纯 Python 循环约 3.4 毫秒;NumPy(max、exp、sum、除法四次遍历)约 0.11 毫秒,快约 30 倍;PyTorch 的 torch.softmax(一个专门的内核)约 0.065 毫秒,快约 53 倍。纯 Python 慢在每个元素都要经过解释器;NumPy 和 PyTorch 的差距则来自内核本身的写法——NumPy 的写法会把中间结果写出来再读回去。写模型的人只需要选对积木,做积木的人负责把它做快——这门课两边都要学。
4.4 语言模型里的计算:从网络层到底层算子
slides 列出了语言模型里常见的网络层:多头注意力、LayerNorm、dropout、线性层、非线性激活(tanh、ReLU、GELU)、softmax。它们最终都归结为四类底层运算:
| 底层运算 | 例子 | 原课作业 |
|---|---|---|
| 矩阵 / 张量乘法 | 线性层、注意力的 Q·Kᵀ 和 权重·V | HW1 的 MatrixMultiply 内核 |
| 归约(reduction) | 求和、求平均、求最大值(softmax 的分母、LayerNorm 的均值和方差) | HW1 的 reduce 内核 |
| 逐元素映射(map / zip) | 加偏置、残差相加、GELU、dropout、缩放 | HW1 的 map、zip 内核 |
| 数据搬运 | 拷贝、转置、拼接、在 CPU 和 GPU 之间 / GPU 和 GPU 之间传输 | 贯穿全课 |
这四类运算的性格完全不同。衡量它的工具是算术强度:一个运算的 FLOPs ÷ 它必须读写的字节数。硬件也有一个对应的数——平衡点(ridge point)= 峰值算力 ÷ 显存带宽。H100 的 BF16 张量核心标称 989 TFLOP/s、显存带宽 3.35 TB/s,平衡点约 295 FLOP/字节:算术强度低于 295 的运算,GPU 还没算完就在等数据,叫访存受限(memory-bound);高于 295 的叫计算受限(compute-bound)。理想耗时用 roofline 模型估计:。画成图(对数坐标)就是一条”屋顶线”:
能达到的算力 FLOP/s(对数)
│ ┌───────────────────────────── 峰值算力(H100 BF16 约 989 TFLOP/s)
│ ╱ 计算受限区:再提高算术强度也不会更快
│ ╱ ● 训练时的大矩阵乘法(强度 ~1600)
│ ╱
│ ╱ ● 训练时的 Q·Kᵀ(强度 ~114)
│ ╱
│ ╱ 访存受限区:斜率 = 显存带宽(3.35 TB/s),能达到的算力 = 强度 × 带宽
│ ╱
│ ╱ ● 解码时的矩阵乘法(强度 ~1)
│ ╱ ● softmax / GELU / LayerNorm(强度 0.2–2)
└──────────────┼────────────────────────────────── 算术强度 FLOP/字节(对数)
平衡点 ≈ 295
一个运算落在斜线上,说明它被带宽卡住了:想让它更快,要么少读写字节(融合、量化、FlashAttention),要么提高它的算术强度(批处理);提升 GPU 的峰值算力对它毫无帮助。
05 号脚本第 1 部分把一层 GPT 式 Transformer 拆成 12 个算子,用公开规格算了一遍(假设每个算子单独执行、中间结果都写回显存)。“假想 100B”模型(d = 10240)对一条 2048 token 的序列做训练前向时:
| 算子 | 类型 | 算术强度 FLOP/字节 | 瓶颈 | 占 FLOPs | 占理想时间 |
|---|---|---|---|---|---|
| QKV 投影 | 矩阵乘法 | 1617 | 计算 | 24.18% | 20.7% |
| Q·Kᵀ | 矩阵乘法 | 114 | 访存 | 1.61% | 3.6% |
| 缩放 + 掩码 + softmax | 归约+逐元素 | 1.2 | 访存 | 0.03% | 6.4% |
| 注意力权重·V | 矩阵乘法 | 114 | 访存 | 1.61% | 3.6% |
| 输出投影 | 矩阵乘法 | 1463 | 计算 | 8.06% | 6.9% |
| MLP 升维 / 降维 | 矩阵乘法 | 1638 | 计算 | 各 32.24% | 各 27.6% |
| GELU | 逐元素 | 2.0 | 访存 | 0.01% | 1.6% |
| LayerNorm ×2、残差相加 ×2 | 归约 / 逐元素 | 0.2–2.0 | 访存 | ≈0 | 合计 2.0% |
汇总起来:
| 场景 | 矩阵乘法占 FLOPs | 矩阵乘法占理想时间 | 一层的理想时间 |
|---|---|---|---|
| 假想 100B,训练 / prefill(1 × 2048 token) | 99.95% | 90.0% | 6.288 ms |
| GPT-2 small(d = 768),训练(8 × 1024 token) | 99.43% | 57.3% | 0.440 ms |
| 假想 100B,解码(1 条序列生成 1 个 token) | 99.95% | 100.0% | 0.777 ms |
三个结论,分别是后面三讲的出发点:
- “小”算子不做运算,却占时间。 softmax、GELU、LayerNorm、残差相加几乎不贡献 FLOPs,却要把整块数据读一遍写一遍。模型越小越明显:GPT-2 small 上矩阵乘法只占 57.3% 的时间。把几个小算子合成一个内核、中间结果不落显存,叫算子融合(kernel fusion)——第 10–11 讲(LightSeq)的核心。
- 注意力里的矩阵乘法也是访存受限的。 Q·Kᵀ 要把”头数 × T × T”的分数矩阵写回显存,softmax 再读出来、写回去。FlashAttention(第 21 讲)的核心就是不把这个矩阵写出去。
- 解码时一切都是访存受限的。 每个 token 只用一次每个权重,算术强度约为 1。一层 0.777 毫秒几乎全是”读权重”的时间——这就是 LLM 推理服务的根本矛盾(§4.7、§5.7)。
4.5 不同抽象层次上的挑战
slides 用一张图把系统挑战按抽象层次分开:
| 层次 | 要解决的问题 | 对应讲次 |
|---|---|---|
| 分布式 / 并行系统 | 巨大的模型、巨大的数据集、很长的序列;怎样切分、调度、通信 | 第 14–18 讲(训练)、第 22–27 讲(服务) |
| 深度学习框架 | 让开发者容易写、容易改模型和算法(自动微分、模块化) | 第 5 讲(miniTorch)、第 12 讲(JAX) |
| 数据块上的算子 | 快速的 CUDA / TPU 内核;数据与模型的压缩 | 第 2–4、10–11、13、19–21 讲 |
后面几乎每一讲都可以放进这张表的某一格。
4.6 模型–算法–系统协同设计
slides 强调:LLM 需要模型、算法、系统一起设计——模型结构、训练 / 推理算法、软件优化(切分、调度、数据搬运、隐藏延迟)、硬件加速(专用指令)。只优化其中一层往往不够。几个本课会讲到的例子:
| 例子 | 改了哪一层 | 换来了什么 | 讲次 |
|---|---|---|---|
| GQA(多个查询头共用一组 K、V) | 模型结构 | KV 缓存缩小 H / H_kv 倍(§5.7:假想 100B 从 3.28 MB/token 降到 328 KB/token) | 7 |
| FlashAttention | 算法 + 内核 | 数学上完全相同,但不再把 T×T 矩阵写回显存 | 21 |
| 推测解码 | 解码算法 | 输出分布不变,一次验证多个 token,缓解访存受限 | 9 |
| 量化(INT8 / INT4) | 数值表示 + 内核 | 权重字节数减半或减到 1/4:既省显存又少读字节 | 19–20 |
| MoE | 模型结构 + 并行策略 | 参数涨十倍、每个 token 的计算几乎不变,代价是 all-to-all 通信 | 17 |
| PagedAttention | 系统(内存管理) | KV 缓存不再碎片化,同样的显存装下更大的批 | 24 |
4.7 计算 vs 数据搬运
slides 的原话很直白:光让计算变快是不够的,搬数据也要时间。 它列了三种数据搬运:大模型有大量参数,在设备 / 节点之间传参数和梯度可能比计算还慢;批量的样本和单条序列的负载不同;长上下文的 LLM 需要很大的工作内存。
本机实测(⏱,05 号脚本第 2 部分)。 单线程下,2048×2048 的矩阵乘法约 3.1–3.3 TFLOP/s(Apple 芯片有专门的矩阵单元),而两个 64 MiB(1677 万个 float32)的数组逐元素相加只有约 9.7 GFLOP/s——慢了 300 多倍。加法每做 1 次运算要搬 12 字节,它的速度由内存带宽(约 116 GB/s)决定,和 CPU 算得多快无关。本机的平衡点约 28 FLOP/字节,H100 是 295:GPU 的算力相对带宽更”富余”,访存受限的问题在 GPU 上更严重。(一个例外:GELU 在本机只有约 9 GB/s,比加法慢约 9 倍——CPU 上 erf 的计算本身就很贵,所以它在 CPU 上其实是计算受限的。同一个算子在不同硬件上瓶颈可以不同。)
搬数据要多久(05 号脚本第 4 部分,公开规格):
| 数据 | 大小 | PCIe 4.0 x16(32 GB/s) | InfiniBand NDR(50 GB/s) | NVLink 4(450 GB/s,单向) | H100 显存(3.35 TB/s) |
|---|---|---|---|---|---|
| 100B 模型的 BF16 权重 | 202 GB | 6.33 s | 4.05 s | 0.45 s | 0.060 s |
| 一层的 BF16 权重 | 2.52 GB | 79 ms | 50 ms | 5.6 ms | 0.75 ms |
| 一条 2048 token 序列的 KV 缓存(MHA) | 6.71 GB | 0.21 s | 0.13 s | 15 ms | 2.0 ms |
对照:这一层对 2048 个 token 做前向,在 H100 上理想耗时只有 5.21 毫秒,而经 PCIe 把这一层的权重搬过来要 79 毫秒(15 倍)。所以”把权重放在 CPU 内存里、用时再搬”(卸载 offload)的方案必须提前预取、和计算重叠(第 18 讲的 ZeRO-Offload、附加讲 A2 的 KTransformers)。不同层次的带宽差两个数量级,“数据放在哪里”决定了能跑多快。
批处理为什么能救推理(⏱,05 号脚本第 5 部分)。 解码一个 token 时,权重矩阵的每个元素只用一次;如果同时解码 b 个序列,读一次权重就能用 b 次。在本机上对一个 4096×4096 的 float32 权重(64 MiB,远大于缓存)做 :b 从 8 增加到 64,处理的 token 多了 8 倍,每次调用的耗时只从约 0.9 毫秒变到约 1.2 毫秒;b = 256 时每个 token 的成本约是 b = 1 的 1/26–1/32,并且达到约 2.6 TFLOP/s,接近矩阵乘法的峰值(变成计算受限,再加大批就没用了)。
这里还出现了一个意外:b = 2、4 时每次调用(约 1.7–1.9 毫秒)比 b = 1(约 0.34–0.42 毫秒)还慢好几倍,甚至比 b = 8 慢。原因是数学库对 b = 1 走专门的矩阵-向量内核(有效带宽约 160–200 GB/s),对 b ≥ 2 换成通用的矩阵乘法内核,后者对很窄的输入并不高效。同一个库对不同形状选不同实现,“理论上的平滑趋势”会被实现细节打断——这也是 GPU 上要为特定形状专门写内核、做自动调优的原因(第 4、10 讲)。
4.8 编程模型的三个层次
slides 最后问:什么是构建 AI 应用的好抽象?它分了三层:
- 上层:把模型集成进产品系统,持续追踪和改进产品质量(附加讲 A1 的应用栈)。
- 中层:构建模型训练和推理软件、流式数据流(第 5、12、14–18、22–27 讲)。
- 底层:GPU 内核、编译器等(第 2–4、10–13、21 讲)。
一个好的抽象,让程序员不再操心一个或多个问题(比如性能或故障),同时还能支撑上面各种各样的应用。这门课的大部分讲次在中层和底层;但每讲完一个技术,都值得问一句:它把什么藏了起来?代价是什么?
5. 回答总题目:训练一个 100B 模型需要多少资源?
这一节用 04 号脚本和 llm_budget.py 把 §1.2 的问题算到底。原则是每条公式先验证、再使用。用来估算的”假想 100B”是 GPT-3 式结构:80 层、d = 10240、80 个头(每头 128 维)、词表 50257、上下文 2048。
5.1 参数量:N ≈ 12·L·d²
推导。 一层 GPT 式 Transformer 里有:注意力的 Q、K、V、输出 4 个 矩阵(),MLP 的两个 矩阵(),合计 ;再加上偏置和两个 LayerNorm 共 个参数。 层就是 ,再加上词嵌入 和位置嵌入 。当 上万时,一次项可以忽略: + 嵌入。
验证 1:和真实建出来的模型比。 04 号脚本第 1 部分对 4 种配置(含不共享输出层、自定义 MLP 宽度)用 PyTorch 真的建出模型数参数,和 tiny_gpt.param_formula、llm_budget.param_count 两套公式逐个相等(例如 109,312 = 109,312 = 109,312)。
验证 2:和公开模型的官方数字比(第 2 部分):
| 模型 | 结构 | 公式 | 官方 | 12·L·d² |
|---|---|---|---|---|
| GPT-3 175B | 96 层、d = 12288 | 174.60B | 175B | 173.9B |
| Llama-3 8B | 32 层、d = 4096、GQA 32/8、SwiGLU | 8.03B | 8.03B | 6.4B |
| Llama-3 70B | 80 层、d = 8192 | 70.55B | 70.6B | 64.4B |
| Llama-3.1 405B | 126 层、d = 16384 | 405.85B | 405B | 405.9B |
| 假想 100B | 80 层、d = 10240 | 101.21B | — | 100.7B |
两点提醒:公式要跟着结构走——LLaMA 用 SwiGLU(3 个矩阵)、GQA(K、V 投影小得多)、不共享的大词表,所以 12·L·d² 对 Llama-3 8B 明显偏低,嵌入和输出层在它的 8B 参数里占了 13.1%;405B 的 12·L·d² 碰巧几乎等于真实值,是因为 SwiGLU 的约 9.75d² 加上 GQA 注意力的约 2.1d² 又凑成了约 12d²——巧合不能当规律用。
5.2 训练 token 数
GPT-3 175B 训练了 300B token,约每个参数 1.7 个 token。Chinchilla(2022)的规模定律指出,在固定计算预算下更好的做法是每个参数约 20 个 token。我们取后者:D = 20 × 101.2B ≈ 2.02T token(这是一个假设,§5.8 看它的影响)。
5.3 训练 FLOPs:6ND
推导。 前向传播时,一个 token 经过一个权重,要做一次乘法和一次加法,即 2 FLOPs,所以前向约 FLOPs / token。反向传播对每个矩阵乘法 要算两个梯度:对输入 (传给前一层)和对权重 (交给优化器),每个都和前向一样大,所以反向约 。合计:
验证。 04 号脚本第 3 部分用 PyTorch 的 FlopCounterMode 数了三个小模型一次前向 + 反向的 FLOPs,和精确公式 ( 是参与矩阵乘法的权重数, 是注意力的 Q·Kᵀ 和 权重·V)逐个相等(例如 368,050,176 = 368,050,176)。
但 6ND 本身是个近似,它忽略了注意力项 。这一项相对于 的比例大约是 的量级:
| 模型 | 训练上下文 T | 注意力项占前向 FLOPs | 6·N·D 比精确值 |
|---|---|---|---|
| GPT-3 175B | 2048 | 2.7% | 偏低约 3% |
| 假想 100B | 2048 | 3.2% | 偏低约 4% |
| Llama-3 8B | 8192 | 22.2% | 偏低约 28% |
| Llama-3.1 405B | 8192 | 7.7% | 偏低约 8% |
所以 6ND 适合做数量级估算;上下文越长、模型越”窄”,越要把注意力项加回来。(这里的注意力按”算满 T×T”计;真实的注意力内核利用因果掩码只算一半,第 21 讲。)
对假想 100B: FLOPs(加上注意力项多 3.3%)。
5.4 时间与钱
H100 的 BF16 标称峰值是 989 TFLOP/s,大规模训练的 MFU 通常在 30%–50%,我们取 40%(假设)。04 号脚本第 4 部分:
| GPU 数 | 天数 | GPU 小时 | 成本(按假设的 2 美元 / GPU 小时) | GPU 满载耗电 |
|---|---|---|---|---|
| 1 | 35,963(约 99 年) | 86.3 万 | 约 170 万美元 | 604 MWh |
| 1024 | 35.1 | 86.3 万 | 约 170 万美元 | 604 MWh |
| 4096 | 8.8 | 86.3 万 | 约 170 万美元 | 604 MWh |
| 16384 | 2.2 | 86.3 万 | 约 170 万美元 | 604 MWh |
第一个结论:必须分布式。 一块 H100 要算 99 年。理想情况下 GPU 数加倍、天数减半,总 GPU 小时和钱不变——前提是并行效率不打折扣,而这正是第 14–18 讲要争取的。(价格和耗电都是粗估:云上价格随时间和合同差别很大;耗电只算了 GPU 的满载功耗 700 W,不含 CPU、网络和制冷。)
5.5 显存
混合精度 + Adam 训练时,每个参数要存(ZeRO 论文的记账方式):BF16 权重 2 字节、BF16 梯度 2 字节、FP32 主权重 4 字节、Adam 的一阶矩 4 字节、二阶矩 4 字节,共 16 字节。对 101.2B 参数:
| 项目 | 每参数字节 | 总量 |
|---|---|---|
| BF16 权重 | 2 | 202 GB |
| BF16 梯度 | 2 | 202 GB |
| FP32 主权重 | 4 | 405 GB |
| FP32 Adam m | 4 | 405 GB |
| FP32 Adam v | 4 | 405 GB |
| 合计(不含激活值) | 16 | 1.62 TB |
至少要 21 块 80 GB 的 H100 才放得下这些”训练状态”。还没算激活值:反向传播要用到前向时各层的中间结果。按 Korthikanti 等人(2022)的公式(每层约 字节, 是序列长度、 是批大小、 是头数),一条 2048 token 的序列、不做任何重计算时要 191 GB——是一块 H100 显存的 2.4 倍。
第二个结论:必须把模型和状态切开。 张量并行和流水线并行切模型(第 16 讲),ZeRO 切优化器状态、梯度和参数(第 18 讲),激活重计算用”多算一遍”换显存(第 18 讲),FlashAttention 不保存 T×T 的注意力矩阵(第 21 讲)。
5.6 通信
假想一个”纯数据并行”的方案:1024 块卡各存一份完整模型(实际放不下,但先这样算),每块卡算自己那份数据的梯度,再用环形 all-reduce(第 14 讲推导)把梯度平均。每块卡要发送(也接收)约 梯度字节数:BF16 梯度 202 GB,走每卡一个 400 Gb/s 的 InfiniBand 网卡(50 GB/s)要 8.09 秒;如果都能走机内的 NVLink(450 GB/s 单向)也要 0.90 秒。而每步(假设全局批 400 万 token)的计算只要 6.00 秒。
第三个结论:通信可能比计算还慢。 串行执行时一步要 14.1 秒,就算通信和计算完全重叠,也要 8.1 秒——瓶颈在通信(§2.3 的工厂比喻)。所以要让通信和计算重叠(第 15 讲 DDP 的分桶)、让流量大的通信走机内 NVLink、机间只同步切过片的梯度(第 14–16 讲)、用 MoE 时还要处理 all-to-all(第 17 讲)。
5.7 推理
权重。 BF16 权重 202 GB,至少要 3 块 H100 才放得下。假设用一台 8 卡 H100 服务器(共 640 GB),放完权重还剩 438 GB 给 KV 缓存。
KV 缓存。 每个 token、每一层要存一个键向量和一个值向量: 字节。
| 模型 | 每 token 的 KV 缓存 | 剩余显存能存多少 token | 相当于多少条 2048 token 的序列 |
|---|---|---|---|
| 假想 100B(MHA,80 个 KV 头) | 3.28 MB | 133,539 | 65 |
| 假想 100B + GQA(8 个 KV 头) | 328 KB | 1,335,391 | 652 |
GQA 让 KV 缓存缩小到 1/10,能同时服务的序列数多 10 倍——“模型结构为系统让路”的典型例子。
解码速度的上限。 每解码一步(每个序列生成 1 个 token),要把全部权重读一遍(整批共享),再读每条序列自己的 KV 缓存;roofline 给出理想耗时的下限(04 号脚本第 5 部分,8 卡理想均分,上下文 2048):
| 模型 | 批大小 | KV 缓存 | 放得下 | 每步毫秒 | 瓶颈 | token/秒(全批合计) |
|---|---|---|---|---|---|---|
| MHA | 1 | 6.7 GB | ✓ | 7.80 | 访存 | 128 |
| MHA | 16 | 107 GB | ✓ | 11.56 | 访存 | 1,384 |
| MHA | 64 | 430 GB | ✓ | 23.58 | 访存 | 2,714 |
| MHA | 256 | 1.72 TB | ✗ | — | — | — |
| GQA-8 | 1 | 0.67 GB | ✓ | 6.45 | 访存 | 155 |
| GQA-8 | 16 | 10.7 GB | ✓ | 6.83 | 访存 | 2,344 |
| GQA-8 | 64 | 43 GB | ✓ | 8.03 | 访存 | 7,971 |
| GQA-8 | 256 | 172 GB | ✓ | 12.84 | 访存 | 19,943 |
- 批大小 1 时每步约 7.8 毫秒、每秒只有 128 个 token,而算力几乎闲着:生成每个 token 都要把 202 GB 权重读一遍。
- 批越大,同一次读权重被越多序列分摊,吞吐上升(GQA 从 155 到 19,943 token/秒)。但每条序列自己的 KV 缓存也要读:MHA 批 64 时 KV 缓存(430 GB)已经比权重还大,吞吐只比批 16 多 2.0 倍;批 256 干脆放不下。
- 表里所有情况都是访存受限。“怎样在显存里塞下更大的批、怎样少读一些字节”,正是第 19–20 讲(量化)、第 21 讲(FlashAttention)、第 22–24 讲(连续批处理、PagedAttention)的核心。
5.8 敏感性:哪个假设影响最大
04 号脚本第 6 部分改变三个假设(1024 卡),看训练天数怎样变:
| GPU | MFU | 训练 token 数 | 天数 |
|---|---|---|---|
| A100 | 20% | 20×N = 2.02T | 222.7 |
| A100 | 40% | 20×N | 111.3 |
| H100 | 20% | 20×N | 70.2 |
| H100 | 40% | 20×N | 35.1 |
| H100 | 60% | 20×N | 23.4 |
| H100 | 40% | 3×N = 0.30T(GPT-3 式) | 5.3 |
- 训练 token 数从约 3×N 到 20×N:计算量差将近 7 倍——这是模型 / 数据的选择。
- A100 换成 H100:快约 3 倍——这是硬件。
- MFU 从 20% 提到 60%:快 3 倍——这是系统。同样的模型、同样的数据、同样的硬件,系统做得好不好,训练时间能差几倍、成本能差上百万美元。
一页纸的总账(假想 100B,Chinchilla 式训练):
| 问题 | 答案 | 关键公式 | 后续讲次 |
|---|---|---|---|
| 多少参数 | 101.2B | 12·L·d² + 嵌入 | 6、7 |
| 多少数据 | 2.02T token | 20 × N(假设) | 7 |
| 多少计算 | 1.23×10²⁴ FLOPs | 6ND(+注意力项) | 6、14 |
| 多少时间 / 钱 | 1024 块 H100 约 35 天,约 86 万 GPU 小时 | C / (GPU 数 × 峰值 × MFU) | 2–4、10–13 |
| 多少显存 | 训练状态 1.62 TB + 激活值 | 16 字节 × N | 16、18、21 |
| 多少通信 | 每步 all-reduce 约 200 GB 梯度 | 2 × 梯度字节 / 带宽 | 14–17 |
| 服务一个用户 | 至少 3 块 H100;批 1 约 128 token/秒;每 token KV 3.28 MB | 权重字节 / 带宽 | 9、19–27 |
6. 课程地图 · The course map
本讲义一讲对应原课一讲,完整大纲见 SYLLABUS.md。按单元看:
| 单元 | 讲次 | 回答 §5 总账里的哪一行 |
|---|---|---|
| ① LLM 与 GPU 编程基础 | 1 导论;2–3 GPU 编程基础;4 GPU 加速(分块);5 深度学习框架与自动微分 | “一块 GPU 能算多快”;“框架怎样把模型变成算子” |
| ② Transformer 与 LLM 基础 | 6 Transformer;7 预训练 LLM(T5、GPT-3、LLaMA);8 分词与嵌入;9 生成与推测解码 | “多少参数、多少计算”;“服务时每步做什么” |
| ③ 在加速器上加速 Transformer | 10–11 LightSeq(推理 / 训练的算子融合);12 TPU 与 JAX;13 Pallas 与 Splash Attention | “怎样把 MFU 提上去” |
| ④ 分布式训练与大模型 | 14 分布式训练与 all-reduce;15 DDP;16 流水线 / 张量并行;17 MoE;18 ZeRO | “多少显存、多少通信”;“怎样用一千块 GPU” |
| ⑤ 压缩与高效注意力 | 19–20 量化(GPTQ);21 FlashAttention | “怎样少读字节、少占显存” |
| ⑥ 服务、微调与 RL 系统 | 22 SGLang;23 LoRA / QLoRA;24 vLLM;25 RLHF 系统;26 预填充 / 解码分离;27 KV 缓存 | “怎样服务成千上万个用户”;“怎样便宜地微调和对齐” |
原课的 7 次作业沿着同一条线:HW1 写 map / zip / reduce / 矩阵乘法的 CUDA 内核(§4.4 那张表的四类运算),HW2 写自动微分,HW3 搭一个 decoder-only Transformer,HW4 写融合的 softmax 和 LayerNorm 内核(§4.4 的结论 1),HW5–6 做数据并行、流水线并行、ZeRO + LoRA 和 SGLang 推理,HW7 做 RLHF。
本讲的概念在后面哪里展开:
| 本讲的概念 | 在哪里展开 |
|---|---|
| 注意力掩码、KV 缓存 | 第 6 讲(实现)、第 9 讲(解码)、第 22、24、27 讲(管理) |
| 打包、数据并行的大批量 | 第 14–15 讲 |
| 困惑度作为”质量底线” | 第 19–20 讲(量化后的困惑度)、第 23 讲 |
| 算术强度、roofline | 第 2–4 讲(GPU)、第 10、21 讲(融合、FlashAttention) |
| 16 字节 / 参数、激活值 | 第 18 讲(ZeRO、重计算) |
| 环形 all-reduce | 第 14 讲 |
| GQA、SwiGLU、RoPE | 第 7 讲 |
| 批处理与访存受限的解码 | 第 22、24、26 讲 |
7. 怎样学这门课 · How to approach this course
- 先估算,再动手。 遇到任何一个系统问题,先用 §5 的那几条公式算一下理想情况下的上下限。估算错了也没关系,错在哪里本身就很有信息量(本讲的 04 号脚本里,“6ND 在大模型上只差几个百分点”就被 Llama-3 8B 的数字推翻了)。
- 测量,而不是猜测。 本讲 05 号脚本里”批大小 2 比批大小 1 还慢”这种现象,不测是想不到的。看到反常的数字,先确认测量方法没错,再去找原因。
- 找瓶颈。 优化不是瓶颈的部分毫无意义(§2.3)。每一讲都问:瓶颈在计算、访存、通信还是调度?
- 没有 GPU 也能学。 原课作业在 GPU 集群上完成;本讲义的代码全部在 CPU 上运行:GPU 内核用模拟器执行并逐项计数,性能用公开规格估算,多卡用”列表里的多个设备”模拟。原理是一样的,而且每一个”为什么这样更快”都能被数出来。
- 读原始论文。 这门课的阅读材料几乎都是近几年的系统论文(LightSeq、ZeRO、Megatron、GPTQ、FlashAttention、vLLM、SGLang、DistServe……)。每讲的”延伸阅读”会列出它们。
8. 小结 · Summary
- LLM 的规模七年涨了一万倍,远快于单块硬件的增长;差出来的部分只能靠系统补。2024 年以后最大的模型大多是 MoE,总参数和每个 token 的激活参数分开了。
- 每种 LLM 能力都对应一种负载形状(输入长度、输出长度、调用次数、时延要求),服务系统的优化都是针对这些形状做的。
- 语言模型 = 下一个 token 的条件概率;链式法则是恒等式,模型之间的区别只在怎样近似每个条件概率。看多长的上下文、能不能在上下文之间共享统计信息,决定了模型质量。
- 三类语言模型的差别只在注意力掩码;因果掩码让 decoder-only 模型训练时每个 token 都是训练信号、生成时能用 KV 缓存(我们逐位验证了缓存的正确性,并测出生成 59 个 token 时省下 32.6 倍的 FLOPs)。
- 预训练最小化交叉熵;交叉熵 = 不可约的熵 + 可约的 KL。困惑度 = exp(交叉熵)。在城市语言上,小 GPT 的可约损失只有最好 n-gram 的 14%。规模和数据都重要:数据少时大模型过拟合,模型太小时数据再多也没用。
- LLM 系统的核心问题:用更少的资源、更快地、在更大的数据上计算更大的模型。网络层归结为矩阵乘法、归约、逐元素映射、数据搬运四类底层运算;用算术强度和 roofline 判断瓶颈。“小”算子不贡献 FLOPs 却占时间;注意力在训练时也访存受限;解码时一切都是访存受限的。
- 训练一个 100B 模型:约 1.2×10²⁴ FLOPs,1024 块 H100(MFU 40%)约 35 天、86 万 GPU 小时;训练状态 1.62 TB;纯数据并行时梯度通信比计算还慢。服务它:至少 3 块 H100,批 1 时每秒约 128 个 token,每个 token 的 KV 缓存 3.28 MB(GQA 降到 1/10)。必须分布式、必须切分、必须重叠通信、必须批处理——这就是这门课后面所有内容的出发点。
9. 常见误区 · Common pitfalls
- “困惑度越低越好,最低能到 1。” 只有在数据完全确定时才能到 1。语言本身有随机性,任何模型的交叉熵都不可能低于真实分布的熵(城市语言里困惑度的下界是 2.118)。比较困惑度时还要保证词表和测试集相同——换一个分词器,困惑度就不可比了(第 8 讲)。
- “链式法则是一种近似。” 链式法则是恒等式;近似发生在用模型估计每个条件概率的时候(例如 n-gram 只看最近 n−1 个 token)。
- “KV 缓存是一种近似 / 会改变输出。” 对因果模型,KV 缓存是精确的:02 号脚本里缓存与不缓存的 logits 只差浮点舍入(2×10⁻⁷),贪心生成的结果完全相同。它只适用于因果注意力。
- “FLOPs 少了就会同样比例地变快。” 02 号脚本里 FLOPs 少了 32.6 倍,⏱ 实测只快了约 1.6 倍;05 号脚本里 softmax 只占 0.03% 的 FLOPs,却占 6.4% 的理想时间。时间取决于瓶颈:计算、访存、开销中最慢的那个。
- “6ND 是精确公式。” 它忽略了注意力项,长上下文、窄模型时会明显偏低(Llama-3 8B 在 8K 上下文时偏低约 28%);也不含逐元素运算和通信。
- “训练显存 = 参数量 × 2 字节(BF16)。” 混合精度 + Adam 训练要 16 字节/参数,还要加上激活值;2 字节/参数只是推理时权重的大小。
- “GPU 数加倍,训练就快一倍。” 只有在计算之外的开销(通信、同步、负载不均)都能被重叠或摊薄时才成立。§5.6 的例子里,通信本身就比计算慢。
- “推理比训练便宜,不需要优化。” 单次推理便宜,但模型会被调用几十亿次;而且解码是访存受限的,单个请求只能用上 GPU 算力的一小部分(§5.7 的批 1:每秒 128 个 token)。
- “低温采样 = 更单调。” 只有在每一步分布都有明显高峰时才成立。03 号脚本里,低温反而让不同句子从 470 个增加到 601 个,因为它把句型推向了组合最多的 T3。
10. 练习 · Exercises
练习 1(困惑度)。 (a) 证明:一个对 个 token 均匀猜测的模型,困惑度正好是 。(b) 城市语言的熵率是 0.7506 nat/token,换成 bit/token 是多少?对应的困惑度是多少?
参考答案:(a) 每个位置 ,交叉熵 ,困惑度 。(b) bit/token;困惑度 (用 bit 算是 ,结果一样——困惑度与对数的底无关)。
练习 2(链式法则与马尔可夫假设)。 01 号脚本第 3 部分里,trigram 近似把 “the students from pittsburgh who swim at night love the bridges .” 的概率低估了 4.00 倍。只看那张表,说明这个 4.00 是怎么来的。
参考答案:12 个位置里只有第 11 个(bridges)的两个概率不同:真实 0.6000,trigram 0.1501;其余位置相同,连乘时相互抵消。比值 0.6000 / 0.1501 ≈ 4.00。
练习 3(n-gram 的两种误差)。 在 01 号脚本第 4 部分的表里,为什么 2000 句数据时最好的 n 是 6,而 20000 句时是 8?如果有 200 万句数据,你预计最好的 n 和它的困惑度会怎样?
参考答案:n 越大近似误差越小(无限数据时 n = 8 到达下界),但需要估计的上下文组合越多、估计误差越大。数据越多,估计误差越小,最优的 n 就往大的方向移。有 200 万句时,n = 8 的估计误差会继续缩小,困惑度会更接近 2.123;n > 8 不再降低近似误差(已经是 0),只会增加估计误差,所以最优的 n 大概率仍是 8(或者和 n = 10 几乎打平)。写作时我用 80000 句试过:n = 8、10、14 的困惑度分别是 2.132、2.136、2.183,趋势与此一致。你可以把 01 号脚本里
make_splits的n_train改大来验证(注意运行时间会变长)。
练习 4(KV 缓存为什么只适用于因果模型)。 用一两句话解释:为什么 BERT 式的双向模型在逐词生成时不能用 KV 缓存?02 号脚本里哪个数字直接说明了这一点?
参考答案:双向注意力里,旧位置能”看到”新加入的 token,所以每加入一个 token,所有旧位置每一层的隐藏状态(以及由它们算出的 、)都会改变,之前存下的结果就作废了。02 号脚本第 3 部分:追加一个 token 后,双向模型旧位置的隐藏状态最大变化 0.817(因果模型只有 1.19×10⁻⁶)。
练习 5(参数量)。 一个 GPT 式模型:24 层、d = 2048、16 个头、词表 50257、上下文 2048、输出层与嵌入共享。用 §5.1 的公式算出它的参数量,并说出 12·L·d² 占了多少。
参考答案:;一次项 ;词嵌入 ;位置嵌入 ;最后的 LayerNorm 。合计 1,315,723,264(约 1.32B),其中 12·L·d² 占约 91.8%,词嵌入约 7.8%,位置嵌入约 0.3%。(可以用
llm_budget.param_count(ModelShape("ex", 24, 2048, 16, 50257, 2048))验证。)
练习 6(训练计算量)。 Llama-3 8B(8.03B 参数)用了约 15T token 预训练。(a) 用 6ND 估算训练 FLOPs。(b) 按 H100 峰值 989 TFLOP/s、MFU 40%,要多少 GPU 小时?(c) 它的训练上下文是 8192,用精确公式(含注意力项)重算 (b)。
参考答案:(a) FLOPs。(b) 万 GPU 小时。(c) 精确公式给出 FLOPs(多约 20%),约 61.0 万 GPU 小时。(这两个数是在假设的 MFU 下的理想估算,不是 Meta 报告的实际用量。)
练习 7(KV 缓存的显存)。 Llama-3 70B:80 层、8 个 KV 头、每头 128 维、BF16。(a) 每个 token 的 KV 缓存是多少字节?(b) 同时服务 32 条长度 8192 的序列,KV 缓存一共多大?和它的 BF16 权重(约 141 GB)比呢?
参考答案:(a) 字节 = 320 KiB。(b) GB,约是权重的 61%。如果它用 64 个 KV 头的标准多头注意力,这个数会变成 8 倍(约 687 GB)——GQA 的价值一目了然。
练习 8(解码速度的上限)。 在一块 H100(显存带宽 3.35 TB/s)上用 BF16 跑 Llama-3 8B(权重约 16.06 GB),批大小 1。(a) 忽略 KV 缓存,每秒最多能生成多少个 token?(b) 为什么提高 GPU 的算力对 (a) 没有帮助?
参考答案:(a) 每生成一个 token 要把全部权重读一遍: 毫秒,即每秒最多约 209 个 token。(加上上下文 8192 的 KV 缓存,
decode_step_seconds给出约 196 token/秒。)(b) 批 1 时算术强度约为 1 FLOP/字节,远低于 H100 的平衡点 295,计算单元大部分时间在等数据;瓶颈是显存带宽,不是算力。要提速只能少读字节(量化)、或者让一次读取服务更多 token(批处理、推测解码)。
练习 9(算术强度)。 两个各有 个元素的 BF16 张量逐元素相加。(a) 它的算术强度是多少?(b) 在 H100 上按 roofline 估算耗时,计算和访存各要多久?
参考答案:(a) 每个元素 1 次加法,读 2 个数、写 1 个数,共 6 字节: FLOP/字节。(b) 计算: 微秒;访存: 毫秒。访存比计算慢约 1800 倍——逐元素运算是典型的访存受限运算,这正是算子融合(第 10 讲)要解决的问题。
练习 10(瓶颈)。 §2.3 的工厂里,如果把二号反应器换成一个 400 吨的(仍然要 4 小时),整条线的产能变成多少?瓶颈换成了哪道工序?这对系统优化有什么启示?
参考答案:二号反应器的产能变成 100 吨/小时,新的瓶颈是锁斗(55 吨/小时),整条线从 50 提高到 55 吨/小时——把瓶颈的产能翻倍,整体只提高了 10%。启示:消除一个瓶颈之后,瓶颈会转移到下一个最慢的环节;每次优化后都要重新测量、重新找瓶颈。
练习 11(规模与数据)。 在 §3.9 的表里,d = 64 在 500 句数据上训练集”超出下界”是负数(−56.9),这意味着什么?为什么在 20000 句数据上就不再是负数了?
参考答案:负数意味着模型给训练句子分配的概率比真实概率还高——它记住了这些具体的句子(过拟合),而这些概率在没见过的测试句上就不成立了,所以测试集损失最高(178.3)。20000 句数据里,同样大小的模型已经无法逐句记忆(每个具体句子出现得太少、需要记的太多),只能学习一般规律,于是训练集和测试集的损失都接近下界(6.4 和 6.1)。
练习 12(思考题:协同设计)。 §5.7 的表里,GQA-8 在批 256 时仍然是访存受限的。设想你可以改动模型、算法、系统三者中的任何一个,列出至少三种能提高这台 8 卡服务器解码吞吐的办法,并说出每种办法改的是哪一层、代价是什么。
参考答案(不唯一):① 权重量化到 INT4(模型 / 数值表示):读权重的字节数降到 1/4,代价是可能损失精度(第 19–20 讲)。② KV 缓存量化或压缩(系统 / 数值表示):降低每条序列要读的字节、能装下更大的批,代价是精度和编解码开销(第 27 讲)。③ 推测解码(算法):用一个小模型先猜几个 token、大模型一次验证,一次读权重产生多个 token,代价是草稿模型的计算和猜错时的浪费(第 9 讲)。④ PagedAttention(系统):消除 KV 缓存碎片,同样的显存装下更多序列(第 24 讲)。⑤ 更少的 KV 头 / MQA,或 MoE(模型结构):分别缩小 KV 缓存或每个 token 的计算,代价是可能损失质量或增加通信(第 7、17 讲)。
11. 延伸阅读 · Further reading
- 原课材料:第 1 讲 slides(llmsys-01-intro.pdf);课程主页 https://llmsystem.github.io/llmsystem2026spring/ ;作业网站 https://llmsystem.github.io/llmsystemhomework/ 。
- 规模与训练计算量:Brown et al. (2020) GPT-3(表 2.1 的模型形状、训练 token 数);Kaplan et al. (2020) Scaling Laws for Neural Language Models(2.1 节的 FLOPs 推导);Hoffmann et al. (2022) Chinchilla(计算最优的参数 / 数据配比)。
- 显存与激活值:Rajbhandari et al. (2020) ZeRO(16 字节/参数的记账,第 18 讲的阅读材料);Korthikanti et al. (2022) Reducing Activation Recomputation in Large Transformer Models(激活值公式)。
- roofline 模型:Williams, Waterman & Patterson (2009) Roofline: An Insightful Visual Performance Model for Multicore Architectures。
- 硬件规格:NVIDIA H100 / A100 的数据手册(本讲用的是 SXM 版本的 dense BF16 标称值)。
- 系统化的估算练习:Austin et al. (2025) How to Scale Your Model(Google DeepMind 的在线书,对 TPU 上的同类估算讲得很细,和第 12–13 讲互补)。
12. 下一讲预告 · Next lecture
slides 的最后一页写着下一讲的两个主题:GPU 内部是什么样的,以及怎样给 GPU 编程。第 2 讲会从本讲 §4.4 那张”底层运算”的表出发:一个文本分类网络怎样拆成矩阵乘法、逐元素运算和归约;一台 GPU 服务器由哪些部件组成、数据在 CPU、内存、PCIe、NVLink 和 GPU 之间怎样流动;GPU 由哪些 SM 组成、为什么能同时跑上万个线程;一个 CUDA 内核是怎样被切成网格、线程块和线程束来执行的。我们会写一个 Python 的 SIMT 模拟器,让每一个 CUDA 概念都能被运行、被计数——这也是原课 HW1(map、zip、reduce、矩阵乘法内核)的起点。