跳到正文
LLM 推理
S18分布式服务·186

Prefill/Decode 分离部署

prefill 受算力约束,decode 受带宽约束。挤在同一张卡上,两个 SLO 都达不到;那就把它们拆开,再把 KV cache 传过去。

  • P/D 拆分
  • KV 传输
  • NIXL/RDMA
  • SLO 达标
  • 实例配比调优

为什么重要

问题

分块 prefill(S11)让 prefill 和 decode 在同一张 GPU 上相安无事了,但没有让它们相容。它们想要的依然是相反的东西:

  • prefill 想要大 batch 和纯粹的算力。它每个请求只跑一次,一来就是一阵爆发,占用的缓存很少。
  • decode 想要显存带宽和一个大 KV 池。它每个请求要跑成千上万次,而且绝不能被打断。

混合部署时,每个旋钮都是妥协。调高 token 预算来加速 prefill,token 间延迟就变差。调高并发来提升 decode 吞吐,KV 池就挤掉了 prefill 的批处理空间。首 token 时间和 token 间延迟这两个 SLO,通过同一组参数耦合在一起,你没法只修一个而不动另一个。

核心思路

解法

别再共享了。让 prefill 跑在一组机器上、decode 跑在另一组上。prefill 完成后,把 KV 缓存经网络送到某个 decode 实例,然后交接。

现在每个池子只为一件事调优,每个 SLO 有自己的扩容旋钮,两种负载谁也干扰不了谁。TTFT 不达标就加 prefill 节点;token 间延迟不达标就加 decode 节点。

交接python
async def serve(request):
    p = router.pick_prefill_instance(request)     # ideally cache-aware
    d = router.pick_decode_instance(request)

    # Tell the decode instance to allocate blocks BEFORE prefill starts,
    # so the transfer can begin the moment the first layer is done.
    handle = await d.reserve_blocks(request.prompt_len)
    kv_ref = await p.prefill(request, push_to=handle)

    async for token in d.decode(request, kv_ref):
        yield token
    # The KV never round-trips through the router. It moves GPU to GPU.

工程量都在传输上

一个带 GQA 的 8B 模型,每 1000 个 prompt token 产生约 128 MB 的 KV。32k token 的 prompt 就是 4 GB,必须在第一个 token 输出之前送到另一台机器。在 100 Gb 以太网上,这大约给 TTFT 增加 300 ms。单张 400 Gb/s 的 RDMA 网卡是 50 GB/s,这次传输仍需约 80 ms;条带化到一台节点的八张网卡上(合计约 400 GB/s)才降到约 10 ms。所以分离部署是一项 RDMA 特性。没有 RDMA,传输的代价会超过它消除的干扰。

工作原理

工作原理

示意图prefill 池、KV 传输、decode 池
两个阶段,胃口正好相反prefill算力受限 · 要的是 FLOPs · 每个请求一次大爆发decode带宽受限 · 要的是 HBM 和 KV 容量 · 成千上万个小步把两者放在同一张 GPU 上,它们会互相干扰:prefill 会把 token 间延迟顶出尖刺,而 decode 常驻的 KV cache 又限制了你能批多少 prefill。分离式部署路由器挑一对池子PREFILL 池GPUGPUGPU为算力调优:大 batch、大分块、小 KV 池KVRDMA / NIXLDECODE 池GPUGPUGPUGPU为带宽调优:超大 KV 池、最大并发、小步长—— 永远没有东西打断 decode流式输出代价是什么整个 KV cache 都得过网络:8B 模型每 1k token 就是 128 MB。只有在 RDMA 上才可行,而且它直接计入 TTFT。逐层流式传输能把其中大部分藏在剩余的 prefill 层背后。换来了什么TTFT 和 token 间延迟变得可以独立调优:你只需扩容违反 SLO 的那个池子,而不是整个集群。超过几个节点才划算;低于这个规模纯属自找麻烦。
路由器挑一对实例,缓存经 RDMA 在 GPU 之间搬运,每个池子只为一种负载配置。

把传输藏起来

传输不必等到最后一次性完成。第 i 层的 KV 在第 i 层跑完的那一刻就已定型,因此可以在第 i+1 层计算的同时把它发出去。用逐层流式传输,除最后一层外的所有传输都被藏在 prefill 计算后面。

NIXL 和 vLLM 的 KV connector 接口正是为这件事而生:一个与计算重叠、而不是跟在计算后面的传输引擎。

如何选配比,以及为什么这很难

prefill:decode 的配比取决于负载形态。长 prompt 配短回答(分类、抽取、RAG)是 prefill 重的。短 prompt 配长回答(智能体、推理链)是 decode 重的。早上九点正确的 1:3 配比,下午三点可能就是错的。

