这篇文章已超过一年。较旧的文章可能包含过时的内容。请检查页面中的信息自发布以来是否已失效。

在 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 上运行达到最佳性能。