Pollux: Co-adaptive Cluster Scheduling for Goodput-Optimized Deep Learning

现有的深度学习调度器程序要求提交作业的用户指定各种超参数,如果设置得不正确,这些参数会大大降低作业性能和资源效率。在这些参数中,batch size和learning rate非常依赖于其资源分配(GPU数量),这使得在共享资源环境中很难预先确定这些参数。此外,深度学习任务能否有效利用的资源分配不仅取决于所训练模型的结构,而且还取决于batch size和learning rate。资源数量、batch size、learning rate之间的这种相互依赖性,使得用户必须同时考虑到这些因素才能配置其作业以实现高效执行和资源利用。

从根本上讲,一个有效配置的深度学习任务需要在两种期望之间取得trade off:(1)系统吞吐量,即每个单位时间处理的训练实例的数量,以及(2)统计效率,即每个处理的训练实例所取得的进展量。如图1a所示,可以通过增加batch size来增加系统吞吐量。较大的batch size可以提高更多资源的利用率(例如,较大数量的GPU)。但是,当batch size增加时,必须重新调整learning rate。否则,统计效率将会降低,使得总训练时间不会更短,从而浪费额外分配的GPU。

图1:Batch size、resource scalability、stage of training之间的trade-off(ResNet18 on CIFAR-10,为每一个batch size调整了learning rate)

本文提出了一种混合资源调度器Pollux,可以在为共享集群中的每个深度学习任务调整batch size和learning rate的同时协同适应地分配资源。这篇工作由CMU和磐腾科技合作完成,获得OSDI’21会议的best paper。

然而,调度器或许不应该任意调整用户指定的batch size,尤其是在调整超参数的任务中,这一行为是不被允许的。

背景

深度训练任务有两种衡量指标:System Throughput和Statistical Efficiency。
系统吞吐量(System Throughput)是每单位时间处理的训练样本数。当一个DL任务分布在多个节点上时,其系统吞吐量由几个因素决定,包括(1)分配给该作业的资源的分配和放置,(2)分布式执行和同步的方法,以及(3)SGD算法使用的batch size大小。由于深度学习任务的scalibility是次线性的(sublinear),使用太多数量的GPU不一定会提升系统吞吐量。通常我们可以使用更大的batch size来提升系统的吞吐量。

然而,我们也不能一味地增加batch size,因为在增大batch size的同时,统计效率(Statistical Efficiency)会下降。统计效率是每处理一单位数据所取得的训练进度,当统计效率下降时,训练所需要的epoch数量会增加。最佳的训练效果需要在系统吞吐量和统计效率之间权衡。

DL训练的Goodput

本文定义了goodput:在第t个iteration的goodput为系统吞吐量和统计效率的乘积。

$$
GOODPUT_t(a,m) = THROUGHPUT(a,m) * EFFICIENCY_t(m)
$$

其中,$a \in R^N$是allocation vector,$a_n$是从节点n分配的GPU数量,$m$是batch size。

一个DL任务在使用batch size m ≥ m0时的统计效率是相对于用m0来说,每用m个训练样本所获得的训练进度。
This quantity can be framed in terms of the gradient noise scale φt .
统计效率可以被计算为:

$$
EFFICIENCY_t(m) = r_tm_0/m = (\phi_t+m_0)/(\phi_t+m)
$$

Pollux架构与设计

  1. PolluxAgent对每个任务所给定的GPU资源分配结果,找到能够最大化有效吞吐量的batch size和梯度累积步数。Pollux使用用户指定的learning rate调整规则来根据batch size调整learning rate。
  2. PolluxSched根据集群中每个任务的有效吞吐量,为每一个任务找到合适的GPU资源分配方案。
图2:Pollux的Co-adaptive scheduling架构

实验评估

作者在16个节点、没个节点有四块NVIDIA T4 GPU的集群上进行了实验评估,使用的job trace包含微软公开的Philly集群job trace中最繁忙的8个小时内的160个job。Pollux可以将训练所需时间缩短37-50%。

表1:实验结果总结

通过图3中(A)处可以看出,在集群中的资源较紧张的时候,Pollux会使用更大的batch size以提升系统吞吐量,此时统计效率较低。在此之后,通过(B)可以看出,Pollux会给任务减少GPU资源的分配,使用较小的batch size,从而提升统计效率。

图3:ImageNet训练任务中Pollux与baseline的实验结果比较

除了集群调度之外,Pollux还可以应用于云端弹性训练、超参数搜索等场景。

总结

