返回 文章 apply CMS 文章

使用 NVIDIA KvikIO 将数据直接读入 GPU 内存:GDS 实践教程

通过 KvikIO 在 Python 中实现 GDS 加速的 GPU 直接存储读取,并验证数据路径。

NVIDIAGPUDirect StorageKvikIOPython
成长分 / 100 73 综合收获、行动、留存与影响

使用 NVIDIA KvikIO 将数据直接读入 GPU 内存:GDS 实践教程
为什么值得读了解如何绕过 CPU 内存暂存,使用 GDS 直接读取存储到 GPU 内存,提升数据供给效率。

学习使用 KvikIO Python API 进行高性能文件 I/O 的完整步骤。

关键洞察
  1. KvikIO 提供 Python 和 C++ 高级接口,位于 cuFile 之上,简化 GDS 的使用。
  2. KvikIO 的兼容模式(compat_mode)和 GDS 阈值(gds_threshold)控制是否使用 cuFile 路径。
  3. cuFile 根据系统配置决定使用加速的 GDS 路径还是 CPU 暂存的兼容路径。
转成行动

深入阅读

正文与原文对照

原文保真覆盖:全文原文字符:16462

GPU 应用正在使用日益强大的加速器处理不断增长的数据集。根据 2025 年斯坦福 AI 指数,知名 AI 模型的计算量大约每五个月翻一番,而用于训练 LLM 的数据集规模大约每八个月翻一番。随着计算与数据规模同步增长,高效地从存储移动数据已成为为加速器持续供数的必要条件。

NVIDIA Magnum IO GPUDirect Storage(GDS) 通过在受支持的存储与 GPU 内存之间建立直接数据路径来应对这一挑战。这避免了经由 CPU 内存进行不必要的暂存,并降低了系统中的整体 CPU 利用率。GDS 受到不断壮大的生态系统的支持,其中包括许多主要存储厂商。

在应用层,Python 在现代 AI 开发中扮演着核心角色。截至 2025 年 8 月,GitHub 上近一半的新 AI 项目主要使用 Python 构建,而 GDS 的主要应用接口是 cuFile C API。

NVIDIA KvikIO 是一个用于高性能文件 I/O 的 Python 和 C++ 库,它弥合了这一差距。它提供高层、符合语言习惯的 Python 和 C++ 接口,并额外提供对 I/O 请求执行方式的控制。

本文中的教程演示如何使用 NVIDIA KvikIO Python API 配置存储 I/O、将数据读入 GPU 内存,并确认 GDS 内的加速路径已被激活。它还展示了如何检查影响每个操作所走路径的 KvikIO 和 cuFile 设置。

NVIDIA KvikIO 如何利用 GDS?

GDS 是更广泛的技术,可在受支持的存储与 GPU 内存之间实现加速的数据移动。对于基于文件系统的存储,cuFile 提供 C 库和 API,应用程序和框架通过它们访问 GDS。KvikIO 位于 cuFile 之上,提供更高层的 Python 和 C++ 接口,并自带对 I/O 请求执行方式的控制。图 1 展示了这些层次及其两个独立的路径选择点。为清晰起见,初始化失败、不受支持的配置和错误处理路径均已省略。

比较 KvikIO 经由主机内存的 CPU 暂存路径与从存储直接到 GPU 内存的 GDS 路径的示意图。
图 1. KvikIO 读取的成功执行流程

应用程序提供文件系统路径和 GPU 缓冲区,KvikIO 决定是通过 cuFile 发送请求,还是使用其 POSIX 兼容路径(该路径经由主机内存暂存数据)。当选择 cuFile 时,KvikIO 注册文件并使用目标 GPU 缓冲区发起 I/O。

随后 cuFile 根据系统支持、文件系统及其配置确定实际传输路径。它可能使用加速的 GDS 路径,也可能使用其 CPU 暂存的兼容路径。因此,到达 cuFile API 只能确认 KvikIO 选择了 cuFile,但本身并不能确认加速的 GDS 路径已被激活。

以下各节将逐步介绍如何配置系统、执行 KvikIO 读取,并验证最终所走的路径。

先决条件

