본문 바로가기

AI 개발

K8s AI 워크로드 1편—GPU 노드 설정과 인프라 기초, 비싼 GPU 낭비 없이 쓰는 법

반응형

 

AI 서비스 Kubernetes에 올렸더니 GPU 노드에 일반 Pod가 들어가고, GPU Pod는 Pending에서 안 떠나는 경험, 한 번쯤 합니다. 일반 웹앱 운영이랑 GPU 워크로드는 출발점부터 다릅니다.


📌 핵심 요약
→ GPU 노드는 반드시 Taint — 안 하면 CPU Pod가 $30K짜리 노드에 들어옴
→ Device Plugin vs GPU Operator: 소규모는 Plugin, 프로덕션은 Operator
→ K8s 1.34 DRA GA — Device Plugin 정수 단위 할당의 한계를 구조화 파라미터로 해결
→ GPU 공유 3가지: MIG(하드웨어 격리) / Time-slicing(소프트 공유) / MPS(동시 실행)
→ requests = limits 필수 — GPU는 오버커밋 불가, 불일치 시 스케줄러 패닉
→ Namespace별 RBAC + ResourceQuota: 팀별 GPU 할당량 강제
→ PriorityClass: 추론 > 학습 > 실험 순으로 선점 정책
→ 평균 GPU 활용률 30~40% — 설정 하나로 50~75% 비용 절감 가능

 


실전1 — GPU 노드풀 구성: 라벨·Taint 전략

GPU 노드는 CPU 노드와 섞이면 안 됩니다. 제일 먼저 할 일은 물리적 분리입니다.

# GPU 노드에 상세 라벨 부착 (EKS 예시 — 다른 클라우드도 동일 개념)
kubectl label node gpu-node-1 \
  node-type=gpu \
  gpu-type=a100 \
  gpu-memory=40gb \
  workload-class=inference   # inference / training / notebook

# GPU 노드 전용 Taint — 이게 없으면 일반 Pod가 GPU 노드 점거
kubectl taint node gpu-node-1 \
  nvidia.com/gpu=present:NoSchedule \
  dedicated=gpu:NoSchedule

# 노드 상태 확인
kubectl describe node gpu-node-1 | grep -A5 "Taints:"
# Taints: dedicated=gpu:NoSchedule
#         nvidia.com/gpu=present:NoSchedule
# gpu-nodepool-labels.yaml — 노드 라벨 체계 설계
#
# 레이블 네임스페이스 설계 원칙:
#   node-type:      gpu / cpu / memory-optimized
#   gpu-type:       a100 / h100 / t4 / v100
#   gpu-memory:     80gb / 40gb / 16gb
#   workload-class: inference / training / notebook
#
# 이 라벨 체계가 있어야 Pod가 맞는 GPU에 배치됨
# 예: 추론 서버는 A100, 학습은 H100, 개발은 T4

# 라벨 한 번에 적용 (노드 그룹 단위)
apiVersion: v1
kind: ConfigMap
metadata:
  name: gpu-node-labels-guide
  namespace: kube-system
data:
  guide: |
    A100 노드:  gpu-type=a100, gpu-memory=40gb, workload-class=training
    H100 노드:  gpu-type=h100, gpu-memory=80gb, workload-class=training
    T4 노드:    gpu-type=t4,   gpu-memory=16gb, workload-class=inference
    노트북 노드: gpu-type=t4,   gpu-memory=16gb, workload-class=notebook
→ Taint 없이 운영 시 발생하는 문제:
   - 일반 웹 Pod가 GPU 노드에 스케줄 → GPU 노드 CPU·메모리 점거
   - GPU Pod는 "no nodes available" → Pending 상태
   - 시간당 $4~$30 GPU 노드를 nginx가 쓰는 상황 발생
→ NoSchedule vs NoExecute:
   NoSchedule  → 새 Pod만 차단 (기존 Pod 유지)
   NoExecute   → 기존 Pod까지 퇴거 (강제 이전 시 사용)
→ dedicated=gpu Taint는 nvidia.com/gpu Taint와 함께 이중으로 걸 것

