TKG上にTanzu PackageでデプロイしたKubeflowとKubeRayを連携する

本記事は、[TUNA-JP Advent Calendar 2023](https://qiita.com/advent-calendar/2023/tuna-jp) の19日目になる。
ただし、TUNA-JP Advent CalendarなのにTanzu要素はメインではなく、TKG使ってるくらいな点はご容赦を。。。

先日のVMware ExploreでVMware(現Broadcom)はPrivate AIに注力すると発表しており、リファレンスアーキテクチャとしてVMware Cloud Foundation上に[Ray](https://www.ray.io/)や[Kubeflow](https://www.kubeflow.org/)を載せたものを公開している。
![1702625651209.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/9880d0f3-7c81-0c2a-f101-453facb5e5a9.png)
([Ray Integration with Kubeflow on vSphere](https://core.vmware.com/resource/ray-integration-kubeflow-vsphere#ray-integration-with-kubeflow-vmware-distribution)より引用)

そういう流行りに乗っかって、過去にRayとKubeflowについていくつか試してメモ化してきた。
- [KubeflowをTanzu Mission Controlでインストールして使ってみる](https://qiita.com/ipppppei/items/371cd0eed13dd28f7c4f)
- [Tanzu Kubernetes Grid上でllama.cppをサクッと使う](https://qiita.com/ipppppei/items/d984c3c3e1fc797099f0)
- [Ray ClusterをvSphere上にRay CLIでデプロイする](https://qiita.com/ipppppei/items/80d3c81f480ad117bf6b)

ただ、上記のメモではRayとKubeflowは分かれていたが、ここではこれらを統合させて使ってみる。
元ネタは[Ray Integration with Kubeflow on vSphere](https://core.vmware.com/resource/ray-integration-kubeflow-vsphere#ray-integration-with-kubeflow-vmware-distribution)というブログとなる。
なお今回の記事ではRayとは何か、Kubeflowとは何か、については特に触れていないので、必要であれば他の人の記事等を参照して欲しい。
## 今回のゴール
ここでのゴールは元ネタの記事の以下の図を実現する。
![1702615703086.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/a906125d-7e9a-e1a8-0127-cab4065abfc4.png)
こちらの図はvSphereが提供する、MLエンジニアやデータサイエンティストにとって望ましい開発環境とのことなので、これを作って作成手順などを確認する。

## 前提
KubeflowとRayを連携させる前提として、以下が終わっているものとする。
- KubeflowをTKG上で構築済み
- 作業端末にはHelmをインストール済み

なお、TKGノードにはGPUは含まないものとする。（含んでいてもいいけど、ここでは使わない）
Kubeflowの構築は[TMCで構築](https://qiita.com/ipppppei/items/371cd0eed13dd28f7c4f)したが、他のやり方でも多分問題ない。

## 検証
元ネタの記事に[run.sh](https://github.com/vsphere-tmm/RayIntegrationWithKubeflowVMwareDistribution/blob/main/RayInstallationandIntegration/run.sh)という自動構築・設定スクリプトが配布されているが、今回は挙動確認のために使わずに構築・設定する。
なお、このスクリプトを使う人は以下の点に注意。
- 失敗した場合の戻しの処理はない
- GPUを使わない場合は先頭の`rayclusterwithgpu`をfalseにする必要あり
- GPUを使う場合、Ray ClusterのWorkerとなるPodの`requests`がCPU8、メモリ32Gと大きい点に注意
- **Integration設計がKubeflowのリコンサイルで最大5分しか持たない**

### KuberayとRayクラスタの構築
RayのKubernetes版であるKuberayをHelmでインストールする。
```sh
helm repo add kuberay https://ray-project.github.io/kuberay-helm/
helm install kuberay-operator kuberay/kuberay-operator --version 0.5.0 --create-namespace -n ray
```
なお、バージョンを指定せずに新しいものを使ってみたが、Rayクラスタの起動に失敗したのでrun.shに書いてあったバージョンを指定している。
インストールに成功すると、Operator Podが起動する。
```sh
$ kubectl get pod -n ray
NAME                                READY   STATUS    RESTARTS   AGE
kuberay-operator-6bf887b5fb-kgfqb   1/1     Running   0          6m36s
```
次にRayクラスタを作成する。
ここではHelmで作成しているが、`kind: RayCluster`を自作しても問題ない。

```sh
helm install raycluster kuberay/ray-cluster --set image.tag=2.2.0-py38-cpu -n ray  --version 0.5.0 --set service.type=LoadBalancer
```
`--set service.type=LoadBalancer`をつけているが、クラスタ外から`ray`CLIなどで叩きたい等の要望がなければ外してOK。今回の検証でも利用しない。
その他、設定を変更したい場合は[values.yamlの仕様](https://github.com/ray-project/kuberay/blob/master/helm-chart/ray-cluster/values.yaml)を確認するとよい。
起動すると、HeadとWorkerが1台ずつ起動する。
```sh
$ kubectl get raycluster -n ray
NAME                 DESIRED WORKERS   AVAILABLE WORKERS   STATUS   AGE
raycluster-kuberay   1                 1                   ready    2m11s
$ kubectl get pod -n ray
NAME                                          READY   STATUS    RESTARTS   AGE
kuberay-operator-7fc785cbbd-4vqm9             1/1     Running   0          3m16s
raycluster-kuberay-head-st895                 1/1     Running   0          2m28s
raycluster-kuberay-worker-workergroup-dn72h   1/1     Running   0          2m28s
```
### KuberayとKubeflowの連携
Kubeflow側に、Kubeflowの中で/ray-cluster/を参照した場合にRayクラスタのServiceを参照するよう`VirtualService`を設定する。
（Kubeflowインストール時にIstioもインストールされており、`VirtualService`リソースが使えるようになっている）
```yaml
cat << EOF | kubectl apply -f -
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: ray-cluster-virtual-service
  namespace: kubeflow
spec:
  gateways:
  - kubeflow-gateway
  hosts:
  - '*'
  http:
  - match:
    - uri:
        prefix: /ray-cluster/
    rewrite:
      uri: /
    route:
    - destination:
        host: raycluster-kuberay-head-svc.ray.svc
        port:
          number: 8265
EOF
```
KubeflowのダッシュボードのサイドバーにRayへのリンクを追加するために、`ConfigMap`を修正する。
ただし、普通に`ConfigMap`を修正してもkapp-controllerにより元に戻されてしまうため、Overlayする形で`Secret`として差し込む。
まずはOverlayする内容を確認するために、変更後の`ConfigMap`を作成する。
```sh
kubectl get configmap centraldashboard-config -n kubeflow -o jsonpath='{.data.links}' > current.json
new_item='{
  "type": "item",
  "link": "/ray-cluster/",
  "text": "Ray on vSphere",
  "icon": "book"
}'
cat current.json | jq '.menuLinks |= map(if .link == "/pipeline/#/artifacts" then ., '"$new_item"' else . end)' > updated.json
kubectl get configmap centraldashboard-config -n kubeflow -o jsonpath='{.data.settings}' > settings.json
kubectl create configmap centraldashboard-config --from-file=links=updated.json --from-file=settings=settings.json -n kubeflow --dry-run=client -o yaml > update-cm.yaml
```

次にkapp-controllerがOverlayするための準備をする。
公式手順は[Overlays with PackageInstall](https://carvel.dev/kapp-controller/docs/v0.48.x/package-install-extensions/)に記載がある。
ざっくり、仕組みは以下となっている。
1. Overlayしたい内容を`Secret`として作成する
2. `PackageInstall`オブジェクトの`annotation`に`ext.packaging.carvel.dev/ytt-paths-from-secret-name.0` に作成したSecret名を記載する
3. 5分間隔くらいでPollingしているkapp-controllerが`Secret`を参照し、Overlayを実施する

ということで、最初にOverlayするYAMLを作成する。
先程作成したupdate-cm.yamlを元に、以下のような`Secret`を作成・Applyする。
<details><summary>OverlayのSecret（クリックすると表示）</summary><div>

```yaml
cat << EOF | kubectl apply -n my-packages -f -
apiVersion: v1
kind: Secret
metadata:
  name: my-overlay-secret
stringData:
  update-for-ray.yml: |
    #@ load("@ytt:overlay", "overlay")
    #@overlay/match by=overlay.subset({"kind":"ConfigMap", "metadata":{"name":"centraldashboard-config"}}),expects="1+"
    ---
    data:
      #@overlay/match missing_ok=True
      links: |
        {
          "menuLinks": [
            {
              "type": "item",
              "link": "/jupyter/",
              "text": "Notebooks",
              "icon": "book"
            },
            {
              "type": "item",
              "link": "/tensorboards/",
              "text": "Tensorboards",
              "icon": "assessment"
            },
            {
              "type": "item",
              "link": "/volumes/",
              "text": "Volumes",
              "icon": "device:storage"
            },
            {
              "type": "item",
              "link": "/kserve-endpoints/",
              "text": "Models",
              "icon": "kubeflow:models"
            },
            {
              "type": "item",
              "link": "/katib/",
              "text": "Experiments (AutoML)",
              "icon": "kubeflow:katib"
            },
            {
              "type": "item",
              "text": "Experiments (KFP)",
              "link": "/pipeline/#/experiments",
              "icon": "done-all"
            },
            {
              "type": "item",
              "link": "/pipeline/#/pipelines",
              "text": "Pipelines",
              "icon": "kubeflow:pipeline-centered"
            },
            {
              "type": "item",
              "link": "/pipeline/#/runs",
              "text": "Runs",
              "icon": "maps:directions-run"
            },
            {
              "type": "item",
              "link": "/pipeline/#/recurringruns",
              "text": "Recurring Runs",
              "icon": "device:access-alarm"
            },
            {
              "type": "item",
              "link": "/pipeline/#/artifacts",
              "text": "Artifacts",
              "icon": "editor:bubble-chart"
            },
            {
              "type": "item",
              "link": "/ray-cluster/",
              "text": "Ray on vSphere",
              "icon": "book"
            },
            {
              "type": "item",
              "link": "/pipeline/#/executions",
              "text": "Executions",
              "icon": "av:play-arrow"
            }
          ],
          "externalLinks": [],
          "quickLinks": [
            {
              "text": "Upload a pipeline",
              "desc": "Pipelines",
              "link": "/pipeline/"
            },
            {
              "text": "View all pipeline runs",
              "desc": "Pipelines",
              "link": "/pipeline/#/runs"
            },
            {
              "text": "Create a new Notebook server",
              "desc": "Notebook Servers",
              "link": "/jupyter/new?namespace=kubeflow"
            },
            {
              "text": "View Katib Experiments",
              "desc": "Katib",
              "link": "/katib/"
            }
          ],
          "documentationItems": [
            {
              "text": "Getting Started with Kubeflow",
              "desc": "Get your machine-learning workflow up and running on Kubeflow",
              "link": "https://www.kubeflow.org/docs/started/getting-started/"
            },
            {
              "text": "MiniKF",
              "desc": "A fast and easy way to deploy Kubeflow locally",
              "link": "https://www.kubeflow.org/docs/distributions/minikf/"
            },
            {
              "text": "Microk8s for Kubeflow",
              "desc": "Quickly get Kubeflow running locally on native hypervisors",
              "link": "https://www.kubeflow.org/docs/distributions/microk8s/kubeflow-on-microk8s/"
            },
            {
              "text": "Kubeflow on GCP",
              "desc": "Running Kubeflow on Kubernetes Engine and Google Cloud Platform",
              "link": "https://www.kubeflow.org/docs/gke/"
            },
            {
              "text": "Kubeflow on AWS",
              "desc": "Running Kubeflow on Elastic Container Service and Amazon Web Services",
              "link": "https://www.kubeflow.org/docs/aws/"
            },
            {
              "text": "Requirements for Kubeflow",
              "desc": "Get more detailed information about using Kubeflow and its components",
              "link": "https://www.kubeflow.org/docs/started/requirements/"
            }
          ]
        }
      #@overlay/match missing_ok=True
      settings: |-
        {
          "DASHBOARD_FORCE_IFRAME": true
        }
EOF
```
</div></details>

次にKubeflowの`PackageInstall`オブジェクトの`annotation`を更新する。
```sh
kubectl patch pkgi --type merge -p "{\"metadata\":{\"annotations\":{\"ext.packaging.carvel.dev/ytt-paths-from-secret-name.0\":\"my-overlay-secret\"}}}" kubeflow -n my-packages
```
これで準備OKなので、kapp-controllerが更新するのを待つ。
待ちきれない人は以下を実行してリコンサイルを促すとよい。
```sh
kubectl patch app --type merge -p "{\"spec\":{\"paused\": true}}" kubeflow -n my-packages
```
リコンサイルが終わると、Kubeflowにリンクが追加され、リンクをクリックすればRayのダッシュボードが見えるはずだ。
istio-ingressgatwayのExternal-IPを参照し、Kubeflowを開く。
![1702861682637.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/2c1fbf94-57c1-1113-b6f5-7237a887cb4b.png)

左サイドバーに`Ray on vSphere`という項目が追加され、Ray Dashboardが参照できるようになった。
これでインフラ担当者としては開発者にKubeflowとRayが統合された環境を渡せる状態となった。

## Jupyter Notebookをデプロイして、Rayクラスタ上でワークロードを実行する
ここからは開発者視点で、Jupyter Notebookをデプロイして、Rayクラスタ上でワークロードを実行してみる。
なお、ワークロードとなるサンプルコードは[こちら](https://qiita.com/ipppppei/items/80d3c81f480ad117bf6b)の記事で使ったものを利用している。
サンプルコードの説明等はそちらの記事を参照してほしい。

Kubeflowのダッシュボードの左サイドバーから`Notebooks`を選択し、`New Notebook`をクリックする。

![1702862107831.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/777c5480-44b9-d3a4-ca02-d60e227d79b9.png)

作成画面では以下のように設定した。
- `Name`：ray-test
- `Image`：kubeflownotebookswg/jupyter-tensorflow-cuda-full:v1.6.0
- `CPU`：2
- `Memory`：8G
- `Data volumes`：10G

数分待つと環境の払い出しが終わって以下のような画面になるので、`CONNECT`をクリックしてJupyter Notebookにアクセスする。
![1702862826336.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/665445cc-3ed6-610d-d295-020ebf4e7def.png)

メニューバーから`File`->`New`->`Notebook`で新規にNotebookを作成し、`ray`CLIをインストールする。
```sh
pip install 'ray[default]'
```
サンプルコードをダウンロードする。
```sh
! wget https://raw.githubusercontent.com/ray-project/ray/master/doc/yarn/example.py
```
なお、サンプルコードではノードを4台要求しているが、今回構築したRayクラスタはHeadとWorkerがそれぞれ1台デプロイされた構成であるため、サンプルコードは待ち状態で動かなくなってしまう。
KubeRayを使った場合、設定次第で[オートスケールすることが可能](https://docs.ray.io/en/latest/cluster/kubernetes/user-guides/configuring-autoscaling.html)だが、今回はオートスケールは目的としていないため、待つノードの台数を2に変更して進める。
```sh
! sed -i "s/wait_for_nodes(4)/wait_for_nodes(2)/g" example.py
```
`ray`CLIを実行する。アドレスに、RayクラスタのHeadの`Service`を指定する。
```sh
! RAY_ADDRESS='http://raycluster-kuberay-head-svc.ray.svc:8265' ray job submit --working-dir . -- python example.py
```
RayクラスタのPod名が表示された。
![1702865312991.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547981/dd899f6f-7a8e-030c-77bb-b1343bc0745c.png)
これによりRayクラスタ上でコードが実行できたことが確認できた。

## まとめ
[vSphere Machine Learning Extension](https://vmware.github.io/vSphere-machine-learning-extension/)のKubeflowとKubeRayを組み合わることで、ML開発者はUIから開発環境(Jupyter Notebook)を簡単にデプロイ出来るようになり、かつ実行環境(Ray)も用意せずに使える世界が実現できる見込みを得た。
開発環境という意味ではVMware（現Broadcom）はTanzu Application Platformもあるが、こちらと将来的に連携するのかとか今後の発展を期待したいところである。