\\\\ ٩( 'ω' )و ////

いろんな形がなかったりするものをこねてこねてこねまくります

Prometheusとdcgm-exporterを使ってGPUの監視をする

Prometheus を使うとなったとき、 node-exporter を使ってマシンの CPU 使用率やメモリの使用量を監視すると思います。 しかし、GPU の監視をしようとしたとき、node-exporter だけでは GPU のメトリクスを取ることができません。 そこで NVIDIA/gpu-monitoring-tools にある dcgm-exporter という exporter を使うことで GPU のメトリクスを取ることができます。

dcgm-exporter は何をしているのか

dcgm-exporter 自身は NVIDIA が公開している Data Center GPU Manager(DCGM) というデータセンター向けの GPU 管理ツールを用いて GPU のメトリクスを取得しています。そのデータを Shell Script で node-exporter の Textfile Collector にメトリクスを渡している仕組みです。

インストール

(ここは基本的にNVIDIA/gpu-monitoring-toolsの README と同じことが書いてあります)

dcgm-exporter は systemd と Docker Image (nvidia/dcgm-exporter)が提供されています。

dcgm-exporter を使うには systemd と Docker の両方の場合で NVIDIA Driver のバージョン 384 以上を先にインストールしておく必要があります。

README には NVIDIA Tesla drivers と書いてありますが GeForce でも使えることを確認しています。ただし、EEC メモリなどに対応してないため一部のメトリクスは取得できません。

systemd

systemd を使った dcgm-exporter のインストールには別途 DCGM の rpm パッケージもしくは deb パッケージを https://developer.nvidia.com/data-center-gpu-manager-dcgm からダウンロードしてくる必要があります。

インストールには make のスクリプトが用意されています。

git clone https://github.com/NVIDIA/gpu-monitoring-tools
cd path/to/gpu-monitoring-tools/exporters/prometheus-dcgm

# Download and install DCGM, then

sudo make install
sudo systemctl start prometheus-dcgm

を実行すればバックグラウンドで dcgm-exporter のデーモンが動くので /run/prometheus 以下に dcgm.prom というファイルが生成されます。 これを node-exporter が読み込み、 Prometheus 側にメトリクスを渡してくれます。

アンインストールするときは

cd path/to/gpu-monitoring-tools/exporters/prometheus-dcgm

sudo systemctl stop prometheus-dcgm
sudo make uninstall

をすればいいです。

Docker

Docker で dcgm-exporter を使う場合、Docker の runtime を nvidia-docker2 にする必要があるので、インストールしておきましょう。

docker-compose で動かすならすでにあるので docker-compose up で動かせばすぐ使えます。

git clone https://github.com/NVIDIA/gpu-monitoring-tools
cd path/to/gpu-monitoring-tools/exporters/prometheus-dcgm

docker-compose up

Kubernetes

Kubernetes で動かす場合、リポジトリにある node-exporter-daemonset.yaml を動かせば良いです。

しかし Helm を使って prometheus をデプロイした場合は node-exporter の DaemonSet にまとめることはできません。 そこで node-exporter と dcgm-exporter の DaemonSet を分けることで使えるようになります。

まず dcgm-exporter のみの Pod をデプロイする DaemonSet を作成します。

apiVersion: extensions/v1beta1
kind: DaemonSet
metadata:
  labels:
    app: prometheus
    chart: prometheus-<version>
    component: node-exporter
    heritage: Tiller
    release: prometheus
  name: prometheus-dcgm-exporter
  namespace: <yournamespace>
spec:
  selector:
    matchLabels:
      app: prometheus
      component: node-exporter
      release: prometheus
  template:
    metadata:
      labels:
        app: prometheus
        component: node-exporter
        release: prometheus
    spec:
      nodeSelector:
        nvidia.com/gpu: true
      containers:
        - image: nvidia/dcgm-exporter:<version>
          name: nvidia-dcgm-exporter
          securityContext:
            runAsNonRoot: false
            runAsUser: 0
          volumeMounts:
            - name: collector-textfiles
              mountPath: /run/prometheus
      hostNetwork: true
      hostPID: true
      serviceAccount: prometheus-node-exporter
      serviceAccountName: prometheus-node-exporter
      volumes:
        - hostPath:
            path: /host/path/to/dcgm-exporter
          name: collector-textfiles

ここで重要なのは securityContextvolumeMounts の部分です。

