TT Lab
はじめる
学ぶ 学習パス コース

GPU Operatorとタイムスライシング

カーネルが一段上がった途端、そのノードだけドライバーが無い

TT Labで続きを見る

目標

ノードのラベルを事実の置き場として、事前コンパイル済みドライバーイメージのタグを作るツールと、ずれたノードを見つけ出すチェッカーを作り、ノードが増えたときに何が先に必要になるかを体験し、PodDisruptionBudgetが止める場面を通過して、ドレインからuncordonまで、アップグレードの手順を最後まで進めます。

なぜ重要なのか

GPUドライバーは、アプリケーションではなくカーネルモジュールです。GPU Operatorがドライバーをコンテナとして載せても、そのコンテナが行うことは、ホストカーネルにモジュールを読み込むことであり、そのため、カーネルが変わればドライバーも、そのカーネルに合わせて再ビルドされる必要があります。事前コンパイル済みドライバーイメージのタグが<드라이버브랜치>-<커널판>-<OS태그>であるのは(プレースホルダーはドライバーブランチとカーネルバージョンとOSタグです)、この事実を名前に埋め込んだものです。ここから2つのことが導かれます。1つ目は、ノードが1台増えたり、カーネルが1つ上がったりすることは、すなわちイメージの作業です。知らずに通り過ぎると、そのノードでだけ、ドライバーのPodがイメージなしで落ちます。2つ目は、ドライバーを載せ替えるには、そのノードのGPUワークロードを先に下ろす必要があり、その操作はeviction APIを通るので、PodDisruptionBudgetがアップグレードを止めます。アップグレードが1台のノードで止まったという報告の相当数が、ドライバーの問題ではなくバジェットの問題で、原因を知らないと、ドライバーのログだけを何時間も読むことになります。

