如果你做过一段时间的 LLM 应用开发,大概率会碰到这样一种情况:模型跑得好好的,单次推理延迟也可接受,但流量稍微一上来,线上服务就像被什么东西卡住一样,排队延迟飙升,部分请求直接超时,GPU 利用率却不一定满载。这个时候你会发现,LLM serving 真正棘手的地方,可能不是模型本身,而是请求的突发性。
“Burstiness is all you need for LLM serving”这句话初看很像经典论文标题的戏仿,但它点出了一个容易被低估的事实:在大模型服务里,短时间内的请求集中到达,才是决定系统能不能稳住的胜负手。模型推理速度再快,如果服务架构不能吸收突发流量,用户体验一样会崩塌。
这篇文章想结合生产环境里常见的几个真实问题,聊聊为什么突发性对 LLM serving 如此关键,以及围绕突发性做服务设计时,哪些机制比调一个推理参数更值得投入。
1. 先搞清楚 LLM 服务里的“突发性”到底指什么
很多人谈到突发性,第一反应是“瞬时 QPS 很高”。这不算错,但太粗糙了。LLM 服务的特殊性在于,请求不是一个固定大小的网络包,而是一个长度未知、输出长度更未知的生成过程。
1.1 突发性的三个常见来源
第一个来源是用户行为。高峰时段、活动投放、某个功能突然被大量调用,都会让请求在几秒内集中到达。这个在普通 Web 服务里也常见,但 LLM 服务的响应时间比普通接口长得多,所以同样的突发请求数会带来更长时间的排队积压。
第二个来源是请求本身的“内部波动”。同样是调用同一个模型,有的 prompt 只有几十个 token,有的 prompt 长达几千 token;有的请求只需要生成几十个字,有的要生成几百上千字。即便每秒请求数没有变化,系统需要处理的 token 吞吐量也可能瞬间翻几倍。这种波动不体现在 RPS 上,但会直接压垮 KV cache 和调度器。
第三个来源是多业务共享集群。多个应用、多个租户共用一套推理服务时,单个业务的一次突发可能会把全局显存、算力都拖进来。这也是为什么很多团队一开始用单实例服务时感觉挺好,一旦接入多个上游调用方,问题就层出不穷。
1.2 为什么它比普通 Web 流量更难处理
普通 Web 服务接收一个请求,处理时间通常是毫秒级,请求之间相对独立。LLM 服务不是这样,一个请求在 prefill 阶段需要大量并行计算来编码 prompt,在 decode 阶段又变成逐步生成;不同阶段的资源需求差异很大。
如果同一时刻到达的请求有多个,系统就要在显存里为每个请求准备 KV cache。突发流量意味着 KV cache 的占用会快速上涨,可能瞬间触达显存上限。显存满了以后,要么拒绝新请求,要么把旧的缓存换出去,而换出缓存又会产生额外的调度开销。这就把问题从“显存不够”逐步放大成了“排队延迟”和“服务不可用”。
1.3 量化突发性时该看哪些指标
不要只盯着平均 QPS。更务实的做法是同时观察几个维度:
- 每秒到达的请求数和峰值/平均值之比。
- 正在并发处理的请求数,以及最大排队长度。
- 每秒钟实际处理的 prompt token 和 generation token 总量。
- 请求在排队中等待的时间,以及完成时间的中位数和 P99。
- KV cache 的剩余空间和驱逐次数。
这些指标综合起来,才能判断一个系统是不是真的能处理突发,而不是在“低水位下看起来很稳定”。
2. 突发性如何一步步传导到系统瓶颈
理解了突发性的来源,再来看它具体怎么影响一个 LLM 服务。这个过程不是单一环节的问题,而是从请求进入网关开始,一路传导到 GPU 计算、显存分配,最后反映在用户的体感上。
2.1 请求从进入到排队的“挤兑”过程
当突发请求同时到达,网关或推理引擎首先要把请求放入排队队列。这里的核心矛盾是:允许排队的请求太多,后面的请求可能要等很久;允许排队的请求太少,又会直接丢弃请求,导致服务可用性下降。
在普通服务里,处理能力是固定的,排队通常只是时间问题。在 LLM 服务里,排队还会占用后续的资源估算。如果调度器在排队阶段就为每个请求预留 KV cache,那么排队的请求越多,真正能运行的请求就越少。很多团队遇到过“队列里明明没多少请求,但新请求就是进不来”的情况,原因就在这里。
2.2 连续批处理和动态调度为什么比固定批处理更稳
传统 NLP 服务里,常见做法是固定 batch size 地批量处理。但 LLM 请求的长度差异太大,固定 batch 很容易造成算力浪费。于是出现了连续批处理(continuous batching)的思路:每当一个请求完成生成,就把它从当前 batch 里移出,同时把等待中的新请求补进来。
这意味着 systemized 不再是“一批一批地结束”,而是“不断有请求完成、不断有新请求进入”。连续批处理在一定程度上能吸收突发流量,但它也不是万能的。如果同一时刻进入的请求都特别长,显存和算力一样会被快速占满;如果调度器没有考虑请求长度,短请求可能会被长请求堵在后面,延迟分位数变得非常难看。
2.3 显存和 KV cache 成为最大约束
突发流量对显存的影响往往是连锁反应。假设原来每个请求平均占用 1GB KV cache,并发 10 个就是 10GB。如果突发使得并发在瞬间变成 30 个,显存立刻不够用。
这时系统会选择暂停新的请求、驱逐部分缓存,或者复算已经解码过的内容。驱逐和复算会额外增加延迟,极端情况下甚至会导致吞吐下跌。所以很多生产系统会限制最大并发数,而不是让 GPU 自行“发挥”。
2.4 为什么不能只看平均负载做容量规划
最常见的一个误判,是拿“平均请求量 × 平均生成长度”来预估需要的 GPU 数。真实流量几乎不可能均匀分布。某段时间内的 token 吞吐可能达到平均值的数倍,如果按平均值扩容,突发时系统必然过载;如果按峰值扩容,大部分时间又在浪费资源。
比较好的容量规划思路是:先定义自己可以接受的最差延迟,比如 P99 不超过 5 秒,然后通过压测找出系统在多大并发下仍能满足这个目标,最后再根据流量预测为峰值预留一定的余量。这比看平均利用率更符合 LLM 服务的特点。
3. 围绕突发性做服务设计,真正值得投入的是这几块
如果把 LLM serving 看成一场“应付突发流量”的工程,那么关键不在某一个小功能上,而是一整套配合机制。下面这些点,是我认为最值得优先做的。
3.1 请求入口:超时、排队上限、背压和降级
不要等到请求进入 GPU 之后才做约束。在入口处就应该把行为定义清楚:
- 给每个请求设置合理的最大等待时间,超过就直接返回超时或降级结果。
- 设置排队队列的最大长度,队列满了以后直接拒绝新请求,而不是无限堆积。
- 如果上游服务依赖 LLM 的结果,考虑提供降级响应,比如返回一个缓存结果或简化版本。
这些机制看起来简单,但能避免“突发流量把整个服务拖死”的最坏场景。你可以在入口做限流,但限流不是简单的计数,还要考虑请求长度和输出长度的预估权重。
3.2 批处理策略:优先做连续批处理,但要注意长请求隔离
前面提到了连续批处理,这里补充一点:它可以显著提高资源利用率,但最好搭配“最长处理时间限制”和“长度感知调度”。如果一个请求的预估输出特别长,可以把它放到独立的低优先级队列,避免一直占据批量窗口。
在常见推理框架里,很多参数都是可以调节的,比如最大 batch token 数、最大并发序列数等。你需要根据实际业务请求的长度分布来调,而不是用默认值直接上生产。
3.3 自动缩放:不要只看 GPU 利用率,要看排队长度
Kubernetes 场景下,很多人喜欢按 CPU 或 GPU 利用率来设置 HPA。但 LLM 服务里,GPU 利用率高不一定代表吞吐好,有可能是在处理大量无谓的缓存驱逐;GPU 利用率低也不一定代表没有突发,可能只是部分请求在等待显存。
更好的弹性伸缩信号是“排队请求的等待时间”和“队列长度”。当等待时间超过阈值时,及时扩容;当队列清空并稳定一段时间后,再缩容。这样既能在突发时快速反应,也能避免频繁扩缩容。
3.4 缓存和复用:把突发里的重复计算消掉
突发流量里往往有大量相似请求。同一批用户可能在相同时间问类似问题,多轮对话里有重复的系统提示词,或者多个请求共享同一个较长的 prompt 前缀。这些场景都适合做“前缀缓存”。
如果你的请求有固定的系统提示或较长的 few-shot 示例,建议优先开启前缀缓存。这样即使请求整体是新的,但 prefill 阶段的大部分计算可以直接复用,能把突发流量对算力的冲击降低不少。更激进一点,还可以对常见问题做语义缓存,但要注意语义一致性和结果时效性。
3.5 多级队列和租户隔离
如果多个业务共用一套服务,一定要做资源隔离。比较现实的做法是给每个租户设置独立的最大并发、排队长度和 token 吞吐上限。这样即使某个业务出现突发,也只会影响它自己的“水位”,不会把其他业务的请求全部堵住。
租户隔离的粒度可以是上游应用、项目、企业客户等。工程上需要一套配额系统,而配额的核心不是固定数值,而是“每个租户即使在突发时也不能超过全局资源的安全水位”。
4. 实操建议:从单实例压测到突发场景下的参数调优
理论听再多,不如压测一轮来得直观。下面这套流程,是我自己验证过、推进项目落地时用到的思路。注意,具体命令和参数请结合你自己的框架版本调整,这里更重要的是“顺序”和“判断逻辑”。
4.1 先构造一个能触发突发的压测脚本
不要只做均匀的“每秒固定请求数”压测。要逼出真实问题,需要用两个阶段模拟突发:
- 第一阶段:用较低的请求速率跑 2 到 3 分钟,把系统预热到稳定状态。
- 第二阶段:在 10 到 20 秒内,把请求速率提升到平时峰值的 2 到 3 倍。
- 第三阶段:把速率降到正常水平,观察系统是否能在短时间内恢复。
请求本身的 prompt 长度和输出长度也要有一定分布。不要全是短请求,最好混合 20% 的长 prompt 和 20% 的长输出。这样才能覆盖真实工作的资源波动。
4.2 参数调整优先级
突发压测时,建议按下面的优先级调整,不要一上来就改模型参数:
- 请求超时时间。先确认超时时间不是导致异常失败的触发点。
- 最大排队长度。把它设得保守一点,优先保证已受理请求能完成。
- 最大并发序列数。这是保护显存和 KV cache 的关键。
- 最大 batch token 数。这个参数决定 GPU 能否把算力用满。
- 是否开启前缀缓存。如果观察到请求有重复前缀,优先开。
参数之间是联动的。比如你把最大并发调高,但 batch token 上限很低,可能实际吞吐并不高;你把排队长度调大,但并发不增加,请求只会越积越多。
4.3 一条高效的排查链路
如果压测中出现了“QPS 不高但延迟很高”的情况,请按这个顺序排查:
- 看请求是否在排队。如果队列等待时间远大于推理时间,说明入口放行太快,或者并发上限太低。
- 看 KV cache 是否被频繁驱逐。如果显存利用率很高且有大量驱逐日志,说明并发设置过高,或缓存复用机制没生效。
- 看 GPU 算力有没有打满。如果利用率中等,但 token 吞吐停滞,可能是 batch 里的请求长度分布不合理,或者调度器经常被打断。
- 看服务端日志里有没有请求被抢断。很多推理框架会因为超时或资源不足主动取消长请求,这会引发“看起来处理了很多请求,但实际完成率很低”的问题。
排查时,不要同时改很多参数。一次只改一个变量,观察对应指标变化。
4.4 不同部署形态下的突发应对差异
单机多卡、K8s 集群、Serverless 是三种常见形态,应对侧重点完全不同:
| 部署形态 | 突发吸收方式 | 主要瓶颈 | 关键策略 |
|---|---|---|---|
| 单机多卡 | 单机内调度 | 显存和算力交织 | 限制并发、开前缀缓存、做排队控制 |
| K8s 集群 | 水平扩展 | 扩容速度和流量调度 | 按排队长度做 HPA、多副本隔离 |
| Serverless | 自动扩缩容 | 冷启动和实例上限 | 缩短冷启动、配置最大并发、为长请求预留超时 |
没有一种部署能同时解决所有突发问题。单机可以做到低延迟,但弹性差;K8s 能扩容,但扩容速度可能跟不上突发;Serverless 扩容快,但对长请求和成本又不太友好。你需要根据业务属性选择,再针对短板做补偿。
5. 应对突发性的三段式框架:先削峰、再控制、最后工程化
前面讲的都是具体机制。如果把它提炼成一套可复用的方法论,我习惯用“削峰、控制、工程化”这三个词来总结。
5.1 削峰:让到达系统的请求更平滑
削峰不是要拒绝所有突发请求,而是通过限流、排队、缓存、降级等手段,把短时间的尖峰压平。比如在入口设置令牌桶,按权重估算每个请求的“成本”;比如对容易重复的请求做缓存;比如让上游调用方在时间上稍微错峰。
这里的关键是,削峰不能只依赖一个限流器,还要和业务方约定好异常时的行为。否则你把请求限掉了,业务方拿不到结果就会重试,重试又变成新的突发,形成恶性循环。
5.2 控制:让每个请求在可控范围里运行
削峰之后,系统依然会承受一定程度的突发。这时需要让每一个进入系统的请求都处于可控范围。用大白话说,就是“这个请求最多占多少资源、最多等多久、最多生成多少个 token”。
具体到实现上,至少要做以下几件事:
- 给单请求设置最大生成 token 数和最长处理时间。
- 在调度器里限制最大并发序列数。
- 在显存层面对 KV cache 进行上限管理。
- 对不同类型的请求设置优先级,保证重要请求不被长尾请求拖住。
控制层做得好,突发流量只会让部分请求变慢,而不是让整个服务崩掉。
5.3 工程化:把应对突发变成系统能力
削峰和控制都是被动应对。真正长期有效的,是把突发监测、自动伸缩、容量预测和复盘机制固化下来。比如:
- 记录每次突发的时间、请求量、token 吞吐、排队指标和服务表现。
- 把突发的特征和业务事件关联起来,比如定时任务、运营活动、上游接口调用链。
- 基于历史数据做容量预估,并在突发前主动扩容。
这些能力不需要一次性做完,可以从“每次突发后能拿到一份完整复盘报告”开始,逐步完善。
5.4 回到标题:突发性为什么是 LLM serving 的首要问题
你可以优化算子、量化模型、升级推理框架,这些都能提升单请求性能。但只要服务要面对真实用户,突发性就会一直在那里。它像一个水位,决定了系统有没有冗余空间。
如果只能记住一句话,我会说:LLM serving 的设计重点,不是让单次请求变快,而是让突发流量出现时,整个系统依然能保持可用、可控、可观测。
这也是“Burstiness is all you need for LLM serving”最直接的意思。它不是否定模型优化,而是在提醒我们,真正决定线上体验的,往往不是模型跑得有多快,而是系统如何在一瞬间涌入大量请求时,仍然稳定地完成每一次生成。