
做推理优化做到一定阶段迟早会遇到一个诡异的现象GPU利用率看着挺高用户还是嫌慢明明吞吐也上去了个别请求的延迟却忽高忽低。如果你也在排查这类问题多半会撞上“PD分离”这个词——也就是 Prefill/Decode 分离。我写这篇东西是想把 PD 分离的来龙去脉、三种实现形态、KV Cache 传输的隐藏成本以及在实际部署里踩过的坑一次讲清楚。适合正在做推理服务优化、或者准备把单机服务改造成多机推理集群的工程师参考读完你至少能判断自己到底需不需要上这套东西。1. 先算算账Prefill 和 Decode 为什么注定要打架1.1 Prefill 是算力密集型一块 GPU 能被它“吃满”Prefill 阶段处理的是用户输入的 prompt。模型要一次性把整段 prompt 的所有 token 并行算一遍生成完整的 Key、Value填充 KV Cache。这个阶段的特点是token 多、批量大、矩阵乘法的计算量非常集中。以 Llama-3-8B 为例粗略估算模型每处理一个 tokenforward 计算量大约在 16 GFLOPs 量级约等于 2 倍参数量。一个 2048 token 的 promptprefill 的总计算量大概就是 2048 × 16 GFLOPs ≈ 33 TFLOPs。A100 80G 的 FP16 稠密算力在 312 TFLOPS 左右理论上一口气算完只要 0.1 秒左右实际因为 kernel launch、数据搬运、中间激活落显存等开销通常会到 0.2 到 0.3 秒但整体依然是把 GPU 的算力按满来跑。这就像让一个阅读者快速把一整页书扫完他一口气能吸收大量信息但这段过程里 CPU 也好 GPU 也好算力都被高度占用。prefill 是典型的 compute-bound 任务算力不够就是不够靠增加带宽解决不了问题。1.2 Decode 是访存密集型算力根本用不满Decode 阶段是逐 token 生成。每生成一个 token模型都只做一次“小步快跑”的计算根据当前已生成的 token取对应 embedding经过全部 transformer 层走一遍注意力最后输出下一个 token 的 logits。问题在于每一步计算量不大但你依然要把完整的模型权重从 HBM 搬进寄存器或片上缓存。还是以 Llama-3-8B 为例FP16 权重大约 16GBA100 的 HBM 带宽约 2TB/s光把权重过一遍的理论时间就到了 8ms 左右。所以 decode 阶段的算力利用率很低瓶颈在显存带宽是典型的 memory-bound 任务。这个阶段更像一个抄写员一次只能写一个字写这个字的时间有一大半花在“翻书找素材”上。你试图让抄写员更快加再多人帮忙算他手头的纸得先从柜子里拿出来效率照样上不去。1.3 混在一起的必然结果互相拖后腿P99 失控如果 prefill 和 decode 混在同一批、共用同一块 GPU矛盾马上就出来了。一个用户提交了 8K 的长 promptprefill 要狂吃算力可能持续几秒甚至更久期间其他用户正在 decode 的请求全部排队等待中的每个 token 都出不来。我曾经处理过一个线上案例单机部署、P50 延迟看着只有 200ms但 P99 能飙到 5 秒以上。查了半天罪魁祸首就是每过一阵子就有大 prompt 进来做 prefill算力被抢占后所有正在生成的请求全部被堵住。长尾延迟就是这么被拉出来的。混布还会带来第二个问题显存和 batch 调不动。decode 阶段为了吞吐希望 batch 尽可能大但 prefill 阶段一个长 prompt 就要占大量中间激活和 KV Cachebatch 一大显存直接爆。这不是一个参数能救回来的是任务特征本身就在互相消耗。2. PD 分离不是一种技术是一套解耦思路2.1 核心思想让计算密集型的活儿和访存密集型的活儿分家PD 分离的核心思路很简单把 prefill 和 decode 两个物理特征完全不同的任务拆开放到不同的资源上执行。这样 prefill 节点可以选高算力卡decode 节点可以选大显存、高带宽卡互不拖累还能各自扩缩容。这里要先澄清一个常见误区PD 分离不等于多机部署也不等于 Chunked Prefill。它是一个广义的解耦思路下面至少分化出三种形态实例级完全分离、同节点时间片交错、混合资源池动态调度。不同形态解决的痛点不同代价也不同。理解 PD 分离的关键在于它不是为了让每个请求更快而是为了让“最慢的那批请求”不再被“最大的那个请求”拖死。换句话说优化目标是 P99不是 P50。2.2 形态一实例级完全 PD 分离实例级完全分离是把 prefill 和 decode 部署成两套独立服务。用户请求先到达 Decode 服务Decode 服务如果没有对应的 KV Cache就把 prompt 转发给 Prefill 服务Prefill 服务算完 prompt 的 KV Cache 后通过网络把 KV Cache 传给 Decode 服务Decode 再开始逐 token 生成。这套方案的好处是隔离最彻底算力竞争被物理隔开长 prompt 再大也只影响 Prefill 节点不会拖累 Decode 节点。两套节点可以独立扩缩容读多场景给 Decode 多挂实例写多场景给 Prefill 加算力。缺点也很直接增加了网络传输和额外的一跳延迟KV Cache 的传输如果带宽不够整个链路反而更慢。另外小流量场景下多出一批 Prefill 节点资源开支明显上升单个请求的生成延迟也未必比混布更好。2.3 形态二Chunked Prefill 时间片交错Chunked Prefill 是我个人最常用的入门方案它不把任务分配到不同机器而是把 prefill 请求切成多个 chunk在同一个 GPU 上按时间片和 decode 请求交错执行。比如一个 2048 token 的 prompt如果 chunk size 设为 256prefill 就会被切成 8 个 chunk。每个 chunk 计算量小计算完一个 chunk 就把 GPU 让出来给 decode 跑一小段再继续下一个 chunk。这样即便有大 prefill 进来decode 的 token 也只是被短暂打断而不是被堵住几秒钟。vLLM 里实现 Chunked Prefill 的关键参数是max-num-batched-tokens它限制的是每个 batch 里参与 attention 的 token 总数。开启 chunked prefill 后长 prefill 会被切割到不超过这个数字的块里。实际调参时这个值我一般从 128 起试最长别超过 512具体原因后面实操部分细说。2.4 形态三混合资源池与动态调度第三种形态介于前两者之间本质是资源池化管理你有一批 GPU一部分偏算力、一部分偏显存带宽调度器根据请求特征动态决定去哪儿执行。简单做法是网关层做优先级分流小请求直接进 decode 优先队列大 prefill 请求进专门的 prefill 队列。更复杂的做法是像多家推理框架的“disaggregated serving”一样把节点编排成 P 池和 D 池再通过 KV Cache 亲和度调度让相同前缀的请求尽量命中同一个节点。这个形态的好处是资源利用率最高坏处是调度和网络组件的复杂度上了一个台阶。如果团队还在单机推理阶段我通常不建议直接上第三形态先从 chunked prefill 开始确认瓶颈点确实在 prefill/decode 互扰后再考虑投入做完全分离。2.5 三种形态的选型对比形态隔离程度额外成本典型场景实例级完全 PD 分离最强算力与访存互不影响显著增加机器与网络开销大并发、长 prompt 占比高、SLO 严格Chunked Prefill同节点时间片交错降低互扰几乎为零只需调参单机/少机部署入门首选混合资源池动态调度灵活可按负载动态配置调度器与网络复杂度较高多机集群、请求特征复杂我的判断标准很简单如果线上 P99 和 P50 差距极大先开 chunked prefill如果开了之后依然压不住 P99且请求量足够大再考虑实例级完全分离。不要一上来就堆机器很多系统的问题并不在多机而在调度。3. 实操把 Chunked Prefill 配置起来并验证效果3.1 怎么开启、调哪些参数以 vLLM 为例chunked prefill 通常不需要改代码启动服务时加开关、调参数就行。核心参数有两个--enable-chunked-prefill和--max-num-batched-tokens前者负责开启 chunked 逻辑后者负责控制 chunk 大小上限。不同版本里参数名可能略有差别有些版本默认开启有些版本需要显式设置所以配置前一定先看当前版本的文档。我个人建议不要偷懒每次调整参数后顺便看一眼启动日志里的配置确认是否生效这也是排查延迟问题时最容易被忽略的一步。除了这两个参数还需要配合调整max-num-seqs它控制的是单 batch 里最多同时处理多少个序列。chunk size 减小以后如果不限制序列数显存里的中间解耦结构可能会被塞爆。一般经验是max-num-batched-tokens从 256 开始max-num-seqs保持默认或略降一点先用压力测试跑一轮再根据 TTFT 和 TPOT 微调。3.2 两个关键指标TTFT 与 TPOT验证 PD 分离效果不能只看平均延迟。我建议盯住两个指标TTFTTime To First Token首 token 延迟和 TPOTTime Per Output Token每输出一个 token 的平均耗时。TTFT 反映的是 prefill 阶段和排队阶段的整体表现长 prompt 请求卡顿的时候TTFT 会明显拉高TPOT 反映的是 decode 阶段的稳定程度如果 decode 被 prefill 抢占TPOT 的波动会非常大。压测时至少记录 P50、P95、P99 三个分位点。只看 P50 会掩盖长尾问题只看平均值更是自欺欺人。我在本地压测时习惯输出如下信息指标分位点说明TTFTp50 / p99判断 prefill 阻塞程度TPOTp50 / p99判断 decode 被干扰程度端到端延迟p99综合判断用户体验3.3 我跑过的一组基准与参数选择心得我拿一台 8 卡 A100 做过测试模型是 Llama-3-8Bprompt 长度 2048输出长度 256并发 32 路。混合部署时 P99 TTFT 到了 2.4 秒TPOT 波动也很厉害开启 chunked prefill、chunk size 设为 256 后P99 TTFT 降到了 700ms 左右TPOT 的 P99 也稳了很多。后续我又把 chunk size 调到 512发现 TTFT 略有上升TPOT 反而变得更不稳原因是单个 chunk 还是太长decode 每轮等的时间变久了。调回 256 后整体最均衡。如果你的并发更高、prompt 更长可以试试把max-num-batched-tokens控制在 128 到 384 之间不要盲目追求单次算更多 token。还有一个很实用的小技巧观察 TTFT 和 TPOT 的比值。正常情况下对于 2K prompt、8B 模型TTFT 应该在 TPOT 的十几倍以内如果 TTFT 是 TPOT 的几十倍甚至上百倍说明 prefill 阶段的阻塞非常严重这正是需要进一步做 PD 分离的信号。4. 进阶实例级 PD 分离的真实成本——KV Cache 传输4.1 KV Cache 到底有多大先会算这笔账实例级 PD 分离有一个绕不开的硬成本KV Cache 从 Prefill 节点传到 Decode 节点。很多人低估了这笔开销我先给个公式你可以套自己的模型算单个 token 的 KV Cache 大小 2 × 层数 × KV 头数 × 头维度 × 精度字节数以 Llama-3-8B 为例32 层、8 个 KV 头、头维度 128、FP16 占 2 字节算下来每个 token 的 KV Cache 是 2 × 32 × 8 × 128 × 2 131072 字节正好 128KB。一个 2048 token 的 promptKV Cache 就是 256MB。如果并发 32 路光这批 KV Cache 就接近 8GB。别小看这个数。8B 模型权重才 16GBKV Cache 随并发和长度增长后很容易把显存吃穿。更关键的是256MB 的 KV Cache 要跨节点传。机房内网如果是 10Gbps传一次就是 200ms 以上100Gbps 网络也要 20ms对于 TTFT 本来就要求几百毫秒的服务来说这笔开销非常可观。4.2 为什么不能简单地把 KV 都放在 Decode 节点有人会问既然 KV Cache 是 decode 要用的直接在 decode 节点算不就行了那就回到了混合部署的起点。另一条思路是 Prefill 节点算完 KV Cache 后把完整 KV 长期缓存在本地decode 节点需要时再取。这个思路的问题在于显存隔离如果所有 KV Cache 都堆在 decode 节点decode 的显存很快就会被长 prompt 的大请求占满反而限制 decode 并发。所以实际部署里KV Cache 需要在节点间流动。要么按需传输要么做分区缓存。分区缓存又涉及调度同一前缀的请求要尽量打到一个节点否则每次都要重新传 KV网络损耗会被放大。4.3 降低传输开销的四个手段第一前缀缓存复用。如果很多请求共享同一段系统 prompt 或文档前缀Prefill 节点可以直接复用之前算好的 KV Cache不需重新计算也不需重新传输。这就是 Prefix Caching 的意义。vLLM 里开启后命中缓存时 TTFT 能降低一个数量级。第二KV Cache 量化。FP16 换成 FP8KV Cache 尺寸直接砍半传输量也砍半。代价是精度略降但很多场景下对生成质量影响很小值得做 A/B 对比测试。第三流式传输。Prefill 节点不需要等一整段 prompt 全部算完再传 KV它可以边算边推。decoder 节点边收边开始生成。这样网络延迟被计算时间隐藏掉一截启动延迟能明显下降。第四KV Cache 亲和调度。调度器尽量把相同前缀或相同用户的请求分配到同一个 decode 节点让复用概率最大化。这个方案在集群规模大时收益明显但实现复杂度也最高适合已经上了资源池的团队。4.4 一个人就能做的成本估算即使不上完整系统你也能先估算 PD 分离的收益。我把流程记在这里方便大家照抄统计线上请求的平均 prompt 长度P50、P95、P99按上面的公式算出平均 KV Cache 大小、P95 KV Cache 大小用你的机房内网带宽除以 KV 大小得到单次 KV 传输延迟再对比当前混布时的 P99 TTFT。如果混布 P99 TTFT 本身就比单次 KV 传输延迟高几倍PD 分离大概率有收益如果混布延迟已经很好纯粹为了“新架构”去做分离多半会亏。我当时就是这个方法线上 P99 TTFT 接近 2 秒KV 传输估算只有 30ms 左右差距巨大才下定决心推进分离。先把这笔账算清楚比自己拍脑袋上架构靠谱得多。5. 常见问题排查与踩坑实录5.1 P99 延迟还是高一套排查顺序如果你开了 chunked prefillP99 依然居高不下我的排查顺序是这样的先看 TTFT 高还是 TPOT 高。TTFT 高就往 prefill 侧看chunk size 是不是调太大、前缀缓存命中率是不是太低、排队请求是不是太多。TPOT 高则往往意味着 decode 节点自身算力或带宽不够甚至可能被某个异常长请求的资源占用拖住。另一个容易踩的坑是chunked prefill 开了但日志里根本没生效。很多框架的参数名在不同版本里直接变了或者要同时满足别的条件才会真正启用。我习惯在启动日志里搜索 “chunked” 和 “token” 相关字段确认实际生效再继续调参。5.2 显存不够用的处理思路显存不够时很多人的第一反应是调低gpu_memory_utilization把整体显存比例降下来。这个方向没问题但更值得先检查 KV Cache 限额是不是被长 prompt 地址打满了。你可以为不同长度请求设置不同的并发上限或者把 KV Cache 量化到 FP8通常能救回不少显存。如果显存实在紧张另一个选择是 offload 冷 KV Cache 到 CPU 内存或远端存储。代价是请求命中冷缓存时需要重新加载延迟会波动。我建议只在冷热请求比例很悬殊时才做 offload否则收益不划算。5.3 PD 分离会影响模型精度吗早点把这个疑问解决掉chunked prefill 在数学上不会改变模型生成结果。attention 计算中对同一个 query所有 key/value 的加权求和是确定的chunked 只是沿序列维度切分后用 online softmax 方式合并浮点累加顺序会有细微误差但对生成结果的影响可以忽略。实例级完全 PD 分离也只是把 prefill 和 decode 的执行位置分开模型权重和计算方式没变结果一致。要注意的是如果你同时开了 KV Cache 量化、权重量化那类技术那属于另外的精度话题和 PD 分离本身无关。排查问题时不要把两者混为一谈。5.4 什么时候别上 PD 分离最后说点反直觉的经验不是所有系统都适合 PD 分离。如果你服务的是内部低并发场景prompt 很短并发不超过 8那混布可能已经很好了强行分离只会增加机器成本、网络链路和运维复杂度。另外如果请求长度差异不大prefill 和 decode 的矛盾本身就小PD 分离带来的收益也很小。我见过一个团队花了三周做实例级分离最后发现他们 P99 高主要是有一路定时任务在凌晨发大量长 prompt错峰就能解决。分离翻倍了成本收益却没体现。所以我的底线建议是先做负载画像统计请求长度分布、到达速率、P99 与 P50 的比值再决定要不要做、做成哪种形态。很多问题动调度器就能解决动架构是最后的手段。聊到最后分享一下我的个人体会PD 分离本质上是资源编排问题不是单纯的技术升级。它把两个物理特征完全不同的计算任务拆开解决的是互扰而不是单个请求的速度极限。我自己最失误的一次就是没先算 KV Cache 传输开销就上了完整分离结果网络差点成为新瓶颈。如果让我重新来一遍我一定会先开 chunked prefill、把前缀缓存命中率调上去再评估是否值得走最后一步。最后送大家一个小技巧定期把 TTFT 和 TPOT 的 P99 比值画成曲线一旦发现比值持续拉大就说明 prefill/decode 互扰又回来了这时候再去检查配置和调度方向基本不会错。