Prhub

#42285 Secondary tier implementation for PD disaggregation

原始 PR 作者 liranschour 合并时间 2026-06-30 12:51 文件变更 22 提交数 96 评论 199 代码增减 +7471 / -0

执行摘要

实现 PD 解耦的 P2P KV 缓存次级层

在 PD 解耦架构中,Prefiller 和 Decoder 运行在不同节点,需要高效共享 KV 缓存。本 PR 基于 vLLM CPU KV cache 的 canonical layout(统一 TP 块大小),实现一个 P2P 次级层作为 SecondaryTierManager 接口的实现,由 TieringManager 统一编排,对次级层透明。

建议精读,尤其是:

  1. 会话状态机设计(P2PSession + ClientRole + ServerRole)体现了协议驱动的分层思想。
  2. 控制面与数据面分离抽象(ControlTransport vs DataTransport)便于替换实现。
  3. review 中 Claude 指出的正确性边界案例是理解分布式一致性的宝贵材料。
  4. 性能优化方面,注意高频路径的空列表缓存和 O(N) 扫描改进。
    团队应优先解决 NIXL 缺失降级和线程安全问题。
讨论亮点

评审中 orozery 和 Claude 识别了多项关键问题:

  • NIXL 缺失崩溃(gemini-code-assist):lookup 等方法在 NIXL 未安装时直接访问 self._agent 导致 AttributeError
  • 空列表分配性能(orozery):ZmqConnection.recv()ZmqTransport.poll() 等高频路径每次调用都分配新空列表,建议缓存共享哨兵。
  • write_blocks 失败挂起(Claude):write_blocks 返回 None 时作业永久挂起,无 StoreResult(success=False) 触发。
  • 重复 completion 矛盾(Claude):超时作业可能在超时后收到成功 completion,产生重复/矛盾结果。
  • 会话状态验证缺失(Claude):连接建立前接受业务消息应视为协议错误并断开。
  • 线程安全风险(Claude):调度线程和轮询线程可能同时访问 ZMQ 套接字。
  • shutdown RDMA use-after-deregister(Claude):关闭时未等待 inflight 传输即注销内存注册。
  • 设计改进(orozery):建议将 session.py 拆分为 ClientRoleServerRole 独立类,便于测试和维护。

实现拆解

  1. 数据面抽象:在 data/base.py 中定义 DataTransport 抽象类,封装本地 KV 块内存区域、远程对端注册和异步块写入/轮询/取消接口,并通过 config_fingerprint 机制验证对端兼容性。
  2. 控制面抽象:在 control/base.py 中定义 ControlConnectionControlTransport 抽象,提供双向消息通道和连接管理接口。
  3. ZMQ 控制面实现control/zmq.pyZmqTransportZmqConnection 基于 ZMQ ROUTER/DEALER 实现,支持心跳检测和断开监测,消息内容无关。
  4. NIXL 数据面实现data/nixl.pyNixlTransport 封装 NIXL C 库,支持 UCX 等后端,提供 RDMA 块传输的异步提交、轮询完成和取消。
  5. 双向会话session/session.pyP2PSession 同时扮演客户端(向对端请求块)和服务端(向对端提供块),通过 ClientRoleServerRole 分别管理状态,协议消息定义在 protocol.pyserver.py 实现块匹配与传输驱动。
  6. 二级层管理器manager.pyP2PSecondaryTierManager 管理所有会话生命周期,实现 lookupon_new_requestsubmit_storesubmit_loadon_request_finishedget_finished_jobs 等接口,并处理 kv_transfer_params 中的嵌套配置。
  7. 测试配套:包含单元测试(test_sessions.pyFakeDataTransport 模拟、test_manager.py)和集成测试代理(p2p_connector_proxy.py)。
  8. 文档:更新 kv_offloading_usage.md 说明 P2P tier 配置方法。
文件 模块 状态 重要度
vllm/v1/kv_offload/tiering/p2p/manager.py 管理层 added 9.08
vllm/v1/kv_offload/tiering/p2p/session/session.py 会话层 added 9.08
vllm/v1/kv_offload/tiering/p2p/data/base.py 数据层 added 8.89
vllm/v1/kv_offload/tiering/p2p/control/zmq.py 控制层 added 8.89
vllm/v1/kv_offload/tiering/p2p/data/nixl.py 数据层 added 8.89
vllm/v1/kv_offload/tiering/p2p/session/protocol.py 协议层 added 8.98
vllm/v1/kv_offload/tiering/p2p/session/client.py 客户端 added 8.91
vllm/v1/kv_offload/tiering/p2p/control/base.py 控制层 added 8.82
tests/v1/kv_offload/tiering/p2p/test_sessions.py 测试 added 7.76
tests/v1/kv_offload/tiering/p2p/test_manager.py 测试 added 7.76
tests/v1/kv_offload/tiering/p2p/p2p_connector_proxy.py 测试 added 7.48

