AI AI infra · 推理侧
第 1 章
AI infra · 推理侧

第 1 章 推理是什么

推理是模型生命周期里的第二个阶段。训练从数据里学出权重,推理把权重变成服务:用户发来一句话,模型回一段话,中间的每一步都要在可接受的时间内完成。这个定义平淡,但它把三类完全不同的问题捆在了一起——单机上怎么把一次前向算快,集群上怎么承接不断变化的流量,工程师用什么抽象去控制这一切。任何一层单独做好,都不足以让一个推理服务成立。

三层:运行时、基础设施、工具

Inference Engineering 把推理拆成三层,这个拆法值得先记住,因为后面所有章节都能落到其中一层。

运行时(runtime)解决的是"一个模型在一块 GPU 上能不能跑得足够快"。它下面是 CUDA、PyTorch、推理引擎(vLLM、SGLang、TensorRT-LLM)和 FlashAttention 这类算子;上面是批处理、缓存复用、量化、投机解码、并行、预填充与解码拆解这些手段。这一层的变化最快,也最容易被误当成全部——很多关于推理的讨论实际上只在运行时里打转,而一个服务倒下时,原因常常在另外两层。

基础设施(infrastructure)解决的是"一台机器不够了怎么办"。单实例的吞吐总有上限,流量超过上限之后,需要的是自动扩缩、容量管理,以及在几百张卡之后把负载分散到多个区域和多个云。这一层关心的指标不是单次请求多快,而是可用性、扩缩速度和资源利用率。它和运行时是两种问题:把 kernel 优化到极限,也不会让一个过载的服务多出一台机器。

工具(tooling)解决的是"人怎么控制前面两层"。光谱的一端是全托管——把权重交给平台,拿回一个 API;另一端是裸原语——只给算力、网络和磁盘。站在中间的位置才是工程上常用的:既能对关键路径有足够的控制,又不至于每换一个模型都重写一遍部署。这一层的好坏体现在改一个参数需要多久,而不是 benchmark 上多几个点。

三层之间不是包含关系,而是互相设限。运行时的优化会被基础设施的扩缩粒度抵消,基础设施的容量又会被工具层的抽象挡住——一个只能整体发布的平台,扩缩再快也用不上。

一次请求经过什么

把镜头拉近到单次请求,可以看到一条固定的路径。

请求到达后先进入队列。排队时间不属于模型计算,却完整地计入用户等待,这一点在后面讨论指标时还会反复出现。轮到它时,文本被切成 token;切法由 tokenizer 决定,同一段中文在不同 tokenizer 下可能相差两三成,而后面所有的计算量和显存占用都按 token 数计。

接下来是预填充(prefill)。整段输入一次性送入模型,每一层都要算注意力、写下一份 Key 和 Value 缓存,最后取最后一个位置的输出,得到第一个 token 的概率分布。这一段的计算量随输入长度增长,矩阵规模大,GPU 的算力容易被吃满。

从第二个 token 开始进入逐字生成(decode)。每一步只处理一个新 token,但为了算出它,模型要回看此前所有 token 的表示,因此每一步都要把全部权重和整份缓存从显存搬进计算单元。计算量很小,搬运量很大,GPU 的算力利用率反而上不去。

生成到结束符为止,token 逐步变回文本并以流式返回。用户感知到的两个量分别由两段决定:等多久看见第一个字,由排队和预填充决定;之后出字顺不顺,由逐字生成的每一步决定。

这条路径上有两个容易被忽略的事实。第一,预填充和逐字生成对硬件的要求不同,一个吃算力,一个吃显存带宽,所以"换一张更快的卡"对两段的收益并不一样。第二,两者共享同一份权重,却各自承担不同的固定开销,这决定了很多优化只能改善其中一段。

训练与推理的分歧

训练和推理用的是同一套矩阵运算、同一批 GPU,但对系统的要求几乎相反。MLSysBook 把这件事称为 serving inversion:训练时被验证有效的优化直觉,到了服务端往往要反过来。

训练面向的是一批已经排好序的样本。只要在可接受的时间内把整批处理完,中间某一步慢一点并不影响最终结果,因此系统的目标是把吞吐做满:批越大、卡越多、有效算力占比越高,摊到每个样本上的成本就越低。训练愿意为此付出很长的启动时间、很大的显存占用,甚至容忍偶发的重算。