실전2 — GPU Operator 설치: Device Plugin이냐 Operator냐

[선택 기준]

Device Plugin (단순)          GPU Operator (프로덕션)
──────────────────────        ──────────────────────────────
소규모 단일팀 클러스터          멀티테넌트·대규모 클러스터
GPU 드라이버 직접 관리          드라이버·런타임 전체 자동 관리
MIG·Time-slicing 수동 설정     MIG·Time-slicing 자동화
DCGM 메트릭 별도 설치           DCGM Exporter 내장
설정 단순                       컴포넌트 7개 자동 배포

→ 2026년 기준: 프로덕션이면 Operator, 테스트 환경이면 Device Plugin
# GPU Operator 설치 (Helm)
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

helm install gpu-operator nvidia/gpu-operator \
  --namespace gpu-operator \
  --create-namespace \
  --set driver.enabled=true \
  --set toolkit.enabled=true \
  --set devicePlugin.enabled=true \
  --set dcgmExporter.enabled=true \    # Prometheus 메트릭
  --set migManager.enabled=true \      # MIG 자동 관리 (A100/H100)
  --wait

# 설치 확인 — 7개 컴포넌트 전부 Running이어야 함
kubectl get pods -n gpu-operator
# nvidia-driver-daemonset-xxx         Running  ← 드라이버
# nvidia-container-toolkit-xxx        Running  ← 컨테이너 런타임
# nvidia-device-plugin-xxx            Running  ← GPU 노출
# nvidia-dcgm-exporter-xxx            Running  ← 메트릭
# nvidia-mig-manager-xxx              Running  ← MIG 관리
# nvidia-operator-validator-xxx       Running  ← 설치 검증
# gpu-operator-xxx                    Running  ← 컨트롤러
# GPU가 스케줄 가능한지 확인
kubectl get nodes -o custom-columns=\
  "NAME:.metadata.name,\
  GPU:.status.allocatable.nvidia\.com/gpu,\
  TYPE:.metadata.labels.gpu-type"

# NAME            GPU   TYPE
# gpu-node-1      4     a100
# gpu-node-2      8     h100
# cpu-node-1      <none> <none>
→ 설치 후 Pending 가장 흔한 원인:
   1) validator Pod가 Running 아님 → 업스트림 컴포넌트 확인
   2) driver Pod CrashLoopBack → 커널 버전·CUDA 버전 불일치
   3) nvidia.com/gpu = 0 → device plugin 미기동
→ 드라이버 버전 고정 필수: 노드마다 다른 버전이면 MIG 재설정 레이스 발생

실전3 — GPU 공유 전략: MIG / Time-slicing / MPS 선택

DRA는 Device Plugin을 대체하고, KAI는 AI 큐를 위한 기본 스케줄러를 대체하며, MIG는 H100을 7개의 격리된 테넌트로 분할하고, MPS는 소프트 공유를 하며, Time-slicing은 개발 클러스터 기본값입니다.

# Time-slicing 설정 — 개발·테스트 환경 (격리 없음, 간단)
# T4 노드에 적용: 물리 GPU 1개를 4개로 나눠 보이게 함
apiVersion: v1
kind: ConfigMap
metadata:
  name: time-slicing-config
  namespace: gpu-operator
data:
  any: |-
    version: v1
    flags:
      migStrategy: none
    sharing:
      timeSlicing:
        resources:
        - name: nvidia.com/gpu
          replicas: 4      # GPU 1개 → 4개로 광고
                           # 4개 Pod가 순번제로 GPU 공유
# Time-slicing ConfigMap GPU Operator에 적용
kubectl patch clusterpolicy gpu-cluster-policy \
  -n gpu-operator \
  --type merge \
  -p '{"spec":{"devicePlugin":{"config":{"name":"time-slicing-config"}}}}'

