2026/10/9 1:55:52

vLLM Prefill/Decode分离架构实战:从拆解到部署调参

vLLM Prefill/Decode分离架构实战:从拆解到部署调参 1. 先想清楚为什么要把 Prefill 和 Decode 拆开大概半年前我把手上一套对外提供服务的 vLLM 推理集群从“一台 GPU 干到底”改成了 Prefill/Decode 分离顺带把 API 前端和调度器挪到一台纯 CPU 的机器上。当时团队里还有人觉得这纯属折腾等跑过一轮压测、经历过几次长尾高负载之后大家才承认这套拆法是值得的。今天这篇就围绕 vLLM 0.30 这一代版本把我从架构拆解到部署落地、再到调参排障的完整经验整理出来。先说一个最基本的认知一次 LLM 推理请求看起来是连续吐字实际上内部是两个性质完全不同的计算阶段。第一个阶段叫 Prefill要把用户整段 Prompt 一次性算完产出 KV cache第二个阶段叫 Decode从第一个 token 开始逐个生成每次只往前走一步。前者是典型的计算密集后者是典型的内存带宽密集。这俩阶段挤在同一块 GPU 上跑天然就会互相干扰尤其是当某些请求的 Prompt 特别长、有些请求的输出又要连续生成几百个 token 时两者都在争同一批算力和同一批显存带宽谁都跑不痛快。我更愿意用一个生活化的比喻来理解它Prefill 像是餐厅后厨一次性炒一大锅菜火力越猛上菜越快Decode 则是服务员一盘一盘往外端端菜速度不取决于炒菜技能而取决于传菜通道有多宽、桌子摆了几张。以前所有环节共用同一个后厨和同一批服务员一个长 Prompt 进来相当于一锅菜把灶台火全占了后面几百桌客人都得等着。Prefill/Decode 分离就是把炒菜和端菜分成两条线灶台归灶台传菜通道归传菜通道谁也别抢谁的资源。1.1 一次推理请求里两个阶段完全不是一回事从计算特点上看Prefill 阶段要对整段输入序列做一次完整的前向计算。假设模型是 72B 参数处理一个 token 所需的计算量大约在 6×72B 这个量级也就是几百 GFLOPs 起步。这意味着 Prompt 越长Prefill 需要吃掉的计算量就越大而且是一次性集中爆发出来的。它最关心的指标是 TTFT也就是用户发出请求后到第一个 token 出现的时间。Decode 阶段则完全不同。每生成一个 token模型都要把之前所有位置的 KV cache 重新读一遍输出长度越长KV cache 就越大读内存的时间占比就越高。到了大上下文场景decode 的瓶颈往往不在算力而在显存带宽。它最关心的指标是 TPOT也就是每生成一个 token 的平均时间。你可以把 KV cache 想象成一本越来越厚的笔记每写一个新字都要从头把笔记翻一遍。所以要提升 decode 吞吐最该做的是提高显存带宽利用率、增大 batch 里的并发请求数而不是单纯堆算力。一旦这两个阶段混在同一批 worker 上调度器就必须做一个很难的取舍。把 Prefill 批次排得太大TTFT 会飙升把 Decode 的 batch 塞得太满又可能挤占显存导致 Prefill 放不下。vLLM 老版本里那套“单实例全包”的路子本质上就是在两个目标之间不断做折中无论怎么调总有一部分请求在吃亏。1.2 混合跑时浪费在哪里我最直观的感受来自一次线上故障。当时有个模型服务开了--max-num-seqs偏大decode 阶段能容纳更多并发请求但碰上连续几个 8K 长 Prompt 进来PreFill 瞬间占满了所有可用的算力整卡温度的波动都看得见。结果就是短 Prompt 用户的 TTFT 从 300ms 涨到 2 秒多而 decode 阶段的 GPU 利用率反而没上去多少因为 KV cache 被 prefill 的中间结果挤掉了。混合负载造成的浪费主要有三种计算阶段互抢prefill 的突发计算会打断 decode 的稳定流水decode 又会在显存带宽上拖慢 prefill 的批处理速度。显存规划难统一prefill 阶段需要临时保存很大的中间状态decode 阶段需要尽量多腾空间做 KV cache显存分配怎么调都不够完美。扩缩容只能整机操作请求量起来了你没法只给长 Prompt 集中处理的部分加资源只能整台实例一起扩造成资源冗余。把两个阶段拆开之后这些问题变成了明确的资源边界。Prefill 节点专心处理那些“一次性高计算量”的请求Decode 节点专心处理“连续低计算量高带宽”的请求显存、batch、带宽都能分别按自己的特点去优化。1.3 vLLM 0.30 为这套玩法提供了什么很多人听到“Prefill/Decode 分离”会下意识觉得这是大厂的私有架构其实从 vLLM 0.30 这一代版本开始社区已经把很多关键能力下放到通用版本里了。一个典型标志是分布式执行不再局限于 Ray 集群里的数据并行而是把调度器和执行 worker 的角色做了更细的切分另一个能力是 KV cache 可以通过网络在节点间转移让 prefill 算出来的结果直接交给 decode 节点继续用。vLLM 这版里的 API 服务也不再强制要求“服务进程必须和 GPU 进程在同一台机器”无 GPU 前端的概念因此变得可行。API 层、路由层、调度层可以部署在纯 CPU 的机器上GPU 只负责真正的模型计算。对于已经从单机服务往多机集群迁移的团队这个变化省下来的不仅是 GPU 卡还有一整套运维上的灵活性。2. 无 GPU 前端 PD 分离的架构请求是怎么流动的2.1 API 前端、调度器、Prefill、Decode 各自的角色我把这套架构里的角色分成四个理解清楚各自边界部署时就不会乱角色运行位置主要职责核心资源依赖API 前端无 GPU CPU 节点接收 HTTP 请求、鉴权、限流、组装元数据CPU、内存、网络Dispatcher 调度器无 GPU CPU 节点决定请求去 prefill 还是 decode、维护 worker 状态CPU、内存、网络Prefill WorkerGPU 节点处理 Prompt 预填充生成 KV cache高算力、较大显存Decode WorkerGPU 节点逐 token 生成维护 KV cache 长链高显存带宽、充足显存用户请求到达 API 前端后API 层只做轻量解析把请求变成内部结构体带着 request_id、prompt 内容、采样参数和优先级丢给 Dispatcher。Dispatcher 是整个拆分架构的核心它维护着一张“当前有哪些 worker、每个 worker 负载如何、显存剩余多少”的表然后把请求分配到合适的 prefill worker。Prefill worker 执行完自家那块计算后不急着生成 token而是把算好的 KV cache 传给后续的 decode worker。Decode worker 拿到 KV cache 后开始一段连续的生成循环每生成一个 token 都有结果回传直到碰到停止条件。这套链路里最容易忽略的是API 前端和 Dispatcher 所在的 CPU 节点并不加载模型权重也不做任何张量运算。它们维护的只是一堆队列、计数器和元数据。所以一台没有 GPU 的机器完全可以承担较大 QPS 的前端调度工作前提是你给它足够的 CPU 和网卡带宽。2.2 KV cache 的传递PD 分离最关键的一跳prefill 和 decode 拆开后KV cache 从 prefill 节点转移到 decode 节点是绕不开的一步。这一步如果做不好前面拆得再干净都没有意义。KV cache 的大小取决于 Prompt 长度、模型层数、注意力头数和显存块大小。拿 72B 模型举例一个 2K 长度的 Prompt 产生的 KV cache 可能就要占用几十 MB 到上百 MB 的显存如果网络带宽不够传输耗时反而会吃光拆分省下来的时间。所以我一般建议集群内部走 RDMA 或至少 25GbE 以上的高速网络条件允许的话直接把 prefill 和 decode 放在同一台物理机的不同 GPU 卡上甚至同一机架内把 KV cache 迁移延迟控制在毫秒级。vLLM 0.30 里做 KV cache 传输的配置思路本质上就是“先把 KV 序列化再通过高速通道搬运最后在 decode 节点重新分配显存块”。实际操作中很多团队发现瓶颈往往不是算力不够而是 KV cache 在节点间的搬运排队太严重。想避开传输瓶颈还有一个实用的小技巧不要等 prefill 完全算完才开始做 KV 转移而是支持流式传输。Prompt 很长时可以按 chunk 切分算好一部分就传一部分decode 节点边接收边准备。这样能极大压低端到端延迟跟以前单实例的 TTFT 差距会明显缩小。2.3 无 GPU 前端为什么能做到“零显存参与”有些朋友会问API 前端不加载模型那它到底在跑什么东西答案其实很简单它跑的是请求的 HTTP 协议处理、参数校验、负载均衡、请求队列和日志采集。这些东西在传统的 Web 后端里再常见不过用 CPU 跑完全够用跟 GPU 的显存没有任何关系。把前端挪到无 GPU 节点之后有几个非常实际的好处。第一GPU 集群升级时你可以先滚动更新前端节点旧版和新版平滑切换不再需要整个推理集群陪着重启。第二安全隔离更干净外部流量只打到 API 层不直接接触 GPU worker 的内部地址。第三扩缩容的维度更细流量涨了可以先扩无 GPU 前端节点模型真正吃紧时再扩 GPU worker两层的伸缩策略可以分开做。当然“无 GPU”不等于“低配”。API 前端节点要处理高并发连接CPU 内核数和网卡队列数必须给够。我在 pod 配置里见过很多次前端节点因为只有 2 核、单网卡结果在 QPS 只有几百的时候网络栈先崩掉GPU 集群却闲得发慌。无 GPU 前端对单核性能不敏感但对并发连接数和网络吞吐非常敏感这一点在资源规划时一定要重视。3. 部署实操动手搭一套可运行的分离集群3.1 规划拓扑先把每台机器的角色定死进入实际操作前我习惯先把部署拓扑图画在纸上。下面是我线上部署时常用的一套拓扑供参考节点 AAPI 前端 Dispatcher 调度器16 核 CPU、32GB 内存、25GbE 网卡无 GPU。节点 BPrefill Worker4 张 80GB 显存的 GPU主要做长 Prompt 的预填充计算。节点 CDecode Worker8 张 80GB 显存的 GPU负责生成阶段显存大部分留给 KV cache。网络节点 B 和节点 C 之间走高速内网最好支持 RDMA避免 KV cache 搬运变成瓶颈。如果你手里的 GPU 资源有限也可以把 prefill 和 decode 混在同一台物理机但通过不同的 CUDA device 做逻辑隔离。不过这样收益会打折扣因为你仍然要面对显存互相挤占的问题。最舒服的形态还是物理隔离哪怕 prefill 节点少一卡、decode 节点多一卡都能比混布跑得更稳。3.2 节点启动模板与关键参数下面这段命令是我实际部署时用的启动模板。需要提前说明的是vLLM 0.30 之后的各个小版本对角色参数命名改得比较频繁我这边已经做过脱敏重点看结构而不是死记参数名。每个节点先用自己对应的角色启动再把调度地址指到同一个 CPU 节点。# 节点 A无 GPU 前端 Dispatcher vllm serve \ --host 0.0.0.0 \ --port 8000 \ --dispatcher-address 10.0.0.10:8001 \ --role frontend \ --distributed-executor-backend ray \ --enable-chunked-prefill # 节点 BPrefill Worker vllm serve \ --host 0.0.0.0 \ --port 8002 \ --dispatcher-address 10.0.0.10:8001 \ --role prefill \ --model /models/Qwen2.5-72B-Instruct \ --tensor-parallel-size 4 \ --gpu-memory-utilization 0.9 \ --max-model-len 32768 \ --max-num-batched-tokens 4096 \ --enable-chunked-prefill # 节点 CDecode Worker vllm serve \ --host 0.0.0.0 \ --port 8003 \ --dispatcher-address 10.0.0.10:8001 \ --role decode \ --model /models/Qwen2.5-72B-Instruct \ --tensor-parallel-size 8 \ --gpu-memory-utilization 0.85 \ --max-model-len 32768 \ --max-num-seqs 1024这套模板里有几个参数值得专门解释。Prefill 节点的--gpu-memory-utilization可以拉得高一些我设到 0.9因为它需要足够的临时显存容纳大 batch 的中间结果Decode 节点则要留一部分显存余量给 KV cache 的动态增长设 0.85 比较稳妥。--max-num-batched-tokens控制 prefill 阶段一次最多处理的 token 数太大容易造成瞬时算力尖峰太小则浪费 GPUS 的并行能力需要反复试。还有一个很重要的点所有节点必须使用同一个模型路径和同一套分词器配置。如果 prefill 和 decode 的模型权重不一致哪怕差的只是一个小版本生成阶段都会出现概率分布错乱。我在部署时就因为 prefill 节点读了一份旧权重、decode 节点读了一份新权重导致输出内容明显变“怪”排查了好久。3.3 联调验证从一次请求看全链路节点都起来之后不要急着接真实流量先在 API 前端发一个测试请求观察请求是否走完“前端→Dispatcher→Prefill→Decode→返回”的完整链路。vLLM 0.30 的日志在这个阶段非常有用Prefill worker 会打出它接收到 prompt 的 token 数Decode worker 会打出每次生成 batch 的规模Dispatcher 会打印 route 决策。我习惯用 curl 做一次小并发验证curl -s http://10.0.0.10:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: /models/Qwen2.5-72B-Instruct, messages: [{role: user, content: 讲一个三句话的笑话}], max_tokens: 128 } | jq .choices[0].message.content关键看两件事。一是首 token 返回时间如果明显比单实例正常值慢优先排查 KV cache 传输链路二是输出是否连续稳定如果出现断断续续的现象很可能 decode worker 队列没建立好或者 Dispatcher 把一个请求的后续生成分给了多个不一致的 decode worker。出现这种情况我习惯把调度策略改成“同一个请求固定绑定同一个 decode worker 直到生成结束”可以避免很多难查的偶发问题。4. 调参与容量规划把分离的收益吃满4.1 请求特征决定拓扑先算再配上线前我强烈建议大家先做一次请求特征统计平均 Prompt 长度是多少平均输出长度是多少QPS 峰值是多少。这三个数直接决定了 prefill 和 decode 的资源比例。举个简单例子假设平均 Prompt 是 1200 token平均输出是 600 token峰值 QPS 是 500。那么 Prefill 每秒要处理的总 token 数是 1200×50060 万 tokenDecode 每秒要生成的总 token 数是 600×50030 万 token。单从 token 量看prefill 是 decode 的两倍但两者对资源的要求不同。prefill 更需要算力峰值decode 更需要高带宽和并发 batch。如果你的服务是短 Prompt、长输出居多比如聊天机器人decode 节点占比就该远大于 prefill如果是文档理解、RAG 场景长 Prompt 多prefill 节点就必须给足算力。这套拆分架构最不适合的负载是“短 Prompt 短输出、QPS 极低”的业务。这种场景下一次请求的 KV cache 很小prefill 和 decode 几乎瞬间就能完成中间任何一次网络跳转都是纯开销收益自然为负。我见过有人明明只是个人工具类的服务也要硬上 PD 分离结果延迟反而变差这就是没搞清楚适用边界。4.2 三个对性能影响最大的参数PD 分离后需要重点调的参数不止显存利用率下面三个在拆分场景里影响最大第一个是--max-num-batched-tokens。这个参数决定 prefill 节点一次最多处理多少 token直接影响 TTFT。我通常从 1024 开始压测后逐步往上加。如果 TTFT 普遍偏高先把这个值调小如果 GPU 算力明显没用满再逐步调大。它是一个典型的“尖峰平滑”开关调的目的不是让 GPU 永远满负荷而是让突发计算被切成可控的小块。第二个是--max-num-seqs。这个参数主要影响 decode 节点的并发能力。Decode 阶段的显存会被 KV cache 占掉大部分而一个并发序列就要对应一份 KV cache。--max-num-seqs调得越大吞吐越高但显存浪费也可能越大。在 decode 节点上我习惯给 KV cache 块预分配多一点同时留一定比例的空闲显存避免某些长输出请求突然把剩余序列空间占满。第三个是 KV cache 块大小。vLLM 默认的块大小一般在 16 token 一个块块越小显存碎片越少但 block 表管理和 KV cache 传输的粒度就越细。拆分集群里我建议把块大小控制在 8~32 之间。块太大长 Prompt 的 KV cache 传输会带很多空隙块太小调度器维护 block 表的 CPU 开销会明显上升。我们线上一般设 16长上下文模型的集群有时会改成 8。4.3 真实负载下的调参走向拿我线上的表现来说在没有做 PD 分离前单台 8 卡 A100 跑 72B 模型混合请求下 TTFT 经常被长 Prompt 拖到 1.5 秒以上。拆完之后prefill 和 decode 各跑各的短 Prompt 用户的 TTFT 稳定在 400ms 以内decode 的 TPOT 因为不再被 prefill 抢带宽也从 80ms 左右降到 50ms 上下。需要提醒的是这种收益并不是百分之百稳定。当总 QPS 远超容量的时候decode 节点会因为排队请求太多而进入过载状态TTFT 反而可能比混合部署更不稳定。所以拆分之后我更建议在 Dispatcher 层面加一层请求优先级和排队机制。简单场景下可以先把“TTFT 敏感类请求”和“普通生成类请求”做成两个队列前者优先分到 prefill 资源后者用更宽松的调度窗口。这个策略不需要改模型只需要在 API 前端多做一层分类标记带来的稳定性提升立竿见影。5. 踩坑记录与排查方式5.1 高频问题的现象、原因、解法PD 分离这套架构运行时间一长会遇到一些比较典型的问题我把高频的几个整理成一个速查表方便大家直接对着查现象可能原因排查思路与解法TTFT 比单实例还高KV cache 传输走的是普通以太网延迟过高检查节点间网络类型尽量走 RDMA缩短 prefill 与 decode 的物理距离Prefill 节点 GPU 利用率很高decode 节点却长期吃不满Dispatcher 的 prefill 调度窗口太小解码端经常空等调大 prefill 批大小或者增加并发请求数让 KV 传输流水线化偶发输出中断或明显卡顿同一个请求被分配给多个 decode workerKV cache 上下文没接上让 Dispatcher 为每个请求绑定固定 decode worker直到生成结束前端节点 CPU 100%GPU 节点却空转API 前端过载请求在 HTTP 层就排队了给前端加 CPU/网卡资源或在前面再加一层负载均衡显存报 OOM但实际显存使用率不高KV cache 块分配策略太保守碎块太多调小 KV block 大小或适当降低 gpu-memory-utilization 留出缓冲decode worker 出现莫名其妙的生成质量下降节点间模型权重版本不一致检查所有节点的模型路径、tokenizer 配置和版本 hash 是否一致5.2 上线前必做的检查清单最后整理一份检查清单建议每次发布这类集群前都过一遍。这几点都是我踩过的坑换来的。模型服务端口是否只绑定内部网段外部流量一定要经过无 GPU 前端不要直接暴露 GPU worker 地址。KV cache 传输链路是否具备高带宽和低延迟建议用ping、iperf或ib_write_bw先打一次底确认网络达标再压测。Dispatcher 的请求路由策略是否考虑了“长 Prompt 优先分给 prefill 节点短请求直接进 decode 缓存复用”的细节而不是无脑全量转发。所有节点的模型路径是否一致建议启动脚本里对模型的 SHA256 做一次校验。压测时要同时观察 TTFT、TPOT、端到端延迟和 KV cache 传输延迟任何一个指标异常都要能分开定位不要在混合指标里猜。GPU worker 的心跳超时和断开重连逻辑要提前验证避免前端升级导致 worker 被误判下线。最后补一个不是很容易想到的细节PD 分离后API 前端的健康检查接口非常关键。很多团队直接把“前端进程活着”当成节点健康但实际可能调度器已经连不上后端 worker照进来的流量全部排队超时。我会把健康检查做成两级第一级看前端进程存活第二级看 Dispatcher 到 Prefill、Decode 的连通状态和最近五分钟的成功调度率但凡调度成功率掉到 95% 以下前端节点就自动摘除。这套机制看起来简单真正上线后救过我好几次。从单个 vLLM 实例到把请求拆成 prefill 和 decode 两段再到把 API 前端从 GPU 节点上摘出去整个过程做下来最深的体会是这套架构不是炫技而是把“计算”和“流转”彻底解耦。只要你的业务请求特征不是极端单薄PD 分离加无 GPU 前端带来的稳定性、可维护性和资源利用率提升都是实实在在值的。希望这篇整理能帮你少走我走过的弯路。