Skip to content

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

한국어
0%
정확도 0%
💡 왼쪽 원문을 읽으면서 오른쪽에 따라 써보세요. Tab 키로 힌트를 받을 수 있습니다.

들어가며 — 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와 리소스 제한을 직접 써 보며 감을 잡아 보세요.

시리즈

참고 자료

현재 단락 (1/68)

`kubectl describe node`를 찍으면 GPU 노드에는 `nvidia.com/gpu`라는 줄이 있고 그 옆에 숫자가 붙어 있습니다. 처음 보면 쿠버네티스가 GPU를 알...

작성 글자: 0원문 글자: 5,176작성 단락: 0/68