出现问题的场景

vLLM 0.18+ 启用了 KV Cache Offloading:当 GPU 内存不足时,部分 KV blocks 被卸载到 CPU。当请求重新到达时,需要从 CPU 加载回 GPU。

50 个并发请求共享同一个 10,000-token prefix,且 blocks 正在从 CPU 加载时,scheduler 的 _lookup() 对后续请求返回 None,导致它们全部被放回等待队列,等第一个 load 完成才能继续。

性能影响(2× L40S, Qwen3-8B, concurrency=50):

解决的方案

核心改动

OffloadingConnectorScheduler._lookup() 中:

# 旧逻辑:发现 blocks 正在加载 → 延迟整个请求
if any(key in self._blocks_being_loaded for key in offload_keys):
    return None

# 新逻辑:引入压力感知门控
if any(key in self._blocks_being_loaded for key in offload_keys):
    if self._gpu_kv_cache_under_pressure:
        return None   # GPU 有压力 → 等待 load 完成
    else:
        return 0      # GPU 无压力 → 跳过 CPU hit,直接 prefill

压力标志的实现

  1. 设置:在 update_state_after_alloc() 中,当 num_external_tokens > 0 时设为 True
  2. 重置:在 build_connector_meta() 开头设为 False(每个 schedule step)

作者的意图

问题分析

1. 压力判断不严谨

num_external_tokens > 0 的含义是 “有请求需要从 CPU 加载”,不等于 “GPU 内存不够了”

Blocks 可能既在 GPU 里,也被 offload 到 CPU 做备份。此时”需要 CPU 加载”不意味着 GPU 一定不够用。

2. 重置时机与调度时序脱节

调用顺序:

schedule() 开始
  → 循环处理 Request 1-N
     → _lookup()          ← 决策点,检查压力标志
     → allocate_slots()
     → update_state_after_alloc()  ← 设置压力标志
  → build_connector_meta() ← 重置压力标志

问题build_connector_meta() 在每个 step 末尾重置压力标志,而下一个 step 开头又是 False。这意味着:

Step 1: 压力=False (reset from Step 0)
  Request 1: 压力=False → return 0 → 放行 → update 后压力=True
  Request 2-50: 压力=True → return None → 全部延迟

  build_connector_meta → 压力=False

Step 2: 压力=False (刚被重置)
  Request 2: 压力=False → return 0 → 放行 → update 后压力=True
  Request 3-50: 压力=True → return None → 全部延迟

本质上每个 step 只能放行一个请求,只是从”全等”改成了”一个一个放”。

3. 并发 prefill 制造恶性循环

当压力标志为 False 时,多个请求被放行并执行 prefill:

Step 1: 压力=False
  50 个请求全部被放行 → 同时 prefill → 分配大量 GPU blocks
  → GPU 内存耗尽 → 触发 eviction
  → 更多 blocks 被存到 CPU → blocks_being_loaded 堆积
  → 下一轮 convoy 更严重

这形成了一个 “放行 → 耗尽 → convoy → 重置 → 又放行” 的震荡循环。每次放行都加剧 GPU 内存压力,导致更多 blocks 被 evict 到 CPU。