TT Lab
ブログ

한국어 English 日本語 中文

はじめる
ブログ

ブログ

NVIDIA GPU Operator読み解き:1つのClusterPolicyがGPUノードの準備を整え、dcgm-exporterがGPUの所有者を見つけるまでの道筋

状態:ドラフト(2026-10-17)、一部実測。 基準バージョン:NVIDIA/gpu-operator v26.3.3(コミット b0a49c0e7b2e)、NVIDIA/k8s-device-plugin v0.19.3(570e776d6813)、NVIDIA/dcgm-exporter 4.5.3-4.8.2(691c92762eb5)。小さなクラスターがまさにこれらのバージョンを稼働させているため、実測は2026-10-17にそのクラスターから 読み取りのみ を行って取得しました(ClusterPolicy・デーモンセット・ノードラベル・ポッド構成の読み取り、APIサーバープロキシを通じた /metrics の読み取り、実行中ポッド内のファイルの読み取り)。2026-10-16により長く測定した記録も併せて引用します。作成ルール:実測データを基本とし、測定できなかったものは「未測定」、確認できなかったものは「未確認」と表示します。

目次

1. 一行要約:GPUノード向けの「設置担当者 + チェックリスト」

KubernetesでGPUを使用するには、ノードごとに複数のレイヤーがかみ合う必要があります。カーネルドライバ、コンテナがGPUデバイスを認識できるようにするコンテナツールキット、kubeletに nvidia.com/gpu リソースを通知するデバイスプラグイン、GPUの仕様をノードラベルとして付与するGPU Feature Discovery(GFD)、指標をエクスポートするdcgm-exporter。1つでも順序が狂うと、ポッドは Init の状態で停止するか、GPUを見つけられなくなります。

GPU Operatorは、これらのレイヤーを 設置担当者のように順にインストールし、 チェックリストのように前の段階が完了したことを確認してから次の段階に進みます。ユーザーが使用するのは ClusterPolicy という1つのCRのみです。

コンポーネント 役割 動作場所
gpu-operator(オペレータ) ClusterPolicyを確認して状態のリストを順次適用 Deployment 1つ
NFD(node-feature-discovery) PCIデバイスを確認し、 feature.node.kubernetes.io/pci-10de.present などのラベルを付与 マスターDeployment + ワーカーデーモンセット(全ノード)
driver カーネルモジュールをインストール(コンテナドライバ)。ホストにドライバがある場合は無効化 GPUノードデーモンセット
container-toolkit ランタイムの設定、CDI spec(management.nvidia.com)の生成 GPUノードデーモンセット
operator-validator ドライバ・ツールキット・CUDA・プラグインが実際に動作するかを確認し、「準備完了ファイル」を残す GPUノードデーモンセット
device plugin nvidia.com/gpu 広告・割り当て、時分割複製、CDI spec(k8s.device-plugin.nvidia.com) GPUノードデーモンセット
GFD GPUの数・複製数・共有方式などをノードラベルとして付与 GPUノードデーモンセット
dcgm-exporter DCGMの値をPrometheus形式で、ポッドラベルを付与して出力 GPUノードデーモンセット

2. 構成図:状態のリストとノード内の準備チェーン

ClusterPolicy 하나가 상태 목록을 차례로 적용한다 ClusterPolicy 클러스터에 하나 Reconcile → step() 반복 안 된 상태가 있으면 5초 뒤 다시 상태 = assets/ 폴더 하나(매니페스트 묶음) → 변환 → 적용 1 pre-requisites 2 operator-metrics 3 driver 4 container-toolkit 5 operator-validation 6 device-plugin 7 mps-control 8 dcgm 9 dcgm-exporter 10 gpu-feature-disc. 11 mig-manager 12 node-status-exp. 13–20 vGPU·샌드박스·kata·cc 계열(샌드박스 워크로드용) 켜짐 꺼짐(작은 클러스터 설정 — 호스트 드라이버 사용 등) GPU 노드 안: 파일 하나씩으로 잇는 준비 사슬 operator-validator 파드의 initContainer 순서 driver 검증 driver-ready toolkit 검증 toolkit-ready cuda 검증 cuda-ready plugin 검증 plugin-ready 파일은 노드의 /run/nvidia/validations 에 남는다 device-plugin·dcgm-exporter 는 toolkit-ready 를 기다린 뒤 뜬다
図:ClusterPolicyが状態リストを順に適用し、ノード内では検証ファイルによって準備の順序が繋がる

