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

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

ドライバー版を合わせる — なぜタグの中にカーネル版が入っているのか

TT Labで続きを見る

一言でいうと

コンテナとしてデプロイされるGPUドライバーが実際に行うことは、ホストカーネルにモジュールを読み込むことなので、カーネルが変わればドライバーも再ビルドが必要になり(そのため、事前コンパイル済みイメージのタグにカーネルバージョンが入っています)、ドライバーを載せ替えるには、そのノードのGPUワークロードを先に空にする必要があります(そのため、PodDisruptionBudgetがアップグレードを止めます)。

なぜ問題になるのか

通常のコンテナは、分離されたユーザー空間で動きます。ホストに何がインストールされていても、イメージの中のライブラリを使うので、「コンテナにすればホストと無関係になる」という直感が成り立ちます。

ドライバーはそうではありません。NVIDIAのドキュメントは、ドライバーのアップグレードに特別な配慮が必要な理由を、こう書いています。ドライバーのカーネルモジュールは、ドライバーコンテナが再起動されるたびに、いったん下ろされてから、再び読み込まれる必要があります。そしてその手順は、5つあります。ドライバーを使っているすべてのクライアントを止め、現在のカーネルモジュールを下ろし、新しいドライバーのPodを起動し、新しいモジュールを読み込み、クライアントを再び起動します。

ここから、2つの性質が出てきます。1つ目は、ドライバーはホストカーネルに依存することです。カーネルモジュールは、カーネルのバージョンに合わせてコンパイルされる必要があるので、カーネルが1つ上がれば、そのカーネル用のドライバーが別に必要になります。2つ目は、ドライバーの入れ替えは、そのノードのGPUワークロードに必ず影響することです。モジュールを下ろすには、それを使っているプロセスがない必要があるからです。

どう動くのか: イメージタグが契約です

既定の方式のドライバーコンテナは、ノードでカーネルヘッダーとコンパイラーを取得して、起動時にモジュールをビルドします。インターネットが必要で、時間がかかり、CPUを消費します。そのためNVIDIAは、事前コンパイル済みのドライバーコンテナを別に提供しています。ドキュメントは、その利点を、インターネットアクセスが制限された場所と、リソースに余裕のない場所で、特に価値があると書いています。

事前コンパイル済みイメージのタグは、このような形です。

<driver-branch>-<linux-kernel-version>-<os-tag>

예: 525-5.15.0-69-generic-ubuntu22.04

3つの部品が1つのタグに埋め込まれていることが、この方式の契約です。ドライバーブランチが同じでもカーネルバージョンが違えば別のイメージで、カーネルバージョンが同じでもOSが違えばまた別のイメージです。そのため、運用では、次のことがすべて「イメージの作業」になります。

なければ、どう表面化するでしょうか。そのノードでだけ、ドライバーのPodがイメージを取得できずに落ちます。他のノードは問題ないので、クラスターレベルのアラートは鳴らず、そのノードのGPUだけが静かに外れます。ドキュメントは、サポートされる組み合わせが表で決まっていることと、一覧にないカーネルの派生版を使う場合は、自分でイメージをビルドして自分のレジストリに載せる必要があることを、はっきり書いています。

そのため、成熟したチームは、「自分たちが持っているドライバーイメージの一覧」を台帳として持ち、ノードの現在の組み合わせと定期的に照合します。カーネルアップグレードの計画が決まったら、その照合を先に実行して、欠けている組み合わせを事前にビルドします。この順序を守らないと、ノードを空にしたまま、イメージを待つことになります。

CUDAの互換は一方向だけ

ドライバーとCUDAランタイムの間にも、バージョンの問題があり、向きが片方にだけ開いています。古いCUDAコンテナは、新しいドライバーの上でうまく動きます。逆はだめです。ドライバーは、自分より後に出たCUDAランタイムを知らないからです。

実務でこれが意味することは単純です。新しいフレームワークのイメージを使い始めるときは、クラスターで最も低いドライバーバージョンが、そのイメージを支えられるかをまず見ます。そして、その要件を人の記憶ではなくPodのスペックに書いておきます。gpu-feature-discoveryが付けてくれるnvidia.com/cuda.driver.majorラベルにnodeAffinityの条件をかければ、合わないノードに行って実行中に失敗する代わりに、スケジュール段階で止まります。失敗を前倒しすることが、この条件の価値です。

アップグレードが止まる場所

