Clipperを使った機械学習モデルのデプロイ管理


機械学習モデルもdockerで管理するのが好きな人もいると思うので、
nipsで紹介されてたのを動かして確認してみました。
今回はモデルを登録して、api機能をデプロイするところをやってみました。


http://clipper.ai/tutorials/basic_concepts/
https://github.com/ucbrise/clipper

#Clipperクラスタ

Clipperは３つPython,R,PySparkに対応しています。
Clipperクラスタのコアは、クエリフロントエンド、管理フロントエンド、および構成データベースの3つのコンポーネントで構成されています。
<img width="946" alt="スクリーンショット 2018-03-20 15.06.06.png" src="https://qiita-image-store.s3.amazonaws.com/0/64334/29ba50c5-a79a-d568-4990-136fe0cb708a.png">

- クエリフロントエンド： 受信した予測リクエストとスケジュールをリッスンし、それらをデプロイされたモデルにルーティングします。ClipperのREST予測インターフェースを照会すると、フロントエンドに要求が送信されます。

- 管理フロントエンド： Clipperの内部構成状態を管理および更新します。新しいアプリケーションの登録や新しいモデルの展開など、Clipperクラスタの設定を変更するときは、管理フロントエンドのadmin RESTインターフェイスに対してコマンドを発行しています。

- 構成データベース： Clipperの内部構成状態を永続的に保存するために使用されるRedisインスタンス。Clipperの内部構成の変更は、管理フロントエンドからRedisに伝えられ、永続的に保存されます。
クエリフロントエンドは、構成データベースへの変更を監視し、変更が検出されたときにその状態を適切に更新します。



#自分で試した環境

```
cat /etc/os-release
>>>NAME="Ubuntu"
>>>VERSION="16.04.1 LTS (Xenial Xerus)"
>>>ID=ubuntu
>>>ID_LIKE=debian
>>>PRETTY_NAME="Ubuntu 16.04.1 LTS"
>>>VERSION_ID="16.04"
>>>...
pyenv versions
>>>* system (set by /home/user1/.pyenv/version)
>>>  anaconda2-4.4.0
>>>  anaconda2-4.4.0/envs/py27
python -V
>>>Python 2.7.13 :: Continuum Analytics, Inc.
docker -v
>>>Docker version 18.02.0-ce, build fc4de44
```


#Python2.7の環境設定:Ubuntu

<b>python2.7系しかclipperではサポートされていない。</b>

```
git clone https://github.com/yyuu/pyenv.git ~/.pyenv
```

bachrcに設定。

```
#pyenv                                                                                                                                                        
export PYENV_ROOT=$HOME/.pyenv
export PATH=$PYENV_ROOT/bin:$PATH
eval "$(pyenv init -)"
```

```
pyenv install anaconda2-4.4.0
```

bachrcに設定。

```
export PATH="$PYENV_ROOT/versions/anaconda2-4.4.0/bin:$PATH"
```

conda環境を作る。

```
conda create -n py27 python=2.7
```

環境に入る。

```
source activate py27
```

参照
https://qiita.com/miyamotok0105/items/5f26e4ae41f0e35ded16

#clipperの環境設定

```
#clipper
sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates
pip install git+https://github.com/ucbrise/clipper.git@develop#subdirectory=clipper_admin
```


#clipperの基礎を動かしてみる。


```
git clone https://github.com/ucbrise/clipper.git
cd examples/basic_query
python example_client.py
```