securityContextrunAsNonRootrunAsUser には dcgm-exporter が GPU のプロセス情報を取ってくるために Root ユーザーでコンテナを動かす設定です。

volumeMounts には dcgm-exporter のデフォルトアウトプット先である /run/prometheus を指定します。これを別の場所に指定しまうと node-exporter 側でメトリクスが取れなくなってしまいます。

他にも、 volumes は dcgm-exporter が取得したメトリクスのファイルを node-exporter へ渡すために使います。 なので、node-exporter の values にも同じ path を指定します。

nodeSelectorGPU が入っている Node と GPU がない Node が混在している場合に、 nodeSelector を用いて GPU がない Node には dcgm-exporter をデプロイしないような設定が必要です。

yaml が書けたら kubectl コマンドでデプロイします。

続いて、 stable/prometheus の values に node-exporter が dcgm-exporter のメトリクスファイルを取得する設定を追加していきます。

nodeExporter:
  ## Additional node-exporter container arguments
  ##
  extraArgs:
    collector.textfile.directory: /srv/txt-collector

  ## Additional node-exporter hostPath mounts
  ##
  extraHostPathMounts:
    - name: collector-textfiles
      mountPath: /srv/txt-collector
      hostPath: /host/path/to/dcgm-exporter
      readOnly: true

stable/prometheus の values には dcgm-exporter のメトリクスファイルを取得するために extraArgsextraHostPathMounts を指定します。

extraHostPathMounts にはマウントさせる名前とコンテナにマウントする Path、ホスト側の Path、readOnly にするかどうかを指定します。 ここで namemountPath は自由に決めても大丈夫です。 hostPath に関しては dcgm-exporter の DaemonSet に書かれた volumes の Path を指定してください。

extraArgs には https://github.com/prometheus/node_exporter#textfile-collector にあるように collector.textfile.directory のオプションに対して extraHostPathMounts で指定した mountPath を指定します。

書けたら helm install で Prometheus をデプロイします。

helm install --namespace <yournamespace> --name prometheus -f apps/prometheus/helm.yml stable/prometheus

しばらくすると Prometheus 側で dcgm_gpu_temp のようなメトリクスが取得できます。

取得するメトリクスをカスタマイズする

ほとんどの場合、すでに組み込まれているメトリクスのみで十分ですが、GPU のブランド名や GPU ファンの回転率などのメトリクスを取りたい場合には独自でカスタマイズをする必要があります。

NVIDIA/gpu-monitoring-tools にある dcgm-exporter の Dockerfile を見てみると、dcgm-exporter というスクリプトを実行していることがわかります。

実際に dcgm-exporterスクリプトをみると 43 行から 130 行のところに Data Center GPU Manager のコマンドラインツールである dcgmiというコマンドを使ってメトリクスを生成しています。 よく読むと e オプションで指定されている ID らしきものを追加で指定することで新しいメトリックを取ることが出来そうです。 ですがその ID が何の意味を持つのか分からないので、一覧で表示させます。

dcgm-exporter のコンテナに bash でログインして dcgmi dmon -l を実行するとそれぞれのフィールドの Long name、Short name、Field Id が一覧でみることができます。

## コンテナにログイン
kubectl -n <yournamespace> exec -it prometheus-dcgm-exporter-768gf bash\

## 以下 コンテナ内
root@hostname:/% dcgmi dmon -l
___________________________________________________________________________________
           Long Name                              Short Name       Field Id
___________________________________________________________________________________
driver_version                                         DRVER              1
nvml_version                                           NVVER              2
process_name                                           PRNAM              3
device_count                                           DVCNT              4
name                                                   DVNAM             50
brand                                                  DVBRN             51
nvml_index                                             NVIDX             52
serial_number                                          SRNUM             53
uuid                                                   UUID#             54
minor_number                                           MNNUM             55
oem_inforom_version                                    OEMVR             56
pci_busid                                              PCBID             57
pci_combined_id                                        PCCID             58
pci_subsys_id                                          PCSID             59

...

vgpu_instance_vm_id                                    VVMID            520
vgpu_instance_vm_name                                  VMNAM            521
vgpu_instance_type                                     VITYP            522
vgpu_instance_uuid                                     VUUID            523
vgpu_instance_driver_version                           VDVER            524
vgpu_instance_memory_usage                             VMUSG            525
vgpu_instance_license_status                           VLCST            526
vgpu_instance_frame_rate_limit                         VFLIM            527
vgpu_instance_enc_stats                                VSTAT            528
vgpu_instance_enc_sessions_info                        VSINF            529
nvlink_recovery_errors                                 RECER            600
nvlink_fatal_errors                                   GNVFTL            601