在开始本教程之前,你需要:

  • 一台安装了 NVIDIA GPU 和 GPUDirect Storage 的 Linux 系统。
  • 受 GDS 支持的存储和文件系统配置。
  • 在存储端点创建、读取和写入文件的权限,以及对 gdscheck 和 gds_stats 等 GDS 工具的访问权限。

有关完整的安装和平台要求,请参阅 NVIDIA GPUDirect Storage 文档。

教程系统

本教程使用以下配置进行测试:

  • 一台配备 NVIDIA H100 GPU、CUDA 13.3 和 cuFile 1.18 的 x86_64 系统。
  • 一个使用 ext4 格式化的本地 NVMe 设备。
  • 所选 GPU 与 NVMe 设备之间为 PIX 连接,即路径最多经过一个 PCIe 桥。
  • 禁用 IOMMU 和 ACS,以保留 PCIe 点对点传输。
  • 加载 nvidia-fs 内核模块以提供加速路径。

你可以使用以下命令检查 GPU 到存储的拓扑:

$ nvidia-smi topo -nvme

步骤 1:设置 NVIDIA KvikIO

首先,创建一个 Python 环境并安装 CUDA 13 版本的 KvikIO。CuPy 将作为依赖项安装。

$ python3 -m venv kvikio-env
$ source kvikio-env/bin/activate
$ python -m pip install kvikio-cu13

你可以使用以下命令验证安装:

$ python -m pip show kvikio-cu13 cupy-cuda13x

步骤 2:检查配置

在执行任何 I/O 之前,检查当前生效的 KvikIO 设置,并确认系统识别到 GDS 组件。在 KvikIO 默认值 API 中,兼容模式设置名为 compat_mode,其 name 属性报告当前生效的模式为 AUTO、ON 或 OFF。以下命令报告已加载的 KvikIO 和 cuFile 版本、当前生效的兼容模式,以及 KvikIO 发送给 cuFile 的最小请求大小:

import kvikio
import kvikio.cufile_driver as cufile_driver
import kvikio.defaults as defaults

kvikio.__version__
cufile_driver.libcufile_version()
defaults.get("compat_mode").name
defaults.get("gds_threshold")

在教程系统上,KvikIO 报告的兼容模式为 AUTO,GDS 阈值为 16 KiB。AUTO 指示 KvikIO 尝试使用 cuFile,并在 cuFile 不可用时回退到其 POSIX 后端。小于该阈值的读取直接使用 KvikIO POSIX 路径。这些设置也可以通过 KVIKIO_COMPAT_MODE 和 KVIKIO_GDS_THRESHOLD 控制。完整列表请参阅 KvikIO 运行时设置。

接下来,检查 GDS 平台和 cuFile 配置:

$ /usr/local/cuda-13.3/gds/tools/gdscheck.py -p 2>&1 | less -R

输出列出了已安装的 GDS 组件、受支持的文件系统、检测到的 GPU 以及当前生效的 cuFile 配置。图 2 展示了如何检查本教程中使用的 KvikIO 和 GDS 配置。

动画终端会话检查 KvikIO 和 cuFile 版本、兼容模式及 GDS 阈值,然后运行 GDS 诊断。
图 2. 终端会话检查 KvikIO 版本、cuFile 版本、兼容模式和 GDS 阈值,然后运行 GDS 诊断

KvikIO 设置也可以在 Python 中更改。例如,defaults.set 接受一个字典以同时更改多个设置,或接受一个名称和值以更改单个设置:

defaults.set({"task_size": 1024**3, "num_threads": 8})
defaults.set("compat_mode", kvikio.CompatMode.OFF)

步骤 3:将文件读入 GPU 内存

接下来,在运行 Python 示例之前先创建一次输入文件。以下命令使用直接 I/O 写入一个包含随机数据的 1 GiB 文件,并将其刷新到存储:

$ dd if=/dev/urandom \
    of=/mnt/nvme1/kvikio-tutorial/kvikio-example.bin \
    bs=1M count=1024 \
    oflag=direct conv=fsync \
    status=progress

以下程序在 GPU 内存中分配一个 1 GiB 的 CuPy 数组,并将其作为目标缓冲区提供给 KvikIO:

from pathlib import Path
import cupy as cp
import kvikio

