AI训练服务器如何规划存储?数据集读取、NVMe SSD与吞吐量分析

训练跑起来之后,最让人沮丧的场景是:nvidia-smi 显示 GPU 利用率只有 30%,其余时间卡在那儿不动。查 CPU 也没跑满,网络也没瓶颈——最后发现是数据加载慢,GPU 在等硬盘。

买了一堆 H100 却让它们空转,是 AI 训练里最昂贵的浪费之一。而存储规划恰恰是最容易被忽视的环节:采购时注意力都在显卡上,存储往往按"够放就行"来配。本文从吞吐量需求出发,讲清楚训练服务器的存储该怎么规划、怎么测算、怎么诊断瓶颈。

一、先算清楚:训练到底需要多少读取吞吐

1.1 需求的来源

训练过程中,每个 step 都要从数据集取一个 batch。所需的持续读取吞吐为:

所需吞吐 (MB/s) = 单样本平均大小 (MB) × 全局 batch size × 每秒迭代次数 (steps/s)

其中:全局 batch size = 单卡 batch × GPU 数量 × 梯度累积步数

举两个典型例子:

场景单样本大小全局 batch迭代速度所需持续吞吐
图像分类(224×224,约 150KB/张)0.15 MB10242 steps/s约 307 MB/s
大语言模型预训练(4K token/条)约 8 KB约 480 条1 step/s约 4 MB/s
视频训练(10 秒 1080p 片段)约 30 MB640.5 steps/s约 960 MB/s
医学影像 3D(512³ 体素)约 500 MB80.2 steps/s约 800 MB/s

可以看出规律:大语言模型预训练对吞吐要求反而不高(因为文本压缩率高),而图像、视频、医学影像类任务对存储吞吐要求很高。这就是为什么不能照搬别人的存储方案。

1.2 真实需求还要乘上放大系数

上面的数字只是理论下限,实际规划时要考虑:

  • 数据增强(Data Augmentation):随机裁剪、翻转、色彩抖动等都在 CPU 上做,会消耗额外算力,进而要求更多的数据预读线程和缓存。
  • 随机访问:训练要求数据打乱(shuffle),读取是随机而非顺序的,随机 IO 性能远低于顺序读写,尤其对机械盘是灾难。
  • 多个 epoch:数据集会被反复读取多次,因此能被系统页缓存(page cache)命中的数据可以省去磁盘 IO。数据集小于内存的 50% 时,第二 epoch 之后基本可以全部命中缓存。
  • 多机多卡:如果多台机器共享同一个网络存储,所有节点的读取压力会叠加到存储后端。

工程上的经验:在理论吞吐基础上乘 3~5 倍作为存储系统的设计目标,并给系统内存留出足够的页缓存空间。

二、存储介质怎么选

2.1 三种介质的性能量级

介质顺序读吞吐随机读 IOPS 量级容量成本量级训练场景定位
HDD 机械盘(7200rpm)约 150~250 MB/s约 100~200最低仅冷备份,不适合训练
SATA SSD约 500~550 MB/s数万低入门,小规模数据集
NVMe SSD(PCIe 4.0)约 3~7 GB/s数十万到百万中训练主力,本地热数据
NVMe SSD(PCIe 5.0)约 10~14 GB/s百万级较高超大吞吐需求
分布式存储(Ceph/Lustre,全闪)聚合可达数十 GB/s取决于规模最高多机共享、超大数据集

关键判断:机械盘完全不适合训练。随机读取时,机械盘每次寻道约 10ms,IOPS 只有一两百,几百 MB/s 的理论吞吐在随机访问下会掉到零头。即便做 RAID 0 也只是缓解,不能解决根本问题。

2.2 分层架构:热数据和冷数据分开

合理的训练存储架构是分层的:

  • 热数据层(本地 NVMe):当前训练任务正在用的数据集。容量需求 = 当前数据集大小 × 1.5(留出预处理产物空间)。
  • 温数据层(分布式/网络存储):全部数据集、模型 checkpoint、训练日志。容量通常是热数据层的 5~10 倍。
  • 冷数据层(对象存储 / 磁带):历史数据集、归档 checkpoint。成本最低,读取慢。

工作流是:训练前把数据集从温层预取(prefetch)到热层,训练过程中只访问本地 NVMe,训练结束把 checkpoint 写回温层。这样既保证了训练速度,又不需要给每台机器配超大本地盘。

三、文件系统与数据格式

3.1 文件系统的选择

  • ext4:最通用,稳定可靠,适合大多数场景。注意挂载时可以加 noatime 减少不必要的元数据写入。
  • XFS:大文件和高并发读写场景表现更好,很多 AI 训练集群用它做数据盘。
  • NFS:方便共享,但要注意版本(NFSv4 优于 v3)和挂载参数。多机同时读一个大文件时,NFS 的单连接吞吐可能成为瓶颈。
  • Lustre / GPFS / Weka:高性能并行文件系统,多机大规模训练的标准方案,但部署和运维复杂度高。
