Files
agentic-pd-hybrid/docs/TEAM_REPORT_AGENTIC_PD_HYBRID_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

30 KiB
Raw Permalink Blame History

agentic-pd-hybrid 现框架性能与结构性问题报告

对象:项目团队同学 前置假设:读者没看过 v3-v6 KVC 实验日志 数据范围:项目仓库 outputs/ 下截止 2026-05-06 的全部实验产物 目的:把"现状"和"问题"分别交代清楚,给后续改造提供共同事实基础


0. 给没看过实验的读者:基础概念速览

0.1 项目目标

验证 session-aware / KV-cache-aware P/D routingagentic coding workload(多轮 session、长 context、增量 append上能否降低端到端延迟。基线对比对象是 vanilla SGLang xPyD。

0.2 三种部署机制(这三个名词全程会用

机制 形态 KV 流向
pd-disaggregation"PD disagg" P 和 D 是独立进程、分占不同 GPU 每个请求 P 算 prefill → mooncake 推 KV → D 解码
pd-colo"DP"data-parallel 没有 PD 拆分N 个独立完整 worker每个自己 prefill+decode 没有 KV transferrouter 按 hash 分配请求
kvcache-centric"KVC" 部署形态同 PD disaggD 上多了 SessionAwareCache,能跨 turn 保留 session KV 运行时决策:可走 direct-to-D无 P、可走 P→D disagg、可走带 reseed 的混合

Direct-to-D"D-direct"KVC 的快路径——D 上已有该 session 的 KV新 turn 在 D 本地做 append-prefill零 P 介入、零 mooncake transfer。这是 KVC 理论上能省时间的核心。

FallbackKVC admission 拒了 / 阈值不满足 / D 不健康时,退化到普通 PD disagg 路径。

Routing policy(与机制正交):

  • default:纯 round-robin
  • stickyturn 2+ 黏到 session 的 last D
  • kv-aware:按 hash overlap + sticky 评分选 DKVC 必须配它才能正确工作)

0.3 数据来源

  • Traceoutputs/qwen35-swebench-50sess.jsonlSWE-Bench 抽样4449 reqs / 52 sessions / 每 session 8-150 turns / time-scale=10 / concurrency=32
  • 模型Qwen3.5-35B-A3B (TP4) 和 Qwen3-30B-A3B (TP1) 两组
  • 硬件:单机 8×H100 80GBmooncake TCP loopback 模拟 P→D 传输

第一部份:性能数据现象

1.1 三种机制在 Qwen3.5-35B (TP4) SWE 50sess 上的表现

来源:outputs/swebench-exps/

Run Mechanism Policy Errors Lat mean Lat P50 Lat P99 TTFT mean TTFT P50
pd-disaggregation-default-20260426T202540Z pd-disagg default 0/4449 1.66s 0.97s 7.68s 0.45s 0.34s
pd-colo-default-20260426T210129Z pd-colo default 4447/4449
pd-colo-default-20260427T033519Z pd-colo default 0/4449 1.77s 0.86s 9.67s 0.29s 0.25s
pd-colo-kv-aware-20260427T042034Z pd-colo kv-aware 469/4449 1.52s 0.82s 8.27s 0.26s 0.23s
pd-colo-kv-aware-20260427T044944Z pd-colo kv-aware 0/4449 1.57s 0.81s 8.48s 0.22s 0.17s
kvcache-centric-default-worker-admission-20260426T210800Z KVC default 4390/4449

现象解读

(1) pd-disagg 是稳定基线1.66s mean / 0 errors / 4199 cache hits94.4%)。可以正常服务。

(2) pd-coloDP有两次 run第一次几乎全 crash第二次稳定

  • 04-26 的 4447/4449 errors 来自 SGLang --disaggregation-mode null + Qwen3.5-35B-A3BMamba/GDN hybridtoken_to_kv_pool_allocator memory leak bugcrash 了
  • 04-27 的两次 pd-colo run 都跑通了。pd-colo-kv-aware-20260427T044944Z 是这一组实验里跑分最好的配置——0 errors / TTFT P50 = 0.171spd-disagg 的 50%

