Skip to content

Split View: 디바이스 플러그인과 GPU 스케줄링 — nvidia.com/gpu는 어디서 오는가

✨ Learn with Quiz
|

디바이스 플러그인과 GPU 스케줄링 — nvidia.com/gpu는 어디서 오는가

들어가며 — nvidia.com/gpu는 어디서 오는가

kubectl describe node를 찍으면 GPU 노드에는 nvidia.com/gpu라는 줄이 있고 그 옆에 숫자가 붙어 있습니다. 처음 보면 쿠버네티스가 GPU를 알아본 것처럼 보이지만, 사실은 정반대입니다. 쿠버네티스 코어에는 GPU라는 개념이 없습니다. 저 줄은 노드에서 돌고 있는 프로세스 하나가 kubelet에게 "나한테 이런 이름의 물건이 여덟 개 있다"고 말해서 생긴 것뿐입니다.

그 프로세스가 디바이스 플러그인입니다. 이 글은 그 등록 과정과, 거기서 파생되는 스케줄링의 제약들을 봅니다. GPU 파드가 Pending에 머무를 때 어디를 봐야 하는지가 여기서 갈립니다.

메트릭 이름과 설정은 2026-08-12에 공식 문서·저장소에서 확인했습니다. 버전에 따라 다를 수 있으니 사용 중인 버전에서 다시 확인하세요.

디바이스 플러그인이 하는 일

쿠버네티스 문서는 디바이스 플러그인이 구현해야 할 gRPC 서비스를 명시합니다. 실제 인터페이스는 다섯 개의 메서드로 되어 있습니다.

service DevicePlugin {
      rpc GetDevicePluginOptions(Empty) returns (DevicePluginOptions) {}
      rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
      rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
      rpc GetPreferredAllocation(PreferredAllocationRequest) returns (PreferredAllocationResponse) {}
      rpc PreStartContainer(PreStartContainerRequest) returns (PreStartContainerResponse) {}
}

이 중 GetPreferredAllocationPreStartContainer는 필수가 아닙니다. kubelet이 먼저 GetDevicePluginOptions를 호출해서 어떤 선택적 기능이 있는지 확인합니다.

등록은 유닉스 소켓으로 이뤄집니다. 플러그인은 /var/lib/kubelet/device-plugins/kubelet.sock에 연결해 자신을 등록하고, 자기 자신의 gRPC 서버도 같은 디렉터리 아래 소켓으로 엽니다. 문서는 이 경로가 하드코딩되어 있으며 kubelet의 --root-dir 같은 설정에 영향받지 않는다고 못박습니다. 즉 이 파일이 없거나 플러그인 파드가 이 디렉터리를 마운트하지 못하면 등록 자체가 일어나지 않습니다.

자원 이름은 vendor-domain/resourcetype 형식을 따라야 하고, NVIDIA GPU는 그래서 nvidia.com/gpu가 됩니다. 이름에 벤더 도메인이 들어가는 것은 규칙이지 관습이 아닙니다.

플러그인의 동작은 몇 개의 플래그로 조절됩니다. 저장소 문서에서 확인한 것 중 알아 둘 만한 것은 다음과 같습니다. --mig-strategy는 환경 변수 MIG_STRATEGY에 대응하며 기본값은 none입니다. --fail-on-init-error는 기본값이 참이라 초기화에 실패하면 플러그인이 그대로 죽는데, 이 동작이 중요합니다. 조용히 살아 있으면서 자원을 광고하지 않는 것보다 죽어서 재시작을 반복하는 편이 진단하기 쉽기 때문입니다. 그 밖에 장치 목록을 컨테이너에 전달하는 방식을 정하는 --device-list-strategy가 기본값 envvar를, 장치 식별자 방식을 정하는 --device-id-strategy가 기본값 uuid를 가집니다.

장치 상태는 두 가지뿐입니다. Healthy와 Unhealthy입니다. 플러그인이 ListAndWatch 응답으로 어떤 장치를 비정상으로 표시하면, kubelet은 해당 자원의 노드 allocatable을 줄입니다. 문서가 명확히 하는 부분은 그다음입니다. capacity 값은 바뀌지 않습니다. 이 비대칭이 진단에 유용합니다. capacity는 8인데 allocatable이 7이면, 카드 한 장이 죽었고 그것을 플러그인이 이미 알고 있다는 뜻입니다.