file_path = Path(
    "/mnt/nvme1/kvikio-tutorial/kvikio-example.bin"
)

# Allocate a GPU buffer large enough to hold the file.
gpu_buffer = cp.empty(
    file_path.stat().st_size,
    dtype=cp.uint8,
)

# Read the file into the GPU buffer.
with kvikio.CuFile(file_path, "r") as file:
    bytes_read = file.read(gpu_buffer)

# Validate that 1 GiB was read.
assert bytes_read == gpu_buffer.nbytes

上下文管理器打开文件并完成读入 GPU 内存的操作。尽管返回值确认整个文件已被读取,但它并不标识底层的数据路径。

步骤 4:确定数据路径

验证任何基于 cuFile 的应用程序所用数据路径的一种方法是观察 cuFile 运行时的每进程统计信息。可以通过设置 CUFILE_STATS_LEVEL=3 来启用这些统计信息,然后在进程保持活动状态时用 gds_stats 进行检查。

对于这个 KvikIO 示例,环境变量将该设置的作用范围限定为单次运行,不过它也可以在 cufile.json 中配置。

$ CUFILE_STATS_LEVEL=3 python3 -i read_with_kvikio.py

以交互方式运行 Python 可在读取后保持进程存活,从而允许 gds_stats 检查其累积的计数器。

图 3 演示了这一工作流程。在每 GPU 输出中,nvfs 统计代表经由 nvidia-fs 内核模块所走路径的操作数,而 posix 统计由 cuFile 兼容路径处理的操作数。

动画终端会话启用 cuFile 统计信息并运行一次 KvikIO 读取;结果显示 256 次 nvfs 读取,总计 1024 MiB。
图 3. 一次 KvikIO 读取的 cuFile 统计信息终端会话,显示 256 次 nvfs 读取,总计 1,024 MiB

KvikIO 默认的 4 MiB 任务大小将 1 GiB 请求划分为 256 次读取操作。每 GPU 统计信息报告 n=256、nvfs=256 和 posix=0,表明全部 256 次操作都使用了 nvidia-fs 路径。

步骤 5:比较可用路径

可以用不同的设置重复相同的读取,以演示这两个控制层。首先,要求 KvikIO 使用 cuFile:

$ CUFILE_STATS_LEVEL=3 \
    KVIKIO_COMPAT_MODE=off \
    python3 -i read_with_kvikio.py

在教程系统上,gds_stats 报告 nvfs 操作,确认 cuFile 选择了加速路径。接下来,强制 cuFile 使用其兼容路径:

$ CUFILE_STATS_LEVEL=3 \
    KVIKIO_COMPAT_MODE=off \
    CUFILE_FORCE_COMPAT_MODE=true \
    python3 -i read_with_kvikio.py

KvikIO 仍然调用 cuFile,但操作现在出现在 posix 下而不是 nvfs 下。最后,KvikIO 兼容模式完全绕过 cuFile:

$ KVIKIO_COMPAT_MODE=on \
    python3 read_with_kvikio.py

在这种情况下,KvikIO 通过其 CPU 暂存的 POSIX 后端执行读取,因此 cuFile 不会记录该操作。下一节将考察控制请求执行的 cuFile 设置。

步骤 6:KvikIO 读取的 cuFile 配置

在 KvikIO 选择其 cuFile 路径后,cuFile 会应用另一组控制来决定请求如何执行。这些控制定义在 cufile.json 中,并独立于 KvikIO 运行时设置运行。

请求分解与并行执行

KvikIO 和 cuFile 都可以将大型操作划分为更小的片段。KvikIO 首先应用 KVIKIO_TASK_SIZE,由 KVIKIO_NTHREADS 控制这些任务如何调度。cuFile 路径上的每个任务都会成为单独的 cuFile 请求。一旦 cuFile 收到其中一个请求,其执行设置就决定了该请求是否可以再次划分并使用 cuFile 内部线程池处理。以下 cuFile 设置控制此行为:

表 1. 影响 KvikIO 请求处理方式的 cuFile 执行设置

