1
Help us understand the problem. What are the problem?

posted at

updated at

Organization

TensorFlow を低コストのサーバレス(AWS SAM)で実行する

はじめに

以前、 SAM を使って TensorFlow をサーバレスで実行する方法について記事を書きました

ざっくり言うと、超低コストで AI を動かそう、と言うことです

しかし、その時から SAM がバージョンアップして、 sam init のときの質問などが変わってしまいました

また、最終的に出来上がったコードを GitHub に上げていなかったため、今どうすればいいのか少し迷子になりました

というわけで、改めて TensorFlow を SAM でビルド、デプロイする方法とコードをまとめておきます

実装コードはこちら

今回は sam init ではなく、このリポジトリーのコードを使ってデプロイする手順を紹介します

Lambda と SAM の解説は過去の自分に任せます

必要な環境

  • asdf
    私はいつも asdf を使っているので、 asdf で言語やツールを入れています
    もちろん、 asdf を使わずにインストールしても問題ありません
    Windows だと使えませんしね

  • docker
    TensorFlow の実行環境をコンテナで作るため、 Docker が必要です
    Docker Desktop が有償化されたため、私は Rancher Desktop を使っています

  • curl
    API をテストするのに使います

セットアップ

リポジトリーをクローンしてきて、クローン先のディレクトリーに移動します

git clone https://github.com/RyoWakabayashi/tensorflow-lambda.git
cd tensorflow-lambda

asdf を使う場合は各種プラグインを追加してインストールしてください

asdf plugin-add aws-sam-cli \
  ; asdf plugin-add direnv \
  ; asdf plugin-add jq \
  ; asdf plugin-add nodejs \
  ; asdf plugin-add python \
  ; asdf plugin-add yarn
asdf install

モデルのダウンロード

TensorFlow (Keras) で実行するモデルファイルをダウンロードします

python download_models.py

今回は MobileNetv2 を使っています

この辺りはこちらの記事を参考にしています

download_models.py

# coding: utf-8

import os

from tensorflow.keras.applications.mobilenet_v2 import MobileNetV2

os.makedirs("sam/classifier/models", exist_ok=True)

model = MobileNetV2(weights="imagenet")
model.save("sam/classifier/models/model.h5", include_optimizer=False)

学習済モデルはコンテナ実行中にダウンロードすると初回起動が遅くなるので、
必ずビルド時にダウンロードするようにコンテナを実装しましょう

ビルド

sam ディレクトリーに移動します
SAM 関連の定義は sam ディレクトリー配下に置いています
ルートディレクトリーは主に pre-commit などの設定ファイルを置いています

cd sam

Docker が起動していることを確認してから、ビルドを実行します

$ sam build
Building codeuri: ...
...
Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Validate SAM template: sam validate
[*] Invoke Function: sam local invoke
[*] Test Function in the Cloud: sam sync --stack-name {stack-name} --watch
[*] Deploy: sam deploy --guided

Docker コンテナのビルドが開始されます

しばらく待つと、 Build Succeeded と表示されます

コンテナの内容

SAM の定義は以下のようになっています

sam/template.yml

...
Globals:
  Function:
    Timeout: 120
    MemorySize: 1000 # 必要な量に調整する

Resources:
  RestApi:
    Type: AWS::Serverless::Api
    Name: tensorflow-sample-api
    Properties:
      StageName: v1
      BinaryMediaTypes:
        - image~1jpeg # バイナリの JPG 画像を受け付けるようにする

  TensorflowSampleFunction:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image # 関数を Docker コンテナで定義する
      FunctionName: tensorflow-sample
      Events:
        Inference:
          Type: Api
          Properties:
            RestApiId: !Ref RestApi
            Path: /predictions
            Method: post
    Metadata:
      Dockerfile: Dockerfile
      DockerContext: ./classifier

Outputs:
  Api: # デプロイ実行後、 API の URL を表示する
    Description: "API Gateway endpoint URL for v1 stage for Tensorflow sample function"
    Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/v1/predictions"

PackageType: Image が肝ですね
Metadata で Docker コンテナの定義先を指定しています

sam/classifier/Dockerfile

FROM tensorflow/tensorflow:2.9.1

ENV PYTHONUNBUFFERED=TRUE
ENV PYTHONDONTWRITEBYTECODE=TRUE

# awslambdaric を実行するスクリプト
COPY entry.sh "/entry.sh"
RUN chmod +x /entry.sh

# hadolint ignore=DL3013
RUN pip install --no-cache-dir --upgrade pip

# モデルファイルのコピー
RUN mkdir -p /opt/ml
COPY models /opt/ml/model
RUN chmod -R +r /opt/ml/model

WORKDIR /work

COPY ./requirements.txt /work/

# hadolint ignore=DL3006
RUN pip install --no-cache-dir --requirement requirements.txt

