Files
agentic-pd-hybrid/docs/archive/V5_PROFILE_INVESTIGATION_ZH.md
kzlin 7590e55189 docs: archive deprecated docs to docs/archive/, drop E1 from onboarding
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>
2026-05-11 22:40:35 +08:00

20 KiB
Raw Blame History

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(已修订)

  1. 真实容量: 每张 D 的 token_to_kv_pool_allocator.size = 92086 tokens (~92K)⚠️ 单 turn 真实 footprint 不是 50-100K;cached_tokens p50=18K、p90=48K、p99=67K。原稿过度夸张。
  2. 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 + 在途传输是错的。
  3. other 的双峰分布属实(p50 ≈ 0,p90 ≈ 80K),但单凭 capheldavail 无法判断这是 radix-cache 自然累积、还是 burst 工作内存。P1 的细分 instrument 必须先做
  4. errors 与 other 在时间上相关属实,但不能被解释为因果。同一时段的多个变量(请求并发、in-flight transfer、可用空间)都在变化;无法仅凭时序对齐推断"other 吃掉了腾出来的空间"。
  5. 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 复跑去伪。
  6. errors 集中在 18 个 session 上(总共 52 个),每个 session 钉死在 1 个 D。per-D error rate 差异无法解释为 D 的结构差别,本质是 18 个"坏 session"如何被路由分配。
  7. 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_infointernal_states[0].session_cache 的来源是 session_controller.py:get_streaming_session_cache_statustree_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_statussession_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(已扩充)

  1. ⚠️ held_tokens 语义在原稿被解读颠倒,引发 other 的因果归因错误(已纠正,见 §1.2)。
  2. other 字段是计算所得且未细分,无法直接归因。需要 P1 instrument 才能区分 radix-cache、running batch、inflight 等。
  3. ⚠️ EXP2+profile 的 415 errors 与 baseline 9 errors 量级差异无法 deconfound;polling 是 leading suspect 但未证实。P0 是必经步骤
  4. N=1 的实验配置:任何 v5+profile vs v5 baseline 的延迟/失败差异都属于 single-run variance 合理范围,不能作为方向性结论
  5. trace 是 single-shot,52 sessions × 4449 reqs 的特定结构可能放大某些路径。
  6. capacity = 92086token_to_kv_pool_allocator.size,来自 mem_fraction_static(未抽具体值),与"H100 80GB 的物理上限"差距是 SGLang 的安全裕量。
  7. ⚠️ §3.1 t=1622 持续高 other 30+ tick 的现象 未与 last_gen_throughput 交叉验证;原稿"running batch + 在途传输"的解释是猜想而非证据。
  8. ⚠️ 18/52 失败 session 的特征(turn_id、input 长度、prefix shape)未做对比分析;不能排除某个 session 类型本来就会触发某个固定 bug。
  9. polling 频率 1Hz 错过亚秒级 burst —— other 的双峰可能比测到的更剧烈。
  10. critic 指出 pd-router-d-session-reseed 在 EXP1 涨(193 vs 152)、EXP2 跌(127 vs 152)的反向移动未在原稿分析,这是 admission/路由 决策的清晰信号,应该在 P1 之后回看。

9. 后续指令(已更新顺序)

  1. P0: 跑 scripts/sweep_tp1_v5_baseline_rerun_exp2.sh,3 次 EXP2 baseline,无 polling。
  2. P1: 同时改 SGLang 把 other 真正拆开。
  3. 完成 P0+P1 后:
    • 重跑 EXP2 一次 + 新 instrument(同 polling),拿到 other 拆分。
    • 对比 baseline-rerun 三次的 errors 分布。
    • 决定是否回退 polling、调 admission、还是攻 specific 18 个 session 的工作负载特征。
  4. 任何 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 拿机器可读输出)。