如何在Python中使用NVIDIA GPUDirect Storage将数据读入GPU内存
NVIDIA 发布 KvikIO Python API 教程,演示如何通过 GPUDirect Storage 将文件直接读入 GPU 内存,跳过 CPU 内存中转。文章详解 KvikIO 与 cuFile 的两级路径选择机制,并介绍使用 gds_stats 工具验证 nvidia-fs 加速路径是否激活的方法。
GPU应用程序正在用日益强大的加速器处理不断增长的数据集。根据2025年斯坦福AI指数报告,知名AI模型的算力大约每五个月翻一番,而用于训练大语言模型的数据集规模大约每八个月翻一番。随着计算量和数据量同步扩张,高效地将数据从存储设备传输出来,成为保证加速器持续获得数据供给的必要条件。
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设置。
GDS是支持在受支持存储和GPU内存之间实现加速数据传输的更广泛技术。对于基于文件系统的存储,cuFile提供了应用程序和框架访问GDS所使用的C库和API。KvikIO位于cuFile之上,提供更高层次的Python和C++接口,以及其自身对I/O请求执行方式的控制能力。图1展示了这些层级及其两个独立的路径选择点,为清晰起见,图中省略了初始化失败、不支持的配置以及错误处理路径。
应用程序提供文件系统路径和GPU缓冲区,KvikIO决定是通过cuFile发送请求,还是使用其POSIX兼容路径(该路径通过主机内存中转数据)。当选择cuFile时,KvikIO会注册该文件,并使用目标GPU缓冲区发出I/O请求。
然后,cuFile根据系统支持情况、文件系统以及自身配置来决定实际的传输路径。它可能使用加速的GDS路径,也可能使用CPU中转的兼容路径。因此,请求到达cuFile API只能确认KvikIO选择了cuFile,但本身并不能确认加速的GDS路径已被激活。
一台配备NVIDIA GPU并已安装GPUDirect Storage的Linux系统。
在存储端点创建、读取和写入文件的权限,以及访问gdscheck和gds_stats等GDS工具的权限。
完整的安装和平台要求,请参阅NVIDIA GPUDirect Storage文档。
一台配备NVIDIA H100 GPU、CUDA 13.3和cuFile 1.18的x86_64系统。
首先,创建一个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
在执行任何I/O操作之前,先检查当前生效的KvikIO设置,并确认系统能够识别GDS组件。在KvikIO的defaults 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运行时设置文档。
$ /usr/local/cuda-13.3/gds/tools/gdscheck.py -p 2>&1 | less -R
输出结果列出了已安装的GDS组件、支持的文件系统、检测到的GPU,以及当前生效的cuFile配置。图2展示了如何检查本教程中使用的KvikIO和GDS配置。
KvikIO设置也可以在Python中修改。例如,defaults.set可以接受一个字典来一次性更改多个设置,或者接受名称和值来更改单个设置:
defaults.set({"task_size": 1024**3, "num_threads": 8}) defaults.set("compat_mode", kvikio.CompatMode.OFF)
接下来,在运行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" ) # 分配足以容纳该文件的GPU缓冲区。 gpu_buffer = cp.empty( file_path.stat().st_size, dtype=cp.uint8, ) # 将文件读入GPU缓冲区。 with kvikio.CuFile(file_path, "r") as file: bytes_read = file.read(gpu_buffer) # 验证是否读取了完整的1 GiB数据。 assert bytes_read == gpu_buffer.nbytes
上下文管理器会打开文件并完成读入GPU内存的操作。尽管返回值可以确认整个文件已被完整读取,但它并不能表明底层使用的数据路径。
验证任何基于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兼容路径处理的操作数量。
KvikIO默认的4 MiB任务大小将1 GiB的请求划分为256次读取操作。每GPU统计数据显示n=256、nvfs=256、posix=0,表明全部256次操作均使用了nvidia-fs路径。
可以通过改变不同的设置来重复上述读取操作,以展示两个控制层级的作用。首先,强制要求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设置。
在KvikIO选择其cuFile路径之后,cuFile会应用另一组控制参数来决定请求的执行方式。这些控制参数在cufile.json中定义,其运作独立于KvikIO运行时设置。
KvikIO和cuFile都可以将一个大型操作分解为更小的部分。KvikIO首先应用KVIKIO_TASK_SIZE,并由KVIKIO_NTHREADS控制这些任务的调度方式。cuFile路径上的每个任务都会成为一个独立的cuFile请求。一旦cuFile收到其中一个请求,其执行设置就会决定该请求能否被再次分解,并使用cuFile内部线程池进行处理。以下cuFile设置控制着这一行为:
此处使用的配置拥有四个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缓冲区(bounce buffer),然后再将数据复制到目标缓冲区中。自GDS 1.17版本起,这些缓冲区可以配置为slab形式。在此配置下,nvidia-fs路径使用的slab不超过16 MiB,因此每个256 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完整接收该请求。
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在GDS所提供的存储加速能力之上,扮演着面向应用层I/O层的更广泛角色。
本教程演示了如何将数据读入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工作负载。