因此生产系统会让配比动态化,随流量变化在两种角色之间重新分配实例。这套机制连同 RDMA 网络,正是分离部署只有在一定集群规模以上才划算的原因。几个节点以下它纯粹是额外开销,单池上的分块 prefill 才是更好的答案。

动手观察

动手试试

模拟器分离部署 vs 混合部署,对着 SLO 比
部署方式
TTFT
1210 ms
token 间延迟
9.0 ms
两个 SLO 都达标
KV 传输量
512 MB · 10 ms
请求路径

prefill 池 · 3 张 GPU

KV 传输 · 10 ms

decode 池 · 5 张 GPU

prefill 跑在为算力调优的机器上,decode 跑在为带宽和 KV 容量调优的机器上,缓存则通过互联在两者之间搬运。现在每个 SLO 都由自己的池子单独控制。

拆分比例扫描 · 绿色表示两个 SLO 都达标
1P
2P
3P
4P
5P
6P
7P
  • 两个 SLO 都达标
  • 违反了某个 SLO

柱高是 TTFT。prefill 的 GPU 太少,队列就会爆炸;decode 的 GPU 太少,token 间延迟就会爆炸。绿色区间很窄,而且负载结构一变它就移动 —— 让部署一直待在这个区间里,才是分离部署真正的运维成本。

在混合模式下设一个 prompt 重的负载:token 间延迟冲破 SLO,因为 prefill 正在吃掉 decode 的步。切到分离部署,调整拆分比例,直到两个指标都变绿;可行区间很窄。然后把 KV 链路带宽降下来,这次换成 TTFT 不达标,因为传输成了瓶颈。这就是这套架构做出的取舍。

亲手实现

实现

code/s18_disaggregation.py(节选)python
class LayerwiseKVSender:
    """Overlap the KV transfer with the prefill that is still running."""

    def __init__(self, transport, n_layers):
        self.transport, self.n_layers = transport, n_layers
        self.inflight = []

    def on_layer_done(self, layer: int, k, v, dest_blocks):
        # Layer i's KV is final the moment layer i finishes. Send it now,
        # while layers i+1..L are still computing.
        self.inflight.append(self.transport.send_async(layer, k, v, dest_blocks))

    def finish(self):
        for fut in self.inflight:
            fut.wait()          # only the LAST layer is actually exposed

在 prefill 开始之前就预留目标 block

如果 decode 实例要等 prefill 完成后才分配 block,传输就无法提前开始。更糟的是,那个 decode 池可能显存不足,于是一整次 prefill 白做了。先预留,再 prefill。这份预留同时给了路由器真正的背压:如果没有任何 decode 实例能预留,那就干脆别准入这个请求。
在本地运行
在一个共同的「GPU 秒」成本模型上模拟混合部署与分离部署,并显式计入 KV 传输项,对着可配置的 SLO 报告 TTFT 和 ITL,并扫描 prefill:decode 配比以找出可行区域。
$ python code/s18_disaggregation.py
预期输出: prompt 重负载下混合部署违反 token 间延迟 SLO,而 7P:1D 的分离部署两项都达标;配比扫描中一条很窄的可行带;以及一次带宽扫描:在 32k prompt 下,单是传输就决定了达标与否。

只需要 NumPy — 查看环境准备.

生产实践

在生产环境中

  • DistServeSplitwise —— 确立这一想法及其 goodput 论证的两篇论文。
  • vLLM —— 提供 KV connector 接口,带 NIXL 和 LMCache 两种后端;分离部署由生产栈支持,而不是由单进程引擎支持。
  • NVIDIA Dynamo —— 一套完整的分离部署服务框架,支持在 prefill 与 decode worker 之间动态改派角色。
  • Mooncake —— 更进一步:以 KV 缓存为中心的架构,用一个任何 decode 实例都能读取的共享缓存池,把分离部署与全局前缀缓存合二为一。

练习

  1. 1
    算出交叉带宽:在多快的互联下,KV 传输给 TTFT 带来的代价开始超过它消除掉的 prefill 干扰?答案取决于 prompt 长度,把曲线画出来。
  2. 2
    实现逐层流式传输,测量有多少传输被藏在了 prefill 计算后面。
  3. 3
    加一个动态控制器,根据实测的 SLO 违规情况在两个池之间改派实例,并展示它如何跟上一个中途从 prefill 重变成 decode 重的负载。

继续学习

接下来

引擎又快又能扩展了,但用户还是没法和它说话。S19 会构建服务层:OpenAI 兼容的流式 API、对话模板、请求取消,以及你真正拿来调优的那些指标。

自测

习题

先作答,再看解析。答错比答对更有价值,因为解析会指出你该回头重读哪一部分。

自测 1 题 / 共 6

为什么有了分块 prefill,还是需要分离部署?

得分 0/6