requests와 limits의 규칙

CPU에 익숙한 사람이 GPU에서 가장 먼저 틀리는 것이 이 부분입니다. 쿠버네티스 문서의 규칙은 세 줄입니다.

  • limits만 쓰고 requests를 생략할 수 있습니다. 쿠버네티스가 limit 값을 request로 씁니다.
  • 둘 다 쓸 수 있지만 두 값이 반드시 같아야 합니다.
  • requests만 쓰고 limits를 생략할 수는 없습니다.

즉 GPU는 항상 Guaranteed에 해당하는 형태로만 요청됩니다. CPU에서 흔히 쓰는 "request는 낮게, limit은 높게" 전략이 통하지 않습니다.

apiVersion: v1
kind: Pod
metadata:
  name: gpu-single
spec:
  restartPolicy: OnFailure
  containers:
    - name: worker
      image: 'nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0-ubuntu22.04'
      resources:
        limits:
          nvidia.com/gpu: 1

왜 GPU는 CPU처럼 나눠 쓸 수 없나

쿠버네티스 문서는 GPU가 오버커밋될 수 없고, 공유될 수 없으며, 소수 단위로 나뉠 수 없다고 못박습니다. 이유는 두 층에 걸쳐 있습니다.

첫째는 자원 모델의 층입니다. 확장 리소스는 정수 단위 자원입니다. CPU의 밀리코어 같은 개념이 애초에 정의되어 있지 않습니다. 그래서 nvidia.com/gpu: 0.5는 문법 오류가 아니라 스케줄러가 이해할 수 없는 값입니다.

둘째는 하드웨어와 드라이버의 층입니다. CPU 시간은 커널 스케줄러가 프로세스 단위로 강제로 쪼갤 수 있고, 메모리는 cgroup이 한도를 걸 수 있습니다. GPU에는 기본적으로 그 두 가지가 다 없습니다. 같은 카드에 두 컨테이너가 붙으면 VRAM은 먼저 잡는 쪽이 가져가고, 커널 실행은 서로 밀어냅니다. 한쪽이 메모리를 다 쓰면 다른 쪽은 그냥 실패합니다. 격리가 없는 상태에서 자원 숫자만 쪼개 놓으면 스케줄러가 만든 약속을 하드웨어가 지키지 못합니다.

그래서 GPU를 나눠 쓰는 문제는 스케줄러 설정이 아니라 별도의 기술로 풀립니다. 시간을 나누는 time-slicing과 하드웨어를 나누는 MIG인데, 이 둘은 격리 수준이 완전히 다릅니다. 다음 글의 주제가 정확히 이것입니다.

노드 라벨로 GPU를 고르기

카드 종류가 섞인 클러스터에서는 자원 개수만으로는 부족합니다. GPU Feature Discovery가 노드에 붙이는 라벨을 씁니다. 저장소 문서에서 확인한 라벨 중 실무에서 자주 쓰는 것은 nvidia.com/gpu.product, nvidia.com/gpu.count, nvidia.com/gpu.memory, nvidia.com/gpu.family, nvidia.com/gpu.machine, nvidia.com/cuda.driver-version.full, nvidia.com/cuda.runtime-version.full, nvidia.com/gpu.compute.major, nvidia.com/gpu.compute.minor, nvidia.com/mig.capable, nvidia.com/gpu.sharing-strategy, nvidia.com/gfd.timestamp 정도입니다.

# GPU 노드의 라벨과 자원 확인
kubectl get nodes -L nvidia.com/gpu.product,nvidia.com/gpu.count,nvidia.com/gpu.memory

kubectl describe node <노드이름> | sed -n '/Capacity/,/System Info/p'

# 플러그인이 등록되었는지 kubelet 소켓 디렉터리에서 확인
kubectl debug node/<노드이름> -it --image=busybox -- ls /host/var/lib/kubelet/device-plugins/

라벨을 스케줄링에 쓰는 방법은 평범한 nodeSelector입니다.

apiVersion: v1
kind: Pod
metadata:
  name: gpu-on-specific-card
spec:
  restartPolicy: OnFailure
  nodeSelector:
    nvidia.com/gpu.product: Tesla-T4
  containers:
    - name: worker
      image: 'nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0-ubuntu22.04'
      resources:
        limits:
          nvidia.com/gpu: 1

