4. 存储系统设计:并行文件系统(Lustre/GPFS)、对象存储(MinIO)、数据缓存策略

存储系统,说白了就是AI训练集群的“粮仓”。

我见过太多团队,GPU算力堆得挺猛,结果一到数据加载就卡成PPT。训练一小时,等数据等半小时——这谁受得了?

今天咱们就聊聊怎么把这个“粮仓”建好。我会从并行文件系统、对象存储,再到数据缓存策略,一层层拆开来讲。

4.1 并行文件系统:Lustre vs GPFS

先说说并行文件系统。这东西是高性能存储的基石。

我个人习惯把并行文件系统理解为“多个仓库管理员同时干活”。传统NAS就一个管理员,忙不过来。并行系统呢,几十上百个管理员一起上,效率自然高。

4.1.1 Lustre:开源界的扛把子

Lustre我用了很多年。它最大的特点是——元数据和数据分离。

什么意思?

你想想看,文件系统里有两类操作:查目录(元数据操作)和读写文件(数据操作)。Lustre把这两件事分开,用专门的元数据服务器(MDS)管目录,用对象存储服务器(OSS)管数据。

我在项目中遇到过一个问题:训练脚本里有个bug,每个step都去stat一下文件。结果MDS直接被打满,整个集群的IO都卡住了。嗯,后来我们加了元数据缓存,才解决这个问题。

Lustre核心组件:

  • MDS (Metadata Server):管理目录结构、文件属性
  • OSS (Object Storage Server):管理实际数据块
  • OST (Object Storage Target):OSS管理的存储设备
  • 客户端:挂载后像本地文件系统一样使用

部署Lustre时,我建议MDS用NVMe SSD,OST用HDD或者混合盘。元数据操作对延迟极其敏感,而数据读写更看重吞吐。

# 一个典型的Lustre挂载命令
mount -t lustre 10.0.0.1@tcp:/lustre /mnt/lustre

# 查看Lustre状态
lfs df -h /mnt/lustre
lfs getstripe /mnt/lustre/training_data

4.1.2 GPFS(现称IBM Storage Scale):商业级的稳定

GPFS我接触得晚一些,但印象很深。它和Lustre最大的区别是——没有独立的元数据服务器。

GPFS把所有节点对等看待,元数据和数据都分布在所有节点上。这种架构的好处是:没有单点故障。坏处是:配置复杂,价格贵。

我记得有一次帮客户调优GPFS,他们训练数据里小文件特别多(几百万个几KB的图片)。GPFS默认的block size是1MB,结果每个小文件都占1MB空间,浪费严重。后来我们把block size改成256KB,空间利用率从30%提升到85%。

特性 Lustre GPFS
架构 元数据/数据分离 全对等分布式
元数据性能 优秀(专用MDS) 良好(分布式)
小文件处理 一般 较好
成本 开源免费 商业授权
运维复杂度 中等 较高

我的选择建议:

预算有限、团队有Linux内核开发能力 → 选Lustre

预算充足、追求稳定、小文件多 → 选GPFS

4.2 对象存储:MinIO的妙用

说完并行文件系统,咱们聊聊对象存储。

你可能会问:训练集群用并行文件系统不就够了?为什么还要对象存储?

嗯,这里有个场景:数据备份、模型checkpoint归档、多集群数据共享。这些场景下,对象存储比文件系统更合适。

MinIO是我用得最多的对象存储方案。它兼容S3 API,部署简单,性能也不错。

4.2.1 MinIO部署要点

MinIO的部署其实很简单,但有几个坑要注意。

# 启动一个4节点的MinIO集群
minio server --address :9000 \
  http://node1/data1 http://node2/data1 \
  http://node3/data1 http://node4/data1

我曾经犯过一个错误:所有MinIO节点用同一块物理磁盘。结果磁盘坏了,整个集群数据全丢。后来我学乖了,每个节点至少两块盘,用纠删码(Erasure Coding)保护数据。

注意:MinIO的纠删码是N/2的冗余。比如4块盘,最多坏2块。但如果你只有2块盘,坏1块就完蛋。所以至少4块盘起步。

4.2.2 对象存储 vs 文件系统:怎么选?

我个人的经验是:

  • 训练数据加载 → 用并行文件系统(Lustre/GPFS)
  • 模型checkpoint保存 → 用对象存储(MinIO)
  • 数据备份和归档 → 用对象存储
  • 多集群共享数据 → 用对象存储

说白了,文件系统适合“热数据”,对象存储适合“温数据”和“冷数据”。

4.3 数据缓存策略:让数据离GPU更近

