반응형
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 전체 워크로드 드레인 필요 — 운영 중 변경 불가
반응형
'AI 개발' 카테고리의 다른 글
| K8s AI 워크로드 3편—KEDA 스케일링과 멀티테넌시, HPA가 LLM에 왜 안 되는지부터 (0) | 2026.05.23 |
|---|---|
| K8s AI 워크로드 2편—LLM 추론 서버 배포, vLLM·TGI·Triton 실전 Deployment 완전 가이드 (0) | 2026.05.23 |
| AWS Kiro 3편—AgentCore 연동과 CDK 자동화, 코드 한 줄 없이 서버리스 풀스택 배포까지 (0) | 2026.05.22 |
| AWS Kiro 2편—Specs Wave 실행·Steering 아키텍처·Autonomous Agent, 팀 프로덕션에서 살아남는 법 (0) | 2026.05.22 |
| OpenAI, 사상 최대 IPO를 향해 달린다 — S-1 비밀 제출, 지금 개발자가 알아야 할 것 (0) | 2026.05.22 |