LabHub

블로그

[AWS] Karpenter로 GPU 노드 관리하기: AI/ML 워크로드 최적화

한국어English日本語

목차

1. Karpenter를 이용한 GPU 노드 프로비저닝

GPU 워크로드의 특수성

AI/ML 워크로드는 일반 컴퓨팅과 다른 고유한 요구사항을 가집니다:

+---------------------------------------------------------------+
|                GPU 워크로드 특성                               |
+---------------------------------------------------------------+
| - 고가의 GPU 인스턴스 (시간당 수~수십 달러)                   |
| - 학습 작업의 장시간 실행 (수시간~수일)                       |
| - 추론 작업의 낮은 지연 시간 요구                             |
| - GPU 메모리(VRAM) 기반 리소스 제약                           |
| - 인스턴스 타입별 GPU 성능 차이가 큼                          |
| - Spot 중단 시 학습 진행 상황 손실 위험                       |
+---------------------------------------------------------------+

Karpenter가 GPU 관리에 적합한 이유

+------------------------------------------+
|         기존 방식 (Cluster Autoscaler)    |
|                                           |
|  GPU Node Group A: p3.2xlarge             |
|  GPU Node Group B: g5.xlarge              |
|  GPU Node Group C: g5.2xlarge             |
|  GPU Node Group D: p4d.24xlarge           |
|  ...                                      |
|  각 Node Group을 개별 관리 (비효율적)     |
+------------------------------------------+

+------------------------------------------+
|         Karpenter 방식                    |
|                                           |
|  단일 GPU NodePool:                       |
|  - Pod 요구사항 분석                      |
|  - 최적 GPU 인스턴스 자동 선택            |
|  - Spot/On-Demand 자동 전환               |
|  - 비용 기반 인스턴스 최적화              |
+------------------------------------------+

2. GPU NodePool 설정

범용 GPU NodePool

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-general
spec:
  template:
    metadata:
      labels:
        node-type: gpu
        workload: ai-ml
    spec:
      requirements:
        # GPU 인스턴스만 선택
        - key: karpenter.k8s.aws/instance-gpu-count
          operator: Gt
          values: ['0']

        # GPU 인스턴스 패밀리
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ['g', 'p']

        # 용량 타입
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['on-demand', 'spot']

        # 가용 영역
        - key: topology.kubernetes.io/zone
          operator: In
          values: ['us-east-1a', 'us-east-1b', 'us-east-1c']

        # x86 아키텍처만
        - key: kubernetes.io/arch
          operator: In
          values: ['amd64']

      # GPU 전용 taint
      taints:
        - key: nvidia.com/gpu
          value: 'true'
          effect: NoSchedule

      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu-optimized

      # GPU 노드는 더 긴 만료 시간
      expireAfter: 336h # 14일

  limits:
    cpu: '500'
    memory: 2000Gi
    nvidia.com/gpu: '100'

  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 5m
    budgets:
      - nodes: '1'

  weight: 80

AWS GPU 인스턴스 타입 가이드

+------------------+----------+----------+------------------+-------------------+
| 인스턴스 타입    | GPU      | GPU 수   | GPU 메모리       | 주요 용도         |
+------------------+----------+----------+------------------+-------------------+
| g4dn.xlarge      | T4       | 1        | 16 GB            | 추론, 경량 학습   |
| g4dn.12xlarge    | T4       | 4        | 64 GB            | 다중 추론         |
| g5.xlarge        | A10G     | 1        | 24 GB            | 추론, 미세 조정   |
| g5.12xlarge      | A10G     | 4        | 96 GB            | 중형 학습         |
| g5.48xlarge      | A10G     | 8        | 192 GB           | 대형 학습         |
| g6.xlarge        | L4       | 1        | 24 GB            | 추론 최적화       |
| g6.12xlarge      | L4       | 4        | 96 GB            | 멀티모달 추론     |
| p3.2xlarge       | V100     | 1        | 16 GB            | 범용 학습         |
| p3.8xlarge       | V100     | 4        | 64 GB            | 대규모 학습       |
| p4d.24xlarge     | A100     | 8        | 320 GB (40GB x8) | 초대규모 학습     |
| p5.48xlarge      | H100     | 8        | 640 GB (80GB x8) | 최대 성능 학습    |
+------------------+----------+----------+------------------+-------------------+

추론 전용 NodePool

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-inference
spec:
  template:
    metadata:
      labels:
        node-type: gpu-inference
        workload: inference
    spec:
      requirements:
        # 추론에 적합한 인스턴스
        - key: karpenter.k8s.aws/instance-gpu-name
          operator: In
          values: ['t4', 'a10g', 'l4']

        # Spot 인스턴스 우선 (추론은 stateless)
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['spot', 'on-demand']

        # 인스턴스 크기 제한
        - key: karpenter.k8s.aws/instance-size
          operator: In
          values: ['xlarge', '2xlarge', '4xlarge']

      taints:
        - key: nvidia.com/gpu
          value: 'true'
          effect: NoSchedule

      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu-optimized

  limits:
    nvidia.com/gpu: '50'

  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 2m

  weight: 60

학습 전용 NodePool

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-training
spec:
  template:
    metadata:
      labels:
        node-type: gpu-training
        workload: training
    spec:
      requirements:
        # 학습에 적합한 고성능 GPU
        - key: karpenter.k8s.aws/instance-gpu-name
          operator: In
          values: ['a100', 'h100', 'a10g']

        # On-Demand 전용 (학습은 중단 비용이 큼)
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['on-demand']

        # 대형 인스턴스
        - key: karpenter.k8s.aws/instance-gpu-count
          operator: Gt
          values: ['0']

      taints:
        - key: nvidia.com/gpu
          value: 'true'
          effect: NoSchedule

      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu-training

      # 학습용 노드는 만료 없음
      expireAfter: 720h # 30일

  limits:
    nvidia.com/gpu: '32'

  disruption:
    # 학습 중 통합 비활성화
    consolidationPolicy: WhenEmpty
    consolidateAfter: 30m
    budgets:
      - nodes: '0'

  weight: 90

