Skip to main content

GPU는 왜 CPU처럼 다룰 수 없을까

쿠버네티스를 처음 배울 때 우리는 CPU와 메모리를 자연스럽게 요청한다. cpu: 500m, memory: 1Gi처럼 쓰면 스케줄러가 적당한 노드를 찾아준다. CPU는 0.5개를 요청할 수 있고, 메모리는 비교적 작은 단위로 쪼개서 요청할 수 있다. 그래서 GPU도 비슷하게 생각하기 쉽다. "GPU도 그냥 gpu: 0.5처럼 요청하면 되는 것 아닌가?"

하지만 GPU는 그렇게 단순하지 않다. GPU는 단순한 계산량이 아니라 하나의 복잡한 장치다. 그 안에는 GPU 메모리, 연산 유닛, L2 캐시, PCIe 연결, NVLink 연결, 드라이버 상태, CUDA 런타임, 전력 제한, 온도 상태가 함께 존재한다. CPU 0.5개를 두 컨테이너가 나눠 쓰는 것과, H100 한 장을 두 모델 서버가 나눠 쓰는 것은 완전히 다른 문제다.

예를 들어 7B급 모델을 4비트로 양자화하면 비교적 작은 GPU 메모리에서도 추론할 수 있다. 그런데 A100 80GB 한 장을 통째로 할당하면 상당한 메모리가 남을 수 있다. 반대로 70B 모델은 한 장의 GPU에 들어가지 않거나, 들어가더라도 긴 컨텍스트와 동시 요청을 감당할 KV 캐시 여유가 부족할 수 있다. 이때는 여러 GPU를 묶어 텐서 병렬 처리를 해야 하는데, 아무 GPU 네 장이나 받는다고 끝나지 않는다. 같은 노드 안에서 NVLink로 잘 연결된 GPU 네 장과, 서로 다른 PCIe 경로를 지나거나 노드 밖 네트워크를 건너는 GPU 네 장은 성능이 다르다.

GPU 스케줄링이 어려운 이유는 결국 네 가지로 요약된다.

  • GPU는 비싸고 희소하다. 하나를 잘못 배치하면 비용 손실이 바로 보인다.

  • GPU는 메모리와 연산 장치가 결합된 특수 하드웨어다. 단순한 정수 개수만으로는 요구 사항을 표현하기 어렵다.

  • GPU 워크로드는 학습, 추론, 임베딩, 개발 노트북처럼 성격이 다르다. 어떤 워크로드는 독점이 필요하고, 어떤 워크로드는 공유가 효율적이다.

  • GPU 성능은 토폴로지의 영향을 크게 받는다. 특히 분산 학습과 대형 LLM 추론에서는 GPU 사이의 연결 구조가 성능을 좌우한다.

이 장에서는 쿠버네티스가 GPU를 어떻게 이해해 왔는지, 그리고 2026년 현재 어떤 방향으로 바뀌고 있는지를 차근차근 살펴본다. 출발점은 오랫동안 표준처럼 쓰였던 nvidia.com/gpu 확장 리소스 모델이다. 도착점은 Dynamic Resource Allocation, 즉 DRA다. 다만 DRA는 한 번의 릴리스에서 모든 것이 갑자기 완성된 기능이 아니다. 1.33에서 크게 성숙했고, 1.34 이후 일반 사용 가능성으로 이동했으며, 1.36에서는 일부 선택·관리 기능이 안정화되는 식으로 단계적으로 자리 잡고 있다. 이 장에서는 특정 마이너 버전에 매달리기보다, GPU를 쿠버네티스에서 표현하는 모델이 어떻게 바뀌고 있는지에 집중한다.

GPU 스케줄링의 모든 것

오래된 표준: nvidia.com/gpu 확장 리소스 모델

쿠버네티스는 원래 GPU를 기본 리소스로 알지 못한다. CPU와 메모리는 쿠버네티스가 처음부터 이해하는 리소스지만, GPU, FPGA, 고속 네트워크 카드 같은 장치는 벤더마다 생김새와 제어 방식이 다르다. 그래서 쿠버네티스는 Device Plugin Framework라는 확장 지점을 제공했다. NVIDIA는 이 인터페이스를 구현한 NVIDIA Device Plugin을 제공했고, 이 플러그인이 GPU를 nvidia.com/gpu라는 확장 리소스로 쿠버네티스에 등록한다.1

가장 단순한 GPU 테스트 Pod는 다음처럼 생겼다.

apiVersion: v1
kind: Pod
metadata:
  name: gpu-smoke-test
spec:
  restartPolicy: Never
  containers:
    - name: cuda
      image: nvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04
      command: ["nvidia-smi"]
      resources:
        limits:
          nvidia.com/gpu: 1

이 YAML은 "이 컨테이너에 NVIDIA GPU 한 장을 달라"는 뜻이다. 쿠버네티스 스케줄러는 각 노드의 status.allocatable을 보고 nvidia.com/gpu가 1개 이상 남은 노드를 찾는다. 그런 다음 Pod를 그 노드에 배치한다. 실제로 어떤 GPU를 컨테이너에 연결할지는 노드의 kubelet과 NVIDIA Device Plugin이 처리한다.

노드 상태를 보면 GPU가 다음처럼 보인다.

status:
  capacity:
    cpu: "64"
    memory: 512Gi
    nvidia.com/gpu: "8"
  allocatable:
    cpu: "62"
    memory: 500Gi
    nvidia.com/gpu: "8"

