自宅サーバ（PowerEdge R720）で機械学習環境を作成する

# 背景

自宅サーバとしてPowerEdge R720を購入しカスタマイズしました。
- CPU: 16コア32スレッド（8コア16スレッド2.60 GHz×2）
- MEM: 256GiB (DDR3L、16GiB×16)
- SSD: 256GiB (SATA3、256GiB×1、RAID0)

https://qiita.com/itortie/items/9c8d9768b699fc7730e1

このスペックを活かして何かするには機械学習かk8sだろうということで、まずは機械学習に挑戦してみます。

機械学習といえばGPUも欲しくなりますが、PowerEdge R720で使えるのはNVIDIA Tesla Kシリーズが限界で、GPU本体のほか1100Wの電源や取り付け器具など追加出費が必要です。

https://www.dell.com/community/en/conversations/poweredge-hardware-general/tesla-k80s-in-dell-r720-server/647f8e90f4ccf8a8def78c63

また機器自体が古く最新のライブラリの機能が動かない可能性もあります。まずはCPUのみでどれだけ頑張れるか実際にやってみます。

# 環境構築
今回はJupyterでTensorFlowが実行できる環境を目指します。自宅のNW環境は、マンション備え付けの回線（無線LAN、有線LANポート1）という環境なので工夫しながらやります。

## OSインストール
OSはAlmaLinux9.2のminimalを使用します。イメージファイルをダウンロードでは、回線が詰まっていると時間がかかります。日本のミラーサイトを色々試したところ、Nishi Network　からのダウンロードがとても速くて助かりました。聞き覚えのない組織名だったので調べてみると個人運用でした。とてもありがたいです。

https://mirror.nishi.network/

iDRACから仮想ディスクをマッピングしてサーバを起動しOSインストールを行いました。最初起動に失敗しましたがBISOメニューからUEFIを有効にすると起動できるようになりました。

## インターネットへの接続
サーバには無線LANアダプタがないので、ノートPC（Windows10）経由でインターネットに接続させます。

https://qiita.com/MakotoA67/items/936c551d90916775236a

DHCPを使うと毎回IPアドレスを調べないといけなくて不便なので静的に設定しておきます。
L2SWもなくLANケーブルも1本しかないのでやりくりします。

1. iDRACはサーバのフロントパネルからIPを192.168.137.254に設定
1. ノートPCとiDRACのポートを接続
1. 仮想コンソールからサーバにログイン
    1. nmcliを使ってポートを設定
        1. `nmcli con eno1 edit`
        1. `set ipv4.addresses 192.168.137.254/24`
        1. `set ipv4.gateway 192.168.137.1`
        1. `set ipv4.dns 8.8.8.8`
        1. `set ipv4.method manual`
        1. `save`
        1. `activate`
    1. SSHでrootにパスワードログイン設定
        1. `vim /etc/ssh/sshd_config`
        1. `PermitRootLogin yes`
        1. `PasswordAuthentication yes`
        1. `systemctl restart sshd`
1. ノートPCとサーバのポートを接続
1. TeraTermを使ってID/PASSでSSH接続

## Jupyter環境の構築
今回はDockerを使ってJupyter環境を立ち上げます。

### Dockerインストール
1. `dnf config-manager --add-repo=https://download.docker.com/linux/centos/docker-ce.repo`
1. `dnf install docker-ce docker-ce-cli containerd.io`
1. `systemctl start docker`
1. `systemctl enable docker`

### Jupyterイメージの起動
1. `docker pull jupyter/base-notebook`
1. `docker run -p 8888:8888 -v /path/to/your/notebooks:/home/jovyan/work jupyter/base-notebook`

コンテナ起動時に表示されるTokenを使ってブラウザからアクセスするとJupyterが使えます。

# ベンチマーク
機械学習を試す前に、参考情報としてサーバのベンチマークを試します。

## sysbenchのインストール
1. `dnf install epel-release`
1. `dnf update`
1. `dnf install sysbench`

## CPU
passmarkでCPUのスコアを調べると9942（Single Thread Rating: 1687）でした。物理CPUを2個並べてますので、単純計算すると2万弱になります。
クロック数が高くないのでシングルスレッドの性能は劣りますが、全体の性能としてはAMD Ryzen 5 5500やIntel Core i5-12400が目安になりそうです。

