Skip to content

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 드라이버

nvidia-smi --query-gpu=name,memory.total,driver_version --format=csv

GPU 이름과 메모리가 나오면 된다.

2. nvidia-container-toolkit 설치

dpkg -l | grep 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

kubectl get runtimeclass nvidia

k3s 는 기동 시 nvidia-container-runtime자동 감지해 containerd 설정에 넣고 nvidia RuntimeClass 를 만든다. 없다면 toolkit 설치 후 k3s 를 재시작한다:

sudo systemctl restart k3s

설치

sudo 가 필요 없다. kubeconfig 만 있으면 된다.

kubectl apply -f scripts/serving/nvidia-device-plugin.yml

매니페스트

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개로 보고한다 — 파드는 Running 이라 겉보기에는 정상이다.

확인

# 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'))
"

문제 해결

증상

원인

조치

파드는 Running 인데 allocatable 이 비어 있음

runtimeClassName: nvidia 누락 → NVML 을 못 연다

매니페스트에 그 줄을 넣고 재적용

파드가 CrashLoopBackOff

toolkit 미설치 또는 containerd 설정 누락

toolkit 설치 후 sudo systemctl restart k3s

allocatable0

드라이버가 GPU 를 못 봄

호스트에서 nvidia-smi 먼저 통과시킬 것

서빙 파드가 Pending, Insufficient nvidia.com/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

적용 후 allocatable4 로 바뀐다.

반드시 알고 켤 것

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.yaml env + 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'

마지막에 이렇게 출력됨:

NAME         ALLOC
spark-9234   8

알고 있어야 할 것

시분할은 시간만 나눕니다. 메모리는 격리되지 않습니다. 128 GB 통합 메모리를 8개 파드가 그냥 같이 씁니다. 즉 replicas: 8은 "동시에 띄울 수 있는 수"이지 "안전하게 띄울 수 있는 수"가 아닙니다 — 실패 지점이 스케줄러(Pending)에서 런타임(OOM)으로 옮겨갈 뿐이라, 인스턴스별 메모리 예산은 여전히 직접 지켜야 합니다.

두 가지 플래그를 의도적으로 이렇게 뒀습니다:

  • renameByDefault: false — true면 자원 이름이 nvidia.com/gpu.shared로 바뀌어 config.py:514serving_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 컨테이너 런타임 지원 항목 참조.

See also

Favorite site