# D→P Phase 1:底层 RDMA 链路(已验收) **日期**:2026-05-13 **状态**:底层链路通过 smoke test 验收 **前置**:`docs/D_TO_P_SYNC_DESIGN_ZH.md` **对应 commit**:`feat(snapshot): D→P snapshot link over mooncake RDMA` --- ## 0. 一句话 实现一个独立于 SGLang `MooncakeKVManager` 的**最小 RDMA 字节传输模块**(`src/agentic_pd_hybrid/snapshot_link.py`),双进程 smoke test 跑通 1 KB → 64 MB 一共 5 个 size,全部 SHA 校验通过,64 MB 单次 RDMA write 实测 315 Gbps(mlx5_60 NDR 400 Gb 的约 80%)。 ## 1. 设计动机 `docs/D_TO_P_SYNC_DESIGN_ZH.md` 选定 Option C(D→P snapshot push + P SessionSlot + prefill bypass)。这个方案的最底层依赖是"D 进程能把字节通过 RDMA 推到 P 进程的预注册缓冲区"。 直接复用 SGLang 的 `MooncakeKVManager` 不可行: - `add_transfer_request` 在 `conn.py:1563` 硬 assert `disaggregation_mode == PREFILL` - PD pipeline 的发送 / 接收 thread / queue / staging 紧耦合 PD 角色 - 改 PD 路径风险大(影响现有 E1/E2/E3 配置) 因此把 D→P link 单独写成一个轻量模块,直接调 `mooncake.engine.TransferEngine` 的 `transfer_sync_write` / `batch_transfer_sync_write`,不经过 PD pipeline。 ## 2. 实现 ### 2.1 `snapshot_link.SnapshotPeer` ```python peer = SnapshotPeer(host, port, ib_device, receive_capacity_bytes) endpoint = peer.endpoint # SnapshotEndpoint(session_id, base_ptr, capacity_bytes) peer.register_send_buffer(ptr, length) peer.push(target_endpoint, local_ptr, local_off, length, remote_off=0) peer.batch_push(target, local_addrs, remote_addrs, lengths) peer.read_bytes(offset, length) -> bytes peer.close() ``` - 每个 `SnapshotPeer` 拥有自己的 `TransferEngine`,绑定 `host:port` - `receive_capacity_bytes > 0` 时分配一段 ctypes `c_ubyte` 数组 + `register_memory` - `push` 直接走 `engine.transfer_sync_write(peer_session_id, local_ptr, remote_ptr, length)` - 角色完全对称——任何 `SnapshotPeer` 既可以发送也可以接收,由 caller 决定 ### 2.2 Smoke test 双进程结构 ``` 父进程 (sender) 子进程 (receiver, subprocess.Popen) │ │ │ spawn → ──────────────────────────────►│ │ │ SnapshotPeer(recv_capacity=64MB) │ │ write endpoint.json │ read endpoint.json ◄───────────────────│ │ │ │ SnapshotPeer(no recv buf) │ │ register_send_buffer(64MB) │ │ │ │ for size in [1K, 16K, 1M, 16M, 64M]: │ │ fill_pattern(send_buf, seed) │ │ peer.push(endpoint, 0, size) ─RDMA──►│ │ │ wait signal │ write endpoint.do{size} ────────────►│ read signal seed │ │ compute expected SHA │ │ recv_bytes = peer.read_bytes │ wait endpoint.ack{size} │ compare SHA → emit JSON event │ │ write endpoint.ack{size} │ ... │ │ │ │ drain child stdout, parse JSON │ exit │ verify each event has ok=true │ ``` ### 2.3 性能(首次 smoke run) | Size | Push duration | Throughput | |---:|---:|---:| | 1 KB | 9.0 ms | 0.001 Gbps | | 16 KB | 0.037 ms | 3.5 Gbps | | 1 MB | 0.102 ms | 82 Gbps | | 16 MB | 0.577 ms | 232 Gbps | | **64 MB** | **1.70 ms** | **316 Gbps** | - 1 KB 第一次有 ~9 ms 的 mooncake p2p handshake/openSegment overhead(一次性) - 16 KB 之后是稳态,吞吐随 size 增长接近线速 - mlx5_60 是 mlx5 ConnectX-7 NDR 400 Gb(4× 100Gb lanes);64 MB 测到 316 Gbps 是 79% 的链路利用率,对单次 RDMA write 来说正常(剩余空间留给 verb dispatch / completion handling overhead) ## 3. 验收 - ✅ 5/5 size SHA 校验全部通过 - ✅ 64 MB 一次 RDMA 1.7 ms - ✅ 双进程独立,不耦合 SGLang PD pipeline - ✅ Smoke test 脚本 `scripts/smoke_snapshot_link.py` 可重跑 ## 4. 当前覆盖范围(清单) - ✅ Host CPU 内存的 D→P RDMA byte transfer (`scripts/smoke_snapshot_link.py`) - ✅ **GPU 内存** cuda:0 → cuda:1 的 D→P RDMA(`scripts/smoke_snapshot_link_gpu.py`,5/5 size 全 SHA 校验通过,256 MB 8.5 ms / 251 Gbps) - ✅ 单 IB device (mlx5_60) - ✅ 同节点 loopback(127.0.0.1) - ⏳ 跨节点(远端 IP)—— 设计上一致,未验证 - ⏳ 多 D → 单 P(多 sender → 共享 recv buffer 的 offset 协调)—— 留给 Phase 3 整合时设计 - ⏳ ZeroCopy 入 SGLang kv_pool slot —— 留给 Phase 2/3 ### GPU smoke 性能 | Size | Push duration | Throughput | |---:|---:|---:| | 16 KB | 8.27 ms (cold) | 0.016 Gbps | | 1 MB | 0.096 ms | 87.6 Gbps | | 16 MB | 0.844 ms | 159 Gbps | | 64 MB | 2.52 ms | 213 Gbps | | **256 MB** | **8.54 ms** | **251 Gbps** | GPU↔GPU 比 host↔host 慢一些(251 vs 316 Gbps for 64MB),但仍接近 mlx5_60 NDR 400Gb 的 60% 线率。对 KVC 单 session ~50K tokens × ~80 KB/token ≈ 4 GB 量级的 transfer,对应 D→P 时间约 130 ms。 ## 5. 下一步(Phase 2 / Phase 3) 详见 `docs/D_TO_P_SYNC_DESIGN_ZH.md` §5。本 phase 1 解锁后,整个 D→P 同步可以正式开始整合到 SGLang scheduler: | Phase | 描述 | 风险 | |---|---|---| | 2 | D-side commit hook:`cache_finished_req` 完成后 enqueue snapshot push | 中。需要在 scheduler 后台线程跑 push,不能阻塞 schedule loop | | 3 | P-side snapshot store + prefill bypass:P scheduler 收到 use-snapshot 请求时跳过 `model.forward()`,直接用 snapshot KV 触发 P→D' transfer | **最高**。需要深入 SGLang prefill 流程 | | 4 | agentic-pd-hybrid hook:`_invoke_kvcache_seeded_router` 先 probe P → 决定走 bypass 还是 fallback | 低 | | 5 | CLI flag + structural log | 低 | | 6 | 端到端 smoke + E4 sweep | 中 | ## 6. 知识沉淀 ### 易踩坑 | 坑 | 原因 | 修法 | |---|---|---| | 多进程 `multiprocessing.Process` 子进程崩溃信息丢失 | spawn context 下 child 没有继承 parent 的 stderr | 改用 `subprocess.Popen` + stderr 重定向到文件 | | `bytes(ctypes.c_byte * N)` 失败 `ValueError: bytes must be in range(0, 256)` | `c_byte` 是 **signed**,>= 128 的 byte 在 Python 看就是负数 | 用 `c_ubyte` 或 `ctypes.string_at(addr, length)` 做内存复制 | | 第一次 push 有 ~9ms openSegment overhead | mooncake p2p handshake lazy 建链 | 稳态忽略;如需 warm-up,提前发 1 KB pre-flight | ### mooncake API 速查 ```python engine = TransferEngine() engine.initialize(f"{host}:{port}", "P2PHANDSHAKE", "rdma", ib_device) engine.register_memory(ptr, length) # mr 注册 engine.transfer_sync_write(peer_session_id, local_ptr, remote_ptr, length) # RDMA write engine.batch_transfer_sync_write(peer_session_id, [local_ptrs], [remote_ptrs], [lengths]) engine.unregister_memory(ptr) ``` `peer_session_id` 是 `"host:rpc_port"`,其中 `rpc_port = peer_engine.get_rpc_port()`。 --- **核心句**:D→P 底层 RDMA 链路独立模块跑通,64 MB 1.7 ms / 316 Gbps,与 SGLang PD pipeline 完全解耦。Phase 2/3 可以放心在这上面叠加。