Jetson nanoでGPUを使えるPodを起動するまで

# 概要
知り合いからJetson nano（以降Jetson）を提供いただいたので少し触ってみております。
その中でJetsonをWorker Nodeとして構築してPod上でGPUを使うことも試した見たため、備忘として本記事を作成しています。

## 本記事のゴール
Jetsonをk3sのWorker Nodeとして稼働させて、Pod上のTensorflowにてGPUを使えることを確認するまでとします。
今回k3sを採用している背景として、Jetsonでは一部k8sで必要なカーネルパラメータの設定等が不足しています。
対応としてカーネルのソースを展開して、パラメータを編集してコンパイルして入れ替えてあげる必要等があるもようです。
今回はサクッと試す中での話であったため、上記の影響を受けなかったk3sを用いています。

## 本記事の流れ
以下手順で進めていきます。
Jetsonは1から構築を行う形として進めていきます。
1. Jetson nanoのセットアップ
1. Jetson nanoのDocker上のTensorflowでGPUの動作確認
1. k3sのセットアップ
1. Jetson nanoのk3s上のTensorflowでGPUの動作確認

## 環境図
環境のイメージ図は以下のような形で、Master Node用のUbuntu1台と今回セットアップするJetson nano1台の構成とします。
Ubuntuは基本的にはk3sが動作して、Jetsonと同一NW上であれば、基本的にはなんでも問題ないです。
操作自体はそれぞれsshで行いますが、Jetsonは初期設定完了までは直接操作します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/944311f0-acd6-5efc-6aca-41f4a05a65bf.png)

# Jetson nanoのセットアップ
Jetson nanoのセットアップは以下工程となります。
1. SDカードへOSイメージの書き込み
1. 初期設定
1. (任意) その他設定

## SDカードへOSイメージの書き込み
まずはSDカードへOSイメージの書き込みを行う必要があります。
今回はver4.6.1のイメージをダウンロードして、SDカードへ書き込みを行っておきます。

https://developer.nvidia.com/embedded/jetpack-sdk-461

書き込みを行うツールは書き込みさえ行えれば、基本的には何でも問題ない認識です。

## 初期設定
書き込みを行ったSDカードをJetsonへ挿し、起動すればそのまま初期設定の画面が表示されます。
ベースのイメージがUbuntuのため、UbuntuのGUIを触られたことがある方なら見覚えがあると思います。

まず初めにライセンス規約への同意が表示されるため、チェックボックスにチェックを入れcontinueを選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/7acf1c99-7ca8-71d2-bf13-b0d04c11c30f.png)


続いて、言語選択とキーボードの設定が表示されるので、それぞれ任意で選択します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/cb4d4fd9-cb83-7274-e9c1-8b854e8c19e9.png)

次はインターネットの設定となります。
Jetsonにwifi nicを追加でつけた場合などはwifiの候補も表示されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/b4cd8436-1069-791f-d7e8-148eaf7e2fa5.png)

リージョンも任意で設定を行っておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/a4340d62-fc5d-2cb0-7d98-f4d4f09ad766.png)

そしてログインユーザーの設定とホスト名の設定を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/24a257d6-345d-9669-3c75-d28f5d8e1562.png)

最後にリソースの設定をそれぞれ終えたら完了となります。
完了後は自動で再起動が入るため、再起動完了まで待っておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/684dd34b-007a-c9cf-562a-691e9ec1b743.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/acec44d9-549d-ce1b-f1f5-94a17d8cc4fe.png)

## (任意) その他設定
最後のセットアップとしてその他周辺周りの設定を行っていきます。
今回は以下3つを行っていきます。
1. IPアドレスの固定化
1. GUIの無効化
1. リソース可視化ツールの導入

まず今後の操作の観点からIPアドレスは固定にしておきたいため、設定を行います。
セットアップの再起動完了後に、ログインを行いデスクトップ右上からインターネットの設定に移ります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/fafaa11e-50ed-13f6-fcce-e4e81e7e883a.jpeg)

