背景
我們知道,如果在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鏡像,直接使用官方提供的即可。