開發與維運

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官網,詳細介紹了各個組件的關係。

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鏡像,直接使用官方提供的即可。

Leave a Reply

Your email address will not be published. Required fields are marked *