ステップ

  1. /root/gpudrvで作業します(export KUBECONFIG=/root/.kube/config)。まずネームスペースgpu-drvを作成してください(後のステップのPodがここで起動します)。3台のノードにラベルを付けてください。lab-node-0: nvidia.com/gpu.present=true、nvidia.com/cuda.driver.major=550、nvidia.com/cuda.driver.minor=90、nvidia.com/cuda.driver.rev=07、feature.node.kubernetes.io/kernel-version.full=5.15.0-119-generic、feature.node.kubernetes.io/system-os_release.ID=ubuntu、feature.node.kubernetes.io/system-os_release.VERSION_ID=22.04。lab-node-1: 同じキーで、ドライバーは535/183/06、カーネルは5.15.0-107-generic、ubuntuと22.04。lab-node-2: ドライバーは550/90/07、カーネルは6.8.0-45-generic、ubuntuと24.04。3台ともnvidia.com/gpu.present=trueです。
  2. /root/gpudrv/drv-tag.sh <노드이름>を作成してください(プレースホルダーはノード名です)。そのノードのラベルを読んで、事前コンパイル済みドライバーイメージのタグを1行で出力します。形式は公式ドキュメントの<드라이버브랜치>-<커널판>-<OS태그>で(プレースホルダーはドライバーブランチとカーネルバージョンとOSタグです)、ブランチはnvidia.com/cuda.driver.major、カーネルバージョンはfeature.node.kubernetes.io/kernel-version.full、OSタグはsystem-os_release.IDとsystem-os_release.VERSION_IDを連結したものです(例: ubuntuと22.04ならubuntu22.04)。3台のノードに順番に実行して、/root/gpudrv/out/tags.txtに、<노드> <태그>の形で3行を書いてください(プレースホルダーはノードとタグです)。ノード名やタグをスクリプトの中に書いておかないでください。採点ツールが、ノードごとに直接呼び出します。
  3. /root/gpudrv/support-matrix.csvを作成してください。1行目はヘッダーdriver_branch,kernel,os_tagで、その後ろに、社内レジストリに実際にビルドしてある組み合わせを3行書きます。550,5.15.0-119-generic,ubuntu22.04、535,5.15.0-107-generic,ubuntu22.04、550,6.8.0-45-generic,ubuntu24.04です。そして/root/gpudrv/drv-audit.shを作成してください。nvidia.com/gpu.present=trueのすべてのノードを回り、そのノードの組み合わせがこの表にあるかを確認して、なければ<노드> MISSING <태그>を1行ずつ出力して終了コード1で、1つもなければOKの1行と0で終わります(プレースホルダーはノードとタグです)。作成したら実行して、出力を/root/gpudrv/out/audit.txtに保存してください(今はOKになっている必要があります)。
  4. /root/gpudrv/k8s/node3.yamlで、新しいノードlab-node-3をクラスターに入れてください。ラベルはnvidia.com/gpu.present=true、ドライバー550/90/07、カーネル6.8.0-52-generic、ubuntuと24.04で、kwok.x-k8s.io/node: fakeアノテーションとReady条件を備えた偽のノードです(形式は例を見てください)。適用したあと、drv-audit.shを実行して、出力を/root/gpudrv/out/skew.txtに保存してください。新しいノードが引っかかるのが正常です。そのあと、その組み合わせをビルドしてレジストリに載せたことにして、support-matrix.csvに行を1つ追加し、もう一度実行した出力を/root/gpudrv/out/skew-fixed.txtに保存してください(今度はOKになっている必要があります)。
  5. ステップ1で作成したネームスペースgpu-drvに、Podを2つ起動します。/root/gpudrv/k8s/cuda12-job.yamlに、cuda12-jobというPodを書いてください。コンテナ名はtrainer、イメージはnvcr.io/nvidia/pytorch:24.07-py3、required nodeAffinityの1つのterm内に条件が2つです。nvidia.com/cuda.driver.majorがInで550、そしてfeature.node.kubernetes.io/system-os_release.VERSION_IDがInで22.04です。/root/gpudrv/k8s/cuda13-job.yamlに、cuda13-jobというPodを書いてください。イメージはnvcr.io/nvidia/pytorch:25.03-py3、条件はnvidia.com/cuda.driver.majorがGtで560の1つだけです。両方を適用したあと、cuda12-jobはlab-node-0で起動し、cuda13-jobは待機することを確認して、cuda13-jobのPodScheduled条件のメッセージを/root/gpudrv/out/cuda.txtに保存してください。
  6. /root/gpudrv/k8s/trainer.yamlに、trainerというDeploymentを書いてください。ネームスペースgpu-drv、replicas: 4、Podのラベルとセレクターはapp: trainer、コンテナtrainer、イメージnvcr.io/nvidia/pytorch:24.07-py3、そしてrequired podAntiAffinityで、topologyKey: kubernetes.io/hostnameについて、同じapp: trainer同士が1つのノードに2つ来ないようにしてください(ノード4台に1つずつ分散されます)。/root/gpudrv/k8s/pdb.yamlに、trainer-pdbというPodDisruptionBudgetを書いてください。minAvailable: 4、セレクターはapp: trainerです。4つのPodがすべてRunningになったら、kubectl drain lab-node-1 --ignore-daemonsets --delete-emptydir-data --timeout=20sを実行して、出力を標準エラー出力も含めて/root/gpudrv/out/drain-blocked.txtに保存してください。拒否されるのが正常です。
  7. アップグレードの順序をそのままたどります。(1): lab-node-1をドライバー550.90.07に上げると、必要なタグは550-5.15.0-107-generic-ubuntu22.04です。その組み合わせを、先にsupport-matrix.csvに追加してください(イメージを確保したことになります)。(2): バジェットに余裕を作ってください。trainer-pdbのminAvailableを3に下げます。(3): 同じdrainコマンドをもう一度実行して、今度は成功させ、出力を/root/gpudrv/out/drain-ok.txtに保存してください。(4): lab-node-1のドライバーラベル3つを550/90/07に変更してください(ドライバーを新しく載せたことになります。この環境で実際のインストールは行いません)。(5): kubectl uncordon lab-node-1でノードを元に戻してください。最後に、drv-audit.shをもう一度実行して、OKが出ることを確認してください。
  8. GPU Operatorのアップグレードコントローラーは、ノードのラベルnvidia.com/gpu-driver-upgrade-stateで進行状況を表します。/root/gpudrv/out/upgrade-states.txtに、その状態をドキュメントに出てくる順に8行書いてください。upgrade-required、cordon-required、pod-deletion-required、drain-required、pod-restart-required、validation-required、uncordon-required、upgrade-doneです。そして、lab-node-1にnvidia.com/gpu-driver-upgrade-state=upgrade-doneラベルを付けてください。最後に、/root/gpudrv/out/upgrade-report.txtに5行を書いてください。NODES=<gpu.present 가 true 인 노드 수>、SKEW=<drv-audit.sh 가 낸 MISSING 줄 수>、LAB_NODE_1_TAG=<drv-tag.sh 가 lab-node-1 에 대해 내는 태그>、MATRIX_ROWS=<support-matrix.csv 의 머리글을 뺀 줄 수>、UPGRADE_STATE=upgrade-doneです(プレースホルダーは、gpu.presentがtrueのノード数、drv-audit.shが出力したMISSING行の数、drv-tag.shがlab-node-1について出力するタグ、support-matrix.csvのヘッダー行を除いた行数です)。数字とタグは、今の状態からコマンドで取り出して埋めてください。