(3) KVC 在 SWE 35B 上的唯一一次 run 几乎全 crash4390/4449 = 98.7% errors。但那 56 个跑通的 direct-to-D 请求性能优异——Lat mean 1.24sTTFT P50 0.081sKV transfer 196 块vs PD disagg 的 105K 块,99.8%)。说明 KVC 机制本身有效,但 admission control 把绝大多数请求过滤掉了。

一句话:在 Qwen3.5-35B 上,pd-colo + kv-aware 是头名KVC 机制配置不当几乎不可用。


1.2 同 trace 切到 Qwen3-30B (TP1)v1→v6 演进

为绕开 Mamba 模型的 SGLang bug团队后续切到 Qwen3-30B-A3B (TP1) 跑 KVC 调优 sweep。所有结果用同一份 SWE 50sess trace,可以横向比较。来源:outputs/qwen3-30b-tp1-* 各目录。

1.2.1 各版本配置概览

版本 关键改动(一句话)
v2 KVC + --policy default(这个 policy 选择 是 bug,下文 §2.5
v3 KVC + --policy kv-aware
v4 v3 + replay 端 session soft_cap 从 4 抬到 16
v5 (Option D) 把 admission 决策从 replay 估算改成 D worker 真实容量回答(worker-mode admission
v5+profile v5 + 1Hz /server_info polling 做时序 instrument
v6 P0 v5 baseline 同配置 rerun ×3 验证可复现性

1.2.2 各版本同 trace 结果总表

版本 Errors Lat mean Lat P50 Lat P90 Lat P99 TTFT P50 direct-to-D%
8-way DP cache-aware 0 1.43s 0.65s 3.61s 8.37s 0.093s
v3 1P7D KVC 363 (8.2%) 4.88s 1.75s 12.67s 28.72s 0.363s 39%
v3 2P6D KVC 9 (0.2%) 3.58s 1.52s 9.23s 18.70s 0.328s 31%
v4 1P7D cap=16 435 (10%) 4.21s 1.08s 13.38s 24.45s 0.056s 49%
v4 2P6D cap=16 403 (9%) 2.51s 0.84s 6.51s 18.34s 0.051s 53%
v5 1P7D Option D 9 (0.2%) 5.18s 1.59s 14.67s 26.09s 0.207s 45%
v5 2P6D Option D 9 (0.2%) 3.49s 1.31s 9.09s 24.92s 0.244s 41%
v5+profile 1P7D 6 (0.1%) 4.21s 1.18s 11.33s 28.83s 0.060s 55%
v5+profile 2P6D 415 (9.3%) 3.23s 1.11s 8.36s 20.26s 0.168s 41%
v5 rerun ×3无 profile 372 / 912 / 396 3.003.50s 0.941.22s 7.688.65s 18.9720.37s 0.070.18s 40-42%

8DP CA 在每一项指标都是头名

  • Latency mean 比所有 KVC 配置好 +43%~+260%
  • TTFT P50 0.093sKVC 最佳 v4 2P6D 是 0.051s——TTFT 单项 KVC 是有优势的,但被整体 P99 灾难抵消)
  • 0 errorsKVC 任一配置 errors 在 9-912 之间漂移)

1.2.3 v5+profile 的诡异:加 1Hz polling 让 errors 从 9 涨到 415

这条单独看v5 baseline 跑出来 9 errors加上 1Hz /server_info polling 之后 415 errors46×)。原因机理见 §2.5。

1.2.4 v6 P0 用 ×3 rerun 验证可复现性,结果是不能复现

关键事实v5 baseline 完全相同配置跑 3 次:

Run Errors Lat mean Lat P50 TTFT P50
rerun1 372 3.50s 1.11s 0.147s
rerun2 912 3.00s 0.94s 0.071s
rerun3 396 3.42s 1.22s 0.183s

errors 漂移 2.5×372→912。Latency mean / P50 也漂移 ~30%。这意味着 v3-v6 之前所有"single-run"对比的差异 < 30% 的都不可信。

但要注意:3 次 v5 中最优的 P500.94s)仍然比 8DP CA0.65s)慢 1.45×——这个差距大于 single-run variance所以"DP 全胜 KVC"的头条结论不受 variance 影响。

1.2.5 一个有趣的反差v4 vs v5

  • v4errors 多(~10%、direct-to-D 占比高53-58%)、整体 P50 较好0.84s
  • v5errors 少0.2%、direct-to-D 占比降低41-45%)、整体 P50 反而退步1.31s

