EC2 Inf1インスタンスを使ってみた

## はじめに

低コストで高性能な推論が可能なEC2 Inf1インスタンスを使ってみました。
これまでElastic Inferenceは利用したことがあるので、Elastic Inferenceと比較してみました。

### Inf1インスタンスとは

AWSのページでは以下のように紹介されています。
> 同等の現行世代の GPU ベースの Amazon EC2 インスタンスと比較して、スループットが最大 2.3 倍高く、推論あたりのコストが最大 70% 低くなっています。Inf1 インスタンスは機械学習推論アプリケーションをサポートするため、1 から構築されました。同社は、AWS が設計開発した高機能の機械学習推論チップである AWS Inferentia チップを最大 16 個搭載しています。

また、多くのAmazonサービスでもInf1インスタンスが利用されています。

## 今回実施すること

* Inf1インスタンスを用いて、ECS上に推論サーバを構築する
* Inf1インスタンスとElastic Inferenceを比較する

## 利用するモデル

モデルとして、物体検出モデルのYOLOを利用します。
Neuron SDK(Inf1で利用するSDK)の[チュートリアル](https://awsdocs-neuron.readthedocs-hosted.com/en/latest/neuron-guide/neuron-frameworks/tensorflow-neuron/tutorials/index.html#tensorflow-tutorials)にある[YOLOv4](https://github.com/aws/aws-neuron-sdk/blob/master/src/examples/tensorflow/yolo_v4_demo/evaluate.ipynb)を利用します。

## モデルのコンパイル

Inf1の推論チップを活用するために、モデルをコンパイルする必要があります。

### 環境

モデルをコンパイルするためにNeuron SDKをインストールする必要があります。
AWS Deep Learning AMI(DLAMI)には、すでにNeuron SDKがインストールされたAMIが提供されています。
今回はDLAMIを利用します。DLAMI IDは以下のコマンドで取得できます。

```sh
aws ec2 describe-images --region ap-northeast-1 --owners amazon \
--filters 'Name=name,Values=Deep Learning AMI (Ubuntu 18.04) Version ??.?' 'Name=state,Values=available' \
--query 'reverse(sort_by(Images, &CreationDate))[:1].ImageId' --output text
```

### コンパイル

YOLOv4のチュートリアルのコードを利用します。以下のコードでは、Neuronランタイムのオーバーヘッドを減らすために、`no_fuse_ops`と`minimum_segment_size`を指定しています。

```python
import shutil

import tensorflow as tf
import tensorflow.neuron as tfn


def no_fuse_condition(op):
    return any(op.name.startswith(pat) for pat in ['reshape', 'lambda_1/Cast', 'lambda_2/Cast', 'lambda_3/Cast'])

model_path = "<your_model_path>"
new_model_path = "<new_model_path>"

with tf.Session(graph=tf.Graph()) as sess:
    tf.saved_model.loader.load(sess, ['serve'], model_path)
    no_fuse_ops = [op.name for op in sess.graph.get_operations() if no_fuse_condition(op)]

shutil.rmtree(new_model_path, ignore_errors=True)
tfn.saved_model.compile(
    model_path, 
    new_model_path,
    no_fuse_ops=no_fuse_ops,
    minimum_segment_size=100,
    batch_size=1,
    dynamic_batch_size=True,
)
```

コンパイルするだけであれば、以下のコードだけで大丈夫です。`new_model_path`で指定したパスに、コンパイルされたモデルが生成されます。

```python
import tensorflow.neuron as tfn

model_path = "<your_model_path>"
new_model_path = "<new_model_path>"

tfn.saved_model.compile(model_path, new_model_path)
```

## ECS環境の構築

### Docker Imageの作成

Base Imageは、AWSから提供されている[Deep Learning Containers Images](https://github.com/aws/deep-learning-containers/blob/master/available_images.md)を用います。

```Dockerfile
FROM 763104351884.dkr.ecr.ap-northeast-1.amazonaws.com/tensorflow-inference-neuron:1.15.5-neuron-py37-sdk1.19.0-ubuntu18.04

ENV MODEL_CONFIG_FILE /models/models.config

COPY models/yolov4/1 /models/yolov4/1
COPY models.config /models/models.config
COPY entrypoint.sh /usr/bin/entrypoint.sh

ENTRYPOINT ["/usr/bin/entrypoint.sh"]
```


```models.config
model_config_list: {
  config: {
    name: "yolov4",
    base_path: "/models/yolov4",
    model_platform: "tensorflow",
  }
}
```

`tensorflow_model_server`コマンドではなく、`tensorflow_model_server_neuron`コマンドを利用してサーバを起動させます。
```entrypoint.sh
#!/bin/bash

tensorflow_model_server_neuron --port 8500 --model_config_file=${MODEL_CONFIG_FILE}
```

### EC2 AMI

AWSでは、ECS Optimized Inferentia AMIが提供されています。必要なNeuronパッケージがすべて組み込まれています。そのため、ECS Optimized Inferentia AMIを利用します。
以下のコマンドで、AMI IDを取得できます。

```sh
aws ssm get-parameters --names /aws/service/ecs/optimized-ami/amazon-linux-2/inf/recommended
```

### タスク定義

タスク定義では、`linuxParameters`パラメータに、Neuron Deviceへのパスを指定する必要があります。

```json
[
    {
        "name": "inf1",
        "image": "${image}:${tag}",
        "essential": true,
        "portMappings": [
            {
                "protocol": "tcp",
                "hostPort": 8500,
                "containerPort": 8500
            }
        ],
        "linuxParameters": {
            "devices": [
                {
                    "containerPath": "/dev/neuron0",
                    "hostPath": "/dev/neuron0",
                    "permissions": [
                        "read",
                        "write"
                    ]
                }
            ],
            "capabilities": {
                "add": [
                    "IPC_LOCK"
                ]
            }
        },
        "logConfiguration": {
            "logDriver": "awslogs",
            "options": {
                "awslogs-region": "ap-northeast-1",
                "awslogs-group": "${log_group}"
            }
        }
    }
]
```

### その他

あとは、普段のECSでサービスを作成する場合と同様です。
[公式ドキュメント](https://awsdocs-neuron.readthedocs-hosted.com/en/latest/neuron-deploy/dlc-then-ecs-devflow.html)にもInf1インスタンスをECSで動かすやり方が記載されています。

## Elastic Inferenceとの比較

Inf1インスタンスとElastic Inferenceの比較評価を行います。

### 内容

Inf1インスタンスは、`inf1.xlarge`を使用します。
Elastic Inferenceは、`c5.xlarge` + `eia1.medium`の組み合わせを使用します。
画像1000枚の処理が完了するまでの時間を比較します。

### 結果

今回の検証では、Inf1インスタンスの方が、高速に処理できることがわかりました。また、料金もInf1インスタンスの方が安く済みます。

| | 1枚あたりの平均処理時間(秒) | 1枚あたりの最大理時間(秒) | 1枚あたりの最小処理時間(秒) | 合計処理時間(秒) | 料金(USD/時間) |
| --- | --- | --- | --- | --- | --- |
|Inf1インスタンス(inf1.xlarge)| 0.165 | 0.198 | 0.141 | 165.263| 0.308 |
|Elastic inference(c5.xlarge + eia.medium) | 0.197 | 0.229 | 0.188 | 197.051 | 0.434(0.214+0.220)|


## 終わりに

Inf1インスタンスをECSで動かしてみました。またElastic Inferenceとの比較を行いました。
処理速度やコスト改善を考えている方は、一度Inf1インスタンスを検討してみてはいかがでしょうか

## 参考

* [Available Deep Learning Containers Images](https://github.com/aws/deep-learning-containers/blob/master/available_images.md)
* [Amazon ECS の推論ワークロードの操作](https://docs.aws.amazon.com/ja_jp/AmazonECS/latest/developerguide/ecs-inference.html)
* [Evaluate YOLO v4 on Inferentia](https://github.com/aws/aws-neuron-sdk/blob/master/src/examples/tensorflow/yolo_v4_demo/evaluate.ipynb)
* [Neuron SDKドキュメント](https://awsdocs-neuron.readthedocs-hosted.com/en/latest/neuron-intro/get-started.html)