推理优化这两年成了大模型落地绕不开的话题,尤其是当你的服务从"能跑通"进入"要扛量"的阶段,Prefill 和 Decode 这两个阶段的资源争抢问题就会赤裸裸地摆在面前。PD 分离(Prefill-Decode Disaggregation)不是什么新概念,但真正把它讲清楚、讲透"为什么这么拆""拆完怎么接""哪些坑必须提前踩"的资料并不多。我最近在几个推理服务项目里反复折腾这套架构,从最初的"看起来很美"到中间的"性能反而下降",再到最后稳定跑起来,中间踩的坑足够写一篇长文。这篇就把 PD 分离的基础逻辑、核心机制、实操要点和我自己的经验教训一次性讲清楚,适合正在做推理服务优化、准备上 PD 分离或者已经上了但效果不达预期的同学参考。不管你是刚接触推理优化的新手,还是已经在调参一线的老手,应该都能从里面找到对自己有用的东西。
1. 为什么要把 Prefill 和 Decode 拆开看
1.1 两个阶段的本质差异
大模型推理的过程,说白了就是两件事:先把你的输入 prompt 吃进去算出第一波结果,然后一个 token 一个 token 地往外吐。前者叫 Prefill,后者叫 Decode。很多人一开始会觉得这不就是同一个模型的前后两步吗,为什么要拆?问题恰恰出在"同一个模型"这四个字上。
Prefill 阶段是一次性处理整个输入序列,所有 token 并行计算,矩阵乘法的规模大、并行度高,属于典型的计算密集型任务。GPU 的算力在这个阶段能被吃得很满,显存带宽反而没那么紧张。而 Decode 阶段每次只处理一个新 token,但需要读取之前所有 token 的 KV Cache,计算量小、访存量大,属于典型的访存密集型任务。这两个阶段的硬件需求方向完全相反:一个要算力,一个要带宽。
我打个比方,Prefill 像是餐厅后厨一次性接了个大单,所有厨师一起上,灶台火力全开;Decode 像是客人一道一道点菜,每道菜都要翻一遍之前的菜单记录,厨师大部分时间花在翻记录上而不是炒菜。你把这两种活儿混在一个厨房里干,必然互相干扰。
1.2 混部带来的真实问题
在实际服务中,Prefill 和 Decode 混在同一个 GPU 上跑,会出现几个很典型的问题。
第一个是长尾延迟。当一个长 prompt 的 Prefill 任务进来时,它会占用大量算力,导致正在进行的 Decode 任务被卡住。用户那边看到的就是"打字机突然卡了一下"。这个卡顿在交互式应用里非常致命,因为人对延迟的感知是非线性的,偶尔卡一下比整体慢一点更让人难受。
第二个是资源利用率上不去。因为两个阶段的资源需求不同,你没法针对性地优化。给 Prefill 配的算力在 Decode 阶段闲置,给 Decode 配的带宽在 Prefill 阶段用不上。整体 GPU 利用率可能只有 30% 到 40%,剩下的都浪费在等待和切换上。
第三个是批处理策略难做。Prefill 适合大 batch 提升吞吐,Decode 适合小 batch 降低延迟,两者的最优 batch size 往往差一个数量级。混在一起时,你只能取个折中值,结果两边都不讨好。
提示:如果你的服务 QPS 很低、延迟要求也不严格,混部其实够用,没必要为了架构而架构。PD 分离的收益在规模化场景下才明显。
1.3 分离之后能拿到什么
把两个阶段拆到不同的实例上,最直接的好处是各管各的。Prefill 实例可以专门堆算力,用大 batch 把吞吐拉满;Decode 实例可以专门优化访存,用小 batch 保证低延迟。两者通过 KV Cache 的传输来衔接。
实测下来,在合适的负载下,PD 分离能把整体吞吐提升 1.5 到 3 倍,同时把 P99 延迟压下来。但这个"合适"两个字很关键,后面会详细讲什么情况下收益最大、什么情况下反而亏。
2. KV Cache 的搬运才是 PD 分离的真正主角
2.1 KV Cache 到底是什么
要理解 PD 分离,必须先搞明白 KV Cache。Transformer 在生成每个 token 时,需要用到之前所有 token 的 Key 和 Value 向量。如果每次都重新算一遍,计算量会随序列长度平方增长,完全没法用。所以工程上会把已经算过的 K 和 V 缓存下来,Decode 时直接读取,这就是 KV Cache。
KV Cache 的大小可以粗略估算:2 × 层数 × 注意力头数 × 头维度 × 序列长度 × 数据类型字节数。以一个 70B 级别的模型为例,单条 4K 长度的序列,KV Cache 可能就要几百 MB 甚至上 GB。这个数字直接决定了 PD 分离的传输成本。
2.2 传输为什么是瓶颈
PD 分离后,Prefill 实例算完 KV Cache,需要把它传给 Decode 实例。这个传输过程有几个特点让它特别容易成为瓶颈。
首先是数据量大。上面算过,单条序列的 KV Cache 就是几百 MB 级别,如果并发几十条,瞬间就是几十 GB 的传输量。其次是延迟敏感。Decode 实例必须等 KV Cache 到齐才能开始生成第一个 token,传输慢一点,首 token 延迟就高一点。最后是传输频率高。每个请求都要传一次,不是一次性的。
所以 PD 分离架构里,KV Cache 的传输方案设计得好不好,直接决定了整套系统能不能用。这也是为什么很多团队兴冲冲上了 PD 分离,结果发现性能还不如混部——传输开销把分离带来的收益全吃掉了。
2.3 几种传输方案的取舍
目前主流的 KV Cache 传输方案有这么几种,各有适用场景。
| 方案 | 原理 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 网络传输 | 通过 RDMA 或 TCP 在节点间传 | 灵活,支持跨节点 | 带宽受限,延迟较高 | 大规模分布式部署 |
| 共享存储 | 写入共享内存或分布式存储 | 解耦彻底 | 读写延迟大 | 对延迟不敏感的场景 |
| 同机共享 | 同节点内通过共享内存传 | 延迟极低 | 无法跨节点 | 单机多卡场景 |
| 计算换传输 | Decode 端重算部分 KV | 省传输 | 浪费算力 | 传输成本极高的场景 |
我自己的经验是,同机共享内存是延迟最低的方案,如果 Prefill 和 Decode 能部署在同一台机器的不同 GPU 上,优先考虑这个。跨节点的话,RDMA 是标配,但要注意网卡带宽和拓扑,别让流量绕远路。
注意:传输方案的选择要和你的部署规模匹配。小规模部署硬上 RDMA 是浪费,大规模部署用 TCP 会拖垮性能。
3. 调度策略决定了 PD 分离的上限
3.1 请求怎么分配
PD 分离后,一个请求要经过 Prefill 实例和 Decode 实例两道手,调度器需要决定:这个请求的 Prefill 交给哪个实例,算完的 KV Cache 又该送到哪个 Decode 实例。
最简单的做法是轮询,请求依次分给各个 Prefill 实例,KV Cache 再依次分给各个 Decode 实例。这个方案实现简单,但在负载不均时会出问题——某个 Prefill 实例可能因为处理了长 prompt 而变慢,后续请求还往它身上堆,雪上加霜。
好一点的做法是基于负载的调度,调度器实时感知各实例的队列长度和资源占用,把新请求分给最闲的实例。这个方案需要维护全局状态,实现复杂度上来了,但效果明显更好。
再进一步是亲和性调度,尽量让同一个请求的 Prefill 和 Decode 落在网络距离近的实例上,减少 KV Cache 传输开销。这个在跨节点部署时特别重要。
3.2 Prefill 和 Decode 的配比
一个经常被问到的问题是:Prefill 实例和 Decode 实例该按什么比例配?这个没有标准答案,取决于你的负载特征。
如果输入普遍很长、输出很短(比如文档问答、摘要生成),Prefill 的压力大,需要多配 Prefill 实例。如果输入短、输出长(比如创意写作、对话),Decode 的压力大,需要多配 Decode 实例。实际生产中,负载是动态变化的,所以理想情况下配比也应该能动态调整。
我见过一些团队用固定配比,结果白天和晚上的负载特征不一样,白天 Prefill 排队、Decode 闲置,晚上反过来。这种时候要么做弹性伸缩,要么至少准备两套配比按时间段切换。
3.3 批处理的时机把握
Prefill 端适合攒大 batch,但攒 batch 意味着等待,等待就意味着延迟。这里有个权衡:batch 越大吞吐越高,但首 token 延迟也越高。
我的做法是设置一个最大等待时间,比如 50ms,在这个时间内尽量攒 batch,超时了就立即发车。这样既保证了吞吐,又给延迟设了个上限。Decode 端则相反,batch 要小,甚至可以考虑 continuous batching,让新请求随时插入,不用等当前 batch 结束。
4. 动手搭一套最小可用的 PD 分离
4.1 环境准备与组件选型
要搭一套能跑的 PD 分离,你需要这么几个组件:推理引擎(vLLM、TensorRT-LLM、SGLang 都支持 PD 分离)、KV Cache 传输层、调度器。
推理引擎我推荐从 vLLM 入手,它的 PD 分离支持比较成熟,社区文档也全。传输层如果同机部署,直接用共享内存;跨节点的话,vLLM 支持通过 NCCL 或 RDMA 传输。调度器可以自己写个简单的,也可以直接用引擎自带的。
环境上,确保你的 GPU 驱动、CUDA 版本、NCCL 版本匹配,这几个版本不匹配是新手最容易踩的坑。我建议先用官方推荐的版本组合跑通,再考虑升级。
4.2 配置的关键参数
以 vLLM 为例,PD 分离的核心配置大概长这样:
# Prefill 实例 python -m vllm.entrypoints.openai.api_server \ --model /path/to/model \ --port 8000 \ --kv-transfer-config '{"kv_connector":"PyNcclConnector","kv_role":"kv_producer","kv_rank":0,"kv_parallel_size":2}' # Decode 实例 python -m vllm.entrypoints.openai.api_server \ --model /path/to/model \ --port 8001 \ --kv-transfer-config '{"kv_connector":"PyNcclConnector","kv_role":"kv_consumer","kv_rank":1,"kv_parallel_size":2}'几个关键参数要理解清楚:kv_role区分生产者和消费者,kv_rank和kv_parallel_size决定传输拓扑。这些参数配错了,实例之间连不上,请求会一直卡着。
4.3 跑通第一个请求
配置好之后,先别急着压测,用单个请求验证链路是否通。发一个简单的请求到 Prefill 实例,观察日志里 KV Cache 是否成功传输到 Decode 实例,Decode 是否正常生成。
这一步最容易出问题的地方是网络配置。如果 Prefill 和 Decode 在不同节点,确保它们之间的端口是通的,防火墙没拦。我遇到过折腾半天发现是安全组没开端口的情况,白白浪费一下午。
跑通单请求后,逐步增加并发,观察延迟和吞吐的变化曲线。如果并发一上来性能就崩,多半是传输层或者调度出了问题。
5. 那些让我熬夜的坑
5.1 传输带宽被吃满
第一次上 PD 分离时,我按理论值算了带宽需求,觉得千兆网够用。结果一压测,KV Cache 传输直接把网卡打满,Decode 端等数据等到超时。
后来才想明白,理论值算的是平均带宽,但实际传输是突发的。Prefill 算完一批 KV Cache,会瞬间产生一个传输高峰,这个峰值带宽可能是平均值的几倍。所以网络容量要按峰值留余量,至少留 2 到 3 倍。
5.2 显存碎片导致 OOM
KV Cache 在 Decode 端需要显存来存放,如果显存管理没做好,跑一段时间就会出现碎片,明明总显存够用,却分配不出连续空间,直接 OOM。
解决办法是用分页显存管理,把 KV Cache 切成固定大小的块,按块分配,避免大块连续分配。vLLM 的 PagedAttention 就是干这个的,一定要开启。
5.3 调度器的状态不一致
分布式调度器最容易出的问题是状态不一致:调度器以为某个实例空闲,实际它已经排队了。结果请求分过去,延迟飙升。
根因通常是状态同步有延迟。解决办法是缩短心跳间隔,同时在调度时加一层"乐观锁"——分配前再确认一次实例状态。这个额外确认会增加一点开销,但比请求分错地方强。
5.4 长 prompt 的特殊处理
超长 prompt(比如 32K 以上)的 Prefill 会占用大量资源,如果和普通请求混在一起调度,会把普通请求全堵住。
我的做法是给长 prompt 单独开一条队列,用专门的 Prefill 实例处理,和普通请求隔离。这样长 prompt 慢就慢,不影响其他用户。
6. 什么情况下 PD 分离反而拖后腿
6.1 低负载场景
如果你的服务 QPS 很低,GPU 本来就闲着,PD 分离带来的传输开销和调度复杂度纯属负担。这种场景下混部简单直接,性能也够用。
判断标准很简单:如果混部时 GPU 利用率长期低于 50%,说明负载不够,没必要分离。
6.2 短序列场景
如果输入输出都很短(比如分类任务、短问答),KV Cache 本身就很小,传输开销占比低,分离的收益不明显,反而增加了架构复杂度。
6.3 网络条件差
如果 Prefill 和 Decode 之间的网络带宽有限或者延迟高,KV Cache 传输会成为瓶颈,分离后的性能可能还不如混部。这种情况下要么改善网络,要么放弃分离。
提示:上 PD 分离前,先用小规模流量做 A/B 测试,对比分离前后的吞吐和延迟。数据说话,别凭感觉。
7. 一些实战中的调优心得
7.1 监控要到位
PD 分离后,链路变长了,出问题的环节也多了。必须把每个环节的指标都监控起来:Prefill 的排队时间、计算时间、KV Cache 传输时间、Decode 的等待时间、生成时间。哪个环节是瓶颈,一眼就能看出来。
我习惯用一张链路耗时分解图,把每个请求的各阶段耗时画出来,瓶颈一目了然。这个图在排查问题时特别有用。
7.2 压测要贴近真实
压测时别只用固定长度的请求,真实负载的输入输出长度是变化的。用真实流量的分布来压测,才能暴露真实问题。我一般会从生产环境采样一批请求,脱敏后作为压测集。
7.3 灰度上线
PD 分离是个架构级改动,别想着一次性全量切换。先灰度一小部分流量,观察稳定性和性能,确认没问题再逐步放量。灰度期间保留混部通道,出问题能快速回滚。
7.4 参数调优的顺序
调优要有顺序,别东一榔头西一棒子。我的顺序是:先调传输层(确保不是瓶颈),再调调度策略(让负载均衡),最后调批处理参数(优化吞吐和延迟的平衡)。顺序错了,可能在一个不是瓶颈的地方使劲,白费功夫。
8. 关于 PD 分离未来的一些判断
从目前的技术演进看,PD 分离正在从"可选优化"变成"标配架构"。越来越多的推理引擎原生支持,传输方案也越来越成熟。但它的复杂度是实打实的,不是所有团队都需要。
我的判断是,当你的推理服务达到一定规模——比如需要多卡多节点、对延迟有明确 SLA、GPU 利用率需要压榨到 60% 以上——PD 分离就是值得投入的。规模不到这个程度,先把混部调优做好,收益可能更直接。
另外,KV Cache 的压缩和量化技术也在发展,未来 KV Cache 变小了,传输压力也会小,PD 分离的门槛会进一步降低。但那是后话,眼下还是得老老实实把传输和调度这两块啃下来。
我在实际项目里最大的体会是:PD 分离的难点不在"拆",而在"接"。拆开两个阶段是明面上的事,真正花时间的是把 KV Cache 的传输、调度器的状态同步、显存的管理这些衔接环节做扎实。这些环节任何一个出问题,整套架构的性能就上不去。所以如果你准备上 PD 分离,把至少一半的精力放在衔接环节的设计和验证上,别只盯着 Prefill 和 Decode 各自的优化。