3. GPU 전용 EC2NodeClass

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: gpu-optimized
spec:
  # GPU 드라이버가 포함된 AMI
  amiSelectorTerms:
    - alias: al2023@latest

  role: KarpenterNodeRole-my-cluster

  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: my-cluster
        network-type: private

  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: my-cluster

  # GPU 워크로드용 대용량 디스크
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 200Gi
        volumeType: gp3
        iops: 6000
        throughput: 250
        encrypted: true
        deleteOnTermination: true

  metadataOptions:
    httpEndpoint: enabled
    httpPutResponseHopLimit: 2
    httpTokens: required

  tags:
    Environment: production
    NodeType: gpu
    ManagedBy: karpenter

  # GPU 드라이버 설치를 위한 사용자 데이터
  userData: |
    #!/bin/bash
    echo "GPU node bootstrap"
    # NVIDIA 드라이버는 GPU Operator가 처리

학습 전용 EC2NodeClass (대용량 스토리지)

apiVersion: karpenter.k8s.aws/v1
kind: EC2NodeClass
metadata:
  name: gpu-training
spec:
  amiSelectorTerms:
    - alias: al2023@latest

  role: KarpenterNodeRole-my-cluster

  subnetSelectorTerms:
    - tags:
        karpenter.sh/discovery: my-cluster

  securityGroupSelectorTerms:
    - tags:
        karpenter.sh/discovery: my-cluster

  # 학습 데이터용 대용량 + 고성능 스토리지
  blockDeviceMappings:
    - deviceName: /dev/xvda
      ebs:
        volumeSize: 500Gi
        volumeType: gp3
        iops: 16000
        throughput: 1000
        encrypted: true
        deleteOnTermination: true

  tags:
    Environment: production
    NodeType: gpu-training
    ManagedBy: karpenter

4. Spot GPU 인스턴스 전략

Spot GPU의 비용 절감 효과

+------------------+-------------------+-------------------+---------+
| 인스턴스 타입    | On-Demand (시간)  | Spot 예상 (시간)  | 절감율  |
+------------------+-------------------+-------------------+---------+
| g4dn.xlarge      | ~0.526            | ~0.158            | ~70%    |
| g5.xlarge        | ~1.006            | ~0.302            | ~70%    |
| g5.2xlarge       | ~1.212            | ~0.364            | ~70%    |
| g5.12xlarge      | ~5.672            | ~1.702            | ~70%    |
| p3.2xlarge       | ~3.060            | ~0.918            | ~70%    |
+------------------+-------------------+-------------------+---------+
 (가격은 리전과 시기에 따라 변동됩니다)

Spot GPU NodePool - 추론용

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-spot-inference
spec:
  template:
    metadata:
      labels:
        node-type: gpu-spot
        workload: inference
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['spot']

        # 추론에 적합한 다양한 GPU 타입
        - key: karpenter.k8s.aws/instance-gpu-name
          operator: In
          values: ['t4', 'a10g', 'l4']

        # 다양한 크기로 Spot 가용성 확보
        - key: karpenter.k8s.aws/instance-size
          operator: In
          values: ['xlarge', '2xlarge', '4xlarge', '8xlarge', '12xlarge']

        # 여러 AZ 활용
        - key: topology.kubernetes.io/zone
          operator: In
          values: ['us-east-1a', 'us-east-1b', 'us-east-1c']

      taints:
        - key: nvidia.com/gpu
          value: 'true'
          effect: NoSchedule

      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu-optimized

  limits:
    nvidia.com/gpu: '40'

  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m

  weight: 70

Spot 중단 대비 전략

# Pod에 do-not-disrupt 어노테이션 적용 (장시간 학습 작업)
apiVersion: v1
kind: Pod
metadata:
  name: training-job
  annotations:
    karpenter.sh/do-not-disrupt: 'true'
spec:
  containers:
    - name: training
      image: my-training-image:latest
      resources:
        requests:
          nvidia.com/gpu: '1'
          cpu: '4'
          memory: 16Gi
        limits:
          nvidia.com/gpu: '1'
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists
      effect: NoSchedule
  terminationGracePeriodSeconds: 120

5. NVIDIA GPU Operator 연동

GPU Operator 개요

+----------------------------------------------------------------+
|                    NVIDIA GPU Operator                          |
|                                                                |
|  +------------------+  +-------------------+  +--------------+ |
|  | NVIDIA Driver    |  | Container Toolkit |  | Device Plugin| |
|  | (자동 설치)      |  | (자동 설정)       |  | (자동 배포)  | |
|  +------------------+  +-------------------+  +--------------+ |
|                                                                |
|  +------------------+  +-------------------+  +--------------+ |
|  | GPU Feature      |  | DCGM Exporter    |  | MIG Manager  | |
|  | Discovery        |  | (메트릭 수집)     |  | (MIG 관리)   | |
|  +------------------+  +-------------------+  +--------------+ |
+----------------------------------------------------------------+

GPU Operator 설치

# NVIDIA GPU Operator Helm 리포지토리 추가
helm repo add nvidia https://helm.ngc.nvidia.com/nvidia
helm repo update

# GPU Operator 설치
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 \
  --set migManager.enabled=false \
  --set gfd.enabled=true

GPU Operator와 Karpenter 연동 확인

# GPU 노드의 레이블 확인
kubectl get nodes -l node-type=gpu -o json | \
  jq '.items[].metadata.labels | with_entries(select(.key | startswith("nvidia")))'

# GPU 리소스 확인
kubectl describe node gpu-node-name | grep -A 5 "nvidia.com/gpu"

# DCGM Exporter Pod 확인
kubectl get pods -n gpu-operator -l app=nvidia-dcgm-exporter

GPU 워크로드 배포 예제

apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpu-inference-server
  namespace: ml-serving
spec:
  replicas: 3
  selector:
    matchLabels:
      app: inference-server
  template:
    metadata:
      labels:
        app: inference-server
    spec:
      containers:
        - name: inference
          image: nvcr.io/nvidia/tritonserver:24.01-py3
          ports:
            - containerPort: 8000
              name: http
            - containerPort: 8001
              name: grpc
            - containerPort: 8002
              name: metrics
          resources:
            requests:
              cpu: '4'
              memory: 16Gi
              nvidia.com/gpu: '1'
            limits:
              nvidia.com/gpu: '1'
          volumeMounts:
            - name: model-store
              mountPath: /models
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      nodeSelector:
        node-type: gpu-inference
      volumes:
        - name: model-store
          persistentVolumeClaim:
            claimName: model-store-pvc

6. 멀티 아키텍처 지원 (x86 + ARM/Graviton)