v5 没有让性能变好,只是把"硬错误"转成了"诚实拒绝"——v4 的 admission 是乐观估算admit 进来后 D 装不下变成 mooncake 32s timeout统计成 errorsv5 让 D 自己拍板admit 拒得早,请求改走 fallback统计成低 direct-to-D 率)。容量本身没变。


1.3 microbench 上 KVC 击败 PD disagg —— 但本仓库没保留实际 run

docs/PROJECT_OVERVIEW.md 写明:

micro-benchmark 上,kvcache-centric 可以比 pd-disaggregation 好。原因很简单:session 少、D KV 放得下turn2+ 可以直接走 D session。

outputs/没有 microbench 实际 run只有 microbench trace 生成器 microbench.py 和它的几个示例 trace 文件)。所以 microbench 的"KVC 赢"是基于设计预期 + 历史口口相传,没有可重现的产物

这本身是个问题——下文 §2.6 会解释 microbench 的默认参数4 sessions × 30K input × 1K append正好把所有 KVC 失效条件都规避掉了。


1.4 头条结论Part 1 总结)

工作负载 / 模型 头名机制 KVC 表现
Microbench8 session × 30K × 1K append KVC > PD disagg无落地数据按设计 设计上必然赢
SWE 35B (TP4) pd-colo + kv-aware1.57s mean, 0 errors KVC 唯一 run 中 98.7% errors
SWE 30B (TP1) 8-way DP cache-aware1.43s mean, 0 errors KVC 6 个配置全输;最佳的 v4 2P6D 慢 75%、errors 9%

真实 agentic 工作负载SWE-BenchKVC 机制目前没有任何配置能跑赢 naive DP cache-aware。


第二部份:结构性问题分析

每条按 (1) 现象(实锤数据)、(2) 根因(代码位置)、(3) 影响量化 三段交代。

2.1 KvAwarePolicy 不感知 D 容量 + Session 永久 pin 在初始 D 上 ★ 最严重

2.1.1 现象(实锤)

(a) 每个 session 整 run 中只访问 1 个 D——基于 v5 rerun1/2/3 全部 4449×3 = 13347 条 metrics

Run sessions avg distinct-D-per-session
rerun1 52 1.00
rerun2 52 1.00
rerun3 52 1.00

3 次独立 run、156 次 session 实例,没有一个 session 跨 D 迁移过。

(b) Direct-to-D 命中率呈极端双峰——以 rerun1 为例(其他两次形态相同):

direct-to-D rate session 数
020%"饿死" 15
2040% 7
4060% 11
6080% 5
80100%"顺利" 14

中间档稀少,两端拥挤。

(c) 跨 3 次 run 一致饿死的 session = 13/52且这些 session 的 input 是顺利 session 的 1.98×

13 sessions starved (<20% direct-to-D) in ALL 3 runs
  avg peak input of consistently-starved sessions: 62043 tokens
  avg peak input of consistently-lucky sessions:   31344 tokens

结构性、可复现、与 session 大小强相关。 排除"运气"假说。

2.1.2 根因(代码)

policies.py:166-172 KvAwarePolicy.select() 评分函数:

score = (
    overlap + sticky * self.sticky_bonus,    # 主项:历史 KV overlap
    sticky,                                   # 二级
    inflight_penalty,                         # 三级
    assignment_penalty,                       # 四级
)

评分中完全没有 D 当前容量项

session X 第一次落到 D-2 → 在 D-2 上积累 hash_id → 之后不管 D-2 多满X 的 turn N+1 的 overlap 在 D-2 上仍是最大 → 永远选 D-2。即使 D-5 全空也轮不到。

RoutingState.decode_resident_blocks (policies.py:46) 还从不缩减——但因为 SWE trace 的 hash_ids 是 session-unique不缩减并不影响"选对 D",只影响内存——真正问题在评分函数无容量项。

2.1.3 影响量化

  • 25%13/52的 session 几乎每个 turn 走 fallback 路径
  • fallback 路径 mean lat 约 3.5s vs direct-to-D ~0.5s——饿死 session 每 turn 慢 6×
  • 这 13 个 session 还容易撞 mooncake 32s timeout见 §2.2、§2.3P99 完全由它们决定
  • SLO 视角下25% 的用户体验是系统性糟糕

