TT Lab
はじめる
学ぶ 学習パス コース

GPU Operatorとタイムスライシング

GPU 障害の分類 — 同じメッセージから違う原因を切り分ける

TT Labで続きを見る

目標

GPU Podが止まる7つの状態を本物のスケジューラー上に作り、オブジェクトだけを見て原因を1単語で答える分類ツールを作ります。

なぜ重要なのか

「GPU Podが起動しない」という報告1件に、実際の原因は6、7個あり、調べるべき場所がすべて違います。しかも、リソース未アドバタイズ・allocatable 0・空きの枯渇の3つは、スケジューラーがまったく同じ文を出します。そのため、メッセージを読むだけでは終わらず、nodeSelector → テイント → capacity → allocatable → 残りの空きの順序で、オブジェクトを照合して、初めて原因が1つに絞られます。順序が重要な理由は、前の段階が真なら、後ろの段階は検査する対象そのものがないからです。順序を破ると、問題のないテイントを消すように、クラスターだけが壊れます。この判定を人が毎回行うと、基準が揺らぐので、最後にツールとして固めておきます。

ステップ

  1. 作業ディレクトリ/root/gputri/cases・/root/gputri/out・/root/gputri/binを作成して、ネームスペースgpu-triageを作成してください。lab-node-0のstatus.capacityとstatus.allocatableの両方に、nvidia.com/gpuを"2"として入れて、テイントnvidia.com/gpu=present:NoScheduleを設定してください。/root/gputri/cases/case-ok.yamlに、case-okというPodを書いてください。ネームスペースgpu-triage、nodeSelectorはkubernetes.io/hostname: lab-node-0、そのテイントを許容するトレラレーション、コンテナ名cuda、イメージnvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04、limitsにnvidia.com/gpu: 1です。適用してRunningになったら、/root/gputri/out/01-ok.txtに2行を書いてください。NODE=とPHASE=です。
  2. lab-node-1は、何もアドバタイズしないままにしておきます(device pluginが落ちたノードです)。/root/gputri/cases/case-nogpunode.yamlに、case-nogpunodeというPodを書いてください。nodeSelectorでlab-node-1に固定し、トレラレーションはステップ1と同じにして、nvidia.com/gpu: 1を要求します。適用したあと、Pendingであることを確認して、PodScheduled条件のreasonとmessageを、/root/gputri/out/02-nogpunode.txtに、REASON=とMESSAGE=の2行で保存してください。
  3. lab-node-2のstatus.capacityにはnvidia.com/gpuを"4"、status.allocatableには"0"として入れてください(ドライバーの検証に失敗して、ノードがGPUを出せない状態です)。/root/gputri/cases/case-alloc0.yamlに、case-alloc0というPodを書いてください。lab-node-2に固定し、残りはステップ2と同じです。適用したあと、/root/gputri/out/03-alloc0.txtに3行を書いてください。CAPACITY=、ALLOCATABLE=、MESSAGE=です。前のステップのメッセージと比べてみてください。
  4. /root/gputri/cases/case-taint.yamlに、case-taintというPodを書いてください。lab-node-0に固定し、nvidia.com/gpu: 1を要求しますが、トレラレーションは入れません。残りはステップ1のPodと同じです。適用したあと、/root/gputri/out/04-taint.txtに2行を書いてください。TAINT=nvidia.com/gpu=present:NoScheduleとMESSAGE=です。リソースは余っているのに、なぜ止まるのかを、メッセージで確認してください。
  5. /root/gputri/cases/case-label.yamlに、case-labelというPodを書いてください。nodeSelectorをnvidia.com/gpu.product: NVIDIA-A100-SXM4-40GBにして(このラベルを持つノードはありません)、トレラレーションとnvidia.com/gpu: 1の要求はそのままにします。適用したあと、/root/gputri/out/05-label.txtに2行を書いてください。MATCHING_NODES=には、そのラベルを実際に持つノード数を、MESSAGE=には、条件のメッセージを入れます。
  6. /root/gputri/cases/case-exhaust.yamlに、case-exhaustというPodを書いてください。lab-node-0に固定し、トレラレーションを入れて、nvidia.com/gpu: 2を要求します。そのノードは2枚をアドバタイズしていますが、ステップ1のcase-okが、すでに1枚を確保しています。適用したあと、/root/gputri/out/06-exhaust.txtに3行を書いてください。ALLOCATABLE=(そのノードがアドバタイズした量)、ALLOCATED=(そのノードに配置されたPodたちのGPU要求の合計)、REQUESTED=2です。
  7. まず/root/gputri/cases/case-norequest.yamlに、case-norequestというPodを書いてください。lab-node-0に固定し、トレラレーションは入れますが、リソース要求はまったく入れません。適用すると起動します。次に、/root/gputri/cases/case-rtc.yamlに、runtimeClassName: nvidiaを付けたcase-rtcというPodを書いて適用し、標準エラー出力も含めた出力を/root/gputri/out/07-runtimeclass.txtに保存してください(RuntimeClassは作成しないでください)。続いて、ネームスペースgpu-quotaを作成して、/root/gputri/cases/quota.yamlに、gpu-quotaというResourceQuotaを書いてrequests.nvidia.com/gpuを"1"に制限したあと、/root/gputri/cases/case-quota.yamlのcase-quotaというPod(GPU3枚を要求)を適用して、出力を/root/gputri/out/07-quota.txtに保存してください。そして、/root/gputri/out/07-symptoms.txtに3行を書いてください。NOREQUEST=、RUNTIMECLASS=、QUOTA=です。あとの2行には、Podオブジェクトが作られたかどうかを、createdまたはabsentで書きます。
  8. /root/gputri/bin/triage.shを作成してください。bash triage.sh <네임스페이스> <파드>で呼び出すと(プレースホルダーはネームスペースとPodです)、標準出力に1単語だけを出力して終わります。答えは7種類です。no-request、ok、label-mismatch、taint、no-gpu-node、allocatable-zero、exhaustedです。Podがなければ、標準エラー出力に案内を出して、1で終わります。判定は、kubectl get -o jsonが返したオブジェクトだけで行い、順序は、要求なし → すでに配置済み → nodeSelector → テイント → capacity → allocatable → 残りの空きです。作成したら、7つのPod(case-ok、case-nogpunode、case-alloc0、case-taint、case-label、case-exhaust、case-norequest)のすべてにかけて、/root/gputri/out/triage.txtに、<파드이름> <원인>の形で7行を書いてください(プレースホルダーはPod名と原因です)。

