Files
agentic-kvc/FIXES.md
Gahow Wang fc445df0ad Add FIXES.md with prioritized repo cleanup checklist
Captures the full review of bugs, fake/half-implemented features, dead
branches, and quality gaps found in cache_aware_proxy.py, replayer, and
the shell scripts. Each item has file:line, problem, fix, and verification
steps so any contributor can pick it up directly.
2026-05-23 20:35:56 +08:00

769 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Repo 修复指南 (FIXES.md)
> 本文档对应 2026-05-23 的 repo review。每条 issue 自包含:定位、动机、复现/验证、改法。按严重度从高到低排列,建议**自上而下**逐项修复,每条修完独立提交一个 commit。
---
## 目录
- [B1. 删除死状态 `_inst_cumulative_tokens`](#b1)
- [B2. 修复 replayer CLI 与 shell 脚本不一致(阻断实验)](#b2)
- [B3. 处理 PD-sep `--fire-and-forget` 损坏路径](#b3)
- [B4. 实现或移除 H4 cache-ratio gate](#b4)
- [B5. 修复 `_percentile` off-by-one](#b5)
- [B6. 统一 `bench.sh` 的模型路径](#b6)
- [M1. `cached_blocks` 替换策略改为真正的 LRU](#m1)
- [M2. P 候选选择避开 `active_p_offloads`](#m2)
- [M3. 把 `MAX_OFFLOAD_INFLIGHT` 暴露为 CLI 参数](#m3)
- [M4. `session_affinity` 在 combined / pd-sep 之间命名空间隔离](#m4)
- [M5. fallback 路径 client 断流时的资源泄漏](#m5)
- [M6. `_send_prefill_async` 与同步路径的核算不一致](#m6)
- [D1. 移除 `_send_prefill_async` 与 `--fire-and-forget`](#d1)
- [D2. 删除/归档 `run_benchmark.sh` 与 `run_experiments.sh`](#d2)
- [D3. 归档历史一次性 `analyze_*.py` / `compare_*.py`](#d3)
- [D4. 修正 `compute_roofline.py` 的硬编码 trace 路径](#d4)
- [D5. `HEAVY_THRESHOLD` / `OVERLOAD_FACTOR` 改读 args](#d5)
- [S1. 给 `replayer/metrics.py` 与 cost-model 加单元测试](#s1)
- [S2. 给 vLLM patch 加 import-time 校验](#s2)
- [S3. REPORT.md 加 errata block](#s3)
- [验收清单](#验收清单)
---
<a id="b1"></a>
## B1. 删除死状态 `_inst_cumulative_tokens`
**严重度**: High误导性死代码
**定位**: `scripts/cache_aware_proxy.py:76, 102104, 125`
**问题**:
- `_inst_cumulative_tokens` 是 module-level list每次 turn 1 路由后 `+= input_length`
- 全 repo grep 这个名字只有写入点,没有任何读取。
**验证**:
```bash
grep -rn "_inst_cumulative_tokens" /home/gahow/phd/agentic-kv
# 只应该看到 cache_aware_proxy.py 自己的 5 行;如有其它读取者,先确认意图再删
```
**改法**:
1. 删除 `cache_aware_proxy.py:76``_inst_cumulative_tokens: list[int] = []`
2. 删除 `pick_instance` 内的 `global _inst_cumulative_tokens``:103-104` 的初始化。
3. 删除 `:125` 的累加。
4. 不需要替代实现——load 计算用 `inst.ongoing_tokens`session 粘性用 `affinity` dict。
---
<a id="b2"></a>
## B2. 修复 replayer CLI 与 shell 脚本不一致(最高优先级)
**严重度**: Critical阻断 REPORT 自己规定的 next-step 实验)。
**定位**:
- `replayer/__main__.py:14-26`: argparse 当前**只**接受 `--trace --output --endpoint --model --concurrency-limit --request-timeout --request-limit -v`
- `scripts/run_benchmark.sh:32, 70-71`: 仍传 `--time-scale``--max-inflight-sessions`
- `scripts/run_experiments.sh:58-59`: 同样问题。
- `REPORT.md:521, 541`: 把 `--max-inflight-sessions 64+` 列为 next step。
**问题**:
- 跑这两个 shell 脚本会立刻 `SystemExit(2)`unrecognized arguments。
- 报告里的"下一步实验"无法执行。
**决策**: 两条路线,**二选一**,本 repo 推荐路线 A。
### 路线 A推荐恢复 `--max-inflight-sessions`,保持 `--time-scale` 移除
理由REPORT §3.6 已经论证 trace-driven replay无时间压缩是正确的但高并发实验需要一个并发上限旋钮。把 `--max-inflight-sessions` 重新加回来,语义为"全局活跃 session 数上限的 semaphore"。
**改法**:
1. 修改 `replayer/__main__.py`
```python
p.add_argument("--max-inflight-sessions", type=int, default=None,
help="Cap concurrent active sessions (None = unlimited; "
"use to simulate higher-than-trace concurrency)")
```
并把它塞进 `ReplayConfig`
```python
config = ReplayConfig(
...
max_inflight_sessions=args.max_inflight_sessions,
)
```
2. 修改 `replayer/replay.py` 的 `ReplayConfig` 与 dispatch 逻辑:
- 在 `ReplayConfig` 里加 `max_inflight_sessions: int | None = None`。
- 在 `replay_trace` 里:若 `max_inflight_sessions` 不为 None创建 `asyncio.Semaphore(max_inflight_sessions)`,每个 session 任务 `async with sem:` 包住整段 session 重放(不是单个 request
- 若为 None保持现有行为无上限仅 `concurrency_limit` 是 HTTP 层 safety semaphore
3. 删除 shell 脚本里的 `--time-scale`
- `scripts/run_benchmark.sh:32, 70`: 删除 `--time-scale` 选项与传参。
- `scripts/run_experiments.sh:58`: 同上。
4. 验证:
```bash
python -m replayer --trace traces/w600_r0.0015_st30.jsonl \
--output /tmp/x.jsonl --endpoint http://localhost:9090 \
--max-inflight-sessions 64
# 不应再报 unrecognized arguments
```
5. 同步更新 `REPORT.md:430` 的 CLI 表格(删 `--time-scale`,保留 `--max-inflight-sessions`)。
### 路线 B彻底删掉这两个参数 + 删 shell 脚本
如果不打算再跑高并发实验,则:
1. 删 `scripts/run_benchmark.sh` 和 `scripts/run_experiments.sh`(与 D2 合并)。
2. 修订 `REPORT.md:521, 541, 530` 中提到 `--max-inflight-sessions` 的全部段落,明确说"该参数已删除,对应实验留给后续工作"。
**任选一条,但不能保留现状。**
---
<a id="b3"></a>
## B3. PD-sep `--fire-and-forget` 路径损坏
**严重度**: Highreachable-but-broken
**定位**: `scripts/cache_aware_proxy.py:552-554, 570-573, 507-521`。
**问题**:
- `_handle_pd_sep` 在 `--fire-and-forget` 时 `asyncio.create_task(_send_prefill_async(...))` **不等 P 完成**。
- 紧接 `:570-583` 立刻发起 D 端 decodedecode 携带 `remote_bootstrap_addr` + `remote_engine_id` + `transfer_id`。
- 但 P 端此时可能尚未注册 `transfer_id`Mooncake 拉取失败 → D 端 502。
- 此外 `_send_prefill_async:507-521` 在异常分支只 `breakdown["prefill_error"] = True`,错误不会传递给 client。
**改法**:
如果按 [D1](#d1) 直接删,那这一条自动消失。
否则按以下方式修:
1. 在 `_send_prefill_async` 里加一个 `asyncio.Event`
```python
async def _send_prefill_async(p_inst, api, prefill_data, p_headers,
token_ids, input_length, breakdown,
ready: asyncio.Event):
try:
resp = await p_inst.client.post(api, json=prefill_data, headers=p_headers)
resp.raise_for_status()
await resp.aclose()
breakdown["t_prefill_done"] = _time.monotonic()
p_inst.record_prefix(token_ids)
except Exception as e:
breakdown["t_prefill_done"] = _time.monotonic()
breakdown["prefill_error"] = str(e)
finally:
p_inst.ongoing_tokens -= input_length
ready.set()
```
2. 在 `_handle_pd_sep` 里,发 decode 之前 `await ready.wait()`(带超时),保证 transfer_id 已注册:
```python
ready = asyncio.Event()
asyncio.create_task(_send_prefill_async(..., ready=ready))
try:
await asyncio.wait_for(ready.wait(), timeout=PREFILL_TIMEOUT_S)
except asyncio.TimeoutError:
raise HTTPException(502, "Prefill not registered in time")
if "prefill_error" in breakdown:
raise HTTPException(502, breakdown["prefill_error"])
```
3. 这样语义其实就跟同步等待几乎一样了——更佳决策是按 [D1](#d1) 直接删。
---
<a id="b4"></a>
## B4. 实现或移除 H4 cache-ratio gate
**严重度**: Highdesign doc 与代码不一致 / fake feature
**定位**: `scripts/cache_aware_proxy.py:288, 308``analysis/elastic_hypotheses.md``scripts/run_h4_cache_gate.sh`。
**问题**:
- `cache_ratio = cache_hit / max(input_length, 1)` 计算后**仅写入 breakdown**,没有任何分支根据它决策。
- `analysis/elastic_hypotheses.md` 与 `run_h4_cache_gate.sh` 都假定"当 cache_ratio < 阈值时不 offload";目前完全无效。
**改法(推荐:实现)**:
1. 在 `cache_aware_proxy.py` 顶部加常量与 CLI
```python
CACHE_GATE_RATIO = 0.3 # default; overridden by --cache-gate-ratio
```
```python
p.add_argument("--cache-gate-ratio", type=float, default=0.3,
help="Min cache_hit/input ratio to allow offload "
"(0.0 disables gate, 1.0 disables offload)")
```
并在 `__main__` 里 `CACHE_GATE_RATIO = global_args.cache_gate_ratio`(参考 [D5](#d5),最好不要用 module-level 赋值,直接读 args
2. 在 `:312` 之前加 gate
```python
if cache_ratio < CACHE_GATE_RATIO:
offload_reason = "cache_gate_%.2f" % cache_ratio
elif current_offloads >= MAX_OFFLOAD_INFLIGHT:
offload_reason = "cap_reached_%d" % current_offloads
elif offload_cost < colocated_cost:
use_offload = True
offload_reason = "cost_model_%.1fvs%.1f" % (offload_cost, colocated_cost)
else:
offload_reason = "colocated_cheaper_%.1fvs%.1f" % (colocated_cost, offload_cost)
```
3. 把 `--cache-gate-ratio` 加到 `scripts/bench.sh` 与 `scripts/launch_phase1_ps.sh` 的 proxy 启动行(默认值 0.3elastic 模式生效)。
**或者(不实现)**: 把 `:288` 的 `cache_ratio` 计算与写入删除,并在 `analysis/elastic_hypotheses.md` 顶部加一句"H4 gate 设计未落地,结论待验证"。
---
<a id="b5"></a>
## B5. `_percentile` off-by-one
**严重度**: Medium影响所有 summary 数据)。
**定位**: `replayer/metrics.py:103-107`。
**问题**:
```python
idx = round((len(sorted_vals) - 1) * pct)
```
对 len=100, pct=0.5 → `round(49.5) = 50`Python banker's rounding 偶向偶)。
对 len=2, pct=0.5 → `round(0.5) = 0`,但 `round(1.5) = 2` 等场景不稳定;银行家舍入让结果在偶数 idx 上偏倚。
所有 p50 在偶数 sample 上偏向上中位。
**改法**:
替换为线性插值(与 numpy.percentile 默认一致):
```python
def _percentile(sorted_vals: list[float], pct: float) -> float:
n = len(sorted_vals)
if n == 1:
return sorted_vals[0]
rank = pct * (n - 1)
lo = int(rank)
hi = min(lo + 1, n - 1)
frac = rank - lo
return sorted_vals[lo] * (1 - frac) + sorted_vals[hi] * frac
```
**验证**:
```python
# 单测:见 S1
assert _percentile([1, 2, 3, 4], 0.5) == 2.5
assert _percentile([1, 2], 0.5) == 1.5
assert _percentile([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 0.9) == 9.1
```
---
<a id="b6"></a>
## B6. 统一 `bench.sh` 的模型路径
**严重度**: Medium新机器跑直接挂
**定位**: `scripts/bench.sh:23`。
**问题**:
- `bench.sh:23`: `MODEL="${MODEL_PATH:-/home/admin/cpfs/wjh/models/Qwen/Qwen3-Coder-30B-A3B-Instruct}"`
- 其它脚本 (`launch_vllm.sh`、`launch_elastic_p2p.sh`) 与 `TODO.md``$HOME/models/Qwen/Qwen3-Coder-30B-A3B-Instruct`。
**改法**:
把 `bench.sh:23` 的默认值改为:
```bash
MODEL="${MODEL_PATH:-$HOME/models/Qwen/Qwen3-Coder-30B-A3B-Instruct}"
```
并 `grep -rn "/home/admin/cpfs"` 检查整 repo 没有其它残留:
```bash
grep -rn "/home/admin/cpfs" /home/gahow/phd/agentic-kv
```
若有则一并替换为 `$HOME/models/...`。
---
<a id="m1"></a>
## M1. `cached_blocks` 替换策略改为真正的 LRU
**严重度**: Mediumrouter 估算 cache_hit 与真实 vLLM APC 长期偏差)。
**定位**: `scripts/cache_aware_proxy.py:71-72``record_prefix`)。
**问题**:
```python
if len(self.cached_blocks) > 200000:
self.cached_blocks = set(list(self.cached_blocks)[-100000:])
```
- `set` 迭代顺序在 CPython 不保证插入序,"取后 100k"等价于随机丢一半。
- 这与 vLLM 内部 LRU 完全不一致,是 §3.6 提到的 24pp APC gap 的部分来源。
**改法**:
把 `cached_blocks: set[int]` 改成 `OrderedDict[int, None]` 充当 LRU
```python
from collections import OrderedDict
class InstanceState:
def __init__(self, ...):
...
self.cached_blocks: OrderedDict[int, None] = OrderedDict()
self.cache_capacity = 200000 # blocks; tune with --cache-capacity-blocks
def estimate_cache_hit(self, token_ids):
if not token_ids or len(token_ids) < BLOCK_SIZE:
return 0
hit = 0
for i in range(0, len(token_ids) - BLOCK_SIZE + 1, BLOCK_SIZE):
bh = hash(tuple(token_ids[i:i + BLOCK_SIZE]))
if bh in self.cached_blocks:
self.cached_blocks.move_to_end(bh) # LRU touch
hit += BLOCK_SIZE
else:
break
return hit
def record_prefix(self, token_ids):
if not token_ids:
return
for i in range(0, len(token_ids) - BLOCK_SIZE + 1, BLOCK_SIZE):
bh = hash(tuple(token_ids[i:i + BLOCK_SIZE]))
if bh in self.cached_blocks:
self.cached_blocks.move_to_end(bh)
else:
self.cached_blocks[bh] = None
if len(self.cached_blocks) > self.cache_capacity:
self.cached_blocks.popitem(last=False) # evict LRU
```
**进阶**: 容量应根据真实 KV cache 大小标定vLLM 启动后 `total_blocks * block_size`),不要写死 200000。可以
- 加 `--cache-capacity-blocks` CLI默认 200000
- 或者从 vLLM `/metrics` 抓 `vllm:gpu_cache_usage_perc` 反推容量。
---
<a id="m2"></a>
## M2. P 候选选择避开 `active_p_offloads`
**严重度**: Medium。
**定位**: `scripts/cache_aware_proxy.py:291-295`。
**问题**:
- 选 P 候选只按 `c.ongoing_tokens`,没有考虑某 instance 已经在为别人做 offload。
- 配合 `MAX_OFFLOAD_INFLIGHT=4` 是 global cap单 instance 可能扛多个 offload。
**改法**:
把 `:291-292` 的 key 加上 P-offload 罚项:
```python
def _p_pick_score(inst):
return (inst.ongoing_tokens
+ inst.active_p_offloads * HEAVY_THRESHOLD)
p_candidate = min((c for c in combined_instances if c is not best_inst),
key=_p_pick_score)
```
并把 `MAX_OFFLOAD_INFLIGHT` 拆成 per-instance
```python
if any(c.active_p_offloads >= MAX_OFFLOAD_PER_INSTANCE
for c in combined_instances):
# 全员上限,不 offload
...
elif p_candidate.active_p_offloads >= MAX_OFFLOAD_PER_INSTANCE:
offload_reason = "p_inst_cap_reached"
```
---
<a id="m3"></a>
## M3. 把 `MAX_OFFLOAD_INFLIGHT` 暴露为 CLI
**严重度**: LowMedium。
**定位**: `cache_aware_proxy.py:32, 312`。
**问题**: 模块常量 `MAX_OFFLOAD_INFLIGHT = 4`,未暴露 CLI高并发实验时会成为隐性 bottleneck。
**改法**:
1. `parse_args` 里加:
```python
p.add_argument("--max-offload-inflight", type=int, default=4,
help="Global cap on concurrent P-role offloads")
```
2. 在 `_handle_combined` 里读 `global_args.max_offload_inflight` 而不是常量(与 [D5](#d5) 一致)。
3. 同步 `bench.sh` / `launch_phase1_ps.sh`elastic 模式可设大一点。
---
<a id="m4"></a>
## M4. `session_affinity` 在 combined / pd-sep 之间命名空间隔离
**严重度**: Low当前不会同时跑两种模式但属隐患
**定位**: `cache_aware_proxy.py:158, 532`。
**问题**: 全局 `session_affinity: dict[str, int]`combined 模式 idx 指向 `combined_instances`pd-sep 模式同 dict 又被 `pick_instance(prefill_instances, ...)` 写入并指向 `prefill_instances`。同一个 session_id 在两种模式下索引含义不同。
**改法**:
把 `session_affinity` 改成两个:
```python
session_affinity_combined: dict[str, int] = {}
session_affinity_prefill: dict[str, int] = {}
```
`_handle_combined` 用前者,`_handle_pd_sep` 用后者。`pick_instance` 签名不变,只在调用方传不同 dict。
---
<a id="m5"></a>
## M5. fallback 路径 client 断流时的资源泄漏
**严重度**: LowMedium高并发下可能累积
**定位**: `cache_aware_proxy.py:438-467``_handle_heavy_offload` fallback`:364-387``_handle_combined` 主路径);`:585-598``_handle_pd_sep`)。
**问题**:
- StreamingResponse 返回后,若 client 在 generator 未被消费时断开generator 不会进入 `try``finally` 不会触发。
- 结果:`d_inst.ongoing_tokens` / `num_requests` / `pending_prefill_tokens` 永不释放shadow state 与真实 load 越走越偏。
- 长时间运行后 router 认为某些 instance 一直满载,路由失衡。
**改法**:
把"扣减"从 `finally` 换成 `BackgroundTasks`/`FastAPI` 的 lifecycle 不可靠,最稳妥是**在路由阶段就只做"加",扣减在异步监听 client disconnect 的协程里做**。简化版改法:
1. 包一层 `try/finally` 在调用 `StreamingResponse(generate(), ...)` 之前,并把状态扣减用 `request.is_disconnected()` 轮询或注册到 `BackgroundTask`。
2. 或者更简单:在 `inst.ongoing_tokens += input_length` 的同时把"应在结束时扣减的值"塞进一个 dictkey=request_id并在 `app` 层每 30s 扫一次 stale 请求(超过 `request_timeout * 2` 的)做兜底回收。
**最小可行修复**:周期性 reconcile在 `cache_aware_proxy.py` 里加一个后台 task
```python
async def _reconcile_loop():
while True:
await asyncio.sleep(60)
for inst in combined_instances + prefill_instances + decode_instances:
# 简单 sanity: ongoing_tokens 永远 >= 0
if inst.ongoing_tokens < 0:
inst.ongoing_tokens = 0
if inst.num_requests < 0:
inst.num_requests = 0
# 进阶:与 vLLM /metrics 对账,详见 TODO.md item 6
```
并在 `lifespan` 启动该 task。这只是兜底不解决根因根因解决要走 TODO.md 第 6 条的 vLLM → Redis exact-state 路线。
---
<a id="m6"></a>
## M6. `_send_prefill_async` 与同步路径的核算不一致
**严重度**: Low与 D1 一并解决)。
**定位**: `cache_aware_proxy.py:507-521` vs `:556-568`。
**问题**:
- 同步路径在 finally 扣 `p_inst.ongoing_tokens`
- async 路径同样扣 `ongoing_tokens`,但 `pending_prefill_tokens` 在 PD-sep 路径中**两边都没维护**——表面一致,但与 combined 路径的语义不一致。
**改法**: 看 [D1](#d1)。如果保留 fire-and-forget加上 `breakdown` 的 ready event[B3](#b3))后,同时确保两路径核算字段对称。
---
<a id="d1"></a>
## D1. 移除 `_send_prefill_async` 与 `--fire-and-forget`
**严重度**: Cleanup。
**定位**: `cache_aware_proxy.py:507-521`function、`:552-554`caller、`:634-635`CLI flag
**问题**:
- grep 全 repo 所有 launch / bench / experiment 脚本,`--fire-and-forget` 0 处使用。
- 配合 [B3](#b3),这条 reachable 但 broken 的路径是 dead-on-arrival。
**改法**:
1. 删除 `_send_prefill_async` 整个函数。
2. 删除 `_handle_pd_sep` 里 `if global_args.fire_and_forget: ... else:` 的分支,只保留同步 path。
3. 删除 CLI 里的 `p.add_argument("--fire-and-forget", ...)`。
4. `grep -rn "fire-and-forget\|fire_and_forget"` 确认无残留。
---
<a id="d2"></a>
## D2. 删除/归档 `run_benchmark.sh` 与 `run_experiments.sh`
**严重度**: Cleanup。
**定位**: `scripts/run_benchmark.sh`、`scripts/run_experiments.sh`。
**问题**: 与 [B2](#b2) 同源,两脚本仍传已删 CLI 参数;事实上不再可运行。
**改法**:
1. `mkdir -p scripts/legacy`
2. `git mv scripts/run_benchmark.sh scripts/run_experiments.sh scripts/legacy/`
3. 在 `scripts/legacy/README.md` 写一行:"这些脚本对应早期 `--time-scale` / `--max-inflight-sessions` API已归档新实验请用 `scripts/bench.sh`。"
4. 若选择 [B2](#b2) 路线 A 重新加回 `--max-inflight-sessions`,可顺便把 `run_benchmark.sh` 从 legacy 拉回并修参数。
---
<a id="d3"></a>
## D3. 归档历史一次性 `analyze_*.py` / `compare_*.py`
**严重度**: Cleanup影响新人理解
**定位**: `scripts/` 下约 20 个 `analyze_*.py` / `compare_*.py`。
**问题**:
- 大量脚本指向 `outputs/<exp>/...` 的旧实验路径(被 `.gitignore` 忽略,实际不存在)。
- `compute_roofline.py:165` 硬编码 `traces/sampled_1000req_seed42.jsonl`(已不存在,详见 [D4](#d4))。
- 多个 `compare_*.py` 引用已删除实验目录。
**改法**:
1. 列一张表(在本文件下方"附录 A"或新建 `scripts/INVENTORY.md`),把每个 analyze/compare 脚本归类:
- **保留**: 有结构化用法、对当前 trace/output 仍可跑(如 `analyze_trace.py`、`analyze_breakdown.py`、`analyze_cache_hit.py`、`analyze_eviction.py`、`compare_results.py`)。
- **归档**: 一次性、特定实验 ID如 `compare_ab_final.py`、`compare_balanced.py`、`compare_elastic_v4.py`、`compare_p2p.py`、`final_*.py`、`compare_aggregation.py`、`analyze_3way.py`、`analyze_h4_results.py`、`analyze_h5_rdma.py`、`profile_*.py`、`plot_gpu_timeline.py` 等)。
2. `git mv` 归档类到 `scripts/legacy/`。
3. 保留类的脚本:
- 顶部加 docstring写明输入路径变量与示例命令。
- 凡是硬编码 `outputs/...` 路径的,全改成 `argparse` 参数。
**最小行动**: 至少把以下"明显死"的归档:
```
scripts/legacy/
├── compare_ab_final.py
├── compare_adaptive.py
├── compare_aggregation.py
├── compare_balanced.py
├── compare_elastic_v4.py
├── compare_p2p.py
├── final_all_comparison.py
├── final_comparison.py
├── final_gpu_comparison.py
├── analyze_3way.py
├── analyze_aggregation.py
├── analyze_h4_results.py
├── analyze_h5_rdma.py
├── analyze_p2p_cache.py
├── analyze_gpu_ab.py
├── analyze_ablations.py
├── plot_gpu_timeline.py
├── profile_fnf.py
├── profile_why_pdsep_loses.py
├── ab_gpu_test.sh
├── run_elastic_stability_test.sh
├── run_h4_cache_gate.sh
├── run_lmetric_ab.sh
├── run_ps_ablation.sh
├── run_ps_flexd.sh
├── run_ps_remaining.sh
└── run_v2_offload.sh
```
---
<a id="d4"></a>
## D4. 修正 `compute_roofline.py` 的硬编码 trace 路径
**严重度**: Low。
**定位**: `scripts/compute_roofline.py:165`。
**问题**: 写死 `trace_path = "traces/sampled_1000req_seed42.jsonl"`,文件已不存在。
**改法**:
```python
import argparse
def main():
p = argparse.ArgumentParser()
p.add_argument("--trace", type=str,
default="traces/w600_r0.0015_st30.jsonl",
help="Trace JSONL path")
args = p.parse_args()
trace_path = args.trace
...
```
---
<a id="d5"></a>
## D5. `HEAVY_THRESHOLD` / `OVERLOAD_FACTOR` 改读 args
**严重度**: Low。
**定位**: `cache_aware_proxy.py:30-34, 663-664, 88, 112`。
**问题**:
- 顶部 `HEAVY_THRESHOLD = 20000``__main__` 里 `HEAVY_THRESHOLD = global_args.heavy_threshold` 是给 module-level 名字赋值;
- 函数体里 `_p_offload_penalty(inst)` 与 `pick_instance` 直接读 `HEAVY_THRESHOLD` 名字globals运行时正常生效
- 但若以后把 module 当库 import例如加单测`__main__` 块不执行CLI 覆盖丢失。
**改法**:
把所有"运行时可调"常量挪到一个 `Settings` dataclass 里:
```python
from dataclasses import dataclass
@dataclass
class Settings:
heavy_threshold: int = 20000
overload_factor: float = 2.0
max_offload_inflight: int = 4
cache_gate_ratio: float = 0.3
prefill_throughput: float = 7000.0
rdma_overhead_s: float = 2.0
cache_capacity_blocks: int = 200000
SETTINGS = Settings()
```
`parse_args` 后直接 `SETTINGS = Settings(**vars(args).filter(...))` 或逐字段赋值。函数体里改用 `SETTINGS.heavy_threshold` 等。
---
<a id="s1"></a>
## S1. 给 `replayer/metrics.py` 与 cost-model 加单元测试
**严重度**: Quality。
**问题**: 整 repo 0 个测试。`_percentile`、`InstanceState.estimate_cache_hit`、`pick_instance`、cost-model 都该有最小覆盖。
**改法**:
1. 新建 `tests/` 目录,加 `tests/__init__.py`。
2. `tests/test_metrics.py`
```python
from replayer.metrics import _percentile
def test_percentile_even():
assert _percentile([1, 2, 3, 4], 0.5) == 2.5
def test_percentile_odd():
assert _percentile([1, 2, 3, 4, 5], 0.5) == 3
def test_percentile_p99():
assert _percentile(list(range(1, 101)), 0.99) == 99.01
```
3. `tests/test_proxy_pick.py`
```python
import sys, pathlib
sys.path.insert(0, str(pathlib.Path(__file__).parent.parent / "scripts"))
from cache_aware_proxy import InstanceState, pick_instance, BLOCK_SIZE
def _new_inst(url="http://x"):
inst = InstanceState.__new__(InstanceState)
inst.url = url
inst.ongoing_tokens = 0
inst.pending_prefill_tokens = 0
inst.num_requests = 0
inst.active_p_offloads = 0
inst.cached_blocks = type(inst).__dict__.get(
"cached_blocks", set)()
return inst
# ...session affinity & overload tests
```
4. `pyproject.toml` 加 `[tool.pytest]` 段,跑 `pytest -q`。
---
<a id="s2"></a>
## S2. 给 vLLM patch 加 import-time 校验
**严重度**: Quality。
**定位**: `patches/0001-fix-kv-transfer-abort-race.patch`。
**问题**: 单 assert→warn 替换。未来升级 vLLM 时极易漏打 patch当前没有运行时自检。
**改法**:
在 `scripts/cache_aware_proxy.py` 启动时(`lifespan` 开头)加:
```python
def _verify_vllm_patch():
"""启动时自检:被 patch 的 scheduler 是否仍包含期望的 warn 路径。"""
import inspect
try:
from vllm.v1.core.sched.scheduler import Scheduler
src = inspect.getsource(Scheduler)
if "assert req_id in self.requests" in src:
print("WARNING: vLLM scheduler still has the unpatched assert; "
"expect engine death on KV transfer abort race. "
"Apply patches/0001-fix-kv-transfer-abort-race.patch.")
except Exception as e:
print(f"vLLM patch self-check skipped: {e}")
```
并在 `lifespan` 最开始调用。
---
<a id="s3"></a>
## S3. REPORT.md 加 errata block
**严重度**: Quality避免读者引用过期结论
**定位**: `REPORT.md` 顶部。
**改法**:
在 §1 后插入:
```markdown
## 0. Errata / 已废弃章节
> 本报告为多次方法论修订后的累积版本,下列章节结论已被后续小节修订或推翻:
>
> - §3.1PD-sep vs PD-combined 初版对比):使用旧采样 + `--time-scale`,被 §3.6 推翻,**勿引用**。
> - §3.5elastic v3warm-vs-fresh 对比无效baseline 实例未冷启动),**勿引用**。
> - §X 中提到的 `--max-inflight-sessions 64+` 实验CLI 已删除,对应实验需先按 FIXES.md B2 路线 A 恢复参数后再做。
>
> 当前**唯一权威的**结果章节为 §3.6 与 §3.7。
```
---
## 验收清单
修复完成后,按此清单逐项验证。
- [ ] `grep -rn "_inst_cumulative_tokens" .` → 0 hitsB1
- [ ] `python -m replayer --help` 列表里**有**或**没有** `--max-inflight-sessions`(视 B2 路线选择,二者必须自洽)
- [ ] `bash scripts/bench.sh ...` 在干净 repo 上能跑通至少 baseline 模式
- [ ] `grep -rn "fire-and-forget\|fire_and_forget" scripts/` → 0 hitsD1 完成)
- [ ] `grep -rn "/home/admin/cpfs" .` → 0 hitsB6
- [ ] `cache_aware_proxy.py` 中 `cache_ratio` 出现且**被某个分支引用**B4 完成)
- [ ] `pytest -q` 跑通新加的最小测试S1
- [ ] `REPORT.md` 有 §0 Errata 段S3
- [ ] 单跑 elastic 模式启动时打印 vLLM patch self-check 结果S2
- [ ] `scripts/legacy/` 下能找到归档的脚本D2、D3
- [ ] `_percentile([1,2,3,4], 0.5) == 2.5`B5
## 修复顺序建议(按 PR 切分)
1. **PR 1不破坏行为纯清理**: B1、D1、D2、D3、D4、B6、S3
2. **PR 2修 bug**: B5、M1、M5轻量 reconcile
3. **PR 3恢复实验能力**: B2 路线 A恢复 `--max-inflight-sessions`),同步 S1 加单测
4. **PR 4落地设计**: B4cache-ratio gate、M2、M3、D5
5. **PR 5健壮性**: M4、S2、剩余 M5 进阶版
修完 PR 13 即可重新运行 REPORT 自己规定的 next-step 实验PR 45 是 elastic 真正落地的前置。