Files
agentic-pd-hybrid/docs/MIGRATION_V1_FINDINGS_ZH.md
kzlin 2ec0debef4 feat(kvc): session migration with reset-on-success + direct-append threshold tuning
KVC v2 beats 4DP at ts=1 same-scale on 7/8 metrics:
  TTFT mean -24%, p50 -54%, p90 -64%; lat mean -0.8%, p50 -12.6%, p90 -0.7%.
  Direct-to-D rate jumped 42.8% -> 91.7%. REFACTOR_PLAN_V1 scenario C achieved.

Two-knob fix:
- reset-on-success blacklist decay: clear (sess, D) reject counter on
  successful direct-to-D path. Eliminates v1 thrashing where session 6880
  was stable on decode-1 for 70 turns then collapsed to 75 D-changes after
  cumulative transient pressure tripped the permanent blacklist.
- bump --kvcache-direct-max-uncached-tokens default 2048 -> 8192 via CLI flag.
  41% of v1 fallbacks were 'real-large-append' (>2048 token append); raising
  the threshold lets these go through the direct-to-D fast path.

Code:
- policies.py: RoutingState.session_d_rejects counter + KvAwarePolicy
  migration_reject_threshold; degenerate fallback picks least-rejected D.
- replay.py: record_admission_reject + reset-on-success in _run_request;
  _fallthrough_reason classifies turn-2+ fall-throughs as session-not-resident
  / real-large-append / etc, replacing misleading 'large-append' suffix
  (TEAM_REPORT §2.7).
- cli.py + benchmark.py: --kvcache-migration-reject-threshold flag wiring.

Docs:
- REFACTOR_PLAN_V1_ZH.md: forward-looking plan after ts=1 validation.
- MIGRATION_V1_FINDINGS_ZH.md: v1 thrashing root-cause analysis.
- V2_RESULTS_ZH.md: v2 results, scenario C achievement, attribution.
- TEAM_REPORT_AGENTIC_PD_HYBRID_ZH.md: comprehensive team report.

Scripts:
- sweep_ts1_kvc_n3_plus_dp.sh: ts=1 baseline (KVC 1P3D N=3 + 4DP CA).
- sweep_ts1_migration_v1.sh / v2.sh: validation runs.
- analyze_ts1_validation.py: 4-way comparison analyzer.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 01:18:13 +08:00

