Use Nvidia Device Plugin at Kubernetes

쿠버네티스 환경에서 GPU 사용을 위한 Nvidia Device Plugin 추가

Sigrid Jin
28 min readApr 27, 2024
https://itnext.io/enabling-nvidia-gpus-on-k3s-for-cuda-workloads-a11b96f967b0

기존 Docker에서 GPU Container를 실행시키는 것은 매우 간단했다. GPU 리소스 중 몇 번의 디바이스를 사용할 지에 대해 지정하는 것이 그리 어려운 일도 아니었다. Docker를 설치하고 이후에 nvidia-docker2를 설치하면 금방 해결이 되는 문제였다. default-runtime이 nvidia로 설정되어 있는 모습은 아래와 같다.

worker5@worker5:~$  ls -alF /etc/docker/daemon.json
-rw-r--r-- 1 root root 136 Mar 31 2023 /etc/docker/daemon.json

worker5@worker5:~$ cat /etc/docker/daemon.json
{
"runtimes": {
"nvidia": {
"path": "nvidia-container-runtime",
"runtimeArgs": []
}
}
}
https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/arch-overview.html
https://velog.io/@mirrorkyh/Containerd%EC%99%80-runc-%EC%9D%98-%EC%B0%A8%EC%9D%B4#runc

nvidia-docker는 나열된 패키지에서 맨 아래 레이터부터 가장 높은 레이어로의 의존성이 존재한다.
1. nvidia-docker2
2. nvidia-container-runtime
3. nvidia-container-toolkit
4. libnvidia-container

먼저 libnvidia-container에 대해 알아보면, container가 nvidia CUDA를 쓸 수 있도록 준비해준다. 서로 다른 run-time에서 nvidia GPU를 쓸 수 있도록 API와 잘 래핑된 CLI를 제공한다.

nvidia-container-toolkit: runC의 prestart hook에서 필요한 인터페이스 구현 스크립트를 제공하게 된다. 컨테이너 생성 후 runC에 의해 호출되어 libnvidia-container를 실행하고, 컨테이너에 어떤 GPU 장치를 설정할 지 결정해야 한다.

nvidia-container-runtime: NVIDIA에 특화된 runC의 포크(fork)로, 현재는 runC를 래핑하는 형태로 동작한다. nvidia-container-toolkit 스크립트를 prestart hook으로 설정한 뒤 native runC를 호출하는 방식이다. Docker에 특화된 것은 아니고 runC에 의존적이다.

nvidia-docker2: Docker에 특화된 유일한 패키지로, nvidia-container-runtime을 Docker의 설정(/etc/docker/daemon.json)에 자동으로 추가해 줍니다. 이를 통해 ‘docker run — runtime=nvidia’ 옵션을 사용할 수 있게 되고, GPU를 컨테이너에 자동 할당할 수 있다. 또한 NV_GPU라는 호스트 환경변수 설정을 통해 컨테이너에 포함될 GPU를 선택할 수도 있다.

하지만 Kubernetes 클러스터는 OCI의 containerd를 사용하므로 데몬부터가 다르기 때문에 기존의 방식으로 접근할 수도 없기 때문에 조금 고생을 했다. 먼저 워커 노드에서 lshw 명령어를 통해 우분투 운영체제가 GPU를 정확하게 인식하고 있는 지 여부를 확인한다. 아래 상황에서는 RTX 3090 GPU가 인식되어 동작하는 것으로 확인되고 있다.

worker12@worker12:/etc/containerd$ sudo lshw -C display
*-display
description: VGA compatible controller
product: GA102 [GeForce RTX 3090]
vendor: NVIDIA Corporation
physical id: 0
bus info: pci@0000:73:00.0
version: a1
width: 64 bits
clock: 33MHz
capabilities: pm msi pciexpress vga_controller bus_master cap_list rom
configuration: driver=nvidia latency=0
resources: irq:437 memory:c4000000-c4ffffff memory:b0000000-bfffffff memory:c0000000-c1ffffff ioport:9000(size=128) memory:c0000-dffff
*-graphics
product: EFI VGA
physical id: 4
logical name: /dev/fb0
capabilities: fb
configuration: depth=32 resolution=800,600