멀티 아키텍처 NodePool

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: multi-arch
spec:
  template:
    spec:
      requirements:
        # x86과 ARM 모두 허용
        - key: kubernetes.io/arch
          operator: In
          values: ['amd64', 'arm64']

        # Graviton 인스턴스 포함
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ['c', 'm', 'r']

        - key: karpenter.k8s.aws/instance-generation
          operator: Gt
          values: ['5']

        - key: karpenter.sh/capacity-type
          operator: In
          values: ['on-demand', 'spot']

      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: default

  limits:
    cpu: '1000'
    memory: 2000Gi

  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 1m

Graviton GPU 대안: Inferentia/Trainium

# AWS Inferentia 추론 전용 NodePool
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: inferentia
spec:
  template:
    metadata:
      labels:
        accelerator: inferentia
    spec:
      requirements:
        - key: node.kubernetes.io/instance-type
          operator: In
          values: ['inf2.xlarge', 'inf2.8xlarge', 'inf2.24xlarge', 'inf2.48xlarge']

        - key: karpenter.sh/capacity-type
          operator: In
          values: ['on-demand']

      taints:
        - key: aws.amazon.com/neuron
          value: 'true'
          effect: NoSchedule

      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: inferentia-nodes

  limits:
    aws.amazon.com/neuron: '32'

  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 5m

7. 비용 최적화 전략

Spot과 On-Demand 혼합 전략

+-------------------------------------------------------------+
|              비용 최적화 의사결정 트리                        |
+-------------------------------------------------------------+
|                                                             |
|  워크로드 유형 확인                                         |
|      |                                                      |
|      +-- 추론 (Stateless) --> Spot 우선 + On-Demand 대체    |
|      |                                                      |
|      +-- 미세 조정 (단기) --> Spot + 체크포인트 전략        |
|      |                                                      |
|      +-- 대규모 학습 (장기) --> On-Demand + 예약 인스턴스   |
|      |                                                      |
|      +-- 배치 처리 --> Spot 전용                            |
|                                                             |
+-------------------------------------------------------------+

가중 우선순위를 사용한 인스턴스 패밀리 전략

# 1순위: G5 Spot (가장 비용 효율적인 추론)
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-tier1-g5-spot
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['spot']
        - key: karpenter.k8s.aws/instance-gpu-name
          operator: In
          values: ['a10g']
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ['g']
      taints:
        - key: nvidia.com/gpu
          value: 'true'
          effect: NoSchedule
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu-optimized
  weight: 100
  limits:
    nvidia.com/gpu: '20'
---
# 2순위: G4dn Spot (대체)
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-tier2-g4dn-spot
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['spot']
        - key: karpenter.k8s.aws/instance-gpu-name
          operator: In
          values: ['t4']
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ['g']
      taints:
        - key: nvidia.com/gpu
          value: 'true'
          effect: NoSchedule
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu-optimized
  weight: 50
  limits:
    nvidia.com/gpu: '20'
---
# 3순위: G5 On-Demand (최후의 대체)
apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-tier3-g5-ondemand
spec:
  template:
    spec:
      requirements:
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['on-demand']
        - key: karpenter.k8s.aws/instance-gpu-name
          operator: In
          values: ['a10g']
        - key: karpenter.k8s.aws/instance-category
          operator: In
          values: ['g']
      taints:
        - key: nvidia.com/gpu
          value: 'true'
          effect: NoSchedule
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu-optimized
  weight: 10
  limits:
    nvidia.com/gpu: '10'

Consolidation 정책 최적화

# GPU 노드의 Consolidation 설정
disruption:
  # GPU 노드는 WhenEmpty만 사용 (실행 중인 GPU 작업 보호)
  consolidationPolicy: WhenEmpty
  # 빈 노드 감지 후 5분 대기 (일시적 비활성 고려)
  consolidateAfter: 5m
  budgets:
    # 동시에 최대 1개 노드만 중단
    - nodes: '1'
    # 업무 시간에는 중단 차단
    - nodes: '0'
      schedule: '0 9 * * MON-FRI'
      duration: 10h

8. 노드 중단 예산 (Node Disruption Budgets)

GPU 워크로드를 위한 Disruption Budget

apiVersion: karpenter.sh/v1
kind: NodePool
metadata:
  name: gpu-training-protected
spec:
  template:
    spec:
      requirements:
        - key: karpenter.k8s.aws/instance-gpu-count
          operator: Gt
          values: ['0']
        - key: karpenter.sh/capacity-type
          operator: In
          values: ['on-demand']
      taints:
        - key: nvidia.com/gpu
          value: 'true'
          effect: NoSchedule
      nodeClassRef:
        group: karpenter.k8s.aws
        kind: EC2NodeClass
        name: gpu-training

  disruption:
    consolidationPolicy: WhenEmpty
    consolidateAfter: 30m
    budgets:
      # 학습 시간에는 중단 완전 차단
      - nodes: '0'
        schedule: '0 0 * * *'
        duration: 23h

      # 유지보수 창 (매일 1시간)
      - nodes: '1'
        schedule: '0 23 * * *'
        duration: 1h

      # 드리프트로 인한 중단은 별도 관리
      - nodes: '1'
        reasons:
          - 'Drifted'

Pod 수준 보호

# 장시간 학습 Pod: Karpenter 중단 방지
apiVersion: v1
kind: Pod
metadata:
  name: long-training-job
  annotations:
    # 이 어노테이션으로 Karpenter의 자발적 중단을 방지
    karpenter.sh/do-not-disrupt: 'true'
spec:
  containers:
    - name: trainer
      image: my-training-image:v1
      resources:
        requests:
          nvidia.com/gpu: '4'
          cpu: '16'
          memory: 64Gi
        limits:
          nvidia.com/gpu: '4'
  tolerations:
    - key: nvidia.com/gpu
      operator: Exists
      effect: NoSchedule
  # 충분한 종료 유예 시간 (체크포인트 저장)
  terminationGracePeriodSeconds: 300

PDB (Pod Disruption Budget) 설정

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: inference-server-pdb
  namespace: ml-serving
spec:
  minAvailable: 2
  selector:
    matchLabels:
      app: inference-server

9. Prometheus/Grafana를 이용한 모니터링

Karpenter 메트릭 수집 설정

# Karpenter ServiceMonitor
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: karpenter
  namespace: karpenter
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: karpenter
  endpoints:
    - port: http-metrics
      interval: 15s
      path: /metrics

주요 Karpenter 메트릭

