PythonからNVMLの関数を実行してGPUプロセッサ利用率などを取得する

# tl;dr
- ホントはChainer/CuPy Advent Calendarのネタとして,CuPyに[NVIDIA Management Library(以下NVML)](https://developer.nvidia.com/nvidia-management-library-nvml)の関数を呼べるメソッドを追加するネタを書くはずだった
- しかしCythonのエコシステム含め事前知識が思った以上に必要で,yak shavingしているうちに時間足りなくなって担当日に完成できず... :bow: 
- なので,取り急ぎPythonからNVMLの関数を使用する別のpythonラッパーを使って対応したのでその紹介

# PythonからNVMLの関数を実行したい忙しい人向けの結論
- NVMLのPython用ラッパーライブラリである[nvidia-ml-py3](https://pypi.org/project/nvidia-ml-py3/)があるのでこれを使う
- サンプルコードは[ここ](#nvidia-ml-py3を試してみる)
- ただしPython版のドキュメントがなさそうなので,使い方は[NVML API Reference Guide](https://docs.nvidia.com/deploy/nvml-api/nvml-api-reference.html#nvml-api-reference)を見て当該のCの関数を探して使う必要がある

# 背景
- とある実験のためChainerで実装した訓練用Scriptの実行中のGPUのプロセッサ利用率やメモリ利用率を計測する必要があった.
- 当初はNVIDIAが提供しているプロファイル用CLIである[nvidia-smi](https://developer.nvidia.com/nvidia-system-management-interface)をバックグラウンドで動かして計測していた

例

```sh
# 普通に実行するとフォアグラウンドで動くので&つけてバックグラウンドプロセスにする
$ nvidia-smi -i 0 -f nvidia_smi_result.csv -l 1 \
 --query-gpu=index,timestamp,memory.free,memory.used,utilization.gpu,utilization.memory \
 --format=csv,nounits &
$ nvidia_smi_proc=$!
$ python train.py
# 明示的にkillしないとファイルに計測内容がダンプされない
# のでほんとはNVML使って定期的にdumpするCLIを自作したい
$ kill -SIGINT $nvidia_smi_proc
```

- 最初はこれで計測した結果を用いて性能評価などをしていた
- が,次のステップとして,訓練用Script本体からGPU利用率を計測して,訓練中にパラメータを自動チューニングする機構を実装する必要が出てきた
- なので,pythonからGPUのプロファイルを行う方法について調査した

# NVIDIA Management Libraryとは
- NVIDIAが提供しているGPUのモニタリングや設定を変更するためのCライブラリ
- これを使うとGPUのプロセッサ利用率,メモリ使用容量,メモリI/Oの利用率などを取得できる
- CUDA Toolkitに含まれているのでCUDAをインストールすると付いてくる
- 他にもGPUの設定を動的に変更することができるようだが今回はプロファイルのみで良いのでそれ以外の機能については触れない

## Cでサンプルコードを実装してみる

まずは[NVML API Reference Guide](https://docs.nvidia.com/deploy/nvml-api/nvml-api-reference.html#nvml-api-reference)を見ながらcで直接関数を呼んでみて動かして取得したい数値が参照できるかを確認した.

```c
#include <stdio.h>
#include <stdlib.h>
#include <nvml.h>

int main(){
    nvmlReturn_t result;

    result = nvmlInit();
    if (NVML_SUCCESS != result){
         printf("Failed to initialize NVML: %s\n", nvmlErrorString(result));
         printf("Press ENTER to continue...\n");
         getchar();
         return 1;
    }

    unsigned int device_count;
    result = nvmlDeviceGetCount(&device_count);
    if (NVML_SUCCESS != result){
        printf("Failed to query device count: %s\n", nvmlErrorString(result));
        return 1;
    }

    for(int i=0; i<device_count; i++){
        nvmlDevice_t device;
        result = nvmlDeviceGetHandleByIndex(i, &device);
        if (NVML_SUCCESS != result){
            printf("Failed to get device handle: %s\n", nvmlErrorString(result));
            return 1;
        }

        nvmlUtilization_t util;
        result = nvmlDeviceGetUtilizationRates(device, &util);
        if (NVML_SUCCESS != result){
            printf("Failed to get utilization rates: %s\n", nvmlErrorString(result));
            return 1;
        }

        printf("gpu: %d\n", util.gpu);
        printf("memory: %d\n", util.memory);

        nvmlMemory_t memory;
        result = nvmlDeviceGetMemoryInfo(device, &memory);
        if (NVML_SUCCESS != result){
            printf("Failed to get device memory info: %s\n", nvmlErrorString(result));
            return 1;
        }

        printf("free: %lld\n", memory.free);
        printf("used: %lld\n", memory.used);
        printf("total: %lld\n", memory.total);
    }

    nvmlShutdown();
}
```

手元の環境では以下のようにコンパイルした

```sh
$ gcc -std=c99 -Wall nvml_test.c \
-I /usr/local/cuda-9.1/include \
-L /usr/lib64/nvidia \
-lnvidia-ml \
-o nvml_test 
```

これをGPUが2枚ささっているマシンで実行すると下記のような結果となった.
基本的にちゃんとGPUとCUDAが使える環境になっていれば動くはず.そうでないと最初の `nvmlInit()` の結果がエラーで終了する

```sh
$ ./nvml_test
gpu: 0
memory: 0
free: 16945512448
used: 0
total: 16945512448
gpu: 0
memory: 0
free: 16945512448
used: 0
total: 16945512448
```

# 対応方針

今回はChainerで実装したScriptでこれらの値を参照する必要がある.  
ChainerではGPUレイヤーに対してはCuPyからアクセスするようにしているので,CuPyからNVMLの関数を呼ぶように拡張するのがアーキテクチャとしては正しいと思った.

# すみません間に合いませんでした :bow: 
- そもそもCythonを使うのが初めてでCythonの学習自体に時間を要した
- だいたいわかったところでCuPyに実装してみるぞとなったところで,CuPyのpyxファイルのビルド自体が独自のスクリプトを用いて依存ライブラリの存在チェックなどを含めた機構が構築されており,その辺を読み解く必要があった

## ちなみに現在の状況

- 結果的にnvmlをリンクしいくつかの関数をCuPyから実行できるところまではこぎつけたが全部の関数を実装し終えることができず…
- あとCFLAGSで指定したnvmlをリンクするためのライブラリパスがコンパイラ側で何故か参照されずリンクできない問題が未解決でとりあえず無理やりpathをハードコーディングしてたり色々無理してるのでこの問題も解決が必要
  - 今は `install/build.py` の [get_compiler_setting()](https://github.com/cupy/cupy/blob/9b61e3bd86efc0f5f99f4b15d0c96a36d7c22c6f/install/build.py#L91) の内部で `library_dirs.append("/usr/lib64/nvidia")` をハードコーディングして無理やりnvmlのリンク時にnvmlのsoファイルが置いてあるpathを探索できるようにしてビルドできるようにしてたりするのでいずれはなんとかしたい
  - 手元の環境では諸事情によりcudnnをcudaと同じpathにインストールできない関係で、pip install時にCLFAGSとLDFLAGSでcudnnのパスを個別に指定していて,cudnnは問題なくリンクできているのだが,libnvmlのsoファイルがある `/usr/lib64/nvidia` が探索されないのかnvmlの方はリンクが失敗してしまう

```sh
export CFLAGS="-I/path/to/cudnn/include -I/usr/local/cuda-9.1/include"
export LDFLAGS="-L/path/to/cudnn/lib64 -L/usr/lib64/nvidia"
```

  - 取り急ぎのworkaround

```patch
--- a/install/build.py
+++ b/install/build.py
@@ -95,8 +95,10 @@ def get_compiler_setting():
     library_dirs = []
     define_macros = []
 
+    library_dirs.append("/usr/lib64/nvidia")
     if cuda_path:
         include_dirs.append(os.path.join(cuda_path, 'include'))
```

# とりあえずの対応
- とりあえずPythonからNVMLを呼べれば良いので,これを満たすことを優先とした
- NVIDIAから提供されているNVMLのPythonラッパーである[nvidia-ml-py3](https://pypi.org/project/nvidia-ml-py3/)を使ってみる

## nvidia-ml-py3を試してみる
今回の用途においては関数の呼び出し方はCでの実装とあまり変わらないようだった.
nvidia-ml-py3自体のドキュメントはないようなのでNVMLのAPIリファレンスを見ながら使う感じになりそう.

```python
import pynvml

pynvml.nvmlInit()

handle0 = pynvml.nvmlDeviceGetHandleByIndex(0)
handle1 = pynvml.nvmlDeviceGetHandleByIndex(1)

util_rate0 = pynvml.nvmlDeviceGetUtilizationRates(handle0)
util_rate1 = pynvml.nvmlDeviceGetUtilizationRates(handle1)

print(f'device0 nvml.util.gpu: {util_rate0.gpu}')
print(f'device0 nvml.util.memory: {util_rate0.memory}')

print(f'device1 nvml.util.gpu: {util_rate1.gpu}')
print(f'device1 nvml.util.memory: {util_rate1.memory}')
print('')

mem0 = pynvml.nvmlDeviceGetMemoryInfo(handle0)
mem1 = pynvml.nvmlDeviceGetMemoryInfo(handle1)

print(f'device0 nvml.mem.total: {mem0.total}')
print(f'device0 nvml.mem.used: {mem0.used}')
print(f'device0 nvml.mem.free: {mem0.free}')

print(f'device1 nvml.mem.total: {mem1.total}')
print(f'device1 nvml.mem.used: {mem1.used}')
print(f'device1 nvml.mem.free: {mem1.free}')

pynvml.nvmlShutdown()
```

```sh
$ python nvml_test.py
device0 nvml.util.gpu: 0
device0 nvml.util.memory: 0
device1 nvml.util.gpu: 0
device1 nvml.util.memory: 0

device0 nvml.mem.total: 16945512448
device0 nvml.mem.used: 0
device0 nvml.mem.free: 16945512448
device1 nvml.mem.total: 16945512448
device1 nvml.mem.used: 0
device1 nvml.mem.free: 16945512448
```

# 今後の課題
- CuPyのビルド周りの機構をちゃんと理解する
    - 特に[cupy_setup_build.py](https://github.com/cupy/cupy/blob/master/cupy_setup_build.py)で何をやっているかを理解して,新しい依存ライブラリを追加するにはどう変更すべきかを正しく検討する
- CuPy経由で今回やりたかったことをできるようにする

## いつぐらいまでに対応するか
- 来年1月下旬ぐらいまでには何とかしたい [^1]
- できたらまたQiitaの別記事としてまとめます :bow: 

[^1]: 今月は別の研究タスクと期末のレポート祭りで来月は確定申告の準備でちょっと時間がアレ…