Prhub

#50301 [KV Offload] Enable single-copy MLA layout for CPUOffloadingSpec

原始 PR 作者 Change72 合并时间 2026-07-31 11:20 文件变更 3 提交数 6 评论 6 代码增减 +119 / -17

执行摘要

默认 KV 卸载后端启用 MLA 单副本布局,容量 ×TP

Issue #47929 指出 MLA 模型的 latent KV 在 TP rank 间是逻辑复制且期望字节一致的(MLA 硬编码 num_kv_heads=1,latent 投影不做 TP 分片),但原生 offload 路径没有利用这一点:每个 TP worker 各存一份副本,CPUOffloadingSpec 按 per-worker bytes × world size 计价,TP=N 时唯一内容容量只剩配置预算的约 1/N。PR body 明确说明目标是“Enable the single-copy (replicated) MLA layout for the default KV-offload backend CPUOffloadingSpec, closing out the plan in #47929”,并强调这是与 TieringOffloadingSpec 已用能力对齐的收尾工作。

值得精读。核心看点有两个:一是用 _uses_shared_region() 单一事实来源同时约束容量计算和分配路径,杜绝配置与行为漂移;二是对非共享区域部署 fail-closed 的数据保护设计——把“平台不支持”从性能权衡提升为正确性约束,并用专门测试钉死。对从事 KV offload、分布式缓存去重或 MLA 推理优化的工程师有直接参考价值。

讨论亮点

Review 由维护者 orozery 主导,全部意见均已落实并获 approve。

  • docstring 精简:orozery 对 _uses_shared_region 的冗长说明提出“Can we minimize this comment?”,作者在 3161201 提交中改为要点式说明,完整的数据丢失理由保留在 PR 描述和测试注释里(category: style)。
  • 测试命名:orozery 转述 Claude 建议,将 test_cpu_spec_replicated_config_ignored_off_shared_region 改名为 test_cpu_spec_replicated_disabled_without_shared_region,作者在 3d396f3 落实(category: testing)。
  • 参数化 rank 测试:orozery 建议把只覆盖 replicated 分支的 rank 测试改造成参数化用例,覆盖 (replicated, device_index, world_size, expected_rank) 组合,作者在 8e251a0 落实,降低测试脆弱性(category: testing)。
    • 另外 Claude bot 因 fork PR 自动 review 被禁用,未产生实质讨论。