+-----------------------------------------------+----------------------------------------+
| 메트릭                                        | 설명                                   |
+-----------------------------------------------+----------------------------------------+
| karpenter_nodeclaims_launched_total           | 총 시작된 NodeClaim 수                 |
| karpenter_nodeclaims_registered_total         | 총 등록된 NodeClaim 수                 |
| karpenter_nodeclaims_terminated_total         | 총 종료된 NodeClaim 수                 |
| karpenter_pods_state                          | Pod 상태 (노드, 네임스페이스 등)       |
| karpenter_nodepool_usage                      | NodePool별 리소스 사용량               |
| karpenter_nodepool_limit                      | NodePool별 리소스 한도                 |
| karpenter_voluntary_disruption_eligible_nodes | 자발적 중단 대상 노드 수              |
| karpenter_disruption_actions_performed_total  | 수행된 중단 작업 수                    |
| karpenter_nodes_allocatable                   | 노드별 할당 가능 리소스               |
| karpenter_nodes_total_daemon_requests         | 데몬셋 리소스 요청 총합               |
+-----------------------------------------------+----------------------------------------+

GPU 전용 Grafana 대시보드 쿼리

# GPU 노드 수 추적
count(karpenter_nodes_allocatable{resource_type="nvidia.com/gpu"} > 0)

# GPU 활용률 (DCGM Exporter 필요)
DCGM_FI_DEV_GPU_UTIL

# NodePool별 GPU 사용량 vs 한도
karpenter_nodepool_usage{resource_type="nvidia.com/gpu"}
  /
karpenter_nodepool_limit{resource_type="nvidia.com/gpu"}

# 프로비저닝 지연 시간
histogram_quantile(0.99,
  rate(karpenter_provisioner_scheduling_duration_seconds_bucket[5m])
)

DCGM Exporter 메트릭

# DCGM Exporter ServiceMonitor
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
  name: dcgm-exporter
  namespace: gpu-operator
spec:
  selector:
    matchLabels:
      app: nvidia-dcgm-exporter
  endpoints:
    - port: metrics
      interval: 15s

알림 규칙 예제

apiVersion: monitoring.coreos.com/v1
kind: PrometheusRule
metadata:
  name: karpenter-gpu-alerts
  namespace: monitoring
spec:
  groups:
    - name: karpenter-gpu
      rules:
        # GPU NodePool이 한도의 90%에 도달
        - alert: GPUNodePoolNearLimit
          expr: |
            karpenter_nodepool_usage{nodepool="gpu-general", resource_type="nvidia.com/gpu"}
            /
            karpenter_nodepool_limit{nodepool="gpu-general", resource_type="nvidia.com/gpu"}
            > 0.9
          for: 5m
          labels:
            severity: warning
          annotations:
            summary: 'GPU NodePool approaching resource limit'

        # GPU 활용률이 낮은 노드 감지
        - alert: LowGPUUtilization
          expr: |
            avg_over_time(DCGM_FI_DEV_GPU_UTIL[30m]) < 10
          for: 1h
          labels:
            severity: info
          annotations:
            summary: 'GPU utilization below 10 percent for 1 hour'

        # Karpenter 프로비저닝 실패
        - alert: KarpenterProvisioningFailed
          expr: |
            increase(karpenter_nodeclaims_terminated_total{reason="ProvisioningFailed"}[15m]) > 0
          labels:
            severity: critical
          annotations:
            summary: 'Karpenter failed to provision GPU node'

10. 실전 예제: 학습 클러스터

분산 학습 클러스터 구성

# PyTorch 분산 학습 Job
apiVersion: batch/v1
kind: Job
metadata:
  name: distributed-training
  namespace: ml-training
spec:
  parallelism: 4
  completions: 4
  template:
    metadata:
      labels:
        app: distributed-training
      annotations:
        karpenter.sh/do-not-disrupt: 'true'
    spec:
      containers:
        - name: pytorch-trainer
          image: my-pytorch-training:v1
          command: ['torchrun']
          args:
            - '--nproc_per_node=1'
            - '--nnodes=4'
            - '--node_rank=$(JOB_COMPLETION_INDEX)'
            - '--master_addr=training-master'
            - '--master_port=29500'
            - 'train.py'
          env:
            - name: JOB_COMPLETION_INDEX
              valueFrom:
                fieldRef:
                  fieldPath: metadata.annotations['batch.kubernetes.io/job-completion-index']
          resources:
            requests:
              cpu: '8'
              memory: 32Gi
              nvidia.com/gpu: '1'
            limits:
              nvidia.com/gpu: '1'
          volumeMounts:
            - name: shared-data
              mountPath: /data
            - name: checkpoints
              mountPath: /checkpoints
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      nodeSelector:
        node-type: gpu-training
      restartPolicy: OnFailure
      volumes:
        - name: shared-data
          persistentVolumeClaim:
            claimName: training-data-pvc
        - name: checkpoints
          persistentVolumeClaim:
            claimName: checkpoint-pvc

11. 실전 예제: 추론 클러스터

오토스케일링 추론 서비스

# 추론 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference
  namespace: ml-serving
spec:
  replicas: 2
  selector:
    matchLabels:
      app: llm-inference
  template:
    metadata:
      labels:
        app: llm-inference
    spec:
      containers:
        - name: vllm-server
          image: vllm/vllm-openai:latest
          args:
            - '--model'
            - 'meta-llama/Llama-3-8B'
            - '--tensor-parallel-size'
            - '1'
            - '--gpu-memory-utilization'
            - '0.9'
          ports:
            - containerPort: 8000
              name: http
          resources:
            requests:
              cpu: '4'
              memory: 16Gi
              nvidia.com/gpu: '1'
            limits:
              nvidia.com/gpu: '1'
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 60
            periodSeconds: 10
      tolerations:
        - key: nvidia.com/gpu
          operator: Exists
          effect: NoSchedule
      nodeSelector:
        node-type: gpu-inference
---
# HPA 설정
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-inference-hpa
  namespace: ml-serving
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-inference
  minReplicas: 2
  maxReplicas: 10
  metrics:
    - type: Pods
      pods:
        metric:
          name: gpu_utilization
        target:
          type: AverageValue
          averageValue: '70'
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
        - type: Pods
          value: 2
          periodSeconds: 60
    scaleDown:
      stabilizationWindowSeconds: 300
      policies:
        - type: Pods
          value: 1
          periodSeconds: 120

12. 트러블슈팅 가이드

일반적인 GPU 노드 문제

# 1. GPU 리소스가 표시되지 않는 경우
kubectl describe node gpu-node | grep -A 10 "Allocatable"
# nvidia.com/gpu가 없으면 GPU Operator 확인