이제 인식된 GPU를 사용할 수 있도록 드라이버를 설치한다. 현재 내가 설치하고 사용할 수 있는 nvidia-driver를 확인하고, 이에 맞게 드라이버를 설치하고 재부팅 후 실행한다.

$ apt search nvidia-driver

# graphics display 이외의 기능을 담은 드라이버 패키지(커널 드라이버, cuda 드라이버, 유틸)
nvidia-headless-510/jammy-updates,jammy-security 510.85.02-0ubuntu0.22.04.1 amd64
NVIDIA headless metapackage

# graphics display 이외의 기능을 담은 드라이버 패키지(커널 드라이버, cuda 드라이버, 유틸)
nvidia-headless-510-server/jammy-updates,jammy-security 510.85.02-0ubuntu0.22.04.1 amd64
NVIDIA headless metapackage

# 모든 드라이버 패키지(커널 드라이버, 2d/3d xorg 드라이버, cuda 드라이버, 유틸)
nvidia-driver-510/jammy-updates,jammy-security,now 510.85.02-0ubuntu0.22.04.1 amd64 [installed]
NVIDIA driver metapackage

# 모든 드라이버 패키지(커널 드라이버, 2d/3d xorg 드라이버, cuda 드라이버, 유틸)
nvidia-driver-510-server/jammy-updates,jammy-security 510.85.02-0ubuntu0.22.04.1 amd64
NVIDIA Server Driver metapackage
worker12@worker12:/etc/containerd$ nvidia-smi
Sun Apr 28 02:09:16 2024
+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 545.29.06 Driver Version: 545.29.06 CUDA Version: 12.3 |
|-----------------------------------------+----------------------+----------------------+
| 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 NVIDIA GeForce RTX 3090 Off | 00000000:73:00.0 Off | N/A |
| 30% 38C P8 27W / 350W | 28MiB / 24576MiB | 0% Default |
| | | N/A |
+-----------------------------------------+----------------------+----------------------+

+---------------------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=======================================================================================|
| 0 N/A N/A 2334 G /usr/lib/xorg/Xorg 9MiB |
| 0 N/A N/A 2495 G /usr/bin/gnome-shell 8MiB |
+---------------------------------------------------------------------------------------+

이제 nvidia-container-toolkit을 설치해본다. Toolkit의 설치 과정은 시간에 따라 변경될 가능성이 존재하므로 항상 nvidia 공식 문서를 참고하여 설치하는 것을 추천한다.

# https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html

$ distribution=$(. /etc/os-release;echo $ID$VERSION_ID) \
&& curl -s -L https://nvidia.github.io/libnvidia-container/gpgkey | sudo apt-key add - \
&& curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

$ sudo apt-get update \
&& sudo apt-get install -y nvidia-container-toolkit

런타임을 설치했다면 containerd가 Nvidia Container Runtime을 사용할 수 있도록 etc/containerd/config.toml 파일을 아래와 같이 수정하고 containerd를 재시작한다. 이것은 container의 container runtime을 nvidia-container-runtime을 사용하는 container로 바꾸는 것이다.

<--/etc/containerd/config.toml-->
...
privileged_without_host_devices = false
base_runtime_spec = ""
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc.options]
- SystemdCgroup = false
+ SystemdCgroup = true
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia]
+ privileged_without_host_devices = false
+ runtime_engine = ""
+ runtime_root = ""
+ runtime_type = "io.containerd.runc.v1"
+ [plugins."io.containerd.grpc.v1.cri".containerd.runtimes.nvidia.options]
+ BinaryName = "/usr/bin/nvidia-container-runtime"
+ SystemdCgroup = true
[plugins."io.containerd.grpc.v1.cri".cni]
bin_dir = "/opt/cni/bin"
conf_dir = "/etc/cni/net.d"
...

