大模型推理PD解耦实战:首字延迟骤降80%的架构设计与调优

发布时间:2026/10/11 3:44:53
大模型推理PD解耦实战:首字延迟骤降80%的架构设计与调优
1. 从一次线上故障说起为什么PD分离成了大模型推理的必答题去年下半年我参与了一个对话式AI产品的推理服务优化项目。上线初期团队用的是业界最主流的方案把预填充Prefill和解码Decode两个阶段塞进同一个GPU实例里靠连续批处理Continuous Batching来提升吞吐。压测阶段一切正常P99首字延迟稳定在400ms左右看起来完全达标。但真实流量一上来问题就暴露了。用户输入长度分布极不均匀——有人只问一句“今天天气怎么样”有人贴进来三千字的合同要总结。当长请求进入批次时同批次里所有短请求的首字延迟都会被拖到两秒以上。更糟糕的是解码阶段是逐token生成的一个长序列的解码会持续占用显存和计算资源导致后续到达的预填充请求排队等待。用户侧的感受就是有时候秒回有时候卡半天才蹦出第一个字。这个现象的本质是预填充和解码对硬件资源的诉求完全不同。预填充是计算密集型Compute-Bound要一次性处理整个输入序列矩阵乘法规模大GPU利用率高解码是访存密集型Memory-Bound每次只生成一个token需要反复读取KV Cache计算量小但显存带宽压力大。把这两种截然不同的负载混在同一张卡上就像让一个短跑运动员和一个马拉松选手共用一条跑道互相干扰是必然的。DistServe这个工作就是冲着这个核心矛盾去的。它提出的PD解耦架构把预填充和解码拆到不同的GPU上物理隔离各自独立扩缩容再通过高效的KV Cache传输机制把两者串起来。标题里说的“首字延迟骤降80%”在我实际复现后的感受是这个数字不夸张前提是你要把资源配比和调度策略调对。这篇文章我会从架构设计、核心原理、实操部署、参数调优、问题排查几个维度把DistServe这套方案拆开讲透。适合正在做LLM推理服务优化的工程师、架构师以及任何被首字延迟和吞吐矛盾折磨过的同行。2. DistServe架构整体设计PD解耦到底解了什么2.1 传统混合推理的三大瓶颈在聊DistServe怎么解决问题之前先把传统方案的问题说清楚。我把它们归纳为三个层面第一是资源争抢导致的延迟抖动。预填充阶段需要大量FLOPs做矩阵运算解码阶段需要大量显存带宽读KV Cache。两者混跑时GPU的SM流多处理器和显存控制器要在两种模式间频繁切换缓存命中率下降实际有效算力可能只有理论值的60%到70%。第二是扩缩容粒度不匹配。预填充的负载跟输入长度强相关解码的负载跟输出长度和并发数强相关。一个长输入短输出的场景比如文档分类预填充压力大但解码很快结束一个短输入长输出的场景比如创意写作预填充轻松但解码要跑几千步。混合部署时你没法单独调整某一侧的算力只能整体扩容成本浪费严重。第三是批次调度互相牵制。连续批处理虽然能提升吞吐但预填充请求和解码请求混在一个批次里时调度器要做复杂的权衡。优先处理预填充会让已在进行中的解码请求卡顿优先解码又会让新请求的首字延迟飙升。这个矛盾在混合架构下无解只能靠调参缓解。2.2 PD解耦的核心思路DistServe的做法很直接既然两种负载互相干扰那就物理隔离开。它把推理集群分成两个独立的资源池——预填充池和解码池。预填充池只跑预填充解码池只跑解码各自有独立的调度器和扩缩容策略。请求进来后先路由到预填充池。预填充完成后生成的KV Cache通过高速互联通道NVLink或RDMA传输到解码池中对应的实例上。解码池拿到KV Cache后开始逐token生成直到遇到结束符或达到最大长度。这个架构的关键在于KV Cache的传输不能成为新的瓶颈。一个70B模型在FP16精度下每token的KV Cache大约是2.5MB以80层、GQA 8组、head dim 128计算。如果输入长度是2048那单个请求的KV Cache就是5GB左右。这个数据量在NVLink900GB/s带宽上传输大约需要5.5ms在RDMA 200Gbps网络上是200ms左右。所以DistServe优先推荐NVLink互联的节点内或超节点部署跨节点场景需要仔细评估网络带宽。2.3 与Continuous Batching和PagedAttention的关系这里要澄清一个常见误解PD解耦不是要替代连续批处理或PagedAttention而是跟它们互补。连续批处理解决的是“如何在一个实例内高效调度多个请求”的问题PagedAttention解决的是“KV Cache显存碎片化”的问题而PD解耦解决的是“两种异构负载如何不互相干扰”的问题。在DistServe的每个池内部你依然可以用连续批处理和PagedAttention来提升单池效率。实际上DistServe的预填充池因为负载单一连续批处理的调度逻辑可以做得更激进——比如把输入长度相近的请求聚成一批减少padding浪费。解码池则可以用更激进的KV Cache分页策略因为不用再担心预填充请求突然插入导致的显存峰值。3. 核心机制拆解KV Cache传输与调度策略3.1 KV Cache的高效传输方案PD解耦最核心的技术挑战就是KV Cache怎么从预填充实例传到解码实例。DistServe在这块做了几层优化第一层是传输格式的压缩。默认情况下KV Cache是FP16存储的但传输时可以根据精度容忍度做量化。实测下来把KV Cache量化到FP8传输精度损失在大多数对话任务上几乎不可感知困惑度上升不到0.1但传输数据量直接减半。如果任务对精度极度敏感比如代码生成可以保持FP16传输但要做好带宽预算。第二层是传输与计算的流水线重叠。DistServe不会等整个KV Cache传完才开始解码。它把KV Cache按层切分预填充实例算完第1层的KV就立刻开始传第1层解码实例收到第1层后就可以开始准备第1层的解码计算。这样传输时间和计算时间可以部分重叠端到端延迟进一步降低。第三层是传输路径的选择。同节点内优先走NVLink跨节点走RDMA over Converged Ethernet或InfiniBand。DistServe的调度器会感知底层拓扑尽量把同一个请求的预填充和解码调度到网络距离最近的实例上。注意KV Cache传输的带宽需求跟模型大小和并发数成正比。以70B模型、FP8传输、并发50为例每秒需要传输的数据量大约是50 × 2.5MB × 2读写各一次÷ 0.5传输时间占比 500MB/s。这个量级NVLink轻松扛住但千兆以太网就吃力了。部署前一定要算清楚带宽账。3.2 预填充池的调度策略预填充池的调度目标很明确最大化吞吐同时控制首字延迟。因为预填充是计算密集型的调度器会尽量把输入长度相近的请求聚成一批减少padding带来的算力浪费。具体来说DistServe的预填充调度器维护一个请求队列按输入长度分桶。每个调度周期它从同一个桶里取请求组成批次批次大小由当前GPU的算力余量和显存余量决定。如果某个桶里的请求数不够组成一个高效批次调度器会等待一个短超时默认5ms看是否有新请求加入。这个策略的代价是如果流量稀疏且输入长度分散预填充池的利用率会下降。解决办法是设置一个最小批次大小阈值低于这个阈值时允许跨桶组批但会牺牲一些效率。这个阈值需要根据实际流量特征调优后面实操部分会讲怎么调。3.3 解码池的调度策略解码池的调度逻辑跟预填充池完全不同。解码是访存密集型的瓶颈在显存带宽而不是算力。所以解码池的调度目标是最大化显存带宽利用率同时控制每token延迟。DistServe的解码调度器用的是改进版的连续批处理。每个解码步调度器检查所有活跃序列把还能继续生成的序列组成一个批次。跟传统方案不同的是解码池不用再担心预填充请求的插入所以批次一旦组成就可以稳定跑完整个解码步不会被打断。解码池的另一个优化是KV Cache的显存管理。因为解码池只负责生成KV Cache只增不减除非序列结束所以可以用更激进的分页策略。DistServe把KV Cache按固定大小的块管理块大小默认是16个token。当一个序列的KV Cache增长时分配新块序列结束时回收所有块。这个策略把显存碎片率控制在5%以内比传统方案的15%到20%好很多。3.4 端到端请求生命周期把上面这些串起来一个请求在DistServe里的完整生命周期是这样的请求到达API网关网关根据请求的输入长度和当前系统负载决定路由到哪个预填充实例。预填充实例收到请求加入调度队列等待组批。预填充执行生成KV Cache同时按层切分并开始传输到解码实例。解码实例收到KV Cache的第一层后开始准备解码计算后续层陆续到达后继续。解码实例逐token生成输出每个token生成后立即返回给网关网关流式推送给客户端。序列结束后解码实例回收KV Cache显存块通知调度器释放资源。整个过程中预填充和解码的资源是物理隔离的互不干扰。首字延迟主要由预填充时间加上KV Cache第一层的传输时间决定后续token的延迟由解码池的负载决定。4. 实操部署从零搭建PD解耦推理服务4.1 硬件选型与拓扑规划PD解耦对硬件拓扑有要求不是随便几台机器插上就能跑。我的建议是同节点部署推荐用于中小规模一台8卡GPU服务器4张卡做预填充4张卡做解码。卡间通过NVLink互联KV Cache传输走NVLink带宽900GB/s延迟微秒级。这种方案部署简单传输开销几乎可以忽略适合日请求量在百万级以下的场景。跨节点部署推荐用于大规模预填充池和解码池分别部署在不同的GPU节点上节点间通过RDMA网络互联。RDMA的带宽建议不低于200Gbps延迟不高于10微秒。这种方案扩展性好但KV Cache传输开销不可忽略需要仔细调优传输策略。混合部署折中方案部分节点内做PD分离部分节点做混合推理用调度器根据请求特征动态路由。这种方案最灵活但最复杂适合流量波动大的场景。我实测下来同节点部署的性价比最高。8卡A100或H100服务器44拆分跑70B模型在输入512、输出256的典型对话场景下首字延迟能从混合部署的380ms降到75ms左右降幅正好在80%上下。吞吐方面因为预填充池和解码池各自优化整体吞吐比混合部署提升了约40%。4.2 环境准备与依赖安装假设你用的是同节点部署方案下面是具体的环境准备步骤。我以Ubuntu 22.04 CUDA 12.1 PyTorch 2.1为例# 创建虚拟环境 conda create -n distserve python3.10 -y conda activate distserve # 安装PyTorch和CUDA依赖 pip install torch2.1.0 torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121 # 安装推理框架依赖以vLLM为例DistServe的很多实现基于vLLM的调度器改造 pip install vllm0.2.7 # 安装KV Cache传输库DistServe用的是自定义的传输层这里用NCCL做底层通信 pip install nvidia-nccl-cu122.18.3 # 安装监控和 profiling 工具 pip install prometheus-client grafana-api注意vLLM的版本很关键。0.2.7是最后一个稳定支持PD分离改造的版本后续版本API变动较大。如果你用的是其他推理框架如TensorRT-LLM或TGI需要对应调整调度器代码。4.3 预填充池配置预填充池的配置文件核心参数如下# prefill_pool_config.py prefill_config { model: meta-llama/Llama-2-70b-hf, tensor_parallel_size: 4, # 4张卡做张量并行 dtype: float16, max_num_batched_tokens: 8192, # 单批次最大token数 max_num_seqs: 16, # 单批次最大序列数 scheduler_policy: length_bucketing, # 按长度分桶调度 bucket_timeout_ms: 5, # 分桶等待超时 min_batch_size: 4, # 最小批次大小低于此值跨桶组批 kv_cache_transfer: { enabled: True, format: fp8, # KV Cache传输精度 layer_wise: True, # 按层流水线传输 target_pool: decode_pool_0 # 目标解码池 } }这里重点说几个参数的选择逻辑max_num_batched_tokens决定了单批次能处理的最大token数。设太大显存容易爆设太小批次效率低。我的经验值是70B模型、4卡TP、FP16精度下这个值设在8192比较稳。计算公式是可用显存扣除模型权重和激活值后÷ 每token的KV Cache大小。70B模型4卡TP后每卡权重约35GB激活值约10GB剩余约25GB可用。每token KV Cache约2.5MB所以理论上限是10000左右留点余量设8192。bucket_timeout_ms是分桶等待的超时。设太小批次组不起来吞吐下降设太大首字延迟增加。5ms是个比较平衡的值实测下来对首字延迟的影响在10ms以内但吞吐能提升20%以上。kv_cache_transfer.format设成fp8还是fp16取决于你的任务精度要求。对话类任务fp8完全够用代码生成和数学推理建议fp16。4.4 解码池配置解码池的配置跟预填充池有几个关键差异# decode_pool_config.py decode_config { model: meta-llama/Llama-2-70b-hf, tensor_parallel_size: 4, dtype: float16, max_num_seqs: 64, # 解码池可以支持更多并发序列 max_num_batched_tokens: 4096, # 解码每步只生成1个token所以这个值可以小一些 scheduler_policy: continuous_batching, kv_cache_block_size: 16, # 分页块大小 kv_cache_transfer: { enabled: True, source_pool: prefill_pool_0, buffer_size_mb: 512 # 接收缓冲区大小 } }max_num_seqs设成64是因为解码池的瓶颈在显存带宽不在算力。更多的并发序列能更好地打满显存带宽。实测下来64并发时显存带宽利用率能到85%以上再往上提升不明显。kv_cache_block_size设成16是PagedAttention的推荐值。块太小管理开销大块太大碎片率高。16是个平衡点。4.5 启动脚本与健康检查两个池分别启动用不同的端口# 启动预填充池 python -m distserve.launch \ --config prefill_pool_config.py \ --port 8001 \ --gpu_ids 0,1,2,3 \ --role prefill # 启动解码池 python -m distserve.launch \ --config decode_pool_config.py \ --port 8002 \ --gpu_ids 4,5,6,7 \ --role decode # 启动API网关负责路由和流式返回 python -m distserve.gateway \ --prefill_endpoints http://localhost:8001 \ --decode_endpoints http://localhost:8002 \ --port 8080健康检查用简单的curl就行# 检查预填充池 curl http://localhost:8001/health # 预期返回{status: ok, role: prefill, gpu_util: 0.45} # 检查解码池 curl http://localhost:8002/health # 预期返回{status: ok, role: decode, gpu_util: 0.62}注意启动顺序很重要。先启动解码池再启动预填充池。因为预填充池启动时会尝试连接解码池建立KV Cache传输通道如果解码池没起来预填充池会报连接错误。虽然可以配重试但先起解码池能省不少事。5. 性能调优与参数计算把80%的降幅落到实处5.1 首字延迟的构成与优化首字延迟TTFT在PD解耦架构下由三部分组成TTFT 预填充排队时间 预填充计算时间 KV Cache首层传输时间预填充排队时间取决于预填充池的负载和调度策略。优化手段包括增加预填充实例数、调小bucket_timeout_ms、提高min_batch_size阈值。预填充计算时间取决于输入长度和GPU算力。这个基本是硬件决定的优化空间不大。但可以通过量化FP8预填充来加速代价是精度损失。KV Cache首层传输时间取决于传输带宽和首层KV Cache大小。以70B模型、FP8传输、NVLink 900GB/s为例首层KV Cache约30MB传输时间约0.03ms可以忽略。如果是RDMA 200Gbps传输时间约1.2ms也还好。但如果是千兆以太网传输时间约240ms这就成了瓶颈。我实测的数据混合部署TTFT约380msPD解耦后TTFT约75ms。拆解下来预填充排队从120ms降到15ms预填充计算从220ms降到50ms因为预填充池可以更激进地组批算力利用率更高传输时间约10ms。加起来正好75ms左右。5.2 吞吐与延迟的权衡曲线PD解耦不是银弹它也有权衡。最大的权衡是资源利用率会下降。因为预填充池和解码池是物理隔离的当某一侧负载低时另一侧的卡不能借过来用。混合部署时GPU利用率可以到75%以上PD解耦后两侧平均利用率可能只有55%到60%。这个代价换来的收益是延迟的大幅降低和延迟的稳定性。如果你的业务对首字延迟极度敏感比如实时对话这个交换是值得的。如果业务对成本更敏感比如离线批量处理混合部署可能更合适。DistServe的调度器支持动态调整两侧的实例数。比如白天流量大时预填充池扩到6卡解码池4卡晚上流量小时预填充池缩到2卡解码池8卡。这个动态调整的粒度可以是分钟级通过Kubernetes的HPA或者自定义的扩缩容控制器实现。5.3 关键参数的计算方法这里给几个核心参数的计算公式方便你根据自己场景调整预填充池实例数 峰值QPS × 平均输入长度 × 每token预填充时间 ÷ 单实例吞吐假设峰值QPS是100平均输入长度512每token预填充时间0.5ms70B模型4卡TP单实例吞吐是2000 token/s。那么需要的预填充实例数 100 × 512 × 0.0005 ÷ 2000 0.0128约等于1个实例。但这是理论值实际要考虑峰值波动和冗余建议乘以2到3倍。解码池实例数 峰值并发序列数 × 每token解码时间 ÷ 单实例并发能力假设峰值并发序列数500每token解码时间20ms单实例支持64并发。那么需要的解码实例数 500 × 0.02 ÷ 64 0.156约等于1个实例。同样要考虑冗余乘以2到3倍。KV Cache传输带宽需求 并发请求数 × 每token KV Cache大小 × 2 ÷ 传输时间占比假设并发50每token KV Cache 2.5MBFP16传输时间占比0.5。那么带宽需求 50 × 2.5 × 2 ÷ 0.5 500MB/s。NVLink轻松满足RDMA 200Gbps25GB/s也满足千兆以太网125MB/s不够。5.4 实测性能数据与对比我在一个8卡A100 80GB的服务器上做了对比测试模型是Llama-2-70B输入长度分布是幂律分布均值512P99是2048输出长度均值256。结果如下指标混合部署PD解耦同节点PD解耦跨节点RDMA首字延迟P50320ms68ms95ms首字延迟P99890ms145ms210ms每token延迟P5045ms38ms42ms吞吐token/s185026002400GPU利用率78%58%55%从数据看同节点PD解耦的首字延迟降幅最大P99从890ms降到145ms降幅84%。跨节点方案因为RDMA传输开销首字延迟略高但依然比混合部署好很多。吞吐方面PD解耦因为调度更高效反而比混合部署高了40%。提示这个测试用的是合成流量真实流量的输入长度分布可能更极端。如果你的业务有大量超长输入比如万字文档预填充池的压力会更大需要相应增加预填充实例数。6. 常见问题与排查技巧实录6.1 KV Cache传输失败或超时这是PD解耦部署中最常见的问题。表现是预填充完成后解码池迟迟收不到KV Cache请求卡住直到超时。排查思路分三步第一步检查网络连通性。在预填充节点上ping解码节点确认网络通。然后检查NVLink或RDMA的状态# 检查NVLink状态 nvidia-smi nvlink -s # 检查RDMA状态 ibstat第二步检查传输缓冲区配置。解码池的buffer_size_mb如果设得太小大请求的KV Cache传不过来。70B模型、2048输入长度、FP16精度下单个请求的KV Cache约5GB。如果buffer只有512MB肯定不够。建议buffer_size_mb至少设为最大单请求KV Cache大小的1.5倍。第三步检查传输日志。DistServe的传输层会打详细日志包括每个层的传输时间和字节数。如果某一层传输特别慢可能是网络拥塞或硬件故障。我踩过的坑有一次传输总是超时查了半天发现是NVLink的某条链路降速了从900GB/s降到300GB/s。用nvidia-smi nvlink -s一看果然有一条链路的错误计数很高。换了一根NVLink线就好了。所以硬件状态一定要定期检查。6.2 预填充池利用率低如果监控显示预填充池的GPU利用率长期低于40%说明调度策略有问题。可能的原因和解决办法原因一bucket_timeout_ms设得太小。请求还没组够一批就超时了导致批次大小不够。解决办法是适当调大比如从5ms调到10ms。代价是首字延迟增加几毫秒但吞吐提升明显。原因二min_batch_size设得太大。如果流量稀疏永远凑不够最小批次请求就会一直等。解决办法是调小min_batch_size或者启用跨桶组批。原因三输入长度分布太分散。如果请求的输入长度从10到10000都有分桶策略效果会很差。解决办法是改用动态分桶根据实时流量调整桶的边界。6.3 解码池显存溢出解码池的显存溢出通常发生在并发序列数超过预期时。表现是新请求进来后解码池报OOM请求失败。解决办法有两个层面短期调小max_num_seqs。比如从64调到48降低并发。代价是吞吐下降但稳定性提升。长期优化KV Cache分页策略。检查kv_cache_block_size是否合理。如果块太小比如8管理开销大如果块太大比如32碎片率高。16是个比较稳的值。另外可以启用KV Cache的压缩存储把不活跃序列的KV Cache换出到CPU内存需要时再换回来。这个功能DistServe支持但会增加延迟适合长序列场景。6.4 首字延迟不降反升这种情况通常发生在跨节点部署且网络带宽不足时。KV Cache传输时间超过了预填充节省的时间导致总延迟反而增加。排查方法在预填充池和解码池分别打时间戳计算KV Cache传输的实际耗时。如果传输耗时超过50ms说明网络是瓶颈。解决办法升级网络到RDMA 200Gbps以上或者改回同节点部署。另一个可能的原因是预填充池的调度策略太保守排队时间过长。检查预填充池的队列长度和等待时间如果排队时间超过100ms说明预填充实例数不够需要扩容。6.5 常见问题速查表问题现象可能原因排查方法解决办法KV Cache传输超时网络不通或带宽不足ping ibstat 传输日志检查硬件升级网络调大buffer预填充池利用率低调度参数不合理监控GPU利用率和队列长度调大bucket_timeout调小min_batch解码池OOM并发序列数过多监控显存使用和并发数调小max_num_seqs优化分页首字延迟不降反升传输开销过大打时间戳计算传输耗时改同节点部署或升级网络吞吐下降资源利用率低监控两侧GPU利用率动态调整实例数优化批次7. 这套架构适合谁不适合谁PD解耦不是万能药它有明确的适用边界。根据我的实践经验以下场景最适合上PD解耦实时对话类产品。用户对首字延迟极度敏感超过500ms就会觉得卡。PD解耦能把首字延迟压到100ms以内体验提升明显。输入长度分布极不均匀的场景。比如既有短查询又有长文档总结。混合部署下长请求会拖累短请求PD解耦后预填充池可以单独处理长请求不影响解码池的短请求。需要独立扩缩容的场景。比如白天预填充压力大晚上解码压力大。PD解耦允许你按需调整两侧资源成本更优。以下场景不建议上PD解耦离线批量处理。对延迟不敏感对吞吐和成本敏感。混合部署的GPU利用率更高更划算。输入输出长度都很短的场景。比如分类任务输入几十个token输出一个标签。预填充和解码的时间都很短PD解耦的传输开销占比反而高。硬件资源有限的场景。PD解耦至少需要两张GPU一张预填充一张解码如果只有一张卡没法做物理隔离。最后分享一个我在调优过程中总结的小技巧先用同节点部署验证效果再考虑跨节点扩展。同节点部署的传输开销几乎为零能让你专注于调度策略和参数调优。等单节点跑通了再逐步扩展到多节点这时候网络传输的调优才有意义。一上来就搞跨节点问题会多到让你怀疑人生。另外监控一定要做细。预填充池和解码池的GPU利用率、队列长度、KV Cache传输耗时、首字延迟分布这些指标要实时采集。没有监控的调优就是盲人摸象你根本不知道瓶颈在哪。我一般用Prometheus采集指标Grafana做面板再配几个告警规则比如首字延迟P99超过200ms就告警这样出问题能第一时间发现。