# 2. GPU Operator Pod 상태 확인
kubectl get pods -n gpu-operator
kubectl logs -n gpu-operator -l app=nvidia-driver-daemonset

# 3. Karpenter 프로비저닝 로그 확인
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter \
  | grep -i "gpu\|nvidia\|instance-type"

# 4. NodeClaim 상태 확인
kubectl get nodeclaims -o wide

# 5. Pending Pod 원인 분석
kubectl describe pod gpu-pod-name | grep -A 20 "Events"

자주 발생하는 문제와 해결

+---------------------------------------------+------------------------------------------+
| 문제                                        | 해결 방법                                |
+---------------------------------------------+------------------------------------------+
| GPU 리소스가 노드에 표시되지 않음           | GPU Operator 재설치 또는 드라이버 확인   |
| Spot GPU 인스턴스를 찾을 수 없음            | 더 많은 GPU 인스턴스 타입과 AZ 추가      |
| GPU 노드 프로비저닝 시간 초과               | EC2NodeClass 서브넷/보안그룹 태그 확인   |
| 학습 중 노드가 중단됨                       | do-not-disrupt 어노테이션 추가           |
| GPU 메모리 부족 (OOM)                       | 더 큰 GPU 인스턴스 타입 허용             |
| 불필요한 GPU 노드가 유지됨                  | Consolidation 정책 및 consolidateAfter   |
|                                             | 값 확인                                  |
| 특정 GPU 타입만 프로비저닝됨                | NodePool requirements 범위 확장          |
+---------------------------------------------+------------------------------------------+

GPU 메모리 확인 명령

# 노드에서 직접 GPU 상태 확인 (디버그 Pod 사용)
kubectl run gpu-debug --rm -it \
  --image=nvidia/cuda:12.0.0-base-ubuntu22.04 \
  --overrides='{"spec":{"tolerations":[{"key":"nvidia.com/gpu","operator":"Exists","effect":"NoSchedule"}],"nodeSelector":{"node-type":"gpu"}}}' \
  --restart=Never \
  -- nvidia-smi

13. 베스트 프랙티스 요약

GPU 노드 관리 체크리스트

+---+------------------------------------------------------------+
| # | 베스트 프랙티스                                            |
+---+------------------------------------------------------------+
| 1 | 추론과 학습 워크로드의 NodePool을 분리                     |
| 2 | GPU taint를 설정하여 비 GPU 워크로드 스케줄링 방지        |
| 3 | 추론은 Spot, 학습은 On-Demand 사용                         |
| 4 | 장시간 학습에 do-not-disrupt 어노테이션 적용               |
| 5 | 체크포인트 전략으로 학습 진행 상황 보호                     |
| 6 | GPU Operator로 드라이버 관리 자동화                        |
| 7 | DCGM Exporter로 GPU 메트릭 수집                           |
| 8 | NodePool limits로 GPU 비용 상한 설정                       |
| 9 | 여러 GPU 인스턴스 타입을 허용하여 가용성 확보              |
| 10| PDB로 추론 서비스의 최소 가용성 보장                       |
| 11| Disruption Budget으로 학습 시간 중 중단 차단               |
| 12| HPA와 Karpenter를 연동하여 자동 스케일링 구현             |
+---+------------------------------------------------------------+

비용 최적화 전략 요약

전략 1: 계층형 NodePool
  - Spot GPU (높은 가중치) -> On-Demand GPU (낮은 가중치)
  - 추론 워크로드에 최적

전략 2: 인스턴스 다각화
  - 여러 GPU 패밀리 (g4dn, g5, g6) 허용
  - 여러 인스턴스 크기 허용
  - Spot 가용성 극대화

전략 3: 자동 축소
  - WhenEmpty consolidation으로 빈 GPU 노드 즉시 제거
  - consolidateAfter를 짧게 설정 (추론)
  - 학습 노드는 더 긴 대기 시간 설정

전략 4: 적절한 리소스 한도
  - NodePool limits로 최대 GPU 수 제한
  - 예상치 못한 비용 폭주 방지
  - 팀/프로젝트별 할당량 관리

Karpenter + GPU 아키텍처 최종 다이어그램

+---------------------------------------------------------------------+
|                        EKS Cluster                                  |
|                                                                     |
|  +-------------------+  +-------------------+  +-----------------+  |
|  | NodePool:         |  | NodePool:         |  | NodePool:       |  |
|  | gpu-inference     |  | gpu-training      |  | multi-arch      |  |
|  | (Spot, weight:60) |  | (OD, weight:90)   |  | (Mixed, w:50)   |  |
|  +--------+----------+  +--------+----------+  +--------+--------+  |
|           |                      |                       |           |
|  +--------v----------+  +--------v----------+  +--------v--------+  |
|  | EC2NodeClass:     |  | EC2NodeClass:     |  | EC2NodeClass:   |  |
|  | gpu-optimized     |  | gpu-training      |  | default         |  |
|  | (200GB, gp3)      |  | (500GB, gp3)      |  | (100GB, gp3)    |  |
|  +-------------------+  +-------------------+  +-----------------+  |
|                                                                     |
|  +-------------------+  +-------------------+                       |
|  | GPU Operator      |  | Prometheus +      |                       |
|  | (NVIDIA Driver,   |  | Grafana           |                       |
|  |  Device Plugin,   |  | (Karpenter +      |                       |
|  |  DCGM Exporter)   |  |  DCGM Metrics)    |                       |
|  +-------------------+  +-------------------+                       |
+---------------------------------------------------------------------+

14. GPU Pod의 요청이 인스턴스가 되기까지

위의 NodePool들은 전부 requirements 목록으로 되어 있지만, 그 목록이 실제로 무엇을 하는지는 설명하지 않았습니다. Karpenter는 미리 만들어 둔 노드 그룹 중에서 고르는 도구가 아닙니다. 스케줄되지 못한 Pod을 보고, 그 Pod의 리소스 요청과 nodeSelector, affinity, toleration을 NodePool의 requirements와 교집합으로 묶은 다음, EC2 인스턴스 타입 카탈로그 전체에서 그 교집합을 만족하는 후보를 계산합니다. requirements에 쓰는 well-known 레이블은 그 계산에 쓰이는 어휘입니다.

