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>
This commit is contained in:
kzlin
2026-05-11 22:40:35 +08:00
parent 5a2fb8799c
commit 7590e55189
14 changed files with 83 additions and 53 deletions

View File

@@ -0,0 +1,434 @@
# Agentic 场景下的结构性设计缺陷分析
**日期**2026-05-06
**对照数据**`outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/exp2_2p6d_run1_*`KVC kv-aware Option D2P6D4449 reqs / 52 sessions+ `outputs/qwen3-30b-tp1-exps/exp1_8way_dp_cache_aware_summary.json`(同 trace 8-way DP cache-aware baseline
**模型**Qwen3-30B-A3BTP1单机 8×H100 80GB。
**研究问题**:把 SWE trace 视为"真实 agentic"的代表KVC 机制相对 vanilla DP 系统性输在哪里——除了"D 容量 4.6× 过载"之外的结构性原因。
> 本文是对 `docs/KVC_DEBUG_JOURNEY_V1_TO_V5.md` 与 `docs/V5_PROFILE_INVESTIGATION_ZH.md` 的补充:版本演进与瓶颈定位之外,从设计层看哪些假设和真实 agentic workload 不匹配。
---
## TL;DR
按重要性排序的结构性缺陷:
| # | 缺陷 | 数据 | 修复方向 | 工程量 |
|---|---|---|---|---|
| 1 | **KvAwarePolicy 不感知 D 容量session 永久 pin 到首次落点 D** | session 平均访问的不同 D 数 = **1.00**direct-to-D 命中率呈极端双峰15 session 0-20%、14 session 80-100% | score 函数加 capacity-aware 项;允许跨 D session 迁移 | 中 |
| 2 | **D 端 LRU 只能 evict idle sessionhot session 永远踢不掉** | D 跑全程仅 9-43 次 trim 事件 vs 80-150 次 transfer 错误token_usage 顶到 1.00 | 加 score-based eviction按访问频率/最近性多层) | 中 |
| 3 | **没有 D→Router→Replay 的 backpressure 通道** | concurrency 一路 32 不降D 失败时 replay 无感 | admission 响应加 `recommended_pause_ms`replay 端按它降并发 | 小 |
| 4 | **Admission HTTP round-trip 与 scheduler 主循环耦合** | v5+profile 仅加 1Hz polling 就让 errors 从 9 涨到 415 | 拆成 lock-free `/probe` + 进 scheduler 队列的 `/commit_evict` | 中 |
| 5 | **P-side round-robin 不感知 D 健康** | prefill-0 出 367 KVTransferErrorprefill-1 仅 4——但请求量近乎对半 | router 选 P 时考虑目标 D 健康度 | 中 |
| 6 | **Replay 端 session footprint 估算膨胀 30×** | `_estimate_session_resident_tokens = input + output`,把 turn-50 的 80K 上下文当成"需要全新 80K 空间" | 改成"增量 token"估算 | 小 |
| 7 | **time-scale=10 把测试条件人为推到失真区间** | inter-turn gap p50 从 2.5s 压到 0.25s——KVC 想利用的"自然 idle 窗口"被消除 | 跑一组 time-scale=1 baseline 验证 | 小(仅配置) |
**最重要的对照事实**:同 trace、同硬件、同模型下 8-way DP cache-aware无 PD 拆分、无 KVC、无 session 抽象):
| 指标 | 8-way DP CA | v5 KVC 2P6D |
|---|---|---|
| Errors | **0** | 372 (8.4%) |
| Latency mean | **1.43s** | 3.50s |
| Latency P50 | **0.65s** | 1.11s |
| Latency P99 | **8.37s** | 20.37s |
| TTFT mean | **0.12s** | 2.13s |
| TTFT P90 | **0.26s** | 6.47s |
| Per-worker 请求量分布 | 508619±10% | 561858±26% |
**naive DP 在每一项都赢,包括 latency mean 的 145% 优势**。这定义了 KVC 在该 workload 下"必须超过"的基线。
---
## 1. Session 永久 pin 到 D + 容量盲选(最核心问题)
### 1.1 现象
每个 session 在整次运行中只访问 **1.00 个不同 D worker**(见上文数据)。结合 direct-to-D 命中率分布:
```
direct-to-D 命中率分桶n=52 sessions
0-20%: 15 sessions ← 几乎每 turn 都失败回退到 P→D 全量传输
20-40%: 7
40-60%: 11
60-80%: 5
80-100%: 14 sessions ← 几乎每 turn 都走 direct-to-D 快路径
```
**几乎没有中间态**——这是典型的不公平资源分配信号。
被饿死与被照顾的 session 在工作量上差异明显:
- 饿死 session 平均 peak input56,011 token
- 顺利 session 平均 peak input31,344 token**1.8× 差距**
**大 session 倾向被饿死**——因为它们在容量已紧张的 D 上更容易触发 admission 拒。
### 1.2 根因(代码级)
`policies.py:166-172` `KvAwarePolicy.select`
```python
score = (
overlap + sticky * self.sticky_bonus, # 主项: 历史 KV overlap
sticky, # 二级: 是否 last_decode_worker
inflight_penalty, # 三级: 当前 inflight 数(很小)
assignment_penalty, # 四级: 累计被分配数(更小)
)
```
评分中**完全无 D 当前容量项**。Session X 第一次落到 D-2 时积累 hash_id 在 D-2 上;之后无论 D-2 多满X 的 turn N+1 都会被打分到 D-2因为 overlap 主导)。
更糟的是 `RoutingState.decode_resident_blocks``policies.py:46`)从不缩减——即使 D 早 evict 了某些块replay 仍认为它们在那。运行中期所有 D 的 overlap 集合都接近"trace 全部 hash_id"policy 退化为纯 sticky。
### 1.3 后果——具体到 session 的体验
**饿死 session如 session 50400105 turns0 次 direct-to-D每 turn 流程**
1. policy 选 D永远是同一个
2. admission 拒D 容量已被占住)
3. 走 fallback-session-cap → P 全量 prefill 50K-100K token
4. mooncake 推 KV → D 仍无空间 → 32s timeout 或 KVTransferError
5. 用户每 turn 体验 5-10s 延迟,反复出错
**顺利 session如 session 3840118 turns97% direct-to-D每 turn 流程**
1. policy 选 D永远是该 session 的初始 D
2. admission 通过(这个 session 一直占着这个 D 的 slot
3. direct-to-DD 上 append-prefill 几百 token零 P 介入、零 mooncake transfer
4. TTFT 0.043s、E2E 0.495s
**这不是"平均慢一点",是结构性不公平**——SLO 视角下 P99 是被饿死那 15 session 的尾巴拉出来的。
### 1.4 为什么 naive DP 反而赢
8-way DP cache-aware 用纯 hash-based 路由,没有 session 抽象,没有 PD 拆分:
- 每个请求按 prefix hash 路由到一个 worker → 同 session 的 turn 在 worker 上自然有 prefix 命中
- 容量过载时 SGLang 自己的 radix cache + 调度器统一管 KV 池
- 不存在 admission/fallback/reseed 路径
- 不存在 mooncake transfer
- per-worker 负载误差 ±10%vs KVC ±26%),自动接近均衡
**KVC 引入的 session affinity / KV 复用 / admission 三件套,在容量紧张时反而加剧了不均衡,没有任何一项能挽回 vs DP 的差距。**
### 1.5 修复方向
`KvAwarePolicy.select` 里加:
```python
# 当前 D 容量利用率worker-mode admission 已经能查到)
capacity_penalty = -worker_capacity_used_ratio[worker.worker_id]
# 当多个 D 都有 overlap 时,按容量挑最空的;
# 当某 D 容量 > 阈值时,禁止该 D 进入候选
if worker_capacity_used_ratio[worker.worker_id] > HARD_CAP:
continue
score = (
overlap_capped, # overlap 但限幅,避免单个 D 永远赢
capacity_penalty, # ← 新增
sticky,
inflight_penalty,
)
```
更激进的修法:当一个 session 被某 D 反复拒 N 次后,主动 release 它在该 D 上的 session 状态,**允许下次 turn 走另一个 D**(代价是丢失已积累的 KV但目前 fallback 路径本来也丢了)。
---
## 2. D 端 LRU eviction 跟不上压力
### 2.1 数据
每个 D 全程:
| Worker | Trim 事件(主动 LRU | KVTransferError + OOM | 峰值 token_usage |
|---|---:|---:|---:|
| decode-0 | 9 | 0 | 0.99 |
| decode-1 | 43 | 12 (4 err + 8 oom) | 0.99 |
| decode-2 | 16 | 459 (153 err + 306 oom) | 0.97 |
| decode-3 | 37 | 87 (29 err + 58 oom) | 0.99 |
| decode-4 | 28 | 270 (90 err + 180 oom) | **1.00** |
| decode-5 | 30 | 279 (93 err + 186 oom) | **1.00** |
**LRU 触发频率比错误次数低 5-15 倍。** D-4 / D-5 直接顶到 token_usage=1.00。
### 2.2 根因
`scheduler.py:2040` `evict_idle_streaming_sessions_lru` 的 idle 判定:
```python
# 只能 evict "所有 req 都 finished + streaming 模式" 的 session
```
但 SWE 高并发下每个 session 几乎一直有 inflight reqtime-scale=10 又压缩了 inter-turn gap。**hot session 永远不 idleLRU 永远找不到东西可踢**。结果 D 一路开到 100% → 下一笔 transfer 来直接 OOM/timeout。
### 2.3 修复方向
引入分层 eviction
1. **Idle session 优先**(当前)
2. **冷 session 次优**(最近 N 秒无访问,即使有 inflight也可以 retract 那个 inflight 让位)
3. **hot session 强制 retract**(在 hard cap 触发时)
vanilla SGLang 已有 `disagg_decode_prealloc_queue.retracted_queue` 机制(看 `admit_direct_append` 引用),但**没有人主动触发 retract**——目前只有内部异常时才会进 retracted_queue。需要把 retract 提升为正常 admission 路径的一部分。
---
## 3. 没有 D→Replay 的 backpressure 通道
### 3.1 名词解释
**Backpressure反压** = 流式系统下游过载时把信号反向传给上游让它降速。例TCP 滑动窗口、Kafka consumer lag、gRPC HTTP/2 flow control。
### 3.2 当前状态
- D 端 transfer queue 堆 → 32s 后 timeout → 抛 KVTransferError
- error 抛回 P → P 抛给 router → router 抛给 replay → replay 走 fallback 路径
- **整个链路上没有"D 过载,请慢点发"的信号**——concurrency 一直保持上限
后果D 一旦开始失败,会**持续失败**(因为 replay 没降速),直到 D 自己消化完积压。
### 3.3 修复方向
`admit_direct_append` 响应里加:
```python
{
"can_admit": ...,
"recommended_pause_ms": int, # ← 新增:下次发同类请求前建议等多久
"queue_depth": int, # ← 新增D transfer queue 当前深度
...
}
```
replay 端在 admission 拒被拒时按 `recommended_pause_ms` 降并发或退避。**这是最便宜的一条改动**——不改协议、不改 SGLang 内部,只改两端代码。
---
## 4. Admission RPC 与 scheduler 耦合——结构 vs 工程的精确边界
### 4.1 现象
`docs/V5_PROFILE_INVESTIGATION_ZH.md` 报告:仅加 1Hz `/server_info` polling 就让 EXP2 errors 从 9 涨到 415。`/server_info` 在 scheduler 主循环里遍历 session slots 算 `is_idle`1 Hz × 8 worker 就足以扰动调度。
但实际负载下 admission RPC 频率远高于 1Hz每个 turn 1 + reseed + direct-to-D 都调一次。concurrency=32 + 4449 reqs / ~2700s ≈ **每秒 16+ 次 admission RPC**
### 4.2 这是结构问题还是工程问题——精确拆解
`admit_direct_append``scheduler.py:3581`)做两件事:
```python
# (a) 读池子状态——轻
available_tokens = self.token_to_kv_pool_allocator.available_size()
# (b) 触发 LRU 扫描——重,且必须修改池子状态
trim_result = self.maybe_trim_decode_session_cache(...)
```
| 部分 | 性质 | 是否能靠工程化解决 |
|---|---|---|
| (a) 读池子状态 | 几个原子读 | **完全可工程化**——做成 lock-free shared-memory snapshot 即可 |
| (b) LRU eviction | 修改 GPU 池子,必须独占 | **结构性的**——Python GIL + 共享 GPU 池子无法并发修改 |
**关键观察**:实际负载里 (b) 是少数路径——大部分 admission 只需要"看一下够不够",不需要立即 evict。
### 4.3 工程化修复方案
把 admission API 拆成两个端点:
```
POST /session_cache/probe ← 90% 流量
- 只读 lock-free snapshot
- 返回 (can_admit_estimate, available_tokens, queue_depth)
- 不进 scheduler 队列
POST /session_cache/commit_evict ← 10% 流量
- probe 不够时才调
- 进 scheduler 队列,做实际 LRU
- 保留当前 admit_direct_append 语义
```
snapshot 由 scheduler 在每个 step 末尾写到一段 mmap 共享内存atomic publishreplay 端 mmap 读,零 syscall 零序列化。一秒内能撑数千次 probe。
### 4.4 关于"协程/多线程/多进程/换语言"
| 工具 | 对本问题的实际效果 |
|---|---|
| asyncio 协程 | SGLang 已用,对 scheduler 主循环本身无帮助 |
| Python 多线程 | GIL 拦着,且 GPU 池子状态只能 scheduler 进程改 |
| 多进程 | scheduler 已是独立进程;问题是它**自己的 step 循环**串行了 admission 与 decode |
| orjson / uvloop | 网络/JSON 加速 5-10×但 LRU 遍历不在那条热路径 |
| Rust/C++ 重写 scheduler | 把 LRU 遍历提速 5-10×但**结构性共享问题仍在** |
**正确的工程化解法是重设计 API拆 probe / commit不是单纯换更快的库或语言。**
---
## 5. P-side 路由不感知 D 健康
### 5.1 数据
```
prefill-0: 367 KVTransferError, 361 "Decode instance could be dead"
prefill-1: 4 KVTransferError, 0 "Decode instance could be dead"
请求量对比:
prefill-0: 2225 requests
prefill-1: 2224 requests ← 几乎对半
```
**两 P 请求量完全均衡,错误率差 92×**。日志里 prefill-0 的错误反复指向某个特定 D`10.45.80.47:XXXXX`)——它跟某个 hot D 形成了"死亡链路"。
### 5.2 根因
`pd_router.py:43-49` 的 P 选择是裸 round-robin
```python
prefill_url, bootstrap_port = self.config.prefill_urls[
self.prefill_cursor % len(self.config.prefill_urls)
]
```
不知道 D 是否健康,不会避开"正在和 D-X 死磕"的 P。
### 5.3 修复方向
router 选 P 时考虑 (P 当前 inflight transfer 数, 目标 D 健康度) 联合得分。健康度可以用 §3 提的 `queue_depth` 字段。
---
## 6. Replay 端 session footprint 估算膨胀 30×
### 6.1 代码
`replay.py:898-899`
```python
def _estimate_session_resident_tokens(request: TraceRequest) -> int:
return request.input_length + request.output_length
```
被用于 `_decode_session_soft_cap``replay.py:1051`)和 `_should_admit_new_decode_session`
### 6.2 问题
对一个已经在 D 上有 80K KV 的 turn 50
- 真实增量需求input 新增几千 token + output 几百 token = ~3K
- 估算返回值80K + 1K = 81K**膨胀 ~27×**
后果router-mode admission 系统性误判——本来能 admit 的 session 被 replay 自己拒掉。v5 worker-mode 让 D 自己看真实容量部分修了这个,**但 KvAwarePolicy 选 D 时仍用这个膨胀估算**——选 D 仍然是错的。
### 6.3 修复
```python
def _estimate_session_resident_tokens(request: TraceRequest) -> int:
if request.turn_id == 1:
return request.input_length + request.output_length
# turn 2+: only the increment matters for additional reservation
return max(0, request.input_length - request.cached_tokens) + request.output_length
```
---
## 7. time-scale=10 测量失真
### 7.1 它是什么
`replay.py` 把原始 trace 每个请求的 `timestamp` 字段做 `t / time_scale` 缩放后再按这个时间发。
- 原始 trace 跨度 ~6000s≈100 分钟)
- time-scale=10 → 实际 replay 跨度 ~600s≈10 分钟)
### 7.2 为什么这么设计
**纯粹为了节省测试时间**——单次 1× 跑 100 分钟sweep 5 版 × 3 重复 = 25h GPU 时间10× 只要 2.5h。
### 7.3 它扭曲了什么
| 维度 | 原始 trace | replay (time-scale=10) |
|---|---|---|
| inter-turn gap p10 | 1.6s | 0.16s |
| inter-turn gap p50 | 2.5s | 0.25s |
| inter-turn gap p90 | 7.8s | 0.78s |
| inter-turn gap max | 261s | 26s |
真实 agentic 用户/agent 在每个 turn 之间停 2-8 秒思考、打字、tool call。**这些间隙正好是 KVC 想利用的"自然 idle 窗口"**——session 短暂 idle 时 LRU 可以 evict、其他 session 可以 admit。
time-scale=10 把这些窗口压到 0.2-0.8s**人为消除了 KVC 的设计前提条件**。
### 7.4 严重的实验有效性威胁
所有 v3-v6 数据基于 time-scale=10。这意味着前面所有"KVC 在 SWE 上输给 baseline"的结论都带着这个失真。**真实部署里 inter-turn gap 是 2.5s 的话KVC 可能根本不会撞到当前看到的容量瓶颈**——D 有时间在 turn 之间释放/重排。
**应该单独跑一组 time-scale=1 的 baseline 对比**,才能判断 KVC 输给 DP 是因为机制本身不行,还是因为 benchmark 把它推到了不该工作的区间。这是这个项目目前**最重要但还没做**的验证。
---
## 8. 应用层抽象不需要在引擎层引入(撤回)
之前草稿里提过"框架不支持 speculative 多分支、嵌套 sub-agent、tool call 中断"——这是过度抽象。**应用层模式都可以由 timestamp + 独立 session_id 隐式表达**
| 应用层模式 | 表现在 trace 里 | 推理引擎需要做什么 |
|---|---|---|
| Tool call 异步返回 | turn N 与 N+1 之间 timestamp gap 很大 | 啥都不用,按时间发请求即可 |
| 嵌套 sub-agent | 父 session timestamp 突然停顿sub-agent 是独立 session_id | 把它们当成两个独立 session 即可KV 也无需共享) |
| Speculative N 分支 | N 个独立 session_id 同时发 | 用 radix prefix cache 自然命中前缀;不需要任何额外抽象 |
**这条不构成结构性缺陷。** 已从结论中移除。
---
## 9. 行动项(按 ROI 排序)
### 优先级 P0修了显著改善饿死/不公平)
1. **[§1] KvAwarePolicy 加 capacity-aware penalty + 允许 session 跨 D 迁移** — 工程量中、收益最大
2. **[§2] D 端引入分层 eviction冷 session、hot retract** — 工程量中、收益大
3. **[§7] 跑一组 time-scale=1 baseline** — 工程量小(仅配置),但**不做这条所有结论都不可信**
### 优先级 P1修了把工程稳定性补齐
4. **[§3] D→Replay backpressure 通道**admission 响应加 pause hint — 工程量小
5. **[§4] 拆 admission 为 probe + commit_evict** — 工程量中
6. **[§6] 修 `_estimate_session_resident_tokens` 用增量** — 工程量小
### 优先级 P2等 P0 数据后再决定)
7. **[§5] P-side 选 P 时考虑 D 健康** — 工程量中
---
## 10. 局限与未验证假设
1. **N=1**:所有数据来自单次 runv6 P0 已证 EXP2 errors 在 9-912 间漂移single-run variance 巨大)。本文所有数字都应理解为"代表性观察"而非"统计显著结论"。
2. **time-scale=10 失真**§7所有"KVC 输给 DP"的程度可能是被 benchmark 放大的。这是最大的不确定性。
3. **8DP 对比的硬件优势**DP 是 8 个 worker 全部跑 prefill+decodeKVC 是 2P+6D只有 6 个能解码。理论上 8 worker 对 6 worker 自带 1.33× 解码并发优势。本文未折算这部分——但 8DP 优势远大于 1.33×latency mean 145% 优势所以核心结论KVC 在该 workload 下系统性输)不受此影响。
4. **mooncake TCP loopback**:所有 transfer 错误是单机 TCP 模拟下的产物。生产环境 RDMA 下错误率分布可能完全不同。
5. **KvAwarePolicy 的 stale `decode_resident_blocks`**§1.2 末尾)现象有数据观察支撑(运行中期 overlap 失去判别力),但**没有系统性测过"清掉 stale 状态会怎样"**。
6. **P-side 错误集中在 prefill-0**§5.1)的因果链是推测——可能也是"prefill-0 早启动 + race"的偶然结果。N>1 数据未验证。
---
## 附录 A数据产物索引
```
outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/
├── exp2_2p6d_run1_metrics.jsonl ← 本文主数据源
├── exp2_2p6d_run1_summary.json
├── exp2_2p6d_run2_* (errors=912, single-run variance 证据)
├── exp2_2p6d_run3_* (errors=396)
└── kvcache-centric-*-20260429T142429Z/logs/
├── decode-{0..5}.log ← §2.1 LRU vs error 计数
└── prefill-{0,1}.log ← §5.1 P 错误分布
outputs/qwen3-30b-tp1-exps/
├── exp1_8way_dp_cache_aware_summary.json ← 对照 baseline
└── RESULTS_SUMMARY.md
```
## 附录 B相关文档
- `docs/PROJECT_OVERVIEW.md` — 项目目标与已实现功能
- `docs/KVC_DEBUG_JOURNEY_V1_TO_V5.md` — v1→v5 版本演进
- `docs/V5_PROFILE_INVESTIGATION_ZH.md` — v5+profile 调查(已 critic 修订)
- `docs/SWEBENCH_EXPERIMENT_RESULTS.md` — Qwen3.5-35B-A3B SWE 实验

