vLLM 分离式 Prefill/Decode 部署实战:XpYd 代理、多轮 KV 复用与 KV Cache 事件发布
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
本文围绕 disaggregated_serving 示例目录 展开,讲解 vLLM 的 Disaggregated Serving(Prefill/Decode 分离部署)能力:从最经典的 XpYd(X 个 Prefill 实例 + Y 个 Decode 实例)拉取模式代理,到双向 KV 传输的多轮对话代理、Push 模式代理,再到 KV cache 事件的 ZMQ 发布与订阅。读完本文,你可以直接复制仓库中的脚本搭建一套 P/D 分离的推理集群,并理解代理如何转发kv_transfer_params完成跨实例 KV cache 迁移。
示例目录概览
examples/disaggregated/disaggregated_serving/README.md 说明该目录包含演示 vLLM disaggregated serving 特性的脚本。README 中列出的核心文件如下:
| 文件 | 作用 |
|---|---|
| disagg_proxy_demo.py | 演示 XpYd(X 个 prefill 实例、Y 个 decode 实例)的拉取模式代理 |
| kv_events.sh | 演示 KV cache 事件发布(KV cache event publishing) |
| mooncake_connector | MooncakeConnector 的代理演示(位于相邻的mooncake_connector目录) |
此外,当前仓库中该目录还包含以下未在 README 中列出的补充脚本(可以推断是后续迭代加入的):
- disagg_proxy_multiturn.py:支持双向 KV 传输的多轮对话代理,让每一轮复用上一轮 Decode 节点上的 KV 块;
- disagg_proxy_pushconnector_demo.py:与拉取模式配套的 Push 模式代理演示;
- moriio_toy_proxy_server.py:MoRI-IO P/D 分离的单节点协议参考代理。
XpYd 拉取模式代理(disagg_proxy_demo.py)
这是分离式部署最典型的形态:启动多个 vLLM 实例(示例中 2 个 prefill + 2 个 decode),再启动代理进行统一路由。文件头部 docstring 给出的启动方式为:
python3 examples/disaggregated/disaggregated_serving/disagg_proxy_demo.py \ --model $model_name \ --prefill localhost:8100 localhost:8101 \ --decode localhost:8200 localhost:8201 \ --port 8000命令行参数
parse_args(disagg_proxy_demo.py#L419-L446)定义了四个参数:
| 参数 | 缩写 | 说明 |
|---|---|---|
--model/-m | 必填 | 模型名,用于启动时校验各实例服务的模型是否一致 |
--prefill/-p | 多个 | Prefill 节点 URL 列表(host:port) |
--decode/-d | 多个 | Decode 节点 URL 列表(host:port) |
--port | 默认 8000 | 代理自身监听端口 |
启动时ProxyServer.validate_parsed_serve_args会强制要求 prefill、decode 节点各至少一个,并逐一校验实例地址格式(host 为 IP 或localhost,端口在 1~65535 之间),随后调用verify_model_config请求每个实例的/v1/models,比对模型名后缀是否一致,不一致则直接抛错——这避免了 P/D 节点模型错位导致的隐性故障。
两阶段请求转发
代理对/v1/completions与/v1/chat/completions均执行同样的两段式转发(以create_completion为例,见 disagg_proxy_demo.py#L250-L278):
- Prefill 阶段:复制一份请求并将
max_tokens改写为 1(chat 接口同时改写max_completion_tokens),转发给轮询选中的 prefill 实例。该实例只完成提示词的 KV 计算,产出 token 被丢弃; - Decode 阶段:将原始请求转发给轮询选中的 decode 实例,以
StreamingResponse流式返回给客户端。KV 数据的实际迁移由 P/D 两侧引擎通过 KV connector 完成,代理只负责编排两次 HTTP 调用。
调度策略由SchedulingPolicy抽象类定义,默认实现RoundRobinSchedulingPolicy基于itertools.cycle做轮询;上游连接失败时,forward_request抛出HTTPException,代理会调用remove_instance_endpoint将故障实例摘除并重建 cycler,实现简单的故障剔除。
代理的 HTTP 接口
setup_routes(disagg_proxy_demo.py#L67-L85)注册了四个路由:
POST /v1/completions、POST /v1/chat/completions:客户端入口,均要求Content-Type: application/json(否则返回 415);GET /status:返回 prefill/decode 节点数量与地址列表,可用于运维监控;POST /instances/add:运行时动态扩容节点,需要X-API-Key请求头且服务端设置了ADMIN_API_KEY环境变量;请求体形如{"type": "prefill", "instance": "host:port"},代理会先通过/v1/models校验模型一致后再加入实例池。
需要注意 docstring 中的一段说明:该 demo 计划在内置 PDController 支持 XpYd 之后移除,因此它定位是协议演示,而非生产级路由组件。
多轮对话的双向 KV 传输(disagg_proxy_multiturn.py)
多轮聊天场景下,第一轮结束后 KV 块留在 Decode 节点上;如果第二轮重新走 Prefill 重算,会浪费大量计算。disagg_proxy_multiturn.py 头部 docstring 给出了其架构与请求流:
Client ──► Proxy ──► Prefill (P) ──► Decode (D)每个请求的处理流程为:
- 客户端向代理发送 chat/completions 请求;
- 代理按
conversation_id查询上一轮缓存的 D 节点块信息; - 命中缓存时,代理把 D 的块信息附到请求上,让 P 直接读取 D 上的 KV 块而不是重算;
- 代理将请求发给 P(
max_tokens=1,非流式); - P 返回携带自身块信息的
kv_transfer_params; - 代理将请求和 P 的块信息转发给 D(流式);
- D 流式返回响应,最后一个 chunk 中包含 D 的
kv_transfer_params,代理将其缓存供下一轮使用。
其中conversation_id是请求 JSON 顶层的非标准扩展字段,用于跨轮次关联同一个会话;缺少该字段时代理无法串联轮次,会退化为无缓存行为。docstring 同时提醒:严格的 OpenAI 兼容前端会拒绝未知字段,因此该字段只被代理消费、不会转发给 vLLM 引擎。
Push 模式代理(disagg_proxy_pushconnector_demo.py)
disagg_proxy_pushconnector_demo.py 是拉取模式的“孪生”演示,客户端侧 API 完全相同,区别在于 P/D 之间 KV 传输的协调方向(见其头部 docstring):
- Pull 模式(
disagg_proxy_demo.py):代理把 P 的kv_transfer_params(含remote_block_ids)转发给 D,由 D 通过 NIXL READ 主动从 P 拉取 KV; - Push 模式:代理只把 P 的坐标(
remote_engine_id、remote_host、remote_port、tp_size、pp_size)和共享的remote_request_id交给 D;D 先通过 NIXL 通知向 P 注册自己本地分配的块,随后由 P 通过 NIXL WRITE 把 KV 推给 D。
Push 模式下 vLLM 实例需配置NixlPushConnector以及匹配的engine_id/side_channel_port,代理的启动命令比 Pull 模式多了 P 侧坐标参数:
python3 examples/disaggregated/disaggregated_serving/\ disagg_proxy_pushconnector_demo.py \ --model $model_name \ --prefill localhost:8100 \ --decode localhost:8200 \ --prefill-engine-id prefill-engine-001 \ --prefill-kv-host 10.0.0.1 \ --prefill-side-channel-port 5600 \ --prefill-tp-size 1 \ --prefill-pp-size 1 \ --port 8000KV Cache 事件发布(kv_events.sh)
kv_events.sh 演示引擎对外发布 KV cache 事件的完整链路。脚本开头即声明该功能为实验特性("The usage of KV cache events is experimental and subject to change")。
服务端配置
脚本通过环境变量HF_MODEL_NAME(默认meta-llama/Meta-Llama-3.1-8B-Instruct)确定模型,并设置VLLM_HOST_IP为机器第一个 IP。核心是带--kv-events-config的启动命令(kv_events.sh#L37-L44):
vllm serve "$MODEL_NAME" \ --port 8100 \ --max-model-len 100 \ --enforce-eager \ --gpu-memory-utilization 0.8 \ --trust-remote-code \ --kv-events-config \ '{"enable_kv_cache_events": true, "publisher": "zmq", "topic": "kv-events"}'其中--kv-events-config是一个 JSON 配置:enable_kv_cache_events开启事件发布,publisher指定发布通道为 ZMQ,topic为订阅主题名kv-events。脚本随后用wait_for_server轮询/v1/completions直到服务就绪(超时 1200 秒),再启动订阅者,最后发送两条示例请求触发事件,并打印两次 completions 的响应。
需要注意一个路径细节:脚本按$SCRIPT_DIR/kv_events_subscriber.py定位订阅者,即期望它与kv_events.sh位于同一目录;而当前仓库中该文件实际位于 examples/features/kv_events/kv_events_subscriber.py,运行前需将其放到kv_events.sh所在目录(或将路径改为指向 features 目录),否则订阅进程无法启动。
订阅者实现要点
kv_events_subscriber.py 展示了事件消费端的标准写法:
- 消息类型:使用
msgspec定义KVEventBatch(含时间戳ts与事件列表),事件包括BlockStored(新增块,携带block_hashes、parent_block_hash、token_ids、block_size等字段,块哈希类型来自 vllm/v1/core/kv_cache_utils.py 的ExternalBlockHash)、BlockRemoved(块被逐出)与AllBlocksCleared(缓存清空); - 双通道 ZMQ:主通道为 SUB socket,连接
tcp://localhost:5557并订阅kv-events主题;另外用 REQ socket 连接tcp://localhost:5558作为重放(replay)通道; - 断流补齐:每条消息携带 8 字节大端序列号,若发现
seq > last_seq + 1说明有消息丢失,订阅者会向 replay 通道请求从last_seq + 1开始补齐,直到序列号追平(重放结束以空 payload 帧标记)——这让外部系统可以基于事件流重建 KV 块状态的完整视图。
这类事件对上层调度系统很有价值:路由/缓存感知组件可以据此掌握各实例上 KV 块的分布,为跨实例前缀复用提供依据。
MoRI-IO 单节点参考代理(moriio_toy_proxy_server.py)
moriio_toy_proxy_server.py 是 MoRI-IO 分离式部署的最小单节点参考代理。docstring 明确其定位与边界:
- 它演示 connector 期望的DP rank 固定(pinning)契约:每个请求选定一个 prefill DP rank(
flat_interleaved_dp_route),并通过X-data-parallel-rank请求头加上kv_transfer_params中的remote_dp_rank/remote_dp_size/remote_tp_size把请求的两段(P 与 D)都固定到该 rank 上; - 它有意不提供跨 Pod 的 Wide-EP(如 2P2D)对等节点信息(
remote_hosts、multi_pod_hosts、data_parallel_size_local、is_request_leader等)。生产级多 Pod 部署中,这些信息通常由路由 sidecar(如 llm-d-router 或 NIXL 形态的路由器)生成; - 因此该文件应作为协议参考使用,而不是生产路由器。
小结与适用建议
- 理解 P/D 分离的代理层协议、
kv_transfer_params的流转方式,从disagg_proxy_demo.py入手最直接; - 需要多轮会话复用 Decode 侧 KV 时,参考
disagg_proxy_multiturn.py的双向传输与conversation_id设计; - 评估 Push 模式(P 侧主动 NIXL WRITE)时,使用
disagg_proxy_pushconnector_demo.py并配置NixlPushConnector; - 构建缓存感知的外部调度/路由系统时,
kv_events.sh+ 订阅者是完整的 ZMQ 事件链路范例(注意其实验状态); - 更完整的分离式部署形态(如 LMCache 集成、NIXL connector 的 prefill/decode 独立脚本、encoder 分离等)可继续参考 examples/disaggregated/lmcache/、examples/disaggregated/example_connector/ 与 examples/disaggregated/disaggregated_encoder/。
以上脚本均为示例级实现:轮询调度、单进程状态、内存中的会话缓存等设计决定了它们适合验证协议与拓扑,生产环境应结合仓库中正式的 KV connector 实现与外部路由组件评估使用。
【免费下载链接】vllmA high-throughput and memory-efficient inference and serving engine for LLMs项目地址: https://gitcode.com/GitHub_Trending/vl/vllm
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考