Node Feature Discovery — 発見された事実がラベルになるまで
一言でいうと
GPU Operatorは、ノードに接続されたカードを直接見ずにノードのラベルを見ており、そのラベルは、Node Feature Discovery(NFD)とGPU Feature Discovery(GFD)とオペレーター自身が、それぞれ分担して付けます。そのため、ラベルがないと、エラーも警告もなく、ただ何も起きません。
なぜ必要なのか
GPUノードを扱うソフトウェアは、最低でも3つのことを知る必要があります。このノードにNVIDIAのカードがあるか、何枚で、どのモデルか、ドライバーはどのバージョンか。これを調べる方法は、もともとノードに入ってlspciを実行し、nvidia-smiを動かすことでした。ノードが3台ならやれますが、300台なら不可能です。
Kubernetesがこの問題に出した答えは、「ハードウェアの事実をノードオブジェクトのラベルとして載せておこう」というものでした。いったんラベルになってしまえば、そのあとはすべて、Kubernetesの日常的なツールで解決できます。スケジューラーのnodeSelector・nodeAffinityがそのラベルを読み、DaemonSetのnodeSelectorがそのラベルで配置を決め、人はkubectl get node -Lで一目で見ます。ハードウェアを調べる問題が、ラベルを調べる問題に変わったことが、この設計のすべてです。
NFDは、この仕事を担うKubernetesのSIGプロジェクトです。構造は2つに分かれます。各ノードでDaemonSetとして動くnfd-workerが、CPU・カーネル・PCI・USB・メモリ・ストレージ・ネットワークといったカテゴリで事実を集めて報告し、nfd-masterがその報告を受けて、ノードオブジェクトにラベルを書きます。ワーカーが直接ノードを書き換えず、マスターを経由させているのは、ノードオブジェクトを書き換える権限を1か所に集めるためです。
どう動くのか
ラベルは、プレフィックスで由来がわかります。この1点だけ覚えておいても、画面を読む速度が変わります。
| プレフィックス | 付与する主体 | 例 |
|---|---|---|
feature.node.kubernetes.io/ |
nfd-workerの報告を受けたnfd-master | pci-10de.present, kernel-version.full, system-os_release.ID |
nvidia.com/gpu.*, nvidia.com/cuda.* |
gpu-feature-discovery | gpu.product, gpu.count, gpu.memory, cuda.driver.major |
nvidia.com/gpu.deploy.* |
GPU Operator | gpu.deploy.driver, gpu.deploy.container-toolkit |
NVIDIAのドキュメントは、GPUワーカーノードを、feature.node.kubernetes.io/pci-10de.present=trueラベルの存在で識別すると書いています。0x10deはNVIDIAに割り当てられたPCIベンダーIDです。NFDのPCIラベルは、既定では<class>_<vendor>形式のデバイス名を使いますが、GPU Operatorは、ベンダーだけを見るように設定して使います。
GFDが付けるラベルは、より具体的です。公式ドキュメントの出力例をそのまま移すと、このような形です。
{
"nvidia.com/cuda.driver.major": "450",
"nvidia.com/cuda.driver.minor": "80",
"nvidia.com/cuda.driver.rev": "02",
"nvidia.com/cuda.runtime.major": "11",
"nvidia.com/cuda.runtime.minor": "0",
"nvidia.com/gpu.compute.major": "8",
"nvidia.com/gpu.count": "1",
"nvidia.com/gpu.family": "ampere",
"nvidia.com/gpu.memory": "40537",
"nvidia.com/gpu.product": "A100-SXM4-40GB"
}
ドライバーのバージョンが、major・minor・revの3つに分かれている点に注目する価値があります。ラベル1つに450.80.02を丸ごと入れなかった理由は、比較のためです。nodeAffinityのGt・Lt演算子は値を整数として読むので、パーツに分けておかないと、「ドライバー550以上のノード」のような条件を書けません。
ラベルの値には、構文の制約があります。63文字以下、英数字で始まって終わり、途中には-・_・.だけが来られます。ところが、ドライバーが教えてくれるデバイス名は、NVIDIA A100-SXM4-40GBのように空白が入っています。そのまま入れようとすると、APIサーバーがInvalid valueで拒否します。そのため、GFDは名前を整えて付けており、私たちが画面で見るNVIDIA-A100-SXM4-40GBは、その正規化の結果です。この事実を知らないと、「ドキュメントに書かれたモデル名で条件をかけたのに、どのノードにも合わない」という場所で、長い時間を過ごします。
ラベルを読む側にも規則があります。nodeSelectorは、キーと値がちょうど同じかどうかだけを見ますが、nodeAffinityは、In・NotIn・Exists・DoesNotExist・Gt・Ltを使えます。ここで、よく間違えられる構造が1つあります。
nodeSelectorTermsはリストで、項目同士はORです。1つだけ合っても、そのノードは候補になります。- 1つの項目の中の
matchExpressionsはANDです。すべて合っている必要があります。
そしてNotInは、そのキーがそもそもないノードも通過させます。「A10だけを除いてGPUノードに」をNotIn1つだけで書くと、GPUがないノードまで候補になります。Existsを一緒にかけて、先に範囲を絞るのが定番の書き方になった理由です。
名前の末尾のIgnoredDuringExecutionも、ただ付いている言葉ではありません。requiredDuringSchedulingIgnoredDuringExecutionは、スケジュールするときだけ条件を要求し、すでに起動したPodは、条件が壊れても追い出しません。そのため、ラベルを間違って消しても、動いていたワークロードは問題なく、次に新しく起動するPodから、静かにPendingになります。障害が数時間後に表面化する構造です。
ラベルの所有権
NFDが付けたラベルは、NFDのものです。ワーカーが定期的に再報告し、マスターがノードをその報告に合わせるので、人が手で直した値は、次の周期で静かに元に戻ります。初めて経験すると、「ラベルが何度も復活する」という幽霊を追いかけることになりますが、わかってみれば当然の動作です。検出された事実を人が上書きできるなら、そのラベルを信じる理由がなくなるからです。
そのため、人が決める事実は、別のキーに書きます。チームの所有、ワークロードの等級、メンテナンス窓口のようなものは、自社のプレフィックスを使ったラベルに書き、検出ラベルは読むだけにします。逆に、NFDに人のルールを実行させたいなら、NodeFeatureRuleでルールを宣言して、NFDに付けさせます。そうすれば、そのラベルもNFDのものになり、一貫して管理されます。
現場での姿
第一に、何も起きません。GPU Operatorをインストールしたのにオペランドが1つも起動しないという報告の、最もよくある原因は、pci-10de.presentラベルがないことです。NFDを別にインストールしたときにラベルのプレフィックスの設定が変わったか、ノードが新しく入ってきたのに、nfd-workerのDaemonSetがそのノードで起動しなかったか。どちらの場合も、エラーログはありません。条件に合わなければ配置が起きず、起きなかったことは、ログを残さないからです。
第二に、モデル名が微妙に違います。nvidia.com/gpu.productは、GFDの正規化を通った値で、MIGを有効にすると、NVIDIA-H100-80GB-HBM3-MIG-1g.10gbのようにサフィックスが付きます。タイムスライシングを有効にすると、また別のサフィックスが付きます。そのため、モデル名で条件をかけるときは、ドキュメントの名前ではなく、今のクラスターのラベルの値を見る必要があります。
第三に、ラベルのチェッカーを持ち歩くチームは速いです。ノードがGPUノードだと主張しながら、モデル・枚数・メモリのどれかが空だったり、数字であるべき値が数字でなかったりすることは、実際に起きます。これを人の目で見つける代わりに、小さなスクリプトで見つけるようにしておくと、ノードが新しく入るたびに、30秒で確認が終わります。重要なのは、そのスクリプトがノード一覧をその都度取得することです。ノード名を埋め込んだチェッカーは、増設した日から嘘をつき始めます。
第四に、この環境の正直な限界です。ラボの環境には、GPUもNFDもGFDもありません。そのため、「検出」はラベルを使って自分で作ります。その代わり、ラベル値の検証、nodeAffinityの評価、スケジューラーの配置判断、ノードの追加は、kwokが起動した本物のAPIサーバーと本物のスケジューラーが行います。模擬ではなく実物です。
参考ドキュメント
- Node Feature Discoveryを始める: https://kubernetes-sigs.github.io/node-feature-discovery/stable/get-started/index.html
- NFDが付ける機能ラベルの一覧: https://kubernetes-sigs.github.io/node-feature-discovery/stable/usage/features.html
- GPU Feature Discoveryが付けるラベル(MIG戦略のドキュメントの出力例): https://docs.nvidia.com/datacenter/cloud-native/kubernetes/latest/index.html
- Podをノードに配置する(nodeSelector・nodeAffinity): https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/
- ラベルとセレクター(値の構文の制約): https://kubernetes.io/docs/concepts/overview/working-with-objects/labels/
次のラボですること
kwokが起動した本物のAPIサーバー上で、3つの由来のラベルを直接作ります。空白の入ったデバイス名を入れて、APIサーバーに拒否されてみて、その名前をラベルの値として整えるツールを作ります。In・NotIn・Exists・Gtとtermのリストで、Podがどのノードに行くかをスケジューラーに直接確認させ、ラベルを消したあとも動いていたPodが残ることと、新しいPodだけがPendingになることを、目で確かめます。最後に、検出結果を元に戻す同期ツールと、新しいノードが入っても嘘をつかないラベル規約チェッカーを作ります。