关键符号

P2PSecondaryTierManager.__init__ P2PSecondaryTierManager.lookup P2PSecondaryTierManager.on_new_request P2PSecondaryTierManager.on_request_finished P2PSession.__init__ P2PSession.poll P2PSession.request_blocks DataTransport.__init__ DataTransport.write_blocks DataTransport.poll ZmqTransport.__init__ ZmqConnection.send NixlTransport.__init__ NixlTransport._init NixlTransport.write_blocks

关键源码片段

vllm/v1/kv_offload/tiering/p2p/manager.py core-logic

二级层管理器入口,实现 SecondaryTierManager 接口,负责会话生命周期和与 TieringManager 交互

import time
from collections.abc import Iterable, Sequence
from dataclasses import dataclass, field
from vllm.logger import init_logger
from vllm.v1.kv_offload.base import LookupResult, OffloadKey
from vllm.v1.kv_offload.tiering.base import SecondaryTierManager
from vllm.v1.kv_offload.tiering.p2p.control import ZmqTransport
from vllm.v1.kv_offload.tiering.p2p.data import NixlTransport
from vllm.v1.kv_offload.tiering.p2p.session import P2PSessionlogger = init_logger(__name__)_UNBOUND_STORE_TIMEOUT_S = 60.0 # 未绑定的 store 批处理超时
_SHUTDOWN_DRAIN_TIMEOUT_S = 3.0 # 关闭排空超时
_DRAIN_SLEEP_S = 0.001 # 排空循环休眠间隔def _prefill_params(kv_params: dict | None) -> dict | None:
    if not kv_params:
        return None
    return kv_params.get("prefill") # Decoder 请求携带 prefill 参数def _decode_params(kv_params: dict | None) -> dict | None:
    if not kv_params:
        return None
    return kv_params.get("decode") # Prefiller 请求携带 decode 参数@dataclass
class _UnboundStoreBatch:
    """在 peer 连接前临时保管 submit_store 批处理"""
    job_id: int
    keys: list[OffloadKey]
    block_ids: Sequence[int]
    submitted_at: float = field(default_factory=time.monotonic)class P2PSecondaryTierManager(SecondaryTierManager):
    def __init__(self, offloading_spec, primary_kv_view, tier_type="p2p",
                 host="0.0.0.0", port=7777, backends=None, num_threads=4, **kwargs):
        # 控制面:ZMQ ROUTER/DEALER,绑定在指定地址
        self._control = ZmqTransport(local_id, host, port)
        # 数据面:NIXL RDMA 传输,注册本地 KV 块内存
        self._data = NixlTransport(local_id, primary_kv_view, backends=backends, num_threads=num_threads)
        self._sessions: dict[str, P2PSession] = {} # peer_id -> session
        self._unbound_batches: dict[str, list[_UnboundStoreBatch]] = {}
        # ... 其他初始化
vllm/v1/kv_offload/tiering/p2p/session/session.py core-logic

双向会话协调器,同时处理客户端块请求和服务端块供应

from typing import NamedTuple
from vllm.v1.kv_offload.tiering.p2p.session.client import ClientRole, LoadResult
from vllm.v1.kv_offload.tiering.p2p.session.server import ServerRole, StoreResultclass SessionPollResult(NamedTuple):
    loads: list[LoadResult]
    stores: list[StoreResult]
    new_fetch_ids: list[str] # 本轮新到达的 FetchMsg 的 kv_request_idclass P2PSession:
    def __init__(self, peer_id: str, data: DataTransport,
                 control: ControlConnection | None = None):
        self.peer_id = peer_id
        self._data = data
        self._control = control # 可为 None,表示 pending 状态
        self._client = ClientRole(...) # 负责加载请求
        self._server = ServerRole(...) # 负责存储响应
        self._dispatch_errors = 0 # 连续分发错误计数
        self._connected = False
        self._send_buffer: list[dict] = [] # 等待连接确认后发送的消息
​
    @property
    def alive(self) -> bool:
        return self._control is not None and self._control.alive
​
    @property
    def connected(self) -> bool:
        return self._connected
​
    def poll(self) -> SessionPollResult:
        loads = self._client.poll()
        stores = self._server.poll()
        new_fetch_ids = self._server.drain_new_fetch_ids()
        return SessionPollResult(loads, stores, new_fetch_ids)
vllm/v1/kv_offload/tiering/p2p/data/base.py core-logic

数据面抽象基类,定义 RDMA 块传输接口和内存模型