2.2 D 端 LRU 只能 evict idle session → 跟不上压力

2.2.1 现象(实锤)

来源:outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/.../logs/decode-{0..5}.log,全 run 计数:

D worker "Trimmed decode session cache" 事件 KVTransferError 峰值 token_usage
decode-0 9 0 0.99
decode-1 43 4 0.99
decode-2 16 153 0.97
decode-3 37 29 0.99
decode-4 28 90 1.00
decode-5 30 93 1.00

所有 6 个 D 都顶到 token_usage ≥ 0.972 个顶到 1.00KV 池完全耗尽。LRU 触发 9-43 次远不够——transfer 错误是 LRU 触发量的 5-10×

decode-2 极端trim 16 次 vs error 153 次 = LRU 跑得比错误慢 9.5×。

2.2.2 根因(代码)

scheduler.py:2040evict_idle_streaming_sessions_lru 实际只能 evict

所有 req 都 finished + streaming 模式 + 该 session 没有 inflight transfer

但 SWE 高并发concurrency=32 + time-scale=10 → effective inter-turn gap p50=0.25s)下,每个 session 几乎一直有 inflight req。hot session 永远不 idleLRU 永远找不到东西可踢。

2.2.3 影响量化

  • 单 run 累计 KVTransferError6 个 D 之和 = 369 次
  • 对应 ~8% 请求失败率v5 errors 9/372/912 三次平均 ~430/4449 = 9.7%
  • 每次 mooncake timeout = 32s——直接构成 P99 18-26s 的尾巴

修复需要 SGLang 内部分层 eviction除 idle session 外,按访问频率 / 时序加权强制 retract——不在当前 KISS 边界


2.3 没有 D → Replay backpressure 通道

2.3.1 现象

§2.2 数据显示 D 顶到 token_usage=1.00 时仍在持续接收新请求,最终撞 mooncake 32s timeout。整个错误链路里没有"D 过载,请慢点发"的反向信号

