GPU 指標 — 公開形式を書き、解析し、アラートにつなぐ
目標
DCGM Exporterの公開形式を自分で書き、パースしてカードごとの表に変え、割り当て率と使用率を並べて、アラートのルールと収集対象のオブジェクトまで作ります。
なぜ重要なのか
GPUは、組織で最も高価なリソースなのに、Kubernetesはその使用率を知りません。知っているのは、「このPodに1枚を割り当てた」ということだけです。そのため、スケジューラーの目にはいっぱいのクラスターが、電気代の観点ではほとんど遊んでいる状態が、何か月も続きます。このギャップを数字にするには、Kubernetes APIの割り当て量とDCGMの使用率を、同じ画面に載せる必要があり、そのためには、まずメトリクスの実際の形を知る必要があります。メトリクスで重要なのは、値ではなくラベルです。ラベルがメトリクスをカードとノードとPodにつなぎ、そのつながりが途切れた場所で、「持ち主がいないままメモリを確保したカード」のような障害が表面化します。
ステップ
/root/gpumet/metrics、/root/gpumet/bin、/root/gpumet/out、/root/gpumet/rules、/root/gpumet/k8sを作成して、/root/gpumet/metrics/dcgm.promに、A100が4枚搭載されたノードの/metrics出力を書いてください。メトリクスは5つです。DCGM_FI_DEV_GPU_UTIL、DCGM_FI_DEV_FB_USED、DCGM_FI_DEV_FB_FREE、DCGM_FI_DEV_GPU_TEMP、DCGM_FI_DEV_XID_ERRORSです。メトリクスごとに、# HELPの1行と# TYPE <이름> gaugeの1行を先頭に置き(プレースホルダーはメトリクス名です)、カード4枚分のサンプルを続けて書きます。サンプルのラベルは4つです。gpu(0から3まで)、UUID(GPU-a1b2c3d4-0000-0000-0000-00000000000<번호>)、device(nvidia<번호>)、Hostname(lab-node-0)です(プレースホルダーはどちらもカード番号です)。値は、gpu 0は使用率97・FB_USED 38000・FB_FREE 2760・温度71・XID 0、gpu 1は2・9000・31760・41・2、gpu 2は0・0・40760・33・0、gpu 3は1・21000・19760・40・0です。/root/gpumet/bin/parse-metrics.sh <노출파일>を作成してください(プレースホルダーは公開ファイルです)。カード1枚ごとに1行ずつ、<gpu> <util> <fb_used> <fb_total> <mem_pct>の5つの欄を空白で区切って出力します。fb_totalはFB_USEDとFB_FREEの合計で、mem_pctはfb_used * 100 / fb_totalの切り捨ての整数です。行はgpu番号の昇順である必要があり、引数で受け取ったファイルだけを読む必要があります(パスを中に埋め込まないでください)。作成したら、ステップ1のファイルにかけて、出力を/root/gpumet/out/per-gpu.txtに保存してください。/root/gpumet/metrics/dcgm-k8s.promに、Kubernetesマッピングを有効にした版を書いてください。ステップ1と同じ5つのメトリクス・4枚のカードに、gpu 0とgpu 3にだけラベルを3つ追加します(namespace="gpu-metrics"、gpu 0はpod="train-a"とcontainer="trainer"、gpu 3はpod="infer-b"とcontainer="server")。gpu 1とgpu 2には、Podラベルがありません。そして/root/gpumet/bin/find-orphan.sh <노출파일>を作成してください(プレースホルダーは公開ファイルです)。PodラベルがないのにFB_USEDが1024を超えるカードのgpu番号を、昇順に1行に1つずつ出力します。ステップ1ではなく、この新しいファイルにかけて、出力を/root/gpumet/out/orphan.txtに保存してください。lab-node-0のstatus.capacityとstatus.allocatableの両方に、nvidia.com/gpuを"4"として入れて、ネームスペースgpu-metricsを作成したあと、/root/gpumet/k8s/workloads.yamlに、Pod3つ(train-a、infer-b、idle-c)を書いてください。すべてlab-node-0に固定し、それぞれnvidia.com/gpu: 1を要求します。適用して3つとも起動したら、/root/gpumet/out/gap.txtに5行を書いてください。PHYSICAL=(ノードがアドバタイズした枚数)、ALLOCATED=(Podたちが要求して出ていった枚数)、ALLOC_PCT=(2つの比率、切り捨ての整数)、MEAN_UTIL=(ステップ3の公開ファイルの4枚のカードの平均使用率、切り捨ての整数)、IDLE_BUT_ALLOCATED=(Podラベルが付いているのに使用率が10未満のカードの数)です。/root/gpumet/rules/gpu-alerts.yamlに、Prometheusのルールファイルを書いてください。最上位にgroupsがあり、グループ1つのnameはgpuで、その中にルールがちょうど3つあります。GpuXidError(DCGM_FI_DEV_XID_ERRORSを使う式、for: 0m、severity: critical)、GpuTempHigh(DCGM_FI_DEV_GPU_TEMPを使う式、for: 10m、severity: warning)、GpuAllocatedButIdle(DCGM_FI_DEV_GPU_UTILを使う式、for: 2h、severity: info)です。ルールごとに、alert、expr、for、labels.severity、annotations.summaryの5つをすべて備えてください。summaryは、「何をすべきか」が含まれた1文で書きます。/root/gpumet/bin/check-rules.sh <규칙파일> <노출파일>を作成してください(プレースホルダーはルールファイルと公開ファイルです)。ルールファイルのすべてのexprから、DCGM_FI_で始まるメトリクス名を取り出して、その名前のサンプルが公開ファイルに1つもなければ、MISSING=<이름>を1行ずつ出力して1で終わります(プレースホルダーはメトリクス名です)。すべてあれば、OK=<노출에 있는 지표 수>を出力して0で終わります(プレースホルダーは公開にあるメトリクスの数です)。作成したら、ステップ5のルールとステップ1の公開ファイルにかけて、出力を/root/gpumet/out/rulecheck.txtに保存してください。/root/gpumet/k8s/servicemonitor-crd.yamlに、servicemonitors.monitoring.coreos.comというCRDを書いて適用してください。グループはmonitoring.coreos.com、バージョンはv1、ネームスペーススコープ、種類はServiceMonitorです。スキーマは、spec.selector.matchLabels(文字列のマップ)、spec.endpoints(配列、minItems: 1、項目ごとにportが必須の文字列、pathは文字列、intervalは^[0-9]+(ms|s|m|h)$のパターンを持つ文字列)で、specには、selectorとendpointsがどちらも必須です。次に、/root/gpumet/k8s/servicemonitor.yamlに、gpu-metricsネームスペースのnvidia-dcgm-exporterを書いて適用してください(セレクターはapp: nvidia-dcgm-exporter、エンドポイント1つにport: gpu-metrics、path: /metrics、interval: 15s)。最後に、/root/gpumet/k8s/servicemonitor-bad.yamlに、名前dcgm-bad-intervalで同じものを書きますが、intervalを引用符なしの30で書いて適用し、拒否の出力を、標準エラー出力も含めて/root/gpumet/out/07-reject.txtに保存してください。/root/gpumet/bin/gpu-report.sh <노출파일> <규칙파일>を作成してください(プレースホルダーは公開ファイルとルールファイルです)。6行を順番に出力します。GPUS=(公開に出てくるカード数)、MEAN_UTIL=(平均使用率、切り捨て)、MAX_TEMP=(最高温度)、ORPHAN_GPUS=(Podラベルなしで、FB_USEDが1024を超えるカード数)、XID_GPUS=(XIDエラーが0より大きいカード数)、ALERT_RULES=(ルールファイルのルールの総数)です。数字はすべて、引数で受け取った2つのファイルから計算する必要があります。ステップ3の公開ファイルとステップ5のルールファイルにかけて、出力を/root/gpumet/out/report.txtに保存してください。
参考
- このPodには、GPUもDCGMもPrometheusもありません。そのため、公開テキストをステップ1で自分で作り、そのあとのステップがそれを材料として使います。メトリクス名とラベル名は、公式ドキュメントの実際の出力のままです。
- PromQLは実行しません。アラートルールは、構造と、参照するメトリクス名の存在までを判定します。
- 公開形式の1行の形:
이름{라벨="값",…} 숫자(プレースホルダーは名前とラベルと値と数値です)。コメントは# HELPと# TYPEの2種類だけです。 - メモリの総量のメトリクスはありません。FB_USEDとFB_FREEを足して得ます。
- よくある間違い: 比率を四捨五入すると、採点ツールが検出します。このラボのすべての比率は、切り捨ての整数です。
- よくある間違い: スクリプトの中にステップ1のファイルの数字やパスを埋め込むと、採点ツールが別のファイルをかけたときに失敗します。
DCGM Exporterの公開形式を自分で書く
/root/gpumet/metrics、/root/gpumet/bin、/root/gpumet/out、/root/gpumet/rules、/root/gpumet/k8sを作成して、/root/gpumet/metrics/dcgm.promに、A100が4枚搭載されたノードの/metrics出力を書いてください。メトリクスは5つです。DCGM_FI_DEV_GPU_UTIL、DCGM_FI_DEV_FB_USED、DCGM_FI_DEV_FB_FREE、DCGM_FI_DEV_GPU_TEMP、DCGM_FI_DEV_XID_ERRORSです。メトリクスごとに、# HELPの1行と# TYPE <이름> gaugeの1行を先頭に置き(プレースホルダーはメトリクス名です)、カード4枚分のサンプルを続けて書きます。サンプルのラベルは4つです。gpu(0から3まで)、UUID(GPU-a1b2c3d4-0000-0000-0000-00000000000<번호>)、device(nvidia<번호>)、Hostname(lab-node-0)です(プレースホルダーはどちらもカード番号です)。値は、gpu 0は使用率97・FB_USED 38000・FB_FREE 2760・温度71・XID 0、gpu 1は2・9000・31760・41・2、gpu 2は0・0・40760・33・0、gpu 3は1・21000・19760・40・0です。
Prometheusの公開形式は、1行が이름{라벨="값",…} 숫자です(プレースホルダーは名前とラベルと値と数値です)。コメント2行(# HELP・# TYPE)は、メトリクスごとに1回だけ出てきて、そのあとに、ラベルだけが違うサンプルが何行も続きます。メモリの総量のメトリクスが別にないことに注意してください。FB_USEDとFB_FREEを足して初めて総量が出て、この表では4枚とも合計が同じです。手で20行を打つ代わりに、シェルのループで作ってもかまいません。
公開テキストをカードごとの表に変える
/root/gpumet/bin/parse-metrics.sh <노출파일>を作成してください(プレースホルダーは公開ファイルです)。カード1枚ごとに1行ずつ、<gpu> <util> <fb_used> <fb_total> <mem_pct>の5つの欄を空白で区切って出力します。fb_totalはFB_USEDとFB_FREEの合計で、mem_pctはfb_used * 100 / fb_totalの切り捨ての整数です。行はgpu番号の昇順である必要があり、引数で受け取ったファイルだけを読む必要があります(パスを中に埋め込まないでください)。作成したら、ステップ1のファイルにかけて、出力を/root/gpumet/out/per-gpu.txtに保存してください。
ラベル文字列からgpu="…"を取り出してカードを分け、メトリクス名で値を集めます。正規表現1つで、이름{라벨} 값を3つの部分に分けられます(プレースホルダーは名前とラベルと値です)。切り捨ての整数は、Pythonの//ですぐに出ます。四捨五入すると、採点ツールが検出します。採点ツールは、このスクリプトを別の値が入ったファイルにもかけます。そのため、ステップ1のファイルの数字を答えとして埋め込んではいけません。
Podラベルを付けて、持ち主のいない占有を見つける
/root/gpumet/metrics/dcgm-k8s.promに、Kubernetesマッピングを有効にした版を書いてください。ステップ1と同じ5つのメトリクス・4枚のカードに、gpu 0とgpu 3にだけラベルを3つ追加します(namespace="gpu-metrics"、gpu 0はpod="train-a"とcontainer="trainer"、gpu 3はpod="infer-b"とcontainer="server")。gpu 1とgpu 2には、Podラベルがありません。そして/root/gpumet/bin/find-orphan.sh <노출파일>を作成してください(プレースホルダーは公開ファイルです)。PodラベルがないのにFB_USEDが1024を超えるカードのgpu番号を、昇順に1行に1つずつ出力します。ステップ1ではなく、この新しいファイルにかけて、出力を/root/gpumet/out/orphan.txtに保存してください。
死んだプロセスがコンテキストを手放さないと、メモリは確保されているのに、そのカードを持っているPodはありません。Kubernetesはそのカードが空いていると見て新しいPodを送りますが、そのPodはメモリ不足で失敗します。判定に使うシグナルは2つです。podラベルの有無とFB_USEDの値です。ラベルが付いた行と付いていない行が、同じカードの中で混ざらないように注意してください。このラボでは、カード単位で一貫しています。採点ツールは、別の値が入ったファイルにもかけます。
割り当て率と使用率を同じ画面に載せる
lab-node-0のstatus.capacityとstatus.allocatableの両方に、nvidia.com/gpuを"4"として入れて、ネームスペースgpu-metricsを作成したあと、/root/gpumet/k8s/workloads.yamlに、Pod3つ(train-a、infer-b、idle-c)を書いてください。すべてlab-node-0に固定し、それぞれnvidia.com/gpu: 1を要求します。適用して3つとも起動したら、/root/gpumet/out/gap.txtに5行を書いてください。PHYSICAL=(ノードがアドバタイズした枚数)、ALLOCATED=(Podたちが要求して出ていった枚数)、ALLOC_PCT=(2つの比率、切り捨ての整数)、MEAN_UTIL=(ステップ3の公開ファイルの4枚のカードの平均使用率、切り捨ての整数)、IDLE_BUT_ALLOCATED=(Podラベルが付いているのに使用率が10未満のカードの数)です。
このステップの要点は、2つの数字が別のシステムから来ることです。前の3行はKubernetes APIから、後ろの2行は公開ファイルから出てきます。どちらも単独では、「高価なのに遊んでいる」とは言えません。割り当て量は、kubectl get pods -n <ns> -o jsonのlimitsを足して数えます。平均は、四捨五入せずに、切り捨てで計算してください。
人が動く必要のあるものだけをアラートにする
/root/gpumet/rules/gpu-alerts.yamlに、Prometheusのルールファイルを書いてください。最上位にgroupsがあり、グループ1つのnameはgpuで、その中にルールがちょうど3つあります。GpuXidError(DCGM_FI_DEV_XID_ERRORSを使う式、for: 0m、severity: critical)、GpuTempHigh(DCGM_FI_DEV_GPU_TEMPを使う式、for: 10m、severity: warning)、GpuAllocatedButIdle(DCGM_FI_DEV_GPU_UTILを使う式、for: 2h、severity: info)です。ルールごとに、alert、expr、for、labels.severity、annotations.summaryの5つをすべて備えてください。summaryは、「何をすべきか」が含まれた1文で書きます。
アラートは、値が大きいからではなく、人のすべきことが決まるときに設定します。XIDはハードウェアの事象なのでノードを空にする必要があり、温度は冷却の問題であり、遊んでいる割り当てはコストの問題なので、ジョブの持ち主に連絡する必要があります。3つのルールのforが違う理由も、それです。XIDは1回でもあれば即座に、使用率は、しばらく見守ってから。このファイルは、クラスターには適用しません。次のステップのチェッカーが読む材料です。
ルールが呼ぶメトリクスが実際に出ているかを照合する
/root/gpumet/bin/check-rules.sh <규칙파일> <노출파일>を作成してください(プレースホルダーはルールファイルと公開ファイルです)。ルールファイルのすべてのexprから、DCGM_FI_で始まるメトリクス名を取り出して、その名前のサンプルが公開ファイルに1つもなければ、MISSING=<이름>を1行ずつ出力して1で終わります(プレースホルダーはメトリクス名です)。すべてあれば、OK=<노출에 있는 지표 수>を出力して0で終わります(プレースホルダーは公開にあるメトリクスの数です)。作成したら、ステップ5のルールとステップ1の公開ファイルにかけて、出力を/root/gpumet/out/rulecheck.txtに保存してください。
アラートルールのメトリクス名を1文字間違えると、そのアラートは永遠に鳴りません。Prometheusは、存在しないメトリクスをエラーとは見ず、空の結果として見るからです。そのため、この照合は、デプロイ前に設定しておく価値があります。DCGM_FI_DEV_GPU_UTILIZATIONのような、もっともらしい誤字が、実際によく起きます。公開ファイルにある名前は、이름{で始まる行から取り出せばよく(プレースホルダーは名前です)、コメント行の名前を数えてはいけません。採点ツールは、わざと間違えたルールファイルにもかけます。
収集対象をスキーマのあるオブジェクトにする
/root/gpumet/k8s/servicemonitor-crd.yamlに、servicemonitors.monitoring.coreos.comというCRDを書いて適用してください。グループはmonitoring.coreos.com、バージョンはv1、ネームスペーススコープ、種類はServiceMonitorです。スキーマは、spec.selector.matchLabels(文字列のマップ)、spec.endpoints(配列、minItems: 1、項目ごとにportが必須の文字列、pathは文字列、intervalは^[0-9]+(ms|s|m|h)$のパターンを持つ文字列)で、specには、selectorとendpointsがどちらも必須です。次に、/root/gpumet/k8s/servicemonitor.yamlに、gpu-metricsネームスペースのnvidia-dcgm-exporterを書いて適用してください(セレクターはapp: nvidia-dcgm-exporter、エンドポイント1つにport: gpu-metrics、path: /metrics、interval: 15s)。最後に、/root/gpumet/k8s/servicemonitor-bad.yamlに、名前dcgm-bad-intervalで同じものを書きますが、intervalを引用符なしの30で書いて適用し、拒否の出力を、標準エラー出力も含めて/root/gpumet/out/07-reject.txtに保存してください。
これが、CRDで扱う利点です。設定ファイルなら静かに無視されたはずの誤字が、適用時点で引っかかります。構造的スキーマで、minItemsは配列の長さの下限、patternは文字列の正規表現の制約です。YAMLでは、引用符なしの30は整数として読まれ、"30s"は文字列として読まれます。その違いが、このステップのすべてです。CRDを適用したあと、APIサーバーが新しい型を受け入れるまでに、1、2回の往復がかかります。
6行のGPU状況レポートを計算して出す
/root/gpumet/bin/gpu-report.sh <노출파일> <규칙파일>を作成してください(プレースホルダーは公開ファイルとルールファイルです)。6行を順番に出力します。GPUS=(公開に出てくるカード数)、MEAN_UTIL=(平均使用率、切り捨て)、MAX_TEMP=(最高温度)、ORPHAN_GPUS=(Podラベルなしで、FB_USEDが1024を超えるカード数)、XID_GPUS=(XIDエラーが0より大きいカード数)、ALERT_RULES=(ルールファイルのルールの総数)です。数字はすべて、引数で受け取った2つのファイルから計算する必要があります。ステップ3の公開ファイルとステップ5のルールファイルにかけて、出力を/root/gpumet/out/report.txtに保存してください。
前のステップで作成した2つのスクリプトが、すでに半分を行っています。ここでは、その計算を1つのツールにまとめます。カード数は、gpuラベルの異なる値の個数であって、サンプルの行数ではありません。平均は、四捨五入せずに切り捨てで出してください。採点ツールは、このスクリプトを別の公開ファイルと別のルールファイルにもかけます。そのため、数字を埋め込むと失敗します。Podラベルが付いたカードと付いていないカードが混在しているので、ラベルの有無はカード単位でまとめて判定してください。