Prhub

#55424 [Bugfix][KV Connector] Only enforce disk block alignment for O_DIRECT

原始 PR 作者 mevince 合并时间 2026-09-12 04:13 文件变更 2 提交数 4 评论 11 代码增减 +63 / -3

执行摘要

仅 O_DIRECT 强制块对齐,修复 page-cache 磁盘 offload 启动失败

PR body 明确指出:DiskBackend.init 无条件 assert total_block_bytes % 4096 == 0,该不变式仅对 O_DIRECT 成立(内核 DMA 要求 buffer、文件 offset 与传输长度块对齐),而 use_page_cache=True 时文件不带 O_DIRECT 打开,buffered preadv/pwritev 没有对齐约束。断言在打开标志决定前触发,使 KV per-block stride 为 512 对齐而非 4096 对齐的混合模型(如 DeepSeek 每 token 576 值 latent、GLM-5.2 叠加 132 字节 sparse-attention index)在 page cache 模式下直接启动失败:AssertionError: total_block_bytes=3098112 not aligned to 4096。

值得精读。改动虽小,但完整展示了从运行时报错定位到“约束来源”并只对真正需要约束的路径强制的思路:断言应贴近产生约束的 I/O 模式,而不是无条件全局生效。建议同步关注 mevince 提出的 O_DIRECT 写时 padding follow-up,若落地将让更多模型在直接 I/O 下受益。

讨论亮点

ivanium 最初质疑:"Thanks for the PR but I feel a 4k alignment is not a unreasonable requirement. Can you elaborate on when this fix may help?" mevince 回应:DeepSeek 把每个 token 压缩为单个 576 值 latent vector,GLM-5.2 又叠加 132 字节/token 的 sparse-attention index,两者都凑不出 4096 对齐;Qwen 的 8 头 128 维 KV 恰好是 4096 倍数。ivanium 认可:"It makes sense to me now. The PR as the current shape looks good to me." 并 APPROVE。mevince 还提出可为 O_DIRECT 场景在写盘时 padding 到 4096 作为 follow-up。

实现拆解

  1. 根因确认:在 vllm/v1/simple_kv_offload/disk_backend.py 的 DiskBackend.init 中,total_block_bytes % _ALIGNMENT == 0 的断言先于文件打开方式决策执行;O_DIRECT 是唯一需要 4096 对齐的路径。
  2. 条件化断言:把断言包进 if not use_page_cache 分支,并补充注释说明对齐约束来自内核 DMA;O_DIRECT 路径保持原有强校验,page cache 路径不再误伤合法布局。
  3. 回归测试:在 tests/v1/simple_kv_offload/test_worker.py 新增参数化测试 test_disk_backend_alignment_only_required_for_direct_io,三组参数分别覆盖 4096+page cache(通过)、512+page cache(通过)、512+非 page cache(AssertionError),并校验 page cache 下 backing file 按 num_blocks * block_bytes 创建。
  4. 测试隔离:最后一次提交为对齐门测试 stub pin_tensor/unpin_tensor,使测试不依赖 cudaHostRegister 的注册状态或前序测试残留区间,避免 flake;pin/unpin 生命周期由独立回归测试覆盖。
  5. 验证:通过 CUDA/ROCm gated 的 pytest 命令 tests/v1/simple_kv_offload/test_worker.py -k alignment_only_required_for_direct_io,以及 Buildkite CI(两次 /ci run、一次 /ci retry)验证。
文件 模块 状态 重要度
vllm/v1/simple_kv_offload/disk_backend.py 磁盘后端 modified 5.71
tests/v1/simple_kv_offload/test_worker.py Worker 测试 modified 5.88

关键符号

DiskBackend.init test_disk_backend_alignment_only_required_for_direct_io

关键源码片段

vllm/v1/simple_kv_offload/disk_backend.py core-logic

核心修复:将对齐断言从无条件改为仅 O_DIRECT 路径执行,解除 page cache 模式下混合模型启动被拒的问题;O_DIRECT 不变式保持不变。

