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

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

同じメッセージ、違う原因 — GPU 障害の分類表

TT Labで続きを見る

一言でいうと

「GPU Podが起動しない」という報告を受けたら、まずPodオブジェクトがあるかないかで2つに分け、あるならPendingかRunningかでさらに分けたうえで、ノードのstatusとPodのspecを照合して、7つの原因のどれかに絞ります。スケジューラーが残す文は、3つの異なる原因から1文字も違わず同じように出てくるので、メッセージを読むだけでは終わりません。

なぜ必要なのか

GPUクラスターの運用に最もよく来る問い合わせは、いつも同じ一文です。「私のジョブが動きません」。ところが、この一文の裏にある実際の状態は、少なくとも次のくらい違います。

この3つは、調べるべき場所がまったく違います。1つ目はアドミッション(クォータ・RuntimeClass・ポリシー)、2つ目はスケジューラー、3つ目はPodのspecとノードのランタイムです。ところが、報告者はこの3つを区別してくれません。そのため、受け取った側が最初の3分で切り分けを決める手順を持っている必要があります。その手順がないと、device pluginのログを1時間調べたあとで、「ああ、Podがそもそも作られていなかった」と知ることになります。

さらに悪い事情があります。Kubernetesのスケジューラーは、失敗の理由を親切に残すほうですが、拡張リソースについては、その親切さが原因を覆い隠す方向に働きます。次の3つの状況で出てくる文が、同じです。

3つの場合とも、Insufficient nvidia.com/gpuです。原因はそれぞれ、「プラグインを復活させる」「ノードを直す」「空きを待つか増やす」と、まったく違うのに、メッセージは区別してくれません。この1点が、このモジュールが存在する理由です。

症状を分ける最初の2つの質問

質問1: Podオブジェクトはあるか

kubectl get pod <이름> -n <네임스페이스> -o json

なければ、スケジューラーはこの件とまったく関係がありません。オブジェクトは、APIサーバーのアドミッションを通過して初めて作られるのですが、GPU関連でアドミッションが止める代表的な2つが、ResourceQuotaの超過と、存在しないRuntimeClassの参照です。どちらも、kubectl applyを実行した人のターミナルにだけエラーが表示され、クラスターには何の痕跡も残りません。報告者がその画面を閉じてしまったなら、同じマニフェストをもう一度適用して、エラーを再現するのが最も速いです。

質問2: spec.nodeNameは空か

kubectl get pod <이름> -n <네임스페이스> -o jsonpath='{.spec.nodeName}'

空なら、スケジューリングの問題です。埋まっているのに問題があるなら、それはノード上の問題(ランタイムハンドラー、ドライバー、リクエストをしていないマニフェスト)です。kubectl get podのSTATUS欄ではなく、このフィールドを見る理由は、Pendingという1つの単語が、「ノードを選べなかった」と「ノードは選んだのにコンテナを作れない」の両方を覆い隠してしまうからです。

原因がどの欄に現れるか

原因 決定的な証拠がある場所 確認するコマンド
クォータ超過 Podがない。作成時のエラー文言 kubectl get resourcequota -n <ns> -o json
RuntimeClassがない Podがない。作成時のエラー文言 kubectl get runtimeclass
リクエストをしていない spec.containers[].resources.limitsにnvidia.com/gpuがない kubectl get pod -o json
ラベルの不一致 spec.nodeSelectorを満たすノードが0個 kubectl get nodes --show-labels
テイント 候補ノードのspec.taintsを、Podのtolerationsが許容できない kubectl get node -o json
リソース未アドバタイズ 候補ノードのstatus.capacityに、リソース名そのものがない kubectl get node -o json
allocatableが0 capacityは正の数なのに、status.allocatableが0 同じ出力の別の欄
空きの枯渇 allocatableは正の数なのに、そのノードのPodの要求の合計が、その数に達している kubectl get pods -A -o jsonを合計する

