转载

OpenAI研究 将 Kubernetes 扩展到 7,500 个节点

我们已将 Kubernetes 集群扩展到 7,500 个节点,为GPT-3、  CLIP和 DALL·E等大型模型提供了可扩展的基础设施 ,同时也为 神经语言模型的缩放定律等快速小规模迭代研究提供了基础设施。

很少有人将单个 Kubernetes 集群扩展到这种规模,并且需要特别小心,但好处是一个简单的基础设施,使我们的机器学习研究团队能够更快地移动并在不更改代码的情况下进行扩展。

自从我们上一篇关于 扩展到 2,500 个节点的文章以来 ,我们一直在不断发展我们的基础设施以满足研究人员的需求,在此过程中吸取了许多额外的经验教训。这篇文章总结了这些经验教训,以便 Kubernetes 社区中的其他人可以从中受益,并以我们接下来要解决的仍然面临的问题作为结尾。

chart = RuntimeError: 获取失败
chart2 = RuntimeError: 获取失败

我们的工作量

在我们走得太远之前,描述我们的工作量很重要。我们使用 Kubernetes 运行的应用程序和硬件与您在典型公司可能遇到的完全不同。我们的问题和相应的解决方案可能会或可能不会很好地匹配您自己的设置!

大型机器学习作业跨越多个节点,当它可以访问每个节点上的所有硬件资源时,运行效率最高。这允许 GPU 直接使用NVLink进行交叉通信 ,或者 GPU 直接使用GPUDirect与 NIC 通信 。因此,对于我们的许多工作负载,单个 pod 占据了整个节点。任何 NUMA、CPU 或 PCIE 资源争用都不是调度因素。装箱或碎片不是常见问题。我们当前的集群具有完整的二等分带宽,因此我们也没有考虑任何机架或网络拓扑。所有这一切意味着,虽然我们有很多节点,但调度程序的压力相对较小。

也就是说,kube-scheduler 的压力非常大。一项新工作可能包括同时创建数百个 pod,然后恢复到相对较低的流失率。

chart = RuntimeError: 获取失败

我们最大的作业运行 MPI,作业中的所有 pod 都参与一个 MPI 通信器。如果任何参与的 Pod 死亡,整个作业将停止并需要重新启动。作业定期检查点,并在重新启动时从最后一个检查点恢复。因此,我们认为 pod 是 半状态的——可以替换被杀死的 pod 并且可以继续工作,但这样做会造成破坏,应该保持在最低限度。

我们不太依赖 Kubernetes 负载平衡。我们的 HTTPS 流量非常少,不需要 A/B 测试、蓝/绿测试或金丝雀测试。Pod 通过 SSH 使用 MPI 在它们的 Pod IP 地址上直接相互通信,而不是服务端点。服务“发现”是有限的;我们只是在作业启动时一次性查找哪些 pod 参与了 MPI。

大多数作业与某种形式的 blob 存储交互。他们通常要么直接从 blob 存储流式传输数据集的一些分片或检查点,要么将其缓存到快速的本地临时磁盘。对于 POSIX 语义有用的情况,我们有一些 PersistentVolume,但 blob 存储的可扩展性要高得多,并且不需要缓慢的分离/附加操作。

最后,我们的工作本质上是研究,这意味着工作量本身是不断变化的。虽然超级计算团队努力提供我们所认为的“生产”质量水平的计算基础设施,但在该集群上运行的应用程序是短暂的,并且它们的开发人员会快速迭代。新的使用模式随时可能出现,挑战我们对趋势和适当权衡的假设。我们需要一个可持续的系统,让我们能够在事情发生变化时迅速做出反应。

联网

随着集群中节点和 pod 数量的增加,我们发现 Flannel 难以扩展所需的吞吐量。我们转为使用本机 pod 网络技术进行 Azure VMSS 和相关 CNI 插件的 IP 配置。这使我们能够在 Pod 上获得主机级网络吞吐量。

我们转而使用基于别名的 IP 寻址的另一个原因是,在我们最大的集群上,我们可能同时使用大约 200,000 个 IP 地址。当我们测试基于路由的 pod 网络时,我们发现我们可以有效使用的路由数量存在很大限制。

避免封装增加了对底层 SDN 或路由引擎的需求,但它使我们的网络设置变得简单。添加 VPN 或隧道无需任何额外的适配器即可完成。由于网络的某些部分具有较低的 MTU,我们无需担心数据包碎片。网络策略和流量监控很简单;数据包的来源和目的地没有歧义。

我们在主机上使用 iptables 标记来跟踪每个命名空间和 pod 的网络资源使用情况。这让研究人员可以可视化他们的网络使用模式。特别是,由于我们的许多实验具有不同的 Internet 和 pod 内通信模式,因此能够调查可能发生瓶颈的位置通常很有用。