IPv4 SettingsからManualを選択し、IPアドレス等を設定しておきます。
固定IPアドレスの反映は再起動が必要となりますが、他の設定でも再起動が必要なため一旦このまま進めます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/83b7bb37-2278-0bb5-5c11-c62f1d5d8326.jpeg)


続いてデスクトップ上のTerminalを開き、以下コマンドを実行してGUIを無効化しておきます。
今後の作業は基本的にはSSHで実施するため、使わない観点で無効にしています。
```bash
## GUI無効化
sudo systemctl set-default multi-user.target

## GUI有効化
sudo systemctl set-default graphical.target
```

なお、以下コマンドで簡単に有効化できますので、その点はご安心ください。
```bash
## GUI有効化
sudo systemctl set-default graphical.target
```

最後に可視化ツールをインストールしていきます。
このあとのセクションにてGPUの稼働を見ていきたいため、jtopというツールをインストールします。
Terminelにて以下コマンドにてインストールすることができます。
```bash
sudo apt-get update
sudo apt-get install python3-pip
sudo pip3 install -U jetson-stats
```

ここまでの設定が完了しましたら、以下コマンドで再起動を行いそれぞれ反映させます。
```
sudo reboot
```

再起動完了後は固定IPアドレスでのSSH接続ができるため、ここからはSSHに切り替えていきます。
先ほど指定した固定IPアドレスでSSHできるかを確認します。
ssh完了後、以下コマンドにて先程インストールした可視化ツールを起動してみます。
```bash
jtop
```

無事に動作しましたら、以下のような画面が確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/39d914a9-a4ed-ce6d-5003-90e485cbf85e.png)

なおこの画面は基本的には常に出しておくことを推奨しますため、複数のコンソールでSSHしておくと良いです。

以上でセットアップは完了となります。

# Jetson nanoのDocker上のTensorflowでGPUの動作確認
それでは用意しましたJetsonでDockerを動かして、Tensorflowを見ていきます。
JetsonではDockerは標準で用意されているため、Tensorflowのコンテナの展開を早速行います。

ここで注意点として、Jetsonは少し特殊な仕組みなようで、NVIDIAにてチューニングされたコンテナイメージをベースとするのが基本形のようです。
そのため、利用するTensorflowのイメージを探しに行きます。

なお、Jetson用に用意されているイメージはOSバージョンごとに存在します。
例えば今回のわたしの環境では以下コマンドで確認すると、「R32の7.1」ということがわかります。
```bash
cat /etc/nv_tegra_release
# R32 (release), REVISION: 7.1, GCID: 29818004, BOARD: t210ref, EABI: aarch64, DATE: Sat Feb 19 17:05:08 UTC 2022
```
そうしましたら、以下Nvidiaのカタログサイトより、R32.7.1を探しイメージの名称を確認します。

https://catalog.ngc.nvidia.com/orgs/nvidia/containers/l4t-tensorflow

今回は以下名称のイメージが該当のようでした。
```
l4t-tensorflow:r32.7.1-tf2.7-py3
```

それではこちらのイメージをそのまま展開!!といきたいところですが、実はこのままイメージを使いますと、Tensorflowとkerasのバージョン不一致にて一部動作に問題が発生します。
というのも最近はTensorflowインストール時にkerasも一緒にインストールされるのですが、そこでインストールされるkerasは最新版で固定されているもようです。
そのため、バージョン不一致が発生してしまう形になっているようです。

そのため、今回はkerasをバージョン指定して再インストールしたイメージを用意します。
以下Dockerfileを作成して、docker buildでコンテナイメージを作成しておきます。
```Dockerfile:Dockerfile
# ベースイメージは先程Nvidiaのサイトから見つけたもの
FROM nvcr.io/nvidia/l4t-tensorflow:r32.7.1-tf2.7-py3

# Kerasを入れ替えておく
RUN pip3 uninstall -y keras && pip3 install keras==2.3.1

CMD ["/bin/bash"]
```

```bash
sudo docker build -t custom-tensorflow-image .
```

作成完了後以下コマンドで実行して、そのままコンテナのbashに入ります。
```
sudo docker run -it --rm --runtime nvidia --network host custom-tensorflow-image:latest
```