여기까지 보면 CPU나 메모리와 크게 다르지 않아 보인다. 하지만 중요한 차이가 있다. nvidia.com/gpu는 정수 단위 확장 리소스다. 기본 모델에서는 0.5를 요청할 수 없다. 한 Pod가 GPU 1개를 요청하면 그 GPU는 해당 Pod가 독점한다. 다른 Pod는 같은 GPU를 동시에 요청할 수 없다.

또 하나의 특징은 GPU 요청을 보통 limits에 쓴다는 점이다. 쿠버네티스의 확장 리소스는 오버커밋을 전제로 하지 않는다. CPU는 requests보다 더 많이 쓸 수 있지만, GPU는 그렇게 다루지 않는다. GPU 1개를 요청하면 한 장을 할당받고, GPU 2개를 요청하면 두 장을 할당받는다. 그 사이의 애매한 상태는 기본 모델에 없다.

이 단순함이 장점이던 시절도 있었다. 연구실이나 초기 ML 플랫폼에서 "노트북 하나에 GPU 한 장", "학습 Job 하나에 GPU 네 장" 정도의 사용 패턴이라면 충분했다. 문제는 LLM 추론이 프로덕션 워크로드가 되면서 시작되었다. 이제 클러스터에는 실시간 채팅 추론 서버, 배치 임베딩 작업, 파인튜닝 Job, 개발용 노트북, 벡터 인덱싱 작업이 동시에 올라온다. 모두 GPU를 쓰지만 필요한 GPU의 크기와 방식은 다르다.

nvidia.com/gpu: 1 모델은 이 차이를 표현하기 어렵다.

Device Plugin은 실제로 무엇을 하는가

NVIDIA Device Plugin은 보통 DaemonSet으로 배포된다. GPU 노드마다 Device Plugin Pod가 떠 있고, 이 Pod가 kubelet과 통신한다.

동작 흐름을 조금 더 구체적으로 보면 다음과 같다.

  • Device Plugin Pod가 시작되면 /var/lib/kubelet/device-plugins/ 디렉터리에 Unix domain socket을 만든다.

  • Device Plugin은 kubelet에 자신이 nvidia.com/gpu 리소스를 관리한다고 등록한다.

  • kubelet은 Device Plugin의 ListAndWatch 호출을 통해 노드의 GPU 목록과 상태를 지속적으로 받는다.

  • kubelet은 이 정보를 노드의 capacity와 allocatable에 반영한다.

  • GPU를 요청한 Pod가 해당 노드에 배치되면 kubelet은 Device Plugin의 Allocate 호출로 실제 장치 할당 정보를 받는다.

마지막 단계가 중요하다. 스케줄러는 "이 노드에 GPU가 남아 있다"는 것까지만 판단한다. 컨테이너에 어떤 /dev/nvidia0 장치를 넣을지, 어떤 드라이버 라이브러리를 마운트할지, 어떤 환경 변수를 설정할지는 노드 쪽에서 처리한다.

전통적인 방식에서는 NVIDIA Container Runtime이 NVIDIA_VISIBLE_DEVICES 같은 정보를 읽고 컨테이너 안에 필요한 장치 파일과 CUDA 라이브러리를 주입했다. 최근에는 CDI(Container Device Interface)가 더 표준적인 장치 주입 방식으로 떠오르고 있다. CDI는 장치를 벤더별 환경 변수와 hook에만 의존하지 않고, 컨테이너 런타임이 이해할 수 있는 표준 스펙으로 표현하려는 흐름이다.

현재 클러스터의 GPU 인식 상태는 다음 명령으로 빠르게 볼 수 있다.

kubectl get nodes -o custom-columns=NAME:.metadata.name,GPU:.status.allocatable.nvidia.com/gpu

실제 GPU 접근이 되는지는 작은 테스트 Pod로 확인한다.

kubectl apply -f gpu-smoke-test.yaml
kubectl logs gpu-smoke-test

nvidia-smi 출력이 보이면 최소한 드라이버, 컨테이너 런타임, Device Plugin, kubelet 사이의 기본 연결은 성공한 것이다.

하지만 운영 환경에서는 여기서 끝나지 않는다. GPU 노드는 비싸기 때문에 일반 웹 Pod가 실수로 GPU 노드에 올라오면 안 된다. 보통 GPU 노드에는 taint를 걸고, GPU 워크로드에는 toleration을 붙인다.

kubectl taint nodes gpu-node-1 nvidia.com/gpu=present:NoSchedule
kubectl label nodes gpu-node-1 nvidia.com/gpu.present=true

그리고 GPU Pod에는 다음처럼 명시한다.

spec:
  nodeSelector:
    nvidia.com/gpu.present: "true"
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists
      effect: NoSchedule

이 패턴은 아직도 널리 쓰인다. 단순한 추론 서버, 소규모 학습 Job, 개발 환경에서는 충분히 실용적이다. 그러나 운영 규모가 커질수록 "단순한 정수 GPU" 모델의 한계가 드러난다.

정수 단위 독점 할당의 한계

nvidia.com/gpu 모델의 첫 번째 한계는 부분 할당이 어렵다는 점이다. A100 80GB 한 장을 가진 노드에서 작은 임베딩 모델 네 개를 돌리고 싶다고 해보자. 각 모델은 GPU 메모리 일부만 쓴다. 이상적으로는 한 장의 GPU에 여러 모델 서버를 함께 올리고 싶다. 하지만 기본 Device Plugin 모델에서는 각 Pod가 nvidia.com/gpu: 1을 요청하므로 첫 번째 Pod가 GPU를 독점한다. 나머지 Pod는 다른 GPU를 찾아야 한다.

