多 GPU 训练吞吐也受文件系统制约
文章分析 DataLoader 并发读取与大规模检查点写入如何引发元数据开销、锁竞争和缓存回写,进而影响 GPU 利用率。针对本地 NVMe 临时存储,讨论 XFS 的分配组、区段分配和延迟分配机制。
AI 深度解读
四块 PCIe Gen5 NVMe、八张 H100,训练时 GPU 利用率却在 40% 到 95% 之间波动,问题可能藏在数据进入 GPU 之前。这篇文章把文件系统放回训练流水线里考察:并发读取样本、查找文件元数据,以及写入巨型 checkpoint,都会影响训练节奏。
作者推荐把 XFS 列为本地 NVMe 临时训练盘的候选,但更有价值的结论是:先确认瓶颈,再调整数据组织,最后比较文件系统和参数。原文没有给出 XFS 胜过 ext4 的对照实测,不能把架构分析当成性能承诺。
GPU 吃不饱,为什么要查文件系统
PyTorch 的 torch.utils.data.DataLoader 通过 num_workers 启动多个工作进程。八个训练进程各配 16 个 worker,就是 128 个独立 worker。它们除了读取数据,还会打开和关闭文件、解析路径、查找 inode,以及按挂载配置更新访问时间。
元数据就是文件的管理信息,例如文件在哪里、占用了哪些块。大量 worker 同时访问零散文件时,存储压力可能集中在这些管理操作上,NVMe 的顺序读取带宽未必用得起来。
另一类压力来自 checkpoint。每个 rank,也就是参与分布式训练的进程,可能写入 10 GB 到超过 200 GB 的文件,内容包括 PyTorch state dictionaries、分布式训练或 ZeRO 分区,以及 BF16、FP8 张量。序列化、页缓存回写与样本读取还可能争抢资源。
因此,torchrun 启动后看到 nvidia-smi 利用率波动,只能确定 GPU 工作不连续。开头的场景没有附带完整实验记录,不能据此认定是 ext4 拖慢了 H100。
XFS 的优势来自哪里,又解决不了什么
ext4 使用 JBD2 事务协调元数据更新。大量创建、重命名和更新文件,以及并发分配 inode 和数据块,都可能带来协调成本。但 ext4 并不存在一把把所有操作串起来的全局互斥锁。
XFS 的关键设计是分配组(Allocation Group,AG)。文件系统被划分为多个 AG,各自管理空闲空间、inode 分配等元数据,相关结构通常使用 B+ 树。并发任务有机会分散到不同 AG,减少对共享分配结构的争用。
这里有个前提:实际访问模式得能利用这种分散能力。只读、缓存命中的数据访问,与持续创建文件的负载,受益方式不会相同。XFS 也需要锁,更不能靠增加 AG 数量自动提高吞吐。
大文件写入则涉及 extent 和延迟分配。extent 用一条记录描述一段连续块,能让大文件的块映射更紧凑;缓冲写入时,XFS 可以等到回写阶段再决定物理位置,让分配器掌握更多信息。但磁盘剩余空间、并发写入和既有碎片都会影响结果,连续布局没有保证。
Btrfs、ZFS 的快照、校验和与写时复制(CoW)也有使用价值,不能仅凭写入负载就判定它们不合适。共享块被修改时,额外分配和元数据更新可能增加成本,影响大小还取决于配置。XFS 启用 reflink 后,修改共享 extent 同样会触发 CoW;普通未共享文件的写入不会自动变成 CoW。
比起换文件系统,先减少每个样本的文件操作
原文举了两种数据集:120 万张、每张约 20 KB 的 JPEG,以及数百万个、每个约 15 KB 的 WAV。每读一个样本,都可能经历 open()、元数据查找、read()、close(),带来内核切换和缓存压力。
示例中的 LooseAudioDataset.__getitem__ 每次取路径,再调用 torchaudio.load(path)。这种布局的问题不局限于某个文件系统。
WebDataset 示例把样本放进 TAR 分片,让读取围绕较大的容器展开:
import webdataset as wds
dataset = (
wds.WebDataset(
"/mnt/fast_data/shards/train-{000000..000499}.tar"
)
.decode("torch")
.to_tuple("wav", "cls")
)
关键是 train-{000000..000499}.tar:数据源变成编号 000000 到 000499 的分片。原文没有提供打包和分布式分片配置,这段代码只展示读取入口,不能直接视为完整训练方案。
作者建议从数百 MB 到几 GB 的分片起步,再根据 worker 数量、随机化策略、容量和恢复要求调整。
先减少每个样本的元数据操作,再比较文件系统,通常更值得优先尝试。
这仍是作者的工程建议,原文没有给出收益比例。
调参之前,怎样把瓶颈查清楚
原文提供的格式化示例是:
sudo mkfs.xfs -f \
-d agcount=32 \
-m reflink=1,crc=1 \
-b size=4096 \
/dev/nvme0n1
先看 -f:这是会覆盖目标文件系统的格式化操作。agcount=32 只是示例,作者建议从 mkfs.xfs 默认值开始,有基准证据再指定 AG 数量。reflink=1,crc=1 和 size=4096 也属于这份示例配置。
对应的 /etc/fstab 示例是 UUID=xxxx-xxxx-xxxx-xxxx /mnt/fast_data xfs noatime,nodiratime,logbufs=8,logbsize=256k,allocsize=64M,inode64 0 0。其中 allocsize=64M 控制缓冲写入的默认推测预分配大小,不保证按固定 64M 分块,也不保证 checkpoint 只有几个 extent。
这些参数前面应该有诊断:
- 用
iostat -xz 1 /dev/nvme0n1看rMB/s、wMB/s、平均等待时间await和平均队列长度aqu-sz。%util单独不能证明 NVMe 已经饱和。 - 用
sudo perf top看 CPU 时间究竟花在文件系统、块 I/O、解压、Python 还是数据变换上。内核暴露的符号和事件可能不同。 - 用
xfs_bmap -v /mnt/fast_data/checkpoints/model_epoch_10.pt查看 extent,再结合顺序读写测量判断碎片是否影响性能。
确认方向后,才有理由试参数。先用 cat /sys/block/nvme0n1/queue/scheduler 查看调度器,再考虑 echo none | sudo tee /sys/block/nvme0n1/queue/scheduler。none 是许多现代 NVMe 系统可用的基线。
顺序分片读取可以测试 sudo blockdev --setra 4096 /dev/nvme0n1,即 4096 个扇区,按 512 字节计算通常为 2 MiB。sudo sysctl -w vm.dirty_background_ratio=5 和 sudo sysctl -w vm.dirty_ratio=10 则影响全系统脏页回写及进程限速,无法保证 checkpoint 更快。
我的判断
我认同作者把文件系统视为数据流水线组成部分,也认同先改数据布局、再做参数实验的顺序。它能避免一种常见误判:硬盘标称带宽很高,就默认存储路径没有问题。
但 XFS 的结构适合某类负载,与它在具体机器上更快,是两件需要分别证明的事。原文没有提供内核版本、完整设备拓扑、对照配置和训练步耗时,也没有实际呈现所提到的比较表。四块 NVMe 如何组织,同样没说清。它适合用来形成测试假设,不能用来估算收益。
与继续使用 ext4 相比,换 XFS 需要迁移和验证成本;与改成分片相比,它也未必能消除逐样本开文件的开销。若团队依赖快照、克隆或校验功能,还要把这些需求一起比较。本文讨论的是本地 NVMe 临时训练盘,结论不能直接推广到共享存储或普通业务服务器。
能拿来做什么
- 负责训练平台或 AI 基础设施时,先同时记录训练步耗时、CPU 活动和设备等待情况,再决定是否比较 XFS 与 ext4。
- 遇到数百万 JPEG、WAV 零散文件时,拿一部分数据试做 WebDataset / TAR 分片,验证元数据开销是否下降,并检查随机化与恢复要求。
- 日常 AI 编程中让助手生成运维命令时,要求它逐项解释
agcount、allocsize和脏页参数的作用,并为每次改动指定观测指标。 - 主要做前端或 Node 业务、没有本地多 GPU 训练负载时,不必照搬这些参数;可以直接借用「利用率是结果,定位要看整条流水线」的排查方法。