使用 ChaosBlade 验证 DLRover 的弹性和容错的稳定性

文|王勤龙 (花名:长凡)

蚂蚁集团 AI 系统工程师

ChaosBlade 是阿里巴巴开源的一款遵循混沌工程原理和混沌实验模型的实验注入工具,可以用于验证云原生系统的稳定性。DLRover 作为云原生的分布式训练系统,提供了弹性和容错功能来提升分布式训练的稳定性。为此我们使用 ChaosBlade 创建各种混沌实验来验证 DLRover 弹性容错的稳定性。

前提:创建 k8s 集群并部署 DLRover ElasticJob

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...

相关推荐
许野平42 分钟前
Rust: 利用 chrono 库实现日期和字符串互相转换
开发语言·后端·rust·字符串·转换·日期·chrono
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
LunarCod3 小时前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
非著名程序员3 小时前
腾讯为什么支持开源?
开源
CSDN云计算3 小时前
如何以开源加速AI企业落地,红帽带来新解法
人工智能·开源·openshift·红帽·instructlab
码农派大星。3 小时前
Spring Boot 配置文件
java·spring boot·后端
杜杜的man4 小时前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*4 小时前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
llllinuuu4 小时前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s4 小时前
Golang--协程和管道
开发语言·后端·golang