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

12 KiB
Raw Blame History

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.pyRoutingState.session_d_rejects CounterKvAwarePolicy.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_reasonpd-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-spaced-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 永久性

# 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

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

# 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 → 不计

需改 ExecutionResultadmission_reject_reason 字段,并在 fallback 链路传递。不在第一轮——先看 #1 是否够用。

6.4 v2 应保留的 v1 设计

  • 阈值 3不变
  • record_admission_reject 的子串匹配(不变)
  • 新 fallback labelssession-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. 可能还有未发现的设计 bugv2 也许还会暴露新问题
  5. 三方对比需 same trace + same scale + same ts=1baseline 已有 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