推理服务是怎么运作的
批处理、调度与显存管理。吞吐量和延迟为什么彼此拉扯,以及一个服务系统到底在做什么。
为一个请求跑一次模型很简单。同时服务成千上万个请求则是一个有自己的词汇表的工程问题,搞懂它就能解释你在外面观察到的大部分现象——延迟为什么忽高忽低、长 prompt 为什么慢、服务商为什么要给上下文设上限。
批处理是最根本的杠杆
生成一个 token 需要从显存里读出所有活跃的参数。这次读取主导了成本,而算术相比之下很便宜。
所以一次只处理一个请求,浪费掉了硬件的绝大部分算力。把 32 个请求放在一起处理,参数只读一次,服务 32 个请求的成本只比服务 1 个高出一点点。
因此批处理不是一种优化,而是推理服务的核心经济账。它也是投机解码能帮到单个用户、却帮不到繁忙服务器的原因——它利用的是已经被批处理占走了的空闲算力。
连续批处理
朴素的批处理要等凑满一批,把整批请求跑到结束,再开下一批。问题在于请求的完成时间各不相同,于是最长的那个还在跑的时候,空出来的位置就一直闲着。
连续批处理——有时也叫迭代级调度——改成按 token 步来调度。一个请求一完成,等待中的请求立刻顶上它的位置。这一批的组成一直在变。
这大幅提升了利用率,也是现代服务系统的标配。它也解释了为什么你的延迟取决于别人的流量:你的请求在每一次前向传播里,都和其他所有在场的请求共享同一批。
prefill 与 decode 相互竞争
这两个阶段的特性正好相反。
prefill 一次处理完整个 prompt——高度并行、计算密集,并且随 prompt 长度平方增长。
decode 每一步为每个请求生成一个 token——受显存带宽限制,单个请求很便宜。
把两者混在一批里,一个很长的 prefill 就会卡住这一批里其他所有请求的 decode,用户会看到回答中途明显停顿。服务系统的解决办法是把 prefill 切块,或者把 prefill 和 decode 放到分开的硬件池里跑。
你可能观察到过这样一个现象,背后的机制就是这个:别人提交了一个超长 prompt,你的流式输出就变慢了。
显存是那个硬约束
显存里占位置的有两样:模型权重,这个是固定的;以及 KV cache,它随着每个活跃请求的每一个 token 增长。
规模上去以后,通常是 cache 占大头,它决定了并发上限。并发请求越多,需要的 cache 越多;上下文越长,每个请求占的 cache 越多。
**分页注意力(paged attention)**是关键手段。它不再为每个请求按最大可能长度预留一整块连续显存,而是按需以固定大小的块分配 cache——和虚拟内存是一个思路。到不了最大长度的请求就不会白白浪费那份预留,于是在同样的硬件上能撑起高得多的并发。
**前缀共享(prefix sharing)**可以和它叠加:开头前缀相同的请求——同一个 system prompt、同一批检索到的文档——可以共用那些 cache 块。这就是 prompt 缓存折扣背后的机制,也是稳定内容应该放在 prompt 最前面的原因。
吞吐量与延迟的对立
这是核心张力,也是为什么不存在一个配置能被简单称为「最优」。
更大的批提高总的每秒 token 数,同时抬高每个请求的延迟,因为每个请求在一个更大的组里排队等自己那一轮。更小的批则相反。
服务商是朝着某个目标调的——交互式端点偏向延迟,批量端点偏向吞吐量。这就是批量 API便宜得多的原因:它被允许凑大批判,按效率而不是按响应速度来调度。
从外面能看到的现象
**延迟忽高忽低。**你的请求在和其他流量共享批次。
**长 prompt 的首 token 延迟很高。**prefill 是平方级的,而且必须在生成开始之前跑完。
**上下文上限低于架构本身的能力。**并发数乘以 cache 大小,在模型撞上自己的长度极限之前就先撞上了显存。
**速率限制是按 token 算的,不只是按请求数。**消耗那个受限资源的是 token,不是请求。
**更便宜的批量档位。**放宽延迟就允许高效调度。
实际影响
把稳定内容放最前面,这样前缀缓存才能生效。不花成本,也是最容易拿到的一个收益。
缩短 prompt 的好处比你预期的大,因为 prefill 是平方级的。
没有人在等结果的时候用批量端点。
预期延迟会波动,并为此做设计——超时、重试、流式输出,让用户看得到进展。
要点回顾
- 批处理是推理服务的经济账:参数整批只读一次。
- 连续批处理按 token 步调度,所以你的延迟取决于同时段的其他流量。
- prefill 和 decode 相互竞争;别人一个长 prompt 就能卡住你的流。
- KV cache 是那个硬性的显存约束;分页注意力和前缀共享能缓解它。
- 吞吐量和延迟此消彼长,这也是批量档位更便宜的原因。
下一篇: 模型融合与组合