参考

カーネルバージョンとドライバーバージョンを、ノードの事実として作る

/root/gpudrvで作業します(export KUBECONFIG=/root/.kube/config)。まずネームスペースgpu-drvを作成してください(後のステップのPodがここで起動します)。3台のノードにラベルを付けてください。lab-node-0: nvidia.com/gpu.present=true、nvidia.com/cuda.driver.major=550、nvidia.com/cuda.driver.minor=90、nvidia.com/cuda.driver.rev=07、feature.node.kubernetes.io/kernel-version.full=5.15.0-119-generic、feature.node.kubernetes.io/system-os_release.ID=ubuntu、feature.node.kubernetes.io/system-os_release.VERSION_ID=22.04。lab-node-1: 同じキーで、ドライバーは535/183/06、カーネルは5.15.0-107-generic、ubuntuと22.04。lab-node-2: ドライバーは550/90/07、カーネルは6.8.0-45-generic、ubuntuと24.04。3台ともnvidia.com/gpu.present=trueです。

この環境にはGPUもドライバーもないので、ラベルが事実の置き場です。実際のクラスターでは、カーネルとOSのラベルはnfd-workerが、nvidia.com/cuda.driver.*はgpu-feature-discoveryが付けます。ドライバーバージョンは、major.minor.revの3つにラベルが分かれます。550.90.07なら、それぞれ550、90、07です。kubectl label node <이름> <키>=<값> --overwriteで、一度に複数を付けられます(プレースホルダーはノード名とキーと値です)。22.04のようにドットが入った値は、ラベル値の規約に反しません。

ノード1台が必要とするドライバーイメージのタグを計算する

/root/gpudrv/drv-tag.sh <노드이름>を作成してください(プレースホルダーはノード名です)。そのノードのラベルを読んで、事前コンパイル済みドライバーイメージのタグを1行で出力します。形式は公式ドキュメントの<드라이버브랜치>-<커널판>-<OS태그>で(プレースホルダーはドライバーブランチとカーネルバージョンとOSタグです)、ブランチはnvidia.com/cuda.driver.major、カーネルバージョンはfeature.node.kubernetes.io/kernel-version.full、OSタグはsystem-os_release.IDとsystem-os_release.VERSION_IDを連結したものです(例: ubuntuと22.04ならubuntu22.04)。3台のノードに順番に実行して、/root/gpudrv/out/tags.txtに、<노드> <태그>の形で3行を書いてください(プレースホルダーはノードとタグです)。ノード名やタグをスクリプトの中に書いておかないでください。採点ツールが、ノードごとに直接呼び出します。

NVIDIAのドキュメントの例のタグは、525-5.15.0-69-generic-ubuntu22.04です。ブランチの後ろにカーネルバージョンが来て、その後ろにOSタグが来ます。カーネルバージョンの中にもハイフンがあるので、タグを逆から分割して読もうとすると混乱します。作るときは、部品をそれぞれラベルから取り出して連結すればよいです。ラベルが1つでもないと、タグが静かにおかしくなります。空の値に出会ったら、エラーで終了するほうがよいです。jqでラベルを取り出すときに//を使うと、ないラベルと空の値が区別できません。

レジストリにある組み合わせと照合して、ずれたノードを見つける

/root/gpudrv/support-matrix.csvを作成してください。1行目はヘッダーdriver_branch,kernel,os_tagで、その後ろに、社内レジストリに実際にビルドしてある組み合わせを3行書きます。550,5.15.0-119-generic,ubuntu22.04、535,5.15.0-107-generic,ubuntu22.04、550,6.8.0-45-generic,ubuntu24.04です。そして/root/gpudrv/drv-audit.shを作成してください。nvidia.com/gpu.present=trueのすべてのノードを回り、そのノードの組み合わせがこの表にあるかを確認して、なければ<노드> MISSING <태그>を1行ずつ出力して終了コード1で、1つもなければOKの1行と0で終わります(プレースホルダーはノードとタグです)。作成したら実行して、出力を/root/gpudrv/out/audit.txtに保存してください(今はOKになっている必要があります)。

