NVIDIA GPU Operator分析二:NVIDIA Container Toolkit安装

背景

我们知道,如果在Kubernetes中支持GPU设备调度,需要做如下的工作:

  • 节点上安装nvidia驱动
  • 节点上安装nvidia-docker
  • 集群部署gpu device plugin,用于为调度到该节点的pod分配GPU设备。

除此之外,如果你需要监控集群GPU资源使用情况,你可能还需要安装DCCM exporter结合Prometheus输出GPU资源监控信息。

要安装和管理这么多的组件,对于运维人员来说压力不小。基于此,NVIDIA开源了一款叫NVIDIA GPU Operator的工具,该工具基于Operator Framework实现,用于自动化管理上面我们提到的这些组件。

NVIDIA GPU Operator有以下的组件构成:

  • 安装nvidia driver的组件
  • 安装nvidia container toolkit的组件
  • 安装nvidia devcie plugin的组件
  • 安装nvidia dcgm exporter组件
  • 安装gpu feature discovery组件

本系列文章不打算一上来就开始讲NVIDIA GPU Operator,而是先把各个组件的安装详细的分析一下,然后手动安装这些组件,最后再来分析NVIDIA GPU Operator就比较简单了。

在本篇文章中,我们将详细介绍NVIDIA GPU Operator安装NVIDIA Container Toolkit组件的原理。

NVIDIA Container Toolkit简介

如果您需要在容器中使用NVIDIA GPU设备,那么NVIDIA Container Toolkit是必不可少的组件。它的主要作用是将NVIDIA GPU设备挂载到容器中。

支持docker的NVIDIA Container Tookit由如下的组件构成:

  • nvidia-docker2
  • nvidia-container-runtime
  • nvidia-container-toolkit
  • libnvidia-container

下面这幅架构图来自NVIDIA官网,详细介绍了各个组件的关系。

NVIDIA GPU Operator分析二:NVIDIA Container Toolkit安装

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-docker2介绍

nvidia-docker2只适用于docker,其主要作用是可以通过环境变量指定容器需要使用节点上哪些GPU。

基于容器安装NVIDIA Container Toolkit

基于容器安装NVIDIA Container Toolkit所涉及的脚本请参考项目container-config,项目所涉及的代码在src目录下,有几个脚本需要说明一下:

  • run.go:容器启动时的执行脚本(入口),由go语言编写,即容器中/work/nvidia-toolkit这个二进制文件。
  • toolkit.sh:用于安装NVIDIA Container Toolkit,/work/nvidia-toolkit将会调用该脚本。
  • docker.go:由go语言编写,该源文件将会编译成容器中/work/docker二进制文件,用户更新节点/etc/docker/daemon.json并重启docker进程使配置生效,/work/docker同样也会被/work/nvidia-toolkit调用。

1.镜像构建

Dockerfile.ubi8为例进行分析,比较重要的是如下的这两步:

  • 将以.go文件编译成二进制文件。
RUN go build -o nvidia-toolkit run.go
RUN go build -o containerd containerd.go
RUN go build -o crio crio.go
RUN go build -o docker docker.go
RUN go build -o toolkit toolkit.go
RUN rm -rf go.* && \
    rm -rf *.go && \
    rm -rf vendor
  • 安装包libnvidia-container1,libnvidia-container-tools,nvidia-container-toolkit, nvidia-container-runtime,然后容器启动时,将这些包中的二进制文件拷贝到宿主机上。
RUN /bin/bash -c " \
    yum install -y procps \
    libnvidia-container1-\${LIBNVIDIA_CONTAINER_VERSION/-/-0.1.} \
    libnvidia-container-tools-\${LIBNVIDIA_CONTAINER_VERSION/-/-0.1.} \
    nvidia-container-toolkit-\${NVIDIA_CONTAINER_TOOLKIT_VERSION/-/-0.1.} \
    nvidia-container-runtime-\${NVIDIA_CONTAINER_RUNTIME_VERSION/-/-0.1.}"

2.挂载相关目录到容器

在启动容器时,需要将节点上如下的目录挂载到容器中。

  • /etc/docker => /etc/docker(仅对docker有效,对containerd需要挂载/etc/containerd):因为需要修改节点/etc/docker/daemon.json这个文件,然后重启docker服务,NVIDIA Container Toolkit才会生效。
  • /run/nvidia => /run/nvidia: 前面我们介绍过,基于容器安装NVIDIA驱动后,节点的/run/nvidia/driver其实是整个driver的rootfs,NVIDIA Container Toolkit需要使用到该目录。
  • /usr/local/nvidia => /usr/local/nvidia:NVIDIA Container Toolkit将会安装到节点的/usr/local/nvidia目录下,所以该目录需要挂载。
  • /var/run => /var/run:重启docker的过程中,需要用到/var/run/docker.sock这个文件,所以该目录需要挂载。

