Files
agentic-pd-hybrid/docs/KVC_DEBUG_JOURNEY_V1_TO_V4.md
kzlin 74194e660a docs: v4 final results, error analysis, and updated journey
Add v4 sweep results and post-mortem analysis showing:

- direct-to-D path: 54.3% (1P7D) / 58.0% (2P6D) of requests now use
  KVC cleanly. P50=0.5s and TTFT P50=0.043s; this path beats baseline
  8DP across the board (P50 -24%, TTFT P50 -54%, TTFT P90 -79%).

- Overall vs baseline (errors+truncated excluded):
  v4 2P6D P50=0.85s vs baseline 0.66s (28% slower).
  Reason is not errors -- 35% of requests still hit
  fallback-large-append-session-cap, where capacity-based
  cap = usable_tokens / target_tokens evaluates to 1-2 (not 16)
  for large agentic inputs.

- 9-10% errors on KVC variants are mooncake TCP transfer timeouts,
  not SGLang logic bugs. Prefill log shows
  "Failed to send kv chunk ... 32s timeout ... session not alive".
  Errors concentrate in turn>=31 (large inputs) after run >44.8%.

Track:
- docs/KVC_DEBUG_JOURNEY_V1_TO_V4.md: append v4 results table,
  per-mode breakdown, and error root cause.
- scripts/analysis/{analyze_v3,analyze_v4,analyze_errors,compare_no_error}.py
- outputs/qwen3-30b-tp1-v{3,4}*/exp*_summary.json (force-added,
  small JSON; metrics.jsonl excluded due to size).
- outputs/qwen3-30b-tp1-v{3,4}*/sweep_results.txt

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 23:34:01 +08:00

14 KiB
Raw Blame History

KVC 实验踩坑记录与代码 Bug 分析v1 → v4

记录从 v1 到 v4 KVC 实验的踩坑过程、错误诊断、以及最终定位的代码 bug。 模型: Qwen3-30B-A3B (TP1),硬件: 单节点 8×H100 80GB。 Trace: qwen35-swebench-50sess.jsonl4449 请求52 sessions

TL;DR

版本 关键变化 截断率 direct-to-D 占比 P50 主要瓶颈
v1 (smoke / 早期) mechanism 跑通 - - - -
v2 KVC + --policy default 56.8% / 61.4% <0.1% 0.08s* Routing 错位(默认策略)
v3 KVC + --policy kv-aware 0.9% 30-42% 1.5-1.8s session-cap fallback (52-65%)
v4 v3 + soft_cap 4→16 (待数据) (待数据) (待数据) (待数据)

* v2 的 P50 是假数字——超过半数请求只生成 1 个 token 就被 abort。

v2 踩坑Default policy 与 KVC 机制根本不兼容

表象

scripts/sweep_tp1_v2_fixed.sh 跑出来:

  • Exp18-way DPbaseline4449/4449 成功P50=0.65serror=0
  • Exp21P7D KVC2524 truncated (56.8%)18 errorsP50=0.08s* (假)
  • Exp32P6D KVC2733 truncated (61.4%)17 errorsP50=0.08s* (假)

每个截断请求 actual_output_tokens=1finish_reason="abort: session id X does not exist"

错误的早期诊断

之前 RESULTS_SUMMARY.md 把锅扣在 SGLang 的 --disaggregation-decode-allow-local-prefill flag 上,认为是 D worker 在有 bootstrap_room 时仍然做了 local prefill。这个诊断完全错误——查 scheduler.py:1975-1980_should_allow_local_prefill_on_decode

def _should_allow_local_prefill_on_decode(self, req: Req) -> bool:
    return (
        self.disaggregation_mode == DisaggregationMode.DECODE
        and self.server_args.disaggregation_decode_allow_local_prefill
        and req.bootstrap_room is None  # ← 有 bootstrap_room 不会走 local prefill
    )

KVC reseed 路径的请求都带 bootstrap_room,根本不会触发 local prefill。

实际根因Replay 与 PD Router 的 round-robin 错位

实验脚本里 KVC 用 --policy default,而 baseline 用 --policy kv-aware。 看 benchmark.py:287-300 这两者的差别巨大:

def _decode_policy_for(policy_name: str) -> str:
    if policy_name == "sticky":      return "manual"
    if policy_name == "kv-aware":    return "consistent_hashing"
    return "round_robin"  # default

def _header_mode_for(policy_name: str) -> str:
    if policy_name == "sticky":      return "routing-key"
    if policy_name == "kv-aware":    return "target-worker"
    return "none"  # default

default policy + KVC 机制下:

  1. Replay policypolicies.py:DefaultPolicyround-robin 选一个 D比如 D-3
  2. Replay 在 D-3 上 open_session(session_id=X)replay.py:1722-1731
  3. Replay 通过 PD Router 发请求(带 session_params),但 header_mode=none不发任何 routing header
  4. PD Router (pd_router.py:_select_decode_index) 看到 decode_policy=round_robin,用自己独立的计数器round-robin发到了 D-5
  5. D-5 的 scheduler 看到 session_params 里有 session_id但自己的 session_controller 里没这个 sessionsession 在 D-3 上)→ abort with "Invalid request: session id X does not exist" (scheduler.py:1824-1836)

两个独立的 round-robin 计数器只要一次错位(任何并发或 direct-to-D 绕过 router 的请求都会引起)就永远对不上。

为什么 turn 0 不出问题?

Turn 0 走 _invoke_plain_routerreplay.py:1894),不带 session_params,作为普通 PD disagg 请求处理,发到任何 D 都行。Turn 1+ 才开始走带 session_params 的 KVC 路径,撞上路由错位。

数据特征验证per-session pattern

session 11360 (58 turns): pattern = .TTTTT.TTTTTTT.TTTTTT...   ← turn 0 OK1+ 全 T
session 18720 (87 turns): pattern = .TTTTTTTTTTTTTTTTTT...

每个 D worker 收到了全部 52 个 session 的请求(理想情况下应该是 ~7-8 个/D因为 round-robin 把 session 完全打散)。

修复

唯一正确的修复是把 KVC 的 policy 从 default 改成 kv-aware

- --policy default
+ --policy kv-aware

KvAwarePolicy (policies.py:146-187) 做两件事:

  1. _overlap_blocks + sticky_bonus 给每个 D 打分session 自然粘在同一个 Dsession 亲和性
  2. header_mode=target-worker,发 x-smg-target-worker header
  3. PD Router 用 consistent_hashing 模式,看到 header 就直接用,不再 round-robin

v3 改 kv-aware policy 后:路由对了,但新瓶颈出现

scripts/sweep_tp1_v3_kvaware.sh 把所有 KVC 实验改成 --policy kv-aware,结果:

指标 v2 1P7D (default) v3 1P7D (kv-aware) v3 2P6D 8-way DP baseline
截断 56.8% 0.9% 0.9% 1.5%
Errors 18 363 (8.2%) 9 0
Mean 4.74s 4.88s 3.58s 1.43s
P50 0.08s* (假) 1.75s 1.52s 0.65s
P90 12.14s 12.67s 9.23s 3.61s
TTFT P50 - 0.36s 0.33s 0.09s

截断从 56.8% 降到 0.9%,路由问题彻底解决 但 P50 仍然是 baseline 的 2-3 倍。

Direct-to-D 路径表现优秀KVC 该有的样子)

按 execution_mode 拆开看:

路径 Exp1 1P7D 占比 Exp1 1P7D P50 Exp1 1P7D TTFT P50
kvcache-direct-to-d-session 42.0% 0.495s 0.043s
pd-router-fallback-large-append-session-cap 🔥 52.6% 5.6s 3.7s

Direct-to-D 路径下:

  • P50 = 0.495s比 baseline 0.65s 快 25%
  • TTFT P50 = 0.043s比 baseline 0.093s 快 2 倍
  • KV transfer = 0无 P 介入,纯 D 上 append-prefill

这才是 KVC 真正的价值。但只有 30-42% 请求走到这条路。

新瓶颈session-cap fallback 占了 52-65%

pd-router-fallback-large-append-session-cap 占 1P7D 的 52.6%、2P6D 的 65.4%。这条路径意味着 router 想开新 session 在 D 上,但 admission 拒绝了("d-session-cap"),只好回退到 plain routerP 全量 prefill + 传给 D无 session 复用)。