図の読み方:

3. コードマップ

3つのリポジトリを合わせて見ていきます。最上位の中から重要な部分のみを抜粋しました。

パス 内容 最初に読むべきファイル
gpu-operator api/ ClusterPolicy・NVIDIADriver CRDのGo型定義 api/nvidia/v1/clusterpolicy_types.go
gpu-operator controllers/ リコンサイルループ、状態管理、オブジェクトごとの適用・変換 clusterpolicy_controller.go, state_manager.go
gpu-operator assets/ 状態ごとのマニフェストバンドル(state-device-plugin/0500_daemonset.yaml など) state-operator-validation/0500_daemonset.yaml
gpu-operator cmd/nvidia-validator/ ノード内バリデータ(ドライバ・ツールキット・CUDA・プラグイン) main.go
k8s-device-plugin internal/plugin/ kubelet device plugin gRPCサーバー server.go
k8s-device-plugin internal/rm/ デバイスリストの作成、時分割複製(::N) device_map.go, devices.go
k8s-device-plugin internal/cdi/ CDI specの生成・保存 cdi.go
dcgm-exporter internal/pkg/transformation/ 指標へのpod・namespaceラベルの付与 kubernetes.go
dcgm-exporter internal/pkg/server/ /metrics 処理、変換の適用 server.go

controllers/object_controls.go は5,000行を超えています。最初から読まず、下記のパスで名前が登場した際にその関数のみを探して読む方が効率的です。

4. コアパスを辿る

4.1 リコンサイルループ:状態を1つずつ「一歩ずつ進める」

  1. オペレータがClusterPolicyを受信すると、 controllers/clusterpolicy_controller.go:93 Reconcile が稼働します。ClusterPolicyはクラスター内に1つのみ認められ(2つ目以降は Ignored)、初回であれば init を呼び出します。
  2. controllers/state_manager.go:854 init はGPUノードを探してラベルを付与し(state_manager.go:521 labelGPUNodes)、状態リストを登録します。GPUノードであるかはNFDラベル feature.node.kubernetes.io/pci-10de.present=true(10deはNVIDIAのPCIベンダーID)で判定し、そのようなノードに nvidia.com/gpu.present=true と nvidia.com/gpu.deploy.<구성요소>=true のラベルを付与します。各デーモンセットのnodeSelectorがまさにこのラベルです。
  3. 登録は state_manager.go:175 addState がフォルダパスごとに controllers/resource_manager.go:102 addResourcesControls を呼び出し、YAMLの kind を確認して「このオブジェクトを適用する関数」のリストを作成します。1つの状態 = 1つの関数リストです。
  4. Reconcile は state_manager.go:1051 step を最後の状態まで繰り返します。 step はその状態の関数をすべて実行し、1つでも準備ができていなければその状態をNotReadyに示して 次の状態に進みます。最後まで実行した後に準備ができていない状態があれば、5秒後に再度実行されます。
for _, fs := range n.controls[n.idx] {
    stat, err := fs(*n)
    if err != nil {
        return stat, err
    }
    // successfully deployed resource, now check if its ready
    if stat != gpuv1.Ready {
        result = stat
    }
}
// move to next state
n.idx++

出所:NVIDIA/gpu-operator v26.3.3 controllers/state_manager.go(step の一部)、Apache-2.0。

ここで設計上の選択が見えてきます。 前の状態の準備ができるのを待って処理をブロックすることはありません。 デーモンセットはとりあえずすべて作成し、実際の順序はノード内の準備ファイルによって合わせます(4.2)。オペレータがノードごとに待機するように作ってしまうと、1つのノードの処理が遅延した際にクラスター全体の適用が停止してしまうためです。

  1. デーモンセットを適用する関数は controllers/object_controls.go:4621 DaemonSet です。状態が無効化されている場合はオブジェクトを削除し、 Disabled を返します。有効な場合は object_controls.go:728 preProcessDaemonSet がデーモンセット名に応じた 変換関数を選択します(nvidia-device-plugin-daemonset → TransformDevicePlugin, nvidia-dcgm-exporter → TransformDCGMExporter …)。ClusterPolicyのイメージ・環境変数・設定がYAMLに埋め込まれるのはこれら変換関数の中です。準備ができているかどうかは object_controls.go:3989 isDaemonSetReady が確認します。
  2. 新しいGPUノードが参加すると、 clusterpolicy_controller.go:246 addWatchNewGPUNode のノード監視がリコンサイルを再び呼び起こします。