라벨 값의 정확한 문자열은 카드마다 다릅니다. 추측하지 말고 위의 kubectl get nodes -L로 실제 값을 먼저 확인하는 편이 언제나 빠릅니다.

마치며 — Pending의 원인은 대개 세 곳 중 하나다

GPU 파드가 뜨지 않을 때 원인은 거의 항상 세 곳 중 하나에 있습니다. 노드가 자원을 아예 광고하지 않거나(플러그인 미등록), 광고는 하는데 남은 수량이 없거나(다른 파드가 점유), 라벨 조건이 어느 노드와도 맞지 않는 경우입니다. 셋 다 kubectl describe nodekubectl describe pod 두 명령으로 몇 초 만에 갈립니다.

정리하면 이렇습니다. 디바이스 플러그인은 자원의 존재를 만들고, 확장 리소스 규칙은 그 자원을 정수로 못 박고, GFD 라벨은 그중 어느 것인지를 고르게 해 줍니다. 한 장을 여럿이 나눠 쓰고 싶다는 요구는 이 세 층 어디에서도 풀리지 않고, 그래서 별도의 기술이 필요합니다. 다음 글에서 그 두 가지를 비교합니다.

직접 해보기

  • 쿠버네티스 놀이터 — 리소스 요청을 바꿔 가며 스케줄링 결과가 어떻게 달라지는지 눈으로 확인해 보세요.
  • kubectl 명령어 찾기 — 위의 확인 명령들을 상황별로 찾아보세요.
  • K8s 실습 랩 — nodeSelector와 리소스 제한을 직접 써 보며 감을 잡아 보세요.

시리즈

참고 자료

Device Plugins and GPU Scheduling — Where nvidia.com/gpu Comes From

Introduction — Where nvidia.com/gpu Comes From

Run kubectl describe node against a GPU node and you see a line reading nvidia.com/gpu with a number beside it. At first glance Kubernetes appears to have recognized the GPU. The truth is the opposite. There is no concept of a GPU anywhere in Kubernetes core. That line exists only because a process running on the node told the kubelet, in effect, that it has eight of a thing by that name.

That process is the device plugin. This post walks its registration path and the scheduling constraints that follow from it. When a GPU pod sits in Pending, this is where you find out which way to look.

Metric names and configuration were verified against the official documentation and repositories on 2026-08-12. They can differ between releases, so check again against the version you are running.

What a Device Plugin Does

The Kubernetes documentation spells out the gRPC service a device plugin must implement. The interface is five methods.

service DevicePlugin {
      rpc GetDevicePluginOptions(Empty) returns (DevicePluginOptions) {}
      rpc ListAndWatch(Empty) returns (stream ListAndWatchResponse) {}
      rpc Allocate(AllocateRequest) returns (AllocateResponse) {}
      rpc GetPreferredAllocation(PreferredAllocationRequest) returns (PreferredAllocationResponse) {}
      rpc PreStartContainer(PreStartContainerRequest) returns (PreStartContainerResponse) {}
}

Of these, GetPreferredAllocation and PreStartContainer are not required. The kubelet calls GetDevicePluginOptions first to find out which optional functions exist.

Registration happens over a Unix socket. The plugin connects to /var/lib/kubelet/device-plugins/kubelet.sock to register itself, and opens its own gRPC server on a socket under the same directory. The documentation is explicit that this path is hardcoded and is not affected by the kubelet --root-dir or other configuration. Which means: if that file is missing, or the plugin pod fails to mount that directory, registration never happens at all.

Resource names must follow the vendor-domain/resourcetype scheme, which is why an NVIDIA GPU becomes nvidia.com/gpu. The vendor domain in the name is a rule, not a convention.

Plugin behavior is tuned by a handful of flags. Among those documented in the repository, a few are worth knowing. --mig-strategy corresponds to the environment variable MIG_STRATEGY and defaults to none. --fail-on-init-error defaults to true, so the plugin dies outright when initialization fails, and that behavior matters: dying and restarting in a loop is far easier to diagnose than quietly staying alive while advertising nothing. Beyond those, --device-list-strategy governs how the device list reaches the container and defaults to envvar, while --device-id-strategy governs the identifier form and defaults to uuid.