この表の価値は、「何を見るか」ではなく、「どの順序で見るか」にあります。順序は上から下です。前のものが真なら、後ろのものは見る必要がなく、順序を守らないと、見当違いの結論になります。たとえば、nodeSelectorがどのノードとも合わないPodは、テイントもリソースも検査する対象がそもそもありません。ところが、「テイントを消したのに起動しない」というような対処を先にしてしまうと、問題のないクラスター設定だけが壊れます。

最後の行(空きの枯渇)が、唯一計算を求めます。ノードオブジェクトを見るだけではわからず、そのノードに配置されたPodを集めて、GPUの要求を足す必要があります。ここに1つ落とし穴があります。SucceededやFailedで終わったPodは、枠を確保していないので、合計から外す必要があります。外さないと、実際には空いているノードを「いっぱいだ」と誤って判定します。

拡張リソースだからこその事情

nvidia.com/gpuは、cpuやmemoryとは違い、kubeletが自分では数えられない拡張リソースです。公式ドキュメントは、拡張リソースの規則を2つに固定しています。オーバーコミットをサポートせず、requestsとlimitsが同じで、値は整数でなければなりません。そのため、GPUの要求を読むときは、limitsだけを見れば十分です。逆にいえば、半枚や1.5枚のような値は存在できず、足りない分だけ受け取るような動作もありません。要求は、丸ごと満たされるか、Podが待つか、どちらかです。

そして、拡張リソースは「デバイスがあるか」とは無関係に成立します。ノードのstatusに数字だけが書かれていれば、スケジューラーはそのノードにPodを送ります。逆に、カードが8枚接続されていても、statusに数字がなければ、スケジューラーにとって、そのノードはGPUがないノードです。調査するときは、物理的な事実ではなく、ノードオブジェクトが、スケジューラーにとって唯一の真実であることを、押さえておく必要があります。

現場での姿

第一に、kubectl describeだけを読んで原因を決めつける習慣が、最も高くつきます。先ほど見たように、3つの原因が同じ文を出します。特に夜間に「Insufficient nvidia.com/gpu」を見て、自動でノードを増やすスクリプトを設定していた組織がありましたが、実際の原因は、ノード1台のallocatableが0に落ちたことでした。ノードを増やしても、そのノードはずっと0のままで、コストだけが増えました。

第二に、人が判定すると、順序を守れません。そのため、この判定はツールとして固めておくほうがよいです。入力はPod1つ、出力は原因1単語。こうしたツールがあれば、一次対応者が、報告を受けたその場で「これはクォータです」と答えられ、判定基準が人によって変わりません。このモジュールのラボが作るのは、まさにそのツールです。

第三に、出力は1単語であって初めて役に立ちます。文を出力すると、人が読み直す必要があり、自動化につなげられません。no-gpu-node・allocatable-zero・exhaustedのように、次の行動が1つに決まる語彙を選んでおけば、そのままアラートのルーティングキーになります。原因の語彙を決めることが、実は運用手順を決めることです。

第四に、この環境の正直な限界です。ラボのクラスターには、GPUもdevice pluginもありません。そのため、「デバイスプラグインが落ちた」という状態は、プラグインを落とすのではなく、ノードのstatusにリソースを書かないことで作ります。これは模擬ではなく、同じ状態です。実際の障害でも、スケジューラーが見るのはstatusだけだからです。ただし、「PodはRunningなのに、コンテナの中でデバイスが見えない」の後半は、コンテナが実際には動かないので、確認できません。その分岐は、Podのspecにリソース要求がないという事実までを判定します。

参考ドキュメント

次のラボですること

kwokが起動した本物のスケジューラー上に、7つの状態を1つずつ作ります。アドバタイズが問題ないノード、リソース名そのものがないノード、capacityだけがあってallocatableが0のノードを立てて、そこにそれぞれPodを差し込み、3つの原因が同じ文を出すことを、直接見ます。テイントだけが違うPodとnodeSelectorだけが違うPodを加え、アドミッションで拒否されてオブジェクトすら作られない2つ(RuntimeClass・クォータ)を体験します。最後に、kubectl get -o jsonだけを見て原因を1単語で答える分類ツールを作り、7つのPodすべてにかけてみます。