训练跑起来之后,最让人沮丧的场景是: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 MB | 1024 | 2 steps/s | 约 307 MB/s |
| 大语言模型预训练(4K token/条) | 约 8 KB | 约 480 条 | 1 step/s | 约 4 MB/s |
| 视频训练(10 秒 1080p 片段) | 约 30 MB | 64 | 0.5 steps/s | 约 960 MB/s |
| 医学影像 3D(512³ 体素) | 约 500 MB | 8 | 0.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,再由后台任务同步到对象存储,避免网络波动影响训练。
六、存储规划与排查步骤清单
- 算需求吞吐:单样本大小 × 全局 batch × 每秒 step 数,再乘 3~5 倍安全系数。
- 估数据集总量:当前数据集大小 × 3~5(含多版本、预处理产物)。
- 选介质:热数据用本地 NVMe,冷数据用对象存储或分布式存储。
- 定容量:本地 NVMe 至少能装下当前数据集的 1.5 倍。
- 选文件系统:本地盘用 XFS 或 ext4,挂载加
noatime。 - 打包数据:把小文件打成 100MB~1GB 的 shard。
- 配内存:留出足够页缓存,让多 epoch 训练能命中缓存。
- 调 DataLoader:设置
num_workers、pin_memory、persistent_workers。 - 压测验证:用 fio 测出真实 IOPS 与带宽,与需求对比。
- 跑起来后诊断:观察 GPU 利用率曲线是否锯齿,用
iostat确认%util是否饱和。 - 持续监控:把磁盘 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 实测和真实训练验证为准。
企业QQ咨询




