
做过 AI 训练基础架构之后你会发现一个很反直觉的现象最折磨人的往往不是 GPU 利用率上不去而是数据路径上某一个你平时根本不会关注的环节突然崩了。训练脚本里定期调一次torch.save或框架的 checkpoint 接口几十个节点同时把几十 GB 的权重写回共享存储数据管线清洗完一轮小文件瞬间又是成千上万的元数据请求。如果底层文件系统撑不住这种“平时稳态、峰值暴增”的混合压力前面的算力投入都会被卡在 I/O 上。最近我详细拆了 PFS L3 这套并行文件存储技术架构的设计逻辑。它不是一个普通文件系统换个名字而是把数据入口、中间缓存、持久化存储串成一个三级体系专门适配 AI 生产负载。这篇文章我会沿着架构分层、数据布局、检查点写入路径、部署观测这些维度去讲重点放在“为什么要这样设计”以及“实际落地时容易忽略什么”。如果你正在给训练集群选存储、调存储或者在排查 checkpoint 写入慢、小文件加载慢这些问题这篇文章应该能帮上忙。1. AI 生产负载为何要把文件系统逼到墙角很多人一开始觉得AI 训练对存储的需求无非就是“大”容量够大就行。真正跑起来才发现AI 生产负载的流量模型非常分裂既有大文件的顺序读写又有大量小文件的随机访问不同阶段对延迟和吞吐的要求也完全不同。1.1 高吞吐与低时延并存存储不能只偏科训练任务启动后第一个高峰就是数据加载。图片、视频帧、文本序列经过预处理后会切成很多小 batchDataLoader 不断从存储里拉数据到 GPU。这个过程里带宽很重要但更致命的是时延。一个数据集有几万个小文件每个文件只有几百 KB文件系统如果每个请求都要经过一次高延迟元数据查询那么瓶颈立刻会从网卡转移到存储服务端。传统 HPC 场景里常见的方法是按大文件顺序读文件系统天然擅长这一点但 AI 数据管线的读取模式更像“高并发随机读”每个 worker 读各自的文件这对目录索引、inode 缓存、元数据服务的并发能力提出了完全不同的要求。推理服务还会放大这种压力。线上推理不像训练那样可以攒一个大 batch它需要频繁拉取模型分片和 embedding 向量。延迟一旦超过几十毫秒SLA 就受影响。所以 AI 场景的文件系统必须同时具备两副面孔对 checkpoint 这种大对象要提供几十 GB/s 的聚合带宽对 embedding 和样本数据要提供稳定低延迟。单一调优思路很难同时满足两个方向。1.2 checkpoint 写入模式与传统文件系统冲突在哪checkpoint 是训练存储里最特殊的流量。模型规模越大checkpoint 文件越大而且它不是单个节点写单个文件而是多个加速卡、多个节点同时往同一个共享文件或同一组文件里写分片。这个模式有两个问题第一文件系统要处理“多客户端并发写同一文件”的锁语义。传统 POSIX 语义要求每一个写操作在字节范围上保持一致服务器端要进行复杂的锁仲裁。Lustre 这类系统用分布式锁管理来协调但当几十个进程同时扩展写一个大文件时锁的竞争会变成热点。第二checkpoint 的写入是周期性突发的训练过程大部分时间都在读数据突然某分钟写入需求飙到峰值。如果系统按峰值时刻预留带宽和缓存平时就是浪费如果不预留峰值一来就会反压训练任务严重的话直接把训练阻塞住。我在实测中见过一种典型情况存储的基准带宽测出来很高大概是理论峰值的 80% 以上结果真正做 64 卡 checkpoint 时聚合写入速度连一半都不到。原因就在于多客户端同时写一个文件时数据分布的同步策略出了问题所有分片都挤到同一批存储节点上条带化形同虚设。1.3 小文件与元数据风暴也是 AI 模型训练的隐形炸弹很多人只关心 checkpoint其实小文件问题在 AI 生产环境一样严重。数据预处理把原始视频抽帧一个稍微大点的数据集就是上百万个 JPEG模型评估阶段还会持续产出日志、指标、中间结果同样是海量小文件。传统文件系统的元数据操作通常要经过一个中心化节点每秒钟能处理的 create/lookup/delete 数量有上限。当多个训练任务同时跑数据准备和评估时即使数据总量不大只要文件数量足够多文件系统就会出现创建文件越来越慢、ls目录卡住、任务莫名其妙等 I/O 的现象。所以判断一套文件系统行不行不能只看带宽还要看三点单目录下高并发创建的性能、海量小文件下的读时延、以及周期性大文件并发写的稳定性。PFS L3 的设计逻辑基本就是围绕这三个问题展开的。2. PFS L3 的三层架构不是简单地加缓存“L3”这个命名容易让人误以为就是三级缓存实际上它的内核是一个三级并行文件存储体系。每一层都有自己的职责数据可以在层与层之间流动但对应用层暴露的是一个统一命名空间。2.1 L1算力节点上的热数据窗口L1 是部署在每个计算节点本地的 NVMe 或内存级缓存。它的价值在于兜住那些高频、重复读取的小数据。训练 epoch 之间会反复读取同一批样本第一次从远端拉完后样本进入 L1后续访存直接命中本地不再走网络。这个设计和 CPU 的缓存思路很像只不过粒度更大它以文件块或对象为单元。L1 还负责写缓冲。小文件在 L1 本地先聚合达到一定大小再异步刷到下一层这样应用层不再需要逐个小文件等待确认整体写入时延会明显下降。但要特别注意的是L1 是易失性质的高速窗口不是数据持久层。如果节点宕机只有 L1 里尚未刷出的数据会受影响所以真正的重要数据还是要尽快落到低层持久化存储L1 只能承担“缓冲加速”的角色。这里其实需要配置一个策略判断哪些数据值得留在 L1比如可以根据文件大小和访问频率设置冷热阈值。2.2 L2训练集群内部的近计算存储池L2 是我觉得 PFS L3 设计里最有意思的一层。它把多个计算节点上空闲的 NVMe 组成一个近计算存储池横向扩展能力和数据访问延迟介于本机 L1 和大规模共享存储 L3 之间。这里的核心动机是算力集群内部的网络带宽通常远大于存储后端到计算端的网络带宽把数据放在离 GPU 更近的池子里可以大幅减少后端存储的压力。L2 的典型应用场景是 checkpoint 的暂存位置。很多训练框架的 checkpoint 写入频率很高但真正需要长期保留的版本不多。模型每训练一定步数就产出一个 checkpoint旧的版本往往只是用来容灾恢复过一段时间就会被清理。如果每次都直接写到底层持久存储底层存储要承担极高频的突发写入和删除但如果先把 checkpoint 写到 L2 近计算存储池再从 L2 异步落到底层前端训练只感知到“数据写完了”的低延迟后台再按策略把需要保留的版本转移到 L3。L2 在逻辑上更像一个缓冲区和分层调度器而不是单纯的缓存。L2 的另一个作用是大文件分段预取。训练集经常是整个文件整体读入L2 可以根据任务对数据集文件的访问预取列表提前把下一批数据从 L3 拉到近计算池等 GPU 真正需要时数据已经在本地网络内了。它能做到这一点是因为它和调度系统之间有协同知道哪个任务马上要读哪个目录。这是传统存储系统很难做到的因为传统文件系统通常对上层任务调度一无所知。2.3 L3真正的持久并行文件系统底层 L3 才是负责可靠性和容量扩展的数据持久层。它的角色类似传统并行文件系统但在很多细节上做了面向 AI 负载的取舍。L3 内部由两类节点组成一类是元数据服务节点负责目录、文件属性、命名空间的维护另一类是数据存储节点负责具体文件块和对象的数据读写。两类节点可以独立扩展尤其是元数据节点可以根据目录树的热度做分片和负载均衡。L3 把最终数据以分布式方式存放在多台存储服务器上写入时会按照条带化策略将文件切成多个数据块同时计算校验块在节点间做纠删码或副本冗余。相比 HPC 场景随时可能遇到的超大文件AI 负载的数据大小分布其实更不均匀所以 L3 的条带大小并不固定而是按照文件类型和大小动态调整。大 checkpoint 文件会分得更宽小文件则避免过度条带化减少额外的小 I/O 放大。值得一提的是L3 并不是简单的“对象存储加 POSIX 网关”。对象存储擅长高吞吐、海量数据的顺序读写但它的目录列举和随机写性能很弱传统 POSIX 并行文件系统功能全但锁开销大。L3 在命名空间上支持类似文件系统的目录层级对应用暴露标准文件接口但在内部存储和索引上吸收了对象存储的设计把大文件和数据对象统一管理这样既照顾到训练框架直接读写文件的习惯又能获得对象存储那样的大规模扩展能力。3. 数据路径与控制路径的分离把并发写冲突降到最低任何分布式文件系统都要面对一个核心矛盾控制信息需要全局一致数据信息需要最大并行。PFS L3 的解决方式是严格分离控制路径和数据路径。3.1 文件被拆成什么才不会让多个 GPU 写同一个文件时互相打架当多个节点并发写一个 checkpoint 文件时最理想的状况是每个节点负责文件中不同范围的数据大家各自写各自的分片彼此之间根本没有锁竞争。这要求文件系统把文件内部的逻辑地址空间从一开始就映射到明确的存储节点和存储块上。PFS L3 的做法是把一个文件看作由多个可寻址的数据单元组成的对象每个数据单元有独立的物理位置。应用写入时客户端会先向控制面请求“我打算写文件 A 的哪个范围”控制面把对应范围的布局信息返回给客户端客户端拿到布局后直接绕过控制面通过 RDMA 或普通 TCP 把数据发给对应的数据节点。由于不同节点写的是不同地址范围内的不同数据单元数据节点之间不需要频繁协调锁。只有在多个客户端写同一逻辑块的极端场景下才会触发更严格的锁与冲突合并机制但这种场景在 checkpoint 写入里基本不会出现。这种设计的本质是把“文件锁”这种全局同步原语替换成了“基于布局的路由”。文件系统不再需要维护一个全局的字节范围锁表只需要维护文件的逻辑到物理映射关系。映射关系本身是稳定的不会因为并发写而频繁变化因此元数据服务的锁压力大幅下降。3.2 元数据节点为什么要水平分割元数据服务的瓶颈几乎总是“单点通知”和“锁风暴”导致的。PFS L3 对命名空间做分区管理把不同的目录子树分配到不同元数据节点例如训练数据集、checkpoint 目录、日志目录可以分散在多个元数据节点上。每个元数据节点只负责自己的子树处理请求时不需要和其他节点反复确认。一个节点的处理能力不够时可以继续把某个热点目录下再拆成更细粒度的分片迁移部分文件到新节点。这种机制能显著改善 AI 工作负载里的常见问题。前面提过的百万级小文件——每个文件创建、打开、读取属性本来都要经过同一个元数据节点做了子树分片后不同训练任务的数据集分散在不同元数据节点上单个节点承担的压力就只有原来的几分之一。我实际排查过的不少小文件性能问题最后都指向同一个原因所有进程都在一个目录下创建文件所有元数据请求都压在一个名字空间节点上。PFS L3 的调度策略会尽量让不同任务使用不同目录这样元数据自然就打散了。3.3 强一致性语义给 AI 负载留了一个可开关的口子很多并行文件系统为了顺从传统应用默认提供非常强的 POSIX 一致性一个客户端写完数据另一个客户端立刻能看到并且数据内容是最新的。这个语义安全但代价很高。为了保证强一致写入以后必须立刻做同步、索引更新和可见性通知在分布式环境下这些都是额外开销。PFS L3 做了一件值得赞赏的事它把一致性语义做成了可配置项。对模型训练数据这类“只读共享、每个节点各读各的”场景可以使用相对弱的缓存一致性允许客户端缓存读结果一段时间对 checkpoint 这类需要快速确认落盘的场景提供强一致的写确认而面对“分布式训练一个节点写权重、另一个节点读权重”这种跨节点依赖才会要求真正的全局可见性。在实际生产里训练框架通常通过文件关闭或fsync来判断 “这个文件训练好了可以给别人读了”。只要文件系统和训练框架配合得当弱一致缓存并不会导致数据错误反而能明显减少日志和元数据更新的次数。它们把一致性选择权交给使用者而不是强制一刀切这是面向高并发 AI 场景很实际的做法。4. 实施 PFS L3 时的四个观测点与实际操作经验架构看得再明白部署和调优阶段仍有许多意想不到的问题。下面这几条是我实操中沉淀下来的经验不一定每个版本都完全适用但排查思路是可以复用的。4.1 别用统一的“块大小”处理所有文件很多并行文件系统默认会把所有文件切成同样大小的条带。这对小文件还好但对 checkpoint 这种 10 GB 以上的大文件条带大小偏小会带来大量不必要的控制消息条带太宽又会造成小文件的 IO 放大。PFS L3 的动态布局策略给了我一个启发调优最重要的不是死记参数而是理解文件系统的条带决策依据。实际操作时我建议至少区分三类文件分别建目录或者卷组checkpoint 目录文件大、并发写、需要高带宽配置大条带、宽条带。数据集目录多为中小文件顺序读为主配置适中条带和较强预读。日志与临时目录文件小、生命周期短避免过度冗余可以关闭同步写。这种区分能让底层存储把资源用在该用的地方。很多团队习惯把所有数据塞进一个池子里结果文件系统只能按“中庸”策略应对所有文件两头都不讨好。4.2 写 checkpoint 不会自动变成“宽条带”有一个经常被忽略的细节即使文件系统支持大文件宽条带如果写入请求是流式顺序的且单客户端只写一个固定范围带宽依然会被一个存储节点限制住。PFS L3 的折中方案是从数据单元层面并行既有真正的条带也有按地址切片的并行。但实际中要发挥这一层的优势客户端配置很重要。训练任务侧最好把一次 checkpoint 逻辑拆成多个文件分片或使用框架自带的分片策略这是因为多客户端并行写入多个独立分片比单客户端单文件大带宽更容易调度。一些大模型训练框架支持“分片 checkpoint”模式就是每个 rank 保存自己负责的权重分片这样每个 rank 访问自己独立的小文件文件系统可以做最大维度的并行。我见过有人为了留存方便硬是把所有分片合并成一个超大文件再写入结果写入时间反而增加了两倍以上。做 checkpoint 策略时不要为了“好看”牺牲并行度。4.3 观测指标要分四个层面不能只看带宽文件系统的问题往往在发生很久之后才会反映到带宽上所以生产监控必须分维度做监控维度关键指标常见问题信号元数据每秒 create/delete/lookup 次数、元数据队列深度目录并发操作时耗时突增数据写路径聚合写带宽、单节点写带宽、写延迟带宽高但任务仍卡看是否有锁等待数据读路径预读命中率、客户端缓存命中率、网络重传率命中率长期偏低要调整预取策略存储节点资源CPU、内存、磁盘队列深度、网络吞吐某个存储节点长期打满说明热点分布不均实际踩过的坑是带宽监控一切正常但训练任务周期性地出现几秒停顿。最后查出来是很多节点在做小文件的目录扫描元数据服务每秒请求数飙到上限数据路径并未拥堵但控制面已经被打满。很多监控体系只看存储节点流量不看元数据服务状态所以这类问题隐藏得很深。4.4 从对象存储或旧数据源做初次迁移时要照顾小文件的数据摄入把一个已有数据集从外部对象存储迁入 PFS L3 时最常见的失败模式是“目录建得飞快但文件灌入速度非常低”。原因不难理解一个几千万文件的对象桶按目录映射进来后首先触发的是大量元数据创建操作。比如源端一个桶里有 1 亿个小文件迁移时每个文件都对应一次create和一次数据写入如果迁移工具不做分组整个元数据服务就会被拖垮。我的经验是迁移前先在文件系统里把目录层级规划好避免所有目录集中在一个元数据节点同时迁移工具要做“多目录并发”和“批量创建”的优化不要串行执行文件创建。另外迁移过程中要注意控制源端读取的并行度防止把源端对象存储的限流打出来导致迁移越跑越慢。PFS L3 本身不是瓶颈瓶颈往往在线程模型和并发策略配得不够合理。5. 几个容易被忽略但影响很大的“绕行路径”5.1 网络拓扑与数据节点分布要一起看并行文件系统的性能上限常常不是存储节点的磁盘速度而是计算节点到存储节点之间的网络拓扑。如果数据被乱序分散到多个存储节点而存储节点分布在不同的网络交换机下即使总带宽足够端到端往返也会产生很高延迟。实践里建议把数据节点的网络位置信息纳入调度尽量让高频访问的数据分片落在距离计算节点较近的存储节点上。这个优化对延迟敏感型推理负载效果尤其明显。5.2 元数据高可用不能只靠节点数量很多新一代文件系统都通过“多活元数据节点”提升可靠性但多活并不意味着没有单点风险比如某些关键目录范围仍然绑定在单一节点上。若目录树上层的热点还在固定的元数据节点单点故障依然会造成整个命名空间的不可用。我建议在配置系统时重点看目录分片迁移能力而不是只看元数据节点总数。实在无法自动迁移时至少要保证根目录、大目录的管理节点不会和数据热点集中在同一台物理机上。5.3 容器环境下要关注挂载点密度不少训练平台现在用 Kubernetes 来调度训练任务每个 Pod 都可能挂载同一个文件系统。这里有一个“隐藏的坑”挂载点数量过高时即使没有实际文件读写内核的文件系统客户端和元数据服务之间也会产生心跳、属性和状态查询消息。大量临时 Pod 创建销毁会让挂载表频繁刷新有些文件系统会因为客户端注册风暴而出现部分节点暂时无响应。解决思路有两种一种是限制同时挂载的 Pod 数量让多个容器共享一个存储客户端另一种是尽量延长 Pod 生命周期不要让短暂任务频繁挂载/卸载。我们在一套测试环境里把训练 Pod 的挂载策略从“每任务新挂载”改成“常驻客户端池”元数据请求量立刻下降了将近一半训练任务的启动速度也明显提升。5.4 别低估读写方向带来的性能差异分布式存储系统在不同读写方向上的表现很不一样。通常写路径需要完成数据冗余计算、多副本确认和元数据更新读路径则更依赖预取和缓存命中。PFS L3 对写路径和读路径分别做了优化例如写路径把数据先写入日志型缓存异步再合并为大块刷盘读路径则强调客户端缓存和大块预取。这也导致我们做压测时必须拆开看只测写带宽只能说明数据进入缓存层的能力只测读带宽又不能反映小文件和元数据的真实压力。我建议在验收时至少准备四类压测顺序大文件写、顺序大文件读、海量小文件随机读、高并发小文件创建删除。四类都过了系统才算真正适合 AI 生产负载而不是只适合展示 benchmark。回到 PFS L3 的整体设计它的核心价值不在于某个模块有多巧妙而在于它把一个核心事实贯彻到底AI 存储的需求不是“越大的带宽越好”而是“在正确的时间把数据放到正确的位置并且用最少的锁和握手完成这件事”。如果你正在做训练集群的存储选型建议不要只盯着厂商给的最大带宽参数多问一句当一百个节点同时写 checkpoint 时你是怎么控制锁和布局的当元数据服务跨多个节点分片时我能不能看到热点数据落在哪里这两个问题的答案往往才是生产环境里真正的分水岭。