Prhub

#40020 [kv_offload] Add multi-tier KV cache offloading framework

原始 PR 作者 ronensc 合并时间 2026-05-13 22:21 文件变更 14 提交数 62 评论 95 代码增减 +1841 / -11

执行摘要

新增多级 KV 缓存卸载框架,支持链式二级存储 / 网络

现有 CPU 单级卸载容量有限,无法满足大规模 KV 缓存或跨节点共享需求。本 PR 实现 RFC #38260 提出的 Multi-tier KV offloading 方案,将卸载堆栈扩展为可链接的二级层(如存储、网络),并保持对上层模块透明的接口。

值得精读,特别是抽象接口设计和异步批处理模式。可关注层次化管理器的错误处理和生命周期管理。

讨论亮点
  • @orozery 建议 get_finished 限制到每引擎步一次,作者在 take_events 中实现去重。
  • @liranschour 和 @orozery 认为单块 load 效率低,作者引入 PendingPromotion 按 (tier, req) 分组延迟批量提交。
  • @gemini-code-assist 指出 memoryview 可能因底层张量重分配不安全,作者添加 assert 确认零拷贝并计划后续跟踪。
  • @orozery 建议移除 store_threshold 并用静态 get_tier_type() 替代 tier_name,作者采纳。
  • @orozery 最终批准,非关键问题留后续 PR。

实现拆解

  1. 抽象接口层:在 base.py 中定义 SecondaryTierManager ABC,包含异步 lookup/submit_store/submit_load/get_finished 接口,以及 JobMetadata/JobResult 数据类型。
  2. CPU 主层包装manager.pyCPUPrimaryTierOffloadingManager 继承 CPUOffloadingManager,暴露 prepare_read/complete_read 等别名,并提供 get_kv_memoryview() 零拷贝视图。
  3. 多级编排器TieringOffloadingManager 实现级联存储(store 到所有次级)、分阶段提升(load 从次级到主层后再给 GPU)、引用计数保护、get_finished 限频(每引擎步一次)及 load 批处理。
  4. 示例实现example/__init__.py 中的 ExampleSecondaryTier 演示 LRU 驱逐和异步模拟。
  5. 配置与工厂spec.py 读取 secondary_tiers 配置,factory.py 通过 get_tier_type() 静态方法创建对应类型实例。
  6. 配套修改SharedOffloadRegion 新增 create_kv_memoryview()CPUOffloadingSpec._create_handlers 扩展分支以兼容 TieringOffloadingSpec
  7. 测试test_tiering_offloading.py 共 16 个测试用例覆盖基础、级联、提升、部分查找、无次级层等场景。
文件 模块 状态 重要度
vllm/v1/kv_offload/tiering/manager.py 卸载编排器 added 9.08
vllm/v1/kv_offload/tiering/example/__init__.py 二次层示例 added 8.87
vllm/v1/kv_offload/tiering/spec.py 卸载规范 added 8.87
vllm/v1/kv_offload/tiering/base.py 抽象接口 added 8.76
tests/v1/kv_offload/test_tiering_offloading.py 测试 added 7.76

关键符号

TieringOffloadingManager.prepare_store TieringOffloadingManager.lookup TieringOffloadingManager.prepare_load CPUPrimaryTierOffloadingManager.get_kv_memoryview ExampleSecondaryTier.submit_store ExampleSecondaryTier.submit_load TieringOffloadingSpec.get_manager create_secondary_tier

关键源码片段

vllm/v1/kv_offload/tiering/manager.py core-logic

核心编排器,包含 CPUPrimaryTierOffloadingManager 和 TieringOffloadingManager,实现级联存储、提升、引用计数保护等核心逻辑。

# 包装 CPUOffloadingManager 并添加读写别名,方便二级层调用
class CPUPrimaryTierOffloadingManager(CPUOffloadingManager):
    def __init__(
        self,
        num_blocks: int,
        mmap_region: SharedOffloadRegion,
        cache_policy: str = 'lru',
        enable_events: bool = False,
    ):
        super().__init__(
            num_blocks=num_blocks,
            cache_policy=cache_policy,
            enable_events=enable_events,
        )
        self._mmap_region = mmap_region
        # 别名:read/write 用于 CPU <-> secondary,load/store 用于 CPU <-> GPU
        self.prepare_read = self.prepare_load
        self.complete_read = self.complete_load
        self.prepare_write = self.prepare_store
        self.complete_write = self.complete_store
        # 持有一个内存视图供二级层零拷贝访问 CPU 缓存
        self._kv_memoryview = mmap_region.create_kv_memoryview()
​
    def get_kv_memoryview(self) -> memoryview:
        """返回形状为 (num_blocks, row_stride_bytes) 的 memoryview"""
        return self._kv_memoryview
​
    def shutdown(self) -> None:
        super().shutdown()
        self._kv_memoryview.release()
        self._mmap_region.cleanup()
vllm/v1/kv_offload/tiering/example/__init__.py core-logic

示例二级层实现,演示如何实现 SecondaryTierManager 接口,包含 LRU 驱逐和异步模拟。

# 纯内存实现,用于测试和作为实现参考
class ExampleSecondaryTier(SecondaryTierManager):
    def __init__(
        self,
        vllm_config: 'VllmConfig',
        primary_kv_view: memoryview,
        max_blocks: int = 1000,
        simulate_async: bool = False,
    ):
        super().__init__(vllm_config, primary_kv_view)
        self.max_blocks = max_blocks
        self.simulate_async = simulate_async
        self.blocks: OrderedDict[OffloadKey, bool] = OrderedDict()
        self.completed_jobs: list[JobResult] = []
        self.pending_jobs: list[_JobMetadata] = []
