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.
28 KiB
Repo 修复指南 (FIXES.md)
本文档对应 2026-05-23 的 repo review。每条 issue 自包含:定位、动机、复现/验证、改法。按严重度从高到低排列,建议自上而下逐项修复,每条修完独立提交一个 commit。
目录
- B1. 删除死状态
_inst_cumulative_tokens - B2. 修复 replayer CLI 与 shell 脚本不一致(阻断实验)
- B3. 处理 PD-sep
--fire-and-forget损坏路径 - B4. 实现或移除 H4 cache-ratio gate
- B5. 修复
_percentileoff-by-one - B6. 统一
bench.sh的模型路径 - M1.
cached_blocks替换策略改为真正的 LRU - M2. P 候选选择避开
active_p_offloads - M3. 把
MAX_OFFLOAD_INFLIGHT暴露为 CLI 参数 - M4.
session_affinity在 combined / pd-sep 之间命名空间隔离 - M5. fallback 路径 client 断流时的资源泄漏
- M6.
_send_prefill_async与同步路径的核算不一致 - D1. 移除
_send_prefill_async与--fire-and-forget - D2. 删除/归档
run_benchmark.sh与run_experiments.sh - D3. 归档历史一次性
analyze_*.py/compare_*.py - D4. 修正
compute_roofline.py的硬编码 trace 路径 - D5.
HEAVY_THRESHOLD/OVERLOAD_FACTOR改读 args - S1. 给
replayer/metrics.py与 cost-model 加单元测试 - S2. 给 vLLM patch 加 import-time 校验
- S3. REPORT.md 加 errata block
- 验收清单
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 这个名字只有写入点,没有任何读取。
验证:
grep -rn "_inst_cumulative_tokens" /home/gahow/phd/agentic-kv
# 只应该看到 cache_aware_proxy.py 自己的 5 行;如有其它读取者,先确认意图再删
改法:
- 删除
cache_aware_proxy.py:76行_inst_cumulative_tokens: list[int] = []。 - 删除
pick_instance内的global _inst_cumulative_tokens与:103-104的初始化。 - 删除
:125的累加。 - 不需要替代实现——load 计算用
inst.ongoing_tokens,session 粘性用affinitydict。
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"。
改法:
-
修改
replayer/__main__.py: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:config = ReplayConfig( ... max_inflight_sessions=args.max_inflight_sessions, ) -
修改
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)。
- 在
-
删除 shell 脚本里的
--time-scale:scripts/run_benchmark.sh:32, 70: 删除--time-scale选项与传参。scripts/run_experiments.sh:58: 同上。
-
验证:
python -m replayer --trace traces/w600_r0.0015_st30.jsonl \ --output /tmp/x.jsonl --endpoint http://localhost:9090 \ --max-inflight-sessions 64 # 不应再报 unrecognized arguments -
同步更新
REPORT.md:430的 CLI 表格(删--time-scale,保留--max-inflight-sessions)。
路线 B:彻底删掉这两个参数 + 删 shell 脚本
如果不打算再跑高并发实验,则:
- 删
scripts/run_benchmark.sh和scripts/run_experiments.sh(与 D2 合并)。 - 修订
REPORT.md:521, 541, 530中提到--max-inflight-sessions的全部段落,明确说"该参数已删除,对应实验留给后续工作"。
任选一条,但不能保留现状。
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 直接删,那这一条自动消失。 否则按以下方式修:
-
在
_send_prefill_async里加一个asyncio.Event: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() -
在
_handle_pd_sep里,发 decode 之前await ready.wait()(带超时),保证 transfer_id 已注册: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"]) -
这样语义其实就跟同步等待几乎一样了——更佳决策是按 D1 直接删。
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";目前完全无效。
改法(推荐:实现):
-
在
cache_aware_proxy.py顶部加常量与 CLI:CACHE_GATE_RATIO = 0.3 # default; overridden by --cache-gate-ratiop.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,最好不要用 module-level 赋值,直接读 args)。 -
在
:312之前加 gate: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) -
把
--cache-gate-ratio加到scripts/bench.sh与scripts/launch_phase1_ps.sh的 proxy 启动行(默认值 0.3,elastic 模式生效)。
或者(不实现): 把 :288 的 cache_ratio 计算与写入删除,并在 analysis/elastic_hypotheses.md 顶部加一句"H4 gate 设计未落地,结论待验证"。
B5. _percentile off-by-one
严重度: Medium(影响所有 summary 数据)。
定位: replayer/metrics.py:103-107。
问题:
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 默认一致):
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
验证:
# 单测:见 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
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 的默认值改为:
MODEL="${MODEL_PATH:-$HOME/models/Qwen/Qwen3-Coder-30B-A3B-Instruct}"
并 grep -rn "/home/admin/cpfs" 检查整 repo 没有其它残留:
grep -rn "/home/admin/cpfs" /home/gahow/phd/agentic-kv
若有则一并替换为 $HOME/models/...。
M1. cached_blocks 替换策略改为真正的 LRU
严重度: Medium(router 估算 cache_hit 与真实 vLLM APC 长期偏差)。
定位: scripts/cache_aware_proxy.py:71-72(record_prefix)。
问题:
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:
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-blocksCLI(默认 200000); - 或者从 vLLM
/metrics抓vllm:gpu_cache_usage_perc反推容量。
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 罚项:
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:
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"
M3. 把 MAX_OFFLOAD_INFLIGHT 暴露为 CLI
严重度: Low–Medium。
定位: cache_aware_proxy.py:32, 312。
问题: 模块常量 MAX_OFFLOAD_INFLIGHT = 4,未暴露 CLI;高并发实验时会成为隐性 bottleneck。
改法:
-
parse_args里加:p.add_argument("--max-offload-inflight", type=int, default=4, help="Global cap on concurrent P-role offloads") -
在
_handle_combined里读global_args.max_offload_inflight而不是常量(与 D5 一致)。 -
同步
bench.sh/launch_phase1_ps.sh,elastic 模式可设大一点。
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 改成两个:
session_affinity_combined: dict[str, int] = {}
session_affinity_prefill: dict[str, int] = {}
_handle_combined 用前者,_handle_pd_sep 用后者。pick_instance 签名不变,只在调用方传不同 dict。
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 的协程里做。简化版改法:
- 包一层
try/finally在调用StreamingResponse(generate(), ...)之前,并把状态扣减用request.is_disconnected()轮询或注册到BackgroundTask。 - 或者更简单:在
inst.ongoing_tokens += input_length的同时把"应在结束时扣减的值"塞进一个 dict(key=request_id),并在app层每 30s 扫一次 stale 请求(超过request_timeout * 2的)做兜底回收。
最小可行修复:周期性 reconcile,在 cache_aware_proxy.py 里加一个后台 task:
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 路线。
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。如果保留 fire-and-forget,加上 breakdown 的 ready event(B3)后,同时确保两路径核算字段对称。
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-forget0 处使用。 - 配合 B3,这条 reachable 但 broken 的路径是 dead-on-arrival。
改法:
- 删除
_send_prefill_async整个函数。 - 删除
_handle_pd_sep里if global_args.fire_and_forget: ... else:的分支,只保留同步 path。 - 删除 CLI 里的
p.add_argument("--fire-and-forget", ...)。 grep -rn "fire-and-forget\|fire_and_forget"确认无残留。
D2. 删除/归档 run_benchmark.sh 与 run_experiments.sh
严重度: Cleanup。
定位: scripts/run_benchmark.sh、scripts/run_experiments.sh。
问题: 与 B2 同源,两脚本仍传已删 CLI 参数;事实上不再可运行。
改法:
mkdir -p scripts/legacygit mv scripts/run_benchmark.sh scripts/run_experiments.sh scripts/legacy/- 在
scripts/legacy/README.md写一行:"这些脚本对应早期--time-scale/--max-inflight-sessionsAPI,已归档,新实验请用scripts/bench.sh。" - 若选择 B2 路线 A 重新加回
--max-inflight-sessions,可顺便把run_benchmark.sh从 legacy 拉回并修参数。
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)。- 多个
compare_*.py引用已删除实验目录。
改法:
-
列一张表(在本文件下方"附录 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等)。
- 保留: 有结构化用法、对当前 trace/output 仍可跑(如
-
git mv归档类到scripts/legacy/。 -
保留类的脚本:
- 顶部加 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
D4. 修正 compute_roofline.py 的硬编码 trace 路径
严重度: Low。
定位: scripts/compute_roofline.py:165。
问题: 写死 trace_path = "traces/sampled_1000req_seed42.jsonl",文件已不存在。
改法:
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
...
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 里:
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 等。
S1. 给 replayer/metrics.py 与 cost-model 加单元测试
严重度: Quality。
问题: 整 repo 0 个测试。_percentile、InstanceState.estimate_cache_hit、pick_instance、cost-model 都该有最小覆盖。
改法:
-
新建
tests/目录,加tests/__init__.py。 -
tests/test_metrics.py: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 -
tests/test_proxy_pick.py: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 -
pyproject.toml加[tool.pytest]段,跑pytest -q。
S2. 给 vLLM patch 加 import-time 校验
严重度: Quality。
定位: patches/0001-fix-kv-transfer-abort-race.patch。
问题: 单 assert→warn 替换。未来升级 vLLM 时极易漏打 patch;当前没有运行时自检。
改法:
在 scripts/cache_aware_proxy.py 启动时(lifespan 开头)加:
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 最开始调用。
S3. REPORT.md 加 errata block
严重度: Quality(避免读者引用过期结论)。
定位: REPORT.md 顶部。
改法:
在 §1 后插入:
## 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 切分)
- PR 1(不破坏行为,纯清理): B1、D1、D2、D3、D4、B6、S3
- PR 2(修 bug): B5、M1、M5(轻量 reconcile)
- PR 3(恢复实验能力): B2 路线 A(恢复
--max-inflight-sessions),同步 S1 加单测 - PR 4(落地设计): B4(cache-ratio gate)、M2、M3、D5
- PR 5(健壮性): M4、S2、剩余 M5 进阶版
修完 PR 1–3 即可重新运行 REPORT 自己规定的 next-step 实验;PR 4–5 是 elastic 真正落地的前置。