学習は終わった。よし、API提供だ!Deep LearningのServing比較してみた

TensorFlow, Pytorchなどの機械学習/Deep Learning向けのフレームワークが普及し、Pre-Traininedモデルもたくさん出回っているので、自前のAI学習が簡単にできるようになってきました。でも、

- 学習を回してみた後、結局これをどうサービスに組み込めばよいのだっけ？
- どうやってお客様に使ってもらえばいいんだっけ？

ってなることないですか？学習から推論API提供までやってくれるサービスとかあるけれど、クラウド前提だったりして、オンプレが良いのだけど...など。
そんな時に推論APIを簡単に提供するための**Serving**ライブラリを比較してみたいと思います。

これのNTTドコモ R&D アドベントカレンダー2020(控室)の13日目の記事です。
担当はサービスイノベーション部の酒井です。

## これは何?

学習済みモデルを推論用webAPIとして簡単に提供するためのServingライブラリを比較してみました。

- TensorFlow Serving
- Torch Serve
- Triton Inference Server
- OpenVINO Model Server

## Servingライブラリとは?

学習済みの機械学習モデルを用いて、推論機能をREST APIやgRPCなどのAPIとして提供するためのライブラリです。
推論機能を提供するためのサーバを用意し、サーバ上でServingライブラリを起動するだけで以下ができるようになります。

- 推論機能の提供
- モデル管理
  - 複数のモデルを登録、Servingすることができます
  - 複数のGPUカードを搭載しているサーバの際、GPUカードごとにモデルを起動させたり、一部のモデルをCPU上で動作させる事もできます
  - モデル管理もAPI化されている事が多いので、新しいモデルの追加や、古いモデルの削除なども可能です。
- メトリクス
  - リソースの利用状況などもAPI経由で確認することができます

Servingライブラリを使うだけで作ったモデルを直ぐにAPI提供ができるようになる、ぜひ使いこなしたいライブラリです。

## Servingまとめ

各Servingライブラリには、以下のような機能が備わっています。

|| TensorFlow Serving | Torch Serve | Triton Inference Server | Open VINO Model Server |
| :-- | :-- | :-- | :-- | :-- |
| 対応フォーマット | Tensorflow SavedModel | *.pthファイル or *.pthを<br>アーカイバで変換<br>(.marファイル) | Tensorflow graphdef/saved model<br>Tensorrt GraphDef<br>TensorRT Plans<br>Caffe2 NetDef<br>ONNX graph<br>pytorch JIT(.pb) | Tensorflow<br>Caffe<br>ONNX<br>MXNET |
| 対応ハードウェア | CPU, NVIDIA GPU, TPU | CPU, GPU | CPU, GPU |  Intel Movidius Myriad VPUs,<br>GPU and HDDL |
| RESTful API | 〇 | 〇 | 〇 | 〇 |
| gRPC | 〇 | - | 〇 | 〇 |
| クライアントSDK | - | - | 〇 | sample code |
| API側での前処理 | 〇 | 〇 | 〇 | △ |
| API側での後処理 | 〇 | 〇 | 〇 | △ |
| モデル管理API | 〇 | 〇 | 〇 | 〇 | 〇 |
| バージョニング | 〇 | 〇 | 〇 | 〇 |
| 複数モデルでのパイプライン処理/アンサンブル処理 | 〇 | - | 〇 | 〇 |
| バッチ処理によるリソース効率化 | 〇 | 〇 | 〇 | 〇 |
| ログ確認 | 〇 | 〇 | 〇 | 〇 |
| メトリクス | 〇 | 〇 | 〇 | 〇 |
| networkを推論デバイス向けに最適化 | - | - | 〇 | 〇 |
| GPU/CPU間の処理を効率化 | - | - | 〇 | - |

### 前処理/後処理に関する捕捉

前処理を必要とする処理の場合(例えば、画像認識で正規化が必要な場合など)、ServingしているAPIにpostする前にクライアント側で処理を行う方法と、API側で処理をする方法があります。各ServingライブラリでAPI側での前処理を行う方法をまとめます(後処理も同様です)。