View File

@@ -0,0 +1,263 @@
# KV-cache centric P/D routing 当前进展
本文记录当前原型在 SGLang xPyD 上围绕 session-aware / KV-cache-aware P/D routing 的实现、实验结果和阶段性结论。实验日期为 2026-04-24 至 2026-04-25。
## 目标和核心假设
目标是在单机 8 GPU xPyD 环境中验证:针对 agentic coding workloadsession-aware / KV-cache-aware P/D routing 是否能提升端到端延迟。
当前重点假设:
1. 在 PD-disaggregation 下P 节点和 D 节点可能同时保留同一个 session 的 prefix KV形成 P/D duplicate。
2. 如果预测某个 session 后续会 direct-to-D那么 P 侧在 radix/prefix cache eviction 时可以优先淘汰这部分 prefix cache。
3. 这样可以给 P 节点释放 cache 空间,提高 P 侧 prefix cache reuse。
4. 如果 P 侧 reuse 提升后 D 侧开始成为瓶颈,可以通过增加 xPyD 中 D 的配比,也就是增加 y缓解 decode 侧压力。
实验结果表明:第 2、3 点在 P cache 高压 workload 下成立;第 4 点只部分成立,因为瓶颈会从 decode prealloc 转移到 P->D transfer/bootstrap pipeline。
## 已实现机制
### 1. Trace profile 和 paired comparison
新增 `agentic_pd_hybrid profile` 子命令和 `src/agentic_pd_hybrid/profile.py`
能力:
- 统计 trace 的 session 数、turn2+ 数、append tokens、overlap ratio、direct-to-D eligible turn。
- 对比 baseline/candidate metrics输出 paired E2E latency delta。
- 用于解释 micro-benchmark 与 Ali filtered workload 的差异。
### 2. P 侧 priority eviction 支持
修改 SGLang server args允许
```bash
--radix-eviction-policy priority
```
router/replay 支持内部字段:
- `smg_prefill_priority`
- `smg_decode_priority`
router 会将内部字段剥离,只把标准 `priority` 分别传给 P/D backend。这样可以让 direct-to-D predicted session 在 P 侧使用更低优先级,例如 `-100`,普通请求使用 `100`
### 3. Kvcache seed/direct admission 控制
新增多种 seed/reseed 过滤:
- `kvcache_seed_max_resident_tokens`
- `kvcache_seed_max_output_tokens`
- `kvcache_seed_min_turn_id`
- `kvcache_seed_only_multiturn_sessions`
- `kvcache_seed_max_inflight_decode`
新增 P streaming session backup 策略:
- `release-after-transfer`P->D transfer 后释放 P 侧 session backup。
- `capacity-backup`:容量允许时保留 P 侧 backup。
当前主实验使用 `release-after-transfer`
### 4. 稳定性修复
之前 worker-admission 实验会在 replay 尾部卡住,不写出 metrics。原因是 benchmark 的长 `timeout_s=3600` 同时用于:
- SGLang stack 启动等待
- replay client 单请求
- router 到 P/D backend 的单请求
修复后新增:
- `BenchmarkConfig.request_timeout_s`
- CLI `benchmark-live --request-timeout-s`
- launch plan `router_request_timeout_s`
当前做法:
- `timeout_s=3600` 继续用于 SGLang 启动和整体 stack 等待。
- `request_timeout_s=180` 用于 replay client 和 router 到 P/D backend 的单请求。
- control-plane probe/open/close session 使用 2s timeoutfail closed。
修复效果worker-admission 从“尾部卡死不落盘”变为“卡住请求记录为 ReadTimeout整轮实验完成并写 metrics”。
## 关键实验结果
### P cache pressure 下 priority eviction 是否提升 P 侧 reuse
配置2P2DP `--max-total-tokens 90000`micro workload 316 requests / 58 sessions。
| 配置 | ok/total | mean E2E | p99 | request cached tokens | P log cached tokens | P new-token total |
|---|---:|---:|---:|---:|---:|---:|
| LRU | 314/316 | 28.171s | 43.409s | 8.204M | 7.783M | 2.236M |
| Priority | 314/316 | 28.165s | 41.935s | 8.401M | 7.981M | 2.039M |
结论:
- 在 P cache eviction 高频触发时priority eviction 确实提高 P 侧 prefix reuse。
- cached tokens 增加约 197knew prefill tokens 减少约 197k。
- 但 mean E2E 基本不变,说明性能瓶颈转移到 D decode/transfer。
### 增加 D 配比前的 D 侧瓶颈证据
2P2D priority pressure 下:
- decode `#queue-req`max/mean/p90 = 0/0/0
- decode token usagemax 0.98mean 0.842p90 0.96
- decode `#transfer-req`max 7mean 4.61p90 7
- decode `#prealloc-req`max 21mean 11.6p90 21
解释:
- D 侧不是普通 waiting queue 堆积。
- 真正压力在 token usage、transfer queue 和 prealloc queue。
### D scalingrouter admission 旧结果
| 配置 | ok/total | mean | p50 | p90 | p99 | error |
|---|---:|---:|---:|---:|---:|---:|
| 2P2D | 314/316 | 28.165s | 30.576s | 38.267s | 41.935s | 2 |
| 2P3D | 290/316 | 29.915s | 31.428s | 40.856s | 45.964s | 26 |
| 2P4D | 285/316 | 30.566s | 32.823s | 40.566s | 44.838s | 31 |
结论:
- 直接增加 D 不稳定。
- 2P3D/2P4D 的错误主要来自 `kvcache-centric` seed/direct 路径。
- 日志显示 decode 侧出现 `WaitingForInput` timeout 和 `KVTransferError`
### D scalingworker admission + request timeout 修复后
配置P `--max-total-tokens 90000`priority evictionworker admission`request_timeout_s=180`
| 配置 | ok/total | mean | p50 | p90 | p99 | error |
|---|---:|---:|---:|---:|---:|---:|
| 2P2D | 313/316 | 29.838s | 30.742s | 39.641s | 52.506s | 3 |
| 2P3D | 299/316 | 29.349s | 30.569s | 42.161s | 46.113s | 17 |
| 2P4D | 312/316 | 26.442s | 27.759s | 38.197s | 47.970s | 4 |
对应 decode log 摘要:
| 配置 | decode usage mean | decode transfer mean | decode prealloc mean |
|---|---:|---:|---:|
| 2P2D | 0.859 | 4.86 | 11.3 |
| 2P3D | 0.877 | 5.70 | 6.92 |
| 2P4D | 0.809 | 5.31 | 3.46 |
结论:
- 2P4D + worker admission + request timeout 是当前最好的 D scaling 配置。
- 相比旧 2P4D成功率从 285/316 提升到 312/316mean 从 30.566s 降到 26.442s。
- 但 p99 仍未稳定改善tail 仍由 P->D transfer/bootstrap timeout 主导。
- 2P3D 不稳定,错误 17 个,不适合作为当前推荐配置。
### 与默认 PD-disaggregation 的同配置对比
为了避免和旧 2P2D/no-pressure 基线混比,补跑了同一 workload、同一 2P4D、同一 P `--max-total-tokens 90000`、同一 `time_scale=50`、同一 `request_timeout_s=180` 的默认 PD-disaggregation 基线。
| 策略 | ok/total | mean | p50 | p90 | p99 | cached total | direct-to-D |
|---|---:|---:|---:|---:|---:|---:|---:|
| 默认 PD-disaggregation 2P4D | 316/316 | 29.210s | 28.940s | 47.434s | 52.605s | 8.165M | 0 |
| KVC 2P4D latency-best | 312/316 | 26.442s | 27.759s | 38.197s | 47.970s | 7.882M | 11 |
| KVC 2P4D seed-min2 | 316/316 | 26.729s | 25.840s | 43.589s | 50.426s | 8.337M | 3 |
结论:
- 如果只看成功请求延迟KVC 2P4D latency-best 相比默认 PD
- mean 改善约 9.5%
- p50 改善约 4.1%
- p90 改善约 19.5%
- p99 改善约 8.8%。
- 但 latency-best 有 4 个 `ReadTimeout`,全部来自 turn1 大 seed 的 P->D transfer/bootstrap timeout。
- `kvcache_seed_min_turn_id=2` 可以消除这些错误,达到 316/316 成功,同时 mean/p50/p90/p99 仍然优于默认 PD。
- 因此当前推荐分成两档:
- 追求最低 mean/p90使用 KVC 2P4D latency-best。
- 追求稳定性:使用 KVC 2P4D seed-min2。
### 进一步优化尝试
在当前最佳配置基础上尝试了三个优化方向:
| 策略 | ok/total | mean | p50 | p90 | p99 | 结论 |
|---|---:|---:|---:|---:|---:|---|
| KVC 2P4D latency-best | 312/316 | 26.442s | 27.759s | 38.197s | 47.970s | 延迟最优,但有 4 个 turn1 seed timeout |
| transfer-cap4 | 304/316 | 27.961s | 29.785s | 38.730s | 45.192s | 不可取,实时 transfer queue snapshot 滞后,错误更多 |
| disable-failed-session | 285/316 | 29.179s | 30.878s | 42.794s | 46.411s | 不可取,失败 session 降级会放大后端异常状态 |
| seed-min2 | 316/316 | 26.729s | 25.840s | 43.589s | 50.426s | 稳定性最优 |
| inflight0 | 316/316 | 29.497s | 31.186s | 43.498s | 48.022s | 太保守,几乎关闭 KVC 收益 |
从错误明细看latency-best 的 4 个错误全部是 turn1 seed timeout每个都是约 1250 KV blocks 的大 seed。`seed-min2` 跳过 turn1 seed 后,错误全部消失,说明当前主要稳定性问题是启动阶段 seed 风暴,而不是后续 direct append 本身。
transfer-cap4 没有效果,说明仅依赖 D worker 的实时 `decode_transfer_queue_reqs` 不够;并发请求可能同时读取到尚可的 snapshot然后一起进入 P->D transfer导致 backlog 在 admission 后形成。
## Ali filtered 当前状态
Ali filtered small-append trace
- 81 requests
- 28 sessions
- 53 turn2+
- max input 18901
- max output 1925
- span 5414s
PD baseline
- ok 81/81
- mean 9.072s
- p50 7.086s
- p90 21.761s
- p99 26.813s
Kvcache-centric 在 Ali filtered 上曾出现 58-67 个 router 200 后挂住、无 metrics 的问题。当前 request timeout 和 control-plane timeout 修复后,应重新跑 Ali filtered在未重跑前不把 Ali filtered 纳入最终性能结论。
## 当前结论
1. kvcache centric 可以提高 KV reuse但需要满足 workload 条件:
- session 有多 turn
- turn2+ append 较小;
- prefix overlap 高;
- P 侧 cache 有 eviction pressure
- D 侧 seed/direct admission 不把 transfer pipeline 打爆。
2. 不适合的 workload
- 单 turn 或 session 间隔过长;
- turn2+ append 很大direct-to-D 不能省掉多少 prefill
- prefix overlap 低;
- P cache 没有 eviction pressure
- D transfer/prealloc 已经高压。
3. 用户关于 P/D duplicate 的假设部分成立:
- 如果 D session 已经 residentP 侧对应 streaming session backup 可以视为 duplicate。
- `release-after-transfer` 可以避免长期保留 P/D 两份 session KV。
- priority eviction 进一步让 P 在必须 eviction 时优先淘汰 direct-to-D predicted session prefix。
4. 但当前机制还没有完全解决性能问题:
- P 侧 reuse 提升后E2E 不一定改善。
- 主要原因是 D 侧 transfer/bootstrap pipeline 成为瓶颈。
- 增加 D 可以降低 prealloc但不能自动降低 transfer backlog。
- 当前同配置下 KVC 已经可以优于默认 PD但必须在 latency-best 和 stable 两种策略之间取舍。
## 下一步优化方向
1. transfer-aware admission
- seed/direct 不只看 D token capacity也要看 `decode_transfer_queue_reqs``decode_prealloc_queue_reqs``decode_retracted_queue_reqs`
- 当 transfer queue 高时,应该主动走 PD fallback。
- 当前实验显示,单纯使用实时 transfer queue threshold 不够,需要配合本地 reservation。
2. per-D transfer budget
- 对每个 D 设置 seed/reseed 并发上限。
- 不能只按 session residency 或 token headroom 判断。
- 特别要限制 turn1 大 seed 的并发,避免启动阶段 seed 风暴。
3. P/D ratio 联合调度:
- 2P4D 当前最好,但 P queue 也随 D 增加而上升。
- 后续需要测试 3P3D、3P4D、4P4D 等组合,确认 P transfer source 是否成为瓶颈。
4. Ali filtered 重跑:
- 使用 request timeout 修复后的版本重新跑 Ali filtered。
- 如果仍然没有收益,需要按 session gap、append size、overlap ratio 分桶分析。
5. 更严格的成功率指标:
- 当前不能只看成功请求的 mean/p90。
- 必须同时报告 ok/total、timeout/error 类型和 tail latency。