今回はイメージ側にGPUを使うコマンドを入れていないため、コンテナのbashからpythonのコードを実行していきます。
まず以下でpythonのプロンプトに入ります。
```bash
python3
```

そうしましたら、以下コードを実行してGPUが認識されていることを確認します。
```bash
import tensorflow as tf

print(tf.config.experimental.list_physical_devices('GPU'))

## 出力
[PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')]
```

じつはこのタイミングにてコンテナ内のpythonプロセスにてGPUを掴んでいます。
その様子を先程の可視化ツールの2のGPU画面より確認できます。
画像下のプロセスを見ると「python3」というのが新しくできています。
※コンテナ終了時に消えます
ただし学習などのGPUをしっかり使う処理を行っていないため、基本このタイミングではGPUのリソース使用率は0%のままです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/bc3904c0-390b-3666-720a-5b3de0d012e7.png)

それでは以下コードを実行し、GPUが稼働するところ見ていきます。
```python
from tensorflow.keras import layers, models, datasets

# データセット読み込み
(x_train, y_train), (x_test, y_test) = datasets.mnist.load_data()
x_train, x_test = x_train / 255.0, x_test / 255.0

# model準備
model = models.Sequential([
    layers.Flatten(input_shape=(28, 28)),
    layers.Dense(128, activation='relu'),
    layers.Dropout(0.2),
    layers.Dense(10)
])
predictions = model(x_train[:1]).numpy()
print(predictions)
print(tf.nn.softmax(predictions).numpy())
loss_fn = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True)
print(loss_fn(y_train[:1], predictions).numpy())

# model コンパイル
model.compile(optimizer='adam', loss=loss_fn, metrics=['accuracy'])

# model学習
model.fit(x_train, y_train, epochs=5)

# 学習成果確認
model.evaluate(x_test, y_test, verbose=2)
```

実行していきますと、だいたい「model.fit」の学習のタイミングから一気にGPUの使用率が上昇することが確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/70346443-e3d1-80ff-6561-7be8293435c6.png)

以上でコンテナでのTensorflowのGPU確認は完了となります。

なお、このあとのk3sでも今回利用したコンテナイメージは使っていきますので、以下コマンドでdocker hubにアップロードしておきます。

```bash
docker login
# usernameとpasswordの入力が要求

docker tag <Image ID> <docker hub user>/custom-tensorflow-image:latest

docker push <docker hub user>/custom-tensorflow-image:latest
```

なお、アップロード等めんどい方で、同じ環境の方がいらっしゃいましたら、今回わたしが作成したコンテナイメージをお使いください。
```
storoveryjam/custom-tensorflow-image:latest
```

# k3sのセットアップ
それではコンテナは確認できましたので、k3sのセットアップに移ります。
なお、k3sは非常に簡単に動作せることができるため、そこまで重たい工程ではないです。

まずMaster Nodeにする端末へSSHを行い、以下コマンドを実行します。
今回の私の環境では、Ubuntuのマシンで実施しています。
```bash
curl -sfL https://get.k3s.io | sh -
```
これでMaster Nodeへの展開自体は完了となります。
（私は普段はEKSかkubeadmしか使ってなかったので、非常に簡単で驚きました）

念の為、Readyになっているか以下コマンドで確認しておきます。
k3sでは以下のように頭にk3sとつけて実行してあげる形となります。
```
sudo k3s kubectl get nodes

## 出力
NAME                 STATUS   ROLES                  AGE   VERSION
ubuntu-kube-master   Ready    control-plane,master   10m   v1.28.7+k3s1
```

それではWorkerを追加するために、実行用のコマンドを用意していきます。
まずMaster側で発行されているtokenが必要になるため、以下コマンドで取得します。
```
sudo cat /var/lib/rancher/k3s/server/token

##出力
K10<~~省略~~>4dac3::server:904<~~省略~~>d933
```

そうしましたら、Worker側にするJetsonのコンソールに戻り、以下コマンドを実行します。
```
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="--docker" K3S_URL=https://<MasterのIP>:6443 K3S_TOKEN=<取得したtoken? sh -
```

