乐趣区

Nvidia GPU如何在Kubernetes 里工作

Nvidia GPU 如何在 Kubernetes 里工作
本文介绍 Nvidia GPU 设备如何在 Kubernetes 中管理调度。整个工作流程分为以下两个方面:

如何在容器中使用 GPU
Kubernetes 如何调度 GPU

如何在容器中使用 GPU
想要在容器中的应用可以操作 GPU,需要实两个目标

容器中可以查看 GPU 设备
容器中运行的应用,可以通过 Nvidia 驱动操作 GPU 显卡

详细介绍可见:https://devblogs.nvidia.com/gpu-containers-runtime/
Nvidia-docker
GitHub: https://github.com/NVIDIA/nvidia-dockerNvidia 提供 Nvidia-docker 项目,它是通过修改 Docker 的 Runtime 为 nvidia runtime 工作,当我们执行 nvidia-docker create 或者 nvidia-docker run    时,它会默认加上 –runtime=nvidia 参数。将 runtime 指定为 nvidia。当然,为了方便使用,可以直接修改 Docker daemon 的启动参数,修改默认的 Runtime 为 nvidia-container-runtime 
cat /etc/docker/daemon.json
{
“default-runtime”: “nvidia”,
“runtimes”: {
“nvidia”: {
“path”: “/usr/bin/nvidia-container-runtime”,
“runtimeArgs”: []
}
}
}
gpu-containers-runtime
GitHub:  https://github.com/NVIDIA/nvidia-container-runtimegpu-containers-runtime  是一个 NVIDIA 维护的容器 Runtime,它在 runc 的基础上,维护了一份 Patch,我们可以看到这个 patch 的内容非常简单,唯一做的一件事情就是在容器启动前,注入一个 prestart  的 hook 到容器的 Spec 中(hook 的定义可以查看 OCI 规范)。这个 hook 的执行时机是在容器启动后(Namespace 已创建完成),容器自定义命令 (Entrypoint) 启动前。nvidia-containers-runtime 定义的 prestart 的命令很简单,只有一句  nvidia-container-runtime-hook prestart  
gpu-containers-runtime-hook
GitHub: https://github.com/NVIDIA/nvidia-container-runtime/tree/master/hook/nvidia-container-runtime-hook gpu-containers-runtime-hook  是一个简单的二进制包,定义在 Nvidia container runtime 的 hook 中执行。目的是将当前容器中的信息收集并处理,转换为参数调用 nvidia-container-cli。主要处理以下参数:

根据环境变量 NVIDIA_VISIBLE_DEVICES 判断是否会分配 GPU 设备,以及挂载的设备 ID。如果是未指定或者是 void,则认为是非 GPU 容器,不做任何处理。否则调用 nvidia-container-cli,GPU 设备作为 –devices  参数传入
环境环境变量 NVIDIA_DRIVER_CAPABILITIES 判断容器需要被映射的 Nvidia 驱动库。
环境变量 NVIDIA_REQUIRE_*  判断 GPU 的约束条件。例如 cuda>=9.0 等。作为 –require= 参数传入
传入容器进程的 Pid

gpu-containers-runtime-hook  做的事情,就是将必要的信息整理为参数,传给 nvidia-container-cli configure 并执行。
nvidia-container-cli
nvidia-container-cli 是一个命令行工具,用于配置 Linux 容器对 GPU 硬件的使用。支持

list:  打印 nvidia 驱动库及路径
info:  打印所有 Nvidia GPU 设备
configure:进入给定进程的命名空间,执行必要操作保证容器内可以使用被指定的 GPU 以及对应能力(指定 Nvidia 驱动库)。configure 是我们使用到的主要命令,它将 Nvidia 驱动库的 so 文件 和 GPU 设备信息,通过文件挂载的方式映射到容器中。

