# PR #55461 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[Bugfix] Fall back to T1 when ARC cannot reclaim enough entries from T2
- 合并时间：2026-09-06 18:28
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/55461

---

# 执行摘要

- 一句话：修复 CPU KV 卸载 ARC 策略中，因 T2 无法满足驱逐需求而直接失败的问题。
- 推荐动作：该 PR 值得精读。它是一个高质量的 bugfix，清晰地定义了问题、提供了最小化的正确修复、并通过良好的参数化测试覆盖了关键边界情况。其设计决策（在 T2 优先策略无法实现时回退到 T1）值得在实现自适应策略时参考。

# 功能与动机

CPU KV offload stores can fail even when ARC has enough evictable entries. When T1 is below its adaptive target, eviction prefers T2 but currently returns `None` once T2 has no eligible candidates. This also occurs partway through a batch, or when T2 entries are pinned or protected. Fall back to eligible T1 entries after exhausting T2. Preserve the existing preference order, monotonic queue scans, pin/protection rules, and atomic failure when the total candidate count is insufficient.

# 实现拆解

1. **核心逻辑修复 (arc.py)**：修改 `ARCCachePolicy.evict()` 方法中的驱逐选择循环。原逻辑在优先尝试从 T2 驱逐（当虚拟 T1 大小低于目标时）失败后，会直接返回 `None`。修复后，在 T2 驱逐失败时，会继续利用已初始化的单调迭代器 `t1_iter` 从 T1 中寻找下一个候选条目，并相应地更新 `virtual_t1_size` 计数器，以确保批量驱逐的原子性和正确性。这样，只要 T1 或 T2 的并集总候选数足够，驱逐就不会失败。
2. **回归测试 (test_manager.py)**：新增一个参数化测试用例 `test_store_falls_back_to_t1_when_t2_cannot_satisfy_eviction`。该测试模拟多种 T2 被耗尽（固定或保护）的场景，验证在每种情况下，策略都能正确回退到 T1 完成驱逐，并确保原子性（在候选数不足时整体失败）。测试还验证了在回退成功后，新的存储操作能完成，且被固定 / 保护的条目未被驱逐。
3. **无配套变更**：此修复是纯粹的 bugfix，不涉及配置、Schema 或部署变更。

关键文件：
- `vllm/v1/kv_offload/cpu/policies/arc.py`（模块 缓存策略；类别 source；类型 core-logic）: 核心逻辑文件，包含了 ARC 驱逐策略的修复代码，是 Bug 的直接修复点。
- `tests/v1/kv_offload/cpu/test_manager.py`（模块 测试用例；类别 test；类型 test-coverage；符号 test_store_falls_back_to_t1_when_t2_cannot_satisfy_eviction）: 测试文件，新增了针对性的回归测试用例，用于验证修复的正确性并防止未来回归。

关键符号：ARCCachePolicy.evict, TestCpuKVManager.test_store_falls_back_to_t1_when_t2_cannot_satisfy_eviction

## 关键源码片段

### `vllm/v1/kv_offload/cpu/policies/arc.py`

核心逻辑文件，包含了 ARC 驱逐策略的修复代码，是 Bug 的直接修复点。

```python
# vllm/v1/kv_offload/cpu/policies/arc.py (ARC 策略 evict 方法的核心驱逐循环 )
for _ in range(n):
    candidate: tuple[OffloadKey, BlockStatus, bool] | None = None

    # 如果 T1 虚拟大小 >= 目标，优先从 T1 驱逐。
    if virtual_t1_size >= int(self.target_t1_size):
        entry = next_candidate(t1_iter)
        if entry is not None:
            candidate = (*entry, True)
            virtual_t1_size -= 1

    # 【修复点】如果上一步未找到候选（T1 未达到目标或 T1 无候选），尝试从 T2 驱逐。
    if candidate is None:
        entry = next_candidate(t2_iter)
        if entry is not None:
            candidate = (*entry, False)

    # 【修复点】关键回退：如果 T2 也未能提供候选，则回退到 T1 继续查找。
    if candidate is None:
        entry = next_candidate(t1_iter)
        if entry is None:
            # T1 和 T2 均无可用候选，驱逐原子性失败。
            return None
        candidate = (*entry, True)
        # 使用 T1 的一个候选，更新虚拟大小。
        virtual_t1_size -= 1

    candidates.append(candidate)
# ... 后续应用驱逐

```

### `tests/v1/kv_offload/cpu/test_manager.py`

测试文件，新增了针对性的回归测试用例，用于验证修复的正确性并防止未来回归。