これをみると、ドライバのバージョンを取得するときは ID に 1 を追加することで取得できることがわかります。

独自のメトリックを取る

今回はファンのスピードを追加してみます。

dcgmi dmon -l コマンドでみたファンのスピードを取得する ID は 191 です

root@hostname:/% dcgmi dmon -l | grep fan
fan_speed                                              FANSP            191

なので dcgm-exporter に以下の設定を追加します。

dcgmi dmon -d "${COLLECT_INTERVAL_MS}" -e \
"54,"\
"100,101,"\
- "140,150,155,"\
+ "140,150,155,191,"\
"202,203,204,206,207,"\
"230,240,241,242,243,244,245,246,"\
"251,252,"\
"310,311,312,313,"\
"390,391,392,"\
"409,419,429,439" | \
awk -v "out=${OUTPUT_FILE}" -v "ngpus=$(nvidia-smi -L | wc -l)" '
function metric(name, type, help, value) {
    if (value !~ "N/A") {
        if (gpu == 0) {
            printf "# HELP dcgm_%s %s\n", name, help > out".swp"
            printf "# TYPE dcgm_%s %s\n", name, type > out".swp"
        }
        printf "dcgm_%s{gpu=\"%s\",uuid=\"%s\"} %s\n", name, gpu, uuid, value > out".swp"
    }
}
(NF && NR > 2 && !($1 ~ "^#" || $1 ~ "^Id")) {
    # Labels
    i = 1
    gpu = $(i++)                                                                                                      # field 0 (implicit)
    uuid = $(i++)                                                                                                     # field 54
    # Clocks
    metric("sm_clock", "gauge", "SM clock frequency (in MHz).", $(i++))                                               # field 100
    metric("memory_clock", "gauge", "Memory clock frequency (in MHz).", $(i++))                                       # field 101
    # Temperature
    metric("memory_temp", "gauge", "Memory temperature (in C).", $(i++))                                              # field 140
    metric("gpu_temp", "gauge", "GPU temperature (in C).", $(i++))                                                    # field 150
    # Power
    metric("power_usage", "gauge", "Power draw (in W).", $(i++))                                                      # field 155
    #metric("total_energy_consumption", "counter", "Total energy consumption since boot (in mJ).", $(i++))             # field 156 TOBEFIXED

+   # Fan
+   metric("fan_speed", "gauge", "Fan speed (in %).", $(i++))                                                         # field 191

    # PCIe
    #metric("pcie_tx_throughput", "counter", "Total number of bytes transmitted through PCIe TX (in KB)", $(i++))                                                                     # field 200 TOBEFIXED
    #metric("pcie_rx_throughput", "counter", "Total number of bytes received through PCIe RX (in KB)", $(i++))                                                                     # field 201 TOBEFIXED
    metric("pcie_replay_counter", "counter", "Total number of PCIe retries.", $(i++))                                 # field 202

    # ...

    #metric("nvlink_bandwidth_total", "counter", "Total number of NVLink bandwidth counters for all lanes", $(i++))   # field 449 TODO
    # Flush output file and move it for atomicity
    if (gpu == ngpus - 1) {
        close(out".swp")
        system("mv "out".swp "out)
    }
}' &

metric を追加するときは、 e オプションに追加した ID とコメントで書かれた field ID の順番を同じにしないと追加した箇所以下のメトリックが全て違う値になってしまうので注意してください。

この方法では GPU の製品名(GeForce 1080 Ti など)を取ることができますが、awk のデフォルトの区切り文字がスペース or tab なのでメトリックがずれていくことになるので、別途その修正を入れる必要があります。

例えば、GeForce 1080 Ti の場合、GeForce と 1080 と Ti で分割されることになりずれます。 じゃあ awk-Fオプションで区切り文字を変えればいいじゃないかと考えてしまいますが image のベースになっている Ubuntu では awk の実装に mawk を用いているので -F オプションでの正規表現の range をサポートしていません。なので sed などで 2 つ以上のスペースを tab に変換するなどの作業が必要です。

image を作る

欲しいメトリックが取れるようなスクリプトを作ったら docker build コマンドで image を作ります。

docker build -t dcgm-exporter .

できたイメージを Docker Hub などに Push して dcgm-exporter の DaemonSet の image を作った image に変えてもらうことで独自のメトリクスを取ることができます。