3.安装过程说明

整个安装过程由nvidia-toolkit这个工具完成,容器的启动命令为:

nvidia-toolkit /usr/local/nvidia

其中,/usr/local/nvidia为NVIDIA Container Toolkit的安装路径。

nvidia-toolkit(由run.go编译而成)这个工具主要做如下的几件事:

  • 将自身的进程ID写入/run/nvidia/toolkit.pid中,并启动goroutine捕获系统信号,执行退出清理操作(删除已安装的NVIDIA Container Toolkit,当容器被kill时,会执行卸载NVIDIA Container Toolkit操作)。
  • 如果节点上的/usr/local/nvidia/toolkit存在,表明该节点已经安装了NVIDIA Container Toolkit,那么删除/usr/local/nvidia/toolkit这个目录,重新安装。
  • 调用toolkit.sh执行安装操作。
  • 调用/work/docker(由docker.go编译而成)命令修改docker的配置(/etc/docker/daemon.json),并重启docker。
  • 处于sleep状态,等待系统信号。
// Run runs the core logic of the CLI
func Run(c *cli.Context) error {
	err := VerifyFlags()
	if err != nil {
		return fmt.Errorf("unable to verify flags: %v", err)
	}
  // 1.初始化,主要是将自身进程id写入/run/nvidia/toolkit.pid,并启动goroutine捕获系统信号
	err = Initialize()
	if err != nil {
		return fmt.Errorf("unable to initialize: %v", err)
	}
	defer Shutdown()
  // 2.调用toolkit.sh执行安装操作
	err = InstallToolkit()
	if err != nil {
		return fmt.Errorf("unable to install toolkit: %v", err)
	}
  // 修改runtime配置,以docker为例,主要是修改/etc/docker/daemon.json这个文件
	err = SetupRuntime()
	if err != nil {
		return fmt.Errorf("unable to setup runtime: %v", err)
	}

	if !noDaemonFlag {
		err = WaitForSignal()
		if err != nil {
			return fmt.Errorf("unable to wait for signal: %v", err)
		}

		err = CleanupRuntime()
		if err != nil {
			return fmt.Errorf("unable to cleanup runtime: %v", err)
		}
	}

	return nil
}

toolkit.sh这个脚本主要完成如下的一些事件:

  • 将容器中NVIDIA Container Toolkit组件所涉及的命令行工具和库文件移动到/usr/local/nvidia/toolkit目录下
$  ls /usr/local/nvidia/toolkit/
libnvidia-container.so.1      nvidia-container-cli.real      nvidia-container-runtime.real
libnvidia-container.so.1.3.2  nvidia-container-runtime	     nvidia-container-toolkit
nvidia-container-cli	      nvidia-container-runtime-hook  nvidia-container-toolkit.real
  • 在 /usr/local/nvidia/toolkit/.config/nvidia-container-runtime创建nvidia-container-runtime的配置文件config.toml,并设置nvidia-container-cli.root的值为/run/nvidia/driver。
$ cat /usr/local/nvidia/toolkit/.config/nvidia-container-runtime/config.toml
disable-require = false
#swarm-resource = "DOCKER_RESOURCE_GPU"
#accept-nvidia-visible-devices-envvar-when-unprivileged = true
#accept-nvidia-visible-devices-as-volume-mounts = false

[nvidia-container-cli]
root = "/run/nvidia/driver"
#path = "/usr/bin/nvidia-container-cli"
environment = []
#debug = "/var/log/nvidia-container-toolkit.log"
#ldcache = "/etc/ld.so.cache"
load-kmods = true
#no-cgroups = false
#user = "root:video"
ldconfig = "@/sbin/ldconfig"

[nvidia-container-runtime]
#debug = "/var/log/nvidia-container-runtime.log"

整个安装过程还是比较清晰的,如果需要了解更详细的内容,可以去读一下这些脚本。

/work/docker(针对Runtime为docker)主要是修改/etc/docker/daemon.json的Runtime为nvidia,并重启docker。

    "runtimes": {
        "nvidia": {
            "args": [],
            "path": "/usr/local/nvidia/toolkit/nvidia-container-runtime"
        }
    },

在K8s集群的节点上安装NVIDIA Container Toolkit

这一部分将演示如何基于容器的方式为k8s集群的节点安装NVIDIA Container Toolkit。

前提条件

