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.
This commit is contained in:
768
FIXES.md
Normal file
768
FIXES.md
Normal file
@@ -0,0 +1,768 @@
|
||||
# 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, 102–104, 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` 路径损坏
|
||||
|
||||
**严重度**: High(reachable-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 端 decode,decode 携带 `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
|
||||
|
||||
**严重度**: High(design 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.3,elastic 模式生效)。
|
||||
|
||||
**或者(不实现)**: 把 `: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
|
||||
|
||||
**严重度**: Medium(router 估算 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
|
||||
|
||||
**严重度**: Low–Medium。
|
||||
|
||||
**定位**: `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 断流时的资源泄漏
|
||||
|
||||
**严重度**: Low–Medium(高并发下可能累积)。
|
||||
|
||||
**定位**: `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` 的同时把"应在结束时扣减的值"塞进一个 dict(key=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.1(PD-sep vs PD-combined 初版对比):使用旧采样 + `--time-scale`,被 §3.6 推翻,**勿引用**。
|
||||
> - §3.5(elastic v3):warm-vs-fresh 对比无效(baseline 实例未冷启动),**勿引用**。
|
||||
> - §X 中提到的 `--max-inflight-sessions 64+` 实验:CLI 已删除,对应实验需先按 FIXES.md B2 路线 A 恢复参数后再做。
|
||||
>
|
||||
> 当前**唯一权威的**结果章节为 §3.6 与 §3.7。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 验收清单
|
||||
|
||||
修复完成后,按此清单逐项验证。
|
||||
|
||||
- [ ] `grep -rn "_inst_cumulative_tokens" .` → 0 hits(B1)
|
||||
- [ ] `python -m replayer --help` 列表里**有**或**没有** `--max-inflight-sessions`(视 B2 路线选择,二者必须自洽)
|
||||
- [ ] `bash scripts/bench.sh ...` 在干净 repo 上能跑通至少 baseline 模式
|
||||
- [ ] `grep -rn "fire-and-forget\|fire_and_forget" scripts/` → 0 hits(D1 完成)
|
||||
- [ ] `grep -rn "/home/admin/cpfs" .` → 0 hits(B6)
|
||||
- [ ] `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(落地设计)**: B4(cache-ratio gate)、M2、M3、D5
|
||||
5. **PR 5(健壮性)**: M4、S2、剩余 M5 进阶版
|
||||
|
||||
修完 PR 1–3 即可重新运行 REPORT 自己规定的 next-step 实验;PR 4–5 是 elastic 真正落地的前置。
|
||||
Reference in New Issue
Block a user