Bimodal session 分布starvation

Session Total turns Direct-to-D Session-cap fallback
22080 129 98% 0%
3840 118 97% 0%
70560 150 0% 99%
39360 148 0% 99%
61600 117 0% 99%

要么完全幸运,要么完全饿死——典型的双峰分布。

根因:硬编码 cap=4

replay.py:_decode_session_soft_cap 原始代码:

def _decode_session_soft_cap(...) -> int:
    target_tokens = max(1, _estimate_session_resident_tokens(request))
    usable_capacity_tokens = _usable_capacity_tokens(residency, server_url)
    ...
    if usable_capacity_tokens <= 0:
        return 4
    return max(1, min(4, usable_capacity_tokens // target_tokens))
    #              ^^^ 硬编码上限 4

7 个 D × 每个 D 最多 4 个 session = 28 个 session slot 总容量。Trace 有 52 个 session → 24 个 session 永远抢不到 slot。

启动期 race condition 决定了哪些 session 是"幸运儿"——前 28 个挤进来的 session 的所有后续 turn 都走 direct-to-D剩下 24 个 session 永远走 session-cap fallback

v4 改进:把硬 cap 从 4 提到 16

replay.py:_decode_session_soft_cap 一行修改:

-    if usable_capacity_tokens <= 0:
-        return 4
-    return max(1, min(4, usable_capacity_tokens // target_tokens))
+    if usable_capacity_tokens <= 0:
+        return 16
+    return max(1, min(16, usable_capacity_tokens // target_tokens))

7 D × 16 = 112 个 slot远超 52 个 session 需求。

v4 实际结果vs v3 1P7D / 2P6D

指标 v3 1P7D v4 1P7D v3 2P6D v4 2P6D baseline 8DP
Errors 363 (8%) 435 (10%) 9 (0%) 403 (9%) 0
截断 42 43 42 36 68
direct-to-D 38.6% 54.3% 30.5% 58.0% -
session-cap fallback 48.3% 37.4% 65.4% 34.7% -
Session reused 1716 2180 1358 2348 -
KV transfer blocks 62K 53K 79K 51K -
Mean 4.88s 4.21s 3.58s 2.51s 1.43s
P50 1.75s 1.08s 1.52s 0.84s 0.65s
P90 12.67s 13.38s 9.23s 6.51s 3.61s
P99 28.72s 24.45s 18.70s 18.34s 8.38s
TTFT P50 0.36s 0.056s 0.33s 0.051s 0.094s
TTFT P90 10.97s 11.90s 6.95s 2.64s 0.26s

✓ direct-to-D 占比从 v3 的 30-38% 涨到 v4 的 54-58% ✓ session 复用 +27% (1P7D) / +73% (2P6D) ✓ KV transfer 量 -15% (1P7D) / -36% (2P6D) ✓ TTFT P50 反超 baseline 46%0.051s vs 0.094s

Direct-to-D 路径全面碾压 baselineKVC 真实价值)

Config n Lat P50 Lat P90 TTFT P50 TTFT P90
baseline 8DP 4381 0.66s 3.65s 0.094s 0.256s
v4 1P7D direct-to-D 2179 0.495s 3.03s 0.044s 0.055s
v4 2P6D direct-to-D 2348 0.499s 2.86s 0.043s 0.054s

direct-to-D 子集相对 baseline

  • P50 快 24-30%
  • P90 快 16-22%
  • TTFT P50 快 54%
  • TTFT P90 快 79%

整体性能(去掉 errors 和 truncatedvs baseline

Config clean Mean P50 P90 P99
baseline 8DP 4381 1.45s 0.66s 3.65s 8.38s
v4 2P6D 4010 2.53s 0.85s 6.55s 18.33s

vs baselineP50 慢 28%、P90 慢 80%、P99 慢 119%。即使错误率为 0整体仍输 baseline——根因是 35% 请求被推到 fallback 路径。

新瓶颈 135% 请求仍走 session-cap fallback

抬到 16 后真实瓶颈是 capacity-based 计算:min(16, usable_capacity_tokens // target_tokens)

  • target_tokens = input + outputagentic 里常见 50-100K
  • D 的 KV pool ≈ 100-150K tokens80GB H100, mem_fraction=0.835
  • usable / target = 1-2远没到 16 → 真实 cap 是 capacity 算出来的小数字

要解决必须改 capacity-based 估算逻辑(或上方案 D让 D 自己决定)。

新瓶颈 29-10% errorsmooncake 传输超时)

P-side log 显示:

KVTransferError: Failed to send kv chunk of <bootstrap_room> to 10.45.7.165:40319
Sync batch data transfer timeout after 32722558107ns  (32 秒超时)
Decode instance could be dead, remote mooncake session ... is not alive

特征:

  • 所有 errors 在 run 的 44.8% 之后出现(系统压力累积)
  • 98% errors 集中在 turn ≥ 31大 input 的请求)
  • v3 cap=4 时 1P7D 已有 363 errors仅 1 个 D 集中受冲击v4 cap=16 把压力均匀分布但量级更大

是 mooncake TCP loopback 在并发上去后撞超时,不是 SGLang 逻辑 bug。修复方向:

  1. 加长 mooncake transfer timeout现在 32s
  2. 限制并发 inflight transfer 数量
  3. 改用 RDMAloopback 是单机模拟,生产环境换真 RDMA
  4. chunked KV transfer

后续可以考虑的更深方案:让 D 自己决定 admission方案 D

v4 的硬 cap 抬高只是把数字调大,实际容量管理还是 replay 自己估算。代码里 replay.py:_decode_session_soft_captarget_tokens = input + output(基于当前请求的 size估算每个 session footprint

  • agentic context 越攒越长target_tokens 动态增长cap 会随之缩小
  • 多个并发请求查询时 replay 视图会过期
  • replay 自己写了 LRU eviction 逻辑(_reserve_decode_session_capacity_from_router_state),与 SGLang 内部的 maybe_trim_decode_session_cache 重复且永远滞后

SGLang 已经提供 /session_cache/admit_direct_append 端点(scheduler.py:3497D worker 自己回答能不能 admit并且查询时主动调用 LRU eviction。但这个端点只在 direct-to-D 路径用seed/reseed 路径用的是 replay 自己估算的 soft_cap。

理想方案是扩展端点支持 seed_new / reseed 模式replay 完全交给 D 决策——但这需要 SGLang 侧 patch + replay 重构(~200 行),工程量比 v4 大得多。

关键文件与代码位置索引

现象 代码位置
Replay policy round-robin policies.py:63-67 RoutingState.next_decode_worker_id
KV-aware policysession 亲和) policies.py:146-187 KvAwarePolicy.select
PD router decode 选择 pd_router.py:51-74 _select_decode_index
Header 构建 replay.py:2407-2424 _build_headers
Policy → router config 映射 benchmark.py:287-300 _decode_policy_for/_header_mode_for
Session admission 软 cap replay.py:889-905 _decode_session_soft_cap
已有的 D 侧 admission 端点 scheduler.py:3497-3580 admit_direct_append
Session 在 D 上找不到的报错 scheduler.py:1824-1836
_should_allow_local_prefill_on_decode scheduler.py:1975-1980
Reseed 流程入口 replay.py:1665-1809 _invoke_kvcache_seeded_router
Direct-to-D 流程 replay.py:2351-2398 _invoke_decode_session_direct

经验教训

  1. policy 和 mechanism 是两个正交维度——--policy default 不是"无脑默认值",它真的是 round-robin 无 session 亲和性。KVC 机制必须配 session 亲和的 policy。

  2. 不要无脑相信前一个 agent 的 RESULTS_SUMMARY——v2 的诊断("local prefill bug")和实际 finish_reason"session id does not exist")完全对不上。任何错误诊断必须用 finish_reason、execution_mode 这些原始字段交叉验证。

  3. bimodal 分布是 starvation 的强信号——v3 数据里某些 session 100% 走快路径、某些 100% 走慢路径,几乎肯定是某种"先到先得"的资源竞争。看到这种模式立刻去找硬编码 cap 或全局共享资源。

  4. 测量要看分组而非整体均值——v3 整体 P50=1.5s 看似比 baseline 慢,但拆开看 direct-to-D 子集 P50=0.495s 已经反超 baseline。整体均值被 fallback 路径拖累,但 KVC 的核心价值是真实存在的。