一分为二:面向 SLO 的 LLM 预填充/解码分离架构
本页目录
一分为二:面向 SLO 的 LLM 预填充/解码分离架构
单个服务实例把提示词处理与逐词元生成混在一起,实现起来很简单,对很多部署来说也是正确选择。但一旦你需要在持续请求速率下满足延迟目标,LLM 请求的两个阶段就会在同一批 GPU 上互相干扰。预填充/解码分离(prefill/decode disaggregation)通过把两个阶段拆分到独立的机器池、并用快速的 KV 缓存交接把它们连接起来,消除了这种冲突。本文基于主要的系统论文(DistServe、Splitwise、Mooncake)以及生产引擎(vLLM、NVIDIA Dynamo)的现行官方文档,说明分离架构到底买到了什么——以及买不到什么。
文中所有性能数字均引自所列论文与官方文档,并附有各自的测试环境;本文未在本机复现任何一项。文章最后给出一个可在单机上执行的确定性轨迹回放实验设计,用来实测这一权衡。
两种延迟,一条队列#
一个 LLM 请求包含资源画像截然相反的两个阶段:
- 预填充(prefill):用一次前向传播处理完整提示词并产出第一个词元。它受计算限制:对 13B 模型而言,512 个词元的提示词已足以让一块 A100 接近计算饱和 [1]。
- 解码(decoding):逐词元、逐步生成后续输出。每一步都要读取整个 KV 缓存,因此受显存带宽限制,计算单元大量空闲 [1, 2]。
两个延迟指标由此而来。首词元时间(time to first token,TTFT)主要由预填充阶段决定;每输出词元时间(time per output token,TPOT)是解码阶段的平均单步时间。vLLM 官方文档把 TPOT 称为 inter-token latency(ITL),并将 TTFT 与 ITL 视为两个可独立调优的在线服务目标 [4]。
问题在于:在共置(单体)实例中,两个阶段共享同一块 GPU 和同一条队列。连续批处理(continuous batching)刻意把两者混批,于是一次漫长的预填充会拉长与其同批的解码步骤(TPOT 恶化),而同批的解码工作也会推迟预填充(TTFT 恶化)。DistServe 在 13B 模型上的测量清楚地展示了这一效应:在单块 A100 上以 90% SLO 达成率为目标,单体系统大约只能支撑 1.6 请求/秒,而同一块 GPU 若只做预填充可支撑 5.6 请求/秒,只做解码可支撑 10 请求/秒。用 2 块 GPU 做预填充、1 块做解码,即可服务 10 rps——即每 GPU 3.3 rps,是单体系统的 2.1 倍 [1]。
分块预填充(chunked prefill)是经典的缓解手段:把长预填充切成块,与解码步骤交错。但它只能减轻、无法消除干扰;本质上是拿 TTFT 换 TPOT;而且每计算一个后续分块都要重新读取之前所有分块的 KV 缓存——把一次预填充切成 $N$ 块,KV 缓存读取量从 $O(N)$ 变成 $O(N^2)$ [1]。vLLM 文档从生产侧给出了同样的结论:分块预填充”很难找出正确的分块大小”,因此分离架构是”控制尾部 ITL 的可靠得多的方式” [4]。
这就引出了 goodput(有效吞吐) 的概念。原始吞吐量(throughput)是不考虑延迟的每秒请求数或词元数;goodput 是在满足延迟 SLO 的前提下能达到的最大请求速率——DistServe 把每 GPU goodput 定义为维持某个 SLO 达成率目标(如 90%)的最大速率 [1]。本文中所有分离式系统的优化目标都是 goodput,而非吞吐量。
Figure 1 — Throughput vs goodput (illustrative; pattern per DistServe Fig. 1 [1])
served (req/s)
10 ┤ ─────────────────────────╱ raw throughput (saturates, ignores latency)
9 ┤ ╱
8 ┤ ╱
7 ┤ ╱╲ goodput (meets TTFT and TPOT SLOs)
6 ┤ ╱ ╲
5 ┤ ╱ ╲
4 ┤ ╱ ╲
3 ┤ ╱ ╲
2 ┤ ╱ ╲
1 ┤ ╱ ╲
0 └────────────────┴───────┴──────┴──────► offered load (req/s)
0 5 7 10
└── knee: beyond this, queueing pushes SLO
attainment below target; goodput falls
分离式部署的解剖#
分离架构让每个资源池各持一份完整模型权重,因此预填充实例与解码实例可以使用不同的并行策略(例如预填充用张量并行压缩 TTFT,解码用流水线/副本扩展速率),甚至使用不同类型的 GPU [1, 2]。路由(或集群级调度器)把每个请求分配给某个预填充实例;预填充实例跑完提示词后,把生成的 KV 缓存交给某个解码实例,由后者生成其余词元 [1, 2, 3]。
Figure 2 — Monolithic vs disaggregated topology (original diagram)
(a) Monolithic: one pool, mixed batches
clients ──► router ──► [ GPU pool: prefill + decode batched together ]
│ a long prefill delays decode steps;
▼ decode work inflates TTFT
tokens out
(b) Disaggregated: two pools, one KV handoff
clients ──► router ──► [ prefill pool ] compute-bound, small batches,
│ TP-heavy parallelism
│ KV cache per layer, transferred as
│ it is produced (overlapped)
▼
┌──────────────────────────┐
│ KV transfer / connector │ NVLink, InfiniBand,
│ (e.g. vLLM Connector, │ or RoCE; optional
│ Mooncake Transfer Eng.)│ DRAM/SSD KV-store tier
└──────────────────────────┘
│
▼
[ decode pool ] memory-bound, large batches,
│ older or power-capped GPUs OK
▼
tokens out
交接(handoff)是整个分离架构的全部代价:单体系统中根本不需要移动任何 KV 缓存。其余一切——调度、放置、资源池规模——都是为了让这次交接变得廉价或不可见。
KV 交接:多少数据、多快传输#
有多少数据。 一个词元的 KV 缓存大小为:
KV 字节/词元 = 2(K 和 V)× 层数 × KV 头数 × 头维度 × 每元素字节数
其中 GQA 模型的 KV 头数少于查询头数,FP16 每元素占 2 字节。把该公式套到当前常见模型配置上(表 1):Llama-3.1-8B 的 2k 词元提示词约产生 256 MiB KV 缓存;同式可得 LLaMA3-70B 约 320 KiB/词元,与 Mooncake 文档中”128k 词元的 LLaMA3-70B KV 缓存约 40 GB”的表述一致 [5]。
| 模型 | 层数 | KV 头数 | 头维度 | KV 字节/词元(FP16) |
|---|---|---|---|---|
| Llama-3.2-1B | 16 | 8 | 64 | 32 KiB |
| Qwen2.5-1.5B | 28 | 2 | 128 | 28 KiB |
| Llama-3.1-8B | 32 | 8 | 128 | 128 KiB |
| Llama-3-70B | 80 | 8 | 128 | 320 KiB |
表 1 — 每个词元的 KV 缓存大小,按上文公式由各模型公开配置计算;Llama-3-70B 一行与 Mooncake 文档记载的 128k 词元约 40 GB 一致 [5]。
要多快。 所需传输带宽为 KV 字节/词元 × 提示词长度 × 到达速率。DistServe 给出的实例:OPT-66B 上 512 词元请求的 KV 缓存约 1.13 GB;按 10 请求/秒计算,需要持续 11.3 GB/s(约 90 Gbps)的传输带宽才能让交接”隐形” [1]。其测试环境跨节点带宽只有 25 Gbps(4 节点 × 8×A100-80GB),因此放置算法只能在可行时把预填充-解码配对放到 NVLink 互联的 GPU 上 [1]。
走什么路径。 有三种代表性设计:
- 串行传输:预填充结束后一次性搬移全部 KV 缓存,之后才开始首个解码步骤。实现简单,但传输时间直接叠加到第二个词元的延迟上。
- 按层重叠传输(Splitwise):每一层算完,就用零拷贝、单边 RDMA put(基于 MSCCL++ 实现)把该层的 KV 发送出去,同时计算下一层。未能重叠的残余传输时间在 A100(200 Gbps InfiniBand)上约 8 ms、在 H100(400 Gbps)上约 5 ms,不到提示词计算时间的 7%;对于小提示词(< 512 词元),Splitwise 回退到串行传输,因为 KV 太小不值得做按层传输 [2]。
- 多网卡传输引擎(Mooncake):Transfer Engine 聚合多张 RDMA 网卡、按拓扑选择最优路径并自动故障切换。它搬运 40 GB(即 LLaMA3-70B 128k 词元的 KV 缓存)时,在 4×200 Gbps RoCE 网络上达到 87 GB/s,在 8×400 Gbps RoCE 网络上达到 190 GB/s——分别约为 TCP 的 2.4 倍和 4.6 倍 [3, 5]。
Figure 3 — Request timeline and the KV handoff (original diagram)
arrival queue prefill KV transfer decode
│ │ │ │ │
├───────────┼──────────────┼───────────────┼─────────────────┤
│ router │ prefill │ layer-wise │ first token │
│ assigns │ computes │ RDMA puts │ served, then │
│ prefill │ prompt; │ overlap with │ steady TPOT │
│ instance │ KV produced │ next layer │ (decode pool) │
▼ ▼ ▼ ▼ ▼
└───────────┴──────────────┴───────────────┴─────────────────┴──► time
TTFT = queue + prefill + residual transfer + first decode step
TPOT = time per subsequent decode step (transfer fully overlapped
by then, or hidden behind batching)
Measured impact of the residual (Splitwise, coding trace):
serialized transfer: second-token latency +64%, E2E up to +3%
layer-wise transfer: second-token latency +16.5%, E2E +0.8%
调度、网络与资源池规模#
调度。 三篇论文路线各异,目标都是同一个 goodput:
- DistServe 根据给定的 TTFT/TPOT 组合,为每个阶段联合优化 GPU 分配与并行策略,再依据集群真实带宽放置实例以最小化跨节点 KV 流量;实例内按 FCFS 运行,落地方案前用基于重采样轨迹的模拟器评估 SLO 达成率 [1]。
- Splitwise 采用两级调度:集群级调度器(CLS)在提示词/词元/混合三个池之间用最短队列(JSQ)路由请求;机器级调度器(MLS)按阶段批处理——提示词机器把总批限制在约 2048 词元,词元机器则在显存允许下尽量加大批次。负载升高时机器动态进出混合池以避免碎片化;高负载下一切最终退化为混合批处理 [2]。
- Mooncake 以 KV 缓存为中心:调度器在有效吞吐与延迟 SLO 之间取平衡,过载时采用基于预测的提前拒绝(early rejection),而不是让队列无限增长 [3]。
网络。 带宽需求随提示词长度 × 到达速率增长(见上文 90 Gbps 的例子)。这正是每个系统都依赖高速网络的原因:Splitwise 运行在 200/400 Gbps InfiniBand 上 [2];Mooncake 聚合 4×200 Gbps 或 8×400 Gbps 的 RoCE 网卡 [3, 5];DistServe 在 25 Gbps 测试环境下用 NVLink 共置放置来补偿 [1]。如果集群只有普通以太网而没有 RDMA,TCP 会把传输时间放大 2.4–4.6 倍(Mooncake 的实测差距),交接将无法再被隐藏。
资源池规模。 预填充与解码池的比例应当由负载的输入/输出词元分布决定,而不是拍脑袋:
- Splitwise 对编码(coding)轨迹(提示词大、输出中位数约 13 词元)的等功率配置是 H100 上 35 台预填充 : 5 台词元;对话(conversation)轨迹(输出中位数约 129 词元)则要 25 : 15 [2]。
- DistServe 的动机示例中,13B 摘要负载需要 2 块预填充 GPU 配 1 块解码 GPU [1]。
- NVIDIA Dynamo 文档补充了一条容量警告:解码侧的 KV 容量可能成为瓶颈,增加预填充副本反而可能降低系统总容量——如果真正限制并发的是解码 KV [6]。
分离并不存在普适的词元数阈值——生产文档明确如此说,池比例是需要实测的工作负载属性,而不是常量 [6]。
系统对比#
| 系统 | 出处/状态 | SLO 模型 | 调度 | KV 传输 | 代表性结果 | 测试环境/注意事项 |
|---|---|---|---|---|---|---|
| DistServe | OSDI’24 [1] | TTFT + TPOT 达成率 ≥ 90%(如 OPT-13B 对话 0.25 s / 0.1 s) | FCFS;分阶段并行联合优化;带宽感知放置 | 拉取式,NCCL + 异步拷贝;NVLink 共置配对 | 请求量提升 7.4 倍或 SLO 收紧 12.6 倍(对比 vLLM/DeepSpeed-MII) | 4 节点 × 8×A100-80GB,跨节点 25 Gbps;OPT 时代 MHA 模型 |
| Splitwise | ISCA’24 [2] | P50/P90/P99 × TTFT/TBT/E2E(9 项 SLO) | 两级 CLS/MLS;JSQ;动态混合池 | 按层 MSCCL++ 单边 put;小提示词串行 | 成本降低 20% 时吞吐提升 1.4 倍;同等成本+功耗下 2.35 倍 | DGX-A100/H100 虚机,InfiniBand 200/400 Gbps;BLOOM-176B、Llama-70B |
| Mooncake | FAST’25 最佳论文 [3, 5] | 过载下的延迟 SLO;提前拒绝 | KV 缓存中心调度器 | Transfer Engine:多网卡 RDMA 87/190 GB/s;DRAM/SSD 存储分层 | 模拟长上下文场景吞吐 +525%;Kimi 真实负载请求量 +75% | Moonshot/Kimi 生产环境;RoCE;仓库持续演进 |
| vLLM 分离式预填充 | 实验特性 [4] | 分阶段 TTFT / ITL | 实例内调度器;前置路由 | 连接器:NIXL、Mooncake、LMCache、Offloading、FlexKV、Multi、MoRIIO(ROCm)、示例 | TTFT/ITL 独立调优;尾部 ITL 控制 | 官方明确不提升原始吞吐;至少需要两个实例 |
| NVIDIA Dynamo | 生产框架,文档变动快 [6] | TTFT/ITL;容量规划 | PrefillRouter(感知 KV);运行时可重构 xPyD 池 | NIXL VRAM→VRAM 非阻塞;按后端区分传输模式 | 生产框架内的拓扑感知 KV 路由 | 低并发时单体胜出;解码 KV 容量可能成为瓶颈 |
表 2 — 预填充/解码分离系统,依据各论文与官方文档。
生产实践这一行值得特别强调,因为 vLLM 官方文档对分离式预填充不做什么说得非常明确:
分离式预填充不会提升吞吐量(Disaggregated prefill DOES NOT improve throughput)。
按文档的说法,其价值有两点:(1) 可以分开调优 TTFT 与 ITL——例如只给预填充实例分配不同的张量/流水线并行,而不影响解码;(2) 获得确定性的尾部 ITL 控制,因为预填充任务不再插入解码批次。实现上运行两个 vLLM 实例(预填充 + 解码),由 Connector 连接;核心抽象是 Connector(生产者→消费者)、LookupBuffer(非阻塞 insert、阻塞 drop_select)和 Pipe(FIFO 张量收发),并通过词元 ID 复用让解码实例跳过重复的提示词分词。文档目前列出九种连接器,从 NixlConnector(UCX/GDS 后端)和 MooncakeConnector 到 OffloadingConnector、FlexKVConnectorV1 [4]。相关研究中,分块预填充 + 两级调度的 TetriInfer(arXiv 2401.11181)在模拟中报告资源减少 38%、平均 TTFT 降低 97%、平均任务完成时间降低 47%——提醒我们分离架构只是更大调度设计空间中的一个点。
何时不应分离#
- 小规模部署。 分离至少需要两个实例(外加一个路由);单 GPU 上纯属开销。它是一种部署拓扑,不是可以随手开启的库开关。
- 低并发。 NVIDIA Dynamo 文档指出,低并发下单体配置胜出——队列都空着的时候,根本不存在需要消除的干扰 [6]。
- 没有延迟 SLO 的批处理负载。 Splitwise 自己的评估显示,在持续高负载下分离式集群会退化到混合批处理基线——goodput 的优势恰恰来自 SLO 过滤,而批处理任务不需要它,也就一无所获 [2]。
- 弱互联。 如果持续传输带宽无法逼近
KV 字节/词元 × 提示词长度 × 速率(DistServe OPT-66B 例子中的 90 Gbps [1]),交接就无法被隐藏,你将为一份权重付两份显存却换来更慢的系统。纯 PCIe、单网卡或非 RDMA 环境都处于这个区间。 - 短提示词、短输出。 Splitwise 对小提示词(< 512 词元)采用串行传输,因为两种方式开销都微不足道;反过来,解码阶段越短,传输开销占比越显眼 [2]。
- 运维成本。 两个池意味着两份权重、两个故障域、额外的可观测性,以及必须尊重解码 KV 瓶颈的容量规划 [6]。前缀缓存、精心调过分块大小的分块预填充、以及为重复前缀服务的 KV 存储,都可能以少得多的机制拿到部分收益。
一个确定性的轨迹回放实验#
集群级数字(7.4 倍、2.35 倍、+525%)来自特定的多节点测试环境,无法在工作站上复现。但机制本身——goodput 拐点随资源池比例如何移动——完全可以在单机上确定性地测出来,且不依赖墙上时钟:
环境。 一个约 300 行的事件驱动模拟器(可直接复用开源的 Splitwise 事件模拟器 [8],也可自写),输入只有三类:(a) 请求轨迹;(b) 固定性能画像表;(c) 固定参数(链路带宽、SLO、池比例)。
- 轨迹。 (a) ShareGPT,例如 vLLM
benchmark_serving.py使用的样例;(b)AzurePublicDataset中发布的 Azure LLM 推理轨迹子集(到达时间 + 输入/输出词元数)[7];(c) Mooncake 发布的 FAST’25 轨迹mooncake_trace.jsonl(到达时间、输入/输出词元、重映射后的块哈希)[5]。Mooncake 轨迹还便于日后扩展前缀缓存实验。 - 画像表。 在目标 GPU 上一次性测出(例如 TITAN RTX 24 GB 单卡跑 Llama-3.2-1B FP16):批大小 1/2/4/8 时的预填充词元/秒,以及批大小 1–64 时的解码词元/秒。把表及其 SHA-256 一并入库。
- 服务模型。 请求生命周期:到达 → 预填充队列 → 预填充服务(感知批大小,查表)→ KV 传输时间 =
KV 字节 / 链路带宽(参数,如 PCIe 12 GB/s 或以太网 25 Gbps)→ 解码服务(查表)。单体基线:混批服务时间同样取自该画像。 - 确定性。 墙上时钟完全不进入模型——所有服务时间都来自画像表;事件按
(到达时间, 请求 ID)排序并以固定规则打破平局;不采样、不用随机数。回放同一条轨迹和同一张表必然得到相同输出。结果头部记录轨迹与画像表的哈希。 - 扫描。 到达速率倍数 λ ∈ {0.2 … 2.0};固定 GPU 总数下改变预填充:解码比例(如 2:1、3:1、1:1、1:2);SLO:TTFT ∈ {0.5, 1, 2 s},TPOT ∈ {20, 50, 100 ms}。
输出与预期结论。 每种配置的 SLO 达成率-λ 曲线,以及供对照的原始吞吐:低 λ 时单体曲线持平或优于分离(交接纯属成本);越过拐点后,分离配置能维持更高的达成率,而胜出的池比例随负载的输入/输出词元分布移动——长输入轨迹需要更多预填充 GPU,与 Splitwise 的 35:5 对比 25:15 的配置规律一致 [2]。实验的目的是展示曲线形状(goodput ≠ 吞吐量;池比例取决于负载),而非产出可与集群等价的数字——每 GPU 服务速率来自本地画像,任何绝对数字被采信之前,都应先用双实例真实运行验证模拟器(单 GPU 上跑两个 vLLM 实例 + NixlConnector 或 LMCacheMPConnector;预期低并发时单体胜出,负载升高后出现尾部 ITL 控制)[4]。
状态与时效性说明(2026-08-09)#
- vLLM 分离式预填充仍是实验特性,随时可能变动;文档中的连接器清单与
kv_transfer_paramsAPI 可能随版本变化 [4]。 - 所有引用数字都有测试环境与时代局限:DistServe 与 Splitwise 实测于 A100/H100 时代、25–400 Gbps 网络 [1, 2];Mooncake 的数字来自 Kimi 生产环境与 RoCE 集群 [3, 5]。均为引述,未在本机复现。
- NVIDIA Dynamo 文档处于快速开发期;本文依据的是 dev 文档,内容可能已经变化 [6]。
- Mooncake 已远超出 FAST’25 论文的范围(Transfer Engine、Store、EP/PG);论文与官方仓库是稳定参考 [3, 5]。
参考文献#
- Yinmin Zhong, Shengyu Liu, Junda Chen, Jianbo Hu, Yibo Zhu, Xuanzhe Liu, Xin Jin, Hao Zhang. DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving(OSDI ‘24). arXiv:2401.09670.
- Pratyush Patel, Esha Choukse, Chaojie Zhang, Aashaka Shah, Íñigo Goiri, Saeed Maleki, Ricardo Bianchini. Splitwise: Efficient generative LLM inference using phase splitting(ISCA ‘24). arXiv:2311.18677.
- Ruoyu Qin, Zheming Li, Weiran He, Mingxing Zhang, Yongwei Wu, Weimin Zheng, Xinran Xu. Mooncake: A KVCache-centric Disaggregated Architecture for LLM Serving(FAST ‘25). arXiv:2407.00079.
- vLLM 官方文档. Disaggregated Prefilling (experimental). https://docs.vllm.ai/en/stable/features/disagg_prefill.html
- Mooncake 项目. 官方仓库与文档(Transfer Engine、Store、轨迹). https://github.com/kvcache-ai/Mooncake · https://kvcache-ai.github.io/Mooncake/
- NVIDIA Dynamo 文档. Disaggregated serving(dev). https://docs.nvidia.com/dynamo/dev/knowledge-base/concepts/system-architecture/disaggregated-serving
- Microsoft. Azure LLM Inference Trace(AzurePublicDataset). https://github.com/Azure/AzurePublicDataset
- Splitwise 事件模拟器(开源). https://github.com/Mutinifni/splitwise-sim