4.2 ノード内の準備チェーン:バリデータと「-ready」ファイル

バリデータは cmd/nvidia-validator/main.go の1つのファイルにあります。状態ファイルを配置する場所は /run/nvidia/validations(146行目 defaultStatusPath)であり、ファイル名が161〜189行目に定数としてまとめられています(driver-ready, toolkit-ready, cuda-ready, plugin-ready …).

ファイルで繋ぐ方式は単純ですが大きなメリットがあります。ノードが再起動されると /run が空になるため、検証が自然と最初から再び実行され、管理者がノード上で ls /run/nvidia/validations を1回実行するだけでどこで停止したかを確認できます。

4.3 デバイスプラグイン:時分割複製とCDI spec

デバイスプラグインは、kubeletのdevice plugin APIを実装したgRPCサーバーです。フローは3つの関数に要約されます。

  1. internal/plugin/server.go:229 Register — kubeletソケットに「nvidia.com/gpu を自分が担当する」と登録。
  2. server.go:267 ListAndWatch — デバイスリスト(健康状態を含む)をkubeletにストリーミング。
  3. server.go:306 Allocate — ポッドにデバイスを割り当てる際、コンテナに渡すべき環境変数・デバイス・CDI名を返却。

時分割(time-slicing) は2番目のステップで発生します。設定に replicas: 5 がある場合、 internal/rm/device_map.go:286 updateDeviceMapWithReplicas は1枚のGPUを GPU-<uuid>::0 … ::4 5つの「デバイス」に拡張して広告します。 ::N を付け外しする作業は internal/rm/devices.go:243 NewAnnotatedID 付近の小さな型が担当します。kubeletはこれが同じGPUであることに気づかず、5つのポッドに1つずつ割り当てます。メモリの分離はありません。この ::N が第5節の問題に繋がります。

CDI spec は、コンテナランタイムに対して「このデバイスを渡すには、このデバイスファイルとこのライブラリを含めよ」と記述したファイルです。 internal/cdi/cdi.go:187 CreateSpecFile が /var/run/cdi(41行目 cdiRoot)にspecを保存し、この関数はプラグインの起動時に一度だけ呼び出されます(cmd/nvidia-device-plugin/plugin-manager.go:84)。specにはライブラリファイル名が ドライババージョンまで付いた状態で 含まれます。そのため、ホストのドライバのみを更新してプラグインを再起動しない場合、specが指すライブラリと実際にロードされているカーネルモジュールのバージョンが食い違う可能性があります。プラグインはkubeletの再起動・設定変更・シグナルを受信すると再起動するため(main.go start のrestartループ)、ドライバの更新後はノードの再起動またはプラグインポッドの再起動が実質的に必要となります。

4.4 dcgm-exporter:「このGPUは誰のものか」

dcgm-exporter 는 스크랩마다 「이 GPU 는 누구 것」 을 묻는다 DCGM 값 GPU UUID 별 한 줄 PodMapper.Process() kubelet pod-resources List() List() 응답 — 시분할로 한 GPU 를 세 파드가 나눔 파드 P GPU-uuid::0 파드 Q GPU-uuid::1 파드 R GPU-uuid::2 순서는 kubelet 의 Go 맵 순회 → 부를 때마다 다르다 toDeviceToPod ::N 을 떼고 map[UUID] = 파드 → 마지막 파드가 이긴다 실측: 10초 간격 스크랩 6번의 pod 라벨 1회 P 2회 Q 3회 P 4회 R 5회 Q 6회 R 같은 GPU 의 시계열 하나가 세 파드 사이를 오간다 --kubernetes-virtual-gpus 를 켜면 toDeviceToSharingPods → 파드마다 한 줄씩 복사(일부 필드는 프로세스별 값)
図:dcgm-exporterがスクラップごとにkubeletにポッドを問い合わせ、時分割GPUでは最後のポッドが勝つ

