Nvidia docker runtime原理
场景:
docker 本身并不原生支持GPU,但使用docker的现有功能可以对GPU的使用进行支持。
docker run \
--device /dev/nvidia0:/dev/nvidia0 \
--device /dev/nvidiactl:/dev/nvidiactl \
--device /dev/nvidia-uvm:/dev/nvidia-uvm \
-v /usr/local/nvidia:/usr/local/nvidia \
-it --privileged nvidia/cuda
如上所述,通过 --device 来指定挂载的GPU设备,通过 -v 来将宿主机上的 nvidia gpu 的命令行工具和相关的依赖库挂载到容器。这样,在容器中就可以看到和使用宿主机上的GPU设备了。 这样使用,对于GPU的可用性(哪些GPU是空闲的等)需要人为的判断,效率很低。为了提高Nvidia GPU在 docker 中的易用性, Nvidia 通过对原生docker的封装实现了自己的 nvidia-docker 工具。
nvidia-docker 对于使用GPU资源的docker容器支持的层次关系:
Nvidia-docker的原理图以及各个部分的作用解析
libnvidia-container:
libnvidia-container提供了一个库和简单的CLI工具,以实现在容器当中支持使用GPU设备的目标。
nvidia-container-toolkit:
nvidia-container-toolkit是一个实现了runC prestart hook接口的脚本,该脚本在runC创建一个容器之后,启动该容器之前调用,其主要作用就是修改与容器相关联的config.json,注入一些在容器中使用NVIDIA GPU设备所需要的一些信息(比如:需要挂载哪些GPU设备到容器当中)。
nvidia-container-runtime:
nvidia-container-runtime主要用于将容器runC spec作为输入,然后将nvidia-container-toolkit脚本作为一个prestart hook注入到runC spec中,将修改后的runC spec交给runC处理。
nvidia-container-runtime 才是真正的核心部分,它在原有的docker容器运行时runc的基础上增加一个prestart hook,用于调用libnvidia-container库。
RunC:
RunC 是一个轻量级的工具,它是用来运行容器的,只用来做这一件事,并且这一件事要做好。我们可以认为它就是个命令行小工具,可以不用通过 docker 引擎,直接运行容器。事实上,runC 是标准化的产物,它根据 OCI 标准来创建和运行容器。而 OCI(Open Container Initiative)组织,旨在围绕容器格式和运行时制定一个开放的工业化标准。
直接使用RunC的命令行即可以完成创建一个容器,并提供了简单的交互能力。
容器创建
正常创建一个容器的流程是这样的:
docker --> dockerd --> containerd–> containerd-shim -->runc --> container-process
docker客户端将创建容器的请求发送给dockerd, 当dockerd收到请求任务之后将请求发送给containerd, containerd经过查看校验启动containerd-shim或者自己来启动容器进程。
创建一个使用GPU的容器
docker–> dockerd --> containerd --> containerd-shim–> nvidia-container-runtime --> nvidia-container-runtime-hook --> libnvidia-container --> runc – > container-process
基本流程和不使用GPU的容器差不多,只是把docker默认的运行时替换成了NVIDIA自家的nvidia-container-runtime。
这样当nvidia-container-runtime创建容器时,先执行nvidia-container-runtime-hook这个hook去检查容器是否需要使用GPU(通过环境变NVIDIA_VISIBLE_DEVICES来判断)。如果需要则调用libnvidia-container来暴露GPU给容器使用。否则走默认的runc逻辑。
NVIDIA Docker 整体工作架构
1、硬件,服务器上安装了英伟达 GPU
2、宿主机,安装了操作系统和 Cuda Driver,以及 Docker 引擎
3、容器,包含容器 OS 用户空间,Cuda Toolkit,以及用户应用程序
最重要的是,宿主机上需要安装 cuda driver,容器内需要安装 cuda toolkit。容器内无需安装 cuda driver。
NVIDIA 提供了一些官方镜像,其中已经安装好了 cuda toolkit,但还是需要在宿主机安装 cuda driver。
CUDA 结构图如下:
提供了 cuda driver api,cuda runtime api 和 cuda liabaries 三层 API
CUDA Driver API:
GPU设备的抽象层,通过提供一系列接口来操作GPU设备,性能最好,但编程难度高,
一般不会使用该方式开发应用程序。
CUDA Runtime API:
对CUDA Driver API进行了一定的封装,调用该类API可简化编程过程,降低开发难度;
CUDA Libraries:
是对CUDA Runtime API更高一层的封装,通常是一些成熟的高效函数库,
开发者也可以自己封装一些函数库便于使用;
CUDA调用关系:
应用程序可调用CUDA Libraries或者CUDA Runtime API来实现功能,当调用CUDA Libraries时,CUDA Libraries会调用相应的CUDA Runtime API,CUDA Runtime API再调用CUDA Driver API,CUDA Driver API再操作GPU设备。
如何实现CUDA容器化:
目标:CUDA容器化的目标就是要能让应用程序可以在容器内调用CUDA API来操作GPU。
因此需要实现:
1、在容器内应用程序可调用CUDA Runtime API和CUDA Libraries
2、在容器内能使用CUDA Driver相关库。因为CUDA Runtime API其实就是CUDA Driver API的封装,底层还是要调用到CUDA Driver API
3、在容器内可操作GPU设备
因此容器中访问 GPU 资源过程为:
要在容器内操作GPU设备,需要将GPU设备挂载到容器里,Docker可通过 --device 挂载需要操作的设备,或者直接使用特权模式(不推荐)。NVIDIA Docker是通过注入一个 prestart 的hook 到容器中,在容器自定义命令启动前就将GPU设备挂载到容器中。至于要挂载哪些GPU,可通过 NVIDIA_VISIBLE_DEVICES 环境变量控制。
挂载GPU设备到容器后,还要在容器内可调用CUDA API。CUDA Runtime API和CUDA Libraries通常跟应用程序一起打包到镜像里,而CUDA Driver API是在宿主机里,需要将其挂载到容器里才能被使用。NVIDIA Docker挂载CUDA Driver库文件到容器的方式和挂载GPU设备一样,都是在runtime hook里实现的。
注意:
该方案也有一些问题,即容器内的 cuda toolkit 同宿主机的 cuda driver 可能存在版本不兼容的问题。
NVIDIA Docker CUDA容器化分析:
这里主要分析下NVIDIA Docker 2.0的实现
修改Docker daemon 的启动参数,将默认的 Runtime修改为 nvidia-container-runtime后,可实现将GPU设备,CUDA Driver库挂载到容器中。
cat /etc/docker/daemon.json
{
"default-runtime": "nvidia",
"runtimes": {
"nvidia": {
"path": "/usr/bin/nvidia-container-runtime",
"runtimeArgs": []
}
}
}
nvidia-container-runtime实现如下:
nvidia-container-runtime其实就是在runc基础上多实现了nvidia-container-runime-hook,该hook是在容器启动后(Namespace已创建完成),容器自定义命令(Entrypoint)启动前执行。当检测到NVIDIA_VISIBLE_DEVICES环境变量时,会调用libnvidia-container挂载GPU Device和CUDA Driver。如果没有检测到NVIDIA_VISIBLE_DEVICES就会执行默认的runc。
NVIDIA Container Toolkit
如上图所示, NVIDIA 将原来 CUDA 应用依赖的API环境划分为两个部分:
1、驱动级API:由libcuda.so.major.minor动态库和内核module提供支持,图中表示为CUDA Driver
驱动级API属于底层API,每当NVIDIA公司释放出某一个版本的驱动时,如果你要升级主机上的驱动,那么内核模块和libcuda.so.major.minor这2个文件就必须同时升级到同一个版本,这样原有的程序才能正常工作,
不同版本的驱动不能同时存在于宿主机上
2、非驱动级API:由动态库libcublas.so等用户空间级别的API组成,图中表示为CUDA Toolkit
非驱动级API的版本号是以Toolkit自身的版本号来管理, 比如cuda-10,cuda-11
不同版本的Toolkit可以同时运行在相同的宿主机上
非驱动级API算是对驱动级API的一种更高级的封装,最终还是要调用驱动级API来实现功能
为了让使用GPU的容器更具可扩展性,关于非驱动级的API被 NVIDIA 打包进了 NVIDIA Container Toolkit,因此在容器中使用GPU之前,每个机器需要先安装好NVIDIA驱动,之后配置好 NVIDIA Container Toolkit之后,就可以在容器中方便使用GPU了。