参考

正常なGPUノードを1台作る

作業ディレクトリ/root/gputri/cases・/root/gputri/out・/root/gputri/binを作成して、ネームスペースgpu-triageを作成してください。lab-node-0のstatus.capacityとstatus.allocatableの両方に、nvidia.com/gpuを"2"として入れて、テイントnvidia.com/gpu=present:NoScheduleを設定してください。/root/gputri/cases/case-ok.yamlに、case-okというPodを書いてください。ネームスペースgpu-triage、nodeSelectorはkubernetes.io/hostname: lab-node-0、そのテイントを許容するトレラレーション、コンテナ名cuda、イメージnvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04、limitsにnvidia.com/gpu: 1です。適用してRunningになったら、/root/gputri/out/01-ok.txtに2行を書いてください。NODE=とPHASE=です。

拡張リソースは、kubeletが自分では数えられません。実際のクラスターでは、device pluginがノードのstatusに書いてくれますが、ここではkubectl patch node <이름> --subresource=status --type=mergeで、同じ場所に同じ値を直接書きます(プレースホルダーはノード名です)。capacityだけを書いても、スケジューラーが使える空きは生まれません。2つの欄を、どちらも埋めてください。運用のGPUノードは、ほとんど常にテイントがかかっていて、一般のワークロードが流れ込まないようになっています。そのため、GPU Podには、トレラレーションが付いてきます。

リソース名そのものがないノード