iptables mangle 规则可用于任意标记符合特定条件的数据包。以下是我们检测流量是内部流量还是互联网流量的规则。这些 FORWARD 规则涵盖来自 pod 的流量、vs INPUT 和 OUTPUT 来自主机的流量:

iptables -t mangle -A INPUT ! -s 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-in"
iptables -t mangle -A FORWARD ! -s 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-in"
iptables -t mangle -A OUTPUT ! -d 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-out"
iptables -t mangle -A FORWARD ! -d 10.0.0.0/8 -m comment --comment "iptables-exporter openai traffic=internet-out"
无效的

标记后,iptables 将启动计数器来跟踪与此规则匹配的字节数和数据包数。您可以通过使用自身来观察这些计数器 iptables :

% iptables -t mangle -L -v
Chain FORWARD (policy ACCEPT 50M packets, 334G bytes)
 pkts bytes target     prot opt in     out     source               destination
....
1253K  555M            all  --  any    any     anywhere            !10.0.0.0/8           /* iptables-exporter openai traffic=internet-out */
1161K 7937M            all  --  any    any    !10.0.0.0/8           anywhere             /* iptables-exporter openai traffic=internet-in */
无效的

我们使用一个名为iptables-exporter的开源 Prometheus 导出器  ,然后将它们跟踪到我们的监控系统中。这是一种跟踪匹配各种不同类型条件的数据包的简单方法。

chart = RuntimeError: 获取失败

我们网络模型的一个有点独特的方面是我们将节点、pod 和服务网络 CIDR 范围完全公开给我们的研究人员。我们有一个中心辐射网络模型,并使用本地节点和 pod CIDR 范围来路由该流量。研究人员连接到中心,并从那里可以访问任何单独的集群(辐条)。但是集群本身不能相互交谈。这确保集群保持隔离,没有可能破坏故障隔离的跨集群依赖性。

我们使用“NAT”主机为来自集群外部的流量转换服务网络 CIDR 范围。这种设置使我们的研究人员可以非常灵活地选择他们能够为实验选择的网络配置方式和类型。

API 服务器

Kubernetes API 服务器和 etcd 是健康工作集群的关键组件,因此我们特别注意这些系统的压力。我们使用kube-prometheus提供的 Grafana 仪表板 ,以及其他内部仪表板。我们发现将 API 服务器上的 HTTP 状态 429(请求过多)和 5xx(服务器错误)的速率作为高级问题信号发出警报很有用。

chart = RuntimeError: 获取失败

虽然有些人在 kube 中运行 API 服务器,但我们总是在集群本身之外运行它们。etcd 和 API 服务器都运行在它们自己的专用节点上。我们最大的集群运行 5 个 API 服务器和 5 个 etcd 节点来分散负载,并在其中一个出现故障时将影响降至最低。自从我们在上一篇博文中将 Kubernetes Events 拆分到他们自己的 etcd 集群以来,我们在 etcd 方面没有遇到明显的问题 。API 服务器是无状态的,通常很容易在自我修复的实例组或规模集中运行。我们还没有尝试构建 etcd 集群的任何自我修复自动化,因为事件极其罕见。

API 服务器会占用相当多的内存,并且往往会随着集群中节点的数量线性扩展。对于我们拥有 7,500 个节点的集群,我们观察到每个 API 服务器使用了高达 70GB 的堆,所以幸运的是,这在未来应该继续在硬件能力范围内。

chart = RuntimeError: 获取失败

API 服务器的一大压力是 WATCHes on Endpoints。有一些服务,例如“kubelet”和“node-exporter”,集群中的每个节点都是其成员。当一个节点被添加到集群或从集群中删除时,这个 WATCH 将被触发。而且因为通常每个节点本身都通过 kube-proxy 监视 kubelet 服务,所以这些响应中所需的 # 和带宽将是 2个否2个 巨大的,偶尔 1GB/s 或更多。 在 Kubernetes 1.17 中推出的EndpointSlices带来了巨大的好处,可以将此负载降低 1000 倍。

chart = RuntimeError: 获取失败

一般来说,我们非常注意任何随集群大小扩展的 API 服务器请求。我们尽量避免让任何 DaemonSet 与 API 服务器交互。在确实需要每个节点监视更改的情况下,引入中间缓存服务(例如 Datadog  Cluster Agent)似乎是避免集群范围瓶颈的好模式。

随着我们集群的增长,我们对集群的实际自动缩放会减少。但是,当一次自动缩放太多时,我们偶尔会遇到麻烦。新节点加入集群时会产生很多请求,一次添加数百个节点会使 API 服务器容量过载。解决这个问题,即使只是几秒钟,也有助于避免中断。

Prometheus 和 Grafana 的时间序列指标

