Skip to content
Published on

KubeVirt GPU パススルー VM はなぜ112日間スケジュールされなかったのか — 実際のクラスタ剖検

シェア
Authors

はじめに — 剖検を始める

前編で Rust で書いた GPU オペレーターが予想外の診断を下しました。GPU ノード4台がすべて NotReady、つまりクラスタの GPU 能力が丸ごと死んでいる。 この記事は、その死の原因を実際のクラスタで最後まで追跡した剖検の記録です。対象は8ノードのホームラボ(GPU Operator v25.3.0、KubeVirt v1.7.0)で、証拠はすべて実測です。この記事の背景概念が必要であれば、GPU Operator × KubeVirt 総まとめ編を先に読むとよいでしょう。

第1部 — 症状:112日間止まっている VM

まず KubeVirt VM たちの状態を見ました。実際のクラスタの VM フェーズ分布です。

$ kubectl get vm -A
フェーズ              個数   備考
────────────────  ────  ─────────────────────────────
Running              3
Paused               2
Stopped              1
ErrorUnschedulable   2    ← gpu-fedora, rhel9-gpu-vm  (どちらも GPU VM!)

gpu-fedorarhel9-gpu-vmGPU を要求する VM 2つだけが正確に ErrorUnschedulable です。ではどれくらい長く? kubectl get vm の AGE は112日。112日間スケジュールされないまま放置されていました。GPU を使わない VM たち(Running・Paused)は問題ありません。犯人は GPU 経路のどこかです。

GPU Operator オペランドのポッドたちの状態も健全ではありませんでした。

$ kubectl get pods -n gpu-operator  (状態ヒストグラム)
Running       14
Error          6     ← sandbox-device-plugin, sandbox-validator など
Terminating    4

第2部 — GPU はきちんと登録されていた

興味深い逆転です。GPU 設定そのものは非の打ちどころがありませんでした。 KubeVirt CR の permittedHostDevices を見ると、5090 と3090 が PCI ID まで正確に登録されています。

$ kubectl get kubevirt -n kubevirt -o yaml   (permittedHostDevices 抜粋)
pciHostDevices:
  - pciVendorSelector: "10de:2b85"   # NVIDIA GB202 = RTX 5090
    resourceName: nvidia.com/GB202_GEFORCE_RTX_5090
    externalResourceProvider: true
  - pciVendorSelector: "10de:2204"   # NVIDIA GA102 = RTX 3090
    resourceName: nvidia.com/GA102_GEFORCE_RTX_3090
    externalResourceProvider: true

sandboxWorkloads.enabled: truedriver.enabled: false(ホストドライバーを使用)まで、VM に GPU を渡すための舞台はすべて整っていました。それでも VM は起動しませんでした。設定が正しくてもノードがなければ意味がない — これが剖検の方向を変えました。

第3部 — 本当の原因:kubelet の1行

GPU VM たちがスケジュールされうる唯一のノード(omen2、RTX 5090 搭載)に降りて kubelet ログを見ました。犯人は華やかな GPU の問題ではなく、この1行でした。

$ journalctl -u kubelet -n 5   (omen2)
E0710 15:39:51 run.go:72] "command failed" err="failed to run Kubelet:
    running with swap on is not supported, please disable swap
    or set --fail-swap-on flag to false"
systemd[1]: kubelet.service: Main process exited, code=exited, status=1/FAILURE

systemctl is-active kubeletactivating(つまり crash-loop)。誰かがこのノードに32GB の swapfile をオンにしており、Kubernetes の kubelet はデフォルトで swap がオンだと 起動を拒否します。kubelet が死ぬとノードが NotReady になり、NFD が GPU ラベルを消し、device-plugin が nvidia.com/gpu を広告できず — その上に載っているすべてが崩れたのです。

因果の連鎖を1行で描くと、こうなります。

swap on  →  kubelet 拒否(crash-loop)  →  ノード NotReady
         →  NFD ラベル消滅 + device-plugin 停止
         →  GPU VM スケジュール不可(ErrorUnschedulable, 112日)

第4部 — パススルーの物理法則:なぜ「他のノードへ」行けないのか

通常のポッドであれば、ノードが1台死んでもスケジューラーが別のノードへ移します。GPU パススルー VM はなぜ移せないのでしょうか? 物理デバイスに縛られているからです。 gpu-fedoranvidia.com/GB202_GEFORCE_RTX_5090 というリソースを要求しますが、その物理 5090 は omen2 にのみ 挿さっています。そのノードが死ぬと、VM は行き場がありません。

これはバグではなく設計です — 私が GPU Operator × KubeVirt 編で「GPU パススルーされた VM はライブマイグレーションができない、これはバージョンに関係のない物理法則だ」と書いた、あの文の実物です。コンテナ GPU(MIGで分割して複数のポッドが共有)とは違い、VM パススルーは GPU 1枚 = ノード1台 に強く結合します。密度を諦める代わりに強い分離を得るトレードオフの影です。

第5部 — 復旧:まずラベル、その次に swap

復旧にはもう1つ罠がありました。omen2 のワークロードラベルは nvidia.com/gpu.workload.config=vm-passthrough でした。この状態でそのまま kubelet を復活させると、GPU Operator が vfio-manager を立ち上げて5090 を vfio-pci ドライバーに再バインドします — するとそのノードで動いていたすべての CUDA/torch ジョブが即座に死にます(この GPU は 小さなモデル実験編で使っていた、まさにそのカードです)。

だからこそ復旧の順序が重要です。

① (kubelet を復活させる前に) ラベルを container に変えて vfio 再バインドを防ぐ
     kubectl label node omen2 nvidia.com/gpu.workload.config=container --overwrite
② swap をオフにして永続化
     sudo swapoff -a
     sudo sed -i '/swap/s/^/#/' /etc/fstab
③ kubelet を再起動 → ノード Ready、5090 が nvidia.com/gpu としてポッドに公開
     sudo systemctl restart kubelet

①を先に行う理由が核心です — driver.enabled: false なので GPU Operator はホストドライバーをそのまま使うため、container ラベルであれば ホストの nvidia ドライバーを維持したまま GPU をポッドに公開します。つまり torch も生き、k8s も GPU を見ます。(①はすでに適用済みで、②③は sudo が必要なのでノード所有者の手が必要です。)

おわりに — 剖検の教訓

最も大きな教訓は陳腐ですが毎回新しいものです。華やかなレイヤーの障害は、たいてい退屈なレイヤーから来る。 GPU パススルー・vfio・permittedHostDevices・PCI ID まで完璧でしたが、崩したのは swap on の1行でした。2つ目の教訓はパススルーの物理学です — VM に GPU を丸ごと与えると、その VM はそのノードに釘付けになります。密度が必要ならコンテナ+MIG、分離が必要なら VM パススルー、その選択の代償は「ノードが死ねば一緒に死ぬ」です。そしてこのすべての診断の出発点が 私が Rust で書いた20行のオペレーターだったこと — インフラは自分で触れた分だけ見えます。

参考資料