在这篇文章中,我们会和大家分享如何开展深度学习的研究,也会一并介绍我们在研究中选用的基础架构和开源技术 kubernetes-ec2-autoscaler,这是一种用于 Kubernetes 批处理任务的弹性伸缩管理器(batch-optimized scaling manager)。
用例
深度学习的演进通常源于一个能够在小问题上被验证的构想。在这个阶段,你需要快速地进行大量随机实验。理想情况下,只需远程登录到一台机器,运行一个脚本,不到一个小时就可以得到结果。
但是构建一个真正可用的模型通常会经历很多次失败,需要我们不停地去修复这些缺陷。(这和其他新建的软件系统一样,你需要多次运行代码才能判断它是如何运转的。)
你需要通过多个角度的计算来检测模型,从而意识到它是如何学习的。Dario Amodei 的这种增强学习机制(控制右侧的球拍)可以在击球游戏中获得很高的分数,但你会发现,游戏中右侧的球拍完全没有移动。
因此深度学习的基础架构要能允许用户灵活地反观模型,仅仅展示一些统计结果是不够的。
当模型表现出一定的
应用前景,你会希望将它扩展到更大的数据集和更多的 GPU 上运行,但这会花费大量的时间。而且你需要认真地管理实验并非常谨慎地去选择超参数(hyperparameters)的范围。
这种科研的过程在早期是快速且缺乏系统性的;到了后期,过程会逐渐有条理却很耗费精力,但为了获得完美的结果,这是必不可少的。
案例
论文 Improved Techniques for Training GANs 开篇讲述了 Tim Salimans 对于如何改进生成对抗网络(GAN)训练机制的一些看法。我们会挑其中较简单的一个进行介绍(这虽然不是最好的半监督学习案例,但它生成了最好看的样本)。
GANs 由一个生成器网络和一个鉴别器网络构成。生成器会不停地去干扰鉴别器,而鉴别器会尽力地将生成器造出的数据和真实数据区分开来。通常来说,判断生成器的好坏,看它能不能骗过所有鉴别器就行了,但难题仍然存在:如果生成器一直输出完全相同的(几乎和真实的一样)样本会造成网络的崩溃。
Tim 提出可以用小批次的样本数据代替原先的一整个样本提供给鉴别器。这样鉴别器就可以判断生成器是否一直在传同样的图像。当“崩溃”发生时,生成器将会进行梯度调整来修正这个问题。
下一步就是基于 MNIST 和 CIFAR-10 将构想转化为原型。这需要快速地构建出一个初步的模型,然后运行真实的数据并检测结果。在经过几次快速的迭代之后,Tim 得到了 CIFAR-10 的样本,这次的结果十分振奋人心,几乎是我们见过的在这个数据集上跑出的最好样本了。
深度学习(以及常说的 AI 算法)如果要真正形成一定影响就必须扩大实验规模,一个小型神经网络可以验证概念,而大型的神经网络才能真正解决问题。因此 Ian Goodfellow 开始把模型扩展到 ImageNet 进行验证。
模型学习生成 ImageNet 的图像
有了更大的模型和数据集,Ian 就需要用更多的 GPU 来并行地运行模型。任务运行时机器的 CPU 和 GPU 利用率会飙升至 90%,但是即使这样仍需要花费很多天才能完成模型训练。在这种模式下,每一次实验机会都显得无比珍贵,他也会非常细致地记录下每次实验的结果。
虽然实验最终得到了不错的结果,但仍没有达到我们的预期。为了找到原因我们做了很多尝试,但仍然攻克不了。这大概就是科学的本质吧。
基础架构
软件
TensorFlow 代码的样例
我们绝大部分的研究代码是用 Python 完成的,详细内容可以在我们的开源项目中查看到。我们通常使用 TensorFlow(在特殊情况下也会使用 Theano)来进行 GPU 计算;使用 Numpy 或其他方法来进行 CPU 计算。研究人员有时也会使用更上层的框架,比如基于 TensorFlow 的 Keras。
和多数深度学习社区一样,我们会使用 Python2.7。Anaconda 也经常会用到,它可以方便地给 OpenCV 打包,并对一些科学算法库进行性能优化。
硬件
对于理想的批处理任务,将集群计算节点的数量翻倍会减半任务执行时间。不幸的是,在深度学习中,GPU 数量的增加只会引起任务亚线性的加速。因此顶级的计算性能只能依靠顶级的 GPU 来实现。我们也使用了许多 CPU 用于构建模拟器、增强学习环境或是小规模的模型(这类模型跑在 GPU 上时运行效率不会有明显的增加)。
nvidia-smi 下满载的 Titan Xs
AWS 慷慨地为我们提供了大量计算资源。这些资源被用于 CPU 实例以及 GPU 任务的水平扩展。我们也有自己的物理机,用的是 Titan X GPU。我们期望之后可以使用混合云:对不同的 GPU、连接以及其他技术开展实验是非常具有价值的,这对深度学习未来的发展也有着重要影响。
相同物理单元上的 htop 显示了大量空闲的 CPU。我们通常将 CPU 密集型和 GPU 密集型的任务分开运行。
配置
我们对待基础架构就像许多公司对待他们的产品一样:它的界面必须简洁,必须兼顾功能性和可用性。我们会使用一致的工具来统一管理所有服务器,并且尽可能地对他们进行相同的配置。
用于管理弹性伸缩组的 Terraform 配置文件片段。Terraform 可以创建、修改或销毁正在运行的云资源来匹配配置文件。
我们使用 Terraform 来创建 AWS 的云资源(实例、网络路由、DNS 记录等)。我们的云端节点和物理节点都运行 Ubuntu 系统,并使用 Chef 来做配置。为了实现加速,我们使用 Packer 来预先制作集群镜像(AMI)。我们的所有集群都使用非交叉的 IP 范围,用户可以通过笔记本上的 OpenVPN 及物理节点上的 strongSwan(AWS 的客户网关)连接到公网。
最后,我们将用户的 home 目录、数据集和结果存储在 NFS(基于物理硬件)和 EFS/S3(基于 AWS)上。
编排
可扩展的基础架构通常会使原本简单的用例复杂化。我们在对不同规模作业的基础架构研究上投入了同等的精力,也在同步优化工具套件,使得分布式的用例能像本地用例一样好用。
我们为随机实验提供了 SSH 节点的(有些有 GPU 有些没有)集群,并且使用 Kubernetes 来调度物理节点和 AWS 节点。我们的集群横跨 3 个 AWS 域——因为有时任务量会突然爆发,从而占满单个区域的所有资源。
Kubernetes 要求每一个任务都是一个 Docker 容器,这样就可以实现依赖隔离和代码快照。但是创建一个新的 Docker 容器会增加迭代周期的时间,这个时间十分宝贵,所以我们也提供工具,将研究人员笔记本上的代码转成标准镜像。
TensorBoard 中的模型学习曲线
我们将 Kubernetes 的 flannel 网络直接暴露至研究人员的电脑,使用户可以无缝访问正在运行的任务。这对于访问 TensorBoard 这类监控服务特别有帮助。(为了实现绝对的隔离,我们最初要求针对每一个暴露的端口都要创建 Kubernetes 服务,但这样会带来很多困难。)
kubernetes-ec2-autoscaler
我们的任务负载具有突发性和不可预测性:原先只需要单节点的实验可能很快就发展到需要 1000 个核。比如在几周的时间里,实验从只需要一个 Titan X 的交互阶段发展到了需要 60 个 Titan X 的实验阶段,这需要将近 1600 个 AWS 的 GPU。因此,我们的云架构要能动态配置 Kubernetes 节点。
在弹性伸缩组中运行 Kubernetes 节点非常简单,困难的是如何正确地配置这些组的规模。在提交批处理任务后,集群可以准确地知道它需要的资源并直接进行分配。(相反,AWS 的扩展策略会不断地启动新的节点碎片来提供足够的资源,这是一个多次迭代的过程。)集群还需要在终止节点前进行任务迁移(drain)操作,避免丢失正在运行的任务。
很多人想直接使用原始的 EC2 来处理大批量的任务,我们一开始也是这么做的。但是 Kubernetes 的生态具有更多优势:比如易用的工具、日志记录、监控、从运行实例中区分管理物理节点的能力等。合理配置 Kubernetes 使其能够正确地动态扩展要比在原始 EC2 上重建这种环境来的简单。
我们发布的 kubernetes-ec2-autoscaler,是一种用于 Kubernetes 批处理任务的弹性伸缩管理器。它在 Kubernetes 上作为一个普通的 Pod 运行,且只要求你的工作节点运行在弹性伸缩组内。
Kubernetes 集群的启动配置
自动扩展器会轮询 Kubernetes 主节点的状态,包括用于计算集群所需资源和容量的所有信息。如果超出了容量,它会将相关节点的任务迁移(drain)后将其终止。如果需要更多的资源,它会计算需要创建什么样的服务器并适当地增加弹性伸缩组的规模(或直接解锁 (uncordon) 执行过 drain 操作的节点,来避免新节点增加的启动时间)。
kubernetes-ec2-autoscaler 管理着多个弹性伸缩组、CPU 之外的资源(内存和 GPU)以及对任务细粒度的约束,例如 AWS 区域和实例大小。另外,突增的负载会引起弹性伸缩组的超时和报错,因为即使是 AWS 也不具备无限扩展的容量。这种情况下,kubernetes-ec2-autoscaler 会检测到错误并将超出部分的任务分配到次级的 AWS 区域执行。
我们的基础架构设计旨在最大程度地提高科研人员的工作效率,使他们能够专注于科研本身。我们将继续深入优化基础架构和工作流程,之后也会陆续和大家分享经验。我们期待与您的合作,共同促进深度学习的发展!