Prhub

#49152 [KV-offload][FS] : Batch store/load_block in C

原始 PR 作者 varun-sundar-rabindranath 合并时间 2026-07-28 11:21 文件变更 4 提交数 3 评论 6 代码增减 +452 / -58

执行摘要

批量 KV 读写操作下沉到 C 层以释放 GIL

PR 指出 Python 线程池争用 GIL 导致磁盘利用率不足,因此将重 I/O 移至 C 层并引入批量操作以增加磁盘吞吐。

值得精读,该 PR 展示了如何将 GIL 敏感的 I/O 操作迁移到 C 层并引入批量处理以提升性能,同时也记录了低并发退化的设计取舍,为后续优化提供了方向。

讨论亮点

Reviewer @orozery 建议简化 fs_io.cpp 中的错误报告机制,移除 IOResult 结构体和 safe_* 包装器,改用返回出错索引的方式,可减少约 70 行代码。作者同意并落实了简化。此外,针对 _HAS_FSIO_C 检查位置,orozery 指出调用方已检查,作者移除了多余检查。

实现拆解

  1. C 扩展实现:在 csrc/fs_io.cpp 中添加 _store_block 和 _load_block 的内联函数,使用 POSIX open/write/read/rename 系统调用,并在 Py_BEGIN_ALLOW_THREADS 块中释放 GIL。新增 batch_store_block 和 batch_load_block 的 Python 绑定函数,接受路径列表和内存块列表。

  2. Python 适配:在 vllm/v1/kv_offload/tiering/fs/io.py 中,尝试导入 C 扩展的 batch_store_block 和 batch_load_block,失败时标记 _HAS_FSIO_C=False。新增 _validate_offsets 函数用于批量边界检查。将原有的 store_block/load_block 重命名为 _store_block/_load_block 作为后备。新增 batch_store_block/batch_load_block 作为统一入口:若 C 扩展可用则调用 C 版本,否则遍历调用 _store_block/_load_block。

  3. 管理器调整:在 vllm/v1/kv_offload/tiering/fs/manager.py 中,将 submit_store/submit_load 从为每个 key 创建独立 functools.partial 任务改为构造一个批量任务,该任务一次性调用 batch_store_block/batch_load_block。这改变了任务粒度:一个 job 的所有 I/O 被分配到同一个线程执行,避免了多个线程之间的额外协调开销。

  4. 测试补充:在 tests/v1/kv_offload/tiering/test_fs_tier.py 中,将 test_store_load_data_integrity 参数化以覆盖不同批次大小(0,1,2,5)以及是否启用 C 扩展。新增对空批次、仅部分命中、以及 _validate_offsets 越界检查的测试。扩大默认块数从 4 到 8 以为测试提供更多空间。

文件 模块 状态 重要度
vllm/v1/kv_offload/tiering/fs/io.py I/O 层 modified 8.42
csrc/fs_io.cpp C 扩展 modified 7.72
tests/v1/kv_offload/tiering/test_fs_tier.py 测试覆盖 modified 7.36
vllm/v1/kv_offload/tiering/fs/manager.py 调度层 modified 6.6

关键符号

batch_store_block batch_load_block _store_block _load_block _validate_offsets

关键源码片段

vllm/v1/kv_offload/tiering/fs/io.py core-logic

核心修改文件:新增 batch_store_block/load_block 入口,实现 C 扩展回退逻辑,增加边界检查。

def batch_store_block(
    paths: list[str],
    view: memoryview,
    offsets: list[int],
    block_size: int,
    use_o_direct: bool = True,
) -> None:
    """在单个函数调用中批量写入多个 KV 块到磁盘。
​
    每个块的源数据是 view[offsets[i]:offsets[i]+block_size],
    目标路径为 paths[i]。写入采用临时文件 + 原子替换保证一致性。
    优先使用 C 扩展(无 GIL),否则回退到 Python 循环。
    """
    # 先验证所有偏移量合法,避免 Python 切片静默截断
    _validate_offsets(view, offsets, block_size)