lab-node-1は、何もアドバタイズしないままにしておきます(device pluginが落ちたノードです)。/root/gputri/cases/case-nogpunode.yamlに、case-nogpunodeというPodを書いてください。nodeSelectorでlab-node-1に固定し、トレラレーションはステップ1と同じにして、nvidia.com/gpu: 1を要求します。適用したあと、Pendingであることを確認して、PodScheduled条件のreasonとmessageを、/root/gputri/out/02-nogpunode.txtに、REASON=とMESSAGE=の2行で保存してください。

条件は、kubectl get pod <이름> -n <ns> -o jsonpath='{.status.conditions[0].reason}'で取り出せます(プレースホルダーはPod名です)。メッセージに何が書かれるかに、注目してください。次の2つのステップで、原因がまったく違うPodが、同じ文を出します。ノードにnvidia.com/gpuを書かなかったことは、「カードがない」ではなく、「スケジューラーにはないと見える」という意味です。

capacityはあるのに、出せないノード

lab-node-2のstatus.capacityにはnvidia.com/gpuを"4"、status.allocatableには"0"として入れてください(ドライバーの検証に失敗して、ノードがGPUを出せない状態です)。/root/gputri/cases/case-alloc0.yamlに、case-alloc0というPodを書いてください。lab-node-2に固定し、残りはステップ2と同じです。適用したあと、/root/gputri/out/03-alloc0.txtに3行を書いてください。CAPACITY=、ALLOCATABLE=、MESSAGE=です。前のステップのメッセージと比べてみてください。

capacityは「このノードが持つ量」、allocatableは「スケジューラーに出せる量」です。この2つが分かれるのは正常な動作であり(システム予約分が、その差です)、GPUでは、この差が0まで開くことが実際に起きます。2つの値は、同じkubectl get node -o jsonの出力の、別の欄にあります。メッセージをステップ2のファイルと並べて見ると、このラボがなぜ必要なのかが、一目でわかります。

トレラレーションだけが抜けたPod

/root/gputri/cases/case-taint.yamlに、case-taintというPodを書いてください。lab-node-0に固定し、nvidia.com/gpu: 1を要求しますが、トレラレーションは入れません。残りはステップ1のPodと同じです。適用したあと、/root/gputri/out/04-taint.txtに2行を書いてください。TAINT=nvidia.com/gpu=present:NoScheduleとMESSAGE=です。リソースは余っているのに、なぜ止まるのかを、メッセージで確認してください。

ステップ1で、case-okが同じノードで起動したのに、このPodは行けません。違いは、トレラレーション1つだけです。このステップのメッセージは、前の2つのステップとは違います。テイントは、スケジューラーが原因を正確に伝えてくれる、数少ない場所です。ノードにかかっているテイントは、kubectl get node lab-node-0 -o jsonpath='{.spec.taints}'で見られます。

候補ノードがそもそもないPod

/root/gputri/cases/case-label.yamlに、case-labelというPodを書いてください。nodeSelectorをnvidia.com/gpu.product: NVIDIA-A100-SXM4-40GBにして(このラベルを持つノードはありません)、トレラレーションとnvidia.com/gpu: 1の要求はそのままにします。適用したあと、/root/gputri/out/05-label.txtに2行を書いてください。MATCHING_NODES=には、そのラベルを実際に持つノード数を、MESSAGE=には、条件のメッセージを入れます。

GPU Feature Discoveryが付けるラベルを、そのまま選んで使ったマニフェストが、クラスターにそのラベルがないと、こうなります。このPodは、テイントもリソースも検討する対象がありません。候補ノードが0個だからです。そのため、判定の順序で、nodeSelectorが一番前に来ます。ラベルを持つノード数は、kubectl get nodes -l <키> --no-headers | wc -lで数えられます(プレースホルダーはラベルキーです)。

アドバタイズも問題ないのに、空きがない

