Prhub

#48017 [Perf][V1] Skip LRU hash-split in free_blocks when prefix caching is off

原始 PR 作者 adobrzyn 合并时间 2026-07-25 17:06 文件变更 1 提交数 7 评论 5 代码增减 +5 / -3

执行摘要

禁用前缀缓存时跳过 LRU 哈希分割,解码吞吐提升 3.5%

BlockPool.free_blocks() 每个引擎步调用一次。自PR #42656 引入LRU分割后,每次调用都会构建两个列表(blocks_with_hash / blocks_without_hash),并发出两次队列操作(prepend_n + append_n)。当禁用前缀缓存(enable_caching=False)时,没有任何块携带哈希,分割操作纯属多余,在短解码步(小模型或低批量)中成为可测的开销。PR body指出需要恢复PR #42656之前的行为以消除此开销。

值得快速合并的轻量性能优化。review 过程中揭示的‘GPU 缓存局部性’洞察具有一般性——减少 CPU 工作不是性能提升的唯一来源,数据放置模式同样关键。对于关注 V1 引擎调度和缓存细节的工程师,建议精读 PR #42656 及此 PR,理解 LRU 分割与快速路径的权衡逻辑。

讨论亮点
  • njhill 指出:“I looked into this because the performance difference was surprising. It turns out it's not related to any extra CPU work but rather better GPU cache locality from reusing recently used blocks.” 性能来源被重新归因为 GPU 缓存局部性,而非 CPU 开销节省。
  • njhill 建议只需修改一行条件,删除之前更复杂的早期返回方案,以保持代码简洁。
  • adobrzyn 接受建议,还原了早期返回分支,应用了单行条件修改。

实现拆解

  1. 定位热路径:BlockPool.free_blocks() 每个 engine step 被调用,接受按 eviction priority 排序的 blocks。原有逻辑(PR #42656)将无哈希块放入 blocks_without_hash 然后 prepend_n(即优先 evict),有哈希块 append_n(即保留顺序)。当 enable_caching=False,所有 block_hash 均为 None,所以 blocks_with_hash 为空,分割与二次队列操作完全浪费。
  2. 尝试早期返回:初始提交增加了一个 if not self.enable_caching 的早期分支,直接对所有 block 只做 append_n。Reviewer njhill 指出性能提升的主要来源不是节省 CPU 时间,而是改变队列插入位置带来的 GPU 缓存局部性改善(将最近释放的块集中在队列尾部,后续分配时更可能重用热数据)。
  3. 简化实现:njhill 建议删除早期返回,仅修改条件行,将 if block.block_hash is None: 改为 if block.block_hash is None and self.enable_caching:。这样当 enable_caching=False 时,所有块(包括无哈希块)都进入 blocks_with_hash 列表,进而统一调用 append_n,达到相同效果。adobrzyn 接受建议,还原早期返回,最终 diff 仅为一行条件加一条注释。
  4. 性能验证:在解码密集型场景(FP8 密集模型,prefix caching off, 4096 in / 1024 out, max-concurrency 8)测试,输出吞吐从 ~734 tok/s 提升至 ~760 tok/s,median TPOT 从 ~10.5ms 下降到 ~10.1ms。enable_caching=True 路径性能无变化。
  5. 测试与配置:无新增配置项。已有 CI 测试覆盖两种缓存模式,提交前通过 pre-commit run --all-files(ruff-check + ruff-format)。无专门针对此逻辑的单元测试变更,但核心逻辑变化极小,回归风险低。
文件 模块 状态 重要度
vllm/v1/core/block_pool.py 块池 modified 5.8

关键符号

BlockPool.free_blocks

关键源码片段

vllm/v1/core/block_pool.py core-logic

唯一修改的文件,包含核心变更:在 free_blocks 方法中调整条件以跳过 LRU 分割。

def free_blocks(self, ordered_blocks: Iterable[KVCacheBlock]) -> None:
    """Free a list of blocks. The blocks should be ordered by their
    eviction priority, where the first block will be evicted first.    Args:
        ordered_blocks: A list of blocks to free ordered by their eviction
            priority.
    """
    # Identify blocks with hash (LRU cache) and without it (never match APC)
    blocks_with_hash = []
    blocks_without_hash = []
    for block in ordered_blocks:
        block.ref_cnt -= 1
        if block.ref_cnt == 0 and not block.is_null:
            # When caching is disabled we always append for better
            # GPU cache locality from reusing recently used blocks
            if block.block_hash is None and self.enable_caching:
                blocks_without_hash.append(block)
            else:
                blocks_with_hash.append(block)
​
    # Blocks without hash get evicted first - prepend them last to the tail
    self.free_block_queue.prepend_n(blocks_without_hash)
    self.free_block_queue.append_n(blocks_with_hash)

评论区精华

性能来源与实现简化 性能

njhill 指出性能提升主要来自 GPU 缓存局部性而非 CPU 节省,并建议简化早期返回实现为单条件修改。

结论:接受建议,还原早期分支,改为在条件行追加 and self.enable_caching。 · 已解决

风险与影响

风险极低。变更仅在 enable_caching=False 的路径生效,enable_caching=True 路径完全不变。当 enable_caching=False 时,所有被释放的 block 必然 block_hash is None,因此条件 (block.block_hash is None and self.enable_caching) 为假,所有块进入 blocks_with_hash 列表并统一调用 append_n。这一行为与删除分割后的预期一致,且与原有 prepend_n + 空 append_n 相比,唯一的区别是队列插入位置变为尾部,不会影响引用计数或安全条件。潜在风险是新注释可能被误读,但代码逻辑本身无歧义。由于无新增配置或依赖,部署零风险。

对禁用前缀缓存的用户:解码吞吐提升约 3.5%,延迟降低相应比例。对启用前缀缓存的用户:无任何影响,代码路径不变。对系统:无兼容性或接口变更。对团队:简便的优化,后续需注意如果引入无哈希块在其他场景可能变化,但当前逻辑固定。测试层面:原 PR 作者通过性能基准验证,但缺少针对 free_blocks 逻辑的单元测试,建议未来补充以防回归。

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论