# 実行コードのコピー
COPY ./prg /work/prg
WORKDIR /work/prg
RUN chmod +x /work/prg/lambda.py
ENV PATH="/work/prg:${PATH}"

# awslambdaric を実行する
ENTRYPOINT [ "/bin/bash", "/entry.sh" ]

CMD ["lambda.lambda_handler"]

entry.sh は awslambdaric を使って Lambda サービスと実行コードのインタフェースの役割をします
これが動いていないと、コードが実行できません

sam/classifier/entry.sh

#!/bin/bash

exec /usr/local/bin/python -m awslambdaric "$1"

実行コードは以下のようにしています

sam/classifier/prg/lambda.py

# coding: utf-8

"""
メインモジュール
"""

import base64
import json
import logging
import traceback
from datetime import datetime
from io import BytesIO

import tensorflow as tf
from PIL import Image
from tensorflow.keras.applications.mobilenet_v2 import (
    decode_predictions,
    preprocess_input,
)
from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing.image import img_to_array

logging.basicConfig()
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# モデルのロード(起動時のみ実行される)
MODEL_PATH = "/opt/ml/model/model.h5"
model = load_model(MODEL_PATH, compile=False)


def lambda_handler(event, context):
    """画像分類応答"""

    try:

        begin_time = datetime.now()

        # API Gateway からはBase64になってくるため、デコードする
        img = Image.open(BytesIO(base64.b64decode(event["body"])))

        # MobileNetV2 用のサイズに変換する
        img = img.resize((224, 224))
        img = img_to_array(img)
        img = img[tf.newaxis, ...]
        img = preprocess_input(img)

        predictions = model.predict(img)
        predictions = decode_predictions(predictions, top=3)

        # 実行結果をレスポンス(JSON)用に整形する
        predictions = list(
            map(
                lambda prediction: {
                    "class": prediction[1],
                    "score": float(prediction[2]),
                },
                predictions[0],
            )
        )

        # 一定スコア以下の推論結果は排除する
        predictions = list(
            filter(
                lambda prediction: prediction["score"] > 0.3,
                predictions,
            )
        )

        logger.info("Elapsed time: %s", datetime.now() - begin_time)

        return {
            "statusCode": 200,
            "body": json.dumps({"success": True, "predictions": predictions}),
        }

    except Exception:  # pylint: disable=broad-except
        traceback.print_exc()
        return {
            "statusCode": 500,
            "body": json.dumps({"success": False}),
        }

ローカル実行

デプロイ前にローカル実行します

$ sam local start-api
Mounting TensorflowSampleFunction at http://127.0.0.1:3000/predictions [POST]
...
2022-08-01 16:38:42  * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)

ローカルの 3000 番ポートで API が起動します

別のターミナルを起動時、ローカルの API に対してリクエストを投げます

@imgs/sample.jpg に部分は JPG であれば何でも良いです

$ curl -XPOST \
  http://127.0.0.1:3000/predictions \
  -H "Content-Type: image/jpeg" \
  --data-binary @imgs/sample.jpg | jq
{
  "success": true,
  "predictions": [
    {
      "class": "laptop",
      "score": 0.45820939540863037
    },
    {
      "class": "notebook",
      "score": 0.39051398634910583
    }
  ]
}

うまく動いていれば、 JSON が返ってきます

デプロイ

初回デプロイ時はコンテナ用のリポジトリーなどの設定を作るため、ガイド付きで実行します

sam deploy --guided

基本的にデフォルトのまま答えていけば良いです

TensorflowSampleFunction may not have authorization defined, Is this okay? [y/N]: だけは y を入力してください
今回はサンプルなので、認証を入れていません
本番運用時は API に認証を入れるようにしてください

しばらくすると、以下のように API の URL が表示され、デプロイが正常終了します

Value の some-endpoint になっているところが環境毎に違う値になります

...
-------------------------------------------------------------------------------------------------------------------------------
Outputs                                                                                                                       
-------------------------------------------------------------------------------------------------------------------------------
Key                 Api                                                                                                       
Description         API Gateway endpoint URL for v1 stage for Tensorflow sample function                                      
Value               https://some-endpoint.execute-api.ap-northeast-1.amazonaws.com/v1/predictions                                
-------------------------------------------------------------------------------------------------------------------------------

設定ファイルに設定を保存したので、2回目以降は --guided を付けずに実行できます

テスト

デプロイ後に表示された URL に対してリクエストを投げ、テストします

$ curl -XPOST \
  https://some-endpoint.execute-api.ap-northeast-1.amazonaws.com/v1/predictions \
  -H "Content-Type: image/jpeg" \
  --data-binary @imgs/sample.jpg | jq
{
  "success": true,
  "predictions": [
    {
      "class": "laptop",
      "score": 0.45820939540863037
    },
    {
      "class": "notebook",
      "score": 0.39051398634910583
    }
  ]
}

実行ログを確認します

