Skip to content
Search

NVIDIA device plugin for Kubernetes

lastmod 2026-08-12created 2026-07-205 edits

NVIDIA device plugin for Kubernetes

About

The procedure for making a Kubernetes node advertise its GPU as a schedulable resource (nvidia.com/gpu).

Why is it needed?

RuntimeClass nvidia alone is enough to push a GPU into a pod. But then the cluster does not know about GPU occupancy. If you create two serving endpoints on a node with only one GPU, both grab the same 8 GB and OOM each other.

When the device plugin advertises nvidia.com/gpu, the second endpoint waits in Pending instead of dying at runtime. Even if the result is the same "can't use it", a failure the scheduler knows about is better — because the cause is written in a single line of kubectl describe pod rather than in the logs.

Prerequisites

All three must be satisfied before installation. If any one is missing, the plugin starts but reports 0 GPUs.

1. NVIDIA driver

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

It is fine if the GPU name and memory are shown.

2. Install nvidia-container-toolkit

dpkg -l | grep nvidia-container-toolkit

If it is not there:

## 배포판 저장소 등록
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

On startup, k3s auto-detects nvidia-container-runtime, adds it to the containerd configuration, and creates the nvidia RuntimeClass. If it is missing, restart k3s after installing the toolkit:

sudo systemctl restart k3s

Installation

sudo is not needed. A kubeconfig is all you need.

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

Manifest

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

Differences from the original manifest

Only one line, runtimeClassName: nvidia, was added to the original (NVIDIA/k8s-device-plugin v0.17.1).

NOTE

The default runtime of k3s is not nvidia. Without this line, the plugin container cannot open NVML and reports 0 GPUs — the pod is Running, so it looks normal on the surface.

Verification

# 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}'

Success if the second command prints 1 (or the number of GPUs). If it is empty or 0, go to #Troubleshooting below.

Everything at a glance:

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

Troubleshooting

Symptom

Cause

Fix

Pod is Running but allocatable is empty

runtimeClassName: nvidia missing → cannot open NVML

Add that line to the manifest and reapply

Pod is in CrashLoopBackOff

Toolkit not installed or containerd configuration missing

Install the toolkit, then sudo systemctl restart k3s

allocatable is 0

Driver cannot see the GPU

Get nvidia-smi to pass on the host first

Serving pod is Pending, Insufficient nvidia.com/gpu

Working as intended. The GPU is already exclusively allocated to another pod

See #Adjusting the GPU count below

Who is holding the 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}\")
"

Adjusting the GPU count

The default is 1 GPU = 1 pod (exclusive allocation). To let multiple pods share one GPU, enable 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개처럼 광고

Connecting to the DaemonSet

        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

After applying, allocatable changes to 4.

Know this before enabling

WARNING

Time-slicing does not partition memory. Even if it is advertised as 4, the 8 GB is still a single pool that the pods share. If the total exceeds it, you get a CUDA OOM at runtime.

This choice partially undoes the reason for installing this plugin — it trades "waiting the scheduler knows about" for "possible OOM at runtime". Enable it only when you know how much memory each pod actually uses.

Comparison of alternatives

Method

Memory isolation

In this environment

Exclusive allocation (default)

Full

Possible — only 1 pod at a time

time-slicing

None (shared)

Possible — within 8 GB total

MPS

None (shared)

Possible, but heavier to configure

MIG

Hard partitioning

Not possible — data center cards (A100/H100 class) only

Per-tenant limits

The geo-mlops configuration has no GPU quota (only serving_gpu_runtime_class and serving_gpu_resource). To prevent a tenant from monopolizing the GPU, put a Kubernetes ResourceQuota on the namespace:

apiVersion: v1
kind: ResourceQuota
metadata:
  name: gpu-quota
  namespace: tenant-kia
spec:
  hard:
    requests.nvidia.com/gpu: "1"

Full time-slicing code

# 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

I added the time-slicing configuration to scripts/serving/nvidia-device-plugin.yml. YAML parsing and the wiring (ConfigMap ↔ CONFIG_FILE ↔ volumeMount) have been verified.

Why Unschedulable appears

DGX Spark physically has only 1 GPU, so the plugin advertises only nvidia.com/gpu: 1.

Once the first instance occupies that one, every instance from the second onward stays in Pending forever with 0/1 nodes are available: 1 Insufficient nvidia.com/gpu

GB10 does not support MIG, so hardware partitioning is not an option, and advertising N logical GPUs via time-slicing is the only practical approach.

Changes

  • Added ConfigMap nvidia-device-plugin-config — timeSlicing.resources[nvidia.com/gpu].replicas: 8
  • CONFIG_FILE=/etc/nvidia-device-plugin/config.yaml env + ConfigMap mount on the DaemonSet
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'

The final output looks like this:

NAME         ALLOC
spark-9234   8

Things to know

Time-slicing only divides time. Memory is not isolated. The 8 pods simply share the 128 GB of unified memory. In other words, replicas: 8 is "the number that can run at the same time", not "the number that can run safely" — the failure point merely moves from the scheduler (Pending) to runtime (OOM), so you still have to enforce the per-instance memory budget yourself.

Two flags were set this way intentionally:

  • renameByDefault: false — if true, the resource name changes to nvidia.com/gpu.shared, and serving_gpu_resource in config.py:514 and the training runner's requests would all have to be changed to match.
  • failRequestsGreaterThanOne: true — 2 slots are not 2 GPUs. Without this, a gpu: 2 request from the training side would schedule successfully and then actually split one GPU among itself, a silently wrong state. With it enabled, the request is rejected immediately at admission, making the problem visible. As a consequence, however, gpu: 2 training jobs fail on Spark with UnexpectedAdmissionError, so if there are such jobs, they must be lowered to gpu: 1.

The replicas number is set to 8 — since it is a number that is only advertised, spare slots cost nothing, so it just needs to be comfortably larger than the number of concurrent instances. Changing it requires a rollout restart (editing the ConfigMap alone does not take effect).

NVIDIA container runtime support in k3s

See Lightweight Kubernetes#NVIDIA container runtime support.

See also

Favorite site

author Zer0 sitemap