Confirms snapshot_link works for cuda device pointers, not just host memory. Sender on cuda:0 pushes to receiver on cuda:1 via RDMA over mlx5_60. All 5 sizes (16K, 1M, 16M, 64M, 256M) pass SHA verification. 16 KB 8.3 ms 0.016 Gbps (cold openSegment) 1 MB 0.10 ms 87.6 Gbps 16 MB 0.84 ms 159 Gbps 64 MB 2.52 ms 213 Gbps 256 MB 8.54 ms 251 Gbps (~60% NDR400 line rate) For Inferact-scale sessions (~50K tokens × ~80 KB layer-per-token = ~4 GB), this projects D→P transfer time at ~130 ms — within the "reseed-savings" envelope sketched in design doc §3.2. Files: scripts/snapshot_link_receiver_gpu.py scripts/smoke_snapshot_link_gpu.py Next: SGLang scheduler integration for D-side dump + P-side ingest.
153 lines
7.5 KiB
Markdown
153 lines
7.5 KiB
Markdown
# 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 可以放心在这上面叠加。
|