두 번째 한계는 GPU 속성을 세밀하게 표현하기 어렵다는 점이다. nvidia.com/gpu: 1은 "GPU 한 장"이라는 말만 한다. 이 GPU가 T4인지, A10G인지, A100 40GB인지, H100 80GB인지 직접 표현하지 못한다. 물론 GPU Feature Discovery가 노드에 라벨을 붙여주면 nodeSelector나 node affinity로 어느 정도 해결할 수 있다. 그러나 이 방식은 결국 "노드 라벨 조합을 사람이 관리하는 방식"이다. 스케줄러가 GPU 장치 자체의 속성을 일급 객체로 이해하는 것은 아니다.

세 번째 한계는 토폴로지다. 분산 학습이나 텐서 병렬 추론에서는 GPU 사이의 연결이 중요하다. 같은 노드에 있는 GPU라도 NVLink로 가까이 연결된 쌍이 있고, PCIe 루트 복합체가 달라 상대적으로 먼 쌍이 있다. 기본 확장 리소스 모델은 이런 구조를 충분히 표현하지 못한다.

네 번째 한계는 공유와 수명 관리다. 어떤 워크로드는 Pod가 재시작되어도 같은 GPU 파티션을 유지하고 싶다. 어떤 워크로드는 여러 Pod가 하나의 GPU 리소스를 함께 참조해야 한다. 하지만 Device Plugin 모델에서 장치는 Pod 실행 시점에 붙는 부속물에 가깝다. 스토리지의 PersistentVolumeClaim처럼 "리소스 요청 객체"가 독립적으로 존재하지 않는다.

이 문제를 해결하기 위해 쿠버네티스 생태계는 두 방향으로 발전했다. 하나는 NVIDIA GPU Operator처럼 GPU 소프트웨어 스택 자체를 자동화하는 방향이다. 다른 하나는 DRA처럼 쿠버네티스의 자원 모델을 더 풍부하게 만드는 방향이다. 먼저 GPU Operator부터 살펴보자.

NVIDIA GPU Operator: GPU 노드 운영의 자동 조종 장치

초기 GPU 클러스터 운영은 꽤 손이 많이 갔다. 각 GPU 노드에 NVIDIA 드라이버를 설치하고, CUDA 호환성을 확인하고, NVIDIA Container Toolkit을 설치하고, 컨테이너 런타임을 설정하고, Device Plugin을 배포하고, DCGM Exporter를 붙이고, MIG를 쓸 경우 별도 설정을 해야 했다. 노드가 2대일 때는 할 만하다. 20대가 되면 귀찮아진다. 200대가 되면 사람이 직접 관리할 일이 아니다.

NVIDIA GPU Operator는 이 작업을 쿠버네티스 방식으로 자동화한다. Operator는 쿠버네티스 컨트롤러의 한 종류다. 사용자가 원하는 상태를 선언하면, 컨트롤러가 실제 클러스터 상태를 그 선언에 맞춰 계속 조정한다.

GPU Operator가 관리하는 대표 구성 요소는 다음과 같다.2

  • NVIDIA Driver: GPU 커널 모듈과 사용자 공간 라이브러리를 노드에 설치하거나 관리한다.

  • NVIDIA Container Toolkit: containerd나 CRI-O 같은 컨테이너 런타임이 GPU 장치를 컨테이너에 전달할 수 있게 설정한다.

  • NVIDIA Device Plugin 또는 DRA Driver: GPU를 쿠버네티스 스케줄링 리소스로 노출한다.

  • GPU Feature Discovery: GPU 모델, 메모리, MIG 지원 여부 같은 정보를 노드 라벨로 노출한다.

  • DCGM Exporter: GPU 사용률, 메모리 사용량, 전력, 온도, 에러 상태 같은 메트릭을 Prometheus로 내보낸다.

  • MIG Manager: A100, H100 같은 GPU의 MIG 파티션 구성을 관리한다.

운영 관점에서 GPU Operator의 가치는 "설치 편의성"보다 "일관성"에 있다. GPU 노드는 드라이버와 런타임 버전이 조금만 어긋나도 이상한 문제가 생긴다. 어떤 노드에서는 CUDA 12.4 이미지가 잘 뜨는데, 다른 노드에서는 라이브러리 오류가 나는 식이다. Operator를 사용하면 GPU 노드 소프트웨어 스택을 쿠버네티스 리소스로 통제할 수 있다.

개념적으로는 다음 한 줄에 가깝다.

helm install gpu-operator nvidia/gpu-operator \
  --namespace gpu-operator \
  --create-namespace

실제 운영에서는 클라우드 매니지드 노드인지, 베어메탈인지, 드라이버를 호스트에 이미 설치했는지, 보안 정책상 privileged 컨테이너를 허용할 수 있는지에 따라 옵션이 달라진다. 중요한 것은 GPU Operator를 "GPU 클러스터의 패키지 매니저" 정도로만 이해하면 안 된다는 점이다. GPU Operator는 드라이버, 런타임, 장치 노출, 모니터링, MIG 관리를 하나의 운영 단위로 묶는다.

2026년 기준으로 새 GPU 클러스터를 만든다면 다음 원칙을 추천한다. 단순 실험이나 작은 k3s 환경에서는 Device Plugin만 직접 설치해도 된다. 그러나 여러 팀이 공유하는 프로덕션 클러스터라면 GPU Operator를 기본값으로 고려하자. 특히 DCGM Exporter와 MIG Manager를 함께 관리하려면 Operator 방식이 훨씬 안정적이다.

GPU 공유 전략: MIG, time-slicing, MPS

