- 들어가며 — nvidia.com/gpu는 어디서 오는가
- 디바이스 플러그인이 하는 일
- requests와 limits의 규칙
- 왜 GPU는 CPU처럼 나눠 쓸 수 없나
- 노드 라벨로 GPU를 고르기
- 마치며 — Pending의 원인은 대개 세 곳 중 하나다
- 직접 해보기
- 시리즈
- 참고 자료
들어가며 — 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) {}
}
이 중 GetPreferredAllocation과 PreStartContainer는 필수가 아닙니다. 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 node와 kubectl describe pod 두 명령으로 몇 초 만에 갈립니다.
정리하면 이렇습니다. 디바이스 플러그인은 자원의 존재를 만들고, 확장 리소스 규칙은 그 자원을 정수로 못 박고, GFD 라벨은 그중 어느 것인지를 고르게 해 줍니다. 한 장을 여럿이 나눠 쓰고 싶다는 요구는 이 세 층 어디에서도 풀리지 않고, 그래서 별도의 기술이 필요합니다. 다음 글에서 그 두 가지를 비교합니다.
직접 해보기
- 쿠버네티스 놀이터 — 리소스 요청을 바꿔 가며 스케줄링 결과가 어떻게 달라지는지 눈으로 확인해 보세요.
- kubectl 명령어 찾기 — 위의 확인 명령들을 상황별로 찾아보세요.
- K8s 실습 랩 — nodeSelector와 리소스 제한을 직접 써 보며 감을 잡아 보세요.
시리즈
참고 자료
- Kubernetes Device Plugins: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
- Kubernetes Schedule GPUs: https://kubernetes.io/docs/tasks/manage-gpus/scheduling-gpus/
- NVIDIA k8s-device-plugin 저장소: https://github.com/NVIDIA/k8s-device-plugin
- GPU Feature Discovery 문서: https://github.com/NVIDIA/k8s-device-plugin/blob/main/docs/gpu-feature-discovery/README.md
현재 단락 (1/68)
`kubectl describe node`를 찍으면 GPU 노드에는 `nvidia.com/gpu`라는 줄이 있고 그 옆에 숫자가 붙어 있습니다. 처음 보면 쿠버네티스가 GPU를 알...