# 挂载选项建议(noatime 减少元数据写,nodiratime 同理)
/dev/nvme0n1p1  /data  xfs  defaults,noatime,nodiratime  0  0

# 查看当前挂载参数
mount | grep data

# 检查文件系统类型与剩余空间
df -Th /data

# NVMe 健康检查(关注 percentage_used 与 available_spare)
sudo nvme smart-log /dev/nvme0n1 | grep -E "percentage_used|available_spare|temperature|critical_warning"

3.2 数据格式:别用海量小文件

这是训练存储最经典的性能陷阱。一百万张 100KB 的图片,如果用一百万个独立小文件存储,训练时的读取开销会极其惊人——每次打开文件的系统调用、元数据查找、磁盘寻道,加起来远超实际数据传输时间。而且小文件会严重浪费 inode 和磁盘块。

解决方案是把小文件打包成大文件:

方案原理适用框架优点
TFRecord把样本序列化打包成大文件TensorFlow顺序读,支持流式解析
WebDataset基于 tar 分片,按 shard 流式读取PyTorch纯 tar 格式,无需专用库也能查看
LMDB / MDB内存映射键值数据库通用随机访问快,零拷贝
HDF5层级数据格式科学计算常用支持分块与压缩
RecordIO / MXNet rec打包格式MXNet紧凑,读取快
Arrow / Parquet列式存储表格类数据压缩率高,支持谓词下推

打包时的经验值:每个 shard 大小控制在 100MB~1GB 之间。太小则文件数量仍然很多,太大则不便于分布式切分和断点续传。

四、诊断:怎么确认瓶颈在存储

4.1 看 GPU 利用率曲线

# 每秒采样 GPU 利用率,观察是否有规律性掉零
nvidia-smi --query-gpu=timestamp,utilization.gpu,utilization.memory,memory.used \
  --format=csv -l 1 -f /tmp/gpu_util.csv

# 或直接用 dmon
nvidia-smi dmon -s um -d 1 -c 300

判断标准:如果 GPU 利用率呈现周期性锯齿——冲高到 90% 以上后掉到接近 0,再冲高——那几乎可以肯定是数据加载跟不上。掉零的那段时间就是 GPU 在等下一批数据。反之如果利用率平稳维持在低位(比如 20%),则更可能是 batch size 太小或模型本身太小。

4.2 看磁盘 IO

# 查看各磁盘的吞吐与繁忙度(%util 接近 100% 说明磁盘已饱和)
iostat -x 1 10

# 只看 NVMe 设备
iostat -x -d /dev/nvme0n1 1 10

# 查看进程级 IO
sudo iotop -o -P

# 查看内存与换页情况(si/so 持续大于 0 说明在换页)
vmstat 1 10
free -g

关键指标解读:

  • %util(或 %busy)持续接近 100%:磁盘已饱和,是明确的 IO 瓶颈证据。
  • await(IO 等待时间)明显偏高:NVMe 上正常应低于 1ms,超过 5ms 说明有排队或磁盘本身慢。
  • r_await 高但 %util 不高:可能是随机访问太多,或者网络存储延迟高。
  • si/so 持续大于 0:正在换页,说明内存不足,页缓存被挤掉了,会连锁放大磁盘 IO。

4.3 测一下磁盘真实速度

# 顺序写测试(oflag=direct 绕过页缓存,测真实磁盘速度)
dd if=/dev/zero of=/data/testfile bs=1M count=4096 oflag=direct

# 清页缓存后测顺序读
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches
dd if=/data/testfile of=/dev/null bs=1M iflag=direct

# 用 fio 做更贴近真实的测试(随机读,4K 块,16 并发,libaio)
fio --name=randread --ioengine=libaio --direct=1 \
    --rw=randread --bs=4k --numjobs=16 --iodepth=32 \
    --size=2G --runtime=60 --time_based --group_reporting \
    --filename=/data/fiotest

# 测完记得删掉测试文件
rm -f /data/testfile /data/fiotest

把 fio 测出的 IOPS 和带宽,与前面算出的需求做对比,就能定量判断存储是否够用。

五、优化:让 GPU 吃饱的七个手段

5.1 数据加载器调优

# PyTorch DataLoader 关键参数
loader = DataLoader(
    dataset,
    batch_size=64,
    num_workers=8,          # 建议设为 CPU 物理核数的 1/2 到 1/4,每个 worker 是独立进程
    pin_memory=True,        # 锁页内存,加速 CPU→GPU 拷贝
    prefetch_factor=4,      # 每个 worker 预取 batch 数
    persistent_workers=True,# 跨 epoch 复用 worker,避免反复启停
    shuffle=True,
    drop_last=True
)

参数经验值:

  • num_workers:不是越大越好。过多会导致进程切换开销和内存暴涨。通常从 CPU核数 / 4 开始试,逐步调优。
  • pin_memory=True:几乎总是应该开启,它把数据放在锁页内存中,让 CPU 到 GPU 的传输可以用 DMA,能明显加速。
  • persistent_workers=True:避免每个 epoch 重新 fork 进程,长训练任务收益明显。
  • 内存不足是常见限制:每个 worker 都会复制一份数据集对象。如果 num_workers=8 且数据集对象较大,内存占用会成倍增长。