GPU를 항상 한 Pod가 독점해야 하는 것은 아니다. 오히려 LLM 추론과 임베딩 서버에서는 독점이 비용 낭비가 되는 경우가 많다. GPU 공유 전략은 크게 세 가지가 있다. MIG, time-slicing, MPS다.

방식격리 수준장점단점적합한 환경
MIG하드웨어 수준 격리메모리와 장애 격리가 강함지원 GPU가 제한되고 재구성이 무거움프로덕션 멀티테넌트 추론
time-slicing시간 분할설정이 쉽고 다양한 GPU에서 사용 가능메모리 격리가 약하고 지연시간 변동이 큼개발, 테스트, 낮은 우선순위 작업
MPSCUDA 수준 동시 실행작은 작업의 GPU 활용률을 높이기 좋음메모리·장애 격리가 약함비슷한 신뢰 수준의 추론 서버

MIG: 한 장의 GPU를 여러 개의 작은 GPU처럼 쓰기

MIG(Multi-Instance GPU)는 NVIDIA A100, H100 계열에서 사용할 수 있는 하드웨어 파티셔닝 기능이다. 하나의 물리 GPU를 여러 개의 독립적인 GPU 인스턴스로 나눈다. 각 인스턴스는 전용 GPU 메모리와 컴퓨트 자원을 가진다. 그래서 한 인스턴스에서 문제가 생겨도 다른 인스턴스에 미치는 영향이 time-slicing보다 작다.3

예를 들어 A100 80GB를 1g.10gb 프로파일 7개로 나누면, 쿠버네티스 입장에서는 10GB급 작은 GPU 7개가 생긴 것처럼 다룰 수 있다. 작은 LLM, 임베딩 모델, 분류 모델을 각각 독립 서비스로 운영할 때 효과적이다.

Device Plugin 기반 환경에서는 MIG 리소스가 다음처럼 보일 수 있다.

resources:
  limits:
    nvidia.com/mig-1g.10gb: 1

MIG의 가장 큰 장점은 격리다. 여러 팀이 같은 물리 GPU를 나눠 쓰더라도 각 팀은 자신의 MIG 인스턴스만 본다. GPU 메모리도 파티션별로 나뉘기 때문에 한 Pod가 전체 GPU 메모리를 다 잡아먹는 일을 막기 쉽다.

단점도 있다. MIG 구성은 마음대로 즉시 바꾸기 어렵다. GPU 리셋이 필요할 수 있고, 이미 실행 중인 워크로드와 충돌할 수 있다. 따라서 프로덕션에서는 "추론 노드 풀은 1g.10gb 7개", "파인튜닝 노드 풀은 3g.40gb 2개"처럼 노드 풀별 표준 프로파일을 정해두는 편이 안전하다.

time-slicing: GPU를 시간으로 나눠 쓰기

time-slicing은 하나의 물리 GPU를 여러 Pod가 번갈아 쓰게 만드는 방식이다. NVIDIA Device Plugin 설정에서 replicas를 지정하면 물리 GPU 하나가 여러 개의 스케줄링 단위처럼 보인다.

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

GPU가 2장인 노드에서 replicas: 4를 설정하면 쿠버네티스에는 nvidia.com/gpu가 8개 있는 것처럼 보일 수 있다. 즉 최대 8개의 Pod가 GPU를 요청해 스케줄링될 수 있다.

time-slicing은 설정이 쉽고, MIG를 지원하지 않는 GPU에서도 쓸 수 있다는 장점이 있다. 개발자 노트북, CI 테스트, 낮은 우선순위 배치 추론처럼 "조금 느려져도 괜찮은" 작업에 유용하다.

하지만 time-slicing은 강한 격리 기술이 아니다. 여러 Pod가 같은 GPU 메모리 공간을 경쟁적으로 사용한다. 한 Pod가 GPU 메모리를 많이 잡으면 다른 Pod가 오류를 만날 수 있다. 또한 컨텍스트 스위칭 때문에 지연시간이 튈 수 있다. 실시간 사용자 요청을 처리하는 LLM 추론 서버에 무심코 time-slicing을 적용하면 P99 지연시간이 크게 나빠질 수 있다.

MPS: CUDA 프로세스가 동시에 GPU를 쓰게 하기

MPS(Multi-Process Service)는 여러 CUDA 프로세스가 하나의 GPU를 더 효율적으로 공유하도록 돕는 NVIDIA 기술이다. time-slicing이 "번갈아 쓰기"에 가깝다면, MPS는 여러 프로세스가 CUDA 수준에서 더 자연스럽게 함께 실행되도록 돕는다.

MPS는 작은 추론 서버 여러 개가 GPU를 충분히 채우지 못할 때 유용하다. 예를 들어 짧은 임베딩 요청을 처리하는 서버 여러 개가 하나의 GPU를 공유하면 전체 GPU 활용률을 높일 수 있다.

그러나 MPS도 완전한 멀티테넌트 격리 기술은 아니다. 메모리 격리와 장애 격리가 MIG보다 약하다. 한 프로세스의 CUDA 오류가 같은 GPU를 공유하는 다른 프로세스에 영향을 줄 수 있다. 따라서 서로 신뢰할 수 없는 테넌트가 섞인 환경이라면 MPS보다 MIG가 낫다.

실무 기준은 간단하다. 프로덕션 멀티테넌트 추론은 MIG를 먼저 검토한다. 개발과 실험은 time-slicing으로 시작한다. 같은 팀의 비슷한 추론 워크로드가 GPU를 조금씩만 쓰고 있다면 MPS를 검토한다. 대형 학습이나 지연시간에 민감한 핵심 추론은 여전히 whole GPU 독점이 가장 안전하다.

