【強化学習】クラウドサービスを利用した分散強化学習（kubernetes編）


この記事は自作している[強化学習フレームワーク](https://qiita.com/pocokhc/items/a2f1ba993c79fdbd4b4d)の解説記事です。

https://github.com/pocokhc/simple_distributed_rl

この記事のコード場所：[examples/kubernetes](https://github.com/pocokhc/simple_distributed_rl/tree/main/examples/kubernetes
) 

無料編の続きです。

1：[【強化学習】クラウドサービスを利用した分散強化学習（無料編）](https://qiita.com/pocokhc/items/f7a32ee6c62cba54d6ab)
2：ここ
3:[【強化学習】クラウドサービスを利用した分散強化学習（GKE/有料編）](https://qiita.com/pocokhc/items/e08aab0fe56566ab9407)

アーキテクチャ等の概要は前の記事を見てください。
まずはローカルでkubernetesを作成してみます。


# 全体イメージ

今回作成したい構成は複数のサービスが入り混じっています。
こういった複数のサーバを一元管理する方法の１つに kubernetes(k8s) があります。
k8sは今回初めて触るのである程度は大目に見てください…。

k8sの概念を簡単に言うと、Podが1つの仮想的なPCで、Podの中に複数のDockerコンテナがあるイメージです。
今回作成する構成の全体イメージは以下です。

![a-ページ7.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/396150/61c24e53-3bce-2842-cc18-d2c6677ed26b.png)

大きく3つのPodを作成します。

1. RedisPod
   + 全体で1Pod
   + RedisサーバコンテナとRedis-commander(GUI用)の2つのコンテナが起動
   + TaskBoard/ParameterBoard/Queueの3つの役割を兼任
   + 外部/内部を通してアクセスできるのはこのPodに対してのみを想定
1. TrainerPod
   + 全体で1Pod
   + Trainer用のコンテナが1つ動く想定
1. ActorPod
   + 全体でN個のPodが起動
   + 1Podに対してActor用のコンテナが1つ動く想定

ActorですがPodをレプリカという機能で複製します。（正しい使い方か分かりませんが…）
各PodはServiceという概念でそれぞれ接続します。（図でいう矢印）
※Pod内のコンテナはlocalhost扱いでそのまま通信できます
※Redisの各役割は別Podに切り出し可能です。最初はQueueの役割を別PodにRabbitMQを使っていましたが煩雑だったので統合しました。

# DockerDesktop + kubernetes

k8s は DockerDesktop が公式で機能を提供しているので勉強もかねてこれを利用します。

DockerDesktopがない場合は以下からインストールします。
https://www.docker.com/products/docker-desktop/

k8sの有効化は以下です。
https://matsuand.github.io/docs.docker.jp.onthefly/desktop/kubernetes/

詳細は省略しますが、有効にすれば立ち上がってきます。
k8sはコンテキストという単位でプロジェクトを扱っているようで、docker-desktopというものが出来てるはずです。

"kubectl" コマンドが使えるかの確認も兼ねて、以下でコンテキストを確認しておきます。

``` bash
# コンテキスト変更
> kubectl config use-context docker-desktop
> kubectl config get-contexts
CURRENT   NAME            CLUSTER         AUTHINFO       NAMESPACE
*         docker-desktop  docker-desktop  docker-desktop
```

## Podの概念

ここが結構悩んだので備忘録で書いておきます。
k8sには Pod,ReplicaSet,Deployment という概念があり、以下の意味になります。

+ Podはk8sの最小単位で1つのPodに複数のコンテナが入り、Pod内は同じリソースが共有される(CPU,メモリやネットワーク等が共通)
+ ReplicaSetは同じPodを複製する機能
+ DeploymentはReplicaSetをラップし、デプロイ・更新・ロールバック等も含めて管理する

包含関係としては、Deployment ⊃ ReplicaSet ⊃ Pod となります。

基本はDeploymentを使えば問題ないですが、今回のバッチ処理みたいな要件だと相性が悪いようで、次の記事のGKEでリソースの確保で失敗するわするわ…。
Deploymentは少々高機能かもしれないのでPodを基本利用します。

一般的にDeploymentを使わずにPodを使う場合は以下のようです。（ChatGPT曰く）

+ 学習やテスト：Deploymentは複数のPodを管理するためのもので、単一のPodを使う場合は直接Podを定義することがあります
+ 一時的なジョブやタスク：例えばバッチ処理やデータのクリーニングなどの一時的な作業を行う場合は、そのジョブ専用のPodを直接作成することがあります
+ 特定の制御が必要な場合：Deploymentはデプロイや更新の管理に便利なツールですが、特定の制御が必要な場合はPodを直接作成する場合があります

ほぼ今回の場合ですね。
私の解釈ですが、永続的なサービスはDeployment、バッチ処理系はPod(or ReplicaSet)で扱った方が安定するイメージです。


## 1. Redisの立ち上げ

まずはWeb上のコンテナイメージをそのまま使えるRedisを立ち上げてみます。
マニフェストは以下です。
GEA側もあるので作業ディレクトリは分けたいと思います。
ディレクトリ構成は以下です。

```
./
 └ docker_desktop/ # new
    └ redis.yaml   # new
```

``` yaml: ./docker_desktop/redis.yaml
apiVersion: v1
kind: Service
metadata:
  name: redis-internal-service
spec:
  type: ClusterIP
  selector:
    app: redis
  ports:
  - name: redis-internal-port
    protocol: TCP
    port: 6379
    targetPort: 6379

---
apiVersion: v1
kind: Service
metadata:
  name: redis-external-service
spec:
  type: NodePort
  selector:
    app: redis
  ports:
  - name: redis-external-port
    protocol: TCP
    port: 6379
    nodePort: 30001
  - name: redis-web-port
    protocol: TCP
    port: 8081
    nodePort: 30002


---
apiVersion: v1
kind: ConfigMap
metadata:
  name: redis-config
data:
  redis.conf: |-
    bind 0.0.0.0
    port 6379

---
apiVersion: v1
kind: Pod
metadata:
  name: redis-pod
  labels:
    app: redis
spec:
  containers:
    - name: redis-server
      image: redis:7.2-alpine
      ports:
        - containerPort: 6379
      command: ["redis-server", "/etc/redis/redis.conf"]
      volumeMounts:
        - name: redis-config
          mountPath: /etc/redis
      resources:
        limits:
          cpu: "900m"
    - name: redis-commander
      image: rediscommander/redis-commander:latest
      env: [{"name": "REDIS_HOSTS", "value": "local:localhost:6379"}]
      ports:
        - containerPort: 8081
      resources:
        limits:
          cpu: "200m"
          memory: "64Mi"
  volumes:
    - name: redis-config
      configMap:
        name: redis-config

```

内容はRedisのPodを作成し、クラスタ内部に6379ポートを、外部に向けて30001ポート(Redis)、30002ポート(Redis管理画面)を開放する内容になっています。

ファイルを作成したらプロンプトで以下を実行し、起動させます。

``` sh
> kubectl apply -f ./docker_desktop/redis.yaml
```

以下で確認できます。

``` sh
> kubectl get pods
NAME        READY   STATUS    RESTARTS   AGE
redis-pod   2/2     Running   0          14s
```

起動ができていれば以下から管理画面に入ることができます。

http://localhost:30002/

削除する場合は以下です。

``` sh
> kubectl delete -f ./docker_desktop/redis.yaml
```


## 2. コンテナイメージの作成

Actor/Trainerで使用するコンテナイメージを作成します。
このイメージはGKEでも同じものを使います。
ここで作成するファイルは以下です。

```
./
 ├ docker_desktop/
 |  └ redis.yaml
 |
 ├ dockerfile        # new
 ├ server_actor.py   # new
 └ server_trainer.py # new
```

まずはエントリーポイント用のコードを作成します。

``` python: ./server_actor.py
from srl.runner.distribution import RedisParameters, actor_run_forever
from srl.utils.common import logger_print

logger_print()
actor_run_forever(RedisParameters(host="redis-internal-service"), None)
```

``` python: ./server_trainer.py
from srl.runner.distribution import RedisParameters, trainer_run_forever
from srl.utils.common import logger_print

logger_print()
trainer_run_forever(RedisParameters(host="redis-internal-service"),None)
```

k8s内部からのアクセスになるので "redis-internal-service" でアクセスします。
（これはredis.yamlで定義しています）

次にDockerfileを作成します。
Tensorflowのイメージはサイズが大きいです…

``` dockerfile: ./dockerfile
# syntax=docker/dockerfile:1

# --- select image CPU(1.76GB) or GPU(7.38GB)
FROM tensorflow/tensorflow:2.14.0-gpu
#FROM tensorflow/tensorflow:2.14.0

WORKDIR /code
RUN apt-get update \
 && apt install -y --no-install-recommends git \
 && apt-get install -y --no-install-recommends libgl1-mesa-dev libglib2.0-0 \
 && rm -rf /var/lib/apt/lists/* \
 && pip install --no-cache-dir git+https://github.com/pocokhc/simple_distributed_rl@v0.13.3 \
# 必要なライブラリを適宜入れる
 && pip install --no-cache-dir opencv-python pygame gymnasium redis async_timeout

# エントリーポイントのファイルをコピー
COPY server_*.py /code/

```

以下でイメージ化します。(最後の.も必要)

``` sh
> docker build --pull --rm -t srl_qiita:latest .
```

## 3. Actor/Trainerマニフェストファイル

最後にActor/Trainer用のマニフェストファイルを作成します。

```
./
 ├ docker_desktop/
 |  ├ redis.yaml
 |  ├ actor.yaml    # new
 |  └ trainer.yaml  # new
 |
 ├ dockerfile
 ├ server_actor.py
 └ server_trainer.py
```

``` yaml: ./docker_desktop/trainer.yaml
apiVersion: v1
kind: Pod
metadata:
  name: trainer-pod
spec:
  containers:
    - name: trainer-node
      image: srl_qiita:latest
      imagePullPolicy: Never  # この行でローカルのimageを見るようにしている
      #command: ["sh", "-c", "while true; do sleep 3600; done"]
      command: ["python", "-u", "/code/server_trainer.py"]
      resources:
        limits:
          cpu: "950m"

```

※DockerDesktopのk8sはGPUには対応していないとの事
※pythonは"-u"オプションを追加し、printがk8sのlogに出力されるようにしています

``` yaml: ./docker_desktop/actor.yaml
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: actor-pods
spec:
  replicas: 1  # ここがそのままActor数になる
  selector:
    matchLabels:
      app: actor
  template:
    metadata:
      labels:
        app: actor
    spec:
      containers:
        - name: actor-node
          image: srl_qiita:latest
          imagePullPolicy: Never  # この行でローカルのimageを見るようにしている
          #command: ["sh", "-c", "while true; do sleep 3600; done"]
          command: ["python", "-u", "/code/server_actor.py"]
          resources:
            limits:
              cpu: "950m"

```

ActorはReplicaSetで作成しています。
replicasの数が複製するPodの数を表し、そのままActor数になります。
ただ、その数だけCPUも必要になりますが…。

以下で起動します。

``` sh
> kubectl apply -f ./docker_desktop/trainer.yaml
> kubectl apply -f ./docker_desktop/actor.yaml
```

以下で確認します。

``` sh
> kubectl get pods
NAME               READY   STATUS    RESTARTS   AGE
actor-pods-2h47z   1/1     Running   0          61s
redis-pod          2/2     Running   0          19m
trainer-pod        1/1     Running   0          13s
```

もし起動しない場合は command をデバッグ用に変えて起動し、以下で中に入って調べてみてください。
（[NAME]には get pods の NAME を入力）

``` sh
> kubectl exec -it [NAME] -- bash
```

また、各Podのログを見るのは以下です。
-fはオプションを追加するとリアルタイムでログが見れます。

``` sh
> kubectl logs -f [NAME]
```


## 4. 学習の実行

これで準備ができたので学習します。
Redisのポート30001に向けてアクセスします。

``` python: main.py
import srl
from srl.algorithms import dqn
from srl.runner.distribution import RedisParameters

rl_config = dqn.Config()
rl_config.hidden_block.set_mlp((64, 64))
rl_config.memory.set_proportional_memory()
rl_config.memory.capacity = 5000
runner = srl.Runner("Pendulum-v1", rl_config)

runner.train_distribution(
    RedisParameters(host="localhost", port=30001),
    actor_num=1,
    max_train_count=20_000,
)

print(runner.evaluate())
```



実行結果

``` bash
> python main.py
18:45:35 ACTIVE  1.00s(     - left),      0tr (-1451.4eval)
 trainer  573db05d-d1e4-4178-a2c0-b36f03fbf303  0.9s:     0tr/s,    0recv/s,        tr,       0recv
 actor0   not assigned
18:46:36 ACTIVE   1.0m(  3.5m left),   4487tr (-1244.9eval)
 trainer  573db05d-d1e4-4178-a2c0-b36f03fbf303 10.2s:    74tr/s,  440recv/s,    4487tr,   26578recv
 actor0   b456bf97-deda-4839-8a83-fb26977abe18 10.2s:   372st/s,  371send/s,   22461st,   22424send
18:47:36 ACTIVE   2.0m(  1.7m left),  10291tr (-0.968eval)
 trainer  573db05d-d1e4-4178-a2c0-b36f03fbf303  9.5s:    96tr/s,  455recv/s,   10291tr,   54000recv
 actor0   b456bf97-deda-4839-8a83-fb26977abe18  9.5s:   457st/s,  457send/s,   50006st,   49987send
18:48:36 ACTIVE   3.0m(43.33s left),  15941tr (-127.4eval)
 trainer  573db05d-d1e4-4178-a2c0-b36f03fbf303  9.8s:    93tr/s,  444recv/s,   15941tr,   80832recv
 actor0   b456bf97-deda-4839-8a83-fb26977abe18  9.8s:   446st/s,  447send/s,   76966st,   76950send
18:49:11 END   3.6m( 0.00s left),  20000tr ( -13.4eval)
 trainer  573db05d-d1e4-4178-a2c0-b36f03fbf303  1.0s:   115tr/s,  436recv/s,   20000tr,   96217recv
 actor0   b456bf97-deda-4839-8a83-fb26977abe18  5.0s:   509st/s,  509send/s,   94930st,   94915send

[-13.179330630227923, -302.80894811078906, -12.675792848691344, -128.10729961842299, -247.43346573412418, -12.799946028739214, -365.9429765045643, -132.10430204682052, -14.009112168103456, -134.4337202552706]
```

終わったら削除しておきます。
この構成はDBへの保存は全くないので、削除すればすべて初期化されます。

```
# 一括削除
> kubectl delete -f ./docker_desktop

# 一括起動
> kubectl apply -f ./docker_desktop
```

次はこのk8s環境を Google Kubernetes Engine (GKE) に作成してみたいと思います。
[【強化学習】クラウドサービスを利用した分散強化学習（GKE/有料編）](https://qiita.com/pocokhc/items/e08aab0fe56566ab9407)