資源の関門 — アドバタイズ・要求ルール・テイントで分かれるPending
目標
GPU Podが通過しなければならない2つのゲートのうち、リソース側を最後まで進みます。ノードにリソースをアドバタイズさせ、拡張リソースの要求規則2つが実際に拒否するのを確認し、同じPendingでも原因がテイントなのかリソースなのかを切り分けて読むところまで行います。
なぜ重要なのか
「GPU Podが起動しない」という報告で、最初の3分で切り分けを決められないと、何時間も間違った場所で費やすことになります。Podがノードに割り当てられてすらいなければ、それはスケジューラーの段階であり、スケジューラーが見るのはデバイスではなく、ノードのstatusに書かれた数字です。その数字を書いてくれるのがdevice pluginであり、その数字がなければ、デバイスが正常に接続されていても、Podはいつまでも待機します。
拡張リソースには、さらに規則が2つあります。requestsとlimitsが同じで、値が整数でなければなりません。オーバーコミットができず、デバイスを分割できないからです。両方の規則ともapiserverが検査するので、破ればPodはそもそも作成されません。最後に、GPUノードにはたいていテイントが付いているので、リソースが余っていても、トレラレーションがなければスケジュールされません。この3つが、リソースのゲートのすべてです。
環境
このPodには、GPUもdevice pluginもありません。代わりに、kwokが起動した本物のコントロールプレーンがあり、ノードのstatusに数字を書くと、本物のスケジューラーがそれを見て判定します。拡張リソースの検証も、本物のapiserverが行います。作業ディレクトリは/root/gpuwhyで、マニフェストはk8s/に、出力物はout/に置きます。
ステップ
gpu-whyネームスペースを作成し、lab-node-0に検出ラベルを4つ付けます。- そのノードのstatusに
nvidia.com/gpu: 2をアドバタイズし、out/advertised.txtに保存します。 - 規則に違反するマニフェストを2つ作成し、拒否メッセージを
out/rejected.txtに保存します。 trainerというPodを条件なしで作成し、リソースだけでノードが決まることを確認します。- ノードにテイントを設定し、
cpu-onlyが待機する理由をout/tainted.txtに保存します。 - トレラレーションを付けた
trainer2で、2つのゲートを一緒に通過します。 trainer3で空きを使い切り、待機理由がリソースに変わることをout/exhausted.txtに保存します。out/gate-report.txtに7行でまとめます。
参考
- statusはサブリソースです。
kubectl patch node <이름> --subresource=statusを使い(プレースホルダーはノード名です)、JSONポインターではスラッシュを~1でエスケープします。 capacityだけを書いてallocatableを抜かすと、スケジューラーが使える空きが生まれません。よくあるミスなので、ステップ3のあとでもPodが待機し続ける場合は、まずここを確認してください。- ステップ5とステップ7は、どちらもPendingです。2つのファイルを並べて、メッセージがどう違うかを比べてみてください。その違いが、次に見る場所を決めます。
- テイントを削除して通過させないでください。GPUノードを予約しておいた理由がなくなります。
GPUノードに検出ラベルを付ける
gpu-whyネームスペースを作成し、lab-node-0にラベルを4つ付けてください。nvidia.com/gpu.present=true、nvidia.com/gpu.product=NVIDIA-A100-SXM4-40GB、nvidia.com/gpu.count=2、nvidia.com/gpu.deploy.device-plugin=trueです。残りの2台のノードには付けないでください。
実際のクラスターでは、gpu-feature-discovery DaemonSetがデバイスを読み取って、これらのラベルを付けます。オペレーターはnvidia.com/gpu.deploy.*ラベルで、どのノードにどのDaemonSetを起動するかをゲーティングします。ここでは、その結果だけを手で作ります。kubectl label node <이름> <키>=<값> --overwriteを使い(プレースホルダーはノード名とキーと値です)、確認はkubectl get node --show-labelsで行います。
ノードのstatusに拡張リソースをアドバタイズする
lab-node-0のstatusに、nvidia.com/gpuを2として書いてください。capacityとallocatableの両方に入れる必要があります。そのあと、3台のノードのアドバタイズ量を/root/gpuwhy/out/advertised.txtに保存してください。
拡張リソースは、kubeletが自分で数える値ではなく、device pluginが伝えた内容をノードのstatusに書き込んだ数字です。ここでは、その場所を直接書きます。statusは別のサブリソースなので、kubectl patch node <이름> --subresource=status --type=json -p '[...]'で修正します(プレースホルダーはノード名です)。JSONポインターではスラッシュを~1でエスケープする必要があるため、パスは/status/capacity/nvidia.com~1gpuになります。capacityだけを書いても、スケジューラーが使える空きは生まれません。
拡張リソースの2つの規則が何を拒否するかを確認する
拒否されるPodのマニフェストを2つ作成してください。/root/gpuwhy/k8s/bad-mismatch.yamlは、nvidia.com/gpuのrequestsとlimitsを異なる値で書き、/root/gpuwhy/k8s/bad-fraction.yamlは、小数で要求します。2つを適用して、拒否メッセージを/root/gpuwhy/out/rejected.txtに保存してください。
拡張リソースはオーバーコミットを許可しないので、requestsとlimitsは必ず同じで、値は整数でなければなりません。デバイスは、分割できない単位で割り当てられるからです。両方の規則ともapiserverが検査するので、Podはそもそも作成されません。コマンドが失敗することが正解で、メッセージは標準エラー出力に出るため、2>&1で受けないとファイルに残りません。
条件を指定しなくても、リソースがノードを決める
/root/gpuwhy/k8s/trainer.yamlで、gpu-whyネームスペースにtrainerというPodを作成してください。nvidia.com/gpuをlimitsにだけ1と書き、nodeSelectorは付けないでください。
拡張リソースは、limitsだけを書くと、requestsが同じ値で自動的に埋められます。作成後にkubectl -n gpu-why get pod trainer -o yamlで、requestsが埋められたことを自分で確認してください。ノードを指定していないのに、GPUをアドバタイズしているノードに行くことが、このステップの要点です。スケジューラーが見るのはデバイスではなく、数字です。
リソースが余っていても、テイントが拒否する
lab-node-0にnvidia.com/gpu=present:NoScheduleのテイントを設定し、/root/gpuwhy/k8s/cpu-only.yamlで、GPUを使わないcpu-onlyというPodを作成してください。そのPodは、nvidia.com/gpu.present: "true"のノードセレクターで、GPUノードを狙う必要があります。待機状態になったら、スケジュール条件のメッセージを/root/gpuwhy/out/tainted.txtに保存してください。
GPUノードは1台あたりのコストが大きいので、一般のワークロードがCPUの空きを埋めてしまうと、肝心のGPU Podが入る場所がなくなります。テイントは、そのノードをGPU専用に予約するための仕組みです。kubectl taint node <이름> <키>=<값>:<효과>で設定し(プレースホルダーはノード名とキーと値とエフェクトです)、待機理由はkubectl -n gpu-why get pod cpu-only -o jsonpath='{.status.conditions[?(@.type=="PodScheduled")].message}'で取り出します。すでに起動していたPodは、NoScheduleでは追い出されないという点も、一緒に確認してください。
トレラレーションを付けて、2つのゲートを一緒に通過する
/root/gpuwhy/k8s/trainer2.yamlで、trainer2というPodを作成してください。先ほど設定したテイントを許容するトレラレーションと、nvidia.com/gpu1枚の要求を、一緒に入れます。テイントは削除しないでください。
トレラレーションのkey・value・effectが、ノードのテイントと一致する必要があります。同じノードで、cpu-onlyは待機し続け、trainer2だけが入ることが、確認ポイントです。テイントを削除して通過させると、GPUノードを予約しておいた理由がなくなります。
空きがなくなると、同じPodも待機する
/root/gpuwhy/k8s/trainer3.yamlで、trainer2とまったく同じPodのtrainer3をもう1つ作成してください。待機状態になったら、スケジュール条件のメッセージを/root/gpuwhy/out/exhausted.txtに保存してください。
ノードがアドバタイズした空きは2つで、前の2つのPodがすでに使い切っています。トレラレーションはそのままにしておく必要があり、そうすることで、今回の待機理由がテイントではなくリソースであることが明らかになります。スケジューラーのメッセージがInsufficient nvidia.com/gpuに変わることを確認してください。同じPendingでも、理由が違えば見る場所が違います。
ゲートを数字と1単語でまとめる
/root/gpuwhy/out/gate-report.txtに7行を書いてください。ALLOCATABLE、USED、PENDING、TAINT、REQUESTS_EQUAL_LIMITS、FRACTIONAL、SCHEDULER_SEESです。
3つの数字は、推測せずにクラスターで数えて書きます。USEDは、lab-node-0に割り当てられたPodのnvidia.com/gpu要求の合計で、PENDINGは、gpu-whyネームスペースで待機中のPodの数です。TAINTは、ステップ5で設定したものを키=값:효과の形で書き(プレースホルダーはキーと値とエフェクトです)、残りの3つは、ステップ3とステップ4で確認した規則を1単語で書きます。SCHEDULER_SEESは、スケジューラーがデバイスを見るのか数字を見るのかという問いへの答えです。