在AI应用容器化时,会碰到cuda failure 35错误,查了下是跟CUDA驱动版本有关。但有时同一个镜像在不同环境运行仍会有问题,查了下宿主机的显卡驱动版本,也没发现什么问题。为了彻底解决这类问题,了解了CUDA API的体系结构,并对NVIDIA Docker实现CUDA容器化原理进行了分析。
CUDA是由NVIDIA推出的通用并行计算架构,通过一些CUDA库提供了一系列API供应用程序调用。开发者可调用这些API充分利用GPU来处理图像,视频解码等。
CUDA API体系包括:CUDA函数库(CUDA Libraries),CUDA运行时API(CUDA Runtime API),CUDA驱动API(CUDA Driver API),结构图如下
GPU设备的抽象层,通过提供一系列接口来操作GPU设备,性能最好,但编程难度高,一般不会使用该方式开发应用程序。
对CUDA Driver API进行了一定的封装,调用该类API可简化编程过程,降低开发难度;
是对CUDA Runtime API更高一层的封装,通常是一些成熟的高效函数库,开发者也可以自己封装一些函数库便于使用;
应用程序可调用CUDA Libraries或者CUDA Runtime API来实现功能,当调用CUDA Libraries时,CUDA Libraries会调用相应的CUDA Runtime API,CUDA Runtime API再调用CUDA Driver API,CUDA Driver API再操作GPU设备。
了解了CUDA API体系结构后,看下如何将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设备挂载到容器里,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里实现的。
接下来分析NVIDIA Docker中是如何实现将GPU Device和CUDA Driver挂载到容器中的。
NVIDIA Docker分两个版本,1.0版本通过docker volume 将CUDA Driver挂载到容器里,应用程序要操作GPU,需要在LD_LIBRARY_PATH
环境变量中配置CUDA Driver库所在路径。2.0版本通过修改docker的runtime实现GPU设备和CUDA Driver挂载。
这里主要分析下NVIDIA Docker 2.0的实现。修改Docker daemon 的启动参数,将默认的 Runtime修改为 nvidia-container-runtime
后,可实现将GPU设备,CUDA Driver库挂载到容器中。
[root@VM_8_4_centos ~]# cat /etc/docker/daemon.json
{
“default-runtime”: “nvidia”,
“runtimes”: {
“nvidia”: {
“path”: “/usr/bin/nvidia-container-runtime”,
“runtimeArgs”: []
}
}
}
nvidia-container-runtime其实就是在runc基础上多实现了nvidia-container-runime-hook,该hook是在容器启动后(Namespace已创建完成),容器自定义命令(Entrypoint)启动前执行。当检测到NVIDIA_VISIBLE_DEVICES
环境变量时,会调用libnvidia-container挂载GPU Device和CUDA Driver。如果没有检测到NVIDIA_VISIBLE_DEVICES就会执行默认的runc。
看下libnvidia-container库的实现(详细代码可见https://github.com/NVIDIA/libnvidia-container)
/* Query the driver and device information. */
if (perm_set_capabilities(&err, CAP_EFFECTIVE, ecaps[NVC_INFO], ecaps_size(NVC_INFO)) < 0) {
warnx(“permission error: %s”, err.msg);
goto fail;
}
if ((drv = nvc_driver_info_new(nvc, NULL)) == NULL ||
(dev = nvc_device_info_new(nvc, NULL)) == NULL) {
warnx(“detection error: %s”, nvc_error(nvc));
goto fail;
}
/* Select the visible GPU devices. */
if (dev->ngpus > 0) {
gpus = alloca(dev->ngpus * sizeof(*gpus));
memset(gpus, 0, dev->ngpus * sizeof(*gpus));
if (select_devices(&err, ctx->devices, gpus, dev->gpus, dev->ngpus) < 0) {
warnx(“device error: %s”, err.msg);
goto fail;
}
}
nvc_driver_info_new()和nvc_device_info_new()方法分别获取了CUDA Driver和GPU Device相关信息,如driver libraries,driver binaries路径,cuda version等。再通过select_devices()方法刷选出容器可用的GPU Device。
获取到CUDA Driver Libraries/Binaries路径,以及可用的GPU后,将其挂载到容器中。实现如下
/* Mount the driver and visible devices. */
if (perm_set_capabilities(&err, CAP_EFFECTIVE, ecaps[NVC_MOUNT], ecaps_size(NVC_MOUNT)) < 0) {
warnx(“permission error: %s”, err.msg);
goto fail;
}
if (nvc_driver_mount(nvc, cnt, drv) < 0) {
warnx(“mount error: %s”, nvc_error(nvc));
goto fail;
}
for (size_t i = 0; i < dev->ngpus; ++i) {
if (gpus[i] != NULL && nvc_device_mount(nvc, cnt, gpus[i]) < 0) {
warnx(“mount error: %s”, nvc_error(nvc));
goto fail;
}
}
libnvidia-container是采用linux c mount --bind功能将CUDA Driver Libraries/Binaries一个个挂载到容器里,而不是将整个目录挂载到容器中。
NVIDIA_DRIVER_CAPABILITIES
环境变量指定要挂载的driver libraries/binaries。NVIDIA Docker提供多种可挂载的CUDA Driver Libraries。取值如下(详情可见 https://github.com/NVIDIA/nvidia-container-runtime)compute: required for CUDA and OpenCL applications.
compat32: required for running 32-bit applications.
graphics: required for running OpenGL and Vulkan applications.
utility: required for using nvidia-smi and NVML.
video: required for using the Video Codec SDK.
display: required for leveraging X11 display.
如下,通过指定NVIDIA_DRIVER_CAPABILITIES值,将utility相关库文件和二进制文件挂载到容器里,并在容器中执行mount可查看挂载情况
[root@VM_8_4_centos ~]# docker run --rm --env NVIDIA_DRIVER_CAPABILITIES="utility"
–env NVIDIA_VISIBLE_DEVICES=0
busybox:latest mount | egrep ‘nvidia|cuda’
tmpfs on /proc/driver/nvidia type tmpfs (rw,nosuid,nodev,noexec,relatime,mode=555)
/dev/vda1 on /usr/bin/nvidia-smi type ext3 (ro,nosuid,nodev,noatime,data=ordered)
/dev/vda1 on /usr/bin/nvidia-debugdump type ext3 (ro,nosuid,nodev,noatime,data=ordered)
/dev/vda1 on /usr/bin/nvidia-persistenced type ext3 (ro,nosuid,nodev,noatime,data=ordered)
/dev/vda1 on /usr/lib64/libnvidia-ml.so.418.67 type ext3 (ro,nosuid,nodev,noatime,data=ordered)
/dev/vda1 on /usr/lib64/libnvidia-cfg.so.418.67 type ext3 (ro,nosuid,nodev,noatime,data=ordered)
devtmpfs on /dev/nvidiactl type devtmpfs (ro,nosuid,noexec,size=28763092k,nr_inodes=7190773,mode=755)
devtmpfs on /dev/nvidia0 type devtmpfs (ro,nosuid,noexec,size=28763092k,nr_inodes=7190773,mode=755)
proc on /proc/driver/nvidia/gpus/0000:00:06.0 type proc (ro,nosuid,nodev,noexec,relatime)
CUDA 三层API中,CUDA Libraries和CUDA Runtime API是和应用程序一起打包到镜像中的,所以在应用程序和CUDA Libraries以及CUDA Runtime间通常不会有什么问题。主要问题是在CUDA Runtime和CUDA Driver之间。CUDA Driver库是在创建容器时从宿主机挂载到容器中的,很容易出现版本问题,需要保证CUDA Driver的版本不低于CUDA Runtime版本。
但有时会发现,宿主机CUDA Driver的版本确实高于CUDA Runtime版本,却仍会报CUDA failure 35,甚至CUDA failure 38错误。这时就要检查下容器中真实在使用的CUDA Driver了。比如NVIDIA Docker 1.0版本需要设置LD_LIBRARY_PATH来指定CUDA Driver库路径。而NVIDIA Docker 2.0版本直接将CUDA Driver挂载到/usr/lib64等默认被使用的路径中,不需要再指定。
参考:
https://devblogs.nvidia.com/gpu-containers-runtime/
https://github.com/NVIDIA/libnvidia-container
https://github.com/NVIDIA/nvidia-container-runtime
http://hpcc.qdio.ac.cn/sites/default/files/%E7%9B%B8%E5%85%B3%E8%B5%84%E6%96%99/CUDA%20%E8%BD%AF%E4%BB%B6%E4%BD%93%E7%B3%BB%E5%92%8C%E5%87%BD%E6%95%B0%E5%BA%93.pdf
http://cist.buct.edu.cn/staff/zheng/gpgpu/03-GPU%20Programming%201.pdf