Seldon Core を使って MLモデルを手軽に API化

## はじめに

どうも、最近 kubeflow 気になっているマンです。

kubeflow といえばMLモデル構築、学習、デプロイ、MLOps が可能になる ML 何でもセットです。
そのツール群の中には Serving と呼ばれる、学習したMLモデルを推論APIにするツールがあります。要は作ったモデルをサービスとして提供できるようにするためのものです。

kubeflow の[公式ドキュメント](https://www.kubeflow.org/docs/components/serving/overview/)で紹介されているものは KFServing と Seldon Core の２つです。申し訳程度に [BentoML](https://github.com/bentoml/BentoML) も紹介されていますが比較対象にはされていません。

また kubeflow に限った話ではなく、他にも以下のような Serving ツールがあります。

- [TensorFlow Serving](https://www.tensorflow.org/tfx/guide/serving) 
- [TorchServe](https://pytorch.org/serve/)
- [AI Platform Prediction](https://cloud.google.com/ml-engine/docs/prediction?hl=ja)


またもちろん自分で API 部分を実装して EC2, Fargate, GCE, CloudRun で管理するパターンもあります。

今回は Seldon Core が面白いと思ったのでツールの説明とインストール手順を書いていきます。自分がk8sやその他ツールに不慣れだったのもありますが、利用するまでなかなか手こずったので備忘録も兼ねています。



### 前提


|    |  バージョン  |
| ---- | ---- |
|  GKE マスター  |  1.14.10-gke.36  |
|  Helm  |  v3.2.1  |
|  Seldon Core  |  v1.1.0  |





### [Seldon Core](https://github.com/SeldonIO/seldon-core) とは

<img width="400" alt="core-logo-small.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164543/4e9c6508-e353-c6b2-7652-04517de2c27b.png">



>An open source platform to deploy your machine learning models on Kubernetes at massive scale.
>Seldon core converts your ML models (Tensorflow, Pytorch, H2o, etc.) or language wrappers (Python, Java, etc.) into production REST/GRPC microservices.


とあるように Seldon Core は学習したモデルを kubernetes 上にデプロイし API化するためのツールであることがわかります。もちろん REST/gRPC にも対応し、言語やフレームワークの制限も比較的少ないように思えます。

後述しますが、Seldon Core を使うことで GCS に保存されているMLモデルをいくつかのマニフェストファイルとコマンドで推論API化出来てしまいます。もしちろん pod として動いているのでスケジューリングの対象にもなります。


<img width="700" alt="seldon-core-high-level.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164543/3c2ce615-d873-46ac-99e3-61f446918185.jpeg">


セールスポイントとしては以下を挙げてます。


- 容易なコンテナ化（Containerise）
- 容易なデプロイ（Deploy)
- モニタリング（Monitor）

[inference graphs](https://docs.seldon.io/projects/seldon-core/en/v1.1.0/graph/inference-graph.html?highlight=inference) というものを予め定義してやれば A/B テスト、アンサンブル、多腕バンディットなど実装が面倒なものもやってくれるようになります。

今回は Seldon Core でいうところの Hello world、つまりインストールから推論結果の取得までいきます。


## 手順

- Seldon Core をクラスターにインストール
- Ingress on GKE を有効化
- Ambassador をクラスターにインストール
- SSL証明書を取得し HTTPS を有効にする
- MLモデルを作成
- Seldon Core を使ってAPI化



k8sクラスターは既にインストールされている前提で進めます。また筆者はクラスターとして GKE を用いています。

## Seldon Coreをクラスターにインストール

公式に倣って Helm で必要なものをインストールします。

```sh

kubectl create namespace seldon-system

helm install seldon-core seldon-core-operator \
    --repo https://storage.googleapis.com/seldon-charts \
    --set usageMetrics.enabled=true \
    --namespace seldon-system \
    --set istio.enabled=true
    # You can set ambassador instead with --set ambassador.enabled=true
```


最後のフラグに `--set istio.enabled=true` とあります。これはAPI化されたエンドポイントを叩く際に必要になるためです。ただしコメントアウトで `--set ambassador.enabled=true` ともあります。istioを選択した場合は、istio api gateway を使ってAPIを叩けるように Seldon Core の設定を進めます。ソースコードを見てみると、フラグがセットされないと ambassador が代わりにインストールされることになっています。

ちなみに今回は istio よりも ambassador が都合がいいと考えて以下のコマンドにしています。既に istio が入っている人はもしかしたら istio を使ったほうがいいかもしれません。

```sh

kubectl create namespace seldon-system

helm install seldon-core seldon-core-operator \
    --repo https://storage.googleapis.com/seldon-charts \
    --set usageMetrics.enabled=true \
    --namespace seldon-system \
    --set ambassador.enabled=true
```


## Ingress on GKE を有効化

GKE の中の API を叩くということはインターネットから GKE 内にアクセスをすることになるので、Ingress が必要になります。既に Ingress が有効化されている人は飛ばしてもいいです。

>On GKE, Ingress is implemented using Cloud Load Balancing. When you create an Ingress in your cluster, GKE creates an HTTP(S) load balancer and configures it to route traffic to your application.

とあるように、 Ingress を**作成すると**ロードバランサー(Google Cloud Load Balancing)が作成されます。正確には Ingress controller が GCLB を作成します。

 
```ingress.yaml
---
apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: ml-ingress
  namespace: seldon-system
spec:
  backend:
    serviceName: ml
    servicePort: 8080
```

このマニフェストファイルを apply すると Ingress が作成されます。

```sh

$ kubens seldon-system
$ kubectl apply ingress.yaml

$ kubectl get ingress 
NAME         HOSTS   ADDRESS         PORTS   AGE
ml-ingress   *       xx.xx.xx.xx      80     10s
```

またGCPのコンソールを確認するとロードバランサーも作成されているはずです。


## Ambassador をクラスターにインストール

<img width="400" alt="bb371600-c42e-11e9-9121-e0f7190fd929.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/164543/fa011bcf-82bc-c947-561a-7028a7f2fcdc.png">


先に書いた Ambassador をここでインストールします。Ambassador はマイクロサービス用の APIGateway です。日本語の記事としては[こちら](https://qiita.com/t-sato/items/e4b8bd59cd32cf0ce102)が詳しいと思います。


Ambassador を入れる理由は GKE 内に配置される各 API へのルーティングを楽にするためです。これによって、

```

Internet => LB（Ingress） => Ambassador（pod） => 各API（pod）
```

という流れでトラフィックが流れるようになります。

### Ambassador API Gateway をデプロイする

`Ingress => Ambassador` を実現するために、Ambassador API Gateway をデプロイします。やっていることは[この手順](https://www.getambassador.io/docs/latest/topics/install/install-ambassador-oss/#1-deploying-the-ambassador-api-gateway)と同じです。

これを実行すると Ambassador の deployment とヘルスチェックを一括して行う ambassador-admin がデプロイされます。

```sh

$ kubectl create clusterrolebinding my-cluster-admin-binding --clusterrole=cluster-admin --user=$(gcloud info --format="value(config.account)")

$ kubectl apply -f https://www.getambassador.io/yaml/ambassador/ambassador-crds.yaml

$ kubectl apply -f https://www.getambassador.io/yaml/ambassador/ambassador-rbac.yaml
```

一点注意が必要で、このままだと Ambassador API Gateway が default の namespace にデプロイされます。上記の２つのマニフェストファイルをダウンロードしたところ、その様に記述されていました。

>The ingress and the ambassador service need to run in the same namespace

[という記述](https://www.getambassador.io/docs/latest/topics/running/ambassador-with-gke/#overview-of-steps)があったり、結局は Seldon Core と Ambassador も同じ namespace にあったほうがいいよなという判断もあり、私は今回上記のマニフェストファイルの namespace を 自分で作った `seldon-system` に書き換えてます（というかそれも面倒なので、厳密には kustomize でそうなるようにしています）。

そして以下のファイルを apply します。LBからトラフィックを受け取る NodePort を作成します。

```ambassador.yaml

---
apiVersion: v1
kind: Service
metadata:
  name: ambassador
  namespace: seldon-system
spec:
  type: NodePort
  ports:
   - port: 8080
     targetPort: 8080
  selector:
    service: ambassador
```

### Ambassador と Ingress を接続する

先程作成した Ingress を編集します。

```ingress.yaml
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: ml-ingress
  namespace: seldon-system
spec:
  backend:
    serviceName: ambassador
    servicePort: 8080
```


## SSL証明書を取得しHTTPSを有効にする


- ドメインを習得する
- SSL証明書を習得する
- Ingress に適用する

ドメインは既に取得していることを前提に進めます。またドメインは`example.com`とします。

またGCPでは[マネージドSSL証明書管理サービス](https://cloud.google.com/load-balancing/docs/ssl-certificates?hl=ja)があります。今回はこれを使います。

```certificate.yaml
---
apiVersion: networking.gke.io/v1beta1
kind: ManagedCertificate
metadata:
  name: example-com
  namespace: seldon-system
spec:
  domains:
    - example.com
```

確認してみます。

```sh

$ kubectl get ManagedCertificate
NAME          AGE
example-com   10s
```

次に Ingress にSSL証明書を適用します。これで2度目の修正ですが、 対象の ingress.yaml は同じものです。
annotations として上記で作成した ManagedCertificate を渡します。

```ingress.yaml
---
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: basic-ingress
  namespace: seldon-system
  annotations:
    networking.gke.io/managed-certificates: example-com
spec:
  backend:
    serviceName: ambassador
    servicePort: 8080
```

## MLモデルを作成

やっと土壌が出来ました。

ここからは以下の3つが必要になります。

- GCSバケットに学習済みモデルを配置
- マニフェストファイルを作成
- apply する

まずはモデルを作成します。あくまで例なのでサクッといきます。Iris の分類をするモデルを作成します。


```model.py

from sklearn import datasets
from sklearn.externals import joblib
import xgboost as xgb

iris = datasets.load_iris()
dtrain = xgb.DMatrix(iris.data, label=iris.target)
bst = xgb.train({}, dtrain, 20)

joblib.dump(bst, 'model.joblib')
```

```sh

# use gsutil to upload model from local to GCS bucket.
# it is ok to use it on Notebook with '!'.

gsutil cp model.joblib gs://[DESTINATION_BUCKET_NAME]/[FOLDER]
```


ここでハマりポイント2つをご紹介します。

まずバケットにフォルダを作成せずにそのままモデルを配置すると Seldon Core が読み取ってくれずエラーになります。[ソースコード](https://github.com/SeldonIO/seldon-core/blob/master/python/seldon_core/storage.py)に書いてあり気づきました。テスト的にバケットを作成し、フォルダを作らずにバケット直下に置いて試すという怠惰が産んだハマりなので私に原因がありますが…。

そしてモデル名は `model.joblib` でないといけません。

公式ドキュメントでは

>You only have to upload your model binaries into your preferred object store, in this case we have a trained scikit-learn iris model in a Google bucket:

>gs://seldon-models/sklearn/iris/model.pickle

とあるように pickle 拡張子でバケットにあげてますが、自分の環境では `sample.joblib` とか `model.pickle` だと Seldon Core 側でエラーを投げてきます。 しっかり`model.joblib` でないといけません。そのためモデル自体の命名規則でモデルを管理するのではなく、バケットとその中のフォルダ構成で管理するのが推奨であると予想されます。

ただしよく考えてみたらバケットに直置きなんてことは実際やらないので1つ目のハマりは当たり前と言われればその通りです。2つ目ですが、GCP AI Platform Prediction も同じ仕様だったのでこちらも納得です。そういうものだと思うことにします。

ちなみに中身は joblib だけど拡張子は pickle みたいなパターンは試してません。誰か試したら教えて下さい。


## Seldon Core を使って API化

Seldon Core には [Prepackaged Model Servers](https://docs.seldon.io/projects/seldon-core/en/v1.1.0/servers/overview.html) と呼ばれるバックエンドサーバがあります。メジャーなフレームワークやライブラリに関しては Seldon Core 側で用意してくれているので、自分たちでコンテナ化などの設定をせずともよりスピーディに・簡単にデプロイまでできるようになっています。

sklearn を使っている場合は `implementation: SKLEARN_SERVER` のように記述してやれば バケット内モデルを pod にぶち上げて API化してくれます。

sklearn 以外も XGBoost, Tensorflow, MLflow が Prepackaged Model Servers として対応しています。


```seldon.yaml

---
apiVersion: machinelearning.seldon.io/v1
kind: SeldonDeployment
metadata:
  name: sklearn-test
spec:
  name: sklearn-test
  predictors:
  - graph:
      children: []
      implementation: SKLEARN_SERVER
      modelUri: gs://[DESTINATION_BUCKET_NAME]/[FOLDER]/model.joblib
      name: classifier
    name: default
    replicas: 1
```

apply します。

```sh

$ kubectl apply seldon.yaml

$ kubectl get deployment,pod,svc

NAME                                                      READY   UP-TO-DATE   AVAILABLE   AGE
deployment.extensions/seldon-controller-manager           1/1     1            1           1m
deployment.extensions/sklearn-test-default-0-classifier   1/1     1            1           1m

NAME                                                     READY   STATUS    RESTARTS   AGE
pod/seldon-controller-manager-576464f779-vmd27           1/1     Running   0          1m
pod/sklearn-test-default-0-classifier-85457bd68b-g7jgc   2/2     Running   0          1m

NAME                                      TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)             AGE
service/seldon-webhook-service            ClusterIP   xx.xx.xx.xx    <none>        443/TCP             1m
service/sklearn-test-default              ClusterIP   yy.yy.yy.yy    <none>    8000/TCP,5001/TCP   1m
service/sklearn-test-default-classifier   ClusterIP   zz.zz.zz.zz    <none>        9000/TCP            1m
```

API は `http://<ingress_url>/seldon/[model-namespace]/[seldon-model-name]/api/v1.0/predictions` で表現されます。

curl を使って json 形式で推論して欲しい内容を投げてみます。結果が返ってきました！

```sh

$ curl -X POST https://example.com/seldon/seldon-system/sklearn-test/api/v1.0/predictions \
       -H 'Content-Type: application/json' \
       -d '{ "data": { "ndarray": [[1,2,3,4]]} }' | json_pp

% Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   112  100    75  100    37    750    370 --:--:-- --:--:-- --:--:--  1120
{
   "data" : {
      "ndarray" : [
         [
            0,
            0.1,
            0.9
         ]
      ],
      "names" : [
         "t:0",
         "t:1",
         "t:2"
      ]
   },
   "meta" : {}
}
```


## 最後に

冒頭で説明した通り、Kubernetes 上に Serving するためのツールはたくさんあります。

AI Platform Prediction は GCP のマネージドサービスなのでかなり楽に API化とその管理が出来ます。しかし同様にそれなりの制約もあります。バッチ推論とオンライン推論の2つのサービスを備えていますが、前者に関してはホストできるモデルの大きさが500MB以下であることと TensorFlow で作成されたモデルであることが条件としてあります。また一部地域でしかまだ使えないなどの制限もあります。

KFServing も良さげだなと思ってますが、istio と Knative を使うというのがどうしてもハードルとして高くまだ挑戦出来ていません…。

また組織によっては Serving ツールをわざわざ利用せずとも自分たちで推論器をつくって Flask などで簡単なアプリにして管理するところも多いのではないでしょうか。MLOps まで踏み込まないようなものであればそちらのほうがコスパはいい気もします。

今回は Hello World で止まりましたが Seldon Core の本領は Containerise だけではなく継続的な MLOps のためのツールが沢山入っていることにもあります。また Prometheus や Grafana との連携でロギングも簡単にできるとのことです（試してない）。