代码如下:https://github.com/NVIDIA/libnvidia-container/blob/master/src/cli/configure.c#L272
/* 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;
}
}

如果对其他模块感兴趣,可以在 https://github.com/NVIDIA/libnvidia-container  阅读代码。
以上就是一个 nvidia-docker 的容器启动的所有步骤。

当我们安装了 nvidia-docker,我们可以通过以下方式启动容器
docker run –rm -it -e NVIDIA_VISIBLE_DEVICES=all ubuntu:18.04
在容器中执行 mount  命令,可以看到名为 libnvidia-xxx.so 和 /proc/driver/nvidia/gpus/xxx  映射到容器中。以及 nvidia-smi 和 nvidia-debugdump 等 nvidia 工具。
# mount
## ….
/dev/vda1 on /usr/bin/nvidia-smi type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/bin/nvidia-debugdump type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/bin/nvidia-persistenced type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/bin/nvidia-cuda-mps-control type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/bin/nvidia-cuda-mps-server type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-ml.so.396.37 type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-cfg.so.396.37 type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/lib/x86_64-linux-gnu/libcuda.so.396.37 type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-opencl.so.396.37 type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-ptxjitcompiler.so.396.37 type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-fatbinaryloader.so.396.37 type ext4 (ro,nosuid,nodev,relatime,data=ordered)
/dev/vda1 on /usr/lib/x86_64-linux-gnu/libnvidia-compiler.so.396.37 type ext4 (ro,nosuid,nodev,relatime,data=ordered)
devtmpfs on /dev/nvidiactl type devtmpfs (ro,nosuid,noexec,relatime,size=247574324k,nr_inodes=61893581,mode=755)
devtmpfs on /dev/nvidia-uvm type devtmpfs (ro,nosuid,noexec,relatime,size=247574324k,nr_inodes=61893581,mode=755)
devtmpfs on /dev/nvidia-uvm-tools type devtmpfs (ro,nosuid,noexec,relatime,size=247574324k,nr_inodes=61893581,mode=755)
devtmpfs on /dev/nvidia4 type devtmpfs (ro,nosuid,noexec,relatime,size=247574324k,nr_inodes=61893581,mode=755)
proc on /proc/driver/nvidia/gpus/0000:00:0e.0 type proc (ro,nosuid,nodev,noexec,relatime)
我们可以执行 nvidia-smi 查看容器中被映射的 GPU 卡

Kubernetes 如何调度 GPU
之前我们介绍了如何在容器中使用 Nvidia GPU 卡。那么当一个集群中有成百上千个节点以及 GPU 卡,我们的问题变成了如何管理和调度这些 GPU。
Device plugin
Kubernetes 提供了 Device Plugin 的机制,用于异构设备的管理场景。原理是会为每个特殊节点上启动一个针对某个设备的 DevicePlugin pod,这个 pod 需要启动 grpc 服务,给 kubelet 提供一系列接口。
type DevicePluginClient interface {
// GetDevicePluginOptions returns options to be communicated with Device
// Manager
GetDevicePluginOptions(ctx context.Context, in *Empty, opts …grpc.CallOption) (*DevicePluginOptions, error)
// ListAndWatch returns a stream of List of Devices
// Whenever a Device state change or a Device disapears, ListAndWatch
// returns the new list
ListAndWatch(ctx context.Context, in *Empty, opts …grpc.CallOption) (DevicePlugin_ListAndWatchClient, error)
// Allocate is called during container creation so that the Device
// Plugin can run device specific operations and instruct Kubelet
// of the steps to make the Device available in the container
Allocate(ctx context.Context, in *AllocateRequest, opts …grpc.CallOption) (*AllocateResponse, error)
// PreStartContainer is called, if indicated by Device Plugin during registeration phase,
// before each container start. Device plugin can run device specific operations
// such as reseting the device before making devices available to the container
PreStartContainer(ctx context.Context, in *PreStartContainerRequest, opts …grpc.CallOption) (*PreStartContainerResponse, error)
}
DevicePlugin 注册一个 socket 文件到 /var/lib/kubelet/device-plugins/ 目录下,kubelet 通过这个目录下的 socket 文件向对应的 Device plugin 发送 grpc 请求。本文不过多介绍 Device Plugin 的设计,感兴趣可以阅读这篇文章:https://yq.aliyun.com/articles/498185
Nvidia plugin
Github:https://github.com/NVIDIA/k8s-device-plugin 为了能够在 Kubernetes 中管理和调度 GPU,Nvidia 提供了 Nvidia GPU 的 Device Plugin。主要功能如下

支持 ListAndWatch 接口,上报节点上的 GPU 数量
支持 Allocate 接口,支持分配 GPU 的行为。

Allocate 接口只做了一件事情,就是给容器加上 NVIDIA_VISIBLE_DEVICES  环境变量。https://github.com/NVIDIA/k8s-device-plugin/blob/v1.11/server.go#L153
// Allocate which return list of devices.
func (m *NvidiaDevicePlugin) Allocate(ctx context.Context, reqs *pluginapi.AllocateRequest) (*pluginapi.AllocateResponse, error) {
devs := m.devs
responses := pluginapi.AllocateResponse{}
for _, req := range reqs.ContainerRequests {
response := pluginapi.ContainerAllocateResponse{
Envs: map[string]string{
“NVIDIA_VISIBLE_DEVICES”: strings.Join(req.DevicesIDs, “,”),
},
}

for _, id := range req.DevicesIDs {
if !deviceExists(devs, id) {
return nil, fmt.Errorf(“invalid allocation request: unknown device: %s”, id)
}
}

responses.ContainerResponses = append(responses.ContainerResponses, &response)
}

return &responses, nil
}
前面我们提到,Nvidia 的 gpu-container-runtime  根据容器的 NVIDIA_VISIBLE_DEVICES 环境变量,会决定这个容器是否为 GPU 容器,并且可以使用哪些 GPU 设备。而 Nvidia GPU device plugin 做的事情,就是根据 kubelet 请求中的 GPU DeviceId,转换为 NVIDIA_VISIBLE_DEVICES 环境变量返回给 kubelet,kubelet 收到返回内容后,会自动将返回的环境变量注入到容器中。当容器中包含环境变量,启动时 gpu-container-runtime  会根据 NVIDIA_VISIBLE_DEVICES 里声明的设备信息,将设备映射到容器中,并将对应的 Nvidia Driver Lib 也映射到容器中。
总体流程
整个 Kubernetes 调度 GPU 的过程如下:

GPU Device plugin 部署到 GPU 节点上,通过 ListAndWatch  接口,上报注册节点的 GPU 信息和对应的 DeviceID。
当有声明 nvidia.com/gpu  的 GPU Pod 创建出现,调度器会综合考虑 GPU 设备的空闲情况,将 Pod 调度到有充足 GPU 设备的节点上。
节点上的 kubelet 启动 Pod 时,根据 request 中的声明调用各个 Device plugin 的 allocate 接口,由于容器声明了 GPU。kubelet 根据之前 ListAndWatch 接口收到的 Device 信息,选取合适的设备,DeviceID 作为参数,调用 GPU DevicePlugin 的 Allocate 接口
GPU DevicePlugin,接收到调用,将 DeviceID 转换为 NVIDIA_VISIBLE_DEVICES 环境变量,返回 kubelet
kubelet 将环境变量注入到 Pod,启动容器
容器启动时,gpu-container-runtime 调用 gpu-containers-runtime-hook 

gpu-containers-runtime-hook  根据容器的 NVIDIA_VISIBLE_DEVICES 环境变量,转换为 –devices 参数,调用 nvidia-container-cli prestart  

nvidia-container-cli 根据 –devices,将 GPU 设备映射到容器中。并且将宿主机的 Nvidia Driver Lib 的 so 文件也映射到容器中。此时容器可以通过这些 so 文件,调用宿主机的 Nvidia Driver。

本文作者:萧元阅读原文
本文为云栖社区原创内容,未经允许不得转载。

退出移动版