Amazon SageMaker でモデルをデプロイしてみた

1. はじめに

こちらは「Applibot Advent Calendar 2022」と「AI/ML on AWS Advent Calendar 2022」の17日目の記事になります.

アプリボットで最も新しく、最も勢いのある事業部、DX事業部でエンジニアをしている新本です。
DX事業部は製造業の生産性向上に貢献するため、ITを用いたアプローチで課題解決や業務フロー改善を目指すことを目的として、2021年10月に立ち上げられたばかりの部署となります。
しかし、製造業での知見がまだまだ少ないため「素早く作って素早く当てる」をモットーに、徹底したマーケットインでサービス開発を行ってきました。
本記事では、開発→フィードバックのサイクルを最大化するべく色々模索してきた中で、私が担当した SageMakerを使ったAIモデルのデプロイをご紹介します。

目次

2. 対象読者

  • SageMaker を使ったモデルデプロイの実装を知りたい方
  • 作成済みのモデルをサッとデプロイしたい方

3. モデルのデプロイにおける課題

AIのモデルをデプロイする際の考慮事項は、シンプルなWebサービスと比較すると多いです。

  • スループット、レイテンシー(SLA)
  • 予算、モニタリング(コスト)
  • GPUかCPUか、リソースはどう分配するか(インフラ)
  • データ収集頻度、ペイロードサイズ(データ)
  • サイズ、ランタイム、モデルの更新(モデル)

先述した通り、「素早く作って素早く当てる」というモットーに動いている我々は、とにかく早くお客さんに触って頂きフィードバックが欲しいので、「作成したモデルを最速でデプロイせねば」とヤキモキし、最適解を模索していました。
調査する中で「マネージドサービスに乗っかればいいんじゃない?検証すぐできそうだから、とりあえず使ってみよう。」ということになり、SageMaker でのデプロイを検証することにしました。

4. 検証結果

検証結果を一言でまとめると「デプロイから削除までが SageMaker上のJupyter Notebookで完結するので早くて楽だった」です(雑)。Jupyter Noetebook上で完結できるのは大きなメリットだと考えています。

  • 計算リソース変更など諸々の変更がコードベースでできて良い
  • リソースの削除もできる
  • 属人性の排除

立ち上げまもない事業部で、リソースが限られる中、SageMaker に乗っかるのはとてもアリだと思えました。今回はモデル作成は colab を使ってやっていましたが、学習データ管理、モデルのバージョン管理、バージョンの性能評価を考えると、丸ごと SageMaker に乗っかっても良さそうと思えるほど、うまく管理してくれそうでした。機械学習プロジェクトのPoCにおいて、もっともコストがかかるのは人件費だと思うので、総費用を考えると これがベストなのではとも思えてきました。

5. 実装

(※以下のコードは計算リソースにGPUを使ったリアルタイム推論でのデプロイを想定しています)
基本的な流れは、モデル(前処理も含む)をパッケージング → s3 → デプロイ → エンドポイント作成 です。

5.1. コンテナイメージの作成→ECRに登録