```py:example_client.py
from __future__ import print_function
from clipper_admin import ClipperConnection, DockerContainerManager
from clipper_admin.deployers import python as python_deployer
import json
import requests
from datetime import datetime
import time
import numpy as np
import signal
import sys


def predict(addr, x, batch=False):
    url = "http://%s/simple-example/predict" % addr

    if batch:
        req_json = json.dumps({'input_batch': x})
    else:
        req_json = json.dumps({'input': list(x)})

    headers = {'Content-type': 'application/json'}
    start = datetime.now()
    r = requests.post(url, headers=headers, data=req_json)
    end = datetime.now()
    latency = (end - start).total_seconds() * 1000.0
    print("'%s', %f ms" % (r.text, latency))


def feature_sum(xs):
    return [str(sum(x)) for x in xs]


# Stop Clipper on Ctrl-C
def signal_handler(signal, frame):
    print("Stopping Clipper...")
    clipper_conn = ClipperConnection(DockerContainerManager())
    clipper_conn.stop_all()
    sys.exit(0)


if __name__ == '__main__':
    signal.signal(signal.SIGINT, signal_handler)
    clipper_conn = ClipperConnection(DockerContainerManager())
    clipper_conn.start_clipper()
    python_deployer.create_endpoint(clipper_conn, "simple-example", "doubles",
                                    feature_sum)
    time.sleep(2)

    # For batch inputs set this number > 1
    batch_size = 1

    try:
        while True:
            if batch_size > 1:
                predict(
                    clipper_conn.get_query_addr(),
                    [list(np.random.random(200)) for i in range(batch_size)],
                    batch=True)
            else:
                predict(clipper_conn.get_query_addr(), np.random.random(200))
            time.sleep(0.2)
    except Exception as e:
        clipper_conn.stop_all()
```

動いた。

```
python example_client.py 
18-03-21:15:40:06 INFO     [docker_container_manager.py:106] Starting managed Redis instance in Docker
18-03-21:15:40:09 INFO     [clipper_admin.py:114] Clipper is running
18-03-21:15:40:09 INFO     [clipper_admin.py:189] Application simple-example was successfully registered
18-03-21:15:40:09 INFO     [deployer_utils.py:49] Saving function to /tmp/clipper/tmpajrwFc
18-03-21:15:40:13 INFO     [deployer_utils.py:58] Anaconda environment found. Verifying packages.
18-03-21:15:40:24 INFO     [deployer_utils.py:158] Fetching package metadata .........
Solving package specifications: .

18-03-21:15:40:24 INFO     [deployer_utils.py:159] Using Anaconda API: https://api.anaconda.org

18-03-21:15:40:24 INFO     [deployer_utils.py:67] Supplied environment details
18-03-21:15:40:25 INFO     [deployer_utils.py:79] Supplied local modules
18-03-21:15:40:25 INFO     [deployer_utils.py:85] Serialized and supplied predict function
18-03-21:15:40:25 INFO     [python.py:184] Python closure saved
18-03-21:15:40:25 INFO     [clipper_admin.py:391] Building model Docker image with model data from /tmp/clipper/tmpajrwFc
18-03-21:15:40:26 INFO     [clipper_admin.py:395] Pushing model Docker image to simple-example:1
18-03-21:15:40:28 INFO     [docker_container_manager.py:243] Found 0 replicas for simple-example:1. Adding 1
18-03-21:15:40:29 INFO     [clipper_admin.py:569] Successfully registered model simple-example:1
18-03-21:15:40:29 INFO     [clipper_admin.py:487] Done deploying model simple-example:1.
18-03-21:15:40:29 INFO     [clipper_admin.py:232] Model simple-example is now linked to application simple-example
'{"query_id":0,"output":94.6718686566,"default":false}', 17.186000 ms
'{"query_id":1,"output":97.4406443212,"default":false}', 7.134000 ms
'{"query_id":2,"output":95.5675770539,"default":false}', 6.241000 ms
'{"query_id":3,"output":101.933600191,"default":false}', 6.785000 ms
...
```

dockerでバアアアっと立ち上がってる。

