目次
1. KarpenterによるGPUノードプロビジョニング
GPUワークロードの特殊性
AI/MLワークロードは一般的なコンピューティングとは異なる固有の要件があります:
+---------------------------------------------------------------+
| GPUワークロードの特性 |
+---------------------------------------------------------------+
| - 高額なGPUインスタンス(1時間あたり数〜数十ドル) |
| - 長時間の学習ジョブ(数時間〜数日) |
| - 推論作業の低レイテンシ要件 |
| - 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: ['ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d']
# 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メモリ | 主な用途 |
+------------------+----------+----------+------------------+---------------------+
| 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:
# 学習中のConsolidation無効化
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: ['ap-northeast-1a', 'ap-northeast-1c', 'ap-northeast-1d']
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中断対策
# 長時間学習ジョブに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ドライバ | | 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 | DaemonSetリソース要求合計 |
+-----------------------------------------------+------------------------------------------+
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がリソース制限に近づいています'
# GPU使用率が低いノードの検知
- alert: LowGPUUtilization
expr: |
avg_over_time(DCGM_FI_DEV_GPU_UTIL[30m]) < 10
for: 1h
labels:
severity: info
annotations:
summary: 'GPU使用率が1時間にわたり10%未満です'
# Karpenterプロビジョニング失敗
- alert: KarpenterProvisioningFailed
expr: |
increase(karpenter_nodeclaims_terminated_total{reason="ProvisioningFailed"}[15m]) > 0
labels:
severity: critical
annotations:
summary: 'KarpenterがGPUノードのプロビジョニングに失敗しました'
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のサブネット/SGタグを確認 |
| 学習中にノードが中断された | 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ドライバ、 | | Grafana | |
| | Device Plugin、 | | (Karpenter + | |
| | DCGM Exporter) | | DCGMメトリクス) | |
| +-------------------+ +-------------------+ |
+---------------------------------------------------------------------+
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-countへGtと'0'を付けた表現がまさにこれです。GPUを一つでも持つすべてのインスタンスタイプという意味であり、インスタンスタイプ名を列挙していないため、AWSが新しいGPUファミリを出してもNodePoolを直す必要がありません。
逆に、プロビジョニング失敗の原因の第一位は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はconsolidationPolicyとconsolidateAfter、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 9時から10時間は日本時間で18時から翌朝4時です。ちょうど逆に掛かっています。日本の業務時間である9時から19時を塞ぐにはUTC 0時に始める必要があります。8節の'0 23 * * *'とduration: 1hも同様に、日本時間の朝8時から9時までのメンテナンス枠になります。時差のある地域で運用しているなら、この一行が学習ジョブを深夜に切る原因になります。
reasonsに書ける値はDrifted、Underutilized、Emptyの三つだけです。8節の最後のbudgetがDriftedだけを指定しているのは、ドリフトによる中断だけを別枠で管理するという意味であり、残りの理由は上のbudgetの適用を受けます。budgetをまったく定義しない場合のデフォルトは10%が一つです。
# budgetsのscheduleはUTCとしてのみ解釈される(JST = 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とは無関係にノードは消えます。ノードの最大寿命はexpireAfterとterminationGracePeriodの和であり、後者はPodの退避に許される最大時間です。チェックポイントを保存する時間が必要なら、PodのterminationGracePeriodSecondsだけでなくNodePool側の値も合わせて見る必要があります。
ドリフトはまた別の規則を使います。KarpenterはNodeClaimTemplateSpecのハッシュをNodePoolとEC2NodeClassにアノテーションとして残し、そのハッシュが変わると既存ノードをドリフトと判定します。ここから除外されるフィールドがあります。spec.weight、spec.limits、spec.disruption配下の値のように動作だけを変えるフィールドはドリフト検知から外れます。weightを調整したのにノードが置き換わらないと慌てる必要はないという意味であり、逆にAMIやサブネットのセレクタを変えると既存の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/gpuとvpc.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-countにGt 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-typeにreservedがあることはこの状況を扱う余地があるという意味ですが、固定ノードグループをそのまま残し、その外側のバーストだけをKarpenterに任せる構成のほうが単純な場合が多いのです。
非常に長い学習ジョブも境界の一つです。数十時間に及ぶ単一ジョブで自発的な中断が一度も許容されないなら、15節で見たようにdo-not-disruptだけでは足りず、期限切れと中断通知まですべて封じる必要があります。そこまで来るとKarpenterを使いながら自動化をすべて切った状態になるので、最初から固定ノードグループを置くほうが運用者にとって理解しやすくなります。
GPUのAMIとドライバのバージョンを手で固定しなければならないクラスタも同様です。特定のCUDAバージョンに縛られたワークロードや認証が必要な環境では、AMIを人間が決め、その決定が変わらないようにする必要があります。KarpenterはEC2NodeClassの変更をドリフトとして検知しノードを置き換えるのが正常な動作なので、この要求とは方向が逆です。alias: al2023@latestのような可変の参照を使っているならなおさらです。
19. 参考資料
- Karpenter — Disruption — 2026-08-16確認
- Karpenter — NodePools — 2026-08-16確認
- Karpenter — Scheduling (well-known labels) — 2026-08-16確認
- Karpenter — Troubleshooting — 2026-08-16確認