import ctypes, hashlib, json
from abc import ABC, abstractmethod
from typing import NamedTupleclass PollResult(NamedTuple):
    done: Sequence[int] # 成功完成的 transfer_id
    failed: Sequence[int] # 失败的 transfer_idclass DataTransport(ABC):
    def __init__(self, view: memoryview, config_fields: dict | None = None):
        # 将 memoryview 转换为连续块区域描述
        self._view = view
        self._base_addr = ctypes.addressof(ctypes.c_char.from_buffer(view))
        self._num_blocks = view.shape[0]
        self._block_len = view.shape[1]
        # 模型配置哈希,用于对端兼容性检查
        self._config_fingerprint = self._compute_fingerprint(config_fields)
​
    @property
    def base_addr(self) -> int:
        return self._base_addr
​
    @property
    def num_blocks(self) -> int:
        return self._num_blocks
​
    @property
    def block_len(self) -> int:
        return self._block_len
​
    @property
    def config_fingerprint(self) -> str:
        return self._config_fingerprint
​
    @staticmethod
    def _compute_fingerprint(config_fields: dict | None) -> str:
        if not config_fields:
            return ""
        raw = json.dumps(config_fields, sort_keys=True)
        return hashlib.sha256(raw.encode()).hexdigest()
​
    @abstractmethod
    def add_remote_peer(self, peer_id: str, metadata: bytes,
                        base_addr: int, num_blocks: int, block_len: int) -> None: ...
    @abstractmethod
    def remove_remote_peer(self, peer_id: str) -> None: ...
    @abstractmethod
    def write_blocks(self, peer_id: str, local_idxs: list[int],
                     remote_idxs: list[int]) -> int | None: ...
    @abstractmethod
    def poll(self) -> PollResult: ...
    @abstractmethod
    def cancel(self, transfer_ids: list[int], mode: str = "immediate") -> None: ...
    @abstractmethod
    def close(self) -> None: ...

评论区精华

NIXL 未安装时崩溃 正确性

gemini-code-assist 指出 lookup 和 _handle_message 等方法在 NIXL 未安装时直接访问 self._agent 导致 AttributeError,应增加可用性检查。

结论:作者未明确回复,但应在后续提交中添加 guard。 · 待处理

空列表分配性能 性能

orozery 指出 ZmqConnection.recv()、ZmqTransport.poll() 等高频路径每次调用分配新空列表,建议使用共享哨兵对象。

结论:作者标记为 Done(但仅回复了部分),最终代码使用了 _EMPTY_INBOX 等哨兵。 · 已解决

write_blocks 失败导致作业永久挂起 正确性

Claude 分析:当 write_blocks 返回 None 时,没有为贡献的 job_id 触发 StoreResult(success=False),导致作业一直留在 _store_jobs 直至超时。

结论:需在提交中修复,注入失败结果。 · 待处理

重复 completion 矛盾 正确性

Claude 指出:超时作业可能稍后收到成功 completion,产生既失败又成功的矛盾结果。

结论:需在提交中确保超时后忽略后续完成事件。 · 待处理

会话状态验证缺失 设计

Claude 分析:连接建立前接受业务消息应视为协议错误并断开连接,当前代码未严格验证状态。

结论:建议在 _dispatch_message 中增加连接状态断言。 · 待处理

线程安全风险 正确性

Claude 指出调度线程调用 submit_store 时可能同时与轮询线程访问 ZMQ 套接字,导致竞态。

结论:需加锁或改为单线程驱动。 · 待处理

shutdown RDMA use-after-deregister 正确性

Claude 分析:关闭时 session.close() 触发 cancel 但未等待 inflight 传输完成,随后 deregister 内存可能导致硬件 DMA 访问已注销内存。

结论:需确保 cancel(mode='wait') 完成后再 deregister。 · 待处理

风险与影响

  1. NIXL 依赖NixlTransport 假设 NIXL 可用,未提供降级路径,无 NIXL 时所有 P2P 操作将崩溃。
  2. 线程安全P2PSecondaryTierManager 使用单独轮询线程,与调度线程可能同时访问 ZmqTransport(ZMQ 套接字非线程安全)。
  3. RDMA 安全性close() 时未确保 inflight 传输已完成即注销内存,可能导致硬件 DMA 操作悬空。
  4. 测试 flaky_free_port() 存在 TOCTOU 竞态,time.sleep(0.05) 在负载下不可靠。
  5. 内存分配性能:高频路径存在不必要的列表分配,可能增加 GC 压力。

用户:需通过 kv_connector_extra_config['secondary_tiers'] 配置 P2P tier,并确保 NIXL 库可用(否则功能不可用)。
系统:新增约 7500 行代码,引入 ZMQ 和 NIXL 依赖,增加内存占用(每个会话的 inbox、inflight 跟踪)。
团队:新增 kv-connector 子系统模块,需持续维护会话协议和数据层抽象。
影响范围:限于 vllm/v1/kv_offload/tiering/p2p,不涉及推理核心路径,但通过 SecondaryTierManager 接口与 TieringManager 耦合。

NIXL 缺失崩溃 线程不安全 RDMA use-after-free 测试 flaky write_blocks 失败挂起

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论