この表が、そのまま「自分たちが持っているドライバーイメージの一覧」です。実際には、NGCレジストリのタグ一覧か、社内でビルドして載せたイメージの一覧で、どちらでも、ないタグは、PodがImagePullBackOffで落ちることでしか表面化しません。そのため、カーネルを上げる前に、この照合を先に行うのが順序です。ノード一覧は、その都度取得してください。採点ツールが、ノード1台のカーネルラベルを一時的に変えてから呼び出します。タグを作り直す必要はありません。ステップ2で作成したスクリプトを呼べばよいです。

ノードが1台増えると、まずドライバーイメージが足りなくなる

/root/gpudrv/k8s/node3.yamlで、新しいノードlab-node-3をクラスターに入れてください。ラベルはnvidia.com/gpu.present=true、ドライバー550/90/07、カーネル6.8.0-52-generic、ubuntuと24.04で、kwok.x-k8s.io/node: fakeアノテーションとReady条件を備えた偽のノードです(形式は例を見てください)。適用したあと、drv-audit.shを実行して、出力を/root/gpudrv/out/skew.txtに保存してください。新しいノードが引っかかるのが正常です。そのあと、その組み合わせをビルドしてレジストリに載せたことにして、support-matrix.csvに行を1つ追加し、もう一度実行した出力を/root/gpudrv/out/skew-fixed.txtに保存してください(今度はOKになっている必要があります)。

新しいノードは、たいてい最新のイメージでインストールされているので、カーネルが既存のノードより先に進んでいます。ドライバーブランチが同じでも、カーネルバージョンが違えば、別のイメージが必要です。事前コンパイル済みドライバーのタグにカーネルバージョンが入っている理由です。ノードの追加がすなわちイメージのビルド作業だという事実を知らないと、新しいノードでだけドライバーのPodがイメージなしで落ちるという場面で、迷います。この環境では、ノードもただのAPIオブジェクトなので、kubectl applyで作成できます。CSVに行を追加するときは、ヘッダーの順序(ブランチ、カーネル、OSタグ)を守ってください。

CUDAは一方向にだけ互換性がある: その要件をラベルの条件で書く

ステップ1で作成したネームスペースgpu-drvに、Podを2つ起動します。/root/gpudrv/k8s/cuda12-job.yamlに、cuda12-jobというPodを書いてください。コンテナ名はtrainer、イメージはnvcr.io/nvidia/pytorch:24.07-py3、required nodeAffinityの1つのterm内に条件が2つです。nvidia.com/cuda.driver.majorがInで550、そしてfeature.node.kubernetes.io/system-os_release.VERSION_IDがInで22.04です。/root/gpudrv/k8s/cuda13-job.yamlに、cuda13-jobというPodを書いてください。イメージはnvcr.io/nvidia/pytorch:25.03-py3、条件はnvidia.com/cuda.driver.majorがGtで560の1つだけです。両方を適用したあと、cuda12-jobはlab-node-0で起動し、cuda13-jobは待機することを確認して、cuda13-jobのPodScheduled条件のメッセージを/root/gpudrv/out/cuda.txtに保存してください。

ドライバーは、自分より後に出たCUDAランタイムを知りません。逆に、古いCUDAコンテナは新しいドライバーでうまく動きます。互換性が片方向にだけ開いているという意味です。そのため、「このコンテナはドライバーのバージョンがいくつ以上必要」を、ノードのラベルの条件で書いておくと、合わないノードに行って実行中に失敗する代わりに、スケジュール段階で止まります。2つの条件を1つのtermに入れると、両方を満たす必要があります。待機理由は、.status.conditionsのPodScheduledのメッセージにあります。Gtは値を整数として読むので、ブランチ番号のように整数のラベルにだけ使えます。

アップグレードを止めるのは、ドライバーではなくバジェットである

/root/gpudrv/k8s/trainer.yamlに、trainerというDeploymentを書いてください。ネームスペースgpu-drv、replicas: 4、Podのラベルとセレクターはapp: trainer、コンテナtrainer、イメージnvcr.io/nvidia/pytorch:24.07-py3、そしてrequired podAntiAffinityで、topologyKey: kubernetes.io/hostnameについて、同じapp: trainer同士が1つのノードに2つ来ないようにしてください(ノード4台に1つずつ分散されます)。/root/gpudrv/k8s/pdb.yamlに、trainer-pdbというPodDisruptionBudgetを書いてください。minAvailable: 4、セレクターはapp: trainerです。4つのPodがすべてRunningになったら、kubectl drain lab-node-1 --ignore-daemonsets --delete-emptydir-data --timeout=20sを実行して、出力を標準エラー出力も含めて/root/gpudrv/out/drain-blocked.txtに保存してください。拒否されるのが正常です。

