第 1 讲 · LLM 导论与 LLM 系统的挑战 · Introduction to LLMs & the Challenges of LLM Systems

Large Language Model Systems · Lecture 01。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 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

学完这一讲,你应该能够:

  1. 说出 LLM 规模增长的大致轨迹,并解释为什么”规模”让系统问题变成了 LLM 研究的核心问题之一。
  2. 从系统负载的角度看 LLM 的各种能力:翻译、对话、少样本提示、推理、写代码并调用工具,分别对输入长度、输出长度、调用次数和时延提出了什么要求。
  3. 写出语言模型的数学定义(下一个 token 的条件概率、链式法则),区分 encoder-only、encoder-decoder、decoder-only 三类模型,并能用实验说明:它们的差别只在注意力掩码,系统代价却天差地别(为什么只有因果模型能用 KV 缓存)。
  4. 解释交叉熵损失和困惑度,知道”理论最优困惑度”的含义;能区分 n-gram 的近似误差和估计误差,理解神经语言模型为什么能同时利用长上下文和有限数据。
  5. 说出 LLM 系统的核心问题,把一个 Transformer 层拆成矩阵乘法、逐元素映射、归约、数据搬运四类底层运算,并用算术强度判断一个运算是计算受限还是访存受限。
  6. 用几条公式(参数量 ≈ 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(全课统一)

记号意思
xtx_t,x1:Tx_{1:T},x<tx_{<t}第 tt 个 token;长度为 TT 的序列;xtx_t 之前的所有 token(“前文”)
V\mathcal{V},V=∣V∣V = \lvert\mathcal{V}\rvert词表及其大小
pθ(⋅)p_\theta(\cdot)参数为 θ\theta 的模型给出的概率
NN模型参数量(parameters)
DD训练用的 token 数(data)
LL,dd,HH,dhd_h层数、隐藏维度(d_model)、注意力头数、每个头的维度(d=H⋅dhd = H \cdot d_h)
TT序列长度 / 上下文长度
BB,bb批大小(batch size)
FLOPs浮点运算次数(一次乘法或一次加法各算 1 次)
FLOP/s每秒浮点运算次数(算力)
算术强度 arithmetic intensity一个运算的 FLOPs ÷ 它必须读写的字节数,单位 FLOP/字节
MFUModel FLOPs Utilization:实际达到的有效算力 ÷ 硬件峰值算力
nat以 ee 为底的信息量单位(以 2 为底时叫 bit);1 nat ≈ 1.443 bit
KB / MB / GB / TB以 1000 为进位(10310^3、10610^6、10910^9、101210^{12} 字节);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 列出三个学习目标:

  1. 理解现代 LLM 系统的关键技术——例如,训练一个 100B 模型需要多少资源?
  2. 工程能力:亲手实现 LLM 系统的关键部件——快速的 CUDA 内核、可扩展的训练系统、高效的推理。
  3. 创新:发现机会,解决 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
T3the <人群> from <城市> who <活动> <时间> love the <特色> .the students from pittsburgh who swim at night love the bridges .0.35
T4in <城市> 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 规模”图。把图上的模型和它们公开的参数量列出来:

年份模型参数量备注
2017Transformer(原始论文)base 约 65M,big 约 213M机器翻译
2018GPT-10.117B第一个”预训练 + 微调”的 GPT
2019GPT-21.5B
2020GPT-3175B少样本提示、上下文学习
2021Gopher(DeepMind)280B
2022PaLM(Google)540B
2023GPT-4未公开原课第 14 讲的同一张图上标的是 1.8T,这是外界流传的估计
2024LLaMA-3.1405B开放权重里最大的稠密模型之一
2024DeepSeek-V3671B(每个 token 激活 37B)混合专家(MoE)
2025Qwen3235B(激活 22B)MoE
2025Kimi K2约 1T(激活 32B)MoE

两个趋势一眼就能看出来:

  1. 七年涨了一万倍。 从 GPT-1 的 1 亿参数到万亿参数。同一时期,单块 GPU 的显存只从十几 GB 涨到一两百 GB(V100 16/32 GB、A100 80 GB、H100 80 GB、B200 约 192 GB),16 位精度的峰值算力涨了十几倍。模型增长的速度远远超过了单块硬件增长的速度——差出来的部分,只能靠系统来补:多卡并行、显存优化、压缩、更好的内核。
  2. 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 就有点奇怪但不是不可能。语言模型做的就是这件事,只不过它给出的不是一个答案,而是整个词表上的一个概率分布:

P(xt∣提示词, x1:t−1)P(x_t \mid \text{提示词},\ x_{1:t-1})

为什么说 “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 ofbridges 0.600 · rivers 0.200 · steel 0.120 · hills 0.060 · corn 0.020
santa barbara has very nicebeaches 0.500 · weather 0.400 · views 0.060 · food 0.030 · snow 0.010
the students from pittsburgh who swim at night love thebridges 0.600 · rivers 0.200 · steel 0.120 · hills 0.060 · corn 0.020
the chefs from seattle who read on weekends love thecoffee 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

两点值得注意:

  1. 答案是分布,不是一个词。 corn 只有 0.02 的概率,但它不是 0。“按分布采样”时偶尔会生成它——这就是为什么同一个提示词问两次,LLM 会给出不同的回答。
  2. 看多长的上下文,决定了能答多好。 同样以 “love the” 结尾,pittsburgh 的人最可能爱 bridges(0.60),seattle 的人最可能爱 coffee(0.50)。如果只看最近两个 token,就只能给出所有城市的平均:bridges 降到 0.150。能利用多长的上下文,直接决定模型质量——而长上下文正是 LLM 系统最昂贵的需求之一(KV 缓存随上下文线性增长,注意力的计算随它平方增长)。

3.2 链式法则:恒等式,而不是假设

一句话的概率怎样由”下一个 token 的概率”得到?slides 写的是概率论里的链式法则:

P(x1:T)=∏t=1TP(xt∣x1:t−1)P(x_{1:T}) = \prod_{t=1}^{T} P(x_t \mid x_{1:t-1})

例如 P(Pittsburgh is a city of bridges)=P(Pittsburgh)⋅P(is∣Pittsburgh)⋅P(a∣Pittsburgh is)⋯P(\text{Pittsburgh is a city of bridges}) = P(\text{Pittsburgh}) \cdot P(\text{is} \mid \text{Pittsburgh}) \cdot P(\text{a} \mid \text{Pittsburgh is}) \cdots。

为什么这样做? 直接给每个完整句子一个概率是不可能的——可能的句子有无穷多个。链式法则把”给句子打分”变成了”一次只预测一个 token”,而后者可以用同一个函数(神经网络)反复调用来实现。

关键的一点:链式法则是恒等式,它不做任何假设。 只要每一步都以全部前文为条件,连乘的结果一定等于句子的真实概率。01 号脚本第 3 部分验证了这一点:对 “the students from pittsburgh who swim at night love the bridges .”,12 个真实条件概率连乘得到 log⁡p=−8.3630425116\log p = -8.3630425116,直接查表也是 −8.3630425116-8.3630425116,只差 5.3×10−155.3 \times 10^{-15}(浮点舍入)。

近似发生在模型这一步:用什么东西去估计每一个 P(xt∣x<t)P(x_t \mid x_{<t})。最古老的办法是 n-gram 的马尔可夫假设——只看最近 n−1n-1 个 token。把上面那句话换成”只看最近两个 token”的最好估计,前 11 个位置都和真实值一样(那些位置本来就只依赖最近的词),唯独最后的 bridges 从 0.600 变成 0.150,整句的概率被低估了 4.00 倍——它依赖的 pittsburgh 在窗口之外。slides 说得很简洁:用神经网络(Transformer、CNN、RNN)来预测每一个条件概率。各种语言模型的区别,只在于怎样近似这一串条件概率。

3.3 为什么 ChatGPT 改变了 AI 的格局

slides 给了三条原因:

  1. 在海量原始文本上预训练(约 300B token)+ 少量人类反馈。 预训练的数据不需要人工标注,互联网上有的是;人类反馈只需要相对很少的量,就能把一个”续写机器”变成一个”助手”。
  2. 能听懂指令(instruction following)。 用户用自然语言描述任务即可,不需要为每个任务训练一个模型。
  3. 上下文学习(in-context learning)。 在提示里给几个例子,模型就能泛化到新任务(§2.1 的尤达风格改写)。

从系统的角度看,这三条有一个共同的后果:一个模型、一种接口(续写文本)服务所有任务。这让”把这一个模型训练好、服务好”的价值变得极高,也让这门课的所有优化都有了用武之地。

3.4 三类语言模型:差别只在”谁能看谁”

slides 把语言模型分成三类:

  • encoder-only(BERT):掩码语言模型(masked LM)。随机遮住一些 token,让模型根据两边的上下文猜被遮住的词,建模 P(xmask∣x其余)P(x_{\text{mask}} \mid x_{\text{其余}})。它主要用来做理解类任务;也可以用来生成(例如非自回归翻译 NAT,以及 Lei Li 组的 REDER),但不常见。
  • encoder-decoder(原始 Transformer、T5、翻译模型):编码器读完整个输入 xx,解码器一个一个地生成输出 yy,建模 P(y∣x)=∏tP(yt∣y<t,x)P(y \mid x) = \prod_t P(y_t \mid y_{<t}, x)。编码器可以是双向 LSTM、Transformer 或 CNN;解码器可以是 LSTM、Transformer、非自回归 Transformer 或 CNN。
  • decoder-only(GPT 系列、LLaMA 及今天几乎所有 LLM):因果语言模型(causal LM),直接建模 P(x)=∏tP(xt∣x<t)P(x) = \prod_t P(x_t \mid x_{<t})。可以用 Transformer 或单向 LSTM 实现。slides 的评价是:最流行的 LLM 结构。

用 Transformer 实现时,三类模型用的是同一种注意力运算,区别只在注意力掩码(attention mask)——位置 ii 可以”看”哪些位置 jj。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 都有用。 因果掩码意味着:改动第 jj 个 token,只有位置 ≥j\ge j 的输出会变。02 号脚本第 2 部分把 “pittsburgh” 换成 “seattle”,因果模型里前 4 个位置的隐藏状态完全不变(○○○○●●●●●●●●●),双向模型里所有位置都变了(●●●●●●●●●●●●●)。所以一个长度为 TT 的序列,一次前向就能同时得到 TT 个”预测下一个词”的训练样本,而且每个预测都看不到答案。BERT 式的掩码语言模型只对被遮住的约 15% 的位置计算损失(原始 BERT 的设定);encoder-decoder 只对目标语言那一半计算损失(第 5 部分):

模型 / 训练目标处理的 token有监督的预测每个 token 的训练信号
decoder-only(下一个 token)5125121.00
encoder-only(遮住 15%)512760.15
encoder-decoder(源、目标各一半)5122560.50

(2) 生成时可以缓存过去的计算(KV 缓存)。 自回归生成时,每生成一个新 token 都要再跑一次模型。因果模型里,旧位置看不到新 token,所以它们在每一层算出的键 kk 和值 vv 永远不会变——算一次存起来,下一步直接复用。用一层注意力画出来:

 预填充 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 后,因果模型旧位置的隐藏状态最多只变 1.19×10−61.19 \times 10^{-6}(浮点舍入量级),双向模型变了 0.8170.817;用 KV 缓存逐个 token 计算出的 logits 和整句一次计算的结果最多差 2.09×10−72.09 \times 10^{-7}。双向模型每来一个新 token,所有位置都要重算——没有东西可缓存。

(3) 缓存省下的计算随长度平方增长。 02 号脚本第 4 部分用 PyTorch 的 FLOP 计数器数了一下(提示词 5 个 token,贪心生成 G 个):

生成 token 数 G位置计算次数(无缓存 / 有缓存)MFLOPs(无缓存 / 有缓存)倍数结果相同
868 / 1214.4 / 2.55.7×是
32656 / 36144.0 / 7.818.5×是
592006 / 63458.6 / 14.132.6×是

没有缓存时,第 kk 步要把 P+kP+k 个位置全部重算,总量随 GG 平方增长;有缓存时每步只算一个新位置。但要注意两点:有缓存时每步的计算并不是常数——新 token 仍要和所有旧的 kk、vv 做注意力,这一项随上下文线性增长;而且缓存要占显存,每个 token、每一层都要存一个 kk 和一个 vv。§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 的训练画成三个阶段:

  1. 预训练(pre-training):在海量原始文本(维基百科、书籍、Reddit 等)上做下一个 token 预测。数据越多越好、通常从网上爬取,但质量很重要(维基百科、书籍、过滤过的 Reddit 帖子)。模型结构上的选择(稀疏注意力、混合专家等)也在这一阶段决定。
  2. 监督微调(supervised fine-tuning, SFT):用”问题 → 好的回答”这样的成对数据继续训练,让模型学会按指令回答。
  3. 强化学习(RL,例如 RLHF):用偏好数据(同一个问题的两个回答,哪个更好)训练,让模型的回答更符合人的偏好。

从系统的角度看,三个阶段的负载完全不同:预训练是超大规模、长时间、吞吐优先的训练(第 14–18 讲);SFT 的数据量小得多,常常用参数高效微调(第 23 讲);RLHF 要同时运行好几个模型、在”生成”和”训练”之间来回切换(第 25 讲)。

3.6 训练目标:交叉熵

预训练的损失是下一个 token 预测的交叉熵(cross-entropy loss):

LCE=−1T∑t=1Tlog⁡Pθ(xt∣x<t)\mathcal{L}_{\text{CE}} = -\frac{1}{T} \sum_{t=1}^{T} \log P_\theta(x_t \mid x_{<t})

直觉。 模型在第 tt 步给真正出现的那个 token 分配了概率 Pθ(xt∣x<t)P_\theta(x_t \mid x_{<t});分配得越高,−log⁡-\log 就越小。把所有位置平均起来,就是”平均每个 token 有多惊讶”。

为什么是交叉熵? 最小化交叉熵等价于最大似然估计:让模型给训练数据分配的总概率最大。它还有一个漂亮的分解:

E[−log⁡Pθ]⏟模型的交叉熵=H(P真实)⏟不可约:语言本身的随机性+KL(P真实 ∥ Pθ)⏟可约:模型还没学到的\underbrace{\mathbb{E}\big[-\log P_\theta\big]}_{\text{模型的交叉熵}} = \underbrace{H(P_{\text{真实}})}_{\text{不可约:语言本身的随机性}} + \underbrace{\mathrm{KL}(P_{\text{真实}} \,\|\, P_\theta)}_{\text{可约:模型还没学到的}}

KL 散度永远 ≥0\ge 0,所以任何模型的交叉熵都不可能低于真实分布的熵。在城市语言里,我们能精确算出第一项(熵率 0.7506 nat/token),于是每个模型”还差多少”就是第二项——本讲后面的表格里”超出下界”的那一列就是它(单位:毫 nat/token)。

训练是怎样并行的? 因为因果掩码(§3.4),一个长度为 TT 的窗口一次前向就给出 TT 个位置的预测,损失是它们的平均——这叫教师强制(teacher forcing):训练时每个位置的”前文”用的是真实数据,而不是模型自己生成的。

数据怎样喂进去:打包(packing)。 句子长短不一,最直接的做法是把每句补齐(padding)到同样长度——但补齐的位置算了也白算。03 号脚本第 1 部分在 20000 句训练集上算了一下:

做法需要计算的位置数浪费在填充上
每句补齐到全局最长280,00030.2%
随机每 32 句一批、补齐到批内最长278,78429.9%
先按长度排序再分批(分桶)195,4240.1%
首尾相接打包成一条 token 流195,3080.0%

随机分批几乎没有改善:每 32 句里几乎总有一句最长的 T3。分桶能消除填充,但同一批里几乎全是同一种句型,批次不再是整体分布的随机样本。LLM 预训练几乎都用打包:把文档首尾相接(中间放一个分隔符,这里是 <bos>),按固定长度的窗口切开,每个位置都是真实 token。

3.7 怎样衡量一个语言模型

困惑度(perplexity) 是语言模型最常用的指标:

PPL=exp⁡ ⁣(−1T∑t=1Tlog⁡Pθ(xt∣x<t))=exp⁡(LCE)\mathrm{PPL} = \exp\!\Big(-\frac{1}{T}\sum_{t=1}^{T} \log P_\theta(x_t \mid x_{<t})\Big) = \exp(\mathcal{L}_{\text{CE}})

直觉。 困惑度可以理解成”模型平均在几个候选之间犹豫”。如果模型对每个 token 都在 VV 个词里均匀地猜,困惑度正好是 VV;如果它每次都确定地猜对,困惑度是 1。城市语言的理论最优困惑度是 2.118:即使知道真实分布,平均每个位置也还有约 2 个”同样合理”的选择。

01 号脚本第 4 部分在同一个测试集(2000 句、17577 个 token)上比较了各种 n-gram,并把误差拆成两种:

n(看 n−1 个 token)12345681014
无限数据(精确计算)44.9873.0312.4802.4122.3572.2602.1232.1232.123
2000 句训练数据45.0393.0762.5012.4392.4102.3702.4572.6082.934
20000 句训练数据44.9953.0362.4822.4152.3622.2762.1702.1922.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-gram44.9953053.6
3-gram2.482156.0
8-gram(n-gram 里最好的)2.17021.8
小 GPT(109,312 参数)2.1303.1
真实分布(理论下界)2.1230

小 GPT 的可约损失只有最好的 n-gram 的 14%。

它学到的分布像不像? 03 号脚本第 3 部分把小 GPT 和 3-gram 的下一个 token 分布与真实分布比较(每格依次是 真实 / 小 GPT / 3-gram):

前缀前两个 tokenKL(真实‖小 GPT)KL(真实‖3-gram)
pittsburgh is a city ofbridges 0.60/0.62/0.15 · rivers 0.20/0.19/0.050.00631.361
santa barbara has very nicebeaches 0.50/0.47/0.09 · weather 0.40/0.42/0.110.00821.210
the students from pittsburgh who swim at night love thebridges 0.60/0.61/0.15 · rivers 0.20/0.18/0.040.00511.382
the chefs from seattle who read on weekends love thecoffee 0.50/0.48/0.09 · rain 0.25/0.30/0.040.01691.405
the doctors from miami who sing in winter love thebeaches 0.60/0.62/0.10 · music 0.20/0.16/0.080.02351.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.00.9890.0190.3710.197470
0.51.0000.0000.6530.311601

温度 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ᵀ 和 权重·VHW1 的 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 模型估计:t≈max⁡(FLOPs/峰值算力, 字节数/带宽)t \approx \max(\text{FLOPs} / \text{峰值算力},\ \text{字节数} / \text{带宽})。画成图(对数坐标)就是一条”屋顶线”:

 能达到的算力 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

三个结论,分别是后面三讲的出发点:

  1. “小”算子不做运算,却占时间。 softmax、GELU、LayerNorm、残差相加几乎不贡献 FLOPs,却要把整块数据读一遍写一遍。模型越小越明显:GPT-2 small 上矩阵乘法只占 57.3% 的时间。把几个小算子合成一个内核、中间结果不落显存,叫算子融合(kernel fusion)——第 10–11 讲(LightSeq)的核心。
  2. 注意力里的矩阵乘法也是访存受限的。 Q·Kᵀ 要把”头数 × T × T”的分数矩阵写回显存,softmax 再读出来、写回去。FlashAttention(第 21 讲)的核心就是不把这个矩阵写出去。
  3. 解码时一切都是访存受限的。 每个 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 GB6.33 s4.05 s0.45 s0.060 s
一层的 BF16 权重2.52 GB79 ms50 ms5.6 ms0.75 ms
一条 2048 token 序列的 KV 缓存(MHA)6.71 GB0.21 s0.13 s15 ms2.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,远大于缓存)做 y=xW⊤y = x W^\top: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 个 d×dd \times d 矩阵(4d24d^2),MLP 的两个 d×4dd \times 4d 矩阵(8d28d^2),合计 12d212d^2;再加上偏置和两个 LayerNorm 共 13d13d 个参数。LL 层就是 12Ld2+13Ld12Ld^2 + 13Ld,再加上词嵌入 VdVd 和位置嵌入 TdTd。当 dd 上万时,一次项可以忽略:N≈12Ld2N \approx 12 L d^2 + 嵌入。