```python
# tests/v1/kv_offload/cpu/test_manager.py ( 新增测试方法的核心逻辑 )
@pytest.mark.parametrize(
    "pinned, protected, new_keys, expected_evicted",
    [
        ([], [], [7, 8, 9], [6, 5, 3]),       # T2 耗尽，回退 T1，驱逐成功
        ([], [], [7, 8, 9, 10], [6, 5, 3, 4]), # 同上，驱逐更多条目
        ([5, 6], [], [7, 8], [3, 4]),          # T2 部分被固定，回退 T1，驱逐成功
        ([], [5, 6], [7, 8], [3, 4]),          # T2 部分被保护，回退 T1，驱逐成功
        ([5, 6], [3], [7], [4]),               # T2 大部分被固定 / 保护，仅剩条目可驱逐
        ([5, 6], [3], [7, 8], None),           # 候选总数不足，原子性失败
    ],
)
def test_store_falls_back_to_t1_when_t2_cannot_satisfy_eviction(
    self, pinned, protected, new_keys, expected_evicted
):
    """A preference for T2 must not reject stores that can reclaim T1."""
    manager, policy = self._make_manager(enable_events=False)
    # ... 初始化缓存状态 ...
    # 执行准备存储操作，并根据预期验证驱逐结果是否正确
    output = manager.prepare_store(to_keys(protected + new_keys), _EMPTY_REQ_CTX)
    if expected_evicted is None:
        # 验证原子性失败：操作返回 None 且缓存状态未变
        assert output is None
        # ...
    else:
        # 验证驱逐成功：操作返回结果，且新键已存储，旧键已驱逐
        assert output is not None
        assert output.evicted_keys == to_keys(expected_evicted)
        # ...

```

# 评论区精华

核心讨论由 Manny7717 的详细审核展开，确认了 Bug 的存在并验证了修复的正确性。讨论要点包括：
- **Bug 确认**：‘Bug is real. `ARCCachePolicy.evict()` preferred T2 whenever T1 was below its adaptive target, and returned `None` (store failure → `prepare_store` fails) whenever T2 had no eligible candidates — even when T1 held plenty of idle, unprotected blocks.’
- **修复验证**：‘After the T2 scan yields nothing, the code now continues the (already-positioned, monotonic) T1 iterator and evicts from T1, decrementing `virtual_t1_size` so the batch simulation stays consistent. Scans stay single-pass — no quadratic restart.’
- **设计决策**：‘Design note: evicting from T1 when it is below its adaptive target deviates from the pure-ARC ‘never shrink T1 under target’ rule, but that rule exists to prefer T2 — when T2 cannot satisfy, failing the store is not the right outcome either.’ 这表明修复在坚持算法核心思想（优先 T2）与工程实践（保证操作完成）之间做出了合理的权衡。

 - ARC 策略在 T2 无法满足驱逐时的回退行为 (correctness): 修复被采纳。回退到 T1 是合理且必要的，以保证操作的完成性和缓存的可用性。
 - 修复对 ARC 自适应目标规则的影响 (design): 该设计决策（在必要时违反 T1 目标规则）被视为一种务实且正确的工程权衡，未见反对意见。

# 风险与影响

- 风险：
 1. **回归风险**：修改了核心驱逐路径，但回归测试（新旧测试套件共 34 个用例）全部通过，且手动验证了在基础分支上新测试能重现原 Bug，降低了回归风险。
 2. **性能影响**：修复逻辑增加了条件判断，但保持了单次遍历的复杂度，对性能影响极小。
 3. **正确性风险**：主要风险在于对 ARC 自适应目标 (`target_t1_size`) 的跟踪是否仍然正确。修复通过同步更新 `virtual_t1_size` 来维持一致性，但长期影响需结合更多集成测试观察。
- 影响：
 1. **用户 / 功能影响**：直接影响使用 CPU KV 卸载功能的用户，提高了其可靠性。修复了在特定缓存状态下本应成功的存储操作被错误拒绝的 Bug，提升了系统在高负载或特定访问模式下的稳定性。
 2. **系统 / 模块影响**：影响范围局限于 CPU KV 卸载的 ARC 缓存策略模块 (`vllm/v1/kv_offload/cpu/policies/arc.py`)，是该模块内部的健壮性修复。
 3. **团队影响**：此修复展示了对边界条件的深入理解和测试驱动的修复方法，可为团队维护类似缓存策略提供参考。
 - 风险标记：核心路径变更 , 需要集成测试验证

# 关联脉络

- PR #50992 [Perf] [CPU Offload] Single-pass eviction scan for ARC: 本 PR 的修复明确指出要“Preserve the single-pass optimization merged in #50992”。#50992 引入了单次遍历的驱逐扫描以优化性能，而本次 Bugfix 在添加回退逻辑时，通过继续使用已有的单调迭代器，严格遵守了这一性能优化，避免了退化为二次复杂度。
- PR #50422 [CPU Offload] Add session-aware policy context: PR body 中提到此 PR “adds session-aware policy context without fixing this selection path”，说明 #50422 扩展了策略上下文但未触及本次修复的驱逐选择逻辑，两者是功能上的正交演进。