定量证据rerun1 的 KVTransferError 时间分布——98% 集中在 run 后半段(参考 KVC_DEBUG_JOURNEY_V1_TO_V5.md §v4。前期 D 容量充裕时正常,达到上限后所有后续请求集中失败——典型的"无 backpressure 系统在过载点雪崩"模式。

2.3.2 根因(代码)

链路:

replay 端按 trace 时序 + concurrency=32 持续发请求
  ↓
PD Router 裸 round-robin (pd_router.py:43-49)
  ↓
P 收到请求做 prefill → mooncake 推 KV → D 端
  ↓
D 端 transfer queue 堆积 → 32s timeout
  ↓
errno 抛回 replay → fallback 路径,但 concurrency 不降

D 端的 admit_direct_append 响应里只有 can_admit/reason 等过去时字段,没有任何"建议节流"的指示

2.3.3 修复(本次代码改动已实现)

代码已加 recommended_pause_ms 字段:

  • third_party/sglang/.../io_struct.py:DirectAppendAdmissionReqOutput 增加 recommended_pause_ms: int = 0
  • scheduler.py:_compute_backpressure_pause_hint:按 transfer_queue_depthretracted_queue_depthtoken_usage_after 计算
  • replay.pyadmission 响应里读到 hint → 更新 DecodeResidencyState.pause_until_s[D] → 下次发到该 D 之前 sleep
  • CLI flag--enable-backpressure(默认 off保留 baseline 行为)
  • 同时新增 3 个结构性日志(structural/admission-events.jsonl / backpressure-events.jsonl / session-d-binding.jsonl

待 GPU smoke 验证。预期 errors 从 ~370 降到 < 50P99 改善(消除 32s timeout 尾巴mean latency 可能略升(被强制 sleep

修复脚本:scripts/sweep_backpressure_smoke.sh4 个 run × 30-60 min分析器scripts/analysis/analyze_backpressure_smoke.py

2.3.4 注意

backpressure 是降级机制,不是性能优化——它把"硬错误32s timeout"换成"主动等待"。整体 throughput 不会因此提升,但 P99 应大幅改善。


2.4 P-side round-robin 不感知 D 健康

2.4.1 现象(实锤)

来源v5 rerun1 prefill-{0,1}.log,全 run 计数:

Worker KVTransferError "Decode instance could be dead" 请求量
prefill-0 367 361 2225
prefill-1 2 0 2224

两 P 请求量完全均衡round-robin错误率差 180×。日志里 prefill-0 的失败反复指向某个特定 D 的 IPto 10.45.80.47:XXXXX)。

2.4.2 根因(代码)

pd_router.py:43-49

prefill_url, bootstrap_port = self.config.prefill_urls[
    self.prefill_cursor % len(self.config.prefill_urls)
]
self.prefill_cursor += 1

裸 round-robin。不感知

  • P 当前 inflight transfer 数
  • 目标 D 的健康状态 / 容量

后果:当某个 D 进入 hot 状态时,被 round-robin 派去给它推 KV 的 P 持续失败;另一个 P 接到的请求恰好命中健康 D完全没事。单 P 故障不会被路由层避开。

2.4.3 影响量化

  • prefill-0 几乎独自承担了全部 KVTransferError 的 99%367/(367+2)
  • 如果 router P 选择能避开"正在和 hot D 死磕"的链路,这部分 ~8% 的整体错误率应可降到 < 1%

2.4.4 备注

这条结论目前来自单次 run 的 N=1 数据。需要跨 N≥3 次 rerun 验证一致性才能完全确信——加上 §2.1.1 (b/c) 也证明 P-D 链路绑定结构性强相关,"prefill-0 死磕某 D"很可能在每次 run 都重复(由初始 session 落点决定)。


2.5 Admission RPC 进 scheduler 主循环 → 自我干扰

2.5.1 现象(实锤)

v5 baseline 配置不开 pollingerrors = 9 完全相同配置 + 1Hz /server_info pollingerrors = 41546×

来源:outputs/qwen3-30b-tp1-v5-optD/exp2_2p6d_kvc_optD_summary.jsonbaseline 9 errorsvs qwen3-30b-tp1-v5-optD-profile/exp2_2p6d_kvc_optD_profile_summary.json415 errors

2.5.2 根因(代码)

/server_info(被 polling 调用)和 admit_direct_append 都进 SGLang scheduler 主循环:

  • /server_infoscheduler.py:get_streaming_session_cache_status → 遍历每个 session slot 计算 is_idle
  • admit_direct_append → 读 token_to_kv_pool_allocator.available_size() + 触发 maybe_trim_decode_session_cache

scheduler 主循环本身在跑 decode/prefill 的 forward。这些 RPC 进队列就和 forward 抢调度。

2.5.3 真实负载下 admission RPC 频率远高于 1Hz

  • 4449 reqs / ~2700s ≈ 1.6 reqs/s
  • 每个 turn 做 1-3 次 admission probedirect-append + 可能的 seed retry
  • × 8 worker = 每秒 ~16-40 次 admission RPC

也就是 admission 流量本身比 1Hz polling 高一个量级。如果 1Hz polling 都能让 errors 涨 46×admission 自己的扰动至少同等。

2.5.4 修复

不在本轮 KISS 内。设计方向是把 admission 拆成两个端点:

  • POST /probe → lock-free 读 snapshot90% 流量走这条
  • POST /commit_evict → 进 scheduler 队列,做实际 LRU仅 probe 不够时调

这部分需要 SGLang 内部 atomic publish snapshot 到共享内存——结构性改动

2.5.5 注意

v6 P0 的 ×3 baseline rerun不开 pollingerrors 也是 372/912/396——polling 不是 415 唯一原因。本身 v5 admission 设计就敏感polling 是放大器。


2.6 Replay 时间被 time-scale=10 压缩 → 测量学失真

2.6.1 现象(实锤)

v5 rerun1 metrics 解出的真实 inter-turn gap 分布:

原始 trace inter-turn gap (n=4397):
  p10=1.6s   p50=2.5s   p90=7.8s   p99=25.1s   max=261s

time-scale=10 实际 replay gap (= 原始 / 10):
  p10=0.16s  p50=0.25s  p90=0.78s  p99=2.5s    max=26s

2.6.2 这意味着什么

真实 agentic 用户/agent 在每个 turn 之间停 2-8 秒——思考、打字、tool call 异步返回、agent reasoning。

microbench.py:20-21 的默认 inter_turn_gap_s=1.0 + session_stagger_s=0.1 也大致符合这个量级1 秒左右)。

但 SWE replay 设的 time-scale=10 把这个间隔人为压到 0.25 秒——D 还没消化完 turn Nturn N+1 就来了。

2.6.3 为什么这么设计

纯粹节省测试时间

  • 原始 trace 跨度 ~6000s≈100 分钟)
  • time-scale=10 → ~600s≈10 分钟)
  • sweep 5 版本 × 3 重复 = 25h vs 2.5h