验证 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 175B96 层、d = 12288174.60B175B173.9B
Llama-3 8B32 层、d = 4096、GQA 32/8、SwiGLU8.03B8.03B6.4B
Llama-3 70B80 层、d = 819270.55B70.6B64.4B
Llama-3.1 405B126 层、d = 16384405.85B405B405.9B
假想 100B80 层、d = 10240101.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,所以前向约 2N2N FLOPs / token。反向传播对每个矩阵乘法 Y=XWY = XW 要算两个梯度:对输入 ∂L/∂X=(∂L/∂Y)W⊤\partial L/\partial X = (\partial L/\partial Y) W^\top(传给前一层)和对权重 ∂L/∂W=X⊤(∂L/∂Y)\partial L/\partial W = X^\top (\partial L/\partial Y)(交给优化器),每个都和前向一样大,所以反向约 4N4N。合计:

Ctrain≈6NDC_{\text{train}} \approx 6 N D

验证。 04 号脚本第 3 部分用 PyTorch 的 FlopCounterMode 数了三个小模型一次前向 + 反向的 FLOPs,和精确公式 3×(2W+4LTd)×token 数3 \times (2W + 4LTd) \times \text{token 数}(WW 是参与矩阵乘法的权重数,4LTd4LTd 是注意力的 Q·Kᵀ 和 权重·V)逐个相等(例如 368,050,176 = 368,050,176)。