推理没有这个余地。每个请求都带着自己的截止时间到达,用户在看见第一个字之前不关心机器的整体产能,而一旦等待超过阈值,这次计算就失去了意义。同一张卡在训练里被要求别闲着,在推理里被要求先回答这一个。这两种要求并不总能同时满足,架构上的许多取舍——批的大小、缓存的预留、是否牺牲单条延迟去换吞吐——都是这条分歧的下游结果。

分歧的规模可以用一个数字感受。MLSysBook 给过一个换算:一天处理十亿次请求的服务,每次请求省下一毫秒,一天就能省出十一天以上的机器时间。反过来看,每次请求多花一毫秒,代价同样是这个量级。这就是为什么推理侧的优化常常围绕毫秒展开,而训练侧更愿意谈百分之几的吞吐提升。

时间的账:三个因子

既然一切都要落到时间上,就需要一个能拆开时间的写法。MLSysBook 提出的 Iron Law 把任何一个机器学习任务的执行时间写成三个因子的乘积:处理多少样本、每个样本需要多少次浮点运算、每次运算实际花多少秒。

写成一句话是:总时间 = 样本数 × 每样本运算量 × 单位运算耗时。前两个因子相乘得到总运算量,第三个因子是机器把一次运算变成结果的实际速度。这个式子对训练和推理都成立,区别只在三个因子各取什么值。

训练时,"任务"是一次训练运行,样本数是整个语料的 token 数,可能到万亿量级;每样本运算量包含前向和反向,稠密 Transformer 大约是每个 token 六倍参数量。推理时,"任务"是一次请求,样本数是这次请求要生成的 token 数,可能只有几百;每样本运算量只有前向,大约是每个 token 两倍参数量。

这个差别解释了为什么推理看起来"轻",实际却难做。训练把成千上万个 token 拼成一个大矩阵,同一份权重读一次就服务了整批,单位运算的耗时可以压到接近硬件峰值;推理每一步只处理一个 token,权重必须完整读一遍,单位运算的耗时就取决于显存带宽,而不是算力。

机器那一项:搬运、计算、固定开销

第三个因子最值得拆。一台真实机器跑不到数据手册上的峰值,原因有三类:数据要在内存和计算单元之间搬,搬的速度由带宽决定;算术单元跑不满,实际利用率要打折;此外还有一批与计算量无关的固定开销,比如 kernel 启动、调度、通信握手。

把这三类写在一起,一次执行的时间约等于"搬运字节数除以带宽"加上"运算量除以峰值算力与实际利用率的乘积",再加上固定开销。哪一项最大,系统就先撞哪一堵墙。

这个拆法给出了一条判断规则:先算搬运量和运算量的比值,再和机器自身的算力带宽比对照。前者小于后者,说明搬运是瓶颈,加算力没有用;前者大于后者,说明计算是瓶颈,加带宽没有用。

以一张 A100 为例,它的 FP16 稠密算力约 312 TFLOPS,显存带宽约 2039 GB/s,两者相除约为 153,也就是每从显存读一个字节,机器有能力做 153 次浮点运算。这个数就是这台机器的平衡点。

两段不同的账

把平衡点代进一次请求,两段的结果完全不同。

逐字生成一个 token,运算量约为两倍参数量,7B 模型是 14 GFLOP;读取量约为全部权重,按 FP16 算是 14 GB。两者相除,算术强度约为每字节一次浮点运算,远低于 153。这意味着逐字生成无论如何都卡在带宽上:14 GB 除以 2039 GB/s,一个字的时间下限约为 6.9 毫秒,折合单条请求每秒不到一百五十个字,而且这还是没有计算注意力、没有读缓存的理想值。

预填充一次处理 8K 输入,运算量约为 2×7×10⁹×8192,即 114 TFLOP;读取量仍然是 14 GB 权重。算术强度约为每字节八千次浮点运算,远高于 153,于是这一段卡在算力上:114 TFLOP 除以 312 TFLOPS,理想情况下约 0.37 秒。

同一张卡、同一个模型,两段代码,两个瓶颈,而且相差两个数量级。这就是为什么推理优化不能笼统地谈"更快":任何一项手段,都要先说清它改善的是搬运还是计算。

批处理:同一个式子里的另一种解法

上一节算的是单条请求。把式子里的样本数从一改成批大小,结论会变。每生成一个 token,权重仍然只读一遍,但这一次读取服务了整批请求;缓存则各读各的,随批大小线性增长。于是每一步的搬运量从"权重加一份缓存"变成"权重加若干份缓存",总吞吐按接近批大小的比例上升,单条请求的出字间隔同步变长。