karpenter.k8s.aws/instance-gpu-count          # GPU 개수
karpenter.k8s.aws/instance-gpu-name           # 예: t4
karpenter.k8s.aws/instance-gpu-manufacturer   # 제조사
karpenter.k8s.aws/instance-gpu-memory         # 메비바이트(MiB) 단위
karpenter.k8s.aws/instance-category           # g, p, c, m, r ...
karpenter.k8s.aws/instance-family
karpenter.k8s.aws/instance-generation
karpenter.k8s.aws/instance-size
karpenter.k8s.aws/instance-cpu
karpenter.k8s.aws/instance-memory             # 메비바이트(MiB) 단위
karpenter.k8s.aws/instance-local-nvme         # 기비바이트(GiB) 단위
karpenter.k8s.aws/instance-hypervisor
karpenter.k8s.aws/instance-encryption-in-transit-supported
karpenter.sh/capacity-type                    # spot, on-demand, reserved

여기서 한 번쯤 발을 헛디디는 지점이 단위입니다. instance-gpu-memory는 메비바이트이고 instance-memory도 마찬가지입니다. VRAM 24GB짜리를 고르겠다고 24000 같은 값을 적으면 의도한 것과 다른 집합이 나옵니다. 그리고 capacity-type에는 spot과 on-demand 외에 reserved가 있습니다. 용량 예약을 이미 사 둔 조직이라면 이 값이 존재한다는 사실 자체가 설계에 영향을 줍니다.

비교를 쓸 수 있다는 점도 중요합니다. 문서는 지원 연산자로 In, NotIn, Exists, DoesNotExist, Gt, Lt, Gte, Lte 여덟 개를 명시합니다. 위 2절에서 GPU 인스턴스만 고르려고 쓴 instance-gpu-countGt'0'을 붙인 표현이 바로 이것입니다. GPU가 하나라도 있는 모든 인스턴스 타입이라는 뜻이고, 인스턴스 타입 이름을 나열하지 않았기 때문에 AWS가 새 GPU 패밀리를 내놓아도 NodePool을 고치지 않아도 됩니다.

반대로 프로비저닝이 실패하는 원인 1위는 requirements를 너무 좁게 잡는 것입니다. GPU 이름을 세 개로 못 박고, 인스턴스 크기를 세 개로 못 박고, AZ를 세 개로 못 박고, 거기에 Spot까지 요구하면 남는 후보 조합은 손에 꼽습니다. 그 조합에 Spot 용량이 없는 순간 Pod은 그대로 Pending이 되고 로그에는 "no instance type met the scheduling requirements or had a required offering"이 남습니다. 문서는 이 문자열의 뒷부분, 즉 required offering이 특정 가용 영역에서의 인스턴스 가용성을 가리킨다고 설명합니다. EBS 볼륨이 특정 AZ에 있는 스테이트풀 워크로드에서 특히 자주 나옵니다.

# 너무 좁은 조합 — 후보가 거의 남지 않는다
requirements:
  - key: karpenter.k8s.aws/instance-gpu-name
    operator: In
    values: ['a10g']
  - key: karpenter.k8s.aws/instance-size
    operator: In
    values: ['xlarge']
  - key: karpenter.sh/capacity-type
    operator: In
    values: ['spot']
  - key: topology.kubernetes.io/zone
    operator: In
    values: ['us-east-1a']

# 성질을 기술하는 방식 — 후보 집합이 넓게 유지된다
requirements:
  - key: karpenter.k8s.aws/instance-gpu-count
    operator: Gt
    values: ['0']
  - key: karpenter.k8s.aws/instance-gpu-memory
    operator: Gt
    values: ['16000']
  - key: karpenter.sh/capacity-type
    operator: In
    values: ['spot', 'on-demand']

limits와 weight도 겉보기보다 미묘합니다. 문서는 한도를 넘으면 일부 노드가 종료될 때까지 프로비저닝이 막힌다고 말하면서, 동시에 "limit checking is eventually consistent, which can result in overrun during rapid scale outs"라고 덧붙입니다. 즉 GPU 32개로 상한을 걸어 두어도 대규모 스케일 아웃 순간에는 잠깐 그 위로 넘어갈 수 있습니다. limits는 비용 사고를 막는 하드 스톱이 아니라 폭주를 잡는 브레이크로 이해해야 하고, 청구서 방어는 별도의 예산 알림이 담당해야 합니다. weight는 NodePool이 여러 개일 때 우선순위를 정하는 값이며, 문서는 "Specifying no weight is equivalent to specifying a weight of 0"이라고 못 박습니다. 위 7절의 계층형 구성에서 weight를 적지 않은 NodePool이 하나라도 섞여 있으면 그것이 가장 마지막 후보가 된다는 뜻입니다.

15. 이 글의 disruption 설정이 실제로 하는 일

앞의 YAML들은 consolidationPolicyconsolidateAfter, budgets를 값만 다르게 반복해서 썼습니다. 각 값이 무엇을 의미하는지는 짚고 넘어가야 합니다.

consolidateAfter는 노드가 후보가 되기 전까지 기다리는 시간인데, 문서는 Pod이 노드에 추가되거나 제거될 때마다 이 타이머가 리셋된다고 명시합니다. 노드는 consolidateAfter 기간 전체 동안 안정적이었을 때만 통합 후보가 됩니다. 추론 노드에 1분을 걸어 두었는데 Pod이 계속 들락거리면 그 노드는 영영 통합되지 않습니다. 반대로 학습 노드에 30분을 건 것은 그 시간 동안 아무 변화가 없어야 손을 댄다는 뜻이므로 의도대로 보수적으로 동작합니다. disruption 블록을 아예 쓰지 않으면 기본값이 적용되는데, 그 기본값은 다음과 같습니다.

# disruption을 명시하지 않았을 때의 기본값
spec:
  disruption:
    consolidationPolicy: WhenEmptyOrUnderutilized
    consolidateAfter: 0s

GPU NodePool에서 이 기본값을 그대로 두는 것은 위험합니다. 비어 있거나 저활용 상태로 보이면 대기 시간 없이 통합 대상이 되기 때문입니다. GPU를 잡고 있지만 CPU와 메모리 요청이 작은 Pod은 Karpenter 입장에서 저활용으로 보이기 쉽습니다.

budgets에는 함정이 하나 있습니다. schedule은 cron 표기이고 문서상 UTC로만 해석됩니다. 위 7절의 '0 9 * * MON-FRI' + duration: 10h는 "업무 시간에는 중단 차단"이라는 주석과 함께 쓰여 있지만, UTC 09시부터 10시간은 한국 시간으로 18시부터 다음 날 새벽 4시입니다. 정확히 반대로 걸린 것입니다. 한국 업무 시간인 09시부터 19시를 막으려면 UTC 00시에 시작해야 합니다. 8절의 '0 23 * * *' + duration: 1h도 마찬가지로 한국 시간 오전 8시부터 9시까지의 유지보수 창이 됩니다. 시차가 있는 지역에서 운영한다면 이 한 줄이 학습 작업을 새벽에 끊어 먹는 원인이 됩니다.

