本文超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已变得不正确。

在 Kubernetes 上使用 PaddlePaddle 运行深度学习

什么是 PaddlePaddle

PaddlePaddle 是一个易于使用、高效、灵活且可扩展的深度学习平台,最初由百度于 2014 年开发,用于将深度学习应用于百度产品。

使用 PaddlePaddle 创造了 50 多项创新,支持 15 种百度产品,涵盖搜索引擎、在线广告、问答和系统安全等领域。

2016 年 9 月,百度开源了 PaddlePaddle,很快吸引了百度以外的许多贡献者。

为什么在 Kubernetes 上运行 PaddlePaddle

PaddlePaddle 的设计简洁,并且独立于计算基础设施。用户可以在 Hadoop、Spark、Mesos、Kubernetes 等之上运行它。我们对 Kubernetes 非常感兴趣,因为它具有灵活性、效率和丰富的功能。

在我们百度各种产品中应用 PaddlePaddle 时,我们注意到 PaddlePaddle 的两种主要用法——研究和产品。研究数据不会经常变化,重点是快速实验以达到预期的科学测量结果。产品数据经常变化。它通常来自 Web 服务生成的日志消息。

一个成功的深度学习项目包括研究和数据处理管道。有许多参数需要调整。许多工程师同时在项目的不同部分工作。

为了确保项目易于管理并有效利用硬件资源,我们希望在同一基础设施平台上运行项目的所有部分。

该平台应提供

  • 容错能力。它应该将管道的每个阶段抽象为一个服务,该服务由许多进程组成,这些进程通过冗余提供高吞吐量和鲁棒性。

  • 自动缩放。白天,通常有许多活跃用户,平台应该扩展在线服务。而在夜间,平台应该释放一些资源用于深度学习实验。

  • 作业打包和隔离。它应该能够将需要 GPU 的 PaddlePaddle 训练器进程、需要大量内存的 Web 后端服务以及需要磁盘 IO 的 CephFS 进程分配到同一个节点,以充分利用其硬件。

我们想要的是一个平台,可以在同一个集群上运行深度学习系统、Web 服务器(例如 Nginx)、日志收集器(例如 fluentd)、分布式队列服务(例如 Kafka)、日志连接器以及使用 Storm、Spark 和 Hadoop MapReduce 编写的其他数据处理器。我们希望在同一个集群上运行所有作业——在线和离线、生产和实验——以便我们可以充分利用集群,因为不同类型的作业需要不同的硬件资源。

我们选择了基于容器的解决方案,因为虚拟机引入的开销与我们的效率和利用率目标相矛盾。

根据我们对不同基于容器的解决方案的研究,Kubernetes 最符合我们的要求。

在 Kubernetes 上进行分布式训练

PaddlePaddle 原生支持分布式训练。PaddlePaddle 集群中有两个角色:**参数服务器**和**训练器**。每个参数服务器进程维护全局模型的一个分片。每个训练器都有其本地模型副本,并使用其本地数据更新模型。在训练过程中,训练器将模型更新发送到参数服务器,参数服务器负责聚合这些更新,以便训练器可以将其本地副本与全局模型同步。

| | | 图 1:模型被划分为两个分片。分别由两个参数服务器管理。 |

其他一些方法使用一组参数服务器在多台主机上的 CPU 内存空间中共同保存一个非常大的模型。但在实践中,我们并不经常拥有如此大的模型,因为由于 GPU 内存的限制,处理非常大的模型效率非常低。在我们的配置中,多个参数服务器主要用于快速通信。假设只有一个参数服务器进程与所有训练器一起工作,则参数服务器必须聚合来自所有训练器的梯度并成为瓶颈。根据我们的经验,实验有效的配置包括相同数量的训练器和参数服务器。我们通常在同一个节点上运行一对训练器和参数服务器。在以下 Kubernetes 作业配置中,我们启动一个运行 N 个 Pod 的作业,并且每个 Pod 中都有一个参数服务器和一个训练器进程。

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:两个节点上运行的三个 pod 的作业 A 和一个 pod 的作业 B。 |

每个 pod 的入口点是 start.sh。它从存储服务下载数据,以便训练器可以从 pod 本地磁盘空间快速读取。下载完成后,它运行一个 Python 脚本 start_paddle.py,该脚本启动一个参数服务器,等待所有 pod 的参数服务器准备好服务,然后在 pod 中启动训练器进程。

这种等待是必要的,因为每个训练器都需要与所有参数服务器通信,如图 1 所示。Kubernetes API 使训练器能够检查 pod 的状态,因此 Python 脚本可以等到所有参数服务器的状态更改为“运行”后再触发训练过程。

目前,从数据分片到 pod/训练器的映射是静态的。如果我们要运行 N 个训练器,我们需要将数据划分为 N 个分片,并将每个分片静态分配给一个训练器。我们再次依靠 Kubernetes API 列出一个作业中的 pod,以便我们可以将 pod / 训练器从 1 索引到 N。第 i 个训练器将读取第 i 个数据分片。

训练数据通常在分布式文件系统上提供。在实践中,我们在本地集群上使用 CephFS,在 AWS 上使用 Amazon Elastic File System。如果您有兴趣构建 Kubernetes 集群来运行分布式 PaddlePaddle 训练作业,请按照 本教程操作。

下一步是什么

我们正在努力使 PaddlePaddle 更顺畅地与 Kubernetes 运行。

您可能注意到当前的训练器调度完全依赖于基于静态分区映射的 Kubernetes。这种方法易于启动,但可能会导致一些效率问题。

首先,缓慢或失效的训练器会阻塞整个作业。初始部署后没有受控的抢占或重新调度。其次,资源分配是静态的。因此,如果 Kubernetes 拥有的可用资源比我们预期的多,我们必须手动更改资源需求。这是一项繁琐的工作,并且与我们的效率和利用率目标不一致。

为了解决上述问题,我们将添加一个了解 Kubernetes API 的 PaddlePaddle 主服务器,它可以动态添加/删除资源容量,并以更动态的方式将分片调度到训练器。PaddlePaddle 主服务器使用 etcd 作为分片到训练器的动态映射的容错存储。因此,即使主服务器崩溃,映射也不会丢失。Kubernetes 可以重新启动主服务器,作业将继续运行。

另一个潜在的改进是更好的 PaddlePaddle 作业配置。我们拥有相同数量的训练器和参数服务器的经验主要来自使用专用集群。观察到该策略在我们客户端仅运行 PaddlePaddle 作业的集群上表现良好。但是,此策略在运行多种作业的通用集群上可能不是最佳的。

PaddlePaddle 训练器可以利用多个 GPU 来加速计算。GPU 还不是 Kubernetes 中的一等资源。我们必须半手动管理 GPU。我们很乐意与 Kubernetes 社区合作改进 GPU 支持,以确保 PaddlePaddle 在 Kubernetes 上运行最佳。