2.6.4 它扭曲了什么

  1. 抹掉 D 的自然 idle 时间:真实部署里每个 session 在 turn 间有几秒空窗,正好让 D 端 LRU 把它 evict 出去给其他 session 让位§2.2 idle 判定。time-scale=10 下几乎所有 session 一直忙——LRU 永远找不到 idle session。
  2. 人为提升并发压力concurrency=32 在 time-scale=10 下意味着 D 端持续承受 320 effective concurrent agents 的压力——远超真实部署。
  3. 掩盖 backpressure 等慢节奏机制的价值:如果 inter-turn gap 是 2.5sbackpressure 让 replay 等 0.5s 几乎不影响吞吐time-scale=10 下 0.5s 的 sleep 等于直接跳过下一个 turn。

2.6.5 严重性:所有 KVC vs DP 结论都带这个失真

v3-v6 全部数据基于 time-scale=10。所以"KVC 在 SWE 上输给 DP"的程度可能被 benchmark 放大。真实部署里 inter-turn gap 是 2.5s 的话KVC 可能根本不会撞到当前看到的容量瓶颈

这是项目当前最严重但还没修的测量学问题。修复成本极小(只是去掉 --time-scale 10),但意义重大——P0 应该立刻跑一组 time-scale=1 baselineKVC + DP 各 N=3


2.7 direct-to-D append 阈值 = 2048 是个 magic number

2.7.1 现象(实锤)

replay.py:51 默认值:

kvcache_direct_max_uncached_tokens: int = 2048

判定(replay.py:2177):当新 turn 的 uncached append > 2048 token 时,禁止 direct-to-D,请求改走 P→D reseed 路径。

实测 v5 rerun1 的 uncached append 分布(input_length - cached_tokens

所有 4449 请求:
  p10=50  p25=181  p50=610  p75=2907  p90=36495  p99=91600  max=103971

> 2048: 1222/4449 = 27.5%

双峰分布median 只有 610但 p90 已经 36K。

2.7.2 根因(代码)

阈值是个 magic number——没有任何代码注释解释为什么是 2048git log 里也没人调过它。

合理推测它存在的理由(按可信度):

理由 是否成立
D 是 decode-tunedmax-prefill-tokens 通常 4-8Kappend > 2K 会触发 D 内部多 chunk prefill 拖慢 decode
大 append 在 D 上 prefill 会阻塞当前正在 decoding 的其他 session 的 TPOT
P 有更优化的 prefill kernel 和 batch D 的 prefill kernel 同源)
工程上的"安全默认值",没认真测过 git log 印证)

2.7.3 但更严重的 bugexecution_mode 标签命名错位

execution_mode 名字里带 "large-append" 的请求一共 2060 个,其中:

  • 1222 个59.3%)实际 uncached append ≤ 2048

也就是说,"large-append" 这个标签名对超过一半的实例是错的。看 replay.py:2168-2178 的判断:

if (
    _should_bypass_prefill(...)              # 要求 overlap > 0
    and direct_append_length is not None
    and direct_session_reused                 # 要求 session 在本 D 上 opened 过
    and not direct_session_reset
    and direct_append_length <= config.kvcache_direct_max_uncached_tokens
):
    # direct-to-D
else:
    # 进入 "large-append" 分支

这个 else 分支的 5 个进入条件里,"append > 2048" 只是其中一个。 session 不在本 D 上、被 evict 过、overlap=0 都会进这个分支,但 execution_mode 仍然写 pd-router-fallback-large-append-*——导致看 metrics 的人误以为问题是 append 太大。

2.7.4 实际阈值不是主要瓶颈session 不在 D 上才是