ドライバーを載せ替えるには、そのノードのGPUワークロードを下ろす必要があり、下ろす操作はeviction APIを通ります。そしてeviction APIの前には、PodDisruptionBudgetがあります。現在生きている数がバジェットを下回ると、APIはリクエストを拒否します。

error when evicting pods/"trainer-..." (will retry after 5s):
Cannot evict pod as it would violate the pod's disruption budget.

kubectl drainは、ノードを先にcordonしてからPodを1つずつevictするので、止まってもノードはすでにスケジュール不可の状態です。ここで気づかないと、そのノードは新しいPodを受け付けないまま、ドライバーも載せ替えられない中途半端な状態で残ります。--timeoutなしで実行したdrainは、永遠に再試行を続けるので、画面を見るだけだと「遅い」と見えます。

GPU Operatorのアップグレードコントローラーは、この手順を自動化しながら、進行状況をノードのラベルnvidia.com/gpu-driver-upgrade-stateに書きます。ドキュメントが定義した状態は、次のとおりです。

状態 意味
upgrade-required ドライバーのPodが最新ではありません
cordon-required ノードをスケジュール不可にする番です
wait-for-jobs-required 指定したジョブが終わるのを待ちます
pod-deletion-required GPUを割り当てられたPodを削除します
drain-required Podの削除だけでは足りず、ノードをドレインします
pod-restart-required ドライバーのPodを再起動して、新しいバージョンを載せます
validation-required 新しいドライバーを検証します
uncordon-required ノードをスケジュール可能な状態に戻します
upgrade-done 完了です

このラベルの価値は、状態の名前そのものではなく、どこで止まったのかが1行でわかることにあります。cordon-requiredで止まったノードと、pod-deletion-requiredで止まったノードは、原因が違います。ノード全体を一度に取り出して見ると、どの段階にどれだけ溜まっているかが、すぐに見えます。

ドキュメントが特に強く警告しているのがdrain.enableです。既定値がオフになっているのには、理由があります。ドレインは、GPUと無関係なワークロードまで、そのノードからすべて追い出します。まずGPU Podの削除の設定を調整し、それでも足りないときだけドレインを有効にして、podSelectorで範囲を絞るようにと書かれています。

現場での姿

第一に、「1台のノードだけGPUが見えない」。カーネルがそのノードだけ上がったか、そのノードだけがあとから入ってきてイメージが違うか。確認は、そのノードのカーネルラベルとドライバーのPodのイメージタグを、並べて見ることで終わります。

第二に、「アップグレードが3時間も1台のノードで止まっている」。ドライバーのログをどれだけ読んでも出てこないのは、原因がドライバーではないからです。PDBがevictionを拒否しているか、コントローラーが待つように指定したジョブが終わっていないか。アップグレード状態のラベルを先に見れば、3秒で終わります。

第三に、事前にインストールされたドライバーです。イメージにドライバーを焼き込んでおくチームでは、オペレーターがドライバーを管理しません。ドキュメントも、ホストに事前インストールされたドライバーのライフサイクルは、オペレーターが管理しないと書いています。楽に見えますが、その代わりバージョン管理がすべて人の担当になるので、どちらがよいかは、チームのイメージパイプラインの成熟度によります。

第四に、この環境の正直な限界です。ラボでは、ドライバーを実際にはインストールしません。GPUもカーネルモジュールもnvidia-smiもありません。そのため、カーネルバージョンとドライバーバージョンはノードのラベルで表現し、アップグレードは、そのラベルを変更することで表します。実際のクラスターでも、スケジューラーとオペレーターが見るのは、結局そのラベルです。一方、ノードの追加、PodDisruptionBudgetの判定、evictionの拒否、drainとuncordonは、本物のコントロールプレーンがそのまま実行します。

参考ドキュメント

次のラボですること

ノードのラベルを事実の置き場として、ノードが必要とするドライバーイメージのタグを計算するツールと、それを社内のイメージ一覧と照合するチェッカーを作ります。ノードを1台増やして、そのノードだけ組み合わせがないことを、チェッカーが先に教えてくれることを確認し、CUDAの要件をnodeAffinityで書いて、合わない要求がスケジュール段階で止まるのを見ます。そして、PodDisruptionBudgetがドレインを実際に拒否する場面を通過したあと、イメージの確保からuncordonまで、アップグレードの手順を順番に最後まで進めます。