本文整理自《CNCF x Alibaba 云原生技术公开课》第 20 讲。git
导读:2016 年,随着 AlphaGo 的走红和 TensorFlow 项目的异军突起,一场名为 AI 的技术革命迅速从学术圈蔓延到了工业界,所谓 AI 革命今后拉开了帷幕。该热潮的背后推手正是云计算的普及和算力的巨大提高。
通过近几年的发展,AI 有了许许多多的落地场景,包括智能客服、人脸识别、机器翻译、以图搜图等功能。其实机器学习或者说是人工智能,并非什么新鲜的概念。而此次热潮的背后,云计算的普及以及算力的巨大提高,才是真正将人工智能从象牙塔带到工业界的一个重要推手。docker
与之相对应的,从 2016 年开始,Kubernetes 社区就不断收到来自不一样渠道的大量诉求:但愿能在 Kubernetes 集群上运行 TensorFlow 等机器学习框架。这些诉求中,除了以前文章所介绍的,像 Job 这些离线任务的管理以外,还有一个巨大的挑战:深度学习所依赖的异构设备及英伟达的 GPU 支持。编程
咱们不由好奇起来:Kubernetes 管理 GPU 能带来什么好处呢?json
本质上是成本和效率的考虑。因为相对 CPU 来讲,GPU 的成本偏高。在云上单 CPU 一般是一小时几毛钱,而 GPU 的花费则是从单 GPU 每小时 10 元 ~ 30 元不等,这就要千方百计的提升 GPU 的使用率。api
为何要用 Kubernetes 管理以 GPU 为表明的异构资源?安全
具体来讲是三个方面:服务器
首先是加速部署,避免把时间浪费在环境准备的环节中。经过容器镜像技术,将整个部署过程进行固化和复用,若是同窗们关注机器学习领域,能够发现许许多多的框架都提供了容器镜像。咱们能够借此提高 GPU 的使用效率。框架
经过分时复用,来提高 GPU 的使用效率。当 GPU 的卡数达到必定数量后,就须要用到 Kubernetes 的统一调度能力,使得资源使用方可以作到用即申请、完即释放,从而盘活整个 GPU 的资源池。curl
而此时还须要经过 Docker 自带的设备隔离能力,避免不一样应用的进程运行同一个设备上,形成互相影响。在高效低成本的同时,也保障了系统的稳定性。机器学习
上面了解到了经过 Kubernetes 运行 GPU 应用的好处,经过以前系列文章的学习也知道,Kubernetes 是容器调度平台,而其中的调度单元是容器,因此在学习如何使用 Kubernetes 以前,咱们先了解一下如何在容器环境内运行 GPU 应用。
在容器环境下使用 GPU 应用,实际上不复杂。主要分为两步:
有两个方法准备:
好比直接从 docker.hub 或者阿里云镜像服务中寻找官方的 GPU 镜像,包括像 TensorFlow、Caffe、PyTorch 等流行的机器学习框架,都有提供标准的镜像。这样的好处是简单便捷,并且安全可靠。
固然若是官方镜像没法知足需求时,好比你对 TensorFlow 框架进行了定制修改,就须要从新编译构建本身的 TensorFlow 镜像。这种状况下,咱们的最佳实践是:依托于 Nvidia 官方镜像继续构建,而不要从头开始。
以下图中的 TensorFlow 例子所示,这个就是以 Cuda 镜像为基础,开始构建本身的 GPU 镜像。
要了解如何构建 GPU 容器镜像,先要知道如何要在宿主机上安装 GPU 应用。
以下图左边所示,最底层是先安装 Nvidia 硬件驱动;再到上面是通用的 Cuda 工具库;最上层是 PyTorch、TensorFlow 这类的机器学习框架。
上两层的 CUDA 工具库和应用的耦合度较高,应用版本变更后,对应的 CUDA 版本大几率也要更新;而最下层的 Nvidia 驱动,一般状况下是比较稳定的,它不会像 CUDA 和应用同样,常常更新。
同时 Nvidia 驱动须要内核源码编译,如上图右侧所示,英伟达的 GPU 容器方案是:在宿主机上安装 Nvidia 驱动,而在 CUDA 以上的软件交给容器镜像来作。同时把 Nvidia 驱动里面的连接以 Mount Bind 的方式映射到容器中。
这样的一个好处是:当你安装了一个新的 Nvidia 驱动以后,你就能够在同一个机器节点上运行不一样版本的 CUDA 镜像了。
有了前面的基础,咱们就比较容易理解 GPU 容器的工做机制。下图是一个使用 Docker 运行 GPU 容器的例子。
咱们能够观察到,在运行时刻一个 GPU 容器和普通容器之间的差异,仅仅在于须要将宿主机的设备和 Nvidia 驱动库映射到容器中。
上图右侧反映了 GPU 容器启动后,容器中的 GPU 配置。右上方展现的是设备映射的结果,右下方显示的是驱动库以 Bind 方式映射到容器后,能够看到的变化。
一般你们会使用 Nvidia-docker 来运行 GPU 容器,而 Nvidia-docker 的实际工做就是来自动化作这两个工做。其中挂载设备比较简单,而真正比较复杂的是 GPU 应用依赖的驱动库。
对于深度学习,视频处理等不一样场景,所使用的一些驱动库并不相同。这又须要依赖 Nvidia 的领域知识,而这些领域知识就被贯穿到了 Nvidia 的容器之中。
首先看一下如何给一个 Kubernetes 节点增长 GPU 能力,咱们以 CentOS 节点为例。
如上图所示:
因为 Nvidia 驱动须要内核编译,因此在安装 Nvidia 驱动以前须要安装 gcc 和内核源码。
安装完 Nvidia Docker2 须要从新加载 docker,能够检查 docker 的 daemon.json 里面默认启动引擎已经被替换成了 nvidia,也能够经过 docker info 命令查看运行时刻使用的 runC 是否是 Nvidia 的 runC。
从 Nvidia 的 git repo 下去下载 Device Plugin 的部署声明文件,而且经过 kubectl create 命令进行部署。
这里 Device Plugin 是以 deamonset 的方式进行部署的。这样咱们就知道,若是须要排查一个 Kubernetes 节点没法调度 GPU 应用的问题,须要从这些模块开始入手,好比我要查看一下 Device Plugin 的日志,Nvidia 的 runC 是否配置为 docker 默认 runC 以及 Nvidia 驱动是否安装成功。
当 GPU 节点部署成功后,咱们能够从节点的状态信息中发现相关的 GPU 信息。
站在用户的角度,在 Kubernetes 中使用 GPU 容器仍是很是简单的。
只须要在 Pod 资源配置的 limit 字段中指定 nvidia.com/gpu 使用 GPU 的数量,以下图样例中咱们设置的数量为 1;而后再经过 kubectl create 命令将 GPU 的 Pod 部署完成。
部署完成后能够登陆到容器中执行 nvidia-smi 命令观察一下结果,能够看到在该容器中使用了一张 T4 的 GPU 卡。说明在该节点中的两张 GPU 卡其中一张已经能在该容器中使用了,可是节点的另一张卡对于改容器来讲是彻底透明的,它是没法访问的,这里就体现了 GPU 的隔离性。
Kubernetes 自己是经过插件扩展的机制来管理 GPU 资源的,具体来讲这里有两个独立的内部机制。
Extend Resources 属于 Node-level 的 api,彻底能够独立于 Device Plugin 使用。而上报 Extend Resources,只须要经过一个 PACTH API 对 Node 对象进行 status 部分更新便可,而这个 PACTH 操做能够经过一个简单的 curl 命令来完成。这样,在 Kubernetes 调度器中就可以记录这个节点的 GPU 类型,它所对应的资源数量是 1。
固然若是使用的是 Device Plugin,就不须要作这个 PACTH 操做,只须要听从 Device Plugin 的编程模型,在设备上报的工做中 Device Plugin 就会完成这个操做。
介绍一下 Device Plugin 的工做机制,整个 Device Plugin 的工做流程能够分红两个部分:
Device Plugin 的开发很是简单。主要包括最关注与最核心的两个事件方法:
对于每个硬件设备,都须要它所对应的 Device Plugin 进行管理,这些 Device Plugin 以客户端的身份经过 GRPC 的方式对 kubelet 中的 Device Plugin Manager 进行链接,而且将本身监听的 Unis socket api 的版本号和设备名称好比 GPU,上报给 kubelet。
咱们来看一下 Device Plugin 资源上报的整个流程。总的来讲,整个过程分为四步,其中前三步都是发生在节点上,第四步是 kubelet 和 api-server 的交互。
须要注意的是 kubelet 在向 api-server 进行汇报的时候,只会汇报该 GPU 对应的数量。而 kubelet 自身的 Device Plugin Manager 会对这个 GPU 的 ID 列表进行保存,并用来具体的设备分配。而这个对于 Kubernetes 全局调度器来讲,它不掌握这个 GPU 的 ID 列表,它只知道 GPU 的数量。
这就意味着在现有的 Device Plugin 工做机制下,Kubernetes 的全局调度器没法进行更复杂的调度。好比说想作两个 GPU 的亲和性调度,同一个节点两个 GPU 可能须要进行经过 NVLINK 通信而不是 PCIe 通信,才能达到更好的数据传输效果。在这种需求下,目前的 Device Plugin 调度机制中是没法实现的。
Pod 想使用一个 GPU 的时候,它只须要像以前的例子同样,在 Pod 的 Resource 下 limits 字段中声明 GPU 资源和对应的数量 (好比nvidia.com/gpu: 1)。Kubernetes 会找到知足数量条件的节点,而后将该节点的 GPU 数量减 1,而且完成 Pod 与 Node 的绑定。
绑定成功后,天然就会被对应节点的 kubelet 拿来建立容器。而当 kubelet 发现这个 Pod 的容器请求的资源是一个 GPU 的时候,kubelet 就会委托本身内部的 Device Plugin Manager 模块,从本身持有的 GPU 的 ID 列表中选择一个可用的 GPU 分配给该容器。
此时 kubelet 就会向本机的 DeAvice Plugin 发起一个 Allocate 请求,这个请求所携带的参数,正是即将分配给该容器的设备 ID 列表。
Device Plugin 收到 AllocateRequest 请求以后,它就会根据 kubelet 传过来的设备 ID,去寻找这个设备 ID 对应的设备路径、驱动目录以及环境变量,而且以 AllocateResponse 的形式返还给 kubelet。
AllocateResponse 中所携带的设备路径和驱动目录信息,一旦返回给 kubelet 以后,kubelet 就会根据这些信息执行为容器分配 GPU 的操做,这样 Docker 会根据 kubelet 的指令去建立容器,而这个容器中就会出现 GPU 设备。而且把它所须要的驱动目录给挂载进来,至此 Kubernetes 为 Pod 分配一个 GPU 的流程就结束了。
在本文中,咱们一块儿学习了在 Docker 和 Kubernetes 上使用 GPU。
最后咱们来思考一个问题,如今的 Device Plugin 是否天衣无缝?
须要指出的是 Device Plugin 整个工做机制和流程上,实际上跟学术界和工业界的真实场景有比较大的差别。这里最大的问题在于 GPU 资源的调度工做,实际上都是在 kubelet 上完成的。
而做为全局的调度器对这个参与是很是有限的,做为传统的 Kubernetes 调度器来讲,它只能处理 GPU 数量。一旦你的设备是异构的,不能简单地使用数目去描述需求的时候,好比个人 Pod 想运行在两个有 nvlink 的 GPU 上,这个 Device Plugin 就彻底不能处理。
更不用说在许多场景上,咱们但愿调度器进行调度的时候,是根据整个集群的设备进行全局调度,这种场景是目前的 Device Plugin 没法知足的。
更为棘手的是在 Device Plugin 的设计和实现中,像 Allocate 和 ListAndWatch 的 API 去增长可扩展的参数也是没有做用的。这就是当咱们使用一些比较复杂的设备使用需求的时候,其实是没法经过 Device Plugin 来扩展 API 实现的。
所以目前的 Device Plugin 设计涵盖的场景实际上是很是单一的, 是一个可用可是很差用的状态。这就能解释为何像 Nvidia 这些厂商都实现了一个基于 Kubernetes 上游代码进行 fork 了本身解决方案,也是不得已而为之。
本文做者:车漾
本文为阿里云内容,未经容许不得转载。