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>
12 KiB
Migration v1 实验发现:blacklist 永久性导致 thrashing
日期:2026-05-08 状态:v1 run 进行中(~23% 完成时的中期分析) 前置文档:
docs/REFACTOR_PLAN_V1_ZH.md§6.2(v1 设计)docs/TEAM_REPORT_AGENTIC_PD_HYBRID_ZH.md§2.1(§1 starvation claim)
触发:v1 实现的 session migration(rejection blacklist 机制)部署后,观测到 session-level thrashing——某些 session 在 3 个 D 之间 round-robin 高达 75-116 次。本文记录中期数据、根因诊断、v2 设计。
0. TL;DR
- v1 修复了 §1 starvation 但引入了新的 thrashing 失效模式——不是 admission 过严,是 blacklist 永久累积的设计 bug
- 核心证据:session 6880 在 decode-1 上稳定 70 turns,然后某瞬时 burst 把 reject 计数累积到阈值,被永久 blacklist,之后陷入 3-D 间 round-robin 死循环
- 85% admission 拒绝是
session-not-resident——非 D 真容量问题,而是迁移后"新 D 第一次见你"的正常语义 - v2 设计:reset-on-success 让 reject 计数在成功 turn 后清零,只有持续失败才迁移
- 深层观察:baseline 的"100% pin 但稳定"可能比"分布均匀但 thrashing"更好——糟糕的优化可能比不优化还糟
1. v1 实施回顾
1.1 改动文件
src/agentic_pd_hybrid/policies.py:RoutingState.session_d_rejectsCounter;KvAwarePolicy.migration_reject_threshold=3skip blacklisted D;degenerate fallback 选最少拒的 Dsrc/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 后崩溃:
- 某个瞬时 burst(其他 session 的活动?)让 decode-1 短暂饱和
- session 6880 在 decode-1 上连续 3 次被 admission 拒(
no-space或d-session-cap) - v1 的
state.session_d_rejects[(6880, decode-1)]累积到 3 → blacklist - policy 改选 decode-0 → 同样发生 → blacklist
- 改选 decode-2 → 同样 → blacklist
- 3 D 全部 blacklisted → degenerate fallback 在 3 D 间 round-robin
- 每次 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 1:blacklist 永久性
# 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 2:degenerate 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-resident(85% 占比,正常迁移后语义)
→ fallback 试 seed
→ seed admit ok(142/153 = 93%)→ execution_mode = pd-router-d-session-reseed-*(不计 reject)
→ seed no-space(11/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
| baseline(v0) | 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-1:reset-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-2:sliding window(如果 #1 不够)
把 Counter 改成 dict[(sess, D), deque[float]] 存最近 K 次拒绝时间戳。判断时用最近 N 秒(或 N 个 turn)内的次数。
更稳健但更复杂。若 #1 已能彻底解决 thrashing,跳过此项。
6.3 v2-fix-3:reject 类型分离(如果 #1 + #2 不够)
把 admission reason 显式传到 _run_request,区分:
no-space/session-cap/backpressure→ 计 rejectsession-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-1(reset-on-success) | 30 min |
| 4 | smoke test | 10 min |
| 5 | 完整 v2 run(KVC 1P3D ts=1 N=1) | ~5.5h |
| 6 | 三方对比:baseline / v1 / v2 | 30 min |
| 7 | 决定是否需要 v2-fix-2 / v2-fix-3 | – |
8. 三方对比预测(待数据验证)
| 指标 | baseline(v0) | v1(thrashing) | v2(self-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. 局限与未验证
- v1 中期数据 (23%) 推测:完整数据可能改变 thrashing 严重性的判断
- session 6880 trajectory 的崩溃机理是推断:基于 admission events 数据 + streak 模式,但没有直接日志证明 reject 计数何时跨阈值(需要在 v2 加 instrument 输出)
- reset-on-success 的预测效果未验证:基于"70 turn 成功" + "1-2 次瞬时 reject" 的假设;如果 burst 持续多 turn,仍可能跨阈值
- 可能还有未发现的设计 bug:v2 也许还会暴露新问题
- 三方对比需 same trace + same scale + same ts=1:baseline 已有 N=3,v1/v2 各 N=1(ts=1 确定性 → N=1 可信)
10. 给 TEAM_REPORT 和 REFACTOR_PLAN_V1 的更新建议
完成 v2 验证后:
- 在
TEAM_REPORT§3 ts=1 验证更新章节加入 §3.3 "Migration mechanism evolution: v0 → v1 → v2" - 在
REFACTOR_PLAN_V1§6.2 标注实施反思——预设的 "rejection blacklist" 设计漏掉了 reset-on-success 这条 - 在新文档
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 |