$ sam logs --stack-name tensorflow-sample
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:29.240000 OpenBLAS WARNING - could not determine the L2 cache size on this system, assuming 256k
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:31.837000 2022-08-01 08:00:31.837143: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 FMA
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:31.837000 To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:32.964000 START RequestId: 003de742-9b67-4d66-8dca-a5ba9aefed1a Version: $LATEST
1/1 [==============================] - 2s 2s/stepcf2 2022-08-01T08:00:34.687000 1/1 [==============================] - ETA: 0s
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:34.688000 Downloading data from https://storage.googleapis.com/download.tensorflow.org/data/imagenet_class_index.json
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:34.771000 8192/35363 [=====>........................] - ETA:35363/35363 [==============================] - 0s 0us/step
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:34.772000 [INFO]  2022-08-01T08:00:34.772Z        003de742-9b67-4d66-8dca-a5ba9aefed1a      Elapsed time: 0:00:01.795632
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:34.774000 END RequestId: 003de742-9b67-4d66-8dca-a5ba9aefed1a
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:34.774000 REPORT RequestId: 003de742-9b67-4d66-8dca-a5ba9aefed1a    Duration: 1809.27 ms    Billed Duration: 7092 ms        Memory Size: 1000 MB    Max Memory Used: 539 MB Init Duration: 5282.14 ms
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:36.518000 START RequestId: 405baf63-5782-4120-bdaa-9da3858ee1e8 Version: $LATEST
1/1 [==============================] - 0s 97ms/step2 2022-08-01T08:00:36.702000 1/1 [==============================] - ETA: 0s
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:36.703000 [INFO]  2022-08-01T08:00:36.703Z        405baf63-5782-4120-bdaa-9da3858ee1e8      Elapsed time: 0:00:00.175178
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:36.704000 END RequestId: 405baf63-5782-4120-bdaa-9da3858ee1e8
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:36.704000 REPORT RequestId: 405baf63-5782-4120-bdaa-9da3858ee1e8    Duration: 180.51 ms     Billed Duration: 181 ms Memory Size: 1000 MB    Max Memory Used: 544 MB
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:37.918000 START RequestId: 84105e41-83c1-47aa-a0e9-c211c5dde432 Version: $LATEST
1/1 [==============================] - 0s 133ms/step 2022-08-01T08:00:38.138000 1/1 [==============================] - ETA: 0s
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:38.139000 [INFO]  2022-08-01T08:00:38.139Z        84105e41-83c1-47aa-a0e9-c211c5dde432      Elapsed time: 0:00:00.210684
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:38.140000 END RequestId: 84105e41-83c1-47aa-a0e9-c211c5dde432
2022/08/01/[$LATEST]88c82f652d364d80b86a25b4689d1cf2 2022-08-01T08:00:38.140000 REPORT RequestId: 84105e41-83c1-47aa-a0e9-c211c5dde432    Duration: 216.40 ms     Billed Duration: 217 ms Memory Size: 1000 MB    Max Memory Used: 559 MB

3回実行すると、 Billed Duration が以下のようになっています

  • 7092 ms
  • 181 ms
  • 217 ms

初回だけはモデルのロードに時間がかかってしまいますが、2回目以降は1秒未満で実行できていることが分かります
また、メモリ使用量は以下のようになっています

  • 539 MB
  • 544 MB
  • 559 MB

メモリは 1GB あれば十分そうです

費用

API Gateway

API Gateway の料金表はこちら

100万リクエスト あたり 4.25 USD

1万回りクエストの場合、 0.04 USD しかかかりません

ただし、リクエスト 100 万件までは無料利用枠なので実質無料になります

Lambda

Lambda の料金表はこちら

Lambda は割り当てたメモリ量と実行時間で課金されます

GB-秒あたり 0.0000166667USD となっているので、

今回の関数を 1 万回実行した場合、

1 GB * 7 sec * 10,000 回 * 0.0000166667 USD = 1.167 USD

となります(厳密な 1GB ではありませんが)

ただし、 Lambda には 月当たり 100万リクエストと 40万 GB-秒 の無料利用枠があります

つまり、月に 1 万回程度しか実行しない場合は実質無料になります

ECR

ECR の料金表はこちら

GB/月あたり 0.10USD

今回デプロイしたイメージは 467.98 GB でした

従っておよそ1月 0.5 USD になります

しかし、 ECR にも無料利用枠があり、 月々 500 MB までは無料です

と言うことで、こちらも実質無料になります

ただし、複数世代リポジトリーに保持していると 500 MB を超えてしまうため、
古いイメージは適宜削除しましょう

まとめ

SAM でサーバレス構成を作ることによって、 TensorFlow を低コストで動かすことができました

大規模で大量リクエストの場合は考慮が必要ですが、
リクエスト数が少ない場合はかなり有用ですね

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
1
Help us understand the problem. What are the problem?