# vllm/v1/simple_kv_offload/disk_backend.py
class DiskBackend:
    # 磁盘 KV offload 后端:支持 O_DIRECT 直写与 page cache 缓冲两种模式
​
    def init(
        self,
        gpu_caches: dict[str, torch.Tensor],
        device: torch.device,
        load_stream: torch.cuda.Stream,
        store_stream: torch.cuda.Stream,
        disk_path: str,
        num_disk_slots: int,
        total_block_bytes: int,
        num_buffer_slots: int = 2,
        use_page_cache: bool = False,
    ) -> None:
        self._load_stream = load_stream
        self._store_stream = store_stream
        self._total_block_bytes = total_block_bytes
        self._num_buffer_slots = num_buffer_slots
        self._tensor_names = list(gpu_caches.keys())
        # 每个 tensor 的每 block 字节数(stride(0) * element_size())
        self._per_tensor_bpb = [
            t.stride(0) * t.element_size() for t in gpu_caches.values()
        ]
​
        # 4096 对齐约束只属于 O_DIRECT:内核 DMA 要求 buffer、文件 offset
        # 与传输长度都按块对齐;走 page cache 的 buffered preadv/pwritev
        # 并没有这个约束。此前无条件 assert 会让 per-block stride 仅 512
        # 对齐的混合模型(如 DeepSeek、GLM-5.2/5.3)在 use_page_cache=True
        # 时也无法启动,因此把强校验收敛到 O_DIRECT 路径。
        if not use_page_cache:
            assert total_block_bytes % _ALIGNMENT == 0, (
                f'total_block_bytes={total_block_bytes} not aligned to {_ALIGNMENT}'
            )
​
        # store/load 两个独立 buffer 池,供后台线程使用
        self._store_buffer_caches = {}
        self._load_buffer_caches = {}
        for name, gpu_t in gpu_caches.items():
            bpb = gpu_t.stride(0) * gpu_t.element_size()
            store_buf = _alloc_aligned(num_buffer_slots, bpb)
            pin_tensor(store_buf)
            self._store_buffer_caches[name] = store_buf
            load_buf = _alloc_aligned(num_buffer_slots, bpb)
            pin_tensor(load_buf)
            self._load_buffer_caches[name] = load_buf
​
        # 预构建每个 slot 的 iovec 视图,避免每次传输都调用 .numpy()
        self._store_slot_views = [
            [
                memoryview(self._store_buffer_caches[name][slot].numpy())
                for name in self._tensor_names
            ]
            for slot in range(num_buffer_slots)
        ]
        self._load_slot_views = [
            [
                memoryview(self._load_buffer_caches[name][slot].numpy())
                for name in self._tensor_names
            ]
            for slot in range(num_buffer_slots)
        ]
tests/v1/simple_kv_offload/test_worker.py test-coverage

新增参数化回归测试,三组参数验证对齐约束只在 O_DIRECT 生效,并校验 backing file 大小语义;stub pin/unpin 保证 hermetic。

# tests/v1/simple_kv_offload/test_worker.py
# 三组参数:4096+page cache(基线通过)、512+page cache(修复目标)、
# 512+O_DIRECT(约束仍生效)
@pytest.mark.parametrize(
    ('block_bytes', 'use_page_cache', 'expect_ok'),
    [
        (4096, True, True), # aligned control
        (3 * 512, True, True), # 512-aligned only: fine for buffered I/O
        (3 * 512, False, False), # O_DIRECT keeps enforcing 4096 alignment
    ],
)
def test_disk_backend_alignment_only_required_for_direct_io(
    tmp_path, monkeypatch, block_bytes: int, use_page_cache: bool, expect_ok: bool
):
    # 4096 对齐断言只属于 O_DIRECT;page-cache I/O 不应触发它。
    # 混合模型(例如每个 rank 有 101 个 cache tensor)的 block stride
    # 可能是 512 对齐但不是 4096 对齐,此前会让 use_page_cache=True 也无法启动。
    # host pinning 是独立生命周期问题,对齐门不应依赖注册状态;
    # 打桩可避免 cudaHostRegister 失败或前序测试残留注册区间导致 flake。
    monkeypatch.setattr(
        'vllm.v1.simple_kv_offload.disk_backend.pin_tensor', lambda t: None
    )
    monkeypatch.setattr(
        'vllm.v1.simple_kv_offload.disk_backend.unpin_tensor',
        lambda t: None,
        raising=False, # 部分分支 teardown 侧没有 unpin,宽容处理
    )
    num_blocks = 4
    gpu = {'k': torch.zeros((num_blocks, block_bytes), dtype=torch.int8, device='cuda')}
    backend = DiskBackend()
    disk_path = tmp_path / 'kv-offload.bin'