View File

@@ -0,0 +1,367 @@
# KVC 实验踩坑记录与代码 Bug 分析v1 → v5
记录从 v1 到 v5 KVC 实验的踩坑过程、错误诊断、以及最终定位的代码 bug。
模型: Qwen3-30B-A3B (TP1),硬件: 单节点 8×H100 80GB。
Trace: `qwen35-swebench-50sess.jsonl`4449 请求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 416 | 1.0% | 54-58% | 1.08 / 0.84s | session-cap fb 35%、9-10% mooncake errors |
| v5 | Option Dworker-mode 驱动 seed/reseed | 0.9% | 41-45% | 1.59 / 1.31s | D KV pool 真容量不足 fallback 反而 46-51% |
`*` v2 P50 是假数字——超过半数请求只生成 1 token 就被 abort
## v2 踩坑Default policy 与 KVC 机制根本不兼容
### 表象
`scripts/sweep_tp1_v2_fixed.sh` 跑出来
- Exp18-way DPbaseline4449/4449 成功P50=0.65serror=0
- Exp21P7D KVC**2524 truncated (56.8%)**18 errorsP50=0.08s* ()
- Exp32P6D KVC**2733 truncated (61.4%)**17 errorsP50=0.08s* ()
每个截断请求 `actual_output_tokens=1``finish_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`
```python
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` 这两者的差别巨大
```python
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 policy`policies.py:DefaultPolicy`round-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_router``replay.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`
```diff
- --policy default
+ --policy kv-aware
```
`KvAwarePolicy` (`policies.py:146-187`) 做两件事
1. `_overlap_blocks` + `sticky_bonus` 给每个 D 打分session 自然粘在同一个 D**session 亲和性**
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` 原始代码
```python
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` 一行修改
```diff
- 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 + output`agentic 里常见 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
## v5 落地方案 Dworker-mode 驱动 seed/reseed
`scripts/sweep_tp1_v5_optD.sh` 真正把方案 D 落到了代码里改动核心 `--kvcache-admission-mode` `local`(replay 估算) 改成 `worker`(D 决策)并扩展到 **direct_append + seed + reseed 全部路径**
### 关键代码改动
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 不降反升**3746% 3551%)。说明 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 是部署变更
## 关键文件与代码位置索引
| 现象 | 代码位置 |
|------|----------|
| 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`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 evictionv5 引入 | `--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` |
| 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_reasonexecution_mode 这些原始字段交叉验证
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 决策是否在说谎