しばらくすると（環境によりけりかもしれないが10秒足らず）、でWorkerの追加が完了します。
念の為、先ほどと同様にget nodesコマンドで確認をしておきます。
```
sudo k3s kubectl get nodes

## 出力
NAME                 STATUS   ROLES                  AGE   VERSION
jetson-worker01      Ready    <none>                 2m    v1.28.7+k3s1
ubuntu-kube-master   Ready    control-plane,master   18m   v1.28.7+k3s1
```

以上でk3sの準備は完了となります。

# Jetson nanoのk3s上のTensorflowでGPUの動作確認
それでは最後にk3s上でTensorflowを動かして、GPUの稼働を見ていきます。

コンテナのところで作成したイメージをもとに以下yamlを用意します。
ここで注意点としてコンテナのときと異なり、ホスト側(Jetson)のファイルをいくつかマウントするよう指示しておく必要があります。
以下yamlのvolumeMountsとvolumesが該当部分となります。
利用しているJetsonによってはファイル名が異なったりするため、環境に合わせて変更を行ってください。
（例：cuda10.2→cuda10.0, libcudnn.so.8→libcudnn.so.7等）
```tensorflow-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: tensorflow-pod
spec:
  containers:
  - name: tensorflow-container
    image: storoveryjam/custom-tensorflow-image:latest
    securityContext:
      privileged: true
    command: ["/bin/bash", "-c"]
    args:
      - |
        sleep infinity
    volumeMounts:
    - mountPath: /usr/lib/aarch64-linux-gnu/tegra
      name: lib
    - mountPath: /usr/local/cuda-10.2
      name: cuda10-2
    - mountPath: /usr/lib/aarch64-linux-gnu/libcudnn.so.8
      name: cudnn
  volumes:
  - name: lib
    hostPath:
      path: /usr/lib/aarch64-linux-gnu/tegra
  - name: cuda10-2
    hostPath:
      path: /usr/local/cuda-10.2
  - name: cudnn
    hostPath:
      path: /usr/lib/aarch64-linux-gnu/libcudnn.so.8
  restartPolicy: Never
```

それではyamlの準備ができましたら、以下コマンドでk3sへ適用していきます。
```bash
sudo k3s kubectl apply -f tensorflow-pod.yaml
```

適用後以下コマンドでStatusを確認しておきます。
```bash
sudo k3s kubectl get pods -o wide --watch
```

StatusがRunningになりましたら、以下コマンドでPodのコンテナ内へ入ります。
（本来はファイルマウントでPod生成と共に実行する形が良いかなと思いつつ、色々試したかったのでこの方法で。。）
```bash
sudo k3s kubectl exec -it tensorflow-pod -- /bin/bash
```

入れましたら、コンテナのときと同様の手順でpythonコードを実行していき、同様にGPUが稼働していたら無事完了となります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/ef428380-6c85-7ef8-1f45-0d0c7760b464.png)

# まとめ
今回はJetsonのGPUをPod上でも動かそうというところで無事動作を見るところまではできました。
お手軽に使えるローカルのGPU環境としては検証等では使えるかなという印象です。
また、今回はわりと突発的に調べながら触ってみましたが、他にもたくさんやれることがありそうなのでまだまだ楽しめそうです。
今後はどっちかといえば、Podとかではなく純粋に触ってみようかなと思います。

## 参考リンク
https://catalog.ngc.nvidia.com/?filters=&orderBy=weightPopularDESC&query=
https://qiita.com/ysakashita/items/566e082a5d060eef5046
https://qiita.com/ntrlmt/items/5faf16d73102feeb5856
https://www.tensorflow.org/tutorials/quickstart/beginner


## おまけ
なんとなくKubeflowをJetsonに展開してみましたら、、リソースがパンクしました。。
皆様もリソース要件は気をつけましょう!!
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/79cce877-9b48-03f9-7bf0-c4c200b75616.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/406495/eeefb55a-7c85-df22-00ba-0c383ecd17e6.png)

以上!!