# 적용 후 확인: GPU 1개 → 4개로 보임
kubectl get node gpu-node-1 -o jsonpath='{.status.allocatable}' | jq
# "nvidia.com/gpu": "4"   ← 물리 1개지만 4개로 광고
# MIG 설정 — 프로덕션 추론 (하드웨어 격리, A100/H100만 지원)
# A100 40GB → 7개 MIG 인스턴스로 분할
apiVersion: v1
kind: ConfigMap
metadata:
  name: mig-config
  namespace: gpu-operator
data:
  config.yaml: |
    version: v1
    mig-configs:
      # 추론 서버용: 7개 소형 인스턴스
      inference-7:
        - devices: [0,1,2,3]          # 노드의 0~3번 GPU
          mig-enabled: true
          mig-devices:
            "1g.5gb": 7               # A100을 5GB×7 인스턴스로 분할

      # 학습용: 4개 중형 인스턴스
      training-4:
        - devices: [4,5,6,7]          # 노드의 4~7번 GPU
          mig-enabled: true
          mig-devices:
            "2g.10gb": 4              # A100을 10GB×4 인스턴스로 분할
[GPU 공유 방식 선택 가이드]

방식          격리   메모리 보장  동시 실행  지원 GPU     추천 상황
──────────────────────────────────────────────────────────────
Time-slicing  ❌     ❌          ❌ (순번)  모든 GPU     개발·테스트
MIG           ✅     ✅          ✅         A100/H100    프로덕션 멀티테넌트
MPS           ❌     △           ✅ (동시)  모든 GPU     신뢰된 단일팀
DRA (K8s 1.34+) ✅  ✅          ✅         드라이버 지원  2026년 신규 프로젝트

→ MIG: 메모리·장애 하드웨어 수준 격리 → 팀간 격리 필수면 MIG
→ Time-slicing: 격리 없지만 설정 간단 → 개발팀 노트북 환경
→ MPS: CUDA 커널 동시 실행으로 활용률 최고 → 단일팀 추론 최적화
→ DRA: K8s 1.34 GA, 구조화 파라미터로 세밀한 요청 가능 → 신규 설계시 채택

실전4 — Pod 스케줄링: nodeSelector / Toleration / Affinity 실전 조합

# inference-deployment.yaml — 추론 서버 Pod 스케줄링 완전 예시
apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference-server
  namespace: ml-serving
spec:
  replicas: 2
  selector:
    matchLabels:
      app: llm-inference
  template:
    metadata:
      labels:
        app: llm-inference
    spec:
      # ── Toleration: GPU 노드 Taint 허용 ─────────────────
      tolerations:
      - key: "nvidia.com/gpu"
        operator: "Equal"
        value: "present"
        effect: "NoSchedule"
      - key: "dedicated"
        operator: "Equal"
        value: "gpu"
        effect: "NoSchedule"

      # ── Affinity: 반드시 T4 GPU 노드에만 배치 ───────────
      affinity:
        nodeAffinity:
          # 필수 조건 (이걸 못 지키면 Pending)
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: gpu-type
                operator: In
                values: ["t4", "a100"]          # T4 또는 A100
              - key: workload-class
                operator: In
                values: ["inference"]           # 추론 전용 노드만

          # 선호 조건 (이걸 못 지켜도 스케줄 됨)
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 80
            preference:
              matchExpressions:
              - key: gpu-memory
                operator: In
                values: ["40gb", "80gb"]        # 큰 메모리 선호

        # 동일 노드에 추론 서버 여러 개 분산 배치
        podAntiAffinity:
          preferredDuringSchedulingIgnoredDuringExecution:
          - weight: 100
            podAffinityTerm:
              labelSelector:
                matchLabels:
                  app: llm-inference
              topologyKey: kubernetes.io/hostname

      containers:
      - name: inference-server
        image: vllm/vllm-openai:latest
        resources:
          requests:
            cpu: "8"
            memory: "32Gi"
            nvidia.com/gpu: "1"     # ← requests = limits 필수!
          limits:
            cpu: "8"
            memory: "32Gi"
            nvidia.com/gpu: "1"     # GPU는 오버커밋 불가
        env:
        - name: MODEL_NAME
          value: "meta-llama/Llama-3-8B-Instruct"
        - name: CUDA_VISIBLE_DEVICES
          value: "0"
