workload.config — ラベル一つがノードの正体を決める
一言でいうと
GPU Operatorは、nvidia.com/gpu.workload.configラベルの値(container・vm-passthrough・vm-vgpu)を見て、そのノードにどのオペランドを載せるかを丸ごと入れ替え、その結果、ノードがアドバタイズするリソース名まで変わります。コンテナノードはnvidia.com/gpuをアドバタイズし、パススルーのノードはnvidia.com/GA102GL_A10のようなデバイスモデル名を、vGPUノードはnvidia.com/NVIDIA_A10-12Qのようなプロファイル名をアドバタイズします。
なぜ必要なのか
GPUを使うワークロードが、すべてコンテナというわけではありません。ライセンスが特定のOSを要求したり、Windowsドライバーが必要だったり、レガシーなシミュレーションが丸ごと仮想マシンの中にあったりする場合が、実際に多くあります。そのような組織は、長い間クラスターを2つに分けて運用してきました。コンテナ用のKubernetes1つと、仮想マシン用のハイパーバイザー1つです。同じGPUの在庫を2か所に分けておくと、片方が遊んでいても、もう片方に貸せません。
KubeVirtは、仮想マシンをKubernetesのオブジェクトにすることで、その壁をなくし、GPU Operatorは、その上でワーカーノードを2種類の用途にそれぞれ準備できるようになりました。公式ドキュメントは、その違いを1行で要約しています。コンテナにはデータセンタードライバーが、GPUパススルーにはvfio-pciドライバーが、vGPUにはNVIDIA vGPU Managerが必要です。必要なドライバーが違うことが、すべての違いの根っこです。
ラベル1つがノードのソフトウェアを丸ごと変える
ノードにラベルを付けるのがすべてです。
kubectl label node <노드이름> --overwrite nvidia.com/gpu.workload.config=vm-vgpu
すると、オペレーターがそのノードに載せるオペランドが変わります。
| ラベルの値 | そのノードに載るもの | 役割 |
|---|---|---|
container |
データセンタードライバー・コンテナツールキット・Kubernetesデバイスプラグイン・DCGMとDCGM Exporter | コンテナがGPUを使えるようにして、メトリクスを出力します |
vm-passthrough |
VFIO Manager・サンドボックスデバイスプラグイン | vfio-pciを読み込んで、そのノードのすべてのGPUにバインドし、パススルーGPUをkubeletにアドバタイズします |
vm-vgpu |
vGPU Manager・vGPU Device Manager・サンドボックスデバイスプラグイン | vGPUドライバーをインストールし、vGPUデバイスを作成してから、kubeletにアドバタイズします |
ここで必ず押さえるべき文が2つあります。
第一に、ラベルがなければ、既定値はcontainerです。オペレーターは、ラベルのないノードをコンテナ用に準備します。既定を変えるには、ClusterPolicyのsandboxWorkloads.defaultWorkloadを直します。
第二に、このラベルは、sandboxWorkloads.enabledが有効なときにしか使われません。そのフラグは既定でオフで、オフならすべてのノードがコンテナ用に準備され、ラベルはそもそも読まれません。ラベルだけを付けて、なぜ変わらないのかと何時間も見ている場所が、ここです。ラベルは構文エラーでもなく、警告も出しません。
そして、公式ドキュメントが固定する制約がもう1つあります。1台のワーカーノードは、1種類のGPUワークロードしか動かしません。コンテナとパススルーVMを、1台のノードで混在させることはできません。vfio-pciがそのノードのGPUをすべて持っていくので、ホストのデータセンタードライバーがそのカードを手放す必要があるからです。在庫計画がノード単位で固まるという意味で、これが実務で最も大きく体感される制約です。
リソース名が変わる
コンテナノードがアドバタイズするのは、私たちがよく知っているnvidia.com/gpuです。サンドボックスのノードは違います。公式ドキュメントの出力例が、そのまま語ってくれます。
# 패스스루 노드
$ kubectl get node <노드> -o json | jq '.status.allocatable | with_entries(select(.key | startswith("nvidia.com/")))'
{ "nvidia.com/GA102GL_A10": "1" }
# vGPU 노드 (기본 설정: 카드마다 절반 크기 Q 프로파일 두 개)
{ "nvidia.com/NVIDIA_A10-12Q": "4" }
前者はPCIデバイスのモデル名から、後者はvGPUプロファイル名から来ています。A10のカード2枚に既定の設定を適用すると、カードごとに12Qが2つできて、合計4つになります。ここで、ノードのラベルnvidia.com/vgpu.configでプロファイルを変えられます。A10-4Qを選ぶと、カードごとに6つずつ、2枚なら12個がアドバタイズされます。同じハードウェアなのに、ラベル1つでアドバタイズ量とリソース名が一緒に変わります。
名前が違うことの結果は、単純で厳しいものです。nvidia.com/gpuを要求するPodは、パススルーノードには絶対に行けません。空きが残っていても、スケジューラーにとって、そのノードはそのリソースを持たないノードです。逆も同じです。クラスターがカードの在庫を分けておくと、片方で待ち行列が長くても、もう片方の空きは空いたまま残ります。
VMにカードを接続する形
デバイスがアドバタイズされたからといって、VMがすぐ使えるわけではありません。KubeVirt側に許可リストを別に書く必要があります。これが、人が最も抜かしやすい段階です。KubeVirtカスタムリソースのpermittedHostDevicesの下に、パススルーならpciHostDevices、vGPUならmediatedDevicesとして書き、どちらにもexternalResourceProvider: trueを付けます。そのフラグは、「このリソースは自分たちが作ったものではなく、外部のデバイスプラグイン(ここではサンドボックスデバイスプラグイン)がアドバタイズする」という意味です。
spec:
configuration:
developerConfiguration:
featureGates:
- GPU
- DisableMDEVConfiguration
permittedHostDevices:
pciHostDevices:
- externalResourceProvider: true
pciVendorSelector: 10DE:2236
resourceName: nvidia.com/GA102GL_A10
そのあとで初めて、VM側からカードを要求できます。Podのresources.limitsとは書く場所が違います。VirtualMachineInstanceは、spec.domain.devices.gpusに書きます。
spec:
domain:
devices:
gpus:
- deviceName: nvidia.com/GA102GL_A10
name: gpu1
deviceNameがリソース名で、nameは、VMの中でそのデバイスを呼ぶエイリアスです。見た目は違いますが、スケジューリング段階で起きることはPodと同じです。VMを代わりに実行するPodがそのリソースを要求し、そのリソースをアドバタイズするノードに行きます。
最後に、公式ドキュメントがはっきり書いている事実が1つあります。GPU Operatorは、VMの中のNVIDIAドライバーのインストールを自動化しません。カードをVMに接続するところまでがその仕事で、ゲストOSの中のドライバーは、VMイメージを作る側の担当です。
現場での姿
第一に、BIOSとカーネルパラメーターが前提条件です。仮想化拡張とIOMMUがBIOSで有効になっている必要があり、カーネルのコマンドラインにintel_iommu=onまたはamd_iommu=onがある必要があります。Ampere以降のカードでvGPUを使うには、BIOSでSR-IOVも有効にする必要があります。これらは再起動が必要な変更なので、ノードをクラスターに入れる前に、イメージの段階で済ませておかないと、あとでノードを1台ずつ空にしながら直すことになります。
第二に、ノードを用途の間で移すことは、無停止ではありません。ラベルを変えるとオペランドが入れ替わり、ドライバーが変わります。公式ドキュメントは、vGPUの設定を変えるとき、そのノードで動いていたVMを先に停止するか移動するようにと書いています。在庫を柔軟に回そうとする計画は、この点で現実と出会います。1日単位で移すことはできますが、分単位ではできません。
第三に、リソース名がカードのモデルに結び付いていて、マニフェストがハードウェアに縛られます。nvidia.com/GA102GL_A10を要求するVMのマニフェストは、A10がないクラスターでは、ずっとPendingです。コンテナ側のnvidia.com/gpuがモデルと無関係だったこととは対照的です。そのため、サンドボックスワークロードを使う組織は、モデル名をマニフェストに直接書かず、Helmの値やkustomizeのパッチで1か所にまとめておきます。
第四に、この環境の正直な限界です。ラボのクラスターには、KubeVirtもvfio-pciもなく、VMは起動できません。そのため、次のラボでは、VMの要求を同じリソース名を要求するPodとして作ります。スケジューリング段階で起きることが実際に同じなので(どちらも拡張リソースを要求するPodです)、どこに行き、どこで止まるのかは、実物で学べます。ただし、VMが実際に起動してカードを確保する後半は確認できず、その部分は、上のVMIマニフェストの形で代用します。
参考ドキュメント
- GPU OperatorとKubeVirt(ワークロードラベル・オペランド・リソース名): https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/gpu-operator-kubevirt.html
- GPU Operatorの概要: https://docs.nvidia.com/datacenter/cloud-native/gpu-operator/latest/index.html
- KubeVirtのホストデバイス割り当て: https://kubevirt.io/user-guide/compute/host-devices/
- ノードに拡張リソースをアドバタイズする: https://kubernetes.io/docs/tasks/administer-cluster/extended-resource-node/
- Podをノードに割り当てる: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/
次のラボですること
3台のノードを、それぞれcontainer・vm-passthrough・vm-vgpuでラベル付けして、各ラベルに合うリソース名をノードのstatusに作ります。ラベル値ごとに、どのオペランドが起動する必要があるかを表ファイルにまとめて、3種類のワークロード(コンテナのPod・パススルーの要求・vGPUの要求)を載せ、それぞれが自分のノードに行くことと、他のノードのリソースを要求するとどこにも起動できないことを、実際のスケジューラーで確認します。最後に、ノードのダンプを受け取って、「このラベルとこのリソースのアドバタイズが合っているか」を判定するチェッカーを作り、わざとずらしたノードを、そのチェッカーが見つけ出すかを見ます。