Nvidia GPU如何在Kubernetes 裡工作

Nvidia GPU如何在Kubernetes 裡工作

本文介紹Nvidia GPU設備如何在Kubernetes中管理調度。 整個工作流程分為以下兩個方面:

  • 如何在容器中使用GPU
  • Kubernetes 如何調度GPU

如何在容器中使用GPU

想要在容器中的應用可以操作GPU, 需要實兩個目標

  1. 容器中可以查看GPU設備
  2. 容器中運行的應用,可以通過Nvidia驅動操作GPU顯卡

詳細介紹可見: https://devblogs.nvidia.com/gpu-containers-runtime/

Nvidia-docker

GitHub: https://github.com/NVIDIA/nvidia-docker

Nvidia提供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-runtime

gpu-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 GPU如何在Kubernetes 裡工作

當我們安裝了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卡

Nvidia GPU如何在Kubernetes 裡工作

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。


分享到:


相關文章: