22 章 · 9,450 行代码 · 不需要 GPU
从零构建LLM 推理引擎的全过程。
调用推理 API 谁都会,难的是说清楚 vLLM 为什么能比一段简短的 PyTorch 循环快 40 倍。本课程要补的就是这段落差:从最朴素的生成循环出发,一次消除一个瓶颈。
每章配一张机制图、一个可以自己上手调的模拟器、六道带解析的习题,还有一个能直接运行、并回头验证本章结论的文件。
第 1 章 · 整个引擎
01
先写出循环,再逐一消除瓶颈。
引擎调用模型、采样一个 token、把它追加到序列,然后重复。后续章节要处理的,就是这个短循环藏起来的东西:重复计算、硬件空闲,以及迟早会耗尽的显存。
02
几乎所有优化都是显存优化。
decode 要读完整个模型才产出一个 token,瓶颈在于搬运的字节数,而非算术运算。KV 缓存、分页、前缀复用和量化都由此而来,它们也占了本课程的大半篇幅。
03
逐步建立阅读生产级引擎的能力。
每章都把核心概念落到 vLLM、SGLang、llama.cpp、picoLM 或 quant.cpp 上,读完总有一份具体的源码可以接着打开。
这门课写给谁
四种走到这里的路径
前置知识:会 Python,大致知道 transformer 是什么。微积分、CUDA 和 GPU 都用不上。
你在推理引擎之上做产品
搞清楚哪些参数真正影响性能,以及为什么。批处理、调度和服务层这几章,会把配置项和你能观察到的线上行为对应起来。
S09–S11 · 调度→你在面推理 / 系统方向的岗位
把「decode 为什么受限于显存带宽」「PagedAttention 到底往 page 里放什么」这类反复出现的问题,答成经得起追问的版本。
S05–S06 · 缓存→你翻过一次 vLLM 源码,然后放弃了
每章给出的源码对应关系,会把课程里的精简实现映射到 vLLM、SGLang、llama.cpp 等真实引擎上,让你知道自己该看哪个文件。
引擎对比→你喜欢通过实验来学习
二十二个模拟器跑的是真实实现,不是预设动画。把 KV 池压满,看着请求被抢占;引擎就在浏览器里运行,验证一个判断只要几秒。
S06 · 试试模拟器→
课程结构
五个层次,二十章,外加两章收官
每一层都建立在上一层引出的约束之上。第一遍建议按顺序读;顺序一乱,技术之间的依赖关系就看不出来了。
模型本身
S01–S04把权重变成 token:分词器、Transformer 前向传播、采样。任何优化出现之前,这个循环就已经在了。
- S01137 行
生成循环
自回归解码
推理引擎就是一个把模型自己的输出再喂回去的 while 循环。这门课余下的全部内容,都是在优化这个循环。
- S02232 行
分词
从字节到 token id
模型从来看不到文本。BPE 分词器把字节压缩进一个固定词表,你报出的每个延迟数字都以 token 计,而不是以字符计。
- S03251 行
Transformer 前向传播
RMSNorm、RoPE、GQA、SwiGLU
现代 decoder block 只有七个操作。用 NumPy 老老实实写一遍,之后所有 kernel 优化就都有了对照的正确答案。
- S04206 行
采样
把 logits 变成一个 token
采样器是引擎里最便宜的部分,却是用户感受最强烈的部分。temperature、top-k、top-p、min-p 和各种惩罚,全都是在对 logits 动手术。
……于是循环正确了,却慢到不能用。所以:
显存与 KV 缓存
S05–S08几乎所有推理优化本质上都是显存优化:缓存 KV、给它分页、共享、压缩。
- S05205 行
KV 缓存
decode 为什么是显存带宽瓶颈
没有缓存时,生成第 N 个 token 的代价是 O(N²);有了缓存则是 O(N)。瓶颈也随之从算力转移到显存带宽。
- S06283 行
PagedAttention
给 KV 缓存做虚拟内存
为每个请求分配连续 KV 缓冲区,会因碎片浪费掉 60–80% 的 KV 缓存显存。把缓存分页成固定大小的 block,几乎能全部收回。
- S07263 行
前缀缓存
同一段 prompt 绝不算第二遍
共享的 system prompt、few-shot 示例和多轮对话意味着:大多数 prefill token 之前已经算过了。给 block 做哈希,然后复用。
- S08252 行
量化
让每个权重和每条 KV 少占几个 bit
decode 速度取决于每个 token 搬运的字节数,位宽减半,延迟也几乎减半。剩下要决定的,是让误差落在哪里。
……于是稀缺资源变成了显存,而不是算力。所以:
批处理与调度
S09–S12单个请求会浪费一整张 GPU。连续批处理、调度器、抢占和分块 prefill 让它保持满载。
- S09244 行
连续批处理
迭代级调度
静态批处理让所有请求都得等最慢的那一个。改成在每次前向之间进出请求,吞吐能提升数倍。
- S10442 行
调度器
队列、预算与抢占
每一步调度器都要决定哪些请求能跑。公平性、延迟 SLO、显存不足后的恢复策略,都在这里。
- S11190 行
分块 Prefill
别让长 prompt 卡住所有 decode
一次 32k token 的 prefill 会把排在它后面的所有 decode 堵上一秒。把 prefill 切成小块,混进 decode 批次里。
- S12183 行
FlashAttention
在线 softmax,不落 N² 矩阵
注意力根本不需要把分数矩阵物化出来。分块加上滚动的最大值与求和,就把 O(N²) 的显存开销变成了 O(N)。
……于是一张 GPU 被大量请求填满了。所以:
解码加速
S13–S16打破「一次前向只出一个 token」的规则,并约束模型被允许说出什么。
- S13185 行
算子融合与 CUDA Graph
干掉 kernel 启动开销
小 batch 时 GPU 是在等 CPU。融合算子、回放已捕获的 graph,能省掉每个 token 数百次的 kernel 启动。
- S14188 行
投机解码
一次前向吐出不止一个 token
廉价的草稿模型先猜 k 个 token,大模型一次前向就能验证全部 k 个。拒绝采样保证输出分布可证明地完全一致。
- S15338 行
结构化输出
受语法约束的解码
合法 JSON 不是一个 prompt 问题。把 schema 编译成状态机,然后把语法不允许的 logit 全部屏蔽掉。
- S16152 行
专家混合(MoE)
稀疏计算,稠密显存
一个 MoE 层对每个 token 只激活 256 个专家中的 8 个。算力降了,显存没降,路由则变成了负载均衡问题。
……于是单机能给的都榨干了。所以:
分布式服务
S17–S20把模型切分到多张 GPU,把 prefill 与 decode 拆到不同机器,再在前面加一层 API。
- S17216 行
张量并行与流水线并行
一个模型,多张 GPU
先按列切、再按行切每个 matmul,每个子层只需要一次 all-reduce。流水线并行则是用延迟换容量。
- S18186 行
Prefill/Decode 分离部署
两种负载,两组机器
prefill 受算力约束,decode 受带宽约束。挤在同一张卡上,两个 SLO 都达不到;那就把它们拆开,再把 KV cache 传过去。
- S19363 行
服务层
OpenAI 兼容的流式 API
用户看到的是 TTFT 和 token 间延迟,不是 FLOPs。API 层负责流式推送 SSE、处理取消,并导出你真正拿来调优的指标。
- S20421 行
完整的引擎
把所有零件接起来
把前十九章的零件装成一个引擎:调度器、分页缓存、前缀复用、投机解码、流式服务器,最后跑一遍基准测试。
收官之作
S21–S22拿一个真实的 2944 行 C 引擎,逐模块用 Python 重写一遍。全课程只有这一章的参考实现,是别人正在用的生产代码。
每一章都有
一张机制图
沿着真实的数据流,观察计算、内存传输与延迟分别在何处产生。
一个交互式模拟器
把输入推到边界情况出现为止:耗尽 KV 池,挪动采样阈值,然后看系统怎么应对。
一组带解析的习题
每章六道判分题,每道题都配有解析,并指向需要复习的核心概念。
一个能跑的文件
自包含,只依赖 NumPy;TypeScript 收官章除外,它用 Node 运行。文件会断言自己的结论,两秒内跑完。不用下载模型,不用 PyTorch,也不需要 GPU。
从生成循环开始。
第 1 章是九行 Python 和一次令人不适的测量。后面的全部内容,都从这次测量推出来。