设置 用途
execution.parallel_io 启用 cuFile 内部并行处理。
execution.max_io_threads 限制每个 GPU 队列可用的 cuFile 工作线程数。
execution.min_io_threshold_size_kb 为内部调度的片段设置目标最小大小;最后的剩余部分可能更小。
execution.max_request_parallelism 限制 cuFile 为单个请求准备的并行度。
execution.max_io_queue_depth 限制内部队列中待处理工作项的数量。

一次 1 GiB 读取的分步过程

此处使用的配置有四个 cuFile 工作线程,max_request_parallelism 设为 4,min_io_threshold_size_kb 设为 8192(8 MiB)。在 KvikIO 默认 4 MiB 任务大小下,一次 1 GiB 读取会变成 256 个 cuFile 请求。每个 4 MiB 请求由 cuFile 作为单个片段处理。这就是在先前统计信息中产生 nvfs=256 的那次运行。

如果将 KVIKIO_TASK_SIZE 设为 1 GiB,KvikIO 会改为向 cuFile 发送一个大型读取请求。CuPy 目标缓冲区未显式注册到 cuFile,因此 cuFile 使用其未注册缓冲区调度路径。它将请求限制为 min(4 个工作线程,4 个最大请求并行度)= 4 个片段。

将 1 GiB 分成四份,每份为 256 MiB,已经大于 min_io_threshold_size_kb 的 8 MiB。由于队列中有空间,cuFile 将三个片段分配给工作线程,在调用线程上处理第四个片段,并等待它们完成。

这些 256 MiB 片段是调度工作单元,而不是单独的存储传输。由于目标缓冲区未注册用于存储 DMA,cuFile 会读入已注册的 GPU 反弹缓冲区,再将数据复制到目标缓冲区。自 GDS 1.17 起,这些反弹缓冲区可以配置为 slab。此配置中的 nvidia-fs 路径使用不超过 16 MiB 的 slab,因此每个 256 MiB 片段都通过更小的传输来处理。8 MiB 调度目标并不决定它们的传输大小。

8 MiB 拆分目标何时起作用

在此配置中,execution.min_io_threshold_size_kb 设为 8192(8 MiB)。该目标不会改变 1 GiB 的拆分,但会影响 16 MiB 的 cuFile 请求。四个等分片段将各为 4 MiB,因此 cuFile 改为调度两个 8 MiB 片段。KvikIO 必须使用至少 16 MiB 的任务大小,cuFile 才能完整收到该请求。

NVIDIA CUDA-X for data science 中的 KvikIO

KvikIO 是 NVIDIA CUDA-X 数据科学库(原 RAPIDS)的一部分。其开发与最初为 NVIDIA cuDF GPU DataFrame 库构建的 GDS 集成密切相关,在该库中,并行 cuFile 读取加速了 Parquet 和 ORC 数据摄取。这一设计后来被纳入 KvikIO,建立在 cuDF 最初的 GDS 数据摄取工作 之上。

此后,KvikIO 已与 GDS 一起用于 cuCIM 数字病理工作流程 和 用于科学图像分析的 cuPhoton xDataReader,而相关的 GPU 加速 Presto 工作 则展示了 GDS 在数据分析工作负载中的优势。这些示例共同展示了 KvikIO 作为面向应用的 I/O 层,在 GDS 提供的存储加速之上所发挥的更广泛作用。

开始使用 NVIDIA KvikIO

本教程演示了如何将数据读入 GPU 内存,并使用三级 GDS cuFile 统计信息来区分加速的 nvidia-fs 路径与 CPU 暂存的兼容路径。这些路径决策对于诸如 cuDF 等使用 KvikIO 执行许多 I/O 操作的流行库 而言至关重要。

关键要点是 KvikIO 和 cuFile 会做出各自独立的决策。KvikIO 选择是否调用 cuFile,而 cuFile 决定最终的传输路径和执行策略。理解这些层次使应用程序能够了解 I/O 所走的路径,并调整对其工作负载至关重要的设置。

准备好开始了吗?在启用了 GDS 的系统上运行本文中介绍的 KvikIO Python 示例。如需更多示例,请浏览 MagnumIO GDS 示例目录 中的 C++ 和 Python KvikIO 代码。使用 gds_stats 检查读取是否出现在 nvfs 或 posix 下,然后将相同的验证方法应用于您自己的 KvikIO 工作负载。