サンドボックスワークロード — ラベルが決める資源名と、分かれてしまう在庫
目標
3台のノードを、それぞれ別のGPUワークロードの用途として作り、用途ごとにリソース名が変わることと、その結果、在庫が分かれることを、実際のスケジューラーで確認したうえで、ラベルとアドバタイズが合っているかを判定するチェッカーを作ります。
なぜ重要なのか
GPUを使うワークロードが、すべてコンテナというわけではありません。ライセンスやレガシーのために、仮想マシンの中で動く必要があるものがあり、そのようなVMにカードを渡すには、ノードにインストールされるドライバーから違う必要があります。コンテナにはデータセンタードライバー、パススルーにはvfio-pci、vGPUにはvGPU Managerです。GPU Operatorは、その選択をノードラベル1行で受け付けます。そして、その選択の結果が、ノードがアドバタイズするリソース名まで変えます。名前が違うと、スケジューラーにとっては別のリソースなので、片方の待ち行列が長くても、もう片方の空きは空いたまま残ります。在庫を用途別に分けることが、なぜ元に戻しにくい決定なのかが、ここから出てきます。
ステップ
/root/gpuwl/out・/root/gpuwl/bin・/root/gpuwl/k8sを作成して、ネームスペースgpu-vmを作成してください。3台のノードに、ラベルnvidia.com/gpu.workload.configを付けてください。lab-node-0はcontainer、lab-node-1はvm-passthrough、lab-node-2はvm-vgpuです。そして/root/gpuwl/out/01-nodes.txtに3行を書いてください。1行が<노드이름> <라벨값>の形です(ノード名の昇順、プレースホルダーはノード名とラベルの値です)。/root/gpuwl/out/operands.txtに5行を書いてください。前の3行は<라벨값>=<오퍼랜드 목록>で(プレースホルダーはラベルの値とオペランドの一覧です)、一覧はカンマでつなぎます(順序は問いません)。containerはdatacenter-driver、container-toolkit、device-plugin、dcgm-exporter、vm-passthroughはvfio-manager、sandbox-device-plugin、vm-vgpuはvgpu-manager、vgpu-device-manager、sandbox-device-pluginです。4行目は、NO_LABEL=に、ラベルがないときにオペレーターが想定する値を、5行目は、ENABLE_FLAG=に、このラベルを使われるようにするClusterPolicyのフラグ名を書きます。- 3台のノードの
status.capacityとstatus.allocatableの両方に、それぞれのリソースを入れてください。lab-node-0はnvidia.com/gpuを"4"、lab-node-1はnvidia.com/GA102GL_A10を"2"、lab-node-2はnvidia.com/NVIDIA_A10-12Qを"4"です。そして/root/gpuwl/out/03-resources.txtに3行を書いてください。1行が<노드이름> <자원이름> <광고량>の形です(ノード名の昇順、プレースホルダーはノード名とリソース名とアドバタイズ量です)。 /root/gpuwl/k8s/pod-container.yamlに、job-containerというPodを書いてください。ネームスペースgpu-vm、コンテナ名cuda、イメージnvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04、limitsにnvidia.com/gpu: 1です。nodeSelectorは書かないでください。適用したあと、どのノードで起動したかを、/root/gpuwl/out/04-container.txtに1行で書いてください。NODE=<노드이름>です(プレースホルダーはノード名です)。/root/gpuwl/k8s/pod-passthrough.yamlに、vmi-passthroughというPodを書いてください。ネームスペースgpu-vm、limitsにnvidia.com/GA102GL_A10: 1、nodeSelectorなしです。残りはステップ4と同じです。適用したあと、/root/gpuwl/out/05-passthrough.txtに2行を書いてください。NODE=とRESOURCE=(要求したリソース名そのまま)です。/root/gpuwl/k8s/pod-vgpu.yamlに、vmi-vgpuというPodを書いてください。ネームスペースgpu-vm、limitsにnvidia.com/NVIDIA_A10-12Q: 1、nodeSelectorなしです。適用したあと、/root/gpuwl/out/06-placement.txtに3行を書いてください。ここまでに作成した3つのPodが、それぞれどこで起動したかを<파드이름> <노드이름>の形で(プレースホルダーはPod名とノード名です)、Pod名の昇順で書きます。/root/gpuwl/k8s/pod-cross.yamlに、job-crossというPodを書いてください。ネームスペースgpu-vm、nodeSelectorでnvidia.com/gpu.workload.config: vm-passthroughをかけて、limitsにはnvidia.com/gpu: 1を要求します。パススルーノードに、コンテナ用のリソースを求めることになります。適用したあと、/root/gpuwl/out/07-cross.txtに3行を書いてください。PHASE=、NODE=(空ならnone)、MESSAGE=(PodScheduled条件のメッセージ)です。/root/gpuwl/out/nodes.jsonにkubectl get nodes -o jsonの結果を保存して、/root/gpuwl/bin/check-config.sh <노드JSON파일>を作成してください(プレースホルダーはノードのJSONファイルです)。ファイルの中のノードのうち、nvidia.com/gpu.workload.configラベルがあるものだけを見て、その値とアドバタイズしたnvidia.com/のリソース名が合っているかを判定します。containerはnvidia.com/gpuである必要があり、vm-vgpuはvGPUのプロファイル名(番号と大文字で終わるもの)である必要があり、vm-passthroughは、その2つのどちらでもないデバイスモデル名である必要があります。合わないノードごとに、MISMATCH=<노드이름> <이유>を1行ずつ出力して1で終わり(プレースホルダーはノード名と理由です)、すべて合っていれば、OK=<검사한 노드 수>を出力して0で終わります(プレースホルダーは検査したノード数です)。作成したら、保存したダンプにかけて、出力を/root/gpuwl/out/consistency.txtに保存してください。
参考
- このクラスターには、KubeVirtもvfio-pciもなく、VMは起動できません。VMの要求は、同じリソース名を要求するPodで代用します。スケジューリング段階で起きることが、実際に同じだからです。
- 本物のクラスターでは、VMは
spec.domain.devices.gpus[].deviceNameにリソース名を書き、その前に、KubeVirtカスタムリソースのpermittedHostDevicesに、そのリソースが載っている必要があります。 - ノードのstatusは、
kubectl patch node <이름> --subresource=status --type=mergeで修正します(プレースホルダーはノード名です)。 - jsonpathでリソースを読むときは、名前のドットをエスケープします:
{.status.allocatable.nvidia\.com/gpu}。 - よくある間違い: リソース名を1文字間違えただけでも、そのノードは候補から静かに消えます。エラーも警告もありません。
- よくある間違い: アドバタイズ量が"0"のリソースを「ある」と数えると、最後のチェッカーが見当違いの判定をします。
ノード3台に用途を書く
/root/gpuwl/out・/root/gpuwl/bin・/root/gpuwl/k8sを作成して、ネームスペースgpu-vmを作成してください。3台のノードに、ラベルnvidia.com/gpu.workload.configを付けてください。lab-node-0はcontainer、lab-node-1はvm-passthrough、lab-node-2はvm-vgpuです。そして/root/gpuwl/out/01-nodes.txtに3行を書いてください。1行が<노드이름> <라벨값>の形です(ノード名の昇順、プレースホルダーはノード名とラベルの値です)。
このラベル1つが、そのノードに載るオペレーターのソフトウェアを丸ごと変えます。ラベルは、kubectl label node <이름> --overwrite <키>=<값>で付けます(プレースホルダーはノード名とキーと値です)。値は3つだけで、タイプミスをしても何の警告もありません。そのため、付けたあとに確認する習慣が必要です。kubectl get nodes -L <키>で、1画面で見られます(プレースホルダーはキーです)。
ラベルの値ごとに、何が載るのかを表にする
/root/gpuwl/out/operands.txtに5行を書いてください。前の3行は<라벨값>=<오퍼랜드 목록>で(プレースホルダーはラベルの値とオペランドの一覧です)、一覧はカンマでつなぎます(順序は問いません)。containerはdatacenter-driver、container-toolkit、device-plugin、dcgm-exporter、vm-passthroughはvfio-manager、sandbox-device-plugin、vm-vgpuはvgpu-manager、vgpu-device-manager、sandbox-device-pluginです。4行目は、NO_LABEL=に、ラベルがないときにオペレーターが想定する値を、5行目は、ENABLE_FLAG=に、このラベルを使われるようにするClusterPolicyのフラグ名を書きます。
3行を並べて見ると、共通項がsandbox-device-pluginだけであることが見えます。VM側の2つの用途は、デバイスをアドバタイズする方法は同じで、ドライバーの準備方法が違います。5行目が最も重要です。そのフラグがオフだと(既定値はオフです)、ラベルをどれだけ正確に付けても読まれず、すべてのノードがコンテナ用に準備されます。フラグ名は、ドットでつながった2つの単語です。
用途ごとに違うリソース名をアドバタイズする
3台のノードのstatus.capacityとstatus.allocatableの両方に、それぞれのリソースを入れてください。lab-node-0はnvidia.com/gpuを"4"、lab-node-1はnvidia.com/GA102GL_A10を"2"、lab-node-2はnvidia.com/NVIDIA_A10-12Qを"4"です。そして/root/gpuwl/out/03-resources.txtに3行を書いてください。1行が<노드이름> <자원이름> <광고량>の形です(ノード名の昇順、プレースホルダーはノード名とリソース名とアドバタイズ量です)。
実際のクラスターでは、ノードごとに違うデバイスプラグインがこの場所を埋めます。コンテナノードはKubernetesデバイスプラグインが、VM側の2台のノードはサンドボックスデバイスプラグインが埋めます。パススルーのリソース名はPCIデバイスのモデルから来て、vGPUのリソース名はプロファイルから来ます。後者だけが番号と大文字で終わるという規則が、最後のステップの判定の根拠になります。JSONパッチで、リソース名のドットとスラッシュをエスケープする必要はありません。jsonpathで読むときだけ、ドットをエスケープします。
コンテナワークロードは、コンテナノードに行く
/root/gpuwl/k8s/pod-container.yamlに、job-containerというPodを書いてください。ネームスペースgpu-vm、コンテナ名cuda、イメージnvcr.io/nvidia/cuda:12.4.1-base-ubuntu22.04、limitsにnvidia.com/gpu: 1です。nodeSelectorは書かないでください。適用したあと、どのノードで起動したかを、/root/gpuwl/out/04-container.txtに1行で書いてください。NODE=<노드이름>です(プレースホルダーはノード名です)。
条件を1つも指定していないのに、行けるノードは1か所だけです。リソース名そのものがノードを選んだのです。このラボ全体の要点が、この1行に入っています。拡張リソースは、limitsにだけ書きます。requestsとlimitsが同じでなければならないという規則があるため、limitsだけで十分です。
パススルーの要求は、デバイスモデル名を呼ぶ
/root/gpuwl/k8s/pod-passthrough.yamlに、vmi-passthroughというPodを書いてください。ネームスペースgpu-vm、limitsにnvidia.com/GA102GL_A10: 1、nodeSelectorなしです。残りはステップ4と同じです。適用したあと、/root/gpuwl/out/05-passthrough.txtに2行を書いてください。NODE=とRESOURCE=(要求したリソース名そのまま)です。
本物のクラスターでは、ここにVirtualMachineInstanceが来て、リソースはspec.domain.devices.gpus[].deviceNameに書きます。このPodにはKubeVirtがなくVMを起動できませんが、スケジューリング段階で起きることは同じです。VMも結局、そのリソースを要求するPodが代わりに実行されるからです。リソース名はPCIデバイスのモデルから来ます。1文字だけ間違えても、そのノードは候補から消えます。
vGPUの要求は、プロファイル名を呼ぶ
/root/gpuwl/k8s/pod-vgpu.yamlに、vmi-vgpuというPodを書いてください。ネームスペースgpu-vm、limitsにnvidia.com/NVIDIA_A10-12Q: 1、nodeSelectorなしです。適用したあと、/root/gpuwl/out/06-placement.txtに3行を書いてください。ここまでに作成した3つのPodが、それぞれどこで起動したかを<파드이름> <노드이름>の形で(プレースホルダーはPod名とノード名です)、Pod名の昇順で書きます。
3行を並べると、リソース名1つが配置を丸ごと決めたことが、一目でわかります。どのPodにもnodeSelectorを指定していないのに、3つがそれぞれ別のノードに行きました。vGPUのプロファイル名は、番号と大文字で終わります(例: 12Q)。その形が、パススルーの名前と区別できる点で、ステップ8のチェッカーが使うシグナルでもあります。配置は、kubectl get pods -n <ns> -o custom-columns=で、一度に取り出せます。
他のリソースを呼ぶと、どこにも行けない
/root/gpuwl/k8s/pod-cross.yamlに、job-crossというPodを書いてください。ネームスペースgpu-vm、nodeSelectorでnvidia.com/gpu.workload.config: vm-passthroughをかけて、limitsにはnvidia.com/gpu: 1を要求します。パススルーノードに、コンテナ用のリソースを求めることになります。適用したあと、/root/gpuwl/out/07-cross.txtに3行を書いてください。PHASE=、NODE=(空ならnone)、MESSAGE=(PodScheduled条件のメッセージ)です。
そのノードには、カードが2枚もアドバタイズされていて、空きもあります。それでも行けません。スケジューラーは、リソース名を文字列として照合するだけだからです。クラスターが在庫を用途別に分けておくと、片方の待ち行列が長くても、もう片方の空きは空いたまま残ります。これが、サンドボックスワークロードを導入するときに、最初に計画すべき制約です。メッセージが何と言っているかに注目してください。「リソースが足りない」と言います。
ラベルとリソースのアドバタイズが合っているかを判定するチェッカー
/root/gpuwl/out/nodes.jsonにkubectl get nodes -o jsonの結果を保存して、/root/gpuwl/bin/check-config.sh <노드JSON파일>を作成してください(プレースホルダーはノードのJSONファイルです)。ファイルの中のノードのうち、nvidia.com/gpu.workload.configラベルがあるものだけを見て、その値とアドバタイズしたnvidia.com/のリソース名が合っているかを判定します。containerはnvidia.com/gpuである必要があり、vm-vgpuはvGPUのプロファイル名(番号と大文字で終わるもの)である必要があり、vm-passthroughは、その2つのどちらでもないデバイスモデル名である必要があります。合わないノードごとに、MISMATCH=<노드이름> <이유>を1行ずつ出力して1で終わり(プレースホルダーはノード名と理由です)、すべて合っていれば、OK=<검사한 노드 수>を出力して0で終わります(プレースホルダーは検査したノード数です)。作成したら、保存したダンプにかけて、出力を/root/gpuwl/out/consistency.txtに保存してください。
このチェッカーは、クラスターには接続せず、ファイル1つだけを読みます。そうすれば、他の人が送ってくれたダンプにも、デプロイ前の審査にも使えます。判定の核心は、名前の形です。vGPUプロファイルは、-12Q、-4Qのように、ハイフンの後ろに数字と大文字1文字で終わります。1台のノードが複数種類のnvidiaリソースをアドバタイズしているのも、不一致です。1台のワーカーノードは、1種類のGPUワークロードしか動かしません。アドバタイズ量が"0"のリソースは、ないものとして扱う必要があります。採点ツールは、わざとずらしたダンプにもかけます。