34
docs/archive/README.md Normal file
View File

@@ -0,0 +1,34 @@
# 归档文档说明
本目录保留项目历史阶段的过程文档。**新加入项目的 agent / 人员不需要阅读这些文档**,直接看 `docs/ONBOARDING_NEXT_AGENT_ZH.md` 即可。
保留它们的目的:
1. 论文写作时追溯 v1-v5 调优演化过程
2. 未来若回到 ts=10 高压区间或更大 trace 时,可参考当年的结构性问题诊断
3. 满足学术可追溯性要求
## 每个文档的简要说明
| 文档 | 归档原因 | 何时回头看 |
|---|---|---|
| `AGENTIC_FIT_ANALYSIS_ZH.md` | ts=10 时代的 §1-§7 结构性问题分析;结论已被 ts=1 数据全面 supersede | 想知道当年 ts=10 下我们认为有什么结构性问题时 |
| `STRUCTURAL_VALIDATION_REPORT_ZH.md` | 用 ts=10 数据对 AGENTIC_FIT_ANALYSIS 的 claim 做验证;同样被 ts=1 时代 supersede | 同上 |
| `KVC_DEBUG_JOURNEY_V1_TO_V5.md` | v1-v5 5 个调优 sweep 的过程笔记;包含 errors 9→912 漂移、direct-to-D 占比变化等历史数据 | 写 paper 时要写 "as we explored configurations v1-v5..." 段落 |
| `V5_PROFILE_INVESTIGATION_ZH.md` | 给 v5 加 1Hz polling instrumentation 的调查;让 errors 涨 46× 的现象记录 | 想理解 "admission RPC 干扰 scheduler 主循环" 这条 §5 残留风险时 |
| `REFACTOR_PLAN_ZH.md` | v0 重构计划,**已被 `REFACTOR_PLAN_V1_ZH.md` supersede** | 不需要看;只有想看作者一开始的设想时翻一翻 |
| `KVCACHE_CENTRIC_PROGRESS_ZH.md` | 项目最早期2026-04-27的进度记录当时还没有完整的 sweep 数据 | 几乎不需要看;满足"项目起源记录"职能 |
| `SWEBENCH_EXPERIMENT_PROGRESS.md` | SWE-Bench trace 早期实验进度记录 | 想知道当年的 trace 生成 / 采样配置时 |
| `SWEBENCH_EXPERIMENT_RESULTS.md` | 同上,早期 result snapshot | 同上 |
## 当前活跃文档(在 `docs/` 顶层)
跳转去看:
- `docs/ONBOARDING_NEXT_AGENT_ZH.md` — 新人上手手册
- `docs/PROJECT_OVERVIEW.md` — 项目目标 + 术语
- `docs/KVC_ROUTER_ALGORITHM.md` — 算法形式化
- `docs/V2_DEEP_ANALYSIS_ZH.md` — v2 完整分析
- `docs/V2_RESULTS_ZH.md` — v2 原始战报
- `docs/REFACTOR_PLAN_V1_ZH.md` — ts=1 方向决策
- `docs/MIGRATION_V1_FINDINGS_ZH.md` — v1 thrashing 诊断
- `docs/RESEED_SLOW_PATH_AND_D_TO_P_GAP_ZH.md` — reseed 长尾 + D→P 缺口审计
- `docs/TEAM_REPORT_AGENTIC_PD_HYBRID_ZH.md` — ts=10 时代的结构性问题清单(作为历史 baseline 仍在主目录)