Devices have exactly two states: Healthy and Unhealthy. When the plugin marks a device unhealthy in its ListAndWatch response, the kubelet decreases the node allocatable for that resource. What the documentation makes clear next matters: the capacity count does not change. That asymmetry is diagnostically useful. Capacity 8 with allocatable 7 means one card has died and the plugin already knows it.

The Rules for requests and limits

This is where people coming from CPU habits get it wrong first. The Kubernetes rules are three lines.

  • You can specify limits and omit requests. Kubernetes uses the limit value as the request.
  • You can specify both, but the two values must be equal.
  • You cannot specify requests without specifying limits.

In other words, a GPU is only ever requested in a shape equivalent to Guaranteed. The familiar CPU strategy of a low request and a high limit does not apply.

apiVersion: v1
kind: Pod
metadata:
  name: gpu-single
spec:
  restartPolicy: OnFailure
  containers:
    - name: worker
      image: 'nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0-ubuntu22.04'
      resources:
        limits:
          nvidia.com/gpu: 1

Why a GPU Cannot Be Shared the Way a CPU Is

The Kubernetes documentation states flatly that GPUs are not overcommittable, not shareable, and not fractional. The reason spans two layers.

The first is the resource model layer. Extended resources are integer-valued. There is no equivalent to the CPU millicore defined at all. So nvidia.com/gpu: 0.5 is not a syntax error so much as a value the scheduler cannot interpret.

The second is the hardware and driver layer. CPU time can be forcibly divided per process by the kernel scheduler, and memory can be capped by cgroups. A GPU has neither of those by default. Put two containers on the same card and VRAM goes to whoever grabs it first, while kernel launches shove each other around. If one side exhausts memory, the other simply fails. Splitting the resource number while isolation is absent means the scheduler makes a promise the hardware cannot keep.

So GPU sharing is solved by separate technology rather than by scheduler configuration. Time-slicing divides time, MIG divides hardware, and their isolation guarantees are not remotely comparable. That is exactly the subject of the next post.

Targeting GPUs by Node Label

In a cluster with mixed card types, a resource count is not enough. You use the labels GPU Feature Discovery applies to the node. Among the labels documented in the repository, the ones that see regular use are nvidia.com/gpu.product, nvidia.com/gpu.count, nvidia.com/gpu.memory, nvidia.com/gpu.family, nvidia.com/gpu.machine, nvidia.com/cuda.driver-version.full, nvidia.com/cuda.runtime-version.full, nvidia.com/gpu.compute.major, nvidia.com/gpu.compute.minor, nvidia.com/mig.capable, nvidia.com/gpu.sharing-strategy, and nvidia.com/gfd.timestamp.

# Inspect labels and resources on GPU nodes
kubectl get nodes -L nvidia.com/gpu.product,nvidia.com/gpu.count,nvidia.com/gpu.memory

kubectl describe node <node-name> | sed -n '/Capacity/,/System Info/p'

# Check whether the plugin registered, from the kubelet socket directory
kubectl debug node/<node-name> -it --image=busybox -- ls /host/var/lib/kubelet/device-plugins/

Using those labels for scheduling is an ordinary nodeSelector.

apiVersion: v1
kind: Pod
metadata:
  name: gpu-on-specific-card
spec:
  restartPolicy: OnFailure
  nodeSelector:
    nvidia.com/gpu.product: Tesla-T4
  containers:
    - name: worker
      image: 'nvcr.io/nvidia/k8s/cuda-sample:vectoradd-cuda12.5.0-ubuntu22.04'
      resources:
        limits:
          nvidia.com/gpu: 1

The exact label value string differs per card. Rather than guessing, reading the real value with the kubectl get nodes -L above is always faster.

Closing — Pending Almost Always Has One of Three Causes

When a GPU pod will not start, the cause is nearly always in one of three places. The node advertises no such resource at all (the plugin never registered), it advertises the resource but none are free (other pods hold them), or the label constraints match no node. All three separate within seconds using kubectl describe node and kubectl describe pod.

To summarize: the device plugin creates the existence of the resource, the extended resource rules pin it to whole integers, and GFD labels let you choose which one. The desire to have several workloads share one card is not solved at any of those three layers, which is why separate technology exists. The next post compares the two options.

Try It Yourself

  • Kubernetes Playground — change resource requests and watch the scheduling outcome shift.
  • kubectl Command Finder — look up the inspection commands above by situation.
  • K8s Lab — write nodeSelector and resource limits yourself to build intuition.

Series

References