dcgm-exporter は DCGM から GPU ごとの値気を取り出して /metrics に出力します。 DCGM_EXPORTER_KUBERNETES=true の場合、各行に pod·namespace·container ラベルを付与しますが、その情報を Kubernetes API ではなく kubelet の pod-resources gRPC(デフォルトは /var/lib/kubelet/pod-resources/kubelet.sock)から取得します。 「このコンテナにどのデバイス ID が割り当てられたか」を知っているのは kubelet だからです。

  1. /metrics リクエストが来ると、 internal/pkg/server/server.go:249 Metrics → render が 変換リストを巡回します(server.go:309 transformation.Process). スクラップごと に巡回します)。
  2. internal/pkg/transformation/kubernetes.go:296 getMappings がソケットに接続し、 kubernetes.go:533 listPods に向かって List() を呼び出します。
  3. デフォルト(--kubernetes-virtual-gpus 無効)の状態では、 kubernetes.go:712 toDeviceToPod が 「デバイス ID → 単一のポッド」のマップを作成します。時分割デバイス ID の GPU-<uuid>::N は、 :: の前部分を切り捨てて GPU UUID をキーとして使用します。
} else if strings.Contains(deviceID, "::") {
    gpuInstanceID := strings.Split(deviceID, "::")[0]
    // (디버그 로그 생략)
    deviceToPodMap[gpuInstanceID] = podInfo
}

出典: NVIDIA/dcgm-exporter 4.5.3-4.8.2 internal/pkg/transformation/kubernetes.go 887〜899行目の要約、Apache-2.0。

同じ GPU を 3 つのポッドで共有している場合、3 つのポッドが順に 同じキーを上書きします(kubernetes.go:898)。 結果として、レスポンスの最後に出現したポッドが勝つことになります。また、そのレスポンスの順序は kubelet がポッドのリストを Go のマップから取り出して作成するため、呼び出すたびに変動します (kubelet v1.34 pkg/kubelet/pod/pod_manager.go におけるマップの走査、ソースに基づく)。 コード 1 行の「後勝ち(last-wins)」とレスポンス順序のランダム性が組み合わさることで、 スクラップごとにラベルが変化します。

--kubernetes-virtual-gpus(環境変数 KUBERNETES_VIRTUAL_GPUS)を有効にすると、 kubernetes.go:662 toDeviceToSharingPods が 1 つの GPU にポッドの リストを紐付け、デバイス指標をポッドごとに 1 行ずつ複製します(一部のフィールドはプロセスごとの値に分割)。 GPU Operator のデフォルトのデプロイではこの値は有効化されません。

5. 筆者の貢献と関連する箇所

6. 実測: 小規模クラスターでの観測結果

2026-10-17、すべて読み取りコマンドです(kubectl get、実行中のポッド内 ls·grep、API サーバープロキシ kubectl get --raw …/proxy/metrics)。 ノードは役割ごとに、ポッドは P・Q・R でマスキングしています。

ClusterPolicy と DaemonSet。

項目 読み取った値
ClusterPolicy のステータス state: ready、条件 Reconciled(2026-10-14 から)
driver.enabled false — ホストにインストールされたドライバーを使用(ステータス 3 でドライバー無効)
dcgm.enabled / dcgmExporter.enabled false / true — 別途 DCGM を使用せず exporter 内のエンジンを使用
cdi.enabled true
migManager.enabled, sandboxWorkloads.enabled 両方とも false
devicePlugin.config デフォルト no-sharing、設定バンドル名 time-slicing-config
DaemonSet 望ましい数 / 準備完了数 nodeSelector
NFD worker 8 / 8 なし(すべてのノード)
container-toolkit 5 / 5 nvidia.com/gpu.deploy.container-toolkit=true
operator-validator 5 / 5 …deploy.operator-validator=true
device-plugin 5 / 5 …deploy.device-plugin=true
mps-control-daemon 0 / 0 上記 + nvidia.com/mps.capable=true(該当ノードなし)
dcgm-exporter 5 / 5 …deploy.dcgm-exporter=true
gpu-feature-discovery 5 / 5 …deploy.gpu-feature-discovery=true

