DCGM Exporter の指標と、その指標が語らないこと
一言でいうと
GPU観測の材料は、DCGM Exporterが/metricsで出力するプレーンテキストであり、そのテキストで最も重要なのは、値ではなくラベルです。ラベルが、メトリクスをカードとホストとPodにつなぎ、そのつながりがあって初めて、「割り当ては全部出ているのに、誰も使っていない」という文を、数字で証明できます。
なぜ必要なのか
GPUは、組織で最も高価なリソースなのに、最も見えにくいリソースでもあります。cpuとmemoryは、kubeletが自分で数えてくれて、kubectl topですぐに見えますが、GPUは、Kubernetesが使用率を知りません。Kubernetesが知っているのは、「このPodに1枚を割り当てた」ということだけで、その1枚が100%で動いているのか、止まっているのかは、関心の外です。
そのため、ごくありふれた状態が生まれます。ノードのnvidia.com/gpuのallocatableがすべて使い切られて、新しいジョブはずっとPendingなのに、肝心のカードの実際の使用率は1桁という状態です。スケジューラーの観点では、このクラスターはいっぱいのクラスターで、電気代と減価償却の観点では、ほとんど遊んでいるクラスターです。2つの数字が別のシステムにあって、誰も並べて見ないと、この状態は何か月も続きます。
割り当て率はKubernetes APIから、使用率はDCGMから出てきます。このモジュールがやろうとしていることは、その2つを同じ画面に載せることです。
どう動くのか
DCGM Exporterは、ノードごとにDaemonSetとして起動し、NVIDIAのDCGMライブラリでカードから値を読んで、Prometheusの公開形式で出力します。公式ドキュメントに載っている実際の出力は、このような形です。
# HELP DCGM_FI_DEV_GPU_TEMP GPU temperature (in C).
# TYPE DCGM_FI_DEV_GPU_TEMP gauge
DCGM_FI_DEV_GPU_TEMP{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 61
DCGM_FI_DEV_FB_FREE{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 35690
DCGM_FI_DEV_FB_USED{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 4845
DCGM_FI_DEV_XID_ERRORS{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 0
DCGM_FI_PROF_GR_ENGINE_ACTIVE{gpu="0",UUID="GPU-34319582-d595-d1c7-d1d2-179bcfa61660",device="nvidia0",Hostname="ub20-a100-k8s"} 0.995630
ここで押さえることが3つあります。
第一に、名前はDCGMのフィールド名そのままです。DCGM_FI_DEV_で始まるものはデバイスレベルの値(温度・メモリ・XIDエラー・クロック)で、DCGM_FI_PROF_で始まるものはプロファイリングの値(グラフィックスエンジンの活性比率、テンソルパイプの活性比率、DRAMの活性比率)です。使用率を見るためによく使われるDCGM_FI_DEV_GPU_UTILは、「サンプリングの瞬間にカーネルが動いていたか」に近い粗い数字で、DCGM_FI_PROF_GR_ENGINE_ACTIVEは、その区間の間にエンジンが実際にどれだけ活性だったかを、0から1の比率で与えます。1つの枠を長く遊ばせるジョブは、前者の数字では忙しく見え、後者の数字では暇に見えます。容量計画をするとき、どちらの数字を使うかで結論が変わります。
第二に、メモリは2つのメトリクスで出てきます。DCGM_FI_DEV_FB_USEDとDCGM_FI_DEV_FB_FREEです(単位はMiB)。全体のメモリのメトリクスを別に探さず、2つを足して総量を得るのが普通です。この2つが重要な理由は、GPU障害の大きな部分が、「使用率は0なのにメモリは確保されている」という形で現れるからです。死んだプロセスがコンテキストを手放していないのであり、そのカードは、生きたまま、誰にも戻ってきません。
第三に、ラベルがこのテキストの半分です。gpuはノード内のインデックス、UUIDはカードのグローバルな識別子、deviceはデバイスノード名、Hostnameはノードです。ここにexporterのKubernetesマッピングを有効にすると(環境変数DCGM_EXPORTER_KUBERNETES、フラグ-k)、そのカードを確保しているPodの情報がラベルとしてさらに付きます。このマッピングがあって初めて、「誰がこのカードを確保しているのか」という問いに答えられ、なければ、メトリクスはカードの話しかできず、人の話ができません。
MIGを有効にすると、同じメトリクスが、カードレベルとGPUインスタンスレベルの両方で出力され、インスタンスの行には、GPU_I_PROFILE・GPU_I_IDラベルがさらに付きます。そのため、MIGクラスターでメトリクスを何も考えずにsum()すると、同じ値を2回足してしまいます。ダッシュボードの合計が、物理的な枚数より大きいクラスターは、ほとんどこれが理由です。
収集経路とアラート
メトリクスをPrometheusが取りに行くには、対象の定義が必要です。Prometheus Operatorを使っている場所なら、その定義がServiceMonitorカスタムリソースです。どのServiceのどのポートを、どの間隔で、どのパスから取得するかを書いておくと、オペレーターがPrometheusの設定を代わりに作ってくれます。重要なのは、これがスキーマを持つAPIオブジェクトだという点です。intervalを30sではなく30と書くと、APIサーバーが型の不一致で拒否します。設定ファイルなら静かに無視されたはずの誤字が、適用時点で引っかかります。
アラートにするものは、値が大きいからといって設定せず、人のすべきことが決まるものを選びます。
| アラート | 根拠メトリクス | なぜ人が動く必要があるか |
|---|---|---|
| XIDエラーの発生 | DCGM_FI_DEV_XID_ERRORS |
ハードウェアやドライバーの事象です。カードを外すか、ノードを空にする必要があります |
| 温度のしきい値超過 | DCGM_FI_DEV_GPU_TEMP |
冷却の問題であり、放置するとクロックが下がって、性能が静かに落ちます |
| 持ち主のいないメモリ占有 | DCGM_FI_DEV_FB_USEDとPodラベルの不在 |
死んだプロセスがカードを確保しています。ノードに手を入れる必要があります |
| 割り当てられているのに遊んでいる | 割り当て(API)とDCGM_FI_DEV_GPU_UTILのギャップ |
コストの問題です。ジョブの持ち主に返してもらう必要があります |
最後の行がこのモジュールの核心で、単一のメトリクスでは作れないアラートです。片方はKubernetes APIから、もう片方はexporterから来る必要があります。
現場での姿
第一に、使用率ダッシュボードが初めて表示される日に、組織が驚きます。平均使用率15%、それなのに待ち行列は長い。原因はたいてい、技術ではなく習慣です。人々がノートブックを起動したまま帰宅し、ジョブ1つがカード1枚を丸ごと確保して、データの前処理をCPUでやっています。数字を見せる前には、誰も自分がそうしているとは知りません。
第二に、タイムスライシングを有効にすると、メトリクスが紛らわしくなります。タイムスライシングは、リソース名の個数を増やすだけで、カードを分けません。そのため、スロットを5つ使うノードのメトリクスは、依然としてカード1枚の行で出てきます。Pod別に使用率を分けて見たくても、そのような値はありません。この事実を知らずに「スロットあたりの使用率」を作ろうとして、時間を無駄にすることがよくあります。タイムスライシングのノードでは、カード単位のメトリクスとPod単位の割り当てを別々に見て、2つを掛け合わせて解釈しないようにする必要があります。
第三に、ラベルのカーディナリティが静かに問題になります。メトリクスごとにUUIDとPod名が付くので、短命で死ぬPodが多いクラスターでは、時系列が新しく生まれ続けます。Pod名が入った時系列を長期保管の対象にそのまま入れると、ストレージのほうが先に崩れます。長期保管はカード単位に絞っておき、Pod単位は短く持つ、という分離が必要です。
第四に、この環境の正直な限界です。ラボのPodには、GPUもDCGMもPrometheusもありません。そのため、次のラボでは、公開形式のテキストを自分で作って、それを材料にします。これは模擬に見えるかもしれませんが、実際の運用で行う仕事の大きな部分も、まさにこれです。他の人が渡してくれた/metricsダンプを読み、パースし、ルールが参照する名前が実際に出ているかを照合する仕事です。PromQL自体は、Prometheusなしでは実行できないので実行せず、ルールファイルの構造と、参照するメトリクス名の存在までを判定します。
参考ドキュメント
- DCGM Exporterの実行と出力: https://docs.nvidia.com/datacenter/cloud-native/gpu-telemetry/latest/dcgm-exporter.html
- GPUテレメトリの概要: https://docs.nvidia.com/datacenter/cloud-native/gpu-telemetry/latest/index.html
- kube-prometheusとGPUメトリクス: https://docs.nvidia.com/datacenter/cloud-native/gpu-telemetry/latest/kube-prometheus.html
- DCGMフィールド識別子の一覧: https://docs.nvidia.com/datacenter/dcgm/latest/dcgm-api/dcgm-api-field-ids.html
- カスタムリソース定義: https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definitions/
次のラボですること
DCGM Exporterの公開形式のテキストを、自分で書きます。5つのメトリクスを4枚のカードについて、ラベルまで揃えて書き、それをパースしてカードごとの表に変えるツールを作ります。Kubernetesマッピングを有効にした版を別に作って、Podラベルがないのにメモリを確保しているカードを見つけ出し、同じクラスターの割り当て量をAPIで数えて、使用率と並べます。アラートのルールファイルを書き、ルールが参照するメトリクス名が実際に公開に出ているかを照合するチェッカーを作ります。最後に、ServiceMonitorのCRDを自分で定義してAPIサーバーに入れ、誤って書いたintervalがどのように拒否されるかを見ます。