从 SGLang 看现代 LLM 推理引擎:调度、缓存与延迟权衡
对外看,LLM 推理引擎只是把 OpenAI 风格的请求转换成一串 Token;对内看,它更像一个专门为自回归模型设计的操作系统:在不断变化的请求之间分配 GPU 计算、显存和执行时间,同时维护每个请求持续增长的状态。
真正困难的并不是完成一次模型 Forward,而是持续回答三个问题:
- 下一轮 GPU 应该处理哪些请求?
- 有限显存应该保留哪些 KV Cache?
- 怎样在首 Token 延迟、生成流畅度和系统吞吐之间取舍?
SGLang、vLLM 等现代推理引擎的大部分优化,都可以放回这三个问题中理解。
推理引擎面对的是一组彼此冲突的工作负载
一个生成请求在执行层面分为两个阶段:
1 | Prompt ── Prefill ──> First Token ── Decode ──> Remaining Tokens |
Prefill 一次处理整段输入,矩阵规模较大,通常更容易利用 GPU 的计算能力;Decode 每轮只为每个请求生成少量新 Token,却需要反复读取模型权重和不断增长的 KV Cache,通常更受显存带宽限制。
两者共享同一块 GPU,却有不同的资源特征:
| 阶段 | 主要工作 | 常见瓶颈 | 直接影响 |
|---|---|---|---|
| Prefill | 处理输入并建立 KV Cache | 计算量、长 Prompt | TTFT |
| Decode | 逐步生成并扩展 KV Cache | 显存带宽、批处理效率 | TPOT / ITL |
这也是推理调度比普通 Web 请求调度更棘手的地方。新到达的长 Prompt 可以高效占满 GPU,却可能阻塞正在流式输出的请求;优先照顾 Decode 可以让输出更平滑,却会延长新请求的等待时间。
Scheduler 是 Runtime 的控制面
现代推理引擎通常不会固定一个 Batch 并等待其中所有请求结束。它们采用 Continuous Batching:每轮 Forward 之后,已完成的请求退出,新请求或等待中的请求进入,Batch 始终处于变化之中。
1 | Step 1: A(decode) B(decode) C(prefill) |
因此,Scheduler 调度的不是一个静态请求列表,而是每个执行步中的工作量。它需要同时考虑:
- 哪些请求已经可以继续 Decode;
- 哪些新请求可以进入 Prefill;
- 本轮计算容量还能容纳多少 Token;
- KV Cache 是否还有足够的 Block;
- 是否需要抢占、延后或重新计算某些请求。
Continuous Batching 提高了 GPU 利用率,但也把性能问题变成了策略问题:相同的模型和硬件,仅仅改变请求进入顺序、Prefill 大小或调度优先级,就可能得到完全不同的尾延迟。
Chunked Prefill:把长任务变成可抢占的工作
如果一个很长的 Prefill 独占某次 Forward,正在 Decode 的请求就只能等待,用户看到的结果通常是流式输出突然停顿。Chunked Prefill 将长 Prompt 拆成多个片段,并把它们与 Decode 工作穿插执行:
1 | 未切分: Prefill ────────────────────> Decode 恢复 |
它不是单纯的“加速开关”,而是在重新分配延迟:
- Chunk 较小,Decode 被阻塞的时间更短,ITL 通常更稳定;
- Chunk 过小,会增加调度与 Kernel 启动开销,整体吞吐可能下降;
- Decode 优先级过高,Prefill 排队时间会增加,TTFT 可能恶化。
在单个引擎内,Chunked Prefill 缓解了两类任务的互相干扰;在更大规模的部署中,还可以进一步将 Prefill 与 Decode 放到不同的 Worker 上,即 PD Disaggregation。两者解决的是同一矛盾在不同规模下的表现。
KV Cache 才是并发能力的硬约束
自回归生成之所以不必在每一步重算全部上下文,是因为 Attention 的 Key 和 Value 被保存在 KV Cache 中。它的大小会随上下文长度和并发请求数线性增长。
对于常见 Transformer,单个请求的 KV Cache 可以粗略估算为:
1 | 2 × 层数 × Token 数 × KV Head 数 × Head Dim × 每元素字节数 |
其中的 2 对应 Key 和 Value。实际单卡占用还会受到 Tensor Parallel、KV Cache 精度和模型结构影响,但这个近似已经揭示了关键事实:模型权重能够装进显存,不代表服务能够承受目标并发。
推理时的显存大致被以下几部分共同占用:
1 | Model Weights + KV Cache + Activations + Runtime Buffers |
权重加载后基本固定,KV Cache 却会随流量持续分配和回收。于是 Runtime 不仅要“算 Token”,还要像内存管理器一样处理动态增长、碎片、共享与淘汰。
PagedAttention 与 RadixAttention 解决的不是同一个问题
理解 vLLM 和 SGLang 时,一个常见误区是把 PagedAttention 与 RadixAttention 当成两种互相竞争的 KV Cache 方案。更准确的划分是:
- PagedAttention 主要回答 KV Cache 如何分配和寻址;
- RadixAttention 主要回答不同请求的公共前缀如何组织、匹配和复用。
分页:让物理显存不必连续
如果每个请求都需要一段连续显存,KV Cache 随请求增长和结束会产生碎片,也难以提前准确预留空间。PagedAttention 把 KV Cache 切成固定大小的 Block,让逻辑连续的 Token 可以映射到不连续的物理显存:
1 | Request A: [block 1] -> [block 4] -> [block 7] |
这套设计将 KV Cache 的分配粒度从“整个请求”缩小到 Block,使扩容、回收和共享都更灵活。它是 vLLM 最具代表性的设计,但分页式 KV 管理如今也已经出现在包括 SGLang 在内的其他推理引擎中。
前缀树:让相同上下文不必重复计算
考虑一组 Agent 请求:
1 | System Prompt + Tool Definitions + Repo Context + Question A |
如果前面的大段 Token 完全相同,对它们重复 Prefill 就是在重复计算。SGLang 的 RadixAttention 使用 Radix Tree 组织已生成的 KV Cache,让新请求匹配最长公共前缀,只计算尚未命中的部分:
1 | System Prompt |
这对多轮对话、Few-shot、RAG 和 Agent 工作负载尤其有效,因为这些请求往往共享稳定且较长的前缀。
不过,“支持 Prefix Cache”也已经不是 SGLang 与 vLLM 的功能分界线:vLLM 同样支持 Automatic Prefix Caching,SGLang 也支持 Paged Attention。两者更值得比较的,是缓存索引、淘汰策略和调度器如何协同,而不是简单检查功能列表。
Prefix Cache 会改变调度目标
缓存命中并不只取决于请求内容,也取决于请求顺序。
假设等待队列里有两个请求:一个先到达但没有缓存命中,另一个稍晚到达却能复用很长的前缀。严格 FCFS 更公平;优先后者则能减少 Prefill 计算、提高吞吐,还可能更快释放队列压力。
这正是 Prefix-aware Scheduling 的价值:Scheduler 不再只看请求到达时间和长度,还把缓存局部性纳入决策。SGLang 的 Radix Cache 采用缓存感知的调度与淘汰机制,使“保存了什么”和“接下来算什么”成为一个联合问题。
代价也同样存在:
- 为未来复用而保留 KV Cache,会减少当前请求可用的显存;
- 过度偏向高命中请求,可能导致其他请求饥饿;
- 缓存容量不足时,频繁淘汰和重算会抵消命中收益。
所以 Prefix Cache Hit Rate 不能脱离吞吐和尾延迟单独评价。更高的命中率是手段,而不是最终目标。
Speculative Decoding:减少 Decode 的串行步数
调度和缓存主要改善“如何组织工作”,Speculative Decoding 则直接攻击自回归生成的串行依赖。
它先用 Draft Model、Medusa/EAGLE 一类 Draft Head,或其他廉价方法提出多个候选 Token,再由目标模型一次验证。连续通过的候选可以一并接受,从而用一次目标模型执行推进多个 Token。
1 | Draft: A B C D |
但减少目标模型执行次数不等于系统吞吐一定提高。收益取决于候选接受率、Draft 成本、目标模型大小、并发度以及验证开销。低并发时,Decode 没有充分利用 GPU 计算资源,额外的 Draft 工作可能很划算;高并发时,Continuous Batching 本身已经在利用这些空余能力,Speculative Decoding 反而可能与正常请求争夺资源。
因此任何“加速 N 倍”的结果,都至少需要说明模型、硬件、并发度、输入输出长度、Draft 方法、接受率,以及测量的是单请求延迟还是系统吞吐。
从指标反推 Runtime 内部发生了什么
只看 GPU Utilization 很难判断推理服务是否健康。更有用的做法,是把用户侧延迟与 Runtime 内部状态连起来:
| 现象 | 应同时观察 | 可能的机制 |
|---|---|---|
| TTFT 与 Waiting Requests 一起上升 | 排队时间、Running/Waiting、Prefill 吞吐 | 到达率超过处理能力或调度拥塞 |
| TTFT 上升且 Prefix 命中下降 | Cache Hit Rate、被缓存 Token 数 | 公共前缀变化或缓存被淘汰,Prefill 重算增加 |
| 流式输出偶发停顿 | P99 ITL、Prefill 长度、Chunk 大小 | 长 Prefill 干扰 Decode |
| TPOT 持续变差 | Decode Batch、KV Cache 使用率、显存带宽 | Decode 负载增加或内存系统接近饱和 |
| 吞吐提高但尾延迟恶化 | P50/P99 TTFT、ITL、并发度 | 更激进的批处理用等待时间换取利用率 |
TTFT 不只等于 Prefill 时间,它还包含排队、调度和网络开销;TPOT 是请求级平均值,可能掩盖某几个 Token 的长时间停顿,因此流式服务还应该关注 ITL 分布,尤其是 P99。
应该怎样比较 vLLM 与 SGLang
今天再用“vLLM 有 PagedAttention、SGLang 有 RadixAttention”来做二选一,已经过于简单。两个项目都在快速演进,也在吸收相似的设计。更可靠的比较方式是固定模型、精度、硬件和并行策略,然后使用接近生产的流量进行端到端测试:
- 输入长度与输出长度的真实分布;
- 请求到达速率,而不只是固定并发;
- 公共前缀的长度、比例和重复模式;
- 冷缓存与热缓存分别测试;
- 在 TTFT、TPOT 或 ITL 约束下能够达到的最大吞吐;
- P50 之外的 P95/P99 延迟和稳定性。
对于 Prefix-heavy 的 Agent 或多轮对话流量,缓存组织和缓存感知调度会非常关键;对于共享前缀很少的普通生成流量,模型支持、Kernel 效率、量化、并行策略和运维成熟度可能更重要。
Runtime 的选择最终不是功能表打勾,而是工作负载与实现策略的匹配。
总结
现代 LLM 推理引擎可以归结为三个相互耦合的系统问题:
- 执行调度:通过 Continuous Batching 和 Chunked Prefill 安排每轮 GPU 工作;
- 状态管理:通过分页、共享和淘汰管理持续增长的 KV Cache;
- 性能权衡:在 TTFT、ITL、TPOT、吞吐和显存容量之间寻找平衡。
PagedAttention 改善 KV Cache 的物理管理,RadixAttention 提升公共前缀的复用效率,Speculative Decoding 减少串行生成步数。它们切入的层次不同,但最终都服务于同一个目标:让有限的 GPU 资源完成更多有效工作,同时把用户可感知的等待控制在可接受范围内。
理解一个 Runtime,不妨从四个问题开始:它如何组成 Batch,如何处理长 Prefill,如何管理与复用 KV Cache,以及性能数字是在什么流量模型下测出来的。答案通常比功能列表更接近系统的真实能力。
参考资料
- SGLang: Efficient Execution of Structured Language Model Programs
- Fast and Expressive LLM Inference with RadixAttention and SGLang
- SGLang:Prefill-Decode Disaggregation
- Efficient Memory Management for Large Language Model Serving with PagedAttention
- vLLM:Automatic Prefix Caching
- vLLM:Optimization and Tuning
- vLLM:Benchmark CLI and Latency Metrics