View File

@@ -0,0 +1,123 @@
# Refactor Plan v0极简版
**日期**2026-05-06
**目标**:用最小改动 + 轻量实验,验证 `docs/AGENTIC_FIT_ANALYSIS_ZH.md` 提出的结构性缺陷是否真实存在、影响多大。
**预算**8h GPU 时间(约 4-6 次 ~30-60 min smoke run
**KISS 边界**:不动 SGLang `scheduler.py` 主循环结构;不引入新 mooncake 协议;不实现 cross-D session migration不做 admission probe/commit 拆分;不动 LRU eviction 策略。
## 计划结论(与用户已确认的)
回审 plan-v0 时发现两个原 Phase 1 改动**都不是 bug**
- `_estimate_session_resident_tokens` 返回 full prompt 是设计如此——所有需要"增量"的 call site 都已经做 `target - current` 减法(`replay.py:1247-1254``:1393-1394``:1490-1491`)。
- `decode_resident_blocks` 不缩减只是浪费几 MB 内存,**不影响 routing 决策**SWE trace 的 hash_ids 是 session-uniquepolicy 仍能正确选 D
最终极简版只做一件代码改动(**加 backpressure**+ 大量 instrumentation。
## 唯一代码改动Backpressure 信号
### 改动点 1SGLang `admit_direct_append` 响应增加两个字段
文件:`third_party/sglang/python/sglang/srt/managers/io_struct.py``scheduler.py`
```python
@dataclass
class DirectAppendAdmissionReqOutput:
... # 已有字段保留
recommended_pause_ms: int = 0 # 新增
queue_depth: int = 0 # 新增
```
`scheduler.py:admit_direct_append` 末尾计算 hint
```python
def _compute_backpressure_pause_hint(self) -> float:
depth = len(self.disagg_decode_transfer_queue.queue)
if depth < 8:
return 0.0
return min(2000.0, depth * 100.0) # 简单线性
```
### 改动点 2replay 端按 hint 退避
文件:`src/agentic_pd_hybrid/replay.py`
- `DecodeResidencyState` 新增 `pause_until_s: dict[str, float]`
- `_query_decode_direct_admission` 解析响应里的 `recommended_pause_ms`,更新 `pause_until_s[server_url] = now + pause_ms / 1000`
- 在调 `_invoke_router` / `_invoke_decode_session_direct` 前检查 `pause_until_s[decode_url]`,若 `now < pause_until` 则 sleep 到该时刻
### 改动点 3新 CLI flag
`src/agentic_pd_hybrid/cli.py``benchmark.py`
```
--enable-backpressure # 默认 false保留 baseline 行为
```
### 改动点 4观测日志
每个 run dir 新增三个 jsonl
- `admission-events.jsonl`:每次 admission RPCtimestamp, session, D, can_admit, queue_depth, pause_ms, latency_s, available_tokens, evicted_session_count
- `backpressure-events.jsonl`:每次实际 sleeptimestamp, D, sleep_ms, queue_depth_at_signal
- `session-d-binding.jsonl`:每个 session 第一次 open 在某 D 时记录timestamp, session, D, turn_id
## 实验矩阵8h 预算内)
按"先做 anchor再做单变量对照"排序。每行右侧是预估机时。
| ID | 配置 | 目的 | 机时 |
|---|---|---|---|
| **E0 (existing)** | v5 baselinetime-scale=10无 backpressure | Anchor已存在 `outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/run1` | 0 |
| **E1** | v5 + backpressure ONtime-scale=10全 trace | 验证 Claim §3backpressure 是否能消除 KVTransferError 雪崩) | ~50 min |
| **E2** | v5 baselinetime-scale=1**短 trace**(前 12 sessions ≈ 1000 reqs | 验证 Claim §7time-scale=10 失真);不开 backpressure | ~60 min |
| **E3** | 8DP CAtime-scale=1同 E2 trace | E2 的对照——真实时序下 KVC 是否仍输 DP | ~60 min |
| **E4** | v5 + backpressuretime-scale=1同 E2 trace | backpressure 在真实时序下还有用吗? | ~60 min |
| **E5**(备选) | v5 baselinetime-scale=10**concurrency=4**,全 trace | 验证 Claim §1高并发是不是必要条件 | ~50 min |
4-5 个 run~3-5h。剩余预算给失败重跑/分析。
## 实验目标——回到 §1-§7 一一对照
| 文档 § | Claim | 由哪个 exp 证伪/支持 | 需要的指标 |
|---|---|---|---|
| §1 | Session 永久 pin + 容量盲选造成双峰 | 已有 E0 数据足够 | direct-to-D rate per session distribution |
| §2 | LRU 跟不上压力 | 已有 E0 logs 足够 + E1 看 backpressure 之后 trim/error 比例变化 | trim 事件数 vs OOM 数 |
| §3 | 没 backpressure 是雪崩源 | E0 vs E1 | KVTransferError 数、P99 latency |
| §4 | admission RPC 干扰 scheduler | 不在本轮实验范围(需要 admission probe 拆分才能验,不做) | |
| §5 | P-side 不感知 D 健康 | 已有 E0 logs 足够prefill-0 vs prefill-1 错误数) | per-P KVTransferError |
| §6 | (已撤回) | | |
| §7 | time-scale=10 失真 | E0 vs E2同 KVC不同 time-scaleE2 vs E3同 time-scaleKVC vs DP | latency 分布、direct-to-D rate |
## Final 实验报告交付
跑完后输出 `docs/STRUCTURAL_VALIDATION_REPORT_ZH.md`,按 §1-§7 每条给出:
- **Claim 字面**
- **数据证据**(哪个 exp、哪个 metric
- **结论**:成立 / 部分成立 / 推翻
- **影响量化**:数字差异
- **不确定性**N=1 风险、其他 confounder
## 不做的事KISS 边界)
| 想做但不做 | 理由 |
|---|---|
| 跑 N=3 重复 | 8h 装不下single-run 可看大方向 |
| 全 sweep 参数 | 只调 time-scale 和 backpressure 一个 boolean |
| 改 LRU eviction | 不在本轮范围 |
| Cross-D migration | 不在本轮范围 |
| Admission probe/commit 拆分 | 不在本轮范围 |
| P-side D-health routing | 不在本轮范围 |
| 修两个"非 bug"estimate / aging | 验证后非真实 bug |
## 预期失败路径
- **GPU 资源紧张**smoke trace 进一步压缩(前 8 sessions / 600 reqs
- **time-scale=1 跑超 1.5h**:截断到 600s 内能完成的部分
- **backpressure 配错**:先用 sleep_ms = depth * 100 简单线性;调不通就回滚到 0无 backpressure
- **SGLang patch 编译错**:所有 patch 在 io_struct.py 和 scheduler.py 的少量行内,可单独 git restore
---
接下来:实现 → 跑 smoke → 写报告。

View File

@@ -0,0 +1,304 @@
# 结构性缺陷验证报告
**日期**2026-05-06
**对照数据源**
- `outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/`v5 KVC kv-aware Option D2P6D**3 次同配置 rerun**
- `outputs/qwen3-30b-tp1-exps/exp1_8way_dp_cache_aware_summary.json`(同 trace 8DP CA
- `outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/.../logs/decode-{0..5}.log``prefill-{0,1}.log`
**模型**Qwen3-30B-A3BTP1单机 8×H100 80GBtrace `qwen35-swebench-50sess.jsonl`4449 reqs / 52 sessions
**报告作用域**:验证 `docs/AGENTIC_FIT_ANALYSIS_ZH.md` §1-§7 提出的结构性 claim 是否真实存在;量化影响。
> ⚠️ **环境限制**:本轮缺 GPU 访问,未跑新 sweep。所有数据来自已存在的 v5 rerun + 8DP baseline。Backpressure 代码已实现但**未端到端验证**——下文标注为"预期收益pending GPU smoke"。
---
## 0. 实验有效性锚点N=1 不可信
3 次 v5 baseline EXP2**完全相同配置**)的 errors 漂移:
| Run | Errors | Lat P50 | Lat P90 | TTFT P50 |
|---|---:|---:|---:|---:|
| run1 | **372** | 1.11s | 8.65s | 0.147s |
| run2 | **912** | 0.94s | 7.68s | 0.071s |
| run3 | **396** | 1.22s | 8.43s | 0.183s |
errors 漂移 **2.5×**372 → 912P50 latency 漂移 **30%**。**任何 N=1 比较 < 30% 差异都不可信。** 后续所有" trace 不同配置 / 不同代码"的对比都需要 N3 才有意义
**对 KVC vs DP 的 headline 数据3 次 KVC 的最佳值P50=0.94s)仍然是 DPP50=0.65s)的 1.45×**——8 way DP 的优势远超 single-run variance 范围这一头条结论不受 variance 影响
---
## §1. Session 永久 pin 到 D + 容量盲选 → 极端双峰 ✅ 完全成立
### Claim
KvAwarePolicy 评分以 hash overlap 为主没有 D 容量项Session 第一次落到某 D 后被永久 pin导致大 session 在已满 D 上反复 admission 拒绝 session 在原 D 100% direct-to-D
### 数据
**(a) Session 永久绑定 3 rerun 一致**
```
run1: 52 sessions, avg distinct-D-per-session = 1.00
run2: 52 sessions, avg distinct-D-per-session = 1.00
run3: 52 sessions, avg distinct-D-per-session = 1.00
```
每个 session 在整个运行中只访问 **1 个** D worker3 次独立 run 完全一致。**不是巧合是结构。**
**(b) Direct-to-D 命中率呈极端双峰**
| Direct-to-D rate | run1 | run2 | run3 |
|---|---:|---:|---:|
| 0-20%饿死 | 15 | 18 | 16 |
| 20-40% | 7 | 6 | 7 |
| 40-60% | 11 | 7 | 9 |
| 60-80% | 5 | 6 | 4 |
| 80-100%顺利 | 14 | 15 | 16 |
中间态稀少两端拥挤
**(c) 3 run 一致饿死的 session session 大小强相关**
```
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
ratio: 1.98× — starved sessions are exactly 2× larger.
```
**13/52 = 25% 的 session 在 3 次独立 run 中都被饿死,且这些 session 的 peak input 恰好是顺利 session 的 2 倍。** 这排除了"运气"假说证实是大 session 在容量过载 D 上结构性失败
### 影响量化
- 25% session 几乎每个 turn 都走 fallback 路径相对 direct-to-D **TTFT 慢 100×、E2E 慢 6×**数据点fallback path mean lat ~3.5s vs direct ~0.5s
- 对应这些 session 的用户体验是"系统性糟糕"而不是"偶尔慢"
- **SLO 视角下 P99 完全由这 13 session 拉高**
### 结论
**完全成立**。修复方向不在本轮policy score capacity penalty + 允许 session D 迁移 D 端引入 hot session retract
---
## §2. D 端 LRU 只 evict idle session → 跟不上压力 ✅ 完全成立
### Claim
`scheduler.py:2040` `evict_idle_streaming_sessions_lru` 只能 evict "所有 req finished + streaming 模式" session高并发下 hot session 永远不 idleLRU 找不到东西可踢结果 D 顶到 100% 然后撞 mooncake transfer timeout
### 数据v5 baseline rerun run1
| D worker | Trim 事件 | 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 全部峰值 ≥ 0.97**其中 2 个直接顶到 1.00KV 池完全耗尽)。**LRU 触发 9-43 远不及 transfer 错误的 90-153 。**
decode-2 极端trim 16 vs error 153 = LRU 比错误慢 **9.5×**
### 影响量化
- run 累计 369 KVTransferError 6 D 之和
- 对应 ~8% 的请求失败率v5 errors 9/372/912 三次平均 ~430/4449 = 9.7%
- **每次 mooncake timeout 32s**—— P99 latency 直接贡献几十秒尾巴
### 结论
**完全成立**。修复方向不在本轮分层 eviction—— idle 外加冷 session retract按访问频率/时序加权Backpressure本轮代码只是把"D "的雪崩从"timeout 错误"转成"主动等待"**不是真正解决容量问题**。
---
## §3. 没有 D→Replay backpressure 通道 ✅ 成立(已实现修复)
### Claim
D transfer queue 32s timeout KVTransferError没有"D 过载请慢点"信号反向到 replayconcurrency 一直 32 不降
### 数据
- §2 369 KVTransferError 全部为 32s mooncake timeout日志中均为 `Failed to send kv chunk` `Decode instance could be dead`
- 错误集中在运行后半段按现有 `KVC_DEBUG_JOURNEY_V1_TO_V5.md` §v4错误均在 run 44.8% 之后开始累积
- 表明**前期 D 容量充裕时正常达到容量上限后所有后续请求集中失败**——典型无 backpressure 系统行为
### 修复(本轮已实现,待 GPU smoke 验证)
代码改动
1. `third_party/sglang/python/sglang/srt/managers/io_struct.py``DirectAppendAdmissionReqOutput` 增加 `recommended_pause_ms` 字段
2. `third_party/sglang/python/sglang/srt/managers/scheduler.py:admit_direct_append`基于 `transfer_queue_depth``retracted_queue_depth``token_usage_after` 计算 hint
```python
def _compute_backpressure_pause_hint(...):
if retracted_queue_depth > 0: return 1500
if token_usage_after >= 0.90: return max(200, min(2000, overshoot * 5))
if transfer_queue_depth >= 8: return min(2000, transfer_queue_depth * 100)
return 0
```
3. `src/agentic_pd_hybrid/replay.py`
- `DecodeResidencyState.pause_until_s: dict[str, float]`
- `_query_decode_direct_admission` 解析 hint 更新 `pause_until_s`
- 新增 `_wait_for_decode_pause`,在 `_invoke_router` / `_invoke_session_direct` 入口检查
4. CLI flag`--enable-backpressure`、`--backpressure-max-pause-s 2.0`(默认关闭)
5. 结构性日志:`structural/admission-events.jsonl`、`backpressure-events.jsonl`、`session-d-binding.jsonl`
### 预期收益pending GPU smoke E2 vs E1
- KVTransferError 应从 ~370 / 4449 跌到 < 50 / 4449
- P99 应改善(消除 32s timeout 尾巴)
- 整体 latency mean 可能**略升**(被强制 pause但 P99 应大幅降
- backpressure-events.jsonl 应显示 D-4 / D-5 累积大量 pause 事件(与 §2 数据吻合)
### 结论
**Claim 成立;修复已实现,待 smoke 验证**。注意backpressure 是**降级**机制,不是性能优化——它把"硬错误"换成"主动等待",整体 throughput 不会因此提升。
---
## §4. Admission RPC 与 scheduler 主循环耦合 ⚠️ 间接证据,本轮未直接验证
### Claim
`admit_direct_append` 进 scheduler 主循环遍历 session slotadmission RPC 频率 16+/s 时与 decode 抢调度。
### 现有间接证据
- `docs/V5_PROFILE_INVESTIGATION_ZH.md`:仅加 1Hz `/server_info` polling 就让 EXP2 errors 从 9 涨到 41546×但 v6 P0 三次 baseline 不开 polling 同样得到 372/912/396——**polling 不是唯一原因,主循环负载本身就敏感**。
### 本轮未做
- 没有"admission probe 拆 fast/slow"的对照实验。需要 SGLang 较深的改动(提供 lock-free snapshot不在 KISS 边界。
### 结论
**Claim 间接成立,本轮未直接验证**。Backpressure 实现里 admission RPC 的频率没有变(仍每个 turn 一次),只是结果会触发 sleep。如果这条 claim 成立,加 backpressure 后 admission RPC 数量大致不变但每次响应里的 `pause_ms` 会非零——**新增的 admission-events.jsonl 可在 GPU smoke 后用来直接验证此现象**。
---
## §5. P-side round-robin 不感知 D 健康 ✅ 成立
### Claim
`pd_router.py:_select_decode_index` 是裸 round-robin。任一 P 撞到 hot D 时反复失败,另一 P 完全不受影响。
### 数据v5 baseline rerun run1
| Worker | KVTransferError | "Decode could be dead" |
|---|---:|---:|
| prefill-0 | **367** | 361 |
| prefill-1 | **2** | 0 |
prefill-0 的请求量从 summary 看是 2225 vs prefill-1 的 2224——**请求量近乎对半,错误率差 180×**。
### 影响量化
- 失败请求集中在 P-0 → 某个 hot D 的链路上(日志中反复出现 `to 10.45.80.47:XXXXX`
- 单 P 的"死亡链路"贡献了 **99%** 的全部 KVTransferError
- 如果 P 选择能避开"正在和 hot D 死磕"的链路,**理论上可消除单 P 故障的雪崩效应**
### 备注
- 此现象**未在 v6 P0 的 3 次 rerun 中横向验证**——只有 run1 的日志可读。需要在新 sweep 的 prefill-{0,1}.log 上重复确认,避免 N=1 嫌疑。
### 结论
**单 run 数据成立,多 run 一致性未验证**。修复方向不在本轮router 选 P 时考虑 (P 当前 inflight transfer 数, 目标 D 健康度)。
---
## §6. 已撤回Replay 端 session footprint 估算膨胀
写计划时仔细看代码后撤回——`_estimate_session_resident_tokens` 返回 full prompt但所有需要"增量"的 call site (`replay.py:1247-1254`、`:1393-1394`、`:1490-1491`) 都已用 `target - current` 减法处理。**不是 bug**。
---
## §7. time-scale=10 把 inter-turn gap 压到 1/10 ✅ 完全成立
### 数据
```
原始 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:
p10=0.16s p50=0.25s p90=0.78s p99=2.5s max=26s
```
真实 agentic 用户/agent 在 turn 之间停 2-8 秒思考、打字、tool call、agent reasoning。time-scale=10 把这些窗口压到 0.16-0.78 秒——**人为消除了 D 的自然 idle 时间**,正好是 KVC 想利用的"session 短暂 idle 时 LRU 可以 evict、其他 session 可以 admit"机会。
### 测量学影响
- 所有 v3-v6 数据基于 time-scale=10
- 意味着所有"KVC 在 SWE 上输给 baseline"的结论**可能被 benchmark 放大了**
- §1 的 25% session 永久饿死现象,在 time-scale=1 下可能因为 D 有更多 drain 时间而显著缓解
### 本轮未做
- 没跑 time-scale=1 baseline。这是项目当前**最重要但缺失的验证**。
- Smoke sweep 脚本(`scripts/sweep_backpressure_smoke.sh`E3、E4 包含了 time-scale=1 的 KVC + DP 短 trace 对比,等 GPU 时跑。
### 结论
**Claim 完全成立time-scale=1 验证为 P0 待办**。
---
## 头条对比(同 trace、同硬件
```
8-way DP cache-aware (TP1):
errors= 0 | latency mean=1.426s p50=0.654s p90=3.609s
| TTFT mean=0.123s p50=0.093s p90=0.256s
KVC v5 2P6D (3 reruns, no polling):
run1: errors=372 | mean=3.50s p50=1.11s p90=8.65s | TTFT mean=2.13s
run2: errors=912 | mean=3.00s p50=0.94s p90=7.68s | TTFT mean=1.64s
run3: errors=396 | mean=3.42s p50=1.22s p90=8.43s | TTFT mean=2.07s
```
KVC 三次 run 全输 DP且差距远超 single-run variance
- Latency meanDP 优 **+110%**KVC 平均 3.30s vs DP 1.43s
- Latency P50DP 优 **+65%**KVC 平均 1.09s vs DP 0.65s
- TTFT meanDP 优 **+1500%**KVC 平均 1.95s vs DP 0.12s——慢 17×
- ErrorsDP 0 vs KVC 平均 ~560
**这是这个项目当前最严肃的事实**——所有 KVC 复杂度回报为负。
---
## 综合结论
按"是否结构性 + 影响大小"的二维分类:
| Claim | 结构性 | 影响 | 本轮验证 | 修复KISS 内) | 修复KISS 外) |
|---|---|---|---|---|---|
| §1 Session pin + 容量盲选 | 强 | 大25% session 饿死) | ✅ 3 run 一致 | ❌ | capacity-aware policy + 跨 D 迁移 |
| §2 LRU 跟不上 | 强 | 大(每次 ~370 KVTransferError | ✅ 6 D 数据 | ❌ | 分层 eviction、hot retract |
| §3 无 backpressure | 强 | 中-大(消除 32s timeout 雪崩) | ⚠️ 已实现,待 smoke | ✅ **本轮交付** | |
| §4 admission RPC 干扰 | 弱-中 | 中 | ⚠️ 间接 | ❌ | probe / commit_evict 拆分 |
| §5 P-side 不感知 D 健康 | 中 | 中(单 P 错误率差 180× | ✅ N=1需 N≥3 复核 | ❌ | router P 选择带 D 健康反馈 |
| §6 estimate 膨胀 | | | ❌ 已撤回 | | |
| §7 time-scale=10 失真 | 强(测量学) | 大(可能颠覆所有 KVC vs DP 结论) | ✅ 数据明确 | ✅ 改 flag | |
### 最关键的两个 takeaway
1. **§7 time-scale=1 是当前项目所有结论的前置依赖**——必须先做。如果 time-scale=1 下 KVC 与 DP 接近,前面所有 v3-v6 的"KVC 输得彻底"诊断都需要重新解读。
2. **§1 + §2 是双胞胎结构性问题**——session 被永久 pin 在某个 D + D 不能 evict 已满 = 大 session 永久卡死。任何不动 policy + 不动 LRU 的修复(包括本轮的 backpressure只能让症状好看不能消除根因。
---
## 本轮代码改动汇总git diff 范围)
```
src/agentic_pd_hybrid/replay.py # +结构性日志 + backpressure pause 检查 + admission 增强
src/agentic_pd_hybrid/cli.py # +CLI flags
src/agentic_pd_hybrid/benchmark.py # +CLI flags 透传
third_party/sglang/python/sglang/srt/managers/io_struct.py
third_party/sglang/python/sglang/srt/managers/scheduler.py
# +recommended_pause_ms 字段 + hint 计算
scripts/sweep_backpressure_smoke.sh # 4-run smoke sweep待 GPU 跑)
scripts/analysis/analyze_backpressure_smoke.py
# 配套分析器
docs/REFACTOR_PLAN_ZH.md # 计划文档
docs/STRUCTURAL_VALIDATION_REPORT_ZH.md
# 本报告
```
代码默认行为**不变**`enable_backpressure=False`)——所有现有脚本/配置无影响。
---
## 待 GPU 时执行
```bash
bash scripts/sweep_backpressure_smoke.sh
python3 scripts/analysis/analyze_backpressure_smoke.py outputs/sweep_backpressure_smoke
```
预算4 个 run × 30-60 min ≈ 3-4h GPU 时间。
按 §3 的预期E2 (KVC + backpressure) 相对 E1 (KVC baseline) 应有 errors 降 70%+P99 改善TTFT P50 持平或略升。E3 (KVC + backpressure @ time-scale=1) vs E4 (DP @ time-scale=1) 是验证 §7 的关键对照。
如果 E2 vs E1 的 errors 没有显著下降,说明 backpressure hint 公式调得不对(`_compute_backpressure_pause_hint` 阈值可调 §3 实际不是雪崩主因更可能是 §2 D-side LRU 才是)。

View File

@@ -0,0 +1,95 @@
# SWE-Bench PD Hybrid Experiment Progress
## 实验目标
在单节点 8xH100 上复现 agentic-pd-hybrid 三种 serving mechanism对比 Qwen3.5-35B-A3B 在 SWE-Bench 500 instance agentic trajectory 上的性能。
## 硬件环境
- 8x H100 80GB (NVLink 互联, 2 NUMA nodes: GPU 0-3 / GPU 4-7)
- 无 RDMA/IB 设备
- Transfer backend: **mooncake TCP** (nixl UCX 因 pip 包缺少 CUDA 支持导致 segfault已放弃)
## 实验矩阵
| 实验 | Mechanism | Workers | GPU 分配 | Router | Policy |
|------|-----------|---------|----------|--------|--------|
| A | pd-disaggregation | 1P + 1D (TP4 each) | P: 0-3, D: 4-7 | Yes | default |
| B | pd-colo | 2 direct (TP4 each) | D0: 0-3, D1: 4-7 | No | default |
| C | kvcache-centric | 1P + 1D (TP4 each) | P: 0-3, D: 4-7 | Yes | default |
## 测试负载
- 源数据: `simm-swe-bench/outputs/20260416-205833-hicache-qwen35-verified-0-500/audit.jsonl`
- 39,417 lines (turns), 497 unique instances (sessions)
- 每个 instance 8-150 turns (均值 79.3)
- 转换为 agentic-pd-hybrid trace 格式: `outputs/qwen35-swebench-500.jsonl`
## 关键发现
### Transfer Backend 选择
- **nixl (UCX)**: pip 安装的 nixl_cu12 包自带的 UCX 库没有 CUDA 支持,导致 GPU memory registration 时 segfault。系统 UCX (/opt/hpcx/ucx) 有 CUDA 支持但因 RPATH 无法被 NIXL 使用。
- **mooncake (TCP)**: 可用。需要两处修改:
1. `third_party/sglang/.../mooncake_transfer_engine.py`: 从环境变量 `MOONCAKE_PROTOCOL` 读取协议,而非硬编码 `"rdma"`
2. `src/agentic_pd_hybrid/stack.py`: 当 `transfer_backend == "mooncake"` 且非 `force_rdma` 时,自动设置 `MOONCAKE_PROTOCOL=tcp`
### 代码修改记录
1. **`third_party/sglang/python/sglang/srt/distributed/device_communicators/mooncake_transfer_engine.py`**
-`"rdma"` 硬编码改为 `os.environ.get("MOONCAKE_PROTOCOL", "rdma")`
2. **`src/agentic_pd_hybrid/stack.py`**
-`_build_process_env()` 中添加: mooncake 非 force_rdma 时默认设置 `MOONCAKE_PROTOCOL=tcp`
3. **`scripts/convert_audit_to_trace.py`** (新建)
- 将 sibench audit.jsonl 转换为 agentic-pd-hybrid trace 格式
## 实验进度
- [x] Step 0: 环境准备 (uv sync, nixl/mooncake 安装)
- [x] Step 1: Trace 格式转换 (39,417 lines 验证通过)
- [x] Step 2: Smoke test (pd-disaggregation, mooncake TCP, 100 requests) — **通过**
- 100/100 requests, 0 errors
- Mean latency: 1.53s, P50: 0.77s, P90: 2.82s
- TTFT: mean 0.49s, P50 0.29s; TPOT: mean 4.7ms
- 91/100 cache hits
- [x] Step 3a: 实验 A 全量尝试 (39K reqs, 497 sessions) — **中止**
- Run dir: `outputs/swebench-exps/pd-disaggregation-default-20260426T171113Z` (无metrics,被kill)
- 前 90% 完成 ~80min (~8-10 req/s), 但尾部 D 侧 KV cache 98% 饱和
- 497 并发 session 争抢 D 侧 token 空间, mamba 80-93 sessions 无法 drain
- **教训**: 1P+1D (TP4) 无法支撑 497 并发 session, 需减少 session 数量或降低 concurrency
- [x] Step 3b: 实验 A — pd-disaggregation (52 sessions, 4449 reqs, concurrency=32) — **完成**
- Run dir: `outputs/swebench-exps/pd-disaggregation-default-20260426T202540Z`
- Trace: `outputs/qwen35-swebench-50sess.jsonl` (10% sample, 52 sessions)
- **结果**: 4449/4449 成功, 0 errors
- Latency: mean=1.66s, P50=0.97s, P90=3.64s, P99=7.68s
- TTFT: mean=0.45s, P50=0.34s, P90=0.88s
- TPOT: mean=5.2ms, P50=5.2ms
- Cache hit: 4199/4449 (94.4%)
- [x] Step 4: 实验 B — pd-colo — **失败: SGLang bug**
- Run dir: `outputs/swebench-exps/pd-colo-default-20260426T210129Z`
- **Bug**: `--disaggregation-mode null` (colocation) 下 Qwen3.5-35B-A3B 模型触发 token_to_kv_pool_allocator 内存泄漏
- 错误: `ValueError: token_to_kv_pool_allocator memory leak detected!`
- 两个 direct worker 在处理 ~5 个请求后均 crash (Scheduler exception)
- **结论**: 当前 vendored SGLang v0.5.10 不支持 Qwen3.5-35B-A3B 的 colocation 模式
- [x] Step 5: 实验 C — kvcache-centric — **完成 (高错误率)**
- Run dir: `outputs/swebench-exps/kvcache-centric-default-worker-admission-20260426T210800Z`
- 4390/4449 errors (98.7%) — admission control 过于保守
- 59 成功请求: mean latency 1.24s (比 pd-disagg 快 25%), TTFT 0.18s (快 60%)
- 详细分析见 `docs/SWEBENCH_EXPERIMENT_RESULTS.md`
- [x] Step 6: 结果对比分析 — **完成**
- 完整报告: `docs/SWEBENCH_EXPERIMENT_RESULTS.md`
## 启动脚本
- `scripts/run_exp_a_pd_disagg.sh` — 实验 A
- `scripts/run_exp_b_pd_colo.sh` — 实验 B
- `scripts/run_exp_c_kvcache_centric.sh` — 实验 C
- `scripts/convert_audit_to_trace.py` — Trace 转换
## 已知风险
1. Qwen3.5-35B-A3B TP4 可用 mem ~12GB/GPU (after model + CUDA graph),长 session (150 turns) 可能 OOM
2. mooncake TCP loopback 延迟远低于真实跨机,结果偏乐观
3. 原始 trace 时间跨度 ~6000s全量回放非常耗时

View File

@@ -0,0 +1,121 @@
# SWE-Bench PD Hybrid Experiment Results
## 实验配置
- **模型**: Qwen3.5-35B-A3B (MoE, 35B total / 3B active), TP4
- **硬件**: 8x H100 80GB, NVLink, 单节点
- **Transfer backend**: mooncake TCP (loopback)
- **Trace**: 52 sessions, 4,449 requests (10% sample of SWE-Bench 500 instances)
- **时间压缩**: time-scale=10, concurrency-limit=32
## 结果汇总
### Experiment A: pd-disaggregation (baseline)
| Metric | Value |
|--------|-------|
| Run dir | `pd-disaggregation-default-20260426T202540Z` |
| Requests | 4,449 / 4,449 (100%) |
| Errors | 0 |
| **Mean Latency** | **1.662s** |
| P50 Latency | 0.973s |
| P90 Latency | 3.644s |
| P99 Latency | 7.676s |
| Mean TTFT | 0.445s |
| P50 TTFT | 0.340s |
| P90 TTFT | 0.880s |
| Mean TPOT | 5.20ms |
| Cache Hit Rate | 94.4% (4199/4449) |
| Mean Cached Tokens | 27,794 |
| KV Transfer Blocks | 105,235 |
### Experiment B: pd-colo (colocation) — FAILED
| Metric | Value |
|--------|-------|
| Run dir | `pd-colo-default-20260426T210129Z` |
| Status | **CRASHED** |
| Error | `token_to_kv_pool_allocator memory leak detected!` |
| Root Cause | SGLang v0.5.10 `--disaggregation-mode null` 与 Qwen3.5-35B-A3B (Mamba/GDN hybrid) 不兼容 |
| Requests | ~10 / 4,449 (0.2%) |
**结论**: 当前 vendored SGLang 不支持此模型的 colocation 模式。需要修复 token_to_kv_pool_allocator 中 Mamba 模型的内存管理。
### Experiment C: kvcache-centric (session-aware PD)
| Metric | Value |
|--------|-------|
| Run dir | `kvcache-centric-default-worker-admission-20260426T210800Z` |
| Requests | 4,449 total |
| **Errors** | **4,390 (98.7%)** |
| Successful | 59 (1.3%) |
| Mean Latency (success) | 1.238s |
| P50 Latency (success) | 0.484s |
| P90 Latency (success) | 2.550s |
| Mean TTFT (success) | 0.179s |
| P50 TTFT (success) | 0.081s |
| Mean TPOT (success) | 4.70ms |
| Direct-to-D Sessions | 56 |
| KV Transfer (actual) | 196 blocks (vs 105,235 planned) |
**Execution Mode 分布**:
- `kvcache-centric` (failed): 4,390
- `kvcache-direct-to-d-session` (success): 56
- `pd-router-*` variants: 3
## 关键分析
### 1. pd-disaggregation (A) — 稳定可靠
- 100% 成功率0 错误
- Mean latency 1.66s 合理 (包含 P→D KV transfer 开销)
- 94.4% cache hit 说明 prefix cache 在 P 侧工作良好
- KV transfer 105K blocks = 主要开销来源
- **适合生产使用**
### 2. pd-colo (B) — 不可用
- Qwen3.5-35B-A3B 的 Mamba/GDN hybrid 架构在 `disaggregation-mode null` 下触发内存泄漏
- 这是 SGLang 的 bug不是 agentic-pd-hybrid 的问题
- **需要 SGLang 修复后重新测试**
### 3. kvcache-centric (C) — Admission 过于保守
- 98.7% 错误率说明 admission control 拒绝了几乎所有请求
- `kvcache-seed-min-turn-id=2` 过滤了 turn 1 的 seed正确行为
- 但绝大多数 turn 2+ 请求也走 `kvcache-centric` 模式后失败
- 可能原因:
- Worker admission 查询发现 D 侧没有对应 session 的 KV cache因为 turn 1 没有 seed
- D 侧 transfer queue 积压导致 admission 拒绝
- 成功的 56 个 `direct-to-d-session` 请求表现优异: TTFT 0.08s (P50), 比 pd-disagg 的 0.34s 快 4x
- **需要调优 admission 参数,或使用 `kvcache-seed-min-turn-id=1` 允许 turn 1 seed**
### 4. kvcache-centric 成功请求 vs pd-disaggregation 对比
| Metric | pd-disagg (A) | kvcache-centric (C, success only) | Delta |
|--------|:---:|:---:|:---:|
| Mean Latency | 1.662s | 1.238s | **-25.5%** |
| P50 Latency | 0.973s | 0.484s | **-50.3%** |
| Mean TTFT | 0.445s | 0.179s | **-59.8%** |
| P50 TTFT | 0.340s | 0.081s | **-76.2%** |
| Mean TPOT | 5.20ms | 4.70ms | -9.6% |
| Actual KV Transfer | 105,235 blk | 196 blk | **-99.8%** |
**当 kvcache-centric 成功时,性能提升显著:**
- TTFT 降低 60-76% (D 侧直接 append无需 P→D transfer)
- 端到端 latency 降低 25-50%
- KV transfer 减少 99.8%
## 后续建议
1. **修复 pd-colo**: 提交 SGLang issue 关于 Mamba/GDN 模型在 disaggregation-mode null 下的内存泄漏
2. **调优 kvcache-centric admission**:
- 尝试 `--kvcache-seed-min-turn-id 1` 允许 turn 1 seed
- 放宽 `--kvcache-seed-max-decode-transfer-queue-reqs` 阈值
- 使用 `--kvcache-admission-mode router` (shadow state, 不在 critical path)
3. **增加 D 侧内存**: 调整 `--mem-fraction-static` 给 KV cache 更多空间
4. **多 P/D 配置**: 测试 2P2D (TP2) 配置以增加并行度
## 实验日期
2026-04-27

View File

@@ -0,0 +1,305 @@
# 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_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,PD 传输已开始**,死于下游(server-side abort流被关生成阶段失败)。
- **`session_reused=False` 415/415**(全部是 seed,无一是 direct append)。
- **失败集中在 18 unique session**(top 5: 58080decode-5 66 errs / 70560decode-2 54 / 67200decode-4 40 / 59200decode-4 35 / 77280decode-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 ,而是 PD 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(已扩充)
1. `held_tokens` 语义在原稿被解读颠倒,引发 `other` 的因果归因错误(已纠正, §1.2)。
2. `other` 字段是计算所得且**未细分**,无法直接归因需要 P1 instrument 才能区分 radix-cacherunning batchinflight
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 = 92086` `token_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_idinput 长度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` 拿机器可读输出)。