diff --git a/docs/KVC_DEBUG_JOURNEY_V1_TO_V4.md b/docs/KVC_DEBUG_JOURNEY_V1_TO_V5.md similarity index 65% rename from docs/KVC_DEBUG_JOURNEY_V1_TO_V4.md rename to docs/KVC_DEBUG_JOURNEY_V1_TO_V5.md index 9e76a81..cd88d12 100644 --- a/docs/KVC_DEBUG_JOURNEY_V1_TO_V4.md +++ b/docs/KVC_DEBUG_JOURNEY_V1_TO_V5.md @@ -1,6 +1,6 @@ -# KVC 实验踩坑记录与代码 Bug 分析(v1 → v4) +# KVC 实验踩坑记录与代码 Bug 分析(v1 → v5) -记录从 v1 到 v4 KVC 实验的踩坑过程、错误诊断、以及最终定位的代码 bug。 +记录从 v1 到 v5 KVC 实验的踩坑过程、错误诊断、以及最终定位的代码 bug。 模型: Qwen3-30B-A3B (TP1),硬件: 单节点 8×H100 80GB。 Trace: `qwen35-swebench-50sess.jsonl`(4449 请求,52 sessions)。 @@ -11,7 +11,8 @@ Trace: `qwen35-swebench-50sess.jsonl`(4449 请求,52 sessions)。 | 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 | (待数据) | (待数据) | (待数据) | (待数据) | +| v4 | v3 + soft_cap 4→16 | 1.0% | 54-58% | 1.08 / 0.84s | session-cap fb 仍 35%、9-10% mooncake errors | +| v5 | Option D:worker-mode 驱动 seed/reseed | 0.9% | 41-45% | 1.59 / 1.31s | D KV pool 真容量不足 → fallback 反而 ↑ 至 46-51% | `*` v2 的 P50 是假数字——超过半数请求只生成 1 个 token 就被 abort。 @@ -251,16 +252,88 @@ Decode instance could be dead, remote mooncake session ... is not alive 3. 改用 RDMA(loopback 是单机模拟,生产环境换真 RDMA) 4. chunked KV transfer -## 后续可以考虑的更深方案:让 D 自己决定 admission(方案 D) +## v5 落地方案 D:worker-mode 驱动 seed/reseed -v4 的硬 cap 抬高只是把数字调大,实际容量管理还是 replay 自己估算。代码里 `replay.py:_decode_session_soft_cap` 用 `target_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` 重复且永远滞后 +`scripts/sweep_tp1_v5_optD.sh` 真正把方案 D 落到了代码里。改动核心:把 `--kvcache-admission-mode` 从 `local`(replay 估算) 改成 `worker`(D 决策),并扩展到 **direct_append + seed + reseed 全部路径**。 -SGLang 已经提供 `/session_cache/admit_direct_append` 端点(`scheduler.py:3497`),D worker 自己回答能不能 admit,并且查询时**主动调用 LRU eviction**。但这个端点只在 direct-to-D 路径用,seed/reseed 路径用的是 replay 自己估算的 soft_cap。 +### 关键代码改动 -理想方案是扩展端点支持 `seed_new` / `reseed` 模式,replay 完全交给 D 决策——但这需要 SGLang 侧 patch + replay 重构(~200 行),工程量比 v4 大得多。 +1. SGLang 侧:`scheduler.py` 的 `admit_direct_append` 端点新增 `mode` 字段,支持 `direct_append | seed`,seed 模式会触发 D 真正去 reserve KV pool 块并主动调用 `maybe_trim_decode_session_cache` 做 LRU。 +2. Replay 侧:`replay.py` 中 reseed / turn-1 seed / large-append-reseed 都改走同一个 admit endpoint;`_decode_session_soft_cap` 在 worker mode 下被完全 bypass。 +3. 新增运行参数:`--kvcache-admission-mode worker`、`--kvcache-seed-min-turn-id 1`、`--kvcache-seed-max-inflight-decode -1`、`--kvcache-prefill-backup-policy release-after-transfer`、`--kvcache-prefill-priority-eviction`。 + +### 假设 + +- v4 的 35% session-cap fallback 来自 replay 视图过期 + capacity-based 计算保守 → 让 D 自己看 KV pool 应该把这 35% 救回来。 +- D 主动 LRU eviction 比 replay 自己写的 reservation 更准确,**应该**让更多 session 能 seed 进来。 + +### v5 实际结果(vs v4 同配置) + +| 指标 | v4 1P7D | **v5 1P7D** | v4 2P6D | **v5 2P6D** | baseline 8DP | +|------|:---:|:---:|:---:|:---:|:---:| +| Errors | 435 (10%) | **9 (0.2%)** ⭐ | 403 (9%) | **9 (0.2%)** ⭐ | 0 | +| 截断 | 43 | 42 | 36 | 42 | 68 | +| direct-to-D | 54.3% | 44.7% ↓ | 58.0% | 41.3% ↓ | - | +| **session-cap fallback** | 37.4% | **45.6%** ↑ | 34.7% | **50.6%** ↑ | - | +| no-d-capacity fallback | 0.3% | 1.2% | 0.2% | 0.8% | - | +| pd-router-turn1-seed (新可见) | - | 1.2% | - | 1.1% | - | +| pd-router-d-session-reseed (新可见) | - | 4.8% | - | 3.4% | - | +| pd-router-large-append-reseed (新可见) | - | 1.0% | - | 1.0% | - | +| Session reused | 2180 | 1990 | 2348 | 1837 | - | +| KV transfer blocks | 53K | 66K | 51K | 69K | - | +| Mean | 4.21s | 5.18s | 2.51s | 3.49s | 1.45s | +| **P50** | 1.08s | 1.59s | 0.84s | 1.31s | 0.66s | +| P90 | 13.38s | 14.67s | 6.51s | 9.09s | 3.65s | +| P99 | 24.45s | 26.09s | 18.34s | 24.92s | 8.38s | +| TTFT P50 | 0.056s | 0.21s | 0.051s | 0.24s | 0.094s | +| TTFT P90 | 11.90s | 13.06s | 2.64s | 6.90s | 0.26s | + +✅ **可靠性大幅提升**:mooncake 传输超时 errors 从 9-10% 跌到 0.2%。D 真容量决策避免了 v4 那种"乐观 admit → 30s 后超时"的死亡链路。 +✅ reseed / turn1-seed 路径首次显式出现,证明 admission 端点对 seed 模式确实生效了。 +❌ **session-cap fallback 不降反升**(37→46% 与 35→51%)。说明 v4 的本地 soft_cap 实际上**比 D 真实容量更乐观**——admit 进来后转身就 OOM,统计成了 error 而不是 fallback。 +❌ 直接结果:**direct-to-D 占比下降、整体延迟全面变差**。P50/P90/P99 与 TTFT 都退步。 + +### Direct-to-D 子集还是稳的(KVC 真实价值仍在) + +| Config | n | Lat P50 | Lat P90 | TTFT P50 | TTFT P90 | +|--------|:---:|:---:|:---:|:---:|:---:| +| baseline 8DP | 4381 | 0.66s | 3.65s | 0.094s | 0.256s | +| v4 2P6D direct-to-D | 2348 | 0.499s | 2.86s | 0.043s | 0.054s | +| **v5 1P7D direct-to-D** | 1990 | 0.475s | 3.04s | 0.043s | 0.055s | +| **v5 2P6D direct-to-D** | 1837 | 0.483s | 3.04s | 0.043s | 0.054s | + +direct-to-D 的尾延迟和 TTFT 与 v4 几乎完全一致(端点决策开销可忽略),**v5 的回退不是路径本身变慢,而是更多请求被赶到 fallback**。 + +### Fallback 路径反而比 v4 更糟 + +| Config | n | Lat P50 | Lat P90 | TTFT P50 | +|--------|:---:|:---:|:---:|:---:| +| v5 1P7D session-cap fallback | 2027 | 6.38s | 17.47s | 4.49s | +| v5 2P6D session-cap fallback | 2253 | 3.13s | 11.25s | 0.89s | + +由于 fallback 占比上升、且这条路径本身就比 direct-to-D 慢一个数量级,整体均值被拖累得更厉害。 + +### v5 真正暴露的瓶颈:D 的 KV pool 物理容量 + +把 admission 决策权交给 D 之后,瓶颈从"replay 估得太死"变成"D 真的装不下": + +- 80GB H100 × `mem_fraction_static=0.835` → D 单卡 KV pool ≈ 100-150K tokens +- agentic 长 context session 单 turn footprint 50-100K +- 单 D 上能并存的 session 数量本就 2-3 个 → 7 个 D 装 50 session 基本不可能 + +v4 的 cap=16 之所以"看起来好",部分是因为本地 soft_cap 没真的查 D 的 free pool,开了一堆**最终会失败**的 session(统计成 errors 而非 fallback)。v5 把这部分洗成了"诚实的拒绝"——可靠性跃升的代价是看见了真实容量上限。 + +### v6 应该针对什么 + +把 D 物理容量管理打开,而不是再调 replay: + +1. **prefill backup 提早 release**(已经加了 `release-after-transfer` 但可能还不够及时) → 让 P 上的 backup blocks 不要长期占用 KV pool。 +2. **priority eviction 策略调优**(已开 `--kvcache-prefill-priority-eviction`):当前 LRU 可能把 hot session 误踢;需要按 session 命中频率/最近访问做加权。 +3. **chunked / streamed seed**:不要一次 reserve 整个 prompt 的容量,按 chunk 分摊。 +4. **跨 D 的 session migration**:当一个 D 满了但隔壁 D 空时主动迁移,而不是直接 fallback 到 P。 +5. **真正的多机 RDMA**:单机 mooncake loopback 是 errors 的根因之一;上多机 + RDMA 才能让 prefill backup release 后的 KV transfer 真的稳。 + +工程量:1-3 是 SGLang 内部改 (`scheduler.py` + `session_controller.py`),4 需要 router 协议扩展,5 是部署变更。 ## 关键文件与代码位置索引 @@ -272,7 +345,10 @@ SGLang 已经提供 `/session_cache/admit_direct_append` 端点(`scheduler.py: | 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` | +| 已有的 D 侧 admission 端点 | `scheduler.py:3497-3580` `admit_direct_append`(v5 扩展支持 `mode=seed`) | +| Worker-mode admission 调用方 | `replay.py` reseed / turn1-seed / large-append-reseed 路径 | +| Prefill backup 释放策略(v5 引入) | `--kvcache-prefill-backup-policy release-after-transfer` | +| Prefill priority eviction(v5 引入) | `--kvcache-prefill-priority-eviction` | | 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` | @@ -287,3 +363,5 @@ SGLang 已经提供 `/session_cache/admit_direct_append` 端点(`scheduler.py: 3. **bimodal 分布是 starvation 的强信号**——v3 数据里某些 session 100% 走快路径、某些 100% 走慢路径,几乎肯定是某种"先到先得"的资源竞争。看到这种模式立刻去找硬编码 cap 或全局共享资源。 4. **测量要看分组而非整体均值**——v3 整体 P50=1.5s 看似比 baseline 慢,但拆开看 direct-to-D 子集 P50=0.495s 已经反超 baseline。整体均值被 fallback 路径拖累,但 KVC 的核心价值是真实存在的。 + +5. **errors 与 fallback 是同一类资源压力的两副面孔**——v4 的"低 fallback 率 + 高 error 率"不是更优解,是把容量超限的失败从"显式拒绝"伪装成"超时失败"。v5 把决策权交给真容量后,fallback 升、errors 降,这是更诚实的指标,不要被 v4 的 fallback 数字误导。当看到错误率和 fallback 率呈反相关时,要警惕 admission 决策是否在说谎。