はじめに
id:masayosu です。 この記事は、はてなエンジニア Advent Calendar 2025 2026年1月15日の記事であり、はてなのSREが毎月交代で書いているSRE連載の2026年1月号の記事です。
本記事では、Amazon EKS + Karpenter を使ってGPUノードを構築した話しと、その中で フラクショナル GPU インスタンス利用時に直面した課題をまとめます。
GPU ノードが必要になった背景
GPU ノードを導入するにあたり、以下のような背景がありました。
- チーム内で GPU を使うアプリケーション・検証用途が増えてきた
- とはいえ GPU ノードを常設するのはコスト的に厳しい
- 「使うときだけ GPU を使いたい」
前提として、EKS は on EC2で運用しており、ノード管理にはすでに Karpenter を導入しています。
Karpenter で GPU を扱うメリット
Karpenter を利用することで、GPU ノードを既存のノード管理の延長として扱えます。
- GPU ノードを常設せず、Pod の要求に応じて起動できる
- GPU 専用のスケーリング構成を別途用意する必要がない
- ノード管理を Karpenter に一本化できる
結果として、GPU ノードは NodePool を追加するだけで導入できました。
GPU ノードの構成概要
構成としては非常にシンプルです。
- GPU 専用の NodePool を作成
- 通常ノードとは taint / toleration で分離
(taint / toleration による Pod 分離の考え方についてはこちら が参考になります) - GPU を要求する Pod が来たときだけ GPU ノードを起動
この構成により
- 通常 Pod が GPU ノードに載ることはない
- GPU Pod が存在しないときは GPU ノードは 0 台
という状態を実現できます。
Pod から GPU ノードが起動する仕組み
Pod 側では、以下のように GPU を request / limit するだけです。
resources: limits: nvidia.com/gpu: 1 requests: nvidia.com/gpu: 1
この Pod がスケジュールされると、
- GPU を要求している Pod が Pending になる
- Karpenter が条件を満たす Node を探索
- GPU 対応インスタンスで Node を起動
- Pod が GPU ノード上で実行される
という流れになります。
処理が終わり Pod が消えれば、GPU ノードも Karpenter によってスケールダウンされます。
Bottlerocket + GPU AMI を採用した理由
GPU ノードの OS には、Bottlerocket の GPU 最適化 AMI(GPU バリアント) を採用しました。
理由は以下です。
- 通常ノードで既に Bottlerocket を導入済み
- 軽量なOSなのでノード起動が速い
- GPU ドライバや device plugin がプリインストール済み
AWS の公式ブログでも以下のように説明されています。
このバリアントには、コンテナランタイム用にプリインストールおよび設定された GPU ドライバが付属しています。GPU ドライバをインストールまたは設定したり、k8s-device-plugin を実行したりする必要はありません。
GPU ノード特有の初期セットアップが不要となり、Karpenter によるノード起動後すぐに GPU Pod をスケジュールできます。
インスタンスタイプ選定
GPU ノードを構築するにあたり利用するインスタンスタイプについて検討しました。
Amazon EC2 では、用途に応じて複数の GPU インスタンスファミリーが提供されており、1 枚の GPU を複数インスタンスで共有できる フラクショナル GPU をサポートするインスタンスタイプも登場しています。
フラクショナル GPU は、GPU メモリや計算資源を分割して利用できるため、リソースの無駄が出にくく、GPU 利用コストを抑えやすいといったメリットがあります。
今回の GPU ワークロードでも、これらの理由からフラクショナル GPU インスタンスの利用を検討しました。
実際に起きた問題
一部のフラクショナル GPU インスタンス(例: g6f 系)を Karpenter の NodePool に指定したところ、GPU Pod がいつまでも Pending のままノードが起動しないという事象に遭遇しました。
原因を調べるために DescribeInstanceTypes を確認すると、以下のような結果になっていました。
aws ec2 describe-instance-types \
--region ap-northeast-1 \
--instance-types g6f.large g6f.xlarge g6f.2xlarge g6f.4xlarge \
--query "InstanceTypes[].{
InstanceType: InstanceType,
GPUModel: GpuInfo.Gpus[].Name,
GPUCount: GpuInfo.Gpus[].Count,
TotalGPU: GpuInfo.TotalGpuMemoryInMiB
}" \
--output json
{ "InstanceType": "g6f.large", "GPUModel": ["L4"], "GPUCount": [0], "TotalGPU": 2861 }
- GPUCount が 0 として扱われている
Karpenter は、EC2 の DescribeInstanceTypes API が返す情報をもとに、各インスタンスタイプが持つ CPU やメモリ、GPU などのリソース情報を判定しています。 そのため、g6f など一部のインスタンスタイプでは、API 側で GPU 数が 0 として返されることで、GPU を要求する Pod のスケジューリングに影響が出ていることがわかりました。
なお、この挙動については GitHub Issue でも議論されています。
【補足】プレウォーム用 Pod によるワークアラウンド
本件については暫定的なワークアラウンドとして、 Node を起動するための「プレウォーム用 Pod」を用意する方法も試しました。
具体的には、GPU を request しない(CPU / Memory のみを request する)Pod を先にデプロイし、 g6f インスタンスの Node を起動させた上で、その後に GPU を利用する Pod をデプロイする方法です。
ただし、この方法は GPU の request に応じた自動スケールはできません。 プレウォーム Pod は GPU リソースをトリガーにしているわけではないため、GPU Pod の増減とノード台数が連動せず、必要な台数を別途コントロールする必要があります。
実際に使用した Deployment の manifest は、記事の最後に掲載します。
まとめ
Karpenter を利用することで、GPU ノードをオンデマンドで起動・停止できる構成を構築できました。 GPU ノードを常設せずに運用できている点や、Bottlerocket + GPU AMI により初期セットアップの手間を抑えられた点は運用上の利点だと感じました。
一方で、GPU インスタンスであれば必ず Karpenter で利用できるとは限らないことも分かりました。 フラクショナル GPU インスタンスについては、現時点では制約があるので今後の対応が待たれます。
現時点の実運用環境では、フル GPU インスタンスを利用する構成が望ましいと判断しました。
補足資料:プレウォーム用 Deployment
apiVersion: apps/v1 kind: Deployment metadata: name: gpu-node-warmup spec: # 起動したいノード数に応じて調整 replicas: 3 selector: matchLabels: app: gpu-node-warmup template: metadata: labels: app: gpu-node-warmup annotations: karpenter.sh/do-not-evict: "true" spec: tolerations: - key: "example.com/gpu" operator: "Exists" effect: "NoSchedule" nodeSelector: node-role.kubernetes.io/gpu: "true" affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: gpu-node-warmup topologyKey: kubernetes.io/hostname terminationGracePeriodSeconds: 0 containers: - name: hold image: public.ecr.aws/eks-distro/kubernetes/pause:3.9 resources: requests: cpu: "200m" memory: "128Mi"