5.2 其他有效手段

手段做法效果
打包成大文件TFRecord / WebDataset / LMDB消除海量小文件开销,通常是最有效的单一优化
数据预取到本地训练前 rsync 到本机 NVMe彻底摆脱网络存储的不确定性
用 /dev/shm把小数据集放进共享内存读写速度接近内存带宽,零磁盘 IO
预解码/预处理缓存提前把增强结果算好存盘把 CPU 压力转移到离线阶段
调大页缓存保证系统内存充足多 epoch 训练时数据集可命中缓存
异步拷贝non_blocking=True 配合 pin_memory让数据传输与计算重叠
关闭 atime挂载参数加 noatime减少每次读取的元数据写入
# 把数据集放到共享内存(适合小于内存 50% 的数据集)
df -h /dev/shm
cp -r /data/dataset_small /dev/shm/

# 训练前把远程数据同步到本地 NVMe
rsync -av --progress user@storage:/datasets/imagenet/ /data/imagenet/

5.3 checkpoint 写入也要考虑

训练中定期保存 checkpoint 是必须的,但大模型 checkpoint 很大(70B 模型约 140GB),频繁写盘会造成明显的训练停顿。建议:

  • 降低保存频率:按 step 数或时间间隔保存,而不是每个 epoch 都存。
  • 异步保存:把保存操作放到独立线程或进程,让 GPU 不必等待。
  • 只存增量:如 LoRA 微调只存适配器权重(通常几十 MB),不要存全量模型。
  • 本地存 + 异步上传:先写到本地 NVMe,再由后台任务同步到对象存储,避免网络波动影响训练。

六、存储规划与排查步骤清单

  1. 算需求吞吐:单样本大小 × 全局 batch × 每秒 step 数,再乘 3~5 倍安全系数。
  2. 估数据集总量:当前数据集大小 × 3~5(含多版本、预处理产物)。
  3. 选介质:热数据用本地 NVMe,冷数据用对象存储或分布式存储。
  4. 定容量:本地 NVMe 至少能装下当前数据集的 1.5 倍。
  5. 选文件系统:本地盘用 XFS 或 ext4,挂载加 noatime。
  6. 打包数据:把小文件打成 100MB~1GB 的 shard。
  7. 配内存:留出足够页缓存,让多 epoch 训练能命中缓存。
  8. 调 DataLoader:设置 num_workers、pin_memory、persistent_workers。
  9. 压测验证:用 fio 测出真实 IOPS 与带宽,与需求对比。
  10. 跑起来后诊断:观察 GPU 利用率曲线是否锯齿,用 iostat 确认 %util 是否饱和。
  11. 持续监控:把磁盘 IO、使用率、NVMe 健康度纳入监控告警。

七、常见坑

  • 用机械盘或小文件直接训练:这是最典型的性能杀手。
  • num_workers 设得过大:导致内存暴涨、CPU 上下文切换开销反超收益。
  • 多机共享一个 NFS 且数据量大:所有节点同时读,NFS 服务端瞬间成为瓶颈。应改为各节点本地缓存。
  • 忽略 checkpoint 写入开销:大模型保存时训练会停顿几十秒甚至几分钟。
  • 磁盘写满导致训练崩溃:df -h 显示还有空间但 inode 用尽(小文件场景),要用 df -i 检查。
  • NVMe 长时间高负载掉速:消费级 NVMe 在持续写入后 SLC 缓存耗尽会明显掉速,数据中心场景应选用企业级盘。
  • 不做磁盘健康监控:NVMe 有写入寿命限制,用 nvme smart-log 的 percentage_used 提前预警。
# 检查 inode 使用情况(小文件场景必查)
df -i /data

# 统计某个目录下文件数量,判断是否小文件过多
find /data/dataset -type f | wc -l

# 查看 NVMe 寿命与温度
sudo nvme smart-log /dev/nvme0n1 | grep -E "percentage_used|available_spare|critical_warning|temperature"

小结

AI 训练服务器的存储规划,核心逻辑是从吞吐量需求倒推,而不是从容量需求倒推。先用"单样本大小 × 全局 batch × 迭代速度"算出需要多少 MB/s,再乘上 3~5 倍安全系数去选介质和配置。

三个最有效的优化手段依次是:把海量小文件打包成大 shard(收益最大,也最常被忽略);训练前把数据预取到本地 NVMe;正确配置 DataLoader 的 num_workers 和 pin_memory。

诊断方法也要记住:GPU 利用率曲线呈现周期性锯齿掉零,是数据加载瓶颈的典型特征;iostat 里 %util 持续接近 100% 是磁盘饱和的直接证据。训练开始后花五分钟做一次这样的检查,往往能发现被浪费掉的大量算力——毕竟,让 GPU 空等硬盘,是 AI 训练里最贵的浪费。


本文给出的吞吐计算公式与参数经验值为工程近似方法,实际性能受硬件型号、文件系统、框架版本与数据特征影响,建议以 fio 实测和真实训练验证为准。