tensorflow+Geforce RTXシリーズでパフォーマンスを確認してみる（その２）

# 概要

[前記事](https://qiita.com/kuroyagi/items/202c11247d08b314fb31)を受けて。

* Tensorflow+CUDA10+RTXシリーズ+fp16設定なのにTensorコア使われてない？の疑問を突っ込んで調べてみた
* もう少し単純なモデル＋Nvidia Visual Profilerで見ると、ちゃんとTensorコア使われてるっぽい。
* が、何も考えずにfp16にすると学習精度がだめだめ
* Backward計算を細かく設定できるPytorchとかに行った方がよさそう？もしくはTensorflow2.0を待つか..

ちなみに、前記事で何故fp16(TensorCore)使われなかったのかはまだ判明していません。だめじゃん。

# 確認したこと

1. もう少し単純なモデルでfp16設定→効いてるみたい
1. Nvidia Visual ProfilerでJupyter notebook経由のTensorflow CUDA呼び出し確認→fp16呼んでるみたい
1. 学習曲線を確認→死んでるみたい

## 具体手順
### 1. nvprof.exeの後に実行したいコマンドを渡してプロファイル収集
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.0\bin\nvprof.exe（パス通ってるはず）を実行

```:DOS窓
C:\Users\Kuroyagi\Jupyter_home > nvprof -o profile%p.nvvp --profile-child-processes jupyter notebook
```
--profile-child-processesと-oについてる%pに注意。
前者のオプションでjupyterからpythonまで芋づる式に拾ってくれるようになります。このオプションを指定した場合は出力ファイル名に%p(プロセス番号に置換)を含めないと駄目らしい。

### 2. Jupyter側で操作

Jupyter notebook側（ブラウザ）でtensorflow実行、終わったらDOS窓でCtrl+Cして終了させます。
最初は前記事のImages/Pix2Pixを解析しようとしたんですが、プロファイル結果が訳わからないことになったのでもう少し単純なものを。
[Keras作者の本](https://www.amazon.co.jp/dp/B07D498RJK/ref=dp-kindle-redirect?_encoding=UTF8&btkr=1)を写経したやつが残ってたので、これを使うことにしました。3.5章のやつです。
fp16/fp32の切り替えは例によってkeras.backend.set_floatx()で行っています。

```python
#題材はロイター記事の解析
from keras.datasets import reuters
(train_data, train_label), (test_data, test_label) = reuters.load_data(num_words=10000)

#fp16で実行する場合は以下を有効に
#import keras.backend as K
#K.set_floatx('float16')

#出現した単語群から記事のカテゴリを推測する
import numpy as np
from keras.utils.np_utils import to_categorical

def vectorize_sequentials(sequences, dimension=10000):
    results = np.zeros((len(sequences), dimension),dtype=np.float32)
    for i,seq in enumerate(sequences):
        results[i,seq] = 1.0
    return results

x_train = vectorize_sequentials(train_data)
x_test = vectorize_sequentials(test_data)
y_train = to_categorical(train_label)
y_test = to_categorical(test_label)

#単純なDense積層分類
from keras import models
from keras import layers

model = models.Sequential()
model.add(layers.Dense(256, activation="relu", input_shape=(10000,)))
model.add(layers.Dense(256, activation="relu"))
model.add(layers.Dense(46, activation="softmax"))

model.compile(optimizer='rmsprop', loss='categorical_crossentropy', metrics=['accuracy'])

#訓練/検証データ分離
x_val = x_train[:1000]
partial_x_train = x_train[1000:]
y_val = y_train[:1000]
partial_y_train = y_train[1000:]

#学習！
history = model.fit(partial_x_train, partial_y_train, epochs=20, batch_size=512, validation_data=(x_val,y_val))
```

### 3. CUDA10のインストールディレクトリにあるnvvp.bat(Windowsの場合)を実行する

C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v10.0\bin\nvvp.batを実行するとこの画面になるので適当に作業ディレクトリ設定。
![nvvp.png](https://qiita-image-store.s3.amazonaws.com/0/262860/5f419eb2-e7b8-4a7e-10c6-fb3419e409e6.png)

~~トリミング雑ぅ!とか言わないでね~~

### 4. 1.～2.でできたファイルを開く

メニュー→ファイル→Open

### 5. 実行されているAPI名を見て何が行われてるかなんとなく推測する

![fp32.png](https://qiita-image-store.s3.amazonaws.com/0/262860/850dd1a0-9569-5604-70b9-712ff2a89741.png)

volta_xxxとか出てるのを見るとちょっとテンション上がります。pascal(GTX10xxシリーズ)とは違うのだよ！
どうせならturing_xxだと尚良かったですが..

## 得られた結果

### 1. fp16指定時、明確に学習結果が変わった

タスクマネージャで見たところ、ちゃんとfp16指定時にもGPUがそれなりに動いていた(fp32でcompute_0の利用率10%、fp16で6%くらい)
ので、ちゃんとGPU側でfp16処理してると思われます。
fp16の方が遅くなってしまっているのはCPU/numpy側でのfp32(64?)→fp16変換のせい？

* デフォルト(fp32)時のKerasログ
 
```
Train on 7982 samples, validate on 1000 samples
Epoch 1/20
7982/7982 [==============================] - 2s 311us/step - loss: 1.9760 - acc: 0.5725 - val_loss: 1.2292 - val_acc: 0.7180
Epoch 2/20
7982/7982 [==============================] - 0s 54us/step - loss: 0.9583 - acc: 0.7879 - val_loss: 0.9796 - val_acc: 0.7910
...
Epoch 20/20
7982/7982 [==============================] - 0s 56us/step - loss: 0.0882 - acc: 0.9598 - val_loss: 1.1226 - val_acc: 0.8000
```

![fp32_train.png](https://qiita-image-store.s3.amazonaws.com/0/262860/5d6b0521-8504-d254-a262-83449e177cca.png)


* fp16指定時のKerasログ

```
Train on 7982 samples, validate on 1000 samples
Epoch 1/20
7982/7982 [==============================] - 3s 359us/step - loss: 0.3068 - acc: 0.0063 - val_loss: 0.0000e+00 - val_acc: 0.0060
...
Epoch 20/20
7982/7982 [==============================] - 1s 101us/step - loss: 0.0000e+00 - acc: 0.0061 - val_loss: 0.0000e+00 - val_acc: 0.0060
```

![fp16_train.png](https://qiita-image-store.s3.amazonaws.com/0/262860/47e38e3d-8c9e-ad99-aaa5-7cc3bc6bc209.png)

### 2. Nvidia Visual Profilerでみてもfp16処理が呼び出されてるっぽい

「具体手順」の5.ではfp32時のプロファイル結果をのせていますが、fp16の場合は以下のように変わります。turing_fp16_xxとか書いてありますね。

![fp16.png](https://qiita-image-store.s3.amazonaws.com/0/262860/5d0c3d10-3b4c-ef72-54e6-bf2ba5a2cb6f.png)


## 結論

というわけで、前の記事でのやり方は大筋では間違ってないと思われます。ちゃんとRTX2080でTensorCoreは使える..筈です。
fp16の方が遅くなってしまったのはモデルが単純すぎてCPU→GPU間転送の比率が大きくなってしまったためで
モデルがもっと大きくなればちゃんとfp16での高速化がみられるに違いありません。見られるんじゃないかな。多分見られると思う。~~でもちょっと覚悟はしておけ~~

しかし今回のモデルではfp16にすることで学習が全くダメダメになってしまいました。
学習精度を下げないためには特にbackward計算でfp32精度を混ぜる必要があるらしい（知ったかぶり）ですが
少なくともKerasでそんな複雑なことをできる気がしませんね。
Tensorflow 2.0ではこのあたりの仕組みが組み込まれるらしいですが、今すぐ使いたいならPytorchとかの方に流れた方が良いのかもしれません。