ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

PD 分离推理架构详解(全网最全):从 prefill/decode 到 KV Cache 的配置骨架

PD 分离推理架构详解(全网最全):从 prefill/decode 到 KV Cache 的配置骨架 1. 为什么要把 prefill 和 decode 拆开跑如果你正在自建 LLM 推理服务大概率遇到过这种场景单卡上跑 vLLM短请求响应挺快一旦有长 prompt 混进来正在逐字输出的对话突然卡顿TPOT 从 40ms 飙到几百毫秒。这不是配置问题而是 prefill 和 decode 两个阶段被塞进同一个 batch 后互相干扰的必然结果。PD 分离Prefill-Decode Disaggregation推理架构要解决的就是这件事把计算密集的 prefill 阶段和内存密集的 decode 阶段放到不同的 GPU 实例上各自用最适合的并行策略和 batching 参数从而在满足 TTFT 和 TPOT 两个 SLO 的前提下把有效吞吐量Goodput拉起来。它适合谁适合已经跑通单机推理、开始遇到延迟抖动、准备做多实例部署的开发者也适合想理解 vLLM KV Connector 到底在传什么、怎么配的人。这篇不讲论文综述直接给一套可复制的配置骨架config.toml定义 prefill/decode 两个 worker 的角色与 KV 传输方式settings.json定义统一入口和 Key 通道再配合验证请求确认链路真的通了。KV Cache 怎么从 prefill 端传到 decode 端、TaoToken 的统一 Key/API 通道怎么接进来都会落到具体字段上。2. TaoToken 前置统一 Key 与 API 通道PD 分离之后你的服务会变成「一个入口 多个 worker」的结构。入口负责鉴权、路由、限流worker 只管算。这时候如果每个 worker 各自维护一套上游 Key运维会非常痛苦。我试过把上游调用统一收敛到 TaoToken 的 API 通道入口只认一个 Keyworker 侧不直接持有上游凭证改配置时只动一处。TaoToken 在这里扮演的是统一模型调用通道你拿一个 Key就能通过兼容接口访问多种模型入口层用它做上游转发prefill/decode worker 只处理本地 KV 逻辑。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 基址是 https://taotoken.net/api 注意 API 地址不带 UTM 参数配置里直接写这个。你需要先拿到 Key再去控制台确认配额和模型列表。这一步别跳过后面settings.json里的api_key字段就填它。获取 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite控制台https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite接入文档https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite注意Key 只放在入口服务的环境变量或settings.json里不要写进 worker 的启动命令避免日志泄露。3. 可复制的 config.toml 与 settings.json 骨架下面这套骨架假设你用 vLLM 作为推理引擎prefill 和 decode 各起一个实例KV 通过 NIXL 走 P2P 传输。字段名按 vLLM v1 的 KV Connector 约定来你可以按自己集群的网卡和显存调整。3.1 config.toml定义两个 worker 角色# config.toml —— PD 分离双 worker 骨架 [cluster] name pd-disagg-demo # 入口服务监听地址worker 不直接对外 gateway_host 0.0.0.0 gateway_port 8000 [kv_transfer] # 传输后端nixl 走高速互联shared_storage 走共享文件系统 backend nixl # 传输粒度request 请求级 / layer 层级 / block 块级 granularity layer # 单次传输超时毫秒长序列适当放大 timeout_ms 30000 [prefill] role prefill model Qwen/Qwen3-8B gpu_ids [0] # prefill 是 compute-boundbatch 不宜过大 max_num_batched_tokens 8192 max_num_seqs 8 # 张量并行度TTFT 严格时可调高 tensor_parallel_size 1 enable_chunked_prefill false [decode] role decode model Qwen/Qwen3-8B gpu_ids [1] # decode 是 memory-boundbatch 可以放大 max_num_batched_tokens 2048 max_num_seqs 64 tensor_parallel_size 1 # decode 端需要长期持有 KV cache gpu_memory_utilization 0.90 [connector] # vLLM KV Connector 类型 kv_connector NixlConnector kv_role kv_both # 生产端prefill保存消费端decode加载 kv_buffer_device cuda几个关键点解释一下。granularity layer表示每算完一层就异步把该层 KV 传过去和下一层计算重叠能压低 TTFT如果你的 prompt 普遍很短改成request更省事。max_num_batched_tokens在 prefill 端给大、decode 端给小是因为 prefill 吃算力、decode 吃显存带宽两者最优 batch 区间不一样。3.2 settings.json统一入口与 Key 通道{ gateway: { listen: 0.0.0.0:8000, upstream_base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, request_timeout_s: 120, max_retries: 2 }, routing: { prefill_endpoint: http://127.0.0.1:8100, decode_endpoint: http://127.0.0.1:8200, strategy: pd_disagg, kv_connector: NixlConnector }, slo: { ttft_ms: 400, tpot_ms: 40, p90: true }, logging: { level: info, log_kv_transfer: true } }api_key_env指向环境变量名而不是明文 Key启动前export TAOTOKEN_API_KEY你的Key即可。slo段不是装饰入口层可以据此做早期拒绝当预测 TTFT 或 TPOT 会超标时直接返回避免无效请求占着 decode 的 KV 空间。3.3 启动命令# 终端 1prefill worker export TAOTOKEN_API_KEY你的Key python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen3-8B \ --port 8100 \ --kv-transfer-config {kv_connector:NixlConnector,kv_role:kv_both} \ --max-num-batched-tokens 8192 # 终端 2decode worker python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen3-8B \ --port 8200 \ --kv-transfer-config {kv_connector:NixlConnector,kv_role:kv_both} \ --max-num-seqs 64 # 终端 3入口网关读取 settings.json python gateway.py --config settings.json4. 验证请求与预期返回链路搭好后别急着压测先用一个短请求确认 KV 真的从 prefill 传到了 decode。curl -s http://127.0.0.1:8000/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: Qwen/Qwen3-8B, messages: [{role: user, content: 用一句话解释 KV Cache}], max_tokens: 32, stream: false }预期返回是一个标准 OpenAI 格式的 JSONchoices[0].message.content里有模型输出。同时去看 prefill worker 的日志应该出现类似save_kv_layer的记录decode worker 日志里对应出现start_load_kv和wait_for_layer_load。如果两边日志都有说明 KV 传输链路通了。再验证一下流式场景下的 TTFTcurl -N http://127.0.0.1:8000/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: Qwen/Qwen3-8B, messages: [{role: user, content: 写一段 200 字的自我介绍}], max_tokens: 256, stream: true }流式返回里第一个data:chunk 到达的时间就是 TTFT。如果这个值明显低于你之前单卡混跑时的首 token 延迟PD 分离就生效了。想更直观地对比模型输出质量可以到模型对话页面手动跑几条https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel_chatutm_campaignrewrite5. 本篇常见错排查5.1 KV 传输超时或 decode 端一直等最常见的原因是kv_connector两端不一致。prefill 配了NixlConnectordecode 配了SharedStorageConnector元数据对不上decode 会一直wait_for_layer_load直到超时。检查两边--kv-transfer-config的kv_connector字段是否完全相同。另一个原因是网络。NIXL 依赖高速互联如果两台机器之间只有千兆以太网2048 token 的 KV 传输可能超过单次 decode 步耗时表现为 TPOT 抖动。这时候要么换SharedStorageConnector走本地 NVMe要么把 prefill 和 decode 放到同一节点用 NVLink。5.2 入口返回 401 或 403先确认TAOTOKEN_API_KEY环境变量在网关进程里可见。settings.json里写的是api_key_env不是 Key 本身如果你直接填了 Key 字符串网关会把它当环境变量名去找自然找不到。用env | grep TAOTOKEN确认一下。5.3 prefill 端显存爆掉max_num_batched_tokens给太大长 prompt 一次性把 KV 全算出来显存直接顶满。把enable_chunked_prefill打开让长序列分块计算或者把max_num_batched_tokens降到 4096 试试。decode 端的gpu_memory_utilization也别设到 0.95 以上留点余量给 KV 传输的 buffer。5.4 日志里 KV 传输成功但输出乱码这通常是两端模型版本或 tokenizer 不一致。prefill 和 decode 必须加载同一个模型权重、同一份 tokenizer 配置。检查--model参数是否完全一致别一个用本地路径一个用远端名称。6. 长期跑编码/Agent 场景的接入建议如果你搭 PD 分离不是为了单次对话而是给 Coding Agent 或长上下文编码助手做后端那请求模式会变成「长 prompt 长输出 高并发」对 KV 传输的稳定性要求更高。这种场景建议把granularity设成layer让传输和计算重叠同时入口层开启 SLO 早期拒绝避免慢请求拖垮整个 decode 批次。长期跑的话Key 和配额管理也会变成日常。Coding Plan 适合需要持续调用、按周期结算的场景配置入口在https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite如果你用的是 Claude Code 这类编码工具想让它的请求走统一通道可以参考 Anthropic 兼容接入的配置方式https://taotoken.net/claude-code-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaude_codeutm_campaignrewrite最后提醒一句PD 分离的收益在「同时有严格 TTFT 和 TPOT 要求」时才明显。如果你的服务只关心总吞吐、对首 token 延迟不敏感单卡 continuous batching 加 chunked-prefill 可能更省事。先把config.toml里的两个 worker 跑起来用第 4 节的验证请求确认 KV 链路通了再根据实际 SLO 调 batch 和并行参数比一上来就堆配置靠谱得多。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进