공유 전략을 고르는 기준

GPU 공유 전략은 기술 취향으로 고르는 것이 아니다. 워크로드의 위험도와 비용 구조로 골라야 한다.

첫 번째 질문은 "실패 격리가 필요한가?"이다. 서로 다른 고객의 모델을 같은 GPU에 올리거나, 한 팀의 장애가 다른 팀의 서비스를 멈추면 안 되는 환경이라면 MIG가 가장 안전하다. time-slicing이나 MPS는 비용 효율은 좋지만 격리 수준이 낮다.

두 번째 질문은 "지연시간이 중요한가?"이다. 사용자 앞단의 채팅 API처럼 P95, P99 지연시간이 중요한 서비스라면 무리한 공유는 위험하다. 평균 지연시간은 괜찮아 보여도 꼬리 지연시간이 나빠질 수 있다. 반대로 야간 배치 임베딩이나 문서 요약 작업은 지연시간보다 처리량과 비용이 중요하므로 공유에 더 적합하다.

세 번째 질문은 "GPU 메모리가 병목인가, 연산량이 병목인가?"이다. 모델이 GPU 메모리를 거의 꽉 채운다면 공유할 공간이 없다. 이때는 GPU를 나누는 것이 아니라 모델 양자화, tensor parallelism, 더 큰 GPU 선택을 고민해야 한다. 반대로 메모리는 많이 남는데 GPU utilization이 낮다면 공유 전략이 효과적일 수 있다.

정리하면 다음과 같다.

상황추천 전략
7B 이하 모델 여러 개를 안정적으로 서비스MIG 우선 검토
개발자 노트북과 실험 Pod가 많음time-slicing
같은 팀의 작은 CUDA 추론 프로세스가 많음MPS
70B 이상 모델, 텐서 병렬 추론whole GPU 또는 토폴로지 인식 DRA
분산 학습whole GPU, gang scheduling, 토폴로지 인식 배치

이제 질문은 자연스럽게 다음으로 넘어간다. "이런 선택지를 쿠버네티스 API 안에서 더 잘 표현할 수는 없을까?" 바로 그 답이 DRA다.

DRA: GPU를 일급 리소스에 가깝게 다루기

Dynamic Resource Allocation, 줄여서 DRA는 쿠버네티스가 GPU 같은 특수 하드웨어를 더 구조적으로 다루기 위해 만든 자원 모델이다. 기존 Device Plugin 모델이 "노드에 nvidia.com/gpu가 몇 개 있는가"를 보고했다면, DRA는 "어떤 장치가 있고, 그 장치가 어떤 속성을 가지며, 워크로드가 어떤 조건의 장치를 원하는가"를 표현하는 방향으로 발전한다.

여기서 버전을 정확히 이해해야 한다. DRA는 Kubernetes 1.26에서 alpha로 등장했고, 1.31에서 큰 설계 변경을 거쳤으며, 1.32에서 핵심 기능이 beta로 올라왔다. 1.33에서는 partitionable devices, device taints, prioritized list 같은 중요한 기능이 추가되며 GA를 향한 준비가 진행되었다. 1.34 이후 DRA의 핵심 흐름이 더 일반화되었고, 1.36에서는 DRA 생태계의 일부 선택·관리 기능이 stable로 승격되었다.4 따라서 운영 문서에서 "DRA는 1.33부터 모든 환경에서 안정 기능"이라고 단정하면 안 된다. 클러스터 버전, feature gate, API 버전, 사용하는 DRA 드라이버를 반드시 확인해야 한다.

DRA를 이해할 때는 네 가지 객체를 기억하면 된다.

  • DeviceClass: 어떤 종류의 장치를 요청할 수 있는지 정의한다. 스토리지의 StorageClass와 비슷한 역할로 이해하면 쉽다.

  • ResourceClaim: 실제 장치 할당 요청이다. 스토리지의 PersistentVolumeClaim과 비슷하다.

  • ResourceClaimTemplate: Pod마다 ResourceClaim을 자동으로 만들기 위한 템플릿이다.

  • ResourceSlice: DRA 드라이버가 노드의 실제 장치 목록과 속성을 쿠버네티스 API에 보고하는 객체다.

기존 방식이 다음처럼 "GPU 한 개"를 요청했다면,

resources:
  limits:
    nvidia.com/gpu: 1

DRA 방식은 "특정 조건을 만족하는 GPU"를 요청하는 방향으로 간다.

apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: high-memory-nvidia-gpu
spec:
  selectors:
    - cel:
        expression: |
          device.driver == 'gpu.nvidia.com' &&
          device.attributes['gpu.nvidia.com'].memory.compareTo(quantity('40Gi')) >= 0

그리고 워크로드는 이 DeviceClass를 참조하는 ResourceClaim을 사용한다.

apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
  name: llm-gpu-claim
  namespace: ai-serving
spec:
  devices:
    requests:
      - name: gpu
        deviceClassName: high-memory-nvidia-gpu
        count: 1

Pod는 resources.claims와 resourceClaims를 통해 이 claim을 연결한다.

apiVersion: v1
kind: Pod
metadata:
  name: vllm-dra-example
  namespace: ai-serving
spec:
  containers:
    - name: vllm
      image: vllm/vllm-openai:v0.8.0
      args:
        - --model=Qwen/Qwen2.5-7B-Instruct
        - --gpu-memory-utilization=0.9
      resources:
        claims:
          - name: gpu
  resourceClaims:
    - name: gpu
      resourceClaimName: llm-gpu-claim

