NVIDIA device plugin for Kubernetes
NVIDIA device plugin for Kubernetes
About
Kubernetes 노드가 GPU 를 스케줄 가능한 자원(nvidia.com/gpu)으로 광고하게 만드는 절차.
왜 필요한가?
RuntimeClass nvidia 만으로도 파드에 GPU 를 밀어 넣을 수는 있다. 그러나 그렇게 하면 클러스터가 GPU 점유를 모른다. GPU 가 하나뿐인 노드에서 서빙 엔드포인트를 둘 만들면 둘 다 같은 8 GB 를 잡고 서로 OOM 을 낸다.
device plugin 이 nvidia.com/gpu 를 광고하면 두 번째 엔드포인트는 런타임에 죽는 대신 Pending 으로 대기한다. 같은 "못 쓴다" 라도 스케줄러가 아는 실패가 낫다 — 원인이 로그가 아니라 kubectl describe pod 한 줄에 적히기 때문이다.
사전 조건
설치 전에 셋 다 만족해야 한다. 하나라도 빠지면 플러그인은 뜨지만 GPU 를 0개로 보고한다.
1. NVIDIA 드라이버
GPU 이름과 메모리가 나오면 된다.
2. nvidia-container-toolkit 설치
없다면:
## 배포판 저장소 등록
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey \
| sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
## 설치
sudo apt-get install -y nvidia-container-toolkit
3. RuntimeClass nvidia
k3s 는 기동 시 nvidia-container-runtime 을 자동 감지해 containerd 설정에 넣고 nvidia RuntimeClass 를 만든다. 없다면 toolkit 설치 후 k3s 를 재시작한다:
설치
sudo 가 필요 없다. kubeconfig 만 있으면 된다.
매니페스트
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:
runtimeClassName: nvidia # <-- k3s 용 로컬 수정 (아래 참고)
tolerations:
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
priorityClassName: "system-node-critical"
containers:
- image: nvcr.io/nvidia/k8s-device-plugin:v0.17.1
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
원본 매니페스트와의 차이
원본(NVIDIA/k8s-device-plugin v0.17.1)에서 runtimeClassName: nvidia 한 줄만 더했다.
| NOTE |
| k3s 의 기본 런타임은 nvidia 가 아니다. 이 줄이 없으면 플러그인 컨테이너가 NVML 을 열지 못하고 GPU 를 0개로 보고한다 — 파드는 |
확인
# 1) 파드가 떴는가
kubectl -n kube-system get pods -l name=nvidia-device-plugin-ds
# 2) 노드가 GPU 를 광고하는가 (핵심)
kubectl get node -o jsonpath='{.items[0].status.allocatable.nvidia\.com/gpu}'
두 번째 명령이 1(또는 GPU 개수)을 찍으면 성공이다. 빈 값이나 0 이면 아래 #문제 해결로.
전체를 한눈에:
kubectl get nodes -o json | python3 -c "
import json,sys
for n in json.load(sys.stdin)['items']:
st = n['status']
print(n['metadata']['name'],
'capacity=', st.get('capacity',{}).get('nvidia.com/gpu'),
'allocatable=', st.get('allocatable',{}).get('nvidia.com/gpu'))
"
문제 해결
| 증상 | 원인 | 조치 |
| 파드는 | | 매니페스트에 그 줄을 넣고 재적용 |
| 파드가 | toolkit 미설치 또는 containerd 설정 누락 | toolkit 설치 후 |
| | 드라이버가 GPU 를 못 봄 | 호스트에서 |
| 서빙 파드가 | 정상 동작. GPU 가 이미 다른 파드에 배타 할당됨 | 아래 #GPU 개수 조절 참고 |
GPU 를 누가 잡고 있는지:
kubectl get pods -A -o json | python3 -c "
import json,sys
for p in json.load(sys.stdin)['items']:
for c in p['spec'].get('containers',[]):
r = c.get('resources',{})
g = (r.get('limits') or {}).get('nvidia.com/gpu') or (r.get('requests') or {}).get('nvidia.com/gpu')
if g:
print(f\"{p['metadata']['namespace']}/{p['metadata']['name']} \"
f\"phase={p['status'].get('phase')} gpu={g}\")
"
GPU 개수 조절
기본값은 GPU 1장 = 파드 1개(배타 할당)다. 파드 여러 개가 한 GPU 를 나눠 쓰게 하려면 time-slicing 을 켠다.
ConfigMap
apiVersion: v1
kind: ConfigMap
metadata:
name: nvidia-device-plugin-config
namespace: kube-system
data:
config.yaml: |
version: v1
sharing:
timeSlicing:
resources:
- name: nvidia.com/gpu
replicas: 4 # 1장을 4개처럼 광고
데몬셋 연결
env:
- name: FAIL_ON_INIT_ERROR
value: "false"
- name: CONFIG_FILE
value: /config/config.yaml
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
- name: plugin-config
mountPath: /config
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
- name: plugin-config
configMap:
name: nvidia-device-plugin-config
적용 후 allocatable 이 4 로 바뀐다.
반드시 알고 켤 것
| WARNING |
| time-slicing 은 메모리를 분할하지 않는다. 4개로 광고해도 8 GB 는 그대로 하나이고 파드들이 그것을 나눠 쓴다. 합계가 넘으면 런타임 CUDA OOM 이다. |
이는 이 플러그인을 설치한 이유를 부분적으로 되돌리는 선택이다 — "스케줄러가 아는 대기" 를 "런타임 OOM 가능성" 과 맞바꾼다. 각 파드가 실제로 쓰는 메모리를 알고 있을 때만 켠다.
대안 비교
| 방식 | 메모리 격리 | 이 환경에서 |
| 배타 할당 (기본) | 완전 | 가능 — 동시에 1개 파드만 |
| time-slicing | 없음 (공유) | 가능 — 합계 8 GB 안에서 |
| MPS | 없음 (공유) | 가능하나 설정이 더 무겁다 |
| MIG | 하드 분할 | 불가 — 데이터센터 카드(A100/H100급) 전용 |
테넌트별 상한
geo-mlops 설정에는 GPU 쿼터가 없다(serving_gpu_runtime_class, serving_gpu_resource 둘뿐). 테넌트가 GPU 를 독점하지 못하게 하려면 네임스페이스에 Kubernetes ResourceQuota 를 건다:
apiVersion: v1
kind: ResourceQuota
metadata:
name: gpu-quota
namespace: tenant-kia
spec:
hard:
requests.nvidia.com/gpu: "1"
시분할 전체 코드
# NVIDIA device plugin — 노드가 GPU 를 **스케줄 가능한 자원**으로 광고하게 한다.
# (plan 17 S9)
#
# 왜 필요한가: `RuntimeClass nvidia` 만으로도 파드에 GPU 를 밀어 넣을 수는 있지만,
# 그러면 클러스터가 GPU 점유를 모른다. GPU 가 하나뿐인 노드에서 서빙 엔드포인트를 둘
# 만들면 둘 다 같은 8 GB 를 잡고 서로 OOM 낸다. 이 플러그인이 `nvidia.com/gpu` 를
# 광고하면 두 번째 엔드포인트는 런타임에 죽는 대신 Pending 으로 대기한다 — 같은 실패라도
# 스케줄러가 아는 실패가 낫다. 그 상한을 올리는 것이 아래 시분할 설정이다.
#
# 원본: https://github.com/NVIDIA/k8s-device-plugin (v0.19.3, Apache-2.0)
# 로컬 수정: `runtimeClassName: nvidia` 한 줄. k3s 의 기본 런타임은 nvidia 가 아니라서
# 이게 없으면 플러그인 컨테이너가 NVML 을 못 열고 GPU 를 0개로 보고한다.
#
# 버전 하한: v0.18.2 ("Ignore errors getting device memory using NVML"). 통합 메모리
# GPU 는 `nvmlDeviceGetMemoryInfo` 에 `Not Supported` 를 돌려주는데, 그 이전 버전은
# 이것을 치명적 오류로 보고 디바이스 열거 자체를 포기한다 — GPU 를 정상 인식하고도
# `error building device map ... error getting device memory: Not Supported` 로 죽는다.
#
# 시분할(time-slicing): DGX Spark 는 GPU 가 **하나**다. 기본 설정이면 노드는
# `nvidia.com/gpu: 1` 만 광고하므로 두 번째 엔드포인트부터 영원히 Pending 이다
# (`0/1 nodes are available: 1 Insufficient nvidia.com/gpu`). 아래 ConfigMap 이
# 그 하나를 `replicas` 개의 논리 GPU 로 광고해 여러 인스턴스가 같은 GPU 를 나눠 쓰게
# 한다. GB10 은 MIG 를 지원하지 않으므로 하드웨어 분할은 선택지가 아니다.
#
# 시분할이 나눠 주는 것은 **실행 시간뿐이다.** 메모리는 격리되지 않는다 — 128 GB 통합
# 메모리를 N 개 파드가 그냥 같이 쓴다. 즉 `replicas` 는 "동시에 띄울 인스턴스 수"이지
# "안전하게 띄울 수 있는 수"가 아니다. 인스턴스 하나가 메모리를 다 먹으면 나머지는
# 여전히 OOM 이다. 다만 실패 지점이 스케줄러(Pending)에서 런타임으로 옮겨갈 뿐이다.
#
# `replicas` 조정: 동시에 돌릴 인스턴스 수보다 넉넉하게 잡는다. 광고만 하는 숫자라
# 남아도는 슬롯에 비용은 없다. 바꾼 뒤에는 **반드시 롤아웃**해야 반영된다:
# kubectl -n kube-system rollout restart ds/nvidia-device-plugin-daemonset
#
# `renameByDefault: false` 를 유지하는 이유: true 면 자원 이름이
# `nvidia.com/gpu.shared` 로 바뀌어 앱의 `serving_gpu_resource`(config.py) 와
# 학습 러너의 요청까지 전부 따라 바꿔야 한다.
#
# `failRequestsGreaterThanOne: true` 를 켜는 이유: 시분할 슬롯 2개는 GPU 2장이
# 아니다. 이게 없으면 `gpu: 2` 요청이 스케줄에 성공한 뒤 실제로는 GPU 하나를 자기끼리
# 시분할하는, 조용히 틀린 상태가 된다. 켜 두면 admission 단계에서 즉시 거부된다.
#
# 적용 (sudo 불필요 — kubeconfig 만 있으면 된다):
# kubectl apply -f scripts/serving/nvidia-device-plugin.yml
#
# 확인 (노드가 여럿이면 `items[0]` 은 GPU 노드가 아닐 수 있다 — 전부 본다):
# kubectl get nodes -o custom-columns='NAME:.metadata.name,ALLOC:.status.allocatable.nvidia\.com/gpu'
apiVersion: v1
kind: ConfigMap
metadata:
name: nvidia-device-plugin-config
namespace: kube-system
data:
# 파일 하나만 두고 DaemonSet 이 `CONFIG_FILE` 로 직접 가리킨다. 노드마다 다른 설정을
# 쓰려면(`nvidia.com/device-plugin.config` 라벨) config-manager 사이드카가 필요한데,
# 단일 노드에는 과하다.
config.yaml: |-
version: v1
flags:
migStrategy: none
sharing:
timeSlicing:
renameByDefault: false
failRequestsGreaterThanOne: true
resources:
- name: nvidia.com/gpu
replicas: 8
---
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:
runtimeClassName: 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.19.3
name: nvidia-device-plugin-ctr
env:
- name: FAIL_ON_INIT_ERROR
value: "false"
# 시분할 설정을 읽는다. 이 줄이 없으면 ConfigMap 은 마운트만 되고 무시된다.
- name: CONFIG_FILE
value: /etc/nvidia-device-plugin/config.yaml
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop: ["ALL"]
volumeMounts:
- name: device-plugin
mountPath: /var/lib/kubelet/device-plugins
- name: plugin-config
mountPath: /etc/nvidia-device-plugin
readOnly: true
volumes:
- name: device-plugin
hostPath:
path: /var/lib/kubelet/device-plugins
- name: plugin-config
configMap:
name: nvidia-device-plugin-config
scripts/serving/nvidia-device-plugin.yml에 time-slicing 설정을 넣었습니다. YAML 파싱과 배선(ConfigMap ↔ CONFIG_FILE ↔ volumeMount)은 검증했습니다.
왜 Unschedulable이 뜨나
DGX Spark는 GPU가 물리적으로 1개라 플러그인이 nvidia.com/gpu: 1만 광고합니다.
첫 인스턴스가 그 하나를 점유하면 두 번째부터는 0/1 nodes are available: 1 Insufficient nvidia.com/gpu로 영원히 Pending입니다
GB10은 MIG를 지원하지 않으므로 하드웨어 분할은 선택지가 아니고, 시분할로 논리 GPU를 N개로 광고하는 게 유일한 실용적 방법입니다.
변경 내용
-
ConfigMap nvidia-device-plugin-config추가 —timeSlicing.resources[nvidia.com/gpu].replicas: 8 - DaemonSet에
CONFIG_FILE=/etc/nvidia-device-plugin/config.yamlenv + ConfigMap 마운트
kubectl apply -f scripts/serving/nvidia-device-plugin.yml
kubectl -n kube-system rollout restart ds/nvidia-device-plugin-daemonset
kubectl get nodes -o custom-columns='NAME:.metadata.name,ALLOC:.status.allocatable.nvidia\.com/gpu'
마지막에 이렇게 출력됨:
알고 있어야 할 것
시분할은 시간만 나눕니다. 메모리는 격리되지 않습니다. 128 GB 통합 메모리를 8개 파드가 그냥 같이 씁니다. 즉 replicas: 8은 "동시에 띄울 수 있는 수"이지 "안전하게 띄울 수 있는 수"가 아닙니다 — 실패 지점이 스케줄러(Pending)에서 런타임(OOM)으로 옮겨갈 뿐이라, 인스턴스별 메모리 예산은 여전히 직접 지켜야 합니다.
두 가지 플래그를 의도적으로 이렇게 뒀습니다:
-
renameByDefault: false— true면 자원 이름이nvidia.com/gpu.shared로 바뀌어config.py:514의serving_gpu_resource와 학습 러너 요청까지 전부 따라 바꿔야 합니다. -
failRequestsGreaterThanOne: true— 슬롯 2개는 GPU 2장이 아닙니다. 이게 없으면 학습 쪽gpu: 2요청이 스케줄에 성공한 뒤 실제로는 GPU 하나를 자기끼리 쪼개 쓰는, 조용히 틀린 상태가 됩니다. 켜 두면 admission에서 즉시 거부되어 드러납니다. 다만 그 결과로 Spark에서는gpu: 2학습 잡이UnexpectedAdmissionError로 실패하니, 그런 잡이 있다면gpu: 1로 내려야 합니다.
replicas 숫자는 8로 잡아뒀습니다 — 광고만 하는 숫자라 남는 슬롯에 비용은 없으니, 동시 인스턴스 수보다 넉넉하면 됩니다. 바꿀 때는 rollout restart가 필요합니다(ConfigMap 수정만으로는 반영되지 않습니다).
k3s 에서 NVIDIA 컨테이너 런타임 지원
Lightweight Kubernetes#NVIDIA 컨테이너 런타임 지원 항목 참조.