ドライバーを載せ替えるには、そのノードのGPUワークロードを先に下ろす必要があり、下ろす操作はeviction APIを通ります。PodDisruptionBudgetは、まさにそのAPIを止める仕組みです。現在生きている数がバジェットを下回ると、拒否します。そのため、「ドライバーのアップグレードが1台のノードで止まった」の本当の原因が、ドライバーではなくバジェットである場合が、よくあります。drainは、ノードを先にcordonしてから、Podを1つずつevictします。そのため、止まってもノードはすでにスケジュール不可の状態です。--timeoutを指定しないと、drainは永遠に再試行します。

先にイメージを確保し、余裕を作り、手順を最後までたどる

アップグレードの順序をそのままたどります。(1): lab-node-1をドライバー550.90.07に上げると、必要なタグは550-5.15.0-107-generic-ubuntu22.04です。その組み合わせを、先にsupport-matrix.csvに追加してください(イメージを確保したことになります)。(2): バジェットに余裕を作ってください。trainer-pdbのminAvailableを3に下げます。(3): 同じdrainコマンドをもう一度実行して、今度は成功させ、出力を/root/gpudrv/out/drain-ok.txtに保存してください。(4): lab-node-1のドライバーラベル3つを550/90/07に変更してください(ドライバーを新しく載せたことになります。この環境で実際のインストールは行いません)。(5): kubectl uncordon lab-node-1でノードを元に戻してください。最後に、drv-audit.shをもう一度実行して、OKが出ることを確認してください。

順序が、このステップのすべてです。イメージを確保する前にドレインすると、ノードを空にしたまま待つことになり、バジェットに余裕を作る前にドレインすると、前のステップのように拒否されます。PDBは、kubectl patch pdb <이름> --type=merge -p '{"spec":{"minAvailable":N}}'で修正できます(プレースホルダーはPDB名です)。バジェットを修正した直後は、コントローラーがstatus.disruptionsAllowedを再計算する時間が、数秒必要です。ドレインが終わると、そのノードはcordonされたまま残ります。uncordonを忘れると、そのノードは静かに遊ぶことになります。

アップグレードのステートマシンと、締めくくりのレポート

GPU Operatorのアップグレードコントローラーは、ノードのラベルnvidia.com/gpu-driver-upgrade-stateで進行状況を表します。/root/gpudrv/out/upgrade-states.txtに、その状態をドキュメントに出てくる順に8行書いてください。upgrade-required、cordon-required、pod-deletion-required、drain-required、pod-restart-required、validation-required、uncordon-required、upgrade-doneです。そして、lab-node-1にnvidia.com/gpu-driver-upgrade-state=upgrade-doneラベルを付けてください。最後に、/root/gpudrv/out/upgrade-report.txtに5行を書いてください。NODES=<gpu.present 가 true 인 노드 수>、SKEW=<drv-audit.sh 가 낸 MISSING 줄 수>、LAB_NODE_1_TAG=<drv-tag.sh 가 lab-node-1 에 대해 내는 태그>、MATRIX_ROWS=<support-matrix.csv 의 머리글을 뺀 줄 수>、UPGRADE_STATE=upgrade-doneです(プレースホルダーは、gpu.presentがtrueのノード数、drv-audit.shが出力したMISSING行の数、drv-tag.shがlab-node-1について出力するタグ、support-matrix.csvのヘッダー行を除いた行数です)。数字とタグは、今の状態からコマンドで取り出して埋めてください。

ステートマシンを暗記しろということではなく、アップグレードが止まったとき、どこで止まったのかが1行でわかるという事実が重要です。kubectl get node -l nvidia.com/gpu.present -o jsonpathで、ノードごとにこのラベルを一度に取り出してみると、cordon-requiredで止まったノードと、pod-deletion-requiredで止まったノードの原因が、互いに違うことがすぐに見えます。このステップの数字は、前のステップの記憶ではなく、今のクラスターと今のファイルから出てくる必要があります。ステップ7で表に行を追加して、ドライバーのバージョンも変わったので、値が前のステップとは違います。