8 台のノードのうち GPU ノードは 5 台であり、5 台すべてに nvidia.com/gpu.present=true と gpu.deploy.* の 8 つのラベルが付与されていました(4.1 のステップ 2)。

検証の連鎖。 operator-validator ポッドの initContainer は driver-validation → toolkit-validation → cuda-validation → plugin-validation の順序であり、device plugin ポッドは toolkit-validation → config-manager-init、dcgm-exporter ポッドは toolkit-validation が 1 つでした。図の下部の連鎖そのものです。

時分割。 1 台の GPU ノード(GPU ノード B)のみが nvidia.com/device-plugin.config=shared-5 ラベルにより異なる設定を使用していました。その設定は replicas: 5, failRequestsGreaterThanOne: true(1 つのポッドが複数のスロットを占有しないように制限)、 renameByDefault: false であり、結果は次のようになります。

GPU ノード gpu.sharing-strategy gpu.replicas allocatable nvidia.com/gpu
B time-slicing 5 5
残りの 4 台 none 1 1

1 枚の GPU(24GB 相当)が kubelet には 5 枚として認識されます。ラベル gpu.replicas·gpu.sharing-strategy は GFD によって付与されたものです。

CDI spec。 device plugin ポッド内で /var/run/cdi を確認したところ、2 つのファイルが存在していました。プラグインによって作成された k8s.device-plugin.nvidia.com-gpu.json と ツールキットによって作成された management.nvidia.com-gpu.yaml です。プラグイン spec の更新日時は 5 台のノードすべてにおいて そのプラグインポッドが起動した時刻であり(4.3 の 「起動時に一度」)、spec 内の libnvidia-ml.so.<판> のバージョンと /proc/driver/nvidia/version のカーネルモジュールバージョンは本日は 5 台のノードすべてで同一でした。 ノードごとに spec の日付が 9 月初旬から 10 月中旬まで分散していることから、ドライバーを更新したノードではその後にプラグインも再起動されたことがわかります。

dcgm-exporter のポッドラベル。 GPU ノード B の exporter の /metrics を 10 秒間隔で 6 回読み取りました。1 回あたり 15 個の指標(設定ファイル dcp-metrics-included.csv)が GPU ごとに 1 行ずつ出力され、そのノードで GPU を要求して実行中だったポッドは 3 つでした。

スクラップ 1 2 3 4 5 6
DCGM_FI_DEV_GPU_UTIL の pod P Q P R Q R

5 回の遷移の間、5 回すべて変化しました。2026-10-16 に同じノードを 2 秒間隔で 30 回読み取った際は、3 つのポッドがそれぞれ 19・8・3 回(2 回目の実行時は 19・10・1、連続する 2 回の間での変化は 12 回)であり、Prometheus(15 秒間隔)に蓄積された 1 時間分のデータでは 1 つの GPU が 3 つの時系列に分割されて 129・76・35 サンプルとなり、24 時間の間に異なる exported_pod の値が 7 種類存在していました。ダッシュボードでポッドごとにグループ化すると線がまばらに途切れる原因はこれです。

7. どこから読み始めるべきか

  1. controllers/clusterpolicy_controller.go:93 Reconcile — 150 行以内に全体像のフローが記載されています。
  2. controllers/state_manager.go:854 init と 1051 行目の step — ステータス一覧と詳細なステップ。
  3. assets/state-device-plugin/ フォルダー — 「個別のステータス」がどのような YAML バンドルであるかを視覚的に確認。
  4. object_controls.go:1520 TransformDevicePlugin — ClusterPolicy の値が YAML に組み込まれる実際の例。
  5. internal/plugin/server.go の Register·ListAndWatch·Allocate — device plugin API の 3 つの関数。
  6. internal/pkg/transformation/kubernetes.go:296 から Process まで — 指標にポッドが結び付くまでのプロセス。

8. 初めてのコントリビュートに最適な箇所

9. 制限事項

ティーティの一言。 1台の卓球台を5人で交代して使っているのですが、スコアボードの所有者欄には「最後尾に並んだ人」の名前が書かれるんです。並ぶ順番が毎回変わるから、所有者も毎回変わります。名前順に並べるだけでも静かになるはずなのに!

参考文献

閲覧日はすべて 2026-10-17 です。

ログインするといいねできます

コメント

まだコメントはありません。

ログインするとコメントできます