위 예시는 개념을 보여주기 위한 형태다. 실제 API 버전과 필드 이름, 속성 이름은 Kubernetes 버전과 DRA 드라이버에 따라 달라질 수 있다. 핵심은 YAML의 모양이 아니라 사고방식이다. 이제 Pod는 "아무 GPU 한 장"이 아니라 "40GiB 이상의 메모리를 가진 NVIDIA GPU 한 장"을 요청하는 방향으로 갈 수 있다.

ResourceClaim은 GPU의 PVC다

DRA를 처음 보면 객체가 많아 복잡하게 느껴진다. 이때 스토리지를 떠올리면 이해가 쉬워진다.

쿠버네티스에서 Pod가 디스크를 직접 고르지 않는다. Pod는 PVC를 참조하고, PVC는 StorageClass와 조건을 통해 실제 볼륨에 바인딩된다. Pod는 "이 클라우드의 이 디스크 ID를 붙여줘"라고 말하지 않는다. "빠른 SSD 100GiB가 필요해"라고 말한다.

DRA의 ResourceClaim도 비슷하다. Pod는 GPU의 물리 주소나 장치 번호를 직접 지정하지 않는다. "H100이면 좋고, 없으면 A100도 괜찮다", "최소 40GiB 메모리가 필요하다", "같은 NVLink 도메인 안의 GPU 네 장이 필요하다", "3g.40gb MIG 파티션이 필요하다"처럼 요구 사항을 선언하는 방향으로 간다.

이것은 실제 운영에서 중요하다. AI 클러스터는 생각보다 균질하지 않다. 처음에는 A100으로 시작했다가, 다음 분기에 H100을 추가하고, 나중에는 L40S나 B200 계열이 들어올 수 있다. 모든 워크로드 YAML을 노드 라벨 기준으로 직접 고치는 방식은 오래 버티기 어렵다.

ResourceClaim은 수명 관리 측면에서도 유용하다. Pod와 독립적으로 존재할 수 있기 때문에 장치 할당을 더 명시적으로 추적할 수 있다. 어떤 claim이 어떤 장치에 바인딩되었는지, 왜 Pending인지, 장치 health status가 어떤지 API 객체로 확인할 수 있다. GPU가 더 이상 "컨테이너 실행 시점에 몰래 붙는 부속물"이 아니라 쿠버네티스가 추적하는 자원이 되는 셈이다.

DeviceClass로 GPU 티어 설계하기

플랫폼 팀은 모든 개발자에게 GPU 모델명을 외우게 해서는 안 된다. "A100 80GB PCIe", "H100 SXM", "L40S", "MIG 1g.10gb" 같은 세부사항은 플랫폼 팀이 관리하고, 애플리케이션 팀은 자신의 워크로드에 맞는 추상화된 클래스를 선택하는 편이 좋다.

예를 들어 다음처럼 DeviceClass를 나눌 수 있다.

apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: llm-small-inference
spec:
  selectors:
    - cel:
        expression: |
          device.driver == 'gpu.nvidia.com' &&
          device.attributes['gpu.nvidia.com'].memory.compareTo(quantity('10Gi')) >= 0
---
apiVersion: resource.k8s.io/v1
kind: DeviceClass
metadata:
  name: llm-large-inference
spec:
  selectors:
    - cel:
        expression: |
          device.driver == 'gpu.nvidia.com' &&
          device.attributes['gpu.nvidia.com'].memory.compareTo(quantity('80Gi')) >= 0

실제 운영에서는 이보다 더 정교하게 나눈다. 예를 들어 embedding-shared, realtime-inference, batch-inference, fine-tuning, training-high-priority처럼 워크로드 성격을 반영한 이름을 붙일 수 있다. 중요한 것은 이름을 하드웨어 모델 그대로 노출하지 않는 것이다.

개발자는 다음처럼 말할 수 있어야 한다.

apiVersion: resource.k8s.io/v1
kind: ResourceClaimTemplate
metadata:
  name: realtime-inference-gpu
spec:
  spec:
    devices:
      requests:
        - name: gpu
          deviceClassName: llm-small-inference
          count: 1

이렇게 하면 나중에 플랫폼 팀이 llm-small-inference의 실제 매칭 조건을 바꿔도 애플리케이션 YAML을 크게 바꿀 필요가 없다. 오늘은 A100 MIG를 사용하고, 내년에는 H100 MIG나 다른 가속기 파티션을 사용할 수 있다.

DRA와 토폴로지 인식 배치

대형 AI 워크로드에서 가장 아까운 낭비는 "GPU는 할당됐는데 성능이 안 나오는" 상황이다. Pod는 정상적으로 Running이고, GPU도 네 장을 받았다. 그런데 학습 속도가 예상보다 느리다. 알고 보니 GPU 간 통신 경로가 좋지 않거나, GPU와 NIC가 다른 NUMA 도메인에 있었다. 이런 문제는 단순 GPU 개수만 보고는 잡기 어렵다.

DRA는 장치 속성과 제약 조건을 통해 이런 문제를 더 잘 표현할 수 있는 기반을 제공한다. ResourceSlice는 장치의 속성, 용량, 연결 정보를 드라이버가 쿠버네티스에 보고하는 통로다. ResourceClaim은 이 정보에 대해 조건을 건다.

예를 들어 분산 학습 Job이 GPU 네 장을 요청한다고 해보자. 단순히 count: 4만으로는 부족할 수 있다. 네 장이 같은 NVLink 도메인에 있어야 한다는 조건이 필요할 수 있다. DRA의 제약 조건은 이런 요구를 API에 더 가깝게 끌어올리는 방향으로 발전하고 있다.