我们使用 Prometheus 收集时间序列指标,使用 Grafana 收集图表、仪表板和警报。我们从部署 kube-prometheus开始 ,它收集各种指标和良好的可视化仪表板。随着时间的推移,我们添加了许多我们自己的仪表板、指标和警报。

随着我们添加越来越多的节点,我们开始与普罗米修斯收集的大量指标作斗争。虽然 kube-prometheus 公开了大量有用的数据,但其中一些我们实际上从未查看过,还有一些过于细化,无法有效地收集、存储和查询。我们使用 普罗米修斯规则 来“删除”其中一些指标,使其不被摄取。

有一段时间,我们一直在为 Prometheus 消耗越来越多内存的问题而苦苦挣扎,直到最终在内存不足错误 (OOM) 中使容器崩溃。即使在向应用程序投入大量内存容量之后,这似乎也会发生。更糟糕的是,当它确实崩溃时,在它再次可用之前需要花费很多时间来启动重放预写日志文件。

最终我们 追踪到这些 OOM 的来源 是 Grafana 和 Prometheus 之间的交互,其中 Grafana 将使用 /api/v1/series Prometheus 上的 API 进行查询 {le!=""} (基本上,“给我所有的直方图指标”)。的实现 /api/v1/series 在时间和空间上都是无限的——对于一个有很多结果的查询,这将继续消耗更多的内存和时间。即使在请求者放弃并关闭连接后,它也会继续增长。对我们来说,内存永远不够,普罗米修斯最终会崩溃。我们 修补了 Prometheus 以将此 API 包含在上下文中以强制执行超时,从而完全修复了它。

虽然 Prometheus 崩溃的频率要低得多,但在我们确实需要重新启动它的时候,WAL 重放仍然是一个问题。在 Prometheus 开始收集新指标和服务查询之前,重放所有 WAL 日志通常需要几个小时。在Robust Perception的帮助下 ,我们发现应用 a GOMAXPROCS=24 有很大的改进。Prometheus 在 WAL 重放期间尝试使用所有核心,并且对于具有大量核心的服务器,争用会破坏所有性能。

我们正在探索新的选项来提高我们的监控能力,如以下“未解决的问题”部分所述。

健康检查

对于这么大的集群,我们当然要依靠自动化来检测并从集群中删除行为不端的节点。随着时间的推移,我们已经建立了许多健康检查系统。

被动健康检查

一些健康检查是被动的,总是在所有节点上运行。它们监控基本系统资源,例如网络可达性、坏磁盘或满磁盘,或者 GPU 错误。GPU 以多种不同方式出现问题,但最常见的一种是“无法纠正的 ECC 错误”。Nvidia 的数据中心 GPU 管理器 (DCGM) 工具可以轻松查询此错误和许多其他“Xid”错误。我们跟踪这些错误的一种方法是通过 dcgm-exporter 将指标提取到我们的监控系统 Prometheus 中。这将显示为 DCGM_FI_DEV_XID_ERRORS 指标并设置为最近发生的错误代码。此外,  NVML 设备查询 API 公开了有关 GPU 的运行状况和操作的更多详细信息。

一旦我们检测到错误,通常可以通过重置 GPU 或系统来修复它们,但在某些情况下,这确实会导致底层 GPU 需要进行物理更换。

另一种形式的健康检查跟踪来自上游云提供商的维护事件。每个主要的云提供商都提供了一种方法来了解当前的 VM 是否需要进行最终会导致中断的即将到来的维护事件。VM 可能需要重新启动,以便可以应用底层管理程序补丁或将物理节点换成其他硬件。

这些被动健康检查在所有节点的后台持续运行。如果健康检查开始失败,该节点将自动封锁,因此不会在该节点上安排新的 pod。对于更严重的健康检查失败,我们还将尝试 pod 驱逐以请求所有当前运行的 pod 立即退出。它仍然取决于 pod 本身,可通过 Pod Disruption Budget 进行配置,以决定是否允许这种驱逐发生。最终,在所有 pod 终止后,或者 7 天过去了(我们 SLA 的一部分),我们将强制终止 VM。

主动 GPU 测试

不幸的是,并非所有 GPU 问题都表现为通过 DCGM 可见的错误代码。我们已经建立了自己的测试库,可以使用 GPU 来捕获其他问题并确保硬件和驱动程序按预期运行。这些测试无法在后台运行——它们需要独占使用 GPU 数秒或数分钟才能运行。

我们首先在启动时在节点上运行这些测试,在我们称为“预检”的系统中。所有节点都加入集群,并应用“预检”污点和标签。这种污点会阻止正常的 pod 被调度到节点上。DaemonSet 配置为在具有此标签的所有节点上运行预检测试 pod。成功完成测试后,测试本身会删除污点和标签,然后节点可用于一般用途。

