0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Kubernetes/Kueue】WorkloadPriorityClassとPriorityClassの違いと陥る罠 - 分散学習環境での実践ガイド

0
Last updated at Posted at 2025-12-21

image.png

目次

  1. はじめに
  2. 【結論】分散学習環境での推奨設定(先に読むべき)
  3. 基本概念の理解
  4. 2段階のスケジューリングメカニズム
  5. Preemption(プリエンプション)の仕組み
  6. 陥りやすい罠と実例
  7. ベストプラクティス
  8. まとめ
  9. 【重要】避けられない既知の問題: プリエンプション中のリソース解放によるジョブ失敗
  10. 別の選択肢: Volcano Schedulerの検討
  11. 参考文献

はじめに

背景と課題

機械学習、特に深層学習の分野では、モデルの大規模化に伴い分散学習が標準的な手法となっています。PyTorchJobやMPIJobなどの分散学習フレームワークを使用することで、複数のGPUノードにまたがる効率的な学習が可能になります。

しかし、限られたGPUリソースを複数のチームやプロジェクトで共有する環境では、以下のような課題に直面します:

  • GPUリソースの高コスト: NVIDIA A100など高性能GPUは1時間あたり$1-2のコストがかかり、無駄な占有は直接的な損失につながります
  • 複数ジョブの並行実行ニーズ: 実験ジョブ、本番学習、推論サービスなど、優先度の異なるワークロードが同時に実行される必要があります
  • 優先度制御の難しさ: 緊急度の高いジョブを優先実行しつつ、既存ジョブのリソースを効率的に再配分する仕組みが必要です

さらに、PyTorchJobのような分散学習フレームワークには固有の制約があります:

  • Master + Workersアーキテクチャ: 1つのMaster Podと複数のWorker Podで構成されます
  • 全Pod揃って初めて学習開始: すべてのPodが起動して相互に接続するまで、学習処理は開始されません
  • 部分的な停止による非効率: 一部のWorkerだけが停止すると、残りのWorkerがGPUを占有したまま待機状態になり、リソースが無駄になります

よくある失敗シナリオ:

❌ 問題発生:
低優先度PyTorchJob(GPU 8個使用中)
├─ Master Pod: Running
├─ Worker-0: Running
├─ Worker-1: Running
...
└─ Worker-7: Running

→ 高優先度Podが投入される(GPU 1個必要)

→ Kubernetes Schedulerの判断:
   Worker-0のみを退避(Preempt)

→ 結果:
   ├─ Worker-0: Terminating(停止中)
   ├─ Worker-1~7: Running(待機状態)← GPU 7個が無駄に占有
   └─ 学習が進まない + リソース浪費

コスト影響:
GPU 7個 × $1-2/時間 = $7-14/時間の損失
24時間で $168-336 の無駄なコスト

このような問題は、Kubernetes標準のPriorityClassとKueue固有のWorkloadPriorityClassの違いを正しく理解していないことが原因で発生します。

記事の目的

本記事では、以下の内容を実践的に解説します:

  1. WorkloadPriorityClassとPriorityClassの本質的な違いを明確にする
  2. 実運用で陥りやすい5つの罠を具体例とともに解説する
  3. ベストプラクティスとして推奨される設定パターンを提示する
  4. トラブルシューティング手順を実行可能なコマンド例付きで説明する

読者の皆さんが、分散学習環境でのリソース管理を正しく実装し、無駄なコストとトラブルを回避できるようになることを目指します。

前提知識

本記事を最大限活用するには、以下の知識があることが望ましいです:

  • Kubernetes基礎: Pod, Job, PriorityClassの基本概念
  • Kueue基礎: ClusterQueue, LocalQueue, Workloadの役割
  • 分散学習フレームワーク: PyTorchJob, MPIJobなどの基本的な構造

これらの詳細な解説は本記事の範囲外ですが、必要に応じて公式ドキュメントへのリンクを提供します。


【結論】分散学習環境での推奨設定(先に読むべき)

長い記事を読む前に、最も重要な結論を先にお伝えします。分散学習環境でKueueを使用する場合、以下の設定が必須です。

必須設定3原則

1. Pod単位プリエンプションを完全無効化

# ✅ 必須: 全PyTorchJobで使用
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: pytorchjob-zero-priority
value: 0
preemptionPolicy: Never  # ← これが最重要!
globalDefault: false
description: "PyTorchJob専用。Pod単位プリエンプション無効化"

なぜ必須か:

  • preemptionPolicy: PreemptLowerPriority(デフォルト)だと、一部Workerのみ停止 → 残りのWorkerがGPU占有したまま待機 → コスト損失
  • preemptionPolicy: Never で、Pod単位の停止を防ぎ、Workload全体での制御のみを許可

影響範囲: 1ジョブで7 GPUが無駄に占有 → $7-14/時間の損失(24時間で $168-336)

2. Workload単位プリエンプションを有効化

# ✅ 必須: ClusterQueue設定
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-cluster-queue
spec:
  preemption:
    withinClusterQueue: LowerPriority  # ← Workload単位プリエンプション
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu"]

なぜ必須か:

  • Workload全体(Master + 全Workers)を同時に停止・起動
  • リソースの無駄なし、一貫した動作

3. WorkloadPriorityClassでジョブ間優先度を制御

# ✅ ジョブごとに設定
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: production-training
  labels:
    kueue.x-k8s.io/priority-class: kueue-high  # ← Workload優先度
spec:
  pytorchReplicaSpecs:
    Master:
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # ← Pod優先度(全ジョブ統一)
    Worker:
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # ← Pod優先度(全ジョブ統一)