https://www.cpubenchmark.net/cpu.php?cpu=Intel+Xeon+E5-2650+v2+%40+2.60GHz

sysbenchを使って実際に試してみます。比較用に`--cpu-max-prime=9999`で試します

### threads=1
```
sysbench cpu --cpu-max-prime=9999 run

CPU speed:
    events per second:  1020.73

General statistics:
    total time:                          10.0003s
    total number of events:              10212
```

### threads=2
```
sysbench cpu --cpu-max-prime=9999 --threads=2 run

CPU speed:
    events per second:  2022.73

General statistics:
    total time:                          10.0011s
    total number of events:              20233
```

### threads=4
```
sysbench cpu --cpu-max-prime=9999 --threads=4 run

CPU speed:
    events per second:  3949.42

General statistics:
    total time:                          10.0011s
    total number of events:              39505
```

### threads=8
```
sysbench cpu --cpu-max-prime=9999 --threads=8 run

CPU speed:
    events per second:  7395.63

General statistics:
    total time:                          10.0010s
    total number of events:              73977
```

### threads=16
```
sysbench cpu --cpu-max-prime=9999 --threads=16 run

CPU speed:
    events per second: 14556.50

General statistics:
    total time:                          10.0011s
    total number of events:              145606
```

### threads=32
```
sysbench cpu --cpu-max-prime=9999 --threads=32 run

CPU speed:
    events per second: 20776.00

General statistics:
    total time:                          10.0017s
    total number of events:              207829
```

### 評価

| threads | CPU speed | threads=1の単純倍との差分[%] | 
| --- | --- | --- |
| 1 | 1020.73 | 100 |
| 2 | 2022.73 | 99.1 |
| 4 | 3949.42 | 96.7 |
| 8 | 7395.63 | 90.1 |
| 16 | 14556.50 | 89.1 |
| 32 | 20776.00 | 63.6 |

2から4、4から8、16から32でそれぞれパフォーマンスが落ちています。
2から4では、ある程度線形に1％弱落ちています。
4から8では、ある程度線形ですが、落ち幅が大きくなり1％強落ちています。
8から16では、ほぼ差がありませんでした。
16から32では、26％も落ちています。

4コア程度まではターボブーストで特定コアのクロック数が高い状態となり、8コア以上はターボブーストがない状態なのでパフォーマンスの差が無くなったかと考えます。
物理コアを上回りハイパースレッドなどを使う場合、3割減のパフォーマンスになると思ってよさそうです。全体のスコアは上がっているので、同時に多数の処理を行う場合にはとても有効そうです。

以下のサイトの結果と比較すると「Ryzen 7 PRO 4750GE (3.1GHz)」と「Ryzen 7 5800HS」の間になります。

https://qiita.com/bellx2/items/a373cb786c36ec64f37d

Passmarkでは以下の通りなので、おおむね評価通りかと思います。
- Ryzen 7 PRO 4750GEのは18432（Single Thread Rating: 2688）
- Ryzen 7 5800HSは21166（Single Thread Rating: 3100）

## メモリ
比較対象の情報があまりないので結果を張るだけにとどめておきます。DDR3L-1333なので、速くはないはずです。

```
sysbench memory run

Total operations: 53872491 (5384814.25 per second)

52609.85 MiB transferred (5258.61 MiB/sec)

General statistics:
    total time:                          10.0002s
    total number of events:              53872491
```

## MNIST
MNISTをやってみます。最初に`!pip install tensorflow`でTensorFlowをインストールしておきます。
比較用にGoogle Colaboratoryでも同様のプログラムを実行しました。GPUモデルはT4です。

共通のコード
```
import tensorflow as tf

mnist = tf.keras.datasets.mnist
(x_train, y_train), (x_test, y_test) = mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
  tf.keras.layers.Flatten(input_shape=(28, 28)),
  tf.keras.layers.Dense(128, activation='relu'),
  tf.keras.layers.Dropout(0.2),
  tf.keras.layers.Dense(10, activation='softmax')
])

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
```

### batch_size=32
`model.fit(x_train, y_train, epochs=5)`
```
Epoch 1/5
1875/1875 [==============================] - 10s 5ms/step - loss: 0.2986 - accuracy: 0.9134
Epoch 2/5
1875/1875 [==============================] - 9s 5ms/step - loss: 0.1456 - accuracy: 0.9563
Epoch 3/5
1875/1875 [==============================] - 9s 5ms/step - loss: 0.1094 - accuracy: 0.9671
Epoch 4/5
1875/1875 [==============================] - 9s 5ms/step - loss: 0.0898 - accuracy: 0.9727
Epoch 5/5
1875/1875 [==============================] - 9s 5ms/step - loss: 0.0768 - accuracy: 0.9761
```