实现拆解

  1. 新增平台门控 hook(vllm/v1/kv_offload/cpu/spec.py):删除类属性 SUPPORTS_REPLICATED_LAYOUT,新增 _uses_shared_region(),默认返回 current_platform.is_cuda_alike()。该 hook 是 replicated 布局的单一事实来源,明确表达了“只有 worker CPU buffer 实际分配在共享 mmap 区域时才能去重”的前提。
  2. 重构 sizing 门控(cpu/spec.py __init__replicated_layoutconfig.replicated_layout and self._uses_shared_region() 决定;当开启时 num_copies = 1kv_bytes_per_chunk 只计单副本,cpu_page_size_per_workernum_blocks 相应按单副本计算,使相同 cpu_bytes_to_use 下可缓存块数提升到 ×TP。
  3. 调整 create_worker 分配路径(cpu/spec.py):在共享区域路径下,若 replicated_layout 为真则所有 rank 映射到 slot 0(单副本),否则沿用原逻辑把物理设备索引折叠进 [0, world_size)。两步共用同一 hook,避免 sizing 与分配路径漂移导致配置失效。
  4. 保护 TieringOffloadingSpec 行为(tiering/spec.py):override _uses_shared_region() 恒返回 True,因为 tiering 在所有平台上都分配在共享区域,不能被 CUDA-alike 检查收窄;其余逻辑不变。
  5. 测试配套(tests/v1/kv_offload/test_factory.py):新增 test_cpu_spec_replicated_layout_truth_matrix(配置×平台全排列)、test_cpu_spec_replicated_sizing_on_shared_region(单副本 sizing)、test_cpu_spec_replicated_disabled_without_shared_region(数据丢失保护)以及参数化的 test_cpu_spec_create_worker_rank_assignment(比较 replicated/非 replicated 两分支与边界)。
文件 模块 状态 重要度
vllm/v1/kv_offload/cpu/spec.py KV 卸载 modified 6.61
vllm/v1/kv_offload/tiering/spec.py KV 卸载 modified 5.27
tests/v1/kv_offload/test_factory.py 单元测试 modified 6.93

关键符号

CPUOffloadingSpec._uses_shared_region CPUOffloadingSpec.create_worker TieringOffloadingSpec._uses_shared_region

关键源码片段

vllm/v1/kv_offload/cpu/spec.py core-logic

核心变更文件:新增 _uses_shared_region() hook 并重构 replicated_layout 门控与 create_worker rank 分配,是去重逻辑的唯一事实来源。

class CPUOffloadingSpec(OffloadingSpec):
    BLOCK_SIZE_ALIGNMENT = SharedOffloadRegion.BLOCK_SIZE_ALIGNMENT
​
    def __init__(self, config: OffloadingConfig):
        super().__init__(config)
​
        cpu_bytes_to_use = self.extra_config.get("cpu_bytes_to_use")
        if not cpu_bytes_to_use:
            raise Exception("cpu_bytes_to_use must be specified in kv_connector_extra_config")
​
        world_size = config.parallel.world_size
        self.num_blocks = 0
        self.kv_bytes_per_chunk = 0
        self.cpu_page_size_per_worker = 0
        # 去重门控:配置开启 AND 部署实际落在共享 mmap 区域上。
        # 平台不满足(如 XPU 的私有 pinned tensor)时必须 fail-closed,
        # 否则 rank-0 writer gate 会 ack 掉 rank>0 的 store 而 buffer 是空的。
        self.replicated_layout = config.replicated_layout and self._uses_shared_region()
        if config.worker_kv_bytes_per_block > 0 and world_size > 0:
            num_copies = 1 if self.replicated_layout else world_size
            kv_bytes_per_block = config.worker_kv_bytes_per_block * num_copies
            kv_bytes_per_chunk = kv_bytes_per_block * self.blocks_per_chunk
            # 每个 worker 平均分到的页面大小:去重时即单副本大小
            self.cpu_page_size_per_worker = kv_bytes_per_chunk // num_copies
            aligned_kv_bytes_per_chunk = round_up(
                kv_bytes_per_chunk, self.BLOCK_SIZE_ALIGNMENT
            )
            self.num_blocks = int(cpu_bytes_to_use) // aligned_kv_bytes_per_chunk
            self.kv_bytes_per_chunk = aligned_kv_bytes_per_chunk
​
    def _uses_shared_region(self) -> bool:
        """worker CPU buffer 是否落在共享 mmap 区域(而非每 rank 私有 tensor)。"""
        return current_platform.is_cuda_alike()
​
    def create_worker(self, kv_caches: CanonicalKVCaches) -> CPUOffloadingWorker:
        mmap_region: SharedOffloadRegion | None = None
        if self._uses_shared_region() and self.num_blocks > 0:
            # replicated 布局:所有 rank 共用 slot 0(单一 MLA 副本)
            if self.replicated_layout:
                rank = 0
            else:
                # 非去重:把物理设备索引折叠进 [0, world_size) 槽位
                world_size = self.config.parallel.world_size
                rank = torch.accelerator.current_device_index() % world_size
            mmap_region = SharedOffloadRegion(
                engine_id=self.config.engine_id,
                num_blocks=self.num_blocks,
                rank=rank,
                kv_bytes_per_block=self.kv_bytes_per_chunk,
                cpu_page_size=self.cpu_page_size_per_worker,
            )
        # num_blocks == 0 时退回 tensor 路径(空 tensor 场景)
        return CPUOffloadingWorker(
            kv_caches=kv_caches,
            blocks_per_chunk=self.blocks_per_chunk,
            num_cpu_blocks=self.num_blocks,
            mmap_region=mmap_region,
        )
vllm/v1/kv_offload/tiering/spec.py core-logic

通过 override _uses_shared_region() 恒返回 True,保持 tiering 后端在所有平台上的 replicated 行为不变,避免被 CPU spec 的 CUDA-alike 检查收窄。

class TieringOffloadingSpec(CPUOffloadingSpec):
    BLOCK_SIZE_ALIGNMENT = SharedOffloadRegion.BLOCK_SIZE_ALIGNMENT
​
    @override
    def _uses_shared_region(self) -> bool:
        # Tiering 在所有平台上都分配在共享区域,因此 replicated 门控
        # 不能被 CPU spec 的 CUDA-alike 检查收窄,行为保持与之前一致。
        return True
​
    @override
    def create_worker(self, kv_caches: CanonicalKVCaches) -> CPUOffloadingWorker:
        world_size = self.config.parallel.world_size
        if self.replicated_layout:
            rank = 0
        else:
            rank = torch.accelerator.current_device_index() % world_size
        worker_mmap = SharedOffloadRegion(
            engine_id=self._engine_id,
            num_blocks=self.num_blocks,
            rank=rank,
            kv_bytes_per_block=self.kv_bytes_per_chunk,
            cpu_page_size=self.cpu_page_size_per_worker,
        )
        return CPUOffloadingWorker(
            kv_caches=kv_caches,
            blocks_per_chunk=self.blocks_per_chunk,
            num_cpu_blocks=self.num_blocks,
            mmap_region=worker_mmap,
        )
tests/v1/kv_offload/test_factory.py test-coverage

新增 5 组测试覆盖平台×配置真值表、共享区域单副本 sizing、非共享区域数据丢失保护、create_worker rank 分配参数化,是 PR 正确性论证的关键支撑。

@pytest.mark.parametrize("world_size", [2, 4, 8])
def test_cpu_spec_replicated_disabled_without_shared_region(
    monkeypatch, world_size: int
):
    # 数据丢失保护:非 CUDA-alike 平台仍是每 rank 私有 pinned tensor,
    # replicated 布局必须保持关闭;否则 rank-0 writer gate 会 ack 掉
    # rank>0 的 store 而不执行 D2H copy,后续 load 读到空 buffer。
    import vllm.v1.kv_offload.cpu.spec as cpu_spec_module
​
    monkeypatch.setattr(
        cpu_spec_module.current_platform, "is_cuda_alike", lambda: False
    )
    worker_kv_bytes_per_block = SharedOffloadRegion.BLOCK_SIZE_ALIGNMENT
    spec = _create_spec(
        cpu_bytes_to_use=worker_kv_bytes_per_block * world_size * 2,
        worker_kv_bytes_per_block=worker_kv_bytes_per_block,
        world_size=world_size,
        replicated_layout=True,
    )
​
    assert isinstance(spec, CPUOffloadingSpec)
    assert spec.replicated_layout is False
    assert spec.cpu_page_size_per_worker == worker_kv_bytes_per_block
    assert spec.kv_bytes_per_chunk == worker_kv_bytes_per_block * world_size
    assert spec.num_blocks == 2

评论区精华

精简 _uses_shared_region docstring style

orozery 对新增 hook 的冗长 docstring 提出“Can we minimize this comment?”,认为数据丢失的完整理由不需要都写在 docstring 里。

结论:作者在 3161201 提交中精简为要点式说明,完整理由保留在 PR 描述与专门测试中。 · 已解决

测试命名建议 测试

orozery 转述 Claude 建议,将 `test_cpu_spec_replicated_config_ignored_off_shared_region` 改名为 `test_cpu_spec_replicated_disabled_without_shared_region`,更准确表达 fail-closed 语义。

结论:作者在 3d396f3 提交中完成改名。 · 已解决

create_worker rank 测试参数化 测试

orozery 提出原 replicated-only rank 测试较脆弱,建议参数化覆盖 replicated(slot 0)、非 replicated(设备索引取模)及边界情况。

结论:作者在 8e251a0 提交中改为参数化 `test_cpu_spec_create_worker_rank_assignment`。 · 已解决

风险与影响

  1. 平台门控依赖(fail-closed 风险)_uses_shared_region() 目前只认 is_cuda_alike(),非 CUDA-alike(当前是 XPU)仍走每 rank 私有 pinned tensor。若未来平台能力变化或 hook 实现漂移,可能在私有 buffer 上误开 replicated 布局,触发 PR 描述的“rank-0 writer gate ack 了 rank>0 的 store 但没做 D2H copy,后续 load 读到空 buffer”数据损坏。测试虽用 monkeypatch 钉住了真值表,但无法覆盖真实 XPU 硬件。
  2. MLA 字节一致假设:去重成立的前提是 homogeneous TP 且无 context parallelism 时 per-rank MLA KV 字节一致。若未来支持 CP 或异构 TP,单副本布局会读到错误数据;当前 replicated_layout 配置门控本身仍无平台/并行约束,风险被显式继承而非消除。
  3. create_worker 分支改动:非 replicated 路径的 rank 计算被重构(从 is_cuda_alike() 分支改为 _uses_shared_region() 分支),逻辑等价但改变了代码结构,可能影响未来维护者对分配路径的理解;好在有参数化测试覆盖。
  4. 验证范围:E2E 仅在 A100 TP=2 单节点验证,store bytes 精确降至 1/TP 且 greedy 输出一致;多节点、更大 TP、tiering 次级存储场景未在 CI 覆盖。

用户侧:使用 MLA 模型(如 DeepSeek-V2-Lite)+ TP>1 + 默认 CPU offload 后端的部署,在 CUDA/ROCm 上自动获得缓存容量 ×TP、D2H store 流量降至 1/TP 的收益,无需改配置;store 吞吐指标变为仅统计 writer(rank 0),与 tiering 已有口径一致。非 CUDA-alike 平台(XPU)行为不变,继续使用每 rank 私有 tensor。系统侧_uses_shared_region() 成为 offload spec 的重要扩展点,后续新后端接入 replicated 布局只需实现该 hook;TieringOffloadingSpec 行为完全不变。团队侧:该 PR 是 #47929 计划的收尾,并为 #48408 的多组/非 MLA uniform 布局扩展铺路;风险收益比较清晰,测试覆盖充分,维护成本低。

平台门控 fail-closed 依赖 MLA 副本字节一致假设 XPU 保持私有路径 E2E 仅验证单节点 TP=2

关联 Issue

#47929 [Feature]: Deduplicate replicated MLA KV across TP ranks in native offloading

完整报告

参与讨论