Pollux通过优化Goodput,协同调整集群和任务的参数。Pollux 基于对有用作业完成进度提出更有意义的衡量指标,来提升深度学习作业竞争资源的公平性,并揭示了在云环境下降低深度学习成本具有新机会。


代码阅读笔记

Pollux / AdaptDL 部署 (该Section内容主要来自我的好伙伴xxt)

文档:https://adaptdl.readthedocs.io/en/latest/index.html

Step 0:Deploying MicroK8s for AdaptDL

首先安装snap,之后用snap安装microk8s,这里大概率需要用到代理

$ sudo snap set system proxy.http="代理地址"
$ sudo snap set system proxy.https="代理地址"

$ sudo snap install microk8s --classic --channel=1.18/stable

microk8s装好后,依次按文档执行都没有什么问题

$ sudo microk8s enable dns
$ sudo microk8s enable gpu storage
$ sudo microk8s enable helm
$ sudo microk8s helm init --stable-repo-url=https://charts.helm.sh/stable
$ sudo helm repo add stable https://charts.helm.sh/stable   

接下来运行

$ sudo microk8s.kubectl get nodes
NAME       STATUS   ROLES    AGE   VERSION
xinjin-1   Ready    <none>   20d   v1.18.20     

之后按文档可以为了省去sudo,以及用kubectl代替microk8s.kubectl,需要执行如下命令。

$ sudo usermod -a -G microk8s $USER
$ mkdir -p $HOME/.kube
$ sudo microk8s kubectl config view --raw > $HOME/.kube/config
$ sudo chown -f -R $USER ~/.kube    

这些由于服务器上的$HOME路径改动过,需执行以下命令后生效

