文|王勤龙 (花名:长凡)
蚂蚁集团 AI 系统工程师
ChaosBlade 是阿里巴巴开源的一款遵循混沌工程原理和混沌实验模型的实验注入工具,可以用于验证云原生系统的稳定性。DLRover 作为云原生的分布式训练系统,提供了弹性和容错功能来提升分布式训练的稳定性。为此我们使用 ChaosBlade 创建各种混沌实验来验证 DLRover 弹性容错的稳定性。
前提:创建 k8s 集群并部署 DLRover ElasticJob
- 创建 k8s 集群,且本地可以通过 Kubectl 访问该集群。后面的实验中,我们使用的是阿里云的 ACK 集群。
- 在 k8s 集群上部署 DLRover ElasticJob,可以参考文档。
- 制作训练镜像,并在镜像中安装 chaosblade,可以参考 Dockerfile。同时我们也提供了训练 mnist 的镜像,registry.cn-hangzhou.aliyuncs.com/intell-ai/dlrover:torch201-mnist,下面实验我们将使用该镜像。
Python 分布式训练弹性容错
我们将实验模拟如下场景来验证 DLRover 分布式训练的弹性容错功能:
- 训练 Pod 被抢占或者驱逐。
- 训练 Pod 是一个慢节点。
- 训练 Pod 被调度在了一个故障机上。
- 训练过程中,训练节点的网络故障。
- 训练 Pod 中训练进程崩溃。
- 训练自动扩缩容。
训练 Pod 被抢占
此实验中,我们使用 MNIST 例子 来验证 DLRover 可以恢复被抢占的 Pod。我们将作业 yaml 中的 command 替换成如下命令:
less
command:
- /bin/bash
- -c
- "dlrover-run --network-check --exclude-straggler --nnodes=3:$WORKER_NUM \
--nproc_per_node=2 --max_restarts=3 --rdzv_conf pend_timeout=600 \
examples/pytorch/mnist/cnn_train.py --num_epochs 5 \
--training_data /data/mnist_png/training/ \
--validation_data /data/mnist_png/testing/"
提交一个 4 节点的作业
bash
kubectl -n dlrover apply -f examples/pytorch/mnist/chaos_test_job.yaml
提交作业后,我们通过 kubectl -n dlrover get pods
可以看到如下 Pod:
sql
chaos-test-edljob-worker-0 1/1 Running 0 85s
chaos-test-edljob-worker-1 1/1 Running 0 85s
chaos-test-edljob-worker-2 1/1 Running 0 85s
chaos-test-edljob-worker-3 1/1 Running 0 85s
elasticjob-chaos-test-dlrover-master 1/1 Running 0 89s
我们手动删除一个 Pod 来模拟 Pod 被抢占。
arduino
kubectl -n dlrover delete pod chaos-test-edljob-worker-0
我们看到 worker-0 被删除了,然后新的 worker-4 启动了来恢复被删除的 worker-0。
sql
chaos-test-edljob-worker-1 1/1 Running 0 2m3s
chaos-test-edljob-worker-2 1/1 Running 0 2m3s
chaos-test-edljob-worker-3 1/1 Running 0 2m3s
chaos-test-edljob-worker-4 1/1 Running 0 30s
elasticjob-chaos-test-dlrover-master 1/1 Running 0 2m7s
通过 worker-1 的日志,我们可以看到训练继续进行
ini
kubectl -n dlrover logs chaos-test-edljob-worker-1
>>>
loss = 2.298487901687622, step = 0
loss = 2.195965051651001, step = 20
loss = 1.2307546138763428, step = 40
loss = 0.6579511761665344, step = 60
loss = 1.0608341693878174, step = 80
loss = 0.7761049270629883, step = 100
训练节点是慢节点
为了模拟训练节点中有个节点是慢节点,我们使用 ChaosBlade 来将一个 Pod 的 CPU 负载提升到 90%.
lua
blade create cpu load --cpu-percent 90
我们将 MNIST 例子 yaml 文件中的 command 替换成下面的 command。其中,start_chaos.sh cpu-overload
会将 worker-1 的 CPU 负载提升到 90%,使其成为慢节点。
less
command:
- /bin/bash
- -c
- "(bash examples/pytorch/mnist/start_chaos.sh cpu-overload &) && \
dlrover-run --network-check --exclude-straggler --nnodes=3:$WORKER_NUM \
--nproc_per_node=2 --max_restarts=3 --rdzv_conf pend_timeout=600 \
examples/pytorch/mnist/cnn_train.py --num_epochs 5 \
--training_data /data/mnist_png/training/ \
--validation_data /data/mnist_png/testing/"
然后使用 kubectl -n dlrover apply -f examples/pytorch/mnist/choas_test_job.yaml
提交一个作业。Pod 如下:
lua
elasticjob-torch-mnist-debug-dlrover-master 0/1 Completed 0 3h17m
torch-mnist-debug-edljob-worker-0 0/1 Completed 0 3h17m
torch-mnist-debug-edljob-worker-1 0/1 Error 0 3h17m
torch-mnist-debug-edljob-worker-2 0/1 Completed 0 3h17m
torch-mnist-debug-edljob-worker-3 0/1 Completed 0 3h17m
torch-mnist-debug-edljob-worker-4 0/1 Completed 0 3h10m
可以看到,worker-1 报错退出了。从worker-1 的日志可以看到,worker-1 因为是慢节点而退出
arduino
[2023-09-26 03:52:20,235] [INFO] [training.py:707:run] Fault nodes are: [] and stragglers are: [1].
Traceback (most recent call last):
File "/usr/local/bin/dlrover-run", line 8, in <module>
sys.exit(main())
...
File "/usr/local/lib/python3.8/site-packages/dlrover/python/elastic_agent/torch/training.py", line 733, in run
raise RuntimeError("The node is a straggler and exits.")
RuntimeError: The node is a straggler and exits.
这是因为这个作业开启了节点检查和慢节点自动报错退出的功能,dlrover --network-check --exclude-straggler
。如果不想让慢节点报错退出,可以去掉--exclude-straggler
。节点检查时, dlrover-run
让每个节点执行一个简单的矩阵乘法和 allgather 任务并统计耗时。
同时我们可以从 job master 节点 elasticjob-torch-mnist-debug-dlrover-master
的日志中查看各个节点执行网络检测任务的耗时。
yaml
kubectl -n dlrover logs elasticjob-torch-mnist-debug-dlrover-master | grep elapsed
>>>
Round 0: The node elapsed time are {2: 20.307, 3: 20.265, 0: 206.872, 1: 151.752}
Round 1: The node elapsed time are {2: 20.307, 3: 20.265, 0: 23.174, 1: 135.961}
Round 2: The node elapsed time aree {2: 21.491, 0: 22.685, 3: 20.889, 1: 23.097}
DLRover 的每次网络检测分为两轮,可以看到经过前两轮检测,worker-1 的耗时远高于其他节点。在 worker-1 报错退出后,DLRover 重启了 worker-4 来替换 worker-1。worker-4 是正常节点,在网络检测中,耗时基本与其他节点相近,所以没有了慢节点影响。
训练 Pod 被调度在故障机上
如果训练 Pod 调度在故障机上,比如集群的 GPU 卡故障了,那么训练进程是无法启动的。为了模拟故障机,我们使用 ChaosBlade 来终止 PyTorch 的子进程,然子进程报错退出。我们将 mnist 例子 yaml 文件中的 command 替换成如下 command。
less
command:
- /bin/bash
- -c
- "(bash examples/pytorch/mnist/start_chaos.sh kill-process &) && \
dlrover-run --network-check --exclude-straggler --nnodes=3:$WORKER_NUM \
--nproc_per_node=2 --max_restarts=3 --rdzv_conf pend_timeout=600 \
examples/pytorch/mnist/cnn_train.py --num_epochs 5 \
--training_data /data/mnist_png/training/ \
--validation_data /data/mnist_png/testing/"
start_chaos.sh kill-process
会在 worker-1 中 kill 掉 dlrover-run
启动的网络检测子进程。从而模拟 worker-1 是故障机,即故障机上无法正常启动 GPU 进程。提交作业后,我们可以看到 worker-1 报错退出,并重启了 worker-4,而 worker-4 是正常节点。
sql
chaos-test-edljob-worker-0 1/1 Running 0 12m
chaos-test-edljob-worker-1 0/1 Error 0 12m
chaos-test-edljob-worker-2 1/1 Running 0 12m
chaos-test-edljob-worker-3 1/1 Running 0 12m
chaos-test-edljob-worker-4 1/1 Running 0 3m59s
elasticjob-chaos-test-dlrover-master 1/1 Running 0 12m
通过查看 worker-1 的日志,我们可以看到 worker-1 是因为故障机而退出的。
arduino
Traceback (most recent call last):
....
File "/usr/local/lib/python3.8/site-packages/dlrover/python/elastic_agent/torch/training.py", line 732, in run
raise RuntimeError("The node network is breakdown.")
RuntimeError: The node network is breakdown.
同时我们可以从 job master 节点elasticjob-torch-mnist-debug-dlrover-master
的日志中查看各个节点网络检测的结果。每次检测分为两次,我们可以看到 worker-1 在前两轮的检查结果都是失败,故其报错退出。worker-1 退出后,新启动的 worker-4 不是故障机节点,所以检查通过,开始训练模型。
yaml
Round 1: The node status are {1: False, 2: True, 3: True, 0: False}.
Round 2: The node status are {1: False, 2: True, 3: True, 0: True}.
Round 3: The node status are {3: True, 0: True, 1: True, 2: True}.
训练过程中 Pod 网络故障
此实验中,我们首先启动一个正常的训练作业,我们将 mnist 例子 yaml 文件中的 command 替换成如下 command。
less
command:
- /bin/bash
- -c
- "(bash examples/pytorch/mnist/start_chaos.sh no-chaos &) && \
dlrover-run --network-check --exclude-straggler --nnodes=3:$WORKER_NUM \
--nproc_per_node=2 --max_restarts=3 --rdzv_conf pend_timeout=600 \
examples/pytorch/mnist/cnn_train.py --num_epochs 5 \
--training_data /data/mnist_png/training/ \
--validation_data /data/mnist_png/testing/"
待 worker-1 的日志中出现模型训练的loss 信息后,我们进入到 worker-1 中,使用 chaosblade 让其网络丢包率为 100%,制造网络故障。
bash
kubectl -n dlrover exec -it chaos-test-edljob-worker-1 bash
./chaosblade-1.7.2/blade create network loss --percent 100 --interface eth0
然后,我们看到 worker-1 会报错退出,且 worker-4 启动了。
sql
chaos-test-edljob-worker-0 1/1 Running 0 4m39s
chaos-test-edljob-worker-1 0/1 Error 0 4m39s
chaos-test-edljob-worker-2 1/1 Running 0 4m39s
chaos-test-edljob-worker-3 1/1 Running 0 4m39s
chaos-test-edljob-worker-4 1/1 Running 0 17s
elasticjob-chaos-test-dlrover-master 1/1 Running 0 4m43s
然后通过 worker-0 的日志,我们可以看到训练恢复继续进行。
ini
loss = 0.24101698398590088, step = 0
loss = 0.4646361768245697, step = 20
训练进程崩溃
此实验中,我们先启动一个正常的训练作业,然后通过 kill -9
来杀掉一个进程,观察训练是否恢复。我们需要将 MNIST 例子 yaml 文件 command 中的首行命令替换为 (bash examples/pytorch/mnist/start_chaos.sh no-chaos &) &&
,然后启动作业。
perl
kubectl -n dlrover exec -it chaos-test-edljob-worker-1 bash
ps -aux | grep cnn_train.py
待 worker-1 的日志中出现模型训练的loss 信息后,我进入 Pod 中找到训练进程。然后我们通过 kill -9 ${PID}
来终止任意训练进程。通过查看 Pod 状态,可以看到 Pod 并没报错退出。说明训练在继续进行。因为 dlrover-run
会在 Pod 里重启子进程。
sql
chaos-test-edljob-worker-0 1/1 Running 0 3m4s
chaos-test-edljob-worker-1 1/1 Running 0 3m4s
chaos-test-edljob-worker-2 1/1 Running 0 3m4s
chaos-test-edljob-worker-3 1/1 Running 0 3m4s
elasticjob-chaos-test-dlrover-master 1/1 Running 0 3m9s
训练自动扩缩容
此实验中,我们使用 MNIST 例子 来在集群资源不满足全部4个节点的情况下提交作业。可以看到,作业只启动了 3 个 Pod,有一个因为资源不足 pending。
sql
elasticjob-torch-mnist-dxlrover-master 1/1 Running 0 57s
torch-mnist-edljob-worker-0 1/1 Running 0 47s
torch-mnist-edljob-worker-1 1/1 Running 0 47s
torch-mnist-edljob-worker-2 1/1 Running 0 47s
torch-mnist-edljob-worker-3 0/1 Pending 0 47s
因为这个作业中,我们设置了 --nnodes=3:$WORKER_NUM
,其中WORKER_NUM
是DLRover 自动设置在 Pod 中环境变量,其值为 replicas,此实验中为 4。大约 2min 后,我们可以从 worker-0 的日志中看到这 3 个运行的节点开始训练了。Rendezvous 的日志中 group_world_size=3
说明有 3 个节点组网训练。
ini
[2023-09-27 02:23:21,097] [INFO] [training.py:344:_rendezvous] [default] Rendezvous complete for workers. Result:
restart_count=0
master_addr=192.168.0.71
master_port=36725
group_rank=0
group_world_size=3
local_ranks=[0, 1]
role_ranks=[0, 1]
global_ranks=[0, 1]
role_world_sizes=[6, 6]
global_world_sizes=[6, 6]
rank 1 is initialized local_rank = 1
loss = 2.3198373317718506, step = 0
loss = 1.7543025016784668, step = 20
然后我们 kill 集群上的其他作业,让 worker-3 能启动。
sql
elasticjob-torch-mnist-dlrover-master 1/1 Running 0 5m39s
torch-mnist-edljob-worker-0 1/1 Running 0 5m34s
torch-mnist-edljob-worker-1 1/1 Running 0 5m34s
torch-mnist-edljob-worker-2 1/1 Running 0 5m34s
torch-mnist-edljob-worker-3 1/1 Running 0 5m34s
然后从 worker-0 的日志中,我们可以看到有 4 个节点组网训练。说明此作业由 3 个节点扩容到了 4 个节点。
ini
[2023-09-27 02:25:43,362] [INFO] [training.py:344:_rendezvous] [default] Rendezvous complete for workers. Result:
restart_count=1
master_addr=192.168.0.71
master_port=58241
group_rank=0
group_world_size=4
local_ranks=[0, 1]
role_ranks=[0, 1]
global_ranks=[0, 1]
role_world_sizes=[8, 8]
global_world_sizes=[8, 8]
rank 1 is initialized local_rank = 1rank 0 is initialized local_rank = 0
loss = 2.2984073162078857, step = 0
loss = 2.1407980918884277, step = 20
loss = 1.1324385404586792, step = 40
接着,我们在worker-1 里制造程序故障,让 worker-1 报错退出。这个 MNIST 例子 因为没有配置 Pod 报错重启,所以 worker-1 报错后,该作业并没有启动新的 worker。
sql
elasticjob-torch-mnist-dlrover-master 1/1 Running 0 7m43s
torch-mnist-edljob-worker-0 1/1 Running 0 7m38s
torch-mnist-edljob-worker-1 0/1 Error 0 7m38s
torch-mnist-edljob-worker-2 1/1 Running 0 7m38s
torch-mnist-edljob-worker-3 1/1 Running 0 7m38s
然后从 worker-0 的日志我们通过 group_world_size=3
可以看到,训练又缩容到了 3 个节点。
ini
[2023-09-27 03:18:00,815] [INFO] [training.py:344:_rendezvous] [default] Rendezvous complete for workers. Result:
restart_count=1
master_addr=192.168.0.66
master_port=39705
group_rank=0
group_world_size=3
local_ranks=[0, 1]
role_ranks=[0, 1]
global_ranks=[0, 1]
role_world_sizes=[6, 6]
global_world_sizes=[6, 6]
[2023-09-27 03:18:05,957] [INFO] [sampler.py:153:load_state_dict] Load epoch = 0, completed num = 51200, num_samples = 1467
[2023-09-27 03:18:05,958] [INFO] [sampler.py:153:load_state_dict] Load epoch = 0, completed num = 51200, num_samples = 1467
loss = 0.2617453336715698, step = 0
loss = 0.2548859417438507, step = 20
TensorFlow PS 分布式训练的容错
我们将模拟如下场景来验证 DLRover 在 TensorFlow PS 分布式训练的弹性容错:
- Worker Pod 被驱逐
- Worker Pod OOM
- PS Pod 被驱逐
Worker Pod 被驱逐
我们使用 TF 训练例子 来提交一个有 2 个 worker 和一个 PS 节点的作业。提交后,Pod 如下:
sql
kubectl -n dlrover apply -f examples/tensorflow/criteo_deeprec/manual_job.yaml
>>>
deepctr-manual-scale-edljob-chief-0 1/1 Running 0 88s
deepctr-manual-scale-edljob-ps-0 1/1 Running 0 88s
deepctr-manual-scale-edljob-worker-0 1/1 Running 0 88s
elasticjob-deepctr-manual-scale-dlrover-master 1/1 Running 0 99s
可以看到作业启动了一个 chief-0,worker-0 和 ps-0。在 TensorFlow PS 作业中,chief 也是一个 worker。所以这个作业启动了两个worker,一个 PS。然后我们手动 kill worker-0 来模拟 Pod 被驱逐。
sql
kubectl -n dlrover delete pod deepctr-manual-scale-edljob-worker-0
>>>
NAME READY STATUS RESTARTS AGE
deepctr-manual-scale-edljob-chief-0 1/1 Running 0 2m57s
deepctr-manual-scale-edljob-ps-0 1/1 Running 0 2m57s
deepctr-manual-scale-edljob-worker-1 1/1 Running 0 60s
elasticjob-deepctr-manual-scale-dlrover-master 1/1 Running 0 3m8s
可以看到 worker-0 被 kill 后,作业启动了 worker-1 来恢复。
接着,我们使用 ChaosBlade 在 chief-0 里制造 OOM。
lua
kubectl -n dlrover exec -it deepctr-manual-scale-edljob-worker-0 bash
chaosblade-1.7.2/blade create mem load --mode ram --mem-percent 80
然后我们可以看到 chief-0 因为 OOMKilled 退出了,且 chief-1 启动了。
sql
deepctr-manual-scale-edljob-chief-0 0/1 OOMKilled 0 4m53s
deepctr-manual-scale-edljob-chief-1 1/1 Running 0 64s
deepctr-manual-scale-edljob-ps-0 1/1 Running 0 4m53s
deepctr-manual-scale-edljob-worker-1 1/1 Running 0 2m56s
通过查看 chief-0 和 chief-1 的资源配置,我们可以看到 chief-1 的内存由 4Gi 增加到了 8Gi。因为 DLRover 对于 OOMKilled 的 Pod 会自动增加内存并重启 Pod,防止 OOM 再次发生。
yaml
kubectl -n dlrover get pod deepctr-manual-scale-edljob-chief-0 -o yaml | grep memory
>>>
memory: 4Gi
memory: 4Gi
kubectl -n dlrover get pod deepctr-manual-scale-edljob-chief-1 -o yaml | grep memory
>>>
memory: 8Gi
memory: 8Gi
PS Pod 被驱逐
在上面提交的作业上,我们手动删除 ps-0。接着 ps-1 启动了,来恢复 ps-0 的角色。
sql
kubectl -n dlrover delete pod deepctr-manual-scale-edljob-ps-0
deepctr-manual-scale-edljob-chief-0 0/1 OOMKilled 0 10m
deepctr-manual-scale-edljob-chief-1 1/1 Running 0 7m1s
deepctr-manual-scale-edljob-ps-1 1/1 Running 0 109s
deepctr-manual-scale-edljob-worker-1 0/1 OOMKilled 0 8m53s
deepctr-manual-scale-edljob-worker-2 1/1 Running 0 4m13s
elasticjob-deepctr-manual-scale-dlrover-master 1/1 Running 0 11m
从 chief-1 的日志中,我们看到了 chief 从 checkpoint 中加载了模型,并继续开始训练。
ini
[2023-09-26 19:24:00,861] [INFO][saver.py:1531:restore] Restoring parameters from /nas/deepctr/model.ckpt-126
[2023-09-26 19:24:03,473] [INFO][session_manager.py:511:_try_run_local_init_op] Running local_init_op.
[2023-09-26 19:24:03,580] [INFO] [resource.py:164:report_resource] Report Resource CPU : 0.98, Memory 7146, GPU []
[2023-09-26 19:24:03,670] [INFO][session_manager.py:513:_try_run_local_init_op] Done running local_init_op.
[2023-09-26 19:24:07,665] [INFO][basic_session_run_hooks.py:627:_save] Saving checkpoints for 126 into /nas/deepctr/model.ckpt.
总结
通过上述实验,我们使用 ChaosBlade 验证了 DLRover 可以自动恢复各种训练故障,提升分布式训练的稳定性。这样可以大幅降低人工运维成本并提升训练效率。下一篇,我们将介绍 DLRover 自动调整 DataLoader 的 Batch size 来自动提升训练吞吐。
DLRover
DLRover (Distributed Deep Learning System) 是蚂蚁集团 AI Infra 团队维护的开源社区,是基于云原生技术打造的智能分布式深度学习系统。DLRover 使得开发人员能够专注于模型架构的设计,而无需处理任何工程方面的细节,例如硬件加速和分布式运行等。目前,DLRover 支持使用 K8s、Ray 进行自动化操作和维护深度学习训练任务。更多 AI Infra 技术请关注 DLRover 项目。
加入 DLRover 钉钉技术交流群:31525020959
如果你认为有收获,欢迎点击为 DLRover Star 一下: github.com/intelligent...