# pd分离在vllm中用法 > **来源**:CSDN 博客(博主:小徐炸酱面) > **作者**:小徐炸酱面(qq_44319972) > **发布日期**:2025-12-01(2025-12-02 修改) > **原文链接**:https://blog.csdn.net/qq_44319972/article/details/155456852 --- ### 1. 背景:为什么需要 PD 分离 大模型在线服务一般包含两种阶段: - **Prefill 阶段** - 输入:完整的 prompt - 动作:一次性跑完所有输入 token 的前向,构建 KV Cache - 特点: - **计算密集**(GEMM-heavy,吃算力) - 对显存需求大(一次性占用大量 KV) - 对带宽要求相对没那么极端(一次性大算子) - **Decode 阶段** - 输入:每轮只处理很少的新 token(一般 1~几 token) - 动作:基于已有 KV Cache 不断迭代生成新 token - 特点: - 单步计算量相对小 - 访问 KV Cache 频繁,**更吃显存带宽 / 内存带宽 / 通信带宽** - 很多小 kernel,调度频率高 **PD 分离(Producer / Decoder Separation)** 的目标就是: - 把 prefill 和 decode 拆到**不同的进程 / 节点 / 设备**上 - 利用不同节点的算力/带宽特性,整体提升吞吐、降低延迟 ### 2. PD 分离的基本概念与架构 #### 2.1 角色定义 - **P 节点(Producer)** - 负责:prefill 阶段 - 对应操作: - 处理新请求的 prompt - 运行大批量 GEMM - 把得到的 KV Cache 写入某种共享介质(或通过 KV 通道发送给 D 节点) - 资源特征: - 更看重:**算力、GPU FLOPs** - 对长尾小请求和频繁通信不是特别敏感 - **D 节点(customer)** - 负责:decode 阶段 - 对应操作: - 不断从 KV 中取数据 - 迭代生成每个 request 的后续 token - 资源特征: - 更看重:**显存带宽 / 网络带宽 / 延迟** - 关注 tail latency(TPOT、e2e) #### 2.2 KV Cache 的流转 PD 分离的**核心**就是:**prefill 生成的 KV,如何让 decode 能够用起来**。 一般数据流可以抽象成: 1. 请求进来,P 节点接收并执行 prefill 2. P 节点为该 request 分配 KV Block(或 Block 列表) 3. P 节点把 KV 的位置信息 + request 的 metadata 通过某种 **KV Connector / RPC 通道** 告诉 D 节点 4. D 节点接管该 request 的 decode: - 知道对应 KV 在哪里 - 在后续 decode 中,始终通过 KV Connector 访问这些 KV 典型注意事项: - 如果有 DP/TP,P/D 侧的并行策略也要匹配(否则会出现 KV 维度不一致、block id 对不上等问题) - 如果跨机器,需要额外考虑网络带宽、RDMA、压缩等问题 ### 3. vLLM 中的 PD 分离思路(概念层面) > 这里以 "vLLM + 自研 PD 功能" 的思路来整理,只写通用逻辑,不绑死具体实现细节。 #### 3.1 与调度器(scheduler)的关系 vLLM 的 serving 里,有一个统一的 scheduler 管理: - 新请求的进入(prefill 阶段) - 已有请求的继续 decode - 每一轮 step,要算多少 token(`num_scheduled_tokens`) - 每个 request 一共已经算了多少 token(`num_computed_tokens`) PD 分离后,逻辑可以拆解为: 1. **Producer 侧 scheduler**:只负责 prefill 2. **Decoder 侧 scheduler**:只负责 decode 3. 通过某种方式保证两边对 request 状态的约定一致,例如: - request id - 已生成 token 数 - KV block id 列表 - 当前是否已经结束(EOS / abort) ### 4. 在 vLLM 中使用 PD 分离的典型启动方式(示意) > 下面用伪代码/伪命令展示结构,变量名你可以换成你们环境里的 `VLLM_USE_V1` / `vllm_PD_ROLE` / `kv_transfer_config` 等。 #### 4.1 不分离时(baseline) ``` # 单机单进程示意 python3 -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen3-32B \ --tensor-parallel-size 2 \ --dtype bfloat16 \ --max-model-len 32768 \ --port 8000 \ --host 0.0.0.0 ``` Benchmark 示意: ``` python3 benchmarks/benchmark_serving.py \ --backend openai-chat \ --base-url http://:8000 \ --endpoint /v1/chat/completions \ --model Qwen3-32B \ --tokenizer /data/models/Qwen3-32B \ --dataset-name random \ --random-input-len 2000 \ --random-output-len 200 \ --request-rate 5 \ --num-prompts 100 \ --percentile-metrics ttft,tpot,e2el ``` #### 4.2 1P1D(单机或双机) **4.2.1 Producer 节点** ``` export VLLM_USE_V1=1 export vllm_PD_ROLE=producer python3 -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen3-32B \ --tensor-parallel-size 2 \ --max-model-len 32768 \ --max-num-batched-tokens 8192 \ --max-num-seqs 32 \ --port 8209 \ --host 0.0.0.0 \ --kv-transfer-config \ '{"kv_connector":"P2pNcclConnector", "kv_role":"kv_producer", "kv_rank":0, "kv_parallel_size":2, "kv_buffer_size":1, "kv_port":9109, "kv_connector_extra_config":{ "proxy_ip":"0.0.0.0", "proxy_port":30001, "http_port":8109 }}' \ &> Qwen3-32B-producer.log & ``` **4.2.2 Decoder 节点(D,kv_consumer)** > ⚠️ 原文此处的代码块与 Producer 节点完全相同(`vllm_PD_ROLE=producer`、`kv_role:"kv_producer"`),疑为作者复制粘贴遗漏修改。按原文忠实保留如下,实际部署时 Decoder 侧应改为 `vllm_PD_ROLE=decoder` / `kv_role:"kv_consumer"`、`kv_rank` 相应调整。 ``` export VLLM_USE_V1=1 export vllm_PD_ROLE=producer python3 -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen3-32B \ --tensor-parallel-size 2 \ --max-model-len 32768 \ --max-num-batched-tokens 8192 \ --max-num-seqs 32 \ --port 8209 \ --host 0.0.0.0 \ --kv-transfer-config \ '{"kv_connector":"P2pNcclConnector", "kv_role":"kv_producer", "kv_rank":0, "kv_parallel_size":2, "kv_buffer_size":1, "kv_port":9109, "kv_connector_extra_config":{ "proxy_ip":"0.0.0.0", "proxy_port":30001, "http_port":8109 }}' \ &> Qwen3-32B-producer.log & ``` **`kv-transfer-config` 参数解释:** - **`kv_connector`**:用什么通道传 KV。现在用:`P2pNcclConnector`,表示走 NCCL P2P 通信。 - **`kv_role`**:这个进程是**发 KV 的**还是**收 KV 的**。 - P:`"kv_producer"` - D:`"kv_consumer"` - **`kv_rank`**:在这组通道里的编号,从 0 开始。多个并行通道时,用它来区分是谁↔谁。 - **`kv_parallel_size`**:一共有几路 KV 通道(也可以理解为这组里有多少 rank)。一般设成和 TP/world size 一致。P 和 D 必须一样。 - **`kv_buffer_size`**:通道里最多允许多少块 KV "在路上"。值越大,流水越深、占用显存可能越多;不确定就先用 `1`,稳一点。 - **`kv_port`**:KV 通道用的基础端口。P / D 两边要一致,别和其他服务冲突。 **`kv_connector_extra_config` 里的:** - **`proxy_ip`**:KV 代理服务的 IP,或者要连的那台机的 IP。单机可以写 `0.0.0.0` 或 `127.0.0.1`,跨机就写对端/代理机的 IP。 - **`proxy_port`**:KV 代理服务监听的端口。P / D 配成同一个值,比如 30001。 - **`http_port`**:**Decoder vLLM HTTP 服务的端口**(就是 D 起 server 时的 `--port`)。用来让 KV 组件知道 D 的 HTTP 在哪里。 **关于 xPyD:** 一般来说 xPyd,其中每一个 p 节点或者 d 节点都要能独立加载完整个模型。 总卡数量:总 GPU 数 = (P + D) × TP × DP 最后使用 vllm 自带的代理端口转发即可: ``` python3 vllm/examples/online_serving/disaggregated_serving_p2p_nccl_xpyd/disagg_proxy_p2p_nccl_xpyd.py ``` --- > **下载说明**:本文原文含大量 CSDN 页面噪声(侧边栏推荐文章、广告、评论 UI、VIP 弹窗等),已剔除非作者内容,仅保留正文与代码;CSDN 推荐栏中出现的其他博主的 PD 分离相关文章摘要未纳入。