# training-job.yaml — 학습 Pod (다른 조건)
apiVersion: batch/v1
kind: Job
metadata:
  name: model-finetuning
  namespace: ml-training
spec:
  template:
    spec:
      tolerations:
      - key: "nvidia.com/gpu"
        operator: "Exists"
        effect: "NoSchedule"
      - key: "dedicated"
        operator: "Equal"
        value: "gpu"
        effect: "NoSchedule"

      affinity:
        nodeAffinity:
          requiredDuringSchedulingIgnoredDuringExecution:
            nodeSelectorTerms:
            - matchExpressions:
              - key: gpu-type
                operator: In
                values: ["a100", "h100"]        # 학습은 고성능 GPU만
              - key: workload-class
                operator: In
                values: ["training"]

      restartPolicy: OnFailure
      containers:
      - name: trainer
        image: pytorch/pytorch:2.3.0-cuda12.1-cudnn8-runtime
        resources:
          requests:
            nvidia.com/gpu: "4"                 # 멀티GPU 학습
          limits:
            nvidia.com/gpu: "4"
→ nodeSelector vs nodeAffinity:
   nodeSelector: 단순 라벨 매칭 (구버전, 필수 조건만)
   nodeAffinity:  required + preferred 분리 → 필수·선호 조건 동시 표현 가능
→ Toleration + Affinity 반드시 함께:
   Toleration만: GPU 노드에 배치 가능하지만 강제 아님
   Affinity만:   Taint 없으면 아무 노드나 감
   둘 다:        GPU 노드에만, 반드시 배치
→ requests = limits GPU는 항상 동일하게:
   불일치 시 스케줄러 혼란 → 실제로 배포 실패 케이스 다수

실전5 — Namespace RBAC + PriorityClass: 팀별 격리와 우선순위

# namespace-setup.yaml — AI 워크로드 Namespace 구조
apiVersion: v1
kind: Namespace
metadata:
  name: ml-serving        # 추론 서버 (프로덕션)
  labels:
    team: platform
    env: production
---
apiVersion: v1
kind: Namespace
metadata:
  name: ml-training       # 학습 작업
  labels:
    team: ml-research
    env: production
---
apiVersion: v1
kind: Namespace
metadata:
  name: ml-notebook       # 개발·실험
  labels:
    team: ml-research
    env: development
# rbac.yaml — 팀별 최소 권한
apiVersion: v1
kind: ServiceAccount
metadata:
  name: inference-server
  namespace: ml-serving
  annotations:
    automountServiceAccountToken: "false"   # 불필요한 토큰 마운트 차단
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: inference-role
  namespace: ml-serving
rules:
# 추론 서버는 모델 설정만 읽을 수 있음
- apiGroups: [""]
  resources: ["configmaps", "secrets"]
  verbs: ["get"]
  resourceNames: ["model-config", "hf-token"]
# Pod 상태 조회 (헬스체크용)
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: inference-rolebinding
  namespace: ml-serving
subjects:
- kind: ServiceAccount
  name: inference-server
  namespace: ml-serving
roleRef:
  kind: Role
  apiGroup: rbac.authorization.k8s.io
  name: inference-role
# priority-class.yaml — 추론 > 학습 > 실험 선점 정책
# GPU 부족 시 낮은 우선순위 Pod를 퇴거시키고 높은 우선순위 Pod 배치

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: inference-critical
value: 1000000            # 가장 높음 — 절대 선점 안 됨
globalDefault: false
preemptionPolicy: Never   # 이 클래스는 남을 선점하지 않음
description: "프로덕션 추론 서버 — 최고 우선순위"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: training-normal
value: 500000             # 중간
globalDefault: false
preemptionPolicy: PreemptLowerPriority
description: "학습 작업 — 실험 Pod 선점 가능"
---
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: experiment-low
value: 100000             # 낮음 — GPU 부족 시 먼저 퇴거
globalDefault: true
preemptionPolicy: PreemptLowerPriority
description: "실험·노트북 — GPU 부족 시 자동 퇴거"
# Deployment에 PriorityClass 적용
spec:
  template:
    spec:
      priorityClassName: inference-critical   # 추론 서버
      # priorityClassName: training-normal    # 학습 작업
      # priorityClassName: experiment-low     # 실험·개발