但 6ND 本身是个近似,它忽略了注意力项 4LTd4LTd。这一项相对于 2N≈24Ld22N \approx 24Ld^2 的比例大约是 T/(6d)T/(6d) 的量级:

模型训练上下文 T注意力项占前向 FLOPs6·N·D 比精确值
GPT-3 175B20482.7%偏低约 3%
假想 100B20483.2%偏低约 4%
Llama-3 8B819222.2%偏低约 28%
Llama-3.1 405B81927.7%偏低约 8%

所以 6ND 适合做数量级估算;上下文越长、模型越”窄”,越要把注意力项加回来。(这里的注意力按”算满 T×T”计;真实的注意力内核利用因果掩码只算一半,第 21 讲。)

对假想 100B:C≈6×101.2B×2.02T≈1.23×1024C \approx 6 \times 101.2\text{B} \times 2.02\text{T} \approx 1.23 \times 10^{24} FLOPs(加上注意力项多 3.3%)。

5.4 时间与钱

天数=CGPU 数×每卡峰值算力×MFU÷86400\text{天数} = \frac{C}{\text{GPU 数} \times \text{每卡峰值算力} \times \text{MFU}} \div 86400

H100 的 BF16 标称峰值是 989 TFLOP/s,大规模训练的 MFU 通常在 30%–50%,我们取 40%(假设)。04 号脚本第 4 部分:

GPU 数天数GPU 小时成本(按假设的 2 美元 / GPU 小时)GPU 满载耗电
135,963(约 99 年)86.3 万约 170 万美元604 MWh
102435.186.3 万约 170 万美元604 MWh
40968.886.3 万约 170 万美元604 MWh
163842.286.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 权重2202 GB
BF16 梯度2202 GB
FP32 主权重4405 GB
FP32 Adam m4405 GB
FP32 Adam v4405 GB
合计(不含激活值)161.62 TB

至少要 21 块 80 GB 的 H100 才放得下这些”训练状态”。还没算激活值:反向传播要用到前向时各层的中间结果。按 Korthikanti 等人(2022)的公式(每层约 s⋅b⋅d⋅(34+5as/d)s \cdot b \cdot d \cdot (34 + 5 a s / d) 字节,ss 是序列长度、bb 是批大小、aa 是头数),一条 2048 token 的序列、不做任何重计算时要 191 GB——是一块 H100 显存的 2.4 倍。

第二个结论:必须把模型和状态切开。 张量并行和流水线并行切模型(第 16 讲),ZeRO 切优化器状态、梯度和参数(第 18 讲),激活重计算用”多算一遍”换显存(第 18 讲),FlashAttention 不保存 T×T 的注意力矩阵(第 21 讲)。