在执行安装操作前,你需要确认以下的条件是否满足:

  • k8s集群节点上不能安装NVIDIA Container Toolkit,如果已经安装,需要卸载掉。
  • k8s集群节点已经通过基于容器的方式安装了NVIDIA驱动,如果还没安装驱动,请参考本系列之前的文章《NVIDIA GPU Operator分析一:NVIDIA Driver安装》。
  • 本次演示的集群节点的操作系统为Centos7.7,如果是其他类型的操作,不保证安装步骤是否可用。

安装步骤

1.下载gpu-opeator源码。

$ git clone -b 1.6.2 https://github.com/NVIDIA/gpu-operator.git
$ cd gpu-operator
$ export GPU_OPERATOR=$(pwd) 

2.确认节点NVIDIA驱动已经安装。

$ kubectl get po -n gpu-operator-resources -l app=nvidia-driver-daemonset
NAME                            READY   STATUS    RESTARTS   AGE
nvidia-driver-daemonset-4kxr2   1/1     Running   0          10m
nvidia-driver-daemonset-8fdqz   1/1     Running   0          10m
nvidia-driver-daemonset-ng6jn   1/1     Running   0          10m

# 进入容器执行nvidia-smi,检查能否执行成功。
$ kubectl exec -ti nvidia-driver-daemonset-4kxr2 -n gpu-operator-resources -- nvidia-smi
Thu Mar 25 12:29:43 2021
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 450.102.04   Driver Version: 450.102.04   CUDA Version: 11.0     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|                               |                      |               MIG M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  On   | 00000000:00:07.0 Off |                    0 |
| N/A   29C    P0    24W / 300W |      0MiB / 16160MiB |      0%      Default |
|                               |                      |                  N/A |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                                  |
|  GPU   GI   CI        PID   Type   Process name                  GPU Memory |
|        ID   ID                                                   Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

3.确认节点已经打了标签nvidia.com/gpu.present=true。

$ kubectl get nodes -L nvidia.com/gpu.present
NAME                       STATUS   ROLES    AGE   VERSION            GPU.PRESENT
cn-beijing.192.168.8.44    Ready    <none>   13d   v1.16.9-aliyun.1   true
cn-beijing.192.168.8.45    Ready    <none>   13d   v1.16.9-aliyun.1   true
cn-beijing.192.168.8.46    Ready    <none>   13d   v1.16.9-aliyun.1   true
cn-beijing.192.168.9.159   Ready    master   13d   v1.16.9-aliyun.1
cn-beijing.192.168.9.160   Ready    master   13d   v1.16.9-aliyun.1
cn-beijing.192.168.9.161   Ready    master   13d   v1.16.9-aliyun.1

4.修改assets/state-container-toolkit/0300_rolebinding.yaml,注释两个字段,否则无法提交:

  • 将userNames这一行和其后面的一行注释。
#userNames:
#- system:serviceaccount:gpu-operator:nvidia-container-toolkit 
  • 将roleRef.namespace这一行注释。
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: nvidia-container-toolkit
  #namespace: gpu-operator-resources

5.修改assets/state-container-toolkit/0400_container_toolkit.yml这个文件。对于1.6.2这个版本,直接提交这个yaml会失败,所以需要做如下修改:

apiVersion: apps/v1
kind: DaemonSet
metadata:
  labels:
    app: nvidia-container-toolkit-daemonset
  name: nvidia-container-toolkit-daemonset
  namespace: gpu-operator-resources
  annotations:
    openshift.io/scc: hostmount-anyuid
