这篇文章已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已失效。
在 Kubernetes 上使用 PaddlePaddle 运行深度学习
什么是 PaddlePaddle
PaddlePaddle 是一个易于使用、高效、灵活且可扩展的深度学习平台,最初由百度开发,自 2014 年起用于将深度学习应用于百度产品。
使用 PaddlePaddle 已创建 50 多项创新成果,支持了 15 款百度产品,涵盖搜索引擎、在线广告、问答系统和系统安全等。
2016 年 9 月,百度开源了 PaddlePaddle,并很快吸引了许多来自百度外部的贡献者。
为什么要在 Kubernetes 上运行 PaddlePaddle
PaddlePaddle 设计得轻量且独立于计算基础设施。用户可以在 Hadoop, Spark, Mesos, Kubernetes 等平台上运行它。我们对 Kubernetes 有浓厚兴趣,因为它具有灵活性、效率和丰富的功能。
在我们将 PaddlePaddle 应用于各种百度产品时,我们注意到两种主要的 PaddlePaddle 用途——研究和产品。研究数据不常变化,重点在于快速实验以达到预期的科学测量。产品数据经常变化。它通常来自 Web 服务生成的日志消息。
一个成功的深度学习项目包括研究和数据处理流水线。有许多参数需要调整。许多工程师同时在项目的不同部分工作。
为了确保项目易于管理并高效利用硬件资源,我们希望在同一基础设施平台上运行项目的所有部分。
该平台应提供
容错能力。它应将流水线的每个阶段抽象为一个服务,该服务由许多进程组成,通过冗余提供高吞吐量和鲁棒性。
自动扩缩容。白天通常有许多活跃用户,平台应扩展在线服务。而夜间,平台应释放一些资源用于深度学习实验。
作业打包和隔离。它应能够将需要 GPU 的 PaddlePaddle trainer 进程、需要大内存的 Web 后端服务以及需要磁盘 IO 的 CephFS 进程分配到同一节点,以充分利用其硬件。
我们想要的是一个平台,它能够在同一个集群上运行深度学习系统、Web 服务器(例如 Nginx)、日志收集器(例如 fluentd)、分布式队列服务(例如 Kafka)、日志合并器以及使用 Storm、Spark 和 Hadoop MapReduce 编写的其他数据处理器。我们希望在同一个集群上运行所有作业——在线和离线、生产和实验——这样我们就可以充分利用集群,因为不同类型的作业需要不同的硬件资源。
我们选择基于容器的解决方案,因为虚拟机引入的开销与我们追求效率和利用率的目标相矛盾。
基于我们对不同容器解决方案的研究,Kubernetes 最符合我们的要求。
在 Kubernetes 上进行分布式训练
PaddlePaddle 原生支持分布式训练。PaddlePaddle 集群中有两个角色:参数服务器 (parameter server) 和 trainer。每个参数服务器进程维护全局模型的一个分片。每个 trainer 拥有模型的本地副本,并使用其本地数据更新模型。在训练过程中,trainer 将模型更新发送给参数服务器,参数服务器负责聚合这些更新,以便 trainer 可以将其本地副本与全局模型同步。
| | | 图 1: 模型被划分为两个分片,分别由两个参数服务器管理。 |
其他一些方法使用一组参数服务器,在多个主机上的 CPU 内存空间中共同持有非常大的模型。但在实践中,我们不常使用如此大的模型,因为受限于 GPU 内存,处理非常大的模型会非常低效。在我们的配置中,多个参数服务器主要是为了实现快速通信。假设只有一个参数服务器进程与所有 trainer 协同工作,该参数服务器将不得不聚合所有 trainer 的梯度,从而成为瓶颈。根据我们的经验,实验上高效的配置是 trainer 和参数服务器数量相同。我们通常在同一节点上运行一对 trainer 和参数服务器。在下面的 Kubernetes 作业配置中,我们启动了一个运行 N 个 Pod 的作业,每个 Pod 中都有一个参数服务器和一个 trainer 进程。
yaml
apiVersion: batch/v1
kind: Job
metadata:
name: PaddlePaddle-cluster-job
spec:
parallelism: 3
completions: 3
template:
metadata:
name: PaddlePaddle-cluster-job
spec:
volumes:
- name: jobpath
hostPath:
path: /home/admin/efs
containers:
- name: trainer
image: your\_repo/paddle:mypaddle
command: ["bin/bash", "-c", "/root/start.sh"]
env:
- name: JOB\_NAME
value: paddle-cluster-job
- name: JOB\_PATH
value: /home/jobpath
- name: JOB\_NAMESPACE
value: default
volumeMounts:
- name: jobpath
mountPath: /home/jobpath
restartPolicy: Never
从配置中可以看到,parallelism 和 completions 都设置为 3。因此,该作业将同时启动 3 个 PaddlePaddle Pod,当所有 3 个 Pod 完成时,该作业将结束。
图 2: 作业 A (三个 Pod) 和作业 B (一个 Pod) 运行在两个节点上。 |
每个 Pod 的入口点是 start.sh。它从存储服务下载数据,以便 trainer 可以从 Pod 本地磁盘空间快速读取。下载完成后,它运行一个 Python 脚本 start_paddle.py,该脚本启动一个参数服务器,等待所有 Pod 的参数服务器准备好服务,然后启动 Pod 中的 trainer 进程。
这种等待是必要的,因为每个 trainer 都需要与所有参数服务器通信,如图 1 所示。Kubernetes API 使得 trainer 能够检查 Pod 的状态,因此 Python 脚本可以在所有参数服务器的状态变为“运行中”之前等待,然后才触发训练过程。
目前,数据分片到 Pod/trainer 的映射是静态的。如果我们打算运行 N 个 trainer,我们需要将数据分成 N 个分片,并将每个分片静态分配给一个 trainer。我们再次依赖 Kubernetes API 列出作业中的 Pod,以便我们可以将 Pod/trainer 从 1 到 N 索引。第 i 个 trainer 将读取第 i 个数据分片。
训练数据通常存储在分布式文件系统上。在实践中,我们在本地集群使用 CephFS,在 AWS 上使用 Amazon Elastic File System。如果您有兴趣构建一个 Kubernetes 集群来运行分布式 PaddlePaddle 训练作业,请参考本教程。
未来展望
我们正致力于让 PaddlePaddle 在 Kubernetes 上运行得更流畅。
您可能注意到,当前的 trainer 调度完全依赖于基于静态分区映射的 Kubernetes。这种方法入门简单,但可能会导致一些效率问题。
首先,慢速或死掉的 trainer 会阻塞整个作业。初始部署后没有受控的抢占或重新调度。其次,资源分配是静态的。因此,如果 Kubernetes 的可用资源比我们预期的多,我们就必须手动更改资源需求。这是一项繁琐的工作,与我们追求效率和利用率的目标不符。
为了解决上述问题,我们将添加一个理解 Kubernetes API 的 PaddlePaddle master,它可以动态地增减资源容量,并以更动态的方式将分片分派给 trainer。PaddlePaddle master 使用 etcd 作为分片到 trainer 动态映射的容错存储。因此,即使 master 崩溃,映射也不会丢失。Kubernetes 可以重启 master,作业将继续运行。
另一个潜在的改进是优化 PaddlePaddle 作业配置。我们采用相同数量的 trainer 和参数服务器的经验主要来自于使用专用集群。在该策略在仅运行 PaddlePaddle 作业的客户集群上表现良好。然而,在运行多种类型作业的通用集群上,这种策略可能不是最优的。
PaddlePaddle trainer 可以利用多个 GPU 来加速计算。GPU 在 Kubernetes 中尚未成为一级资源。我们不得不半手动管理 GPU。我们非常愿意与 Kubernetes 社区合作,改进 GPU 支持,以确保 PaddlePaddle 在 Kubernetes 上运行达到最佳性能。
- 下载 Kubernetes
- 在 GitHub 上参与 Kubernetes 项目
- 在 Stack Overflow 上提问(或回答问题)
- 在 Slack 上与社区交流
- 在 Twitter 上关注我们 @Kubernetesio 获取最新动态