reasons에 쓸 수 있는 값은 Drifted, Underutilized, Empty 셋뿐입니다. 8절의 마지막 budget이 Drifted만 지정한 것은 드리프트로 인한 중단만 별도 한도로 관리하겠다는 뜻이고, 나머지 사유는 위쪽 budget들의 적용을 받습니다. budget을 아예 정의하지 않으면 기본값은 10% 하나입니다.

# budgets의 schedule은 UTC로만 해석된다 (KST = UTC+9)
disruption:
  budgets:
    # 한국 시간 09:00~19:00 동안 중단 차단
    - nodes: '0'
      schedule: '0 0 * * MON-FRI'
      duration: 10h
    # 드리프트로 인한 중단만 별도 한도
    - nodes: '1'
      reasons:
        - 'Drifted'

karpenter.sh/do-not-disrupt도 정확히 이해해야 합니다. 값으로 "true" 또는 Go duration 문자열을 받고, Node에도 Pod에도 붙일 수 있습니다. 문서는 이 어노테이션이 붙은 Pod이 활성 상태인 노드는 Consolidation에서 제외되고 Drift에서는 조건부로 제외된다고 설명합니다. 그런데 결정적인 제한이 있습니다. 이 어노테이션은 강제적(forceful) 중단을 막지 못합니다. 만료와 인터럽션이 그 강제적 방법에 해당합니다. 위 4절과 8절에서 장시간 학습 Pod에 이 어노테이션을 붙였지만, 그것만으로 학습이 안전해지지는 않습니다.

만료가 여기에 걸립니다. expireAfter의 기본값은 720h이고, 만료는 강제적 중단 방법입니다. 2절에서 학습 NodePool에 expireAfter: 720h를 적어 두고 "만료 없음"이라고 주석을 단 것은 사실이 아닙니다. 30일이 지나면 do-not-disrupt와 무관하게 노드가 사라집니다. 노드의 최대 수명은 expireAfterterminationGracePeriod의 합이며, 후자는 Pod 축출에 허용되는 최대 시간입니다. 체크포인트를 저장할 시간이 필요하다면 Pod의 terminationGracePeriodSeconds만이 아니라 NodePool 쪽 값도 함께 봐야 합니다.

드리프트는 또 다른 규칙을 씁니다. Karpenter는 NodeClaimTemplateSpec의 해시를 NodePool과 EC2NodeClass에 어노테이션으로 남기고, 그 해시가 달라지면 기존 노드를 드리프트로 판정합니다. 여기서 제외되는 필드가 있습니다. spec.weight, spec.limits, spec.disruption 아래의 값들처럼 동작만 바꾸는 필드는 드리프트 감지에서 빠집니다. weight를 조정했는데 노드가 교체되지 않는다고 당황할 필요가 없다는 뜻이고, 반대로 AMI나 subnet 선택자를 바꾸면 기존 GPU 노드가 전부 교체 대상이 됩니다.

마지막으로 Spot 인터럽션 처리는 자동으로 켜지지 않습니다. 문서는 인터럽션 처리에 --interruption-queue가 필요하다고 명시하고, 그 큐는 EventBridge가 넣어 주는 SQS 큐입니다. 이것을 설정하지 않으면 Spot 회수 통지가 Karpenter에 도달하지 않고, 노드는 예고 없이 사라진 것처럼 보입니다. Spot 인터럽션은 EC2가 인스턴스를 회수하기 2분 전에 통지되므로, 그 2분 안에 무엇을 할지가 곧 Spot GPU 전략의 전부입니다. 체크포인트 주기가 2분보다 길다면 Spot에서 학습을 돌린다는 결정은 재검토해야 합니다.

16. Pending에서 Running까지: GPU Pod 하나를 추적하기

GPU Pod이 뜨지 않을 때 가장 흔한 실수는 Karpenter 로그부터 보는 것입니다. 순서는 Pod에서 시작해서 밖으로 나가야 합니다. 각 단계에서 나오는 답이 다음에 볼 곳을 결정합니다.

# 1) Pod 이벤트 — 스케줄러가 왜 배치하지 못했는지가 여기 남는다
kubectl describe pod llm-inference-0 -n ml-serving | grep -A 20 "Events"

# 2) Karpenter가 반응해서 NodeClaim을 만들었는가
kubectl get nodeclaims

# 3) 어떤 인스턴스 타입으로 결정됐는가
kubectl get nodeclaims -o wide

# 4) NodeClaim이 없거나 계속 대기 중이면 컨트롤러 로그
kubectl logs -n karpenter -l app.kubernetes.io/name=karpenter --tail=200

1단계에서 Pod 이벤트에 스케줄 실패만 있고 Karpenter가 남긴 흔적이 없다면 문제는 Pod 쪽입니다. GPU 노드에는 taint가 걸려 있으므로 toleration이 없으면 Karpenter는 이 Pod을 위해 노드를 띄울 이유를 찾지 못합니다. 2절의 NodePool들이 전부 nvidia.com/gpu taint를 갖고 있다는 점을 기억하세요.

2단계에서 NodeClaim이 만들어졌다면 Karpenter는 이미 결정을 내린 것이고, 이제 문제는 EC2 쪽이거나 노드 초기화 쪽입니다. NodeClaim은 있는데 노드가 Ready가 되지 않거나, 노드는 Ready인데 Pod이 여전히 Pending이면 다음을 봅니다.

# 5) 노드가 기대한 리소스를 실제로 갖추었는가
NODECLAIM=$(kubectl get nodeclaims -o jsonpath='{.items[0].metadata.name}')
kubectl get nodeclaim "$NODECLAIM" \
  -o jsonpath='{.status.conditions[?(@.type=="ConsistentStateFound")]}'

# 6) device plugin이 도착하면 allocatable에 GPU가 나타난다
kubectl get node "$NODE" -o json | jq '.status.allocatable'