​
    if _HAS_FSIO_C:
        # C 扩展:一次性传入所有参数,在 C 层释放 GIL 执行 I/O
        view_B = view.cast("B")
        view_slices = [view_B[x: x + block_size] for x in offsets]
        tmp_paths = [p + _get_tmp_suffix() for p in paths]
        return batch_store_block_C(tmp_paths, paths, view_slices, use_o_direct)
    else:
        # 后备:Python 逐块写入
        for path, offset in zip(paths, offsets):
            _store_block(path, view, offset, block_size, use_o_direct)
csrc/fs_io.cpp core-logic

性能提升核心:C 实现 store/load 系统调用,在 GIL 外执行,并支持批量操作。

// Core single-block store: src/size are raw pointer + byte count. Returns 0
// on success, or the errno of the failing step on failure -- captured
// before any subsequent cleanup call can overwrite it. On failure, the temp
// file is removed.
inline int _store_block(const char* tmp_path, const char* dest_path,
                        const char* src, size_t size, bool use_o_direct) {
  // 如果目标已存在则跳过(等价于 Python 的 os.path.exists 检查)
  if (access(dest_path, F_OK) == 0) {
    return 0;
  }
​
  // 创建父目录(等价于 _ensure_dirs)
  if (const int err = ensure_parent_dirs(dest_path); err != 0) {
    return err;
  }
​
  const int o_direct_flag = use_o_direct ? kODirectFlag : 0;
  const int fd = open(
      tmp_path, O_CREAT | O_EXCL | O_WRONLY | O_TRUNC | o_direct_flag, 0644);
  if (fd < 0) {
    return errno;
  }
​
  const ssize_t written = write(fd, src, size);
  if (written < 0 || static_cast<size_t>(written) != size) {
    const int err = written < 0 ? errno : EIO;
    close(fd); // 先关闭 fd
    unlink(tmp_path); // 失败时清理临时文件
    return err;
  }
​
  if (close(fd) != 0) {
    const int err = errno;
    unlink(tmp_path);
    return err;
  }
​
  if (rename(tmp_path, dest_path) != 0) {
    const int err = errno;
    unlink(tmp_path);
    return err;
  }
​
  return 0;
}

评论区精华

简化 fs_io.cpp 的错误报告机制 设计

Reviewer @orozery 建议移除 IOResult 结构和 safe_* 包装器,改为返回出错索引,可节省约 70 行代码。

结论:作者采纳了建议,简化了实现。 · 已解决

冗余的 _HAS_FSIO_C 检查 正确性

在 io.py 的 _store_block 和 batch_store_block 中再次检查 _HAS_FSIO_C,但调用方 manager.py 已经检查过。

结论:作者移除了多余检查,避免重复判断。 · 已解决

风险与影响

C 扩展依赖:若 fs_io_C 扩展因编译失败或平台不支持而无法导入,自动降级为纯 Python 实现,功能正常但无性能提升。批处理语义:单个 job 的所有 I/O 现在由单个线程执行,低并发场景下可能增加延迟(作者已预见到并计划在后续 PR 中在 keys 级别分片)。错误处理:C 扩展通过返回 errno 和异常路径的索引来报告错误,Python 端基于此构建异常,新错误处理路径未经过充分线上验证,可能掩盖某些错误。此外,O_DIRECT 在部分文件系统(如 overlayfs、旧 tmpfs)上可能失败,已在 probe_o_direct 中处理。

用户侧:使用 KV offload 文件系统的用户将看到显著的吞吐量提升(基准测试中吞吐量提升约 30%),尤其在高并发场景(如 64 concurrent 请求)下。系统侧:编译产物新增 fs_io_C 共享库,要求构建环境支持 C++17(使用 std::filesystem)。团队侧:需持续维护 C 扩展代码,包括与 Python 绑定的类型安全。

C 扩展依赖 低并发退化 文件系统兼容性 错误处理透传 O_DIRECT 平台限制

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论