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>
642 lines
30 KiB
Markdown
642 lines
30 KiB
Markdown
# agentic-pd-hybrid 现框架性能与结构性问题报告
|
||
|
||
**对象**:项目团队同学
|
||
**前置假设**:读者**没看过** v3-v6 KVC 实验日志
|
||
**数据范围**:项目仓库 `outputs/` 下截止 2026-05-06 的全部实验产物
|
||
**目的**:把"现状"和"问题"分别交代清楚,给后续改造提供共同事实基础
|
||
|
||
---
|
||
|
||
## 0. 给没看过实验的读者:基础概念速览
|
||
|
||
### 0.1 项目目标
|
||
验证 **session-aware / KV-cache-aware P/D routing** 在 **agentic coding workload**(多轮 session、长 context、增量 append)上能否降低端到端延迟。基线对比对象是 vanilla SGLang xPyD。
|
||
|
||
### 0.2 三种部署机制(**这三个名词全程会用**)
|
||
|
||
| 机制 | 形态 | KV 流向 |
|
||
|---|---|---|
|
||
| **pd-disaggregation**("PD disagg") | P 和 D 是独立进程、分占不同 GPU | 每个请求 P 算 prefill → mooncake 推 KV → D 解码 |
|
||
| **pd-colo**("DP",data-parallel) | 没有 PD 拆分,N 个独立完整 worker(每个自己 prefill+decode) | 没有 KV transfer;router 按 hash 分配请求 |
|
||
| **kvcache-centric**("KVC") | 部署形态同 PD disagg;**D 上多了 SessionAwareCache**,能跨 turn 保留 session KV | 运行时决策:可走 direct-to-D(无 P)、可走 P→D disagg、可走带 reseed 的混合 |
|
||
|
||
**Direct-to-D**("D-direct"):KVC 的快路径——D 上已有该 session 的 KV,新 turn 在 D 本地做 append-prefill,零 P 介入、零 mooncake transfer。这是 KVC 理论上能省时间的核心。
|
||
|
||
**Fallback**:KVC admission 拒了 / 阈值不满足 / D 不健康时,退化到普通 PD disagg 路径。
|
||
|
||
**Routing policy**(与机制正交):
|
||
- `default`:纯 round-robin
|
||
- `sticky`:turn 2+ 黏到 session 的 last D
|
||
- `kv-aware`:按 hash overlap + sticky 评分选 D(**KVC 必须配它**才能正确工作)
|
||
|
||
### 0.3 数据来源
|
||
- Trace:`outputs/qwen35-swebench-50sess.jsonl`(SWE-Bench 抽样,4449 reqs / **52 sessions** / 每 session 8-150 turns / time-scale=10 / concurrency=32)
|
||
- 模型:Qwen3.5-35B-A3B (TP4) 和 Qwen3-30B-A3B (TP1) 两组
|
||
- 硬件:单机 8×H100 80GB,mooncake TCP loopback 模拟 P→D 传输
|
||
|
||
---
|
||
|
||
# 第一部份:性能数据现象
|
||
|
||
## 1.1 三种机制在 Qwen3.5-35B (TP4) SWE 50sess 上的表现
|
||
|
||
来源:`outputs/swebench-exps/`。
|
||
|
||
| Run | Mechanism | Policy | Errors | Lat mean | Lat P50 | Lat P99 | TTFT mean | TTFT P50 |
|
||
|---|---|---|---:|---:|---:|---:|---:|---:|
|
||
| `pd-disaggregation-default-20260426T202540Z` | pd-disagg | default | **0/4449** | 1.66s | 0.97s | 7.68s | 0.45s | 0.34s |
|
||
| `pd-colo-default-20260426T210129Z` | pd-colo | default | **4447/4449** | – | – | – | – | – |
|
||
| `pd-colo-default-20260427T033519Z` | pd-colo | default | **0/4449** | 1.77s | 0.86s | 9.67s | 0.29s | 0.25s |
|
||
| `pd-colo-kv-aware-20260427T042034Z` | pd-colo | kv-aware | 469/4449 | 1.52s | 0.82s | 8.27s | 0.26s | 0.23s |
|
||
| `pd-colo-kv-aware-20260427T044944Z` | pd-colo | kv-aware | **0/4449** | **1.57s** | 0.81s | 8.48s | **0.22s** | **0.17s** |
|
||
| `kvcache-centric-default-worker-admission-20260426T210800Z` | KVC | default | **4390/4449** | – | – | – | – | – |
|
||
|
||
### 现象解读
|
||
|
||
**(1) pd-disagg 是稳定基线**:1.66s mean / 0 errors / 4199 cache hits(94.4%)。可以正常服务。
|
||
|
||
**(2) pd-colo(DP)有两次 run,第一次几乎全 crash,第二次稳定**:
|
||
- 04-26 的 4447/4449 errors 来自 SGLang `--disaggregation-mode null` + Qwen3.5-35B-A3B(Mamba/GDN hybrid)的 `token_to_kv_pool_allocator memory leak` bug,crash 了
|
||
- 04-27 的两次 pd-colo run 都跑通了。**`pd-colo-kv-aware-20260427T044944Z` 是这一组实验里跑分最好的配置**——0 errors / TTFT P50 = 0.171s(pd-disagg 的 50%)
|
||
|
||
**(3) KVC 在 SWE 35B 上的唯一一次 run 几乎全 crash**:4390/4449 = 98.7% errors。但**那 56 个跑通的 direct-to-D 请求性能优异**——Lat mean 1.24s,TTFT P50 0.081s,KV transfer 196 块(vs PD disagg 的 105K 块,**−99.8%**)。说明 KVC 机制本身有效,但 admission control 把绝大多数请求过滤掉了。
|
||
|
||
### 一句话:在 Qwen3.5-35B 上,**pd-colo + kv-aware 是头名**,KVC 机制配置不当几乎不可用。
|
||
|
||
---
|
||
|
||
## 1.2 同 trace 切到 Qwen3-30B (TP1):v1→v6 演进
|
||
|
||
为绕开 Mamba 模型的 SGLang bug,团队后续切到 Qwen3-30B-A3B (TP1) 跑 KVC 调优 sweep。**所有结果用同一份 SWE 50sess trace**,可以横向比较。来源:`outputs/qwen3-30b-tp1-*` 各目录。
|
||
|
||
### 1.2.1 各版本配置概览
|
||
|
||
| 版本 | 关键改动(一句话) |
|
||
|---|---|
|
||
| v2 | KVC + `--policy default`(这个 policy 选择 **是 bug**,下文 §2.5) |
|
||
| v3 | KVC + `--policy kv-aware` |
|
||
| v4 | v3 + replay 端 session soft_cap 从 4 抬到 16 |
|
||
| v5 (Option D) | 把 admission 决策从 replay 估算改成 D worker 真实容量回答(`worker-mode admission`) |
|
||
| v5+profile | v5 + 1Hz `/server_info` polling 做时序 instrument |
|
||
| v6 P0 | v5 baseline 同配置 rerun ×3 验证可复现性 |
|
||
|
||
### 1.2.2 各版本同 trace 结果总表
|
||
|
||
| 版本 | Errors | Lat mean | Lat P50 | Lat P90 | Lat P99 | TTFT P50 | direct-to-D% |
|
||
|---|---:|---:|---:|---:|---:|---:|---:|
|
||
| **8-way DP cache-aware** | **0** | **1.43s** | **0.65s** | **3.61s** | **8.37s** | **0.093s** | – |
|
||
| v3 1P7D KVC | 363 (8.2%) | 4.88s | 1.75s | 12.67s | 28.72s | 0.363s | 39% |
|
||
| v3 2P6D KVC | 9 (0.2%) | 3.58s | 1.52s | 9.23s | 18.70s | 0.328s | 31% |
|
||
| v4 1P7D cap=16 | 435 (10%) | 4.21s | 1.08s | 13.38s | 24.45s | 0.056s | 49% |
|
||
| v4 2P6D cap=16 | 403 (9%) | 2.51s | 0.84s | 6.51s | 18.34s | 0.051s | 53% |
|
||
| v5 1P7D Option D | 9 (0.2%) | 5.18s | 1.59s | 14.67s | 26.09s | 0.207s | 45% |
|
||
| v5 2P6D Option D | 9 (0.2%) | 3.49s | 1.31s | 9.09s | 24.92s | 0.244s | 41% |
|
||
| v5+profile 1P7D | 6 (0.1%) | 4.21s | 1.18s | 11.33s | 28.83s | 0.060s | 55% |
|
||
| v5+profile 2P6D | **415 (9.3%)** | 3.23s | 1.11s | 8.36s | 20.26s | 0.168s | 41% |
|
||
| v5 rerun ×3(无 profile) | **372 / 912 / 396** | 3.00–3.50s | 0.94–1.22s | 7.68–8.65s | 18.97–20.37s | 0.07–0.18s | 40-42% |
|
||
|
||
**8DP CA 在每一项指标都是头名**:
|
||
- Latency mean **比所有 KVC 配置好 +43%~+260%**
|
||
- TTFT P50 **0.093s**(KVC 最佳 v4 2P6D 是 0.051s——TTFT 单项 KVC 是有优势的,但被整体 P99 灾难抵消)
|
||
- 0 errors(KVC 任一配置 errors 在 9-912 之间漂移)
|
||
|
||
### 1.2.3 v5+profile 的诡异:加 1Hz polling 让 errors 从 9 涨到 415
|
||
|
||
这条单独看:v5 baseline 跑出来 9 errors,加上 1Hz `/server_info` polling 之后 415 errors(**46×**)。原因机理见 §2.5。
|
||
|
||
### 1.2.4 v6 P0 用 ×3 rerun 验证可复现性,结果是不能复现
|
||
|
||
**关键事实**:v5 baseline 完全相同配置跑 3 次:
|
||
|
||
| Run | Errors | Lat mean | Lat P50 | TTFT P50 |
|
||
|---|---:|---:|---:|---:|
|
||
| rerun1 | **372** | 3.50s | 1.11s | 0.147s |
|
||
| rerun2 | **912** | 3.00s | 0.94s | 0.071s |
|
||
| rerun3 | **396** | 3.42s | 1.22s | 0.183s |
|
||
|
||
errors 漂移 **2.5×**(372→912)。Latency mean / P50 也漂移 ~30%。**这意味着 v3-v6 之前所有"single-run"对比的差异 < 30% 的都不可信。**
|
||
|
||
但要注意:**3 次 v5 中最优的 P50(0.94s)仍然比 8DP CA(0.65s)慢 1.45×**——这个差距大于 single-run variance,所以"DP 全胜 KVC"的头条结论不受 variance 影响。
|
||
|
||
### 1.2.5 一个有趣的反差:v4 vs v5
|
||
|
||
- v4:errors 多(~10%)、direct-to-D 占比高(53-58%)、整体 P50 较好(0.84s)
|
||
- v5:errors 少(0.2%)、direct-to-D 占比降低(41-45%)、整体 P50 反而退步(1.31s)
|
||
|
||
**v5 没有让性能变好,只是把"硬错误"转成了"诚实拒绝"——v4 的 admission 是乐观估算,admit 进来后 D 装不下变成 mooncake 32s timeout(统计成 errors);v5 让 D 自己拍板,admit 拒得早,请求改走 fallback(统计成低 direct-to-D 率)。容量本身没变。**
|
||
|
||
---
|
||
|
||
## 1.3 microbench 上 KVC 击败 PD disagg —— 但本仓库没保留实际 run
|
||
|
||
`docs/PROJECT_OVERVIEW.md` 写明:
|
||
|
||
> micro-benchmark 上,`kvcache-centric` 可以比 `pd-disaggregation` 好。原因很简单:**session 少、D KV 放得下**,turn2+ 可以直接走 D session。
|
||
|
||
但 `outputs/` 里**没有** microbench 实际 run(只有 microbench trace 生成器 `microbench.py` 和它的几个示例 trace 文件)。所以 microbench 的"KVC 赢"是基于设计预期 + 历史口口相传,**没有可重现的产物**。
|
||
|
||
**这本身是个问题**——下文 §2.6 会解释 microbench 的默认参数(4 sessions × 30K input × 1K append)正好把所有 KVC 失效条件都规避掉了。
|
||
|
||
---
|
||
|
||
## 1.4 头条结论(Part 1 总结)
|
||
|
||
| 工作负载 / 模型 | 头名机制 | KVC 表现 |
|
||
|---|---|---|
|
||
| Microbench(8 session × 30K × 1K append) | KVC > PD disagg(无落地数据,按设计) | 设计上必然赢 |
|
||
| SWE 35B (TP4) | **pd-colo + kv-aware**(1.57s mean, 0 errors) | KVC 唯一 run 中 98.7% errors |
|
||
| SWE 30B (TP1) | **8-way DP cache-aware**(1.43s mean, 0 errors) | KVC 6 个配置全输;最佳的 v4 2P6D 慢 75%、errors 9% |
|
||
|
||
**真实 agentic 工作负载(SWE-Bench)上,KVC 机制目前没有任何配置能跑赢 naive DP cache-aware。**
|
||
|
||
---
|
||
|
||
# 第二部份:结构性问题分析
|
||
|
||
每条按 (1) 现象(实锤数据)、(2) 根因(代码位置)、(3) 影响量化 三段交代。
|
||
|
||
## 2.1 KvAwarePolicy 不感知 D 容量 + Session 永久 pin 在初始 D 上 ★ 最严重
|
||
|
||
### 2.1.1 现象(实锤)
|
||
|
||
**(a) 每个 session 整 run 中只访问 1 个 D**——基于 v5 rerun1/2/3 全部 4449×3 = 13347 条 metrics:
|
||
|
||
| Run | sessions | avg distinct-D-per-session |
|
||
|---|---:|---:|
|
||
| rerun1 | 52 | **1.00** |
|
||
| rerun2 | 52 | **1.00** |
|
||
| rerun3 | 52 | **1.00** |
|
||
|
||
3 次独立 run、156 次 session 实例,**没有一个** session 跨 D 迁移过。
|
||
|
||
**(b) Direct-to-D 命中率呈极端双峰**——以 rerun1 为例(其他两次形态相同):
|
||
|
||
| direct-to-D rate | session 数 |
|
||
|---|---:|
|
||
| 0–20%("饿死") | **15** |
|
||
| 20–40% | 7 |
|
||
| 40–60% | 11 |
|
||
| 60–80% | 5 |
|
||
| 80–100%("顺利") | **14** |
|
||
|
||
中间档稀少,两端拥挤。
|
||
|
||
**(c) 跨 3 次 run 一致饿死的 session = 13/52,且这些 session 的 input 是顺利 session 的 1.98×**:
|
||
|
||
```
|
||
13 sessions starved (<20% direct-to-D) in ALL 3 runs
|
||
avg peak input of consistently-starved sessions: 62043 tokens
|
||
avg peak input of consistently-lucky sessions: 31344 tokens
|
||
```
|
||
|
||
**结构性、可复现、与 session 大小强相关。** 排除"运气"假说。
|
||
|
||
### 2.1.2 根因(代码)
|
||
|
||
`policies.py:166-172` `KvAwarePolicy.select()` 评分函数:
|
||
|
||
```python
|
||
score = (
|
||
overlap + sticky * self.sticky_bonus, # 主项:历史 KV overlap
|
||
sticky, # 二级
|
||
inflight_penalty, # 三级
|
||
assignment_penalty, # 四级
|
||
)
|
||
```
|
||
|
||
**评分中完全没有 D 当前容量项**。
|
||
|
||
session X 第一次落到 D-2 → 在 D-2 上积累 hash_id → 之后不管 D-2 多满,X 的 turn N+1 的 overlap 在 D-2 上仍是最大 → 永远选 D-2。即使 D-5 全空也轮不到。
|
||
|
||
`RoutingState.decode_resident_blocks` (`policies.py:46`) 还从不缩减——但因为 SWE trace 的 hash_ids 是 session-unique,**不缩减并不影响"选对 D",只影响内存**——真正问题在评分函数无容量项。
|
||
|
||
### 2.1.3 影响量化
|
||
|
||
- 25%(13/52)的 session 几乎每个 turn 走 fallback 路径
|
||
- fallback 路径 mean lat 约 3.5s vs direct-to-D ~0.5s——**饿死 session 每 turn 慢 6×**
|
||
- 这 13 个 session 还容易撞 mooncake 32s timeout(见 §2.2、§2.3),P99 完全由它们决定
|
||
- **SLO 视角下:25% 的用户体验是系统性糟糕**
|
||
|
||
---
|
||
|
||
## 2.2 D 端 LRU 只能 evict idle session → 跟不上压力
|
||
|
||
### 2.2.1 现象(实锤)
|
||
|
||
来源:`outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/.../logs/decode-{0..5}.log`,全 run 计数:
|
||
|
||
| D worker | "Trimmed decode session cache" 事件 | KVTransferError | 峰值 token_usage |
|
||
|---|---:|---:|---:|
|
||
| decode-0 | 9 | 0 | 0.99 |
|
||
| decode-1 | 43 | 4 | 0.99 |
|
||
| decode-2 | 16 | **153** | 0.97 |
|
||
| decode-3 | 37 | 29 | 0.99 |
|
||
| decode-4 | 28 | **90** | **1.00** |
|
||
| decode-5 | 30 | **93** | **1.00** |
|
||
|
||
**所有 6 个 D 都顶到 token_usage ≥ 0.97,2 个顶到 1.00(KV 池完全耗尽)。LRU 触发 9-43 次,远不够——transfer 错误是 LRU 触发量的 5-10×。**
|
||
|
||
decode-2 极端:trim 16 次 vs error 153 次 = LRU 跑得比错误慢 9.5×。
|
||
|
||
### 2.2.2 根因(代码)
|
||
|
||
`scheduler.py:2040` 的 `evict_idle_streaming_sessions_lru` 实际只能 evict:
|
||
|
||
> 所有 req 都 finished + streaming 模式 + 该 session 没有 inflight transfer
|
||
|
||
但 SWE 高并发(concurrency=32 + time-scale=10 → effective inter-turn gap p50=0.25s)下,每个 session 几乎一直有 inflight req。**hot session 永远不 idle,LRU 永远找不到东西可踢。**
|
||
|
||
### 2.2.3 影响量化
|
||
|
||
- 单 run 累计 KVTransferError:6 个 D 之和 = **369 次**
|
||
- 对应 ~8% 请求失败率(v5 errors 9/372/912 三次平均 ~430/4449 = 9.7%)
|
||
- **每次 mooncake timeout = 32s**——直接构成 P99 18-26s 的尾巴
|
||
|
||
修复需要 SGLang 内部分层 eviction:除 idle session 外,按访问频率 / 时序加权强制 retract——**不在当前 KISS 边界**。
|
||
|
||
---
|
||
|
||
## 2.3 没有 D → Replay backpressure 通道
|
||
|
||
### 2.3.1 现象
|
||
|
||
§2.2 数据显示 D 顶到 token_usage=1.00 时仍在持续接收新请求,最终撞 mooncake 32s timeout。**整个错误链路里没有"D 过载,请慢点发"的反向信号**。
|
||
|
||
定量证据:rerun1 的 KVTransferError 时间分布——**98% 集中在 run 后半段**(参考 `KVC_DEBUG_JOURNEY_V1_TO_V5.md` §v4)。前期 D 容量充裕时正常,达到上限后**所有后续请求集中失败**——典型的"无 backpressure 系统在过载点雪崩"模式。
|
||
|
||
### 2.3.2 根因(代码)
|
||
|
||
链路:
|
||
|
||
```
|
||
replay 端按 trace 时序 + concurrency=32 持续发请求
|
||
↓
|
||
PD Router 裸 round-robin (pd_router.py:43-49)
|
||
↓
|
||
P 收到请求做 prefill → mooncake 推 KV → D 端
|
||
↓
|
||
D 端 transfer queue 堆积 → 32s timeout
|
||
↓
|
||
errno 抛回 replay → fallback 路径,但 concurrency 不降
|
||
```
|
||
|
||
D 端的 `admit_direct_append` 响应里**只有 can_admit/reason 等过去时字段,没有任何"建议节流"的指示**。
|
||
|
||
### 2.3.3 修复(本次代码改动已实现)
|
||
|
||
代码已加 `recommended_pause_ms` 字段:
|
||
- `third_party/sglang/.../io_struct.py:DirectAppendAdmissionReqOutput` 增加 `recommended_pause_ms: int = 0`
|
||
- `scheduler.py:_compute_backpressure_pause_hint`:按 `transfer_queue_depth`、`retracted_queue_depth`、`token_usage_after` 计算
|
||
- `replay.py`:admission 响应里读到 hint → 更新 `DecodeResidencyState.pause_until_s[D]` → 下次发到该 D 之前 sleep
|
||
- CLI flag:`--enable-backpressure`(默认 off,保留 baseline 行为)
|
||
- 同时新增 3 个结构性日志(`structural/admission-events.jsonl` / `backpressure-events.jsonl` / `session-d-binding.jsonl`)
|
||
|
||
**待 GPU smoke 验证。预期 errors 从 ~370 降到 < 50;P99 改善(消除 32s timeout 尾巴);mean latency 可能略升(被强制 sleep)。**
|
||
|
||
修复脚本:`scripts/sweep_backpressure_smoke.sh`(4 个 run × 30-60 min);分析器:`scripts/analysis/analyze_backpressure_smoke.py`。
|
||
|
||
### 2.3.4 注意
|
||
|
||
backpressure 是**降级机制**,不是性能优化——它把"硬错误(32s timeout)"换成"主动等待"。整体 throughput 不会因此提升,但 P99 应大幅改善。
|
||
|
||
---
|
||
|
||
## 2.4 P-side round-robin 不感知 D 健康
|
||
|
||
### 2.4.1 现象(实锤)
|
||
|
||
来源:v5 rerun1 `prefill-{0,1}.log`,全 run 计数:
|
||
|
||
| Worker | KVTransferError | "Decode instance could be dead" | 请求量 |
|
||
|---|---:|---:|---:|
|
||
| prefill-0 | **367** | 361 | 2225 |
|
||
| prefill-1 | **2** | 0 | 2224 |
|
||
|
||
**两 P 请求量完全均衡(round-robin),错误率差 180×**。日志里 prefill-0 的失败反复指向某个特定 D 的 IP(`to 10.45.80.47:XXXXX`)。
|
||
|
||
### 2.4.2 根因(代码)
|
||
|
||
`pd_router.py:43-49`:
|
||
|
||
```python
|
||
prefill_url, bootstrap_port = self.config.prefill_urls[
|
||
self.prefill_cursor % len(self.config.prefill_urls)
|
||
]
|
||
self.prefill_cursor += 1
|
||
```
|
||
|
||
裸 round-robin。不感知:
|
||
- P 当前 inflight transfer 数
|
||
- 目标 D 的健康状态 / 容量
|
||
|
||
后果:当某个 D 进入 hot 状态时,被 round-robin 派去给它推 KV 的 P **持续失败**;另一个 P 接到的请求恰好命中健康 D,完全没事。**单 P 故障不会被路由层避开。**
|
||
|
||
### 2.4.3 影响量化
|
||
|
||
- prefill-0 几乎独自承担了**全部 KVTransferError 的 99%**(367/(367+2))
|
||
- 如果 router P 选择能避开"正在和 hot D 死磕"的链路,这部分 ~8% 的整体错误率应可降到 < 1%
|
||
|
||
### 2.4.4 备注
|
||
|
||
这条结论目前来自单次 run 的 N=1 数据。需要跨 N≥3 次 rerun 验证一致性才能完全确信——加上 §2.1.1 (b/c) 也证明 P-D 链路绑定结构性强相关,"prefill-0 死磕某 D"很可能在每次 run 都重复(由初始 session 落点决定)。
|
||
|
||
---
|
||
|
||
## 2.5 Admission RPC 进 scheduler 主循环 → 自我干扰
|
||
|
||
### 2.5.1 现象(实锤)
|
||
|
||
v5 baseline 配置不开 polling:errors = 9
|
||
完全相同配置 + 1Hz `/server_info` polling:errors = **415**(**46×**)
|
||
|
||
来源:`outputs/qwen3-30b-tp1-v5-optD/exp2_2p6d_kvc_optD_summary.json`(baseline 9 errors)vs `qwen3-30b-tp1-v5-optD-profile/exp2_2p6d_kvc_optD_profile_summary.json`(415 errors)。
|
||
|
||
### 2.5.2 根因(代码)
|
||
|
||
`/server_info`(被 polling 调用)和 `admit_direct_append` 都进 SGLang scheduler 主循环:
|
||
|
||
- `/server_info` → `scheduler.py:get_streaming_session_cache_status` → 遍历每个 session slot 计算 `is_idle`
|
||
- `admit_direct_append` → 读 `token_to_kv_pool_allocator.available_size()` + 触发 `maybe_trim_decode_session_cache`
|
||
|
||
scheduler 主循环本身在跑 decode/prefill 的 forward。这些 RPC 进队列就和 forward 抢调度。
|
||
|
||
### 2.5.3 真实负载下 admission RPC 频率远高于 1Hz
|
||
|
||
- 4449 reqs / ~2700s ≈ **1.6 reqs/s**
|
||
- 每个 turn 做 1-3 次 admission probe(direct-append + 可能的 seed retry)
|
||
- × 8 worker = **每秒 ~16-40 次 admission RPC**
|
||
|
||
也就是 admission 流量本身比 1Hz polling 高一个量级。如果 1Hz polling 都能让 errors 涨 46×,admission 自己的扰动至少同等。
|
||
|
||
### 2.5.4 修复
|
||
|
||
不在本轮 KISS 内。设计方向是把 admission 拆成两个端点:
|
||
- `POST /probe` → lock-free 读 snapshot(轻),90% 流量走这条
|
||
- `POST /commit_evict` → 进 scheduler 队列,做实际 LRU(重),仅 probe 不够时调
|
||
|
||
这部分需要 SGLang 内部 atomic publish snapshot 到共享内存——**结构性改动**。
|
||
|
||
### 2.5.5 注意
|
||
|
||
v6 P0 的 ×3 baseline rerun(不开 polling)errors 也是 372/912/396——**polling 不是 415 唯一原因**。本身 v5 admission 设计就敏感,polling 是放大器。
|
||
|
||
---
|
||
|
||
## 2.6 Replay 时间被 time-scale=10 压缩 → 测量学失真
|
||
|
||
### 2.6.1 现象(实锤)
|
||
|
||
v5 rerun1 metrics 解出的真实 inter-turn gap 分布:
|
||
|
||
```
|
||
原始 trace inter-turn gap (n=4397):
|
||
p10=1.6s p50=2.5s p90=7.8s p99=25.1s max=261s
|
||
|
||
time-scale=10 实际 replay gap (= 原始 / 10):
|
||
p10=0.16s p50=0.25s p90=0.78s p99=2.5s max=26s
|
||
```
|
||
|
||
### 2.6.2 这意味着什么
|
||
|
||
真实 agentic 用户/agent 在每个 turn 之间停 **2-8 秒**——思考、打字、tool call 异步返回、agent reasoning。
|
||
|
||
`microbench.py:20-21` 的默认 `inter_turn_gap_s=1.0` + `session_stagger_s=0.1` 也大致符合这个量级(1 秒左右)。
|
||
|
||
但 SWE replay 设的 time-scale=10 把这个间隔**人为压到 0.25 秒**——D 还没消化完 turn N,turn N+1 就来了。
|
||
|
||
### 2.6.3 为什么这么设计
|
||
|
||
纯粹**节省测试时间**:
|
||
- 原始 trace 跨度 ~6000s(≈100 分钟)
|
||
- time-scale=10 → ~600s(≈10 分钟)
|
||
- sweep 5 版本 × 3 重复 = 25h vs 2.5h
|
||
|
||
### 2.6.4 它扭曲了什么
|
||
|
||
1. **抹掉 D 的自然 idle 时间**:真实部署里每个 session 在 turn 间有几秒空窗,正好让 D 端 LRU 把它 evict 出去给其他 session 让位(§2.2 idle 判定)。time-scale=10 下几乎所有 session 一直忙——LRU 永远找不到 idle session。
|
||
2. **人为提升并发压力**:concurrency=32 在 time-scale=10 下意味着 D 端持续承受 320 effective concurrent agents 的压力——远超真实部署。
|
||
3. **掩盖 backpressure 等慢节奏机制的价值**:如果 inter-turn gap 是 2.5s,backpressure 让 replay 等 0.5s 几乎不影响吞吐;time-scale=10 下 0.5s 的 sleep 等于直接跳过下一个 turn。
|
||
|
||
### 2.6.5 严重性:所有 KVC vs DP 结论都带这个失真
|
||
|
||
**v3-v6 全部数据基于 time-scale=10**。所以"KVC 在 SWE 上输给 DP"的程度可能被 benchmark 放大。**真实部署里 inter-turn gap 是 2.5s 的话,KVC 可能根本不会撞到当前看到的容量瓶颈**。
|
||
|
||
这是项目当前**最严重但还没修的测量学问题**。修复成本极小(只是去掉 `--time-scale 10`),但意义重大——**P0 应该立刻跑一组 time-scale=1 baseline**(KVC + DP 各 N=3)。
|
||
|
||
---
|
||
|
||
## 2.7 direct-to-D append 阈值 = 2048 是个 magic number
|
||
|
||
### 2.7.1 现象(实锤)
|
||
|
||
`replay.py:51` 默认值:
|
||
|
||
```python
|
||
kvcache_direct_max_uncached_tokens: int = 2048
|
||
```
|
||
|
||
判定(`replay.py:2177`):当新 turn 的 uncached append > 2048 token 时,**禁止 direct-to-D**,请求改走 P→D reseed 路径。
|
||
|
||
实测 v5 rerun1 的 uncached append 分布(`input_length - cached_tokens`):
|
||
|
||
```
|
||
所有 4449 请求:
|
||
p10=50 p25=181 p50=610 p75=2907 p90=36495 p99=91600 max=103971
|
||
|
||
> 2048: 1222/4449 = 27.5%
|
||
```
|
||
|
||
**双峰分布**:median 只有 610,但 p90 已经 36K。
|
||
|
||
### 2.7.2 根因(代码)
|
||
|
||
阈值是个 magic number——**没有任何代码注释解释为什么是 2048**,git log 里也没人调过它。
|
||
|
||
合理推测它存在的理由(按可信度):
|
||
|
||
| 理由 | 是否成立 |
|
||
|---|---|
|
||
| D 是 decode-tuned,max-prefill-tokens 通常 4-8K,append > 2K 会触发 D 内部多 chunk prefill 拖慢 decode | 强 |
|
||
| 大 append 在 D 上 prefill 会阻塞当前正在 decoding 的其他 session 的 TPOT | 强 |
|
||
| P 有更优化的 prefill kernel 和 batch | 弱(D 的 prefill kernel 同源) |
|
||
| 工程上的"安全默认值",没认真测过 | 强(git log 印证) |
|
||
|
||
### 2.7.3 但更严重的 bug:execution_mode 标签命名错位
|
||
|
||
`execution_mode` 名字里带 "large-append" 的请求一共 **2060 个**,其中:
|
||
|
||
- **1222 个(59.3%)实际 uncached append ≤ 2048**
|
||
|
||
也就是说,**"large-append" 这个标签名对超过一半的实例是错的**。看 `replay.py:2168-2178` 的判断:
|
||
|
||
```python
|
||
if (
|
||
_should_bypass_prefill(...) # 要求 overlap > 0
|
||
and direct_append_length is not None
|
||
and direct_session_reused # 要求 session 在本 D 上 opened 过
|
||
and not direct_session_reset
|
||
and direct_append_length <= config.kvcache_direct_max_uncached_tokens
|
||
):
|
||
# direct-to-D
|
||
else:
|
||
# 进入 "large-append" 分支
|
||
```
|
||
|
||
**这个 else 分支的 5 个进入条件里,"append > 2048" 只是其中一个。** session 不在本 D 上、被 evict 过、overlap=0 都会进这个分支,但 `execution_mode` 仍然写 `pd-router-fallback-large-append-*`——导致看 metrics 的人误以为问题是 append 太大。
|
||
|
||
### 2.7.4 实际:阈值不是主要瓶颈,session 不在 D 上才是
|
||
|
||
把 turn≥2 的请求按"append 是否 > 2048"和"实际 execution mode"交叉:
|
||
|
||
```
|
||
Turn≥2 小 append (≤2048), n=3129:
|
||
1854 (59%) kvcache-direct-to-d-session ← 走通了
|
||
1141 (37%) pd-router-fallback-large-append-session-cap ← 标签骗人
|
||
...
|
||
|
||
Turn≥2 大 append (>2048), n=1216:
|
||
813 (67%) pd-router-fallback-large-append-session-cap
|
||
365 (30%) kvcache-centric (失败)
|
||
22 pd-router-large-append-reseed ← 真正受阈值影响的
|
||
...
|
||
```
|
||
|
||
**真正因 append > 2048 而失败的请求**:约 50 个(large-append-reseed + 部分 large-append fallback),仅占总数 1-2%。
|
||
|
||
**绝大多数 fallback 实际是 §2.1 的 session 不在 D 上**——名字里带 "large-append" 是误导。
|
||
|
||
### 2.7.5 修复
|
||
|
||
两件事:
|
||
1. 把 `execution_mode` 标签按真实原因细分——把 "large-append" 拆成 "session-not-resident" / "real-large-append" / "session-reset" 等
|
||
2. 阈值本身可以做 sweep(2048 / 4096 / 8192 / 16384)找最优——但收益空间有限(最多改善那 1-2% 的请求)
|
||
|
||
---
|
||
|
||
## 2.8 跨 run variance 巨大:N=1 不可信
|
||
|
||
### 2.8.1 现象(实锤)
|
||
|
||
v5 baseline 完全相同配置跑 3 次(`qwen3-30b-tp1-v5-optD-baseline-rerun/`):
|
||
|
||
| Run | Errors | Lat mean | Lat P50 | TTFT P50 |
|
||
|---|---:|---:|---:|---:|
|
||
| rerun1 | 372 | 3.50s | 1.11s | 0.147s |
|
||
| rerun2 | **912** | 3.00s | 0.94s | 0.071s |
|
||
| rerun3 | 396 | 3.42s | 1.22s | 0.183s |
|
||
|
||
errors 漂移 **2.5×**(372→912),P50 latency 漂移 ~30%,TTFT P50 漂移 **2.6×**。
|
||
|
||
### 2.8.2 根因(推测)
|
||
|
||
源头不止一个,至少包含:
|
||
|
||
1. **§2.1 + §2.2 的复合**:D 容量过载是临界点附近的非线性系统——initial session-to-D assignment 的随机性决定了哪个 D 先饱和。
|
||
2. **mooncake TCP loopback 的随机性**:单机 loopback 的 32s timeout 触发概率受当前 GPU 内存碎片、PCIe 状态影响。
|
||
3. **scheduler 主循环里 admission RPC 与 decode 抢资源的随机性**(§2.5)。
|
||
|
||
### 2.8.3 影响
|
||
|
||
**所有 single-run 比较 < 30% 差异都不可信**。这意味着:
|
||
- v3 vs v4 的 P50 差异(1.75s vs 1.08s)勉强有意义(差异 38%)
|
||
- v4 vs v5 的 P50 差异(0.84s vs 1.31s)勉强有意义(差异 56%)
|
||
- v5+profile 的 1P7D vs baseline(mean 4.21s vs 5.18s)→ 差异 18%,**不可信**
|
||
- 所有 `direct-to-D 占比 ±5%` 的差异都是噪声
|
||
|
||
### 2.8.4 这条规则要求所有后续实验
|
||
|
||
**要任何 KVC 配置间或 KVC vs DP 的对比,最少跑 N=3,最好 N=5。** 不跑 N≥3 的实验在做"碰运气科研"。
|
||
|
||
8h 一次 sweep 装不下 N=3 + 多版本对比,所以必须**牺牲版本数量保 N≥3**。
|
||
|
||
---
|
||
|
||
## 2.9 microbench 的 KVC 优势不能外推到真实 agentic
|
||
|
||
`microbench.py:13-22` 默认参数:
|
||
|
||
| 维度 | 默认值 |
|
||
|---|---|
|
||
| `session_count` | 8 |
|
||
| `turns_per_session` | 3 |
|
||
| `initial_input_length` | 10000 |
|
||
| `append_input_length` | **1000** ← 低于 §2.7 的 2048 阈值 |
|
||
| `output_length` | 1000 |
|
||
| `inter_turn_gap_s` | **1.0** ← 接近真实 agentic |
|
||
| `session_stagger_s` | 0.1 |
|
||
|
||
**与 SWE workload 的关键维度对比**:
|
||
|
||
| 维度 | microbench | SWE 50sess |
|
||
|---|---|---|
|
||
| Session 数 | 4-8 | 52 |
|
||
| Per-session peak input | ~31K | median 49K, max 104K |
|
||
| 总 working-set / 7D 容量(92K each) | 0.19×(5× 冗余) | **3.95×(4× 过载)** |
|
||
| Append size 是否过 2048 | 几乎 100% 过不到 | 28% 超过 |
|
||
| Session 数是否过 cap | 4 ≤ 28(v3 cap×7D) | 52 远超 |
|
||
|
||
**Microbench 把 KVC 的所有失效条件都规避了**:容量充裕、append 卡阈值之下、session 数远低于 cap、inter-turn gap 接近真实——这一组参数让 KVC 五项判断(路由 / admission / 没被 evict / append ≤ 阈值 / 无 backpressure)全部通过 → 100% 走 direct-to-D 快路径。
|
||
|
||
**而 SWE workload 在每一项上都把 KVC 推过临界点。**
|
||
|
||
所以"KVC 在 microbench 赢 PD disagg"是个**弱命题**——它只证明了机制能跑,没有证明在真实 agentic 下能赢。
|
||
|
||
---
|
||
|
||
# 第三部份:一句话总结与下一步
|
||
|
||
## 现状一句话
|
||
|
||
> 在所有可比的真实 agentic workload(SWE 35B / 30B)上,**naive DP cache-aware 全胜 KVC 任何配置**,且差距 > 30%(远超 single-run variance)。Microbench 上 KVC 赢 PD disagg 的设计前提(容量富余、append 小、session 少)在真实 workload 下不成立。
|
||
|
||
## 排序后的结构性问题(按修复 ROI)
|
||
|
||
| 排名 | 问题 | 影响 | 修复成本 |
|
||
|---|---|---|---|
|
||
| **P0** | §2.6 time-scale=10 失真 → 所有 KVC vs DP 结论可能被 benchmark 放大 | 颠覆性 | 极低(改 flag) |
|
||
| **P0** | §2.1 session 永久 pin + 容量盲选 | 25% session 永远饿死 | 中(改 policy) |
|
||
| **P0** | §2.2 D-side LRU 跟不上 | ~8% errors 来自此 | 中(改 SGLang) |
|
||
| P1 | §2.3 没 backpressure | 把 timeout 雪崩变可控 | **已实现**(待 GPU smoke) |
|
||
| P1 | §2.4 P-side 不感知 D 健康 | 单 P 出错率差 180× | 中 |
|
||
| P1 | §2.7 / 2.8 metrics 标签命名错位 | 数据解读经常出错 | 低(改字符串) |
|
||
| P2 | §2.5 admission RPC 进 scheduler 主循环 | 自我干扰 | 高(结构改动) |
|
||
| P2 | §2.8 N=1 不可信 | 实验方法学 | 0(团队约定) |
|
||
|
||
## 立刻能做的三件事
|
||
|
||
1. **跑 time-scale=1 baseline**(KVC v5 + 8DP CA 各 N=3,~6h GPU)—— 不修代码、单变量、决定后续路线。
|
||
2. **跑 backpressure smoke**(已实现,4 run × ~30-60 min,~3-4h GPU)—— 验证 §2.3 修复的端到端效果。
|
||
3. **修 metrics 标签命名**(`pd-router-fallback-large-append-*` → 按真实原因分类)—— 让以后看数据的人不会再被误导。
|
||
|
||
## 不立刻做但要重新讨论的
|
||
|
||
- **§2.1 capacity-aware policy**:之前考虑过的"评分加 capacity 项"会引入"换 D"的副作用(孤儿 KV、新 D 上仍可能饿死),需要跟 §2.2 的 D 端 hot retract 一起设计。
|
||
- **§2.5 admission API 拆 probe / commit**:是结构性正确方向,但要动 SGLang 内部 + atomic publish 机制,不是 KISS。
|
||
- **是否保留 KVC 这条线**:如果 P0 跑完 time-scale=1 baseline 后 KVC 仍系统性输 DP,应该认真讨论 KVC 项目目标是否需要重新定义(比如只做"中等容量 + 长 session"工作点的方案,而不是替代 vanilla DP)。
|
||
|
||
---
|
||
|
||
## 附录 A:本报告所有数据的来源
|
||
|
||
| 章节 | 数据源 |
|
||
|---|---|
|
||
| 1.1 SWE 35B | `outputs/swebench-exps/{pd-disagg,pd-colo,kvcache-centric}-*` |
|
||
| 1.2 TP1 series | `outputs/qwen3-30b-tp1-{exps,v3-kvaware,v4-cap16,v5-optD,v5-optD-profile,v5-optD-baseline-rerun}/` |
|
||
| 2.1 session pinning | `outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/exp2_2p6d_run{1,2,3}_metrics.jsonl` |
|
||
| 2.2 D LRU 计数 | `outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/.../logs/decode-{0..5}.log` |
|
||
| 2.4 P imbalance | `outputs/qwen3-30b-tp1-v5-optD-baseline-rerun/.../logs/prefill-{0,1}.log` |
|
||
| 2.5 polling 影响 | v5 baseline summary vs v5+profile summary |
|
||
| 2.6 inter-turn gap | rerun1 metrics 的 `trace_timestamp_s` 字段 |
|
||
| 2.7 append 分布 | rerun1 metrics 的 `input_length - cached_tokens` |
|
||
| 2.8 variance | rerun1/2/3 三组 summary |
|
||
|
||
## 附录 B:相关已有文档
|
||
|
||
- `docs/PROJECT_OVERVIEW.md` — 项目目标、microbench 结论
|
||
- `docs/archive/AGENTIC_FIT_ANALYSIS_ZH.md` — 结构性缺陷的早期分析(本报告 §2 的来源)
|
||
- `docs/archive/KVC_DEBUG_JOURNEY_V1_TO_V5.md` — v1→v5 详细演进日记
|
||
- `docs/archive/V5_PROFILE_INVESTIGATION_ZH.md` — v5+profile 调查(含 critic 修订)
|
||
- `docs/archive/SWEBENCH_EXPERIMENT_RESULTS.md` — SWE 35B 早期实验
|
||
- `docs/archive/REFACTOR_PLAN_ZH.md` — 当前重构计划
|
||
- `docs/archive/STRUCTURAL_VALIDATION_REPORT_ZH.md` — 结构性 claim 验证(本报告的精简版)
|