prefill 吃算力,decode 吃带宽。长 prompt 一旦混进 decode 批,尾延迟就被拉出尖峰。 chunked prefill 把 prompt 切片塞进 decode 的空档;PD 分离 则把两个阶段放到不同 GPU 池。 前者省掉 KV 传输,后者省掉相互干扰,代价各自不同。
两个阶段的资源曲线不同:prefill 一次并行处理整段 prompt,是 compute-bound;decode 每步每请求只处理 1 个 token,却要读权重与全部 KV,是 memory-bound。 把它们塞进同一个 batch,长 prefill 会把 decode 的迭代时间顶长 Figure 7,尾延迟出现尖峰 Figure 1。
chunked prefill 的关键不是「切片」本身,而是 token budget:每步先装 decode,再用剩余预算切 prefill。Sarathi-Serve 用预先选定的 token budget 做到 stall-free;vLLM V1 则是所有请求共享 max_num_batched_tokens,先跑 running、再跑 waiting,另有 long_prefill_token_threshold 等实现细节,两者不能互相冒充。
PD 分离用 KV 传输换掉相互干扰:prefill 池与 decode 池可以各自选 TP/PP,代价是 KV 必须过网络。DistServe 的算例是 512-token、OPT-66B 请求约 1.13 GB KV;10 req/s 就需要 11.3 GB/s,即 90 Gbps DistServe §3.3。 vLLM 文档明确写着:disaggregated prefill 不提升吞吐 docs/features/disagg_prefill.md:16,它是用来分别调 TTFT 与 ITL 的 docs/features/disagg_prefill.md:12。
读完这节你应该知道:为什么 prefill 与 decode 混批会互相伤害,以及为什么单纯加大 batch 不解决。
矩阵乘的 token 维度大,算术强度高,GPU 计算单元接近饱和。Sarathi-Serve 的测量显示,prefill 吞吐在序列长度约 512 token 时就已接近饱和 §4.1;继续加长 prompt 主要增加计算时间,而不是提高效率。
每个请求每步只处理 1 个 token,但要读一遍模型权重和全部 KV。batch 增大能提高 decode 的 GPU 利用率 Figure 3;但单条请求的算术强度很低,属于 memory-bound。
这两条曲线放在一张 GPU 上就会冲突。Sarathi-Serve 的 Figure 1 记录了 Yi-34B 在两张 A100 上服务 128 条 arxiv 请求时持续数秒的 generation stall:vLLM 把 prefill 插在连续 decode 之间,decode 的 token 间隔被整段 prefill 拉长 Figure 1。 Figure 7 把机制画得更清楚:vLLM 会尽量多排 prefill,再恢复 decode;Orca 虽然支持 hybrid batch,但一整段长 prefill 仍在一次迭代里执行,batch 时间依旧很长 Figure 7。
Sarathi-Serve 量化的代价是:把完整 prefill 与 decode 混在一起,TBT 相比 decode-only batch 最多增加到 28.3 倍(Mistral-7B / 1×A100 / budget 256;LLaMA2-70B / 4×A100 / budget 512) Figure 9。 DistServe 从另一个角度给出同样的结论:向 decode batch 里加入 prefill 会延长整批执行时间,prefill 越长越明显;向已经饱和的 prefill batch 里加入 decode 也会延长执行时间 Figure 2。
硬件趋势让问题更尖锐。Splitwise 的 Table I 对比 A100 与 H100:H100 的 TFLOPs 是 A100 的 3.43 倍,HBM 带宽只增加到 1.64 倍,HBM 容量仍是 1.00 倍,功耗是 1.75 倍 Table I。 decode 受带宽与容量约束,prefill 受算力约束;同一代 GPU 的缩放比例不同,于是「用不同硬件跑两个阶段」开始有吸引力。这正是 PD 分离的出发点。
读完这节你应该知道:prefill 优先、decode 优先、chunked prefill、PD 分离各自优化什么,又把代价留在哪里。
表 1 · 四条路线的调度顺序与代价(本页整理,证据列在末列)
| 路线 | 每一步先排什么 | 长 prompt 的影响 | 主要代价 | 代表与出处 |
|---|---|---|---|---|
| prefill 优先 | 尽量多排 prefill,再恢复 decode | 长 prefill 占满一步,decode 被推迟 | TBT 尾延迟出现尖峰 | vLLM;Sarathi-Serve §2.5 / Figure 2 |
| decode 优先 | 清空进行中的 decode,再排新 prefill | decode 稳定,但 prefill 等待 | decode-only batch 小,吞吐低 | FasterTransformer;Sarathi-Serve Figure 2 |
| chunked prefill | decode + 一个或多个 prefill chunk | chunk 被 token budget 截断 | chunk 开销;仍在同一池内竞争 | Sarathi-Serve §4;vLLM V1 默认开启 |
| PD 分离 | prefill 池与 decode 池各自调度 | 两阶段互不干扰 | KV 传输;池子碎片与额外资源 | DistServe §1;Splitwise §IV |
Sarathi-Serve 在 §2.5 把当时的调度器分成 prefill-prioritizing 与 decode-prioritizing 两类:前者优化吞吐但牺牲 TBT 尾延迟,后者保护 TBT 但 decode batch 小、吞吐上不去 §2.5。 Orca 的 iteration-level batching 已经能在一个 batch 里混合 prefill 和 decode,但它没有把长 prefill 切成受 token budget 约束的 chunk;只要 prompt 够长,这一批的执行时间仍然会被拉长,generation stall 没有被消除 §3.2。
chunked prefill 与 PD 分离是两条不同的补救路线。chunked prefill 保留单池,把长 prefill 拆成多步,用预算约束每一步的计算量;它不引入 KV 迁移,但 chunk 本身有开销,而且 decode 仍与 prefill 共享 GPU。PD 分离则把两个阶段放到不同 GPU,彻底消除同池干扰,并把「每个阶段用多少 GPU、用什么并行策略」变成独立变量;代价是每个请求的 KV 必须从 prefill 实例传到 decode 实例 §1。
Splitwise 把这一思路扩展成三个机器池:prompt 池、token 池,以及随负载扩缩的 mixed 池;mixed 池里的机器仍保留原身份,空闲后再回到原来的池子 §IV-A。 mixed 池用于降低 prompt 池与 token 池之间的碎片,并在负载升高时维持 SLO §IV-A。
读完这节你应该知道:stall-free batching 的顺序约束是什么,chunk 大小由谁决定,以及 vLLM V1 的共享预算与它差在哪里。
Sarathi-Serve 的调度器是 iteration-level 的。每个迭代先装入所有正在运行的 decode(Algorithm 3 lines 6–8),再装入任何尚未完成的 prefill chunk(lines 10–12),最后才在剩余 token budget 内接纳新请求(lines 13–20) Algorithm 3。 关键在于:新 prefill 只能使用「decode 装完之后剩下的预算」,所以它不会把已经在跑的 decode 暂停或推迟到下一迭代。论文把这一点称为 stall-free batching。
“By restricting the computational load in every iteration, stall-free batching ensures that decodes never experience a generation stall due to a co-running prefill chunk.”
Sarathi-Serve §4.2
把上述顺序写成简化式,每一步给 prefill 的空间是:
简化式,不是 Algorithm 3 的逐行还原。B 是每迭代 token budget,Dt 是该步已装入的 decode token 数,Pleft 是尚未计算的 prompt token 数;论文还会把新请求和进行中的 chunk 一起装,最后一块通常短于前面的块。
论文没有把 B 写死。B 由 TBT SLO、chunk 开销、tile quantization 和 pipeline bubble 共同决定,作者用 Vidur 做一次性 profiling,选「在不违反 TBT SLO 的前提下能装下的最大 token 数」§4.3。 评测里,宽松 SLO 用 2048,严格 SLO 用 512;LLaMA2-70B 的宽松配置用 1536 以减少 pipeline bubble §5.1。
vLLM V1 的调度器没有独立的 prefill / decode 队列。每个请求只记 num_computed_tokens 与总 token 数,调度器每步把它「补齐」;所有请求共享一个 token budget。源码里 token_budget = self.max_num_scheduled_tokens、input_budget = max_num_batched_tokens,先遍历 running,再遍历 waiting 源码 vllm/v1/core/sched/scheduler.py:741。
每个 running 请求本步可拿到的 token 数,按下面的顺序被逐步截断:
num_new_tokens = num_tokens_with_spec + placeholders − num_computed_tokensif 0 < long_prefill_token_threshold < num_new_tokens: num_new_tokens = thresholdnum_new_tokens = min(num_new_tokens, token_budget, input_budget − draft_slots)
源码 vllm/v1/core/sched/scheduler.py:861-870 的简化转录;随后还有 max_model_len、Mamba 块对齐、encoder 预算等截断,未全部列出。
调度成功后,num_scheduled_tokens[request_id] = num_new_tokens,并从两个预算里扣减 源码 vllm/v1/core/sched/scheduler.py:1023。
这一步的输出由 SchedulerOutput.num_scheduled_tokens 带到 worker 源码 vllm/v1/core/sched/output.py:250;请求的 num_computed_tokens、num_output_tokens 也在 CachedRequestData 里回传 源码 vllm/v1/core/sched/output.py:157,is_context_phase 以 num_output_tokens == 0 判定 prefill 阶段 源码 vllm/v1/core/sched/output.py:195。
Sarathi-Serve:token budget 是按 TBT SLO 预先选定的;调度顺序把 decode 放在最前,目标是让 co-running 的 prefill chunk 不造成 generation stall(§4.2–§4.3)。
vLLM V1:预算默认由硬件与 UsageContext 选择,调度器仍先 running 后 waiting。公开调度参数里没有以毫秒为单位的 TBT 上限,尾延迟由 decode 优先、token budget 与 chunk cap 间接控制;KV 不足时会抢占并 RECOMPUTE docs/configuration/optimization.md:47,prefix cache 会让 num_computed_tokens 先跳到命中块边界 源码 vllm/v1/core/sched/scheduler.py:1262,Mamba 模型还要按块对齐切 chunk 源码 vllm/v1/core/sched/scheduler.py:883。
因此,页面不把 vLLM 的 chunked prefill 写成 Sarathi-Serve 的复现:两者的共同点是「一步只算一段 token」,差别在预算怎么来、尾延迟有没有硬约束、以及是否允许抢占和 prefix-cache 跳转。
读完这节你应该知道:预算变小为什么改善 TBT、却增加额外开销,以及 vLLM 的三个相关开关各自做什么。
预算 B 的两端很清楚:B 小,单步 prefill 少,decode 等待短,TBT 更好;但同样的 prompt 被切成更多块,每块都要重新读前面所有块的 KV,固定开销与 HBM 读放大。B 大,prefill 更快完成、TTFT 更好,但单步变长,decode 的尾延迟更容易被顶起来。 Sarathi-Serve 的 §4.3 用一个具体例子描述读放大:prompt 被切成 N 块,第一块的 KV 会被后续块读 N−1 次,第二块读 N−2 次,依此类推;不过即使 chunk 很小,attention prefill 仍是 compute-bound,额外开销主要来自 kernel launch 等固定成本 §4.3。
Figure 14 给出 Yi-34B 上的实测:chunk 512 时,prefill 计算开销最高约 25%;chunk 2048 时几乎可以忽略 Figure 14。 还有 tile quantization:矩阵维度不整除 tile 时,部分 thread block 做无效计算;论文报告在某些情况下 chunk size 257 相比 256 可让 prefill 时间多 32% §4.3。Figure 14 的评测配置用的就是 512、1024、2048 这类 2 的幂。
表 2 · vLLM V1 中与 chunked prefill 直接相关的开关(commit e740bad)
| 开关 | 代码默认 | 行为与出处 |
|---|---|---|
enable_chunked_prefill |
True |
config 里的默认值;EngineArgs 在用户未显式设置时按 model_config.is_chunked_prefill_supported 决定。encoder-decoder 模型在 __post_init__ 强制关闭 chunked prefill;同一段日志声明同时关闭 prefix caching。来源:vllm/config/scheduler.py:147、vllm/engine/arg_utils.py:3078、vllm/config/scheduler.py:335。 |
max_num_batched_tokens |
2048 |
SchedulerConfig 的代码默认;EngineArgs 会按 GPU 显存与 UsageContext 覆写。API server 在默认档(含 A100 与 <70 GiB)为 2048,H100/H200 档为 8192,≥160 GiB 档为 16384;offline LLM 类对应 8192 / 16384。throughput 模式还会在用户未设置时翻倍。来源:vllm/config/scheduler.py:41、vllm/engine/arg_utils.py:3004、vllm/engine/arg_utils.py:3215。 |
long_prefill_token_threshold |
0 |
0 表示不设单请求 cap。非 0 时,若本步该请求的 num_new_tokens 超过它,就截到该值;仅当 chunked prefill 开启且运行中 + 等待中的请求数超过 1 时生效。请求是唯一一条时,阈值被忽略,因为它没有别人可饿。来源:vllm/config/scheduler.py:91、vllm/v1/core/sched/scheduler.py:781。 |
long_prefill_token_threshold_adaptive |
False |
开启后,把有效阈值抬到「max_num_batched_tokens / 可调度请求数」的公平份额,避免阈值把单个请求压到公平份额以下;仅在 threshold 非 0 时生效。来源:vllm/config/scheduler.py:99、vllm/v1/core/sched/scheduler.py:786。 |
max_num_scheduled_tokens |
None |
调度器单步最多发出的 token 数;默认等于 max_num_batched_tokens,speculative decoding 可能让它更小,以便给 draft token 留位置。来源:vllm/config/scheduler.py:55、vllm/v1/core/sched/scheduler.py:139。 |
关闭 chunked prefill 有一个硬约束:代码在 max_num_batched_tokens < max_model_len 时抛 ValueError,即禁止 max_num_batched_tokens 小于 max_model_len vllm/config/scheduler.py:358;文档的措辞是 must be greater than max_model_len,并警告更小会在启动时崩溃 docs/configuration/optimization.md:70。
默认开启的说明在 optimization 文档里写得很直接:V1 在模型支持时默认启用 chunked prefill,调度先排 decode,再用剩余预算排 prefill,装不下就自动切块 docs/configuration/optimization.md:53。
5000/128/2048/512 为页面算例;B=2048/512 可对照 Sarathi-Serve §5.1。交互是简化模拟,不是源码还原。它只算「固定预算、单请求、无抢占」的静态切片;未覆盖 CUDA Graph、KV 传输、优先级调度、prefix cache、speculative decoding 与 DP 负载均衡。
算例 A(§04 交互 + §05 交互):chunk 侧取 P=5000、D=128、B=2048、C=512。剩余预算 2048 − 128 = 1920;每步 min(512, 1920) = 512;5000 = 9 × 512 + 392,所以 10 步,最后一步 392 tokens。JS 在同参数下输出 chunk=512、steps=10、last=392。KV 侧取 P=512、R=10:1.13 × 512 / 512 = 1.13 GB,1.13 × 10 = 11.3 GB/s,11.3 × 8 = 90.4 Gbps;JS 输出 1.13、11.3、90.4。
算例 B(§04 交互 + §05 交互):chunk 侧取 P=5000、D=1024、B=2048、C=1024。剩余预算 2048 − 1024 = 1024;每步 min(1024, 1024) = 1024;5000 = 4 × 1024 + 904,所以 5 步,最后一步 904 tokens。JS 输出 chunk=1024、steps=5、last=904。KV 侧取 P=2048、R=5:1.13 × 2048 / 512 = 4.52 GB,4.52 × 5 = 22.6 GB/s,22.6 × 8 = 180.8 Gbps;JS 输出 4.52、22.6、180.8。
未覆盖分支:§04 模拟不建模 KV 不足时的抢占、Mamba 块对齐、encoder 输入预算、max_num_scheduled_tokens 与 max_num_batched_tokens 的差异,也不建模 CUDA Graph、prefix cache 命中、speculative decoding、优先级调度和 DP prefill balancing。§05 模拟假设 KV 随 token 数线性、请求率恒定;不建模分块/分层传输、压缩、量化、链路竞争、connector 协议开销与排队。
读完这节你应该知道:混批一步里到底装了什么,PD 分离的 KV 传输门槛有多大,以及部署拓扑为什么必须围着带宽设计。
同池混批的一步可以用一句话概括:decode tokens + prefill chunk ≤ token budget。decode 部分每请求只贡献 1 个 token(speculative decoding 下更多),prefill 部分贡献一个 chunk;两者的和不能超过预算。chunk 装不下就留到下一步,prompt 因此被摊到多个迭代里完成。
PD 分离则把这一步拆开:prefill 实例只做 prefill,生成首 token 和 KV;decode 实例接着做自回归生成。两个实例之间没有共享的 batch,也就不存在「prefill 把 decode 顶慢」的问题,但 KV 必须跨实例搬运。
DistServe 的测试床是 4 个节点、32 张 A100-80GB,节点内 NVLink,跨节点带宽只有 25 Gbps §6.1。 在这个约束下,它采用低节点亲和的放置算法,尽量让配对的 prefill / decode 实例落在同一节点内,让 KV 走 NVLink;只有当跨节点带宽足够时,才把两阶段随意放到不同节点 §4.2。 这不是实现细节,而是 PD 分离能否成立的前提:KV 传输不是免费的,拓扑必须围着带宽设计。
DistServe 给出的 KV 账本很具体:OPT-66B 上一条 512-token 请求的 KV 约 1.13 GB;若平均到达率 10 req/s,需要搬运 11.3 GB/s,等价于 90 Gbps §3.3。 90.4 Gbps 是测试床 25 Gbps 跨节点链路的 3.62 倍,但只占论文举例的 800 Gbps InfiniBand 的 11.3%;A100 节点内 NVLink 峰值 600 GB/s。三个数字对应三种放置:跨节点低带宽、跨节点高带宽、同节点。 也正因如此,OPT-175B 的延迟拆分里,KV 传输只占总延迟的 不到 0.1% §6.3;在 Figure 10(b) 三个 OPT 模型的 CDF 中,95% 以上的请求传输延迟低于 30 ms Figure 10——前提是放置算法把传输留在 NVLink 上。
DistServe 的另一半是「每个阶段独立选并行策略」。Table 3 里,OPT-66B 的 prefill 用 TP4/PP1,decode 用 TP2/PP2;OPT-175B 的 prefill 用 TP3/PP3,decode 用 TP4/PP3 Table 3。 同池部署做不到这一点:一套 TP/PP 同时服务两个阶段,prefill 需要的算力并行与 decode 需要的带宽/容量并行被绑在一起。
vLLM 的 PD 分离走的是另一条工程路线:起两个 vLLM 实例,一个 prefill、一个 decode,用 connector 把 prefill 实例的 KV cache 与结果传到 decode 实例 docs/features/disagg_prefill.md:90。 文档列出 NixlConnector、MooncakeConnector、LMCacheConnectorV1、OffloadingConnector 等多种 connector docs/features/disagg_prefill.md:20;scheduler 进程里的 connector 负责调度传输 docs/features/disagg_prefill.md:115,worker 进程里的 connector 负责执行 docs/features/disagg_prefill.md:116。 但文档同时把话说死:该特性是 experimental docs/features/disagg_prefill.md:6,且 disaggregated prefill 不提升吞吐 docs/features/disagg_prefill.md:16;它的用途是分别调 TTFT 与 ITL docs/features/disagg_prefill.md:12、控制尾 ITL docs/features/disagg_prefill.md:13。
按 DistServe §3.3 的 OPT-66B / 512-token 算例做线性外推:1.13 GB × (tokens / 512),再乘请求率。简化模拟,不是源码还原;未覆盖 KV 压缩、分块/分层传输、链路竞争与 connector 协议开销。
读完这节你应该知道:Sarathi-Serve 的消融表怎么读,容量收益的条件是什么,以及 DistServe/Splitwise 的数字为什么不能直接横向比较。
Sarathi-Serve 的 Table 4 把 hybrid batching 与 chunked prefills 分开做消融:128 条请求、Yi-34B、两张 A100、token budget 1024 Table 4。 表里 openchat_sharegpt4 与 arxiv_summarization 两个数据集各有 P50 TTFT、P99 TBT 两列,共 4 个数值列。
Table 4 · hybrid batching 与 chunked prefills 的消融(单位:秒;条件见上)
| Scheduler | openchat P50 TTFT | openchat P99 TBT | arxiv P50 TTFT | arxiv P99 TBT |
|---|---|---|---|---|
| hybrid-batching-only | 0.53 | 0.68 | 3.78 | 1.38 |
| chunked-prefills-only | 1.04 | 0.17 | 5.38 | 0.20 |
| Sarathi-Serve (combined) | 0.76 | 0.14 | 3.90 | 0.17 |
先看 TBT。combined 相比 hybrid-only,openchat 的 P99 TBT 从 0.68 s 降到 0.14 s,低 0.54 s,为前者的 1/4.86(约 4.86 倍加速);arxiv 从 1.38 s 降到 0.17 s。这正是 stall-free 的目标:长 prefill 被切碎,decode 不再被整段 prefill 卡住。
再看 TTFT。combined 相比 chunked-only,openchat 的 P50 TTFT 从 1.04 s 降到 0.76 s,低 0.28 s,为前者的 1/1.37;arxiv 从 5.38 s 降到 3.90 s,低 1.48 s,为前者的 1/1.38。 但 combined 的 TTFT 并不优于 hybrid-only:openchat 是 0.76 s 对 0.53 s,高 0.23 s;arxiv 是 3.90 s 对 3.78 s,高 0.12 s。切片让首 token 更晚,这是 chunked prefill 的代价,不是免费的收益。
容量收益也都有条件。Sarathi-Serve 在 Mistral-7B / 单 A100 上相对 vLLM 达到 2.6 倍 serving capacity;Yi-34B / 两张 A100 上最高 3.7 倍;Falcon-180B 在 pipeline parallelism 下端到端最高 5.6 倍 §1。 在严格 SLO(Mistral-7B,100 ms)下用 token budget 512,容量是 vLLM 的 3.5 倍;在宽松 SLO(Yi-34B,1 s)下用 2048,是 1.65 倍 §5.2。 这些数字对应不同模型、GPU 和 SLO,不能把「2.6 倍」当成通用常数。
PD 分离的数字来自另一套指标。DistServe 在 90% SLO 达成下,相对 DeepSpeed-MII 的 chatbot 基线(1.6–7.4×)可服务 7.4 倍更多请求;相对 vLLM 的 summarization/OPT-66B 结果,可承受 12.6 倍更紧的 SLO §6.2。 Splitwise 的集群设计则报告:相比现有设计,吞吐高 1.4 倍且成本低 20%;或者在相同功耗与成本下吞吐高 2.35 倍 §1。 这些是不同论文、不同基线、不同 SLO 定义下的结果;把它们与 Sarathi 的「2.6 倍」并列可以看趋势,不能当成同一张排行榜。
一个反直觉的点:vLLM 的 disaggregated prefill 实现并不提升吞吐。vLLM 文档明确写「Disaggregated prefill DOES NOT improve throughput」docs/features/disagg_prefill.md:16,它优化的是 TTFT/ITL 的独立调节 docs/features/disagg_prefill.md:12与尾 ITL 的控制 docs/features/disagg_prefill.md:13。 DistServe 的 7.4 倍是「在同样的 TTFT/TPOT SLO 下,每 GPU 能服务的请求率」,不是单卡 token/s 的裸吞吐。把 goodput 与 throughput 混为一谈,会读错这一类结果。
读完这节你应该知道:chunked prefill 与 prefix caching 如何相互影响,PD 分离的传输开销什么时候会吞掉收益,以及哪些 vLLM 默认值不能直接套。
chunk 512 时 Yi-34B 的 prefill 开销最高约 25%,chunk 2048 时接近可忽略 Figure 14;chunk 257 相比 256 可多 32% 的 prefill 时间 §4.3。若 SLO 不紧,没必要把预算压得很小。
prefix cache 只缓存完整块 docs/design/prefix_caching.md:22。调度器先把 num_computed_tokens 推到命中前缀的块边界,chunked prefill 再对剩余 token 切片;同一个 prompt 在冷缓存与热缓存下的 chunk 数不同。
512-token / OPT-66B 约 1.13 GB;10 req/s 就要 90 Gbps §3.3。跨节点只有 25 Gbps 时,必须把配对的 prefill/decode 放到同节点走 NVLink,否则传输会成为瓶颈。
两者共享 num_computed_tokens 这个进度变量。prefix cache 命中后,调度器从命中前缀的末尾开始补 token;chunked prefill 只对「尚未计算的后缀」切片,所以一个看起来很长的 prompt,在热缓存下可能只剩很短的 prefill。反过来,冷缓存下同一 prompt 会被切成更多块,TTFT 更晚、chunk 开销更高。
由于 vLLM 只缓存完整块 docs/design/prefix_caching.md:22,chunk 边界未落在块边界时,该 chunk 末尾不足一块的部分不会进入 prefix cache;它要等块被填满后才能被后续请求复用。chunk 切得越碎,可复用的粒度越受限,这是 chunked prefill 与 prefix cache 之间的一个结构性摩擦 推断(依据:prefix cache 的「只缓存完整块」规则与 chunk 的任意 token 边界)。这不影响输出正确性,但会影响多轮对话、共享 system prompt 这类前缀复用场景的收益。
还有一层:prefix cache 命中会让 long_prefill_token_threshold 更容易「不触发」。阈值比较的是本步 num_new_tokens,而 cache 命中已经把可计算 token 数减小;原本会被 cap 的长 prompt,热缓存下可能一次就补完 源码 vllm/v1/core/sched/scheduler.py:866 推断(依据:阈值比较的是本步 num_new_tokens,prefix cache 命中会先减小它)。调阈值时要把缓存命中率一起看,不能只看 prompt 长度。
OPT-175B 的延迟拆分里 KV 传输只占不到 0.1% 总延迟 §6.3;Figure 10(b) 三个 OPT 模型的 CDF 中,95% 以上的请求低于 30 ms Figure 10;但这个结果依赖「同节点 NVLink」的放置。跨节点带宽只有 25 Gbps 的测试床上,10 req/s 的 1.13 GB/请求对应 90 Gbps,已经超过链路容量。DistServe 因此把放置算法作为系统的一部分,而不是部署后的调优项 §4.2。
PD 分离还有资源账。Sarathi-Serve 在相关工作里指出:分离需要把每个请求的 KV 从 prefill 迁到 decode,缺少高带宽互联时会很困难;同时 prefill 副本的 GPU 显存容量没有被充分利用,因为只有 decode 副本负责长期保存 KV §6。 Splitwise 用 mixed pool 缓解池子碎片,但 mixed 池使用 mixed continuous batching §IV-A,原本 prompt 池与 token 池之间的隔离在这个池内不再成立 推断(依据:mixed 池把 prompt 与 token 任务放进同一 continuous batch)。
长上下文会同时放大两边:KV 大小随 prompt 线性增长,prefill 计算随 prompt 平方增长。DistServe 在讨论长上下文时认为,传输的相对占比会因 prefill 计算增长更快而下降,但绝对传输量仍在上升;分离在长上下文下依然有价值,前提仍是带宽与放置 §7。
DistServe 的 §7 还划了两条边界:离线、吞吐优先的场景,chunked-prefill 这类同池方案可能更合适;只有少量 GPU 的资源受限场景,分离架构的搜索空间很小,非分离系统更简单 §7。这是适用范围的问题:DistServe 优化的是 goodput,不是所有场景的默认答案;离线吞吐优先或资源受限时,同池方案仍然更合适。
max_num_batched_tokens 的 2048 只是 SchedulerConfig 的代码默认;真实服务由 EngineArgs 按 GPU 与 UsageContext 覆写:A100 80GB 的 API server 落在 2048 档,H100/H200 的 API server 是 8192,≥160 GiB 的 GPU 是 16384;offline LLM 类则分别是 8192 / 16384 vllm/engine/arg_utils.py:3004。
所以「vLLM 默认 2048」只在特定硬件与使用方式下成立,不能当成所有部署的默认。
enable_chunked_prefill 的 True 也不是无条件:EngineArgs 在用户未设置时按模型是否支持来决定,encoder-decoder 模型会被强制关闭 vllm/config/scheduler.py:335;关闭后还必须满足 max_num_batched_tokens ≥ max_model_len vllm/config/scheduler.py:358。
long_prefill_token_threshold 默认 0(不设 cap),且只在「chunked prefill 开启 + 不止一条可调度请求」时生效;单请求场景下它被忽略 vllm/v1/core/sched/scheduler.py:781。
选择上可以按三本账判断:如果只有一个 GPU 池、SLO 主要卡 TBT、prompt 长但还能接受额外算力,chunked prefill 是低改造成本的选择;如果 TTFT 与 TPOT 需要分别调、集群有高带宽互联、流量足以填满两个池,PD 分离能把干扰与并行策略解耦;如果跨节点带宽有限又无法同节点配对,PD 的 KV 传输会先成为瓶颈,此时不要把 DistServe 的 0.1% 当成自己集群的预期。
最后一句话留给口径:Sarathi-Serve 的 2.6/3.7/5.6 倍是「serving capacity」,DistServe 的 7.4 倍是「90% SLO 下的请求率」,Splitwise 的 1.4/2.35 倍是「集群吞吐或同功耗吞吐」;vLLM 的 2048 是「代码默认值」。这些数字回答的是不同问题,页面把它们分开放,就是为了避免互相冒充。