### batch_size=32 GPU
```
Epoch 1/5
1875/1875 [==============================] - 11s 3ms/step - loss: 0.2925 - accuracy: 0.9138
Epoch 2/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.1426 - accuracy: 0.9574
Epoch 3/5
1875/1875 [==============================] - 5s 3ms/step - loss: 0.1075 - accuracy: 0.9674
Epoch 4/5
1875/1875 [==============================] - 5s 3ms/step - loss: 0.0881 - accuracy: 0.9725
Epoch 5/5
1875/1875 [==============================] - 6s 3ms/step - loss: 0.0739 - accuracy: 0.9764
```

### batch_size=1024
`model.fit(x_train, y_train, epochs=5, batch_size=1024)`
```
Epoch 1/5
59/59 [==============================] - 1s 7ms/step - loss: 0.0511 - accuracy: 0.9841
Epoch 2/5
59/59 [==============================] - 1s 9ms/step - loss: 0.0454 - accuracy: 0.9863
Epoch 3/5
59/59 [==============================] - 1s 10ms/step - loss: 0.0431 - accuracy: 0.9866
Epoch 4/5
59/59 [==============================] - 1s 10ms/step - loss: 0.0422 - accuracy: 0.9872
Epoch 5/5
59/59 [==============================] - 1s 10ms/step - loss: 0.0400 - accuracy: 0.9876
```

### batch_size=1024 GPU
```
Epoch 1/5
59/59 [==============================] - 1s 7ms/step - loss: 0.0547 - accuracy: 0.9828
Epoch 2/5
59/59 [==============================] - 0s 6ms/step - loss: 0.0497 - accuracy: 0.9849
Epoch 3/5
59/59 [==============================] - 0s 4ms/step - loss: 0.0472 - accuracy: 0.9857
Epoch 4/5
59/59 [==============================] - 0s 4ms/step - loss: 0.0462 - accuracy: 0.9858
Epoch 5/5
59/59 [==============================] - 0s 4ms/step - loss: 0.0443 - accuracy: 0.9861
```


### 評価
Google Colaboratoryの方がbatch_size=32で1.5倍ほど速いです。
batch_size=1024でも5倍ほど速いですが。
ただし元々の学習時間が短いこともあり、15秒未満の違いに留まりました。

| batch_size | CPU[s] | GPU[s] |  CPU/GPU | 
| --- | --- | --- | --- |
| 32 | 46 | 33 | 1.4 |
| 1024 | 5 | 1 | 5.0 |



## CIFAR-10
より学習データの入力量が大きな場合を試してみます。
比較用にGoogle Colaboratoryでも同様のプログラムを実行しました。GPUモデルはT4です。

共通のコード
```
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Conv2D, MaxPooling2D, Flatten, Dense, Dropout

cifar10 = tf.keras.datasets.cifar10
(x_train, y_train), (x_test, y_test) = cifar10.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

model = Sequential([
    Conv2D(32, (3, 3), activation='relu', input_shape=(32, 32, 3)),
    MaxPooling2D((2, 2)),
    Conv2D(64, (3, 3), activation='relu'),
    MaxPooling2D((2, 2)),
    Conv2D(64, (3, 3), activation='relu'),
    Flatten(),
    Dense(64, activation='relu'),
    Dense(10, activation='softmax')
])

model.compile(optimizer='adam',
              loss='sparse_categorical_crossentropy',
              metrics=['accuracy'])
```