개념적으로는 다음과 같은 요청이 가능해진다.

apiVersion: resource.k8s.io/v1
kind: ResourceClaim
metadata:
  name: training-gpu-claim
spec:
  devices:
    requests:
      - name: gpu
        deviceClassName: distributed-training-nvlink
        count: 4
    constraints:
      - requests: ["gpu"]
        matchAttribute: gpu.nvidia.com/nvlink-domain

이 예시도 드라이버별 실제 속성 이름은 다를 수 있다. 그러나 방향은 분명하다. 사용자는 더 이상 노드 이름, GPU 번호, 라벨 조합을 손으로 맞추는 대신 "함께 배치되어야 하는 장치의 관계"를 선언한다. 스케줄러와 DRA 드라이버가 그 조건을 해석한다.

이 변화는 분산 학습과 disaggregated 추론에서 특히 중요해진다. prefill과 decode를 분리한 LLM 추론 아키텍처, 여러 GPU에 걸친 tensor parallel 그룹, NVLink/NVSwitch 기반 대형 노드에서는 "몇 개의 GPU"보다 "어떤 관계의 GPU"가 더 중요하다.

CDI: 장치를 컨테이너에 넣는 표준 방식

GPU 스케줄링은 두 단계로 나뉜다. 첫 번째는 스케줄링이다. 어떤 노드의 어떤 장치를 이 Pod에 줄지 결정한다. 두 번째는 주입이다. 결정된 장치를 실제 컨테이너 안에서 사용할 수 있게 만든다.

과거에는 NVIDIA Container Runtime과 환경 변수 중심의 방식이 많이 쓰였다. NVIDIA_VISIBLE_DEVICES에 GPU UUID를 넣고, 런타임 hook이 이를 읽어 /dev/nvidia* 장치와 드라이버 라이브러리를 컨테이너에 마운트하는 식이다. 이 방식은 NVIDIA GPU에는 잘 맞았지만, 벤더 중립적인 표준이라고 보기는 어려웠다.

CDI(Container Device Interface)는 이 문제를 해결하려는 표준 장치 주입 방식이다. CDI에서는 장치가 표준화된 이름과 스펙으로 표현된다. 컨테이너 런타임은 CDI 스펙을 읽고 필요한 장치를 컨테이너에 주입한다. 애플리케이션이나 kubelet이 벤더별 환경 변수와 장치 파일 세부사항을 직접 다룰 필요가 줄어든다.

DRA와 CDI는 서로 잘 맞는다. DRA는 "어떤 장치를 할당할지"를 쿠버네티스 API 수준에서 결정하고, CDI는 "그 장치를 컨테이너에 어떻게 넣을지"를 런타임 수준에서 표준화한다. 이 변화는 NVIDIA만을 위한 것이 아니다. TPU, AMD GPU, FPGA, SmartNIC, AI ASIC 같은 장치도 각자의 DRA 드라이버와 CDI 스펙을 통해 비슷한 방식으로 다뤄질 수 있다.

DRA는 Device Plugin을 당장 대체하는가

현실적인 질문을 해보자. 지금 운영 중인 클러스터가 NVIDIA Device Plugin으로 잘 돌아가고 있다면 당장 DRA로 갈아타야 할까?

답은 "상황에 따라 다르다"다.

작은 클러스터에서 GPU 몇 장으로 단일 모델 추론 서버를 돌리는 정도라면 Device Plugin 모델은 여전히 충분하다. nvidia.com/gpu: 1은 단순하고, 문서와 사례가 많고, 문제를 검색하기 쉽다. GPU Operator와 함께 쓰면 운영 자동화도 꽤 괜찮다.

반대로 다음 조건에 해당한다면 DRA 도입을 진지하게 검토해야 한다.

  • 여러 종류의 GPU가 섞여 있고, 워크로드마다 필요한 GPU 메모리와 모델이 다르다.

  • MIG, MPS, time-slicing 같은 공유 전략을 정책적으로 관리해야 한다.

  • NVLink, NUMA, PCIe, NIC 근접성 같은 토폴로지가 성능에 중요하다.

  • 여러 팀이 하나의 GPU 클러스터를 공유하고, quota와 우선순위를 세밀하게 운영해야 한다.

  • 앞으로 NVIDIA GPU뿐 아니라 TPU, AMD GPU, 다른 AI 가속기도 같은 플랫폼에서 다룰 가능성이 있다.

또 하나 중요한 점이 있다. Device Plugin과 DRA 드라이버를 같은 GPU 자원에 대해 동시에 쓰는 것은 일반적으로 권장되지 않는다. 같은 물리 GPU를 두 시스템이 동시에 스케줄링 리소스로 노출하면 자원 이중 계산이나 충돌이 생길 수 있다. 따라서 마이그레이션은 점진적으로 설계해야 한다. 새 노드 풀이나 새 네임스페이스에서 DRA 기반 워크로드를 먼저 테스트하고, 기존 프로덕션 워크로드는 Device Plugin 기반으로 유지할 수 있다. 그런 다음 워크로드별로 ResourceClaim 모델로 옮긴다.

운영에서 자주 만나는 GPU Pending 문제

GPU 스케줄링 장애의 대부분은 Pod가 Pending에 머무는 형태로 나타난다. 이때 로그부터 찾으면 안 된다. Pending Pod는 아직 컨테이너가 시작되지 않았으므로 애플리케이션 로그가 없다. 먼저 이벤트를 읽어야 한다.

kubectl describe pod <pod-name> -n <namespace>