​
    def submit_store(self, job_metadata: JobMetadata) -> None:
        job_id = job_metadata.job_id
        keys = job_metadata.keys
        block_ids = job_metadata.block_ids
        assert len(keys) == len(block_ids), 'keys / block_ids 长度不匹配'
        # 过滤已存在的块
        blocks_to_store = [k for k in keys if k not in self.blocks]
        if not blocks_to_store:
            return
        # LRU 驱逐
        num_evict = len(blocks_to_store) - (self.max_blocks - len(self.blocks))
        if num_evict > 0:
            protected = set(keys)
            evicted = []
            for key in self.blocks:
                if key not in protected:
                    evicted.append(key)
                    if len(evicted) == num_evict:
                        break
            else:
                return # 无法腾出足够空间
            for key in evicted:
                del self.blocks[key]
        # 记录传输作业
        internal = _JobMetadata(job_id=job_id, keys=blocks_to_store, is_store=True)
        if self.simulate_async:
            self.pending_jobs.append(internal)
        else:
            self._complete_store_job(internal)
​
    def lookup(self, key: OffloadKey, req_context: ReqContext) -> bool | None:
        return key in self.blocks
vllm/v1/kv_offload/tiering/spec.py dependency-wiring

入口 Spec,负责解析配置并组装 TieringOffloadingManager 堆栈。

# 读取配置,创建主层和所有二级层
class TieringOffloadingSpec(CPUOffloadingSpec):
    def get_manager(self) -> OffloadingManager:
        if not self._manager:
            # 创建 scheduler 端 mmap
            world_size = self.vllm_config.parallel_config.world_size
            scheduler_mmap = SharedOffloadRegion(
                instance_id=self.vllm_config.instance_id,
                total_size_bytes=self.cpu_page_size_per_worker * world_size * self.num_blocks,
                num_blocks=self.num_blocks,
                rank=None,
                num_workers=world_size,
                cpu_page_size=self.cpu_page_size_per_worker,
            )
            self._scheduler_mmap = scheduler_mmap
            # 创建 CPU 主层
            primary_tier = CPUPrimaryTierOffloadingManager(
                num_blocks=self.num_blocks,
                cache_policy=self.eviction_policy,
                enable_events=enable_events,
                mmap_region=scheduler_mmap,
            )
            # 为所有二级层提供同一份内存视图(零拷贝共享)
            primary_kv_view = primary_tier.get_kv_memoryview()
            secondary_tiers = []
            for cfg in self.secondary_tier_configs:
                tier = create_secondary_tier(cfg, primary_kv_view, self.vllm_config)
                secondary_tiers.append(tier)
            self._manager = TieringOffloadingManager(
                primary_tier=primary_tier,
                secondary_tiers=secondary_tiers,
                enable_events=enable_events,
            )
        return self._manager

评论区精华

文件结构重组为 tiering/ 子目录 设计

orozery 建议将 secondary tier 相关代码统一放到 tiering/ 下,包括 base.py、manager.py、factory.py 等,并参考 #33689。作者按此重新组织。

结论:按建议重组,将文件从 vllm/v1/kv_offload/abstract.py 等地迁移到 tiering/。 · 已解决

get_finished 调用频率优化 性能

orozery 指出在每次 lookup/prepare 时都调用 get_finished 会带来轮询开销,建议改为每个引擎步调用一次。作者在 take_events 中设置标志位实现去重。

结论:在 take_events 中批量处理已完成任务,减少轮询频率。 · 已解决

批处理 load 请求以提升性能 性能

liranschour 和 orozery 观察到当前每次 promotion 只提交单个块,建议将多个块合并到一次 submit_load。作者实现 PendingPromotion 按 (tier, req) 分组延迟提交。

结论:引入 PendingPromotion,在引擎步末批量提交 load 请求。 · 已解决

内存视图零拷贝安全性 正确性

gemini-code-assist 提出 memoryview 可能因底层 PyTorch 张量重分配导致未定义行为,建议确保张量连续且存储存活。作者添加 assert 验证零拷贝并计划后续跟踪。

结论:添加 assert 确认 np 和 torch 共享存储,留待后续彻底解决。 · partially_resolved

移除 store_threshold 和 tier_name 配置 设计

orozery 认为 store_threshold 在分层模式下意义不大,应移除;同时建议用静态 get_tier_type 替代配置中的 tier_name。作者采纳。

结论:移除 store_threshold,添加 get_tier_type 静态方法,配置不再需要 tier_name。 · 已解决

风险与影响

  • 零拷贝安全memoryview 引用的缓冲区需严格生命周期管理,虽有 assert 但生产环境需额外保障。
  • 性能风险:异步轮询和引用计数增加 Scheduler 负担,但限频和批处理已缓解。
  • 配置兼容性secondary_tiers 格式错误将引发异常,默认关闭无影响。
  • 测试覆盖:单元测试全面,但缺少真实二级后端(如存储)的端到端验证。
  • 用户:默认行为不变,仅当配置 secondary_tiers 时激活多层卸载;ExampleSecondaryTier 可用于快速体验。
  • 系统:新增约 1800 行代码,修改 6 个现有文件但均为轻量兼容分支。
  • 团队:为开发真实二级后端(存储、网络)奠定架构基础,需维护抽象接口一致性。
零拷贝内存安全风险 实验性功能 缺少真实二级后端验证

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论