はじめに
以前、 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 を低コストで動かすことができました
大規模で大量リクエストの場合は考慮が必要ですが、
リクエスト数が少ない場合はかなり有用ですね