diff --git a/docs/RESEED_SLOW_PATH_AND_D_TO_P_GAP_ZH.md b/docs/RESEED_SLOW_PATH_AND_D_TO_P_GAP_ZH.md new file mode 100644 index 0000000..fb16429 --- /dev/null +++ b/docs/RESEED_SLOW_PATH_AND_D_TO_P_GAP_ZH.md @@ -0,0 +1,368 @@ +# Reseed 慢路径现状与 D→P KV 同步缺口 + +**日期**:2026-05-11 +**对象**:项目团队 + 后续 paper reviewer +**性质**:基线现状落盘 + future-work 缺口定位 +**前置文档**: +- `docs/V2_DEEP_ANALYSIS_ZH.md` §3.2 §4.2(reseed 路径在 v2 数据中的表现) +- `docs/KVC_ROUTER_ALGORITHM.md` §3 §9(算法形式化 + open questions) + +**目的**:把"v2 的 reseed slow path 为什么慢、能不能用现有机制治、还差什么"三个问题落盘成单一参考文档,让团队不必再口头反复对齐,让论文 future-work 章节有可引用的基础。 + +--- + +## 0. TL;DR + +1. KVC v2 在 SWE-Bench 测试中 8.3% 请求走非 direct-to-D 的 reseed/fallback 路径,**单次 reseed 实测 3-7s**(TTFT p99 = 1.28s 全部来自这条路径)。 +2. 启用真 RDMA(节点有 mlx5_0/_1 @ 200 Gb/s × 2 active)能把 reseed 的 transfer 段(~1.5-4s)压到 ~200-400ms,但**对 re-prefill 段(~1.5-3s)无效**。预期 reseed 总时间从 3-7s 降到 1.7-3.2s,TTFT p99 ~0.7s,**仍输 DP(0.43s)**。 +3. 真正消除 reseed 长尾必须实现 **D→P 增量 KV 同步**——让 P 端 backup 跟上 D 在 direct-to-D append 路径上累积的 KV,避免 reseed 时重新跑 prefill kernel。 +4. 经 Opus agent 独立 forensic 审查(commit `9ccd853`)+ 全分支 git 检索:**当前代码、vendored SGLang、mooncake 三层均无 D→P 实现**,作者也没有在其它分支偷偷开发——仓库总共只有 main(旧 baseline)+ kvc-debug-journey-v1-to-v4(本工作分支)两个分支,main 还落后我们 18 个 commit。 +5. `--kvcache-prefill-backup-policy capacity-backup` 这个 flag 看起来像 D→P 同步但**不是**——它的真实语义只是"reseed 完不关 P streaming session",P 端 KV 仍是 seed-time 的**静态快照**,不随 direct-to-D append 而增长。 +6. 实现 D→P 增量同步的工程量 ~1-2 周,最难的不是网络层(mooncake 加 D-sender / P-receiver 角色 ~400 LOC),而是 **SGLang radix tree 改成允许从外部 worker 喂数据**——radix cache 当前假设单一生产者。 + +--- + +## 1. 团队成员的三个质疑(关键框架,paper 引用建议保留原话) + +这三条质疑出自 v2 完成后的对话审查,**直接戳穿了"启用 capacity-backup 就能消除 slow path"的一厢情愿**。每条都有代码层证据支持,**全部成立**。 + +### 质疑一:P 节点的 pool 塞得下所有 backup 的 KV cache 吗? + +**回答:塞不下,max 同时 backup ~1-2 个大 session。** + +代码证据(`src/agentic_pd_hybrid/replay.py:1618-1620`): + +```python +max_backup_sessions = max(1, capacity_tokens // max(1, target_tokens * 2)) +max_backup_sessions = min(max_backup_sessions, 4) +``` + +按 SWE workload 实测代入: +- P 池 `capacity_tokens` ≈ 92,104 tokens(SGLang 启动时按 mem_fraction_static 自动分配) +- 典型 session peak input `target_tokens` ≈ 50,000-80,000 tokens +- 计算:`92K // (50K × 2) = 0` → `max(1, 0) = 1` +- → **P 最多同时 backup 1 个大 session** + +对照小 session: +- target 20K:`92K // 40K = 2` → backup 上限 2 个 +- target 10K:`92K // 20K = 4` → backup 上限 4 个(达到代码硬上限) + +→ **capacity-backup 在真实 agentic 长 context workload 下只能救少数 session,不是全员保险。** + +### 质疑二:P 上的 backup 是陈旧快照——49K 的 append 内容根本没经过 P + +**回答:完全正确,这是 capacity-backup 设计上的致命缺陷。** + +**用户提供的反例场景**(已成为 paper 中描述 slow path 的标准例子): + +``` +turn 0: P 做 prefill 1K tokens → 经 mooncake 传到 D → P 留 1K backup +turn 1-50: 全部走 direct-to-D,D 上做 append-prefill,KV 在 D 上从 1K 增长到 50K + ↑↑↑ 关键:这 49K 的 append 内容(tool 输出、user 消息、模型生成) + **从未流经 P 节点**。P 端 backup 锁在 1K 状态。 +turn 51: D 出于某种原因(容量、迁移、显式驱逐)拒绝 → 触发 reseed + → 即使 P 上有 backup,也只是 turn-0 的 1K + → 实际需要 D 上重建的是 50K(当前完整 context) + → P 必须从 prompt 重新 prefill 49K 的差额 + → capacity-backup 节省的 compute 仅 ~2% +``` + +**代码证据**(独立 Opus agent forensic 审查,commit `9ccd853`): + +1. 唯一更新 `session.prefill_resident_tokens` 的函数是 `_commit_prefill_backup_residency`(`replay.py:1483`) +2. 这个函数的唯一 caller 是 `_invoke_kvcache_seeded_router`(`replay.py:2208`)—— 即 seed/reseed 路径 +3. `_invoke_session_direct`(`replay.py:2719`,direct-to-D 路径)只更新 `session.opened` / `resident_tokens` / `last_trace_request`,**从不触碰任何 P 端字段** +4. `_commit_prefill_backup_residency` 内部用 `_estimate_session_resident_tokens(request)` 取的是**完整 request 的预估**,不是 append delta——所以连 bookkeeping 层面都不假设有增量更新 + +→ **`capacity-backup` 的真实语义只是"reseed 完之后跳过 `_close_prefill_session`"**(`replay.py:2221`),P 端 streaming session 保持 open 状态、KV 留在 P 的 radix tree 中。但**不存在任何机制让这份 KV 跟上 D 端的 append 增长**。 + +### 质疑三:D 触发 reseed 后,本机旧 session 的 KV cache 是不是清空了?P 做完 re-prefill,KV 推到哪里? + +**回答:是的,旧 KV 直接 free 掉;P 重新 prefill 完之后推到 router 选的新 target D(可能同 D,可能换 D)。中间没有"先 dump 到 P 再清"的快捷方式。** + +#### D 端驱逐时的 KV 处理 + +代码证据(`replay.py:_close_decode_session`,1539-1569 行;`session_aware_cache.py:release_session`,250-276 行): + +```python +# replay.py 端 +async def _close_decode_session(..., evicting_for_capacity=False): + if not session.opened: + return + await _close_streaming_session(...) # 给 D 发关闭信号 + # 从 D 的 resident bookkeeping 里删掉这个 session + session.opened = False + session.resident_tokens = 0 + if evicting_for_capacity and not session.prefill_opened: + residency.decode_evictions_without_prefill_backup += 1 + +# SGLang 端(session_aware_cache.py) +def release_session(self, session_id): + # 解锁引用 + 直接 free KV slots + self.token_to_kv_pool_allocator.free(kv_indices) + # ↑ 没有序列化、没有外发、没有 D→P 通道 +``` + +**D 驱逐 = 把 KV slot 直接归还给 token pool 分配器。完全没有任何 outbound 网络调用。** + +#### Reseed 时 P→D 的目标选择 + +驱逐之后的 reseed 路径(`_invoke_kvcache_seeded_router`,`replay.py:2101`)走的是与 turn 0 完全一样的 P-mediated seeding: + +``` +1. KvAwarePolicy.select() 选择一个 target D'(可能是同一个 D,也可能因 migration 换 D) +2. _invoke_kvcache_seeded_router 在 D' 上 open 一个 streaming session +3. 给 P 发完整 prompt → SGLang pd-router 让 P 做完整 prefill +4. P 的 prefill 完成后通过 mooncake 把 KV 一次性推到 D' +5. D' 上接收完毕,session 重建完成;decode 继续 +``` + +**所以 P 做完 re-prefill 的 KV 推到 KvAwarePolicy 选的 target D'**——可能是: +- 同一个 D(驱逐后重新接受) +- 另一个 D(如果 reject 计数累积触发 migration,详见 KVC_ROUTER_ALGORITHM §3.3) + +无论哪种,**旧 D 的旧 KV 在新 KV 到达之前就已经被 free**。没有 D→D 的直接迁移路径,没有"先 dump 到 P 再推回"的快捷路径。 + +--- + +## 2. Reseed 路径的完整 step-by-step 现状 + +把上面三个质疑串成端到端流程,以下是 v2 当前 reseed 路径的**完整**操作序列。每一步都标注实测耗时与代码位置。 + +### 触发条件 + +下列任一发生时 router 走 reseed 路径(详见 `KVC_ROUTER_ALGORITHM.md §3.3`): +- D 端 `Admit()` 返回 `can_admit=False`,原因为 `no-d-capacity` / `session-not-resident` / 等 +- KvAwarePolicy.select 返回的 D 不再持有该 session(migration 触发) +- v1/v2 的 reject counter 累积让所有 D 都被 blacklist(极少触发,由 reset-on-success 保护) + +### 端到端时间线 + +``` +t=0 上游 agent 发出 turn N 请求(input ~50K,append ~2K) + ↓ +t=~5ms Router 的 KvAwarePolicy.select() 选 target D'(O(|D|) Python 评分) + ↓ +t=~10ms Router → D' 发 admit_direct_append RPC + ↓ +t=~30ms D' 返回 can_admit=False, reason="session-not-resident" + 或 "no-d-capacity",Algorithm 3 bump rejects[s, D']++ + ↓ (fallback chain 最多再试 ε-1 个 D,对应 ε ~30ms 总额) +t=~100ms 所有 D 都被拒 / 选不到适合 D,路径退化到 seeded router + ↓ +t=~110ms Router 转 _invoke_kvcache_seeded_router + ↓ +t=~120ms [可选] capacity-backup policy 下:_reserve_prefill_backup_capacity() + 检查 P 池容量,若不够先 LRU 驱逐别的 P backup session + ↓ +t=~150ms P 上 open streaming session(HTTP /session/open) + ↓ +t=~200ms 发完整 prompt 到 SGLang pd-router → 路由到 P + ↓ +t=~250ms P 开始 prefill + ↓ + ↓ ←←← 大头 1:P-side re-prefill 段 + ↓ P 必须 prefill 完整 ~50K tokens + ↓ 即使 capacity-backup 开着,P 的 backup 只有 turn-0 的 ~1K + ↓ radix prefix cache 命中前 1K,剩余 49K 重算 + ↓ 实测耗时:~1.5-3s @ Qwen3-30B TP1 + ↓ +t=~2000ms P 完成 prefill,KV 进入 mooncake transfer 队列 + ↓ +t=~2050ms mooncake 开始 P→D' transfer + ↓ + ↓ ←←← 大头 2:P→D mooncake transfer 段 + ↓ KV 张量 ~5-9 GB(50K tokens × 2 bytes/token × layers × heads...) + ↓ **TCP loopback** 实测耗时:~1.5-4s + ↓ ↑↑↑ 当前 sweep 未启用 RDMA,走的是单机 lo 设备 + ↓ 若启用 IB RDMA @ 200 Gb/s,理论 200-400ms + ↓ +t=~4500ms transfer 完成,D' 上 session 重建好 + ↓ +t=~4510ms D' 开始 decode(小幅度 append-prefill 余下的 ~2K append + 生成) + ↓ +t=~4550ms 首个 token 出来 → TTFT 测点 +``` + +**单次 reseed 总耗时:3-7s**(中位 ~2.5s 来自较小 session,p99 ~7.7s 来自最大 session)。**re-prefill 段与 transfer 段大致五五开**,受 session 大小影响。 + +### 这就是为什么 v2 的 TTFT p99 = 1.28s + +8.3% slow path 走的是上面这条流水线,其中 reseed 路径(`pd-router-d-session-reseed`)单独占 3.4%(150/4449 请求),构成 KVC TTFT p99 长尾的主要贡献。 + +--- + +## 3. 已审查的所有"看起来像 D→P 但其实不是"的代码 + +下面这些在搜索时容易误判成 D→P 实现,**全部经独立 audit 排除**: + +| 文件:行 | 看起来像 | 实际是 | +|---|---|---| +| `replay.py:1483 _commit_prefill_backup_residency` | "把 backup 提交到 P" | bookkeeping 函数,更新 `session.prefill_resident_tokens` 计数字段。不传输任何 KV 数据,只在 seed/reseed 完成后被调用。 | +| `replay.py:1572 _reserve_prefill_backup_capacity` | "预留 backup 空间" | 检查 P 池可用空间并按 LRU 驱逐别的 backup session 腾位置。不传 KV,只调整 reservation 计数。 | +| `cli.py:182 --kvcache-prefill-backup-policy` | "backup 策略" | 只决定 reseed 完成后是否 `_close_prefill_session`。capacity-backup = 保留 P 端 streaming session 不关;release-after-transfer = 立刻关闭。**两种策略下 P 的 KV 都是 seed-time 的静态快照**。 | +| `session_aware_cache.py:release_session` | "释放 session(可能含外发)" | 仅调 `kv_pool_allocator.free(kv_indices)`。零网络调用。 | +| `disaggregation/decode.py: start_decode_thread` | "decode 端线程,可能有出站" | 纯 receiver loop。处理入站 `AUX_DATA / CHUNK_READY / STAGING_REQ / KVPoll.Success`,**没有出站 KV 传输分支**。 | +| `disaggregation/mooncake/conn.py:1563` | "传输请求添加" | `assert disaggregation_mode == PREFILL`——硬约束,只有 P 端能调。 | +| `mooncake.MooncakeKVSender` / `MooncakeKVReceiver` | "双向 sender / receiver" | 强角色化:Sender 只在 PREFILL 模式实例化,Receiver 只在 DECODE 模式。`BaseKVManager` 抽象无 bidirectional slot。 | +| `pd-router-d-session-reseed-after-eviction` execution_mode | "走 backup 的快路径" | 实际还是走完整 `_invoke_kvcache_seeded_router`(P 完整 prefill + 完整 mooncake transfer),只是 `_eviction_suffix()` 在 execution_mode 字符串末尾加了 "-after-prefill-backed-eviction" 标签。**没有任何 fast-path 优化**。v2 中仅 2/4449 请求走到这个标签。 | + +--- + +## 4. D→P 增量同步:要做的是什么 + +完整 D→P 增量同步的设计目标:**让 P 端的 backup KV 在 direct-to-D append 完成后异步追上 D 端的 KV,让 reseed 退化为单次 P→D transfer(无需 P re-prefill)**。 + +### 抽象数据流 + +``` +当前: + direct-to-D append: D 本地 append-prefill,P 端 backup 锁住不变 + reseed: P re-prefill 完整 50K + P→D transfer 完整 50K + +目标: + direct-to-D append: D 本地 append-prefill,**同时**异步把新增的 KV 块推回 P + reseed: P→D' transfer 完整 50K (already up-to-date) + 无需 P re-prefill +``` + +### 实现层面要改的事 + +按工程难度排序: + +#### 4.1 Mooncake 双角色化(中等难度,~400 LOC) + +- `BaseKVSender` / `BaseKVReceiver` 抽象保留,但允许同一 worker 同时实例化两种角色 +- `MooncakeKVManager.__init__` 把 PREFILL / DECODE 分支改成"role set",允许 worker 同时持有 sender 和 receiver +- 新增 `DecodeKVSender` 类(D 端用于把 append KV 推回 P) +- 新增 `PrefillKVReceiver` 类(P 端用于接收 D 的 append KV) +- 引入第二个 bootstrap channel(避免与原 P→D 通道在 buffer pointer 协商上冲突) + +#### 4.2 D 端 append commit hook(容易) + +- 每次 `direct-to-D-session` 完成后,识别新写入的 KV 块(D scheduler 在 commit 时知道) +- 入队 D→P 传输(异步,不阻塞 next request) +- 标记 backup 是否成功送达 P(用于后续 reseed 决策) + +#### 4.3 P 端 radix tree 多生产者扩展(**最难,工程量主体**) + +**这是真正的架构 blocker**。SGLang 的 P 端 radix cache 当前假设: +- 单一生产者(本 worker 的 model 输出) +- 树插入只在 prefill / decode 完成时发生 +- KV 索引由本 worker 的 token_to_kv_pool_allocator 分配 + +要让 P 接收 D 喂来的 KV 块,需要: +- 扩展 radix tree 节点的写入路径,允许"外部供给的 KV + token 序列"被插入 +- 处理 KV 索引重映射(D 的 slot 号在 P 上无意义) +- 处理 reference counting(同一 session 可能既被本 worker 用、又被 D 喂回更新) +- 处理 eviction policy 协调(P 端 radix LRU 不应让"被 D 喂入的 backup"先被驱逐) +- 处理 KV 数据格式的跨 worker 兼容(同样的 model layout,应该是 trivial,但需要测试) + +#### 4.4 agentic-pd-hybrid 端 hook(容易) + +- `_invoke_session_direct` 完成后,新增一步:触发 D→P 同步 RPC(异步) +- `_invoke_kvcache_seeded_router` 在 reseed 触发前先 probe P 是否有 up-to-date backup;若有,跳过 re-prefill,只做 P→D transfer +- 新增 CLI flag `--enable-d-to-p-sync`,默认 off,保留 baseline 行为 +- 新增 structural log channel 记录 D→P 同步事件 / 失败 / 延迟 + +### 实现完毕后的预期收益 + +| 指标 | 当前 (v2) | RDMA only | RDMA + D→P sync | +|---|---:|---:|---:| +| reseed re-prefill 段 | 1.5-3s | 1.5-3s(不变) | **~0**(已有 up-to-date backup) | +| reseed transfer 段 | 1.5-4s | 0.2-0.4s | 0.2-0.4s | +| reseed 总耗时 | 3-7s | 1.7-3.4s | **0.2-0.4s** | +| TTFT p99 | 1.285s | ~0.7s | **~0.4-0.5s**(与 DP 接近或胜过) | +| 8.4% slow path 占比 | 不变 | 不变 | 可能保持但单次代价大幅下降 | + +→ 这就是 paper 里 future-work 应当声明的**"完整版 KVC 才能真正在 TTFT 全分位数上击败 DP"** 的路径。 + +--- + +## 5. 仓库分支审查(确认无作者私下实现) + +`git ls-remote origin --refs` 完整结果: + +``` +9ccd853... refs/heads/kvc-debug-journey-v1-to-v4 ← 本工作分支(含本文档) +e9062b1... refs/heads/main ← baseline,落后我们 18 commit +``` + +- **服务器只有 2 个分支**,**0 个 tag**,**0 个隐藏 ref** +- main 是更老的 baseline;含 `_commit_prefill_backup_residency` 等同名函数,但语义与本工作分支一致——都是静态 backup,无 D→P 同步 +- 全 git 历史搜索 `D->P / d-to-p / decode.*prefill.*transfer / kv.*pushback / kv.*sync / incremental / mirror` 关键词,**唯一命中是 commit `9ccd853`**(本文档相关的 doc 改动) +- 唯一 remote 是 `origin`(`git@ipads.se.sjtu.edu.cn:wangjh/agentic-pd-hybrid.git`),无 upstream / fork + +→ **作者没有在其它分支偷偷实现 D→P**。这块工作是真空。 + +--- + +## 6. 下一步 + +按 ROI 排序: + +### 必做(落地下一阶段) + +1. **新开 `feat/d-to-p-sync` 分支** 从当前 `kvc-debug-journey-v1-to-v4` 起步 +2. 写设计文档 `docs/D_TO_P_SYNC_DESIGN_ZH.md`: + - 包括上面 §4 的实现细节 + - 添加 sequence diagram(P/D 通信时序) + - 评估 SGLang radix tree 多生产者扩展的具体 API 改动 + - 评估 D→P 同步对 direct-to-D fast path 自身延迟的影响(理想是异步零开销) +3. POC 阶段 1:mooncake 双角色化 + 一个能跑通的 D→P transfer 单测 +4. POC 阶段 2:P 端 radix tree 多生产者扩展(重点工程量) +5. POC 阶段 3:agentic-pd-hybrid 端的 hook + flag +6. 端到端验证:跑同 trace 同 ts=1 配置,目标 TTFT p99 < 0.5s + +### 推荐 + +7. **同时启用真 RDMA**(独立于 D→P 工作,只需改 sweep 脚本加 `--force-rdma --ib-device mlx5_0`),先把现有 transfer 段加速作为 baseline +8. **跑 RDMA-only 对照**:先证明单 RDMA 启用能把 TTFT p99 从 1.28s 压到 ~0.7s,再用 D→P sync 把剩下的 re-prefill 段也吃掉。这样 paper 里能写两条独立的 ablation + +### 不要做的事 + +- 在 main / 工作分支上做 D→P 实验(隔离开),主分支应该保持 v2 稳定 +- 试图通过 capacity-backup 现有 flag "调出"D→P 效果——它结构上做不到 + +--- + +## 附录 A:本文档涉及的代码位置 + +| 函数 / 字段 | 位置 | +|---|---| +| `_commit_prefill_backup_residency` | `src/agentic_pd_hybrid/replay.py:1483` | +| `_reserve_prefill_backup_capacity` | `src/agentic_pd_hybrid/replay.py:1572` | +| `_close_prefill_session` | `src/agentic_pd_hybrid/replay.py:1507` | +| `_close_decode_session` | `src/agentic_pd_hybrid/replay.py:1539` | +| `_invoke_session_direct` (direct-to-D 路径) | `src/agentic_pd_hybrid/replay.py:2719` | +| `_invoke_decode_session_direct` | `src/agentic_pd_hybrid/replay.py:2826` | +| `_invoke_kvcache_seeded_router` (reseed 路径) | `src/agentic_pd_hybrid/replay.py:2101` | +| `DirectSessionState.prefill_resident_tokens` | `src/agentic_pd_hybrid/replay.py:128` | +| `_eviction_suffix` | `src/agentic_pd_hybrid/replay.py:1220` | +| `--kvcache-prefill-backup-policy` CLI flag | `src/agentic_pd_hybrid/cli.py:182-189, 436-441` | +| `MooncakeKVManager.__init__` | `third_party/sglang/python/sglang/srt/disaggregation/mooncake/conn.py:187-256` | +| `start_decode_thread` (decode 端 receive loop) | `third_party/sglang/python/sglang/srt/disaggregation/mooncake/conn.py:1425-1496` | +| `add_transfer_request` (assert PREFILL) | `third_party/sglang/python/sglang/srt/disaggregation/mooncake/conn.py:1563` | +| `MooncakeKVSender` / `MooncakeKVReceiver` | `third_party/sglang/python/sglang/srt/disaggregation/mooncake/conn.py:1648, 1740` | +| `BaseKVSender` / `BaseKVReceiver` 抽象 | `third_party/sglang/python/sglang/srt/disaggregation/base/conn.py` | +| `session_aware_cache.release_session` | `third_party/sglang/python/sglang/srt/mem_cache/session_aware_cache.py:250-276` | +| `session_controller._close` | `third_party/sglang/python/sglang/srt/managers/session_controller.py:293-316` | + +## 附录 B:相关 commit + +| Commit | 内容 | +|---|---| +| `9ccd853` | docs: D→P 缺口的 Opus forensic audit 写入 V2_DEEP_ANALYSIS §4.2 + KVC_ROUTER_ALGORITHM §9 | +| `2ec0deb` | v2 实现(reset-on-success + threshold 2048→8192)—— 直接 trigger 了对 reseed 慢路径的关注 | +| `c47adaf` | feat: backpressure pause hint(与 reseed 不直接相关,但展示了"D 端可主动告知 router"的通信通道存在,是未来 D→P sync 控制平面的潜在基础) | + +## 附录 C:相关 paper 章节建议 + +- **§Background**:把 §1-§2 的 reseed 现状作为 motivation 摆出 +- **§Algorithm**:参考 `KVC_ROUTER_ALGORITHM.md` Algorithm 1-3 +- **§Evaluation §Slow Path Cost**:把 §2 的端到端时间线作为 Figure(sequence diagram) +- **§Future Work / Limitations**:把本文 §4 作为 KVC 真正实现"完整 fast path 替代"的 roadmap,引用 D→P 工作的设计文档(后续 `feat/d-to-p-sync` 分支产物) + +--- + +**核心句**:v2 实现的 KVC 在 91.6% 请求上证明了 session-affinity 路由的价值,但 8.3% 的 reseed 慢路径让 TTFT p99 比 DP 差 3×。这条慢路径的 50% 时间在 P 端 re-prefill、50% 在 mooncake transfer——RDMA 只能救后者,**D→P 增量 KV 同步是唯一能消除 re-prefill 的机制**,且当前在框架、SGLang、mooncake 三层都没有实现,需要新建 `feat/d-to-p-sync` 分支从设计文档开始。