​
    def _init() -> None:
        backend.init(
            gpu,
            gpu['k'].device,
            torch.cuda.Stream(),
            torch.cuda.Stream(),
            str(disk_path),
            num_blocks,
            block_bytes,
            use_page_cache=use_page_cache,
        )
​
    # O_DIRECT 场景必须依旧拒绝非 4096 对齐布局
    if not expect_ok:
        with pytest.raises(AssertionError, match='not aligned'):
            _init()
        return
​
    # 通过场景:page cache 允许 512 对齐,且 backing file 按 num_slots * stride 创建
    _init()
    try:
        assert disk_path.stat().st_size == num_blocks * block_bytes
    finally:
        backend.shutdown()

评论区精华

4k 对齐要求是否不合理,该修复何时有帮助 question

ivanium 在 review 中质疑 4k 对齐并非不合理要求,请作者说明修复适用场景。mevince 回应:DeepSeek 每 token 压缩为 576 值 latent vector,GLM-5.2 又叠加 132 字节 /token sparse-attention index,导致 block stride 只有 512 对齐;Qwen 的 8 头 128 维 KV 恰好 4096 对齐。

结论:ivanium 认可解释,认为 PR 当前形态 OK 并 APPROVE。 · 已解决

O_DIRECT 场景是否写时 padding 到 4096 设计

mevince 提出 O_DIRECT 场景下可在写盘时将 KV cache padding 到 4096 作为 follow-up,询问是否合理。ivanium 认为当前 PR 可以合并,未对 follow-up 表态。

结论:作为 follow-up 未实施;当前 PR 仅放宽 page cache 路径。 · 待处理

对齐测试对 host pinning 的 hermetic 隔离 测试

最后一笔提交将测试改为 stub pin_tensor/unpin_tensor,避免对齐门测试依赖 cudaHostRegister 的注册状态,防止前序测试残留注册区间导致 flake。

结论:已实现并通过 CI;pin/unpin 生命周期由单独回归测试覆盖。 · 已解决

风险与影响

风险点集中在放宽后的 page cache 路径:1)disk_backend.py 中除断言外,可能还有其他代码假设 total_block_bytes 4096 对齐(例如文件偏移、iovec 构造、mmap 映射),本 PR 未逐项审计;测试验证了文件 size 语义但未覆盖 offset 对齐。2)O_DIRECT 模式仍会拒绝 512 对齐模型,若用户误开 use_page_cache=False 仍启动失败,需要文档提示。3)测试 stub pin_tensor/unpin_tensor 会掩盖真实 host 注册失败,不过该生命周期有专门测试兜底,风险低。4)若后续实现 O_DIRECT padding,需要与 slot 分配和文件布局同步演进,存在兼容性影响。

对用户:使用磁盘 offload + use_page_cache=True 的混合模型(DeepSeek、GLM-5.2/5.3 等)从无法启动变为可用;O_DIRECT 用户行为不变。对系统:仅放宽启动时校验,不改 I/O 语义。对团队:明确了 4096 对齐约束的来源边界,新增参数化回归测试防止未来误伤,并为 O_DIRECT padding follow-up 提供了决策上下文。

page cache 路径需审计 offset/iovec 对齐 测试 stub pin 可能掩盖注册问题 O_DIRECT padding follow-up 未定

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论