# 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` 分支从设计文档开始。