把 turn≥2 的请求按"append 是否 > 2048"和"实际 execution mode"交叉:

Turn≥2 小 append (≤2048), n=3129:
   1854 (59%)  kvcache-direct-to-d-session            ← 走通了
   1141 (37%)  pd-router-fallback-large-append-session-cap  ← 标签骗人
   ...

Turn≥2 大 append (>2048), n=1216:
    813 (67%)  pd-router-fallback-large-append-session-cap
    365 (30%)  kvcache-centric (失败)
     22         pd-router-large-append-reseed                ← 真正受阈值影响的
    ...

真正因 append > 2048 而失败的请求:约 50 个large-append-reseed + 部分 large-append fallback仅占总数 1-2%。

绝大多数 fallback 实际是 §2.1 的 session 不在 D 上——名字里带 "large-append" 是误导。

2.7.5 修复

两件事:

  1. execution_mode 标签按真实原因细分——把 "large-append" 拆成 "session-not-resident" / "real-large-append" / "session-reset" 等
  2. 阈值本身可以做 sweep2048 / 4096 / 8192 / 16384找最优——但收益空间有限最多改善那 1-2% 的请求)

2.8 跨 run variance 巨大N=1 不可信

2.8.1 现象(实锤)

v5 baseline 完全相同配置跑 3 次(qwen3-30b-tp1-v5-optD-baseline-rerun/

Run Errors Lat mean Lat P50 TTFT P50
rerun1 372 3.50s 1.11s 0.147s
rerun2 912 3.00s 0.94s 0.071s
rerun3 396 3.42s 1.22s 0.183s

errors 漂移 2.5×372→912P50 latency 漂移 ~30%TTFT P50 漂移 2.6×

2.8.2 根因(推测)

源头不止一个,至少包含:

  1. §2.1 + §2.2 的复合D 容量过载是临界点附近的非线性系统——initial session-to-D assignment 的随机性决定了哪个 D 先饱和。
  2. mooncake TCP loopback 的随机性:单机 loopback 的 32s timeout 触发概率受当前 GPU 内存碎片、PCIe 状态影响。
  3. scheduler 主循环里 admission RPC 与 decode 抢资源的随机性§2.5)。

2.8.3 影响

所有 single-run 比较 < 30% 差异都不可信。这意味着:

  • v3 vs v4 的 P50 差异1.75s vs 1.08s)勉强有意义(差异 38%
  • v4 vs v5 的 P50 差异0.84s vs 1.31s)勉强有意义(差异 56%
  • v5+profile 的 1P7D vs baselinemean 4.21s vs 5.18s)→ 差异 18%不可信
  • 所有 direct-to-D 占比 ±5% 的差异都是噪声

2.8.4 这条规则要求所有后续实验

要任何 KVC 配置间或 KVC vs DP 的对比,最少跑 N=3最好 N=5。 不跑 N≥3 的实验在做"碰运气科研"。

8h 一次 sweep 装不下 N=3 + 多版本对比,所以必须牺牲版本数量保 N≥3


2.9 microbench 的 KVC 优势不能外推到真实 agentic

microbench.py:13-22 默认参数:

维度 默认值
session_count 8
turns_per_session 3
initial_input_length 10000
append_input_length 1000 ← 低于 §2.7 的 2048 阈值
output_length 1000
inter_turn_gap_s 1.0 ← 接近真实 agentic
session_stagger_s 0.1

与 SWE workload 的关键维度对比

维度 microbench SWE 50sess
Session 数 4-8 52
Per-session peak input ~31K median 49K, max 104K
总 working-set / 7D 容量92K each 0.19×5× 冗余) 3.95×4× 过载)
Append size 是否过 2048 几乎 100% 过不到 28% 超过
Session 数是否过 cap 4 ≤ 28v3 cap×7D 52 远超