5.6 通信

假想一个”纯数据并行”的方案:1024 块卡各存一份完整模型(实际放不下,但先这样算),每块卡算自己那份数据的梯度,再用环形 all-reduce(第 14 讲推导)把梯度平均。每块卡要发送(也接收)约 2×2 \times 梯度字节数: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、每一层要存一个键向量和一个值向量:2×L×Hkv×dh×22 \times L \times H_{kv} \times d_h \times 2 字节。

模型每 token 的 KV 缓存剩余显存能存多少 token相当于多少条 2048 token 的序列
假想 100B(MHA,80 个 KV 头)3.28 MB133,53965
假想 100B + GQA(8 个 KV 头)328 KB1,335,391652

GQA 让 KV 缓存缩小到 1/10,能同时服务的序列数多 10 倍——“模型结构为系统让路”的典型例子。

解码速度的上限。 每解码一步(每个序列生成 1 个 token),要把全部权重读一遍(整批共享),再读每条序列自己的 KV 缓存;roofline 给出理想耗时的下限(04 号脚本第 5 部分,8 卡理想均分,上下文 2048):

模型批大小KV 缓存放得下每步毫秒瓶颈token/秒(全批合计)
MHA16.7 GB✓7.80访存128
MHA16107 GB✓11.56访存1,384
MHA64430 GB✓23.58访存2,714
MHA2561.72 TB✗———
GQA-810.67 GB✓6.45访存155
GQA-81610.7 GB✓6.83访存2,344
GQA-86443 GB✓8.03访存7,971
GQA-8256172 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 卡),看训练天数怎样变:

