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

28 KiB
Raw Permalink Blame History

Repo 修复指南 (FIXES.md)

本文档对应 2026-05-23 的 repo review。每条 issue 自包含:定位、动机、复现/验证、改法。按严重度从高到低排列,建议自上而下逐项修复,每条修完独立提交一个 commit。


目录


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 这个名字只有写入点,没有任何读取。

验证:

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_tokenssession 粘性用 affinity dict。

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

    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,
    )
    
  2. 修改 replayer/replay.pyReplayConfig 与 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. 验证:

    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.shscripts/run_experiments.sh(与 D2 合并)。
  2. 修订 REPORT.md:521, 541, 530 中提到 --max-inflight-sessions 的全部段落,明确说"该参数已删除,对应实验留给后续工作"。

任选一条,但不能保留现状。


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-forgetasyncio.create_task(_send_prefill_async(...)) 不等 P 完成
  • 紧接 :570-583 立刻发起 D 端 decodedecode 携带 remote_bootstrap_addr + remote_engine_id + transfer_id
  • 但 P 端此时可能尚未注册 transfer_idMooncake 拉取失败 → D 端 502。
  • 此外 _send_prefill_async:507-521 在异常分支只 breakdown["prefill_error"] = True,错误不会传递给 client。

改法:

如果按 D1 直接删,那这一条自动消失。 否则按以下方式修:

  1. _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()
    
  2. _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"])
    
  3. 这样语义其实就跟同步等待几乎一样了——更佳决策是按 D1 直接删。


B4. 实现或移除 H4 cache-ratio gate

严重度: Highdesign doc 与代码不一致 / fake feature

定位: scripts/cache_aware_proxy.py:288, 308analysis/elastic_hypotheses.mdscripts/run_h4_cache_gate.sh

问题:

  • cache_ratio = cache_hit / max(input_length, 1) 计算后仅写入 breakdown,没有任何分支根据它决策。
  • analysis/elastic_hypotheses.mdrun_h4_cache_gate.sh 都假定"当 cache_ratio < 阈值时不 offload";目前完全无效。

改法(推荐:实现):

  1. cache_aware_proxy.py 顶部加常量与 CLI

    CACHE_GATE_RATIO = 0.3  # default; overridden by --cache-gate-ratio
    
    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,最好不要用 module-level 赋值,直接读 args

  2. :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)
    
  3. --cache-gate-ratio 加到 scripts/bench.shscripts/launch_phase1_ps.sh 的 proxy 启动行(默认值 0.3elastic 模式生效)。

或者(不实现): 把 :288cache_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) = 50Python 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.shlaunch_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

严重度: Mediumrouter 估算 cache_hit 与真实 vLLM APC 长期偏差)。

定位: scripts/cache_aware_proxy.py:71-72record_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-blocks CLI默认 200000
  • 或者从 vLLM /metricsvllm: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

严重度: LowMedium。

定位: cache_aware_proxy.py:32, 312

问题: 模块常量 MAX_OFFLOAD_INFLIGHT = 4,未暴露 CLI高并发实验时会成为隐性 bottleneck。

改法:

  1. parse_args 里加:

    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 一致)。

  3. 同步 bench.sh / launch_phase1_ps.shelastic 模式可设大一点。


M4. session_affinity 在 combined / pd-sep 之间命名空间隔离

严重度: Low当前不会同时跑两种模式但属隐患

定位: cache_aware_proxy.py:158, 532

问题: 全局 session_affinity: dict[str, int]combined 模式 idx 指向 combined_instancespd-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 断流时的资源泄漏

严重度: LowMedium高并发下可能累积

定位: cache_aware_proxy.py:438-467_handle_heavy_offload fallback:364-387_handle_combined 主路径);:585-598_handle_pd_sep)。

问题:

  • StreamingResponse 返回后,若 client 在 generator 未被消费时断开generator 不会进入 tryfinally 不会触发。
  • 结果: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 的)做兜底回收。

最小可行修复:周期性 reconcilecache_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 eventB3)后,同时确保两路径核算字段对称。


D1. 移除 _send_prefill_async--fire-and-forget

严重度: Cleanup。

定位: cache_aware_proxy.py:507-521function:552-554caller:634-635CLI flag

问题:

  • grep 全 repo 所有 launch / bench / experiment 脚本,--fire-and-forget 0 处使用。
  • 配合 B3,这条 reachable 但 broken 的路径是 dead-on-arrival。

改法:

  1. 删除 _send_prefill_async 整个函数。
  2. 删除 _handle_pd_sepif global_args.fire_and_forget: ... else: 的分支,只保留同步 path。
  3. 删除 CLI 里的 p.add_argument("--fire-and-forget", ...)
  4. grep -rn "fire-and-forget\|fire_and_forget" 确认无残留。

D2. 删除/归档 run_benchmark.shrun_experiments.sh

严重度: Cleanup。

定位: scripts/run_benchmark.shscripts/run_experiments.sh

问题: 与 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 路线 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 引用已删除实验目录。

改法:

  1. 列一张表(在本文件下方"附录 A"或新建 scripts/INVENTORY.md),把每个 analyze/compare 脚本归类:

    • 保留: 有结构化用法、对当前 trace/output 仍可跑(如 analyze_trace.pyanalyze_breakdown.pyanalyze_cache_hit.pyanalyze_eviction.pycompare_results.py)。
    • 归档: 一次性、特定实验 IDcompare_ab_final.pycompare_balanced.pycompare_elastic_v4.pycompare_p2p.pyfinal_*.pycompare_aggregation.pyanalyze_3way.pyanalyze_h4_results.pyanalyze_h5_rdma.pyprofile_*.pyplot_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

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 个测试。_percentileInstanceState.estimate_cache_hitpick_instance、cost-model 都该有最小覆盖。

改法:

  1. 新建 tests/ 目录,加 tests/__init__.py

  2. 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
    
  3. 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
    
  4. 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.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.pycache_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.5B5

修复顺序建议(按 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 真正落地的前置。