存储系统再快,也快不过GPU的胃口。这时候就需要缓存策略了。

我见过最夸张的情况:训练一个BERT模型,GPU利用率只有30%,剩下70%的时间都在等数据。加了缓存之后,GPU利用率直接飙到95%。

4.3.1 本地缓存:NVMe的威力

最简单的缓存策略:在每个GPU节点上挂几块NVMe SSD,把数据从远端存储拉到本地。

怎么做?

  • 训练开始前,把数据从Lustre拷贝到本地NVMe
  • 训练过程中,直接从本地读数据
  • 训练结束后,把checkpoint写回远端存储

我建议用rsync或者distcp来做数据同步。但要注意:如果数据量太大(比如几百TB),拷贝时间可能比训练时间还长。这时候就需要“流式缓存”了。

4.3.2 流式缓存:边训练边加载

流式缓存的核心思想是:不一次性加载所有数据,而是按需加载。

举个例子:训练一个图像分类模型,数据集有100万张图片。传统做法是先全部拷贝到本地,再开始训练。流式缓存的做法是:先拷贝前1万张,开始训练;同时后台继续拷贝剩下的数据。

# 一个简单的流式缓存脚本(伪代码)
def streaming_cache(remote_path, local_path, cache_size=10000):
    # 先缓存前10000个文件
    cache_files(remote_path, local_path, count=cache_size)
    
    # 开始训练,同时后台继续缓存
    train_thread = start_training(local_path)
    cache_thread = start_caching(remote_path, local_path)
    
    train_thread.join()
    cache_thread.join()

流式缓存的关键参数:

  • 缓存窗口大小:一次缓存多少数据(建议是epoch大小的10%-20%)
  • 预取线程数:后台缓存用几个线程(建议和CPU核数一致)
  • 淘汰策略:缓存满了怎么办(LRU、FIFO、还是手动管理)

4.3.3 分布式缓存:Alluxio和JuiceFS

如果集群规模大(几十个节点以上),本地缓存的管理就变得很麻烦。这时候可以考虑分布式缓存。

Alluxio是我用得比较多的方案。它把多个节点的内存和SSD组成一个统一的缓存层。数据只需要缓存一次,所有节点都能访问。

JuiceFS是后起之秀,它把对象存储和本地缓存结合起来。数据存在MinIO里,热点数据自动缓存到本地。

我个人的经验:

  • 10个节点以下 → 本地缓存就够了
  • 10-50个节点 → 考虑Alluxio
  • 50个节点以上 → JuiceFS或者自研方案

4.4 实战:一个完整的存储方案

说了这么多,咱们来个实战案例。

假设我们要部署一个32节点的训练集群,每节点8张A100 GPU。训练数据是1TB的图片,模型是ResNet-50。

我的方案:

  1. 并行文件系统:用Lustre,4个MDS(NVMe),16个OSS(HDD+SSD缓存)
  2. 对象存储:用MinIO,8个节点,每节点4块NVMe
  3. 缓存策略:每个GPU节点配2块NVMe SSD(3.2TB),用流式缓存

数据流是这样的:

  • 原始数据存在MinIO里
  • 训练前,数据从MinIO同步到Lustre
  • 训练时,数据从Lustre流式缓存到本地NVMe
  • 模型checkpoint先写本地,再异步同步到Lustre和MinIO

这个方案我实际部署过,效果不错。GPU利用率从65%提升到92%,训练时间缩短了40%。

避坑指南:

我曾经犯过一个错:所有节点同时从Lustre拉数据,结果Lustre网络被打爆。后来加了“错峰加载”策略,每个节点随机延迟1-5分钟再开始拉数据,问题就解决了。

4.5 本章小结

存储系统设计,说白了就是平衡“快”和“大”的矛盾。

并行文件系统解决“大”的问题,对象存储解决“共享”的问题,缓存策略解决“快”的问题。

我个人的建议是:别想着一个方案解决所有问题。分层设计,各司其职,才是正道。

AI训练集群存储系统架构 GPU计算节点(32节点 × 8 A100) 本地NVMe缓存(每节点2×3.2TB) 流式缓存 + 错峰加载 并行文件系统(Lustre) MDS × 4(NVMe) | OSS × 16(HDD + SSD缓存) 元数据与数据分离,高吞吐读写 对象存储(MinIO) 8节点 × 4 NVMe | 纠删码保护 数据备份、模型归档、多集群共享 原始数据源(1TB图片数据集) 数据流方向 ↓ 训练时读取 ↑ Checkpoint写入

可可精品资料库