[PriorityClass 동작 원리]

GPU 노드 포화 상태에서 새 추론 서버 Pod 요청 시:

1. 스케줄러: "inference-critical(100만) > experiment-low(10만)"
2. experiment-low Pod 퇴거 (Evict)
3. 해당 GPU 슬롯에 inference-critical Pod 배치
4. 퇴거된 실험 Pod: 다른 노드 가용 시 재스케줄

→ 실전 효과: 프로덕션 추론이 절대 GPU를 빼앗기지 않음
→ 학습 Job: 체크포인트 저장 로직 필수 (선점 시 재시작하면 처음부터)
→ 개발 노트북: GPU 부족하면 자동 퇴거 → 사용자에게 알림 필요

실전6 — 설치 검증: GPU Pod 정상 동작 확인

# 1. GPU 할당 가능 확인
kubectl get nodes -o json | jq '.items[] | {
  name: .metadata.name,
  gpu: .status.allocatable["nvidia.com/gpu"],
  gpu_type: .metadata.labels["gpu-type"]
}'

# 2. GPU 테스트 Pod 실행
kubectl run gpu-test \
  --image=nvidia/cuda:12.1.0-base-ubuntu22.04 \
  --restart=Never \
  --overrides='{
    "spec": {
      "tolerations": [{"key":"nvidia.com/gpu","operator":"Exists","effect":"NoSchedule"}],
      "containers": [{
        "name": "gpu-test",
        "image": "nvidia/cuda:12.1.0-base-ubuntu22.04",
        "command": ["nvidia-smi"],
        "resources": {"limits": {"nvidia.com/gpu": "1"}}
      }]
    }
  }' \
  --rm -it

# 정상 출력 예시:
# +-----------------------------------------------------------------------------+
# | NVIDIA-SMI 545.29.06    Driver Version: 545.29.06    CUDA Version: 12.3     |
# | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
# | 0  Tesla T4            Off  | 00000000:00:1E.0 Off |                    0 |

# 3. GPU Operator 컴포넌트 전체 상태
kubectl get pods -n gpu-operator -o wide

# 4. GPU 리소스 현황
kubectl describe nodes | grep -A5 "Allocated resources" | grep nvidia
# nvidia.com/gpu:  2 (50%)   ← 4개 중 2개 사용 중
→ 가장 흔한 트러블슈팅 3가지:
   1) GPU Pod Pending: kubectl describe pod → "0/N nodes available" 확인
      → Toleration 누락 or affinity 조건 너무 엄격
   2) nvidia-smi 안 보임: driver DaemonSet 상태 확인 → 커널 버전 불일치
   3) GPU 할당 후 CUDA 에러: container runtime 버전 확인 → Operator 재설치

✅ 1편 핵심 정리
✅ GPU 노드 반드시 Taint — 안 하면 비싼 노드 CPU Pod가 점거
✅ GPU Operator: 드라이버·Device Plugin·DCGM 전체 자동 관리 (프로덕션 표준)
✅ 공유 전략 3가지: MIG(격리 필요) / Time-slicing(개발) / MPS(동시 실행)
✅ Toleration + Affinity 반드시 함께 — 둘 중 하나만 쓰면 효과 없음
✅ requests = limits GPU는 반드시 동일 설정
✅ PriorityClass: 추론 > 학습 > 실험 선점 정책으로 GPU 우선순위 보장

❌ nodeSelector 단독으로 쓰면 Taint 있는 노드 못 감 — Toleration 필수
❌ Time-slicing은 격리 없음 — 멀티테넌트 프로덕션에선 MIG 사용
❌ DRA(K8s 1.34+) 아직 드라이버 지원 확인 필요 — 기존 클러스터는 Device Plugin 유지
❌ MIG 재설정 시 해당 GPU 전체 워크로드 드레인 필요 — 운영 중 변경 불가

 

반응형