### batch_size=32
`model.fit(x_train, y_train, epochs=10, validation_data=(x_test, y_test))`
```
Epoch 1/10
1563/1563 [==============================] - 29s 18ms/step - loss: 1.5656 - accuracy: 0.4290 - val_loss: 1.2707 - val_accuracy: 0.5429
Epoch 2/10
1563/1563 [==============================] - 27s 17ms/step - loss: 1.1862 - accuracy: 0.5786 - val_loss: 1.1050 - val_accuracy: 0.6101
Epoch 3/10
1563/1563 [==============================] - 27s 17ms/step - loss: 1.0397 - accuracy: 0.6337 - val_loss: 1.0132 - val_accuracy: 0.6454
Epoch 4/10
1563/1563 [==============================] - 27s 17ms/step - loss: 0.9467 - accuracy: 0.6665 - val_loss: 0.9479 - val_accuracy: 0.6712
Epoch 5/10
1563/1563 [==============================] - 27s 17ms/step - loss: 0.8724 - accuracy: 0.6943 - val_loss: 0.9690 - val_accuracy: 0.6690
Epoch 6/10
1563/1563 [==============================] - 27s 17ms/step - loss: 0.8178 - accuracy: 0.7137 - val_loss: 0.9132 - val_accuracy: 0.6847
Epoch 7/10
1563/1563 [==============================] - 27s 17ms/step - loss: 0.7615 - accuracy: 0.7334 - val_loss: 0.9359 - val_accuracy: 0.6845
Epoch 8/10
1563/1563 [==============================] - 27s 17ms/step - loss: 0.7242 - accuracy: 0.7450 - val_loss: 0.8493 - val_accuracy: 0.7121
Epoch 9/10
1563/1563 [==============================] - 27s 17ms/step - loss: 0.6886 - accuracy: 0.7600 - val_loss: 0.9016 - val_accuracy: 0.6951
Epoch 10/10
1563/1563 [==============================] - 27s 17ms/step - loss: 0.6524 - accuracy: 0.7724 - val_loss: 0.9093 - val_accuracy: 0.6949
```

### batch_size=32 GPU
```
Epoch 1/10
1563/1563 [==============================] - 18s 5ms/step - loss: 1.5053 - accuracy: 0.4541 - val_loss: 1.3427 - val_accuracy: 0.5255
Epoch 2/10
1563/1563 [==============================] - 9s 6ms/step - loss: 1.1350 - accuracy: 0.5994 - val_loss: 1.0751 - val_accuracy: 0.6107
Epoch 3/10
1563/1563 [==============================] - 9s 6ms/step - loss: 0.9933 - accuracy: 0.6502 - val_loss: 0.9809 - val_accuracy: 0.6568
Epoch 4/10
1563/1563 [==============================] - 8s 5ms/step - loss: 0.8965 - accuracy: 0.6853 - val_loss: 0.9127 - val_accuracy: 0.6857
Epoch 5/10
1563/1563 [==============================] - 9s 6ms/step - loss: 0.8229 - accuracy: 0.7119 - val_loss: 0.8965 - val_accuracy: 0.6845
Epoch 6/10
1563/1563 [==============================] - 8s 5ms/step - loss: 0.7612 - accuracy: 0.7328 - val_loss: 0.8555 - val_accuracy: 0.7043
Epoch 7/10
1563/1563 [==============================] - 8s 5ms/step - loss: 0.7062 - accuracy: 0.7522 - val_loss: 0.8778 - val_accuracy: 0.7011
Epoch 8/10
1563/1563 [==============================] - 8s 5ms/step - loss: 0.6569 - accuracy: 0.7684 - val_loss: 0.9124 - val_accuracy: 0.6982
Epoch 9/10
1563/1563 [==============================] - 8s 5ms/step - loss: 0.6137 - accuracy: 0.7833 - val_loss: 0.8634 - val_accuracy: 0.7139
Epoch 10/10
1563/1563 [==============================] - 8s 5ms/step - loss: 0.5740 - accuracy: 0.7981 - val_loss: 0.9002 - val_accuracy: 0.7071
```