/root/gputri/cases/case-exhaust.yamlに、case-exhaustというPodを書いてください。lab-node-0に固定し、トレラレーションを入れて、nvidia.com/gpu: 2を要求します。そのノードは2枚をアドバタイズしていますが、ステップ1のcase-okが、すでに1枚を確保しています。適用したあと、/root/gputri/out/06-exhaust.txtに3行を書いてください。ALLOCATABLE=(そのノードがアドバタイズした量)、ALLOCATED=(そのノードに配置されたPodたちのGPU要求の合計)、REQUESTED=2です。

この判定だけは、ノードオブジェクト1つでは終わりません。そのノードに配置されたPodを集めて、要求を足す必要があります。kubectl get pods -A --field-selector spec.nodeName=lab-node-0 -o jsonが出発点です。終わったPod(Succeeded・Failed)は、枠を確保していないので、合計から外す必要があります。メッセージは、ステップ2・3とまた同じです。3回目に同じ文を目にすることになります。

Podがそもそも作られない2つのケース

まず/root/gputri/cases/case-norequest.yamlに、case-norequestというPodを書いてください。lab-node-0に固定し、トレラレーションは入れますが、リソース要求はまったく入れません。適用すると起動します。次に、/root/gputri/cases/case-rtc.yamlに、runtimeClassName: nvidiaを付けたcase-rtcというPodを書いて適用し、標準エラー出力も含めた出力を/root/gputri/out/07-runtimeclass.txtに保存してください(RuntimeClassは作成しないでください)。続いて、ネームスペースgpu-quotaを作成して、/root/gputri/cases/quota.yamlに、gpu-quotaというResourceQuotaを書いてrequests.nvidia.com/gpuを"1"に制限したあと、/root/gputri/cases/case-quota.yamlのcase-quotaというPod(GPU3枚を要求)を適用して、出力を/root/gputri/out/07-quota.txtに保存してください。そして、/root/gputri/out/07-symptoms.txtに3行を書いてください。NOREQUEST=、RUNTIMECLASS=、QUOTA=です。あとの2行には、Podオブジェクトが作られたかどうかを、createdまたはabsentで書きます。

このステップの3つは、前の5つと症状の分岐が違います。1つ目は、Podが問題なく起動するのにデバイスが接続されず、あとの2つは、Podオブジェクトがそもそも作られません。アドミッションで拒否されると、クラスターには何の痕跡もなく、エラーは適用した人のターミナルにだけ残ります。そのため、2>&1で出力をファイルに残しておく習慣が、調査で決定的です。Podがないことは、kubectl get pod <이름> -n <ns>の終了コードで確認できます(プレースホルダーはPod名です)。

オブジェクトだけを見て原因を1単語で答える分類ツール

/root/gputri/bin/triage.shを作成してください。bash triage.sh <네임스페이스> <파드>で呼び出すと(プレースホルダーはネームスペースとPodです)、標準出力に1単語だけを出力して終わります。答えは7種類です。no-request、ok、label-mismatch、taint、no-gpu-node、allocatable-zero、exhaustedです。Podがなければ、標準エラー出力に案内を出して、1で終わります。判定は、kubectl get -o jsonが返したオブジェクトだけで行い、順序は、要求なし → すでに配置済み → nodeSelector → テイント → capacity → allocatable → 残りの空きです。作成したら、7つのPod(case-ok、case-nogpunode、case-alloc0、case-taint、case-label、case-exhaust、case-norequest)のすべてにかけて、/root/gputri/out/triage.txtに、<파드이름> <원인>の形で7行を書いてください(プレースホルダーはPod名と原因です)。

Pod名で答えを決めてはいけません。初めて見るPodにかけたときに正しくて初めて、役に立ちます。必要な入力は3つです。そのPod、ノード全体、そしてすべてのネームスペースのPod(空きの計算用)です。トレラレーションの判定は、operator: ExistsとEqualの2つの場合を、どちらも扱う必要があり、effectが空なら、すべてのeffectを許容します。拡張リソースは、limitsだけを見ればよいです。requestsとlimitsが同じでなければならないという規則があるからです。JSONをシェルでパースしようと苦労せずに、python3に渡すと、ずっと短くなります。