GPUMFU训练 token 数天数
A10020%20×N = 2.02T222.7
A10040%20×N111.3
H10020%20×N70.2
H10040%20×N35.1
H10060%20×N23.4
H10040%3×N = 0.30T(GPT-3 式)5.3
  • 训练 token 数从约 3×N 到 20×N:计算量差将近 7 倍——这是模型 / 数据的选择。
  • A100 换成 H100:快约 3 倍——这是硬件。
  • MFU 从 20% 提到 60%:快 3 倍——这是系统。同样的模型、同样的数据、同样的硬件,系统做得好不好,训练时间能差几倍、成本能差上百万美元。

一页纸的总账(假想 100B,Chinchilla 式训练):

问题答案关键公式后续讲次
多少参数101.2B12·L·d² + 嵌入6、7
多少数据2.02T token20 × N(假设)7
多少计算1.23×10²⁴ FLOPs6ND(+注意力项)6、14
多少时间 / 钱1024 块 H100 约 35 天,约 86 万 GPU 小时C / (GPU 数 × 峰值 × MFU)2–4、10–13
多少显存训练状态 1.62 TB + 激活值16 字节 × N16、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 生成与推测解码“多少参数、多少计算”;“服务时每步做什么”
③ 在加速器上加速 Transformer10–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

  1. 先估算,再动手。 遇到任何一个系统问题,先用 §5 的那几条公式算一下理想情况下的上下限。估算错了也没关系,错在哪里本身就很有信息量(本讲的 04 号脚本里,“6ND 在大模型上只差几个百分点”就被 Llama-3 8B 的数字推翻了)。
  2. 测量,而不是猜测。 本讲 05 号脚本里”批大小 2 比批大小 1 还慢”这种现象,不测是想不到的。看到反常的数字,先确认测量方法没错,再去找原因。
  3. 找瓶颈。 优化不是瓶颈的部分毫无意义(§2.3)。每一讲都问:瓶颈在计算、访存、通信还是调度?
  4. 没有 GPU 也能学。 原课作业在 GPU 集群上完成;本讲义的代码全部在 CPU 上运行:GPU 内核用模拟器执行并逐项计数,性能用公开规格估算,多卡用”列表里的多个设备”模拟。原理是一样的,而且每一个”为什么这样更快”都能被数出来。
  5. 读原始论文。 这门课的阅读材料几乎都是近几年的系统论文(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。” 只有在数据完全确定时才能到 1。语言本身有随机性,任何模型的交叉熵都不可能低于真实分布的熵(城市语言里困惑度的下界是 2.118)。比较困惑度时还要保证词表和测试集相同——换一个分词器,困惑度就不可比了(第 8 讲)。
  2. “链式法则是一种近似。” 链式法则是恒等式;近似发生在用模型估计每个条件概率的时候(例如 n-gram 只看最近 n−1 个 token)。
  3. “KV 缓存是一种近似 / 会改变输出。” 对因果模型,KV 缓存是精确的:02 号脚本里缓存与不缓存的 logits 只差浮点舍入(2×10⁻⁷),贪心生成的结果完全相同。它只适用于因果注意力。
  4. “FLOPs 少了就会同样比例地变快。” 02 号脚本里 FLOPs 少了 32.6 倍,⏱ 实测只快了约 1.6 倍;05 号脚本里 softmax 只占 0.03% 的 FLOPs,却占 6.4% 的理想时间。时间取决于瓶颈:计算、访存、开销中最慢的那个。
  5. “6ND 是精确公式。” 它忽略了注意力项,长上下文、窄模型时会明显偏低(Llama-3 8B 在 8K 上下文时偏低约 28%);也不含逐元素运算和通信。
  6. “训练显存 = 参数量 × 2 字节(BF16)。” 混合精度 + Adam 训练要 16 字节/参数,还要加上激活值;2 字节/参数只是推理时权重的大小。
  7. “GPU 数加倍,训练就快一倍。” 只有在计算之外的开销(通信、同步、负载不均)都能被重叠或摊薄时才成立。§5.6 的例子里,通信本身就比计算慢。
  8. “推理比训练便宜,不需要优化。” 单次推理便宜,但模型会被调用几十亿次;而且解码是访存受限的,单个请求只能用上 GPU 算力的一小部分(§5.7 的批 1:每秒 128 个 token)。
  9. “低温采样 = 更单调。” 只有在每一步分布都有明显高峰时才成立。03 号脚本里,低温反而让不同句子从 470 个增加到 601 个,因为它把句型推向了组合最多的 T3。

10. 练习 · Exercises

练习 1(困惑度)。 (a) 证明:一个对 VV 个 token 均匀猜测的模型,困惑度正好是 VV。(b) 城市语言的熵率是 0.7506 nat/token,换成 bit/token 是多少?对应的困惑度是多少?

参考答案:(a) 每个位置 P=1/VP = 1/V,交叉熵 =−log⁡(1/V)=log⁡V= -\log(1/V) = \log V,困惑度 =exp⁡(log⁡V)=V= \exp(\log V) = V。(b) 0.7506/ln⁡2≈1.0830.7506 / \ln 2 \approx 1.083 bit/token;困惑度 =e0.7506≈2.118= e^{0.7506} \approx 2.118(用 bit 算是 21.0832^{1.083},结果一样——困惑度与对数的底无关)。

练习 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,所有旧位置每一层的隐藏状态(以及由它们算出的 kk、vv)都会改变,之前存下的结果就作废了。02 号脚本第 3 部分:追加一个 token 后,双向模型旧位置的隐藏状态最大变化 0.817(因果模型只有 1.19×10⁻⁶)。

练习 5(参数量)。 一个 GPT 式模型:24 层、d = 2048、16 个头、词表 50257、上下文 2048、输出层与嵌入共享。用 §5.1 的公式算出它的参数量,并说出 12·L·d² 占了多少。

参考答案:12Ld2=12×24×20482=1,207,959,55212Ld^2 = 12 \times 24 \times 2048^2 = 1{,}207{,}959{,}552;一次项 13Ld=638,97613Ld = 638{,}976;词嵌入 Vd=102,926,336Vd = 102{,}926{,}336;位置嵌入 Td=4,194,304Td = 4{,}194{,}304;最后的 LayerNorm 2d=4,0962d = 4{,}096。合计 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) 6×8.03×109×15×1012≈7.23×10236 \times 8.03 \times 10^9 \times 15 \times 10^{12} \approx 7.23 \times 10^{23} FLOPs。(b) 7.23×1023/(989×1012×0.4)/3600≈50.77.23 \times 10^{23} / (989 \times 10^{12} \times 0.4) / 3600 \approx 50.7 万 GPU 小时。(c) 精确公式给出 8.69×10238.69 \times 10^{23} 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) 2×80×8×128×2=327,6802 \times 80 \times 8 \times 128 \times 2 = 327{,}680 字节 = 320 KiB。(b) 327,680×8192×32≈85.9327{,}680 \times 8192 \times 32 \approx 85.9 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 要把全部权重读一遍:16.06 GB/3.35 TB/s≈4.816.06 \text{ GB} / 3.35 \text{ TB/s} \approx 4.8 毫秒,即每秒最多约 209 个 token。(加上上下文 8192 的 KV 缓存,decode_step_seconds 给出约 196 token/秒。)(b) 批 1 时算术强度约为 1 FLOP/字节,远低于 H100 的平衡点 295,计算单元大部分时间在等数据;瓶颈是显存带宽,不是算力。要提速只能少读字节(量化)、或者让一次读取服务更多 token(批处理、推测解码)。

练习 9(算术强度)。 两个各有 10910^9 个元素的 BF16 张量逐元素相加。(a) 它的算术强度是多少?(b) 在 H100 上按 roofline 估算耗时,计算和访存各要多久?

参考答案:(a) 每个元素 1 次加法,读 2 个数、写 1 个数,共 6 字节:1/6≈0.171/6 \approx 0.17 FLOP/字节。(b) 计算:109/989×1012≈1.010^9 / 989 \times 10^{12} \approx 1.0 微秒;访存:6×109/3.35×1012≈1.796 \times 10^9 / 3.35 \times 10^{12} \approx 1.79 毫秒。访存比计算慢约 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、矩阵乘法内核)的起点。

← 返回课程目录