导语:Meta 的存储架构复盘给出了一条明确路径
AI 摘要
Meta 运营数百 EB 级存储集群,基于 Tectonic 分层存储层构建 BLOB 存储架构,以应对两大挑战:最大化 GPU 利用率与研究迭代速度。传统 BLOB 架构的多层元数据查询可导致数百毫秒延迟,使 GPU 因 I/O 等待停顿。新架构将训练栈逐步迁移到 BLOB 存储接口上,利用闪存提供可预测的低 pMax 延迟,避免单 GPU 慢速拖慢整批任务。同时,统一的数据湖访问支持地理分布 GPU 间的数据高速注入与跨区移动,提升研究效率。
过去几年间,模型能力与训练数据集规模呈现出指数级增长。而在最近一年左右的时间里,新一代前沿模型的发布间隔已从数月缩短至数周。对于这场 AI 创新的速度与计算成本而言,可靠且快速的数据存储至关重要。如果把 AI 比作大脑,那么存储就是记忆:能力与速度高度取决于记忆容量与检索速度。
然而,尽管 AI 计算性能大约每两年翻三倍,存储和互连性能的增长却相对平缓。其结果是,存储瓶颈依然是导致 AI 工作负载中 GPU 空转的主要原因之一,直接影响了支出成本和上市时间。除了 GPU 利用率之外,存储架构还直接影响 AI 研究的迭代速度;随着 GPU 日益分布在全球各地、数据集规模变得极其庞大,研究人员花费大量时间进行跨区域的数据摄取与迁移,从而拖慢了研究节奏。在这篇博客文章中,我们将讨论 Meta 的 BLOB 存储架构如何演进,以应对两大核心挑战:最大化 GPU 利用率和最大化研究速度。
存储架构概述
Meta 运营着数百个艾字节级的存储集群,服务于 Meta 所有外部和内部产品,包括 Facebook、Instagram、Reality Labs、Meta AI、广告、数据仓库和内部数据库。我们的存储服务提供对象存储、文件系统和块设备 API,这些 API 抽象构建在一个名为 Tectonic 的水平可扩展基础块层之上。Tectonic 层是一个区域性、多租户存储架构,利用纠删码技术提供高持久性和高可用性,支持跨介质类型(如 HDD 和闪存)的分层存储,并管理热数据、冷数据和温数据的智能放置,以实现跨租户的高效 I/O 利用。在 Tectonic 之上运行的 BLOB 存储层展现了一个全局、无限可扩展的存储架构,并提供策略让用户在持久性和可用性之间进行权衡。
在之前题为 " 训练 Llama:存储视角 " 的 @Scale 演讲中,我们讨论了 Meta 如何通过在 Tectonic 块层之上暴露类似 NFS 的文件系统接口来直接训练 Llama。虽然这种架构在 Meta 内部仍被广泛使用,但我们的现代训练堆栈已开始逐步迁移到 BLOB 存储接口之上,这也是整个行业的趋势。这一转变是由于需要统一访问 BLOB 存储层中的海量数据湖以及高性能需求所驱动的。
最大化 GPU 利用率
现代 AI 工作负载 " 数据饥渴 ",并且具有与传统 Web 应用截然不同的工作负载特征:突发性和持续高吞吐量、可预测且有界的最大延迟、以及变化的 I/O 模式。近年来,BLOB 存储的重点已基本转向最大化 GPU 利用率。
为什么延迟至关重要
要理解为何有界且低 pMax 延迟如此重要,我们不妨考虑模型训练的过程。在训练过程中,数十万块 GPU 会多次遍历存储中的海量数据(即多个训练周期),并以批次方式训练数据集。每隔一定数量的步数或批次,GPU 之间会同步各自的状态。如果某一块 GPU 速度变慢,这一步就会拖慢所有 GPU 乃至整个训练过程。
图 1 展示了一个跨两块 GPU 的数据加载流水线。每个 GPU 主机上的数据加载器会预取下一个数据集批次,同时 GPU 正在处理当前批次以实现计算与 I/O 的最大重叠。对于 GPU1,存储读取延迟完全在界限之内,因此 GPU 从不会因等待 I/O 而停滞。对于 GPU2,出现两次存储读取延迟过高的情况,导致 GPU 停滞。这些停滞导致整个步骤完成时间被延长。
图 1:跨两块 GPU 的数据加载。
传统 BLOB 存储架构尚未为 AI 做好准备
多年来,BLOB 存储以有机方式演进,以真正的面向服务风格层层叠加。其中许多层是有状态的,并维护着自己的元数据存储。虽然这些元数据访问延迟对于全球 HDD 所服务的传统用例而言通常不是瓶颈,但对于要求毫秒级闪存数据访问的 AI 工作负载来说,它们却是致命障碍。图 2 展示了一个典型的 getObject ( "/bucket/path" ) API 的请求流程。请求到达 API 服务器后,服务器会在名称层、卷层和容器层中进行多次元数据查找,然后将路径解析为一组 ( blockId, offset, size ) 元组。其中一些查找可能跨区域,延迟累积到几百毫秒的情况并不少见;只要其中一次查找响应缓慢就足以造成问题。查找完成后,API 服务器将数据从 Tectonic 层代理转发到客户端。
图 2:getObject API 的旧请求流程。
尽管这一架构在传统工作负载上表现出色,但指导设计权衡的基本假设已经发生了变化。其中一些变化包括:
· 性能与延迟:如前所述,传统工作负载对延迟要求并不高,而 AI 工作负载要求从低分位一直到最高分位(pMax)都保持可预测且有边界的延迟。
· 可靠性与持久性:旧架构设计为即使在区域故障的情况下也能高度持久且可用;数据与元数据默认全局复制。虽然 AI 工作负载要求极高的可用性,但 " 默认全局复制 " 这一设计选择已不再适用。
· 成本效率:传统存储栈基于 HDD 构建,高度优化每字节成本。AI 工作负载对 IOPS 的要求迫使使用闪存,此外,相对于 GPU 的计算成本,存储的计算成本已变得微不足道。
· 能源效率:由于 GPU 的使用,数据中心日益受到电力限制而非空间限制。每千瓦电力用于存储,就意味着没有用于 GPU。这是 AI 工作负载带来的新约束。
简而言之,权衡空间已经发生了足够大的变化,促使我们重新思考整个架构。
重建基础
在着手构建新基础时,我们做出了以下主要设计选择:
· 统一元数据模式:我们重写了元数据子系统,将原本分散在不同层的元数据合并为基于 ZippyDB 的统一扁平模式。这为实现路径到存储地址的 O ( 1 ) 查找铺平了道路,是一次阶跃式的改进。
· 无数据面代理:我们移除了数据面代理,构建了一个胖客户端 SDK,能够直接从存储服务器向客户端流式传输字节。这有助于实现能效目标,同时也能实现更高的吞吐量和更低的延迟。
· 区域部署:BLOB 存储栈现在更加精简,可以灵活地作为区域或全局服务进行部署。我们现在在每个 AI 区域部署一套与 GPU 同地协作的区域 BLOB 存储栈。
图 3 展示了 getObject ( "/bucket/path" ) 的新请求流程。
当客户端上的 SDK 收到该 API 调用时,它现在会向 API 服务器发出一个 getReadPlan ( "/bucket/path" ) 请求。API 服务器对每个数据块进行一次 O ( 1 ) 查找,在元数据存储中将路径映射为 ( blockId, offset, size ) 元组,然后将 ReadPlanResult 返回给 SDK。SDK 内部嵌入了 Tectonic BlockClient,因此能够直接从 Tectonic 流式读取这些数据块的数据。通过这些改动,我们重新构建了基础架构,并实现了在 Tectonic 之上零额外开销的目标。通过消除数据代理,我们还满足了功耗预算限制。
应对突发流量与热点问题
在数据和检查点加载过程中,AI 工作负载通常会跨数百个 GPU 并发访问数据。像模型权重这样的数据子集往往是 " 热点 ",而 GPU 重启等事件会引发剧烈的流量尖峰。在解决基础问题之后,我们的下一个挑战便是应对这些突发流量与热点。幸运的是,BLOB 存储层多年来已积累应对热点的经验,因此我们将现有解决方案适配至 AI 工作负载。具体来说,我们采用了两种方法:
· 分布式数据缓存:我们利用 GPU 主机上的空闲内存作为分布式数据缓存,用于缓存频繁且并发访问的数据。为此,我们复用了 Meta Owl 子系统的组件:将 Owl 子系统中的对等节点直接集成到 BLOB 存储客户端 SDK 中,从而使所有数据访问都经过该数据缓存。
· 读取计划元数据缓存:读取计划(Readplan)指的是从路径到存储地址的映射。现在,我们将频繁访问的 BLOB 的读取计划缓存在一个类似 memcache 的分布式内存存储中。
实践中,我们观察到分布式数据缓存的平均命中率达到 80%,读取计划缓存可在 1-2 毫秒内提供元数据访问。本质上,这些简单机制实现了三件事:
· 吸收突发流量,减少对存储的 I/O 需求。
· 解决元数据热点分片问题。
· 通过从内存提供服务,降低 p50 和 p99 延迟。
协议优化
到目前为止讨论的内容已经帮我们完成了 80% 的工作。剩下 20% 是通过识别并修复整个技术栈中的瓶颈来实现的。以下是一些值得注意的问题,但绝非完整清单:
滞后节点:一个慢速存储节点导致尾延迟。这是一个已被充分理解的问题,我们通过在客户端采用对冲读取(hedged reads)来缓解。
出口流量尖峰:在检查点事件期间,客户端常常会产生剧烈的出口流量尖峰。这反过来又会导致拥塞、超时和重试,最终使 GPU 停滞。我们通过在客户端 SDK 中构建动态并发控制来解决这一问题,该机制根据应用层拥塞信号自动调整并行度。
综合以上所有优化,新的 BLOB 存储栈现在能够服务于 AI 工作负载而不会造成 GPU 停滞,并且在 Tectonic 层之上引入的开销几乎可以忽略不计。我们的下一个工作重点转向了研究。
最大化研究速度
GPU 资源稀缺且日益地理分布化;与此同时,出于性能原因,训练工作负载需要数据与 GPU 同地部署。这给研究人员带来了一个有趣的挑战:他们现在需要负责跨区域摄取和移动数据集。
在 Meta,一个典型的训练任务提交包含以下步骤:
研究人员从各种来源整理数据,对其进行丰富处理后持久化存储到 BLOB 存储中。
研究人员选择一个区域来运行任务。
研究人员提交一个数据摄取任务,该任务将训练数据集以针对 GPU 主机内数据加载优化的文件格式,在目标区域创建快照。
然后研究人员等待摄取完成;根据数据集大小,这可能需要数小时。
研究人员提交他们的训练任务并监控运行情况。
研究人员分析输出结果,调整数据集,然后从第 3 步开始再次迭代。
步骤 2 到 4 可能耗时数小时,直接影响研究人员的迭代速度。理想情况下,我们希望研究人员把时间花在调优模型上,而不是等待存储。目前,研究人员会在启动任务前复制快照,以便让数据与 GPU 同位置存放,从而获得最优性能。虽然这种性能优化对于持续数周或数月的大规模训练任务来说合情合理,但绝大多数任务规模要小得多;负责这些任务的研究人员更愿意用偶尔的性能下降来换取迭代速度。
因此,我们需要一个系统,让研究人员能够一次性摄入数据,并在任何地方访问数据,无需考虑区域边界。我们需要一套工作流,让研究人员能够在几分钟内完成迭代,而不是几小时。当我们重新回到设计阶段时,这些数据集 " 一次写入、多次读取 " 的特性让我们灵光一现。如果我们把存储视为一台行星级计算机中的磁盘,并从操作系统领域借鉴一些想法呢?当一个运行在 CPU 核心上的 Linux 进程试图从磁盘读取文件时,操作系统会透明地按需从各层缓存——内存中的页缓存以及 L2 和 L1 CPU 缓存——中补充数据。这一直觉催生了图 4 中的架构演进:
图 4:数据加载架构演进。
核心思想是将各种本地和远程存储资源作为分层缓存来利用,并以 HDD 支持的全局 BLOB 存储结构作为最终的真实数据源。具体来说,我们将 GPU 主机上的内存和闪存作为 L1 和 L2 缓存。我们将由闪存支持的区域 BLOB 存储结构作为 L3 缓存,数据加载器继续通过熟悉的 BLOB 存储 SDK 访问存储。为了有效隐藏延迟并简化数据生命周期,我们依赖以下机制:
· 数据加载器预取:数据加载器在处理当前批次的同时,将下一批数据集预取到内存中。这种预取操作在 BLOB 存储 SDK 层面会体现为一次读取操作。
· 深度预取:我们在 BLOB 存储 SDK 中暴露了一个显式的 `prefetch ( ) ` API。数据加载器会在后台调用 `prefetch ( ) ` API,主动预取接下来几分钟所需的数据。这个 API 会触发从远程存储到本地区域 L3 缓存的数据水合(hydration),同时预热元数据缓存。
· 自动数据生命周期:L3 区域分布式闪存层中的数据通常会保存一段配置的时间,以便在训练周期中跨轮次复用。我们支持自定义的淘汰策略,包括 TTL 和 LRU 策略。这些淘汰策略也考虑了容量 / 配额限制。
从生产上线开始,这种新的数据加载范式就被迅速采用,目前我们在生产环境中同时支持两种数据加载范式。为了用数字说明影响,图 5 展了所有工作负载在上线前后的数据摄入时间对比:
图 5:上线前后的数据摄入时间。
在一个新前沿模型每隔几周就会发布的世界里,这种数据加载范式的转变正是为了更快前进而亟需的变化。
关键要点
现代 AI 工作负载对数据的需求极大,存储对计算成本和创新速度都起着重要作用。存储瓶颈直接影响 GPU 利用率和计算成本,而在 GPU 分布全球的场景下,跨区域数据摄入所花费的时间会直接影响研究的迭代速度。Meta 的 BLOB 存储架构最初是为 Meta 的系列应用服务的,我们需要在性能上实现阶梯式提升,以支持 AI 工作负载。这促使我们重新思考整个架构。通过重建元数据子系统,并采用分层的缓存架构(包含预取和按需水合),我们能够有效满足当前工作负载的需求。
未来工作
我们在 Meta 持续演进存储系统,以跟上硬件演进和工作负载需求。这一领域未来的工作将包括:
· 将存储扩展到网络极限。
· 在更大规模下支持检查点操作,而不暂停 GPU。
· 推理工作负载面临新的挑战,我们正着手应对。
来源:艾瑞网