- TensorFlow: SavedMolelのexport時に併せて前処理関数を出力するするようです。例えば、tf_kerasの[serving_fnを使って実装する方法](https://qiita.com/cvusk/items/abe68076bbbb87966539#%E7%94%BB%E5%83%8F%E5%88%86%E9%A1%9E)があります。
- Pytorch: [handler](https://pytorch.org/serve/custom_service.html)として前処理/後処理を定義し、アーカイビング時に指定します。画像分類などには[デフォルトのハンドラ](https://pytorch.org/serve/default_handlers.html)が定義されています。
- Triton: 複数のモデルを順番に適用するアンサンブルができるので、前処理、後処理をモデルとして定義し、アンサンブルで実現します。前のバージョンまでは、画像認識用の前処理モデルをbuildするサンプルがgithubで公開されていましたが、執筆時点の最新v2.5では[DALI](https://github.com/triton-inference-server/dali_backend)というライブラリを利用した前処理がメインになっているようです。また、TensorFlowerのsavedModelを読み込む際は、svedModel内で定義された前処理が利用できる模様。
- Open VINO: Model Ensemble機能がpreviewのようなので、Tritonと同じように前処理/後処理のモデルを定義する事も可能になると思われます。また、TensorFlowerのsavedModelを読み込む際は、svedModel内で定義された前処理が利用できる模様。


### バッチ処理によるリソース効率化に関する捕捉

GPUを使った推論の際などは、入力データを一つずつfoward処理するより、複数枚まとめてミニバッチとしてfowardした方が、計算リソースを効率的に利用することができます。
よって、APIに大量のリクエストが一度に来るような状況が発生するような利用状況においては、複数の入力データを一つのミニバッチにまとめて処理する機能があれば、処理が効率化されます。
Tensorflow, Pyotch, Tritonでは、起動時にmax_batch_sizeと、許容する遅延を指定することで、上記のようなバッチ処理を実施する機能が実装されています。

OpenVINOに関しては、バッチとして入力した際にバッチ処理する機能しか確認できませんでした。

### デバイスによる推論の効率化に関する捕捉

- TensorFlow, Pytorch, TritonはNVIDIA GPUを用いた処理の高速化が図れる
- TensorFlowはTPUが使えるという優位点がある
- TritonはNVIDIA GPUでの処理の効率化/Networkの最適化を行ってくれるTensorRTが使えるので、NVIDIA GPUでの推論のパフォーマンスが高くなる可能性がある
- OpenVINOはIntel CPU/GPU向けの処理の最適化を行ってくれるので、Intel CPU/GPUを搭載したマシンでのパフォーマンスが高くなる可能性がある

## 読み込み用のモデルを作ってみる

今回は、ResNet152を対象にし、Tensorflow, Pytorchでモデルを準備します。どちらもimagenetで学習済みのモデルがあるのでそちらを拝借します。
Pytorchについては、デフォルトのhandlerを使うことでAPI側での前処理/後処理が簡単に実装できるようなので、活用します。
他のライブラリに関しては、クライアント側で前処理を行うこととします。

必要なファイルは以下からダウンロードします。

- [index_to_name.json](https://github.com/pytorch/serve/blob/master/examples/image_classifier/resnet_152_batch/index_to_name.json)
- [kitten.jpg](https://github.com/pytorch/serve/blob/master/examples/image_classifier/resnet_152_batch/images/kitten.jpg)

また、作業環境は以下の通りです。

- NVIDIA-driver:450.80.02
- python: 3.6.9
- torch==1.7.0
- torchvision==0.8.1
- tensorflow==2.3.1
- torch-model-archiver==0.2.0

### TensorFlowのsavedModelの作成

TensorFlow Serving, TRTIS, OpenVINOように、TensorFlowベースのsavedModelを作ります。
今回は、Kerasを通して作ってみます。

```python:save_model.py
import json
import tensorflow as tf
model = tf.keras.applications.ResNet152()
tf.saved_model.save(
    model, './tf_savedModel/1'
)

tf.saved_model.save(
    model, './trtis_models/resnet_152/1/model.savedmodel'
)
```

`./tf_savedModel/1`の`1`はバージョンです。

## Triton Inference Server用モデル

上記KerasモデルをベースにTriton用のモデルを作ります。
Triton用のmodel_repositoryも作成します。上記スクリプト内でtrtis_models/resnet_152/配下に必要なモデルは出力されているので、あとは以下のconfigを作成すれば完了です。


```text
:trtis_models/resnet_152/config.pbtxt
name: "resnet_152"
platform: "tensorflow_savedmodel"
max_batch_size: 8
input [
  {
    name: "input_1"
    data_type: TYPE_FP32
    format: FORMAT_NHWC
    dims: [ 224, 224, 3 ]
  }
]
output [
  {
    name: "predictions"
    data_type: TYPE_FP32
    dims: [ 1000 ]
    label_filename: "labels.txt"
  }
]
```

configに書くinput,outputの内容は以下のコマンドで調べています。

```python
import tensorflow as tf
loaded = tf.saved_model.load('./trtis_models/resnet_152/1/model.savedmodel')
print(list(loaded.signatures.keys()))
# ["serving_default"]
infer = loaded.signatures["serving_default"]
print(infer.structured_outputs)
#{'predictions': TensorSpec(shape=(None, 1000), dtype=tf.float32, name='predictions')}
print(infer.structured_input_signature)
#((), {'input_1': TensorSpec(shape=(None, 224, 224, 3), dtype=tf.float32, name='input_1')})
```

### Pytorch用のモデルの作成

Torch Serve用にアーカイブを作成します。Torch serve用のモデルの保存方法は二つあります

- モデルの定義ファイル(.pyファイル内に、`nn.Module`が定義されている)+state_dictを保存した.pthファイルを用意する方法
- TorchScriptを用いて保存した.ptファイルを用意する方法

今回は、[公式サイト](https://github.com/pytorch/serve/tree/master/examples/image_classifier/resnet_152_batch)を参考に後者の手法を採用します。

```python:save_pt_model.py
from torchvision import models
import torch
model = models.resnet152(pretrained=True)
sm = torch.jit.script(model)
sm.save("resnet-152-batch.pt")

input_names = [ "input" ]
output_names = [ "output" ]

input_shape = (3, 224, 224) 
batch_size = 1
dummy_input = torch.randn(batch_size, *input_shape) # ダミーインプット生成
torch.onnx.export(
    model, dummy_input, './openvino_models/resnet152.onnx', \
    verbose=False, input_names=input_names, output_names=output_names
)
```

つぎに、アーカイバを使ってアーカイブを作成します。
pthファイルを使っているときは、アーカイバにモデル定義ファイル(`*.py`)も指定する必要がありますが、今回は.ptを使っているので不要です。

```
torch-model-archiver --model-name resnet-152-batch --version 1.0  --serialized-file openvino_models/resnet-152-batch.pt --extra-files index_to_name.json  --handler image_classifier
```

`--handler image_classifier`が、前処理/後処理を定義しているハンドラーで、独自の前処理/後処理をしたいときは、自前で書く必要があります。

### OpenVINO用モデルファイルの準備

OpenVINO用のモデルはONNXから作ります。前述のpytorchのモデル作成の際に、onnxも一緒に作っているので、そちらを利用します。

```bash
docker run --rm -w  /opt/intel/openvino/deployment_tools/model_optimizer \
    --mount type=bind,source=$PWD/openvino_models/,target=/tmp/models \
    openvino/ubuntu18_dev \
    python3 mo_onnx.py \
        --input_model /tmp/models/resnet152.onnx --output_dir /tmp/models
```

`openvino_models`配下に、resnet152.binなどが生成されます。フォルダ構造を変更しておきます。

```bash
mkdir openvino/resnet152/1
mv openvino/resnet152.xml openvino/resnet152.bin openvino/resnet152.mapping openvino/resnet152/1
```

## 失敗談: OpenVINO用のモデルファイル準備(TensorFlowから)

前記KerasモデルをベースにOpenVINO用のモデルを作ろうとしましたが、エラーが出て進めなくなってしまいました。

```bash
docker run --rm -w -u root /opt/intel/openvino/deployment_tools/model_optimizer \
    --mount type=bind,source=$PWD/tf_savedModel/,target=/tmp/models \
    openvino/ubuntu18_dev \
    python3 mo_tf.py --saved_model_dir /tmp/models/1
```

実行すると、以下のようなエラーが出ます。

```
[ ERROR ]  Exception occurred during running replacer "REPLACEMENT_ID" (<class 'extensions.load.tf.loader.TFLoader'>): Unexpected exception happened during extracting attributes for node Const.
Original exception message: 'ascii' codec can't decode byte 0xcb in position 1: ordinal not in range(128)
```

TensorFlowの2.X系を使っている際に発生することが[あるよう](https://community.intel.com/t5/Intel-Distribution-of-OpenVINO/Original-exception-message-ascii-codec-can-t-decode-byte-0xee-in/td-p/1190202?profile.language=ja)です。

そこで、openvino用に以下のような環境を作って再実行しました。

- tensorflow==1.15
- h5py < 3.0.0

しかし、エラーは変らずでした。tf.kerasのモデルを使っているのが悪いのでしょうか。力及ばすこれ以上先に進めませんでした。

## 起動してみる

各サービングモジュールでAPIを起動してみます。どれもdockerを使うと導入が楽ちんです。

### リクエスト用(OpenVINO/TensorFlow)のjsonを作成する

Tensorflow, Triton, OpenVINOについては、前処理をクライアント側で行う事にしました。
Tensorflow, OpenVINO用の前処理を行ったjsonファイルをあらかじめ作り、リクエスト用に使う事にします。

```create_request.py
import json
import numpy as np
from tensorflow.keras.preprocessing import image
from tensorflow.keras.applications.resnet50 import preprocess_input

# tensorflow用の前処理を作成
img = image.img_to_array(image.load_img('kitten.jpg', target_size=(224, 224)))
X = img[np.newaxis, ...].astype('float32')#kerasはnwhc
keras_payload = dict(instances=preprocess_input(X).tolist())
print(keras_payload.keys())

json.dump(keras_payload, open('./request.json', 'w'))

# openvino用のrequest作成
## pytorch版のresnetを使っているので公式(https://pytorch.org/hub/pytorch_vision_resnet/)のpreprocessに従う
X = (img/255-np.array([0.485, 0.456, 0.406]))/np.array([0.229, 0.224, 0.225])
X = X[np.newaxis].transpose([0, 3, 1, 2]) #pytorchはncwh
pyload = dict(instances=X.tolist()
json.dump(pyload, open('./request_openvino.json', 'w'))

```

Tritonに関しては後述するようにclient SDKを用いるので、作成不要です。

### TensorFLow Serving

以下の通りサーバの起動とリクエストを実行します。

```bash
docker pull tensorflow/serving
docker run -p 8501:8501 -d \
  --mount type=bind,source=$PWD/tf_savedModel/,target=/models/my_model \
  -e MODEL_NAME=my_model -t tensorflow/serving
curl -d @request.json \
    -X POST http://localhost:8501/v1/models/my_model:predict
```

jsonで、imagenetの各クラスに対する確率値が返却されます。一部を抜粋すると、以下のようなものが返却されます。0.676939905が281行目でtabby、次がtiger_cat(日本語ではどちらもトラネコでしょうか)です。無事、猫に分類されている事が分かります。

```json
{
    "predictions": [[
        ...
        1.96341716e-08,
        3.07374791e-07,
        0.676939905,
        0.309618443,
        1.71803072e-06,
        ...
    ]]
}
```

### TorchServe

以下の通りサーバの起動とリクエストを実行します。

```bash
docker run --rm -d --shm-size=1g \
        --gpus=1 \
        --ulimit memlock=-1 \
        --ulimit stack=67108864 \
        -p8080:8080 \
        -p8081:8081 \
        --mount type=bind,source=$PWD/torch_model_store/,target=/tmp/models \
        pytorch/torchserve:0.2.0-cuda10.1-cudnn7-runtime \
        torchserve --model-store=/tmp/models --models resnet_152=resnet-152-batch.mar
curl -X POST http://127.0.0.1:8080/predictions/resnet_152 -T kitten.jpg
```

marファイルを作成する際に指定してclassify_handlerは、入力を画像ファイルのバイト列で受け取れる。base64エンコーディングしたinputも受け取れ機能が、github上では実装が進んでいる模様(2020/12/8現在)。
レスポンスは以下のようになり、猫と認識されている事が分かります。

```json
{
  "tiger_cat": 0.5848351120948792,
  "tabby": 0.37827444076538086,
  "Egyptian_cat": 0.03441951051354408,
  "lynx": 0.0005633491091430187,
  "quilt": 0.00026982976123690605
}
```

### Triton Inference Server

以下の通りサーバを起動します。

```bash
docker run --gpus=1 --rm -p 8000:8000 -p 8001:8001 -p 8002:8002 \
    -v $PWD/trtis_models:/models \
    nvcr.io/nvidia/tritonserver:20.10-py3 \
    tritonserver --model-repository=/models
```

Tritonはclientを使ってのアクセスをメインで想定しているようなので、clientを使ってアクセスします。

```bash
docker pull nvcr.io/nvidia/tritonserver:20.10-py3-sdk
docker run --rm --net=host -it \
    -v $PWD/kitten.jpg:/workspace/images/kitten.jpg \
    nvcr.io/nvidia/tritonserver:20.10-py3-clientsdk \
    python /workspace/install/python/image_client.py -m resnet_152 -c 1000 -s NONE /workspace/images/kitten.jpg
```

こちらも以下のように、無事猫と認識されたようです。

```txt
Request 1, batch size 1
    0.536923 (282) = tiger_cat
    0.264265 (281) = tabby
    0.175551 (285) = Egyptian_cat
    0.008855 (287) = lynx
```

Triton Inference Serverの説明は[こちらの記事](https://qiita.com/dcm_yamaya/items/985a57598d516e77894f)に詳しいので、こちらもぜひご覧ください。

### OpenVINO

以下の通りサーバの起動とリクエストを実行します。

```bash
docker run -d -u $(id -u):$(id -g) \
    -v $(pwd)/openvino_models:/models -p 9000:9000 \
    openvino/model_server:latest \
    --model_path /models/resnet152 \
    --model_name resnet152 --rest_port 9000 --log_level DEBUG --shape auto
curl -d @request_openvino.json     -X POST http://localhost:9000/v1/models/resnet152:predict
```

jsonで、imagenetの各クラスに対するスコア(softmaxをかける前のもの）が返却されます。
最大値となった周辺を抜粋すると、281行目tabbyが最大値、次がtiger_catに分類されていました。

```json
{
    "predictions": [[
        ...
        1.85837758,
        5.70143843,
        14.7201872,
        14.3262253,
        2.59828043,
        ...
    ]]
}
```

## まとめ

以上、Servingの比較と、起動実験でした。各Servingの使いどころは以下かなと思います。

- TensorFlow Serving: 元モデルがTensorFlow or TPU使いたい
- Torch Serve: 元モデルがTorch
- Triton Inference Server: GPUを効率的な処理を行いたい or 色々なフレームワークのモデルを使いたい
- Open VINO Model Server: Intel CPU/GPUで効率的な処理を行いたい or 色々なフレームワークのモデルを使いたい

また、前処理/後処理を自分で設定したい、画像分類や物体検出のような一般的なタスク以外のタスクをやりたいときは、TensorFlow Servingか、Torch Serveが実装が簡単そうに見えました。
次は、各ライブラリでAPI側でのカスタム前処理の設定や、batch処理をした際の処理速度の比較をしてみたい所です。