OpenAI 的负载是上千节点规模的并行训练,在过去几年间,OpenAI 的超算团队(Supercomputing team)在维护大规模 k8s 集群积累了丰富经验,这几篇文章主要介绍维护该集群遇到的各种问题和解决方案。
在 OpenAI,算法研发的工作模式常常是用小数据集先做一个小实验(此时通常需要 ssh 到一台机器上,跑一个 1 小时的任务)。如果实验有效,可能会扩容到上千节点上进行大规模训练。
2016 年,OpenAI 有一个自建的 TitanX 物理集群,AWS 也向 OpenAI 捐赠了大量的 GPU 弹性资源。这些节点被划分为 2 部分,一部分用于临时实验,支持 ssh 登录;另一部分通过 Kubernetes 管理起来,集群中包含 3 个可用区的节点。
这个阶段 OpenAI 关注的主要问题是怎么用好 AWS 提供的免费弹性资源。OpenAI 开发了 kubernetes-ec2-autoscaler 来自动化的扩容节点,也是控制器模式的方案。通过 reconcile 对齐当前状态和期望状态的差距,如果节点多了就 drain+termintate,如果少了就 create/uncordon;如果一个可用区资源不够了就换个可用区。k8s 原生的 cluster autoscaler 在 1.8 版本才正式发布(201709)。
为了方便算法团队使用,OpenAI 还开发了一些傻瓜功能。比如支持将本地代码自动打包成镜像(不需要写 dockerfile)、支持通过 flannel 网络直接访问集群中的 tensorboard service。
2018 年,OpenAI 已经拥有多个 k8s 集群,最大规模的是 Azure 上一个 2500 个 node 的集群。这篇文章中记录了 OpenAI k8s 集群从 500 个 node 扩展到 2500 个 node 时遇到的问题和解决方案。这个阶段问题主要出在存储、网络和资源初始化上,解决一个问题的时候往往也会引发一些新的副作用。
- 500 个 node:kubectl 频繁超时,扩展 api server 到 10 个副本依然不能解决问题,通过 datadog 发现 etcd 磁盘写延迟 spike 超过 100ms。将 etcd 底层存储依赖的网络 SSD 切换到本地 SSD,写延迟降低到 200us,该问题得到解决。
- 1000 个 node
- 又发现比较严重的提交延迟,发现 api server 每秒从 etcd 读 500M 数据,list api 被频繁调用。最终定位到是 Fluentd 和 datadog 频繁查询导致,降低查询频率该问题得到解决。
- 到 1000node 的时候 etcd 默认 2G 存储会不够,此时需要修改默认的 quota-backend-bytes。也可以通过将 event 存储到另一个 etcd 集群减少主集群的容量。
- 由于节点都是通过 autoscaler 启动起来的,为了增加减少浪费,快速启动,给调度器增加了 MostRequestedPriority、InterPodAffinityPriority 两个优选策略,保证 pod 可以被优先调度到最常使用的节点、多个 pod 尽量调度到相同的节点(感觉类似于 binpack)。但这个新策略会导致多个 kubeDNS 被调度到相同节点,所以又给 KubeDNS 加了反亲和性策略……【这个需求应该给 job 指定单独的 scheduler,能影响到 kube-system 也是有点费解】
- 为了加快镜像拉取速度,把 kubectl 默认的 serialize-image-pulls 关掉,设置 max-concurrent-downloads,把 docker root 地址也换成了本地的 ssd。但并行拉取会产生一些副作用,比如下载超时(此时需要修改 image-pull-progress-deadline);从 http://gcr.io 拉取 kubelet 镜像被封 ip,通过 docker image load 命令预加载镜像可解决这个问题。【通过 DragonFly 做 p2p 文件分发是更好的解决方法】
- 云上的集群使用 Flannel 网络配置,机间通信万兆带宽,但 pods 间通信只能到 2Gb/s,最后通过打开 hostNetwork,直接使用宿主机网络解决的。【作者说 Azure 上的 Flannel 有问题,自建机房没问题】
- ARP 缓存超限:ARP 表中存储了 IP 地址和 MAC 地址的映射关系,在 HPC 集群里,改大 ARP 限制是常规操作。
2021 年 OpenAI 集群规模达到 7500 个节点,在这个基础架构上诞生了 GPT-3,CLIP,DALL·E 大模型。
这个规模的集群已经比较罕见了,维护需要一定技巧。但由于运行的负载是大规模分布式训练任务,很多问题不用过多考虑。
- 调度:一般一个 pod 占用一个 node 所有资源;因此不需要考虑 NUMA、binpack 等调度策略。网络是二分带宽,不需要考虑网络拓扑。调度逻辑比较简单,scheduler 压力不大。
- 服务状态:一段时间记录一次 checkpoint,挂了重启可以恢复,因此是 semi-stateful 的。
- 网络负载:基本没有 https 请求,主要是 MPI 和 SSH。因此 service mesh 那一套基本用不上。
- 存储:用的最多的是 blob storage,可扩展性比较强。PV 和 posix 语义的系统用的较少。
在扩展到 7500 节点途中,主要遇到了以下几个方面问题:
- 网络:Flannel 性能已经达不到要求了,直接使用了 Azure 的 VMSSes CNI 插件。在 iptables 里增加了 mangle 规则,给流量打上 tag,通过 iptables-exporter 上报到 prometheus 监控起来。
- API Server:etcd 和 API servers 部署到各自单独的服务器上,各部署了 5 个节点。
- 稳定性:API server 做了故障自愈,etcd 没做,也很少出问题。
- 内存:7500 规模的集群 API server 每个节点占了 75G 内存,未来集群规模继续扩展,硬件层面也能 hold 住。
- Watch 请求:watch 量大概是 N^2,大概 1GB/ s 得量级。通过 EndpointSlice 可以将负载降低 1000 倍。
- Cluster Autoscaler 加节点不能太快,经验值是一次加上百个节点就会把 API server 打爆。
- Prometheus 和 Grafana
- Prometheus 监控的数据太细,为了减少数据量,可以通过 Prometheus rules 丢掉一些。
- 使用过程中发现了导致 Prometheus OOM 的 bug,修了,感兴趣可以看原文。但更严重的问题是每次重启,Prometheus 都会花很久重放 WAL,调大 GOMAXPROCS=24 有一定帮助。
- 健康检查:使用了比较激进的健康检查策略
- 通过 dcgm-exporter 监听 ECC 等 xid 错误错误,或者使用 nvml device query api 查询。发现不健康就把节点置为 cordoned,严重的时候也可以通过 Pod Disruption Budget 把 pod 都停了。
- 初始化机器之后,先给机器加上污点,用 deamonset 做 preflight 测试,通过之后把污点摘掉。
- 资源分配
- 由于多个团队同时在使用,可以通过污点把节点分给不同的团队。低优先级 pod 可以加比较大的容忍策略,借用其他节点的资源,有更高优先级 pod 的时候被抢占。
- Cluster autoscaler 扩容太慢,主要慢在云厂商初始化服务器上,因此 OpenAI 使用一种叫 balloons 的策略。简单的说就是用一个低优先级 pod 占着整个 node,要用节点的时候被自动驱逐掉。
- Gang Scheduling:通过 k8s 1.18 发布的 Coscheduling plugin 解决。
未解决的问题:
- Prometheus 内置的 TSDB 重放 WAL 的问题,打算迁移到其他兼容 Prometheus 的数据库上。【WAL 没用了是不是应该定时清理?】
- 流量管控,集群规模太大很容易对互联网上其他服务发起 ddos。
维护一个超大规模算力集群需要掌握通过各种工具链发现问题解决问题的技巧,需要对存储、网络、etcd 和 k8s 有深刻理解。OpenAI 的以上经验可以通过 etcd->api server->cri->csi->cni->scheduler->cluster autoscaler->observable 的顺序思考理顺。
原文链接:https://zhuanlan.zhihu.com/p/622199079