パッケージングは Docker で行います。image は AWS が管理・公開しているコンテナイメージを用います。このコンテナイメージを使うことにより、以下のメリットが享受できます。

  • ライブラリが予めある程度入っている(特に GPU 対応コンテナの場合は CUDA 周りのインストールが不要)
  • AWS の便利機能が使える(S3とのやり取りなど、別途 toolkit をインストールするのも可
  • セキュリティに問題があった場合 AWS が update してくれる(が、ベースイメージとしてカスタマイズする場合は、再度ビルドする必要がある)

Base Container Image の URI を取得

container_image_uri = sagemaker.image_uris.retrieve(
    "pytorch",  # PyTorch のマネージドコンテナを利用
    sagemaker.session.Session().boto_region_name, # ECR のリージョンを指定
    version='1.12', # PyTorch のバージョンを指定
    instance_type = 'ml.g5.xlarge', # インスタンスタイプを指定
    image_scope = 'inference' # 推論コンテナを指定
)

print(container_image_uri)
# >> 763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-inference:1.12-gpu-py38

dockerfile の作成

dockerfile_txt = f'''FROM {container_image_uri}

RUN apt update && apt install -y poppler-utils && pip install einops kornia yacs pdf2image
'''
print(dockerfile_txt)
with open('./docker/Dockerfile','wt') as f:
    f.write(dockerfile_txt)

コンテナビルド

ecr_repository = 'techbot-sagemaker-inference'
!aws ecr get-login-password --region ap-northeast-1 | docker login --username AWS --password-stdin $container_image_uri
%cd docker
!docker build -t {ecr_repository} .
%cd ../

ECR にプッシュ

# boto3の機能を使ってリポジトリ名に必要な情報を取得する
account_id = boto3.client('sts').get_caller_identity().get('Account')
region = boto3.session.Session().region_name
tag = ':latest'

image_uri = f'{account_id}.dkr.ecr.{region}.amazonaws.com/{ecr_repository+tag}'

!$(aws ecr get-login --region $region --registry-ids $account_id --no-include-email)

# リポジトリの作成
# すでにある場合はこのコマンドは必要ない
!aws ecr create-repository --repository-name $ecr_repository

!docker tag {ecr_repository + tag} $image_uri
!docker push $image_uri

print(f'コンテナは {image_uri} へ登録されています。')
# >> コンテナは XXXXXXXXXXXX.dkr.ecr.ap-northeast-1.amazonaws.com/techbot-sagemaker-inference:latest へ登録されています

5.2. 推論コードの作成

AWS のコンテナイメージの場合や toolkit を利用していると、コンテナを起動したあと、外部から前処理のコードや、推論コードを持ってくる仕組みがあります。今回使用したモデルは画像解析するモデルで、画像の前処理をどこで実装するかを一番気にしていたポイントでしたが、ここも内包してくれました。以下を設定することができます。

  • model_fn
    モデルをロードするメソッドで、ロードしたモデルを返す。return したモデルは predict_fn の引数に入る。model_fn の引数にはモデルファイルが置いてあるディレクトリが入る。
  • input_fn
    前処理を行うメソッド。リクエストの生データと、ContentType が引数に入るので、ContentType に応じた処理を記述できる。前処理済みのデータを返す。
  • predict_fn
    推論するメソッド。input_fn で処理したデータと model_fn で読み込んだモデルが引数に入る。推論結果を返す。
  • output_fn
    後処理を行うメソッド。predict_fn の返り値と、Accept が引数に入るので、Accept に応じた処理を書ける。後処理後のデータを返す。

推論コード

%%writefile src/inference.py
# 標準モジュール
import os
from glob import glob
import json
import shutil
import logging
from uuid import uuid4
import sys
# 3rd モジュール
import torch
# 自作モジュール
import classes.ModelA.eval_ModelA as eval_ModelA
import classes.ModelB.eval_ModelB as eval_ModelB
from pdf2png import pdf2png
from png2feat import png2feat

logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
logger.addHandler(logging.StreamHandler(sys.stdout))

# モデルをロードする関数
# エンドポイント立ち上げ時に一度だけ呼ばれる
def model_fn(model_dir):
    logger.info('loading ModelA...')
    ModelA = eval_ModelA.load_model(compute='gpu' if torch.cuda.is_available() else 'cpu')
    logger.info('loading PCR...')
    ModelB = eval_ModelB.load_model()
    logger.info('completed model_fn')
    return {'ModelA':ModelA, 'ModelB':ModelB}

# データの前処理を行う関数
# エンドポイントにリクエストされた時に最初呼ばれる
# input_fn が存在しない場合は default_input_fn が呼ばれる
def input_fn(input_data, content_type):
    if content_type == 'application/pdf':
        name = str(uuid4())
        logger.debug(f'{name=}')
        pdf_path = f'/tmp/{name}.pdf'
        logger.debug(f'{pdf_path=}')
        png_dir = f'/tmp/{name}/'
        logger.debug(f'{png_dir=}')
        with open(pdf_path, 'wb') as f:
            f.write(input_data)
        os.makedirs(png_dir)
        logger.info('pdf2png start...')
        is_success = pdf2png(pdf_path, png_dir)
        logger.info('pdf2png end')
        pngs = glob(os.path.join(png_dir,'*.png'))
        logger.debug(f'{pngs=}')
        os.remove(pdf_path)
        return pngs
    else:
        raise TypeError('ContentType is only allowed application/pdf.')

# 推論する関数
def predict_fn(pngs, model):
    logger.info('png2feat start...')
    feat = png2feat(model['ModelA'], model['ModelB'], pngs)
    logger.info('png2feat end')
    png_dir = pngs[0].replace(pngs[0].split('/')[-1],'')
    logger.info('delete {png_dir}')
    shutil.rmtree(png_dir)
    return feat

# 推論結果の後処理を行う関数
# output_fn が存在しない場合は default_output_fn が呼ばれる
def output_fn(feat, accept):
    if accept == 'application/json':
        response = json.dumps({'result_array':feat.tolist()})
    elif accept == 'text/csv':
        response = ','.join([str(element) for element in feat.tolist()])
    else:
        raise TypeError('Accept is only allowed application/json or text/csv')
    return response

5.3. モデルのデプロイ&エンドポイントの作成

一式そろえた model.tar.gz を S3 にアップロードし、モデルをデプロイします。
リソース周りはエンドポイントのconfigを設定します。
推論コード、モデルmodel.tar.gz に固める

with tarfile.open('model.tar.gz', mode='w:gz') as tar:
    tar.add('src/','code/') # コードを追加
    tar.add('./wait.ckpt')
    tar.add('./ModelB_model_320x240_4096')

# model.tar.gz check
with tarfile.open('./model.tar.gz','r:gz') as tar:
    tar.list()

モデルのs3アップロード

source_s3_uri = sagemaker.session.Session().upload_data(
    f'./model.tar.gz',
    key_prefix = 'techbot-gpu'
)
print(source_s3_uri)

モデルのデプロイ

base_name = 'techbot-gpu'
model_name = f'{base_name}-model'
sm_client.create_model(
    ModelName=model_name,
    PrimaryContainer={
        'Image': image_uri,
        'Mode': 'SingleModel',
        'ModelDataUrl': source_s3_uri,
        'Environment':{
            'SAGEMAKER_CONTAINER_LOG_LEVEL': '20',
            'SAGEMAKER_PROGRAM': 'inference.py',
            'SAGEMAKER_REGION': 'region',
            'SAGEMAKER_SUBMIT_DIRECTORY': '/opt/ml/model/code'
        }
    },
    ExecutionRoleArn=role
)

エンドポイントの設定作成

endpoint_config_name = f'{base_name}-endpoint-config'
sm_client.create_endpoint_config(
    EndpointConfigName=endpoint_config_name,
    ProductionVariants=[{
        'VariantName': 'AllTraffic',
        'ModelName': model_name,
        'InitialInstanceCount': 1,
        'InstanceType': 'ml.g5.xlarge',
        'InitialVariantWeight': 1.0
    }]
)

エンドポイント作成

endpoint_name = f'{base_name}-endpoint'
sm_client.create_endpoint(
    EndpointName=endpoint_name,
    EndpointConfigName = endpoint_config_name
)

エンドポイントが受付開始するまで待機

endpoint_inservice_waiter.wait(
    EndpointName=endpoint_name,
    WaiterConfig={'Delay': 5,}
)

5.4. 推論

いよいよ推論です。

推論

%%time
response = smr_client.invoke_endpoint(
    EndpointName=endpoint_name,
    ContentType='application/pdf',
    Accept='application/json',
    Body=content
)
result = json.loads(response['Body'].read().decode('utf-8'))
pprint(result)

5.5. 削除

エンドポイント、エンドポイントコンフィグ、モデルを削除します。
削除も数行で完了します。
作成したリソース諸々削除

sm_client.delete_endpoint(EndpointName=endpoint_name)
sm_client.delete_endpoint_config(EndpointConfigName=endpoint_config_name)
sm_client.delete_model(ModelName=model_name)

6. まとめ

推論エンドポイントが想定より簡単に作成できました。今回はリアルタイム推論のエンドポイントを作成しましたが、設定部分を少し変更するだけで非同期推論やバッチ推論も作成可能です。
また、今回はモデル作成部分は colab を使っていたのですが、ここもマルっと SageMaker で完結できそう(管理も楽そう!!)ですので、積極的に使っていきたいと思います。

以上「Applibot Advent Calendar 2022」と「AI/ML on AWS Advent Calendar 2022」の17日目の記事でした!

参考資料

AWSさんの解説動画が大変参考になりましたmm これらをみると、より理解が深まると思います。


関連記事一覧