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

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

什么是 PaddlePaddle

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

使用 PaddlePaddle 已经创建了超过50项创新,支持15个百度产品,范围从搜索引擎、在线广告到问答和系统安全。

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

为什么要在 Kubernetes 上运行 PaddlePaddle

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

在我们将 PaddlePaddle 应用于各种百度产品时,我们注意到 PaddlePaddle 的两种主要使用方式——研究和产品。研究数据不常变动,重点是快速实验以达到预期的科学测量。产品数据经常变动。它通常来自网络服务生成的日志消息。

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

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

该平台应提供:

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

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

  • 作业打包和隔离。它应该能够将需要 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:作业A包含三个 Pod,作业B包含一个 Pod,在两个节点上运行。

每个 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 Master,它可以动态地添加/删除资源容量,并以更动态的方式将分片调度到训练器。PaddlePaddle Master 使用 etcd 作为分片到训练器动态映射的容错存储。因此,即使 Master 崩溃,映射也不会丢失。Kubernetes 可以重启 Master,作业将继续运行。

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

PaddlePaddle 训练器可以利用多个 GPU 来加速计算。GPU 在 Kubernetes 中尚未成为一级资源。我们必须半手动管理 GPU。我们很乐意与 Kubernetes 社区合作,改进 GPU 支持,以确保 PaddlePaddle 在 Kubernetes 上运行得最好。