284 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Migration v1 实验发现blacklist 永久性导致 thrashing
**日期**2026-05-08
**状态**v1 run 进行中(~23% 完成时的中期分析)
**前置文档**
- `docs/REFACTOR_PLAN_V1_ZH.md` §6.2v1 设计)
- `docs/TEAM_REPORT_AGENTIC_PD_HYBRID_ZH.md` §2.1§1 starvation claim
**触发**v1 实现的 session migrationrejection blacklist 机制)部署后,观测到 session-level thrashing——某些 session 在 3 个 D 之间 round-robin 高达 75-116 次。本文记录中期数据、根因诊断、v2 设计。
---
## 0. TL;DR
1. **v1 修复了 §1 starvation 但引入了新的 thrashing 失效模式**——不是 admission 过严,是 blacklist 永久累积的设计 bug
2. **核心证据**session 6880 在 decode-1 上稳定 70 turns然后某瞬时 burst 把 reject 计数累积到阈值,被永久 blacklist之后陷入 3-D 间 round-robin 死循环
3. **85% admission 拒绝是 `session-not-resident`**——非 D 真容量问题,而是迁移后"新 D 第一次见你"的正常语义
4. **v2 设计**reset-on-success 让 reject 计数在成功 turn 后清零,只有**持续**失败才迁移
5. **深层观察**baseline 的"100% pin 但稳定"可能比"分布均匀但 thrashing"更好——糟糕的优化可能比不优化还糟
---
## 1. v1 实施回顾
### 1.1 改动文件
- `src/agentic_pd_hybrid/policies.py``RoutingState.session_d_rejects` Counter`KvAwarePolicy.migration_reject_threshold=3` skip blacklisted Ddegenerate fallback 选最少拒的 D
- `src/agentic_pd_hybrid/replay.py``_run_request` 末尾 `state.record_admission_reject(sess, D)`(基于 execution_mode 子串匹配);`_fallthrough_reason``pd-router-fallback-large-append-*` 拆成 `session-not-resident` / `real-large-append` / 等
- CLI / benchmark wiring
### 1.2 v1 假设(事后看部分错误)
- "reject 计数 + 阈值 3 = 容忍短期波动 + 持续失败迁移" ← **错**counter 永久增长导致迁移成必然
- "迁移到新 D 后 session 在新 D 稳定下来" ← **部分错**,迁移到的新 D 也很可能很快 reject
- "session-not-resident 不会触发计数" ← **大致对**,但下游 fallback 可能间接触发
---
## 2. 中期数据1023/4449 reqs~23%
### 2.1 头部指标 vs baseline
| 指标 | baseline kvc_1p3d_run1 | v1中期 |
|---|---:|---:|
| Per-D 调用分布 | 1502/1445/1502±3.8%| 796/785/779**±1.1%**,更均衡)|
| Per-D 峰值 token_usage | 0.99/0.99/0.99 | 0.31/0.30/0.00**容量充裕**,未顶到 1.00|
| KVTransferError | 5全程| 6中期趋势相近|
| 已见 sessions | 52全程| 29中期|
**好的方面**
- 负载均衡度跃升±26%→±1.1% if normalized
- D 容量从未饱和——§2 假设的"D drain time"机制配合 ts=1 充分发挥
- 0 sessions 永久 stuck 在饿死状态
### 2.2 Migration 触发情况(已见 29 sessions
| 类别 | 数量 | 占比 |
|---|---:|---:|
| 仍 pin 在 1 个 D | 9 | 31% |
| 触碰 2 个 D | 3 | 10% |
| **触碰所有 3 个 D** | **17** | **59%** |
**D-切换次数分布**
- mean = 26 次/session
- median = 16 次
- **max = 116 次**
- 15 sessions 切换 >10 次(明显 thrashing
- **6 sessions 切换 >50 次**(严重 thrashing
---
## 3. 根因诊断session 6880 的轨迹
### 3.1 数据
```
turn 0-70: 全部在 decode-1 (71-turn 稳定 streak) ← §1 baseline 行为
turn 71-150: 在 3 个 D 间剧烈 thrashing
decode-0: 26 个短 streak
decode-1: 25 个短 streak
decode-2: 25 个短 streak
平均 streak 长度 = 2 turns
total streaks = 76
```
### 3.2 解读
**前 70 turn 完美稳定**session 6880 在 decode-1 上正常运行 70 个 turn每次都成功是 baseline §1 "100% pin" 的复现——稳定但不公平(其他 session 没分到 decode-1 的资源)。
**第 71 turn 后崩溃**
1. 某个瞬时 burst其他 session 的活动?)让 decode-1 短暂饱和
2. session 6880 在 decode-1 上连续 3 次被 admission 拒(`no-space``d-session-cap`
3. v1 的 `state.session_d_rejects[(6880, decode-1)]` 累积到 3 → blacklist
4. policy 改选 decode-0 → 同样发生 → blacklist
5. 改选 decode-2 → 同样 → blacklist
6. **3 D 全部 blacklisted** → degenerate fallback 在 3 D 间 round-robin
7. 每次 round-robin 又触发新 reject → 计数继续涨 → 永远在 thrashing 死循环
### 3.3 admission 数据交叉验证
中期 1932 admission events 解构:
| mode × can_admit × reason | count |
|---|---:|
| `direct_append, True, None` | 1721成功|
| `direct_append, False, session-not-resident` | **62** |
| `seed, True, None` | 142成功|
| `seed, False, no-space` | **11** |
**只有 11 个 "no-space" 才是真容量拒绝**(占总 admission 的 0.6%。62 个 "session-not-resident" 是迁移后"新 D 第一次见你"的正常语义。
但因为 v1 用 `_is_admission_rejection_mode` 通过 execution_mode 子串匹配,下游 fallback chain 会把 `session-not-resident` 也间接累积到计数器fallback 链路本身可能触发 session-cap
---
## 4. 设计 bug 三层
### 4.1 Bug 1blacklist 永久性
```python
# policies.py 当前实现
if rejects >= self.migration_reject_threshold:
continue # skip this D forever
```
`session_d_rejects[(sess, D)]` 是单调递增 Counter。一旦达到阈值**永远**被 skip。但 D 的容量是动态的——70 个 turn 后短暂饱和不代表它后续不能服务这个 session。
### 4.2 Bug 2degenerate fallback 加剧问题
当所有 D 都被 blacklist
```python
best_decode_worker_id = min(
(w.worker_id for w in topology.route_workers),
key=lambda wid: state.session_d_rejects.get((sess, wid), 0),
)
```
选"最少被拒"的 D。但每次 fallback 又增加该 D 的计数 → 下次选另一个 D → 形成完美 round-robin永远走不出 thrashing。
### 4.3 Bug 3信号归并粗糙
`_is_admission_rejection_mode` 子串匹配 `session-cap` / `no-d-capacity` / `d-backpressure`,但执行链路可能这样:
```
direct_append → session-not-resident85% 占比,正常迁移后语义)
→ fallback 试 seed
→ seed admit ok142/153 = 93%)→ execution_mode = pd-router-d-session-reseed-*(不计 reject
→ seed no-space11/153 = 7%)→ execution_mode = pd-router-fallback-X-no-d-capacity计 reject
```
绝大多数 fallback 不会触发 reject 计数。但 thrashing 一旦开始,很容易踩到那 7% no-space 路径calculator 增长一次。15+ 次 thrashing 后,单 D 计数累到 3 完全可能。
**所以设计 bug 不在信号粗糙,而在永久累积 + degenerate round-robin。**
---
## 5. 深层观察:稳定 vs 公平的 trade-off
| | baselinev0| v1 |
|---|---|---|
| 公平性 | 18/52 永久饿死 | 0 永久饿死 |
| 稳定性 | 100% pin结构稳定| 6/29 严重 thrashing |
| Per-D 负载均衡 | ±26% | ±1.1% |
| 大 session 体验 | 慢但稳定(每 turn 都走 fallback ~1.0s| 不稳定 + 频繁 D 切换 + 丢 KV state |
**预想反直觉的结果**v1 在头部指标per-D 均衡)赢,但在 session 体验可能输——
- baseline 的 fallback 路径有稳定 ~1s latency
- v1 的 thrashing session 每次 D 切换都 close 旧 session、丢 KV、新 D 上重新建立——有可能 latency 反而更高
需要等 run 结束的 lat mean / TTFT mean 数据验证。**糟糕的优化可能比不优化还糟。**
---
## 6. v2 设计
按 ROI 排序的修复层。**先做 #1,验证后再决定是否需要 #2/#3**。
### 6.1 v2-fix-1reset-on-success最高 ROI
```python
# replay.py _run_request 末尾,在 state.finish 后
if execution.execution_mode == "kvcache-direct-to-d-session":
# 这次 direct-to-D 成功 = D-X 仍能服务这个 session
# 清零累积的 reject 计数(消除永久 blacklist
state.session_d_rejects[(request.session_id, decision.decode_worker_id)] = 0
```
**预测效果**
- session 6880 在 decode-1 上 70 个成功 turn 把计数反复清零
- 即使中间出现 1-2 次瞬时 reject下次成功立刻清零
- 只有**持续**失败reject 后 reject 后 reject没有夹杂 success才能累到阈值
- 真饿死的 session如 35680/39360 input >92K才会触发迁移
**工程量**~5 行代码 + 1 个 smoke + 1 个完整 run~5.5h
### 6.2 v2-fix-2sliding window如果 #1 不够)
`Counter` 改成 `dict[(sess, D), deque[float]]` 存最近 K 次拒绝时间戳。判断时用最近 N 秒(或 N 个 turn内的次数。
更稳健但更复杂。**若 #1 已能彻底解决 thrashing跳过此项。**
### 6.3 v2-fix-3reject 类型分离(如果 #1 + #2 不够)
把 admission reason 显式传到 _run_request区分
- `no-space` / `session-cap` / `backpressure` → 计 reject
- `session-not-resident` → 不计
需改 `ExecutionResult``admission_reject_reason` 字段,并在 fallback 链路传递。**不在第一轮**——先看 #1 是否够用。
### 6.4 v2 应保留的 v1 设计
- 阈值 3不变
- `record_admission_reject` 的子串匹配(不变)
- 新 fallback labels`session-not-resident` 等)(不变)
- degenerate fallback 选最少拒的 D不变但因为 reset-on-success 几乎不会触发到此分支)
---
## 7. 实验计划
| 阶段 | 动作 | 时间 |
|---|---|---|
| 1 | 等 v1 run 完成ETA ~16:30| 自然 |
| 2 | 跑 analyzer 量化 v1 thrashing 实际代价 | 5 min |
| 3 | 实现 v2-fix-1reset-on-success| 30 min |
| 4 | smoke test | 10 min |
| 5 | 完整 v2 runKVC 1P3D ts=1 N=1| ~5.5h |
| 6 | 三方对比baseline / v1 / v2 | 30 min |
| 7 | 决定是否需要 v2-fix-2 / v2-fix-3 | |
---
## 8. 三方对比预测(待数据验证)
| 指标 | baselinev0| v1thrashing| **v2self-healing 预测)** |
|---|---:|---:|---:|
| Errors | 5 | ? | 2-5仅 35680/39360 等真容量超限)|
| Per-D 均衡 | ±26% | **±1.1%** | ±5-10%(部分 pin 仍 sticky|
| Direct-to-D rate | 42.8% | ?(可能因 thrash 反而下降)| **65-75%**(持续 affinity转换 §1 fallback|
| Lat mean | 1.574s | ?(可能因 thrash 上升)| **1.30-1.45s**(达到 4DP 1.443s 水平)|
| TTFT mean | 0.244s | ? | **0.10-0.15s** |
| 最大 D-switches/session | 0 | 116 | <10仅真饿死 session|
| Sessions 永久饿死 | 18 | 0 | 2-3仅真容量超限|
预测核心v2 应该结合 baseline 的稳定性70-turn streak 应保留+ v1 的公平性无永久饿死消除 v1 thrashing 副作用
---
## 9. 局限与未验证
1. **v1 中期数据 (23%) 推测**完整数据可能改变 thrashing 严重性的判断
2. **session 6880 trajectory 的崩溃机理是推断**基于 admission events 数据 + streak 模式但没有直接日志证明 reject 计数何时跨阈值需要在 v2 instrument 输出
3. **reset-on-success 的预测效果未验证**基于"70 turn 成功" + "1-2 次瞬时 reject" 的假设如果 burst 持续多 turn仍可能跨阈值
4. **可能还有未发现的设计 bug**v2 也许还会暴露新问题
5. **三方对比需 same trace + same scale + same ts=1**baseline 已有 N=3v1/v2 N=1ts=1 确定性 N=1 可信
---
## 10. 给 TEAM_REPORT 和 REFACTOR_PLAN_V1 的更新建议
完成 v2 验证后
1. `TEAM_REPORT` §3 ts=1 验证更新章节加入 §3.3 "Migration mechanism evolution: v0 v1 v2"
2. `REFACTOR_PLAN_V1` §6.2 标注实施反思——预设的 "rejection blacklist" 设计漏掉了 reset-on-success 这条
3. 在新文档 `docs/POLICY_DESIGN_PRINCIPLES_ZH.md` 提炼出原则"任何会累积的代价机制必须配 healing/decay 机制否则会陷入 self-amplifying 失效模式"
---
## 附录 A本文数据来源
| 章节 | 数据源 |
|---|---|
| §2 | `outputs/qwen3-30b-tp1-ts1-migration-v1/kvcache-centric-*/` 中期日志 |
| §3.1 | `structural/session-d-binding.jsonl` turn 序列 |
| §3.3 | `structural/admission-events.jsonl` mode/reason 交叉表 |
## 附录 B相关代码位置
| 内容 | 位置 |
|---|---|
| RoutingState.session_d_rejects | `src/agentic_pd_hybrid/policies.py:46` |
| KvAwarePolicy.select 跳过 blacklisted D | `src/agentic_pd_hybrid/policies.py:155-162` |
| Degenerate fallback 选最少拒的 D | `src/agentic_pd_hybrid/policies.py:184-192` |
| record_admission_reject 触发位置 | `src/agentic_pd_hybrid/replay.py:359-364`_run_request |
| _is_admission_rejection_mode 子串集合 | `src/agentic_pd_hybrid/replay.py` `_ADMISSION_REJECTION_SUBSTRINGS` |
| _fallthrough_reason 分类 | `src/agentic_pd_hybrid/replay.py` `_fallthrough_reason` |