然后,我们还会在节点的生命周期内定期运行这些测试。我们将其作为 CronJob 运行,允许它登陆集群中的任何可用节点。诚然,这对于测试哪些节点有点随机和不受控制,但我们发现随着时间的推移,它提供了足够的覆盖范围,并且协调或中断最少。

配额和资源使用

随着我们扩大集群规模,研究人员开始发现他们很难获得分配给他们的所有容量。传统的作业调度系统有许多不同的功能可用于在竞争团队之间公平地运行工作,这是 Kubernetes 所没有的。随着时间的推移,我们从这些作业调度系统中汲取灵感,并以 Kubernetes 原生方式构建了多项功能。

团队污点

我们在每个集群中都有一个服务,“team-resource-manager”,它具有多种功能。它的数据源是一个 ConfigMap,它为在给定集群中具有容量的所有研究团队指定元组(节点选择器、要应用的团队标签、分配量)。它将此与集群中的当前节点进行协调,用 污染适当数量的节点 openai.com/team=teamname:NoSchedule

team-resource-manager”也有一个 admission webhook 服务,这样在提交每个作业时,都会根据提交者的团队成员资格应用相应的容忍度。使用污点允许我们灵活地约束 Kubernetes pod 调度程序,例如允许对较低优先级 pod 的“任何”容忍,这允许团队借用彼此的容量而无需重量级协调。

CPU 和 GPU 气球

除了使用 cluster-autoscaler 来动态扩展我们的 VM 支持的集群之外,我们还使用它来修复(删除和重新添加)集群中不健康的成员。为此,我们将集群的“最小大小”设置为零,将集群的“最大大小”设置为可用容量。但是,如果 cluster-autoscaler 发现空闲节点,它将尝试缩小到仅需要的容量。由于多种原因(虚拟机启动延迟、预分配成本、上述 API 服务器影响),这种空闲扩展并不理想。

因此,我们为纯 CPU 主机和 GPU 主机引入了气球部署。此 Deployment 包含一个具有“最大大小”数量的低优先级 pod 的 ReplicaSet。这些 pod 占用节点内的资源,因此自动缩放器不会将它们视为空闲。然而,由于它们的优先级较低,调度程序可以立即驱逐它们以为实际工作腾出空间。(我们选择使用 Deployment 而不是 DaemonSet,以避免 DaemonSet 被视为节点上的空闲工作负载。)

需要注意的一件事是,我们使用 Pod 反亲和性来确保 Pod 在节点之间均匀分布。早期版本的 Kubernetes 调度器有一个 (2个) 奥( ñ2个) Pod 反亲和性的性能问题。自 Kubernetes 1.18 以来,此问题已得到纠正。



帮派调度

我们的实验通常涉及一个或多个 StatefulSet,每个 StatefulSet 负责训练工作的不同部分。对于优化器,研究人员需要对 StatefulSet 的所有成员进行调度,然后才能进行任何训练(因为我们经常使用 MPI 在优化器成员之间进行协调,而 MPI 对组成员变化很敏感)。

但是,默认情况下,Kubernetes 不一定优先满足来自一个 StatefulSet 的所有请求。例如,如果两个实验每个都请求 100% 的集群容量,而不是调度一个或另一个实验的全部,Kubernetes 可能只调度每个实验的一半 pod,从而导致两个实验都无法取得进展的死锁。

我们尝试了一些需要自定义调度程序的事情,但遇到了导致与正常 pod 调度方式发生冲突的边缘情况。Kubernetes 1.18 为核心 Kubernetes 调度程序引入了插件架构,使得在本地添加此类功能变得更加容易。我们最近使用 Coscheduling 插件 作为解决这个问题的好方法。

未解决的问题

在我们扩展 Kubernetes 集群的过程中,仍有许多问题需要解决。其中一些包括:

指标

在我们的规模上,我们遇到了很多困难,因为 Prometheus 的内置 TSDB 存储引擎压缩速度很慢,并且需要很长时间才能在重新启动时重播 WAL(预写日志)。查询也往往会导致“查询处理会加载太多样本”错误。我们正在迁移到不同的 Prometheus 兼容存储和查询引擎。期待未来的一篇关于它如何发展的博文!

Pod 网络流量整形

当我们扩展我们的集群时,每个 pod 都被计算为有一定数量的可用互联网带宽。每个人对 Internet 带宽的总需求已经变得很大,我们的研究人员现在有能力无意中对 Internet 上的其他位置造成重大资源压力,例如下载数据集和安装软件包。

结论

我们发现 Kubernetes 是一个非常灵活的平台,可以满足我们的研究需求。它能够扩展以满足我们对其施加的最苛刻的工作负载。尽管它还有很多需要改进的地方,OpenAI 的超级计算团队将继续探索 Kubernetes 如何扩展。