```
docker ps -a
CONTAINER ID        IMAGE                                 COMMAND                  CREATED              STATUS                        PORTS                    NAMES
81c72516f3aa        simple-example:1                      "/container/python_c…"   55 seconds ago       Exited (137) 26 seconds ago                            simple-example_1-46743
2e652c31cec7        prom/prometheus:v2.1.0                "/bin/prometheus --c…"   About a minute ago   Exited (0) 26 seconds ago                              metric_frontend-35815
a6d0987a6f74        clipper/frontend-exporter:develop     "python ./front_end_…"   About a minute ago   Exited (137) 15 seconds ago                            query_frontend_exporter-88393
5c1523a4b5ea        clipper/query_frontend:develop        "/clipper/release/sr…"   About a minute ago   Exited (137) 5 seconds ago                             query_frontend-88393
687d4f2e650a        clipper/management_frontend:develop   "/clipper/release/sr…"   About a minute ago   Up About a minute             0.0.0.0:1338->1338/tcp   mgmt_frontend-10174
81d8ec025f20        redis:alpine                          "docker-entrypoint.s…"   About a minute ago   Up About a minute             0.0.0.0:6379->6379/tcp   redis-75736
```

いらないのであれば消しておく。


```
docker system prune
docker rm -f `docker ps -a -q`
```

#アプリケーション


- clipperを動かす。

まだ動いてないなら
clipper_conn.start_clipper()
もう動いてるなら接続する。
clipper_conn.connect()


register_applicationでアプリを登録して、deploy_python_closureでデプロイする。
link_model_to_appでモデル同士を接続できる。

- アプリケーションの登録は ClipperConnection.register_application

Clipperにアプリケーションを登録すると、そのアプリケーションのRESTエンドポイントが作成されます。

```
URL: /<app_name>/predict
Method: POST
Data Params: {"input": <input>}
```

![register_app.png](https://qiita-image-store.s3.amazonaws.com/0/64334/d85324fb-add1-2007-2b61-82e11cbe77c8.png)


- モデルをアプリケーションにリンクするには ClipperConnection.link_model_to_app

モデルを登録したらリンクしてルーティングする。

![link_model.png](https://qiita-image-store.s3.amazonaws.com/0/64334/eb5ae3c2-659c-7b69-11f9-87a4cfa1a8a7.png)




```py:001.py
from clipper_admin import ClipperConnection, DockerContainerManager
clipper_conn = ClipperConnection(DockerContainerManager())

#Start Clipper
clipper_conn.start_clipper()

#Register an application
#a prediction REST endpoint at http://localhost:1337/hello_world/predict
clipper_conn.register_application(name="hello-world", input_type="doubles", default_output="-1.0", slo_micros=100000)

#Inspect Clipper to see the registered apps
print(clipper_conn.get_all_apps())

def feature_sum(xs):
    return [str(sum(x)) for x in xs]


from clipper_admin.deployers import python as python_deployer

python_deployer.deploy_python_closure(clipper_conn, name="sum-model", version=1, input_type="doubles", func=feature_sum)

clipper_conn.link_model_to_app(app_name="hello-world", model_name="sum-model")
```

ローカルから呼び出す。

```
curl -X POST --header "Content-Type:application/json" -d '{"input": [1.1, 2.2, 3.3]}' 127.0.0.1:1337/hello-world/predict
>>>{"query_id":0,"output":6.6,"default":false}
```

外部から呼び出す時はipを変えるだけ。

```
curl -X POST --header "Content-Type:application/json" -d '{"input": [1.1, 2.2, 3.3]}' 150.95.145.xxx:1337/hello-world/predict
{"query_id":1,"output":6.6,"default":false}
```

pythonで呼び出す。

```py:post.py
import requests, json, numpy as np
headers = {"Content-type": "application/json"}
requests.post("http://localhost:1337/hello-world/predict", headers=headers, data=json.dumps({"input": list(np.random.random(10))})).json()
```

なんとなく動かせたので、
今度は違うサンプルか他のものを動かしたいと思います。

#エラー集

AttributeError: 'bool' object has no attribute 'rfind'
→このエラーで何度かハマった。Python3系を使ったり、２系入れてもanacondaのバージョンの問題なのかエラーが出てた。自分のubuntu16環境ではanaconda2-4.4.0を使ったら治った。あとはdockerバージョンも新しくしておいた気がする。
https://github.com/ucbrise/clipper/issues/382