대표적인 메시지는 다음과 같다.

0/5 nodes are available: 3 Insufficient nvidia.com/gpu, 2 node(s) had untolerated taint
0/5 nodes are available: 5 Insufficient memory
0/5 nodes are available: 1 node(s) didn't match Pod's node affinity/selector
pod has unbound immediate PersistentVolumeClaims

이 메시지는 스케줄러가 남긴 설명이다. Insufficient nvidia.com/gpu는 GPU가 부족하다는 뜻이다. untolerated taint는 GPU 노드에 taint가 있는데 Pod에 toleration이 없다는 뜻이다. node affinity 문제는 특정 GPU 모델이나 노드 라벨을 요구했는데 맞는 노드가 없다는 뜻이다. PVC가 바인딩되지 않았으면 스토리지 문제가 먼저다.

GPU Pod라면 리소스 요청을 확인한다.

resources:
  limits:
    nvidia.com/gpu: 1

NVIDIA GPU는 전통적인 extended resource로 노출될 때 limits에 지정해야 한다. CPU처럼 requests만 적고 기대하면 스케줄링이 되지 않는다. 또한 GPU는 기본적으로 정수 단위로 요청한다. fractional GPU, time-slicing, MIG 같은 공유 방식은 별도 설정과 리소스 이름을 통해 구현해야 한다.

노드의 GPU 자원은 다음 명령으로 확인한다.

kubectl describe node gpu-node-01 | grep -A8 -E "Capacity|Allocatable"
kubectl get nodes -o jsonpath='{range .items[*]}{.metadata.name}{"t"}{.status.allocatable.nvidia.com/gpu}{"n"}{end}'

DRA를 쓰는 환경이라면 ResourceClaim도 함께 본다.

kubectl get resourceclaim -A
kubectl describe resourceclaim <claim-name> -n <namespace>

Pending은 실패가 아니다. 스케줄러가 "이 조건으로는 배치할 수 없다"고 알려주는 상태다. 이 메시지를 잘 읽으면 불필요한 로그 검색을 줄일 수 있다.

GPU 스케줄링은 비용 정책이다

GPU 스케줄링은 단순히 "파드를 어디에 놓을까"의 문제가 아니다. 비용 정책이다. 7B 모델이 H100 전체를 독점하면 비용 정책이 잘못된 것이다. 프로덕션 추론과 실험용 노트북이 같은 GPU 풀에서 경쟁하면 우선순위 정책이 없는 것이다. tensor parallel이 필요한 모델이 아무 GPU 네 장이나 받아 성능이 나오지 않는다면 토폴로지 정책이 빠진 것이다.

따라서 AI 클러스터의 GPU 운영은 다음 질문으로 시작해야 한다.

  • 어떤 워크로드는 whole GPU를 독점해야 하는가?

  • 어떤 워크로드는 MIG나 time-slicing으로 나눠 써도 되는가?

  • 어떤 GPU 모델을 어떤 팀과 서비스에 제공할 것인가?

  • 실시간 추론과 배치 학습 중 누가 우선권을 가져야 하는가?

  • GPU 노드의 라벨, taint, quota, PriorityClass, Kueue 정책은 서로 맞는가?

  • DRA를 도입한다면 어떤 노드 풀과 워크로드부터 시작할 것인가?

이 질문에 답하지 않은 채 GPU Operator만 설치하면, 클러스터는 돌아가지만 비용은 새어 나간다. GPU가 보인다고 GPU 플랫폼이 완성된 것은 아니다. GPU를 어떤 단위로 나누고, 누구에게 주고, 어떤 조건에서 회수하고, 어떤 지표로 효율을 판단할지 정해야 비로소 운영이 시작된다.

정리: GPU는 개수가 아니라 조건이다

이 장의 핵심은 단순하다. 쿠버네티스의 전통적인 GPU 모델은 nvidia.com/gpu: 1이었다. 이 모델은 단순하고 강력하며, 지금도 많은 환경에서 충분히 쓸 만하다. 하지만 AI 워크로드가 커지고 다양해지면서 "GPU 한 장"이라는 표현은 점점 부족해지고 있다.

MIG, time-slicing, MPS는 한 장의 GPU를 더 잘 나눠 쓰기 위한 전략이다. GPU Operator는 드라이버, 런타임, Device Plugin, DCGM Exporter, MIG Manager를 묶어 GPU 노드 운영의 일관성을 높인다. DRA는 여기서 한 걸음 더 나아가 GPU 같은 특수 장치를 속성, 조건, claim, class로 표현하려는 방향이다.

앞으로의 GPU 스케줄링은 "몇 장이 필요한가"만 묻지 않는다. "어떤 메모리 크기의 GPU가 필요한가", "같은 토폴로지 안에 있어야 하는가", "MIG slice여도 되는가", "H100이 없으면 A100으로 대체 가능한가", "이 워크로드가 실시간 추론보다 낮은 우선순위인가"를 함께 묻는다.

AI 시대의 쿠버네티스에서 GPU는 개수가 아니라 조건이다. 이 관점을 이해하면, 뒤에서 다룰 Kueue, JobSet, LLMInferenceService, disaggregated 추론, KEDA 기반 오토스케일링이 왜 필요한지 훨씬 자연스럽게 보인다.

참고

1Kubernetes Device Plugins

2NVIDIA GPU Operator Documentation

3NVIDIA Multi-Instance GPU User Guide

4Kubernetes v1.33: New features in DRA

책 목록

AI 시대의 쿠버네티스: LLM 추론과 에이전트를 운영하는 인프라 전략

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기