Two cleanups:
1. Drop "E1: naive 1P3D default" experiment from the onboarding manual.
GPU hours are precious; naive 1P3D + policy=default has near-certain
loss on multi-turn cache hit (it's round-robin without prefix awareness),
so the comparison doesn't add information vs E1=naive 1P3D kv-aware.
The new manifest has only 2 runs: E1 (naive 1P3D kv-aware) + E2 (KVC
v2 + RDMA). Run-time budget drops from 16.5h serial to 11h serial /
5.5h parallel. Updated:
- §0 TL;DR ("3 组" -> "2 组")
- §2 H1 hypothesis (drop "default and kv-aware each one" -> just kv-aware)
- §3.1 experiment matrix (3 rows -> 2 rows + rationale for the drop)
- §3.2 startup config (drop E1 default section, renumber E2/E3 -> E1/E2)
- §6 decision table + expected-range table
- §7 FAQ ("3 个 E1-E3" -> "2 个 E1-E2")
- §9 deliverables
2. Move 8 deprecated docs to docs/archive/:
AGENTIC_FIT_ANALYSIS_ZH.md (ts=10 era analysis; superseded)
STRUCTURAL_VALIDATION_REPORT_ZH.md (ts=10 era validation; superseded)
KVC_DEBUG_JOURNEY_V1_TO_V5.md (v1-v5 sweep process notes)
V5_PROFILE_INVESTIGATION_ZH.md (v5 1Hz polling investigation)
REFACTOR_PLAN_ZH.md (v0 plan; superseded by V1)
KVCACHE_CENTRIC_PROGRESS_ZH.md (earliest 2026-04-27 progress)
SWEBENCH_EXPERIMENT_PROGRESS.md (early SWE trace setup)
SWEBENCH_EXPERIMENT_RESULTS.md (early SWE result snapshot)
All cross-references in active docs (V2_DEEP_ANALYSIS / V2_RESULTS /
REFACTOR_PLAN_V1 / TEAM_REPORT / ONBOARDING) rewritten from
`docs/FOO.md` to `docs/archive/FOO.md` via sed pass.
Added `docs/archive/README.md` explaining what each archived doc is
and when (if ever) to reopen it. Designed so a new reader hitting
the archive dir immediately knows it's not required reading.
After this commit the active docs in docs/ are 9 files (down from 17),
which should make the onboarding doc's "Level 1 / Level 2 / Level 3"
classification self-evident.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
20 KiB
v5+Profile 调查报告(经 critic 审计修订版)
日期: 2026-04-29(原稿)/ 2026-04-29(经审计修订)
实验配置: Qwen3-30B-A3B (TP1)、单机 8×H100 80GB、trace = qwen35-swebench-50sess.jsonl (4449 reqs / 52 sessions)、time-scale=10、concurrency=32
数据集: outputs/qwen3-30b-tp1-v5-optD-profile/(EXP1 1P7D + EXP2 2P6D,均加入 1Hz /server_info 时序采样)
v5 baseline 对照: outputs/qwen3-30b-tp1-v5-optD/(无 polling)
研究问题: v5 (Option D) 把 errors 从 9-10% 降到 0.2%,但 session-cap fallback 反而升到 46-51%。fallback / errors 究竟来自哪里。
本稿是经过 hostile audit 后的修订版。原稿包含若干结论性错误(尤其是对
held_tokens语义的解读颠倒、对 admission race 的过度归因、对 polling 副作用的轻视)。审计意见保存在本会话记录中,关键纠错以 ⚠️ 标注。
TL;DR(已修订)
- 真实容量: 每张 D 的
token_to_kv_pool_allocator.size = 92086 tokens (~92K)。⚠️ 单 turn 真实 footprint 不是 50-100K;cached_tokensp50=18K、p90=48K、p99=67K。原稿过度夸张。 other = capacity − held − available的解读已修订: ⚠️held_tokens = sum(slot.kv_allocated_len − slot.cache_protected_len)(代码:session_aware_cache.py:278-282),即"slot 拿到但不在 radix tree 保护范围内的部分"。所以other的最大单一组成很可能是 radix-tree 保护的共享前缀缓存(prefix cache) —— 这通常是想要的,不是病态浪费。原稿把other全归因为 running batch + 在途传输是错的。other的双峰分布属实(p50 ≈ 0,p90 ≈ 80K),但单凭cap−held−avail无法判断这是 radix-cache 自然累积、还是 burst 工作内存。P1 的细分 instrument 必须先做。- errors 与
other在时间上相关属实,但不能被解释为因果。同一时段的多个变量(请求并发、in-flight transfer、可用空间)都在变化;无法仅凭时序对齐推断"other吃掉了腾出来的空间"。 - EXP2 2P6D errors 9 → 415:⚠️ polling 被升级为 leading hypothesis,而非"无关"。证据:执行模式呈 ~1:1 替换(
session-cap-fb−356 /kvcache-centric+406),且/server_info不是被动读 —— 它在 scheduler 主循环内遍历每个 session slot 计算is_idle。需要 P0 三次 baseline 复跑去伪。 - errors 集中在 18 个 session 上(总共 52 个),每个 session 钉死在 1 个 D。per-D error rate 差异无法解释为 D 的结构差别,本质是 18 个"坏 session"如何被路由分配。
- v5+profile 1P7D 的延迟优于 baseline 完全在 single-run variance 范围内。N=1,不能作为任何性能结论。
1. 方法论
1.1 Instrument 改动
src/agentic_pd_hybrid/replay.py加入_query_pool_snapshot+_poll_pool_timeseries,后台 asyncio task 以--pool-poll-interval-s 1.0周期访问每个 P/D worker 的/server_info。- 每 tick 写一行 jsonl 到
<run_dir>/d-pool-timeseries.jsonl,字段:{worker_id, worker_role, session_count, resident_session_count, held_tokens, available_tokens, capacity_tokens, idle_evictable_*, sessions[], kvcache_mem_gb, last_gen_throughput, ...}。 - 分析脚本:
scripts/analysis/analyze_pool_timeseries.py。
1.2 字段定义(已修订 ⚠️)
/server_info → internal_states[0].session_cache 的来源是 session_controller.py:get_streaming_session_cache_status → tree_cache(SessionAwareCache)。
| 字段 | 真实含义 | 备注 |
|---|---|---|
held_tokens |
sum_over_slots(ceil(kv_allocated_len, page_size) − cache_protected_len) |
不是 "session 在 cache 中占用的全部";只统计slot-private、未被 radix tree 保护的部分 |
cache_protected_len |
radix tree 保护的共享前缀部分 | 多个 session 共享时只计一次 |
available_tokens |
token_to_kv_pool_allocator.available_size() |
全局 KV 池剩余空间 |
capacity_tokens |
allocator.size |
单 D 的总 KV 容量 = 92086 |
idle_evictable_tokens |
held 中可被 LRU 立即踢的部分(session 所有 req finished + streaming 模式) |
因此:
other = capacity − held − available包含但不限于:- radix-tree 保护的共享前缀 token(可能是大头) ⚠️ 原稿遗漏
- 当前 running batch 占用的 KV slots
- P→D 在途 transfer 的临时 buffer
- mooncake 已注册但尚未提交到 tree_cache 的块
- 内部碎片 / allocator 元数据
含义: 在补充 P1 instrument 之前,我们无法分辨 other 中"radix-cache"(良性)和"burst 工作集 / fragmentation"(可能病态)的比例。
1.3 配置一致性与风险
- v5+profile 与 v5 baseline 唯一差别:加了
--pool-poll-interval-s 1.0(其余 CLI 参数完全一致)。 - 两次 run 时间间隔 ~21 小时(2026-04-28 15:39/16:27 vs 2026-04-29 12:08/12:59)⚠️ 原稿误写 ~6h。同一台机,但 GPU 温度、PCIe、NUMA 分配未控制。
- N=1 比较没有统计意义;任何延迟差异 < 30% 都属于 single-run variance 合理范围。
2. 整体性能对比
| 指标 | v5 1P7D | v5+profile 1P7D | v5 2P6D | v5+profile 2P6D |
|---|---|---|---|---|
| 总 requests | 4449 | 4449 | 4449 | 4449 |
| errors | 9 (0.2%) | 6 (0.1%) | 9 (0.2%) | 415 (9.3%) |
| truncated | 42 | 43 | 42 | 42 |
| direct-to-D | 44.7% | 54.9% | 41.3% | 41.1% |
| session-cap fallback | 45.6% | 36.1% | 50.6% | 42.6% |
| no-d-capacity | 1.2% | 0.7% | 0.8% | 0.6% |
| pd-router-d-session-reseed | 4.8% | 4.3% | 3.4% | 2.9% |
| pd-router-turn1-seed | 1.2% | 1.2% | 1.1% | 1.1% |
| kvcache-centric (failed mode) | 0.2% (9) | 0.1% (6) | 0.2% (9) | 9.3% (415) |
| latency mean / p50 / p90 / p99 (s) | 5.18/1.59/14.7/26.1 | 4.21/1.18/11.3/28.8 | 3.49/1.31/9.1/24.9 | 3.23/1.11/8.4/20.3 |
⚠️ 不要从此表得出"v5+profile 改进了延迟" —— N=1 single run,且 EXP2 引入了 415 个 errors 相当于换了一种回退策略,延迟均值的下降很可能只是剔除了慢路径请求的副作用。
2.1 EXP2+profile 415 errors 解构(已修订)
Error type 分布:
| Error Type | 数量 |
|---|---|
RuntimeError: generate stream ended before producing any token |
407 |
ReadTimeout: |
8 |
⚠️ 关键约束:
- 414/415 个 error 的
kv_transfer_blocks > 0(从 metrics jsonl 验证)。这些请求已经过了 admission,P→D 传输已开始,死于下游(server-side abort、流被关、生成阶段失败)。 session_reused=False占 415/415(全部是 seed,无一是 direct append)。- 失败集中在 18 个 unique session(top 5: 58080→decode-5 66 errs / 70560→decode-2 54 / 67200→decode-4 40 / 59200→decode-4 35 / 77280→decode-2 33),每个 session 钉死在一台 D。
Per-D error rate(已修正百分比):
| Decode Worker | Errors | Total Reqs | Error Rate |
|---|---|---|---|
| decode-0 | 56 | 758 | 7.4% |
| decode-1 | 5 | 561 | 0.9% |
| decode-2 | 141 | 858 | 16.4% |
| decode-3 | 0 | 838 | 0.0% |
| decode-4 | 106 | 731 | 14.5% |
| decode-5 | 107 | 703 | 15.2% |
⚠️ 不要解读为"decode-3 健康、decode-2 病态"。每个 session 钉死在一台 D,18 个坏 session 是否落到某个 D 是路由分配的随机结果。当前 N=1 数据无法分辨"D 结构差异"与"session 分配运气"。
3. D KV pool 时序分解(EXP1 1P7D 关键结果)
每张 D capacity=92086 tokens,运行 ~2696 秒(去掉前 10% 暖机):
| Worker | mean_other | p50_other | p90_other | max_other | mean_held | mean_avail |
|---|---|---|---|---|---|---|
| decode-0 | 13599 | 63 | 77189 | 90959 | 47124 | 31363 |
| decode-1 | 21242 | 0 | 76854 | 91074 | 37024 | 33820 |
| decode-2 | 39333 | 46841 | 82782 | 91996 | 17381 | 35372 |
| decode-3 | 30543 | 15864 | 81512 | 91511 | 9584 | 51959 |
| decode-4 | 32659 | 32365 | 72995 | 92082 | 7643 | 51784 |
| decode-5 | 31745 | 20366 | 86341 | 91211 | 11305 | 49036 |
| decode-6 | 24602 | 701 | 82291 | 91000 | 20967 | 46517 |
已修订观察(去掉了原稿的过度归因):
other是双峰(p50 接近 0,p90 接近 80K,mean 在 14-39K)。这一形态属实。- 不同 D 的 mean_held / mean_other 差异巨大 —— 但⚠️ 不能直接归类为 "session-heavy" 或 "transfer-heavy",因为我们不知道
other里 radix-cache vs 工作内存的比例。P1 的拆分必做。 - 由于
held不包含 radix-protected token,mean_held低不代表该 D 上 sessions 占用少 —— 只代表它们的"slot 私有部分"少;共享前缀可能很大,完全藏在other里。
3.1 other 在某些时段持续高位(EXP1 decode-2 抽样)
| t (s) | held | avail | other | sess_count | last_gen_throughput |
|---|---|---|---|---|---|
| 3 | 0 | 92086 | 0 | 0/0 | (未抽) |
| 273 | 65310 | 26776 | 0 | 1/1 | (未抽) |
| 543 | 15296 | 76589 | 201 | 1/1 | (未抽) |
| 812 | 0 | 92086 | 0 | 0/0 | (未抽) |
| 1082 | 52507 | 39579 | 0 | 1/1 | (未抽) |
| 1351 | 40985 | 30175 | 20926 | 2/2 | (未抽) |
| 1622 | 0 | 17703 | 74383 | 0/0 | 未核 |
| 1891 | 0 | 46376 | 45710 | 0/0 | (未抽) |
| 2161 | 0 | 27667 | 64419 | 0/0 | (未抽) |
| 2430 | 0 | 62224 | 29862 | 0/0 | (未抽) |
⚠️ t=1622 之后(约 30+ tick)持续 held=0/sess=0/other≈45-74K —— 这种持久状态不是 burst 工作集的形态(burst 应是亚秒级)。更可能的解释包括:
- 一个 stuck request 的 KV 块未能正常释放
- mooncake 注册但未 commit 的 transfer buffer 滞留
- 某个 cleanup 路径未触发
未在原稿中验证 last_gen_throughput,该字段记录在 timeseries 但未对齐分析。P1 时一并补。
4. Errors 与 Saturation 时序相关性(EXP2 2P6D)
4.1 等数量 vs 等时间 decile(已修订 ⚠️)
原稿仅展示等时间分箱,有"第 10 decile 系统恢复"的视觉错觉。两种分箱并列:
| Decile | 等时间(reqs / errs / rate) | 等数量(reqs / errs / rate) |
|---|---|---|
| 1 | 567 / 0 / 0.0% | 444 / 0 / 0.0% |
| 2 | 268 / 0 / 0.0% | 445 / 0 / 0.0% |
| 3 | 517 / 0 / 0.0% | 445 / 0 / 0.0% |
| 4 | 189 / 0 / 0.0% | 445 / 0 / 0.0% |
| 5 | 662 / 3 / 0.5% | 445 / 3 / 0.7% |
| 6 | 417 / 27 / 6.5% | 445 / 28 / 6.3% |
| 7 | 486 / 39 / 8.0% | 445 / 42 / 9.4% |
| 8 | 612 / 177 / 28.9% | 445 / 114 / 25.6% |
| 9 | 486 / 128 / 26.3% | 445 / 119 / 26.7% |
| 10 | 245 / 41 / 16.7% | 445 / 109 / 24.5% |
⚠️ 第 10 decile 不是"系统恢复"。等数量分箱显示 24.5% 的 error rate,与 decile 8/9 持平。原稿"恢复"叙事是分母 245 vs 612 造成的视觉假象。
4.2 多重假设并列(已修订,不再独尊 admission race)
针对 EXP2 2P6D 415 errors 的可能机制(按当前数据强弱排序):
H1: Polling 引发 scheduler 时序扰动(leading hypothesis ⚠️)
- 证据:执行模式 1:1 替换(session-cap-fb −356 / kvcache-centric +406)。
- 证据:
/server_info进 scheduler 主循环遍历 session slot,1 Hz × 8 worker 不是 0 开销。 - 证伪条件:P0(三次 baseline EXP2 复跑)如果都得到 ~9 errors,本假设确认。
H2: v5 自身存在 admission/transfer race
- v5 baseline 也出 9 个 errors(均为 ReadTimeout),说明该 race 在 baseline 已存在,profile 是被放大了。
- 证据弱化:原稿提的 "admission race"(admit_direct_append snapshot 过期)与数据冲突 —— 414/415 errors 的
kv_transfer_blocks > 0,他们都过了 admission,死在下游。所以即便有 race,也不是发生在 admission 端,而是 P→D transfer 后 / 生成开始前。
H3: 18 个特定 session 的工作负载结构性失败
- 18/52 session 集中失败,每个 session 都是高 turn_id (median=70)。
- 这些 session 可能 input 特别长,或某种 trace 结构会触发某个特定路径。
- 证伪条件:在 P0 三次 baseline 复跑后,看是否仍是同一组 18 个 session 失败。
H4: 单次运行的 GPU/PCIe 状态扰动
- ~21 小时间隔,GPU 温度/clock 不同。
- 证伪条件:P0 三次 baseline 都 ~9 errors → 排除单次扰动主导。
⚠️ 原稿独推 admission-race(H2)是错的。当前数据无法决定 H1-H4 哪个是主因。
5. 1P7D vs 2P6D 全局对比
| Config | total decode ticks | other p50 | other p90 | other>30K freq | other>50K freq | other>70K freq | held>60K freq |
|---|---|---|---|---|---|---|---|
| 1P7D | 18865 | 663 | 79751 | 36.9% | 27.9% | 14.8% | 15.5% |
| 2P6D | 14016 | 14459 | 77199 | 43.2% | 30.4% | 13.9% | 4.8% |
⚠️ 原稿"2P6D 的 p50_other 是 1P7D 的 22 倍 → 2P 推送压力更大"过度解读。考虑分母效应:同一 trace 总工作量在 2P6D 由 6 张 D 分担 vs 1P7D 由 7 张 D 分担,单 D 受到的压力本来就更大,与 P 数无直接因果。这个数据只能说"2P6D 单 D 负担更高",不能得出"2P 在 transfer 上比 1P 更激进"。
6. 关键解读(已大幅修订)
6.1 v5 真实瓶颈尚不明确
原稿声称"瓶颈是 D 的 KV pool 在压力期被 'other' 占据"。⚠️ 此结论已撤回。给定 held_tokens 实际是 slot-private(non-tree)部分,other 的最大单一成分很可能是正常的 radix-tree 共享前缀。"被 running batch / 在途传输占据"是未经验证的猜想。需要 P1 的细分 instrument 才能给出真瓶颈。
6.2 LRU eviction 的行为暂无可靠解读
原稿基于 mean_held 在压力期"暴跌"推断 LRU 在拼命踢。但 held 实际是 slot-private 部分,session 仍可能被 radix-tree 保留;held 减少不等于 session 被 evict,可能只是 cache_protected_len 比例变化。P1 拆分前不下结论。
6.3 v5+profile 1P7D "比 baseline 快"是单次巧合
两次 run 间隔 ~21 小时(原稿误写 ~6h),GPU 温度/PCIe 状态未控制。N=1,任何性能差异 < 30% 都不可声称。
6.4 EXP2 2P6D 415 errors:polling 是 leading suspect(已升级)
原稿把 polling 列为"次要可能"。⚠️ 现在升级为主嫌疑:
- 执行模式 1:1 替换(session-cap-fb −356 / kvcache-centric +406)说明 polling 改变了 admission 走哪条路。
/server_info不是只读旁路 —— 调度内部循环 + 遍历 session slots 计算is_idle。- 必须做 P0 三次 baseline 复跑去伪;在那之前不能动 v6。
6.5 "Other" 在 P 上 90% 不是 backup blocks
prefill-0 的 SessionAwareCache 未启用(replay 数据 held=0),P 的 "other" 等于"P 全部 KV 使用量"(radix cache + running batch + 备份)。⚠️ 当前数据无法分辨 prefill-backup-policy 是不是真的释放了。需在 P 加单独的 prefill_backup_tokens 字段。
7. v6 行动项(已重排,以 P0 起步)
P0:验证 EXP2 errors=9 的可复现性(最高优先级,先做)
操作: 跑 3 次 v5 baseline EXP2(同 v5 配置,不开 polling),比较 error 分布。
- 如果 3 次都得到 ~9 errors → polling 被坐实为 415 暴涨主因。必须把 polling 改成更轻量的形式(如降低频率、改成 streaming push、或用 sidecar metrics 而非 HTTP poll)再做后续。
- 如果 3 次都得到 ~400 errors → polling 不是主因,415 是 v5 admission/transfer race + 单次 GPU 状态扰动的复合。
- 如果 3 次结果分布很广(如 9 / 50 / 400) → run-to-run variance 才是主导,任何 single-run 比较失效。
预期工程量: 1 个新 sweep 脚本(只跑 EXP2,3 次)+ ~3 × 50 min = ~2.5h GPU 时间。 风险: 0(纯重跑现有配置)。
P1:把 D 的 other 拆开打表(P0 跑的同时并行做代码)
操作: 改 SGLang scheduler.py:get_streaming_session_cache_status 与 session_aware_cache.py,在返回的 dict 里加:
radix_protected_tokens=sum(slot.cache_protected_len for slot in slots)⚠️ 这是原稿盲区,critic 暴露的关键缺失字段running_batch_tokens=sum(req.fill_ids size for req in running_batch.reqs)inflight_transfer_tokens=sum(req.size for req in disagg_decode_transfer_queue.queue)prealloc_tokens=sum(req.size for req in disagg_decode_prealloc_queue.queue)retracted_tokens=sum(req.size for req in disagg_decode_prealloc_queue.retracted_queue)last_gen_throughput(已有)更细 —— 加running_batch_size(req 数)
预期收益: other_unaccounted = capacity − held − available − radix_protected − running_batch − inflight − prealloc − retracted 应该接近 0。剩余的就是真"病态"内存。
风险: 低(纯只读 stat,不改 admission 逻辑)。
工程量: ~80 行 SGLang patch + 同步 replay.py 的 _query_pool_snapshot + analyzer。
P2:如果 P0 暴露 polling 是主因,改 polling 实现
- 选项 A:把
/server_info改成事件驱动 push(scheduler 在 step 末尾把 stats 写到环形缓冲区,polling 只读不进 scheduler 队列) - 选项 B:把 polling 频率从 1Hz 降到 5Hz/10s,在 P1 的拆分数据上验证够用
- 选项 C:scheduler 端加锁分离,把 stats 读和 admission 决策的临界区拆开
P3(条件性,等 P0+P1 数据):决定真正的优化方向
原稿 §7 的 5 条优先级在 other 模型纠正后全部需要重新评估。等真实拆分数据出来再排。
8. 局限与 Confounders(已扩充)
- ⚠️
held_tokens语义在原稿被解读颠倒,引发other的因果归因错误(已纠正,见 §1.2)。 other字段是计算所得且未细分,无法直接归因。需要 P1 instrument 才能区分 radix-cache、running batch、inflight 等。- ⚠️ EXP2+profile 的 415 errors 与 baseline 9 errors 量级差异无法 deconfound;polling 是 leading suspect 但未证实。P0 是必经步骤。
- N=1 的实验配置:任何 v5+profile vs v5 baseline 的延迟/失败差异都属于 single-run variance 合理范围,不能作为方向性结论。
- trace 是 single-shot,52 sessions × 4449 reqs 的特定结构可能放大某些路径。
capacity = 92086是token_to_kv_pool_allocator.size,来自mem_fraction_static(未抽具体值),与"H100 80GB 的物理上限"差距是 SGLang 的安全裕量。- ⚠️ §3.1 t=1622 持续高
other30+ tick 的现象 未与last_gen_throughput交叉验证;原稿"running batch + 在途传输"的解释是猜想而非证据。 - ⚠️ 18/52 失败 session 的特征(turn_id、input 长度、prefix shape)未做对比分析;不能排除某个 session 类型本来就会触发某个固定 bug。
- polling 频率 1Hz 错过亚秒级 burst ——
other的双峰可能比测到的更剧烈。 - critic 指出
pd-router-d-session-reseed在 EXP1 涨(193 vs 152)、EXP2 跌(127 vs 152)的反向移动未在原稿分析,这是 admission/路由 决策的清晰信号,应该在 P1 之后回看。
9. 后续指令(已更新顺序)
- P0: 跑
scripts/sweep_tp1_v5_baseline_rerun_exp2.sh,3 次 EXP2 baseline,无 polling。 - P1: 同时改 SGLang 把
other真正拆开。 - 完成 P0+P1 后:
- 重跑 EXP2 一次 + 新 instrument(同 polling),拿到
other拆分。 - 对比 baseline-rerun 三次的 errors 分布。
- 决定是否回退 polling、调 admission、还是攻 specific 18 个 session 的工作负载特征。
- 重跑 EXP2 一次 + 新 instrument(同 polling),拿到
- 任何 v6 代码改动(优化 admission / eviction / transfer)必须在 P0+P1 之后。
10. 数据产物
outputs/qwen3-30b-tp1-v5-optD-profile/
├── exp{1,2}_*_metrics.jsonl # 4449 行 / 实验
├── exp{1,2}_*_summary.json
├── exp{1,2}_*_pool_timeseries.jsonl # 12 MB / 10 MB
└── kvcache-centric-...20260429T{120847,125911}Z/ # 原始 run dir
outputs/qwen3-30b-tp1-v5-optD/ # baseline 对照(N=1)
└── exp{1,2}_1p7d_kvc_optD_*
# 待 P0 产生:
outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/
└── exp2_2p6d_run{1,2,3}_*
分析脚本:scripts/analysis/analyze_pool_timeseries.py(--json 拿机器可读输出)。