# sudo systemctl restart containerd

containerd에서 GPU가 정상적으로 인식되는지 확인하기 위해 아래와 같이 요청해본다.

sudo ctr image pull docker.io/nvidia/cuda:11.0.3-base-ubuntu20.04
sudo ctr run --rm -t \
--runc-binary=/usr/bin/nvidia-container-runtime \
--env NVIDIA_VISIBLE_DEVICES=all \
docker.io/nvidia/cuda:11.0.3-base-ubuntu20.04 \
cuda-11.0.3-base-ubuntu20.04 nvidia-smi

+-----------------------------------------------------------------------------+
| NVIDIA-SMI 510.85.02 Driver Version: 510.85.02 CUDA Version: 12.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 NVIDIA GeForce ... Off | 00000000:03:00.0 Off | N/A |
| N/A 42C P8 N/A / N/A | 4MiB / 4096MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+

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

클러스터의 각 노드들에 달려있는 GPU의 수를 정확하게 이해하고 그 상태를 지속적으로 튜닝하기 위해서는 전체 노드에 하나씩 배포되는 DaemonSet으로 배포하고 설치해야 한다. GPU가 달려있는 노드에 gpu=nvidia라고 하는 적절한 레이블을 담겨줄 것이다. 특히 GPU를 사용해야 하는 서비스의 경우 GPU가 붙어있는 노드에서만 GPU-job 관련 Plugin을 설치하지 않아도 된다.

# node에 label 추가
$ kubectl label nodes [gpu가 장착된 노드명 1] gpu=nvidia

# git에 올라온 경로대로 DaemonSet을 생성하는 yaml을 파일을 다운
$ curl -O https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.12.3/nvidia-device-plugin.yml

# gpu가 달려있는 노드에 plugin이 배포될 수 있도록 nodeSelector를 추가한다.
nodeSelector:
gpu: nvidia
# nvidia-device-plugin/templates/nvidia-device-plugin.yml

# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-device-plugin-daemonset
namespace: kube-system
spec:
selector:
matchLabels:
name: nvidia-device-plugin-ds
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
name: nvidia-device-plugin-ds
spec:
nodeSelector:
{{- with .Values.nodeSelector }}
{{- toYaml . | nindent 8 }}
{{- end }}
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
# 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.
# See https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/
priorityClassName: "system-node-critical"
containers:
- image: {{ .Values.image }}
name: nvidia-device-plugin-ctr
env:
- name: FAIL_ON_INIT_ERROR
value: "false"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins

이제 GPU가 할당된 노드에 플러그인이 배포될 수 있도록 nodeSelector를 추가하고, RuntimeClass를 생성하여 Pod가 nvidia-runtime을 선택할 수 있도록 잘 동작한다.

nodeSelector:
gpu: nvidia
# nvidia-device-plugin.yml

# Copyright (c) 2019, NVIDIA CORPORATION. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

apiVersion: apps/v1
kind: DaemonSet
metadata:
name: nvidia-device-plugin-daemonset
namespace: kube-system
spec:
selector:
matchLabels:
name: nvidia-device-plugin-ds
updateStrategy:
type: RollingUpdate
template:
metadata:
labels:
name: nvidia-device-plugin-ds
spec:
+ nodeSelector:
+ gpu: nvidia
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
# 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.
# See https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/
priorityClassName: "system-node-critical"
containers:
- image: nvcr.io/nvidia/k8s-device-plugin:v0.12.3
name: nvidia-device-plugin-ctr
env:
- name: FAIL_ON_INIT_ERROR
value: "false"
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins

# gpu를 사용하는 pod을 생성해보자.
# test-gpu-pod.yml
apiVersion: v1
kind: Pod
metadata:
name: gpu
spec:
restartPolicy: Never
runtimeClassName: "nvidia" # nvidia runtime 사용
nodeSelector:
gpu: nvidia # gpu가 장착된 node에만 배포
containers:
- name: gpu
image: "nvidia/cuda:11.4.1-base-ubuntu20.04"
command: [ "/bin/bash", "-c", "--" ]
args: [ "while true; do sleep 30; done;" ]

$ cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
name: gpu-pod
spec:
restartPolicy: Never
containers:
- name: cuda-container
image: nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda10.2
resources:
limits:
nvidia.com/gpu: 1 # requesting 1 GPU
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
EOF

실제 Production 환경에서는 Helm을 이용하되 배포할 때는 helm template command를 사용해서 리소스에 적용될 YAML을 꼭 double-check하는 것이 필요하다. 아래는 nvidia-device-plugin 저장소에서 values.yaml을 바탕으로 리소스를 generate하기 위해 필요한 것 중 하나다.

# Default values for nvidia-device-plugin.
# This is a YAML-formatted file.
# Declare variables to be passed into your templates.

replicaCount: 1

image: nvcr.io/nvidia/k8s-device-plugin:v0.13.0
imagePullSecrets: []
nameOverride: ""
fullnameOverride: ""

serviceAccount:
# Specifies whether a service account should be created
create: true
# Automatically mount a ServiceAccount's API credentials?
automount: true
# Annotations to add to the service account
annotations: {}
# The name of the service account to use.
# If not set and create is true, a name is generated using the fullname template
name: ""

podAnnotations: {}
podLabels: {}

podSecurityContext: {}
# fsGroup: 2000

securityContext: {}
# capabilities:
# drop:
# - ALL
# readOnlyRootFilesystem: true
# runAsNonRoot: true
# runAsUser: 1000


resources: {}
# We usually recommend not to specify default resources and to leave this as a conscious
# choice for the user. This also increases chances charts run on environments with little
# resources, such as Minikube. If you do want to specify resources, uncomment the following
# lines, adjust them as necessary, and remove the curly braces after 'resources:'.
# limits:
# cpu: 100m
# memory: 128Mi
# requests:
# cpu: 100m
# memory: 128Mi


# secret:
# secretName: mysecret
# optional: false

# Additional volumeMounts on the output Deployment definition.
volumeMounts: []
# - name: foo
# mountPath: "/etc/foo"
# readOnly: true

nodeSelector:
gpu_use: "true"

tolerations: []

affinity: {}

필요한 경우 노드 별로 추가 레이블을 부여하는 것이 좋다. 예를 들어서 최근에는 GPU를 사용한 TGI helm charts를 작성하고 있는데, 3090 GPU와 4090 GPU를 온프레미스에서 모두 사용하고 있다면 각각의 아키텍처가 다르고 이에 따른 이미지 버전이 상이할 수 있다.

노드에서 주어진 레이블을 읽고 이에 따라 GPU 모델의 이미지를 동적으로 불러와야만 하는 상황, dgcm exporter 등 다른 필수적인 요소들을 수동으로 설치해야 한다는 점이 쉬워보이지 않는다. 이번에 Issue Raising하는 것을 보면 label을 추가해야만 한다.

kubectl label node worker13 gpu_use="true"

3번을 잘 수행했다면 kube-system namespace의 worker13에 nvidia device plugin Pod가 잘 뜰 것임.
replicaCount: 1

gpuModelImageMap:
rtx4090: ghcr.io/huggingface/text-embeddings-inference:89-1.2
rtx3090: ghcr.io/huggingface/text-embeddings-inference:86-1.2

defaultImage: # fallback - cpu
repository: ghcr.io/huggingface/text-embeddings-inference
tag: 1.2.2

model:
id: .../multi-emb-sup-v5
apiToken: hf_가나다라

service:
port: 3002

extraArgs:
- --max-client-batch-size=1024
- --max-batch-tokens=65536
- --pooling=cls
- --dtype=float32

--

--