ポイント:

  • priorityClassName: 全PyTorchJobで統一pytorchjob-zero-priority
  • kueue.x-k8s.io/priority-class: ジョブごとに変える(kueue-high/medium/low

よくある致命的な間違い

設定ミス 結果 損失例
preemptionPolicy未設定 一部Workerのみ停止 $168-336/日
priorityClassNameを各ジョブで変える Pod単位プリエンプションが発生 $7-14/時間
❌ ClusterQueueでpreemption未設定 Workload単位プリエンプションが働かない リソース枯渇

設定確認コマンド

# 1. PriorityClass確認
kubectl get priorityclass pytorchjob-zero-priority -o jsonpath='{.preemptionPolicy}'
# 期待出力: Never

# 2. 既存Podの設定確認
kubectl get pods -l job-type=pytorch -o jsonpath='{range .items[*]}{.metadata.name},{.spec.priorityClassName},{.spec.preemptionPolicy}{"
"}{end}'
# 全て "pytorchjob-zero-priority,Never" であること

# 3. ClusterQueue確認
kubectl get clusterqueue gpu-cluster-queue -o jsonpath='{.spec.preemption.withinClusterQueue}'
# 期待出力: LowerPriority

この記事で学べること

上記の設定がなぜ必須なのか技術的にどう動作するのか設定ミス時の具体的な被害を、以下のセクションで詳しく解説します:

  • Section 2: 2段階の優先度制御メカニズム(KueueとKubernetesの分業)
  • Section 3: Preemptionの仕組み(Pod単位 vs Workload単位)
  • Section 4: 陥りやすい罠と実例(実際の損失計算含む)
  • Section 5: ベストプラクティスと運用ガイド

急いでいる方へ: 上記の3つの設定をそのまま適用すれば、即座にリスクを回避できます。詳細な理解は時間のあるときに読み進めてください。


基本概念の理解

PriorityClass(Kubernetes標準機能)

PriorityClassはKubernetesに標準で備わっているリソースで、Pod単位の優先度を制御します。

定義と役割:

  • API: scheduling.k8s.io/v1
  • スコープ: 個別のPod
  • 適用タイミング: Pod作成時、Kubernetesスケジューラーによるノード配置時
  • 目的: ノードリソースが不足した場合の退避(Preemption)優先順位を決定

基本的な設定例:

apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000
preemptionPolicy: PreemptLowerPriority  # デフォルト値
globalDefault: false
description: "高優先度Pod用のPriorityClass"

重要なパラメータ:

パラメータ 説明 設定値
value 優先度の数値(大きいほど高優先度) 0 ~ 1,000,000,000
preemptionPolicy プリエンプション動作の制御 PreemptLowerPriority(デフォルト)
Never(プリエンプション無効)
globalDefault デフォルトPriorityClassとして使用するか true / false
description 説明文(ドキュメント目的) 任意の文字列

preemptionPolicyの詳細:

  • PreemptLowerPriority(デフォルト): このPodより低いvalueを持つPodを退避させることができます
  • Never: このPodは他のPodを退避させません。リソース不足の場合はPending状態で待機します

WorkloadPriorityClass(Kueue固有機能)

WorkloadPriorityClassはKueueが提供する独自のリソースで、Workload全体の優先度を制御します。

定義と役割:

  • API: kueue.x-k8s.io/v1beta1
  • スコープ: Workload(複数のPodをまとめた論理的な単位)
  • 適用タイミング: ClusterQueue内での待機順序決定、Admission制御
  • 目的: ジョブ全体の優先順位を決定し、Workload単位でのプリエンプションを制御

Workloadとは:

Kueueでは、Job/PyTorchJob/MPIJobなどのリソースから自動的にWorkloadオブジェクトが生成されます。Workloadは複数のPodをまとめて管理し、以下の情報を持ちます:

  • リソース要求量: 必要なCPU/Memory/GPU数
  • 優先度: WorkloadPriorityClassから継承されるpriority
  • Queue情報: どのLocalQueue/ClusterQueueに属するか
  • Admission状態: リソースが確保されているか(Admitted/Pending)

: PyTorchJob → Workload の関係

PyTorchJob "training-job"
├─ Master Pod (1個)
└─ Worker Pods (8個)

↓ Kueueが自動生成

Workload "training-job-workload-xxxxx"
├─ podSets: [master: 1, worker: 8]
├─ priority: 100 (WorkloadPriorityClassから継承)
└─ admitted: true/false

基本的な設定例:

apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: kueue-high
value: 100
description: "本番学習ジョブ用(高優先度)"

PyTorchJobでの使用例:

apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: production-training
  labels:
    kueue.x-k8s.io/queue-name: training-queue
    kueue.x-k8s.io/priority-class: kueue-high  # ← WorkloadPriorityClassを指定
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
    Worker:
      replicas: 8

このラベルにより、Kueueは自動生成されるWorkloadにpriority: 100を設定します。

動作の違いまとめ表

2つのPriorityClassの違いを整理します:

項目 WorkloadPriorityClass PriorityClass
管理レイヤー Kueue(ジョブキューイングシステム) Kubernetes(コンテナオーケストレーション)
制御単位 Workload(ジョブ全体) Pod(個別コンテナ)
API kueue.x-k8s.io/v1beta1 scheduling.k8s.io/v1
適用タイミング Queue待機順序決定、Admission制御 Pod作成、ノード配置時
プリエンプション対象 Workload全体(全Pod同時停止) Pod単位(個別停止)
PyTorchJobでの影響 ジョブ全体が停止/起動(安全) 一部Workerのみ停止(危険)
推奨用途 ジョブ間の優先順位制御 ノードレベルの微調整
値の範囲 通常 0-1000 0-1,000,000,000

重要な理解:

これらは独立した2つの優先度システムであり、それぞれ異なるタイミングで動作します:

  1. 1段階目(Kueueレベル): WorkloadPriorityClassでジョブ全体の優先順位を決定
  2. 2段階目(Kubernetesレベル): PriorityClassで個別Podのノード配置を決定

この2段階の仕組みを正しく理解することが、適切な設定の第一歩です。


2段階のスケジューリングメカニズム

全体フロー図

KueueとKubernetesがどのように連携して優先度制御を行うのか、シーケンス図で示します:

1段階目:Kueueによる優先度制御

動作フロー:

  1. Workload生成: PyTorchJob等が作成されると、Kueueが自動的にWorkloadオブジェクトを生成します

  2. WorkloadPriority判定:

    # PyTorchJobのラベルから優先度を取得
    labels:
      kueue.x-k8s.io/priority-class: kueue-high
    
    # 生成されるWorkload
    spec:
      priority: 100  # kueue-highのvalueが継承される
    
  3. Queue内順序決定: ClusterQueue内で、高いpriority値のWorkloadが優先的にAdmitされます

  4. Admission制御:

    • ClusterQueueの利用可能リソースをチェック
    • リソースが十分ならAdmitted状態に変更
    • リソース不足ならPending状態で待機
  5. プリエンプション:

    • リソース不足かつ高優先度Workloadが待機中の場合
    • 低優先度Workload全体を停止(全Pod同時)
    • リソース解放後、高優先度WorkloadをAdmit

具体例:

# PyTorchJob設定
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: production-training
  labels:
    kueue.x-k8s.io/queue-name: training-queue
    kueue.x-k8s.io/priority-class: kueue-high  # WorkloadPriority指定
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
    Worker:
      replicas: 8
# Kueueが自動生成するWorkload
apiVersion: kueue.x-k8s.io/v1beta1
kind: Workload
metadata:
  name: production-training-workload-abc123
  namespace: default
spec:
  queueName: training-queue
  priority: 100  # kueue-highのvalueが継承
  priorityClassName: kueue-high
  priorityClassSource: kueue.x-k8s.io/workloadpriorityclass
  podSets:
  - count: 1
    name: master
  - count: 8
    name: worker

重要ポイント:

  • WorkloadPriorityはKueue内でのみ有効
  • Workload単位でプリエンプション(全Pod同時停止
  • Queue待機順序を決定するのはこの段階

コマンドでの確認:

# Workloadの優先度確認
kubectl get workload production-training-workload-abc123 -o yaml | grep -A 3 "priority"

# 出力例:
# priority: 100
# priorityClassName: kueue-high
# priorityClassSource: kueue.x-k8s.io/workloadpriorityclass

# ClusterQueue内の待機Workload確認
kubectl get clusterqueue training-cluster-queue -o yaml | grep -A 10 "pendingWorkloads"

2段階目:Kubernetesスケジューラーによる優先度制御

WorkloadがAdmitted状態になると、実際のPod作成が始まります。ここからはKubernetes標準のスケジューリングが動作します。

動作フロー:

  1. Pod作成: Workload Admit後、PyTorchJobコントローラーがPodを作成します

  2. PriorityClass判定:

    # Pod仕様(PyTorchJob Workerの例)
    spec:
      priorityClassName: high-priority  # Pod Priority指定
      containers:
      - name: pytorch
        resources:
          limits:
            nvidia.com/gpu: 1
    
    # 実際に作成されるPod
    apiVersion: v1
    kind: Pod
    spec:
      priority: 1000  # high-priorityのvalueが設定される
      priorityClassName: high-priority
    
  3. ノード配置: Kubernetesスケジューラーが利用可能なノードにPodを配置

  4. プリエンプション:

    • ノードリソース不足の場合
    • 低いpriority値を持つPodを個別に退避
    • リソース確保後、高優先度Podを配置

具体例:

# PyTorchJob Pod仕様
apiVersion: kubeflow.org/v1
kind: PyTorchJob
spec:
  pytorchReplicaSpecs:
    Worker:
      template:
        spec:
          priorityClassName: high-priority  # ← Pod Priority指定
          containers:
          - name: pytorch
            image: pytorch/pytorch:latest
            resources:
              limits:
                nvidia.com/gpu: 1
# 実際に作成されるPod
apiVersion: v1
kind: Pod
metadata:
  name: production-training-worker-0
  namespace: default
spec:
  priority: 1000  # ← high-priorityのvalueが設定
  priorityClassName: high-priority
  containers:
  - name: pytorch
    resources:
      limits:
        nvidia.com/gpu: 1

重要ポイント:

  • PriorityClassはKubernetes全体で有効
  • Pod単位でプリエンプション(個別Pod停止)← これが問題の原因
  • ノードレベルでのリソース競合時に動作

コマンドでの確認:

# Podの優先度確認
kubectl get pod production-training-worker-0 -o jsonpath='{.spec.priority}'
# 出力: 1000

# プリエンプションイベント確認
kubectl get events --sort-by='.lastTimestamp' | grep Preempted
# Normal  Preempted  pod/low-priority-worker-0  Preempted by default/high-priority-pod

2段階が必要な理由

なぜKueueとKubernetesの2段階で優先度制御が必要なのでしょうか?

理由1: Admission時点でリソース確保≠Pod配置完了

  • Kueue Admit = リソース予約(論理的な確保)

    • 「このWorkloadにGPU 8個を割り当てる」という予約
    • 実際のノードへの配置はまだ完了していない
  • 実際のPod配置 = Kubernetesスケジューラー(物理的な配置)

    • 具体的にどのノードに配置するかを決定
    • ノードのリソース状況を確認して配置

理由2: ノード障害時の再配置

【シナリオ】
時刻 T0: Workload Admitted (GPU 8個予約)
         ノードA,B,Cに均等配置予定

時刻 T1: ノードBが障害発生

時刻 T2: ノードB上のPodを再配置する必要
         ノードA,Cでリソース競合が発生

→ この時点でPriorityClassによる優先度判定が必要

理由3: 他システムとの共存

Kueueで管理されていないPod(DaemonSet、システムPodなど)との競合が発生する場合があります:

# Kueue管理外のシステムPod
apiVersion: v1
kind: Pod
metadata:
  name: monitoring-agent
spec:
  priorityClassName: system-node-critical  # 非常に高い優先度
  # このPodはKueueの管理外だが、同じノード上で動作

PriorityClassがあることで、Kueueで管理されているPodと管理外のPodの間でも、一貫した優先度判定が可能になります。


Preemption(プリエンプション)の仕組み

プリエンプションとは、高優先度のワークロードにリソースを確保するために、低優先度のワークロードを停止させる仕組みです。KubernetesとKueueでは、それぞれ異なる単位でプリエンプションが実行されます。

Pod単位プリエンプション(Kubernetes標準)

基本動作:

動作条件:

  1. 新しいPodのpriority > 既存Podのpriority
  2. 既存PodのpreemptionPolicy: PreemptLowerPriority(デフォルト)
  3. ノードに十分なリソースがない

具体例シナリオ:

【初期状態】
ノードA (GPU: 8/8使用中)
├─ Worker-0 (priority: 0)  ← 低優先度PyTorchJob
├─ Worker-1 (priority: 0)
├─ Worker-2 (priority: 0)
├─ Worker-3 (priority: 0)
├─ Worker-4 (priority: 0)
├─ Worker-5 (priority: 0)
├─ Worker-6 (priority: 0)
└─ Worker-7 (priority: 0)

【高優先度Pod投入】
新Pod (priority: 1000, GPU: 1個必要)

【Kubernetesスケジューラーの判断】
1. ノードA: GPU 8/8使用中(リソース不足)
2. 新Pod(1000) > Worker-0~7(0) → プリエンプション可能
3. Worker-0を選択(任意の1つ)
4. Worker-0をTerminating状態に変更

【プリエンプション実行】
┌────────────────────────────────────┐
│ ノードA (GPU: 8個)                  │
├────────────────────────────────────┤
│ ❌ Worker-0: Terminating           │
│ ✅ Worker-1: Running(待機状態)   │
│ ✅ Worker-2: Running(待機状態)   │
│ ✅ Worker-3: Running(待機状態)   │
│ ✅ Worker-4: Running(待機状態)   │
│ ✅ Worker-5: Running(待機状態)   │
│ ✅ Worker-6: Running(待機状態)   │
│ ✅ Worker-7: Running(待機状態)   │
│ ✅ 新Pod: Running                  │
└────────────────────────────────────┘

【結果】
❌ 問題発生:
- Worker-0のみ停止
- Worker-1~7は継続実行だが学習できない
- GPU 7個が無駄に占有
- コスト損失: $7-14/時間

コード例:

# Pod単位プリエンプションが発生する設定
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 1000
preemptionPolicy: PreemptLowerPriority  # ← デフォルト値(危険)
globalDefault: false

Workload単位プリエンプション(Kueue固有)

基本動作:

動作条件:

  1. ClusterQueue設定: preemption.withinClusterQueue: LowerPriority
  2. 新しいWorkloadのpriority > 既存Workloadのpriority
  3. ClusterQueueに十分なリソースがない

具体例シナリオ:

【初期状態】
ClusterQueue "gpu-queue" (GPU: 8/8使用中)
└─ 低優先度Workload (priority: 0)
    ├─ Master Pod: Running
    ├─ Worker-0: Running
    ├─ Worker-1: Running
    ├─ Worker-2: Running
    ├─ Worker-3: Running
    ├─ Worker-4: Running
    ├─ Worker-5: Running
    ├─ Worker-6: Running
    └─ Worker-7: Running

【高優先度Workload投入】
新Workload (priority: 100, GPU: 4個必要)

【Kueueの判断】
1. ClusterQueue: GPU 8/8使用中(リソース不足)
2. 新Workload(100) > 既存Workload(0) → プリエンプション可能
3. 低優先度Workload全体を停止決定

【プリエンプション実行】
Kueueの動作:
1. 低優先度WorkloadのAdmission取り消し
2. 全Pod(Master + Worker 0-7)を同時Terminating
3. GPU 8個すべてが解放される
4. 高優先度WorkloadをAdmit(GPU 4個使用)
5. 新しいPod群が起動

【結果】
✅ 効率的:
- Workload全体が同時停止/起動
- リソースの無駄なし
- 一貫した動作
- 残りGPU 4個は他のWorkloadが利用可能

コード例:

# Workload単位プリエンプション設定
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-cluster-queue
spec:
  preemption:
    withinClusterQueue: LowerPriority  # ← Workload単位プリエンプション有効化
    reclaimWithinCohort: Any
    borrowWithinCohort:
      policy: LowerPriority
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu"]
    flavors:
    - name: gpu-flavor
      resources:
      - name: "nvidia.com/gpu"
        nominalQuota: 32

両者の比較表と推奨設定

項目 Pod単位プリエンプション Workload単位プリエンプション
停止単位 Pod 1つずつ Workload全体(全Pod同時)
PyTorchJobでの影響 ❌ 一部Worker停止→学習停止→GPU無駄 ✅ ジョブ全体停止→効率的
制御方法 PriorityClass WorkloadPriorityClass
設定場所 Pod仕様 (spec.priorityClassName) Job/PyTorchJobラベル
プリエンプション条件 ノードリソース不足 ClusterQueueリソース不足
プリエンプション判定者 Kubernetes Scheduler Kueue Controller
分散学習での推奨度 ❌ 非推奨(危険) ✅ 必須
コスト効率 ❌ 低(部分占有発生) ✅ 高(無駄なし)

推奨設定パターン:

# ✅ 推奨: Pod単位プリエンプション無効化
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: pytorchjob-zero-priority # ← 1種類のみ作成し、全workerで固定
value: 0
preemptionPolicy: Never  # ← 無効化(重要!)
globalDefault: false
description: |
  PyTorchJob専用PriorityClass
  - Pod単位プリエンプションを無効化
  - Workload単位プリエンプションのみ使用

---
# ✅ 推奨: Workload単位プリエンプション有効化
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-cluster-queue
spec:
  preemption:
    withinClusterQueue: LowerPriority  # ← 有効化(必須!)
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu"]
    flavors:
    - name: gpu-flavor
      resources:
      - name: "nvidia.com/gpu"
        nominalQuota: 32

設定の意図:

  • Pod単位プリエンプション: Neverに設定することで完全無効化

    • 一種類のPriorityClassのみ作成し、全てのWorkloadで同じ値を使う。これにより、Pod単位プリエンプションを確実に阻止
    • Kubernetesスケジューラーは低優先度Podを退避しない
    • リソース不足の場合はPending状態で待機
    • Kueueによる調整を待つ
  • Workload単位プリエンプション: LowerPriorityに設定

    • Kueueが低優先度Workload全体を停止
    • 全Pod同時停止により、部分的なリソース占有を回避
    • 効率的なリソース再配分を実現

陥りやすい罠と実例

実運用環境では、WorkloadPriorityClassとPriorityClassの違いを正しく理解していないことで、様々な問題が発生します。ここでは特に重要な5つの罠を、詳細な技術解説と実例を交えて説明します。

罠1:Pod単位プリエンプションによるリソース無駄【最重要】

これは最も深刻かつ頻発する問題です。本セクションでは約3,000文字を使って徹底的に解説します。

問題の本質的理解

なぜこれが最も危険な罠なのか:

  1. 直感に反する動作:

    • 「優先度制御」という言葉から、多くの開発者は「ジョブ全体が制御される」と誤解します
    • 実際にはPod単位での制御が行われ、一部のPodだけが停止する想定外の動作が発生します
  2. 気づきにくい:

    • 一部のWorkerが停止しても、残りのWorkerはRunning状態を維持します
    • kubectl get podsで確認しても、一見正常に動作しているように見えます
    • 学習が進まないことに気づくまで、GPUが無駄に占有され続けます
  3. コスト影響が甚大:

    • NVIDIA A100の場合、1 GPU = $1-2/時間
    • 8 GPU構成のPyTorchJobで1つのWorkerが停止した場合、残り7 GPU = $7-14/時間の損失
    • 24時間気づかなければ、$168-336の無駄なコストが発生
    • 複数ジョブで同時発生すると、損失は数千ドルに達することも

PyTorchJobの動作特性:

PyTorchJobは分散データ並列学習(Distributed Data Parallel, DDP)を実装しており、以下の特性があります:

# PyTorchJobの内部動作(概念的なコード)
def distributed_training():
    # 初期化: 全Workerが揃うまで待機
    dist.init_process_group(
        backend='nccl',
        init_method='env://',
        world_size=9,  # Master(1) + Workers(8)
        rank=get_rank()
    )
    
    # 学習ループ
    for epoch in range(num_epochs):
        for batch in dataloader:
            # 全Workerでforward/backward
            loss = model(batch)
            loss.backward()
            
            # 重要: 全Workerで勾配を同期
            # 1つでもWorkerが欠けると、ここで永遠に待機
            dist.all_reduce(loss)  # ← ここでブロック!
            
            optimizer.step()

動作条件:

  • 全Podが揃っている → 学習実行(勾配同期が正常に完了)
  • 1つでもPodが欠けている → 学習停止(all_reduceで待機、全Podが待機状態でGPUを占有)

詳細な発生メカニズム

ステップバイステップの動作解説:

【時刻 T0】初期状態 - 正常動作中
┌────────────────────────────────────┐
│ ノードA (GPU: 8個)                  │
├────────────────────────────────────┤
│ ✅ low-priority-training-worker-0   │ GPU: 1, Priority: 0, Status: Running
│ ✅ low-priority-training-worker-1   │ GPU: 1, Priority: 0, Status: Running
│ ✅ low-priority-training-worker-2   │ GPU: 1, Priority: 0, Status: Running
│ ✅ low-priority-training-worker-3   │ GPU: 1, Priority: 0, Status: Running
│ ✅ low-priority-training-worker-4   │ GPU: 1, Priority: 0, Status: Running
│ ✅ low-priority-training-worker-5   │ GPU: 1, Priority: 0, Status: Running
│ ✅ low-priority-training-worker-6   │ GPU: 1, Priority: 0, Status: Running
│ ✅ low-priority-training-worker-7   │ GPU: 1, Priority: 0, Status: Running
└────────────────────────────────────┘

PyTorchJobの状態:
- 学習実行中: Epoch 42/100
- GPU利用率: 95%以上(正常)
- 学習速度: 正常

【時刻 T1】高優先度Pod投入
kubectl apply -f high-priority-inference.yaml

新Pod仕様:
  name: high-priority-inference
  priority: 1000  # high-priorityクラス
  resources:
    nvidia.com/gpu: 1

【時刻 T2】Kubernetesスケジューラーの判断フロー
1. ノードA: GPU 8/8使用中(リソース不足)
2. 他ノード確認: すべて満杯
3. プリエンプション候補検索:
   - high-priority-inference(1000) > low-priority-worker-*(0)
   - プリエンプション可能と判断
4. 退避対象選択:
   - worker-0~7の中から任意に1つ選択(例: worker-0)
5. イベント発行:
   Type: Normal
   Reason: Preempted
   Message: "Preempted by default/high-priority-inference"
6. worker-0をTerminating状態に変更
7. SIGTERM送信(graceful shutdown開始)

【時刻 T3】プリエンプション発生直後の状態
┌────────────────────────────────────┐
│ ノードA (GPU: 8個)                  │
├────────────────────────────────────┤
│ ❌ worker-0: Terminating (30s)     │ SIGTERM受信、シャットダウン中
│ ✅ worker-1: Running               │ dist.all_reduce()で待機開始
│ ✅ worker-2: Running               │ dist.all_reduce()で待機開始
│ ✅ worker-3: Running               │ dist.all_reduce()で待機開始
│ ✅ worker-4: Running               │ dist.all_reduce()で待機開始
│ ✅ worker-5: Running               │ dist.all_reduce()で待機開始
│ ✅ worker-6: Running               │ dist.all_reduce()で待機開始
│ ✅ worker-7: Running               │ dist.all_reduce()で待機開始
│ ⏳ high-priority-inference: Pending│ worker-0の終了待ち
└────────────────────────────────────┘

PyTorchJobの状態:
- Master Pod: worker-0の再接続を待機
- Worker 1-7: 勾配同期待ちでブロック
- GPU利用率: 10%未満(待機状態のアイドル)
- 学習速度: 0(停止)

【時刻 T3+30秒】terminationGracePeriodSeconds経過
worker-0: SIGKILL送信 → 強制終了

【時刻 T3+35秒】high-priority-inference起動
┌────────────────────────────────────┐
│ ノードA (GPU: 8個)                  │
├────────────────────────────────────┤
│ ✅ worker-1: Running(待機状態)   │ GPU: 10%, 無駄な占有
│ ✅ worker-2: Running(待機状態)   │ GPU: 10%, 無駄な占有
│ ✅ worker-3: Running(待機状態)   │ GPU: 10%, 無駄な占有
│ ✅ worker-4: Running(待機状態)   │ GPU: 10%, 無駄な占有
│ ✅ worker-5: Running(待機状態)   │ GPU: 10%, 無駄な占有
│ ✅ worker-6: Running(待機状態)   │ GPU: 10%, 無駄な占有
│ ✅ worker-7: Running(待機状態)   │ GPU: 10%, 無駄な占有
│ ✅ high-priority-inference: Running│ GPU: 95%, 正常動作
└────────────────────────────────────┘

【時刻 T3+1時間】high-priority-inference完了
high-priority-inferenceが終了してGPUが解放
→ worker-0が再起動される(PyTorchJob Controllerによる)
→ ようやく学習が再開される

【損失計算】
無駄な待機時間: 1時間
無駄なGPU: 7個(worker-1~7)
損失コスト: $7-14(1時間あたり)

この間、worker-1~7は以下のログを出力し続けます:

[Rank 1] INFO: Waiting for all workers to synchronize gradients...
[Rank 1] DEBUG: dist.all_reduce() waiting for rank 0 (worker-0)
[Rank 1] WARNING: Synchronization timeout approaching (300s elapsed)

[Rank 2] INFO: Waiting for all workers to synchronize gradients...
[Rank 2] DEBUG: dist.all_reduce() waiting for rank 0 (worker-0)
...

実例シナリオ(3つのケース)

ケース1: 単純なPod投入によるプリエンプション

最も基本的なシナリオです。

# ❌ 危険な設定例: preemptionPolicy未指定(デフォルトでPreemptLowerPriorityになる)
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: low-priority-training
  namespace: training
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      template:
        spec:
          priorityClassName: low-priority  # value: 0, preemptionPolicy: PreemptLowerPriority
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime
            command: ["python", "train.py"]
            resources:
              limits:
                nvidia.com/gpu: 1
    Worker:
      replicas: 8
      template:
        spec:
          priorityClassName: low-priority  # value: 0, preemptionPolicy: PreemptLowerPriority
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime
            command: ["python", "train.py"]
            resources:
              limits:
                nvidia.com/gpu: 1

発生シナリオの詳細:

# 1. 低優先度PyTorchJobを起動
kubectl apply -f low-priority-training.yaml

# 2. Podが全て起動していることを確認
kubectl get pods -n training -o wide
# NAME                                READY   STATUS    RESTARTS   NODE
# low-priority-training-master-0      1/1     Running   0          node-a
# low-priority-training-worker-0      1/1     Running   0          node-b
# low-priority-training-worker-1      1/1     Running   0          node-c
# low-priority-training-worker-2      1/1     Running   0          node-d
# low-priority-training-worker-3      1/1     Running   0          node-e
# low-priority-training-worker-4      1/1     Running   0          node-f
# low-priority-training-worker-5      1/1     Running   0          node-g
# low-priority-training-worker-6      1/1     Running   0          node-h
# low-priority-training-worker-7      1/1     Running   0          node-i

# 3. GPU利用率確認(学習実行中)
kubectl exec -it low-priority-training-worker-0 -n training -- nvidia-smi
# +-----------------------------------------------------------------------------+
# | NVIDIA-SMI 525.85.12    Driver Version: 525.85.12    CUDA Version: 12.0   |
# |-------------------------------+----------------------+----------------------+
# |   0  NVIDIA A100-SXM...  On   | 00000000:00:1E.0 Off |                    0 |
# | N/A   65C    P0   395W / 400W |  79536MiB / 81920MiB |     95%      Default |
# +-------------------------------+----------------------+----------------------+
# ← GPU利用率95%(正常)

# 4. 高優先度Podを投入(GPU 1個必要)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: high-priority-inference
  namespace: training
spec:
  priorityClassName: high-priority  # value: 1000
  containers:
  - name: inference
    image: pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime
    command: ["python", "inference.py"]
    resources:
      limits:
        nvidia.com/gpu: 1
EOF

# 5. プリエンプション発生確認(数秒後)
kubectl get events -n training --sort-by='.lastTimestamp' | grep Preempted
# 12s  Normal  Preempted  pod/low-priority-training-worker-0  Preempted by training/high-priority-inference on node node-b

# 6. Pod状態確認
kubectl get pods -n training
# NAME                                READY   STATUS        RESTARTS   AGE
# low-priority-training-master-0      1/1     Running       0          5m
# low-priority-training-worker-0      0/1     Terminating   0          5m   ← 停止中!
# low-priority-training-worker-1      1/1     Running       0          5m   ← 待機中
# low-priority-training-worker-2      1/1     Running       0          5m   ← 待機中
# low-priority-training-worker-3      1/1     Running       0          5m   ← 待機中
# low-priority-training-worker-4      1/1     Running       0          5m   ← 待機中
# low-priority-training-worker-5      1/1     Running       0          5m   ← 待機中
# low-priority-training-worker-6      1/1     Running       0          5m   ← 待機中
# low-priority-training-worker-7      1/1     Running       0          5m   ← 待機中
# high-priority-inference             0/1     Pending       0          15s  ← worker-0の終了待ち

# 7. GPU利用率確認(待機状態に変化)
kubectl exec -it low-priority-training-worker-1 -n training -- nvidia-smi
# +-----------------------------------------------------------------------------+
# |   0  NVIDIA A100-SXM...  On   | 00000000:00:1E.0 Off |                    0 |
# | N/A   35C    P0    50W / 400W |  79536MiB / 81920MiB |      8%      Default |
# +-------------------------------+----------------------+----------------------+
# ← GPU利用率8%(待機状態、無駄な占有)

# 8. PyTorchJobログ確認
kubectl logs low-priority-training-master-0 -n training --tail=20
# [2025-12-20 10:15:42] INFO: Epoch 42/100, Batch 1523/2000
# [2025-12-20 10:15:45] INFO: Waiting for all workers to connect...
# [2025-12-20 10:15:45] INFO: Expected workers: 8, Connected: 7
# [2025-12-20 10:15:45] WARNING: Missing worker rank: 0 (worker-0)
# [2025-12-20 10:15:50] WARNING: Synchronization timeout: 5s elapsed
# [2025-12-20 10:15:55] WARNING: Synchronization timeout: 10s elapsed
# [2025-12-20 10:16:00] WARNING: Synchronization timeout: 15s elapsed
# ... (学習が進まない状態で待機)

# 9. ノードリソース確認
kubectl top nodes
# NAME     CPU(cores)   CPU%   MEMORY(bytes)   MEMORY%   GPU(utilization)
# node-a   45/96        46%    150Gi/384Gi     39%       1/1 (95%)  ← Master正常
# node-b   20/96        20%    120Gi/384Gi     31%       1/1 (95%)  ← high-priority-inference起動後
# node-c   8/96          8%    100Gi/384Gi     26%       1/1 (8%)   ← worker-1待機中(無駄)
# node-d   8/96          8%    100Gi/384Gi     26%       1/1 (8%)   ← worker-2待機中(無駄)
# node-e   8/96          8%    100Gi/384Gi     26%       1/1 (8%)   ← worker-3待機中(無駄)
# node-f   8/96          8%    100Gi/384Gi     26%       1/1 (8%)   ← worker-4待機中(無駄)
# node-g   8/96          8%    100Gi/384Gi     26%       1/1 (8%)   ← worker-5待機中(無駄)
# node-h   8/96          8%    100Gi/384Gi     26%       1/1 (8%)   ← worker-6待機中(無駄)
# node-i   8/96          8%    100Gi/384Gi     26%       1/1 (8%)   ← worker-7待機中(無駄)
#                                                                    ↑ 7 GPUが無駄に占有

# 10. コスト計算
# 無駄なGPU: 7個
# 単価: $1-2/時間 × 7 = $7-14/時間
# high-priority-inferenceの実行時間が1時間の場合: $7-14の損失

ケース2: 複数PyTorchJobでの連鎖的プリエンプション

より深刻なシナリオです。複数のPyTorchJobが動作している環境で、1つの高優先度ジョブが投入されると、複数のジョブから少しずつWorkerが奪われ、すべてのジョブが学習停止状態に陥ります。

# 初期状態: 3つの低優先度PyTorchJobが動作中
kubectl get pytorchjob -n training
# NAME                    STATE     AGE   MASTER   WORKERS
# low-priority-train-1    Running   30m   1/1      8/8   ← GPU 9個使用
# low-priority-train-2    Running   25m   1/1      8/8   ← GPU 9個使用
# low-priority-train-3    Running   20m   1/1      8/8   ← GPU 9個使用
# 合計GPU使用: 27個

# クラスターGPU容量確認
kubectl get nodes -o json | jq -r '.items[] | select(.status.allocatable."nvidia.com/gpu" != null) | .metadata.name + ": " + .status.allocatable."nvidia.com/gpu"' | awk '{sum+=$2} END {print "Total GPUs: " sum}'
# Total GPUs: 32
# 利用可能GPU: 32 - 27 = 5個

# 高優先度PyTorchJobを投入(GPU 12個必要 = Master 1 + Workers 11)
cat <<EOF | kubectl apply -f -
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: high-priority-train
  namespace: training
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      template:
        spec:
          priorityClassName: high-priority  # value: 1000
          containers:
          - name: pytorch
            resources:
              limits:
                nvidia.com/gpu: 1
    Worker:
      replicas: 11
      template:
        spec:
          priorityClassName: high-priority  # value: 1000
          containers:
          - name: pytorch
            resources:
              limits:
                nvidia.com/gpu: 1
EOF

# 数秒後、連鎖的プリエンプション発生
kubectl get events -n training --sort-by='.lastTimestamp' | grep Preempted
# 8s  Normal  Preempted  pod/low-priority-train-1-worker-0  Preempted by training/high-priority-train-master-0
# 8s  Normal  Preempted  pod/low-priority-train-1-worker-1  Preempted by training/high-priority-train-worker-0
# 8s  Normal  Preempted  pod/low-priority-train-2-worker-0  Preempted by training/high-priority-train-worker-1
# 8s  Normal  Preempted  pod/low-priority-train-2-worker-1  Preempted by training/high-priority-train-worker-2
# 8s  Normal  Preempted  pod/low-priority-train-2-worker-2  Preempted by training/high-priority-train-worker-3
# 8s  Normal  Preempted  pod/low-priority-train-3-worker-0  Preempted by training/high-priority-train-worker-4
# 8s  Normal  Preempted  pod/low-priority-train-3-worker-1  Preempted by training/high-priority-train-worker-5
# ... (合計12個のWorkerが個別に停止)

# Pod状態確認
kubectl get pods -n training | grep -E "(train-1|train-2|train-3)" | grep worker
# low-priority-train-1-worker-0      0/1     Terminating   0          30m  ← 停止
# low-priority-train-1-worker-1      0/1     Terminating   0          30m  ← 停止
# low-priority-train-1-worker-2      1/1     Running       0          30m  ← 待機
# low-priority-train-1-worker-3      1/1     Running       0          30m  ← 待機
# low-priority-train-1-worker-4      1/1     Running       0          30m  ← 待機
# low-priority-train-1-worker-5      1/1     Running       0          30m  ← 待機
# low-priority-train-1-worker-6      1/1     Running       0          30m  ← 待機
# low-priority-train-1-worker-7      1/1     Running       0          30m  ← 待機
# low-priority-train-2-worker-0      0/1     Terminating   0          25m  ← 停止
# low-priority-train-2-worker-1      0/1     Terminating   0          25m  ← 停止
# low-priority-train-2-worker-2      0/1     Terminating   0          25m  ← 停止
# low-priority-train-2-worker-3      1/1     Running       0          25m  ← 待機
# low-priority-train-2-worker-4      1/1     Running       0          25m  ← 待機
# low-priority-train-2-worker-5      1/1     Running       0          25m  ← 待機
# low-priority-train-2-worker-6      1/1     Running       0          25m  ← 待機
# low-priority-train-2-worker-7      1/1     Running       0          25m  ← 待機
# low-priority-train-3-worker-0      0/1     Terminating   0          20m  ← 停止
# low-priority-train-3-worker-1      0/1     Terminating   0          20m  ← 停止
# low-priority-train-3-worker-2      1/1     Running       0          20m  ← 待機
# ... (以下同様)

# 結果分析:
# - 3つのPyTorchJobすべてが部分的に停止
# - train-1: 6 Workers待機(GPU 6個無駄)
# - train-2: 5 Workers待機(GPU 5個無駄)
# - train-3: 6 Workers待機(GPU 6個無駄)
# - 合計: 17 GPUが無駄に占有
# - コスト損失: $17-34/時間

# さらに、3つのMaster PodもWorkerを待っているため、合計20 GPUが無駄

ケース3: ノード障害後の再配置によるプリエンプション

ノード障害が発生すると、Podの再配置が必要になります。この過程でリソース競合が発生し、意図しないプリエンプションが引き起こされることがあります。

# 初期状態: PyTorchJobが正常動作中
kubectl get pods -n training -o wide | grep low-priority-training
# NAME                                READY   STATUS    NODE
# low-priority-training-master-0      1/1     Running   node-a
# low-priority-training-worker-0      1/1     Running   node-b
# low-priority-training-worker-1      1/1     Running   node-c
# low-priority-training-worker-2      1/1     Running   node-d
# low-priority-training-worker-3      1/1     Running   node-e
# low-priority-training-worker-4      1/1     Running   node-f
# low-priority-training-worker-5      1/1     Running   node-g
# low-priority-training-worker-6      1/1     Running   node-h
# low-priority-training-worker-7      1/1     Running   node-i

# ノード障害発生(例: node-bでハードウェア障害)
# (実際の環境では突然発生、ここでは模擬的にdrainする)
kubectl drain node-b --ignore-daemonsets --delete-emptydir-data --force

# ノード状態確認
kubectl get nodes
# NAME     STATUS                     ROLES    AGE
# node-a   Ready                      worker   10d
# node-b   Ready,SchedulingDisabled   worker   10d  ← drainされた
# node-c   Ready                      worker   10d
# ...

# worker-0が再配置される
kubectl get pods -n training -o wide | grep worker-0
# NAME                                READY   STATUS    NODE
# low-priority-training-worker-0      0/1     Pending   -   ← node-bから退避、再配置先探索中

# 再配置試行: node-c~iのいずれかに配置しようとする
# しかし、すでに各ノードにworker-1~7が配置されており、リソース不足

# 高優先度Podが存在する場合、プリエンプションが発生
kubectl get pods -n training | grep high-priority
# high-priority-inference-1  1/1     Running   0          node-c
# high-priority-inference-2  1/1     Running   0          node-d

# worker-0の再配置のために、worker-1または他の低優先度Podがプリエンプトされる可能性
kubectl get events -n training --sort-by='.lastTimestamp' | grep Preempted
# 5s  Normal  Preempted  pod/low-priority-training-worker-1  Preempted by training/low-priority-training-worker-0
# (worker-0を配置するためにworker-1が退避される)

# 結果: 
# - ノード障害により一時的にworker-0が停止
# - 再配置過程でworker-1もプリエンプト
# - 2つのWorkerが欠けた状態で、残り6 Workerが待機
# - GPU 6個が無駄に占有

preemptionPolicyの詳細解説

preemptionPolicyは、PriorityClassの重要なパラメータです。2つの設定値があり、それぞれ全く異なる動作をします。

2つの設定値の技術的な違い:

設定値 動作 Kubernetesスケジューラーの判断フロー
PreemptLowerPriority
(デフォルト)
このPodより低優先度のPodを退避可能 1. 高優先度Pod配置可能か判定
2. リソース不足の場合、低優先度Podを検索
3. 退避候補Podを1つずつ停止
4. リソース確保後、高優先度Podを配置
Never このPodは他Podを退避させない 1. 高優先度Pod配置可能か判定
2. リソース不足の場合、Pending状態で待機
3. プリエンプション実行しない
4. Kueueによる調整を待つ

コード例と動作比較:

# ❌ PreemptLowerPriority(デフォルト、分散学習では危険)
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: low-priority
value: 0
preemptionPolicy: PreemptLowerPriority  # ← デフォルト値
globalDefault: false
description: "低優先度Pod用"

動作フロー(PreemptLowerPriority):

# ✅ Never(推奨、分散学習では必須)
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: pytorchjob-zero-priority
value: 0
preemptionPolicy: Never  # ← 明示的に無効化
globalDefault: false
description: |
  PyTorchJob専用PriorityClass
  - Pod単位プリエンプションを無効化
  - Workload単位プリエンプションのみ使用
  - 全PyTorchJobで共通使用

  背景:
  分散学習では、一部Workerのみが停止すると残りのWorkerがGPUを
  占有したまま待機状態になり、リソースが無駄になる。
  preemptionPolicy: Never により、Pod単位での退避を防ぎ、
  Kueueによる Workload単位の制御のみを許可する。

動作フロー(Never):

Workload単位プリエンプション後のPod単位プリエンプションリスク【最重要】

この設定が絶対に必要な理由

多くの開発者が誤解している点として、「Workload単位プリエンプションを有効にすれば、Pod単位プリエンプションは自動的に無効になる」という認識があります。これは誤りです

実際には、Workload単位プリエンプションとPod単位プリエンプションは完全に独立したメカニズムであり、両方が同時に動作可能です。特に危険なのは、Workload単位プリエンプションが正常に完了した後でも、Pod単位プリエンプションが発生し得るという点です。

具体的なシナリオ

図の説明

この PlantUML シーケンス図は、Workload単位プリエンプション完了後も、Kubernetes SchedulerによるPod単位プリエンプションが別途発生する問題を視覚化しています。

  • T0: KueueがWorkload全体を正しく停止
  • T0+10秒: 終了処理中にKubernetes Schedulerが別のWorkloadから個別Podをプリエンプション
  • T0+15秒: 中途半端なPod構成が発生し、GPUが無駄に占有される

なぜこれが発生するのか

  1. Kueueの責任範囲はWorkload単位まで

    • KueueはWorkload全体の停止は制御できる
    • しかし、Kubernetes schedulerによる個別Pod停止は制御できない
  2. Kubernetes Schedulerは独立して動作

    • ノードレベルでのリソース不足を検出
    • PriorityClassの設定に基づいて個別Podをプリエンプション
    • Workloadの状態(Admitted/Deactivated)は考慮しない
  3. preemptionPolicy: PreemptLowerPriorityがデフォルト

    • 明示的にNeverを設定しない限り、Pod単位プリエンプションが有効
    • Workload単位プリエンプションとは無関係

実際の被害例

【ケース1:部分的なPod停止】
- Workload A (high priority): 正常動作中
- Workload B (medium priority): 正常動作中
  ↓
【高優先度Pod群が投入】
  ↓
【結果】
- Workload A: Worker-3だけが停止(preemptionPolicy未設定)
- Workload B: Worker-1,5だけが停止(preemptionPolicy未設定)
- 両方のジョブが中途半端に残る
- GPUリソース: 14 GPU無駄占有(A: 7個 + B: 7個)
- 時間損失: $14-28/時間

preemptionPolicy: Neverの保護メカニズム

この設定は二重の保護として機能します:

  1. Workload単位プリエンプション前の保護

    • Kubernetes Schedulerによる個別Pod停止を阻止
    • リソース不足時はPending状態で待機
    • Kueueによる調整を待つ
  2. Workload単位プリエンプション後の保護(これが重要!)

    • Workloadが停止した後も、他のWorkloadのPodは保護される
    • 新しい高優先度Podが投入されても、既存のWorkloadから個別Podが奪われない
    • Kueueによる次のWorkload単位プリエンプションを待つ

設定確認方法

# PriorityClassのpreemptionPolicy確認
kubectl get priorityclass pytorchjob-zero-priority -o jsonpath='{.preemptionPolicy}'
# 期待出力: Never

# 既存Podの設定確認
kubectl get pod training-worker-0 -o jsonpath='{.spec.priority},{.spec.preemptionPolicy}'
# 期待出力: 0,Never

# すべてのPyTorchJobのPod確認
kubectl get pods -n training -l job-type=pytorch -o jsonpath='{range .items[*]}{.metadata.name},{.spec.priorityClassName},{.spec.preemptionPolicy}{"
"}{end}'
# すべてのPodで "pytorchjob-zero-priority,Never" を確認

まとめ

  • preemptionPolicy: NeverはWorkload単位プリエンプションとは独立した保護機構
  • ✅ Workload単位プリエンプション後も、Pod単位プリエンプションは別途発生可能
  • ✅ 両方の設定を必ず組み合わせることで、完全な保護を実現
  • ❌ Workload単位プリエンプションだけでは不十分

この理解なしに分散学習環境を運用すると、中途半端なPod停止によるGPUリソースの無駄が頻発し、運用コストが大幅に増加します。

SIGTERM/SIGKILL処理とWorkload解放のタイミング【重要】

検証実験で明らかになった重要な技術的詳細として、SIGTERM時点でKueueがworkloadを解放するという仕組みがあります。これはpreemptionPolicy: Never設定の効果を理解する上で極めて重要です。

Podプリエンプション時の詳細なシーケンス:

図の説明

この PlantUML シーケンス図は、Pod プリエンプション時の SIGTERM/SIGKILL 処理とKueueによるWorkload解放の詳細なタイミングを示しています。

重要ポイント

  • T0+即座: KueueはSIGTERM時点で即座にworkloadを解放(SIGKILLまで待たない)
  • T0+0〜30秒: アプリケーションはGraceful shutdown処理を実行
  • T0+30秒: terminationGracePeriodSeconds経過後、必要に応じてSIGKILL

この仕組みにより、リソース利用効率が向上し、Workload単位プリエンプションが実現されます。

terminationGracePeriodSeconds推奨設定

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60  # チェックポイント保存に十分な時間

なぜSIGTERM時点での解放が重要か:

  1. リソース利用効率の向上

    • SIGKILLまで待つと最大30秒のリソース無駄
    • SIGTERM時点で解放により、即座に他Workloadが利用可能
  2. Workload単位プリエンプションの実現

    • Kueueが全PodのTerminatingを検知
    • Workload全体を素早く解放
    • 新しいWorkloadをすぐにAdmit可能
  3. Graceful shutdownの保証

    • SIGTERMで適切な終了処理が実行される
    • チェックポイント保存などが正常に完了
    • 学習再開時のデータ整合性を保証

terminationGracePeriodSecondsの推奨設定:

# ✅ 推奨設定: PyTorchJob用
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: production-training
spec:
  pytorchReplicaSpecs:
    Worker:
      template:
        spec:
          terminationGracePeriodSeconds: 60  # チェックポイント保存に十分な時間
          containers:
          - name: pytorch
            # アプリケーション側でSIGTERMハンドリング実装

アプリケーション側のシグナルハンドリング例:

import signal
import sys
import torch
import logging
import traceback

logger = logging.getLogger(__name__)

class GracefulShutdownHandler:
    """SIGTERM受信時のGraceful shutdown処理"""

    def __init__(self, model, optimizer, checkpoint_path):
        self.model = model
        self.optimizer = optimizer
        self.checkpoint_path = checkpoint_path
        self.should_stop = False
        self.current_epoch = 0
        self.current_step = 0

        # SIGTERMハンドラー登録
        signal.signal(signal.SIGTERM, self._handle_sigterm)

    def _handle_sigterm(self, signum, frame):
        """SIGTERM受信時の処理"""
        logger.info("Received SIGTERM, initiating graceful shutdown...")

        # 1. 現在のイテレーション完了を待つフラグ設定
        self.should_stop = True

        # 2. チェックポイント保存
        try:
            logger.info(f"Saving checkpoint to {self.checkpoint_path}")
            torch.save({
                'model_state_dict': self.model.state_dict(),
                'optimizer_state_dict': self.optimizer.state_dict(),
                'epoch': self.current_epoch,
                'step': self.current_step,
            }, self.checkpoint_path)
            logger.info("Checkpoint saved successfully")
        except Exception:
            logger.error(f"Failed to save checkpoint: {traceback.format_exc()}")

        # 3. 分散グループから離脱
        if torch.distributed.is_initialized():
            logger.info("Destroying process group...")
            torch.distributed.destroy_process_group()

        # 4. 正常終了
        logger.info("Graceful shutdown completed")
        sys.exit(0)

# 学習ループでの使用例
def train():
    handler = GracefulShutdownHandler(model, optimizer, checkpoint_path)

    for epoch in range(num_epochs):
        handler.current_epoch = epoch
        for step, batch in enumerate(dataloader):
            handler.current_step = step

            # Graceful shutdown要求確認
            if handler.should_stop:
                logger.info("Stopping training due to SIGTERM")
                break

            # 通常の学習処理
            loss = model(batch)
            loss.backward()
            optimizer.step()

重要な実装ポイント:

  1. イテレーション途中での中断を避ける

    • SIGTERM受信後、現在のイテレーション完了を待つ
    • dist.all_reduce()などの集団通信が途中で切れないようにする
  2. チェックポイント保存の優先

    • SIGTERM受信後、最優先でチェックポイント保存
    • terminationGracePeriodSeconds内に完了させる
  3. 分散グループの適切な離脱

    • torch.distributed.destroy_process_group()を呼び出す
    • 他Workerが待機状態にならないようにする
  4. ログ出力の充実

    • Graceful shutdown開始、完了を明確にログ出力
    • デバッグ時に動作確認できる様にする

実験・検証で判明したバグ再現条件:

以下の条件が揃うと、問題が発生することが報告されています:

  1. Pod単位でのpreemption発生時

    • preemptionPolicy: PreemptLowerPriorityが設定されている
    • 高優先度Podが投入され、一部Workerのみがプリエンプトされる
  2. Workload開始時間とPod開始時間の関係性

    • Workloadは全Pod揃ってからAdmitted状態になる
    • 一部Podがプリエンプトされると、Workloadは部分的に実行状態
  3. SIGTERM無視設定の影響

    • アプリケーションがSIGTERMを適切にハンドリングしていない
    • terminationGracePeriodSeconds経過後にSIGKILL
    • チェックポイント未保存のまま強制終了

これらの条件下では、以下の問題が発生します:

  • Workloadは「実行中」だがPodは「Terminating」
  • 残りのWorkerがdist.all_reduce()で永遠に待機
  • GPUリソースが無駄に占有される
  • 学習が再開しない

解決策サマリー:

  • preemptionPolicy: Never設定により、Pod単位プリエンプションを完全に無効化
  • ✅ Workload単位でのみプリエンプションが発生するようにする
  • ✅ 適切なSIGTERMハンドリング実装により、Graceful shutdownを保証
  • ✅ terminationGracePeriodSeconds を十分な値(60秒推奨)に設定

解決策の詳細

この問題を根本的に解決するには、以下の手順を実施します。

ステップ1: PriorityClass作成

# ファイル: pytorchjob-zero-priority.yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: pytorchjob-zero-priority
value: 0
preemptionPolicy: Never  # ← 重要: Pod単位プリエンプション無効化
globalDefault: false
description: |
  PyTorchJob専用PriorityClass
  - Pod単位プリエンプションを無効化
  - Workload単位プリエンプションのみ使用
  - 全PyTorchJobで共通使用

  設定理由:
  分散学習(PyTorchJob, MPIJob等)では、一部Workerのみが停止すると
  残りのWorkerがGPUを占有したまま待機状態になり、リソースが無駄になる。
  
  preemptionPolicy: Never により、Kubernetes標準のPod単位プリエンプション
  を無効化し、Kueueによる Workload単位の制御のみを許可する。
  
  これにより、以下を実現:
  1. Master + Worker全Podが同時に停止/起動(Atomic Preemption)
  2. 部分的なPod停止による無駄なGPU占有を回避
  3. 一貫したリソース管理とコスト効率の向上
  
  注意:
  - このPriorityClassは全PyTorchJobで共通使用することを推奨
  - value=0 により、Kubernetes標準の優先度判定では最低優先度
  - Workload間の優先順位はWorkloadPriorityClassで制御
# 適用
kubectl apply -f pytorchjob-zero-priority.yaml

# 確認
kubectl get priorityclass pytorchjob-zero-priority -o yaml

# 重要なフィールド確認
kubectl get priorityclass pytorchjob-zero-priority -o jsonpath='{.preemptionPolicy}'
# 期待出力: Never

ステップ2: PyTorchJob設定

# ✅ 正しい設定: preemptionPolicy: Never を使用
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: production-training
  namespace: training
  labels:
    kueue.x-k8s.io/queue-name: training-queue
    kueue.x-k8s.io/priority-class: kueue-high  # Workload優先度(ジョブごと)
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # ← Pod優先度(全ジョブ統一)
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime
            command: ["python", "train.py"]
            resources:
              limits:
                nvidia.com/gpu: 1
    Worker:
      replicas: 8
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # ← Pod優先度(全ジョブ統一)
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime
            command: ["python", "train.py"]
            resources:
              limits:
                nvidia.com/gpu: 1

重要なポイント:

  1. 全PyTorchJobで同じPriorityClass:

    • すべてのPyTorchJobでpytorchjob-zero-priorityを使用
    • Pod単位での優先度差をなくし、プリエンプションを防ぐ
  2. WorkloadPriorityClassでジョブ間の優先度を制御:

    • kueue-high, kueue-medium, kueue-lowなどを使い分け
    • Kueueレベルでジョブ全体の優先順位を管理

検証方法

設定が正しく適用されているか、以下の手順で検証します。

検証1: preemptionPolicy確認

# PriorityClassの設定確認
kubectl get priorityclass pytorchjob-zero-priority -o jsonpath='{.preemptionPolicy}'
# 期待出力: Never

# 詳細確認
kubectl describe priorityclass pytorchjob-zero-priority
# Name:              pytorchjob-zero-priority
# Value:             0
# GlobalDefault:     false
# PreemptionPolicy:  Never  # ← ここを確認
# Description:       PyTorchJob専用PriorityClass...

検証2: 全PodのPriorityClass統一確認

# 特定のPyTorchJobの全Podを確認
kubectl get pods -n training -l pytorch-job-name=production-training \
  -o custom-columns=\
NAME:.metadata.name,\
PRIORITY-CLASS:.spec.priorityClassName,\
PRIORITY-VALUE:.spec.priority,\
STATUS:.status.phase

# 期待出力:
# NAME                                PRIORITY-CLASS              PRIORITY-VALUE   STATUS
# production-training-master-0        pytorchjob-zero-priority    0                Running
# production-training-worker-0        pytorchjob-zero-priority    0                Running
# production-training-worker-1        pytorchjob-zero-priority    0                Running
# production-training-worker-2        pytorchjob-zero-priority    0                Running
# ...

# すべて同じPriorityClass(pytorchjob-zero-priority)と
# 同じ優先度値(0)であることを確認

検証3: プリエンプション動作テスト

実際にプリエンプションが発生しないことを確認します。

# 1. 低優先度PyTorchJob起動
kubectl apply -f production-training.yaml

# 2. Podが全て起動していることを確認
kubectl get pods -n training -l pytorch-job-name=production-training
# すべてRunning状態

# 3. 高優先度Pod投入(GPU 1個必要、priority: 1000)
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Pod
metadata:
  name: high-priority-test-pod
  namespace: training
spec:
  priorityClassName: high-priority  # value: 1000, preemptionPolicy: PreemptLowerPriority
  containers:
  - name: test
    image: nvidia/cuda:11.8.0-base-ubuntu22.04
    command: ["sleep", "3600"]
    resources:
      limits:
        nvidia.com/gpu: 1
EOF

# 4. プリエンプション未発生を確認
kubectl get events -n training --sort-by='.lastTimestamp' | grep Preempted
# (出力なし = プリエンプション発生していない)

# 5. 高優先度PodがPending状態であることを確認
kubectl get pod high-priority-test-pod -n training
# NAME                      READY   STATUS    RESTARTS   AGE
# high-priority-test-pod   0/1     Pending   0          30s

# 6. Pending理由を確認
kubectl describe pod high-priority-test-pod -n training | grep -A 10 Events
# Events:
#   Type     Reason            Age   From               Message
#   ----     ------            ----  ----               -------
#   Warning  FailedScheduling  25s   default-scheduler  0/9 nodes are available: 
#                                                        9 Insufficient nvidia.com/gpu.
#   Normal   NotTriggerScaleUp 20s   cluster-autoscaler Pod didn't trigger scale-up

# → 期待通り: preemptionPolicy: Neverにより、プリエンプションが発生せず
#            Pending状態で待機している

# 7. Kueueによる Workload単位調整を待つ
# (Kueueが設定されている場合、低優先度Workload全体が停止され、
#  高優先度Podが起動可能になる)

コスト影響の定量化

Pod単位プリエンプションによる損失を具体的な数値で示します。

GPU無駄による損失計算:

【前提条件】
- GPU単価: $1-2/時間(NVIDIA A100想定)
- PyTorchJob構成: Master(1) + Worker(8) = 9 GPU
- プリエンプション発生: Worker 1個停止
- 待機状態: Worker 8個(Master含む9個中、実質8個が無駄)

【Pod単位プリエンプション発生時の損失】
無駄なGPU: 8個(停止したWorker以外のMaster + Workers)
時間単価: $1-2/時間 × 8 = $8-16/時間
1日(24時間)の損失: $8-16 × 24 = $192-384/日
1ヶ月(30日)の損失: $192-384 × 30 = $5,760-11,520/月

【Workload単位プリエンプション時】
無駄なGPU: 0個(全Pod同時停止/起動)
損失: $0

実際のインシデント例(実環境で発生した事例):

【発生事象】
- 本番環境: 20個のPyTorchJob(各8 GPU)が動作中
- 設定ミス: preemptionPolicy未指定(デフォルトのPreemptLowerPriorityが適用)
- トリガー: 高優先度推論Pod群(30個)が一斉投入
- 結果: 各PyTorchJobから1-2個のWorkerがプリエンプト
- 合計被害: 30個のWorkerが停止、残り130-140個のWorkerが待機状態

【損失計算】
停止Worker: 30個(新しい推論Podに置き換わった)
待機Worker: 130個(無駄に占有)← 問題
時間損失: $130-260/時間
発見までの時間: 12時間(夜間発生、翌朝発見)
総損失: $1,560-3,120

【対策実施後】
- pytorchjob-zero-priorityを全環境に展開
- WorkloadPriorityClassによるジョブ間優先度制御に切り替え
- 同様の問題は以降発生していない

複数ジョブでの連鎖的損失:

【シナリオ】
3つのPyTorchJob(各9 GPU: Master 1 + Workers 8)が動作中
高優先度PyTorchJob(12 GPU必要)が投入される

【Pod単位プリエンプション時】
- Job1: Worker 0,1 停止 → 残り7 Worker待機 → 7 GPU無駄
- Job2: Worker 0,1,2 停止 → 残り6 Worker待機 → 6 GPU無駄
- Job3: Worker 0,1,2,3,4,5,6 停止 → 残り2 Worker待機 → 2 GPU無駄
- 合計無駄: 15 GPU
- 時間損失: $15-30/時間
- 24時間損失: $360-720

【Workload単位プリエンプション時】
- Job1, Job2は継続実行(影響なし)
- Job3全体が停止(9 GPU解放)
- 残り3 GPUで他Jobから一部停止
- 無駄なGPU: 0個
- 損失: $0

この定量化により、preemptionPolicy: Never の設定がいかに重要かが明確になります。

罠2:優先度の不整合

WorkloadPriorityClassとPriorityClassの値が異なる設定をすると、予期しない動作が発生します。

問題の詳細

不整合パターン1: WorkloadPriority高、PodPriority低

# ❌ 不整合な設定例
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: inconsistent-job
  labels:
    kueue.x-k8s.io/queue-name: training-queue
    kueue.x-k8s.io/priority-class: kueue-high  # WorkloadPriority: 100
spec:
  pytorchReplicaSpecs:
    Worker:
      template:
        spec:
          priorityClassName: low-priority  # PodPriority: 0
          # ...

発生する問題:

  1. Kueueレベル(Workload Admission):

    • 高優先度として扱われる(priority: 100
    • Queue内で優先的にAdmitされる
    • 低優先度Workloadを押しのけて早期に起動
  2. Kubernetesレベル(Pod Scheduling):

    • Admit後のPodは低優先度(priority: 0
    • ノード上で他の高優先度Podに退避される可能性
    • せっかく早期Admitされたのに、Pod配置で遅延

実例シナリオ:

# 状況設定
# ClusterQueue: GPU 10個(8個使用中、2個空き)
# 待機中のWorkload:
# - medium-job (WorkloadPriority: 50, PodPriority: 500)  # GPU 5個必要
# - inconsistent-job (WorkloadPriority: 100, PodPriority: 0)  # GPU 3個必要

# 1. Kueueの判断(WorkloadPriorityで比較)
# inconsistent-job (100) > medium-job (50)
# → inconsistent-jobを先にAdmit

# 2. inconsistent-jobのPod作成開始
kubectl get pods -n training | grep inconsistent
# NAME                              READY   STATUS    AGE
# inconsistent-job-worker-0         0/1     Pending   5s
# inconsistent-job-worker-1         1/1     Running   5s
# inconsistent-job-worker-2         0/1     Pending   5s

# 3. 一部Podがノード配置に失敗
kubectl describe pod inconsistent-job-worker-0 -n training
# Events:
#   Warning  FailedScheduling  10s   default-scheduler  
#            0/10 nodes are available: 3 node(s) had untolerated taint,
#            7 node(s) didn't match Pod's node affinity/selector.

# 4. medium-jobのPodが後から投入されるが、PodPriority高いため優先配置
kubectl get events -n training --sort-by='.lastTimestamp' | grep Preempted
# Normal  Preempted  pod/inconsistent-job-worker-1  
#         Preempted by training/medium-job-worker-0

# 結果:
# - Kueueでは高優先度だったinconsistent-jobが、
#   Kubernetesレベルで低優先度のためプリエンプトされる
# - 優先度制御が機能していない

不整合パターン2: WorkloadPriority低、PodPriority高

# ❌ 別の不整合例
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  labels:
    kueue.x-k8s.io/priority-class: kueue-low  # WorkloadPriority: 0
spec:
  pytorchReplicaSpecs:
    Worker:
      template:
        spec:
          priorityClassName: high-priority  # PodPriority: 1000

発生する問題:

  1. Kueueでは低優先度として後回し
  2. しかしAdmit後のPodは高優先度で、他のPodを退避させる可能性
  3. Kueueの管理意図と実際の動作が乖離

解決策

推奨アプローチ: 役割の明確な分離

# ✅ 正しい設定パターン
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: production-training
  labels:
    kueue.x-k8s.io/queue-name: training-queue
    kueue.x-k8s.io/priority-class: kueue-high  # Workload優先度(ジョブごとに変える)
spec:
  pytorchReplicaSpecs:
    Worker:
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # Pod優先度(全ジョブ統一)
          # ...

設計方針:

  1. WorkloadPriorityClass: ジョブごとに設定

    • kueue-high: 本番学習ジョブ
    • kueue-medium: 実験ジョブ
    • kueue-low: バックグラウンド処理
  2. PriorityClass: 全ジョブで統一

    • pytorchjob-zero-priority: すべてのPyTorchJobで使用
    • value: 0, preemptionPolicy: Never
  3. 優先度制御の責務分担:

    • Kueue: ジョブ間の優先順位(WorkloadPriorityで制御)
    • Kubernetes: プリエンプション無効化(PriorityClassで無効化)

検証方法:

# 1. WorkloadPriority確認
kubectl get workload -n training production-training-xxx -o yaml | grep -A 3 priority
# priority: 100
# priorityClassName: kueue-high
# priorityClassSource: kueue.x-k8s.io/workloadpriorityclass

# 2. PodPriority確認
kubectl get pod production-training-worker-0 -n training -o yaml | grep -A 2 priority
# priority: 0
# priorityClassName: pytorchjob-zero-priority

# 3. 全Podで統一確認
kubectl get pods -n training -o jsonpath='{range .items[*]}{.metadata.name}{"	"}{.spec.priorityClassName}{"
"}{end}' | grep -v pytorchjob-zero-priority
# (出力なし = すべて pytorchjob-zero-priority)

罠3:globalDefaultの意図しない影響

クラスター内にglobalDefault: trueのPriorityClassが存在すると、明示的に指定しない場合、意図しない優先度が設定されます。

問題の詳細

# クラスター内のPriorityClass確認
kubectl get priorityclass
# NAME                      VALUE        GLOBAL-DEFAULT   AGE
# system-cluster-critical   2000000000   false            100d
# system-node-critical      2000001000   false            100d
# default-priority          100          true             50d  ← globalDefault
# pytorchjob-zero-priority  0            false            1d

問題のシナリオ:

# ❌ priorityClassName未指定
apiVersion: kubeflow.org/v1
kind: PyTorchJob
spec:
  pytorchReplicaSpecs:
    Worker:
      template:
        spec:
          # priorityClassName未指定
          # → default-priority (value: 100, preemptionPolicy: PreemptLowerPriority) が自動設定
          containers:
          - name: pytorch
            # ...
# 作成されたPodの優先度確認
kubectl get pod <pod-name> -o jsonpath='{.spec.priority}'
# 出力: 100(期待値: 0)

kubectl get pod <pod-name> -o jsonpath='{.spec.priorityClassName}'
# 出力: default-priority(期待値: pytorchjob-zero-priority)

# preemptionPolicy確認
kubectl get priorityclass default-priority -o jsonpath='{.preemptionPolicy}'
# 出力: PreemptLowerPriority(危険!)

この状態では、Pod単位プリエンプションが有効になり、罠1の問題が発生します。

解決策

必ず明示的に指定:

# ✅ priorityClassNameを必ず明示的に指定
apiVersion: kubeflow.org/v1
kind: PyTorchJob
spec:
  pytorchReplicaSpecs:
    Master:
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # ← 必ず指定
    Worker:
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # ← 必ず指定

検証方法:

# 1. クラスター内のglobalDefault確認
kubectl get priorityclass -o custom-columns=\
NAME:.metadata.name,\
VALUE:.value,\
GLOBAL-DEFAULT:.globalDefault,\
PREEMPTION-POLICY:.preemptionPolicy

# 2. 全PyTorchJobのPodで正しいPriorityClass使用確認
kubectl get pods -n training -l app=pytorch-training \
  -o jsonpath='{range .items[*]}{.metadata.name}{"	"}{.spec.priorityClassName}{"	"}{.spec.priority}{"
"}{end}' \
  | awk '$2 != "pytorchjob-zero-priority" {print "❌ WRONG: " $0}'
# (出力なし = すべて正しい)

# 3. PyTorchJob YAMLテンプレートでの強制チェック(推奨)
# Admission Webhook等で、priorityClassNameの明示的指定を強制

自動化: Admission Controllerでの強制

# ValidatingWebhookConfiguration例(参考)
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
  name: pytorchjob-priorityclass-enforcer
webhooks:
- name: validate-pytorchjob-priorityclass.example.com
  rules:
  - apiGroups: ["kubeflow.org"]
    apiVersions: ["v1"]
    operations: ["CREATE", "UPDATE"]
    resources: ["pytorchjobs"]
  clientConfig:
    service:
      name: webhook-service
      namespace: kueue-system
      path: "/validate-pytorchjob"
  admissionReviewVersions: ["v1"]
  sideEffects: None

Webhookで以下をチェック:

  1. spec.pytorchReplicaSpecs.*.template.spec.priorityClassNameが設定されているか
  2. 設定されている場合、pytorchjob-zero-priorityであるか

ベストプラクティス

ここまでの内容を踏まえ、推奨される設定パターンと運用方針をまとめます。

推奨アーキテクチャ

全体構成:

┌─────────────────────────────────────────────┐
│ PyTorchJob/MPIJob(分散学習ジョブ)          │
├─────────────────────────────────────────────┤
│ metadata:                                   │
│   labels:                                   │
│     kueue.x-k8s.io/priority-class: kueue-X │ ← Workload優先度(ジョブごと)
│                                             │
│ spec:                                       │
│   pytorchReplicaSpecs:                      │
│     Master/Worker:                          │
│       template:                             │
│         spec:                               │
│           priorityClassName: pytorchjob-zero│ ← Pod優先度(全ジョブ統一)
└─────────────────────────────────────────────┘

設計原則:

  1. Pod単位プリエンプション完全無効化

    • 全PyTorchJobでpytorchjob-zero-priorityvalue: 0, preemptionPolicy: Never
    • Pod間での優先度差をなくす
  2. Workload単位プリエンプション有効化

    • ジョブごとにkueue-high/medium/lowを設定
    • ClusterQueueでpreemption.withinClusterQueue: LowerPriority

完全な設定例

1. Kubernetes PriorityClass

# ファイル: priority-classes.yaml
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: pytorchjob-zero-priority
value: 0
preemptionPolicy: Never  # Pod単位プリエンプション無効化
globalDefault: false
description: |
  PyTorchJob/MPIJob専用PriorityClass
  - Pod単位プリエンプションを完全無効化
  - Workload単位プリエンプションのみ使用
  - 全分散学習ジョブで共通使用
  
  理由:
  分散学習では一部Podのみ停止するとリソース無駄。
  preemptionPolicy: Neverでこれを防ぐ。
kubectl apply -f priority-classes.yaml

2. Kueue WorkloadPriorityClass

# ファイル: workload-priority-classes.yaml
---
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: kueue-high
value: 100
description: "本番学習ジョブ用(高優先度)"

---
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: kueue-medium
value: 50
description: "実験ジョブ用(中優先度)"

---
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: kueue-low
value: 0
description: "バックグラウンド処理用(低優先度)"
kubectl apply -f workload-priority-classes.yaml

3. ClusterQueue

# ファイル: cluster-queue.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: gpu-cluster-queue
spec:
  preemption:
    withinClusterQueue: LowerPriority  # Workload単位プリエンプション有効
    reclaimWithinCohort: Any
    borrowWithinCohort:
      policy: LowerPriority
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu", "cpu", "memory"]
    flavors:
    - name: gpu-flavor
      resources:
      - name: "nvidia.com/gpu"
        nominalQuota: 32
      - name: "cpu"
        nominalQuota: 256
      - name: "memory"
        nominalQuota: 1024Gi
kubectl apply -f cluster-queue.yaml

4. LocalQueue

# ファイル: local-queue.yaml
apiVersion: kueue.x-k8s.io/v1beta1
kind: LocalQueue
metadata:
  name: training-queue
  namespace: training
spec:
  clusterQueue: gpu-cluster-queue
kubectl apply -f local-queue.yaml

5. PyTorchJob(本番用)

# ファイル: production-training.yaml
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: production-training
  namespace: training
  labels:
    kueue.x-k8s.io/queue-name: training-queue
    kueue.x-k8s.io/priority-class: kueue-high  # Workload優先度: 高
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # Pod優先度: 統一
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime
            command: ["python", "train.py"]
            args: ["--config", "prod-config.yaml"]
            resources:
              requests:
                nvidia.com/gpu: 1
                cpu: 8
                memory: 32Gi
              limits:
                nvidia.com/gpu: 1
                cpu: 8
                memory: 32Gi
    Worker:
      replicas: 8
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # Pod優先度: 統一
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0.1-cuda11.8-cudnn8-runtime
            command: ["python", "train.py"]
            args: ["--config", "prod-config.yaml"]
            resources:
              requests:
                nvidia.com/gpu: 1
                cpu: 8
                memory: 32Gi
              limits:
                nvidia.com/gpu: 1
                cpu: 8
                memory: 32Gi

6. PyTorchJob(実験用)

# ファイル: experiment-training.yaml
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: experiment-training
  namespace: training
  labels:
    kueue.x-k8s.io/queue-name: training-queue
    kueue.x-k8s.io/priority-class: kueue-medium  # Workload優先度: 中
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # Pod優先度: 統一
          # ... (以下同様)
    Worker:
      replicas: 4
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # Pod優先度: 統一
          # ...

まとめ

重要ポイントの再確認

本記事で解説した内容を振り返ります。

1. 2つのPriorityClassは別物

  • WorkloadPriorityClass (Kueue): ジョブ全体の優先順位を制御、Workload単位でプリエンプション
  • PriorityClass (Kubernetes): Pod個別の優先度を制御、Pod単位でプリエンプション
  • これらは独立した2段階の仕組みで、それぞれ異なるタイミングで動作

2. 分散学習では2段階制御が必須

  • 1段階目(Kueueレベル): WorkloadPriorityでジョブ間の優先順位を決定
  • 2段階目(Kubernetesレベル): PriorityClassで Pod単位プリエンプションを無効化
  • この組み合わせにより、Workload全体が一貫して制御される

3. 最も危険な罠: Pod単位プリエンプション

  • デフォルト設定(preemptionPolicy: PreemptLowerPriority)では、一部Workerのみが停止
  • 残りのWorkerがGPUを占有したまま待機状態になり、莫大なコスト損失
  • 解決策: preemptionPolicy: Neverで完全無効化

推奨設定まとめ

すべての分散学習ジョブで実施すべき設定:

# ✅ 1. Kubernetes PriorityClass(全ジョブ統一)
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: pytorchjob-zero-priority
value: 0
preemptionPolicy: Never  # ← Pod単位プリエンプション無効化

---
# ✅ 2. Kueue WorkloadPriorityClass(ジョブごと)
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: kueue-high  # または kueue-medium, kueue-low
value: 100  # ジョブ優先度に応じて

---
# ✅ 3. ClusterQueue(Workload単位プリエンプション有効化)
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
spec:
  preemption:
    withinClusterQueue: LowerPriority  # ← Workload単位プリエンプション

---
# ✅ 4. PyTorchJob設定
apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  labels:
    kueue.x-k8s.io/priority-class: kueue-high  # Workload優先度
spec:
  pytorchReplicaSpecs:
    Master/Worker:
      template:
        spec:
          priorityClassName: pytorchjob-zero-priority  # Pod優先度(統一)

【重要】避けられない既知の問題: プリエンプション中のリソース解放によるジョブ失敗

これまで解説してきた設定をすべて正しく実装しても、完全には回避できない問題が1つ存在します。それは「プリエンプション中にリソースが解放されると、プリエンプション中のジョブが失敗する」という問題です。

問題の概要

この問題は以下のタイミングで発生します:

【発生条件】
1. LowJobA(低優先度)が実行中
2. HighJob(高優先度)が投入される
3. Kueueが LowJobA をWorkload単位でプリエンプション開始
4. LowJobA の全Podが Terminating 状態に移行(まだ物理的に存在)
5. 【タイミング問題】別のJobB が正常終了し、リソースが解放される
6. Kueueが空きリソースを検出し、LowJobA を即座に再起動しようとする
7. 【失敗】既存のTerminating状態のPodと名前が衝突し、ジョブが Failed に

重要: この問題は、正しい設定(preemptionPolicy: Never, withinClusterQueue: LowerPriority)をしていても発生します。

詳細なシーケンス図

なぜこの問題が避けられないのか

1. Graceful Shutdown期間の必要性

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 60  # チェックポイント保存に必要
      containers:
      - name: training
        # ...

目的:

  • モデルチェックポイントの保存
  • 分散学習状態の同期
  • ログの確実な書き込み

問題:

  • この期間中、PodはTerminating状態で物理的に存在し続ける
  • リソース(GPU)も占有したまま
  • 60秒間は名前衝突が発生し得る

2. Kueueの即座の再割り当てロジック

【Kueueの動作】
リソース解放イベント検出
  ↓
待機中のWorkloadをスキャン
  ↓
最優先のWorkloadを選択
  ↓
即座にAdmissionを試みる

目的:

  • リソース利用率の最大化
  • 待機時間の最小化
  • 効率的なスケジューリング

問題:

  • プリエンプション中(Terminating)のWorkloadも「待機中」と判断される可能性
  • Terminating Podが完全に削除される前に再起動を試みる
  • タイミング次第で名前衝突

3. Kubernetes Pod名の一意性制約

【Kubernetes制約】
同一Namespace内で同じ名前のPodは1つのみ存在可能

【状態】
- 既存Pod: training-job-worker-0 (Terminating)
- 新規Pod: training-job-worker-0 (Creating) ← 衝突!

結果:

  • APIサーバーが"already exists"エラーを返す
  • ジョブコントローラーがFailed状態に遷移

発生頻度と影響範囲

発生条件(すべて同時に満たす必要):

  1. ✅ プリエンプションが実行される
  2. ✅ Terminating期間中(0-60秒)に別ジョブが終了
  3. ✅ プリエンプション中のWorkloadが最優先の待機中Workload
  4. ✅ 解放されたリソースが、そのWorkloadに十分

発生頻度の目安:

クラスタ規模 ジョブ数 プリエンプション頻度 推定発生率
小規模(GPU 16個) 5-10ジョブ 1回/日 月1-2回
中規模(GPU 64個) 20-30ジョブ 5回/日 月5-10回
大規模(GPU 256個) 50-100ジョブ 20回/日 月20-30回

影響:

  • データ損失なし: チェックポイントは保存済み(Graceful shutdownが完了している場合)
  • ⚠️ ジョブ再投入が必要: 手動またはリトライポリシーで対応
  • ⚠️ 一時的なリソースロック: Terminating Podがリソースを占有し続ける

現実的な対処法

この問題を完全に解決することは現状不可能ですが、以下の対処法で影響を最小化できます。

対処法1: terminationGracePeriodSecondsの短縮(トレードオフあり)

spec:
  template:
    spec:
      terminationGracePeriodSeconds: 10  # 60秒 → 10秒に短縮
      containers:
      - name: training
        lifecycle:
          preStop:
            exec:
              command:
              - /bin/sh
              - -c
              - |
                # 高速チェックポイント保存(10秒以内)
                python save_checkpoint.py --quick-save

メリット:

  • 名前衝突のウィンドウ期間が短くなる(60秒 → 10秒)
  • 問題発生確率が約1/6に減少

デメリット:

  • ⚠️ チェックポイント保存が間に合わない可能性
  • ⚠️ 大規模モデルでは不十分
  • ⚠️ 10秒で完了しない場合、SIGKILLで強制終了

推奨:

  • 小規模モデル(<1GB): 5-10秒
  • 中規模モデル(1-10GB): 30秒
  • 大規模モデル(>10GB): 60秒以上

対処法2: Jobリトライポリシーの設定

apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: training-with-retry
spec:
  # リトライポリシー設定
  runPolicy:
    backoffLimit: 3  # 最大3回まで自動リトライ
    cleanPodPolicy: Running  # 失敗時もRunning Podを保持
  pytorchReplicaSpecs:
    # ...

動作:

1. ジョブがFailedになる
   ↓
2. PyTorchJob Operatorが自動的に再投入
   ↓
3. backoffLimit回まで繰り返す
   ↓
4. 成功するか、上限に達するまで継続

メリット:

  • ✅ 手動介入不要
  • ✅ 一時的な問題を自動回復
  • ✅ チェックポイントから再開可能

デメリット:

  • ⚠️ 根本解決ではない
  • ⚠️ リトライ回数の上限あり
  • ⚠️ 永続的な問題には無効

対処法3: リソースプールの分離(高度な運用)

---
# プリエンプション専用ClusterQueue
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: preemptible-gpu-queue
spec:
  preemption:
    withinClusterQueue: LowerPriority  # プリエンプション有効
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu"]
    flavors:
    - name: preemptible-gpu-flavor
      resources:
      - name: "nvidia.com/gpu"
        nominalQuota: 48  # 合計64個のうち48個

---
# 安定実行専用ClusterQueue(プリエンプションなし)
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
  name: stable-gpu-queue
spec:
  preemption:
    withinClusterQueue: Never  # プリエンプション無効
  resourceGroups:
  - coveredResources: ["nvidia.com/gpu"]
    flavors:
    - name: stable-gpu-flavor
      resources:
      - name: "nvidia.com/gpu"
        nominalQuota: 16  # 合計64個のうち16個

運用方針:

  • preemptible-gpu-queue: 実験ジョブ、短時間ジョブ(プリエンプション許容)
  • stable-gpu-queue: 本番ジョブ、長時間ジョブ(プリエンプション禁止)

メリット:

  • ✅ 重要ジョブを問題から完全に隔離
  • ✅ リソース利用率とジョブ安定性のバランス
  • ✅ 問題発生時の影響範囲を限定

デメリット:

  • ⚠️ リソースの静的な分割が必要
  • ⚠️ 柔軟性が低下
  • ⚠️ 運用が複雑化

対処法4: モニタリングとアラート

# Prometheusアラートルール例
groups:
- name: kueue-preemption-race-condition
  rules:
  - alert: JobFailedDuringTerminating
    expr: |
      increase(kueue_workload_eviction_total[5m]) > 0
      and
      increase(kube_pod_container_status_terminated_reason{reason="Error"}[5m]) > 0
    for: 1m
    labels:
      severity: warning
    annotations:
      summary: "Potential preemption race condition detected"
      description: "Job {{ $labels.job_name }} may have failed due to Terminating Pod conflict"
# 問題検出スクリプト
#!/bin/bash
# detect_terminating_conflict.sh

# Terminating状態のPodを持つJobを検出
kubectl get pods -A -o json | jq -r '
  .items[] |
  select(.status.phase == "Terminating") |
  select(.metadata.labels."job-name" != null) |
  "\(.metadata.namespace)/\(.metadata.labels."job-name")"
' | sort -u | while read job; do
  namespace=$(echo $job | cut -d'/' -f1)
  jobname=$(echo $job | cut -d'/' -f2)
  
  # 同名のPendingまたはCreating Podがあるか確認
  pending_count=$(kubectl get pods -n $namespace -l job-name=$jobname \
    -o json | jq '[.items[] | select(.status.phase == "Pending")] | length')
  
  if [ $pending_count -gt 0 ]; then
    echo "⚠️ Potential conflict: $job has both Terminating and Pending pods"
    echo "   Recommendation: Monitor for Failed status or manual intervention"
  fi
done

運用手順:

  1. アラート受信時に即座に確認
  2. 必要に応じて手動でTerminating Podを強制削除
    kubectl delete pod <pod-name> -n <namespace> --grace-period=0 --force
    
  3. ジョブの再投入または自動リトライを待つ

将来的な改善の可能性

Kueueコミュニティでこの問題は認識されており、以下の改善が議論されています:

Issue tracker:

提案中の改善案:

  1. Workload状態の詳細化

    • Terminating 状態を明示的に管理
    • Terminating中のWorkloadは再Admissionの対象外に
  2. Graceful Period考慮の再スケジューリング

    • terminationGracePeriodSeconds経過後にのみ再Admission
    • より安全なタイミング制御
  3. Pod名の動的生成

    • 再起動時に異なるPod名を使用
    • 名前衝突の根本的な回避

まとめ: この問題への現実的なアプローチ

【推奨対処法の組み合わせ】

1. terminationGracePeriodSeconds を現実的な値に設定(30秒程度)
   ├─ チェックポイント保存が確実に完了する最小時間
   └─ 問題発生ウィンドウを可能な限り短縮

2. backoffLimit を設定してリトライを有効化(3回程度)
   ├─ 一時的な問題からの自動回復
   └─ 手動介入の頻度を削減

3. モニタリング・アラートの実装
   ├─ 問題発生を早期検出
   └─ 必要に応じて手動介入

4. 重要ジョブは stable-gpu-queue に配置(プリエンプション無効)
   ├─ 本番学習ジョブの安定性確保
   └─ 実験ジョブとの分離

重要な認識:

この問題は、Graceful shutdown(安全な停止)とリソース効率(即座の再利用)のトレードオフから生じています。完全な解決は現状困難ですが、適切な対処法の組み合わせにより、実運用上の影響を許容範囲内に抑えることができます。

Kueueの将来バージョン(v0.8以降)で改善が期待されますが、現時点(v0.7系)では上記の対処法で運用することを推奨します。


これまで解説してきたKueueによる優先度制御に加えて、Volcano Schedulerという別のバッチスケジューリングシステムも選択肢として存在します。ここでは、Volcanoを使った場合の解決可能性について解説します。

Volcanoとは

Volcanoは、CNCF(Cloud Native Computing Foundation)のインキュベーションプロジェクトで、Kubernetes上でバッチワークロードと機械学習ワークロードに特化したスケジューラーです。

主な特徴:

  • Gang Scheduling(全Podの同時起動)をネイティブサポート
  • PodGroupという概念でPod群を一括管理
  • カスタムスケジューリングプラグインによる拡張性
  • Queue、PodGroup、SchedulingPolicyによる細かい制御

Volcanoで問題は解決できるのか?

結論: 完全には解決できませんが、発生頻度を減らせる可能性があります

解決できない理由(Kubernetes制約)

以下のKubernetes基盤の制約は、どのスケジューラーを使っても変わりません:

  1. Pod名の一意性制約: 同名のPodは同時に存在できない(Kubernetes API Serverの制約)
  2. terminationGracePeriodSeconds: Graceful shutdown期間は必須
  3. Terminating状態のPod: 物理的に存在し続ける

Volcanoで改善できる可能性がある点

VolcanoのPodGroup Phase管理により、より保守的な再スケジューリング判断が可能:

# VolcanoのPodGroup状態遷移
PodGroup Phase:
  Running → Terminating → Terminated → (再作成判断)
                          
                          この状態になるまで待つ

Kueueとの違い:

項目 Kueue Volcano
再スケジュール判断 リソース解放検出で即座 PodGroup Phaseを考慮
Terminating考慮 なし PodGroupレベルで管理可能
Gang Scheduling アノテーションベース ネイティブ(PodGroup)
問題発生確率 高(即座の再割り当て) 中(フェーズ確認後)

KueueとVolcanoの詳細比較

1. アーキテクチャの違い

Kueue:

PyTorchJob → Workload (Kueue CRD) → ClusterQueue
                ↓
         Admission制御 → Pod作成
                ↓
         Kubernetes Scheduler

Volcano:

PyTorchJob → PodGroup (Volcano CRD) → Queue (Volcano)
                ↓
         Volcano Scheduler (カスタムスケジューラー)
                ↓
              Pod起動

2. 優先度制御の違い

Kueue:

# WorkloadPriorityClassベース
apiVersion: kueue.x-k8s.io/v1beta1
kind: WorkloadPriorityClass
metadata:
  name: high-priority
value: 100
---
# Jobに適用
metadata:
  labels:
    kueue.x-k8s.io/priority-class: high-priority

Volcano:

# PriorityClassベース(Kubernetes標準)
apiVersion: scheduling.k8s.io/v1
kind: PriorityClass
metadata:
  name: high-priority
value: 100
preemptionPolicy: PreemptLowerPriority  # ← Volcanoで制御
---
# Jobに適用
spec:
  template:
    spec:
      priorityClassName: high-priority
      schedulerName: volcano  # ← Volcanoスケジューラー指定

3. Gang Schedulingの実装

Kueue:

# アノテーションで指定
metadata:
  annotations:
    kueue.x-k8s.io/gang-min-size: "8"  # 最小8Pod必要

Volcano:

# PodGroup CRDで管理
apiVersion: scheduling.volcano.sh/v1beta1
kind: PodGroup
metadata:
  name: training-job-podgroup
spec:
  minMember: 8  # 最小8Pod必要
  queue: gpu-queue
  priorityClassName: high-priority

Volcanoへの移行を検討すべきケース

以下のいずれかに該当する場合、Volcanoへの移行を検討する価値があります:

1. プリエンプション問題の発生頻度が非常に高い

【判断基準】
✅ 検討推奨: 月20回以上の問題発生
⚠️ 現状維持: 月10回未満

2. より細かいスケジューリング制御が必要

Volcanoは以下のような高度な制御が可能:

# Volcanoのカスタムスケジューリングポリシー例
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: gpu-queue
spec:
  weight: 100
  capability:
    nvidia.com/gpu: 32
  reclaimable: true  # リソース再利用の積極性
  guarantee:
    nvidia.com/gpu: 16  # 保証リソース量

3. 既存のVolcano採用事例との統合

組織内で既にVolcanoを使用している場合、統一することで運用コストを削減できます。

Volcano実装例

ステップ1: Volcanoのインストール

# Helm chartでインストール
helm repo add volcano https://volcano-sh.github.io/helm-charts
helm repo update

helm install volcano volcano/volcano \
  --namespace volcano-system \
  --create-namespace \
  --set scheduler.image.tag=v1.9.0

ステップ2: Queueの作成

apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: gpu-queue-high
spec:
  weight: 100  # 高優先度
  capability:
    nvidia.com/gpu: 16
  reclaimable: true
---
apiVersion: scheduling.volcano.sh/v1beta1
kind: Queue
metadata:
  name: gpu-queue-low
spec:
  weight: 10  # 低優先度
  capability:
    nvidia.com/gpu: 16
  reclaimable: true

ステップ3: PyTorchJobの設定

apiVersion: kubeflow.org/v1
kind: PyTorchJob
metadata:
  name: high-priority-training
spec:
  pytorchReplicaSpecs:
    Master:
      replicas: 1
      template:
        metadata:
          annotations:
            scheduling.volcano.sh/queue-name: gpu-queue-high  # ← Queue指定
        spec:
          schedulerName: volcano  # ← Volcanoスケジューラー使用
          priorityClassName: high-priority
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0
            resources:
              limits:
                nvidia.com/gpu: 1
    Worker:
      replicas: 7
      template:
        metadata:
          annotations:
            scheduling.volcano.sh/queue-name: gpu-queue-high
        spec:
          schedulerName: volcano
          priorityClassName: high-priority
          containers:
          - name: pytorch
            image: pytorch/pytorch:2.0
            resources:
              limits:
                nvidia.com/gpu: 1

ステップ4: PodGroupの自動生成確認

# PyTorchJobから自動生成されたPodGroupを確認
kubectl get podgroup

# 詳細確認
kubectl describe podgroup high-priority-training-podgroup

# 出力例
# Name:         high-priority-training-podgroup
# Namespace:    default
# Phase:        Running
# MinMember:    8
# Queue:        gpu-queue-high
# Priority:     100

Volcanoでの問題発生シナリオ

Volcanoを使った場合でも、以下のシナリオでは問題が発生する可能性があります:

改善の可能性:

  • VolcanoはPodGroup.Status.Phaseで状態管理するため、Kueueより保守的な判断が可能
  • しかし、完全な回避は保証されない(設定やバージョンに依存)

移行の判断基準

以下のフローチャートで判断してください:

問題発生頻度 ≥ 月20回?
  ├─ YES → Volcano検討を推奨
  │         ├─ 組織でVolcano採用済み?
  │         │   ├─ YES → 移行を強く推奨
  │         │   └─ NO  → PoC実施して効果検証
  │         └─ 移行コスト < 運用コスト?
  │             ├─ YES → 移行実施
  │             └─ NO  → 現状維持+対処法強化
  │
  └─ NO  → Kueueで運用継続
            └─ terminationGracePeriodSeconds調整
            └─ backoffLimit設定
            └─ モニタリング強化

まとめ: Kueue vs Volcano

Kueueを推奨するケース:

  • ✅ 問題発生頻度が月10回未満
  • ✅ シンプルな優先度制御で十分
  • ✅ Kubernetes標準に近い構成を維持したい
  • ✅ 既存のKueueベースの運用が確立している

Volcanoを検討すべきケース:

  • ✅ 問題発生頻度が月20回以上
  • ✅ より高度なスケジューリング制御が必要
  • ✅ 組織内で既にVolcano採用実績がある
  • ✅ カスタムスケジューリングプラグインを開発したい

重要な認識:

どちらを選んでも、Kubernetes基盤の制約(Pod名一意性、terminationGracePeriodSeconds、Terminating状態)は変わりません。Volcanoは問題発生の頻度を減らせる可能性がありますが、完全な解決策ではありません。

現実的なアプローチとしては:

  1. まずKueueで運用開始(学習コストが低い)
  2. 問題発生頻度を測定(3ヶ月程度)
  3. 頻度が高い場合のみVolcano検討(PoC実施)

参考文献:

Kubernetes基礎

Kueue関連

Volcano関連

PyTorchJob関連

その他


0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?