sudo dpkg-reconfigure apparmor
sudo rm -f /etc/apparmor.d/cache/* /var/cache/apparmor/snap.*
sudo reboot

Step 1:Installing the AdaptDL Scheduler

按文档运行

$ helm install adaptdl adaptdl-sched --repo https://github.com/petuum/adaptdl/raw/helm-repo --namespace adaptdl --create-namespace --set docker-registry.enabled=true

随后执行

$ kubectl get pods -n adaptdl

发现STATUS并非Running,而是ContainerCreating,查看日志发现是缺少一些镜像,可以手动拉取,可以参考:https://www.cnbugs.com/post-3276.html

补全pause和tiller镜像后,问题自动解决,状态变为STATUS。

$ kubectl get pods -n adaptdl
NAME                                     READY   STATUS    RESTARTS   AGE
adaptdl-adaptdl-sched-67c7bf5b59-597bj   3/3     Running   71         15d
adaptdl-registry-864758d854-757s6        1/1     Running   23         15d
adaptdl-validator-66ff95d99c-wsnkd       1/1     Running   23         15d

Step 2: Submitting a Simple Job

接下来尝试用命令行工具运行adaptdl job。

首先,运行python3 -m pip install adaptdl-cli,接着创建文件夹hello_world并将以下内容复制到hello_world/hello_world.py

1
2
3
4
5
6
7
8
9
10
import adaptdl.env
import os
import time

print("Hello, world!")

with open(os.path.join(adaptdl.env.share_path(), "foo.txt"), "w") as f:
f.write("Hello, world!")

time.sleep(100)

接着,创建hello_world/Dockerfile,内容如下

1
2
3
4
5
6
FROM python:3.7-slim
RUN python3 -m pip install adaptdl

COPY hello_world.py /root/hello_world.py

ENV PYTHONUNBUFFERED=true

因为在提交步骤中出现

python3: error while loading shared libraries: libpython3.7m.so.1.0: cannot open shared object file: No such file or directory

经排查发现是docker中缺少动态链接地址的原因,在Dockerfile中加上了ENV LD_LIBRARY_PATH=:/usr/local/lib 后恢复正常.

最后,拷贝以下内容至hello_world/adaptdljob.yaml

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: adaptdl.petuum.com/v1
kind: AdaptDLJob
metadata:
generateName: hello-world-
spec:
template:
spec:
containers:
- name: main
command:
- python3
- /root/hello_world.py

运行adaptdl submit hello_world后,任务提交成功,可以通过文档后续的命令查看输出并观察执行情况。

如果提交后出现ModuleNotFoundError,可以尝试运行:

1
2
pip install mitmproxy==6.0.2
pip install markupsafe==2.0.1

Pollux Testbed Benchmark复现

因为我没有用aws的机器,所以复现的过程极其坎坷,在此记录一下。

环境配置与调度器的安装

最开始以为按照文档配好调度器就可以了,但是调度的时候遇到PVC claim的问题。

然后发现了benchmark目录下的main.tf文件,应该是master和worker都需要按照文件里面的remote-exec配置好环境。所以卸载了所有安装的东西重新配环境。

在执行一些kubectl apply -f的时候遇到了很多:

1
Unable to retrieve Cluster Version or Type: resource does not exist: XXX(default) with error: the server 

经搜索发现可能是kubernetes的版本太高了导致的,把kubernetes的版本降低到v1.21.0就解决了这些问题了。

配置好环境后执行python run_workload.py pollux workloads/workload-6.csv,遇到问题:

1
2
The push refers to repository [localhost:32000/pollux]
Get "http://localhost:32000/v2/": dial tcp 127.0.0.1:32000: connect: connection refused

经查看,端口32000是scheduler使用的,所以应该还是要先安装上scheduler才能跑benchmark。目前我还不知道如何用源码安装scheduler,所以暂时用了helm安装:

1
2
sudo helm repo add stable https://charts.helm.sh/stable
sudo helm install adaptdl adaptdl-sched --repo https://github.com/petuum/adaptdl/raw/helm-repo --namespace default --set docker-registry.enabled=true

在执行了安装scheduler的命令后,发现scheduler不能正常启动,都是pending状态:

这是因为当前master节点带有taint(可以通过kubectl describe node <node>查看taint),这些tain后带有NoSchedule的标记,所以调度器找不到可以调度的资源了。我当时看到了两个taint,一个taint是说当前节点为master,另一个是Disk pressure。可以用kubectl taint nodes --all <taint>-命令来去掉所有taint。但是有disk pressure的话后续还是会失败,清理磁盘到/目录的使用率在80%以下就没问题了。

调度器终于是running的状态之后,发现还是有失败的pod:

1
2
3
4
5
6
$ kubectl describe pod copy-7pc4z
Warning FailedScheduling 115s default-scheduler 0/1 nodes are available: 1 pod has unbound immediate PersistentVolumeClaims.
$ kubectl get pvc # the status of the PVC is pending
$ kubectl describe pvc pollux
Normal Provisioning 3m35s (x27 over 92m) rook-ceph.cephfs.csi.ceph.com_csi-cephfsplugin-provisioner-64d4468bbf-rwrfl_9d4dda0e-3e39-47e5-a5b5-b66b886b22d8 External provisioner is provisioning volume for claim "default/pollux"
Normal ExternalProvisioning 3m1s (x361 over 93m) persistentvolume-controller waiting for a volume to be created, either by external provisioner "rook-ceph.cephfs.csi.ceph.com" or manually created by system administrator

这居然是因为开源的artifact里面没有为PVC创建相应的PV!我只好把所有代码里涉及到PVC的目录全部改成hardcode的目录(/mnt),删掉创建PVC的pod和已经创建了的PVC,这样PVC就没有问题了。

然后发现提交了的adaptdljobs一直在pending,没有跑这些job的pod,但是可以通过kubectl get adaptdljobs看到这些job。kube describe没有报错信息。只能去查看scheduler pod的log。

1
kubectl logs adaptdl-adaptdl-sched-cbc794b8f-br79v -c allocator 

发现log里有好多的信息,而且node上居然没有GPU这种资源类型:

可能是GPU没有被scheduler发现。但是之前已经安装过nvidia的k8s plugin了。这种情况下只能去kube-system namespace下看看pod有什么问题。这个namespace下的所有pod都是正常运行的状态,所以要去看这些pod的log。

1
2
3
4
5
6
$ kubectl logs nvidia-device-plugin-daemonset-n7f7q -n kube-system
2022/05/27 07:25:44 Loading NVML
2022/05/27 07:25:44 Failed to initialize NVML: could not load NVML library.
2022/05/27 07:25:44 If this is a GPU node, did you set the docker default runtime to `nvidia`?
2022/05/27 07:25:44 You can check the prerequisites at: https://github.com/NVIDIA/k8s-device-plugin#prerequisites
2022/05/27 07:25:44 You can learn how to set the runtime at: https://github.com/NVIDIA/k8s-device-plugin#quick-start

按照最后一行的link修改了/etc/docker/daemon.json文件并重启了docker service(systemctl restart docker),job终于是running状态了!

后来,在全新的环境里配置kubernetes的时候遇到问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
$ sudo kubeadm init --pod-network-cidr=192.168.0.0/16 --v=5
I0615 07:02:56.149065 39512 initconfiguration.go:115] detected and using CRI socket: /var/run/dockershim.sock
I0615 07:02:56.149317 39512 interface.go:431] Looking for default routes with IPv4 addresses
I0615 07:02:56.149328 39512 interface.go:436] Default route transits interface "eth0"
I0615 07:02:56.149899 39512 interface.go:208] Interface eth0 is up
I0615 07:02:56.149966 39512 interface.go:256] Interface "eth0" has 2 addresses :[10.5.0.7/24 fe80::20d:3aff:fe49:1b40/64].
I0615 07:02:56.149982 39512 interface.go:223] Checking addr 10.5.0.7/24.
I0615 07:02:56.149987 39512 interface.go:230] IP found 10.5.0.7
I0615 07:02:56.150002 39512 interface.go:262] Found valid IPv4 address 10.5.0.7 for interface "eth0".
I0615 07:02:56.150007 39512 interface.go:442] Found active IP 10.5.0.7
I0615 07:02:56.245405 39512 version.go:185] fetching Kubernetes version from URL: https://dl.k8s.io/release/stable-1.txt
I0615 07:02:56.587172 39512 version.go:254] remote version is much newer: v1.24.1; falling back to: stable-1.21
I0615 07:02:56.587215 39512 version.go:185] fetching Kubernetes version from URL: https://dl.k8s.io/release/stable-1.21.txt
[init] Using Kubernetes version: v1.21.13
[preflight] Running pre-flight checks
I0615 07:02:56.929034 39512 checks.go:582] validating Kubernetes and kubeadm version
I0615 07:02:56.929090 39512 checks.go:167] validating if the firewall is enabled and active
I0615 07:02:56.937774 39512 checks.go:202] validating availability of port 6443
I0615 07:02:56.937886 39512 checks.go:202] validating availability of port 10259
I0615 07:02:56.937905 39512 checks.go:202] validating availability of port 10257
I0615 07:02:56.937926 39512 checks.go:287] validating the existence of file /etc/kubernetes/manifests/kube-apiserver.yaml
I0615 07:02:56.937939 39512 checks.go:287] validating the existence of file /etc/kubernetes/manifests/kube-controller-manager.yaml
I0615 07:02:56.937949 39512 checks.go:287] validating the existence of file /etc/kubernetes/manifests/kube-scheduler.yaml
I0615 07:02:56.937954 39512 checks.go:287] validating the existence of file /etc/kubernetes/manifests/etcd.yaml
I0615 07:02:56.937964 39512 checks.go:437] validating if the connectivity type is via proxy or direct
I0615 07:02:56.937981 39512 checks.go:476] validating http connectivity to first IP address in the CIDR
I0615 07:02:56.937999 39512 checks.go:476] validating http connectivity to first IP address in the CIDR
I0615 07:02:56.938010 39512 checks.go:103] validating the container runtime
I0615 07:02:57.019188 39512 checks.go:129] validating if the "docker" service is enabled and active
[WARNING IsDockerSystemdCheck]: detected "cgroupfs" as the Docker cgroup driver. The recommended driver is "systemd". Please follow the guide at https://kubernetes.io/docs/setup/cri/
I0615 07:02:57.117386 39512 checks.go:336] validating the contents of file /proc/sys/net/bridge/bridge-nf-call-iptables
I0615 07:02:57.117456 39512 checks.go:336] validating the contents of file /proc/sys/net/ipv4/ip_forward
I0615 07:02:57.117489 39512 checks.go:654] validating whether swap is enabled or not
I0615 07:02:57.117530 39512 checks.go:377] validating the presence of executable conntrack
I0615 07:02:57.117575 39512 checks.go:377] validating the presence of executable ip
I0615 07:02:57.117600 39512 checks.go:377] validating the presence of executable iptables
I0615 07:02:57.117625 39512 checks.go:377] validating the presence of executable mount
I0615 07:02:57.117650 39512 checks.go:377] validating the presence of executable nsenter
I0615 07:02:57.117670 39512 checks.go:377] validating the presence of executable ebtables
I0615 07:02:57.117691 39512 checks.go:377] validating the presence of executable ethtool
I0615 07:02:57.117712 39512 checks.go:377] validating the presence of executable socat
[WARNING FileExisting-socat]: socat not found in system path
I0615 07:02:57.117753 39512 checks.go:377] validating the presence of executable tc
I0615 07:02:57.117774 39512 checks.go:377] validating the presence of executable touch
I0615 07:02:57.117795 39512 checks.go:525] running all checks
I0615 07:02:57.209961 39512 checks.go:408] checking whether the given node name is valid and reachable using net.LookupHost
I0615 07:02:57.210512 39512 checks.go:623] validating kubelet version
I0615 07:02:57.270497 39512 checks.go:129] validating if the "kubelet" service is enabled and active
I0615 07:02:57.279877 39512 checks.go:202] validating availability of port 10250
I0615 07:02:57.279947 39512 checks.go:202] validating availability of port 2379
I0615 07:02:57.279966 39512 checks.go:202] validating availability of port 2380
I0615 07:02:57.279985 39512 checks.go:250] validating the existence and emptiness of directory /var/lib/etcd
[preflight] Some fatal errors occurred:
[ERROR FileExisting-conntrack]: conntrack not found in system path
[preflight] If you know what you are doing, you can make a check non-fatal with `--ignore-preflight-errors=...`
error execution phase preflight
k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow.(*Runner).Run.func1
/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow/runner.go:235
k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow.(*Runner).visitAll
/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow/runner.go:421
k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow.(*Runner).Run
/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/cmd/phases/workflow/runner.go:207
k8s.io/kubernetes/cmd/kubeadm/app/cmd.newCmdInit.func1
/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/cmd/init.go:152
k8s.io/kubernetes/vendor/github.com/spf13/cobra.(*Command).execute
/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/spf13/cobra/command.go:850
k8s.io/kubernetes/vendor/github.com/spf13/cobra.(*Command).ExecuteC
/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/spf13/cobra/command.go:958
k8s.io/kubernetes/vendor/github.com/spf13/cobra.(*Command).Execute
/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/vendor/github.com/spf13/cobra/command.go:895
k8s.io/kubernetes/cmd/kubeadm/app.Run
/workspace/src/k8s.io/kubernetes/_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/app/kubeadm.go:50
main.main
_output/dockerized/go/src/k8s.io/kubernetes/cmd/kubeadm/kubeadm.go:25
runtime.main
/usr/local/go/src/runtime/proc.go:225
runtime.goexit
/usr/local/go/src/runtime/asm_amd64.s:1371

是因为没有安装conntrack:

1
apt install conntrack

作业的运行

虽然job是running状态,但是job的一些行为十分诡异,需要注意解决。

问题1 OSError: [Errno 30] Read-only file system: ‘/mnt/tensorboard’

在job还在运行的时候,具体的job的log可以通过kubectl logs 看到。
通过log发现:OSError: [Errno 30] Read-only file system: '/mnt/tensorboard/'
无论是在docker外面还是里面,检查/mnt下的目录的权限,都是所有人可读写的,甚至这个目录存在的时候,会出现在里面写文件失败的情况,也是Read-only file system这种报错。
最后发现是每一个job的adaptdljob.yaml里有这样的一段:

1
2
3
4
volumeMounts:
- name: data
mountPath: /mnt
readOnly: true

把true改成false,删掉daemonsets,删掉build好的container镜像,然后重新运行脚本build镜像就没有这个报错了。

问题2 把模型放到GPU内存里过程缓慢,需要10min,且训练卡死在forward阶段

经过profile发现net = net.to(device)需要10min之久,且训练会卡死在第一个outputs = net(inputs)。GPU的内存有一定的使用,但是计算上的利用率一直是0%。

经过百度发现可能是CUDA、cuDNN、python等版本不匹配的问题。然后我发现在container里面,`torch.version.cuda`的CUDA版本是10.X,但是`nvidia-smi`得到的CUDA版本是11.X,而在container外面,两种方式得到的CUDA版本都是11.X,所以问题一定出在了container的环境上。把每一种job的Dockerfile第一行的镜像版本都改成`nvcr.io/nvidia/pytorch:21.10-py3`之后,还是删掉daemonsets,删掉build好的container镜像,然后重新运行脚本build镜像,一些都变得正常了。

后来在全新的环境里,又遇到了全新的问题,https://github.com/petuum/adaptdl/issues/124

Simulator代码阅读

  • simulator/traces/model/placements.csv文件的含义,可以通过simulator/application.py看到
    • 第一列的字符串,有几个字符,就相当于有几个node
    • 每个数字代表这个node上有几块GPU在用

原文作者:Aurick Qiao, Sang Keun Choe, Suhas Jayaram Subramanya, Willie Neiswanger, Qirong Ho, Hao Zhang, Gregory R. Ganger, Eric P. Xing
原文链接:https://arxiv.org/pdf/2008.12260v2.pdf
项目代码:https://github.com/petuum/adaptdl
参考文献:[1] 计算机系统软件顶会OSDI 2021最佳论文出炉,邢波团队研究入选, https://zhuanlan.zhihu.com/p/390040932