一分为二:面向 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 后端)和 MooncakeConnectorOffloadingConnectorFlexKVConnectorV1 [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 实例 + NixlConnectorLMCacheMPConnector;预期低并发时单体胜出,负载升高后出现尾部 ITL 控制)[4]。

状态与时效性说明(2026-08-09)#

  • vLLM 分离式预填充仍是实验特性,随时可能变动;文档中的连接器清单与 kv_transfer_params API 可能随版本变化 [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]。

参考文献#

  1. 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.
  2. 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.
  3. 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.
  4. vLLM 官方文档. Disaggregated Prefilling (experimental). https://docs.vllm.ai/en/stable/features/disagg_prefill.html
  5. Mooncake 项目. 官方仓库与文档(Transfer Engine、Store、轨迹). https://github.com/kvcache-ai/Mooncake · https://kvcache-ai.github.io/Mooncake/
  6. NVIDIA Dynamo 文档. Disaggregated serving(dev). https://docs.nvidia.com/dynamo/dev/knowledge-base/concepts/system-architecture/disaggregated-serving
  7. Microsoft. Azure LLM Inference Trace(AzurePublicDataset). https://github.com/Azure/AzurePublicDataset
  8. Splitwise 事件模拟器(开源). https://github.com/Mutinifni/splitwise-sim
字体
阴影
滤镜
圆角
主题色