spec:
  selector:
    matchLabels:
      app: nvidia-container-toolkit-daemonset
  template:
    metadata:
      # Mark this pod as a critical add-on; when enabled, the critical add-on scheduler
      # reserves resources for critical add-on pods so that they can be rescheduled after
      # a failure.  This annotation works in tandem with the toleration below.
      annotations:
        scheduler.alpha.kubernetes.io/critical-pod: ""
      labels:
        app: nvidia-container-toolkit-daemonset
    spec:
      tolerations:
      # Allow this pod to be rescheduled while the node is in "critical add-ons only" mode.
      # This, along with the annotation above marks this pod as a critical add-on.
      - key: CriticalAddonsOnly
        operator: Exists
      - key: nvidia.com/gpu
        operator: Exists
        effect: NoSchedule
      serviceAccount: nvidia-container-toolkit
      hostPID: true
      initContainers:
      - name: driver-validation
        # 替换initContainer的IMAGE
        image: "nvcr.io/nvidia/cuda@sha256:ed723a1339cddd75eb9f2be2f3476edf497a1b189c10c9bf9eb8da4a16a51a59"
        imagePullPolicy: IfNotPresent
        command: ['sh', '-c']
        args: ["export SYS_LIBRARY_PATH=$(ldconfig -v 2>/dev/null | grep -v '^[[:space:]]' | cut -d':' -f1 | tr '[[:space:]]' ':'); \
        export NVIDIA_LIBRARY_PATH=/run/nvidia/driver/usr/lib/x86_64-linux-gnu/:/run/nvidia/driver/usr/lib64; \
        export LD_LIBRARY_PATH=${SYS_LIBRARY_PATH}:${NVIDIA_LIBRARY_PATH}; echo ${LD_LIBRARY_PATH}; \
        export PATH=/run/nvidia/driver/usr/bin/:${PATH}; \
        until nvidia-smi; do echo waiting for nvidia drivers to be loaded; sleep 5; done"]
        securityContext:
          privileged: true
          seLinuxOptions:
            level: "s0"
        volumeMounts:
          - name: nvidia-install-path
            mountPath: /run/nvidia
            mountPropagation: Bidirectional
      containers:
        # 替换container的IMAGE
      - image: "nvcr.io/nvidia/k8s/container-toolkit:1.4.3-ubi8"
        args: ["/usr/local/nvidia"]
        env:
        - name: RUNTIME_ARGS
          value: ""
        imagePullPolicy: IfNotPresent
        name: nvidia-container-toolkit-ctr
        securityContext:
          privileged: true
          seLinuxOptions:
            level: "s0"
        # 需要添加挂载点/etc/docker,/var/run    
        volumeMounts:
          - name: docker-config
            mountPath: /etc/docker
          - name: docker-socket
            mountPath: /var/run
          - name: nvidia-install-path
            mountPath: /run/nvidia
            mountPropagation: Bidirectional
          - name: nvidia-local
            mountPath: /usr/local/nvidia
          - name: crio-hooks
            mountPath: /usr/share/containers/oci/hooks.d
      volumes:
        - name: nvidia-install-path
          hostPath:
            path: /run/nvidia
        # 将节点的/etc/docker挂载到容器的/etc/docker    
        - name: docker-config
          hostPath:
            path: /etc/docker
        # 将节点的/var/run挂载到容器的/var/run
        - name: docker-socket
          hostPath:
            path: /var/run
        - name: nvidia-local
          hostPath:
            path: /usr/local/nvidia
        - name: crio-hooks
          hostPath:
            path: /run/containers/oci/hooks.d
      nodeSelector:
        nvidia.com/gpu.present: "true"

6.提交应用。

$ kubectl apply -f assets/state-container-toolkit

7.检查所有pod是否处于running。

$ kubectl get po -n gpu-operator-resources -l app=nvidia-container-toolkit-daemonset
NAME                                       READY   STATUS    RESTARTS   AGE
nvidia-container-toolkit-daemonset-6dksq   1/1     Running   0          14h
nvidia-container-toolkit-daemonset-q7f2l   1/1     Running   0          14h
nvidia-container-toolkit-daemonset-rv79v   1/1     Running   0          14h

8.查看pod日志。

$ kubectl logs nvidia-container-toolkit-daemonset-6dksq -n gpu-operator-resources --tail 10

time="2021-03-25T12:25:55Z" level=info msg="Parsing arguments: [/usr/local/nvidia/toolkit]"
time="2021-03-25T12:25:55Z" level=info msg="Successfully parsed arguments"
time="2021-03-25T12:25:55Z" level=info msg="Loading config: /etc/docker/daemon.json"
time="2021-03-25T12:25:55Z" level=info msg="Successfully loaded config"
time="2021-03-25T12:25:55Z" level=info msg="Flushing config"
time="2021-03-25T12:25:55Z" level=info msg="Successfully flushed config"
time="2021-03-25T12:25:55Z" level=info msg="Sending SIGHUP signal to docker"
time="2021-03-25T12:25:55Z" level=info msg="Successfully signaled docker"
time="2021-03-25T12:25:55Z" level=info msg="Completed 'setup' for docker"
time="2021-03-25T12:25:55Z" level=info msg="Waiting for signal"

可以看到,nvidia-container-toolkit已经安装成功了。

缺点

目前,基于容器安装NVIDIA Container Toolkit组件有如下的一个缺点:集群中所有节点都必须安装同一种Runtime,而且如果Runtime不是Docker,还需要特别指定。如果一个集群某些节点的Runtime为Docker,而另一些节点的Runtime为Containerd这种情况是不允许的。

总结

本篇文章说明了在k8s集群中基于容器安装NVIDIA Container Toolkit的原理。与安装驱动不同的是,一般情况下,我们无需定制安装NVIDIA Container Toolkit组件的docker镜像,直接使用官方提供的即可。

上一篇:Android根据联系人姓名首字符顺序读取通讯录


下一篇:关于Android Assets读取文件为File对象