Microbench 把 KVC 的所有失效条件都规避了容量充裕、append 卡阈值之下、session 数远低于 cap、inter-turn gap 接近真实——这一组参数让 KVC 五项判断(路由 / admission / 没被 evict / append ≤ 阈值 / 无 backpressure全部通过 → 100% 走 direct-to-D 快路径。

而 SWE workload 在每一项上都把 KVC 推过临界点。

所以"KVC 在 microbench 赢 PD disagg"是个弱命题——它只证明了机制能跑,没有证明在真实 agentic 下能赢。


第三部份:一句话总结与下一步

现状一句话

在所有可比的真实 agentic workloadSWE 35B / 30Bnaive DP cache-aware 全胜 KVC 任何配置,且差距 > 30%(远超 single-run variance。Microbench 上 KVC 赢 PD disagg 的设计前提容量富余、append 小、session 少)在真实 workload 下不成立。

排序后的结构性问题(按修复 ROI

排名 问题 影响 修复成本
P0 §2.6 time-scale=10 失真 → 所有 KVC vs DP 结论可能被 benchmark 放大 颠覆性 极低(改 flag
P0 §2.1 session 永久 pin + 容量盲选 25% session 永远饿死 中(改 policy
P0 §2.2 D-side LRU 跟不上 ~8% errors 来自此 中(改 SGLang
P1 §2.3 没 backpressure 把 timeout 雪崩变可控 已实现(待 GPU smoke
P1 §2.4 P-side 不感知 D 健康 单 P 出错率差 180×
P1 §2.7 / 2.8 metrics 标签命名错位 数据解读经常出错 低(改字符串)
P2 §2.5 admission RPC 进 scheduler 主循环 自我干扰 高(结构改动)
P2 §2.8 N=1 不可信 实验方法学 0团队约定

立刻能做的三件事

  1. 跑 time-scale=1 baselineKVC v5 + 8DP CA 各 N=3~6h GPU—— 不修代码、单变量、决定后续路线。
  2. 跑 backpressure smoke已实现4 run × ~30-60 min~3-4h GPU—— 验证 §2.3 修复的端到端效果。
  3. 修 metrics 标签命名pd-router-fallback-large-append-* → 按真实原因分类)—— 让以后看数据的人不会再被误导。

不立刻做但要重新讨论的

  • §2.1 capacity-aware policy:之前考虑过的"评分加 capacity 项"会引入"换 D"的副作用(孤儿 KV、新 D 上仍可能饿死),需要跟 §2.2 的 D 端 hot retract 一起设计。
  • §2.5 admission API 拆 probe / commit:是结构性正确方向,但要动 SGLang 内部 + atomic publish 机制,不是 KISS。
  • 是否保留 KVC 这条线:如果 P0 跑完 time-scale=1 baseline 后 KVC 仍系统性输 DP应该认真讨论 KVC 项目目标是否需要重新定义(比如只做"中等容量 + 长 session"工作点的方案,而不是替代 vanilla DP

附录 A本报告所有数据的来源

章节 数据源
1.1 SWE 35B outputs/swebench-exps/{pd-disagg,pd-colo,kvcache-centric}-*
1.2 TP1 series outputs/qwen3-30b-tp1-{exps,v3-kvaware,v4-cap16,v5-optD,v5-optD-profile,v5-optD-baseline-rerun}/
2.1 session pinning outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/exp2_2p6d_run{1,2,3}_metrics.jsonl
2.2 D LRU 计数 outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/.../logs/decode-{0..5}.log
2.4 P imbalance outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/.../logs/prefill-{0,1}.log
2.5 polling 影响 v5 baseline summary vs v5+profile summary
2.6 inter-turn gap rerun1 metrics 的 trace_timestamp_s 字段
2.7 append 分布 rerun1 metrics 的 input_length - cached_tokens
2.8 variance rerun1/2/3 三组 summary

附录 B相关已有文档

  • docs/PROJECT_OVERVIEW.md — 项目目标、microbench 结论
  • docs/archive/AGENTIC_FIT_ANALYSIS_ZH.md — 结构性缺陷的早期分析(本报告 §2 的来源)
  • docs/archive/KVC_DEBUG_JOURNEY_V1_TO_V5.md — v1→v5 详细演进日记
  • docs/archive/V5_PROFILE_INVESTIGATION_ZH.md — v5+profile 调查(含 critic 修订)
  • docs/archive/SWEBENCH_EXPERIMENT_RESULTS.md — SWE 35B 早期实验
  • docs/archive/REFACTOR_PLAN_ZH.md — 当前重构计划
  • docs/archive/STRUCTURAL_VALIDATION_REPORT_ZH.md — 结构性 claim 验证(本报告的精简版)