5단계의 ConsistentStateFound 조건은 Karpenter가 이 노드에 있을 것이라고 예상한 리소스와 실제로 등록된 리소스가 어긋났음을 알려 주는 신호입니다. 문서는 이 조건이 False가 되는 흔한 원인으로 nvidia.com/gpuvpc.amazonaws.com/pod-eni가 나타나지 않는 경우를 듭니다. GPU에서는 대부분 device plugin이 아직 안 뜬 것입니다. NVIDIA GPU Operator가 드라이버를 설치하고 컨테이너 툴킷을 설정하고 device plugin을 배포하기까지는 노드가 Ready가 된 뒤에도 수 분이 더 걸립니다. 6단계에서 allocatable에 nvidia.com/gpu가 나타나는 순간이 GPU Pod이 실제로 배치될 수 있게 되는 시점입니다. 이 시간 차를 모르면 정상 동작을 장애로 오해하게 됩니다.

17. 실패 사례와 진단 순서

# 1) 스케줄링 자체가 불가능
"no instance type met the scheduling requirements or had a required offering"
→ requirements를 넓힌다. GPU 이름/크기/AZ를 나열하는 대신 성질로 기술한다.

# 2) g6f 부분 GPU 인스턴스
EC2 DescribeInstanceTypes가 g6f의 GPU Count=0을 보고한다
→ nvidia.com/gpu 를 1 요청한 Pod은 g6f에 매칭되지 않고 Pending으로 남는다
→ NodeOverlay로 스케줄링 시뮬레이션에 GPU 용량을 주입한다

# 3) 노드는 떴는데 컨테이너가 IP를 못 받는다
"failed to assign an IP address to container"
→ maxPods가 ENI 용량을 초과. Prefix Delegation 활성화 / maxPods 축소 /
  Security Groups for Pods를 쓴다면 RESERVED_ENIS=1

# 4) VPC CNI가 낡아서 새 인스턴스 타입을 모른다
"No entry for [instance-type] in /etc/eks/eni-max-pods.txt"

2번 g6f 사례는 특히 헷갈립니다. 인스턴스 타입은 분명히 GPU를 가지고 있는데 EC2의 DescribeInstanceTypes API가 GPU 개수를 0으로 보고하기 때문에, Karpenter의 스케줄링 시뮬레이션에서는 이 패밀리가 GPU 없는 인스턴스로 보입니다. requirements를 아무리 손봐도 해결되지 않고, 문서가 제시하는 해법은 NodeOverlay로 스케줄링 시뮬레이션에 GPU 용량을 주입하는 것입니다. 14절에서 instance-gpu-countGt 0을 걸어 두었다면 g6f는 애초에 후보에서 빠져 있습니다.

3번과 4번은 GPU와 무관해 보이지만 GPU 노드에서 유독 자주 나옵니다. GPU 인스턴스는 대개 CPU와 메모리도 크고, 그래서 DaemonSet과 사이드카를 포함해 Pod 밀도를 높게 잡게 됩니다. 그 밀도가 ENI가 감당할 수 있는 IP 수를 넘으면 노드는 Ready인데 Pod만 계속 실패합니다. 4번은 클러스터를 오래 쓰다가 새 GPU 패밀리를 처음 띄울 때 나오는 전형적인 증상이고, 원인은 VPC CNI 버전입니다.

두 개의 관측 지점을 더 걸어 두면 이 문제들이 사후가 아니라 실시간으로 보입니다.

# 노드가 기대한 리소스를 갖추지 못한 상태를 세는 메트릭
operator_status_condition_count{type="ConsistentStateFound",kind="NodeClaim",status="False"}

# allocatable이 예상보다 작을 때 조정하는 전역 설정 (기본값 7.5%)
VM_MEMORY_OVERHEAD_PERCENT

첫 번째 메트릭이 0보다 크다는 것은 어떤 NodeClaim이 예상한 리소스를 확보하지 못한 채 남아 있다는 뜻입니다. GPU 클러스터에서는 이 값이 잠깐 올라갔다 내려오는 것이 정상입니다. device plugin이 도착하기 전까지의 구간이기 때문입니다. 문제는 이 값이 내려오지 않고 유지되는 경우이고, 그때는 GPU Operator 쪽을 봐야 합니다. 두 번째는 allocatable이 기대보다 작을 때 조정하는 전역 설정입니다. Karpenter는 인스턴스 타입과 AMI에 따라 달라지는 암묵적인 메모리 감소를 이 비율로 모델링하고, 문서는 기본값 7.5%가 대부분의 인스턴스에서 균형을 맞춘 값이라고 설명합니다. Karpenter는 대체로 가용 메모리를 과소평가하는 쪽으로 동작하고 첫 노드 기동 후 관측값을 캐시합니다. 이 값을 낮추면 추정이 촘촘해지지만, 과대평가하면 워크로드에 비해 작은 노드를 띄우게 됩니다.

18. 언제 Karpenter로 GPU를 다루지 않나

이미 대금을 지불한 고정 자원이 있다면 Karpenter의 강점이 사라집니다. Capacity Reservation이나 Savings Plan으로 특정 GPU 인스턴스를 확보해 둔 조직은 그 자원을 놀리지 않는 것이 최적화입니다. 동적으로 가장 싼 인스턴스를 찾아 띄우는 동작은 이 경우 오히려 예약을 비워 두게 만듭니다. capacity-typereserved가 있다는 점은 이 상황을 다룰 여지가 있다는 뜻이지만, 고정 노드 그룹을 그대로 두고 그 바깥의 버스트만 Karpenter에 맡기는 구성이 더 단순한 경우가 많습니다.

아주 긴 학습 작업도 경계 지점입니다. 수십 시간짜리 단일 작업에서 자발적 중단이 한 번이라도 허용되지 않는다면, 앞의 15절에서 본 것처럼 do-not-disrupt만으로는 부족하고 만료와 인터럽션까지 모두 봉인해야 합니다. 그 지점에 이르면 Karpenter를 쓰되 모든 자동화를 끈 상태가 되므로, 처음부터 고정 노드 그룹을 두는 편이 운영자가 이해하기 쉽습니다.

GPU AMI와 드라이버 버전을 손으로 고정해야 하는 클러스터도 마찬가지입니다. 특정 CUDA 버전에 묶인 워크로드나 인증이 필요한 환경에서는 AMI를 사람이 결정하고 그 결정이 바뀌지 않아야 합니다. Karpenter는 EC2NodeClass의 변경을 드리프트로 감지해 노드를 교체하는 것이 정상 동작이므로, 이 요구와는 방향이 반대입니다. alias: al2023@latest 같은 표기를 쓰고 있다면 더욱 그렇습니다.

19. 참고 자료

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다