用第 5 章会算出的数字提前看一眼。7B、8K 的请求,一条缓存约 4 GiB,权重 13 GiB。批为一的时候每步读 17 GiB,约 8.3 毫秒,折合每秒 120 个 token;批为十四时每步读 69 GiB,约 34 毫秒,但一步出十四个 token,折合每秒 411 个 token。吞吐涨了三倍半,单条请求的出字间隔慢了四倍。

这说明第三个因子并不是固定的。批处理把权重读取这笔固定成本摊到了更多样本上,单位运算的实际耗时随之下降;机器没有变快,只是同一份搬运服务了更多人。代价写在同一个式子里:缓存读取随批增长,固定开销也随批增长,加到某个点之后,每步耗时的增长会追上吞吐的增长,收益就停了。

预算:延迟与成本

在动手优化之前,还需要两个约束,否则"更快"没有终点。

延迟预算是用户能接受的时间。它通常拆成两段:首字延迟和出字间隔。前者决定用户会不会觉得"卡住了",后者决定读起来顺不顺。这两个阈值由产品定,不由工程师定;工程师的工作是判断当前系统离阈值有多远,以及哪一段最接近阈值。

成本预算是每次请求能花多少钱。它最终会折算成每百万 token 的成本,而成本由三部分构成:GPU 小时数、单位时间的价格、以及这段时间里产出了多少有效 token。前两项由采购和硬件决定,第三项由利用率决定,也是工程师唯一能大幅改动的一项。

两个预算合起来约束了优化的方向。延迟预算紧,就要优先压缩首字延迟,可能牺牲吞吐;成本预算紧,就要优先提高批大小和利用率,可能牺牲单条延迟。绝大多数"这个方案好不好"的争论,本质上是双方对哪个预算更紧没有共识。

三种失败

预算的作用之一,是把"慢"拆成可以分别判断的三种情况。

第一种是迟到的正确答案。请求算完了,但超过了截止时间,用户已经离开。这类失败在指标上表现为分位数变差,平均值可能仍然好看,因为它只影响排在后面的那一小部分请求。

第二种是及时的错答案。服务准时返回,结果却不对——输入分布变了,或者训练与服务的预处理不一致。MLSysBook 把这类问题称为静默失败:硬件健康、延迟达标、日志干净,只有预测质量在下降,常规监控看不到。它最难发现,因为没有任何一个基础设施指标会报警。

第三种是没有答案。请求被丢弃、超时或者显存分配失败,用户直接看到错误。这类失败最容易被发现,也最容易被误判成"容量不够",而真实原因可能是调度策略、内存碎片,或者一条过长的上下文。

三种失败对应三套检查:第一种看分位数,第二种看质量回归,第三种看错误率和队列长度。把它们混在一起谈"服务不稳定",就失去了定位的线索。

这本书的地图

后面十一章都落在这张图上。

第 2 章到第 4 章打基础:token、注意力、精度这些概念,模型结构里影响推理的部分,以及硬件的算力、带宽、容量和互联。第 5 章和第 6 章进入运行时:请求如何被拼成批、如何排队、缓存如何分配和复用。第 7 章到第 10 章是四类手段:量化、投机解码、长上下文、并行与拆解。第 11 章回到预算,讲指标、压测和成本。第 12 章用一次完整的判断把这些串起来。

每一章都回答一个具体问题,而且答案会改变下一章的问题。第 2 章算出一张卡能并几条,第 5 章就会问为什么只用上了三成;第 6 章把缓存管理讲完,第 7 章就会问总量还是不够时能压什么。这个顺序不是按重要性排的,是按"上一章留下的缺口"排的。

什么情况下这套框架不成立

这套框架有三个前提,前提变了,结论也要跟着变。

第一个前提是权重需要被完整读取。前缀缓存命中率很高的场景下,大量请求共享同一段前缀,缓存只存一份,显存和带宽的账都会比估算乐观。这时更该关心的是命中率和驱逐策略,而不是单条请求的搬运量。

第二个前提是批大小为常数。实际服务里批是动态拼出来的,长度不一、到达不均,平均批大小和峰值批大小可能差好几倍。用平均批算出来的吞吐,往往会高估真实产能。

第三个前提是单卡。一旦模型跨卡,每生成一个 token 都要做一次跨卡同步,通信时间会加进每一步的固定开销里。这时算力带宽比不再决定一切,互联带宽和拓扑变成新的约束。第 10 章会专门处理这件事。

框架的价值不在于它总是对,而在于它把"哪里可能不对"标了出来。拿到一个新场景,先用它算一遍,再看三个前提里哪一个最先失效,通常就能找到真正的问题所在。