### batch_size=1024
`model.fit(x_train, y_train, epochs=10, batch_size=1024, validation_data=(x_test, y_test))`
```
Epoch 1/10
13/13 [==============================] - 9s 678ms/step - loss: 0.4405 - accuracy: 0.8484 - val_loss: 0.8206 - val_accuracy: 0.7357
Epoch 2/10
13/13 [==============================] - 8s 621ms/step - loss: 0.4373 - accuracy: 0.8499 - val_loss: 0.8209 - val_accuracy: 0.7349
Epoch 3/10
13/13 [==============================] - 8s 615ms/step - loss: 0.4355 - accuracy: 0.8506 - val_loss: 0.8206 - val_accuracy: 0.7347
Epoch 4/10
13/13 [==============================] - 8s 612ms/step - loss: 0.4336 - accuracy: 0.8509 - val_loss: 0.8221 - val_accuracy: 0.7351
Epoch 5/10
13/13 [==============================] - 8s 612ms/step - loss: 0.4323 - accuracy: 0.8518 - val_loss: 0.8229 - val_accuracy: 0.7353
Epoch 6/10
13/13 [==============================] - 8s 610ms/step - loss: 0.4311 - accuracy: 0.8517 - val_loss: 0.8242 - val_accuracy: 0.7373
Epoch 7/10
13/13 [==============================] - 8s 619ms/step - loss: 0.4298 - accuracy: 0.8527 - val_loss: 0.8237 - val_accuracy: 0.7373
Epoch 8/10
13/13 [==============================] - 8s 612ms/step - loss: 0.4285 - accuracy: 0.8532 - val_loss: 0.8245 - val_accuracy: 0.7345
Epoch 9/10
13/13 [==============================] - 8s 609ms/step - loss: 0.4275 - accuracy: 0.8536 - val_loss: 0.8254 - val_accuracy: 0.7359
Epoch 10/10
13/13 [==============================] - 8s 620ms/step - loss: 0.4259 - accuracy: 0.8531 - val_loss: 0.8267 - val_accuracy: 0.7355
```

### batch_size=1024 GPU
```
Epoch 1/10
49/49 [==============================] - 3s 55ms/step - loss: 0.4408 - accuracy: 0.8488 - val_loss: 0.8452 - val_accuracy: 0.7326
Epoch 2/10
49/49 [==============================] - 1s 30ms/step - loss: 0.3924 - accuracy: 0.8664 - val_loss: 0.8475 - val_accuracy: 0.7360
Epoch 3/10
49/49 [==============================] - 1s 28ms/step - loss: 0.3780 - accuracy: 0.8711 - val_loss: 0.8548 - val_accuracy: 0.7345
Epoch 4/10
49/49 [==============================] - 1s 25ms/step - loss: 0.3686 - accuracy: 0.8744 - val_loss: 0.8592 - val_accuracy: 0.7345
Epoch 5/10
49/49 [==============================] - 1s 26ms/step - loss: 0.3609 - accuracy: 0.8778 - val_loss: 0.8642 - val_accuracy: 0.7347
Epoch 6/10
49/49 [==============================] - 1s 26ms/step - loss: 0.3535 - accuracy: 0.8796 - val_loss: 0.8685 - val_accuracy: 0.7363
Epoch 7/10
49/49 [==============================] - 1s 25ms/step - loss: 0.3470 - accuracy: 0.8829 - val_loss: 0.8716 - val_accuracy: 0.7356
Epoch 8/10
49/49 [==============================] - 1s 26ms/step - loss: 0.3401 - accuracy: 0.8857 - val_loss: 0.8788 - val_accuracy: 0.7358
Epoch 9/10
49/49 [==============================] - 1s 26ms/step - loss: 0.3341 - accuracy: 0.8880 - val_loss: 0.8825 - val_accuracy: 0.7355
Epoch 10/10
49/49 [==============================] - 1s 26ms/step - loss: 0.3280 - accuracy: 0.8903 - val_loss: 0.8892 - val_accuracy: 0.7353
```

### 評価
比較用にGoogle Colaboratoryで同様のプログラムを実行しました。GPUモデルはT4です。
Google Colaboratoryの方がbatch_size=32で約3倍速いです。
batch_size=1024では約7倍速くなりました。
ただし元々の学習時間が短いこともあり、5分未満の違いに留まりました。

| batch_size | CPU[s] | GPU[s] |  CPU/GPU | 
| --- | --- | --- | --- |
| 32 | 272 | 93 | 2.9 |
| 1024 | 81 | 12 | 6.8 |

また、データのダウンロードについて、自宅サーバは7403秒かかったのに対し、Google Colaboratoryは2秒でした。
```
自宅サーバ

Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 [==============================] - 7403s 43us/step
```
```
Google Colaboratory

Downloading data from https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
170498071/170498071 [==============================] - 2s 0us/step
```

# まとめ
入力データのサイズが大きくない、数が少ないなどで元々学習に時間がかからないようなケースでは自宅サーバのCPUでも十分そうです。

しかし実際のケースではデータサイズも数も大きくなり、モデルを何度も調整するため繰り返し学習を行う場合がほとんどだと思います。また、GPUでしか動かない機能やコードもあるかと思いますので、素直にGoogle Colaboratoryを利用した方がよさそうです。